fix(thumos): record the KDF parameters a device's secrets were derived under - #929
Draft
forkwright wants to merge 2 commits into
Draft
fix(thumos): record the KDF parameters a device's secrets were derived under#929forkwright wants to merge 2 commits into
forkwright wants to merge 2 commits into
Conversation
added 2 commits
August 21, 2026 06:52
…d under Stored secrets carried no record of how their keys were derived, so changing the KDF later would silently make every provisioned device unreadable with no way to tell which algorithm a given sector predates. The format version moves 1 -> 2 and gains a bounds-checked 16-byte KDF record. V1_KDF is defined as PBKDF2/100k -- byte-identical to the retired constant -- and MASTER_KDF currently equals it, so no derivation output changes for any existing device. parse() accepts both versions and resolves a v1 header to V1_KDF. Two defects found while finishing this, both in the branch's own earlier work. A doc comment described a slot count the function had stopped writing. And a test asserted an on-disk slot count of 2 where store_boot_verifier always renders three slots -- it had been patched to compile against the new signature without being reconciled to the new semantics. The code is right and the test was stale: the module format doc and parse()'s own accepted-count logic both independently say v2 carries 2 or 3, and rendering 2 would have made parse() stop recognising its own verifier. Two tests are added because their absence was the real gap. A v1 fixture built from literal bytes rather than from render(), since a fixture generated by the writer under test only proves the writer and reader agree with each other. And the assertion the issue actually asks for: the same passphrase and salt under different KDF parameters must produce different keys, without which the record is decorative. Refs #914
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — one gap is named below and should be closed before this merges
Finding
Stored secrets carry no record of how their keys were derived. Changing the KDF later would silently make every provisioned device unreadable, with no way to tell which algorithm a given sector predates.
What changes
Format version 1 → 2, adding a bounds-checked 16-byte KDF record.
V1_KDFis PBKDF2/100k — byte-identical to the retiredPBKDF2_ITERATIONS— andMASTER_KDFcurrently equals it, so no derivation output changes for any existing device.parse()accepts both versions and resolves a v1 header toV1_KDF.Every call site of
derive_from_passphrase,store_boot_verifier,render(, and everyDeviceSecrets{}construction acrosscrates/thumos/srcwas checked — none left un-threaded.Two defects found while finishing the branch's own earlier work
secrets.rs:329— a doc comment describing a slot count (2 when present, 1 otherwise) the function had stopped writing.secrets.rs:732— a test asserting an on-disk slot count of 2 wherestore_boot_verifieralways renders three slots. It had been patched to compile against the new signature without being reconciled to the new semantics.The code is right and the test was stale, established on three independent sources rather than by re-deriving the same arithmetic twice: the module-level format doc (
secrets.rs:46-48, untouched and predating both),parse()'s own accepted-count andhas_verifierlogic (:221-224,:240-244), and the caller's behaviour. Rendering 2 would have madeparse()stop recognising its own verifier.Two tests added, because their absence was the real gap
v1_preamble_bytes_parse_under_v1_kdf— builds a v1 sector from literal bytes, never viarender().render()only ever writes v2 today, and a fixture generated by the writer under test proves only that the writer and reader agree with each other. Covers both real v1 shapes (salt-only, and salt+verifier). Before this,VERSION_V1appeared only in productionparse()branches and zero times in tests.derive_from_passphrase_differs_per_kdf_parameters— same passphrase, same salt, two KDF parameter sets, different derived keys. This is #914's own "Done when", and without it the record is decorative.The gap that remains — read this before merging
The v1 fixture would NOT catch a wrong
V1_KDFvalue. It provesparse()routes a v1-tagged sector to whateverV1_KDFcurrently is — a self-consistency check betweenparse()and the constant, not an external check on the constant. If the iteration count were typo'd or silently repriced relative to what real provisioned devices used, this test still passes tautologically.What would close it: a known-answer test — a fixed passphrase and salt with an expected key derived independently of this crate, under PBKDF2-HMAC-SHA256 at 100,000 iterations. Nothing in this suite asserts
V1_KDF's value against any external reference, before or after this change.Until that exists, "cannot break existing secrets" is argued from reading, not proven. For a master-key derivation on a device whose stored secrets become unreadable if it is wrong, that distinction is the whole thing.
Verification status
Nothing here has been compiled or run — this box has no build role for this repo. Confidence comes from manual tracing: type and visibility checks, every call site cross-referenced, and the three-source argument above.
cargo test -p thumoson a build box is the actual verdict.