Skip to content

About

cli written in zig calling bhwi ffi

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

bhwi-zig-example

A proof that bhwi's sans-io design actually delivers on its promise: a Zig CLI talks to hardware wallets while owning all of the IO, and reuses only the pure protocol logic from Rust through a small C FFI: one device-generic API (bhwi's common interpreter) for BitBox02, Ledger and Jade.

This matters because Zig has its own opinionated concept of IO: since 0.15, std.Io is an explicit interface passed as a parameter, letting the application choose the implementation (blocking, thread pool, io_uring, green threads) without the library caring. A protocol library that did its own IO, or dragged in a foreign async runtime, would fight that model. Because bhwi is sans-io, this CLI plugs the interpreters straight into Zig's native std.Io handling (TCP connections, file access and device IO all go through the Io instance provided to main), and the same would hold for any other host language with its own IO story.

The claim is demonstrated. This CLI runs end-to-end against the official BitBox02 firmware simulator: it pairs (noise XX handshake, pairing code shown, config persisted so re-pairing is silent), seeds the device, and returns the master fingerprint 4c00739d and an xpub at m/84'/0'/0' that match, byte for byte, the values derived host-side from the simulator's known seed, with every wire byte moved by Zig and every protocol byte produced by Rust. The same C API and the same Zig driving loop also drive a Ledger (speculos emulator or real hardware) and a Jade (QEMU emulator or real hardware over serial); only the wire framing underneath differs.

             Zig (owns all IO)                 │      Rust (pure, no IO)
                                               │
  main.zig ──── driving loop (driver.zig)      │   bhwi-ffi: one C API
                       │                       │        │
              Link (per-device framing)        │   bhwi::common::{Command,
        ┌──────────────┼───────────────┐       │     Transmit, Response, Error}
     BitBox02        Ledger          Jade      │        │
  hww.zig  HWW   ledger_hid.zig   jade.zig     │   BitBoxInterpreter (noise XX,
  u2f.zig  U2F   or speculos      CBOR-RPC,    │   pairing, protobuf)
           frames  APDUs          pinserver    │   LedgerInterpreter (APDUs,
        │            │            HTTP leg     │   Merkle client commands)
   hidraw.zig / transport.zig (USB, TCP,       │   JadeInterpreter (CBOR-RPC,
                serial)                        │   PIN-server auth)
                                               │
        (payload bytes, recipient, encrypted) ⇄ reply bytes

