Skip to main content

lib_q_random/
kt128_expander.rs

1//! KT128 (`KangarooTwelve`) deterministic byte expansion for test and KAT RNGs.
2//!
3//! Uses RFC 9861 KT128 as an XOF with explicit domain separation labels.
4
5use lib_q_k12::Kt128;
6use lib_q_k12::digest::{
7    ExtendableOutput,
8    Update,
9    XofReader,
10};
11
12/// Domain label for general lib-Q deterministic RNG (`new_deterministic`, etc.).
13pub const DOMAIN_LIBQ_DET_RNG: &[u8] = b"libQ-DET-RNG-v1";
14
15/// Domain label for HPKE [`crate::Kt128Rng`] (unchanged from prior inline implementation).
16pub const DOMAIN_HPKE_RNG: &[u8] = b"HPKE-RNG";
17
18/// Domain label for optional Saturnin CTR deterministic path.
19#[cfg(feature = "deterministic-saturnin")]
20pub const DOMAIN_LIBQ_DET_SATURNIN: &[u8] = b"libQ-DET-SATURNIN-v1";
21
22/// Expand a 256-bit seed (and optional domain) into an arbitrary-length byte stream via KT128.
23#[derive(Clone, Debug)]
24pub struct Kt128Expander {
25    domain: &'static [u8],
26    buffer: [u8; 32],
27    position: usize,
28    counter: u64,
29}
30
31impl Kt128Expander {
32    /// Create an expander from a 32-byte seed and domain label.
33    #[must_use]
34    pub fn from_seed_32(domain: &'static [u8], seed: [u8; 32]) -> Self {
35        Self::from_seed(domain, &seed)
36    }
37
38    /// Create an expander from variable-length seed material and a domain label.
39    #[must_use]
40    pub fn from_seed(domain: &'static [u8], seed: &[u8]) -> Self {
41        let mut k12 = Kt128::new(domain);
42        k12.update(seed);
43        let mut reader = k12.finalize_xof();
44        let mut buffer = [0u8; 32];
45        reader.read(&mut buffer);
46        Self {
47            domain,
48            buffer,
49            position: 0,
50            counter: 0,
51        }
52    }
53
54    /// Create an expander using [`DOMAIN_LIBQ_DET_RNG`] and a 32-byte seed.
55    #[must_use]
56    pub fn from_det_seed_32(seed: [u8; 32]) -> Self {
57        Self::from_seed_32(DOMAIN_LIBQ_DET_RNG, seed)
58    }
59
60    /// Create an expander using [`DOMAIN_LIBQ_DET_RNG`] and SplitMix64-expanded `u64` seed material.
61    #[must_use]
62    pub fn from_det_u64(seed: u64) -> Self {
63        Self::from_seed_32(DOMAIN_LIBQ_DET_RNG, seed_32_from_u64(seed))
64    }
65
66    /// Refill the internal 32-byte buffer from the chained KT128 XOF step.
67    pub fn refill(&mut self) {
68        let mut k12 = Kt128::new(self.domain);
69        k12.update(&self.buffer);
70        k12.update(&self.counter.to_le_bytes());
71        let mut reader = k12.finalize_xof();
72        reader.read(&mut self.buffer);
73        self.counter = self.counter.wrapping_add(1);
74        self.position = 0;
75    }
76
77    /// Fill `dest` with deterministic pseudorandom bytes.
78    pub fn fill_bytes(&mut self, dest: &mut [u8]) {
79        let mut remaining = dest.len();
80        let mut offset = 0;
81
82        while remaining > 0 {
83            if self.position >= self.buffer.len() {
84                self.refill();
85            }
86
87            let available = self.buffer.len() - self.position;
88            let to_copy = core::cmp::min(remaining, available);
89
90            dest[offset..offset + to_copy]
91                .copy_from_slice(&self.buffer[self.position..self.position + to_copy]);
92
93            self.position += to_copy;
94            offset += to_copy;
95            remaining -= to_copy;
96        }
97    }
98}
99
100/// SplitMix64-style expansion of a `u64` test seed into 32 bytes (four mixing rounds).
101#[must_use]
102pub fn seed_32_from_u64(seed: u64) -> [u8; 32] {
103    let mut out = [0u8; 32];
104    let mut s = seed;
105    for chunk in out.chunks_mut(8) {
106        s = splitmix64_step(s);
107        chunk.copy_from_slice(&s.to_le_bytes());
108    }
109    out
110}
111
112#[inline]
113fn splitmix64_step(mut s: u64) -> u64 {
114    s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
115    s = (s ^ (s >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
116    s = (s ^ (s >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
117    let out = s ^ (s >> 31);
118    if out == 0 { 1 } else { out }
119}
120
121/// `DOMAIN_LIBQ_DET_RNG` + `[0u8; 32]` → first 64 output bytes.
122pub const KT128_DET_GOLDEN_ZERO_SEED_64: [u8; 64] = [
123    221, 250, 174, 112, 192, 10, 162, 130, 180, 58, 67, 124, 118, 240, 140, 65, 32, 215, 8, 34,
124    140, 63, 13, 205, 241, 229, 59, 9, 57, 190, 20, 124, 197, 138, 246, 213, 80, 155, 64, 77, 70,
125    54, 191, 17, 7, 229, 73, 226, 157, 172, 235, 183, 104, 145, 73, 150, 229, 58, 50, 22, 40, 119,
126    178, 69,
127];
128
129/// `from_det_u64(0x0123_4567_89ab_cdef)` → first 64 output bytes.
130pub const KT128_DET_GOLDEN_U64_SEED_64: [u8; 64] = [
131    252, 181, 230, 112, 248, 141, 49, 132, 104, 217, 21, 202, 22, 213, 11, 151, 255, 181, 150, 56,
132    230, 170, 210, 70, 45, 58, 246, 36, 221, 142, 143, 69, 198, 102, 112, 157, 221, 138, 218, 8,
133    136, 45, 198, 171, 31, 205, 147, 64, 120, 114, 35, 21, 207, 61, 174, 238, 179, 102, 189, 172,
134    16, 254, 132, 2,
135];
136
137// Golden vectors are generated in `tests/data/kt128_det_rng_v1.json` and asserted in integration tests.
138// Unit tests compare live output against values captured from this implementation (see `gen_kt128_goldens` test).
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_domain_separation_same_seed() {
146        let seed = [7u8; 32];
147        let mut det = Kt128Expander::from_seed_32(DOMAIN_LIBQ_DET_RNG, seed);
148        let mut hpke = Kt128Expander::from_seed_32(DOMAIN_HPKE_RNG, seed);
149        let mut a = [0u8; 32];
150        let mut b = [0u8; 32];
151        det.fill_bytes(&mut a);
152        hpke.fill_bytes(&mut b);
153        assert_ne!(a, b);
154    }
155
156    #[test]
157    fn test_u64_differs_from_raw_32_byte_seed() {
158        let seed_u64 = 0x0123_4567_89AB_CDEF_u64;
159        let mut from_u64 = Kt128Expander::from_det_u64(seed_u64);
160        let mut from_bytes = Kt128Expander::from_det_seed_32(seed_32_from_u64(seed_u64));
161        let mut a = [0u8; 64];
162        let mut b = [0u8; 64];
163        from_u64.fill_bytes(&mut a);
164        from_bytes.fill_bytes(&mut b);
165        assert_eq!(a, b);
166        let mut wrong = Kt128Expander::from_det_seed_32({
167            let mut s = [0u8; 32];
168            s[..8].copy_from_slice(&seed_u64.to_le_bytes());
169            s
170        });
171        let mut c = [0u8; 64];
172        wrong.fill_bytes(&mut c);
173        assert_ne!(a, c);
174    }
175
176    #[test]
177    fn test_deterministic_repeatability() {
178        let seed = [1u8; 32];
179        let mut e1 = Kt128Expander::from_det_seed_32(seed);
180        let mut e2 = Kt128Expander::from_det_seed_32(seed);
181        let mut out1 = [0u8; 128];
182        let mut out2 = [0u8; 128];
183        e1.fill_bytes(&mut out1);
184        e2.fill_bytes(&mut out2);
185        assert_eq!(out1, out2);
186    }
187
188    /// Golden bytes for `DOMAIN_LIBQ_DET_RNG` + zero seed (first 64 output bytes).
189    #[test]
190    fn test_golden_zero_seed_64_bytes() {
191        let mut expander = Kt128Expander::from_det_seed_32([0u8; 32]);
192        let mut out = [0u8; 64];
193        expander.fill_bytes(&mut out);
194        assert_eq!(out, KT128_DET_GOLDEN_ZERO_SEED_64);
195    }
196
197    /// Golden bytes for `from_det_u64(0x0123_4567_89ab_cdef)` (first 64 output bytes).
198    #[test]
199    fn test_golden_u64_seed_64_bytes() {
200        let mut expander = Kt128Expander::from_det_u64(0x0123_4567_89AB_CDEF);
201        let mut out = [0u8; 64];
202        expander.fill_bytes(&mut out);
203        assert_eq!(out, KT128_DET_GOLDEN_U64_SEED_64);
204    }
205
206    /// One-shot helper to print committed goldens (run with `--ignored --nocapture` after changing expansion).
207    #[test]
208    #[ignore = "manual: cargo test gen_kt128_goldens -- --ignored --nocapture -p lib-q-random"]
209    fn gen_kt128_goldens() {
210        let mut z = Kt128Expander::from_det_seed_32([0u8; 32]);
211        let mut zu = [0u8; 64];
212        z.fill_bytes(&mut zu);
213        let mut u = Kt128Expander::from_det_u64(0x0123_4567_89AB_CDEF);
214        let mut uu = [0u8; 64];
215        u.fill_bytes(&mut uu);
216        println!("zero_seed_64 = {zu:?}");
217        println!("u64_seed_64 = {uu:?}");
218    }
219}