structured_pkix/crl/freshness.rs
1//! Update interval facts using explicitly supplied UTC evidence.
2
3use super::Authenticated;
4use crate::error::{Error, Result};
5use crate::temporal::{LeapTable, Reading, Uncertainty};
6use structured_asn1::memory::Work;
7
8/// Freshness of an authenticated source, independently of target scope.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Freshness {
11 /// The supplied moment is in [thisUpdate, nextUpdate).
12 Current,
13 /// Exactly nextUpdate. Cache reuse and newly fetched replacement have
14 /// different endpoint rules in RFC 5280 §6.3.3(a); no policy is inferred.
15 AtNextUpdate,
16 /// The source was issued after the supplied moment.
17 NotYetCurrent,
18 /// The supplied moment is strictly after nextUpdate.
19 Stale,
20 /// No next update was supplied; no lifetime is inferred.
21 MissingNextUpdate,
22 /// The encoded update interval is empty or inverted.
23 InvalidUpdateOrder,
24 /// UTC facts cannot establish one of the compared labels.
25 Uncertain(Uncertainty),
26}
27
28pub(crate) fn update_window<W: Work>(
29 this: crate::Time,
30 next: Option<crate::Time>,
31 now: Reading,
32 utc: &LeapTable<'_>,
33 execution: &W,
34 origin: (&'static str, u64),
35) -> Result<Freshness> {
36 let (field, at) = origin;
37 execution
38 .charge_work(1)
39 .map_err(|reason| Error::resource(reason, field, at))?;
40 // Three labels, each with three binary searches. Fixed work covers the
41 // scalar checks/arithmetic; the full data-dependent cost precedes shifts.
42 let searches = u64::from(usize::BITS - utc.transitions().len().leading_zeros() + 1);
43 let comparisons = if next.is_some() { 9 } else { 6 };
44 execution
45 .charge_work(searches * comparisons + 64)
46 .map_err(|reason| Error::resource(reason, field, at))?;
47 let this = this.instant();
48 let remainder = now.has_subnanosecond_remainder();
49 let now = now.start();
50 for label in [this, now] {
51 if let Err(reason) = utc.shift(label, 0) {
52 return Ok(Freshness::Uncertain(reason));
53 }
54 }
55 let Some(next) = next else {
56 // Missing expiry must not hide a future issue time or unknown UTC
57 // label. RFC6960 §4.2.2.1 applies thisUpdate reliability independently.
58 // https://www.rfc-editor.org/rfc/rfc6960#section-4.2.2.1
59 return Ok(if now < this {
60 Freshness::NotYetCurrent
61 } else {
62 Freshness::MissingNextUpdate
63 });
64 };
65 let next = next.instant();
66 if let Err(reason) = utc.shift(next, 0) {
67 return Ok(Freshness::Uncertain(reason));
68 }
69 Ok(if next <= this {
70 Freshness::InvalidUpdateOrder
71 } else if now < this {
72 Freshness::NotYetCurrent
73 } else if now == next && !remainder {
74 Freshness::AtNextUpdate
75 } else if now >= next {
76 Freshness::Stale
77 } else {
78 Freshness::Current
79 })
80}
81
82impl Authenticated<'_, '_> {
83 /// Establish update-interval facts without reading a clock or fetching UTC
84 /// announcements. RFC 5280 §§5.1.2.4–5.1.2.5 define whole-second update
85 /// times. `Current` covers the half-open interval; exact nextUpdate is
86 /// preserved separately because cached and newly obtained sources have
87 /// different endpoint requirements in RFC 5280 §6.3.3(a).
88 /// <https://www.rfc-editor.org/rfc/rfc5280#section-5.1.2.5>
89 ///
90 /// A subnanosecond remainder distinguishes exact nextUpdate from after it.
91 /// All compared UTC labels are validated against the supplied table first.
92 /// Missing coverage and contradicted leap labels remain uncertainty.
93 pub fn freshness<W: Work>(
94 &self,
95 now: Reading,
96 utc: &LeapTable<'_>,
97 execution: &W,
98 ) -> Result<Freshness> {
99 update_window(
100 self.crl().tbs().this_update(),
101 self.crl().tbs().next_update(),
102 now,
103 utc,
104 execution,
105 ("certificateList.freshness", self.crl().span().start),
106 )
107 }
108}