Skip to main content

crypto_core/
entropy.rs

1//! Entropy health checks for runtime fail-closed controls.
2
3use crate::{CryptoError, Result};
4
5const ENTROPY_SAMPLE_SIZE: usize = 32;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct EntropyHealthReport {
9    pub source: &'static str,
10    pub sample_size: usize,
11}
12
13/// Perform a lightweight runtime entropy self-test using OS randomness.
14///
15/// The check intentionally avoids blocking diagnostics and only verifies that:
16/// - OS RNG is reachable,
17/// - samples are not trivially degenerate.
18pub fn entropy_health_check() -> Result<EntropyHealthReport> {
19    let mut sample_a = [0u8; ENTROPY_SAMPLE_SIZE];
20    let mut sample_b = [0u8; ENTROPY_SAMPLE_SIZE];
21
22    getrandom::getrandom(&mut sample_a)
23        .map_err(|e| CryptoError::KeyError(format!("OS entropy unavailable: {e}")))?;
24    getrandom::getrandom(&mut sample_b)
25        .map_err(|e| CryptoError::KeyError(format!("OS entropy unavailable: {e}")))?;
26
27    if sample_a.iter().all(|b| *b == 0) && sample_b.iter().all(|b| *b == 0) {
28        return Err(CryptoError::KeyError(
29            "Entropy self-test failed: degenerate all-zero samples".to_string(),
30        ));
31    }
32
33    if sample_a == sample_b {
34        return Err(CryptoError::KeyError(
35            "Entropy self-test failed: identical consecutive RNG samples".to_string(),
36        ));
37    }
38
39    Ok(EntropyHealthReport {
40        source: "os_rng",
41        sample_size: ENTROPY_SAMPLE_SIZE,
42    })
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn test_entropy_health_check_reports_ok() {
51        let report = entropy_health_check().expect("os entropy health check");
52        assert_eq!(report.source, "os_rng");
53        assert_eq!(report.sample_size, ENTROPY_SAMPLE_SIZE);
54    }
55}