Skip to content

release: reconcile 7.15 tests after 7.14.3 - #65

Open
BitHighlander wants to merge 25 commits into
fix/7143-bitcoin-only-test-gatefrom
reconcile/7.15-after-7.14.3
Open

release: reconcile 7.15 tests after 7.14.3#65
BitHighlander wants to merge 25 commits into
fix/7143-bitcoin-only-test-gatefrom
reconcile/7.15-after-7.14.3

Conversation

@BitHighlander

Copy link
Copy Markdown
Owner

Summary

  • carry the green 7.14.3 Bitcoin-only security/integration gate forward
  • merge the canonical release/7.15-audit-fixes companion line
  • preserve the canonical 7.15 capability gates, report validation, and signing coverage

Dependency order

This fork PR depends on #62 and is the companion pin intended for firmware 7.15 after firmware #627.

Validation

  • python3 -m compileall -q keepkeylib tests scripts/generate-test-report.py
  • fixture manifest SHA-256: cafd5d9eb14d0186680380bc7c486092d94a547767ec9a5e852ad3a915178d55
  • report/Zcash static tests: 14 passed
  • exact-head CI is required before the firmware pin is finalized

No upstream PR, signing, tagging, publishing, or release action is part of this PR.

BitHighlander and others added 25 commits August 22, 2026 19:01
The job ran the firmware's firmware-unit suite alongside this repo's tests and
failed if EITHER reported non-zero. No change in this repository can affect
firmware C++, and the firmware repo already runs that suite in its own CI, so
the only thing it contributed was failing python-keepkey for reasons no python
change caused.

It is failing that way right now: this branch trims the built-in token table,
which requires a matching firmware change to tokens.def. The job clones
firmware master, so it cannot go green until that change reaches master -- a
release away -- even though this repo's own suite passes 417/0.

Also hardens the verdict. The old check was

  [ "$(cat test-reports/python-keepkey/status)$(cat .../firmware-unit/status)" = "00" ]

which printed 'cat: ... No such file or directory' and compared an empty string
whenever a container died before writing its status -- so a crashed run could
not report a verdict at all. Missing status is now an explicit failure.
Forward ThorchainMsgSend.denom on firmware 7.15+, retain the fail-closed RUNE-only path on older firmware, and add offline and device-path coverage. Add regression tests for the reviewed EIP-712 array-order and signed ABI integer fixes.
The comment claimed firmware enforces uosmo-only on direct OsmosisMsgAck
traffic. It does not. Since 7.14.2 (firmware c9dccf68)
osmosis_signTxUpdateMsgSend escapes the host-supplied denom straight into
the signed Amino document, and the only remaining strcmp against "uosmo"
in firmware selects the display exponent. The raw-wire test
test_osmosis_send_denom_is_committed_to_the_signature is correct as
written; the comment was the stale artifact.

State the real reason the host check stays: this helper is not
version-gated, and firmware older than 7.14.2 hardcoded uosmo in the
serializer, so forwarding a non-uosmo denom there would silently sign a
uosmo transfer the caller never asked for.
zcash_sign_pczt count-checked the Orchard signatures but accepted the
deferred ZcashTransparentSigned response unconditionally. A device that
omitted the message entirely, or returned fewer signatures than there were
transparent inputs, still returned success and handed the caller a
transaction whose transparent inputs can never be spent.

Require one signature per transparent input, and reject a present-but-empty
entry. Transparent signatures are DER ECDSA, so there is no fixed length to
check the way the 64-byte RedPallas signatures are checked. Both checks run
after the Failure and response-type arms so a device-reported error still
surfaces its own message.

Adds four scripted-flow tests: short list, omitted message, empty entry,
and the matching-count case. The three negative tests fail against the
previous client.
tests/test_msg_bitcoin_only_variant.py calls requires_bitcoinOnly() in
setUp(), and ci.yml built and started only the regular emulator. All eleven
product-boundary tests therefore ran as skips in the one required
integration job, so the advertised bitcoin-only coverage was never executed
by any check. The module docstring also claimed "NOTHING HERE SKIPS",
which the unconditional gate had already made false.

Add an integration-btc job that builds the emulator with
-DKK_BITCOIN_ONLY=ON via the Dockerfile's existing coinsupport build arg,
asserts features.firmware_variant is EmulatorBTC before pytest runs, and
fails when any test in the module skips -- pytest exits 0 on a fully
skipped module, so a green run proves nothing unless the skip count is
zero. Rewrite the docstring to describe the real scope and name the job the
file now depends on.
setUp() inherits a device wipe and never loads a seed, so every SignMessage
in this file was refused with Failure_NotInitialized before confirm_bytes()
was ever reached. _sign_message_screens() caught the CallException and
returned None, and _assert_distinguishable() returned early on None without
executing its assertNotEqual.

Four tests therefore reported PASS having exercised zero display logic, and
the fifth -- written specifically to catch exactly this -- skipped ITSELF on
the same condition. Reintroducing the NUL-truncation bug, so that
b"benign login\x00 AND APPROVE TRANSFER OF ALL FUNDS" renders identically to
b"benign login" while the signature covers all 46 bytes, would leave every
test, the JUnit validation and the ci-gate green. The release PDF certifies
this coverage as passing.

Three changes:

- setUp() loads a seed, so the device actually renders the screens under test.

- A refusal no longer silently satisfies the property. It asserts the device
  is initialized first -- a refusal only means something from a device that
  could have signed and chose not to -- and otherwise fails by name. An
  intended refusal should be asserted explicitly, not inferred from None.

- The anti-vacuity control no longer skips itself. That test exists to prove
  the rest of the file is not vacuous, so skipping when the device will not
  sign is the one failure mode it cannot be allowed to have.
The Ironwood fixture set orchard_digest to b'\x00' * 32 -- arbitrary filler --
with a comment explaining that the field "only feeds the locally derived
sighash". That comment described the vulnerability as if it were the design.

A v6 transaction streams and verifies only its Ironwood actions, so an
orchard_digest other than the empty-bundle value names a bundle the device
never inspected and still commits to in the sighash it signs. Exploitable:
point it at a real Orchard bundle spending one of this seed's notes, reuse the
alpha of an approved Ironwood action so rk is byte-identical, and the single
RedPallas signature the device emits verifies in BOTH bundles, because
verification is [s]G = R + [H(R||rk||M)]rk and rk and M are shared. The
malicious bundle's valueBalance never reaches the device's fee arithmetic.

The fixture now uses the ZIP-244 value, BLAKE2b-256 of the empty string
personalized "ZTxIdOrchardHash", which the firmware requires. That also fixes
test_pool_selection_is_honoured, which was reaching the new refusal before it
could reach the commitment mismatch it asserts.

Adds Z26: an Ironwood request carrying a non-empty Orchard bundle must be
refused. Registered in SECTIONS so it actually runs -- an unregistered test is
not in the CI filter and would never execute.
MultisigRedeemScriptType.signatures is declared max_size:73, so the decoder
accepts 73 bytes, but a DER-encoded ECDSA signature is at most 72: 0x30 len,
then two 0x02-tagged integers of at most 33 bytes each.

The witness serializer appended the sighash byte AT signatures[i].size, so a
73-byte value wrote one past the end of bytes[73] -- onto signatures[i+1].size
for i < 14, which can revive a slot the host deliberately left empty and
change the witness stack after the user reviewed it, or onto has_m at i == 14.

A declared max_size is a DECODER bound and never a runtime one. Registered as
Z27 so it is in the CI filter and actually runs.
…ability-gates

test(bitcoin-only): gate unsupported 7.15 handlers
# Conflicts:
#	scripts/generate-test-report.py
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