Skip to content

feat(keepkey): on-device dice entropy on ResetDevice, with tests - #63

Merged
BitHighlander merged 7 commits into
masterfrom
feat/dice-entropy-master
Aug 11, 2026
Merged

feat(keepkey): on-device dice entropy on ResetDevice, with tests#63
BitHighlander merged 7 commits into
masterfrom
feat/dice-entropy-master

Conversation

@BitHighlander

Copy link
Copy Markdown
Collaborator

Adds the diceEntropy flag to core.ResetDevice so a host can ask the device to collect dice rolls with its own button and fold them into the internal entropy before the seed is committed. The rolls never reach the host — that is the point: desktop-entered dice only help while the device's own randomness stays secret, so they are no defence against a compromised host combined with a weak device RNG.

The vault's create-wallet flow needs this; without it the dice step cannot ship.

The property that matters

Not that the flag can be set, but that ResetDevice.dice_entropy stays absent unless requested. The field only exists in firmware >= 7.15.0, and older firmware rejects a message carrying an unknown field — so setting it unconditionally would break wallet creation on every device already in the field.

reset-dice-entropy.test.ts pins that with five cases: set-when-requested, absent-when-omitted, absent-when-explicitly-false, a wire-format round trip (present-but-false is not the same as absent), and that the other reset fields still populate alongside it.

These were verified to fail for the right reason: replacing the guard with an unconditional setDiceEntropy(!!msg.diceEntropy) turns three of them red.

device-protocol pin

The feature branch pinned @keepkey/device-protocol at BitHighlander/device-protocol#dice-entropy-v1, which would have put a personal fork in the dependency chain of a published package. That is unnecessary — the same commit is already merged into keepkey/device-protocol on up/release-protocol, the branch the vault tracks — so this pins its current tip fbb483f1 on the canonical repo instead.

Also included

da50506b derives BIP-86 account xpubs with SPENDADDRESS. fsm_msgGetPublicKey picks xpub version bytes from the script type and only recognises SPENDADDRESS / SPENDP2SHWITNESS / SPENDWITNESS, so sending SPENDTAPROOT fails outright and BTC account discovery dropped Taproot entirely. P2TR is still sent on address and signing requests, where firmware does handle it. Ships with its own coverage in taproot.test.ts.

Testing

Full hdwallet-keepkey suite: 13 suites, 96 tests, all passing on the canonical device-protocol package.

…ccepts

fsm_msgGetPublicKey selects the xpub version bytes from the script type
and recognises only SPENDADDRESS, SPENDP2SHWITNESS and SPENDWITNESS;
anything else falls through to "Invalid combination of coin and
script_type". Sending SPENDTAPROOT therefore fails outright, so BTC
account discovery dropped Taproot from the supported set and no P2TR
account could be listed.

BIP-86 account xpubs use the same serialization as legacy account
xpubs, so SPENDADDRESS is the correct wire type for this message.
P2TR is still sent on address and signing requests, where the firmware
does handle it.
Adds an optional diceEntropy flag to core.ResetDevice and sets
ResetDevice.dice_entropy when it is requested, so a host can ask the device
to collect dice rolls with its own button before the seed is committed.

The rolls never reach the host. That is the point: desktop-entered dice only
help while the device's own randomness stays secret, so they are no defence
against a compromised host combined with a weak device RNG. Entering them on
the signer removes the host from the trust path entirely.

Set only when requested -- firmware older than 7.15.0 has no such field and
would reject an unknown one, so an unconditional set would break every
existing device.

device-protocol moves to the BitHighlander fork at tag dice-entropy-v1
(= cf308fd5e, the merge carrying ResetDevice.dice_entropy). A tag rather
than a bare SHA because yarn resolves refs, not arbitrary commits.

hdwallet-core and hdwallet-keepkey both build.
The vault's create-wallet flow offers on-device dice entropy, which needs
ResetDevice to carry the diceEntropy flag (8cfd1fb). That commit was never
merged with the clearsign/Ironwood work the vault pins today (5ed583d), so
neither pin was a superset and the vault could not have both.

Clean merge: adds the diceEntropy field to ResetDevice in hdwallet-core and
the keepkey wallet, and keeps the Zcash/Ironwood surface intact.
The feature shipped with no tests. The property worth pinning is not that the
flag can be set, but that ResetDevice.dice_entropy stays ABSENT unless asked
for: the field only exists in firmware >= 7.15.0 and older firmware rejects a
message carrying an unknown field, so setting it unconditionally would break
wallet creation on every device already in the field.

