Skip to content

keystore: fail closed on OS CSPRNG failure instead of panicking (#85) - #124

Merged
hellno merged 1 commit into
mainfrom
hellno/victoria
Jun 21, 2026
Merged

keystore: fail closed on OS CSPRNG failure instead of panicking (#85)#124
hellno merged 1 commit into
mainfrom
hellno/victoria

Conversation

@hellno

@hellno hellno commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Closes #85.

What

Every key-derived random fill in the trust core (deckard-core) went through the infallible RngCore::fill_bytes / SliceRandom::shuffle. Those panic if the OS CSPRNG is unavailable. They fail closed (a panic can never hand back a weak key), so this is a no-panic / fail-closed reliability fix, not a key-strength fix — but a panic in the create/seal path still tears down the process mid-onboarding with no clean error to show the user.

This routes every secure fill through a single fallible chokepoint and bans the panicking APIs in clippy so the invariant can't silently regress.

Changes

  • fill_secure() / fill_secure_with() — one entropy-acquisition chokepoint built on try_fill_bytes. The OS error is preserved as the error source (the buffer is output-only, carries no key material); the top-line message stays plain.
  • create / seal gain private _with_rng seams so a failing-RNG test can drive the new Err branch (real OsRng can't be made to fail on demand). Public signatures are unchanged and OsRng stays the only production entropy source — the seam is deliberately private so nothing can substitute a non-OsRng source into the trust core.
  • random_word_positions had the same latent panic (shufflefill_bytes). Reworked to Fisher–Yates over try_fill_bytes returning Result, with rejection sampling (bounded_index) so there is no modulo bias. The caller (shell.rs create flow) now surfaces the error via auth_error instead of relying on an infallible call.
  • clippy.tomlRngCore::fill_bytes and SliceRandom::shuffle are now disallowed-methods, enforced by just check (-D warnings) on both the default and tray configs.

Verification

  • cargo fmt --all --check — clean
  • just check — green (clippy -D warnings, default and --features tray); the two new clippy bans pass.
  • deckard-core tests — 58 passed, including the new create_fails_closed_when_entropy_unavailable, seal_fails_closed_when_entropy_unavailable, and random_word_positions_distinct_sorted_in_range, plus the existing create/seal/unlock round-trips that exercise the changed *_with_rng paths.
  • No dependency changes (anyhow::Context was already a dep).

⚠️ Local cargo test --workspace caveat: 5 deckard-signerd anvil_e2e tests failed locally because my dev disk is 100% full (~130 MiB free) — the harness panics writing temp vaults / spawning the daemon (no space left on device). They're unrelated to this diff (no signerd file is touched; the failures are filesystem-write expects in tests/common/mod.rs). CI runs them in a clean-disk environment — relying on CI as the source of truth for that suite.

Visual

No reachable visual change: the only new UI branch is auth_error shown when the OS CSPRNG fails, which can't be induced without a failing OS RNG. The happy-path backup-quiz screen renders identically. (App launch for screenshots was also blocked by the disk-full state above.)

Every key-derived random fill in the trust core went through the infallible
`RngCore::fill_bytes` / `SliceRandom::shuffle`, which *panic* if the OS CSPRNG
is unavailable. They fail closed (a panic can't yield a weak key), so this is a
no-panic / fail-closed reliability fix, not a key-strength fix — but a panic in
the create/seal path still aborts the process mid-onboarding with no clean error.

- fill_secure()/fill_secure_with(): one entropy-acquisition chokepoint built on
  the fallible `try_fill_bytes`; the OS error is preserved as the source (the
  buffer is output-only, carries no key material) and the top line stays plain.
- create/seal gain private `_with_rng` seams so a failing-RNG test can drive the
  new Err branch (real OsRng can't be made to fail). Public signatures unchanged;
  OsRng stays the only production entropy source — the seam is deliberately
  private so nothing can substitute a non-OsRng source into the trust core.
- random_word_positions: same latent panic (shuffle -> fill_bytes). Reworked to
  Fisher-Yates over try_fill_bytes returning Result, with rejection sampling
  (bounded_index) so there is no modulo bias. Caller (shell.rs create flow)
  handles the error via auth_error instead of unwrapping.
- clippy.toml: ban RngCore::fill_bytes and SliceRandom::shuffle so the no-panic
  invariant can't silently regress (enforced by `just check` -D warnings).

Tests: create/seal fail closed under an injected FailingRng; random_word_positions
stays distinct/sorted/in-range and caps k at word_count.
@hellno
hellno merged commit 34a68a5 into main Jun 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden keystore RNG failure handling

1 participant