feat(10-06): name every entropy source and guard key draws (KEY-05 a/d)
Closes F-10a. Nothing here fixes a present defect: on the pinned rand 0.8.5, rand::random() and thread_rng() both resolve to a ChaCha12 CSPRNG seeded from getrandom(2). What they lack is a STATED backend — it is fixed by dependency and build configuration rather than by the calling code, with no compile error if that changes. That is the structural shape behind the 2026-07-30 COLDCARD entropy defect, and here the blast radius includes Cashu blinded-key-exchange values, X3DH prekey material, session bearer tokens and a ChaCha20-Poly1305 nonce. Layer (a) — every production key, nonce and token draw now names rand::rngs::OsRng at its own call site. The mnemonic seam is bound to entropy::KeyGenRng, a SEALED allowlist whose supertrait lives in a private module, so the set of RNGs that can drive the master key hierarchy is exactly what one file says it is. This retires the false promise at seed.rs:656: rand::CryptoRng is a marker with no compiler-checked content, and the crate now contains zero impls of it. Layer (d) — key material and AEAD nonces of >=12 bytes run a degenerate-entropy predicate that refuses all-zero, all-identical and wrapping +/-1 counter draws. Nothing heuristic: no entropy estimator, no chi-squared. Each of the three shapes has a false-positive probability computable in closed form (3 * 2^-88 at 12 bytes, 3 * 2^-248 at 32), and a predicate whose false-positive rate cannot be computed cannot be argued safe on a key-generation path. There is deliberately no retry — a retry would paper over the broken RNG this exists to surface. Layer (e) — the kernel-CSPRNG readiness verdict at master-seed generation is now durable (backlog R-09). It was previously computed, logged and thrown away, so a node could never answer after the fact whether its keys were born from a seeded pool. The record holds a schema version, timestamp, verdict and event name — no entropy, no key bytes. Formats and wire shapes are proven unchanged rather than asserted: storage_crypto and the credential store each open a HARDCODED pre-migration ciphertext vector (a same-process round trip would pass even if the envelope had changed), the vector was produced by an independent RFC 8439 implementation so it pins the documented nonce||ciphertext format rather than this implementation's output, and the x3dh prekey bundle and bdhke values keep their field set and order. totp.rs migrates its SOURCE only: the % charset.len() reduction and the 32-char charset are untouched. The bias there is presently zero (32 divides 256) and fixing the latent bias is R-12, which stays deferred. Verified: cargo build clean; cargo test -p archipelago 1068 passed, 2 failed. Both failures are container::boot_reconciler timing tests (second_pass_fires_after_interval, shutdown_terminates_loop) in a file this change does not touch — pre-existing, not caused here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a05956c4ce
commit
09a1f7621c
@@ -98,9 +98,28 @@ fn readable_nonempty(path: &Path) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Fill `buf` from an explicitly named `OsRng`, guarded when it is long enough
|
||||
/// for the degenerate predicate's false-positive bound to hold.
|
||||
///
|
||||
/// KEY-05 / F-10: these are the manifest-declared `generated_secrets` — app
|
||||
/// passwords and API keys — and were the original F-10 finding. Every production
|
||||
/// caller requests 16 or 32 bytes, so the guard is live in practice; the short
|
||||
/// branch exists so a future caller asking for fewer cannot trip the guard's
|
||||
/// length assertion, which is a programmer-error panic and not an input
|
||||
/// condition.
|
||||
fn fill_secret_bytes(buf: &mut [u8]) {
|
||||
if buf.len() >= crate::entropy::MIN_GUARDED_LEN {
|
||||
crate::entropy::draw_key_bytes(&mut rand::rngs::OsRng, buf).unwrap_or_else(|e| {
|
||||
panic!("refusing to generate an app secret from degenerate entropy: {e} (KEY-05)")
|
||||
});
|
||||
} else {
|
||||
rand::rngs::OsRng.fill_bytes(buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn random_hex(bytes: usize) -> String {
|
||||
let mut buf = vec![0u8; bytes];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
fill_secret_bytes(&mut buf);
|
||||
hex::encode(buf)
|
||||
}
|
||||
|
||||
@@ -109,7 +128,7 @@ fn random_hex(bytes: usize) -> String {
|
||||
fn random_base64(bytes: usize) -> String {
|
||||
use base64::Engine as _;
|
||||
let mut buf = vec![0u8; bytes];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
fill_secret_bytes(&mut buf);
|
||||
base64::engine::general_purpose::STANDARD.encode(buf)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user