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}