diff --git a/doc/architecture.md b/doc/architecture.md index 98d871917..7b9bd6475 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 USB Security dongle from GPG key backup" from the GPG Management Menu. Otherwise the recovery shell is unauthenticated. An "Ignore tampering and force a boot (Unsafe!)" option is available to override this. See [recovery-shell.md](recovery-shell.md#authentication) for details. - **Separation of duties** — the public key that verifies `/boot` signatures is stored in CBFS (ROM). The private key that signs `/boot` stays on a USB security dongle and never leaves it. - **Auditability** — all source is open, builds are reproducible, ROM images are verifiable diff --git a/doc/configuring-keys.md b/doc/configuring-keys.md index df2dcab9e..103535fd7 100644 --- a/doc/configuring-keys.md +++ b/doc/configuring-keys.md @@ -160,6 +160,57 @@ If you already have a provisioned USB Security dongle: 3. Follow the steps. After reflashing, reboot. 4. Generate a new TOTP/HOTP secret when prompted. +## Restoring Keys from Backup + +If you chose the in-memory backup path during OEM factory reset and your +dongle is lost, broken, or wiped: + +1. Insert the backup USB thumb drive and your (new) dongle. +2. Go to `Options -> GPG Management -> 'k' Reprovision USB Security dongle from GPG key backup`. +3. Enter the backup passphrase when prompted. +4. Heads detects the key type from the backup, factory-resets the dongle, + restores the subkeys, sets the card identity, resets the TPM, creates a + fresh rollback counter, and re-signs /boot (see below). +5. After success, flash the public key to the running BIOS for persistence + (skipped automatically on QEMU boards). +6. Reboot to finalize. + +This requires the LUKS-encrypted backup USB drive created during OEM factory +reset (answer Y to "format an encrypted USB Thumb drive"). Without it, run +a new OEM Factory Reset / Re-Ownership to rekey the device. + +The reprovision flow is reachable from several places, not just the GPG +Management Menu: + +- `Options -> GPG Management -> 'k'` — always visible (gpg-gui.sh). +- `'K'` in the empty-GPG-keyring error dialog (`check_gpg_key` in gui-init.sh). +- `'K'` in the measured integrity report (`report_integrity_measurements` in + gui_functions.sh), both the normal and the DONGLE KEY NOT ROM-TRUSTED + variants. +- `'K'` in the TPM State Inconsistent (rollback preflight) dialog — since + reprovisioning resets the TPM and creates a fresh counter, the preflight + check re-runs afterwards and the gate proceeds to boot when it passes. +- `'K'` in the clean boot wizard (`clean_boot_check`), next to `F` (OEM + Factory Reset), `i` (ignore), and `x` (recovery shell). + +**TPM handling:** reprovisioning is an ownership-level operation, so like +OEM Factory Reset it resets the TPM itself rather than punting a manual +"Reset the TPM" step to you. After you set the new TPM owner passphrase, +Heads clears the old ownership, drops stale rollback-counter references, +creates a fresh rollback counter, and re-signs /boot — all through the same +`kexec-sign-config.sh` path used by OEM factory reset. Signing is atomic: +the new manifests are staged under /tmp and moved into /boot only after +signing and verification succeed. If any step fails, /boot keeps its +previous valid signatures, no ROM flash is offered, and Heads explains how +to recover (re-sign via `Options -> Update checksums and sign all files in +/boot`, or reset the TPM first if that failed). + +Because the TPM was reset, unsealing the old TOTP/HOTP secrets on the next +boot will fail; Heads offers to regenerate them (choose `g`). + +After reprovisioning, the recovery shell and USB boot will require GPG +smartcard authentication; see [recovery-shell.md](recovery-shell.md#authentication). + ## Forgotten GPG User PIN From Recovery Shell with the dongle inserted: diff --git a/doc/gpg.md b/doc/gpg.md index 6de0e55b4..64fa41f92 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 -> 'k' Reprovision USB Security dongle from GPG key backup`. +3. Enter the backup passphrase. + +The backup drive must have been created by the OEM factory reset flow with +the "format an encrypted USB Thumb drive" option set to Y. + ## Nitrokey 3 Specifics - Supports NIST P-256 ECC keys in addition to RSA — significantly faster key diff --git a/doc/modules.md b/doc/modules.md index d9731c932..ecc65acb0 100644 --- a/doc/modules.md +++ b/doc/modules.md @@ -151,10 +151,10 @@ nix develop --command make BOARD=$BOARD | Target | What it does | |--------|-------------| | `real.clean` | Remove all build artifacts | -| `real.gitclean` | `git clean` — remove all untracked files | -| `real.gitclean_keep_packages` | `git clean` but keep downloaded tarballs in `packages/` | -| `real.remove_canary_files-extract_patch_rebuild_what_changed` | Remove all `.canary` sentinels, clear install + coreboot/board build caches, then rebuild. Use this after changing patches. | -| `real.gitclean_keep_packages_and_build` | Keep packages + clean + full rebuild | +| `real.gitclean` | `git clean -fxd` — remove all untracked and ignored files | +| `real.gitclean_keep_packages` | `git clean -fxd` but keep downloaded tarballs in `packages/` | +| `real.gitclean_keep_packages_and_build` | `git clean -fxd` keeping `packages/` and `build/` | +| `real.remove_canary_files-extract_patch_rebuild_what_changed` | Delete `.canary` stamps so the next `make` re-extracts, re-patches, and rebuilds only what changed | All run under `nix develop` (local) or `./docker_repro.sh` (Docker): diff --git a/doc/qemu.md b/doc/qemu.md index bed602f7d..2fbb4519d 100644 --- a/doc/qemu.md +++ b/doc/qemu.md @@ -48,8 +48,9 @@ and ext4 filesystem. Older images (from before `qemu-img create`) may be flat — check with `sudo fdisk -l` first. Note: the Docker container bind-mounts only the cloned Heads directory -(`$(pwd)`), so images must reside within the clone — use the `qemu_img/` -directory inside the repo as a backing store (see hardlink workflow below). +(`$(pwd)`), so images used by QEMU must live inside the clone. Backup +copies belong in `~/Qemu_img/` (same filesystem as the clone) and are +hardlinked back into `build/` when Docker needs them (see below). 2. Build Heads @@ -98,25 +99,22 @@ Ex: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub ## Saving Disk Images from Build-Dir Wipes **The Docker container can only see files inside the cloned Heads directory** -(`docker/common.sh` line 1446: `-v "$(pwd):$(pwd)"`). Any backup copy -must live at a path inside the clone — `~/QemuImages/` and other -user-home paths are invisible to Docker. +(`docker/common.sh` line 1446: `-v "$(pwd):$(pwd)"`). `~/Qemu_img/` is +invisible to Docker — files must be hardlinked back into `build/` before +Docker can use them. **The build directory (`build/x86//`) is ephemeral.** A `make clean` or fresh checkout deletes `build/` entirely, including installed OS images -and populated USB disks. Use hardlinks to keep safe copies inside the -clone and share across board variants: +and populated USB disks. Hardlink important files to `~/Qemu_img/` +(same filesystem as the clone) to keep them safe across wipes: - mkdir -p qemu_img # safe storage inside clone - cp build/x86//root.qcow2 qemu_img/ # copy OS install to safety - rm build/x86//root.qcow2 # remove build-tree copy - cp -alf qemu_img/root.qcow2 build/x86// # hardlink back - # Now both paths point to the same data on disk. - # Wiping build/ won't touch qemu_img/. + cp -alf build/x86//root.qcow2 ~/Qemu_img/ # backup + cp -alf build/x86//usb_fd.raw ~/Qemu_img/ # backup - # Restore after a wipe: - cp -alf qemu_img/root.qcow2 build/x86//root.qcow2 - cp -alf qemu_img/usb_fd.img build/x86//usb_fd.raw +After a wipe, restore from `~/Qemu_img/`: + + cp -alf ~/Qemu_img/root.qcow2 build/x86// # restore + cp -alf ~/Qemu_img/usb_fd.raw build/x86// # restore `cp -alf` creates a hardlink — a second directory entry pointing to the same data blocks (zero additional space). Data is freed only when the @@ -126,17 +124,13 @@ Use `qemu-img snapshot` before modifying the root disk. ### USB flash drive workflow ```bash -mkdir -p qemu_img # safe storage inside clone - # Step 1: Create the USB image via the Makefile. ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 \ QEMU_USB_SIZE=64G run # → build/x86/.../usb_fd.raw now exists. # Step 2: Save a master copy IMMEDIATELY (before population). -cp build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw qemu_img/usb_fd.img -rm build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw -cp -alf qemu_img/usb_fd.img build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw +cp -alf build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw ~/Qemu_img/ # Step 3: Populate with ISOs. sudo losetup --find --show --partscan build/x86/.../usb_fd.raw @@ -144,23 +138,83 @@ sudo mount /dev/loop0p1 /mnt cp ~/Downloads/ISOs/*.iso /mnt/ sudo umount /mnt && sudo losetup -d /dev/loop0 -# Step 4: Hardlink into other board build directories. -cp -alf qemu_img/usb_fd.img build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw -cp -alf qemu_img/usb_fd.img build/x86/qemu-coreboot-fbwhiptail-tpm2-hotp/usb_fd.raw +# Step 4: Restore after a wipe or hardlink into other board build dirs. +cp -alf ~/Qemu_img/usb_fd.raw build/x86/qemu-coreboot-fbwhiptail-tpm2/usb_fd.raw +cp -alf ~/Qemu_img/usb_fd.raw build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw # Next run uses the hardlink — Makefile skips creation since the file exists. ``` ### Daily development cycle -After OS install + USB provisioned, reference both from `./qemu_img/`: +Run the daily cycle directly against the `build/` images: ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp \ PUBKEY_ASC=pubkey.asc \ USB_TOKEN=Nitrokey3NFC \ - ROOT_DISK_IMG=./qemu_img/root.qcow2 \ inject_gpg run +After an OS install or any other state you want to keep, refresh the +master copies in `~/Qemu_img/` (same filesystem, zero-cost hardlinks) +so `make clean` won't destroy them: + + cp -alf build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 ~/Qemu_img/ + cp -alf build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw ~/Qemu_img/ + +If a rebuild wiped the `build/` images, restore them from the master +copies before running: + + cp -alf ~/Qemu_img/root.qcow2 build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/ + cp -alf ~/Qemu_img/usb_fd.raw build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/ + +### Testing GPG key reprovision from a backup drive + +The GPG key reprovision flow ('k' in the GPG Management Menu, or the 'K' +option when signing fails) restores subkeys from a LUKS-encrypted backup +drive created during OEM factory reset onto a (new) OpenPGP smartcard. +This can be tested in QEMU. + +First run OEM factory reset to populate the virtual USB drive with backup +material (this creates `build/x86//usb_fd.raw` with the LUKS private ++ exFAT public partition layout). Afterwards, save the virtual USB drive +and canokey state, then hardlink back for the second run: + +```bash +# ~/Qemu_img is the home-directory store for QEMU images (outside the +# clone). It must be on the same partition/disk as the ~/heads build +# directory so cp -alf hardlinks work (hardlinks cannot cross filesystems). +mkdir -p ~/Qemu_img + +# Save canokey state and populated USB backup image (hardlink, zero-cost). +cp -al build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/.canokey-file \ + ~/Qemu_img/.canokey-file +cp -al build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw \ + ~/Qemu_img/backup_drive.raw + +# Hardlink back into the build dir so make run picks them up; make +# requires the raw image to live inside the local heads dir. +cp -alf ~/Qemu_img/.canokey-file \ + build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/.canokey-file +cp -alf ~/Qemu_img/backup_drive.raw \ + build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw + +# Second run uses the preserved backup with no USB_FD_IMG override. +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp run +``` + +Inside the VM: Options -> GPG Options -> 'k' Reprovision USB Security dongle +from GPG key backup. Enter the backup passphrase (the Admin PIN you set +during OEM factory reset). The flow will: +- Detect the key type (RSA or ECC) from the backup +- Factory-reset the virtual canokey and set matching key attributes +- Import the master key and subkeys from the LUKS partition +- Move subkeys to the smartcard via keytocard +- Set the card identity (name, email) from the backup key's UID +- Reset the TPM and create a fresh rollback counter (TPM boards) +- Re-sign /boot so the next boot trusts the restored state +- Skip flashing the public key to ROM automatically (QEMU cannot reflash; + inject the exported pubkey into the firmware image instead) + Running via Docker wrappers === @@ -268,6 +322,31 @@ How I tested these wrappers (smoke checks) - Minimal: `source docker/common.sh && build_docker_opts` — should print a short description and show flags such as `--device=/dev/kvm` when KVM is available and `-v /tmp/heads-docker-xauth-XXXXXX:...` (or `-v /tmp/.docker.xauth-:...` 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..9e1c4cc37 100644 --- a/doc/recovery-shell.md +++ b/doc/recovery-shell.md @@ -20,6 +20,114 @@ 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 set in the user config. It is +persisted to CBFS only after a successful ROM flash: the reprovision +flow updates `/etc/config.user` in RAM and lets the user decline +flashing, and QEMU skips internal flashing entirely. Only after a +successful flash is recovery shell and USB boot authentication enforced +across reboots, 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..dc98691e4 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,12 @@ while true; do gpg_post_gen_mgmt fi ;; + "k") + # The function returns non-zero on benign paths (user declining, + # wrong passphrase, signing failure); this script runs under + # set -e, so guard the call to stay inside the management menu. + reprovision_smartcard_from_backup || true + ;; esac done diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index ce03d953b..7884a6fc9 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" ;; @@ -543,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() { @@ -559,9 +582,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" @@ -578,6 +602,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" @@ -972,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" @@ -993,6 +1021,17 @@ EOF BG_COLOR_MAIN_MENU="normal" fi ;; + K) + reprovision_smartcard_from_backup + # Reprovision re-owns the TPM and rewrites kexec_rollback.txt, + # so the preflight can pass; re-validate like the o/t arms so + # a successful run exits this gate instead of looping the + # error menu. + if preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="n" + BG_COLOR_MAIN_MENU="normal" + fi + ;; t) if reset_tpm && preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then rollback_preflight_failed="n" diff --git a/initrd/bin/mount-usb.sh b/initrd/bin/mount-usb.sh index f3f32bf35..ab214db05 100755 --- a/initrd/bin/mount-usb.sh +++ b/initrd/bin/mount-usb.sh @@ -8,9 +8,9 @@ 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) @@ -18,6 +18,7 @@ parameters: --pass: passphrase for LUKS device (default: none) --whole-disk: probe whole USB disks (not partitions) for a bootable filesystem, e.g. a dd-written hybrid ISO (default: off) + --pass-file: path to file containing passphrase --help: Show this help USAGE_END } @@ -26,6 +27,7 @@ MODE="ro" DEVICE="" MOUNTPOINT="/media" PASS="" +PASS_FILE="" WHOLE_DISK="" #Only assign --mode, --device, --mountpoint and --pass parameters only if variables following them are not empty @@ -65,6 +67,16 @@ while [ $# -gt 0 ]; do WHOLE_DISK="y" shift ;; + --pass-file) + if [ -z "$2" ]; then + DIE "ERROR: --pass-file requires a file argument" + elif [ ! -r "$2" ]; then + DIE "ERROR: --pass-file: unreadable file: $2" + fi + PASS_FILE="$2" + shift + shift + ;; *) usage exit 1 @@ -73,7 +85,7 @@ while [ $# -gt 0 ]; do done #Show parameters content but not LUKS passphrase: if empty, show "empty", if provided, show "provided" -DEBUG "Parameters: --mode=$MODE, --device=${DEVICE:-empty}, --mountpoint=$MOUNTPOINT, --pass=${PASS:+provided}" +DEBUG "Parameters: --mode=$MODE, --device=${DEVICE:-empty}, --mountpoint=$MOUNTPOINT, --pass=${PASS:+provided}${PASS_FILE:+ (file: $PASS_FILE)}" enable_usb enable_usb_storage @@ -142,7 +154,7 @@ else # When a passphrase is provided and multiple devices are present, # auto-select the LUKS partition (e.g. GPG backup drive: LUKS private + exFAT public). # This avoids burdening the user with selecting the right partition. - if [ -z "$USB_MOUNT_DEVICE" ] && [ -n "$PASS" ]; then + if [ -z "$USB_MOUNT_DEVICE" ] && { [ -n "$PASS" ] || [ -n "$PASS_FILE" ]; }; then luks_dev="" luks_count=0 while IFS= read -r dev; do @@ -209,14 +221,30 @@ if cryptsetup isLuks "$USB_MOUNT_DEVICE"; then cryptsetup close "usb_mount_$(basename "$USB_MOUNT_DEVICE")" fi DEBUG "Opening LUKS device $USB_MOUNT_DEVICE" - #Pass LUKS passphrase to cryptsetup only if we received one - if [ -z "$PASS" ]; then - #We haven't received a passphrase - cryptsetup open "$USB_MOUNT_DEVICE" "usb_mount_$(basename "$USB_MOUNT_DEVICE")" || + #Pass the LUKS passphrase to cryptsetup only if we received one. + if [ -n "$PASS_FILE" ]; then + # A passphrase file path was given: pass it directly to cryptsetup + # via --key-file. The secret stays on disk; it is never read into a + # shell variable nor staged in a temp file. + cryptsetup open "$USB_MOUNT_DEVICE" "usb_mount_$(basename "$USB_MOUNT_DEVICE")" --key-file "$PASS_FILE" || + DIE "ERROR: Failed to open ${USB_MOUNT_DEVICE} LUKS device" + elif [ -n "$PASS" ]; then + # We received a passphrase via --pass. Avoid forking `echo` with the + # passphrase in its argv (leakable via /proc//cmdline): stage it + # under /tmp/secret and pass that to cryptsetup, then shred/rm it on + # both success and failure. + mkdir -p /tmp/secret + key_file=/tmp/secret/cryptsetup_keyfile + printf '%s' "$PASS" >"$key_file" + if cryptsetup open "$USB_MOUNT_DEVICE" "usb_mount_$(basename "$USB_MOUNT_DEVICE")" --key-file "$key_file"; then + shred -n 10 -z -u "$key_file" 2>/dev/null || rm -f "$key_file" + else + shred -n 10 -z -u "$key_file" 2>/dev/null || rm -f "$key_file" DIE "ERROR: Failed to open ${USB_MOUNT_DEVICE} LUKS device" + fi else - #We received a pasphrase - cryptsetup open "$USB_MOUNT_DEVICE" "usb_mount_$(basename "$USB_MOUNT_DEVICE")" --key-file <(echo -n "${PASS}") || + # We haven't received a passphrase + cryptsetup open "$USB_MOUNT_DEVICE" "usb_mount_$(basename "$USB_MOUNT_DEVICE")" || DIE "ERROR: Failed to open ${USB_MOUNT_DEVICE} LUKS device" fi diff --git a/initrd/bin/oem-factory-reset.sh b/initrd/bin/oem-factory-reset.sh index 89d86aae6..7f3094fab 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 @@ -676,109 +562,23 @@ wipe_thumb_drive_and_copy_gpg_key_material() { } gpg_key_factory_reset() { + # OEM wrapper: calls shared gpg_card_factory_reset, dies on failure, + # and adds Nitrokey Storage AES key reset side effect. TRACE_FUNC - #enable usb storage enable_usb - # Factory reset GPG card - STATUS "GPG factory reset of $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 --card-edit \ - >/tmp/gpg_card_edit_output 2>&1 - TRACE_FUNC - DEBUG "GPG factory-reset output: $(cat /tmp/gpg_card_edit_output)" - if [ $? -ne 0 ]; then - ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "GPG Key factory reset failed!\n\n$ERROR" + if ! gpg_card_factory_reset "$GPG_ALGO" "$RSA_KEY_LENGTH" "$ADMIN_PIN_DEF"; then + whiptail_error_die "GPG Key factory reset failed!" fi - # If Nitrokey Storage is inserted, reset AES keys as well if [ "$DONGLE_BRAND" = "Nitrokey Storage" ] && [ -x /bin/hotp_verification ]; then STATUS "Resetting Nitrokey Storage AES keys" - hotp_verification regenerate ${ADMIN_PIN_DEF} - # see https://github.com/Nitrokey/heads/commit/397a46203bedcb77aeac24917e7fe254465128fb - STATUS "Restarting scdaemon to remove possible exclusive lock of dongle" + hotp_verification regenerate "${ADMIN_PIN_DEF}" release_scdaemon STATUS_OK "Nitrokey Storage AES keys reset" fi - # Toggle forced sig (good security practice, forcing PIN request for each signature request) - if gpg --card-status | grep "Signature PIN" | grep -q "not forced"; then - STATUS "Enabling forced signature PIN on smartcard" - { - echo admin # admin menu - echo forcesig # toggle forcesig - echo ${ADMIN_PIN_DEF} # local keyring PIN - } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ - >/tmp/gpg_card_edit_output 2>&1 - TRACE_FUNC - DEBUG "GPG forcesig toggle output: $(cat /tmp/gpg_card_edit_output)" - if [ $? -ne 0 ]; then - ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "GPG Key forcesig toggle on failed!\n\n$ERROR" - fi - STATUS_OK "Forced signature PIN enabled" - fi - - # use NIST P-256 for key generation if requested - if [ "$GPG_ALGO" = "p256" ]; then - STATUS "Setting NIST-P256 key attributes on $DONGLE_BRAND" - { - echo admin # admin menu - echo key-attr # key attributes - echo 2 # ECC - echo 3 # P-256 - echo ${ADMIN_PIN_DEF} # local keyring PIN - echo 2 # ECC - echo 3 # P-256 - echo ${ADMIN_PIN_DEF} # local keyring PIN - echo 2 # ECC - echo 3 # P-256 - echo ${ADMIN_PIN_DEF} # local keyring PIN - } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ - >/tmp/gpg_card_edit_output 2>&1 - TRACE_FUNC - DEBUG "GPG NIST-P256 key-attr output: $(cat /tmp/gpg_card_edit_output)" - if [ $? -ne 0 ]; then - ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "Setting key to NIST-P256 in $DONGLE_BRAND failed." - fi - STATUS_OK "NIST-P256 key attributes set on $DONGLE_BRAND" - # fallback to RSA key generation by default - elif [ "$GPG_ALGO" = "RSA" ]; then - STATUS "Setting RSA ${RSA_KEY_LENGTH}-bit key attributes on $DONGLE_BRAND (may take a minute)" - # Set RSA key length - { - echo admin - echo key-attr - echo 1 # RSA - echo ${RSA_KEY_LENGTH} #Signing key size set to RSA_KEY_LENGTH - echo ${ADMIN_PIN_DEF} #Local keyring PIN - echo 1 # RSA - echo ${RSA_KEY_LENGTH} #Encryption key size set to RSA_KEY_LENGTH - echo ${ADMIN_PIN_DEF} #Local keyring PIN - echo 1 # RSA - echo ${RSA_KEY_LENGTH} #Authentication key size set to RSA_KEY_LENGTH - echo ${ADMIN_PIN_DEF} #Local keyring PIN - } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ - >/tmp/gpg_card_edit_output 2>&1 - TRACE_FUNC - DEBUG "GPG RSA key-attr output: $(cat /tmp/gpg_card_edit_output)" - if [ $? -ne 0 ]; then - ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "Setting key attributed to RSA ${RSA_KEY_LENGTH} bits in $DONGLE_BRAND failed." - fi - STATUS_OK "RSA ${RSA_KEY_LENGTH}-bit key attributes set on $DONGLE_BRAND" - else - #Unknown GPG_ALGO - whiptail_error_die "Unknown GPG_ALGO: $GPG_ALGO" - fi - TRACE_FUNC } @@ -821,29 +621,11 @@ generate_OEM_gpg_keys() { } gpg_key_change_pin() { + # OEM wrapper: calls shared gpg_card_change_pin, dies on failure. TRACE_FUNC - # 1 = user PIN, 3 = admin PIN - PIN_TYPE=$1 - PIN_ORIG=${2} - PIN_NEW=${3} - # Change PIN - { - echo admin # admin menu - echo passwd # change PIN - echo ${PIN_TYPE} # 1 = user PIN, 3 = admin PIN - echo ${PIN_ORIG} # old PIN - echo ${PIN_NEW} # new PIN - echo ${PIN_NEW} # confirm new PIN - echo q # quit - echo q - } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit \ - >/tmp/gpg_card_edit_output 2>&1 - TRACE_FUNC - DEBUG "GPG PIN change output: $(cat /tmp/gpg_card_edit_output)" - if [ $? -ne 0 ]; then - ERROR=$(cat /tmp/gpg_card_edit_output | fold -s) - whiptail_error_die "GPG Key PIN change failed!\n\n$ERROR" + if ! gpg_card_change_pin "$1" "$2" "$3"; then + whiptail_error_die "GPG Key PIN change failed!" fi TRACE_FUNC @@ -1228,9 +1010,7 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then INFO "The chosen passphrase must be between 8 and $MAX_HOTP_GPG_PIN_LENGTH characters in length." - while [[ ${#CUSTOM_SINGLE_PASS} -lt 8 ]] || [[ ${#CUSTOM_SINGLE_PASS} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - INPUT "Enter the passphrase (8-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r CUSTOM_SINGLE_PASS - done + _read_pin "Enter the passphrase (8-${MAX_HOTP_GPG_PIN_LENGTH} chars):" 8 "$MAX_HOTP_GPG_PIN_LENGTH" CUSTOM_SINGLE_PASS TPM_PASS=${CUSTOM_SINGLE_PASS} USER_PIN=${CUSTOM_SINGLE_PASS} ADMIN_PIN=${CUSTOM_SINGLE_PASS} @@ -1256,23 +1036,17 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then fi if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then NOTE "NK3 Secrets app PIN / GPG Admin PIN: seals HOTP measurements and manages OpenPGP card. 3 attempts max. DO NOT FORGET. Recommended: 2 words" - while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - INPUT "Enter desired NK3 Secrets app PIN / GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r ADMIN_PIN - done + _read_pin "Enter desired NK3 Secrets app PIN / GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" 6 "$MAX_HOTP_GPG_PIN_LENGTH" ADMIN_PIN else NOTE "GPG Admin PIN: management tasks on $DONGLE_BRAND, seal measurements under HOTP. 3 attempts max, locks Admin out. DO NOT FORGET. Recommended: 2 words" - while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - INPUT "Enter desired GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r ADMIN_PIN - done + _read_pin "Enter desired GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" 6 "$MAX_HOTP_GPG_PIN_LENGTH" ADMIN_PIN fi #USER PIN not required in case of GPG_GEN_KEY_IN_MEMORY not requested of if GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD is # That is, if keys were NOT generated in memory (on smartcard only) or # if keys were generated in memory but are to be moved from local keyring to smartcard if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then NOTE "GPG User PIN: sign/encrypt content, sign hashes under Heads. 3 attempts max. DO NOT FORGET. Recommended: 2 words" - while [[ ${#USER_PIN} -lt 6 ]] || [[ ${#USER_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - INPUT "Enter desired GPG User PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r USER_PIN - done + _read_pin "Enter desired GPG User PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" 6 "$MAX_HOTP_GPG_PIN_LENGTH" USER_PIN fi # The user knows these passphrases, we don't need to # badger them to record them diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 2f5f6a15e..c372936ec 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -432,6 +432,23 @@ INPUT() { fi } +# Prompt until the entered value's length is within [min,max], then store it +# in the variable named by var (indirect assignment). Input is collected with +# `read -s` so secrets are not echoed. Used by every PIN/passphrase prompt +# with both a minimum and maximum length. +_read_pin() { + local prompt="$1" min="$2" max="$3" var="$4" + local val="" + while :; do + INPUT "$prompt" -r -s val + if [ -n "$val" ] && [ "${#val}" -ge "$min" ] && [ "${#val}" -le "$max" ]; then + break + fi + NOTE "Invalid length: must be $min-$max chars." + done + printf -v "$var" '%s' "$val" +} + # Filter known harmless LVM warning noise while preserving all other stderr. # Messages that are expected during device scanning (e.g. "not an LVM PV") are # redirected to the debug log only - they are not errors and should not appear @@ -2420,7 +2437,14 @@ update_checksums() { # Print the file and directory structure of /boot to caller's stdout print_tree() { TRACE_FUNC - find ./ ! -path './kexec*' -print0 | sort -z + DEBUG "print_tree: CWD=$(pwd)" + local _pt_tmp + # The fallback tree must byte-match the sorted regeneration done at + # verification time even when /tmp is unavailable. + _pt_tmp=$(mktemp) || { find ./ ! -path './kexec*' -print0 | sort -z; return; } + find ./ ! -path './kexec*' -print0 >"$_pt_tmp" + sort -z <"$_pt_tmp" + rm -f "$_pt_tmp" } # Escape zero-delimited standard input to safely display it to the user in e.g. @@ -2500,6 +2524,18 @@ assert_signable() { local user_out="/tmp/hash_output_mismatches" local add="Please investigate!" [ -f "$user_out" ] && add="Please investigate the following relative paths to /boot (where # are sanitized invalid characters):"$'\n'"$(cat "$user_out")" + # Name the offending entries so field reports are actionable: + # list every path containing bytes outside printable ASCII, or a + # backslash, escaped for safe display. + local bad_list="" _as_entry + while IFS= read -r -d '' _as_entry; do + case "$_as_entry" in + *[!\ -~]*|*\\*) + bad_list+="${bad_list:+$'\n'}$(printf '%s' "$_as_entry" | escape_zero '')" + ;; + esac + done /dev/null || true fi - return 1 } gpg_add_key_to_standalone_rom() { @@ -148,3 +147,714 @@ gpg_replace_key_reflash() { [ -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 + local admin_pin_file=/tmp/secret/gpg_admin_pin + + STATUS "Factory resetting $DONGLE_BRAND OpenPGP smartcard" + mkdir -p /tmp/secret + printf '%s' "$card_admin_pin" >"$admin_pin_file" + { + 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-file "$admin_pin_file" --card-edit \ + >/tmp/gpg_card_edit_output 2>&1 + rc=$? + shred -n 10 -z -u "$admin_pin_file" 2>/dev/null || rm -f "$admin_pin_file" + 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 on smartcard" + return 1 + else + STATUS_OK "Forced signature PIN enabled" + fi + fi + + 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_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 + local pin_file=/tmp/secret/gpg_admin_pin + local rc + + [ -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" + mkdir -p /tmp/secret + printf '%s' "$card_admin_pin" >"$pin_file" + { + 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 \ + --passphrase-file "$pin_file" --card-edit \ + >/tmp/gpg_card_edit_output 2>&1 || { + rc=$? + shred -n 10 -z -u "$pin_file" 2>/dev/null || rm -f "$pin_file" + return $rc + } + shred -n 10 -z -u "$pin_file" 2>/dev/null || rm -f "$pin_file" + + 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 +} + +# Standard reprovision failure exit: tear down the backup keyring and any +# mounted LUKS mapper, show the error dialog, and return failure to the menu. +_reprovision_fail() { + local title="$1" msg="$2" + _luks_cleanup + whiptail_error --title "ERROR: $title" --msgbox "$msg" 0 80 + return 1 +} + +_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 + # Wipe reprovision temp secrets so error paths (and DIE) do not leave + # PIN material behind, and drop the throwaway import keyring. + for f in /tmp/secret/gpg_pin /tmp/secret/backup_pass \ + /tmp/secret/gpg_admin_pin /tmp/secret/gpg_backup_passphrase; do + [ -e "$f" ] || continue + shred -n 10 -z -u "$f" 2>/dev/null || rm -f "$f" + done + rm -rf /tmp/secret/reprovision_gnupghome 2>/dev/null || true + # Remove any master secret key material from the live keyring so it is + # not left on disk on ANY exit path (success or error). Idempotent. + find "${GNUPGHOME:-$HOME/.gnupg}/private-keys-v1.d" \ + -name '*.key' -delete 2>/dev/null || true +} + +reprovision_smartcard_from_backup() { + TRACE_FUNC + local admin_pin key_algo rsa_key_length key_name key_email key_comment + local card_admin_pin key_id uid_line + local algo_code bit_len + local reprovision_gnupghome=/tmp/secret/reprovision_gnupghome + local backup_pin_file=/tmp/secret/gpg_backup_passphrase + + 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 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. + release_scdaemon + mkdir -p /tmp/secret + printf '%s' "$admin_pin" >/tmp/secret/backup_pass + STATUS "Mounting GPG key backup (LUKS private partition)" + if ! mount-usb.sh --mode ro --mountpoint /media --pass-file /tmp/secret/backup_pass; then + DEBUG "Could not mount backup LUKS partition" + _reprovision_fail 'Backup Mount Failed' \ + "Could not mount the backup USB drive.\n\nVerify that the correct backup drive is inserted\nand the passphrase is correct." + 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 + WARN "privkey.sec not found on backup drive -- not a valid GPG key backup" + _reprovision_fail 'No Backup Found' \ + "No privkey.sec found on this drive.\n\nThis does not appear to be a valid\nGPG key backup drive." + return 1 + fi + + # Phase 3: import the private key (master + subkeys) into a throwaway + # GNUPGHOME. The running (ROM-loaded) keyring is left untouched until + # the user confirms the destructive reprovision, so a bad passphrase or + # import error can never leave an empty live keyring behind. + mkdir -p "$reprovision_gnupghome" + chmod 700 "$reprovision_gnupghome" 2>/dev/null || true + mkdir -p /tmp/secret + printf '%s' "$admin_pin" >"$backup_pin_file" + STATUS "Importing GPG keys from backup" + if ! GNUPGHOME="$reprovision_gnupghome" gpg --pinentry-mode=loopback \ + --passphrase-file "$backup_pin_file" \ + --import-options restore --import /media/privkey.sec >/dev/null 2>/tmp/gpg_import_err; then + ERROR="$(cat /tmp/gpg_import_err)" + WARN "GPG key import from backup failed: $(head -3 /tmp/gpg_import_err 2>/dev/null)" + _reprovision_fail 'Key Import Failed' \ + "Failed to import GPG keys from backup.\n\n${ERROR}" + return 1 + fi + shred -n 10 -z -u "$backup_pin_file" 2>/dev/null || rm -f "$backup_pin_file" + DEBUG "privkey.sec imported into throwaway keyring successfully" + STATUS_OK "GPG keys imported" + + # Phase 4: detect key type and extract identity from the imported key. + algo_code="$(GNUPGHOME="$reprovision_gnupghome" gpg --with-colons --list-keys 2>/dev/null | grep '^pub:' | cut -d: -f4 | head -1)" || true + bit_len="$(GNUPGHOME="$reprovision_gnupghome" gpg --with-colons --list-keys 2>/dev/null | grep '^pub:' | cut -d: -f3 | head -1)" || true + uid_line="$(GNUPGHOME="$reprovision_gnupghome" gpg --with-colons --list-keys 2>/dev/null | grep '^uid:' | head -1 | cut -d: -f10)" || true + + 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" + ;; + *) + _reprovision_fail 'Unknown Key Type' \ + "Could not detect the key type from the backup\n(algorithm $algo_code).\n\nThe backup file may be corrupted." + 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 + # UID format is "Given Surname (comment) "; strip any leftover + # portion from key_name so it never lands in the cardholder + # name fields when no comment parens were present. + key_name="$(echo "$key_name" | sed 's/ *<[^>]*>$//')" + key_email="$(echo "$uid_line" | grep -o '<[^>]*>' | tr -d '<>' | head -1)" || true + [ -z "$key_name" ] && key_name="$uid_line" + + if [ -n "$key_email" ]; then + key_id="$key_email" + else + key_id="$(GNUPGHOME="$reprovision_gnupghome" gpg --list-secret-keys --with-colons 2>/dev/null | grep '^sec:' | cut -d: -f5 | head -1)" || true + [ -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="$(GNUPGHOME="$reprovision_gnupghome" 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 4b: swap the validated backup keyring into the live keyring now + # that the user confirmed the destructive reprovision. Deferred so a bad + # passphrase/import or a declined confirmation never leaves the running + # keyring empty or polluted. + rm -rf /.gnupg + mkdir -p /.gnupg + cp -a "$reprovision_gnupghome"/. /.gnupg/ 2>/dev/null || { + _luks_cleanup + DIE "Failed to activate imported keyring" + } + chmod 700 /.gnupg 2>/dev/null || true + rm -rf "$reprovision_gnupghome" 2>/dev/null || true + DEBUG "Activated imported keyring in ~/.gnupg" + + # 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 + 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" + _reprovision_fail 'Factory Reset Failed' \ + "Could not factory reset the $DONGLE_BRAND smartcard.\n\n${ERROR}\n\nCheck that the admin PIN is correct." + 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 + ERROR="$(cat /tmp/gpg_card_edit_output)" + WARN "GPG keytocard operation failed: $(head -3 /tmp/gpg_card_edit_output 2>/dev/null)" + _reprovision_fail 'Keytocard Failed' \ + "Failed to move subkeys to smartcard.\n\n${ERROR}" + 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" || WARN "Could not set smartcard identity" + + # 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" + # The admin PIN is capped at MAX_HOTP_GPG_PIN_LENGTH (25) for every + # dongle brand: on Nitrokey 3 it doubles as the Secrets app PIN, + # which hotp_verification rejects past that length. + local pin_max=25 + + 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, 8-${pin_max} chars." + _read_pin "Enter new ${pin_label_admin} (8-${pin_max} chars):" 8 "$pin_max" new_admin_pin + if ! gpg_card_change_pin 3 "12345678" "$new_admin_pin"; then + ERROR="$(cat /tmp/gpg_card_edit_output | fold -s)" + _reprovision_fail 'Admin PIN Change Failed' \ + "Could not change the Admin PIN.\n\n${ERROR}" + return 1 + fi + + release_scdaemon + if gpg_reset_nk3_secret_app "$new_admin_pin"; then + STATUS_OK "${pin_label_admin} changed" + else + WARN "GPG Admin PIN changed, but the NK3 Secrets app PIN update failed." + WARN "HOTP will need the default PIN (12345678) until the Secrets app is reset." + fi + + NOTE "GPG User PIN: signing /boot and encryption, 3 attempts max.\nRecommended: 2 diceware words (6-25 chars)" + _read_pin "Enter new GPG User PIN (6-25 chars):" 6 25 new_user_pin + if ! gpg_card_change_pin 1 "123456" "$new_user_pin"; then + ERROR="$(cat /tmp/gpg_card_edit_output | fold -s)" + _reprovision_fail 'User PIN Change Failed' \ + "Could not change the User PIN.\n\n${ERROR}" + return 1 + fi + STATUS_OK "GPG User PIN changed" + printf '%s' "$new_user_pin" >/tmp/secret/gpg_pin + else + DEBUG "User declined custom PINs; keeping factory defaults" + printf '%s' "123456" >/tmp/secret/gpg_pin + 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 + # Reprovisioning is an ownership-level operation: it must leave a + # freshly-owned TPM with a valid rollback counter and + # kexec_rollback.txt rather than punting a manual "Reset the TPM" + # step to the user. The reset cannot be skipped: counter creation + # defines an NV index with owner-hierarchy auth, so against a TPM + # owned with an old/unknown passphrase it would loop on + # authorization failures (0x9a2). prompt_new_owner_password caches + # the new passphrase under /tmp/secret/tpm_owner_passphrase, which + # _tpm_auth_retry then picks up as the owner auth. + if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then + local tpm_reset_ok="n" + prompt_new_owner_password + if tpmr.sh reset "$tpm_owner_passphrase" >/dev/null 2>/tmp/tpm_reset_err; then + tpm_reset_ok="y" + # Counter references are invalid once the TPM is re-owned; + # drop the stale rollback file so kexec-sign-config.sh -r + # creates a fresh counter instead of trying to increment a + # dead one. The primary-handle hash is refreshed by + # update_checksums() before signing. + rm -f /boot/kexec_rollback.txt /boot/kexec_primhdl_hash.txt 2>/dev/null + else + WARN "Unable to reset TPM: $(tail -n 1 /tmp/tpm_reset_err 2>/dev/null | fold -s)" + fi + if [ "$tpm_reset_ok" != "y" ]; then + # Without a successful reset no rollback counter can be + # created. Keep /boot exactly as-is (the old signature + # still covers it) and bail out; the user can retry after + # a GUI TPM reset. + mount -o ro,remount /boot 2>/dev/null || true + _luks_cleanup + WARN "TPM reset failed; /boot signatures were NOT updated and no ROM flash will be performed." + WARN "Reset the TPM via Options -> TPM/TOTP/HOTP Options -> Reset the TPM, then reprovision again." + return 1 + fi + fi + + # Hash + sign atomically through the canonical master path: + # update_checksums() -> kexec-sign-config.sh -p /boot -u [-r]. + # All writes land in a staging directory under /tmp and move into + # /boot only after signing AND check_config verification succeed, + # so a failure leaves the previous manifests untouched. + if update_checksums; then + STATUS_OK "/boot files signed and ready" + # A fresh counter + rollback file resolves the reason the + # preflight set the reset-required marker; clear it so later + # dialogs this session reflect reality. + rm -f "$(tpm_reset_required_marker_path)" 2>/dev/null || true + else + mount -o ro,remount /boot 2>/dev/null || true + _luks_cleanup + WARN "/boot signing failed; the smartcard was reprovisioned but /boot" + WARN "signatures were NOT updated and no ROM flash will be performed." + WARN "Re-sign /boot via Options -> Update checksums and sign all files in /boot." + return 1 + fi + 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-Fa-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 [[ "$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 + + 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" + # Phase 8 resets the TPM and signs /boot atomically before we get here; + # any failure returns early, so a plain reboot prompt is always correct. + local reboot_msg + reboot_msg="The $DONGLE_BRAND smartcard has been reprovisioned\nfrom the GPG key backup.\n\nYou should reboot to finalize.\n\nReboot now?" + if whiptail_warning --title 'Reboot?' --yesno "$reboot_msg" 0 80; then + DEBUG "User accepted reboot" + /bin/reboot.sh + fi + DEBUG "User declined reboot" + + unset admin_pin card_admin_pin + TRACE_FUNC +} diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index 6c7547a26..66f0b4612 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -419,7 +419,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" @@ -458,8 +462,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 @@ -473,8 +482,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) @@ -483,6 +493,12 @@ report_integrity_measurements() { INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh return 0 ;; + K) + # Benign returns (user decline, failures) must not kill a + # set -e caller; stay in the calling menu instead. + reprovision_smartcard_from_backup || true + return 0 + ;; c | *) return 0 ;; @@ -493,10 +509,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) @@ -514,6 +531,12 @@ report_integrity_measurements() { INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh return 0 ;; + K) + # Benign returns (user decline, failures) must not kill a + # set -e caller; stay in the calling menu instead. + reprovision_smartcard_from_backup || true + return 0 + ;; *) return 0 ;;