fix(store): validate WORM/ledger key length from the vault #239

Merged
KofTwentyTwo merged 1 commits from story/m6-vault-key-length into dev 2026-07-30 03:19:15 +00:00
KofTwentyTwo commented 2026-07-30 03:17:51 +00:00 (Migrated from github.com)

Summary

Closes #200.

M6-S2 (#199) wrapped the WORM audit and filter-execution-ledger HMAC keys in Zeroizing but did not length-validate them, unlike storage_key. A corrupted or hand-edited vault entry of the wrong length would be silently accepted as a 16/8/whatever-byte HMAC signing key for the tamper-evident WORM log and filter ledger.

This PR:

  • Changes worm_key/ledger_key on DatabaseEngine from Zeroizing<Vec<u8>> to Zeroizing<[u8; 32]>, matching storage_key (preserves the #199 zeroize-on-drop hardening — no regression).
  • resolve_engine_keys now try_into()s each key into a [u8; 32] with a distinct DatabaseError::KeyProvisioning message per key ("WORM audit key material must be exactly 32 bytes" / "ledger key material must be exactly 32 bytes") before wrapping in Zeroizing, mirroring the existing storage-key pattern exactly.
  • Updates the few call sites that pass &self.worm_key/&self.ledger_key into &[u8]-typed parameters (HMAC new_from_slice, verify_worm_chain) to &self.worm_key[..]/&self.ledger_key[..], since &Zeroizing<[u8; 32]> does not auto-coerce to &[u8].
  • Kept #[allow(clippy::type_complexity)] on resolve_engine_keys — clippy still flags the 3-tuple return type even with all three elements now the same simple type.
  • Adds two new tests seeding a mock vault with a 16-byte (wrong-length) WORM/ledger key and asserting DatabaseEngine::connect_file fails closed with DatabaseError::KeyProvisioning, mirroring get_or_create_key_bytes_fails_closed_on_corrupt_material but at the engine-construction layer.

Test plan

  • cargo fmt --all -- --check — clean
  • cargo check-all (clippy --all-targets --workspace -D warnings) — 0 warnings
  • cargo test-all — all green, 0 ignored
    • nuncio-store unit tests: 61 passed, 0 failed, 0 ignored (includes new db::tests::connect_file_fails_closed_on_wrong_length_worm_key and db::tests::connect_file_fails_closed_on_wrong_length_ledger_key)
    • Full workspace: all crates green (nuncio-cal 25+2, nuncio-cli 28+4, nuncio-contacts 12+3, nuncio-core 52, nuncio-filter 26+2, nuncio-mail 52+3, nuncio-proto 23, nuncio-store 61, nunciod 117 + 13 E2E integration tests across accounts/calendar/contacts/daemon/filters-triage/grpc-auth/spine), doc-tests all 0/0
  • Pre-commit hook (runs the same fmt/clippy/test gate) passed on commit
## Summary Closes #200. M6-S2 (#199) wrapped the WORM audit and filter-execution-ledger HMAC keys in `Zeroizing` but did not length-validate them, unlike `storage_key`. A corrupted or hand-edited vault entry of the wrong length would be silently accepted as a 16/8/whatever-byte HMAC signing key for the tamper-evident WORM log and filter ledger. This PR: - Changes `worm_key`/`ledger_key` on `DatabaseEngine` from `Zeroizing<Vec<u8>>` to `Zeroizing<[u8; 32]>`, matching `storage_key` (preserves the #199 zeroize-on-drop hardening — no regression). - `resolve_engine_keys` now `try_into()`s each key into a `[u8; 32]` with a distinct `DatabaseError::KeyProvisioning` message per key (`"WORM audit key material must be exactly 32 bytes"` / `"ledger key material must be exactly 32 bytes"`) before wrapping in `Zeroizing`, mirroring the existing storage-key pattern exactly. - Updates the few call sites that pass `&self.worm_key`/`&self.ledger_key` into `&[u8]`-typed parameters (HMAC `new_from_slice`, `verify_worm_chain`) to `&self.worm_key[..]`/`&self.ledger_key[..]`, since `&Zeroizing<[u8; 32]>` does not auto-coerce to `&[u8]`. - Kept `#[allow(clippy::type_complexity)]` on `resolve_engine_keys` — clippy still flags the 3-tuple return type even with all three elements now the same simple type. - Adds two new tests seeding a mock vault with a 16-byte (wrong-length) WORM/ledger key and asserting `DatabaseEngine::connect_file` fails closed with `DatabaseError::KeyProvisioning`, mirroring `get_or_create_key_bytes_fails_closed_on_corrupt_material` but at the engine-construction layer. ## Test plan - [x] `cargo fmt --all -- --check` — clean - [x] `cargo check-all` (clippy `--all-targets --workspace -D warnings`) — 0 warnings - [x] `cargo test-all` — all green, 0 ignored - `nuncio-store` unit tests: 61 passed, 0 failed, 0 ignored (includes new `db::tests::connect_file_fails_closed_on_wrong_length_worm_key` and `db::tests::connect_file_fails_closed_on_wrong_length_ledger_key`) - Full workspace: all crates green (nuncio-cal 25+2, nuncio-cli 28+4, nuncio-contacts 12+3, nuncio-core 52, nuncio-filter 26+2, nuncio-mail 52+3, nuncio-proto 23, nuncio-store 61, nunciod 117 + 13 E2E integration tests across accounts/calendar/contacts/daemon/filters-triage/grpc-auth/spine), doc-tests all 0/0 - [x] Pre-commit hook (runs the same fmt/clippy/test gate) passed on commit
Sign in to join this conversation.