From 3d8ccd6d8f9ae122b2d8d6778d03da39c3e5efec Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 17 Jul 2026 14:06:11 -0400 Subject: [PATCH] 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 69ce6649c..53f60f1f8 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -132,7 +132,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 e90ee62c3..c435e6f38 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 }