Files
ledger-handshake/.github/copilot-instructions.md
eskimo 1b70b558c6
Some checks failed
Checks on the Python tests / Call Ledger Python linters (push) Has been cancelled
Build and run functional tests using ragger through reusable workflow / Build application using the reusable workflow (push) Has been cancelled
Run coding style check / Check linting using the reusable workflow (push) Has been cancelled
Ensure compliance with Ledger guidelines / Call Ledger guidelines_enforcer (push) Has been cancelled
Misspellings checks / Check misspellings (push) Has been cancelled
Build and run functional tests using ragger through reusable workflow / Run standalone ragger tests using the reusable workflow (push) Has been cancelled
Handshake Ledger app
Hardware-wallet app for Handshake (HNS). Derives Handshake addresses and signs
Handshake transactions, including name-auction covenants, on Ledger Stax, Flex,
Nano S+, Nano X and Apex+.

The design premise is that the host computer is untrusted: every APDU byte is
attacker-controlled, and the device's job is to be correct when the host lies.
Two failures outrank all others, and the code is shaped around them.

Signing something other than what the user approved. Any field the signature
commits to and the host can vary has to appear on the review screen, and
anything the screen cannot represent honestly is refused rather than shown under
a label that does not describe it. So the device signs SIGHASH_ALL only,
checked per input rather than once for the session; it displays a TRANSFER
covenant's destination, which decides who ends up owning the name and appears
nowhere else in the output; it shows a covenant's name hash when the plaintext
name cannot be verified against it; and it refuses covenant kinds it cannot
name, outputs whose values exceed their inputs, and declared input totals that
would overflow the fee arithmetic.

Misusing key material. Derivation paths are constrained to
m/44'/{5353..5356}'/account'[/change/index] in the app itself, not only by the
install manifest, because BOLOS terminates the app on an out-of-whitelist path
instead of returning an error. One coin type per signing session, since the
review renders every address under a single HRP.

Panics count too: set_panic!(exiting_panic) means a panic kills the app and
strands the session, so nothing unwraps on host-derived data and the session
ceilings are sized to the configured heap.

Consensus follows shd, the Swift Handshake node, which is the reference for the
sighash preimage, covenant item layouts and address encoding.

SIGN_TX is a PSBT-style state machine driven by P1 (BEGIN / ADD_INPUT /
ADD_OUTPUT / REVIEW / SIGN_INPUT): the host streams the transaction as
structured records, the device accumulates its own view of the values, shows one
review, then signs each input on demand.

Tests run under speculos in a container (./scripts/test), 66 of them, green on
all five devices. test_sign_tx_policy.py covers each refusal above and needs no
golden snapshots because every case is rejected before anything is drawn; the
snapshots that do exist are the record of what the user sees before approving.
tests/application_client/handshake_sighash.py is a deliberately independent
reimplementation of the sighash, used to verify real signatures.

Status: v0.1, pre-audit. Not for mainnet keys. Signing has not yet been
exercised on physical hardware.

Forked from LedgerHQ/app-boilerplate-rust (Apache-2.0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 15:13:36 -04:00

97 lines
4.4 KiB
Markdown

# Handshake Ledger app development guide
Rust app for Ledger hardware wallets (Stax, Flex, Nano S+, Nano X, Apex+) using
`ledger_device_sdk`. It signs [Handshake](https://handshake.org) transactions,
including name-auction covenants.
## The threat model drives everything
The host computer is untrusted. Every APDU byte is attacker-controlled, and the
device's job is to be correct when the host lies. Two failures outrank all
others:
1. **Signing something other than what the user approved.** Any field the
signature commits to and the host can vary must appear on the review screen.
If it cannot be displayed honestly, refuse to sign rather than showing a
label that does not describe the bytes.
2. **Misusing key material.** Signing under an unapproved path, or returning
anything derived from the seed without the user seeing what they authorised.
A panic is also a real defect: `set_panic!(exiting_panic)` means the app dies
and the user's session is stranded. Never `unwrap()` on host-derived data.
## Architecture
**APDU flow**: `Comm` receives CLA=0xe0; `Instruction` parses INS/P1/P2
(`src/main.rs`); handlers in `src/handlers/` return
`Result<CommandResponse, AppSW>`; `AppSW` maps errors to status words.
**SIGN_TX is a state machine, not a chunked blob.** P1 selects the sub-op:
`BEGIN` / `ADD_INPUT` / `ADD_OUTPUT` / `REVIEW` / `SIGN_INPUT`. The host streams
the transaction as structured records, the device accumulates its own view of
the values, shows one review, then signs each input on demand. P2 chunks a
single large `ADD_OUTPUT` only. See the table at the top of
`src/handlers/sign_tx.rs`.
**Session state** lives in `TxContext`, with a `Phase` enum enforcing ordering
and a BLAKE2b fingerprint captured at approval so nothing can change between
`REVIEW` and `SIGN_INPUT`.
**Protocol primitives** are in `src/handshake/`: addresses, bech32, BLAKE2b,
covenants, sighash, transaction types. These mirror `shd` (the Swift Handshake
node), which is the reference for every consensus question. Check against it
rather than from memory; a divergence in the sighash is a chain-split risk.
**UI** is NBGL: `NbglHomeAndSettings` for home, `NbglReview` with `Field` arrays
for transaction and message review, `NbglAddressReview` for address confirm.
Glyphs load via `include_gif!()` under `#[cfg(target_os = "...")]`.
## Build and test
```bash
./scripts/build stax # or flex / nanox / nanosplus / apex_p
```
Use `scripts/build`, not `cargo ledger build`: on macOS the latter writes
neither the Intel HEX nor the manifest, and cannot compile the Nano NBGL sources
without a `uint` define. The manifest is generated from the ELF by
`scripts/gen-manifest` and must never be hand-written, because `dataSize` has to
match the binary.
```bash
pip install -r tests/standalone/requirements.txt
pytest tests/standalone --tb=short -v --device stax
```
`tests/standalone/test_sign_tx_policy.py` is the regression suite for what the
device refuses; it needs no golden snapshots because every case is rejected
before anything is drawn. `tests/application_client/handshake_sighash.py` is an
independent sighash implementation used to cross-check real signatures.
## Key patterns
**Error handling**: map SDK errors to `AppSW` variants
(`.map_err(|_| AppSW::TxSignFail)`). Never `unwrap()` outside `build.rs`.
**BIP-32 paths**: length byte + big-endian 4-byte components. `Bip32Path` in
`src/utils.rs`; `validate_path` enforces
`m/44'/{5353..5356}'/account'[/change/index]` and every handler calls it before
deriving. BOLOS terminates the app on an out-of-whitelist path rather than
returning an error, which is why the check happens in-app first.
**Cryptography**: `Secp256k1::derive_from_path()` for derivation;
**BLAKE2b-256** for the sighash and signed messages (not SHA256d, not Keccak);
BLAKE2b-160 for witness programs; SHA3-256 for name hashes (`NameRules.hashName`
is SHA3, and Keccak differs in padding). `SIGN_TX` returns a 64-byte compact
`r||s` plus the sighash byte; `SIGN_MESSAGE` returns
`recovery_id || sig || pubkey`.
**Memory**: `#![no_std]`, `alloc::vec::Vec` and `alloc::format!`. The heap is set
in `.cargo/config.toml` and the review screen's strings are the binding
constraint, so session ceilings in `sign_tx.rs` are sized against it. Do not
reserve capacity for host-declared counts.
**Metadata**: `[package.metadata.ledger]` in `Cargo.toml` defines the app name,
icons, derivation-path whitelist and flags per device. Keep the whitelist and
`validate_path` in agreement.