Skip to main content

structured_cms/timestamp/
check.rs

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