From b1ba9d875f875dc834ea3b72984cedd38ec0bb64 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Mon, 27 Apr 2026 15:30:20 -0400 Subject: [PATCH 1/8] initrd: wait for known USB dongle VID before branding detection Delay branding detection until a known USB security dongle vendor ID appears in sysfs, then run lsusb matching. - Add bounded VID polling in detect_usb_security_dongle_branding() - Keep branding fallback path when no known VID appears - Initialize USB in integrity report path before branding detection Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 20 ++++++++++++++++++++ initrd/etc/gui_functions.sh | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 66e9bb9b6..7d00e6a63 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -515,6 +515,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. if [ "$DONGLE_BRAND" != "USB Security dongle" ] && [ -n "$DONGLE_BRAND" ]; then DEBUG "Branding already specific ($DONGLE_BRAND), skipping lsusb scan" diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index 0f1cb40db..b57e9d5e1 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 From 9eedbce8fba99efe821743603f899a6410b8750e Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 29 Apr 2026 18:18:26 -0400 Subject: [PATCH 2/8] initrd: standardize STATUS NOTE WARN and INFO messaging semantics Normalize user-visible logging across initrd scripts and documentation so output levels are applied consistently. - Align STATUS/STATUS_OK usage for action start and success - Reserve NOTE for user guidance requiring attention - Keep WARN/ERROR messaging actionable and consistent - Update doc/logging.md to match runtime behavior 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 | 60 ++++++++--- initrd/etc/gui_functions.sh | 4 +- initrd/init | 16 +-- initrd/sbin/insmod.sh | 4 +- 21 files changed, 251 insertions(+), 133 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 99bb2acae..fa4964b9a 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -257,11 +257,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" @@ -293,10 +293,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 @@ -370,6 +369,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=$( @@ -450,6 +450,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 @@ -487,12 +488,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 @@ -667,7 +670,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 @@ -896,8 +898,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) @@ -905,7 +909,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 @@ -925,14 +928,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 4d67d1d96..f7125cfa0 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" } @@ -1049,6 +1057,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 @@ -1107,9 +1116,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/" @@ -1140,7 +1149,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 @@ -1148,12 +1157,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 @@ -1366,7 +1375,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 @@ -1389,12 +1398,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 @@ -1510,6 +1518,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 @@ -1554,6 +1563,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" @@ -1588,12 +1598,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 @@ -1635,7 +1647,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 e405fb7a1..af544cd8c 100755 --- a/initrd/bin/seal-hotpkey.sh +++ b/initrd/bin/seal-hotpkey.sh @@ -139,8 +139,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 9dd7609a3..46f4581d8 100755 --- a/initrd/bin/tpmr.sh +++ b/initrd/bin/tpmr.sh @@ -269,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() { @@ -849,7 +849,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 @@ -1101,7 +1101,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" @@ -1157,20 +1157,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 @@ -1183,7 +1186,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. @@ -1211,9 +1216,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) @@ -1238,7 +1253,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 7d00e6a63..23150adef 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}" @@ -510,14 +521,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 @@ -525,16 +545,23 @@ 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. if [ "$DONGLE_BRAND" != "USB Security dongle" ] && [ -n "$DONGLE_BRAND" ]; then DEBUG "Branding already specific ($DONGLE_BRAND), skipping lsusb scan" @@ -956,7 +983,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 @@ -976,6 +1003,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 @@ -1352,7 +1386,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 b57e9d5e1..29d86e171 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" @@ -377,13 +376,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 1d4f249b1b92dc6a6be26839cec78ad5a1065a71 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 6 May 2026 17:45:12 -0400 Subject: [PATCH 3/8] initrd/etc/functions.sh: add 15s cancellable dongle VID wait Relative to origin/master (c2fb345e7d): - Introduces wait_for_usb_security_dongle_vid() with visible countdown. - Adds user-cancel path during wait (keyboard/serial). Current state: - Wait exits early once a known VID appears. - Wait times out after 15 seconds to avoid indefinite boot stall. - Branding fallback path remains unchanged. Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 159 ++++++++++++++++++++++++++++++---------- 1 file changed, 122 insertions(+), 37 deletions(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 23150adef..05ad3c384 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -499,6 +499,122 @@ pin_color() { # 1050:0115 Yubikey 4/5 (OTP+U2F+CCID) - FIDO+CCID # 1050:0404 Yubikey 5 (FIDO+CCID) +# Returns 0 if the given tty path is a serial console. +heads_tty_is_serial() { + case "$1" in + /dev/ttyS* | /dev/ttyUSB* | /dev/ttyAMA* | /dev/ttyO*) return 0 ;; + *) return 1 ;; + esac +} + +# Returns 0 if a known USB security dongle VID is present in sysfs. +# Known VIDs: 20a0 (Nitrokey/Canokey QEMU), 316d (Librem Key), 16d0 (Canokey), 1050 (Yubikey) +usb_security_dongle_vid_present() { + grep -l -E "20a0|316d|16d0|1050" /sys/bus/usb/devices/*/idVendor 2>/dev/null | grep -q . +} + +# Wait up to 15 seconds for a known USB security dongle VID to appear in sysfs. +# Displays a countdown line updated in place (mirrors show_totp_until_esc pattern). +# Framebuffer: any key cancels. Serial (ttyS*, ttyUSB*, ttyAMA*, ttyO*): Enter cancels. +# Returns 0 if a dongle VID is detected, 1 if timed out or cancelled. +wait_for_usb_security_dongle_vid() { + TRACE_FUNC + local interactive_tty="${HEADS_TTY}" + local is_serial=0 + local deadline remaining ch status_line last_remaining="" + + if heads_tty_is_serial "$interactive_tty"; then + is_serial=1 + fi + + # Reserve a countdown line; drain stray input on framebuffer. + if [ -n "$interactive_tty" ]; then + printf "\n" >"$interactive_tty" 2>/dev/null + else + printf "\n" + fi + if [ "$is_serial" = "0" ]; then + if [ -n "$interactive_tty" ]; then + while IFS= read -r -t 0 -n 1 junk <"$interactive_tty" 2>/dev/null; do :; done + else + while IFS= read -r -t 0 -n 1 junk; do :; done + fi + fi + + deadline=$(( $(date +%s) + 15 )) + + while :; do + # Exit immediately when a known VID appears. + if usb_security_dongle_vid_present; then + if [ -n "$interactive_tty" ]; then + printf "\n\n" >"$interactive_tty" 2>/dev/null + else + printf "\n\n" + fi + DEBUG "USB security dongle VID detected in sysfs (countdown wait)" + return 0 + fi + + remaining=$(( deadline - $(date +%s) )) + if [ "$remaining" -le 0 ]; then + if [ -n "$interactive_tty" ]; then + printf "\n\n" >"$interactive_tty" 2>/dev/null + else + printf "\n\n" + fi + DEBUG "Timeout waiting for USB security dongle VID after 15s" + return 1 + fi + + # Rewrite countdown only when the value changes to avoid flicker. + if [ "$remaining" != "$last_remaining" ]; then + last_remaining="$remaining" + local sec_display + sec_display=$(printf "%02d" "$remaining") + if [ "$is_serial" = "1" ]; then + status_line="\033[1mWaiting for USB security dongle... ${sec_display}s | Press Enter to skip\033[0m" + else + status_line="\033[1mWaiting for USB security dongle... ${sec_display}s | Press Esc to skip\033[0m" + fi + if [ -n "$interactive_tty" ]; then + printf "\r%b\033[K" "$status_line" >"$interactive_tty" 2>/dev/null + else + printf "\r%b\033[K" "$status_line" + fi + fi + + if [ "$is_serial" = "1" ]; then + if [ -n "$interactive_tty" ]; then + if IFS= read -r -t 1 ch <"$interactive_tty" 2>/dev/null; then + printf "\n\n" >"$interactive_tty" 2>/dev/null + DEBUG "User cancelled USB dongle wait (Enter on serial)" + return 1 + fi + else + if IFS= read -r -t 1 ch; then + printf "\n\n" + DEBUG "User cancelled USB dongle wait (Enter on serial)" + return 1 + fi + fi + else + if [ -n "$interactive_tty" ]; then + if IFS= read -r -t 0.2 -n 1 ch <"$interactive_tty" 2>/dev/null; then + printf "\n\n" >"$interactive_tty" 2>/dev/null + DEBUG "User cancelled USB dongle wait (key on framebuffer)" + return 1 + fi + else + if IFS= read -r -t 0.2 -n 1 ch; then + printf "\n\n" + DEBUG "User cancelled USB dongle wait (key on framebuffer)" + return 1 + fi + fi + fi + done +} + # 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) @@ -531,42 +647,11 @@ detect_usb_security_dongle_branding() { fi wait_for_usb_devices + # Wait up to 15s for a known dongle VID to appear; user can press Esc (fb) or Enter (serial) to skip. + # Best-effort wait only — branding detection continues via lsusb regardless. + wait_for_usb_security_dongle_vid || true # 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 - DEBUG "USB security dongle VID detected in sysfs" - break - fi - - # 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. - if [ "$DONGLE_BRAND" != "USB Security dongle" ] && [ -n "$DONGLE_BRAND" ]; then - DEBUG "Branding already specific ($DONGLE_BRAND), skipping lsusb scan" - return - fi local lsusb_out lsusb_out="$(lsusb)" DEBUG "lsusb output: $lsusb_out" @@ -2842,9 +2927,9 @@ show_totp_until_esc() { # tcsetattr, but some serial line disciplines block indefinitely despite the # -t timeout. On serial we accept Enter (line-mode read) instead of Esc. local is_serial=0 - case "$interactive_tty" in - /dev/ttyS* | /dev/ttyUSB* | /dev/ttyAMA* | /dev/ttyO*) is_serial=1 ;; - esac + if heads_tty_is_serial "$interactive_tty"; then + is_serial=1 + fi if [ -n "$interactive_tty" ]; then printf "\n" >"$interactive_tty" 2>/dev/null # reserve a line for updates From 4e8c7c3dbce4db498c380053ab88281d4f0fd754 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 6 May 2026 17:45:12 -0400 Subject: [PATCH 4/8] initrd/etc/functions.sh: harden recovery shell handoff Relative to origin/master (c2fb345e7d): - Aligns pause_recovery() with hardened recovery checks. - Drains serial input queue before launching recovery shell. Current state: - Buffered serial bytes are no longer interpreted as shell commands. - PCR extension/auth path is preserved before shell handoff. Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 46 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 05ad3c384..61d396b7d 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -1095,6 +1095,14 @@ recovery() { INFO "$line" done + # Drain any queued serial input before starting the interactive shell. + # This avoids stale bytes being interpreted as bash commands on entry. + if [ -n "$RECOVERY_TTY" ]; then + while IFS= read -r -t 0 -n 1 _junk <"$RECOVERY_TTY" 2>/dev/null; do :; done + else + while IFS= read -r -t 0 -n 1 _junk 2>/dev/null; do :; done + fi + STATUS "Starting recovery shell" if [ -n "$RECOVERY_TTY" ]; then @@ -1115,7 +1123,43 @@ recovery() { pause_recovery() { TRACE_FUNC INPUT "Press Enter to proceed to recovery shell" - recovery $* + + # Re-detect TTY so INPUT uses the correct device + detect_heads_tty + + if [ "$CONFIG_TPM" = "y" ]; then + INFO "TPM: Extending PCR[4] with content of string 'recovery' to prevent further secret unsealing" + tpmr.sh extend -ix 4 -ic recovery + fi + + gpg_auth + + if [ -n "$*" ]; then + WARN "$*" + fi + + INFO "TPM: PCR state on entering recovery shell:" + pcrs | while IFS= read -r line; do + INFO "$line" + done + + # Drain any queued serial input before starting the interactive shell. + # This avoids stale bytes being interpreted as bash commands on entry. + if [ -n "$RECOVERY_TTY" ]; then + while IFS= read -r -t 0 -n 1 _junk <"$RECOVERY_TTY" 2>/dev/null; do :; done + else + while IFS= read -r -t 0 -n 1 _junk 2>/dev/null; do :; done + fi + + STATUS "Starting recovery shell" + + if [ -n "$RECOVERY_TTY" ]; then + setsid /bin/bash <>"$RECOVERY_TTY" >&0 2>&0 + elif [ -x /bin/setsid ]; then + /bin/setsid -c /bin/bash + else + /bin/bash + fi } combine_configs() { From 07faae9457f27176c9cedbcac57385f61fb6f4d8 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Sat, 2 May 2026 18:54:37 -0400 Subject: [PATCH 5/8] initrd/init: switch to PID-tracked bootscript respawn Relative to origin/master (c2fb345e7d): - Replaces asymmetric script handling with PID-tracked respawn loop. - Tracks process ids per console path and restarts only when dead. Current state: - Main and auxiliary consoles respawn predictably without tight loops. - Existing cttyhack/agetty split is preserved. Signed-off-by: Thierry Laurion --- initrd/init | 45 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/initrd/init b/initrd/init index 42db1b7b9..4b8ff6a4d 100755 --- a/initrd/init +++ b/initrd/init @@ -256,16 +256,41 @@ else fi if [ -x "$CONFIG_BOOTSCRIPT" ]; then - echo '***** Normal boot:' $CONFIG_BOOTSCRIPT - - if [ -x /bin/setsid ] && [ -x /bin/agetty ]; then - for console in $CONFIG_BOOT_EXTRA_TTYS; do - setsid agetty -aroot -l"$CONFIG_BOOTSCRIPT" "$console" linux & - done - fi - - #Setup a control tty so that all terminals outputs correct tty when tty is called - exec cttyhack "$CONFIG_BOOTSCRIPT" + DEBUG "Entering boot script respawn loop: $CONFIG_BOOTSCRIPT" + + #Never DIE in init: PID-track all boot script instances and + #restart only the ones that died. Uses `wait` to sleep until + #a child exits rather than polling a single blocking instance. + while true; do + echo '***** Normal boot:' $CONFIG_BOOTSCRIPT + + # Main console: cttyhack sets up the controlling terminal. + main_pid_file="/tmp/bootscript_pid_main" + main_pid=$(cat "$main_pid_file" 2>/dev/null) + if [ -z "$main_pid" ] || ! kill -0 "$main_pid" 2>/dev/null; then + DEBUG "Starting boot script on main console" + cttyhack "$CONFIG_BOOTSCRIPT" & + echo $! >"$main_pid_file" + fi + + # Extra TTYs: agetty provides separate session + controlling + # terminal per console (avoids shared-ctty GPG issues). + if [ -x /bin/setsid ] && [ -x /bin/agetty ]; then + for console in $CONFIG_BOOT_EXTRA_TTYS; do + pid_file="/tmp/bootscript_pid_${console//\//_}" + pid=$(cat "$pid_file" 2>/dev/null) + if [ -z "$pid" ] || ! kill -0 "$pid" 2>/dev/null; then + DEBUG "Starting agetty on $console" + setsid agetty -aroot -l"$CONFIG_BOOTSCRIPT" "$console" linux & + echo $! >"$pid_file" + fi + done + fi + + # Wait for any boot script instance to die before checking + # which one needs restart. Prevents busy-polling. + wait + done else # wait for boot via network to occur pause_recovery 'Override network boot. Entering recovery shell' From 82883ccfbde5e6ec63308a7ee729204a9594a7c4 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 6 May 2026 18:14:28 -0400 Subject: [PATCH 6/8] initrd/init: restore startup DEBUG decision traces Relative to origin/master (c2fb345e7d): - Reintroduces DEBUG lines for critical startup branching decisions. - Covers TPM/USB gating, recovery paths, and boot flow selection. Current state: - Early-boot decision points are observable in debug logs. - Runtime behavior is unchanged; this is diagnostics-only. Signed-off-by: Thierry Laurion --- initrd/init | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/initrd/init b/initrd/init index 4b8ff6a4d..32fd33249 100755 --- a/initrd/init +++ b/initrd/init @@ -146,6 +146,7 @@ fi # set CONFIG_TPM dynamically off before init if no TPM device is present if [ ! -e /dev/tpm0 ]; then + DEBUG "No TPM device (/dev/tpm0) found; disabling CONFIG_TPM and CONFIG_TPM2_TOOLS" CONFIG_TPM='n' CONFIG_TPM2_TOOLS='n' fi @@ -162,6 +163,7 @@ else fi if [ "$CONFIG_TPM" = "y" ]; then + DEBUG "TPM enabled: initializing TPM2 encrypted sessions" # Initialize tpm2 encrypted sessions here tpmr.sh startsession fi @@ -180,6 +182,7 @@ export GPG_TTY=/dev/console # Setup recovery serial shell if [ ! -z "$CONFIG_BOOT_RECOVERY_SERIAL" ]; then + DEBUG "Recovery serial console enabled on $CONFIG_BOOT_RECOVERY_SERIAL" stty -F "$CONFIG_BOOT_RECOVERY_SERIAL" 115200 pause_recovery 'Serial console recovery shell' \ <"$CONFIG_BOOT_RECOVERY_SERIAL" \ @@ -188,6 +191,7 @@ fi # load USB modules for boards using a USB keyboard if [ "$CONFIG_USB_KEYBOARD_REQUIRED" = y ] || [ "$CONFIG_USER_USB_KEYBOARD" = "y" ]; then + DEBUG "USB keyboard required by board or user config; loading USB modules" enable_usb fi @@ -204,11 +208,13 @@ read \ echo if [ "$boot_option" = "r" ]; then + DEBUG "User requested recovery shell (key 'r')" # Start an interactive shell recovery 'User requested recovery shell' # just in case... exit elif [ "$boot_option" = "o" ]; then + DEBUG "User requested OEM Factory Reset mode (key 'o')" # Launch OEM Factory Reset mode echo -e "***** Entering OEM Factory Reset mode\n" >/dev/tty0 oem-factory-reset.sh --mode oem @@ -217,6 +223,7 @@ elif [ "$boot_option" = "o" ]; then fi if [ "$CONFIG_BASIC" = "y" ]; then + DEBUG "BASIC mode: tamper detection bypassed, overriding boot script" echo -e "***** BASIC mode: tamper detection disabled\n" >/dev/tty0 fi @@ -236,19 +243,25 @@ fi setconsolefont.sh if [ "$CONFIG_BASIC" = "y" ]; then + DEBUG "Overriding boot script to gui-init-basic.sh, disabling HOTP" CONFIG_BOOTSCRIPT=/bin/gui-init-basic.sh export CONFIG_HOTPKEY=n fi # Perform board-specific init if present if [ -x /bin/board-init.sh ]; then + DEBUG "Running board-specific init: /bin/board-init.sh" /bin/board-init.sh +else + DEBUG "No board-init.sh found; skipping board-specific init" fi if [ ! -x "$CONFIG_BOOTSCRIPT" -a ! -x "$CONFIG_BOOTSCRIPT_NETWORK" ]; then + DEBUG "No boot script found (CONFIG_BOOTSCRIPT=$CONFIG_BOOTSCRIPT, CONFIG_BOOTSCRIPT_NETWORK=$CONFIG_BOOTSCRIPT_NETWORK); entering recovery" recovery 'Boot script missing? Entering recovery shell' else if [ -x "$CONFIG_BOOTSCRIPT_NETWORK" ]; then + DEBUG "Starting network boot script: $CONFIG_BOOTSCRIPT_NETWORK" echo '***** Network Boot:' $CONFIG_BOOTSCRIPT_NETWORK $CONFIG_BOOTSCRIPT_NETWORK echo '***** Network Boot Completed:' $CONFIG_BOOTSCRIPT_NETWORK From 90dacc9f112e8d127dd031ab4dedc6bfc10d1b4b Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 6 May 2026 18:20:16 -0400 Subject: [PATCH 7/8] Makefile: sanitize branch token in artifact filenames Relative to origin/master (c2fb345e7d): - GIT_BRANCH-derived token used in artifact names is sanitized. - Slashes/whitespace in branch names no longer create invalid output paths. Current state: - Artifact basenames remain traceable to branch context. - Build copy/install steps no longer fail on branch names like feature/foo. Signed-off-by: Thierry Laurion --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d2e4bd966..759b3af25 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,8 @@ GIT_STATUS := $(shell \ fi) HEADS_GIT_VERSION := $(shell git describe --abbrev=7 --tags --dirty) GIT_TIMESTAMP := $(shell git log -1 --format=%cd --date=format:'%Y%m%d-%H%M%S') -GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD | cut -c1-30) +# Keep branch identifier path-safe for artifact filenames (e.g. feature/foo -> feature_foo). +GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD | tr '/[:space:]' '__' | cut -c1-30) # Release builds: HEAD is exactly on a tag AND working tree is clean. # Dev builds: any untagged commit, commits ahead of a tag, or dirty tree. # Dev filenames include timestamp + branch for traceability without From 3867858f5444ac3fbad1a56fb30ed5d17afc8b7f Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 6 May 2026 19:40:21 -0400 Subject: [PATCH 8/8] initrd/docs: describe DUK as 128-byte random key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct DUK documentation from "128 characters" to "128 bytes" in runtime status/error messages and security/TPM docs. Add explicit notation of the brute-force space (2^1024) to clarify entropy magnitude. The DUK is 128 bytes from /dev/urandom (1024 bits of entropy). Brute-force time grows exponentially with entropy: a 128-byte random secret has 2^1024 possible values, requiring an attacker to try about 2^1023 guesses on average. Using the formula time ≈ 2^(H-1)/R (where H is entropy in bits, R is guesses/second): - At 10^12 guesses/second, expected time is ~2^1023/10^12 seconds - This is unimaginably longer than the age of the universe (~4×10^17 seconds) - Every bit of entropy doubles the search space, making exponential growth the key property For practical comparison: 80 Diceware words provide ~1032 bits of entropy, roughly comparable to 128 random bytes. Every attack rate is dominated by the exponential requirement. Important caveat: this protection applies only to offline brute-force against a correctly stored secret. Online rate limits or poor storage would override these estimates. Signed-off-by: Thierry Laurion --- doc/security-model.md | 2 +- doc/tpm.md | 7 ++++--- initrd/bin/kexec-seal-key.sh | 10 +++++++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/doc/security-model.md b/doc/security-model.md index 2f16e7714..5c394286b 100644 --- a/doc/security-model.md +++ b/doc/security-model.md @@ -420,7 +420,7 @@ creating additional hardware binding: The LUKS Disk Unlock Key (DUK) is a random binary key that: -1. Is generated from `/dev/urandom` by `kexec-seal-key` (128 characters — 1024 bits of entropy). +1. Is generated from `/dev/urandom` by `kexec-seal-key` (128 bytes — 1024 bits of entropy, i.e. a $2^{1024}$ brute-force space). 2. Is sealed to TPM NVRAM with PCR policy `0,1,2,3,4,5,6,7`. 3. Is added as a LUKS key slot alongside the user's Disk Recovery Key (DRK). 4. At boot, `kexec-insert-key` unseals it and injects it into a minimal diff --git a/doc/tpm.md b/doc/tpm.md index 012b0953f..90f7ec064 100644 --- a/doc/tpm.md +++ b/doc/tpm.md @@ -169,9 +169,10 @@ PCRs (e.g. enabling an optional coreboot feature) would break the seal. #### LUKS Disk Unlock Key (DUK) — kexec-seal-key -The DUK is a 128-character random key (128 bytes from `/dev/urandom`, providing -1024 bits of entropy). It is added to a dedicated LUKS key slot and sealed to -TPM NVRAM with the policy below. +The DUK is a 128-byte random key generated from `/dev/urandom` (1024 bits of +entropy). This brute-force space ($2^{1024}$) is vastly larger than practical +password-style secrets and far beyond normal attacker capabilities. It is added +to a dedicated LUKS key slot and sealed to TPM NVRAM with the policy below. | PCR | How obtained | Reason | | --- | --- | --- | diff --git a/initrd/bin/kexec-seal-key.sh b/initrd/bin/kexec-seal-key.sh index 79cc5b14b..bb267488a 100755 --- a/initrd/bin/kexec-seal-key.sh +++ b/initrd/bin/kexec-seal-key.sh @@ -165,15 +165,19 @@ if [ $attempts -ge 3 ]; then DIE "Failed to set a valid Disk Unlock Key (DUK) passphrase after 3 attempts. Exiting..." fi -# Generate key file -STATUS "Generating new randomized key of 128 characters for LUKS TPM Disk Unlock Key" +# Generate key file: 128 bytes from /dev/urandom = 1024 bits of entropy. +# This provides a brute-force space of 2^1024 possible values. An attacker +# would need to try ~2^1023 guesses on average. Even at an absurd 10^12 guesses/second, +# this would require ~2^1023/10^12 seconds — vastly longer than the age of the universe. +# See doc/tpm.md and doc/security-model.md for full entropy analysis. +STATUS "Generating new 128-byte random key for LUKS TPM Disk Unlock Key" dd \ if=/dev/urandom \ of="$DUK_KEY_FILE" \ bs=1 \ count=128 \ 2>/dev/null || - DIE "Unable to generate random key of 128 characters" + DIE "Unable to generate random key of 128 bytes" STATUS_OK "LUKS TPM Disk Unlock Key generated" previous_luks_header_version=0