test: firmware 7.x release test harness (hive, zcash, insight, thorchain denom, ripple memo)#196
Draft
BitHighlander wants to merge 48 commits into
Draft
test: firmware 7.x release test harness (hive, zcash, insight, thorchain denom, ripple memo)#196BitHighlander wants to merge 48 commits into
BitHighlander wants to merge 48 commits into
Conversation
Same firmware as the standalone UDP kkemu binary, loaded in-process via
ctypes. Lets python-keepkey exercise the firmware contract that the
keepkey-vault FFI path imposes — most importantly, the caller-driven
polling model (no daemon thread to call kkemu_poll for you).
- keepkeylib/transport_dylib.py: DylibState (process-wide singleton over
ctypes-loaded libkkemu) + DylibTransport (one per iface 0/1).
Pumps kkemu_poll on every read/write so the firmware actually makes
forward progress on caller turns.
- tests/config.py: KK_TRANSPORT=dylib KK_DYLIB=/path/to/libkkemu.dylib
routes the same fixture to the FFI transport instead of UDP.
- tests/test_dylib_confirm_flow.py: regression for the confirm-flow
contract (Initialize, WipeDevice, LoadDevice, GetAddress). Skipped
unless KK_TRANSPORT=dylib so it won't break the default UDP run.
Reproduces the keepkey-vault hang deterministically: Initialize round-
trips fine, wipe_device hangs because confirm_helper busy-loops on a
ButtonAck the dylib silently consumed but never delivered. Caught in
~10s, no electrobun / bun stack required.
Run:
cd tests && KK_TRANSPORT=dylib KK_DYLIB=.../libkkemu.dylib \
PYTHONPATH=..:../keepkeylib python3 -m pytest \
test_dylib_confirm_flow.py -v
…ntics The existing test_dylib_confirm_flow covers the caller-driven polling contract — Initialize / Wipe / LoadDevice / GetAddress — but never asks the firmware for a layout. Two changes that just landed in the firmware emulator runtime PR (BitHighlander/keepkey-firmware#217) need functional coverage that confirm-flow doesn't provide: 1. RINGBUF_CAPACITY in lib/emulator/ringbuf.h was bumped from 32 to 128. DebugLinkState's 2048-byte `layout` plus the rest of the message serializes to ~44 HID reports through the output ring; the previous capacity left effective room for 31 reports, so screenshot capture truncated mid-layout (msg_debug_write ignores emulatorSocketWrite's 0-on-full return). 2. fsm_msgDebugLinkGetState in lib/firmware/fsm_msg_debug.h now does a single display_refresh() instead of force_animation_start() + animate(). The old form overwrote static layouts with stale animation frames or no-ops depending on queue state, so screenshots captured something different from what the user was seeing. Both fixes are functionally invisible to the existing test suite. Without these tests, regressing either change ships green. This commit adds: - tests/test_dylib_screenshot.py — four tests: * test_layout_round_trip_fits_through_ring (RINGBUF_CAPACITY) * test_layout_repeated_reads_no_truncation (RINGBUF_CAPACITY) * test_layout_stable_across_idle_reads (canvas semantics) * test_layout_features_dont_corrupt_capture (iface separation) Constructs a fresh KeepKeyDebuglinkClient against the dylib singleton WITHOUT going through common.KeepKeyTest.setUp — that fixture wipes the device on every test and exercises the confirm-flow path that test_dylib_confirm_flow is itself a pending regression for. Reading a layout doesn't require any of that; we just init and ask DebugLink for the home-screen capture. - tests/config.py — explicit-transport precedence fix: Previously HID/WebUSB were always autodetected first. With a real KeepKey plugged in, KK_TRANSPORT=dylib was silently overridden — the dylib regression suite would either route to hardware or crash on hid.pyx. Now the explicit env var (KK_TRANSPORT=dylib) skips hardware enumeration entirely, the dylib path runs as requested, and the default (no env var set) falls back to the existing UDP behavior. Verified locally: cmake -DKK_EMULATOR=1 -DKK_BUILD_DYLIB=1 -DKK_DEBUG_LINK=ON \ -DCMAKE_POLICY_VERSION_MINIMUM=3.5 -B build-emu . cmake --build build-emu --target kkemulator_dylib KK_TRANSPORT=dylib KK_DYLIB=build-emu/lib/libkkemu.dylib \ PYTHONPATH=keepkeylib:. python -m pytest tests/test_dylib_screenshot.py ======================== 4 passed in 0.36s ======================== Out of scope: SignTx + other multi-step flows that go through confirm_helper. They share the same hang as test_dylib_confirm_flow's test_load_device_with_auto_confirm — copying the pattern would just produce a second red regression for the same underlying firmware bug, not new coverage. Once the confirm-flow regression goes green, signtx expansion is a follow-up.
…ANSPORT, split confirm-flow setUp Three findings from review of PR #14: #1 (High) test_dylib_confirm_flow used common.KeepKeyTest.setUp which calls wipe_device() — the same path the file's pending regression is for. Hangs in setUp can't be classified by xfail or interrupted by pytest-timeout, so test_features_round_trip ("just Initialize") was actually wipe + Initialize. Refactored to construct KeepKeyDebuglinkClient directly in setUp (matching test_dylib_screenshot's pattern), moved wipe + load_device into the one pending test. Tried the reviewer-suggested @pytest.mark.xfail(strict=True) + @pytest.mark.timeout combo. pytest-timeout (both signal and thread methods) cannot interrupt the C-level kkemu_poll busy-loop — the hang locks up the entire test runner instead of failing the test. Switched to @unittest.skip with explicit rationale documenting exactly that, plus the promotion path: when firmware lands the confirm fix, drop the skip; if a future change makes kkemu_poll GIL-friendly, switch back to xfail+timeout. #2 (Medium) tests/config.py treated any non-empty KK_TRANSPORT as "explicit" and skipped HID/WebUSB autodetect, but only "dylib" was actually handled. A typo like KK_TRANSPORT=dyllib silently fell through to UDP with hardware disabled. Now scoped to a _KNOWN_TRANSPORTS set; unsupported values raise at config import, surfacing typos at test collection time. Verified end-to-end: `KK_TRANSPORT=dyllib pytest test_msg_signtx.py` now errors on collection with the typo'd value in the message. #3 (Medium/Low) DylibTransport.ready_to_read appended raw frame bytes to read_buffer but DylibTransport._pump_one stripped the leading '?' HID marker first. Inconsistent stripping corrupts multi-frame message reassembly: _read_headers can scan a stray '?' from one chunk into the middle of contiguous payload bytes from another, decoding the wrong message-type / length. Centralised the read+strip into a private _poll_and_stash helper shared by both ready_to_read (no sleep) and _pump_one (sleeps on miss). Now the buffer always contains continuation+payload bytes only; the leading '?' is stripped at the single point of stashing. Trailing HID padding zeros from short messages are still tolerated by _read_headers' magic-character search. Verified locally: KK_TRANSPORT=dylib KK_DYLIB=build-emu/lib/libkkemu.dylib \ pytest tests/test_dylib_screenshot.py tests/test_dylib_confirm_flow.py ================== 5 passed, 1 skipped in 0.15s ==================
Wires the ZIP-32 §6.1 seed fingerprint binding into the python-keepkey
client to mirror the firmware-side validation.
device-protocol submodule
- URL: keepkey/device-protocol -> BitHighlander/device-protocol
(zcash work pins to fork master while seed_fingerprint sits in
long-term review for upstream; revert when upstream merges.)
- pin: d0b8d80 -> 4337c452 (BitHighlander/master with PR keepkey#27 merged).
- messages_zcash_pb2.py regenerated via docker_build_pb.sh
(kktech/firmware:v8 → libprotoc 3.5.1, the canonical toolchain).
Selective regen — other pb2 files are intentionally NOT
regenerated because they currently include content from
BitHighlander/device-protocol open PRs (#18 SolanaTokenInfo,
#19 TRON clear-signing, #20 TON clear-signing, #21
EthereumTxMetadata). Until those merge, regenerating them
against current master would back out work that the existing
python-keepkey client relies on.
keepkeylib/zcash.py (new)
calculate_seed_fingerprint(seed) -> 32 bytes
Pure-Python helper. BLAKE2b-256("Zcash_HD_Seed_FP",
I2LEBSP_8(len) || seed). Matches the firmware C
implementation byte-for-byte and the keystone3-firmware
reference vector
seed = 000102...1f
fp = deff604c246710f7176dead02aa746f2fd8d5389f7072556dcb555fdbe5e3ae3
keepkeylib/client.py
zcash_display_address — add expected_seed_fingerprint kwarg
zcash_sign_pczt — add expected_seed_fingerprint kwarg
Both pass through unchanged when the kwarg is None
(backward compatible).
tests/test_msg_zcash_seed_fingerprint.py (new)
Pure-Python helper:
- reference vector (Keystone3 cross-check)
- rejects all-zero, all-0xFF, short, long
Device-backed:
- GetOrchardFVK returns non-empty seed_fingerprint
- fingerprint stable across accounts (bound to seed, not account)
- DisplayAddress: matching expected_seed_fingerprint succeeds,
response carries seed_fingerprint
- DisplayAddress: wrong expected_seed_fingerprint rejected
- DisplayAddress: omitting expected_seed_fingerprint still works
- SignPCZT: wrong expected_seed_fingerprint rejected before
any signing crypto runs
Addresses review of PR #15. Test structure Helper tests (no device) move to a dedicated module: tests/test_zcash_seed_fingerprint_helper.py This module deliberately does NOT import common, transport, or any protobuf bindings, so it runs on a stock dev box: pytest tests/test_zcash_seed_fingerprint_helper.py The previous file inherited common.KeepKeyTest, whose setUp wipes the device — pytest -k 'helper' was never actually offline. Client wrapper coverage Device-backed tests now go through the public client helpers (self.client.zcash_display_address(... expected_seed_fingerprint=...) and self.client.zcash_sign_pczt(... expected_seed_fingerprint=...)) rather than building raw protobuf messages with self.client.call(). Confirms the kwarg pass-through end-to-end. New test test_device_fingerprint_matches_python_helper: cross-checks the device-computed fingerprint against the python-keepkey helper for the same seed (all-allallall mnemonic, empty passphrase). Ties the firmware C, python-keepkey helper, and ZIP-32 §6.1 reference vector to the same byte-for-byte output.
feat(zcash): seed_fingerprint client + tests
… ≤ 7.14.0)
Pairs the device, signs a 1550-byte EIP-1559 transaction with the
all-all-all test mnemonic, and asserts that ECDSA recovery against the
canonical type-2 pre-image yields the device's own address.
Catches a firmware/ethereum.c ordering bug present in 7.x.0 .. 7.14.0
where the empty access-list byte (0xC0) — which closes the EIP-1559 RLP
body and must be the last byte fed to keccak before signing — was being
hashed inside ethereum_signing_init() right after the initial 1024-byte
data chunk, BEFORE the host had a chance to send the remaining
EthereumTxAck frames. For any tx whose data exceeded the single-chunk
threshold, the resulting pre-image was:
keccak( ...header...
|| data_len_prefix
|| data[0..1024]
|| 0xC0 (bug: should be after ALL data)
|| data[1024..end] )
The signature was mathematically valid for that mangled hash so RPCs
accepted the broadcast, but the recovered signer was a wrong-but-
deterministic address. The mempool dropped the tx because the recovered
"from" had no balance / wrong nonce. Production symptom: every Uniswap
Universal Router swap, Permit2 batch, and large multicall hung at
"Confirm in wallet."
Single-chunk transactions (<= 1024 bytes) escaped the bug only by
accident — the misplaced 0xC0 happened to land at the end anyway.
Recovery-based assertion (eth-keys, eth-utils.keccak) — works on any
seed, no golden vectors to capture, the test asserts the actual
invariant: "signature recovers to the signer." Fails on broken
firmware, passes on 7.14.1+.
CI: eth-keys added to the existing pip install line; ships a pure-Python
keccak via eth-utils so no native deps are required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
requires_message("EthereumTxAck") sends an empty EthereumTxAck as a
discovery probe. The firmware (correctly) rejects that with
Failure_UnexpectedMessage because we're not mid-sign, which skips the
test before the actual assertion runs.
requires_firmware("7.2.1") is sufficient — EthereumTxAck has been part
of the protocol since EIP-1559 support landed in 7.2.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
eth-utils ships keccak via the eth-hash adapter, which auto-selects between pycryptodome and pysha3 at import time. Without either backend installed, importing keccak raises: ImportError: None of these hashing backends are installed: ['pycryptodome', 'pysha3']. The new EIP-1559 chunked-data regression test imports keccak from eth_utils to build the canonical type-2 pre-image, so it failed at import rather than at the recovery assertion. Adding pycryptodome to the existing pip-install line fixes it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
KeepKeyTest overrides unittest's assertEqual with a 2-arg version (common.py:104) that doesn't accept the optional msg parameter — passing one raises: TypeError: KeepKeyTest.assertEqual() takes 3 positional arguments but 4 were given Print the regression diagnostic before asserting instead. Pytest captures stdout on failure, so the divergence (expected vs recovered, canonical hash, sig values) still surfaces in the failure report. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstreaming this test as a permanent regression guard rather than a one-shot bug catcher. Bumping requires_firmware from 7.2.1 (the version where EIP-1559 support originally landed) to 7.14.1 (the first version where the access-list ordering bug is fixed) so CI on broken builds skips this test instead of flagging a known-broken state as a new regression. The header comment already documents the affected range (7.x.0 .. 7.14.0) and the fix landing in 7.14.1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…gnition - Bump device-protocol submodule to 8f80bcd (adds memo field to RippleSignTx) - Update messages_ripple_pb2.py with memo field (field 7, optional string) compatible with protobuf==3.20.3 (old-format serialized_pb descriptor) - Add test_sign_with_thorchain_memo in test_msg_ripple_sign_tx.py: verifies serialized XRPL tx ends with canonical Memos array binary (F9 EA 7D <len> <memo> E1 F1), requires firmware 7.14.2 - Add test_msg_ethereum_thorchain_deposit.py: covers legacy deposit() 0x1fece7b4 selector, new depositWithExpiry() 0x44bc937b selector (requires 7.14.2, no AdvancedMode), and verifies non-THORChain addresses are still blocked without AdvancedMode
feat(7.14.2): XRP THORChain memo + EVM depositWithExpiry tests
* feat(hive): add Hive blockchain support - messages_hive_pb2.py — generated from messages-hive.proto (IDs 1600-1603) - hive.py — get_public_key / sign_tx client helpers - mapping.py — register HiveGetPublicKey, HivePublicKey, HiveSignTx, HiveSignedTx wire IDs - client.py — hive_get_public_key / hive_sign_tx methods on ProtocolMixin * feat(hive): add HiveGetPublicKeys, HiveSignAccountCreate, HiveSignAccountUpdate - messages_hive_pb2.py: regenerated from updated proto; now includes all 10 message types (HiveGetPublicKey/Keys, HivePublicKey/Keys, HiveSignTx/ed, HiveSignAccountCreate/ed, HiveSignAccountUpdate/ed). Added role field to HiveGetPublicKey. - mapping.py: register wire IDs 1604-1609 for the six new message types. - hive.py: add get_public_keys(), sign_account_create(), sign_account_update() helpers. get_public_key() gains optional role parameter. - client.py: add hive_get_public_keys(), hive_sign_account_create(), hive_sign_account_update() mixin methods with @expect decorators.
…format Regenerated using protoc from kktech/firmware:v15 (protobuf 3.17.3). The previous version used the builder API (protobuf 3.20+) which is incompatible with the 3.20.3 Python runtime pinned in CI.
Test bugs fixed (mirrors BitHighlander/keepkey-firmware alpha CI fixes): - ETH THORChain deposit: assertIn(sig_v, [27,28]) -> [37,38] (EIP-155 chain_id=1) - XRP no-memo check: b'\xf9' -> b'\xf9\xea' (0xF9 appears in DER sigs naturally) - Zcash FVK validation: skipTest until feature lands in firmware
Covers the full Hive message surface (all 5 firmware handlers) using the standard 12-word seed (mnemonic12, "alcohol ... aisle"): - HiveGetPublicKey — active-role key format + 33-byte raw - HiveGetPublicKeys — 4 distinct STM role keys; single/bulk agreement - HiveSignTx — transfer (op 2), signature recovers to active key - HiveSignAccountCreate — account_create (op 9), recovers to owner key + binds the 4 device keys and account name into the signed bytes - HiveSignAccountUpdate — account_update (op 10), recovers to owner key Account-op tests are self-validating: they recover the signer from the 65-byte device signature over SHA256(chain_id || serialized_tx) and assert it equals the device-derived key — exercising the device and validating the attestation digest (keepkey-vault docs/HIVE-ATTESTATION-DIGEST-SPEC.md). No golden vector required; recovery is an independent check. Hive was the one alpha-firmware feature with full firmware+client support and zero test coverage.
Addresses review: substring-presence was too weak — a role swap (both keys present), a creator rewrite, or an amount change could still pass. Add a cursor-based Graphene reader matching the firmware append_* layout exactly (incl. account_update's 0x01 optional-present flags, asset symbol padding, and the no-wrapper memo_key) and rewrite all three signing tests to parse and assert each field at its expected position + assert_end() for no trailing bytes: - transfer: from / to / amount / precision / symbol / memo - account_create: fee / creator / name / owner|active|posting authority slots / memo_key - account_update: account / each replacement key in its slot / memo_key Recovery assertions retained. Parser validated offline against hand-built firmware-format bytes.
test(hive): vendored SLIP-0048 multi-key + account-op device tests
KeepKeyTest overrides assertEqual(self, lhs, rhs) with no msg parameter, so the 3-arg calls raised TypeError. Verified: all 5 tests pass against the feature/hive emulator (build-emu/bin/kkemu, fw 7.15.0) — get_public_key(s), sign_tx, sign_account_create, sign_account_update, with signature recovery + full serialized_tx field-binding.
test(hive): fix assertEqual signature — all 5 hive tests green on emulator
…ests Integration-test layer for the firmware Insight clear-signing feature (keepkey-firmware feat/evm-clear-signing-alpha, PR #257). signed_metadata.py: - Fix the key_id/slot footgun: serialize_metadata defaults key_id=3, the DEBUG_LINK CI slot whose pubkey == firmware METADATA_PUBKEYS[3] (the test signer derives to slot 3, NOT slot 0). Production/Pioneer callers must pass key_id=0 explicitly. assert_test_key_matches_slot3() pins this invariant. - sign_metadata fails loud if `ecdsa` is missing (was a silent zero-signature that firmware would reject as MALFORMED, disguising the real cause). Signs the identical byte range firmware hashes (version..key_id, excl. sig+recovery). - Add pure-python keccak256 + EIP-155/EIP-1559 RLP sighash helpers so a metadata blob's tx_hash binds the REAL signing digest. Cross-checked against the device: recovering an existing erc20-approve signature over eth_sighash_legacy yields the test mnemonic's m/44'/60'/0'/0/0 address. test_msg_ethereum_clear_signing.py: - All vectors use key_id=3. - New offline (verified green here, 12/12): slot-3 pubkey assertion, key_id=3 default, keccak256 known vectors. - New device-class cases (run on the kkemu/DEBUG_LINK emulator): tx_hash binding happy path (signs + recovers correct signer), replay reject (metadata bound to tx A, sign tx B → "Metadata does not match signed transaction", no signature), AdvancedMode gate (OFF+unknown→reject, ON→sign, native ERC-20 unaffected), and cancel-clears-metadata (stale blob not reused). Offline portion verified with PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python. Device-class cases require the firmware emulator (libkkemu) + DEBUG_LINK.
… SECTIONS The feature ships in the 7.15.0 firmware tree, so gate the device tests at 7.15.0 (was 7.15.1, which left them dormant on the current build). - test setUp: requires_firmware 7.15.1 -> 7.15.0. - generate-test-report.py SECTIONS 'V' (EVM Clear-Signing) min_firmware 7.15.1 -> 7.15.0; add V9-V12 mapping the new device-class tests (full tx-hash binding happy path, replay reject, AdvancedMode gate, cancel-clears-metadata) with OLED screenshot expectations so the report-driven Phase-1 capture includes them. Verified on the containerized kkemu emulator (docker compose, CI-faithful): all 28 clear-signing tests pass; OLED screenshots captured for the verified flow (INSIGHT VERIFIED icon + decoded method/contract/args), the replay reject, and the AdvancedMode gate.
test(insight): EVM clear-signing integration tests + metadata signer
…tract gate) Covers the firmware Ethereum signing pre-image / clear-sign correctness guards (firmware PR BitHighlander/keepkey-firmware#255, merged to alpha): - type=2 without chain_id is rejected (chain_id over-declared the RLP header) - type=2 with max_fee but no max_priority_fee still signs (priority is a mandatory 0x80-encoded field; Stage 1 and Stage 2 must agree) - type=2 carrying only gas_price, and legacy carrying max_fee_per_gas, rejected - a contract clear-sign handler selector with calldata streamed beyond the initial chunk signs the full data via the generic path instead of confirming a prefix (screen-level assertion verified on-device/emulator)
transformERC20 to the 0x Exchange Proxy is blind contract data; since 7.15.0 the device hard-rejects blind data unless AdvancedMode is on (Insight clear- signing policy). Matches the existing test_sign_longdata_swap pattern in this file. Fixes the lone python-integration-tests failure after the firmware clear-signing merge.
…cedmode test(0x): enable AdvancedMode for transformERC20 blind-sign
…ial chunk) 7.15 contract clear-sign handlers require the entire calldata in the initial chunk (data_total == data_initial_chunk.size); transformERC20 calldata is larger, so it now routes through the blind-sign path, which requires the AdvancedMode policy. Set AdvancedMode and gate the test on 7.15.0. The signed bytes are unchanged, so the asserted signature is unchanged.
…15 workaround) The firmware now clear-signs transformERC20 at any calldata size (pinned 0x proxy, bounded by displayed amounts) instead of forcing the blind-sign path, so restore the original no-AdvancedMode test. Supersedes the interim AdvancedMode workaround.
test(eth): EIP-1559 + contract clear-sign signing-guard regression tests
# Conflicts: # device-protocol # keepkeylib/client.py # keepkeylib/transport_dylib.py
…ed routers thor_isThorchainTx is now pinned to the THORChain router (v4 d37bbe..) and thor_isMayachainTx to the Maya router (d89dce..). The eth_btc_swap and eth_add_liquidity vectors used stale (42a5ed v1) / bogus (41e556) to-addresses that the no-pin handler accepted; update them to the real pinned routers and assert signature structure (exact r/s regenerate on-device with the new to).
…oto regen Phase 1 firmware ships with no built-in metadata verification keys; signers are loaded at runtime with a mandatory on-device confirm and a per-tx warning screen naming the alias. Client + tests follow: - device-protocol pin -> 2ec999a9 (up/release-protocol + LoadClearsignSigner message 117); regenerate all pb2 modules (also picks up the zcash/thorchain proto updates the old 5c2d45fc pin was missing; messages-hive added to build_pb.sh so hive regenerates too) - client.load_clearsign_signer(key_id, pubkey, alias) + mapping entry - clear-signing tests: setUp loads the CI test key into slot 3 via the production path (alias 'CI Test'); new tests: load-before-verify order, cancel-at-load refusal, invalid pubkey / alias / key_id rejection - signed_metadata.py: slot-binding comments updated to the loaded-key model Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zcash display-address / seed-fingerprint tests were written against the old "host supplies FVK, device compares" design; the proto has since made ZcashDisplayAddress a request (address/ak/nk/rivk reserved) where the device derives its OWN unified address and optionally checks expected_seed_fingerprint. The version bump to 7.15.0 unlocked these (they skipped on 7.14.1) and exposed the staleness. Realign client.zcash_display_address + the 4 failing tests to the current design. Report generator (scripts/generate-test-report.py): - V (clear-signing): rewrite for phase 1 — no built-in key, every loaded-signer clearsign shows the warning screen; add V13-V16 for the load-signer flow. - G (Hive): NEW section — role keys, transfer, account-create attestation, update. - Z (Zcash): add display-address (Z10-11) + seed-fingerprint (Z12-18); document the Z5-Z7 legacy-sighash gap in section text (no silent skips). - Frame picker: setUp wipe/load frames are now dropped at capture time (client.reset_screenshots, called from setup_mnemonic_* and the clearsign setUp); picker drops blank/full frames and takes the most content-rich one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rt extra-frames
- test_msg_ethereum_erc20_uniswap_liquidity: gate test_sign_uni_approve_liquidity_ETH
on the emulator (approving an UNKNOWN token contract does not complete on the
emulator — same limitation as its add_liquidity sibling; known-token approves
pass; pre-existing, unrelated to clear-signing).
- clear-signing: add semantic-injection alias cases (quote breakout, "."/"("
appending a false "verified by KeepKey" claim) to the bad-alias rejection test.
- report generator: extra-frame selection no longer assumes a 2-frame setUp
prefix (setUp noise is now dropped at capture time); pick meaningful frames
by content instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sages GetBip85Mnemonic has `required` word_count/index, so the empty probe in requires_message() failed to serialize client-side and wrongly concluded "not supported by this firmware build" — skipping all 6 BIP-85 tests even though the firmware handles the message (messagemap.def). Serialize the probe first; a client-side EncodeError means the proto class exists but needs fields (not a firmware signal), so proceed and let requires_firmware gate the version. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…arg formats
The device was showing decoded args but amounts as raw wei and no protocol
name — not the "Amount: 10.5 DAI / protocol: Aave V3" the clear-signing plan
calls for. Add two attested arg formats to the payload + serializer:
- ARG_FORMAT_STRING (4): printable label, e.g. protocol "Aave V3".
- ARG_FORMAT_TOKEN_AMOUNT (5): decimals + symbol + amount -> device renders a
decimal-scaled amount with the ticker ("10.5 DAI"); all-0xFF -> "UNLIMITED".
token_amount_value() helper builds the value; max arg value grows 32 -> 44.
Make the flagship happy-path test a REAL Aave V3 supply(): byte-accurate
4-arg calldata (132 bytes, selector 0x617ba037), metadata decoding it to
protocol/asset/amount(10.5 DAI)/onBehalfOf, and drop the AdvancedMode-toggle
frame so the captured OLED sequence is exactly the who/what/why review.
Report: V section rewritten to explain who/what/why; V9 now renders EVERY
review screen in order (FULL_SEQUENCE_TESTS) and lists the actual tx + payload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Will I be able to run all the clearsign txs and confirm them without ever
seeing tx hex?" — this makes that provable. Adds the remaining 6 real
payloads (mirroring keepkey-sdk tests/evm-clearsign) as full-confirm device
tests, all with AdvancedMode OFF and per-tx-bound metadata:
V17 USDC transfer -> "amount: 1 USDC"
V18 USDC approve -> spender + "1000 USDC"
V19 USDC approve UNLIMITED -> "amount: UNLIMITED USDC" (not 32 bytes of ff)
V20 UniV2 swap ETH->USDC -> protocol, "9.5 USDC" min-out, value shown
on the Transaction screen ("Send 0.01 ETH")
V21 UniV2 swap USDC->ETH -> "100 USDC" in / "0.003 ETH" min-out
V22 UniV3 exactInputSingle -> typed in/out amounts
V23 UniV3 multicall -> named protocol, no inner-call hex
Shared _clearsign_flow() helper: build real calldata -> bind metadata to the
exact sighash -> VERIFIED -> sign -> recover signer over that digest. Report
renders V19/V20 as full ordered screen sequences.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lete signer reference Single source of truth for every clear-sign payload: - CLEARSIGN_FLOWS: all 8 real-world flows (Aave supply + the 7 SDK payloads) with exact tx bytes, typed who/what/why args, and what the OLED shows. Per-flow device tests (V17-V23) now read from it. - test_clearsign_batch_all_payloads (V24): signs the whole catalog in one batch; device validates each blob VERIFIED and the same blob with one tampered byte MALFORMED. - TestClearsignReferenceVectors (offline): RFC 6979 deterministic signing (sign_metadata now uses sign_digest_deterministic — no RNG dependence, byte-identical blobs), signatures self-verify, sha256+length snapshots frozen per flow, and a format guard banning hex-rendering RAW/BYTES args from the catalog. - `python3 test_msg_ethereum_clear_signing.py --flows` dumps the full external reference: to/value/calldata/tx_hash/blob hex per flow, for signer implementations (pioneer-insight, keepkey-sdk) to build against. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Research-grounded expansion from 8 to 51 flows across 20+ protocol categories: DEX swaps (Uniswap V2/V3/V4, Curve), lending (Aave V3, Compound V3, Spark), liquid staking/restaking (Lido, Rocket Pool, ether.fi, EigenLayer), approvals/permits (increase/decreaseAllowance, EIP-2612, Permit2 approve + permitTransferFrom, DAI's non-standard permit, USDT), NFTs (ERC-721/1155 transfer + setApprovalForAll, single + batch), governance (Compound GovernorBravo, ENS), bridges (Hop, Wormhole, Across depositV3), vaults (MetaMorpho, Yearn V2/V3, Compound III), core tokens (WETH wrap/ unwrap, transferFrom), and the newest 2024-2026 tx shapes explicitly researched against Ledger/Trezor's ERC-7730 clear-signing standard and the EIPs it targets: ERC-4337 handleOps, EIP-7702 set-code authorization, Safe execTransaction. Architecture: - keepkeylib/clearsign_abi.py: deterministic Solidity ABI encoder for static types (selectors always derived via keccak256, never hand-typed). Cross-checked against 9+ known real-world selectors before use. - keepkeylib/clearsign_catalog.py: single source of truth (was duplicated across the test file and report generator) — flow()/flow_raw() builders, 35 flows via the static encoder, ~9 hand-built for genuinely dynamic ABI shapes (arrays of dynamic tuples, nested structs), each verified via an offline round-trip decode before being committed. - Data-quality bugs caught and fixed before touching the device: 5 research- agent-transcribed addresses/hashes were off-by-one-hex-char (including an initially-wrong Permit2 address, corrected after independent web verification); an ENS namehash was recomputed via this repo's own keccak256 rather than trusted from research; a uint160 Permit2 max-approval needed a 32-byte all-0xFF DISPLAY value (independent of the real uint160 calldata) to trigger firmware's UNLIMITED rendering. - Test file: 50 per-flow device tests + the batch test are now generated FROM the catalog (setattr loop) instead of hand-written — adding a flow to the catalog is now sufficient, no test-file changes needed. Comprehensive offline pre-flight validator (arg counts, STRING/TOKEN_AMOUNT layout, signed-blob minimum size) catches malformed flows before any device call. - Report generator: V section's catalog-driven entries are now generated from CLEARSIGN_FLOWS too (was hand-duplicated V17-V23, silently drifted from the real test names after the dynamic-generation refactor). Verified 100% match between report-generated method names and pytest-collected test names. - sign_metadata() now uses RFC 6979 deterministic ECDSA (no RNG dependence) so reference blobs are byte-reproducible; REFERENCE_BLOB_SNAPSHOTS grown to all 51 flows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…drs), not arg names twice Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
e9ce308 to
1545299
Compare
test_msg_bip85 gated requires_firmware("7.14.0"), but GetBip85Mnemonic
was introduced in 7.15.0. On 7.14.x firmware the version gate passed and
the real GetBip85Mnemonic call hit Failure_UnexpectedMessage ("Unknown
message"), turning the suite red (6 failures) instead of skipping. Bump
the floor to 7.15.0 so firmware without BIP-85 skips cleanly and CI stays
green regardless of which firmware the emulator is built from. The
redundant requires_message("GetBip85Mnemonic") probe is dropped — it
no-ops on required-field messages, and the version gate now covers it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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 — prepped/staged, do NOT merge yet. Companion to device-protocol#111.
Summary
Brings the entire firmware 7.x release test surface to
keepkey/python-keepkeymaster: keepkey/master merged into the fork (dedup'd against upstream's convergent dylib/EIP-1559/message-signing work), plus the fork-unique release tests — Hive (SLIP-0048 device tests), Zcash (Orchard/PCZT + seed-fingerprint), Insight EVM clear-signing metadata vectors, THORChain denom + Ripple memo, 0x/transformERC20 AdvancedMode, and the firmware-pinned router test vectors.Verified
Green against the alpha emulator on both firmware CI (run #28422476385, python-integration-tests SUCCESS) and a local CI-faithful docker-compose run (426 passed / 33 skipped / 0 failed).
deps/device-protocolis currently pinned to a fork commit (has the release protos). This re-pins tokeepkey/device-protocolmaster once device-protocol#111 merges. Held as draft until then.