test(7.15): hive signing suites (1614–1617) + clearsign v2 harness + chain test updates#27
test(7.15): hive signing suites (1614–1617) + clearsign v2 harness + chain test updates#27BitHighlander wants to merge 21 commits into
Conversation
Tied to the 7.15 release (bitcoin-only + Zcash-privacy split, PR #282):
1. Zcash section prose claimed shielded FVK/PCZT/unified-address are "all live";
they are WITHHELD on the default build (KK_ZCASH_PRIVACY off pending audit).
Retitled to "Zcash Shielded (Orchard)" and rewrote the background to say the
tests skip BY DESIGN (build-flag gated), not for missing/broken support.
2. Skip accounting conflated design-skips with "pending" and labelled a
supported-but-withheld chain "no firmware support yet". Now counts skip vs
missing separately, a build with 0 failures/0 missing headlines GREEN
("N/M PASSED, K skipped (withheld)"), and withheld sections get their own
"Withheld on this build (build-flag gated; skipped by design)" header.
3. Added a "Zcash Transparent" section (Y) for the t-address path that actually
ships and passes (test_msg_signtx_zcash); removed the duplicate B28 that had
it buried under Bitcoin.
4. Device-Specs appendix: added FIRMWARE VARIANTS (KeepKeyBTC/EmulatorBTC) and
SEED LOCK blocks; qualified the curve list (Pallas only on KK_ZCASH_PRIVACY=ON).
5. _pick_best_frame now falls back to a readable frame instead of an "OLED
needed" placeholder when nothing lands in the density band (single dense
QR/address frames were dropped); removed dead _is_setup_frame.
6. _lookup dropped its bare-method fallback (a cross-module method-name
collision could render a never-run test as PASS, defeating --validate-junit);
all SECTIONS modules are test_msg_* so mod::meth always resolves.
7. Non-Latin-1 punctuation (em dashes etc.) was rendering as '?'; added an
_ascii sanitizer at the PDF content-stream boundary.
8. Removed stale "deferred to 7.15+" copy from the TRON/TON sections of a report
that IS firmware 7.15.0.
9. ver_t() tolerates pre-release tags (7.15.0-rc3) and short versions instead of
crashing.
10. Replaced an unprofessional test-fixture memo rendered on an OLED screenshot
with a neutral value (recomputed the expected signature).
Verified: emulator report run green (475 passed, 43 skipped, 0 failed);
generated report shows 18 sections, 199 passed / 20 skipped (withheld) / 0
pending, --validate-junit passes.
Adds the python-keepkey test-framework support for firmware v2 (static schema,
METADATA_VERSION_SCHEMA): a blob that attests only the decode schema (no tx_hash,
no arg values) so it can be signed once offline — the device decodes the arg
values from the calldata it signs.
- keepkeylib/signed_metadata.py: serialize_schema_metadata() (v2 wire format) +
schema_calldata() (ABI fixed-word calldata a v2 schema decodes).
- tests: TestClearSignV2SchemaOffline (7 offline byte-format tests incl. a frozen
body snapshot as a drift gate) + TestClearSignV2Device (on-device decode+sign+
recover, gated requires_firmware('7.16.0') so it skips until a v2 firmware ships).
- report: VS1-VS5 in the EVM Clear-Signing section documenting v2 (no tx_hash,
static decimals/symbol, frozen format, fixed-word scope, on-device round-trip).
Mirrors firmware feat/clearsign-static-schema (keepkey-firmware PR #284).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test_tron_sign_transfer_legacy_raw_data / test_tron_sign_deterministic / test_tron_sign_different_accounts use a hand-rolled, truncated raw_data blob that was never a fully valid TransferContract — it exercised the old unconditional blind-sign path. Firmware's new raw_data clear-sign parser correctly fails to decode it and falls back to the opaque blind-sign path, which now requires AdvancedMode. Enable the policy for these three tests; they're testing signature plumbing (shape/determinism/per-account uniqueness), not contract-content parsing.
…que cases The old test_solana_sign_versioned_v0_opaque built a v0 tx whose instruction only touched static accounts (no address table lookups) and asserted it must be rejected without AdvancedMode. That's the pre-clearsign assumption; firmware now treats static-only v0 messages as fully verifiable (same as legacy) and clear-signs them directly. Split into: - test_solana_sign_versioned_v0_static_verified: static-only v0 clear-signs without AdvancedMode. - test_solana_sign_versioned_v0_opaque: a v0 tx whose instruction resolves an account via the address lookup table (genuinely unverifiable) still requires AdvancedMode.
…gaps
Audit of fork develop CI run 28679110349 (PR #284 clearsign v2 static-
schema, 526 junit tests) against the who/what/why standard for every
major daily-driver EVM tx format. Ten fixes:
1. Lower 8 stale requires_firmware("7.15.1") gates to 7.15.0 (signing
guards x5, blind_sign_blocked/allowed, bip39-word-reject) — the
behaviors ship in the 7.15.0 line under test; the gate zeroed out
the EIP-1559 wrong-signer regression suite and the blind-sign
policy negative path on every 7.15.0 CI run.
2. Document the ETH-section amount-unit rule (Wei below 1 gwei, scaled
ETH/ticker above) so toy conformance-vector screenshots aren't
misread as a "never wei" violation.
3. Disclose EIP-712 typed-data hash-signing as a known gap (E16): the
device signs two host-computed hashes with zero readable domain/
message display — a daily-driver format with no who/what/why today.
4. Fix the ERC-4337 handleOps flow's overclaiming "innerCall: decoded
separately, not raw" placeholder — the representative UserOp's
inner callData is actually empty; the claim is now honest about
what this flow does and does not prove (regenerated its frozen
reference-blob snapshot to match).
5. Add 2 new on-device tests for the v2 static-schema headline security
property, previously unit-tested but never proven on real device
behavior: calldata/schema length-mismatch falls back to the
blind-sign gate (VS6), and an unsupported dynamic arg format is
independently rejected as MALFORMED by the device parser (VS7).
6. Surface the 3 silently-skipped Uniswap V2 liquidity tests (E17-E19)
with their documented emulator-limitation reason instead of leaving
them absent from SECTIONS entirely.
7. Surface the 3 passing-but-invisible THORChain-router EVM deposit
tests (E20-E22), including the router-pin blind-sign-gate proof
that closed a CRITICAL bypass.
8. Surface 6 passing-but-invisible malformed-blob rejection tests
(V7a-V7f: empty/truncated/trailing-bytes/wrong-version/zero-sig/
empty-slot) — the WHY = fail-closed story had no rejection-path
evidence in the release-gate document.
9. Fix the stale "7.15.1 NEW FEATURES" banner comment and the V8
context text that claimed "covered in 7.15.0+" while pointing at a
test gated to 7.15.1 (a self-contradiction in the PDF's own prose,
resolved by fix #1).
10. Fix print_clearsign_flows() (--flows external-signer reference
dump): KeyError on flow['shows'] (no catalog flow has that key —
regression from the move to the shared catalog module) and a
missing required key_id arg to flow_blob().
Verified: all 6 edited files compile; SECTIONS loaded in the real
firmware-checkout env (245 unique entries, 224->245, zero ID
collisions); every new/changed (module, method) SECTIONS reference
resolves to a real test function; all 51 REFERENCE_BLOB_SNAPSHOTS
still match (only the ERC-4337 entry's hash/length changed, matching
the ARG_FORMAT_STRING value edit in fix #4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…1 cap
CI on the previous commit failed: test_clearsign_erc4337_entrypoint_v0_7_handleops
and test_clearsign_batch_all_payloads both got MALFORMED instead of VERIFIED
(AssertionError: 2 != 1). Root cause: legacy v1 ARG_FORMAT_STRING values are
hard-capped at 32 bytes (arg_value_ok(), lib/firmware/signed_metadata.c:138 —
"legacy formats keep their original 32-byte cap"). My disclosure string from
the previous commit ("empty in this representative UserOp", 35 bytes) exceeded
it; the device rejected the whole blob at parse time.
Shortened to "empty (representative)" (22 bytes) — same honest disclosure,
fits the cap. Regenerated the flow's frozen reference-blob snapshot and
verified all 51 REFERENCE_BLOB_SNAPSHOTS match, plus added a blanket check
that no catalog STRING arg exceeds 32 bytes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…7.15 canonical reconcile/upstream-sync was missing the entire v2 clear-signing test harness (serialize_schema_metadata, 51-flow catalog, report generator, solana/tron/ erc4337 fixes) that develop actually pins via c101137. Merge it in so the canonical 7.15 branch is complete, keeping this branch's bip85-gate fix. Conflict-free — the two lines touched disjoint files.
Solana versioned (v0) transaction parsing (solana_parseVersionedTx) landed in the 7.15 line. The two v0 tests asserted against it unconditionally, so on a pre-7.15 firmware (e.g. a stacked release PR that hasn't yet added v0 support) the 0x80 version prefix is rejected as a legacy tx (Failure_SyntaxError) and the tests FAIL instead of skipping. Add the same requires_firmware(7.15.0) gate the bip85 tests use so they skip cleanly on <7.15 and run on 7.15+.
Firmware now gates length-only blind TON transaction signing behind the AdvancedMode policy (same fence as TonSignMessage and Solana/TRON opaque signing).
…y tests Firmware MAYA_ROUTER was corrected from the dead d89dce57.. to the Etherscan-verified v4 e3985e6b..46d, so these ETH swaps must target the real router to be clear-signed (else they fall to the AdvancedMode blind-sign gate). Assertions are structural; no exact-vector regen needed.
- reject foreign path (BIP-44), wrong network (3054'), unassigned role - memo >440 fails with the specific error; 440 boundary still signs+recovers - config.py: KK_FORCE_UDP=1 local-only escape hatch to reach the UDP emulator with a real device plugged in (not for CI)
Review follow-up on keepkey-firmware#305: transfers must reject owner/ memo/posting paths (post-HF28 hived drops higher-role substitution; the cold owner key must never sign a transfer), and account_create/update must reject non-owner paths (the attestation contract recovers to the device owner key).
…tract tests 7 new tests: posting login recovery, all-4-roles, non-printable buffer, 1024 boundary, oversize reject, bad-path fence, chain-id-prefix collision reject.
… printable boundary - all_roles now covers the three Keychain-exposed roles (posting/active/memo) - owner' path moved to the reject fence - new: 300-byte printable message signs via the hex-preview confirm
…ps test suite Own Graphene mini-serializer (dhive-equivalent) so parser and serializer bugs can't cancel out. 9 new tests: vote/downvote (+default chain id), unicode-body post, custom_json posting+active, 2/9/10 permanent exclusion, malformed structure (op count 0/5, extensions, trailing, overlong varint, weight range), role fences (active/memo/owner on posting tx, posting on active tx, mixed-tier, both-auths), oversize.
…rify Maya signatures
Addresses three review findings on this stack.
1) Bindings were generated from a STALE protocol. The device-protocol submodule
was pinned at 2ec999a9 — the same commit that lacks LoadClearsignSigner's
icon fields and Hive 1614–1617 — so build_pb.sh regenerated incomplete
bindings. Bump the pin to f7b4580 and regenerate:
- messages_ethereum_pb2.py: LoadClearsignSigner now exposes icon,
icon_width, icon_height, persist (was key_id/pubkey/alias only, so
constructing with icon raised ValueError).
- messages_pb2.py: real MessageType_HiveSign{Message,Operations} /
Signed{Message,Operations} = 1614–1617 constants. mapping.py's manual
wire-id table was masking their absence.
client.load_clearsign_signer() now accepts icon/icon_width/icon_height/
persist and documents the RLE contract (was RAM-only, no icon).
2) The icon wire contract was mis-documented (device-protocol#36 fixes the
proto). Adds TestClearsignSignerIcon: a reference RLE decoder traced from
draw_bitmap_mono_rle(), the published golden vector (03 FF FF 00, w=2 h=2
-> FF FF FF 00), RUN/LITERAL packets, n==0 and truncation rejects, the
icon/dims/persist round-trip, and text-only identities. Also asserts the
arithmetic that forces RLE: a packed 1bpp 64x64 needs 512 bytes > the
384-byte cap, so the previously documented packed format was impossible.
3) The two Maya EVM tests asserted only v and 32-byte r/s lengths, so a wrong
digest, calldata or key would still pass. (The weakening predates 560b897 —
its parent already had the structural asserts; the router change inherited
them.) Replace with recover_eth_signer(): rebuild the EIP-155 sighash from
the exact tx fields, recover the signer from (v,r,s), and assert it equals
the device's own address for the path. Verified offline: a good signature
recovers to the signer, and tampered calldata does NOT — so the tests now
actually fail on a wrong digest. Recovery keeps them correct across router
changes without re-freezing r/s vectors.
Offline suites: 26 passed.
|
Thanks — all three findings confirmed and fixed in #2 (Medium) — incomplete bindings. Root cause found: this repo's own
#1 (High) — icon wire contract. Fixed in device-protocol#36: the proto documented a packed 1bpp bitmap while firmware ran an RLE decoder. The RLE turned out to be load-bearing — a packed 1bpp 64×64 needs 512 bytes vs the 384-byte cap, so the documented format was impossible at the dimensions firmware accepts. The grammar + golden vector ( #3 (Medium) — Maya tests didn't verify signatures. Correct, and worth noting the weakening predates 560b897 (its parent already had the structural asserts; the router change inherited them). Rather than re-freeze r/s, both sites now use Offline suites: 26 passed. |
…rrected proto The reference RLE decoder accepted a 128-byte literal (n = -128 / 0x80) because Python ints don't overflow — masking a real incompatibility: firmware's counter is int8_t, so -(-128) wraps to -128 and the packet is undecodable (assert on debug/emulator; negative-counter signed-overflow UB under NDEBUG). A decoder that accepts what the device cannot is worse than no reference at all. Mirror firmware exactly: reject n == -128 and n == 0. Adds the boundary tests the review asked for — 0x80 literal rejected, and the valid -127/127 boundaries still decode — plus a guard that the icon_width cap is the 40px text column (LEFT_MARGIN_WITH_ICON), not the 64px height, so the two can't be conflated. Re-pins device-protocol to 7182973 (the corrected contract: literals [-127,-1], 0x80 invalid, icon_width 1..40). Comment-only upstream, so the regenerated bindings are byte-identical — pb2 files unchanged. Firmware enforcement: BitHighlander/keepkey-firmware#310. Offline suites: 30 passed.
|
All three confirmed. Fixed in Reference decoder masked the 0x80 incompatibility — correct, and the most important catchMy decoder accepted a 128-byte literal because Python ints don't overflow, hiding that firmware's Fork pin — correct, and now gatedYou're right that this doesn't satisfy
So rather than pretend otherwise, #27 is now draft with a fix-before-merge section mirroring keepkey#196's, listing the required sequence: after keepkey#111 lands → url back to Re-pinned to Offline suites: 30 passed. |
…ware validator The reference decoder repeated the drawing path's leniency: it filled w*h pixels and returned, so it accepted a final run that straddles the image (05ff at 2x2) and silently ignored trailing packets. A reference that accepts streams the device rejects is the same class of bug as the 0x80 case — it hands client authors an encoder that produces invalid icons. Mirror firmware's draw_bitmap_mono_rle_valid(): reject leftover run counters and any unconsumed input. Adds the straddling-run and trailing-packet tests. Firmware side: BitHighlander/keepkey-firmware#310.
|
Fixed in Reference decoder repeated the leniency — correct. It filled On the CircleCI finding (#3): this one is expected rather than a gap. python-keepkey's CircleCI can't clone fork branches, but these tests don't get their coverage there — they run in the firmware repo's CI ( Firmware fixes for the icon findings: BitHighlander/keepkey-firmware#310 (full suite 262/262). |
Bumps the integration pins to the heads of the two companion PRs (BitHighlander/device-protocol#36, BitHighlander/python-keepkey#27) so firmware CI actually exercises them. Review correctly pointed out that the earlier "the pyk tests run in firmware CI" answer did not hold at the old pin: firmware CI runs whatever commit firmware PINS, and 15d95ec predates the regenerated bindings + Maya signature recovery, the 0x80 correction, and the exact-decoder tests. Those three commits had no CI anywhere. This pin closes that — python-integration-tests now runs pyk@25df24c against the emulator built from this tree, and check-submodules covers dp@7182973. Local verification at these pins: firmware-unit 262/262.
…tests Three review points, all confirmed against a full integration run. 1) The Maya tests were BROKEN, not merely unverified. Running rc10 against these pins surfaced it: `TypeError: assertEqual() takes 3 positional arguments but 4 were given` — common.KeepKeyTest overrides assertEqual with no msg param (the same trap 591dba8 already fixed once). Both sites failed, so the signature verification never actually ran. Drop the msg argument, and reuse the recovery helper already proven in test_msg_ethereum_clear_signing.py (same signature, hashfunc=None) instead of the bespoke one I added. Integration now: 546 passed, 0 failed (was 544 passed, 2 failed). 2) The public contract was contradictory. client.py still documented all negative counts (including 0x80) and both dimensions as 1..64. Corrected to literals [-127,-1] with 0x80 invalid, width 1..40 (LEFT_MARGIN_WITH_ICON), height 1..64, plus the exactness rule (no straddling run, whole input consumed) and an accurate persist description. 3) The "packed 64x64 needs 512 bytes" rationale is obsolete and is removed. 64px width is no longer legal, and a packed icon at the legal maximum (40x64) is 320 bytes — it WOULD fit the 384-byte cap. RLE is the format because draw_bitmap_mono_rle() is the decoder of record and every bundled image already uses it, not because packed wouldn't fit. The test now asserts that honestly rather than encoding dead arithmetic. Offline suites: 32 passed.
|
All three points confirmed and fixed in The pin waiver was wrong, and running rc10 against these pins proved itYou were right that firmware CI runs whatever firmware pins, so the last three commits had no coverage anywhere. We ran rc10 against these exact pins before upstreaming, and the Maya tests failed:
Now: 546 passed, 0 failed. Firmware #310's pin is bumped to The contract was contradictory — fixed
The "64×64 can't fit" rationale is dead — you're right, and I've removed itOnce width was capped at 40, my own argument collapsed: a packed icon at the legal maximum is 40×64/8 = 320 bytes, which fits the 384-byte cap. The honest reason for RLE is that
|
device-protocolis pinned to a fork commit (7182973, BitHighlander) and.gitmodulespoints atBitHighlander/device-protocol. This does not satisfy the#36 → #111 → master → #196promotion contract, and mirrors the gate upstream keepkey#196 already holds itself draft for.The fork pin is currently unavoidable: the release protos (LoadClearsignSigner icon fields, Hive 1614–1617) exist on no canonical
keepkey/device-protocolmaster commit until device-protocol#111 merges. The bindings here were regenerated from that pin.Required before promoting keepkey#196 — once keepkey#111 lands on
keepkey/device-protocol:master:.gitmodulesurl back tohttps://github.com/keepkey/device-protocol.gitdocker_build_pb.sh, confirm bindings are byte-identical(The fork url in
.gitmodulesis pre-existing — set bye88ff15, already present at keepkey#196's heade728e311— not introduced here. But this PR keeps it alive, so the gate applies.)Stacks into
reconcile/upstream-sync— the branch that PRs to master as keepkey#196. Per SOP the master merge becomes the canonical pin for firmware work, so the harness firmware runs against must ride it.Companion to BitHighlander/device-protocol#36 (protos) and BitHighlander/keepkey-firmware#310 (icon hardening).
Why
Firmware fork
developpins python-keepkey 15d95ec;reconcile/upstream-syncwas at e728e31 — 17 commits behind, leaving the merged Hive work with no upstream test coverage.Review fixes applied
device-protocolsubmodule was pinned at2ec999a9, which lacks the icon fields and 1614–1617, sobuild_pb.shregenerated from a protocol that never had them. Re-pinned + regenerated:LoadClearsignSignernow exposesicon/icon_width/icon_height/persist(constructing withicon=no longer raisesValueError);messages_pb2.pycarries real 1614–1617 constants (mapping.py's manual table was masking their absence);client.load_clearsign_signer()accepts icon/dims/persist.0x80(n = −128), which it previously accepted and thereby masked a real incompatibility. Golden vector + RUN/LITERAL + boundary/reject tests.len(r)==32structural asserts (which predate 560b897). Verified offline: tampered calldata does not recover to the signer.Offline suites: 30 passed.