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
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