feat(seed): audit kernel CSPRNG readiness + RNG non-determinism regression test
Seed entropy comes from bip39 -> rand::thread_rng -> getrandom(2), which blocks until the kernel pool is initialized -- but that ordering was invisible in logs on first-boot ISO flows where the seed is generated early. MasterSeed::generate() now probes getrandom(GRND_NONBLOCK) and logs whether the pool was already seeded (warn if it would block). Also adds a regression test that 64 generated mnemonics are all unique with sane word diversity, guarding against a fixed/seeded RNG ever being wired into seed generation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
35f6e75e83
commit
08ee5ed036
@ -49,9 +49,46 @@ pub struct MasterSeed {
|
||||
bytes: [u8; SEED_LEN],
|
||||
}
|
||||
|
||||
/// Probe whether the kernel CSPRNG is fully initialized, without blocking.
|
||||
///
|
||||
/// `getrandom(2)` already blocks until the entropy pool is initialized, so a
|
||||
/// seed can never be drawn from an unseeded pool. This probe exists to make
|
||||
/// that ordering auditable in the logs: first-boot ISO flows generate the seed
|
||||
/// early, when a slow-to-seed pool would otherwise be invisible.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn kernel_csprng_ready() -> Option<bool> {
|
||||
let mut byte = [0u8; 1];
|
||||
let ret = unsafe {
|
||||
libc::getrandom(
|
||||
byte.as_mut_ptr() as *mut libc::c_void,
|
||||
byte.len(),
|
||||
libc::GRND_NONBLOCK,
|
||||
)
|
||||
};
|
||||
if ret == 1 {
|
||||
Some(true)
|
||||
} else if std::io::Error::last_os_error().raw_os_error() == Some(libc::EAGAIN) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn kernel_csprng_ready() -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
impl MasterSeed {
|
||||
/// Generate a new 24-word BIP-39 mnemonic and derive the master seed.
|
||||
pub fn generate() -> Result<(bip39::Mnemonic, Self)> {
|
||||
match kernel_csprng_ready() {
|
||||
Some(true) => tracing::info!("kernel CSPRNG initialized; generating master seed"),
|
||||
Some(false) => tracing::warn!(
|
||||
"kernel CSPRNG not yet initialized; getrandom() will block until the pool is seeded"
|
||||
),
|
||||
None => {}
|
||||
}
|
||||
let mnemonic = bip39::Mnemonic::generate(24)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to generate mnemonic: {}", e))?;
|
||||
let seed = Self::from_mnemonic(&mnemonic);
|
||||
@ -557,6 +594,36 @@ mod tests {
|
||||
assert_eq!(mnemonic.word_count(), 24);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_nondeterministic() {
|
||||
// Regression guard against a fixed/seeded RNG ever being wired into
|
||||
// seed generation: with real entropy, collisions are impossible.
|
||||
let mnemonics: Vec<String> = (0..64)
|
||||
.map(|_| MasterSeed::generate().unwrap().0.to_string())
|
||||
.collect();
|
||||
let unique: std::collections::HashSet<&String> = mnemonics.iter().collect();
|
||||
assert_eq!(unique.len(), mnemonics.len(), "duplicate mnemonics generated");
|
||||
|
||||
// 64 × 24 = 1536 draws from the 2048-word list should hit ~1080
|
||||
// distinct words; a low-entropy source concentrates on far fewer.
|
||||
let distinct: std::collections::HashSet<&str> = mnemonics
|
||||
.iter()
|
||||
.flat_map(|m| m.split_whitespace())
|
||||
.collect();
|
||||
assert!(
|
||||
distinct.len() > 384,
|
||||
"only {} distinct words across 64 mnemonics",
|
||||
distinct.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_kernel_csprng_ready_on_test_host() {
|
||||
// By the time tests run, the pool has long been initialized.
|
||||
assert_eq!(kernel_csprng_ready(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_mnemonic_rejected() {
|
||||
let result = MasterSeed::from_mnemonic_words("not a valid mnemonic");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user