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