Skip to main content

structured_cades/material/
checks.rs

1//! Whole-source X.509 material requirements from the actual shared closure.
2
3use super::{Index, Placement, Pool};
4use crate::{
5    Cades, Error,
6    prepare::Preparation,
7    source::{DocumentId, Node},
8};
9use structured_asn1::memory::{OwnedVec, Work};
10use structured_ltv::{
11    baseline::X509Material,
12    material::{self, ArtifactRef, Incorporation, IncorporationError, IndexError},
13    pki::{self, ParticipantClosure, ParticipantRequirements, UsesError},
14    source::Query,
15    timestamp::TimestampScope,
16};
17
18/// One real artifact whose required original incorporation was not established.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Finding {
21    /// No exact original DER copy is incorporated in root or timestamp sources.
22    Missing {
23        /// Actual artifact-census ordinal, not a document byte offset.
24        artifact: usize,
25    },
26    /// An actual copy exists but not in its required certificate/revocation set.
27    Placement {
28        /// Actual artifact-census ordinal.
29        artifact: usize,
30    },
31    /// A known original validation value violates its selected syntax.
32    Malformed {
33        /// Original material enclosure.
34        node: Node,
35    },
36}
37
38/// Complete whole-signature X.509 presence and placement facts. Actual actor,
39/// member/status and authority requirements come from the SAME LTV closure;
40/// certificate-free credentials do not waive other format-specific material.
41pub struct Checks<'closure, 'requirements, 'view, 'certificate, 'document, 'model> {
42    inputs: &'closure ParticipantClosure<
43        'requirements,
44        'view,
45        'certificate,
46        'document,
47        Node,
48        DocumentId<'model, 'document>,
49        (),
50        Cades,
51    >,
52    presence:
53        Incorporation<'certificate, 'document, Node, DocumentId<'model, 'document>, (), Placement>,
54    findings: OwnedVec<Finding>,
55}
56impl<'d, 'm> Checks<'_, '_, '_, '_, 'd, 'm> {
57    /// Every actual required certificate/complete+delta CRL/OCSP artifact.
58    #[must_use]
59    pub fn presence(&self) -> &Incorporation<'_, 'd, Node, DocumentId<'m, 'd>, (), Placement> {
60        &self.presence
61    }
62    /// Missing/misplaced/known-malformed facts, independently of status usability.
63    #[must_use]
64    pub fn findings(&self) -> &[Finding] {
65        &self.findings
66    }
67    /// Presence and mandatory container placement only; not Good status,
68    /// historical trust, complete credential material or attained B-LT.
69    #[must_use]
70    pub fn all_x509_requirements_met(&self) -> bool {
71        self.findings.is_empty() && self.presence.all_present()
72    }
73}
74impl<'v, 'd, 'm> X509Material<'v, 'd> for Checks<'_, '_, 'v, '_, 'd, 'm> {
75    type Id = Node;
76    type Document = DocumentId<'m, 'd>;
77    type Generation = ();
78    type Issuer = Cades;
79    fn requirements(
80        &self,
81    ) -> &ParticipantRequirements<'v, 'd, Node, DocumentId<'m, 'd>, (), Cades> {
82        self.inputs.requirements()
83    }
84    fn all_x509_requirements_met(&self) -> bool {
85        Self::all_x509_requirements_met(self)
86    }
87}
88
89/// Actual source/census/index refusal; no successful partial material report.
90#[derive(Debug)]
91pub enum CheckError {
92    /// Complete whole-signature source/selection preflight refused.
93    Processing(Error),
94    /// Original actual actor/status-authority graph attribution refused.
95    Uses(UsesError<Node>),
96    /// Original complete incorporation lookup refused.
97    Presence(IncorporationError<Node>),
98    /// Exact original indexed-occurrence lookup refused.
99    Index(IndexError<Node>),
100}
101
102/// Check ALL actual validation artifacts using the existing LTV role closure,
103/// exact-artifact index and incorporation kernel. Root/counter/credential/TSA
104/// and reached status-authority artifacts cannot be replaced with an arbitrary
105/// nonempty bag. B-LT requires certificates in the root certificate set
106/// (§6.3(d)); archive augmentation may reuse existing timestamp material
107/// (§5.5.3 Note5). CRLs and whole OCSP values use their actual revocation choices
108/// (§§5.4.2/6.3(q,r)). No parser, math, path or status evaluation is replayed.
109/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf>
110pub fn check_participants<'cl, 'r, 'v, 'c, 'd, 'm, Q, W>(
111    prepared: &Preparation<'m, 'd>,
112    pool: &Pool<'_, 'm, 'd>,
113    inputs: &'cl ParticipantClosure<'r, 'v, 'c, 'd, Node, DocumentId<'m, 'd>, (), Cades>,
114    index: &Index<'_, 'm, 'd, W>,
115    query: &Q,
116) -> Result<Checks<'cl, 'r, 'v, 'c, 'd, 'm>, CheckError>
117where
118    Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
119    W: Work,
120{
121    let selected = prepared.document().locate(prepared.signer().span());
122    query
123        .admit(selected.node, 1)
124        .map_err(|cause| CheckError::Index(IndexError::Scope(cause)))?;
125    if !core::ptr::eq(pool.prepared(), prepared)
126        || inputs.requirements().signature() != selected.node
127        || query.snapshot() != selected.snapshot
128        || !matches!(inputs.requirements().scope(), TimestampScope::All(_))
129    {
130        return Err(CheckError::Processing(Error::Foreign {
131            at: selected.node.span().start,
132        }));
133    }
134    index
135        .require_signature(selected.node, query)
136        .map_err(CheckError::Index)?;
137    let uses = pki::authority_uses(inputs, query).map_err(CheckError::Uses)?;
138    let artifacts = material::artifact_uses(&uses, query).map_err(CheckError::Uses)?;
139    let presence =
140        material::incorporation(&artifacts, index, query).map_err(CheckError::Presence)?;
141    let mut findings = query.execution().vector();
142    let archive_augmentation = prepared.requested() == crate::Level::Archival;
143    let resource = |cause| {
144        CheckError::Processing(Error::resource(
145            cause,
146            "cades.material",
147            selected.node.span().start,
148        ))
149    };
150    for (ordinal, entry) in artifacts.entries().iter().enumerate() {
151        query.execution().charge_work(1).map_err(resource)?;
152        let artifact = entry.artifact();
153        let mut copies = index.artifact(artifact, query).map_err(CheckError::Index)?;
154        let mut present = false;
155        let mut allowed = false;
156        while let Some(copy) = copies.next().transpose().map_err(CheckError::Index)? {
157            present = true;
158            allowed |= match artifact {
159                ArtifactRef::Certificate(_) => {
160                    // EN319122-1 §6.3(d) names SignedData.certificates for
161                    // B-LT. §5.5.3 Note5's reuse of token validation data is
162                    // specific to archive augmentation, not a B-LT waiver.
163                    matches!(copy.placement(), Placement::RootCertificate(_))
164                        || (archive_augmentation
165                            && (matches!(copy.placement(), Placement::TokenCertificate { .. })
166                                || matches!(copy.placement(), Placement::CertificateValues(location)
167                                    if archived_values(prepared, location))
168                                || (copy.embedded_certificate().is_some()
169                                    && embedded_placement(prepared, copy.placement()))))
170                }
171                ArtifactRef::Crl(_) | ArtifactRef::Ocsp(_) => {
172                    matches!(
173                        copy.placement(),
174                        Placement::RootRevocation(_) | Placement::TokenRevocation { .. }
175                    ) || (archive_augmentation
176                        && matches!(copy.placement(), Placement::CrlValues(location)
177                            | Placement::OcspValues(location) if archived_values(prepared, location)))
178                }
179            };
180        }
181        if !present {
182            findings
183                .try_push(Finding::Missing { artifact: ordinal })
184                .map_err(resource)?;
185        } else if !allowed {
186            findings
187                .try_push(Finding::Placement { artifact: ordinal })
188                .map_err(resource)?;
189        }
190    }
191    for entry in pool.open() {
192        query.execution().charge_work(1).map_err(resource)?;
193        if entry.cause.is_some() {
194            findings
195                .try_push(Finding::Malformed { node: entry.node })
196                .map_err(resource)?;
197        }
198    }
199    super::legacy::malformed(
200        pool,
201        |node| {
202            query
203                .admit(node, 1)
204                .map_err(|cause| CheckError::Index(IndexError::Scope(cause)))
205        },
206        |node, fault| {
207            query.execution().charge_work(24).map_err(resource)?;
208            if fault.attribute == crate::properties::legacy::LEGACY[2]
209                || fault.attribute == crate::properties::legacy::LEGACY[3]
210            {
211                findings
212                    .try_push(Finding::Malformed { node })
213                    .map_err(resource)?;
214            }
215            Ok(())
216        },
217    )?;
218    Ok(Checks {
219        inputs,
220        presence,
221        findings: findings.into_owned(),
222    })
223}
224
225fn archived_values(prepared: &Preparation<'_, '_>, location: super::LegacyLocation) -> bool {
226    // These are immutable, format-produced placement facts. This branch does
227    // NOT waive §6.3(d)'s root certificate-set rule for ordinary B-LT, nor
228    // admit signed legacy values. §5.5.3 permits reusing archive-token material.
229    if !location.unsigned {
230        return false;
231    }
232    let Some(token) = location.token else {
233        return false;
234    };
235    prepared
236        .timestamps()
237        .get(token)
238        .is_some_and(|entry| entry.purpose() == structured_ltv::timestamp::Purpose::Archive)
239}
240
241fn embedded_placement(prepared: &Preparation<'_, '_>, placement: Placement) -> bool {
242    match placement {
243        Placement::CertificateValues(location)
244        | Placement::CrlValues(location)
245        | Placement::OcspValues(location) => archived_values(prepared, location),
246        _ => true,
247    }
248}