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    check_in(prepared, pool, inputs, index, None, query)
122}
123
124/// Same full material/actor closure with sealed historical `PoE` preservation.
125/// EN319122-1 §5.5.3 permits archive augmentation material in a pre-existing
126/// long-term-validation attribute. Its actual source/proof must be closed;
127/// ordinary B-LT root-set requirements are retained.
128pub fn check_participants_preserved<'cl, 'r, 'v, 'c, 'd, 'm, Q, W>(
129    prepared: &Preparation<'m, 'd>,
130    pool: &Pool<'_, 'm, 'd>,
131    inputs: &'cl ParticipantClosure<'r, 'v, 'c, 'd, Node, DocumentId<'m, 'd>, (), Cades>,
132    index: &Index<'_, 'm, 'd, W>,
133    preservation: &crate::evidence::Closure<'_, '_, 'm, 'd, '_>,
134    query: &Q,
135) -> Result<Checks<'cl, 'r, 'v, 'c, 'd, 'm>, CheckError>
136where
137    Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
138    W: Work,
139{
140    check_in(prepared, pool, inputs, index, Some(preservation), query)
141}
142fn check_in<'cl, 'r, 'v, 'c, 'd, 'm, Q, W>(
143    prepared: &Preparation<'m, 'd>,
144    pool: &Pool<'_, 'm, 'd>,
145    inputs: &'cl ParticipantClosure<'r, 'v, 'c, 'd, Node, DocumentId<'m, 'd>, (), Cades>,
146    index: &Index<'_, 'm, 'd, W>,
147    preservation: Option<&crate::evidence::Closure<'_, '_, 'm, 'd, '_>>,
148    query: &Q,
149) -> Result<Checks<'cl, 'r, 'v, 'c, 'd, 'm>, CheckError>
150where
151    Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
152    W: Work,
153{
154    if let Some(proofs) = preservation
155        && !core::ptr::eq(proofs.prepared(), prepared)
156    {
157        return Err(CheckError::Processing(Error::Foreign {
158            at: prepared.signer().span().start,
159        }));
160    }
161    let selected = prepared.document().locate(prepared.signer().span());
162    query
163        .admit(selected.node, 1)
164        .map_err(|cause| CheckError::Index(IndexError::Scope(cause)))?;
165    if !core::ptr::eq(pool.prepared(), prepared)
166        || inputs.requirements().signature() != selected.node
167        || query.snapshot() != selected.snapshot
168        || !matches!(inputs.requirements().scope(), TimestampScope::All(_))
169    {
170        return Err(CheckError::Processing(Error::Foreign {
171            at: selected.node.span().start,
172        }));
173    }
174    index
175        .require_signature(selected.node, query)
176        .map_err(CheckError::Index)?;
177    let uses = pki::authority_uses(inputs, query).map_err(CheckError::Uses)?;
178    let artifacts = material::artifact_uses(&uses, query).map_err(CheckError::Uses)?;
179    let presence =
180        material::incorporation(&artifacts, index, query).map_err(CheckError::Presence)?;
181    let mut findings = query.execution().vector();
182    let archive_augmentation = prepared.requested() == crate::Level::Archival;
183    let resource = |cause| {
184        CheckError::Processing(Error::resource(
185            cause,
186            "cades.material",
187            selected.node.span().start,
188        ))
189    };
190    for (ordinal, entry) in artifacts.entries().iter().enumerate() {
191        query.execution().charge_work(1).map_err(resource)?;
192        let artifact = entry.artifact();
193        let mut copies = index.artifact(artifact, query).map_err(CheckError::Index)?;
194        let mut present = false;
195        let mut allowed = false;
196        while let Some(copy) = copies.next().transpose().map_err(CheckError::Index)? {
197            present = true;
198            let poe = match copy.placement() {
199                Placement::PoeCertificate(location) | Placement::PoeRevocation(location)
200                    if archive_augmentation =>
201                {
202                    preserved_extra(prepared, preservation, location, query)?
203                }
204                _ => false,
205            };
206            allowed |= match artifact {
207                ArtifactRef::Certificate(_) => {
208                    // EN319122-1 §6.3(d) names SignedData.certificates for
209                    // B-LT. §5.5.3 Note5's reuse of token validation data is
210                    // specific to archive augmentation, not a B-LT waiver.
211                    matches!(copy.placement(), Placement::RootCertificate(_))
212                        || poe
213                        || (archive_augmentation
214                            && (matches!(copy.placement(), Placement::TokenCertificate { .. })
215                                || matches!(copy.placement(), Placement::CertificateValues(location)
216                                    if archived_values(prepared, location))
217                                || (copy.embedded_certificate().is_some()
218                                    && embedded_placement(prepared, copy.placement()))))
219                }
220                ArtifactRef::Crl(_) | ArtifactRef::Ocsp(_) => {
221                    matches!(
222                        copy.placement(),
223                        Placement::RootRevocation(_) | Placement::TokenRevocation { .. }
224                    ) || poe
225                        || (archive_augmentation
226                            && matches!(copy.placement(), Placement::CrlValues(location)
227                            | Placement::OcspValues(location) if archived_values(prepared, location)))
228                }
229            };
230        }
231        if !present {
232            findings
233                .try_push(Finding::Missing { artifact: ordinal })
234                .map_err(resource)?;
235        } else if !allowed {
236            findings
237                .try_push(Finding::Placement { artifact: ordinal })
238                .map_err(resource)?;
239        }
240    }
241    retain_malformed(pool, query, selected.node, &mut findings)?;
242    Ok(Checks {
243        inputs,
244        presence,
245        findings: findings.into_owned(),
246    })
247}
248
249fn retain_malformed<'m, 'd, Q, W: Work>(
250    pool: &Pool<'_, 'm, 'd>,
251    query: &Q,
252    selected: Node,
253    findings: &mut OwnedVec<Finding, W>,
254) -> Result<(), CheckError>
255where
256    Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
257{
258    let resource = |cause| {
259        CheckError::Processing(Error::resource(
260            cause,
261            "cades.material",
262            selected.span().start,
263        ))
264    };
265    for entry in pool.open() {
266        query.execution().charge_work(1).map_err(resource)?;
267        if entry.cause.is_some() {
268            findings
269                .try_push(Finding::Malformed { node: entry.node })
270                .map_err(resource)?;
271        }
272    }
273    super::legacy::malformed(
274        pool,
275        |node| {
276            query
277                .admit(node, 1)
278                .map_err(|cause| CheckError::Index(IndexError::Scope(cause)))
279        },
280        |node, fault| {
281            query.execution().charge_work(24).map_err(resource)?;
282            if fault.attribute == crate::properties::legacy::LEGACY[2]
283                || fault.attribute == crate::properties::legacy::LEGACY[3]
284            {
285                findings
286                    .try_push(Finding::Malformed { node })
287                    .map_err(resource)?;
288            }
289            Ok(())
290        },
291    )?;
292    Ok(())
293}
294
295fn preserved_extra<'m, 'd, Q>(
296    prepared: &Preparation<'m, 'd>,
297    preservation: Option<&crate::evidence::Closure<'_, '_, 'm, 'd, '_>>,
298    location: super::PoeLocation,
299    query: &Q,
300) -> Result<bool, CheckError>
301where
302    Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
303{
304    let resource = |cause| {
305        CheckError::Processing(Error::resource(
306            cause,
307            "cades.material",
308            location.property.span().start,
309        ))
310    };
311    query.execution().charge_work(1).map_err(resource)?;
312    let Some(proofs) = preservation else {
313        return Ok(false);
314    };
315    let entry = prepared
316        .parsed
317        .proofs
318        .entries
319        .get(location.proof)
320        .ok_or_else(|| {
321            CheckError::Processing(Error::Foreign {
322                at: location.property.span().start,
323            })
324        })?;
325    if entry.required.signature != location.signature
326        || entry.required.property != location.property
327    {
328        return Err(CheckError::Processing(Error::Foreign {
329            at: location.property.span().start,
330        }));
331    }
332    proofs
333        .contains(&entry.required, query.execution())
334        .map_err(resource)
335}
336
337fn archived_values(prepared: &Preparation<'_, '_>, location: super::LegacyLocation) -> bool {
338    // These are immutable, format-produced placement facts. This branch does
339    // NOT waive §6.3(d)'s root certificate-set rule for ordinary B-LT, nor
340    // admit signed legacy values. §5.5.3 permits reusing archive-token material.
341    if !location.unsigned {
342        return false;
343    }
344    let Some(token) = location.token else {
345        return false;
346    };
347    prepared
348        .timestamps()
349        .get(token)
350        .is_some_and(|entry| entry.purpose() == structured_ltv::timestamp::Purpose::Archive)
351}
352
353fn embedded_placement(prepared: &Preparation<'_, '_>, placement: Placement) -> bool {
354    match placement {
355        Placement::CertificateValues(location)
356        | Placement::CrlValues(location)
357        | Placement::OcspValues(location) => archived_values(prepared, location),
358        _ => true,
359    }
360}