Skip to main content

hardware_rust_crypto/random/
mod.rs

1//! Fast random byte generation for hot key/nonce paths.
2//!
3//! This crate ships only the hardware-only AES-256-CTR generator (derived
4//! from the `rand_aes` AES-256-CTR-128 backend with software fallback state
5//! removed): the dependency graph contains no software cipher, and OS entropy
6//! for seeding comes from `getrandom` directly. Software stream ciphers used
7//! for benchmark comparison live in the workspace's dev-dependencies only.
8//!
9//! # Constant-time notes
10//!
11//! Generation never branches on, or indexes memory by, generated secret
12//! bytes: 32-byte keys and 12-byte nonces are direct fixed-size draws with
13//! no rejection sampling, and buffer accounting depends only on requested
14//! lengths and cursor positions. The AES-CTR backend is constant time by
15//! construction: it runs on hardware AES instructions with no table lookups
16//! or data-dependent branches (see the `aes_ctr` module docs). Lifecycle
17//! branches (reseed interval, fork detection) read public counters and
18//! process state only.
19
20#![allow(unsafe_code)]
21
22mod aes_ctr;
23mod entropy;
24mod fork;
25
26pub use entropy::cpu_rng_available;
27
28use zeroize::{Zeroize as _, Zeroizing};
29
30/// AES-256 key size.
31pub const KEY_SIZE: usize = 32;
32/// AES-GCM nonce size.
33pub const NONCE_SIZE: usize = 12;
34/// AES-CTR generator seed size: 32-byte AES-256 key plus 16-byte counter.
35pub const AES_CTR_SEED_SIZE: usize = 48;
36
37const AES_BLOCK_SIZE: usize = 16;
38const DEFAULT_RESEED_INTERVAL_BYTES: u64 = 1 << 30;
39
40/// Random generation errors.
41#[derive(Debug)]
42#[non_exhaustive]
43pub enum Error {
44    /// OS entropy failed while seeding the fast CSPRNG.
45    OsEntropy(getrandom::Error),
46    /// Required AES hardware support is unavailable.
47    UnsupportedCpu {
48        /// Required CPU features for the current target.
49        required: &'static str,
50    },
51    /// Generator state was inherited across a process fork.
52    ForkDetected,
53}
54
55impl std::fmt::Display for Error {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            Self::OsEntropy(err) => write!(f, "OS entropy failed: {err}"),
59            Self::UnsupportedCpu { required } => {
60                write!(
61                    f,
62                    "required AES-CTR hardware support is unavailable: {required}"
63                )
64            }
65            Self::ForkDetected => f.write_str("random generator state crossed a process fork"),
66        }
67    }
68}
69
70impl std::error::Error for Error {}
71
72/// Snapshot used to detect generator state crossing a process fork.
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74enum ForkGuard {
75    /// Fork-generation snapshot maintained by a `pthread_atfork` child
76    /// handler; checking is an atomic load and survives pid reuse.
77    Generation(u64),
78    /// Fallback when the atfork handler is unavailable: process-id snapshot
79    /// compared via `getpid` per check.
80    ProcessId(u32),
81}
82
83impl ForkGuard {
84    fn capture() -> Self {
85        fork::generation().map_or_else(|| Self::ProcessId(current_process_id()), Self::Generation)
86    }
87
88    fn check(self) -> Result<(), Error> {
89        let unchanged = match self {
90            Self::Generation(seen) => fork::generation() == Some(seen),
91            Self::ProcessId(seen) => current_process_id() == seen,
92        };
93        if unchanged {
94            Ok(())
95        } else {
96            Err(Error::ForkDetected)
97        }
98    }
99
100    #[cfg(test)]
101    fn corrupted(self) -> Self {
102        match self {
103            Self::Generation(seen) => Self::Generation(seen.wrapping_add(1)),
104            Self::ProcessId(seen) => Self::ProcessId(seen.wrapping_add(1)),
105        }
106    }
107}
108
109/// Common key-generation contract for benchmarked CSPRNG backends.
110pub trait KeyGenerator {
111    /// Fills `out` with CSPRNG output.
112    ///
113    /// # Errors
114    ///
115    /// Returns entropy, hardware, or fork-detection failures.
116    fn fill_bytes(&mut self, out: &mut [u8]) -> Result<(), Error>;
117
118    /// Generates a 32-byte AES-256 key.
119    ///
120    /// # Errors
121    ///
122    /// Returns the same errors as [`Self::fill_bytes`].
123    fn key_32(&mut self) -> Result<[u8; KEY_SIZE], Error> {
124        let mut out = [0_u8; KEY_SIZE];
125        self.fill_bytes(&mut out)?;
126        Ok(out)
127    }
128
129    /// Generates a 12-byte AES-GCM nonce.
130    ///
131    /// # Errors
132    ///
133    /// Returns the same errors as [`Self::fill_bytes`].
134    fn nonce_12(&mut self) -> Result<[u8; NONCE_SIZE], Error> {
135        let mut out = [0_u8; NONCE_SIZE];
136        self.fill_bytes(&mut out)?;
137        Ok(out)
138    }
139}
140
141/// Fills `out` from the operating-system entropy source.
142fn fill_from_os(out: &mut [u8]) -> Result<(), Error> {
143    getrandom::fill(out).map_err(Error::OsEntropy)
144}
145
146/// Size and alignment for a reusable generator state.
147#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148pub struct StateLayout {
149    /// State size in bytes.
150    pub size: usize,
151    /// State alignment in bytes.
152    pub align: usize,
153}
154
155/// Hardware-only AES-256-CTR fast random generator.
156///
157/// The state contains a 128-bit counter, AES-256 encryption round keys, and a
158/// small buffered-output block. It does not contain a software AES fallback or
159/// a runtime enum that can hold one.
160pub struct AesCtrKeyGenerator {
161    backend: aes_ctr::Backend,
162    buffer: [u8; AES_BLOCK_SIZE],
163    buffer_pos: usize,
164    generated_since_reseed: u64,
165    reseed_interval: u64,
166    fork_guard: ForkGuard,
167}
168
169impl AesCtrKeyGenerator {
170    /// Returns whether this process currently exposes the required AES hardware
171    /// functionality for the AES-CTR backend.
172    #[must_use]
173    pub fn hardware_available() -> bool {
174        aes_ctr::hardware_available()
175    }
176
177    /// Returns the state layout for this owned generator type.
178    #[must_use]
179    pub const fn state_layout() -> StateLayout {
180        StateLayout {
181            size: core::mem::size_of::<Self>(),
182            align: core::mem::align_of::<Self>(),
183        }
184    }
185
186    /// Seeds a new AES-CTR generator from OS entropy.
187    ///
188    /// # Errors
189    ///
190    /// Returns [`Error::UnsupportedCpu`] if the required AES hardware support is
191    /// unavailable. Returns [`Error::OsEntropy`] if the platform entropy source
192    /// cannot provide a seed.
193    pub fn from_os_entropy() -> Result<Self, Error> {
194        if !Self::hardware_available() {
195            return Err(Error::UnsupportedCpu {
196                required: aes_ctr::REQUIRED_FEATURES,
197            });
198        }
199
200        let mut seed = Zeroizing::new([0_u8; AES_CTR_SEED_SIZE]);
201        fill_from_os(seed.as_mut())?;
202        Self::from_seed_bytes(&seed)
203    }
204
205    /// Seeds a new AES-CTR generator from explicit seed material and zeroizes
206    /// the provided seed before returning.
207    ///
208    /// `seed[..32]` is the AES-256 key and `seed[32..]` is the initial 128-bit
209    /// little-endian counter. This constructor exists for deterministic tests
210    /// and benchmarks; production callers should use [`Self::from_os_entropy`].
211    ///
212    /// # Errors
213    ///
214    /// Returns [`Error::UnsupportedCpu`] if the required AES hardware support is
215    /// unavailable.
216    pub fn from_seed(seed: &mut [u8; AES_CTR_SEED_SIZE]) -> Result<Self, Error> {
217        let result = Self::from_seed_bytes(seed);
218        seed.zeroize();
219        result
220    }
221
222    fn from_seed_bytes(seed: &[u8; AES_CTR_SEED_SIZE]) -> Result<Self, Error> {
223        if !Self::hardware_available() {
224            return Err(Error::UnsupportedCpu {
225                required: aes_ctr::REQUIRED_FEATURES,
226            });
227        }
228
229        let (key, counter) = split_seed(seed);
230        let backend = aes_ctr::Backend::new(key, counter).ok_or(Error::UnsupportedCpu {
231            required: aes_ctr::REQUIRED_FEATURES,
232        })?;
233
234        Ok(Self {
235            backend,
236            buffer: [0_u8; AES_BLOCK_SIZE],
237            buffer_pos: AES_BLOCK_SIZE,
238            generated_since_reseed: 0,
239            reseed_interval: DEFAULT_RESEED_INTERVAL_BYTES,
240            fork_guard: ForkGuard::capture(),
241        })
242    }
243
244    /// Generates a 32-byte AES-256 key.
245    ///
246    /// # Errors
247    ///
248    /// Returns entropy, hardware, or fork-detection failures.
249    pub fn key_32(&mut self) -> Result<[u8; KEY_SIZE], Error> {
250        KeyGenerator::key_32(self)
251    }
252
253    /// Generates a 12-byte AES-GCM nonce.
254    ///
255    /// # Errors
256    ///
257    /// Returns entropy, hardware, or fork-detection failures.
258    pub fn nonce_12(&mut self) -> Result<[u8; NONCE_SIZE], Error> {
259        KeyGenerator::nonce_12(self)
260    }
261
262    fn drain_buffer(&mut self, out: &mut [u8]) -> usize {
263        // `available`/`take` derive from cursor positions and the requested
264        // length, never from generated byte values, so this path leaks
265        // nothing about the output through timing.
266        let available = AES_BLOCK_SIZE - self.buffer_pos;
267        let take = available.min(out.len());
268        if take == 0 {
269            return 0;
270        }
271
272        let end = self.buffer_pos + take;
273        out[..take].copy_from_slice(&self.buffer[self.buffer_pos..end]);
274        self.buffer[self.buffer_pos..end].zeroize();
275        self.buffer_pos = end;
276        take
277    }
278
279    /// Reseeds the generator.
280    ///
281    /// If a CPU hardware RNG is available (and the `cpu-rng-reseed` feature is
282    /// enabled), fresh CPU entropy is blended through the current secret state
283    /// instead of querying the OS - see [`Self::reseed_blend`]. Otherwise the
284    /// generator is reset directly from the OS entropy source.
285    fn reseed(&mut self) -> Result<(), Error> {
286        let mut entropy_input = Zeroizing::new([0_u8; AES_CTR_SEED_SIZE]);
287        if entropy::cpu_rng_fill(entropy_input.as_mut()) {
288            self.reseed_blend(&entropy_input);
289        } else {
290            fill_from_os(entropy_input.as_mut())?;
291            self.reset_from_seed(&entropy_input)?;
292        }
293        self.buffer.zeroize();
294        self.buffer_pos = AES_BLOCK_SIZE;
295        self.generated_since_reseed = 0;
296        self.fork_guard = ForkGuard::capture();
297        Ok(())
298    }
299
300    /// Resets the backend from full-entropy seed material (OS path).
301    fn reset_from_seed(&mut self, seed: &[u8; AES_CTR_SEED_SIZE]) -> Result<(), Error> {
302        let (key, counter) = split_seed(seed);
303        self.backend = aes_ctr::Backend::new(key, counter).ok_or(Error::UnsupportedCpu {
304            required: aes_ctr::REQUIRED_FEATURES,
305        })?;
306        Ok(())
307    }
308
309    /// CTR_DRBG-style reseed update: derive the new 48-byte seed as
310    /// `AES-CTR keystream(current secret state) XOR cpu_entropy`, then rekey
311    /// the backend from it.
312    ///
313    /// The keystream block is secret (it depends on the current key, which is
314    /// rooted in the original OS seed), so a malicious CPU RNG that controls
315    /// `cpu_entropy` still cannot force the new seed to a known value unless
316    /// it *also* already knows the current state. Conversely, if the CPU RNG
317    /// is honest, the fresh entropy makes the new seed unpredictable even if
318    /// the prior state had leaked. The result is safe unless both the state
319    /// is compromised and the CPU RNG is malicious - strictly stronger than
320    /// trusting either source alone.
321    fn reseed_blend(&mut self, cpu_entropy: &[u8; AES_CTR_SEED_SIZE]) {
322        let mut seed = Zeroizing::new([0_u8; AES_CTR_SEED_SIZE]);
323        // 48 bytes == 3 AES blocks of keystream from the current state.
324        for block in seed.chunks_exact_mut(AES_BLOCK_SIZE) {
325            let mut keystream = Zeroizing::new([0_u8; AES_BLOCK_SIZE]);
326            self.backend.fill_block(&mut keystream);
327            block.copy_from_slice(keystream.as_ref());
328        }
329        for (seed_byte, entropy_byte) in seed.iter_mut().zip(cpu_entropy.iter()) {
330            *seed_byte ^= entropy_byte;
331        }
332        let (key, counter) = split_seed(&seed);
333        // first_chunk on a 48-byte array is infallible; on the impossible
334        // None we keep the existing backend rather than panic.
335        if let Some(backend) = aes_ctr::Backend::new(key, counter) {
336            self.backend = backend;
337        }
338    }
339
340    fn fill_checked(&mut self, mut out: &mut [u8]) -> Result<(), Error> {
341        self.fork_guard.check()?;
342        while !out.is_empty() {
343            // Clamp so a zero interval cannot stall the loop with zero-byte
344            // chunks while reseeding forever.
345            let reseed_interval = self.reseed_interval.max(1);
346            if self.generated_since_reseed >= reseed_interval {
347                self.reseed()?;
348            }
349
350            let remaining_before_reseed = reseed_interval - self.generated_since_reseed;
351            let take = out
352                .len()
353                .min(usize::try_from(remaining_before_reseed).unwrap_or(usize::MAX));
354            let (chunk, rest) = out.split_at_mut(take);
355            self.fill_without_lifecycle_checks(chunk);
356            self.generated_since_reseed += u64::try_from(chunk.len()).unwrap_or(u64::MAX);
357            out = rest;
358        }
359        Ok(())
360    }
361
362    fn fill_without_lifecycle_checks(&mut self, mut out: &mut [u8]) {
363        let copied = self.drain_buffer(out);
364        out = &mut out[copied..];
365
366        let mut chunks = out.chunks_exact_mut(AES_BLOCK_SIZE);
367        for chunk in &mut chunks {
368            let mut block = Zeroizing::new([0_u8; AES_BLOCK_SIZE]);
369            self.backend.fill_block(&mut block);
370            chunk.copy_from_slice(block.as_ref());
371        }
372
373        let remainder = chunks.into_remainder();
374        if !remainder.is_empty() {
375            self.backend.fill_block(&mut self.buffer);
376            remainder.copy_from_slice(&self.buffer[..remainder.len()]);
377            self.buffer[..remainder.len()].zeroize();
378            self.buffer_pos = remainder.len();
379        }
380    }
381}
382
383impl Drop for AesCtrKeyGenerator {
384    fn drop(&mut self) {
385        self.buffer.zeroize();
386        self.buffer_pos = AES_BLOCK_SIZE;
387        self.generated_since_reseed = 0;
388        self.reseed_interval = 0;
389    }
390}
391
392impl std::fmt::Debug for AesCtrKeyGenerator {
393    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394        f.debug_struct("AesCtrKeyGenerator").finish_non_exhaustive()
395    }
396}
397
398impl KeyGenerator for AesCtrKeyGenerator {
399    /// Fills `out` with AES-CTR CSPRNG output.
400    fn fill_bytes(&mut self, out: &mut [u8]) -> Result<(), Error> {
401        self.fill_checked(out)
402    }
403}
404
405fn current_process_id() -> u32 {
406    std::process::id()
407}
408
409/// Splits a seed into the AES-256 key and initial counter segments.
410#[allow(clippy::expect_used)] // Infallible: the const assertion pins the split.
411fn split_seed(seed: &[u8; AES_CTR_SEED_SIZE]) -> (&[u8; KEY_SIZE], &[u8; AES_BLOCK_SIZE]) {
412    const _: () = assert!(AES_CTR_SEED_SIZE == KEY_SIZE + AES_BLOCK_SIZE);
413    let (key, counter) = seed.split_at(KEY_SIZE);
414    (
415        key.try_into().expect("seed key segment is KEY_SIZE bytes"),
416        counter
417            .try_into()
418            .expect("seed counter segment is AES_BLOCK_SIZE bytes"),
419    )
420}
421
422#[cfg(test)]
423mod tests {
424    #![allow(clippy::expect_used)]
425
426    use super::{AesCtrKeyGenerator, KeyGenerator, AES_BLOCK_SIZE, AES_CTR_SEED_SIZE, KEY_SIZE};
427
428    #[cfg(unix)]
429    #[test]
430    fn detects_state_inherited_across_real_fork() {
431        if !AesCtrKeyGenerator::hardware_available() {
432            return;
433        }
434
435        let mut rng =
436            AesCtrKeyGenerator::from_os_entropy().expect("OS entropy should be available");
437        let mut out = [0_u8; 1];
438        rng.fill_bytes(&mut out)
439            .expect("parent generation should succeed");
440
441        // SAFETY: the forked child only performs fork-safe work (atomic loads,
442        // getpid, in-memory generation) and exits via _exit without running
443        // atexit handlers or touching inherited locks.
444        let pid = unsafe { libc::fork() };
445        assert!(pid >= 0, "fork should succeed");
446        if pid == 0 {
447            let code = match rng.fill_bytes(&mut out) {
448                Err(super::Error::ForkDetected) => 0,
449                Ok(()) | Err(_) => 1,
450            };
451            // SAFETY: _exit is the fork-safe process exit path.
452            unsafe { libc::_exit(code) };
453        }
454
455        let mut status = 0;
456        // SAFETY: pid is the child forked above and status is a valid out
457        // pointer.
458        let waited = unsafe { libc::waitpid(pid, &raw mut status, 0) };
459        assert_eq!(waited, pid, "waitpid should reap the forked child");
460        assert!(libc::WIFEXITED(status), "child should exit normally");
461        assert_eq!(
462            libc::WEXITSTATUS(status),
463            0,
464            "child must observe ForkDetected from inherited generator state"
465        );
466    }
467
468    #[test]
469    fn reseed_blend_is_deterministic_and_entropy_dependent() {
470        if !AesCtrKeyGenerator::hardware_available() {
471            return;
472        }
473
474        // Two generators seeded identically, blended with the same CPU
475        // entropy, must reach the same state (deterministic mixing); blending
476        // with different entropy must diverge (the entropy actually feeds in).
477        let entropy_a = [0x11_u8; AES_CTR_SEED_SIZE];
478        let entropy_b = [0x22_u8; AES_CTR_SEED_SIZE];
479
480        let mut seed = [0x5e_u8; AES_CTR_SEED_SIZE];
481        let mut rng_a = AesCtrKeyGenerator::from_seed(&mut seed.clone())
482            .expect("AES hardware should be available");
483        let mut rng_a2 = AesCtrKeyGenerator::from_seed(&mut seed.clone())
484            .expect("AES hardware should be available");
485        let mut rng_b =
486            AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
487
488        rng_a.reseed_blend(&entropy_a);
489        rng_a2.reseed_blend(&entropy_a);
490        rng_b.reseed_blend(&entropy_b);
491
492        let mut out_a = [0_u8; 32];
493        let mut out_a2 = [0_u8; 32];
494        let mut out_b = [0_u8; 32];
495        rng_a.fill_without_lifecycle_checks(&mut out_a);
496        rng_a2.fill_without_lifecycle_checks(&mut out_a2);
497        rng_b.fill_without_lifecycle_checks(&mut out_b);
498
499        assert_eq!(out_a, out_a2, "same seed + same entropy must blend equally");
500        assert_ne!(
501            out_a, out_b,
502            "different entropy must change the blended state"
503        );
504    }
505
506    #[test]
507    fn reseed_blend_changes_state() {
508        if !AesCtrKeyGenerator::hardware_available() {
509            return;
510        }
511
512        let mut seed = [0x33_u8; AES_CTR_SEED_SIZE];
513        let mut rng =
514            AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
515        let mut before = [0_u8; 32];
516        rng.fill_without_lifecycle_checks(&mut before);
517
518        rng.reseed_blend(&[0x44_u8; AES_CTR_SEED_SIZE]);
519        let mut after = [0_u8; 32];
520        rng.fill_without_lifecycle_checks(&mut after);
521        assert_ne!(before, after, "reseed must change subsequent output");
522    }
523
524    #[test]
525    fn cpu_rng_available_does_not_panic() {
526        // Result is platform-dependent (true on Graviton/x86 with RDSEED,
527        // false on Apple Silicon); we only assert it runs.
528        let _ = super::cpu_rng_available();
529    }
530
531    #[test]
532    fn aes_ctr_matches_aes256_ctr_known_answer() {
533        if !AesCtrKeyGenerator::hardware_available() {
534            return;
535        }
536
537        let mut seed = [0_u8; AES_CTR_SEED_SIZE];
538        seed[..KEY_SIZE].copy_from_slice(&[
539            0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
540            0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b,
541            0x1c, 0x1d, 0x1e, 0x1f,
542        ]);
543        seed[KEY_SIZE..].copy_from_slice(&[
544            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
545            0xee, 0xff,
546        ]);
547
548        let mut rng =
549            AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
550        let mut out = [0_u8; AES_BLOCK_SIZE * 2];
551        rng.fill_bytes(&mut out)
552            .expect("AES-CTR generation should succeed");
553
554        assert_eq!(
555            out,
556            [
557                0x8e, 0xa2, 0xb7, 0xca, 0x51, 0x67, 0x45, 0xbf, 0xea, 0xfc, 0x49, 0x90, 0x4b, 0x49,
558                0x60, 0x89, 0x81, 0xae, 0x7d, 0x5e, 0x41, 0x38, 0xbf, 0x73, 0x0d, 0x2a, 0x88, 0x71,
559                0xfe, 0xc2, 0xcd, 0x0c,
560            ]
561        );
562    }
563
564    #[test]
565    fn aes_ctr_fill_bytes_is_contiguous_across_partial_calls() {
566        if !AesCtrKeyGenerator::hardware_available() {
567            return;
568        }
569
570        let mut seed = [7_u8; AES_CTR_SEED_SIZE];
571        let mut seed_copy = seed;
572        let mut one_shot =
573            AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
574        let mut split = AesCtrKeyGenerator::from_seed(&mut seed_copy)
575            .expect("AES hardware should be available");
576
577        let mut expected = [0_u8; 64];
578        one_shot
579            .fill_bytes(&mut expected)
580            .expect("AES-CTR generation should succeed");
581
582        let mut actual = [0_u8; 64];
583        split
584            .fill_bytes(&mut actual[..3])
585            .expect("AES-CTR generation should succeed");
586        split
587            .fill_bytes(&mut actual[3..19])
588            .expect("AES-CTR generation should succeed");
589        split
590            .fill_bytes(&mut actual[19..31])
591            .expect("AES-CTR generation should succeed");
592        split
593            .fill_bytes(&mut actual[31..])
594            .expect("AES-CTR generation should succeed");
595
596        assert_eq!(actual, expected);
597    }
598
599    #[test]
600    fn aes_ctr_from_seed_zeroizes_seed_buffer() {
601        if !AesCtrKeyGenerator::hardware_available() {
602            return;
603        }
604
605        let mut seed = [0x5a_u8; AES_CTR_SEED_SIZE];
606        let _rng =
607            AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
608
609        assert_eq!(seed, [0_u8; AES_CTR_SEED_SIZE]);
610    }
611
612    #[test]
613    fn aes_ctr_reseeds_after_generation_interval() {
614        if !AesCtrKeyGenerator::hardware_available() {
615            return;
616        }
617
618        let mut seed = [3_u8; AES_CTR_SEED_SIZE];
619        let mut rng =
620            AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
621        rng.reseed_interval = 8;
622        let mut out = [0_u8; 32];
623        rng.fill_bytes(&mut out)
624            .expect("fill across reseed boundary should succeed");
625        assert!(rng.generated_since_reseed <= rng.reseed_interval);
626    }
627
628    #[test]
629    fn aes_ctr_rejects_forked_state() {
630        if !AesCtrKeyGenerator::hardware_available() {
631            return;
632        }
633
634        let mut seed = [3_u8; AES_CTR_SEED_SIZE];
635        let mut rng =
636            AesCtrKeyGenerator::from_seed(&mut seed).expect("AES hardware should be available");
637        rng.fork_guard = rng.fork_guard.corrupted();
638        let mut out = [0_u8; 1];
639        assert!(matches!(
640            rng.fill_bytes(&mut out),
641            Err(super::Error::ForkDetected)
642        ));
643    }
644
645    #[test]
646    fn aes_ctr_state_stays_within_initial_target_size() {
647        if !AesCtrKeyGenerator::hardware_available() {
648            return;
649        }
650
651        let layout = AesCtrKeyGenerator::state_layout();
652        assert!(
653            layout.size <= 320,
654            "AES-CTR state size {} exceeded 320-byte target",
655            layout.size
656        );
657        assert!(
658            layout.align >= 16,
659            "AES-CTR state alignment {} should hold AES block vectors",
660            layout.align
661        );
662    }
663}