Skip to main content

structured_cms/timestamp/
check.rs

1//! Complete common timestamp checks; path/status/policy acceptance stays external.
2
3mod asynchronous;
4
5use super::profile::{
6    self, MappedProfileCheckError, ProfileCheckError, ProfileFailure, ProfileInput,
7};
8use super::{
9    AuthorityFacts, ComputedInput, ImprintAgreement, RawInput, RecognitionError, Request,
10    RequestAgreement, RequestFields, RequestWindow, RetainedFacts, SignatureError, Timeliness,
11    Token, TokenExtension, TokenSignature, TsaIdentity, TsaUsage,
12};
13use crate::{
14    Error, ErrorCode,
15    content_source::{ContentError, SourceMap},
16    directory, ess,
17    provider::{SignatureAnswer, SignatureProvider},
18    stream::Source,
19    verify,
20};
21use core::convert::Infallible;
22use structured_asn1::{
23    Reader,
24    memory::{Execution, OwnedVec, Work},
25    value::OwnedOid,
26};
27use structured_pkix::{
28    Certificate,
29    path::WorkingKey,
30    temporal::{Instant, LeapTable},
31};
32
33/// Original parsed request and its producing reader, not reconstructed tuples.
34pub struct Requested<'a, 'r> {
35    /// Complete original request.
36    pub request: &'r Request<'a>,
37    /// Its own original producing session.
38    pub parent: &'r Reader<'a>,
39}
40
41/// Explicit caller selection/time facts; this does not select a trusted path.
42pub struct CheckContext<'a, 'r> {
43    /// Caller-selected candidate, independently subject to trust/path checks.
44    pub certificate: &'r Certificate<'a>,
45    /// That certificate's own original producing reader.
46    pub certificate_parent: &'r Reader<'a>,
47    /// Effective key retaining independently established inherited parameters.
48    pub key: WorkingKey<'a>,
49    /// Complete live request, absent for archived evidence.
50    pub requested: Option<Requested<'a, 'r>>,
51    /// Explicit UTC correction facts, without implicit clock lookup.
52    pub utc: &'r LeapTable<'a>,
53    /// Caller-supplied comparison instant, when available.
54    pub reference: Option<Instant>,
55    /// Caller-established outstanding-imprint uniqueness/window fact.
56    pub window: Option<RequestWindow>,
57}
58
59/// Scoped execution refusal retaining the actual source/operand and cause.
60#[derive(Debug)]
61pub enum InfoCheckError<SE, PE, RE> {
62    /// Inspection retained schema faults; complete execution did not start.
63    Nonconforming {
64        /// Original information range, interpreted through the accompanying map.
65        span: structured_asn1::Span,
66    },
67    /// Original CMS execution failed after information syntax preflight.
68    Signature(SignatureError<PE>),
69    /// Original ESS execution failed after information syntax preflight.
70    Ess(ess::BindingError<PE>),
71    /// Original token processing/resource refusal.
72    Processing(Error),
73    /// Source-map retention refusal already located at a physical carrier.
74    /// Do not project this offset again as a logical token coordinate.
75    Provenance(Error),
76    /// Covered-content acquisition or selected digest execution failed.
77    Imprint(super::ImprintError<SE, PE>),
78    /// Complete request-imprint comparison failed.
79    RequestImprint(super::RequestImprintError<PE>),
80    /// Request nonce/policy comparison failed.
81    RequestField(super::RequestFieldError),
82    /// Token/certificate source role and original refusal.
83    Authority(super::AuthorityError),
84    /// Original extension handler refusal.
85    Recognition(RecognitionError<RE>),
86}
87
88/// Execution refusal, never a semantic partial success.
89#[derive(Debug)]
90pub enum CheckError<SE, PE, RE> {
91    /// Original envelope/ESS inspection faults prevent complete execution.
92    Nonconforming {
93        /// Original outer field enclosure, not a resource error.
94        span: structured_asn1::Span,
95    },
96    /// Original outer processing/resource refusal.
97    Processing(Error),
98    /// Caller supplied a different original SPKI for the selected certificate.
99    KeyMismatch {
100        /// Honest enclosure of the selected certificate, not a document fault.
101        certificate: structured_asn1::Span,
102    },
103    /// Certificate hint comparison failed, not a mismatch.
104    Identifier(directory::Error),
105    /// Request source role and original refusal are preserved.
106    RequestField(super::RequestFieldError),
107    /// Scoped original cause accompanied by independently retained source map.
108    Information(ContentError<super::InfoError<InfoCheckError<SE, PE, RE>>>),
109}
110
111/// A check in a logical CMS envelope retains its own source context separately
112/// from the inner `TSTInfo` map. CMS errors must not use `TSTInfo` coordinates.
113#[derive(Debug)]
114pub enum MappedCheckError<SE, PE, RE> {
115    /// Caller source/session mismatch, refused before map admission or work.
116    /// The original operand identifies its reader; no copied map is required.
117    Preflight(CheckError<SE, PE, RE>),
118    /// Envelope map admission failed at its already physical carrier.
119    Provenance(Error),
120    /// Original cause and independently retained envelope coordinate context.
121    Execution {
122        /// Maps the original CMS envelope's coordinates, not inner `TSTInfo`.
123        envelope: SourceMap,
124        /// Original check cause; source/operand variants select its context.
125        source: CheckError<SE, PE, RE>,
126    },
127}
128
129/// Independently retained cryptographic/time evidence, not a trust verdict.
130/// The caller must establish TSA path/status, enforce signed ESS restrictions
131/// and apply algorithm/policy permissions before granting product acceptance.
132#[derive(Debug)]
133pub struct Evidence {
134    facts: RetainedFacts,
135    source: SourceMap,
136}
137
138/// Independent-copy refusal retains the coordinate context of its cause.
139#[derive(Debug)]
140pub enum EvidenceCopyError {
141    /// Map admission failed at its already physical carrier enclosure.
142    Provenance(Error),
143    /// Fact admission failed in logical/original fact coordinates.
144    Facts {
145        /// Original refusal, unchanged and distinct from verification failure.
146        source: Error,
147        /// Independent map surviving source evidence/parser drop.
148        context: SourceMap,
149    },
150}
151impl Evidence {
152    /// Makes a genuine independently charged copy, without infallible Clone.
153    /// Map admission precedes fact copying so an inner refusal can retain its
154    /// provenance without allocating while already handling that refusal.
155    #[expect(
156        clippy::result_large_err,
157        reason = "retain admitted source metadata with the original copy refusal; an error box would itself require unavailable capacity"
158    )]
159    pub fn try_copy_in<W: Work>(&self, work: &W) -> Result<Self, EvidenceCopyError> {
160        let context = self
161            .source
162            .try_copy_in(work)
163            .map_err(EvidenceCopyError::Provenance)?;
164        match self.facts.try_copy_in(work) {
165            Ok(facts) => Ok(Self {
166                facts,
167                source: context,
168            }),
169            Err(source) => Err(EvidenceCopyError::Facts { source, context }),
170        }
171    }
172    /// Generation/imprint facts bound to this successfully checked token.
173    #[must_use]
174    pub fn facts(&self) -> &RetainedFacts {
175        &self.facts
176    }
177    /// Independent original physical projection of logical facts.
178    #[must_use]
179    pub fn source(&self) -> &SourceMap {
180        &self.source
181    }
182}
183
184/// Common check facts including original ESS validation-set restrictions.
185pub struct CheckedTimestamp<'a> {
186    /// Actual CMS signature/content or explicit unusable preparation answer.
187    pub signature: TokenSignature<'a>,
188    /// Selected-certificate hint agreement, not pool uniqueness/trust.
189    pub identifier: directory::CertificateAgreement,
190    /// Original independent V1/V2 identifiers and signed path restrictions.
191    pub ess: ess::SigningAttributes<'a>,
192    /// Independent selected-certificate binding answers.
193    pub bindings: ess::Bindings,
194    /// Required TSA purpose and optional name agreement.
195    pub authority: AuthorityFacts,
196    /// Complete caller-covered-content agreement.
197    pub imprint: ImprintAgreement,
198    /// Complete live request-imprint equality, when supplied.
199    pub requested_imprint: Option<RequestAgreement>,
200    /// Independent requested nonce/policy agreement.
201    pub requested_fields: Option<RequestFields>,
202    /// Certificate carriage requirement, when requested.
203    pub carried: Option<bool>,
204    /// Explicit freshness answer, without an implicitly read clock.
205    pub timeliness: Timeliness,
206    /// Readable Accuracy scalar refusal, without clamping/narrowing.
207    pub accuracy_error: Option<super::DeviationError>,
208    /// Unknown critical identifiers with independently retained charges.
209    pub unknown_critical: OwnedVec<OwnedOid>,
210    evidence: Option<Evidence>,
211}
212impl CheckedTimestamp<'_> {
213    /// Moves independently retained evidence out while keeping every original
214    /// negative/check fact available to a scoped format adapter. No copy or
215    /// second signature/content pass occurs, and mutable public facts still
216    /// cannot manufacture evidence.
217    pub fn take_evidence(&mut self) -> Option<Evidence> {
218        self.evidence.take()
219    }
220    /// Moves retained evidence without deep copy or another storage charge.
221    pub fn into_evidence(self) -> Option<Evidence> {
222        self.evidence
223    }
224    /// Only the checker constructs evidence; editing public facts cannot do so.
225    #[must_use]
226    pub fn evidence(&self) -> Option<&Evidence> {
227        self.evidence.as_ref()
228    }
229}
230
231struct Information {
232    authority: AuthorityFacts,
233    imprint: ImprintAgreement,
234    requested_imprint: Option<RequestAgreement>,
235    requested_fields: Option<RequestFields>,
236    timeliness: Timeliness,
237    accuracy_error: Option<super::DeviationError>,
238    unknown: OwnedVec<OwnedOid>,
239    evidence: Option<Evidence>,
240}
241#[derive(Clone, Copy)]
242enum CheckPurpose {
243    Ordinary,
244    Historical,
245}
246
247impl Information {
248    fn holds(&self, purpose: CheckPurpose) -> bool {
249        self.accuracy_error.is_none()
250            && matches!(self.authority.usage, TsaUsage::TimeStampingOnly)
251            && matches!(
252                self.authority.identity,
253                TsaIdentity::Absent | TsaIdentity::Matches
254            )
255            && self.imprint == ImprintAgreement::Matches
256            && self
257                .requested_imprint
258                .is_none_or(|answer| answer == RequestAgreement::Matches)
259            && self
260                .requested_fields
261                .is_none_or(|fields| fields.nonce != Some(false) && fields.policy != Some(false))
262            && self.unknown.is_empty()
263            && match purpose {
264                CheckPurpose::Ordinary => matches!(
265                    self.timeliness,
266                    Timeliness::NotRequested | Timeliness::LocalWindow | Timeliness::RequestNonce
267                ),
268                // RFC4998 §5.3 qualifies archival TSA/path/algorithm validity
269                // through its explicit historical validator and next token.
270                // This pass asserts no live request/current-clock freshness.
271                CheckPurpose::Historical => self.timeliness == Timeliness::Unverified,
272            }
273    }
274}
275
276struct Inspection<'a, 'r, P, S, R> {
277    provider: &'a P,
278    context: &'r CheckContext<'a, 'r>,
279    covered: &'r mut S,
280    recognizes: &'r mut R,
281    common_holds: bool,
282    purpose: CheckPurpose,
283}
284
285impl<P, S, R> Inspection<'_, '_, P, S, R> {
286    fn inspect<'scope, PE, RE>(
287        &mut self,
288        info: &super::TokenInfo<'scope>,
289        inside: &Reader<'scope>,
290        source: &SourceMap,
291    ) -> Result<Information, InfoCheckError<S::Error, PE, RE>>
292    where
293        P: for<'input> SignatureProvider<'input, Error = PE>,
294        S: ComputedInput<P, PE>,
295        R: FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
296    {
297        let authority = info
298            .authority(
299                self.context.certificate,
300                inside,
301                self.context.certificate_parent,
302            )
303            .map_err(InfoCheckError::Authority)?;
304        let requested_fields = self
305            .context
306            .requested
307            .as_ref()
308            .map(|request| info.matches_request_fields(request.request, inside, request.parent))
309            .transpose()
310            .map_err(InfoCheckError::RequestField)?;
311        let unknown = unrecognized(info, inside, self.recognizes)?;
312        let (imprint, requested_imprint) = match &self.context.requested {
313            Some(request) => {
314                let (imprint, agreement) = info
315                    .imprint()
316                    .check_requested_input(
317                        request.request.imprint(),
318                        self.provider,
319                        (inside, request.parent),
320                        self.covered,
321                    )
322                    .map_err(|error| match error {
323                        super::RequestedContentError::Request(source) => {
324                            InfoCheckError::RequestImprint(source)
325                        }
326                        super::RequestedContentError::Content(source) => {
327                            InfoCheckError::Imprint(source)
328                        }
329                    })?;
330                (imprint, Some(agreement))
331            }
332            None => (
333                info.imprint()
334                    .check_input(self.provider, inside, self.covered)
335                    .map_err(InfoCheckError::Imprint)?,
336                None,
337            ),
338        };
339        finish_information(
340            info,
341            inside,
342            source,
343            self.context,
344            InformationFacts {
345                authority,
346                imprint,
347                requested_imprint,
348                requested_fields,
349                unknown,
350            },
351            self.common_holds,
352            self.purpose,
353        )
354    }
355}
356
357struct InformationFacts {
358    authority: AuthorityFacts,
359    imprint: ImprintAgreement,
360    requested_imprint: Option<RequestAgreement>,
361    requested_fields: Option<RequestFields>,
362    unknown: OwnedVec<OwnedOid>,
363}
364
365fn finish_information<SE, PE, RE>(
366    info: &super::TokenInfo<'_>,
367    inside: &Reader<'_>,
368    source: &SourceMap,
369    context: &CheckContext<'_, '_>,
370    facts: InformationFacts,
371    common_holds: bool,
372    purpose: CheckPurpose,
373) -> Result<Information, InfoCheckError<SE, PE, RE>> {
374    let InformationFacts {
375        authority,
376        imprint,
377        requested_imprint,
378        requested_fields,
379        unknown,
380    } = facts;
381    let (accuracy, accuracy_error) = match info
382        .accuracy()
383        .map(super::AccuracyReading::deviation)
384        .transpose()
385    {
386        Ok(accuracy) => (accuracy, None),
387        Err(error) => (None, Some(error)),
388    };
389    let timeliness = super::timeliness(
390        info.gen_time(),
391        accuracy,
392        context.utc,
393        context.reference,
394        context.requested.is_some(),
395        requested_fields.and_then(|fields| fields.nonce),
396        context.window,
397    );
398    let mut information = Information {
399        authority,
400        imprint,
401        requested_imprint,
402        requested_fields,
403        timeliness,
404        accuracy_error,
405        unknown,
406        evidence: None,
407    };
408    if common_holds && !info.has_faults() && information.holds(purpose) {
409        information.evidence = Some(Evidence {
410            facts: info
411                .retain_facts(inside)
412                .map_err(InfoCheckError::Processing)?,
413            source: source
414                .try_copy_in(&inside.execution())
415                .map_err(InfoCheckError::Provenance)?,
416        });
417    }
418    Ok(information)
419}
420
421fn unrecognized<SE, PE, RE>(
422    info: &super::TokenInfo<'_>,
423    inside: &Reader<'_>,
424    recognizes: &mut impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
425) -> Result<OwnedVec<OwnedOid>, InfoCheckError<SE, PE, RE>> {
426    let scoped = inside.execution();
427    let mut unknown = scoped.vector();
428    if let Some(extensions) = info.extensions() {
429        for kind in extensions
430            .unrecognized_critical(inside, recognizes)
431            .map_err(InfoCheckError::Recognition)?
432        {
433            let owned = OwnedOid::from_oid_in(kind, &scoped).map_err(|reason| {
434                InfoCheckError::Processing(Error::resource(
435                    reason,
436                    "tstInfo.extensions",
437                    extensions.span().start,
438                ))
439            })?;
440            unknown.try_push(owned).map_err(|reason| {
441                InfoCheckError::Processing(Error::resource(
442                    reason,
443                    "tstInfo.extensions",
444                    extensions.span().start,
445                ))
446            })?;
447        }
448    }
449    Ok(unknown.into_owned())
450}
451
452fn common_holds(
453    signature: &TokenSignature<'_>,
454    bindings: ess::Bindings,
455    identifier: &directory::CertificateAgreement,
456    carried: Option<bool>,
457) -> bool {
458    matches!(signature, TokenSignature::Checked(checked)
459        if checked.signature() == Some(SignatureAnswer::Valid)
460        && matches!(checked.content(), verify::ContentAgreement::Matches | verify::ContentAgreement::Direct))
461        && (bindings.v1 == Some(ess::Binding::Matches)
462            || bindings.v2 == Some(ess::Binding::Matches))
463        && bindings
464            .v1
465            .is_none_or(|binding| binding == ess::Binding::Matches)
466        && bindings
467            .v2
468            .is_none_or(|binding| binding == ess::Binding::Matches)
469        && matches!(identifier, directory::CertificateAgreement::Matches)
470        && carried != Some(false)
471}
472
473impl<'doc> Token<'doc> {
474    /// Checks original CMS, ESS, TSA, content, request and time together.
475    ///
476    /// Caller path/status/policy and signed ESS restriction enforcement remain
477    /// external. Semantic negatives remain facts, never evidence. Processing,
478    /// provider and source refusals abort. Provider errors are source-independent
479    /// so a temporary joined information buffer cannot escape in their type.
480    /// Optional rejection of unlisted digest algorithms (RFC5652 §5.3) belongs
481    /// to caller policy. Evidence does not assert that every sender SHOULD or
482    /// application-profile requirement was satisfied.
483    #[expect(
484        clippy::result_large_err,
485        reason = "preserve original cause and accounted source context without allocating on refusal"
486    )]
487    pub fn check<'a, P, S: Source, PE, RE>(
488        &'a self,
489        provider: &'a P,
490        parent: &Reader<'a>,
491        context: &CheckContext<'a, '_>,
492        covered: &mut S,
493        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
494    ) -> Result<CheckedTimestamp<'a>, CheckError<S::Error, PE, RE>>
495    where
496        'doc: 'a,
497        P: for<'scope> SignatureProvider<'scope, Error = PE>,
498    {
499        self.check_with_profile(provider, parent, context, covered, recognizes, |_| {
500            Ok::<(), ProfileFailure<Infallible>>(())
501        })
502        .map_err(ProfileCheckError::into_common)
503    }
504
505    /// Checks the same common timestamp once, with a scoped profile inspection
506    /// before any ESS binding, provider resolution, signature/content execution
507    /// or extension-recognition callback. Original syntax faults still refuse
508    /// first. The profile sees original fields, both producing readers and
509    /// their coordinate contexts; it must fund its own work before execution.
510    /// Refusal is terminal and cannot borrow a temporary information buffer.
511    /// The common core does not roll back external side effects of the profile.
512    #[expect(
513        clippy::result_large_err,
514        reason = "keep opaque common/profile refusal and its source context without allocating while returning an error"
515    )]
516    pub fn check_with_profile<'a, P, S: Source, PE, RE, FE>(
517        &'a self,
518        provider: &'a P,
519        parent: &Reader<'a>,
520        context: &CheckContext<'a, '_>,
521        covered: &mut S,
522        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
523        profile: impl for<'view, 'scope> FnOnce(
524            ProfileInput<'view, 'doc, 'a, 'scope>,
525        ) -> Result<(), ProfileFailure<FE>>,
526    ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<S::Error, PE, RE, FE>>
527    where
528        'doc: 'a,
529        P: for<'scope> SignatureProvider<'scope, Error = PE>,
530    {
531        self.check_input_with_profile(
532            provider,
533            parent,
534            context,
535            &mut RawInput::new(covered),
536            recognizes,
537            profile,
538        )
539    }
540
541    /// Same complete timestamp, with a computed imprint rather than raw bytes.
542    /// Common CMS/ESS/authority/time/profile checks execute once; only the
543    /// recipe defines the normative hash input. A closed completed comparison
544    /// is required before mathematical timestamp evidence can be retained.
545    #[expect(
546        clippy::result_large_err,
547        reason = "retain opaque input/provider/profile failures and original source maps"
548    )]
549    pub fn check_input_with_profile<'a, P, I, PE, RE, FE>(
550        &'a self,
551        provider: &'a P,
552        parent: &Reader<'a>,
553        context: &CheckContext<'a, '_>,
554        covered: &mut I,
555        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
556        profile: impl for<'view, 'scope> FnOnce(
557            ProfileInput<'view, 'doc, 'a, 'scope>,
558        ) -> Result<(), ProfileFailure<FE>>,
559    ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<I::Error, PE, RE, FE>>
560    where
561        'doc: 'a,
562        P: for<'scope> SignatureProvider<'scope, Error = PE>,
563        I: ComputedInput<P, PE>,
564    {
565        self.check_in(
566            provider,
567            (parent, None),
568            (context, CheckPurpose::Ordinary),
569            covered,
570            recognizes,
571            profile,
572        )
573    }
574
575    /// Archival mathematical/profile pass for ERS and historical `CAdES` `PoE`.
576    /// Its evidence establishes the bound mathematical timestamp, not current
577    /// trust/status or a complete archive. The format composition MUST close
578    /// historical authority, algorithm suitability and renewal chronology
579    /// independently before issuing its own complete preservation receipt.
580    /// Current-clock/live-request assertions are forbidden rather than ignored.
581    #[expect(
582        clippy::result_large_err,
583        reason = "historical checking preserves the same original mapped input and profile causes as the complete token check, without allocating on refusal"
584    )]
585    pub fn check_historical_input_with_profile<'a, P, I, PE, RE, FE>(
586        &'a self,
587        provider: &'a P,
588        parent: &Reader<'a>,
589        context: &CheckContext<'a, '_>,
590        covered: &mut I,
591        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
592        profile: impl for<'view, 'scope> FnOnce(
593            ProfileInput<'view, 'doc, 'a, 'scope>,
594        ) -> Result<(), ProfileFailure<FE>>,
595    ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<I::Error, PE, RE, FE>>
596    where
597        'doc: 'a,
598        P: for<'scope> SignatureProvider<'scope, Error = PE>,
599        I: ComputedInput<P, PE>,
600    {
601        if context.requested.is_some() || context.reference.is_some() || context.window.is_some() {
602            return Err(CheckError::Processing(Error::schema(
603                ErrorCode::InvalidLocation,
604                "tstInfo.historical",
605                self.message().span().start,
606            ))
607            .into());
608        }
609        self.check_in(
610            provider,
611            (parent, None),
612            (context, CheckPurpose::Historical),
613            covered,
614            recognizes,
615            profile,
616        )
617    }
618
619    /// Checks a token decoded in another joined source using the same core.
620    /// The map corresponds to its producing envelope reader. Child `TSTInfo`
621    /// maps are composed to physical fragments; outer CMS errors separately
622    /// retain the envelope map instead of borrowing a temporary joined buffer.
623    #[expect(
624        clippy::result_large_err,
625        reason = "preserve both source frames and original opaque refusal without a fallible error box"
626    )]
627    pub fn check_mapped<'a, P, S: Source, PE, RE>(
628        &'a self,
629        provider: &'a P,
630        source: (&Reader<'a>, &SourceMap),
631        context: &CheckContext<'a, '_>,
632        covered: &mut S,
633        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
634    ) -> Result<CheckedTimestamp<'a>, MappedCheckError<S::Error, PE, RE>>
635    where
636        'doc: 'a,
637        P: for<'scope> SignatureProvider<'scope, Error = PE>,
638    {
639        self.check_mapped_with_profile(provider, source, context, covered, recognizes, |_| {
640            Ok::<(), ProfileFailure<Infallible>>(())
641        })
642        .map_err(MappedProfileCheckError::into_common)
643    }
644
645    /// Same profile/core kernel for a logically joined CMS envelope. Both
646    /// independent maps survive refusal; an outer profile location must use
647    /// the envelope map rather than the inner information map. Reader/session
648    /// misuse is refused before map admission or profile/provider dispatch.
649    #[expect(
650        clippy::result_large_err,
651        reason = "preserve both independently retained source frames and the original opaque refusal without a fallible error allocation"
652    )]
653    pub fn check_mapped_with_profile<'a, P, S: Source, PE, RE, FE>(
654        &'a self,
655        provider: &'a P,
656        source: (&Reader<'a>, &SourceMap),
657        context: &CheckContext<'a, '_>,
658        covered: &mut S,
659        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
660        profile: impl for<'view, 'scope> FnOnce(
661            ProfileInput<'view, 'doc, 'a, 'scope>,
662        ) -> Result<(), ProfileFailure<FE>>,
663    ) -> Result<CheckedTimestamp<'a>, MappedProfileCheckError<S::Error, PE, RE, FE>>
664    where
665        'doc: 'a,
666        P: for<'scope> SignatureProvider<'scope, Error = PE>,
667    {
668        self.check_mapped_input_with_profile(
669            provider,
670            source,
671            context,
672            &mut RawInput::new(covered),
673            recognizes,
674            profile,
675        )
676    }
677
678    /// Computed input with the same independent envelope/information source
679    /// maps as the raw path. Foreign sessions refuse before profile dispatch.
680    #[expect(
681        clippy::result_large_err,
682        reason = "preserve both source frames and opaque refusal without allocating on failure"
683    )]
684    pub fn check_mapped_input_with_profile<'a, P, I, PE, RE, FE>(
685        &'a self,
686        provider: &'a P,
687        source: (&Reader<'a>, &SourceMap),
688        context: &CheckContext<'a, '_>,
689        covered: &mut I,
690        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
691        profile: impl for<'view, 'scope> FnOnce(
692            ProfileInput<'view, 'doc, 'a, 'scope>,
693        ) -> Result<(), ProfileFailure<FE>>,
694    ) -> Result<CheckedTimestamp<'a>, MappedProfileCheckError<I::Error, PE, RE, FE>>
695    where
696        'doc: 'a,
697        P: for<'scope> SignatureProvider<'scope, Error = PE>,
698        I: ComputedInput<P, PE>,
699    {
700        let (parent, source) = source;
701        preflight_sessions(self, parent, context)
702            .map_err(|error| MappedProfileCheckError::Preflight(error.into()))?;
703        let envelope = source
704            .try_copy_in(&parent.execution())
705            .map_err(MappedProfileCheckError::Provenance)?;
706        self.check_in(
707            provider,
708            (parent, Some(source)),
709            (context, CheckPurpose::Ordinary),
710            covered,
711            recognizes,
712            profile,
713        )
714        .map_err(|source| MappedProfileCheckError::Execution { envelope, source })
715    }
716
717    #[expect(
718        clippy::result_large_err,
719        reason = "retain original cause and accounted source context on refusal"
720    )]
721    fn check_in<'a, P, S, PE, RE, FE>(
722        &'a self,
723        provider: &'a P,
724        origin: (&Reader<'a>, Option<&SourceMap>),
725        context: (&CheckContext<'a, '_>, CheckPurpose),
726        covered: &mut S,
727        mut recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
728        profile: impl for<'view, 'scope> FnOnce(
729            ProfileInput<'view, 'doc, 'a, 'scope>,
730        ) -> Result<(), ProfileFailure<FE>>,
731    ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<S::Error, PE, RE, FE>>
732    where
733        'doc: 'a,
734        P: for<'scope> SignatureProvider<'scope, Error = PE>,
735        S: ComputedInput<P, PE>,
736    {
737        let (parent, source) = origin;
738        let (context, purpose) = context;
739        preflight(self, parent, context).map_err(CheckError::from)?;
740        let certificate = context.certificate;
741        let identifier =
742            directory::matches_certificate_in(self.signer().sid(), certificate, parent)
743                .map_err(CheckError::Identifier)?;
744        let ess =
745            ess::SigningAttributes::read(self.signer(), parent).map_err(CheckError::Processing)?;
746        let carried = context
747            .requested
748            .as_ref()
749            .map(|requested| {
750                self.matches_cert_req(
751                    requested.request,
752                    Some(certificate),
753                    parent,
754                    requested.parent,
755                )
756            })
757            .transpose()
758            .map_err(CheckError::RequestField)?;
759        if !self.faults().is_empty()
760            || !self.message().faults().is_empty()
761            || !self.signer().faults().is_empty()
762            || !ess.faults().is_empty()
763            || ess.v1().is_some_and(|reading| !reading.faults().is_empty())
764            || ess.v2().is_some_and(|reading| !reading.faults().is_empty())
765        {
766            return Err(CheckError::Nonconforming {
767                span: self.message().span(),
768            }
769            .into());
770        }
771        let inspect = |info: &super::TokenInfo<'_>,
772                       inside: &Reader<'_>,
773                       information: &SourceMap|
774         -> Result<_, profile::InformationFailure<S::Error, PE, RE, FE>> {
775            if info.has_faults() {
776                return Err(InfoCheckError::Nonconforming { span: info.span() }.into());
777            }
778            profile(ProfileInput {
779                token: self,
780                parent,
781                envelope: source,
782                ess: &ess,
783                info,
784                reader: inside,
785                source: information,
786            })
787            .map_err(profile::InformationFailure::Profile)?;
788            let bindings = ess
789                .bind_certificate(provider, certificate, parent)
790                .map_err(InfoCheckError::Ess)?;
791            let signature = self
792                .check_signature(provider, context.key, parent)
793                .map_err(InfoCheckError::Signature)?;
794            let common_holds = common_holds(&signature, bindings, &identifier, carried);
795            let mut inspection = Inspection {
796                provider,
797                context,
798                covered,
799                recognizes: &mut recognizes,
800                common_holds,
801                purpose,
802            };
803            let checked = inspection.inspect(info, inside, information)?;
804            Ok((signature, bindings, checked))
805        };
806        let (signature, bindings, information) = match source {
807            Some(source) => self.with_info_mapped(parent, source, inspect),
808            None => self.with_info(parent, inspect),
809        }
810        .map_err(profile::information_error)?;
811        Ok(CheckedTimestamp {
812            signature,
813            identifier,
814            ess,
815            bindings,
816            authority: information.authority,
817            imprint: information.imprint,
818            requested_imprint: information.requested_imprint,
819            requested_fields: information.requested_fields,
820            carried,
821            timeliness: information.timeliness,
822            accuracy_error: information.accuracy_error,
823            unknown_critical: information.unknown,
824            evidence: information.evidence,
825        })
826    }
827}
828
829enum PreflightError {
830    Processing(Error),
831    Requested(super::RequestFieldError),
832    KeyMismatch(structured_asn1::Span),
833}
834
835impl<SE, PE, RE> From<PreflightError> for CheckError<SE, PE, RE> {
836    fn from(error: PreflightError) -> Self {
837        match error {
838            PreflightError::Processing(error) => Self::Processing(error),
839            PreflightError::Requested(error) => Self::RequestField(error),
840            PreflightError::KeyMismatch(certificate) => Self::KeyMismatch { certificate },
841        }
842    }
843}
844
845fn preflight(
846    token: &Token<'_>,
847    parent: &Reader<'_>,
848    context: &CheckContext<'_, '_>,
849) -> Result<(), PreflightError> {
850    preflight_sessions(token, parent, context)?;
851    let certificate = context.certificate;
852    let written = certificate.tbs().public_key().as_bytes();
853    let key = context.key.key().as_bytes();
854    let length = u64::try_from(written.len().max(key.len()))
855        .ok()
856        .and_then(|length| length.checked_add(1))
857        .ok_or_else(|| {
858            PreflightError::Processing(Error::schema(
859                ErrorCode::LengthOverflow,
860                "certificate.subjectPublicKeyInfo",
861                certificate.span().start,
862            ))
863        })?;
864    parent.execution().charge_work(length).map_err(|reason| {
865        PreflightError::Processing(Error::resource(
866            reason,
867            "certificate.subjectPublicKeyInfo",
868            certificate.span().start,
869        ))
870    })?;
871    if written != key {
872        return Err(PreflightError::KeyMismatch(certificate.span()));
873    }
874    Ok(())
875}
876
877fn preflight_sessions(
878    token: &Token<'_>,
879    parent: &Reader<'_>,
880    context: &CheckContext<'_, '_>,
881) -> Result<(), PreflightError> {
882    token
883        .signer()
884        .require_session(parent, "signerInfo")
885        .map_err(PreflightError::Processing)?;
886    let certificate = context.certificate;
887    if !certificate
888        .tbs()
889        .public_key()
890        .algorithm()
891        .same_session(context.certificate_parent)
892    {
893        return Err(PreflightError::Processing(Error::pkix(
894            structured_pkix::Error::new(
895                structured_pkix::ErrorCode::ForeignReader,
896                "certificate",
897                certificate.span().start,
898            ),
899        )));
900    }
901    if let Some(requested) = &context.requested
902        && !requested.request.same_session(requested.parent)
903    {
904        return Err(PreflightError::Requested(super::RequestFieldError {
905            operand: super::ImprintOperand::Requested,
906            source: Error::pkix(structured_pkix::Error::new(
907                structured_pkix::ErrorCode::ForeignReader,
908                "timeStampReq",
909                requested.request.span().start,
910            )),
911        }));
912    }
913    Ok(())
914}