Skip to main content

structured_xades/validation/
ref_catalogue.rs

1//! Complete digest relations over one original parsed material catalogue.
2
3use super::{ReferenceDigestError, ReferenceKind};
4use crate::{
5    binary::{self, BinaryXml, Location},
6    certificate::{Agreement, DigestProvider, FormatDigest},
7    digest,
8    material::{self, Retained},
9    prepare::{Preparation, Prepared},
10    timestamp::{CarriedMaterial, TokenSource},
11    xml::{Access, Xml},
12};
13use core::{cmp::Ordering, ops::Range};
14use structured_asn1::digest::{Digest, Resolution};
15use structured_asn1::memory::{HashWork, MemoryError, OwnedMap, OwnedVec, Work};
16use structured_cms::stream::RangesError;
17use structured_ltv::material::ArtifactSource;
18
19/// How one original reference participated in the complete digest search.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ReferenceSearch {
22    /// Every compatible original material source was compared. An empty set is
23    /// genuinely empty, not a successful digest match or proof of completeness.
24    Compared,
25    /// No OCSP digest was written; its identifier remains an obligation.
26    IdentifierOnly,
27    /// An open-format reference needs its format owner's interpretation.
28    OtherFormat,
29    /// Method or declared-value processing completed without a usable digest.
30    Unavailable(Agreement),
31}
32
33/// One original reference and its exact range in the comparison catalogue.
34pub struct ReferenceRow<N> {
35    property: usize,
36    entry: usize,
37    node: N,
38    search: ReferenceSearch,
39    range: Range<usize>,
40}
41
42impl<N: Copy> ReferenceRow<N> {
43    /// Property ordinal in the SAME preparation's complete reference catalogue.
44    #[must_use]
45    pub fn property(&self) -> usize {
46        self.property
47    }
48    /// Entry ordinal within that original property.
49    #[must_use]
50    pub fn entry(&self) -> usize {
51        self.entry
52    }
53    /// Original `Cert`, `CRLRef`, `OCSPRef` or open-format reference element.
54    #[must_use]
55    pub fn node(&self) -> N {
56        self.node
57    }
58    /// Whether digest search completed or another explicit obligation remains.
59    #[must_use]
60    pub fn search(&self) -> ReferenceSearch {
61        self.search
62    }
63}
64
65/// Completed digest fact for one original source, not path/authority agreement.
66#[derive(Debug, Clone, Copy)]
67pub struct MaterialDigest {
68    source: ArtifactSource,
69    embedded: Option<usize>,
70    agreement: Agreement,
71}
72
73impl MaterialDigest {
74    /// Ordinal in the SAME frozen material source catalogue retained by report.
75    #[must_use]
76    pub fn source(&self) -> Option<usize> {
77        match self.source {
78            ArtifactSource::Material(index) => Some(index),
79            _ => None,
80        }
81    }
82    /// Original pool/ordinal discriminator; token ordinals never address XML
83    /// sources. An embedded certificate uses its enclosing response's origin.
84    #[must_use]
85    pub fn origin(&self) -> ArtifactSource {
86        self.source
87    }
88    /// Original certificate ordinal inside this source's OCSP basic response.
89    /// None identifies the whole encapsulated certificate/CRL/OCSPResponse.
90    #[must_use]
91    pub fn embedded_certificate(&self) -> Option<usize> {
92        self.embedded
93    }
94    /// Exact full-output comparison or explicit unavailable capability.
95    #[must_use]
96    pub fn agreement(&self) -> Agreement {
97        self.agreement
98    }
99}
100
101/// All references against the complete parsed original XML material pool.
102/// Equal digests alone do not establish issuer/time/serial, actual actor use,
103/// applicability, authentication, required incorporation or a baseline grade.
104pub struct ReferenceDigests<'references, 'source, N, D, G> {
105    prepared: &'references Preparation<N, D, G>,
106    sources: &'source [Retained<N, D, G>],
107    tokens: Option<&'source [TokenSource<N, D, G>]>,
108    rows: OwnedVec<ReferenceRow<N>>,
109    comparisons: OwnedVec<MaterialDigest>,
110}
111
112impl<'references, 'source, N, D, G> ReferenceDigests<'references, 'source, N, D, G> {
113    /// Same complete original preparation, not a caller-selected property set.
114    #[must_use]
115    pub fn prepared(&self) -> &'references Preparation<N, D, G> {
116        self.prepared
117    }
118    /// Complete frozen source pool. OCSP entries retain whole `OCSPResponse` DER.
119    #[must_use]
120    pub fn sources(&self) -> &'source [Retained<N, D, G>] {
121        self.sources
122    }
123    /// Original additional token source pool, when included by the caller.
124    /// This alone does not assert whole-counter/token receipt completeness.
125    #[must_use]
126    pub fn tokens(&self) -> Option<&'source [TokenSource<N, D, G>]> {
127        self.tokens
128    }
129    /// Every original reference once, in original property order.
130    #[must_use]
131    pub fn rows(&self) -> &[ReferenceRow<N>] {
132        &self.rows
133    }
134    /// All compatible source comparisons for this original reference ordinal.
135    /// No first-match truncation; duplicates and completed mismatches survive.
136    #[must_use]
137    pub fn comparisons(&self, reference: usize) -> Option<&[MaterialDigest]> {
138        let row = self.rows.get(reference)?;
139        self.comparisons.get(row.range.clone())
140    }
141}
142
143/// Complete catalogue comparison with unchanged XML/provider/resource causes.
144pub type ReferenceCatalogueResult<'references, 'source, X, PE> = binary::BinaryResult<
145    ReferenceDigests<
146        'references,
147        'source,
148        <X as Xml>::Id,
149        <X as Xml>::Document,
150        <X as Xml>::Generation,
151    >,
152    X,
153    ReferenceDigestError<PE>,
154>;
155
156#[derive(Clone, Copy, PartialEq, Eq, Hash)]
157struct CacheKey {
158    source: usize,
159    method: usize,
160}
161
162#[derive(Clone, Copy, PartialEq, Eq, Hash)]
163struct SourceKey<'source> {
164    kind: material::Kind,
165    bytes: &'source [u8],
166}
167
168#[derive(Clone, Copy)]
169struct Candidate<'source> {
170    source: ArtifactSource,
171    embedded: Option<usize>,
172    kind: material::Kind,
173    bytes: &'source [u8],
174}
175
176fn candidates<'source, N: Copy, D: Copy, G: Copy, W: Work>(
177    view: &material::View<'source, N, D, G>,
178    tokens: Option<&CarriedMaterial<'source, N, D, G>>,
179    work: &W,
180) -> Result<OwnedVec<Candidate<'source>, W>, MemoryError> {
181    let mut values = work.vector();
182    values.try_reserve_exact(view.sources().len())?;
183    for (source, retained) in view.sources().iter().enumerate() {
184        work.charge_work(1)?;
185        values.try_push(Candidate {
186            source: ArtifactSource::Material(source),
187            embedded: None,
188            kind: retained.source().kind(),
189            bytes: retained.source().bytes(),
190        })?;
191    }
192    for basic in view.basic_responses() {
193        work.charge_work(1)?;
194        for (embedded, certificate) in basic.response().certificates().iter().enumerate() {
195            work.charge_work(1)?;
196            values.try_push(Candidate {
197                source: ArtifactSource::Material(basic.source_index()),
198                embedded: Some(embedded),
199                kind: material::Kind::Certificate,
200                bytes: certificate.as_bytes(),
201            })?;
202        }
203    }
204    if let Some(tokens) = tokens {
205        for (token, reading) in tokens.view.readings().iter().enumerate() {
206            work.charge_work(1)?;
207            for (certificate, value) in reading.token().message().certificates().iter().enumerate()
208            {
209                work.charge_work(1)?;
210                values.try_push(Candidate {
211                    source: ArtifactSource::TokenCertificate { token, certificate },
212                    embedded: None,
213                    kind: material::Kind::Certificate,
214                    bytes: value.as_bytes(),
215                })?;
216            }
217        }
218        for crl in tokens.crls() {
219            work.charge_work(1)?;
220            values.try_push(Candidate {
221                source: ArtifactSource::TokenRevocation(crl.origin),
222                embedded: None,
223                kind: material::Kind::Crl,
224                bytes: crl.value.as_bytes(),
225            })?;
226        }
227        for response in tokens.responses() {
228            work.charge_work(1)?;
229            values.try_push(Candidate {
230                source: ArtifactSource::TokenRevocation(response.origin),
231                embedded: None,
232                kind: material::Kind::Ocsp,
233                bytes: response.value.as_bytes(),
234            })?;
235        }
236        for basic in tokens.basic_responses() {
237            work.charge_work(1)?;
238            for (embedded, certificate) in basic.value.certificates().iter().enumerate() {
239                work.charge_work(1)?;
240                values.try_push(Candidate {
241                    source: ArtifactSource::TokenRevocation(basic.origin),
242                    embedded: Some(embedded),
243                    kind: material::Kind::Certificate,
244                    bytes: certificate.as_bytes(),
245                })?;
246            }
247        }
248    }
249    Ok(values)
250}
251
252impl HashWork for SourceKey<'_> {
253    fn hash_work(&self) -> Result<u64, MemoryError> {
254        u64::try_from(self.bytes.len())
255            .map_err(|_| MemoryError::WorkExceeded)?
256            .checked_add(2)
257            .ok_or(MemoryError::WorkExceeded)
258    }
259}
260
261fn source_ids<W: Work>(
262    sources: &[Candidate<'_>],
263    work: &W,
264) -> Result<OwnedVec<usize, W>, MemoryError> {
265    let mut index = work.map();
266    let mut ids = work.vector();
267    ids.try_reserve_exact(sources.len())?;
268    for (ordinal, source) in sources.iter().enumerate() {
269        work.charge_work(1)?;
270        let key = SourceKey {
271            kind: source.kind,
272            bytes: source.bytes,
273        };
274        let canonical = if let Some(&existing) = index.get(&key, work)? {
275            existing
276        } else {
277            index.try_insert(key, ordinal)?;
278            ordinal
279        };
280        ids.try_push(canonical)?;
281    }
282    Ok(ids)
283}
284
285impl HashWork for CacheKey {
286    fn hash_work(&self) -> Result<u64, MemoryError> {
287        Ok(2)
288    }
289}
290
291pub(super) struct Memo<'provider, 'source, P: Digest<'provider>, W: Work> {
292    provider: &'provider P,
293    identities: OwnedVec<FormatDigest<P::Identity>, W>,
294    entries: OwnedMap<CacheKey, Option<Resolution<P::Output>>, W>,
295    sources: Option<OwnedVec<usize, W>>,
296    candidates: Option<OwnedVec<Candidate<'source>, W>>,
297}
298
299#[derive(Clone, Copy)]
300pub(super) struct CandidatePools<'view, 'source, N, D, G> {
301    pub(super) xml: &'view material::View<'source, N, D, G>,
302    pub(super) tokens: Option<&'view CarriedMaterial<'source, N, D, G>>,
303}
304
305impl<N: Copy + Eq, D: Copy + Eq, G: Copy + Eq> CandidatePools<'_, '_, N, D, G> {
306    pub(super) fn same_source<X: Xml<Id = N, Document = D, Generation = G>, W: Work>(
307        self,
308        location: Location<N, D, G>,
309        access: &Access<'_, X, W>,
310    ) -> Result<bool, MemoryError> {
311        if self.xml.snapshot() != location.snapshot {
312            return Ok(false);
313        }
314        if let Some(tokens) = self.tokens {
315            if tokens.view.snapshot() != location.snapshot {
316                return Ok(false);
317            }
318            if !access.equal_id(tokens.view.signature(), location.node)? {
319                return Ok(false);
320            }
321        }
322        access.equal_id(self.xml.signature(), location.node)
323    }
324}
325
326impl<'provider, 'source, P: Digest<'provider>, W: Work> Memo<'provider, 'source, P, W> {
327    pub(super) fn new(provider: &'provider P, work: &W) -> Self {
328        Self {
329            provider,
330            identities: work.vector(),
331            entries: work.map(),
332            sources: None,
333            candidates: None,
334        }
335    }
336    fn row<N: Copy, D: Copy, G: Copy>(
337        &mut self,
338        identity: FormatDigest<P::Identity>,
339        expected: &[u8],
340        kind: material::Kind,
341        pools: CandidatePools<'_, 'source, N, D, G>,
342        comparisons: &mut OwnedVec<MaterialDigest, W>,
343        work: &W,
344    ) -> Result<(), digest::Error<RangesError, P::Error>> {
345        let method = self.method(identity, work)?;
346        if self.sources.is_none() {
347            let values =
348                candidates(pools.xml, pools.tokens, work).map_err(digest::Error::Resource)?;
349            self.sources = Some(source_ids(&values, work).map_err(digest::Error::Resource)?);
350            self.candidates = Some(values);
351        }
352        let count = self
353            .candidates
354            .as_ref()
355            .expect("prepared original candidates")
356            .len();
357        for ordinal in 0..count {
358            work.charge_work(1).map_err(digest::Error::Resource)?;
359            let candidate = self
360                .candidates
361                .as_ref()
362                .expect("prepared original candidates")[ordinal];
363            if candidate.kind != kind {
364                continue;
365            }
366            comparisons
367                .try_reserve(1)
368                .map_err(digest::Error::Resource)?;
369            let canonical = self
370                .sources
371                .as_ref()
372                .expect("prepared original source identities")[ordinal];
373            let agreement = self.compare(canonical, candidate.bytes, method, expected, work)?;
374            comparisons
375                .try_push(MaterialDigest {
376                    source: candidate.source,
377                    embedded: candidate.embedded,
378                    agreement,
379                })
380                .map_err(digest::Error::Resource)?;
381        }
382        Ok(())
383    }
384
385    pub(super) fn method(
386        &mut self,
387        identity: FormatDigest<P::Identity>,
388        work: &W,
389    ) -> Result<usize, digest::Error<RangesError, P::Error>> {
390        for (index, existing) in self.identities.iter().enumerate() {
391            work.charge_work(1).map_err(digest::Error::Resource)?;
392            if self
393                .provider
394                .compare(identity.identity(), existing.identity(), work)
395                .map_err(digest::Error::Provider)?
396                == Ordering::Equal
397            {
398                return Ok(index);
399            }
400        }
401        let index = self.identities.len();
402        self.identities
403            .try_push(identity)
404            .map_err(digest::Error::Resource)?;
405        Ok(index)
406    }
407
408    pub(super) fn compare(
409        &mut self,
410        source: usize,
411        bytes: &[u8],
412        method: usize,
413        expected: &[u8],
414        work: &W,
415    ) -> Result<Agreement, digest::Error<RangesError, P::Error>> {
416        let key = CacheKey { source, method };
417        if let Some(output) = self
418            .entries
419            .get(&key, work)
420            .map_err(digest::Error::Resource)?
421        {
422            let output = output.as_ref().expect("completed prior cache entry");
423            return self.matches(&self.identities[method], output, expected, work);
424        }
425        // Admission precedes backend allocation and consuming the exact source.
426        self.entries
427            .try_insert(key, None)
428            .map_err(digest::Error::Resource)?;
429        let identity = &self.identities[method];
430        let output =
431            digest::compute_resolved::<P, _, W>(identity, &mut [bytes], self.provider, work)?;
432        let answer = self.matches(identity, &output, expected, work)?;
433        *self
434            .entries
435            .get_mut(&key, work)
436            .map_err(digest::Error::Resource)?
437            .expect("admitted cache entry") = Some(output);
438        Ok(answer)
439    }
440
441    fn matches(
442        &self,
443        identity: &FormatDigest<P::Identity>,
444        output: &Resolution<P::Output>,
445        expected: &[u8],
446        work: &W,
447    ) -> Result<Agreement, digest::Error<RangesError, P::Error>> {
448        let output = match output {
449            Resolution::Supported(output) => output,
450            Resolution::Unsupported => return Ok(Agreement::Unsupported),
451            Resolution::Malformed => return Ok(Agreement::MalformedMethod),
452        };
453        work.charge_work(1).map_err(digest::Error::Resource)?;
454        self.provider
455            .matches(identity.identity(), output, expected, work)
456            .map(|same| {
457                if same {
458                    Agreement::Matches
459                } else {
460                    Agreement::Differs
461                }
462            })
463            .map_err(digest::Error::Provider)
464    }
465}
466
467/// Compare EVERY prepared reference with EVERY compatible original parsed
468/// material view. Hashes are memoized by exact kind/bytes and complete resolved
469/// provider identity, never by width, URI spelling or unauthenticated hint.
470/// EN319132-1 Annex A.1.1–A.1.4 digest whole certificate/CRL/OCSPResponse DER.
471/// <https://www.etsi.org/deliver/etsi_en/319100_319199/31913201/01.03.01_60/en_31913201v010301p.pdf>
472///
473/// Uses the same method/width/MD5 and streaming kernels as single comparisons.
474/// No DER reparse/re-encoding, XML acquisition, normalization or provider change.
475/// Each output is finalized once per identity/artifact, then dropped with scoped
476/// scratch. Refusal drops the entire partial report; arbitrary provider side
477/// effects are not rolled back. Token-carried material is a separate source pool.
478pub fn compare_reference_catalogues<
479    'references,
480    'source,
481    'p,
482    X: BinaryXml,
483    P: DigestProvider<'p, X>,
484    W: Work,
485>(
486    xml: &X,
487    prepared: &'references Prepared<X>,
488    view: &material::View<'source, X::Id, X::Document, X::Generation>,
489    provider: &'p P,
490    work: &W,
491) -> ReferenceCatalogueResult<'references, 'source, X, P::Error> {
492    compare_catalogues(xml, prepared, view, None, provider, work)
493}
494
495/// Include original timestamp-carried certificates, complete/delta CRLs and
496/// WHOLE OCSP envelopes in the same paid candidate/hash memo. Equal DER copied
497/// between XML and tokens shares one digest output while every location stays
498/// in the report. Embedded OCSP certificates keep their enclosing token/choice.
499/// No source is reparsed or copied. Both pools must belong to the same original
500/// generation/selected signature before any allocation/provider work.
501///
502/// The carried pool's root/counter scope is unchanged; whole-signature level
503/// assembly must still require the complete timestamp participant receipt.
504pub fn compare_reference_catalogues_with_tokens<
505    'references,
506    'source,
507    'p,
508    X: BinaryXml,
509    P: DigestProvider<'p, X>,
510    W: Work,
511>(
512    xml: &X,
513    prepared: &'references Prepared<X>,
514    view: &material::View<'source, X::Id, X::Document, X::Generation>,
515    tokens: &CarriedMaterial<'source, X::Id, X::Document, X::Generation>,
516    provider: &'p P,
517    work: &W,
518) -> ReferenceCatalogueResult<'references, 'source, X, P::Error> {
519    compare_catalogues(xml, prepared, view, Some(tokens), provider, work)
520}
521
522fn compare_catalogues<'references, 'source, 'p, X: BinaryXml, P: DigestProvider<'p, X>, W: Work>(
523    xml: &X,
524    prepared: &'references Prepared<X>,
525    view: &material::View<'source, X::Id, X::Document, X::Generation>,
526    tokens: Option<&CarriedMaterial<'source, X::Id, X::Document, X::Generation>>,
527    provider: &'p P,
528    work: &W,
529) -> ReferenceCatalogueResult<'references, 'source, X, P::Error> {
530    let original = prepared.bound().containers();
531    let location = Location {
532        snapshot: original.snapshot(),
533        node: original.signature(),
534    };
535    let access = Access::new(xml, location.snapshot, work);
536    access
537        .query(location.node, |_, _, _| Ok(()))
538        .map_err(|cause| binary::Error::Access { location, cause })?;
539    if !(CandidatePools { xml: view, tokens })
540        .same_source(location, &access)
541        .map_err(|cause| binary::Error::Callback {
542            location,
543            cause: ReferenceDigestError::Digest(digest::Error::Resource(cause)),
544        })?
545    {
546        return Err(binary::Error::Callback {
547            location,
548            cause: ReferenceDigestError::Foreign,
549        });
550    }
551    let resource = |cause| binary::Error::Callback {
552        location,
553        cause: ReferenceDigestError::Digest(digest::Error::Resource(cause)),
554    };
555    let mut memo = Memo::<P, W>::new(provider, work);
556    let mut rows = work.vector();
557    let mut comparisons = work.vector();
558    for (property, references) in prepared.validation_references().iter().enumerate() {
559        work.charge_work(1).map_err(resource)?;
560        rows.try_reserve_exact(references.entries().len())
561            .map_err(resource)?;
562        for (ordinal, entry) in references.entries().iter().enumerate() {
563            work.charge_work(1).map_err(resource)?;
564            let start = comparisons.len();
565            let kind = match entry.kind() {
566                ReferenceKind::Certificate => Some(material::Kind::Certificate),
567                ReferenceKind::Crl => Some(material::Kind::Crl),
568                ReferenceKind::Ocsp => Some(material::Kind::Ocsp),
569                ReferenceKind::Other => None,
570            };
571            let search = if let Some(kind) = kind {
572                if let Some((method, value)) = entry.digest() {
573                    let reading = digest::check_with(
574                        xml,
575                        digest::Reference {
576                            snapshot: references.snapshot(),
577                            method,
578                            value,
579                        },
580                        provider,
581                        work,
582                        |identity, expected, work| {
583                            memo.row(
584                                identity,
585                                expected,
586                                kind,
587                                CandidatePools { xml: view, tokens },
588                                &mut comparisons,
589                                work,
590                            )
591                        },
592                    )
593                    .map_err(|cause| cause.map_callback(ReferenceDigestError::Digest))?;
594                    match reading {
595                        digest::Checked::Value(()) => ReferenceSearch::Compared,
596                        digest::Checked::Answer(answer) => ReferenceSearch::Unavailable(answer),
597                    }
598                } else {
599                    ReferenceSearch::IdentifierOnly
600                }
601            } else {
602                ReferenceSearch::OtherFormat
603            };
604            rows.try_push(ReferenceRow {
605                property,
606                entry: ordinal,
607                node: entry.node(),
608                search,
609                range: start..comparisons.len(),
610            })
611            .map_err(resource)?;
612        }
613    }
614    access
615        .query(location.node, |_, _, _| Ok(()))
616        .map_err(|cause| binary::Error::Access { location, cause })?;
617    Ok(ReferenceDigests {
618        prepared,
619        sources: view.sources(),
620        tokens: tokens.map(|carried| carried.view.sources()),
621        rows: rows.into_owned(),
622        comparisons: comparisons.into_owned(),
623    })
624}