diff --git a/initrd/.bash_history b/initrd/.bash_history index 7f03d3ee1..44fd60529 100644 --- a/initrd/.bash_history +++ b/initrd/.bash_history @@ -5,9 +5,9 @@ find /boot/kexec*.txt | gpg --verify /boot/kexec.sig - #remove invalid kexec_* signed files mount /dev/sda1 /boot && mount -o remount,rw /boot && rm /boot/kexec* && mount -o remount,ro /boot #Generate keys on OpenPGP smartcard: -mount-usb && gpg --home=/.gnupg/ --card-edit +mount-usb --mode rw && gpg --home=/.gnupg/ --card-edit #Copy generated public key, private_subkey, trustdb and artifacts to external media for backup: -mount -o remount,rw /media && mkdir -p /media/gpg_keys; gpg --export-secret-keys --armor email@address.com > /media/gpg_keys/private.key && gpg --export --armor email@address.com > /media/gpg_keys/public.key && gpg --export-ownertrust > /media/gpg_keys/otrust.txt && cp -r ./.gnupg/* /media/gpg_keys/ 2> /dev/null +mkdir -p /media/gpg_keys; gpg --export-secret-keys --armor email@address.com > /media/gpg_keys/private.key && gpg --export --armor email@address.com > /media/gpg_keys/public.key && gpg --export-ownertrust > /media/gpg_keys/otrust.txt && cp -r ./.gnupg/* /media/gpg_keys/ 2> /dev/null #Insert public key and trustdb export into reproducible rom: cbfs -o /media/coreboot.rom -a "heads/initrd/.gnupg/keys/public.key" -f /media/gpg_keys/public.key && cbfs -o /media/coreboot.rom -a "heads/initrd/.gnupg/keys/otrust.txt" -f /media/gpg_keys/otrust.txt #Flush changes to external media: diff --git a/initrd/bin/generic-init b/initrd/bin/generic-init index 0a4a17a63..a3b9f34e6 100755 --- a/initrd/bin/generic-init +++ b/initrd/bin/generic-init @@ -48,14 +48,14 @@ while true; do if [ "$totp_confirm" = "m" ]; then # Try to select a kernel from the menu mount_boot - kexec-select-boot -m -b /boot -c "grub.cfg" + DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" continue fi if [ "$totp_confirm" = "y" -o -n "$totp_confirm" ]; then # Try to boot the default mount_boot - kexec-select-boot -b /boot -c "grub.cfg" \ + DO_WITH_DEBUG kexec-select-boot -b /boot -c "grub.cfg" \ || recovery "Failed default boot" fi diff --git a/initrd/bin/gui-init b/initrd/bin/gui-init index 7d4bf2eaa..7eedc4757 100755 --- a/initrd/bin/gui-init +++ b/initrd/bin/gui-init @@ -150,16 +150,21 @@ generate_totp_hotp() { TRACE_FUNC tpm_owner_password="$1" # May be empty, will prompt if needed and empty 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" - /bin/seal-hotpkey + /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 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 fi - /bin/seal-hotpkey + 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" @@ -183,17 +188,6 @@ update_totp() { TOTP="NO TPM" else TOTP=$(unseal-totp) - # On platforms using CONFIG_BOOT_EXTRA_TTYS multiple processes may try to - # access TPM at the same time, failing with EBUSY. The order of execution - # is unpredictable, so the error may appear on main console, secondary one, - # or neither of them if the calls are sufficiently staggered. Try up to - # three times (including previous one) with small delays in case of error, - # instead of immediately scaring users with "you've been pwned" message. - while [ $? -ne 0 ] && [ $tries -lt 2 ]; do - sleep 0.5 - ((tries++)) - TOTP=$(unseal-totp) - done if [ $? -ne 0 ]; then BG_COLOR_MAIN_MENU="error" if [ "$skip_to_menu" = "true" ]; then @@ -280,7 +274,10 @@ update_hotp() { HOTP='N/A' fi - if [[ "$CONFIG_TPM" = n && "$HOTP" = "Invalid code" ]]; then + 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 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 \ 'g' ' Generate new TOTP/HOTP secret' \ @@ -553,21 +550,29 @@ 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 hash under /boot" + INFO "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 rm -f /boot/kexec_primhdl_hash.txt # create Heads TPM counter before any others check_tpm_counter /boot/kexec_rollback.txt "" "$tpm_owner_password" || die "Unable to find/create tpm counter" - counter="$TPM_COUNTER" - increment_tpm_counter $counter >/dev/null 2>&1 || + TRACE_FUNC + + TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1 || die "Unable to increment tpm counter" - sha256sum /tmp/counter-$counter >/boot/kexec_rollback.txt || + DO_WITH_DEBUG sha256sum /tmp/counter-$TPM_COUNTER >/boot/kexec_rollback.txt || die "Unable to create rollback file" + 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 @@ -576,7 +581,8 @@ reset_tpm() { --msgbox "Failed to update checksums / sign default config" 0 80 fi else - die "TPM reset successful, but user chose not to update checksums" + warn "TPM reset successful, but user chose not to update+sign /boot checksums. Rebooting" + reboot fi mount -o ro,remount /boot @@ -593,7 +599,7 @@ select_os_boot_option() { TRACE_FUNC mount_boot if verify_global_hashes; then - kexec-select-boot -m -b /boot -c "grub.cfg" -g + DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g fi } @@ -606,11 +612,13 @@ attempt_default_boot() { fi DEFAULT_FILE=$(find /boot/kexec_default.*.txt 2>/dev/null | head -1) if [ -r "$DEFAULT_FILE" ]; then - kexec-select-boot -b /boot -c "grub.cfg" -g || + TRACE_FUNC + DO_WITH_DEBUG kexec-select-boot -b /boot -c "grub.cfg" -g || recovery "Failed default boot" elif (whiptail_warning --title 'No Default Boot Option Configured' \ --yesno "There is no default boot option configured yet.\nWould you like to load a menu of boot options?\nOtherwise you will return to the main menu." 0 80); then - kexec-select-boot -m -b /boot -c "grub.cfg" -g + TRACE_FUNC + DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g fi } diff --git a/initrd/bin/gui-init-basic b/initrd/bin/gui-init-basic index 033e561af..af9da581e 100755 --- a/initrd/bin/gui-init-basic +++ b/initrd/bin/gui-init-basic @@ -159,7 +159,7 @@ select_os_boot_option() { TRACE_FUNC mount_boot - kexec-select-boot -m -b /boot -c "grub.cfg" -g -i + DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g -i } attempt_default_boot() @@ -174,11 +174,11 @@ attempt_default_boot() if [ "$CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" != "y" ]; then basic-autoboot.sh elif [ -r "$DEFAULT_FILE" ]; then - kexec-select-boot -b /boot -c "grub.cfg" -g -i -s \ + DO_WITH_DEBUG kexec-select-boot -b /boot -c "grub.cfg" -g -i -s \ || recovery "Failed default boot" elif (whiptail_warning --title 'No Default Boot Option Configured' \ --yesno "There is no default boot option configured yet.\nWould you like to load a menu of boot options?\nOtherwise you will return to the main menu." 0 80) then - kexec-select-boot -m -b /boot -c "grub.cfg" -g -i + DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g -i fi } diff --git a/initrd/bin/kexec-save-default b/initrd/bin/kexec-save-default index fda5d01a1..32ac305ab 100755 --- a/initrd/bin/kexec-save-default +++ b/initrd/bin/kexec-save-default @@ -8,10 +8,10 @@ TRACE_FUNC while getopts "b:d:p:i:" arg; do case $arg in - b) bootdir="$OPTARG" ;; - d) paramsdev="$OPTARG" ;; - p) paramsdir="$OPTARG" ;; - i) index="$OPTARG" ;; + b) bootdir="$OPTARG" ;; + d) paramsdev="$OPTARG" ;; + p) paramsdir="$OPTARG" ;; + i) index="$OPTARG" ;; esac done @@ -354,7 +354,7 @@ if [ "$CONFIG_TPM" = "y" ]; then fi fi if [ "$CONFIG_BASIC" != "y" ]; then - kexec-sign-config -p $paramsdir $extparam || + DO_WITH_DEBUG kexec-sign-config -p $paramsdir $extparam || die "Failed to sign default config" fi # switch back to ro mode diff --git a/initrd/bin/kexec-save-key b/initrd/bin/kexec-save-key index a6ceb71ba..0fe2373dc 100755 --- a/initrd/bin/kexec-save-key +++ b/initrd/bin/kexec-save-key @@ -77,10 +77,11 @@ kexec-seal-key $paramsdir || if [ "$skip_sign" != "y" ]; then extparam= if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then + DEBUG "kexec-save-key: CONFIG_IGNORE_ROLLBACK is not set, will sign with -r" extparam=-r fi # sign and auto-roll config counter - kexec-sign-config -p $paramsdir $extparam || + DO_WITH_DEBUG kexec-sign-config -p $paramsdir $extparam || die "Failed to sign updated config" fi diff --git a/initrd/bin/kexec-seal-key b/initrd/bin/kexec-seal-key index 823c85468..5cda58bbf 100755 --- a/initrd/bin/kexec-seal-key +++ b/initrd/bin/kexec-seal-key @@ -63,57 +63,70 @@ fi DEBUG "$(pcrs)" - +# First, collect all the LUKS devices that need to be tested luks_drk_passphrase_valid=0 -for dev in $key_devices ; do - attempts=0 - while [ $attempts -lt 3 ]; do - if [ "$luks_drk_passphrase_valid" == "0" ]; then - # Ask for the passphrase only once - read -s -p "Enter LUKS Disk Recovery Key (DRK) passphrase that can unlock $key_devices: " disk_recovery_key_passphrase - #Using he provided passphrase as the DRK "keyfile" for unattended operations - echo -n "$disk_recovery_key_passphrase" >"$DISK_RECOVERY_KEY_FILE" - echo - fi +attempts=0 - DEBUG "Testing $DISK_RECOVERY_KEY_FILE keyfile created from provided passphrase against $dev individual key slots" - if cryptsetup open $dev --test-passphrase --key-file "$DISK_RECOVERY_KEY_FILE" >/dev/null 2>&1; then - echo "++++++ $dev: LUKS device unlocked successfully with the DRK passphrase" - luks_drk_passphrase_valid=1 +# Ask for the DRK passphrase first, before testing any devices +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 + 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 + 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 - attempts=$((attempts + 1)) - if [ "$attempts" == "3" ] && [ "$luks_drk_passphrase_valid" == "0" ]; then - die "Failed to unlock LUKS device $dev with the provided passphrase. Exiting..." - elif [ "$attempts" != "3" ] && [ "$luks_drk_passphrase_valid" == "1" ]; then - #We failed unlocking with DRK passphrase another LUKS container - die "LUKS device $key_devices cannot all be unlocked with same passphrase. Please make $key_devices devices unlockable with the same passphrase. Exiting" - else - warn "Failed to unlock LUKS device $dev with the provided passphrase. Please try again." - fi + echo "++++++ $dev: LUKS device unlocked successfully with the DRK passphrase" fi done + + if [ $all_devices_unlocked -eq 1 ]; then + luks_drk_passphrase_valid=1 + else + attempts=$((attempts + 1)) + if [ $attempts -eq 3 ]; then + die "Failed to unlock all LUKS devices with the provided passphrase after 3 attempts. Exiting..." + else + warn "Please try again." + fi + fi done +# Now that all devices are verified with the DRK passphrase, proceed with DUK setup +MIN_PASSPHRASE_LENGTH=12 attempts=0 while [ $attempts -lt 3 ]; do - read -s -p "New LUKS TPM Disk Unlock Key (DUK) passphrase for booting: " key_password + read -r -s -p $'\nNew LUKS TPM Disk Unlock Key (DUK) passphrase for booting (minimum '"$MIN_PASSPHRASE_LENGTH"' characters): ' key_password echo - read -s -p "Repeat LUKS TPM Disk Unlock Key (DUK) passphrase for booting: " key_password2 + 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 if [ "$key_password" != "$key_password2" ]; then attempts=$((attempts + 1)) - if [ "$attempts" == "3" ]; then - die "Disk Unlock Key (DUK) passphrases do not match. Exiting..." - else - warn "Disk Unlock Key (DUK) passphrases do not match. Please try again." - fi + warn "Disk Unlock Key (DUK) passphrases do not match. Please try again." else break fi done +if [ $attempts -ge 3 ]; then + die "Failed to set a valid Disk Unlock Key (DUK) passphrase after 3 attempts. Exiting..." +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" dd \ if=/dev/urandom \ @@ -159,7 +172,7 @@ for dev in $key_devices; do # Get all the key slots that are used on $dev luks_used_keyslots=($(cryptsetup luksDump "$dev" | grep -E "$regex" | sed "$sed_command")) DEBUG "$dev LUKS key slots: ${luks_used_keyslots[*]}" - + #Find the key slot that can be unlocked with the provided passphrase drk_key_slot=$(find_drk_key_slot) @@ -181,8 +194,8 @@ 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 1 -r - echo + read -p $'Are you sure you want to wipe it? [y/N]\n' -n 1 -r + echo "" # If user does not confirm, skip this slot if [[ $REPLY =~ ^[Yy]$ ]]; then wipe_desired="yes" @@ -203,7 +216,6 @@ for dev in $key_devices; do fi done - echo "++++++ $dev: Adding LUKS TPM Disk Unlock Key to LUKS key slot $duk_keyslot" DO_WITH_DEBUG cryptsetup luksAddKey \ --key-file "$DISK_RECOVERY_KEY_FILE" \ diff --git a/initrd/bin/kexec-select-boot b/initrd/bin/kexec-select-boot index cc1283249..405713934 100755 --- a/initrd/bin/kexec-select-boot +++ b/initrd/bin/kexec-select-boot @@ -20,25 +20,25 @@ force_boot="n" skip_confirm="n" while getopts "b:d:p:a:r:c:uimgfs" arg; do case $arg in - b) bootdir="$OPTARG" ;; - d) paramsdev="$OPTARG" ;; - p) paramsdir="$OPTARG" ;; - a) add="$OPTARG" ;; - r) remove="$OPTARG" ;; - c) config="$OPTARG" ;; - u) unique="y" ;; - m) force_menu="y" ;; - i) - valid_hash="y" - valid_rollback="y" - ;; - g) gui_menu="y" ;; - f) - force_boot="y" - valid_hash="y" - valid_rollback="y" - ;; - s) skip_confirm="y" ;; + b) bootdir="$OPTARG" ;; + d) paramsdev="$OPTARG" ;; + p) paramsdir="$OPTARG" ;; + a) add="$OPTARG" ;; + r) remove="$OPTARG" ;; + c) config="$OPTARG" ;; + u) unique="y" ;; + m) force_menu="y" ;; + i) + valid_hash="y" + valid_rollback="y" + ;; + g) gui_menu="y" ;; + f) + force_boot="y" + valid_hash="y" + valid_rollback="y" + ;; + s) skip_confirm="y" ;; esac done @@ -120,14 +120,14 @@ verify_rollback_counter() { TPM_COUNTER=$(grep counter $TMP_ROLLBACK_FILE | cut -d- -f2) if [ -z "$TPM_COUNTER" ]; then - die "$TMP_ROLLBACK_FILE: TPM counter not found?" + die "$TMP_ROLLBACK_FILE: TPM counter not found. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" fi read_tpm_counter $TPM_COUNTER >/dev/null 2>&1 || - die "Failed to read TPM counter" + die "Failed to read TPM counter. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" sha256sum -c $TMP_ROLLBACK_FILE >/dev/null 2>&1 || - die "Invalid TPM counter state. TPM Reset required" + die "Invalid TPM counter state. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" valid_rollback="y" } @@ -239,7 +239,7 @@ save_default_option() { -i "$option_index" \ ; then echo "+++ Saved defaults to device" - sleep 2 + default_failed="n" force_menu="n" return @@ -314,17 +314,8 @@ user_select() { # continue below to boot the new default option true else - echo "+++ Rebooting to start the new default option" - sleep 2 - if [ "$CONFIG_DEBUG_OUTPUT" != "y" ]; then - reboot || - die "!!! Failed to reboot system" - else - DEBUG "Rebooting is required prior of booting default boot entry" - # Instead of rebooting, drop to a recovery shell - # for a chance to inspect debug output - recovery "Entering recovery to permit inspection of /tmp/debug.log output, reboot to continue" - fi + NOTE "Rebooting to start the new default option" + reboot fi fi @@ -361,9 +352,9 @@ do_boot() { while true; do if [ "$force_boot" = "y" -o "$CONFIG_BASIC" = "y" ]; then - check_config $paramsdir force + DO_WITH_DEBUG check_config $paramsdir force else - check_config $paramsdir + DO_WITH_DEBUG check_config $paramsdir fi TMP_DEFAULT_FILE=$(find /tmp/kexec/kexec_default.*.txt 2>/dev/null | head -1) || true TMP_MENU_FILE="/tmp/kexec/kexec_menu.txt" diff --git a/initrd/bin/kexec-sign-config b/initrd/bin/kexec-sign-config index 52e6add6d..a3f1a7c32 100755 --- a/initrd/bin/kexec-sign-config +++ b/initrd/bin/kexec-sign-config @@ -27,24 +27,29 @@ fi paramsdir="${paramsdir%%/}" assert_signable - -confirm_gpg_card +TRACE_FUNC # remount /boot as rw mount -o remount,rw /boot +DEBUG "Signing kexec parameters in $paramsdir, rollback=$rollback, update=$update, counter=$counter" + # update hashes in /boot before signing if [ "$update" = "y" ]; then ( + TRACE_FUNC + DEBUG "update=y: Updating kexec hashes in /boot" cd /boot find ./ -type f ! -path './kexec*' -print0 | xargs -0 sha256sum >/boot/kexec_hashes.txt if [ -e /boot/kexec_default_hashes.txt ]; then + DEBUG "/boot/kexec_default_hashes.txt exists, updating /boot/kexec_default_hashes.txt" DEFAULT_FILES=$(cat /boot/kexec_default_hashes.txt | cut -f3 -d ' ') echo $DEFAULT_FILES | xargs sha256sum >/boot/kexec_default_hashes.txt fi #also save the file & directory structure to detect added files print_tree >/boot/kexec_tree.txt + TRACE_FUNC ) [ $? -eq 0 ] || die "$paramsdir: Failed to update hashes." @@ -56,35 +61,68 @@ fi if [ "$rollback" = "y" ]; then rollback_file="$paramsdir/kexec_rollback.txt" + DEBUG "rollback=y, counter=$counter, paramsdir=$paramsdir, rollback_file=$rollback_file" + TRACE_FUNC + if [ -n "$counter" ]; then - # use existing counter - read_tpm_counter $counter >/dev/null 2>&1 || + DEBUG "rollback=y: provided counter=$counter, will read tpm counter next" + TRACE_FUNC + + # use existing tpm counter + DO_WITH_DEBUG read_tpm_counter "$counter" >/dev/null 2>&1 || die "$paramsdir: Unable to read tpm counter '$counter'" else - # increment counter - check_tpm_counter $rollback_file >/dev/null 2>&1 || - die "$paramsdir: Unable to find/create tpm counter" - counter="$TPM_COUNTER" + DEBUG "rollback=y: counter was not provided: checking for existing TPM counter from TPM rollback_file=$rollback_file" + TRACE_FUNC + + if [ -e "$rollback_file" ]; then + # Extract TPM_COUNTER from rollback file + TPM_COUNTER=$(grep -o 'counter-[0-9a-f]*' "$rollback_file" | cut -d- -f2) + DEBUG "rollback=y: Found TPM counter $TPM_COUNTER in rollback file $rollback_file" + else + DEBUG "Rollback file $rollback_file does not exist. Creating new TPM counter." + DO_WITH_DEBUG check_tpm_counter $rollback_file || + die "$paramsdir: Unable to find/create tpm counter" + + TRACE_FUNC + TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1 || + die "$paramsdir: Unable to increment tpm counter" - increment_tpm_counter $counter >/dev/null 2>&1 || - 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." + 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 - sha256sum /tmp/counter-$counter >$rollback_file || + DEBUG "TPM counter file '$incremented_counter_file' found." + + # Create the rollback file + sha256sum "$incremented_counter_file" >$rollback_file || die "$paramsdir: Unable to create rollback file" fi +TRACE_FUNC param_files=$(find $paramsdir/kexec*.txt) if [ -z "$param_files" ]; then die "$paramsdir: No kexec parameter files to sign" fi for tries in 1 2 3; do - if sha256sum $param_files | gpg \ - --detach-sign \ - -a \ - >$paramsdir/kexec.sig \ - ; then + confirm_gpg_card + TRACE_FUNC + + if DO_WITH_DEBUG sha256sum $param_files | gpg --detach-sign -a >$paramsdir/kexec.sig; then # successful - update the validated params check_config $paramsdir diff --git a/initrd/bin/kexec-unseal-key b/initrd/bin/kexec-unseal-key index 346eda9b8..3573d250c 100755 --- a/initrd/bin/kexec-unseal-key +++ b/initrd/bin/kexec-unseal-key @@ -26,7 +26,7 @@ DEBUG "Show PCRs" DEBUG "$(pcrs)" for tries in 1 2 3; do - read -s -p "Enter LUKS TPM Disk Unlock Key passphrase (blank to abort): " tpm_password + read -r -s -p $'\nEnter LUKS TPM Disk Unlock Key passphrase (blank to abort): ' tpm_password echo if [ -z "$tpm_password" ]; then die "Aborting unseal disk encryption key" diff --git a/initrd/bin/key-init b/initrd/bin/key-init index dcfaf5295..3213a9d43 100755 --- a/initrd/bin/key-init +++ b/initrd/bin/key-init @@ -10,19 +10,19 @@ TRACE_FUNC # Good system clock is required for GPG to work properly. # if system year is less then 2024, prompt user to set correct time if [ "$(date +%Y)" -lt 2024 ]; then - if whiptail_warning --title "System Time Incorrect" \ - --yesno "The system time is incorrect. Please set the correct time." \ - 0 80 --yes-button Continue --no-button Skip --clear; then - change-time.sh - fi + if whiptail_warning --title "System Time Incorrect" \ + --yesno "The system time is incorrect. Please set the correct time." \ + 0 80 --yes-button Continue --no-button Skip --clear; then + change-time.sh + fi fi # Import user's keys if they exist if [ -d /.gnupg/keys ]; then - # This is legacy location for user's keys. cbfs-init takes for granted that keyring and trustdb are in /.gnupg - # oem-factory-reset generates keyring and trustdb which cbfs-init dumps to /.gnupg - # TODO: Remove individual key imports. This is still valid for distro keys only below. - gpg --import /.gnupg/keys/*.key /.gnupg/keys/*.asc 2>/dev/null || warn "Importing user's keys failed" + # This is legacy location for user's keys. cbfs-init takes for granted that keyring and trustdb are in /.gnupg + # oem-factory-reset generates keyring and trustdb which cbfs-init dumps to /.gnupg + # TODO: Remove individual key imports. This is still valid for distro keys only below. + gpg --import /.gnupg/keys/*.key /.gnupg/keys/*.asc 2>/dev/null || warn "Importing user's keys failed" fi # Import trusted distro keys allowed for ISO signing diff --git a/initrd/bin/network-init-recovery b/initrd/bin/network-init-recovery index fb09d2ff9..638d2b3b0 100755 --- a/initrd/bin/network-init-recovery +++ b/initrd/bin/network-init-recovery @@ -28,7 +28,7 @@ mobile_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..." -n 1 -r + read -p "Press Enter to continue..." -r network_modules="mii usbnet cdc_ether cdc_ncm cdc_eem" for module in $(echo $network_modules); do @@ -44,7 +44,7 @@ mobile_tethering() 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..." -n 1 -r + read -p "Press Enter to continue..." -r fi fi } diff --git a/initrd/bin/oem-factory-reset b/initrd/bin/oem-factory-reset index 70c2f791a..d637dcd8a 100755 --- a/initrd/bin/oem-factory-reset +++ b/initrd/bin/oem-factory-reset @@ -731,14 +731,20 @@ generate_checksums() { 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" + if [ -s /tmp/counter-$TPM_COUNTER ]; then - # create rollback file - sha256sum /tmp/counter-$TPM_COUNTER >/boot/kexec_rollback.txt 2>/dev/null || - whiptail_error_die "Unable to create rollback file" - else + # increment TPM counter + increment_tpm_counter $TPM_COUNTER >/dev/null 2>&1 || + 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 + fi + + # If HOTP is enabled from board config, create HOTP counter + if [ -x /bin/hotp_verification]; then ## needs to exist for initial call to unseal-hotp echo "0" >/boot/kexec_hotp_counter fi @@ -772,11 +778,10 @@ generate_checksums() { DEBUG "Detach-signing boot files under kexec.sig: ${param_files}" - if sha256sum $param_files 2>/dev/null | gpg \ + if sha256sum $param_files 2>/dev/null | gpg --detach-sign \ --pinentry-mode loopback \ --passphrase-file <(echo -n "$USER_PIN") \ --digest-algo SHA256 \ - --detach-sign \ -a \ >/boot/kexec.sig 2>/tmp/error; then # successful - update the validated params diff --git a/initrd/bin/reboot b/initrd/bin/reboot index 34313ad4a..490003d03 100755 --- a/initrd/bin/reboot +++ b/initrd/bin/reboot @@ -2,7 +2,6 @@ . /etc/functions TRACE_FUNC - # Shut down TPM if [ "$CONFIG_TPM" = "y" ]; then tpmr shutdown @@ -14,16 +13,15 @@ echo s > /proc/sysrq-trigger # Remount all mounted filesystems in read-only mode echo u > /proc/sysrq-trigger +# If debug output is enabled, give the user an opportunity to stop and +# 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 - #Generalize user prompt to continue reboot or go to recovery shell - read -r -n 1 -s -p "Press any key to continue reboot or 'r' to go to recovery shell: " REPLY - echo - if [ "$REPLY" = "r" ] || [ "$REPLY" = "R" ]; then + read -r -n 1 -s -p "Press any key to continue reboot or 'r' to go to recovery shell: " REPLY + echo + if [ "$REPLY" = "r" ] || [ "$REPLY" = "R" ]; then recovery "Reboot call bypassed to go into recovery shell to debug" - fi -fi - -if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + fi DEBUG "DEBUG: TPM shutdown and filesystem operations complete" read -r -p "Press Enter to issue final reboot syscall: " fi diff --git a/initrd/bin/seal-hotpkey b/initrd/bin/seal-hotpkey index 6ef5319d5..271547b97 100755 --- a/initrd/bin/seal-hotpkey +++ b/initrd/bin/seal-hotpkey @@ -22,14 +22,6 @@ mount_boot() { TRACE_FUNC -fatal_error() { - echo -e "\nERROR: ${1}; press Enter to continue." - read - # get lsusb output for debugging - DEBUG "lsusb output: $(lsusb)" - die "$1" -} - # Use stored HOTP key branding (this might be useful after OEM reset) if [ -r /boot/kexec_hotp_key ]; then HOTPKEY_BRANDING="$(cat /boot/kexec_hotp_key)" @@ -40,7 +32,7 @@ fi if [ "$CONFIG_TPM" = "y" ]; then DEBUG "Sealing HOTP secret reuses TOTP sealed secret..." tpmr unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET" || - fatal_error "Unable to unseal HOTP secret" + die "Unable to unseal HOTP secret" 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" @@ -66,8 +58,10 @@ counter_value=1 enable_usb +TRACE_FUNC + # Make sure no conflicting GPG related services are running, gpg-agent will respawn -killall gpg-agent scdaemon >/dev/null 2>&1 +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 @@ -77,7 +71,7 @@ if ! hotp_token_info="$(hotp_verification info)"; then if ! hotp_token_info="$(hotp_verification info)"; then # don't leak key on failure shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null - fatal_error "Unable to find $HOTPKEY_BRANDING" + die "Unable to find $HOTPKEY_BRANDING" fi fi @@ -90,9 +84,13 @@ else HOTPKEY_BRANDING="HOTP USB Security dongle" fi +DEBUG "HOTP USB Security dongle branding is $HOTPKEY_BRANDING" + # Truncate the secret if it is longer than the maximum HOTP secret truncate_max_bytes 20 "$HOTP_SECRET" +TRACE_FUNC + # Check when the signing key was created to consider trying the default PIN # (Note: we must avoid using gpg --card-status here as the Nitrokey firmware # locks up, https://github.com/Nitrokey/nitrokey-pro-firmware/issues/54) @@ -146,22 +144,19 @@ fi if [ "$admin_pin_status" -ne 0 ]; then # prompt user for PIN and retry - echo "" - read -s -p "Enter your $HOTPKEY_BRANDING $prompt_message PIN: " admin_pin - echo -e "\n" - + read -r -s -p $'\nEnter your '"$HOTPKEY_BRANDING $prompt_message"' PIN: ' admin_pin + echo hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$HOTPKEY_BRANDING" if [ $? -ne 0 ]; then - echo -e "\n" - read -s -p "Error setting HOTP secret, re-enter $prompt_message PIN and try again: " admin_pin - echo -e "\n" + read -r -s -p $'\nError setting HOTP secret, re-enter '"$prompt_message"' PIN and try again: ' admin_pin + echo 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 if [ "$HOTPKEY_BRANDING" == "Nitrokey" ]; then - fatal_error "Setting HOTP secret failed, to reset $prompt_message PIN, redo Re-Ownership procedure, use the Nitrokey App 2 or contact Nitrokey support" + die "Setting HOTP secret failed, to reset $prompt_message PIN, redo Re-Ownership procedure, use the Nitrokey App 2 or contact Nitrokey support" else - fatal_error "Setting HOTP secret failed" + die "Setting HOTP secret failed" fi fi fi @@ -183,7 +178,7 @@ mount -o remount,rw /boot counter_value=$(expr $counter_value + 1) echo $counter_value >$HOTP_COUNTER || - fatal_error "Unable to create hotp counter file" + die "Unable to create hotp counter file" # Store/overwrite HOTP USB Security dongle branding found out beforehand echo $HOTPKEY_BRANDING >$HOTP_KEY || diff --git a/initrd/bin/tpmr b/initrd/bin/tpmr index 73293379e..9877419e1 100755 --- a/initrd/bin/tpmr +++ b/initrd/bin/tpmr @@ -589,7 +589,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 TPM to seal secret to TPM NVRAM" + warn "No TPM primary handle. You must reset the TPM to seal secret to TPM NVRAM" exit 1 fi @@ -639,7 +639,7 @@ tpm1_unseal() { rm -f "$sealed_file" - tpm nv_readvalue \ + DO_WITH_DEBUG tpm nv_readvalue \ -in "$index" \ -sz "$sealed_size" \ -of "$sealed_file" || @@ -719,7 +719,7 @@ tpm1_reset() { DO_WITH_DEBUG tpm physicalsetdeactivated -c &>/dev/null DO_WITH_DEBUG tpm forceclear &>/dev/null DO_WITH_DEBUG tpm physicalenable &>/dev/null - DO_WITH_DEBUG tpm takeown -pwdo "$tpm_owner_password" &>/dev/null + DO_WITH_DEBUG --mask-position 3 tpm takeown -pwdo "$tpm_owner_password" &>/dev/null # And now turn it all back on DO_WITH_DEBUG tpm physicalpresence -s &>/dev/null diff --git a/initrd/bin/unseal-hotp b/initrd/bin/unseal-hotp index 8565ac612..5fae80da9 100755 --- a/initrd/bin/unseal-hotp +++ b/initrd/bin/unseal-hotp @@ -7,12 +7,12 @@ HOTP_SECRET="/tmp/secret/hotp.key" HOTP_COUNTER="/boot/kexec_hotp_counter" mount_boot_or_die() { - TRACE_FUNC - # Mount local disk if it is not already mounted - if ! grep -q /boot /proc/mounts; then - mount -o ro /boot || - die "Unable to mount /boot" - fi + TRACE_FUNC + # Mount local disk if it is not already mounted + if ! grep -q /boot /proc/mounts; then + mount -o ro /boot || + die "Unable to mount /boot" + fi } TRACE_FUNC @@ -29,27 +29,33 @@ mount_boot_or_die #counter_value=$(read_tpm_counter $counter | cut -f2 -d ' ' | awk 'gsub("^000e","")') # -counter_value=$(cat $HOTP_COUNTER) +#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" +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" + die "Unable to read HOTP counter" fi #counter_value=$(printf "%d" 0x${counter_value}) if [ "$CONFIG_TPM" = "y" ]; then - 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 "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" 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" + # without a TPM, generate a secret based on the SHA-256 of the ROM + secret_from_rom_hash >"$HOTP_SECRET" || die "Reading ROM failed" fi # Truncate the secret if it is longer than the maximum HOTP secret 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?' + shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null + die 'Unable to compute HOTP hash?' fi shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null @@ -65,7 +71,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" + die "Unable to create hotp counter file" mount -o remount,ro /boot exit 0 diff --git a/initrd/bin/unseal-totp b/initrd/bin/unseal-totp index 3ca7cf281..da61deeea 100755 --- a/initrd/bin/unseal-totp +++ b/initrd/bin/unseal-totp @@ -8,11 +8,12 @@ TOTP_SECRET="/tmp/secret/totp.key" TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then - tpmr unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET" || - die "Unable to unseal TOTP secret from TPM" + 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" fi -if ! totp -q <"$TOTP_SECRET"; then +if ! DO_WITH_DEBUG totp -q <"$TOTP_SECRET"; then shred -n 10 -z -u "$TOTP_SECRET" 2>/dev/null die 'Unable to compute TOTP hash?' fi diff --git a/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS b/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS new file mode 100644 index 000000000..cd523f64d --- /dev/null +++ b/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS @@ -0,0 +1,10 @@ +Welcome to the Recovery Shell! + +- /tmp/debug.log: contains corresponding log level (Quiet/Info/Debug) debug traces + - Read them locally through: 'less /tmp/debug/log' +- If you faced a bug: + - Preformat/connect a ext3/ext4/fat32/exfat USB thumb drive, and then: + - 'mount-usb --mode rw' # Mounts a connected USB drive in Read+Write mode + - 'cp /tmp/debug.log /media' # copy the log to mounted Read+Write partition under /media + - 'umount /media' # Makes sure buffered write operations are done and "ejects" the USB drive +- Share the debug.log with the developers. diff --git a/initrd/etc/functions b/initrd/etc/functions index fb5e35d95..c1da2f721 100644 --- a/initrd/etc/functions +++ b/initrd/etc/functions @@ -3,12 +3,17 @@ # ------- Start of functions coming from /etc/ash_functions die() { + TRACE_FUNC + #TODO: add colors to output, here red for ERROR? if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then echo -e " !!! ERROR: $* !!!" | tee -a /tmp/debug.log /dev/kmsg >/dev/null else echo -e "!!! ERROR: $* !!!" >&2 fi - sleep 2 + + # ask user to press Enter prior to exit + read -r -p $'Press Enter to continue...\n\n' + exit 1 } @@ -16,6 +21,8 @@ die() { # correct, and when we are able to continue with degraded functionalty. # Do not overuse this! See doc/logging.md. warn() { + TRACE_FUNC + #TODO: add colors to output, here yellow for WARNING? if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then echo -e " *** WARNING: $* ***" | tee -a /tmp/debug.log /dev/kmsg >/dev/null else @@ -54,7 +61,15 @@ TRACE() { # Don't overuse this - too much NOTE output will cause users to ignore it. 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, @@ -62,6 +77,7 @@ NOTE() { # level to troubleshoot basic problems, so it must make sense without deep # knowledge of how Heads works. 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 @@ -126,10 +142,7 @@ confirm_gpg_card() { message="Please confirm that your GPG card is inserted [Y/n]: " fi - read \ - -n 1 \ - -p "$message" \ - card_confirm + read -r -n 1 -p $'\n'"$message" card_confirm echo if [ "$card_confirm" != "y" \ @@ -151,11 +164,9 @@ confirm_gpg_card() { shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" >/dev/null 2>&1 || true #Prompt user for configured GPG Admin PIN that will be passed along to mount-usb and to import gpg subkeys - echo gpg_admin_pin="" while [ -z "$gpg_admin_pin" ]; do - #TODO: change all passphrase prompts in codebase to include -r to prevent backslash escapes - read -r -s -p "Please enter GPG Admin PIN needed to use the GPG backup thumb drive: " gpg_admin_pin + read -r -s -p $'\nPlease enter GPG Admin PIN needed to use the GPG backup thumb drive: ' gpg_admin_pin echo done #prompt user to select the proper encrypted partition, which should the first one on next prompt @@ -203,13 +214,10 @@ confirm_gpg_card() { gpg_output=$(gpg --card-status 2>&1) || die "gpg card read failed" fi - # restore prev errexit state - if [ "$errexit" = "on" ]; then - set -e - fi # Extract and display GPG PIN retry counters # output excerpt: "PIN retry counter : 3 0 3" + gpg_output=$(gpg --card-status 2>&1) pin_retry_counters=$(echo "$gpg_output" | grep 'PIN retry counter' | awk -F': ' '{print $2}') user_pin_retries=$(echo "$pin_retry_counters" | awk '{print $1}') admin_pin_retries=$(echo "$pin_retry_counters" | awk '{print $3}') @@ -220,6 +228,11 @@ confirm_gpg_card() { echo "" NOTE "Your GPG User PIN, followed by Enter key will be required for input at: 'Please unlock the card' next prompt" echo "" + + # restore prev errexit state + if [ "$errexit" = "on" ]; then + set -e + fi } gpg_auth() { @@ -311,8 +324,17 @@ recovery() { #Going to recovery shell should be authenticated if supported gpg_auth + #if we have DEBUG_OUTPUT=y, we instruct users to use the debug log + if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + cat /etc/DEBUG_LOG_COPY_INSTRUCTIONS + fi + + #Guide user into enabling debug output in case of a discovered bug + if [ "$CONFIG_DEBUG_OUTPUT" != "y" ]; then + #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'" + fi echo >&2 "!!!!! Starting recovery shell" - sleep 1 if [ -x /bin/setsid ]; then /bin/setsid -c /bin/bash @@ -397,7 +419,6 @@ enable_usb() { insmod /lib/modules/ehci-pci.ko || die "ehci_pci: module load failed" insmod /lib/modules/xhci-hcd.ko || die "xhci_hcd: module load failed" insmod /lib/modules/xhci-pci.ko || die "xhci_pci: module load failed" - sleep 2 # For resiliency, test CONFIG_USB_KEYBOARD_REQUIRED explicitly rather # than having it imply CONFIG_USER_USB_KEYBOARD at build time. @@ -536,14 +557,33 @@ DO_WITH_DEBUG() { return "$exit_status" } -# Trace the current script and function. +# TRACE_FUNC outputs the function call stack in a readable format. +# It helps debug the execution path leading to the current function. +# +# The format of the output is: +# main(/path/to/script:line) -> function1(/path/to/file:line) -> function2(/path/to/file:line) +# +# Usage: +# Call TRACE_FUNC within any function to print the call hierarchy. TRACE_FUNC() { # Index [1] for BASH_SOURCE and FUNCNAME give us the caller location. # FUNCNAME is 'main' if called from a script outside any function. # BASH_LINENO is offset by 1, it provides the line that the # corresponding FUNCNAME was _called from_, so BASH_LINENO[0] is the # location of the caller. - TRACE "${BASH_SOURCE[1]}(${BASH_LINENO[0]}): ${FUNCNAME[1]}" + + local i stack_trace="" + + # Traverse the call stack from the earliest caller to the direct caller of TRACE_FUNC + for ((i = ${#FUNCNAME[@]} - 1; i > 1; i--)); do + stack_trace+="${FUNCNAME[i]}(${BASH_SOURCE[i]}:${BASH_LINENO[i - 1]}) -> " + done + + # Append the direct caller (without extra " -> " at the end) + stack_trace+="${FUNCNAME[1]}(${BASH_SOURCE[1]}:${BASH_LINENO[0]})" + + # Print the final trace output + TRACE "${stack_trace}" } # Show the entire current call stack in debug output - useful if a catastrophic @@ -628,8 +668,13 @@ reseal_tpm_disk_decryption_key() { done NOTE "LUKS header hash changed under /boot/kexec_luks_hdr_hash.txt" echo "Updating checksums and signing all files under /boot/kexec.sig" + attempt=1 while ! update_checksums; do - warn "Checksums were not signed. Preceding errors should explain possible causes" + warn "Attempt $attempt: Checksums were not signed. Preceding errors should explain possible causes" + if [ "$attempt" -ge 3 ]; then + die "Failed to sign checksums after 3 attempts" + fi + attempt=$((attempt + 1)) done NOTE "Rebooting in 3 seconds to enable booting default boot option" sleep 3 @@ -740,8 +785,8 @@ prompt_tpm_owner_password() { return 0 fi - read -s -p "TPM Owner Password: " tpm_owner_password - echo # new line after password prompt + read -r -s -p $'\nTPM Owner Password: ' tpm_owner_password + echo # Cache the password externally to be reused by who needs it DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password" @@ -759,25 +804,24 @@ 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 -s -p "New TPM Owner Password (2 words suggested, 1-32 characters max): " tpm_owner_password - echo - - read -s -p "Repeat chosen TPM Owner Password: " tpm_owner_password2 - echo + 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 if [ "$tpm_owner_password" != "$tpm_owner_password2" ]; then - echo "Passphrases entered do not match. Try again!" echo + echo "Passphrases entered do not match. Try again!" fi + echo done # Cache the password externally to be reused by who needs it DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password" mkdir -p /tmp/secret || die "Unable to create /tmp/secret" - echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password || die "Unable to cache TPM password under /tmp/secret" + echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password || die "Unable to cache TPM password under /tmp/secret/tpm_owner_password" } check_tpm_counter() { + # $1: rollback file path TRACE_FUNC LABEL=${2:-3135106223} @@ -785,35 +829,72 @@ check_tpm_counter() { # if the /boot.hashes file already exists, read the TPM counter ID # from it. if [ -r "$1" ]; then - TPM_COUNTER=$(grep counter- "$1" | cut -d- -f2) + # Robustly extract the first hex string after 'counter-' on any line + 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" + # 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 + tpmr counter_create \ -pwdc '' \ -la $LABEL | tee /tmp/counter >/dev/null 2>&1 || die "Unable to create TPM counter" - TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1 || - die "Counter read failed" + local counter_id + counter_id="$(echo "$1" | tr -d '\n')" + if [ ! -e /tmp/counter-"$counter_id" ]; then + DEBUG "Counter file /tmp/counter-$counter_id not found. Attempting to read from TPM." + DO_WITH_DEBUG tpmr counter_read -ix "$counter_id" | tee /tmp/counter-"$counter_id" >/dev/null 2>&1 || + die "Counter read failed for index $counter_id" + fi + DEBUG "Counter file /tmp/counter-$counter_id read successfully." } -# Increment the TPM counter value in the TPM. increment_tpm_counter() { TRACE_FUNC - tpmr counter_increment -ix "$1" -pwdc '' | - tee /tmp/counter-$1 >/dev/null 2>&1 || - die "TPM counter increment failed for rollback prevention. Please reset the TPM" + local counter_id + counter_id="$(echo "$1" | tr -d '\n')" + + # Check if counter exists by reading it first + 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" + 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 + + # 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 + NEW_COUNTER=$(cut -d: -f1 /tmp/hash_output || ret=1 + sha256sum -c "$TMP_HASH_FILE" >/tmp/hash_output 2>/dev/null || ret=1 # also make sure that the file & directory structure didn't change # (sha256sum won't detect added files) print_tree >/tmp/tree_output || ret=1 - if ! cmp -s "$TMP_TREE_FILE" /tmp/tree_output &>/dev/null; then + if ! cmp -s "$TMP_TREE_FILE" /tmp/tree_output 2>/dev/null; then ret=1 [[ "$gui" != "y" ]] && exit "$ret" # produce a diff that can safely be presented to the user # this is relatively hard as file names may e.g. contain backslashes etc., # which are interpreted by whiptail, less, ... - escape_zero "(new) " <"$TMP_TREE_FILE" >"${TMP_TREE_FILE}.user" - escape_zero "(new) " /tmp/tree_output.user - diff "${TMP_TREE_FILE}.user" /tmp/tree_output.user | grep -E '^\+\(new\).*$' | sed -r 's/^\+\(new\)/(new)/g' >>/tmp/hash_output + if [ -r "$TMP_TREE_FILE" ]; then + escape_zero "(new) " <"$TMP_TREE_FILE" >"${TMP_TREE_FILE}.user" 2>/dev/null + else + touch "${TMP_TREE_FILE}.user" + fi + if [ -r /tmp/tree_output ]; then + escape_zero "(new) " /tmp/tree_output.user 2>/dev/null + else + touch /tmp/tree_output.user + fi + diff "${TMP_TREE_FILE}.user" /tmp/tree_output.user 2>/dev/null | grep -E '^\+\(new\).*$' | sed -r 's/^\+\(new\)/(new)/g' >>/tmp/hash_output 2>/dev/null rm -f "${TMP_TREE_FILE}.user" rm -f /tmp/tree_output.user fi @@ -1214,7 +1307,6 @@ scan_boot_options() { fi } - # truncate a file to a size only if it is longer (busybox truncate lacks '<' and # always sets the file size) truncate_max_bytes() { @@ -1244,13 +1336,13 @@ fromhex_plain() { print_battery_charge() { local battery battery="$1" - echo "$((100*$(cat "${battery}/charge_now")/$(cat "${battery}/charge_full")))" + echo "$((100 * $(cat "${battery}/charge_now") / $(cat "${battery}/charge_full")))" } print_battery_health() { local battery battery="$1" - echo "$((100*$(cat "${battery}/charge_full")/$(cat "${battery}/charge_full_design")))" + echo "$((100 * $(cat "${battery}/charge_full") / $(cat "${battery}/charge_full_design")))" } print_battery_name() { diff --git a/initrd/sbin/insmod b/initrd/sbin/insmod index b079fcc0c..7ca6a28fe 100755 --- a/initrd/sbin/insmod +++ b/initrd/sbin/insmod @@ -33,9 +33,9 @@ if lsmod | sed 's/_/-/g' | grep -q "^$module_name\\b"; then fi if [ ! -r /sys/class/tpm/tpm0/pcrs -o ! -x /bin/tpm ]; then - if [ ! -c /dev/tpmrm0 -o ! -x /bin/tpm2 ]; then - tpm_missing=1 - fi + if [ ! -c /dev/tpmrm0 -o ! -x /bin/tpm2 ]; then + tpm_missing=1 + fi fi if [ -z "$tpm_missing" ]; then