On this page
Installing it, and what happens at first boot
Where this stands today
There is a card you can build and flash. There is no download: you build it,
which is the point, and make image prints the checksum you verify it with.
Nothing here has run on a Raspberry Pi. The image boots under QEMU and the signing daemon starts on it, which is a real thing to have proven and is not the same as your board coming up. The firmware path from power-on to the kernel is carried on the card and has never been executed, and the kiosk browser has never had a display to draw on. Flash a spare card. Expect to debug.
| What you can do | State |
|---|---|
| Run the whole device on your computer | Works today. make dev. |
| Build a card and check it is reproducible | Works today, needs Docker. make image, make image-repro. |
| Boot that card in an emulator, including a tampered copy | Works today. make image-boot-test. |
| Boot it on a Pi | Untested. You would be the first. |
What you need
The parts list is in the README, under What you need. One line of it matters here: the device never uses a network, so you do not need to give it one, and the wifi and Bluetooth firmware are removed from the image rather than switched off.
Running it on your computer
$ git clone https://github.com/Xaxis/nullroute
$ cd nullroute
$ npm ci
$ make dev
That builds everything, runs the verification system, and opens the frontend at
127.0.0.1:5180. It refuses to start if the code and the specifications
disagree, which is the same refusal the device makes at boot.
Your wallet goes in .nullroute-store/ in the checkout. It is encrypted the
same way it is on the device, and it is not a device: your computer has a
network, a swap file and a browser, so treat anything you create here as a toy.
Building a card
Needs Docker, and builds for arm64. On Apple silicon this runs natively; on an Intel machine it runs under emulation and takes considerably longer.
$ make image
That builds the root filesystem, makes the erofs system partition, computes the dm-verity hash tree over it, assembles the four partitions, and writes:
out/release/nullroute-0.1.0.img the card
out/release/system.roothash the dm-verity root hash
out/release/SHA256SUMS checksums over both
Two other targets are worth running before you flash anything.
$ make image-repro # builds it twice, in separate containers, and compares
$ make image-boot-test # boots it, then boots a copy with one byte changed
The second one is the one that matters. It opens the dm-verity mapping, mounts the root through it, reads every block, and starts the daemon; then it corrupts a single byte inside the system partition and requires that boot to fail. A device that cannot tell those apart is a device whose integrity check is decoration.
Flashing it
Find the card. On macOS diskutil list, on Linux lsblk. Get this wrong and
you will overwrite something else, so read the size and confirm it is the card.
$ cd out/release
$ shasum -a 256 -c SHA256SUMS
$ diskutil unmountDisk /dev/diskN # macOS
$ sudo dd if=nullroute-0.1.0.img of=/dev/rdiskN bs=4m status=progress
On Linux the device is /dev/sdX or /dev/mmcblkN and the block size flag is
bs=4M. Read the card back and compare it before you boot it:
$ sudo dd if=/dev/rdiskN bs=4m count=<image size in 4MB blocks> | shasum -a 256
That is provisioning tier 0: you verified the bytes you wrote. It costs nothing irreversible and it is the only tier that works on any board.
What the device does at boot
Nothing is typed and nothing is configured.
- The Pi firmware reads
config.txt, loadskernel8.imgand the device tree for your board, and hands over to the kernel withinitramfs.img. - The initramfs loads dm-verity and erofs, reads
system.roothashoff the boot partition, opens the system partition through a verity mapping, and mounts it read only. If the partition does not match that hash, it stops here and says so rather than continuing. nullroute-state.servicefinds the fourth partition, makes a filesystem on it the first time only, and mounts it at/var/lib.nullrouted.servicechecks its own verification report against the build it is running from, prints the manifest root, and listens on a Unix socket. It refuses to start if the report is missing or does not describe this build.nullroute-bridge.serviceserves the frontend on127.0.0.1:5180and forwards to that socket.nullroute-kiosk.servicestarts Chromium against it, full screen.
The lock screen is what you should see, showing the manifest root before you
type a passphrase, so you can compare it against what make image printed.
If it does not boot, attach a serial console or a monitor and read where it stopped. Every failure path in the initramfs prints a sentence saying what it was looking for. That is deliberate: a signer that fails silently is worse than one that fails loudly.
Verifying before you flash, and the tiers
Building the card yourself, checking its hash, and reading it back after writing is provisioning tier 0. It works on any board, it costs nothing irreversible, and it is what the commands above are.
Tier 1 is the dm-verity tree the device opens at boot, and it is on this card. It 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 that partition supplies their own number and the device displays exactly what they chose. Only tier 2, a signed boot chain, closes that, and it burns one-time fuses and stays in phase 7.
docs/VERIFICATION.md has what each tier actually proves. Read it before relying on any of them.
For what you can check without a Pi at all, see How you verify it.