Skip to main content

structured_xades/
certificate.rs

1//! Original signing-certificate references, before digest/path validation.
2
3use crate::binary::{self, BinaryXml, Location};
4use crate::binding::{Bound, SignatureXml};
5use crate::container::XADES;
6use crate::properties::{Error, PropertyResult, Reading};
7use crate::xml::{Access, AccessError, Name, Snapshot, Xml};
8use structured_asn1::digest::{Digest, OnceError, Resolution, matches_once};
9use structured_asn1::{
10    Reader, Tag,
11    memory::{MemoryError, OwnedVec, Work},
12    universal,
13};
14use structured_cms::ess::IssuerSerialReading;
15use structured_pkix::{Certificate, path::WorkingKey};
16
17mod path;
18pub use path::{
19    CheckedPathCertificate, PathCheckResult, PathDigest, PathDigestResult, PathDigests, check_path,
20    match_path,
21};
22
23/// Association with the key that actually verified the selected XML signature.
24/// Trust and source association are independent: a caller-trusted key may still
25/// lack a receipt linking it to a particular certificate.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum KeyAssociation {
28    /// The selected successful verification key belongs to this certificate.
29    Same,
30    /// Existing key evidence proves a different certificate/key association.
31    Differs,
32    /// Mathematical verification succeeded but no such association was retained.
33    Unproven,
34}
35
36/// Read-only selected-key evidence from the XML owner, not a second verifier.
37pub trait CertificateXml: SignatureXml {
38    /// Reads the successful selected key/certificate association for this exact
39    /// signature and source generation. `KeyInfo` presence, any valid signature,
40    /// an issuer hint or caller trust alone must not produce `Same`. Missing
41    /// association evidence is `Unproven`, not unsupported mathematics. Any
42    /// fingerprint/key comparisons and lazy access are paid before execution.
43    /// `key` is the actual expected effective key, including inherited path
44    /// parameters. `Same` additionally requires the parameter semantics used
45    /// by the original successful operation, not just a certificate fingerprint.
46    /// Missing selected-key evidence is never repaired by invoking a resolver,
47    /// constructing a standing key, re-encoding SPKI or repeating verification.
48    fn key_association<W: Work>(
49        &self,
50        snapshot: Snapshot<Self::Document, Self::Generation>,
51        signature: Self::Id,
52        certificate: &Certificate<'_>,
53        key: WorkingKey<'_>,
54        execution: &W,
55    ) -> Result<KeyAssociation, Self::Error>;
56}
57
58/// XML `DigestMethod` resolution of the selected provider, layered over its
59/// [`Digest`] execution.
60///
61/// Resolution must enforce original method syntax/parameters and compiled XML
62/// algorithm policy before hashing; no ASN.1 identifier is constructed from an
63/// XML algorithm URI. Every callback admits its transitive CPU, allocation and
64/// output capacity; this contract alone does not qualify an opaque backend's
65/// accounting.
66pub trait DigestProvider<'a, X: Xml>: Digest<'a> {
67    /// Resolves the original method through the selected XML provider dispatcher.
68    fn resolve<W: Work>(
69        &self,
70        access: &Access<'_, X, W>,
71        method: X::Id,
72    ) -> Result<Resolution<Self::Identity>, Self::Error>;
73
74    /// O(1) metadata of the ACTUAL resolved native digest, independent of its
75    /// URI spelling or output width. Resolution already performed any parsing;
76    /// this lookup does not rescan parameters, allocate or select another engine.
77    /// A signature suite without an external digest is `Intrinsic`, not a usable
78    /// digest capability. The common baseline guard rejects it and MD5 before
79    /// invoking hash primitives; compiled deployment acceptance stays in resolve.
80    fn classify(&self, identity: &Self::Identity) -> crate::hashes::HashClass;
81}
82
83/// Completed digest agreement, separate from selected-key association/path trust.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum Agreement {
86    /// A syntactically identified algorithm is forbidden by the baseline format.
87    ProhibitedAlgorithm,
88    /// Full original certificate digest matches the signed reference.
89    Matches,
90    /// Completed digest comparison differs.
91    Differs,
92    /// Selected provider does not implement this XML digest identity.
93    Unsupported,
94    /// Original XML digest method violates its selected algorithm's syntax.
95    MalformedMethod,
96    /// Declared digest value has an impossible fixed width.
97    MalformedValue,
98}
99
100/// Digest execution failure in the original XML method/value enclosure.
101#[derive(Debug)]
102pub enum DigestError<E> {
103    /// Caller selected an ordinal outside the parsed reference catalogue.
104    /// This is processing misuse, not malformed XML or resource exhaustion.
105    ReferenceIndex {
106        /// Requested catalogue ordinal.
107        index: usize,
108    },
109    /// Original selected-provider/policy/resource cause.
110    Provider(E),
111    /// Common dispatch admission refused before provider execution.
112    Resource(MemoryError),
113    /// The provider's output contradicts its own identity's fixed width: a
114    /// provider contract failure, never an ordinary mismatch.
115    OutputWidth {
116        /// Width promised by the resolved identity.
117        expected: usize,
118        /// Width actually returned.
119        actual: usize,
120    },
121}
122
123/// One original `Cert` entry in the signed `SigningCertificateV2` property.
124/// The first entry identifies the signing certificate. Neither an optional URI
125/// nor an issuer/serial hint substitutes for hashing the complete certificate.
126#[derive(Debug, Clone, Copy)]
127pub struct Reference<N> {
128    node: N,
129    method: N,
130    digest: N,
131    issuer: Option<N>,
132}
133
134impl<N: Copy> Reference<N> {
135    /// Original `Cert` node; its optional URI is a caller-owned acquisition hint.
136    #[must_use]
137    pub fn node(&self) -> N {
138        self.node
139    }
140    /// Original ds:DigestMethod, including its algorithm URI and parameters.
141    #[must_use]
142    pub fn method(&self) -> N {
143        self.method
144    }
145    /// Original ds:DigestValue, whose base64 decoding belongs to the XML owner.
146    #[must_use]
147    pub fn digest(&self) -> N {
148        self.digest
149    }
150    /// Optional original base64 DER RFC5035 `IssuerSerial` hint.
151    #[must_use]
152    pub fn issuer_serial(&self) -> Option<N> {
153        self.issuer
154    }
155}
156
157/// The accounted original list, preserving the first-entry signing-key role.
158/// Structural reading alone does not establish a certificate digest match,
159/// signer-key association, validated path or attained baseline level.
160pub struct References<N, D, G> {
161    snapshot: Snapshot<D, G>,
162    signature: N,
163    node: N,
164    entries: OwnedVec<Reference<N>>,
165}
166
167impl<N: Copy, D: Copy, G: Copy> References<N, D, G> {
168    /// Exact original signed source generation.
169    #[must_use]
170    pub fn snapshot(&self) -> Snapshot<D, G> {
171        self.snapshot
172    }
173    /// Exact selected signature that authenticates this certificate catalogue.
174    #[must_use]
175    pub fn signature(&self) -> N {
176        self.signature
177    }
178    /// Original signed property container.
179    #[must_use]
180    pub fn node(&self) -> N {
181        self.node
182    }
183    /// Original entries in their normative order; the list is nonempty.
184    #[must_use]
185    pub fn entries(&self) -> &[Reference<N>] {
186        &self.entries
187    }
188}
189
190/// Compares one original reference with a candidate's complete original encoding.
191///
192/// The caller must separately bind this candidate to the successfully selected
193/// XML signature key. A digest match, URI or issuer hint does not establish that
194/// association and does not produce baseline/path/trust evidence here.
195pub fn compare<'p, X: BinaryXml, P: DigestProvider<'p, X>, W: Work>(
196    xml: &X,
197    reference: &Catalogue<X>,
198    index: usize,
199    certificate: &Certificate<'_>,
200    provider: &'p P,
201    execution: &W,
202) -> binary::BinaryResult<Agreement, X, DigestError<P::Error>> {
203    // An invalid ordinal is caller misuse, not malformed signed XML. Represent
204    // the enclosure honestly without indexing or starting a provider operation.
205    let Some(entry) = reference.entries.get(index) else {
206        return Err(binary::Error::Callback {
207            location: Location {
208                snapshot: reference.snapshot,
209                node: reference.node,
210            },
211            cause: DigestError::ReferenceIndex { index },
212        });
213    };
214    let location = Location {
215        snapshot: reference.snapshot,
216        node: entry.method,
217    };
218    let access = Access::new(xml, reference.snapshot, execution);
219    if let Some(answer) = preflight_method(&access, entry.method)
220        .map_err(|cause| binary::Error::Access { location, cause })?
221    {
222        return Ok(answer);
223    }
224    binary::with_octets(
225        xml,
226        Location {
227            snapshot: reference.snapshot,
228            node: entry.digest,
229        },
230        execution,
231        |expected| {
232            Ok::<_, core::convert::Infallible>(compare_expected(
233                &access,
234                entry,
235                certificate,
236                provider,
237                expected,
238            ))
239        },
240    )
241    .map_err(|error| error.map_callback(|never| match never {}))?
242}
243
244fn compare_expected<'p, X: Xml, P: DigestProvider<'p, X>, W: Work>(
245    access: &Access<'_, X, W>,
246    entry: &Reference<X::Id>,
247    certificate: &Certificate<'_>,
248    provider: &'p P,
249    expected: &[u8],
250) -> binary::BinaryResult<Agreement, X, DigestError<P::Error>> {
251    let identity = match resolve_expected(access, entry, provider, expected)? {
252        Expected::Ready(identity) => identity,
253        Expected::Answer(answer) => return Ok(answer),
254    };
255    compare_resolved(access, entry, certificate, provider, &identity, expected)
256}
257
258pub(crate) enum Expected<I> {
259    Ready(FormatDigest<I>),
260    Answer(Agreement),
261}
262
263/// A resolved identity that passed the shared baseline digest-class gate.
264/// Only `baseline_digest` constructs it; optimized paths cannot bypass the gate
265/// merely by already possessing an opaque provider identity.
266pub(crate) struct FormatDigest<I>(I);
267
268impl<I> FormatDigest<I> {
269    pub(crate) fn identity(&self) -> &I {
270        &self.0
271    }
272}
273
274pub(crate) fn baseline_digest<'p, X: Xml, P: DigestProvider<'p, X>>(
275    identity: P::Identity,
276    provider: &P,
277) -> Expected<P::Identity> {
278    match resolved_digest::<true, X, P>(identity, provider) {
279        Ok(identity) => Expected::Ready(FormatDigest(identity)),
280        Err(answer) => Expected::Answer(answer),
281    }
282}
283
284pub(crate) fn resolved_digest<'p, const BASELINE: bool, X: Xml, P: DigestProvider<'p, X>>(
285    identity: P::Identity,
286    provider: &P,
287) -> Result<P::Identity, Agreement> {
288    // EN319132-1 V1.3.1 §6.2.1 prohibits the actual MD5 digest. URI aliases
289    // cannot change that requirement. XMLDSig1.1 §4.4.3.5 selects a DIGEST
290    // algorithm here, not an intrinsic signature suite with no external hash.
291    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
292    // https://www.w3.org/TR/2013/REC-xmldsig-core1-20130411/#sec-DigestMethod
293    match provider.classify(&identity) {
294        crate::hashes::HashClass::Md5 if BASELINE => Err(Agreement::ProhibitedAlgorithm),
295        crate::hashes::HashClass::Intrinsic => Err(Agreement::MalformedMethod),
296        crate::hashes::HashClass::Md5 | crate::hashes::HashClass::OtherKnown => Ok(identity),
297    }
298}
299
300fn resolve_expected<'p, X: Xml, P: DigestProvider<'p, X>, W: Work>(
301    access: &Access<'_, X, W>,
302    entry: &Reference<X::Id>,
303    provider: &P,
304    expected: &[u8],
305) -> binary::BinaryResult<Expected<P::Identity>, X, DigestError<P::Error>> {
306    let location = Location {
307        snapshot: access.snapshot(),
308        node: entry.method,
309    };
310    let resolved = access
311        .query(entry.method, |_, _, _| {
312            Ok(provider.resolve(access, entry.method))
313        })
314        .map_err(|cause| binary::Error::Access { location, cause })?
315        .map_err(|cause| binary::Error::Callback {
316            location,
317            cause: DigestError::Provider(cause),
318        })?;
319    let identity = match resolved {
320        Resolution::Supported(identity) => identity,
321        Resolution::Unsupported => return Ok(Expected::Answer(Agreement::Unsupported)),
322        Resolution::Malformed => return Ok(Expected::Answer(Agreement::MalformedMethod)),
323    };
324    let identity = match baseline_digest::<X, P>(identity, provider) {
325        Expected::Ready(identity) => identity,
326        answer @ Expected::Answer(_) => return Ok(answer),
327    };
328    if let Some(width) = provider.width(identity.identity())
329        && expected.len() != width
330    {
331        return Ok(Expected::Answer(Agreement::MalformedValue));
332    }
333    Ok(Expected::Ready(identity))
334}
335
336fn compare_resolved<'p, X: Xml, P: DigestProvider<'p, X>, W: Work>(
337    access: &Access<'_, X, W>,
338    entry: &Reference<X::Id>,
339    certificate: &Certificate<'_>,
340    provider: &'p P,
341    identity: &FormatDigest<P::Identity>,
342    expected: &[u8],
343) -> binary::BinaryResult<Agreement, X, DigestError<P::Error>> {
344    let execution = access.execution();
345    let location = Location {
346        snapshot: access.snapshot(),
347        node: entry.digest,
348    };
349    let check = || {
350        execution.charge_work(1).map_err(DigestError::Resource)?;
351        // One update over the whole original certificate; no re-encoding,
352        // implicit acquisition or width-based algorithm inference.
353        let answer = matches_once(
354            provider,
355            identity.identity(),
356            certificate.as_bytes(),
357            expected,
358            execution,
359        )
360        .map_err(|error| match error {
361            OnceError::Provider(cause) => DigestError::Provider(cause),
362            OnceError::OutputWidth { expected, actual } => {
363                DigestError::OutputWidth { expected, actual }
364            }
365        })?;
366        Ok(match answer {
367            Resolution::Supported(true) => Agreement::Matches,
368            Resolution::Supported(false) => Agreement::Differs,
369            Resolution::Unsupported => Agreement::Unsupported,
370            Resolution::Malformed => Agreement::MalformedMethod,
371        })
372    };
373    check().map_err(|cause| binary::Error::Callback { location, cause })
374}
375
376pub(crate) fn preflight_method<X: Xml, W: Work>(
377    access: &Access<'_, X, W>,
378    node: X::Id,
379) -> Result<Option<Agreement>, AccessError<X::Error, X::Id>> {
380    access
381        .attribute(
382            node,
383            Name {
384                namespace: "",
385                local: "Algorithm",
386            },
387            |value| {
388                let Some(value) = value else {
389                    return Ok(Some(Agreement::MalformedMethod));
390                };
391                let value = access.trim_xml_whitespace(value)?;
392                preflight_algorithm(value, access.execution())
393            },
394        )?
395        .map_err(|cause| AccessError::Resource { node, cause })
396}
397
398pub(crate) fn preflight_algorithm<W: Work>(
399    algorithm: &str,
400    work: &W,
401) -> Result<Option<Agreement>, MemoryError> {
402    // EN319132-1 §6.2.1 unconditionally excludes MD5 before deployment policy.
403    // Creation and existing-node validation share this format rule.
404    // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
405    // RFC9231 §2.1.1 defines this identifier.
406    // https://www.rfc-editor.org/rfc/rfc9231.html#section-2.1.1
407    work.charge_work(algorithm.len() as u64)?;
408    Ok(
409        if algorithm == "http://www.w3.org/2001/04/xmldsig-more#md5" {
410            Some(Agreement::ProhibitedAlgorithm)
411        } else {
412            None
413        },
414    )
415}
416
417/// Certificate-reference catalogue using the XML owner's original identities.
418pub type Catalogue<X> = References<<X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>;
419
420/// Completed selected-key and signed certificate-digest facts for this candidate.
421/// Original borrowed sources remain attached, so facts for one certificate or
422/// signature cannot silently become evidence for a different original object.
423/// This is not a baseline-level assertion or path/revocation/trust verdict.
424pub struct CheckedCertificate<'view, 'document, N, D, G> {
425    reference: &'view References<N, D, G>,
426    certificate: &'view Certificate<'document>,
427    key: KeyAssociation,
428    digest: Agreement,
429}
430
431impl<'view, 'document, N, D, G> CheckedCertificate<'view, 'document, N, D, G> {
432    /// Actual candidate whose original bytes were compared.
433    #[must_use]
434    pub fn certificate(&self) -> &'view Certificate<'document> {
435        self.certificate
436    }
437    /// Original signed reference catalogue and its selected signature/source.
438    #[must_use]
439    pub fn reference(&self) -> &'view References<N, D, G> {
440        self.reference
441    }
442    /// Independent actual verification-key association.
443    #[must_use]
444    pub fn key(&self) -> KeyAssociation {
445        self.key
446    }
447    /// Independent complete signed digest comparison.
448    #[must_use]
449    pub fn digest(&self) -> Agreement {
450        self.digest
451    }
452    /// Both selected-key association and signed certificate digest are proven.
453    /// Other property syntax and path/status/policy checks remain necessary.
454    #[must_use]
455    pub fn is_bound(&self) -> bool {
456        self.key == KeyAssociation::Same && self.digest == Agreement::Matches
457    }
458}
459
460/// Checked facts indexed by original identities, independent of a scoped XML view.
461pub type Checked<'view, 'document, X> = CheckedCertificate<
462    'view,
463    'document,
464    <X as Xml>::Id,
465    <X as Xml>::Document,
466    <X as Xml>::Generation,
467>;
468
469/// Selected-certificate conformance/processing refusal, separate from key/digest mismatch.
470#[derive(Debug)]
471pub enum CheckError<E, PE, N, D, G> {
472    /// Catalogue belongs to another selected signature or original generation.
473    /// This is caller/source misuse, not a fault in the signed document.
474    ForeignCatalogue {
475        /// Requested selected signature.
476        signature: N,
477        /// Original supplied reference container.
478        property: N,
479    },
480    /// Current baseline prohibits the optional general-schema Cert URI attribute.
481    ForbiddenCertificateUri {
482        /// Original Cert entry, with exact XML document/generation.
483        location: Location<N, D, G>,
484    },
485    /// Original read-only selected-key access/refusal.
486    Access(AccessError<E, N>),
487    /// Original digest execution and binary/XML source enclosure.
488    Digest(binary::Error<E, DigestError<PE>, N, D, G>),
489}
490
491/// Selected-certificate check result retaining both original borrowed operands.
492pub type CheckResult<'v, 'd, X, PE> = CertificateResult<Checked<'v, 'd, X>, X, PE>;
493
494/// Certificate binding result in the XML owner's original error/source domains.
495pub type CertificateResult<T, X, PE> = Result<
496    T,
497    CheckError<<X as Xml>::Error, PE, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
498>;
499
500/// Scoped optional issuer-hint refusal; this hint never replaces digest binding.
501#[derive(Debug)]
502pub enum IssuerError<E> {
503    /// Caller selected an ordinal outside the original certificate catalogue.
504    ReferenceIndex {
505        /// Requested catalogue ordinal.
506        index: usize,
507    },
508    /// Original decoded ASN.1 source failure, including its resource class.
509    Encoding(structured_asn1::Error),
510    /// Original shared ESS schema/diagnostic-retention refusal.
511    Reading(structured_cms::Error),
512    /// The source contains bytes after the one required DER instance.
513    Trailing {
514        /// Original position in the decoded source, not XML bytes.
515        offset: u64,
516    },
517    /// Original inspection callback refused.
518    Callback(E),
519}
520
521/// Reads the original optional hint with the existing RFC5035 field kernel.
522///
523/// EN319132-1 §5.2.2 fixes this value to one DER `IssuerSerial`. The scoped
524/// callback sees its original reading and every retained diagnostic. A hint's
525/// readability or issuer agreement is not certificate digest/key/path evidence.
526pub fn with_issuer_serial<X: BinaryXml, R, E>(
527    xml: &X,
528    reference: &Catalogue<X>,
529    index: usize,
530    operation: &Reader<'_>,
531    visitor: impl for<'source> FnOnce(IssuerSerialReading<'source>) -> Result<R, E>,
532) -> binary::BinaryResult<Option<binary::LocatedValue<R, X>>, X, IssuerError<E>> {
533    let location = Location {
534        snapshot: reference.snapshot,
535        node: reference.node,
536    };
537    let Some(entry) = reference.entries.get(index) else {
538        return Err(binary::Error::Callback {
539            location,
540            cause: IssuerError::ReferenceIndex { index },
541        });
542    };
543    let execution = operation.execution();
544    Access::new(xml, reference.snapshot, &execution)
545        .query(entry.node, |_, _, _| Ok(()))
546        .map_err(|cause| binary::Error::Access { location, cause })?;
547    let Some(node) = entry.issuer else {
548        return Ok(None);
549    };
550    let location = Location {
551        snapshot: reference.snapshot,
552        node,
553    };
554    with_issuer_serial_at(xml, location, operation, visitor).map(Some)
555}
556
557pub(crate) fn with_issuer_serial_at<X: BinaryXml, R, E>(
558    xml: &X,
559    location: Location<X::Id, X::Document, X::Generation>,
560    operation: &Reader<'_>,
561    visitor: impl for<'source> FnOnce(IssuerSerialReading<'source>) -> Result<R, E>,
562) -> binary::BinaryResult<binary::LocatedValue<R, X>, X, IssuerError<E>> {
563    binary::with_der_reader(xml, location, operation, |reader| {
564        let element = reader
565            .expect(Tag::constructed(universal::SEQUENCE))
566            .map_err(IssuerError::Encoding)?;
567        let reading = IssuerSerialReading::read(reader, &element, "xades.issuerSerialV2")
568            .map_err(IssuerError::Reading)?;
569        if !reader.is_empty() {
570            return Err(IssuerError::Trailing {
571                offset: reader.offset(),
572            });
573        }
574        visitor(reading)
575            .map(|value| binary::Located { location, value })
576            .map_err(IssuerError::Callback)
577    })
578}
579
580/// Checks the first signed reference against the actual selected candidate.
581/// The first-entry role is defined by EN319132-1 §5.2.2. Additional entries stay
582/// available in the original catalogue; they are not silently treated as trust.
583/// Baseline reference conformance is checked before key/provider callbacks;
584/// low-level [`read`] still supports inspection of the general reference schema.
585pub fn check<
586    'view,
587    'document,
588    'p,
589    X: BinaryXml + CertificateXml,
590    P: DigestProvider<'p, X>,
591    W: Work,
592>(
593    xml: &X,
594    bound: &Bound<X::Id, X::Document, X::Generation>,
595    reference: &'view Catalogue<X>,
596    certificate: &'view Certificate<'document>,
597    provider: &'p P,
598    execution: &W,
599) -> CheckResult<'view, 'document, X, P::Error> {
600    let key = bind_key::<X, P::Error, W>(
601        xml,
602        bound,
603        reference,
604        certificate,
605        WorkingKey::standing(certificate.tbs().public_key()),
606        execution,
607    )?;
608    let digest =
609        compare(xml, reference, 0, certificate, provider, execution).map_err(CheckError::Digest)?;
610    Ok(CheckedCertificate {
611        reference,
612        certificate,
613        key,
614        digest,
615    })
616}
617
618fn bind_key<X: CertificateXml, PE, W: Work>(
619    xml: &X,
620    bound: &Bound<X::Id, X::Document, X::Generation>,
621    reference: &Catalogue<X>,
622    certificate: &Certificate<'_>,
623    key: WorkingKey<'_>,
624    execution: &W,
625) -> CertificateResult<KeyAssociation, X, PE> {
626    let source = bound.containers();
627    if reference.snapshot != source.snapshot() {
628        return Err(CheckError::ForeignCatalogue {
629            signature: source.signature(),
630            property: reference.node,
631        });
632    }
633    let access = Access::new(xml, source.snapshot(), execution);
634    if !access
635        .equal_id(reference.signature, source.signature())
636        .map_err(|cause| {
637            CheckError::Access(AccessError::Resource {
638                node: reference.node,
639                cause,
640            })
641        })?
642    {
643        return Err(CheckError::ForeignCatalogue {
644            signature: source.signature(),
645            property: reference.node,
646        });
647    }
648    for index in 0..reference.entries.len() {
649        access
650            .query(reference.node, |_, _, work| Ok(work.charge_work(1)))
651            .map_err(CheckError::Access)?
652            .map_err(|cause| {
653                CheckError::Access(AccessError::Resource {
654                    node: reference.node,
655                    cause,
656                })
657            })?;
658        let entry = &reference.entries[index];
659        let written = access
660            .attribute(
661                entry.node,
662                Name {
663                    namespace: "",
664                    local: "URI",
665                },
666                |value| value.is_some(),
667            )
668            .map_err(CheckError::Access)?;
669        // EN319132-1 V1.3.1 §6.3(i): a current-baseline generator shall not
670        // write Cert/@URI, even though the general §5.2.2 schema allows it.
671        // Preserve inspection separately, but do not run acceptance callbacks
672        // before reporting this baseline conformance failure.
673        // https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf
674        if written {
675            return Err(CheckError::ForbiddenCertificateUri {
676                location: Location {
677                    snapshot: reference.snapshot,
678                    node: entry.node,
679                },
680            });
681        }
682    }
683    access
684        .query(source.signature(), |xml, snapshot, work| {
685            xml.key_association(snapshot, source.signature(), certificate, key, work)
686        })
687        .map_err(CheckError::Access)
688}
689
690/// Reads the signed certificate-reference schema without acquisition or hashing.
691///
692/// EN319132-1 V1.3.1 §5.2.2 and its versioned 1.3.2 schema define a nonempty
693/// Cert list, each with `CertDigest` followed by optional `IssuerSerialV2`. The
694/// digest consists of the original `DigestMethod` then `DigestValue`, exactly once.
695/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
696pub fn read<X: Xml, W: Work>(
697    access: &Access<'_, X, W>,
698    signed: &Reading<X>,
699) -> PropertyResult<Catalogue<X>, X> {
700    let node = signed.signing_certificate();
701    if signed.snapshot() != access.snapshot() {
702        return Err(Error::Access(AccessError::Changed { node }));
703    }
704    let mut entries = access
705        .query(node, |_, _, work| Ok(work.vector()))
706        .map_err(Error::Access)?;
707    for child in access.children(node) {
708        let child = child.map_err(Error::Child)?;
709        expect_name(access, child, XADES, "Cert")?;
710        let entry = read_entry(access, child)?;
711        entries
712            .try_push(entry)
713            .map_err(|cause| Error::Resource { node: child, cause })?;
714    }
715    if entries.is_empty() {
716        return Err(Error::Missing {
717            parent: node,
718            local: "Cert",
719        });
720    }
721    Ok(References {
722        snapshot: signed.snapshot(),
723        signature: signed.signature(),
724        node,
725        entries: entries.into_owned(),
726    })
727}
728
729pub(crate) fn read_entry<X: Xml, W: Work>(
730    access: &Access<'_, X, W>,
731    node: X::Id,
732) -> PropertyResult<Reference<X::Id>, X> {
733    let mut children = access.children(node);
734    let digest = required(&mut children, node, "CertDigest")?;
735    expect_name(access, digest, XADES, "CertDigest")?;
736    let issuer = children.next().transpose().map_err(Error::Child)?;
737    if let Some(issuer) = issuer {
738        expect_name(access, issuer, XADES, "IssuerSerialV2")?;
739    }
740    if let Some(extra) = children.next() {
741        return Err(Error::Unexpected {
742            node: extra.map_err(Error::Child)?,
743        });
744    }
745    let (method, value) = crate::properties::digest_value(access, digest)?;
746    Ok(Reference {
747        node,
748        method,
749        digest: value,
750        issuer,
751    })
752}
753
754fn required<X: Xml, W: Work>(
755    children: &mut crate::xml::Children<'_, '_, X, W>,
756    parent: X::Id,
757    local: &'static str,
758) -> PropertyResult<X::Id, X> {
759    children
760        .next()
761        .transpose()
762        .map_err(Error::Child)?
763        .ok_or(Error::Missing { parent, local })
764}
765
766fn expect_name<X: Xml, W: Work>(
767    access: &Access<'_, X, W>,
768    node: X::Id,
769    namespace: &str,
770    local: &str,
771) -> PropertyResult<(), X> {
772    if !access
773        .has_name(node, Name { namespace, local })
774        .map_err(Error::Access)?
775    {
776        return Err(Error::Unexpected { node });
777    }
778    Ok(())
779}