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 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
Expand Up @@ -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 protected] > /media/gpg_keys/private.key && gpg --export --armor [email protected] > /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 protected] > /media/gpg_keys/private.key && gpg --export --armor [email protected] > /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:
Expand Down
4 changes: 2 additions & 2 deletions initrd/bin/generic-init
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
53 changes: 31 additions & 22 deletions initrd/bin/gui-init
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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' \
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
}

Expand All @@ -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
}

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

Expand Down
10 changes: 5 additions & 5 deletions initrd/bin/kexec-save-default
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand Down
87 changes: 51 additions & 36 deletions initrd/bin/kexec-seal-key
Original file line number Diff line number Diff line change
Expand Up @@ -63,56 +63,72 @@ 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
echo ""
read -s -p "Enter 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
echo
echo ""
read -s -p "New LUKS TPM Disk Unlock Key (DUK) passphrase for booting (minimum $MIN_PASSPHRASE_LENGTH characters): " key_password
echo ""
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

echo ""
read -s -p "Repeat LUKS TPM Disk Unlock Key (DUK) passphrase for booting: " key_password2
echo
echo ""
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 "++++++ Generating new randomized 128 bytes key file that will be sealed/unsealed by LUKS TPM Disk Unlock Key passphrase"
dd \
Expand Down Expand Up @@ -159,7 +175,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)

Expand All @@ -181,8 +197,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"
Expand All @@ -203,7 +219,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" \
Expand Down
Loading