From 1f546ef23ad52b12c59941008c0c95173354b8c7 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Mon, 27 Apr 2026 15:30:20 -0400 Subject: [PATCH 1/9] initrd: wait for known USB dongle VID 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 3f5a132aacddb72347d3a8f587816c3ad8ea8f8b Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 29 Apr 2026 18:18:26 -0400 Subject: [PATCH 2/9] initrd: standardize STATUS/NOTE/WARN/INFO semantics Signed-off-by: Thierry Laurion --- doc/logging.md | 154 ++++++++++------ initrd/bin/cbfs-init.sh | 9 +- initrd/bin/gpg-gui.sh | 4 +- initrd/bin/gui-init.sh | 38 ++-- 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 | 57 ++++-- initrd/bin/qubes-measure-luks.sh | 2 +- initrd/bin/seal-hotpkey.sh | 16 +- 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 | 273 +++++++++++++++++++++------- initrd/etc/gui_functions.sh | 4 +- initrd/init | 16 +- initrd/sbin/insmod.sh | 4 +- 21 files changed, 458 insertions(+), 189 deletions(-) diff --git a/doc/logging.md b/doc/logging.md index 6a7be6cd3..bcf5dd0ca 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,13 @@ 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. + +For best-effort operations (for example bounded waits that may continue by design), +it's acceptable to follow the initial STATUS with a second STATUS describing the +continuation path (for example "continuing without dongle detection") when neither +STATUS_OK nor WARN accurately represents the result. Use STATUS when an action is beginning or underway: @@ -182,17 +201,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 +222,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 +235,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 +259,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 +306,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 +343,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 +370,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 +388,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..72c0419f0 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -224,29 +224,36 @@ gate_reseal_with_integrity_report() { fi INTEGRITY_REPORT_HASH_STATE="UNKNOWN" + STATUS "Running integrity report before resealing secrets" report_integrity_measurements local report_rc=$? DEBUG "gate_reseal_with_integrity_report: report_integrity_measurements rc=$report_rc" DEBUG "gate_reseal_with_integrity_report: INTEGRITY_REPORT_HASH_STATE=$INTEGRITY_REPORT_HASH_STATE" if [ "$INTEGRITY_REPORT_HASH_STATE" != "OK" ]; then DEBUG "returned from integrity report, now running investigation" + STATUS "Investigating integrity discrepancies before resealing" if ! investigate_integrity_discrepancies; then DEBUG "investigation indicated problem, aborting gate" + WARN "Integrity investigation did not clear discrepancies; reseal action aborted" return 1 fi DEBUG "gate_reseal_with_integrity_report: about to verify detached signature" + STATUS "Verifying /boot detached signature before resealing" DEBUG "ls -l /boot/kexec.sig: $(ls -l /boot/kexec.sig 2>/dev/null || echo missing)" if ! detached_kexec_signature_valid /boot; then DEBUG "detached_kexec_signature_valid failed" + WARN "Detached signature verification failed; refusing reseal action" local sig_fail_msg sig_fail_msg="Cannot proceed with sealing new secrets because /boot/kexec.sig could not be verified with your current keyring.\n\nTreat /boot as untrusted and recover ownership first." whiptail_error --title 'ERROR: Signature Verification Failed' \ --msgbox "$sig_fail_msg" 0 80 return 1 fi + STATUS_OK "Integrity checks passed for reseal prerequisites" else DEBUG "gate_reseal_with_integrity_report: integrity is OK, skipping investigation and detached signature verification" + STATUS_OK "Integrity checks passed for reseal prerequisites" fi if [ -x /bin/hotp_verification ]; then @@ -257,11 +264,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 +300,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 +376,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 +457,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 +495,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 +677,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,16 +905,22 @@ 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. - wait_for_gpg_card || true + # 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. + STATUS "Preparing USB and GPG signing key access for /boot signing" + enable_usb + if wait_for_gpg_card; then + STATUS_OK "USB initialized and GPG card is accessible for /boot signing" + else + WARN "GPG card was not accessible during /boot signing preparation; retrying through key checks" + fi while true; do GPG_KEY_COUNT=$(gpg -K 2>/dev/null | wc -l) if [ "$GPG_KEY_COUNT" -eq 0 ]; then 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 +940,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..ece2f8f59 100755 --- a/initrd/bin/oem-factory-reset.sh +++ b/initrd/bin/oem-factory-reset.sh @@ -24,8 +24,10 @@ rm -f /tmp/hotpkey_fw_shown TRACE_FUNC # Detect branding early — $DONGLE_BRAND is used throughout this script. -# enable_usb is called internally by detect_usb_security_dongle_branding. +STATUS "Initializing USB security dongle detection" +enable_usb detect_usb_security_dongle_branding +STATUS_OK "USB security dongle detection initialized" # use TERM to exit on error trap "exit 1" TERM @@ -218,6 +220,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 +239,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 +258,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 +284,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 +313,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 +334,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 +351,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 +371,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" } @@ -1004,9 +1014,10 @@ set_default_boot_option() { usb_security_token_capabilities_check() { TRACE_FUNC + local capabilities_check_degraded="n" # Always detect dongle branding from USB VID:PID — never read a stored file. - # enable_usb is called internally by detect_usb_security_dongle_branding. + enable_usb detect_usb_security_dongle_branding DEBUG "USB Security dongle detected: $DONGLE_BRAND" # Only show generic "Detected" if no specific brand was identified @@ -1034,7 +1045,13 @@ usb_security_token_capabilities_check() { # Show firmware version for USB Security dongle # Also capture firmware version for timing guidance in key generation message # Wait for gpg card to be ready before hotp_verification - wait_for_gpg_card + STATUS "Waiting for GPG card access on $DONGLE_BRAND" + if wait_for_gpg_card; then + STATUS_OK "GPG card is accessible on $DONGLE_BRAND" + else + WARN "GPG card was not accessible while checking $DONGLE_BRAND capabilities" + capabilities_check_degraded="y" + fi DONGLE_FW_VERSION="" if [ -x /bin/hotp_verification ]; then if hotp_token_info="$(hotp_verification info 2>/dev/null)"; then @@ -1049,6 +1066,11 @@ usb_security_token_capabilities_check() { DEBUG "$DONGLE_BRAND firmware version: $DONGLE_FW_VERSION" fi fi + if [ "$capabilities_check_degraded" = "y" ]; then + STATUS "Completed $DONGLE_BRAND capability checks with limited token access" + else + STATUS_OK "$DONGLE_BRAND capabilities checked" + fi } # usb_security_token_capabilities_check now handles all USB Security dongle logic @@ -1107,9 +1129,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 +1162,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 +1170,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 +1388,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 +1411,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 +1531,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 +1576,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 +1611,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 +1660,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..283006fcb 100755 --- a/initrd/bin/seal-hotpkey.sh +++ b/initrd/bin/seal-hotpkey.sh @@ -23,11 +23,15 @@ TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then DEBUG "Sealing HOTP secret reuses TOTP sealed secret..." + STATUS "Unsealing HOTP secret from TPM" tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET" || DIE "Unable to unseal HOTP secret" + STATUS_OK "HOTP secret unsealed from TPM" else # without a TPM, generate a secret based on the SHA-256 of the ROM + STATUS "Deriving HOTP secret from ROM hash" secret_from_rom_hash >"$HOTP_SECRET" || DIE "Reading ROM failed" + STATUS_OK "HOTP secret derived from ROM hash" fi # Store counter in file instead of TPM for now, as it conflicts with Heads @@ -48,9 +52,13 @@ mount_boot || exit 1 counter_value=1 -# Detect branding (enable_usb is called internally) +# HOTP configuration requires USB token access. +STATUS "Preparing USB security dongle access for HOTP configuration" +enable_usb +# Detect branding from current USB state detect_usb_security_dongle_branding DEBUG "$DONGLE_BRAND detected via USB VID:PID" +STATUS_OK "$DONGLE_BRAND detection ready for HOTP configuration" TRACE_FUNC @@ -59,6 +67,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 +STATUS "Checking $DONGLE_BRAND presence for HOTP setup" 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 @@ -67,6 +76,7 @@ if ! hotp_token_info="$(hotp_verification info)"; then DIE "Unable to find $DONGLE_BRAND" fi fi +STATUS_OK "$DONGLE_BRAND is present for HOTP setup" # Re-detect branding now that the dongle is confirmed present. detect_usb_security_dongle_branding @@ -139,8 +149,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..f728ae028 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -370,13 +370,39 @@ LOG() { # Sets and exports HEADS_TTY and GPG_TTY. # Must be called at script top-level (not inside a subshell) to take effect. detect_heads_tty() { + local _active _dev _candidate if ! HEADS_TTY=$(tty 2>/dev/null); then - local _active _dev + HEADS_TTY="" + DEBUG "detect_heads_tty: tty(1) unavailable, will resolve from active consoles" + else + DEBUG "detect_heads_tty: tty(1) resolved HEADS_TTY=$HEADS_TTY" + fi + + # On dual-console boards (notably qemu with CONFIG_BOOT_RECOVERY_SERIAL), + # gui-init may inherit the recovery serial tty even though its real UI lives + # on the main console. Prefer a non-recovery active console when available. + if [ -n "$RECOVERY_TTY" ] && [ "$HEADS_TTY" = "$RECOVERY_TTY" ]; then + _active=$(cat /sys/class/tty/console/active 2>/dev/null) + DEBUG "detect_heads_tty: HEADS_TTY matches RECOVERY_TTY ($RECOVERY_TTY), active consoles='${_active:-}'" + for _dev in $_active; do + _candidate="/dev/$_dev" + if [ "$_candidate" != "$RECOVERY_TTY" ]; then + [ "$_dev" = "tty0" ] && _candidate="/dev/$(cat /sys/class/tty/tty0/active 2>/dev/null || echo tty0)" + HEADS_TTY="$_candidate" + DEBUG "detect_heads_tty: switched interactive tty away from recovery console to $HEADS_TTY" + break + fi + done + fi + + if [ -z "$HEADS_TTY" ]; then _active=$(cat /sys/class/tty/console/active 2>/dev/null) _dev="${_active##* }" [ "$_dev" = "tty0" ] && _dev=$(cat /sys/class/tty/tty0/active 2>/dev/null || echo tty0) HEADS_TTY="/dev/${_dev:-console}" + DEBUG "detect_heads_tty: falling back to HEADS_TTY=$HEADS_TTY from active consoles='${_active:-}'" fi + DEBUG "detect_heads_tty: exporting HEADS_TTY=$HEADS_TTY GPG_TTY=$HEADS_TTY" export HEADS_TTY export GPG_TTY="$HEADS_TTY" } @@ -486,18 +512,138 @@ pin_color() { } # Detect USB security dongle branding from USB VID:PID via lsusb. +# Runtime dongle IDs are sourced from /etc/dongle-versions. # Sources: hotp-verification/src/device.c and targets/qemu.mk -# USB Security dongle (OpenPGP smart card) VID:PID table: -# 20a0:42b2 Nitrokey 3 (3A Mini / 3A NFC / 3C NFC - all share this PID) -# 20a0:42d4 Canokey QEMU -# 20a0:4108 Nitrokey Pro / Pro 2 (Pro and Pro 2 share the same PID) -# 20a0:4109 Nitrokey Storage / Storage 2 -# 316d:4c4b Librem Key -# 16d0:21dc Canokey -# 1050:0113 Yubikey 4/5 (OTP+U2F+CCID) - legacy -# 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) + +load_usb_security_dongle_ids() { + # /etc/dongle-versions is the single source of truth for runtime IDs. + [ -r /etc/dongle-versions ] || return 1 + . /etc/dongle-versions || return 1 + [ -n "$USB_SECURITY_DONGLE_VIDS" ] || return 1 + return 0 +} + +# 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() { + load_usb_security_dongle_ids || return 1 + local vid + for vid in $USB_SECURITY_DONGLE_VIDS; do + if grep -l -E "^${vid}$" /sys/bus/usb/devices/*/idVendor 2>/dev/null | grep -q .; then + return 0 + fi + done + return 1 +} + +# Wait up to 15 seconds for a known USB security dongle VID to appear in sysfs. +# 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 allow_user_cancel="y" + local deadline remaining ch + + if heads_tty_is_serial "$interactive_tty"; then + is_serial=1 + fi + DEBUG "wait_for_usb_security_dongle_vid: interactive_tty='${interactive_tty:-}' is_serial=$is_serial RECOVERY_TTY='${RECOVERY_TTY:-}'" + + # Never consume keystrokes from the active recovery shell tty. + if [ -n "$RECOVERY_TTY" ] && [ "$interactive_tty" = "$RECOVERY_TTY" ]; then + allow_user_cancel="n" + DEBUG "Disabling USB dongle wait key-cancel on recovery tty ($RECOVERY_TTY)" + fi + + # In non-interactive/background contexts, poll only and avoid read() input capture. + if [ -z "$interactive_tty" ] && [ ! -t 0 ]; then + allow_user_cancel="n" + DEBUG "wait_for_usb_security_dongle_vid: no interactive tty and stdin is not a tty, disabling user-cancel reads" + fi + DEBUG "wait_for_usb_security_dongle_vid: allow_user_cancel=$allow_user_cancel" + + # Drain stray buffered input on framebuffer so stale keystrokes do not + # immediately cancel this wait. + if [ "$allow_user_cancel" = "y" ] && [ "$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 + + if [ "$allow_user_cancel" != "y" ]; then + STATUS "Waiting up to 15s for USB security dongle detection" + elif [ "$is_serial" = "1" ]; then + STATUS "Waiting up to 15s for USB security dongle detection (press Enter to skip)" + else + STATUS "Waiting up to 15s for USB security dongle detection (press any key to skip)" + fi + + deadline=$(( $(date +%s) + 15 )) + + while :; do + # Exit immediately when a known VID appears. + if usb_security_dongle_vid_present; then + DEBUG "USB security dongle VID detected in sysfs" + STATUS_OK "USB security dongle detected" + return 0 + fi + + remaining=$(( deadline - $(date +%s) )) + if [ "$remaining" -le 0 ]; then + DEBUG "Timeout waiting for USB security dongle VID after 15s" + STATUS "No known USB security dongle detected within 15s; continuing" + return 1 + fi + + if [ "$allow_user_cancel" != "y" ]; then + sleep 1 + elif [ "$is_serial" = "1" ]; then + if [ -n "$interactive_tty" ]; then + if IFS= read -r -t 1 ch <"$interactive_tty" 2>/dev/null; then + DEBUG "User cancelled USB dongle wait (Enter on serial)" + STATUS "USB security dongle wait skipped by user; continuing" + return 1 + fi + else + if IFS= read -r -t 1 ch; then + DEBUG "User cancelled USB dongle wait (Enter on serial)" + STATUS "USB security dongle wait skipped by user; continuing" + 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 + DEBUG "User cancelled USB dongle wait (key on framebuffer)" + STATUS "USB security dongle wait skipped by user; continuing" + return 1 + fi + else + if IFS= read -r -t 0.2 -n 1 ch; then + DEBUG "User cancelled USB dongle wait (key on framebuffer)" + STATUS "USB security dongle wait skipped by user; continuing" + return 1 + fi + fi + fi + done +} + +# Detect USB security dongle branding (Nitrokey, Yubikey, Canokey, etc.) from VID:PID. +# This helper enables USB and waits for enumeration before scanning with lsusb. +# Branding detection requires USB modules/device nodes to be available. detect_usb_security_dongle_branding() { TRACE_FUNC local usb_was_enabled="${_USB_ENABLED:-n}" @@ -515,55 +661,40 @@ 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 - + # Wait up to 15s for a known dongle VID to appear; user can press any key (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. - if [ "$DONGLE_BRAND" != "USB Security dongle" ] && [ -n "$DONGLE_BRAND" ]; then - DEBUG "Branding already specific ($DONGLE_BRAND), skipping lsusb scan" + [ "$DONGLE_BRAND" != "USB Security dongle" ] && [ -n "$DONGLE_BRAND" ] && return + if ! load_usb_security_dongle_ids; then + DEBUG "Failed to load USB security dongle IDs from /etc/dongle-versions" + export DONGLE_BRAND="USB Security dongle" return fi local lsusb_out lsusb_out="$(lsusb)" DEBUG "lsusb output: $lsusb_out" # Check NK3 (42b2) before the broader 20a0 vendor match - if echo "$lsusb_out" | grep -q "20a0:42b2"; then - DEBUG "Detected Nitrokey 3 (20a0:42b2)" + if echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_NK3_VIDPID"; then + DEBUG "Detected Nitrokey 3 ($USB_SECURITY_DONGLE_NK3_VIDPID)" export DONGLE_BRAND="Nitrokey 3" - elif echo "$lsusb_out" | grep -q "20a0:42d4"; then - DEBUG "Detected Canokey QEMU (20a0:42d4)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_CANOKEY_QEMU_VIDPID"; then + DEBUG "Detected Canokey QEMU ($USB_SECURITY_DONGLE_CANOKEY_QEMU_VIDPID)" export DONGLE_BRAND="Canokey" - elif echo "$lsusb_out" | grep -q "20a0:4108"; then - DEBUG "Detected Nitrokey Pro (20a0:4108)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_NITROKEY_PRO_VIDPID"; then + DEBUG "Detected Nitrokey Pro ($USB_SECURITY_DONGLE_NITROKEY_PRO_VIDPID)" export DONGLE_BRAND="Nitrokey Pro" - elif echo "$lsusb_out" | grep -q "20a0:4109"; then - DEBUG "Detected Nitrokey Storage (20a0:4109)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_NITROKEY_STORAGE_VIDPID"; then + DEBUG "Detected Nitrokey Storage ($USB_SECURITY_DONGLE_NITROKEY_STORAGE_VIDPID)" export DONGLE_BRAND="Nitrokey Storage" - elif echo "$lsusb_out" | grep -q "316d:4c4b"; then - DEBUG "Detected Librem Key (316d:4c4b)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_LIBREM_KEY_VIDPID"; then + DEBUG "Detected Librem Key ($USB_SECURITY_DONGLE_LIBREM_KEY_VIDPID)" export DONGLE_BRAND="Librem Key" - elif echo "$lsusb_out" | grep -q "16d0:21dc"; then - DEBUG "Detected Canokey (16d0:21dc)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_CANOKEY_VIDPID"; then + DEBUG "Detected Canokey ($USB_SECURITY_DONGLE_CANOKEY_VIDPID)" export DONGLE_BRAND="Canokey" - elif echo "$lsusb_out" | grep -q "1050:"; then - DEBUG "Detected Yubikey (1050:*)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_YUBIKEY_VID_PREFIX"; then + DEBUG "Detected Yubikey (${USB_SECURITY_DONGLE_YUBIKEY_VID_PREFIX}*)" export DONGLE_BRAND="Yubikey" else DEBUG "No known USB Security dongle detected" @@ -781,8 +912,9 @@ cache_gpg_signing_pin() { -name '*.key' -delete >/dev/null 2>&1 || true DEBUG "Cleared private-keys-v1.d; agent will re-discover keys via scdaemon" - # USB will be enabled by wait_for_gpg_card() and detect_usb_security_dongle_branding() + # USB will be enabled by wait_for_gpg_card() and detect_usb_security_dongle_branding(). # Wait for USB enumeration before accessing GPG card to avoid race condition + STATUS "Waiting for USB device enumeration before checking GPG card" wait_for_usb_devices STATUS "Verifying presence of USB Security dongle" @@ -804,6 +936,7 @@ cache_gpg_signing_pin() { DIE "gpg card read failed" DEBUG "Retry succeeded" fi + STATUS_OK "GPG card is accessible" # Read card status and display PIN retry counters before prompting. # output excerpt: "PIN retry counter : 3 0 3" @@ -956,26 +1089,36 @@ 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 #Going to recovery shell should be authenticated if supported gpg_auth - #if we have DEBUG_OUTPUT=y, we instruct users to use the debug log - if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - cat /etc/DEBUG_LOG_COPY_INSTRUCTIONS - fi - - #Guide user into enabling debug output in case of a discovered bug - if [ "$CONFIG_DEBUG_OUTPUT" != "y" ]; then - NOTE "To file a bug report with debug logs:\n 1. Options --> Change configuration settings --> Configure $CONFIG_BRAND_NAME informational / debug output --> select Debug, save and flash firmware changes\n 2. After reboot: Options --> TPM/TOTP/HOTP Options --> Generate new TOTP/HOTP secret to reseal secrets" - fi + # Debug and measurement logs are always captured; show copy guidance directly. + cat /etc/DEBUG_LOG_COPY_INSTRUCTIONS # display any custom recovery message just before the banner if [ -n "$*" ]; then WARN "$*" fi + + # Show PCR state when entering recovery shell only when TPM is enabled. + if [ "$CONFIG_TPM" = "y" ]; then + INFO "TPM: PCR state on entering recovery shell:" + pcrs | while IFS= read -r line; do + INFO "$line" + done + fi + + # 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 @@ -996,7 +1139,7 @@ recovery() { pause_recovery() { TRACE_FUNC INPUT "Press Enter to proceed to recovery shell" - recovery $* + recovery "$@" } combine_configs() { @@ -1110,12 +1253,14 @@ wait_for_usb_devices() { if [ $peripheral_count -gt 0 ]; then DEBUG "USB peripheral devices ready after ${elapsed}s (iteration $iteration): found $peripheral_count device(s)" + STATUS_OK "USB peripheral devices detected" return fi # Timeout after 2 seconds if awk -v s="$start" -v n="$now" 'BEGIN{exit (n - s > 2.0) ? 0 : 1}'; then DEBUG "USB wait timeout at ${elapsed}s (iter $iteration): only found $peripheral_count peripheral device(s)" + WARN "USB peripheral devices were not detected within 2s, continuing" return fi done @@ -1352,7 +1497,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 @@ -2808,9 +2953,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 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 0263e8b5701e4075e39cb31b30b8c742534863d6 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 6 May 2026 17:45:12 -0400 Subject: [PATCH 3/9] initrd/etc/functions.sh: add cancellable 15s dongle VID wait Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 260 +++++++++++++++++++++------------------- 1 file changed, 139 insertions(+), 121 deletions(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index f728ae028..61d396b7d 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -370,39 +370,13 @@ LOG() { # Sets and exports HEADS_TTY and GPG_TTY. # Must be called at script top-level (not inside a subshell) to take effect. detect_heads_tty() { - local _active _dev _candidate if ! HEADS_TTY=$(tty 2>/dev/null); then - HEADS_TTY="" - DEBUG "detect_heads_tty: tty(1) unavailable, will resolve from active consoles" - else - DEBUG "detect_heads_tty: tty(1) resolved HEADS_TTY=$HEADS_TTY" - fi - - # On dual-console boards (notably qemu with CONFIG_BOOT_RECOVERY_SERIAL), - # gui-init may inherit the recovery serial tty even though its real UI lives - # on the main console. Prefer a non-recovery active console when available. - if [ -n "$RECOVERY_TTY" ] && [ "$HEADS_TTY" = "$RECOVERY_TTY" ]; then - _active=$(cat /sys/class/tty/console/active 2>/dev/null) - DEBUG "detect_heads_tty: HEADS_TTY matches RECOVERY_TTY ($RECOVERY_TTY), active consoles='${_active:-}'" - for _dev in $_active; do - _candidate="/dev/$_dev" - if [ "$_candidate" != "$RECOVERY_TTY" ]; then - [ "$_dev" = "tty0" ] && _candidate="/dev/$(cat /sys/class/tty/tty0/active 2>/dev/null || echo tty0)" - HEADS_TTY="$_candidate" - DEBUG "detect_heads_tty: switched interactive tty away from recovery console to $HEADS_TTY" - break - fi - done - fi - - if [ -z "$HEADS_TTY" ]; then + local _active _dev _active=$(cat /sys/class/tty/console/active 2>/dev/null) _dev="${_active##* }" [ "$_dev" = "tty0" ] && _dev=$(cat /sys/class/tty/tty0/active 2>/dev/null || echo tty0) HEADS_TTY="/dev/${_dev:-console}" - DEBUG "detect_heads_tty: falling back to HEADS_TTY=$HEADS_TTY from active consoles='${_active:-}'" fi - DEBUG "detect_heads_tty: exporting HEADS_TTY=$HEADS_TTY GPG_TTY=$HEADS_TTY" export HEADS_TTY export GPG_TTY="$HEADS_TTY" } @@ -512,16 +486,18 @@ pin_color() { } # Detect USB security dongle branding from USB VID:PID via lsusb. -# Runtime dongle IDs are sourced from /etc/dongle-versions. # Sources: hotp-verification/src/device.c and targets/qemu.mk - -load_usb_security_dongle_ids() { - # /etc/dongle-versions is the single source of truth for runtime IDs. - [ -r /etc/dongle-versions ] || return 1 - . /etc/dongle-versions || return 1 - [ -n "$USB_SECURITY_DONGLE_VIDS" ] || return 1 - return 0 -} +# USB Security dongle (OpenPGP smart card) VID:PID table: +# 20a0:42b2 Nitrokey 3 (3A Mini / 3A NFC / 3C NFC - all share this PID) +# 20a0:42d4 Canokey QEMU +# 20a0:4108 Nitrokey Pro / Pro 2 (Pro and Pro 2 share the same PID) +# 20a0:4109 Nitrokey Storage / Storage 2 +# 316d:4c4b Librem Key +# 16d0:21dc Canokey +# 1050:0113 Yubikey 4/5 (OTP+U2F+CCID) - legacy +# 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) # Returns 0 if the given tty path is a serial console. heads_tty_is_serial() { @@ -534,47 +510,30 @@ heads_tty_is_serial() { # 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() { - load_usb_security_dongle_ids || return 1 - local vid - for vid in $USB_SECURITY_DONGLE_VIDS; do - if grep -l -E "^${vid}$" /sys/bus/usb/devices/*/idVendor 2>/dev/null | grep -q .; then - return 0 - fi - done - return 1 + 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 allow_user_cancel="y" - local deadline remaining ch + local deadline remaining ch status_line last_remaining="" if heads_tty_is_serial "$interactive_tty"; then is_serial=1 fi - DEBUG "wait_for_usb_security_dongle_vid: interactive_tty='${interactive_tty:-}' is_serial=$is_serial RECOVERY_TTY='${RECOVERY_TTY:-}'" - # Never consume keystrokes from the active recovery shell tty. - if [ -n "$RECOVERY_TTY" ] && [ "$interactive_tty" = "$RECOVERY_TTY" ]; then - allow_user_cancel="n" - DEBUG "Disabling USB dongle wait key-cancel on recovery tty ($RECOVERY_TTY)" - fi - - # In non-interactive/background contexts, poll only and avoid read() input capture. - if [ -z "$interactive_tty" ] && [ ! -t 0 ]; then - allow_user_cancel="n" - DEBUG "wait_for_usb_security_dongle_vid: no interactive tty and stdin is not a tty, disabling user-cancel reads" + # 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 - DEBUG "wait_for_usb_security_dongle_vid: allow_user_cancel=$allow_user_cancel" - - # Drain stray buffered input on framebuffer so stale keystrokes do not - # immediately cancel this wait. - if [ "$allow_user_cancel" = "y" ] && [ "$is_serial" = "0" ]; then + 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 @@ -582,58 +541,73 @@ wait_for_usb_security_dongle_vid() { fi fi - if [ "$allow_user_cancel" != "y" ]; then - STATUS "Waiting up to 15s for USB security dongle detection" - elif [ "$is_serial" = "1" ]; then - STATUS "Waiting up to 15s for USB security dongle detection (press Enter to skip)" - else - STATUS "Waiting up to 15s for USB security dongle detection (press any key to skip)" - fi - deadline=$(( $(date +%s) + 15 )) while :; do # Exit immediately when a known VID appears. if usb_security_dongle_vid_present; then - DEBUG "USB security dongle VID detected in sysfs" - STATUS_OK "USB security dongle detected" + 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" - STATUS "No known USB security dongle detected within 15s; continuing" return 1 fi - if [ "$allow_user_cancel" != "y" ]; then - sleep 1 - elif [ "$is_serial" = "1" ]; then + # 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)" - STATUS "USB security dongle wait skipped by user; continuing" return 1 fi else if IFS= read -r -t 1 ch; then + printf "\n\n" DEBUG "User cancelled USB dongle wait (Enter on serial)" - STATUS "USB security dongle wait skipped by user; continuing" 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)" - STATUS "USB security dongle wait skipped by user; continuing" 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)" - STATUS "USB security dongle wait skipped by user; continuing" return 1 fi fi @@ -642,8 +616,15 @@ wait_for_usb_security_dongle_vid() { } # Detect USB security dongle branding (Nitrokey, Yubikey, Canokey, etc.) from VID:PID. -# This helper enables USB and waits for enumeration before scanning with lsusb. -# Branding detection requires USB modules/device nodes to be available. +# 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}" @@ -656,45 +637,45 @@ 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 - # Wait up to 15s for a known dongle VID to appear; user can press any key (fb) or Enter (serial) to skip. + # 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 - if ! load_usb_security_dongle_ids; then - DEBUG "Failed to load USB security dongle IDs from /etc/dongle-versions" - export DONGLE_BRAND="USB Security dongle" - return - fi local lsusb_out lsusb_out="$(lsusb)" DEBUG "lsusb output: $lsusb_out" # Check NK3 (42b2) before the broader 20a0 vendor match - if echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_NK3_VIDPID"; then - DEBUG "Detected Nitrokey 3 ($USB_SECURITY_DONGLE_NK3_VIDPID)" + if echo "$lsusb_out" | grep -q "20a0:42b2"; then + DEBUG "Detected Nitrokey 3 (20a0:42b2)" export DONGLE_BRAND="Nitrokey 3" - elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_CANOKEY_QEMU_VIDPID"; then - DEBUG "Detected Canokey QEMU ($USB_SECURITY_DONGLE_CANOKEY_QEMU_VIDPID)" + elif echo "$lsusb_out" | grep -q "20a0:42d4"; then + DEBUG "Detected Canokey QEMU (20a0:42d4)" export DONGLE_BRAND="Canokey" - elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_NITROKEY_PRO_VIDPID"; then - DEBUG "Detected Nitrokey Pro ($USB_SECURITY_DONGLE_NITROKEY_PRO_VIDPID)" + elif echo "$lsusb_out" | grep -q "20a0:4108"; then + DEBUG "Detected Nitrokey Pro (20a0:4108)" export DONGLE_BRAND="Nitrokey Pro" - elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_NITROKEY_STORAGE_VIDPID"; then - DEBUG "Detected Nitrokey Storage ($USB_SECURITY_DONGLE_NITROKEY_STORAGE_VIDPID)" + elif echo "$lsusb_out" | grep -q "20a0:4109"; then + DEBUG "Detected Nitrokey Storage (20a0:4109)" export DONGLE_BRAND="Nitrokey Storage" - elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_LIBREM_KEY_VIDPID"; then - DEBUG "Detected Librem Key ($USB_SECURITY_DONGLE_LIBREM_KEY_VIDPID)" + elif echo "$lsusb_out" | grep -q "316d:4c4b"; then + DEBUG "Detected Librem Key (316d:4c4b)" export DONGLE_BRAND="Librem Key" - elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_CANOKEY_VIDPID"; then - DEBUG "Detected Canokey ($USB_SECURITY_DONGLE_CANOKEY_VIDPID)" + elif echo "$lsusb_out" | grep -q "16d0:21dc"; then + DEBUG "Detected Canokey (16d0:21dc)" export DONGLE_BRAND="Canokey" - elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_YUBIKEY_VID_PREFIX"; then - DEBUG "Detected Yubikey (${USB_SECURITY_DONGLE_YUBIKEY_VID_PREFIX}*)" + elif echo "$lsusb_out" | grep -q "1050:"; then + DEBUG "Detected Yubikey (1050:*)" export DONGLE_BRAND="Yubikey" else DEBUG "No known USB Security dongle detected" @@ -912,9 +893,8 @@ cache_gpg_signing_pin() { -name '*.key' -delete >/dev/null 2>&1 || true DEBUG "Cleared private-keys-v1.d; agent will re-discover keys via scdaemon" - # USB will be enabled by wait_for_gpg_card() and detect_usb_security_dongle_branding(). + # USB will be enabled by wait_for_gpg_card() and detect_usb_security_dongle_branding() # Wait for USB enumeration before accessing GPG card to avoid race condition - STATUS "Waiting for USB device enumeration before checking GPG card" wait_for_usb_devices STATUS "Verifying presence of USB Security dongle" @@ -936,7 +916,6 @@ cache_gpg_signing_pin() { DIE "gpg card read failed" DEBUG "Retry succeeded" fi - STATUS_OK "GPG card is accessible" # Read card status and display PIN retry counters before prompting. # output excerpt: "PIN retry counter : 3 0 3" @@ -1096,20 +1075,25 @@ recovery() { #Going to recovery shell should be authenticated if supported gpg_auth - # Debug and measurement logs are always captured; show copy guidance directly. - cat /etc/DEBUG_LOG_COPY_INSTRUCTIONS + #if we have DEBUG_OUTPUT=y, we instruct users to use the debug log + if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + cat /etc/DEBUG_LOG_COPY_INSTRUCTIONS + fi + + #Guide user into enabling debug output in case of a discovered bug + if [ "$CONFIG_DEBUG_OUTPUT" != "y" ]; then + NOTE "To file a bug report with debug logs:\n 1. Options --> Change configuration settings --> Configure $CONFIG_BRAND_NAME informational / debug output --> select Debug, save and flash firmware changes\n 2. After reboot: Options --> TPM/TOTP/HOTP Options --> Generate new TOTP/HOTP secret to reseal secrets" + fi # display any custom recovery message just before the banner if [ -n "$*" ]; then WARN "$*" fi - # Show PCR state when entering recovery shell only when TPM is enabled. - if [ "$CONFIG_TPM" = "y" ]; then - INFO "TPM: PCR state on entering recovery shell:" - pcrs | while IFS= read -r line; do - INFO "$line" - done - 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 # Drain any queued serial input before starting the interactive shell. # This avoids stale bytes being interpreted as bash commands on entry. @@ -1139,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() { @@ -1253,14 +1273,12 @@ wait_for_usb_devices() { if [ $peripheral_count -gt 0 ]; then DEBUG "USB peripheral devices ready after ${elapsed}s (iteration $iteration): found $peripheral_count device(s)" - STATUS_OK "USB peripheral devices detected" return fi # Timeout after 2 seconds if awk -v s="$start" -v n="$now" 'BEGIN{exit (n - s > 2.0) ? 0 : 1}'; then DEBUG "USB wait timeout at ${elapsed}s (iter $iteration): only found $peripheral_count peripheral device(s)" - WARN "USB peripheral devices were not detected within 2s, continuing" return fi done From 853ac918bc343c1774256378c6f391f35d44c98d Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Sat, 2 May 2026 18:54:37 -0400 Subject: [PATCH 4/9] initrd/init: use PID-tracked bootscript respawn loop Signed-off-by: Thierry Laurion --- initrd/init | 58 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/initrd/init b/initrd/init index 42db1b7b9..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 @@ -256,16 +269,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 4544577b299bf422ce2fd7e92da3d025fbb5cd2f Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 6 May 2026 18:20:16 -0400 Subject: [PATCH 5/9] Makefile: sanitize branch token in artifact filenames Signed-off-by: Thierry Laurion --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d2e4bd966..e4a9433f4 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 069ffa070e5a94d3cbe765bcd0509c8508f425ff Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 6 May 2026 19:40:21 -0400 Subject: [PATCH 6/9] initrd/docs+status: align recovery flow, dongle mapping, DUK wording, and long-op STATUS outcomes Align documentation and runtime messaging to match current behavior: - docs: synchronize recovery flow, dongle mapping, DUK/measurement wording - initrd runtime: make long dongle/GPG/HOTP waits report explicit STATUS outcomes (success, degraded continuation, or warning) instead of silent/bare waits - keep wording context-accurate between reseal and /boot signing paths This captures the combined scope now present in this commit after autosquash. Signed-off-by: Thierry Laurion --- doc/boot-process.md | 10 +- doc/development.md | 87 +++++++++ doc/prerequisites.md | 7 +- doc/recovery-shell.md | 3 + doc/security-model.md | 2 +- doc/tpm.md | 7 +- doc/ux-patterns.md | 12 ++ initrd/bin/kexec-seal-key.sh | 10 +- initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS | 19 +- initrd/etc/dongle-versions | 13 ++ initrd/etc/functions.sh | 260 ++++++++++++------------- initrd/etc/gui_functions.sh | 4 +- initrd/init | 8 +- 13 files changed, 279 insertions(+), 163 deletions(-) diff --git a/doc/boot-process.md b/doc/boot-process.md index 6491aaf86..8624145db 100644 --- a/doc/boot-process.md +++ b/doc/boot-process.md @@ -43,10 +43,14 @@ kexec ← hands off to OS kernel 3. Runs `cbfs-init` to extract user configuration from CBFS into `/etc/config.user`. 4. Calls `combine_configs()` to merge all `/etc/config*` files into `/tmp/config`, then sources `/tmp/config` so all subsequent scripts see the merged settings. -5. Checks for a quick `r` keypress (100 ms timeout) to drop to a recovery shell +5. If `CONFIG_BOOT_RECOVERY_SERIAL` is set, starts `pause_recovery` on that + serial TTY (`/dev/ttyS*`) and waits for Enter before launching the recovery + shell there. +6. Checks for a quick `r` keypress (100 ms timeout) to drop to a recovery shell before any GUI starts. -6. Execs `cttyhack $CONFIG_BOOTSCRIPT` (default: `/bin/gui-init`), which sets up - a controlling TTY and hands off to the boot script. +7. Starts `cttyhack $CONFIG_BOOTSCRIPT` (default: `/bin/gui-init`) under a PID 1 + respawn loop, so the boot script is relaunched if it exits unexpectedly while + init stays alive. ### Config file merge diff --git a/doc/development.md b/doc/development.md index 1ab804c6e..c2eaed336 100644 --- a/doc/development.md +++ b/doc/development.md @@ -11,6 +11,93 @@ git commit -S -s -m "component: short description" - **`-S`** — GPG-sign the commit (required; see [CONTRIBUTING.md](../CONTRIBUTING.md)) - **`-s`** — add `Signed-off-by:` trailer for [DCO](https://developercertificate.org/) compliance (required; CI enforces this) +### Enforce Signing In This Repository + +Git allows repository-local config to override your global config. Verify and +enforce signing in this clone during onboarding: + +```bash +# Require cryptographic commit signatures in this repository +git config commit.gpgsign true + +# Confirm effective values and where they come from +git config --show-origin --get-all commit.gpgsign +git config --show-origin --get-all user.signingkey +``` + +### Select The Correct Signing Key Fingerprint + +Use the full 40-hex fingerprint (not a short key ID) and ensure the UID email +matches your Git commit email. + +```bash +# List secret keys with full fingerprints +gpg --list-secret-keys --keyid-format=long + +# Show your configured commit email +git config user.email +``` + +Choose the fingerprint for the key whose UID matches `git config user.email`, +then configure Git to use that exact fingerprint: + +```bash +# Use the full fingerprint shown by gpg +git config user.signingkey + +# Optional: set globally instead of repo-local +git config --global user.signingkey +``` + +Verify the effective configuration and signature status: + +```bash +git config --show-origin --get-all user.signingkey +git commit --allow-empty -S -s -m "test: verify signing setup" +git log -1 --show-signature +``` + +### Use The Factory Reset / Re-Ownership Public Key In Dev Cycles + +`OEM Factory Reset / Re-Ownership` already exports a public key to USB for you: + +- In-memory key backup path: public partition contains `pubkey.asc` +- Separate USB export path: file is named `.asc` + +Import that exported public key into your developer workstation keyring, then +point Git signing at the same fingerprint used by the corresponding private key +material (dongle or restored backup keyring): + +```bash +# Import the exported ownership key +gpg --import /path/to/pubkey.asc + +# Confirm full fingerprint and UID +gpg --list-keys --fingerprint --keyid-format=long + +# Use the full 40-hex fingerprint shown above +git config user.signingkey +git config commit.gpgsign true +``` + +For repeated development/contribution cycles on new systems, reuse the exported +public key file from ownership provisioning as your canonical reference, and +verify with: + +```bash +git log -1 --show-signature +``` + +Expected output for `commit.gpgsign` should include `true` for `.git/config` +or for your global config, and must not include `false`. + +If you already created unsigned commits, rewrite them before opening a PR: + +```bash +# Re-sign all commits ahead of upstream/master (keeps Signed-off-by trailers) +git rebase -S --rebase-merges origin/master +``` + ### Message Format ```text diff --git a/doc/prerequisites.md b/doc/prerequisites.md index 1b9cd1893..d37cf7f62 100644 --- a/doc/prerequisites.md +++ b/doc/prerequisites.md @@ -17,14 +17,19 @@ version. Without HOTP, Heads falls back to TPMTOTP (smartphone-based). | YubiKey 5 Series | Yes | No | OpenPGP signing only; no HOTP | | Nitrokey Pro (v1, fw < 0.8) | Yes | Limited | Older firmware may report no HOTP support; test before use | -Heads detects dongle branding at runtime via USB VID:PID: +Heads detects dongle branding at runtime via USB VID:PID. + +Source of truth for IDs is `initrd/etc/dongle-versions`. | VID:PID | Dongle | |---------|--------| | `20a0:42b2` | Nitrokey 3 | +| `20a0:42d4` | Canokey (QEMU) | | `20a0:4108` | Nitrokey Pro | | `20a0:4109` | Nitrokey Storage | | `316d:4c4b` | Purism Librem Key | +| `16d0:21dc` | Canokey | +| `1050:*` | YubiKey | ## HOTP vs. TPMTOTP diff --git a/doc/recovery-shell.md b/doc/recovery-shell.md index c0ae078da..cab3d690b 100644 --- a/doc/recovery-shell.md +++ b/doc/recovery-shell.md @@ -7,6 +7,9 @@ gives direct access to block devices, GPG, TPM tools, and flash utilities. - At power-on: press `r` repeatedly during the Heads splash screen. - From the Heads GUI: `Options -> Recovery Shell`. +- If `CONFIG_BOOT_RECOVERY_SERIAL` is enabled by the board config, `/init` + starts a serial `pause_recovery` path that waits for Enter, then launches + recovery on that serial TTY. ## Limitations 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/doc/ux-patterns.md b/doc/ux-patterns.md index 731a0dfb8..08998243a 100644 --- a/doc/ux-patterns.md +++ b/doc/ux-patterns.md @@ -118,6 +118,11 @@ output and `read` use that device — bypassing any stdout/stderr redirections t When `HEADS_TTY` is unset, the prompt goes to stderr and `read` uses stdin (serial recovery shell convention). +On dual-console setups (for example qemu boards that also enable +`CONFIG_BOOT_RECOVERY_SERIAL`), `detect_heads_tty` prefers a non-recovery +active console for gui-init interaction when possible. This avoids routing +gui-init inline prompts to the recovery serial shell TTY. + **Do not use INPUT for yes/no choices** — use `whiptail_warning --yesno` or `whiptail_error --yesno` for those so the user has a clear graphical dialog. @@ -304,6 +309,13 @@ self-upgradeable regardless of firmware version (always shown as NOTE directing users to contact Purism support). Nitrokey Storage has a separate firmware codebase and is not subject to this threshold. +#### Centralized dongle VID/PID mapping + +USB dongle VID/PID detection IDs are centralized in `initrd/etc/dongle-versions`. +`detect_usb_security_dongle_branding` and `usb_security_dongle_vid_present` in +`initrd/etc/functions.sh` read those values so additions/changes are made in one +place instead of hardcoded in multiple functions. + #### Parsing hotp_verification output `hotp_verification info` tab-indents all output lines (`\tFirmware: v0.15`). 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 diff --git a/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS b/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS index 346ccb74d..a566fec84 100644 --- a/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS +++ b/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS @@ -1,10 +1,13 @@ Welcome to the Recovery Shell! -- /tmp/debug.log: contains corresponding log level (Quiet/Info/Debug) debug traces - - Read them locally through: 'less /tmp/debug/log' -- If you faced a bug: - - Preformat/connect a ext3/ext4/fat32/exfat USB thumb drive, and then: - - 'mount-usb.sh --mode rw' # Mounts a connected USB drive in Read+Write mode - - 'cp /tmp/debug.log /media' # copy the log to mounted Read+Write partition under /media - - 'umount /media' # Makes sure buffered write operations are done and "ejects" the USB drive -- Share the debug.log with the developers. +- /tmp/debug.log: detailed runtime logs in all output modes. +- /tmp/measuring_trace.log: INFO-level security/measurement log in all output modes. + - Read locally with: + - 'less /tmp/debug.log' + - 'less /tmp/measuring_trace.log' +- If you hit a bug: + - Connect an ext3/ext4/fat32/exfat USB thumb drive, then run: + - 'mount-usb.sh --mode rw' # mount USB read-write at /media + - 'cp /tmp/debug.log /tmp/measuring_trace.log /media/' # copy both logs + - 'umount /media' # flush writes and safely unmount +- Share both log files with developers. diff --git a/initrd/etc/dongle-versions b/initrd/etc/dongle-versions index 4947a0123..cb4536c5f 100644 --- a/initrd/etc/dongle-versions +++ b/initrd/etc/dongle-versions @@ -6,6 +6,19 @@ # Version strings must be prefixed with 'v' and use dot notation (e.g. v1.8.3) # so that sort -V ordering works correctly for the comparison in hotpkey_fw_display. +# USB VID/PID source-of-truth for runtime branding detection. +# Used by initrd/etc/functions.sh: +# - usb_security_dongle_vid_present (VID pre-check during bounded wait) +# - detect_usb_security_dongle_branding (brand mapping from lsusb output) +USB_SECURITY_DONGLE_VIDS="20a0 316d 16d0 1050" +USB_SECURITY_DONGLE_NK3_VIDPID="20a0:42b2" +USB_SECURITY_DONGLE_CANOKEY_QEMU_VIDPID="20a0:42d4" +USB_SECURITY_DONGLE_NITROKEY_PRO_VIDPID="20a0:4108" +USB_SECURITY_DONGLE_NITROKEY_STORAGE_VIDPID="20a0:4109" +USB_SECURITY_DONGLE_LIBREM_KEY_VIDPID="316d:4c4b" +USB_SECURITY_DONGLE_CANOKEY_VIDPID="16d0:21dc" +USB_SECURITY_DONGLE_YUBIKEY_VID_PREFIX="1050:" + # Nitrokey 3 (NK3) - VID:PID 20a0:42b2 (3A Mini / 3A NFC / 3C NFC all share this PID) # Identified by "Firmware Nitrokey 3: vX.Y.Z" in hotp_verification info output. HOTPKEY_NK3_MIN_VER="v1.8.3" diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 61d396b7d..f728ae028 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -370,13 +370,39 @@ LOG() { # Sets and exports HEADS_TTY and GPG_TTY. # Must be called at script top-level (not inside a subshell) to take effect. detect_heads_tty() { + local _active _dev _candidate if ! HEADS_TTY=$(tty 2>/dev/null); then - local _active _dev + HEADS_TTY="" + DEBUG "detect_heads_tty: tty(1) unavailable, will resolve from active consoles" + else + DEBUG "detect_heads_tty: tty(1) resolved HEADS_TTY=$HEADS_TTY" + fi + + # On dual-console boards (notably qemu with CONFIG_BOOT_RECOVERY_SERIAL), + # gui-init may inherit the recovery serial tty even though its real UI lives + # on the main console. Prefer a non-recovery active console when available. + if [ -n "$RECOVERY_TTY" ] && [ "$HEADS_TTY" = "$RECOVERY_TTY" ]; then + _active=$(cat /sys/class/tty/console/active 2>/dev/null) + DEBUG "detect_heads_tty: HEADS_TTY matches RECOVERY_TTY ($RECOVERY_TTY), active consoles='${_active:-}'" + for _dev in $_active; do + _candidate="/dev/$_dev" + if [ "$_candidate" != "$RECOVERY_TTY" ]; then + [ "$_dev" = "tty0" ] && _candidate="/dev/$(cat /sys/class/tty/tty0/active 2>/dev/null || echo tty0)" + HEADS_TTY="$_candidate" + DEBUG "detect_heads_tty: switched interactive tty away from recovery console to $HEADS_TTY" + break + fi + done + fi + + if [ -z "$HEADS_TTY" ]; then _active=$(cat /sys/class/tty/console/active 2>/dev/null) _dev="${_active##* }" [ "$_dev" = "tty0" ] && _dev=$(cat /sys/class/tty/tty0/active 2>/dev/null || echo tty0) HEADS_TTY="/dev/${_dev:-console}" + DEBUG "detect_heads_tty: falling back to HEADS_TTY=$HEADS_TTY from active consoles='${_active:-}'" fi + DEBUG "detect_heads_tty: exporting HEADS_TTY=$HEADS_TTY GPG_TTY=$HEADS_TTY" export HEADS_TTY export GPG_TTY="$HEADS_TTY" } @@ -486,18 +512,16 @@ pin_color() { } # Detect USB security dongle branding from USB VID:PID via lsusb. +# Runtime dongle IDs are sourced from /etc/dongle-versions. # Sources: hotp-verification/src/device.c and targets/qemu.mk -# USB Security dongle (OpenPGP smart card) VID:PID table: -# 20a0:42b2 Nitrokey 3 (3A Mini / 3A NFC / 3C NFC - all share this PID) -# 20a0:42d4 Canokey QEMU -# 20a0:4108 Nitrokey Pro / Pro 2 (Pro and Pro 2 share the same PID) -# 20a0:4109 Nitrokey Storage / Storage 2 -# 316d:4c4b Librem Key -# 16d0:21dc Canokey -# 1050:0113 Yubikey 4/5 (OTP+U2F+CCID) - legacy -# 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) + +load_usb_security_dongle_ids() { + # /etc/dongle-versions is the single source of truth for runtime IDs. + [ -r /etc/dongle-versions ] || return 1 + . /etc/dongle-versions || return 1 + [ -n "$USB_SECURITY_DONGLE_VIDS" ] || return 1 + return 0 +} # Returns 0 if the given tty path is a serial console. heads_tty_is_serial() { @@ -510,30 +534,47 @@ heads_tty_is_serial() { # 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 . + load_usb_security_dongle_ids || return 1 + local vid + for vid in $USB_SECURITY_DONGLE_VIDS; do + if grep -l -E "^${vid}$" /sys/bus/usb/devices/*/idVendor 2>/dev/null | grep -q .; then + return 0 + fi + done + return 1 } # 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="" + local allow_user_cancel="y" + local deadline remaining ch if heads_tty_is_serial "$interactive_tty"; then is_serial=1 fi + DEBUG "wait_for_usb_security_dongle_vid: interactive_tty='${interactive_tty:-}' is_serial=$is_serial RECOVERY_TTY='${RECOVERY_TTY:-}'" - # Reserve a countdown line; drain stray input on framebuffer. - if [ -n "$interactive_tty" ]; then - printf "\n" >"$interactive_tty" 2>/dev/null - else - printf "\n" + # Never consume keystrokes from the active recovery shell tty. + if [ -n "$RECOVERY_TTY" ] && [ "$interactive_tty" = "$RECOVERY_TTY" ]; then + allow_user_cancel="n" + DEBUG "Disabling USB dongle wait key-cancel on recovery tty ($RECOVERY_TTY)" fi - if [ "$is_serial" = "0" ]; then + + # In non-interactive/background contexts, poll only and avoid read() input capture. + if [ -z "$interactive_tty" ] && [ ! -t 0 ]; then + allow_user_cancel="n" + DEBUG "wait_for_usb_security_dongle_vid: no interactive tty and stdin is not a tty, disabling user-cancel reads" + fi + DEBUG "wait_for_usb_security_dongle_vid: allow_user_cancel=$allow_user_cancel" + + # Drain stray buffered input on framebuffer so stale keystrokes do not + # immediately cancel this wait. + if [ "$allow_user_cancel" = "y" ] && [ "$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 @@ -541,73 +582,58 @@ wait_for_usb_security_dongle_vid() { fi fi + if [ "$allow_user_cancel" != "y" ]; then + STATUS "Waiting up to 15s for USB security dongle detection" + elif [ "$is_serial" = "1" ]; then + STATUS "Waiting up to 15s for USB security dongle detection (press Enter to skip)" + else + STATUS "Waiting up to 15s for USB security dongle detection (press any key to skip)" + 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)" + DEBUG "USB security dongle VID detected in sysfs" + STATUS_OK "USB security dongle detected" 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" + STATUS "No known USB security dongle detected within 15s; continuing" 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 [ "$allow_user_cancel" != "y" ]; then + sleep 1 + elif [ "$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)" + STATUS "USB security dongle wait skipped by user; continuing" return 1 fi else if IFS= read -r -t 1 ch; then - printf "\n\n" DEBUG "User cancelled USB dongle wait (Enter on serial)" + STATUS "USB security dongle wait skipped by user; continuing" 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)" + STATUS "USB security dongle wait skipped by user; continuing" 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)" + STATUS "USB security dongle wait skipped by user; continuing" return 1 fi fi @@ -616,15 +642,8 @@ wait_for_usb_security_dongle_vid() { } # 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. +# This helper enables USB and waits for enumeration before scanning with lsusb. +# Branding detection requires USB modules/device nodes to be available. detect_usb_security_dongle_branding() { TRACE_FUNC local usb_was_enabled="${_USB_ENABLED:-n}" @@ -637,45 +656,45 @@ detect_usb_security_dongle_branding() { return fi - # 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 + # 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 - # Wait up to 15s for a known dongle VID to appear; user can press Esc (fb) or Enter (serial) to skip. + # Wait up to 15s for a known dongle VID to appear; user can press any key (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 + if ! load_usb_security_dongle_ids; then + DEBUG "Failed to load USB security dongle IDs from /etc/dongle-versions" + export DONGLE_BRAND="USB Security dongle" + return + fi local lsusb_out lsusb_out="$(lsusb)" DEBUG "lsusb output: $lsusb_out" # Check NK3 (42b2) before the broader 20a0 vendor match - if echo "$lsusb_out" | grep -q "20a0:42b2"; then - DEBUG "Detected Nitrokey 3 (20a0:42b2)" + if echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_NK3_VIDPID"; then + DEBUG "Detected Nitrokey 3 ($USB_SECURITY_DONGLE_NK3_VIDPID)" export DONGLE_BRAND="Nitrokey 3" - elif echo "$lsusb_out" | grep -q "20a0:42d4"; then - DEBUG "Detected Canokey QEMU (20a0:42d4)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_CANOKEY_QEMU_VIDPID"; then + DEBUG "Detected Canokey QEMU ($USB_SECURITY_DONGLE_CANOKEY_QEMU_VIDPID)" export DONGLE_BRAND="Canokey" - elif echo "$lsusb_out" | grep -q "20a0:4108"; then - DEBUG "Detected Nitrokey Pro (20a0:4108)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_NITROKEY_PRO_VIDPID"; then + DEBUG "Detected Nitrokey Pro ($USB_SECURITY_DONGLE_NITROKEY_PRO_VIDPID)" export DONGLE_BRAND="Nitrokey Pro" - elif echo "$lsusb_out" | grep -q "20a0:4109"; then - DEBUG "Detected Nitrokey Storage (20a0:4109)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_NITROKEY_STORAGE_VIDPID"; then + DEBUG "Detected Nitrokey Storage ($USB_SECURITY_DONGLE_NITROKEY_STORAGE_VIDPID)" export DONGLE_BRAND="Nitrokey Storage" - elif echo "$lsusb_out" | grep -q "316d:4c4b"; then - DEBUG "Detected Librem Key (316d:4c4b)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_LIBREM_KEY_VIDPID"; then + DEBUG "Detected Librem Key ($USB_SECURITY_DONGLE_LIBREM_KEY_VIDPID)" export DONGLE_BRAND="Librem Key" - elif echo "$lsusb_out" | grep -q "16d0:21dc"; then - DEBUG "Detected Canokey (16d0:21dc)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_CANOKEY_VIDPID"; then + DEBUG "Detected Canokey ($USB_SECURITY_DONGLE_CANOKEY_VIDPID)" export DONGLE_BRAND="Canokey" - elif echo "$lsusb_out" | grep -q "1050:"; then - DEBUG "Detected Yubikey (1050:*)" + elif echo "$lsusb_out" | grep -q "$USB_SECURITY_DONGLE_YUBIKEY_VID_PREFIX"; then + DEBUG "Detected Yubikey (${USB_SECURITY_DONGLE_YUBIKEY_VID_PREFIX}*)" export DONGLE_BRAND="Yubikey" else DEBUG "No known USB Security dongle detected" @@ -893,8 +912,9 @@ cache_gpg_signing_pin() { -name '*.key' -delete >/dev/null 2>&1 || true DEBUG "Cleared private-keys-v1.d; agent will re-discover keys via scdaemon" - # USB will be enabled by wait_for_gpg_card() and detect_usb_security_dongle_branding() + # USB will be enabled by wait_for_gpg_card() and detect_usb_security_dongle_branding(). # Wait for USB enumeration before accessing GPG card to avoid race condition + STATUS "Waiting for USB device enumeration before checking GPG card" wait_for_usb_devices STATUS "Verifying presence of USB Security dongle" @@ -916,6 +936,7 @@ cache_gpg_signing_pin() { DIE "gpg card read failed" DEBUG "Retry succeeded" fi + STATUS_OK "GPG card is accessible" # Read card status and display PIN retry counters before prompting. # output excerpt: "PIN retry counter : 3 0 3" @@ -1075,25 +1096,20 @@ recovery() { #Going to recovery shell should be authenticated if supported gpg_auth - #if we have DEBUG_OUTPUT=y, we instruct users to use the debug log - if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - cat /etc/DEBUG_LOG_COPY_INSTRUCTIONS - fi - - #Guide user into enabling debug output in case of a discovered bug - if [ "$CONFIG_DEBUG_OUTPUT" != "y" ]; then - NOTE "To file a bug report with debug logs:\n 1. Options --> Change configuration settings --> Configure $CONFIG_BRAND_NAME informational / debug output --> select Debug, save and flash firmware changes\n 2. After reboot: Options --> TPM/TOTP/HOTP Options --> Generate new TOTP/HOTP secret to reseal secrets" - fi + # Debug and measurement logs are always captured; show copy guidance directly. + cat /etc/DEBUG_LOG_COPY_INSTRUCTIONS # display any custom recovery message just before the banner 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 + # Show PCR state when entering recovery shell only when TPM is enabled. + if [ "$CONFIG_TPM" = "y" ]; then + INFO "TPM: PCR state on entering recovery shell:" + pcrs | while IFS= read -r line; do + INFO "$line" + done + fi # Drain any queued serial input before starting the interactive shell. # This avoids stale bytes being interpreted as bash commands on entry. @@ -1123,43 +1139,7 @@ recovery() { pause_recovery() { TRACE_FUNC INPUT "Press Enter to proceed to recovery shell" - - # 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 + recovery "$@" } combine_configs() { @@ -1273,12 +1253,14 @@ wait_for_usb_devices() { if [ $peripheral_count -gt 0 ]; then DEBUG "USB peripheral devices ready after ${elapsed}s (iteration $iteration): found $peripheral_count device(s)" + STATUS_OK "USB peripheral devices detected" return fi # Timeout after 2 seconds if awk -v s="$start" -v n="$now" 'BEGIN{exit (n - s > 2.0) ? 0 : 1}'; then DEBUG "USB wait timeout at ${elapsed}s (iter $iteration): only found $peripheral_count peripheral device(s)" + WARN "USB peripheral devices were not detected within 2s, continuing" return fi done diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index 29d86e171..a6e168592 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" - # Enable USB first for proper branding detection (user-initiated, won't break DUK unseal) - enable_usb + # Detect USB dongle branding for integrity output. This may initialize USB + # via detect_usb_security_dongle_branding(). detect_usb_security_dongle_branding if [ "$CONFIG_TPM" = "y" ]; then diff --git a/initrd/init b/initrd/init index 32fd33249..069221f8a 100755 --- a/initrd/init +++ b/initrd/init @@ -300,9 +300,11 @@ else done fi - # Wait for any boot script instance to die before checking - # which one needs restart. Prevents busy-polling. - wait + # Wait for the first child exit when supported; otherwise + # fall back to light polling so dead children get respawned. + if ! wait -n 2>/dev/null; then + sleep 1 + fi done else # wait for boot via network to occur From 87a731767c566fa4e493699fc8435f90ef62f523 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Thu, 7 May 2026 14:12:12 -0400 Subject: [PATCH 7/9] doc/boot-process: clarify serial recovery path is asynchronous Signed-off-by: Thierry Laurion --- doc/boot-process.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/boot-process.md b/doc/boot-process.md index 8624145db..7e966bbe6 100644 --- a/doc/boot-process.md +++ b/doc/boot-process.md @@ -43,9 +43,9 @@ kexec ← hands off to OS kernel 3. Runs `cbfs-init` to extract user configuration from CBFS into `/etc/config.user`. 4. Calls `combine_configs()` to merge all `/etc/config*` files into `/tmp/config`, then sources `/tmp/config` so all subsequent scripts see the merged settings. -5. If `CONFIG_BOOT_RECOVERY_SERIAL` is set, starts `pause_recovery` on that - serial TTY (`/dev/ttyS*`) and waits for Enter before launching the recovery - shell there. +5. If `CONFIG_BOOT_RECOVERY_SERIAL` is set, starts a background `pause_recovery` + path on that serial TTY (`/dev/ttyS*`) that waits for Enter and then launches + the recovery shell there. 6. Checks for a quick `r` keypress (100 ms timeout) to drop to a recovery shell before any GUI starts. 7. Starts `cttyhack $CONFIG_BOOTSCRIPT` (default: `/bin/gui-init`) under a PID 1 From 9a98d79ff4a2dbc39d4eb5b2cfe33838df6da610 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Thu, 7 May 2026 16:12:21 -0400 Subject: [PATCH 8/9] init: WARN prior of loading USB keyboard driver to force awareness Signed-off-by: Thierry Laurion --- initrd/init | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/initrd/init b/initrd/init index 48c2cd353..74ac6cfcd 100755 --- a/initrd/init +++ b/initrd/init @@ -191,7 +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" + WARN "USB keyboard required by board or user config; loading USB modules" enable_usb_keyboard fi From 13fef222d71fe20de6bd2a4594a2db87e2205688 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 8 May 2026 11:08:44 -0400 Subject: [PATCH 9/9] .circleci/config.yml: regroup boards by coreboot fork, sort alphabetically, document seeds - Fix regression from a2a027f661: replace x230-hotp-maximized_usb-kb with x230-hotp-maximized in CI (was accidentally swapped) - Group all boards under their respective coreboot fork - Sort entries alphabetically within each fork group - Move EOL_t480 and EOL_t480s into the 25.09 group (were orphaned at bottom) - Reorder fork seeds alphabetically - Document downstream boards per seed Signed-off-by: Thierry Laurion --- .circleci/config.yml | 223 ++++++++++++++++++++++--------------------- 1 file changed, 113 insertions(+), 110 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 450a27a88..8196c8d63 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -457,87 +457,107 @@ workflows: requires: - x86_blobs - # ── x86 coreboot fork seeds ─────────────────────────────────────────── + # ── x86 coreboot fork seeds (alphabetical by board target) ────────────── # Chain: x86_musl_cross_make -> x86_coreboot (linear per fork) # Each seed builds its coreboot fork and saves fork-specific cache. # No fan-in -> no workspace collision. - # dasharo_nv4x fork + # 4.11 fork + # Downstream boards: (none — standalone) - x86_coreboot: - name: novacustom-nv4x_adl - target: novacustom-nv4x_adl + name: EOL_librem_l1um + target: EOL_librem_l1um subcommand: "" - coreboot_dir: coreboot-dasharo_nv4x + coreboot_dir: coreboot-4.11 requires: - x86-musl-cross-make - # dasharo_v56 fork + # 25.09 fork — builds coreboot-25.09 toolchain + # Downstream boards: (all 28 board builds below) - x86_coreboot: - name: novacustom-v560tu - target: novacustom-v560tu + name: EOL_t480-hotp-maximized + target: EOL_t480-hotp-maximized subcommand: "" - coreboot_dir: coreboot-dasharo_v56 + coreboot_dir: coreboot-25.09 requires: - x86-musl-cross-make - # purism fork + # dasharo_msi_z690 fork + # Downstream boards: UNTESTED_msi_z690a_ddr5 - x86_coreboot: - name: librem_14 - target: librem_14 + name: UNTESTED_msi_z690a_ddr4 + target: UNTESTED_msi_z690a_ddr4 subcommand: "" - coreboot_dir: coreboot-purism + coreboot_dir: coreboot-dasharo_msi_z690 requires: - x86-musl-cross-make - # 25.09 fork - # Seed workspace for downstream 25.09 boards and x86_save_modules_cache. + # dasharo_msi_z790 fork + # Downstream boards: msi_z790p_ddr5 - x86_coreboot: - name: EOL_t480-hotp-maximized - target: EOL_t480-hotp-maximized + name: UNTESTED_msi_z790p_ddr4 + target: UNTESTED_msi_z790p_ddr4 subcommand: "" - coreboot_dir: coreboot-25.09 + coreboot_dir: coreboot-dasharo_msi_z790 requires: - x86-musl-cross-make - # 4.11 fork + # dasharo_nv4x fork + # Downstream boards: UNTESTED_nitropad-ns50 - x86_coreboot: - name: EOL_librem_l1um - target: EOL_librem_l1um + name: novacustom-nv4x_adl + target: novacustom-nv4x_adl subcommand: "" - coreboot_dir: coreboot-4.11 + coreboot_dir: coreboot-dasharo_nv4x requires: - x86-musl-cross-make - # dasharo_msi_z690 fork + # dasharo_v56 fork + # Downstream boards: novacustom-v540tu - x86_coreboot: - name: UNTESTED_msi_z690a_ddr4 - target: UNTESTED_msi_z690a_ddr4 + name: novacustom-v560tu + target: novacustom-v560tu subcommand: "" - coreboot_dir: coreboot-dasharo_msi_z690 + coreboot_dir: coreboot-dasharo_v56 requires: - x86-musl-cross-make - # dasharo_msi_z790 fork + # purism fork — builds coreboot-purism toolchain + # Downstream boards: EOL_librem_13v2, EOL_librem_13v4, EOL_librem_15v3, EOL_librem_15v4, + # librem_11, librem_l1um_v2, librem_mini, librem_mini_v2 - x86_coreboot: - name: UNTESTED_msi_z790p_ddr4 - target: UNTESTED_msi_z790p_ddr4 + name: librem_14 + target: librem_14 subcommand: "" - coreboot_dir: coreboot-dasharo_msi_z790 + coreboot_dir: coreboot-purism requires: - x86-musl-cross-make - # Those onboarding new boards should add their entries below. - # coreboot 25.09 boards + # ── coreboot 25.09 boards (alphabetical) ─────────────────────────────── - build: - name: EOL_x220-hotp-maximized - target: EOL_x220-hotp-maximized + name: EOL_optiplex-7010_9010-hotp-maximized + target: EOL_optiplex-7010_9010-hotp-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_x220-maximized - target: EOL_x220-maximized + name: EOL_optiplex-7010_9010-maximized + target: EOL_optiplex-7010_9010-maximized + subcommand: "" + requires: + - EOL_t480-hotp-maximized + + - build: + name: EOL_optiplex-7010_9010_TXT-hotp-maximized + target: EOL_optiplex-7010_9010_TXT-hotp-maximized + subcommand: "" + requires: + - EOL_t480-hotp-maximized + + - build: + name: EOL_optiplex-7010_9010_TXT-maximized + target: EOL_optiplex-7010_9010_TXT-maximized subcommand: "" requires: - EOL_t480-hotp-maximized @@ -564,59 +584,57 @@ workflows: - EOL_t480-hotp-maximized - build: - name: EOL_x230-maximized - target: EOL_x230-maximized + name: EOL_t430-maximized + target: EOL_t430-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_optiplex-7010_9010-maximized - target: EOL_optiplex-7010_9010-maximized + name: EOL_t440p-hotp-maximized + target: EOL_t440p-hotp-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_optiplex-7010_9010-hotp-maximized - target: EOL_optiplex-7010_9010-hotp-maximized + name: EOL_t440p-maximized + target: EOL_t440p-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_optiplex-7010_9010_TXT-maximized - target: EOL_optiplex-7010_9010_TXT-maximized + name: EOL_t480-maximized + target: EOL_t480-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_optiplex-7010_9010_TXT-hotp-maximized - target: EOL_optiplex-7010_9010_TXT-hotp-maximized + name: EOL_t480s-hotp-maximized + target: EOL_t480s-hotp-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - # Runs in parallel with other 25.09 board builds. - - build: - name: EOL_x230-hotp-maximized_usb-kb - target: EOL_x230-hotp-maximized_usb-kb + name: EOL_t480s-maximized + target: EOL_t480s-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_x230-maximized-fhd_edp - target: EOL_x230-maximized-fhd_edp + name: EOL_UNTESTED_t530-hotp-maximized + target: EOL_UNTESTED_t530-hotp-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_x230-hotp-maximized-fhd_edp - target: EOL_x230-hotp-maximized-fhd_edp + name: EOL_UNTESTED_t530-maximized + target: EOL_UNTESTED_t530-maximized subcommand: "" requires: - EOL_t480-hotp-maximized @@ -628,13 +646,6 @@ workflows: requires: - EOL_t480-hotp-maximized - - build: - name: EOL_t430-maximized - target: EOL_t430-maximized - subcommand: "" - requires: - - EOL_t480-hotp-maximized - - build: name: EOL_w530-maximized target: EOL_w530-maximized @@ -643,57 +654,57 @@ workflows: - EOL_t480-hotp-maximized - build: - name: EOL_UNTESTED_t530-maximized - target: EOL_UNTESTED_t530-maximized + name: EOL_w541-hotp-maximized + target: EOL_w541-hotp-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_UNTESTED_t530-hotp-maximized - target: EOL_UNTESTED_t530-hotp-maximized + name: EOL_w541-maximized + target: EOL_w541-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_t440p-maximized - target: EOL_t440p-maximized + name: EOL_x220-hotp-maximized + target: EOL_x220-hotp-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_t440p-hotp-maximized - target: EOL_t440p-hotp-maximized + name: EOL_x220-maximized + target: EOL_x220-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_w541-maximized - target: EOL_w541-maximized + name: EOL_x230-hotp-maximized + target: EOL_x230-hotp-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_w541-hotp-maximized - target: EOL_w541-hotp-maximized + name: EOL_x230-hotp-maximized-fhd_edp + target: EOL_x230-hotp-maximized-fhd_edp subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: qemu-coreboot-fbwhiptail-tpm2-hotp - target: qemu-coreboot-fbwhiptail-tpm2-hotp + name: EOL_x230-maximized + target: EOL_x230-maximized subcommand: "" requires: - EOL_t480-hotp-maximized - build: - name: EOL_z220-cmt-maximized - target: EOL_z220-cmt-maximized + name: EOL_x230-maximized-fhd_edp + target: EOL_x230-maximized-fhd_edp subcommand: "" requires: - EOL_t480-hotp-maximized @@ -705,45 +716,45 @@ workflows: requires: - EOL_t480-hotp-maximized - # librem boards: use coreboot-purism fork (librem_14 builds the fork's toolchain) - build: - name: EOL_librem_13v2 - target: EOL_librem_13v2 + name: EOL_z220-cmt-maximized + target: EOL_z220-cmt-maximized subcommand: "" requires: - - librem_14 + - EOL_t480-hotp-maximized - build: - name: EOL_librem_15v3 - target: EOL_librem_15v3 + name: qemu-coreboot-fbwhiptail-tpm2-hotp + target: qemu-coreboot-fbwhiptail-tpm2-hotp subcommand: "" requires: - - librem_14 + - EOL_t480-hotp-maximized + # ── purism boards (alphabetical) ────────────────────────────────────── - build: - name: EOL_librem_13v4 - target: EOL_librem_13v4 + name: EOL_librem_13v2 + target: EOL_librem_13v2 subcommand: "" requires: - librem_14 - build: - name: EOL_librem_15v4 - target: EOL_librem_15v4 + name: EOL_librem_13v4 + target: EOL_librem_13v4 subcommand: "" requires: - librem_14 - build: - name: librem_mini - target: librem_mini + name: EOL_librem_15v3 + target: EOL_librem_15v3 subcommand: "" requires: - librem_14 - build: - name: librem_mini_v2 - target: librem_mini_v2 + name: EOL_librem_15v4 + target: EOL_librem_15v4 subcommand: "" requires: - librem_14 @@ -762,29 +773,21 @@ workflows: requires: - librem_14 - # t480 is based on 25.09 coreboot release - - build: - name: EOL_t480-maximized - target: EOL_t480-maximized - subcommand: "" - requires: - - EOL_t480-hotp-maximized - - build: - name: EOL_t480s-hotp-maximized - target: EOL_t480s-hotp-maximized + name: librem_mini + target: librem_mini subcommand: "" requires: - - EOL_t480-hotp-maximized + - librem_14 - build: - name: EOL_t480s-maximized - target: EOL_t480s-maximized + name: librem_mini_v2 + target: librem_mini_v2 subcommand: "" requires: - - EOL_t480-hotp-maximized + - librem_14 - # Dasharo nv4x: shares dasharo_nv4x toolchain with novacustom-nv4x_adl + # ── Dasharo nv4x ───────────────────────────────────────────────────── - build: name: UNTESTED_nitropad-ns50 target: UNTESTED_nitropad-ns50 @@ -792,7 +795,7 @@ workflows: requires: - novacustom-nv4x_adl - # NovaCustom v56: shares dasharo_v56 toolchain with novacustom-v560tu + # ── NovaCustom v56 ────────────────────────────────────────────────── - build: name: novacustom-v540tu target: novacustom-v540tu @@ -800,7 +803,7 @@ workflows: requires: - novacustom-v560tu - # Dasharo msi_z690: shares dasharo_msi_z690 toolchain with UNTESTED_msi_z690a_ddr4 + # ── Dasharo msi_z690 ──────────────────────────────────────────────── - build: name: UNTESTED_msi_z690a_ddr5 target: UNTESTED_msi_z690a_ddr5 @@ -808,7 +811,7 @@ workflows: requires: - UNTESTED_msi_z690a_ddr4 - # Dasharo msi_z790: shares dasharo_msi_z790 toolchain with UNTESTED_msi_z790p_ddr4 + # ── Dasharo msi_z790 ──────────────────────────────────────────────── - build: name: msi_z790p_ddr5 target: msi_z790p_ddr5