Skip to content

Commit 3a7203e

Browse files
committed
initrd/gpg: add reprovision smartcard from GPG key backup flow
- Add gpg_card_factory_reset() shared function for factory-reset + forcesig + key-attr setting (RSA and ECC/P-256), extracted from oem-factory-reset.sh with explicit --passphrase-file support - Add gpg_keytocard_subkeys() shared function: enable USB, verify card, move subkeys to slots 1-3 (sign, encrypt, auth) - Add gpg_set_card_identity() shared function for cardholder name and login fields with explicit admin PIN - Add gpg_reset_nk3_secret_app() shared function for Nitrokey 3 Secrets app reset, parameterized on admin PIN - Replace oem-factory-reset.sh function bodies with thin wrappers that call the shared gpg_* functions via positional args - Add reprovision_smartcard_from_backup(): 10-phase flow that mounts the LUKS backup, detects key type from imported key, mounts public partition, factory-resets card, restores subkeys, sets identity, offers ROM flash - Add k option to GPG Management Menu (gpg-gui.sh), always visible - Add K option in prompt_missing_gpg_key_action (gui-init.sh) - Add clean boot wizard offering OEM reset, reprovision, or ignore - Update doc/gpg.md, doc/configuring-keys.md, doc/architecture.md for the new recovery path - Document QEMU testing workflow and hardlink preservation pattern in doc/qemu.md (USB flash drive workflow + reprovision test) - Add Authentication and Resetting Configuration sections to doc/recovery-shell.md Extends PR #1515 (2023): authentication via gpg_auth can now be enforced with key material restored from a backup, allowing recovery shell access control even after dongle replacement. Tested on Nitrokey 3: OEM factory reset creates LUKS backup + provisions dongle successfully. Tested on Nitrokey Pro 2: reprovision from backup restores subkeys to (new) dongle, PIN changes, public key import, ROM flash, and gpg_auth works after reboot. Config wipe from Options menu correctly triggers clean boot wizard with OEM/reprovision/ignore choice. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
1 parent 8d0064f commit 3a7203e

9 files changed

Lines changed: 907 additions & 282 deletions

File tree

doc/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ The CI pipeline's workspace and cache behavior is documented in
132132

133133
- **No network at boot** — all verification is local; no certificate authorities
134134
- **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.
135-
- **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.
135+
- **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.
136136
- **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.
137137
- **Auditability** — all source is open, builds are reproducible, ROM images are verifiable
138138

doc/configuring-keys.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,23 @@ If you already have a provisioned USB Security dongle:
160160
3. Follow the steps. After reflashing, reboot.
161161
4. Generate a new TOTP/HOTP secret when prompted.
162162

163+
### Restoring Keys from Backup
164+
165+
If you chose the in-memory backup path during OEM factory reset and your
166+
dongle is lost, broken, or wiped:
167+
168+
1. Insert the backup USB thumb drive and your (new) dongle.
169+
2. Go to `Options -> GPG Management -> Reprovision smartcard from GPG key backup`.
170+
3. Enter the backup passphrase when prompted.
171+
4. Heads detects the key type from the backup, factory-resets the dongle,
172+
restores the subkeys, and sets the card identity.
173+
5. After success, flash the public key to the running BIOS for persistence.
174+
6. Re-sign /boot via `Options -> Update checksums and sign all files in /boot`.
175+
176+
This requires the LUKS-encrypted backup USB drive created during OEM factory
177+
reset (answer Y to "format an encrypted USB Thumb drive"). Without it, run
178+
a new OEM Factory Reset / Re-Ownership to rekey the device.
179+
163180
## Forgotten GPG User PIN
164181

165182
From Recovery Shell with the dongle inserted:

doc/gpg.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,18 @@ public key into the Heads firmware:
130130
3. From Heads: `Options -> GPG Management -> Add a GPG key to the running BIOS + reflash`.
131131
4. Reboot. Generate a new TOTP/HOTP secret when prompted.
132132

