Skip to the content
nullroute
Browse the docs

Invariants

The full list lives in the spec files and is checked by make verify. The security-critical ones are below.

Every row here is enforced by a spec and its tests unless it is marked Planned, which means the design is on record and nothing implements it yet. A planned invariant is a statement about a future build and protects you from nothing today. make invariant-claims checks that this page and the spec files agree about which is which, so a row cannot quietly become a promise the code does not keep.

ID Statement
INV-NET-1 The daemon binds only to a Unix domain socket or 127.0.0.1. No listener exists on any external interface.
INV-NET-2 No source file imports http, https, net, dgram, dns, or fetch outside an allowlisted loopback IPC layer. Enforced by lint.
INV-NET-3 The frontend CSP is default-src 'none' and connect-src 'self', so nothing can be fetched from anywhere. No CDN, no remote fonts, no telemetry. Three narrower allowances exist and are asserted by make device-csp: 'wasm-unsafe-eval' in script-src for the QR decoder, data: in img-src for the codes this device draws, and blob: in media-src for the camera preview. None of them can reach the network.
INV-BRIDGE-1 The loopback bridge that serves the kiosk browser binds 127.0.0.1 and nothing else. The host is a constant in packages/daemon/src/bridge/server.ts, not a parameter, so no caller and no environment variable can widen it. It exists because a page in a browser speaks HTTP and cannot open a Unix socket. Not numbered INV-NET-4: that id is taken by a core invariant about Bitcoin network version bytes, which is a different meaning of the word sharing the same prefix.
INV-KEY-1 Private key material and seed bytes never leave the daemon process, with three named exceptions: seed.reveal, which shows a mnemonic only between generation and confirmation and never for a seed loaded from storage; bip85.derive, which returns a hardened child mnemonic because writing it down is the point of BIP-85, capped per unlock; and backup.create with includeSeed, which seals the seed under a caller-chosen passphrase and says so in its response.
INV-KEY-2 All buffers holding secrets are zeroized after use, through a typed Secret wrapper with explicit dispose().
INV-SIG-1 Every ECDSA signature uses RFC 6979 deterministic nonces. Every Schnorr signature uses BIP-340 with aux_rand set to 32 zero bytes.
INV-SIG-2 The same seed and the same PSBT produce byte-identical output, confirmed against an independent implementation.
INV-PSBT-1 The device refuses to sign any input whose script cannot be matched to a registered descriptor.
INV-PSBT-2 Any output not provably derivable from a registered descriptor at a known change path is displayed as an external payment.
INV-PSBT-3 Sighash types other than SIGHASH_ALL / SIGHASH_DEFAULT are refused unless advanced mode is explicitly enabled, per operation, never persisted.
INV-BUILD-1 npm run verify must pass before the app starts. On failure the UI shows the error and refuses to load the wallet.
INV-ENT-7 No file in packages/core or packages/daemon calls Math.random(), and the frontend calls neither it nor crypto.getRandomValues(). Entropy is collected in the daemon or it is not collected. Enforced by lint.
INV-UI-102 A verdict field that crossed the IPC boundary is compared to true rather than read for truthiness, so a value that is not a boolean reads as no. Enforced by lint on the frontend, where responses are cast to an interface without being checked.
INV-WALLET-1 packages/wallet may import packages/core, never the reverse. Removing it leaves a functional signer.
INV-WALLET-2 Planned, phase 5, not enforced today. The wallet layer proposes but never signs. packages/wallet does not exist yet, so there is nothing to constrain.
INV-INTEROP-1 Every wallet is fully recoverable from the BIP-39 mnemonic plus a standard descriptor, with third-party software and no nullroute code. Drilled in CI against a real Bitcoin Core on regtest for p2wpkh, sh(wpkh), p2pkh and p2tr, and for a 2-of-3 quorum, both wsh(sortedmulti) and taproot tr(NUMS, sortedmulti_a), with three distinct seeds where two separate devices sign in sequence.
INV-INTEROP-2 A quorum is recoverable from its descriptor, which mnemonics alone cannot replace. The drill asserts that one signature does NOT finalise a 2-of-3, so a threshold that silently became 1 fails the build rather than shipping.
INV-MULTI-6 A multisig descriptor in which this device holds no key is refused at registration, rather than producing a wallet that can receive and never spend.
INV-MULTI-7 Quorum membership is decided by key material. A key origin claiming this device's fingerprint does not make a stranger's key ours.
INV-STORE-1 A seed is never written to disk in a form readable without the passphrase.
INV-STORE-2 A wrong passphrase fails authentication and returns nothing. It never produces plaintext.
INV-STORE-3 The key derivation parameters are authenticated, so editing them in the file breaks the open rather than weakening the next guess.
INV-STORE-4 Consecutive failed unlocks are counted, and passing the limit erases the sealed blob before the error is raised.
INV-STORE-5 A store is never left half written and an existing wallet is never silently overwritten.
INV-IDLE-1 Only a touch resets the idle clock. Any other IPC request, however many arrive, leaves the deadline where it was.
INV-IDLE-2 A request arriving after the deadline finds the wallet locked and the seed zeroized, not merely unreachable.
INV-IDLE-3 Remaining time never reads as more than the window and never reads as negative, whatever the system clock does.
INV-DURESS-1 Planned, phase 7, not enforced today. The encrypted store does not reveal how many profiles exist or which slots are in use.
INV-DURESS-2 Planned, phase 7, not enforced today. Unlock latency is independent of which PIN was entered and whether it was correct.

Rendered from docs/THREAT-MODEL.md, the same bytes that ship with the code. SHA-256 d932f13db7fe836f45194ab98069c47dc556d4f29b2cfbf0ef1ab3bf7b334ffd