crates/admit/src/daykey.rs
//! **The day key, lock-free**: holger's privacy construction
//! ([`crate::visitors`]) — `blake3::keyed_hash(key, address)[..16]` under a
//! key drawn from the OS RNG for each UTC day and destroyed at its end — for
//! the admission path, where a global mutex per connection is not allowed.
//!
//! [`crate::visitors::Visitors`] keeps its key under the mutex that also
//! guards its per-door tally, and that is right for a tally. Here the key is
//! the only state, so it is four `AtomicU64` words under a **sequence lock**:
//!
//! * a reader loads the sequence (retrying while it is odd — a rotation in
//! progress), the day and the four key words, and the sequence again; equal
//! sequences mean it read one key whole. Loads only, no lock.
//! * the ONE rotation per day is claimed by a CAS on the sequence (even → odd),
//! so exactly one thread rotates: it **zeroes the old key first**, draws the
//! new one, stamps the day, and releases the sequence (odd → even). From that
//! store on, no token of the closed day can be recomputed by anybody.
//!
//! One token per **connection**, not per request: the caller mints it when a
//! connection is accepted and carries it (`Consumer::Anonymous`) for every
//! request on that connection.
//!
//! A clock that goes backwards keeps the current day's key (the visitors rule:
//! the current window keeps counting rather than reopening a closed one).
use std::net::IpAddr;
use std::sync::atomic::{fence, AtomicU64, Ordering};
use crate::visitors::{Client, WINDOW_SECS};
use crate::DayToken;
/// The anonymous consumer's key for the current UTC day.
pub struct DayKey {
seq: AtomicU64,
day: AtomicU64,
key: [AtomicU64; 4],
}
impl std::fmt::Debug for DayKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Never the key.
f.debug_struct("DayKey").field("day", &self.day.load(Ordering::Relaxed)).finish()
}
}
fn fresh() -> [u64; 4] {
let mut k = [0u8; 32];
if getrandom::getrandom(&mut k).is_err() {
// No OS RNG: the same fallback the visitors key uses.
let mut h = blake3::Hasher::new();
let t = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
h.update(&t.to_le_bytes());
h.update(&(std::ptr::addr_of!(k) as usize).to_le_bytes());
h.update(&std::process::id().to_le_bytes());
k = *h.finalize().as_bytes();
}
let mut w = [0u64; 4];
for (i, word) in w.iter_mut().enumerate() {
*word = u64::from_le_bytes(k[i * 8..i * 8 + 8].try_into().expect("8 bytes"));
}
k.fill(0);
w
}
fn bytes(w: [u64; 4]) -> [u8; 32] {
let mut k = [0u8; 32];
for (i, word) in w.iter().enumerate() {
k[i * 8..i * 8 + 8].copy_from_slice(&word.to_le_bytes());
}
k
}
fn canonical(ip: IpAddr) -> IpAddr {
match ip {
IpAddr::V6(v6) => v6.to_ipv4_mapped().map(IpAddr::V4).unwrap_or(IpAddr::V6(v6)),
v4 => v4,
}
}
impl DayKey {
/// A key for the UTC day of `now_unix`.
pub fn new(now_unix: u64) -> DayKey {
let w = fresh();
DayKey {
seq: AtomicU64::new(0),
day: AtomicU64::new(now_unix / WINDOW_SECS),
key: w.map(AtomicU64::new),
}
}
/// **The day token for `client` at `now_unix`.** Lock-free; rotates the
/// key first when the UTC day has turned (one thread does, the others see
/// the new key).
pub fn token(&self, client: Client<'_>, now_unix: u64) -> DayToken {
let today = now_unix / WINDOW_SECS;
loop {
let s1 = self.seq.load(Ordering::Acquire);
if s1 & 1 == 1 {
std::hint::spin_loop();
continue;
}
let day = self.day.load(Ordering::Acquire);
if today > day {
self.rotate(s1, today);
continue;
}
let w = [0, 1, 2, 3].map(|i| self.key[i].load(Ordering::Acquire));
fence(Ordering::Acquire);
if self.seq.load(Ordering::Relaxed) != s1 {
continue;
}
let mut k = bytes(w);
let digest = match client {
Client::Ip(ip) => match canonical(ip) {
IpAddr::V4(v4) => blake3::keyed_hash(&k, &v4.octets()),
IpAddr::V6(v6) => blake3::keyed_hash(&k, &v6.octets()),
},
Client::Raw(s) => blake3::keyed_hash(&k, s.as_bytes()),
};
k.fill(0);
let mut token = [0u8; 16];
token.copy_from_slice(&digest.as_bytes()[..16]);
return DayToken { day, token };
}
}
/// Claim the rotation (even → odd), destroy the old key, draw the new one.
fn rotate(&self, seen: u64, today: u64) {
if self
.seq
.compare_exchange(seen, seen + 1, Ordering::AcqRel, Ordering::Relaxed)
.is_err()
{
return; // another thread is rotating, or already did
}
for w in &self.key {
w.store(0, Ordering::Relaxed);
}
let new = fresh();
for (w, v) in self.key.iter().zip(new) {
w.store(v, Ordering::Relaxed);
}
self.day.store(today, Ordering::Relaxed);
self.seq.store(seen + 2, Ordering::Release);
}
/// The UTC day the key in force belongs to.
pub fn day(&self) -> u64 {
self.day.load(Ordering::Acquire)
}
/// Test seam: the key words in force.
#[doc(hidden)]
pub fn key_words(&self) -> [u64; 4] {
[0, 1, 2, 3].map(|i| self.key[i].load(Ordering::Acquire))
}
}
#[cfg(test)]
mod tests {
use super::*;
const DAY0: u64 = 20_000 * WINDOW_SECS;
fn ip(s: &str) -> Client<'static> {
Client::Ip(s.parse().unwrap())
}
/// ★ **The day token rotates with the key, and yesterday's key is GONE.**
/// Same client, same day ⇒ same token; next day ⇒ a different token AND a
/// different key in the one place a key is held — so the old token cannot
/// be recomputed from the address by anyone, this process included. What
/// is asserted is exactly what can be: the key words were overwritten, and
/// no token computed with what is held now equals the old one.
#[test]
fn a_day_token_is_stable_within_a_day_and_unrecoverable_after_it() {
let k = DayKey::new(DAY0 + 10);
let t0 = k.token(ip("203.0.113.7"), DAY0 + 20);
assert_eq!(t0, k.token(ip("203.0.113.7"), DAY0 + 80_000), "same client, same day");
assert_eq!(t0, k.token(ip("::ffff:203.0.113.7"), DAY0 + 30), "one host whichever stack");
assert_eq!(t0.day, DAY0 / WINDOW_SECS);
assert_ne!(t0, k.token(ip("198.51.100.9"), DAY0 + 30));
let key0 = k.key_words();
let t1 = k.token(ip("203.0.113.7"), DAY0 + WINDOW_SECS + 1);
assert_eq!(t1.day, t0.day + 1);
assert_ne!(t1.token, t0.token, "a new day is a new token");
assert_ne!(k.key_words(), key0, "the key rotated at the boundary");
assert_eq!(k.token(ip("203.0.113.7"), DAY0 + WINDOW_SECS + 99), t1);
// No token under today's key reproduces yesterday's; asking with
// yesterday's clock (a clock gone backwards) uses TODAY's key.
assert_ne!(k.token(ip("203.0.113.7"), DAY0 + 20).token, t0.token);
assert!(!t0.token.windows(4).any(|w| w == [203, 0, 113, 7]), "the address is in the token");
}
/// ★ Many threads across a midnight: exactly one rotation, and every
/// thread's token afterwards is the same.
#[test]
fn one_rotation_however_many_threads_cross_midnight() {
let k = std::sync::Arc::new(DayKey::new(DAY0));
let tokens: Vec<_> = (0..8)
.map(|_| {
let k = std::sync::Arc::clone(&k);
std::thread::spawn(move || {
let mut last = None;
for _ in 0..1000 {
last = Some(k.token(ip("203.0.113.7"), DAY0 + WINDOW_SECS + 5));
}
last.unwrap()
})
})
.collect::<Vec<_>>()
.into_iter()
.map(|t| t.join().unwrap())
.collect();
assert!(tokens.windows(2).all(|w| w[0] == w[1]), "two keys for one day");
assert_eq!(k.seq.load(Ordering::Relaxed), 2, "exactly one rotation");
}
}