133+
### Restoring Keys from Backup
134+
135+
See [configuring-keys.md](configuring-keys.md#restoring-keys-from-backup) for
136+
the full recovery flow. In short:
137+
138+
1. Insert the backup USB thumb drive and the replacement dongle.
139+
2. From Heads: `Options -> GPG Management -> Reprovision smartcard from GPG key backup`.
140+
3. Enter the backup passphrase.
141+
142+
The backup drive must have been created by the OEM factory reset flow with
143+
the "format an encrypted USB Thumb drive" option set to Y.
144+
133145
## Nitrokey 3 Specifics
134146

135147
- Supports NIST P-256 ECC keys in addition to RSA — significantly faster key

doc/qemu.md

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,27 +126,32 @@ Use `qemu-img snapshot` before modifying the root disk.
126126
### USB flash drive workflow
127127

128128
```bash
129-
mkdir -p qemu_img # safe storage inside clone
129+
mkdir -p ~/Qemu_img # persistent storage outside repo
130+
# (same filesystem as repo;
131+
# hardlinks only work within
132+
# a single partition)
130133

131134
# Step 1: Create the USB image via the Makefile.
132135
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 \
133136
QEMU_USB_SIZE=64G run
134-
# build/x86/.../usb_fd.raw now exists.
137+
# -> build/x86/.../usb_fd.raw now exists.
135138

136-
# Step 2: Save a master copy IMMEDIATELY (before population).
137-
cp build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw qemu_img/usb_fd.img
138-
rm build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw
139-
cp -alf qemu_img/usb_fd.img build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw
139+
# Step 2: Save a master copy (hardlink, zero-cost, survives make clean).
140+
cp -al build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw \
141+
~/Qemu_img/usb_fd.img
140142

141143
# Step 3: Populate with ISOs.
142-
sudo losetup --find --show --partscan build/x86/.../usb_fd.raw
144+
sudo losetup --find --show --partscan \
145+
build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw
143146
sudo mount /dev/loop0p1 /mnt
144147
cp ~/Downloads/ISOs/*.iso /mnt/
145148
sudo umount /mnt && sudo losetup -d /dev/loop0
146149

147-
# Step 4: Hardlink into other board build directories.
148-
cp -alf qemu_img/usb_fd.img build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw
149-
cp -alf qemu_img/usb_fd.img build/x86/qemu-coreboot-fbwhiptail-tpm2-hotp/usb_fd.raw
150+
# Step 4: Hardlink into other board build directories (zero-cost).
151+
cp -alf ~/Qemu_img/usb_fd.img \
152+
build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw
153+
cp -alf ~/Qemu_img/usb_fd.img \
154+
build/x86/qemu-coreboot-fbwhiptail-tpm2-hotp/usb_fd.raw
150155

151156
# Next run uses the hardlink — Makefile skips creation since the file exists.
152157
```
@@ -162,6 +167,50 @@ After OS install + USB provisioned, reference both from `./qemu_img/`:
162167
inject_gpg run
163168

164169

170+
### Testing GPG key reprovision from a backup drive
171+
172+
The GPG key reprovision flow ('k' in the GPG Management Menu, or the 'K'
173+
option when signing fails) restores subkeys from a LUKS-encrypted backup
174+
drive created during OEM factory reset onto a (new) OpenPGP smartcard.
175+
This can be tested in QEMU.
176+
177+
First run OEM factory reset to populate the virtual USB drive with backup
178+
material (this creates `build/x86/<board>/usb_fd.raw` with the LUKS private
179+
+ exFAT public partition layout). Afterwards, save the virtual USB drive
180+
and canokey state, then hardlink back for the second run:
181+
182+
```bash
183+
# ~/Qemu_img/ stores files OUTSIDE the repo so they survive make clean.
184+
# Hardlinks work because ~/heads/ and ~/Qemu_img/ share the same partition.
185+
mkdir -p ~/Qemu_img
186+
187+
# Save canokey state and populated USB backup image (hardlink, zero-cost).
188+
cp -al build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/.canokey-file \
189+
~/Qemu_img/.canokey-file
190+
cp -al build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw \
191+
~/Qemu_img/backup_drive.raw
192+
193+
# Hardlink back into the build dir so make run picks them up.
194+
cp -alf ~/Qemu_img/.canokey-file \
195+
build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/.canokey-file
196+
cp -alf ~/Qemu_img/backup_drive.raw \
197+
build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw
198+
199+
# Second run uses the preserved backup with no USB_FD_IMG override.
200+
./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp run
201+
```
202+
203+
Inside the VM: Options -> GPG Options -> 'k' Reprovision smartcard from GPG
204+
key backup. Enter the backup passphrase (the Admin PIN you set during OEM
205+
factory reset). The flow will:
206+
- Detect the key type (RSA or ECC) from the backup
207+
- Factory-reset the virtual canokey and set matching key attributes
208+
- Import the master key and subkeys from the LUKS partition
209+
- Move subkeys to the smartcard via keytocard
210+
- Set the card identity (name, email) from the backup key's UID
211+
- Offer to flash the public key to ROM (decline -- QEMU cannot reflash)
212+
213+
165214
Running via Docker wrappers
166215
===
167216
We provide convenient wrapper scripts at the repository root that encapsulate Docker invocation and automatically handle common host integrations needed for QEMU runs.
@@ -188,7 +237,7 @@ environment reference. Important ones are `HEADS_DISABLE_USB`
188237

189238
Make variables such as `USB_TOKEN`, `PUBKEY_ASC`, `INSTALL_IMG`,
190239
`QEMU_MEMORY_SIZE`, `QEMU_DISK_SIZE`, `QEMU_USB_SIZE`,
191-
`ROOT_DISK_IMG`, `CPUS` and `V`
240+
`ROOT_DISK_IMG`, `USB_FD_IMG`, `CPUS` and `V`
192241
are forwarded to the `make` invocation and affect how
193242
`targets/qemu.mk` runs QEMU. See `targets/qemu.mk` for token formats
194243
and examples.

doc/recovery-shell.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,57 @@ The Recovery Shell boots with PCR 4 set to `recovery` instead of
2020
- TOTP/HOTP sealing and TPM Disk Unlock Key creation/unsealing do not work.
2121
- To perform seal/unseal operations return to the normal GUI boot.
2222

23+
## Authentication
24+
25+
When `CONFIG_HAVE_GPG_KEY_BACKUP=y` (set during OEM Factory Reset / Re-Ownership
26+
with the in-memory backup path, or by running the reprovision flow from the GPG
27+
Management Menu), the recovery shell requires GPG smartcard authentication before
28+
the bash prompt opens.
29+
30+
**Scope:** This ONLY guards recovery shell entry. USB boot, TPM operations,
31+
flash/update, GPG management, and all other GUI menu functions are NOT gated
32+
by this check — they remain accessible from the main menu.
33+
34+
**What it guards against:** Recovery shell access gives full bash within the
35+
initrd, including direct block device read/write, SPI flash access, TPM
36+
commands, and GPG key operations. An unauthenticated physical attacker with
37+
shell access could flash malicious firmware, delete `/boot` content, lock TPM
38+
PIN counters permanently, sign unauthorized `/boot` content, or attempt
39+
LUKS disk decryption.
40+
41+
**How it works:** `gpg_auth()` in `initrd/etc/functions.sh` generates a random
42+
nonce, the user signs it with their GPG key (smartcard or backup USB drive)
43+
within 3 attempts, and the signature is verified against the ROM-fused public
44+
keyring. On failure, `DIE` exits the session. With `CONFIG_RESTRICTED_BOOT=y`,
45+
the shell is blocked entirely and the system reboots after 5 seconds.
46+
47+
**Without authentication:** If `CONFIG_HAVE_GPG_KEY_BACKUP` is not set,
48+
`gpg_auth()` is a no-op and the recovery shell opens without prompting.
49+
50+
## Resetting Configuration
51+
52+
`Options -> Change configuration settings -> 'r'` (`Clear GPG key(s) and reset
53+
all user settings`) wipes the running system configuration:
54+
55+
- Clears `~/.gnupg` (GPG keyring, trustdb)
56+
- Deletes `/boot/kexec*` (signatures, checksums, boot options)
57+
- Removes all `heads/` files from CBFS (keyring, trustdb, config.user)
58+
- Reflashes the cleaned firmware
59+
- Resets the TPM if present
60+
61+
**Attestation impact:** Removing `heads/` files from CBFS changes the SPI flash
62+
contents measured into PCR 7 by `cbfs-init` at next boot. Combined with the
63+
TPM reset, TOTP/HOTP unseal will fail on the next boot — Heads shows the
64+
standard red-menu TOTP error prompt.
65+
66+
**Recovery after wipe:** Run OEM Factory Reset / Re-Ownership (Options -> 'F')
67+
to fully reprovision ([configuring-keys.md](configuring-keys.md)). If you have a
68+
GPG key backup USB drive from a previous in-memory OEM reset, use
69+
`GPG Options -> 'k' Reprovision USB Security dongle from GPG key backup`
70+
to restore subkeys from the backup ([configuring-keys.md#restoring-keys-from-backup](configuring-keys.md#restoring-keys-from-backup)),
71+
then flash the public key to ROM, re-sign /boot, and generate new TOTP/HOTP
72+
secrets.
73+
2374
## Common Operations
2475

2576
### Manual boot

initrd/bin/gpg-gui.sh

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,20 @@ TRACE_FUNC
1010

1111
while true; do
1212
unset menu_choice
13+
# Build menu options dynamically
14+
menu_options=(
15+
'r' ' Add GPG key to running BIOS and reflash'
16+
'a' ' Add GPG key to standalone BIOS image and flash'
17+
'e' ' Replace GPG key(s) in the current ROM and reflash'
18+
'l' ' List GPG keys in your keyring'
19+
'p' ' Export public GPG key to USB drive'
20+
'g' ' Generate GPG keys manually on a USB security dongle'
21+
'k' ' Reprovision USB Security dongle from GPG key backup'
22+
)
23+
menu_options+=('x' ' Exit')
1324
whiptail_type $BG_COLOR_MAIN_MENU --title "GPG Management Menu" \
1425
--menu 'Select the GPG function to perform' 0 80 10 \
15-
'r' ' Add GPG key to running BIOS and reflash' \
16-
'a' ' Add GPG key to standalone BIOS image and flash' \
17-
'e' ' Replace GPG key(s) in the current ROM and reflash' \
18-
'l' ' List GPG keys in your keyring' \
19-
'p' ' Export public GPG key to USB drive' \
20-
'g' ' Generate GPG keys manually on a USB security dongle' \
21-
'x' ' Exit' \
26+
"${menu_options[@]}" \
2227
2>/tmp/whiptail || recovery "GUI menu failed"
2328

2429
menu_choice=$(cat /tmp/whiptail)
@@ -64,6 +69,9 @@ while true; do
6469
gpg_post_gen_mgmt
6570
fi
6671
;;
72+
"k")
73+
reprovision_smartcard_from_backup
74+
;;
6775
esac
6876

6977
done

initrd/bin/gui-init.sh

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,18 @@ prompt_missing_gpg_key_action() {
241241
retry_label=" Retry (after connecting $DONGLE_BRAND)"
242242
retry_msg="Cannot sign /boot because no private GPG signing key is available ($DONGLE_BRAND not inserted, wiped, or key not set up).\n\nInsert your $DONGLE_BRAND and retry.\n\nHow would you like to proceed?"
243243
fi
244+
menu_options=(
245+
'r' "$retry_label"
246+
'F' ' OEM Factory Reset / Re-Ownership'
247+
'K' ' Reprovision USB Security dongle from GPG key backup'
248+
)
249+
menu_options+=(
250+
'm' ' Return to main menu'
251+
'x' ' Exit to recovery shell'
252+
)
244253
whiptail_error --title "ERROR: GPG signing key unavailable" \
245-
--menu "$retry_msg" 0 80 4 \
246-
'r' "$retry_label" \
247-
'F' ' OEM Factory Reset / Re-Ownership' \
248-
'm' ' Return to main menu' \
249-
'x' ' Exit to recovery shell' \
254+
--menu "$retry_msg" 0 80 6 \
255+
"${menu_options[@]}" \
250256
2>/tmp/whiptail || recovery "GUI menu failed"
251257

252258
option=$(cat /tmp/whiptail)
@@ -257,6 +263,9 @@ prompt_missing_gpg_key_action() {
257263
F)
258264
oem-factory-reset.sh
259265
;;
266+
K)
267+
reprovision_smartcard_from_backup
268+
;;
260269
x)
261270
recovery "User requested recovery shell"
262271
;;
@@ -543,9 +552,26 @@ clean_boot_check() {
543552
fi
544553

545554
# OS is installed, no kexec files present, no GPG keys in keyring, security token present
546-
# prompt user to run OEM factory reset
547-
oem-factory-reset.sh \
548-
"Clean Boot Detected - Perform OEM Factory Reset / Re-Ownership?"
555+
# offer to provision or reprovision
556+
if ! whiptail_warning --title "Heads setup wizard" \
557+
--menu "No GPG keys, no /boot signatures, and a USB Security dongle\ndetected. /boot has an installed OS.\n\nHow would you like to proceed?" 0 80 5 \
558+
'F' ' OEM Factory Reset / Re-Ownership (full provisioning)' \
559+
'K' ' Reprovision USB Security dongle from GPG key backup' \
560+
'i' ' Ignore and continue to main menu' \
561+
2>/tmp/whiptail; then
562+
return
563+
fi
564+
565+
option=$(cat /tmp/whiptail)
566+
case "$option" in
567+
F)
568+
oem-factory-reset.sh \
569+
"Clean Boot Detected - Perform OEM Factory Reset / Re-Ownership?"
570+
;;
571+
K)
572+
reprovision_smartcard_from_backup
573+
;;
574+
esac
549575
}
550576

