1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! OA2-E0.4 of `docs/internal/plans/OA2E_INTEGRATION_DESIGN.md` — a single
//! paired clock sample for provider admission.
//!
//! Admission checks two things against time: proof/credential
//! FRESHNESS (a wall clock — "is this cert/grant/proof still within
//! its validity window?") and replay RETENTION (a monotonic clock —
//! "how long must this `(caller, call_id)` stay in the guard?"). If
//! those read the wall clock at two DIFFERENT moments, a wall-clock
//! jump between them can immediately expire the replay entry for a
//! proof that just passed its freshness check (addendum §3).
//!
//! [`ClockSample`] captures BOTH clocks together, once, so every
//! per-admission decision derives from the same instant. E0 lands
//! this helper; the RULE that admission uses it (freshness reads
//! `wall_ns`, the replay deadline derives from the same sample) is
//! enforced when E1 wires the admission gate — this module has no
//! caller yet.
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
/// A single paired capture of wall-clock and monotonic time.
///
/// Take one per admission with [`Self::now`], then use `wall_ns` for
/// every certificate/grant/proof freshness check and
/// [`Self::monotonic_deadline_for`] to translate the proof's
/// wall-clock expiry into the monotonic deadline the replay guard
/// retains it under — all relative to the SAME sample.
#[derive(Debug, Clone, Copy)]
pub struct ClockSample {
/// Wall-clock now, unix NANOSECONDS. Every freshness check reads
/// this — never a freshly-sampled clock.
pub wall_ns: u64,
/// Monotonic now, paired with `wall_ns`. Replay-retention
/// deadlines derive from this so a wall-clock jump cannot shift
/// retention out from under a freshness check.
pub monotonic: Instant,
}
impl ClockSample {
/// Capture both clocks together. A pre-epoch system clock saturates
/// `wall_ns` to 0 (the same fail-safe the org module's
/// `current_timestamp` uses).
///
/// §33 — what that fail-safe actually buys, corrected. The previous note
/// said admission "treats every finite expiry as in the future, which is
/// fine". The first half is right and the conclusion is not: with
/// `wall_ns == 0`, `check_expiry_at(0, skew)` can never expire a proof and
/// its TTL ceiling collapses to `MAX_ORG_PROOF_TTL + skew`, so proof
/// FRESHNESS is entirely defeated, not merely permissive.
///
/// The call is still denied — but by an unrelated check:
/// `is_valid_at_with_skew(0, skew)` returns `NotYetValid` for any real
/// certificate, because no certificate's `not_before` is 0. So the
/// composition is fail-closed today for a reason this comment did not
/// state, and any future `ClockSample` consumer that checks only an UPPER
/// time bound would fail OPEN on a machine that boots with a dead RTC.
///
/// Reachable in practice: a board with a dead battery comes up at the
/// epoch, and this is exactly when an operator is least likely to be
/// watching admission decisions.
pub fn now() -> Self {
let wall_ns = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos().min(u64::MAX as u128) as u64)
.unwrap_or(0);
Self {
wall_ns,
monotonic: Instant::now(),
}
}
/// The monotonic deadline corresponding to a wall-clock expiry
/// (`wall_deadline_ns`, unix ns), derived RELATIVE to this one
/// sample: `monotonic + max(0, wall_deadline_ns - wall_ns)`.
///
/// A deadline already at or behind `wall_ns` yields
/// `self.monotonic` (a zero-length retention — the proof is
/// never held beyond its own life), so a proof that just failed
/// freshness cannot be retained, and one that just passed is
/// retained by exactly its remaining validity — consistent with
/// the freshness check that used the same `wall_ns`.
pub fn monotonic_deadline_for(&self, wall_deadline_ns: u64) -> Instant {
let remaining_ns = wall_deadline_ns.saturating_sub(self.wall_ns);
self.monotonic + Duration::from_nanos(remaining_ns)
}
/// This sample's wall clock in whole unix seconds — the single
/// ns→s conversion the second-granularity credential checks
/// (`is_valid_at_with_skew`, provider `self_verify_at`) share, so
/// caller AND provider verification read the SAME instant with no
/// hidden second wall read (AV-6 item 6).
pub fn wall_secs(&self) -> u64 {
self.wall_ns / 1_000_000_000
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn now_captures_both_clocks_coherently() {
let before = Instant::now();
let s = ClockSample::now();
let after = Instant::now();
assert!(s.monotonic >= before && s.monotonic <= after);
// The wall sample is a plausible recent unix-ns value (past
// 2020: 1.5e18 ns) unless the system clock is pre-epoch.
assert!(s.wall_ns == 0 || s.wall_ns > 1_500_000_000_000_000_000);
}
#[test]
fn future_deadline_is_that_far_ahead_on_the_monotonic_clock() {
let s = ClockSample {
wall_ns: 1_000_000_000_000_000_000,
monotonic: Instant::now(),
};
// 30 s in the future (wall) → 30 s ahead of the sample's
// monotonic instant.
let deadline = s.monotonic_deadline_for(s.wall_ns + 30_000_000_000);
assert_eq!(deadline, s.monotonic + Duration::from_secs(30));
}
#[test]
fn past_or_equal_deadline_never_retains_beyond_the_sample() {
let s = ClockSample {
wall_ns: 1_000_000_000_000_000_000,
monotonic: Instant::now(),
};
// Exactly now → zero retention.
assert_eq!(s.monotonic_deadline_for(s.wall_ns), s.monotonic);
// Already in the past → still clamped to the sample instant
// (saturating), never before it.
assert_eq!(
s.monotonic_deadline_for(s.wall_ns - 5_000_000_000),
s.monotonic
);
}
#[test]
fn one_sample_yields_consistent_deadlines() {
// Two deadlines derived from ONE sample are ordered by their
// wall-clock expiries and both anchored to the same monotonic
// instant — no second wall read can perturb them.
let s = ClockSample::now();
let near = s.monotonic_deadline_for(s.wall_ns + 10_000_000_000);
let far = s.monotonic_deadline_for(s.wall_ns + 20_000_000_000);
assert!(far > near);
assert_eq!(far - near, Duration::from_secs(10));
}
}