Skip to content

test(7.15): ERC-7730 block 7 remediation and Phase 0 capability mirror - #227

Open
BitHighlander wants to merge 12 commits into
reconcile/upstream-syncfrom
release/715-stack07-phase0
Open

BitHighlander wants to merge 12 commits into
reconcile/upstream-syncfrom
release/715-stack07-phase0

Conversation

@BitHighlander

Copy link
Copy Markdown
Contributor

Into the canonical 7.15 test branch (PR #197).

  • Merges the block 7 audit remediation (formerly fork PR test(7.15): Stack 07 audit remediation for ERC-7730 and streamed EIP-712 BitHighlander/python-keepkey#113, head 9cea0ad) onto canonical 98c717f.
  • Phase 0 of the ERC-7730 formatter plan. Firmware now refuses at preload every program its runtime cannot finish, using one capability table. erc7730_compiler.DEVICE_CAPABILITIES mirrors that table. compile_calldata/compile_eip712 raise DeviceCannotExecute with the exact reason unless executable_only=False.
  • The registry test now asserts that the device verifier and this mirror agree on all 1,440 compiled formats, in both directions. It also asserts exactly 92 signable formats. The earlier estimate of 94 was wrong: two raw fields read the @.from container, which the runtime never captured.
  • Pins device-protocol to keepkey/device-protocol feat/debuglink-confirm-text (5fec9e6, PR into up/release-protocol), regenerates messages_pb2.py with protoc 3.5.1, and adds DebugLink.read_confirm_text().

Local evidence, run against the firmware validator of the matching firmware head: test_erc7730_compiler.py, test_erc7730_catalog.py and test_erc7730_protocol_bindings.py all pass (25 tests).

BitHighlander and others added 11 commits September 22, 2026 23:43
…oder

Array-of-structs, Permit2 batch and multidimensional device walks now
compare the device's domain/message hashes with a spec-derived reference
encoder and recover the signature to the device address. The multidim
walk is Stack 07 work and is gated by the structured-EIP-712 probe, not
by the later evm-unknown-token-review capability.
…e wire

set_expected_responses() raised a CallException whose text embedded the
expected message, so the old assertIn passed whatever the device sent.
Send EthereumSignTx with call_raw and require the first response to be
Failure_ActionCancelled with the exact message (no ButtonRequest first).
…nputs

The firmware-validation step ran only behind 'if validator:' so compile
tests passed without ever reaching firmware. Missing validator or
registry is now an explicit skip, and a failure when CI sets
KK_REQUIRE_ERC7730_EVIDENCE=1.
…ong bytes

Every signing walk now requires exactly one ButtonRequest_SignTx
"Sign Typed Data" screen at the end. New device tests: declining that
screen returns ActionCancelled and no signature; primaryType
EIP712Domain signs keccak(0x1901||domainSeparator) (checked by the
independent encoder and signer recovery); unlimited EIP-2612 and Permit2
amounts are refused before their leaf is displayed; a 1024-byte bytes
leaf signs and verifies independently.
The registry conformance test had never run against a firmware
validator. Run through the firmware's own catalog verifier, 56 of the
1,450 official calldata formats compiled to programs the device
refuses:

- 46 tokenAmount fields with no token compiled to formatter kind 3,
  which the verifier refuses without a token argument. With no token the
  device can only show the raw integer, so compile the raw formatter.
- 8 paths iterated two nested arrays; the verifier accepts one "[]"
  step per path. Refuse them by name.
- 2 ABIs nested deeper than the device's 8 levels. Refuse them by name.

The registry test now requires every format to pass the firmware
validator or be refused for one of those named device limits, and pins
that count at 10.
The device response buffer is shared with DebugLink, and a state read
during the final sign screen erased an address written before it. The
signature still recovered to the right key, so only the reported address
was wrong. Under screenshot capture the unfixed firmware returns an empty
address and these assertions fail.
Firmware calldata and typed-data captures refuse ERC7730_ABI_MAX_DEPTH (8)
or more path steps, and the catalog verifier now does too. Refuse such
paths by name at compile time. No official registry format is affected.
Firmware now refuses at preload every ERC-7730 program its runtime cannot
finish, using one capability table shared by the verifier and the runtime.
Mirror that table here as DEVICE_CAPABILITIES:

- compile_calldata/compile_eip712 raise DeviceCannotExecute with the exact
  reason unless executable_only=False;
- device_refusal() reports why a compiled program would be refused;
- the registry test asserts that the device verifier and this mirror agree
  on every compiled format in both directions, and that exactly 92 of the
  1,450 official formats are signable (not 94: two raw fields read the
  @.from container, which the runtime never captured).

Also pin device-protocol at the canonical release protocol plus the Solana
LUT merge and DebugLinkState.confirm_title/confirm_body, and add
DebugLink.read_confirm_text() so tests can assert exact screen text.
BitHighlander added a commit to BitHighlander/keepkey-firmware that referenced this pull request Sep 25, 2026
…ot run

Phase 0 of docs/security/HANDOFF-ERC7730-715-FORMATTERS.md (issue #861).
The preload verifier accepted formatter kinds, display opcodes, path shapes
and conditions that the signing runtime cannot execute, so a signed program
could load and then fail after the user had approved the signer, the intent
and earlier fields. Copilot found three narrower cases of this drift.

- erc7730_capabilities.{h,c}: one table of what the runtime executes
  (intent at pc 0, raw fields without conditions, end; formatter kind 1 with
  one role-1 value; path source 1 with index steps; no conditions). The
  verifier and the runtime call the same predicates.
- The verifier walks every value path against the ABI at preload: tuple
  index in range, fixed-array index in [-length, length), dynamic-array
  index in [-64, 64), and a leaf target. The walk table lives in the
  delegate-record buffer, unused until the bindings, so SRAM is unchanged
  (full reserve 18,336 B).
- DebugLinkState.confirm_title/confirm_body (DEBUG_LINK only) record the
  last confirmation, so wire tests assert exact text instead of counts.
- Pins device-protocol 5fec9e6 (keepkey/device-protocol#123 into
  up/release-protocol) and python-keepkey 67f561c (keepkey/python-keepkey#227
  into reconcile/upstream-sync): both canonical heads plus these changes, and
  dp now also carries dd9c85d, which canonical pyk already pinned.

Registry: 92 of 1,450 formats are signable (the plan said 94; two raw fields
read the @.from container, which the runtime never captured). The device
and the compiler's mirror agree on all 1,440 compiled formats.
…actly

The capability mirror treated every permitted role as required, walked
non-value paths against the ABI and read an index after every step opcode.
None of that could fire under the Phase 0 table, but each would misfire as
soon as the table widens. Separate required roles, list executable
containers, and parse whole-array and slice steps as the firmware does.
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