Skip to content

HOTP asked to be resealed even if TOTP good (Picks up on a reinstalled OS even if firmware measurements haven't changed) #1935

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1f6a975
Codebase up to TPM DUK: ident, add DEBUG+TRACE_FUNC, TRACE_FUNC now g…
tlaurion Feb 7, 2025
f9def6b
gui-init: reboot as told to user if he refused to update+sign /boot c…
tlaurion Mar 27, 2025
b5cb7ae
etc/functions: die+warn+INFO: add TRACE_FUNC and TODO to add colors (…
tlaurion Mar 28, 2025
c4b7fef
bin/kexec-select-boot/reboot/recovery: reuse reboot+DEBUG conditional…
tlaurion Mar 28, 2025
0d3b3b6
WiP
tlaurion Mar 31, 2025
68da322
WiP: tpm2 increment still fails when tpm reseal is done, and past DUK…
tlaurion Mar 31, 2025
03c5d39
WiP: so tpm reset+reboot+TPM DUK works. HOTP reseal fails at post DUK…
tlaurion Mar 31, 2025
6d90480
kexec-seal-key: force minimal DUK to 12 chars (2 words DICEWARE passp…
tlaurion Apr 28, 2025
08c8862
Merge+adapt remote-tracking branch 'osresearch/master' into hotp_fixu…
tlaurion Apr 28, 2025
4ef7473
prompts: Only continue on Enter if we ask to press Enter
JonathonHall-Purism Apr 15, 2025
d9730be
kexec-select-boot+functions : Use NOTE instead of warn, and have NOTE…
tlaurion Apr 28, 2025
9c7ef3f
kexec-seal-key: have proper spacing before and after console read pro…
tlaurion Apr 29, 2025
d10a424
kexec-seal-key: make sure TPM DRK passphrase is verified to unlock al…
tlaurion Apr 29, 2025
19d400a
/etc/functions: simplify logic of increment_tpm_counter to ease its u…
tlaurion Apr 29, 2025
67a7fd9
etc/functions: NOTE echoes to console before and after its message an…
tlaurion Apr 29, 2025
5dcad9e
Merge remote-tracking branch 'osresearch/master' into HEAD
tlaurion Jun 23, 2025
1928995
initrd/etc/functions: fix TPM counter newline presence/stripping
tlaurion Jun 23, 2025
faf32fb
initrd/etc/functions: silence output of verify_checksums + escape_zer…
tlaurion Jun 30, 2025
b95ffcc
initrd/etc/functions: check_tpm_counter(), remove unused code, add wa…
tlaurion Jun 30, 2025
c43af60
codebase: unify all 'read' prompts to inject newline prior of asking …
tlaurion Jul 2, 2025
f475296
codebase: unify all 'read' prompts to add -r, so that backslackes in …
tlaurion Jul 2, 2025
21adb0d
codebase: fix console echo needed accordingly to previous read changes
tlaurion Jul 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions initrd/.bash_history
Original file line number Diff line number Diff line change
@@ -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 link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bash history now promotes mount-usb --mode rw. nitpick

#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:
4 changes: 2 additions & 2 deletions initrd/bin/generic-init
Original file line number Diff line number Diff line change
@@ -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

53 changes: 31 additions & 22 deletions initrd/bin/gui-init
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No more 3 attempts on boot to unseal TPMTOTP: if multiple consoles (Eg Talos-2 with display console + BMC, at worst we could intruduce small delay if race condition still happening, while die asks user to press Enter now, guiding to reseal TPMTOTP or reset TPM if unable to access TPM NVRAM.

# 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
Copy link
Collaborator Author

@tlaurion tlaurion Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check only verified if HOTP was invalid if no TPM was in use.

So now, if there is no /boot/kexec_hotp_counter and TPMTOTP can unseal, user is promoted to reseal HOTP alone (OS reinstall use case without firmware upgrade)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what is we have kexec_rollback.txt? All of this doesn't make any sense: user should reset TPM here of more logic needs to be refactored.

#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,30 @@ 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 </tmp/counter)
DEBUG "TPM_COUNTER: $TPM_COUNTER"
#TODO was counter supposed to be empty and that was ok?!?!?!

DO_WITH_DEBUG increment_tpm_counter $TPM_COUNTER>/dev/null 2>&1 ||
die "Unable to increment tpm counter"

sha256sum /tmp/counter-$counter >/boot/kexec_rollback.txt ||
#TODO: should this be here?
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 +582,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 +600,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 +613,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
}

6 changes: 3 additions & 3 deletions initrd/bin/gui-init-basic
Original file line number Diff line number Diff line change
@@ -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
}

10 changes: 5 additions & 5 deletions initrd/bin/kexec-save-default
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion initrd/bin/kexec-save-key
Original file line number Diff line number Diff line change
@@ -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

82 changes: 47 additions & 35 deletions initrd/bin/kexec-seal-key
Original file line number Diff line number Diff line change
@@ -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" \
63 changes: 27 additions & 36 deletions initrd/bin/kexec-select-boot
Original file line number Diff line number Diff line change
@@ -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"
70 changes: 54 additions & 16 deletions initrd/bin/kexec-sign-config
Original file line number Diff line number Diff line change
@@ -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 </tmp/counter | tr -d '\n')
DEBUG "rollback=y: Created new TPM counter $TPM_COUNTER"
fi
fi

TRACE_FUNC

# Increment the TPM counter
DEBUG "rollback=y: Incrementing counter $TPM_COUNTER."
DO_WITH_DEBUG increment_tpm_counter $TPM_COUNTER >/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

2 changes: 1 addition & 1 deletion initrd/bin/kexec-unseal-key
Original file line number Diff line number Diff line change
@@ -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"
18 changes: 9 additions & 9 deletions initrd/bin/key-init
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions initrd/bin/network-init-recovery
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 14 additions & 9 deletions initrd/bin/oem-factory-reset
Original file line number Diff line number Diff line change
@@ -731,14 +731,20 @@ generate_checksums() {
whiptail_error_die "Unable to create TPM counter"
TPM_COUNTER=$(cut -d: -f1 </tmp/counter)

# increment TPM counter
increment_tpm_counter $TPM_COUNTER >/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
4 changes: 2 additions & 2 deletions initrd/bin/reboot
Original file line number Diff line number Diff line change
@@ -5,9 +5,9 @@ TRACE_FUNC

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
read -r -p "Press Enter to continue reboot or 'r' to go to recovery shell: " REPLY
echo
if [ "$REPLY" = "r" ] || [ "$REPLY" = "R" ]; then
if [ "$REPLY" = "r" ] || [ "$REPLY" = "R" ]; then
recovery "Reboot call bypassed to go into recovery shell to debug"
fi
fi
37 changes: 16 additions & 21 deletions initrd/bin/seal-hotpkey
Original file line number Diff line number Diff line change
@@ -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 ||
6 changes: 3 additions & 3 deletions initrd/bin/tpmr
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEBUG was exposing TPM owner passphrase on log + debug.log


# And now turn it all back on
DO_WITH_DEBUG tpm physicalpresence -s &>/dev/null
36 changes: 21 additions & 15 deletions initrd/bin/unseal-hotp
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator Author

@tlaurion tlaurion Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marmarek the issue was that previously, if TOTPM previously sealed/unsealed (measured boot from coreboot+heads), Heads was not looking to see if HOTP counter under /boot/kexec_hotp_counter was still present.

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
7 changes: 4 additions & 3 deletions initrd/bin/unseal-totp
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS
Original file line number Diff line number Diff line change
@@ -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.
180 changes: 136 additions & 44 deletions initrd/etc/functions
Original file line number Diff line number Diff line change
@@ -3,19 +3,26 @@
# ------- 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
}

# Use warn only for output that indicates a _likely_ problem, is _actionable_ to
# correct, and when we are able to continue with degraded functionalty.
# Do not overuse this! See doc/logging.md.
warn() {
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,14 +61,23 @@ 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,
# but that isn't generally needed to use Heads. Non-developers might use this
# level to troubleshoot basic problems, so it must make sense without deep
# knowledge of how Heads works. See doc/logging.md.
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,61 +804,97 @@ 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}
tpm_password="$3"
# 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 </tmp/counter)
TPM_COUNTER=$(cut -d: -f1 </tmp/counter | tr -d '\n')
DEBUG "Created new TPM_COUNTER: '$TPM_COUNTER'"
fi

if [ -z "$TPM_COUNTER" ]; then
die "$1: TPM Counter not found?"
die "No TPM counter could be found or created."
fi
}

