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