Skip to main content

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}