This document describes the security architecture of Heads: how trust is established, how integrity is verified at each boot, and how secrets are protected.
See also: architecture.md, tpm.md, boot-process.md, ux-patterns.md.
Heads implements a cross-validated boot chain where multiple security mechanisms verify each other, preventing single points of failure.
┌─────────────────────────────────────────────────────────────────────────────┐
│ HEADS CROSS-VALIDATED BOOT CHAIN │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ SPI Flash │───▶│ TPM │───▶│ /boot │───▶│ OS │ │
│ │ ROM │ │ PCRs │ │ GPG Sig │ │ Disk │ │
│ │ (Hardware │ │ (Measured) │ │ (Signed) │ │ (LUKS) │ │
│ │ RoT) │ │ │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ROLLBACK COUNTER (TPM NVRAM) │ │
│ │ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ TPM NVRAM │◀────────────▶│ /boot disk │ │ │
│ │ │ (counter) │ 2-way │ kexec_ │ │ │
│ │ │ │ binding │ rollback.txt │ │ │
│ │ └───────────────┘ └───────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ TPM-SEALED SECRETS (NVRAM) │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────────┐ │ │
│ │ │ TOTP/HOTP SHARED SECRET (NVRAM index 4d47) │ │ │
│ │ │ 20 bytes random, sealed to PCRs 0,1,2,3,4,7 │ │ │
│ │ │ │ │ │
│ │ │ Same secret ──▶ TOTP ──▶ Phone authenticator app │ │ │
│ │ │ └──▶ Reverse HOTP ──▶ USB dongle (verifies code) │ │ │
│ │ └────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────────────────┐ │ │
│ │ │ LUKS DUK (Disk Unlock Key) │ │ │
│ │ │ 128 bytes random, sealed to PCRs 0,1,2,3,4,5,6,7 │ │ │
│ │ │ - PCR 5: kernel modules, PCR 6: LUKS header │ │ │
│ │ └────────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
This table shows how each component verifies the others:
| Component | Verifies | Verified By | Prevents |
|---|---|---|---|
| SPI Flash ROM | TPM PCRs | TPM measured boot | Firmware tampering |
| TPM PCRs | /boot files | TPM unseal policy | Firmware change |
| /boot files | GPG signature | ROM public key | Boot config tampering |
| GPG signature | Rollback counter hash | TPM (via PCRs) | Old /boot rollback |
| Rollback counter | TPM + /boot binding | Both must match | TPM/boot swap attack |
| TOTP/HOTP | PCR match (same secret) | TPM unseal | Firmware tampering |
| LUKS DUK | PCR + /boot | TPM unseal + sig | Disk theft |
The diagram below shows the standard TPM-based boot path. For boards without TPM hardware, see HOTP on boards without a TPM.
SPI flash ROM (hardware root of trust)
│
│ coreboot SRTM measures boot block + payload into PCR 2; PCRs 0,1,3 unused
▼
TPM PCR values (hardware-attested firmware state)
│
│ Heads unseals TOTP/HOTP secret only when PCRs match expected values
▼
TOTP/HOTP code (proves firmware was not tampered since last seal)
│
│ User verifies TOTP/HOTP matches the value on their phone/token
▼
User-approved boot (human-in-the-loop verification)
│
│ GPG signature on /boot/kexec.sig verified against ROM-fused public key
▼
/boot integrity (OS bootloader, kernel, and initrd authenticated)
│
│ LUKS DUK unsealed from TPM (only when PCRs match + /boot is signed)
▼
Decrypted OS disk (disk encryption key delivered without passphrase prompt)
The trust anchor is the SPI flash ROM containing coreboot. Heads treats this as the immutable starting point:
- Coreboot measures firmware stages and the Linux payload into TPM PCR 2 (SRTM) before executing it.
- The Linux payload is embedded in the ROM (no network, no external media required).
- The ROM is physically write-protected on supported boards. See wp-notes.md for current status.
There is no certificate authority, no boot server, and no runtime network access during the verified boot path.
The bootblock (IBB — Initial Boot Block) is the Static Core Root of Trust for Measurement (S-CRTM): the first code executed by the CPU, directly from SPI flash, before anything else has run. All subsequent stages are measured from it.
Coreboot's measured boot (CONFIG_TPM_MEASURED_BOOT=y) measures the full
firmware chain into PCR 2 (CONFIG_PCR_SRTM=2):
bootblock → romstage → ramstage → Heads Linux kernel + initrd (payload)
On boards with CONFIG_TPM_MEASURED_BOOT=y + CONFIG_TPM_INIT_RAMSTAGE=y
(the majority of maintained boards), ramstage initializes the TPM, reads each
prior stage from CBFS, and extends PCR 2. Older coreboot versions (4.11) used
CONFIG_TPM_INIT=y before this config key existed; some boards have no TPM
hardware. See tpm.md for the full breakdown.
PCRs 0, 1, and 3 are unused — the CONFIG_PCR_* entries for those registers
are slot assignments for optional coreboot features that are not enabled. They
remain at zero and are anchored as zero in sealing policies.
Heads extends additional PCRs during userspace boot:
- PCR 4 — boot mode tracking; see below
- PCR 5 — each kernel module loaded via the
insmodwrapper (binary + parameters) - PCR 6 — LUKS header dump (by
qubes-measure-luks) before disk unlock - PCR 7 — each CBFS/UEFI file extracted from ROM (by
cbfs-init/uefi-init)
Heads extends PCR 4 further depending on execution path:
- Normal boot:
calcfuturepcr 4pre-computes the expected value and secrets are sealed against it. - Recovery shell: PCR 4 is extended with
"recovery", invalidating normal-boot unsealing for the rest of the session.
See tpm.md for the full PCR table and sealing policies. For board-specific RoT configuration and the files that control each PCR, see tpm.md — Configuration reference for developers.
TOTP and HOTP share the same 20-byte secret sealed to TPM NVRAM (index 4d47). The secret can only be unsealed when the firmware PCR state matches what was recorded at seal time. A firmware change causes a PCR mismatch and unseal failure, which the user observes as a TOTP/HOTP mismatch.
A 20-byte random secret is generated at OEM Factory Reset and sealed to
TPM NVRAM. At each boot, unseal-totp retrieves it and generates the current
30-second code. The user compares this against their authenticator app.
The same secret is used for HOTP. On supported dongles (Nitrokey Pro/Storage/3, Librem Key), Heads uses reverse HOTP verification:
- Heads unseals the shared secret from TPM
- Heads computes the HOTP code using secret + counter
- Heads sends the computed code to the dongle
- Dongle verifies the code matches its own computation and signals via LED:
- Green blinking: code verified
- Red blinking: code mismatch
This is "reverse" because normally the dongle generates the code - here the computer generates it and the dongle verifies. The dongle provides a tamper signal independent of the display (visible before screen is initialized).
The HOTP counter is stored in /boot/kexec_hotp_counter (a plain file, not
in TPM NVRAM).
On boards where CONFIG_NO_TPM=y (currently the Librem Mini, Librem Mini v2,
and Librem 11), there is no TPM to seal secrets against PCR values. Heads falls
back to a different HOTP secret derivation implemented in secret_from_rom_hash
in initrd/etc/functions:
- At seal time,
flash.shreads the full SPI ROM via flashrom/flashprog. - The SHA-256 of the ROM image is used directly as the HOTP secret.
- The secret is programmed onto the USB security dongle.
- At each boot, the ROM is read again, SHA-256 recomputed, and the HOTP code sent to the dongle for comparison. A changed ROM produces a different hash, a different code, and a dongle failure signal.
The HOTP counter is stored in /boot/kexec_hotp_counter as a plain file
(not in TPM NVRAM, which does not exist on these platforms).
Known limitations of ROM-hash HOTP (publicly noted):
- The secret is deterministic and derived from public data — anyone with physical access to read the ROM can derive the HOTP secret independently, without owning the dongle.
- No hardware platform binding: the secret is not tied to the specific hardware instance, only to ROM contents.
- ROM reading via flashrom/flashprog at every boot expands attack surface and is slower than a TPM unseal.
- The counter file on
/bootis not TPM-protected and could in principle be manipulated to extend the HOTP window (the token accepts codes within a ±5-count lookahead window). - There is no equivalent of TOTP on these boards; time-based attestation without a TPM is not implemented.
- LUKS disk encryption key sealing to TPM (DUK) is not available; disk unlock requires the user's passphrase at every boot.
The ROM-hash HOTP mode provides a weaker attestation model than the TPM-based path. Its value is in detecting ROM modifications via the dongle's LED, but it does not provide the same tamper-evident guarantees as TPM PCR sealing.
If TOTP or HOTP unseal fails, INTEGRITY_GATE_REQUIRED is set and all TPM
secret sealing operations are blocked until the integrity gate passes.
See ux-patterns.md.
All files in /boot are protected by a SHA-256 hash manifest and a GPG
detached signature (kexec.sig).
When the user installs or updates the OS, kexec-sign-config:
- Hashes all non-
kexec*files in/bootintokexec_hashes.txtand generates a directory tree listing inkexec_tree.txt. - Signs the hash manifest with a GPG key, producing
kexec.sig. - Increments the TPM rollback counter and stores the new counter hash in
kexec_rollback.txt.
The signing key lives on a hardware security dongle (OpenPGP smartcard),
never in the ROM. Signing requires physical possession of the card and
knowledge of the card PIN. To reduce repeated PIN prompts within the same
session, Heads caches the validated User PIN in /tmp/secret/gpg_pin (mode
600, tmpfs; cleared at power-off). See
ux-patterns.md — GPG User PIN caching
for the caching mechanism and its security properties.
At each boot, verify_global_hashes in kexec-select-boot calls
verify_checksums and check_config to confirm that every /boot file
matches its stored hash and that kexec.sig is valid. A hash or signature
failure causes die — there is no "boot anyway" path.
The ROM contains only the public key. Verification uses gpgv with
the ROM keyring; no private key material is needed at boot.
The rollback counter creates a two-way binding between the TPM hardware and the /boot disk, preventing swap attacks.
┌─────────────────────────────────────────────────────────────────────────────┐
│ ROLLBACK COUNTER ATTACK PREVENTION │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ OLD TPM │ │ OLD /boot │ │
│ │ │ │ │ │
│ │ counter=5 │ │ hash=5 │ │
│ │ (NVRAM) │ │ (disk) │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ATTACK SCENARIO: Old TPM + Old /boot │ │
│ │ │ │
│ │ Attacker uses old TPM (counter=5) with old /boot │ │
│ │ (hash=5). This would bypass security updates! │ │
│ │ │ │
│ │ → BLOCKED: TPM unseal requires current PCR values │ │
│ │ → BLOCKED: GPG signature must match current /boot │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ NEW TPM │ │ OLD /boot │ │
│ │ │ │ │ │
│ │ counter=X │ │ hash=5 │ │
│ │ (no secrets │ │ (disk) │ │
│ │ sealed!) │ │ │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ATTACK SCENARIO: New TPM + Old /boot │ │
│ │ │ │
│ │ Attacker swaps TPM. New TPM has no sealed secrets. │ │
│ │ Old /boot has old counter hash. │ │
│ │ │ │
│ │ → BLOCKED: TOTP/HOTP/DUK unseal fails (no secrets) │ │
│ │ → BLOCKED: Rollback counter mismatch detected │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
- TPM stores counter: A monotonic counter is created in TPM NVRAM at OEM Factory Reset
- /boot stores hash: SHA-256 hash of counter value is stored in
/boot/kexec_rollback.txt - Counter increments on update: Every
kexec-sign-configrun increments the counter and updates the hash - Verification at boot:
kexec-select-bootverifies the counter matches the stored hash
┌─────────────────────────────────────────────────────────────────────────────┐
│ ROLLBACK COUNTER LIFECYCLE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ OEM FACTORY RESET NORMAL BOOT │
│ ───────────────── ──────────── │
│ │
│ 1. Create counter in TPM NVRAM 1. Read counter from TPM │
│ └─▶ counter_value = 0 │ │
│ 2. Hash counter → /boot ▼ │
│ └─▶ kexec_rollback.txt 2. Hash counter │
│ contains hash of "0" │ │
│ ▼ │
│ 3. Compare with /boot hash │
│ │ │
│ ▼ │
│ OS UPDATE 4. Match? → Continue │
│ ────────── 5. Mismatch? → Die │
│ │
│ 1. kexec-sign-config runs TPM SEALED SECRETS │
│ │ ────────────────── │
│ ▼ TOTP/HOTP/DUK can only unseal │
│ 2. Increment TPM counter if: │
│ └─▶ counter_value = 1 - PCRs match seal policy │
│ 3. Hash new value → /boot - TPM is the SAME TPM │
│ └─▶ kexec_rollback.txt - /boot is the SAME /boot │
│ updates hash of "1" │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
TPM-sealed secrets (TOTP, HOTP, LUKS DUK) are bound to specific PCR values, creating additional hardware binding:
┌─────────────────────────────────────────────────────────────────────────────┐
│ TPM SEALING PCR POLICIES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ TOTP/HOTP Secret Seal Policy: PCRs 0,1,2,3,4,7 │ │
│ │ │ │
│ │ PCR 0,1,2,3: Platform configuration (unused but anchored as zero) │ │
│ │ PCR 4: Boot mode (normal/recovery/usb) │ │
│ │ PCR 7: CBFS/ROM files (user-injected) │ │
│ │ │ │
│ │ NOT included: PCR 5 (kernel modules), PCR 6 (LUKS headers) │ │
│ │ → Allows disk updates without resealing TOTP/HOTP │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ LUKS DUK Seal Policy: PCRs 0,1,2,3,4,5,6,7 │ │
│ │ │ │
│ │ PCR 0,1,2,3: Platform configuration │ │
│ │ PCR 4: Boot mode │ │
│ │ PCR 5: Kernel modules (if loaded) │ │
│ │ PCR 6: LUKS header (measured at seal time) │ │
│ │ PCR 7: CBFS/ROM files │ │
│ │ │ │
│ │ Includes: PCR 5, PCR 6 → More restrictive │ │
│ │ → Changing kernel modules or LUKS headers requires resealing DUK │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ PCR 16 (Scratch) │ │
│ │ │ │
│ │ Used internally for calcfuturepcr (pre-computing future values) │ │
│ │ Not part of any sealing policy - purely for calculation │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
The LUKS Disk Unlock Key (DUK) is a random binary key that:
- Is generated from
/dev/urandombykexec-seal-key(128 characters — 1024 bits of entropy). - Is sealed to TPM NVRAM with PCR policy
0,1,2,3,4,5,6,7. - Is added as a LUKS key slot alongside the user's Disk Recovery Key (DRK).
- At boot,
kexec-insert-keyunseals it and injects it into a minimal initrd prepended to the OS initrd. The OS kernel unlocks LUKS without prompting the user.
If the TPM refuses to unseal (PCR mismatch, TPM reset), the OS falls back to prompting for the DRK passphrase. The DRK is always a valid recovery path.
Before any operation that seals new TPM secrets, gate_reseal_with_integrity_report
in gui-init verifies:
- TPM is not in a reset-required state.
- No prior TOTP/HOTP failure is recorded (
INTEGRITY_GATE_REQUIREDis unset). /boothash verification passes.kexec.sigis valid and signed by a key in the current keyring.- If HOTP is enabled: the USB security token is present.
- User explicitly confirms proceeding.
If any check fails, the sealing operation is aborted. This prevents new
secrets from being sealed against a potentially compromised /boot.
For the UNKNOWN_KEY scenario and correct error messaging, see ux-patterns.md.
oem-factory-reset re-establishes full ownership of the device in five phases:
- TPM reset — clears the TPM owner hierarchy, removes all NVRAM indices, and invalidates all sealed secrets.
- GPG key initialization — generates new keys (in-memory RSA or ECC, or on-smartcard) and configures the OpenPGP card PINs. The card PIN length is limited to 25 characters due to a firmware constraint on supported tokens (Librem Key / Nitrokey HOTP).
- TPM rollback counter creation — creates a new monotonic counter and
stores its initial hash in
/boot/kexec_rollback.txt. /bootsigning — hashes and GPG-signs the initial/bootstate.- TOTP/HOTP and LUKS DUK sealing — TOTP/HOTP secrets are sealed immediately; LUKS DUK sealing is performed by the user on the next boot via the GUI menu.
All verification failures are fatal by default:
- GPG signature mismatch →
die(recovery shell) - Hash mismatch →
die(recovery shell) - TPM counter mismatch →
die(recovery shell) - TOTP unseal failure → error menu (no unattended boot)
- LUKS DUK unseal failure → OS prompts for DRK passphrase (no silent failure)
There is no "continue anyway" path for integrity failures.