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:
- The manifest lists every file under the implementer's declared source roots that is tracked by the implementer's version control.
- 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 coreutilssha256sum. - Lines are ordered by path, compared byte by byte, which is
sortunderLC_ALL=C. Locale-aware ordering MUST NOT be used, because it makes the result depend on the verifier's language settings. - 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.
- 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.
- The root is a hash of a file, not of the running code. On nullroute the
build copies
MANIFEST.lockinto the image (provisioning/build/build-system.shline 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). - 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 bysha256sum -cwhen the report is written, and by dm-verity block by block on a tier 1 build. - Dependencies are in the root by reference, not by content. nullroute's
manifest covers tracked files under
packages/,spec/andprovisioning/, and the rootpackage.jsonandpackage-lock.json(SP-ATT-5), so a changed dependency version or integrity hash moves the displayed value. The installednode_modulesis 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. - 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.yamllines 187-193). - Firmware and hardware are not covered. The SoC boot ROM, GPU firmware and any radio silicon sit below every value in this section.
- No release value exists to compare against yet. Nothing has been
published or signed (
docs/THREAT-MODEL.mdline 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