Skip to main content

structured_xades/certificate/
path.rs

1//! Original signed certificate digests against the actually validated path.
2
3use super::{
4    Agreement, Catalogue, CertificateXml, CheckedCertificate, DigestError, DigestProvider,
5    Expected, References,
6};
7use crate::binary::{self, BinaryXml, Location};
8use crate::xml::{Access, Xml};
9use structured_asn1::memory::{OwnedVec, Work};
10use structured_ltv::material::PathCertificate;
11use structured_ltv::pki::{CheckedPath, SignerReceipt};
12
13/// Completed digest result for one original signed certificate reference.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct PathDigest {
16    /// Original reference ordinal, preserving its first-signing-certificate role.
17    pub reference: usize,
18    /// Complete selected-provider outcome; unsupported/malformed is not mismatch.
19    pub agreement: Agreement,
20    /// Actual member matched by this digest, or the existing anchor certificate.
21    pub certificate: Option<PathCertificate>,
22}
23
24/// Complete digest processing against exactly this original checked path.
25/// This does not replace selected-key binding, optional hint syntax, scalar
26/// validation, revocation or policy acceptance with certificate hash agreement.
27pub struct PathDigests<'view, 'document, N, D, G> {
28    references: &'view References<N, D, G>,
29    path: &'view CheckedPath<'document>,
30    entries: OwnedVec<PathDigest>,
31    all_match: bool,
32}
33
34/// Path-digest result using the XML owner's original source/error identities.
35pub type PathDigestResult<'view, 'document, X, PE> = binary::BinaryResult<
36    PathDigests<'view, 'document, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
37    X,
38    DigestError<PE>,
39>;
40
41/// Selected original signing certificate and every original path digest.
42/// The first digest is computed once, then reused by the selected-key receipt;
43/// root composition need not call the standalone first-entry checker again.
44pub struct CheckedPathCertificate<'view, 'document, N, D, G> {
45    selected: CheckedCertificate<'view, 'document, N, D, G>,
46    digests: PathDigests<'view, 'document, N, D, G>,
47}
48
49impl<'view, 'document, N, D, G> CheckedPathCertificate<'view, 'document, N, D, G> {
50    /// Actual selected certificate/key/digest facts from this same invocation.
51    #[must_use]
52    pub fn selected(&self) -> &CheckedCertificate<'view, 'document, N, D, G> {
53        &self.selected
54    }
55    /// All declared original digest restrictions and the actual checked path.
56    #[must_use]
57    pub fn digests(&self) -> &PathDigests<'view, 'document, N, D, G> {
58        &self.digests
59    }
60    /// Selected key/certificate binding AND agreement of all path references.
61    /// Scalar/hint syntax, status, temporal/trust/policy facts remain separate.
62    #[must_use]
63    pub fn is_bound(&self) -> bool {
64        self.selected.is_bound() && self.digests.all_match()
65    }
66}
67
68// The signing-certificate binding is the common participant census's signer.
69impl<'view, 'document, N: Copy, D: Copy, G: Copy> SignerReceipt<'view, 'document>
70    for CheckedPathCertificate<'view, 'document, N, D, G>
71{
72    type Id = N;
73    type Document = D;
74    type Generation = G;
75
76    fn signature(&self) -> Location<N, D, G> {
77        let references = self.selected.reference();
78        Location {
79            snapshot: references.snapshot(),
80            node: references.signature(),
81        }
82    }
83
84    fn path(&self) -> Option<&'view CheckedPath<'document>> {
85        self.is_bound().then_some(self.digests.path)
86    }
87}
88
89/// Complete selected/path digest result with original source/provider causes.
90pub type PathCheckResult<'view, 'document, X, PE> = super::CertificateResult<
91    CheckedPathCertificate<
92        'view,
93        'document,
94        <X as Xml>::Id,
95        <X as Xml>::Document,
96        <X as Xml>::Generation,
97    >,
98    X,
99    PE,
100>;
101
102/// Bind the actual selected XML key and all signed certificate digests to the
103/// supplied common-validator path. Uses the same foreign-catalogue/Cert URI
104/// preflight and selected-key kernel as the standalone first-entry checker.
105/// No XML/path signature verification or first certificate hash is repeated.
106/// This still is not a complete baseline-level or application trust decision.
107///
108/// # Panics
109/// Only if the private nonempty checked-path/reference construction invariant
110/// is violated. Both public readers/validators reject empty inputs.
111pub fn check_path<
112    'view,
113    'document,
114    'p,
115    X: BinaryXml + CertificateXml,
116    P: DigestProvider<'p, X>,
117    W: Work,
118>(
119    xml: &X,
120    bound: &crate::binding::Bound<X::Id, X::Document, X::Generation>,
121    references: &'view Catalogue<X>,
122    path: &'view CheckedPath<'document>,
123    provider: &'p P,
124    execution: &W,
125) -> PathCheckResult<'view, 'document, X, P::Error> {
126    let certificate = path
127        .members()
128        .last()
129        .expect("a checked path has a target")
130        .certificate();
131    let key = super::bind_key::<X, P::Error, W>(
132        xml,
133        bound,
134        references,
135        certificate,
136        path.facts().key,
137        execution,
138    )?;
139    let digests = match_path(xml, references, path, provider, execution)
140        .map_err(super::CheckError::Digest)?;
141    let digest = digests
142        .entries
143        .first()
144        .expect("certificate references are nonempty")
145        .agreement;
146    Ok(CheckedPathCertificate {
147        selected: CheckedCertificate {
148            reference: references,
149            certificate,
150            key,
151            digest,
152        },
153        digests,
154    })
155}
156
157impl<'view, 'document, N, D, G> PathDigests<'view, 'document, N, D, G> {
158    /// Same authenticated signed-reference catalogue whose digests were read.
159    #[must_use]
160    pub fn references(&self) -> &'view References<N, D, G> {
161        self.references
162    }
163    /// Actual common-validator path used for every candidate.
164    #[must_use]
165    pub fn path(&self) -> &'view CheckedPath<'document> {
166        self.path
167    }
168    /// Each original reference, including unsupported/malformed/unmatched ones.
169    #[must_use]
170    pub fn entries(&self) -> &[PathDigest] {
171        &self.entries
172    }
173    /// Whether the first digest matches the target and all other digests match
174    /// a certificate in this path. This alone is not a baseline-level verdict.
175    #[must_use]
176    pub fn all_match(&self) -> bool {
177        self.all_match
178    }
179}
180
181/// Matches the original digest restrictions with one resolution/decode per
182/// reference, without re-encoding certificates or reparsing their paths.
183///
184/// EN319132-1 V1.3.1 ยง5.2.2 assigns the FIRST reference to the signing
185/// certificate and permits subsequent references to path/anchor certificates.
186/// A matching root digest cannot substitute for an incorrect first target.
187/// The optional `IssuerSerialV2` is only an identification hint (note2), not a
188/// substitute for digest agreement. Its DER syntax is a separate conformance
189/// check through the shared issuer-serial kernel.
190/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
191///
192/// All output storage is admitted before provider callbacks. Source/provider/
193/// resource failure returns no successful partial catalogue. Completed negative
194/// results are retained and do not stop inspection of subsequent references.
195///
196/// # Panics
197/// Only if the private checked-path invariant is violated: accepted paths are
198/// nonempty and their last issuer-to-subject member is the actual target.
199pub fn match_path<'view, 'document, 'p, X: BinaryXml, P: DigestProvider<'p, X>, W: Work>(
200    xml: &X,
201    references: &'view Catalogue<X>,
202    path: &'view CheckedPath<'document>,
203    provider: &'p P,
204    execution: &W,
205) -> PathDigestResult<'view, 'document, X, P::Error> {
206    let access = Access::new(xml, references.snapshot(), execution);
207    let location = Location {
208        snapshot: references.snapshot(),
209        node: references.node(),
210    };
211    access
212        .query(references.node(), |_, _, _| Ok(()))
213        .map_err(|cause| binary::Error::Access { location, cause })?;
214    let mut entries = execution.vector();
215    entries
216        .try_reserve_exact(references.entries().len())
217        .map_err(|cause| binary::Error::Callback {
218            location,
219            cause: DigestError::Resource(cause),
220        })?;
221    let mut all_match = true;
222    for ordinal in 0..references.entries().len() {
223        execution
224            .charge_work(1)
225            .map_err(|cause| binary::Error::Callback {
226                location,
227                cause: DigestError::Resource(cause),
228            })?;
229        let reference = &references.entries()[ordinal];
230        let answer = if let Some(answer) = super::preflight_method(&access, reference.method)
231            .map_err(|cause| binary::Error::Access {
232                location: Location {
233                    node: reference.method,
234                    ..location
235                },
236                cause,
237            })? {
238            PathDigest {
239                reference: ordinal,
240                agreement: answer,
241                certificate: None,
242            }
243        } else {
244            binary::with_octets(
245                xml,
246                Location {
247                    node: reference.digest,
248                    ..location
249                },
250                execution,
251                |expected| {
252                    Ok::<_, core::convert::Infallible>(match_reference(
253                        &access, reference, ordinal, path, provider, expected,
254                    ))
255                },
256            )
257            .map_err(|error| error.map_callback(|never| match never {}))??
258        };
259        if answer.agreement != Agreement::Matches {
260            all_match = false;
261        }
262        entries
263            .try_push(answer)
264            .map_err(|cause| binary::Error::Callback {
265                location,
266                cause: DigestError::Resource(cause),
267            })?;
268    }
269    Ok(PathDigests {
270        references,
271        path,
272        entries: entries.into_owned(),
273        all_match,
274    })
275}
276
277fn match_reference<'p, X: Xml, P: DigestProvider<'p, X>, W: Work>(
278    access: &Access<'_, X, W>,
279    reference: &super::Reference<X::Id>,
280    ordinal: usize,
281    path: &CheckedPath<'_>,
282    provider: &'p P,
283    expected: &[u8],
284) -> binary::BinaryResult<PathDigest, X, DigestError<P::Error>> {
285    let identity = match super::resolve_expected(access, reference, provider, expected)? {
286        Expected::Ready(identity) => identity,
287        Expected::Answer(agreement) => {
288            return Ok(PathDigest {
289                reference: ordinal,
290                agreement,
291                certificate: None,
292            });
293        }
294    };
295    let mut answer = PathDigest {
296        reference: ordinal,
297        agreement: Agreement::Differs,
298        certificate: None,
299    };
300    let admit = || {
301        access
302            .execution()
303            .charge_work(1)
304            .map_err(|cause| binary::Error::Callback {
305                location: Location {
306                    snapshot: access.snapshot(),
307                    node: reference.node,
308                },
309                cause: DigestError::<P::Error>::Resource(cause),
310            })
311    };
312    let members = path.members();
313    if ordinal == 0 {
314        admit()?;
315        let target = members.last().expect("a checked path has a target");
316        answer.agreement = super::compare_resolved(
317            access,
318            reference,
319            target.certificate(),
320            provider,
321            &identity,
322            expected,
323        )?;
324        if answer.agreement == Agreement::Matches {
325            answer.certificate = Some(PathCertificate::Member(members.len() - 1));
326        }
327        return Ok(answer);
328    }
329    for (index, member) in members.iter().enumerate() {
330        admit()?;
331        match super::compare_resolved(
332            access,
333            reference,
334            member.certificate(),
335            provider,
336            &identity,
337            expected,
338        )? {
339            Agreement::Matches => {
340                answer.agreement = Agreement::Matches;
341                answer.certificate = Some(PathCertificate::Member(index));
342                return Ok(answer);
343            }
344            Agreement::Differs => {}
345            // The provider cannot digest under this identity at all; no later
346            // candidate can change that answer into a mismatch.
347            other => {
348                answer.agreement = other;
349                return Ok(answer);
350            }
351        }
352    }
353    admit()?;
354    if let Some(certificate) = path.anchor().certificate() {
355        let agreement = super::compare_resolved(
356            access,
357            reference,
358            certificate,
359            provider,
360            &identity,
361            expected,
362        )?;
363        if agreement != Agreement::Differs {
364            answer.agreement = agreement;
365        }
366        if agreement == Agreement::Matches {
367            answer.certificate = Some(PathCertificate::Anchor);
368        }
369    }
370    Ok(answer)
371}