Skip to main content

structured_cms/timestamp/
check.rs

1//! Complete common timestamp checks; path/status/policy acceptance stays external.
2
3use super::{
4    AuthorityFacts, ImprintAgreement, RecognitionError, Request, RequestAgreement, RequestFields,
5    RequestWindow, RetainedFacts, SignatureError, Timeliness, Token, TokenExtension,
6    TokenSignature, TsaIdentity, TsaUsage,
7};
8use crate::{
9    Error, ErrorCode,
10    content_source::{ContentError, SourceMap},
11    directory, ess,
12    provider::{SignatureAnswer, SignatureProvider},
13    stream::Source,
14    verify,
15};
16use structured_asn1::{
17    Reader,
18    memory::{Execution, OwnedVec, Work},
19    value::OwnedOid,
20};
21use structured_pkix::{
22    Certificate,
23    path::WorkingKey,
24    temporal::{Instant, LeapTable},
25};
26
27/// Original parsed request and its producing reader, not reconstructed tuples.
28pub struct Requested<'a, 'r> {
29    /// Complete original request.
30    pub request: &'r Request<'a>,
31    /// Its own original producing session.
32    pub parent: &'r Reader<'a>,
33}
34
35/// Explicit caller selection/time facts; this does not select a trusted path.
36pub struct CheckContext<'a, 'r> {
37    /// Caller-selected candidate, independently subject to trust/path checks.
38    pub certificate: &'r Certificate<'a>,
39    /// That certificate's own original producing reader.
40    pub certificate_parent: &'r Reader<'a>,
41    /// Effective key retaining independently established inherited parameters.
42    pub key: WorkingKey<'a>,
43    /// Complete live request, absent for archived evidence.
44    pub requested: Option<Requested<'a, 'r>>,
45    /// Explicit UTC correction facts, without implicit clock lookup.
46    pub utc: &'r LeapTable<'a>,
47    /// Caller-supplied comparison instant, when available.
48    pub reference: Option<Instant>,
49    /// Caller-established outstanding-imprint uniqueness/window fact.
50    pub window: Option<RequestWindow>,
51}
52
53/// Scoped execution refusal retaining the actual source/operand and cause.
54#[derive(Debug)]
55pub enum InfoCheckError<SE, PE, RE> {
56    /// Inspection retained schema faults; complete execution did not start.
57    Nonconforming {
58        /// Original information range, interpreted through the accompanying map.
59        span: structured_asn1::Span,
60    },
61    /// Original CMS execution failed after information syntax preflight.
62    Signature(SignatureError<PE>),
63    /// Original ESS execution failed after information syntax preflight.
64    Ess(ess::BindingError<PE>),
65    /// Original token processing/resource refusal.
66    Processing(Error),
67    /// Source-map retention refusal already located at a physical carrier.
68    /// Do not project this offset again as a logical token coordinate.
69    Provenance(Error),
70    /// Covered-content acquisition or selected digest execution failed.
71    Imprint(super::ImprintError<SE, PE>),
72    /// Complete request-imprint comparison failed.
73    RequestImprint(super::RequestImprintError<PE>),
74    /// Request nonce/policy comparison failed.
75    RequestField(super::RequestFieldError),
76    /// Token/certificate source role and original refusal.
77    Authority(super::AuthorityError),
78    /// Original extension handler refusal.
79    Recognition(RecognitionError<RE>),
80}
81
82/// Execution refusal, never a semantic partial success.
83#[derive(Debug)]
84pub enum CheckError<SE, PE, RE> {
85    /// Original envelope/ESS inspection faults prevent complete execution.
86    Nonconforming {
87        /// Original outer field enclosure, not a resource error.
88        span: structured_asn1::Span,
89    },
90    /// Original outer processing/resource refusal.
91    Processing(Error),
92    /// Caller supplied a different original SPKI for the selected certificate.
93    KeyMismatch {
94        /// Honest enclosure of the selected certificate, not a document fault.
95        certificate: structured_asn1::Span,
96    },
97    /// Certificate hint comparison failed, not a mismatch.
98    Identifier(directory::Error),
99    /// Request source role and original refusal are preserved.
100    RequestField(super::RequestFieldError),
101    /// Scoped original cause accompanied by independently retained source map.
102    Information(ContentError<super::InfoError<InfoCheckError<SE, PE, RE>>>),
103}
104
105/// A check in a logical CMS envelope retains its own source context separately
106/// from the inner `TSTInfo` map. CMS errors must not use `TSTInfo` coordinates.
107#[derive(Debug)]
108pub enum MappedCheckError<SE, PE, RE> {
109    /// Caller source/session mismatch, refused before map admission or work.
110    /// The original operand identifies its reader; no copied map is required.
111    Preflight(CheckError<SE, PE, RE>),
112    /// Envelope map admission failed at its already physical carrier.
113    Provenance(Error),
114    /// Original cause and independently retained envelope coordinate context.
115    Execution {
116        /// Maps the original CMS envelope's coordinates, not inner `TSTInfo`.
117        envelope: SourceMap,
118        /// Original check cause; source/operand variants select its context.
119        source: CheckError<SE, PE, RE>,
120    },
121}
122
123/// Independently retained cryptographic/time evidence, not a trust verdict.
124/// The caller must establish TSA path/status, enforce signed ESS restrictions
125/// and apply algorithm/policy permissions before granting product acceptance.
126#[derive(Debug)]
127pub struct Evidence {
128    facts: RetainedFacts,
129    source: SourceMap,
130}
131
132/// Independent-copy refusal retains the coordinate context of its cause.
133#[derive(Debug)]
134pub enum EvidenceCopyError {
135    /// Map admission failed at its already physical carrier enclosure.
136    Provenance(Error),
137    /// Fact admission failed in logical/original fact coordinates.
138    Facts {
139        /// Original refusal, unchanged and distinct from verification failure.
140        source: Error,
141        /// Independent map surviving source evidence/parser drop.
142        context: SourceMap,
143    },
144}
145impl Evidence {
146    /// Makes a genuine independently charged copy, without infallible Clone.
147    /// Map admission precedes fact copying so an inner refusal can retain its
148    /// provenance without allocating while already handling that refusal.
149    #[expect(
150        clippy::result_large_err,
151        reason = "retain admitted source metadata with the original copy refusal; an error box would itself require unavailable capacity"
152    )]
153    pub fn try_copy_in<W: Work>(&self, work: &W) -> Result<Self, EvidenceCopyError> {
154        let context = self
155            .source
156            .try_copy_in(work)
157            .map_err(EvidenceCopyError::Provenance)?;
158        match self.facts.try_copy_in(work) {
159            Ok(facts) => Ok(Self {
160                facts,
161                source: context,
162            }),
163            Err(source) => Err(EvidenceCopyError::Facts { source, context }),
164        }
165    }
166    /// Generation/imprint facts bound to this successfully checked token.
167    #[must_use]
168    pub fn facts(&self) -> &RetainedFacts {
169        &self.facts
170    }
171    /// Independent original physical projection of logical facts.
172    #[must_use]
173    pub fn source(&self) -> &SourceMap {
174        &self.source
175    }
176}
177
178/// Common check facts including original ESS validation-set restrictions.
179pub struct CheckedTimestamp<'a> {
180    /// Actual CMS signature/content or explicit unusable preparation answer.
181    pub signature: TokenSignature<'a>,
182    /// Selected-certificate hint agreement, not pool uniqueness/trust.
183    pub identifier: directory::CertificateAgreement,
184    /// Original independent V1/V2 identifiers and signed path restrictions.
185    pub ess: ess::SigningAttributes<'a>,
186    /// Independent selected-certificate binding answers.
187    pub bindings: ess::Bindings,
188    /// Required TSA purpose and optional name agreement.
189    pub authority: AuthorityFacts,
190    /// Complete caller-covered-content agreement.
191    pub imprint: ImprintAgreement,
192    /// Complete live request-imprint equality, when supplied.
193    pub requested_imprint: Option<RequestAgreement>,
194    /// Independent requested nonce/policy agreement.
195    pub requested_fields: Option<RequestFields>,
196    /// Certificate carriage requirement, when requested.
197    pub carried: Option<bool>,
198    /// Explicit freshness answer, without an implicitly read clock.
199    pub timeliness: Timeliness,
200    /// Readable Accuracy scalar refusal, without clamping/narrowing.
201    pub accuracy_error: Option<super::DeviationError>,
202    /// Unknown critical identifiers with independently retained charges.
203    pub unknown_critical: OwnedVec<OwnedOid>,
204    evidence: Option<Evidence>,
205}
206impl CheckedTimestamp<'_> {
207    /// Moves retained evidence without deep copy or another storage charge.
208    pub fn into_evidence(self) -> Option<Evidence> {
209        self.evidence
210    }
211    /// Only the checker constructs evidence; editing public facts cannot do so.
212    #[must_use]
213    pub fn evidence(&self) -> Option<&Evidence> {
214        self.evidence.as_ref()
215    }
216}
217
218struct Information {
219    authority: AuthorityFacts,
220    imprint: ImprintAgreement,
221    requested_imprint: Option<RequestAgreement>,
222    requested_fields: Option<RequestFields>,
223    timeliness: Timeliness,
224    accuracy_error: Option<super::DeviationError>,
225    unknown: OwnedVec<OwnedOid>,
226    evidence: Option<Evidence>,
227}
228
229impl Information {
230    fn holds(&self) -> bool {
231        self.accuracy_error.is_none()
232            && matches!(self.authority.usage, TsaUsage::TimeStampingOnly)
233            && matches!(
234                self.authority.identity,
235                TsaIdentity::Absent | TsaIdentity::Matches
236            )
237            && self.imprint == ImprintAgreement::Matches
238            && self
239                .requested_imprint
240                .is_none_or(|answer| answer == RequestAgreement::Matches)
241            && self
242                .requested_fields
243                .is_none_or(|fields| fields.nonce != Some(false) && fields.policy != Some(false))
244            && self.unknown.is_empty()
245            && matches!(
246                self.timeliness,
247                Timeliness::NotRequested | Timeliness::LocalWindow | Timeliness::RequestNonce
248            )
249    }
250}
251
252struct Inspection<'a, 'r, P, S, R> {
253    provider: &'a P,
254    context: &'r CheckContext<'a, 'r>,
255    covered: &'r mut S,
256    recognizes: &'r mut R,
257    common_holds: bool,
258}
259
260impl<P, S: Source, R> Inspection<'_, '_, P, S, R> {
261    fn inspect<'scope, PE, RE>(
262        &mut self,
263        info: &super::TokenInfo<'scope>,
264        inside: &Reader<'scope>,
265        source: &SourceMap,
266    ) -> Result<Information, InfoCheckError<S::Error, PE, RE>>
267    where
268        P: for<'input> SignatureProvider<'input, Error = PE>,
269        R: FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
270    {
271        let authority = info
272            .authority(
273                self.context.certificate,
274                inside,
275                self.context.certificate_parent,
276            )
277            .map_err(InfoCheckError::Authority)?;
278        let requested_fields = self
279            .context
280            .requested
281            .as_ref()
282            .map(|request| info.matches_request_fields(request.request, inside, request.parent))
283            .transpose()
284            .map_err(InfoCheckError::RequestField)?;
285        let unknown = unrecognized(info, inside, self.recognizes)?;
286        let (imprint, requested_imprint) = match &self.context.requested {
287            Some(request) => {
288                let (imprint, agreement) = info
289                    .imprint()
290                    .check_requested_content(
291                        request.request.imprint(),
292                        self.provider,
293                        (inside, request.parent),
294                        self.covered,
295                    )
296                    .map_err(|error| match error {
297                        super::RequestedContentError::Request(source) => {
298                            InfoCheckError::RequestImprint(source)
299                        }
300                        super::RequestedContentError::Content(source) => {
301                            InfoCheckError::Imprint(source)
302                        }
303                    })?;
304                (imprint, Some(agreement))
305            }
306            None => (
307                info.imprint()
308                    .check_content(self.provider, inside, self.covered)
309                    .map_err(InfoCheckError::Imprint)?,
310                None,
311            ),
312        };
313        let (accuracy, accuracy_error) = match info
314            .accuracy()
315            .map(super::AccuracyReading::deviation)
316            .transpose()
317        {
318            Ok(accuracy) => (accuracy, None),
319            Err(error) => (None, Some(error)),
320        };
321        let timeliness = super::timeliness(
322            info.gen_time(),
323            accuracy,
324            self.context.utc,
325            self.context.reference,
326            self.context.requested.is_some(),
327            requested_fields.and_then(|fields| fields.nonce),
328            self.context.window,
329        );
330        let mut information = Information {
331            authority,
332            imprint,
333            requested_imprint,
334            requested_fields,
335            timeliness,
336            accuracy_error,
337            unknown,
338            evidence: None,
339        };
340        if self.common_holds && !info.has_faults() && information.holds() {
341            information.evidence = Some(Evidence {
342                facts: info
343                    .retain_facts(inside)
344                    .map_err(InfoCheckError::Processing)?,
345                source: source
346                    .try_copy_in(&inside.execution())
347                    .map_err(InfoCheckError::Provenance)?,
348            });
349        }
350        Ok(information)
351    }
352}
353
354fn unrecognized<SE, PE, RE>(
355    info: &super::TokenInfo<'_>,
356    inside: &Reader<'_>,
357    recognizes: &mut impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
358) -> Result<OwnedVec<OwnedOid>, InfoCheckError<SE, PE, RE>> {
359    let scoped = inside.execution();
360    let mut unknown = scoped.vector();
361    if let Some(extensions) = info.extensions() {
362        for kind in extensions
363            .unrecognized_critical(inside, recognizes)
364            .map_err(InfoCheckError::Recognition)?
365        {
366            let owned = OwnedOid::from_oid_in(kind, &scoped).map_err(|reason| {
367                InfoCheckError::Processing(Error::resource(
368                    reason,
369                    "tstInfo.extensions",
370                    extensions.span().start,
371                ))
372            })?;
373            unknown.try_push(owned).map_err(|reason| {
374                InfoCheckError::Processing(Error::resource(
375                    reason,
376                    "tstInfo.extensions",
377                    extensions.span().start,
378                ))
379            })?;
380        }
381    }
382    Ok(unknown.into_owned())
383}
384
385fn common_holds(
386    signature: &TokenSignature<'_>,
387    bindings: ess::Bindings,
388    identifier: &directory::CertificateAgreement,
389    carried: Option<bool>,
390) -> bool {
391    matches!(signature, TokenSignature::Checked(checked)
392        if checked.signature() == Some(SignatureAnswer::Valid)
393        && matches!(checked.content(), verify::ContentAgreement::Matches | verify::ContentAgreement::Direct))
394        && (bindings.v1 == Some(ess::Binding::Matches)
395            || bindings.v2 == Some(ess::Binding::Matches))
396        && bindings
397            .v1
398            .is_none_or(|binding| binding == ess::Binding::Matches)
399        && bindings
400            .v2
401            .is_none_or(|binding| binding == ess::Binding::Matches)
402        && matches!(identifier, directory::CertificateAgreement::Matches)
403        && carried != Some(false)
404}
405
406impl<'doc> Token<'doc> {
407    /// Checks original CMS, ESS, TSA, content, request and time together.
408    ///
409    /// Caller path/status/policy and signed ESS restriction enforcement remain
410    /// external. Semantic negatives remain facts, never evidence. Processing,
411    /// provider and source refusals abort. Provider errors are source-independent
412    /// so a temporary joined information buffer cannot escape in their type.
413    /// Optional rejection of unlisted digest algorithms (RFC5652 ยง5.3) belongs
414    /// to caller policy. Evidence does not assert that every sender SHOULD or
415    /// application-profile requirement was satisfied.
416    #[expect(
417        clippy::result_large_err,
418        reason = "preserve original cause and accounted source context without allocating on refusal"
419    )]
420    pub fn check<'a, P, S: Source, PE, RE>(
421        &'a self,
422        provider: &'a P,
423        parent: &Reader<'a>,
424        context: &CheckContext<'a, '_>,
425        covered: &mut S,
426        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
427    ) -> Result<CheckedTimestamp<'a>, CheckError<S::Error, PE, RE>>
428    where
429        'doc: 'a,
430        P: for<'scope> SignatureProvider<'scope, Error = PE>,
431    {
432        self.check_in(provider, (parent, None), context, covered, recognizes)
433    }
434
435    /// Checks a token decoded in another joined source using the same core.
436    /// The map corresponds to its producing envelope reader. Child `TSTInfo`
437    /// maps are composed to physical fragments; outer CMS errors separately
438    /// retain the envelope map instead of borrowing a temporary joined buffer.
439    #[expect(
440        clippy::result_large_err,
441        reason = "preserve both source frames and original opaque refusal without a fallible error box"
442    )]
443    pub fn check_mapped<'a, P, S: Source, PE, RE>(
444        &'a self,
445        provider: &'a P,
446        source: (&Reader<'a>, &SourceMap),
447        context: &CheckContext<'a, '_>,
448        covered: &mut S,
449        recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
450    ) -> Result<CheckedTimestamp<'a>, MappedCheckError<S::Error, PE, RE>>
451    where
452        'doc: 'a,
453        P: for<'scope> SignatureProvider<'scope, Error = PE>,
454    {
455        let (parent, source) = source;
456        preflight_sessions(self, parent, context)
457            .map_err(|error| MappedCheckError::Preflight(error.into()))?;
458        let envelope = source
459            .try_copy_in(&parent.execution())
460            .map_err(MappedCheckError::Provenance)?;
461        self.check_in(
462            provider,
463            (parent, Some(source)),
464            context,
465            covered,
466            recognizes,
467        )
468        .map_err(|source| MappedCheckError::Execution { envelope, source })
469    }
470
471    #[expect(
472        clippy::result_large_err,
473        reason = "retain original cause and accounted source context on refusal"
474    )]
475    fn check_in<'a, P, S: Source, PE, RE>(
476        &'a self,
477        provider: &'a P,
478        origin: (&Reader<'a>, Option<&SourceMap>),
479        context: &CheckContext<'a, '_>,
480        covered: &mut S,
481        mut recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
482    ) -> Result<CheckedTimestamp<'a>, CheckError<S::Error, PE, RE>>
483    where
484        'doc: 'a,
485        P: for<'scope> SignatureProvider<'scope, Error = PE>,
486    {
487        let (parent, source) = origin;
488        preflight(self, parent, context)?;
489        let certificate = context.certificate;
490        let identifier =
491            directory::matches_certificate_in(self.signer().sid(), certificate, parent)
492                .map_err(CheckError::Identifier)?;
493        let ess =
494            ess::SigningAttributes::read(self.signer(), parent).map_err(CheckError::Processing)?;
495        let carried = context
496            .requested
497            .as_ref()
498            .map(|requested| {
499                self.matches_cert_req(
500                    requested.request,
501                    Some(certificate),
502                    parent,
503                    requested.parent,
504                )
505            })
506            .transpose()
507            .map_err(CheckError::RequestField)?;
508        if !self.faults().is_empty()
509            || !self.message().faults().is_empty()
510            || !self.signer().faults().is_empty()
511            || !ess.faults().is_empty()
512            || ess.v1().is_some_and(|reading| !reading.faults().is_empty())
513            || ess.v2().is_some_and(|reading| !reading.faults().is_empty())
514        {
515            return Err(CheckError::Nonconforming {
516                span: self.message().span(),
517            });
518        }
519        let inspect = |info: &super::TokenInfo<'_>, inside: &Reader<'_>, source: &SourceMap| {
520            if info.has_faults() {
521                return Err(InfoCheckError::Nonconforming { span: info.span() });
522            }
523            let bindings = ess
524                .bind_certificate(provider, certificate, parent)
525                .map_err(InfoCheckError::Ess)?;
526            let signature = self
527                .check_signature(provider, context.key, parent)
528                .map_err(InfoCheckError::Signature)?;
529            let common_holds = common_holds(&signature, bindings, &identifier, carried);
530            let mut inspection = Inspection {
531                provider,
532                context,
533                covered,
534                recognizes: &mut recognizes,
535                common_holds,
536            };
537            let information = inspection.inspect(info, inside, source)?;
538            Ok((signature, bindings, information))
539        };
540        let (signature, bindings, information) = match source {
541            Some(source) => self.with_info_mapped(parent, source, inspect),
542            None => self.with_info(parent, inspect),
543        }
544        .map_err(CheckError::Information)?;
545        Ok(CheckedTimestamp {
546            signature,
547            identifier,
548            ess,
549            bindings,
550            authority: information.authority,
551            imprint: information.imprint,
552            requested_imprint: information.requested_imprint,
553            requested_fields: information.requested_fields,
554            carried,
555            timeliness: information.timeliness,
556            accuracy_error: information.accuracy_error,
557            unknown_critical: information.unknown,
558            evidence: information.evidence,
559        })
560    }
561}
562
563enum PreflightError {
564    Processing(Error),
565    Requested(super::RequestFieldError),
566    KeyMismatch(structured_asn1::Span),
567}
568
569impl<SE, PE, RE> From<PreflightError> for CheckError<SE, PE, RE> {
570    fn from(error: PreflightError) -> Self {
571        match error {
572            PreflightError::Processing(error) => Self::Processing(error),
573            PreflightError::Requested(error) => Self::RequestField(error),
574            PreflightError::KeyMismatch(certificate) => Self::KeyMismatch { certificate },
575        }
576    }
577}
578
579fn preflight(
580    token: &Token<'_>,
581    parent: &Reader<'_>,
582    context: &CheckContext<'_, '_>,
583) -> Result<(), PreflightError> {
584    preflight_sessions(token, parent, context)?;
585    let certificate = context.certificate;
586    let written = certificate.tbs().public_key().as_bytes();
587    let key = context.key.key().as_bytes();
588    let length = u64::try_from(written.len().max(key.len()))
589        .ok()
590        .and_then(|length| length.checked_add(1))
591        .ok_or_else(|| {
592            PreflightError::Processing(Error::schema(
593                ErrorCode::LengthOverflow,
594                "certificate.subjectPublicKeyInfo",
595                certificate.span().start,
596            ))
597        })?;
598    parent.execution().charge_work(length).map_err(|reason| {
599        PreflightError::Processing(Error::resource(
600            reason,
601            "certificate.subjectPublicKeyInfo",
602            certificate.span().start,
603        ))
604    })?;
605    if written != key {
606        return Err(PreflightError::KeyMismatch(certificate.span()));
607    }
608    Ok(())
609}
610
611fn preflight_sessions(
612    token: &Token<'_>,
613    parent: &Reader<'_>,
614    context: &CheckContext<'_, '_>,
615) -> Result<(), PreflightError> {
616    token
617        .signer()
618        .require_session(parent, "signerInfo")
619        .map_err(PreflightError::Processing)?;
620    let certificate = context.certificate;
621    if !certificate
622        .tbs()
623        .public_key()
624        .algorithm()
625        .same_session(context.certificate_parent)
626    {
627        return Err(PreflightError::Processing(Error::pkix(
628            structured_pkix::Error::new(
629                structured_pkix::ErrorCode::ForeignReader,
630                "certificate",
631                certificate.span().start,
632            ),
633        )));
634    }
635    if let Some(requested) = &context.requested
636        && !requested.request.same_session(requested.parent)
637    {
638        return Err(PreflightError::Requested(super::RequestFieldError {
639            operand: super::ImprintOperand::Requested,
640            source: Error::pkix(structured_pkix::Error::new(
641                structured_pkix::ErrorCode::ForeignReader,
642                "timeStampReq",
643                requested.request.span().start,
644            )),
645        }));
646    }
647    Ok(())
648}