The only thing crossing the boundary is bytes: the interpreter says "send this payload" (and to whom: the device, or, during Jade auth, Blockstream's PIN server), the Zig side moves it over whatever wire it wants, and feeds the reply back. The BitBox noise handshake, the Ledger Merkle/client-command protocol, the Jade CBOR-RPC encoding and the multi-round state machines all run unchanged in Rust; the U2F and Ledger-HID framings, the HWW retry protocol, CBOR message boundary detection, the PIN-server HTTP POST, device discovery and pairing persistence are all Zig. No async runtime anywhere: the interpreters are synchronous and IO-free.

Layout

  • ffi/: Rust crate wrapping bhwi's common interpreter surface as a staticlib. include/bhwi.h is the normative C contract (ownership, error codes, the noise-outlives-interpreter rule). Per-device constructors (bhwi_interp_new_bitbox / bhwi_interp_new_ledger / bhwi_interp_new_jade), everything else is device-generic.
  • cli/: the Zig program. src/ffi.zig mirrors the header; everything else is IO owned by Zig (link.zig is the per-device framing seam).

Build

nix develop            # zig, cargo/rustc, gcc
make cli               # cargo build --release (ffi) + zig build
make test              # U2F + Ledger-HID + CBOR framing unit tests

Run against the BitBox02 firmware simulator

nix run .#bitbox starts the pinned simulator (listens on 127.0.0.1:15423, auto-confirms pairing). Then, in another terminal:

make e2e
# or by hand:
./cli/zig-out/bin/bhwi-zig --simulator seed          # idempotent; prints the pairing code on first pair
./cli/zig-out/bin/bhwi-zig --simulator fingerprint   # 4c00739d
./cli/zig-out/bin/bhwi-zig --simulator xpub "m/84'/0'/0'"
# xpub6BrUqPwnpwg7jpetDNH4r2y7Y2ffL1P6qkVAGE87Z3nL3frMLj2ZifEQCHbRPQzYG9Lkyb2syWJFBuoR7c63WdzGvEQRNSgvKDZRBXb1pBQ
./cli/zig-out/bin/bhwi-zig --simulator version       # v9.26.1 BitBox IHAR initialized=1

Run against the speculos Ledger emulator

nix run .#ledger starts speculos with the Ledger Bitcoin app (re-exported from bhwi's flake, which builds both from source; expect a long first run). APDU port 127.0.0.1:9999. Then, in another terminal:

make e2e-ledger
# or by hand (the speculos app is testnet):
./cli/zig-out/bin/bhwi-zig --speculos fingerprint    # f5acc2fd
./cli/zig-out/bin/bhwi-zig --speculos xpub "m/44'/1'/0'"
# tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT
./cli/zig-out/bin/bhwi-zig --speculos version        # 2.4.1 Bitcoin Test

No pairing, no unlock: the Ledger interpreter is stateless; the app just has to be open on the device.

Run against the Jade QEMU emulator

nix run .#jade starts the Jade firmware in QEMU (re-exported from bhwi's flake, which builds the ESP32 firmware from source; expect a long first run). CBOR-RPC port 127.0.0.1:30121. Seed it once with nix run .#jade-init (sets the fixed test mnemonic and points the device at a local pinserver). Then:

make e2e-jade
# or by hand:
./cli/zig-out/bin/bhwi-zig --jade --network testnet fingerprint   # e3ebcc79
./cli/zig-out/bin/bhwi-zig --jade --network testnet xpub "m/44'/1'/0'"
# tpubDCKD5cdxMEFd2i4cNa3PJUbUHMsGDxsnfqjxVpMoG1ymWYUQUaZzTcHQo3JwYgaKe2FyKGA2FzGPSVczBoAiHGyERuA1mZ2UkGKufEnUxKk
./cli/zig-out/bin/bhwi-zig --jade --network testnet version       # 1.0.39-...

Every command starts with the auth flow. On the freshly initialized emulator the device reports itself unlocked and auth completes in one round trip; on a locked Jade the interpreter routes one payload to the PIN server instead of the device, and the CLI carries it as an HTTP POST (nix run .#jade-pinserver runs a local one on port 8096 for the emulator). That HTTP leg is ordinary caller-owned IO, same as the device wire: sans-io means the interpreter only ever says "deliver these bytes to this recipient".

Run against real hardware

./cli/zig-out/bin/bhwi-zig fingerprint               # auto-scans /sys/class/hidraw for a BitBox02 or Ledger
./cli/zig-out/bin/bhwi-zig --hidraw /dev/hidraw3 fingerprint
./cli/zig-out/bin/bhwi-zig --serial /dev/ttyACM0 fingerprint   # Jade (USB CDC serial, not HID)

Discovery matches a BitBox02 (VID 03eb, vendor usage page 0xffff) or a Ledger (VID 2c97, usage page 0xffa0) and picks the wallet interface by report descriptor. A Jade enumerates as a serial port instead; the CLI opens it raw at 115200 with DTR/RTS cleared (setting them reboots the device). On first BitBox pair the CLI prints the pairing code to confirm on the device, then persists the noise config to $XDG_CONFIG_HOME/bhwi-zig-example/noise.conf so later sessions skip the confirmation. Accessing /dev/hidrawN may need udev rules such as:

SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="2c97", TAG+="uaccess"

FFI contract in one paragraph

Single-threaded. The noise handle (BitBox-only) must outlive any BitBox interpreter created from it and must not be touched while one is alive (except polling the pairing code). bhwi_interp_start consumes the command, bhwi_interp_end consumes the interpreter, both even on error. Buffers returned via out-pointers are freed with bhwi_bytes_free / bhwi_string_free. Status codes: 0 ok, -5 authentication refused (pairing rejected / aborted on device), other negatives are host errors with a message in bhwi_last_error(). Ledger replies must keep their trailing SW1SW2 status word; the interpreter parses it. Jade replies are one complete CBOR message, and a payload whose *out_pinserver_url is set travels as an HTTP POST to that URL instead of to the device. If linking the staticlib fails in your environment, switch cli/build.zig to link ffi/target/release/libbhwi_ffi.so instead (the cdylib is also built).

About

cli written in zig calling bhwi ffi

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages