feat(dice): opt-in verifiable dice modes, MIXED and DICE ONLY (7.15) - #757
Draft
BitHighlander wants to merge 6 commits into
Draft
feat(dice): opt-in verifiable dice modes, MIXED and DICE ONLY (7.15)#757BitHighlander wants to merge 6 commits into
BitHighlander wants to merge 6 commits into
Conversation
A user can currently prove their rolls were captured -- the digest is
SHA256(rolls) and they can recompute it -- but not that the rolls reached the
seed. The derivation is
seed = SHA256( SHA256(int_entropy || rolls) || ext_entropy )
and of those three inputs the user holds exactly one. int_entropy is never
disclosed and ext_entropy comes from the host, which on our own stack generates
it inside the hdwallet transport and never surfaces it. Firmware that showed a
correct digest and then ignored the rolls would be indistinguishable from
outside.
Adds the mode that closes that, matching ColdCard's Dice-Rolls-Only:
seed = SHA256(rolls)
The RNG draw is discarded rather than folded in, and the host's EntropyAck is
consumed but its bytes dropped, so the wire flow and every existing host are
unchanged. Nothing in the derivation is unknown to the user, so tools/
verify_dice_seed.py recomputes the whole wallet offline from the roll string
with no secret from the device and no network. Its BIP39 implementation was
checked against the reference `mnemonic` library over 600 vectors, 0 mismatches.
The choice is made ON THE DEVICE, after the digest confirm, not by a protobuf
field. A host can therefore neither select it nor force it, which is the same
property ColdCard gets by having no host entropy command at all -- and it needs
no device-protocol change, so no submodule pin moves.
It is opt-in and cannot become a default: confirm() requires a deliberate button
hold, and declining leaves the mixed derivation in place. That matters, because
the mode is strictly less forgiving. Mixing protects a user whose dice are bad;
dice-only protects a user whose device is bad. ColdCard's July 2026 disclosure
-- a five-year PRNG regression cutting the effective search space to about 72
bits -- is the case where only the second helped, which is why both modes should
exist and why this one must be chosen deliberately.
Staged as its own unit off the 7.15 candidate rather than into it: this is a new
derivation mode, and the SOP excludes new features from a product at acceptance.
Replaces the earlier "Dice Only?" confirm, which had a design flaw the dice
test exposed: a one-button confirm() ends only by hold or by the host's
Cancel, so it could not be declined without aborting the whole reset. The
choice is now a selector like roll entry itself -- short press toggles, hold
commits, MIXED first -- driven through the same button/ISR code, which is
factored out (dice_poll, dice_session_begin/end) rather than copied so the two
screens cannot drift apart in how they treat a stale press or the host ack.
Opting into dice_entropy now always yields a derivation the user can recompute
from what they hold. The host's EntropyAck is still consumed, so the wire flow
and every existing host are unchanged, but its bytes are dropped in both modes.
MIXED: user = SHA256("KK\x01D" || rolls)
seed = SHA256(SHA256("KK\x01SM" || device_draw || user))
The 32-byte device draw is shown as 24 BIP-39 words BEFORE the rolls
are entered, so it is committed before the device has seen them and
cannot be chosen to steer the result. Showing it is safe for the
mirror-image reason it was unsafe under display_random: the other
half is dice the host never sees.
ONLY: seed = SHA256(rolls). Coldcard's Dice-Rolls-Only, byte for byte. The
device draw is discarded.
The default, no-dice reset is untouched. Host entropy stays there because it
is the one backstop against a device RNG that is broken but honest -- the
Coldcard July 2026 class of failure, which no on-device health test detects --
and it is removed exactly where it blocked verification.
Also: rolls with any face over 30% are refused before a digest is drawn
(Coldcard's rule); the digest is shown in full, not truncated to 8 bytes; the
mnemonic pager is factored into show_mnemonic_pages() so the device-entropy
words and the backup words share one code path and one DebugLink surface.
tools/verify_dice_seed.py gains --device-words for MIXED. Unit vectors were
computed in Python from the published formulas, not captured from this code,
and the same formulas are restated independently in the host suite; all three
agree on every vector.
The tag bytes are arrays, not string literals: "\x01D" is one byte in C.
The host tests that drive the on-device dice mode selector live on keepkey:feat/dice-modes @ 28cfaf91d, a feature branch off canonical reconcile/upstream-sync @ 7f538a9. Canonical cannot take them yet: the release products do not carry the selector, and a version gate cannot tell the 7.15 candidate apart from this 7.15 unit. When the unit is accepted and ported to 7.14.3, the branch merges into canonical and every product re-pins.
…selector
Owner direction: the mode is a host-side choice made before the ceremony
starts, so the wallet can explain what is coming. ResetDevice.dice_entropy
alone is MIXED; with the new dice_only field it is DICE ONLY. dice_only
without dice_entropy is refused with a SyntaxError before any screen.
The device still gates it: a consent screen names the mode it was asked for
-- "Dice Only" / "Dice + Device", with the roll count -- and requires a hold.
This is the correct use of confirm() where the selector was not: on a
one-button device a confirm() ends only by hold or by the host's Cancel, and
cancelling the reset is exactly the right answer to a mode the user did not
choose. A host therefore cannot select dice-only silently; it can only ask.
Removes dice_mode_select() and its draw code. The button-poll and session
factoring stays, since the roll collector uses it.
Pins:
deps/device-protocol -> 5517c8de3, feat/dice-only-field-7.14.3: the one
dice_only commit cherry-picked onto the products' current pin 8545cd5b6.
It is not based on canonical up/release-protocol (27d3fa1f6) because the
twelve commits between carry EIP-712 streaming fields with no .options on
either product, and the pb_callback_t gate would fail the build. The
canonical-based twin, feat/dice-only-field @ 451e9a7, is the PR target;
this pin retires when the products advance their dp pin to canonical.
deps/python-keepkey -> feat/dice-modes: tests for the host-selected modes,
with bindings regenerated from the canonical-based dp tip.
Findings from an adversarial review of the unit (86 agents, 3 refuters per finding) and from the first CI runs, all confirmed against the code: - The full 7.15 image failed to link: "Insufficient runtime SRAM: require 16 KiB stack/heap reserve". Its base reserve was 16,420 B against a 16,384 B floor, and the unit had added a 128-byte static for the digest page. That page is now formatted into current_words -- 265 bytes, already CONFIDENTIAL, idle between roll entry and the backup pager -- so the unit adds no static RAM beyond the 4-byte mode. - dice with no_backup is refused with SyntaxError before any screen, as display_random with no_backup was: the modes exist to be checked against the backup words, and a reset that never shows them would put seed material on the OLED under a WARNING that recovery is impossible. - The MIXED consent screen now says the 24 entropy words are NOT a backup, since the pages that follow are otherwise indistinguishable from the backup pages. Both consent bodies are shortened so neither can ever paginate. - Features.supports_dice_modes (28) is reported. Older firmware skips the unknown dice_only field and derives a different wallet without complaint, so hosts and the test suite gate on this bit rather than on a version. - dice_mix() was dead once the modes replaced it; removed with its tests. The non-collision test now compares against the old formula's Python-computed vector instead. A unit test that placed its invalid byte past `count` is corrected. - tools/verify_dice_seed.py looked for a wordlist file the repo does not ship; it now reads the firmware's own bip39_english.c (slicing "abandon" to "zoo", since the file quotes other strings too) and was checked to yield exactly the reference English list. - Stale comments describing an on-device mode selector are corrected; docs/DiceEntropy.md "What happens" now describes the shipped ceremony. Pins: deps/device-protocol -> 0848887cf04a67342c468a40b36fa67d15cdca9b (feat/dice-only-field-7.14.3, adds Features.supports_dice_modes = 28 on the products' dp base); deps/python-keepkey -> 51d7cd112aee21daea44c124310c7b08e921ee07 (feat/dice-modes: capability-gated tests, the digest-subpage and roll-injection driver fixes, no_backup and consent-cancel tests, atlas entries).
feat/dice-modes @ 03c2fe11f704720c7a0230908ae9971717765aa5: the report atlas names the unit's eight Dice gtests in place of the four removed with dice_mix(), which the 7.15 bitcoin-only catalog validation had flagged as missing.
BitHighlander
force-pushed
the
audit/715-dice-only
branch
from
September 11, 2026 07:57
25aa57e to
66d0943
Compare
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.
Stacked on #756 (
audit/715-scope-repair). Six commits; the dice commits are unchanged from the tree that went green on run 34568530751 — the only difference under them is #756's report-evidence port (fail-closed wrapper, manifest + sha256 beside the PDF), which this unit now inherits.What
Two opt-in ways to seed a device from dice, selected by the host on
ResetDeviceand confirmed on the device before anything is collected:dice_entropy=trueSHA256d(tagM ‖ device_draw ‖ SHA256(tagU ‖ rolls))— the device shows its 32-byte draw as 24 words before you rolldice_entropy=true, dice_only=trueSHA256(rolls)— byte-for-byte ColdCard Dice-Rolls-OnlyCancelaborts the ceremony.dice_onlywithoutdice_entropy, and either mode withno_backup, are refused withSyntaxError.EntropyAckis still consumed in both dice modes and its bytes discarded. The default (no-dice) reset is unchanged:SHA256(device ‖ host), as it has been since 2014 — seedocs/dice-vs-coldcard.mdfor why that stays.Features.supports_dice_modes(field 28) so hosts gate on capability, never version: older firmware silently skips the unknowndice_onlyfield and derives a different wallet.tools/verify_dice_seed.py --rolls … --words 24 [--device-words "…"]recomputes the mnemonic from what the user wrote down. No device secret, no network.Evidence
current_words.Pins
keepkey:feat/dice-modes@ 03c2fe1 (test(reset): host-selected dice modes, verified against the published derivations keepkey/python-keepkey#222, →reconcile/upstream-sync)keepkey:feat/dice-only-field-7.14.3@ 0848887 — the same commit as feat(reset): ResetDevice.dice_only selects the dice-only derivation keepkey/device-protocol#122 (→up/release-protocol) cherry-picked onto the products' current pin, because the products carry no.optionsfor canonical's EIP-712 streaming fields yet.Twin of the 7.14.3 unit; the two products'
reset.c/dice_input.c/docs/tests are identical apart from the mnemonic-pager scratch buffers.