Skip to main content

structured_cades/
timestamp.rs

1//! Source-selected timestamp checks. The selected input is derived here;
2//! callers select providers, original certificates and independently checked
3//! paths, never replacement signature bytes or a writable successful receipt.
4
5use crate::basic::{BindError, PathRestriction};
6use crate::prepare::{Preparation, Timestamp};
7use crate::source::{DocumentId, Node};
8use crate::{Cades, Error, ISSUER};
9use structured_asn1::{
10    Reader,
11    memory::{Execution, OwnedVec, Work},
12    value::OwnedOid,
13};
14use structured_cms::{
15    octets::Piece,
16    provider::{SignatureAnswer, SignatureProvider},
17    stream::{Receiver, Refused, Source},
18    timestamp::{
19        self as tsp, CheckContext, ProfileCheckError, ProfileFailure, ProfileSource, Token,
20    },
21    verify::ContentAgreement,
22};
23use structured_ltv::{
24    pki::CheckedPath,
25    timestamp::{CheckedActor, Purpose, Verified},
26};
27use structured_pkix::temporal::LeapTable;
28
29/// Explicit UTC facts and optional caller-established comparison instant.
30/// No clock or tolerance is discovered implicitly. Without a usable reference
31/// the common TSP result keeps Unverified timeliness and cannot issue evidence;
32/// archived signature/math facts remain available independently in the report.
33pub struct CheckTime<'a, 'document> {
34    /// Qualified correction table covering every temporal comparison.
35    pub utc: &'a LeapTable<'document>,
36    /// Actual caller-supplied reference, never derived from the token itself.
37    pub reference: Option<structured_pkix::temporal::Instant>,
38}
39
40mod archive;
41pub mod historical;
42pub mod references;
43mod requirements;
44pub use archive::{
45    ArchiveCheckError, ArchiveChecked, ArchiveError, ArchiveStreamError, ArchiveView, NoDetached,
46    ScopeError as ArchiveScopeError,
47};
48
49/// Source/refusal or opaque caller cause. Unreadable occurrences stay in the
50/// preparation's full census and cannot become successful missing entries.
51#[derive(Debug)]
52pub enum VisitError<E> {
53    /// Original source/ASN.1/resource failure before the visitor.
54    Processing(Error),
55    /// Original occurrence has no readable encapsulated value.
56    Unreadable {
57        /// Actual original property.
58        property: Node,
59    },
60    /// Caller failure, unchanged and without a successful partial return.
61    Callback(E),
62}
63
64/// Borrows exactly one original prepared token under current execution.
65/// No envelope is reparsed; retained evidence owns its own storage.
66pub fn with_token<'m, 'd, R, E>(
67    prepared: &Preparation<'m, 'd>,
68    ordinal: usize,
69    operation: &Reader<'d>,
70    visitor: impl for<'scope> FnOnce(Context<'scope, 'm, 'd>) -> Result<R, E>,
71) -> Result<R, VisitError<E>> {
72    prepared
73        .document()
74        .message()
75        .require_session(operation, "cades.timestamp")
76        .map_err(|cause| VisitError::Processing(Error::Cms(cause)))?;
77    operation
78        .charge(1)
79        .map_err(|cause| VisitError::Processing(Error::encoding(cause, "cades.timestamp")))?;
80    let entry = prepared.timestamps().get(ordinal).ok_or_else(|| {
81        VisitError::Processing(Error::Foreign {
82            at: prepared.signer().span().start,
83        })
84    })?;
85    let source = entry.source().ok_or(VisitError::Unreadable {
86        property: entry.property(),
87    })?;
88    let reader = source
89        .reader_in(operation)
90        .map_err(VisitError::Processing)?;
91    let Some(Some(index)) = prepared.parsed.ordinals.get(ordinal) else {
92        return Err(VisitError::Unreadable {
93            property: entry.property(),
94        });
95    };
96    let token = &prepared.parsed.models[*index].token;
97    visitor(Context {
98        prepared,
99        ordinal,
100        entry,
101        token,
102        reader: &reader,
103    })
104    .map_err(VisitError::Callback)
105}
106
107/// Short original token view. Any references into its movable parsed model are
108/// restricted to this callback, independently of original document lifetimes.
109pub struct Context<'scope, 'model, 'document> {
110    prepared: &'scope Preparation<'model, 'document>,
111    ordinal: usize,
112    entry: &'scope Timestamp<'document>,
113    token: &'scope Token<'document>,
114    reader: &'scope Reader<'document>,
115}
116
117pub(crate) fn with_existing<'m, 'd, R, E>(
118    prepared: &Preparation<'m, 'd>,
119    ordinal: usize,
120    token: &Token<'d>,
121    operation: &Reader<'d>,
122    visitor: impl for<'scope> FnOnce(Context<'scope, 'm, 'd>) -> Result<R, E>,
123) -> Result<R, VisitError<E>> {
124    prepared
125        .document()
126        .message()
127        .require_session(operation, "cades.timestamp")
128        .map_err(|cause| VisitError::Processing(Error::Cms(cause)))?;
129    operation
130        .charge(1)
131        .map_err(|cause| VisitError::Processing(Error::encoding(cause, "cades.timestamp")))?;
132    let entry = prepared.timestamps().get(ordinal).ok_or_else(|| {
133        VisitError::Processing(Error::Foreign {
134            at: prepared.signer().span().start,
135        })
136    })?;
137    let source = entry.source().ok_or(VisitError::Unreadable {
138        property: entry.property(),
139    })?;
140    let reader = source
141        .reader_in(operation)
142        .map_err(VisitError::Processing)?;
143    visitor(Context {
144        prepared,
145        ordinal,
146        entry,
147        token,
148        reader: &reader,
149    })
150    .map_err(VisitError::Callback)
151}
152
153/// Complete mathematical/content result, without borrowing a temporary signer.
154#[derive(Debug)]
155pub enum Signature {
156    /// Original completed CMS content and signature answers, including None.
157    Checked {
158        /// Separate payload-to-signed-attribute agreement.
159        content: ContentAgreement,
160        /// Actual selected-provider arithmetic.
161        signature: Option<SignatureAnswer>,
162    },
163    /// Original selected provider did not implement this mechanism.
164    Unsupported(structured_cms::prepare::Part),
165    /// Original algorithm/schema failure, unchanged and located.
166    Malformed(structured_cms::Error),
167}
168
169/// Independently retained completed facts, including negative outcomes. A
170/// successful actor is private and requires the common Evidence plus all
171/// signed ESS path restrictions; public diagnostics cannot manufacture one.
172pub struct Checked<'model, 'document, 'path> {
173    /// Original CMS result, independently of authority/path acceptance.
174    pub signature: Signature,
175    /// Actual selected-certificate SID/hint comparison.
176    pub identifier: structured_cms::directory::CertificateAgreement,
177    /// Both independent original ESS hash bindings.
178    pub bindings: structured_cms::ess::Bindings,
179    /// Original exclusive EKU and optional TSA-name answers.
180    pub authority: tsp::AuthorityFacts,
181    /// Complete original format-selected input agreement.
182    pub imprint: tsp::ImprintAgreement,
183    /// Original readable scalar accuracy failure, never clamped.
184    pub accuracy: Option<tsp::DeviationError>,
185    /// Original freshness/reference result, including Unverified/Uncertain.
186    pub timeliness: tsp::Timeliness,
187    /// Every original unknown critical identifier, with retained storage charge.
188    pub unknown_critical: OwnedVec<OwnedOid>,
189    /// Signed later-identifier restrictions against the actual selected path.
190    pub restrictions: PathRestriction,
191    actor: Option<CheckedActor<'path, 'document, Node, DocumentId<'model, 'document>, (), Cades>>,
192}
193
194impl<'m, 'd, 'p> Checked<'m, 'd, 'p> {
195    /// Original successfully bound actor, or explicit absence of proof.
196    #[must_use]
197    pub fn actor(&self) -> Option<&CheckedActor<'p, 'd, Node, DocumentId<'m, 'd>, (), Cades>> {
198        self.actor.as_ref()
199    }
200    /// Moves the independently retained evidence without copying/re-hashing.
201    #[must_use]
202    pub fn into_actor(self) -> Option<CheckedActor<'p, 'd, Node, DocumentId<'m, 'd>, (), Cades>> {
203        self.actor
204    }
205}
206
207/// A format-selected source pass, preserving its original acquisition cause.
208#[derive(Debug)]
209pub enum InputError<E> {
210    /// Original explicit detached source failed.
211    Detached(E),
212    /// Original carried source/segment traversal failed.
213    Carried(structured_cms::octets::SourceError),
214    /// Original metadata admission failed before accessing a fragment.
215    Resource(structured_asn1::memory::MemoryError),
216    /// Common stream holds the original refused receiver cause.
217    Receiver(Refused),
218}
219
220/// The original complete timestamp/common/profile failure.
221#[derive(Debug)]
222pub enum CheckError<SE, PE, RE, FE = core::convert::Infallible> {
223    /// Actual MD5 use violates the format independently of provider support.
224    ForbiddenDigest {
225        /// Original algorithm enclosure.
226        at: u64,
227    },
228    /// Requested entry does not cover this kind of input, or a path has no
229    /// actual target. Refused before provider/source side effects.
230    Selection(Error),
231    /// Known validation-values syntax is malformed. The original unchanged
232    /// causes remain available from this context's retained properties.
233    Material {
234        /// Actual containing timestamp attribute, in the original source.
235        property: Node,
236    },
237    /// Original input/callback/processing failure and its source maps.
238    Check(ProfileCheckError<SE, PE, RE, ProfileError<PE, FE>>),
239}
240
241/// Original source-specific baseline preflight failure.
242#[derive(Debug)]
243pub enum ProfileError<PE, E> {
244    /// Located profile/placement violation on the actual archive TSA signer.
245    Cades(structured_cms::Error),
246    /// Actual signed ESS/path restriction execution refused.
247    Restriction(BindError<PE>),
248    /// The format prohibits actual MD5 use (§6.2.1), independently of support.
249    Md5 {
250        /// Original declared algorithm position.
251        at: u64,
252    },
253    /// The actual archive/content profile's independent original cause.
254    Input(E),
255}
256
257struct Pieces<'s, 'd>(&'s [Piece<'d>]);
258impl Source for Pieces<'_, '_> {
259    type Error = InputError<core::convert::Infallible>;
260    fn stream<W: Work>(
261        &mut self,
262        receiver: &mut impl Receiver,
263        work: &W,
264    ) -> Result<(), Self::Error> {
265        for piece in self.0 {
266            work.charge_work(1).map_err(InputError::Resource)?;
267            receiver
268                .write(piece.as_bytes())
269                .map_err(InputError::Receiver)?;
270        }
271        Ok(())
272    }
273}
274
275enum ContentInput<'s, 'd, S> {
276    Carried(&'s structured_cms::Octets<'d>),
277    Counter(&'s [Piece<'d>]),
278    Detached(&'s mut S),
279}
280impl<S: Source> Source for ContentInput<'_, '_, S> {
281    type Error = InputError<S::Error>;
282    fn stream<W: Work>(
283        &mut self,
284        receiver: &mut impl Receiver,
285        work: &W,
286    ) -> Result<(), Self::Error> {
287        match self {
288            Self::Carried(source) => source.stream(receiver, work).map_err(InputError::Carried),
289            Self::Detached(source) => source.stream(receiver, work).map_err(InputError::Detached),
290            Self::Counter(pieces) => {
291                for piece in *pieces {
292                    work.charge_work(1).map_err(InputError::Resource)?;
293                    receiver
294                        .write(piece.as_bytes())
295                        .map_err(InputError::Receiver)?;
296                }
297                Ok(())
298            }
299        }
300    }
301}
302
303impl<'m, 'd> Context<'_, 'm, 'd> {
304    /// First-read original legacy validation models and unchanged diagnostics.
305    /// This does not reparse the timestamp or a material field.
306    #[must_use]
307    pub fn properties(&self) -> Option<&crate::properties::Properties<'d>> {
308        let index = self.prepared.parsed.ordinals.get(self.ordinal)?.as_ref()?;
309        self.prepared.parsed.models[*index].properties.as_ref()
310    }
311    /// Original envelope for pool selection/inspection without another parse.
312    #[must_use]
313    pub fn token(&self) -> &Token<'d> {
314        self.token
315    }
316    /// Current original envelope reader for original CMS queries.
317    #[must_use]
318    pub fn reader(&self) -> &Reader<'d> {
319        self.reader
320    }
321
322    /// Checks a content timestamp against the actual original carried input.
323    /// At a counter this is the parent's signature octets; at the root this is
324    /// eContent. An absent root eContent requires the separate explicit detached
325    /// operation, and never silently selects the signature or empty bytes.
326    #[expect(
327        clippy::result_large_err,
328        reason = "carried-content failures retain original envelope/information maps without allocating while reporting resource refusal"
329    )]
330    pub fn check_content<'a, 'p, P, PE, RE>(
331        &'a self,
332        provider: &'a P,
333        path: &'p CheckedPath<'d>,
334        certificate_parent: &Reader<'d>,
335        time: &CheckTime<'_, 'd>,
336        recognizes: impl FnMut(&tsp::TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
337    ) -> Result<Checked<'m, 'd, 'p>, CheckError<InputError<core::convert::Infallible>, PE, RE>>
338    where
339        P: for<'scope> SignatureProvider<'scope, Error = PE>,
340    {
341        if self.entry.purpose() != Purpose::Content {
342            return Err(CheckError::Selection(Error::Foreign {
343                at: self.entry.property().span().start,
344            }));
345        }
346        let original = if let Some(pieces) = self
347            .prepared
348            .counter_content_input(self.ordinal, self.reader)
349            .map_err(CheckError::Selection)?
350        {
351            ContentInput::<NoDetached>::Counter(pieces)
352        } else {
353            let source = self
354                .prepared
355                .document()
356                .message()
357                .content()
358                .content()
359                .ok_or_else(|| {
360                    CheckError::Selection(Error::Foreign {
361                        at: self.entry.property().span().start,
362                    })
363                })?;
364            ContentInput::Carried(source)
365        };
366        self.check(
367            provider,
368            path,
369            certificate_parent,
370            time,
371            &mut { original },
372            recognizes,
373        )
374    }
375
376    /// Checks an explicit detached root content input. A counter, signature
377    /// timestamp or original carried payload refuses before source/provider use.
378    #[expect(
379        clippy::result_large_err,
380        reason = "detached acquisition cause and both original source maps must survive the failed pass without a new allocation"
381    )]
382    pub fn check_detached_content<'a, 'p, P, S: Source, PE, RE>(
383        &'a self,
384        provider: &'a P,
385        path: &'p CheckedPath<'d>,
386        certificate_parent: &Reader<'d>,
387        time: &CheckTime<'_, 'd>,
388        detached: &mut S,
389        recognizes: impl FnMut(&tsp::TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
390    ) -> Result<Checked<'m, 'd, 'p>, CheckError<InputError<S::Error>, PE, RE>>
391    where
392        P: for<'scope> SignatureProvider<'scope, Error = PE>,
393    {
394        if self.entry.purpose() != Purpose::Content
395            || self.entry.owner() != Node::of(self.prepared.signer().span())
396            || !self.prepared.document().message().content().is_detached()
397        {
398            return Err(CheckError::Selection(Error::Foreign {
399                at: self.entry.property().span().start,
400            }));
401        }
402        self.check(
403            provider,
404            path,
405            certificate_parent,
406            time,
407            &mut ContentInput::Detached(detached),
408            recognizes,
409        )
410    }
411
412    /// Check a root OR nested counter's signature timestamp over that actual
413    /// owner's signature CONTENT octets. Whole `SignerInfo` encodings, parent
414    /// signatures and caller replacement bytes cannot enter this operation.
415    /// EN319122-1 §5.3 and RFC5652 §11.4 define these different coverages.
416    /// <https://www.rfc-editor.org/rfc/rfc5652#section-11.4>
417    #[expect(
418        clippy::result_large_err,
419        reason = "original signature-input and nested token refusals carry their source maps even on memory exhaustion"
420    )]
421    pub fn check_signature<'a, 'p, P, PE, RE>(
422        &'a self,
423        provider: &'a P,
424        path: &'p CheckedPath<'d>,
425        certificate_parent: &Reader<'d>,
426        time: &CheckTime<'_, 'd>,
427        recognizes: impl FnMut(&tsp::TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
428    ) -> Result<Checked<'m, 'd, 'p>, CheckError<InputError<core::convert::Infallible>, PE, RE>>
429    where
430        P: for<'scope> SignatureProvider<'scope, Error = PE>,
431    {
432        if self.entry.purpose() != Purpose::Signature {
433            return Err(CheckError::Selection(Error::Foreign {
434                at: self.entry.property().span().start,
435            }));
436        }
437        let pieces = self
438            .prepared
439            .signature_input(self.ordinal, self.reader)
440            .map_err(CheckError::Selection)?
441            .ok_or_else(|| {
442                CheckError::Selection(Error::Foreign {
443                    at: self.entry.property().span().start,
444                })
445            })?;
446        self.check(
447            provider,
448            path,
449            certificate_parent,
450            time,
451            &mut Pieces(pieces),
452            recognizes,
453        )
454    }
455
456    #[expect(
457        clippy::result_large_err,
458        reason = "dispatch preserves the complete mapped refusal from the one common profile check"
459    )]
460    fn check<'a, 'p, P, S: Source, PE, RE>(
461        &'a self,
462        provider: &'a P,
463        path: &'p CheckedPath<'d>,
464        certificate_parent: &Reader<'d>,
465        time: &CheckTime<'_, 'd>,
466        covered: &mut S,
467        recognizes: impl FnMut(&tsp::TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
468    ) -> Result<Checked<'m, 'd, 'p>, CheckError<S::Error, PE, RE>>
469    where
470        P: for<'scope> SignatureProvider<'scope, Error = PE>,
471    {
472        self.check_profile(
473            provider,
474            path,
475            certificate_parent,
476            time,
477            covered,
478            (recognizes, |_| {
479                Ok::<(), ProfileFailure<core::convert::Infallible>>(())
480            }),
481        )
482    }
483
484    #[expect(
485        clippy::result_large_err,
486        reason = "the shared profile refusal owns envelope and information provenance; boxing during refusal would require additional admitted storage"
487    )]
488    fn check_profile<'a, 'p, P, S: Source, PE, RE, FE>(
489        &'a self,
490        provider: &'a P,
491        path: &'p CheckedPath<'d>,
492        certificate_parent: &Reader<'d>,
493        time: &CheckTime<'_, 'd>,
494        covered: &mut S,
495        callbacks: (
496            impl FnMut(&tsp::TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
497            impl for<'view, 'scope> FnOnce(
498                tsp::ProfileInput<'view, 'd, 'a, 'scope>,
499            ) -> Result<(), ProfileFailure<FE>>,
500        ),
501    ) -> Result<Checked<'m, 'd, 'p>, CheckError<S::Error, PE, RE, FE>>
502    where
503        P: for<'scope> SignatureProvider<'scope, Error = PE>,
504    {
505        self.check_computed_profile(
506            provider,
507            path,
508            certificate_parent,
509            time,
510            &mut tsp::RawInput::new(covered),
511            callbacks,
512        )
513    }
514
515    #[expect(
516        clippy::result_large_err,
517        reason = "mapped original causes remain unboxed on an exhausted account"
518    )]
519    fn check_computed_profile<'a, 'p, P, S, PE, RE, FE>(
520        &'a self,
521        provider: &'a P,
522        path: &'p CheckedPath<'d>,
523        certificate_parent: &Reader<'d>,
524        time: &CheckTime<'_, 'd>,
525        covered: &mut S,
526        callbacks: (
527            impl FnMut(&tsp::TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
528            impl for<'view, 'scope> FnOnce(
529                tsp::ProfileInput<'view, 'd, 'a, 'scope>,
530            ) -> Result<(), ProfileFailure<FE>>,
531        ),
532    ) -> Result<Checked<'m, 'd, 'p>, CheckError<S::Error, PE, RE, FE>>
533    where
534        P: for<'scope> SignatureProvider<'scope, Error = PE>,
535        S: tsp::ComputedInput<P, PE>,
536    {
537        let (recognizes, profile) = callbacks;
538        if self
539            .properties()
540            .is_some_and(|properties| !properties.faults().is_empty())
541        {
542            return Err(CheckError::Material {
543                property: self.entry.property(),
544            });
545        }
546        let digest = self.token.signer().digest_algorithm();
547        self.reader
548            .charge(1 + digest.algorithm().as_bytes().len().min(8) as u64)
549            .map_err(|cause| {
550                CheckError::Selection(Error::encoding(cause, "cades.timestamp.digestAlgorithm"))
551            })?;
552        // EN319122-1 §6.2.1: actual MD5 use is forbidden independently of
553        // provider capabilities. This gate precedes ESS/digest/source work.
554        // https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf
555        if digest.algorithm()
556            == structured_asn1::Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 2, 5])
557        {
558            return Err(CheckError::ForbiddenDigest {
559                at: digest.offset(),
560            });
561        }
562        let Some(target) = path.members().last() else {
563            return Err(CheckError::Selection(Error::Foreign {
564                at: self.entry.property().span().start,
565            }));
566        };
567        let certificate = target.certificate();
568        let key = target.key();
569        let selection = CheckContext {
570            certificate,
571            certificate_parent,
572            key,
573            requested: None,
574            utc: time.utc,
575            reference: time.reference,
576            window: None,
577        };
578        let restriction = core::cell::Cell::new(PathRestriction::Unproven);
579        let checked = self
580            .token
581            .check_input_with_profile(
582                provider,
583                self.reader,
584                &selection,
585                covered,
586                recognizes,
587                |input| {
588                    if self.entry.purpose() == Purpose::Archive
589                        && self.entry.preservation_kind().is_none()
590                    {
591                        Self::archive_profile(&input)?;
592                    }
593                    restriction.set(Self::profile_rules(&input, path, provider)?);
594                    profile(input).map_err(|cause| ProfileFailure {
595                        source: cause.source,
596                        cause: ProfileError::Input(cause.cause),
597                    })
598                },
599            )
600            .map_err(CheckError::Check)?;
601        Ok(self.checked(checked, restriction.get(), certificate, key))
602    }
603    fn checked<'p>(
604        &self,
605        mut checked: tsp::CheckedTimestamp<'_>,
606        restriction: PathRestriction,
607        certificate: &'p structured_pkix::Certificate<'d>,
608        key: structured_pkix::path::WorkingKey<'d>,
609    ) -> Checked<'m, 'd, 'p> {
610        let actor = match checked.take_evidence() {
611            Some(evidence)
612                if matches!(
613                    restriction,
614                    PathRestriction::Unstated | PathRestriction::Matches
615                ) =>
616            {
617                let verified = Verified::new(
618                    ISSUER,
619                    self.prepared.document().locate(
620                        self.entry
621                            .value()
622                            .expect("with_token requires original value")
623                            .span(),
624                    ),
625                    self.entry.owner(),
626                    self.entry.property(),
627                    self.entry.purpose(),
628                    evidence,
629                );
630                Some(CheckedActor::new(ISSUER, verified, certificate, key))
631            }
632            _ => None,
633        };
634        let signature = match checked.signature {
635            tsp::TokenSignature::Checked(value) => {
636                let (_, _, content, signature) = value.into_parts();
637                Signature::Checked { content, signature }
638            }
639            tsp::TokenSignature::Unsupported(part) => Signature::Unsupported(part),
640            tsp::TokenSignature::Malformed(cause) => Signature::Malformed(cause),
641        };
642        Checked {
643            signature,
644            identifier: checked.identifier,
645            bindings: checked.bindings,
646            authority: checked.authority,
647            imprint: checked.imprint,
648            accuracy: checked.accuracy_error,
649            timeliness: checked.timeliness,
650            unknown_critical: checked.unknown_critical,
651            restrictions: restriction,
652            actor,
653        }
654    }
655    fn archive_profile<PE, FE>(
656        input: &tsp::ProfileInput<'_, 'd, '_, '_>,
657    ) -> Result<(), ProfileFailure<ProfileError<PE, FE>>> {
658        // EN319122-1 §5.5.2 permits the hash index only in
659        // unsigned attributes of this archive TSA signer.
660        // Reuse the ESS reading of this very check; a valid
661        // unsigned index cannot hide a prohibited signed copy.
662        let faults = structured_cms::cades::findings_from_reading(
663            input.token.signer(),
664            structured_cms::cades::Role::ArchiveTimestamp,
665            input.ess,
666            input.parent,
667        )
668        .map_err(|cause| ProfileFailure {
669            source: ProfileSource::Envelope,
670            cause: ProfileError::Cades(cause),
671        })?;
672        if let Some(cause) = faults.into_iter().next() {
673            return Err(ProfileFailure {
674                source: ProfileSource::Envelope,
675                cause: ProfileError::Cades(cause),
676            });
677        }
678        Ok(())
679    }
680    fn profile_rules<'a, P, PE, FE>(
681        input: &tsp::ProfileInput<'_, 'd, 'a, '_>,
682        path: &CheckedPath<'d>,
683        provider: &'a P,
684    ) -> Result<PathRestriction, ProfileFailure<ProfileError<PE, FE>>>
685    where
686        P: for<'scope> SignatureProvider<'scope, Error = PE>,
687    {
688        // Admit each metadata/OID comparison before inspecting it. A known
689        // forbidden digest is not Unsupported and no content source runs.
690        let md5 = structured_asn1::Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 2, 5]);
691        for attribute in [input.ess.v1(), input.ess.v2()].into_iter().flatten() {
692            for certificate in attribute.identifiers() {
693                input.parent.charge(1).map_err(|cause| ProfileFailure {
694                    source: ProfileSource::Envelope,
695                    cause: ProfileError::Restriction(BindError::Processing(Error::encoding(
696                        cause,
697                        "cades.timestamp.ess",
698                    ))),
699                })?;
700                if let structured_cms::provider::DigestAlgorithm::Written(algorithm) =
701                    certificate.digest_algorithm()
702                {
703                    input
704                        .parent
705                        .charge(1 + algorithm.algorithm().as_bytes().len().min(8) as u64)
706                        .map_err(|cause| ProfileFailure {
707                            source: ProfileSource::Envelope,
708                            cause: ProfileError::Restriction(BindError::Processing(
709                                Error::encoding(cause, "cades.timestamp.ess"),
710                            )),
711                        })?;
712                    if algorithm.algorithm() == md5 {
713                        return Err(ProfileFailure {
714                            source: ProfileSource::Envelope,
715                            cause: ProfileError::Md5 {
716                                at: algorithm.offset(),
717                            },
718                        });
719                    }
720                }
721            }
722        }
723        input.reader.charge(12).map_err(|cause| ProfileFailure {
724            source: ProfileSource::Information,
725            cause: ProfileError::Restriction(BindError::Processing(Error::encoding(
726                cause,
727                "cades.timestamp",
728            ))),
729        })?;
730        if let tsp::HashAlgorithmReading::Identifier(identifier) = input.info.imprint().algorithm()
731            && identifier.algorithm() == md5
732        {
733            return Err(ProfileFailure {
734                source: ProfileSource::Information,
735                cause: ProfileError::Md5 {
736                    at: identifier.offset(),
737                },
738            });
739        }
740        let result = crate::basic::restrictions(input.ess, Some(path), provider, input.parent)
741            .map_err(|cause| ProfileFailure {
742                source: ProfileSource::Envelope,
743                cause: ProfileError::Restriction(cause),
744            })?;
745        Ok(result)
746    }
747}