Skip to the content
nullroute
Browse the docs

4. Attestation: the software identity shown before secrets

4.1 What is displayed

Before the user enters a passphrase, PIN or mnemonic, the signer shows a value that identifies the software it runs, in a form the user can recompute from source with tools that did not come from the signer's authors.

SP-ATT-1. The software identity MUST be displayed before any secret is entered, and the interface MUST read it before rendering anything else. Check: nullroute INV-UI-1, INV-UI-64; manual on hardware: power on and confirm the value is on the first screen that accepts input.

SP-ATT-2. The identity MUST be the manifest root, defined exactly as follows:

  1. The manifest lists every file under the implementer's declared source roots that is tracked by the implementer's version control.
  2. Each line is <hash><space><space><path><LF>, where <hash> is the file's SHA-256 as 64 lowercase hexadecimal characters and <path> is relative to the repository root. This is the output format of coreutils sha256sum.
  3. Lines are ordered by path, compared byte by byte, which is sort under LC_ALL=C. Locale-aware ordering MUST NOT be used, because it makes the result depend on the verifier's language settings.
  4. The manifest root is SHA-256 of the manifest file's bytes, which is sha256sum MANIFEST.lock.

Check: vector manifest-root.json (a small tree whose paths sort differently under C and a common UTF-8 locale, with the expected manifest and root); nullroute make manifest-check and make manifest-recipe.

SP-ATT-3. A user MUST be able to recompute the root from the published source using only generic tools. The implementer MUST publish the commands. For nullroute they are (docs/VERIFICATION.md, "Checking it"):

$ sha256sum -c MANIFEST.lock
$ sha256sum MANIFEST.lock
$ git ls-files -z package.json package-lock.json packages spec provisioning | LC_ALL=C sort -z | xargs -0 sha256sum | sha256sum

Check: manual: run the three commands on a clean checkout of the release commit, on a machine that has never had the signer's tooling installed, and compare the last two outputs with each other and with the signer's screen.

SP-ATT-4. The implementer MUST state which roots the manifest covers and which executed code it does not cover. Check: manual review of the published verification document. For nullroute: docs/VERIFICATION.md, "What the manifest deliberately excludes".

SP-ATT-5. The manifest SHOULD also cover the dependency lock file, so that the displayed value changes when a dependency does. Check: vector manifest-root.json does not cover this; manual: confirm the lock file appears in the manifest.

SP-ATT-6. If the signer's own verification fails, or its record of that verification does not match the manifest on disk, the signer MUST refuse to unlock and MUST NOT offer a way to proceed. Check: nullroute INV-UI-2, INV-UI-70 for the screen, and INV-BUILD-1 for the daemon's refusal to start (packages/daemon/test/verification-gate.test.ts).

SP-ATT-7. Wherever the value is shown, the screen MUST state that it is reported by the software being checked. Check: nullroute INV-UI-5, INV-UI-69.

SP-ATT-8. The full value MUST be viewable on the signer. An abbreviated form MAY be shown first; nullroute shows the first and last eight characters, grouped in fours, and expands on tap. Check: nullroute INV-UI-1 (expands-to-the-full-hash), INV-UI-6.

SP-ATT-9. The value shown after unlocking MUST be the one shown before, not recomputed or fetched again. Check: nullroute INV-UI-72.

SP-ATT-10. Where the signer checks its system partition with a hash tree (dm-verity or equivalent), the root it displays MUST be read from the mapping the running kernel enforces, not from a file on a partition the tree does not cover. Where there is no mapping, the signer MUST show that there is none rather than a blank or a zero. Check: nullroute INV-BOOT-1.

SP-ATT-11. The signer SHOULD show the wallet fingerprint before the passphrase is confirmed, since a wrong BIP-39 passphrase produces a valid, different wallet rather than an error. Check: nullroute INV-UI-4, INV-KEY-6.

SP-ATT-12. The signer and its documentation MUST name the provisioning tier (section 4.3) a build reaches and MUST NOT describe it in a higher tier's terms. A hash tree over the system partition "detects modification of the system partition"; it does not stop it. Check: manual review of screen copy and documentation; make prose catches a narrow list of absolute claims only.

