heartwood-ledger
July 12, 2026 · View on GitHub
Working prototype — emulator-proven, not yet bench-tested on hardware. Part of the ForgeSworn identity stack — the Heartwood hardware Nostr signer, running as a Ledger embedded app instead of an ESP32/ESP8266.
The Ledger takes the place of the ESP behind heartwood-bridge: the host delivers the body of an
ENCRYPTED_REQUEST (serial frame 0x10) over chunked APDUs, and the app does everything on-device —
NIP-44 decrypt → NIP-46 dispatch → re-encrypt → sign the kind:24133 envelope — then returns the
SIGN_ENVELOPE_RESPONSE (0x35) JSON. The host never sees plaintext or key material.
Why this works
- Same identity. The master key is the BIP-32 node at
m/44'/1237'/727'/0'/0'— the exact path the ESP firmware, provision CLI and sapwood derive from a BIP-39 phrase. Restore a heartwood phrase through Ledger's own onboarding and you get the identical npub and identical nsec-tree personas. The host driver proves this against the frozen all-zero vector. - Same code. The NIP-44, NIP-46, derivation and identity logic is
heartwood-common— the firmware'sno_stdcrate — compiled unmodified for the Ledger target.src/sign_path.rsis the ESP8266 dispatch loop with the OLED/button seams removed. - Keys on hardware. Derivation happens through
os_perso_derive_node_bip32; the OS restricts this app to the44'/1237'subtree and the raw seed is never accessible.
Build (Docker)
docker run --rm -v "$(dirname "$PWD")":/ws -w /ws/heartwood-ledger \
ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest \
cargo ledger build nanosplus
(The parent directory is mounted so the heartwood-common path dependency resolves.)
Prove it (Speculos)
Seed the emulator with the canonical all-zero test phrase, then run the host driver:
docker run --rm -v "$(dirname "$PWD")":/ws -w /ws/heartwood-ledger -p 9999:9999 \
ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest \
speculos --model nanosp --display headless --apdu-port 9999 \
--seed "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" \
target/nanosplus/release/heartwood-ledger
cd host && cargo run
The driver asserts, in order: derivation matches the frozen vector (identity interop), a full
NIP-46 get_public_key round trip (envelope BIP-340 signature + NIP-44 verified), sign_event as
master gated by the on-device TOFU approval (the driver walks Speculos's buttons to Approve),
persona derive/switch/sign with no buttons pressed (proving the TOFU grant), and an unknown
client's signing request rejected on-device (signed denial envelope).
Signing policy
The Heartwood TOFU model, on Ledger buttons: non-signing methods (get_public_key, NIP-44/04
encrypt/decrypt) are the connect-safe tier and never prompt. The first sign_event from an
unknown client blocks on an Approve/Reject choice screen showing the client npub prefix and the
event kind; approval stores the client in app NVM and grants unattended signing thereafter — the
bunker model. Every curve operation runs on the OS's cx syscalls (the taproot path), not in
app RAM.
Using it with the bridge
heartwood-bridge (branch ledger-transport) speaks this app's APDU protocol directly:
HEARTWOOD_TRANSPORT=ledger-tcp HEARTWOOD_SERIAL_PORT=127.0.0.1:9999 heartwood-bridge
No bridge.secret — a Ledger authenticates its user (PIN) and gates signing on-device.
Remaining caveats
- Not yet run on a physical Nano S+ — sideload + bench test is the next gate. The bridge's USB
HID transport (
ledger-hid, hand-rolled hidraw) is framing-tested but likewise bench-gated. - 24 KB heap bounds the maximum event size well below the ESP32's.
- Sideload only (Nano S/S+). Distribution to Nano X/Stax/Flex requires Ledger's review and a paid third-party security audit.
All curve operations — pubkey derivation, BIP-340 signing, NIP-44 ECDH (including the even-y lift
of the peer key) — run through the OS's cx syscalls via heartwood-common's ledger-backend
feature (branch ledger-backend in heartwood-esp32); k256 is no longer in the app at all.
Licence
MIT.