Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -570,13 +570,6 @@ workflows:
requires:
- EOL_t480-hotp-maximized [seed:coreboot-25.09]

- build:
name: EOL_t420-hotp-maximized
target: EOL_t420-hotp-maximized
subcommand: ""
requires:
- EOL_t480-hotp-maximized [seed:coreboot-25.09]

- build:
name: EOL_t420-maximized
target: EOL_t420-maximized
Expand Down Expand Up @@ -675,13 +668,6 @@ workflows:
requires:
- EOL_t480-hotp-maximized [seed:coreboot-25.09]

- build:
name: EOL_x220-hotp-maximized
target: EOL_x220-hotp-maximized
subcommand: ""
requires:
- EOL_t480-hotp-maximized [seed:coreboot-25.09]

- build:
name: EOL_x220-maximized
target: EOL_x220-maximized
Expand Down
2 changes: 1 addition & 1 deletion doc/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ The CI pipeline's workspace and cache behavior is documented in

- **No network at boot** — all verification is local; no certificate authorities
- **Hardware root of trust** — the coreboot bootblock (IBB) is the Static Core Root of Trust for Measurement (S-CRTM): the first code executed by the CPU, directly from SPI flash. Coreboot implements a transitive measurement chain: the CRTM measures FMAP and the bootblock image into the preram log, then each subsequent stage measures the next before executing it — bootblock measures romstage, romstage measures ramstage, ramstage measures the Heads payload. Measurements are taken during CBFS file loading, before decompression, and are recorded in TPM PCR 2 (SRTM) once the TPM hardware is initialized (`tpm_setup()`). Measurements taken before TPM init are cached in the preram log and flushed to PCR 2 by `tspi_measure_cache_to_pcr()` during `tpm_setup()`. The full chain — bootblock → romstage → ramstage → Heads Linux kernel + initrd — is recorded into PCR 2. PCRs 0, 1, and 3 remain zero as policy anchors. See [tpm.md](tpm.md#srtm-in-coreboot) for TPM init timing per board. See [wp-notes.md](wp-notes.md#pr0-chipset-locking) for SPI write-protection and PR0 chipset locking details.
- **Fail-closed** — failed integrity verification drops to a recovery shell. Recovery shell authentication via GPG smartcard is enforced when GPG key backup has been configured (`CONFIG_HAVE_GPG_KEY_BACKUP=y`), which is set by answering "y" to `"Would you like to format an encrypted USB Thumb drive to store GPG key material? (Required to enable GPG authentication)"` during OEM Factory Reset / Re-Ownership. Otherwise the recovery shell is unauthenticated. An "Ignore tampering and force a boot (Unsafe!)" option is available to override this.
- **Fail-closed** — failed integrity verification drops to a recovery shell. Recovery shell authentication via GPG smartcard is enforced when GPG key backup has been configured (`CONFIG_HAVE_GPG_KEY_BACKUP=y`), which is set by answering "y" to `"Would you like to format an encrypted USB Thumb drive to store GPG key material? (Required to enable GPG authentication)"` during OEM Factory Reset / Re-Ownership, or by running "Reprovision smartcard from GPG key backup" from the GPG Management Menu. Otherwise the recovery shell is unauthenticated. An "Ignore tampering and force a boot (Unsafe!)" option is available to override this. See [recovery-shell.md](recovery-shell.md#authentication) for details.
- **Separation of duties** — the public key that verifies `/boot` signatures is stored in CBFS (ROM). The private key that signs `/boot` stays on a USB security dongle and never leaves it.
- **Auditability** — all source is open, builds are reproducible, ROM images are verifiable

Expand Down
49 changes: 49 additions & 0 deletions doc/configuring-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,55 @@ If you already have a provisioned USB Security dongle:
3. Follow the steps. After reflashing, reboot.
4. Generate a new TOTP/HOTP secret when prompted.

## Restoring Keys from Backup

If you chose the in-memory backup path during OEM factory reset and your
dongle is lost, broken, or wiped:

1. Insert the backup USB thumb drive and your (new) dongle.
2. Go to `Options -> GPG Management -> Reprovision smartcard from GPG key backup`.
3. Enter the backup passphrase when prompted.
4. Heads detects the key type from the backup, factory-resets the dongle,
restores the subkeys, and sets the card identity.
5. After success, flash the public key to the running BIOS for persistence.
6. Re-sign /boot via `Options -> Update checksums and sign all files in /boot`.

This requires the LUKS-encrypted backup USB drive created during OEM factory
reset (answer Y to "format an encrypted USB Thumb drive"). Without it, run
a new OEM Factory Reset / Re-Ownership to rekey the device.

The reprovision flow is reachable from several places, not just the GPG
Management Menu:

- `Options -> GPG Management -> 'k'` — always visible (gpg-gui.sh).
- `'K'` in the empty-GPG-keyring error dialog (`check_gpg_key` in gui-init.sh).
- `'K'` in the measured integrity report (`report_integrity_measurements` in
gui_functions.sh), both the normal and the DONGLE KEY NOT ROM-TRUSTED
variants.
- `'K'` in the TPM State Inconsistent (rollback preflight) dialog — the menu
loop re-displays afterwards since key provisioning does not fix the TPM
counter problem.
- `'K'` in the clean boot wizard (`clean_boot_check`), next to `F` (OEM
Factory Reset), `i` (ignore), and `x` (recovery shell).

**When the TPM needs a reset:** if the rollback counter preflight failed
(`tpm_reset_required` is set), the reprovision flow skips the TPM counter
creation step entirely — the TPM owner passphrase is unknown at that point,
so prompting would be a dead end. Key restoration still completes, and Heads
then warns:

TPM rollback counter was not created. Reset the TPM from
Options -> TPM/TOTP/HOTP Options -> Reset the TPM before
the next boot to avoid being dropped into recovery shell.

The integrity report reflects the same state: TOTP shows
`SEALED SECRET UNAVAILABLE - TPM reset required (rollback counter cannot be
verified)` and the report note directs you to `Reset the TPM`, which re-creates
the rollback counter and regenerates TOTP/HOTP secrets.

After reprovisioning, the recovery shell and USB boot will require GPG
smartcard authentication; see [recovery-shell.md](recovery-shell.md#authentication).

## Forgotten GPG User PIN

From Recovery Shell with the dongle inserted:
Expand Down
12 changes: 12 additions & 0 deletions doc/gpg.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ public key into the Heads firmware:
3. From Heads: `Options -> GPG Management -> Add a GPG key to the running BIOS + reflash`.
4. Reboot. Generate a new TOTP/HOTP secret when prompted.

## Restoring Keys from Backup

See [configuring-keys.md](configuring-keys.md#restoring-keys-from-backup) for
the full recovery flow. In short:

1. Insert the backup USB thumb drive and the replacement dongle.
2. From Heads: `Options -> GPG Management -> Reprovision smartcard from GPG key backup`.
3. Enter the backup passphrase.

The backup drive must have been created by the OEM factory reset flow with
the "format an encrypted USB Thumb drive" option set to Y.

## Nitrokey 3 Specifics

- Supports NIST P-256 ECC keys in addition to RSA — significantly faster key
Expand Down
53 changes: 43 additions & 10 deletions doc/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,20 +148,53 @@ nix develop --command make BOARD=$BOARD

### Maintenance targets

All run under `nix develop` (local) or `./docker_repro.sh` (Docker) with `make BOARD=$BOARD <target>`.

#### Nuclear clean (`real.gitclean*`)

Use `git clean -ffxd` (double `-f`). Deletes everything not tracked by Git, including
downloaded git repositories (coreboot forks, dasharo-ec, tpm-gpio-fail, etc.).
Produces a pristine working tree — all modules must be re-cloned and rebuilt from scratch.
No `.canary` is recreated after the wipe.

| Target | What it preserves |
|--------|-------------------|
| `real.gitclean` | Nothing — full wipe |
| `real.gitclean_keep_pkg` | `packages/` (downloaded tarballs) |
| `real.gitclean_keep_build` | `packages/` and `build/` |

#### Developer clean (`real.devclean*`)

Use `git clean -fxd` (single `-f`). Same as nuclear clean but **skips** downloaded git
repositories, preserving coreboot forks, dasharo-ec, tpm-gpio-fail, etc. Recreates a
bogus `.canary` for the configured board's coreboot tree, forcing a full rebuild of
coreboot-dependent modules without re-cloning the repos.

| Target | What it preserves |
|--------|-------------------|
| `real.devclean` | Nothing — wipe untracked, skip git repos |
| `real.devclean_pkg` | `packages/` (downloaded tarballs) |
| `real.devclean_build` | `packages/` and `build/` |

#### Other

| Target | What it does |
|--------|-------------|
| `real.clean` | Remove all build artifacts |
| `real.gitclean` | `git clean` — remove all untracked files |
| `real.gitclean_keep_packages` | `git clean` but keep downloaded tarballs in `packages/` |
| `real.clean` | Remove all build artifacts and install output; keep crossgcc; recreate `.canary` to force rebuild |
| `real.remove_canary_files-extract_patch_rebuild_what_changed` | Remove all `.canary` sentinels, clear install + coreboot/board build caches, then rebuild. Use this after changing patches. |
| `real.gitclean_keep_packages_and_build` | Keep packages + clean + full rebuild |

All run under `nix develop` (local) or `./docker_repro.sh` (Docker):

```bash
nix develop --command make BOARD=$BOARD real.clean
nix develop --command make BOARD=$BOARD real.remove_canary_files-extract_patch_rebuild_what_changed
```
#### When to use which

| Scenario | Target |
|----------|--------|
| Full from-scratch rebuild (CI, release, broken state) | `real.gitclean` |
| Full rebuild, keep cached downloads | `real.gitclean_keep_pkg` |
| Full rebuild, keep everything compiled | `real.gitclean_keep_build` |
| Daily work: wipe artifacts but keep git clones | `real.devclean` |
| Daily work: keep downloaded tarballs too | `real.devclean_pkg` |
| Daily work: keep all cached artifacts | `real.devclean_build` |
| Changed a patch, need to re-extract and rebuild one module | `real.remove_canary_files-extract_patch_rebuild_what_changed` |
| Quick rebuild after source changes, keep crossgcc | `real.clean` |

### Module-level helpers

Expand Down
25 changes: 25 additions & 0 deletions doc/qemu.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,31 @@ How I tested these wrappers (smoke checks)
- Minimal: `source docker/common.sh && build_docker_opts` — should print a short description and show flags such as `--device=/dev/kvm` when KVM is available and `-v /tmp/heads-docker-xauth-XXXXXX:...` (or `-v /tmp/.docker.xauth-<uid>:...` as fallback) when Xauthority was created.
- Functional (examples tested by PR author): see the tests in the PR body (Ubuntu, Debian, Fedora installer flows). Consider testing `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` locally to verify KVM+GTK behavior.

Resetting state
---

QEMU boards using the default virtual token persist canokey and TPM state
between runs. To simulate a fresh dongle and TPM for testing:

```bash
# Wipe the virtual Canokey (new dongle, no keys on card).
sudo rm -f build/x86/<BOARD>/.canokey-file

# Wipe the virtual TPM (new TPM, no sealed secrets or counters).
sudo rm -rf build/x86/<BOARD>/vtpm/
```

The next `make run` will create fresh `.canokey-file` and `vtpm/`
directories automatically. The Heads setup wizard will then offer OEM
factory reset (F) or reprovision from backup (K).

To preserve canokey state for reuse:

```bash
cp build/x86/<BOARD>/.canokey-file ~/Qemu_img/.canokey-file.bak
cp ~/Qemu_img/.canokey-file.bak build/x86/<BOARD>/.canokey-file
```

Troubleshooting
---

Expand Down
105 changes: 105 additions & 0 deletions doc/recovery-shell.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,111 @@ The Recovery Shell boots with PCR 4 set to `recovery` instead of
- TOTP/HOTP sealing and TPM Disk Unlock Key creation/unsealing do not work.
- To perform seal/unseal operations return to the normal GUI boot.

## Authentication

`gpg_auth()` in `initrd/etc/functions.sh` guards the recovery shell and
external media boot entry. On boards with `CONFIG_HAVE_GPG_KEY_BACKUP=y`,
recovery calls `gpg_auth()` before opening the bash prompt, and
`media-scan.sh` (called from `usb-init.sh`) also invokes `gpg_auth` before
scanning USB devices. TPM operations, flash/update, GPG management, and
all other GUI menu functions are NOT gated by this check — they remain
accessible from the main menu.

After OEM Factory Reset / Re-Ownership or the reprovision flow,
`CONFIG_HAVE_GPG_KEY_BACKUP=y` is persisted in the user config stored in
CBFS, so that recovery shell and USB boot authentication is enforced
even on boards where the compile-time default differs.

To enable `gpg_auth` during OEM Factory Reset / Re-Ownership, answer Y to
"format an encrypted USB Thumb drive to store GPG key material? (Required
to enable GPG authentication)". Choosing N skips the backup drive creation
and leaves `gpg_auth` disabled (recovery shell and USB boot remain
unauthenticated).

**What `gpg_auth` guards — and what it does not:**

`gpg_auth()` is called from exactly two places: the recovery shell entry
point (`recovery()` in `functions.sh`) and the USB/external media boot
scanner (`media-scan.sh`, called from `usb-init.sh`). The normal GUI boot
flow (`gui-init.sh`) does NOT invoke `gpg_auth()` — it handles GPG key
presence through its own `check_gpg_key` mechanism, which is separate from
`gpg_auth`.

Many destructive operations are already available from the GUI without
`gpg_auth`: the user can reflash firmware (`Options -> Flash/Update`),
wipe GPG keys and config (`Options -> Clear GPG key(s) and reset all user
settings`), reset the TPM, re-sign boot hash manifests
(`Options -> Update checksums and sign all files in /boot`),
and generate new GPG keys. All of these are tamper-evident — the TPM
PCR measurements and HOTP/TOTP codes will detect changes.

What `gpg_auth` prevents:

- **Recovery shell entry without authentication.** The recovery shell
gives unrestricted root access to raw block devices, SPI flash, TPM
commands, and GPG key operations. The GUI does not expose
`flash.sh -r` (dump running firmware). An attacker with recovery
shell access could dump the full SPI flash for offline analysis, forge
a malicious firmware image, and flash it back undetected. `gpg_auth`
blocks this path. (Note: an attacker with physical access can still
extract the SPI flash via an external programmer after disassembly —
`gpg_auth` only protects the in-software path through the running
system.)

- **USB/external media boot from an untrusted drive.** `media-scan.sh`
(called from `usb-init.sh`) invokes `gpg_auth` before scanning USB
devices. When `CONFIG_HAVE_GPG_KEY_BACKUP` is not set, `gpg_auth`
is a no-op and USB boot proceeds without restriction. When the flag
is set (after OEM factory reset or reprovision), the user must
authenticate with their GPG smartcard before external media boot is
allowed. (Note: the boot process locks the SPI flash controller,
making Heads the only internal flasher. An attacker booting from
USB cannot reflash the firmware even with root access. External
SPI flashing via hardware programmer after disassembly is always
possible regardless — see [wp-notes.md](wp-notes.md).)

**What `gpg_auth` does NOT prevent:**

- Reflashing firmware through the GUI
- Wiping GPG keys, config, or TPM state through the GUI
- Re-signing `/boot` through the GUI
- Any operation the user can perform from the main menu

**How it works:** `gpg_auth()` generates a random nonce, the user signs it
with their GPG key (smartcard or backup USB drive) within 3 attempts, and the
signature is verified against the ROM-fused public keyring. On failure,
`DIE` exits the session. With `CONFIG_RESTRICTED_BOOT=y`, the shell is
blocked entirely and the system reboots after 5 seconds.

**Without authentication:** If the board does not have
`CONFIG_HAVE_GPG_KEY_BACKUP` set, `gpg_auth()` is a no-op and the recovery
shell opens without prompting.

## Resetting Configuration

`Options -> Change configuration settings -> 'r'` (`Clear GPG key(s) and reset
all user settings`) wipes the running system configuration:

- Clears `~/.gnupg` (GPG keyring, trustdb)
- Deletes `/boot/kexec*.txt` and `/boot/kexec.sig` (boot hash manifests,
checksums, rollback counter, signatures)
- Removes all `heads/` files from CBFS (keyring, trustdb, config.user)
- Reflashes the cleaned firmware
- Resets the TPM if present

**Attestation impact:** Removing `heads/` files from CBFS changes the SPI flash
contents measured into PCR 7 by `cbfs-init` at next boot. Combined with the
TPM reset, TOTP/HOTP unseal will fail on the next boot — Heads shows the
standard red-menu TOTP error prompt.

**Recovery after wipe:** Run OEM Factory Reset / Re-Ownership (Options -> 'F')
to fully reprovision ([configuring-keys.md](configuring-keys.md)). If you have a
GPG key backup USB drive from a previous in-memory OEM reset, use
`GPG Options -> 'k' Reprovision USB Security dongle from GPG key backup`
to restore subkeys from the backup ([configuring-keys.md#restoring-keys-from-backup](configuring-keys.md#restoring-keys-from-backup)),
then flash the public key to ROM, re-sign /boot, and generate new TOTP/HOTP
secrets.

## Common Operations

### Manual boot
Expand Down
6 changes: 5 additions & 1 deletion initrd/bin/gpg-gui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ TRACE_FUNC
while true; do
unset menu_choice
whiptail_type $BG_COLOR_MAIN_MENU --title "GPG Management Menu" \
--menu 'Select the GPG function to perform' 0 80 10 \
--menu 'Select the GPG function to perform' 0 80 11 \
'r' ' Add GPG key to running BIOS and reflash' \
'a' ' Add GPG key to standalone BIOS image and flash' \
'e' ' Replace GPG key(s) in the current ROM and reflash' \
'l' ' List GPG keys in your keyring' \
'p' ' Export public GPG key to USB drive' \
'g' ' Generate GPG keys manually on a USB security dongle' \
'k' ' Reprovision USB Security dongle from GPG key backup' \
'x' ' Exit' \
2>/tmp/whiptail || recovery "GUI menu failed"

Expand Down Expand Up @@ -64,6 +65,9 @@ while true; do
gpg_post_gen_mgmt
fi
;;
"k")
reprovision_smartcard_from_backup
;;
esac

done
Expand Down
Loading