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