4.2 What this does not prove

Stated as plainly as the requirements, because a reviewer will look here first.

  1. The value is drawn by the software it describes. Software that has been replaced can display any number, including the right one. The check catches an accident, a failed write and a crude substitution. It does not catch an attacker who replaced the code that draws it. SP-ATT-7 puts this sentence on the screen.
  2. The root is a hash of a file, not of the running code. On nullroute the build copies MANIFEST.lock into the image (provisioning/build/build-system.sh line 536) and the lock screen hashes that copy. It says which source tree the image claims to come from. That the running bundle was built from that tree rests on reproducing the image (tier 0) and on the hash tree detecting later modification of the partition that holds both (tier 1).
  3. Files are not re-hashed at start. nullroute's daemon compares the root in its verification report with SHA-256 of MANIFEST.lock; it opens no source file (packages/daemon/src/boot/attestation.ts, comment above the staleness check). File contents are checked by sha256sum -c when the report is written, and by dm-verity block by block on a tier 1 build.
  4. Dependencies are in the root by reference, not by content. nullroute's manifest covers tracked files under packages/, spec/ and provisioning/, and the root package.json and package-lock.json (SP-ATT-5), so a changed dependency version or integrity hash moves the displayed value. The installed node_modules is not hashed into it: npm checks each package against the lock file's integrity hash when it installs, and the image checksum of tier 0 covers the result.
  5. The boot partition is covered by nothing below tier 2. Neither the manifest nor dm-verity covers the boot partition, the kernel command line or the initramfs. An attacker who rewrites it supplies their own verity root and their own initramfs, and the device displays the number they chose (docs/VERIFICATION.md, "The three tiers"; daemon.spec.yaml lines 187-193).
  6. Firmware and hardware are not covered. The SoC boot ROM, GPU firmware and any radio silicon sit below every value in this section.
  7. No release value exists to compare against yet. Nothing has been published or signed (docs/THREAT-MODEL.md line 74). Today the comparison is against a root the user computes from source.

4.3 Tier boundaries

Tier Establishes Does not establish
0 The image is byte-identical to one built from the source, checked before flashing and after reading the card back That the card still holds that image at the next boot
1 Modification of the system partition after the build is detected as blocks are read, and the root of the enforced mapping is shown at boot Anything about the boot partition, which supplies that root
2 The boot chain is verified by the silicon, so the displayed root is not chosen by whoever last wrote the card Trust in the closed boot ROM that performs the check

Source: docs/VERIFICATION.md, "The three tiers", and docs/THREAT-MODEL.md, "Out of scope". Tier 2 burns one-time fuses and is irreversible; this profile does not require it.

4.4 nullroute conformance

Id Status Evidence
SP-ATT-1 Met in tests; unverified on the panel INV-UI-1, INV-UI-64
SP-ATT-2 Met Makefile target manifest; INV-BUILD-1
SP-ATT-3 Met docs/VERIFICATION.md recipe, make manifest-recipe
SP-ATT-4 Met docs/VERIFICATION.md
SP-ATT-5 Met MANIFEST_ROOTS := package.json package-lock.json packages spec provisioning in Makefile; package-lock.json is the first line of MANIFEST.lock
SP-ATT-6 Met Screen half: INV-UI-2, INV-UI-70. Daemon half: requirePassingVerification, now INV-BUILD-1 with five tests in packages/daemon/test/verification-gate.test.ts, which also refuses a report that records no checks
SP-ATT-7 Met INV-UI-5, INV-UI-69
SP-ATT-8 Met INV-UI-1, INV-UI-6
SP-ATT-9 Met INV-UI-72
SP-ATT-10 Met in tests; unverified on hardware INV-BOOT-1
SP-ATT-11 Met INV-UI-4, INV-KEY-6
SP-ATT-12 Manual docs/THREAT-MODEL.md, docs/VERIFICATION.md

Rendered from spec/signer-profile.md, the same bytes that ship with the code. SHA-256 4138440d8a2b925d07a9b6d1d96d4189a74c399b5fc717ae27a79d65c0065817