Skip to main content

structured_xades/material/
conditional.rs

1//! Mandatory copies in a present signature's certificate/status containers.
2
3use super::catalogue::{ContainerKind, container_kind};
4use super::{Index, Placement};
5use crate::Xades;
6use crate::pki::CounterPaths;
7use crate::prepare::Prepared;
8use crate::xml::{Access, Snapshot, Xml};
9use structured_asn1::memory::{HashWork, MemoryError, OwnedMap, OwnedVec, Work};
10use structured_ltv::material::status::{
11    ArtifactVisitError, StatusPart, member_source, visit_artifacts,
12};
13use structured_ltv::material::{ArtifactRef, IndexError};
14use structured_ltv::pki::{
15    Participant, ParticipantAuthority, ParticipantClosure, ParticipantRequirements, PathStatuses,
16};
17use structured_ltv::source::ScopeError;
18use structured_ltv::timestamp::TimestampScope;
19
20/// Why a mandatory copy in a present container could not be established.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum ConditionalFailure {
23    /// No exact copy is incorporated anywhere in the indexed signature.
24    Missing,
25    /// Exact copies exist, but none satisfies this container's mandatory rule.
26    Elsewhere,
27}
28
29/// One unsatisfied conditional requirement, with the actual actor and artifact.
30pub struct ConditionalFinding<'certificate, 'document, N> {
31    /// Original property whose presence activates this requirement.
32    pub property: N,
33    /// Actual participant; a credential's signing authority is not the root signer.
34    pub participant: Participant<N>,
35    /// Exact certificate or contributing complete/delta/OCSP artifact.
36    pub artifact: ArtifactRef<'certificate, 'document>,
37    /// Absence and an ineligible copy are distinct from processing refusal.
38    pub cause: ConditionalFailure,
39}
40
41/// Container-specific obligations of the root and ALL actual counters. This supplements
42/// the whole participant incorporation, exclusivity and TSA-placement reports.
43/// It does not assert a child's baseline, format-specific credential material,
44/// usable status, chronology, trust or an attained level.
45pub struct ConditionalPlacement<'certificate, 'document, N, D, G> {
46    snapshot: Snapshot<D, G>,
47    signature: N,
48    findings: OwnedVec<ConditionalFinding<'certificate, 'document, N>>,
49}
50
51impl<'certificate, 'document, N: Copy, D: Copy, G: Copy>
52    ConditionalPlacement<'certificate, 'document, N, D, G>
53{
54    /// Exact source generation under which the obligations were derived.
55    #[must_use]
56    pub fn snapshot(&self) -> Snapshot<D, G> {
57        self.snapshot
58    }
59    /// Original signature owning the checked properties.
60    #[must_use]
61    pub fn signature(&self) -> N {
62        self.signature
63    }
64    /// Missing/misplaced copies in property, then original actor/artifact order.
65    #[must_use]
66    pub fn findings(&self) -> &[ConditionalFinding<'certificate, 'document, N>] {
67        &self.findings
68    }
69}
70
71/// Scope/source/current-operation refusal; none is a missing-material fact.
72#[derive(Debug)]
73pub enum ConditionalError<E, N> {
74    /// An archive antecedent subset cannot establish whole-signature conditions.
75    Scope,
76    /// Original signature, source, lookup or storage/work refusal.
77    Index(IndexError<N>),
78    /// Original child unsigned/material container structure or access refusal.
79    Property(crate::properties::Error<E, N>),
80}
81
82/// Result in the original XML identity/error domain.
83pub type ConditionalResult<'certificate, 'document, X> = Result<
84    ConditionalPlacement<
85        'certificate,
86        'document,
87        <X as Xml>::Id,
88        <X as Xml>::Document,
89        <X as Xml>::Generation,
90    >,
91    ConditionalError<<X as Xml>::Error, <X as Xml>::Id>,
92>;
93
94// A direct copy in this property is mandatory. Only ordinary signature
95// CertificateValues/RevocationValues permit the owning KeyInfo alternative.
96#[derive(Clone, Copy)]
97enum CopyRule<N> {
98    Property(N),
99    PropertyOrKeyInfo { property: N, signature: N },
100}
101
102fn eligible<N: Copy, E>(
103    rule: CopyRule<N>,
104    placement: Placement<N>,
105    embedded: bool,
106    mut equal: impl FnMut(N, N) -> Result<bool, E>,
107) -> Result<bool, E> {
108    if embedded {
109        return Ok(false);
110    }
111    match (rule, placement) {
112        (CopyRule::Property(property), Placement::Attribute(actual))
113        | (CopyRule::PropertyOrKeyInfo { property, .. }, Placement::Signature(actual)) => {
114            equal(property, actual)
115        }
116        (CopyRule::PropertyOrKeyInfo { signature, .. }, Placement::KeyInfo(actual)) => {
117            equal(signature, actual)
118        }
119        _ => Ok(false),
120    }
121}
122
123/// Check mandatory copies in root and counter `CertificateValues`,
124/// `RevocationValues`, `AttrAuthoritiesCertValues` and `AttributeRevocationValues`.
125/// EN319132-1 §§5.4.2(1–3)/5.4.3(1–2) allow the owning `KeyInfo` alternative;
126/// §§5.4.4(1)/5.4.5(1) require the actual credential SIGNER's certificate/status
127/// in its attribute container even if another copy exists elsewhere.
128/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
129///
130/// Absent optional containers do not acquire an invented presence requirement:
131/// §6.3(p/r/t/w/cc) permits the other incorporation service. Remaining path and
132/// status-authority material uses the whole incorporation report; the more
133/// specific TSA/exclusive-CA restrictions are separate. Duplication SHOULDs are
134/// not upgraded into prohibitions. Counter material may be carried here but
135/// does not replace the selected root's own obligations. Child sources come from
136/// the COMPLETE actual counter-path receipt, including plain `XMLDSig` leaves;
137/// their original properties reuse the retention role kernel without imposing
138/// the root's B-level inventory/header rules. No parse/provider or
139/// status evaluation is repeated; every original lookup and report allocation
140/// is admitted before work. A late refusal drops the complete partial report.
141///
142/// # Panics
143/// Only if a private construction invariant is violated: accepted checked paths
144/// are nonempty, and their complete status roots follow Path requirements.
145pub fn conditional_placement<'certificate, 'document, X: Xml, W: Work, Q: Work>(
146    prepared: &Prepared<X>,
147    inputs: &Inputs<'_, '_, 'certificate, 'document, X>,
148    counters: &CounterPaths<'_, 'document, X::Id, X::Document, X::Generation>,
149    index: &Index<'_, X::Id, X::Document, X::Generation, W>,
150    access: &Access<'_, X, Q>,
151) -> ConditionalResult<'certificate, 'document, X> {
152    let required = inputs.requirements();
153    if !matches!(required.scope(), TimestampScope::All(_)) {
154        return Err(ConditionalError::Scope);
155    }
156    let signature = prepared.bound().containers().signature();
157    let resource = |cause| ConditionalError::Index(IndexError::Resource { signature, cause });
158    if prepared.bound().containers().snapshot() != access.snapshot()
159        || required.scope().location().snapshot != access.snapshot()
160        || counters.snapshot() != access.snapshot()
161        || !access
162            .equal_id(required.signature(), signature)
163            .map_err(resource)?
164        || !access
165            .equal_id(counters.signature(), signature)
166            .map_err(resource)?
167    {
168        return Err(ConditionalError::Index(IndexError::Foreign { signature }));
169    }
170    index
171        .require_signature(signature, access)
172        .map_err(ConditionalError::Index)?;
173    let owners = Owners::build(required, access.execution()).map_err(resource)?;
174    owners.require_counters(inputs, counters, signature, access)?;
175    let mut findings = access.execution().vector();
176    let mut record = |owner, property, kind| {
177        check_property(
178            inputs,
179            &owners,
180            (owner, property, kind),
181            index,
182            access,
183            &mut findings,
184        )
185    };
186    for entry in prepared.unsigned().entries() {
187        access.execution().charge_work(1).map_err(resource)?;
188        if let Some(kind) = ContainerKind::inventory(entry.role) {
189            record(signature, entry.node, kind)?;
190        }
191    }
192    for actor in counters.actors() {
193        access.execution().charge_work(1).map_err(resource)?;
194        let Some(layout) = actor.containers() else {
195            continue;
196        };
197        let [parent, _] = crate::unsigned::containers(access, layout.unsigned())
198            .map_err(ConditionalError::Property)?;
199        let Some(parent) = parent else {
200            continue;
201        };
202        for child in access.children(parent) {
203            let node = child.map_err(|cause| {
204                ConditionalError::Property(crate::properties::Error::Child(cause))
205            })?;
206            if let Some(kind) = container_kind(access, node).map_err(ConditionalError::Property)? {
207                record(layout.signature(), node, kind)?;
208            }
209        }
210    }
211    index
212        .require_signature(signature, access)
213        .map_err(ConditionalError::Index)?;
214    Ok(ConditionalPlacement {
215        snapshot: access.snapshot(),
216        signature,
217        findings: findings.into_owned(),
218    })
219}
220
221type Inputs<'requirements, 'view, 'certificate, 'document, X> = ParticipantClosure<
222    'requirements,
223    'view,
224    'certificate,
225    'document,
226    <X as Xml>::Id,
227    <X as Xml>::Document,
228    <X as Xml>::Generation,
229    Xades,
230>;
231
232struct ActorLink {
233    requirement: usize,
234    root: Option<usize>,
235    next: Option<usize>,
236}
237
238// One current-operation index avoids scanning every unrelated actor for every
239// counter's properties. Links borrow neither temporary XML views nor a parser.
240struct Owners<N: HashWork, W: Work> {
241    heads: OwnedMap<N, (usize, usize), W>,
242    links: OwnedVec<ActorLink, W>,
243    counters: usize,
244}
245
246impl<N: Copy + Eq + HashWork, W: Work> Owners<N, W> {
247    fn build<D: Copy, G: Copy>(
248        required: &ParticipantRequirements<'_, '_, N, D, G, Xades>,
249        work: &W,
250    ) -> Result<Self, MemoryError> {
251        let mut result = Self {
252            heads: work.map(),
253            links: work.vector(),
254            counters: 0,
255        };
256        let mut root = 0;
257        for (requirement, entry) in required.entries().iter().enumerate() {
258            work.charge_work(1)?;
259            let path = if matches!(entry.authority(), ParticipantAuthority::Path(_)) {
260                let index = root;
261                root += 1;
262                Some(index)
263            } else {
264                None
265            };
266            let owner = match entry.participant() {
267                Participant::Signer(owner)
268                | Participant::Credential {
269                    signature: owner, ..
270                } => owner,
271                Participant::Counter {
272                    signature: owner, ..
273                } => {
274                    result.counters += 1;
275                    owner
276                }
277                Participant::Timestamp { .. } => continue,
278            };
279            let previous = result.heads.get(&owner, work)?.copied();
280            let ordinal = result.links.len();
281            result.links.try_push(ActorLink {
282                requirement,
283                root: path,
284                next: None,
285            })?;
286            let head = if let Some((head, tail)) = previous {
287                result.links[tail].next = Some(ordinal);
288                head
289            } else {
290                ordinal
291            };
292            result.heads.try_insert(owner, (head, ordinal))?;
293        }
294        Ok(result)
295    }
296
297    fn require_counters<X: Xml<Id = N>, Q: Work>(
298        &self,
299        inputs: &Inputs<'_, '_, '_, '_, X>,
300        counters: &CounterPaths<'_, '_, N, X::Document, X::Generation>,
301        signature: N,
302        access: &Access<'_, X, Q>,
303    ) -> Result<(), ConditionalError<X::Error, N>> {
304        let foreign = || ConditionalError::Index(IndexError::Foreign { signature });
305        let resource = |cause| ConditionalError::Index(IndexError::Resource { signature, cause });
306        if self.counters != counters.actors().len() {
307            return Err(foreign());
308        }
309        for actor in counters.actors() {
310            access.execution().charge_work(1).map_err(resource)?;
311            let original = actor.signer().counter().original();
312            let mut next = self
313                .heads
314                .get(&original.child(), access.execution())
315                .map_err(resource)?
316                .map(|&(head, _)| head);
317            let mut found = false;
318            while let Some(ordinal) = next {
319                access.execution().charge_work(1).map_err(resource)?;
320                let link = &self.links[ordinal];
321                next = link.next;
322                let entry = &inputs.requirements().entries()[link.requirement];
323                if let Participant::Counter { property, .. } = entry.participant() {
324                    let ParticipantAuthority::Path(path) = entry.authority() else {
325                        return Err(foreign());
326                    };
327                    if !core::ptr::eq(path, actor.signer().path())
328                        || !access
329                            .equal_id(property, original.node())
330                            .map_err(resource)?
331                    {
332                        return Err(foreign());
333                    }
334                    found = true;
335                    break;
336                }
337            }
338            if !found {
339                return Err(foreign());
340            }
341        }
342        Ok(())
343    }
344}
345
346fn check_property<'certificate, 'document, X: Xml, W: Work, Q: Work>(
347    inputs: &Inputs<'_, '_, 'certificate, 'document, X>,
348    owners: &Owners<X::Id, Q>,
349    (owner, property, kind): (X::Id, X::Id, ContainerKind),
350    index: &Index<'_, X::Id, X::Document, X::Generation, W>,
351    access: &Access<'_, X, Q>,
352    findings: &mut OwnedVec<ConditionalFinding<'certificate, 'document, X::Id>, Q>,
353) -> Result<(), ConditionalError<X::Error, X::Id>> {
354    let signature = inputs.requirements().signature();
355    let resource = |cause| ConditionalError::Index(IndexError::Resource { signature, cause });
356    let mut record = |participant, artifact, rule| {
357        if let Some(cause) = availability(index, artifact, rule, signature, access)? {
358            findings
359                .try_push(ConditionalFinding {
360                    property,
361                    participant,
362                    artifact,
363                    cause,
364                })
365                .map_err(resource)?;
366        }
367        Ok(())
368    };
369    let mut next = owners
370        .heads
371        .get(&owner, access.execution())
372        .map_err(resource)?
373        .map(|&(head, _)| head);
374    while let Some(ordinal) = next {
375        access.execution().charge_work(1).map_err(resource)?;
376        let link = &owners.links[ordinal];
377        next = link.next;
378        let entry = &inputs.requirements().entries()[link.requirement];
379        let participant = entry.participant();
380        let ordinary = matches!(
381            participant,
382            Participant::Signer(_) | Participant::Counter { .. }
383        );
384        if ordinary == kind.attribute() {
385            continue;
386        }
387        match entry.authority() {
388            ParticipantAuthority::Path(_) => {
389                let statuses =
390                    inputs.authorities().roots()[link.root.expect("Path actor has a status root")];
391                let properties = if kind.certificates() {
392                    [Some(property), None]
393                } else {
394                    [None, Some(property)]
395                };
396                path_copies(
397                    statuses,
398                    participant,
399                    properties,
400                    owner,
401                    access,
402                    &mut record,
403                )?;
404            }
405            ParticipantAuthority::Anchor(anchor) => {
406                if kind.certificates()
407                    && let Some(certificate) = anchor.anchor().certificate()
408                {
409                    record(
410                        participant,
411                        ArtifactRef::Certificate(certificate),
412                        copy_rule(property, owner, ordinary),
413                    )?;
414                }
415            }
416            ParticipantAuthority::CertificateFree => {}
417        }
418    }
419    Ok(())
420}
421
422fn availability<X: Xml, W: Work, Q: Work>(
423    index: &Index<'_, X::Id, X::Document, X::Generation, W>,
424    artifact: ArtifactRef<'_, '_>,
425    rule: CopyRule<X::Id>,
426    signature: X::Id,
427    access: &Access<'_, X, Q>,
428) -> Result<Option<ConditionalFailure>, ConditionalError<X::Error, X::Id>> {
429    let resource = |cause| ConditionalError::Index(IndexError::Resource { signature, cause });
430    let mut present = false;
431    for occurrence in index
432        .artifact(artifact, access)
433        .map_err(ConditionalError::Index)?
434    {
435        let occurrence = occurrence.map_err(ConditionalError::Index)?;
436        present = true;
437        if eligible(
438            rule,
439            occurrence.placement(),
440            occurrence.embedded_certificate().is_some(),
441            |left, right| access.equal_id(left, right).map_err(resource),
442        )? {
443            return Ok(None);
444        }
445    }
446    Ok(Some(if present {
447        ConditionalFailure::Elsewhere
448    } else {
449        ConditionalFailure::Missing
450    }))
451}
452
453fn copy_rule<N: Copy>(property: N, signature: N, ordinary: bool) -> CopyRule<N> {
454    if ordinary {
455        CopyRule::PropertyOrKeyInfo {
456            property,
457            signature,
458        }
459    } else {
460        CopyRule::Property(property)
461    }
462}
463
464fn path_copies<'certificate, 'document, X: Xml, W: Work>(
465    statuses: &PathStatuses<'_, 'certificate, 'document>,
466    participant: Participant<X::Id>,
467    [certificate, status]: [Option<X::Id>; 2],
468    signature: X::Id,
469    access: &Access<'_, X, W>,
470    mut record: impl FnMut(
471        Participant<X::Id>,
472        ArtifactRef<'certificate, 'document>,
473        CopyRule<X::Id>,
474    ) -> Result<(), ConditionalError<X::Error, X::Id>>,
475) -> Result<(), ConditionalError<X::Error, X::Id>> {
476    let resource = |cause| ConditionalError::Index(IndexError::Resource { signature, cause });
477    let work = access.execution();
478    let path = statuses.path();
479    let ordinary = matches!(
480        participant,
481        Participant::Signer(_) | Participant::Counter { .. }
482    );
483    let rule = |property| copy_rule(property, signature, ordinary);
484    if let Some(property) = certificate {
485        if ordinary && let Some(anchor) = path.anchor().certificate() {
486            record(
487                participant,
488                ArtifactRef::Certificate(anchor),
489                rule(property),
490            )?;
491        }
492        let members = if ordinary {
493            path.members()
494        } else {
495            path.members()
496                .last_chunk::<1>()
497                .expect("checked path has a target")
498        };
499        for member in members {
500            work.charge_work(1).map_err(resource)?;
501            record(
502                participant,
503                ArtifactRef::Certificate(member.certificate()),
504                rule(property),
505            )?;
506        }
507    }
508    if let Some(property) = status {
509        let entries = if ordinary {
510            statuses.entries()
511        } else {
512            statuses
513                .entries()
514                .last_chunk::<1>()
515                .expect("complete target status input")
516        };
517        for input in entries {
518            work.charge_work(1).map_err(resource)?;
519            let Some(source) = member_source(input) else {
520                continue;
521            };
522            visit_artifacts(source, signature, access, |artifact, _, part| {
523                // The authentication signer is not the status target.
524                // Every complete/delta contributor is still required.
525                if part == StatusPart::SigningCertificate {
526                    return Ok(());
527                }
528                record(participant, artifact, rule(property))
529            })
530            .map_err(|cause| match cause {
531                ArtifactVisitError::Scope(ScopeError::Resource { cause, .. }) => resource(cause),
532                ArtifactVisitError::Scope(cause) => {
533                    ConditionalError::Index(IndexError::Scope(cause))
534                }
535                ArtifactVisitError::Visitor(cause) => cause,
536            })?;
537        }
538    }
539    Ok(())
540}