From cf136701629f4136625171717b9a663dfa946daf Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 17 Jul 2026 14:06:11 -0400 Subject: [PATCH 01/10] initrd/gpg: refactor OEM card management into shared functions for reuse, add reprovision flow Extract shared OpenPGP smartcard management functions from initrd/bin/oem-factory-reset.sh into initrd/etc/gpg_functions.sh so they can be reused by multiple callers. OEM functions become thin wrappers passing global variables as positional args; the reprovision flow calls the same shared functions to restore GPG subkeys from a LUKS-encrypted backup USB onto a replacement USB Security dongle. Shared functions (replacing OEM inline code): - gpg_card_factory_reset(): factory-reset + forcesig + key-attr (RSA and ECC/P-256), parameterized on admin PIN, with --expert on RSA key-attr for key sizes above 2048 bit - gpg_keytocard_subkeys(): enable USB, verify card, move subkeys to slots 1-3 (sign, encrypt, auth) - gpg_set_card_identity(): cardholder name and login fields - gpg_reset_nk3_secret_app(): Nitrokey 3 Secrets app reset - gpg_card_change_pin(): GPG User/Admin PIN change - _luks_cleanup(): unmount /media and close LUKS mappings New reprovision flow (reprovision_smartcard_from_backup()): 10-phase flow: mount LUKS backup -> detect key type -> factory- reset card -> import subkeys -> set identity -> sign /boot -> flash GUI integration: - k option in GPG Management Menu (gpg-gui.sh), always visible - K option in prompt_missing_gpg_key_action (gui-init.sh) - Clean boot wizard: OEM reset, reprovision, or ignore Code quality fixes: - Add --pass-file option to mount-usb.sh (reads file into $PASS) - Reset card_admin_pin to default 12345678 after factory-reset - Wait for gpg card after USB storage init (release_scdaemon) - Validate backup passphrase is non-empty before proceeding - Use glob-based param_files ordering matching check_config (fixes BAD signature regression where manual ordering differed) - Track tpm_counter_ok flag and warn if TPM counter not created Documentation: - doc/recovery-shell.md: Authentication section (gpg_auth scope, what it prevents and does not prevent, SPI flash dump threat, USB boot guard), Resetting Configuration section - doc/configuring-keys.md: Restoring Keys from Backup section - doc/gpg.md: Restoring Keys from Backup section (cross-ref) - doc/architecture.md: reprovision option in CONFIG_HAVE description - doc/qemu.md: Resetting state section for QEMU testing Tested on QEMU with canokey-qemu (virtual Canokey): OEM factory reset creates LUKS backup, reprovision restores subkeys to replacement dongle, /boot signed and verified, gpg_auth works after reboot. Signed-off-by: Thierry Laurion --- doc/architecture.md | 2 +- doc/configuring-keys.md | 20 + doc/gpg.md | 12 + doc/qemu.md | 25 ++ doc/recovery-shell.md | 105 +++++ initrd/bin/gpg-gui.sh | 6 +- initrd/bin/gui-init.sh | 6 +- initrd/bin/mount-usb.sh | 12 +- initrd/bin/oem-factory-reset.sh | 128 +----- initrd/etc/gpg_functions.sh | 674 ++++++++++++++++++++++++++++++-- 10 files changed, 833 insertions(+), 157 deletions(-) diff --git a/doc/architecture.md b/doc/architecture.md index 98d871917..b34a27fa8 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -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 diff --git a/doc/configuring-keys.md b/doc/configuring-keys.md index df2dcab9e..36070bfe0 100644 --- a/doc/configuring-keys.md +++ b/doc/configuring-keys.md @@ -160,6 +160,26 @@ 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. + +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: diff --git a/doc/gpg.md b/doc/gpg.md index 6de0e55b4..13db87601 100644 --- a/doc/gpg.md +++ b/doc/gpg.md @@ -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 diff --git a/doc/qemu.md b/doc/qemu.md index bed602f7d..37fdb60aa 100644 --- a/doc/qemu.md +++ b/doc/qemu.md @@ -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-:...` 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//.canokey-file + +# Wipe the virtual TPM (new TPM, no sealed secrets or counters). +sudo rm -rf build/x86//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//.canokey-file ~/Qemu_img/.canokey-file.bak +cp ~/Qemu_img/.canokey-file.bak build/x86//.canokey-file +``` + Troubleshooting --- diff --git a/doc/recovery-shell.md b/doc/recovery-shell.md index cab3d690b..9c647280f 100644 --- a/doc/recovery-shell.md +++ b/doc/recovery-shell.md @@ -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 diff --git a/initrd/bin/gpg-gui.sh b/initrd/bin/gpg-gui.sh index b6e662679..972139c7e 100755 --- a/initrd/bin/gpg-gui.sh +++ b/initrd/bin/gpg-gui.sh @@ -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" @@ -64,6 +65,9 @@ while true; do gpg_post_gen_mgmt fi ;; + "k") + reprovision_smartcard_from_backup + ;; esac done diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index ce03d953b..6fed923d6 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -242,9 +242,10 @@ prompt_missing_gpg_key_action() { 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?" fi whiptail_error --title "ERROR: GPG signing key unavailable" \ - --menu "$retry_msg" 0 80 4 \ + --menu "$retry_msg" 0 80 5 \ 'r' "$retry_label" \ 'F' ' OEM Factory Reset / Re-Ownership' \ + 'K' ' Reprovision USB Security dongle from GPG key backup' \ 'm' ' Return to main menu' \ 'x' ' Exit to recovery shell' \ 2>/tmp/whiptail || recovery "GUI menu failed" @@ -257,6 +258,9 @@ prompt_missing_gpg_key_action() { F) oem-factory-reset.sh ;; + K) + reprovision_smartcard_from_backup + ;; x) recovery "User requested recovery shell" ;; diff --git a/initrd/bin/mount-usb.sh b/initrd/bin/mount-usb.sh index a22033108..edb4e59ff 100755 --- a/initrd/bin/mount-usb.sh +++ b/initrd/bin/mount-usb.sh @@ -8,14 +8,15 @@ TRACE_FUNC function usage() { cat < <--device device> <--mountpoint mountpoint> <--pass passphrase> +usage: $0 [options...] <--mode [ro|rw]> <--device device> <--mountpoint mountpoint> [--pass passphrase|--pass-file /path/to/file] $0 --help - + parameters: --mode: ro or rw (default ro) --device: device to mount (default: first USB device found) --mountpoint: where to mount the device (default: /media) --pass: passphrase for LUKS device (default: none) + --pass-file: path to file containing passphrase --help: Show this help USAGE_END } @@ -56,6 +57,13 @@ while [ $# -gt 0 ]; do shift fi ;; + --pass-file) + if [ -n "$2" ] && [ -r "$2" ]; then + PASS="$(cat "$2")" + shift + shift + fi + ;; *) usage exit 1 diff --git a/initrd/bin/oem-factory-reset.sh b/initrd/bin/oem-factory-reset.sh index 89d86aae6..c32bc71e7 100755 --- a/initrd/bin/oem-factory-reset.sh +++ b/initrd/bin/oem-factory-reset.sh @@ -172,27 +172,8 @@ mount_boot() { reset_nk3_secret_app() { TRACE_FUNC - - # Reset Nitrokey 3 Secrets app PIN with $ADMIN_PIN (default 12345678, or customised) - if [ "$DONGLE_BRAND" = "Nitrokey 3" ] && [ -x /bin/hotp_verification ]; then - STATUS "Resetting Nitrokey 3 Secrets app (physical touch will be required)" - # TODO: change message when https://github.com/Nitrokey/nitrokey-hotp-verification/issues/41 is fixed - # Reset Nitrokey 3 secret app with PIN - # Do 3 attempts to reset Nitrokey 3 Secrets app if return code is 3 (no touch) - for attempt in 1 2 3; do - if hotp_verification reset "${ADMIN_PIN}"; then - STATUS_OK "Nitrokey 3 Secrets app reset" - return 0 - else - error_code=$? - if [ $error_code -eq 3 ] && [ $attempt -lt 3 ]; then - whiptail_warning --msgbox "$DONGLE_BRAND requires physical presence: touch the dongle when requested" $HEIGHT $WIDTH --title "$DONGLE_BRAND secrets app reset attempt: $attempt/3" - else - whiptail_error_die "Nitrokey 3's Secrets app reset failed with error:$error_code. Contact Nitrokey support" - fi - fi - done - fi + gpg_reset_nk3_secret_app "$ADMIN_PIN" || \ + whiptail_error_die "Nitrokey 3's Secrets app reset failed with error $?. Contact Nitrokey support" } #Generate a gpg master key: no expiration date, ${RSA_KEY_LENGTH} bits @@ -383,45 +364,11 @@ generate_inmemory_p256_master_and_subkeys() { # Delete the master key from the keyring once key to card is done (already backed up on LUKS private partition) keytocard_subkeys_to_smartcard() { TRACE_FUNC - - #make sure usb ready and USB Security dongle ready to communicate with enable_usb - enable_usb_storage - STATUS "Accessing $DONGLE_BRAND OpenPGP smartcard" - gpg --card-status >/dev/null 2>&1 || DIE "Error getting GPG card status" - - gpg_key_factory_reset - - STATUS "Moving subkeys to $DONGLE_BRAND" - { - echo "key 1" #Toggle on Signature key in --edit-key mode on local keyring - echo "keytocard" #Move Signature key to smartcard - echo "1" #Select Signature key key slot on smartcard - echo "${ADMIN_PIN}" #Local keyring Subkey PIN - echo "${ADMIN_PIN_DEF}" #Smartcard Admin PIN (prompted once; scdaemon caches it for subsequent keytocard ops) - echo "key 1" #Toggle off Signature key - echo "key 2" #Toggle on Encryption key - echo "keytocard" #Move Encryption key to smartcard - echo "2" #Select Encryption key key slot on smartcard - echo "${ADMIN_PIN}" #Local keyring Subkey PIN (card PIN already cached by scdaemon) - echo "key 2" #Toggle off Encryption key - echo "key 3" #Toggle on Authentication key - echo "keytocard" #Move Authentication key to smartcard - echo "3" #Select Authentication key slot on smartcard - echo "${ADMIN_PIN}" #Local keyring Subkey PIN (card PIN still cached by scdaemon) - echo "key 3" #Toggle off Authentication key - echo "save" #Save changes and commit to keyring - } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key "${GPG_USER_MAIL}" \ - >/tmp/gpg_card_edit_output 2>&1 - TRACE_FUNC - DEBUG "GPG keytocard output: $(cat /tmp/gpg_card_edit_output)" - if [ $? -ne 0 ]; then - ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "GPG Key moving subkeys to smartcard failed!\n\n$ERROR" - fi - STATUS_OK "Subkeys moved to smartcard" - - TRACE_FUNC + gpg_card_factory_reset "$GPG_ALGO" "$RSA_KEY_LENGTH" "$ADMIN_PIN_DEF" || \ + whiptail_error_die "Factory resetting OpenPGP smartcard failed" + gpg_keytocard_subkeys "$GPG_USER_MAIL" "$ADMIN_PIN" "$ADMIN_PIN_DEF" || \ + whiptail_error_die "GPG Key moving subkeys to smartcard failed" } #Whiptail prompt to insert to be wiped thumb drive @@ -435,69 +382,8 @@ prompt_insert_to_be_wiped_thumb_drive() { set_card_identity() { TRACE_FUNC - - # Determine which fields we have custom values for - local set_name=0 set_login=0 - local surname given - - # Name: skip if still the OEM default - if [ "$GPG_USER_NAME" != "OEM Key" ] && [ -n "$GPG_USER_NAME" ]; then - set_name=1 - # OpenPGP card stores surname and given name separately; - # gpg displays them as "given surname" - if [[ "$GPG_USER_NAME" == *" "* ]]; then - given="${GPG_USER_NAME% *}" - surname="${GPG_USER_NAME##* }" - else - surname="$GPG_USER_NAME" - given="" - fi - DEBUG "Will set cardholder name: surname='$surname' given='$given'" - else - DEBUG "Skipping cardholder name: no custom name set" - fi - - # Login: skip if still the auto-generated OEM default (oem-*@example.com) - if [ -n "$GPG_USER_MAIL" ] && [[ "$GPG_USER_MAIL" != oem-*@example.com ]]; then - set_login=1 - DEBUG "Will set login data: '$GPG_USER_MAIL'" - else - DEBUG "Skipping login data: no custom email set" - fi - - [ "$set_name" -eq 0 ] && [ "$set_login" -eq 0 ] && return - - STATUS "Setting identity fields on OpenPGP smartcard" - { - echo "admin" - if [ "$set_name" -eq 1 ]; then - echo "name" - echo "${surname}" - echo "${given}" - # scdaemon caches the admin PIN from the preceding keytocard/generate - # session; name and login do not re-prompt for it - fi - if [ "$set_login" -eq 1 ]; then - echo "login" - echo "${GPG_USER_MAIL}" - # scdaemon admin PIN still cached; no re-prompt needed - fi - echo "quit" - } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit || + gpg_set_card_identity "$GPG_USER_NAME" "$GPG_USER_MAIL" "$ADMIN_PIN_DEF" || \ DIE "Failed to set identity fields on OpenPGP smartcard" - - local summary="" - [ "$set_name" -eq 1 ] && summary="${given:+$given }${surname}" - [ "$set_login" -eq 1 ] && summary="${summary:+$summary, }${GPG_USER_MAIL}" - STATUS_OK "Card identity set: $summary" - #TODO: set card `url` field and GPG key preferred keyserver after uploading to keys.openpgp.org - # Two separate operations needed: - # 1. card `url` — set via gpg --card-edit admin → url → - # 2. key `keyserver` preference — set via gpg --edit-key → keyserver → → save - # (applies to both on-card and in-memory key paths) - # Requires: network access in initrd, curl, and user email verification on keyserver. - # Note: keys.openpgp.org hides UID until owner verifies email — upload works but key - # is not searchable by email until verified from a normal OS session after provisioning. } #export master key and subkeys to thumbdrive's private LUKS contained partition diff --git a/initrd/etc/gpg_functions.sh b/initrd/etc/gpg_functions.sh index 885a2b603..de3a9c25e 100644 --- a/initrd/etc/gpg_functions.sh +++ b/initrd/etc/gpg_functions.sh @@ -101,50 +101,662 @@ gpg_add_key_reflash() { if [ ! -s /tmp/gpg-gui.rom ]; then whiptail_error --title 'ERROR: BIOS Read Failed!' \ --msgbox "Unable to read BIOS" 0 80 - return 1 + exit 1 fi + gpg_flash_rom + fi + umount /media + fi +} - if (whiptail --title 'Update ROM?' \ - --yesno "This will reflash your BIOS with the updated version\n\nDo you want to proceed?" 0 80); then - gpg_flash_rom +gpg_replace_key_reflash() { + [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg + [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx + [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg + gpg_add_key_reflash +} + +# --- Reprovision flow shared functions --- + +gpg_reset_nk3_secret_app() { + # Reset Nitrokey 3 Secrets app PIN. + # $1: admin PIN (default 12345678 or user-chosen) + TRACE_FUNC + local admin_pin="$1" + local error_code + if [ "$DONGLE_BRAND" = "Nitrokey 3" ] && [ -x /bin/hotp_verification ]; then + STATUS "Resetting Nitrokey 3 Secrets app (physical touch will be required)" + for attempt in 1 2 3; do + if hotp_verification reset "${admin_pin}"; then + STATUS_OK "Nitrokey 3 Secrets app reset" + return 0 + else + error_code=$? + if [ $error_code -eq 3 ] && [ $attempt -lt 3 ]; then + whiptail_warning --msgbox "$DONGLE_BRAND requires physical presence: touch the dongle when requested" 0 80 --title "$DONGLE_BRAND secrets app reset attempt: $attempt/3" + else + DEBUG "NK3 Secrets app reset failed with error $error_code" + return $error_code + fi fi + done + fi + return 0 +} + +gpg_card_factory_reset() { + # Factory-reset card, set key attributes. + # $1: algo (RSA or p256) + # $2: rsa_key_length (bits, for RSA only) + # $3: card_admin_pin (default 12345678) + TRACE_FUNC + local algo="$1" + local rsa_key_length="$2" + local card_admin_pin="${3:-12345678}" + local rc + + STATUS "Factory resetting $DONGLE_BRAND OpenPGP smartcard" + { + echo admin # admin menu + echo factory-reset # factory reset smartcard + echo y # confirm + echo yes # confirm + } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback \ + --passphrase-fd 3 3< <(echo -n "$card_admin_pin") --card-edit \ + >/tmp/gpg_card_edit_output 2>&1 + rc=$? + TRACE_FUNC + DEBUG "GPG factory-reset output: $(cat /tmp/gpg_card_edit_output)" + if [ $rc -ne 0 ]; then + return 1 + fi + + # After factory reset the card admin PIN is back to default 12345678 + card_admin_pin="12345678" + + if [ "$DONGLE_BRAND" = "Nitrokey Storage" ] && [ -x /bin/hotp_verification ]; then + STATUS "Resetting Nitrokey Storage AES keys" + hotp_verification regenerate "${card_admin_pin}" + STATUS_OK "Nitrokey Storage AES keys reset" + fi + + STATUS_OK "OpenPGP smartcard factory reset" + + if gpg --card-status | grep "Signature PIN" | grep -q "not forced"; then + STATUS "Enabling forced signature PIN on smartcard" + { + echo admin + echo forcesig + echo "${card_admin_pin}" + } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ + >/tmp/gpg_card_edit_output 2>&1 + rc=$? + TRACE_FUNC + DEBUG "GPG forcesig toggle output: $(cat /tmp/gpg_card_edit_output)" + if [ $rc -ne 0 ]; then + WARN "Could not enable forced signature PIN; continuing anyway" + else + STATUS_OK "Forced signature PIN enabled" fi fi - return 1 + + if [ "$algo" = "p256" ]; then + STATUS "Setting NIST P-256 key attributes on $DONGLE_BRAND" + { + echo admin + echo key-attr + echo 2 + echo 3 + echo "${card_admin_pin}" + echo 2 + echo 3 + echo "${card_admin_pin}" + echo 2 + echo 3 + echo "${card_admin_pin}" + } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ + >/tmp/gpg_card_edit_output 2>&1 + rc=$? + TRACE_FUNC + DEBUG "GPG p256 key-attr output: $(cat /tmp/gpg_card_edit_output)" + if [ $rc -ne 0 ]; then + return 1 + fi + STATUS_OK "NIST P-256 key attributes set" + elif [ "$algo" = "RSA" ]; then + STATUS "Setting RSA ${rsa_key_length}-bit key attributes on $DONGLE_BRAND" + { + echo admin + echo key-attr + echo 1 # RSA + echo "${rsa_key_length}" + echo "${card_admin_pin}" + echo 1 # RSA + echo "${rsa_key_length}" + echo "${card_admin_pin}" + echo 1 # RSA + echo "${rsa_key_length}" + echo "${card_admin_pin}" + } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ + >/tmp/gpg_card_edit_output 2>&1 + rc=$? + TRACE_FUNC + DEBUG "GPG RSA key-attr output: $(cat /tmp/gpg_card_edit_output)" + if [ $rc -ne 0 ]; then + return 1 + fi + STATUS_OK "RSA ${rsa_key_length}-bit key attributes set" + else + DIE "Unknown GPG algorithm: $algo" + fi } -gpg_add_key_to_standalone_rom() { - if (whiptail --title 'ROM and GPG public key required' \ - --yesno "This requires you insert a USB drive containing:\n* Your GPG public key (*.key or *.asc)\n* Your BIOS image (*.rom)\n\nAfter you select these files, this program will reflash your BIOS\n\nDo you want to proceed?" 0 80); then - mount_usb - if grep -q /media /proc/mounts; then - find /media -name '*.key' >/tmp/filelist.txt - find /media -name '*.asc' >>/tmp/filelist.txt - file_selector "/tmp/filelist.txt" "Choose your GPG public key" - if [ "$FILE" == "" ]; then - return 1 - fi - PUBKEY=$FILE +gpg_set_card_identity() { + # Set cardholder name and login on OpenPGP smartcard. + # $1: gpg_name (cardholder name, empty or "OEM Key" to skip) + # $2: gpg_email (login, oem-*@example.com to skip) + # $3: card_admin_pin (default 12345678) + TRACE_FUNC + local gpg_name="$1" + local gpg_email="$2" + local card_admin_pin="$3" + local set_name=0 set_login=0 + local surname given - find /media -name '*.rom' >/tmp/filelist.txt - file_selector "/tmp/filelist.txt" "Choose the ROM to load your key onto" - if [ "$FILE" == "" ]; then - return 1 + [ -n "$gpg_name" ] && [ "$gpg_name" != "OEM Key" ] && set_name=1 + if [ -n "$gpg_email" ]; then + case "$gpg_email" in + oem-*@example.com) ;; + *) set_login=1 ;; + esac + fi + + if [ "$set_name" -eq 0 ] && [ "$set_login" -eq 0 ]; then + DEBUG "No custom identity to set on smartcard" + return + fi + + if [ "$set_name" -eq 1 ]; then + case "$gpg_name" in + *" "*) + given="${gpg_name% *}" + surname="${gpg_name##* }" + ;; + *) + surname="$gpg_name" + given="" + ;; + esac + DEBUG "Will set cardholder name: surname='$surname' given='$given'" + fi + + STATUS "Setting identity fields on OpenPGP smartcard" + { + echo "admin" + if [ "$set_name" -eq 1 ]; then + echo "name" + echo "${surname}" + echo "${given}" + fi + if [ "$set_login" -eq 1 ]; then + echo "login" + echo "${gpg_email}" + fi + echo "quit" + } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit \ + >/tmp/gpg_card_edit_output 2>&1 || + DIE "Failed to set identity fields on OpenPGP smartcard" + + local summary="" + [ "$set_name" -eq 1 ] && summary="${given:+$given }${surname}" + [ "$set_login" -eq 1 ] && summary="${summary:+$summary, }${gpg_email}" + STATUS_OK "Card identity set: $summary" +} + +gpg_card_change_pin() { + # Change GPG PIN (user or admin) on OpenPGP smartcard. + # $1: pin_type (1 = user PIN, 3 = admin PIN) + # $2: old_pin (current PIN value) + # $3: new_pin (new PIN value) + TRACE_FUNC + local pin_type="$1" + local old_pin="$2" + local new_pin="$3" + local rc + { + echo admin + echo passwd + echo "${pin_type}" + echo "${old_pin}" + echo "${new_pin}" + echo "${new_pin}" + echo q + echo q + } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit \ + >/tmp/gpg_card_edit_output 2>&1 + rc=$? + TRACE_FUNC + DEBUG "GPG PIN change output: $(cat /tmp/gpg_card_edit_output)" + if [ $rc -ne 0 ]; then + return 1 + fi + TRACE_FUNC +} + +gpg_keytocard_subkeys() { + # Move subkeys from local keyring to OpenPGP smartcard. + # Pipe sends key 1/2/3 toggle -> keytocard -> slot -> subkey_pin -> card_pin + # for each of sign/encrypt/auth slots, then save. + # $1: key_id (email or fingerprint for --edit-key) + # $2: subkey_pin (passphrase for local keyring subkeys) + # $3: card_pin (default 12345678, admin PIN for smartcard) + TRACE_FUNC + local key_id="$1" + local subkey_pin="$2" + local card_pin="${3:-12345678}" + local rc + + enable_usb + enable_usb_storage + STATUS "Accessing $DONGLE_BRAND OpenPGP smartcard" + gpg --card-status >/dev/null 2>&1 || { + DEBUG "gpg --card-status failed in gpg_keytocard_subkeys" + return 1 + } + DEBUG "Smartcard accessible for keytocard via gpg --card-status" + + STATUS "Moving subkeys to $DONGLE_BRAND" + { + echo "key 1" + echo "keytocard" + echo "1" + echo "${subkey_pin}" + echo "${card_pin}" + echo "key 1" + echo "key 2" + echo "keytocard" + echo "2" + echo "${subkey_pin}" + echo "key 2" + echo "key 3" + echo "keytocard" + echo "3" + echo "${subkey_pin}" + echo "key 3" + echo "save" + } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback \ + --edit-key "$key_id" \ + >/tmp/gpg_card_edit_output 2>&1 + rc=$? + TRACE_FUNC + DEBUG "GPG keytocard output: $(cat /tmp/gpg_card_edit_output)" + if [ $rc -ne 0 ]; then + DEBUG "keytocard failed" + return 1 + fi + STATUS_OK "Subkeys moved to smartcard" + DEBUG "keytocard completed successfully, subkeys now on $DONGLE_BRAND" + + TRACE_FUNC +} + +_luks_cleanup() { + # Unmount /media and close all LUKS usb_mount mappings. + # Idempotent -- safe to call multiple times. + umount /media 2>/dev/null || true + for d in /dev/mapper/usb_mount_*; do + [ -e "$d" ] && cryptsetup close "$(basename "$d")" 2>/dev/null || true + done +} + +reprovision_smartcard_from_backup() { + TRACE_FUNC + local admin_pin key_algo rsa_key_length key_name key_email key_comment + local card_admin_pin identity_summary key_id uid_line + local algo_code bit_len tpm_counter_ok + + enable_usb + detect_usb_security_dongle_branding + DEBUG "Dongle brand: $DONGLE_BRAND" + + STATUS "Checking for $DONGLE_BRAND smartcard" + gpg --card-status >/dev/null 2>&1 || { + whiptail_error --title 'ERROR: No Smartcard' \ + --msgbox "Please insert your $DONGLE_BRAND USB Security dongle." 0 80 + return 1 + } + STATUS_OK "$DONGLE_BRAND smartcard accessible" + + while [ -z "$admin_pin" ]; do + INPUT "Enter GPG key backup passphrase:" -r -s admin_pin + [ -z "$admin_pin" ] && whiptail_error --title 'ERROR: Empty Passphrase' \ + --msgbox "The backup passphrase cannot be empty.\n\nEnter the passphrase that was used during\nOEM Factory Reset to create the backup." 0 80 + done + DEBUG "Backup passphrase collected (${#admin_pin} chars)" + + # Phase 1: wipe ~/.gnupg and initialize an empty keyring. + rm -rf /.gnupg + gpg --list-keys >/dev/null 2>&1 + DEBUG "Wiped ~/.gnupg keyring, initialized empty" + + # Phase 2: mount the LUKS private partition (read-only). + enable_usb + enable_usb_storage + # Loading usb-storage.ko can reset the USB subsystem, making scdaemon's + # existing CCID connection stale. Kill it so the next gpg call starts + # fresh (matching the OEM pattern in oem-factory-reset.sh). + release_scdaemon + mkdir -p /tmp/secret + printf '%s' "$admin_pin" >/tmp/secret/backup_pass + chmod 600 /tmp/secret/backup_pass 2>/dev/null || true + STATUS "Mounting GPG key backup (LUKS private partition)" + if ! mount-usb.sh --mode ro --mountpoint /media --pass-file /tmp/secret/backup_pass; then + shred -n 10 -z -u /tmp/secret/backup_pass 2>/dev/null || rm -f /tmp/secret/backup_pass + DEBUG "Could not mount backup LUKS partition" + whiptail_error --title 'ERROR: Backup Mount Failed' \ + --msgbox "Could not mount the backup USB drive.\n\nVerify that the correct backup drive is inserted\nand the passphrase is correct." 0 80 + return 1 + fi + shred -n 10 -z -u /tmp/secret/backup_pass 2>/dev/null || rm -f /tmp/secret/backup_pass + DEBUG "LUKS partition mounted at /media" + STATUS_OK "Backup LUKS partition mounted" + + if [ ! -f /media/privkey.sec ]; then + _luks_cleanup + WARN "privkey.sec not found on backup drive -- not a valid GPG key backup" + whiptail_error --title 'ERROR: No Backup Found' \ + --msgbox "No privkey.sec found on this drive.\n\nThis does not appear to be a valid\nGPG key backup drive." 0 80 + return 1 + fi + + # Phase 3: import the private key (master + subkeys) into ~/.gnupg. + STATUS "Importing GPG keys from backup" + if ! gpg --pinentry-mode=loopback --passphrase-fd 3 3< <(echo -n "$admin_pin") \ + --import-options restore --import /media/privkey.sec >/dev/null 2>/tmp/gpg_import_err; then + _luks_cleanup + ERROR="$(cat /tmp/gpg_import_err)" + WARN "GPG key import from backup failed: $(head -3 /tmp/gpg_import_err 2>/dev/null)" + whiptail_error --title 'ERROR: Key Import Failed' \ + --msgbox "Failed to import GPG keys from backup.\n\n${ERROR}" 0 80 + return 1 + fi + DEBUG "privkey.sec imported into ~/.gnupg successfully" + STATUS_OK "GPG keys imported" + + # Phase 4: detect key type and extract identity from the now-imported key. + algo_code="$(gpg --with-colons --list-keys 2>/dev/null | grep '^pub:' | cut -d: -f4 | head -1)" + bit_len="$(gpg --with-colons --list-keys 2>/dev/null | grep '^pub:' | cut -d: -f3 | head -1)" + uid_line="$(gpg --with-colons --list-keys 2>/dev/null | grep '^uid:' | head -1 | cut -d: -f10)" + + case "$algo_code" in + 1) + key_algo="RSA" + rsa_key_length="$bit_len" + DEBUG "Detected RSA ${rsa_key_length}-bit key from backup" + ;; + 19) + key_algo="p256" + rsa_key_length="" + DEBUG "Detected NIST P-256 key from backup" + ;; + *) + _luks_cleanup + whiptail_error --title 'ERROR: Unknown Key Type' \ + --msgbox "Could not detect the key type from the backup\n(algorithm $algo_code).\n\nThe backup file may be corrupted." 0 80 + return 1 + ;; + esac + + if echo "$uid_line" | grep -q '('; then + key_name="$(echo "$uid_line" | sed 's/ (.*//')" + key_comment="$(echo "$uid_line" | sed 's/.*(//;s/).*//')" + else + key_name="$uid_line" + key_comment="" + fi + key_email="$(echo "$uid_line" | grep -o '<[^>]*>' | tr -d '<>')" + [ -z "$key_name" ] && key_name="$uid_line" + + if [ -n "$key_email" ]; then + key_id="$key_email" + else + key_id="$(gpg --list-secret-keys --with-colons 2>/dev/null | grep '^sec:' | cut -d: -f5 | head -1)" + [ -z "$key_id" ] && { + _luks_cleanup + DIE "Could not determine key ID from imported backup" + } + fi + DEBUG "Using key_id=$key_id (${key_email:+from email, }${key_email:-from fingerprint})" + + local key_fpr + key_fpr="$(gpg --with-colons --list-keys 2>/dev/null | grep '^fpr' | cut -d: -f10 | head -1)" || \ + DEBUG "Fingerprint extraction returned non-zero, continuing without it" + + DEBUG "Key fingerprint: $key_fpr" + DEBUG "Backup key identity: name='$key_name' email='$key_email' comment='$key_comment' fingerprint='$key_fpr'" + + DEBUG "Checking $DONGLE_BRAND compatibility with $key_algo key" + DEBUG "$key_algo key -- no dongle compatibility concern" + + DEBUG "Showing reprovision confirmation dialog to user" + if ! whiptail_warning --title "Reprovision Smartcard" \ + --yesno "This will:\n\n * ERASE all keys on your $DONGLE_BRAND\n * Import GPG key: $key_name${key_comment:+ ($key_comment)}${key_email:+ <$key_email>}\n Fingerprint: $key_fpr\n (${key_algo}${rsa_key_length:+ $rsa_key_length-bit})\n * Copy subkeys to the smartcard\n\nDo you want to continue?" 0 80; then + _luks_cleanup + DEBUG "User declined reprovision" + return 1 + fi + DEBUG "User confirmed reprovision; proceeding with factory reset" + + # Phase 5: factory-reset the smartcard and configure key attributes. + card_admin_pin="12345678" + release_scdaemon + gpg_reset_nk3_secret_app "$card_admin_pin" || \ + DEBUG "NK3 Secrets app reset failed (non-fatal; HOTP configured later)" + + local factory_reset_ok="n" + for attempt in 1 2; do + DEBUG "Factory reset attempt $attempt with admin PIN (${#card_admin_pin} chars)" + if gpg_card_factory_reset "$key_algo" "$rsa_key_length" "$card_admin_pin"; then + factory_reset_ok="y" + DEBUG "Smartcard factory reset succeeded on attempt $attempt" + break + fi + if [ "$attempt" -eq 1 ]; then + WARN "Factory reset with default admin PIN failed; the card may have a custom PIN." + card_admin_pin="" + while [ -z "$card_admin_pin" ]; do + INPUT "Enter the current $DONGLE_BRAND admin PIN:" -r -s card_admin_pin + done + release_scdaemon + gpg_reset_nk3_secret_app "$card_admin_pin" || \ + DEBUG "NK3 Secrets app reset with custom PIN also failed (non-fatal)" + fi + done + if [ "$factory_reset_ok" != "y" ]; then + _luks_cleanup + ERROR="$(tail -n 3 /tmp/gpg_card_edit_output 2>/dev/null | fold -s)" + WARN "Smartcard factory reset failed after retry with correct admin PIN" + whiptail_error --title 'ERROR: Factory Reset Failed' \ + --msgbox "Could not factory reset the $DONGLE_BRAND smartcard.\n\n${ERROR}\n\nCheck that the admin PIN is correct." 0 80 + return 1 + fi + + card_admin_pin="12345678" + + # Phase 6: move subkeys from the local keyring to the smartcard. + DEBUG "Starting keytocard with key_id=$key_id, admin_pin=${#admin_pin} chars, card_admin_pin=${#card_admin_pin} chars" + if ! gpg_keytocard_subkeys "$key_id" "$admin_pin" "$card_admin_pin"; then + _luks_cleanup + ERROR="$(cat /tmp/gpg_card_edit_output)" + WARN "GPG keytocard operation failed: $(head -3 /tmp/gpg_card_edit_output 2>/dev/null)" + whiptail_error --title 'ERROR: Keytocard Failed' \ + --msgbox "Failed to move subkeys to smartcard.\n\n${ERROR}" 0 80 + return 1 + fi + + # Phase 7: set card identity from the backup key's UID + gpg_set_card_identity "$key_name" "$key_email" "$card_admin_pin" + + # Phase 7b: prompt for custom PINs if desired. + local pin_label_admin="GPG Admin PIN" + [ "$DONGLE_BRAND" = "Nitrokey 3" ] && pin_label_admin="NK3 Secrets app PIN / GPG Admin PIN" + + if whiptail_warning --title "Set Custom PINs?" \ + --yesno "The card is currently using factory-default PINs\n(Admin: 12345678, User: 123456).\n\nWould you like to set custom PINs?" 0 80; then + local new_admin_pin="" new_user_pin="" + NOTE "${pin_label_admin}: for GPG card admin operations, 6-64 chars." + while [ -z "$new_admin_pin" ]; do + INPUT "Enter new ${pin_label_admin} (6-64 chars):" -r -s new_admin_pin + done + if ! gpg_card_change_pin 3 "12345678" "$new_admin_pin"; then + ERROR="$(cat /tmp/gpg_card_edit_output | fold -s)" + whiptail_error --title 'ERROR: Admin PIN Change Failed' \ + --msgbox "Could not change the Admin PIN.\n\n${ERROR}" 0 80 + _luks_cleanup + return 1 + fi + STATUS_OK "${pin_label_admin} changed" + + release_scdaemon + gpg_reset_nk3_secret_app "$new_admin_pin" || \ + DEBUG "NK3 Secrets app PIN update failed -- HOTP will need the default PIN (12345678)" + + NOTE "GPG User PIN: signing /boot and encryption, 3 attempts max.\nRecommended: 2 diceware words (6-25 chars)" + while [ -z "$new_user_pin" ]; do + INPUT "Enter new GPG User PIN (6-25 chars):" -r -s new_user_pin + done + if ! gpg_card_change_pin 1 "123456" "$new_user_pin"; then + ERROR="$(cat /tmp/gpg_card_edit_output | fold -s)" + whiptail_error --title 'ERROR: User PIN Change Failed' \ + --msgbox "Could not change the User PIN.\n\n${ERROR}" 0 80 + _luks_cleanup + return 1 + fi + STATUS_OK "GPG User PIN changed" + printf '%s' "$new_user_pin" >/tmp/secret/gpg_pin + chmod 600 /tmp/secret/gpg_pin 2>/dev/null || true + else + DEBUG "User declined custom PINs; keeping factory defaults" + printf '%s' "123456" >/tmp/secret/gpg_pin + chmod 600 /tmp/secret/gpg_pin 2>/dev/null || true + fi + + # Phase 8: sign /boot so hashes exist on next boot. + STATUS "Signing /boot files for next boot" + detect_boot_device + if mount -o remount,rw /boot 2>/tmp/sign_err; then + rm -f /boot/kexec*.txt /boot/kexec.sig 2>/dev/null + + if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then + tpmr.sh counter_create -pwdc '' -la -3135106223 >/tmp/counter 2>/dev/null || true + local tpm_counter + tpm_counter="$(cut -d: -f1 /dev/null)" + if [ -n "$tpm_counter" ]; then + increment_tpm_counter "$tpm_counter" || true + sha256sum /tmp/counter-"$tpm_counter" >/boot/kexec_rollback.txt 2>/dev/null || true + tpm_counter_ok="y" fi - cp "$FILE" /tmp/gpg-gui.rom + fi + + (cd /boot && find ./ -type f ! -path './kexec*' -print0 | \ + xargs -0 sha256sum >/boot/kexec_hashes.txt 2>/dev/null && \ + print_tree >/boot/kexec_tree.txt) || \ + DEBUG "Hash generation produced warnings" - if (whiptail_warning --title 'Flash ROM?' \ - --yesno "This will replace your old ROM with the selected ROM\n\nDo you want to proceed?" 0 80); then + param_files=() + for f in /boot/kexec*.txt; do + [ -e "$f" ] || continue + param_files+=("$(basename "$f")") + done + if (cd /boot && sha256sum "${param_files[@]}" 2>/dev/null | \ + gpg --detach-sign --pinentry-mode loopback \ + --passphrase-file /tmp/secret/gpg_pin \ + --digest-algo SHA256 -a -o /boot/kexec.sig 2>/tmp/sign_err); then + DEBUG "/boot signed successfully" + rm -f /boot/kexec_default.*.txt 2>/dev/null + check_config /boot >/dev/null 2>/tmp/sign_err && \ + STATUS_OK "/boot files signed and ready" || \ + WARN "/boot verification produced warnings" + else + WARN "/boot signing failed: $(head -3 /tmp/sign_err 2>/dev/null)" + fi + + mount -o ro,remount /boot 2>/dev/null || true + else + WARN "/boot not writable; skipping signing" + fi + + # Phase 9: close LUKS and export public key from keyring. + _luks_cleanup + + STATUS "Exporting public key for ROM flash" + gpg --export --armor "$key_id" >/tmp/reprovision_pubkey.asc 2>/dev/null || { + _luks_cleanup + DIE "Failed to export public key for ROM flash" + } + PUBKEY=/tmp/reprovision_pubkey.asc + DEBUG "Public key exported to $PUBKEY" + + # Establish ultimate trust on the key (needed for gpg_flash_rom + boot) + gpg --list-keys --fingerprint --with-colons 2>/dev/null | \ + sed -E -n -e 's/^fpr:::::::::([0-9A-F]+):$/\1:6:/p' | \ + gpg --import-ownertrust >/dev/null 2>&1 + gpg --update-trust >/dev/null 2>&1 + DEBUG "Public key trusted in ~/.gnupg keyring" + + # Phase 10: set CONFIG_HAVE_GPG_KEY_BACKUP so future boots know a + # backup exists, and offer to flash the public key + config to ROM. + set_user_config "CONFIG_HAVE_GPG_KEY_BACKUP" "y" + DEBUG "Set CONFIG_HAVE_GPG_KEY_BACKUP=y in /etc/config.user" + combine_configs + + STATUS_OK "Smartcard reprovisioned" + + if [ "$tpm_counter_ok" != "y" ]; then + WARN "TPM rollback counter was not created. Reset the TPM from" + WARN "Options -> TPM/TOTP/HOTP Options -> Reset the TPM before" + WARN "the next boot to avoid being dropped into recovery shell." + fi + + if [[ "$CONFIG_BOARD_NAME" == qemu-* ]]; then + WARN "Skipping flash of GPG key to ROM: running in QEMU without internal flashing support." + WARN "Extract the public key and inject it into the firmware image as documented in boards/qemu*/*.md." + NOTE "The public key is in the keyring for this session but will be lost on\nreboot. Use an external GPG injection step (PUBKEY_ASC=... inject_gpg)\nto persist it across boots. See doc/qemu.md." + else + DEBUG "Offering ROM flash to user" + if whiptail_warning --title 'Flash Key to BIOS?' \ + --yesno "The public key is now in the local keyring for this session,\nbut will be lost on reboot unless you flash it to the ROM.\n\nThis will persist the public key and GPG backup setting.\n\nFlash to ROM now?" 0 80; then + DEBUG "User accepted ROM flash; reading BIOS" + [ -f /tmp/gpg-gui.rom ] && rm -f /tmp/gpg-gui.rom + /bin/flash.sh -r /tmp/gpg-gui.rom + if [ ! -s /tmp/gpg-gui.rom ]; then + WARN "Could not read running BIOS for ROM flash" + whiptail_error --title 'ERROR: BIOS Read Failed' \ + --msgbox "Unable to read the running BIOS.\n\nThe key is in the local keyring but will be lost on reboot." 0 80 + else gpg_flash_rom fi + else + DEBUG "User declined ROM flash" fi fi - return 1 -} -gpg_replace_key_reflash() { - [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg - [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx - [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg - gpg_add_key_reflash + DEBUG "Running cleanup: unmounting partitions and closing LUKS mappings" + _luks_cleanup + + release_scdaemon + find "${GNUPGHOME:-$HOME/.gnupg}/private-keys-v1.d" \ + -name '*.key' -delete >/dev/null 2>&1 || true + + shred -n 10 -z -u /tmp/secret/gpg_pin 2>/dev/null || rm -f /tmp/secret/gpg_pin + DEBUG "Offering reboot to finalize provisioning" + if whiptail_warning --title 'Reboot?' \ + --yesno "The $DONGLE_BRAND smartcard has been reprovisioned\nfrom the GPG key backup.\n\nYou should reboot to finalize and then update /boot signatures\nvia Options -> Update checksums and sign all files in /boot.\n\nReboot now?" 0 80; then + DEBUG "User accepted reboot" + /bin/reboot.sh + fi + DEBUG "User declined reboot" + + unset admin_pin card_admin_pin + TRACE_FUNC } From fcfc78d9f512d05688dc9584b614a9fe4e2872e0 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Mon, 3 Aug 2026 17:24:29 -0400 Subject: [PATCH 02/10] build: reorganize clean targets into nuclear and dev variants - Fix real.gitclean to use git clean -ffxd so nested git repos are deleted rather than skipped - Remove overwrite_canary_if_coreboot_git from nuclear targets since repos are wiped - Rename real.gitclean_keep_packages to real.gitclean_keep_pkg - Rename real.gitclean_keep_packages_and_build to real.gitclean_keep_build - Add real.devclean, real.devclean_pkg, real.devclean_build using git clean -fxd that skip nested repos and recreate the canary - Update doc/modules.md reference table Signed-off-by: Thierry Laurion --- doc/modules.md | 53 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/doc/modules.md b/doc/modules.md index d9731c932..20bb05b6b 100644 --- a/doc/modules.md +++ b/doc/modules.md @@ -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 `. + +#### 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 From 9d8e695b127e5c7b4f03a9ed32adee35cf887371 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 7 Aug 2026 17:26:17 -0400 Subject: [PATCH 03/10] initrd/bin/gui-init.sh: propose smartcard reprovision when keyring empty The empty-keyring error dialog in check_gpg_key only offered adding a GPG key, OEM Factory Reset, ignoring the error, or exiting to the recovery shell. Reprovisioning the USB security dongle from the GPG key backup was reachable only one level deeper, via the GPG management menu. - Add 'K' (Reprovision USB Security dongle from GPG key backup) entry to the empty-keyring error menu, mirroring prompt_missing_gpg_key_action. - Route 'K' to reprovision_smartcard_from_backup and clear the error background color on success, matching the 'g' arm. - Bump the menu list height from 4 to 5 for the additional entry. Signed-off-by: Thierry Laurion --- initrd/bin/gui-init.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index 6fed923d6..452711c7d 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -563,9 +563,10 @@ check_gpg_key() { local gpg_error_msg 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?" whiptail_error --title "ERROR: GPG keyring empty!" \ - --menu "$gpg_error_msg" 0 80 4 \ + --menu "$gpg_error_msg" 0 80 5 \ 'g' ' Add a GPG key to the running BIOS' \ 'F' ' OEM Factory Reset / Re-Ownership' \ + 'K' ' Reprovision USB Security dongle from GPG key backup' \ 'i' ' Ignore error and continue to main menu' \ 'x' ' Exit to recovery shell' \ 2>/tmp/whiptail || recovery "GUI menu failed" @@ -582,6 +583,9 @@ check_gpg_key() { F) oem-factory-reset.sh ;; + K) + reprovision_smartcard_from_backup && BG_COLOR_MAIN_MENU="normal" + ;; x) recovery "User requested recovery shell" From 325eb06066be9c04c5e4f102362b6d2a6f64ee8c Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 7 Aug 2026 17:31:47 -0400 Subject: [PATCH 04/10] initrd/bin/kexec-select-boot.sh: accept uppercase Y/D in boot confirmation The confirmation prompt advertises [Y/d/b], but the selection loop in user_select only matched lowercase 'y' and 'd'. Pressing 'Y' re-prompted the menu instead of booting; Enter worked only because an empty read defaults to 'y'. - Normalize option_confirm to lowercase after reading in confirm_menu_option, so 'Y' and 'D' behave like 'y' and 'd'. - Matches the case handling already used for default_confirm in save_default_option. Signed-off-by: Thierry Laurion --- initrd/bin/kexec-select-boot.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 09111a974..809660fb7 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -260,6 +260,9 @@ confirm_menu_option() { STATUS " Final kernel cmdline: $_final_cmdline" INPUT "Boot (Y), make default (d), back to menu (b) [Y/d/b]:" -n 1 option_confirm [ -z "$option_confirm" ] && option_confirm="y" + # Prompt advertises uppercase (Y); normalize so the selection loop + # accepts both cases, matching save_default_option's default_confirm. + option_confirm="${option_confirm,,}" return 0 #fi } From 80d6ee8e4b358c0adc8175779e5c89fb7f331449 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 7 Aug 2026 17:40:58 -0400 Subject: [PATCH 05/10] initrd/etc/gui_functions.sh: offer smartcard reprovision in integrity report The Measured Integrity Report only offered OEM Factory Reset and Continue when /boot was intact but the signing key was missing or not ROM-trusted. Users holding a GPG key backup could not reach reprovision_smartcard_from_backup from the report; they had to exit to the GPG management menu. - Add 'K' (Reprovision USB Security dongle from GPG key backup) to the /boot-intact-but-no-private-key menu, routed to reprovision_smartcard_from_backup. - Add 'K' to the DONGLE KEY NOT ROM-TRUSTED menu (i/r/o/c -> i/r/o/K/c), so a wiped or re-keyed dongle can be restored to the ROM-trusted identity from backup instead of forcing re-ownership. - Bump both menu list heights for the additional entry. Signed-off-by: Thierry Laurion --- initrd/etc/gui_functions.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index cf2ab6d42..bc571c0ca 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -454,8 +454,9 @@ report_integrity_measurements() { # /boot is intact but no private key - direct path is OEM Factory Reset / Re-Ownership while true; do whiptail_type "$BG_COLOR_MAIN_MENU" --title 'Measured Integrity Report' \ - --menu "$msg" 0 80 2 \ + --menu "$msg" 0 80 3 \ 'o' ' OEM Factory Reset / Re-Ownership -->' \ + 'K' ' Reprovision USB Security dongle from GPG key backup' \ 'c' ' Continue to main menu' \ 2>/tmp/whiptail || return 0 report_option=$(cat /tmp/whiptail) @@ -464,6 +465,10 @@ report_integrity_measurements() { INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh return 0 ;; + K) + reprovision_smartcard_from_backup + return 0 + ;; c | *) return 0 ;; @@ -474,10 +479,11 @@ report_integrity_measurements() { if [ "$signing_key_state" = "DONGLE KEY NOT ROM-TRUSTED" ]; then while true; do whiptail_type $BG_COLOR_MAIN_MENU --title 'Measured Integrity Report' \ - --menu "$menu_msg" 0 80 4 \ + --menu "$menu_msg" 0 80 5 \ 'i' ' Investigate discrepancies -->' \ 'r' ' Replace GPG key in current ROM and reflash' \ 'o' ' OEM Factory Reset / Re-Ownership' \ + 'K' ' Reprovision USB Security dongle from GPG key backup' \ 'c' ' Continue' \ 2>/tmp/whiptail || return 0 report_option=$(cat /tmp/whiptail) @@ -495,6 +501,10 @@ report_integrity_measurements() { INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh return 0 ;; + K) + reprovision_smartcard_from_backup + return 0 + ;; *) return 0 ;; From 5d73bef604006f38d99c16c368b7890e50ee62c4 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 7 Aug 2026 18:01:32 -0400 Subject: [PATCH 06/10] initrd/bin/gui-init.sh: offer smartcard reprovision in remaining recovery menus Two recovery paths still funneled users holding a GPG key backup straight into OEM Factory Reset / Re-Ownership without offering reprovision from backup: - The TPM State Inconsistent (rollback preflight) dialog only offered integrity report, OEM Factory Reset, TPM reset, and main menu. - clean_boot_check launched oem-factory-reset.sh directly via a yes/no prompt when no kexec files and no GPG keys were found. - Add 'K' (Reprovision USB Security dongle from GPG key backup) to the rollback preflight menu (i/o/t/m -> i/o/K/t/m), routed to reprovision_smartcard_from_backup; the loop re-displays since the TPM counter issue is unaffected by key provisioning. - Convert the clean-boot yes/no prompt into a menu offering OEM Factory Reset, reprovision from backup, ignore and continue, and recovery shell. - Bump the preflight menu list height from 4 to 5 for the extra entry. Signed-off-by: Thierry Laurion --- initrd/bin/gui-init.sh | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index 452711c7d..7567432cc 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -547,9 +547,28 @@ clean_boot_check() { fi # OS is installed, no kexec files present, no GPG keys in keyring, security token present - # prompt user to run OEM factory reset - oem-factory-reset.sh \ - "Clean Boot Detected - Perform OEM Factory Reset / Re-Ownership?" + # prompt user to run OEM factory reset or reprovision from key backup + whiptail_error --title 'Clean Boot Detected' \ + --menu "Clean boot detected: OS installed, no boot signatures, and no GPG keys in the keyring.\n\nHow would you like to proceed?" 0 80 4 \ + 'F' ' OEM Factory Reset / Re-Ownership' \ + 'K' ' Reprovision USB Security dongle from GPG key backup' \ + 'i' ' Ignore and continue to main menu' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || return + option=$(cat /tmp/whiptail) + case "$option" in + F) + oem-factory-reset.sh "Clean Boot Detected - Perform OEM Factory Reset / Re-Ownership?" + ;; + K) + reprovision_smartcard_from_backup + ;; + x) + recovery "User requested recovery shell" + ;; + i | *) + ;; + esac } check_gpg_key() { @@ -980,9 +999,10 @@ EOF _menu_text="$preflight_menu_text" fi whiptail_error --title 'ERROR: TPM State Inconsistent' \ - --menu "$_menu_text" 26 80 4 \ + --menu "$_menu_text" 26 80 5 \ 'i' ' Show integrity report -->' \ 'o' ' OEM Factory Reset / Re-Ownership -->' \ + 'K' ' Reprovision USB Security dongle from GPG key backup' \ 't' ' Reset the TPM' \ 'm' ' Continue to main menu' \ 2>/tmp/whiptail || recovery "GUI menu failed" @@ -1001,6 +1021,9 @@ EOF BG_COLOR_MAIN_MENU="normal" fi ;; + K) + reprovision_smartcard_from_backup + ;; t) if reset_tpm && preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then rollback_preflight_failed="n" From 6a66b12676d83f252d65d27bf0cef7db94f5b2d6 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 7 Aug 2026 18:14:42 -0400 Subject: [PATCH 07/10] initrd/etc/gpg_functions.sh: skip TPM counter create during smartcard reprovision when TPM reset is required When reprovision_smartcard_from_backup runs after the rollback preflight failed (tpm_reset_required set), Phase 8 still attempted tpmr.sh counter_create. tpm2 nvdefine -C o requires the TPM owner passphrase, which is unknown in that state (TPM was reset or swapped), so the user was prompted 3 times for a passphrase that cannot work, each failing with authorization error 0x9a2. Guard counter creation with ! tpm_reset_required so the flow proceeds straight to the existing reset-TPM guidance. Mirrors the guards already used by generate_totp_hotp and prompt_update_checksums. Signed-off-by: Thierry Laurion --- initrd/etc/gpg_functions.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/initrd/etc/gpg_functions.sh b/initrd/etc/gpg_functions.sh index de3a9c25e..4dc84252d 100644 --- a/initrd/etc/gpg_functions.sh +++ b/initrd/etc/gpg_functions.sh @@ -647,7 +647,12 @@ reprovision_smartcard_from_backup() { if mount -o remount,rw /boot 2>/tmp/sign_err; then rm -f /boot/kexec*.txt /boot/kexec.sig 2>/dev/null - if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then + # Do not attempt counter creation when the TPM state is known + # inconsistent (tpm_reset_required): the owner passphrase is unknown + # and the counter cannot be created, so prompting for it would be a + # dead end. The reset-TPM guidance below then tells the user what + # to do next (mirrors generate_totp_hotp / prompt_update_checksums). + if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_IGNORE_ROLLBACK" != "y" ] && ! tpm_reset_required; then tpmr.sh counter_create -pwdc '' -la -3135106223 >/tmp/counter 2>/dev/null || true local tpm_counter tpm_counter="$(cut -d: -f1 /dev/null)" From 73a7da2f3231809ae53bc0b0cc8101efe635940d Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 7 Aug 2026 18:32:43 -0400 Subject: [PATCH 08/10] initrd/etc/gui_functions.sh: direct TPM reset guidance in integrity report when marker set When the rollback counter preflight fails, set_tpm_reset_required marks the TPM state inconsistent. The integrity report still said "Reseal required" and told the user to generate a new HOTP/TOTP secret, but generate_totp_hotp refuses to run while the marker is set, so the guidance pointed at a blocked action. - Branch the TOTP state display on tpm_reset_required: report "TPM reset required (rollback counter cannot be verified)" instead of "Reseal required" when the marker is present. - Replace the "generate a new HOTP/TOTP secret" note with reset-the-TPM guidance when tpm_reset_required, since reset_tpm re-creates the rollback counter and regenerates TOTP/HOTP in one flow. Signed-off-by: Thierry Laurion --- initrd/etc/gui_functions.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index bc571c0ca..5c95ff521 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -400,7 +400,11 @@ report_integrity_measurements() { local totp_display hotp_display case "$totp_state" in UNAVAILABLE) - totp_display="SEALED SECRET UNAVAILABLE - Reseal required (expected after TPM reset, re-ownership, or firmware update)" + if tpm_reset_required; then + totp_display="SEALED SECRET UNAVAILABLE - TPM reset required (rollback counter cannot be verified)" + else + totp_display="SEALED SECRET UNAVAILABLE - Reseal required (expected after TPM reset, re-ownership, or firmware update)" + fi ;; ERROR) totp_display="ERROR - TOTP calculation failed" @@ -439,8 +443,13 @@ report_integrity_measurements() { DEBUG "report_integrity_measurements: signature_guidance=$sig_guidance signing_key_guidance=$signing_key_guidance" DEBUG "report_integrity_measurements: INTEGRITY_REPORT_HASH_STATE=$INTEGRITY_REPORT_HASH_STATE" if [ "$totp_state" = "UNAVAILABLE" ] && [ "$hash_state" = "OK" ] && [ "$signing_key_state" = "DONGLE MATCHES ROM-TRUSTED KEY" ]; then - DEBUG "report_integrity_measurements: TOTP unseal unavailable but /boot integrity is OK; reseal/update flows may proceed after user confirmation" - report_body="$report_body\n\nNote: /boot is intact - generate a new HOTP/TOTP secret to restore real-time boot attestation." + if tpm_reset_required; then + DEBUG "report_integrity_measurements: TOTP unseal unavailable and TPM reset required; reset flow re-creates counter and regenerates TOTP/HOTP" + report_body="$report_body\n\nNote: /boot is intact, but the TPM rollback counter cannot be verified. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) - that flow re-creates the rollback counter and regenerates TOTP/HOTP." + else + DEBUG "report_integrity_measurements: TOTP unseal unavailable but /boot integrity is OK; reseal/update flows may proceed after user confirmation" + report_body="$report_body\n\nNote: /boot is intact - generate a new HOTP/TOTP secret to restore real-time boot attestation." + fi fi msg="Measured Integrity Report\n\n$report_body" # menu_msg omits the guidance paragraphs to keep the dialog within terminal height From 76f1a62c75d2cbbd6955486ecc5260661cb100cd Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 7 Aug 2026 19:39:49 -0400 Subject: [PATCH 09/10] doc/configuring-keys.md: document reprovision entry points and TPM reset guidance Signed-off-by: Thierry Laurion --- doc/configuring-keys.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/doc/configuring-keys.md b/doc/configuring-keys.md index 36070bfe0..a0109b5db 100644 --- a/doc/configuring-keys.md +++ b/doc/configuring-keys.md @@ -177,6 +177,35 @@ 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). From 918dff7e3e38b6efeed24993849aa02e0b45a873 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 7 Aug 2026 20:53:53 -0400 Subject: [PATCH 10/10] boards: move EOL t420/x220 hotp-maximized variants to unmaintained The smartcard reprovision flow (PR #2158) grew the shared initrd by ~4.5KB compressed. The 8MB flash boards were already at their CBFS limit, so the t420/x220 hotp-maximized variants no longer fit and are moved to unmaintained_boards until issue #590 frees enough space. Users of these platforms are assumed to use TOTP, not HOTP, until proven otherwise. Board owners needing HOTP should work on #590. - git mv boards/EOL_t420-hotp-maximized to unmaintained_boards/UNMAINTAINED_t420-hotp-maximized - git mv boards/EOL_x220-hotp-maximized to unmaintained_boards/UNMAINTAINED_x220-hotp-maximized - Drop both boards from .circleci/config.yml build matrix Signed-off-by: Thierry Laurion --- .circleci/config.yml | 14 -------------- .../UNMAINTAINED_t420-hotp-maximized.config | 0 .../UNMAINTAINED_x220-hotp-maximized.config | 0 3 files changed, 14 deletions(-) rename boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config => unmaintained_boards/UNMAINTAINED_t420-hotp-maximized/UNMAINTAINED_t420-hotp-maximized.config (100%) rename boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config => unmaintained_boards/UNMAINTAINED_x220-hotp-maximized/UNMAINTAINED_x220-hotp-maximized.config (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 191855454..2b96e3637 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 @@ -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 diff --git a/boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_t420-hotp-maximized/UNMAINTAINED_t420-hotp-maximized.config similarity index 100% rename from boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config rename to unmaintained_boards/UNMAINTAINED_t420-hotp-maximized/UNMAINTAINED_t420-hotp-maximized.config diff --git a/boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_x220-hotp-maximized/UNMAINTAINED_x220-hotp-maximized.config similarity index 100% rename from boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config rename to unmaintained_boards/UNMAINTAINED_x220-hotp-maximized/UNMAINTAINED_x220-hotp-maximized.config