Covers set-when-requested, absent-when-omitted, absent-when-explicitly-false,
a wire-format round trip (present-but-false is not the same as absent), and
that the other reset fields still populate alongside it.

Verified these fail for the right reason: replacing the guard with an
unconditional setDiceEntropy(!!msg.diceEntropy) turns three of them red.
…nal fork

The dice-entropy work pinned @keepkey/device-protocol at
BitHighlander/device-protocol#dice-entropy-v1, which would have put a personal
fork in the dependency chain of a published package the moment this reached
master.

It is no longer necessary: the same commit (cf308fd5) is already merged into
keepkey/device-protocol on up/release-protocol — the branch the vault has
tracked all along — so pin its current tip fbb483f1 instead. ResetDevice
still carries dice_entropy, and ButtonRequest_DiceRoll is present in types.

Verified by reinstall: yarn.lock moves only the device-protocol entry, and
the full hdwallet-keepkey suite passes on the canonical package.
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hdwallet-sandbox Ready Ready Preview Aug 11, 2026 12:50am

Request Review

…ore it

The original guard rested on a false premise. Its comment claimed firmware
older than v7.15.0 "would reject an unknown field", so merely omitting
dice_entropy was treated as sufficient. nanopb does the opposite: unknown
fields are SKIPPED, not rejected — lib/transport/pb_decode.c:904, "No match
found, skip data", identical on the v7.14.1 tag.

So reset({ diceEntropy: true }) against pre-7.15 firmware SUCCEEDED. The
device ignored the field and produced an ordinary RNG-only seed while the
caller believed dice rolls had been folded in — a silent downgrade of a
security property that was explicitly requested, with no error anywhere.

Gate it instead: supportsDiceEntropy() reports whether the device honours the
field, reset() throws before sending when it does not, and an unreadable
firmware version reports false so the check fails closed. Naming the version
in the error means a caller on old firmware learns why rather than guessing.

Adds supportsDiceEntropy() as a public capability check so callers can hide
the option up front instead of discovering it at reset time. The vault already
routes around the dice step on old firmware; this makes the library safe for
callers that do not.

Verified the new tests fail for the right reason: removing the gate turns the
three downgrade cases red, including the one asserting no ResetDevice is sent.

Reported in review of #63.
@BitHighlander

Copy link
Copy Markdown
Collaborator Author

Confirmed and fixed in b1b1420. Thanks — the premise the original guard rested on was simply wrong, and the consequence is worse than the comment implied.

Verified the claim

lib/transport/pb_decode.c:904 is /* No match found, skip data */ pb_skip_field(...). I checked the v7.14.1 tag directly rather than trusting current develop, and it is identical there — line 904, same skip. Unknown fields are skipped, not rejected.

So reset({ diceEntropy: true }) against pre-7.15 firmware succeeded, the device ignored the field, and the caller got an ordinary RNG-only seed believing dice rolls had been folded in. Silent downgrade of an explicitly requested security property, no error anywhere. The original comment asserting old firmware "would reject an unknown one" was the load-bearing mistake.

Fix

  • supportsDiceEntropy() — public capability check, semver.gte(fw, "v7.15.0"), following the existing ethSupportsEIP1559() pattern, so callers can hide the option up front rather than discovering the problem at reset time.
  • reset() throws before any transport.call, naming the offending version so the caller learns why instead of guessing.
  • Fails closed: an unreadable/absent firmware version reports false. Assuming support when we cannot tell is the same bug in a different costume.

Coverage for the downgrade path

Four new cases, and the one that matters asserts no ResetDevice was sent — throwing after a successful reset would still have created the RNG-only wallet:

  • throws instead of resetting when firmware predates v7.15.0, and nothing is sent
  • names the offending firmware version in the error
  • fails closed when the firmware version is unreadable
  • still resets normally on old firmware when dice is not requested — the gate must not become a general old-firmware block

Plus a supportsDiceEntropy() truth table including the no-version case.

I verified these fail for the right reason: removing the gate restores the exact reviewed vulnerability and turns those three red while the other twelve stay green. 13 suites / 106 tests pass overall.

On the vault

You are right that it gates its UI — create-briefing routes past both security steps when diceSupported is false, so diceEntropy stays false on old firmware, and that gate fails closed on an unknown version. It was never exposed. But its comment carried the same false "older devices reject the unknown field" rationale, which invited someone to delete the gate as redundant; I corrected that separately so the gate reads as load-bearing.

@BitHighlander
BitHighlander merged commit 87553b9 into master Aug 11, 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.

1 participant