From e12252b46335ccf73745e1b2ac0767dc5c9a2ec6 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Sun, 8 Mar 2026 13:13:27 -0400 Subject: [PATCH] initrd: harden TPM integrity/reseal flows, logging/colored flow and add prod_quiet QEMU configs Improve TPM/TOTP/HOTP recovery and reseal behavior by adding integrity-first gating, coloring and improvements of logging/input wrappers, clearer failure handling, and stronger rollback preflight checks. - add integrity report + investigation flows in GUI, with explicit actions before reseal/reset paths - introduce TPM reset-required markers and rollback preflight validation to fail early on inconsistent TPM state - make unseal/seal paths safer and more recoverable (nonfatal unseal mode, clearer reset/reseal guidance, better TPM1/TPM2 handling) - improve kexec signing reliability with explicit signing key selection and actionable GPG error diagnostics - avoid hiding interactive password/PIN prompts by removing inappropriate debug wrappers around sensitive interactive commands - add run_lvm wrapper and switch runtime scripts to reduce harmless LVM noise - refresh TPM2 primary-handle hash in update/signing flows to keep trust metadata in sync - add new qemu fbwhiptail prod_quiet board configs for TPM1 and TPM2 - fix board-name values for existing qemu hotp prod_quiet variants - document QEMU canokey state reuse and TPM2 pcap capture debugging - ignore exported public key artifacts (*.asc) in .gitignore - rewrite doc/logging.md: debug.log always captures every level regardless of output mode; console visibility is the only mode-dependent behavior; document STATUS, NOTE, INPUT levels and ANSI color coding rationale - fix DEBUG, TRACE, warn to unconditionally write to debug.log (previously only wrote to debug.log when CONFIG_DEBUG_OUTPUT=y) - add STATUS, STATUS_OK NOTE, INPUT logging functions with ANSI color coding; replace bare echo/read patterns across codebase with proper log levels - fix INPUT: echo after read so single-char keypresses do not bleed onto the next output line - demote "No encrypted LVMs/devices found" from INFO to DEBUG - add per-state signing_key_guidance in integrity report (AVAILABLE / CARD UNPROVISIONED / CARD KEY DOES NOT MATCH FIRMWARE / NO CARD DETECTED) replacing a generic catch-all message - suppress redundant Measured Integrity Report when user navigates to OEM Factory Reset from within the report (INTEGRITY_REPORT_ALREADY_SHOWN) - call wait_for_gpg_card silently first; only prompt to insert card if not already detected - call enable_usb unconditionally at gui-init startup (was gated on HOTP) - call wait_for_gpg_card before GPG key count check in reset_tpm loop so card is detected on first pass without requiring a manual retry - reboot: qemu-* calls poweroff from reboot, pauses for recovery Signed-off-by: Thierry Laurion --- .gitignore | 1 + ...oot-fbwhiptail-tpm1-hotp-prod_quiet.config | 2 +- ...coreboot-fbwhiptail-tpm1-prod_quiet.config | 97 ++ ...oot-fbwhiptail-tpm2-hotp-prod_quiet.config | 3 +- ...coreboot-fbwhiptail-tpm2-prod_quiet.config | 98 ++ .../qemu-coreboot-fbwhiptail-tpm2.config | 3 + doc/logging.md | 308 +++-- initrd/bin/change-time.sh | 31 +- initrd/bin/config-gui.sh | 20 +- initrd/bin/flash.sh | 10 +- initrd/bin/generic-init | 8 +- initrd/bin/gpg-gui.sh | 11 +- initrd/bin/gui-init | 440 +++++-- initrd/bin/gui-init-basic | 15 +- initrd/bin/kexec-boot | 16 +- initrd/bin/kexec-insert-key | 29 +- initrd/bin/kexec-iso-init | 23 +- initrd/bin/kexec-save-default | 49 +- initrd/bin/kexec-save-key | 2 +- initrd/bin/kexec-seal-key | 35 +- initrd/bin/kexec-select-boot | 61 +- initrd/bin/kexec-sign-config | 63 +- initrd/bin/kexec-unseal-key | 5 +- initrd/bin/lock_chip | 5 +- initrd/bin/media-scan | 8 +- initrd/bin/mount-usb | 11 +- initrd/bin/network-init-recovery | 75 +- initrd/bin/oem-factory-reset | 291 ++--- initrd/bin/reboot | 20 +- initrd/bin/root-hashes-gui.sh | 24 +- initrd/bin/seal-hotpkey | 19 +- initrd/bin/seal-totp | 19 +- initrd/bin/tpm-reset | 4 +- initrd/bin/tpmr | 228 +++- initrd/bin/uefi-init | 2 +- initrd/bin/unseal-hotp | 35 +- initrd/bin/unseal-totp | 53 +- initrd/bin/usb-autoboot.sh | 5 +- initrd/bin/wget-measure.sh | 6 - initrd/etc/functions | 1045 ++++++++++++++--- initrd/etc/gui_functions | 526 ++++++++- initrd/etc/luks-functions | 97 +- initrd/init | 24 +- initrd/mount-boot | 20 +- initrd/sbin/config-dhcp.sh | 4 +- initrd/sbin/insmod | 4 +- targets/qemu.md | 13 + 47 files changed, 2897 insertions(+), 971 deletions(-) create mode 100644 boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config create mode 100644 boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config diff --git a/.gitignore b/.gitignore index 720f911aa..6613cfe85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.asc *.bad *.bz2 *.cpio diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config index 28e96d0d3..6ebe9fba9 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config @@ -91,7 +91,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config new file mode 100644 index 000000000..bad41b87d --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config @@ -0,0 +1,97 @@ +# Configuration for building a coreboot ROM that works in +# the qemu emulator in console mode thanks to Whiptail +# +# TPM can be used with a qemu software TPM (TIS, 1.2). +export CONFIG_COREBOOT=y +export CONFIG_COREBOOT_VERSION=25.09 +export CONFIG_LINUX_VERSION=6.1.8 + +CONFIG_COREBOOT_CONFIG=config/coreboot-qemu-tpm1-prod.config +CONFIG_LINUX_CONFIG=config/linux-qemu.config + +#Enable only one RESTRICTED/BASIC boot modes below to test them manually (we cannot inject config under QEMU (no internal flashing) +#export CONFIG_RESTRICTED_BOOT=y +#export CONFIG_BASIC=y + +#Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) +#export CONFIG_HAVE_GPG_KEY_BACKUP=y + +#On-demand hardware support (modules.cpio) +CONFIG_LINUX_USB=y +CONFIG_LINUX_E1000=y +#CONFIG_MOBILE_TETHERING=y +#Runtime on-demand additional hardware support (modules.cpio) +export CONFIG_LINUX_USB_COMPANION_CONTROLLER=y + + + +#Modules packed into tools.cpio +ifeq "$(CONFIG_UROOT)" "y" +CONFIG_BUSYBOX=n +else +#Modules packed into tools.cpio +CONFIG_CRYPTSETUP2=y +CONFIG_FLASHPROG=y +CONFIG_FLASHTOOLS=y +CONFIG_GPG2=y +CONFIG_KEXEC=y +CONFIG_UTIL_LINUX=y +CONFIG_LVM2=y +CONFIG_MBEDTLS=y +CONFIG_PCIUTILS=y +#Runtime tools to write to MSR +#CONFIG_MSRTOOLS=y +#Remote attestation support +# TPM2 requirements +#CONFIG_TPM2_TSS=y +#CONFIG_OPENSSL=y +#Remote Attestation common tools +CONFIG_POPT=y +CONFIG_QRENCODE=y +CONFIG_TPMTOTP=y +#HOTP based remote attestation for supported USB Security dongle +#With/Without TPM support +#CONFIG_HOTPKEY=y +#Nitrokey Storage admin tool (deprecated) +#CONFIG_NKSTORECLI=n +#GUI Support +#Console based Whiptail support(Console based, no FB): +#CONFIG_SLANG=y +#CONFIG_NEWT=y +#FBWhiptail based (Graphical): +CONFIG_CAIRO=y +CONFIG_FBWHIPTAIL=y +#Additional tools (tools.cpio): +#SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) +CONFIG_DROPBEAR=y +endif + +#Runtime configuration +#Automatically boot if HOTP is valid +export CONFIG_AUTO_BOOT_TIMEOUT=5 +#TPM2 requirements +#export CONFIG_TPM2_TOOLS=y +#export CONFIG_PRIMARY_KEY_TYPE=ecc +#TPM1 requirements +export CONFIG_TPM=y +#Enable DEBUG output +export CONFIG_DEBUG_OUTPUT=n +export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n +#Enable TPM2 pcap output under /tmp +export CONFIG_TPM2_CAPTURE_PCAP=n +#Enable quiet mode: technical information logged under /tmp/debug.log +export CONFIG_QUIET_MODE=y +export CONFIG_BOOTSCRIPT=/bin/gui-init +#text-based original init: +#export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOT_REQ_HASH=n +export CONFIG_BOOT_REQ_ROLLBACK=n +export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" +export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" +export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-prod_quiet" +#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" + +export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" + +BOARD_TARGETS := qemu diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config index 08026e5dd..53d3a2ce9 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config @@ -17,6 +17,7 @@ CONFIG_LINUX_CONFIG=config/linux-qemu.config #Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) #export CONFIG_HAVE_GPG_KEY_BACKUP=y + #On-demand hardware support (modules.cpio) CONFIG_LINUX_USB=y CONFIG_LINUX_E1000=y @@ -90,7 +91,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config new file mode 100644 index 000000000..00d54892b --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config @@ -0,0 +1,98 @@ +# Configuration for building a coreboot ROM that works in +# the qemu emulator in graphical mode thanks to FBWhiptail +# +# TPM can be used with a qemu software TPM (TIS, 2.0). +export CONFIG_COREBOOT=y +export CONFIG_COREBOOT_VERSION=25.09 +export CONFIG_LINUX_VERSION=6.1.8 + +CONFIG_COREBOOT_CONFIG=config/coreboot-qemu-tpm2-prod.config +CONFIG_LINUX_CONFIG=config/linux-qemu.config + +#Enable only one RESTRICTED/BASIC boot modes below to test them manually (we cannot inject config under QEMU (no internal flashing) +#export CONFIG_RESTRICTED_BOOT=y +#export CONFIG_BASIC=y + +#Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) +#export CONFIG_HAVE_GPG_KEY_BACKUP=y + + +#On-demand hardware support (modules.cpio) +CONFIG_LINUX_USB=y +CONFIG_LINUX_E1000=y +#CONFIG_MOBILE_TETHERING=y +#Runtime on-demand additional hardware support (modules.cpio) +export CONFIG_LINUX_USB_COMPANION_CONTROLLER=y + + + +#Modules packed into tools.cpio +ifeq "$(CONFIG_UROOT)" "y" +CONFIG_BUSYBOX=n +else +#Modules packed into tools.cpio +CONFIG_CRYPTSETUP2=y +CONFIG_FLASHPROG=y +CONFIG_FLASHTOOLS=y +CONFIG_GPG2=y +CONFIG_KEXEC=y +CONFIG_UTIL_LINUX=y +CONFIG_LVM2=y +CONFIG_MBEDTLS=y +CONFIG_PCIUTILS=y +#Runtime tools to write to MSR +CONFIG_MSRTOOLS=y +#Remote attestation support +# TPM2 requirements +CONFIG_TPM2_TSS=y +CONFIG_OPENSSL=y +#Remote Attestation common tools +CONFIG_POPT=y +CONFIG_QRENCODE=y +CONFIG_TPMTOTP=y +#HOTP based remote attestation for supported USB Security dongle +#With/Without TPM support +#CONFIG_HOTPKEY=y +#Nitrokey Storage admin tool (deprecated) +#CONFIG_NKSTORECLI=n +#GUI Support +#Console based Whiptail support(Console based, no FB): +#CONFIG_SLANG=y +#CONFIG_NEWT=y +#FBWhiptail based (Graphical): +CONFIG_CAIRO=y +CONFIG_FBWHIPTAIL=y +#Additional tools (tools.cpio): +#SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) +CONFIG_DROPBEAR=y +endif + +#Runtime configuration +#Automatically boot if HOTP is valid +export CONFIG_AUTO_BOOT_TIMEOUT=5 +#TPM2 requirements +export CONFIG_TPM2_TOOLS=y +export CONFIG_PRIMARY_KEY_TYPE=ecc +#TPM1 requirements +#export CONFIG_TPM=y +#Enable DEBUG output +#export CONFIG_DEBUG_OUTPUT=y +#export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y +#Enable TPM2 pcap output under /tmp +export CONFIG_TPM2_CAPTURE_PCAP=y +#Enable quiet mode: technical information logged under /tmp/debug.log +export CONFIG_QUIET_MODE=y +export CONFIG_BOOTSCRIPT=/bin/gui-init +#text-based original init: +#export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOT_REQ_HASH=n +export CONFIG_BOOT_REQ_ROLLBACK=n +export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" +export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" +export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-prod_quiet" +#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" + +export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" + +BOARD_TARGETS := qemu diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config b/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config index c4839b6b1..a701a95b4 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config @@ -79,6 +79,9 @@ export CONFIG_PRIMARY_KEY_TYPE=ecc export CONFIG_DEBUG_OUTPUT=y export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #Enable TPM2 pcap output under /tmp +# When enabled, tpmr writes TPM2 command/response capture to /tmp/tpm0.pcap +# (inside the Heads runtime). This can be inspected with Wireshark to debug +# TPM interaction similarly to a TPM bus sniffer. export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n diff --git a/doc/logging.md b/doc/logging.md index 7bfdea0ba..316bc3371 100644 --- a/doc/logging.md +++ b/doc/logging.md @@ -3,56 +3,96 @@ Heads produces debug logging to aid development and troubleshooting. Logging is produced in scripts at a _log level_. -Users can set an _output level_ that controls how much output they see on the screen. +Users can set an _output level_ that controls how much output they see on the **screen**. -# Log Levels +**`/tmp/debug.log` always captures every log level regardless of output mode.** +This makes it a complete diagnostic artifact that can be shared with developers after any issue, +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. + +## Log Levels In order from "most verbose" to "least verbose": -LOG > TRACE > DEBUG > INFO > (console) > NOTE > warn +LOG > TRACE > DEBUG > INFO > STATUS / STATUS_OK > NOTE > warn > die -("console" level output is historical and should be replaced with INFO.) +("console" level output is historical and should be replaced with INFO or STATUS.) ## LOG LOG is for very detailed output or output with uncontrolled length. -It never goes to the screen, this always goes to the log file. -Usually, we dump outputs of commands like 'lsblk', 'lsusb', 'gpg --list-keys', etc. at LOG level (using DO_WITH_DEBUG or SINK_LOG), so we can tell the state of the system from a log submitted by a user. -We rarely want these on the console as they usually hide more relevant output with information that we already know. +It never goes to the screen. It always goes to debug.log. +Usually, we dump outputs of commands like `lsblk`, `lsusb`, `gpg --list-keys`, etc. at LOG level +(using `DO_WITH_DEBUG` or `SINK_LOG`), so we can tell the state of the system from a log submitted +by a user. We rarely want these on the console as they usually hide more relevant output. Use this in situations like: -* Dumping information about the state of the system for debugging. The output doesn't indicate any specific action/decision in Heads or a problem, it's just state relevant for troubleshooting the rest of the log. -* Tracing something that might be very long (including "we don't know how long this will be", even if it's sometimes short). Very long output isn't useful on the console, since you can't scroll back, and it hides more important information. -* The output is intended for debugging a specific topic, and usually unintersting otherwise. We want to be able to turn up output to DEBUG/TRACE when working on any topic without excessively filling the console with every topic's detailed output. + +* Dumping information about the state of the system for debugging. The output doesn't indicate any + specific action/decision in Heads or a problem - it's just state relevant for troubleshooting. +* Tracing something that might be very long. Very long output isn't useful on the console since you + can't scroll back, and it hides more important information. +* Output intended for debugging a specific topic that is usually uninteresting otherwise. ## TRACE TRACE is for following execution flow through Heads. -(TRACE_FUNC logs the current source location at TRACE level, you can use this when entering a function or script, this is much more common than using TRACE directly.) +(`TRACE_FUNC` logs the current source location at TRACE level. Use this when entering a function +or script - this is much more common than using TRACE directly.) You can also use TRACE to show parameter values to scripts or functions. -Since TRACE is for execution flow, show the unprocessed parameters as provided by the caller, not an interpreted version. -(This is uncommon though as it is very verbose, and we can also capture interesting call sites with DO_WITH_DEBUG.) +Since TRACE is for execution flow, show the unprocessed parameters as provided by the caller, not +an interpreted version. (This is uncommon as it is very verbose; we can also capture interesting +call sites with `DO_WITH_DEBUG`.) + +If you are tracing the result of a decision, consider using DEBUG instead. + +### Reading TRACE_FUNC output + +Each TRACE_FUNC call emits the full call chain leading to the current function. +The format is: + +```text +TRACE: caller(file:line) -> ... -> current_func(file:line) +``` + +The line number in each entry means something different depending on position: + +* **Non-last entries**: the line number is the **call site** - the line within that function where + it called the next function in the chain. +* **Last entry**: the line number is where **TRACE_FUNC itself** is called inside the current + function (typically the first line of the function body). + +Example - a `tpmr unseal` call triggered from `gui-init`: + +```text +TRACE: main(/init:0) -> main(/bin/gui-init:0) -> main(/bin/tpmr:0) -> main(/bin/tpmr:1037) -> tpm2_unseal(/bin/tpmr:635) +``` -You can invoke TRACE to show specific execution flow when needed, but if you are tracing the result of a decision, consider using DEBUG instead. +* `main(/init:0)` - `/init` is the root script; `:0` marks a cross-process boundary +* `main(/bin/gui-init:0)` - `gui-init` was launched by `/init` as a subprocess +* `main(/bin/tpmr:0)` - `tpmr` was launched by `gui-init` as a subprocess +* `main(/bin/tpmr:1037)` - line 1037 in `tpmr`'s `main` is the call site of `tpm2_unseal "$@"` +* `tpm2_unseal(/bin/tpmr:635)` - line 635 is where `TRACE_FUNC` is in `tpm2_unseal` Use this in situations like: + * Following control flow - use TRACE_FUNC when entering a script or function -* Showing the parameters used to invoke a script/function, when they are especially relevant and not excessively verbose +* Showing the parameters used to invoke a script/function, when especially relevant ## DEBUG DEBUG is for most log information that is relevant if you are a Heads developer. -Use DEBUG to highlight the decisions made in script logic, and the information that affects those decisions. -Generally, focus on decision points (if, else, case, while, for, etc.), because we can keep following straight-line execution without further tracing. +Use DEBUG to highlight the decisions made in script logic, and the information that affects those +decisions. Generally, focus on decision points (if, else, case, while, for, etc.), because we can +keep following straight-line execution without further tracing. -Decision points usually capture program behavior the best. -Show the information that is about to influence a decision (`DEBUG "Found ${#DEVS[@]} block devices: to check for LUKS:" "${DEVS[@]}"`) and/or the results of the decision (`DEBUG "${DEVS[$i]} is not a LUKS device, ignore it`). +Show the information that is about to influence a decision and/or the results of the decision. -Use DO_WITH_DEBUG to capture a particular command execution to the debug log. -The command and its arguments are captured at DEBUG level (as they usually indicate the decisions the command will make), and the command's stdout/stderr are captured at LOG level. -See DO_WITH_DEBUG for examples of usage. +Use `DO_WITH_DEBUG` to capture a particular command execution to the debug log. +The command and its arguments are captured at DEBUG level (as they usually indicate the decisions +the command will make), and the command's stdout/stderr are captured at LOG level. Use this in situations like: @@ -61,129 +101,231 @@ 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. -Users can control whether this is displayed on the console. +INFO is for contextual information that may be of interest to end users, but that is not required +for use of Heads. + +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). -Users might use this to troubleshoot Heads configuration or behavior, but this should not require knowledge of Heads implementation or developer experience. +Users might use this to troubleshoot Heads configuration or behavior, but this should not require +knowledge of Heads implementation or developer experience. 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"`) +* "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"` 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. +They can include configuration values or context, _but_ they should refer to configuration settings +using the user-facing names in the configuration menus. Use this in situations like: -* Showing very high level decision-making information, which is reasonably understandable for users not familiar with Heads implementation +* 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 ## console -This level is historical, use INFO for this. -It is documented as there are still some occurrences in Heads, usually `echo`, `echo >&2`, or `echo >/dev/console`, each intended to produce output directly on the console. -The intent is the same as INFO. +This level is historical, use INFO or STATUS for this. +It is documented as there are still some occurrences in Heads, usually `echo`, `echo >&2`, or +`echo >/dev/console`, each intended to produce output directly on the console. + +(This is different from `echo` used to produce output that might be captured by a caller, which is +not logging at all.) + +Avoid using this, and change existing console output to INFO, STATUS, or another appropriate level. + +## STATUS + +STATUS is for action announcements - operations that are starting or in progress - that all users +must see regardless of output mode. + +Use STATUS when an action is beginning or underway: + +* "Verifying ISO" - a signature check is running +* "Unlocking LUKS device(s) using the Disk Recovery Key passphrase" - an unlock is in progress +* "Executing default boot for $name" - what is about to boot + +Unlike INFO, STATUS is always visible on the console in all output modes. +Unlike NOTE, STATUS does not sleep - it is for routine progress announcements. + +STATUS always goes to debug.log. + +## STATUS_OK -(This is different from `echo` used to produce output that might be captured by a caller, which is not logging at all.) +STATUS_OK is for confirmed successful results - use it when reporting that an operation succeeded, +a verification passed, or a resource was confirmed available. -Avoid using this, and change existing console output to INFO or another level. +Use STATUS_OK (not STATUS) for completed positive outcomes: + +* "ISO signature verified" - verification succeeded +* "LUKS device unlocked successfully" - unlock confirmed +* "GPG signature on kexec boot params verified" - integrity check passed +* "Heads firmware job done - starting your OS" - handoff complete + +STATUS_OK uses two signals so success is scannable without relying on either alone: + +* **`OK` text prefix** — readable in monochrome, on serial consoles, and with color vision deficiency +* **Bold green color** — instant visual scan for sighted users + +This follows the Linux/systemd `[OK]`/`[FAILED]` convention: always pair color with a text label. +The console renders `OK message` (with a leading space) in bold green; debug.log records it in plain text. ## NOTE -NOTE is for contextual information explaining something that is _likely_ to be unexpected or confusing to users new to Heads. +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. -Unlike INFO, it cannot be hidden. Use this only if the behavior is likely to be unexpected or confusing to many users. If it is only possibly unexpected or uncommon that it is confusing, consider INFO instead. +Do not overuse this above INFO. Adding too much output at NOTE causes users to ignore it. -Do not overuse this above INFO. Adding too much output at NOTE causes users to ignore it, as there is too much output. +NOTE always goes to debug.log. For example: -* "Rebooting in 3 seconds to enable booting default boot option". Users probably don't expect the firmware to reboot to accomplish this behavior, this is unique to Heads. Without a message justifying the reboot, it would likely appear that the firmware faulted and reset unexpectedly. -* "Your GPG User PIN, followed by Enter key will be required [...]". GPG prompts are very confusing to users unfamiliar with GPG (which is most users). +* "Proceeding with unsigned ISO boot" - booting without a verified signature is unexpected and + carries risk; the user needs to know it is happening deliberately. +* "TOTP secret no longer accessible: TPM secrets were wiped" - mid-session secret loss requires + immediate user attention. ## 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. +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 level and the utility function are lowercase, as they predate the other levels.) -This is apppriate 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 if this is intentional +This is appropriate when _all_ of the following are true: -**Do not overuse this.** Overuse of this level causes user to become accustomed to ignoring warnings. -This level only has value as long as it does not occur frequently, so users will notice warnings. +* 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 -Warnings must indicate a _likely_ problem. -(Not a rare or remote possibility of a problem.) +**Do not overuse this.** Overuse of this level causes users to become accustomed to ignoring +warnings. This level only has value as long as it does not occur frequently. -Warnings are only appropriate if we're able to continue operating. -If we can't, consider prompting the user instead, since we cannot do what they asked. +Warnings must indicate a _likely_ problem (not a rare or remote possibility). +Warnings are only appropriate if we are able to continue operating. +Warnings must be _actionable_ - only warn if there is a reasonable change the user can make. -Warnings must be _actionable_. Only warn if there is a reasonable change the user can make to avoid the warning. +warn always goes to debug.log. For example: -* Warning when using default passphrases that are completely insecure is reasonable - the user has no security, and if they want that, they should use Basic mode. -* Warning when an unknown variable appears in config.user is not reasonable - there's no reasonable way for the user to address this. -# Output Levels +* Warning when using default passphrases that are completely insecure is reasonable. +* Warning when an unknown variable appears in config.user is not reasonable - there's no reasonable + way for the user to address this. -Users can choose one of three output levels for extra console information. +## die -* None - Show no extra output. Only warnings appear on console. (Some 'console' level output appears that has not been addressed yet.) -* Info - Show information about operations in Heads. (INFO and below.) -* Debug - Show detailed information suitable for debugging Heads. (TRACE and below.) Log file captures all levels. +die is for fatal errors from which Heads cannot recover. Execution stops after die. -TODO: Document what happens for kernel messages too. -This is more complex though since it is influenced by the board's config and user config differently (maybe we should improve that.) +die always goes to debug.log and is always shown on the console regardless of output mode. -TODO: Document the variables that control these levels +## INPUT -## None - no extra output +INPUT is a direct replacement for the `echo "prompt"; read [flags] VAR` pattern. +It displays the prompt in **bold white** to visually distinguish interactive input requests from +progress/info messages. -| Sink | LOG | TRACE | DEBUG | INFO | console | NOTE | warn | -|-------------------------|-----|-------|-------|------|---------|------|------| -| Console (via /dev/kmsg) | | | | | Yes* | Yes | Yes | -| /tmp/debug.log | Yes | | | | | | | +Usage: `INPUT "prompt text" [read-flags] [VARNAME]` -* Most 'console' output should be changed to INFO, that content isn't intended to be displayed in quiet mode - -No extra output is specified with: +```bash +# Instead of: +echo "Enter passphrase:" +read -r -s passphrase +# Use: +INPUT "Enter passphrase:" -r -s passphrase ``` + +INPUT always prints a blank line before the prompt so the user can easily find it on the console. +The prompt text and `INPUT:` label are always recorded in debug.log for tracing. +All read flags (`-r`, `-s`, `-n N`, etc.) and the variable name are passed through unchanged to `read`. + +Do NOT use INPUT for yes/no confirmation dialogs - use whiptail for those. + +## Output Levels + +Users can choose one of three output levels for console information. +**`/tmp/debug.log` always captures all levels regardless of the chosen output level.** + +* **Quiet** - Minimal console output. STATUS, NOTE, warn and die always appear. INFO is suppressed. + 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. + 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. + +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 | + +debug.log and /dev/kmsg always receive plain text without ANSI codes. + +All console output goes to **`/dev/console`** — the kernel console device, which follows +the `console=` kernel parameter and reaches whatever output the system was configured for +(serial port, framebuffer, BMC console, etc.) without requiring any process setup. +This means callers never need to care about redirections: a caller that does +`2>/tmp/whiptail` or `>/boot/kexec_tree.txt` will not accidentally capture log output. + +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. +INPUT prints a blank line before the prompt so the user can easily find it. + +### 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 | + +Quiet output is specified with: + +```text CONFIG_DEBUG_OUTPUT=n CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n CONFIG_QUIET_MODE=y ``` -## Info +### Info -| Sink | LOG | TRACE | DEBUG | INFO | console | NOTE | warn | -|-------------------------|-----|-------|-------|------|---------|------|------| -| Console (via /dev/kmsg) | | | | Yes | Yes | Yes | Yes | -| /tmp/debug.log | 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 | Info output is enabled with: -``` +```text CONFIG_DEBUG_OUTPUT=n CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n CONFIG_QUIET_MODE=n ``` -## Debug +### Debug -| Sink | LOG | TRACE | DEBUG | INFO | console | NOTE | warn | -|-------------------------|-----|-------|-------|------|---------|------|------| -| Console (via /dev/kmsg) | | Yes | Yes | Yes | Yes | Yes | Yes | -| /tmp/debug.log | 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 | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Debug output is enabled with: -``` +```text CONFIG_DEBUG_OUTPUT=y CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y CONFIG_QUIET_MODE=n diff --git a/initrd/bin/change-time.sh b/initrd/bin/change-time.sh index b5d2a4ffe..af8b61b21 100755 --- a/initrd/bin/change-time.sh +++ b/initrd/bin/change-time.sh @@ -3,28 +3,22 @@ clear -echo "The system time is: $(date "+%Y-%m-%d %H:%M:%S %Z")" -echo -echo "Please enter the current date and time in UTC" -echo "To find the current date and time in UTC, please check https://time.is/UTC" -echo +STATUS "System time: $(date "+%Y-%m-%d %H:%M:%S %Z")" +STATUS "Please enter the current date and time in UTC" +INFO "To find the current UTC time: https://time.is/UTC" get_date () { local field_name min max field_name="$1" min="$2" max="$3" - echo -n "Enter the current $field_name [$min-$max]: " - read -r value - echo + INPUT "Enter the current $field_name [$min-$max]:" -r value #must be a number between $2 and $3 while [[ ! $value =~ ^[0-9]+$ ]] || [[ ${value#0} -lt $min ]] || [[ ${value#0} -gt $max ]]; do - echo "Please try again, it must be a number from $min to $max." - echo -n "Enter the current $field_name [$min-$max]: " - read -r value - echo + warn "Please try again, it must be a number from $min to $max." + INPUT "Enter the current $field_name [$min-$max]:" -r value done # Pad with zeroes to length of maximum value. @@ -56,18 +50,13 @@ enter_time_and_change() } while ! enter_time_and_change; do - echo "Could not set the date to $year-$month-$day $hour:$min:$sec" - read -rp "Try again? [Y/n]: " try_again_confirm + warn "Could not set the date to $year-$month-$day $hour:$min:$sec" + INPUT "Try again? [Y/n]:" -r try_again_confirm if [ "${try_again_confirm^^}" = N ]; then exit 1 fi - echo done hwclock -w -echo "The system date has been sucessfully set to $year-$month-$day $hour:$min:$sec UTC" -echo - -echo "Press Enter to return to the menu" -echo -read -r nothing +STATUS_OK "System date set to $year-$month-$day $hour:$min:$sec UTC" +INPUT "Press Enter to return to the menu" diff --git a/initrd/bin/config-gui.sh b/initrd/bin/config-gui.sh index b741bf71d..8831772a8 100755 --- a/initrd/bin/config-gui.sh +++ b/initrd/bin/config-gui.sh @@ -245,15 +245,10 @@ while true; do "D") CURRENT_OPTION="$(load_config_value CONFIG_ROOT_DIRLIST)" - # Separate from prior prompt history on the terminal with two blanks - echo -e "\n" - if [ -n "$CURRENT_OPTION" ]; then - echo -e "The current list of directories to hash is $CURRENT_OPTION" + INFO "The current list of directories to hash is $CURRENT_OPTION" fi - echo -e "Enter the new list of directories separated by spaces:" - echo -e "(Press enter with the list empty to cancel)" - read -r NEW_CONFIG_ROOT_DIRLIST + INPUT "Enter the new list of directories separated by spaces (empty to cancel):" -r NEW_CONFIG_ROOT_DIRLIST # strip any leading forward slashes NEW_CONFIG_ROOT_DIRLIST=$(echo $NEW_CONFIG_ROOT_DIRLIST | sed -e 's/^\///;s/ \// /g') @@ -611,14 +606,9 @@ while true; do elif [[ "$choice" == *.map ]]; then SELECTED_KEYMAP="$BROWSE_DIR/$choice" load_keymap "$SELECTED_KEYMAP" - echo - echo "------------------------------------------------------------" - echo "Keymap loaded: $SELECTED_KEYMAP" - echo - echo "You can now test your keyboard layout in this shell." - echo "Press Enter when done testing to continue..." - echo "------------------------------------------------------------" - read -p $'\nTest your keymap now. Press Enter to continue:\n' dummy + STATUS_OK "Keymap loaded: $SELECTED_KEYMAP" + INFO "You can now test your keyboard layout in this shell." + INPUT "Test your keymap now. Press Enter to continue:" dummy if whiptail --title "Keep this keymap?" \ --yesno "Do you want to use this keymap?\n\n$SELECTED_KEYMAP" 0 70; then set_user_config "CONFIG_KEYBOARD_KEYMAP" "$SELECTED_KEYMAP" diff --git a/initrd/bin/flash.sh b/initrd/bin/flash.sh index c5389a11a..1cfb6d239 100755 --- a/initrd/bin/flash.sh +++ b/initrd/bin/flash.sh @@ -6,8 +6,6 @@ set -e -o pipefail . /etc/functions . /tmp/config -echo - TRACE_FUNC case "$CONFIG_FLASH_OPTIONS" in @@ -16,7 +14,7 @@ case "$CONFIG_FLASH_OPTIONS" in ;; * ) DEBUG "Flash options detected: $CONFIG_FLASH_OPTIONS" - echo "Board $CONFIG_BOARD detected with flash options configured. Continuing..." + STATUS "Board $CONFIG_BOARD detected with flash options configured" ;; esac @@ -34,13 +32,13 @@ flash_rom() { fi # persist serial number from CBFS if cbfs.sh -r serial_number > /tmp/serial 2>/dev/null; then - echo "Persisting system serial" + STATUS "Persisting system serial" cbfs.sh -o /tmp/${CONFIG_BOARD}.rom -d serial_number 2>/dev/null || true cbfs.sh -o /tmp/${CONFIG_BOARD}.rom -a serial_number -f /tmp/serial fi # persist PCHSTRP9 from flash descriptor if [ "$CONFIG_BOARD" = "librem_l1um" ]; then - echo "Persisting PCHSTRP9" + STATUS "Persisting PCHSTRP9" $CONFIG_FLASH_OPTIONS -r /tmp/ifd.bin --ifd -i fd >/dev/null 2>&1 \ || die "Failed to read flash descriptor" dd if=/tmp/ifd.bin bs=1 count=4 skip=292 of=/tmp/pchstrp9.bin >/dev/null 2>&1 @@ -82,7 +80,7 @@ if [ "$READ" -eq 0 ] && [ "${ROM##*.}" = tgz ]; then die "Provided tgz image did not pass hash verification" fi - echo "Reading current flash and building an update image" + STATUS "Reading current flash and building update image" $CONFIG_FLASH_OPTIONS -r /tmp/flash.sh.bak \ || recovery "Read of flash has failed" diff --git a/initrd/bin/generic-init b/initrd/bin/generic-init index a3b9f34e6..79f5455b7 100755 --- a/initrd/bin/generic-init +++ b/initrd/bin/generic-init @@ -32,11 +32,9 @@ while true; do fi if [ "$totp_confirm" = "n" ]; then - echo "" - echo "To correct clock drift: 'date -s HH:MM:SS'" - echo "and save it to the RTC: 'hwclock -w'" - echo "then reboot and try again" - echo "" + INFO "To correct clock drift: date -s HH:MM:SS" + INFO "and save it to the RTC: hwclock -w" + INFO "then reboot and try again" recovery "TOTP mismatch" fi diff --git a/initrd/bin/gpg-gui.sh b/initrd/bin/gpg-gui.sh index 738de34ab..c2c42d2a4 100755 --- a/initrd/bin/gpg-gui.sh +++ b/initrd/bin/gpg-gui.sh @@ -226,14 +226,9 @@ while true; do ;; "g" ) confirm_gpg_card - echo -e "\n\n\n\n" - echo "********************************************************************************" - echo "*" - echo "* INSTRUCTIONS:" - echo "* Type 'admin' and then 'generate' and follow the prompts to generate a GPG key." - echo "* Type 'quit' once you have generated the key to exit GPG." - echo "*" - echo "********************************************************************************" + 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" gpg --card-edit > /tmp/gpg_card_edit_output if [ $? -eq 0 ]; then gpg_post_gen_mgmt diff --git a/initrd/bin/gui-init b/initrd/bin/gui-init index ed32a6143..cc4d5e6b1 100755 --- a/initrd/bin/gui-init +++ b/initrd/bin/gui-init @@ -10,11 +10,29 @@ export BG_COLOR_MAIN_MENU="normal" . /etc/luks-functions . /tmp/config +# Detect the terminal this gui-init session is running on. The user +# interacting with gui-init (via whiptail) is the source of truth for the +# "active" terminal — prompts, GPG/pinentry, and input all go to/from there. +# $(tty) works here because cttyhack (exec'd by /init) has already replaced +# fd0/1/2 with the correct console device before launching this script. +# Fall back to /sys/class/tty/console/active (last entry = preferred console, +# same source used by systemd and busybox cttyhack) when tty is unavailable. +if ! HEADS_TTY=$(tty 2>/dev/null); 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}" +fi +unset _active _dev +export HEADS_TTY +export GPG_TTY="$HEADS_TTY" + # skip_to_menu is set if the user selects "continue to the main menu" from any # error, so we will indeed go to the main menu even if other errors occur. It's # reset when we reach the main menu so the user can retry from the main menu and # # see errors again. skip_to_menu="false" +INTEGRITY_GATE_REQUIRED="n" mount_boot() { TRACE_FUNC @@ -74,7 +92,7 @@ verify_global_hashes() { return 0 elif [[ ! -f "$TMP_HASH_FILE" || ! -f "$TMP_TREE_FILE" ]]; then if (whiptail_error --title 'ERROR: Missing File!' \ - --yesno "One of the files containing integrity information for /boot is missing!\n\nIf you are setting up heads for the first time or upgrading from an\nolder version, select Yes to create the missing files.\n\nOtherwise this could indicate a compromise and you should select No to\nreturn to the main menu.\n\nWould you like to create the missing files now?" 0 80); then + --yesno "$(printf '%b' "One of the files containing integrity information for /boot is missing!\n\nIf you are setting up heads for the first time or upgrading from an older version, select Yes to create the missing files.\n\nOtherwise this could indicate a compromise and you should select No to return to the main menu.\n\nWould you like to create the missing files now?" | fold -s -w 76)" 0 80); then if update_checksums; then BG_COLOR_MAIN_MENU="normal" return 0 @@ -115,70 +133,201 @@ verify_global_hashes() { less /tmp/hash_output_mismatches #move outdated hash mismatch list mv /tmp/hash_output_mismatches /tmp/hash_output_mismatch_old - TEXT="Would you like to update your checksums now?" + TEXT="${CHANGED_FILES_COUNT} files failed the verification process.\n\nThis could indicate a compromise!\n\nWould you like to investigate discrepancies or update your checksums now?" else - TEXT="The following files failed the verification process:\n\n${CHANGED_FILES}\n\nThis could indicate a compromise!\n\nWould you like to update your checksums now?" + TEXT="The following files failed the verification process:\n\n${CHANGED_FILES}\n\nThis could indicate a compromise!\n\nWould you like to investigate discrepancies or update your checksums now?" fi fi - if (whiptail_error --title 'ERROR: Boot Hash Mismatch' --yesno "$TEXT" 0 80); then - if update_checksums; then - BG_COLOR_MAIN_MENU="normal" - return 0 - else - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 - fi - fi - BG_COLOR_MAIN_MENU="error" - return 1 + local menu_text + menu_text="$(printf '%b' "$TEXT" | fold -s -w 76)" + while true; do + TRACE_FUNC + whiptail_error --title 'ERROR: Boot Hash Mismatch' \ + --menu "$menu_text\n\nChoose an action:" 0 80 3 \ + 'i' ' Investigate discrepancies -->' \ + 'u' ' Update checksums now' \ + 'm' ' Return to main menu' \ + 2>/tmp/whiptail || { + BG_COLOR_MAIN_MENU="error" + return 1 + } + + option=$(cat /tmp/whiptail) + case "$option" in + i) + investigate_integrity_discrepancies + ;; + u) + if update_checksums; then + BG_COLOR_MAIN_MENU="normal" + return 0 + else + whiptail_error --title 'ERROR' \ + --msgbox "Failed to update checksums / sign default config" 0 80 + fi + ;; + m | *) + BG_COLOR_MAIN_MENU="error" + return 1 + ;; + esac + done fi } prompt_update_checksums() { TRACE_FUNC + # Signing /boot with -r increments the TPM rollback counter. If the counter + # is broken or absent (tpm_reset_required), the increment will fail and die. + # The user must reset the TPM first; that flow re-creates the counter. + if [ "$CONFIG_TPM" = "y" ] && tpm_reset_required; then + whiptail_error --title 'TPM Reset Required' \ + --msgbox "Cannot sign /boot: TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM), then update checksums." 0 80 + return 1 + fi if (whiptail_warning --title 'Update Checksums and sign all files in /boot' \ --yesno "You have chosen to update the checksums and sign all of the files in /boot.\n\nThis means that you trust that these files have not been tampered with.\n\nYou will need your GPG key available, and this change will modify your disk.\n\nDo you want to continue?" 0 80); then - if ! update_checksums; then + if update_checksums; then + return 0 + else whiptail_error --title 'ERROR' \ --msgbox "Failed to update checksums / sign default config" 0 80 + return 1 + fi + fi + return 1 +} + +gate_reseal_with_integrity_report() { + TRACE_FUNC + local token_ok="y" + if tpm_reset_required; then + debug_tpm_reset_required_state + whiptail_error --title 'ERROR: TPM Reset Required' \ + --msgbox "TPM state is inconsistent for sealing/unsealing operations.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." 0 80 + return 1 + fi + + if [ "$INTEGRITY_GATE_REQUIRED" != "y" ]; then + DEBUG "Skipping integrity gate: no TOTP/HOTP failure context" + return 0 + fi + + INTEGRITY_REPORT_HASH_STATE="UNKNOWN" + 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" + if ! investigate_integrity_discrepancies; then + DEBUG "investigation indicated problem, aborting gate" + return 1 + fi + + DEBUG "gate_reseal_with_integrity_report: about to verify detached signature" + 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" + local sig_fail_msg + sig_fail_msg="$(printf '%b' "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." | fold -s -w 76)" + whiptail_error --title 'ERROR: Signature Verification Failed' \ + --msgbox "$sig_fail_msg" 0 80 + return 1 fi + else + DEBUG "gate_reseal_with_integrity_report: integrity is OK, skipping investigation and detached signature verification" + fi + + if [ -x /bin/hotp_verification ]; then + token_ok="n" + while [ "$token_ok" != "y" ]; do + enable_usb + if hotp_verification info >/dev/null 2>&1; then + token_ok="y" + break + fi + if ! whiptail_warning --title "USB Security Dongle Required" \ + --yes-button "Retry" --no-button "Abort" \ + --yesno "Your USB security dongle must be present before sealing new secrets.\n\nInsert the dongle and choose Retry, or Abort." 0 80; then + return 1 + fi + done fi + + if ! whiptail_warning --title 'Integrity Gate Passed' \ + --yesno "Integrity checks completed.\n\nProceed with TOTP/HOTP reseal action?" 0 80; then + return 1 + fi + INTEGRITY_GATE_REQUIRED="n" + return 0 } generate_totp_hotp() { TRACE_FUNC tpm_owner_password="$1" # May be empty, will prompt if needed and empty + if [ "$CONFIG_TPM" = "y" ] && tpm_reset_required; then + debug_tpm_reset_required_state + whiptail_error --title 'ERROR: TPM Reset Required' \ + --msgbox "Cannot generate a new TPM-backed TOTP/HOTP secret while TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." 0 80 + return 1 + fi 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 - echo "Generating new HOTP secret" + STATUS "Generating new HOTP secret" /bin/seal-hotpkey || die "Failed to generate HOTP secret" - elif echo -e "Generating new TOTP secret...\n\n" && /bin/seal-totp "$BOARD_NAME" "$tpm_owner_password"; then - echo + elif STATUS "Generating new TOTP secret" && /bin/seal-totp "$BOARD_NAME" "$tpm_owner_password"; 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 - echo "Once you have scanned the QR code, hit Enter to configure your HOTP USB Security dongle (e.g. Librem Key or Nitrokey)" - read + INPUT "Once you have scanned the QR code, press Enter to configure your HOTP USB Security dongle (e.g. Librem Key or Nitrokey)" fi TRACE_FUNC /bin/seal-hotpkey || die "Failed to generate HOTP secret" else if [ "$CONFIG_TOTP_SKIP_QRCODE" != y ]; then - echo "Once you have scanned the QR code, hit Enter to continue" - read + INPUT "Once you have scanned the QR code, press Enter to continue" fi fi - # clear screen - printf "\033c" + clear else - warn "Unsealing TOTP/HOTP secret from previous sealed measurements failed" - warn 'Try "Generate new HOTP/TOTP secret" option if you updated firmware content' + # seal-totp already printed an explanatory error (e.g. missing + # primary handle) and guided the user to reset the TPM. Don't add + # confusing generic warnings here, just propagate failure. + return 1 fi } +prompt_missing_gpg_key_action() { + TRACE_FUNC + whiptail_error --title "ERROR: GPG signing key unavailable" \ + --menu "Cannot sign /boot because no private GPG signing key is available (card not inserted, wiped, or key not set up).\n\nIf you have the correct signing card, insert it and retry.\n\nHow would you like to proceed?" 0 80 4 \ + 'r' ' Retry (after connecting the correct signing card)' \ + 'F' ' OEM Factory Reset / Re-Ownership' \ + 'm' ' Return to main menu' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + r) + return 0 + ;; + F) + oem-factory-reset + ;; + x) + recovery "User requested recovery shell" + ;; + m | *) + return 1 + ;; + esac +} + update_totp() { TRACE_FUNC # update the TOTP code @@ -187,38 +336,59 @@ update_totp() { if [ "$CONFIG_TPM" != "y" ]; then TOTP="NO TPM" else - TOTP=$(unseal-totp) + TOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-totp) if [ $? -ne 0 ]; then + local totp_menu_text + INTEGRITY_GATE_REQUIRED="y" BG_COLOR_MAIN_MENU="error" if [ "$skip_to_menu" = "true" ]; then return 1 # Already asked to skip to menu from a prior error fi - DEBUG "CONFIG_TPM: $CONFIG_TPM" - DEBUG "CONFIG_TPM2_TOOLS: $CONFIG_TPM2_TOOLS" - DEBUG "Show PCRs" DEBUG "$(pcrs)" + totp_menu_text=$( + cat </tmp/whiptail || recovery "GUI menu failed" option=$(cat /tmp/whiptail) case "$option" in g) - if (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ + if tpm_reset_required; then + debug_tpm_reset_required_state + whiptail_error --title 'ERROR: TPM Reset Required' \ + --msgbox "Cannot generate a new TPM-backed TOTP/HOTP secret while TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." 0 80 + return 1 + elif gate_reseal_with_integrity_report && (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then - generate_totp_hotp && update_totp && BG_COLOR_MAIN_MENU="normal" && reseal_tpm_disk_decryption_key + if generate_totp_hotp; then + update_totp || true + BG_COLOR_MAIN_MENU="normal" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi fi ;; i) @@ -226,12 +396,16 @@ update_totp() { return 1 ;; p) - reset_tpm && update_totp && BG_COLOR_MAIN_MENU="normal" && reseal_tpm_disk_decryption_key + if gate_reseal_with_integrity_report && reset_tpm && update_totp && BG_COLOR_MAIN_MENU="normal"; then + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi ;; x) recovery "User requested recovery shell" ;; esac + else + INTEGRITY_GATE_REQUIRED="n" fi fi } @@ -253,7 +427,7 @@ update_hotp() { return fi fi - HOTP=$(unseal-hotp) + HOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-hotp) # Don't output HOTP codes to screen, so as to make replay attacks harder hotp_verification check "$HOTP" case "$?" in @@ -275,11 +449,11 @@ update_hotp() { fi if [[ "$HOTP" = "Invalid code" ]]; then - #Do not propose to generate a new secret if there is no /boot/kexec_hotp_counter - # tpm unseal succeeded: so the sealed secret is correct: we should propose to reset TPM if not already - # Here: the OS was most probably reinstalled since TPM can still unseal the secret + INTEGRITY_GATE_REQUIRED="y" + local hotp_error_msg + hotp_error_msg="$(printf '%b' "ERROR: $CONFIG_BRAND_NAME couldn't validate the HOTP code.\n\nIf you just reflashed your BIOS, you should generate a new TOTP/HOTP secret.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" | fold -s -w 76)" whiptail_error --title "ERROR: HOTP Validation Failed!" \ - --menu "ERROR: $CONFIG_BRAND_NAME couldn't validate the HOTP code.\n\nIf you just reflashed your BIOS, you should generate a new TOTP/HOTP secret.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" 0 80 4 \ + --menu "$hotp_error_msg" 0 80 3 \ 'g' ' Generate new TOTP/HOTP secret' \ 'i' ' Ignore error and continue to main menu' \ 'x' ' Exit to recovery shell' \ @@ -288,9 +462,13 @@ update_hotp() { option=$(cat /tmp/whiptail) case "$option" in g) - if (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ + if gate_reseal_with_integrity_report && (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then - generate_totp_hotp && BG_COLOR_MAIN_MENU="normal" && reseal_tpm_disk_decryption_key + if generate_totp_hotp; then + update_totp || true + BG_COLOR_MAIN_MENU="normal" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi fi ;; i) @@ -300,6 +478,8 @@ update_hotp() { recovery "User requested recovery shell" ;; esac + else + INTEGRITY_GATE_REQUIRED="n" fi } @@ -339,8 +519,10 @@ check_gpg_key() { if [ "$skip_to_menu" = "true" ]; then return 1 # Already asked to skip to menu from a prior error fi + local gpg_error_msg + gpg_error_msg="$(printf '%b' "ERROR: $CONFIG_BRAND_NAME couldn't find any GPG keys in your keyring.\n\nIf this is the first time the system has booted, you should add a public GPG key to the BIOS now.\n\nIf you just reflashed a new BIOS, you'll need to add at least one public key to the keyring.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" | fold -s -w 76)" whiptail_error --title "ERROR: GPG keyring empty!" \ - --menu "ERROR: $CONFIG_BRAND_NAME couldn't find any GPG keys in your keyring.\n\nIf this is the first time the system has booted,\nyou should add a public GPG key to the BIOS now.\n\nIf you just reflashed a new BIOS, you'll need to add at least one\npublic key to the keyring.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" 0 80 4 \ + --menu "$gpg_error_msg" 0 80 4 \ 'g' ' Add a GPG key to the running BIOS' \ 'F' ' OEM Factory Reset / Re-Ownership' \ 'i' ' Ignore error and continue to main menu' \ @@ -369,9 +551,9 @@ check_gpg_key() { prompt_auto_default_boot() { TRACE_FUNC - echo -e "\nHOTP verification success\n\n" + STATUS_OK "HOTP verification success" if pause_automatic_boot; then - echo -e "\n\nAttempting default boot...\n\n" + STATUS "Attempting default boot" attempt_default_boot fi } @@ -414,6 +596,7 @@ show_options_menu() { --menu "" 0 80 10 \ 'b' ' Boot Options -->' \ 't' ' TPM/TOTP/HOTP Options -->' \ + 'i' ' Investigate integrity discrepancies -->' \ 'h' ' Change system time' \ 'u' ' Update checksums and sign all files in /boot' \ 'c' ' Change configuration settings -->' \ @@ -435,6 +618,9 @@ show_options_menu() { t) show_tpm_totp_hotp_options_menu ;; + i) + investigate_integrity_discrepancies + ;; h) change-time.sh ;; @@ -510,10 +696,14 @@ show_tpm_totp_hotp_options_menu() { option=$(cat /tmp/whiptail) case "$option" in g) - generate_totp_hotp && reseal_tpm_disk_decryption_key + if gate_reseal_with_integrity_report && generate_totp_hotp; then + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi ;; r) - reset_tpm && reseal_tpm_disk_decryption_key + if gate_reseal_with_integrity_report && reset_tpm; then + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi ;; t) prompt_totp_mismatch @@ -537,9 +727,7 @@ reset_tpm() { --yesno "This will clear the TPM and replace its Owner password with a new one!\n\nDo you want to proceed?" 0 80); then if ! prompt_new_owner_password; then - echo "Press Enter to return to the menu..." - read - echo + INPUT "Press Enter to return to the menu..." return 1 fi @@ -550,7 +738,7 @@ reset_tpm() { mount -o rw,remount /boot #TODO: this is really problematic, we should really remove the primary handle hash - INFO "Removing rollback and primary handle hashes under /boot" + STATUS "Removing rollback and primary handle hashes under /boot" DEBUG "Removing /boot/kexec_rollback.txt and /boot/kexec_primhdl_hash.txt" rm -f /boot/kexec_rollback.txt @@ -566,7 +754,7 @@ reset_tpm() { DEBUG "TPM_COUNTER: $TPM_COUNTER" #TPM_COUNTER can be empty - increment_tpm_counter $TPM_COUNTER>/dev/null 2>&1 || + increment_tpm_counter "$TPM_COUNTER" "$tpm_owner_password" || die "Unable to increment tpm counter" DO_WITH_DEBUG sha256sum /tmp/counter-$TPM_COUNTER >/boot/kexec_rollback.txt || @@ -574,21 +762,39 @@ reset_tpm() { TRACE_FUNC # As a countermeasure for existing primary handle hash, we will now force sign /boot without it - if (whiptail --title 'TPM Reset Successfully' \ - --yesno "Would you like to update the checksums and sign all of the files in /boot?\n\nYou will need your GPG key to continue and this will modify your disk.\n\nOtherwise the system will reboot immediately." 0 80); then - if ! update_checksums; then - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 + # USB is already initialized at startup; run gpg --card-status to populate key stub. + wait_for_gpg_card || true + 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 + return 1 + fi + break fi - else - warn "TPM reset successful, but user chose not to update+sign /boot checksums. Rebooting" - reboot - fi + done mount -o ro,remount /boot - generate_totp_hotp "$tpm_owner_password" + # Reset completed and reseal prerequisites were rebuilt. + # Clear stale preflight marker before generating fresh TOTP/HOTP. + clear_tpm_reset_required + + if ! generate_totp_hotp "$tpm_owner_password"; then + return 1 + 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 - echo "Returning to the main menu" + STATUS "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 @@ -658,15 +864,95 @@ else mount_boot fi +# Fail early on rollback-counter inconsistencies before presenting TOTP/HOTP +# recovery prompts. This avoids guiding users into reseal flows when TPM +# rollback state is already invalid. +rollback_preflight_failed="n" +if ! preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="y" + BG_COLOR_MAIN_MENU="error" + preflight_error_msg="$(cat /tmp/rollback_preflight_error 2>/dev/null)" + if [ -z "$preflight_error_msg" ]; then + preflight_error_msg="TPM rollback counter state could not be validated." + fi + [ -n "$preflight_error_msg" ] && DEBUG "Rollback preflight failure: $preflight_error_msg" + + # Keep the on-screen text concise and user-focused; full raw reason stays in DEBUG logs. + preflight_reason="Stored TPM rollback state is invalid for this system." + if echo "$preflight_error_msg" | grep -qi "missing or unreadable\|initialized but rollback state cannot be validated"; then + preflight_reason="Stored TPM rollback metadata is missing for an initialized system (possible TPM reset/replacement)." + elif echo "$preflight_error_msg" | grep -qi "cannot be read"; then + preflight_reason="Stored TPM rollback metadata cannot be read." + elif echo "$preflight_error_msg" | grep -qi "empty\|no writable\|ownerwrite-only"; then + preflight_reason="Stored TPM rollback metadata does not match current TPM state." + fi + + preflight_menu_text=$( + cat <' \ + 'o' ' OEM Factory Reset / Re-Ownership -->' \ + 't' ' Reset the TPM' \ + 'm' ' Continue to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + i) + report_integrity_measurements + ;; + o) + oem-factory-reset + if preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="n" + BG_COLOR_MAIN_MENU="normal" + fi + ;; + t) + if reset_tpm && preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="n" + BG_COLOR_MAIN_MENU="normal" + fi + ;; + m | *) + break + ;; + esac + if [ "$rollback_preflight_failed" = "y" ]; then + preflight_error_msg="$(cat /tmp/rollback_preflight_error 2>/dev/null)" + [ -n "$preflight_error_msg" ] && DEBUG "Rollback preflight failure: $preflight_error_msg" + fi + done +fi + # detect whether any GPG keys exist in the keyring, if not, initialize that first -check_gpg_key -# Even if GPG init fails, still try to update TOTP/HOTP so the main menu can -# show the correct status. -update_totp -update_hotp - -if [ "$HOTP" = "Success" -a -n "$CONFIG_AUTO_BOOT_TIMEOUT" ]; then - prompt_auto_default_boot +if [ "$rollback_preflight_failed" != "y" ]; then + check_gpg_key + # Even if GPG init fails, still try to update TOTP/HOTP so the main menu can + # show the correct status. + update_totp && update_hotp + + if [ "$HOTP" = "Success" -a -n "$CONFIG_AUTO_BOOT_TIMEOUT" ]; then + prompt_auto_default_boot + fi fi while true; do diff --git a/initrd/bin/gui-init-basic b/initrd/bin/gui-init-basic index af9da581e..557319f52 100755 --- a/initrd/bin/gui-init-basic +++ b/initrd/bin/gui-init-basic @@ -9,6 +9,18 @@ export BG_COLOR_MAIN_MENU="normal" . /etc/gui_functions . /tmp/config +# Detect the terminal this gui-init-basic session is running on. +# Same logic as gui-init — see comments there. +if ! HEADS_TTY=$(tty 2>/dev/null); 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}" +fi +unset _active _dev +export HEADS_TTY +export GPG_TTY="$HEADS_TTY" + # skip_to_menu is set if the user selects "continue to the main menu" from any # error, so we will indeed go to the main menu even if other errors occur. It's # reset when we reach the main menu so the user can retry from the main menu and @@ -64,9 +76,8 @@ mount_boot() prompt_auto_default_boot() { TRACE_FUNC - echo -e "\n\n" if pause_automatic_boot; then - echo -e "\n\nAttempting default boot...\n\n" + STATUS "Attempting default boot" attempt_default_boot fi } diff --git a/initrd/bin/kexec-boot b/initrd/bin/kexec-boot index fa37ebf99..9b684a6ab 100755 --- a/initrd/bin/kexec-boot +++ b/initrd/bin/kexec-boot @@ -92,7 +92,7 @@ do kexeccmd="$kexeccmd -l $filepath" DEBUG "kexeccmd= $kexeccmd" else - DEBUG "unknown kexectype!!!!" + DEBUG "unknown kexectype" kexeccmd="$kexeccmd -l $filepath" fi fi @@ -149,8 +149,8 @@ fi if [ "$dryrun" = "y" ]; then exit 0; fi -echo "Loading the new kernel:" -echo "$kexeccmd" +STATUS "Loading the new kernel" +DEBUG "kexec command: $kexeccmd" # DO_WITH_DEBUG captures the debug output from stderr to the log, we don't need # it on the console as well DO_WITH_DEBUG eval "$kexeccmd" 2>/dev/null \ @@ -158,8 +158,7 @@ DO_WITH_DEBUG eval "$kexeccmd" 2>/dev/null \ if [ "$CONFIG_DEBUG_OUTPUT" = "y" ];then #Ask user if they want to continue booting without echoing back the input (-s) - read -s -n 1 -p "[DEBUG] Continue booting? [Y/n]: " debug_boot_confirm - echo + INPUT "[DEBUG] Continue booting? [Y/n]:" -s -n 1 debug_boot_confirm if [ "${debug_boot_confirm^^}" = N ]; then # abort die "Boot aborted" @@ -174,5 +173,10 @@ if [ -x /bin/io386 -a "$CONFIG_FINALIZE_PLATFORM_LOCKING" = "y" ]; then lock_chip fi -echo "Starting the new kernel" +if [ "$CONFIG_BRAND_NAME" = "Heads" ]; then + STATUS_OK "Heads firmware job done - handing off to your OS. Consider donating: https://opencollective.com/insurgo" + qrenc "https://opencollective.com/insurgo" +else + STATUS_OK "$CONFIG_BRAND_NAME firmware job done - starting your OS" +fi exec kexec -e diff --git a/initrd/bin/kexec-insert-key b/initrd/bin/kexec-insert-key index ff95c1943..eec46a583 100755 --- a/initrd/bin/kexec-insert-key +++ b/initrd/bin/kexec-insert-key @@ -24,7 +24,7 @@ if [ -r "$TMP_KEY_LVM" ]; then if [ -z "$TMP_KEY_LVM" ]; then die "No LVM volume group defined for activation" fi - lvm vgchange -a y $VOLUME_GROUP || + run_lvm vgchange -a y $VOLUME_GROUP || die "$VOLUME_GROUP: unable to activate volume group" fi @@ -47,8 +47,7 @@ if [ -e /boot/kexec_lukshdr_hash.txt ] && [ -e /tmp/luksDump.txt ]; then exit 1 else #LUKS header hash part of detached signed hash digest matches - echo "+++ Encrypted disk keys have not been changed since sealed in TPM Disk Unlock Key" - #TODO: remove "+++" with boot info helper when added, same with "!!!" currently for info. + STATUS_OK "Encrypted disk keys have not changed since sealed in TPM Disk Unlock Key" fi else warn "Could not check for tampering of Encrypted disk keys" @@ -60,8 +59,7 @@ fi unseal_failed="n" if ! kexec-unseal-key "$INITRD_DIR/secret.key"; then unseal_failed="y" - echo - echo "!!! Failed to unseal the TPM LUKS Disk Unlock Key" + warn "Failed to unseal the TPM LUKS Disk Unlock Key" fi # Override PCR 4 so that user can't read the key @@ -73,22 +71,17 @@ tpmr extend -ix 4 -ic generic || # Check to continue if [ "$unseal_failed" = "y" ]; then confirm_boot="n" - read \ - -n 1 \ - -p "Do you wish to boot and use the LUKS Disk Recovery Key? [Y/n] " \ - confirm_boot - echo + INPUT "Do you wish to boot and use the LUKS Disk Recovery Key? [Y/n]:" -n 1 confirm_boot if [ "$confirm_boot" != 'y' \ -a "$confirm_boot" != 'Y' \ -a -n "$confirm_boot" ] \ ; then - die "!!! Aborting boot due to failure to unseal TPM Disk Unlock Key" + die "Aborting boot due to failure to unseal TPM Disk Unlock Key" fi fi -echo -echo '+++ Building initrd' +STATUS "Building initrd" # pad the initramfs (dracut doesn't pad the last gz blob) # without this the kernel init/initramfs.c fails to read # the subsequent uncompressed/compressed cpio @@ -98,8 +91,8 @@ dd if="$INITRD" of="$SECRET_CPIO" bs=512 conv=sync > /dev/null 2>&1 || if [ "$unseal_failed" = "n" ]; then # kexec-save-default might have created crypttab overrides to be injected in initramfs through additional cpio if [ -r "$bootdir/kexec_initrd_crypttab_overrides.txt" ]; then - echo "+++ $bootdir/kexec_initrd_crypttab_overrides.txt found..." - echo "+++ Preparing initramfs crypttab overrides as defined under $bootdir/kexec_initrd_crypttab_overrides.txt to be injected through cpio at next kexec call..." + DEBUG "$bootdir/kexec_initrd_crypttab_overrides.txt found" + DEBUG "Preparing initramfs crypttab overrides from $bootdir/kexec_initrd_crypttab_overrides.txt" # kexec-save-default has found crypttab files under initrd and saved them cat "$bootdir/kexec_initrd_crypttab_overrides.txt" | while read line; do crypttab_file=$(echo "$line" | awk -F ':' {'print $1'}) @@ -107,7 +100,7 @@ if [ "$unseal_failed" = "n" ]; then # Replace each initrd crypttab file with modified entry containing /secret.key path mkdir -p "$INITRD_DIR/$(dirname $crypttab_file)" echo "$crypttab_entry" | tee -a "$INITRD_DIR/$crypttab_file" >/dev/null - echo "+++ initramfs's $crypttab_file will be overriden with: $crypttab_entry" + DEBUG "initramfs $crypttab_file will be overridden with: $crypttab_entry" done else # No crypttab files were found under selected default boot option's initrd file @@ -116,10 +109,10 @@ if [ "$unseal_failed" = "n" ]; then for crypttab_file in $crypttab_files; do mkdir -p "$INITRD_DIR/$(dirname $crypttab_file)" # overwrite crypttab to mirror behavior of seal-key - echo "+++ The following $crypttab_file overrides will be passed through concatenated secret/initrd.cpio at kexec call:" + DEBUG "The following $crypttab_file overrides will be injected via cpio at kexec:" for uuid in $(cat "$TMP_KEY_DEVICES" | cut -d\ -f2); do # NOTE: discard operation (TRIM) is activated by default if no crypptab found in initrd - echo "luks-$uuid UUID=$uuid /secret.key luks,discard" | tee -a "$INITRD_DIR/$crypttab_file" + echo "luks-$uuid UUID=$uuid /secret.key luks,discard" >> "$INITRD_DIR/$crypttab_file" done done fi diff --git a/initrd/bin/kexec-iso-init b/initrd/bin/kexec-iso-init index 53856fec8..0047f7677 100755 --- a/initrd/bin/kexec-iso-init +++ b/initrd/bin/kexec-iso-init @@ -11,7 +11,7 @@ MOUNTED_ISO_PATH="$1" ISO_PATH="$2" DEV="$3" -echo '+++ Verifying ISO' +STATUS "Verifying ISO" # Verify the signature on the hashes ISOSIG="$MOUNTED_ISO_PATH.sig" if ! [ -r "$ISOSIG" ]; then @@ -24,10 +24,10 @@ if [ -r "$ISOSIG" ]; then # Signature found, verify it gpgv --homedir=/etc/distro/ "$ISOSIG" "$MOUNTED_ISO_PATH" \ || die 'ISO signature failed' - echo '+++ ISO signature verified' + STATUS_OK "ISO signature verified" else # No signature found, prompt user with warning - echo '+++ WARNING: No signature found for ISO' + warn "No signature found for ISO" if [ -x /bin/whiptail ]; then if ! whiptail_warning --title 'UNSIGNED ISO WARNING' --yesno \ "WARNING: UNSIGNED ISO DETECTED\n\nThe selected ISO file:\n$MOUNTED_ISO_PATH\n\nDoes not have a detached signature (.sig or .asc file).\n\n\nThis means the integrity and authenticity of the ISO cannot be verified.\nBooting unsigned ISOs is potentially unsafe.\n\nDo you want to proceed with booting this unsigned ISO?" \ @@ -35,19 +35,18 @@ else die "Unsigned ISO boot cancelled by user" fi else - echo "WARNING: The selected ISO file does not have a detached signature" - echo "This means the integrity and authenticity cannot be verified" - echo "Booting unsigned ISOs is potentially unsafe" - read -n1 -p "Do you want to proceed anyway? (y/N): " response - echo + warn "The selected ISO file does not have a detached signature" + warn "Integrity and authenticity of the ISO cannot be verified" + warn "Booting unsigned ISOs is potentially unsafe" + INPUT "Do you want to proceed anyway? (y/N):" -n 1 response if [ "$response" != "y" ] && [ "$response" != "Y" ]; then die "Unsigned ISO boot cancelled by user" fi fi - echo '+++ Proceeding with unsigned ISO boot' + NOTE "Proceeding with unsigned ISO boot" fi -echo '+++ Mounting ISO and booting' +STATUS "Mounting ISO and booting" mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot \ || die '$MOUNTED_ISO_PATH: Unable to mount /boot' @@ -63,14 +62,14 @@ if [ -r $ADD_FILE ]; then NEW_ADD=`cat $ADD_FILE` ADD=$(eval "echo \"$NEW_ADD\"") fi -echo "+++ Overriding standard ISO kernel arguments with additions: $ADD" +DEBUG "Overriding ISO kernel arguments with additions: $ADD" REMOVE_FILE=/tmp/kexec/kexec_iso_remove.txt if [ -r $REMOVE_FILE ]; then NEW_REMOVE=`cat $REMOVE_FILE` REMOVE=$(eval "echo \"$NEW_REMOVE\"") fi -echo "+++ Overriding standard ISO kernel arguments with suppressions: $REMOVE" +DEBUG "Overriding ISO kernel arguments with suppressions: $REMOVE" # Call kexec and indicate that hashes have been verified DO_WITH_DEBUG kexec-select-boot -b /boot -d /media -p "$paramsdir" \ diff --git a/initrd/bin/kexec-save-default b/initrd/bin/kexec-save-default index 32ac305ab..88b70c32a 100755 --- a/initrd/bin/kexec-save-default +++ b/initrd/bin/kexec-save-default @@ -38,7 +38,7 @@ PRIMHASH_FILE="$paramsdir/kexec_primhdl_hash.txt" KEY_DEVICES="$paramsdir/kexec_key_devices.txt" KEY_LVM="$paramsdir/kexec_key_lvm.txt" -lvm_suggest=$(lvm vgscan 2>/dev/null | awk -F '"' {'print $1'} | tail -n +2) +lvm_suggest=$(run_lvm vgscan 2>/dev/null | awk -F '"' {'print $1'} | tail -n +2) num_lvm=$(echo "$lvm_suggest" | wc -l) if [ "$num_lvm" -eq 1 ] && [ -n "$lvm_suggest" ]; then lvm_volume_group="$lvm_suggest" @@ -93,9 +93,7 @@ prompt_for_existing_encrypted_lvms_or_disks() { while [ $selected_lvms_not_existing -ne 0 ]; do { # Read the user input and store it in a variable - read \ - -p "Encrypted LVMs? (choose between/all: $lvm_suggest): " \ - key_lvms + INPUT "Encrypted LVMs? (choose between/all: $lvm_suggest):" -r key_lvms # Split the user input by spaces and add each element to the array IFS=' ' read -r -a key_lvms_array <<<"$key_lvms" @@ -117,10 +115,10 @@ prompt_for_existing_encrypted_lvms_or_disks() { } done elif [ "$num_lvms" -eq 1 ]; then - echo "Single Encrypted LVM found at $lvm_suggest." + INFO "Single Encrypted LVM found at $lvm_suggest" key_lvms=$lvm_suggest else - echo "No encrypted LVMs found." + DEBUG "No encrypted LVMs found" fi # Create an associative array to store the suggested devices and their paths @@ -142,9 +140,7 @@ prompt_for_existing_encrypted_lvms_or_disks() { while [ $selected_luksdevs_not_existing -ne 0 ]; do { # Read the user input and store it in a variable - read \ - -p "Encrypted devices? (choose between/all: $devices_suggest): " \ - key_devices + INPUT "Encrypted devices? (choose between/all: $devices_suggest):" -r key_devices # Split the user input by spaces and add each element to the array IFS=' ' read -r -a key_devices_array <<<"$key_devices" @@ -166,10 +162,10 @@ prompt_for_existing_encrypted_lvms_or_disks() { } done elif [ "$num_devices" -eq 1 ]; then - echo "Single Encrypted Disk found at $devices_suggest." + INFO "Single Encrypted Disk found at $devices_suggest" key_devices=$devices_suggest else - echo "No encrypted devices found." + DEBUG "No encrypted devices found" fi DEBUG "Multiple LUKS devices selected: $key_devices" @@ -193,11 +189,7 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ #check if $KEY_DEVICES file exists and is not empty if [ -r "$KEY_DEVICES" ] && [ -s "$KEY_DEVICES" ]; then DEBUG "LUKS TPM Disk Unlock Key was previously set up from $KEY_DEVICES" - read \ - -n 1 \ - -p "Do you want to reseal a Disk Unlock Key in the TPM [y/N]: " \ - change_key_confirm - echo + INPUT "Do you want to reseal a Disk Unlock Key in the TPM [y/N]:" -n 1 change_key_confirm if [ "$change_key_confirm" = "y" \ -o "$change_key_confirm" = "Y" ]; then @@ -219,12 +211,7 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ fi else DEBUG "No previous LUKS TPM Disk Unlock Key was set up, confirming to add a Disk Unlock Key (DUK) to the TPM" - read \ - -n 1 \ - -p "Do you wish to add a disk encryption key to the TPM [y/N]: " \ - add_key_confirm - #TODO: still not convinced: disk encryption key? decryption key? everywhere TPM Disk Unlock Key. Confusing even more? - echo + INPUT "Do you wish to add a LUKS TPM Disk Unlock Key (DUK) to the TPM [y/N]:" -n 1 add_key_confirm if [ "$add_key_confirm" = "y" \ -o "$add_key_confirm" = "Y" ]; then @@ -236,11 +223,7 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ if [ "$save_key" = "y" ]; then if [ -n "$old_key_devices" ] || [ -n "$old_lvm_volume_group" ]; then DEBUG "Previous LUKS TPM Disk Unlock Key was set up for $old_key_devices $old_lvm_volume_group" - read \ - -n 1 \ - -p "Do you want to reuse configured Encrypted LVM groups/Block devices? (Y/n):" \ - reuse_past_devices - echo + INPUT "Do you want to reuse configured Encrypted LVM groups/Block devices? (Y/n):" -n 1 reuse_past_devices if [ "$reuse_past_devices" = "y" ] || [ "$reuse_past_devices" = "Y" ] || [ -z "$reuse_past_devices" ]; then if [ -z "$key_devices" ] && [ -n "$old_key_devices" ]; then key_devices="$old_key_devices" @@ -304,7 +287,7 @@ if [ "$save_key" = "y" ]; then # Get initrd filename selected to be default initrd that OS could be using to configure LUKS on boot by deploying crypttab files current_default_initrd=$(cat /boot/kexec_default_hashes.txt | grep initr | awk -F " " {'print $NF'} | sed 's/\.\//\/boot\//g') - echo "+++ Extracting current selected default boot's $current_default_initrd to find crypttab files..." + DEBUG "Extracting $current_default_initrd to find crypttab files" unpack_initramfs.sh "$current_default_initrd" "$initrd_decompressed" crypttab_files=$(find "$initrd_decompressed" | grep crypttab 2>/dev/null) || true @@ -329,14 +312,14 @@ if [ "$save_key" = "y" ]; then done #insert current default boot's initrd crypttab locations into tracking file to be overwritten into initramfs at kexec-inject-key - echo "+++ The following OS crypttab file:entry were modified from default boot's initrd:" + STATUS "The following OS crypttab entries were modified from default boot's initrd:" cat $bootdir/kexec_initrd_crypttab_overrides.txt - echo "+++ Heads added /secret.key in those entries and saved them under $bootdir/kexec_initrd_crypttab_overrides.txt" - echo "+++ Those overrides will be part of detached signed digests and used to prepare cpio injected at kexec of selected default boot entry." + STATUS "Heads added /secret.key to those entries and saved overrides for signing" + DEBUG "Crypttab overrides will be included in signed digests and injected via cpio at kexec" else - echo "+++ No crypttab file found in extracted initrd. A generic crypttab will be generated" + STATUS "No crypttab found in initrd; a generic crypttab will be generated" if [ -e "$bootdir/kexec_initrd_crypttab_overrides.txt" ]; then - echo "+++ Removing $bootdir/kexec_initrd_crypttab_overrides.txt" + DEBUG "Removing $bootdir/kexec_initrd_crypttab_overrides.txt" rm -f "$bootdir/kexec_initrd_crypttab_overrides.txt" fi fi diff --git a/initrd/bin/kexec-save-key b/initrd/bin/kexec-save-key index 0fe2373dc..2f2b05249 100755 --- a/initrd/bin/kexec-save-key +++ b/initrd/bin/kexec-save-key @@ -40,7 +40,7 @@ paramsdir="${paramsdir%%/}" DEBUG "kexec-save-key prior of last override: paramsdir: $paramsdir, paramsdev: $paramsdev, lvm_volume_group: $lvm_volume_group" if [ -n "$lvm_volume_group" ]; then - lvm vgchange -a y $lvm_volume_group || + run_lvm vgchange -a y $lvm_volume_group || die "Failed to activate the LVM group" for dev in /dev/$lvm_volume_group/*; do key_devices="$key_devices $dev" diff --git a/initrd/bin/kexec-seal-key b/initrd/bin/kexec-seal-key index 39b8c9e85..1150d879a 100755 --- a/initrd/bin/kexec-seal-key +++ b/initrd/bin/kexec-seal-key @@ -55,7 +55,7 @@ if [ -r "$KEY_LVM" ]; then if [ -z "$VOLUME_GROUP" ]; then die "No LVM volume group defined for activation" fi - lvm vgchange -a y $VOLUME_GROUP || + run_lvm vgchange -a y $VOLUME_GROUP || die "$VOLUME_GROUP: unable to activate volume group" else DEBUG "No LVM volume group defined for activation" @@ -68,22 +68,23 @@ luks_drk_passphrase_valid=0 attempts=0 # Ask for the DRK passphrase first, before testing any devices +STATUS "Unlocking LUKS device(s) using the Disk Recovery Key passphrase" while [ $attempts -lt 3 ] && [ $luks_drk_passphrase_valid -eq 0 ]; do - read -r -s -p $'\nEnter LUKS Disk Recovery Key (DRK) passphrase that can unlock '"$key_devices"': ' disk_recovery_key_passphrase - echo + INPUT "Enter LUKS Disk Recovery Key (DRK) passphrase that can unlock $key_devices:" -r -s disk_recovery_key_passphrase echo -n "$disk_recovery_key_passphrase" >"$DISK_RECOVERY_KEY_FILE" # Test the passphrase against ALL devices before deciding if it's valid all_devices_unlocked=1 for dev in $key_devices; do + STATUS "Testing DRK passphrase against $dev..." DEBUG "Testing $DISK_RECOVERY_KEY_FILE keyfile against $dev" if ! cryptsetup open $dev --test-passphrase --key-file "$DISK_RECOVERY_KEY_FILE" >/dev/null 2>&1; then warn "Failed to unlock LUKS device $dev with the provided passphrase." all_devices_unlocked=0 break else - echo "++++++ $dev: LUKS device unlocked successfully with the DRK passphrase" + STATUS_OK "$dev: LUKS device unlocked successfully with the Disk Recovery Key passphrase" fi done @@ -103,16 +104,14 @@ done MIN_PASSPHRASE_LENGTH=12 attempts=0 while [ $attempts -lt 3 ]; do - read -r -s -p $'\nNew LUKS TPM Disk Unlock Key (DUK) passphrase for booting (minimum '"$MIN_PASSPHRASE_LENGTH"' characters): ' key_password - echo + INPUT "New LUKS TPM Disk Unlock Key (DUK) passphrase for booting (minimum $MIN_PASSPHRASE_LENGTH characters):" -r -s key_password if [ ${#key_password} -lt $MIN_PASSPHRASE_LENGTH ]; then attempts=$((attempts + 1)) warn "Disk Unlock Key (DUK) passphrase is too short. Please try again." continue fi - read -r -s -p $'\nRepeat LUKS TPM Disk Unlock Key (DUK) passphrase for booting: ' key_password2 - echo + INPUT "Repeat LUKS TPM Disk Unlock Key (DUK) passphrase for booting:" -r -s key_password2 if [ "$key_password" != "$key_password2" ]; then attempts=$((attempts + 1)) warn "Disk Unlock Key (DUK) passphrases do not match. Please try again." @@ -126,8 +125,7 @@ if [ $attempts -ge 3 ]; then fi # Generate key file -echo -echo "++++++ Generating new randomized 128 bytes key file that will be sealed/unsealed by LUKS TPM Disk Unlock Key passphrase" +STATUS "Generating new randomized 128-byte key file for LUKS TPM Disk Unlock Key" dd \ if=/dev/urandom \ of="$DUK_KEY_FILE" \ @@ -194,8 +192,7 @@ for dev in $key_devices; do # Heads expects key slot LUKSv1:7 or LUKSv2:31 to be used for TPM DUK setup. # Ask user to confirm otherwise warn "LUKS key slot $keyslot is not typical ($duk_keyslot expected) for TPM Disk Unlock Key setup" - read -p $'Are you sure you want to wipe it? [y/N]\n' -n 1 -r - echo "" + INPUT "Are you sure you want to wipe it? [y/N]:" -n 1 -r REPLY # If user does not confirm, skip this slot if [[ $REPLY =~ ^[Yy]$ ]]; then wipe_desired="yes" @@ -207,7 +204,7 @@ for dev in $key_devices; do fi if [ "$wipe_desired" == "yes" ] && [ "$keyslot" != "$drk_key_slot" ]; then - echo "++++++ $dev: Wiping LUKS key slot $keyslot" + STATUS "$dev: Wiping LUKS key slot $keyslot" DO_WITH_DEBUG cryptsetup luksKillSlot \ --key-file "$DISK_RECOVERY_KEY_FILE" \ $dev $keyslot || @@ -216,7 +213,7 @@ for dev in $key_devices; do fi done - echo "++++++ $dev: Adding LUKS TPM Disk Unlock Key to LUKS key slot $duk_keyslot" + STATUS "$dev: Adding LUKS TPM Disk Unlock Key to key slot $duk_keyslot" DO_WITH_DEBUG cryptsetup luksAddKey \ --key-file "$DISK_RECOVERY_KEY_FILE" \ --new-key-slot $duk_keyslot \ @@ -228,9 +225,11 @@ done # 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" echo "$key_devices" | xargs /bin/qubes-measure-luks || die "Unable to measure the LUKS headers" +STATUS "Reading current PCR values for TPM sealing policy" pcrf="/tmp/secret/pcrf.bin" tpmr pcrread 0 "$pcrf" tpmr pcrread -a 1 "$pcrf" @@ -253,9 +252,13 @@ tpmr calcfuturepcr 6 "/tmp/luksDump.txt" >>"$pcrf" # We take into consideration user files in cbfs tpmr pcrread -a 7 "$pcrf" -DO_WITH_DEBUG --mask-position 7 \ - tpmr seal "$DUK_KEY_FILE" "$TPM_INDEX" 0,1,2,3,4,5,6,7 "$pcrf" \ +# tpmr seal may prompt for TPM owner password; avoid DO_WITH_DEBUG here so the +# prompt remains visible on console. tpmr logs command details internally. +STATUS "Sealing LUKS TPM Disk Unlock Key into TPM NVRAM (this may take a moment)" +DEBUG "tpmr seal $DUK_KEY_FILE $TPM_INDEX 0,1,2,3,4,5,6,7 $pcrf $TPM_SIZE " +tpmr seal "$DUK_KEY_FILE" "$TPM_INDEX" 0,1,2,3,4,5,6,7 "$pcrf" \ "$TPM_SIZE" "$key_password" || die "Unable to write LUKS TPM Disk Unlock Key to NVRAM" +STATUS_OK "LUKS TPM Disk Unlock Key sealed successfully" # should be okay if this fails shred -n 10 -z -u "$pcrf" 2>/dev/null || diff --git a/initrd/bin/kexec-select-boot b/initrd/bin/kexec-select-boot index 405713934..29421099f 100755 --- a/initrd/bin/kexec-select-boot +++ b/initrd/bin/kexec-select-boot @@ -64,12 +64,13 @@ if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then #PRIMHASH_FILE (normally /boot/kexec_primhdl_hash.txt) exists and is not empty sha256sum -c "$PRIMHASH_FILE" >/dev/null 2>&1 || { - echo "FATAL: Hash of TPM2 primary key handle mismatch!" + warn "FATAL: Hash of TPM2 primary key handle mismatch" warn "If you have not intentionally regenerated TPM2 primary key," warn "your system may have been compromised" DEBUG "Hash of TPM2 primary key handle mismatched for $PRIMHASH_FILE" DEBUG "Contents of $PRIMHASH_FILE:" DEBUG "$(cat $PRIMHASH_FILE)" + die "Hash of TPM2 primary key handle mismatch ($PRIMHASH_FILE). If you did not intentionally regenerate the TPM2 primary key, this may indicate compromise." } else warn "Hash of TPM2 primary key handle does not exist" @@ -82,10 +83,10 @@ if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then fi verify_global_hashes() { - INFO "+++ Checking verified boot hash file " + STATUS "Checking verified boot hash file" # Check the hashes of all the files if verify_checksums "$bootdir" "$gui_menu"; then - INFO "+++ Verified boot hashes " + STATUS_OK "Verified boot hashes" valid_hash='y' valid_global_hash='y' else @@ -99,7 +100,7 @@ verify_global_hashes() { # If user enables it, check root hashes before boot as well if [[ "$CONFIG_ROOT_CHECK_AT_BOOT" = "y" && "$force_menu" == "n" ]]; then if root-hashes-gui.sh -c; then - echo "+++ Verified root hashes, continuing boot " + STATUS_OK "Verified root hashes, continuing boot" # if user re-signs, it wipes out saved options, so scan the boot directory and generate if [ ! -r "$TMP_MENU_FILE" ]; then scan_options @@ -158,7 +159,7 @@ get_menu_option() { option_index=$(cat /tmp/whiptail) else - echo "+++ Select your boot option:" + STATUS "Select your boot option:" n=0 while read option; do parse_option @@ -166,9 +167,7 @@ get_menu_option() { echo "$n. $name [$kernel]" done <$TMP_MENU_FILE - read \ - -p "Choose the boot option [1-$n, a to abort]: " \ - option_index + INPUT "Choose the boot option [1-$n, a to abort]:" -r option_index if [ "$option_index" = "a" ]; then die "Aborting boot attempt" @@ -191,14 +190,10 @@ confirm_menu_option() { option_confirm=$(cat /tmp/whiptail) else - echo "+++ Please confirm the boot details for $name:" - echo $option - - read \ - -n 1 \ - -p "Confirm selection by pressing 'y', make default with 'd': " \ - option_confirm - echo + STATUS "Confirm boot details for $name:" + INFO "$option" + + INPUT "Confirm selection by pressing 'y', make default with 'd':" -n 1 option_confirm fi } @@ -208,7 +203,7 @@ parse_option() { } scan_options() { - INFO "+++ Scanning for unsigned boot options" + STATUS "Scanning for unsigned boot options" option_file="/tmp/kexec_options.txt" scan_boot_options "$bootdir" "$config" "$option_file" if [ ! -s $option_file ]; then @@ -223,11 +218,7 @@ scan_options() { save_default_option() { if [ "$gui_menu" != "y" ]; then - read \ - -n 1 \ - -p "Saving a default will modify the disk. Proceed? (Y/n): " \ - default_confirm - echo + INPUT "Saving a default will modify the disk. Proceed? (Y/n):" -n 1 default_confirm fi [ "$default_confirm" = "" ] && default_confirm="y" @@ -238,13 +229,13 @@ save_default_option() { -p "$paramsdir" \ -i "$option_index" \ ; then - echo "+++ Saved defaults to device" + STATUS_OK "Saved defaults to device" default_failed="n" force_menu="n" return else - echo "Failed to save defaults" + warn "Failed to save defaults" fi fi @@ -272,10 +263,10 @@ default_select() { if [ "$CONFIG_BASIC" != "y" ]; then # Enforce that default option hashes are valid - INFO "+++ Checking verified default boot hash file " + STATUS "Checking verified default boot hash file" # Check the hashes of all the files if (cd $bootdir && sha256sum -c "$TMP_DEFAULT_HASH_FILE" >/tmp/hash_output); then - echo "+++ Verified default boot hashes " + STATUS_OK "Verified default boot hashes" valid_hash='y' else if [ "$gui_menu" = "y" ]; then @@ -286,7 +277,7 @@ default_select() { fi fi - echo "+++ Executing default boot for $name:" + STATUS "Executing default boot for $name" do_boot warn "Failed to boot default option" } @@ -324,29 +315,29 @@ user_select() { do_boot() { if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_BOOT_REQ_ROLLBACK" = "y" ] && [ "$valid_rollback" = "n" ]; then - die "!!! Missing required rollback counter state" + die "Missing required rollback counter state" fi if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_BOOT_REQ_HASH" = "y" ] && [ "$valid_hash" = "n" ]; then - die "!!! Missing required boot hashes" + die "Missing required boot hashes" fi if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_TPM" = "y" ] && [ -r "$TMP_KEY_DEVICES" ]; then INITRD=$(kexec-boot -b "$bootdir" -e "$option" -i) || - die "!!! Failed to extract the initrd from boot option" + die "Failed to extract the initrd from boot option" if [ -z "$INITRD" ]; then - die "!!! No initrd file found in boot option" + die "No initrd file found in boot option" fi kexec-insert-key $INITRD || - die "!!! Failed to prepare TPM Disk Unlock Key for boot" + die "Failed to prepare TPM Disk Unlock Key for boot" kexec-boot -b "$bootdir" -e "$option" \ -a "$add" -r "$remove" -o "/tmp/secret/initrd.cpio" || - die "!!! Failed to boot w/ options: $option" + die "Failed to boot w/ options: $option" else kexec-boot -b "$bootdir" -e "$option" -a "$add" -r "$remove" || - die "!!! Failed to boot w/ options: $option" + die "Failed to boot w/ options: $option" fi } @@ -424,4 +415,4 @@ while true; do fi done -die "!!! Shouldn't get here" +die "Shouldn't get here" diff --git a/initrd/bin/kexec-sign-config b/initrd/bin/kexec-sign-config index b994a8b51..a07be90d3 100755 --- a/initrd/bin/kexec-sign-config +++ b/initrd/bin/kexec-sign-config @@ -94,13 +94,14 @@ if [ "$rollback" = "y" ]; then # Increment the TPM counter DEBUG "rollback=y: Incrementing counter $TPM_COUNTER." - increment_tpm_counter $TPM_COUNTER >/dev/null 2>&1 || + increment_tpm_counter $TPM_COUNTER || die "$paramsdir: Unable to increment tpm counter" # Ensure the incremented counter file exists incremented_counter_file="/tmp/counter-$TPM_COUNTER" if [ ! -e "$incremented_counter_file" ]; then DEBUG "TPM counter file '$incremented_counter_file' not found. Attempting to read it again." + # read_tpm_counter doesn't prompt; silence its normal output if called in DEBUG context DO_WITH_DEBUG read_tpm_counter "$TPM_COUNTER" >/dev/null 2>&1 || die "$paramsdir: TPM counter file '$incremented_counter_file' not found after incrementing." fi @@ -118,11 +119,44 @@ if [ -z "$param_files" ]; then die "$paramsdir: No kexec parameter files to sign" fi +# before we even attempt to sign, make sure there is at least one public +# key available so the user gets a clear error instead of a mysterious gpg +# failure. (The GUI path offers "Options -> Add a GPG key" for this.) +if [ "$(gpg -k 2>/dev/null | wc -l)" -eq 0 ]; then + # Provide guidance using the exact menu wording. Users may also choose + # to perform an OEM Factory Reset / Re-Ownership which will create a + # fresh keyring. + die "$paramsdir: no public GPG keys in keyring; add one (Options -> Add a GPG key) or perform OEM Factory Reset / Re-Ownership and try again." +fi + for tries in 1 2 3; do confirm_gpg_card TRACE_FUNC - - if DO_WITH_DEBUG sha256sum $param_files | gpg --detach-sign -a >$paramsdir/kexec.sig; then + DEBUG "kexec-sign-config: signing attempt ${tries}/3 begins after GPG card confirmation" + + # Public keys are not sufficient for signing. After card confirmation, + # force discovery of a usable secret key identity and pass it explicitly + # to gpg, instead of relying on implicit default-key selection. + card_status_output=$(gpg --card-status 2>/dev/null || true) + SIGNING_KEY_ID=$(gpg --with-colons --list-secret-keys 2>/dev/null | awk -F: '$1=="sec"||$1=="ssb" {print $5; exit}') + if [ -z "$SIGNING_KEY_ID" ]; then + CARD_SIGNING_KEY_ID=$(echo "$card_status_output" | awk -F: '/Signature key/ {gsub(/[[:space:]]/,"",$2); print $2; exit}') + if [ -n "$CARD_SIGNING_KEY_ID" ]; then + SIGNING_KEY_ID="$CARD_SIGNING_KEY_ID" + DEBUG "kexec-sign-config: using card-reported signing key id ${SIGNING_KEY_ID}" + fi + fi + if [ -z "$SIGNING_KEY_ID" ]; then + die "$paramsdir: no private signing key is available. A public key in keyring is not enough to sign. Insert/unlock the signing smartcard (or import private key backup material), then retry. If smartcard is inserted, ensure it contains a signing key and that GPG card status shows a non-empty 'Signature key'." + fi + DEBUG "kexec-sign-config: using explicit signing key id ${SIGNING_KEY_ID}" + + # Run the signing command without DO_WITH_DEBUG so that any gpg errors + # print directly to the console. Stdout (the signature) goes to kexec.sig; + # stderr (gpg status/error messages) goes to the log for later analysis. + # Keeping them separate prevents gpg status text from corrupting the sig file. + if sha256sum $param_files | \ + gpg --disable-dirmngr --no-auto-key-retrieve --local-user "$SIGNING_KEY_ID" --detach-sign -a >"$paramsdir/kexec.sig" 2>/tmp/kexec-sign.log; then # successful - update the validated params check_config $paramsdir @@ -131,6 +165,29 @@ for tries in 1 2 3; do exit 0 fi + + DEBUG "kexec-sign-config: signing attempt ${tries}/3 failed" + if [ -r /tmp/kexec-sign.log ]; then + DEBUG "kexec-sign-config: gpg signing stderr/stdout excerpt follows" + DEBUG "$(sed -n '1,40p' /tmp/kexec-sign.log)" + fi + + if grep -Eiq 'no dirmngr|failed to start dirmngr|can.t connect to the dirmngr|error running .*/dirmngr|signing failed: no dirmngr' /tmp/kexec-sign.log 2>/dev/null; then + die "$paramsdir: GPG signing failed because required GPG service 'dirmngr' is unavailable in this session/build. This is not a PIN error. Reboot and retry once; if it persists, use a Heads image/build with working dirmngr/gpg services before re-signing /boot metadata." + fi + + if grep -Eiq 'no default secret key|no secret key|secret key not available|signing failed: no secret key' /tmp/kexec-sign.log 2>/dev/null; then + die "$paramsdir: GPG signing failed because no private signing key is available to gpg. Confirm the expected signing key is present/unlocked on your smartcard or imported backup key material, then retry (Options -> Add a GPG key, or smartcard/backup media setup)." + fi + + # Provide precise, actionable guidance for common smartcard PIN failures. + if grep -Eiq 'bad pin|wrong pin|incorrect pin|pin incorrect|pinentry.*cancel' /tmp/kexec-sign.log 2>/dev/null; then + die "$paramsdir: GPG signing failed due to incorrect or rejected smartcard User PIN. Re-run signing and enter the correct User PIN at the prompt. Check remaining retries in the GPG card status menu; if retries are exhausted, unblock/reset with Admin PIN and try again." + fi + + if grep -Eiq 'pin blocked|card is blocked|authentication failed' /tmp/kexec-sign.log 2>/dev/null; then + die "$paramsdir: GPG signing failed because smartcard PIN is blocked or authentication is denied. Unblock/reset the card PIN with Admin PIN (or use valid backup key material) and retry signing." + fi done # remount /boot as ro diff --git a/initrd/bin/kexec-unseal-key b/initrd/bin/kexec-unseal-key index 12b22c266..9ab304c01 100755 --- a/initrd/bin/kexec-unseal-key +++ b/initrd/bin/kexec-unseal-key @@ -30,9 +30,8 @@ for tries in 1 2 3; do # passphrase prompt. This gives the user context while they prepare to # type the LUKS passphrase. show_totp_until_esc - - read -r -s -p $'\nEnter LUKS TPM Disk Unlock Key passphrase (blank to abort): ' tpm_password - echo + STATUS "Unlocking LUKS with TPM Disk Unlock Key" + INPUT "Enter LUKS TPM Disk Unlock Key passphrase (blank to abort):" -r -s tpm_password if [ -z "$tpm_password" ]; then die "Aborting unseal disk encryption key" fi diff --git a/initrd/bin/lock_chip b/initrd/bin/lock_chip index 26c9c1c78..a5cf84677 100755 --- a/initrd/bin/lock_chip +++ b/initrd/bin/lock_chip @@ -19,9 +19,8 @@ if [ -n "$APM_CNT" -a -n "$FIN_CODE" ]; then # will become write protected in the range specified in the PR0 register. Once # the protection is set and locked, it cannot be disabled # until the next system reset. - echo "Finalizing chipset Write Protection through SMI PR0 lockdown call" + STATUS "Finalizing chipset write protection via SMI PR0 lockdown" io386 -o b -b x $APM_CNT $FIN_CODE else - echo "NOT Finalizing chipset" - echo "lock_chip called without valid APM_CNT and FIN_CODE defined under bin/lock_chip." + NOTE "NOT finalizing chipset - lock_chip called without valid APM_CNT and FIN_CODE" fi diff --git a/initrd/bin/media-scan b/initrd/bin/media-scan index 068fa88a5..b4be785fe 100755 --- a/initrd/bin/media-scan +++ b/initrd/bin/media-scan @@ -54,7 +54,7 @@ get_menu_option() { option_index=$(cat /tmp/whiptail) else - echo "+++ Select your ISO boot option:" + STATUS "Select your ISO boot option:" n=0 while read option do @@ -62,9 +62,7 @@ get_menu_option() { echo "$n. $option" done < /tmp/iso_menu.txt - read \ - -p "Choose the ISO boot option [1-$n, a to abort]: " \ - option_index + INPUT "Choose the ISO boot option [1-$n, a to abort]:" -r option_index fi # Empty occurs when aborting fbwhiptail with esc-esc @@ -101,7 +99,7 @@ if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then die "No ISO files found, bootable USB not allowed with Restricted Boot." fi -echo "!!! Could not find any ISO, trying bootable USB" +warn "Could not find any ISO, trying bootable USB" # Attempt to pull verified config from device if [ -x /bin/whiptail ]; then DO_WITH_DEBUG kexec-select-boot -b /media -c "*.cfg" -u -g -s diff --git a/initrd/bin/mount-usb b/initrd/bin/mount-usb index 8acad1357..a5d101103 100755 --- a/initrd/bin/mount-usb +++ b/initrd/bin/mount-usb @@ -86,8 +86,7 @@ if [ -z "$(cat /tmp/usb_block_devices)" ]; then whiptail_warning --title 'USB Drive Missing' \ --msgbox "Insert your USB drive and press Enter to continue." 0 80 else - echo "+++ USB Drive Missing! Insert your USB drive and press Enter to continue." - read + INPUT "USB Drive Missing! Insert your USB drive and press Enter to continue." fi sleep 1 list_usb_storage > /tmp/usb_block_devices @@ -96,7 +95,7 @@ if [ -z "$(cat /tmp/usb_block_devices)" ]; then whiptail_error --title 'ERROR: USB Drive Missing' \ --msgbox "USB Drive Missing! Aborting mount attempt.\n\nPress Enter to continue." 0 80 else - echo "!!! ERROR: USB Drive Missing! Aborting mount. Press Enter to continue." + die "USB Drive Missing! Aborting mount." fi exit 1 fi @@ -145,7 +144,7 @@ else fi option_index=$(cat /tmp/whiptail) else - echo "+++ Select your USB disk:" + STATUS "Select your USB disk:" n=0 while read option do @@ -153,9 +152,7 @@ else echo "$n. $option" done < /tmp/usb_disk_list - read \ - -p "Choose your USB disk [1-$n, a to abort]: " \ - option_index + INPUT "Choose your USB disk [1-$n, a to abort]:" -r option_index fi if [ "$option_index" = "a" ]; then diff --git a/initrd/bin/network-init-recovery b/initrd/bin/network-init-recovery index 638d2b3b0..dc6995eca 100755 --- a/initrd/bin/network-init-recovery +++ b/initrd/bin/network-init-recovery @@ -10,25 +10,21 @@ mobile_tethering() #Tethering over USB for Mobile phones supporting CDC (Android Pixel 6a+, Librem phone, etc.) if [ -e /lib/modules/cdc_ether.ko ]; then #prompt user if he wants to enable USB tethering and skip if not - echo "" - echo "USB tethering support is available for mobile phones supporting CDC NCM/EEM tethering" - read -p "Do you want to enable USB tethering now? (Y/n)" -n 1 -r REPLY - echo "" + INFO "USB tethering support is available for mobile phones supporting CDC NCM/EEM tethering" + INPUT "Do you want to enable USB tethering now? (Y/n):" -n 1 -r REPLY if [[ $REPLY =~ ^[Nn]$ ]]; then - echo "USB tethering not enabled, skipping..." + DEBUG "USB tethering not enabled, skipping" return 0 fi #first enable USB controllers enable_usb - echo "" - echo "Please connect your mobile phone to a USB port and enable internet connection sharing." - echo "* Android: Select the 'Charging this device via USB' notification and enable tethering." - echo "* Linux: Set the wired connection's IPv4 method on the mobile phone to 'Shared to other computers'." - echo "Heads supports CDC-NCM and CDC-EEM. Android phones using RNDIS and Apple phones are not supported." - echo "" - read -p "Press Enter to continue..." -r + STATUS "Please connect your mobile phone to a USB port and enable internet connection sharing" + STATUS "* Android: Select the 'Charging this device via USB' notification and enable tethering" + STATUS "* Linux: Set the wired connection's IPv4 method on the mobile phone to 'Shared to other computers'" + INFO "Heads supports CDC-NCM and CDC-EEM. Android phones using RNDIS and Apple phones are not supported" + INPUT "Press Enter to continue..." network_modules="mii usbnet cdc_ether cdc_ncm cdc_eem" for module in $(echo $network_modules); do @@ -38,13 +34,11 @@ mobile_tethering() done if ! [ -e /sys/class/net/usb0 ]; then - echo "" - echo "No tethering network interface was found." - echo "* Make sure the phone supports CDC-NCM or CDC-EEM. Many, but not all, Android and Linux phones support these." - echo "* Android phones requiring RNDIS and Apple phones are not supported." - echo "* Make sure the cable used works with data and that the phone has tethering enabled." - echo "" - read -p "Press Enter to continue..." -r + warn "No tethering network interface was found" + INFO "* Make sure the phone supports CDC-NCM or CDC-EEM. Many, but not all, Android and Linux phones support these" + INFO "* Android phones requiring RNDIS and Apple phones are not supported" + INFO "* Make sure the cable used works with data and that the phone has tethering enabled" + INPUT "Press Enter to continue..." fi fi } @@ -53,14 +47,13 @@ ethernet_activation() { TRACE_FUNC #Prompt user if he wants to enable ethernet and skip if not - read -p "Do you want to enable Ethernet now? (Y/n)" -n 1 -r REPLY - echo "" + INPUT "Do you want to enable Ethernet now? (Y/n):" -n 1 -r REPLY if [[ $REPLY =~ ^[Nn]$ ]]; then - echo "Ethernet not enabled, skipping..." + DEBUG "Ethernet not enabled, skipping" return 0 fi - echo "Loading Ethernet network modules..." + STATUS "Loading Ethernet network modules" network_modules="e1000 e1000e igb sfc mdio mlx4_core mlx4_en" for module in $(echo $network_modules); do if [ -f /lib/modules/$module.ko ]; then @@ -77,12 +70,12 @@ ethernet_activation if [ -e /sys/class/net/usb0 ]; then dev=usb0 - echo "USB tethering network interface detected as $dev" + STATUS "USB tethering network interface detected as $dev" elif [ -e /sys/class/net/eth0 ]; then dev=eth0 - echo "Ethernet network interface detected as $dev" + STATUS "Ethernet network interface detected as $dev" else - echo "No network interface detected, please check your hardware and board configuration" + warn "No network interface detected, please check your hardware and board configuration" exit 1 fi @@ -91,40 +84,39 @@ if [ -n "$dev" ]; then #Randomize MAC address for maximized boards if echo "$CONFIG_BOARD" | grep -q maximized; then ifconfig $dev down - echo "Generating random MAC address..." + STATUS "Generating random MAC address" mac=$(generate_random_mac_address) - echo "Assigning randomly generated MAC: $mac to $dev..." + STATUS "Assigning randomly generated MAC $mac to $dev" ifconfig $dev hw ether $mac ifconfig $dev up fi # Set up static IP if configured in board config if [ ! -z "$CONFIG_BOOT_STATIC_IP" ]; then - echo "Setting static IP: $CONFIG_BOOT_STATIC_IP" + STATUS "Setting static IP: $CONFIG_BOOT_STATIC_IP" ifconfig $dev $CONFIG_BOOT_STATIC_IP - echo "No NTP sync with static IP: no DNS server nor gateway defined, set time manually" + INFO "No NTP sync with static IP: no DNS server or gateway defined, set time manually" # Set up DHCP if no static IP elif [ -e /sbin/udhcpc ]; then - echo "Getting IP from first DHCP server answering. This may take a while..." + STATUS "Getting IP from DHCP server (this may take a while)" if udhcpc -T 1 -i $dev -q; then if [ -e /sbin/ntpd ]; then DNS_SERVER=$(grep nameserver /etc/resolv.conf | awk -F " " {'print $2'}) killall ntpd 2 &>1 >/dev/null - echo "Attempting to sync time with NTP server: $DNS_SERVER..." + STATUS "Attempting NTP time sync with $DNS_SERVER" if ! ntpd -d -N -n -q -p $DNS_SERVER; then - echo "NTP sync unsuccessful with DNS server" - echo "Attempting NTP time sync with pool.ntp.org..." + warn "NTP sync unsuccessful with DNS server" + STATUS "Attempting NTP time sync with pool.ntp.org" if ! ntpd -d -d -N -n -q -p pool.ntp.org; then - echo "NTP sync unsuccessful." + warn "NTP sync unsuccessful" else - echo "NTP time sync successful." + STATUS_OK "NTP time sync successful" fi fi - echo "Syncing hardware clock with system time in UTC/GMT timezone..." + STATUS "Syncing hardware clock with system time (UTC)" hwclock -w - echo "" date=$(date "+%Y-%m-%d %H:%M:%S %Z") - echo "Time: $date" + STATUS "Time: $date" fi fi fi @@ -134,7 +126,7 @@ if [ -n "$dev" ]; then if [ ! -d /etc/dropbear ]; then mkdir /etc/dropbear fi - echo "Starting dropbear ssh server..." + STATUS "Starting dropbear SSH server" # Make sure dropbear is not already running killall dropbear > /dev/null 2>&1 || true # Start dropbear with root login and log to stderr @@ -142,7 +134,6 @@ if [ -n "$dev" ]; then # -R create host keys dropbear -B -R fi - echo "" - echo "Network setup complete:" + STATUS_OK "Network setup complete" ifconfig $dev fi diff --git a/initrd/bin/oem-factory-reset b/initrd/bin/oem-factory-reset index e988bda2a..58735663c 100755 --- a/initrd/bin/oem-factory-reset +++ b/initrd/bin/oem-factory-reset @@ -115,10 +115,9 @@ SKIP_BOOT="n" ## functions die() { - local msg=$1 if [ -n "$msg" ]; then - echo -e "\n$msg" + warn "$msg" fi kill -s TERM $TOP_PID exit 1 @@ -154,14 +153,12 @@ reset_nk3_secret_app() { # Reset Nitrokey 3 Secrets app PIN with $ADMIN_PIN (default 12345678, or customised) if lsusb | grep -q "20a0:42b2" && [ -x /bin/hotp_verification ]; then - echo warn "Resetting Nitrokey 3's Secrets app with PIN. Physical presence (touch) will be required" # TODO: change message when https://github.com/Nitrokey/nitrokey-hotp-verification/issues/41 is fixed # Reset Nitrokey 3 secret app with PIN # Do 3 attempts to reset Nitrokey 3 Secrets app if return code is 3 (no touch) for attempt in 1 2 3; do if /bin/hotp_verification reset "${ADMIN_PIN}"; then - echo return 0 else error_code=$? @@ -181,7 +178,7 @@ reset_nk3_secret_app() { generate_inmemory_RSA_master_and_subkeys() { TRACE_FUNC - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits master key..." + STATUS "Generating GPG RSA ${RSA_KEY_LENGTH}-bit master key" # Generate GPG master key { echo "Key-Type: RSA" # RSA key @@ -199,7 +196,7 @@ generate_inmemory_RSA_master_and_subkeys() { whiptail_error_die "GPG Key generation failed!\n\n$ERROR" fi - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits signing subkey..." + STATUS "Generating GPG RSA ${RSA_KEY_LENGTH}-bit signing subkey" # Add signing subkey { echo addkey # add key in --edit-key mode @@ -216,7 +213,7 @@ generate_inmemory_RSA_master_and_subkeys() { whiptail_error_die "GPG Key signing subkey generation failed!\n\n$ERROR" fi - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits encryption subkey..." + STATUS "Generating GPG RSA ${RSA_KEY_LENGTH}-bit encryption subkey" #Add encryption subkey { echo addkey # add key in --edit-key mode @@ -233,7 +230,7 @@ generate_inmemory_RSA_master_and_subkeys() { whiptail_error_die "GPG Key encryption subkey generation failed!\n\n$ERROR" fi - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits authentication subkey..." + STATUS "Generating GPG RSA ${RSA_KEY_LENGTH}-bit authentication subkey" #Add authentication subkey { #Authentication subkey needs gpg in expert mode to select RSA custom mode (8) @@ -264,7 +261,7 @@ generate_inmemory_RSA_master_and_subkeys() { generate_inmemory_p256_master_and_subkeys() { TRACE_FUNC - echo "Generating GPG p256 bits master key..." + STATUS "Generating GPG p256 master key" { echo "Key-Type: ECDSA" # ECDSA key echo "Key-Curve: nistp256" # ECDSA key curve @@ -285,7 +282,7 @@ generate_inmemory_p256_master_and_subkeys() { #Keep Master key fingerprint for add key calls MASTER_KEY_FP=$(gpg --list-secret-keys --with-colons | grep fpr | cut -d: -f10) - echo "Generating GPG nistp256 signing subkey..." + STATUS "Generating GPG nistp256 signing subkey" { echo addkey # add key in --edit-key mode echo 11 # ECC own set capability @@ -300,7 +297,7 @@ generate_inmemory_p256_master_and_subkeys() { whiptail_error_die "Failed to add ECC nistp256 signing key to master key\n\n${ERROR_MSG}" fi - echo "Generating GPG nistp256 encryption subkey..." + STATUS "Generating GPG nistp256 encryption subkey" { echo addkey echo 12 # ECC own set capability @@ -315,7 +312,7 @@ generate_inmemory_p256_master_and_subkeys() { whiptail_error_die "Failed to add ECC nistp256 encryption key to master key\n\n${ERROR_MSG}" fi - echo "Generating GPG nistp256 authentication subkey..." + STATUS "Generating GPG nistp256 authentication subkey" { echo addkey # add key in --edit-key mode echo 11 # ECC own set capability @@ -350,7 +347,7 @@ keytocard_subkeys_to_smartcard() { gpg_key_factory_reset - echo "Moving subkeys to smartcard..." + STATUS "Moving subkeys to smartcard" { echo "key 1" #Toggle on Signature key in --edit-key mode on local keyring echo "keytocard" #Move Signature key to smartcard @@ -558,7 +555,7 @@ gpg_key_factory_reset() { enable_usb # Factory reset GPG card - echo "GPG factory reset of USB Security dongle's OpenPGP smartcard..." + STATUS "GPG factory reset of USB Security dongle OpenPGP smartcard" { echo admin # admin menu echo factory-reset # factory reset smartcard @@ -648,7 +645,7 @@ generate_OEM_gpg_keys() { TRACE_FUNC #This function simply generates subkeys in smartcard following smarcard config from gpg_key_factory_reset - echo "Generating GPG keys in USB Security dongle's OpenPGP smartcard..." + STATUS "Generating GPG keys on USB Security dongle OpenPGP smartcard" { echo admin # admin menu echo generate # generate keys @@ -725,22 +722,25 @@ generate_checksums() { if [ "$CONFIG_TPM" = "y" ]; then if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then tpmr counter_create \ - -pwdc '' \ + -pwdc "${TPM_PASS:-}" \ -la -3135106223 | tee /tmp/counter >/dev/null 2>&1 || whiptail_error_die "Unable to create TPM counter" TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1 || - whiptail_error_die "Unable to increment tpm counter" + # increment TPM counter so /tmp/counter-$TPM_COUNTER is populated, + # then persist rollback metadata under /boot for next-boot preflight. + increment_tpm_counter "$TPM_COUNTER" || + whiptail_error_die "Unable to increment TPM counter" - # create rollback file - sha256sum /tmp/counter-$TPM_COUNTER >/boot/kexec_rollback.txt 2>/dev/null || - whiptail_error_die "Unable to create rollback file" - fi + [ -s /tmp/counter-"$TPM_COUNTER" ] || + whiptail_error_die "TPM counter increment did not produce counter state for rollback file" + + # create rollback file + sha256sum /tmp/counter-"$TPM_COUNTER" >/boot/kexec_rollback.txt 2>/dev/null || + whiptail_error_die "Unable to create rollback file" fi # If HOTP is enabled from board config, create HOTP counter @@ -854,80 +854,10 @@ set_default_boot_option() { TRACE_FUNC } -report_integrity_measurements() { - TRACE_FUNC - - #check for GPG key in keyring - GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) - if [ "$GPG_KEY_COUNT" -ne 0 ]; then - # Check and report TOTP - # update the TOTP code every thirty seconds - date=$(date "+%Y-%m-%d %H:%M:%S %Z") - seconds=$(date "+%s") - half=$(expr \( "$seconds" % 60 \) / 30) - if [ "$CONFIG_TPM" != "y" ]; then - TOTP="NO TPM" - elif [ "$half" != "$last_half" ]; then - last_half=$half - TOTP=$(unseal-totp) >/dev/null 2>&1 - fi - - # Check and report on HOTP status - if [ -x /bin/hotp_verification ]; then - HOTP="Unverified" - enable_usb - for attempt in 1 2 3; do - if ! hotp_verification info >/dev/null 2>&1; then - whiptail_warning --title "WARNING: Please insert your HOTP enabled USB Security dongle (Attempt $attempt/3)" --msgbox "Your HOTP enabled USB Security dongle was not detected.\n\nPlease remove it and insert it again." 0 80 - else - break - fi - done - - if [ $attempt -eq 3 ]; then - die "No HOTP enabled USB Security dongle detected. Please disable 'CONFIG_HOTPKEY' in the board config and rebuild." - fi - - # Don't output HOTP codes to screen, so as to make replay attacks harder - HOTP=$(unseal-hotp) >/dev/null 2>&1 - hotp_verification check $HOTP - case "$?" in - 0) - HOTP="Success" - ;; - 4) - HOTP="Invalid code" - BG_COLOR_MAIN_MENU="error" - ;; - *) - HOTP="Error checking code, Insert USB Security dongle and retry" - BG_COLOR_MAIN_MENU="warning" - ;; - esac - else - HOTP='N/A' - fi - # Check for detached signed digest and report on /boot integrity status - check_config /boot force - TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" - - if (cd /boot && sha256sum -c "$TMP_HASH_FILE" >/tmp/hash_output); then - HASH="OK" - else - HASH="ALTERED" - fi - - #Show results - whiptail_type $BG_COLOR_MAIN_MENU --title "Measured Integrity Report" --msgbox "$date\nTOTP: $TOTP | HOTP: $HOTP\n/BOOT INTEGRITY: $HASH\n\nPress OK to continue or Ctrl+Alt+Delete to reboot" 0 80 - fi - - TRACE_FUNC -} - usb_security_token_capabilities_check() { TRACE_FUNC - echo -e "\nChecking for USB Security dongle...\n" + STATUS "Checking for USB Security dongle" enable_usb # ... first set board config preference @@ -978,72 +908,65 @@ fi #Make sure /boot is mounted if board config defines default mount_boot -# We show current integrity measurements status and time -report_integrity_measurements +# Show integrity report only when prior Heads trust metadata exists and it +# has not already been shown to the user (e.g. when called from the report menu). +if [ "${INTEGRITY_REPORT_ALREADY_SHOWN:-0}" = "1" ]; then + DEBUG "Skipping integrity report in OEM Factory Reset: already shown to user before this call" +elif has_prior_boot_trust_metadata /boot/kexec_rollback.txt; then + report_integrity_measurements +else + DEBUG "Skipping integrity report in OEM Factory Reset: no prior /boot trust metadata detected (fresh first-ownership path)" +fi # Clear the screen clear #Prompt user for use of default configuration options TRACE_FUNC -echo -e -n "Would you like to use default configuration options?\nIf N, you will be prompted for each option [Y/n]: " -read -n 1 use_defaults +INPUT "Would you like to use default configuration options? If N, you will be prompted for each option [Y/n]:" -n 1 use_defaults if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then #Give general guidance to user on how to answer prompts - echo - echo "****************************************************" - echo "**** Factory Reset / Re-Ownership Questionnaire ****" - echo "****************************************************" - echo "The following questionnaire will help you configure the security components of your system." - echo "Each prompt requires a single letter answer: eg. (Y/n)." - echo -e "If you don't know what to answer, pressing Enter will select the default answer for that prompt: eg. Y, above.\n" + 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" # Re-ownership of LUKS encrypted Disk: key, content and passphrase - echo -e -n "\n\nWould you like to change the current LUKS Disk Recovery Key passphrase?\n (Highly recommended if you didn't install the Operating System yourself, so that past configured passphrase would not permit to access content.\n Note that without re-encrypting disk, a backed up header could be restored to access encrypted content with old passphrase) [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to change the current LUKS Disk Recovery Key passphrase? (Highly recommended if you didn't install the OS yourself) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then luks_new_Disk_Recovery_Key_passphrase_desired=1 - echo -e "\n" fi - echo -e -n "Would you like to re-encrypt LUKS encrypted container and generate new LUKS Disk Recovery Key?\n (Highly recommended if you didn't install the operating system yourself: this would prevent any LUKS backed up header to be restored to access encrypted data) [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to re-encrypt LUKS container and generate new LUKS Disk Recovery Key? (Highly recommended if you didn't install the OS yourself) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then TRACE_FUNC test_luks_current_disk_recovery_key_passphrase luks_new_Disk_Recovery_Key_desired=1 - echo -e "\n" fi #Prompt to ask if user wants to generate GPG key material in memory or on smartcard - echo -e -n "Would you like to format an encrypted USB Thumb drive to store GPG key material?\n (Required to enable GPG authentication) [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to format an encrypted USB Thumb drive to store GPG key material? (Required to enable GPG authentication) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ] \ ; then GPG_GEN_KEY_IN_MEMORY="y" - echo " ++++ Master key and subkeys will be generated in memory, backed up to dedicated LUKS container +++" - echo -e -n "Would you like in-memory generated subkeys to be copied to USB Security dongle's OpenPGP smartcard?\n (Highly recommended so the smartcard is used on daily basis and backup is kept safe, but not required) [Y/n]: " - read -n 1 prompt_output - echo + STATUS "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 USB Security dongle's OpenPGP smartcard? (Highly recommended) [Y/n]:" -n 1 prompt_output if [ "$prompt_output" == "n" \ -o "$prompt_output" == "N" ]; then warn "Subkeys will NOT be copied to USB Security dongle's OpenPGP smartcard" warn "Your GPG key material backup thumb drive should be cloned to a second thumb drive for redundancy for production environements" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" else - echo "++++ Subkeys will be copied to USB Security dongle's OpenPGP smartcard ++++" + STATUS "Subkeys will be copied to USB Security dongle's OpenPGP smartcard" warn "Please keep your GPG key material backup thumb drive safe" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="y" fi else - echo "GPG key material will be generated on USB Security dongle's OpenPGP smartcard without backup" + STATUS "GPG key material will be generated on USB Security dongle's OpenPGP smartcard without backup" GPG_GEN_KEY_IN_MEMORY="n" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" fi @@ -1068,22 +991,16 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then fi # Inform user of security components affected for the following prompts - echo - echo -e "The following Security Components will be configured with defaults or further chosen PINs/passwords: - $CUSTOM_PASS_AFFECTED_COMPONENTS\n" + INFO "The following Security Components will be configured with defaults or further chosen PINs/passwords: $CUSTOM_PASS_AFFECTED_COMPONENTS" # Prompt to change default passwords - echo -e -n "Would you like to set a single custom password to all previously stated security components? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to set a single custom password to all previously stated security components? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then - echo -e "\nThe chosen custom password must be between 8 and $MAX_HOTP_GPG_PIN_LENGTH characters in length." + INFO "The chosen custom password must be between 8 and $MAX_HOTP_GPG_PIN_LENGTH characters in length." while [[ ${#CUSTOM_SINGLE_PASS} -lt 8 ]] || [[ ${#CUSTOM_SINGLE_PASS} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - echo -e -n "Enter the custom password: " - read CUSTOM_SINGLE_PASS + INPUT "Enter the custom password (8-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r CUSTOM_SINGLE_PASS done - echo TPM_PASS=${CUSTOM_SINGLE_PASS} USER_PIN=${CUSTOM_SINGLE_PASS} ADMIN_PIN=${CUSTOM_SINGLE_PASS} @@ -1097,35 +1014,26 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then # record it MAKE_USER_RECORD_PASSPHRASES= else - echo -e -n "Would you like to set distinct PINs/passwords to configure previously stated security components? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to set distinct PINs/passwords to configure previously stated security components? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then - echo -e "\nThe TPM Owner Password and Admin PIN must be at least 8, the User PIN at least 6 characters in length.\n" - echo + INFO "TPM Owner Password and Admin PIN must be at least 8 chars, User PIN at least 6 chars." if [ "$CONFIG_TPM" = "y" ]; then while [[ ${#TPM_PASS} -lt 8 ]]; do - echo -e -n "Enter desired TPM Owner Password: " - read TPM_PASS + INPUT "Enter desired TPM Owner Password (min 8 chars):" -r TPM_PASS done fi while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - echo -e -n "\nThis PIN should be between 6 to $MAX_HOTP_GPG_PIN_LENGTH characters in length.\n" - echo -e -n "Enter desired GPG Admin PIN: " - read ADMIN_PIN + INPUT "Enter desired GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r ADMIN_PIN done #USER PIN not required in case of GPG_GEN_KEY_IN_MEMORY not requested of if GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD is # That is, if keys were NOT generated in memory (on smartcard only) or # if keys were generated in memory but are to be moved from local keyring to smartcard if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then while [[ ${#USER_PIN} -lt 6 ]] || [[ ${#USER_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - echo -e -n "\nThis PIN should be between 6 to $MAX_HOTP_GPG_PIN_LENGTH characters in length.\n" - echo -e -n "Enter desired GPG User PIN: " - read USER_PIN + INPUT "Enter desired GPG User PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r USER_PIN done fi - echo # The user knows these passwords, we don't need to # badger them to record them MAKE_USER_RECORD_PASSPHRASES= @@ -1135,47 +1043,33 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then if [ -n "$luks_new_Disk_Recovery_Key_passphrase_desired" -a -z "$luks_new_Disk_Recovery_Key_passphrase" ]; then # We catch here if changing LUKS Disk Recovery Key passphrase was desired # but yet undone. This is if not being covered by the single password - echo -e "\nEnter desired replacement for current LUKS Disk Recovery Key passphrase (At least 8 characters long):" while [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; do - { - read -r luks_new_Disk_Recovery_Key_passphrase - } + INPUT "Enter desired replacement for current LUKS Disk Recovery Key passphrase (min 8 chars):" -r luks_new_Disk_Recovery_Key_passphrase done #We test that current LUKS Disk Recovery Key passphrase is known prior of going further TRACE_FUNC test_luks_current_disk_recovery_key_passphrase - echo -e "\n" fi # Prompt to change default GnuPG key information - echo -e -n "Would you like to set custom user information for the GnuPG key? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to set custom user information for the GnuPG key? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then - echo -e "\n\n" - echo -e "We will generate a GnuPG (PGP) keypair identifiable with the following text form:" - echo -e "Real Name (Comment) email@address.org" + INFO "We will generate a GnuPG (PGP) keypair identifiable as: Real Name (Comment) email@address.org" - echo -e "\nEnter your Real Name (Optional):" - read -r GPG_USER_NAME + INPUT "Enter your Real Name (optional):" -r GPG_USER_NAME - echo -e "\nEnter your email@adress.org:" - read -r GPG_USER_MAIL + INPUT "Enter your email@address.org:" -r GPG_USER_MAIL while ! $(expr "$GPG_USER_MAIL" : '.*@' >/dev/null); do - { - echo -e "\nEnter your email@address.org:" - read -r GPG_USER_MAIL - } + INPUT "Invalid email - enter your email@address.org:" -r GPG_USER_MAIL done - echo -e "\nEnter Comment (Required: Use this to distinguish this key from others, e.g., its purpose or usage context. Must be 1-60 characters):" while true; do - read -r GPG_USER_COMMENT + INPUT "Enter Comment (1-60 chars, distinguishes this key, e.g. its purpose):" -r GPG_USER_COMMENT if [[ ${#GPG_USER_COMMENT} -ge 1 && ${#GPG_USER_COMMENT} -le 60 ]]; then break fi - echo -e "\nComment must be 1-60 characters long. Please try again:" + warn "Comment must be 1-60 characters long. Please try again." done fi @@ -1193,8 +1087,7 @@ if [ "$ADMIN_PIN" == "" ]; then ADMIN_PIN=${ADMIN_PIN_DEF}; fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" ]; then # Prompt to insert USB drive if desired - echo -e -n "\nWould you like to export your public key to an USB drive? [y/N]: " - read -n 1 prompt_output + INPUT "Would you like to export your public key to a USB drive? [y/N]:" -n 1 prompt_output echo if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ] \ @@ -1246,11 +1139,11 @@ killall gpg-agent >/dev/null 2>&1 || true rm -rf /.gnupg/*.kbx /.gnupg/*.gpg >/dev/null 2>&1 || true # detect and set /boot device -echo -e "\nDetecting and setting boot device...\n" +STATUS "Detecting and setting boot device" if ! detect_boot_device; then SKIP_BOOT="y" else - echo -e "Boot device set to $CONFIG_BOOT_DEV\n" + STATUS "Boot device set to $CONFIG_BOOT_DEV" fi # update configs @@ -1273,7 +1166,7 @@ fi ## reset TPM and set password if [ "$CONFIG_TPM" = "y" ]; then - echo -e "\nResetting TPM...\n" + STATUS "Resetting TPM" tpmr reset "$TPM_PASS" >/dev/null 2>/tmp/error fi if [ $? -ne 0 ]; then @@ -1310,13 +1203,13 @@ else #Reset Nitrokey 3 secret app reset_nk3_secret_app #Generate GPG key and subkeys on smartcard only - echo -e "\nResetting USB Security dongle's OpenPGP smartcard with GPG...\n(this may take up to 3 minutes...)\n" + STATUS "Resetting USB Security dongle OpenPGP smartcard with GPG (this may take up to 3 minutes)" gpg_key_factory_reset generate_OEM_gpg_keys fi -# Obtain GPG key ID -GPG_GEN_KEY=$(gpg --list-keys --with-colons | grep "^fpr" | cut -d: -f10 | head -n1) +# Obtain GPG key ID without printing trustdb maintenance chatter to console +GPG_GEN_KEY=$(gpg --list-keys --with-colons 2>/dev/null | grep "^fpr" | cut -d: -f10 | head -n1) #Where to export the public key PUBKEY="/tmp/${GPG_GEN_KEY}.asc" @@ -1330,16 +1223,16 @@ fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then #Only apply smartcard PIN change if smartcard only or if keytocard op is expected next if [ "${USER_PIN}" != "${USER_PIN_DEF}" -o "${ADMIN_PIN}" != "${ADMIN_PIN_DEF}" ]; then - echo -e "\nChanging default GPG Admin PIN\n" + STATUS "Changing default GPG Admin PIN" gpg_key_change_pin "3" "${ADMIN_PIN_DEF}" "${ADMIN_PIN}" - echo -e "\nChanging default GPG User PIN\n" + STATUS "Changing default GPG User PIN" gpg_key_change_pin "1" "${USER_PIN_DEF}" "${USER_PIN}" fi fi ## export pubkey to USB if [ "$GPG_EXPORT" != "0" ]; then - echo -e "\nExporting generated key to USB...\n" + STATUS "Exporting generated key to USB" # copy to USB if ! cp "${PUBKEY}" "/media/${GPG_GEN_KEY}.asc" 2>/tmp/error; then ERROR=$(tail -n 1 /tmp/error | fold -s) @@ -1377,9 +1270,9 @@ else ## flash generated key to ROM # read current firmware; show all output and capture stderr for errors if echo "$CONFIG_FLASH_OPTIONS" | grep -q -- '--progress'; then - echo -e "\nReading current firmware (progress shown below)...\n" + STATUS "Reading current firmware (progress shown below)..." else - echo -e "\nReading current firmware...\n(this may take up to two minutes...)\n" + STATUS "Reading current firmware... (this may take up to two minutes)" fi if ! /bin/flash.sh -r /tmp/oem-setup.rom 2> >(tee /tmp/error >&2); then ERROR=$(tail -n 1 /tmp/error | fold -s) @@ -1414,7 +1307,7 @@ else fi # flash updated firmware image - echo -e "\nAdding generated key to current firmware and re-flashing...\n" + STATUS "Adding generated key to firmware and re-flashing" if ! /bin/flash.sh /tmp/oem-setup.rom 2>/tmp/error; then ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error_die "Error flashing updated firmware image:\n\n$ERROR" @@ -1423,7 +1316,7 @@ fi ## sign files in /boot and generate checksums if [[ "$SKIP_BOOT" == "n" ]]; then - echo -e "\nUpdating checksums and signing all files in /boot...\n" + STATUS "Updating checksums and signing all files in /boot" generate_checksums fi @@ -1466,11 +1359,10 @@ while true; do break fi #Tell user to scan the QR code containing all configured secrets - echo -e "\nScan the QR code below to save the secrets to a secure location" + STATUS "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 - echo -e -n "Please confirm you have scanned the QR code above and/or written down the secrets? [y/N]: " - read -n 1 prompt_output + INPUT "Please confirm you have scanned the QR code above and/or written down the secrets? [y/N]:" -n 1 prompt_output echo if [ "$prompt_output" == "y" -o "$prompt_output" == "Y" ]; then break @@ -1478,11 +1370,26 @@ while true; do done ## all done -- reboot -whiptail --msgbox " - OEM Factory Reset / Re-Ownership has completed successfully\n\n - After rebooting, you will need to generate new TOTP/HOTP secrets\n - when prompted in order to complete the setup process.\n\n - Press Enter to reboot.\n" \ +if [ "${CONFIG_TPM_DISK_UNLOCK_KEY:-n}" = "y" ]; then + boot_next_steps="Then open: Options -> Boot Options -> Show OS boot menu +and set a new default boot option. +This step also configures/reseals the TPM Disk Unlock Key (DUK). +" +else + boot_next_steps="Then open: Options -> Boot Options -> Show OS boot menu +and set a new default boot option. +" +fi + +completion_msg="OEM Factory Reset / Re-Ownership has completed successfully + +After rebooting, you will need to generate new TOTP/HOTP secrets +when prompted in order to complete the setup process. + +${boot_next_steps} +Press Enter to reboot." + +whiptail --msgbox "${completion_msg}" \ $HEIGHT $WIDTH --title "OEM Factory Reset / Re-Ownership Complete" # Clean LUKS secrets diff --git a/initrd/bin/reboot b/initrd/bin/reboot index 490003d03..a98eb8cd2 100755 --- a/initrd/bin/reboot +++ b/initrd/bin/reboot @@ -17,13 +17,25 @@ echo u > /proc/sysrq-trigger # enter a recovery shell. Accept 'r' or 'R' to enter recovery, any other # key continues to the final reboot. if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - read -r -n 1 -s -p "Press any key to continue reboot or 'r' to go to recovery shell: " REPLY - echo + INPUT "Press any key to continue reboot or 'r' to go to recovery shell:" -r -n 1 -s REPLY if [ "$REPLY" = "r" ] || [ "$REPLY" = "R" ]; then recovery "Reboot call bypassed to go into recovery shell to debug" fi - DEBUG "DEBUG: TPM shutdown and filesystem operations complete" - read -r -p "Press Enter to issue final reboot syscall: " + DEBUG "TPM shutdown and filesystem operations complete" + INPUT "Press Enter to issue final reboot syscall:" +fi + +# On qemu-* boards, reboot is broken (q35 bug) - use poweroff instead. +# TODO: revisit when qemu q35 reboot is fixed upstream. +# Always offer a recovery shell first so state can be inspected. +if [[ "$CONFIG_BOARD_NAME" == qemu-* ]]; then + _reboot_choice="" + INPUT "QEMU board - press Enter to poweroff, or 'r' to open a recovery shell first:" -r -n 1 -s _reboot_choice + if [ "$_reboot_choice" = "r" ] || [ "$_reboot_choice" = "R" ]; then + recovery "Entering recovery shell before poweroff (QEMU board)" + fi + poweroff + exit fi # Use busybox reboot explicitly (symlinks removed to avoid conflicts) diff --git a/initrd/bin/root-hashes-gui.sh b/initrd/bin/root-hashes-gui.sh index de645c10e..a5886e450 100755 --- a/initrd/bin/root-hashes-gui.sh +++ b/initrd/bin/root-hashes-gui.sh @@ -46,7 +46,7 @@ update_root_checksums() { fi DEBUG "calculating hashes for $CONFIG_ROOT_DIRLIST_PRETTY on $ROOT_MOUNT" - echo "+++ Calculating hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY " + STATUS "Calculating hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY" # Intentional wordsplit # shellcheck disable=SC2086 (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' -print0 | xargs -0 sha256sum) >"${HASH_FILE}" @@ -99,7 +99,7 @@ check_root_checksums() { fi fi - echo "+++ Checking root hash file signature " + STATUS "Checking root hash file signature" if ! sha256sum `find /boot/kexec*.txt` | gpgv /boot/kexec.sig - > /tmp/hash_output; then ERROR=`cat /tmp/hash_output` whiptail_error --title 'ERROR: Signature Failure' \ @@ -108,7 +108,7 @@ check_root_checksums() { die 'Invalid signature' fi - echo "+++ Checking for new files in $CONFIG_ROOT_DIRLIST_PRETTY " + STATUS "Checking for new files in $CONFIG_ROOT_DIRLIST_PRETTY" (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*') | sort > /tmp/new_file_list cut -d' ' -f3- ${HASH_FILE} | sort | diff -U0 - /tmp/new_file_list > /tmp/new_file_diff || new_files_found=y if [ "$new_files_found" == "y" ]; then @@ -121,12 +121,12 @@ check_root_checksums() { echo "Type \"q\" to exit the list and return to the menu." >> /tmp/new_file_diff less /tmp/new_file_diff else - echo "+++ Verified no files added/removed " + STATUS_OK "Verified no files added or removed" fi - echo "+++ Checking hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY (this might take a while) " + STATUS "Checking hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY (this may take a while)" if (cd $ROOT_MOUNT && sha256sum -c ${HASH_FILE} > /tmp/hash_output 2>/dev/null); then - echo "+++ Verified root hashes " + STATUS_OK "Verified root hashes" valid_hash='y' unmount_root_device @@ -189,7 +189,7 @@ open_block_device_lvm() { local VG="$1" local LV MAPPER_VG MAPPER_LV name lvpath FIRST_LV_PREFERRED FIRST_LV_FALLBACK - if ! lvm vgchange -ay "$VG"; then + if ! run_lvm vgchange -ay "$VG"; then DEBUG "Can't open LVM VG: $VG" return 1 fi @@ -207,7 +207,7 @@ open_block_device_lvm() { FIRST_LV_FALLBACK="" DEBUG "LVM VG $VG has no 'root' LV, enumerating all LVs" # list LV names and prefer root-like names - for name in $(lvm lvs --noheadings -o lv_name --separator ' ' "$VG" 2>/dev/null); do + for name in $(run_lvm lvs --noheadings -o lv_name --separator ' ' "$VG" 2>/dev/null); do # thin pool/metadata and swap-like LVs are not root filesystems case "$name" in *pool*|*tmeta*|*tdata*|*tpool*|swap*) @@ -272,7 +272,7 @@ open_block_device_luks() { # volumes. This is harmless on systems without lvm installed. if command -v lvm >/dev/null 2>&1; then DEBUG "running vgscan to populate /dev/mapper after unlocking LUKS" - lvm vgscan --mknodes >/dev/null 2>&1 || true + run_lvm vgscan --mknodes >/dev/null 2>&1 || true fi open_block_device_layers "/dev/mapper/$LUKSDEV" @@ -365,7 +365,7 @@ close_block_device_lvm() { local VG="$1" # Deactivate the VG directly. This avoids recursive LV close probing noise # for LV paths that are not PVs and matches the minimal initrd workflow. - lvm vgchange -an "$VG" || \ + run_lvm vgchange -an "$VG" || \ DEBUG "Can't close LVM VG: $VG" } @@ -441,7 +441,7 @@ detect_root_device() { TRACE_FUNC - echo "+++ Detecting root device " + STATUS "Detecting root device" if [ ! -e $ROOT_MOUNT ]; then mkdir -p $ROOT_MOUNT @@ -508,7 +508,7 @@ detect_root_device() if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then DEBUG "$ROOT_DETECT_UNSUPPORTED_REASON" fi - echo "Unable to locate $ROOT_MOUNT files on any mounted disk" + warn "Unable to locate $ROOT_MOUNT files on any mounted disk" return 1 } diff --git a/initrd/bin/seal-hotpkey b/initrd/bin/seal-hotpkey index 271547b97..ad8bccd9b 100755 --- a/initrd/bin/seal-hotpkey +++ b/initrd/bin/seal-hotpkey @@ -66,8 +66,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 if ! hotp_token_info="$(hotp_verification info)"; then - echo -e "\nInsert your $HOTPKEY_BRANDING and press Enter to configure it" - read + INPUT "Insert your $HOTPKEY_BRANDING and press Enter to configure it" if ! hotp_token_info="$(hotp_verification info)"; then # don't leak key on failure shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null @@ -122,18 +121,17 @@ month_secs="$((30 * 24 * 60 * 60))" admin_pin_status=1 if [ "$((now_date - gpg_key_create_time))" -gt "$month_secs" ]; then # Remind what the default PIN was in case it still hasn't been changed - echo "Not trying default PIN ($admin_pin)" + DEBUG "Not trying default PIN ($admin_pin)" # Never consume an attempt if there are less than 3 attempts left, otherwise # attempting the default PIN could cause an unexpected lockout before getting a # chance to enter the correct PIN elif [ "$admin_pin_retries" -lt 3 ]; then - echo "Not trying default PIN ($admin_pin), only $admin_pin_retries attempt(s) left" + DEBUG "Not trying default PIN ($admin_pin): only $admin_pin_retries attempt(s) left" else - echo "Trying $prompt_message PIN ($admin_pin) to seal HOTP secret on $HOTPKEY_BRANDING..." + STATUS "Trying $prompt_message PIN to seal HOTP secret on $HOTPKEY_BRANDING" #if we deal with the nk3, say to the user that touch will be required if lsusb | grep -q "20a0:42b2"; then warn "Nitrokey 3 requires physical presence : touch the dongle when prompted" - echo 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 "$HOTPKEY_BRANDING" >/dev/null 2>&1 @@ -144,12 +142,10 @@ fi if [ "$admin_pin_status" -ne 0 ]; then # prompt user for PIN and retry - read -r -s -p $'\nEnter your '"$HOTPKEY_BRANDING $prompt_message"' PIN: ' admin_pin - echo + INPUT "Enter your $HOTPKEY_BRANDING $prompt_message PIN:" -r -s admin_pin hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$HOTPKEY_BRANDING" if [ $? -ne 0 ]; then - read -r -s -p $'\nError setting HOTP secret, re-enter '"$prompt_message"' PIN and try again: ' admin_pin - echo + INPUT "Error setting HOTP secret, re-enter $prompt_message PIN and try again:" -r -s admin_pin if ! hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$HOTPKEY_BRANDING"; then # don't leak key on failure shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null @@ -188,7 +184,6 @@ echo $HOTPKEY_BRANDING >$HOTP_KEY || #|| die "Unable to create hotp counter file" mount -o remount,ro /boot -echo -e "\n$HOTPKEY_BRANDING initialized successfully. Press Enter to continue." -read +STATUS_OK "$HOTPKEY_BRANDING initialized successfully" exit 0 diff --git a/initrd/bin/seal-totp b/initrd/bin/seal-totp index 3c593d697..fcc59eda8 100755 --- a/initrd/bin/seal-totp +++ b/initrd/bin/seal-totp @@ -49,8 +49,21 @@ DEBUG "Sealing TOTP without PCR6 involvement (LUKS header consistency is not fir DEBUG "Sealing TOTP with actual state of PCR7 (User injected stuff in cbfs)" tpmr pcrread -a 7 "$pcrf" #Make sure we clear the TPM Owner Password from memory in case it failed to be used to seal TOTP -tpmr seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSWORD" || - die "Unable to write sealed secret to NVRAM from seal-totp" + +# if the board has TPM2 tools, check for the primary handle before +# attempting to seal; a missing handle is the most common reason for +# failure and we want to give the same message as unseal-totp. +if [ "$CONFIG_TPM2_TOOLS" = "y" ] && [ ! -f "/tmp/secret/primary.handle" ]; then + die "Unable to seal TOTP secret; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) then generate a new TOTP secret." +fi + +# perform sealing via tpmr. Failures may indicate missing primary handle +# or other TPM state issues. Avoid DO_WITH_DEBUG so interactive prompts +# (TPM owner password on TPM1) are not hidden from the user. +if ! tpmr seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSWORD"; then + # tpmr already logged details; guide user generically to reset TPM + die "Unable to seal TOTP secret to TPM NVRAM; reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) and try again." +fi #Make sure we clear TPM TOTP sealed if we succeed to seal TOTP shred -n 10 -z -u "$TOTP_SEALED" 2>/dev/null @@ -59,5 +72,5 @@ url="otpauth://totp/$HOST?secret=$secret" DEBUG "TOTP secret output on screen (both URL and QR code)" qrenc "$url" -echo "TOTP secret for manual input (device without camera): $secret" +STATUS "TOTP secret for manual input (device without camera): $secret" secret="" diff --git a/initrd/bin/tpm-reset b/initrd/bin/tpm-reset index 5049bea02..c88919c28 100755 --- a/initrd/bin/tpm-reset +++ b/initrd/bin/tpm-reset @@ -1,9 +1,7 @@ #!/bin/bash . /etc/functions -echo '*****' -echo '***** WARNING: This will erase all keys and secrets from the TPM' -echo '*****' +NOTE "This will erase all keys and secrets from the TPM" prompt_new_owner_password diff --git a/initrd/bin/tpmr b/initrd/bin/tpmr index ac803f8b6..de57d30cf 100755 --- a/initrd/bin/tpmr +++ b/initrd/bin/tpmr @@ -196,11 +196,11 @@ $0 ~ pcr { replay_pcr() { TRACE_FUNC if [ -z "$2" ]; then - echo >&2 "No PCR number passed" + warn "No PCR number passed" return fi if [ "$2" -ge 8 ]; then - echo >&2 "Illegal PCR number ($2)" + warn "Illegal PCR number ($2)" return fi local log=$(cbmem -L) @@ -257,10 +257,8 @@ tpm2_extend() { esac done tpm2 pcrextend "$index:sha256=$hash" - INFO $(tpm2 pcrread "sha256:$index" 2>&1) - - TRACE_FUNC - DEBUG "TPM: Extended PCR[$index] with hash $hash" + LOG "TPM: PCR[$index] after extend: $(tpm2 pcrread "sha256:$index" 2>&1)" + LOG "TPM: Extended PCR[$index] with hash $hash" } tpm2_counter_read() { @@ -281,6 +279,8 @@ tpm2_counter_read() { tpm2_counter_inc() { TRACE_FUNC + local index pwd + local inc_args=() while true; do case "$1" in -ix) @@ -296,7 +296,10 @@ tpm2_counter_inc() { ;; esac done - tpm2 nvincrement "0x$index" >/dev/console + if [ -n "$pwd" ]; then + inc_args=(-C o -P "$(tpm2_password_hex "$pwd")") + fi + tpm2 nvincrement "${inc_args[@]}" "0x$index" >/dev/console echo "$index: $(tpm2 nvread 0x$index | xxd -pc8)" } @@ -315,13 +318,15 @@ tpm1_counter_create() { DEBUG "tpm1 stderr: $line" done <"$TMP_ERR_FILE" rm -f "$TMP_ERR_FILE" - die "Unable to create counter from tpm1_counter_create" + die "Unable to create counter from tpm1_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." fi rm -f "$TMP_ERR_FILE" } tpm2_counter_create() { TRACE_FUNC + pwd="" # owner password from argument + label="" # label argument while true; do case "$1" in -pwdc) @@ -337,15 +342,33 @@ tpm2_counter_create() { ;; esac done - prompt_tpm_owner_password + + # if caller supplied a password use it, otherwise prompt / use cache + if [ -n "$pwd" ]; then + tpm_owner_password="$pwd" + # cache it so other functions can reuse without prompting + mkdir -p "$SECRET_DIR" || true + echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password 2>/dev/null || true + else + prompt_tpm_owner_password + fi + rand_index="1$(dd if=/dev/urandom bs=1 count=3 2>/dev/null | xxd -pc3)" - tpm2 nvdefine -C o -s 8 -a "ownerread|authread|authwrite|nt=1" \ - -P "$(tpm2_password_hex "$(cat "/tmp/secret/tpm_owner_password")")" "0x$rand_index" >/dev/null 2>&1 || - { - DEBUG "Failed to create counter from tpm2_counter_create. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - die "Unable to create counter from tpm2_counter_create" - } + # capture stderr to a temp file for visibility + TMP_ERR_FILE=$(mktemp) + if ! tpm2 nvdefine -C o -s 8 -a "ownerread|authread|authwrite|nt=1" \ + -P "$(tpm2_password_hex "$tpm_owner_password")" "0x$rand_index" \ + 2>"$TMP_ERR_FILE" >/dev/null; then + DEBUG "Failed to create counter from tpm2_counter_create. Wiping /tmp/secret/tpm_owner_password" + shred -n 10 -z -u /tmp/secret/tpm_owner_password + # log the TPM2 stderr output + while IFS= read -r line; do + DEBUG "tpm2 stderr: $line" + done <"$TMP_ERR_FILE" + rm -f "$TMP_ERR_FILE" + die "Unable to create counter from tpm2_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + rm -f "$TMP_ERR_FILE" echo "$rand_index: (valid after an increment)" } @@ -439,6 +462,10 @@ tpm2_seal() { tpm_password="$7" # Owner password - will prompt if needed and not empty # TPM Owner Password is always needed for TPM2. + # bail early if the TPM hasn't been reset and we lack a primary handle. + if [ ! -f "$PRIMARY_HANDLE_FILE" ]; then + die "Unable to seal secret from TPM; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) and try again." + fi mkdir -p "$SECRET_DIR" bname="$(basename $file)" @@ -476,33 +503,50 @@ tpm2_seal() { # (The default is to allow either policy auth _or_ password auth. In # this case the policy includes the password, and we don't want to allow # the password on its own.) - tpm2 create -Q -C "$PRIMARY_HANDLE_FILE" \ + # mask hex of authorization password when supplied (it will be the + # last parameter if CREATE_PASS_ARGS is non-empty) + # tpm2 create: -u = public area output, -r = private (encrypted) area output. + # File extensions match: .pub for the public area, .priv for the private area. + DO_WITH_DEBUG --mask-position 18 tpm2 create -Q -C "$PRIMARY_HANDLE_FILE" \ -i "$file" \ - -u "$SECRET_DIR/$bname.priv" \ - -r "$SECRET_DIR/$bname.pub" \ + -u "$SECRET_DIR/$bname.pub" \ + -r "$SECRET_DIR/$bname.priv" \ -L "$AUTH_POLICY" \ -S "$DEC_SESSION_FILE" \ -a "fixedtpm|fixedparent|adminwithpolicy" \ "${CREATE_PASS_ARGS[@]}" - tpm2 load -Q -C "$PRIMARY_HANDLE_FILE" \ - -u "$SECRET_DIR/$bname.priv" -r "$SECRET_DIR/$bname.pub" \ + # tpm2 load: -u = public area input, -r = private (encrypted) area input. + DO_WITH_DEBUG tpm2 load -Q -C "$PRIMARY_HANDLE_FILE" \ + -u "$SECRET_DIR/$bname.pub" -r "$SECRET_DIR/$bname.priv" \ -c "$SECRET_DIR/$bname.seal.ctx" - prompt_tpm_owner_password - # remove possible data occupying this handle - tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_password")" \ - -c "$handle" 2>/dev/null || true - DO_WITH_DEBUG --mask-position 6 \ - tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_password")" \ - -c "$SECRET_DIR/$bname.seal.ctx" "$handle" || - { - DEBUG "Failed to write sealed secret to NVRAM from tpm2_seal. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - die "Unable to write sealed secret to TPM NVRAM" - } + # Retry loop: evictcontrol requires the TPM owner password; allow the + # user to re-enter it if it is wrong rather than dying immediately. + local evict_attempts=0 + while true; do + evict_attempts=$((evict_attempts + 1)) + prompt_tpm_owner_password + # remove possible data occupying this handle (failure is expected when + # the handle doesn't exist yet, so ignore errors) + DO_WITH_DEBUG --mask-position 6 tpm2 evictcontrol -Q -C o \ + -P "$(tpm2_password_hex "$tpm_owner_password")" \ + -c "$handle" 2>/dev/null || true + if DO_WITH_DEBUG --mask-position 6 \ + tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_password")" \ + -c "$SECRET_DIR/$bname.seal.ctx" "$handle"; then + break + fi + DEBUG "tpm2_seal: evictcontrol failed (attempt $evict_attempts), wiping cached TPM owner password" + shred -n 10 -z -u /tmp/secret/tpm_owner_password 2>/dev/null + if [ "$evict_attempts" -ge 3 ]; then + die "Unable to write sealed secret to TPM NVRAM after $evict_attempts attempts" + fi + warn "TPM owner password incorrect or TPM error - please try again (attempt $evict_attempts of 3)" + done } tpm1_seal() { TRACE_FUNC + local tmp_err_write tmp_err_define tmp_err_write_after_define file="$1" index="$2" pcrl="$3" #0,1,2,3,4,5,6,7 (does not include algorithm prefix) @@ -516,7 +560,7 @@ tpm1_seal() { POLICY_ARGS=() - DEBUG "tpm1_seal arguments: file=$file index=$index pcrl=$pcrl pcrf=$pcrf sealed_size=$sealed_size pass=$(mask_param "$pass") tpm_password=$(mask_param "$tpm_password")" + DEBUG "tpm1_seal arguments: file=$file index=$index pcrl=$pcrl pcrf=$pcrf sealed_size=$sealed_size pass=$(mask_param "$pass") tpm_owner_password=$(mask_param "$tpm_owner_password")" # If a password was given, add it to the policy arguments if [ "$pass" ]; then @@ -539,29 +583,54 @@ tpm1_seal() { -of "$sealed_file" \ -hk 40000000 \ "${POLICY_ARGS[@]}" + DEBUG "tpm1_seal: sealed blob created at $sealed_file (size=$(wc -c <"$sealed_file" 2>/dev/null || echo 0) bytes), target nv index=$index" # try it without the TPM Owner Password first - if ! tpm nv_writevalue -in "$index" -if "$sealed_file"; then + tmp_err_write="$(mktemp)" + if ! tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write"; then + while IFS= read -r line; do + DEBUG "tpm1_seal nv_writevalue(pre-define) stderr: $line" + done <"$tmp_err_write" + if grep -qi 'illegal index' "$tmp_err_write"; then + DEBUG "tpm1_seal: nv index $index is not defined yet (Illegal index); attempting nv_definespace" + fi + rm -f "$tmp_err_write" # to create an nvram space we need the TPM Owner Password # and the TPM physical presence must be asserted. # # The permissions are 0 since there is nothing special # about the sealed file tpm physicalpresence -s || - warn "Unable to assert physical presence" + { + warn "Unable to assert physical presence" + } prompt_tpm_owner_password - tpm nv_definespace -in "$index" -sz "$sealed_size" \ - -pwdo "$tpm_owner_password" -per 0 || + tmp_err_define="$(mktemp)" + if ! DO_WITH_DEBUG --mask-position 7 tpm nv_definespace -in "$index" -sz "$sealed_size" \ + -pwdo "$tpm_owner_password" -per 0 2>"$tmp_err_define"; then + while IFS= read -r line; do + DEBUG "tpm1_seal nv_definespace stderr: $line" + done <"$tmp_err_define" warn "Unable to define TPM NVRAM space; trying anyway" + fi + rm -f "$tmp_err_define" - tpm nv_writevalue -in "$index" -if "$sealed_file" || + tmp_err_write_after_define="$(mktemp)" + tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write_after_define" || { + while IFS= read -r line; do + DEBUG "tpm1_seal nv_writevalue(post-define) stderr: $line" + done <"$tmp_err_write_after_define" + rm -f "$tmp_err_write_after_define" DEBUG "Failed to write sealed secret to NVRAM from tpm1_seal. Wiping /tmp/secret/tpm_owner_password" shred -n 10 -z -u /tmp/secret/tpm_owner_password die "Unable to write sealed secret to TPM NVRAM" } + rm -f "$tmp_err_write_after_define" + else + rm -f "$tmp_err_write" fi } @@ -589,7 +658,7 @@ tpm2_unseal() { # can't do anything without a primary handle. if [ ! -f "$PRIMARY_HANDLE_FILE" ]; then DEBUG "tpm2_unseal: No primary handle, cannot attempt to unseal" - warn "No TPM primary handle. You must reset the TPM to seal secret to TPM NVRAM" + warn "No TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) before attempting to unseal a secret from TPM NVRAM" exit 1 fi @@ -624,6 +693,7 @@ tpm2_unseal() { tpm1_unseal() { TRACE_FUNC + local tmp_err_read index="$1" pcrl="$2" sealed_size="$3" @@ -639,22 +709,57 @@ tpm1_unseal() { rm -f "$sealed_file" - DO_WITH_DEBUG tpm nv_readvalue \ + # Read the sealed blob from NVRAM. `tpm nv_readvalue` prints a + # spurious warning about index size that we don't want on the console; + # capture stderr in the debug log instead. Password prompts are written + # directly to the tty and are unaffected by this redirection. + tmp_err_read="$(mktemp)" + if ! DO_WITH_DEBUG tpm nv_readvalue \ -in "$index" \ -sz "$sealed_size" \ - -of "$sealed_file" || - die "Unable to read sealed file from TPM NVRAM" + -of "$sealed_file" \ + 2>"$tmp_err_read"; then + while IFS= read -r line; do + DEBUG "tpm1_unseal nv_readvalue stderr: $line" + done <"$tmp_err_read" + rm -f "$tmp_err_read" + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal tpm1_unseal failure: unable to read sealed file from TPM NVRAM" + return 1 + fi + die "Unable to read sealed file from TPM NVRAM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." + fi + rm -f "$tmp_err_read" PASS_ARGS=() if [ "$pass" ]; then PASS_ARGS=(-pwdd "$pass") fi - tpm unsealfile \ - -if "$sealed_file" \ - -of "$file" \ - "${PASS_ARGS[@]}" \ - -hk 40000000 + if [ -n "$pass" ]; then + if ! DO_WITH_DEBUG --mask-position 7 tpm unsealfile \ + -if "$sealed_file" \ + -of "$file" \ + "${PASS_ARGS[@]}" \ + -hk 40000000; then + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal tpm1_unseal failure: unable to unseal TPM NVRAM blob" + return 1 + fi + die "Unable to unseal secret from TPM NVRAM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." + fi + else + if ! DO_WITH_DEBUG tpm unsealfile \ + -if "$sealed_file" \ + -of "$file" \ + -hk 40000000; then + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal tpm1_unseal failure: unable to unseal TPM NVRAM blob" + return 1 + fi + die "Unable to unseal secret from TPM NVRAM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." + fi + fi } # cache_owner_password @@ -692,25 +797,30 @@ tpm2_reset() { fi # 3. Re-own the TPM for Heads: set new owner and endorsement auth. - if ! DO_WITH_DEBUG tpm2 changeauth -c owner "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + # don't echo the owner password in debug logs + # hide the owner auth hex (argument index 4) + if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c owner "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to set owner auth" return 1 fi - if ! DO_WITH_DEBUG tpm2 changeauth -c endorsement "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + # mask endorsement password too (argument index 4) + if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c endorsement "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to set endorsement auth" return 1 fi # 4. Create and persist Heads primary key. - if ! DO_WITH_DEBUG tpm2 createprimary -C owner -g sha256 -G "${CONFIG_PRIMARY_KEY_TYPE:-rsa}" \ + # hide owner password during primary creation (argument index 11) + if ! DO_WITH_DEBUG --mask-position 11 tpm2 createprimary -C owner -g sha256 -G "${CONFIG_PRIMARY_KEY_TYPE:-rsa}" \ -c "$SECRET_DIR/primary.ctx" \ -P "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to create primary" return 1 fi - if ! DO_WITH_DEBUG tpm2 evictcontrol -C owner -c "$SECRET_DIR/primary.ctx" "$PRIMARY_HANDLE" \ + # and hide password when evicting the primary handle (argument index 8) + if ! DO_WITH_DEBUG --mask-position 8 tpm2 evictcontrol -C owner -c "$SECRET_DIR/primary.ctx" "$PRIMARY_HANDLE" \ -P "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to persist primary" shred -u "$SECRET_DIR/primary.ctx" >/dev/null 2>&1 @@ -803,7 +913,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. - echo "Locking TPM2 platform hierarchy..." + STATUS "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" @@ -819,8 +929,7 @@ tpm2_shutdown() { } if [ "$CONFIG_TPM" != "y" ]; then - echo >&2 "No TPM!" - exit 1 + die "No TPM!" fi # TPM1 - most commands forward directly to tpm, but some are still wrapped for @@ -883,8 +992,10 @@ if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then kexec_finalize) ;; # Nothing on TPM1. shutdown) ;; # Nothing on TPM1. *) - DEBUG "Direct translation from tpmr to tpm1 call" - DO_WITH_DEBUG exec tpm "$@" + # Keep passthrough raw here: callers decide whether to wrap with + # DO_WITH_DEBUG (and --mask-position when passing secrets). + DEBUG "Direct translation from tpmr to tpm1 call" + exec tpm "$@" ;; esac exit 0 @@ -940,7 +1051,6 @@ shutdown) tpm2_shutdown "$@" ;; *) - echo "Command $subcmd not wrapped!" - exit 1 + die "Command $subcmd not wrapped!" ;; esac diff --git a/initrd/bin/uefi-init b/initrd/bin/uefi-init index 8f9de1b34..a16f31999 100755 --- a/initrd/bin/uefi-init +++ b/initrd/bin/uefi-init @@ -13,7 +13,7 @@ CONFIG_GUID="74696e69-6472-632e-7069-6f2f75736572" GUID=`uefi -l | grep "^$CONFIG_GUID"` if [ -n "GUID" ]; then - echo "Loading $GUID from ROM" + STATUS "Loading $GUID from ROM" TMPFILE=/tmp/uefi.$$ uefi -r $GUID | gunzip -c > $TMPFILE \ || die "Failed to read config GUID from ROM" diff --git a/initrd/bin/unseal-hotp b/initrd/bin/unseal-hotp index 5fae80da9..ebc9fdbf2 100755 --- a/initrd/bin/unseal-hotp +++ b/initrd/bin/unseal-hotp @@ -6,6 +6,15 @@ HOTP_SECRET="/tmp/secret/hotp.key" HOTP_COUNTER="/boot/kexec_hotp_counter" +fail_unseal() { + TRACE_FUNC + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal unseal-hotp failure: $*" + return 1 + fi + die "$*" +} + mount_boot_or_die() { TRACE_FUNC # Mount local disk if it is not already mounted @@ -31,23 +40,37 @@ mount_boot_or_die #if HOTP_COUNTER is not present, bail out if [ ! -f $HOTP_COUNTER ]; then - die "HOTP counter file not found. If you just reinstalled an OS, you need to reseal the HOTP secret" + fail_unseal "HOTP counter file not found. If you just reinstalled an OS, you need to reseal the HOTP secret" || exit 1 fi # Read the counter from the file counter_value=$(cat $HOTP_COUNTER 2>/dev/null) if [ "$counter_value" == "" ]; then - die "Unable to read HOTP counter" + fail_unseal "Unable to read HOTP counter" || exit 1 fi #counter_value=$(printf "%d" 0x${counter_value}) if [ "$CONFIG_TPM" = "y" ]; then + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + # ensure primary handle exists before any TPM2 operation, to keep + # messaging consistent with unseal-totp + if [ ! -f "/tmp/secret/primary.handle" ]; then + fail_unseal "Unable to unseal HOTP secret from TPM; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI)." || exit 1 + fi + fi DEBUG "Unsealing HOTP secret reuses TOTP sealed secret..." - tpmr unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET" || die "Unable to unseal HOTP secret" + # debug unseal too; no password argument + if ! DO_WITH_DEBUG tpmr 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 + else + 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 else # without a TPM, generate a secret based on the SHA-256 of the ROM - secret_from_rom_hash >"$HOTP_SECRET" || die "Reading ROM failed" + secret_from_rom_hash >"$HOTP_SECRET" || fail_unseal "Reading ROM failed" || exit 1 fi # Truncate the secret if it is longer than the maximum HOTP secret @@ -55,7 +78,7 @@ truncate_max_bytes 20 "$HOTP_SECRET" if ! hotp $counter_value <"$HOTP_SECRET"; then shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null - die 'Unable to compute HOTP hash?' + fail_unseal 'Unable to compute HOTP hash?' || exit 1 fi shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null @@ -71,7 +94,7 @@ mount -o remount,rw /boot DEBUG "Incrementing HOTP counter under $HOTP_COUNTER" counter_value=$(expr $counter_value + 1) echo $counter_value >$HOTP_COUNTER || - die "Unable to create hotp counter file" + fail_unseal "Unable to create hotp counter file" || exit 1 mount -o remount,ro /boot exit 0 diff --git a/initrd/bin/unseal-totp b/initrd/bin/unseal-totp index da61deeea..99ce131cc 100755 --- a/initrd/bin/unseal-totp +++ b/initrd/bin/unseal-totp @@ -5,17 +5,60 @@ TOTP_SECRET="/tmp/secret/totp.key" +fail_unseal() { + TRACE_FUNC + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal unseal-totp failure: $*" + return 1 + fi + die "$*" +} + +fail_unseal_reset_required() { + TRACE_FUNC + # A TPM-side unseal failure generally indicates that reset/re-ownership is + # required before allowing reseal/generate workflows again. + set_tpm_reset_required "$*" "unseal-totp:fail_unseal_reset_required" + DEBUG "fail_unseal_reset_required: reason='$*'" + fail_unseal "$@" +} + TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then - DO_WITH_DEBUG --mask-position 5 \ - tpmr unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET" || - die "Unable to unseal TOTP secret from TPM" + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + # if we are talking to TPM2, ensure the primary handle exists; TPM1 + # does not have the concept, so skip the check. + if [ ! -f "/tmp/secret/primary.handle" ]; then + fail_unseal_reset_required "Unable to unseal TOTP secret from TPM; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI)." || exit 1 + fi + # show unseal invocation; there is no secret argument to mask + if ! DO_WITH_DEBUG \ + tpmr unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET"; then + # A TPM2 unseal failure with primary handle present is commonly a + # policy/PCR mismatch (for example after firmware updates). Keep this + # recoverable via reseal and do not force reset-required marker. + fail_unseal "Unable to unseal TOTP secret from TPM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 + fi + else + # TPM1 path: after reset/re-ownership, unseal failures here are best + # handled by resealing the secret from the GUI flow. + if ! DO_WITH_DEBUG tpmr unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET"; then + fail_unseal "Unable to unseal TOTP secret from TPM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 + fi + fi +fi + +if [ ! -s "$TOTP_SECRET" ]; then + fail_unseal "Unable to unseal TOTP secret from TPM; secret file $TOTP_SECRET is missing or empty. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 fi -if ! DO_WITH_DEBUG totp -q <"$TOTP_SECRET"; then +# Run totp without DO_WITH_DEBUG: stdout is the TOTP code and must not be +# logged (security hazard - code in debug.log could be used to verify OTPs). +# Errors (stderr) are still captured for debugging. +if ! totp -q <"$TOTP_SECRET" 2> >(SINK_LOG "totp stderr"); then shred -n 10 -z -u "$TOTP_SECRET" 2>/dev/null - die 'Unable to compute TOTP hash?' + fail_unseal 'Unable to compute TOTP hash?' || exit 1 fi shred -n 10 -z -u "$TOTP_SECRET" 2>/dev/null diff --git a/initrd/bin/usb-autoboot.sh b/initrd/bin/usb-autoboot.sh index af7a0ac06..780a5068b 100755 --- a/initrd/bin/usb-autoboot.sh +++ b/initrd/bin/usb-autoboot.sh @@ -34,14 +34,13 @@ while read -u 4 -r USB_BLOCK_DEVICE; do USB_DEFAULT_BOOT="$(parse_boot_options /media | head -1)" if [ -n "$USB_DEFAULT_BOOT" ]; then # Boot automatically, unless the user interrupts. - echo -e "\n\n" - echo "Found bootable USB: $(echo "$USB_DEFAULT_BOOT" | cut -d '|' -f 1)" + STATUS "Found bootable USB: $(echo "$USB_DEFAULT_BOOT" | cut -d '|' -f 1)" if ! pause_automatic_boot; then # User interrupted, go to boot menu umount /media exit 0 fi - echo -e "\n\nBooting from USB...\n\n" + STATUS "Booting from USB" kexec-boot -b /media -e "$USB_DEFAULT_BOOT" # If kexec-boot returned, the boot obviously did not occur, # return nonzero below so the normal OS boot will continue. diff --git a/initrd/bin/wget-measure.sh b/initrd/bin/wget-measure.sh index 8e7e9e7bd..c267f9956 100755 --- a/initrd/bin/wget-measure.sh +++ b/initrd/bin/wget-measure.sh @@ -2,12 +2,6 @@ # get a file and extend a TPM PCR . /etc/functions -die() { - TRACE_FUNC - echo >&2 "$@" - exit 1 -} - INDEX="$1" URL="$2" diff --git a/initrd/etc/functions b/initrd/etc/functions index 7872e101e..1ea46a75a 100644 --- a/initrd/etc/functions +++ b/initrd/etc/functions @@ -18,100 +18,418 @@ esac # ------- Start of functions coming from /etc/ash_functions +# die - fatal error: print bold red message, wait for Enter, then exit 1. +# +# Console color: bold red (\033[1;31m). +# Red is the universal "error/danger" signal; the "!!! ERROR:" text prefix +# carries the same meaning for users who cannot distinguish red from other +# colors, so color is an enhancement rather than the sole signal. +# debug.log and /dev/kmsg receive plain text (no ANSI). +# Always visible in all output modes. die() { TRACE_FUNC - #TODO: add colors to output, here red for ERROR? + # Always log to debug.log regardless of output mode - fatal errors must be + # captured for post-mortem analysis even when the console is suppressed. + echo "!!! ERROR: $* !!!" >> /tmp/debug.log if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - echo -e " !!! ERROR: $* !!!" | tee -a /tmp/debug.log /dev/kmsg >/dev/null - else - echo -e "!!! ERROR: $* !!!" >&2 + # debug mode: also route to kmsg for ordering with other debug output + echo "!!! ERROR: $* !!!" >/dev/kmsg 2>/dev/null || true fi + # Always show on console with bold red regardless of output mode. + # /dev/console = kernel console (follows the console= kernel parameter), + # so it reaches whatever output the kernel was configured for — serial, + # framebuffer, BMC — without requiring any process setup and without + # polluting stdout or stderr so callers never need to care about redirections. + echo -e "\033[1;31m!!! ERROR: $* !!!\033[0m" >/dev/console 2>/dev/null # ask user to press Enter prior to exit - read -r -p $'Press Enter to continue...\n\n' + INPUT "Press Enter to continue..." exit 1 } -# Use warn only for output that indicates a _likely_ problem, is _actionable_ to -# correct, and when we are able to continue with degraded functionalty. -# Do not overuse this! See doc/logging.md. +# warn - a likely problem the user should act on. +# +# Use warn when ALL of the following are true: +# - There is a _likely_ problem (not a rare or remote possibility) +# - We are able to continue, possibly with degraded functionality +# - The warning is _actionable_: there is a reasonable change the user +# can make to silence it +# Do NOT use warn for: +# - Informational messages about normal operations (use INFO) +# - Rare or unlikely edge cases that are not actionable (use DEBUG) +# - Fatal errors where we cannot continue (use die) +# +# Console color: bold yellow (\033[1;33m). +# Yellow is the most universally perceptible alert color across all common +# color-deficiency types: it is bright and distinct for deuteranopes, +# protanopes, and tritanopes alike. The "*** WARNING:" text prefix carries +# the meaning independently of color. +# debug.log and /dev/kmsg receive plain text (no ANSI). +# +# Output modes (always visible in all modes): +# Quiet (CONFIG_QUIET_MODE=y): /dev/console + debug.log +# Info (CONFIG_QUIET_MODE=n): /dev/console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log +# +# Do not overuse - warn only has value when it is infrequent enough that +# users still notice and act on it. See doc/logging.md. warn() { TRACE_FUNC - #TODO: add colors to output, here yellow for WARNING? + # Always write to debug.log - complete audit trail regardless of mode. + echo >> /tmp/debug.log + echo " *** WARNING: $* ***" >> /tmp/debug.log + echo >> /tmp/debug.log + # Bold yellow to /dev/console in all modes. + # /dev/console = kernel console (follows console= kernel parameter): reaches + # serial, framebuffer, BMC — no process setup needed, callers never need to + # care about redirections (e.g. 2>/tmp/whiptail). + echo >/dev/console 2>/dev/null + echo -e "\033[1;33m *** WARNING: $* ***\033[0m" >/dev/console 2>/dev/null + echo >/dev/console 2>/dev/null if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - echo -e " *** WARNING: $* ***" | tee -a /tmp/debug.log /dev/kmsg >/dev/null - else - echo -e " *** WARNING: $* ***" >&2 + # debug mode: also route to kmsg for ordering with other debug output (no ANSI - kmsg strips it) + echo " *** WARNING: $* ***" | tee -a /dev/kmsg >/dev/null fi sleep 1 } -# Use DEBUG to track decisions made in script/function logic and the context -# relating to those decisions. Generally, focus on decision points, because -# straight-line execution can usually be followed without further tracing. See -# doc/logging.md. +# DEBUG - decision points and developer-relevant context. +# +# Use DEBUG to show the information that influences logical decisions and the +# result of those decisions. Focus on if/else/case branches: what information +# led to the branch, and which branch was taken. +# Use DO_WITH_DEBUG to capture command invocations (command+args at DEBUG +# level, stdout/stderr at LOG level) rather than calling DEBUG directly. +# Messages may freely include internal variable names, file paths, and +# technical subsystem details - this level targets Heads developers only. +# Do NOT use DEBUG for: +# - Command output or dumps of uncontrolled length (use LOG or DO_WITH_DEBUG) +# - Actions a non-developer user would understand (use INFO) +# +# Console color: none (plain text only; targets developers reading raw output). +# debug.log and /dev/kmsg receive plain text (no ANSI). +# Console output goes to /dev/console (the kernel console, follows the +# console= kernel parameter) so it reaches serial, framebuffer, BMC, etc. +# without requiring any process setup and without polluting stdout or stderr +# so callers never need to care about redirections. +# +# Output modes: +# Quiet (CONFIG_QUIET_MODE=y): debug.log only (no console) +# Info (CONFIG_QUIET_MODE=n): debug.log only (no console) +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log +# +# See doc/logging.md. DEBUG() { + # Always write to debug.log - debug.log is a complete audit trail regardless of mode. + echo "DEBUG: $*" >> /tmp/debug.log if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + # debug mode: also echo to /dev/console and kmsg. # fold -s -w 960 will wrap lines at 960 characters on the last space before the limit - echo "DEBUG: $*" | fold -s -w 960 | while read line; do - echo "$line" | tee -a /tmp/debug.log /dev/kmsg >/dev/null + echo "DEBUG: $*" | fold -s -w 960 | while IFS= read -r line; do + echo "$line" | tee -a /dev/kmsg >/dev/null + echo "$line" >/dev/console 2>/dev/null done fi } -# Use TRACE to trace control flow through Heads. This is usually called by -# TRACE_FUNC, but you can use it to additionally trace parameter values, etc. -# Usually, use this to display unprocessed parameters that your script or -# function received. For information about the logic occurring in your script -# or function, use DEBUG. See doc/logging.md. +# TRACE / TRACE_FUNC - execution flow through scripts and functions. +# +# TRACE_FUNC MUST be called as the first line of every script and function. +# It emits the full call chain (including cross-process subprocess boundaries) +# leading to the current location: +# TRACE: caller(file:line) -> ... -> current_func(file:line) +# Use TRACE directly (in addition to TRACE_FUNC) only to show the raw +# unprocessed parameters received by a script or function from its caller. +# Do NOT use TRACE for logic or decisions inside the function - use DEBUG. +# Do NOT use TRACE to show processed/interpreted values - use DEBUG. +# +# Console color: none (plain text only; targets developers reading raw output). +# debug.log and /dev/kmsg receive plain text (no ANSI). +# Console output goes to /dev/console (the kernel console, follows the +# console= kernel parameter) so it reaches serial, framebuffer, BMC, etc. +# without requiring any process setup and without polluting stdout or stderr +# so callers never need to care about redirections. +# +# Output modes: +# Quiet (CONFIG_QUIET_MODE=y): debug.log only (no console) +# Info (CONFIG_QUIET_MODE=n): debug.log only (no console) +# Debug (CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y): /dev/console + debug.log +# +# See doc/logging.md. TRACE() { + # Always write to debug.log - debug.log is a complete audit trail regardless of mode. + echo "TRACE: $*" >> /tmp/debug.log if [ "$CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" = "y" ]; then - echo "TRACE: $*" | tee -a /tmp/debug.log /dev/kmsg >/dev/null + # tracing mode: also echo to /dev/console and kmsg. + echo "TRACE: $*" | tee -a /dev/kmsg >/dev/null + echo "TRACE: $*" >/dev/console 2>/dev/null fi } -# Use NOTE to explain behaviors that are _likely_ to be unexpected or confusing. -# Unlike INFO, this cannot be hidden, as the explained behavior would be too -# confusing without this output. -# Don't overuse this - too much NOTE output will cause users to ignore it. See -# doc/logging.md. +# NOTE - explains behaviors that are _likely_ to be unexpected or confusing +# to users new to Heads. +# +# Use NOTE only when the behavior is so unexpected that users need this +# explanation to make sense of what they are seeing. Examples: +# - An automatic reboot the user did not explicitly request +# - A GPG PIN prompt appearing at a point the user would not anticipate +# - A required action before the next step can proceed +# Unlike INFO, NOTE cannot be hidden: it always appears in every output mode. +# NOTE sleeps after printing to bring the message to the user's awareness +# and ensure it is not scrolled past before they can read it. +# Do NOT overuse: prefer INFO if the behavior is only sometimes unexpected. +# Too many NOTE messages train users to ignore them, defeating their purpose. +# +# Console color: italic white NOTE: prefix (\033[3;37m). +# White is the highest-contrast neutral hue on dark consoles (VGA/serial). +# Italic distinguishes NOTE from bold STATUS/warn without imposing a semantic +# hue, satisfying WCAG 1.4.1 (color is not the sole signal; the NOTE: prefix +# and surrounding blank lines + 3-second sleep carry meaning independently). +# debug.log receives plain text (no ANSI). +# +# Output modes (always visible in all modes): +# Quiet (CONFIG_QUIET_MODE=y): console + debug.log +# Info (CONFIG_QUIET_MODE=n): console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): console + debug.log +# +# See doc/logging.md. NOTE() { - #TODO: add colors to output, here blue for NOTE? - - # Make sure the user sees this message: seperate it from the rest of the output - echo - echo "NOTE:" "$@" | tee -a /tmp/debug.log - echo - - # Sleep for a second to give the user time to read the message - sleep 1 -} - -# Use INFO for contextual information that might make sense to non-developers, -# but that isn't generally needed to use Heads. Non-developers might use this -# level to troubleshoot basic problems, so it must make sense without deep -# knowledge of how Heads works. See doc/logging.md. + # Console: italic white NOTE: prefix, blank lines before/after, to /dev/console. + # /dev/console = kernel console (follows console= kernel parameter): reaches + # serial, framebuffer, BMC — no process setup needed, callers never need to + # care about redirections. + echo >/dev/console 2>/dev/null + echo -e "\033[3;37mNOTE:\033[0m $*" >/dev/console 2>/dev/null + echo >/dev/console 2>/dev/null + # Log file: plain text - no ANSI codes in debug.log; echo -e so \n in the + # message produces real newlines in the log (multi-line NOTE support). + echo -e "NOTE: $*" >> /tmp/debug.log + + # Sleep to bring the message to the user's awareness: NOTE is infrequent + # and important enough that the user must not scroll past it unread + sleep 3 +} + +# STATUS - announces an action currently in progress or just completed. +# +# Use STATUS for progress and action announcements that all users must see +# regardless of output mode. Examples: +# - An action starting or running: "Verifying ISO", "Building initrd", +# "Calculating hashes - this may take a while" +# - Completion of a security-relevant operation: "ISO signature verified", +# "LUKS device unlocked", "Verified root hashes" +# - A boot-path milestone: "Executing default boot for $name" +# +# Unlike INFO, STATUS is always visible in all output modes - a user in +# quiet mode must still be able to see what Heads is actively doing. +# Unlike NOTE, STATUS does not sleep - it is for routine progress and action +# confirmation, not unexpected behavior requiring deliberate user attention. +# +# Console color: bold only, no hue (\033[1m). +# No color is used for STATUS by design: STATUS is the most frequent visible +# output level and must be readable in every terminal theme (dark, light, +# high-contrast, monochrome) without relying on color perception. The >> +# prefix provides semantic differentiation instead. +# Bold ensures STATUS stands out over plain INFO/LOG text without any color. +# Output goes to /dev/console (kernel console, follows console= kernel +# parameter) so it reaches serial, framebuffer, BMC, etc. without requiring +# any process setup and without polluting stdout or stderr so callers never +# need to care about redirections (e.g. print_tree >/boot/kexec_tree.txt). +# STATUS does NOT sleep and does NOT print blank lines: it is called frequently +# and blank lines would make output very noisy. Use NOTE when blank lines and +# a sleep are needed to draw the user's attention. +# debug.log receives plain text (no ANSI). +# +# Output modes (always visible in all modes): +# Quiet (CONFIG_QUIET_MODE=y): /dev/console + debug.log +# Info (CONFIG_QUIET_MODE=n): /dev/console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log +# +# See doc/logging.md. +STATUS() { + # Console: bold >> prefix to /dev/console - announces an action in progress. + echo -e "\033[1m >>\033[0m $*" >/dev/console 2>/dev/null + # Log file: plain text - no ANSI codes in debug.log + echo " >> $*" >> /tmp/debug.log +} + +STATUS_OK() { + # Console: bold green "OK" prefix to /dev/console - confirms a successful result. + # Use STATUS_OK (not STATUS) when reporting that an operation succeeded, + # a verification passed, or a resource was confirmed available. + # Two signals make success scannable without relying on either alone: + # 1. "OK" text label - readable in monochrome, on serial consoles, + # and by users with color vision deficiency + # 2. Bold green color - instant visual scan for sighted users + # (Same convention as Linux/systemd "[ OK ]" boot messages.) + echo -e "\033[1;32m OK\033[0m $*" >/dev/console 2>/dev/null + # Log file: plain text - no ANSI codes in debug.log + echo " OK $*" >> /tmp/debug.log +} + +# INFO - high-level operational context for non-developer users. +# +# INFO is what makes "Info" output mode meaningful. A security-conscious +# user who enables Info mode expects to see a readable audit trail of what +# Heads is doing: what is being measured, verified, sealed, or decided. +# Use INFO for: +# - TPM PCR extensions: what is being measured and when +# (e.g. "TPM: Extending PCR[4] with boot configuration") +# - High-level operational decisions driven by user configuration +# (e.g. "Not booting automatically, automatic boot is disabled") +# Do NOT use INFO for: +# - Action progress or milestones the user must see in all modes (use STATUS) +# - Heads-internal details: file paths, variable values, CBFS operations, +# code-flow steps with no user-visible effect (use DEBUG instead) +# - Messages that require Heads developer knowledge to interpret (use DEBUG) +# - Behaviors so unexpected users need to be warned (use NOTE instead) +# +# Console color: green (\033[0;32m) in Info mode. +# In Debug mode: plain text to debug.log and /dev/kmsg (no ANSI; maintains +# ordering with DEBUG messages which also route through kmsg). +# Console output goes to /dev/console (kernel console, follows console= +# kernel parameter) so it reaches serial, framebuffer, BMC, etc. without +# requiring any process setup and without polluting stdout or stderr. +# +# Output modes: +# Quiet (CONFIG_QUIET_MODE=y): debug.log only (no console) +# Info (CONFIG_QUIET_MODE=n): /dev/console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log (via kmsg for ordering) +# +# See doc/logging.md. INFO() { TRACE_FUNC - #TODO: add colors to output, here green for INFO? - - # if not CONFIG_QUIET_MODE=y, output to console. If not, output to debug.log if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - echo "$*" | tee -a /tmp/debug.log /dev/kmsg >/dev/null + # debug mode: plain to debug.log and kmsg - no ANSI, maintains ordering + # with DEBUG messages which also route through kmsg + echo "INFO: $*" | tee -a /tmp/debug.log /dev/kmsg >/dev/null elif [ "$CONFIG_QUIET_MODE" = "y" ]; then - echo "$*" >>/tmp/debug.log + # quiet mode: no console output, but context is still captured in debug.log + echo "INFO: $*" >> /tmp/debug.log else - echo "$*" + # info mode: green text to /dev/console AND debug.log. + # /dev/console = kernel console (follows console= kernel parameter): + # reaches serial, framebuffer, BMC — no process setup needed, callers + # never need to care about redirections. + echo -e "\033[0;32m$*\033[0m" >/dev/console 2>/dev/null + echo "INFO: $*" >> /tmp/debug.log fi } -# Write directly to the debug log (but not kmsg), never appears on console -# Main consumer is DO_WITH_DEBUG, which uses this to log command output +# LOG - command output and verbose state dumps, always to debug.log only. +# +# Use LOG to capture raw output of commands (lsblk, lsusb, gpg --list-keys, +# tpm2 pcrread, etc.) and any output of uncontrolled or potentially large +# length. LOG never appears on the console in any output mode - it is purely +# for post-hoc analysis of a submitted debug log. +# Prefer DO_WITH_DEBUG over calling LOG directly: DO_WITH_DEBUG captures the +# command and its arguments at DEBUG level, and routes stdout/stderr to LOG. +# Call LOG directly only when capturing output that is not a direct command +# invocation (e.g. filtering another command's stderr, state summaries). +# Do NOT use LOG for: +# - Short, fixed-length messages about decisions (use DEBUG) +# - Messages a user should ever see on console (use INFO, NOTE, or warn) +# +# Output modes (never on console in any mode): +# Quiet (CONFIG_QUIET_MODE=y): debug.log only +# Info (CONFIG_QUIET_MODE=n): debug.log only +# Debug (CONFIG_DEBUG_OUTPUT=y): debug.log only +# +# See doc/logging.md. LOG() { echo "LOG: $*" >>/tmp/debug.log } +# INPUT - colored prompt for interactive user input. +# +# Direct replacement for the common pattern: +# echo "prompt" +# read [flags] VARNAME +# The prompt is displayed bold white (\033[1;37m) to draw the user's attention. +# Bold white is chosen for maximum contrast on VGA/dark consoles (21:1 ratio) +# without relying on color perception - it is readable under all color +# deficiency types and in monochrome/high-contrast terminal modes. +# Use INPUT for all prompts that require the user to type a response. +# Do NOT use INPUT for yes/no confirmation dialogs (use whiptail instead). +# +# Usage: INPUT "prompt text" [read-flags] [VARNAME] +# Examples: +# INPUT "Enter LUKS passphrase:" -r -s luks_passphrase +# INPUT "Press any key to continue:" -n 1 -s dummy +# INPUT "Enter filename:" -r filename +# +# The read flags and VARNAME are passed through to read after the prompt +# is displayed - any read option (e.g. -r, -s, -n N) is supported. +# +# Output modes (always visible in all modes): +# All modes: console prompt in bold white to /dev/console, plain text in debug.log +# +# See doc/logging.md. +INPUT() { + TRACE_FUNC + local prompt="$1" + shift + # Log file: plain text - no ANSI codes in debug.log + echo "INPUT: $prompt" >>/tmp/debug.log + + if [ -n "$HEADS_TTY" ]; then + # gui-init context: HEADS_TTY is the actual interactive terminal (set by + # gui-init after cttyhack). Use it for both prompt output and read so + # that prompt and input always use the same device regardless of any + # stdout/stderr redirections the caller may have in effect. + echo >"$HEADS_TTY" 2>/dev/null + echo -e "\033[1;37m$prompt\033[0m" >"$HEADS_TTY" 2>/dev/null + echo >"$HEADS_TTY" 2>/dev/null + # Forward remaining args (read flags + variable name) directly to read. + # Note: static analyzers may report the caller's variable as "unassigned" + # because assignment through read "$@" indirection is not visible to them. + # This is a false positive - the variable is assigned correctly at runtime. + read "$@" <"$HEADS_TTY" + echo >"$HEADS_TTY" 2>/dev/null + else + # Pre-gui-init context (e.g. init's serial recovery shell launched with + # explicit stdin/stdout/stderr redirects to the serial device): + # honour the caller's redirections — use stderr for output and stdin for + # read so the correct device is used without hard-coding any path. + echo >&2 + echo -e "\033[1;37m$prompt\033[0m" >&2 + echo >&2 + read "$@" + echo >&2 + fi +} + +# Filter known harmless LVM warning noise while preserving all other stderr. +# Messages that are expected during device scanning (e.g. "not an LVM PV") are +# redirected to the debug log only - they are not errors and should not appear +# on the console, especially in quiet mode. +_filter_lvm_stderr() { + while IFS= read -r line; do + case "$line" in + *"Failed to set up async io, using sync io."*) + continue + ;; + *"leaked on lvm invocation"*) + continue + ;; + *"Cannot use "*": device is too small"* \ + | *"Cannot use "*": device is an LV"* \ + | *"Failed to find physical volume"*) + LOG "lvm: $line" + continue + ;; + esac + printf '%s\n' "$line" >&2 + done +} + +# Wrapper for all runtime lvm invocations so users don't see benign async-io +# fallback warnings, especially in quiet mode. +run_lvm() { + command lvm "$@" 2> >(_filter_lvm_stderr) +} + fw_version() { local FW_VER=$(dmesg | grep 'DMI' | grep -o 'BIOS.*' | cut -f2- -d ' ') # chop off date, since will always be epoch w/timeless builds @@ -134,7 +452,7 @@ preserve_rom() { for old_file in $(echo $old_files); do new_file=$(cbfs.sh -o $1 -l | grep -x $old_file) if [ -z "$new_file" ]; then - echo "+++ Adding $old_file to $1" + DEBUG "Adding $old_file to $1" cbfs -t 50 -r $old_file >/tmp/rom.$$ || die "Failed to read cbfs file from ROM" cbfs.sh -o $1 -a $old_file -f /tmp/rom.$$ || @@ -159,23 +477,24 @@ confirm_gpg_card() { return fi + # If GPG key backup is configured, ask whether to use the dongle or the backup + # thumb drive. Use a full-line read (no -n 1) so that buffered single + # keystrokes from previous prompts cannot silently satisfy this read. + local card_confirm="" if [ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" ]; then - message="Please confirm that your GPG card is inserted(Y/n) or your GPG key material (b)backup thumbdrive is inserted [Y/n/b]: " - else - # Generic message if no known key material backup - message="Please confirm that your GPG card is inserted [Y/n]: " - fi - - read -r -n 1 -p $'\n'"$message" card_confirm - echo - - if [ "$card_confirm" != "y" \ - -a "$card_confirm" != "Y" \ - -a "$card_confirm" != "b" \ - -a -n "$card_confirm" ] \ - ; then - die "gpg card not confirmed" + INPUT "Use your GPG security dongle (Enter/y) or backup thumb drive (b)? [Y/b]:" -r card_confirm + while [ "$card_confirm" != "y" \ + -a "$card_confirm" != "Y" \ + -a "$card_confirm" != "b" \ + -a -n "$card_confirm" ]; do + INPUT 'Invalid choice. Press Enter for dongle, type b for backup thumb drive, or x to abort:' -r card_confirm + if [ "$card_confirm" = "x" ]; then + die "gpg card not confirmed" + fi + done fi + # Non-backup case: skip the upfront confirmation entirely. wait_for_gpg_card + # below does the actual check and prompts on failure. # If user has known GPG key material Thumb drive backup and asked to use it if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" && "$card_confirm" == "b" ]]; then @@ -190,13 +509,12 @@ confirm_gpg_card() { #Prompt user for configured GPG Admin PIN that will be passed along to mount-usb and to import gpg subkeys gpg_admin_pin="" while [ -z "$gpg_admin_pin" ]; do - read -r -s -p $'\nPlease enter GPG Admin PIN needed to use the GPG backup thumb drive: ' gpg_admin_pin - echo + INPUT "Please enter GPG Admin PIN needed to use the GPG backup thumb drive:" -r -s gpg_admin_pin done #prompt user to select the proper encrypted partition, which should the first one on next prompt warn "Please select encrypted LUKS on GPG key material backup thumb drive (not public labeled one)" mount-usb --pass "$gpg_admin_pin" || die "Unable to mount USB with provided GPG Admin PIN" - echo "++++ Testing detach-sign operation and verifiying against fused public key in ROM" + STATUS "Testing detach-sign operation and verifying against public key in ROM" gpg --pinentry-mode=loopback --passphrase-file <(echo -n "${gpg_admin_pin}") --import /media/subkeys.sec >/dev/null 2>&1 || die "Unable to import GPG private subkeys" #Do a detach signature to ensure gpg material is usable and cache passphrase to sign /boot from caller functions @@ -206,7 +524,7 @@ confirm_gpg_card() { die "Unable to detach-sign $CR_NONCE with GPG private signing subkey using GPG Admin PIN" #verify detached signature against public key in rom gpg --verify "$CR_SIG" "$CR_NONCE" >/dev/null 2>&1 && - echo "++++ Local GPG keyring can be used to sign/encrypt/authenticate in this boot session ++++" || + STATUS_OK "Local GPG keyring is available for signing, encryption, and authentication this boot session" || die "Unable to verify $CR_SIG detached signature against public key in ROM" #Wipe any previous CR_NONCE and CR_SIG shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" >/dev/null 2>&1 || true @@ -222,7 +540,7 @@ confirm_gpg_card() { # Wait for USB enumeration before accessing GPG card to avoid race condition wait_for_usb_devices - echo -e "\nVerifying presence of GPG card...\n" + STATUS "Verifying presence of GPG card" # ensure we don't exit without retrying errexit=$(set -o | grep errexit | awk '{print $2}') set +e @@ -230,9 +548,7 @@ confirm_gpg_card() { if ! wait_for_gpg_card; then DEBUG "GPG card access failed with output: $gpg_output" # prompt for reinsertion and try a second time - read -n1 -r -p \ - "Can't access GPG key; remove and reinsert, then press Enter to retry. " \ - ignored + INPUT "Can't access GPG key; remove and reinsert, then press Enter to retry." ignored # restore prev errexit state if [ "$errexit" = "on" ]; then set -e @@ -251,12 +567,9 @@ confirm_gpg_card() { user_pin_retries=$(echo "$pin_retry_counters" | awk '{print $1}') admin_pin_retries=$(echo "$pin_retry_counters" | awk '{print $3}') - echo "" - echo "GPG User PIN retry attempts left before becoming locked: $user_pin_retries" - echo "GPG Admin PIN retry attempts left before becoming locked: $admin_pin_retries" - echo "" - NOTE "Your GPG User PIN, followed by Enter key will be required for input at: 'Please unlock the card' next prompt" - echo "" + STATUS "GPG User PIN retries remaining: $user_pin_retries" + STATUS "GPG Admin PIN retries remaining: $admin_pin_retries" + STATUS "GPG User PIN required at next smartcard prompt" # restore prev errexit state if [ "$errexit" = "on" ]; then @@ -268,7 +581,7 @@ gpg_auth() { if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" ]]; then TRACE_FUNC # If we have a GPG key backup, we can use it to authenticate even if the card is lost - echo >&2 "!!!!! Please authenticate with OpenPGP smartcard/backup media to prove you are the owner of this machine !!!!!" + NOTE "Please authenticate with OpenPGP smartcard/backup media to prove you are the owner of this machine" # Wipe any existing nonce and signature shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" 2>/dev/null || true @@ -310,7 +623,7 @@ gpg_auth() { else shred -n 10 -z -u "$CR_SIG" 2>/dev/null || true if [ "$tries" -lt 3 ]; then - echo >&2 "!!!!! GPG authentication failed, please try again !!!!!" + warn "GPG authentication failed, please try again" continue else die "GPG authentication failed, please reboot and try again" @@ -323,10 +636,7 @@ gpg_auth() { recovery() { TRACE_FUNC - echo >&2 "!!!!! $*" - - # Remove any temporary secret files that might be hanging around - # but recreate the directory so that new tools can use it. + # Remove any temporary secret files, but recreate the directory so that new tools can use it. #safe to always be true. Otherwise "set -e" would make it exit here shred -n 10 -z -u /tmp/secret/* 2>/dev/null || true @@ -348,7 +658,7 @@ recovery() { fi if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then - echo >&2 "Restricted Boot enabled, recovery console disabled, rebooting in 5 seconds" + NOTE "Restricted Boot enabled, recovery console disabled, rebooting in 5 seconds" sleep 5 /bin/reboot fi @@ -363,10 +673,13 @@ recovery() { #Guide user into enabling debug output in case of a discovered bug if [ "$CONFIG_DEBUG_OUTPUT" != "y" ]; then - #User can enable DEBUG_OUTPUT=y and TRACE_FUNCTION_TRACING_OUTPUT=y from Configuration Options - NOTE "If you want to file a bug, please enable Debug mode through 'Options --> Change configuration settings > Configure Heads informational'" + 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 - echo >&2 "!!!!! Starting recovery shell" + # display any custom recovery message just before the banner + if [ -n "$*" ]; then + warn "$*" + fi + STATUS "Starting recovery shell" if [ -x /bin/setsid ]; then /bin/setsid -c /bin/bash @@ -378,7 +691,7 @@ recovery() { pause_recovery() { TRACE_FUNC - read -p $'!!! Hit enter to proceed to recovery shell !!!\n' + INPUT "Press Enter to proceed to recovery shell" recovery $* } @@ -724,6 +1037,76 @@ pcrs() { fi } +# Marker helpers for TPM state that requires reset before reseal/generate paths. +tpm_reset_required_marker_path() { + printf %s "/tmp/secret/tpm_reset_required" +} + +tpm_reset_required_reason_path() { + printf %s "/tmp/secret/tpm_reset_required.reason" +} + +tpm_reset_required_source_path() { + printf %s "/tmp/secret/tpm_reset_required.source" +} + +tpm_reset_required_timestamp_path() { + printf %s "/tmp/secret/tpm_reset_required.timestamp" +} + +debug_tpm_reset_required_state() { + TRACE_FUNC + local marker reason source when + marker="$(tpm_reset_required_marker_path)" + reason="$(tpm_reset_required_reason_path)" + source="$(tpm_reset_required_source_path)" + when="$(tpm_reset_required_timestamp_path)" + + if [ -f "$marker" ]; then + DEBUG "TPM reset marker: PRESENT path=$marker" + DEBUG "TPM reset marker: reason=$(cat "$reason" 2>/dev/null || echo '')" + DEBUG "TPM reset marker: source=$(cat "$source" 2>/dev/null || echo '')" + DEBUG "TPM reset marker: timestamp=$(cat "$when" 2>/dev/null || echo '')" + else + DEBUG "TPM reset marker: ABSENT path=$marker" + fi +} + +set_tpm_reset_required() { + TRACE_FUNC + local reason source + reason="${1:-TPM state marked invalid by unknown caller}" + source="${2:-unknown}" + mkdir -p /tmp/secret || true + echo "$reason" >"$(tpm_reset_required_reason_path)" 2>/dev/null || true + echo "$source" >"$(tpm_reset_required_source_path)" 2>/dev/null || true + date -u "+%Y-%m-%d %H:%M:%S UTC" >"$(tpm_reset_required_timestamp_path)" 2>/dev/null || true + : >"$(tpm_reset_required_marker_path)" + STATUS "TPM reset required: $reason" +} + +clear_tpm_reset_required() { + TRACE_FUNC + rm -f "$(tpm_reset_required_marker_path)" + rm -f "$(tpm_reset_required_reason_path)" + rm -f "$(tpm_reset_required_source_path)" + rm -f "$(tpm_reset_required_timestamp_path)" + STATUS "TPM reset-required marker cleared" +} + +tpm_reset_required() { + TRACE_FUNC + local marker + marker="$(tpm_reset_required_marker_path)" + if [ -f "$marker" ]; then + DEBUG "tpm_reset_required: yes" + debug_tpm_reset_required_state + return 0 + fi + DEBUG "tpm_reset_required: no" + return 1 +} + confirm_totp() { TRACE_FUNC prompt="$1" @@ -765,6 +1148,24 @@ confirm_totp() { reseal_tpm_disk_decryption_key() { TRACE_FUNC + local GPG_KEY_COUNT + if tpm_reset_required; then + warn "Cannot reseal TPM disk decryption key while TPM state is marked invalid. Reset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + # Resealing disk-unlock material eventually requires signing /boot updates; + # do not proceed if keyring is empty. + GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) + if [ "$GPG_KEY_COUNT" -eq 0 ]; then + DEBUG "Skipping TPM disk-key reseal: GPG keyring is empty (caller handles user guidance)" + return 1 + fi + + # only relevant for TPM2; TPM1 has no primary handle concept + if [ "$CONFIG_TPM2_TOOLS" = "y" ] && [ ! -f "/tmp/secret/primary.handle" ]; then + warn "Cannot reseal TPM disk decryption key; no TPM primary handle. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) to reset the TPM first." + return 1 + fi #For robustness, exit early if LUKS TPM Disk Unlock Key is prohibited in board configs if [ "$CONFIG_TPM_DISK_UNLOCK_KEY" == "n" ]; then DEBUG "LUKS TPM Disk Unlock Key is prohibited in board configs" @@ -779,22 +1180,20 @@ reseal_tpm_disk_decryption_key() { fi if [ -s /boot/kexec_key_devices.txt ] || [ -s /boot/kexec_key_lvm.txt ]; then - NOTE "LUKS TPM sealed Disk Unlock Key secret needs to be resealed alongside TOTP/HOTP secret" - echo "Resealing LUKS TPM Disk Unlock Key to be unsealed by LUKS TPM Disk Unlock Key passphrase" - while ! kexec-seal-key /boot; do - warn "Recovery Disk Encryption key passphrase/TPM Owner Password may be invalid. Please try again" - done - NOTE "LUKS header hash changed under /boot/kexec_luks_hdr_hash.txt" - echo "Updating checksums and signing all files under /boot/kexec.sig" + preflight_rollback_counter_before_reseal + STATUS "Resealing TPM Disk Unlock Key alongside TOTP/HOTP secret" + if ! kexec-seal-key /boot; then + die "Failed to reseal TPM Disk Unlock Key" + fi attempt=1 while ! update_checksums; do - warn "Attempt $attempt: Checksums were not signed. Preceding errors should explain possible causes" + warn "Signing attempt $attempt/3 failed" if [ "$attempt" -ge 3 ]; then - die "Failed to sign checksums after 3 attempts" + die "Failed to sign /boot metadata after 3 attempts" fi attempt=$((attempt + 1)) done - NOTE "Rebooting in 3 seconds to enable booting default boot option" + STATUS "Rebooting to enable default boot option" sleep 3 reboot else @@ -809,7 +1208,7 @@ enable_usb_storage() { TRACE_FUNC if ! lsmod | grep -q usb_storage; then timeout=0 - echo "Scanning for USB storage devices..." + STATUS "Scanning for USB storage devices" insmod /lib/modules/usb-storage.ko >/dev/null 2>&1 || die "usb_storage: module load failed" while [[ $(list_usb_storage | wc -l) -eq 0 ]]; do @@ -944,8 +1343,7 @@ prompt_tpm_owner_password() { return 0 fi - read -r -s -p $'\nTPM Owner Password: ' tpm_owner_password - echo + INPUT "TPM Owner Password:" -r -s tpm_owner_password # Cache the password externally to be reused by who needs it DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password" @@ -963,14 +1361,11 @@ prompt_new_owner_password() { tpm_owner_password=1 tpm_owner_password2=2 while [ "$tpm_owner_password" != "$tpm_owner_password2" ] || [ "${#tpm_owner_password}" -gt 32 ] || [ -z "$tpm_owner_password" ]; do - read -r -s -p $'\nNew TPM Owner Password (2 words suggested, 1-32 characters max): ' tpm_owner_password - read -r -s -p $'\nRepeat chosen TPM Owner Password: ' tpm_owner_password2 - + INPUT "New TPM Owner Password (2 words suggested, 1-32 characters max):" -r -s tpm_owner_password + INPUT "Repeat chosen TPM Owner Password:" -r -s tpm_owner_password2 if [ "$tpm_owner_password" != "$tpm_owner_password2" ]; then - echo - echo "Passphrases entered do not match. Try again!" + warn "Passphrases entered do not match. Try again!" fi - echo done # Cache the password externally to be reused by who needs it @@ -992,19 +1387,28 @@ check_tpm_counter() { TPM_COUNTER=$(grep -Eo 'counter-[0-9a-fA-F]+' "$1" | sed -n 's/counter-//p' | head -n1 | tr -d '\n') DEBUG "Extracted TPM_COUNTER: '$TPM_COUNTER' from $1" else - INFO "$1 does not exist; creating new TPM counter" + STATUS "$1 does not exist - creating new TPM counter" # Warn user: TPM Owner Password is required to create a new TPM counter if [ ! -s /tmp/secret/tpm_owner_password ]; then warn "TPM Owner Password is required to create a new TPM counter for /boot content rollback prevention" fi + # attempt to make a new counter, capturing any stderr for debugging + DEBUG "Invoking tpmr counter_create with label $LABEL" + # run it, then record the exit status explicitly; the '!' operator + # cannot be used because it would hide the real return code. tpmr counter_create \ - -pwdc '' \ - -la $LABEL | - tee /tmp/counter >/dev/null 2>&1 || - die "Unable to create TPM counter" + -pwdc "${tpm_password:-}" \ + -la $LABEL \ + > /tmp/counter 2> >(tee >(SINK_LOG "tpm counter_create stderr") >&2) + local rc=$? + if [ $rc -ne 0 ]; then + DEBUG "tpmr counter_create failed with status $rc" + # don't tell the user to reset again; the TPM was just reset + die "Unable to create TPM counter; TPM appears to be in a bad state. Perform OEM Factory Reset / re-ownership and try again." + fi TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1; then + return 0 + fi + fi + return 1 +} + +# Validate rollback counter state before expensive operations. +# This is a non-mutating preflight intended to fail early when the configured +# rollback counter is clearly unusable. +# Parameters: +# $1 optional rollback file path (default: /boot/kexec_rollback.txt) +# $2 optional explicit counter id override +# $3 optional on-error mode: 'die' (default) or 'return' +preflight_rollback_counter_before_reseal() { + TRACE_FUNC + local rollback_file counter_id attrs_lc on_error error_file + rollback_file="${1:-/boot/kexec_rollback.txt}" + counter_id="$2" + on_error="${3:-die}" + local reset_required_marker="/tmp/secret/rollback_reset_required" + error_file="/tmp/rollback_preflight_error" + + fail_preflight() { + local message="$1" + mkdir -p /tmp/secret || true + : >"$reset_required_marker" + set_tpm_reset_required "$message" "preflight_rollback_counter_before_reseal" + if [ "$on_error" = "return" ]; then + echo "$message" >"$error_file" + return 1 + fi + die "$message" + } + + if [ "$CONFIG_TPM" != "y" ] || [ "$CONFIG_IGNORE_ROLLBACK" = "y" ]; then + DEBUG "Skipping rollback counter preflight: rollback checks are disabled" + return 0 + fi + + if [ -z "$counter_id" ]; then + counter_id="$(get_rollback_counter_id "$rollback_file")" + fi + if [ -z "$counter_id" ]; then + # If rollback metadata is missing on an already initialized system, + # this is an inconsistent TPM/boot state and should be handled before + # TOTP/HOTP recovery workflows. + if has_prior_boot_trust_metadata "$rollback_file"; then + fail_preflight "TPM rollback metadata is missing or unreadable in '$rollback_file'. System appears initialized but rollback state cannot be validated. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + DEBUG "Skipping rollback counter preflight: no counter id in $rollback_file (likely first-time initialization)" + return 0 + fi + + DEBUG "Preflight: validating rollback counter $counter_id before protected operations" + if ! tpmr counter_read -ix "$counter_id" >/dev/null 2>&1; then + fail_preflight "TPM rollback counter '$counter_id' cannot be read. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + if attrs_lc="$(tpm2 nvreadpublic "0x$counter_id" 2>/dev/null | tr '[:upper:]' '[:lower:]')"; then + if [ -n "$attrs_lc" ]; then + if echo "$attrs_lc" | grep -q "ownerwrite" && ! echo "$attrs_lc" | grep -q "authwrite"; then + fail_preflight "TPM rollback counter '$counter_id' uses ownerwrite-only policy, which does not match Heads rollback increment path. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + if ! echo "$attrs_lc" | grep -Eq "authwrite|ownerwrite"; then + fail_preflight "TPM rollback counter '$counter_id' has no writable attribute. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + else + fail_preflight "TPM rollback counter '$counter_id' attributes are empty. TPM state is inconsistent. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + else + fail_preflight "TPM rollback counter '$counter_id' attributes cannot be read. TPM state is inconsistent. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + fi + + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + DEBUG "Preflight: rollback counter $counter_id is readable and has acceptable TPM2 write attributes" + else + DEBUG "Preflight: rollback counter $counter_id is readable on TPM1" + DEBUG "Preflight: post OEM Factory Reset / Re-Ownership, TOTP unseal may be unavailable until a new TOTP/HOTP secret is generated" + fi +} + # Read the TPM counter value from the TPM. read_tpm_counter() { TRACE_FUNC @@ -1027,30 +1551,101 @@ read_tpm_counter() { increment_tpm_counter() { TRACE_FUNC - local counter_id + local counter_id counter_present tpm_password increment_ok counter_id="$(echo "$1" | tr -d '\n')" + tpm_password="$2" + counter_present="n" + increment_ok="n" + local reset_required_marker="/tmp/secret/rollback_reset_required" + + # Prefer explicit password, otherwise reuse cached TPM owner password. + if [ -z "$tpm_password" ] && [ -s /tmp/secret/tpm_owner_password ]; then + tpm_password="$(cat /tmp/secret/tpm_owner_password)" + DEBUG "increment_tpm_counter: using cached TPM owner password" + fi + + # TPM1 counter_increment requires owner auth in practice on this path. + # origin/master typically reached this with cached owner password already set, + # but the newer reseal/update flows can call this later in the session after + # that cache is absent. Prompt once and cache to avoid empty -pwdc failures. + if [ "$CONFIG_TPM2_TOOLS" != "y" ] && [ -z "$tpm_password" ]; then + warn "TPM Owner Password is required to update rollback counter before signing updated /boot metadata." + DEBUG "increment_tpm_counter: TPM1 path has no cached/provided owner password; prompting now" + prompt_tpm_owner_password + tpm_password="$tpm_owner_password" + DEBUG "increment_tpm_counter: TPM1 owner password obtained and cached" + fi # Check if counter exists by reading it first + DEBUG "reading TPM counter $counter_id" if ! DO_WITH_DEBUG tpmr counter_read -ix "$counter_id" >/tmp/counter-check 2>/dev/null; then DEBUG "TPM counter $counter_id could not be read before incrementing" # Continue with increment attempt anyway to get detailed error messages else DEBUG "TPM counter $counter_id exists and was read successfully" + counter_present="y" fi - # Try to increment the counter - if ! DO_WITH_DEBUG tpmr counter_increment -ix "$counter_id" -pwdc '' | - tee /tmp/counter-"$counter_id" >/dev/null 2>&1; then + # Try to increment the counter. We normally hide the verbose + # output of tpmr commands to avoid overwhelming the console, but we + # must *not* swallow any interactive prompts. The previous implementation + # redirected the entire `tpmr counter_create` invocation to a file and + # /dev/null, which meant that when the counter was missing the password + # prompt could not be seen by the user even though tpmr printed it to the + # controlling terminal. Instead, capture just the stdout in a temporary + # file while still letting stdout appear on the console (and logging + # stderr to debug log). + DEBUG "incrementing TPM counter $counter_id" + + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + # TPM2 counters created with authwrite commonly require index auth (often + # empty auth) for nvincrement. Try that first, then owner auth fallback. + DEBUG "increment_tpm_counter: TPM2 trying index-auth nvincrement first" + if (set -o pipefail; DO_WITH_DEBUG --mask-position 5 \ + tpmr counter_increment -ix "$counter_id" -pwdc "" \ + 2> >(SINK_LOG "tpm counter_increment stderr") \ + | tee /tmp/counter-"$counter_id" >/dev/null); then + increment_ok="y" + elif [ -n "$tpm_password" ]; then + DEBUG "increment_tpm_counter: TPM2 index-auth increment failed; trying owner-auth fallback" + if (set -o pipefail; DO_WITH_DEBUG --mask-position 5 \ + tpmr counter_increment -ix "$counter_id" -pwdc "${tpm_password}" \ + 2> >(SINK_LOG "tpm counter_increment stderr") \ + | tee /tmp/counter-"$counter_id" >/dev/null); then + increment_ok="y" + fi + fi + else + # TPM1 path uses owner auth in practice. + if (set -o pipefail; DO_WITH_DEBUG --mask-position 5 \ + tpmr counter_increment -ix "$counter_id" -pwdc "${tpm_password:-}" \ + 2> >(SINK_LOG "tpm counter_increment stderr") \ + | tee /tmp/counter-"$counter_id" >/dev/null); then + increment_ok="y" + fi + fi + + if [ "$increment_ok" != "y" ]; then + if [ "$counter_present" = "y" ]; then + mkdir -p /tmp/secret || true + : >"$reset_required_marker" + die "TPM rollback counter '$counter_id' is readable but not incrementable. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + fi # Check if we need to create a new counter DEBUG "TPM counter increment failed. Attempting to create a new counter..." - if DO_WITH_DEBUG tpmr counter_create -pwdc '' -la 3135106223 >/tmp/new-counter 2>/dev/null; then + # run counter_create but tee its stdout to a file so we still see + # the interactive prompt and any informational messages. + if (set -o pipefail; DO_WITH_DEBUG --mask-position 3 \ + tpmr counter_create -pwdc "${tpm_password:-}" -la 3135106223 \ + 2> >(tee >(SINK_LOG "tpm counter_create stderr") >&2) \ + | tee /tmp/new-counter >/dev/null); then NEW_COUNTER=$(cut -d: -f1 TPM/TOTP/HOTP Options -> Reset the TPM) to clear the counter and allow a fresh one to be created." fi DEBUG "TPM counter incremented successfully for index $counter_id" @@ -1080,12 +1675,16 @@ check_config() { if [ "$2" != "force" ]; then DEBUG "second param: $2 != force" # Note that kexec.sig detached signature is solely verifying kexec*.txt files here! - if ! sha256sum $(find $1/kexec*.txt) | gpgv $1/kexec.sig -; then + STATUS "Verifying GPG signature on kexec boot params" + # Capture gpgv output (raw "Good/BAD signature from ..." lines) to LOG only. + # The result is conveyed via STATUS below (success) or die (failure). + if ! sha256sum $(find $1/kexec*.txt) \ + | gpgv $1/kexec.sig - 2> >(SINK_LOG "gpgv kexec.sig"); then die 'Invalid signature on kexec boot params' fi fi - INFO "+++ Found verified kexec boot params" + STATUS_OK "GPG signature on kexec boot params verified" cp $1/kexec*.txt /tmp/kexec || die "Failed to copy kexec boot params to tmp" } @@ -1126,7 +1725,7 @@ replace_config() { secret_from_rom_hash() { local ROM_IMAGE="/tmp/coreboot-notpm.rom" - echo -e "\nTPM not detected; measuring ROM directly\n" 1>&2 + warn "TPM not detected; measuring ROM directly" # Read the ROM if we haven't read it yet if [ ! -f "${ROM_IMAGE}" ]; then @@ -1136,9 +1735,38 @@ secret_from_rom_hash() { sha256sum "${ROM_IMAGE}" | cut -f1 -d ' ' | fromhex_plain } +# Refresh /boot hash of the TPM2 primary handle when available. +# This prevents a follow-up prompt to "set default boot" solely to rebuild +# kexec_primhdl_hash.txt after TPM reset/reseal flows. +refresh_tpm2_primary_handle_hash() { + TRACE_FUNC + local primhash_file="${1:-/boot/kexec_primhdl_hash.txt}" + + if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then + DEBUG "Skipping TPM2 primary handle hash refresh: CONFIG_TPM2_TOOLS != y" + return 0 + fi + + if [ ! -s /tmp/secret/primary.handle ]; then + DEBUG "Skipping TPM2 primary handle hash refresh: /tmp/secret/primary.handle not available" + return 0 + fi + + DEBUG "Refreshing TPM2 primary key handle hash into $primhash_file" + if ! DO_WITH_DEBUG sha256sum /tmp/secret/primary.handle >"$primhash_file"; then + warn "Failed to refresh TPM2 primary key handle hash at $primhash_file" + return 1 + fi + + DEBUG "TPM2 primary key handle hash saved to $primhash_file" + return 0 +} + # Update the checksums of the files in /boot and sign them update_checksums() { TRACE_FUNC + local reset_required_marker="/tmp/secret/rollback_reset_required" + local signing_targets # ensure /boot mounted if ! grep -q /boot /proc/mounts; then mount -o ro /boot || @@ -1156,7 +1784,30 @@ update_checksums() { extparam=-r fi fi - if ! DO_WITH_DEBUG kexec-sign-config -p /boot -u $extparam; then + + # Keep this best-effort and run it before signing while /boot is RW. + # Running after kexec-sign-config can fail because that path may remount + # /boot read-only before returning. + if ! refresh_tpm2_primary_handle_hash; then + warn "Proceeding without refreshed TPM2 primary key handle hash" + fi + + signing_targets="$(find /boot/kexec*.txt 2>/dev/null | tr '\n' ' ')" + DEBUG "update_checksums: signing targets under /boot: ${signing_targets:-}" + DEBUG "update_checksums: rollback marker path is $reset_required_marker" + DEBUG "update_checksums: extparam='$extparam' CONFIG_TPM='${CONFIG_TPM:-}' CONFIG_IGNORE_ROLLBACK='${CONFIG_IGNORE_ROLLBACK:-}'" + DEBUG "update_checksums: signing is required because /boot metadata changed (rollback counter and/or resealed secrets) and must be re-trusted" + + STATUS "Signing $CONFIG_BRAND_NAME verified boot metadata under /boot - GPG prompt will follow" + + # signing may prompt for TPM password; avoid DO_WITH_DEBUG which + # severs the controlling tty for the child process. + DEBUG "running kexec-sign-config -p /boot -u $extparam" + rm -f "$reset_required_marker" + if ! kexec-sign-config -p /boot -u $extparam; then + if [ -e "$reset_required_marker" ]; then + die "TPM rollback counter state is invalid for secure rollback protection. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + fi rv=1 else rv=0 @@ -1309,9 +1960,9 @@ find_lvm_vg_name() { mkdir -p /tmp/root-hashes-gui # Try to query whether DEVICE is an LVM physical volume. On systems # without LVM the command may not exist; treat that like "not a PV". - if ! lvm pvs --noheadings -o vg_name "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/tmp/root-hashes-gui/lvm_err; then - # It's not an LVM PV, or lvm failed entirely. Log stderr for debugging. - DEBUG "lvm pvs failed for $DEVICE, stderr:" "$(cat /tmp/root-hashes-gui/lvm_err)" + if ! run_lvm pvs --noheadings -o vg_name "$DEVICE" >/tmp/root-hashes-gui/lvm_vg; then + # It's not an LVM PV, or lvm failed entirely. + DEBUG "lvm pvs failed for $DEVICE" # try any children shown by lsblk (handles LUKS containers with # internal partitions such as dm-0, dm-1 etc). if command -v lsblk >/dev/null 2>&1; then @@ -1319,7 +1970,7 @@ find_lvm_vg_name() { for part in $(lsblk -np -l -o NAME "$DEVICE" | tail -n +2); do [ -b "$part" ] || continue DEBUG "find_lvm_vg_name: testing child $part" - if lvm pvs --noheadings -o vg_name "$part" >/tmp/root-hashes-gui/lvm_vg 2>/tmp/root-hashes-gui/lvm_err; then + if run_lvm pvs --noheadings -o vg_name "$part" >/tmp/root-hashes-gui/lvm_vg; then VG="$(awk 'NF {print $1; exit}' /tmp/root-hashes-gui/lvm_vg)" [ -n "$VG" ] && { echo "$VG"; return 0; } fi @@ -1490,7 +2141,7 @@ detect_boot_device() { done # no valid boot device found - echo "Unable to locate /boot files on any mounted disk" + warn "Unable to locate /boot files on any mounted disk" DEBUG "detect_boot_device: failed to find a bootable device" return 1 } @@ -1622,12 +2273,12 @@ trap run_at_exit_handlers EXIT # Helper function to generate diceware passphrase generate_passphrase() { usage_generate_passphrase() { - echo "Usage: generate_passphrase --dictionary|-d [--number_words|-n ] [--max_length|-m ] [--lowercase|-l]" - echo "Generates a passphrase using a Diceware dictionary." - echo " --dictionary|-d Path to the Diceware dictionary file (defaults to /etc/diceware_dictionaries/eff_short_wordlist_2_0.txt )." - echo " [--number_words|-n ] Number of words in the passphrase (default: 3)." - echo " [--max_length|-m ] Maximum size of the passphrase (default: 256)." - echo " [--lowercase|-l] Use lowercase words (default: false)." + INFO "Usage: generate_passphrase --dictionary|-d [--number_words|-n ] [--max_length|-m ] [--lowercase|-l]" + INFO "Generates a passphrase using a Diceware dictionary." + INFO " --dictionary|-d Path to the Diceware dictionary file (defaults to /etc/diceware_dictionaries/eff_short_wordlist_2_0.txt )." + INFO " [--number_words|-n ] Number of words in the passphrase (default: 3)." + INFO " [--max_length|-m ] Maximum size of the passphrase (default: 256)." + INFO " [--lowercase|-l] Use lowercase words (default: false)." } # Helper subfunction to get a random word from the dictionary @@ -1766,20 +2417,43 @@ load_keymap() { # TOTP code and waits for the user to press Escape to continue. show_totp_until_esc() { local now_str status_line current_totp ch - local last_totp_time=0 last_totp="" - printf "\n" # reserve a line for updates - - # Clear any pending keystrokes before we start displaying the TOTP. - # In particular, a stray Escape key from the previous passphrase - # prompt could be sitting in the input buffer and would cause the - # function to immediately return on the next iteration, confusing - # the user when they try to hit Esc again. Drain stdin until it's - # empty. - while IFS= read -r -t 0 -n 1 junk; do :; done - - # Poll frequently (200ms) for responsiveness, but only refresh the - # displayed timestamp/TOTP when the displayed second changes. Cache - # the TOTP for a short interval to avoid repeated unseal calls. + # totp_ever_unsealed: set to 1 on first successful unseal; used to detect + # mid-session secret wipe (e.g. another console entered recovery shell). + local last_totp_time=0 last_totp="" totp_ever_unsealed=0 + + # Use the same terminal the user is actively interacting with. + # HEADS_TTY is set by gui-init (after cttyhack) to the actual interactive + # terminal — both output (status line) and input (Esc / Enter detection) + # must use the same device. Falls back to stdout/stdin (file descriptor + # 1/0) when HEADS_TTY is not set so that callers' redirections are + # respected (same behaviour as the original pre-HEADS_TTY code). + local interactive_tty="${HEADS_TTY}" + + # Serial consoles (ttyS*, ttyUSB*, ttyAMA*) do not reliably support raw-mode + # single-character reads: bash's "read -n 1" puts the tty into raw mode via + # 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 [ -n "$interactive_tty" ]; then + printf "\n" >"$interactive_tty" 2>/dev/null # reserve a line for updates + else + printf "\n" # reserve a line for updates + fi + + # Drain any pending keystrokes (e.g. a stray Esc from the previous prompt). + # Skip on serial: "read -n 1 -t 0" also uses raw mode and would block. + if [ "$is_serial" = "0" ]; then + if [ -n "$interactive_tty" ]; then + while IFS= read -r -t 0 -n 1 junk <"$interactive_tty" 2>/dev/null; do :; done + else + while IFS= read -r -t 0 -n 1 junk; do :; done + fi + fi + local last_sec=0 while :; do now_str=$(date -u '+%Y-%m-%d %H:%M:%S UTC') @@ -1787,14 +2461,19 @@ show_totp_until_esc() { now_epoch=$(date +%s) local now_sec=$now_epoch - # Refresh TOTP at most once every 1 second + # Refresh TOTP once per second for fresh validation. if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TOTP_SKIP_QRCODE" != "y" ]; then if [ $((now_epoch - last_totp_time)) -ge 1 ] || [ -z "$last_totp" ]; then if current_totp=$(unseal-totp 2>/dev/null); then last_totp="$current_totp" last_totp_time=$now_epoch + totp_ever_unsealed=1 + elif [ "$totp_ever_unsealed" = "1" ]; then + # Previously succeeded but now fails: TPM secrets were wiped + # mid-session (e.g. another console entered the recovery shell). + die "TOTP secret no longer accessible: TPM secrets were wiped. Boot integrity cannot be confirmed." else - # If unseal fails, clear cached value so we retry later + # Never succeeded yet; clear and retry next second last_totp="" last_totp_time=0 fi @@ -1804,8 +2483,6 @@ show_totp_until_esc() { # Only update display when the second changes to avoid flicker if [ "$now_sec" -ne "$last_sec" ]; then last_sec=$now_sec - # Build an explicit TOTP field so it's clear when no code is - # available (initial state or unseal failure). local totp_field="" if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TOTP_SKIP_QRCODE" != "y" ]; then if [ -n "$last_totp" ]; then @@ -1814,19 +2491,51 @@ show_totp_until_esc() { totp_field=" | TOTP unavailable" fi fi - status_line="[$now_str]${totp_field} | Press Esc to continue..." - printf "\r%s\033[K" "$status_line" + if [ "$is_serial" = "1" ]; then + status_line="\033[1m[$now_str]${totp_field} | Press Enter to continue...\033[0m" + else + status_line="\033[1m[$now_str]${totp_field} | Press Esc to continue...\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 - # Short poll for keypress (200ms). If ESC pressed, exit and return 0. - if IFS= read -r -t 0.2 -n 1 ch; then - if [ "$ch" = $'\e' ]; then - # Print an extra blank line so the next prompt appears after - # an empty line (better UX before the passphrase prompt). - printf "\n\n" - return 0 + if [ "$is_serial" = "1" ]; then + # Line-mode read: no raw mode required; times out after 1 s. + # Any input (Enter) continues. + 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 + return 0 + fi + else + if IFS= read -r -t 1 ch; then + printf "\n\n" + return 0 + fi + fi + else + # Framebuffer: raw single-char poll (200 ms). ESC continues. + if [ -n "$interactive_tty" ]; then + if IFS= read -r -t 0.2 -n 1 ch <"$interactive_tty" 2>/dev/null; then + if [ "$ch" = $'\e' ]; then + printf "\n\n" >"$interactive_tty" 2>/dev/null + return 0 + fi + # Ignore other keys and continue polling + fi + else + if IFS= read -r -t 0.2 -n 1 ch; then + if [ "$ch" = $'\e' ]; then + printf "\n\n" + return 0 + fi + # Ignore other keys and continue polling + fi fi - # Ignore other keys and continue polling fi done } diff --git a/initrd/etc/gui_functions b/initrd/etc/gui_functions index e7844e8e2..656d009f7 100755 --- a/initrd/etc/gui_functions +++ b/initrd/etc/gui_functions @@ -5,6 +5,7 @@ # Pause for the configured timeout before booting automatically. Returns 0 to # continue with automatic boot, nonzero if user interrupted. pause_automatic_boot() { + TRACE_FUNC if IFS= read -t "$CONFIG_AUTO_BOOT_TIMEOUT" -s -n 1 -r -p \ $'Automatic boot in '"$CONFIG_AUTO_BOOT_TIMEOUT"$' seconds unless interrupted by keypress...\n'; then return 1 # Interrupt automatic boot @@ -33,28 +34,61 @@ mount_usb() { } # -- Display related functions -- + +# Rebuild "$@" into global _WHIPTAIL_ARGS, wrapping the body text argument +# (the one immediately following --msgbox, --yesno, --menu, --inputbox, etc.) +# through printf '%b' | fold -s -w 76 so \n escapes are expanded and long +# lines fit inside an 80-column dialog. All other arguments are passed +# through unchanged. Callers must not be called recursively. +_whiptail_preprocess_args() { + _WHIPTAIL_ARGS=() + local _wrap_next=0 _arg + for _arg in "$@"; do + if [ "$_wrap_next" = 1 ]; then + _WHIPTAIL_ARGS+=("$(printf '%b' "$_arg" | fold -s -w 76)") + _wrap_next=0 + else + _WHIPTAIL_ARGS+=("$_arg") + case "$_arg" in + --msgbox|--yesno|--menu|--inputbox|--passwordbox|--checklist|--radiolist) + _wrap_next=1 ;; + esac + fi + done +} + # Produce a whiptail prompt with 'warning' background, works for fbwhiptail and newt whiptail_warning() { + TRACE_FUNC + _whiptail_preprocess_args "$@" if [ -x /bin/fbwhiptail ]; then - whiptail $BG_COLOR_WARNING "$@" + DEBUG "whiptail_warning: whiptail $BG_COLOR_WARNING $*" + whiptail $BG_COLOR_WARNING "${_WHIPTAIL_ARGS[@]}" else - env NEWT_COLORS="root=,$TEXT_BG_COLOR_WARNING" whiptail "$@" + DEBUG "whiptail_warning: NEWT_COLORS=root=,$TEXT_BG_COLOR_WARNING whiptail $*" + env NEWT_COLORS="root=,$TEXT_BG_COLOR_WARNING" whiptail "${_WHIPTAIL_ARGS[@]}" fi } # Produce a whiptail prompt with 'error' background, works for fbwhiptail and newt whiptail_error() { + TRACE_FUNC + _whiptail_preprocess_args "$@" if [ -x /bin/fbwhiptail ]; then - whiptail $BG_COLOR_ERROR "$@" + DEBUG "whiptail_error: whiptail $BG_COLOR_ERROR $*" + whiptail $BG_COLOR_ERROR "${_WHIPTAIL_ARGS[@]}" else - env NEWT_COLORS="root=,$TEXT_BG_COLOR_ERROR" whiptail "$@" + DEBUG "whiptail_error: NEWT_COLORS=root=,$TEXT_BG_COLOR_ERROR whiptail $*" + env NEWT_COLORS="root=,$TEXT_BG_COLOR_ERROR" whiptail "${_WHIPTAIL_ARGS[@]}" fi } # Produce a whiptail prompt of the given type - 'error', 'warning', or 'normal' whiptail_type() { + TRACE_FUNC local TYPE="$1" shift + DEBUG "whiptail_type: type=$TYPE args=$*" case "$TYPE" in error) whiptail_error "$@" @@ -63,7 +97,9 @@ whiptail_type() { whiptail_warning "$@" ;; normal) - whiptail "$@" + _whiptail_preprocess_args "$@" + DEBUG "whiptail_type: whiptail $*" + whiptail "${_WHIPTAIL_ARGS[@]}" ;; esac } @@ -71,6 +107,7 @@ whiptail_type() { # Create display text for a size in bytes in either MB or GB, unit selected # automatically, rounded to nearest display_size() { + TRACE_FUNC local size_bytes unit_divisor unit_symbol size_bytes="$1" @@ -90,6 +127,7 @@ display_size() { # Create display text for the size of a block device using MB or GB, rounded to # nearest display_block_device_size() { + TRACE_FUNC local block_dev disk_size_bytes block_dev="$1" @@ -203,19 +241,497 @@ show_system_info() { --msgbox "$msgbox_rm_tabs" 0 80 } +# Show measured integrity report including TOTP/HOTP status and /boot integrity. +report_integrity_measurements() { + TRACE_FUNC + local date_now hash_state msg menu_msg totp_state hotp_state signature_state sig_status sig_detail sig_guidance report_body report_option signing_key_state + + date_now=$(date "+%Y-%m-%d %H:%M:%S %Z") + totp_state="N/A" + hotp_state="N/A" + DEBUG "integrity report generated at $date_now" + STATUS "Preparing Measured Integrity Report - hashing and verifying /boot" + + if [ "$CONFIG_TPM" = "y" ]; then + totp_state="UNAVAILABLE" + if [ "$CONFIG_TPM2_TOOLS" != "y" ] || [ -f /tmp/secret/primary.handle ]; then + if HEADS_NONFATAL_UNSEAL=y tpmr unseal 4d47 0,1,2,3,4,7 312 /tmp/secret/integrity_totp_key >/dev/null 2>&1; then + truncate_max_bytes 20 /tmp/secret/integrity_totp_key >/dev/null 2>&1 + if totp /tmp/secret/integrity_totp 2>/dev/null; then + totp_state="$(cat /tmp/secret/integrity_totp 2>/dev/null)" + else + totp_state="ERROR" + fi + fi + fi + shred -n 10 -z -u /tmp/secret/integrity_totp_key /tmp/secret/integrity_totp 2>/dev/null + fi + + if [ -x /bin/hotp_verification ]; then + hotp_state="TOKEN MISSING" + enable_usb + if hotp_verification info >/dev/null 2>&1; then + hotp_state="TOKEN PRESENT" + fi + fi + + # Detached signature trust must be established before any hash files are trusted. + signature_state="UNVERIFIED" + if [ ! -r /boot/kexec.sig ]; then + signature_state="MISSING SIGNATURE FILE" + hash_state="UNTRUSTED (DETACHED SIGNATURE MISSING)" + DEBUG "report_integrity_measurements: /boot/kexec.sig is missing" + sig_detail="/boot/kexec.sig does not exist - /boot files cannot be verified as authentic." + sig_guidance="If unexpected, stop and restore a known-good /boot. If expected, choose: Investigate discrepancies -> Update checksums now." + elif detached_kexec_signature_valid /boot; then + signature_state="VERIFIED" + # detached_kexec_signature_valid confirms trust of kexec*.txt; load those trusted references. + check_config /boot force + TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" + TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" + if [ -r "$TMP_HASH_FILE" ] && [ -r "$TMP_TREE_FILE" ] && verify_checksums /boot n; then + hash_state="OK" + else + hash_state="ALTERED OR UNKNOWN" + fi + sig_detail="ROM-fused public key authenticated /boot/kexec.sig - all /boot files match the signed hashes." + sig_guidance="No signature fix needed." + else + sig_status="$(detached_kexec_signature_failure_status /boot)" + case "$sig_status" in + MALFORMED) + signature_state="SIGNATURE FILE IS BROKEN" + hash_state="UNTRUSTED (SIGNATURE FILE IS BROKEN)" + sig_detail="/boot/kexec.sig cannot be parsed - the file appears corrupted or truncated." + sig_guidance="If unexpected, stop and restore a known-good /boot. If expected, choose: Investigate discrepancies -> Update checksums now." + ;; + BAD) + signature_state="SIGNATURE DOES NOT MATCH BOOT FILES" + hash_state="UNTRUSTED (SIGNATURE DOES NOT MATCH FILES)" + sig_detail="The signature does not match the current /boot files - files may have been altered since last signed." + sig_guidance="If unexpected, stop and investigate tampering. If expected, choose: Investigate discrepancies -> Update checksums now." + ;; + UNKNOWN_KEY) + local _signer_info + _signer_info="$(detached_kexec_signature_signer_info)" + signature_state="SIGNED BY UNTRUSTED KEY" + hash_state="UNTRUSTED - content cannot be verified" + if [ -n "$_signer_info" ]; then + sig_detail="/boot was signed by an untrusted key (${_signer_info}). The files cannot be verified and must be treated as compromised. Possible causes: disk swap, /boot signed on a different machine, or firmware reflashed with a new key." + sig_guidance="Only re-sign if you can independently confirm /boot is in expected state, knowing it was signed by ${_signer_info}. If in doubt, restore /boot from a trusted backup. Do NOT re-sign blindly - that would bless a potentially compromised /boot. For intentional re-ownership or a fresh OS install, perform OEM Factory Reset / Re-Ownership." + else + sig_detail="/boot was signed by an untrusted key (signer identity could not be determined). The files cannot be verified and must be treated as compromised. Possible causes: disk swap, /boot signed on a different machine, or firmware reflashed with a new key." + sig_guidance="Treat /boot as compromised and restore from a trusted backup. Do NOT re-sign unverified files - that would bless a potentially compromised /boot. For intentional re-ownership or a fresh OS install, perform OEM Factory Reset / Re-Ownership." + fi + ;; + *) + signature_state="SIGNATURE CHECK FAILED" + hash_state="UNTRUSTED (DETACHED SIGNATURE INVALID)" + sig_detail="The signature check failed for an unknown reason." + sig_guidance="If this was NOT expected, stop and investigate. Only choose Update checksums after you trust the current /boot files." + ;; + esac + DEBUG "report_integrity_measurements: detached signature status=$sig_status detail=$(detached_kexec_signature_failure_detail_one_line)" + fi + INTEGRITY_REPORT_HASH_STATE="$hash_state" + + # Check signing key: try card immediately (USB already up); only prompt if not accessible. + # wait_for_gpg_card sets global gpg_output to the card-status output on success. + enable_usb + gpg_output="" + local _card_detected=0 + if wait_for_gpg_card 2>/dev/null; then + _card_detected=1 + else + whiptail_type "$BG_COLOR_MAIN_MENU" --title 'Signing Card Check' \ + --msgbox "Please insert your OpenPGP signing card (USB security key), then press OK." 0 80 + if wait_for_gpg_card 2>/dev/null; then + _card_detected=1 + fi + fi + + # Determine signing key state from card-status output (gpg_output set by wait_for_gpg_card). + local _card_sig_fpr _rom_fprs signing_key_guidance + if [ "$_card_detected" -eq 0 ]; then + signing_key_state="NO DONGLE DETECTED" + signing_key_guidance="No USB security dongle detected. Insert the correct dongle and retry, or perform OEM Factory Reset / Re-Ownership." + else + _card_sig_fpr=$(echo "$gpg_output" \ + | 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="USB security dongle 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." + 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="" + else + signing_key_state="DONGLE KEY NOT ROM-TRUSTED" + signing_key_guidance="USB security dongle 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." + fi + fi + fi + DEBUG "report_integrity_measurements: signing_key_state=$signing_key_state card_sig_fpr=${_card_sig_fpr:-none}" + + # Build display-friendly variants of TOTP/HOTP state for the report + local totp_display hotp_display + case "$totp_state" in + UNAVAILABLE) + totp_display="SEALED SECRET UNAVAILABLE - Reseal required (expected after TPM reset, re-ownership, or firmware update)" + ;; + ERROR) + totp_display="ERROR - TOTP calculation failed" + ;; + *) + totp_display="$totp_state" + ;; + esac + case "$hotp_state" in + "TOKEN MISSING") + hotp_display="TOKEN NOT CONNECTED" + ;; + "TOKEN PRESENT") + hotp_display="TOKEN CONNECTED (presence confirmed)" + ;; + *) + hotp_display="$hotp_state" + ;; + esac + + local action_guidance + if [ -n "$signing_key_guidance" ]; then + action_guidance="$signing_key_guidance" + else + action_guidance="$sig_guidance" + fi + report_body="Date: $date_now\nTOTP: $totp_display\nHOTP: $hotp_display\n\nBoot signature (/boot/kexec.sig): $signature_state\n$sig_detail\nBoot files: $hash_state\nDongle key: $signing_key_state\n\nAction: $action_guidance" + if [ "$hash_state" != "OK" ]; then + report_body="$report_body\n\nIf /boot integrity is not OK, investigate before sealing new secrets or performing TPM reset or re-ownership." + fi + DEBUG "report_integrity_measurements: totp=$totp_state hotp=$hotp_state signature=$signature_state hash=$hash_state" + DEBUG "report_integrity_measurements: signature_detail=$sig_detail" + DEBUG "report_integrity_measurements: signature_guidance=$sig_guidance signing_key_guidance=$signing_key_guidance" + DEBUG "report_integrity_measurements: INTEGRITY_REPORT_HASH_STATE=$INTEGRITY_REPORT_HASH_STATE" + if [ "$totp_state" = "UNAVAILABLE" ] && [ "$hash_state" = "OK" ] && [ "$signing_key_state" = "DONGLE MATCHES ROM-TRUSTED KEY" ]; then + DEBUG "report_integrity_measurements: TOTP unseal unavailable but /boot integrity is OK; reseal/update flows may proceed after user confirmation" + report_body="$report_body\n\nNote: /boot is intact - generate a new HOTP/TOTP secret to restore real-time boot attestation." + fi + msg="$(printf '%b' "Measured Integrity Report\n\n$report_body" | fold -s -w 76)" + # menu_msg omits the guidance paragraphs to keep the dialog within terminal height + menu_msg="$(printf '%b' "Measured Integrity Report\n\nDate: $date_now\nTOTP: $totp_display\nHOTP: $hotp_display\n\nBoot signature (/boot/kexec.sig): $signature_state\n$sig_detail\nBoot files: $hash_state\nDongle key: $signing_key_state\n\nChoose an action:" | fold -s -w 76)" + + if [ "$hash_state" = "OK" ] && [ "$signing_key_state" = "DONGLE MATCHES ROM-TRUSTED KEY" ]; then + whiptail_type $BG_COLOR_MAIN_MENU --title 'Measured Integrity Report' \ + --msgbox "$msg" 0 80 + return 0 + elif [ "$hash_state" = "OK" ] && [ "$signing_key_state" != "DONGLE MATCHES ROM-TRUSTED KEY" ]; then + # /boot is intact but no private key - direct path is OEM Factory Reset / Re-Ownership + while true; do + whiptail_type "$BG_COLOR_MAIN_MENU" --title 'Measured Integrity Report' \ + --menu "$msg" 0 80 2 \ + 'o' ' OEM Factory Reset / Re-Ownership -->' \ + 'c' ' Continue to main menu' \ + 2>/tmp/whiptail || return 0 + report_option=$(cat /tmp/whiptail) + case "$report_option" in + o) + INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset + return 0 + ;; + c | *) + return 0 + ;; + esac + done + fi + + while true; do + whiptail_type $BG_COLOR_MAIN_MENU --title 'Measured Integrity Report' \ + --menu "$menu_msg" 0 80 2 \ + 'i' ' Investigate discrepancies -->' \ + 'c' ' Continue' \ + 2>/tmp/whiptail || return 0 + report_option=$(cat /tmp/whiptail) + case "$report_option" in + i) + if investigate_integrity_discrepancies; then + # Checksums may have been updated - re-run full measurement + # so the caller sees the refreshed INTEGRITY_REPORT_HASH_STATE + report_integrity_measurements + return + fi + ;; + *) + return 0 + ;; + esac + done +} + +investigate_integrity_discrepancies() { + TRACE_FUNC + local changed_files changed_count details sig_details sig_status + local sig_trust_state investigation_option inv_msg + + # Signature trust must be established first. If detached signature is not + # trusted, checksum success must not be treated as clean integrity. + sig_trust_state="untrusted" + if detached_kexec_signature_valid /boot; then + sig_trust_state="trusted" + fi + DEBUG "investigate_integrity_discrepancies: signature trust state=$sig_trust_state" + + if [ "$sig_trust_state" = "trusted" ]; then + check_config /boot force + TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" + TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" + if verify_checksums /boot y; then + DEBUG "investigate_integrity_discrepancies: detached signature verified and verify_checksums returned success" + whiptail_type $BG_COLOR_MAIN_MENU --title 'Integrity Investigation' \ + --msgbox 'No integrity discrepancies are currently detected for /boot.' 0 80 + return 0 + fi + DEBUG "investigate_integrity_discrepancies: detached signature verified but verify_checksums reported discrepancies" + else + DEBUG "investigate_integrity_discrepancies: detached signature not trusted; treating /boot as untrusted regardless of checksum output" + fi + + if [ "$sig_trust_state" = "trusted" ]; then + changed_files=$(grep -v 'OK$' /tmp/hash_output 2>/dev/null | cut -f1 -d ':' | sed '/^$/d') + if [ -z "$changed_files" ] && [ -r /tmp/hash_output ]; then + changed_files=$(sed '/^$/d' /tmp/hash_output) + fi + DEBUG "investigate_integrity_discrepancies: raw changed_files list=$changed_files" + else + if [ ! -r /boot/kexec.sig ]; then + sig_details="Signature file is missing" + else + sig_status="$(detached_kexec_signature_failure_status /boot)" + sig_details="$(detached_kexec_signature_failure_detail_one_line)" + [ -n "$sig_details" ] || sig_details="Signature verification failed" + case "$sig_status" in + MALFORMED) + sig_details="Signature file is damaged or not a valid signature (${sig_details})" + ;; + BAD) + sig_details="Signature does not match current /boot files (${sig_details})" + ;; + UNKNOWN_KEY) + sig_details="Signature uses a key this firmware does not trust (${sig_details})" + ;; + *) + sig_details="Signature verification failed (${sig_details})" + ;; + esac + fi + changed_files="Signature problem: $sig_details" + DEBUG "investigate_integrity_discrepancies: signature issue details=$sig_details" + fi + + if [ -z "$changed_files" ]; then + whiptail_error --title 'Integrity Investigation' \ + --msgbox 'Integrity is not OK, but no detailed mismatch list is available.' 0 80 + return 1 + fi + + # details remains relative; user is told paths are under /boot + details=$(printf '%s\n' "$changed_files" | sort -u) + changed_count=$(printf '%s\n' "$details" | wc -l | tr -d ' ') + DEBUG "integrity: changed_count=$changed_count" + DEBUG "integrity: details=$details" + + if [ "$sig_trust_state" = "trusted" ]; then + inv_msg=$(printf '%b' "Integrity mismatches were detected.\n\nDetached signature on /boot/kexec.sig verified successfully.\n\nChoose an action:" | fold -s -w 76) + else + inv_msg=$(printf '%b' "Integrity mismatches were detected.\n\nDetached signature on /boot/kexec.sig could not be verified.\n\nTreat /boot as untrusted unless you explicitly expected these changes.\n\nChoose an action:" | fold -s -w 76) + fi + + while true; do + whiptail_error --title 'Integrity Investigation' \ + --menu "$inv_msg" 0 80 5 \ + 'd' ' Show mismatch details -->' \ + 's' ' Show detached signed output -->' \ + 'u' ' Update checksums now' \ + 'r' ' Drop to recovery shell (view discrepancies)' \ + 'c' ' Continue' \ + 2>/tmp/whiptail || return 1 + + investigation_option=$(cat /tmp/whiptail) + case "$investigation_option" in + s) + show_detached_signed_kexec_output + ;; + d) + if [ "$changed_count" -gt 12 ]; then + printf '%s\n' "$details" >/tmp/hash_output_mismatches + echo 'Type "q" to exit the list and return.' >>/tmp/hash_output_mismatches + whiptail_error --title 'Integrity Investigation' \ + --msgbox "$(printf '%b' "${changed_count} discrepancy entries found.\n\nPress OK to review the full list." | fold -s -w 76)" 0 80 + less /tmp/hash_output_mismatches + else + whiptail_error --title 'Integrity Investigation' \ + --msgbox "$(printf '%b' "Discrepancy entries detected:\n\n${details}" | fold -s -w 76)" 0 80 + fi + ;; + r) + local msg + msg=$'Integrity discrepancies detected (paths are under /boot):\n\n'"${details}"$'\n\nTo investigate:\n 1. remount /boot read-write:\n mount -o rw,remount /boot\n 2. edit files with vi (use :wq to save and exit) and save your changes\n 3. unsafe boot is still possible via the Heads menu: Options -> Boot Options -> Ignore tampering and force a boot\n while /boot remains untrusted\n 4. run reboot when done; Heads will re-audit on next boot\n\nBe cautious. If unsure, reinstall and restore from backups.' + recovery "$msg" + ;; + u) + prompt_update_checksums && return 0 + ;; + *) + return 0 + ;; + esac + done +} + +detached_kexec_signature_valid() { + TRACE_FUNC + local boot_dir="$1" + local kexec_txt_files + + [ -n "$boot_dir" ] || boot_dir="/boot" + + if [ "$CONFIG_BASIC" = "y" ]; then + return 1 + fi + + if [ ! -r "$boot_dir/kexec.sig" ]; then + return 1 + fi + + kexec_txt_files=$(find "$boot_dir"/kexec*.txt 2>/dev/null | sort) + if [ -z "$kexec_txt_files" ]; then + DEBUG "detached_kexec_signature_valid: no kexec txt files found under $boot_dir" + return 1 + fi + DEBUG "detached_kexec_signature_valid: txt files=$(echo "$kexec_txt_files" | tr '\n' ' ')" + # perform verification, capturing output for debugging + if sha256sum $kexec_txt_files | gpgv "$boot_dir/kexec.sig" - >/tmp/integrity_sigcheck 2>&1; then + DEBUG "detached signature valid" + mkdir -p /tmp/kexec + cp $kexec_txt_files /tmp/kexec 2>/dev/null || true + return 0 + else + DEBUG "detached signature verification failed, head of /tmp/integrity_sigcheck:" + DEBUG "$(sed -n '1,20p' /tmp/integrity_sigcheck)" + fi + return 1 +} + +detached_kexec_signature_failure_status() { + TRACE_FUNC + local boot_dir="$1" + + [ -n "$boot_dir" ] || boot_dir="/boot" + if [ ! -r "$boot_dir/kexec.sig" ]; then + echo "MISSING" + return 0 + fi + + if grep -Eiq 'no valid openpgp data found' /tmp/integrity_sigcheck 2>/dev/null; then + echo "MALFORMED" + return 0 + fi + if grep -Eiq 'bad signature' /tmp/integrity_sigcheck 2>/dev/null; then + echo "BAD" + return 0 + fi + if grep -Eiq 'no public key|can.t check signature: no public key' /tmp/integrity_sigcheck 2>/dev/null; then + echo "UNKNOWN_KEY" + return 0 + fi + + echo "INVALID" +} + +detached_kexec_signature_failure_detail_one_line() { + TRACE_FUNC + local line + + if [ ! -r /boot/kexec.sig ]; then + echo "/boot/kexec.sig is missing" + return 0 + fi + + line="$(grep -Eim1 'no valid openpgp data found|bad signature|no public key|can.t check signature' /tmp/integrity_sigcheck 2>/dev/null)" + if [ -z "$line" ]; then + line="$(sed -n '1p' /tmp/integrity_sigcheck 2>/dev/null)" + fi + + echo "$line" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//' +} + +detached_kexec_signature_signer_info() { + # Parse gpgv output in /tmp/integrity_sigcheck to extract signer key fingerprint + # and signing date. Returns empty string if not parseable. + # gpgv output for unknown key: + # gpgv: Signature made Wed Mar 11 19:53:41 2026 UTC + # gpgv: using RSA key 8E2364E3F305AACEDFFBB61C03E3D64DDA3E571B + # gpgv: Can't check signature: No public key + local date_str key_id + date_str="$(grep -im1 'signature made' /tmp/integrity_sigcheck 2>/dev/null \ + | sed 's/.*[Ss]ignature made[[:space:]]*//' \ + | sed 's/[[:space:]]*$//')" + key_id="$(grep -im1 'using .* key' /tmp/integrity_sigcheck 2>/dev/null \ + | sed 's/.*using [A-Za-z0-9]* key[[:space:]]*//' \ + | sed 's/[[:space:]]*$//')" + if [ -n "$key_id" ] && [ -n "$date_str" ]; then + echo "fingerprint $key_id, signed on $date_str, owner unknown (key not in firmware keyring)" + elif [ -n "$key_id" ]; then + echo "fingerprint $key_id, date unknown, owner unknown (key not in firmware keyring)" + fi +} + +show_detached_signed_kexec_output() { + TRACE_FUNC + local signed_files signed_count + + signed_files=$(find /tmp/kexec/kexec*.txt 2>/dev/null | sort) + if [ -z "$signed_files" ]; then + whiptail_error --title 'Signed Output' \ + --msgbox 'No verified detached signed output is available to display.' 0 80 + return 1 + fi + + : >/tmp/integrity_signed_output + for signed_file in $signed_files; do + echo "===== $(basename "$signed_file") =====" >>/tmp/integrity_signed_output + cat "$signed_file" >>/tmp/integrity_signed_output + echo >>/tmp/integrity_signed_output + done + + signed_count=$(wc -l < /tmp/integrity_signed_output) + if [ "$signed_count" -gt 20 ]; then + echo 'Type "q" to exit the list and return.' >>/tmp/integrity_signed_output + less /tmp/integrity_signed_output + else + whiptail_type $BG_COLOR_MAIN_MENU --title 'Signed Output' \ + --msgbox "$(cat /tmp/integrity_signed_output)" 0 80 + fi +} + # Get "Enable" or "Disable" to display in the configuration menu, based on a # setting value get_config_display_action() { + TRACE_FUNC [ "$1" = "y" ] && echo "Disable" || echo "Enable" } # Invert a config value invert_config() { + TRACE_FUNC [ "$1" = "y" ] && echo "n" || echo "y" } # Get "Enable" or "Disable" for a config that internally is inverted (because it # disables a behavior that is on by default). get_inverted_config_display_action() { + TRACE_FUNC get_config_display_action "$(invert_config "$1")" } diff --git a/initrd/etc/luks-functions b/initrd/etc/luks-functions index 43fc09aac..56c6ef7ba 100644 --- a/initrd/etc/luks-functions +++ b/initrd/etc/luks-functions @@ -8,7 +8,7 @@ # List all LUKS devices on the system that are not USB list_local_luks_devices() { TRACE_FUNC - lvm vgscan 2>/dev/null || true + run_lvm vgscan 2>/dev/null || true blkid | cut -d ':' -f 1 | while read -r device; do DEBUG "Checking device: $device" if cryptsetup isLuks "$device"; then @@ -38,10 +38,9 @@ list_local_luks_devices() { prompt_luks_passphrase() { TRACE_FUNC while [[ ${#luks_current_Disk_Recovery_Key_passphrase} -lt 8 ]]; do - echo -e "\nEnter the LUKS Disk Recovery Key passphrase (At least 8 characters long):" - read -r luks_current_Disk_Recovery_Key_passphrase + INPUT "Enter the LUKS Disk Recovery Key passphrase (at least 8 characters):" -r luks_current_Disk_Recovery_Key_passphrase if [[ ${#luks_current_Disk_Recovery_Key_passphrase} -lt 8 ]]; then - echo -e "\nPassphrase must be at least 8 characters long. Please try again." + warn "Passphrase must be at least 8 characters long. Please try again." unset luks_current_Disk_Recovery_Key_passphrase continue fi @@ -90,8 +89,8 @@ confirm_luks_partitions() { die "User aborted the operation" fi else - echo -e "$MSG" - read -p "Do you want to use all of these partitions? (y/n): " confirm + INFO "$MSG" + INPUT "Do you want to use all of these partitions? (y/n):" -r confirm if [ "$confirm" != "y" ]; then die "User aborted the operation" fi @@ -127,12 +126,7 @@ select_luks_container_size_percent() { else #console prompt asking user to select ratio of device to use for LUKS container between: 10, 25, 50, 75 #console prompt returns the percentage of the device to use for LUKS container - echo "Select LUKS container size percentage of device:" - echo "1. 10%" - echo "2. 25%" - echo "3. 50%" - echo "4. 75%" - read -p "Choose your LUKS container size percentage of device [1-3]: " option_index + INPUT "Select LUKS container size percentage of device:\n 1. 10%\n 2. 25%\n 3. 50%\n 4. 75%\nChoice [1-4]:" -r option_index if [ "$option_index" = "1" ]; then echo "10" >/tmp/luks_container_size_percent elif [ "$option_index" = "2" ]; then @@ -179,8 +173,7 @@ interactive_prepare_thumb_drive() { shift 2 ;; *) - echo "usage: prepare_thumb_drive [--device device] [--percentage percentage] [--pass passphrase]" - return 1 + die "prepare_thumb_drive: unknown argument '$1' - usage: prepare_thumb_drive [--device device] [--percentage percentage] [--pass passphrase]" ;; esac done @@ -195,29 +188,18 @@ interactive_prepare_thumb_drive() { #If no passphrase was provided, ask user to select passphrase for LUKS container #console based no whiptail while [[ ${#PASSPHRASE} -lt 8 ]]; do - { - echo -e "\nEnter passphrase for LUKS container (At least 8 characters long):" - #hide passphrase input from read command - read -r -s PASSPHRASE - #skip confirmation if passphrase is less then 8 characters long (continue) - if [[ ${#PASSPHRASE} -lt 8 ]]; then - echo -e "\nPassphrase must be at least 8 characters long. Please try again." - unset PASSPHRASE - continue - fi - #validate passphrase and ask user to re-enter if not at least 8 characters long - #confirm passphrase - echo -e "\nConfirm passphrase for LUKS container:" - #hide passphrase input from read command - read -r -s PASSPHRASE_CONFIRM - #compare passphrase and passphrase confirmation - if [ "$PASSPHRASE" != "$PASSPHRASE_CONFIRM" ]; then - echo -e "\nPassphrases do not match. Please try again." - unset PASSPHRASE - unset PASSPHRASE_CONFIRM - fi - - } + INPUT "Enter passphrase for LUKS container (at least 8 characters):" -r -s PASSPHRASE + if [[ ${#PASSPHRASE} -lt 8 ]]; then + warn "Passphrase must be at least 8 characters long. Please try again." + unset PASSPHRASE + continue + fi + INPUT "Confirm passphrase for LUKS container:" -r -s PASSPHRASE_CONFIRM + if [ "$PASSPHRASE" != "$PASSPHRASE_CONFIRM" ]; then + warn "Passphrases do not match. Please try again." + unset PASSPHRASE + unset PASSPHRASE_CONFIRM + fi done fi @@ -229,8 +211,8 @@ interactive_prepare_thumb_drive() { "WARNING: Please disconnect all external drives before proceeding.\n\nHit Enter to continue." 0 80 || die "User cancelled wiping and repartitioning of $DEVICE" else - echo -e -n "Warning: Please disconnect all external drives before proceeding.\n\nHit Enter to continue?" - read -r -p " [Y/n] " response + NOTE "Please disconnect all external drives before proceeding." + INPUT "Continue? [Y/n]:" -r response #transform response to uppercase with bash parameter expansion response=${response^^} #continue if response different then uppercase N @@ -304,8 +286,8 @@ confirm_thumb_drive_format() { whiptail_warning --title "WARNING: Wiping and repartitioning $DEVICE ($DISK_SIZE_DISPLAY)" --yesno \ "$MSG" 0 80 else - echo -e -n "$MSG" - read -r -p " [Y/n] " response + NOTE "$MSG" + INPUT "Continue? [Y/n]:" -r response #transform response to uppercase with bash parameter expansion response=${response^^} #continue if response is Y, y, or empty, abort for anything else @@ -336,8 +318,8 @@ prepare_thumb_drive() { #Calculate percentage of device in MB PERCENTAGE_MB="$((DISK_SIZE_BYTES*PERCENTAGE/100/1024/1024))" - echo -e "Preparing $DEVICE with $PERCENTAGE_MB MB for private LUKS container while rest of device will be assigned to exFAT public partition...\n" - echo "Please wait..." + STATUS "Preparing $DEVICE: ${PERCENTAGE_MB}MB LUKS private + exFAT public partition" + STATUS "Please wait..." DEBUG "Creating empty DOS partition table on device through fdisk to start clean" echo -e "o\nw\n" | fdisk $DEVICE >/dev/null 2>&1 || die "Error creating partition table" DEBUG "partition device with two partitions: first one being the percent applied and rest for second partition through fdisk" @@ -356,7 +338,7 @@ prepare_thumb_drive() { cryptsetup close private > /dev/null 2>&1 || die "Error closing LUKS container" DEBUG "Formatting second partition ${DEVICE}2 with exfat filesystem..." mkfs.exfat -L public ${DEVICE}2 >/dev/null 2>&1 || die "Error formatting second partition with exfat filesystem" - echo "Done." + STATUS_OK "Done." } # Select LUKS container @@ -379,16 +361,14 @@ test_luks_current_disk_recovery_key_passphrase() { PRINTABLE_LUKS=$(echo $LUKS) + STATUS "$PRINTABLE_LUKS: Unlocking with LUKS Disk Recovery Key passphrase" if [ -z "$luks_current_Disk_Recovery_Key_passphrase" ]; then - echo -e "\nEnter the current LUKS Disk Recovery Key passphrase (Configured at OS installation or by OEM):" - read -r luks_current_Disk_Recovery_Key_passphrase + INPUT "Enter the current LUKS Disk Recovery Key passphrase (configured at OS installation or by OEM):" -r luks_current_Disk_Recovery_Key_passphrase echo -n "$luks_current_Disk_Recovery_Key_passphrase" > /tmp/secret/luks_current_Disk_Recovery_Key_passphrase else echo -n "$luks_current_Disk_Recovery_Key_passphrase" > /tmp/secret/luks_current_Disk_Recovery_Key_passphrase fi - echo -e "\n$PRINTABLE_LUKS: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." - for luks_container in $LUKS; do DEBUG "$luks_container: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." if ! cryptsetup open --test-passphrase "$luks_container" --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase; then @@ -401,7 +381,7 @@ test_luks_current_disk_recovery_key_passphrase() { luks_secrets_cleanup unset LUKS else - echo "$luks_container: unlocking LUKS container with current Disk Recovery Key passphrase successful" + STATUS_OK "$luks_container: unlocked with current Disk Recovery Key passphrase" export luks_current_Disk_Recovery_Key_passphrase fi done @@ -430,8 +410,7 @@ luks_reencrypt() { else msg=$(echo -e "This will replace the encrypted container content and its LUKS Disk Recovery Key.\n\nThe passphrase associated with this key will be asked from the user under the following conditions:\n 1-Every boot if no Disk Unlock Key was added to the TPM\n 2-If the TPM fails (hardware failure)\n 3-If the firmware has been tampered with/modified by the user\n\nThis process requires you to type the current LUKS Disk Recovery Key passphrase and will delete the LUKS TPM Disk Unlock Key slot, if set up, by setting a default boot LUKS key slot (1) if present.\n\nAt the next prompt, you may be asked to select which file corresponds to the LUKS device container.\n\nHit Enter to continue." | fold -w 70 -s) whiptail --title 'Reencrypt LUKS encrypted container ?' --msgbox "$msg" 0 80 - echo -e "\nEnter the current LUKS Disk Recovery Key passphrase:" - read -r -s luks_current_Disk_Recovery_Key_passphrase + INPUT "Enter the current LUKS Disk Recovery Key passphrase:" -r -s luks_current_Disk_Recovery_Key_passphrase echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase fi else @@ -487,7 +466,7 @@ luks_reencrypt() { # --force-offline-reencrypt forces the reencryption to be done offline (no read/write operations on the device) # --disable-locks disables the lock feature of cryptsetup, which is enabled by default - echo -e "\nReencrypting $luks_container LUKS encrypted drive content with current Recovery Disk Key passphrase..." + STATUS "Reencrypting $luks_container with current Recovery Disk Key passphrase" warn "DO NOT POWER DOWN MACHINE, UNPLUG AC OR REMOVE BATTERY DURING REENCRYPTION PROCESS" if ! DO_WITH_DEBUG cryptsetup reencrypt \ @@ -528,16 +507,18 @@ luks_change_passphrase() { luks_current_Disk_Recovery_Key_passphrase=$(cat /tmp/secret/luks_current_Disk_Recovery_Key_passphrase) else TRACE_FUNC - echo -e "\nEnter the current LUKS Disk Recovery Key passphrase (Configured at OS installation or by OEM):" - read -r luks_current_Disk_Recovery_Key_passphrase + INPUT "Enter the current LUKS Disk Recovery Key passphrase (configured at OS installation or by OEM):" -r luks_current_Disk_Recovery_Key_passphrase fi elif [ -z "$luks_new_Disk_Recovery_Key_passphrase" ]; then whiptail --title 'Changing LUKS Disk Recovery Key passphrase' --msgbox \ "Please choose a strong passphrase of your own.\n\n**DICEWARE passphrase methodology is STRONGLY ADVISED.**\n\nHit Enter to continue" 0 80 - echo -e "\nEnter your desired replacement for the actual LUKS Disk Recovery Key passphrase (At least 8 characters long):" while [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; do - read -r luks_new_Disk_Recovery_Key_passphrase + INPUT "Enter your new LUKS Disk Recovery Key passphrase (at least 8 characters):" -r luks_new_Disk_Recovery_Key_passphrase + if [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; then + warn "Passphrase must be at least 8 characters long. Please try again." + unset luks_new_Disk_Recovery_Key_passphrase + fi done fi @@ -558,14 +539,14 @@ luks_change_passphrase() { continue fi - echo -e "\nChanging $luks_container LUKS encrypted disk passphrase to the new LUKS Disk Recovery Key passphrase..." + STATUS "Changing $luks_container LUKS passphrase to new Disk Recovery Key passphrase" if ! DO_WITH_DEBUG cryptsetup luksChangeKey "$luks_container" --key-file=/tmp/secret/luks_current_Disk_Recovery_Key_passphrase /tmp/secret/luks_new_Disk_Recovery_Key_passphrase; then whiptail_error --title 'Failed to change LUKS passphrase' --msgbox \ "Failed to change the passphrase for $luks_container.\nPlease try again." 0 80 continue fi - echo "Success changing passphrase for $luks_container." + STATUS_OK "Success: passphrase changed for $luks_container" done # Export the new passphrase if all containers were processed successfully diff --git a/initrd/init b/initrd/init index 8f8e82285..4ca0ed365 100755 --- a/initrd/init +++ b/initrd/init @@ -110,10 +110,10 @@ 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 + NOTE "Quiet mode enabled from board configuration: refer to /tmp/debug.log for boot measurements 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 + NOTE "Runtime applied Quiet mode: refer to /tmp/debug.log for additional boot measurements traces past this point" + NOTE "To suppress earlier boot measurements 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 @@ -121,8 +121,8 @@ 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 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 + NOTE "Early boot measurements traces were suppressed per CONFIG_QUIET_MODE=y in your board configuration at build time (/etc/config)" + NOTE "Runtime applied Quiet mode disabled: refer to /tmp/debug.log for cbfs-init related traces prior of this point" fi fi @@ -130,14 +130,14 @@ TRACE_FUNC # make sure we have sysctl requirements if [ ! -d /proc/sys ]; then - warn "BUG!!! The following requirements to apply runtime kernel tweaks are missing:" + warn "BUG: The following requirements to apply runtime kernel tweaks are missing:" warn "CONFIG_SYSCTL=y" warn "CONFIG_PROC_SYSCTL=y" warn "Please open an issue" fi if [ ! -e /proc/sys/vm/panic_on_oom ]; then - warn "BUG!!! Requirements to setup Panic when under Out Of Memory situation through PROC_SYSCTL are missing (panic_on_oom was not enabled)" + warn "BUG: Requirements to setup Panic when under Out Of Memory situation through PROC_SYSCTL are missing (panic_on_oom was not enabled)" warn "Please open an issue" else DEBUG "Applying panic_on_oom setting to sysctl" @@ -210,14 +210,14 @@ if [ "$boot_option" = "r" ]; then exit elif [ "$boot_option" = "o" ]; then # Launch OEM Factory Reset mode - echo -e "***** Entering OEM Factory Reset mode\n" >/dev/tty0 + STATUS "Entering OEM Factory Reset mode" oem-factory-reset --mode oem # just in case... exit fi if [ "$CONFIG_BASIC" = "y" ]; then - echo -e "***** BASIC mode: tamper detection disabled\n" >/dev/tty0 + STATUS "BASIC mode: tamper detection disabled" fi # export firmware version @@ -249,14 +249,14 @@ if [ ! -x "$CONFIG_BOOTSCRIPT" -a ! -x "$CONFIG_BOOTSCRIPT_NETWORK" ]; then recovery 'Boot script missing? Entering recovery shell' else if [ -x "$CONFIG_BOOTSCRIPT_NETWORK" ]; then - echo '***** Network Boot:' $CONFIG_BOOTSCRIPT_NETWORK + STATUS "Network Boot: $CONFIG_BOOTSCRIPT_NETWORK" $CONFIG_BOOTSCRIPT_NETWORK - echo '***** Network Boot Completed:' $CONFIG_BOOTSCRIPT_NETWORK + STATUS "Network Boot completed: $CONFIG_BOOTSCRIPT_NETWORK" # not blocking fi if [ -x "$CONFIG_BOOTSCRIPT" ]; then - echo '***** Normal boot:' $CONFIG_BOOTSCRIPT + STATUS "Normal boot: $CONFIG_BOOTSCRIPT" if [ -x /bin/setsid ] && [ -x /bin/agetty ]; then for console in $CONFIG_BOOT_EXTRA_TTYS; do diff --git a/initrd/mount-boot b/initrd/mount-boot index be02e08d8..1aecbcb1a 100755 --- a/initrd/mount-boot +++ b/initrd/mount-boot @@ -4,6 +4,8 @@ # the trusted key database, and execute it to mount # the /boot filesystem +. /etc/functions + dev="$1" offset="$2" @@ -24,11 +26,7 @@ fi # dev_size_file="/sys/class/block/`basename $dev`/size" if [ ! -r "$dev_size_file" ]; then - echo >&2 '!!!!!' - echo >&2 '!!!!! $dev file $dev_size_file not found' - echo >&2 '!!!!! Dropping to recovery shell' - echo >&2 '!!!!!' - exit -1 + recovery "Device size file $dev_size_file not found" fi dev_blocks=`cat "$dev_size_file"` @@ -37,22 +35,14 @@ dev_blocks=`cat "$dev_size_file"` # Extract the signed file from the hard disk image # if ! dd if="$dev" of="$cmd_sig" bs=512 skip="`expr $dev_blocks - 1`" > /dev/null 2>&1; then - echo >&2 '!!!!!' - echo >&2 '!!!!! Boot block extraction failed' - echo >&2 '!!!!! Dropping to recovery shell' - echo >&2 '!!!!!' - exit -1 + recovery "Boot block extraction failed" fi # # Validate the file # if ! gpgv --keyring /trustedkeys.gpg "$cmd_sig"; then - echo >&2 '!!!!!' - echo >&2 '!!!!! GPG signature on block failed' - echo >&2 '!!!!! Dropping to recovery shell' - echo >&2 '!!!!!' - exit -1 + recovery "GPG signature on boot block failed" fi # diff --git a/initrd/sbin/config-dhcp.sh b/initrd/sbin/config-dhcp.sh index 6dcb8297b..dbcf5757b 100755 --- a/initrd/sbin/config-dhcp.sh +++ b/initrd/sbin/config-dhcp.sh @@ -24,7 +24,7 @@ case "$1" in /sbin/ifconfig $interface $ip $BROADCAST $NETMASK if [ -n "$router" ] ; then - echo "deleting routers" + DEBUG "deleting routers" while route del default gw 0.0.0.0 dev $interface ; do : done @@ -37,7 +37,7 @@ case "$1" in echo -n > $RESOLV_CONF [ -n "$domain" ] && echo search $domain >> $RESOLV_CONF for i in $dns ; do - echo adding dns $i + DEBUG "adding dns $i" echo nameserver $i >> $RESOLV_CONF done ;; diff --git a/initrd/sbin/insmod b/initrd/sbin/insmod index 7ca6a28fe..9065326e3 100755 --- a/initrd/sbin/insmod +++ b/initrd/sbin/insmod @@ -45,13 +45,13 @@ if [ -z "$tpm_missing" ]; then # different PCR measurement. if [ -n "$*" ]; then TRACE_FUNC - INFO "Extending with module parameters and the module's content" + LOG "Extending with module parameters and the module's content" tpmr extend -ix "$MODULE_PCR" -ic "$*" tpmr extend -ix "$MODULE_PCR" -if "$MODULE" \ || die "$MODULE: tpm extend failed" else TRACE_FUNC - INFO "No module parameters, extending only with the module's content" + LOG "No module parameters, extending only with the module's content" tpmr extend -ix "$MODULE_PCR" -if "$MODULE" \ || die "$MODULE: tpm extend failed" fi diff --git a/targets/qemu.md b/targets/qemu.md index 0a7767f17..866e5ede0 100644 --- a/targets/qemu.md +++ b/targets/qemu.md @@ -181,6 +181,19 @@ How I tested these wrappers (smoke checks) Troubleshooting --- +- Reuse provisioned canokey state across QEMU board build dirs: + - QEMU boards that use the default virtual token store canokey state at `build/x86//.canokey-file` (from `targets/qemu.mk`: `-device canokey,file=$(build)/$(BOARD)/.canokey-file`). + - After provisioning via Heads OEM reset/re-ownership in one QEMU board, you can copy that file into another QEMU board build directory to reuse the same virtual smartcard identity/public key material. + - Example: + - `cp build/x86/qemu-coreboot-fbwhiptail-tpm2/.canokey-file build/x86/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/.canokey-file` + - This is useful when troubleshooting TPM workflows while keeping the same token identity across variants. + +- TPM2 interaction capture (pcap) for debugging, similar to a bus sniffer: + - On TPM2 boards, set `export CONFIG_TPM2_CAPTURE_PCAP=y` in the board config. + - Heads `tpmr` then uses the pcap TCTI and writes captures to `/tmp/tpm0.pcap` inside the booted Heads environment. + - Save/copy that file from the guest (mount-usb --mode rw) and inspect it with Wireshark to analyze TPM command/response traffic. + - This is intended for TPM2 boards (for example the `qemu-coreboot-fbwhiptail-tpm2*` targets). + - Quick checks: - `echo $DISPLAY` — ensure `DISPLAY` is set on the host. - `command -v xauth` — preferred for programmatic Xauthority cookies.