From 4243e7f83afb8a90bf6e360fe2d394951696b635 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Mon, 27 Apr 2026 15:30:20 -0400 Subject: [PATCH 1/5] Wait for USB security dongle VID before branding detection - initrd/etc/functions.sh: Add sysfs polling loop (max 3s) to wait for known dongle VIDs (20a0, 316d, 16d0, 1050) before lsusb branding detection - initrd/etc/gui_functions.sh: Add STATUS_OK for signing key verification when dongle key matches ROM-trusted key in integrity report Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 20 ++++++++++++++++++++ initrd/etc/gui_functions.sh | 5 ++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 20802909f..d65c66d27 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -514,6 +514,26 @@ detect_usb_security_dongle_branding() { enable_usb [ "$usb_was_enabled" != "y" ] && wait_for_usb_devices + # Wait for known USB dongle VID to appear in sysfs (max 3s). + # Known VIDs: 20a0 (Nitrokey/Canokey QEMU), 316d (Librem Key), 16d0 (Canokey), 1050 (Yubikey) + local start_dongle_wait=$(awk '{print $1}' /proc/uptime 2>/dev/null) + while :; do + if ls /sys/bus/usb/devices/*/idVendor 2>/dev/null | \ + xargs grep -l -E "20a0|316d|16d0|1050" 2>/dev/null | grep -q .; then + DEBUG "USB security dongle VID detected in sysfs" + break + fi + + # Timeout after 3 seconds + local now=$(awk '{print $1}' /proc/uptime 2>/dev/null) + if [ -n "$now" ] && [ -n "$start_dongle_wait" ]; then + if awk -v s="$start_dongle_wait" -v n="$now" 'BEGIN{exit (n - s > 3.0) ? 0 : 1}'; then + DEBUG "Timeout waiting for USB security dongle VID after 3s" + break + fi + fi + done + # If branding is already specific, USB is now ready and no re-scan is needed. [ "$DONGLE_BRAND" != "USB Security dongle" ] && [ -n "$DONGLE_BRAND" ] && return local lsusb_out diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index 871f006c9..a5e77391c 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -253,8 +253,8 @@ report_integrity_measurements() { DEBUG "integrity report generated at $date_now" STATUS "Preparing Measured Integrity Report - hashing and verifying /boot" - # Detect branding and initialize USB (detect_usb_security_dongle_branding calls - # enable_usb internally and guards against redundant re-detection). + # Enable USB first for proper branding detection (user-initiated, won't break DUK unseal) + enable_usb detect_usb_security_dongle_branding if [ "$CONFIG_TPM" = "y" ]; then @@ -354,7 +354,6 @@ report_integrity_measurements() { # Check signing key: try card immediately (USB already up); only prompt if not accessible. # wait_for_gpg_card sets global gpg_output to the card-status output on success. STATUS "Verifying signing key on $DONGLE_BRAND" - enable_usb gpg_output="" local _card_detected=0 if wait_for_gpg_card 2>/dev/null; then From bb8c6beefb43c80e86755d93cdca2eb09c74554c Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 29 Apr 2026 18:18:26 -0400 Subject: [PATCH 2/5] Refine STATUS/NOTE/INFO logging for consistent UX and accurate terminology - doc/logging.md: Complete rewrite with clear INFO/NOTE/WARN guidance, table with sleep/blank-line columns, add measuring_trace.log to Quiet mode table - initrd/bin/cbfs-init.sh: Add STATUS_OK for flash read, streamline messages - initrd/bin/gpg-gui.sh: Change INFO to NOTE for GPG instructions - initrd/bin/gui-init.sh: Remove noisy STATUS pairs, add enable_usb for GPG, add STATUS for HOTP verification - initrd/bin/kexec-insert-key.sh: Improve DUK measuring message - initrd/bin/kexec-seal-key.sh: Add STATUS_OK for key gen, slot update, PCR read - initrd/bin/kexec-select-boot.sh: Fix "unsigned boot options" -> "boot options" - initrd/bin/lock_chip.sh: Add STATUS_OK for chipset lock - initrd/bin/network-init-recovery.sh: Add STATUS_OK for modules, clock, SSH - initrd/bin/oem-factory-reset.sh: Many STATUS_OK additions, INFO->NOTE for guidance - initrd/bin/qubes-measure-luks.sh, uefi-init.sh, usb-init.sh: Improve PCR messages - initrd/bin/seal-hotpkey.sh: Add STATUS/STATUS_OK for HOTP secret write - initrd/bin/seal-totp.sh: Change STATUS to NOTE for TOTP secret - initrd/bin/tpmr.sh: Add STATUS/STATUS_OK, INFO->WARN for unseal, add PCR extend INFO - initrd/bin/unseal-hotp.sh: Add STATUS/STATUS_OK for unseal - initrd/etc/functions.sh: PCR5 regression fix (don't auto-load USB), VID wait loop fixes (sleep 0.1, iteration cap), pcrs() formatting fix - initrd/etc/gui_functions.sh: Remove redundant STATUS_OK, add for signing key verification - initrd/init: echo->STATUS_OK, add measuring_trace.log, fix grammar - initrd/sbin/insmod.sh: Improve PCR extend message Signed-off-by: Thierry Laurion --- doc/logging.md | 149 ++++++++++++++++++---------- initrd/bin/cbfs-init.sh | 9 +- initrd/bin/gpg-gui.sh | 4 +- initrd/bin/gui-init.sh | 24 ++--- initrd/bin/kexec-insert-key.sh | 4 +- initrd/bin/kexec-seal-key.sh | 6 +- initrd/bin/kexec-select-boot.sh | 4 +- initrd/bin/lock_chip.sh | 1 + initrd/bin/network-init-recovery.sh | 10 +- initrd/bin/oem-factory-reset.sh | 38 ++++--- initrd/bin/qubes-measure-luks.sh | 2 +- initrd/bin/seal-hotpkey.sh | 4 + initrd/bin/seal-totp.sh | 2 +- initrd/bin/tpmr.sh | 37 +++++-- initrd/bin/uefi-init.sh | 2 +- initrd/bin/unseal-hotp.sh | 2 + initrd/bin/usb-init.sh | 2 +- initrd/etc/functions.sh | 61 +++++++++--- initrd/etc/gui_functions.sh | 4 +- initrd/init | 16 +-- initrd/sbin/insmod.sh | 4 +- 21 files changed, 251 insertions(+), 134 deletions(-) diff --git a/doc/logging.md b/doc/logging.md index 6a7be6cd3..f6e4a411d 100644 --- a/doc/logging.md +++ b/doc/logging.md @@ -10,6 +10,13 @@ This makes it a complete diagnostic artifact that can be shared with developers without requiring the user to reproduce the problem in debug mode first. Console visibility is what varies by mode - the log file never loses information. +**`/tmp/measuring_trace.log` captures INFO-level output emitted by `INFO()`, including in Quiet mode.** +This file isolates TPM measurements, sealing operations, and other security-critical +audit trails from the general debug noise. It is written to by the `INFO()` function +alongside `/tmp/debug.log` (both files receive the same INFO output regardless of console output mode). +Use this file when you need to audit Heads' security operations without wading +through all DEBUG/TRACE output. + ## Log Levels In order from "most verbose" to "least verbose": @@ -101,30 +108,37 @@ Use this in situations like: ## INFO -INFO is for contextual information that may be of interest to end users, but that is not required -for use of Heads. +INFO is for technical/security operations that advanced users want to see. -INFO always goes to debug.log. It is shown on the console in info and debug modes, and suppressed -from the console in quiet mode (where the log file serves as the post-mortem record). +INFO always goes to debug.log. It is shown on the console in **info** mode via `/dev/console`, +routed via `/dev/kmsg` in **debug** mode (so on-console visibility depends on kernel console settings), +and suppressed from the console in **quiet** mode (where the log file serves as the post-mortem record). -Users might use this to troubleshoot Heads configuration or behavior, but this should not require -knowledge of Heads implementation or developer experience. +INFO is for operations that: +- Are technically detailed (TPM PCR extends, key generation, cryptographic operations) +- Advanced users want to see when troubleshooting +- Are NOT hand-off guidance (use NOTE for that) +- Are NOT developer-facing logic tracing (use DEBUG for that) For example: -* "Why can't I enable USB keyboard support?" `INFO "Not showing USB keyboard option, USB keyboard is always enabled for this board"` -* "Why isn't Heads booting automatically?" `INFO "Not booting automatically, automatic boot is disabled in user settings"` -* "Why didn't Heads prompt me for a password?" `INFO "Password has not been changed, using default"` +* `INFO "TPM: Extending PCR[4] with string 'text' (hash: abc123...)"` — string extend +* `INFO "TPM: Extending PCR[4] with content of /path/file (hash: abc123...)"` — file content extend +* `INFO "Measuring /boot/vmlinuz into TPM PCR[4]"` — integrity measurement start +* `INFO "TPM: PCR[4] after extend: 0x..."` — PCR state after extend +* `STATUS "Measuring TPM Disk Unlock Key (DUK) into PCR[6]"` — action announcement (PCR[6] for LUKS sealing) + +Do NOT use INFO for: -These do not include highly technical details. -They can include configuration values or context, _but_ they should refer to configuration settings -using the user-facing names in the configuration menus. +* User guidance or hand-off instructions — use **NOTE** (sleeps 3s, italic white, cannot be hidden) +* High-level user-facing explanations — use **NOTE** or **STATUS** +* Developer-facing logic/decisions — use **DEBUG** Use this in situations like: -* Showing very high level decision-making information, understandable for users not familiar with - Heads implementation -* Explaining a behavior that could reasonably be unexpected for some users +* Reporting technical security operations (TPM extends, measurements, sealing) +* Showing advanced configuration values that power users care about +* Operations that belong in debug.log and info-mode console for audit/diagnostic purposes ## console @@ -142,8 +156,8 @@ Avoid using this, and change existing console output to INFO, STATUS, or another STATUS is for action announcements - operations that are starting or in progress - that all users must see regardless of output mode. -A STATUS message typically precedes a STATUS_OK, WARN, or DIE: it announces the start of something -that has an outcome. If there is no outcome to report, consider INFO instead. +A STATUS message typically precedes STATUS_OK, WARN, or DIE: it announces the start of something +that has an outcome (success, actionable problem, or fatal error). If there is no outcome to report, consider INFO instead. Use STATUS when an action is beginning or underway: @@ -182,17 +196,12 @@ The console renders `OK message` (with a leading space) in bold green; debug.log ## NOTE -NOTE is for contextual information explaining something that is _likely_ to be unexpected or -confusing to users new to Heads. - -Unlike INFO, it cannot be hidden from the console. Use this only if the behavior is likely to be -unexpected or confusing to many users. If it is only possibly unexpected, consider INFO instead. - -Do not overuse this above INFO. Adding too much output at NOTE causes users to ignore it. +NOTE is for **user guidance that needs attention** — it sleeps 3 seconds and prints +blank lines before/after so users cannot scroll past it unread. -NOTE always goes to debug.log. +NOTE uses **italic white** (`\033[3;37m`) and cannot be hidden from console in any output mode. -Two specific patterns where NOTE is the right level: +Use NOTE for two specific patterns: **Security reminders** — advice about consequences or risks the user should not overlook, but that do not indicate a current problem: @@ -208,6 +217,12 @@ tool's prompts or output rather than Heads-formatted messages: * "Nitrokey 3 requires physical presence: touch the dongle when prompted" - hardware-level event * "Please authenticate with OpenPGP smartcard/backup media" - gpg auth flow follows +**Questionnaire/setup guidance** — when walking users through configuration steps: + +* "The following questionnaire will help you configure the security components of your system" +* "Each prompt requires a single letter answer (Y/n)" +* "Master key and subkeys will be generated in memory and backed up to a dedicated LUKS container" + For example: * "Proceeding with unsigned ISO boot" - booting without a verified signature is unexpected and @@ -215,13 +230,17 @@ For example: * "TOTP secret no longer accessible: TPM secrets were wiped" - mid-session secret loss requires immediate user attention. +Unlike INFO (technical operations for advanced users), NOTE is for **user-facing guidance** +that requires the user's attention and cannot be hidden. + +Do not overuse this above INFO. Adding too much output at NOTE causes users to ignore it. + ## WARN WARN is for output that indicates a problem. We think the user should act on it, but we are able to continue, possibly with degraded functionality. This is appropriate when _all_ of the following are true: - * there is a _likely_ problem * we are able to continue, possibly with degraded functionality * the warning is _actionable_ - there is a reasonable change that could silence the warning @@ -235,6 +254,9 @@ Warnings must be _actionable_ - only WARN if there is a reasonable change the us WARN always goes to debug.log. +**WARN sleeps 1 second** and prints blank lines before/after (like NOTE) so users +cannot scroll past it unread. WARN uses **bold yellow** (`\033[1;33m`). + For example: * Warning when using default passphrases that are completely insecure is reasonable. @@ -279,26 +301,32 @@ setup wizards, debug paths) where a full whiptail dialog would be out of place. Users can choose one of three output levels for console information. **`/tmp/debug.log` always captures all levels regardless of the chosen output level.** +**`/tmp/measuring_trace.log` captures INFO-level output emitted by `INFO()` in all modes (including Quiet).** -* **Quiet** - Minimal console output. STATUS, NOTE, WARN and DIE always appear. INFO is suppressed. +* **Quiet** - Minimal console output. STATUS, NOTE, WARN and DIE always appear. INFO is suppressed on console. + `INFO()` output is still captured in `/tmp/debug.log` and `/tmp/measuring_trace.log` for post-mortem analysis. Use this for production/unattended systems where the log file is the post-mortem record. * **Info** - Show information about operations in Heads. INFO and above appear on console. + INFO also goes to `/tmp/measuring_trace.log` for audit trails. Use this for interactive use where the user is watching the screen. * **Debug** - Show detailed information suitable for debugging Heads. TRACE and DEBUG also appear - on console. Use this when actively developing or diagnosing Heads. + on console. INFO goes to `/tmp/measuring_trace.log` and `/dev/kmsg`. + Use this when actively developing or diagnosing Heads. Console output styling - chosen for accessibility across color-deficiency types (WCAG 1.4.1: color is never the sole signal; text prefixes carry meaning independently): -| Level | Style | ANSI code | Rationale | -|-----------|--------------|--------------|---------------------------------------------------------------------------------------------------------------------| -| DIE | bold red | `\033[1;31m` | Red = universal danger signal; `!!! ERROR:` prefix is the semantic carrier | -| WARN | bold yellow | `\033[1;33m` | Most universally perceptible alert color across deuteranopia, protanopia, tritanopia | -| NOTE | italic white | `\033[3;37m` | White = highest-contrast neutral on dark consoles; italic separates NOTE from bold STATUS/WARN, no semantic hue | -| STATUS | bold only | `\033[1m` | In-progress actions - bold without hue readable in every terminal theme; `>>` prefix differentiates semantically | -| STATUS_OK | bold green | `\033[1;32m` | Confirmed success - green is universally understood as success; scannable at a glance against plain bold STATUS | -| INFO | green | `\033[0;32m` | Standard informational color; INFO is optional context, its absence on console is harmless | -| INPUT | bold white | `\033[1;37m` | Maximum contrast (21:1) on VGA/dark consoles; no color dependency, readable under all deficiency types | +| Level | Style | ANSI code | Sleep | Blank lines | Quiet mode | Purpose | +|-----------|--------------|--------------|-------|-------------|------------|---------| +| DIE | bold red | `\033[1;31m` | 0s | Yes | Visible | Fatal errors - execution stops | +| WARN | bold yellow | `\033[1;33m` | 1s | Yes | Visible | Actionable problems - degraded operation | +| NOTE | italic white | `\033[3;37m` | 3s | Yes | Visible | User guidance needing attention | +| STATUS | bold only | `\033[1m` | 0s | No | Visible | In-progress action announcements | +| STATUS_OK | bold green | `\033[1;32m` | 0s | No | Visible | Confirmed success outcomes | +| INFO | green | `\033[0;32m` | 0s | No | Suppressed | Technical operations for advanced users | +| INPUT | bold white | `\033[1;37m` | 0s | No* | Visible | Interactive input prompts | + +\* INPUT prints a newline after user input, not before. debug.log and /dev/kmsg always receive plain text without ANSI codes. @@ -310,17 +338,22 @@ This means callers never need to care about redirections: a caller that does Similarly, scripts that use stdout for a structured protocol can safely call STATUS, STATUS_OK, and any other logging function — log output never appears on stdout. -NOTE, WARN and DIE print a blank line before and after the message so they stand out visually -from surrounding output. STATUS and STATUS_OK do **not** — they are called frequently and blank -lines would make output very noisy. Use NOTE when a sleep and blank lines are needed. +NOTE (3s), WARN (1s), and DIE (0s) print blank lines before and after the message +so they stand out visually from surrounding output. +STATUS and STATUS_OK do **not** — they are called frequently and blank +lines would make output very noisy. INPUT displays the prompt inline (no leading blank line); the cursor stays on the same line as the prompt. +A blank line is printed after the user's input to separate it from subsequent output. ### None / Quiet - minimal console output -| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | -|----------------|-----|-------|-------|------|--------|-----------|------|------|-----| -| Console | | | | | Yes | Yes | Yes | Yes | Yes | -| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | +|--------------------------|-----|-------|-------|------|--------|-----------|------|------|-----| +| Console | | | | | Yes | Yes | Yes | Yes | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| /tmp/measuring_trace.log | | | | Yes | | | | | | + +In Quiet mode, INFO-level output is suppressed on console but still captured in both log files. Quiet output is specified with: @@ -332,10 +365,13 @@ CONFIG_QUIET_MODE=y ### Info -| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | -|----------------|-----|-------|-------|------|--------|-----------|------|------|-----| -| Console | | | | Yes | Yes | Yes | Yes | Yes | Yes | -| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | +|--------------------------|-----|-------|-------|------|--------|-----------|------|------|-----| +| Console | | | | Yes | Yes | Yes | Yes | Yes | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| /tmp/measuring_trace.log | | | | Yes | | | | | | + +In Info mode, INFO appears on console and is captured in both log files. Info output is enabled with: @@ -347,10 +383,17 @@ CONFIG_QUIET_MODE=n ### Debug -| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | -|----------------|-----|-------|-------|------|--------|-----------|------|------|-----| -| Console | | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | +|--------------------------|-----|-------|-------|------|--------|-----------|------|------|-----| +| Console | | Yes* | Yes | [**] | Yes | Yes | Yes | Yes | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| /tmp/measuring_trace.log | | | | Yes | | | | | | + +In Debug mode, INFO goes to `/tmp/measuring_trace.log` and `/dev/kmsg` (not `/dev/console`). +On-console visibility depends on kernel `printk` settings forwarding kmsg to the console. +\* TRACE requires `CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y` (set automatically with Debug mode). + +[**] INFO in Debug mode routes through `/dev/kmsg` (not `/dev/console`); on-console visibility depends on kernel `printk` settings forwarding kmsg to the console. Debug output is enabled with: diff --git a/initrd/bin/cbfs-init.sh b/initrd/bin/cbfs-init.sh index 27b4601dc..e0caac9c9 100755 --- a/initrd/bin/cbfs-init.sh +++ b/initrd/bin/cbfs-init.sh @@ -24,10 +24,12 @@ if [ -z "$CONFIG_PCR" ]; then fi if [ "$CONFIG_CBFS_VIA_FLASHPROG" = "y" ]; then - # Use flashrom directly, because we don't have /tmp/config with params for flash.sh yet - STATUS "Reading board keys and configuration from SPI flash" + # Workaround: cbfs cannot read CBFS directly on rom_hole boards + # See: https://github.com/osresearch/flashtools/issues/10 + STATUS "Reading SPI flash with flashprog (rom_hole workaround)..." if /bin/flashprog -p internal --fmap -i COREBOOT -i FMAP -r /tmp/cbfs-init.rom; then CBFS_ARG=" -o /tmp/cbfs-init.rom" + STATUS_OK "ROM read" else WARN "Failed to read board keys and configuration from SPI flash - some features may not be available" fi @@ -47,7 +49,6 @@ for cbfsname in `echo $cbfsfiles`; do || DIE "$filename: cbfs file read failed" if [ "$CONFIG_TPM" = "y" ]; then TRACE_FUNC - INFO "Measuring $filename into TPM PCR[$CONFIG_PCR]" # Measure both the filename and its content. This # ensures that renaming files or pivoting file content # will still affect the resulting PCR measurement. @@ -57,4 +58,4 @@ for cbfsname in `echo $cbfsfiles`; do fi fi done -STATUS_OK "Board keys and configuration loaded from firmware" +STATUS_OK "GPG keyring, trustdb, and board configuration extracted from firmware" diff --git a/initrd/bin/gpg-gui.sh b/initrd/bin/gpg-gui.sh index 0ec4c3210..b6e662679 100755 --- a/initrd/bin/gpg-gui.sh +++ b/initrd/bin/gpg-gui.sh @@ -57,8 +57,8 @@ while true; do "g") confirm_gpg_card STATUS "INSTRUCTIONS:" - INFO "Type 'admin' then 'generate' and follow the prompts to generate a GPG key" - INFO "Type 'quit' once the key is generated to exit GPG" + NOTE "Type 'admin' then 'generate' and follow the prompts to generate a GPG key" + NOTE "Type 'quit' once the key is generated to exit GPG" gpg --card-edit >/tmp/gpg_card_edit_output if [ $? -eq 0 ]; then gpg_post_gen_mgmt diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index 7c7164e7b..e5a8faccf 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -249,11 +249,11 @@ gate_reseal_with_integrity_report() { # starting the NK3 CCID teardown. This safety call covers the # case where scdaemon was restarted between then and now. release_scdaemon - STATUS "Checking $DONGLE_BRAND presence before sealing" DEBUG "gate_reseal_with_integrity_report: checking HOTP token presence" + STATUS "Checking $DONGLE_BRAND presence before sealing" if hotp_verification info >/dev/null 2>&1; then - token_ok="y" STATUS_OK "$DONGLE_BRAND present and accessible" + token_ok="y" break fi DEBUG "gate_reseal_with_integrity_report: HOTP token not accessible" @@ -285,10 +285,9 @@ generate_totp_hotp() { if [ "$CONFIG_TPM" != "y" ] && [ -x /bin/hotp_verification ]; then # If we don't have a TPM, but we have a HOTP USB Security dongle TRACE_FUNC - STATUS "Generating new HOTP secret" /bin/seal-hotpkey.sh || DIE "Failed to generate HOTP secret" - elif STATUS "Generating new TOTP secret" && /bin/seal-totp.sh "$BOARD_NAME" "$tpm_owner_passphrase"; then + elif /bin/seal-totp.sh "$BOARD_NAME" "$tpm_owner_passphrase"; then if [ -x /bin/hotp_verification ]; then # If we have a TPM and a HOTP USB Security dongle if [ "$CONFIG_TOTP_SKIP_QRCODE" != y ]; then @@ -362,6 +361,7 @@ update_totp() { return 1 # Already asked to skip to menu from a prior error fi + DEBUG "TPM state at TOTP failure:" DEBUG "$(pcrs)" totp_menu_text=$( @@ -437,6 +437,7 @@ update_hotp() { local hotp_token_info hotp_exit attempt # Ensure dongle is present; capture info for PIN counter display + STATUS "Checking $DONGLE_BRAND presence" if ! hotp_token_info="$(hotp_verification info)"; then if [ "$skip_to_menu" = "true" ]; then return 1 # Already asked to skip to menu from a prior error @@ -474,12 +475,14 @@ update_hotp() { # PIN retry count is shown only before a retry so normal boots stay silent. for attempt in 1 2 3; do # Don't output HOTP codes to screen, so as to make replay attacks harder + STATUS "Verifying HOTP code" hotp_verification check "$HOTP" hotp_exit=$? case "$hotp_exit" in 0) HOTP="Success" BG_COLOR_MAIN_MENU="normal" + STATUS_OK "HOTP code verified" return ;; 4 | 7) # 4: code incorrect, 7: not a valid HOTP code — no point retrying same code @@ -654,7 +657,6 @@ check_gpg_key() { prompt_auto_default_boot() { TRACE_FUNC - STATUS_OK "HOTP verification success" if pause_automatic_boot; then STATUS "Attempting default boot" attempt_default_boot @@ -867,8 +869,10 @@ reset_tpm() { DIE "Unable to create rollback file" TRACE_FUNC - # As a countermeasure for existing primary handle hash, we will now force sign /boot without it - # USB is already initialized at startup; run gpg --card-status to populate key stub. + # As a countermeasure for existing primary handle hash, we will now force sign /boot without it. + # NOTE: At seal time, PCR5 is IGNORED (not measured) - only used on HOTP board variants. So USB + # modules loading here don't affect DUK seal. GPG card needs USB to be enabled first. + enable_usb wait_for_gpg_card || true while true; do GPG_KEY_COUNT=$(gpg -K 2>/dev/null | wc -l) @@ -876,7 +880,6 @@ reset_tpm() { prompt_missing_gpg_key_action || return 1 wait_for_gpg_card || true else - STATUS_OK "TPM reset successful - updating /boot checksums and signatures" if ! update_checksums; then whiptail_error --title 'ERROR' \ --msgbox "Failed to update checksums / sign default config" 0 80 @@ -896,14 +899,9 @@ reset_tpm() { fi if [ -s /boot/kexec_key_devices.txt ] || [ -s /boot/kexec_key_lvm.txt ]; then - STATUS_OK "TPM reset successful - resealing TPM Disk Unlock Key (DUK)" reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action fi - else - INFO "Returning to the main menu" fi - else - whiptail_error --title 'ERROR: No TPM Detected' --msgbox "This device does not have a TPM.\n\nPress OK to return to the Main Menu" 0 80 fi } diff --git a/initrd/bin/kexec-insert-key.sh b/initrd/bin/kexec-insert-key.sh index 883ec8897..e1f8d0e62 100755 --- a/initrd/bin/kexec-insert-key.sh +++ b/initrd/bin/kexec-insert-key.sh @@ -29,7 +29,7 @@ if [ -r "$TMP_KEY_LVM" ]; then fi # Measure the LUKS headers before we unseal the LUKS Disk Unlock Key from TPM -STATUS "Measuring LUKS headers" +STATUS "Measuring TPM Disk Unlock Key (DUK) into PCR[6])" cat "$TMP_KEY_DEVICES" | cut -d\ -f1 | xargs /bin/qubes-measure-luks.sh || DIE "LUKS measure failed" @@ -65,7 +65,7 @@ fi # Override PCR 4 so that user can't read the key TRACE_FUNC -INFO "TPM: Extending PCR[4] to prevent any future secret unsealing" +INFO "TPM: Extending PCR[4] with content of string 'generic' to prevent secret unsealing" tpmr.sh extend -ix 4 -ic generic || DIE 'Unable to scramble PCR' diff --git a/initrd/bin/kexec-seal-key.sh b/initrd/bin/kexec-seal-key.sh index e2b9ff741..79cc5b14b 100755 --- a/initrd/bin/kexec-seal-key.sh +++ b/initrd/bin/kexec-seal-key.sh @@ -174,6 +174,7 @@ dd \ count=128 \ 2>/dev/null || DIE "Unable to generate random key of 128 characters" +STATUS_OK "LUKS TPM Disk Unlock Key generated" previous_luks_header_version=0 for dev in $key_devices; do @@ -261,15 +262,17 @@ for dev in $key_devices; do --new-key-slot "$duk_keyslot" \ "$dev" "$DUK_KEY_FILE" || DIE "$dev: Unable to add LUKS TPM Disk Unlock Key to LUKS key slot $duk_keyslot" + STATUS_OK "$dev: LUKS TPM Disk Unlock Key added to slot $duk_keyslot" done # Now that we have setup the new keys, measure the PCRs # We don't care what ends up in PCR 6; we just want # to get the /tmp/luksDump.txt file. We use PCR16 # since it should still be zero -STATUS "Measuring LUKS headers for TPM sealing policy" +STATUS "Measuring TPM Disk Unlock Key (DUK) for sealing policy (PCR[6])" echo "$key_devices" | xargs /bin/qubes-measure-luks.sh || DIE "Unable to measure the LUKS headers" +STATUS_OK "TPM Disk Unlock Key (DUK) measured for sealing policy (PCR[6])" STATUS "Reading current PCR values for TPM sealing policy" pcrf="/tmp/secret/pcrf.bin" @@ -293,6 +296,7 @@ DEBUG "Precomputing TPM future value for PCR6 sealing/unsealing of LUKS TPM Disk tpmr.sh calcfuturepcr 6 "/tmp/luksDump.txt" >>"$pcrf" # We take into consideration user files in cbfs tpmr.sh pcrread -a 7 "$pcrf" +STATUS_OK "PCR values read for TPM sealing policy" # tpmr.sh seal may prompt for TPM owner password; avoid DO_WITH_DEBUG here so the # prompt remains visible on console. tpmr.sh logs command details internally. diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 9f5a07543..10864f77b 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -204,7 +204,7 @@ parse_option() { } scan_options() { - STATUS "Scanning for unsigned boot options" + STATUS "Scanning for boot options" option_file="/tmp/kexec_options.txt" scan_boot_options "$bootdir" "$config" "$option_file" if [ ! -s $option_file ]; then @@ -373,7 +373,7 @@ while true; do if [ ! -r "$TMP_KEY_DEVICES" ]; then # Extend PCR4 as soon as possible TRACE_FUNC - INFO "TPM: Extending PCR[4] to prevent further secret unsealing" + INFO "TPM: Extending PCR[4] with content of string 'generic' to prevent secret unsealing" tpmr.sh extend -ix 4 -ic generic || DIE "Failed to extend TPM PCR[4]" fi diff --git a/initrd/bin/lock_chip.sh b/initrd/bin/lock_chip.sh index 44a3003a4..73c0603a8 100755 --- a/initrd/bin/lock_chip.sh +++ b/initrd/bin/lock_chip.sh @@ -21,6 +21,7 @@ if [ -n "$APM_CNT" -a -n "$FIN_CODE" ]; then # until the next system reset. STATUS "Finalizing chipset write protection via SMI PR0 lockdown" io386 -o b -b x $APM_CNT $FIN_CODE + STATUS_OK "Chipset write protection locked" else NOTE "NOT finalizing chipset - lock_chip.sh called without valid APM_CNT and FIN_CODE" fi diff --git a/initrd/bin/network-init-recovery.sh b/initrd/bin/network-init-recovery.sh index 1b8930584..d8320a535 100755 --- a/initrd/bin/network-init-recovery.sh +++ b/initrd/bin/network-init-recovery.sh @@ -60,6 +60,7 @@ ethernet_activation() insmod.sh /lib/modules/$module.ko fi done + STATUS_OK "Ethernet network modules loaded" } # bring up the ethernet interface @@ -113,10 +114,10 @@ if [ -n "$dev" ]; then STATUS_OK "NTP time sync successful" fi fi - STATUS "Syncing hardware clock with system time (UTC)" - hwclock -w - date=$(date "+%Y-%m-%d %H:%M:%S %Z") - STATUS "Time: $date" + STATUS "Syncing hardware clock with system time (UTC)" + hwclock -w + date=$(date "+%Y-%m-%d %H:%M:%S %Z") + STATUS_OK "Hardware clock synced: $date" fi fi fi @@ -133,6 +134,7 @@ if [ -n "$dev" ]; then # -B background # -R create host keys dropbear -B -R + STATUS_OK "Dropbear SSH server started" fi STATUS_OK "Network setup complete" ifconfig $dev diff --git a/initrd/bin/oem-factory-reset.sh b/initrd/bin/oem-factory-reset.sh index 32539f1da..f4ecd58bd 100755 --- a/initrd/bin/oem-factory-reset.sh +++ b/initrd/bin/oem-factory-reset.sh @@ -218,6 +218,7 @@ generate_inmemory_RSA_master_and_subkeys() { ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key generation failed!\n\n$ERROR" fi + STATUS_OK "RSA ${RSA_KEY_LENGTH}-bit master key generated" STATUS "Generating RSA signing subkey for $DONGLE_BRAND" # Add signing subkey @@ -236,6 +237,7 @@ generate_inmemory_RSA_master_and_subkeys() { ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key signing subkey generation failed!\n\n$ERROR" fi + STATUS_OK "RSA signing subkey generated" STATUS "Generating RSA encryption subkey for $DONGLE_BRAND" #Add encryption subkey @@ -254,6 +256,7 @@ generate_inmemory_RSA_master_and_subkeys() { ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key encryption subkey generation failed!\n\n$ERROR" fi + STATUS_OK "RSA encryption subkey generated" STATUS "Generating RSA authentication subkey for $DONGLE_BRAND" #Add authentication subkey @@ -279,6 +282,7 @@ generate_inmemory_RSA_master_and_subkeys() { ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key authentication subkey generation failed!\n\n$ERROR" fi + STATUS_OK "RSA authentication subkey generated" } #Generate a gpg master key: no expiration date, NIST P-256 key (ECC) @@ -307,6 +311,7 @@ generate_inmemory_p256_master_and_subkeys() { ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG NIST P-256 Key generation failed!\n\n$ERROR" fi + STATUS_OK "NIST P-256 master key generated" #Keep Master key fingerprint for add key calls MASTER_KEY_FP=$(gpg --list-secret-keys --with-colons | grep fpr | cut -d: -f10) @@ -327,6 +332,7 @@ generate_inmemory_p256_master_and_subkeys() { ERROR_MSG=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Failed to add ECC nistp256 signing key to master key\n\n${ERROR_MSG}" fi + STATUS_OK "NIST P-256 signing subkey generated" STATUS "Generating NIST P-256 encryption subkey for $DONGLE_BRAND" { @@ -343,6 +349,7 @@ generate_inmemory_p256_master_and_subkeys() { ERROR_MSG=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Failed to add ECC nistp256 encryption key to master key\n\n${ERROR_MSG}" fi + STATUS_OK "NIST P-256 encryption subkey generated" STATUS "Generating NIST P-256 authentication subkey for $DONGLE_BRAND" { @@ -362,6 +369,7 @@ generate_inmemory_p256_master_and_subkeys() { ERROR_MSG=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Failed to add ECC nistp256 authentication key to master key\n\n${ERROR_MSG}" fi + STATUS_OK "NIST P-256 authentication subkey generated" } @@ -1050,6 +1058,7 @@ usb_security_token_capabilities_check() { DEBUG "$DONGLE_BRAND firmware version: $DONGLE_FW_VERSION" fi fi + STATUS_OK "$DONGLE_BRAND capabilities checked" } # usb_security_token_capabilities_check now handles all USB Security dongle logic @@ -1108,9 +1117,9 @@ INPUT "Would you like to use default configuration options? If N, you will be pr if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then #Give general guidance to user on how to answer prompts STATUS "Factory Reset / Re-Ownership Questionnaire" - INFO "The following questionnaire will help you configure the security components of your system" - INFO "Each prompt requires a single letter answer (Y/n)" - INFO "Pressing Enter selects the default answer for each prompt" + NOTE "The following questionnaire will help you configure the security components of your system" + NOTE "Each prompt requires a single letter answer (Y/n)" + NOTE "Pressing Enter selects the default answer for each prompt" TRACE_FUNC DEBUG "Showing passphrase guidance: QR code from diceware.dmuth.org" qrenc "https://diceware.dmuth.org/" @@ -1141,7 +1150,7 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then -o "$prompt_output" == "Y" ] \ ; then GPG_GEN_KEY_IN_MEMORY="y" - INFO "Master key and subkeys will be generated in memory and backed up to a dedicated LUKS container" + NOTE "Master key and subkeys will be generated in memory and backed up to a dedicated LUKS container" INPUT "Would you like in-memory generated subkeys to be copied to $DONGLE_BRAND's OpenPGP smartcard? (Highly recommended) [Y/n]:" -n 1 prompt_output if [ "$prompt_output" == "n" \ -o "$prompt_output" == "N" ]; then @@ -1149,12 +1158,12 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then NOTE "Your GPG key material backup thumb drive should be cloned to a second thumb drive for redundancy for production environments" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" else - INFO "Subkeys will be copied to $DONGLE_BRAND's OpenPGP smartcard" + NOTE "Subkeys will be copied to $DONGLE_BRAND's OpenPGP smartcard" NOTE "Please keep your GPG key material backup thumb drive safe" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="y" fi else - INFO "GPG key material will be generated on $DONGLE_BRAND's OpenPGP smartcard without backup" + NOTE "GPG key material will be generated on $DONGLE_BRAND's OpenPGP smartcard without backup" GPG_GEN_KEY_IN_MEMORY="n" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" fi @@ -1367,7 +1376,7 @@ STATUS "Detecting and setting boot device" if ! detect_boot_device; then SKIP_BOOT="y" else - STATUS "Boot device set to $CONFIG_BOOT_DEV" + STATUS_OK "Boot device set to $CONFIG_BOOT_DEV" fi # update configs @@ -1390,12 +1399,11 @@ fi ## reset TPM and set passphrase if [ "$CONFIG_TPM" = "y" ]; then - STATUS "Resetting TPM" tpmr.sh reset "$TPM_PASS" >/dev/null 2>/tmp/error -fi -if [ $? -ne 0 ]; then - ERROR=$(tail -n 1 /tmp/error | fold -s) - whiptail_error_die "Error resetting TPM:\n\n${ERROR}" + if [ $? -ne 0 ]; then + ERROR=$(tail -n 1 /tmp/error | fold -s) + whiptail_error_die "Error resetting TPM:\n\n${ERROR}" + fi fi # clear local keyring @@ -1511,6 +1519,7 @@ if [ "$GPG_EXPORT" != "0" ]; then ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error_die "Key export error: unable to copy ${GPG_GEN_KEY}.asc to /media:\n\n$ERROR" fi + STATUS_OK "Generated key exported to USB" mount -o remount,ro /media 2>/dev/null umount /media 2>/dev/null || true else @@ -1555,6 +1564,7 @@ else ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error_die "Error reading current firmware:\n\n$ERROR" fi + STATUS_OK "Current firmware read successfully" if [ ! -s /tmp/oem-setup.rom ]; then ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error_die "Error reading current firmware:\n\n$ERROR" @@ -1589,12 +1599,14 @@ else ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error_die "Error flashing updated firmware image:\n\n$ERROR" fi + STATUS_OK "Firmware updated and flashed successfully" fi ## sign files in /boot and generate checksums if [[ "$SKIP_BOOT" == "n" ]]; then STATUS "Updating checksums and signing all files in /boot" generate_checksums + STATUS_OK "Checksums updated and files signed" fi # passphrases set to be empty first @@ -1636,7 +1648,7 @@ while true; do break fi #Tell user to scan the QR code containing all configured secrets - STATUS "Scan the QR code below to save the secrets to a secure location" + NOTE "Scan the QR code below to save the secrets to a secure location" qrenc "$(echo -e "$passphrases")" # Prompt user to confirm scanning of qrcode on console prompt not whiptail: y/n INPUT "Please confirm you have scanned the QR code above and/or written down the secrets? [y/N]:" -n 1 prompt_output diff --git a/initrd/bin/qubes-measure-luks.sh b/initrd/bin/qubes-measure-luks.sh index 7e2e53f46..2b3cf5ba0 100755 --- a/initrd/bin/qubes-measure-luks.sh +++ b/initrd/bin/qubes-measure-luks.sh @@ -20,6 +20,6 @@ DEBUG "Removing /tmp/lukshdr-*" rm /tmp/lukshdr-* TRACE_FUNC -INFO "TPM: Extending PCR[6] with hash of LUKS headers from /tmp/luksDump.txt" +INFO "TPM: Extending PCR[6] with content of /tmp/luksDump.txt (hash of TPM Disk Unlock Key headers)" tpmr.sh extend -ix 6 -if /tmp/luksDump.txt || DIE "Unable to extend PCR" diff --git a/initrd/bin/seal-hotpkey.sh b/initrd/bin/seal-hotpkey.sh index b73ed9dee..eac1d35d3 100755 --- a/initrd/bin/seal-hotpkey.sh +++ b/initrd/bin/seal-hotpkey.sh @@ -141,8 +141,12 @@ else fi #TODO: silence the output of hotp_initialize once https://github.com/Nitrokey/nitrokey-hotp-verification/issues/41 is fixed #hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$DONGLE_BRAND" >/dev/null 2>&1 + STATUS "Writing HOTP secret to $DONGLE_BRAND" hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$DONGLE_BRAND" admin_pin_status="$?" + if [ "$admin_pin_status" -eq 0 ]; then + STATUS_OK "HOTP secret written to $DONGLE_BRAND" + fi fi if [ "$admin_pin_status" -ne 0 ]; then diff --git a/initrd/bin/seal-totp.sh b/initrd/bin/seal-totp.sh index 44a5c72e9..396efd0ef 100755 --- a/initrd/bin/seal-totp.sh +++ b/initrd/bin/seal-totp.sh @@ -76,5 +76,5 @@ url="otpauth://totp/$HOST?secret=$secret" DEBUG "TOTP secret output on screen (both URL and QR code)" qrenc "$url" -STATUS "TOTP secret for manual input (device without camera): $secret" +NOTE "TOTP secret for manual input (device without camera): $secret" secret="" diff --git a/initrd/bin/tpmr.sh b/initrd/bin/tpmr.sh index 264a15ffa..44c526782 100755 --- a/initrd/bin/tpmr.sh +++ b/initrd/bin/tpmr.sh @@ -257,8 +257,8 @@ tpm2_extend() { esac done tpm2 pcrextend "$index:sha256=$hash" - LOG "TPM: PCR[$index] after extend: $(tpm2 pcrread "sha256:$index" 2>&1)" - LOG "TPM: Extended PCR[$index] with hash $hash" + INFO "TPM: Extended PCR[$index] with hash $hash" + INFO "TPM: PCR[$index] after extend: $(tpm2 pcrread "sha256:$index" 2>&1 | tail -1)" } tpm2_counter_read() { @@ -718,7 +718,7 @@ tpm2_unseal() { # stderr; capture stderr to log. if ! tpm2 unseal -Q -c "$handle" -p "session:$POLICY_SESSION$UNSEAL_PASS_SUFFIX" \ -S "$ENC_SESSION_FILE" >"$file" 2> >(SINK_LOG "tpm2 stderr"); then - INFO "Unable to unseal secret from TPM NVRAM" + WARN "Unable to unseal secret from TPM NVRAM" # should succeed, exit if it doesn't exit 1 @@ -948,7 +948,7 @@ tpm2_kexec_finalize() { # Add a random passphrase to platform hierarchy to prevent TPM2 from # being cleared in the OS. # This passphrase is only effective before the next boot. - STATUS "Locking TPM2 platform hierarchy" + INFO "TPM: Locking TPM2 platform hierarchy" randpass=$(dd if=/dev/urandom bs=4 count=1 status=none 2>/dev/null | xxd -p) tpm2 changeauth -c platform "$randpass" || WARN "Failed to lock platform hierarchy of TPM2" @@ -996,20 +996,23 @@ if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then extend) # Check if we extend with a hash or a file if [ "$4" = "-if" ]; then - DEBUG "TPM: Will extend PCR[$3] hash content of file $5" hash="$(sha1sum "$5" | cut -d' ' -f1)" + INFO "TPM: Extending PCR[$3] with content of $5 (hash: $hash)" elif [ "$4" = "-ic" ]; then string=$(echo -n "$5") - DEBUG "TPM: Will extend PCR[$3] with hash of filename $string" hash="$(echo -n "$5" | sha1sum | cut -d' ' -f1)" + INFO "TPM: Extending PCR[$3] with content of string '$5' (hash: $hash)" fi TRACE_FUNC - INFO "TPM: Extending PCR[$3] with hash $hash" - # Silence stdout/stderr, they're only useful for debugging # and DO_WITH_DEBUG captures them DO_WITH_DEBUG exec tpm "$@" &>/dev/null + + # Read PCR value after extend (TPM1 uses SHA1, read from sysfs) + pcr_value=$(grep -i "PCR-0*$3:" /sys/class/tpm/tpm0/pcrs | head -1 | cut -d: -f2 | tr -d ' ') + INFO "TPM: PCR[$3] after extend: $pcr_value" + INFO "TPM: Extended PCR[$3] with hash $hash" ;; seal) shift @@ -1022,7 +1025,9 @@ if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then ;; reset) shift + INFO "TPM: Resetting TPM" tpm1_reset "$@" + STATUS_OK "TPM reset completed" ;; kexec_finalize) ;; # Nothing on TPM1. shutdown) ;; # Nothing on TPM1. @@ -1050,9 +1055,19 @@ pcrsize) calcfuturepcr) replay_pcr "sha256" "$@" ;; -extend) + extend) TRACE_FUNC - INFO "TPM: Extending PCR[$2] with $4" + # Show INFO message with what's being extended for auditability + # -ic: extend with string, -if: extend with file content + if [ "$3" = "-ic" ]; then + # -ic: the string is passed directly + hash="$(echo -n "$4" | sha256sum | cut -d' ' -f1)" + INFO "TPM: Extending PCR[$2] with content of string '$4' (hash: $hash)" + else + # -if: the file content is used + hash="$(sha256sum "$4" | cut -d' ' -f1)" + INFO "TPM: Extending PCR[$2] with content of $4 (hash: $hash)" + fi tpm2_extend "$@" ;; counter_read) @@ -1077,7 +1092,9 @@ unseal) tpm2_unseal "$@" ;; reset) + INFO "TPM: Resetting TPM" tpm2_reset "$@" + STATUS_OK "TPM reset completed" ;; kexec_finalize) tpm2_kexec_finalize "$@" diff --git a/initrd/bin/uefi-init.sh b/initrd/bin/uefi-init.sh index f95341f12..27865f534 100755 --- a/initrd/bin/uefi-init.sh +++ b/initrd/bin/uefi-init.sh @@ -19,7 +19,7 @@ if [ -n "$GUID" ]; then || DIE "Failed to read config GUID from ROM" if [ "$CONFIG_TPM" = "y" ]; then - INFO "TPM: Extending PCR[$CONFIG_PCR] with UEFI configuration" + INFO "TPM: Extending PCR[$CONFIG_PCR] with content of $TMPFILE (UEFI configuration)" tpmr.sh extend -ix "$CONFIG_PCR" -if $TMPFILE \ || DIE "$GUID: tpm extend failed" fi diff --git a/initrd/bin/unseal-hotp.sh b/initrd/bin/unseal-hotp.sh index e8f4691ed..ad172626e 100755 --- a/initrd/bin/unseal-hotp.sh +++ b/initrd/bin/unseal-hotp.sh @@ -52,6 +52,7 @@ if [ "$CONFIG_TPM" = "y" ]; then fi DEBUG "Unsealing HOTP secret reuses TOTP sealed secret..." # debug unseal too; no password argument + STATUS "Unsealing HOTP secret from TPM" if ! DO_WITH_DEBUG tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET"; then if counter_readable; then fail_unseal "Unable to unseal HOTP secret from TPM; TPM rollback counter intact. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 @@ -59,6 +60,7 @@ if [ "$CONFIG_TPM" = "y" ]; then fail_unseal "Unable to unseal HOTP secret from TPM; TPM rollback counter broken or missing, reset TPM (see Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and then generate a new secret." || exit 1 fi fi + STATUS_OK "HOTP secret unsealed from TPM" else # without a TPM, generate a secret based on the SHA-256 of the ROM secret_from_rom_hash >"$HOTP_SECRET" || fail_unseal "Reading ROM failed" || exit 1 diff --git a/initrd/bin/usb-init.sh b/initrd/bin/usb-init.sh index 93cdfcec3..9dcf9687e 100755 --- a/initrd/bin/usb-init.sh +++ b/initrd/bin/usb-init.sh @@ -8,7 +8,7 @@ TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then # Extend PCR4 as soon as possible - INFO "TPM: Extending PCR[4] for USB boot" + INFO "TPM: Extending PCR[4] with content of string 'usb' for USB boot" tpmr.sh extend -ix 4 -ic usb fi diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index d65c66d27..df315cff2 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -498,6 +498,17 @@ pin_color() { # 1050:0114 Yubikey 4/5 (OTP+U2F+CCID) - OTP+CCID only # 1050:0115 Yubikey 4/5 (OTP+U2F+CCID) - FIDO+CCID # 1050:0404 Yubikey 5 (FIDO+CCID) + +# Detect USB security dongle branding (Nitrokey, Yubikey, Canokey, etc.) from VID:PID. +# This function intentionally does NOT load USB modules automatically - it only scans for +# known dongles when USB is already enabled. Callers that need USB (HOTP/GPG/LUKS operations) +# must call enable_usb() first. +# +# REGRESSION FIX: Previously, this function always called enable_usb() which loaded USB +# kernel modules (ehci-hcd, xhci-hcd, etc.). These modules extend PCR5 in the TPM. +# The Disk Unlock Key (DUK) for LUKS is sealed to PCR5=0 (no modules loaded). If USB +# modules load at boot before DUK unseal, PCR5 mismatch occurs and unseal fails. +# On boards without HOTP support, USB should NOT load at boot - only when needed. detect_usb_security_dongle_branding() { TRACE_FUNC local usb_was_enabled="${_USB_ENABLED:-n}" @@ -509,14 +520,23 @@ detect_usb_security_dongle_branding() { return fi - # Child scripts can inherit DONGLE_BRAND while _USB_ENABLED resets, so always - # initialize USB unless the fast path above was taken. - enable_usb - [ "$usb_was_enabled" != "y" ] && wait_for_usb_devices + # If USB is not enabled, do NOT auto-load modules here (PCR5 extension). + # Callers that need the dongle (HOTP/GPG/LUKS) must call enable_usb first. + # This prevents unnecessary PCR5 extensions at boot that break DUK unseal. + if [ "$usb_was_enabled" != "y" ]; then + DEBUG "detect_usb_security_dongle_branding: USB not enabled, setting default branding" + export DONGLE_BRAND="USB Security dongle" + return + fi + wait_for_usb_devices + + # If branding is already specific, USB is now ready and no re-scan is needed. + [ "$DONGLE_BRAND" != "USB Security dongle" ] && [ -n "$DONGLE_BRAND" ] && return # Wait for known USB dongle VID to appear in sysfs (max 3s). # Known VIDs: 20a0 (Nitrokey/Canokey QEMU), 316d (Librem Key), 16d0 (Canokey), 1050 (Yubikey) local start_dongle_wait=$(awk '{print $1}' /proc/uptime 2>/dev/null) + local iterations=0 while :; do if ls /sys/bus/usb/devices/*/idVendor 2>/dev/null | \ xargs grep -l -E "20a0|316d|16d0|1050" 2>/dev/null | grep -q .; then @@ -524,18 +544,24 @@ detect_usb_security_dongle_branding() { break fi - # Timeout after 3 seconds - local now=$(awk '{print $1}' /proc/uptime 2>/dev/null) - if [ -n "$now" ] && [ -n "$start_dongle_wait" ]; then - if awk -v s="$start_dongle_wait" -v n="$now" 'BEGIN{exit (n - s > 3.0) ? 0 : 1}'; then - DEBUG "Timeout waiting for USB security dongle VID after 3s" - break + # Timeout after 3 seconds (or 30 iterations as hard fallback) + iterations=$((iterations + 1)) + if [ "$iterations" -ge 30 ]; then + DEBUG "Timeout waiting for USB security dongle VID (iteration cap reached)" + break + fi + if [ -n "$start_dongle_wait" ]; then + local now=$(awk '{print $1}' /proc/uptime 2>/dev/null) + if [ -n "$now" ]; then + if awk -v s="$start_dongle_wait" -v n="$now" 'BEGIN{exit (n - s > 3.0) ? 0 : 1}'; then + DEBUG "Timeout waiting for USB security dongle VID after 3s" + break + fi fi fi + sleep 0.1 done - # If branding is already specific, USB is now ready and no re-scan is needed. - [ "$DONGLE_BRAND" != "USB Security dongle" ] && [ -n "$DONGLE_BRAND" ] && return local lsusb_out lsusb_out="$(lsusb)" DEBUG "lsusb output: $lsusb_out" @@ -953,7 +979,7 @@ recovery() { DEBUG "Board $CONFIG_BOARD - version $(fw_version) EC_VER: $(ec_version)" if [ "$CONFIG_TPM" = "y" ]; then - INFO "TPM: Extending PCR[4] to prevent any further secret unsealing" + INFO "TPM: Extending PCR[4] with content of string 'recovery' to prevent further secret unsealing" tpmr.sh extend -ix 4 -ic recovery fi @@ -973,6 +999,13 @@ recovery() { if [ -n "$*" ]; then WARN "$*" fi + + # Show PCR state when entering recovery shell + INFO "TPM: PCR state on entering recovery shell:" + pcrs | while IFS= read -r line; do + INFO "$line" + done + STATUS "Starting recovery shell" if [ -n "$RECOVERY_TTY" ]; then @@ -1344,7 +1377,7 @@ DEBUG_STACK() { pcrs() { if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then - tpm2 pcrread sha256 + tpm2 pcrread sha256 2>&1 | grep -v '^sha256:' elif [ "$CONFIG_TPM" = "y" ]; then head -8 /sys/class/tpm/tpm0/pcrs fi diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index a5e77391c..d7248402e 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -280,7 +280,6 @@ report_integrity_measurements() { DEBUG "report_integrity_measurements: querying HOTP token info" if _hotp_info="$(hotp_verification info 2>/dev/null)"; then hotp_state="$DONGLE_BRAND PRESENT" - STATUS_OK "$DONGLE_BRAND detected" hotpkey_fw_display "$_hotp_info" "$DONGLE_BRAND" elif [ "$DONGLE_BRAND" != "USB Security dongle" ]; then hotp_state="$DONGLE_BRAND INCOMPATIBLE" @@ -376,13 +375,14 @@ report_integrity_measurements() { awk -F: '/Signature key/ {gsub(/[[:space:]]/,"",$2); print $2; exit}') if [ -z "$_card_sig_fpr" ] || [ "$_card_sig_fpr" = "[none]" ]; then signing_key_state="DONGLE NOT PROVISIONED" - signing_key_guidance="$DONGLE_BRAND is connected but has no signing key (unprovisioned or wiped). Provision the dongle with the signing subkey, or perform OEM Factory Reset / Re-Ownership to start fresh with a new key." + signing_key_guidance="$DONGLE_BRAND is connected but has no signing key (unprovisioned). Provision the dongle with the signing subkey, or perform OEM Factory Reset / Re-Ownership to start fresh with a new key." else _rom_fprs=$(gpg --with-colons --list-keys 2>/dev/null | awk -F: '/^fpr/ {print $10}') if echo "$_rom_fprs" | grep -qF "$_card_sig_fpr"; then signing_key_state="DONGLE MATCHES ROM-TRUSTED KEY" signing_key_guidance="" + STATUS_OK "Signing key verified on $DONGLE_BRAND" else signing_key_state="DONGLE KEY NOT ROM-TRUSTED" signing_key_guidance="$DONGLE_BRAND has a signing key that does not match this firmware's trusted key. OEM Factory Reset / Re-Ownership is required to establish new trusted ownership." diff --git a/initrd/init b/initrd/init index 73bfde2f8..42db1b7b9 100755 --- a/initrd/init +++ b/initrd/init @@ -105,24 +105,24 @@ if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then DEBUG "NOTE: DO_WITH_DEBUG std_err and std_out will be redirected to /tmp/debug.log" fi -# report if we are in quiet mode, tell user measurements logs available under /tmp/debug.log +# report if we are in quiet mode, tell user measurements logs available under /tmp/debug.log and /tmp/measuring_trace.log if [ "$CONFIG_QUIET_MODE" = "y" ]; then # check origin of quiet mode setting =y: if it is under /etc/config.user then early cbfs-init outputs are not suppressible # if it is under /etc/config then early cbfs-init outputs are suppressible if grep -q 'CONFIG_QUIET_MODE="y"' /etc/config 2>/dev/null; then - echo "Quiet mode enabled from board configuration: refer to '/tmp/debug.log' for boot measurements traces" >/dev/tty0 + STATUS_OK "Quiet mode enabled from board configuration: refer to '/tmp/debug.log' and '/tmp/measuring_trace.log' for boot traces" else - echo "Runtime applied Quiet mode: refer to '/tmp/debug.log' for additional boot measurements traces past this point" >/dev/tty0 - echo "To suppress earlier boot measurements traces, enable CONFIG_QUIET_MODE=y in your board configuration at build time." >/dev/tty0 + STATUS_OK "Runtime applied Quiet mode: refer to '/tmp/debug.log' and '/tmp/measuring_trace.log' for additional boot traces past this point" + STATUS_OK "To suppress earlier boot traces, enable CONFIG_QUIET_MODE=y in your board configuration at build time." fi # If CONFIG_QUIET_MODE enabled in board config but disabled from Config->Configuration Settings -# warn that early boot measurements output was suppressed prior of this point +# warn that early boot measurements output was suppressed prior to this point elif [ "$CONFIG_QUIET_MODE" = "n" ]; then # if CONFIG_QUIET_MODE=n in /etc/config.user but CONFIG_QUIET_MODE=y in /etc/config then early cbfs-init outputs are suppressed - # both needs to be checked to determine if early boot measurements traces were suppressed + # both needs to be checked to determine if early boot measurement traces were suppressed if grep -q 'CONFIG_QUIET_MODE="y"' /etc/config 2>/dev/null && grep -q 'CONFIG_QUIET_MODE="n"' /etc/config.user 2>/dev/null; then - echo "Early boot measurements traces were suppressed per CONFIG_QUIET_MODE=y in your board configuration at build time (/etc/config)" >/dev/tty0 - echo "Runtime applied Quiet mode disabled: refer to '/tmp/debug.log' for cbfs-init related traces prior of this point" >/dev/tty0 + STATUS_OK "Early boot traces were suppressed per CONFIG_QUIET_MODE=y in your board configuration at build time (/etc/config)" + STATUS_OK "Runtime applied Quiet mode disabled: refer to '/tmp/debug.log' and '/tmp/measuring_trace.log' for cbfs-init related traces prior to this point" fi fi diff --git a/initrd/sbin/insmod.sh b/initrd/sbin/insmod.sh index 9e89197fc..a9a196855 100755 --- a/initrd/sbin/insmod.sh +++ b/initrd/sbin/insmod.sh @@ -39,9 +39,9 @@ if [ ! -r /sys/class/tpm/tpm0/pcrs -o ! -x /bin/tpm ]; then fi if [ -z "$tpm_missing" ]; then - INFO "TPM: Extending PCR[$MODULE_PCR] with $MODULE and parameters '$*' before loading" + INFO "TPM: Extending PCR[$MODULE_PCR] with content of module file '$MODULE' and parameters '$*' before loading" # Extend with the module parameters (even if they are empty) and the - # module. Changing the parameters or the module content will result in a + # module content. Changing the parameters or the module content will result in a # different PCR measurement. if [ -n "$*" ]; then TRACE_FUNC From 29f0e256c9318f947bb8ce819a925f9d38de34ca Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 1 May 2026 12:40:15 -0400 Subject: [PATCH 3/5] Fix TPM counter increment: auth retry on wrong passphrase (TPM1+TPM2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TPM1: counter_increment fell through to raw 'exec tpm' with no error handling — a mistyped owner passphrase killed the signing flow instantly. TPM2: counter_increment had no retry loop, unlike tpm2_seal and tpm2_counter_create which already re-prompted on auth failure. Changes: initrd/bin/tpmr.sh: - _tpm1_auth_retry: shared retry helper for TPM1 counter create/increment, re-prompts owner passphrase up to 3× on auth failure, reuses cached passphrase on first attempt (no double-prompt on happy path) - tpm1_counter_create / tpm1_counter_increment: thin wrappers around helper - tpm1_counter_read: named function (was catch-all exec tpm passthrough) - tpm1_seal: restore define+write retry loop removed by stdout-quirk fix, definespace output to /dev/null (ignored, nv_writevalue is the real test) - tpm2_counter_inc: add retry loop with should_retry flag to distinguish index-auth (-pwdc , no retry) from owner-auth (retry up to 3×) - All shred calls in retry loops: add 2>/dev/null || true guards - TPM1 dispatch: wire counter_read, counter_increment to named functions - TPM1/TPM2 extend/reset: improved PCR INFO messages initrd/bin/gui-init.sh: - reset_tpm: verify tpmr.sh reset exit code, show error and return to menu if reset fails (prevents operations on inconsistent TPM) initrd/etc/functions.sh: - increment_tpm_counter: replace misleading outer SINK_LOG with 2>/dev/null (DO_WITH_DEBUG already captures command stderr via SINK_LOG internally) - Document DO_WITH_DEBUG stderr handling in comments doc/tpm.md: document "out of resources" counter error and recovery flow doc/ux-patterns.md: UX patterns for TPM error recovery Signed-off-by: Thierry Laurion --- doc/tpm.md | 23 +++ doc/ux-patterns.md | 20 ++ initrd/bin/gui-init.sh | 35 +++- initrd/bin/tpmr.sh | 368 +++++++++++++++++++++++++++--------- initrd/etc/functions.sh | 28 ++- initrd/etc/gui_functions.sh | 8 + initrd/init | 25 ++- 7 files changed, 403 insertions(+), 104 deletions(-) diff --git a/doc/tpm.md b/doc/tpm.md index 2713ae002..3276ab75a 100644 --- a/doc/tpm.md +++ b/doc/tpm.md @@ -311,6 +311,29 @@ Failure conditions and their diagnostic messages: | TPM2: counter has `ownerwrite` but not `authwrite` | "TPM counter has invalid security policy." | | TPM2: counter has neither `authwrite` nor `ownerwrite` | "TPM counter is not writable." | | TPM2: counter attributes empty or unreadable | "TPM counter policy is corrupted." | +| TPM1: `counter_create` failed with "out of resources" (0x15) | "TPM has too many counters (out of resources). Reset the TPM from the GUI menu..." | + +When `check_tpm_counter` calls `tpmr counter_create` and it fails, the function +calls `set_tpm_reset_required` to create the `/tmp/secret/tpm_reset_required` +marker. This gates `prompt_update_checksums` on subsequent invocations, +preventing the user from retrying "sign /boot" until the TPM is reset. + +The "out of resources" (TPM 1.2 error 0x15) case occurs when the TPM already +has a counter from a prior firmware session but `/boot/kexec_rollback.txt` is +missing (e.g. after a firmware reflash that changed the boot partition layout). +The only safe recovery is to reset the TPM from the GUI menu, which clears all +counters and creates a fresh one. + +### TPM 1.2 stdout quirk (tpmtotp) + +The tpmtotp C toolkit (`counter_create.c`, `unsealfile.c`, `sealfile2.c` and +others) prints ALL output — both success and error messages — via `printf()` to +**stdout**, NOT stderr. This is a quirk of the tpmtotp codebase. + +All `tpm1_*` functions in `tpmr.sh` must therefore capture stdout (not just +stderr) to detect failures. Use `>"$file" 2>&1` to capture both streams, +and run TPM commands in subshells with `set +e` to avoid `set -e` killing the +script on non-zero exit codes. The exact diagnostic message from `fail_preflight` is shown directly in the error dialog — **not** a vague paraphrase. This tells the user and any support diff --git a/doc/ux-patterns.md b/doc/ux-patterns.md index 731a0dfb8..7e044fe61 100644 --- a/doc/ux-patterns.md +++ b/doc/ux-patterns.md @@ -186,6 +186,26 @@ that seals new TPM secrets. It verifies: If either check fails, the user is shown an error and the sealing operation is aborted. This prevents new TOTP/HOTP/DUK secrets from being sealed against a potentially compromised `/boot`. +### Gate bypass for "Reset the TPM" + +When the integrity gate fails specifically because `tpm_reset_required` is set (the +TPM has stale counters that need clearing), the "Reset the TPM" option in both the +TOTP failure whiptail menu and the TPM/TOTP/HOTP options whiptail menu uses a +gate-bypass pattern to allow the reset to proceed: + +```bash +# If the gate failed *because* TPM reset is required (stale counters), +# proceed to reset_tpm() which clears them and creates a fresh one. +if { gate_reseal_with_integrity_report || tpm_reset_required; } && reset_tpm; then + reseal_tpm_disk_decryption_key +fi +``` + +Without this bypass, selecting "Reset the TPM" triggers the integrity gate, +which calls `check_tpm_counter` which hits "out of resources" and collapses +the flow — the reset never executes. The `|| tpm_reset_required` lets the +reset proceed when the gate failure is itself a symptom of needing the reset. + --- ## GPG User PIN caching diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index e5a8faccf..80953af7d 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 ;; @@ -808,8 +821,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 ;; @@ -839,7 +855,18 @@ 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. + if ! tpmr.sh reset "$tpm_owner_passphrase" 2>/tmp/error; then + ERROR=$(tail -n 1 /tmp/error | fold -s) + 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 diff --git a/initrd/bin/tpmr.sh b/initrd/bin/tpmr.sh index 44c526782..1abe76243 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,9 +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 index pwd should_retry="n" local inc_args=() while true; do case "$1" in @@ -291,6 +315,10 @@ tpm2_counter_inc() { ;; -pwdc) pwd="$2" + # Only retry when a non-empty passphrase was provided (owner + # auth). Empty passphrase (index auth) is intentionally + # unauth'd — no passphrase to re-prompt. + [ -n "$pwd" ] && should_retry="y" shift 2 ;; *) @@ -298,42 +326,155 @@ tpm2_counter_inc() { ;; esac done - if [ -n "$pwd" ]; then - inc_args=(-C o -P "$(tpm2_password_hex "$pwd")") - 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 local attempt=0 + local index_auth_tried="n" # bare nvincrement without -C/-P uses NV index handle auth while true; do attempt=$((attempt + 1)) - prompt_tpm_owner_password - tpm_owner_passphrase="$(cat /tmp/secret/tpm_owner_passphrase)" + if [ -n "$pwd" ]; then + inc_args=(-C o -P "$(tpm2_password_hex "$pwd")") + elif [ "$index_auth_tried" = "n" ]; then + index_auth_tried="y" + # Counters created by nvdefine without -p have empty NV index auth. + # For nvincrement, when -C isn't explicitly passed, tpm2-tools uses + # the NV index handle to authorize (per tpm2_nvincrement man page). + # Try this first before falling back to -C o (owner hierarchy auth). + 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 + prompt_tpm_owner_password + pwd="$tpm_owner_passphrase" + inc_args=(-C o -P "$(tpm2_password_hex "$pwd")") + should_retry="y" + else + prompt_tpm_owner_password + pwd="$tpm_owner_passphrase" + inc_args=(-C o -P "$(tpm2_password_hex "$pwd")") + should_retry="y" + fi TMP_ERR_FILE=$(mktemp) - if tpm counter_create -pwdo "$tpm_owner_passphrase" "$@" 2>"$TMP_ERR_FILE"; then + if tpm2 nvincrement "${inc_args[@]}" "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 + 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 + if [ "$should_retry" != "y" ]; then + DIE "Unable to increment TPM2 counter. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi 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." + DIE "Unable to increment TPM2 counter 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..." + WARN "Failed to increment TPM counter (bad passphrase?). Retrying..." + pwd="" # force re-prompt 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." + DIE "Unable to increment TPM2 counter. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." fi done } +# _tpm1_auth_retry - Shared retry helper for TPM1 commands needing owner auth. +# +# Executes a TPM1 command with owner auth, re-prompting and retrying up to +# 3 times on authorization failures. Writes the command's stdout to the +# function's stdout on success. +# +# Caching behaviour: prompt_tpm_owner_password reuses a previously-cached +# passphrase (/tmp/secret/tpm_owner_passphrase) if one exists. Callers like +# increment_tpm_counter pre-prompt and cache, so the first attempt reuses +# that value — no double-prompt on the happy path. On auth failure the +# cache is shredded; the next iteration's prompt_tpm_owner_password will +# actually ask the user for fresh input. +# +# NOTE: tpmtotp C code prints ALL output (success and errors) via printf() +# to stdout, NOT stderr. We capture stdout to detect failures. +# +# Usage: _tpm1_auth_retry