551577
check_gpg_key() {
@@ -558,27 +584,35 @@ check_gpg_key() {
558584
fi
559585
local gpg_error_msg
560586
gpg_error_msg="ERROR: $CONFIG_BRAND_NAME couldn't find any GPG keys in your keyring.\n\nIf this is the first time the system has booted, you should add a public GPG key to the BIOS now.\n\nIf you just reflashed a new BIOS, you'll need to add at least one public key to the keyring.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?"
587+
menu_options=(
588+
'g' ' Add a GPG key to the running BIOS'
589+
'F' ' OEM Factory Reset / Re-Ownership'
590+
'K' ' Reprovision USB Security dongle from GPG key backup'
591+
)
592+
menu_options+=(
593+
'i' ' Ignore error and continue to main menu'
594+
'x' ' Exit to recovery shell'
595+
)
561596
whiptail_error --title "ERROR: GPG keyring empty!" \
562-
--menu "$gpg_error_msg" 0 80 4 \
563-
'g' ' Add a GPG key to the running BIOS' \
564-
'F' ' OEM Factory Reset / Re-Ownership' \
565-
'i' ' Ignore error and continue to main menu' \
566-
'x' ' Exit to recovery shell' \
597+
--menu "$gpg_error_msg" 0 80 6 \
598+
"${menu_options[@]}" \
567599
2>/tmp/whiptail || recovery "GUI menu failed"
568600

569601
option=$(cat /tmp/whiptail)
570602
case "$option" in
571603
g)
572604
gpg-gui.sh && BG_COLOR_MAIN_MENU="normal"
573605
;;
606+
F)
607+
oem-factory-reset.sh
608+
;;
609+
K)
610+
reprovision_smartcard_from_backup
611+
;;
574612
i)
575613
skip_to_menu="true"
576614
return 1
577615
;;
578-
F)
579-
oem-factory-reset.sh
580-
;;
581-
582616
x)
583617
recovery "User requested recovery shell"
584618
;;

0 commit comments

Comments
 (0)