Skip to main content

structured_cades/evidence/
record.rs

1//! Bind complete RFC4998 execution to the actual CMS object and `CAdES` paths.
2use super::{CmsObject, ObjectError, View};
3use crate::{
4    Error,
5    basic::PathRestriction,
6    prepare::Preparation,
7    preservation::{Kind, Required},
8    timestamp::{self, ProfileError, VisitError},
9};
10use core::{
11    cell::{Cell, RefCell},
12    convert::Infallible,
13};
14use structured_asn1::{
15    Element, Reader,
16    memory::{Execution, OwnedVec, Work},
17};
18use structured_cms::{
19    evidence::{ArchiveTimeStamp, Membership, verify as ers},
20    provider::SignatureProvider,
21    stream::{Receiver, Source},
22    timestamp::TokenExtension,
23};
24use structured_ltv::pki::CheckedPath;
25use structured_pkix::temporal::{Instant, LeapTable};
26
27const FIELD: &str = "cades.evidence.record";
28
29/// The original candidate's already checked path and certificate source.
30pub struct Authority<'path, 'd> {
31    /// Same original effective key/path used for signed ESS restrictions.
32    pub path: &'path CheckedPath<'d>,
33    /// Candidate's own original producing reader, not a token/default parser.
34    pub certificate_parent: &'path Reader<'d>,
35    /// Explicit trusted accuracy bound to the selected TSA's signed policy.
36    pub policy_accuracy: Option<structured_cms::timestamp::PolicyAccuracy<'path>>,
37}
38/// Select paths for actual original ERS timestamps; no crypto runs here.
39pub trait Authorities<'d> {
40    /// Original caller selection failure.
41    type Error;
42    /// Current Work must fund any caller-side catalogue/discovery work.
43    fn select<'path, W: Work>(
44        &'path self,
45        index: usize,
46        token: &structured_cms::timestamp::Token<'d>,
47        work: &W,
48    ) -> Result<Authority<'path, 'd>, Self::Error>
49    where
50        'd: 'path;
51}
52
53/// Original whole-object/explicit detached acquisition failure.
54#[derive(Debug)]
55pub enum SourceError<E> {
56    /// Exact adapted CMS framing/source refusal.
57    Cms(ObjectError),
58    /// Exact caller-supplied separate signed-content refusal.
59    Detached(E),
60}
61enum Object<'s, 'a, 'd, S> {
62    Cms(&'s mut CmsObject<'a, 'd>),
63    Detached(&'s mut S),
64}
65impl<S: Source> Source for Object<'_, '_, '_, S> {
66    type Error = SourceError<S::Error>;
67    fn stream<W: Work>(&mut self, out: &mut impl Receiver, work: &W) -> Result<(), Self::Error> {
68        match self {
69            Self::Cms(source) => source.stream(out, work).map_err(SourceError::Cms),
70            Self::Detached(source) => source.stream(out, work).map_err(SourceError::Detached),
71        }
72    }
73}
74/// Selection preserves caller failures separately from adapter processing refusal.
75#[derive(Debug)]
76pub enum SelectionError<E> {
77    /// Actual caller discovery failure.
78    Caller(E),
79    /// Current work, slot admission or source mismatch.
80    Processing(Error),
81}
82pub(super) struct Selected<'path, 'd, A> {
83    pub(super) source: &'path A,
84    pub(super) start: usize,
85    pub(super) items: RefCell<OwnedVec<Option<Authority<'path, 'd>>>>,
86}
87impl<'path, 'd, A> Selected<'path, 'd, A> {
88    pub(super) fn path<W: Work>(
89        &self,
90        index: usize,
91        work: &W,
92        at: u64,
93    ) -> Result<&'path CheckedPath<'d>, Error> {
94        work.charge_work(1)
95            .map_err(|cause| Error::resource(cause, FIELD, at))?;
96        let items = self.items.try_borrow().map_err(|_| Error::Foreign { at })?;
97        let selected = items
98            .get(index)
99            .and_then(Option::as_ref)
100            .ok_or(Error::Foreign { at })?;
101        Ok(selected.path)
102    }
103}
104impl<'path, 'd, A: Authorities<'d>> ers::Authorities<'d> for Selected<'path, 'd, A>
105where
106    'd: 'path,
107{
108    type Error = SelectionError<A::Error>;
109    fn select<'a, W: Work>(
110        &'a self,
111        index: usize,
112        stamp: &'a ArchiveTimeStamp<'d>,
113        work: &W,
114    ) -> Result<ers::Selection<'a>, Self::Error>
115    where
116        'd: 'a,
117    {
118        let at = stamp.original().element().span().start;
119        work.charge_work(1)
120            .map_err(|cause| SelectionError::Processing(Error::resource(cause, FIELD, at)))?;
121        // Discovery is driven by the common verifier AFTER membership and
122        // preceding-sequence preflight. Never call it in a preparatory loop.
123        let source: &'path A = self.source;
124        let structured_cms::evidence::TimeStamp::Rfc3161(token) = stamp.timestamp() else {
125            return Err(SelectionError::Processing(Error::Foreign { at }));
126        };
127        let selected: Authority<'path, 'd> = source
128            .select(self.start + index, token, work)
129            .map_err(SelectionError::Caller)?;
130        let target = selected
131            .path
132            .members()
133            .last()
134            .ok_or(SelectionError::Processing(Error::Foreign { at }))?;
135        let result = ers::Selection {
136            certificate: target.certificate(),
137            certificate_parent: selected.certificate_parent,
138            key: target.key(),
139            policy_accuracy: selected.policy_accuracy,
140        };
141        let mut items = self
142            .items
143            .try_borrow_mut()
144            .map_err(|_| SelectionError::Processing(Error::Foreign { at }))?;
145        let slot = items
146            .get_mut(index)
147            .ok_or(SelectionError::Processing(Error::Foreign { at }))?;
148        if slot.is_some() {
149            return Err(SelectionError::Processing(Error::Foreign { at }));
150        }
151        *slot = Some(selected);
152        Ok(result)
153    }
154}
155
156/// Complete source-bound report, including every failed/uncertain participant.
157/// Mathematical TSA facts are reused for actors without a second signature
158/// verification. Modern baseline promotion still requires its independent
159/// complete status/material/coverage/chronology closure.
160pub struct Checked<'prepared, 'm, 'd, 'path> {
161    pub(super) prepared: &'prepared Preparation<'m, 'd>,
162    pub(super) required: Required<'d>,
163    pub(super) proof: usize,
164    pub(super) covered: Option<OwnedVec<structured_asn1::Span>>,
165    pub(super) timestamps: core::ops::Range<usize>,
166    pub(super) checks: OwnedVec<timestamp::Checked<'m, 'd, 'path>>,
167    pub(super) ordering: OwnedVec<ers::OrderFinding>,
168    pub(super) complete: bool,
169}
170/// Private-constructor source/recipe/history receipt; public facts cannot make it.
171pub struct Verified<'check, 'prepared, 'm, 'd, 'path>(&'check Checked<'prepared, 'm, 'd, 'path>);
172impl<'prepared, 'm, 'd, 'path> Checked<'prepared, 'm, 'd, 'path> {
173    pub(super) fn bind_coverage<W: Work>(
174        &mut self,
175        spans: Option<OwnedVec<structured_asn1::Span>>,
176        work: &W,
177    ) -> Result<(), Error> {
178        self.covered = spans
179            .map(|spans| {
180                super::coverage::normalize_owned(spans, work, self.required.property.span().start)
181            })
182            .transpose()?;
183        Ok(())
184    }
185    /// Every actual TSA result, in original record order.
186    pub fn checks(
187        &self,
188    ) -> impl DoubleEndedIterator<Item = &timestamp::Checked<'m, 'd, 'path>> + ExactSizeIterator
189    {
190        self.checks.iter().rev()
191    }
192    /// Actual failed adjacent chronology comparisons.
193    #[must_use]
194    pub fn ordering(&self) -> &[ers::OrderFinding] {
195        &self.ordering
196    }
197    /// Present only after the COMPLETE record, input, profiles and history hold.
198    #[must_use]
199    pub fn verified(&self) -> Option<Verified<'_, 'prepared, 'm, 'd, 'path>> {
200        if self.complete {
201            Some(Verified(self))
202        } else {
203            None
204        }
205    }
206}
207impl<'prepared, 'm, 'd, 'path> Verified<'_, 'prepared, 'm, 'd, 'path> {
208    pub(super) fn report(&self) -> &Checked<'prepared, 'm, 'd, 'path> {
209        self.0
210    }
211    /// Exact original `CAdES` attribute/value/placement, not a `genTime` selector.
212    #[must_use]
213    pub fn required(&self) -> Required<'d> {
214        self.0.required
215    }
216    /// Same complete source preparation and its timestamp/material catalogue.
217    #[must_use]
218    pub fn prepared(&self) -> &'prepared Preparation<'m, 'd> {
219        self.0.prepared
220    }
221    /// Original global TSA ordinals covered by this complete receipt.
222    #[must_use]
223    pub fn timestamps(&self) -> core::ops::Range<usize> {
224        self.0.timestamps.clone()
225    }
226}
227
228/// Source/profile/common failures stay distinct; no successful partial record.
229#[derive(Debug)]
230pub enum CheckError<SE, PE, RE, AE, HE> {
231    /// Wrong placement/source/recipe or current processing refusal.
232    Selection(Error),
233    /// Actual previous/current source binding refusal.
234    Snapshot(super::SnapshotError),
235    /// Original schema/algorithm/source-mapped information preflight failure.
236    Plan(ers::PlanError),
237    /// Original caller path selection failure.
238    Authority(AE),
239    /// Unmodified whole common record failure, including `CAdES` ESS restrictions.
240    Record(
241        ers::VerificationError<
242            SourceError<SE>,
243            PE,
244            RE,
245            SelectionError<AE>,
246            HE,
247            ProfileError<PE, Error>,
248        >,
249    ),
250}
251
252/// Complete source-bound record result with independent original failure classes.
253pub type CheckResult<'a, 'm, 'd, 'path, SE, PE, RE, AE, HE> =
254    Result<Checked<'a, 'm, 'd, 'path>, CheckError<SE, PE, RE, AE, HE>>;
255
256impl<'a, 'm, 'd> View<'a, 'm, 'd> {
257    /// Verifies RFC4998 Appendix A's INTERNAL original whole-CMS recipe.
258    /// Whole record/history/path and every actual contained TSA are checked.
259    #[expect(
260        clippy::too_many_arguments,
261        reason = "Original CMS, current execution, selected provider/path and independent trust callbacks are separate agreed boundaries."
262    )]
263    #[expect(
264        clippy::result_large_err,
265        reason = "Source maps and opaque provider/history causes remain inline even on allocation refusal."
266    )]
267    pub fn check_internal<'path, P, PE, A, RE, HE>(
268        &self,
269        envelope: Element<'d>,
270        provider: &P,
271        operation: &Reader<'d>,
272        authorities: &'path A,
273        utc: &LeapTable<'d>,
274        reference: Instant,
275        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
276        historical: impl FnMut(ers::Historical<'_, '_>, &Execution<'_>) -> Result<ers::Decision, HE>,
277    ) -> CheckResult<'a, 'm, 'd, 'path, Infallible, PE, RE, A::Error, HE>
278    where
279        P: for<'scope> SignatureProvider<'scope, Error = PE>,
280        A: Authorities<'d>,
281        'd: 'path,
282    {
283        if self.required().kind != Kind::EvidenceRecordInternal {
284            return Err(CheckError::Selection(Error::Foreign {
285                at: self.required().property.span().start,
286            }));
287        }
288        let mut source = self
289            .cms_object(envelope, operation)
290            .map_err(CheckError::Selection)?;
291        let covered = source
292            .coverage(self, operation)
293            .map_err(CheckError::Selection)?;
294        self.check_objects(
295            covered,
296            provider,
297            operation,
298            authorities,
299            utc,
300            reference,
301            &mut [Object::<timestamp::NoDetached>::Cms(&mut source)],
302            Membership::Object,
303            recognizes,
304            historical,
305        )
306    }
307    /// Same complete INTERNAL record with a genuine earlier CMS source.
308    /// The snapshot must preserve all selected fields and prior attributes;
309    /// lost historical BER coding is never reconstructed from a claimed date.
310    #[expect(
311        clippy::too_many_arguments,
312        reason = "The actual previous snapshot supplements the independent source, provider/path and history inputs."
313    )]
314    #[expect(
315        clippy::result_large_err,
316        reason = "Snapshot and source-mapped verification failures preserve their causes without error-path allocation."
317    )]
318    pub fn check_internal_snapshot<'path, P, PE, A, RE, HE>(
319        &self,
320        envelope: Element<'d>,
321        snapshot: &super::CmsSnapshot<'_, 'd>,
322        provider: &P,
323        operation: &Reader<'d>,
324        authorities: &'path A,
325        utc: &LeapTable<'d>,
326        reference: Instant,
327        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
328        historical: impl FnMut(ers::Historical<'_, '_>, &Execution<'_>) -> Result<ers::Decision, HE>,
329    ) -> CheckResult<'a, 'm, 'd, 'path, Infallible, PE, RE, A::Error, HE>
330    where
331        P: for<'scope> SignatureProvider<'scope, Error = PE>,
332        A: Authorities<'d>,
333        'd: 'path,
334    {
335        if self.required().kind != Kind::EvidenceRecordInternal {
336            return Err(CheckError::Selection(Error::Foreign {
337                at: self.required().property.span().start,
338            }));
339        }
340        let mut source = self
341            .cms_object_snapshot(envelope, snapshot, operation)
342            .map_err(CheckError::Snapshot)?;
343        let covered = source
344            .coverage(self, operation)
345            .map_err(CheckError::Selection)?;
346        self.check_objects(
347            covered,
348            provider,
349            operation,
350            authorities,
351            utc,
352            reference,
353            &mut [Object::<timestamp::NoDetached>::Cms(&mut source)],
354            Membership::Object,
355            recognizes,
356            historical,
357        )
358    }
359    /// Verifies EXTERNAL whole CMS plus the ACTUAL separately supplied signed
360    /// content as one exact document group. No whole-content buffering occurs.
361    #[expect(
362        clippy::too_many_arguments,
363        reason = "The caller's detached source cannot be conflated with original CMS provenance or trust/provider callbacks."
364    )]
365    #[expect(
366        clippy::result_large_err,
367        reason = "Detached acquisition and original mapped/provider failures must survive resource refusal without boxing."
368    )]
369    pub fn check_external<'path, P, PE, A, S, RE, HE>(
370        &self,
371        envelope: Element<'d>,
372        detached: &mut S,
373        provider: &P,
374        operation: &Reader<'d>,
375        authorities: &'path A,
376        utc: &LeapTable<'d>,
377        reference: Instant,
378        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
379        historical: impl FnMut(ers::Historical<'_, '_>, &Execution<'_>) -> Result<ers::Decision, HE>,
380    ) -> CheckResult<'a, 'm, 'd, 'path, S::Error, PE, RE, A::Error, HE>
381    where
382        P: for<'scope> SignatureProvider<'scope, Error = PE>,
383        A: Authorities<'d>,
384        S: Source,
385        'd: 'path,
386    {
387        if self.required().kind != Kind::EvidenceRecordExternal {
388            return Err(CheckError::Selection(Error::Foreign {
389                at: self.required().property.span().start,
390            }));
391        }
392        let mut source = self
393            .cms_object(envelope, operation)
394            .map_err(CheckError::Selection)?;
395        let covered = source
396            .coverage(self, operation)
397            .map_err(CheckError::Selection)?;
398        self.check_objects(
399            covered,
400            provider,
401            operation,
402            authorities,
403            utc,
404            reference,
405            &mut [Object::Cms(&mut source), Object::Detached(detached)],
406            Membership::Group,
407            recognizes,
408            historical,
409        )
410    }
411
412    /// EXTERNAL group using the genuine previous CMS encoding plus actual
413    /// separate content. Snapshot binding runs before source/provider work.
414    #[expect(
415        clippy::too_many_arguments,
416        reason = "Genuine previous CMS and detached content are distinct original sources alongside current verification inputs."
417    )]
418    #[expect(
419        clippy::result_large_err,
420        reason = "Original two-source and snapshot failures remain inline with the complete common verification cause."
421    )]
422    pub fn check_external_snapshot<'path, P, PE, A, S, RE, HE>(
423        &self,
424        envelope: Element<'d>,
425        snapshot: &super::CmsSnapshot<'_, 'd>,
426        detached: &mut S,
427        provider: &P,
428        operation: &Reader<'d>,
429        authorities: &'path A,
430        utc: &LeapTable<'d>,
431        reference: Instant,
432        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
433        historical: impl FnMut(ers::Historical<'_, '_>, &Execution<'_>) -> Result<ers::Decision, HE>,
434    ) -> CheckResult<'a, 'm, 'd, 'path, S::Error, PE, RE, A::Error, HE>
435    where
436        P: for<'scope> SignatureProvider<'scope, Error = PE>,
437        A: Authorities<'d>,
438        S: Source,
439        'd: 'path,
440    {
441        if self.required().kind != Kind::EvidenceRecordExternal {
442            return Err(CheckError::Selection(Error::Foreign {
443                at: self.required().property.span().start,
444            }));
445        }
446        let mut source = self
447            .cms_object_snapshot(envelope, snapshot, operation)
448            .map_err(CheckError::Snapshot)?;
449        let covered = source
450            .coverage(self, operation)
451            .map_err(CheckError::Selection)?;
452        self.check_objects(
453            covered,
454            provider,
455            operation,
456            authorities,
457            utc,
458            reference,
459            &mut [Object::Cms(&mut source), Object::Detached(detached)],
460            Membership::Group,
461            recognizes,
462            historical,
463        )
464    }
465
466    #[expect(
467        clippy::too_many_arguments,
468        reason = "This single kernel shares actual object/group recipes, provider/path checks and independent history callbacks."
469    )]
470    #[expect(
471        clippy::result_large_err,
472        reason = "Common ERS failures retain source mapping and opaque callback causes without a new allocation."
473    )]
474    fn check_objects<'path, P, PE, A, S, RE, HE>(
475        &self,
476        covered: OwnedVec<structured_asn1::Span>,
477        provider: &P,
478        operation: &Reader<'d>,
479        authorities: &'path A,
480        utc: &LeapTable<'d>,
481        reference: Instant,
482        objects: &mut [Object<'_, 'a, 'd, S>],
483        membership: Membership,
484        mut recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
485        mut historical: impl FnMut(ers::Historical<'_, '_>, &Execution<'_>) -> Result<ers::Decision, HE>,
486    ) -> CheckResult<'a, 'm, 'd, 'path, S::Error, PE, RE, A::Error, HE>
487    where
488        P: for<'scope> SignatureProvider<'scope, Error = PE>,
489        A: Authorities<'d>,
490        S: Source,
491        'd: 'path,
492    {
493        let required = self.required();
494        let record = self.record().ok_or_else(|| {
495            CheckError::Selection(Error::Foreign {
496                at: required.property.span().start,
497            })
498        })?;
499        let plan = record.input_plan(operation).map_err(CheckError::Plan)?;
500        let work = operation.execution();
501        let resource = |cause| {
502            CheckError::Selection(Error::resource(
503                cause,
504                FIELD,
505                required.property.span().start,
506            ))
507        };
508        let mut selected = work.vector();
509        let mut restrictions = work.vector();
510        selected
511            .try_reserve_exact(record.timestamps().len())
512            .map_err(resource)?;
513        restrictions
514            .try_reserve_exact(record.timestamps().len())
515            .map_err(resource)?;
516        for _ in record.timestamps() {
517            work.charge_work(1).map_err(resource)?;
518            selected.try_push(None).map_err(resource)?;
519            restrictions
520                .try_push(Cell::new(PathRestriction::Unproven))
521                .map_err(resource)?;
522        }
523        let selected = Selected {
524            source: authorities,
525            start: self.entry.timestamps.start,
526            items: RefCell::new(selected.into_owned()),
527        };
528        let report = plan
529            .verify_with_profile(
530                provider,
531                operation,
532                &selected,
533                utc,
534                reference,
535                objects,
536                membership,
537                |index, extension, execution| {
538                    recognizes(self.entry.timestamps.start + index, extension, execution)
539                },
540                |mut facts, execution| {
541                    facts.index += self.entry.timestamps.start;
542                    historical(facts, execution)
543                },
544                |index, input| {
545                    let path = selected
546                        .path(
547                            index,
548                            &input.parent.execution(),
549                            required.property.span().start,
550                        )
551                        .map_err(|cause| structured_cms::timestamp::ProfileFailure {
552                            source: structured_cms::timestamp::ProfileSource::Envelope,
553                            cause: ProfileError::Input(cause),
554                        })?;
555                    let restriction = timestamp::Context::profile_rules(&input, path, provider)?;
556                    restrictions[index].set(restriction);
557                    Ok(())
558                },
559            )
560            .map_err(CheckError::Record)?;
561        let mut checked = self
562            .finish_record(report, &selected, &restrictions, operation)
563            .map_err(CheckError::Selection)?;
564        checked
565            .bind_coverage(Some(covered), &work)
566            .map_err(CheckError::Selection)?;
567        Ok(checked)
568    }
569    pub(super) fn finish_record<'path, A, W: Work>(
570        &self,
571        report: ers::Report<'_, 'd>,
572        selected: &Selected<'path, 'd, A>,
573        restrictions: &OwnedVec<Cell<PathRestriction>, W>,
574        operation: &Reader<'d>,
575    ) -> Result<Checked<'a, 'm, 'd, 'path>, Error>
576    where
577        'd: 'path,
578    {
579        let required = self.required();
580        let work = operation.execution();
581        let resource = |cause| Error::resource(cause, FIELD, required.property.span().start);
582        let (_, results, ordering, mut complete) = report.into_parts();
583        let mut checks = work.vector();
584        checks.try_reserve_exact(results.len()).map_err(resource)?;
585        for result in results {
586            let (index, common, _, _) = result.into_parts();
587            let path = selected.path(index, &work, required.property.span().start)?;
588            let target = path
589                .members()
590                .last()
591                .expect("nonempty selection admitted above");
592            let global = self.entry.timestamps.start + index;
593            let checked = timestamp::with_token(self.prepared, global, operation, |context| {
594                Ok::<_, Infallible>(context.checked(
595                    common,
596                    restrictions[index].get(),
597                    target.certificate(),
598                    target.key(),
599                ))
600            })
601            .map_err(|cause| match cause {
602                VisitError::Processing(cause) => cause,
603                VisitError::Unreadable { property } => Error::Foreign {
604                    at: property.span().start,
605                },
606                VisitError::Callback(never) => match never {},
607            })?;
608            if checked.actor().is_none() {
609                complete = false;
610            }
611            checks.try_push(checked).map_err(resource)?;
612        }
613        Ok(Checked {
614            prepared: self.prepared,
615            required,
616            proof: self.ordinal,
617            covered: None,
618            timestamps: self.entry.timestamps.clone(),
619            checks: checks.into_owned(),
620            ordering,
621            complete,
622        })
623    }
624}