phantom_protocol/crypto/rng.rs
1//! `RngProvider` — the indirection through which Phantom Protocol obtains
2//! cryptographic randomness. Default is [`OsRng`], which delegates to
3//! [`getrandom::getrandom`] and therefore picks up the platform's CSPRNG on
4//! every supported target (Linux `getrandom(2)`, macOS / iOS
5//! `CCRandomGenerateBytes`, Windows `BCryptGenRandom`, wasm32 via the `js`
6//! feature → `crypto.getRandomValues`, etc.).
7//!
8//! Embedders can swap in their own provider by implementing this trait and
9//! threading it into the relevant `_with_provider` entry points (the
10//! [`HybridSigningKey::generate_with_provider`] demonstration is wired up
11//! in this commit; the rest of the crate continues to call the
12//! `OsRng`-using default until a follow-up sweep lifts the abstraction
13//! through every call site).
14//!
15//! [`HybridSigningKey::generate_with_provider`]: crate::crypto::hybrid_sign::HybridSigningKey::generate_with_provider
16//!
17//! ## Phase 3.8 scope (this commit)
18//!
19//! Trait + default [`OsRng`] impl + tests. **No** new crate dependencies
20//! (this module uses only what already ships in `Cargo.toml`).
21//!
22//! What is intentionally NOT in scope here:
23//!
24//! - Refactoring every existing `thread_rng()` / `OsRng` call site to
25//! thread an `Arc<dyn RngProvider>` through the codebase. That sweep is
26//! a follow-up.
27//! - A real NIST SP 800-90A DRBG (e.g., HMAC-DRBG). The trait is shaped to
28//! accept one, but the impl itself is Phase 5 (FIPS) work.
29//! - A hardware-RNG impl. Those are inherently target-specific and belong
30//! in a downstream HAL adapter crate, not in `phantom_protocol` itself.
31//!
32//! ## Slotting in alternative providers
33//!
34//! ### Hardware TRNG on embedded
35//!
36//! On microcontrollers exposing a true-RNG peripheral (e.g., the STM32
37//! `RNG`, the nRF52 `RNG`, RP2040 ROSC, …), the HAL crate typically
38//! exposes a blocking reader (`embedded_hal::blocking::rng::Read` or the
39//! `rand_core::RngCore` impl that newer HALs wrap it in). A downstream
40//! adapter looks roughly like:
41//!
42//! ```ignore
43//! use phantom_protocol::crypto::rng::RngProvider;
44//! use core::sync::atomic::AtomicBool;
45//! use spin::Mutex; // or critical_section::Mutex on no_std-no-alloc
46//!
47//! pub struct HwRng<R> {
48//! inner: Mutex<R>,
49//! }
50//!
51//! impl<R> HwRng<R> {
52//! pub fn new(peripheral: R) -> Self {
53//! Self { inner: Mutex::new(peripheral) }
54//! }
55//! }
56//!
57//! impl<R> RngProvider for HwRng<R>
58//! where
59//! R: rand_core::RngCore + Send + 'static,
60//! {
61//! fn fill_bytes(&self, dest: &mut [u8]) {
62//! self.inner.lock().fill_bytes(dest);
63//! }
64//! }
65//! ```
66//!
67//! The `Mutex` is needed because `fill_bytes` takes `&self`. A real HAL
68//! adapter should also surface health-test failures from the peripheral
69//! (most TRNGs have a stuck-bit / continuous-test register) rather than
70//! returning silently-biased bytes.
71//!
72//! ### NIST-approved DRBG in FIPS mode
73//!
74//! Phase 5 will add an internal `HmacDrbg` (SP 800-90A § 10.1.2) keyed
75//! from `getrandom` at boot and re-seeded on a request / time interval
76//! per SP 800-90A § 9. The skeleton:
77//!
78//! ```ignore
79//! use phantom_protocol::crypto::rng::RngProvider;
80//! use std::sync::Mutex;
81//!
82//! pub struct HmacDrbg { /* V, Key, reseed_counter, ... */ }
83//! impl HmacDrbg {
84//! pub fn from_entropy() -> Self { /* seed from getrandom */ todo!() }
85//! fn generate(&mut self, out: &mut [u8]) { /* SP 800-90A 10.1.2.5 */ todo!() }
86//! }
87//!
88//! pub struct FipsDrbg(Mutex<HmacDrbg>);
89//! impl RngProvider for FipsDrbg {
90//! fn fill_bytes(&self, dest: &mut [u8]) {
91//! self.0.lock().expect("DRBG poisoned").generate(dest);
92//! }
93//! }
94//! ```
95//!
96//! See `docs/compliance/fips-readiness.md` for the larger picture.
97//!
98//! ### Deterministic test fixture
99//!
100//! See `tests::CounterRng` below for a tiny in-tree example.
101
102#[cfg(not(feature = "fips"))]
103use getrandom::getrandom;
104
105#[cfg(feature = "fips")]
106use aws_lc_rs::rand::{SecureRandom, SystemRandom};
107
108/// Source of cryptographically secure random bytes.
109///
110/// The trait takes `&self` (not `&mut self`) on every method so a single
111/// `Arc<dyn RngProvider>` can be shared across tasks / threads without
112/// callers having to wrap it in a `Mutex`. Implementations that internally
113/// need mutation (a software DRBG, a `ChaChaRng`-backed test fixture, …)
114/// must supply their own interior mutability — see the `CounterRng`
115/// example in the test module.
116///
117/// `Send + Sync + 'static` lets the provider be held in `Arc<dyn …>` for
118/// the lifetime of a long-running listener.
119///
120/// # Failure model
121///
122/// Implementations are expected to be **infallible** at the call boundary
123/// — randomness is required for crypto correctness, and there is no
124/// useful fallback at the Phantom Protocol layer. If the underlying source
125/// can fail (a hardware-RNG health-test trip, an OS RNG that returns
126/// `EIO`, …) the impl must surface that as a panic so the higher layer
127/// fails loudly rather than silently producing biased keys. The default
128/// [`OsRng`] follows this convention via `getrandom`'s
129/// `Result::expect`.
130pub trait RngProvider: Send + Sync + 'static {
131 /// Fill `dest` with cryptographically secure random bytes.
132 fn fill_bytes(&self, dest: &mut [u8]);
133
134 /// Convenience: return a single fresh `u64` of randomness.
135 ///
136 /// Default impl reads 8 bytes from [`fill_bytes`] and decodes them
137 /// little-endian. Implementations with a faster word-aligned path
138 /// (e.g., an HMAC-DRBG outputting 64-bit blocks) may override.
139 ///
140 /// [`fill_bytes`]: RngProvider::fill_bytes
141 fn next_u64(&self) -> u64 {
142 let mut buf = [0u8; 8];
143 self.fill_bytes(&mut buf);
144 u64::from_le_bytes(buf)
145 }
146}
147
148/// Default [`RngProvider`] — delegates to `getrandom` and therefore to the
149/// OS's CSPRNG on every supported target.
150///
151/// Zero-sized; cheap to construct. Hold a single instance per session (or
152/// wrap in `Arc<dyn RngProvider>` if you need to swap providers).
153#[derive(Debug, Default, Clone, Copy)]
154pub struct OsRng;
155
156impl OsRng {
157 /// Construct a fresh [`OsRng`]. Equivalent to `OsRng::default()` /
158 /// `OsRng`; the explicit constructor exists for symmetry with future
159 /// providers that need configuration.
160 pub const fn new() -> Self {
161 Self
162 }
163}
164
165#[cfg(not(feature = "fips"))]
166impl RngProvider for OsRng {
167 fn fill_bytes(&self, dest: &mut [u8]) {
168 // `getrandom` is configured to call the platform CSPRNG on every
169 // target the crate compiles on (the wasm32 `js` feature is enabled
170 // in `Cargo.toml`'s wasm-only block). A failure here means the OS
171 // RNG itself returned an error, which is unrecoverable at this
172 // layer — surface it loudly rather than silently producing zeros.
173 // PANIC-SAFETY: A failure from `getrandom` means the OS CSPRNG itself
174 // is broken or unavailable — a condition from which this crate cannot
175 // recover. Panicking loudly is preferable to silently producing zeros
176 // or propagating a partially-filled buffer that the caller would treat
177 // as good entropy.
178 #[allow(clippy::expect_used)]
179 getrandom(dest).expect("OS RNG (getrandom) failed");
180 }
181}
182
183/// `--features fips` impl: delegates to `aws_lc_rs::rand::SystemRandom`,
184/// which under AWS-LC-FIPS is a CTR_DRBG (NIST SP 800-90A § 10.2.1)
185/// seeded from the OS CSPRNG. This is the FIPS 140-3 approved RNG
186/// substrate that pairs with the rest of the primitive swap (AES-256-
187/// GCM, ECDH-P-256, HKDF-SHA256). The construction is wrapped in a
188/// fresh `SystemRandom` per call — the type is zero-sized and the
189/// underlying DRBG state lives inside AWS-LC's process-global module.
190#[cfg(feature = "fips")]
191impl RngProvider for OsRng {
192 fn fill_bytes(&self, dest: &mut [u8]) {
193 let rng = SystemRandom::new();
194 // PANIC-SAFETY: a fips `OsRng` failure means AWS-LC's CTR_DRBG
195 // itself returned an error (entropy source unavailable or the
196 // FIPS module is in a self-test-failed state) — a condition
197 // unrecoverable at this layer. Same loud-fail policy as the
198 // default `getrandom` impl above.
199 #[allow(clippy::expect_used)]
200 rng.fill(dest).expect("AWS-LC CTR_DRBG fill failed");
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use std::sync::Mutex;
208
209 /// Deterministic test-only RNG. Emits a counter encoded little-endian,
210 /// reseeded by an arbitrary `[u8; 32]` "seed" that XORs each output
211 /// word. Not cryptographically secure — exists solely to demonstrate
212 /// that an interior-mutability-based provider plugs into
213 /// [`RngProvider`].
214 struct CounterRng {
215 state: Mutex<(u64, [u8; 32])>,
216 }
217
218 impl CounterRng {
219 fn from_seed(seed: [u8; 32]) -> Self {
220 Self {
221 state: Mutex::new((0, seed)),
222 }
223 }
224 }
225
226 impl RngProvider for CounterRng {
227 fn fill_bytes(&self, dest: &mut [u8]) {
228 let mut guard = self.state.lock().expect("CounterRng mutex poisoned");
229 let (counter, seed) = &mut *guard;
230 for chunk in dest.chunks_mut(8) {
231 let word = *counter;
232 *counter = counter.wrapping_add(1);
233 let bytes = word.to_le_bytes();
234 let seed_off = ((word as usize) & 0x3) * 8;
235 for (i, b) in chunk.iter_mut().enumerate() {
236 *b = bytes[i] ^ seed[(seed_off + i) % 32];
237 }
238 }
239 }
240 }
241
242 #[test]
243 fn os_rng_fills_with_non_zero_bytes() {
244 let rng = OsRng::new();
245 let mut buf = [0u8; 32];
246 rng.fill_bytes(&mut buf);
247 // 32 zero bytes from a CSPRNG is astronomically unlikely (2^-256).
248 assert!(buf.iter().any(|&b| b != 0), "OsRng returned all-zero block");
249 }
250
251 #[test]
252 fn os_rng_two_calls_differ() {
253 let rng = OsRng;
254 let mut a = [0u8; 32];
255 let mut b = [0u8; 32];
256 rng.fill_bytes(&mut a);
257 rng.fill_bytes(&mut b);
258 assert_ne!(a, b, "Two CSPRNG draws collided");
259 }
260
261 #[test]
262 fn os_rng_next_u64_varies() {
263 let rng = OsRng;
264 let x = rng.next_u64();
265 let y = rng.next_u64();
266 // 2^-64 collision probability — acceptable for a smoke test.
267 assert_ne!(x, y, "next_u64 returned the same value twice");
268 }
269
270 #[test]
271 fn deterministic_test_provider() {
272 let seed = [0x5Au8; 32];
273 let rng_a = CounterRng::from_seed(seed);
274 let rng_b = CounterRng::from_seed(seed);
275
276 let mut a = [0u8; 64];
277 let mut b = [0u8; 64];
278 rng_a.fill_bytes(&mut a);
279 rng_b.fill_bytes(&mut b);
280
281 assert_eq!(a, b, "Same seed must produce identical streams");
282
283 // And a different seed produces a different stream.
284 let rng_c = CounterRng::from_seed([0xA5u8; 32]);
285 let mut c = [0u8; 64];
286 rng_c.fill_bytes(&mut c);
287 assert_ne!(a, c, "Different seeds must produce different streams");
288 }
289
290 #[test]
291 fn object_safety() {
292 // Compile-time proof that `RngProvider` is dyn-compatible — i.e.,
293 // a downstream embedder can erase the concrete type behind
294 // `Arc<dyn RngProvider>`.
295 fn assert_obj_safe(_: &dyn RngProvider) {}
296 let rng = OsRng;
297 assert_obj_safe(&rng);
298
299 // And the `Arc<dyn …>` shape compiles too.
300 use std::sync::Arc;
301 let _shared: Arc<dyn RngProvider> = Arc::new(OsRng::new());
302 }
303
304 /// fips-only: pull a 64-byte block and confirm it is neither
305 /// all-zero nor a constant byte pattern. Smokes the AWS-LC
306 /// CTR_DRBG → `OsRng` wiring.
307 #[cfg(feature = "fips")]
308 #[test]
309 fn fips_os_rng_64_byte_block_has_entropy() {
310 let rng = OsRng::new();
311 let mut buf = [0u8; 64];
312 rng.fill_bytes(&mut buf);
313 // All-zero from a CSPRNG is astronomically unlikely (2^-512).
314 assert!(buf.iter().any(|&b| b != 0), "fips OsRng returned all-zero");
315 // Reject any single-byte fill pattern (all bytes identical).
316 let first = buf[0];
317 assert!(
318 buf.iter().any(|&b| b != first),
319 "fips OsRng returned a constant byte pattern ({:#x} repeated)",
320 first
321 );
322 // Two consecutive draws must differ.
323 let mut buf2 = [0u8; 64];
324 rng.fill_bytes(&mut buf2);
325 assert_ne!(buf, buf2, "fips OsRng repeated a 64-byte block");
326 }
327}