Skip to main content

structured_cades/
material.rs

1//! Original root/token validation material, parsed once and indexed by the
2//! existing exact-artifact LTV core. Reading never authenticates a status.
3
4use crate::{
5    Cades, Error, ISSUER,
6    prepare::Preparation,
7    source::{DocumentId, Node, Original},
8};
9use structured_asn1::{
10    Reader, Tag,
11    memory::{OwnedVec, Work},
12    universal,
13};
14use structured_cms::{
15    SignedData,
16    signed::{CertificateChoice, OtherRevocationInfo},
17    timestamp::Token,
18};
19use structured_ltv::{
20    material::{self, ArtifactRef, ArtifactSource, CarriedOrigin, Occurrence},
21    source::Query,
22};
23use structured_pkix::{crl::CertificateList, ocsp::BasicResponse};
24
25mod checks;
26pub mod credentials;
27mod legacy;
28pub use checks::{CheckError, Checks, Finding, check_participants};
29
30/// Refusal to construct material scope for an actual checked archive owner.
31#[derive(Debug)]
32pub enum ArchiveIndexError {
33    /// Negative timestamp facts cannot establish an archive's owner.
34    Unproven,
35    /// Original source, current query or indexed storage refused.
36    Index(material::IndexError<Node>),
37    /// The archive's exact token/coverage/source binding refused.
38    Coverage(material::CoverageError<Node>),
39}
40
41/// Exact format-defined validation-values placement, without inventing a
42/// root certificate CHOICE or a whole OCSP envelope around a basic body.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct LegacyLocation {
45    /// Actual signer which carries the attribute (including TSA signers).
46    pub signature: Node,
47    /// Original certificate/revocation-values attribute.
48    pub property: Node,
49    /// Member ordinal within that attribute's applicable SEQUENCE OF.
50    pub member: usize,
51    /// Actual outer timestamp carrier, or root/counter placement.
52    pub token: Option<usize>,
53    /// Observed unsigned placement, never a caller-configurable allowance.
54    pub unsigned: bool,
55}
56
57/// Real original material container; an OCSP envelope and the basic body it
58/// carries are never confused with an invented envelope around supplied bytes.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum Placement {
61    /// Original root `SignedData` certificate CHOICE ordinal.
62    RootCertificate(usize),
63    /// Original root `SignedData` revocation CHOICE ordinal.
64    RootRevocation(usize),
65    /// Certificate CHOICE of one original root/counter timestamp token.
66    TokenCertificate {
67        /// Original preparation timestamp ordinal.
68        token: usize,
69        /// Original certificate CHOICE ordinal.
70        choice: usize,
71    },
72    /// Revocation CHOICE of one original root/counter timestamp token.
73    TokenRevocation {
74        /// Original preparation timestamp ordinal.
75        token: usize,
76        /// Original revocation CHOICE ordinal.
77        choice: usize,
78    },
79    /// Actual Annex A certificate-values member.
80    CertificateValues(LegacyLocation),
81    /// Actual complete/delta CRL in Annex A revocation-values.
82    CrlValues(LegacyLocation),
83    /// Actual bare `BasicOCSPResponse` in Annex A revocation-values.
84    OcspValues(LegacyLocation),
85    /// Actual OID-owned open values in Annex A revocation-values.
86    OtherRevocationValues(LegacyLocation),
87}
88
89/// Uninterpreted material remains available to its owner. A known syntax
90/// failure retains its original cause rather than becoming an unknown OID.
91pub struct Open<'a> {
92    /// Untouched original encoding, retaining unknown values without reparse.
93    pub element: structured_asn1::Element<'a>,
94    /// Whole physical original enclosure and protecting document identity.
95    pub node: Node,
96    /// Original container placement, independently of usability or coverage.
97    pub placement: Placement,
98    /// An unknown format has no schema failure; a known malformed value does.
99    pub cause: Option<Error>,
100}
101
102struct Crl<'d> {
103    value: CertificateList<'d>,
104    node: Node,
105    placement: Placement,
106}
107struct Ocsp<'d> {
108    value: BasicResponse<'d>,
109    node: Node,
110    placement: Placement,
111}
112
113/// Complete original source pool. Every readable timestamp envelope is held
114/// once; CRL/OCSP models are parsed once, and index creation never replays them.
115pub struct Pool<'prepared, 'model, 'document> {
116    prepared: &'prepared Preparation<'model, 'document>,
117    tokens: &'prepared [crate::prepare::tokens::Model<'document>],
118    token_index: &'prepared [Option<usize>],
119    token_faults: &'prepared [(Node, crate::prepare::ReadError)],
120    crls: OwnedVec<Crl<'document>>,
121    ocsp: OwnedVec<Ocsp<'document>>,
122    open: OwnedVec<Open<'document>>,
123    related: OwnedVec<legacy::Related<'document>>,
124    legacy_slots: Option<legacy::Slots>,
125}
126
127/// Existing exact-artifact core in this original `CAdES` identity domain.
128pub type Index<'source, 'model, 'document, W> = material::Index<
129    'source,
130    Node,
131    DocumentId<'model, 'document>,
132    (),
133    Placement,
134    Open<'document>,
135    W,
136    Cades,
137>;
138
139impl<'p, 'm, 'd> Pool<'p, 'm, 'd> {
140    /// Read the entire original source pool, preserving unreadable token and
141    /// known material findings. Resource/depth/processing refusal is terminal,
142    /// releases partial models and never returns a successful partial pool.
143    pub fn read(prepared: &'p Preparation<'m, 'd>, operation: &Reader<'d>) -> crate::Result<Self> {
144        prepared
145            .document()
146            .message()
147            .require_session(operation, "cades.material")?;
148        let work = operation.execution();
149        let tokens = &*prepared.parsed.models;
150        let mut crls = work.vector();
151        let mut ocsp = work.vector();
152        let mut open = work.vector();
153        collect(
154            prepared.document().message(),
155            None,
156            operation,
157            &mut crls,
158            &mut ocsp,
159            &mut open,
160        )?;
161        for model in tokens {
162            work.charge_work(1).map_err(|cause| {
163                Error::resource(cause, "cades.material", model.token.message().span().start)
164            })?;
165            collect(
166                model.token.message(),
167                Some(model.ordinal),
168                operation,
169                &mut crls,
170                &mut ocsp,
171                &mut open,
172            )?;
173        }
174        let related = legacy::read_related(prepared, operation)?;
175        legacy::append_open(prepared, tokens, &related, &mut open, &work)?;
176        let mut out = Self {
177            prepared,
178            tokens,
179            token_index: &prepared.parsed.ordinals,
180            token_faults: &prepared.parsed.faults,
181            crls: crls.into_owned(),
182            ocsp: ocsp.into_owned(),
183            open: open.into_owned(),
184            related,
185            legacy_slots: None,
186        };
187        out.legacy_slots = legacy::slots(&out, &work)?;
188        Ok(out)
189    }
190    /// Same source preparation; the pool cannot be relabelled to another root.
191    #[must_use]
192    pub fn prepared(&self) -> &'p Preparation<'m, 'd> {
193        self.prepared
194    }
195    /// Every unreadable original timestamp and its retained failure.
196    #[must_use]
197    pub fn token_faults(&self) -> &[(Node, crate::prepare::ReadError)] {
198        self.token_faults
199    }
200    /// Every original unknown or known-malformed material enclosure.
201    #[must_use]
202    pub fn open(&self) -> &[Open<'d>] {
203        &self.open
204    }
205    /// Number of original readable complete/delta CRL values, without parsing.
206    #[must_use]
207    pub fn crls(&self) -> usize {
208        self.crls.len()
209            + self
210                .legacy_slots
211                .as_ref()
212                .map_or(0, |slots| slots.crls.len())
213    }
214    /// One original parsed CRL under current metadata admission. Authentication
215    /// and status processing reuse this model; they must not reparse its bytes.
216    pub fn crl<W: Work>(
217        &self,
218        ordinal: usize,
219        work: &W,
220    ) -> crate::Result<Option<&CertificateList<'d>>> {
221        work.charge_work(1).map_err(|cause| {
222            Error::resource(
223                cause,
224                "cades.material.crl",
225                self.prepared.signer().span().start,
226            )
227        })?;
228        if ordinal < self.crls.len() {
229            return Ok(Some(&self.crls[ordinal].value));
230        }
231        legacy::crl(self, ordinal - self.crls.len(), work)
232    }
233    /// Number of original readable basic responses in their real wire format.
234    #[must_use]
235    pub fn ocsp_responses(&self) -> usize {
236        self.ocsp.len()
237            + self
238                .legacy_slots
239                .as_ref()
240                .map_or(0, |slots| slots.ocsp.len())
241    }
242    /// One first-read basic response, from a whole RFC5940 envelope or actual
243    /// Annex A revocation-values. The index retains its real placement.
244    pub fn ocsp<W: Work>(
245        &self,
246        ordinal: usize,
247        work: &W,
248    ) -> crate::Result<Option<&BasicResponse<'d>>> {
249        work.charge_work(1).map_err(|cause| {
250            Error::resource(
251                cause,
252                "cades.material.ocsp",
253                self.prepared.signer().span().start,
254            )
255        })?;
256        if ordinal < self.ocsp.len() {
257            return Ok(Some(&self.ocsp[ordinal].value));
258        }
259        legacy::ocsp(self, ordinal - self.ocsp.len(), work)
260    }
261    /// Original timestamp envelopes, indexed by original preparation ordinal.
262    /// This paid query reuses the parsed envelope rather than reparsing it.
263    pub fn token<W: Work>(&self, ordinal: usize, work: &W) -> crate::Result<Option<&Token<'d>>> {
264        work.charge_work(1).map_err(|cause| {
265            Error::resource(cause, "cades.material", self.prepared.signer().span().start)
266        })?;
267        let Some(Some(index)) = self.token_index.get(ordinal) else {
268            return Ok(None);
269        };
270        debug_assert_eq!(self.tokens[*index].ordinal, ordinal);
271        Ok(Some(&self.tokens[*index].token))
272    }
273    /// Visit a previously parsed original token with the current operation.
274    /// Uses its saved parser/encoding/depth, with no second envelope parse.
275    pub fn with_token<R, E>(
276        &self,
277        ordinal: usize,
278        operation: &Reader<'d>,
279        visitor: impl for<'scope> FnOnce(crate::timestamp::Context<'scope, 'm, 'd>) -> Result<R, E>,
280    ) -> Result<R, crate::timestamp::VisitError<E>> {
281        operation.charge(1).map_err(|cause| {
282            crate::timestamp::VisitError::Processing(Error::encoding(cause, "cades.timestamp"))
283        })?;
284        let entry = self.prepared.timestamps().get(ordinal).ok_or_else(|| {
285            crate::timestamp::VisitError::Processing(Error::Foreign {
286                at: self.prepared.signer().span().start,
287            })
288        })?;
289        let token = self
290            .token(ordinal, &operation.execution())
291            .map_err(crate::timestamp::VisitError::Processing)?
292            .ok_or_else(|| crate::timestamp::VisitError::Unreadable {
293                property: entry.property(),
294            })?;
295        crate::timestamp::with_existing(self.prepared, ordinal, token, operation, visitor)
296    }
297    /// Build material scope for the actual successful archive owner, including
298    /// a historical counter or parallel root. Sources are the same original
299    /// pool; no signature is replayed and no parsed artifact is cloned.
300    pub fn index_for_archive<'source, Q>(
301        &'source self,
302        archive: crate::timestamp::ArchiveView<'_, 'm, 'd, '_>,
303        query: &Q,
304    ) -> Result<Index<'source, 'm, 'd, Q::Work>, ArchiveIndexError>
305    where
306        Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
307        'd: 'source,
308    {
309        let actor = archive
310            .timestamp()
311            .actor()
312            .ok_or(ArchiveIndexError::Unproven)?;
313        let evidence = actor.verified();
314        let snapshot = self
315            .prepared
316            .document()
317            .locate(self.prepared.signer().span())
318            .snapshot;
319        if evidence.token().snapshot != snapshot || query.snapshot() != snapshot {
320            return Err(ArchiveIndexError::Index(material::IndexError::Foreign {
321                signature: evidence.signature(),
322            }));
323        }
324        archive
325            .coverage()
326            .require_evidence(evidence, archive.coverage().subject(), query)
327            .map_err(ArchiveIndexError::Coverage)?;
328        self.index_for_signature(evidence.signature(), query)
329            .map_err(ArchiveIndexError::Index)
330    }
331
332    fn index_for_signature<'source, Q>(
333        &'source self,
334        signature: Node,
335        query: &Q,
336    ) -> Result<Index<'source, 'm, 'd, Q::Work>, material::IndexError<Node>>
337    where
338        Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
339        'd: 'source,
340    {
341        let mut index = Index::new(ISSUER, signature, &self.open, query)?;
342        certificates(
343            self.prepared.document().message(),
344            None,
345            self.prepared,
346            &mut index,
347            query,
348        )?;
349        for token in self.tokens {
350            query
351                .admit(signature, 1)
352                .map_err(material::IndexError::Scope)?;
353            certificates(
354                token.token.message(),
355                Some(token.ordinal),
356                self.prepared,
357                &mut index,
358                query,
359            )?;
360        }
361        for crl in &self.crls {
362            query
363                .admit(signature, 1)
364                .map_err(material::IndexError::Scope)?;
365            index.insert(
366                ISSUER,
367                ArtifactRef::Crl(&crl.value),
368                revocation_occurrence(self.prepared, crl.node, crl.placement)?,
369                query,
370            )?;
371        }
372        for response in &self.ocsp {
373            query
374                .admit(signature, 1)
375                .map_err(material::IndexError::Scope)?;
376            index.insert(
377                ISSUER,
378                ArtifactRef::Ocsp(&response.value),
379                revocation_occurrence(self.prepared, response.node, response.placement)?,
380                query,
381            )?;
382        }
383        legacy::index(self, &mut index, query)?;
384        Ok(index)
385    }
386
387    /// Build the one exact-artifact index from actual root/token locations.
388    /// Models remain borrowed; no certificate/CRL/status byte copy or parser
389    /// replay occurs. The current query funds all new hashing/probing/storage.
390    pub fn index<'source, Q>(
391        &'source self,
392        query: &Q,
393    ) -> Result<Index<'source, 'm, 'd, Q::Work>, material::IndexError<Node>>
394    where
395        Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
396        'd: 'source,
397    {
398        let signature = self
399            .prepared
400            .document()
401            .locate(self.prepared.signer().span());
402        if query.snapshot() != signature.snapshot {
403            return Err(material::IndexError::Foreign {
404                signature: signature.node,
405            });
406        }
407        self.index_for_signature(signature.node, query)
408    }
409}
410
411fn certificates<'s, 'm, 'd, Q>(
412    message: &'s SignedData<'d>,
413    token: Option<usize>,
414    prepared: &Preparation<'m, 'd>,
415    index: &mut Index<'s, 'm, 'd, Q::Work>,
416    query: &Q,
417) -> Result<(), material::IndexError<Node>>
418where
419    Q: Query<Id = Node, Document = DocumentId<'m, 'd>, Generation = ()>,
420    'd: 's,
421{
422    for (choice, entry) in message.certificate_choices().iter().enumerate() {
423        index.admit(query)?;
424        if let CertificateChoice::X509(ordinal) = entry {
425            let value = &message.certificates()[*ordinal];
426            let placement = match token {
427                Some(token) => Placement::TokenCertificate { token, choice },
428                None => Placement::RootCertificate(choice),
429            };
430            let source = match token {
431                Some(token) => ArtifactSource::TokenCertificate {
432                    token,
433                    certificate: *ordinal,
434                },
435                None => ArtifactSource::Material(choice),
436            };
437            let enclosure = if let Some(token) = token {
438                token_enclosure(prepared, token)?
439            } else {
440                Node::of(value.span())
441            };
442            index.insert(
443                ISSUER,
444                ArtifactRef::Certificate(value),
445                Occurrence::new(
446                    source,
447                    prepared.document().locate(enclosure.span()),
448                    placement,
449                ),
450                query,
451            )?;
452        }
453    }
454    Ok(())
455}
456
457fn token_enclosure(
458    prepared: &Preparation<'_, '_>,
459    token: usize,
460) -> Result<Node, material::IndexError<Node>> {
461    let Some(value) = prepared
462        .timestamps()
463        .get(token)
464        .and_then(crate::prepare::Timestamp::value)
465    else {
466        return Err(material::IndexError::Foreign {
467            signature: Node::of(prepared.signer().span()),
468        });
469    };
470    Ok(Node::of(value.span()))
471}
472
473fn revocation_occurrence<'m, 'd>(
474    prepared: &Preparation<'m, 'd>,
475    node: Node,
476    placement: Placement,
477) -> Result<Occurrence<Node, DocumentId<'m, 'd>, (), Placement>, material::IndexError<Node>> {
478    let signature = Node::of(prepared.signer().span());
479    let (source, enclosure) = match placement {
480        Placement::RootRevocation(ordinal) => {
481            let ordinal = prepared
482                .document()
483                .message()
484                .certificate_choices()
485                .len()
486                .checked_add(ordinal)
487                .ok_or(material::IndexError::Resource {
488                    signature,
489                    cause: structured_asn1::memory::MemoryError::CapacityOverflow,
490                })?;
491            (ArtifactSource::Material(ordinal), node)
492        }
493        Placement::TokenRevocation { token, choice } => (
494            ArtifactSource::TokenRevocation(CarriedOrigin { token, choice }),
495            token_enclosure(prepared, token)?,
496        ),
497        Placement::RootCertificate(_)
498        | Placement::TokenCertificate { .. }
499        | Placement::CertificateValues(_)
500        | Placement::CrlValues(_)
501        | Placement::OcspValues(_)
502        | Placement::OtherRevocationValues(_) => {
503            return Err(material::IndexError::Foreign { signature });
504        }
505    };
506    // The real token carrier is the whole-item archive coverage boundary.
507    // ArtifactSource+Placement retain the exact inner value ordinal; no ASN.1
508    // inner offset is passed off as a separate top-level source or pool slot.
509    let location = Original {
510        snapshot: prepared.document().locate(enclosure.span()).snapshot,
511        node: enclosure,
512    };
513    Ok(Occurrence::new(source, location, placement))
514}
515
516fn collect<'d, W: Work>(
517    message: &SignedData<'d>,
518    token: Option<usize>,
519    operation: &Reader<'d>,
520    crls: &mut OwnedVec<Crl<'d>, W>,
521    ocsp: &mut OwnedVec<Ocsp<'d>, W>,
522    open: &mut OwnedVec<Open<'d>, W>,
523) -> crate::Result<()> {
524    for (choice, certificate) in message.certificate_choices().iter().enumerate() {
525        operation
526            .charge(1)
527            .map_err(|cause| Error::encoding(cause, "cades.material"))?;
528        let element = match certificate {
529            CertificateChoice::X509(_) => continue,
530            CertificateChoice::Extended(element)
531            | CertificateChoice::AttributeV1(element)
532            | CertificateChoice::AttributeV2(element)
533            | CertificateChoice::Other(element) => *element,
534        };
535        let placement = match token {
536            Some(token) => Placement::TokenCertificate { token, choice },
537            None => Placement::RootCertificate(choice),
538        };
539        push_open(open, element, placement, None)?;
540    }
541    for (choice, element) in message.revocations().iter().enumerate() {
542        operation
543            .charge(1)
544            .map_err(|cause| Error::encoding(cause, "cades.material"))?;
545        let node = Node::of(element.span());
546        let placement = match token {
547            Some(token) => Placement::TokenRevocation { token, choice },
548            None => Placement::RootRevocation(choice),
549        };
550        let reader = message
551            .revocation_reader_in(operation, choice)?
552            .ok_or(Error::Foreign {
553                at: element.span().start,
554            })?;
555        let result = if element.tag() == Tag::constructed(universal::SEQUENCE) {
556            CertificateList::read_element(&reader, element)
557                .map_err(Error::Pkix)
558                .and_then(|value| {
559                    crls.try_push(Crl {
560                        value,
561                        node,
562                        placement,
563                    })
564                    .map_err(|cause| Error::resource(cause, "cades.material", element.span().start))
565                })
566        } else {
567            match OtherRevocationInfo::read(&reader, element)
568                .map_err(Error::Cms)
569                .and_then(|other| other.ocsp().map_err(Error::Cms))
570            {
571                Ok(Some(response)) => match response.basic_in(operation).map_err(Error::Pkix) {
572                    Ok(Some(value)) => ocsp
573                        .try_push(Ocsp {
574                            value,
575                            node,
576                            placement,
577                        })
578                        .map_err(|cause| {
579                            Error::resource(cause, "cades.material", element.span().start)
580                        }),
581                    Ok(None) => push_open(open, *element, placement, None),
582                    Err(cause) => Err(cause),
583                },
584                Ok(None) => push_open(open, *element, placement, None),
585                Err(cause) => Err(cause),
586            }
587        };
588        if let Err(cause) = result {
589            if cause.is_terminal() {
590                return Err(cause);
591            }
592            push_open(open, *element, placement, Some(cause))?;
593        }
594    }
595    Ok(())
596}
597
598fn push_open<'a, W: Work>(
599    out: &mut OwnedVec<Open<'a>, W>,
600    element: structured_asn1::Element<'a>,
601    placement: Placement,
602    cause: Option<Error>,
603) -> crate::Result<()> {
604    out.try_push(Open {
605        element,
606        node: Node::of(element.span()),
607        placement,
608        cause,
609    })
610    .map_err(|cause| Error::resource(cause, "cades.material", element.span().start))
611}