Skip to main content

structured_cms/timestamp/
check.rs

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