Skip to content

Combine: fork alpha into master (master becomes the single target) - #111

Merged
BitHighlander merged 189 commits into
masterfrom
alpha
Sep 19, 2026
Merged

BitHighlander merged 189 commits into
masterfrom
alpha

Conversation

@BitHighlander

Copy link
Copy Markdown
Owner

Why

The fork had two live targets. The ERC-7730 pyk PRs (#92, #93) went to master. The firmware-alpha work (#108 upstream sync, #109, #110) went to alpha. Firmware alpha's .gitmodules already tracks branch = master. From now on master is the single target, and alpha is retired once this lands.

What

alpha (1c5e544e) is 179 commits ahead of master (54d4f6d5) and 0 behind, so this is a fast-forward and every commit the firmware pins stays reachable. Contents:

Evidence so far

Firmware alpha CI ran this exact pyk head: python-integration-tests full = 777 passed, 0 failed; bitcoin-only is green. This PR is the first run of pyk's own CI (7.15 / 7.14.3 release candidates) on the combined content. Its results are compared against master's existing baseline (integration and integration-rc18 already fail on 54d4f6d5).

BitHighlander and others added 30 commits March 17, 2026 22:25
- Hand-written messages_zcash_pb2.py (protobuf 3.x compatible)
- Restore original messages_pb2.py / types_pb2.py (don't recompile)
- Manual Zcash wire ID registration in mapping.py (1300-1307)
- Add zcash_get_orchard_fvk() client method
- Add test_msg_zcash_orchard.py with 5 FVK validation tests
- Add zcash_sign_pczt() session helper with ZcashPCZTActionAck loop
- Remove stale n_transparent_inputs field from ZcashSignPCZT
- Remove stale ZcashTransparentInput/ZcashTransparentSig messages
- Proto bindings now match messages-zcash.proto exactly
The reference vectors are from the orchard Rust crate but the firmware
currently uses seed_proxy instead of the real BIP-39 seed, so the
emulator cannot produce matching output. Mark as @expectedfailure
until the seed derivation is fixed.
- Change account default from 0 to None — only send account field
  when caller explicitly sets it, otherwise firmware derives from
  address_n[2]. Fixes silent wrong-account signing.
- Add @session decorator to keep entire PCZT signing flow in one
  transport session, matching ethereum_sign_tx and eos_sign_tx_raw.
- Add test_msg_zcash_sign_pczt.py: single-action, multi-action,
  signature format, account separation tests
- Remove @expectedfailure from FVK reference test (seed fix landed)
- Restore @expectedfailure on FVK reference vectors (C derivation
  doesn't match orchard crate yet — separate from seed access fix)
- Fix TypeError in signing test: remove msg arg from assertEqual
  (test framework doesn't support 3-arg form)
- Regenerate proto files from device-protocol feature/evm-clear-signing
- Add ethereum_send_tx_metadata() client method
- New messages: EthereumTxMetadata (115), EthereumMetadataAck (116)
- All proto files regenerated with protoc 33.0

Backwards compatible — new method is optional, existing API unchanged.
Change zcash_get_orchard_fvk account default from 0 to None.
Only serialize account field when explicitly set — firmware derives
from address_n[2] otherwise. Same fix as zcash_sign_pczt.

Update tests to rely on address_n path derivation.
- signed_metadata.py: canonical binary serializer + secp256k1 signer
  matching firmware's parse_metadata_binary() format exactly
- test_msg_ethereum_clear_signing.py: 19 test vectors covering:
  - Valid: Aave supply, zero-arg, max-arg (8), Polygon chain
  - Invalid signature: wrong key, tampered method/contract/amount, zero sig
  - Structural: truncated, empty, wrong version, too many args, invalid
    format byte, wrong key slot, extra trailing bytes
  - Binding mismatch: wrong chain/contract/selector (sig valid, binding fails)
  - Policy: EthBlindSigning disabled → hard reject
  - Backwards compat: no metadata → existing flow unchanged

Test key: private=0x01 (secp256k1 generator point G)
Requires: pip install ecdsa
Remove @expectedfailure — the 3 ZIP-32 derivation bugs are now fixed:
1. Child derivation personal: "ZcashIP32Orchard" → "Zcash_ExpandSeed"
2. Domain separator: 0x11 → 0x81
3. Index encoding: big-endian → little-endian (I2LEOSP32)
Identity: keepkey.com/insight (proto=ssh for raw SHA256 signing)
Path: m/13'/44358944'/1285410994'/2003068762'/1451542600'
Pubkey: 02e3b3015c47ddcaabe4f8e872f1ed8f09ca145a8d81770d92213d56da31ab5107

Cross-validated against pioneer-insight TypeScript derivation.
… tests

- zcash_sign_pczt_hybrid(): two-phase signing (transparent ECDSA + Orchard)
- 11 tests: happy path, path validation (7 cases), phase ordering
- Happy path tests now verify DER signatures cryptographically:
  derives pubkey from device, verifies ECDSA against the sighash
- Cross-key test: sig for key[0] must not verify against key[1]
- Multi-input test: each sig verifies against its own sighash only
- Edge cases: out-of-order transparent index, too many inputs (>8)
- Total: 14 tests (3 crypto-verified happy path, 7 path validation,
  1 phase ordering, 3 edge cases)
- Remove Pillow dependency from screenshot capture (client.py)
  Pure Python PNG writer using stdlib struct+zlib. Zero build time.
- Move screenshot capture from call_raw to callback_ButtonRequest
  Captures the actual confirmation screen, not idle state.
- Per-test screenshot directories in common.py setUp
  KEEPKEY_SCREENSHOT=1 SCREENSHOT_DIR=path enables capture.
- Add scripts/generate-test-report.py (stdlib only, no deps)
  Auto-detects firmware version, reads JUnit XML for pass/fail,
  embeds real OLED PNGs in PDF, version-gated sections.
- Remove old generate-zoo-report.py

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ress

- .gitmodules: device-protocol URL → BitHighlander/device-protocol
- device-protocol: pin to 93d251a (includes ZcashDisplayAddress + ZcashAddress)
- Regenerate messages_zcash_pb2.py and messages_pb2.py with protoc 3.5.1
  (via kktech/firmware:v15 Docker image for CI compatibility)
Blind-sign policy gating (AdvancedMode blocking) is 7.15+ scope.
On 7.10-7.14 blind signing is always permitted. Remove
test_ethereum_blind_sign_blocked which expected 7.15 firmware
behavior and update report section V accordingly.
The test's own docstring says the feature it exercises requires firmware
7.15.1+, but the gate admitted 7.15.0. Any 7.15.0 build therefore runs a
test for behaviour that release is not expected to have: entering a word
outside the BIP-39 wordlist returns a CharacterRequest for the next word
rather than a Failure, and the assertion fails.

No behaviour change -- the gate now matches the docstring beside it.
keepkey#216 landed as one squashed commit whose content had already reached keepkey#197 by
another route, so most of the 31 conflicts are the two branches doing the same
thing in different words. Resolved by hand, hunk by hunk; no --ours/--theirs.
Every keepkey#216 addition is verified present in the merged tree, including the
files that never conflicted (test_msg_display_disclosure.py, the eip155
vectors, the ripple/ping/tron/eth-signtx disclosure tests, and the
`chain_id is not None` fix in keepkeylib/client.py -- that one matters, it is
what lets an explicit chain_id=0 reach the wire to be refused).

tests/test_sign_typed_data.py (1 hunk) -- keepkey#197
  Same policy call on both sides; the only difference is `with open(...)` vs
  open/close and one extra clause of comment. Nothing asserted differs.

tests/test_msg_thorchain_signtx.py (2 hunks) -- keepkey#197, and this one is a real
divergence, not a restatement.
  keepkey#197 repointed test_sign_eth_add_liquidity at the firmware-pinned THORChain
  router (to=0xd37bbe57...); that line auto-merged, so it IS the transaction
  the merged test signs. keepkey#216 regenerated exact r/s against the OLD
  to=0x41e55600.... `to` is an RLP field of the legacy sighash, so keepkey#216's
  vectors do not describe the transaction under test and would fail. keepkey#197's
  structural assertions win on those grounds -- NOT because structural is
  preferable. They are genuinely weaker than what keepkey#216 had, and the superseded
  vectors are recorded in a comment at the assertion so the gap is visible and
  regenerable on-device. Same treatment for the memo-length hunk: both sides
  independently corrected 0x3b -> 0x3a (the memo is 58 bytes; the 59th was ABI
  padding), so the comments were combined rather than one dropped.

tests/test_msg_mayachain_signtx.py (2 hunks) -- keepkey#197
  Same router story (to=0xe3985e6b..., Maya router v4), so keepkey#216's exact r/s --
  byte-identical to the thorchain pair, same base fixture -- are stale here too.
  keepkey#197 does not merely relax: it reconstructs the sighash host-side and recovers
  the signer, so a wrong digest, wrong calldata or wrong key fails. Taking keepkey#216
  would also have re-added a stray `)` and restored @unittest.skip on
  test_mayachain_remove_liquidity, which keepkey#197 unskipped. Raw assertion count in
  this file drops 26 -> 13 because seven frozen-vector memo cases became one
  loop over the same seven memos and three skipped tests now actually run; memo
  coverage is unchanged at nine cases and none of them are skipped any more.

tests/test_msg_ton_signtx.py (9 hunks) -- union
  Both sides added the identical AdvancedMode opt-in to nine tests; keepkey#216 also
  carried the four-line reason. Kept keepkey#216's comment and call on all nine.
  keepkey#197 additionally gated three tests keepkey#216 did not -- the reject-path tests --
  and those auto-merged; annotated them, because opening the gate is what stops
  them passing for the wrong reason (with the gate shut the firmware refuses
  every TonSignTx and the raises-assertion is satisfied without the parser ever
  validating a field).

scripts/generate-test-report.py (17 hunks) -- keepkey#197
  Fifteen hunks are section-letter collisions: keepkey#216 filed Display Binding under
  'S' and Display Disclosure under 'D', but 'S' is already Solana and 'D' is
  already BIP-85 Child Derivation, so keepkey#216's own catalog carries each letter
  twice. keepkey#197 assigns 'J' and 'Q' and does not collide; _audit_catalog() asserts
  letter and id uniqueness and would have tripped on keepkey#216's scheme. Entries are
  otherwise identical -- same module, same test, same prose -- so nothing keepkey#216
  catalogued is lost, only relettered. Fixed one cross-reference keepkey#197's rename
  left dangling: J13's prose said "the control for S12", which now points at a
  Solana test; it means J12. The other two hunks are keepkey#197-only additions (the F,
  I, L, U, TD sections and MUST_RUN_MODULES) against an empty keepkey#216 side.

tests/test_msg_ethereum_erc20_0x_signtx.py -- union, and git got this one wrong
  Not one of the 31: it merged clean and merged badly. Both sides inserted an
  AdvancedMode opt-in into test__sign_transformERC20 at different offsets, so
  the merged file called apply_policy twice in a row. Deduped to one call
  carrying both rationales (keepkey#216's 1480-bytes-past-the-1024-chunk mechanism,
  which is what atlas J1 actually claims, plus keepkey#197's no-recognised-handler
  note). Folded in the comment keepkey#197 left three lines above it saying "No
  AdvancedMode policy is set here on purpose", which contradicted its own
  apply_policy call and both sides' findings; kept as an explicitly superseded
  note rather than deleted, so the behaviour change stays legible.

Verified: 0 conflict markers; every .py under tests/, scripts/ and keepkeylib/
parses; _audit_catalog() passes and the report renders 27 sections / 381
distinct tests; all 383 catalog rows resolve to a test that exists (V17-V67
against CLEARSIGN_FLOWS, which generates those methods by the same transform;
K3-K11 are native gtest suites the JUnit parser handles by bare classname).
Not verified: no test was executed against firmware. The one suite that runs
offline, test_storage_version_gate.py, needs the firmware sources checked out
above this tree and errors in setUp without them -- unchanged by this merge.
These three tests lost their emulator guard, and without it they do not
fail on the emulator -- they HANG. The ETH/FOX pool contract is not in the
token table, and approving an unknown token contract never completes, so
the device keeps answering while the approval never finishes. The socket
timeout never fires because reads keep succeeding, so the job goes silent
until CI kills it on its no-output timeout, taking every later test with
it. That is why the suite died at 34% having reported no failure.

Gated rather than deleted, so the coverage comes back on its own once the
firmware completes this path. Known-token approves still run here, and on
hardware the app exercises this path.
TestStorageVersionGateSource asserted STORAGE_VERSION_LAST_SHIPPED exists,
so all eleven of its tests failed at setUp against any firmware that
predates the storage version gate -- which is every branch except alpha.
That is what has been red on CircleCI, which builds firmware master.

The gate is a firmware FEATURE, so gate on the capability. A tree that
never had it has nothing here to assert.

The skip is deliberately narrow: if storage.c references the constant but
storage.h no longer defines it, that is the floor being deleted out from
under the static assert -- the exact regression this suite exists to catch
-- and it still FAILS. The skip cannot swallow the deletion it guards.

Verified against three real trees: 7.14.3 runs (floor 17), upstream develop
skips, alpha runs (V20, floor 17).
BitHighlander and others added 28 commits September 18, 2026 09:39
Firmware alpha (6816915a3) replaced direct dice entry with a host-selected
mode and an on-device consent screen, then shows the device's 24 words
before any roll. That firmware pinned python-keepkey f00e62f; a later
re-pin to b44f1b3 (this alpha line) dropped the matching test, so
test_reset_device_dice acked the consent screen as roll input and the
device answered "Reset cancelled".

Rewrite the test body for consent -> device words -> rolls -> digest
confirm, and check the mnemonic against the published MIXED formula
(device words + rolls; EntropyAck bytes excluded). Seed helpers and the
requires_dice_modes capability gate come from f00e62f. Same test name,
same two captures the report declares.
Firmware refuses a 68-byte approve(spender, 2^256-1) with "Unlimited ERC20
approval is disabled" (release-line policy since 7.14.2). This alpha line
still expected signatures, so three tests failed on every full-variant run.
Take the audit-line versions (f00e62f) that assert the refusal:
test_approve_all, test_sign_uni_approve_liquidity_ETH, and the
erc20-approve-unlimited clear-sign flow. Adds common.firmware_at_least.
Firmware authenticates before showing the SignMessage confirm since
7.14.2; this alpha line still expected the old order. Take the audit-line
version (f00e62f), which gates the order on firmware_at_least.
The blocked-without-AdvancedMode case now uses a binary payload, so it
holds on every firmware. New 7.16.0 case: a SIWS-style text login signs
with AdvancedMode off (BitHighlander/keepkey-firmware#816).
…nonical-protoc

fix(protocol): regenerate bindings with the canonical protoc (unblocks firmware integration CI)
…-advanced-mode

test(solana): plain-text SignMessage signs without AdvancedMode
Brings keepkey/python-keepkey's consolidated harness line (PR keepkey#197 to
upstream master; contains master, hive wire-symbol fix, 7.15 audit fixes,
bitcoin-only capability gates) into fork alpha: 84 commits.

Resolution rules:
- device-protocol pin, .gitmodules and generated bindings: alpha. Upstream's
  dp 27d3fa1 lacks the ERC-7730 and dice-mode protocol (moving to it deletes
  ~590 proto lines); bindings stay canonical-protoc (py3.6/protobuf 3.17).
- Harness (conftest network guard, common, client capture): upstream. Its
  guard admits only the exact configured UDP emulator endpoints plus owned
  leases. Removed the duplicate screenshot-selection hook alpha had ported.
- generate-test-report.py: upstream, keeping --build-variant as an alias of
  --variant (firmware CI passes --build-variant), alpha's --section option,
  and alpha's uniswap-liquidity must-run floor (GH #516) + its bitcoin-only
  waiver.
- test_msg_resetdevice.py: dice-modes version (f00e62f); firmware alpha
  runs the consented MIXED/ONLY flow, reconcile still has the old one.
- test_storage_version_gate.py: alpha's framed-record before/after pair
  (refresh_crc + sector) — firmware alpha stores framed journal records.
- test_msg_solana_signtx.py: alpha's stake layouts (validated against
  firmware alpha in CI) and the plain-text SignMessage test.
- test_msg_eos_signtx.py: alpha's version-gated golden.
- Dropped the unused noncanonical taproot prevtx fixture (upstream fc12c6d).
- CI configs: alpha's firmware targets; upstream's SHA check and the
  printf fix for the backtick-in-echo summary bug, applied to all four.

Offline: test_tx_fixture_integrity, test_network_policy, protocol bindings
15 passed; tx_fixture_manifest --check clean.
The host-selected dice modes with on-device consent that firmware alpha
implements (MIXED, ONLY, bias gate, supports_dice_modes gating) plus the
report catalog entries for the new native dice tests. Bindings and the
device-protocol pin stay alpha's (canonical protoc; already carry the
dice fields). Dropped the requires_dice_modes copy alpha had ported.
Content was already on alpha under different commits; this records the
ancestry. Kept alpha's KK_EXPECTED_FIRMWARE_REVISION provenance check
(upstream's GITHUB_SHA is the synthetic PR merge ref) and alpha's
device-protocol pin.
Takes the CircleCI retirement (alpha's workflow already documents it as
retired). Keeps alpha's integration workflow, which runs against the fork
firmware candidates; this branch's version runs against a published
emulator image and so tests stale firmware by its own note.
Ancestry only, no tree change:
- feat/dice-modes: all 5 patches present via feat/dice-modes-rebased
- fix/defer-blind-sign-to-715: patch present
- feat/test-report-generator: the original March generator; superseded by
  the current scripts/generate-test-report.py
- pre-squash-197-backup: 3428e0b (storage-gate skip) already present via
  reconcile; 1f6c240 (skip the FOX-pool uniswap tests on the emulator)
  intentionally NOT taken -- those tests no longer hang, they assert the
  unlimited-approve refusal and are must-run (GH #516)
The upstream harness stopped capturing a screenshot on every wire Failure
(capture explicitly where firmware renders), so the "Home screen at the
refusal" frames I4/I6 declared no longer exist, and E17 now asserts the
unlimited-approve refusal (one AdvancedMode confirm) rather than a signed
approval. Take the entries from origin/merge/alpha-715, which already
describe these flows. Firmware CI captured exactly 1/3/4 frames.
The debug build raises one ButtonRequest per physical subpage and every
subpage of a word group reports the same reset_word; whether a group
spills to a second subpage depends on glyph widths. With a random
mnemonic this made the test flaky ("Invalid mnemonic, are words in
correct order?"): it passed and then failed on consecutive CI runs of
the same firmware. Same de-duplication test_msg_resetdevice already uses.
…ctations

8d3f4cc aligned these to keepkey/release/7.15 f3d9e058e, which is wrong
on the wire in both places:

- XRP: MemoData is Blob field 13 (0x7D). 0x72 is MessageKey, which
  rippled rejects inside a Memo object (ripple-binary-codec
  definitions.json). Every other firmware line, alpha included, emits 0x7D.
- Hive: memo_key is optional<> in account_update2 (op 43) in both dhive
  and hive-tx; it is mandatory only in legacy account_update (op 10).
  The builder dropped the presence byte, so firmware read 0x02 as a bool
  and failed with "malformed". Vectors 2/3 now carry a present memo key
  and must be refused as an authority change on every line.
Merge all keepkey/python-keepkey upstream branches into alpha
Emulator tests for keepkey-firmware feat/solana-schema-v2 (issue #828),
asserting what the OLED says rather than how many screens there are.

tests/oled_text.py reads text off a DebugLink frame. DebugLinkState.layout
is pixels only, so it renders the expected string with the firmware's own
title and body glyphs (tables generated from lib/board/font.c; `python
oled_text.py font.c` regenerates them), wraps it as draw_string_walk()
does, and requires every line whole and left-aligned on consecutive rows of
one screen. On recorded frames it rejects one-character changes, dropped
characters and prefixes of every checked string.

test_msg_solana_schema_v2.py:
- Certified (public Vault 501 certificate + the real Relay v1 delegate
  signature): a static System Transfer companion is reviewed ("Funding
  account", then "Send 0.002000000 SOL to <dest>?" under INSTR 2/2); with
  SetComputeUnitPrice the review shows "Fee payer <key>" and "Max priority
  fee 0.000200000 SOL" on consecutive FEE screens; a duplicate price or limit
  is refused with "Invalid priority fee". These skip when the emulator
  refuses the certificate because it was built without the alpha ClearSign
  root, and fail on any other refusal.
- Runtime (CI signer in slots 2 and 3): a Transfer companion is refused;
  on the real SoltoshiDICE join signed by slot 2, slot 2's SDICE definition
  shows "1000.000000 SDICE" with the mint beneath it for all three
  TOKEN_AMOUNTs, while slot 3's definition renders pixel-identical to none
  ("1000000000 base units of mint" + mint).
- ClearsignAttestor: a v2 TOKEN_AMOUNT arg costs exactly one extra screen,
  "Arg 1 token mint is / account #3", right after the arg, and the returned
  signature verifies over sha256(payload).

Each of these firmware mutations fails at least one test: certified reviews
not binding the priority fee, the attestor mint screen removed, the mint
dropped beside a trusted symbol, companion instructions not screened, the
fee computed without the explicit compute-unit limit (same screen count,
different text), and runtime token definitions accepted from any slot.
test(solana): KKSOLSC1 v2 schema reviews asserted by screen text
The job starts container kkemu-7143 and reads junit-7143.xml and
status-7143, but its test step still inspected kkemu-rc18 and wrote
junit-rc18.xml / status-rc18. docker inspect failed before pytest ran, so
the 7.14.3 gate reported red with zero tests executed.
e07a95e7d predates keepkey-firmware#824 (unknown-token consent review,
EIP-712 outermost-first dimensions, the release draw.c paging rule,
unlimited-approve refusal), which the suite on this head now expects.
Firmware CI already runs this python-keepkey head against b452f011c.
The real SoltoshiDICE "Blackjack join" (legacy, no lookup tables), re-keyed
so its fee payer is the emulator's key, signed through the certified path
with the deployed ClearSign Worker's material: the delegate signature over
the 154-byte KKSOLSC1 v2 schema, the public 501-scope certificate, and the
KeepKeySolanaTokenDef/2 definition of SDICE. None of it binds a
transaction. AdvancedMode stays off.

- every screen's text, in order: compute-unit limit, the Transfer companion
  (funding account, then 0.002 SOL to the session key), the certified
  signer, Round 86, Revision 980, Seat 1, Buy-in / Allowance / Max wager as
  1000.000000 SDICE over the full mint, the session key, Expires in 1 h,
  the sign prompt; then the ed25519 signature is verified over the message
- the real join sets no compute-unit price, so it has no Fee screens; a
  priced variant shows Fee payer and Max priority fee before the sign prompt
- no definition, or a one-byte-changed definition signature: raw base units
  beside the mint, identical frames, still signs
- AdvancedMode on: the same certified review frame for frame, no Blind Sign

Catalogued as S30-S33 (7.16.0+); S30 declares its 14 screens and shows them
all, in order, in the report.
The class gated only on the bitcoin-only variant, so on 7.14.3 all 13
tests hit "Unknown message" on their first message. No 7.14.x release
or 7.14.3 candidate carries the privacy engine; it ships from 7.15.0.

Gate on requires_firmware("7.15.0") (develop builds labelled 7.14.x
speak the draft protocol) plus a probe of ZcashSignPCZT, the session
opener. ZcashTransparentInput cannot be probed (required field, the
probe no-ops) and ZcashPCZTAction is refused outside a session (the
probe would skip on 7.15/7.16).
The 7.16.0 gate recorded the branch the fix first landed on, not the
firmware that has it. The eos_hashAuthorization() fix (fork #568/#563)
is on the 7.14.3, 7.15 and 7.16 release heads, so 7.14.3 fell through
to the old golden and failed on a correct digest.

Assert 5938294e... from 7.14.3 on. It matches two independent EOSIO
ABI serializers and same-shape mainnet updateauth transactions. The
old golden fb936ef1... hashed a phantom zero wait; older firmware now
skips the SLIP-48 half instead of asserting that known-bad digest.
…t skips

TestSolanaSchemaCertified skipped whenever the device refused the public
501-scope certificate ("Invalid certified Solana certificate"). Every 7.16
build embeds the ClearSign root, so that refusal is a regression, and the
skip turned it into a green run: a scope mutant turned all seven certified
tests into skips. setUp now fails with "7.16+ firmware must embed the
ClearSign root". The version and bitcoin-only skips are unchanged.

No other pyk test skips on a missing root.
test_relay_certified_v0_no_lookup_proof_reaches_signer_check already fails.

Documented: CERT_501 chains to the ALPHA root. Production gets its own
root key after the 7.15 re-release, not before, so the certified tier is
alpha-only. The S30 report text no longer says root-less builds skip.
…efused

An adversarial check found that deleting the delegate-attestation check
from the certified branch of fsm_msg_solana.h (fw 88ed0390c) left every
unit and pyk test green: that firmware signed the SoltoshiDICE join under
a schema renamed "Claim airdrop!" with a signature of 64 x 0x01, AdvancedMode
off, under the "KeepKey Vault / Signer A9531B9D" header.

Three refusals on the real join re-keyed to this device, with the SDICE
definition, so the schema proof is the only thing wrong. Each asserts the
exact Failure (code and message), that no screen was shown, and that no
signature came back:

- the delegate's schema signature with one byte changed:
  "Certified Solana schema does not match transaction"
- the instruction renamed (same length) under the original signature:
  "Certified Solana schema does not match transaction"
- no schema_signature: "Incomplete certified Solana ClearSign proof"

There is no case for a schema signed under another scope: the delegate
signs sha256(schema), which carries no scope, and the alpha root has
issued no public certificate for any scope but 501.

Proven on native kkemu builds of fw 88ed0390c: all pass on the control;
the first two fail on the mutant without the check ("signed after 14
screens"); the third guards the completeness check, which that mutant
keeps.
…ture

Adversarial mutants of fw 88ed0390c lib/firmware/fsm_msg_solana.h left
every pyk and unit test green:

- Sf passes the certificate's own scope instead of 501 (:882, :927).
- L passes a fixed 64 instead of msg->schema_signature.size (:928).

Both signed the SoltoshiDICE join after 14 screens. Two refusals now
cover them, on the real join re-keyed to this device with the SDICE
definition:

- test_certified_wrong_scope_certificate_refused: the delegate's real
  schema signature under CERT_SCOPE1, the public alpha EVM-scope (scope 1)
  certificate for the same delegate 0342f5f9. The delegate signs
  sha256(schema), which carries no scope, so only the scope check refuses
  it: "Invalid certified Solana certificate".
- test_certified_short_schema_signature_refused: SOLTOSHI_SCHEMA_SIG[:63].
  Its last byte is 0x00, so the zeroed 64-byte field holds the real
  signature again: "Certified Solana schema does not match transaction".

CERT_SCOPE1 is a fixed constant, fetched once on 2026-09-19 from POST
https://keepkey-clearsign.bithighlander.workers.dev/v1/evm/schema (the
chainId 1 Relay bridgeDeposit shape): bytes [1:140] of its signedPayload.
Nothing is fetched at test time.

This also corrects the comment that said the alpha root had issued no
public certificate for any scope but 501.

Proven on native kkemu builds of fw 88ed0390c (KK_FORCE_UDP=1):
test_msg_solana_schema_v2.py passes 15/15 on the control. The scope test
fails on Sf and on S (the scope comparison dropped from clearsign_root.c).
The length test fails on L. Each failure is "signed after 14 screens".
All six test_msg_solana_*.py files pass 68/68 on the control.
…ified

test(solana): certified SoltoshiDICE join asserted screen by screen
651f2a462 merges keepkey-firmware#829 (KKSOLSC1 schema v2), which the
certified and runtime schema-v2 tests on this head require. b452f011c
predates it and fails those 5 tests while reporting the same 7.16.0.
@BitHighlander
BitHighlander merged commit aced59d into master Sep 19, 2026
5 of 6 checks passed
@BitHighlander
BitHighlander deleted the alpha branch September 19, 2026 20:51
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.

2 participants