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