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, ImprintAgreement, RecognitionError, Request, RequestAgreement, RequestFields,
8    RequestWindow, RetainedFacts, SignatureError, Timeliness, Token, TokenExtension,
9    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 retained evidence without deep copy or another storage charge.
212    pub fn into_evidence(self) -> Option<Evidence> {
213        self.evidence
214    }
215    /// Only the checker constructs evidence; editing public facts cannot do so.
216    #[must_use]
217    pub fn evidence(&self) -> Option<&Evidence> {
218        self.evidence.as_ref()
219    }
220}
221
222struct Information {
223    authority: AuthorityFacts,
224    imprint: ImprintAgreement,
225    requested_imprint: Option<RequestAgreement>,
226    requested_fields: Option<RequestFields>,
227    timeliness: Timeliness,
228    accuracy_error: Option<super::DeviationError>,
229    unknown: OwnedVec<OwnedOid>,
230    evidence: Option<Evidence>,
231}
232
233impl Information {
234    fn holds(&self) -> bool {
235        self.accuracy_error.is_none()
236            && matches!(self.authority.usage, TsaUsage::TimeStampingOnly)
237            && matches!(
238                self.authority.identity,
239                TsaIdentity::Absent | TsaIdentity::Matches
240            )
241            && self.imprint == ImprintAgreement::Matches
242            && self
243                .requested_imprint
244                .is_none_or(|answer| answer == RequestAgreement::Matches)
245            && self
246                .requested_fields
247                .is_none_or(|fields| fields.nonce != Some(false) && fields.policy != Some(false))
248            && self.unknown.is_empty()
249            && matches!(
250                self.timeliness,
251                Timeliness::NotRequested | Timeliness::LocalWindow | Timeliness::RequestNonce
252            )
253    }
254}
255
256struct Inspection<'a, 'r, P, S, R> {
257    provider: &'a P,
258    context: &'r CheckContext<'a, 'r>,
259    covered: &'r mut S,
260    recognizes: &'r mut R,
261    common_holds: bool,
262}
263
264impl<P, S: Source, R> Inspection<'_, '_, P, S, R> {
265    fn inspect<'scope, PE, RE>(
266        &mut self,
267        info: &super::TokenInfo<'scope>,
268        inside: &Reader<'scope>,
269        source: &SourceMap,
270    ) -> Result<Information, InfoCheckError<S::Error, PE, RE>>
271    where
272        P: for<'input> SignatureProvider<'input, Error = PE>,
273        R: FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
274    {
275        let authority = info
276            .authority(
277                self.context.certificate,
278                inside,
279                self.context.certificate_parent,
280            )
281            .map_err(InfoCheckError::Authority)?;
282        let requested_fields = self
283            .context
284            .requested
285            .as_ref()
286            .map(|request| info.matches_request_fields(request.request, inside, request.parent))
287            .transpose()
288            .map_err(InfoCheckError::RequestField)?;
289        let unknown = unrecognized(info, inside, self.recognizes)?;
290        let (imprint, requested_imprint) = match &self.context.requested {
291            Some(request) => {
292                let (imprint, agreement) = info
293                    .imprint()
294                    .check_requested_content(
295                        request.request.imprint(),
296                        self.provider,
297                        (inside, request.parent),
298                        self.covered,
299                    )
300                    .map_err(|error| match error {
301                        super::RequestedContentError::Request(source) => {
302                            InfoCheckError::RequestImprint(source)
303                        }
304                        super::RequestedContentError::Content(source) => {
305                            InfoCheckError::Imprint(source)
306                        }
307                    })?;
308                (imprint, Some(agreement))
309            }
310            None => (
311                info.imprint()
312                    .check_content(self.provider, inside, self.covered)
313                    .map_err(InfoCheckError::Imprint)?,
314                None,
315            ),
316        };
317        let (accuracy, accuracy_error) = match info
318            .accuracy()
319            .map(super::AccuracyReading::deviation)
320            .transpose()
321        {
322            Ok(accuracy) => (accuracy, None),
323            Err(error) => (None, Some(error)),
324        };
325        let timeliness = super::timeliness(
326            info.gen_time(),
327            accuracy,
328            self.context.utc,
329            self.context.reference,
330            self.context.requested.is_some(),
331            requested_fields.and_then(|fields| fields.nonce),
332            self.context.window,
333        );
334        let mut information = Information {
335            authority,
336            imprint,
337            requested_imprint,
338            requested_fields,
339            timeliness,
340            accuracy_error,
341            unknown,
342            evidence: None,
343        };
344        if self.common_holds && !info.has_faults() && information.holds() {
345            information.evidence = Some(Evidence {
346                facts: info
347                    .retain_facts(inside)
348                    .map_err(InfoCheckError::Processing)?,
349                source: source
350                    .try_copy_in(&inside.execution())
351                    .map_err(InfoCheckError::Provenance)?,
352            });
353        }
354        Ok(information)
355    }
356}
357
358fn unrecognized<SE, PE, RE>(
359    info: &super::TokenInfo<'_>,
360    inside: &Reader<'_>,
361    recognizes: &mut impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
362) -> Result<OwnedVec<OwnedOid>, InfoCheckError<SE, PE, RE>> {
363    let scoped = inside.execution();
364    let mut unknown = scoped.vector();
365    if let Some(extensions) = info.extensions() {
366        for kind in extensions
367            .unrecognized_critical(inside, recognizes)
368            .map_err(InfoCheckError::Recognition)?
369        {
370            let owned = OwnedOid::from_oid_in(kind, &scoped).map_err(|reason| {
371                InfoCheckError::Processing(Error::resource(
372                    reason,
373                    "tstInfo.extensions",
374                    extensions.span().start,
375                ))
376            })?;
377            unknown.try_push(owned).map_err(|reason| {
378                InfoCheckError::Processing(Error::resource(
379                    reason,
380                    "tstInfo.extensions",
381                    extensions.span().start,
382                ))
383            })?;
384        }
385    }
386    Ok(unknown.into_owned())
387}
388
389fn common_holds(
390    signature: &TokenSignature<'_>,
391    bindings: ess::Bindings,
392    identifier: &directory::CertificateAgreement,
393    carried: Option<bool>,
394) -> bool {
395    matches!(signature, TokenSignature::Checked(checked)
396        if checked.signature() == Some(SignatureAnswer::Valid)
397        && matches!(checked.content(), verify::ContentAgreement::Matches | verify::ContentAgreement::Direct))
398        && (bindings.v1 == Some(ess::Binding::Matches)
399            || bindings.v2 == Some(ess::Binding::Matches))
400        && bindings
401            .v1
402            .is_none_or(|binding| binding == ess::Binding::Matches)
403        && bindings
404            .v2
405            .is_none_or(|binding| binding == ess::Binding::Matches)
406        && matches!(identifier, directory::CertificateAgreement::Matches)
407        && carried != Some(false)
408}
409
410impl<'doc> Token<'doc> {
411    /// Checks original CMS, ESS, TSA, content, request and time together.
412    ///
413    /// Caller path/status/policy and signed ESS restriction enforcement remain
414    /// external. Semantic negatives remain facts, never evidence. Processing,
415    /// provider and source refusals abort. Provider errors are source-independent
416    /// so a temporary joined information buffer cannot escape in their type.
417    /// Optional rejection of unlisted digest algorithms (RFC5652 ยง5.3) belongs
418    /// to caller policy. Evidence does not assert that every sender SHOULD or
419    /// application-profile requirement was satisfied.
420    #[expect(
421        clippy::result_large_err,
422        reason = "preserve original cause and accounted source context without allocating on refusal"
423    )]
424    pub fn check<'a, P, S: Source, PE, RE>(
425        &'a self,
426        provider: &'a P,
427        parent: &Reader<'a>,
428        context: &CheckContext<'a, '_>,
429        covered: &mut S,
430        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
431    ) -> Result<CheckedTimestamp<'a>, CheckError<S::Error, PE, RE>>
432    where
433        'doc: 'a,
434        P: for<'scope> SignatureProvider<'scope, Error = PE>,
435    {
436        self.check_with_profile(provider, parent, context, covered, recognizes, |_| {
437            Ok::<(), ProfileFailure<Infallible>>(())
438        })
439        .map_err(ProfileCheckError::into_common)
440    }
441
442    /// Checks the same common timestamp once, with a scoped profile inspection
443    /// before any ESS binding, provider resolution, signature/content execution
444    /// or extension-recognition callback. Original syntax faults still refuse
445    /// first. The profile sees original fields, both producing readers and
446    /// their coordinate contexts; it must fund its own work before execution.
447    /// Refusal is terminal and cannot borrow a temporary information buffer.
448    /// The common core does not roll back external side effects of the profile.
449    #[expect(
450        clippy::result_large_err,
451        reason = "keep opaque common/profile refusal and its source context without allocating while returning an error"
452    )]
453    pub fn check_with_profile<'a, P, S: Source, PE, RE, FE>(
454        &'a self,
455        provider: &'a P,
456        parent: &Reader<'a>,
457        context: &CheckContext<'a, '_>,
458        covered: &mut S,
459        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
460        profile: impl for<'view, 'scope> FnOnce(
461            ProfileInput<'view, 'doc, 'a, 'scope>,
462        ) -> Result<(), ProfileFailure<FE>>,
463    ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<S::Error, PE, RE, FE>>
464    where
465        'doc: 'a,
466        P: for<'scope> SignatureProvider<'scope, Error = PE>,
467    {
468        self.check_in(
469            provider,
470            (parent, None),
471            context,
472            covered,
473            recognizes,
474            profile,
475        )
476    }
477
478    /// Checks a token decoded in another joined source using the same core.
479    /// The map corresponds to its producing envelope reader. Child `TSTInfo`
480    /// maps are composed to physical fragments; outer CMS errors separately
481    /// retain the envelope map instead of borrowing a temporary joined buffer.
482    #[expect(
483        clippy::result_large_err,
484        reason = "preserve both source frames and original opaque refusal without a fallible error box"
485    )]
486    pub fn check_mapped<'a, P, S: Source, PE, RE>(
487        &'a self,
488        provider: &'a P,
489        source: (&Reader<'a>, &SourceMap),
490        context: &CheckContext<'a, '_>,
491        covered: &mut S,
492        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
493    ) -> Result<CheckedTimestamp<'a>, MappedCheckError<S::Error, PE, RE>>
494    where
495        'doc: 'a,
496        P: for<'scope> SignatureProvider<'scope, Error = PE>,
497    {
498        self.check_mapped_with_profile(provider, source, context, covered, recognizes, |_| {
499            Ok::<(), ProfileFailure<Infallible>>(())
500        })
501        .map_err(MappedProfileCheckError::into_common)
502    }
503
504    /// Same profile/core kernel for a logically joined CMS envelope. Both
505    /// independent maps survive refusal; an outer profile location must use
506    /// the envelope map rather than the inner information map. Reader/session
507    /// misuse is refused before map admission or profile/provider dispatch.
508    #[expect(
509        clippy::result_large_err,
510        reason = "preserve both independently retained source frames and the original opaque refusal without a fallible error allocation"
511    )]
512    pub fn check_mapped_with_profile<'a, P, S: Source, PE, RE, FE>(
513        &'a self,
514        provider: &'a P,
515        source: (&Reader<'a>, &SourceMap),
516        context: &CheckContext<'a, '_>,
517        covered: &mut S,
518        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
519        profile: impl for<'view, 'scope> FnOnce(
520            ProfileInput<'view, 'doc, 'a, 'scope>,
521        ) -> Result<(), ProfileFailure<FE>>,
522    ) -> Result<CheckedTimestamp<'a>, MappedProfileCheckError<S::Error, PE, RE, FE>>
523    where
524        'doc: 'a,
525        P: for<'scope> SignatureProvider<'scope, Error = PE>,
526    {
527        let (parent, source) = source;
528        preflight_sessions(self, parent, context)
529            .map_err(|error| MappedProfileCheckError::Preflight(error.into()))?;
530        let envelope = source
531            .try_copy_in(&parent.execution())
532            .map_err(MappedProfileCheckError::Provenance)?;
533        self.check_in(
534            provider,
535            (parent, Some(source)),
536            context,
537            covered,
538            recognizes,
539            profile,
540        )
541        .map_err(|source| MappedProfileCheckError::Execution { envelope, source })
542    }
543
544    #[expect(
545        clippy::result_large_err,
546        reason = "retain original cause and accounted source context on refusal"
547    )]
548    fn check_in<'a, P, S: Source, PE, RE, FE>(
549        &'a self,
550        provider: &'a P,
551        origin: (&Reader<'a>, Option<&SourceMap>),
552        context: &CheckContext<'a, '_>,
553        covered: &mut S,
554        mut recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
555        profile: impl for<'view, 'scope> FnOnce(
556            ProfileInput<'view, 'doc, 'a, 'scope>,
557        ) -> Result<(), ProfileFailure<FE>>,
558    ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<S::Error, PE, RE, FE>>
559    where
560        'doc: 'a,
561        P: for<'scope> SignatureProvider<'scope, Error = PE>,
562    {
563        let (parent, source) = origin;
564        preflight(self, parent, context).map_err(CheckError::from)?;
565        let certificate = context.certificate;
566        let identifier =
567            directory::matches_certificate_in(self.signer().sid(), certificate, parent)
568                .map_err(CheckError::Identifier)?;
569        let ess =
570            ess::SigningAttributes::read(self.signer(), parent).map_err(CheckError::Processing)?;
571        let carried = context
572            .requested
573            .as_ref()
574            .map(|requested| {
575                self.matches_cert_req(
576                    requested.request,
577                    Some(certificate),
578                    parent,
579                    requested.parent,
580                )
581            })
582            .transpose()
583            .map_err(CheckError::RequestField)?;
584        if !self.faults().is_empty()
585            || !self.message().faults().is_empty()
586            || !self.signer().faults().is_empty()
587            || !ess.faults().is_empty()
588            || ess.v1().is_some_and(|reading| !reading.faults().is_empty())
589            || ess.v2().is_some_and(|reading| !reading.faults().is_empty())
590        {
591            return Err(CheckError::Nonconforming {
592                span: self.message().span(),
593            }
594            .into());
595        }
596        let inspect = |info: &super::TokenInfo<'_>,
597                       inside: &Reader<'_>,
598                       information: &SourceMap|
599         -> Result<_, profile::InformationFailure<S::Error, PE, RE, FE>> {
600            if info.has_faults() {
601                return Err(InfoCheckError::Nonconforming { span: info.span() }.into());
602            }
603            profile(ProfileInput {
604                token: self,
605                parent,
606                envelope: source,
607                ess: &ess,
608                info,
609                reader: inside,
610                source: information,
611            })
612            .map_err(profile::InformationFailure::Profile)?;
613            let bindings = ess
614                .bind_certificate(provider, certificate, parent)
615                .map_err(InfoCheckError::Ess)?;
616            let signature = self
617                .check_signature(provider, context.key, parent)
618                .map_err(InfoCheckError::Signature)?;
619            let common_holds = common_holds(&signature, bindings, &identifier, carried);
620            let mut inspection = Inspection {
621                provider,
622                context,
623                covered,
624                recognizes: &mut recognizes,
625                common_holds,
626            };
627            let checked = inspection.inspect(info, inside, information)?;
628            Ok((signature, bindings, checked))
629        };
630        let (signature, bindings, information) = match source {
631            Some(source) => self.with_info_mapped(parent, source, inspect),
632            None => self.with_info(parent, inspect),
633        }
634        .map_err(profile::information_error)?;
635        Ok(CheckedTimestamp {
636            signature,
637            identifier,
638            ess,
639            bindings,
640            authority: information.authority,
641            imprint: information.imprint,
642            requested_imprint: information.requested_imprint,
643            requested_fields: information.requested_fields,
644            carried,
645            timeliness: information.timeliness,
646            accuracy_error: information.accuracy_error,
647            unknown_critical: information.unknown,
648            evidence: information.evidence,
649        })
650    }
651}
652
653enum PreflightError {
654    Processing(Error),
655    Requested(super::RequestFieldError),
656    KeyMismatch(structured_asn1::Span),
657}
658
659impl<SE, PE, RE> From<PreflightError> for CheckError<SE, PE, RE> {
660    fn from(error: PreflightError) -> Self {
661        match error {
662            PreflightError::Processing(error) => Self::Processing(error),
663            PreflightError::Requested(error) => Self::RequestField(error),
664            PreflightError::KeyMismatch(certificate) => Self::KeyMismatch { certificate },
665        }
666    }
667}
668
669fn preflight(
670    token: &Token<'_>,
671    parent: &Reader<'_>,
672    context: &CheckContext<'_, '_>,
673) -> Result<(), PreflightError> {
674    preflight_sessions(token, parent, context)?;
675    let certificate = context.certificate;
676    let written = certificate.tbs().public_key().as_bytes();
677    let key = context.key.key().as_bytes();
678    let length = u64::try_from(written.len().max(key.len()))
679        .ok()
680        .and_then(|length| length.checked_add(1))
681        .ok_or_else(|| {
682            PreflightError::Processing(Error::schema(
683                ErrorCode::LengthOverflow,
684                "certificate.subjectPublicKeyInfo",
685                certificate.span().start,
686            ))
687        })?;
688    parent.execution().charge_work(length).map_err(|reason| {
689        PreflightError::Processing(Error::resource(
690            reason,
691            "certificate.subjectPublicKeyInfo",
692            certificate.span().start,
693        ))
694    })?;
695    if written != key {
696        return Err(PreflightError::KeyMismatch(certificate.span()));
697    }
698    Ok(())
699}
700
701fn preflight_sessions(
702    token: &Token<'_>,
703    parent: &Reader<'_>,
704    context: &CheckContext<'_, '_>,
705) -> Result<(), PreflightError> {
706    token
707        .signer()
708        .require_session(parent, "signerInfo")
709        .map_err(PreflightError::Processing)?;
710    let certificate = context.certificate;
711    if !certificate
712        .tbs()
713        .public_key()
714        .algorithm()
715        .same_session(context.certificate_parent)
716    {
717        return Err(PreflightError::Processing(Error::pkix(
718            structured_pkix::Error::new(
719                structured_pkix::ErrorCode::ForeignReader,
720                "certificate",
721                certificate.span().start,
722            ),
723        )));
724    }
725    if let Some(requested) = &context.requested
726        && !requested.request.same_session(requested.parent)
727    {
728        return Err(PreflightError::Requested(super::RequestFieldError {
729            operand: super::ImprintOperand::Requested,
730            source: Error::pkix(structured_pkix::Error::new(
731                structured_pkix::ErrorCode::ForeignReader,
732                "timeStampReq",
733                requested.request.span().start,
734            )),
735        }));
736    }
737    Ok(())
738}