Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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");
    }
}