1use 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
29pub struct CheckTime<'a, 'document> {
34 pub utc: &'a LeapTable<'document>,
36 pub reference: Option<structured_pkix::temporal::Instant>,
38 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#[derive(Debug)]
54pub enum VisitError<E> {
55 Processing(Error),
57 Unreadable {
59 property: Node,
61 },
62 Callback(E),
64}
65
66pub 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
109pub 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#[derive(Debug)]
157pub enum Signature {
158 Checked {
160 content: ContentAgreement,
162 signature: Option<SignatureAnswer>,
164 },
165 Unsupported(structured_cms::prepare::Part),
167 Malformed(structured_cms::Error),
169}
170
171pub struct Checked<'model, 'document, 'path> {
175 pub signature: Signature,
177 pub identifier: structured_cms::directory::CertificateAgreement,
179 pub bindings: structured_cms::ess::Bindings,
181 pub authority: tsp::AuthorityFacts,
183 pub imprint: tsp::ImprintAgreement,
185 pub accuracy: Option<tsp::DeviationError>,
187 pub timeliness: tsp::Timeliness,
189 pub unknown_critical: OwnedVec<OwnedOid>,
191 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 #[must_use]
199 pub fn actor(&self) -> Option<&CheckedActor<'p, 'd, Node, DocumentId<'m, 'd>, (), Cades>> {
200 self.actor.as_ref()
201 }
202 #[must_use]
204 pub fn into_actor(self) -> Option<CheckedActor<'p, 'd, Node, DocumentId<'m, 'd>, (), Cades>> {
205 self.actor
206 }
207}
208
209#[derive(Debug)]
211pub enum InputError<E> {
212 Detached(E),
214 Carried(structured_cms::octets::SourceError),
216 Resource(structured_asn1::memory::MemoryError),
218 Receiver(Refused),
220}
221
222#[derive(Debug)]
224pub enum CheckError<SE, PE, RE, FE = core::convert::Infallible> {
225 ForbiddenDigest {
227 at: u64,
229 },
230 Selection(Error),
233 Material {
236 property: Node,
238 },
239 Check(ProfileCheckError<SE, PE, RE, ProfileError<PE, FE>>),
241}
242
243#[derive(Debug)]
245pub enum ProfileError<PE, E> {
246 Cades(structured_cms::Error),
248 Restriction(BindError<PE>),
250 Md5 {
252 at: u64,
254 },
255 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 #[must_use]
309 pub fn properties(&self) -> Option<&crate::properties::Properties<'d>> {
310 self.prepared.parsed.token(self.ordinal)?.properties
311 }
312 #[must_use]
314 pub fn token(&self) -> &Token<'d> {
315 self.token
316 }
317 #[must_use]
319 pub fn reader(&self) -> &Reader<'d> {
320 self.reader
321 }
322
323 #[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 #[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 #[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 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 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 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}