Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a4cbefb
feat(zcash): complete clear-signing flow
BitHighlander May 20, 2026
85fc123
chore(zcash): point tests at report CI branch
BitHighlander May 20, 2026
ae516d1
chore(zcash): point tests at github actions branch
BitHighlander May 20, 2026
7d82b05
style(zcash): satisfy github actions formatter
BitHighlander May 20, 2026
70053f9
ci: use nanopb plugin wrapper for macos dylib
BitHighlander May 20, 2026
dda2030
ci: pin protobuf for python integration image
BitHighlander May 20, 2026
4531d4b
ci: use compatible protobuf for integration image
BitHighlander May 20, 2026
98b4fd9
fix(ci): restore compatible zcash protobuf generation
BitHighlander May 20, 2026
1cb48d6
fix(ci): preserve legacy zcash protobuf style
BitHighlander May 20, 2026
c5ee945
fix(ci): include zcash pczt screenshots
BitHighlander May 20, 2026
682665c
fix(ci): avoid transparent input screenshot stall
BitHighlander May 21, 2026
5a417f4
fix: security bugfixes for 7.14.2
BitHighlander May 21, 2026
7974dc4
fix(zcash): three ZIP-244 security fixes for transparent signing
BitHighlander May 21, 2026
ecd32cd
style: apply clang-format to all files touched by ZIP-244 and 7.14.2 …
BitHighlander May 21, 2026
903eb6e
chore: add Makefile with lint/format targets; fix remaining clang-for…
BitHighlander May 21, 2026
bd9c60f
style: format zxappliquid.c with clang-format-20; Makefile auto-selec…
BitHighlander May 21, 2026
aa0cc56
fix: remove bare 'extern int errno' from tiny-json.h
BitHighlander May 21, 2026
a1450dd
fix: add missing hash_rlp_bytes_stripped definition in ethereum.c
BitHighlander May 21, 2026
d9f52b9
chore: bump python-keepkey submodule to feat/zcash-clearsign-tests
BitHighlander May 21, 2026
5825276
chore: bump python-keepkey to d7fd14d (test tuple-unpack fix)
BitHighlander May 21, 2026
ecbf025
chore: bump python-keepkey to d5bb0dc (restored full test file, tuple…
BitHighlander May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 .

Expand Down
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
42 changes: 42 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Convenience targets — mirrors CI jobs so failures are caught locally.
#
# 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
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 /'
2 changes: 1 addition & 1 deletion deps/device-protocol
297 changes: 297 additions & 0 deletions docs/coin-integration/zcash-clearsign-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
# 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
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:

- 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`.
- 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` -> `41bf86a`
- Firmware submodules now point at those commits:
- `deps/device-protocol` -> `6ec974e`
- `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
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 <dir>` 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` ->
`41bf86a534e6cdd0ba0532cbf6bc23f84d2e03e7`

Failed runs 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.
- `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 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.
- `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.

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:

- `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
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.
- 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.

## 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.
Loading
Loading