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/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/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..6ba2d16d7 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -90,8 +90,10 @@ verify_global_hashes() { TMP_PACKAGE_TRIGGER_POST="/tmp/kexec/kexec_package_trigger_post.txt" if verify_checksums /boot; then + DEBUG "clean_boot_check: boot hashes match kexec.sig signature — system integrity OK" return 0 elif [[ ! -f "$TMP_HASH_FILE" || ! -f "$TMP_TREE_FILE" ]]; then + DEBUG "clean_boot_check: hash/tree files missing under /boot — first boot or upgrade detected" if (whiptail_error --title 'ERROR: Missing File!' \ --yesno "One of the files containing integrity information for /boot is missing!\n\nIf you are setting up heads for the first time or upgrading from an older version, select Yes to create the missing files.\n\nOtherwise this could indicate a compromise and you should select No to return to the main menu.\n\nWould you like to create the missing files now?" 0 80); then if update_checksums; then @@ -191,11 +193,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 } @@ -244,16 +254,17 @@ gate_reseal_with_integrity_report() { if [ -x /bin/hotp_verification ]; then token_ok="n" while [ "$token_ok" != "y" ]; do + DEBUG "gate_reseal_with_integrity_report: enabling USB for $DONGLE_BRAND HOTP token verification" enable_usb # wait_for_gpg_card already called release_scdaemon on success, # 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 +296,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 @@ -353,6 +363,7 @@ update_totp() { if [ "$CONFIG_TPM" != "y" ]; then TOTP="NO TPM" else + DEBUG "update_totp: unsealing TOTP secret from TPM" TOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-totp.sh) if [ $? -ne 0 ]; then local totp_menu_text @@ -362,6 +373,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=$( @@ -411,8 +423,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 ;; @@ -437,6 +454,8 @@ update_hotp() { local hotp_token_info hotp_exit attempt # Ensure dongle is present; capture info for PIN counter display + STATUS "Checking $DONGLE_BRAND presence" + DEBUG "update_hotp: querying $DONGLE_BRAND HOTP token (hotp_verification info)" 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 @@ -449,12 +468,14 @@ update_hotp() { BG_COLOR_MAIN_MENU="warning" return fi + DEBUG "update_hotp: retrying $DONGLE_BRAND HOTP token after user inserted dongle" if ! hotp_token_info="$(hotp_verification info)"; then HOTP="Error checking code, Insert $DONGLE_BRAND and retry" BG_COLOR_MAIN_MENU="warning" return fi fi + DEBUG "update_hotp: $DONGLE_BRAND HOTP token info: $(echo "$hotp_token_info" | tr '\n' ' ')" # Show dongle firmware version with color coding so users know when to upgrade hotpkey_fw_display "$hotp_token_info" "$DONGLE_BRAND" @@ -474,12 +495,16 @@ 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 (attempt $attempt/3)" + DEBUG "update_hotp: calling hotp_verification check (attempt $attempt/3)" hotp_verification check "$HOTP" hotp_exit=$? + DEBUG "update_hotp: hotp_verification check exited with code $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 +679,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 @@ -806,8 +830,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 +864,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 @@ -867,8 +905,11 @@ 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. + DEBUG "reset_tpm: enabling USB for GPG card signing and dongle detection" + enable_usb wait_for_gpg_card || true while true; do GPG_KEY_COUNT=$(gpg -K 2>/dev/null | wc -l) @@ -876,7 +917,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 +936,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 } @@ -951,6 +986,7 @@ force_unsafe_boot() { TRACE_FUNC if [ -x /bin/hotp_verification ]; then + DEBUG "gui-init: HOTP verification supported, enabling USB for security dongle detection" enable_usb fi @@ -959,9 +995,11 @@ fi detect_usb_security_dongle_branding if detect_boot_device; then + DEBUG "Boot device detected; running clean boot integrity check" # /boot device with installed OS found clean_boot_check else + DEBUG "No boot device auto-detected; falling back to interactive mount_boot" # can't determine /boot device or no OS installed, # so fall back to interactive selection mount_boot @@ -1053,6 +1091,8 @@ EOF [ -n "$preflight_error_msg" ] && DEBUG "Rollback preflight failure: $preflight_error_msg" fi done +else + DEBUG "Rollback preflight OK: TPM counter validated successfully" fi # detect whether any GPG keys exist in the keyring, if not, initialize that first diff --git a/initrd/bin/kexec-boot.sh b/initrd/bin/kexec-boot.sh index 4043c3118..68b8aa90c 100755 --- a/initrd/bin/kexec-boot.sh +++ b/initrd/bin/kexec-boot.sh @@ -166,6 +166,7 @@ if [ "$CONFIG_DEBUG_OUTPUT" = "y" ];then fi if [ "$CONFIG_TPM" = "y" ]; then + DEBUG "Flushing TPM contexts and locking platform hierarchy before kexec" tpmr.sh kexec_finalize 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/kexec-unseal-key.sh b/initrd/bin/kexec-unseal-key.sh index 50d07368a..ae1eb31f5 100755 --- a/initrd/bin/kexec-unseal-key.sh +++ b/initrd/bin/kexec-unseal-key.sh @@ -29,6 +29,7 @@ for tries in 1 2 3; do # Show updating timestamp/TOTP until user presses Esc to continue to the # passphrase prompt. This gives the user context while they prepare to # type the LUKS passphrase. + DEBUG "kexec-unseal-key: displaying fresh TOTP code (attempt $tries/3)" show_totp_until_esc STATUS "Unlocking LUKS with TPM Disk Unlock Key" INPUT "Enter LUKS TPM Disk Unlock Key passphrase (blank to abort):" -r -s tpm_password @@ -36,6 +37,7 @@ for tries in 1 2 3; do DIE "Aborting unseal disk encryption key" fi + DEBUG "kexec-unseal-key: attempting DUK unseal from TPM (attempt $tries/3)" if DO_WITH_DEBUG --mask-position 6 \ tpmr.sh unseal "$TPM_INDEX" "0,1,2,3,4,5,6,7" "$TPM_SIZE" \ "$key_file" "$tpm_password"; then diff --git a/initrd/bin/key-init.sh b/initrd/bin/key-init.sh index c139e3f26..3db5a382e 100755 --- a/initrd/bin/key-init.sh +++ b/initrd/bin/key-init.sh @@ -22,11 +22,13 @@ if [ -d /.gnupg/keys ]; then # This is legacy location for user's keys. cbfs-init takes for granted that keyring and trustdb are in /.gnupg # oem-factory-reset.sh generates keyring and trustdb which cbfs-init dumps to /.gnupg # TODO: Remove individual key imports. This is still valid for distro keys only below. + DEBUG "Importing $(ls /.gnupg/keys/*.key /.gnupg/keys/*.asc 2>/dev/null | wc -l) user GPG key(s) from /.gnupg/keys" STATUS "Importing user GPG keys" gpg --import /.gnupg/keys/*.key /.gnupg/keys/*.asc 2>/dev/null || WARN "Importing user's keys failed" fi # Import OS distribution signing keys used to authenticate ISO boots +DEBUG "Importing $(ls /etc/distro/keys/* 2>/dev/null | wc -l) distro signing key(s)" STATUS "Loading OS distribution signing keys for ISO boot authentication" gpg --homedir=/etc/distro/ --import /etc/distro/keys/* 2>/dev/null || WARN "Importing distro keys failed" #Set distro keys trust level to ultimate (trust anything that was signed with these keys) @@ -34,5 +36,6 @@ gpg --homedir=/etc/distro/ --list-keys --fingerprint --with-colons|sed -E -n -e gpg --homedir=/etc/distro/ --update-trust 2>/dev/null || WARN "Updating distro keys trust failed" # Add user's key so self-signed ISOs can also be booted from USB +DEBUG "Exporting user GPG keys to distro keyring for self-signed ISO support" STATUS "Adding user GPG key as trusted for ISO signing" gpg --export | gpg --homedir=/etc/distro/ --import 2>/dev/null || WARN "Adding user's keys to distro keys failed" 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..267d056ad 100755 --- a/initrd/bin/seal-hotpkey.sh +++ b/initrd/bin/seal-hotpkey.sh @@ -61,6 +61,7 @@ DO_WITH_DEBUG killall gpg-agent scdaemon >/dev/null 2>&1 || true # While making sure the key is inserted, capture the status so we can check how # many PIN attempts remain +DEBUG "Querying $DONGLE_BRAND HOTP token status (hotp_verification info)" if ! hotp_token_info="$(hotp_verification info)"; then INPUT "Insert your $DONGLE_BRAND and press Enter to configure it" if ! hotp_token_info="$(hotp_verification info)"; then @@ -69,6 +70,7 @@ if ! hotp_token_info="$(hotp_verification info)"; then DIE "Unable to find $DONGLE_BRAND" fi fi +DEBUG "$DONGLE_BRAND HOTP token info retrieved: $(echo "$hotp_token_info" | head -1)" # Re-detect branding now that the dongle is confirmed present. detect_usb_security_dongle_branding @@ -101,6 +103,7 @@ fi admin_pin_retries="${admin_pin_retries:-0}" DEBUG "HOTP related PIN retry counter is $admin_pin_retries" # Show dongle firmware version with color coding so users know when to upgrade +DEBUG "Querying $DONGLE_BRAND firmware and Secrets App versions" hotpkey_fw_display "$hotp_token_info" "$DONGLE_BRAND" # Re-query and display the current PIN retry counter before each manual prompt. @@ -109,6 +112,7 @@ hotpkey_fw_display "$hotp_token_info" "$DONGLE_BRAND" # prompt_message is already set for the device type (NK3 vs older), reuse it. show_pin_retries() { local info + DEBUG "show_pin_retries: re-querying $DONGLE_BRAND PIN retry counter" info="$(hotp_verification info 2>/dev/null)" || true if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then admin_pin_retries=$(echo "$info" | grep "Secrets app PIN counter:" | cut -d ':' -f 2 | tr -d ' ') @@ -141,8 +145,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 @@ -171,6 +179,7 @@ if [ "$admin_pin_status" -ne 0 ]; then # Default PIN tried & failed (3 -> 2 remaining) -> max_attempts = min(2-1, 3) = 1 # Counter read failed (0 or empty) -> max_attempts = 3 (fallback, don't block) # Re-read counter without displaying (loop will show it) + DEBUG "Re-reading $DONGLE_BRAND PIN counter after default PIN attempt" info="$(hotp_verification info 2>/dev/null)" || true if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then admin_pin_retries=$(echo "$info" | grep "Secrets app PIN counter:" | cut -d ':' -f 2 | tr -d ' ') 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..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 @@ -257,8 +269,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() { @@ -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 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