Skip to main content

structured_xades/validation/
ref_credential_certificates.rs

1//! Credential certificate required sets and the protected/ordinary alternatives.
2
3use super::{ReferenceDigestError, ReferenceSearch, ref_catalogue::Memo};
4use crate::{
5    Xades,
6    binary::{self, BinaryXml, Location},
7    certificate::{Agreement, DigestProvider, FormatDigest},
8    digest,
9    prepare::{Preparation, Prepared},
10    unsigned,
11    xml::{Access, Xml},
12};
13use core::ops::Range;
14use structured_asn1::digest::Digest;
15use structured_asn1::memory::{MemoryError, OwnedMap, OwnedVec, Work};
16use structured_ltv::material::PathCertificate;
17use structured_ltv::pki::{Participant, ParticipantAuthority, ParticipantRequirements};
18use structured_ltv::timestamp::TimestampScope;
19use structured_pkix::Certificate;
20
21/// Source of an original certificate reference; protected and ordinary copies
22/// are normative alternatives, not caller-supplied presence flags.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CredentialReferencePlace {
25    /// Original ordinal in protected `SigningCertificateV2`.
26    Protected(usize),
27    /// Original property/entry ordinals in `CompleteCertificateRefsV2`.
28    Ordinary {
29        /// Ordinal of the original ordinary property.
30        property: usize,
31        /// Ordinal within that property's original list.
32        entry: usize,
33    },
34    /// Original property/entry ordinals in `AttributeCertificateRefsV2`.
35    Attribute {
36        /// Ordinal of the original attribute property.
37        property: usize,
38        /// Ordinal within that property's original list.
39        entry: usize,
40    },
41}
42
43/// One certificate actually required by a credential authority path.
44pub struct CredentialCertificate<'view, 'document> {
45    participant: usize,
46    role: PathCertificate,
47    certificate: &'view Certificate<'document>,
48    ordinary: bool,
49    protected: bool,
50    attribute: bool,
51}
52impl<'v, 'd> CredentialCertificate<'v, 'd> {
53    /// Original credential participant/authority ordinal in the complete census.
54    #[must_use]
55    pub fn participant(&self) -> usize {
56        self.participant
57    }
58    /// Existing anchor or original issuer-to-subject member ordinal.
59    #[must_use]
60    pub fn role(&self) -> PathCertificate {
61        self.role
62    }
63    /// Exact original certificate; no decoding, normalization or copy repeats.
64    #[must_use]
65    pub fn certificate(&self) -> &'v Certificate<'d> {
66        self.certificate
67    }
68    /// Matching original protected/ordinary/attribute reference presence.
69    #[must_use]
70    pub fn presence(&self) -> (bool, bool, bool) {
71        (self.protected, self.ordinary, self.attribute)
72    }
73    /// A.1.3's SHOULD NOT duplication; this is not an unconditional rejection.
74    #[must_use]
75    pub fn redundant_attribute_reference(&self) -> bool {
76        self.attribute && (self.ordinary || self.protected)
77    }
78}
79
80/// One original reference, retaining every candidate comparison.
81pub struct CredentialCertificateRow<N> {
82    place: CredentialReferencePlace,
83    node: N,
84    search: ReferenceSearch,
85    range: Range<usize>,
86}
87impl<N: Copy> CredentialCertificateRow<N> {
88    /// Original protected/ordinary/attribute role and ordinals.
89    #[must_use]
90    pub fn place(&self) -> CredentialReferencePlace {
91        self.place
92    }
93    /// Original Cert element.
94    #[must_use]
95    pub fn node(&self) -> N {
96        self.node
97    }
98    /// Completed comparison or exact unavailable algorithm/value outcome.
99    #[must_use]
100    pub fn search(&self) -> ReferenceSearch {
101        self.search
102    }
103}
104
105/// Only A.1.3(1–2)'s required X.509 reference presence, not credential trust,
106/// optional status-authority references, identifier correctness or a grade.
107pub struct CredentialCertificateReferences<'p, 'v, 'd, N, D, G> {
108    prepared: &'p Preparation<N, D, G>,
109    candidates: OwnedVec<CredentialCertificate<'v, 'd>>,
110    rows: OwnedVec<CredentialCertificateRow<N>>,
111    comparisons: OwnedVec<Agreement>,
112    present: bool,
113    covered: bool,
114}
115impl<'p, 'v, 'd, N, D, G> CredentialCertificateReferences<'p, 'v, 'd, N, D, G> {
116    /// Same immutable original preparation.
117    #[must_use]
118    pub fn prepared(&self) -> &'p Preparation<N, D, G> {
119        self.prepared
120    }
121    /// Whether the optional original attribute-reference property was written.
122    #[must_use]
123    pub fn property_present(&self) -> bool {
124        self.present
125    }
126    /// Actual credential path certificate obligations; shared paths keep each
127    /// attribution while their exact bytes share hash work.
128    #[must_use]
129    pub fn candidates(&self) -> &[CredentialCertificate<'v, 'd>] {
130        &self.candidates
131    }
132    /// Every considered original reference in processing order.
133    #[must_use]
134    pub fn rows(&self) -> &[CredentialCertificateRow<N>] {
135        &self.rows
136    }
137    /// One agreement per candidate, in the same candidate order.
138    #[must_use]
139    pub fn comparisons(&self, row: usize) -> Option<&[Agreement]> {
140        self.comparisons.get(self.rows.get(row)?.range.clone())
141    }
142    /// Necessary required-set presence only. An absent optional property does
143    /// not activate its obligations. Unsupported comparisons never prove it.
144    #[must_use]
145    pub fn all_required_referenced(&self) -> bool {
146        self.covered
147    }
148}
149
150/// Original source/provider/resource failures remain terminal and located.
151pub type CredentialCertificateResult<'p, 'v, 'd, X, E> = binary::BinaryResult<
152    CredentialCertificateReferences<
153        'p,
154        'v,
155        'd,
156        <X as Xml>::Id,
157        <X as Xml>::Document,
158        <X as Xml>::Generation,
159    >,
160    X,
161    ReferenceDigestError<E>,
162>;
163
164fn candidates<'v, 'd, N: Copy, D: Copy, G: Copy, W: Work>(
165    required: &ParticipantRequirements<'v, 'd, N, D, G, Xades>,
166    work: &W,
167) -> Result<OwnedVec<CredentialCertificate<'v, 'd>, W>, MemoryError> {
168    let mut values = work.vector();
169    let mut push = |participant, role, certificate| {
170        values.try_push(CredentialCertificate {
171            participant,
172            role,
173            certificate,
174            ordinary: false,
175            protected: false,
176            attribute: false,
177        })
178    };
179    for (ordinal, entry) in required.entries().iter().enumerate() {
180        work.charge_work(1)?;
181        if !matches!(entry.participant(), Participant::Credential { .. }) {
182            continue;
183        }
184        match entry.authority() {
185            ParticipantAuthority::Path(path) => {
186                if let Some(certificate) = path.anchor().certificate() {
187                    push(ordinal, PathCertificate::Anchor, certificate)?;
188                }
189                for (member, value) in path.members().iter().enumerate() {
190                    work.charge_work(1)?;
191                    push(
192                        ordinal,
193                        PathCertificate::Member(member),
194                        value.certificate(),
195                    )?;
196                }
197            }
198            ParticipantAuthority::Anchor(anchor) => {
199                if let Some(certificate) = anchor.anchor().certificate() {
200                    push(ordinal, PathCertificate::Anchor, certificate)?;
201                }
202            }
203            ParticipantAuthority::CertificateFree => (),
204        }
205    }
206    Ok(values)
207}
208
209struct CredentialScan<'provider, 'view, 'document, P: Digest<'provider>, W: Work> {
210    memo: Memo<'provider, 'document, P, W>,
211    candidates: Option<OwnedVec<CredentialCertificate<'view, 'document>, W>>,
212    ids: OwnedVec<usize, W>,
213    comparisons: OwnedVec<Agreement, W>,
214}
215impl<'q, 'v, 'd, P: Digest<'q>, W: Work> CredentialScan<'q, 'v, 'd, P, W> {
216    fn compare<N: Copy, D: Copy, G: Copy>(
217        &mut self,
218        identity: FormatDigest<P::Identity>,
219        expected: &[u8],
220        place: CredentialReferencePlace,
221        required: &ParticipantRequirements<'v, 'd, N, D, G, Xades>,
222        work: &W,
223    ) -> Result<(), digest::Error<structured_cms::stream::RangesError, P::Error>> {
224        let method = self.memo.method(identity, work)?;
225        if self.candidates.is_none() {
226            let values = candidates(required, work).map_err(digest::Error::Resource)?;
227            let mut index: OwnedMap<&[u8], usize, W> = work.map();
228            for (ordinal, candidate) in values.iter().enumerate() {
229                work.charge_work(1).map_err(digest::Error::Resource)?;
230                let bytes = candidate.certificate.as_bytes();
231                let id =
232                    if let Some(&id) = index.get(&bytes, work).map_err(digest::Error::Resource)? {
233                        id
234                    } else {
235                        index
236                            .try_insert(bytes, ordinal)
237                            .map_err(digest::Error::Resource)?;
238                        ordinal
239                    };
240                self.ids.try_push(id).map_err(digest::Error::Resource)?;
241            }
242            self.candidates = Some(values);
243        }
244        let values = self
245            .candidates
246            .as_mut()
247            .expect("initialized credential paths");
248        for (ordinal, candidate) in values.iter_mut().enumerate() {
249            work.charge_work(1).map_err(digest::Error::Resource)?;
250            self.comparisons
251                .try_reserve(1)
252                .map_err(digest::Error::Resource)?;
253            let agreement = self.memo.compare(
254                self.ids[ordinal],
255                candidate.certificate.as_bytes(),
256                method,
257                expected,
258                work,
259            )?;
260            if agreement == Agreement::Matches {
261                match place {
262                    CredentialReferencePlace::Protected(_) => candidate.protected = true,
263                    CredentialReferencePlace::Ordinary { .. } => candidate.ordinary = true,
264                    CredentialReferencePlace::Attribute { .. } => candidate.attribute = true,
265                }
266            }
267            self.comparisons
268                .try_push(agreement)
269                .map_err(digest::Error::Resource)?;
270        }
271        Ok(())
272    }
273}
274
275/// Check the mandatory credential signer/ancestor/existing-anchor reference set
276/// from actual complete participant paths, honoring the normative protected and
277/// ordinary-reference alternatives. No caller-written presence flags, new path
278/// validation, key selection, certificate copies or hash engine are introduced.
279/// Method syntax/width gates precede lazy path catalogues. One provider-identity
280/// memo pays for each exact certificate once even across shared authorities.
281/// A.1.3(1–2)'s repeated-reference SHOULD NOT remains a separate advisory fact.
282/// This is not complete identifier/credential/status/trust or baseline acceptance.
283/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
284///
285/// # Panics
286/// Only if a privately constructed immutable candidate index changes.
287pub fn check_credential_certificate_references<
288    'p,
289    'v,
290    'd,
291    'q,
292    X: BinaryXml,
293    P: DigestProvider<'q, X>,
294    W: Work,
295>(
296    xml: &X,
297    prepared: &'p Prepared<X>,
298    required: &ParticipantRequirements<'v, 'd, X::Id, X::Document, X::Generation, Xades>,
299    provider: &'q P,
300    work: &W,
301) -> CredentialCertificateResult<'p, 'v, 'd, X, P::Error> {
302    let source = prepared.bound().containers();
303    let location = Location {
304        snapshot: source.snapshot(),
305        node: source.signature(),
306    };
307    let access = Access::new(xml, location.snapshot, work);
308    access
309        .query(location.node, |_, _, _| Ok(()))
310        .map_err(|cause| binary::Error::Access { location, cause })?;
311    if required.scope().location().snapshot != location.snapshot
312        || !matches!(required.scope(), TimestampScope::All(_))
313        || !access
314            .equal_id(required.signature(), location.node)
315            .map_err(|cause| binary::Error::Callback {
316                location,
317                cause: ReferenceDigestError::Digest(digest::Error::Resource(cause)),
318            })?
319    {
320        return Err(binary::Error::Callback {
321            location,
322            cause: ReferenceDigestError::Foreign,
323        });
324    }
325    process(xml, prepared, required, provider, &access, location)
326}
327
328fn process<'p, 'v, 'd, 'q, X: BinaryXml, P: DigestProvider<'q, X>, W: Work>(
329    xml: &X,
330    prepared: &'p Prepared<X>,
331    required: &ParticipantRequirements<'v, 'd, X::Id, X::Document, X::Generation, Xades>,
332    provider: &'q P,
333    access: &Access<'_, X, W>,
334    location: Location<X::Id, X::Document, X::Generation>,
335) -> CredentialCertificateResult<'p, 'v, 'd, X, P::Error> {
336    let work = access.execution();
337    let resource = |cause| binary::Error::Callback {
338        location,
339        cause: ReferenceDigestError::Digest(digest::Error::Resource(cause)),
340    };
341    let mut present = false;
342    for property in prepared.validation_references() {
343        work.charge_work(1).map_err(resource)?;
344        present |= property.role() == unsigned::Role::AttributeCertificateReferences;
345    }
346    let mut state = CredentialScan {
347        memo: Memo::new(provider, work),
348        candidates: None,
349        ids: work.vector(),
350        comparisons: work.vector(),
351    };
352    let mut rows = work.vector();
353    if present {
354        let mut row = |place, node, method, value| {
355            work.charge_work(1).map_err(resource)?;
356            rows.try_reserve(1).map_err(resource)?;
357            let start = state.comparisons.len();
358            let search = match digest::check_with(
359                xml,
360                digest::Reference {
361                    snapshot: location.snapshot,
362                    method,
363                    value,
364                },
365                provider,
366                work,
367                |identity, expected, work| state.compare(identity, expected, place, required, work),
368            )
369            .map_err(|cause| cause.map_callback(ReferenceDigestError::Digest))?
370            {
371                digest::Checked::Value(()) => ReferenceSearch::Compared,
372                digest::Checked::Answer(answer) => ReferenceSearch::Unavailable(answer),
373            };
374            rows.try_push(CredentialCertificateRow {
375                place,
376                node,
377                search,
378                range: start..state.comparisons.len(),
379            })
380            .map_err(resource)
381        };
382        for (entry, reference) in prepared.certificates().entries().iter().enumerate() {
383            row(
384                CredentialReferencePlace::Protected(entry),
385                reference.node(),
386                reference.method(),
387                reference.digest(),
388            )?;
389        }
390        for (property, references) in prepared.validation_references().iter().enumerate() {
391            work.charge_work(1).map_err(resource)?;
392            for (entry, reference) in references.entries().iter().enumerate() {
393                work.charge_work(1).map_err(resource)?;
394                let place = match references.role() {
395                    unsigned::Role::CertificateReferences => {
396                        CredentialReferencePlace::Ordinary { property, entry }
397                    }
398                    unsigned::Role::AttributeCertificateReferences => {
399                        CredentialReferencePlace::Attribute { property, entry }
400                    }
401                    _ => continue,
402                };
403                let (method, value) = reference.digest().expect("certificate digest is mandatory");
404                row(place, reference.node(), method, value)?;
405            }
406        }
407    }
408    let mut covered = !present || state.candidates.is_some();
409    let candidates = state.candidates.unwrap_or_else(|| work.vector());
410    for candidate in &candidates {
411        work.charge_work(1).map_err(resource)?;
412        covered &= candidate.ordinary || candidate.protected || candidate.attribute;
413    }
414    access
415        .query(location.node, |_, _, _| Ok(()))
416        .map_err(|cause| binary::Error::Access { location, cause })?;
417    Ok(CredentialCertificateReferences {
418        prepared,
419        candidates: candidates.into_owned(),
420        rows: rows.into_owned(),
421        comparisons: state.comparisons.into_owned(),
422        present,
423        covered,
424    })
425}