Air-gapped Bitcoin signer for the Raspberry Pi
Built to be checked,not to be trusted.
A Raspberry Pi and an 800x480 touchscreen, turned into a Bitcoin signing device with no network of any kind. You roll dice for the seed, carry transactions across the gap by camera, read each one on the device’s own screen, and sign offline.
Every module ships a specification the build checks, so the code, the specs and the tests cannot quietly disagree. The source is the product: build it yourself and compare the hash the device shows you.
StatusRuns today on a Mac or Linux box, and as a card image that boots under QEMU. Hardware bring-up on the Pi 4 is next, then an outside review. Keep real money off it until both are done.
- specs
- 39
- invariants
- 338
- tests bound to invariants
- 977 of 1296

The first screen, before any wallet is open, drawn with sample hashes. On a real device the top one is the manifest root of the software it is about to run. Build the source yourself, and if your number differs, do not enter your PIN. Nothing on this website runs that software.
What it is
Six screens from the real frontend, at the panel’s real 800x480. Rendered by a browser from the same gallery the layout and contrast checks measure, so this is the device rather than a picture of one.

The manifest root of the application, and the dm-verity root the kernel is checking every block of the root filesystem against, read from the live device-mapper table rather than from the card. Compare the first against your own build. The second is absent where there is no mapping, rather than shown as a zero. The values in these pictures are samples, not this build.

The entropy counter is arithmetic you can repeat. 100 rolls, not 99: 99 is 255.911 bits and this project does not round in its own favour.

A standard BIP-39 mnemonic. Any wallet can restore it, which is what makes walking away from this project harmless.

Every output, every amount, and which of them are provably yours. Signing stays disabled until the review has been read to the end.

Shown with its derivation path and its script type, so you can confirm it on a second device rather than trusting this one.

Checking a signature needs no key and works with the wallet locked. Verifying a stranger should not cost the passphrase to your money.

The manifest root of the application, and the dm-verity root the kernel is checking every block of the root filesystem against, read from the live device-mapper table rather than from the card. Compare the first against your own build. The second is absent where there is no mapping, rather than shown as a zero. The values in these pictures are samples, not this build.

The entropy counter is arithmetic you can repeat. 100 rolls, not 99: 99 is 255.911 bits and this project does not round in its own favour.

A standard BIP-39 mnemonic. Any wallet can restore it, which is what makes walking away from this project harmless.

Every output, every amount, and which of them are provably yours. Signing stays disabled until the review has been read to the end.

Shown with its derivation path and its script type, so you can confirm it on a second device rather than trusting this one.

Checking a signature needs no key and works with the wallet locked. Verifying a stranger should not cost the passphrase to your money.
How you check it
Every module ships a specification a machine reads. The build fails when the code, the specs and the tests stop agreeing, so the specification cannot quietly fall behind the thing it describes.
nullroute verification ok coverage 271 of 271 runtime exports covered ok invariants 338 invariants bound to 977 tests, 1296 tests in the suite ok vectors 24 of 24 vector files match their pinned hash ok differential 3 modules cross-checked ok integrity 368 files, root 0886433f...a9857292 39 specs, 338 invariants manifest root 0886433f3c2968433144f92d467cf15e593c29438de9349f5148244aa9857292 verification passed
Check the smallest claim yourself
The device turns dice into a seed by one rule you can repeat without any of this code: SHA-256 of the rolls as ASCII digits, with no trailing newline. Here is the public example from the entropy document, 100 rolls of 123456 repeated. Paste it into a terminal. On macOS, use shasum -a 256.
$ printf '%s' '1234561234561234561234561234561234561234561234561234561234561234561234561234561234561234561234561234' | sha256sum e56403e8522ddeae1b44a1e8148b1ba4d3b4c626ccf20980056eedcc7e0c0f35 -
Those 32 bytes are the seed’s entropy. If your terminal prints a different number, one of us is wrong and it is worth finding out which. Never use this sequence for money: it is public. The whole derivation, down to the 24 words.
The last line is the manifest root: every source file hashed, sorted under LC_ALL=C, in a format coreutils produced and coreutils can check. The device prints the same string on its lock screen. Build the source yourself and compare the two. If they differ, do not enter your PIN. How to do that.
Specified and implemented
- Dice into a seed, checkable by hand
- Addresses and descriptors other software restores
- Transactions reviewed before they are signed
- Multisig across several devices
- Several wallets, encrypted at rest
- Backups, labels and child seeds
- Proving an address, and checking somebody else’s
- Closing itself when you walk away
- Data across the gap by camera
Each line is present because the verifier reports every spec behind it as implemented. Nothing here is typed in by hand.
Where it stands
What is built, what comes next, and what it does not protect against yet. Keep real money off it until the first two are done.
- Not audited yet
- One person wrote it. Before it holds real money, the code that holds keys needs to be read by somebody other than its author.
- Hardware bring-up is next
- The card boots under QEMU, opens its dm-verity mapping, refuses a partition with one byte changed, and starts the signing daemon. The next milestone is the same card on a Pi 4 with the 7 inch panel, which is the first run of the firmware path from power-on to the kernel and the first time the screens are drawn on the panel itself.
- The boot partition is not covered
- dm-verity detects modification of the system partition and does not prevent it. The boot partition holds the root hash and cannot be under the tree that hash describes, so an attacker who rewrites it supplies their own number. A signed boot chain closes that in phase 7, last on purpose, because it burns one-time fuses.
- The browser is the weakest part
- The screens are a Chromium kiosk, and its own sandbox needs the user namespaces that the directives protecting the daemon take away. So trust is kept out of it rather than hardened into it: the daemon holds the keys, and the screens receive only xpubs, addresses, descriptors and transactions.
- It will not save you from a person
- Hidden profiles and a wipe PIN are planned for phase 7. When they exist, they buy time against somebody unsophisticated and nothing more: this codebase is public, so anyone who reads it knows exactly what they do.
The full list, including what this deliberately does not defend against, is in the threat model. It is long on purpose.
Read it yourself
The device and the method behind it are both open: specifications a machine can check, invariants bound to named tests, and a build that fails when a claim stops being true.
There is no download: you build it, which is the point. It runs on a Mac or a Linux box today. The device needs a 64-bit Raspberry Pi with enough memory, an 800x480 touchscreen and a camera. The card built today targets a Pi 4 and the official 7 inch touchscreen, about $100 in parts, and other boards follow as each one is brought up. A wallet here is a BIP-39 mnemonic and a canonical BIP-380 descriptor, so Bitcoin Core restores it with none of this code involved. The most valuable thing you can do with it is find where it is wrong.
What is this safe against, and what is it not?
How do I get this onto a device?
How do I check the device is honest, and build one?
What are the screens, and what does each one refuse?
How do dice become a seed, and how do I check it?
Rendered from docs/ in the repository, the same bytes a reviewer reads in the source.