Skip to main content

structured_cms/evidence/
verify.rs

1//! Complete ERS sequence execution; historical acceptance is explicitly injected.
2
3mod engine;
4
5use super::{
6    ArchiveTimeStamp, EvidenceRecord, FIELD, Membership, PriorSequence, TimeStamp,
7    input::InputError,
8};
9use crate::{
10    Error, ErrorCode,
11    content_source::ContentError,
12    digest::PassError,
13    provider::SignatureProvider,
14    stream::{RangesError, Source},
15    timestamp::{
16        self, CheckContext, CheckedTimestamp, Evidence, InfoError, ProfileCheckError,
17        ProfileFailure, TokenExtension,
18    },
19};
20use core::convert::Infallible;
21use structured_asn1::{
22    Reader,
23    digest::Resolution,
24    memory::{Execution, OwnedVec, Work},
25};
26use structured_pkix::{
27    Certificate,
28    algorithm::digest::{self, Algorithm},
29    path::WorkingKey,
30    temporal::{Instant, LeapTable},
31};
32
33/// Actual certificate and effective key to use for this timestamp's common check.
34pub struct Selection<'a> {
35    /// Original selected candidate, still subject to SID/ESS/TSA checking.
36    pub certificate: &'a Certificate<'a>,
37    /// That certificate's own producing source and resource ceiling.
38    pub certificate_parent: &'a Reader<'a>,
39    /// Effective parameters from the selected candidate's actual path.
40    pub key: WorkingKey<'a>,
41}
42impl<'a> Selection<'a> {
43    fn historical_context<'s>(&'s self, utc: &'s LeapTable<'a>) -> CheckContext<'a, 's> {
44        CheckContext {
45            certificate: self.certificate,
46            certificate_parent: self.certificate_parent,
47            key: self.key,
48            requested: None,
49            utc,
50            reference: None,
51            window: None,
52        }
53    }
54}
55/// Caller selection; the ERS mechanism executes the check on the actual token.
56pub trait Authorities<'document> {
57    /// Original selection/provider-policy refusal.
58    type Error;
59    /// Selects a candidate for this exact original timestamp ordinal.
60    fn select<'a, W: Work>(
61        &'a self,
62        index: usize,
63        stamp: &'a ArchiveTimeStamp<'document>,
64        work: &W,
65    ) -> Result<Selection<'a>, Self::Error>
66    where
67        'document: 'a;
68}
69
70/// Caller-established historical validation of the supplied actual participant.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Decision {
73    /// Path/status, signed ESS restrictions and algorithm suitability all hold.
74    Valid,
75    /// At least one required historical fact is known to fail.
76    Invalid,
77    /// Required historical evidence is unavailable or uncertain.
78    Unknown,
79}
80/// Facts for historical validation, never a request to infer trust from genTime.
81pub struct Historical<'a, 'token> {
82    /// Original flat timestamp ordinal.
83    pub index: usize,
84    /// Complete common result for the actual selected original token.
85    pub current: &'a CheckedTimestamp<'token>,
86    /// Candidate/key used by this same common check.
87    pub selection: &'a Selection<'token>,
88    /// Next token's successful mathematical facts, when present.
89    pub following: Option<&'a Evidence>,
90    /// First token of the next chain: the tree algorithm must remain suitable
91    /// through this renewal, even for earlier tokens in the current chain.
92    pub following_chain: Option<&'a Evidence>,
93    /// Explicit current verification reference for the final timestamp.
94    pub verification_time: Instant,
95}
96
97/// Failed preflight retains the actual original source and carried source map.
98#[derive(Debug)]
99pub enum PlanError {
100    /// Source/work/schema refusal before covered content is accessed.
101    Processing(Error),
102    /// Original carried `TSTInfo` refusal and its independently retained source.
103    Information(ContentError<InfoError<AlgorithmError>>),
104    /// Retained envelope/ESS/CMS facts already prevent a conforming token.
105    Nonconforming {
106        /// Actual original timestamp ordinal.
107        index: usize,
108        /// Honest original envelope enclosure.
109        span: structured_asn1::Span,
110    },
111    /// An alternative timestamp format has no selected execution mechanism.
112    UnsupportedTimestamp {
113        /// Actual original archive timestamp ordinal.
114        index: usize,
115    },
116    /// A digest is unknown to the shared standard algorithm resolver.
117    UnsupportedAlgorithm {
118        /// Actual original enclosing position.
119        at: u64,
120    },
121}
122/// Algorithm inspection preserves unavailable capability separately from syntax
123/// and resource errors, including inside a temporary carried `TSTInfo` source.
124#[derive(Debug)]
125pub enum AlgorithmError {
126    /// Unchanged original schema/encoding/resource refusal.
127    Processing(Error),
128    /// The written digest has no standard semantic resolver.
129    Unsupported {
130        /// Original source-relative algorithm position.
131        at: u64,
132    },
133}
134
135/// Complete operation refusal; no successful partial sequence is returned.
136#[derive(Debug)]
137pub enum VerificationError<SE, PE, RE, AE, HE, FE = Infallible> {
138    /// The declared first hash list cannot cover this actual object/group.
139    Membership {
140        /// Original archive timestamp ordinal.
141        index: usize,
142    },
143    /// Original preflight failure before covered-source I/O.
144    Plan(PlanError),
145    /// Common capacity/work/source refusal.
146    Processing(Error),
147    /// Actual object acquisition or selected-provider hashing failed.
148    Acquisition(PassError<SE, PE>),
149    /// Selection refused this exact original token.
150    Selection {
151        /// Original flat timestamp ordinal.
152        index: usize,
153        /// Unchanged selector refusal.
154        cause: AE,
155    },
156    /// Complete common check over cached actual object digests refused.
157    Timestamp {
158        /// Original flat timestamp ordinal.
159        index: usize,
160        /// Unchanged common/input/provider/recognition failure.
161        cause: ProfileCheckError<InputError<SE, PE>, PE, RE, FE>,
162    },
163    /// Complete common check over the preceding timestamp refused.
164    Renewal {
165        /// Original flat timestamp ordinal.
166        index: usize,
167        /// Unchanged renewal check failure.
168        cause: ProfileCheckError<InputError<RangesError, PE>, PE, RE, FE>,
169    },
170    /// Historical validator failed to execute; not a negative document finding.
171    Historical {
172        /// Original flat timestamp ordinal.
173        index: usize,
174        /// Unchanged validator refusal.
175        cause: HE,
176    },
177}
178
179/// Complete record result with unchanged acquisition, provider, selector,
180/// recognizer, historical and profile causes; diagnostics allocate nothing.
181pub type VerificationResult<'a, 'd, SE, PE, RE, AE, HE, FE = Infallible> =
182    Result<Report<'a, 'd>, VerificationError<SE, PE, RE, AE, HE, FE>>;
183
184/// Result for one original token; editing public facts cannot create a receipt.
185pub struct Checked<'a> {
186    index: usize,
187    common: CheckedTimestamp<'a>,
188    selection: Selection<'a>,
189    history: Decision,
190}
191impl<'a> Checked<'a> {
192    /// Original flat timestamp ordinal, independent of verification order.
193    #[must_use]
194    pub fn index(&self) -> usize {
195        self.index
196    }
197    /// Complete common timestamp result, including negative/unsupported facts.
198    #[must_use]
199    pub fn timestamp(&self) -> &CheckedTimestamp<'_> {
200        &self.common
201    }
202    /// Actual original selected candidate/key from that check.
203    #[must_use]
204    pub fn selection(&self) -> &Selection<'_> {
205        &self.selection
206    }
207    /// Explicit caller-established historical answer.
208    #[must_use]
209    pub fn historical(&self) -> Decision {
210        self.history
211    }
212    /// Moves the SAME common result for a format adapter's actor construction.
213    /// No signature is rechecked and no retained evidence is copied. Selection
214    /// references are deliberately discarded by adapters with their own sealed
215    /// path model; callers must preserve this original index/source binding.
216    pub fn into_parts(self) -> (usize, CheckedTimestamp<'a>, Selection<'a>, Decision) {
217        (self.index, self.common, self.selection, self.history)
218    }
219}
220
221/// Complete, possibly negative sequence report, bound to this original record.
222pub struct Report<'a, 'document> {
223    record: &'a EvidenceRecord<'document>,
224    checks: OwnedVec<Checked<'a>>,
225    ordering: OwnedVec<OrderFinding>,
226    complete: bool,
227}
228/// One actual adjacent pair whose required chronological order is not proven.
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub struct OrderFinding {
231    /// Original earlier archive timestamp ordinal.
232    pub earlier: usize,
233    /// Original next archive timestamp ordinal.
234    pub later: usize,
235    /// Reversal, overlap or missing information, independently of trust facts.
236    pub cause: OrderCause,
237}
238/// Complete temporal comparison outcome; uncertainty is never positive order.
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub enum OrderCause {
241    /// Authenticated generation facts contradict the declared chain order.
242    Reversed,
243    /// Accuracy intervals overlap without a same-TSA ordering assertion.
244    Overlap,
245    /// Supplied UTC facts cannot establish the exact generation order.
246    Utc(structured_pkix::temporal::Uncertainty),
247    /// At least one timestamp has no successful complete mathematical evidence.
248    UnprovenTimestamp,
249    /// A generation endpoint is uncertain (for example accuracy is unstated).
250    Bounds {
251        /// Earlier participant's unavailable bound.
252        earlier: Option<timestamp::BoundUncertainty>,
253        /// Later participant's unavailable bound.
254        later: Option<timestamp::BoundUncertainty>,
255    },
256}
257/// Closed complete record receipt. This incorporates the caller's historical
258/// decisions; it does not choose application trust or claim `CAdES` conformance.
259pub struct Verified<'report, 'record, 'document>(&'report Report<'record, 'document>);
260impl<'a, 'd> Report<'a, 'd> {
261    /// Moves complete results for a higher-level source-bound format adapter.
262    /// Order is the stored reverse execution order; each checked index remains
263    /// original. The boolean is this report's private computed closure result,
264    /// not a constructor for another common receipt.
265    pub fn into_parts(
266        self,
267    ) -> (
268        &'a EvidenceRecord<'d>,
269        OwnedVec<Checked<'a>>,
270        OwnedVec<OrderFinding>,
271        bool,
272    ) {
273        (self.record, self.checks, self.ordering, self.complete)
274    }
275    /// Results in ORIGINAL order, although historical checks run newest first.
276    pub fn checks(&self) -> impl DoubleEndedIterator<Item = &Checked<'a>> + ExactSizeIterator {
277        self.checks.iter().rev()
278    }
279    /// Every failed/uncertain adjacent comparison, with original ordinals.
280    #[must_use]
281    pub fn ordering(&self) -> &[OrderFinding] {
282        &self.ordering
283    }
284    /// Present only when every required check and chronology was established.
285    #[must_use]
286    pub fn verified(&self) -> Option<Verified<'_, 'a, 'd>> {
287        if self.complete {
288            Some(Verified(self))
289        } else {
290            None
291        }
292    }
293}
294impl<'a, 'd> Verified<'_, 'a, 'd> {
295    /// Original record to which all participants and renewal inputs were bound.
296    #[must_use]
297    pub fn record(&self) -> &'a EvidenceRecord<'d> {
298        self.0.record
299    }
300    /// Every actual checked timestamp, in original order.
301    pub fn checks(&self) -> impl DoubleEndedIterator<Item = &Checked<'a>> + ExactSizeIterator {
302        self.0.checks()
303    }
304}
305
306/// Original-bound schema preflight and exact chain-algorithm request mapping.
307/// A format-derived input can prepare its per-algorithm recipe once and reuse
308/// this SAME plan for execution, without reparsing timestamp information.
309pub struct InputPlan<'record, 'document> {
310    record: &'record EvidenceRecord<'document>,
311    chains: OwnedVec<Algorithm>,
312}
313impl<'r, 'd> InputPlan<'r, 'd> {
314    #[expect(
315        clippy::result_large_err,
316        reason = "a carried algorithm failure retains its original source map and cause inline, including when no diagnostic allocation can succeed"
317    )]
318    fn read(record: &'r EvidenceRecord<'d>, operation: &Reader<'_>) -> Result<Self, PlanError> {
319        let origin = record
320            .original
321            .reader_in(operation)
322            .map_err(PlanError::Processing)?;
323        let work = origin.execution_query(operation);
324        let mut declared = work.vector();
325        for identifier in &record.algorithms {
326            let value = known(identifier, &work).map_err(original_algorithm_error)?;
327            insert_algorithm(&mut declared, value, &work, identifier.offset())
328                .map_err(PlanError::Processing)?;
329        }
330        let mut chains = work.vector();
331        let mut chain_index = 0;
332        for (index, stamp) in record.timestamps.iter().enumerate() {
333            work.charge_work(1).map_err(|cause| {
334                PlanError::Processing(Error::resource(
335                    cause,
336                    FIELD,
337                    stamp.original.element.span().start,
338                ))
339            })?;
340            let TimeStamp::Rfc3161(token) = &stamp.token else {
341                return Err(PlanError::UnsupportedTimestamp { index });
342            };
343            if !token.faults().is_empty()
344                || !token.message().faults().is_empty()
345                || !token.signer().faults().is_empty()
346            {
347                return Err(PlanError::Nonconforming {
348                    index,
349                    span: token.message().span(),
350                });
351            }
352            let parent = stamp
353                .timestamp
354                .reader_in(operation)
355                .map_err(PlanError::Processing)?;
356            let timestamp_algorithm =
357                token_algorithm(token, &parent).map_err(PlanError::Information)?;
358            let value = if let Some(identifier) = stamp.algorithm {
359                known(&identifier, &work).map_err(original_algorithm_error)?
360            } else {
361                timestamp_algorithm
362            };
363            require_width(stamp, value.width(), &work).map_err(PlanError::Processing)?;
364            let chain = &record.chains[chain_index];
365            if index == chain.first {
366                chains.try_push(value).map_err(|cause| {
367                    PlanError::Processing(Error::resource(
368                        cause,
369                        FIELD,
370                        chain.original.element.span().start,
371                    ))
372                })?;
373            } else if chains[chain_index] != value {
374                return Err(PlanError::Processing(Error::schema(
375                    ErrorCode::AlgorithmProtection,
376                    FIELD,
377                    stamp.original.element.span().start,
378                )));
379            }
380            if index + 1 == chain.end {
381                chain_index += 1;
382            }
383        }
384        let mut used = work.vector();
385        for algorithm in &chains {
386            insert_algorithm(
387                &mut used,
388                *algorithm,
389                &work,
390                record.original.element.span().start,
391            )
392            .map_err(PlanError::Processing)?;
393        }
394        require_union(
395            &declared,
396            &used,
397            &work,
398            record.original.element.span().start,
399        )
400        .map_err(PlanError::Processing)?;
401        Ok(Self {
402            record,
403            chains: chains.into_owned(),
404        })
405    }
406    /// Original chain order, including repeated algorithms. The digest batch
407    /// retains this ordinal mapping when equal full identities are deduplicated.
408    #[must_use]
409    pub fn algorithms(&self) -> &[Algorithm] {
410        &self.chains
411    }
412    /// SAME original record for this immutable plan.
413    #[must_use]
414    pub fn record(&self) -> &'r EvidenceRecord<'d> {
415        self.record
416    }
417
418    /// Executes asynchronous original objects with the same admitted plan,
419    /// profile-before-input ordering, renewal recipes and historical closure.
420    /// All chain algorithms share one complete pass over each object. Batch
421    /// owners may inspect those actual outputs without acquiring bytes again.
422    #[expect(
423        clippy::too_many_arguments,
424        reason = "the asynchronous driver retains distinct original/provider/authority/UTC inputs and scoped format callbacks"
425    )]
426    #[expect(
427        clippy::result_large_err,
428        reason = "suspension preserves the same source-mapped and opaque failures as resident verification"
429    )]
430    pub async fn verify_async_with_profile<'a, P, PE, A, S, RE, HE, FE>(
431        &self,
432        provider: &'a P,
433        operation: &Reader<'a>,
434        authorities: &'a A,
435        utc: &LeapTable<'a>,
436        reference: Instant,
437        objects: S,
438        membership: Membership,
439        mut recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
440        mut historical: impl FnMut(Historical<'_, 'a>, &Execution<'_>) -> Result<Decision, HE>,
441        mut profile: impl for<'view, 'scope> FnMut(
442            usize,
443            timestamp::ProfileInput<'view, 'd, 'a, 'scope>,
444        ) -> Result<(), ProfileFailure<FE>>,
445    ) -> VerificationResult<'a, 'd, S::Error, PE, RE, A::Error, HE, FE>
446    where
447        'r: 'a,
448        'd: 'a,
449        P: for<'scope> SignatureProvider<'scope, Error = PE>,
450        A: Authorities<'d>,
451        S: super::AsyncObjects<'a, P>,
452    {
453        let record = self.record;
454        let plan = self;
455        engine::execute!(
456            suspending,
457            record,
458            plan,
459            provider,
460            operation,
461            authorities,
462            utc,
463            reference,
464            objects,
465            membership,
466            recognizes,
467            historical,
468            profile
469        )
470    }
471
472    fn token(&self, index: usize) -> (&'r ArchiveTimeStamp<'d>, &'r timestamp::Token<'d>) {
473        let record = self.record;
474        let stamp = &record.timestamps[index];
475        let TimeStamp::Rfc3161(token) = &stamp.token else {
476            unreachable!("the immutable input plan admitted every timestamp format")
477        };
478        (stamp, token)
479    }
480
481    /// Executes literal original objects with this SAME checked input plan.
482    /// Selection callbacks need not cause a second schema/information parse.
483    #[expect(
484        clippy::too_many_arguments,
485        reason = "the immutable plan keeps source, provider, authority and UTC contexts independent of the three differently scoped callbacks"
486    )]
487    #[expect(
488        clippy::result_large_err,
489        reason = "the literal-source entry preserves mapped timestamp and opaque callback failures without a diagnostic allocation"
490    )]
491    pub fn verify_with_profile<'a, P, PE, A, S, RE, HE, FE>(
492        &self,
493        provider: &'a P,
494        operation: &Reader<'a>,
495        authorities: &'a A,
496        utc: &LeapTable<'a>,
497        reference: Instant,
498        objects: &mut [S],
499        membership: Membership,
500        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
501        historical: impl FnMut(Historical<'_, 'a>, &Execution<'_>) -> Result<Decision, HE>,
502        profile: impl for<'view, 'scope> FnMut(
503            usize,
504            timestamp::ProfileInput<'view, 'd, 'a, 'scope>,
505        ) -> Result<(), ProfileFailure<FE>>,
506    ) -> VerificationResult<'a, 'd, S::Error, PE, RE, A::Error, HE, FE>
507    where
508        'r: 'a,
509        'd: 'a,
510        P: for<'scope> SignatureProvider<'scope, Error = PE>,
511        A: Authorities<'d>,
512        S: Source,
513    {
514        self.record.verify_plan_sources(
515            self,
516            provider,
517            operation,
518            authorities,
519            utc,
520            reference,
521            super::objects::Streams(objects),
522            membership,
523            recognizes,
524            historical,
525            profile,
526        )
527    }
528
529    /// Executes per-algorithm format inputs with this already checked plan.
530    /// Every common/token/profile/history check still runs on the actual source.
531    #[expect(
532        clippy::too_many_arguments,
533        reason = "algorithm-dependent source execution has separate producing-reader, provider, authority and callback lifetimes"
534    )]
535    #[expect(
536        clippy::result_large_err,
537        reason = "request-source refusal keeps the actual mapped timestamp and caller causes inline on exhaustion"
538    )]
539    pub fn verify_requests_with_profile<'a, P, PE, A, S, RE, HE, FE>(
540        &self,
541        provider: &'a P,
542        operation: &Reader<'a>,
543        authorities: &'a A,
544        utc: &LeapTable<'a>,
545        reference: Instant,
546        objects: &mut [S],
547        membership: Membership,
548        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
549        historical: impl FnMut(Historical<'_, 'a>, &Execution<'_>) -> Result<Decision, HE>,
550        profile: impl for<'view, 'scope> FnMut(
551            usize,
552            timestamp::ProfileInput<'view, 'd, 'a, 'scope>,
553        ) -> Result<(), ProfileFailure<FE>>,
554    ) -> VerificationResult<'a, 'd, S::Error, PE, RE, A::Error, HE, FE>
555    where
556        'r: 'a,
557        'd: 'a,
558        P: for<'scope> SignatureProvider<'scope, Error = PE>,
559        A: Authorities<'d>,
560        S: crate::digest::RequestSource<'a, P>,
561    {
562        self.record.verify_plan_sources(
563            self,
564            provider,
565            operation,
566            authorities,
567            utc,
568            reference,
569            super::objects::Requests(objects),
570            membership,
571            recognizes,
572            historical,
573            profile,
574        )
575    }
576}
577/// Resolves the actual carried imprint with the same source-mapped syntax
578/// preflight used by ERS. Unknown capability, malformed fields and resource
579/// refusal remain distinct; this does not establish timestamp mathematics.
580#[expect(
581    clippy::result_large_err,
582    reason = "the actual carried information/source-map failure must survive before provider or covered-source side effects"
583)]
584pub fn token_algorithm<'d>(
585    token: &timestamp::Token<'d>,
586    parent: &Reader<'d>,
587) -> Result<Algorithm, ContentError<InfoError<AlgorithmError>>> {
588    token.with_info(parent, |info, reader, _| {
589        if info.has_faults() {
590            return Err(AlgorithmError::Processing(Error::schema(
591                ErrorCode::MalformedValue,
592                FIELD,
593                info.span().start,
594            )));
595        }
596        let timestamp::HashAlgorithmReading::Identifier(identifier) = info.imprint().algorithm()
597        else {
598            return Err(AlgorithmError::Processing(Error::schema(
599                ErrorCode::AlgorithmSyntax,
600                FIELD,
601                info.imprint().span().start,
602            )));
603        };
604        known(identifier, &reader.execution())
605    })
606}
607fn known<W: Work>(
608    identifier: &structured_pkix::AlgorithmIdentifier<'_>,
609    work: &W,
610) -> Result<Algorithm, AlgorithmError> {
611    match digest::algorithm(identifier, work)
612        .map_err(|cause| AlgorithmError::Processing(Error::pkix(cause)))?
613    {
614        Resolution::Supported(value) => Ok(value),
615        Resolution::Unsupported => Err(AlgorithmError::Unsupported {
616            at: identifier.offset(),
617        }),
618        Resolution::Malformed => Err(AlgorithmError::Processing(Error::schema(
619            ErrorCode::AlgorithmSyntax,
620            FIELD,
621            identifier.offset(),
622        ))),
623    }
624}
625fn original_algorithm_error(cause: AlgorithmError) -> PlanError {
626    match cause {
627        AlgorithmError::Processing(cause) => PlanError::Processing(cause),
628        AlgorithmError::Unsupported { at } => PlanError::UnsupportedAlgorithm { at },
629    }
630}
631fn insert_algorithm<W: Work>(
632    set: &mut OwnedVec<Algorithm, W>,
633    value: Algorithm,
634    work: &W,
635    at: u64,
636) -> crate::Result<()> {
637    // The set contains DISTINCT variants of the shared finite algorithm enum.
638    // Duplicate advertisements cannot cause declared_count*timestamp_count
639    // comparisons. Every possible member comparison is admitted before access.
640    for member in &*set {
641        work.charge_work(1)
642            .map_err(|cause| Error::resource(cause, FIELD, at))?;
643        if *member == value {
644            return Ok(());
645        }
646    }
647    set.try_push(value)
648        .map_err(|cause| Error::resource(cause, FIELD, at))
649}
650
651fn require_width<W: Work>(
652    stamp: &ArchiveTimeStamp<'_>,
653    width: usize,
654    work: &W,
655) -> crate::Result<()> {
656    if let Some(tree) = &stamp.tree {
657        for level in tree {
658            for hash in &level.hashes {
659                work.charge_work(1)
660                    .map_err(|cause| Error::resource(cause, FIELD, hash.span().start))?;
661                if hash.len() != width {
662                    return Err(Error::schema(
663                        ErrorCode::DigestWidth,
664                        FIELD,
665                        hash.span().start,
666                    ));
667                }
668            }
669        }
670    }
671    Ok(())
672}
673
674fn require_union<W: Work>(
675    declared: &[Algorithm],
676    used: &[Algorithm],
677    work: &W,
678    at: u64,
679) -> crate::Result<()> {
680    // RFC4998 §3.1 requires a union, not an arbitrary advertisement. Both
681    // sets already contain distinct shared variants, bounding this pass.
682    for (left, right) in [(declared, used), (used, declared)] {
683        for requested in left {
684            let mut found = false;
685            for actual in right {
686                work.charge_work(1)
687                    .map_err(|cause| Error::resource(cause, FIELD, at))?;
688                if requested == actual {
689                    found = true;
690                    break;
691                }
692            }
693            if !found {
694                return Err(Error::schema(ErrorCode::AlgorithmProtection, FIELD, at));
695            }
696        }
697    }
698    Ok(())
699}
700
701impl<'document> EvidenceRecord<'document> {
702    /// Validates every timestamp format/tree width/digest advertisement and
703    /// same-chain algorithm before covered-object acquisition or provider work.
704    #[expect(
705        clippy::result_large_err,
706        reason = "preflight returns the carried-source diagnostic even when no diagnostic allocation can succeed"
707    )]
708    pub fn input_plan(
709        &self,
710        operation: &Reader<'_>,
711    ) -> Result<InputPlan<'_, 'document>, PlanError> {
712        InputPlan::read(self, operation)
713    }
714    /// Verifies every timestamp, both renewal relations, the digest union and
715    /// actual chronology. Objects are read ONCE with all chain algorithms in
716    /// `DigestBatch`'s shared fan-out. Reverse execution exposes the next
717    /// already checked participant to the caller's historical validator.
718    ///
719    /// Historical Valid must cover actual path/status, signed ESS restrictions,
720    /// signature/hash suitability at the next token and tree suitability through
721    /// the next chain's first token. The last participant is validated at the
722    /// explicit verification reference. Unprotected hints never satisfy this.
723    #[expect(
724        clippy::too_many_arguments,
725        reason = "standalone verification receives explicit source/group, provider, authority, UTC and historical/recognition boundaries"
726    )]
727    #[expect(
728        clippy::result_large_err,
729        reason = "standalone verification retains original failure categories without allocating error storage on resource refusal"
730    )]
731    pub fn verify<'a, P, PE, A, S, RE, HE>(
732        &'a self,
733        provider: &'a P,
734        operation: &Reader<'a>,
735        authorities: &'a A,
736        utc: &LeapTable<'a>,
737        reference: Instant,
738        objects: &mut [S],
739        membership: Membership,
740        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
741        historical: impl FnMut(Historical<'_, 'a>, &Execution<'_>) -> Result<Decision, HE>,
742    ) -> VerificationResult<'a, 'document, S::Error, PE, RE, A::Error, HE>
743    where
744        'document: 'a,
745        P: for<'scope> SignatureProvider<'scope, Error = PE>,
746        A: Authorities<'document>,
747        S: Source,
748    {
749        self.verify_with_profile(
750            provider,
751            operation,
752            authorities,
753            utc,
754            reference,
755            objects,
756            membership,
757            recognizes,
758            historical,
759            |_, _| Ok::<_, ProfileFailure<Infallible>>(()),
760        )
761    }
762
763    /// Same complete sequence check with one schema/profile callback per actual
764    /// timestamp. It sees the SAME ESS and `TSTInfo` readings before signature
765    /// or covered-input callbacks; no second token parse or provider is used.
766    #[expect(
767        clippy::too_many_arguments,
768        reason = "profiled standalone verification keeps original provider/key/UTC inputs and three independently scoped callbacks explicit"
769    )]
770    #[expect(
771        clippy::result_large_err,
772        reason = "the complete carried mapping and profile/caller causes survive on the nonallocating failure path"
773    )]
774    pub fn verify_with_profile<'a, P, PE, A, S, RE, HE, FE>(
775        &'a self,
776        provider: &'a P,
777        operation: &Reader<'a>,
778        authorities: &'a A,
779        utc: &LeapTable<'a>,
780        reference: Instant,
781        objects: &mut [S],
782        membership: Membership,
783        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
784        historical: impl FnMut(Historical<'_, 'a>, &Execution<'_>) -> Result<Decision, HE>,
785        profile: impl for<'view, 'scope> FnMut(
786            usize,
787            timestamp::ProfileInput<'view, 'document, 'a, 'scope>,
788        ) -> Result<(), ProfileFailure<FE>>,
789    ) -> VerificationResult<'a, 'document, S::Error, PE, RE, A::Error, HE, FE>
790    where
791        'document: 'a,
792        P: for<'scope> SignatureProvider<'scope, Error = PE>,
793        A: Authorities<'document>,
794        S: Source,
795    {
796        self.verify_sources(
797            provider,
798            operation,
799            authorities,
800            utc,
801            reference,
802            super::objects::Streams(objects),
803            membership,
804            recognizes,
805            historical,
806            profile,
807        )
808    }
809
810    /// Checks schema-defined algorithm-dependent object preimages (for example
811    /// the TS101733 `PoE` eight-hash recipe) through the SAME sequence kernel.
812    /// Request ordinals refer to original chains; repeated identities share
813    /// their completed output. No arbitrary finalized digest is accepted.
814    #[expect(
815        clippy::too_many_arguments,
816        reason = "schema-derived request sources retain their original authority, execution and callback boundaries without a second dispatcher"
817    )]
818    #[expect(
819        clippy::result_large_err,
820        reason = "format-source errors and mapped token/profile refusals remain distinct without allocating error storage"
821    )]
822    pub fn verify_requests_with_profile<'a, P, PE, A, S, RE, HE, FE>(
823        &'a self,
824        provider: &'a P,
825        operation: &Reader<'a>,
826        authorities: &'a A,
827        utc: &LeapTable<'a>,
828        reference: Instant,
829        objects: &mut [S],
830        membership: Membership,
831        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
832        historical: impl FnMut(Historical<'_, 'a>, &Execution<'_>) -> Result<Decision, HE>,
833        profile: impl for<'view, 'scope> FnMut(
834            usize,
835            timestamp::ProfileInput<'view, 'document, 'a, 'scope>,
836        ) -> Result<(), ProfileFailure<FE>>,
837    ) -> VerificationResult<'a, 'document, S::Error, PE, RE, A::Error, HE, FE>
838    where
839        'document: 'a,
840        P: for<'scope> SignatureProvider<'scope, Error = PE>,
841        A: Authorities<'document>,
842        S: crate::digest::RequestSource<'a, P>,
843    {
844        self.verify_sources(
845            provider,
846            operation,
847            authorities,
848            utc,
849            reference,
850            super::objects::Requests(objects),
851            membership,
852            recognizes,
853            historical,
854            profile,
855        )
856    }
857
858    #[expect(
859        clippy::too_many_arguments,
860        reason = "one dispatch forwards the same literal/request-source verification contexts and separately scoped callbacks"
861    )]
862    #[expect(
863        clippy::result_large_err,
864        reason = "forwarding cannot replace preflight, source-map or opaque caller errors with an allocating wrapper"
865    )]
866    fn verify_sources<'a, P, PE, A, S, RE, HE, FE>(
867        &'a self,
868        provider: &'a P,
869        operation: &Reader<'a>,
870        authorities: &'a A,
871        utc: &LeapTable<'a>,
872        reference: Instant,
873        objects: S,
874        membership: Membership,
875        recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
876        historical: impl FnMut(Historical<'_, 'a>, &Execution<'_>) -> Result<Decision, HE>,
877        profile: impl for<'view, 'scope> FnMut(
878            usize,
879            timestamp::ProfileInput<'view, 'document, 'a, 'scope>,
880        ) -> Result<(), ProfileFailure<FE>>,
881    ) -> VerificationResult<'a, 'document, S::Error, PE, RE, A::Error, HE, FE>
882    where
883        'document: 'a,
884        P: for<'scope> SignatureProvider<'scope, Error = PE>,
885        A: Authorities<'document>,
886        S: super::objects::Sources<'a, P>,
887    {
888        let plan = InputPlan::read(self, operation).map_err(VerificationError::Plan)?;
889        self.verify_plan_sources(
890            &plan,
891            provider,
892            operation,
893            authorities,
894            utc,
895            reference,
896            objects,
897            membership,
898            recognizes,
899            historical,
900            profile,
901        )
902    }
903
904    #[expect(
905        clippy::too_many_arguments,
906        reason = "the single execution kernel binds the original plan and all already-admitted source/provider/callback contexts"
907    )]
908    #[expect(
909        clippy::result_large_err,
910        reason = "token, renewal, selector and source-map refusals are returned unchanged when allocation is unavailable"
911    )]
912    fn verify_plan_sources<'a, P, PE, A, S, RE, HE, FE>(
913        &'a self,
914        plan: &InputPlan<'a, 'document>,
915        provider: &'a P,
916        operation: &Reader<'a>,
917        authorities: &'a A,
918        utc: &LeapTable<'a>,
919        reference: Instant,
920        objects: S,
921        membership: Membership,
922        mut recognizes: impl FnMut(usize, &TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
923        mut historical: impl FnMut(Historical<'_, 'a>, &Execution<'_>) -> Result<Decision, HE>,
924        mut profile: impl for<'view, 'scope> FnMut(
925            usize,
926            timestamp::ProfileInput<'view, 'document, 'a, 'scope>,
927        ) -> Result<(), ProfileFailure<FE>>,
928    ) -> VerificationResult<'a, 'document, S::Error, PE, RE, A::Error, HE, FE>
929    where
930        'document: 'a,
931        P: for<'scope> SignatureProvider<'scope, Error = PE>,
932        A: Authorities<'document>,
933        S: super::objects::Sources<'a, P>,
934    {
935        engine::execute!(
936            resident,
937            self,
938            plan,
939            provider,
940            operation,
941            authorities,
942            utc,
943            reference,
944            objects,
945            membership,
946            recognizes,
947            historical,
948            profile
949        )
950    }
951}
952
953enum GroupError {
954    Processing(Error),
955    Membership(usize),
956}
957fn admit_membership<W: Work>(
958    record: &EvidenceRecord<'_>,
959    objects: usize,
960    membership: Membership,
961    work: &W,
962) -> Result<(), GroupError> {
963    let at = record.original.element.span().start;
964    if objects == 0 || (membership == Membership::Object && objects != 1) {
965        return Err(GroupError::Processing(Error::schema(
966            ErrorCode::MissingField,
967            FIELD,
968            at,
969        )));
970    }
971    for chain in &record.chains {
972        work.charge_work(1)
973            .map_err(|cause| GroupError::Processing(Error::resource(cause, FIELD, at)))?;
974        if !super::input::membership_shape(&record.timestamps[chain.first], objects, membership) {
975            return Err(GroupError::Membership(chain.first));
976        }
977    }
978    Ok(())
979}
980type Prefixes<'r, 'd> = OwnedVec<Option<PriorSequence<'r, 'd>>>;
981fn prior_views<'r, 'd, W: Work>(
982    record: &'r EvidenceRecord<'d>,
983    projected: &'r [crate::encoding::Projection<'d>],
984    operation: &Reader<'_>,
985    work: &W,
986) -> crate::Result<Prefixes<'r, 'd>> {
987    let resource = |cause| Error::resource(cause, FIELD, record.original.element.span().start);
988    let mut prefixes = work.vector();
989    prefixes
990        .try_reserve_exact(record.chains.len())
991        .map_err(resource)?;
992    prefixes.try_push(None).map_err(resource)?;
993    for index in 1..record.chains.len() {
994        prefixes
995            .try_push(Some(PriorSequence::borrowed(
996                record, index, projected, operation,
997            )?))
998            .map_err(resource)?;
999    }
1000    Ok(prefixes.into_owned())
1001}
1002fn order_findings<W: Work>(
1003    checks: &[Checked<'_>],
1004    utc: &LeapTable<'_>,
1005    operation: &Reader<'_>,
1006    work: &W,
1007    at: u64,
1008) -> crate::Result<OwnedVec<OrderFinding>> {
1009    let resource = |cause| Error::resource(cause, FIELD, at);
1010    let mut findings = work.vector();
1011    for pair in checks.windows(2) {
1012        work.charge_work(1).map_err(resource)?;
1013        if let Some(cause) = order(&pair[1], &pair[0], utc, operation)? {
1014            findings
1015                .try_push(OrderFinding {
1016                    earlier: pair[1].index,
1017                    later: pair[0].index,
1018                    cause,
1019                })
1020                .map_err(resource)?;
1021        }
1022    }
1023    Ok(findings.into_owned())
1024}
1025
1026fn history_input<'v, 'a>(
1027    record: &EvidenceRecord<'_>,
1028    chain: usize,
1029    index: usize,
1030    current: &'v CheckedTimestamp<'a>,
1031    selection: &'v Selection<'a>,
1032    checks: &'v [Checked<'a>],
1033    reference: Instant,
1034) -> Historical<'v, 'a> {
1035    let following = checks.last().and_then(|next| next.common.evidence());
1036    let following_chain = record
1037        .chains
1038        .get(chain + 1)
1039        .and_then(|next| checks.get(record.timestamps.len() - 1 - next.first))
1040        .and_then(|next| next.common.evidence());
1041    Historical {
1042        index,
1043        current,
1044        selection,
1045        following,
1046        following_chain,
1047        verification_time: reference,
1048    }
1049}
1050
1051fn order(
1052    earlier: &Checked<'_>,
1053    later: &Checked<'_>,
1054    utc: &LeapTable<'_>,
1055    operation: &Reader<'_>,
1056) -> crate::Result<Option<OrderCause>> {
1057    let work = operation.execution();
1058    let (Some(a), Some(b)) = (earlier.common.evidence(), later.common.evidence()) else {
1059        return Ok(Some(OrderCause::UnprovenTimestamp));
1060    };
1061    order_evidence(
1062        a,
1063        earlier.selection.certificate,
1064        b,
1065        later.selection.certificate,
1066        utc,
1067        &work,
1068    )
1069}
1070
1071/// Orders two actual authenticated TSA facts using RFC3161 §2.4.2's SAME-TSA
1072/// ordering rule or complete stated-accuracy intervals. This is shared by
1073/// in-record ERS renewal and `CAdES` cross-proof preservation closure. A readable
1074/// unsigned poeDate is never substituted for either authenticated generation.
1075pub fn order_evidence<W: Work>(
1076    a: &Evidence,
1077    left: &Certificate<'_>,
1078    b: &Evidence,
1079    right: &Certificate<'_>,
1080    utc: &LeapTable<'_>,
1081    work: &W,
1082) -> crate::Result<Option<OrderCause>> {
1083    let resource = |cause| Error::resource(cause, FIELD, a.source().carrier().start);
1084    if a.facts().ordering() && b.facts().ordering() {
1085        let first = left.execution_in(work);
1086        let second = right.execution_in(work);
1087        first.charge_work(1).map_err(resource)?;
1088        second.charge_work(1).map_err(resource)?;
1089        let same = if core::ptr::eq(left, right) {
1090            true
1091        } else if left.as_bytes().len() != right.as_bytes().len() {
1092            false
1093        } else {
1094            first
1095                .charge_work(left.as_bytes().len() as u64)
1096                .map_err(resource)?;
1097            second
1098                .charge_work(right.as_bytes().len() as u64)
1099                .map_err(resource)?;
1100            left.as_bytes() == right.as_bytes()
1101        };
1102        if same {
1103            // RFC3161 §2.4.2: ordering=true for the SAME authenticated TSA
1104            // permits genTime ordering irrespective of stated accuracy. Exact
1105            // retained decimal tails prevent subnanosecond labels collapsing.
1106            // https://www.rfc-editor.org/rfc/rfc3161#section-2.4.2
1107            return match a.facts().compare_generation(b.facts(), utc, work) {
1108                Ok(core::cmp::Ordering::Greater) => Ok(Some(OrderCause::Reversed)),
1109                Ok(_) => Ok(None),
1110                Err(timestamp::GenerationOrderError::Resource(cause)) => Err(resource(cause)),
1111                Err(timestamp::GenerationOrderError::Uncertain(cause)) => {
1112                    Ok(Some(OrderCause::Utc(cause)))
1113                }
1114            };
1115        }
1116    }
1117    let a = match timestamp::interval_in(a.facts().gen_time(), a.facts().accuracy(), utc, work) {
1118        Ok(value) => Ok(value),
1119        Err(timestamp::IntervalError::Resource(cause)) => return Err(resource(cause)),
1120        Err(timestamp::IntervalError::Uncertain(cause)) => Err(cause),
1121    };
1122    let b = match timestamp::interval_in(b.facts().gen_time(), b.facts().accuracy(), utc, work) {
1123        Ok(value) => Ok(value),
1124        Err(timestamp::IntervalError::Resource(cause)) => return Err(resource(cause)),
1125        Err(timestamp::IntervalError::Uncertain(cause)) => Err(cause),
1126    };
1127    match (a, b) {
1128        (Ok(a), Ok(b)) if a.latest <= b.earliest => Ok(None),
1129        (Ok(a), Ok(b)) if a.earliest > b.latest => Ok(Some(OrderCause::Reversed)),
1130        (Ok(_), Ok(_)) => Ok(Some(OrderCause::Overlap)),
1131        (a, b) => Ok(Some(OrderCause::Bounds {
1132            earlier: a.err(),
1133            later: b.err(),
1134        })),
1135    }
1136}