diff --git a/doc/architecture.md b/doc/architecture.md index 4b52a5992..63f2d7bbf 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -53,15 +53,15 @@ execution. Source lives in `initrd/`. | Subsystem | Key files | Purpose | | --- | --- | --- | -| Init / boot flow | `initrd/init`, `initrd/bin/gui-init` | System initialization and main GUI loop | -| TPM abstraction | `initrd/bin/tpmr` | Unified TPM 1.2 / TPM 2.0 wrapper | -| Boot signing | `initrd/bin/kexec-sign-config` | GPG-sign /boot files, create checksums | -| Boot verification | `initrd/bin/kexec-select-boot` | Verify checksums, select and kexec the OS | -| LUKS key sealing | `initrd/bin/kexec-seal-key` | Seal disk encryption key to TPM | -| TOTP/HOTP | `initrd/bin/seal-totp`, `seal-hotpkey` | Seal attestation secrets to TPM | -| OEM reset | `initrd/bin/oem-factory-reset` | Full re-ownership: GPG, TPM, TOTP, checksums | +| Init / boot flow | `initrd/init`, `initrd/bin/gui-init.sh` | System initialization and main GUI loop | +| TPM abstraction | `initrd/bin/tpmr.sh` | Unified TPM 1.2 / TPM 2.0 wrapper | +| Boot signing | `initrd/bin/kexec-sign-config.sh` | GPG-sign /boot files, create checksums | +| Boot verification | `initrd/bin/kexec-select-boot.sh` | Verify checksums, select and kexec the OS | +| LUKS key sealing | `initrd/bin/kexec-seal-key.sh` | Seal disk encryption key to TPM | +| TOTP/HOTP | `initrd/bin/seal-totp.sh`, `initrd/bin/seal-hotpkey.sh` | Seal attestation secrets to TPM | +| OEM reset | `initrd/bin/oem-factory-reset.sh` | Full re-ownership: GPG, TPM, TOTP, checksums | | Config GUI | `initrd/bin/config-gui.sh` | Runtime configuration menus | -| Functions lib | `initrd/etc/functions` | Shared utilities: logging, INPUT, TPM helpers | +| Functions lib | `initrd/etc/functions.sh` | Shared utilities: logging, INPUT, TPM helpers | | GUI lib | `initrd/etc/gui_functions` | Whiptail wrappers, integrity report | --- @@ -74,7 +74,7 @@ Three-layer hierarchy: 2. **`/etc/config.user`** — User overrides extracted from CBFS at runtime 3. **`/tmp/config`** — Combined result, sourced during boot -`combine_configs()` in `initrd/etc/functions` merges these by concatenating +`combine_configs()` in `initrd/etc/functions.sh` merges these by concatenating `/etc/config*` into `/tmp/config`. User settings in CBFS take precedence because they appear last in the concatenation. diff --git a/doc/config.md b/doc/config.md index 223ea8043..0bbace935 100644 --- a/doc/config.md +++ b/doc/config.md @@ -42,7 +42,7 @@ These are not intended to be changed in user config. | Variable | Purpose | |---|---| -| CONFIG_BOARD | Internal name of the board being built. Avoid testing this for specific boards in initrd/, instead add a customization point and override it with boards//initrd/bin/. (For example, boards/librem_mini_v2/initrd/bin/board-init.sh.) | +| CONFIG_BOARD | Internal name of the board being built. Avoid testing this for specific boards in initrd/, instead add a customization point and override it with boards//initrd/bin/.sh. (For example, boards/librem_mini_v2/initrd/bin/board-init.sh.) | | CONFIG_BOARD_NAME | Display name of the board being built. Use this to show the board name to the user. | | CONFIG_BRAND_NAME | Brand name to use to refer to the firmware itself. Upstream, this is "Heads". For example, "Heads main menu", "Enable Heads debug tracing", etc. Distributions can override this to their specific brand name (usually in site-local/config). | diff --git a/doc/tpm.md b/doc/tpm.md index 2713ae002..012b0953f 100644 --- a/doc/tpm.md +++ b/doc/tpm.md @@ -9,9 +9,9 @@ See also: [architecture.md](architecture.md), [boot-process.md](boot-process.md) ## tpmr — unified TPM abstraction -`initrd/bin/tpmr` is a shell script wrapper that presents a single interface +`initrd/bin/tpmr.sh` is a shell script wrapper that presents a single interface over both TPM 1.2 (`tpm` / `trousers`) and TPM 2.0 (`tpm2-tools`). All Heads -scripts call `tpmr` rather than invoking `tpm` or `tpm2` directly. +scripts call `tpmr.sh` rather than invoking `tpm` or `tpm2` directly. ### PCR sizes @@ -155,11 +155,11 @@ unchanged; the TXT mechanism adds the DRTM capability on top of it. | 1 | unused | Zero; anchored in sealing policies | | 2 | coreboot SRTM | Boot block, ROM stage, RAM stage, Heads Linux kernel + initrd | | 3 | unused | Zero; anchored in sealing policies | -| 4 | Heads (`usb-init`, `kexec-insert-key`, `functions`) | Boot mode tracking: `"usb"` during USB init, `"generic"` after DUK unsealed, `"recovery"` when recovery shell entered | +| 4 | Heads (`usb-init.sh`, `kexec-insert-key.sh`, `initrd/etc/functions.sh`) | Boot mode tracking: `"usb"` during USB init, `"generic"` after DUK unsealed, `"recovery"` when recovery shell entered | | 5 | Heads `insmod` wrapper | Each loaded kernel module: parameters + binary content (default `MODULE_PCR=5`) | -| 6 | Heads `qubes-measure-luks` | LUKS header dump for each encrypted drive | -| 7 | Heads `cbfs-init`, `uefi-init` | Each CBFS/UEFI file: filename then content (default `CONFIG_PCR=7`) — covers `config.user`, GPG keyring, user CBFS files | -| 16 | `tpmr calcfuturepcr` (scratch use only) | Resettable debug PCR used as scratch pad during pre-computation of future PCR values; not part of any sealing policy | +| 6 | Heads `qubes-measure-luks.sh` | LUKS header dump for each encrypted drive | +| 7 | Heads `cbfs-init.sh`, `uefi-init.sh` | Each CBFS/UEFI file: filename then content (default `CONFIG_PCR=7`) — covers `config.user`, GPG keyring, user CBFS files | +| 16 | `tpmr.sh calcfuturepcr` (scratch use only) | Resettable debug PCR used as scratch pad during pre-computation of future PCR values; not part of any sealing policy | PCRs 0-3 are read at seal time and included in sealing policies. The zero state of PCRs 0, 1, and 3 is intentional — any unexpected extension of those @@ -214,7 +214,7 @@ ROM configuration integrity, not disk state. ## PCR extension -`tpmr extend -ix -ic ` extends a PCR with the hash of a +`tpmr.sh extend -ix -ic ` extends a PCR with the hash of a string. `-if ` extends with the hash of a file. `calcfuturepcr` replays the expected extend sequence to compute what a PCR @@ -224,7 +224,7 @@ after normal init, before any recovery shell entry). ### Recovery PCR extension -When a recovery shell is entered, `initrd/etc/functions` extends PCR 4 with +When a recovery shell is entered, `initrd/etc/functions.sh` extends PCR 4 with the string `"recovery"`. This permanently invalidates TOTP and LUKS DUK unsealing for the rest of the boot session — the TPM will refuse to unseal secrets that were sealed against the normal-boot PCR 4 value. @@ -283,11 +283,11 @@ validates the counter is readable from TPM, ensuring secrets can actually be unsealed. The counter is created during OEM Factory Reset by `check_tpm_counter` in -`initrd/etc/functions`. +`initrd/etc/functions.sh`. ### Counter state file -`read_tpm_counter` in `initrd/etc/functions` reads the counter from the TPM +`read_tpm_counter` in `initrd/etc/functions.sh` reads the counter from the TPM and writes the result to `/tmp/counter-`. The format is `: `. @@ -324,9 +324,9 @@ counter passes preflight or the user chooses to continue. ### Pipeline safety -`tpmr counter_read` must be called with a direct redirect, not piped through -`tee`. Piping through `tee` hides `tpmr` failures because `||` checks the -exit status of `tee` (always 0), not `tpmr`. See +`tpmr.sh counter_read` must be called with a direct redirect, not piped through +`tee`. Piping through `tee` hides `tpmr.sh` failures because `||` checks the +exit status of `tee` (always 0), not `tpmr.sh`. See [ux-patterns.md](ux-patterns.md#tpm-counter-patterns) for the correct pattern. --- @@ -359,13 +359,13 @@ policy, or investigating why a seal/unseal operation fails. | --- | --- | --- | | Which coreboot PCRs are active on a board | `config/coreboot-.config` | `CONFIG_PCR_SRTM`, `CONFIG_TPM_INIT_RAMSTAGE`, `CONFIG_TPM_MEASURED_BOOT_INIT_BOOTBLOCK`, `CONFIG_INTEL_TXT` | | Which coreboot version / fork a board uses | `modules/coreboot` + `boards//` | `CONFIG_COREBOOT_VERSION` in board config selects the coreboot source defined in `modules/coreboot` | -| LUKS DUK sealing policy (which PCRs) | `initrd/bin/kexec-seal-key` | `tpmr seal` call and surrounding `pcrread` / `calcfuturepcr` calls; DEBUG comments explain each PCR | -| TOTP/HOTP sealing policy (which PCRs) | `initrd/bin/seal-totp` | `tpmr seal` call; DEBUG messages explain why PCR 5 and PCR 6 are excluded | -| PCR 4 (boot mode) tracking | `initrd/bin/usb-init`, `initrd/bin/kexec-insert-key`, `initrd/etc/functions` | `tpmr extend` calls with `"usb"`, `"generic"`, `"recovery"` | +| LUKS DUK sealing policy (which PCRs) | `initrd/bin/kexec-seal-key.sh` | `tpmr.sh seal` call and surrounding `pcrread` / `calcfuturepcr` calls; DEBUG comments explain each PCR | +| TOTP/HOTP sealing policy (which PCRs) | `initrd/bin/seal-totp.sh` | `tpmr.sh seal` call; DEBUG messages explain why PCR 5 and PCR 6 are excluded | +| PCR 4 (boot mode) tracking | `initrd/bin/usb-init.sh`, `initrd/bin/kexec-insert-key.sh`, `initrd/etc/functions.sh` | `tpmr.sh extend` calls with `"usb"`, `"generic"`, `"recovery"` | | PCR 5 (kernel modules) | `initrd/sbin/insmod` | `MODULE_PCR` variable; default `MODULE_PCR=5`; each `insmod` extends PCR 5 | -| PCR 6 (LUKS header) | `initrd/bin/qubes-measure-luks` | `tpmr extend` call against `/tmp/luksDump.txt` | -| PCR 7 (CBFS / ROM files) | `initrd/bin/cbfs-init`, `initrd/bin/uefi-init` | `CONFIG_PCR` variable; default `CONFIG_PCR=7`; each extracted file extends PCR 7 | -| Rollback counter logic | `initrd/etc/functions` | `check_tpm_counter`, `read_tpm_counter`, `counter_increment` | +| PCR 6 (LUKS header) | `initrd/bin/qubes-measure-luks.sh` | `tpmr.sh extend` call against `/tmp/luksDump.txt` | +| PCR 7 (CBFS / ROM files) | `initrd/bin/cbfs-init.sh`, `initrd/bin/uefi-init.sh` | `CONFIG_PCR` variable; default `CONFIG_PCR=7`; each extracted file extends PCR 7 | +| Rollback counter logic | `initrd/etc/functions.sh` | `check_tpm_counter`, `read_tpm_counter`, `counter_increment` | ### Adding a new board diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index 7c7164e7b..99bb2acae 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -191,11 +191,19 @@ prompt_update_checksums() { --yesno "You have chosen to update the checksums and sign all of the files in /boot.\n\nThis means that you trust that these files have not been tampered with.\n\nYou will need your GPG key available, and this change will modify your disk.\n\nDo you want to continue?" 0 80); then if update_checksums; then return 0 + fi + # update_checksums may have set the TPM-reset-required marker + # during its execution (e.g. check_tpm_counter hit "out of + # resources"). Show the targeted TPM message instead of the + # generic failure so the user knows exactly what to do. + if tpm_reset_required; then + whiptail_error --title 'TPM Reset Required' \ + --msgbox "Cannot sign /boot: TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM), then update checksums." 0 80 else whiptail_error --title 'ERROR' \ --msgbox "Failed to update checksums / sign default config" 0 80 - return 1 fi + return 1 fi return 1 } @@ -411,8 +419,13 @@ EOF skip_to_menu="true" return 1 ;; + # "Reset the TPM" from the TOTP failure whiptail menu. + # The gate runs first to verify /boot integrity. If the gate + # fails *because* TPM reset is required (e.g. stale counters), + # the || tpm_reset_required bypass lets reset_tpm() proceed — + # it clears counters and creates a fresh one. p) - if gate_reseal_with_integrity_report && reset_tpm && update_totp && BG_COLOR_MAIN_MENU="normal"; then + if { gate_reseal_with_integrity_report || tpm_reset_required; } && reset_tpm && update_totp && BG_COLOR_MAIN_MENU="normal"; then reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action fi ;; @@ -806,8 +819,11 @@ show_tpm_totp_hotp_options_menu() { update_totp && update_hotp || true fi ;; + # "Reset the TPM" from the TPM/TOTP/HOTP options whiptail menu. + # Same gate-bypass pattern: if the gate fails because TPM + # reset is required, proceed to reset_tpm() anyway. r) - if gate_reseal_with_integrity_report && reset_tpm; then + if { gate_reseal_with_integrity_report || tpm_reset_required; } && reset_tpm; then reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action fi ;; @@ -837,7 +853,20 @@ reset_tpm() { return 1 fi - tpmr.sh reset "$tpm_owner_passphrase" + # Verify TPM reset succeeded before proceeding to counter + # creation, signing, TOTP generation, and DUK resealing. + # A failed reset would leave the TPM in an inconsistent state + # (old passphrase with unknown PCRs), causing confusing errors + # downstream. Show the actual error to the user and return + # to the menu. + local reset_err_file=$(mktemp) + if ! tpmr.sh reset "$tpm_owner_passphrase" >"$reset_err_file" 2>&1; then + ERROR=$(tail -n 1 "$reset_err_file" | fold -s) + rm -f "$reset_err_file" + whiptail_error --title 'ERROR' \ + --msgbox "Error resetting TPM:\n\n${ERROR}" 0 80 + return 1 + fi # now that the TPM is reset, remove invalid TPM counter files mount_boot @@ -951,13 +980,10 @@ force_unsafe_boot() { TRACE_FUNC if [ -x /bin/hotp_verification ]; then - enable_usb + # HOTP required by board config, always detect branding + detect_usb_security_dongle_branding fi -# Detect dongle branding from USB VID:PID -- must run AFTER enable_usb so lsusb -# can see the dongle (NK3 enumerates ~1 second after USB module load). -detect_usb_security_dongle_branding - if detect_boot_device; then # /boot device with installed OS found clean_boot_check diff --git a/initrd/bin/oem-factory-reset.sh b/initrd/bin/oem-factory-reset.sh index 32539f1da..4d67d1d96 100755 --- a/initrd/bin/oem-factory-reset.sh +++ b/initrd/bin/oem-factory-reset.sh @@ -23,8 +23,8 @@ rm -f /tmp/hotpkey_fw_shown TRACE_FUNC -# Enable USB and detect branding early — $DONGLE_BRAND is used throughout this script. -enable_usb +# Detect branding early — $DONGLE_BRAND is used throughout this script. +# enable_usb is called internally by detect_usb_security_dongle_branding. detect_usb_security_dongle_branding # use TERM to exit on error @@ -1005,9 +1005,8 @@ set_default_boot_option() { usb_security_token_capabilities_check() { TRACE_FUNC - enable_usb - # Always detect dongle branding from USB VID:PID — never read a stored file. + # enable_usb is called internally by detect_usb_security_dongle_branding. detect_usb_security_dongle_branding DEBUG "USB Security dongle detected: $DONGLE_BRAND" # Only show generic "Detected" if no specific brand was identified diff --git a/initrd/bin/seal-hotpkey.sh b/initrd/bin/seal-hotpkey.sh index b73ed9dee..e405fb7a1 100755 --- a/initrd/bin/seal-hotpkey.sh +++ b/initrd/bin/seal-hotpkey.sh @@ -48,9 +48,7 @@ mount_boot || exit 1 counter_value=1 -enable_usb - -# Detect branding after USB is up so lsusb can see the device. +# Detect branding (enable_usb is called internally) detect_usb_security_dongle_branding DEBUG "$DONGLE_BRAND detected via USB VID:PID" diff --git a/initrd/bin/tpmr.sh b/initrd/bin/tpmr.sh index 264a15ffa..9dd7609a3 100755 --- a/initrd/bin/tpmr.sh +++ b/initrd/bin/tpmr.sh @@ -1,5 +1,17 @@ #!/bin/bash # TPM Wrapper - to unify tpm and tpm2 subcommands +# +# NOTE: tpmtotp C code (counter_create.c, unsealfile.c, sealfile2.c, etc.) +# prints ALL output (both success and error messages) via printf() to stdout, +# NOT stderr. This is a quirk of the tpmtotp toolkit. When capturing +# output or errors from tpm commands, we must capture stdout (not just stderr). +# +# TPM2 error codes caught by auth-retry grep patterns. +# Reference: TCG TPM2 Part 2 (Structures), Table 18 — TPM_RC (Response Codes) +# https://trustedcomputinggroup.org/resource/tpm-library-specification/ +# 0x98e TPM_RC_AUTH_FAIL — wrong password, retry allowed +# 0x149 TPM_RC_AUTH_UNAVAILABLE — auth handle wrong for entity (e.g. owner +# hierarchy auth vs. NV index auth with authwrite attribute) . /etc/functions.sh @@ -279,10 +291,21 @@ tpm2_counter_read() { echo "$index: $hex_val" } +# tpm2_counter_inc - Increment a TPM2 counter. +# +# Auth behaviour: +# -pwdc "" : try bare nvincrement (index auth — counters created by +# nvdefine have empty NV index auth). On failure, prompt for +# owner passphrase and retry with owner auth up to 3 times. +# -pwdc : use owner auth — retry up to 3 times on auth failure, +# re-prompting passphrase. +# (no -pwdc) : try bare nvincrement first, then prompt and retry as above. +# +# Retry is consistent with tpm2_seal and tpm2_counter_create which also +# re-prompt and retry on authorization failures. tpm2_counter_inc() { TRACE_FUNC - local index pwd - local inc_args=() + local index pass="" while true; do case "$1" in -ix) @@ -290,7 +313,7 @@ tpm2_counter_inc() { shift 2 ;; -pwdc) - pwd="$2" + pass="$2" shift 2 ;; *) @@ -298,100 +321,157 @@ tpm2_counter_inc() { ;; esac done - if [ -n "$pwd" ]; then - inc_args=(-C o -P "$(tpm2_password_hex "$pwd")") + # Try bare nvincrement first (index auth, no owner passphrase needed). + # This is a pre-check before the owner-auth retry loop and is not + # counted against the 3-attempt budget below. + if tpm2 nvincrement "0x$index" 2>/dev/null >/dev/console; then + local hex_val + hex_val="$(tpm2 nvread 0x$index | xxd -pc8)" || return 1 + echo "$index: $hex_val" + return 0 fi - tpm2 nvincrement "${inc_args[@]}" "0x$index" >/dev/console || return 1 - local hex_val - hex_val="$(tpm2 nvread 0x"$index" | xxd -pc8)" || return 1 - echo "$index: $hex_val" -} -tpm1_counter_create() { - TRACE_FUNC + # Retry with owner auth (up to 3 attempts total). + # If -pwdc was provided, it's cached as tpm_owner_passphrase; + # otherwise prompt_tpm_owner_password will prompt and cache. local attempt=0 - while true; do + while [ $attempt -lt 3 ]; do attempt=$((attempt + 1)) - prompt_tpm_owner_password - tpm_owner_passphrase="$(cat /tmp/secret/tpm_owner_passphrase)" - TMP_ERR_FILE=$(mktemp) - if tpm counter_create -pwdo "$tpm_owner_passphrase" "$@" 2>"$TMP_ERR_FILE"; then - rm -f "$TMP_ERR_FILE" + if [ -n "$pass" ]; then + tpm_owner_passphrase="$pass" + else + prompt_tpm_owner_password + fi + local tmp_err_file=$(mktemp) + if tpm2 nvincrement -C o -P "$(tpm2_password_hex "$tpm_owner_passphrase")" "0x$index" 2>"$tmp_err_file" >/dev/console; then + rm -f "$tmp_err_file" + local hex_val + hex_val="$(tpm2 nvread 0x$index | xxd -pc8)" || return 1 + echo "$index: $hex_val" return 0 fi - tmp_err_content="$(cat "$TMP_ERR_FILE")" - rm -f "$TMP_ERR_FILE" - DEBUG "Failed attempt $attempt to create counter from tpm1_counter_create. Stderr: $tmp_err_content" - shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase - if echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission'; then - if [ "$attempt" -ge 3 ]; then - DIE "Unable to create counter from tpm1_counter_create after 3 attempts. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." - fi - WARN "Counter creation failed (bad passphrase?). Retrying..." - else - DIE "Unable to create counter from tpm1_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + local tmp_err_content="$(cat "$tmp_err_file")" + rm -f "$tmp_err_file" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase 2>/dev/null || true + DEBUG "tpm2_counter_inc attempt $attempt failed. Stderr: $tmp_err_content" + if ! echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission|0x98e|0x149'; then + DIE "Can't increment TPM counter for $index, access denied." fi + WARN "Authentication failed, retrying..." done + DIE "Can't increment TPM counter for $index after 3 attempts, access denied." } -tpm2_counter_create() { - TRACE_FUNC - pass="" # owner passphrase from argument - label="" # label argument - while true; do - case "$1" in - -pwdc) - pass="$2" - shift 2 - ;; - -la) - label="$2" - shift 2 - ;; - *) - break - ;; - esac - done - - rand_index="1$(dd if=/dev/urandom bs=1 count=3 2>/dev/null | xxd -pc3)" +# _tpm_auth_retry - Shared retry helper for TPM commands needing owner auth. +# +# Handles both TPM1 (tpmtotp: errors to stdout, uses -pwdo/-pwdc flags) +# and TPM2 (tpm2-tools: errors to stderr, uses -P parameter). +# +# Caching: prompt_tpm_owner_password reuses cached passphrase if available. +# On auth failure the cache is shredded; next prompt will ask the user. +# +# Usage: _tpm_auth_retry