# Read the TPM counter value from the TPM.
read_tpm_counter() {
TRACE_FUNC
tpmr counter_read -ix "$1" | tee "/tmp/counter-$1" >/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/new-counter | tr -d '\n')
DEBUG "Created new TPM counter: $NEW_COUNTER. Update kexec_rollback.txt to use this counter."
fi

die "TPM counter increment failed for rollback prevention. Please reset the TPM or update kexec_rollback.txt with a new counter."
fi

DEBUG "TPM counter incremented successfully for index $counter_id"
}

# Check detached signature on kexec boot params
@@ -828,14 +909,17 @@ check_config() {
fi

if [ ! -r $1/kexec.sig -a "$CONFIG_BASIC" != "y" ]; then
DEBUG "No $1/kexec.sig found"
return
fi

if [ $(find $1/kexec*.txt | wc -l) -eq 0 ]; then
DEBUG "No $1/kexec*.txt found"
return
fi

if [ "$2" != "force" ]; then
DEBUG "second param: $2 != force"
# Note that kexec.sig detached signature is solely verifying kexec*.txt files here!
if ! sha256sum $(find $1/kexec*.txt) | gpgv $1/kexec.sig -; then
die 'Invalid signature on kexec boot params'
@@ -909,10 +993,11 @@ update_checksums() {
extparam=
if [ "$CONFIG_TPM" = "y" ]; then
if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then
DEBUG "add -r to kexec-sign-config since CONFIG_IGNORE_ROLLBACK is not set"
extparam=-r
fi
fi
if ! kexec-sign-config -p /boot -u $extparam; then
if ! DO_WITH_DEBUG kexec-sign-config -p /boot -u $extparam; then
rv=1
else
rv=0
@@ -1020,20 +1105,28 @@ verify_checksums() {
set +e -o pipefail
local ret=0
cd "$boot_dir" || ret=1
sha256sum -c "$TMP_HASH_FILE" >/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 >/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 >/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() {
6 changes: 3 additions & 3 deletions initrd/sbin/insmod
Original file line number Diff line number Diff line change
@@ -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