ZerokeyOS/zerokey-bitcoin.cpp and the vendored
uBitcoin library (ZerokeyOS/libraries/uBitcoin, “Bitcoin” by Stepan
Snigirev). Everything below is verifiable in that source.
Design constraint. The on‑board ATECC608A is a secp256r1 (NIST P‑256)
chip — it cannot produce Bitcoin’s secp256k1 signatures. Therefore every
Bitcoin operation (BIP32/39/84 derivation, secp256k1 ECDSA, PSBT) is done in
software with uBitcoin. The ATECC is used only as a hardware TRNG and,
separately, to protect the AES master key that encrypts the seed.
Trust boundary
The single most important property: the seed never leaves the device over USB. Only public data and signatures cross the wire.| Data | Leaves over USB? | Notes |
|---|---|---|
| 12‑word seed / 16‑byte entropy | Never | Shown only on the OLED (paginated, 3 words/page) |
Account public key zpub | Yes | Public — safe to export |
| Master key fingerprint | Yes | Public — needed so the signer recognises its own inputs |
Output descriptor wpkh(...) | Yes | Public |
Signatures (PSBT_IN_PARTIAL_SIG) | Yes | Produced only after an on‑device hold‑to‑confirm |
1 · Entropy & RNG
Key material comes exclusively from the ATECC608A hardware TRNG (RANDOM
command via zerokeyAtecc.random).
btcGenEntropy()draws 16 fresh bytes with up to 5 retries (the chip can NAK the first command after sleep), rejects an all‑zero result, and refuses (returnsfalse, setsg_btcRngHardwareOk = false) rather than ever falling back to a weak source. Seed generation aborts on refusal.- uBitcoin’s Trezor crypto calls the weak
random32()/random_buffer()symbols. The firmware overrides them (extern "C"inzerokey-bitcoin.cpp) with versions backed by the ATECC TRNG through a 32‑byte cache. - If the TRNG ever fails mid‑run, the override sets
g_btcRngHardwareOk = falseand fills from a non‑trustedmicros()mix only to keep non‑key paths from looping — never for seed material (that path has already refused).
2 · Seed & encrypted storage
The 16 bytes of entropy become a BIP39 12‑word mnemonic (mnemonicFromEntropy(entropy, 16)). The device stores the entropy, not the
words, in one AES‑encrypted EEPROM page at BITCOIN_WALLET_ADDR.
- Encrypted with the same AES‑128‑CBC + ATECC‑protected master key as your credentials — so the seed is bound to your PIN (see AES‑128 encryption).
BITCOIN_WALLET_ADDRis the last 128‑byte slot of the credential region (freed by loweringMAX_CREDENTIALS62→61 inzerokey-memorymap.h); astatic_assertpins it.- On read,
btcReadWallet()verifies magic/version/length before use and zeroes the plaintext buffer afterward.
3 · Key derivation (BIP84, mainnet)
- BIP84 native SegWit, path
m/84'/0'/0',bc1q…addresses,zpubwatch‑only. Mainnet only. - No passphrase (BIP39 passphrase is empty by design in this build).
4 · Watch‑only export
bitcoinExportWatchOnly() prints public data over serial (no PIN gate — it
is not secret):
PSBT::sign matches inputs by memcmp(root.fingerprint(), derivation.fingerprint, 4)
and derivation.pubkey == derived pubkey. Without the origin, a wallet
(Sparrow, BlueWallet, Nunchuk…) would build PSBTs the device won’t recognise. The
bitcoin.html webtool appends the Bitcoin Core descriptor checksum in JS.
5 · PSBT signing
bitcoinReviewPsbt(base64) → on‑screen review → hold Center →
bitcoinConfirmSign().
Parse & review
psbt.parseBase64(); the OLED shows destination address, amount sent and fee
(psbt.fee()), with change detected via each output’s derivation. This is a
sticky review — the device never blind‑signs.Hold to confirm
A center long‑press arms
bitcoinConfirmSign(). Any other button cancels
(PSBT CANCEL).Sign
psbt.sign(hd) produces deterministic RFC 6979 low‑s ECDSA signatures
over the BIP143 sighash for every input that matches this wallet. If it
matches 0 inputs the device replies PSBT ERR NO_INPUTS and signs
nothing (expected for a PSBT from a different wallet).Threat model
- Compromised host: it can lie in the browser, but it cannot forge the OLED review or the hold‑to‑confirm. Always verify address/amount/fee on the device screen. A signature commits to the exact outputs via the sighash, so a tampered transaction can only be rejected by the network, never redirected.
- Physical extraction of EEPROM: yields only the AES‑encrypted
ZKBWpage; without the PIN‑bound master key it is meaningless. - Weak randomness: ruled out by the TRNG‑only entropy path that refuses on hardware failure.
How to audit it yourself
Reproduce the wallet from the words
Read the 12 words with Tools → Bitcoin → Show seed. Feed them into any
independent BIP84 tool (Sparrow, an offline Ian Coleman page, or a Python
bip_utils script). Derive m/84'/0'/0' and compare the fingerprint,
zpub and first receive address to the device’s Watch‑only export.
They must match exactly.Verify a signature
Build a PSBT for the wallet, sign it on the device, and check the returned
PSBT_IN_PARTIAL_SIG is a canonical low‑s ECDSA signature over the
BIP143 sighash for that input’s pubkey. Any PSBT library
(python-bitcoinlib, bitcoinjs, Bitcoin Core analyzepsbt) can finalise
and verify it.Reference validation (2026‑06‑30). An independent pure‑Python BIP84 verifier
reproduced the device’s exact
zpub + first address from the 12 words, and a
fund‑free PSBT test produced a signature byte‑for‑byte equal to the
independently predicted RFC 6979 low‑s signature over the BIP143 sighash,
verifying as valid canonical ECDSA.Source map
| Concern | Where |
|---|---|
| Entropy, RNG override, storage, derivation, PSBT | ZerokeyOS/zerokey-bitcoin.cpp |
| secp256k1 / BIP32/39 / PSBT | ZerokeyOS/libraries/uBitcoin |
| AES master key, page encrypt/decrypt | ZerokeyOS/zerokey-security.cpp |
| EEPROM page I/O, wallet slot address | ZerokeyOS/zerokey-eeprom.cpp, zerokey-memorymap.h |
| Hold‑to‑sign wiring | ZerokeyOS/zerokey-io.cpp |
User guide
Create the wallet, export watch‑only, sign a PSBT.
AES‑128 encryption
The master key that protects the stored seed.