eskimo 4bb1934a34 Remove the GitHub-only CI config
Every one of the five workflows was boilerplate whose entire body was a call to
a reusable workflow hosted at LedgerHQ/ledger-app-workflows. Gitea cannot
resolve a GitHub-hosted reusable workflow, so they could only ever appear as
broken runs: build_and_functional_tests, coding_style_checks,
guidelines_enforcer, misspellings_checks and python_tests_checks.

dependabot.yml goes with them, since Gitea does not read it either.

Kept .github/PULL_REQUEST_TEMPLATE.md, which Gitea does support, and
copilot-instructions.md, which is documentation rather than CI.

tests/README.md no longer points at the workflow for regenerating snapshots;
./scripts/test <device> --golden_run is the only route now.

There is no automated CI after this. ./scripts/test runs the full suite against
speculos in Docker, 66 tests on each of the five devices.

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

Handshake Ledger app

Hardware-wallet app for Handshake (HNS). Lets a Ledger Stax / Flex / Nano S+ / Nano X derive Handshake addresses and sign Handshake transactions, including name-auction covenants.

⚠️ Status: v0.1, pre-audit. Do not use with mainnet keys yet.

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

Devices

Stax (primary target), Flex, Nano S+, Nano X, Apex+. All five compile; only Stax has been exercised on real hardware.

What the app does (v0.1)

INS Command Behavior
0x03 GET_VERSION Returns app version (major.minor.patch).
0x04 GET_APP_NAME Returns Handshake.
0x05 GET_PUBKEY Derives a secp256k1 pubkey from a BIP-32 path under m/44'/5353'/..., encodes as bech32 (hs… mainnet / ts… testnet). With P1=1, shows the address and its derivation path on-device to confirm.
0x06 SIGN_TX PSBT-style state machine selected by P1: BEGIN / ADD_INPUT / ADD_OUTPUT / REVIEW / SIGN_INPUT. The host streams the tx as structured records, the device sums values itself, shows one review, then signs each input on demand. See src/handlers/sign_tx.rs.
0x07 SIGN_MESSAGE Prepends "handshake signed message:\n", BLAKE2b-256 hashes, shows signer address, path and preview, returns recovery_id ‖ sig ‖ pubkey.

CLA: 0xe0. Status words live in src/main.rs::AppSW.

What the device refuses

A hardware wallet's job is to be correct when the host lies, so the app rejects anything its review screen cannot represent honestly. tests/standalone/test_sign_tx_policy.py covers each of these.

  • Sighash types other than ALL (0x01). The review lists concrete outputs and a concrete fee, which is only what the signature commits to under ALL. NONE commits to no outputs, SINGLE/SINGLEREVERSE to one, ANYONECANPAY drops the other inputs and NOINPUT drops the outpoint. Checked per input, not once for the session.
  • Derivation paths outside m/44'/{5353..5356}'/account'[/change/index]. The install manifest whitelists the same prefixes, but BOLOS terminates the app on a path outside them instead of returning an error, so the app checks first. All inputs in one session must share a coin type, since the review renders every address under a single HRP.
  • Sessions larger than 16 inputs / 8 outputs, or 1 KB of covenant items in total. Sized to the configured heap. The counts no longer drive an up-front allocation, so a single BEGIN cannot exhaust it.
  • Covenant kinds above REVOKE (11), and kind 0 carrying items. Either would put bytes into the sighash under a label that does not describe them.
  • Declared input values that sum past u64, or outputs exceeding inputs. The fee is computed in u128 and the session refused rather than truncated into a plausible-looking number.

Building (no Docker)

Native Rust + ARM toolchain: no Docker, no ledger-app-builder.

Prerequisites

# Rust nightly is pinned in rust-toolchain.toml; rustup will install it on demand.
rustup show

# ARM bare-metal toolchain + LLVM (macOS)
brew install --cask gcc-arm-embedded
brew install llvm

# Ledger Cargo subcommand + sideload tool + emulator
cargo install --git https://github.com/LedgerHQ/cargo-ledger
pip install ledgerwallet speculos

scripts/build has the full one-time setup in its header comment, including the BOLOS Secure SDK clone and the API level that must match your device firmware.

Build

./scripts/build stax    # or: flex / nanox / nanosplus / apex_p

Use scripts/build rather than cargo ledger build directly: on macOS the latter writes neither the Intel HEX nor the ledgerctl manifest, and it cannot compile the Nano NBGL sources without a uint define. The script handles all three.

The manifest is generated from the built ELF by scripts/gen-manifest, never hand-written: dataSize has to match the binary, and an undersized one installs cleanly and then refuses to launch.

Binary and manifest land under target/<device>/release/.

Testing

Speculos emulator

speculos --apdu-port 9999 --api-port 5001 --model stax \
  target/stax/release/app-handshake

Ragger functional tests

./scripts/test              # stax
./scripts/test flex         # or nanosp / nanox / apex_p
./scripts/test stax -k policy -v

Speculos does not run natively on macOS, so scripts/test runs it and ragger in a container built from Ledger's dev-tools image (tests/Dockerfile). The image is multi-arch, so this runs natively on Apple silicon. The first invocation builds the image, which takes a few minutes; after that a full device run is about 30 seconds.

Tests that drive the UI compare against golden snapshots under tests/standalone/snapshots/<device>/. Regenerate them with ./scripts/test <device> --golden_run, and look at the diff before committing: those images are the record of what the review screen shows, which is the whole point of the device. test_sign_tx_policy.py needs no snapshots, because every case in it is refused before anything is drawn.

Installing on a real device

Unlock the device and stay on the dashboard (not inside an app):

ledgerctl install -f target/stax/release/app_stax.json

Architecture

  • src/main.rs: APDU dispatcher and app entry.
  • src/handlers/: one file per APDU command.
  • src/app_ui/: NBGL UI screens (menu, address confirm, tx review, message review).
  • src/handshake/: protocol primitives (addresses, covenants, sighash, BLAKE2b).
  • src/swap.rs: Ledger Exchange integration, a stub. handler_sign_review refuses outright when swap parameters are present rather than auto-approving, so wiring Exchange up requires adding the validation deliberately.

Spec / signing

The app mirrors shd:

  • Curve: secp256k1, compressed public keys (33 B).
  • BIP-32 path: m/44'/5353'/account'/change/index. Handshake assigns one SLIP-44 coin type per network (5353' mainnet, 5354' testnet, 5355' regtest, 5356' simnet), and the coin type is what selects the HRP on-device (Network::from_coin_type).
  • Address: bech32 (BIP-173, never bech32m) with HRPs hs / ts / rs / ss, witness version 0, P2WPKH = BLAKE2b-160(compressed_pubkey).
  • Sighash: BIP-143-style preimage, hashed with BLAKE2b-256 (not SHA256d). The preimage builder handles ALL, NONE, SINGLE, SINGLEREVERSE and the NOINPUT / ANYONECANPAY flags so it matches shd byte for byte, but SIGN_TX only signs ALL. tests/application_client/handshake_sighash.py is an independent reimplementation used to cross-check the device's signatures.
  • Signature: 64-byte compact r‖s with a 1-byte sighash type appended. Low-S comes from BOLOS itself: cx_ecdsa_sign canonicalizes unless CX_NO_CANONICAL is set, and the SDK's deterministic_sign does not set it.
  • Outputs carry a covenant field (name auctions). Items are re-serialized verbatim into the sighash preimage, and the review decodes them: the operation label for every kind, the plaintext name for OPEN/BID/FINALIZE once checked against the hash the covenant commits to, the name hash otherwise, and for TRANSFER the destination address, which decides who ends up owning the name and appears nowhere else on screen.

Authoritative source: shd/Sources/Script/SigHash.swift, shd/Sources/Protocol/Transaction.swift, shd/Sources/Covenants/CovenantData.swift.

License

Apache-2.0, see LICENSE.md. Original boilerplate © Ledger SAS.

Description
No description provided
Readme 2.1 MiB
Languages
Rust 56.7%
Python 39.7%
Shell 3.2%
Dockerfile 0.4%