Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
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 USB Security dongle 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
51 changes: 51 additions & 0 deletions doc/configuring-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,57 @@ 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 -> 'k' Reprovision USB Security dongle 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, sets the card identity, resets the TPM, creates a
fresh rollback counter, and re-signs /boot (see below).
5. After success, flash the public key to the running BIOS for persistence
(skipped automatically on QEMU boards).
6. Reboot to finalize.

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 — since
reprovisioning resets the TPM and creates a fresh counter, the preflight
check re-runs afterwards and the gate proceeds to boot when it passes.
- `'K'` in the clean boot wizard (`clean_boot_check`), next to `F` (OEM
Factory Reset), `i` (ignore), and `x` (recovery shell).

**TPM handling:** reprovisioning is an ownership-level operation, so like
OEM Factory Reset it resets the TPM itself rather than punting a manual
"Reset the TPM" step to you. After you set the new TPM owner passphrase,
Heads clears the old ownership, drops stale rollback-counter references,
creates a fresh rollback counter, and re-signs /boot — all through the same
`kexec-sign-config.sh` path used by OEM factory reset. Signing is atomic:
the new manifests are staged under /tmp and moved into /boot only after
signing and verification succeed. If any step fails, /boot keeps its
previous valid signatures, no ROM flash is offered, and Heads explains how
to recover (re-sign via `Options -> Update checksums and sign all files in
/boot`, or reset the TPM first if that failed).

Because the TPM was reset, unsealing the old TOTP/HOTP secrets on the next
boot will fail; Heads offers to regenerate them (choose `g`).

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 -> 'k' Reprovision USB Security dongle 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
8 changes: 4 additions & 4 deletions doc/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,10 @@ nix develop --command make BOARD=$BOARD
| 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.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 |
| `real.gitclean` | `git clean -fxd` — remove all untracked and ignored files |
| `real.gitclean_keep_packages` | `git clean -fxd` but keep downloaded tarballs in `packages/` |
| `real.gitclean_keep_packages_and_build` | `git clean -fxd` keeping `packages/` and `build/` |
| `real.remove_canary_files-extract_patch_rebuild_what_changed` | Delete `.canary` stamps so the next `make` re-extracts, re-patches, and rebuilds only what changed |

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

Expand Down
131 changes: 105 additions & 26 deletions doc/qemu.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ and ext4 filesystem. Older images (from before `qemu-img create`)
may be flat — check with `sudo fdisk -l` first.

Note: the Docker container bind-mounts only the cloned Heads directory
(`$(pwd)`), so images must reside within the clone — use the `qemu_img/`
directory inside the repo as a backing store (see hardlink workflow below).
(`$(pwd)`), so images used by QEMU must live inside the clone. Backup
copies belong in `~/Qemu_img/` (same filesystem as the clone) and are
hardlinked back into `build/` when Docker needs them (see below).


2. Build Heads
Expand Down Expand Up @@ -98,25 +99,22 @@ Ex: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub
## Saving Disk Images from Build-Dir Wipes

**The Docker container can only see files inside the cloned Heads directory**
(`docker/common.sh` line 1446: `-v "$(pwd):$(pwd)"`). Any backup copy
must live at a path inside the clone — `~/QemuImages/` and other
user-home paths are invisible to Docker.
(`docker/common.sh` line 1446: `-v "$(pwd):$(pwd)"`). `~/Qemu_img/` is
invisible to Docker — files must be hardlinked back into `build/` before
Docker can use them.

**The build directory (`build/x86/<board>/`) is ephemeral.** A `make clean`
or fresh checkout deletes `build/` entirely, including installed OS images
and populated USB disks. Use hardlinks to keep safe copies inside the
clone and share across board variants:
and populated USB disks. Hardlink important files to `~/Qemu_img/`
(same filesystem as the clone) to keep them safe across wipes:

mkdir -p qemu_img # safe storage inside clone
cp build/x86/<board>/root.qcow2 qemu_img/ # copy OS install to safety
rm build/x86/<board>/root.qcow2 # remove build-tree copy
cp -alf qemu_img/root.qcow2 build/x86/<board>/ # hardlink back
# Now both paths point to the same data on disk.
# Wiping build/ won't touch qemu_img/.
cp -alf build/x86/<board>/root.qcow2 ~/Qemu_img/ # backup
cp -alf build/x86/<board>/usb_fd.raw ~/Qemu_img/ # backup

# Restore after a wipe:
cp -alf qemu_img/root.qcow2 build/x86/<board>/root.qcow2
cp -alf qemu_img/usb_fd.img build/x86/<board>/usb_fd.raw
After a wipe, restore from `~/Qemu_img/`:

cp -alf ~/Qemu_img/root.qcow2 build/x86/<board>/ # restore
cp -alf ~/Qemu_img/usb_fd.raw build/x86/<board>/ # restore

`cp -alf` creates a hardlink — a second directory entry pointing to the
same data blocks (zero additional space). Data is freed only when the
Expand All @@ -126,41 +124,97 @@ Use `qemu-img snapshot` before modifying the root disk.
### USB flash drive workflow

```bash
mkdir -p qemu_img # safe storage inside clone

# Step 1: Create the USB image via the Makefile.
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 \
QEMU_USB_SIZE=64G run
# → build/x86/.../usb_fd.raw now exists.

# Step 2: Save a master copy IMMEDIATELY (before population).
cp build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw qemu_img/usb_fd.img
rm build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw
cp -alf qemu_img/usb_fd.img build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw
cp -alf build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw ~/Qemu_img/

# Step 3: Populate with ISOs.
sudo losetup --find --show --partscan build/x86/.../usb_fd.raw
sudo mount /dev/loop0p1 /mnt
cp ~/Downloads/ISOs/*.iso /mnt/
sudo umount /mnt && sudo losetup -d /dev/loop0

# Step 4: Hardlink into other board build directories.
cp -alf qemu_img/usb_fd.img build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw
cp -alf qemu_img/usb_fd.img build/x86/qemu-coreboot-fbwhiptail-tpm2-hotp/usb_fd.raw
# Step 4: Restore after a wipe or hardlink into other board build dirs.
cp -alf ~/Qemu_img/usb_fd.raw build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw
cp -alf ~/Qemu_img/usb_fd.raw build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw

# Next run uses the hardlink — Makefile skips creation since the file exists.
```

### Daily development cycle

After OS install + USB provisioned, reference both from `./qemu_img/`:
Run the daily cycle directly against the `build/` images:

./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp \
PUBKEY_ASC=pubkey.asc \
USB_TOKEN=Nitrokey3NFC \
ROOT_DISK_IMG=./qemu_img/root.qcow2 \
inject_gpg run

After an OS install or any other state you want to keep, refresh the
master copies in `~/Qemu_img/` (same filesystem, zero-cost hardlinks)
so `make clean` won't destroy them:

cp -alf build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 ~/Qemu_img/
cp -alf build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw ~/Qemu_img/

If a rebuild wiped the `build/` images, restore them from the master
copies before running:

cp -alf ~/Qemu_img/root.qcow2 build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/
cp -alf ~/Qemu_img/usb_fd.raw build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/

### Testing GPG key reprovision from a backup drive

The GPG key reprovision flow ('k' in the GPG Management Menu, or the 'K'
option when signing fails) restores subkeys from a LUKS-encrypted backup
drive created during OEM factory reset onto a (new) OpenPGP smartcard.
This can be tested in QEMU.

First run OEM factory reset to populate the virtual USB drive with backup
material (this creates `build/x86/<board>/usb_fd.raw` with the LUKS private
+ exFAT public partition layout). Afterwards, save the virtual USB drive
and canokey state, then hardlink back for the second run:

```bash
# ~/Qemu_img is the home-directory store for QEMU images (outside the
# clone). It must be on the same partition/disk as the ~/heads build
# directory so cp -alf hardlinks work (hardlinks cannot cross filesystems).
mkdir -p ~/Qemu_img

# Save canokey state and populated USB backup image (hardlink, zero-cost).
cp -al build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/.canokey-file \
~/Qemu_img/.canokey-file
cp -al build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw \
~/Qemu_img/backup_drive.raw

# Hardlink back into the build dir so make run picks them up; make
# requires the raw image to live inside the local heads dir.
cp -alf ~/Qemu_img/.canokey-file \
build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/.canokey-file
cp -alf ~/Qemu_img/backup_drive.raw \
build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw

# Second run uses the preserved backup with no USB_FD_IMG override.
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp run
```

Inside the VM: Options -> GPG Options -> 'k' Reprovision USB Security dongle
from GPG key backup. Enter the backup passphrase (the Admin PIN you set
during OEM factory reset). The flow will:
- Detect the key type (RSA or ECC) from the backup
- Factory-reset the virtual canokey and set matching key attributes
- Import the master key and subkeys from the LUKS partition
- Move subkeys to the smartcard via keytocard
- Set the card identity (name, email) from the backup key's UID
- Reset the TPM and create a fresh rollback counter (TPM boards)
- Re-sign /boot so the next boot trusts the restored state
- Skip flashing the public key to ROM automatically (QEMU cannot reflash;
inject the exported pubkey into the firmware image instead)


Running via Docker wrappers
===
Expand Down Expand Up @@ -268,6 +322,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
Loading