Skip to content

feat(dice): opt-in verifiable dice modes, MIXED and DICE ONLY (7.14.3) - #758

Draft
BitHighlander wants to merge 3 commits into
audit/7143-scope-repairfrom
audit/7143-dice-verifiable
Draft

feat(dice): opt-in verifiable dice modes, MIXED and DICE ONLY (7.14.3)#758
BitHighlander wants to merge 3 commits into
audit/7143-scope-repairfrom
audit/7143-dice-verifiable

Conversation

@BitHighlander

Copy link
Copy Markdown
Owner

Stacked on #755 (audit/7143-scope-repair). Three commits; the tree is byte-identical to the one that went green on run 34568532481 before the rebase. This is the primary target — the bitcoin-only product.

What

Two opt-in ways to seed a device from dice, selected by the host on ResetDevice and confirmed on the device before anything is collected:

mode request seed verify offline with
MIXED dice_entropy=true SHA256d(tagM ‖ device_draw ‖ SHA256(tagU ‖ rolls)) — the device shows its 32-byte draw as 24 words before you roll rolls + the 24 device words
DICE ONLY dice_entropy=true, dice_only=true SHA256(rolls) — byte-for-byte ColdCard Dice-Rolls-Only rolls alone
  • The consent screen names the mode; host Cancel aborts the ceremony. dice_only without dice_entropy, and either mode with no_backup, are refused with SyntaxError.
  • The roll digest is shown in full on the device; any face over 30% of the rolls is refused (ColdCard's rule).
  • Host EntropyAck is 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 — see docs/dice-vs-coldcard.md for why that stays.
  • Features.supports_dice_modes (field 28) so hosts gate on capability, never version: older firmware silently skips the unknown dice_only field 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.
  • The earlier 7.14.3 dice flow (PR Solana parse/sign buffer offset mismatch #461, SHA256(draw ‖ rolls)) is replaced; the tagged derivation is proven not to collide with it (native K6).

Evidence

  • CI on this head: run 34576312518 (every job, both variants). Report renders section K Seed Generation Hardening 18/18: python K1–K1f (mixed, only, biased-rolls refused, only-requires-entropy, no_backup refused, consent-cancel aborts) and native K3–K7c (derivation vectors computed in Python, not captured from the code; bias gate boundaries; exact-count).
  • Gate 3 hardware evidence (OLED on a real device) is still owed before merge.

Pins

Twin of the 7.15 unit; the two products' reset.c/dice_input.c/docs/tests are identical apart from the mnemonic-pager scratch buffers.

… consent

Port of the 7.15 unit (audit/715-dice-only @ bedb6ee) to the bitcoin-only
product, which the owner named as the primary target for this feature.

dice_input.c/.h, dice.cpp, the capture script, the verifier and both docs are
byte-identical to the 7.15 unit. reset.c is identical except where the two
products already differed: 7.15 shares the mnemonic pager's scratch with its
BIP-85 flow through file-scope buffers, and this product has no BIP-85, so
show_mnemonic_pages() owns its scratch as function-local statics here, as the
backup pager did before it was factored. Nothing else diverges.

What the unit does, on both products:
  - ResetDevice.dice_entropy alone is MIXED: the device shows its 32-byte draw
    as 24 BIP-39 words BEFORE the rolls are entered, then
    seed = SHA256d("KK\x01SM" || draw || SHA256("KK\x01D" || rolls)).
  - dice_entropy + dice_only is ONLY: seed = SHA256(rolls). Coldcard's
    Dice-Rolls-Only byte for byte. dice_only without dice_entropy is refused.
  - The device shows a consent screen naming the mode the host asked for;
    holding proceeds, the only "no" is cancelling the reset.
  - The host's EntropyAck is consumed and its bytes dropped in both modes, so
    the wire flow is unchanged. The default no-dice reset is untouched.
  - Rolls with any face over 30% are refused before a digest is drawn; the
    digest is shown in full; tools/verify_dice_seed.py recomputes the wallet
    offline from the roll string (plus the 24 words for MIXED).

Pins, as on the 7.15 unit:
  deps/device-protocol -> 5517c8de3 (feat/dice-only-field-7.14.3): the
    dice_only field cherry-picked onto this product's current pin 8545cd5b6.
    Not canonical up/release-protocol, because the twelve commits between
    carry EIP-712 streaming fields with no .options here and the
    pb_callback_t gate would fail. Retires when the product advances its pin.
  deps/python-keepkey -> c0e9bd8 (feat/dice-modes): host tests for both modes.
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.
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