From f45b373200692c71012fc43da3c06b327c6e696a Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 30 Jun 2026 15:51:16 -0400 Subject: [PATCH 01/15] initrd: fix regressions from PR #2130 gui_functions.sh: - Guard check_config /boot force: skip when /tmp/kexec already has files (kexec-select-boot main loop already ran check_config with GPG signature verification). - Restore CONFIG_ROOT_CHECK_AT_BOOT support (was lost in refactor). kexec-select-boot.sh: - Restore TPM2 primary key handle hash check (was removed from script body before verify_rollback_counter). Signed-off-by: Thierry Laurion --- initrd/bin/kexec-select-boot.sh | 18 ++++++++++++++++++ initrd/etc/gui_functions.sh | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index dbf5014ce..3f2e7d52d 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -69,6 +69,24 @@ if [ -z "$_HEADS_TEST" ]; then paramsdir="${paramsdir%%/}" fi +PRIMHASH_FILE="$paramsdir/kexec_primhdl_hash.txt" +if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + if [ -s "$PRIMHASH_FILE" ]; then + sha256sum -c "$PRIMHASH_FILE" >/dev/null 2>&1 || + { + WARN "Hash of TPM2 primary key handle mismatch - if you have not intentionally regenerated the TPM2 primary key, 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 - rebuild it by setting a default OS to boot: Options -> Boot Options -> Show OS Boot Menu -> pick OS -> Make default" + default_failed="y" + DEBUG "Hash of TPM2 primary key handle does not exist under $PRIMHASH_FILE" + fi +fi + verify_rollback_counter() { TRACE_FUNC TPM_COUNTER=$(grep counter $TMP_ROLLBACK_FILE | cut -d- -f2) diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index bf8b1de93..faa491984 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -837,7 +837,9 @@ get_inverted_config_display_action() { verify_global_hashes() { TRACE_FUNC # Check the hashes of all the files, ignoring signatures for now - check_config /boot force + if [ ! -r /tmp/kexec/kexec_hashes.txt ]; then + check_config /boot force + fi TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" TMP_PACKAGE_TRIGGER_PRE="/tmp/kexec/kexec_package_trigger_pre.txt" @@ -847,6 +849,20 @@ verify_global_hashes() { DEBUG "verify_global_hashes: verify_checksums passed" valid_hash="y" valid_global_hash="y" + # If user enables it, check root hashes before boot as well + if [[ "$CONFIG_ROOT_CHECK_AT_BOOT" = "y" && "$force_menu" == "n" ]]; then + DEBUG "verify_global_hashes: checking root hashes" + if root-hashes-gui.sh -c; then + STATUS_OK "Verified boot file checksums" + else + # root-hashes-gui.sh handles the GUI error menu, just DIE here + if [ "$gui_menu" = "y" ]; then + whiptail_error --title 'ERROR: Root Hash Mismatch' \ + --msgbox "The root hash check failed!\nExiting to a recovery shell" 0 80 + fi + DIE "root hash mismatch, see /tmp/hash_output_mismatches for details" + fi + fi return 0 elif [[ ! -f "$TMP_HASH_FILE" || ! -f "$TMP_TREE_FILE" ]]; then DEBUG "verify_global_hashes: missing hash or tree file" From a97358f2798d5b4fc7720a2361108f8772ab1536 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 30 Jun 2026 15:51:54 -0400 Subject: [PATCH 02/15] initrd: clarify STATUS/STATUS_OK messages + fix functions.sh scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gui_functions.sh (verify_global_hashes): - _sig_verified tracking for context-aware messages. - 'Verifying boot file checksums' / 'Boot file checksums verified' (hash-only path from gui-init.sh). - 'Verifying boot file checksums against signed boot hashes' / 'Boot file checksums verified against signed boot hashes' (signature-backed path from kexec-select-boot). - Whiptail fold width 76->75 with char-level fallback. - Technical DEBUG lines. kexec-select-boot.sh: - Cmdline display fold width 78->75. functions.sh (check_config): - 'Verifying GPG signature on boot hashes' / 'Boot hashes signature verified'. - STATUS_OK scoped inside if[!force] — no longer prints when check_config is called with 'force' (no GPG verification). Signed-off-by: Thierry Laurion --- initrd/bin/kexec-select-boot.sh | 2 +- initrd/etc/functions.sh | 4 +-- initrd/etc/gui_functions.sh | 47 ++++++++++++++++++++++++++------- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 3f2e7d52d..6c2f3229e 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -241,7 +241,7 @@ confirm_menu_option() { # exactly matches what kexec-boot.sh will execute. local folded_cmdline folded_cmdline=$(_build_final_cmdline "$params" "$add" "$CONFIG_BOOT_KERNEL_REMOVE" "$CONFIG_BOOT_KERNEL_ADD") - folded_cmdline=$(echo "$folded_cmdline" | fold -s -w 78) + folded_cmdline=$(echo "$folded_cmdline" | fold -s -w 75) whiptail_warning --title "Confirm boot details" \ --menu "$name\n\nKernel: $kernel\nInitramfs: ${initrd:--}\nOriginal kernel cmdline: ${params:--}\n${CONFIG_BOOT_KERNEL_ADD:+Board adds: $CONFIG_BOOT_KERNEL_ADD\n}${CONFIG_BOOT_KERNEL_REMOVE:+Board removes: $CONFIG_BOOT_KERNEL_REMOVE\n}${add:+ISO params: $add\n}\nFinal kernel cmdline:\n$folded_cmdline\n" 0 80 8 \ -- 'y' "Boot" 'd' "${default_text}" 'b' "Back to menu" \ diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index ae6de9b7d..d2406612d 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -2229,15 +2229,15 @@ check_config() { # output matches exactly what was produced during signing, where the same # relative names were used. Absolute paths would differ between the # signing staging dir and $paramsdir, causing a spurious mismatch. - STATUS "Verifying GPG signature on kexec boot params" + STATUS "Verifying GPG signature on boot hashes" DEBUG "check_config: running (cd $paramsdir && sha256sum ${param_files[*]}) | gpgv.sh $paramsdir/kexec.sig" if ! (cd "$paramsdir" && sha256sum "${param_files[@]}") | gpgv.sh "$paramsdir/kexec.sig" - 2> >(SINK_LOG "gpgv kexec.sig"); then DIE 'Invalid signature on kexec boot params' fi + STATUS_OK "Boot hashes signature verified" fi - STATUS_OK "GPG signature on kexec boot params verified" DEBUG "check_config: copying kexec*.txt from $paramsdir to /tmp/kexec" cp "$paramsdir"/kexec*.txt /tmp/kexec || DIE "Failed to copy kexec boot params to tmp" diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index faa491984..ae38cc3ba 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -37,7 +37,7 @@ mount_usb() { # 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 +# through printf '%b' | fold -s -w 75 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() { @@ -45,13 +45,14 @@ _whiptail_preprocess_args() { local _wrap_next=0 _arg for _arg in "$@"; do if [ "$_wrap_next" = 1 ]; then - # fold -s breaks at spaces (preserves word boundaries). - # For paths/tokens with no spaces, fall back to - # character-level fold so whiptail can display them. + # fold -s breaks at spaces, preserving word boundaries. + # For long tokens with no spaces (e.g. file paths), + # fall back to character-level fold so they don't + # overflow the dialog. local _folded - _folded=$(printf '%b' "$_arg" | fold -s -w 76) + _folded=$(printf '%b' "$_arg" | fold -s -w 75) if echo "$_folded" | grep -q '.\{76\}'; then - _folded=$(printf '%b' "$_arg" | fold -w 76) + _folded=$(printf '%b' "$_arg" | fold -w 75) fi _WHIPTAIL_ARGS+=("$_folded") _wrap_next=0 @@ -836,24 +837,46 @@ get_inverted_config_display_action() { # (gui_functions.sh), BG_COLOR_MAIN_MENU (exported from gui-init.sh). verify_global_hashes() { TRACE_FUNC - # Check the hashes of all the files, ignoring signatures for now + local _sig_verified="n" + # Called from two contexts: + # 1. gui-init.sh (attempt_default_boot / select_os_boot_option): + # /tmp/kexec/ is empty — run check_config to populate it. + # 2. kexec-select-boot.sh's main loop (via check_config $paramsdir + # which verifies the GPG signature on kexec files): + # /tmp/kexec/ is already populated — skip check_config here to + # avoid the destructive rm -rf /tmp/kexec/* + re-copy. if [ ! -r /tmp/kexec/kexec_hashes.txt ]; then check_config /boot force + _sig_verified="n" + else + # Files already in /tmp/kexec — check_config $paramsdir in + # kexec-select-boot's main loop verified the GPG signature. + _sig_verified="y" fi TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" TMP_PACKAGE_TRIGGER_PRE="/tmp/kexec/kexec_package_trigger_pre.txt" TMP_PACKAGE_TRIGGER_POST="/tmp/kexec/kexec_package_trigger_post.txt" + if [ "$_sig_verified" = "y" ]; then + STATUS "Verifying boot file checksums against signed boot hashes" + else + STATUS "Verifying boot file checksums" + fi + DEBUG "verify_global_hashes: checking /boot files against $TMP_HASH_FILE" if verify_checksums /boot; then - DEBUG "verify_global_hashes: verify_checksums passed" + DEBUG "verify_global_hashes: /boot files match checksums in $TMP_HASH_FILE" valid_hash="y" valid_global_hash="y" # If user enables it, check root hashes before boot as well if [[ "$CONFIG_ROOT_CHECK_AT_BOOT" = "y" && "$force_menu" == "n" ]]; then DEBUG "verify_global_hashes: checking root hashes" if root-hashes-gui.sh -c; then - STATUS_OK "Verified boot file checksums" + if [ "$_sig_verified" = "y" ]; then + STATUS_OK "Boot file and root checksums verified against signed boot hashes" + else + STATUS_OK "Boot file and root checksums verified" + fi else # root-hashes-gui.sh handles the GUI error menu, just DIE here if [ "$gui_menu" = "y" ]; then @@ -862,6 +885,12 @@ verify_global_hashes() { fi DIE "root hash mismatch, see /tmp/hash_output_mismatches for details" fi + else + if [ "$_sig_verified" = "y" ]; then + STATUS_OK "Boot file checksums verified against signed boot hashes" + else + STATUS_OK "Boot file checksums verified" + fi fi return 0 elif [[ ! -f "$TMP_HASH_FILE" || ! -f "$TMP_TREE_FILE" ]]; then From dd07fe9a014487663a87c3b6e98b497e878566ea Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 30 Jun 2026 15:52:05 -0400 Subject: [PATCH 03/15] Makefile: use git init+fetch + cd instead of git clone + git -C git clone inside the Heads working tree creates a .git worktree reference file pointing back to the parent .git/modules/, causing all subsequent git commands to operate on the Heads repo instead of the clone. Replace git clone + git -C with git init + git fetch + cd + bare git to create a standalone .git directory. Signed-off-by: Thierry Laurion --- Makefile | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 462e04c0b..abdd8b461 100644 --- a/Makefile +++ b/Makefile @@ -472,26 +472,40 @@ define define_module = # module-specific cleanup action to get rid of it. $(build)/$($1_base_dir)/.canary: FORCE if [ ! -e "$$@" ] && [ ! -d "$(build)/$($1_base_dir)" ]; then \ - echo "INFO: .canary file and directory not found. Cloning repository $($1_repo) into $(build)/$($1_base_dir)" && \ - git clone $($1_repo) "$(build)/$($1_base_dir)" && \ + echo "INFO: .canary file and directory not found. Creating standalone clone of $($1_repo) at $(build)/$($1_base_dir)" && \ + mkdir -p "$(build)/$($1_base_dir)" && \ + cd "$(build)/$($1_base_dir)" && \ + echo "INFO: Initializing git repository" && \ + git init 2>/dev/null && \ + echo "INFO: Adding remote origin: $($1_repo)" && \ + git remote add origin $($1_repo) && \ + echo "INFO: Fetching commit $($1_commit_hash) (without recursing submodules)" && \ + git fetch origin $($1_commit_hash) --recurse-submodules=no && \ echo "INFO: Resetting repository to commit $($1_commit_hash)" && \ - git -C "$(build)/$($1_base_dir)" reset --hard $($1_commit_hash) && \ + git reset --hard $($1_commit_hash) && \ echo "INFO: Creating .canary file with repo and commit hash" && \ echo -n '$($1_repo)|$($1_commit_hash)' > "$$@" ; \ elif [ ! -e "$$@" ] || [ "$$$$(cat "$$@")" != '$($1_repo)|$($1_commit_hash)' ]; then \ - echo "INFO: .canary file missing or differs. Resetting $1 to $($1_repo) at $($1_commit_hash)" && \ - git -C "$(build)/$($1_base_dir)" reset --hard HEAD^ && \ - echo "INFO: Fetching commit $($1_commit_hash) from $($1_repo) (without recursing submodules)" && \ - git -C "$(build)/$($1_base_dir)" fetch $($1_repo) $($1_commit_hash) --recurse-submodules=no && \ + echo "INFO: .canary file missing or differs. Converting to standalone clone at $(build)/$($1_base_dir)" && \ + echo "INFO: Removing stale worktree reference if present" && \ + rm -f "$(build)/$($1_base_dir)/.git" 2>/dev/null; \ + cd "$(build)/$($1_base_dir)" && \ + echo "INFO: Initializing git repository" && \ + git init 2>/dev/null; \ + echo "INFO: Configuring remote origin: $($1_repo)" && \ + git remote remove origin 2>/dev/null; \ + git remote add origin $($1_repo) && \ + echo "INFO: Fetching commit $($1_commit_hash) (without recursing submodules)" && \ + git fetch origin $($1_commit_hash) --recurse-submodules=no && \ echo "INFO: Resetting repository to commit $($1_commit_hash)" && \ - git -C "$(build)/$($1_base_dir)" reset --hard $($1_commit_hash) && \ + git reset --hard $($1_commit_hash) && \ echo "INFO: Cleaning repository directory (including payloads and util/cbmem)" && \ - git -C "$(build)/$($1_base_dir)" clean -df && \ - git -C "$(build)/$($1_base_dir)" clean -dffx payloads util/cbmem && \ + git clean -df && \ + git clean -dffx payloads util/cbmem && \ echo "INFO: Synchronizing submodules" && \ - git -C "$(build)/$($1_base_dir)" submodule sync && \ + git submodule sync && \ echo "INFO: Updating submodules (init and checkout)" && \ - git -C "$(build)/$($1_base_dir)" submodule update --init --checkout && \ + git submodule update --init --checkout && \ echo "INFO: Cleaning board-specific build directories to prevent stale artifacts" && \ rm -rf "$(build)/$(BOARD)" "$(build)/$($1_base_dir)/$(BOARD)" && \ echo "INFO: Recreating board directories" && \ From 54970f91ea3841fff50d5ecc23cf0719794c012b Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 30 Jun 2026 15:52:05 -0400 Subject: [PATCH 04/15] initrd/bin/seal-hotpkey.sh: combine GPG key age + name check for default PIN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The factory default PIN attempt used a 30-day window based on GPG key age, which also triggered on recent OEM resets where the user had customized PINs — wasting a PIN retry. Add GPG user name check ('OEM Key') alongside the time check: skip default PIN if EITHER the name is not 'OEM Key' (customized) OR the key is older than 30 days. Only try the default when BOTH conditions pass: recent OEM reset AND user kept all defaults. Signed-off-by: Thierry Laurion --- initrd/bin/seal-hotpkey.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/initrd/bin/seal-hotpkey.sh b/initrd/bin/seal-hotpkey.sh index 85b3c3b4c..61111709a 100755 --- a/initrd/bin/seal-hotpkey.sh +++ b/initrd/bin/seal-hotpkey.sh @@ -140,22 +140,22 @@ show_pin_retries() { STATUS "$DONGLE_BRAND ${prompt_message} PIN retries remaining: $(pin_color "$admin_pin_retries")${admin_pin_retries}\033[0m" } -# Try using factory default admin PIN for 1 month following OEM reset to ease -# initial setup. But don't do it forever to encourage changing the PIN and -# so PIN attempts are not consumed by the default attempt. +# Try the factory default admin PIN only when the GPG key still has the +# OEM default name ("OEM Key"), meaning the user hasn't customized the +# dongle yet. The GPG name is readable without consuming PIN attempts, +# unlike probing via hotp_initialize which burns a retry. +# Always require at least 3 PIN attempts remaining as a safety floor. admin_pin="12345678" month_secs="$((30 * 24 * 60 * 60))" +gpg_user_name="$(gpg --list-keys --with-colons 2>/dev/null | grep -m 1 '^uid:' | cut -d: -f10)" 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 - 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 +if [ "$gpg_user_name" != "OEM Key" ] || [ "$((now_date - gpg_key_create_time))" -gt "$month_secs" ]; then + DEBUG "Not trying default PIN ($admin_pin): key_age=$(($((now_date - gpg_key_create_time)) / 86400))d, name='$gpg_user_name'" elif [ "$admin_pin_retries" -lt 3 ]; then DEBUG "Not trying default PIN ($admin_pin): only $admin_pin_retries attempt(s) left" else - STATUS "Trying ${prompt_message} PIN to seal HOTP secret on $DONGLE_BRAND" + STATUS "Trying factory default PIN ($admin_pin) to seal HOTP secret on $DONGLE_BRAND" + DEBUG "Attempting default PIN: key_age=$(($((now_date - gpg_key_create_time)) / 86400))d, GPG name='$gpg_user_name'" # NK3 requires physical touch confirmation for the initialize operation if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then NOTE "Nitrokey 3 requires physical presence: touch the dongle when prompted" From bc4a16e3f96ffc6f138d0608864bbd224347853d Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 30 Jun 2026 16:18:32 -0400 Subject: [PATCH 05/15] initrd: remove duplicate folds, centralized in _whiptail_preprocess_args _whiptail_preprocess_args already folds --menu/--msgbox text at 75 chars. Manual fold before passing to whiptail creates a double-fold that can inflate dialog height and produce incorrect wrapping. kexec-select-boot.sh (confirm_menu_option): - Remove fold -s -w 75 on the kernel cmdline before embedding in --menu text. _whiptail_preprocess_args handles the wrapping. oem-factory-reset.sh (secrets confirmation): - Remove echo -e | fold -w before --msgbox. Same centralization. Signed-off-by: Thierry Laurion --- initrd/bin/kexec-select-boot.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 6c2f3229e..94cdf8723 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -239,11 +239,8 @@ confirm_menu_option() { [[ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" = "y" ]] && default_text="${default_text} and boot" # Build final cmdline preview using shared function so it # exactly matches what kexec-boot.sh will execute. - local folded_cmdline - folded_cmdline=$(_build_final_cmdline "$params" "$add" "$CONFIG_BOOT_KERNEL_REMOVE" "$CONFIG_BOOT_KERNEL_ADD") - folded_cmdline=$(echo "$folded_cmdline" | fold -s -w 75) whiptail_warning --title "Confirm boot details" \ - --menu "$name\n\nKernel: $kernel\nInitramfs: ${initrd:--}\nOriginal kernel cmdline: ${params:--}\n${CONFIG_BOOT_KERNEL_ADD:+Board adds: $CONFIG_BOOT_KERNEL_ADD\n}${CONFIG_BOOT_KERNEL_REMOVE:+Board removes: $CONFIG_BOOT_KERNEL_REMOVE\n}${add:+ISO params: $add\n}\nFinal kernel cmdline:\n$folded_cmdline\n" 0 80 8 \ + --menu "$name\n\nKernel: $kernel\nInitramfs: ${initrd:--}\nOriginal kernel cmdline: ${params:--}\n${CONFIG_BOOT_KERNEL_ADD:+Board adds: $CONFIG_BOOT_KERNEL_ADD\n}${CONFIG_BOOT_KERNEL_REMOVE:+Board removes: $CONFIG_BOOT_KERNEL_REMOVE\n}${add:+ISO params: $add\n}\nFinal kernel cmdline:\n$(_build_final_cmdline "$params" "$add" "$CONFIG_BOOT_KERNEL_REMOVE" "$CONFIG_BOOT_KERNEL_ADD")\n" 0 80 8 \ -- 'y' "Boot" 'd' "${default_text}" 'b' "Back to menu" \ 2>/tmp/whiptail && option_confirm=$(cat /tmp/whiptail) || option_confirm="b" else From 520c9627cace9e3649d1a54feb3298100636d7ca Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 30 Jun 2026 22:19:40 -0400 Subject: [PATCH 06/15] initrd/bin/kexec-select-boot.sh: disable GUI confirm dialog, use console fallback The whiptail --menu confirmation dialog body text (original + final kernel cmdline) can exceed terminal height for ISO boot with long parameters. Comment out the GUI path with a TODO for future simplification; always use text-based console confirmation (STATUS + INPUT prompt) regardless of gui_menu setting. Signed-off-by: Thierry Laurion --- initrd/bin/kexec-select-boot.sh | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 94cdf8723..3dccbff95 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -234,16 +234,19 @@ confirm_menu_option() { # so users can change their selection without restarting the boot flow. # The full cmdline combines the entry's parsed params with the global ADD # params (injected by kexec-iso-init.sh for ISO boot). - if [ "$gui_menu" = "y" ]; then - default_text="Make default" - [[ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" = "y" ]] && default_text="${default_text} and boot" - # Build final cmdline preview using shared function so it - # exactly matches what kexec-boot.sh will execute. - whiptail_warning --title "Confirm boot details" \ - --menu "$name\n\nKernel: $kernel\nInitramfs: ${initrd:--}\nOriginal kernel cmdline: ${params:--}\n${CONFIG_BOOT_KERNEL_ADD:+Board adds: $CONFIG_BOOT_KERNEL_ADD\n}${CONFIG_BOOT_KERNEL_REMOVE:+Board removes: $CONFIG_BOOT_KERNEL_REMOVE\n}${add:+ISO params: $add\n}\nFinal kernel cmdline:\n$(_build_final_cmdline "$params" "$add" "$CONFIG_BOOT_KERNEL_REMOVE" "$CONFIG_BOOT_KERNEL_ADD")\n" 0 80 8 \ - -- 'y' "Boot" 'd' "${default_text}" 'b' "Back to menu" \ - 2>/tmp/whiptail && option_confirm=$(cat /tmp/whiptail) || option_confirm="b" - else + + # TODO : simplify to be able to use whiptail; too big for QubesOS + # No GUI for now, sorry. + # if [ "$gui_menu" = "y" ]; then + # default_text="Make default" + # [[ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" = "y" ]] && default_text="${default_text} and boot" + # # Build final cmdline preview using shared function so it + # # exactly matches what kexec-boot.sh will execute. + # whiptail_warning --title "Confirm boot details" \ + # --menu "$name\n\nKernel: $kernel\nInitramfs: ${initrd:--}\nOriginal kernel cmdline: ${params:--}\n${CONFIG_BOOT_KERNEL_ADD:+Board adds: $CONFIG_BOOT_KERNEL_ADD\n}${CONFIG_BOOT_KERNEL_REMOVE:+Board removes: $CONFIG_BOOT_KERNEL_REMOVE\n}${add:+ISO params: $add\n}\nFinal kernel cmdline:\n$(_build_final_cmdline "$params" "$add" "$CONFIG_BOOT_KERNEL_REMOVE" "$CONFIG_BOOT_KERNEL_ADD")\n" 0 80 8 \ + # -- 'y' "Boot" 'd' "${default_text}" 'b' "Back to menu" \ + # 2>/tmp/whiptail && option_confirm=$(cat /tmp/whiptail) || option_confirm="b" + #else STATUS " Confirm boot details for $name:" STATUS " Kernel: $kernel" STATUS " Initramfs: ${initrd:--}" @@ -258,7 +261,7 @@ confirm_menu_option() { INPUT "Boot (Y), make default (d), back to menu (b) [Y/d/b]:" -n 1 option_confirm [ -z "$option_confirm" ] && option_confirm="y" return 0 - fi + #fi } parse_option() { From 6d58336368254faba39bceacefc7a923c92c2436 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 30 Jun 2026 19:43:00 -0400 Subject: [PATCH 07/15] .circleci/config.yml: update musl-cross-make cache paths for 227df8b bump Bugfix for merged PR #2136 (3dfec51cf06). The musl-cross-make version was bumped from fd6be58 to 227df8b but the CircleCI cache paths in .config.yml were not updated, causing cache miss and stale artifact usage on subsequent CI runs. Replace 4 hardcoded fd6be58 directory references with 227df8b. Signed-off-by: Thierry Laurion --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3bdcb9b1e..916358216 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -253,7 +253,7 @@ jobs: - save_cache: key: x86-musl-cross-make-nix-docker-heads-{{ checksum "./tmpDir/musl-cross-make.sha256sums" }}{{ .Environment.CACHE_VERSION }} paths: - - build/x86/musl-cross-make-fd6be58297ee21fcba89216ccd0d4aca1e3f1c5c + - build/x86/musl-cross-make-227df8b99103f9c59f6570babf892978e293082f - crossgcc/x86 - install/x86 - packages/x86 @@ -305,7 +305,7 @@ jobs: key: x86-coreboot-musl-cross-make-nix-docker-heads-{{ checksum "./tmpDir/coreboot_musl-cross-make.sha256sums" }}{{ .Environment.CACHE_VERSION }}-<< parameters.coreboot_dir >> paths: - build/x86/<< parameters.coreboot_dir >> - - build/x86/musl-cross-make-fd6be58297ee21fcba89216ccd0d4aca1e3f1c5c + - build/x86/musl-cross-make-227df8b99103f9c59f6570babf892978e293082f - crossgcc/x86 - install/x86 - packages/x86 @@ -346,7 +346,7 @@ jobs: - save_cache: key: ppc64-musl-cross-make-nix-docker-heads-{{ checksum "./tmpDir/musl-cross-make.sha256sums" }}{{ .Environment.CACHE_VERSION }} paths: - - build/ppc64/musl-cross-make-fd6be58297ee21fcba89216ccd0d4aca1e3f1c5c + - build/ppc64/musl-cross-make-227df8b99103f9c59f6570babf892978e293082f - crossgcc/ppc64 - install/ppc64 - packages/ppc64 @@ -406,7 +406,7 @@ jobs: key: ppc64-coreboot-musl-cross-make-nix-docker-heads-{{ checksum "./tmpDir/coreboot_musl-cross-make.sha256sums" }}{{ .Environment.CACHE_VERSION }}-<< parameters.coreboot_dir >> paths: - build/ppc64/<< parameters.coreboot_dir >> - - build/ppc64/musl-cross-make-fd6be58297ee21fcba89216ccd0d4aca1e3f1c5c + - build/ppc64/musl-cross-make-227df8b99103f9c59f6570babf892978e293082f - crossgcc/ppc64 - install/ppc64 - packages/ppc64 From 25d76bada4a92c8a5f32e946aca4961a9751daf2 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Jul 2026 10:51:03 -0400 Subject: [PATCH 08/15] initrd/etc/functions.sh: _build_final_cmdline dedup Board ADD + apply REMOVE to Board ADD Two bugs fixed: 1. Duplicate keywords from Board ADD: was appended unconditionally via ' = ', adding words already present (e.g. 'quiet' in original params AND in CONFIG_BOOT_KERNEL_ADD). Now appends per-word with duplicate check. 2. REMOVE was not applied to Board ADD: the for loop over only stripped words from _clean_add and _iso_params, so Board ADD params matching REMOVE entries were never removed. Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index d2406612d..b2af54054 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -3547,15 +3547,18 @@ _build_final_cmdline() { # Clean ADD: strip GRUB --- separator _clean_add=$(echo "$_param_add" | sed 's/ --- / /g;s/^--- //g;s/ ---$//g' | xargs) - # Apply REMOVE to both ADD and ISO params + # Apply REMOVE to ADD, ISO params, and Board ADD for _remove_word in $_param_remove; do _clean_add=" $_clean_add " _clean_add="${_clean_add// $_remove_word / }" _iso_params=" $_iso_params " _iso_params="${_iso_params// $_remove_word / }" + _board_add=" $_board_add " + _board_add="${_board_add// $_remove_word / }" done _clean_add=$(echo "${_clean_add# }" | xargs) _iso_params=$(echo "${_iso_params# }" | xargs) + _board_add=$(echo "${_board_add# }" | xargs) DEBUG "_build_final_cmdline: after remove on ADD='$_clean_add'" DEBUG "_build_final_cmdline: after remove on iso='$_iso_params'" @@ -3586,8 +3589,15 @@ _build_final_cmdline() { fi done - # Append Board ADD last (always wins -- never touched by enforce) - _combined=$(echo "$_combined $_board_add" | xargs) + # Append Board ADD last (always wins -- never touched by enforce). + # Only append words not already present in _combined to avoid duplicates. + for _add_word in $_board_add; do + case " $_combined " in + *" $_add_word "*) ;; + *) _combined="$_combined $_add_word" ;; + esac + done + _combined=$(echo "$_combined" | xargs) DEBUG "_build_final_cmdline: final='$_combined'" echo "$_combined" } From 878c8fc09d3181928f8abb90b410c4b0c93eef40 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Jul 2026 10:54:57 -0400 Subject: [PATCH 09/15] =?UTF-8?q?initrd:=20fix=20whiptail=20prompts=20?= =?UTF-8?q?=E2=80=94=20'a=20to=20abort'=20doesn't=20work=20in=20fbwhiptail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fbwhiptail only recognizes menu options listed with -- (selectable entries). The 'a' key is silently ignored, so the prompt 'a to abort' misleads users. Pressing Esc causes whiptail to return non-zero, which is already handled (aborts with DIE or falls back to option_index='a'). kexec-select-boot.sh: 'a to abort' -> 'Esc to abort' in GUI menu prompts. Esc → whiptail fail → || option_index='a' → abort. Text-mode CLI prompts keep 'a to abort' (INPUT reads characters). gui_functions.sh: remove '[1-n, a to abort]' from file selector prompt since that dialog DIE's on Esc, no fallback. Signed-off-by: Thierry Laurion --- initrd/bin/kexec-select-boot.sh | 4 ++-- initrd/etc/gui_functions.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 3dccbff95..09111a974 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -174,9 +174,9 @@ get_menu_option() { fi if [ -n "$add" ]; then - local menu_prompt="Choose the boot option [1-$n, a to abort, b to select different ISO]:" + local menu_prompt="Choose the boot option [1-$n, Esc to abort, b to select different ISO]:" else - local menu_prompt="Choose the boot option [1-$n, a to abort]:" + local menu_prompt="Choose the boot option [1-$n, Esc to abort]:" fi whiptail_type $BG_COLOR_MAIN_MENU --title "Select your boot option" \ --menu "$menu_prompt" 0 80 8 \ diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index ae38cc3ba..968d51ef7 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -199,7 +199,7 @@ file_selector() { option_index="" while [ -z "$option_index" ]; do whiptail --title "${MENU_TITLE}" \ - --menu "${MENU_MSG} [1-$n, a to abort]:" 20 120 8 \ + --menu "${MENU_MSG}:" 20 120 8 \ -- "${CHOICE_ARGS[@]}" \ 2>/tmp/whiptail || DIE "Aborting" From a52c97bde039b07924e8abf1d0f2f47eca7fd7e1 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Jul 2026 10:59:43 -0400 Subject: [PATCH 10/15] initrd/etc/functions.sh: document preserve_rom, add STATUS/DEBUG per logging.md Added: - Function doc block: explains purpose (preserve runtime config across firmware updates), where called from (flash.sh), and what controls it (CLEAN flag, not a CONFIG_* setting). - STATUS/STATUS_OK for the overall preservation operation with file count. - DEBUG for each skipped or copied file with reasoning. - Cleanup of temp file (/tmp/rom.$$) on completion. - Early return when no heads/ files exist to preserve. Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index b2af54054..5fa713a22 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -478,19 +478,42 @@ ec_version() { preserve_rom() { TRACE_FUNC + # Preserve CBFS files matching 'heads/' from the currently running ROM + # into the new ROM being built for flashing. These files contain runtime + # configuration (GPG keyring, TOTP/HOTP secrets, LUKS key slots, etc.) + # that would otherwise be lost on each firmware update. + # + # Called from: flash.sh (when CLEAN=0, i.e. non-factory-flash). + # Controlled by: CLEAN flag (set to 1 for factory/OEM flashes to skip). + # No CONFIG_* setting directly controls this — flash.sh sets CLEAN + # based on the --clean / -c flag passed by the user or calling script. + # Files preserved: all CBFS type-50 entries under the 'heads/' prefix. new_rom="$1" old_files=$(cbfs -t 50 -l 2>/dev/null | grep "^heads/") + old_file_count=$(echo "$old_files" | wc -w) + + if [ "$old_file_count" -eq 0 ]; then + DEBUG "preserve_rom: no 'heads/' CBFS files to preserve" + return 0 + fi + + STATUS "Preserving $old_file_count heads/ runtime configuration file(s)" + DEBUG "preserve_rom: scanning $new_rom for existing heads/* entries to skip" for old_file in $(echo $old_files); do new_file=$(cbfs.sh -o $1 -l | grep -x $old_file) if [ -z "$new_file" ]; then - DEBUG "Adding $old_file to $1" + DEBUG "preserve_rom: $old_file not found in new ROM — copying from current CBFS" cbfs -t 50 -r $old_file >/tmp/rom.$$ || - DIE "Failed to read cbfs file from ROM" + DIE "preserve_rom: failed to read $old_file from current CBFS" cbfs.sh -o $1 -a $old_file -f /tmp/rom.$$ || - DIE "Failed to write cbfs file to new ROM file" + DIE "preserve_rom: failed to write $old_file to $1" + else + DEBUG "preserve_rom: $old_file already present in new ROM — skipped" fi done + rm -f /tmp/rom.$$ + STATUS_OK "Runtime configuration preserved in new ROM" } # Color-code a PIN/security-token retry counter for the console. From e8e8250c4e0c543951b920dd6e04609b3848a667 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Jul 2026 11:15:06 -0400 Subject: [PATCH 11/15] initrd/bin/flash.sh: add STATUS/STATUS_OK for each flash step Added user-facing STATUS messages for: - ROM copy and preparation - SHA-256 checksum verification - Runtime config preservation (with DEBUG for CLEAN mode skip) - Flash write operation - Success confirmation Documented in a comment what preserve_rom preserves (GPG keys, TOTP/HOTP secrets, LUKS DUK data, runtime settings) and when it is skipped (clean flash with -c flag). Added DEBUG explaining CLEAN flag value and its effect on preservation. Signed-off-by: Thierry Laurion --- initrd/bin/flash.sh | 12 ++++++++++++ initrd/etc/functions.sh | 7 ++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/initrd/bin/flash.sh b/initrd/bin/flash.sh index e769c15c6..716b4f050 100755 --- a/initrd/bin/flash.sh +++ b/initrd/bin/flash.sh @@ -24,11 +24,21 @@ flash_rom() { $CONFIG_FLASH_OPTIONS -r "${ROM}" \ || recovery "Backup to $ROM failed" else + STATUS "Preparing new ROM image for flashing" cp "$ROM" /tmp/${CONFIG_BOARD}.rom + STATUS "Verifying SHA-256 checksum of ROM image" sha256sum /tmp/${CONFIG_BOARD}.rom if [ "$CLEAN" -eq 0 ]; then + # preserve_rom copies heads/ runtime config files (GPG keyring, + # TOTP/HOTP secrets, LUKS DUK slot data, runtime settings) from + # the currently running ROM's CBFS into the new ROM image before + # flashing. These files would otherwise be lost on each firmware + # update. Skip with -c (clean flash) flag. + DEBUG "flash_rom: CLEAN=$CLEAN — preserving heads/ CBFS files" preserve_rom /tmp/${CONFIG_BOARD}.rom \ || recovery "$ROM: Config preservation failed" + else + DEBUG "flash_rom: CLEAN=$CLEAN — skipping config preservation (clean flash)" fi # persist serial number from CBFS if cbfs.sh -r serial_number > /tmp/serial 2>/dev/null; then @@ -46,8 +56,10 @@ flash_rom() { fi WARN "Do not power off computer. Updating firmware, this will take a few minutes" + STATUS "Flashing ROM to chip" $CONFIG_FLASH_OPTIONS -w /tmp/${CONFIG_BOARD}.rom 2>&1 \ || recovery "$ROM: Flash failed" + STATUS_OK "ROM flashed successfully" fi } diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 5fa713a22..11a3fdb66 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -493,11 +493,12 @@ preserve_rom() { old_file_count=$(echo "$old_files" | wc -w) if [ "$old_file_count" -eq 0 ]; then - DEBUG "preserve_rom: no 'heads/' CBFS files to preserve" + DEBUG "preserve_rom: no 'heads/' CBFS files to preserve in current ROM" + STATUS_OK "No configuration overrides or key material found in current firmware" return 0 fi - STATUS "Preserving $old_file_count heads/ runtime configuration file(s)" + STATUS "Preserving configuration overrides and key material from current firmware: $(echo $old_files)" DEBUG "preserve_rom: scanning $new_rom for existing heads/* entries to skip" for old_file in $(echo $old_files); do @@ -513,7 +514,7 @@ preserve_rom() { fi done rm -f /tmp/rom.$$ - STATUS_OK "Runtime configuration preserved in new ROM" + STATUS_OK "Configuration overrides and key material preserved in new firmware" } # Color-code a PIN/security-token retry counter for the console. From f4a6d466f0c712a8bc20fe1436bd0fd4473ccff0 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Jul 2026 12:22:41 -0400 Subject: [PATCH 12/15] initrd/etc/functions.sh: fix DIE message and grep -x per review Copilot review flagged two issues: - DIE 'Invalid signature on kexec boot params' still used old wording after the STATUS was changed to 'boot hashes'. Updated to match. - grep -x in preserve_rom treated CBFS paths as regular expressions. CBFS paths commonly contain '.' (e.g. heads/gnupg/pubring.kbx) which matches any character in regex. Changed to grep -Fx for fixed-string exact matching. Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 11a3fdb66..a109490e1 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -502,7 +502,7 @@ preserve_rom() { DEBUG "preserve_rom: scanning $new_rom for existing heads/* entries to skip" for old_file in $(echo $old_files); do - new_file=$(cbfs.sh -o $1 -l | grep -x $old_file) + new_file=$(cbfs.sh -o $1 -l | grep -Fx "$old_file") if [ -z "$new_file" ]; then DEBUG "preserve_rom: $old_file not found in new ROM — copying from current CBFS" cbfs -t 50 -r $old_file >/tmp/rom.$$ || @@ -2257,7 +2257,7 @@ check_config() { DEBUG "check_config: running (cd $paramsdir && sha256sum ${param_files[*]}) | gpgv.sh $paramsdir/kexec.sig" if ! (cd "$paramsdir" && sha256sum "${param_files[@]}") | gpgv.sh "$paramsdir/kexec.sig" - 2> >(SINK_LOG "gpgv kexec.sig"); then - DIE 'Invalid signature on kexec boot params' + DIE 'Invalid signature on boot hashes' fi STATUS_OK "Boot hashes signature verified" fi From 768fc081df87fa7d4ff6006c150316d23260a037 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Jul 2026 12:25:19 -0400 Subject: [PATCH 13/15] Makefile: consistent error handling in module clone template per review Copilot flagged git init and git remote remove using ; instead of &&. A failure would leave the directory partially initialized and the next command would still run. Changed to || true to explicitly allow failure while maintaining consistent error flow with the && chain. Signed-off-by: Thierry Laurion --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index abdd8b461..d103e7ac0 100644 --- a/Makefile +++ b/Makefile @@ -491,9 +491,9 @@ define define_module = rm -f "$(build)/$($1_base_dir)/.git" 2>/dev/null; \ cd "$(build)/$($1_base_dir)" && \ echo "INFO: Initializing git repository" && \ - git init 2>/dev/null; \ + git init 2>/dev/null || true; \ echo "INFO: Configuring remote origin: $($1_repo)" && \ - git remote remove origin 2>/dev/null; \ + git remote remove origin 2>/dev/null || true; \ git remote add origin $($1_repo) && \ echo "INFO: Fetching commit $($1_commit_hash) (without recursing submodules)" && \ git fetch origin $($1_commit_hash) --recurse-submodules=no && \ From 33bfd2472e06e35d5cfb635a11a696267788392e Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Jul 2026 12:31:06 -0400 Subject: [PATCH 14/15] initrd/etc/gui_functions.sh: remove redundant char-level fallback in _whiptail_preprocess_args BusyBox fold -s already breaks unbreakable tokens at the width limit (same behavior as fold -w). The grep .\{76\} check never triggered because fold -s -w 75 always produces lines <= 75 chars. Coverity flagged the 76 vs 75 mismatch. Remove the fallback entirely and keep just fold -s -w 75. Signed-off-by: Thierry Laurion --- initrd/etc/gui_functions.sh | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index 968d51ef7..22e1d806f 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -46,15 +46,10 @@ _whiptail_preprocess_args() { for _arg in "$@"; do if [ "$_wrap_next" = 1 ]; then # fold -s breaks at spaces, preserving word boundaries. - # For long tokens with no spaces (e.g. file paths), - # fall back to character-level fold so they don't - # overflow the dialog. - local _folded - _folded=$(printf '%b' "$_arg" | fold -s -w 75) - if echo "$_folded" | grep -q '.\{76\}'; then - _folded=$(printf '%b' "$_arg" | fold -w 75) - fi - _WHIPTAIL_ARGS+=("$_folded") + # BusyBox fold -s also handles unbreakable tokens by + # falling back to character-level fold at the width + # limit, so no separate fallback is needed. + _WHIPTAIL_ARGS+=("$(printf '%b' "$_arg" | fold -s -w 75)") _wrap_next=0 else _WHIPTAIL_ARGS+=("$_arg") From e9785442c13bd697318ae634d4f08129f24f4391 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Jul 2026 12:53:57 -0400 Subject: [PATCH 15/15] initrd: track GPG verification via .gpg_verified marker, simplify variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two improvements from review: 1. Instead of inferring _sig_verified from file existence (wrong for force_boot path where check_config copies files without GPG), check_config now writes /tmp/kexec/.gpg_verified after a successful signature verification. verify_global_hashes checks for this marker directly — no intermediate variable needed. 2. The guard to skip check_config now requires both kexec_hashes.txt AND kexec_tree.txt. If only one exists (partial copy after a failed run), check_config will repopulate both. Removed the local _gpg_verified variable. The STATUS/STATUS_OK conditionals now check [ -r /tmp/kexec/.gpg_verified ] directly. Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 13 ++++++++++ initrd/etc/gui_functions.sh | 51 ++++++++++++++++++++++++------------- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index a109490e1..f0ecf0a90 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -2260,6 +2260,19 @@ check_config() { DIE 'Invalid signature on boot hashes' fi STATUS_OK "Boot hashes signature verified" + # Create a marker that verify_global_hashes (gui_functions.sh) + # reads to confirm GPG was verified, so it knows to include + # "against signed boot hashes" in its STATUS message. + # + # Lifecycle: + # 1. check_config starts with rm -rf /tmp/kexec/* — any + # stale .gpg_verified from a previous run is removed. + # 2. After this line: .gpg_verified exists only when GPG + # just passed (the path is not force). + # 3. Next check_config call: rm -rf /tmp/kexec/* removes + # it again. Or verify_global_hashes reads it before + # the next check_config runs. + touch /tmp/kexec/.gpg_verified fi DEBUG "check_config: copying kexec*.txt from $paramsdir to /tmp/kexec" diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index 22e1d806f..cf2ab6d42 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -832,28 +832,45 @@ get_inverted_config_display_action() { # (gui_functions.sh), BG_COLOR_MAIN_MENU (exported from gui-init.sh). verify_global_hashes() { TRACE_FUNC - local _sig_verified="n" - # Called from two contexts: - # 1. gui-init.sh (attempt_default_boot / select_os_boot_option): - # /tmp/kexec/ is empty — run check_config to populate it. - # 2. kexec-select-boot.sh's main loop (via check_config $paramsdir - # which verifies the GPG signature on kexec files): - # /tmp/kexec/ is already populated — skip check_config here to - # avoid the destructive rm -rf /tmp/kexec/* + re-copy. - if [ ! -r /tmp/kexec/kexec_hashes.txt ]; then + # + # Two call contexts, with different check_config semantics: + # + # Context A — gui-init.sh (attempt_default_boot / select_os_boot_option) + # /tmp/kexec/ is empty. We call check_config /boot force here to + # populate it. Because the caller is the GUI menu (not a verified + # boot path), we always pass "force" which copies the kexec files + # from /boot without verifying the GPG detached signature on them. + # The hash check STATUS will say + # "Verifying boot file checksums" (no "against signed boot hashes"). + # + # Context B — kexec-select-boot.sh's main loop + # The main loop already called check_config $paramsdir (which + # verifies the GPG signature on the kexec*.txt files) before + # calling this function. /tmp/kexec/ is already populated. + # Skipping check_config here avoids the destructive rm -rf + # /tmp/kexec/* + re-copy (which, if /boot/kexec.sig is absent, + # would leave /tmp/kexec/ empty and break the caller). + # + # Whether GPG was actually verified is determined by the + # presence of /tmp/kexec/.gpg_verified — a marker file that + # check_config creates after a successful signature verification + # (and its own rm -rf /tmp/kexec/* step cleans up on the next + # call). When present, the STATUS says + # "Verifying boot file checksums against signed boot hashes". + # + # Both files (kexec_hashes.txt and kexec_tree.txt) are required + # before skipping check_config. If only one is present (e.g. a + # partial copy from a failed run), check_config will repopulate. + # + if [ ! -r /tmp/kexec/kexec_hashes.txt -o ! -r /tmp/kexec/kexec_tree.txt ]; then check_config /boot force - _sig_verified="n" - else - # Files already in /tmp/kexec — check_config $paramsdir in - # kexec-select-boot's main loop verified the GPG signature. - _sig_verified="y" fi TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" TMP_PACKAGE_TRIGGER_PRE="/tmp/kexec/kexec_package_trigger_pre.txt" TMP_PACKAGE_TRIGGER_POST="/tmp/kexec/kexec_package_trigger_post.txt" - if [ "$_sig_verified" = "y" ]; then + if [ -r /tmp/kexec/.gpg_verified ]; then STATUS "Verifying boot file checksums against signed boot hashes" else STATUS "Verifying boot file checksums" @@ -867,7 +884,7 @@ verify_global_hashes() { if [[ "$CONFIG_ROOT_CHECK_AT_BOOT" = "y" && "$force_menu" == "n" ]]; then DEBUG "verify_global_hashes: checking root hashes" if root-hashes-gui.sh -c; then - if [ "$_sig_verified" = "y" ]; then + if [ -r /tmp/kexec/.gpg_verified ]; then STATUS_OK "Boot file and root checksums verified against signed boot hashes" else STATUS_OK "Boot file and root checksums verified" @@ -881,7 +898,7 @@ verify_global_hashes() { DIE "root hash mismatch, see /tmp/hash_output_mismatches for details" fi else - if [ "$_sig_verified" = "y" ]; then + if [ -r /tmp/kexec/.gpg_verified ]; then STATUS_OK "Boot file checksums verified against signed boot hashes" else STATUS_OK "Boot file checksums verified"