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 18 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
18 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
1a8d685
initrd/etc/functions: fix TPM counter newline presence/stripping
tlaurion Jun 23, 2025
1e60236
initrd/etc/functions: silence output of verify_checksums when no prev…
tlaurion Jun 30, 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

87 changes: 51 additions & 36 deletions initrd/bin/kexec-seal-key
Original file line number Diff line number Diff line change
@@ -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 \
@@ -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)

@@ -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"
@@ -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" \
Loading