Skip to main content

rite_runtime/
entropy.rs

1//! Ceremony entropy source: a single, auditable, reproducible origin for
2//! every random value a ceremony consumes.
3//!
4//! # The split: gather vs derive
5//!
6//! Randomness enters the source once, by *gathering* non-deterministic bytes
7//! from the host (the machine seed `m`, and optionally free-form human
8//! contributions). Everything after that is *derivation*: a pure, frozen
9//! function of the gathered bytes. The gathered bytes are recorded in the
10//! transcript, so the derivation, and therefore every value drawn, replays
11//! identically from the transcript alone, decades later, in any language.
12//!
13//! # The `rite-kdf/v1` scheme
14//!
15//! All steps use HKDF-SHA-256 (RFC 5869).
16//!
17//! ```text
18//! seed_0      = HKDF-Extract(salt = "rite/seed/v1", IKM = m)
19//! seed_{k+1}  = HKDF-Extract(salt = seed_k,         IKM = utf8(h_k))
20//! path        = "<epoch>/<step>/<purpose>"
21//! value(path) = HKDF-Expand(seed_epoch, info = "rite/nonce/v1/" || path, len)
22//! ```
23//!
24//! The seed is an *epoch chain* (a ratchet): each human contribution `h_k`
25//! advances the epoch by extracting a new seed with the previous seed as the
26//! HMAC salt (key) and the contribution as the IKM (message). This is the
27//! TLS 1.3 key-schedule shape. Because the prior high-entropy seed is the key,
28//! a weak or empty contribution can never reduce strength below the machine
29//! seed; an unpredictable one only adds. A value is drawn from whichever epoch
30//! seed is current when the draw happens, so there is no constraint that human
31//! steps precede draws.
32//!
33//! # Why this is reproducible but a PRNG would not be
34//!
35//! [`CeremonyRandom`] is a deterministic fold over recorded inputs, not opaque
36//! generator state. A verifier rebuilds `seed_0` from the recorded `m`, folds
37//! each recorded contribution in transcript order, and re-derives any value
38//! straight from its recorded path. No draw order, internal buffer, or library
39//! version affects the output.
40
41use std::collections::HashSet;
42
43use hkdf::Hkdf;
44use rite_model::StepId;
45use sha2::Sha256;
46
47/// Frozen-scheme tag recorded with the machine seed. It pins the *entire*
48/// construction (Extract/Expand, the ratchet rule, the `info`/path byte
49/// encodings), not merely the hash. A verifier treats it as a selector among
50/// known-good schemes it already trusts and rejects any value it does not
51/// recognise, it is never an instruction to trust an arbitrary algorithm.
52pub const DERIVATION_V1: &str = "rite-kdf/v1";
53
54/// Salt for the initial `HKDF-Extract` that turns the machine seed into
55/// `seed_0`. Part of the frozen `rite-kdf/v1` scheme.
56const SEED_SALT_V1: &[u8] = b"rite/seed/v1";
57
58/// Prefix prepended to every derivation path to form the `HKDF-Expand` info
59/// string. Part of the frozen `rite-kdf/v1` scheme.
60const VALUE_INFO_PREFIX_V1: &str = "rite/nonce/v1/";
61
62/// Length in bytes of an epoch seed (the HKDF pseudo-random key).
63const SEED_LEN: usize = 32;
64
65/// Longest value a single draw can produce: the `HKDF-Expand` output limit of
66/// `255 * HashLen` bytes (RFC 5869), with SHA-256's 32-byte output. [`expand`]
67/// panics beyond this, so anything replaying a recorded draw must bound the
68/// requested length by this value first.
69pub const MAX_DRAW_LEN: usize = 255 * SEED_LEN;
70
71/// `HKDF-Extract(salt, ikm)` over SHA-256, returning the 32-byte PRK.
72///
73/// The `salt` is a fixed domain-separation constant (RFC 5869 permits a
74/// non-secret, fixed salt). Uniqueness and unpredictability come from the IKM
75/// (the per-ceremony machine entropy), not the salt, so a constant salt here is
76/// correct, not the password-KDF static-salt anti-pattern.
77fn extract(salt: &[u8], ikm: &[u8]) -> [u8; SEED_LEN] {
78    let (prk, _hk) = Hkdf::<Sha256>::extract(Some(salt), ikm);
79    prk.into()
80}
81
82/// `HKDF-Expand(prk, info, len)` over SHA-256.
83///
84/// # Panics
85///
86/// Never in practice. `from_prk` only rejects a PRK shorter than the hash
87/// output, but every seed here is exactly 32 bytes; `expand` only fails when
88/// `len` exceeds [`MAX_DRAW_LEN`], which both untrusted entry points bound
89/// before reaching here (`Reporter::draw` on the draw side, `verify_entropy`
90/// on the replay side). Both `expect`s therefore guard true invariants and
91/// are allowed.
92#[allow(clippy::expect_used)]
93fn expand(prk: &[u8], info: &[u8], len: usize) -> Vec<u8> {
94    let hk = Hkdf::<Sha256>::from_prk(prk).expect("seed is a valid HKDF PRK length");
95    let mut out = vec![0u8; len];
96    hk.expand(info, &mut out)
97        .expect("draw length is within HKDF-Expand limits");
98    out
99}
100
101/// Compute `seed_0` from the machine entropy `m`.
102#[must_use]
103pub fn initial_seed(m: &[u8]) -> [u8; SEED_LEN] {
104    extract(SEED_SALT_V1, m)
105}
106
107/// Advance the ratchet by one epoch, folding in a human contribution.
108#[must_use]
109pub fn fold_seed(current: &[u8; SEED_LEN], contribution: &[u8]) -> [u8; SEED_LEN] {
110    extract(current, contribution)
111}
112
113/// Derive the value at `path` from the given epoch seed.
114///
115/// The verifier calls this directly with a recorded path, so it must stay a
116/// pure function of `(seed_epoch, path, len)`.
117#[must_use]
118pub fn derive_value(seed_epoch: &[u8; SEED_LEN], path: &str, len: usize) -> Vec<u8> {
119    let mut info = Vec::with_capacity(VALUE_INFO_PREFIX_V1.len().saturating_add(path.len()));
120    info.extend_from_slice(VALUE_INFO_PREFIX_V1.as_bytes());
121    info.extend_from_slice(path.as_bytes());
122    expand(seed_epoch, &info, len)
123}
124
125/// Build a derivation path. The single place the path format lives, so the
126/// draw side and the verify side cannot drift. A `purpose` is drawn at most
127/// once per step, so `(epoch, step, purpose)` uniquely identifies a value.
128#[must_use]
129pub fn build_path(epoch: u32, step: &StepId, purpose: &str) -> String {
130    format!("{epoch}/{step}/{purpose}", step = step.as_str())
131}
132
133/// A value drawn from the source, paired with the path that derives it.
134#[derive(Debug, Clone)]
135pub struct Draw {
136    /// Derivation path: `<epoch>/<step>/<purpose>`.
137    pub path: String,
138    /// The derived bytes.
139    pub value: Vec<u8>,
140}
141
142/// The per-ceremony entropy source.
143///
144/// Owns the running epoch seed, the epoch index, and the set of paths already
145/// issued. Held by the [`Reporter`](crate::Reporter), which records a fact for
146/// every gather and every draw so the source can be reconstructed from the
147/// transcript.
148///
149/// The issued-paths set is internal runtime state only, never serialized; it
150/// exists so a repeated `(step, purpose)` is rejected rather than silently
151/// reusing a value.
152#[derive(Debug, Clone)]
153pub struct CeremonyRandom {
154    seed_epoch: [u8; SEED_LEN],
155    epoch: u32,
156    issued: HashSet<String>,
157}
158
159impl CeremonyRandom {
160    /// Build a source from gathered machine entropy `m` (epoch 0).
161    #[must_use]
162    pub fn from_machine_seed(m: &[u8]) -> Self {
163        Self {
164            seed_epoch: initial_seed(m),
165            epoch: 0,
166            issued: HashSet::new(),
167        }
168    }
169
170    /// The current epoch index (0 before any human contribution).
171    #[must_use]
172    pub fn epoch(&self) -> u32 {
173        self.epoch
174    }
175
176    /// Fold a human contribution into the seed, advancing the epoch.
177    pub fn fold(&mut self, contribution: &[u8]) {
178        self.seed_epoch = fold_seed(&self.seed_epoch, contribution);
179        self.epoch = self.epoch.saturating_add(1);
180    }
181
182    /// Draw `len` bytes for `(step, purpose)`.
183    ///
184    /// A `purpose` is drawn at most once per step. Returns `None` if this
185    /// `(step, purpose)` was already drawn, since deriving it again would reuse
186    /// the value; the caller turns that into a hard error. On success returns
187    /// the value together with its derivation path so the caller can record
188    /// both in the transcript.
189    pub fn draw(&mut self, step: &StepId, purpose: &str, len: usize) -> Option<Draw> {
190        let path = build_path(self.epoch, step, purpose);
191        if !self.issued.insert(path.clone()) {
192            return None;
193        }
194        let value = derive_value(&self.seed_epoch, &path, len);
195        Some(Draw { path, value })
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    fn step(id: &str) -> StepId {
204        StepId::new(id)
205    }
206
207    #[test]
208    fn derivation_is_reproducible_from_recorded_inputs() {
209        let m = b"machine entropy";
210        let seed = initial_seed(m);
211        // A verifier with only `m` and the path recomputes the same value.
212        let path = build_path(0, &step("issue"), "cert-serial");
213        let a = derive_value(&seed, &path, 9);
214        let b = derive_value(&initial_seed(m), &path, 9);
215        assert_eq!(a, b);
216        assert_eq!(a.len(), 9);
217    }
218
219    #[test]
220    fn different_paths_yield_different_values() {
221        let seed = initial_seed(b"m");
222        let vp = derive_value(&seed, &build_path(0, &step("s"), "p"), 16);
223        let vq = derive_value(&seed, &build_path(0, &step("s"), "q"), 16);
224        let vs2 = derive_value(&seed, &build_path(0, &step("s2"), "p"), 16);
225        assert_ne!(vp, vq);
226        assert_ne!(vp, vs2);
227    }
228
229    #[test]
230    fn draw_rejects_a_repeated_purpose_in_the_same_step() {
231        let mut r = CeremonyRandom::from_machine_seed(b"m");
232        let first = r.draw(&step("s"), "tpm-quote", 20).expect("first draw");
233        assert_eq!(first.path, "0/s/tpm-quote");
234        // The same (step, purpose) again would reuse the value: rejected.
235        assert!(r.draw(&step("s"), "tpm-quote", 20).is_none());
236        // A different purpose under the same step is fine.
237        let other = r
238            .draw(&step("s"), "cert-serial", 9)
239            .expect("distinct purpose");
240        assert_eq!(other.path, "0/s/cert-serial");
241        assert_ne!(first.value, other.value);
242    }
243
244    #[test]
245    fn folding_advances_epoch_and_changes_draws() {
246        let mut plain = CeremonyRandom::from_machine_seed(b"m");
247        let mut folded = CeremonyRandom::from_machine_seed(b"m");
248        folded.fold(b"3 1 6 4 2 5");
249        assert_eq!(plain.epoch(), 0);
250        assert_eq!(folded.epoch(), 1);
251
252        let p = plain.draw(&step("s"), "p", 16).expect("draw");
253        let f = folded.draw(&step("s"), "p", 16).expect("draw");
254        // Same step/purpose, different epoch seed: different value and path.
255        assert_eq!(p.path, "0/s/p");
256        assert_eq!(f.path, "1/s/p");
257        assert_ne!(p.value, f.value);
258    }
259
260    #[test]
261    fn fold_is_a_deterministic_function_of_inputs() {
262        // Two sources fed the same machine seed and the same contribution in
263        // the same order must derive identically: the verifier's replay.
264        let mut a = CeremonyRandom::from_machine_seed(b"seed");
265        let mut b = CeremonyRandom::from_machine_seed(b"seed");
266        a.fold(b"alice");
267        b.fold(b"alice");
268        assert_eq!(
269            a.draw(&step("s"), "p", 32).expect("draw").value,
270            b.draw(&step("s"), "p", 32).expect("draw").value
271        );
272    }
273
274    #[test]
275    fn empty_contribution_still_advances_but_never_panics() {
276        // A weak or empty human input is the HMAC message, not the key, so it
277        // is always safe to accept.
278        let mut r = CeremonyRandom::from_machine_seed(b"m");
279        r.fold(b"");
280        assert_eq!(r.epoch(), 1);
281        assert_eq!(r.draw(&step("s"), "p", 8).expect("draw").value.len(), 8);
282    }
283}