use std::collections::HashSet;
use hkdf::Hkdf;
use rite_model::StepId;
use sha2::Sha256;
pub const DERIVATION_V1: &str = "rite-kdf/v1";
const SEED_SALT_V1: &[u8] = b"rite/seed/v1";
const VALUE_INFO_PREFIX_V1: &str = "rite/nonce/v1/";
const SEED_LEN: usize = 32;
pub const MAX_DRAW_LEN: usize = 255 * SEED_LEN;
fn extract(salt: &[u8], ikm: &[u8]) -> [u8; SEED_LEN] {
let (prk, _hk) = Hkdf::<Sha256>::extract(Some(salt), ikm);
prk.into()
}
#[allow(clippy::expect_used)]
fn expand(prk: &[u8], info: &[u8], len: usize) -> Vec<u8> {
let hk = Hkdf::<Sha256>::from_prk(prk).expect("seed is a valid HKDF PRK length");
let mut out = vec![0u8; len];
hk.expand(info, &mut out)
.expect("draw length is within HKDF-Expand limits");
out
}
#[must_use]
pub fn initial_seed(m: &[u8]) -> [u8; SEED_LEN] {
extract(SEED_SALT_V1, m)
}
#[must_use]
pub fn fold_seed(current: &[u8; SEED_LEN], contribution: &[u8]) -> [u8; SEED_LEN] {
extract(current, contribution)
}
#[must_use]
pub fn derive_value(seed_epoch: &[u8; SEED_LEN], path: &str, len: usize) -> Vec<u8> {
let mut info = Vec::with_capacity(VALUE_INFO_PREFIX_V1.len().saturating_add(path.len()));
info.extend_from_slice(VALUE_INFO_PREFIX_V1.as_bytes());
info.extend_from_slice(path.as_bytes());
expand(seed_epoch, &info, len)
}
#[must_use]
pub fn build_path(epoch: u32, step: &StepId, purpose: &str) -> String {
format!("{epoch}/{step}/{purpose}", step = step.as_str())
}
#[derive(Debug, Clone)]
pub struct Draw {
pub path: String,
pub value: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct CeremonyRandom {
seed_epoch: [u8; SEED_LEN],
epoch: u32,
issued: HashSet<String>,
}
impl CeremonyRandom {
#[must_use]
pub fn from_machine_seed(m: &[u8]) -> Self {
Self {
seed_epoch: initial_seed(m),
epoch: 0,
issued: HashSet::new(),
}
}
#[must_use]
pub fn epoch(&self) -> u32 {
self.epoch
}
pub fn fold(&mut self, contribution: &[u8]) {
self.seed_epoch = fold_seed(&self.seed_epoch, contribution);
self.epoch = self.epoch.saturating_add(1);
}
pub fn draw(&mut self, step: &StepId, purpose: &str, len: usize) -> Option<Draw> {
let path = build_path(self.epoch, step, purpose);
if !self.issued.insert(path.clone()) {
return None;
}
let value = derive_value(&self.seed_epoch, &path, len);
Some(Draw { path, value })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn step(id: &str) -> StepId {
StepId::new(id)
}
#[test]
fn derivation_is_reproducible_from_recorded_inputs() {
let m = b"machine entropy";
let seed = initial_seed(m);
let path = build_path(0, &step("issue"), "cert-serial");
let a = derive_value(&seed, &path, 9);
let b = derive_value(&initial_seed(m), &path, 9);
assert_eq!(a, b);
assert_eq!(a.len(), 9);
}
#[test]
fn different_paths_yield_different_values() {
let seed = initial_seed(b"m");
let vp = derive_value(&seed, &build_path(0, &step("s"), "p"), 16);
let vq = derive_value(&seed, &build_path(0, &step("s"), "q"), 16);
let vs2 = derive_value(&seed, &build_path(0, &step("s2"), "p"), 16);
assert_ne!(vp, vq);
assert_ne!(vp, vs2);
}
#[test]
fn draw_rejects_a_repeated_purpose_in_the_same_step() {
let mut r = CeremonyRandom::from_machine_seed(b"m");
let first = r.draw(&step("s"), "tpm-quote", 20).expect("first draw");
assert_eq!(first.path, "0/s/tpm-quote");
assert!(r.draw(&step("s"), "tpm-quote", 20).is_none());
let other = r
.draw(&step("s"), "cert-serial", 9)
.expect("distinct purpose");
assert_eq!(other.path, "0/s/cert-serial");
assert_ne!(first.value, other.value);
}
#[test]
fn folding_advances_epoch_and_changes_draws() {
let mut plain = CeremonyRandom::from_machine_seed(b"m");
let mut folded = CeremonyRandom::from_machine_seed(b"m");
folded.fold(b"3 1 6 4 2 5");
assert_eq!(plain.epoch(), 0);
assert_eq!(folded.epoch(), 1);
let p = plain.draw(&step("s"), "p", 16).expect("draw");
let f = folded.draw(&step("s"), "p", 16).expect("draw");
assert_eq!(p.path, "0/s/p");
assert_eq!(f.path, "1/s/p");
assert_ne!(p.value, f.value);
}
#[test]
fn fold_is_a_deterministic_function_of_inputs() {
let mut a = CeremonyRandom::from_machine_seed(b"seed");
let mut b = CeremonyRandom::from_machine_seed(b"seed");
a.fold(b"alice");
b.fold(b"alice");
assert_eq!(
a.draw(&step("s"), "p", 32).expect("draw").value,
b.draw(&step("s"), "p", 32).expect("draw").value
);
}
#[test]
fn empty_contribution_still_advances_but_never_panics() {
let mut r = CeremonyRandom::from_machine_seed(b"m");
r.fold(b"");
assert_eq!(r.epoch(), 1);
assert_eq!(r.draw(&step("s"), "p", 8).expect("draw").value.len(), 8);
}
}