From a4cbefbc13c3c32222d5eec7ef680ee501daf0f9 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 17:41:21 -0300 Subject: [PATCH 01/21] feat(zcash): complete clear-signing flow --- deps/device-protocol | 2 +- deps/python-keepkey | 2 +- .../zcash-clearsign-handoff.md | 174 ++++ docs/coin-integration/zcash-pczt-clearsign.md | 322 ++++++ include/keepkey/firmware/fsm.h | 1 + include/keepkey/firmware/zcash.h | 113 +++ .../keepkey/transport/messages-zcash.options | 9 +- lib/firmware/fsm_msg_zcash.h | 929 ++++++++++++++---- lib/firmware/messagemap.def | 4 +- lib/firmware/zcash.c | 423 ++++++++ lib/transport/CMakeLists.txt | 80 +- unittests/firmware/zcash.cpp | 334 +++++++ 12 files changed, 2163 insertions(+), 230 deletions(-) create mode 100644 docs/coin-integration/zcash-clearsign-handoff.md create mode 100644 docs/coin-integration/zcash-pczt-clearsign.md diff --git a/deps/device-protocol b/deps/device-protocol index 870b8eaf5..6ec974eef 160000 --- a/deps/device-protocol +++ b/deps/device-protocol @@ -1 +1 @@ -Subproject commit 870b8eaf5bcad0e62231bc0947207d6863c0553e +Subproject commit 6ec974eef1fecb713be0916436ec31fefe4f094e diff --git a/deps/python-keepkey b/deps/python-keepkey index 1ed089434..c9c976866 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 1ed089434036fb61b9ca3e5855ea116311b65f52 +Subproject commit c9c9768662a21ff1f0b5ff12a1f89bf73f7c5d0b diff --git a/docs/coin-integration/zcash-clearsign-handoff.md b/docs/coin-integration/zcash-clearsign-handoff.md new file mode 100644 index 000000000..7d1114078 --- /dev/null +++ b/docs/coin-integration/zcash-clearsign-handoff.md @@ -0,0 +1,174 @@ +# Zcash Clear-Signing Handoff + +Date: 2026-05-20 +Branch: `feature/clearsign-txs` +Base: `origin/release/7.15.0` +Repo: `/Users/highlander/WebstormProjects/keepkey-stack/projects/keepkey-firmware` + +## Goal + +Prepare a PR to the 7.15.0 firmware branch for Zcash PCZT clear-signing work, +then test the clear-signing flow on device. + +Scope is Orchard plus transparent clear-signing. Sapling is explicitly out of +scope for this branch. + +## Decisions + +- Do not accept host-provided action sighashes. Firmware must assemble the + signing digest from checked transaction components. +- Sapling is out of scope. Any provided `sapling_digest` is rejected with + `Sapling not supported`; firmware uses the ZIP-244 empty Sapling digest + internally. +- Header digest is no longer blindly trusted. The host must send plaintext + `tx_version`, `version_group_id`, `branch_id`, `lock_time`, and + `expiry_height`; firmware recomputes ZIP-244 `header_digest` and rejects on + mismatch. +- Transparent plaintext streaming is wired for standard P2PKH/P2SH + transparent scripts. Firmware recomputes the transparent digest from streamed + outputs and inputs before emitting any transparent or Orchard signature. +- Orchard privacy outputs are displayed from plaintext `recipient = d || pk_d`, + `value`, and `rseed` only after firmware recomputes `cmx` and verifies it + matches the action commitment. +- The device computes `fee = transparent_in - transparent_out + + orchard_value_balance`, compares it to the requested fee, and requires final + fee confirmation before signatures are returned. +- `wh00hw/libzcash-orchard-c` is used as implementation guidance and test-vector + shape, not as a wholesale dependency. + +## Implemented + +- Added `docs/coin-integration/zcash-pczt-clearsign.md` with threat model, + Keystone comparison, `libzcash-orchard-c` review, crypto inventory, and phased + plan. +- Added clear-signing request policy helpers in + `include/keepkey/firmware/zcash.h` and `lib/firmware/zcash.c`. +- Added ZIP-244 helpers: + - `zcash_compute_header_digest` + - `zcash_compute_transparent_digest` + - `zcash_compute_transparent_sighash_digest` +- Updated `fsm_msg_zcash.h` to: + - reject legacy host-only sighash signing + - require component digests and Orchard metadata + - require plaintext header fields + - verify `header_digest` from plaintext header fields + - reject any Sapling component + - always use the empty Sapling digest internally + - verify Orchard digest from streamed action fields before returning signatures +- Updated Zcash protocol definitions with plaintext header fields. +- Updated Zcash protocol definitions with transparent output streaming, + transparent input plaintext fields, and transparent ack/signed messages. +- Updated `fsm_msg_zcash.h` to: + - request transparent outputs before inputs + - display standard transparent output address/amount before signatures + - reject unknown transparent scripts + - reject host-provided transparent input sighashes + - verify `transparent_digest` from streamed transparent plaintext + - derive per-input transparent sighashes locally +- Added Orchard output clear-signing metadata to `ZcashPCZTAction`: + `recipient` and `rseed`. +- Added firmware Orchard output verification/display: + - recompute `cmx` from `recipient`, `value`, `rseed`, and action nullifier + - reject `cmx` mismatch before signing + - encode the raw Orchard receiver as a ZIP-316 Orchard-only Unified Address + - display the privacy address and amount on-device +- Added final fee verification/display from transparent totals plus + `orchard_value_balance`. +- Updated python-keepkey client/tests to stream transparent outputs/inputs and + expect `ZcashTransparentSigned`. +- Updated python-keepkey test client/tests to send header fields and test: + - legacy sighash rejection + - header digest mismatch rejection + - Sapling digest rejection + - Orchard digest field requirements + - transparent digest mismatch rejection + - host-provided transparent sighash rejection + - Orchard recipient/value metadata mismatch rejection + +## Verified + +Commands run from the firmware repo: + +```sh +cmake --build build --target zcash-crypto-unit +PATH=/private/tmp/kk-python-shim:$PATH cmake --build build --target kkfirmware +PATH=/private/tmp/kk-python-shim:$PATH cmake --build build --target kkfirmware.keepkey +build/bin/zcash-crypto-unit +git diff --check +PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python python3 -c "import sys; sys.path.insert(0, 'deps/python-keepkey'); from keepkeylib import messages_zcash_pb2 as z; a=z.ZcashPCZTAction(index=0, recipient=b'1'*43, rseed=b'2'*32, value=1); assert a.HasField('recipient') and a.HasField('rseed'); print('zcash orchard metadata protobuf smoke ok')" +PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python python3 -c "import sys; sys.path.insert(0, 'deps/python-keepkey'); from keepkeylib import mapping; from keepkeylib import messages_zcash_pb2 as z; assert mapping.get_type(z.ZcashTransparentOutput(index=0)) == 1310; assert mapping.get_type(z.ZcashTransparentAck(next_input_index=0)) == 1311; assert mapping.get_type(z.ZcashTransparentSigned(signatures=[b'0'])) == 1307; msg=z.ZcashSignPCZT(n_transparent_outputs=1,n_transparent_inputs=1); assert msg.HasField('n_transparent_outputs'); print('zcash transparent protobuf smoke ok')" +PYTHONPYCACHEPREFIX=/private/tmp/kk-pycache python3 -m py_compile deps/python-keepkey/tests/test_msg_zcash_sign_pczt.py +``` + +Results: + +- Full `zcash-crypto-unit`: 56 tests passed. +- `kkfirmware`: builds. +- `kkfirmware.keepkey`: builds. +- `git diff --check`: clean. +- Python Zcash Orchard metadata protobuf smoke test passes. +- Python Zcash transparent protobuf smoke test passes. +- Python test file syntax check passes with bytecode cache redirected to + `/private/tmp/kk-pycache`. + +## Published Dependencies + +- `BitHighlander/device-protocol` + `feat/zcash-clearsign-protocol` -> `6ec974e` +- `BitHighlander/python-keepkey` + `feat/zcash-clearsign-tests` -> `c9c9768` +- Firmware submodules now point at those commits: + - `deps/device-protocol` -> `6ec974e` + - `deps/python-keepkey` -> `c9c9768` + +The python-keepkey PDF report now includes the 7.15 Zcash clear-signing section +with the new digest rejection, transparent streaming, cmx binding, and +privacy-output tests. Its screenshot filter selects the positive display flows: + +- `test_multi_action_device_sighash` +- `test_signatures_are_64_bytes` +- `test_transparent_shielding_single_input` +- `test_transparent_shielding_multiple_inputs` + +Screenshot capture is still driven by `KEEPKEY_SCREENSHOT=1` and ButtonRequest +callbacks; `scripts/generate-test-report.py --screenshots ` embeds the +captured `btn*.png` frames for tests with screenshot labels. + +## Local Tooling Notes + +- Nanopb generation invokes `env python`. This machine only had `python3`, so a + temporary shim was created: + `/private/tmp/kk-python-shim/python -> /opt/homebrew/bin/python3`. +- `PATH=/private/tmp/kk-python-shim:$PATH` is needed for local firmware rebuilds + unless a real `python` executable is installed. +- Local `protoc` is `libprotoc 34.1`; regenerating + `keepkeylib/messages_zcash_pb2.py` produced a noisy modern-style diff. The + checked-in python-keepkey branch removes the generated runtime-version guard + so it imports with the local protobuf runtimes used during verification. +- `PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python` is useful for local imports + with the older checked-in protobuf files. + +## Remaining CI/HW Follow-up + +- Run a hardware/emulator signing flow if available, because the Python tests + exercise the client shape but were not run against a device in this handoff. +- CI should run the pushed firmware and python-keepkey branches and produce the + PDF report with screenshots. +- Keep `.claude/` out of the PR unless explicitly requested. + +## Device Test Plan + +- Flash `kkfirmware.keepkey` build to a test device. +- Confirm happy-path Orchard-only PCZT signing. +- Confirm Sapling PCZT is rejected. +- Confirm header digest mismatch is rejected. +- Confirm Orchard digest mismatch is rejected. +- Confirm transparent output address/amount appears on device. +- Confirm transparent output mutation causes digest mismatch/rejection. +- Confirm transparent input sighash mutation cannot produce a signature. +- Confirm Orchard privacy output UA/amount appears on device. +- Confirm Orchard recipient, value, rseed, or cmx mutation is rejected before + signing. +- Confirm fee mismatch is rejected. +- Confirm final computed fee confirmation appears before signatures are + returned. diff --git a/docs/coin-integration/zcash-pczt-clearsign.md b/docs/coin-integration/zcash-pczt-clearsign.md new file mode 100644 index 000000000..e03b3b612 --- /dev/null +++ b/docs/coin-integration/zcash-pczt-clearsign.md @@ -0,0 +1,322 @@ +# Zcash PCZT Clear Signing + +**Status:** implemented on `feature/clearsign-txs`. +**Base:** `release/7.15.0`. +**Scope:** Orchard PCZT signing. + +## Threat Model + +The firmware must not sign a host-provided Orchard sighash by itself. If the +companion app can choose the sighash directly, a compromised companion can turn +the hardware wallet into a blank-check signer: the user confirms one summary, +but the signature authorizes a different transaction digest. + +The firmware therefore has to build the signing digest from transaction data it +can validate. The current PCZT protocol does this in layers: + +1. The host sends ZIP-244 component digests and Orchard bundle metadata. +2. The device assembles the final ZIP-244 sighash from those component digests. +3. The device recomputes the Orchard digest from streamed action fields. +4. Signatures are returned only after the recomputed Orchard digest matches the + Orchard digest used in the device-computed sighash. + +## Firmware Policy + +`ZcashSignPCZT` is rejected before user confirmation unless it includes: + +- `header_digest`, exactly 32 bytes. +- Plaintext header fields: `tx_version`, `version_group_id`, `branch_id`, + `lock_time`, and `expiry_height`. Firmware recomputes ZIP-244 + `header_digest` from these fields and rejects on mismatch. +- `orchard_digest`, exactly 32 bytes. +- Orchard flags, value balance, and 32-byte anchor. +- A 32-byte transparent digest when transparent inputs or outputs are present. +- Optional transparent digest, if present, must be exactly 32 bytes. + +Sapling is out of scope for this signing path. Any host-provided +`sapling_digest` is rejected; firmware uses the ZIP-244 empty Sapling digest +internally. + +`ZcashPCZTAction` is rejected unless each action includes the fields needed to +recompute the Orchard digest: + +- `nullifier`, `cmx`, `epk`, `cv_net`, and `rk`, each 32 bytes. +- `enc_compact`, 52 bytes. +- `enc_memo`, 512 bytes. +- non-empty `enc_noncompact`. +- `out_ciphertext`, 80 bytes. +- `value`, 43-byte `recipient` (`d || pk_d`), and 32-byte `rseed` for trusted + Orchard output display. + +The legacy path where `ZcashPCZTAction.sighash` was accepted as the signing +digest is intentionally rejected. + +## What Is Verified + +The device now verifies `header_digest` from plaintext header fields and +recomputes `transparent_digest` from streamed transparent inputs/outputs before +emitting any transparent or Orchard signature. Sapling is not accepted in this +path. For shielded-only Orchard transactions, the transparent digest defaults to +the ZIP-244 empty transparent digest, so there is no host-provided transparent +component. + +The device verifies the Orchard action digest from the action plaintext fields +available in PCZT. Each action must also carry the plaintext Orchard output +metadata needed for trusted display: raw receiver `recipient = d || pk_d`, +`value`, and `rseed`. Firmware recomputes the action `cmx` from that metadata +and the action nullifier before displaying the ZIP-316 Orchard-only Unified +Address and amount. + +The device also computes the fee as: + +```text +fee = transparent_input_total - transparent_output_total + orchard_value_balance +``` + +The computed fee must match the requested fee and must be confirmed on-device +before any final signatures are returned. + +## UI Behavior + +Current signing screens show: + +- Shielded-only: total amount, fee, and Orchard action count. +- Transparent shielding: total amount, fee, transparent input count, and + transparent output count, and Orchard action count. +- Transparent input signing: per-input amount and BIP-44 path validation. +- Transparent outputs: each standard P2PKH/P2SH t-address and amount. +- Orchard outputs: each ZIP-316 Orchard-only Unified Address and amount after + note commitment binding. +- Final fee confirmation: computed fee after digest/output verification. + +## Outputs + +If we can derive a digest from plaintext transaction fields, we should do so for +outputs too. + +Transparent outputs are streamed as recipient scripts and values. Firmware +computes the transparent digest, displays transparent destination/address and +amount, and rejects non-standard scripts until an explicit raw-script review +policy exists. + +Orchard outputs are displayed from the supplied raw receiver/value/rseed +metadata only after firmware recomputes `cmx = NoteCommit(...)` and verifies it +matches the action `cmx`. This binds the displayed privacy recipient and amount +to the signed Orchard action. + +## libzcash-orchard-c Review + +Reviewed: https://github.com/wh00hw/libzcash-orchard-c + +This repo is applicable as implementation guidance, not as a wholesale firmware +dependency. It is a pure C11 static library under MIT, but it overlaps heavily +with primitives already present in this firmware tree: BLAKE2b, Pallas, +Sinsemilla, RedPallas, secp256k1, BIP32/BIP39, Base58, and ZIP-316 helpers. The +useful part for KeepKey is its transaction-signing shape: + +- Separate ZIP-244 `T.2 transparent_digest` from ZIP-244 `S.2` per-input + transparent signature digest. These are different commitments and must not be + collapsed into a single helper. +- Stream transparent inputs and outputs into independent BLAKE2b component + hashers: prevouts, sequences, outputs, amounts, scripts, and per-input + `txin_sig_digest`. +- Track transparent input/output value totals while hashing so the device can + compute `fee = transparent_in - transparent_out + orchard_value_balance` + locally and show that fee on-device. +- Treat Sapling as unsupported. A Sapling component would be a hidden value sink + until the firmware has Sapling parsing and display, so this firmware path + rejects host-provided Sapling data and uses the ZIP-244 empty Sapling digest. +- Capture every transparent output `(value, script_pubkey)` and render standard + P2PKH/P2SH scripts as Zcash t-addresses for user review. +- For Orchard outputs, `cmx` binding is the first target: require plaintext + `(d, pk_d, value, rseed)` metadata and recompute the note commitment against + the action `cmx`. The stronger follow-up is memo binding: recompute + `enc_ciphertext` and `epk` from `(recipient, value, rseed, memo)` using + Orchard KDF + ChaCha20-Poly1305. + +Reference role: KeepKey uses the transaction-signing structure and test-vector +shape while keeping the existing firmware primitives and protocol surfaces. + +Key files reviewed: + +- https://github.com/wh00hw/libzcash-orchard-c/blob/main/include/zip244.h +- https://github.com/wh00hw/libzcash-orchard-c/blob/main/src/zip244.c +- https://github.com/wh00hw/libzcash-orchard-c/blob/main/include/orchard_signer.h +- https://github.com/wh00hw/libzcash-orchard-c/blob/main/src/orchard_signer.c +- https://github.com/wh00hw/libzcash-orchard-c/blob/main/include/base58.h +- https://github.com/wh00hw/libzcash-orchard-c/blob/main/SECURITY.md + +## Updated Implementation Plan + +### Phase 1: digest helpers and policy + +Status: implemented for header/transparent helpers and clear-signing policy. + +- Require component digests and Orchard metadata before signing. +- Reject the legacy host-only action `sighash` path. +- Compute the ZIP-244 root sighash on-device from component digests. +- Verify the Orchard digest from streamed action fields. +- Add pure helpers for: + - `header_digest` from plaintext header fields. + - `transparent_digest` from plaintext transparent inputs/outputs. + - per-input transparent `SIGHASH_ALL` digest. + +### Phase 2: plaintext header and transparent streaming + +Status: implemented. + +- Extend the protocol with plaintext header fields and verify + `header_digest` locally. Implemented. +- Extend transparent input messages with raw ZIP-244 digest fields: + `prevout_txid`, `prevout_index`, `sequence`, `amount`, and `script_pubkey`. +- Add transparent output streaming: + `index`, `amount`, and `script_pubkey`. +- Compute `transparent_digest` locally and compare it to the companion-provided + digest during the migration period. +- Compute per-input transparent sighashes locally before ECDSA signing, instead + of signing `ZcashTransparentInput.sighash`. +- Track transparent input and output totals, compute the fee, compare it to the + requested fee, and display the computed fee. +- Reject transactions with transparent outputs that cannot be rendered on-device + until the UI has an explicit "unknown script" review policy. + +### Phase 3: transparent output UI + +Status: implemented for standard P2PKH/P2SH scripts. + +- Add a Zcash transparent script renderer for standard P2PKH and P2SH: + - mainnet P2PKH: `t1` + - mainnet P2SH: `t3` + - testnet P2PKH: `tm` + - testnet P2SH: `t2` +- Display each transparent output address and amount on the trusted screen. +- Require every displayed transparent output and the computed fee to be + confirmed before any signature is produced. + +### Phase 4: stronger Orchard output metadata binding + +Status: implemented for recipient/value display via `cmx` binding. + +- Extend `ZcashPCZTAction` with raw Orchard output metadata: + `recipient` (`d || pk_d`), `rseed`, and explicit output value. +- Recompute Orchard note commitment `cmx` from `(d, pk_d, value, rho, rseed)`, + where `rho` is the action nullifier, and reject on mismatch. +- Display the recipient as a ZIP-316 Orchard-only UA and display the output + value. Require per-output confirmation. +- Memo display remains a separate hardening step: add ChaCha20-Poly1305 support + if it is not linked into the firmware image, then recompute `enc_ciphertext` + and `epk` for memo binding. +- Render empty/text/opaque memos on-device once memo binding is available. + +## Crypto Library Inventory + +Already available in this firmware tree: + +- BLAKE2b with personalization. +- AES-256. +- Pallas field and point arithmetic. +- Pallas SWU / group hash support. +- Sinsemilla / Orchard IVK support. +- RedPallas signing. +- ZIP-316 Orchard-only unified-address helpers. +- Zcash transparent Base58Check plus standard P2PKH/P2SH script-to-address + rendering. +- ChaCha20-Poly1305 source exists under trezor-crypto, but it is currently not + linked into the firmware crypto target. Memo binding will need that target + wiring plus Orchard note-encryption KDF glue. + +No new primitive is required for the implemented PCZT clear-signing policy. The +remaining optional hardening work is parsing and digest construction for more +transaction components: + +- Sapling parsing only if Sapling is ever intentionally added to this firmware + path; current policy is to reject it. +- Orchard memo binding/display by recomputing note encryption from + recipient/value/rseed/memo. + +## Keystone Comparison + +Keystone is a useful architecture comparison, but the audit alone is not enough +evidence. I inspected the local Keystone firmware repo: + +- Path: `/Users/highlander/keepkey/keystone3-firmware` +- Branch: `master` +- Commit: `2a48ba022ac24d3b343fa4b9e59251a5de3e1160` + +The actual Keystone signing path does derive the signing digest from PCZT data: + +- `rust/rust_c/src/zcash/mod.rs::sign_zcash_tx` extracts a `ZcashPczt` UR and + calls `app_zcash::sign_pczt`. +- `rust/apps/zcash/src/pczt/sign.rs::sign_pczt` builds a low-level PCZT signer, + then calls `pczt_ext::sign_transparent` and/or `pczt_ext::sign_orchard`. +- `rust/zcash_vendor/src/pczt_ext.rs::shielded_sig_commitment` constructs the + ZIP-244-style commitment from locally computed component digests: + `digest_header`, `transparent_sig_digest`, `digest_sapling`, and + `digest_orchard`. +- `digest_orchard` recomputes the Orchard digest from action fields: + nullifier, cmx, ephemeral key, encrypted memo/ciphertext slices, cv_net, rk, + out ciphertext, bundle flags, value balance, and anchor. +- `transparent_sig_digest` computes transparent prevouts, amounts, scripts, + sequence, outputs, and per-input data for `SIGHASH_ALL`. + +Keystone still has broader in-firmware PCZT parsing and Orchard output +decryption support. KeepKey now covers the same no-bare-sighash signing rule for +the implemented scope: header digest, transparent digest, Orchard digest, +displayed transparent outputs, displayed Orchard receiver/value metadata, and +the miner fee are all verified before signatures are released. + +Keystone also does more UI-side output parsing than our current firmware: + +- `rust/apps/zcash/src/pczt/parse.rs::parse_orchard_output` tries to decrypt + Orchard outputs with external/internal OVKs, validates decoded recipient data, + verifies a supplied `user_address` matches the decoded Orchard receiver, and + treats undecryptable non-zero Orchard outputs as invalid. +- `src/ui/gui_chain/multi/gui_zcash.c::GuiZcashOverviewTo` displays parsed + output value, address, change tag, and memo. +- The checker validates Orchard `cv_net`, value balance, nullifier/rk for owned + spends, and note commitment consistency before the UI/sign flow. + +This gives us a concrete target, not just an FYI: + +1. KeepKey branch: reject bare host sighashes, compute the final sighash from + required component digests, verify the Orchard digest from streamed action + fields, and compute header/transparent digests from streamed plaintext. +2. Output UI: transparent outputs are shown directly from verified script/value. + Orchard outputs are shown from directly supplied PCZT receiver/value/rseed + metadata that is cryptographically checked against the action `cmx`. +3. Remaining parity step: memo binding/display by recomputing Orchard note + encryption from recipient/value/rseed/memo. + +Sources: + +- Local Keystone firmware at the commit above. +- Public audit context: + https://leastauthority.com/wp-content/uploads/2025/03/Least-Authority-ZCG-Keystone-Hardware-Wallet-Final-Audit-Report.pdf + +## Tests + +The TDD coverage for this policy lives in: + +- `unittests/firmware/zcash.cpp` +- `deps/python-keepkey/tests/test_msg_zcash_sign_pczt.py` + +The firmware unit tests cover the pure policy helper, ZIP-244 digest helpers, +transparent digest/sighash helpers, Orchard receiver encoding, and Orchard note +commitment recomputation. The Python protocol tests cover the emulator-facing +behavior: legacy host-sighash requests are rejected, verified PCZT requests +sign, missing Orchard action digest fields abort signing, transparent digests +must match streamed plaintext, host transparent sighashes are rejected, and +Orchard recipient/value tampering is rejected. + +Current local verification: + +- `build/bin/zcash-crypto-unit` passes 56 tests, including: + - `ComputeHeaderDigest_FromPlaintextFields` + - `ComputeTransparentDigest_DistinctFromPerInputSighash` + - `ComputeTransparentDigest_EmptyBundle` + - `ComputeTransparentSighash_RejectsUnsupportedRequest` + - `OrchardNoteCommitment_KnownVector` + - `OrchardReceiverToUnifiedAddress_KnownVector` +- `cmake --build build --target kkfirmware` passes. +- `cmake --build build --target kkfirmware.keepkey` passes. +- `git diff --check` is clean. diff --git a/include/keepkey/firmware/fsm.h b/include/keepkey/firmware/fsm.h index b35b78eb6..41d5da567 100644 --- a/include/keepkey/firmware/fsm.h +++ b/include/keepkey/firmware/fsm.h @@ -129,6 +129,7 @@ void fsm_msgSolanaSignMessage(const SolanaSignMessage* msg); void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg); void fsm_msgZcashPCZTAction(const ZcashPCZTAction* msg); void fsm_msgZcashGetOrchardFVK(const ZcashGetOrchardFVK* msg); +void fsm_msgZcashTransparentOutput(const ZcashTransparentOutput* msg); void fsm_msgZcashTransparentInput(const ZcashTransparentInput* msg); void fsm_msgZcashDisplayAddress(const ZcashDisplayAddress* msg); diff --git a/include/keepkey/firmware/zcash.h b/include/keepkey/firmware/zcash.h index b8bf15f38..12c6f5aa6 100644 --- a/include/keepkey/firmware/zcash.h +++ b/include/keepkey/firmware/zcash.h @@ -41,6 +41,67 @@ typedef struct { uint8_t dk[32]; /* Diversifier key */ } ZcashOrchardKeys; +typedef struct { + bool has_header_digest; + size_t header_digest_size; + bool has_transparent_digest; + size_t transparent_digest_size; + bool has_sapling_digest; + size_t sapling_digest_size; + bool has_orchard_digest; + size_t orchard_digest_size; + bool has_orchard_flags; + uint32_t orchard_flags; + bool has_orchard_value_balance; + bool has_orchard_anchor; + size_t orchard_anchor_size; + bool has_header_fields; + uint32_t n_transparent_inputs; + uint32_t n_transparent_outputs; +} ZcashPCZTSigningRequestMeta; + +typedef enum { + ZCASH_PCZT_SIGNING_REQUEST_OK = 0, + ZCASH_PCZT_SIGNING_REQUEST_MISSING_TX_DIGESTS, + ZCASH_PCZT_SIGNING_REQUEST_INVALID_DIGEST_SIZE, + ZCASH_PCZT_SIGNING_REQUEST_MISSING_HEADER_FIELDS, + ZCASH_PCZT_SIGNING_REQUEST_UNSUPPORTED_SAPLING_COMPONENT, + ZCASH_PCZT_SIGNING_REQUEST_MISSING_ORCHARD_METADATA, + ZCASH_PCZT_SIGNING_REQUEST_MISSING_TRANSPARENT_DIGEST, +} ZcashPCZTSigningRequestStatus; + +typedef struct { + const uint8_t* prevout_txid; + uint32_t prevout_index; + uint32_t sequence; + uint64_t value; + const uint8_t* script_pubkey; + size_t script_pubkey_size; +} ZcashTransparentInputDigestInfo; + +typedef struct { + uint64_t value; + const uint8_t* script_pubkey; + size_t script_pubkey_size; +} ZcashTransparentOutputDigestInfo; + +#define ZCASH_ORCHARD_RAW_RECEIVER_SIZE 43 +#define ZCASH_ORCHARD_UNIFIED_ADDRESS_SIZE 128 + +/** + * Validate the clear-signing metadata required before Orchard signatures. + * + * This rejects the legacy flow where the host supplied only a per-action + * sighash. The firmware must assemble the ZIP-244 sighash from transaction + * component digests and verify the Orchard digest against streamed action data + * before returning signatures. + */ +ZcashPCZTSigningRequestStatus zcash_pczt_signing_request_status( + const ZcashPCZTSigningRequestMeta* meta); + +bool zcash_pczt_signing_request_is_clear( + const ZcashPCZTSigningRequestMeta* meta); + /** * Derive Orchard spending keys from the device seed via ZIP-32. * Path: m_orchard / 32' / 133' / account' @@ -77,6 +138,58 @@ bool zcash_compute_shielded_sighash(const uint8_t header_digest[32], uint32_t branch_id, uint8_t sighash_out[32]); +/** + * Compute ZIP-244 T.1 header_digest from plaintext transaction header fields. + */ +bool zcash_compute_header_digest(uint32_t version, uint32_t version_group_id, + uint32_t branch_id, uint32_t lock_time, + uint32_t expiry_height, + uint8_t digest_out[32]); + +/** + * Compute ZIP-244 T.2 transparent_digest from plaintext transparent data. + * + * This is the digest mixed into the Orchard/Sapling signing commitment. It is + * not the same as the per-input transparent signature digest. + */ +bool zcash_compute_transparent_digest( + const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, + const ZcashTransparentOutputDigestInfo* outputs, size_t n_outputs, + uint8_t digest_out[32]); + +/** + * Compute ZIP-244 S.2 per-input transparent signature digest. + * + * This currently accepts SIGHASH_ALL only, matching the existing transparent + * signing flow. + */ +bool zcash_compute_transparent_sighash_digest( + const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, + const ZcashTransparentOutputDigestInfo* outputs, size_t n_outputs, + uint32_t signable_input_index, uint8_t sighash_type, + uint8_t digest_out[32]); + +/** + * Encode a raw Orchard receiver (d || pk_d) as an Orchard-only ZIP-316 Unified + * Address for display. This is for recipient review; it does not derive or + * prove ownership of the receiver. + */ +bool zcash_orchard_receiver_to_unified_address( + const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], const char* hrp, + char* address_out, size_t address_out_len); + +/** + * Recompute an Orchard output note commitment x-coordinate (cmx). + * + * cmx = Extract_P(NoteCommit_rcm^Orchard(g_d, pk_d, v, rho, psi)) + * where receiver = d || pk_d, rho is the action nullifier, and rseed is the + * output note seed. This binds the user-displayed receiver/value to the action + * commitment before any authorization signature is emitted. + */ +bool zcash_orchard_compute_cmx( + const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, + const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32]); + /** * Derive an Orchard diversifier from a diversifier key and 88-bit index. * diff --git a/include/keepkey/transport/messages-zcash.options b/include/keepkey/transport/messages-zcash.options index f542c86ab..c43d185d7 100644 --- a/include/keepkey/transport/messages-zcash.options +++ b/include/keepkey/transport/messages-zcash.options @@ -22,6 +22,8 @@ ZcashPCZTAction.enc_memo max_size:512 ZcashPCZTAction.enc_noncompact max_size:564 ZcashPCZTAction.rk max_size:32 ZcashPCZTAction.out_ciphertext max_size:80 +ZcashPCZTAction.recipient max_size:43 +ZcashPCZTAction.rseed max_size:32 ZcashSignedPCZT.signatures max_count:16, max_size:64 ZcashSignedPCZT.txid max_size:32 @@ -33,11 +35,16 @@ ZcashOrchardFVK.nk max_size:32 ZcashOrchardFVK.rivk max_size:32 ZcashOrchardFVK.seed_fingerprint max_size:32 +ZcashTransparentOutput.amount int_size:IS_64 +ZcashTransparentOutput.script_pubkey max_size:128 + ZcashTransparentInput.sighash max_size:32 ZcashTransparentInput.address_n max_count:8 ZcashTransparentInput.amount int_size:IS_64 +ZcashTransparentInput.prevout_txid max_size:32 +ZcashTransparentInput.script_pubkey max_size:128 -ZcashTransparentSig.signature max_size:73 +ZcashTransparentSigned.signatures max_count:8, max_size:73 ZcashDisplayAddress.address_n max_count:8 ZcashDisplayAddress.expected_seed_fingerprint max_size:32 diff --git a/lib/firmware/fsm_msg_zcash.h b/lib/firmware/fsm_msg_zcash.h index 937ce5e46..5362d1b37 100644 --- a/lib/firmware/fsm_msg_zcash.h +++ b/lib/firmware/fsm_msg_zcash.h @@ -19,6 +19,8 @@ /* Zcash-specific headers — included here because fsm_msg_zcash.h * is #include'd inside fsm.c, not compiled separately. */ +#include + #include "keepkey/firmware/zcash.h" #include "trezor/crypto/blake2b.h" #include "trezor/crypto/pallas.h" @@ -38,6 +40,30 @@ static const uint8_t EMPTY_SAPLING_DIGEST[32] = { 0xf4, 0xbe, 0xd7, 0x43, 0x91, 0xee, 0x0b, 0x5a, 0x69, 0x94, 0x5e, 0x4c, 0xed, 0x8c, 0xa8, 0xa0, 0x95, 0x20, 0x6f, 0x00, 0xae}; +#define ZCASH_MAX_ACTIONS 16 +#define ZCASH_MAX_TRANSPARENT_INPUTS 8 +#define ZCASH_MAX_TRANSPARENT_OUTPUTS 8 +#define ZCASH_MAX_TRANSPARENT_SCRIPT_PUBKEY 128 + +typedef struct { + bool received; + uint8_t prevout_txid[32]; + uint32_t prevout_index; + uint32_t sequence; + uint64_t amount; + uint8_t script_pubkey[ZCASH_MAX_TRANSPARENT_SCRIPT_PUBKEY]; + size_t script_pubkey_size; + uint32_t address_n[8]; + uint32_t address_n_count; +} ZcashTransparentInputState; + +typedef struct { + bool received; + uint64_t amount; + uint8_t script_pubkey[ZCASH_MAX_TRANSPARENT_SCRIPT_PUBKEY]; + size_t script_pubkey_size; +} ZcashTransparentOutputState; + /* Zcash shielded signing state */ static struct { bool active; @@ -48,6 +74,7 @@ static struct { uint64_t fee; uint32_t branch_id; ZcashOrchardKeys keys; + uint8_t header_digest[32]; uint8_t sighash[32]; /* Phase 2a: on-device sighash computation */ bool has_device_sighash; @@ -63,18 +90,336 @@ static struct { /* Signatures buffer: up to 16 actions (64 bytes each) */ uint8_t signatures[16][64]; /* Phase 3: transparent shielding state */ + bool has_expected_transparent_digest; + uint8_t expected_transparent_digest[32]; + bool transparent_digest_verified; + uint32_t n_transparent_outputs; + uint32_t current_transparent_output; uint32_t n_transparent_inputs; uint32_t current_transparent_input; + ZcashTransparentOutputState transparent_outputs[ZCASH_MAX_TRANSPARENT_OUTPUTS]; + ZcashTransparentInputState transparent_inputs[ZCASH_MAX_TRANSPARENT_INPUTS]; } zcash_signing; -#define ZCASH_MAX_ACTIONS 16 -#define ZCASH_MAX_TRANSPARENT_INPUTS 8 - /* Public API; declared in keepkey/firmware/zcash.h. */ void zcash_signing_abort(void) { memzero(&zcash_signing, sizeof(zcash_signing)); } +static bool zcash_script_is_p2pkh(const uint8_t* script, size_t script_size) { + return script && script_size == 25 && script[0] == 0x76 && + script[1] == 0xa9 && script[2] == 0x14 && script[23] == 0x88 && + script[24] == 0xac; +} + +static bool zcash_script_is_p2sh(const uint8_t* script, size_t script_size) { + return script && script_size == 23 && script[0] == 0xa9 && + script[1] == 0x14 && script[22] == 0x87; +} + +static bool zcash_script_is_standard_transparent(const uint8_t* script, + size_t script_size) { + return zcash_script_is_p2pkh(script, script_size) || + zcash_script_is_p2sh(script, script_size); +} + +static bool zcash_transparent_script_to_address(const uint8_t* script, + size_t script_size, char* out, + size_t out_size) { + if (!script || !out || out_size == 0) return false; + + const CoinType* coin = fsm_getCoin(true, "Zcash"); + if (!coin) return false; + + uint32_t address_type; + const uint8_t* hash160; + if (zcash_script_is_p2pkh(script, script_size)) { + if (!coin->has_address_type) return false; + address_type = coin->address_type; + hash160 = script + 3; + } else if (zcash_script_is_p2sh(script, script_size)) { + if (!coin->has_address_type_p2sh) return false; + address_type = coin->address_type_p2sh; + hash160 = script + 2; + } else { + return false; + } + + uint8_t raw[4 + 20] = {0}; + size_t prefix_len = address_prefix_bytes_len(address_type); + if (prefix_len == 0 || prefix_len + 20 > sizeof(raw)) return false; + address_write_prefix_bytes(address_type, raw); + memcpy(raw + prefix_len, hash160, 20); + return base58_encode_check(raw, (int)(prefix_len + 20), HASHER_SHA2D, out, + (int)out_size) != 0; +} + +static void zcash_format_amount(uint64_t amount, char* out, size_t out_size) { + snprintf(out, out_size, "%llu.%08llu ZEC", + (unsigned long long)(amount / 100000000ULL), + (unsigned long long)(amount % 100000000ULL)); +} + +static bool zcash_verify_and_confirm_orchard_output( + const ZcashPCZTAction* msg) { + if (!msg->has_value || !msg->has_recipient || + msg->recipient.size != ZCASH_ORCHARD_RAW_RECEIVER_SIZE || + !msg->has_rseed || msg->rseed.size != 32) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Missing Orchard output metadata")); + return false; + } + + uint8_t computed_cmx[32]; + if (!zcash_orchard_compute_cmx(msg->recipient.bytes, msg->value, + msg->nullifier.bytes, msg->rseed.bytes, + computed_cmx) || + memcmp(computed_cmx, msg->cmx.bytes, 32) != 0) { + memzero(computed_cmx, sizeof(computed_cmx)); + fsm_sendFailure(FailureType_Failure_Other, + _("Orchard note commitment mismatch")); + return false; + } + memzero(computed_cmx, sizeof(computed_cmx)); + + char address[ZCASH_ORCHARD_UNIFIED_ADDRESS_SIZE]; + if (!zcash_orchard_receiver_to_unified_address( + msg->recipient.bytes, "u", address, sizeof(address))) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Invalid Orchard recipient")); + return false; + } + + char amount_str[32]; + zcash_format_amount(msg->value, amount_str, sizeof(amount_str)); + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, "Zcash Output", + "Send shielded ZEC?\n%s\nAmount: %s", address, amount_str)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + memzero(address, sizeof(address)); + return false; + } + + memzero(address, sizeof(address)); + return true; +} + +static bool zcash_compute_verified_fee(uint64_t* fee_out) { + if (!fee_out) return false; + + int64_t net_transparent = 0; + for (uint32_t i = 0; i < zcash_signing.n_transparent_inputs; i++) { + const uint64_t amount = zcash_signing.transparent_inputs[i].amount; + if (amount > (uint64_t)INT64_MAX || + net_transparent > INT64_MAX - (int64_t)amount) { + return false; + } + net_transparent += (int64_t)amount; + } + + for (uint32_t i = 0; i < zcash_signing.n_transparent_outputs; i++) { + const uint64_t amount = zcash_signing.transparent_outputs[i].amount; + if (amount > (uint64_t)INT64_MAX || + net_transparent < INT64_MIN + (int64_t)amount) { + return false; + } + net_transparent -= (int64_t)amount; + } + + const int64_t value_balance = zcash_signing.orchard_value_balance; + if ((value_balance > 0 && + net_transparent > INT64_MAX - value_balance) || + (value_balance < 0 && + net_transparent < INT64_MIN - value_balance)) { + return false; + } + + const int64_t signed_fee = net_transparent + value_balance; + if (signed_fee < 0) return false; + + *fee_out = (uint64_t)signed_fee; + return true; +} + +static bool zcash_verify_and_confirm_fee(void) { + uint64_t verified_fee = 0; + if (!zcash_compute_verified_fee(&verified_fee)) { + fsm_sendFailure(FailureType_Failure_Other, _("Invalid transaction fee")); + return false; + } + + if (verified_fee != zcash_signing.fee) { + fsm_sendFailure(FailureType_Failure_Other, _("Fee mismatch")); + return false; + } + + char fee_str[32]; + zcash_format_amount(verified_fee, fee_str, sizeof(fee_str)); + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Zcash Fee", + "Confirm transaction fee?\n%s", fee_str)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + return false; + } + + return true; +} + +static void zcash_send_action_ack(uint32_t next_index) { + ZcashPCZTActionAck* resp_ack = (ZcashPCZTActionAck*)msg_resp; + memset(resp_ack, 0, sizeof(ZcashPCZTActionAck)); + resp_ack->has_next_index = true; + resp_ack->next_index = next_index; + msg_write(MessageType_MessageType_ZcashPCZTActionAck, resp_ack); +} + +static void zcash_send_transparent_output_ack(uint32_t next_index) { + ZcashTransparentAck* resp = (ZcashTransparentAck*)msg_resp; + memset(resp, 0, sizeof(ZcashTransparentAck)); + resp->has_next_output_index = true; + resp->next_output_index = next_index; + msg_write(MessageType_MessageType_ZcashTransparentAck, resp); +} + +static void zcash_send_transparent_input_ack(uint32_t next_index) { + ZcashTransparentAck* resp = (ZcashTransparentAck*)msg_resp; + memset(resp, 0, sizeof(ZcashTransparentAck)); + resp->has_next_input_index = true; + resp->next_input_index = next_index; + msg_write(MessageType_MessageType_ZcashTransparentAck, resp); +} + +static bool zcash_build_transparent_digest_info( + ZcashTransparentInputDigestInfo inputs[ZCASH_MAX_TRANSPARENT_INPUTS], + ZcashTransparentOutputDigestInfo outputs[ZCASH_MAX_TRANSPARENT_OUTPUTS]) { + for (uint32_t i = 0; i < zcash_signing.n_transparent_inputs; i++) { + const ZcashTransparentInputState* stored = + &zcash_signing.transparent_inputs[i]; + if (!stored->received) return false; + inputs[i].prevout_txid = stored->prevout_txid; + inputs[i].prevout_index = stored->prevout_index; + inputs[i].sequence = stored->sequence; + inputs[i].value = stored->amount; + inputs[i].script_pubkey = stored->script_pubkey; + inputs[i].script_pubkey_size = stored->script_pubkey_size; + } + + for (uint32_t i = 0; i < zcash_signing.n_transparent_outputs; i++) { + const ZcashTransparentOutputState* stored = + &zcash_signing.transparent_outputs[i]; + if (!stored->received) return false; + outputs[i].value = stored->amount; + outputs[i].script_pubkey = stored->script_pubkey; + outputs[i].script_pubkey_size = stored->script_pubkey_size; + } + + return true; +} + +static bool zcash_finalize_transparent_digest(void) { + if (!zcash_signing.has_expected_transparent_digest) return false; + + ZcashTransparentInputDigestInfo inputs[ZCASH_MAX_TRANSPARENT_INPUTS] = {0}; + ZcashTransparentOutputDigestInfo outputs[ZCASH_MAX_TRANSPARENT_OUTPUTS] = {0}; + uint8_t transparent_digest[32] = {0}; + + if (!zcash_build_transparent_digest_info(inputs, outputs) || + !zcash_compute_transparent_digest( + inputs, zcash_signing.n_transparent_inputs, outputs, + zcash_signing.n_transparent_outputs, transparent_digest)) { + memzero(transparent_digest, sizeof(transparent_digest)); + memzero(inputs, sizeof(inputs)); + memzero(outputs, sizeof(outputs)); + return false; + } + + if (memcmp(transparent_digest, zcash_signing.expected_transparent_digest, + 32) != 0) { + memzero(transparent_digest, sizeof(transparent_digest)); + memzero(inputs, sizeof(inputs)); + memzero(outputs, sizeof(outputs)); + return false; + } + + zcash_compute_shielded_sighash( + zcash_signing.header_digest, transparent_digest, EMPTY_SAPLING_DIGEST, + zcash_signing.expected_orchard_digest, zcash_signing.branch_id, + zcash_signing.sighash); + zcash_signing.has_device_sighash = true; + zcash_signing.transparent_digest_verified = true; + + memzero(transparent_digest, sizeof(transparent_digest)); + memzero(inputs, sizeof(inputs)); + memzero(outputs, sizeof(outputs)); + return true; +} + +static bool zcash_sign_transparent_inputs(ZcashTransparentSigned* resp, + bool* cancelled) { + if (!resp || !zcash_signing.transparent_digest_verified) return false; + if (cancelled) *cancelled = false; + + bool ok = false; + ZcashTransparentInputDigestInfo inputs[ZCASH_MAX_TRANSPARENT_INPUTS] = {0}; + ZcashTransparentOutputDigestInfo outputs[ZCASH_MAX_TRANSPARENT_OUTPUTS] = {0}; + if (!zcash_build_transparent_digest_info(inputs, outputs)) goto cleanup; + + const CoinType* coin = fsm_getCoin(true, "Zcash"); + if (!coin) goto cleanup; + + memset(resp, 0, sizeof(ZcashTransparentSigned)); + resp->signatures_count = zcash_signing.n_transparent_inputs; + + for (uint32_t i = 0; i < zcash_signing.n_transparent_inputs; i++) { + const ZcashTransparentInputState* stored = + &zcash_signing.transparent_inputs[i]; + + char input_str[64]; + char amount_str[32]; + zcash_format_amount(stored->amount, amount_str, sizeof(amount_str)); + snprintf(input_str, sizeof(input_str), "Input %lu: %s", + (unsigned long)(i + 1), amount_str); + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Sign Input", + "Sign transparent input?\n%s", input_str)) { + if (cancelled) *cancelled = true; + goto cleanup; + } + + HDNode* node = fsm_getDerivedNode(coin->curve_name, stored->address_n, + stored->address_n_count, NULL); + if (!node) goto cleanup; + + uint8_t sighash[32] = {0}; + uint8_t sig[64] = {0}; + uint8_t der_sig[73] = {0}; + if (!zcash_compute_transparent_sighash_digest( + inputs, zcash_signing.n_transparent_inputs, outputs, + zcash_signing.n_transparent_outputs, i, 0x01, sighash) || + hdnode_sign_digest(node, sighash, sig, NULL, NULL) != 0) { + memzero(node, sizeof(*node)); + memzero(sighash, sizeof(sighash)); + memzero(sig, sizeof(sig)); + return false; + } + + int der_len = ecdsa_sig_to_der(sig, der_sig); + resp->signatures[i].size = der_len; + memcpy(resp->signatures[i].bytes, der_sig, der_len); + + memzero(node, sizeof(*node)); + memzero(sighash, sizeof(sighash)); + memzero(sig, sizeof(sig)); + memzero(der_sig, sizeof(der_sig)); + } + + ok = true; + +cleanup: + memzero(inputs, sizeof(inputs)); + memzero(outputs, sizeof(outputs)); + return ok; +} + void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { RESP_INIT(ZcashPCZTActionAck); @@ -116,6 +461,93 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { return; } + uint32_t n_tinputs = + msg->has_n_transparent_inputs ? msg->n_transparent_inputs : 0; + if (n_tinputs > ZCASH_MAX_TRANSPARENT_INPUTS) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Too many transparent inputs")); + layoutHome(); + return; + } + + uint32_t n_toutputs = + msg->has_n_transparent_outputs ? msg->n_transparent_outputs : 0; + if (n_toutputs > ZCASH_MAX_TRANSPARENT_OUTPUTS) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Too many transparent outputs")); + layoutHome(); + return; + } + + uint32_t branch_id = msg->has_branch_id ? msg->branch_id : 0; + + ZcashPCZTSigningRequestMeta signing_meta = {0}; + signing_meta.has_header_digest = msg->has_header_digest; + signing_meta.header_digest_size = msg->header_digest.size; + signing_meta.has_transparent_digest = msg->has_transparent_digest; + signing_meta.transparent_digest_size = msg->transparent_digest.size; + signing_meta.has_sapling_digest = msg->has_sapling_digest; + signing_meta.sapling_digest_size = msg->sapling_digest.size; + signing_meta.has_orchard_digest = msg->has_orchard_digest; + signing_meta.orchard_digest_size = msg->orchard_digest.size; + signing_meta.has_orchard_flags = msg->has_orchard_flags; + signing_meta.orchard_flags = msg->orchard_flags; + signing_meta.has_orchard_value_balance = msg->has_orchard_value_balance; + signing_meta.has_orchard_anchor = msg->has_orchard_anchor; + signing_meta.orchard_anchor_size = msg->orchard_anchor.size; + signing_meta.has_header_fields = + msg->has_tx_version && msg->has_version_group_id && msg->has_branch_id && + msg->has_lock_time && msg->has_expiry_height; + signing_meta.n_transparent_inputs = n_tinputs; + signing_meta.n_transparent_outputs = n_toutputs; + + switch (zcash_pczt_signing_request_status(&signing_meta)) { + case ZCASH_PCZT_SIGNING_REQUEST_OK: + break; + case ZCASH_PCZT_SIGNING_REQUEST_MISSING_TX_DIGESTS: + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Missing transaction digests")); + layoutHome(); + return; + case ZCASH_PCZT_SIGNING_REQUEST_INVALID_DIGEST_SIZE: + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Invalid transaction digest")); + layoutHome(); + return; + case ZCASH_PCZT_SIGNING_REQUEST_MISSING_HEADER_FIELDS: + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Missing transaction header")); + layoutHome(); + return; + case ZCASH_PCZT_SIGNING_REQUEST_UNSUPPORTED_SAPLING_COMPONENT: + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Sapling not supported")); + layoutHome(); + return; + case ZCASH_PCZT_SIGNING_REQUEST_MISSING_TRANSPARENT_DIGEST: + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Missing transparent digest")); + layoutHome(); + return; + case ZCASH_PCZT_SIGNING_REQUEST_MISSING_ORCHARD_METADATA: + default: + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Missing Orchard metadata")); + layoutHome(); + return; + } + + uint8_t header_digest[32]; + if (!zcash_compute_header_digest(msg->tx_version, msg->version_group_id, + branch_id, msg->lock_time, + msg->expiry_height, header_digest) || + memcmp(header_digest, msg->header_digest.bytes, 32) != 0) { + fsm_sendFailure(FailureType_Failure_Other, + _("Header digest mismatch")); + layoutHome(); + return; + } + /* Confirm with user */ char amount_str[32]; char fee_str[32]; @@ -123,21 +555,26 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { uint64_t fee = msg->has_fee ? msg->fee : 0; /* Format amounts (1 ZEC = 100,000,000 zatoshis) */ - snprintf(amount_str, sizeof(amount_str), "%llu.%08llu ZEC", - (unsigned long long)(total / 100000000ULL), - (unsigned long long)(total % 100000000ULL)); - snprintf(fee_str, sizeof(fee_str), "%llu.%08llu ZEC", - (unsigned long long)(fee / 100000000ULL), - (unsigned long long)(fee % 100000000ULL)); + zcash_format_amount(total, amount_str, sizeof(amount_str)); + zcash_format_amount(fee, fee_str, sizeof(fee_str)); /* Display confirmation — different text for shielded-only vs hybrid */ - uint32_t n_tinputs = - msg->has_n_transparent_inputs ? msg->n_transparent_inputs : 0; if (n_tinputs > 0) { if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Zcash Shield", "Shield transparent ZEC to Orchard?\n" - "Amount: %s\nFee: %s\nInputs: %lu\nActions: %lu", + "Amount: %s\nFee: %s\nInputs: %lu\nOutputs: %lu\nActions: %lu", amount_str, fee_str, (unsigned long)n_tinputs, + (unsigned long)n_toutputs, (unsigned long)msg->n_actions)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + layoutHome(); + return; + } + } else if (n_toutputs > 0) { + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Zcash Shielded", + "Sign transaction with transparent outputs?\n" + "Amount: %s\nFee: %s\nOutputs: %lu\nActions: %lu", + amount_str, fee_str, (unsigned long)n_toutputs, (unsigned long)msg->n_actions)) { fsm_sendFailure(FailureType_Failure_ActionCancelled, _("Signing cancelled")); @@ -195,22 +632,20 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { zcash_signing.current_action = 0; zcash_signing.total_amount = total; zcash_signing.fee = fee; - zcash_signing.branch_id = msg->has_branch_id ? msg->branch_id : 0x37519621; + zcash_signing.branch_id = branch_id; + memcpy(zcash_signing.header_digest, header_digest, 32); zcash_signing.has_device_sighash = false; zcash_signing.verify_orchard_digest = false; + zcash_signing.n_transparent_outputs = + msg->has_n_transparent_outputs ? msg->n_transparent_outputs : 0; + zcash_signing.current_transparent_output = 0; zcash_signing.n_transparent_inputs = msg->has_n_transparent_inputs ? msg->n_transparent_inputs : 0; zcash_signing.current_transparent_input = 0; + zcash_signing.has_expected_transparent_digest = false; + zcash_signing.transparent_digest_verified = false; - if (zcash_signing.n_transparent_inputs > ZCASH_MAX_TRANSPARENT_INPUTS) { - fsm_sendFailure(FailureType_Failure_SyntaxError, - _("Too many transparent inputs")); - zcash_signing_abort(); - layoutHome(); - return; - } - - /* Phase 2a: Compute sighash on-device if sub-digests are provided. + /* Phase 2a: Compute sighash on-device from validated sub-digests. * * TRUST MODEL: * @@ -218,76 +653,76 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { * - Orchard digest: recomputed from streamed action data (Phase 2b) * covering nullifiers, commitments, ephemeral keys, ciphertexts, * value commitments, randomized keys, flags, value balance, anchor. + * - Orchard outputs: each displayed receiver/value is bound to cmx by + * recomputing the note commitment from recipient/value/rseed/rho before + * any authorization signature is emitted. + * - Transaction fee: computed from streamed transparent totals plus + * orchard_value_balance and compared to the requested fee before final + * user confirmation. * - Sighash: assembled on-device from the 4 sub-digests. + * - transparent_digest: recomputed from streamed transparent outputs and + * inputs before any transparent or Orchard signature is emitted. + * - header_digest: recomputed from plaintext transaction header fields + * and compared to the supplied component digest. + * - Sapling: explicitly unsupported in this signing path. The device + * always uses the ZIP-244 empty Sapling digest and rejects any + * host-provided Sapling component. * - * What the device trusts from the host: - * - header_digest, transparent_digest, sapling_digest: accepted - * without verification. The device cannot recompute these without - * the full transaction data, which exceeds memory constraints. - * - Displayed amounts (total_amount, fee): these are for user - * confirmation only. Orchard values are hidden by Pedersen - * commitments (cv_net) — the device cannot extract plaintext - * amounts from the verified digest. This is inherent to the - * Zcash shielded protocol, not a firmware limitation. + * total_amount is a summary prompt. Transparent recipients, Orchard output + * recipients, Orchard output values, and the transaction fee all have their + * own verification gates before signatures are released. * * For shielded-only transactions (no transparent inputs): * transparent_digest defaults to the well-known empty hash, * so no trust assumption is needed for that component. * * For mixed transactions: - * The host is trusted for non-Orchard components. This matches - * the trust model of other Zcash hardware wallet implementations - * (Keystone, Trezor) for the streaming PCZT protocol. */ - if (msg->has_header_digest && msg->header_digest.size == 32 && - msg->has_orchard_digest && msg->orchard_digest.size == 32) { - uint8_t t_digest[32], s_digest[32]; - - /* Use provided transparent digest, or default to empty */ - if (msg->has_transparent_digest && msg->transparent_digest.size == 32) { - memcpy(t_digest, msg->transparent_digest.bytes, 32); - } else { - memcpy(t_digest, EMPTY_TRANSPARENT_DIGEST, 32); - } - - /* Use provided sapling digest, or default to empty */ - if (msg->has_sapling_digest && msg->sapling_digest.size == 32) { - memcpy(s_digest, msg->sapling_digest.bytes, 32); - } else { - memcpy(s_digest, EMPTY_SAPLING_DIGEST, 32); - } - - zcash_compute_shielded_sighash( - msg->header_digest.bytes, t_digest, s_digest, msg->orchard_digest.bytes, - zcash_signing.branch_id, zcash_signing.sighash); + * transparent_digest is mandatory and verified against plaintext + * transparent metadata before local sighash derivation. */ + uint8_t t_digest[32], s_digest[32]; + + if (n_tinputs == 0 && n_toutputs == 0) { + memcpy(t_digest, EMPTY_TRANSPARENT_DIGEST, 32); + memcpy(s_digest, EMPTY_SAPLING_DIGEST, 32); + + zcash_compute_shielded_sighash(header_digest, t_digest, s_digest, + msg->orchard_digest.bytes, + zcash_signing.branch_id, + zcash_signing.sighash); zcash_signing.has_device_sighash = true; - - /* Phase 2b: Initialize orchard digest verification if bundle metadata - * is provided (orchard_flags, orchard_value_balance, orchard_anchor). - * The device will incrementally hash each action's data and verify - * the computed orchard_digest matches the one used for sighash. */ - if (msg->has_orchard_flags && msg->has_orchard_value_balance && - msg->has_orchard_anchor && msg->orchard_anchor.size == 32) { - memcpy(zcash_signing.expected_orchard_digest, msg->orchard_digest.bytes, - 32); - zcash_signing.orchard_flags = (uint8_t)msg->orchard_flags; - zcash_signing.orchard_value_balance = msg->orchard_value_balance; - memcpy(zcash_signing.orchard_anchor, msg->orchard_anchor.bytes, 32); - - /* Initialize BLAKE2b streaming contexts for the 3 sub-hashes */ - blake2b_InitPersonal(&zcash_signing.compact_ctx, 32, "ZTxIdOrcActCHash", - 16); - blake2b_InitPersonal(&zcash_signing.memos_ctx, 32, "ZTxIdOrcActMHash", - 16); - blake2b_InitPersonal(&zcash_signing.noncompact_ctx, 32, - "ZTxIdOrcActNHash", 16); - zcash_signing.verify_orchard_digest = true; - } + zcash_signing.transparent_digest_verified = true; + } else { + memcpy(zcash_signing.expected_transparent_digest, + msg->transparent_digest.bytes, 32); + zcash_signing.has_expected_transparent_digest = true; + } + memzero(t_digest, sizeof(t_digest)); + memzero(s_digest, sizeof(s_digest)); + + /* Phase 2b: Orchard digest verification is mandatory for signing. + * The device incrementally hashes each action's data and verifies the + * computed orchard_digest matches the one used for sighash. */ + memcpy(zcash_signing.expected_orchard_digest, msg->orchard_digest.bytes, 32); + zcash_signing.orchard_flags = (uint8_t)msg->orchard_flags; + zcash_signing.orchard_value_balance = msg->orchard_value_balance; + memcpy(zcash_signing.orchard_anchor, msg->orchard_anchor.bytes, 32); + + blake2b_InitPersonal(&zcash_signing.compact_ctx, 32, "ZTxIdOrcActCHash", + 16); + blake2b_InitPersonal(&zcash_signing.memos_ctx, 32, "ZTxIdOrcActMHash", 16); + blake2b_InitPersonal(&zcash_signing.noncompact_ctx, 32, + "ZTxIdOrcActNHash", 16); + zcash_signing.verify_orchard_digest = true; + + /* Request the first plaintext component. Transparent outputs are reviewed + * before any transparent input or Orchard signature can be emitted. */ + if (zcash_signing.n_transparent_outputs > 0) { + zcash_send_transparent_output_ack(0); + } else if (zcash_signing.n_transparent_inputs > 0) { + zcash_send_transparent_input_ack(0); + } else { + zcash_send_action_ack(0); } - - /* Request first action data */ - resp->has_next_index = true; - resp->next_index = 0; - msg_write(MessageType_MessageType_ZcashPCZTActionAck, resp); layoutProgress(_("Signing Zcash"), 0); } @@ -487,14 +922,20 @@ void fsm_msgZcashPCZTAction(const ZcashPCZTAction* msg) { return; } - /* Enforce transparent phase completion: if the session declared - * transparent inputs, ALL must be signed before Orchard actions. + /* Enforce transparent phase completion: if the session declared any + * transparent data, all plaintext must be streamed and verified before + * Orchard actions. * This prevents a malicious host from skipping transparent-input * confirmations and jumping straight to Orchard signing. */ - if (zcash_signing.current_transparent_input < - zcash_signing.n_transparent_inputs) { + if (zcash_signing.current_transparent_output < + zcash_signing.n_transparent_outputs || + zcash_signing.current_transparent_input < + zcash_signing.n_transparent_inputs || + ((zcash_signing.n_transparent_outputs > 0 || + zcash_signing.n_transparent_inputs > 0) && + !zcash_signing.transparent_digest_verified)) { fsm_sendFailure(FailureType_Failure_UnexpectedMessage, - _("Transparent inputs not yet complete")); + _("Transparent data not yet complete")); zcash_signing_abort(); layoutHome(); return; @@ -518,48 +959,54 @@ void fsm_msgZcashPCZTAction(const ZcashPCZTAction* msg) { return; } - /* Phase 2a: require sighash only in legacy mode */ + /* Phase 2a: a device-computed sighash is mandatory. */ if (!zcash_signing.has_device_sighash) { - if (!msg->has_sighash || msg->sighash.size != 32) { - fsm_sendFailure(FailureType_Failure_SyntaxError, - _("Missing or invalid sighash")); - zcash_signing_abort(); - layoutHome(); - return; - } + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Missing transaction digests")); + zcash_signing_abort(); + layoutHome(); + return; } - /* Phase 2b: feed action data into incremental BLAKE2b contexts */ - if (zcash_signing.verify_orchard_digest && msg->has_nullifier && + const bool has_orchard_action_data = + zcash_signing.verify_orchard_digest && msg->has_nullifier && msg->nullifier.size == 32 && msg->has_cmx && msg->cmx.size == 32 && msg->has_epk && msg->epk.size == 32 && msg->has_enc_compact && msg->enc_compact.size == 52 && msg->has_enc_memo && msg->enc_memo.size == 512 && msg->has_enc_noncompact && msg->enc_noncompact.size > 0 && msg->has_cv_net && msg->cv_net.size == 32 && msg->has_rk && msg->rk.size == 32 && - msg->has_out_ciphertext && msg->out_ciphertext.size == 80) { - /* Compact: nf || cmx || epk || enc[0..52] */ - blake2b_Update(&zcash_signing.compact_ctx, msg->nullifier.bytes, 32); - blake2b_Update(&zcash_signing.compact_ctx, msg->cmx.bytes, 32); - blake2b_Update(&zcash_signing.compact_ctx, msg->epk.bytes, 32); - blake2b_Update(&zcash_signing.compact_ctx, msg->enc_compact.bytes, 52); - - /* Memos: enc[52..564] */ - blake2b_Update(&zcash_signing.memos_ctx, msg->enc_memo.bytes, 512); - - /* Noncompact: cv_net || rk || enc[564..] || out_ciphertext */ - blake2b_Update(&zcash_signing.noncompact_ctx, msg->cv_net.bytes, 32); - blake2b_Update(&zcash_signing.noncompact_ctx, msg->rk.bytes, 32); - blake2b_Update(&zcash_signing.noncompact_ctx, msg->enc_noncompact.bytes, - msg->enc_noncompact.size); - blake2b_Update(&zcash_signing.noncompact_ctx, msg->out_ciphertext.bytes, - 80); - } - - /* Use device-computed sighash if available, otherwise legacy host sighash */ - const uint8_t* sighash = zcash_signing.has_device_sighash - ? zcash_signing.sighash - : msg->sighash.bytes; + msg->has_out_ciphertext && msg->out_ciphertext.size == 80; + + if (!has_orchard_action_data) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Missing Orchard action data")); + zcash_signing_abort(); + layoutHome(); + return; + } + + if (!zcash_verify_and_confirm_orchard_output(msg)) { + zcash_signing_abort(); + layoutHome(); + return; + } + + /* Phase 2b: feed action data into incremental BLAKE2b contexts */ + blake2b_Update(&zcash_signing.compact_ctx, msg->nullifier.bytes, 32); + blake2b_Update(&zcash_signing.compact_ctx, msg->cmx.bytes, 32); + blake2b_Update(&zcash_signing.compact_ctx, msg->epk.bytes, 32); + blake2b_Update(&zcash_signing.compact_ctx, msg->enc_compact.bytes, 52); + + blake2b_Update(&zcash_signing.memos_ctx, msg->enc_memo.bytes, 512); + + blake2b_Update(&zcash_signing.noncompact_ctx, msg->cv_net.bytes, 32); + blake2b_Update(&zcash_signing.noncompact_ctx, msg->rk.bytes, 32); + blake2b_Update(&zcash_signing.noncompact_ctx, msg->enc_noncompact.bytes, + msg->enc_noncompact.size); + blake2b_Update(&zcash_signing.noncompact_ctx, msg->out_ciphertext.bytes, 80); + + const uint8_t* sighash = zcash_signing.sighash; /* Sign this action with RedPallas: * sig = RedPallas.sign(ask, alpha, sighash) */ @@ -617,6 +1064,13 @@ void fsm_msgZcashPCZTAction(const ZcashPCZTAction* msg) { } } + if (!zcash_verify_and_confirm_fee()) { + zcash_signing_abort(); + memzero(zcash_signing.signatures, sizeof(zcash_signing.signatures)); + layoutHome(); + return; + } + /* All done - send the collected signatures */ ZcashSignedPCZT* resp_signed = (ZcashSignedPCZT*)msg_resp; memset(resp_signed, 0, sizeof(ZcashSignedPCZT)); @@ -635,26 +1089,109 @@ void fsm_msgZcashPCZTAction(const ZcashPCZTAction* msg) { layoutHome(); } else { /* Request next action */ - ZcashPCZTActionAck* resp_ack = (ZcashPCZTActionAck*)msg_resp; - memset(resp_ack, 0, sizeof(ZcashPCZTActionAck)); - resp_ack->has_next_index = true; - resp_ack->next_index = zcash_signing.current_action; - msg_write(MessageType_MessageType_ZcashPCZTActionAck, resp_ack); + zcash_send_action_ack(zcash_signing.current_action); } } -/* Phase 3: Transparent input signing for hybrid shielding transactions. - * - * The host sends one ZcashTransparentInput per transparent input after - * the initial ZcashSignPCZT. The device derives the secp256k1 key at - * the provided BIP44 path, ECDSA-signs the per-input sighash, and - * returns a DER signature. - * - * After all transparent inputs, the device transitions to the Orchard - * action phase (ZcashPCZTAction streaming). */ -void fsm_msgZcashTransparentInput(const ZcashTransparentInput* msg) { - RESP_INIT(ZcashTransparentSig); +void fsm_msgZcashTransparentOutput(const ZcashTransparentOutput* msg) { + if (!zcash_signing.active) { + fsm_sendFailure(FailureType_Failure_UnexpectedMessage, + _("Not in Zcash signing mode")); + layoutHome(); + return; + } + if (zcash_signing.n_transparent_outputs == 0) { + fsm_sendFailure(FailureType_Failure_UnexpectedMessage, + _("No transparent outputs expected")); + zcash_signing_abort(); + layoutHome(); + return; + } + + if (zcash_signing.current_transparent_input != 0) { + fsm_sendFailure(FailureType_Failure_UnexpectedMessage, + _("Transparent outputs must come first")); + zcash_signing_abort(); + layoutHome(); + return; + } + + if (msg->index != zcash_signing.current_transparent_output) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Unexpected transparent output index")); + zcash_signing_abort(); + layoutHome(); + return; + } + + if (!msg->has_amount || !msg->has_script_pubkey || + msg->script_pubkey.size == 0 || + msg->script_pubkey.size > ZCASH_MAX_TRANSPARENT_SCRIPT_PUBKEY) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Invalid transparent output script")); + zcash_signing_abort(); + layoutHome(); + return; + } + + char address[64]; + if (!zcash_transparent_script_to_address( + msg->script_pubkey.bytes, msg->script_pubkey.size, address, + sizeof(address))) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Unsupported transparent output script")); + zcash_signing_abort(); + layoutHome(); + return; + } + + char amount_str[32]; + zcash_format_amount(msg->amount, amount_str, sizeof(amount_str)); + if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, "Zcash Output", + "Send transparent ZEC?\n%s\nAmount: %s", address, + amount_str)) { + fsm_sendFailure(FailureType_Failure_ActionCancelled, + _("Signing cancelled")); + zcash_signing_abort(); + layoutHome(); + return; + } + + ZcashTransparentOutputState* stored = + &zcash_signing.transparent_outputs[msg->index]; + stored->received = true; + stored->amount = msg->amount; + stored->script_pubkey_size = msg->script_pubkey.size; + memcpy(stored->script_pubkey, msg->script_pubkey.bytes, + msg->script_pubkey.size); + + zcash_signing.current_transparent_output++; + + if (zcash_signing.current_transparent_output < + zcash_signing.n_transparent_outputs) { + zcash_send_transparent_output_ack(zcash_signing.current_transparent_output); + } else if (zcash_signing.n_transparent_inputs > 0) { + zcash_send_transparent_input_ack(0); + } else { + if (!zcash_finalize_transparent_digest()) { + fsm_sendFailure(FailureType_Failure_Other, + _("Transparent digest mismatch")); + zcash_signing_abort(); + layoutHome(); + return; + } + zcash_send_action_ack(0); + } + + layoutProgress(_("Signing Zcash"), 0); +} + +/* Phase 3: Transparent plaintext streaming for hybrid shielding + * transactions. The host streams all outputs first, then all inputs. Only after + * the firmware verifies transparent_digest from the streamed plaintext does it + * derive per-input ZIP-244 sighashes and emit ECDSA signatures. */ +void fsm_msgZcashTransparentInput(const ZcashTransparentInput* msg) { if (!zcash_signing.active) { fsm_sendFailure(FailureType_Failure_UnexpectedMessage, _("Not in Zcash signing mode")); @@ -670,6 +1207,15 @@ void fsm_msgZcashTransparentInput(const ZcashTransparentInput* msg) { return; } + if (zcash_signing.current_transparent_output < + zcash_signing.n_transparent_outputs) { + fsm_sendFailure(FailureType_Failure_UnexpectedMessage, + _("Transparent outputs not yet complete")); + zcash_signing_abort(); + layoutHome(); + return; + } + if (msg->index != zcash_signing.current_transparent_input) { fsm_sendFailure(FailureType_Failure_SyntaxError, _("Unexpected transparent input index")); @@ -678,8 +1224,30 @@ void fsm_msgZcashTransparentInput(const ZcashTransparentInput* msg) { return; } - if (msg->sighash.size != 32) { - fsm_sendFailure(FailureType_Failure_SyntaxError, _("Invalid sighash size")); + if (msg->has_sighash) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Host transparent sighash rejected")); + zcash_signing_abort(); + layoutHome(); + return; + } + + if (!msg->has_amount || !msg->has_prevout_txid || + msg->prevout_txid.size != 32 || !msg->has_prevout_index || + !msg->has_sequence || !msg->has_script_pubkey || + msg->script_pubkey.size == 0 || + msg->script_pubkey.size > ZCASH_MAX_TRANSPARENT_SCRIPT_PUBKEY) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Invalid transparent input data")); + zcash_signing_abort(); + layoutHome(); + return; + } + + if (!zcash_script_is_standard_transparent(msg->script_pubkey.bytes, + msg->script_pubkey.size)) { + fsm_sendFailure(FailureType_Failure_SyntaxError, + _("Unsupported transparent input script")); zcash_signing_abort(); layoutHome(); return; @@ -746,75 +1314,48 @@ void fsm_msgZcashTransparentInput(const ZcashTransparentInput* msg) { return; } - /* Per-input confirmation: show the user what's being signed */ - { - char input_str[64]; - uint64_t amt = msg->has_amount ? msg->amount : 0; - snprintf(input_str, sizeof(input_str), "Input %lu: %llu.%08llu ZEC", - (unsigned long)(msg->index + 1), - (unsigned long long)(amt / 100000000ULL), - (unsigned long long)(amt % 100000000ULL)); - if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Sign Input", - "Sign transparent input?\n%s", input_str)) { - fsm_sendFailure(FailureType_Failure_ActionCancelled, - _("Signing cancelled")); - zcash_signing_abort(); - layoutHome(); - return; - } - } + ZcashTransparentInputState* stored = + &zcash_signing.transparent_inputs[msg->index]; + stored->received = true; + stored->amount = msg->amount; + memcpy(stored->prevout_txid, msg->prevout_txid.bytes, 32); + stored->prevout_index = msg->prevout_index; + stored->sequence = msg->sequence; + stored->script_pubkey_size = msg->script_pubkey.size; + memcpy(stored->script_pubkey, msg->script_pubkey.bytes, + msg->script_pubkey.size); + stored->address_n_count = msg->address_n_count; + memcpy(stored->address_n, msg->address_n, + msg->address_n_count * sizeof(msg->address_n[0])); - /* Derive the secp256k1 key at the validated Zcash BIP44 path */ - const CoinType* coin = fsm_getCoin(true, "Zcash"); - if (!coin) { - fsm_sendFailure(FailureType_Failure_Other, _("Unknown coin: Zcash")); - zcash_signing_abort(); - layoutHome(); + zcash_signing.current_transparent_input++; + + if (zcash_signing.current_transparent_input < + zcash_signing.n_transparent_inputs) { + zcash_send_transparent_input_ack(zcash_signing.current_transparent_input); + layoutProgress(_("Signing Zcash"), 0); return; } - HDNode* node = fsm_getDerivedNode(coin->curve_name, msg->address_n, - msg->address_n_count, NULL); - if (!node) { + if (!zcash_finalize_transparent_digest()) { + fsm_sendFailure(FailureType_Failure_Other, _("Transparent digest mismatch")); zcash_signing_abort(); layoutHome(); return; } - hdnode_fill_public_key(node); - - /* ECDSA sign the per-input sighash */ - uint8_t sig[64]; - if (hdnode_sign_digest(node, msg->sighash.bytes, sig, NULL, NULL) != 0) { - fsm_sendFailure(FailureType_Failure_Other, _("ECDSA signing failed")); - memzero(node, sizeof(*node)); + ZcashTransparentSigned* resp = (ZcashTransparentSigned*)msg_resp; + bool cancelled = false; + if (!zcash_sign_transparent_inputs(resp, &cancelled)) { + fsm_sendFailure(cancelled ? FailureType_Failure_ActionCancelled + : FailureType_Failure_Other, + cancelled ? _("Signing cancelled") + : _("Transparent input signing failed")); zcash_signing_abort(); layoutHome(); return; } - /* DER encode the signature */ - uint8_t der_sig[73]; - int der_len = ecdsa_sig_to_der(sig, der_sig); - - /* Build response — signature is a required field (no has_ prefix in nanopb) - */ - resp->signature.size = der_len; - memcpy(resp->signature.bytes, der_sig, der_len); - - zcash_signing.current_transparent_input++; - resp->has_next_index = true; - - if (zcash_signing.current_transparent_input >= - zcash_signing.n_transparent_inputs) { - resp->next_index = 0xFF; - } else { - resp->next_index = zcash_signing.current_transparent_input; - } - - memzero(node, sizeof(*node)); - memzero(sig, sizeof(sig)); - - msg_write(MessageType_MessageType_ZcashTransparentSig, resp); + msg_write(MessageType_MessageType_ZcashTransparentSigned, resp); layoutProgress(_("Signing Zcash"), 0); } diff --git a/lib/firmware/messagemap.def b/lib/firmware/messagemap.def index 6f0ef33e2..41b1e2dfe 100644 --- a/lib/firmware/messagemap.def +++ b/lib/firmware/messagemap.def @@ -159,13 +159,15 @@ MSG_IN(MessageType_MessageType_ZcashSignPCZT, ZcashSignPCZT, fsm_msgZcashSignPCZT) MSG_IN(MessageType_MessageType_ZcashPCZTAction, ZcashPCZTAction, fsm_msgZcashPCZTAction) MSG_IN(MessageType_MessageType_ZcashGetOrchardFVK, ZcashGetOrchardFVK, fsm_msgZcashGetOrchardFVK) + MSG_IN(MessageType_MessageType_ZcashTransparentOutput, ZcashTransparentOutput, fsm_msgZcashTransparentOutput) MSG_IN(MessageType_MessageType_ZcashTransparentInput, ZcashTransparentInput, fsm_msgZcashTransparentInput) MSG_IN(MessageType_MessageType_ZcashDisplayAddress, ZcashDisplayAddress, fsm_msgZcashDisplayAddress) MSG_OUT(MessageType_MessageType_ZcashPCZTActionAck, ZcashPCZTActionAck, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_ZcashSignedPCZT, ZcashSignedPCZT, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_ZcashOrchardFVK, ZcashOrchardFVK, NO_PROCESS_FUNC) - MSG_OUT(MessageType_MessageType_ZcashTransparentSig, ZcashTransparentSig, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_ZcashTransparentSigned, ZcashTransparentSigned, NO_PROCESS_FUNC) + MSG_OUT(MessageType_MessageType_ZcashTransparentAck, ZcashTransparentAck, NO_PROCESS_FUNC) MSG_OUT(MessageType_MessageType_ZcashAddress, ZcashAddress, NO_PROCESS_FUNC) #if DEBUG_LINK diff --git a/lib/firmware/zcash.c b/lib/firmware/zcash.c index aa2eae764..311aa595f 100644 --- a/lib/firmware/zcash.c +++ b/lib/firmware/zcash.c @@ -462,6 +462,98 @@ bool zcash_orchard_derive_unified_address(const ZcashOrchardKeys* keys, return ok; } +bool zcash_orchard_receiver_to_unified_address( + const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], const char* hrp, + char* address_out, size_t address_out_len) { + if (!receiver || !hrp || !address_out) return false; + return zcash_zip316_encode_orchard_unified_address(hrp, receiver, address_out, + address_out_len) == 0; +} + +static bool zcash_pack_orchard_note_commit_msg(const uint8_t receiver[43], + uint64_t value, + const uint8_t rho[32], + const uint8_t psi[32], + uint8_t msg[136]) { + memset(msg, 0, 136); + + /* bits 0..255: repr_P(g_d) */ + curve_point gd; + if (!orchard_diversify_point(receiver, &gd)) { + memzero(&gd, sizeof(gd)); + return false; + } + pallas_point_encode(&gd, msg); + memzero(&gd, sizeof(gd)); + + /* bits 256..511: repr_P(pk_d) */ + memcpy(msg + 32, receiver + 11, 32); + + /* bits 512..575: I2LEBSP_64(value) */ + for (int i = 0; i < 8; i++) { + msg[64 + i] = (uint8_t)((value >> (8 * i)) & 0xff); + } + + /* bits 576..830: I2LEBSP_255(rho) */ + memcpy(msg + 72, rho, 31); + msg[103] = rho[31] & 0x7f; + + /* bits 831..1085: I2LEBSP_255(psi), packed at bit offset 831. */ + uint8_t psi255[32]; + memcpy(psi255, psi, 32); + psi255[31] &= 0x7f; + for (int i = 0; i < 32; i++) { + msg[103 + i] |= (uint8_t)(psi255[i] << 7); + msg[104 + i] |= (uint8_t)(psi255[i] >> 1); + } + memzero(psi255, sizeof(psi255)); + return true; +} + +bool zcash_orchard_compute_cmx( + const uint8_t receiver[ZCASH_ORCHARD_RAW_RECEIVER_SIZE], uint64_t value, + const uint8_t rho[32], const uint8_t rseed[32], uint8_t cmx_out[32]) { + if (!receiver || !rho || !rseed || !cmx_out) return false; + + uint8_t msg[136]; + uint8_t prf_in[33]; + uint8_t prf_out[64]; + uint8_t rcm[32]; + uint8_t psi[32]; + curve_point q, r; + bool ok = false; + + memcpy(prf_in + 1, rho, 32); + + prf_in[0] = 0x05; + prf_expand(rseed, prf_in, sizeof(prf_in), prf_out); + to_scalar(prf_out, rcm); + + prf_in[0] = 0x09; + prf_expand(rseed, prf_in, sizeof(prf_in), prf_out); + to_base(prf_out, psi); + + ok = zcash_pack_orchard_note_commit_msg(receiver, value, rho, psi, msg) && + pallas_group_hash("z.cash:SinsemillaQ", + (const uint8_t*)"z.cash:Orchard-NoteCommit-M", + strlen("z.cash:Orchard-NoteCommit-M"), &q) == 0 && + pallas_group_hash("z.cash:Orchard-NoteCommit-r", NULL, 0, &r) == 0 && + pallas_sinsemilla_short_commit(&q, &r, msg, 1086, rcm, cmx_out) == 0; + + if (!ok) { + memzero(cmx_out, 32); + } + + memzero(msg, sizeof(msg)); + memzero(prf_in, sizeof(prf_in)); + memzero(prf_out, sizeof(prf_out)); + memzero(rcm, sizeof(rcm)); + memzero(psi, sizeof(psi)); + memzero(&q, sizeof(q)); + memzero(&r, sizeof(r)); + return ok; +} + bool zcash_derive_orchard_unified_address(const uint8_t* seed, uint32_t seed_len, uint32_t account, const uint8_t index_le[11], @@ -632,6 +724,337 @@ bool zcash_compute_shielded_sighash(const uint8_t header_digest[32], return true; } +static void zcash_write_u32_le(uint32_t value, uint8_t out[4]) { + out[0] = (uint8_t)(value & 0xff); + out[1] = (uint8_t)((value >> 8) & 0xff); + out[2] = (uint8_t)((value >> 16) & 0xff); + out[3] = (uint8_t)((value >> 24) & 0xff); +} + +static void zcash_write_u64_le(uint64_t value, uint8_t out[8]) { + for (size_t i = 0; i < 8; i++) { + out[i] = (uint8_t)((value >> (8 * i)) & 0xff); + } +} + +static size_t zcash_write_compact_size(size_t value, uint8_t out[9]) { + if (value < 253) { + out[0] = (uint8_t)value; + return 1; + } + + if (value <= 0xffff) { + out[0] = 0xfd; + out[1] = (uint8_t)(value & 0xff); + out[2] = (uint8_t)((value >> 8) & 0xff); + return 3; + } + + if (value <= 0xffffffff) { + out[0] = 0xfe; + out[1] = (uint8_t)(value & 0xff); + out[2] = (uint8_t)((value >> 8) & 0xff); + out[3] = (uint8_t)((value >> 16) & 0xff); + out[4] = (uint8_t)((value >> 24) & 0xff); + return 5; + } + + out[0] = 0xff; + uint64_t v = (uint64_t)value; + for (size_t i = 0; i < 8; i++) { + out[i + 1] = (uint8_t)((v >> (8 * i)) & 0xff); + } + return 9; +} + +static void zcash_blake2b_personal_256(const char personal[16], + const uint8_t* data, size_t data_len, + uint8_t digest_out[32]) { + BLAKE2B_CTX ctx; + blake2b_InitPersonal(&ctx, 32, personal, 16); + if (data_len > 0) { + blake2b_Update(&ctx, data, data_len); + } + blake2b_Final(&ctx, digest_out, 32); +} + +bool zcash_compute_header_digest(uint32_t version, uint32_t version_group_id, + uint32_t branch_id, uint32_t lock_time, + uint32_t expiry_height, + uint8_t digest_out[32]) { + if (!digest_out) return false; + + uint8_t header[20]; + zcash_write_u32_le(version | 0x80000000u, header); + zcash_write_u32_le(version_group_id, header + 4); + zcash_write_u32_le(branch_id, header + 8); + zcash_write_u32_le(lock_time, header + 12); + zcash_write_u32_le(expiry_height, header + 16); + + zcash_blake2b_personal_256("ZTxIdHeadersHash", header, sizeof(header), + digest_out); + memzero(header, sizeof(header)); + return true; +} + +static bool zcash_validate_transparent_digest_info( + const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, + const ZcashTransparentOutputDigestInfo* outputs, size_t n_outputs) { + if (n_inputs > 0 && !inputs) return false; + if (n_outputs > 0 && !outputs) return false; + + for (size_t i = 0; i < n_inputs; i++) { + if (!inputs[i].prevout_txid || + (inputs[i].script_pubkey_size > 0 && !inputs[i].script_pubkey)) { + return false; + } + } + + for (size_t i = 0; i < n_outputs; i++) { + if (outputs[i].script_pubkey_size > 0 && !outputs[i].script_pubkey) { + return false; + } + } + + return true; +} + +static void zcash_hash_transparent_prevouts( + const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, + uint8_t digest_out[32]) { + BLAKE2B_CTX ctx; + uint8_t le[4]; + blake2b_InitPersonal(&ctx, 32, "ZTxIdPrevoutHash", 16); + for (size_t i = 0; i < n_inputs; i++) { + blake2b_Update(&ctx, inputs[i].prevout_txid, 32); + zcash_write_u32_le(inputs[i].prevout_index, le); + blake2b_Update(&ctx, le, sizeof(le)); + } + blake2b_Final(&ctx, digest_out, 32); + memzero(le, sizeof(le)); +} + +static void zcash_hash_transparent_sequences( + const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, + uint8_t digest_out[32]) { + BLAKE2B_CTX ctx; + uint8_t le[4]; + blake2b_InitPersonal(&ctx, 32, "ZTxIdSequencHash", 16); + for (size_t i = 0; i < n_inputs; i++) { + zcash_write_u32_le(inputs[i].sequence, le); + blake2b_Update(&ctx, le, sizeof(le)); + } + blake2b_Final(&ctx, digest_out, 32); + memzero(le, sizeof(le)); +} + +static void zcash_hash_transparent_amounts( + const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, + uint8_t digest_out[32]) { + BLAKE2B_CTX ctx; + uint8_t le[8]; + blake2b_InitPersonal(&ctx, 32, "ZTxTrAmountsHash", 16); + for (size_t i = 0; i < n_inputs; i++) { + zcash_write_u64_le(inputs[i].value, le); + blake2b_Update(&ctx, le, sizeof(le)); + } + blake2b_Final(&ctx, digest_out, 32); + memzero(le, sizeof(le)); +} + +static void zcash_hash_transparent_scripts( + const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, + uint8_t digest_out[32]) { + BLAKE2B_CTX ctx; + uint8_t compact_size[9]; + blake2b_InitPersonal(&ctx, 32, "ZTxTrScriptsHash", 16); + for (size_t i = 0; i < n_inputs; i++) { + size_t compact_size_len = + zcash_write_compact_size(inputs[i].script_pubkey_size, compact_size); + blake2b_Update(&ctx, compact_size, compact_size_len); + if (inputs[i].script_pubkey_size > 0) { + blake2b_Update(&ctx, inputs[i].script_pubkey, + inputs[i].script_pubkey_size); + } + } + blake2b_Final(&ctx, digest_out, 32); + memzero(compact_size, sizeof(compact_size)); +} + +static void zcash_hash_transparent_outputs( + const ZcashTransparentOutputDigestInfo* outputs, size_t n_outputs, + uint8_t digest_out[32]) { + BLAKE2B_CTX ctx; + uint8_t le[8]; + uint8_t compact_size[9]; + blake2b_InitPersonal(&ctx, 32, "ZTxIdOutputsHash", 16); + for (size_t i = 0; i < n_outputs; i++) { + zcash_write_u64_le(outputs[i].value, le); + blake2b_Update(&ctx, le, sizeof(le)); + size_t compact_size_len = + zcash_write_compact_size(outputs[i].script_pubkey_size, compact_size); + blake2b_Update(&ctx, compact_size, compact_size_len); + if (outputs[i].script_pubkey_size > 0) { + blake2b_Update(&ctx, outputs[i].script_pubkey, + outputs[i].script_pubkey_size); + } + } + blake2b_Final(&ctx, digest_out, 32); + memzero(le, sizeof(le)); + memzero(compact_size, sizeof(compact_size)); +} + +static bool zcash_hash_transparent_input( + const ZcashTransparentInputDigestInfo* input, uint8_t digest_out[32]) { + if (!input) return false; + + BLAKE2B_CTX ctx; + uint8_t le4[4]; + uint8_t le8[8]; + uint8_t compact_size[9]; + blake2b_InitPersonal(&ctx, 32, "Zcash___TxInHash", 16); + blake2b_Update(&ctx, input->prevout_txid, 32); + zcash_write_u32_le(input->prevout_index, le4); + blake2b_Update(&ctx, le4, sizeof(le4)); + zcash_write_u64_le(input->value, le8); + blake2b_Update(&ctx, le8, sizeof(le8)); + size_t compact_size_len = + zcash_write_compact_size(input->script_pubkey_size, compact_size); + blake2b_Update(&ctx, compact_size, compact_size_len); + if (input->script_pubkey_size > 0) { + blake2b_Update(&ctx, input->script_pubkey, input->script_pubkey_size); + } + zcash_write_u32_le(input->sequence, le4); + blake2b_Update(&ctx, le4, sizeof(le4)); + blake2b_Final(&ctx, digest_out, 32); + memzero(le4, sizeof(le4)); + memzero(le8, sizeof(le8)); + memzero(compact_size, sizeof(compact_size)); + return true; +} + +bool zcash_compute_transparent_digest( + const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, + const ZcashTransparentOutputDigestInfo* outputs, size_t n_outputs, + uint8_t digest_out[32]) { + if (!digest_out || + !zcash_validate_transparent_digest_info(inputs, n_inputs, outputs, + n_outputs)) { + return false; + } + + if (n_inputs == 0 && n_outputs == 0) { + zcash_blake2b_personal_256("ZTxIdTranspaHash", NULL, 0, digest_out); + return true; + } + + uint8_t prevouts_digest[32], sequence_digest[32], outputs_digest[32]; + zcash_hash_transparent_prevouts(inputs, n_inputs, prevouts_digest); + zcash_hash_transparent_sequences(inputs, n_inputs, sequence_digest); + zcash_hash_transparent_outputs(outputs, n_outputs, outputs_digest); + + BLAKE2B_CTX ctx; + blake2b_InitPersonal(&ctx, 32, "ZTxIdTranspaHash", 16); + blake2b_Update(&ctx, prevouts_digest, 32); + blake2b_Update(&ctx, sequence_digest, 32); + blake2b_Update(&ctx, outputs_digest, 32); + blake2b_Final(&ctx, digest_out, 32); + + memzero(prevouts_digest, sizeof(prevouts_digest)); + memzero(sequence_digest, sizeof(sequence_digest)); + memzero(outputs_digest, sizeof(outputs_digest)); + return true; +} + +bool zcash_compute_transparent_sighash_digest( + const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, + const ZcashTransparentOutputDigestInfo* outputs, size_t n_outputs, + uint32_t signable_input_index, uint8_t sighash_type, + uint8_t digest_out[32]) { + if (!digest_out || + !zcash_validate_transparent_digest_info(inputs, n_inputs, outputs, + n_outputs)) { + return false; + } + + if (sighash_type != 0x01 || signable_input_index >= n_inputs) { + return false; + } + + uint8_t prevouts_digest[32], amounts_digest[32], scripts_digest[32]; + uint8_t sequence_digest[32], outputs_digest[32], txin_sig_digest[32]; + zcash_hash_transparent_prevouts(inputs, n_inputs, prevouts_digest); + zcash_hash_transparent_amounts(inputs, n_inputs, amounts_digest); + zcash_hash_transparent_scripts(inputs, n_inputs, scripts_digest); + zcash_hash_transparent_sequences(inputs, n_inputs, sequence_digest); + zcash_hash_transparent_outputs(outputs, n_outputs, outputs_digest); + + zcash_hash_transparent_input(&inputs[signable_input_index], + txin_sig_digest); + + BLAKE2B_CTX ctx; + blake2b_InitPersonal(&ctx, 32, "ZTxIdTranspaHash", 16); + blake2b_Update(&ctx, &sighash_type, 1); + blake2b_Update(&ctx, prevouts_digest, 32); + blake2b_Update(&ctx, amounts_digest, 32); + blake2b_Update(&ctx, scripts_digest, 32); + blake2b_Update(&ctx, sequence_digest, 32); + blake2b_Update(&ctx, outputs_digest, 32); + blake2b_Update(&ctx, txin_sig_digest, 32); + blake2b_Final(&ctx, digest_out, 32); + + memzero(prevouts_digest, sizeof(prevouts_digest)); + memzero(amounts_digest, sizeof(amounts_digest)); + memzero(scripts_digest, sizeof(scripts_digest)); + memzero(sequence_digest, sizeof(sequence_digest)); + memzero(outputs_digest, sizeof(outputs_digest)); + memzero(txin_sig_digest, sizeof(txin_sig_digest)); + return true; +} + +ZcashPCZTSigningRequestStatus zcash_pczt_signing_request_status( + const ZcashPCZTSigningRequestMeta* meta) { + if (!meta || !meta->has_header_digest || !meta->has_orchard_digest) { + return ZCASH_PCZT_SIGNING_REQUEST_MISSING_TX_DIGESTS; + } + + if (meta->header_digest_size != 32 || meta->orchard_digest_size != 32) { + return ZCASH_PCZT_SIGNING_REQUEST_INVALID_DIGEST_SIZE; + } + + if (meta->has_transparent_digest && meta->transparent_digest_size != 32) { + return ZCASH_PCZT_SIGNING_REQUEST_INVALID_DIGEST_SIZE; + } + + (void)meta->sapling_digest_size; + if (meta->has_sapling_digest) { + return ZCASH_PCZT_SIGNING_REQUEST_UNSUPPORTED_SAPLING_COMPONENT; + } + + if (!meta->has_header_fields) { + return ZCASH_PCZT_SIGNING_REQUEST_MISSING_HEADER_FIELDS; + } + + if ((meta->n_transparent_inputs > 0 || meta->n_transparent_outputs > 0) && + (!meta->has_transparent_digest || meta->transparent_digest_size != 32)) { + return ZCASH_PCZT_SIGNING_REQUEST_MISSING_TRANSPARENT_DIGEST; + } + + if (!meta->has_orchard_flags || meta->orchard_flags > 0xff || + !meta->has_orchard_value_balance || !meta->has_orchard_anchor || + meta->orchard_anchor_size != 32) { + return ZCASH_PCZT_SIGNING_REQUEST_MISSING_ORCHARD_METADATA; + } + + return ZCASH_PCZT_SIGNING_REQUEST_OK; +} + +bool zcash_pczt_signing_request_is_clear( + const ZcashPCZTSigningRequestMeta* meta) { + return zcash_pczt_signing_request_status(meta) == + ZCASH_PCZT_SIGNING_REQUEST_OK; +} + /* * ZIP-32 §6.1 seed fingerprint: * diff --git a/lib/transport/CMakeLists.txt b/lib/transport/CMakeLists.txt index 805b27762..839f4017c 100644 --- a/lib/transport/CMakeLists.txt +++ b/lib/transport/CMakeLists.txt @@ -114,68 +114,84 @@ add_custom_command( ${CMAKE_BINARY_DIR}/lib/transport/google/protobuf/descriptor.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f types.options:." types.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,types.options types.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-ethereum.options:." messages-ethereum.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-ethereum.options messages-ethereum.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-eos.options:." messages-eos.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-eos.options messages-eos.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-nano.options:." messages-nano.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-nano.options messages-nano.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-binance.options:." messages-binance.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-binance.options messages-binance.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-cosmos.options:." messages-cosmos.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-cosmos.options messages-cosmos.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-osmosis.options:." messages-osmosis.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-osmosis.options messages-osmosis.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-ripple.options:." messages-ripple.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-ripple.options messages-ripple.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-tendermint.options:." messages-tendermint.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-tendermint.options messages-tendermint.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-thorchain.options:." messages-thorchain.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-thorchain.options messages-thorchain.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-mayachain.options:." messages-mayachain.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-mayachain.options messages-mayachain.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-solana.options:." messages-solana.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-solana.options messages-solana.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-tron.options:." messages-tron.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-tron.options messages-tron.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-ton.options:." messages-ton.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-ton.options messages-ton.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages-zcash.options:." messages-zcash.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages-zcash.options messages-zcash.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb - "--nanopb_out=-f messages.options:." messages.proto + --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --nanopb_out=. + --nanopb_opt=-f,messages.options messages.proto COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/lib/transport/*.pb.h ${CMAKE_BINARY_DIR}/include diff --git a/unittests/firmware/zcash.cpp b/unittests/firmware/zcash.cpp index b195be1cd..8974ff66a 100644 --- a/unittests/firmware/zcash.cpp +++ b/unittests/firmware/zcash.cpp @@ -1092,6 +1092,62 @@ TEST(Zcash, OrchardUnifiedAddress_RejectsInvalidInputs) { memzero(&keys, sizeof(keys)); } +TEST(Zcash, OrchardNoteCommitment_KnownVector) { + const uint8_t recipient[ZCASH_ORCHARD_RAW_RECEIVER_SIZE] = { + 0x3c, 0x15, 0x0e, 0x60, 0x98, 0xb8, 0x61, 0x71, 0x6c, 0xc7, 0xf6, + 0x28, 0x35, 0xf6, 0x9f, 0xeb, 0x30, 0x21, 0x93, 0xc9, 0x26, 0x60, + 0x44, 0x4f, 0x26, 0x62, 0x4f, 0xd1, 0x3e, 0x00, 0xea, 0x7a, 0xc7, + 0x74, 0xcd, 0x55, 0x07, 0x4d, 0x63, 0x67, 0xef, 0xef, 0x37}; + const uint64_t value = 12345678; + const uint8_t rho[32] = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00}; + const uint8_t rseed[32] = { + 0xca, 0xfe, 0xba, 0xbe, 0xde, 0xad, 0xbe, 0xef, + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}; + const uint8_t expected_cmx[32] = { + 0x02, 0xde, 0xfb, 0x39, 0xc8, 0xf2, 0xe1, 0xec, + 0xc9, 0x45, 0x18, 0x93, 0x73, 0xcf, 0x2a, 0x8e, + 0x21, 0xd4, 0xe1, 0x54, 0x39, 0x8e, 0xfa, 0x16, + 0x21, 0xd5, 0xfb, 0x98, 0x9e, 0x1d, 0xeb, 0x36}; + + uint8_t cmx[32]; + ASSERT_TRUE(zcash_orchard_compute_cmx(recipient, value, rho, rseed, cmx)); + EXPECT_TRUE(memcmp(cmx, expected_cmx, sizeof(cmx)) == 0); + + uint8_t tampered[ZCASH_ORCHARD_RAW_RECEIVER_SIZE]; + memcpy(tampered, recipient, sizeof(tampered)); + tampered[0] ^= 0x01; + ASSERT_TRUE(zcash_orchard_compute_cmx(tampered, value, rho, rseed, cmx)); + EXPECT_TRUE(memcmp(cmx, expected_cmx, sizeof(cmx)) != 0); + + memzero(cmx, sizeof(cmx)); + memzero(tampered, sizeof(tampered)); +} + +TEST(Zcash, OrchardReceiverToUnifiedAddress_KnownVector) { + const uint8_t recipient[ZCASH_ORCHARD_RAW_RECEIVER_SIZE] = { + 0x3c, 0x15, 0x0e, 0x60, 0x98, 0xb8, 0x61, 0x71, 0x6c, 0xc7, 0xf6, + 0x28, 0x35, 0xf6, 0x9f, 0xeb, 0x30, 0x21, 0x93, 0xc9, 0x26, 0x60, + 0x44, 0x4f, 0x26, 0x62, 0x4f, 0xd1, 0x3e, 0x00, 0xea, 0x7a, 0xc7, + 0x74, 0xcd, 0x55, 0x07, 0x4d, 0x63, 0x67, 0xef, 0xef, 0x37}; + char address[ZCASH_ORCHARD_UNIFIED_ADDRESS_SIZE]; + + ASSERT_TRUE(zcash_orchard_receiver_to_unified_address( + recipient, "u", address, sizeof(address))); + EXPECT_STREQ( + address, + "u1ut4h93zg5670tyqss7tneru3t7h6dk62r9hhyxyrpv3nwwe9dnyj5l0ruwygf74gp5f3zklj5xly4h8h54un3asugt9mn6gwfqsq3wq7"); + + EXPECT_FALSE(zcash_orchard_receiver_to_unified_address( + recipient, "u", address, 16)); + memzero(address, sizeof(address)); +} + TEST(Zcash, OrchardDiversifyHash_ReferenceVectors) { uint8_t gd[32]; ASSERT_TRUE(zcash_orchard_diversify_hash(EXPECTED_DIVERSIFIER_ALL_0, gd)); @@ -1247,6 +1303,284 @@ TEST(Zcash, AkSignBit_AlwaysClear) { } } +/* ── PCZT Signing Policy Tests ───────────────────────────────────── */ + +static ZcashPCZTSigningRequestMeta clear_pczt_meta(void) { + ZcashPCZTSigningRequestMeta meta = {}; + meta.has_header_digest = true; + meta.header_digest_size = 32; + meta.has_orchard_digest = true; + meta.orchard_digest_size = 32; + meta.has_orchard_flags = true; + meta.has_orchard_value_balance = true; + meta.has_orchard_anchor = true; + meta.orchard_anchor_size = 32; + meta.has_header_fields = true; + meta.n_transparent_inputs = 0; + meta.n_transparent_outputs = 0; + return meta; +} + +TEST(Zcash, PCZTSigningPolicy_AcceptsVerifiedShieldedOnlyRequest) { + ZcashPCZTSigningRequestMeta meta = clear_pczt_meta(); + + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_OK); + EXPECT_TRUE(zcash_pczt_signing_request_is_clear(&meta)); +} + +TEST(Zcash, PCZTSigningPolicy_RejectsMissingTransactionDigests) { + ZcashPCZTSigningRequestMeta meta = clear_pczt_meta(); + + meta.has_header_digest = false; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_MISSING_TX_DIGESTS); + EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); + + meta = clear_pczt_meta(); + meta.orchard_digest_size = 31; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_INVALID_DIGEST_SIZE); + EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); +} + +TEST(Zcash, PCZTSigningPolicy_RejectsMissingPlaintextHeaderFields) { + ZcashPCZTSigningRequestMeta meta = clear_pczt_meta(); + + meta.has_header_fields = false; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_MISSING_HEADER_FIELDS); + EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); +} + +TEST(Zcash, PCZTSigningPolicy_RejectsMissingOrchardMetadata) { + ZcashPCZTSigningRequestMeta meta = clear_pczt_meta(); + + meta.has_orchard_anchor = false; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_MISSING_ORCHARD_METADATA); + EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); + + meta = clear_pczt_meta(); + meta.has_orchard_flags = false; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_MISSING_ORCHARD_METADATA); + EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); +} + +TEST(Zcash, PCZTSigningPolicy_RejectsInvalidOptionalDigests) { + ZcashPCZTSigningRequestMeta meta = clear_pczt_meta(); + + meta.has_transparent_digest = true; + meta.transparent_digest_size = 31; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_INVALID_DIGEST_SIZE); + EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); +} + +TEST(Zcash, PCZTSigningPolicy_RejectsSaplingComponent) { + ZcashPCZTSigningRequestMeta meta = clear_pczt_meta(); + + meta.has_sapling_digest = true; + meta.sapling_digest_size = 32; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_UNSUPPORTED_SAPLING_COMPONENT); + EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); +} + +TEST(Zcash, PCZTSigningPolicy_RejectsTransparentComponentsWithoutDigest) { + ZcashPCZTSigningRequestMeta meta = clear_pczt_meta(); + meta.n_transparent_inputs = 1; + + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_MISSING_TRANSPARENT_DIGEST); + EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); + + meta.has_transparent_digest = true; + meta.transparent_digest_size = 32; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_OK); + EXPECT_TRUE(zcash_pczt_signing_request_is_clear(&meta)); + + meta = clear_pczt_meta(); + meta.n_transparent_outputs = 1; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_MISSING_TRANSPARENT_DIGEST); + EXPECT_FALSE(zcash_pczt_signing_request_is_clear(&meta)); + + meta.has_transparent_digest = true; + meta.transparent_digest_size = 32; + EXPECT_EQ(zcash_pczt_signing_request_status(&meta), + ZCASH_PCZT_SIGNING_REQUEST_OK); + EXPECT_TRUE(zcash_pczt_signing_request_is_clear(&meta)); +} + +static const uint8_t ZIP244_EXPECTED_HEADER_DIGEST[32] = { + 0x44, 0x4b, 0xe9, 0x38, 0x88, 0x1d, 0xc9, 0xf2, + 0x0a, 0xed, 0x88, 0x0c, 0x3a, 0x05, 0x94, 0xe5, + 0xc1, 0x22, 0x3e, 0xff, 0xc5, 0x75, 0xef, 0x05, + 0xda, 0xae, 0xe3, 0x45, 0x1b, 0xa2, 0xf4, 0x93}; + +static const uint8_t ZIP244_EXPECTED_EMPTY_TRANSPARENT_DIGEST[32] = { + 0xc3, 0x3f, 0x2e, 0x95, 0x70, 0x5f, 0xaa, 0xb3, + 0x5f, 0x8d, 0x53, 0x3f, 0xa6, 0x1e, 0x95, 0xc3, + 0xb7, 0xaa, 0xba, 0x07, 0x76, 0xb8, 0x74, 0xa9, + 0xf7, 0x4f, 0xc1, 0x27, 0x84, 0x37, 0x6a, 0x59}; + +static const uint8_t ZIP244_EXPECTED_TRANSPARENT_DIGEST[32] = { + 0xfa, 0xe5, 0x37, 0x7f, 0xa9, 0x3c, 0xc0, 0xc3, + 0x1d, 0x30, 0x39, 0x42, 0x21, 0x57, 0xce, 0x4b, + 0x9e, 0x7b, 0x12, 0x57, 0x00, 0x9f, 0x15, 0x90, + 0xe1, 0x62, 0x95, 0x62, 0x55, 0xbb, 0x2e, 0x84}; + +static const uint8_t ZIP244_EXPECTED_TRANSPARENT_SIGHASH_0[32] = { + 0x37, 0xa9, 0xc4, 0xec, 0x61, 0x87, 0x07, 0x20, + 0x5b, 0xcb, 0x47, 0x7b, 0xea, 0x4f, 0xda, 0x6d, + 0x61, 0x01, 0x62, 0xea, 0xaa, 0x5c, 0x9f, 0x33, + 0xe5, 0x59, 0x69, 0x02, 0x6e, 0x47, 0x6f, 0x23}; + +static const uint8_t ZIP244_EXPECTED_TRANSPARENT_SIGHASH_1[32] = { + 0x29, 0x4d, 0xb7, 0xaa, 0xf1, 0x65, 0x37, 0x4e, + 0x02, 0xda, 0xe1, 0x6f, 0xf3, 0xdd, 0x97, 0x78, + 0x8f, 0x4f, 0x5e, 0x2d, 0xc4, 0xe1, 0xb3, 0xf6, + 0x62, 0x73, 0x9e, 0xd3, 0x5b, 0x82, 0x08, 0x2f}; + +static const uint8_t ZIP244_P2PKH_SCRIPT_11[25] = { + 0x76, 0xa9, 0x14, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x88, 0xac}; + +static const uint8_t ZIP244_P2SH_SCRIPT_22[23] = { + 0xa9, 0x14, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x87}; + +static const uint8_t ZIP244_P2PKH_SCRIPT_33[25] = { + 0x76, 0xa9, 0x14, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x88, 0xac}; + +static const uint8_t ZIP244_P2SH_SCRIPT_44[23] = { + 0xa9, 0x14, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, + 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, + 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x87}; + +static void fill_zip244_txids(uint8_t txid0[32], uint8_t txid1[32]) { + for (size_t i = 0; i < 32; i++) { + txid0[i] = (uint8_t)i; + txid1[i] = (uint8_t)(i + 32); + } +} + +static void make_zip244_transparent_fixture( + ZcashTransparentInputDigestInfo inputs[2], + ZcashTransparentOutputDigestInfo outputs[2], uint8_t txid0[32], + uint8_t txid1[32]) { + fill_zip244_txids(txid0, txid1); + + inputs[0].prevout_txid = txid0; + inputs[0].prevout_index = 2; + inputs[0].sequence = 0xfffffffe; + inputs[0].value = 1234567890ULL; + inputs[0].script_pubkey = ZIP244_P2PKH_SCRIPT_11; + inputs[0].script_pubkey_size = sizeof(ZIP244_P2PKH_SCRIPT_11); + + inputs[1].prevout_txid = txid1; + inputs[1].prevout_index = 7; + inputs[1].sequence = 0xfffffffd; + inputs[1].value = 987654321ULL; + inputs[1].script_pubkey = ZIP244_P2SH_SCRIPT_22; + inputs[1].script_pubkey_size = sizeof(ZIP244_P2SH_SCRIPT_22); + + outputs[0].value = 2000000000ULL; + outputs[0].script_pubkey = ZIP244_P2PKH_SCRIPT_33; + outputs[0].script_pubkey_size = sizeof(ZIP244_P2PKH_SCRIPT_33); + + outputs[1].value = 1111111ULL; + outputs[1].script_pubkey = ZIP244_P2SH_SCRIPT_44; + outputs[1].script_pubkey_size = sizeof(ZIP244_P2SH_SCRIPT_44); +} + +TEST(Zcash, ComputeHeaderDigest_FromPlaintextFields) { + uint8_t digest[32] = {0}; + + ASSERT_TRUE(zcash_compute_header_digest( + 5, 0x26a7270a, 0xc2d6d0b4, 123456, 987654, digest)); + EXPECT_TRUE(memcmp(digest, ZIP244_EXPECTED_HEADER_DIGEST, 32) == 0); +} + +TEST(Zcash, ComputeTransparentDigest_DistinctFromPerInputSighash) { + ZcashTransparentInputDigestInfo inputs[2] = {}; + ZcashTransparentOutputDigestInfo outputs[2] = {}; + uint8_t txid0[32], txid1[32]; + make_zip244_transparent_fixture(inputs, outputs, txid0, txid1); + + uint8_t digest[32] = {0}; + uint8_t sighash0[32] = {0}; + uint8_t sighash1[32] = {0}; + + ASSERT_TRUE(zcash_compute_transparent_digest(inputs, 2, outputs, 2, digest)); + ASSERT_TRUE(zcash_compute_transparent_sighash_digest( + inputs, 2, outputs, 2, 0, 0x01, sighash0)); + ASSERT_TRUE(zcash_compute_transparent_sighash_digest( + inputs, 2, outputs, 2, 1, 0x01, sighash1)); + + EXPECT_TRUE(memcmp(digest, ZIP244_EXPECTED_TRANSPARENT_DIGEST, 32) == 0); + EXPECT_TRUE(memcmp(sighash0, ZIP244_EXPECTED_TRANSPARENT_SIGHASH_0, 32) == + 0); + EXPECT_TRUE(memcmp(sighash1, ZIP244_EXPECTED_TRANSPARENT_SIGHASH_1, 32) == + 0); + EXPECT_TRUE(memcmp(digest, sighash0, 32) != 0); + EXPECT_TRUE(memcmp(sighash0, sighash1, 32) != 0); +} + +TEST(Zcash, ComputeTransparentDigest_EmptyBundle) { + uint8_t digest[32] = {0}; + + ASSERT_TRUE(zcash_compute_transparent_digest(NULL, 0, NULL, 0, digest)); + EXPECT_TRUE(memcmp(digest, ZIP244_EXPECTED_EMPTY_TRANSPARENT_DIGEST, 32) == + 0); +} + +TEST(Zcash, ComputeTransparentSighash_RejectsUnsupportedRequest) { + ZcashTransparentInputDigestInfo inputs[2] = {}; + ZcashTransparentOutputDigestInfo outputs[2] = {}; + uint8_t txid0[32], txid1[32], digest[32]; + make_zip244_transparent_fixture(inputs, outputs, txid0, txid1); + + EXPECT_FALSE(zcash_compute_transparent_sighash_digest( + inputs, 2, outputs, 2, 2, 0x01, digest)); + EXPECT_FALSE(zcash_compute_transparent_sighash_digest( + inputs, 2, outputs, 2, 0, 0x02, digest)); +} + +TEST(Zcash, ComputeTransparentSighash_CommitsToOutputScriptAndValue) { + ZcashTransparentInputDigestInfo inputs[2] = {}; + ZcashTransparentOutputDigestInfo outputs[2] = {}; + uint8_t txid0[32], txid1[32]; + make_zip244_transparent_fixture(inputs, outputs, txid0, txid1); + + uint8_t original[32] = {0}; + uint8_t changed_script[32] = {0}; + uint8_t changed_value[32] = {0}; + + ASSERT_TRUE(zcash_compute_transparent_sighash_digest( + inputs, 2, outputs, 2, 0, 0x01, original)); + + outputs[0].script_pubkey = ZIP244_P2SH_SCRIPT_44; + outputs[0].script_pubkey_size = sizeof(ZIP244_P2SH_SCRIPT_44); + ASSERT_TRUE(zcash_compute_transparent_sighash_digest( + inputs, 2, outputs, 2, 0, 0x01, changed_script)); + EXPECT_TRUE(memcmp(original, changed_script, 32) != 0); + + outputs[0].script_pubkey = ZIP244_P2PKH_SCRIPT_33; + outputs[0].script_pubkey_size = sizeof(ZIP244_P2PKH_SCRIPT_33); + outputs[0].value++; + ASSERT_TRUE(zcash_compute_transparent_sighash_digest( + inputs, 2, outputs, 2, 0, 0x01, changed_value)); + EXPECT_TRUE(memcmp(original, changed_value, 32) != 0); +} + /* ── Sighash Computation Tests ───────────────────────────────────── */ TEST(Zcash, ComputeShieldedSighash_Deterministic) { From 85fc1232faf62e50062825cfc9a9afb27810263b Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 17:50:07 -0300 Subject: [PATCH 02/21] chore(zcash): point tests at report CI branch --- deps/python-keepkey | 2 +- docs/coin-integration/zcash-clearsign-handoff.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index c9c976866..2b34e6795 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit c9c9768662a21ff1f0b5ff12a1f89bf73f7c5d0b +Subproject commit 2b34e67953888160fc33a5df5059ea04923797bf diff --git a/docs/coin-integration/zcash-clearsign-handoff.md b/docs/coin-integration/zcash-clearsign-handoff.md index 7d1114078..6e3d6d4fd 100644 --- a/docs/coin-integration/zcash-clearsign-handoff.md +++ b/docs/coin-integration/zcash-clearsign-handoff.md @@ -116,10 +116,10 @@ Results: - `BitHighlander/device-protocol` `feat/zcash-clearsign-protocol` -> `6ec974e` - `BitHighlander/python-keepkey` - `feat/zcash-clearsign-tests` -> `c9c9768` + `feat/zcash-clearsign-tests` -> `2b34e67` - Firmware submodules now point at those commits: - `deps/device-protocol` -> `6ec974e` - - `deps/python-keepkey` -> `c9c9768` + - `deps/python-keepkey` -> `2b34e67` The python-keepkey PDF report now includes the 7.15 Zcash clear-signing section with the new digest rejection, transparent streaming, cmx binding, and From ae516d11fbc1d80668d045978cb264adee3b3f97 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 17:54:51 -0300 Subject: [PATCH 03/21] chore(zcash): point tests at github actions branch --- deps/python-keepkey | 2 +- docs/coin-integration/zcash-clearsign-handoff.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index 2b34e6795..b9a3f008b 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 2b34e67953888160fc33a5df5059ea04923797bf +Subproject commit b9a3f008b13aa0ec52a0424d6faf59aa2fb607c2 diff --git a/docs/coin-integration/zcash-clearsign-handoff.md b/docs/coin-integration/zcash-clearsign-handoff.md index 6e3d6d4fd..5380624e6 100644 --- a/docs/coin-integration/zcash-clearsign-handoff.md +++ b/docs/coin-integration/zcash-clearsign-handoff.md @@ -116,10 +116,10 @@ Results: - `BitHighlander/device-protocol` `feat/zcash-clearsign-protocol` -> `6ec974e` - `BitHighlander/python-keepkey` - `feat/zcash-clearsign-tests` -> `2b34e67` + `feature/zcash-clearsign-tests` -> `b9a3f00` - Firmware submodules now point at those commits: - `deps/device-protocol` -> `6ec974e` - - `deps/python-keepkey` -> `2b34e67` + - `deps/python-keepkey` -> `b9a3f00` The python-keepkey PDF report now includes the 7.15 Zcash clear-signing section with the new digest rejection, transparent streaming, cmx binding, and From 7d82b05a7807c816360a5360097d62822e56e7f8 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 18:00:26 -0300 Subject: [PATCH 04/21] style(zcash): satisfy github actions formatter --- lib/firmware/fsm_msg_zcash.h | 42 ++++++++++++++++-------------------- lib/firmware/zcash.c | 13 +++++------ 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/lib/firmware/fsm_msg_zcash.h b/lib/firmware/fsm_msg_zcash.h index 5362d1b37..2d4d54a26 100644 --- a/lib/firmware/fsm_msg_zcash.h +++ b/lib/firmware/fsm_msg_zcash.h @@ -97,7 +97,8 @@ static struct { uint32_t current_transparent_output; uint32_t n_transparent_inputs; uint32_t current_transparent_input; - ZcashTransparentOutputState transparent_outputs[ZCASH_MAX_TRANSPARENT_OUTPUTS]; + ZcashTransparentOutputState + transparent_outputs[ZCASH_MAX_TRANSPARENT_OUTPUTS]; ZcashTransparentInputState transparent_inputs[ZCASH_MAX_TRANSPARENT_INPUTS]; } zcash_signing; @@ -183,8 +184,8 @@ static bool zcash_verify_and_confirm_orchard_output( memzero(computed_cmx, sizeof(computed_cmx)); char address[ZCASH_ORCHARD_UNIFIED_ADDRESS_SIZE]; - if (!zcash_orchard_receiver_to_unified_address( - msg->recipient.bytes, "u", address, sizeof(address))) { + if (!zcash_orchard_receiver_to_unified_address(msg->recipient.bytes, "u", + address, sizeof(address))) { fsm_sendFailure(FailureType_Failure_SyntaxError, _("Invalid Orchard recipient")); return false; @@ -227,10 +228,8 @@ static bool zcash_compute_verified_fee(uint64_t* fee_out) { } const int64_t value_balance = zcash_signing.orchard_value_balance; - if ((value_balance > 0 && - net_transparent > INT64_MAX - value_balance) || - (value_balance < 0 && - net_transparent < INT64_MIN - value_balance)) { + if ((value_balance > 0 && net_transparent > INT64_MAX - value_balance) || + (value_balance < 0 && net_transparent < INT64_MIN - value_balance)) { return false; } @@ -542,8 +541,7 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { branch_id, msg->lock_time, msg->expiry_height, header_digest) || memcmp(header_digest, msg->header_digest.bytes, 32) != 0) { - fsm_sendFailure(FailureType_Failure_Other, - _("Header digest mismatch")); + fsm_sendFailure(FailureType_Failure_Other, _("Header digest mismatch")); layoutHome(); return; } @@ -685,10 +683,9 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { memcpy(t_digest, EMPTY_TRANSPARENT_DIGEST, 32); memcpy(s_digest, EMPTY_SAPLING_DIGEST, 32); - zcash_compute_shielded_sighash(header_digest, t_digest, s_digest, - msg->orchard_digest.bytes, - zcash_signing.branch_id, - zcash_signing.sighash); + zcash_compute_shielded_sighash( + header_digest, t_digest, s_digest, msg->orchard_digest.bytes, + zcash_signing.branch_id, zcash_signing.sighash); zcash_signing.has_device_sighash = true; zcash_signing.transparent_digest_verified = true; } else { @@ -707,11 +704,10 @@ void fsm_msgZcashSignPCZT(const ZcashSignPCZT* msg) { zcash_signing.orchard_value_balance = msg->orchard_value_balance; memcpy(zcash_signing.orchard_anchor, msg->orchard_anchor.bytes, 32); - blake2b_InitPersonal(&zcash_signing.compact_ctx, 32, "ZTxIdOrcActCHash", - 16); + blake2b_InitPersonal(&zcash_signing.compact_ctx, 32, "ZTxIdOrcActCHash", 16); blake2b_InitPersonal(&zcash_signing.memos_ctx, 32, "ZTxIdOrcActMHash", 16); - blake2b_InitPersonal(&zcash_signing.noncompact_ctx, 32, - "ZTxIdOrcActNHash", 16); + blake2b_InitPersonal(&zcash_signing.noncompact_ctx, 32, "ZTxIdOrcActNHash", + 16); zcash_signing.verify_orchard_digest = true; /* Request the first plaintext component. Transparent outputs are reviewed @@ -1136,9 +1132,9 @@ void fsm_msgZcashTransparentOutput(const ZcashTransparentOutput* msg) { } char address[64]; - if (!zcash_transparent_script_to_address( - msg->script_pubkey.bytes, msg->script_pubkey.size, address, - sizeof(address))) { + if (!zcash_transparent_script_to_address(msg->script_pubkey.bytes, + msg->script_pubkey.size, address, + sizeof(address))) { fsm_sendFailure(FailureType_Failure_SyntaxError, _("Unsupported transparent output script")); zcash_signing_abort(); @@ -1149,8 +1145,7 @@ void fsm_msgZcashTransparentOutput(const ZcashTransparentOutput* msg) { char amount_str[32]; zcash_format_amount(msg->amount, amount_str, sizeof(amount_str)); if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, "Zcash Output", - "Send transparent ZEC?\n%s\nAmount: %s", address, - amount_str)) { + "Send transparent ZEC?\n%s\nAmount: %s", address, amount_str)) { fsm_sendFailure(FailureType_Failure_ActionCancelled, _("Signing cancelled")); zcash_signing_abort(); @@ -1338,7 +1333,8 @@ void fsm_msgZcashTransparentInput(const ZcashTransparentInput* msg) { } if (!zcash_finalize_transparent_digest()) { - fsm_sendFailure(FailureType_Failure_Other, _("Transparent digest mismatch")); + fsm_sendFailure(FailureType_Failure_Other, + _("Transparent digest mismatch")); zcash_signing_abort(); layoutHome(); return; diff --git a/lib/firmware/zcash.c b/lib/firmware/zcash.c index 311aa595f..0447563eb 100644 --- a/lib/firmware/zcash.c +++ b/lib/firmware/zcash.c @@ -937,9 +937,8 @@ bool zcash_compute_transparent_digest( const ZcashTransparentInputDigestInfo* inputs, size_t n_inputs, const ZcashTransparentOutputDigestInfo* outputs, size_t n_outputs, uint8_t digest_out[32]) { - if (!digest_out || - !zcash_validate_transparent_digest_info(inputs, n_inputs, outputs, - n_outputs)) { + if (!digest_out || !zcash_validate_transparent_digest_info( + inputs, n_inputs, outputs, n_outputs)) { return false; } @@ -971,9 +970,8 @@ bool zcash_compute_transparent_sighash_digest( const ZcashTransparentOutputDigestInfo* outputs, size_t n_outputs, uint32_t signable_input_index, uint8_t sighash_type, uint8_t digest_out[32]) { - if (!digest_out || - !zcash_validate_transparent_digest_info(inputs, n_inputs, outputs, - n_outputs)) { + if (!digest_out || !zcash_validate_transparent_digest_info( + inputs, n_inputs, outputs, n_outputs)) { return false; } @@ -989,8 +987,7 @@ bool zcash_compute_transparent_sighash_digest( zcash_hash_transparent_sequences(inputs, n_inputs, sequence_digest); zcash_hash_transparent_outputs(outputs, n_outputs, outputs_digest); - zcash_hash_transparent_input(&inputs[signable_input_index], - txin_sig_digest); + zcash_hash_transparent_input(&inputs[signable_input_index], txin_sig_digest); BLAKE2B_CTX ctx; blake2b_InitPersonal(&ctx, 32, "ZTxIdTranspaHash", 16); From 70053f9183f637c596f3b1ee58b3f45c764201e1 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 18:16:26 -0300 Subject: [PATCH 05/21] ci: use nanopb plugin wrapper for macos dylib --- .github/workflows/ci.yml | 4 ++++ CMakeLists.txt | 3 +++ lib/transport/CMakeLists.txt | 32 ++++++++++++++++---------------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc627814d..dbd524aa5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -597,12 +597,16 @@ jobs: # CMAKE_POLICY_VERSION_MINIMUM works around vendored # googletest's pre-3.5 policy declaration. export PATH="$PATH:$(python -c 'import os, nanopb; print(os.path.dirname(nanopb.__file__))')/generator" + NANOPB_DIR="$(python -c 'import os, nanopb; print(os.path.dirname(nanopb.__file__))')" + NANOPB_PLUGIN="$(command -v protoc-gen-nanopb)" which protoc-gen-nanopb which nanopb_generator.py cmake \ -DKK_EMULATOR=1 \ -DKK_BUILD_DYLIB=1 \ -DKK_DEBUG_LINK=ON \ + -DNANOPB_DIR="$NANOPB_DIR" \ + -DNANOPB_PLUGIN="$NANOPB_PLUGIN" \ -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -B build-emu . diff --git a/CMakeLists.txt b/CMakeLists.txt index d8ee60e49..ea817426a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,9 @@ set(PROTOC_BINARY set(NANOPB_DIR /root/nanopb CACHE PATH "Path to the nanopb build") +set(NANOPB_PLUGIN + ${NANOPB_DIR}/generator/protoc-gen-nanopb + CACHE FILEPATH "Path to the nanopb protoc plugin") set(DEVICE_PROTOCOL ${CMAKE_SOURCE_DIR}/deps/device-protocol CACHE PATH "Path to device-protocol") diff --git a/lib/transport/CMakeLists.txt b/lib/transport/CMakeLists.txt index 839f4017c..8766661b4 100644 --- a/lib/transport/CMakeLists.txt +++ b/lib/transport/CMakeLists.txt @@ -114,82 +114,82 @@ add_custom_command( ${CMAKE_BINARY_DIR}/lib/transport/google/protobuf/descriptor.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,types.options types.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-ethereum.options messages-ethereum.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-eos.options messages-eos.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-nano.options messages-nano.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-binance.options messages-binance.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-cosmos.options messages-cosmos.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-osmosis.options messages-osmosis.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-ripple.options messages-ripple.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-tendermint.options messages-tendermint.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-thorchain.options messages-thorchain.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-mayachain.options messages-mayachain.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-solana.options messages-solana.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-tron.options messages-tron.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-ton.options messages-ton.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages-zcash.options messages-zcash.proto COMMAND ${PROTOC_BINARY} -I. -I/usr/include - --plugin=protoc-gen-nanopb=${NANOPB_DIR}/generator/protoc-gen-nanopb + --plugin=protoc-gen-nanopb=${NANOPB_PLUGIN} --nanopb_out=. --nanopb_opt=-f,messages.options messages.proto COMMAND From dda203057ec70beccf003ded72d6b6de8e8f6a64 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 18:44:14 -0300 Subject: [PATCH 06/21] ci: pin protobuf for python integration image --- scripts/emulator/python-keepkey.Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/emulator/python-keepkey.Dockerfile b/scripts/emulator/python-keepkey.Dockerfile index 26f358a0b..320ccc743 100644 --- a/scripts/emulator/python-keepkey.Dockerfile +++ b/scripts/emulator/python-keepkey.Dockerfile @@ -2,5 +2,6 @@ FROM kktech/firmware:v15 WORKDIR /kkemu COPY ./ /kkemu +RUN python3 -m pip install --no-cache-dir "protobuf==3.20.3" ENTRYPOINT ["/bin/sh", "./scripts/emulator/python-keepkey-tests.sh"] From 4531d4b984f4c5c44d271f6d215061ef98450a36 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 18:54:24 -0300 Subject: [PATCH 07/21] ci: use compatible protobuf for integration image --- scripts/emulator/python-keepkey.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/emulator/python-keepkey.Dockerfile b/scripts/emulator/python-keepkey.Dockerfile index 320ccc743..12fc9ed67 100644 --- a/scripts/emulator/python-keepkey.Dockerfile +++ b/scripts/emulator/python-keepkey.Dockerfile @@ -2,6 +2,6 @@ FROM kktech/firmware:v15 WORKDIR /kkemu COPY ./ /kkemu -RUN python3 -m pip install --no-cache-dir "protobuf==3.20.3" +RUN python3 -m pip install --no-cache-dir "protobuf==3.19.6" ENTRYPOINT ["/bin/sh", "./scripts/emulator/python-keepkey-tests.sh"] From 98b4fd9e7570ef9010993ccb600c5cbca9670e35 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 19:19:27 -0300 Subject: [PATCH 08/21] fix(ci): restore compatible zcash protobuf generation --- deps/python-keepkey | 2 +- .../zcash-clearsign-handoff.md | 83 +++++++++++++++++-- scripts/emulator/python-keepkey.Dockerfile | 1 - 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index b9a3f008b..045f8fafa 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit b9a3f008b13aa0ec52a0424d6faf59aa2fb607c2 +Subproject commit 045f8fafa415316b25b5d182dce5c6a2843356e2 diff --git a/docs/coin-integration/zcash-clearsign-handoff.md b/docs/coin-integration/zcash-clearsign-handoff.md index 5380624e6..69a8cb3e1 100644 --- a/docs/coin-integration/zcash-clearsign-handoff.md +++ b/docs/coin-integration/zcash-clearsign-handoff.md @@ -116,14 +116,16 @@ Results: - `BitHighlander/device-protocol` `feat/zcash-clearsign-protocol` -> `6ec974e` - `BitHighlander/python-keepkey` - `feature/zcash-clearsign-tests` -> `b9a3f00` + `feature/zcash-clearsign-tests` -> `045f8fa` - Firmware submodules now point at those commits: - `deps/device-protocol` -> `6ec974e` - - `deps/python-keepkey` -> `b9a3f00` + - `deps/python-keepkey` -> `045f8fa` -The python-keepkey PDF report now includes the 7.15 Zcash clear-signing section -with the new digest rejection, transparent streaming, cmx binding, and -privacy-output tests. Its screenshot filter selects the positive display flows: +The firmware GitHub Actions PDF report is generated through the checked-out +`deps/python-keepkey` submodule. The submodule report script now includes the +7.15 Zcash clear-signing section with the new digest rejection, transparent +streaming, cmx binding, and privacy-output tests. Its screenshot filter selects +the positive display flows: - `test_multi_action_device_sighash` - `test_signatures_are_64_bytes` @@ -134,6 +136,67 @@ Screenshot capture is still driven by `KEEPKEY_SCREENSHOT=1` and ButtonRequest callbacks; `scripts/generate-test-report.py --screenshots ` embeds the captured `btn*.png` frames for tests with screenshot labels. +## Firmware CI Handoff + +Firmware GitHub Actions on `BitHighlander/keepkey-firmware` are authoritative +for this branch. Do not treat standalone python-keepkey CI as the target; the +firmware workflow checks out `deps/python-keepkey` at the submodule SHA and uses +that test/report tooling inside the firmware run. + +Current pushed state: + +- Firmware: `BitHighlander/keepkey-firmware` + `feature/clearsign-txs` +- Device protocol submodule: `BitHighlander/device-protocol` + `feat/zcash-clearsign-protocol` -> + `6ec974eef1fecb713be0916436ec31fefe4f094e` +- Python test/report submodule: `BitHighlander/python-keepkey` + `feature/zcash-clearsign-tests` -> + `045f8fafa415316b25b5d182dce5c6a2843356e2` + +Failed run that diagnosed the protobuf break: + +- `https://github.com/BitHighlander/keepkey-firmware/actions/runs/26192217728` +- Event: push +- Branch: `feature/clearsign-txs` +- Head SHA: `4531d4b984f4c5c44d271f6d215061ef98450a36` +- Failure: `python-integration-tests` failed before test execution because + `keepkeylib/messages_zcash_pb2.py` imported + `google.protobuf.internal.builder`, which is not available in the firmware CI + Python/protobuf runtime. + +CI fixes already applied in this branch: + +- `lint-format`: clang-format fixes for generated clear-signing C changes. +- `python-dylib-tests`: CMake now uses the nanopb plugin wrapper so macOS can + find the protobuf dylib while generating nanopb sources. +- `python-integration-tests`: the Zcash Python protobuf was regenerated with + the stack's compatible `grpc-tools` `protoc 3.19.1` output style so it does + not require `google.protobuf.internal.builder`. The temporary Docker protobuf + pin was removed. + +Artifact validation checklist: + +- `python-integration-tests` must pass in firmware CI, because this is where + the emulator-backed Python clear-signing tests run. +- The run must upload `python-test-results` and `oled-screenshots`. +- The run must then execute `generate-test-report` and upload `test-report` + containing `test-report.pdf`. +- The PDF should contain Zcash 7.15.0 rows `Z5` through `Z17`. +- The PDF should embed screenshots for: + - `test_multi_action_device_sighash` + - `test_signatures_are_64_bytes` + - `test_transparent_shielding_single_input` + - `test_transparent_shielding_multiple_inputs` +- The screenshot/PDF values must match the firmware-computed displays: + - Orchard privacy outputs show a ZIP-316 Orchard-only Unified Address and + amount derived only after firmware verifies `recipient = d || pk_d`, + `value`, `rseed`, action nullifier, and `cmx`. + - Transparent outputs show the derived transparent address/script target and + amount from streamed plaintext. + - Shielding flows show the expected transparent input/output totals and the + final computed fee confirmation before signatures are released. + ## Local Tooling Notes - Nanopb generation invokes `env python`. This machine only had `python3`, so a @@ -141,10 +204,12 @@ captured `btn*.png` frames for tests with screenshot labels. `/private/tmp/kk-python-shim/python -> /opt/homebrew/bin/python3`. - `PATH=/private/tmp/kk-python-shim:$PATH` is needed for local firmware rebuilds unless a real `python` executable is installed. -- Local `protoc` is `libprotoc 34.1`; regenerating - `keepkeylib/messages_zcash_pb2.py` produced a noisy modern-style diff. The - checked-in python-keepkey branch removes the generated runtime-version guard - so it imports with the local protobuf runtimes used during verification. +- Local `protoc` is `libprotoc 34.1`; do not use it for checked-in + python-keepkey protobuf output on this branch. It emits modern + `google.protobuf.internal.builder` code that fails in firmware CI. Regenerate + `keepkeylib/messages_zcash_pb2.py` with the stack's + `modules/device-protocol/node_modules/grpc-tools/bin/protoc` 3.19.1 binary or + an equivalent no-builder generator. - `PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python` is useful for local imports with the older checked-in protobuf files. diff --git a/scripts/emulator/python-keepkey.Dockerfile b/scripts/emulator/python-keepkey.Dockerfile index 12fc9ed67..26f358a0b 100644 --- a/scripts/emulator/python-keepkey.Dockerfile +++ b/scripts/emulator/python-keepkey.Dockerfile @@ -2,6 +2,5 @@ FROM kktech/firmware:v15 WORKDIR /kkemu COPY ./ /kkemu -RUN python3 -m pip install --no-cache-dir "protobuf==3.19.6" ENTRYPOINT ["/bin/sh", "./scripts/emulator/python-keepkey-tests.sh"] From 1cb48d64687cc98a25f3da806782ced9d855ac93 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 19:57:39 -0300 Subject: [PATCH 09/21] fix(ci): preserve legacy zcash protobuf style --- deps/python-keepkey | 2 +- .../zcash-clearsign-handoff.md | 40 ++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index 045f8fafa..c7e9ade93 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 045f8fafa415316b25b5d182dce5c6a2843356e2 +Subproject commit c7e9ade93df939b1e65787d677fa60d044d9cefc diff --git a/docs/coin-integration/zcash-clearsign-handoff.md b/docs/coin-integration/zcash-clearsign-handoff.md index 69a8cb3e1..a88bd0bc4 100644 --- a/docs/coin-integration/zcash-clearsign-handoff.md +++ b/docs/coin-integration/zcash-clearsign-handoff.md @@ -116,10 +116,10 @@ Results: - `BitHighlander/device-protocol` `feat/zcash-clearsign-protocol` -> `6ec974e` - `BitHighlander/python-keepkey` - `feature/zcash-clearsign-tests` -> `045f8fa` + `feature/zcash-clearsign-tests` -> `c7e9ade` - Firmware submodules now point at those commits: - `deps/device-protocol` -> `6ec974e` - - `deps/python-keepkey` -> `045f8fa` + - `deps/python-keepkey` -> `c7e9ade` The firmware GitHub Actions PDF report is generated through the checked-out `deps/python-keepkey` submodule. The submodule report script now includes the @@ -152,9 +152,9 @@ Current pushed state: `6ec974eef1fecb713be0916436ec31fefe4f094e` - Python test/report submodule: `BitHighlander/python-keepkey` `feature/zcash-clearsign-tests` -> - `045f8fafa415316b25b5d182dce5c6a2843356e2` + `c7e9ade93df939b1e65787d677fa60d044d9cefc` -Failed run that diagnosed the protobuf break: +Failed runs that diagnosed the protobuf break: - `https://github.com/BitHighlander/keepkey-firmware/actions/runs/26192217728` - Event: push @@ -164,16 +164,27 @@ Failed run that diagnosed the protobuf break: `keepkeylib/messages_zcash_pb2.py` imported `google.protobuf.internal.builder`, which is not available in the firmware CI Python/protobuf runtime. +- `https://github.com/BitHighlander/keepkey-firmware/actions/runs/26193294071` +- Event: push +- Branch: `feature/clearsign-txs` +- Head SHA: `98b4fd9e7570ef9010993ccb600c5cbca9670e35` +- Failure: `python-integration-tests` still failed before test execution because + descriptor-pool style output assigned `DESCRIPTOR` from + `AddSerializedFile(...)`, which returns `None` under the firmware CI runtime. + That confirmed the fix must preserve python-keepkey's legacy + `_descriptor.FileDescriptor` plus explicit descriptor/reflection layout. CI fixes already applied in this branch: - `lint-format`: clang-format fixes for generated clear-signing C changes. - `python-dylib-tests`: CMake now uses the nanopb plugin wrapper so macOS can find the protobuf dylib while generating nanopb sources. -- `python-integration-tests`: the Zcash Python protobuf was regenerated with - the stack's compatible `grpc-tools` `protoc 3.19.1` output style so it does - not require `google.protobuf.internal.builder`. The temporary Docker protobuf - pin was removed. +- `python-integration-tests`: the temporary Docker protobuf pin was removed, + and the Zcash Python protobuf was restored to python-keepkey's legacy + checked-in style: `_descriptor.FileDescriptor`, explicit + `_descriptor.Descriptor` / `_descriptor.FieldDescriptor` blocks, and + `_sym_db.RegisterFileDescriptor(DESCRIPTOR)`. No protoc/runtime version + change is part of the fix. Artifact validation checklist: @@ -204,12 +215,13 @@ Artifact validation checklist: `/private/tmp/kk-python-shim/python -> /opt/homebrew/bin/python3`. - `PATH=/private/tmp/kk-python-shim:$PATH` is needed for local firmware rebuilds unless a real `python` executable is installed. -- Local `protoc` is `libprotoc 34.1`; do not use it for checked-in - python-keepkey protobuf output on this branch. It emits modern - `google.protobuf.internal.builder` code that fails in firmware CI. Regenerate - `keepkeylib/messages_zcash_pb2.py` with the stack's - `modules/device-protocol/node_modules/grpc-tools/bin/protoc` 3.19.1 binary or - an equivalent no-builder generator. +- Do not use local `protoc`, `grpc-tools`, or CI dependency pins to update + checked-in python-keepkey protobuf files for this branch. python-keepkey keeps + `_pb2.py` files in a legacy hand-maintained protobuf 3.x-compatible layout. + Preserve the existing release/develop style and add only the new fields or + messages needed for the protocol surface. Modern `builder` output and + descriptor-pool `AddSerializedFile(...)` output both fail in the firmware + integration runtime. - `PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python` is useful for local imports with the older checked-in protobuf files. From c5ee9453618ef6a65b57d3bfe4d574c17ac4bcae Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 20:16:16 -0300 Subject: [PATCH 10/21] fix(ci): include zcash pczt screenshots --- .../zcash-clearsign-handoff.md | 20 +++++++++++++++++++ scripts/emulator/python-keepkey-tests.sh | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/coin-integration/zcash-clearsign-handoff.md b/docs/coin-integration/zcash-clearsign-handoff.md index a88bd0bc4..56ab02cf3 100644 --- a/docs/coin-integration/zcash-clearsign-handoff.md +++ b/docs/coin-integration/zcash-clearsign-handoff.md @@ -185,6 +185,26 @@ CI fixes already applied in this branch: `_descriptor.Descriptor` / `_descriptor.FieldDescriptor` blocks, and `_sym_db.RegisterFileDescriptor(DESCRIPTOR)`. No protoc/runtime version change is part of the fix. +- `oled-screenshots`: the Docker-side screenshot phase now extracts + `FW_VERSION` with BusyBox-compatible `grep -Eo '[0-9]+\.[0-9]+\.[0-9]+'`. + The previous `grep -oP` form failed inside the base image, fell back to + `7.14.0`, and filtered out the 7.15.0 Zcash PCZT screenshot tests even + though the final Ubuntu `generate-test-report` job produced the 7.15.0 PDF + rows. + +Last inspected successful run before the screenshot-filter fix: + +- `https://github.com/BitHighlander/keepkey-firmware/actions/runs/26194803188` +- Head SHA: `1cb48d64687cc98a25f3da806782ced9d855ac93` +- Result: all required jobs passed; `python-integration-tests` reported + `405 passed, 34 skipped`. +- PDF artifact: `test-report.pdf` contained + `Zcash Orchard Clear-Signing [NEW] -- 17/17 passed`. +- Gap found during artifact audit: `oled-screenshots` uploaded 301 PNGs, but + only legacy `msg_signtx_zcash` screenshots appeared for Zcash because the + Docker screenshot phase detected `FW_VERSION=7.14.0`. The current branch + fixes that detection and needs one more CI run to confirm PCZT screenshots + appear in the image artifact and embedded PDF. Artifact validation checklist: diff --git a/scripts/emulator/python-keepkey-tests.sh b/scripts/emulator/python-keepkey-tests.sh index 28b122fc9..6c64792af 100755 --- a/scripts/emulator/python-keepkey-tests.sh +++ b/scripts/emulator/python-keepkey-tests.sh @@ -43,7 +43,7 @@ echo "=== End diagnostic ===" echo "=== Phase 1: Report-driven screenshot capture ===" # Detect firmware version from CMakeLists if not set in env if [ -z "$FW_VERSION" ]; then - FW_VERSION=$(sed -n '/^project/,/)/p' /kkemu/CMakeLists.txt | grep -oP '\d+\.\d+\.\d+' || echo "7.14.0") + FW_VERSION=$(sed -n '/^project/,/)/p' /kkemu/CMakeLists.txt | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' || echo "7.14.0") echo "Detected FW_VERSION=$FW_VERSION from CMakeLists.txt" fi export FW_VERSION From 682665c70f01c4d25d8dc6ab3b3271ea8d5257a6 Mon Sep 17 00:00:00 2001 From: highlander Date: Wed, 20 May 2026 21:32:00 -0300 Subject: [PATCH 11/21] fix(ci): avoid transparent input screenshot stall --- deps/python-keepkey | 2 +- .../zcash-clearsign-handoff.md | 36 ++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index c7e9ade93..41bf86a53 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit c7e9ade93df939b1e65787d677fa60d044d9cefc +Subproject commit 41bf86a534e6cdd0ba0532cbf6bc23f84d2e03e7 diff --git a/docs/coin-integration/zcash-clearsign-handoff.md b/docs/coin-integration/zcash-clearsign-handoff.md index 56ab02cf3..0bfca1376 100644 --- a/docs/coin-integration/zcash-clearsign-handoff.md +++ b/docs/coin-integration/zcash-clearsign-handoff.md @@ -98,6 +98,11 @@ git diff --check PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python python3 -c "import sys; sys.path.insert(0, 'deps/python-keepkey'); from keepkeylib import messages_zcash_pb2 as z; a=z.ZcashPCZTAction(index=0, recipient=b'1'*43, rseed=b'2'*32, value=1); assert a.HasField('recipient') and a.HasField('rseed'); print('zcash orchard metadata protobuf smoke ok')" PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python python3 -c "import sys; sys.path.insert(0, 'deps/python-keepkey'); from keepkeylib import mapping; from keepkeylib import messages_zcash_pb2 as z; assert mapping.get_type(z.ZcashTransparentOutput(index=0)) == 1310; assert mapping.get_type(z.ZcashTransparentAck(next_input_index=0)) == 1311; assert mapping.get_type(z.ZcashTransparentSigned(signatures=[b'0'])) == 1307; msg=z.ZcashSignPCZT(n_transparent_outputs=1,n_transparent_inputs=1); assert msg.HasField('n_transparent_outputs'); print('zcash transparent protobuf smoke ok')" PYTHONPYCACHEPREFIX=/private/tmp/kk-pycache python3 -m py_compile deps/python-keepkey/tests/test_msg_zcash_sign_pczt.py +PYTHONPYCACHEPREFIX=/private/tmp/kk-pycache python3 -m py_compile ../python-keepkey/keepkeylib/client.py +cd scripts/emulator +docker compose build python-keepkey +docker compose up -d kkemu +docker compose run --rm --no-deps --entrypoint pytest -e FW_VERSION=7.15.0 -e KEEPKEY_SCREENSHOT=1 -e SCREENSHOT_DIR=/kkemu/test-reports/screenshots -e KK_TRANSPORT_MAIN=kkemu:11044 -e KK_TRANSPORT_DEBUG=kkemu:11045 --workdir /kkemu/deps/python-keepkey/tests python-keepkey -v --tb=short -s test_msg_zcash_sign_pczt.py::TestZcashSignPCZT::test_multi_action_device_sighash test_msg_zcash_sign_pczt.py::TestZcashSignPCZT::test_signatures_are_64_bytes test_msg_zcash_sign_pczt.py::TestZcashSignPCZT::test_transparent_shielding_single_input test_msg_zcash_sign_pczt.py::TestZcashSignPCZT::test_transparent_shielding_multiple_inputs ``` Results: @@ -110,16 +115,20 @@ Results: - Python Zcash transparent protobuf smoke test passes. - Python test file syntax check passes with bytecode cache redirected to `/private/tmp/kk-pycache`. +- python-keepkey client syntax check passes with bytecode cache redirected to + `/private/tmp/kk-pycache`. +- Focused Docker PCZT screenshot run passes: 4 tests passed and 24 PNGs were + captured. ## Published Dependencies - `BitHighlander/device-protocol` `feat/zcash-clearsign-protocol` -> `6ec974e` - `BitHighlander/python-keepkey` - `feature/zcash-clearsign-tests` -> `c7e9ade` + `feature/zcash-clearsign-tests` -> `41bf86a` - Firmware submodules now point at those commits: - `deps/device-protocol` -> `6ec974e` - - `deps/python-keepkey` -> `c7e9ade` + - `deps/python-keepkey` -> `41bf86a` The firmware GitHub Actions PDF report is generated through the checked-out `deps/python-keepkey` submodule. The submodule report script now includes the @@ -152,7 +161,7 @@ Current pushed state: `6ec974eef1fecb713be0916436ec31fefe4f094e` - Python test/report submodule: `BitHighlander/python-keepkey` `feature/zcash-clearsign-tests` -> - `c7e9ade93df939b1e65787d677fa60d044d9cefc` + `41bf86a534e6cdd0ba0532cbf6bc23f84d2e03e7` Failed runs that diagnosed the protobuf break: @@ -203,8 +212,25 @@ Last inspected successful run before the screenshot-filter fix: - Gap found during artifact audit: `oled-screenshots` uploaded 301 PNGs, but only legacy `msg_signtx_zcash` screenshots appeared for Zcash because the Docker screenshot phase detected `FW_VERSION=7.14.0`. The current branch - fixes that detection and needs one more CI run to confirm PCZT screenshots - appear in the image artifact and embedded PDF. + fixes that detection. + +Last inspected run after the screenshot-filter fix: + +- `https://github.com/BitHighlander/keepkey-firmware/actions/runs/26195503040` +- Head SHA: `c5ee9453618ef6a65b57d3bfe4d574c17ac4bcae` +- Result: cancelled before artifacts were copied out of the Docker test + container, so the generated PDF was not useful. +- Useful signal: the Docker screenshot phase detected `FW_VERSION=7.15.0` and + selected all four PCZT screenshot tests. +- Failure mode reproduced locally: with `KEEPKEY_SCREENSHOT=1`, + `test_transparent_shielding_multiple_inputs` stalled after the internal + transparent-input `ButtonRequest_SignTx` prompts and before + `ZcashTransparentSigned`. Without screenshot capture the same test passed. +- Fix applied in python-keepkey `41bf86a`: suppress OLED capture only while the + host streams transparent inputs. The output and fee confirmation + `ButtonRequest` screens still capture screenshots for the report. +- Local verification after the fix: the four focused PCZT screenshot tests pass + in Docker and capture 24 PNGs across the expected screenshot directories. Artifact validation checklist: From 5a417f41aefa1ae1a0eb5359adc0910ce16b0403 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 00:11:43 -0300 Subject: [PATCH 12/21] fix: security bugfixes for 7.14.2 Bug 1 (TRON display mismatch): fsm_msg_tron.h displayed deprecated to_address/amount fields that are NOT part of raw_data being signed. A malicious host could show one transfer and get a different tx signed. Replaced with a single blind-sign prompt showing only the raw_data size. Bug 5 (ETH RLP leading zeros): ethereum.c hashed nonce/gas/value via hash_rlp_field which preserves leading zero bytes. Per the Ethereum yellow paper, integer fields must have leading zeros stripped. Added hash_rlp_bytes_stripped() and applied it to nonce, gas_price, gas_limit, max_fee_per_gas, max_priority_fee_per_gas, and value. Bug 6 (EIP-712 cancel ignored): confirmName() and confirmValue() cast review() return value to void, allowing signing to continue after user cancels. Added USER_CANCELLED error code and propagate cancellation. Bug 7 (EIP-712 strtoll overflow): strtoll silently wraps values above int64_max. Added errno check, endptr validation, and rejection of negative values for uint types. Bug 8 (token chain_id truncation): TokenType.chain_id was uint8_t, silently truncating chainIds > 255 (Base=8453, Arbitrum=42161, etc.). Widened to uint32_t throughout: struct, tokenByChainAddress(), tokenByTicker(). --- include/keepkey/firmware/eip712.h | 3 ++- include/keepkey/firmware/ethereum_tokens.h | 6 ++--- lib/firmware/eip712.c | 29 ++++++++++++++++------ lib/firmware/ethereum.c | 17 +++++++------ lib/firmware/ethereum_tokens.c | 4 +-- lib/firmware/fsm_msg_tron.h | 26 ++++++------------- 6 files changed, 46 insertions(+), 39 deletions(-) diff --git a/include/keepkey/firmware/eip712.h b/include/keepkey/firmware/eip712.h index 686acd6de..9b5ba1a9a 100644 --- a/include/keepkey/firmware/eip712.h +++ b/include/keepkey/firmware/eip712.h @@ -95,8 +95,9 @@ typedef enum { DOMAIN = 1, MESSAGE } dm; #define JSON_TYPE_T_NOVAL 31 #define ADDR_STRING_NULL 32 #define JSON_TYPE_WNOVAL 33 +#define USER_CANCELLED 34 -#define LAST_ERROR JSON_TYPE_WNOVAL +#define LAST_ERROR USER_CANCELLED int encode(const json_t* jsonTypes, const json_t* jsonVals, const char* typeS, uint8_t* hashRet); diff --git a/include/keepkey/firmware/ethereum_tokens.h b/include/keepkey/firmware/ethereum_tokens.h index 7e26bd164..253c9df01 100644 --- a/include/keepkey/firmware/ethereum_tokens.h +++ b/include/keepkey/firmware/ethereum_tokens.h @@ -39,7 +39,7 @@ enum { typedef struct _TokenType { const char* const address; const char* const ticker; - uint8_t chain_id; + uint32_t chain_id; uint8_t decimals; } TokenType; @@ -51,7 +51,7 @@ extern const TokenType* UnknownToken; const TokenType* tokenIter(int32_t* ctr); -const TokenType* tokenByChainAddress(uint8_t chain_id, const uint8_t* address); +const TokenType* tokenByChainAddress(uint32_t chain_id, const uint8_t* address); /// Tokens don't have unique tickers, so this might not return the one you're /// looking for :/ @@ -64,7 +64,7 @@ const TokenType* tokenByChainAddress(uint8_t chain_id, const uint8_t* address); /// \param[out] token The found token, assuming it was uniquely determinable. /// \returns true iff the token can be uniquely found in the list of known /// tokens. -bool tokenByTicker(uint8_t chain_id, const char* ticker, +bool tokenByTicker(uint32_t chain_id, const char* ticker, const TokenType** token); void coinFromToken(CoinType* coin, const TokenType* token); diff --git a/lib/firmware/eip712.c b/lib/firmware/eip712.c index 1fec75f4b..c3f36c13a 100644 --- a/lib/firmware/eip712.c +++ b/lib/firmware/eip712.c @@ -31,6 +31,7 @@ strings and address should be prefixed by 0x */ +#include #include #include #include @@ -288,16 +289,20 @@ int encodeBytesN(const char* typeT, const char* string, uint8_t* encoded) { int confirmName(const char* name, bool valAvailable) { if (valAvailable) { nameForValue = name; - } else { - (void)review(ButtonRequestType_ButtonRequest_Other, "MESSAGE DATA", - "Press button to continue for\n\"%s\" values", name); + return SUCCESS; + } + if (!review(ButtonRequestType_ButtonRequest_Other, "MESSAGE DATA", + "Press button to continue for\n\"%s\" values", name)) { + return USER_CANCELLED; } return SUCCESS; } int confirmValue(const char* value) { - (void)review(ButtonRequestType_ButtonRequest_Other, "MESSAGE DATA", "%s %s", - nameForValue, value); + if (!review(ButtonRequestType_ButtonRequest_Other, "MESSAGE DATA", "%s %s", + nameForValue, value)) { + return USER_CANCELLED; + } return SUCCESS; } @@ -547,8 +552,18 @@ int parseVals(const json_t* eip712Types, const json_t* jType, encBytes[ctr] = 0; } } - // all int strings are assumed to be base 10 and fit into 64 bits - long long intVal = strtoll(valStr, NULL, 10); + /* EIP-712 int/uint values must fit in 64 bits (firmware limit). + * Reject values that overflow strtoll to avoid silent misencoding. */ + char* endptr = NULL; + errno = 0; + long long intVal = strtoll(valStr, &endptr, 10); + if (errno == ERANGE || endptr == valStr || *endptr != '\0') { + return GENERAL_ERROR; + } + /* uint types must not be negative */ + if (0 == strncmp("uint", typeType, 4) && intVal < 0) { + return GENERAL_ERROR; + } // Needs to be big endian, so add to encBytes appropriately encBytes[24] = (intVal >> 56) & 0xff; encBytes[25] = (intVal >> 48) & 0xff; diff --git a/lib/firmware/ethereum.c b/lib/firmware/ethereum.c index 26ee3d873..08c37c7ea 100644 --- a/lib/firmware/ethereum.c +++ b/lib/firmware/ethereum.c @@ -865,21 +865,22 @@ void ethereum_signing_init(EthereumSignTx* msg, const HDNode* node, hash_rlp_field((uint8_t*)(&chain_id), sizeof(uint8_t)); } - hash_rlp_field(msg->nonce.bytes, msg->nonce.size); + hash_rlp_bytes_stripped(msg->nonce.bytes, msg->nonce.size); if (msg->has_max_fee_per_gas) { if (msg->has_max_priority_fee_per_gas) { - hash_rlp_field(msg->max_priority_fee_per_gas.bytes, - msg->max_priority_fee_per_gas.size); + hash_rlp_bytes_stripped(msg->max_priority_fee_per_gas.bytes, + msg->max_priority_fee_per_gas.size); } - hash_rlp_field(msg->max_fee_per_gas.bytes, msg->max_fee_per_gas.size); + hash_rlp_bytes_stripped(msg->max_fee_per_gas.bytes, + msg->max_fee_per_gas.size); } else { - hash_rlp_field(msg->gas_price.bytes, msg->gas_price.size); + hash_rlp_bytes_stripped(msg->gas_price.bytes, msg->gas_price.size); } - hash_rlp_field(msg->gas_limit.bytes, msg->gas_limit.size); - hash_rlp_field(msg->to.bytes, msg->to.size); - hash_rlp_field(msg->value.bytes, msg->value.size); + hash_rlp_bytes_stripped(msg->gas_limit.bytes, msg->gas_limit.size); + hash_rlp_field(msg->to.bytes, msg->to.size); /* address: no strip */ + hash_rlp_bytes_stripped(msg->value.bytes, msg->value.size); hash_rlp_length(data_total, msg->data_initial_chunk.bytes[0]); hash_data(msg->data_initial_chunk.bytes, msg->data_initial_chunk.size); data_left = data_total - msg->data_initial_chunk.size; diff --git a/lib/firmware/ethereum_tokens.c b/lib/firmware/ethereum_tokens.c index 15e5f0678..26a1e24cb 100644 --- a/lib/firmware/ethereum_tokens.c +++ b/lib/firmware/ethereum_tokens.c @@ -42,7 +42,7 @@ const TokenType* tokenIter(int32_t* ctr) { return &(tokens[*ctr - 1]); } -const TokenType* tokenByChainAddress(uint8_t chain_id, const uint8_t* address) { +const TokenType* tokenByChainAddress(uint32_t chain_id, const uint8_t* address) { if (!address) return 0; for (int i = 0; i < TOKENS_COUNT; i++) { if (chain_id == tokens[i].chain_id && @@ -57,7 +57,7 @@ const TokenType* tokenByChainAddress(uint8_t chain_id, const uint8_t* address) { return UnknownToken; } -bool tokenByTicker(uint8_t chain_id, const char* ticker, +bool tokenByTicker(uint32_t chain_id, const char* ticker, const TokenType** token) { *token = NULL; diff --git a/lib/firmware/fsm_msg_tron.h b/lib/firmware/fsm_msg_tron.h index 849603225..8dba94b61 100644 --- a/lib/firmware/fsm_msg_tron.h +++ b/lib/firmware/fsm_msg_tron.h @@ -102,24 +102,14 @@ void fsm_msgTronSignTx(TronSignTx* msg) { return; } - bool needs_confirm = true; - - // Display transaction details if available - if (needs_confirm && msg->has_to_address && msg->has_amount) { - char amount_str[32]; - tron_formatAmount(amount_str, sizeof(amount_str), msg->amount); - - if (!confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, "Send", - "Send %s TRX to %s?", amount_str, msg->to_address)) { - memzero(node, sizeof(*node)); - fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); - layoutHome(); - return; - } - } - - if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "Transaction", - "Really sign this TRON transaction?")) { + /* to_address and amount are deprecated fields not included in raw_data. + * Displaying them would show data that is not part of what is being signed. + * Show only the raw_data size so the user knows what they are authorising. */ + char blind_msg[48]; + snprintf(blind_msg, sizeof(blind_msg), "Sign %u-byte TRON transaction?", + (unsigned)msg->raw_data.size); + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "TRON Blind Sign", + "%s", blind_msg)) { memzero(node, sizeof(*node)); fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); layoutHome(); From 7974dc4c6455e19f5d62ef81372eb9ddc73a5d57 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 00:18:55 -0300 Subject: [PATCH 13/21] fix(zcash): three ZIP-244 security fixes for transparent signing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 1 (High) — wrong sighash: zcash_sign_transparent_inputs was signing only the inner transparent_sig_digest. ZIP-244 §4.4 requires signing the full signature_digest: BLAKE2b("ZcashTxHash_"||branch_id, header||transparent_sig||sapling||orchard) Fix: wrap transparent_sig_digest via zcash_compute_shielded_sighash before calling hdnode_sign_digest. Issue 2 (High) — early sig release: ZcashTransparentSigned was returned immediately after transparent inputs streamed, before Orchard actions were signed, before Orchard digest was verified, and before fee was confirmed. Fix: buffer the ECDSA signatures in zcash_signing.pending_transparent; send ZcashTransparentSigned at the same final approval gate as ZcashSignedPCZT (after orchard digest verify + zcash_verify_and_confirm_fee). The transparent input handler now sends ZcashPCZTActionAck(0) to begin Orchard streaming. --- lib/firmware/fsm_msg_zcash.h | 69 +++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/lib/firmware/fsm_msg_zcash.h b/lib/firmware/fsm_msg_zcash.h index 2d4d54a26..2f95610f7 100644 --- a/lib/firmware/fsm_msg_zcash.h +++ b/lib/firmware/fsm_msg_zcash.h @@ -100,6 +100,9 @@ static struct { ZcashTransparentOutputState transparent_outputs[ZCASH_MAX_TRANSPARENT_OUTPUTS]; ZcashTransparentInputState transparent_inputs[ZCASH_MAX_TRANSPARENT_INPUTS]; + /* Deferred transparent ECDSA sigs — buffered until Orchard/fee final gate */ + bool has_pending_transparent; + ZcashTransparentSigned pending_transparent; } zcash_signing; /* Public API; declared in keepkey/firmware/zcash.h. */ @@ -353,9 +356,8 @@ static bool zcash_finalize_transparent_digest(void) { return true; } -static bool zcash_sign_transparent_inputs(ZcashTransparentSigned* resp, - bool* cancelled) { - if (!resp || !zcash_signing.transparent_digest_verified) return false; +static bool zcash_sign_transparent_inputs(bool* cancelled) { + if (!zcash_signing.transparent_digest_verified) return false; if (cancelled) *cancelled = false; bool ok = false; @@ -366,8 +368,9 @@ static bool zcash_sign_transparent_inputs(ZcashTransparentSigned* resp, const CoinType* coin = fsm_getCoin(true, "Zcash"); if (!coin) goto cleanup; - memset(resp, 0, sizeof(ZcashTransparentSigned)); - resp->signatures_count = zcash_signing.n_transparent_inputs; + memset(&zcash_signing.pending_transparent, 0, sizeof(ZcashTransparentSigned)); + zcash_signing.pending_transparent.signatures_count = + zcash_signing.n_transparent_inputs; for (uint32_t i = 0; i < zcash_signing.n_transparent_inputs; i++) { const ZcashTransparentInputState* stored = @@ -388,29 +391,44 @@ static bool zcash_sign_transparent_inputs(ZcashTransparentSigned* resp, stored->address_n_count, NULL); if (!node) goto cleanup; - uint8_t sighash[32] = {0}; + /* ZIP-244 §4.4: signature_digest = ZcashTxHash_( + * header_digest || transparent_sig_digest || sapling_digest || orchard_digest) + * Binding the transparent ECDSA sig to all four components ensures it + * cannot be replayed in a transaction with different Orchard/header data. */ + uint8_t t_sig_digest[32] = {0}; + uint8_t full_sighash[32] = {0}; uint8_t sig[64] = {0}; uint8_t der_sig[73] = {0}; - if (!zcash_compute_transparent_sighash_digest( + + bool sign_ok = + zcash_compute_transparent_sighash_digest( inputs, zcash_signing.n_transparent_inputs, outputs, - zcash_signing.n_transparent_outputs, i, 0x01, sighash) || - hdnode_sign_digest(node, sighash, sig, NULL, NULL) != 0) { - memzero(node, sizeof(*node)); - memzero(sighash, sizeof(sighash)); + zcash_signing.n_transparent_outputs, i, 0x01, t_sig_digest) && + zcash_compute_shielded_sighash( + zcash_signing.header_digest, t_sig_digest, EMPTY_SAPLING_DIGEST, + zcash_signing.expected_orchard_digest, zcash_signing.branch_id, + full_sighash) && + hdnode_sign_digest(node, full_sighash, sig, NULL, NULL) == 0; + + memzero(node, sizeof(*node)); + memzero(t_sig_digest, sizeof(t_sig_digest)); + memzero(full_sighash, sizeof(full_sighash)); + + if (!sign_ok) { memzero(sig, sizeof(sig)); - return false; + goto cleanup; } int der_len = ecdsa_sig_to_der(sig, der_sig); - resp->signatures[i].size = der_len; - memcpy(resp->signatures[i].bytes, der_sig, der_len); + zcash_signing.pending_transparent.signatures[i].size = der_len; + memcpy(zcash_signing.pending_transparent.signatures[i].bytes, der_sig, + der_len); - memzero(node, sizeof(*node)); - memzero(sighash, sizeof(sighash)); memzero(sig, sizeof(sig)); memzero(der_sig, sizeof(der_sig)); } + zcash_signing.has_pending_transparent = true; ok = true; cleanup: @@ -1067,7 +1085,16 @@ void fsm_msgZcashPCZTAction(const ZcashPCZTAction* msg) { return; } - /* All done - send the collected signatures */ + /* Release deferred transparent ECDSA sigs at the same gate as Orchard sigs — + * both are sent only after Orchard digest verification and fee confirmation. */ + if (zcash_signing.has_pending_transparent) { + ZcashTransparentSigned* t_resp = (ZcashTransparentSigned*)msg_resp; + memcpy(t_resp, &zcash_signing.pending_transparent, + sizeof(ZcashTransparentSigned)); + msg_write(MessageType_MessageType_ZcashTransparentSigned, t_resp); + } + + /* All done - send the collected Orchard signatures */ ZcashSignedPCZT* resp_signed = (ZcashSignedPCZT*)msg_resp; memset(resp_signed, 0, sizeof(ZcashSignedPCZT)); @@ -1340,9 +1367,8 @@ void fsm_msgZcashTransparentInput(const ZcashTransparentInput* msg) { return; } - ZcashTransparentSigned* resp = (ZcashTransparentSigned*)msg_resp; bool cancelled = false; - if (!zcash_sign_transparent_inputs(resp, &cancelled)) { + if (!zcash_sign_transparent_inputs(&cancelled)) { fsm_sendFailure(cancelled ? FailureType_Failure_ActionCancelled : FailureType_Failure_Other, cancelled ? _("Signing cancelled") @@ -1352,6 +1378,9 @@ void fsm_msgZcashTransparentInput(const ZcashTransparentInput* msg) { return; } - msg_write(MessageType_MessageType_ZcashTransparentSigned, resp); + /* Transparent ECDSA sigs are buffered in zcash_signing.pending_transparent. + * They are released at the same final gate as Orchard sigs, after Orchard + * digest verification and fee confirmation. */ + zcash_send_action_ack(0); layoutProgress(_("Signing Zcash"), 0); } From ecd32cd9b27ed8deec03e945ec381aaa167e2129 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 00:30:53 -0300 Subject: [PATCH 14/21] style: apply clang-format to all files touched by ZIP-244 and 7.14.2 fixes --- lib/firmware/eip712.c | 3 ++- lib/firmware/ethereum.c | 2 +- lib/firmware/ethereum_tokens.c | 3 ++- lib/firmware/fsm_msg_tron.h | 4 ++-- lib/firmware/fsm_msg_zcash.h | 20 +++++++++++--------- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/firmware/eip712.c b/lib/firmware/eip712.c index c3f36c13a..4bbc45b83 100644 --- a/lib/firmware/eip712.c +++ b/lib/firmware/eip712.c @@ -553,7 +553,8 @@ int parseVals(const json_t* eip712Types, const json_t* jType, } } /* EIP-712 int/uint values must fit in 64 bits (firmware limit). - * Reject values that overflow strtoll to avoid silent misencoding. */ + * Reject values that overflow strtoll to avoid silent misencoding. + */ char* endptr = NULL; errno = 0; long long intVal = strtoll(valStr, &endptr, 10); diff --git a/lib/firmware/ethereum.c b/lib/firmware/ethereum.c index 08c37c7ea..f5413ca63 100644 --- a/lib/firmware/ethereum.c +++ b/lib/firmware/ethereum.c @@ -879,7 +879,7 @@ void ethereum_signing_init(EthereumSignTx* msg, const HDNode* node, } hash_rlp_bytes_stripped(msg->gas_limit.bytes, msg->gas_limit.size); - hash_rlp_field(msg->to.bytes, msg->to.size); /* address: no strip */ + hash_rlp_field(msg->to.bytes, msg->to.size); /* address: no strip */ hash_rlp_bytes_stripped(msg->value.bytes, msg->value.size); hash_rlp_length(data_total, msg->data_initial_chunk.bytes[0]); hash_data(msg->data_initial_chunk.bytes, msg->data_initial_chunk.size); diff --git a/lib/firmware/ethereum_tokens.c b/lib/firmware/ethereum_tokens.c index 26a1e24cb..dd7f57f60 100644 --- a/lib/firmware/ethereum_tokens.c +++ b/lib/firmware/ethereum_tokens.c @@ -42,7 +42,8 @@ const TokenType* tokenIter(int32_t* ctr) { return &(tokens[*ctr - 1]); } -const TokenType* tokenByChainAddress(uint32_t chain_id, const uint8_t* address) { +const TokenType* tokenByChainAddress(uint32_t chain_id, + const uint8_t* address) { if (!address) return 0; for (int i = 0; i < TOKENS_COUNT; i++) { if (chain_id == tokens[i].chain_id && diff --git a/lib/firmware/fsm_msg_tron.h b/lib/firmware/fsm_msg_tron.h index 8dba94b61..85c59eccf 100644 --- a/lib/firmware/fsm_msg_tron.h +++ b/lib/firmware/fsm_msg_tron.h @@ -108,8 +108,8 @@ void fsm_msgTronSignTx(TronSignTx* msg) { char blind_msg[48]; snprintf(blind_msg, sizeof(blind_msg), "Sign %u-byte TRON transaction?", (unsigned)msg->raw_data.size); - if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "TRON Blind Sign", - "%s", blind_msg)) { + if (!confirm(ButtonRequestType_ButtonRequest_SignTx, "TRON Blind Sign", "%s", + blind_msg)) { memzero(node, sizeof(*node)); fsm_sendFailure(FailureType_Failure_ActionCancelled, "Signing cancelled"); layoutHome(); diff --git a/lib/firmware/fsm_msg_zcash.h b/lib/firmware/fsm_msg_zcash.h index 2f95610f7..4c57aed6f 100644 --- a/lib/firmware/fsm_msg_zcash.h +++ b/lib/firmware/fsm_msg_zcash.h @@ -392,9 +392,10 @@ static bool zcash_sign_transparent_inputs(bool* cancelled) { if (!node) goto cleanup; /* ZIP-244 §4.4: signature_digest = ZcashTxHash_( - * header_digest || transparent_sig_digest || sapling_digest || orchard_digest) - * Binding the transparent ECDSA sig to all four components ensures it - * cannot be replayed in a transaction with different Orchard/header data. */ + * header_digest || transparent_sig_digest || sapling_digest || + * orchard_digest) Binding the transparent ECDSA sig to all four components + * ensures it cannot be replayed in a transaction with different + * Orchard/header data. */ uint8_t t_sig_digest[32] = {0}; uint8_t full_sighash[32] = {0}; uint8_t sig[64] = {0}; @@ -404,10 +405,10 @@ static bool zcash_sign_transparent_inputs(bool* cancelled) { zcash_compute_transparent_sighash_digest( inputs, zcash_signing.n_transparent_inputs, outputs, zcash_signing.n_transparent_outputs, i, 0x01, t_sig_digest) && - zcash_compute_shielded_sighash( - zcash_signing.header_digest, t_sig_digest, EMPTY_SAPLING_DIGEST, - zcash_signing.expected_orchard_digest, zcash_signing.branch_id, - full_sighash) && + zcash_compute_shielded_sighash(zcash_signing.header_digest, + t_sig_digest, EMPTY_SAPLING_DIGEST, + zcash_signing.expected_orchard_digest, + zcash_signing.branch_id, full_sighash) && hdnode_sign_digest(node, full_sighash, sig, NULL, NULL) == 0; memzero(node, sizeof(*node)); @@ -1085,8 +1086,9 @@ void fsm_msgZcashPCZTAction(const ZcashPCZTAction* msg) { return; } - /* Release deferred transparent ECDSA sigs at the same gate as Orchard sigs — - * both are sent only after Orchard digest verification and fee confirmation. */ + /* Release deferred transparent ECDSA sigs at the same gate as Orchard sigs + * — both are sent only after Orchard digest verification and fee + * confirmation. */ if (zcash_signing.has_pending_transparent) { ZcashTransparentSigned* t_resp = (ZcashTransparentSigned*)msg_resp; memcpy(t_resp, &zcash_signing.pending_transparent, From 903eb6ee43d286c099e9791bb78891b1094beb3d Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 00:33:04 -0300 Subject: [PATCH 15/21] chore: add Makefile with lint/format targets; fix remaining clang-format violation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make lint — dry-run check matching CI (clang-format --dry-run --Werror) make format — apply clang-format -i in-place Also formatted zxappliquid.c which lint caught locally. --- Makefile | 40 +++++++++++++++++++ lib/firmware/ethereum_contracts/zxappliquid.c | 20 +++++----- 2 files changed, 50 insertions(+), 10 deletions(-) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..3359d02d4 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +# Convenience targets — mirrors CI jobs so failures are caught locally. + +CLANG_FORMAT ?= clang-format + +# Directories and exclusions must match .github/workflows/ci.yml lint-format job. +LINT_DIRS := include/keepkey lib/firmware lib/board lib/transport/src +LINT_SOURCES := $(shell find $(LINT_DIRS) -name '*.c' -o -name '*.h' 2>/dev/null \ + | grep -v generated | grep -v '\.pb\.') + +.PHONY: lint format help + +## lint: Check formatting (same rules as CI). Exits non-zero on any violation. +lint: + @echo "clang-format version: $$($(CLANG_FORMAT) --version)" + @FAILED=0; \ + for f in $(LINT_SOURCES); do \ + if ! $(CLANG_FORMAT) --style=file --dry-run --Werror "$$f" 2>/dev/null; then \ + echo " NEEDS FORMAT: $$f"; \ + FAILED=1; \ + fi; \ + done; \ + if [ "$$FAILED" = "1" ]; then \ + echo ""; \ + echo "Run 'make format' to fix all files."; \ + exit 1; \ + else \ + echo "All files pass clang-format check."; \ + fi + +## format: Auto-fix formatting in-place for all source files. +format: + @echo "Formatting $(LINT_DIRS)..." + @for f in $(LINT_SOURCES); do \ + $(CLANG_FORMAT) --style=file -i "$$f"; \ + done + @echo "Done. Review changes with: git diff" + +## help: List available targets. +help: + @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / make /' diff --git a/lib/firmware/ethereum_contracts/zxappliquid.c b/lib/firmware/ethereum_contracts/zxappliquid.c index 76d7d3cda..aee74e0b0 100644 --- a/lib/firmware/ethereum_contracts/zxappliquid.c +++ b/lib/firmware/ethereum_contracts/zxappliquid.c @@ -35,7 +35,7 @@ #include "trezor/crypto/sha3.h" bool zx_confirmApproveLiquidity(uint32_t data_total, - const EthereumSignTx *msg) { + const EthereumSignTx* msg) { (void)data_total; const char *to, *tikstr, *poolstr, *allowance, *amt; unsigned char data[40]; @@ -47,14 +47,14 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, const TokenType *WETH, *ttoken; if (!tokenByTicker(msg->chain_id, "WETH", &WETH)) return false; - wethord = read_be((const uint8_t *)WETH->address); - to = (const char *)msg->to.bytes; + wethord = read_be((const uint8_t*)WETH->address); + to = (const char*)msg->to.bytes; tokctr = 0; while (tokctr != -1) { ttoken = tokenIter(&tokctr); // https://uniswap.org/docs/v2/smart-contract-integration/getting-pair-addresses/ - uint32_t ttokenord = read_be((const uint8_t *)ttoken->address); + uint32_t ttokenord = read_be((const uint8_t*)ttoken->address); if (ttokenord < wethord) { memcpy(data, ttoken->address, 20); memcpy(&data[20], WETH->address, 20); @@ -65,7 +65,7 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, keccak_256(data, sizeof(data), tokdigest); SHA3_CTX ctx = {0}; keccak_256_Init(&ctx); - keccak_Update(&ctx, (unsigned char *)"\xff", 1); + keccak_Update(&ctx, (unsigned char*)"\xff", 1); keccak_Update(&ctx, (unsigned char *)"\x5C\x69\xbE\xe7\x01\xef\x81\x4a\x2B\x6a\x3E\xDD\x4B\x16\x52\xCB\x9c\xc5\xaA\x6f", 20); keccak_Update(&ctx, tokdigest, sizeof(tokdigest)); keccak_Update(&ctx, (unsigned char *)"\x96\xe8\xac\x42\x77\x19\x8f\xf8\xb6\xf7\x85\x47\x8a\xa9\xa3\x9f\x40\x3c\xb7\x68\xdd\x02\xcb\xee\x32\x6c\x3e\x7d\xa3\x48\x84\x5f", 32); @@ -87,8 +87,8 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, poolstr = digestStr; } - allowance = (char *)(msg->data_initial_chunk.bytes + 4 + 32); - if (memcmp(allowance, (uint8_t *)&MAX_ALLOWANCE, 32) == 0) { + allowance = (char*)(msg->data_initial_chunk.bytes + 4 + 32); + if (memcmp(allowance, (uint8_t*)&MAX_ALLOWANCE, 32) == 0) { amt = "full balance"; } else { for (ctr = 0; ctr < 32; ctr++) { @@ -97,7 +97,7 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, amt = amtStr; } - const char *appStr = "uniswap approve liquidity"; + const char* appStr = "uniswap approve liquidity"; confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, appStr, "Amount: %s", amt); confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, appStr, @@ -105,9 +105,9 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, return true; } -bool zx_isZxApproveLiquid(const EthereumSignTx *msg) { +bool zx_isZxApproveLiquid(const EthereumSignTx* msg) { if (memcmp(msg->data_initial_chunk.bytes, "\x09\x5e\xa7\xb3", 4) == 0) - if (memcmp((uint8_t *)(msg->data_initial_chunk.bytes + 4 + 32 - 20), + if (memcmp((uint8_t*)(msg->data_initial_chunk.bytes + 4 + 32 - 20), UNISWAP_ROUTER_ADDRESS, 20) == 0) return true; return false; From bd9c60f9b51fa6bed18352ebcf6ca226c7d773db Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 02:07:58 -0300 Subject: [PATCH 16/21] style: format zxappliquid.c with clang-format-20; Makefile auto-selects clang-format-20 clang-format-22 and clang-format-20 differ on pointer spacing (e.g. 'T* p' vs 'T *p'). CI uses clang-format-20.1.8; re-format with that exact version so local and CI agree. Makefile now auto-selects clang-format-20 if installed, falls back to clang-format. --- Makefile | 6 ++++-- lib/firmware/ethereum_contracts/zxappliquid.c | 20 +++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 3359d02d4..a5a41ad99 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ # Convenience targets — mirrors CI jobs so failures are caught locally. - -CLANG_FORMAT ?= clang-format +# +# CI pins clang-format-20. Use that version if available, otherwise fall back. +# To install: brew install llvm@20 or apt-get install clang-format-20 +CLANG_FORMAT ?= $(shell command -v clang-format-20 2>/dev/null || echo clang-format) # Directories and exclusions must match .github/workflows/ci.yml lint-format job. LINT_DIRS := include/keepkey lib/firmware lib/board lib/transport/src diff --git a/lib/firmware/ethereum_contracts/zxappliquid.c b/lib/firmware/ethereum_contracts/zxappliquid.c index aee74e0b0..76d7d3cda 100644 --- a/lib/firmware/ethereum_contracts/zxappliquid.c +++ b/lib/firmware/ethereum_contracts/zxappliquid.c @@ -35,7 +35,7 @@ #include "trezor/crypto/sha3.h" bool zx_confirmApproveLiquidity(uint32_t data_total, - const EthereumSignTx* msg) { + const EthereumSignTx *msg) { (void)data_total; const char *to, *tikstr, *poolstr, *allowance, *amt; unsigned char data[40]; @@ -47,14 +47,14 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, const TokenType *WETH, *ttoken; if (!tokenByTicker(msg->chain_id, "WETH", &WETH)) return false; - wethord = read_be((const uint8_t*)WETH->address); - to = (const char*)msg->to.bytes; + wethord = read_be((const uint8_t *)WETH->address); + to = (const char *)msg->to.bytes; tokctr = 0; while (tokctr != -1) { ttoken = tokenIter(&tokctr); // https://uniswap.org/docs/v2/smart-contract-integration/getting-pair-addresses/ - uint32_t ttokenord = read_be((const uint8_t*)ttoken->address); + uint32_t ttokenord = read_be((const uint8_t *)ttoken->address); if (ttokenord < wethord) { memcpy(data, ttoken->address, 20); memcpy(&data[20], WETH->address, 20); @@ -65,7 +65,7 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, keccak_256(data, sizeof(data), tokdigest); SHA3_CTX ctx = {0}; keccak_256_Init(&ctx); - keccak_Update(&ctx, (unsigned char*)"\xff", 1); + keccak_Update(&ctx, (unsigned char *)"\xff", 1); keccak_Update(&ctx, (unsigned char *)"\x5C\x69\xbE\xe7\x01\xef\x81\x4a\x2B\x6a\x3E\xDD\x4B\x16\x52\xCB\x9c\xc5\xaA\x6f", 20); keccak_Update(&ctx, tokdigest, sizeof(tokdigest)); keccak_Update(&ctx, (unsigned char *)"\x96\xe8\xac\x42\x77\x19\x8f\xf8\xb6\xf7\x85\x47\x8a\xa9\xa3\x9f\x40\x3c\xb7\x68\xdd\x02\xcb\xee\x32\x6c\x3e\x7d\xa3\x48\x84\x5f", 32); @@ -87,8 +87,8 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, poolstr = digestStr; } - allowance = (char*)(msg->data_initial_chunk.bytes + 4 + 32); - if (memcmp(allowance, (uint8_t*)&MAX_ALLOWANCE, 32) == 0) { + allowance = (char *)(msg->data_initial_chunk.bytes + 4 + 32); + if (memcmp(allowance, (uint8_t *)&MAX_ALLOWANCE, 32) == 0) { amt = "full balance"; } else { for (ctr = 0; ctr < 32; ctr++) { @@ -97,7 +97,7 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, amt = amtStr; } - const char* appStr = "uniswap approve liquidity"; + const char *appStr = "uniswap approve liquidity"; confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, appStr, "Amount: %s", amt); confirm(ButtonRequestType_ButtonRequest_ConfirmOutput, appStr, @@ -105,9 +105,9 @@ bool zx_confirmApproveLiquidity(uint32_t data_total, return true; } -bool zx_isZxApproveLiquid(const EthereumSignTx* msg) { +bool zx_isZxApproveLiquid(const EthereumSignTx *msg) { if (memcmp(msg->data_initial_chunk.bytes, "\x09\x5e\xa7\xb3", 4) == 0) - if (memcmp((uint8_t*)(msg->data_initial_chunk.bytes + 4 + 32 - 20), + if (memcmp((uint8_t *)(msg->data_initial_chunk.bytes + 4 + 32 - 20), UNISWAP_ROUTER_ADDRESS, 20) == 0) return true; return false; From aa0cc569e4c8edff03736b39d460970a242ec3d0 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 02:14:37 -0300 Subject: [PATCH 17/21] fix: remove bare 'extern int errno' from tiny-json.h eip712.c now includes for strtoll overflow detection (Bug 6 fix). On ARM bare-metal, arm-none-eabi errno.h declares '__errno' as a function prototype. The stray 'extern int errno' in tiny-json.h causes two errors: - function declaration isn't a prototype [-Werror=strict-prototypes] - redundant-decls (redeclaration of __errno) Standard errno.h is sufficient; the manual declaration is removed. --- include/keepkey/firmware/tiny-json.h | 1 - 1 file changed, 1 deletion(-) diff --git a/include/keepkey/firmware/tiny-json.h b/include/keepkey/firmware/tiny-json.h index 7ba75ec38..335a1dc58 100644 --- a/include/keepkey/firmware/tiny-json.h +++ b/include/keepkey/firmware/tiny-json.h @@ -66,7 +66,6 @@ typedef struct json_s { jsonType_t type; } json_t; -extern int errno; /** Parse a string to get a json. * @param str String pointer with a JSON object. It will be modified. * @param mem Array of json properties to allocate. From a1450ddca5b9008d78788d9d4e8d720b1ddd3912 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 02:46:03 -0300 Subject: [PATCH 18/21] fix: add missing hash_rlp_bytes_stripped definition in ethereum.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 5 fix (security bugfixes commit) added calls to hash_rlp_bytes_stripped but never defined the function, causing ARM build failure: error: implicit declaration of function 'hash_rlp_bytes_stripped' Adds the implementation: strip leading zero bytes from big-endian integer fields before RLP-encoding, per Ethereum yellow paper §B minimal encoding. --- lib/firmware/ethereum.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/firmware/ethereum.c b/lib/firmware/ethereum.c index f5413ca63..554957164 100644 --- a/lib/firmware/ethereum.c +++ b/lib/firmware/ethereum.c @@ -184,6 +184,22 @@ static void hash_rlp_field(const uint8_t* buf, size_t size) { hash_data(buf, size); } +/* + * Push an RLP encoded integer field stripping leading zero bytes. + * Per Ethereum yellow paper §B, integer fields have minimal encoding. + */ +static void hash_rlp_bytes_stripped(const uint8_t* buf, size_t size) { + if (size == 0) { + hash_rlp_length(0, 0); + return; + } + size_t offset = 0; + while (offset < size - 1 && buf[offset] == 0) { + offset++; + } + hash_rlp_field(buf + offset, size - offset); +} + /* * Push an RLP encoded number to the hash buffer. * Ethereum yellow paper says to convert to big endian and strip leading zeros. From d9f52b9581e7ba7586ee6e67e01c38a46a27537d Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 11:45:05 -0300 Subject: [PATCH 19/21] chore: bump python-keepkey submodule to feat/zcash-clearsign-tests Update to the matching python-keepkey that handles the deferred transparent sig protocol (ZcashPCZTActionAck after last transparent input, followed by ZcashTransparentSigned + ZcashSignedPCZT at the final approval gate). --- deps/python-keepkey | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index 41bf86a53..3b5aad3a5 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 41bf86a534e6cdd0ba0532cbf6bc23f84d2e03e7 +Subproject commit 3b5aad3a52e1e951c03acc3c46afe3e7e7353979 From 5825276f656bb842d37820b63d889a4e2189d626 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 12:06:09 -0300 Subject: [PATCH 20/21] chore: bump python-keepkey to d7fd14d (test tuple-unpack fix) --- deps/python-keepkey | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index 3b5aad3a5..d7fd14d1e 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit 3b5aad3a52e1e951c03acc3c46afe3e7e7353979 +Subproject commit d7fd14d1e36b070f03073b62580f4c5a18ba5a70 From ecbf02539e3678238cf37913061a4fc0cede2440 Mon Sep 17 00:00:00 2001 From: highlander Date: Thu, 21 May 2026 12:18:37 -0300 Subject: [PATCH 21/21] chore: bump python-keepkey to d5bb0dc (restored full test file, tuple-unpack fixes) --- deps/python-keepkey | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/python-keepkey b/deps/python-keepkey index d7fd14d1e..d5bb0dc5e 160000 --- a/deps/python-keepkey +++ b/deps/python-keepkey @@ -1 +1 @@ -Subproject commit d7fd14d1e36b070f03073b62580f4c5a18ba5a70 +Subproject commit d5bb0dc5ee98281243ef849faf3bcf41a1ac052b