1use super::profile::{
4 self, MappedProfileCheckError, ProfileCheckError, ProfileFailure, ProfileInput,
5};
6use super::{
7 AuthorityFacts, ComputedInput, ImprintAgreement, RawInput, RecognitionError, Request,
8 RequestAgreement, RequestFields, RequestWindow, RetainedFacts, SignatureError, Timeliness,
9 Token, TokenExtension, TokenSignature, TsaIdentity, TsaUsage,
10};
11use crate::{
12 Error, ErrorCode,
13 content_source::{ContentError, SourceMap},
14 directory, ess,
15 provider::{SignatureAnswer, SignatureProvider},
16 stream::Source,
17 verify,
18};
19use core::convert::Infallible;
20use structured_asn1::{
21 Reader,
22 memory::{Execution, OwnedVec, Work},
23 value::OwnedOid,
24};
25use structured_pkix::{
26 Certificate,
27 path::WorkingKey,
28 temporal::{Instant, LeapTable},
29};
30
31pub struct Requested<'a, 'r> {
33 pub request: &'r Request<'a>,
35 pub parent: &'r Reader<'a>,
37}
38
39pub struct CheckContext<'a, 'r> {
41 pub certificate: &'r Certificate<'a>,
43 pub certificate_parent: &'r Reader<'a>,
45 pub key: WorkingKey<'a>,
47 pub requested: Option<Requested<'a, 'r>>,
49 pub utc: &'r LeapTable<'a>,
51 pub reference: Option<Instant>,
53 pub window: Option<RequestWindow>,
55}
56
57#[derive(Debug)]
59pub enum InfoCheckError<SE, PE, RE> {
60 Nonconforming {
62 span: structured_asn1::Span,
64 },
65 Signature(SignatureError<PE>),
67 Ess(ess::BindingError<PE>),
69 Processing(Error),
71 Provenance(Error),
74 Imprint(super::ImprintError<SE, PE>),
76 RequestImprint(super::RequestImprintError<PE>),
78 RequestField(super::RequestFieldError),
80 Authority(super::AuthorityError),
82 Recognition(RecognitionError<RE>),
84}
85
86#[derive(Debug)]
88pub enum CheckError<SE, PE, RE> {
89 Nonconforming {
91 span: structured_asn1::Span,
93 },
94 Processing(Error),
96 KeyMismatch {
98 certificate: structured_asn1::Span,
100 },
101 Identifier(directory::Error),
103 RequestField(super::RequestFieldError),
105 Information(ContentError<super::InfoError<InfoCheckError<SE, PE, RE>>>),
107}
108
109#[derive(Debug)]
112pub enum MappedCheckError<SE, PE, RE> {
113 Preflight(CheckError<SE, PE, RE>),
116 Provenance(Error),
118 Execution {
120 envelope: SourceMap,
122 source: CheckError<SE, PE, RE>,
124 },
125}
126
127#[derive(Debug)]
131pub struct Evidence {
132 facts: RetainedFacts,
133 source: SourceMap,
134}
135
136#[derive(Debug)]
138pub enum EvidenceCopyError {
139 Provenance(Error),
141 Facts {
143 source: Error,
145 context: SourceMap,
147 },
148}
149impl Evidence {
150 #[expect(
154 clippy::result_large_err,
155 reason = "retain admitted source metadata with the original copy refusal; an error box would itself require unavailable capacity"
156 )]
157 pub fn try_copy_in<W: Work>(&self, work: &W) -> Result<Self, EvidenceCopyError> {
158 let context = self
159 .source
160 .try_copy_in(work)
161 .map_err(EvidenceCopyError::Provenance)?;
162 match self.facts.try_copy_in(work) {
163 Ok(facts) => Ok(Self {
164 facts,
165 source: context,
166 }),
167 Err(source) => Err(EvidenceCopyError::Facts { source, context }),
168 }
169 }
170 #[must_use]
172 pub fn facts(&self) -> &RetainedFacts {
173 &self.facts
174 }
175 #[must_use]
177 pub fn source(&self) -> &SourceMap {
178 &self.source
179 }
180}
181
182pub struct CheckedTimestamp<'a> {
184 pub signature: TokenSignature<'a>,
186 pub identifier: directory::CertificateAgreement,
188 pub ess: ess::SigningAttributes<'a>,
190 pub bindings: ess::Bindings,
192 pub authority: AuthorityFacts,
194 pub imprint: ImprintAgreement,
196 pub requested_imprint: Option<RequestAgreement>,
198 pub requested_fields: Option<RequestFields>,
200 pub carried: Option<bool>,
202 pub timeliness: Timeliness,
204 pub accuracy_error: Option<super::DeviationError>,
206 pub unknown_critical: OwnedVec<OwnedOid>,
208 evidence: Option<Evidence>,
209}
210impl CheckedTimestamp<'_> {
211 pub fn take_evidence(&mut self) -> Option<Evidence> {
216 self.evidence.take()
217 }
218 pub fn into_evidence(self) -> Option<Evidence> {
220 self.evidence
221 }
222 #[must_use]
224 pub fn evidence(&self) -> Option<&Evidence> {
225 self.evidence.as_ref()
226 }
227}
228
229struct Information {
230 authority: AuthorityFacts,
231 imprint: ImprintAgreement,
232 requested_imprint: Option<RequestAgreement>,
233 requested_fields: Option<RequestFields>,
234 timeliness: Timeliness,
235 accuracy_error: Option<super::DeviationError>,
236 unknown: OwnedVec<OwnedOid>,
237 evidence: Option<Evidence>,
238}
239
240impl Information {
241 fn holds(&self) -> bool {
242 self.accuracy_error.is_none()
243 && matches!(self.authority.usage, TsaUsage::TimeStampingOnly)
244 && matches!(
245 self.authority.identity,
246 TsaIdentity::Absent | TsaIdentity::Matches
247 )
248 && self.imprint == ImprintAgreement::Matches
249 && self
250 .requested_imprint
251 .is_none_or(|answer| answer == RequestAgreement::Matches)
252 && self
253 .requested_fields
254 .is_none_or(|fields| fields.nonce != Some(false) && fields.policy != Some(false))
255 && self.unknown.is_empty()
256 && matches!(
257 self.timeliness,
258 Timeliness::NotRequested | Timeliness::LocalWindow | Timeliness::RequestNonce
259 )
260 }
261}
262
263struct Inspection<'a, 'r, P, S, R> {
264 provider: &'a P,
265 context: &'r CheckContext<'a, 'r>,
266 covered: &'r mut S,
267 recognizes: &'r mut R,
268 common_holds: bool,
269}
270
271impl<P, S, R> Inspection<'_, '_, P, S, R> {
272 fn inspect<'scope, PE, RE>(
273 &mut self,
274 info: &super::TokenInfo<'scope>,
275 inside: &Reader<'scope>,
276 source: &SourceMap,
277 ) -> Result<Information, InfoCheckError<S::Error, PE, RE>>
278 where
279 P: for<'input> SignatureProvider<'input, Error = PE>,
280 S: ComputedInput<P, PE>,
281 R: FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
282 {
283 let authority = info
284 .authority(
285 self.context.certificate,
286 inside,
287 self.context.certificate_parent,
288 )
289 .map_err(InfoCheckError::Authority)?;
290 let requested_fields = self
291 .context
292 .requested
293 .as_ref()
294 .map(|request| info.matches_request_fields(request.request, inside, request.parent))
295 .transpose()
296 .map_err(InfoCheckError::RequestField)?;
297 let unknown = unrecognized(info, inside, self.recognizes)?;
298 let (imprint, requested_imprint) = match &self.context.requested {
299 Some(request) => {
300 let (imprint, agreement) = info
301 .imprint()
302 .check_requested_input(
303 request.request.imprint(),
304 self.provider,
305 (inside, request.parent),
306 self.covered,
307 )
308 .map_err(|error| match error {
309 super::RequestedContentError::Request(source) => {
310 InfoCheckError::RequestImprint(source)
311 }
312 super::RequestedContentError::Content(source) => {
313 InfoCheckError::Imprint(source)
314 }
315 })?;
316 (imprint, Some(agreement))
317 }
318 None => (
319 info.imprint()
320 .check_input(self.provider, inside, self.covered)
321 .map_err(InfoCheckError::Imprint)?,
322 None,
323 ),
324 };
325 let (accuracy, accuracy_error) = match info
326 .accuracy()
327 .map(super::AccuracyReading::deviation)
328 .transpose()
329 {
330 Ok(accuracy) => (accuracy, None),
331 Err(error) => (None, Some(error)),
332 };
333 let timeliness = super::timeliness(
334 info.gen_time(),
335 accuracy,
336 self.context.utc,
337 self.context.reference,
338 self.context.requested.is_some(),
339 requested_fields.and_then(|fields| fields.nonce),
340 self.context.window,
341 );
342 let mut information = Information {
343 authority,
344 imprint,
345 requested_imprint,
346 requested_fields,
347 timeliness,
348 accuracy_error,
349 unknown,
350 evidence: None,
351 };
352 if self.common_holds && !info.has_faults() && information.holds() {
353 information.evidence = Some(Evidence {
354 facts: info
355 .retain_facts(inside)
356 .map_err(InfoCheckError::Processing)?,
357 source: source
358 .try_copy_in(&inside.execution())
359 .map_err(InfoCheckError::Provenance)?,
360 });
361 }
362 Ok(information)
363 }
364}
365
366fn unrecognized<SE, PE, RE>(
367 info: &super::TokenInfo<'_>,
368 inside: &Reader<'_>,
369 recognizes: &mut impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
370) -> Result<OwnedVec<OwnedOid>, InfoCheckError<SE, PE, RE>> {
371 let scoped = inside.execution();
372 let mut unknown = scoped.vector();
373 if let Some(extensions) = info.extensions() {
374 for kind in extensions
375 .unrecognized_critical(inside, recognizes)
376 .map_err(InfoCheckError::Recognition)?
377 {
378 let owned = OwnedOid::from_oid_in(kind, &scoped).map_err(|reason| {
379 InfoCheckError::Processing(Error::resource(
380 reason,
381 "tstInfo.extensions",
382 extensions.span().start,
383 ))
384 })?;
385 unknown.try_push(owned).map_err(|reason| {
386 InfoCheckError::Processing(Error::resource(
387 reason,
388 "tstInfo.extensions",
389 extensions.span().start,
390 ))
391 })?;
392 }
393 }
394 Ok(unknown.into_owned())
395}
396
397fn common_holds(
398 signature: &TokenSignature<'_>,
399 bindings: ess::Bindings,
400 identifier: &directory::CertificateAgreement,
401 carried: Option<bool>,
402) -> bool {
403 matches!(signature, TokenSignature::Checked(checked)
404 if checked.signature() == Some(SignatureAnswer::Valid)
405 && matches!(checked.content(), verify::ContentAgreement::Matches | verify::ContentAgreement::Direct))
406 && (bindings.v1 == Some(ess::Binding::Matches)
407 || bindings.v2 == Some(ess::Binding::Matches))
408 && bindings
409 .v1
410 .is_none_or(|binding| binding == ess::Binding::Matches)
411 && bindings
412 .v2
413 .is_none_or(|binding| binding == ess::Binding::Matches)
414 && matches!(identifier, directory::CertificateAgreement::Matches)
415 && carried != Some(false)
416}
417
418impl<'doc> Token<'doc> {
419 #[expect(
429 clippy::result_large_err,
430 reason = "preserve original cause and accounted source context without allocating on refusal"
431 )]
432 pub fn check<'a, P, S: Source, PE, RE>(
433 &'a self,
434 provider: &'a P,
435 parent: &Reader<'a>,
436 context: &CheckContext<'a, '_>,
437 covered: &mut S,
438 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
439 ) -> Result<CheckedTimestamp<'a>, CheckError<S::Error, PE, RE>>
440 where
441 'doc: 'a,
442 P: for<'scope> SignatureProvider<'scope, Error = PE>,
443 {
444 self.check_with_profile(provider, parent, context, covered, recognizes, |_| {
445 Ok::<(), ProfileFailure<Infallible>>(())
446 })
447 .map_err(ProfileCheckError::into_common)
448 }
449
450 #[expect(
458 clippy::result_large_err,
459 reason = "keep opaque common/profile refusal and its source context without allocating while returning an error"
460 )]
461 pub fn check_with_profile<'a, P, S: Source, PE, RE, FE>(
462 &'a self,
463 provider: &'a P,
464 parent: &Reader<'a>,
465 context: &CheckContext<'a, '_>,
466 covered: &mut S,
467 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
468 profile: impl for<'view, 'scope> FnOnce(
469 ProfileInput<'view, 'doc, 'a, 'scope>,
470 ) -> Result<(), ProfileFailure<FE>>,
471 ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<S::Error, PE, RE, FE>>
472 where
473 'doc: 'a,
474 P: for<'scope> SignatureProvider<'scope, Error = PE>,
475 {
476 self.check_input_with_profile(
477 provider,
478 parent,
479 context,
480 &mut RawInput::new(covered),
481 recognizes,
482 profile,
483 )
484 }
485
486 #[expect(
491 clippy::result_large_err,
492 reason = "retain opaque input/provider/profile failures and original source maps"
493 )]
494 pub fn check_input_with_profile<'a, P, I, PE, RE, FE>(
495 &'a self,
496 provider: &'a P,
497 parent: &Reader<'a>,
498 context: &CheckContext<'a, '_>,
499 covered: &mut I,
500 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
501 profile: impl for<'view, 'scope> FnOnce(
502 ProfileInput<'view, 'doc, 'a, 'scope>,
503 ) -> Result<(), ProfileFailure<FE>>,
504 ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<I::Error, PE, RE, FE>>
505 where
506 'doc: 'a,
507 P: for<'scope> SignatureProvider<'scope, Error = PE>,
508 I: ComputedInput<P, PE>,
509 {
510 self.check_in(
511 provider,
512 (parent, None),
513 context,
514 covered,
515 recognizes,
516 profile,
517 )
518 }
519
520 #[expect(
525 clippy::result_large_err,
526 reason = "preserve both source frames and original opaque refusal without a fallible error box"
527 )]
528 pub fn check_mapped<'a, P, S: Source, PE, RE>(
529 &'a self,
530 provider: &'a P,
531 source: (&Reader<'a>, &SourceMap),
532 context: &CheckContext<'a, '_>,
533 covered: &mut S,
534 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
535 ) -> Result<CheckedTimestamp<'a>, MappedCheckError<S::Error, PE, RE>>
536 where
537 'doc: 'a,
538 P: for<'scope> SignatureProvider<'scope, Error = PE>,
539 {
540 self.check_mapped_with_profile(provider, source, context, covered, recognizes, |_| {
541 Ok::<(), ProfileFailure<Infallible>>(())
542 })
543 .map_err(MappedProfileCheckError::into_common)
544 }
545
546 #[expect(
551 clippy::result_large_err,
552 reason = "preserve both independently retained source frames and the original opaque refusal without a fallible error allocation"
553 )]
554 pub fn check_mapped_with_profile<'a, P, S: Source, PE, RE, FE>(
555 &'a self,
556 provider: &'a P,
557 source: (&Reader<'a>, &SourceMap),
558 context: &CheckContext<'a, '_>,
559 covered: &mut S,
560 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
561 profile: impl for<'view, 'scope> FnOnce(
562 ProfileInput<'view, 'doc, 'a, 'scope>,
563 ) -> Result<(), ProfileFailure<FE>>,
564 ) -> Result<CheckedTimestamp<'a>, MappedProfileCheckError<S::Error, PE, RE, FE>>
565 where
566 'doc: 'a,
567 P: for<'scope> SignatureProvider<'scope, Error = PE>,
568 {
569 self.check_mapped_input_with_profile(
570 provider,
571 source,
572 context,
573 &mut RawInput::new(covered),
574 recognizes,
575 profile,
576 )
577 }
578
579 #[expect(
582 clippy::result_large_err,
583 reason = "preserve both source frames and opaque refusal without allocating on failure"
584 )]
585 pub fn check_mapped_input_with_profile<'a, P, I, PE, RE, FE>(
586 &'a self,
587 provider: &'a P,
588 source: (&Reader<'a>, &SourceMap),
589 context: &CheckContext<'a, '_>,
590 covered: &mut I,
591 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
592 profile: impl for<'view, 'scope> FnOnce(
593 ProfileInput<'view, 'doc, 'a, 'scope>,
594 ) -> Result<(), ProfileFailure<FE>>,
595 ) -> Result<CheckedTimestamp<'a>, MappedProfileCheckError<I::Error, PE, RE, FE>>
596 where
597 'doc: 'a,
598 P: for<'scope> SignatureProvider<'scope, Error = PE>,
599 I: ComputedInput<P, PE>,
600 {
601 let (parent, source) = source;
602 preflight_sessions(self, parent, context)
603 .map_err(|error| MappedProfileCheckError::Preflight(error.into()))?;
604 let envelope = source
605 .try_copy_in(&parent.execution())
606 .map_err(MappedProfileCheckError::Provenance)?;
607 self.check_in(
608 provider,
609 (parent, Some(source)),
610 context,
611 covered,
612 recognizes,
613 profile,
614 )
615 .map_err(|source| MappedProfileCheckError::Execution { envelope, source })
616 }
617
618 #[expect(
619 clippy::result_large_err,
620 reason = "retain original cause and accounted source context on refusal"
621 )]
622 fn check_in<'a, P, S, PE, RE, FE>(
623 &'a self,
624 provider: &'a P,
625 origin: (&Reader<'a>, Option<&SourceMap>),
626 context: &CheckContext<'a, '_>,
627 covered: &mut S,
628 mut recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
629 profile: impl for<'view, 'scope> FnOnce(
630 ProfileInput<'view, 'doc, 'a, 'scope>,
631 ) -> Result<(), ProfileFailure<FE>>,
632 ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<S::Error, PE, RE, FE>>
633 where
634 'doc: 'a,
635 P: for<'scope> SignatureProvider<'scope, Error = PE>,
636 S: ComputedInput<P, PE>,
637 {
638 let (parent, source) = origin;
639 preflight(self, parent, context).map_err(CheckError::from)?;
640 let certificate = context.certificate;
641 let identifier =
642 directory::matches_certificate_in(self.signer().sid(), certificate, parent)
643 .map_err(CheckError::Identifier)?;
644 let ess =
645 ess::SigningAttributes::read(self.signer(), parent).map_err(CheckError::Processing)?;
646 let carried = context
647 .requested
648 .as_ref()
649 .map(|requested| {
650 self.matches_cert_req(
651 requested.request,
652 Some(certificate),
653 parent,
654 requested.parent,
655 )
656 })
657 .transpose()
658 .map_err(CheckError::RequestField)?;
659 if !self.faults().is_empty()
660 || !self.message().faults().is_empty()
661 || !self.signer().faults().is_empty()
662 || !ess.faults().is_empty()
663 || ess.v1().is_some_and(|reading| !reading.faults().is_empty())
664 || ess.v2().is_some_and(|reading| !reading.faults().is_empty())
665 {
666 return Err(CheckError::Nonconforming {
667 span: self.message().span(),
668 }
669 .into());
670 }
671 let inspect = |info: &super::TokenInfo<'_>,
672 inside: &Reader<'_>,
673 information: &SourceMap|
674 -> Result<_, profile::InformationFailure<S::Error, PE, RE, FE>> {
675 if info.has_faults() {
676 return Err(InfoCheckError::Nonconforming { span: info.span() }.into());
677 }
678 profile(ProfileInput {
679 token: self,
680 parent,
681 envelope: source,
682 ess: &ess,
683 info,
684 reader: inside,
685 source: information,
686 })
687 .map_err(profile::InformationFailure::Profile)?;
688 let bindings = ess
689 .bind_certificate(provider, certificate, parent)
690 .map_err(InfoCheckError::Ess)?;
691 let signature = self
692 .check_signature(provider, context.key, parent)
693 .map_err(InfoCheckError::Signature)?;
694 let common_holds = common_holds(&signature, bindings, &identifier, carried);
695 let mut inspection = Inspection {
696 provider,
697 context,
698 covered,
699 recognizes: &mut recognizes,
700 common_holds,
701 };
702 let checked = inspection.inspect(info, inside, information)?;
703 Ok((signature, bindings, checked))
704 };
705 let (signature, bindings, information) = match source {
706 Some(source) => self.with_info_mapped(parent, source, inspect),
707 None => self.with_info(parent, inspect),
708 }
709 .map_err(profile::information_error)?;
710 Ok(CheckedTimestamp {
711 signature,
712 identifier,
713 ess,
714 bindings,
715 authority: information.authority,
716 imprint: information.imprint,
717 requested_imprint: information.requested_imprint,
718 requested_fields: information.requested_fields,
719 carried,
720 timeliness: information.timeliness,
721 accuracy_error: information.accuracy_error,
722 unknown_critical: information.unknown,
723 evidence: information.evidence,
724 })
725 }
726}
727
728enum PreflightError {
729 Processing(Error),
730 Requested(super::RequestFieldError),
731 KeyMismatch(structured_asn1::Span),
732}
733
734impl<SE, PE, RE> From<PreflightError> for CheckError<SE, PE, RE> {
735 fn from(error: PreflightError) -> Self {
736 match error {
737 PreflightError::Processing(error) => Self::Processing(error),
738 PreflightError::Requested(error) => Self::RequestField(error),
739 PreflightError::KeyMismatch(certificate) => Self::KeyMismatch { certificate },
740 }
741 }
742}
743
744fn preflight(
745 token: &Token<'_>,
746 parent: &Reader<'_>,
747 context: &CheckContext<'_, '_>,
748) -> Result<(), PreflightError> {
749 preflight_sessions(token, parent, context)?;
750 let certificate = context.certificate;
751 let written = certificate.tbs().public_key().as_bytes();
752 let key = context.key.key().as_bytes();
753 let length = u64::try_from(written.len().max(key.len()))
754 .ok()
755 .and_then(|length| length.checked_add(1))
756 .ok_or_else(|| {
757 PreflightError::Processing(Error::schema(
758 ErrorCode::LengthOverflow,
759 "certificate.subjectPublicKeyInfo",
760 certificate.span().start,
761 ))
762 })?;
763 parent.execution().charge_work(length).map_err(|reason| {
764 PreflightError::Processing(Error::resource(
765 reason,
766 "certificate.subjectPublicKeyInfo",
767 certificate.span().start,
768 ))
769 })?;
770 if written != key {
771 return Err(PreflightError::KeyMismatch(certificate.span()));
772 }
773 Ok(())
774}
775
776fn preflight_sessions(
777 token: &Token<'_>,
778 parent: &Reader<'_>,
779 context: &CheckContext<'_, '_>,
780) -> Result<(), PreflightError> {
781 token
782 .signer()
783 .require_session(parent, "signerInfo")
784 .map_err(PreflightError::Processing)?;
785 let certificate = context.certificate;
786 if !certificate
787 .tbs()
788 .public_key()
789 .algorithm()
790 .same_session(context.certificate_parent)
791 {
792 return Err(PreflightError::Processing(Error::pkix(
793 structured_pkix::Error::new(
794 structured_pkix::ErrorCode::ForeignReader,
795 "certificate",
796 certificate.span().start,
797 ),
798 )));
799 }
800 if let Some(requested) = &context.requested
801 && !requested.request.same_session(requested.parent)
802 {
803 return Err(PreflightError::Requested(super::RequestFieldError {
804 operand: super::ImprintOperand::Requested,
805 source: Error::pkix(structured_pkix::Error::new(
806 structured_pkix::ErrorCode::ForeignReader,
807 "timeStampReq",
808 requested.request.span().start,
809 )),
810 }));
811 }
812 Ok(())
813}