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}
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#[derive(Debug)]
52pub enum VisitError<E> {
53 Processing(Error),
55 Unreadable {
57 property: Node,
59 },
60 Callback(E),
62}
63
64pub 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
107pub 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#[derive(Debug)]
155pub enum Signature {
156 Checked {
158 content: ContentAgreement,
160 signature: Option<SignatureAnswer>,
162 },
163 Unsupported(structured_cms::prepare::Part),
165 Malformed(structured_cms::Error),
167}
168
169pub struct Checked<'model, 'document, 'path> {
173 pub signature: Signature,
175 pub identifier: structured_cms::directory::CertificateAgreement,
177 pub bindings: structured_cms::ess::Bindings,
179 pub authority: tsp::AuthorityFacts,
181 pub imprint: tsp::ImprintAgreement,
183 pub accuracy: Option<tsp::DeviationError>,
185 pub timeliness: tsp::Timeliness,
187 pub unknown_critical: OwnedVec<OwnedOid>,
189 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 #[must_use]
197 pub fn actor(&self) -> Option<&CheckedActor<'p, 'd, Node, DocumentId<'m, 'd>, (), Cades>> {
198 self.actor.as_ref()
199 }
200 #[must_use]
202 pub fn into_actor(self) -> Option<CheckedActor<'p, 'd, Node, DocumentId<'m, 'd>, (), Cades>> {
203 self.actor
204 }
205}
206
207#[derive(Debug)]
209pub enum InputError<E> {
210 Detached(E),
212 Carried(structured_cms::octets::SourceError),
214 Resource(structured_asn1::memory::MemoryError),
216 Receiver(Refused),
218}
219
220#[derive(Debug)]
222pub enum CheckError<SE, PE, RE, FE = core::convert::Infallible> {
223 ForbiddenDigest {
225 at: u64,
227 },
228 Selection(Error),
231 Material {
234 property: Node,
236 },
237 Check(ProfileCheckError<SE, PE, RE, ProfileError<PE, FE>>),
239}
240
241#[derive(Debug)]
243pub enum ProfileError<PE, E> {
244 Cades(structured_cms::Error),
246 Restriction(BindError<PE>),
248 Md5 {
250 at: u64,
252 },
253 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 #[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 #[must_use]
313 pub fn token(&self) -> &Token<'d> {
314 self.token
315 }
316 #[must_use]
318 pub fn reader(&self) -> &Reader<'d> {
319 self.reader
320 }
321
322 #[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 #[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 #[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 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 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 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}