Skip to main content

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}