1use super::profile::{
4 self, MappedProfileCheckError, ProfileCheckError, ProfileFailure, ProfileInput,
5};
6use super::{
7 AuthorityFacts, ImprintAgreement, RecognitionError, Request, RequestAgreement, RequestFields,
8 RequestWindow, RetainedFacts, SignatureError, Timeliness, Token, TokenExtension,
9 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: Source, 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 R: FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
281 {
282 let authority = info
283 .authority(
284 self.context.certificate,
285 inside,
286 self.context.certificate_parent,
287 )
288 .map_err(InfoCheckError::Authority)?;
289 let requested_fields = self
290 .context
291 .requested
292 .as_ref()
293 .map(|request| info.matches_request_fields(request.request, inside, request.parent))
294 .transpose()
295 .map_err(InfoCheckError::RequestField)?;
296 let unknown = unrecognized(info, inside, self.recognizes)?;
297 let (imprint, requested_imprint) = match &self.context.requested {
298 Some(request) => {
299 let (imprint, agreement) = info
300 .imprint()
301 .check_requested_content(
302 request.request.imprint(),
303 self.provider,
304 (inside, request.parent),
305 self.covered,
306 )
307 .map_err(|error| match error {
308 super::RequestedContentError::Request(source) => {
309 InfoCheckError::RequestImprint(source)
310 }
311 super::RequestedContentError::Content(source) => {
312 InfoCheckError::Imprint(source)
313 }
314 })?;
315 (imprint, Some(agreement))
316 }
317 None => (
318 info.imprint()
319 .check_content(self.provider, inside, self.covered)
320 .map_err(InfoCheckError::Imprint)?,
321 None,
322 ),
323 };
324 let (accuracy, accuracy_error) = match info
325 .accuracy()
326 .map(super::AccuracyReading::deviation)
327 .transpose()
328 {
329 Ok(accuracy) => (accuracy, None),
330 Err(error) => (None, Some(error)),
331 };
332 let timeliness = super::timeliness(
333 info.gen_time(),
334 accuracy,
335 self.context.utc,
336 self.context.reference,
337 self.context.requested.is_some(),
338 requested_fields.and_then(|fields| fields.nonce),
339 self.context.window,
340 );
341 let mut information = Information {
342 authority,
343 imprint,
344 requested_imprint,
345 requested_fields,
346 timeliness,
347 accuracy_error,
348 unknown,
349 evidence: None,
350 };
351 if self.common_holds && !info.has_faults() && information.holds() {
352 information.evidence = Some(Evidence {
353 facts: info
354 .retain_facts(inside)
355 .map_err(InfoCheckError::Processing)?,
356 source: source
357 .try_copy_in(&inside.execution())
358 .map_err(InfoCheckError::Provenance)?,
359 });
360 }
361 Ok(information)
362 }
363}
364
365fn unrecognized<SE, PE, RE>(
366 info: &super::TokenInfo<'_>,
367 inside: &Reader<'_>,
368 recognizes: &mut impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
369) -> Result<OwnedVec<OwnedOid>, InfoCheckError<SE, PE, RE>> {
370 let scoped = inside.execution();
371 let mut unknown = scoped.vector();
372 if let Some(extensions) = info.extensions() {
373 for kind in extensions
374 .unrecognized_critical(inside, recognizes)
375 .map_err(InfoCheckError::Recognition)?
376 {
377 let owned = OwnedOid::from_oid_in(kind, &scoped).map_err(|reason| {
378 InfoCheckError::Processing(Error::resource(
379 reason,
380 "tstInfo.extensions",
381 extensions.span().start,
382 ))
383 })?;
384 unknown.try_push(owned).map_err(|reason| {
385 InfoCheckError::Processing(Error::resource(
386 reason,
387 "tstInfo.extensions",
388 extensions.span().start,
389 ))
390 })?;
391 }
392 }
393 Ok(unknown.into_owned())
394}
395
396fn common_holds(
397 signature: &TokenSignature<'_>,
398 bindings: ess::Bindings,
399 identifier: &directory::CertificateAgreement,
400 carried: Option<bool>,
401) -> bool {
402 matches!(signature, TokenSignature::Checked(checked)
403 if checked.signature() == Some(SignatureAnswer::Valid)
404 && matches!(checked.content(), verify::ContentAgreement::Matches | verify::ContentAgreement::Direct))
405 && (bindings.v1 == Some(ess::Binding::Matches)
406 || bindings.v2 == Some(ess::Binding::Matches))
407 && bindings
408 .v1
409 .is_none_or(|binding| binding == ess::Binding::Matches)
410 && bindings
411 .v2
412 .is_none_or(|binding| binding == ess::Binding::Matches)
413 && matches!(identifier, directory::CertificateAgreement::Matches)
414 && carried != Some(false)
415}
416
417impl<'doc> Token<'doc> {
418 #[expect(
428 clippy::result_large_err,
429 reason = "preserve original cause and accounted source context without allocating on refusal"
430 )]
431 pub fn check<'a, P, S: Source, PE, RE>(
432 &'a self,
433 provider: &'a P,
434 parent: &Reader<'a>,
435 context: &CheckContext<'a, '_>,
436 covered: &mut S,
437 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
438 ) -> Result<CheckedTimestamp<'a>, CheckError<S::Error, PE, RE>>
439 where
440 'doc: 'a,
441 P: for<'scope> SignatureProvider<'scope, Error = PE>,
442 {
443 self.check_with_profile(provider, parent, context, covered, recognizes, |_| {
444 Ok::<(), ProfileFailure<Infallible>>(())
445 })
446 .map_err(ProfileCheckError::into_common)
447 }
448
449 #[expect(
457 clippy::result_large_err,
458 reason = "keep opaque common/profile refusal and its source context without allocating while returning an error"
459 )]
460 pub fn check_with_profile<'a, P, S: Source, PE, RE, FE>(
461 &'a self,
462 provider: &'a P,
463 parent: &Reader<'a>,
464 context: &CheckContext<'a, '_>,
465 covered: &mut S,
466 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
467 profile: impl for<'view, 'scope> FnOnce(
468 ProfileInput<'view, 'doc, 'a, 'scope>,
469 ) -> Result<(), ProfileFailure<FE>>,
470 ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<S::Error, PE, RE, FE>>
471 where
472 'doc: 'a,
473 P: for<'scope> SignatureProvider<'scope, Error = PE>,
474 {
475 self.check_in(
476 provider,
477 (parent, None),
478 context,
479 covered,
480 recognizes,
481 profile,
482 )
483 }
484
485 #[expect(
490 clippy::result_large_err,
491 reason = "preserve both source frames and original opaque refusal without a fallible error box"
492 )]
493 pub fn check_mapped<'a, P, S: Source, PE, RE>(
494 &'a self,
495 provider: &'a P,
496 source: (&Reader<'a>, &SourceMap),
497 context: &CheckContext<'a, '_>,
498 covered: &mut S,
499 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
500 ) -> Result<CheckedTimestamp<'a>, MappedCheckError<S::Error, PE, RE>>
501 where
502 'doc: 'a,
503 P: for<'scope> SignatureProvider<'scope, Error = PE>,
504 {
505 self.check_mapped_with_profile(provider, source, context, covered, recognizes, |_| {
506 Ok::<(), ProfileFailure<Infallible>>(())
507 })
508 .map_err(MappedProfileCheckError::into_common)
509 }
510
511 #[expect(
516 clippy::result_large_err,
517 reason = "preserve both independently retained source frames and the original opaque refusal without a fallible error allocation"
518 )]
519 pub fn check_mapped_with_profile<'a, P, S: Source, PE, RE, FE>(
520 &'a self,
521 provider: &'a P,
522 source: (&Reader<'a>, &SourceMap),
523 context: &CheckContext<'a, '_>,
524 covered: &mut S,
525 recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
526 profile: impl for<'view, 'scope> FnOnce(
527 ProfileInput<'view, 'doc, 'a, 'scope>,
528 ) -> Result<(), ProfileFailure<FE>>,
529 ) -> Result<CheckedTimestamp<'a>, MappedProfileCheckError<S::Error, PE, RE, FE>>
530 where
531 'doc: 'a,
532 P: for<'scope> SignatureProvider<'scope, Error = PE>,
533 {
534 let (parent, source) = source;
535 preflight_sessions(self, parent, context)
536 .map_err(|error| MappedProfileCheckError::Preflight(error.into()))?;
537 let envelope = source
538 .try_copy_in(&parent.execution())
539 .map_err(MappedProfileCheckError::Provenance)?;
540 self.check_in(
541 provider,
542 (parent, Some(source)),
543 context,
544 covered,
545 recognizes,
546 profile,
547 )
548 .map_err(|source| MappedProfileCheckError::Execution { envelope, source })
549 }
550
551 #[expect(
552 clippy::result_large_err,
553 reason = "retain original cause and accounted source context on refusal"
554 )]
555 fn check_in<'a, P, S: Source, PE, RE, FE>(
556 &'a self,
557 provider: &'a P,
558 origin: (&Reader<'a>, Option<&SourceMap>),
559 context: &CheckContext<'a, '_>,
560 covered: &mut S,
561 mut recognizes: impl FnMut(&TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
562 profile: impl for<'view, 'scope> FnOnce(
563 ProfileInput<'view, 'doc, 'a, 'scope>,
564 ) -> Result<(), ProfileFailure<FE>>,
565 ) -> Result<CheckedTimestamp<'a>, ProfileCheckError<S::Error, PE, RE, FE>>
566 where
567 'doc: 'a,
568 P: for<'scope> SignatureProvider<'scope, Error = PE>,
569 {
570 let (parent, source) = origin;
571 preflight(self, parent, context).map_err(CheckError::from)?;
572 let certificate = context.certificate;
573 let identifier =
574 directory::matches_certificate_in(self.signer().sid(), certificate, parent)
575 .map_err(CheckError::Identifier)?;
576 let ess =
577 ess::SigningAttributes::read(self.signer(), parent).map_err(CheckError::Processing)?;
578 let carried = context
579 .requested
580 .as_ref()
581 .map(|requested| {
582 self.matches_cert_req(
583 requested.request,
584 Some(certificate),
585 parent,
586 requested.parent,
587 )
588 })
589 .transpose()
590 .map_err(CheckError::RequestField)?;
591 if !self.faults().is_empty()
592 || !self.message().faults().is_empty()
593 || !self.signer().faults().is_empty()
594 || !ess.faults().is_empty()
595 || ess.v1().is_some_and(|reading| !reading.faults().is_empty())
596 || ess.v2().is_some_and(|reading| !reading.faults().is_empty())
597 {
598 return Err(CheckError::Nonconforming {
599 span: self.message().span(),
600 }
601 .into());
602 }
603 let inspect = |info: &super::TokenInfo<'_>,
604 inside: &Reader<'_>,
605 information: &SourceMap|
606 -> Result<_, profile::InformationFailure<S::Error, PE, RE, FE>> {
607 if info.has_faults() {
608 return Err(InfoCheckError::Nonconforming { span: info.span() }.into());
609 }
610 profile(ProfileInput {
611 token: self,
612 parent,
613 envelope: source,
614 ess: &ess,
615 info,
616 reader: inside,
617 source: information,
618 })
619 .map_err(profile::InformationFailure::Profile)?;
620 let bindings = ess
621 .bind_certificate(provider, certificate, parent)
622 .map_err(InfoCheckError::Ess)?;
623 let signature = self
624 .check_signature(provider, context.key, parent)
625 .map_err(InfoCheckError::Signature)?;
626 let common_holds = common_holds(&signature, bindings, &identifier, carried);
627 let mut inspection = Inspection {
628 provider,
629 context,
630 covered,
631 recognizes: &mut recognizes,
632 common_holds,
633 };
634 let checked = inspection.inspect(info, inside, information)?;
635 Ok((signature, bindings, checked))
636 };
637 let (signature, bindings, information) = match source {
638 Some(source) => self.with_info_mapped(parent, source, inspect),
639 None => self.with_info(parent, inspect),
640 }
641 .map_err(profile::information_error)?;
642 Ok(CheckedTimestamp {
643 signature,
644 identifier,
645 ess,
646 bindings,
647 authority: information.authority,
648 imprint: information.imprint,
649 requested_imprint: information.requested_imprint,
650 requested_fields: information.requested_fields,
651 carried,
652 timeliness: information.timeliness,
653 accuracy_error: information.accuracy_error,
654 unknown_critical: information.unknown,
655 evidence: information.evidence,
656 })
657 }
658}
659
660enum PreflightError {
661 Processing(Error),
662 Requested(super::RequestFieldError),
663 KeyMismatch(structured_asn1::Span),
664}
665
666impl<SE, PE, RE> From<PreflightError> for CheckError<SE, PE, RE> {
667 fn from(error: PreflightError) -> Self {
668 match error {
669 PreflightError::Processing(error) => Self::Processing(error),
670 PreflightError::Requested(error) => Self::RequestField(error),
671 PreflightError::KeyMismatch(certificate) => Self::KeyMismatch { certificate },
672 }
673 }
674}
675
676fn preflight(
677 token: &Token<'_>,
678 parent: &Reader<'_>,
679 context: &CheckContext<'_, '_>,
680) -> Result<(), PreflightError> {
681 preflight_sessions(token, parent, context)?;
682 let certificate = context.certificate;
683 let written = certificate.tbs().public_key().as_bytes();
684 let key = context.key.key().as_bytes();
685 let length = u64::try_from(written.len().max(key.len()))
686 .ok()
687 .and_then(|length| length.checked_add(1))
688 .ok_or_else(|| {
689 PreflightError::Processing(Error::schema(
690 ErrorCode::LengthOverflow,
691 "certificate.subjectPublicKeyInfo",
692 certificate.span().start,
693 ))
694 })?;
695 parent.execution().charge_work(length).map_err(|reason| {
696 PreflightError::Processing(Error::resource(
697 reason,
698 "certificate.subjectPublicKeyInfo",
699 certificate.span().start,
700 ))
701 })?;
702 if written != key {
703 return Err(PreflightError::KeyMismatch(certificate.span()));
704 }
705 Ok(())
706}
707
708fn preflight_sessions(
709 token: &Token<'_>,
710 parent: &Reader<'_>,
711 context: &CheckContext<'_, '_>,
712) -> Result<(), PreflightError> {
713 token
714 .signer()
715 .require_session(parent, "signerInfo")
716 .map_err(PreflightError::Processing)?;
717 let certificate = context.certificate;
718 if !certificate
719 .tbs()
720 .public_key()
721 .algorithm()
722 .same_session(context.certificate_parent)
723 {
724 return Err(PreflightError::Processing(Error::pkix(
725 structured_pkix::Error::new(
726 structured_pkix::ErrorCode::ForeignReader,
727 "certificate",
728 certificate.span().start,
729 ),
730 )));
731 }
732 if let Some(requested) = &context.requested
733 && !requested.request.same_session(requested.parent)
734 {
735 return Err(PreflightError::Requested(super::RequestFieldError {
736 operand: super::ImprintOperand::Requested,
737 source: Error::pkix(structured_pkix::Error::new(
738 structured_pkix::ErrorCode::ForeignReader,
739 "timeStampReq",
740 requested.request.span().start,
741 )),
742 }));
743 }
744 Ok(())
745}