Skip to main content

structured_xades/validation/
ref_path.rs

1//! Annex A root-path certificate references, separate from material placement.
2
3use super::{ReferenceDigestError, ReferenceSearch, ref_catalogue::Memo};
4use crate::{
5    binary::{self, BinaryXml, Location},
6    certificate::{Agreement, CheckedPathCertificate, DigestProvider},
7    digest,
8    prepare::{Preparation, Prepared},
9    unsigned,
10    xml::{Access, Xml},
11};
12use core::ops::Range;
13use structured_asn1::digest::Digest;
14use structured_asn1::memory::{MemoryError, OwnedVec, Work};
15use structured_ltv::material::PathCertificate;
16use structured_ltv::pki::CheckedPath;
17
18/// One original `CompleteCertificateRefsV2` entry and its root-path comparisons.
19pub struct RootReferenceRow<N> {
20    property: usize,
21    entry: usize,
22    node: N,
23    search: ReferenceSearch,
24    range: Range<usize>,
25}
26impl<N: Copy> RootReferenceRow<N> {
27    /// Original property ordinal in the same preparation.
28    #[must_use]
29    pub fn property(&self) -> usize {
30        self.property
31    }
32    /// Original entry ordinal in that property.
33    #[must_use]
34    pub fn entry(&self) -> usize {
35        self.entry
36    }
37    /// Original Cert enclosure.
38    #[must_use]
39    pub fn node(&self) -> N {
40        self.node
41    }
42    /// Exact method/declared-digest outcome, not an invented mismatch.
43    #[must_use]
44    pub fn search(&self) -> ReferenceSearch {
45        self.search
46    }
47}
48
49/// Root certificate rules of Annex A.1.1(1–2), bound to the actual selected
50/// path. Optional TSA/status-authority references, exclusive credential CAs,
51/// incorporated presence and authentication/status remain separate checks.
52pub struct RootPathReferences<'prepared, 'path, 'document, N, D, G> {
53    prepared: &'prepared Preparation<N, D, G>,
54    path: &'path CheckedPath<'document>,
55    rows: OwnedVec<RootReferenceRow<N>>,
56    comparisons: OwnedVec<Agreement>,
57    root_rules: bool,
58}
59impl<'p, 'v, 'd, N: Copy, D: Copy, G: Copy> RootPathReferences<'p, 'v, 'd, N, D, G> {
60    /// Same original root preparation, without replaying signature verification.
61    #[must_use]
62    pub fn prepared(&self) -> &'p Preparation<N, D, G> {
63        self.prepared
64    }
65    /// Actual checked path associated with the selected root key/certificate.
66    #[must_use]
67    pub fn path(&self) -> &'v CheckedPath<'d> {
68        self.path
69    }
70    /// Every original ordinary certificate reference, including unsupported ones.
71    #[must_use]
72    pub fn rows(&self) -> &[RootReferenceRow<N>] {
73        &self.rows
74    }
75    /// Each row compares the anchor (if certificate-backed), then every member
76    /// in issuer-to-target order. The target is the forbidden signing certificate.
77    #[must_use]
78    pub fn comparisons(&self, row: usize) -> Option<&[Agreement]> {
79        let row = self.rows.get(row)?;
80        self.comparisons.get(row.range.clone())
81    }
82    /// Original candidate role and bytes without scanning or reparsing them.
83    #[must_use]
84    pub fn candidate(
85        &self,
86        index: usize,
87    ) -> Option<(PathCertificate, &'v structured_pkix::Certificate<'d>)> {
88        candidate(self.path, index)
89    }
90    /// Only A.1.1(1–2): every existing anchor/CA certificate was referenced and
91    /// every entry excludes the signing certificate. This is NOT all Annex A
92    /// rules, presence, status, application trust or a baseline grade.
93    #[must_use]
94    pub fn root_path_rules_met(&self) -> bool {
95        self.root_rules
96    }
97}
98
99/// Actual selected-path reference report or the unchanged terminal cause.
100pub type RootPathReferenceResult<'p, 'v, 'd, X, E> = binary::BinaryResult<
101    RootPathReferences<'p, 'v, 'd, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation>,
102    X,
103    ReferenceDigestError<E>,
104>;
105
106fn candidate<'v, 'd>(
107    path: &'v CheckedPath<'d>,
108    mut ordinal: usize,
109) -> Option<(PathCertificate, &'v structured_pkix::Certificate<'d>)> {
110    if let Some(anchor) = path.anchor().certificate() {
111        if ordinal == 0 {
112            return Some((PathCertificate::Anchor, anchor));
113        }
114        ordinal -= 1;
115    }
116    path.members()
117        .get(ordinal)
118        .map(|member| (PathCertificate::Member(ordinal), member.certificate()))
119}
120
121fn candidate_ids<W: Work>(
122    path: &CheckedPath<'_>,
123    work: &W,
124) -> Result<OwnedVec<usize, W>, MemoryError> {
125    let mut ids = work.vector();
126    let mut index = work.map();
127    let mut ordinal = 0;
128    while let Some((_, certificate)) = candidate(path, ordinal) {
129        work.charge_work(1)?;
130        let bytes = certificate.as_bytes();
131        let identity = if let Some(&identity) = index.get(&bytes, work)? {
132            identity
133        } else {
134            index.try_insert(bytes, ordinal)?;
135            ordinal
136        };
137        ids.try_push(identity)?;
138        ordinal += 1;
139    }
140    Ok(ids)
141}
142
143struct PathComparisons<'provider, 'path, 'document, P: Digest<'provider>, W: Work> {
144    path: &'path CheckedPath<'document>,
145    memo: Memo<'provider, 'document, P, W>,
146    ids: Option<OwnedVec<usize, W>>,
147    covered: OwnedVec<bool, W>,
148    comparisons: OwnedVec<Agreement, W>,
149}
150impl<'provider, 'path, 'document, P: Digest<'provider>, W: Work>
151    PathComparisons<'provider, 'path, 'document, P, W>
152{
153    fn new(path: &'path CheckedPath<'document>, provider: &'provider P, work: &W) -> Self {
154        Self {
155            path,
156            memo: Memo::new(provider, work),
157            ids: None,
158            covered: work.vector(),
159            comparisons: work.vector(),
160        }
161    }
162    fn compare(
163        &mut self,
164        identity: crate::certificate::FormatDigest<P::Identity>,
165        expected: &[u8],
166        work: &W,
167    ) -> Result<(), digest::Error<structured_cms::stream::RangesError, P::Error>> {
168        let method = self.memo.method(identity, work)?;
169        if self.ids.is_none() {
170            let ids = candidate_ids(self.path, work).map_err(digest::Error::Resource)?;
171            let mut covered = work.vector();
172            covered
173                .try_reserve_exact(ids.len())
174                .map_err(digest::Error::Resource)?;
175            for _ in 0..ids.len() {
176                covered.try_push(false).map_err(digest::Error::Resource)?;
177            }
178            self.covered = covered;
179            self.ids = Some(ids);
180        }
181        let ids = self
182            .ids
183            .as_ref()
184            .expect("admitted original path candidates");
185        for ordinal in 0..ids.len() {
186            work.charge_work(1).map_err(digest::Error::Resource)?;
187            let (_, certificate) =
188                candidate(self.path, ordinal).expect("original immutable path candidate");
189            let agreement =
190                self.memo
191                    .compare(ids[ordinal], certificate.as_bytes(), method, expected, work)?;
192            if agreement == Agreement::Matches {
193                self.covered[ordinal] = true;
194            }
195            self.comparisons
196                .try_push(agreement)
197                .map_err(digest::Error::Resource)?;
198        }
199        Ok(())
200    }
201}
202
203/// Check the complete ordinary certificate reference catalogue against the
204/// SAME actually selected certificate path. A.1.1(1–2) requires anchor/CA
205/// references and forbids the signing certificate. Extra references can relate
206/// to other actual TSA/status paths; they are not rejected merely for differing
207/// from this root path. Unsupported comparisons cannot prove signer exclusion.
208/// Every reference shares the existing method/width/MD5/streaming core and one
209/// full-provider-identity/exact-certificate memo. No XML copy, re-encoding,
210/// acquisition, signature/path replay or new crypto dispatcher is needed.
211/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
212///
213/// # Panics
214/// Only if privately constructed original path/reference cardinality changes.
215pub fn check_root_certificate_references<
216    'p,
217    'v,
218    'd,
219    'q,
220    X: BinaryXml,
221    P: DigestProvider<'q, X>,
222    W: Work,
223>(
224    xml: &X,
225    prepared: &'p Prepared<X>,
226    selected: &CheckedPathCertificate<'v, 'd, X::Id, X::Document, X::Generation>,
227    provider: &'q P,
228    work: &W,
229) -> RootPathReferenceResult<'p, 'v, 'd, X, P::Error> {
230    let source = prepared.bound().containers();
231    let location = Location {
232        snapshot: source.snapshot(),
233        node: source.signature(),
234    };
235    let access = Access::new(xml, location.snapshot, work);
236    access
237        .query(location.node, |_, _, _| Ok(()))
238        .map_err(|cause| binary::Error::Access { location, cause })?;
239    if !core::ptr::eq(selected.selected().reference(), prepared.certificates()) {
240        return Err(binary::Error::Callback {
241            location,
242            cause: ReferenceDigestError::Foreign,
243        });
244    }
245    if !selected.is_bound() {
246        return Err(binary::Error::Callback {
247            location,
248            cause: ReferenceDigestError::UnboundPath,
249        });
250    }
251    let resource = |cause| binary::Error::Callback {
252        location,
253        cause: ReferenceDigestError::Digest(digest::Error::Resource(cause)),
254    };
255    let path = selected.digests().path();
256    let mut state = PathComparisons::<P, W>::new(path, provider, work);
257    let mut rows = work.vector();
258    let mut root_rules = true;
259    for (property, references) in prepared.validation_references().iter().enumerate() {
260        work.charge_work(1).map_err(resource)?;
261        if references.role() != unsigned::Role::CertificateReferences {
262            continue;
263        }
264        work.charge_work(
265            u64::try_from(state.covered.len()).map_err(|_| resource(MemoryError::WorkExceeded))?,
266        )
267        .map_err(resource)?;
268        state.covered.fill(false);
269        let mut signer_excluded = true;
270        for (entry, reference) in references.entries().iter().enumerate() {
271            work.charge_work(1).map_err(resource)?;
272            let (method, value) = reference.digest().expect("original mandatory CertDigest");
273            let start = state.comparisons.len();
274            let checked = digest::check_with(
275                xml,
276                digest::Reference {
277                    snapshot: references.snapshot(),
278                    method,
279                    value,
280                },
281                provider,
282                work,
283                |identity, expected, work| state.compare(identity, expected, work),
284            )
285            .map_err(|cause| cause.map_callback(ReferenceDigestError::Digest))?;
286            let search = match checked {
287                digest::Checked::Value(()) => {
288                    signer_excluded &= state.comparisons.last() == Some(&Agreement::Differs);
289                    ReferenceSearch::Compared
290                }
291                digest::Checked::Answer(answer) => {
292                    signer_excluded = false;
293                    ReferenceSearch::Unavailable(answer)
294                }
295            };
296            rows.try_push(RootReferenceRow {
297                property,
298                entry,
299                node: reference.node(),
300                search,
301                range: start..state.comparisons.len(),
302            })
303            .map_err(resource)?;
304        }
305        if state.covered.is_empty() {
306            root_rules = false;
307            continue;
308        }
309        let signer = state.covered.len() - 1;
310        for ordinal in 0..signer {
311            work.charge_work(1).map_err(resource)?;
312            root_rules &= state.covered[ordinal];
313        }
314        root_rules &= signer_excluded;
315    }
316    access
317        .query(location.node, |_, _, _| Ok(()))
318        .map_err(|cause| binary::Error::Access { location, cause })?;
319    Ok(RootPathReferences {
320        prepared,
321        path,
322        rows: rows.into_owned(),
323        comparisons: state.comparisons.into_owned(),
324        root_rules,
325    })
326}