From fafb2e8c98c8fc54a53b0baa776b142636ae0c6f Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 31 Mar 2026 19:33:23 -0400 Subject: [PATCH 01/12] initrd: rename scripts to add .sh extension Use git mv for all script renames for proper tracking. Renamed files: - initrd/bin: cbfs-init, generic-init, gpgv, gui-init, gui-init-basic, kexec-boot, kexec-insert-key, kexec-iso-init, kexec-parse-bls, kexec-parse-boot, kexec-save-default, kexec-save-key, kexec-seal-key, kexec-select-boot, kexec-sign-config, kexec-unseal-key, key-init, lock_chip, media-scan, mount-usb, network-init-recovery, oem-factory-reset, oem-system-info-xx30, poweroff, qubes-measure-luks, reboot, seal-hotpkey, seal-totp, tpm-reset, tpmr, uefi-init, unseal-hotp, unseal-totp, usb-init, wipe-totp - initrd/etc: functions, gui_functions, luks-functions - initrd: mount-boot - initrd/sbin: insmod Signed-off-by: Thierry Laurion --- .../EOL_UNTESTED_t530-hotp-maximized.config | 2 +- .../EOL_UNTESTED_t530-maximized.config | 2 +- boards/EOL_librem_13v2/EOL_librem_13v2.config | 2 +- boards/EOL_librem_13v4/EOL_librem_13v4.config | 2 +- boards/EOL_librem_15v3/EOL_librem_15v3.config | 2 +- boards/EOL_librem_15v4/EOL_librem_15v4.config | 2 +- boards/EOL_librem_l1um/EOL_librem_l1um.config | 2 +- ...L_optiplex-7010_9010-hotp-maximized.config | 2 +- .../EOL_optiplex-7010_9010-maximized.config | 2 +- ...tiplex-7010_9010_TXT-hotp-maximized.config | 2 +- ...OL_optiplex-7010_9010_TXT-maximized.config | 2 +- .../EOL_t420-hotp-maximized.config | 2 +- .../EOL_t420-maximized.config | 2 +- .../EOL_t430-hotp-maximized.config | 2 +- .../EOL_t430-maximized.config | 2 +- .../EOL_t440p-hotp-maximized.config | 2 +- .../EOL_t440p-maximized.config | 2 +- .../EOL_t480-hotp-maximized.config | 2 +- .../EOL_t480-maximized.config | 2 +- .../EOL_t480s-hotp-maximized.config | 2 +- .../EOL_t480s-maximized.config | 2 +- .../EOL_w530-hotp-maximized.config | 2 +- .../EOL_w530-maximized.config | 2 +- .../EOL_w541-hotp-maximized.config | 2 +- .../EOL_w541-maximized.config | 2 +- .../EOL_x220-hotp-maximized.config | 2 +- .../EOL_x220-maximized.config | 2 +- .../EOL_x230-hotp-maximized-fhd_edp.config | 2 +- .../EOL_x230-hotp-maximized.config | 2 +- .../EOL_x230-hotp-maximized_usb-kb.config | 2 +- .../EOL_x230-maximized-fhd_edp.config | 2 +- .../EOL_x230-maximized.config | 2 +- .../EOL_z220-cmt-hotp-maximized.config | 2 +- .../EOL_z220-cmt-maximized.config | 2 +- .../UNTESTED_msi_z690a_ddr4.config | 2 +- .../UNTESTED_msi_z690a_ddr5.config | 2 +- .../UNTESTED_msi_z790p_ddr4.config | 2 +- .../UNTESTED_nitropad-ns50.config | 2 +- boards/librem_11/librem_11.config | 2 +- boards/librem_14/librem_14.config | 2 +- boards/librem_l1um_v2/librem_l1um_v2.config | 2 +- boards/librem_mini/librem_mini.config | 2 +- boards/librem_mini_v2/librem_mini_v2.config | 2 +- boards/msi_z790p_ddr5/msi_z790p_ddr5.config | 2 +- .../novacustom-nv4x_adl.config | 2 +- .../novacustom-v540tu.config | 2 +- .../novacustom-v560tu.config | 2 +- ...-coreboot-fbwhiptail-tpm1-hotp-prod.config | 4 +- ...oot-fbwhiptail-tpm1-hotp-prod_quiet.config | 4 +- .../qemu-coreboot-fbwhiptail-tpm1-hotp.config | 4 +- .../qemu-coreboot-fbwhiptail-tpm1-prod.config | 4 +- .../qemu-coreboot-fbwhiptail-tpm1.config | 4 +- ...-coreboot-fbwhiptail-tpm2-hotp-prod.config | 4 +- ...oot-fbwhiptail-tpm2-hotp-prod_quiet.config | 4 +- .../qemu-coreboot-fbwhiptail-tpm2-hotp.config | 4 +- .../qemu-coreboot-fbwhiptail-tpm2-prod.config | 4 +- .../qemu-coreboot-fbwhiptail-tpm2.config | 4 +- ...mu-coreboot-whiptail-tpm1-hotp-prod.config | 4 +- .../qemu-coreboot-whiptail-tpm1-hotp.config | 4 +- .../qemu-coreboot-whiptail-tpm1-prod.config | 4 +- .../qemu-coreboot-whiptail-tpm1.config | 4 +- ...mu-coreboot-whiptail-tpm2-hotp-prod.config | 4 +- .../qemu-coreboot-whiptail-tpm2-hotp.config | 4 +- .../qemu-coreboot-whiptail-tpm2-prod.config | 4 +- .../qemu-coreboot-whiptail-tpm2.config | 4 +- initrd/bin/basic-autoboot.sh | 2 +- initrd/bin/{cbfs-init => cbfs-init.sh} | 28 +-- initrd/bin/cbfs.sh | 2 +- initrd/bin/config-gui.sh | 4 +- initrd/bin/flash-gui.sh | 4 +- initrd/bin/flash.sh | 2 +- initrd/bin/flashprog-kgpe-d16-openbmc.sh | 2 +- initrd/bin/{generic-init => generic-init.sh} | 20 +- initrd/bin/gpg-gui.sh | 4 +- initrd/bin/{gpgv => gpgv.sh} | 2 +- initrd/bin/gui-init-basic | 212 ------------------ initrd/bin/gui-init-basic.sh | 203 +++++++++++++++++ initrd/bin/{gui-init => gui-init.sh} | 32 +-- initrd/bin/inject_firmware.sh | 2 +- initrd/bin/{kexec-boot => kexec-boot.sh} | 4 +- .../{kexec-insert-key => kexec-insert-key.sh} | 4 +- .../bin/{kexec-iso-init => kexec-iso-init.sh} | 20 +- .../{kexec-parse-bls => kexec-parse-bls.sh} | 2 +- .../{kexec-parse-boot => kexec-parse-boot.sh} | 2 +- ...xec-save-default => kexec-save-default.sh} | 2 +- .../bin/{kexec-save-key => kexec-save-key.sh} | 4 +- .../bin/{kexec-seal-key => kexec-seal-key.sh} | 24 +- ...kexec-select-boot => kexec-select-boot.sh} | 6 +- ...kexec-sign-config => kexec-sign-config.sh} | 2 +- .../{kexec-unseal-key => kexec-unseal-key.sh} | 6 +- initrd/bin/{key-init => key-init.sh} | 4 +- initrd/bin/{lock_chip => lock_chip.sh} | 2 +- initrd/bin/{media-scan => media-scan.sh} | 47 ++-- initrd/bin/{mount-usb => mount-usb.sh} | 6 +- ...init-recovery => network-init-recovery.sh} | 2 +- ...oem-factory-reset => oem-factory-reset.sh} | 12 +- ...stem-info-xx30 => oem-system-info-xx30.sh} | 6 +- initrd/bin/{poweroff => poweroff.sh} | 4 +- ...bes-measure-luks => qubes-measure-luks.sh} | 4 +- initrd/bin/{reboot => reboot.sh} | 4 +- initrd/bin/root-hashes-gui.sh | 4 +- initrd/bin/{seal-hotpkey => seal-hotpkey.sh} | 6 +- initrd/bin/{seal-totp => seal-totp.sh} | 16 +- initrd/bin/setconsolefont.sh | 2 +- initrd/bin/{talos-init => talos-init.sh} | 2 +- initrd/bin/{tpm-reset => tpm-reset.sh} | 4 +- initrd/bin/{tpmr => tpmr.sh} | 2 +- initrd/bin/{uefi-init => uefi-init.sh} | 19 +- initrd/bin/unpack_initramfs.sh | 2 +- initrd/bin/{unseal-hotp => unseal-hotp.sh} | 4 +- initrd/bin/{unseal-totp => unseal-totp.sh} | 4 +- initrd/bin/usb-autoboot.sh | 4 +- initrd/bin/{usb-init => usb-init.sh} | 4 +- initrd/bin/wget-measure.sh | 4 +- initrd/bin/{wipe-totp => wipe-totp.sh} | 4 +- initrd/etc/{functions => functions.sh} | 12 +- .../etc/{gui_functions => gui_functions.sh} | 0 .../etc/{luks-functions => luks-functions.sh} | 0 initrd/init | 14 +- initrd/{mount-boot => mount-boot.sh} | 0 initrd/sbin/{insmod => insmod.sh} | 22 +- ...MAINTAINED_kgpe-d16_server-whiptail.config | 4 +- .../UNMAINTAINED_kgpe-d16_server.config | 2 +- ...D_kgpe-d16_workstation-usb_keyboard.config | 4 +- .../UNMAINTAINED_kgpe-d16_workstation.config | 4 +- ...INTAINED_p8z77-m_pro-tpm1-maximized.config | 2 +- .../UNMAINTAINED_qemu-linuxboot.config | 2 +- .../UNMAINTAINED_t420.config | 2 +- .../UNMAINTAINED_t430-hotp-legacy.config | 2 +- .../UNMAINTAINED_t430-legacy.config | 2 +- .../UNMAINTAINED_t520-hotp-maximized.config | 2 +- .../UNMAINTAINED_t520-maximized.config | 2 +- ...MAINTAINED_t530-dgpu-hotp-maximized.config | 2 +- .../UNMAINTAINED_t530-dgpu-maximized.config | 2 +- ...NED_w530-dgpu-K1000m-hotp-maximized.config | 2 +- ...INTAINED_w530-dgpu-K1000m-maximized.config | 2 +- ...NED_w530-dgpu-K2000m-hotp-maximized.config | 2 +- ...INTAINED_w530-dgpu-K2000m-maximized.config | 2 +- .../UNMAINTAINED_x220.config | 2 +- .../UNMAINTAINED_x230-hotp-legacy.config | 2 +- .../UNMAINTAINED_x230-legacy.config | 2 +- .../UNTESTED_leopard/UNTESTED_leopard.config | 2 +- .../UNTESTED_r630/UNTESTED_r630.config | 2 +- .../UNTESTED_s2600wf/UNTESTED_s2600wf.config | 2 +- .../UNTESTED_tioga/UNTESTED_tioga.config | 2 +- .../UNTESTED_winterfell.config | 2 +- .../x230-hotp-legacy/x230-hotp-legacy.config | 2 +- .../x230-legacy/x230-legacy.config | 2 +- 148 files changed, 515 insertions(+), 526 deletions(-) rename initrd/bin/{cbfs-init => cbfs-init.sh} (66%) rename initrd/bin/{generic-init => generic-init.sh} (76%) rename initrd/bin/{gpgv => gpgv.sh} (85%) delete mode 100755 initrd/bin/gui-init-basic create mode 100755 initrd/bin/gui-init-basic.sh rename initrd/bin/{gui-init => gui-init.sh} (96%) rename initrd/bin/{kexec-boot => kexec-boot.sh} (98%) rename initrd/bin/{kexec-insert-key => kexec-insert-key.sh} (98%) rename initrd/bin/{kexec-iso-init => kexec-iso-init.sh} (83%) rename initrd/bin/{kexec-parse-bls => kexec-parse-bls.sh} (99%) rename initrd/bin/{kexec-parse-boot => kexec-parse-boot.sh} (99%) rename initrd/bin/{kexec-save-default => kexec-save-default.sh} (99%) rename initrd/bin/{kexec-save-key => kexec-save-key.sh} (98%) rename initrd/bin/{kexec-seal-key => kexec-seal-key.sh} (95%) rename initrd/bin/{kexec-select-boot => kexec-select-boot.sh} (99%) rename initrd/bin/{kexec-sign-config => kexec-sign-config.sh} (99%) rename initrd/bin/{kexec-unseal-key => kexec-unseal-key.sh} (91%) rename initrd/bin/{key-init => key-init.sh} (97%) rename initrd/bin/{lock_chip => lock_chip.sh} (98%) rename initrd/bin/{media-scan => media-scan.sh} (72%) rename initrd/bin/{mount-usb => mount-usb.sh} (98%) rename initrd/bin/{network-init-recovery => network-init-recovery.sh} (99%) rename initrd/bin/{oem-factory-reset => oem-factory-reset.sh} (99%) rename initrd/bin/{oem-system-info-xx30 => oem-system-info-xx30.sh} (97%) rename initrd/bin/{poweroff => poweroff.sh} (87%) rename initrd/bin/{qubes-measure-luks => qubes-measure-luks.sh} (92%) rename initrd/bin/{reboot => reboot.sh} (96%) rename initrd/bin/{seal-hotpkey => seal-hotpkey.sh} (98%) rename initrd/bin/{seal-totp => seal-totp.sh} (86%) rename initrd/bin/{talos-init => talos-init.sh} (96%) rename initrd/bin/{tpm-reset => tpm-reset.sh} (71%) rename initrd/bin/{tpmr => tpmr.sh} (99%) rename initrd/bin/{uefi-init => uefi-init.sh} (52%) rename initrd/bin/{unseal-hotp => unseal-hotp.sh} (95%) rename initrd/bin/{unseal-totp => unseal-totp.sh} (85%) rename initrd/bin/{usb-init => usb-init.sh} (80%) rename initrd/bin/{wipe-totp => wipe-totp.sh} (83%) rename initrd/etc/{functions => functions.sh} (99%) rename initrd/etc/{gui_functions => gui_functions.sh} (100%) rename initrd/etc/{luks-functions => luks-functions.sh} (100%) rename initrd/{mount-boot => mount-boot.sh} (100%) rename initrd/sbin/{insmod => insmod.sh} (80%) diff --git a/boards/EOL_UNTESTED_t530-hotp-maximized/EOL_UNTESTED_t530-hotp-maximized.config b/boards/EOL_UNTESTED_t530-hotp-maximized/EOL_UNTESTED_t530-hotp-maximized.config index 6d181b14f..dc6e22902 100644 --- a/boards/EOL_UNTESTED_t530-hotp-maximized/EOL_UNTESTED_t530-hotp-maximized.config +++ b/boards/EOL_UNTESTED_t530-hotp-maximized/EOL_UNTESTED_t530-hotp-maximized.config @@ -68,7 +68,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_UNTESTED_t530-maximized/EOL_UNTESTED_t530-maximized.config b/boards/EOL_UNTESTED_t530-maximized/EOL_UNTESTED_t530-maximized.config index ee1ee88f9..ee92f9a1b 100644 --- a/boards/EOL_UNTESTED_t530-maximized/EOL_UNTESTED_t530-maximized.config +++ b/boards/EOL_UNTESTED_t530-maximized/EOL_UNTESTED_t530-maximized.config @@ -67,7 +67,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_13v2/EOL_librem_13v2.config b/boards/EOL_librem_13v2/EOL_librem_13v2.config index 186b7c571..53ccf1ceb 100644 --- a/boards/EOL_librem_13v2/EOL_librem_13v2.config +++ b/boards/EOL_librem_13v2/EOL_librem_13v2.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_13v4/EOL_librem_13v4.config b/boards/EOL_librem_13v4/EOL_librem_13v4.config index 6aa6ff11c..6a233e4cf 100644 --- a/boards/EOL_librem_13v4/EOL_librem_13v4.config +++ b/boards/EOL_librem_13v4/EOL_librem_13v4.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_15v3/EOL_librem_15v3.config b/boards/EOL_librem_15v3/EOL_librem_15v3.config index b85672e57..b1cfbb21b 100644 --- a/boards/EOL_librem_15v3/EOL_librem_15v3.config +++ b/boards/EOL_librem_15v3/EOL_librem_15v3.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_15v4/EOL_librem_15v4.config b/boards/EOL_librem_15v4/EOL_librem_15v4.config index f5416a0a7..2236667db 100644 --- a/boards/EOL_librem_15v4/EOL_librem_15v4.config +++ b/boards/EOL_librem_15v4/EOL_librem_15v4.config @@ -38,7 +38,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_librem_l1um/EOL_librem_l1um.config b/boards/EOL_librem_l1um/EOL_librem_l1um.config index d34202f6e..2b4c41fed 100644 --- a/boards/EOL_librem_l1um/EOL_librem_l1um.config +++ b/boards/EOL_librem_l1um/EOL_librem_l1um.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="intel_iommu=on" diff --git a/boards/EOL_optiplex-7010_9010-hotp-maximized/EOL_optiplex-7010_9010-hotp-maximized.config b/boards/EOL_optiplex-7010_9010-hotp-maximized/EOL_optiplex-7010_9010-hotp-maximized.config index 144cf8d1f..002ccb90c 100644 --- a/boards/EOL_optiplex-7010_9010-hotp-maximized/EOL_optiplex-7010_9010-hotp-maximized.config +++ b/boards/EOL_optiplex-7010_9010-hotp-maximized/EOL_optiplex-7010_9010-hotp-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_optiplex-7010_9010-maximized/EOL_optiplex-7010_9010-maximized.config b/boards/EOL_optiplex-7010_9010-maximized/EOL_optiplex-7010_9010-maximized.config index c79999e02..dd38eb1df 100644 --- a/boards/EOL_optiplex-7010_9010-maximized/EOL_optiplex-7010_9010-maximized.config +++ b/boards/EOL_optiplex-7010_9010-maximized/EOL_optiplex-7010_9010-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_optiplex-7010_9010_TXT-hotp-maximized/EOL_optiplex-7010_9010_TXT-hotp-maximized.config b/boards/EOL_optiplex-7010_9010_TXT-hotp-maximized/EOL_optiplex-7010_9010_TXT-hotp-maximized.config index 1cd717725..3b877cd7c 100644 --- a/boards/EOL_optiplex-7010_9010_TXT-hotp-maximized/EOL_optiplex-7010_9010_TXT-hotp-maximized.config +++ b/boards/EOL_optiplex-7010_9010_TXT-hotp-maximized/EOL_optiplex-7010_9010_TXT-hotp-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_optiplex-7010_9010_TXT-maximized/EOL_optiplex-7010_9010_TXT-maximized.config b/boards/EOL_optiplex-7010_9010_TXT-maximized/EOL_optiplex-7010_9010_TXT-maximized.config index 1d7f08878..902396cbb 100644 --- a/boards/EOL_optiplex-7010_9010_TXT-maximized/EOL_optiplex-7010_9010_TXT-maximized.config +++ b/boards/EOL_optiplex-7010_9010_TXT-maximized/EOL_optiplex-7010_9010_TXT-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config b/boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config index d7b7bd827..c2f4e86df 100644 --- a/boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config +++ b/boards/EOL_t420-hotp-maximized/EOL_t420-hotp-maximized.config @@ -71,7 +71,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t420-maximized/EOL_t420-maximized.config b/boards/EOL_t420-maximized/EOL_t420-maximized.config index 267d0ebe0..a797cb650 100644 --- a/boards/EOL_t420-maximized/EOL_t420-maximized.config +++ b/boards/EOL_t420-maximized/EOL_t420-maximized.config @@ -69,7 +69,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t430-hotp-maximized/EOL_t430-hotp-maximized.config b/boards/EOL_t430-hotp-maximized/EOL_t430-hotp-maximized.config index 96b64d526..82c78e46d 100644 --- a/boards/EOL_t430-hotp-maximized/EOL_t430-hotp-maximized.config +++ b/boards/EOL_t430-hotp-maximized/EOL_t430-hotp-maximized.config @@ -66,7 +66,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t430-maximized/EOL_t430-maximized.config b/boards/EOL_t430-maximized/EOL_t430-maximized.config index 3cb5d5707..424e3de06 100644 --- a/boards/EOL_t430-maximized/EOL_t430-maximized.config +++ b/boards/EOL_t430-maximized/EOL_t430-maximized.config @@ -66,7 +66,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t440p-hotp-maximized/EOL_t440p-hotp-maximized.config b/boards/EOL_t440p-hotp-maximized/EOL_t440p-hotp-maximized.config index d176e98ad..630a6e28d 100644 --- a/boards/EOL_t440p-hotp-maximized/EOL_t440p-hotp-maximized.config +++ b/boards/EOL_t440p-hotp-maximized/EOL_t440p-hotp-maximized.config @@ -42,7 +42,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOARD_NAME="ThinkPad T440p-hotp-maximized" diff --git a/boards/EOL_t440p-maximized/EOL_t440p-maximized.config b/boards/EOL_t440p-maximized/EOL_t440p-maximized.config index 44c7f0cea..ce9de9dec 100644 --- a/boards/EOL_t440p-maximized/EOL_t440p-maximized.config +++ b/boards/EOL_t440p-maximized/EOL_t440p-maximized.config @@ -42,7 +42,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOARD_NAME="ThinkPad T440p-maximized" diff --git a/boards/EOL_t480-hotp-maximized/EOL_t480-hotp-maximized.config b/boards/EOL_t480-hotp-maximized/EOL_t480-hotp-maximized.config index c7cc76787..76bebb7ef 100644 --- a/boards/EOL_t480-hotp-maximized/EOL_t480-hotp-maximized.config +++ b/boards/EOL_t480-hotp-maximized/EOL_t480-hotp-maximized.config @@ -89,7 +89,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t480-maximized/EOL_t480-maximized.config b/boards/EOL_t480-maximized/EOL_t480-maximized.config index 1a1ff3fc3..c52c15804 100644 --- a/boards/EOL_t480-maximized/EOL_t480-maximized.config +++ b/boards/EOL_t480-maximized/EOL_t480-maximized.config @@ -89,7 +89,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t480s-hotp-maximized/EOL_t480s-hotp-maximized.config b/boards/EOL_t480s-hotp-maximized/EOL_t480s-hotp-maximized.config index c740d5f4a..37a767291 100644 --- a/boards/EOL_t480s-hotp-maximized/EOL_t480s-hotp-maximized.config +++ b/boards/EOL_t480s-hotp-maximized/EOL_t480s-hotp-maximized.config @@ -89,7 +89,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_t480s-maximized/EOL_t480s-maximized.config b/boards/EOL_t480s-maximized/EOL_t480s-maximized.config index eeba196f8..6c4f27dd6 100644 --- a/boards/EOL_t480s-maximized/EOL_t480s-maximized.config +++ b/boards/EOL_t480s-maximized/EOL_t480s-maximized.config @@ -89,7 +89,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_w530-hotp-maximized/EOL_w530-hotp-maximized.config b/boards/EOL_w530-hotp-maximized/EOL_w530-hotp-maximized.config index 984e1176d..c1bceb80e 100644 --- a/boards/EOL_w530-hotp-maximized/EOL_w530-hotp-maximized.config +++ b/boards/EOL_w530-hotp-maximized/EOL_w530-hotp-maximized.config @@ -68,7 +68,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_w530-maximized/EOL_w530-maximized.config b/boards/EOL_w530-maximized/EOL_w530-maximized.config index 15fcd4d18..3437f3697 100644 --- a/boards/EOL_w530-maximized/EOL_w530-maximized.config +++ b/boards/EOL_w530-maximized/EOL_w530-maximized.config @@ -67,7 +67,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_w541-hotp-maximized/EOL_w541-hotp-maximized.config b/boards/EOL_w541-hotp-maximized/EOL_w541-hotp-maximized.config index 1f85ddf95..6baa67129 100644 --- a/boards/EOL_w541-hotp-maximized/EOL_w541-hotp-maximized.config +++ b/boards/EOL_w541-hotp-maximized/EOL_w541-hotp-maximized.config @@ -43,7 +43,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOARD_NAME="ThinkPad W541-hotp-maximized" diff --git a/boards/EOL_w541-maximized/EOL_w541-maximized.config b/boards/EOL_w541-maximized/EOL_w541-maximized.config index 3fb2cb5bd..cb0b985d6 100644 --- a/boards/EOL_w541-maximized/EOL_w541-maximized.config +++ b/boards/EOL_w541-maximized/EOL_w541-maximized.config @@ -43,7 +43,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOARD_NAME="ThinkPad W541-maximized" diff --git a/boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config b/boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config index 78c4e7935..6d7033db6 100644 --- a/boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config +++ b/boards/EOL_x220-hotp-maximized/EOL_x220-hotp-maximized.config @@ -71,7 +71,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x220-maximized/EOL_x220-maximized.config b/boards/EOL_x220-maximized/EOL_x220-maximized.config index f156f70e5..bb37026df 100644 --- a/boards/EOL_x220-maximized/EOL_x220-maximized.config +++ b/boards/EOL_x220-maximized/EOL_x220-maximized.config @@ -70,7 +70,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-hotp-maximized-fhd_edp/EOL_x230-hotp-maximized-fhd_edp.config b/boards/EOL_x230-hotp-maximized-fhd_edp/EOL_x230-hotp-maximized-fhd_edp.config index af21cb0d2..ae360f6de 100644 --- a/boards/EOL_x230-hotp-maximized-fhd_edp/EOL_x230-hotp-maximized-fhd_edp.config +++ b/boards/EOL_x230-hotp-maximized-fhd_edp/EOL_x230-hotp-maximized-fhd_edp.config @@ -80,7 +80,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-hotp-maximized/EOL_x230-hotp-maximized.config b/boards/EOL_x230-hotp-maximized/EOL_x230-hotp-maximized.config index 4dbb958b2..ded711c50 100644 --- a/boards/EOL_x230-hotp-maximized/EOL_x230-hotp-maximized.config +++ b/boards/EOL_x230-hotp-maximized/EOL_x230-hotp-maximized.config @@ -78,7 +78,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-hotp-maximized_usb-kb/EOL_x230-hotp-maximized_usb-kb.config b/boards/EOL_x230-hotp-maximized_usb-kb/EOL_x230-hotp-maximized_usb-kb.config index f4e3fa5de..ef81df5e9 100644 --- a/boards/EOL_x230-hotp-maximized_usb-kb/EOL_x230-hotp-maximized_usb-kb.config +++ b/boards/EOL_x230-hotp-maximized_usb-kb/EOL_x230-hotp-maximized_usb-kb.config @@ -72,7 +72,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-maximized-fhd_edp/EOL_x230-maximized-fhd_edp.config b/boards/EOL_x230-maximized-fhd_edp/EOL_x230-maximized-fhd_edp.config index df4f45a8a..619afd739 100644 --- a/boards/EOL_x230-maximized-fhd_edp/EOL_x230-maximized-fhd_edp.config +++ b/boards/EOL_x230-maximized-fhd_edp/EOL_x230-maximized-fhd_edp.config @@ -79,7 +79,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_x230-maximized/EOL_x230-maximized.config b/boards/EOL_x230-maximized/EOL_x230-maximized.config index b64af87ce..ca17aaab7 100644 --- a/boards/EOL_x230-maximized/EOL_x230-maximized.config +++ b/boards/EOL_x230-maximized/EOL_x230-maximized.config @@ -66,7 +66,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_z220-cmt-hotp-maximized/EOL_z220-cmt-hotp-maximized.config b/boards/EOL_z220-cmt-hotp-maximized/EOL_z220-cmt-hotp-maximized.config index 533c51148..48d5d9c6a 100644 --- a/boards/EOL_z220-cmt-hotp-maximized/EOL_z220-cmt-hotp-maximized.config +++ b/boards/EOL_z220-cmt-hotp-maximized/EOL_z220-cmt-hotp-maximized.config @@ -62,7 +62,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/EOL_z220-cmt-maximized/EOL_z220-cmt-maximized.config b/boards/EOL_z220-cmt-maximized/EOL_z220-cmt-maximized.config index 8d24062e2..1bb026565 100644 --- a/boards/EOL_z220-cmt-maximized/EOL_z220-cmt-maximized.config +++ b/boards/EOL_z220-cmt-maximized/EOL_z220-cmt-maximized.config @@ -62,7 +62,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/UNTESTED_msi_z690a_ddr4/UNTESTED_msi_z690a_ddr4.config b/boards/UNTESTED_msi_z690a_ddr4/UNTESTED_msi_z690a_ddr4.config index 893bafe46..49b93cd14 100644 --- a/boards/UNTESTED_msi_z690a_ddr4/UNTESTED_msi_z690a_ddr4.config +++ b/boards/UNTESTED_msi_z690a_ddr4/UNTESTED_msi_z690a_ddr4.config @@ -32,7 +32,7 @@ CONFIG_LINUX_IGC=y export CONFIG_REQUIRE_USB_KEYBOARD=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_KERNEL_ADD="" export CONFIG_BOOT_KERNEL_REMOVE="" diff --git a/boards/UNTESTED_msi_z690a_ddr5/UNTESTED_msi_z690a_ddr5.config b/boards/UNTESTED_msi_z690a_ddr5/UNTESTED_msi_z690a_ddr5.config index 7fb97e23d..2cfea823e 100644 --- a/boards/UNTESTED_msi_z690a_ddr5/UNTESTED_msi_z690a_ddr5.config +++ b/boards/UNTESTED_msi_z690a_ddr5/UNTESTED_msi_z690a_ddr5.config @@ -32,7 +32,7 @@ CONFIG_LINUX_IGC=y export CONFIG_REQUIRE_USB_KEYBOARD=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_KERNEL_ADD="" export CONFIG_BOOT_KERNEL_REMOVE="" diff --git a/boards/UNTESTED_msi_z790p_ddr4/UNTESTED_msi_z790p_ddr4.config b/boards/UNTESTED_msi_z790p_ddr4/UNTESTED_msi_z790p_ddr4.config index a95719090..67c30d725 100644 --- a/boards/UNTESTED_msi_z790p_ddr4/UNTESTED_msi_z790p_ddr4.config +++ b/boards/UNTESTED_msi_z790p_ddr4/UNTESTED_msi_z790p_ddr4.config @@ -32,7 +32,7 @@ CONFIG_LINUX_IGC=y export CONFIG_REQUIRE_USB_KEYBOARD=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_KERNEL_ADD="" export CONFIG_BOOT_KERNEL_REMOVE="" diff --git a/boards/UNTESTED_nitropad-ns50/UNTESTED_nitropad-ns50.config b/boards/UNTESTED_nitropad-ns50/UNTESTED_nitropad-ns50.config index c8c2e4a57..08459c80e 100644 --- a/boards/UNTESTED_nitropad-ns50/UNTESTED_nitropad-ns50.config +++ b/boards/UNTESTED_nitropad-ns50/UNTESTED_nitropad-ns50.config @@ -72,7 +72,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/librem_11/librem_11.config b/boards/librem_11/librem_11.config index 79cca98cf..e8a78fd78 100644 --- a/boards/librem_11/librem_11.config +++ b/boards/librem_11/librem_11.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/librem_14/librem_14.config b/boards/librem_14/librem_14.config index 4d9189059..a7a36157f 100644 --- a/boards/librem_14/librem_14.config +++ b/boards/librem_14/librem_14.config @@ -35,7 +35,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/librem_l1um_v2/librem_l1um_v2.config b/boards/librem_l1um_v2/librem_l1um_v2.config index fde0be749..c1a70babd 100644 --- a/boards/librem_l1um_v2/librem_l1um_v2.config +++ b/boards/librem_l1um_v2/librem_l1um_v2.config @@ -39,7 +39,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/librem_mini/librem_mini.config b/boards/librem_mini/librem_mini.config index 3ca17433d..72ba6e4a5 100644 --- a/boards/librem_mini/librem_mini.config +++ b/boards/librem_mini/librem_mini.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/librem_mini_v2/librem_mini_v2.config b/boards/librem_mini_v2/librem_mini_v2.config index dba61447f..947145fad 100644 --- a/boards/librem_mini_v2/librem_mini_v2.config +++ b/boards/librem_mini_v2/librem_mini_v2.config @@ -37,7 +37,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/msi_z790p_ddr5/msi_z790p_ddr5.config b/boards/msi_z790p_ddr5/msi_z790p_ddr5.config index 0b2e9671c..79f29adcf 100644 --- a/boards/msi_z790p_ddr5/msi_z790p_ddr5.config +++ b/boards/msi_z790p_ddr5/msi_z790p_ddr5.config @@ -32,7 +32,7 @@ CONFIG_LINUX_IGC=y export CONFIG_REQUIRE_USB_KEYBOARD=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_KERNEL_ADD="" export CONFIG_BOOT_KERNEL_REMOVE="" diff --git a/boards/novacustom-nv4x_adl/novacustom-nv4x_adl.config b/boards/novacustom-nv4x_adl/novacustom-nv4x_adl.config index 8fb8f0194..6056f9371 100644 --- a/boards/novacustom-nv4x_adl/novacustom-nv4x_adl.config +++ b/boards/novacustom-nv4x_adl/novacustom-nv4x_adl.config @@ -71,7 +71,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/novacustom-v540tu/novacustom-v540tu.config b/boards/novacustom-v540tu/novacustom-v540tu.config index 7076b2c66..7f1d023f6 100644 --- a/boards/novacustom-v540tu/novacustom-v540tu.config +++ b/boards/novacustom-v540tu/novacustom-v540tu.config @@ -80,7 +80,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/novacustom-v560tu/novacustom-v560tu.config b/boards/novacustom-v560tu/novacustom-v560tu.config index 2451b1bda..cf6e7a095 100644 --- a/boards/novacustom-v560tu/novacustom-v560tu.config +++ b/boards/novacustom-v560tu/novacustom-v560tu.config @@ -80,7 +80,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config index 01873d98e..feff75db9 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config @@ -83,9 +83,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config index 28e96d0d3..5d500ac68 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config @@ -83,9 +83,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config index 29e360895..1d5b106a7 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config @@ -83,9 +83,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config index 99748401f..017c136fc 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config @@ -81,9 +81,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config b/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config index a74402033..bf56caf8d 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config @@ -81,9 +81,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config index 2880edef0..f40942c83 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config @@ -82,9 +82,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config index 08026e5dd..e310eacdf 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config @@ -82,9 +82,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp/qemu-coreboot-fbwhiptail-tpm2-hotp.config b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp/qemu-coreboot-fbwhiptail-tpm2-hotp.config index f53968267..e6ddfcefd 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp/qemu-coreboot-fbwhiptail-tpm2-hotp.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp/qemu-coreboot-fbwhiptail-tpm2-hotp.config @@ -83,9 +83,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config index 225816b94..c8bbd6838 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config @@ -81,9 +81,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config b/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config index c4839b6b1..11c887cb7 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config @@ -82,9 +82,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config b/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config index fe1723d42..964065cd6 100644 --- a/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config @@ -83,9 +83,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config b/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config index f0282194e..9a2bda98c 100644 --- a/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config +++ b/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config @@ -83,9 +83,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config b/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config index 8e9684eaa..c3e421afa 100644 --- a/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config @@ -81,9 +81,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm1/qemu-coreboot-whiptail-tpm1.config b/boards/qemu-coreboot-whiptail-tpm1/qemu-coreboot-whiptail-tpm1.config index edf85f21c..b8d3cbb8f 100644 --- a/boards/qemu-coreboot-whiptail-tpm1/qemu-coreboot-whiptail-tpm1.config +++ b/boards/qemu-coreboot-whiptail-tpm1/qemu-coreboot-whiptail-tpm1.config @@ -81,9 +81,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config b/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config index 48ee05d86..0fd679f59 100644 --- a/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config @@ -82,9 +82,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config b/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config index 465b9decc..6660907bd 100644 --- a/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config +++ b/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config @@ -83,9 +83,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config b/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config index 1ab354e45..ab57bb2c1 100644 --- a/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config @@ -81,9 +81,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config b/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config index 37be676db..17b2905c0 100644 --- a/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config +++ b/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config @@ -82,9 +82,9 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #text-based original init: -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" diff --git a/initrd/bin/basic-autoboot.sh b/initrd/bin/basic-autoboot.sh index d924affd6..060fc2f41 100755 --- a/initrd/bin/basic-autoboot.sh +++ b/initrd/bin/basic-autoboot.sh @@ -1,7 +1,7 @@ #!/bin/bash set -o pipefail -. /etc/functions +. /etc/functions.sh BOOT_MENU_OPTIONS=/tmp/basic-autoboot-options diff --git a/initrd/bin/cbfs-init b/initrd/bin/cbfs-init.sh similarity index 66% rename from initrd/bin/cbfs-init rename to initrd/bin/cbfs-init.sh index c4c310c08..8b4e96e31 100755 --- a/initrd/bin/cbfs-init +++ b/initrd/bin/cbfs-init.sh @@ -1,9 +1,9 @@ #!/bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh # CBFS extraction and measurement -# This extraction and measurement cannot be suppressed by quiet mode, since +# This extraction and measurement cannot be suppressed by quiet mode, since # config.user is not yet loaded at this point. # To suppress this output, set CONFIG_QUIET_MODE=y needs be be set in /etc/config # which is defined at build time under board configuration file to be part of initrd.cpio @@ -18,31 +18,31 @@ fi if [ "$CONFIG_CBFS_VIA_FLASHPROG" = "y" ]; then # Use flashrom directly, because we don't have /tmp/config with params for flash.sh yet - /bin/flashprog -p internal --fmap -i COREBOOT -i FMAP -r /tmp/cbfs-init.rom \ - && CBFS_ARG=" -o /tmp/cbfs-init.rom" \ - || echo "Failed reading Heads configuration from flash! Some features may not be available." + /bin/flashprog -p internal --fmap -i COREBOOT -i FMAP -r /tmp/cbfs-init.rom && + CBFS_ARG=" -o /tmp/cbfs-init.rom" || + echo "Failed reading Heads configuration from flash! Some features may not be available." fi # Load individual files -cbfsfiles=`cbfs -t 50 -l $CBFS_ARG 2>/dev/null | grep "^heads/initrd/"` +cbfsfiles=$(cbfs -t 50 -l $CBFS_ARG 2>/dev/null | grep "^heads/initrd/") -for cbfsname in `echo $cbfsfiles`; do +for cbfsname in $(echo $cbfsfiles); do filename=${cbfsname:12} if [ ! -z "$filename" ]; then - mkdir -p `dirname $filename` \ - || die "$filename: mkdir failed" + mkdir -p $(dirname $filename) || + die "$filename: mkdir failed" INFO "Extracting CBFS file $cbfsname into $filename" - cbfs -t 50 $CBFS_ARG -r $cbfsname > "$filename" \ - || die "$filename: cbfs file read failed" + cbfs -t 50 $CBFS_ARG -r $cbfsname >"$filename" || + die "$filename: cbfs file read failed" if [ "$CONFIG_TPM" = "y" ]; then TRACE_FUNC INFO "TPM: Extending PCR[$CONFIG_PCR] with filename $filename and then its content" # Measure both the filename and its content. This # ensures that renaming files or pivoting file content # will still affect the resulting PCR measurement. - tpmr extend -ix "$CONFIG_PCR" -ic "$filename" - tpmr extend -ix "$CONFIG_PCR" -if "$filename" \ - || die "$filename: tpm extend failed" + tpmr.sh extend -ix "$CONFIG_PCR" -ic "$filename" + tpmr.sh extend -ix "$CONFIG_PCR" -if "$filename" || + die "$filename: tpm extend failed" fi fi done diff --git a/initrd/bin/cbfs.sh b/initrd/bin/cbfs.sh index a6230cb3f..ca4152e72 100755 --- a/initrd/bin/cbfs.sh +++ b/initrd/bin/cbfs.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh . /tmp/config TRACE_FUNC diff --git a/initrd/bin/config-gui.sh b/initrd/bin/config-gui.sh index b741bf71d..4ca6b7a7b 100755 --- a/initrd/bin/config-gui.sh +++ b/initrd/bin/config-gui.sh @@ -1,8 +1,8 @@ #!/bin/bash # set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config TRACE_FUNC diff --git a/initrd/bin/flash-gui.sh b/initrd/bin/flash-gui.sh index 03deeec9f..4822b0ea6 100755 --- a/initrd/bin/flash-gui.sh +++ b/initrd/bin/flash-gui.sh @@ -1,8 +1,8 @@ #!/bin/bash # set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config TRACE_FUNC diff --git a/initrd/bin/flash.sh b/initrd/bin/flash.sh index c5389a11a..a57c4c45e 100755 --- a/initrd/bin/flash.sh +++ b/initrd/bin/flash.sh @@ -3,7 +3,7 @@ # NOTE: This script is used on legacy-flash boards and runs with busybox ash, # not bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh . /tmp/config echo diff --git a/initrd/bin/flashprog-kgpe-d16-openbmc.sh b/initrd/bin/flashprog-kgpe-d16-openbmc.sh index adf356d86..4b0e6fee3 100755 --- a/initrd/bin/flashprog-kgpe-d16-openbmc.sh +++ b/initrd/bin/flashprog-kgpe-d16-openbmc.sh @@ -1,5 +1,5 @@ #!/bin/bash -. /etc/functions +. /etc/functions.sh TRACE_FUNC diff --git a/initrd/bin/generic-init b/initrd/bin/generic-init.sh similarity index 76% rename from initrd/bin/generic-init rename to initrd/bin/generic-init.sh index a3b9f34e6..4f2071a86 100755 --- a/initrd/bin/generic-init +++ b/initrd/bin/generic-init.sh @@ -1,20 +1,18 @@ #!/bin/bash # Boot from a local disk installation -. /etc/functions +. /etc/functions.sh . /tmp/config -mount_boot() -{ +mount_boot() { TRACE_FUNC # Mount local disk if it is not already mounted - if ! grep -q /boot /proc/mounts ; then - mount -o ro /boot \ - || recovery "Unable to mount /boot" + if ! grep -q /boot /proc/mounts; then + mount -o ro /boot || + recovery "Unable to mount /boot" fi } - # Confirm we have a good TOTP unseal and ask the user for next choice while true; do echo "y) Default boot" @@ -41,22 +39,22 @@ while true; do fi if [ "$totp_confirm" = "u" ]; then - exec /bin/usb-init + exec /bin/usb-init.sh continue fi if [ "$totp_confirm" = "m" ]; then # Try to select a kernel from the menu mount_boot - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" continue fi if [ "$totp_confirm" = "y" -o -n "$totp_confirm" ]; then # Try to boot the default mount_boot - DO_WITH_DEBUG kexec-select-boot -b /boot -c "grub.cfg" \ - || recovery "Failed default boot" + DO_WITH_DEBUG kexec-select-boot.sh -b /boot -c "grub.cfg" || + recovery "Failed default boot" fi done diff --git a/initrd/bin/gpg-gui.sh b/initrd/bin/gpg-gui.sh index 738de34ab..3f8f4b37a 100755 --- a/initrd/bin/gpg-gui.sh +++ b/initrd/bin/gpg-gui.sh @@ -1,8 +1,8 @@ #!/bin/bash # set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config TRACE_FUNC diff --git a/initrd/bin/gpgv b/initrd/bin/gpgv.sh similarity index 85% rename from initrd/bin/gpgv rename to initrd/bin/gpgv.sh index e77197684..84da4030a 100755 --- a/initrd/bin/gpgv +++ b/initrd/bin/gpgv.sh @@ -1,6 +1,6 @@ #!/bin/bash # if we are using the full GPG we need a wrapper for the gpgv executable -. /etc/functions +. /etc/functions.sh TRACE_FUNC exec gpg --verify "$@" diff --git a/initrd/bin/gui-init-basic b/initrd/bin/gui-init-basic deleted file mode 100755 index af9da581e..000000000 --- a/initrd/bin/gui-init-basic +++ /dev/null @@ -1,212 +0,0 @@ -#!/bin/bash -# Boot from a local disk installation - -BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} -MAIN_MENU_TITLE="${BOARD_NAME} | $CONFIG_BRAND_NAME Basic Boot Menu" -export BG_COLOR_MAIN_MENU="normal" - -. /etc/functions -. /etc/gui_functions -. /tmp/config - -# skip_to_menu is set if the user selects "continue to the main menu" from any -# error, so we will indeed go to the main menu even if other errors occur. It's -# reset when we reach the main menu so the user can retry from the main menu and -# # see errors again. -skip_to_menu="false" - -mount_boot() -{ - TRACE_FUNC - # Mount local disk if it is not already mounted - while ! grep -q /boot /proc/mounts ; do - # try to mount if CONFIG_BOOT_DEV exists - if [ -e "$CONFIG_BOOT_DEV" ]; then - mount -o ro $CONFIG_BOOT_DEV /boot - [[ $? -eq 0 ]] && continue - fi - - # CONFIG_BOOT_DEV doesn't exist or couldn't be mounted, so give user options - BG_COLOR_MAIN_MENU="error" - whiptail_error --title "ERROR: No Bootable OS Found!" \ - --menu " No bootable OS was found on the default boot device $CONFIG_BOOT_DEV. - How would you like to proceed?" 0 80 4 \ - 'b' ' Select a new boot device' \ - 'u' ' Boot from USB' \ - 'm' ' Continue to the main menu' \ - 'x' ' Exit to recovery shell' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - b ) - config-gui.sh boot_device_select - if [ $? -eq 0 ]; then - # update CONFIG_BOOT_DEV - . /tmp/config - BG_COLOR_MAIN_MENU="normal" - fi - ;; - u ) - exec /bin/usb-init - ;; - m ) - skip_to_menu="true" - break - ;; - * ) - recovery "User requested recovery shell" - ;; - esac - done -} - -prompt_auto_default_boot() -{ - TRACE_FUNC - echo -e "\n\n" - if pause_automatic_boot; then - echo -e "\n\nAttempting default boot...\n\n" - attempt_default_boot - fi -} - -show_main_menu() -{ - TRACE_FUNC - date=`date "+%Y-%m-%d %H:%M:%S %Z"` - whiptail_type $BG_COLOR_MAIN_MENU --title "$MAIN_MENU_TITLE" \ - --menu "$date" 0 80 10 \ - 'd' ' Default boot' \ - 'o' ' Options -->' \ - 's' ' System Info' \ - 'p' ' Power Off' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - d ) - attempt_default_boot - ;; - o ) - show_options_menu - ;; - s ) - show_system_info - ;; - p ) - poweroff - ;; - esac -} - -show_options_menu() -{ - TRACE_FUNC - whiptail_type $BG_COLOR_MAIN_MENU --title "$CONFIG_BRAND_NAME Basic Options" \ - --menu "" 0 80 10 \ - 'b' ' Boot Options -->' \ - 'c' ' Change configuration settings -->' \ - 'f' ' Flash/Update the BIOS -->' \ - 'x' ' Exit to recovery shell' \ - 'r' ' <-- Return to main menu' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - b ) - show_boot_options_menu - ;; - c ) - config-gui.sh - ;; - f ) - flash-gui.sh - ;; - x ) - recovery "User requested recovery shell" - ;; - r ) - ;; - esac -} - -show_boot_options_menu() -{ - TRACE_FUNC - whiptail_type $BG_COLOR_MAIN_MENU --title "Boot Options" \ - --menu "Select A Boot Option" 0 80 10 \ - 'm' ' Show OS boot menu' \ - 'u' ' USB boot' \ - 'r' ' <-- Return to main menu' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - m ) - # select a kernel from the menu - select_os_boot_option - ;; - u ) - exec /bin/usb-init - ;; - r ) - ;; - esac -} - -select_os_boot_option() -{ - TRACE_FUNC - mount_boot - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g -i -} - -attempt_default_boot() -{ - TRACE_FUNC - mount_boot - - DEFAULT_FILE=`find /boot/kexec_default.*.txt 2>/dev/null | head -1` - # Basic by default boots automatically to the first menu option. This allows - # kernel updates to work in Basic by default without prompting to select a - # new default boot option. - if [ "$CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" != "y" ]; then - basic-autoboot.sh - elif [ -r "$DEFAULT_FILE" ]; then - 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 - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g -i - fi -} - -# gui-init-basic start -TRACE_FUNC - -# USB automatic boot (if configured) occurs before mounting /boot, this should -# work even if no OS is installed -if [ "$CONFIG_BASIC_USB_AUTOBOOT" = "y" ] && usb-autoboot.sh; then - # USB autoboot was offered and interrupted. Don't offer the default boot, - # go to the menu. - skip_to_menu=true -fi - -if ! detect_boot_device ; then - # can't determine /boot device or no OS installed, - # so fall back to interactive selection - mount_boot -fi - -if [ "$skip_to_menu" != "true" -a -n "$CONFIG_AUTO_BOOT_TIMEOUT" ]; then - prompt_auto_default_boot -fi - -while true; do - TRACE_FUNC - skip_to_menu="false" - show_main_menu -done - -recovery "Something failed during boot" diff --git a/initrd/bin/gui-init-basic.sh b/initrd/bin/gui-init-basic.sh new file mode 100755 index 000000000..1a6d8a875 --- /dev/null +++ b/initrd/bin/gui-init-basic.sh @@ -0,0 +1,203 @@ +#!/bin/bash +# Boot from a local disk installation + +BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} +MAIN_MENU_TITLE="${BOARD_NAME} | $CONFIG_BRAND_NAME Basic Boot Menu" +export BG_COLOR_MAIN_MENU="normal" + +. /etc/functions.sh +. /etc/gui_functions.sh +. /tmp/config + +# skip_to_menu is set if the user selects "continue to the main menu" from any +# error, so we will indeed go to the main menu even if other errors occur. It's +# reset when we reach the main menu so the user can retry from the main menu and +# # see errors again. +skip_to_menu="false" + +mount_boot() { + TRACE_FUNC + # Mount local disk if it is not already mounted + while ! grep -q /boot /proc/mounts; do + # try to mount if CONFIG_BOOT_DEV exists + if [ -e "$CONFIG_BOOT_DEV" ]; then + mount -o ro $CONFIG_BOOT_DEV /boot + [[ $? -eq 0 ]] && continue + fi + + # CONFIG_BOOT_DEV doesn't exist or couldn't be mounted, so give user options + BG_COLOR_MAIN_MENU="error" + whiptail_error --title "ERROR: No Bootable OS Found!" \ + --menu " No bootable OS was found on the default boot device $CONFIG_BOOT_DEV. + How would you like to proceed?" 0 80 4 \ + 'b' ' Select a new boot device' \ + 'u' ' Boot from USB' \ + 'm' ' Continue to the main menu' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + b) + config-gui.sh boot_device_select + if [ $? -eq 0 ]; then + # update CONFIG_BOOT_DEV + . /tmp/config + BG_COLOR_MAIN_MENU="normal" + fi + ;; + u) + exec /bin/usb-init.sh + ;; + m) + skip_to_menu="true" + break + ;; + *) + recovery "User requested recovery shell" + ;; + esac + done +} + +prompt_auto_default_boot() { + TRACE_FUNC + echo -e "\n\n" + if pause_automatic_boot; then + echo -e "\n\nAttempting default boot...\n\n" + attempt_default_boot + fi +} + +show_main_menu() { + TRACE_FUNC + date=$(date "+%Y-%m-%d %H:%M:%S %Z") + whiptail_type $BG_COLOR_MAIN_MENU --title "$MAIN_MENU_TITLE" \ + --menu "$date" 0 80 10 \ + 'd' ' Default boot' \ + 'o' ' Options -->' \ + 's' ' System Info' \ + 'p' ' Power Off' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + d) + attempt_default_boot + ;; + o) + show_options_menu + ;; + s) + show_system_info + ;; + p) + poweroff + ;; + esac +} + +show_options_menu() { + TRACE_FUNC + whiptail_type $BG_COLOR_MAIN_MENU --title "$CONFIG_BRAND_NAME Basic Options" \ + --menu "" 0 80 10 \ + 'b' ' Boot Options -->' \ + 'c' ' Change configuration settings -->' \ + 'f' ' Flash/Update the BIOS -->' \ + 'x' ' Exit to recovery shell' \ + 'r' ' <-- Return to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + b) + show_boot_options_menu + ;; + c) + config-gui.sh + ;; + f) + flash-gui.sh + ;; + x) + recovery "User requested recovery shell" + ;; + r) ;; + esac +} + +show_boot_options_menu() { + TRACE_FUNC + whiptail_type $BG_COLOR_MAIN_MENU --title "Boot Options" \ + --menu "Select A Boot Option" 0 80 10 \ + 'm' ' Show OS boot menu' \ + 'u' ' USB boot' \ + 'r' ' <-- Return to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + m) + # select a kernel from the menu + select_os_boot_option + ;; + u) + exec /bin/usb-init.sh + ;; + r) ;; + esac +} + +select_os_boot_option() { + TRACE_FUNC + mount_boot + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -i +} + +attempt_default_boot() { + TRACE_FUNC + mount_boot + + DEFAULT_FILE=$(find /boot/kexec_default.*.txt 2>/dev/null | head -1) + # Basic by default boots automatically to the first menu option. This allows + # kernel updates to work in Basic by default without prompting to select a + # new default boot option. + if [ "$CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" != "y" ]; then + basic-autoboot.sh + elif [ -r "$DEFAULT_FILE" ]; then + DO_WITH_DEBUG kexec-select-boot.sh -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 + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -i + fi +} + +# gui-init-basic start +TRACE_FUNC + +# USB automatic boot (if configured) occurs before mounting /boot, this should +# work even if no OS is installed +if [ "$CONFIG_BASIC_USB_AUTOBOOT" = "y" ] && usb-autoboot.sh; then + # USB autoboot was offered and interrupted. Don't offer the default boot, + # go to the menu. + skip_to_menu=true +fi + +if ! detect_boot_device; then + # can't determine /boot device or no OS installed, + # so fall back to interactive selection + mount_boot +fi + +if [ "$skip_to_menu" != "true" -a -n "$CONFIG_AUTO_BOOT_TIMEOUT" ]; then + prompt_auto_default_boot +fi + +while true; do + TRACE_FUNC + skip_to_menu="false" + show_main_menu +done + +recovery "Something failed during boot" diff --git a/initrd/bin/gui-init b/initrd/bin/gui-init.sh similarity index 96% rename from initrd/bin/gui-init rename to initrd/bin/gui-init.sh index ed32a6143..64a24f040 100755 --- a/initrd/bin/gui-init +++ b/initrd/bin/gui-init.sh @@ -5,9 +5,9 @@ BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} MAIN_MENU_TITLE="${BOARD_NAME} | $CONFIG_BRAND_NAME Boot Menu" export BG_COLOR_MAIN_MENU="normal" -. /etc/functions -. /etc/gui_functions -. /etc/luks-functions +. /etc/functions.sh +. /etc/gui_functions.sh +. /etc/luks-functions.sh . /tmp/config # skip_to_menu is set if the user selects "continue to the main menu" from any @@ -48,7 +48,7 @@ mount_boot() { fi ;; u) - exec /bin/usb-init + exec /bin/usb-init.sh ;; m) skip_to_menu="true" @@ -153,9 +153,9 @@ generate_totp_hotp() { # 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.sh || die "Failed to generate HOTP secret" - elif echo -e "Generating new TOTP secret...\n\n" && /bin/seal-totp "$BOARD_NAME" "$tpm_owner_password"; then + elif echo -e "Generating new TOTP secret...\n\n" && /bin/seal-totp.sh "$BOARD_NAME" "$tpm_owner_password"; then echo if [ -x /bin/hotp_verification ]; then # If we have a TPM and a HOTP USB Security dongle @@ -164,7 +164,7 @@ generate_totp_hotp() { read fi TRACE_FUNC - /bin/seal-hotpkey || die "Failed to generate HOTP secret" + /bin/seal-hotpkey.sh || 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" @@ -327,7 +327,7 @@ clean_boot_check() { # OS is installed, no kexec files present, no GPG keys in keyring, security token present # prompt user to run OEM factory reset - oem-factory-reset \ + oem-factory-reset.sh \ "Clean Boot Detected - Perform OEM Factory Reset / Re-Ownership?" } @@ -357,7 +357,7 @@ check_gpg_key() { return 1 ;; F) - oem-factory-reset + oem-factory-reset.sh ;; x) @@ -488,7 +488,7 @@ show_boot_options_menu() { select_os_boot_option ;; u) - exec /bin/usb-init + exec /bin/usb-init.sh ;; i) force_unsafe_boot @@ -543,7 +543,7 @@ reset_tpm() { return 1 fi - tpmr reset "$tpm_owner_password" + tpmr.sh reset "$tpm_owner_password" # now that the TPM is reset, remove invalid TPM counter files mount_boot @@ -566,7 +566,7 @@ reset_tpm() { DEBUG "TPM_COUNTER: $TPM_COUNTER" #TPM_COUNTER can be empty - increment_tpm_counter $TPM_COUNTER>/dev/null 2>&1 || + increment_tpm_counter $TPM_COUNTER >/dev/null 2>&1 || die "Unable to increment tpm counter" DO_WITH_DEBUG sha256sum /tmp/counter-$TPM_COUNTER >/boot/kexec_rollback.txt || @@ -599,7 +599,7 @@ select_os_boot_option() { TRACE_FUNC mount_boot if verify_global_hashes; then - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g fi } @@ -613,12 +613,12 @@ attempt_default_boot() { DEFAULT_FILE=$(find /boot/kexec_default.*.txt 2>/dev/null | head -1) if [ -r "$DEFAULT_FILE" ]; then TRACE_FUNC - DO_WITH_DEBUG kexec-select-boot -b /boot -c "grub.cfg" -g || + DO_WITH_DEBUG kexec-select-boot.sh -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 TRACE_FUNC - DO_WITH_DEBUG kexec-select-boot -m -b /boot -c "grub.cfg" -g + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g fi } @@ -631,7 +631,7 @@ force_unsafe_boot() { # Run the menu selection in "force" mode, bypassing hash checks if (whiptail_warning --title 'Unsafe Forced Boot Selected!' \ --yesno "WARNING: You have chosen to skip all tamper checks and boot anyway.\n\nThis is an unsafe option!\n\nDo you want to proceed?" 0 80); then - mount_boot && kexec-select-boot -m -b /boot -c "grub.cfg" -g -f + mount_boot && kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -f fi } diff --git a/initrd/bin/inject_firmware.sh b/initrd/bin/inject_firmware.sh index f9e6556e6..ee1bfdddf 100755 --- a/initrd/bin/inject_firmware.sh +++ b/initrd/bin/inject_firmware.sh @@ -23,7 +23,7 @@ set -e -o pipefail . /tmp/config -. /etc/functions +. /etc/functions.sh if [ "$(load_config_value CONFIG_USE_BLOB_JAIL)" != "y" ]; then # Blob jail not active, nothing to do diff --git a/initrd/bin/kexec-boot b/initrd/bin/kexec-boot.sh similarity index 98% rename from initrd/bin/kexec-boot rename to initrd/bin/kexec-boot.sh index fa37ebf99..808597e1f 100755 --- a/initrd/bin/kexec-boot +++ b/initrd/bin/kexec-boot.sh @@ -2,7 +2,7 @@ # Launches kexec from saved configuration entries set -e -o pipefail . /tmp/config -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -167,7 +167,7 @@ if [ "$CONFIG_DEBUG_OUTPUT" = "y" ];then fi if [ "$CONFIG_TPM" = "y" ]; then - tpmr kexec_finalize + tpmr.sh kexec_finalize fi if [ -x /bin/io386 -a "$CONFIG_FINALIZE_PLATFORM_LOCKING" = "y" ]; then diff --git a/initrd/bin/kexec-insert-key b/initrd/bin/kexec-insert-key.sh similarity index 98% rename from initrd/bin/kexec-insert-key rename to initrd/bin/kexec-insert-key.sh index ff95c1943..e26518c7e 100755 --- a/initrd/bin/kexec-insert-key +++ b/initrd/bin/kexec-insert-key.sh @@ -1,7 +1,7 @@ #!/bin/bash # Unseal a LUKS Disk Unlock Key from TPM and add to a new initramfs set -e -o pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -67,7 +67,7 @@ fi # Override PCR 4 so that user can't read the key TRACE_FUNC INFO "TPM: Extending PCR[4] to prevent any future secret unsealing" -tpmr extend -ix 4 -ic generic || +tpmr.sh extend -ix 4 -ic generic || die 'Unable to scramble PCR' # Check to continue diff --git a/initrd/bin/kexec-iso-init b/initrd/bin/kexec-iso-init.sh similarity index 83% rename from initrd/bin/kexec-iso-init rename to initrd/bin/kexec-iso-init.sh index 53856fec8..73af82a15 100755 --- a/initrd/bin/kexec-iso-init +++ b/initrd/bin/kexec-iso-init.sh @@ -1,8 +1,8 @@ #!/bin/bash # Boot from signed ISO set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config TRACE_FUNC @@ -22,8 +22,8 @@ ISO_PATH="${ISO_PATH##/}" if [ -r "$ISOSIG" ]; then # Signature found, verify it - gpgv --homedir=/etc/distro/ "$ISOSIG" "$MOUNTED_ISO_PATH" \ - || die 'ISO signature failed' + gpgv --homedir=/etc/distro/ "$ISOSIG" "$MOUNTED_ISO_PATH" || + die 'ISO signature failed' echo '+++ ISO signature verified' else # No signature found, prompt user with warning @@ -48,10 +48,10 @@ else fi echo '+++ Mounting ISO and booting' -mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot \ - || die '$MOUNTED_ISO_PATH: Unable to mount /boot' +mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot || + die '$MOUNTED_ISO_PATH: Unable to mount /boot' -DEV_UUID=`blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2` +DEV_UUID=$(blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2) ADD="fromiso=/dev/disk/by-uuid/$DEV_UUID/$ISO_PATH img_dev=/dev/disk/by-uuid/$DEV_UUID iso-scan/filename=/${ISO_PATH} img_loop=$ISO_PATH iso=$DEV_UUID/$ISO_PATH" REMOVE="" @@ -60,20 +60,20 @@ check_config $paramsdir ADD_FILE=/tmp/kexec/kexec_iso_add.txt if [ -r $ADD_FILE ]; then - NEW_ADD=`cat $ADD_FILE` + NEW_ADD=$(cat $ADD_FILE) ADD=$(eval "echo \"$NEW_ADD\"") fi echo "+++ Overriding standard ISO kernel arguments with additions: $ADD" REMOVE_FILE=/tmp/kexec/kexec_iso_remove.txt if [ -r $REMOVE_FILE ]; then - NEW_REMOVE=`cat $REMOVE_FILE` + NEW_REMOVE=$(cat $REMOVE_FILE) REMOVE=$(eval "echo \"$NEW_REMOVE\"") fi echo "+++ Overriding standard ISO kernel arguments with suppressions: $REMOVE" # Call kexec and indicate that hashes have been verified -DO_WITH_DEBUG kexec-select-boot -b /boot -d /media -p "$paramsdir" \ +DO_WITH_DEBUG kexec-select-boot.sh -b /boot -d /media -p "$paramsdir" \ -a "$ADD" -r "$REMOVE" -c "*.cfg" -u -i die "Something failed in selecting boot" diff --git a/initrd/bin/kexec-parse-bls b/initrd/bin/kexec-parse-bls.sh similarity index 99% rename from initrd/bin/kexec-parse-bls rename to initrd/bin/kexec-parse-bls.sh index 92bc5f6c8..30f7ce69d 100755 --- a/initrd/bin/kexec-parse-bls +++ b/initrd/bin/kexec-parse-bls.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC bootdir="$1" diff --git a/initrd/bin/kexec-parse-boot b/initrd/bin/kexec-parse-boot.sh similarity index 99% rename from initrd/bin/kexec-parse-boot rename to initrd/bin/kexec-parse-boot.sh index 07e38e3d6..e5e382425 100755 --- a/initrd/bin/kexec-parse-boot +++ b/initrd/bin/kexec-parse-boot.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC diff --git a/initrd/bin/kexec-save-default b/initrd/bin/kexec-save-default.sh similarity index 99% rename from initrd/bin/kexec-save-default rename to initrd/bin/kexec-save-default.sh index 32ac305ab..0db91a955 100755 --- a/initrd/bin/kexec-save-default +++ b/initrd/bin/kexec-save-default.sh @@ -2,7 +2,7 @@ # Save these options to be the persistent default set -e -o pipefail . /tmp/config -. /etc/functions +. /etc/functions.sh TRACE_FUNC diff --git a/initrd/bin/kexec-save-key b/initrd/bin/kexec-save-key.sh similarity index 98% rename from initrd/bin/kexec-save-key rename to initrd/bin/kexec-save-key.sh index 0fe2373dc..890ffa6a5 100755 --- a/initrd/bin/kexec-save-key +++ b/initrd/bin/kexec-save-key.sh @@ -1,11 +1,11 @@ #!/bin/bash # Generate a TPM key used to unlock LUKS disks -. /etc/functions +. /etc/functions.sh TRACE_FUNC set -e -o pipefail -. /etc/functions +. /etc/functions.sh lvm_volume_group="" skip_sign="n" diff --git a/initrd/bin/kexec-seal-key b/initrd/bin/kexec-seal-key.sh similarity index 95% rename from initrd/bin/kexec-seal-key rename to initrd/bin/kexec-seal-key.sh index 39b8c9e85..061542a28 100755 --- a/initrd/bin/kexec-seal-key +++ b/initrd/bin/kexec-seal-key.sh @@ -3,7 +3,7 @@ # with the current PCRs and then store it in the TPM NVRAM. # It will then need to be bundled into initrd that is booted. set -e -o pipefail -. /etc/functions +. /etc/functions.sh find_drk_key_slot() { local temp_drk_key_slot="" @@ -29,7 +29,7 @@ DUK_KEY_FILE="/tmp/secret/secret.key" TPM_SEALED="/tmp/secret/secret.sealed" DISK_RECOVERY_KEY_FILE="/tmp/secret/recovery.key" -. /etc/functions +. /etc/functions.sh . /tmp/config TRACE_FUNC @@ -232,29 +232,29 @@ echo "$key_devices" | xargs /bin/qubes-measure-luks || die "Unable to measure the LUKS headers" pcrf="/tmp/secret/pcrf.bin" -tpmr pcrread 0 "$pcrf" -tpmr pcrread -a 1 "$pcrf" -tpmr pcrread -a 2 "$pcrf" -tpmr pcrread -a 3 "$pcrf" +tpmr.sh pcrread 0 "$pcrf" +tpmr.sh pcrread -a 1 "$pcrf" +tpmr.sh pcrread -a 2 "$pcrf" +tpmr.sh pcrread -a 3 "$pcrf" # Note that PCR 4 needs to be set with the "normal-boot" path value, read it from event log. -tpmr calcfuturepcr 4 >>"$pcrf" +tpmr.sh calcfuturepcr 4 >>"$pcrf" if [ "$CONFIG_USER_USB_KEYBOARD" = "y" -o -r /lib/modules/libata.ko -o -x /bin/hotp_verification ]; then DEBUG "Sealing LUKS TPM Disk Unlock Key with PCR5 involvement (additional kernel modules are loaded per board config)..." # Here, we take pcr 5 into consideration if modules are expected to be measured+loaded - tpmr pcrread -a 5 "$pcrf" + tpmr.sh pcrread -a 5 "$pcrf" else DEBUG "Sealing LUKS TPM Disk Unlock Key with PCR5=0 (NO additional kernel modules are loaded per board config)..." #no kernel modules are expected to be measured+loaded - tpmr calcfuturepcr 5 >>"$pcrf" + tpmr.sh calcfuturepcr 5 >>"$pcrf" fi # Precompute the value for pcr 6 DEBUG "Precomputing TPM future value for PCR6 sealing/unsealing of LUKS TPM Disk Unlock Key..." -tpmr calcfuturepcr 6 "/tmp/luksDump.txt" >>"$pcrf" +tpmr.sh calcfuturepcr 6 "/tmp/luksDump.txt" >>"$pcrf" # We take into consideration user files in cbfs -tpmr pcrread -a 7 "$pcrf" +tpmr.sh pcrread -a 7 "$pcrf" DO_WITH_DEBUG --mask-position 7 \ - tpmr seal "$DUK_KEY_FILE" "$TPM_INDEX" 0,1,2,3,4,5,6,7 "$pcrf" \ + tpmr.sh seal "$DUK_KEY_FILE" "$TPM_INDEX" 0,1,2,3,4,5,6,7 "$pcrf" \ "$TPM_SIZE" "$key_password" || die "Unable to write LUKS TPM Disk Unlock Key to NVRAM" # should be okay if this fails diff --git a/initrd/bin/kexec-select-boot b/initrd/bin/kexec-select-boot.sh similarity index 99% rename from initrd/bin/kexec-select-boot rename to initrd/bin/kexec-select-boot.sh index 405713934..56cbfbc1b 100755 --- a/initrd/bin/kexec-select-boot +++ b/initrd/bin/kexec-select-boot.sh @@ -2,8 +2,8 @@ # Generic configurable boot script via kexec set -e -o pipefail . /tmp/config -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh TRACE_FUNC @@ -382,7 +382,7 @@ while true; do # Extend PCR4 as soon as possible TRACE_FUNC INFO "TPM: Extending PCR[4] to prevent further secret unsealing" - tpmr extend -ix 4 -ic generic || + tpmr.sh extend -ix 4 -ic generic || die "Failed to extend TPM PCR[4]" fi fi diff --git a/initrd/bin/kexec-sign-config b/initrd/bin/kexec-sign-config.sh similarity index 99% rename from initrd/bin/kexec-sign-config rename to initrd/bin/kexec-sign-config.sh index b994a8b51..6b0cd1d87 100755 --- a/initrd/bin/kexec-sign-config +++ b/initrd/bin/kexec-sign-config.sh @@ -2,7 +2,7 @@ # Sign a valid directory of kexec params set -e -o pipefail . /tmp/config -. /etc/functions +. /etc/functions.sh TRACE_FUNC diff --git a/initrd/bin/kexec-unseal-key b/initrd/bin/kexec-unseal-key.sh similarity index 91% rename from initrd/bin/kexec-unseal-key rename to initrd/bin/kexec-unseal-key.sh index 12b22c266..78aa5a464 100755 --- a/initrd/bin/kexec-unseal-key +++ b/initrd/bin/kexec-unseal-key.sh @@ -3,12 +3,12 @@ # The TOTP secret will be shown to the user on each encryption attempt. # It will then need to be bundled into initrd that is booted with Qubes. set -e -o pipefail -. /etc/functions +. /etc/functions.sh TPM_INDEX=3 TPM_SIZE=312 -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -38,7 +38,7 @@ for tries in 1 2 3; do fi if DO_WITH_DEBUG --mask-position 6 \ - tpmr unseal "$TPM_INDEX" "0,1,2,3,4,5,6,7" "$TPM_SIZE" \ + tpmr.sh unseal "$TPM_INDEX" "0,1,2,3,4,5,6,7" "$TPM_SIZE" \ "$key_file" "$tpm_password"; then exit 0 fi diff --git a/initrd/bin/key-init b/initrd/bin/key-init.sh similarity index 97% rename from initrd/bin/key-init rename to initrd/bin/key-init.sh index 3213a9d43..b26da2910 100755 --- a/initrd/bin/key-init +++ b/initrd/bin/key-init.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh TRACE_FUNC diff --git a/initrd/bin/lock_chip b/initrd/bin/lock_chip.sh similarity index 98% rename from initrd/bin/lock_chip rename to initrd/bin/lock_chip.sh index 26c9c1c78..8fb71401c 100755 --- a/initrd/bin/lock_chip +++ b/initrd/bin/lock_chip.sh @@ -5,7 +5,7 @@ # - >=Skylake: same as above and CONFIG_SOC_INTEL_COMMON_SPI_LOCKDOWN_SMM=y, CONFIG_SPI_FLASH_SMM=y and mode (eg: CONFIG_BOOTMEDIA_LOCK_WHOLE_RO=y) # - Heads is actually doing the CONFIG_INTEL_CHIPSET_LOCKDOWN equivalent here. -. /etc/functions +. /etc/functions.sh TRACE_FUNC if [ "$CONFIG_FINALIZE_PLATFORM_LOCKING" = "y" ]; then diff --git a/initrd/bin/media-scan b/initrd/bin/media-scan.sh similarity index 72% rename from initrd/bin/media-scan rename to initrd/bin/media-scan.sh index 068fa88a5..12d8c0d9b 100755 --- a/initrd/bin/media-scan +++ b/initrd/bin/media-scan.sh @@ -1,8 +1,8 @@ #!/bin/bash # Scan for USB installation options set -e -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config TRACE_FUNC @@ -11,23 +11,23 @@ TRACE_FUNC gpg_auth || die "GPG authentication failed" # Unmount any previous boot device -if grep -q /boot /proc/mounts ; then - umount /boot \ - || die "Unable to unmount /boot" +if grep -q /boot /proc/mounts; then + umount /boot || + die "Unable to unmount /boot" fi -available_partitions="$(blkid | while read line; do echo $line | awk -F ":" {'print $1'}; done )" +available_partitions="$(blkid | while read line; do echo $line | awk -F ":" {'print $1'}; done)" if [ "$1" == "usb" ]; then # Mount the USB boot device mount_usb || die "Unable to mount /media" elif $(echo $available_partitions | grep -q "$1"); then if grep -q /media /proc/mounts; then - umount /media \ - || die "Unable to unmount /media" + umount /media || + die "Unable to unmount /media" fi - mount "$1" /media \ - || die "Unable to mount $1 to /media" + mount "$1" /media || + die "Unable to mount $1 to /media" fi # Get USB boot device @@ -38,12 +38,11 @@ get_menu_option() { if [ -x /bin/whiptail ]; then MENU_OPTIONS="" n=0 - while read option - do - n=`expr $n + 1` + while read option; do + n=$(expr $n + 1) option=$(echo $option | tr " " "_") MENU_OPTIONS="$MENU_OPTIONS $n ${option}" - done < /tmp/iso_menu.txt + done /dev/null | sort -r > /tmp/iso_menu.txt || true -if [ `cat /tmp/iso_menu.txt | wc -l` -gt 0 ]; then +find /media -name "*.iso" -type f 2>/dev/null | sort -r >/tmp/iso_menu.txt || true +if [ $(cat /tmp/iso_menu.txt | wc -l) -gt 0 ]; then option_confirm="" - while [ -z "$option" -a "$option_index" != "s" ] - do + while [ -z "$option" -a "$option_index" != "s" ]; do get_menu_option done @@ -104,9 +101,9 @@ fi echo "!!! Could not find any ISO, trying bootable USB" # Attempt to pull verified config from device if [ -x /bin/whiptail ]; then - DO_WITH_DEBUG kexec-select-boot -b /media -c "*.cfg" -u -g -s + DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u -g -s else - DO_WITH_DEBUG kexec-select-boot -b /media -c "*.cfg" -u -s + DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u -s fi die "Something failed in selecting boot" diff --git a/initrd/bin/mount-usb b/initrd/bin/mount-usb.sh similarity index 98% rename from initrd/bin/mount-usb rename to initrd/bin/mount-usb.sh index 8acad1357..e4c4f252b 100755 --- a/initrd/bin/mount-usb +++ b/initrd/bin/mount-usb.sh @@ -1,8 +1,8 @@ #!/bin/bash # Mount a USB device -. /etc/functions -. /etc/gui_functions -. /etc/luks-functions +. /etc/functions.sh +. /etc/gui_functions.sh +. /etc/luks-functions.sh TRACE_FUNC diff --git a/initrd/bin/network-init-recovery b/initrd/bin/network-init-recovery.sh similarity index 99% rename from initrd/bin/network-init-recovery rename to initrd/bin/network-init-recovery.sh index 638d2b3b0..20d8baf4d 100755 --- a/initrd/bin/network-init-recovery +++ b/initrd/bin/network-init-recovery.sh @@ -1,6 +1,6 @@ #!/bin/bash -. /etc/functions +. /etc/functions.sh TRACE_FUNC diff --git a/initrd/bin/oem-factory-reset b/initrd/bin/oem-factory-reset.sh similarity index 99% rename from initrd/bin/oem-factory-reset rename to initrd/bin/oem-factory-reset.sh index e988bda2a..31aa89097 100755 --- a/initrd/bin/oem-factory-reset +++ b/initrd/bin/oem-factory-reset.sh @@ -4,9 +4,9 @@ set -o pipefail ## External files sourced -. /etc/functions -. /etc/gui_functions -. /etc/luks-functions +. /etc/functions.sh +. /etc/gui_functions.sh +. /etc/luks-functions.sh . /tmp/config TRACE_FUNC @@ -724,7 +724,7 @@ generate_checksums() { # create Heads TPM counter if [ "$CONFIG_TPM" = "y" ]; then if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then - tpmr counter_create \ + tpmr.sh counter_create \ -pwdc '' \ -la -3135106223 | tee /tmp/counter >/dev/null 2>&1 || @@ -847,7 +847,7 @@ set_default_boot_option() { echo "$entry" >/boot/kexec_default.$index.txt # validate boot option - (cd /boot && /bin/kexec-boot -b "/boot" -e "$entry" -f | + (cd /boot && /bin/kexec-boot.sh -b "/boot" -e "$entry" -f | xargs sha256sum >$hash_file 2>/dev/null) || whiptail_error_die "Failed to create hashes of boot files" @@ -1274,7 +1274,7 @@ fi ## reset TPM and set password if [ "$CONFIG_TPM" = "y" ]; then echo -e "\nResetting TPM...\n" - tpmr reset "$TPM_PASS" >/dev/null 2>/tmp/error + tpmr.sh reset "$TPM_PASS" >/dev/null 2>/tmp/error fi if [ $? -ne 0 ]; then ERROR=$(tail -n 1 /tmp/error | fold -s) diff --git a/initrd/bin/oem-system-info-xx30 b/initrd/bin/oem-system-info-xx30.sh similarity index 97% rename from initrd/bin/oem-system-info-xx30 rename to initrd/bin/oem-system-info-xx30.sh index 1918863f7..812b7e47f 100755 --- a/initrd/bin/oem-system-info-xx30 +++ b/initrd/bin/oem-system-info-xx30.sh @@ -5,9 +5,9 @@ BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} MAIN_MENU_TITLE="${BOARD_NAME} | Extended System Information" export BG_COLOR_MAIN_MENU="normal" -. /etc/functions -. /etc/gui_functions -. /etc/luks-functions +. /etc/functions.sh +. /etc/gui_functions.sh +. /etc/luks-functions.sh . /tmp/config TRACE_FUNC diff --git a/initrd/bin/poweroff b/initrd/bin/poweroff.sh similarity index 87% rename from initrd/bin/poweroff rename to initrd/bin/poweroff.sh index bbf0a7496..b0ef737c7 100755 --- a/initrd/bin/poweroff +++ b/initrd/bin/poweroff.sh @@ -1,11 +1,11 @@ #!/bin/bash -. /etc/functions +. /etc/functions.sh TRACE_FUNC # Shut down TPM if [ "$CONFIG_TPM" = "y" ]; then - tpmr shutdown + tpmr.sh shutdown fi # Sync all mounted filesystems diff --git a/initrd/bin/qubes-measure-luks b/initrd/bin/qubes-measure-luks.sh similarity index 92% rename from initrd/bin/qubes-measure-luks rename to initrd/bin/qubes-measure-luks.sh index fc6ef2227..6d3b3c2bb 100755 --- a/initrd/bin/qubes-measure-luks +++ b/initrd/bin/qubes-measure-luks.sh @@ -1,7 +1,7 @@ #!/bin/bash # Measure all of the LUKS Disk Encryption headers into # a PCR so that we can detect disk swap attacks. -. /etc/functions +. /etc/functions.sh TRACE_FUNC DEBUG "Arguments passed to qubes-measure-luks: $@" @@ -21,5 +21,5 @@ rm /tmp/lukshdr-* TRACE_FUNC INFO "TPM: Extending PCR[6] with hash of LUKS headers from /tmp/luksDump.txt" -tpmr extend -ix 6 -if /tmp/luksDump.txt || +tpmr.sh extend -ix 6 -if /tmp/luksDump.txt || die "Unable to extend PCR" diff --git a/initrd/bin/reboot b/initrd/bin/reboot.sh similarity index 96% rename from initrd/bin/reboot rename to initrd/bin/reboot.sh index 490003d03..0f576b22d 100755 --- a/initrd/bin/reboot +++ b/initrd/bin/reboot.sh @@ -1,10 +1,10 @@ #!/bin/bash -. /etc/functions +. /etc/functions.sh TRACE_FUNC # Shut down TPM if [ "$CONFIG_TPM" = "y" ]; then - tpmr shutdown + tpmr.sh shutdown fi # Sync all mounted filesystems diff --git a/initrd/bin/root-hashes-gui.sh b/initrd/bin/root-hashes-gui.sh index de645c10e..9871ee6c9 100755 --- a/initrd/bin/root-hashes-gui.sh +++ b/initrd/bin/root-hashes-gui.sh @@ -8,8 +8,8 @@ ROOT_MOUNT="/root" ROOT_DETECT_UNSUPPORTED_REASON="" ROOT_SUPPORTED_LAYOUT_MSG="Filesystem support in this build:\n- ext4 (ext2/ext3 compatible)\n- xfs\n\nSupported root layouts:\n- LUKS + ext4/ext3/ext2 or xfs\n- LUKS+LVM + ext4/ext3/ext2 or xfs\n\nNot supported:\n- btrfs" -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config export CONFIG_ROOT_DIRLIST_PRETTY=$(echo $CONFIG_ROOT_DIRLIST | sed -e 's/^/\//;s/ / \//g') diff --git a/initrd/bin/seal-hotpkey b/initrd/bin/seal-hotpkey.sh similarity index 98% rename from initrd/bin/seal-hotpkey rename to initrd/bin/seal-hotpkey.sh index 271547b97..bd32f16f2 100755 --- a/initrd/bin/seal-hotpkey +++ b/initrd/bin/seal-hotpkey.sh @@ -1,8 +1,8 @@ #!/bin/bash # Retrieve the sealed TOTP secret and initialize a USB Security dongle with it -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh HOTP_SECRET="/tmp/secret/hotp.key" HOTP_COUNTER="/boot/kexec_hotp_counter" @@ -31,7 +31,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" || + tpmr.sh 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 diff --git a/initrd/bin/seal-totp b/initrd/bin/seal-totp.sh similarity index 86% rename from initrd/bin/seal-totp rename to initrd/bin/seal-totp.sh index 3c593d697..596d2f191 100755 --- a/initrd/bin/seal-totp +++ b/initrd/bin/seal-totp.sh @@ -5,7 +5,7 @@ # Pass in a hostname if you want to change it from the default string # -. /etc/functions +. /etc/functions.sh TRACE_FUNC @@ -31,25 +31,25 @@ dd \ secret="$(base32 <$TOTP_SECRET)" pcrf="/tmp/secret/pcrf.bin" DEBUG "Sealing TOTP with actual state of PCR0-3" -tpmr pcrread 0 "$pcrf" -tpmr pcrread -a 1 "$pcrf" -tpmr pcrread -a 2 "$pcrf" -tpmr pcrread -a 3 "$pcrf" +tpmr.sh pcrread 0 "$pcrf" +tpmr.sh pcrread -a 1 "$pcrf" +tpmr.sh pcrread -a 2 "$pcrf" +tpmr.sh pcrread -a 3 "$pcrf" DEBUG "Sealing TOTP with boot state of PCR4 (Going to recovery shell extends PCR4)" # pcr 4 is expected to either: # zero on bare coreboot+linuxboot on x86 (boot mode: init) # already extended on ppc64 per BOOTKERNEL (skiboot) which boots heads. # Read from event log to catch both cases, even when called from recovery shell. -tpmr calcfuturepcr 4 >>"$pcrf" +tpmr.sh calcfuturepcr 4 >>"$pcrf" # pcr 5 (kernel modules loaded) is not measured at sealing/unsealing of totp DEBUG "Sealing TOTP neglecting PCR5 involvement (Dynamically loaded kernel modules are not firmware integrity attestation related)" # pcr 6 (drive LUKS header) is not measured at sealing/unsealing of totp DEBUG "Sealing TOTP without PCR6 involvement (LUKS header consistency is not firmware integrity attestation related)" # pcr 7 is containing measurements of user injected stuff in cbfs DEBUG "Sealing TOTP with actual state of PCR7 (User injected stuff in cbfs)" -tpmr pcrread -a 7 "$pcrf" +tpmr.sh pcrread -a 7 "$pcrf" #Make sure we clear the TPM Owner Password from memory in case it failed to be used to seal TOTP -tpmr seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSWORD" || +tpmr.sh seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSWORD" || die "Unable to write sealed secret to NVRAM from seal-totp" #Make sure we clear TPM TOTP sealed if we succeed to seal TOTP shred -n 10 -z -u "$TOTP_SEALED" 2>/dev/null diff --git a/initrd/bin/setconsolefont.sh b/initrd/bin/setconsolefont.sh index 63aacc782..05f8b7d70 100755 --- a/initrd/bin/setconsolefont.sh +++ b/initrd/bin/setconsolefont.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eo pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC diff --git a/initrd/bin/talos-init b/initrd/bin/talos-init.sh similarity index 96% rename from initrd/bin/talos-init rename to initrd/bin/talos-init.sh index 2addb5f04..9c1aa036b 100755 --- a/initrd/bin/talos-init +++ b/initrd/bin/talos-init.sh @@ -13,4 +13,4 @@ devmem 0x80060300D0010082 8 254 nvram -p ibm,skiboot --update-config fast-reset=0 # Proceed with standard init path -exec /bin/gui-init +exec /bin/gui-init.sh diff --git a/initrd/bin/tpm-reset b/initrd/bin/tpm-reset.sh similarity index 71% rename from initrd/bin/tpm-reset rename to initrd/bin/tpm-reset.sh index 5049bea02..7348d107d 100755 --- a/initrd/bin/tpm-reset +++ b/initrd/bin/tpm-reset.sh @@ -1,5 +1,5 @@ #!/bin/bash -. /etc/functions +. /etc/functions.sh echo '*****' echo '***** WARNING: This will erase all keys and secrets from the TPM' @@ -7,4 +7,4 @@ echo '*****' prompt_new_owner_password -tpmr reset "$tpm_owner_password" +tpmr.sh reset "$tpm_owner_password" diff --git a/initrd/bin/tpmr b/initrd/bin/tpmr.sh similarity index 99% rename from initrd/bin/tpmr rename to initrd/bin/tpmr.sh index ac803f8b6..c0a15c634 100755 --- a/initrd/bin/tpmr +++ b/initrd/bin/tpmr.sh @@ -1,7 +1,7 @@ #!/bin/bash # TPM Wrapper - to unify tpm and tpm2 subcommands -. /etc/functions +. /etc/functions.sh SECRET_DIR="/tmp/secret" PRIMARY_HANDLE="0x81000000" diff --git a/initrd/bin/uefi-init b/initrd/bin/uefi-init.sh similarity index 52% rename from initrd/bin/uefi-init rename to initrd/bin/uefi-init.sh index 8f9de1b34..2870ae600 100755 --- a/initrd/bin/uefi-init +++ b/initrd/bin/uefi-init.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh # Update initrd with CBFS files if [ -z "$CONFIG_PCR" ]; then @@ -10,19 +10,22 @@ fi CONFIG_GUID="74696e69-6472-632e-7069-6f2f75736572" # copy EFI file named $CONFIG_GUID to /tmp, measure and extract -GUID=`uefi -l | grep "^$CONFIG_GUID"` +GUID=$(uefi -l | grep "^$CONFIG_GUID") if [ -n "GUID" ]; then echo "Loading $GUID from ROM" TMPFILE=/tmp/uefi.$$ - uefi -r $GUID | gunzip -c > $TMPFILE \ - || die "Failed to read config GUID from ROM" + uefi -r $GUID | gunzip -c >$TMPFILE || + die "Failed to read config GUID from ROM" if [ "$CONFIG_TPM" = "y" ]; then - tpmr extend -ix "$CONFIG_PCR" -if $TMPFILE \ - || die "$filename: tpm extend failed" + tpmr.sh extend -ix "$CONFIG_PCR" -if $TMPFILE || + die "$filename: tpm extend failed" fi - ( cd / ; cpio -iud < $TMPFILE 2>/dev/null ) \ - || die "Failed to extract config GUID" + ( + cd / + cpio -iud <$TMPFILE 2>/dev/null + ) || + die "Failed to extract config GUID" fi diff --git a/initrd/bin/unpack_initramfs.sh b/initrd/bin/unpack_initramfs.sh index 4fff52f60..7604a4571 100755 --- a/initrd/bin/unpack_initramfs.sh +++ b/initrd/bin/unpack_initramfs.sh @@ -1,7 +1,7 @@ #! /bin/bash set -e -o pipefail -. /etc/functions +. /etc/functions.sh TRACE_FUNC # Unpack a Linux initramfs archive. diff --git a/initrd/bin/unseal-hotp b/initrd/bin/unseal-hotp.sh similarity index 95% rename from initrd/bin/unseal-hotp rename to initrd/bin/unseal-hotp.sh index 5fae80da9..154f90007 100755 --- a/initrd/bin/unseal-hotp +++ b/initrd/bin/unseal-hotp.sh @@ -1,7 +1,7 @@ #!/bin/bash # Retrieve the sealed file and counter from the NVRAM, unseal it and compute the hotp -. /etc/functions +. /etc/functions.sh HOTP_SECRET="/tmp/secret/hotp.key" HOTP_COUNTER="/boot/kexec_hotp_counter" @@ -44,7 +44,7 @@ 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" + tpmr.sh 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" diff --git a/initrd/bin/unseal-totp b/initrd/bin/unseal-totp.sh similarity index 85% rename from initrd/bin/unseal-totp rename to initrd/bin/unseal-totp.sh index da61deeea..c0d71808c 100755 --- a/initrd/bin/unseal-totp +++ b/initrd/bin/unseal-totp.sh @@ -1,7 +1,7 @@ #!/bin/bash # Retrieve the sealed file from the NVRAM, unseal it and compute the totp -. /etc/functions +. /etc/functions.sh TOTP_SECRET="/tmp/secret/totp.key" @@ -9,7 +9,7 @@ TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then DO_WITH_DEBUG --mask-position 5 \ - tpmr unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET" || + tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET" || die "Unable to unseal TOTP secret from TPM" fi diff --git a/initrd/bin/usb-autoboot.sh b/initrd/bin/usb-autoboot.sh index af7a0ac06..8d63fbf7d 100755 --- a/initrd/bin/usb-autoboot.sh +++ b/initrd/bin/usb-autoboot.sh @@ -1,8 +1,8 @@ #!/bin/bash set -o pipefail -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh # Automatically boot to a bootable USB medium if present. This is for # unattended boot; there is no UI. diff --git a/initrd/bin/usb-init b/initrd/bin/usb-init.sh similarity index 80% rename from initrd/bin/usb-init rename to initrd/bin/usb-init.sh index 06fbc1075..2d09f651d 100755 --- a/initrd/bin/usb-init +++ b/initrd/bin/usb-init.sh @@ -1,14 +1,14 @@ #!/bin/bash # Boot a USB installation -. /etc/functions +. /etc/functions.sh . /tmp/config TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then # Extend PCR4 as soon as possible - tpmr extend -ix 4 -ic usb + tpmr.sh extend -ix 4 -ic usb fi DO_WITH_DEBUG media-scan usb diff --git a/initrd/bin/wget-measure.sh b/initrd/bin/wget-measure.sh index 8e7e9e7bd..b1c1a9a6c 100755 --- a/initrd/bin/wget-measure.sh +++ b/initrd/bin/wget-measure.sh @@ -1,6 +1,6 @@ #!/bin/bash # get a file and extend a TPM PCR -. /etc/functions +. /etc/functions.sh die() { TRACE_FUNC @@ -19,6 +19,6 @@ fi wget "$URL" || die "$URL: failed" FILE="`basename "$URL"`" -tpmr extend -ix "$INDEX" -if "$FILE" || die "$FILE: tpm extend failed" +tpmr.sh extend -ix "$INDEX" -if "$FILE" || die "$FILE: tpm extend failed" diff --git a/initrd/bin/wipe-totp b/initrd/bin/wipe-totp.sh similarity index 83% rename from initrd/bin/wipe-totp rename to initrd/bin/wipe-totp.sh index 1a70cefa0..c7d142b77 100755 --- a/initrd/bin/wipe-totp +++ b/initrd/bin/wipe-totp.sh @@ -3,12 +3,12 @@ # rather than deleted, because deletion requires authorization. Wiping the # secret will cause the next boot to prompt to regenerate the secret. -. /etc/functions +. /etc/functions.sh TPM_NVRAM_SPACE=4d47 TPM_SIZE=312 if [ "$CONFIG_TPM" = "y" ]; then - tpmr destroy "$TPM_NVRAM_SPACE" "$TPM_SIZE" \ + tpmr.sh destroy "$TPM_NVRAM_SPACE" "$TPM_SIZE" \ || die "Unable to wipe sealed secret" fi diff --git a/initrd/etc/functions b/initrd/etc/functions.sh similarity index 99% rename from initrd/etc/functions rename to initrd/etc/functions.sh index 7872e101e..8c6e4b456 100644 --- a/initrd/etc/functions +++ b/initrd/etc/functions.sh @@ -344,7 +344,7 @@ recovery() { if [ "$CONFIG_TPM" = "y" ]; then INFO "TPM: Extending PCR[4] to prevent any further secret unsealing" - tpmr extend -ix 4 -ic recovery + tpmr.sh extend -ix 4 -ic recovery fi if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then @@ -998,7 +998,7 @@ check_tpm_counter() { warn "TPM Owner Password is required to create a new TPM counter for /boot content rollback prevention" fi - tpmr counter_create \ + tpmr.sh counter_create \ -pwdc '' \ -la $LABEL | tee /tmp/counter >/dev/null 2>&1 || @@ -1019,7 +1019,7 @@ read_tpm_counter() { 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 || + DO_WITH_DEBUG tpmr.sh 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." @@ -1031,7 +1031,7 @@ increment_tpm_counter() { 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 + if ! DO_WITH_DEBUG tpmr.sh 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 @@ -1039,13 +1039,13 @@ increment_tpm_counter() { fi # Try to increment the counter - if ! DO_WITH_DEBUG tpmr counter_increment -ix "$counter_id" -pwdc '' | + if ! DO_WITH_DEBUG tpmr.sh 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 + if DO_WITH_DEBUG tpmr.sh counter_create -pwdc '' -la 3135106223 >/tmp/new-counter 2>/dev/null; then NEW_COUNTER=$(cut -d: -f1 /dev/tty0 - oem-factory-reset --mode oem + oem-factory-reset.sh --mode oem # just in case... exit fi @@ -236,7 +236,7 @@ fi setconsolefont.sh if [ "$CONFIG_BASIC" = "y" ]; then - CONFIG_BOOTSCRIPT=/bin/gui-init-basic + CONFIG_BOOTSCRIPT=/bin/gui-init-basic.sh export CONFIG_HOTPKEY=n fi diff --git a/initrd/mount-boot b/initrd/mount-boot.sh similarity index 100% rename from initrd/mount-boot rename to initrd/mount-boot.sh diff --git a/initrd/sbin/insmod b/initrd/sbin/insmod.sh similarity index 80% rename from initrd/sbin/insmod rename to initrd/sbin/insmod.sh index 7ca6a28fe..785d87726 100755 --- a/initrd/sbin/insmod +++ b/initrd/sbin/insmod.sh @@ -4,17 +4,17 @@ # The default PCR to be extended is 5, but can be # overridden with the MODULE_PCR environment variable -. /etc/functions +. /etc/functions.sh TRACE_FUNC -MODULE="$1"; shift +MODULE="$1" +shift if [ -z "$MODULE_PCR" ]; then MODULE_PCR=5 fi - if [ -z "$MODULE" ]; then die "Usage: $0 module [args...]" fi @@ -23,7 +23,7 @@ if [ ! -r "$MODULE" ]; then die "$MODULE: not found?" fi -# Check if module is already loaded +# Check if module is already loaded # Transform module name changing _ for - and trailing .ko if present # Unify lsmod output to use - instead of _ for comparison module_name=$(basename "$MODULE" | sed 's/_/-/g' | sed 's/\.ko$//') @@ -46,19 +46,19 @@ if [ -z "$tpm_missing" ]; then if [ -n "$*" ]; then TRACE_FUNC INFO "Extending with module parameters and the module's content" - tpmr extend -ix "$MODULE_PCR" -ic "$*" - tpmr extend -ix "$MODULE_PCR" -if "$MODULE" \ - || die "$MODULE: tpm extend failed" + tpmr.sh extend -ix "$MODULE_PCR" -ic "$*" + tpmr.sh extend -ix "$MODULE_PCR" -if "$MODULE" || + die "$MODULE: tpm extend failed" else TRACE_FUNC INFO "No module parameters, extending only with the module's content" - tpmr extend -ix "$MODULE_PCR" -if "$MODULE" \ - || die "$MODULE: tpm extend failed" + tpmr.sh extend -ix "$MODULE_PCR" -if "$MODULE" || + die "$MODULE: tpm extend failed" fi fi # Since we have replaced the real insmod, we must invoke # the busybox insmod via the original executable DEBUG "Loading $MODULE with busybox insmod" -busybox insmod "$MODULE" "$@" \ - || die "$MODULE: insmod failed" +busybox insmod "$MODULE" "$@" || + die "$MODULE: insmod failed" diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config index 27dccb6bf..fba4c743d 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config @@ -58,8 +58,8 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -#export CONFIG_BOOTSCRIPT=/bin/generic-init -export CONFIG_BOOTSCRIPT=/bin/gui-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery #CONSOLE SELECTION diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config index 8ed475d76..79bd4b6d8 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config @@ -51,7 +51,7 @@ export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y #BOOT SCRIPT SELECTION -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh #export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery #CONSOLE SELECTION diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config index f5c4bfb85..8c03f28ba 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config @@ -48,7 +48,7 @@ export CONFIG_USB_KEYBOARD_REQUIRED=y export CONFIG_TPM=y #BOOT SCRIPT SELECTION -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh #Enable DEBUG output export CONFIG_DEBUG_OUTPUT=n export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n @@ -56,7 +56,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery #CONSOLE SELECTION diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config index 0615434b7..60080756c 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config @@ -49,7 +49,7 @@ export CONFIG_LINUX_USB_COMPANION_CONTROLLER=y export CONFIG_TPM=y #BOOT SCRIPT SELECTION -#export CONFIG_BOOTSCRIPT=/bin/generic-init +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh #Enable DEBUG output export CONFIG_DEBUG_OUTPUT=n export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n @@ -57,7 +57,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh #export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery #CONSOLE SELECTION diff --git a/unmaintained_boards/UNMAINTAINED_p8z77-m_pro-tpm1-maximized/UNMAINTAINED_p8z77-m_pro-tpm1-maximized.config b/unmaintained_boards/UNMAINTAINED_p8z77-m_pro-tpm1-maximized/UNMAINTAINED_p8z77-m_pro-tpm1-maximized.config index 458e978a0..66223ddac 100644 --- a/unmaintained_boards/UNMAINTAINED_p8z77-m_pro-tpm1-maximized/UNMAINTAINED_p8z77-m_pro-tpm1-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_p8z77-m_pro-tpm1-maximized/UNMAINTAINED_p8z77-m_pro-tpm1-maximized.config @@ -68,7 +68,7 @@ CONFIG_LINUX_USB=y CONFIG_MOBILE_TETHERING=y export CONFIG_TPM=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config b/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config index 32beb6741..bfd3ba65b 100644 --- a/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config +++ b/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config @@ -32,7 +32,7 @@ CONFIG_LINUX_SCSI_GDTH=y CONFIG_LINUX_ATA=y CONFIG_LINUX_AHCI=y -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery export CONFIG_BOOT_REQ_HASH=n diff --git a/unmaintained_boards/UNMAINTAINED_t420/UNMAINTAINED_t420.config b/unmaintained_boards/UNMAINTAINED_t420/UNMAINTAINED_t420.config index a4d2c6fd4..f477d062e 100644 --- a/unmaintained_boards/UNMAINTAINED_t420/UNMAINTAINED_t420.config +++ b/unmaintained_boards/UNMAINTAINED_t420/UNMAINTAINED_t420.config @@ -27,7 +27,7 @@ CONFIG_LINUX_USB=y CONFIG_LINUX_E1000E=y export CONFIG_TPM=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t430-hotp-legacy/UNMAINTAINED_t430-hotp-legacy.config b/unmaintained_boards/UNMAINTAINED_t430-hotp-legacy/UNMAINTAINED_t430-hotp-legacy.config index 93001f92e..7cff8d77c 100644 --- a/unmaintained_boards/UNMAINTAINED_t430-hotp-legacy/UNMAINTAINED_t430-hotp-legacy.config +++ b/unmaintained_boards/UNMAINTAINED_t430-hotp-legacy/UNMAINTAINED_t430-hotp-legacy.config @@ -56,7 +56,7 @@ CONFIG_DROPBEAR=n #Ethernet driver (Heads only) CONFIG_LINUX_E1000E=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t430-legacy/UNMAINTAINED_t430-legacy.config b/unmaintained_boards/UNMAINTAINED_t430-legacy/UNMAINTAINED_t430-legacy.config index 09a254988..afd4c9fa9 100644 --- a/unmaintained_boards/UNMAINTAINED_t430-legacy/UNMAINTAINED_t430-legacy.config +++ b/unmaintained_boards/UNMAINTAINED_t430-legacy/UNMAINTAINED_t430-legacy.config @@ -50,7 +50,7 @@ CONFIG_NEWT=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t520-hotp-maximized/UNMAINTAINED_t520-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_t520-hotp-maximized/UNMAINTAINED_t520-hotp-maximized.config index 8d6dd05be..ca6fcbc3c 100644 --- a/unmaintained_boards/UNMAINTAINED_t520-hotp-maximized/UNMAINTAINED_t520-hotp-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_t520-hotp-maximized/UNMAINTAINED_t520-hotp-maximized.config @@ -56,7 +56,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t520-maximized/UNMAINTAINED_t520-maximized.config b/unmaintained_boards/UNMAINTAINED_t520-maximized/UNMAINTAINED_t520-maximized.config index 7346163f8..ba42f6668 100644 --- a/unmaintained_boards/UNMAINTAINED_t520-maximized/UNMAINTAINED_t520-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_t520-maximized/UNMAINTAINED_t520-maximized.config @@ -55,7 +55,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t530-dgpu-hotp-maximized/UNMAINTAINED_t530-dgpu-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_t530-dgpu-hotp-maximized/UNMAINTAINED_t530-dgpu-hotp-maximized.config index 61084c020..6b4e9dd9d 100644 --- a/unmaintained_boards/UNMAINTAINED_t530-dgpu-hotp-maximized/UNMAINTAINED_t530-dgpu-hotp-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_t530-dgpu-hotp-maximized/UNMAINTAINED_t530-dgpu-hotp-maximized.config @@ -60,7 +60,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_t530-dgpu-maximized/UNMAINTAINED_t530-dgpu-maximized.config b/unmaintained_boards/UNMAINTAINED_t530-dgpu-maximized/UNMAINTAINED_t530-dgpu-maximized.config index 94d2780f1..978638ed9 100644 --- a/unmaintained_boards/UNMAINTAINED_t530-dgpu-maximized/UNMAINTAINED_t530-dgpu-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_t530-dgpu-maximized/UNMAINTAINED_t530-dgpu-maximized.config @@ -59,7 +59,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized.config index e35937fd6..015cd1124 100644 --- a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K1000m-hotp-maximized.config @@ -60,7 +60,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-maximized/UNMAINTAINED_w530-dgpu-K1000m-maximized.config b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-maximized/UNMAINTAINED_w530-dgpu-K1000m-maximized.config index f091f80d2..e0b2581ea 100644 --- a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-maximized/UNMAINTAINED_w530-dgpu-K1000m-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K1000m-maximized/UNMAINTAINED_w530-dgpu-K1000m-maximized.config @@ -59,7 +59,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized.config b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized.config index 18d207e25..c55515392 100644 --- a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized/UNMAINTAINED_w530-dgpu-K2000m-hotp-maximized.config @@ -60,7 +60,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-maximized/UNMAINTAINED_w530-dgpu-K2000m-maximized.config b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-maximized/UNMAINTAINED_w530-dgpu-K2000m-maximized.config index a05be9046..c79b9630e 100644 --- a/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-maximized/UNMAINTAINED_w530-dgpu-K2000m-maximized.config +++ b/unmaintained_boards/UNMAINTAINED_w530-dgpu-K2000m-maximized/UNMAINTAINED_w530-dgpu-K2000m-maximized.config @@ -59,7 +59,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=y -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_x220/UNMAINTAINED_x220.config b/unmaintained_boards/UNMAINTAINED_x220/UNMAINTAINED_x220.config index 494648e29..4e1e70dfe 100644 --- a/unmaintained_boards/UNMAINTAINED_x220/UNMAINTAINED_x220.config +++ b/unmaintained_boards/UNMAINTAINED_x220/UNMAINTAINED_x220.config @@ -50,7 +50,7 @@ CONFIG_FBWHIPTAIL=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_x230-hotp-legacy/UNMAINTAINED_x230-hotp-legacy.config b/unmaintained_boards/UNMAINTAINED_x230-hotp-legacy/UNMAINTAINED_x230-hotp-legacy.config index 9cfbc4d6b..9139eebc9 100644 --- a/unmaintained_boards/UNMAINTAINED_x230-hotp-legacy/UNMAINTAINED_x230-hotp-legacy.config +++ b/unmaintained_boards/UNMAINTAINED_x230-hotp-legacy/UNMAINTAINED_x230-hotp-legacy.config @@ -56,7 +56,7 @@ CONFIG_DROPBEAR=n #Ethernet driver (Heads only) CONFIG_LINUX_E1000E=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNMAINTAINED_x230-legacy/UNMAINTAINED_x230-legacy.config b/unmaintained_boards/UNMAINTAINED_x230-legacy/UNMAINTAINED_x230-legacy.config index d093dcdbf..805610639 100644 --- a/unmaintained_boards/UNMAINTAINED_x230-legacy/UNMAINTAINED_x230-legacy.config +++ b/unmaintained_boards/UNMAINTAINED_x230-legacy/UNMAINTAINED_x230-legacy.config @@ -49,7 +49,7 @@ CONFIG_NEWT=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/UNTESTED_leopard/UNTESTED_leopard.config b/unmaintained_boards/UNTESTED_leopard/UNTESTED_leopard.config index 8d874d736..9349c8508 100644 --- a/unmaintained_boards/UNTESTED_leopard/UNTESTED_leopard.config +++ b/unmaintained_boards/UNTESTED_leopard/UNTESTED_leopard.config @@ -37,7 +37,7 @@ CONFIG_LINUX_USB=y #CONFIG_LINUX_E1000E=y #CONFIG_LINUX_NVME=y -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_TPM=n export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/UNTESTED_r630/UNTESTED_r630.config b/unmaintained_boards/UNTESTED_r630/UNTESTED_r630.config index 14d6abb67..09ea71c27 100644 --- a/unmaintained_boards/UNTESTED_r630/UNTESTED_r630.config +++ b/unmaintained_boards/UNTESTED_r630/UNTESTED_r630.config @@ -25,7 +25,7 @@ CONFIG_LINUX_IGB=y CONFIG_LINUX_MEGARAID=y CONFIG_LINUX_E1000E=y -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/UNTESTED_s2600wf/UNTESTED_s2600wf.config b/unmaintained_boards/UNTESTED_s2600wf/UNTESTED_s2600wf.config index 2a0148d73..4e11f6264 100644 --- a/unmaintained_boards/UNTESTED_s2600wf/UNTESTED_s2600wf.config +++ b/unmaintained_boards/UNTESTED_s2600wf/UNTESTED_s2600wf.config @@ -38,6 +38,6 @@ CONFIG_LINUX_ATA=y CONFIG_LINUX_AHCI=y export CONFIG_TPM=n -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/UNTESTED_tioga/UNTESTED_tioga.config b/unmaintained_boards/UNTESTED_tioga/UNTESTED_tioga.config index 109c02fc5..18b44e214 100644 --- a/unmaintained_boards/UNTESTED_tioga/UNTESTED_tioga.config +++ b/unmaintained_boards/UNTESTED_tioga/UNTESTED_tioga.config @@ -40,7 +40,7 @@ CONFIG_LINUX_USB=y CONFIG_LINUX_NVME=y CONFIG_LINUX_BCM=y -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_TPM=n export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/UNTESTED_winterfell/UNTESTED_winterfell.config b/unmaintained_boards/UNTESTED_winterfell/UNTESTED_winterfell.config index d9cae6534..88dc58af0 100644 --- a/unmaintained_boards/UNTESTED_winterfell/UNTESTED_winterfell.config +++ b/unmaintained_boards/UNTESTED_winterfell/UNTESTED_winterfell.config @@ -39,7 +39,7 @@ CONFIG_LINUX_AHCI=y CONFIG_LINUX_E1000E=y CONFIG_LINUX_NVME=y -export CONFIG_BOOTSCRIPT=/bin/generic-init +export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_TPM=n export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n diff --git a/unmaintained_boards/x230-hotp-legacy/x230-hotp-legacy.config b/unmaintained_boards/x230-hotp-legacy/x230-hotp-legacy.config index 1d6233d72..2ad309b58 100644 --- a/unmaintained_boards/x230-hotp-legacy/x230-hotp-legacy.config +++ b/unmaintained_boards/x230-hotp-legacy/x230-hotp-legacy.config @@ -56,7 +56,7 @@ CONFIG_DROPBEAR=n #Ethernet driver (Heads only) CONFIG_LINUX_E1000E=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" diff --git a/unmaintained_boards/x230-legacy/x230-legacy.config b/unmaintained_boards/x230-legacy/x230-legacy.config index bdd821218..566290a19 100644 --- a/unmaintained_boards/x230-legacy/x230-legacy.config +++ b/unmaintained_boards/x230-legacy/x230-legacy.config @@ -49,7 +49,7 @@ CONFIG_NEWT=y #SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) CONFIG_DROPBEAR=n -export CONFIG_BOOTSCRIPT=/bin/gui-init +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_ADD="" From a41bdcc54b929397bf4a50137a1332a0744f3030 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 31 Mar 2026 21:41:23 -0400 Subject: [PATCH 02/12] initrd: add dongle firmware version constants and shared GPG functions Add initrd/etc/dongle-versions with USB security dongle firmware version constants (firmware versions, VID:PID) used by OEM reset and firmware display. Add initrd/etc/gpg_functions.sh with shared GPG functions factored out of gpg-gui.sh for reusability across scripts. Signed-off-by: Thierry Laurion --- initrd/etc/dongle-versions | 42 +++++++++++ initrd/etc/gpg_functions.sh | 143 ++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 initrd/etc/dongle-versions create mode 100644 initrd/etc/gpg_functions.sh diff --git a/initrd/etc/dongle-versions b/initrd/etc/dongle-versions new file mode 100644 index 000000000..4947a0123 --- /dev/null +++ b/initrd/etc/dongle-versions @@ -0,0 +1,42 @@ +# Minimum recommended firmware versions for USB security dongles. +# +# Update these values when a new minimum known-good firmware is established. +# Sourced by initrd/etc/functions (hotpkey_fw_display) at runtime. +# +# Version strings must be prefixed with 'v' and use dot notation (e.g. v1.8.3) +# so that sort -V ordering works correctly for the comparison in hotpkey_fw_display. + +# Nitrokey 3 (NK3) - VID:PID 20a0:42b2 (3A Mini / 3A NFC / 3C NFC all share this PID) +# Identified by "Firmware Nitrokey 3: vX.Y.Z" in hotp_verification info output. +HOTPKEY_NK3_MIN_VER="v1.8.3" +HOTPKEY_NK3_LATEST_VER="v1.8.3" + +# Nitrokey Pro / Nitrokey Pro 2 - VID:PID 20a0:4108 (Pro and Pro 2 share the same PID) +# Identified by " Firmware: vX.Y" in hotp_verification info output (tab-indented). +# Note: Nitrokey Pro and Pro 2 cannot be distinguished by USB ID alone. +HOTPKEY_NITROKEY_MIN_VER="v0.15" +HOTPKEY_NITROKEY_LATEST_VER="v0.15" + +# Nitrokey Storage / Nitrokey Storage 2 - VID:PID 20a0:4109 +# Identified by " Firmware: vX.Y" in hotp_verification info output (same format as Pro). +# Note: Storage has its own firmware tree separate from Nitrokey Pro. +# TODO: confirm minimum recommended firmware version for Storage / Storage 2 upstream. +HOTPKEY_STORAGE_MIN_VER="" +HOTPKEY_STORAGE_LATEST_VER="" + +# Librem Key - VID:PID 316d:4c4b +# Identified by " Firmware: vX.Y" in hotp_verification info output (same format as Pro). +# Note: Librem Key is a Nitrokey Pro rebrand sharing the same firmware base and version space. +# Note: Librem Key CANNOT be upgraded via software regardless of firmware version. +# Any firmware update requires external hardware programmer or Purism servicing. +# HOTPKEY_EXTERNAL_REPROGRAM_BELOW does not apply to Librem Key. +HOTPKEY_LIBREM_LATEST_VER="v0.10" + +# Firmware version below which Nitrokey Pro / Pro 2 CANNOT be upgraded via nitropy. +# v0.10 is the last firmware requiring an external programmer; v0.11+ can be upgraded +# in-system via nitropy. +# Source: https://github.com/Nitrokey/nitrokey-pro-firmware/issues/95 +# Note: This threshold applies to Nitrokey Pro / Pro 2 only. +# Librem Key is never self-upgradeable (see above). +# Nitrokey Storage has a separate firmware codebase and version space. +HOTPKEY_EXTERNAL_REPROGRAM_BELOW="v0.11" diff --git a/initrd/etc/gpg_functions.sh b/initrd/etc/gpg_functions.sh new file mode 100644 index 000000000..0afaccfc6 --- /dev/null +++ b/initrd/etc/gpg_functions.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +gpg_flash_rom() { + if [ "$1" = "replace" ]; then + [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg + [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx + [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg + fi + + cat "$PUBKEY" | gpg --import + gpg --list-keys --fingerprint --with-colons | sed -E -n -e 's/^fpr:::::::::([0-9A-F]+):$/\1:6:/p' | gpg --import-ownertrust + gpg --update-trust + + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/pubring.kbx"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/pubring.kbx" + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/pubring.gpg"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/pubring.gpg" + [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg + fi + fi + + if [ -e /.gnupg/pubring.kbx ]; then + cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/pubring.kbx" -f /.gnupg/pubring.kbx + [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg + fi + if [ -e /.gnupg/pubring.gpg ]; then + cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/pubring.gpg" -f /.gnupg/pubring.gpg + fi + + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/trustdb.gpg"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/trustdb.gpg" + fi + if [ -e /.gnupg/trustdb.gpg ]; then + cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/trustdb.gpg" -f /.gnupg/trustdb.gpg + fi + + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/otrust.txt"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/otrust.txt" + fi + + if cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/etc/config.user"; then + cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/etc/config.user" + fi + if [ -e /etc/config.user ]; then + cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/etc/config.user" -f /etc/config.user + fi + /bin/flash.sh /tmp/gpg-gui.rom +} + +gpg_post_gen_mgmt() { + GPG_GEN_KEY=$(grep -A1 pub /tmp/gpg_card_edit_output | tail -n1 | sed -nr 's/^([ ])*//p') + gpg --export --armor $GPG_GEN_KEY >"/tmp/${GPG_GEN_KEY}.asc" + if (whiptail_warning --title 'Add Public Key to USB disk?' \ + --yesno "Would you like to copy the GPG public key you generated to a USB disk?\n\nYou may need it, if you want to use it outside of Heads later.\n\nThe file will show up as ${GPG_GEN_KEY}.asc" 0 80); then + mount_usb + mount -o remount,rw /media + cp "/tmp/${GPG_GEN_KEY}.asc" "/media/${GPG_GEN_KEY}.asc" + if [ $? -eq 0 ]; then + whiptail_type $BG_COLOR_MAIN_MENU --title "The GPG Key Copied Successfully" \ + --msgbox "${GPG_GEN_KEY}.asc copied successfully." 0 80 + else + whiptail_error --title 'ERROR: Copy Failed' \ + --msgbox "Unable to copy ${GPG_GEN_KEY}.asc to /media" 0 80 + fi + umount /media + fi + if (whiptail --title 'Add Public Key to Running BIOS?' \ + --yesno "Would you like to add the GPG public key you generated to the BIOS?\n\nThis makes it a trusted key used to sign files in /boot\n\n" 0 80); then + /bin/flash.sh -r /tmp/gpg-gui.rom + if [ ! -s /tmp/gpg-gui.rom ]; then + whiptail_error --title 'ERROR: BIOS Read Failed!' \ + --msgbox "Unable to read BIOS" 0 80 + exit 1 + fi + PUBKEY="/tmp/${GPG_GEN_KEY}.asc" + gpg_flash_rom + fi +} + +gpg_add_key_reflash() { + if (whiptail --title 'GPG public key required' \ + --yesno "This requires you insert a USB drive containing:\n* Your GPG public key (*.key or *.asc)\n\nAfter you select this file, this program will copy and reflash your BIOS\n\nDo you want to proceed?" 0 80); then + mount_usb + if grep -q /media /proc/mounts; then + find /media -name '*.key' >/tmp/filelist.txt + find /media -name '*.asc' >>/tmp/filelist.txt + file_selector "/tmp/filelist.txt" "Choose your GPG public key" + if [ "$FILE" = "" ]; then + return 1 + fi + PUBKEY=$FILE + + /bin/flash.sh -r /tmp/gpg-gui.rom + if [ ! -s /tmp/gpg-gui.rom ]; then + whiptail_error --title 'ERROR: BIOS Read Failed!' \ + --msgbox "Unable to read BIOS" 0 80 + return 1 + fi + + if (whiptail --title 'Update ROM?' \ + --yesno "This will reflash your BIOS with the updated version\n\nDo you want to proceed?" 0 80); then + gpg_flash_rom + fi + fi + fi + return 1 +} + +gpg_add_key_to_standalone_rom() { + if (whiptail --title 'ROM and GPG public key required' \ + --yesno "This requires you insert a USB drive containing:\n* Your GPG public key (*.key or *.asc)\n* Your BIOS image (*.rom)\n\nAfter you select these files, this program will reflash your BIOS\n\nDo you want to proceed?" 0 80); then + mount_usb + if grep -q /media /proc/mounts; then + find /media -name '*.key' >/tmp/filelist.txt + find /media -name '*.asc' >>/tmp/filelist.txt + file_selector "/tmp/filelist.txt" "Choose your GPG public key" + if [ "$FILE" == "" ]; then + return 1 + fi + PUBKEY=$FILE + + find /media -name '*.rom' >/tmp/filelist.txt + file_selector "/tmp/filelist.txt" "Choose the ROM to load your key onto" + if [ "$FILE" == "" ]; then + return 1 + fi + cp "$FILE" /tmp/gpg-gui.rom + + if (whiptail_warning --title 'Flash ROM?' \ + --yesno "This will replace your old ROM with the selected ROM\n\nDo you want to proceed?" 0 80); then + gpg_flash_rom + fi + fi + fi + return 1 +} + +gpg_replace_key_reflash() { + [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg + [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx + [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg + gpg_add_key_reflash +} From be86fd0210e88e0e31bd501465cf55e74ec02335 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 31 Mar 2026 23:17:58 -0400 Subject: [PATCH 03/12] initrd: update function libraries and runtime files - functions.sh: Add detect_usb_security_dongle_branding() to identify dongle type by USB VID:PID. Add hotpkey_fw_display() for firmware version display. Add whiptail wrapper functions (whiptail_error, whiptail_info, whiptail_yesno). - gui_functions.sh: Add whiptail wrapper functions for consistent UI. - luks-functions.sh: Add luks_tpm_reseal_prompt() to guide users to reseal TPM after LUKS modifications. - init: Add CBMEM console capture before PCR extensions for measuring_trace.log. Add STATUS messages for boot progress. Add boot script respawn loop. Improve quiet mode messaging. - mount-boot.sh, sbin/*: Various consistency updates. Signed-off-by: Thierry Laurion --- initrd/.bash_history | 12 +- initrd/.gnupg/gpg-agent.conf | 1 + initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS | 2 +- initrd/etc/functions.sh | 1563 +++++++++++++++++++----- initrd/etc/gui_functions.sh | 607 ++++++++- initrd/etc/luks-functions.sh | 299 ++--- initrd/init | 69 +- initrd/mount-boot.sh | 22 +- initrd/sbin/config-dhcp.sh | 5 +- initrd/sbin/insmod.sh | 28 +- 10 files changed, 2106 insertions(+), 502 deletions(-) diff --git a/initrd/.bash_history b/initrd/.bash_history index 44fd60529..7ece3035c 100644 --- a/initrd/.bash_history +++ b/initrd/.bash_history @@ -1,11 +1,11 @@ #mount /boot in read-only by default mount /boot -#verify detached signature of /boot content -find /boot/kexec*.txt | gpg --verify /boot/kexec.sig - +#verify detached signature of /boot content (tries relative paths first; falls back to full paths for sigs made before the staging-dir change) +(cd /boot && sha256sum kexec*.txt) | gpgv.sh /boot/kexec.sig - || sha256sum /boot/kexec*.txt | gpgv.sh /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 --mode rw && gpg --home=/.gnupg/ --card-edit +mount-usb.sh --mode rw && gpg --home=/.gnupg/ --card-edit #Copy generated public key, private_subkey, trustdb and artifacts to external media for backup: 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: @@ -15,10 +15,10 @@ mount -o,remount ro /media #Flash modified reproducible rom with inserted public key and trustdb export from precedent step. Flushes actual rom's keys (-c: clean): flash.sh -c /media/coreboot.rom #Attest integrity of firmware as it is -seal-totp +seal-totp.sh #Verify Intel ME state: cbmem --console | grep '^ME' cbmem --console | less # Reboot/power off (important for devices with no keyboard to escape recovery shell) -reboot # Press Enter with this command to reboot -poweroff # Press Enter with this command to power off +reboot.sh # Press Enter with this command to reboot.sh +poweroff.sh # Press Enter with this command to power off diff --git a/initrd/.gnupg/gpg-agent.conf b/initrd/.gnupg/gpg-agent.conf index eba090d1d..2c05fa624 100644 --- a/initrd/.gnupg/gpg-agent.conf +++ b/initrd/.gnupg/gpg-agent.conf @@ -1,3 +1,4 @@ scdaemon-program /bin/scdaemon pinentry-program /bin/pinentry-tty +allow-loopback-pinentry daemon diff --git a/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS b/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS index cd523f64d..346ccb74d 100644 --- a/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS +++ b/initrd/etc/DEBUG_LOG_COPY_INSTRUCTIONS @@ -4,7 +4,7 @@ Welcome to the Recovery Shell! - 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 + - 'mount-usb.sh --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.sh b/initrd/etc/functions.sh index 8c6e4b456..994cb8258 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -8,8 +8,7 @@ # Only add the current script once to avoid repetition when the same script # sources this file multiple times or invokes TRACE_FUNC repeatedly. case "${TRACE_STACK}" in -*"main($0:"*) - ;; +*"main($0:"*) ;; *) TRACE_STACK="${TRACE_STACK:+$TRACE_STACK -> }main($0:0)" export TRACE_STACK @@ -18,100 +17,432 @@ esac # ------- Start of functions coming from /etc/ash_functions -die() { +# DIE - fatal error: print bold red message, wait for Enter, then exit 1. +# +# Console color: bold red (\033[1;31m). +# Red is the universal "error/danger" signal; the "!!! ERROR:" text prefix +# carries the same meaning for users who cannot distinguish red from other +# colors, so color is an enhancement rather than the sole signal. +# debug.log and /dev/kmsg receive plain text (no ANSI). +# Always visible in all output modes. +DIE() { TRACE_FUNC - #TODO: add colors to output, here red for ERROR? + # Always log to debug.log regardless of output mode - fatal errors must be + # captured for post-mortem analysis even when the console is suppressed. + echo "!!! ERROR: $* !!!" >>/tmp/debug.log if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - echo -e " !!! ERROR: $* !!!" | tee -a /tmp/debug.log /dev/kmsg >/dev/null - else - echo -e "!!! ERROR: $* !!!" >&2 + # debug mode: also route to kmsg for ordering with other debug output + echo "!!! ERROR: $* !!!" >/dev/kmsg 2>/dev/null || true fi + # Always show on console with bold red regardless of output mode. + # /dev/console = kernel console (follows the console= kernel parameter), + # so it reaches whatever output the kernel was configured for — serial, + # framebuffer, BMC — without requiring any process setup and without + # polluting stdout or stderr so callers never need to care about redirections. + echo -e "\033[1;31m!!! ERROR: $* !!!\033[0m" >/dev/console 2>/dev/null # ask user to press Enter prior to exit - read -r -p $'Press Enter to continue...\n\n' + INPUT "Press Enter to continue..." 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() { +# WARN - a likely problem the user should act on. +# +# Use WARN when ALL of the following are true: +# - There is a _likely_ problem (not a rare or remote possibility) +# - We are able to continue, possibly with degraded functionality +# - The warning is _actionable_: there is a reasonable change the user +# can make to silence it +# Do NOT use WARN for: +# - Informational messages about normal operations (use INFO) +# - Rare or unlikely edge cases that are not actionable (use DEBUG) +# - Fatal errors where we cannot continue (use DIE) +# +# Console color: bold yellow (\033[1;33m). +# Yellow is the most universally perceptible alert color across all common +# color-deficiency types: it is bright and distinct for deuteranopes, +# protanopes, and tritanopes alike. The "*** WARNING:" text prefix carries +# the meaning independently of color. +# debug.log and /dev/kmsg receive plain text (no ANSI). +# +# Output modes (always visible in all modes): +# Quiet (CONFIG_QUIET_MODE=y): /dev/console + debug.log +# Info (CONFIG_QUIET_MODE=n): /dev/console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log +# +# Do not overuse - WARN only has value when it is infrequent enough that +# users still notice and act on it. See doc/logging.md. +WARN() { TRACE_FUNC - #TODO: add colors to output, here yellow for WARNING? + # Always write to debug.log - complete audit trail regardless of mode. + echo >>/tmp/debug.log + echo " *** WARNING: $* ***" >>/tmp/debug.log + echo >>/tmp/debug.log + # Bold yellow to /dev/console in all modes. + # /dev/console = kernel console (follows console= kernel parameter): reaches + # serial, framebuffer, BMC — no process setup needed, callers never need to + # care about redirections (e.g. 2>/tmp/whiptail). + echo >/dev/console 2>/dev/null + echo -e "\033[1;33m *** WARNING: $* ***\033[0m" >/dev/console 2>/dev/null + echo >/dev/console 2>/dev/null if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - echo -e " *** WARNING: $* ***" | tee -a /tmp/debug.log /dev/kmsg >/dev/null - else - echo -e " *** WARNING: $* ***" >&2 + # debug mode: also route to kmsg for ordering with other debug output (no ANSI - kmsg strips it) + echo " *** WARNING: $* ***" | tee -a /dev/kmsg >/dev/null fi sleep 1 } -# Use DEBUG to track decisions made in script/function logic and the context -# relating to those decisions. Generally, focus on decision points, because -# straight-line execution can usually be followed without further tracing. See -# doc/logging.md. +# DEBUG - decision points and developer-relevant context. +# +# Use DEBUG to show the information that influences logical decisions and the +# result of those decisions. Focus on if/else/case branches: what information +# led to the branch, and which branch was taken. +# Use DO_WITH_DEBUG to capture command invocations (command+args at DEBUG +# level, stdout/stderr at LOG level) rather than calling DEBUG directly. +# Messages may freely include internal variable names, file paths, and +# technical subsystem details - this level targets Heads developers only. +# Do NOT use DEBUG for: +# - Command output or dumps of uncontrolled length (use LOG or DO_WITH_DEBUG) +# - Actions a non-developer user would understand (use INFO) +# +# Console color: none (plain text only; targets developers reading raw output). +# debug.log and /dev/kmsg receive plain text (no ANSI). +# Console output goes to /dev/console (the kernel console, follows the +# console= kernel parameter) so it reaches serial, framebuffer, BMC, etc. +# without requiring any process setup and without polluting stdout or stderr +# so callers never need to care about redirections. +# +# Output modes: +# Quiet (CONFIG_QUIET_MODE=y): debug.log only (no console) +# Info (CONFIG_QUIET_MODE=n): debug.log only (no console) +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log +# +# See doc/logging.md. DEBUG() { + # Always write to debug.log - debug.log is a complete audit trail regardless of mode. + echo "DEBUG: $*" >>/tmp/debug.log if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then + # debug mode: also echo to /dev/console and kmsg. # fold -s -w 960 will wrap lines at 960 characters on the last space before the limit - echo "DEBUG: $*" | fold -s -w 960 | while read line; do - echo "$line" | tee -a /tmp/debug.log /dev/kmsg >/dev/null + echo "DEBUG: $*" | fold -s -w 960 | while IFS= read -r line; do + echo "$line" | tee -a /dev/kmsg >/dev/null + echo "$line" >/dev/console 2>/dev/null done fi } -# Use TRACE to trace control flow through Heads. This is usually called by -# TRACE_FUNC, but you can use it to additionally trace parameter values, etc. -# Usually, use this to display unprocessed parameters that your script or -# function received. For information about the logic occurring in your script -# or function, use DEBUG. See doc/logging.md. +# TRACE / TRACE_FUNC - execution flow through scripts and functions. +# +# TRACE_FUNC MUST be called as the first line of every script and function. +# It emits the full call chain (including cross-process subprocess boundaries) +# leading to the current location: +# TRACE: caller(file:line) -> ... -> current_func(file:line) +# Use TRACE directly (in addition to TRACE_FUNC) only to show the raw +# unprocessed parameters received by a script or function from its caller. +# Do NOT use TRACE for logic or decisions inside the function - use DEBUG. +# Do NOT use TRACE to show processed/interpreted values - use DEBUG. +# +# Console color: none (plain text only; targets developers reading raw output). +# debug.log and /dev/kmsg receive plain text (no ANSI). +# Console output goes to /dev/console (the kernel console, follows the +# console= kernel parameter) so it reaches serial, framebuffer, BMC, etc. +# without requiring any process setup and without polluting stdout or stderr +# so callers never need to care about redirections. +# +# Output modes: +# Quiet (CONFIG_QUIET_MODE=y): debug.log only (no console) +# Info (CONFIG_QUIET_MODE=n): debug.log only (no console) +# Debug (CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y): /dev/console + debug.log +# +# See doc/logging.md. TRACE() { + # Always write to debug.log - debug.log is a complete audit trail regardless of mode. + echo "TRACE: $*" >>/tmp/debug.log if [ "$CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" = "y" ]; then - echo "TRACE: $*" | tee -a /tmp/debug.log /dev/kmsg >/dev/null + # tracing mode: also echo to /dev/console and kmsg. + echo "TRACE: $*" | tee -a /dev/kmsg >/dev/null + echo "TRACE: $*" >/dev/console 2>/dev/null fi } -# Use NOTE to explain behaviors that are _likely_ to be unexpected or confusing. -# Unlike INFO, this cannot be hidden, as the explained behavior would be too -# confusing without this output. -# Don't overuse this - too much NOTE output will cause users to ignore it. See -# doc/logging.md. +# NOTE - explains behaviors that are _likely_ to be unexpected or confusing +# to users new to Heads. +# +# Use NOTE only when the behavior is so unexpected that users need this +# explanation to make sense of what they are seeing. Examples: +# - An automatic reboot the user did not explicitly request +# - A GPG PIN prompt appearing at a point the user would not anticipate +# - A required action before the next step can proceed +# Unlike INFO, NOTE cannot be hidden: it always appears in every output mode. +# NOTE sleeps after printing to bring the message to the user's awareness +# and ensure it is not scrolled past before they can read it. +# Do NOT overuse: prefer INFO if the behavior is only sometimes unexpected. +# Too many NOTE messages train users to ignore them, defeating their purpose. +# +# Console color: italic white NOTE: prefix (\033[3;37m). +# White is the highest-contrast neutral hue on dark consoles (VGA/serial). +# Italic distinguishes NOTE from bold STATUS/WARN without imposing a semantic +# hue, satisfying WCAG 1.4.1 (color is not the sole signal; the NOTE: prefix +# and surrounding blank lines + 3-second sleep carry meaning independently). +# debug.log receives plain text (no ANSI). +# +# Output modes (always visible in all modes): +# Quiet (CONFIG_QUIET_MODE=y): console + debug.log +# Info (CONFIG_QUIET_MODE=n): console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): console + debug.log +# +# 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. + # Console: italic white NOTE: prefix, blank lines before/after, to /dev/console. + # /dev/console = kernel console (follows console= kernel parameter): reaches + # serial, framebuffer, BMC — no process setup needed, callers never need to + # care about redirections. + echo >/dev/console 2>/dev/null + echo -e "\033[3;37mNOTE:\033[0m $*" >/dev/console 2>/dev/null + echo >/dev/console 2>/dev/null + # Log file: plain text - no ANSI codes in debug.log; echo -e so \n in the + # message produces real newlines in the log (multi-line NOTE support). + echo -e "NOTE: $*" >>/tmp/debug.log + + # Sleep to bring the message to the user's awareness: NOTE is infrequent + # and important enough that the user must not scroll past it unread + sleep 3 +} + +# STATUS - announces an action currently in progress or just completed. +# +# Use STATUS for progress and action announcements that all users must see +# regardless of output mode. Examples: +# - An action starting or running: "Verifying ISO", "Building initrd", +# "Calculating hashes - this may take a while" +# - Completion of a security-relevant operation: "ISO signature verified", +# "LUKS device unlocked", "Verified root hashes" +# - A boot-path milestone: "Executing default boot for $name" +# +# Unlike INFO, STATUS is always visible in all output modes - a user in +# quiet mode must still be able to see what Heads is actively doing. +# Unlike NOTE, STATUS does not sleep - it is for routine progress and action +# confirmation, not unexpected behavior requiring deliberate user attention. +# +# Console color: bold only, no hue (\033[1m). +# No color is used for STATUS by design: STATUS is the most frequent visible +# output level and must be readable in every terminal theme (dark, light, +# high-contrast, monochrome) without relying on color perception. The >> +# prefix provides semantic differentiation instead. +# Bold ensures STATUS stands out over plain INFO/LOG text without any color. +# Output goes to /dev/console (kernel console, follows console= kernel +# parameter) so it reaches serial, framebuffer, BMC, etc. without requiring +# any process setup and without polluting stdout or stderr so callers never +# need to care about redirections (e.g. print_tree >/boot/kexec_tree.txt). +# STATUS does NOT sleep and does NOT print blank lines: it is called frequently +# and blank lines would make output very noisy. Use NOTE when blank lines and +# a sleep are needed to draw the user's attention. +# debug.log receives plain text (no ANSI). +# +# Output modes (always visible in all modes): +# Quiet (CONFIG_QUIET_MODE=y): /dev/console + debug.log +# Info (CONFIG_QUIET_MODE=n): /dev/console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log +# +# See doc/logging.md. +STATUS() { + # Console: bold >> prefix to /dev/console - announces an action in progress. + echo -e "\033[1m >>\033[0m $*" >/dev/console 2>/dev/null + # Log file: plain text - no ANSI codes in debug.log + echo " >> $*" >>/tmp/debug.log +} + +STATUS_OK() { + # Console: bold green "OK" prefix to /dev/console - confirms a successful result. + # Use STATUS_OK (not STATUS) when reporting that an operation succeeded, + # a verification passed, or a resource was confirmed available. + # Two signals make success scannable without relying on either alone: + # 1. "OK" text label - readable in monochrome, on serial consoles, + # and by users with color vision deficiency + # 2. Bold green color - instant visual scan for sighted users + # (Same convention as Linux/systemd "[ OK ]" boot messages.) + echo -e "\033[1;32m OK\033[0m $*" >/dev/console 2>/dev/null + # Log file: plain text - no ANSI codes in debug.log + echo " OK $*" >>/tmp/debug.log +} + +# INFO - high-level operational context for non-developer users. +# +# INFO is what makes "Info" output mode meaningful. A security-conscious +# user who enables Info mode expects to see a readable audit trail of what +# Heads is doing: what is being measured, verified, sealed, or decided. +# Use INFO for: +# - TPM PCR extensions: what is being measured and when +# (e.g. "TPM: Extending PCR[4] with boot configuration") +# - High-level operational decisions driven by user configuration +# (e.g. "Not booting automatically, automatic boot is disabled") +# Do NOT use INFO for: +# - Action progress or milestones the user must see in all modes (use STATUS) +# - Heads-internal details: file paths, variable values, CBFS operations, +# code-flow steps with no user-visible effect (use DEBUG instead) +# - Messages that require Heads developer knowledge to interpret (use DEBUG) +# - Behaviors so unexpected users need to be warned (use NOTE instead) +# +# Console color: green (\033[0;32m) in Info mode. +# In Debug mode: plain text to debug.log and /dev/kmsg (no ANSI; maintains +# ordering with DEBUG messages which also route through kmsg). +# Console output goes to /dev/console (kernel console, follows console= +# kernel parameter) so it reaches serial, framebuffer, BMC, etc. without +# requiring any process setup and without polluting stdout or stderr. +# +# Output modes: +# Quiet (CONFIG_QUIET_MODE=y): debug.log only (no console) +# Info (CONFIG_QUIET_MODE=n): /dev/console + debug.log +# Debug (CONFIG_DEBUG_OUTPUT=y): /dev/console + debug.log (via kmsg for ordering) +# +# 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 if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then - echo "$*" | tee -a /tmp/debug.log /dev/kmsg >/dev/null + # debug mode: plain to debug.log, measuring_trace.log, and kmsg - + # no ANSI, maintains ordering with DEBUG messages which also route through kmsg + echo "INFO: $*" | tee -a /tmp/debug.log /tmp/measuring_trace.log /dev/kmsg >/dev/null elif [ "$CONFIG_QUIET_MODE" = "y" ]; then - echo "$*" >>/tmp/debug.log + # quiet mode: no console output, but captured in both logs + echo "INFO: $*" | tee -a /tmp/debug.log /tmp/measuring_trace.log >/dev/null else - echo "$*" + # info mode: green text to /dev/console AND both log files. + # /dev/console = kernel console (follows console= kernel parameter): + # reaches serial, framebuffer, BMC — no process setup needed, callers + # never need to care about redirections. + echo -e "\033[0;32m$*\033[0m" >/dev/console 2>/dev/null + echo "INFO: $*" | tee -a /tmp/debug.log /tmp/measuring_trace.log >/dev/null fi } -# Write directly to the debug log (but not kmsg), never appears on console -# Main consumer is DO_WITH_DEBUG, which uses this to log command output +# LOG - command output and verbose state dumps, always to debug.log only. +# +# Use LOG to capture raw output of commands (lsblk, lsusb, gpg --list-keys, +# tpm2 pcrread, etc.) and any output of uncontrolled or potentially large +# length. LOG never appears on the console in any output mode - it is purely +# for post-hoc analysis of a submitted debug log. +# Prefer DO_WITH_DEBUG over calling LOG directly: DO_WITH_DEBUG captures the +# command and its arguments at DEBUG level, and routes stdout/stderr to LOG. +# Call LOG directly only when capturing output that is not a direct command +# invocation (e.g. filtering another command's stderr, state summaries). +# Do NOT use LOG for: +# - Short, fixed-length messages about decisions (use DEBUG) +# - Messages a user should ever see on console (use INFO, NOTE, or WARN) +# +# Output modes (never on console in any mode): +# Quiet (CONFIG_QUIET_MODE=y): debug.log only +# Info (CONFIG_QUIET_MODE=n): debug.log only +# Debug (CONFIG_DEBUG_OUTPUT=y): debug.log only +# +# See doc/logging.md. LOG() { echo "LOG: $*" >>/tmp/debug.log } +# INPUT - colored prompt for interactive user input. +# +# Direct replacement for the common pattern: +# echo "prompt" +# read [flags] VARNAME +# The prompt is displayed bold white (\033[1;37m) to draw the user's attention. +# Bold white is chosen for maximum contrast on VGA/dark consoles (21:1 ratio) +# without relying on color perception - it is readable under all color +# deficiency types and in monochrome/high-contrast terminal modes. +# Use INPUT for all prompts that require the user to type a response. +# Do NOT use INPUT for yes/no confirmation dialogs (use whiptail instead). +# +# Usage: INPUT "prompt text" [read-flags] [VARNAME] +# Examples: +# INPUT "Enter LUKS passphrase:" -r -s luks_passphrase +# INPUT "Press any key to continue:" -n 1 -s dummy +# INPUT "Enter filename:" -r filename +# +# The read flags and VARNAME are passed through to read after the prompt +# is displayed - any read option (e.g. -r, -s, -n N) is supported. +# +# Output modes (always visible in all modes): +# All modes: console prompt in bold white to /dev/console, plain text in debug.log +# +# See doc/logging.md. + +# detect_heads_tty - resolve the active interactive terminal and export it. +# Sets and exports HEADS_TTY and GPG_TTY. +# Must be called at script top-level (not inside a subshell) to take effect. +detect_heads_tty() { + if ! HEADS_TTY=$(tty 2>/dev/null); then + local _active _dev + _active=$(cat /sys/class/tty/console/active 2>/dev/null) + _dev="${_active##* }" + [ "$_dev" = "tty0" ] && _dev=$(cat /sys/class/tty/tty0/active 2>/dev/null || echo tty0) + HEADS_TTY="/dev/${_dev:-console}" + fi + export HEADS_TTY + export GPG_TTY="$HEADS_TTY" +} + +INPUT() { + TRACE_FUNC + local prompt="$1" + shift + # Log file: plain text - no ANSI codes in debug.log + echo "INPUT: $prompt" >>/tmp/debug.log + + if [ -n "$HEADS_TTY" ]; then + # gui-init context: HEADS_TTY is the actual interactive terminal (set by + # gui-init after cttyhack). Use it for both prompt output and read so + # that prompt and input always use the same device regardless of any + # stdout/stderr redirections the caller may have in effect. + # Print prompt with a trailing space so the cursor lands immediately after + # the prompt text on the same line — no blank line between prompt and input. + printf '\033[1;37m%s\033[0m ' "$prompt" >"$HEADS_TTY" 2>/dev/null + # Forward remaining args (read flags + variable name) directly to read. + # Note: static analyzers may report the caller's variable as "unassigned" + # because assignment through read "$@" indirection is not visible to them. + # This is a false positive - the variable is assigned correctly at runtime. + read "$@" <"$HEADS_TTY" + echo >"$HEADS_TTY" 2>/dev/null + else + # Pre-gui-init context (e.g. init's serial recovery shell launched with + # explicit stdin/stdout/stderr redirects to the serial device): + # honour the caller's redirections — use stderr for output and stdin for + # read so the correct device is used without hard-coding any path. + printf '\033[1;37m%s\033[0m ' "$prompt" >&2 + read "$@" + echo >&2 + fi +} + +# Filter known harmless LVM warning noise while preserving all other stderr. +# Messages that are expected during device scanning (e.g. "not an LVM PV") are +# redirected to the debug log only - they are not errors and should not appear +# on the console, especially in quiet mode. +_filter_lvm_stderr() { + while IFS= read -r line; do + case "$line" in + *"Failed to set up async io, using sync io."*) + continue + ;; + *"leaked on lvm invocation"*) + continue + ;; + *"Cannot use "*": device is too small"* | \ + *"Cannot use "*": device is an LV"* | \ + *"Failed to find physical volume"*) + LOG "lvm: $line" + continue + ;; + esac + printf '%s\n' "$line" >&2 + done +} + +# Wrapper for all runtime lvm invocations so users don't see benign async-io +# fallback warnings, especially in quiet mode. +run_lvm() { + command lvm "$@" 2> >(_filter_lvm_stderr) +} + fw_version() { local FW_VER=$(dmesg | grep 'DMI' | grep -o 'BIOS.*' | cut -f2- -d ' ') # chop off date, since will always be epoch w/timeless builds @@ -134,95 +465,264 @@ preserve_rom() { for old_file in $(echo $old_files); do new_file=$(cbfs.sh -o $1 -l | grep -x $old_file) if [ -z "$new_file" ]; then - echo "+++ Adding $old_file to $1" + DEBUG "Adding $old_file to $1" cbfs -t 50 -r $old_file >/tmp/rom.$$ || - die "Failed to read cbfs file from ROM" + DIE "Failed to read cbfs file from ROM" cbfs.sh -o $1 -a $old_file -f /tmp/rom.$$ || - die "Failed to write cbfs file to new ROM file" + DIE "Failed to write cbfs file to new ROM file" fi done } -confirm_gpg_card() { +# Color-code a PIN/security-token retry counter for the console. +# green (3+): safe; yellow (2): one attempt used; red (<=1 or unknown): danger. +# Works for both GPG card PIN retries and HOTP dongle (Nitrokey/Librem Key) PIN counters. +pin_color() { + case "$1" in + [3-9] | [1-9][0-9]) printf '\033[1;32m' ;; # green: 3 or more remaining + 2) printf '\033[1;33m' ;; # yellow: one attempt already used + *) printf '\033[1;31m' ;; # red: 0, 1, or unknown (locked/last try) + esac +} + +# Detect USB security dongle branding from USB VID:PID via lsusb. +# Sources: hotp-verification/src/device.c and targets/qemu.mk +# USB Security dongle (OpenPGP smart card) VID:PID table: +# 20a0:42b2 Nitrokey 3 (3A Mini / 3A NFC / 3C NFC - all share this PID) +# 20a0:4108 Nitrokey Pro / Pro 2 (Pro and Pro 2 share the same PID) +# 20a0:4109 Nitrokey Storage / Storage 2 +# 316d:4c4b Librem Key +# 16d0:21dc Canokey +# 1050:0113 Yubikey 4/5 (OTP+U2F+CCID) - legacy +# 1050:0114 Yubikey 4/5 (OTP+U2F+CCID) - OTP+CCID only +# 1050:0115 Yubikey 4/5 (OTP+U2F+CCID) - FIDO+CCID +# 1050:0404 Yubikey 5 (FIDO+CCID) +detect_usb_security_dongle_branding() { + local lsusb_out + lsusb_out="$(lsusb)" + # Check NK3 (42b2) before the broader 20a0 vendor match + if echo "$lsusb_out" | grep -q "20a0:42b2"; then + echo "Nitrokey 3" + elif echo "$lsusb_out" | grep -q "20a0:4108"; then + echo "Nitrokey Pro" + elif echo "$lsusb_out" | grep -q "20a0:4109"; then + echo "Nitrokey Storage" + elif echo "$lsusb_out" | grep -q "316d:4c4b"; then + echo "Librem Key" + elif echo "$lsusb_out" | grep -q "16d0:21dc"; then + echo "Canokey" + elif echo "$lsusb_out" | grep -q "1050:"; then + echo "Yubikey" + else + echo "USB Security dongle" + fi +} + +# Display USB security dongle firmware version with color coding. +# Green if the version meets the minimum known-good version for that device, +# yellow if the firmware is older and should be upgraded. +# Minimum versions are defined in /etc/dongle-versions for easy maintainability. +# $1: raw output from "hotp_verification info" +# $2: dongle branding string (e.g. "Nitrokey", "Librem Key") +hotpkey_fw_display() { + [ -f /tmp/hotpkey_fw_shown ] && return + local info="$1" branding="$2" fw_ver min_ver latest_ver extras critical + extras="" + critical="n" + + # Load minimum recommended firmware versions + . /etc/dongle-versions + + if echo "$info" | grep -q "Firmware Nitrokey 3:"; then + # NK3: "Firmware Nitrokey 3: v1.8.3" + fw_ver="$(echo "$info" | grep "Firmware Nitrokey 3:" | sed 's/.*: *//')" + min_ver="$HOTPKEY_NK3_MIN_VER" + latest_ver="$HOTPKEY_NK3_LATEST_VER" + # Also capture Secrets App version + local app_ver + app_ver="$(echo "$info" | grep "Firmware Secrets App:" | sed 's/.*: *//')" + [ -n "$app_ver" ] && extras=" (Secrets App: ${app_ver})" + elif echo "$info" | grep -q "Firmware:"; then + # Nitrokey Pro / Storage / Librem Key: "Firmware: v0.15" + # hotp_verification prefixes lines with a tab; omit ^ so the pattern matches. + fw_ver="$(echo "$info" | grep "Firmware:" | sed 's/.*: *//')" + # Normalize: ensure fw_ver has 'v' prefix for consistent sort -V comparison. + case "$fw_ver" in v*) ;; *) fw_ver="v$fw_ver" ;; esac + # Flag if below the external-reprogram threshold (cannot upgrade via software) + if [ "$(printf '%s\n' "$fw_ver" "$HOTPKEY_EXTERNAL_REPROGRAM_BELOW" | sort -V | head -1)" != "$HOTPKEY_EXTERNAL_REPROGRAM_BELOW" ]; then + critical="y" + fi + if [ "$branding" = "Librem Key" ]; then + latest_ver="$HOTPKEY_LIBREM_LATEST_VER" + # Check if firmware < v0.11 (requires external programmer/service) + if [ "$(printf '%s\n' "$fw_ver" "$HOTPKEY_EXTERNAL_REPROGRAM_BELOW" | sort -V | head -1)" != "$HOTPKEY_EXTERNAL_REPROGRAM_BELOW" ]; then + NOTE "$branding firmware: ${fw_ver}${extras} (latest known: ${latest_ver}) - firmware below ${HOTPKEY_EXTERNAL_REPROGRAM_BELOW} requires external programming by Purism" + else + NOTE "$branding firmware: ${fw_ver}${extras} (latest known: ${latest_ver}) - Librem Keys cannot be self-upgraded; contact Purism for any future firmware updates" + fi + touch /tmp/hotpkey_fw_shown + return + fi + if [ "$branding" = "Nitrokey Storage" ]; then + latest_ver="$HOTPKEY_STORAGE_LATEST_VER" + if [ -n "$latest_ver" ]; then + STATUS_OK "$branding firmware: ${fw_ver}${extras} (latest known: ${latest_ver})" + else + STATUS_OK "$branding firmware: ${fw_ver}${extras}" + fi + touch /tmp/hotpkey_fw_shown + return + fi + min_ver="$HOTPKEY_NITROKEY_MIN_VER" + latest_ver="$HOTPKEY_NITROKEY_LATEST_VER" + # Update upgrade command for modern nitropy CLI + upgrade_cmd="nitropy nk pro firmware update" + else + return + fi - #TODO: ideally, we ask for confirmation only once per boot session - #TODO: even change logic here to try first and then ask user to confirm if not found - #TODO: or ask GPG User PIN once and cache it for the rest of the boot session for reusal - # This is getting in the way of unattended stuff and GPG prompts are confusing anyway, hide them from user. + # Green: at or above minimum. Yellow: upgrade available. Red: cannot upgrade via software. + if [ "$critical" = "y" ]; then + NOTE "$branding firmware: \033[1;31m${fw_ver}\033[0m${extras} (latest known: ${latest_ver}) - firmware below ${HOTPKEY_EXTERNAL_REPROGRAM_BELOW} cannot be upgraded via nitropy; an external programmer is required" + elif [ "$(printf '%s\n' "$fw_ver" "$min_ver" | sort -V | head -1)" = "$min_ver" ]; then + STATUS_OK "$branding firmware: ${fw_ver}${extras} (minimum: ${min_ver}, latest known: ${latest_ver})" + else + NOTE "$branding firmware: \033[1;33m${fw_ver}\033[0m${extras} (minimum: ${min_ver}, latest known: ${latest_ver}) - upgrade recommended" + fi + touch /tmp/hotpkey_fw_shown +} + +# Release the exclusive CCID device lock so hotp_verification can access the +# dongle. Killing scdaemon alone is insufficient: gpg-agent restarts it +# immediately. Both must be killed; gpg-agent and scdaemon respawn on demand +# for the next GPG operation. +release_scdaemon() { + DEBUG "release_scdaemon: killing gpg-agent and scdaemon to release CCID lock" + killall gpg-agent scdaemon >/dev/null 2>&1 || true +} +cache_gpg_signing_pin() { TRACE_FUNC - #Skip prompts if we are currently using a known GPG key material Thumb drive backup and keys are unlocked pinentry + + # Skip if PIN already cached for this session. + if [ -s /tmp/secret/gpg_pin ]; then + DEBUG "GPG signing PIN already cached for this session; skipping" + return + fi + + #Skip prompts if we are currently using a known GPG key material Thumb drive backup and keys are unlocked #TODO: probably export CONFIG_GPG_KEY_BACKUP_IN_USE but not under /etc/user.config? #Toggle to come in next PR, but currently we don't have a way to toggle it back to n if config.user flashed back in rom if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" && "$CONFIG_GPG_KEY_BACKUP_IN_USE" == "y" ]]; then - DEBUG "Using known GPG key material Thumb drive backup and keys are unlocked and useable through pinentry" + DEBUG "Using known GPG key material Thumb drive backup and keys are unlocked and useable through loopback" return fi + # If GPG key backup is configured, ask whether to use the dongle or the backup + # thumb drive. Use a full-line read (no -n 1) so that buffered single + # keystrokes from previous prompts cannot silently satisfy this read. + local card_confirm="" if [ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" ]; then - message="Please confirm that your GPG card is inserted(Y/n) or your GPG key material (b)backup thumbdrive is inserted [Y/n/b]: " - else - # Generic message if no known key material backup - message="Please confirm that your GPG card is inserted [Y/n]: " - fi - - read -r -n 1 -p $'\n'"$message" card_confirm - echo - - if [ "$card_confirm" != "y" \ - -a "$card_confirm" != "Y" \ - -a "$card_confirm" != "b" \ - -a -n "$card_confirm" ] \ - ; then - die "gpg card not confirmed" + INPUT "Use your GPG security dongle (Enter/y) or backup thumb drive (b)? [Y/b]:" -n 1 -r card_confirm + while [ "$card_confirm" != "y" \ + -a "$card_confirm" != "Y" \ + -a "$card_confirm" != "b" \ + -a -n "$card_confirm" ]; do + INPUT 'Invalid choice. Press Enter for dongle, type b for backup thumb drive, or x to abort:' -n 1 -r card_confirm + if [ "$card_confirm" = "x" ]; then + DIE "gpg card not confirmed" + fi + done + DEBUG "User key source selection: '${card_confirm}' (empty or y/Y = dongle, b = backup thumb drive)" fi + # Non-backup case: skip the upfront confirmation entirely. wait_for_gpg_card + # below does the actual check and prompts on failure. # If user has known GPG key material Thumb drive backup and asked to use it if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" && "$card_confirm" == "b" ]]; then + DEBUG "Backup thumb drive path selected" #Only mount and import GPG key material thumb drive backup once if [ ! "$CONFIG_GPG_KEY_BACKUP_IN_USE" == "y" ]; then - CR_NONCE="/tmp/secret/cr_nonce" - CR_SIG="$CR_NONCE.sig" + DEBUG "Backup key not yet in use this session; proceeding with mount and import" + # Use a distinct path from CR_NONCE so we don't shred the nonce + # that gpg_auth may have already created at /tmp/secret/cr_nonce + # before calling confirm_gpg_card. + local BP_NONCE="/tmp/secret/backup_test_nonce" + local BP_SIG="$BP_NONCE.sig" - #Wipe any previous CR_NONCE and CR_SIG - shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" >/dev/null 2>&1 || true + shred -n 10 -z -u "$BP_NONCE" "$BP_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 gpg_admin_pin="" while [ -z "$gpg_admin_pin" ]; do - read -r -s -p $'\nPlease enter GPG Admin PIN needed to use the GPG backup thumb drive: ' gpg_admin_pin - echo + INPUT "Please enter GPG Admin PIN needed to use the GPG backup thumb drive:" -r -s gpg_admin_pin done - #prompt user to select the proper encrypted partition, which should the first one on next prompt - warn "Please select encrypted LUKS on GPG key material backup thumb drive (not public labeled one)" - mount-usb --pass "$gpg_admin_pin" || die "Unable to mount USB with provided GPG Admin PIN" - echo "++++ Testing detach-sign operation and verifiying against fused public key in ROM" - gpg --pinentry-mode=loopback --passphrase-file <(echo -n "${gpg_admin_pin}") --import /media/subkeys.sec >/dev/null 2>&1 || - die "Unable to import GPG private subkeys" - #Do a detach signature to ensure gpg material is usable and cache passphrase to sign /boot from caller functions - dd if=/dev/urandom of="$CR_NONCE" bs=20 count=1 >/dev/null 2>&1 || - die "Unable to create $CR_NONCE to be detach-signed with GPG private signing subkey" - gpg --pinentry-mode=loopback --passphrase-file <(echo -n "${gpg_admin_pin}") --detach-sign "$CR_NONCE" >/dev/null 2>&1 || - die "Unable to detach-sign $CR_NONCE with GPG private signing subkey using GPG Admin PIN" - #verify detached signature against public key in rom - gpg --verify "$CR_SIG" "$CR_NONCE" >/dev/null 2>&1 && - echo "++++ Local GPG keyring can be used to sign/encrypt/authenticate in this boot session ++++" || - die "Unable to verify $CR_SIG detached signature against public key in ROM" - #Wipe any previous CR_NONCE and CR_SIG - shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" >/dev/null 2>&1 || true + WARN "Please select encrypted LUKS on GPG key material backup thumb drive (not public labeled one)" + mount-usb.sh --pass "$gpg_admin_pin" || DIE "Unable to mount USB with provided GPG Admin PIN" + DEBUG "USB backup thumb drive mounted; clearing card stubs and importing private subkeys" + STATUS "Importing GPG private subkeys from backup thumb drive" + # After keytocard the local keyring has card stubs in + # private-keys-v1.d/. gpg --import returns 0 even when it silently + # skips overwriting an existing stub, so the stub must be removed + # first. Kill agent+scdaemon, delete all stub key files, then + # import so GPG writes actual private key material and the + # subsequent detach-sign uses the local key, not the smartcard. + release_scdaemon + find "${GNUPGHOME:-$HOME/.gnupg}/private-keys-v1.d" \ + -name '*.key' -delete >/dev/null 2>&1 || true + DEBUG "Deleted card stubs from private-keys-v1.d before importing backup key" + gpg --pinentry-mode=loopback --passphrase-file <(echo -n "${gpg_admin_pin}") \ + --import-options restore --import /media/subkeys.sec \ + >/dev/null 2>/tmp/backup-import.log || { + DEBUG "GPG import failed: $(head -5 /tmp/backup-import.log 2>/dev/null)" + DIE "Unable to import GPG private subkeys from backup thumb drive" + } + STATUS_OK "GPG private subkeys imported from backup" + STATUS "Testing detach-sign and verifying against ROM-fused public key" + dd if=/dev/urandom of="$BP_NONCE" bs=20 count=1 >/dev/null 2>&1 || + DIE "Unable to create $BP_NONCE to be detach-signed with GPG private signing subkey" + gpg --pinentry-mode=loopback --passphrase-file <(echo -n "${gpg_admin_pin}") \ + --detach-sign "$BP_NONCE" \ + >/dev/null 2>/tmp/backup-sign.log || { + DEBUG "GPG detach-sign failed: $(head -5 /tmp/backup-sign.log 2>/dev/null)" + DIE "Unable to detach-sign $BP_NONCE with GPG private signing subkey using GPG Admin PIN" + } + DEBUG "Detach-sign succeeded; verifying against ROM-fused public key" + if ! gpg --verify "$BP_SIG" "$BP_NONCE" >/dev/null 2>/tmp/backup-verify.log; then + DEBUG "GPG verify failed: $(head -5 /tmp/backup-verify.log 2>/dev/null)" + DIE "Unable to verify $BP_SIG detached signature against public key in ROM" + fi + STATUS_OK "Local GPG keyring is available for signing, encryption, and authentication this boot session" + printf '%s' "$gpg_admin_pin" >/tmp/secret/gpg_pin + chmod 600 /tmp/secret/gpg_pin + STATUS_OK "GPG Admin PIN cached for this session" + shred -n 10 -z -u "$BP_NONCE" "$BP_SIG" >/dev/null 2>&1 || true #TODO: maybe just an export instead of setting /etc/user.config otherwise could be flashed in weird corner case situation set_user_config "CONFIG_GPG_KEY_BACKUP_IN_USE" "y" - umount /media || die "Unable to unmount USB" + DEBUG "CONFIG_GPG_KEY_BACKUP_IN_USE set; unmounting backup thumb drive" + umount /media || DIE "Unable to unmount USB" return + else + DEBUG "Backup key already in use this session (CONFIG_GPG_KEY_BACKUP_IN_USE=y); skipping mount" fi fi + release_scdaemon + # Clear any private key material left by a previous backup import (or + # stale stubs from a previous smartcard session) so the agent starts + # from a clean state. For the smartcard path the agent re-discovers + # card-resident keys via scdaemon when wait_for_gpg_card runs + # gpg --card-status; fresh stubs are created automatically at that + # point. This mirrors what the backup path does before importing. + find "${GNUPGHOME:-$HOME/.gnupg}/private-keys-v1.d" \ + -name '*.key' -delete >/dev/null 2>&1 || true + DEBUG "Cleared private-keys-v1.d; agent will re-discover keys via scdaemon" + # setup the USB so we can reach the USB Security dongle's OpenPGP smartcard enable_usb # Wait for USB enumeration before accessing GPG card to avoid race condition wait_for_usb_devices - echo -e "\nVerifying presence of GPG card...\n" + STATUS "Verifying presence of GPG card" # ensure we don't exit without retrying errexit=$(set -o | grep errexit | awk '{print $2}') set +e @@ -230,9 +730,7 @@ confirm_gpg_card() { if ! wait_for_gpg_card; then DEBUG "GPG card access failed with output: $gpg_output" # prompt for reinsertion and try a second time - read -n1 -r -p \ - "Can't access GPG key; remove and reinsert, then press Enter to retry. " \ - ignored + INPUT "Can't access GPG key; remove and reinsert, then press Enter to retry." ignored # restore prev errexit state if [ "$errexit" = "on" ]; then set -e @@ -240,23 +738,64 @@ confirm_gpg_card() { # retry card status DEBUG "Retrying gpg --card-status after reinsertion (bounded wait)" wait_for_gpg_card || - die "gpg card read failed" + DIE "gpg card read failed" DEBUG "Retry succeeded" fi - # Extract and display GPG PIN retry counters + # Read card status and display PIN retry counters before prompting. # 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}') - echo "" - echo "GPG User PIN retry attempts left before becoming locked: $user_pin_retries" - echo "GPG Admin PIN retry attempts left before becoming locked: $admin_pin_retries" - echo "" - NOTE "Your GPG User PIN, followed by Enter key will be required for input at: 'Please unlock the card' next prompt" - echo "" + echo >/dev/console 2>/dev/null + STATUS "GPG User PIN retries remaining: $(pin_color "$user_pin_retries")${user_pin_retries}\033[0m" + STATUS "GPG Admin PIN retries remaining: $(pin_color "$admin_pin_retries")${admin_pin_retries}\033[0m" + + # Collect and validate smartcard User PIN via Heads INPUT then loopback + # test-sign. On success, cache the PIN so all subsequent signing calls in + # this session use --pinentry-mode=loopback --passphrase-file without + # prompting the user again. + SC_NONCE="/tmp/secret/sc_nonce" + SC_SIG="$SC_NONCE.sig" + shred -n 10 -z -u "$SC_NONCE" "$SC_SIG" >/dev/null 2>&1 || true + dd if=/dev/urandom of="$SC_NONCE" bs=20 count=1 >/dev/null 2>&1 || + DIE "Unable to create nonce for smartcard PIN test-sign" + STATUS "Testing GPG smartcard signing to cache User PIN for this session" + sc_user_pin="" + sc_pin_tries=0 + while [ "$sc_pin_tries" -lt 3 ]; do + sc_pin_tries=$((sc_pin_tries + 1)) + while [ -z "$sc_user_pin" ]; do + INPUT "Enter GPG User PIN for smartcard:" -r -s sc_user_pin + done + if gpg --pinentry-mode=loopback \ + --passphrase-file <(printf '%s' "$sc_user_pin") \ + --detach-sign "$SC_NONCE" >/dev/null 2>/tmp/sc-sign.log; then + gpg --verify "$SC_SIG" "$SC_NONCE" >/dev/null 2>&1 || + DIE "GPG smartcard test-sign: signature verification failed" + printf '%s' "$sc_user_pin" >/tmp/secret/gpg_pin + chmod 600 /tmp/secret/gpg_pin + STATUS_OK "GPG User PIN cached for this session" + break + fi + sc_user_pin="" + if grep -Eiq 'bad pin|wrong pin|incorrect pin|pin incorrect|pinentry.*cancel' /tmp/sc-sign.log 2>/dev/null; then + if [ "$sc_pin_tries" -lt 3 ]; then + WARN "Incorrect GPG User PIN (attempt $sc_pin_tries/3) - please retry" + # Re-read counter to show updated remaining retries after the failed attempt + 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}') + STATUS "GPG User PIN retries remaining: $(pin_color "$user_pin_retries")${user_pin_retries}\033[0m" + continue + fi + DIE "Incorrect GPG User PIN after 3 attempts. Check remaining PIN retries." + fi + DIE "GPG smartcard test-sign failed: $(head -5 /tmp/sc-sign.log 2>/dev/null)" + done + shred -n 10 -z -u "$SC_NONCE" "$SC_SIG" >/dev/null 2>&1 || true # restore prev errexit state if [ "$errexit" = "on" ]; then @@ -264,22 +803,19 @@ confirm_gpg_card() { fi } +confirm_gpg_card() { + cache_gpg_signing_pin "$@" +} + gpg_auth() { if [[ "$CONFIG_HAVE_GPG_KEY_BACKUP" == "y" ]]; then TRACE_FUNC # If we have a GPG key backup, we can use it to authenticate even if the card is lost - echo >&2 "!!!!! Please authenticate with OpenPGP smartcard/backup media to prove you are the owner of this machine !!!!!" + NOTE "Please authenticate with OpenPGP smartcard/backup media to prove you are the owner of this machine" # Wipe any existing nonce and signature shred -n 10 -z -u "$CR_NONCE" "$CR_SIG" 2>/dev/null || true - # In case of gpg_auth, we require confirmation of the card, so loop with confirm_gpg_card until we get it - false - while [ $? -ne 0 ]; do - # Call confirm_gpg_card in subshell to ensure GPG key material presence - (confirm_gpg_card) - done - # Perform a signing-based challenge-response, # to authencate that the card plugged in holding # the key to sign the list of boot files. @@ -294,11 +830,15 @@ gpg_auth() { count=1 \ bs=20 \ 2>/dev/null || - die "Unable to generate 20 random bytes" + DIE "Unable to generate 20 random bytes" - # Sign the nonce + # Sign the nonce; cache_gpg_signing_pin ensures the PIN is cached and + # the card is accessible before each attempt. for tries in 1 2 3; do + until (confirm_gpg_card); do true; done if gpg --digest-algo SHA256 \ + --pinentry-mode=loopback \ + --passphrase-file /tmp/secret/gpg_pin \ --detach-sign \ -o "$CR_SIG" \ "$CR_NONCE" >/dev/null 2>&1 && @@ -310,10 +850,12 @@ gpg_auth() { else shred -n 10 -z -u "$CR_SIG" 2>/dev/null || true if [ "$tries" -lt 3 ]; then - echo >&2 "!!!!! GPG authentication failed, please try again !!!!!" + WARN "GPG authentication failed (attempt $tries/3), please try again" + # Clear cached PIN so the next attempt re-prompts for the correct PIN. + rm -f /tmp/secret/gpg_pin continue else - die "GPG authentication failed, please reboot and try again" + DIE "GPG authentication failed, please reboot and try again" fi fi done @@ -323,10 +865,7 @@ gpg_auth() { recovery() { TRACE_FUNC - echo >&2 "!!!!! $*" - - # Remove any temporary secret files that might be hanging around - # but recreate the directory so that new tools can use it. + # Remove any temporary secret files, but recreate the directory so that new tools can use it. #safe to always be true. Otherwise "set -e" would make it exit here shred -n 10 -z -u /tmp/secret/* 2>/dev/null || true @@ -348,9 +887,9 @@ recovery() { fi if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then - echo >&2 "Restricted Boot enabled, recovery console disabled, rebooting in 5 seconds" + NOTE "Restricted Boot enabled, recovery console disabled, rebooting in 5 seconds" sleep 5 - /bin/reboot + /bin/reboot.sh fi while [ true ]; do #Going to recovery shell should be authenticated if supported @@ -363,12 +902,22 @@ recovery() { #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'" + NOTE "To file a bug report with debug logs:\n 1. Options --> Change configuration settings --> Configure $CONFIG_BRAND_NAME informational / debug output --> select Debug, save and flash firmware changes\n 2. After reboot: Options --> TPM/TOTP/HOTP Options --> Generate new TOTP/HOTP secret to reseal secrets" fi - echo >&2 "!!!!! Starting recovery shell" - - if [ -x /bin/setsid ]; then + # display any custom recovery message just before the banner + if [ -n "$*" ]; then + WARN "$*" + fi + STATUS "Starting recovery shell" + + if [ -n "$RECOVERY_TTY" ]; then + # Reopen the serial TTY on each iteration so the new session + # leader acquires it as its controlling terminal automatically + # (POSIX: opening a TTY as session leader without O_NOCTTY sets + # it as the controlling terminal). setsid -c with an inherited + # fd fails to respawn correctly after the first bash exits. + setsid /bin/bash <>"$RECOVERY_TTY" >&0 2>&0 + elif [ -x /bin/setsid ]; then /bin/setsid -c /bin/bash else /bin/bash @@ -378,7 +927,7 @@ recovery() { pause_recovery() { TRACE_FUNC - read -p $'!!! Hit enter to proceed to recovery shell !!!\n' + INPUT "Press Enter to proceed to recovery shell" recovery $* } @@ -440,17 +989,19 @@ load_config_value() { enable_usb() { TRACE_FUNC - #insmod ehci_hcd prior of uhdc_hcd and ohci_hcd to suppress dmesg warning - insmod /lib/modules/ehci-hcd.ko || die "ehci_hcd: module load failed" + [ "${_USB_ENABLED:-n}" = "y" ] && return + #insmod.sh ehci_hcd prior of uhdc_hcd and ohci_hcd to suppress dmesg warning + insmod.sh /lib/modules/ehci-hcd.ko || DIE "ehci_hcd: module load failed" if [ "$CONFIG_LINUX_USB_COMPANION_CONTROLLER" = y ]; then - insmod /lib/modules/uhci-hcd.ko || die "uhci_hcd: module load failed" - insmod /lib/modules/ohci-hcd.ko || die "ohci_hcd: module load failed" - insmod /lib/modules/ohci-pci.ko || die "ohci_pci: module load failed" + insmod.sh /lib/modules/uhci-hcd.ko || DIE "uhci_hcd: module load failed" + insmod.sh /lib/modules/ohci-hcd.ko || DIE "ohci_hcd: module load failed" + insmod.sh /lib/modules/ohci-pci.ko || DIE "ohci_pci: module load failed" fi - 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" + insmod.sh /lib/modules/ehci-pci.ko || DIE "ehci_pci: module load failed" + insmod.sh /lib/modules/xhci-hcd.ko || DIE "xhci_hcd: module load failed" + insmod.sh /lib/modules/xhci-pci.ko || DIE "xhci_pci: module load failed" + _USB_ENABLED="y" } # Wait for USB bus enumeration to complete after enable_usb() loads modules. @@ -471,7 +1022,7 @@ wait_for_usb_devices() { local iteration=0 while :; do iteration=$((iteration + 1)) - + # Check for actual USB peripheral devices (format: bus-port like 1-1, 5-3) # Root hubs are named usb1, usb2, etc. - we want devices downstream from them # Pattern: /sys/bus/usb/devices/[0-9]*-[0-9]*/idVendor (e.g., 1-1, 5-3.2) @@ -484,15 +1035,15 @@ wait_for_usb_devices() { fi done fi - + now=$(awk '{print $1}' /proc/uptime) elapsed=$(awk -v s="$start" -v n="$now" 'BEGIN{printf "%.3f", n - s}') - + if [ $peripheral_count -gt 0 ]; then DEBUG "USB peripheral devices ready after ${elapsed}s (iteration $iteration): found $peripheral_count device(s)" return fi - + # Timeout after 2 seconds if awk -v s="$start" -v n="$now" 'BEGIN{exit (n - s > 2.0) ? 0 : 1}'; then DEBUG "USB wait timeout at ${elapsed}s (iter $iteration): only found $peripheral_count peripheral device(s)" @@ -507,7 +1058,9 @@ wait_for_gpg_card() { TRACE_FUNC if [ ! -r /proc/uptime ]; then gpg_output=$(gpg --card-status 2>&1) - return $? + local rc=$? + [ $rc -eq 0 ] && release_scdaemon + return $rc fi local start now elapsed @@ -520,6 +1073,14 @@ wait_for_gpg_card() { now=$(awk '{print $1}' /proc/uptime) elapsed=$(awk -v s="$start" -v n="$now" 'BEGIN{printf "%.3f", n - s}') DEBUG "gpg --card-status succeeded after ${elapsed}s (attempt $attempt)" + # Card output captured; release scdaemon now so the NK3's CCID + # session teardown begins immediately. hotp_verification needs + # the same CCID interface and cannot open a session until the + # previous one is fully closed (~3s on NK3 firmware). Releasing + # here gives the device time to recover while the caller does its + # own processing (e.g. user reads a dialog) before calling + # hotp_verification. + release_scdaemon return 0 fi @@ -541,7 +1102,7 @@ enable_usb_keyboard() { # desktop/server), they could lock themselves out, only recoverable by # hardware flash. if [ "$CONFIG_USB_KEYBOARD_REQUIRED" = y ] || [ "$CONFIG_USER_USB_KEYBOARD" = y ]; then - insmod /lib/modules/usbhid.ko || die "usbhid: module load failed" + insmod.sh /lib/modules/usbhid.ko || DIE "usbhid: module load failed" fi } @@ -614,7 +1175,7 @@ SINK_LOG() { # ^-- adding DO_WITH_DEBUG will show the block device, mountpoint, and whether # the mount fails # -# [DO_WITH_DEBUG --mask-position 7] tpmr seal "$KEY" "$IDX" "$pcrs" "$pcrf" "$size" "$PASSWORD" +# [DO_WITH_DEBUG --mask-position 7] tpmr.sh seal "$KEY" "$IDX" "$pcrs" "$pcrf" "$size" "$PASSWORD" # ^-- trace the resulting invocation, but mask the password in the log # # if ! [DO_WITH_DEBUG] umount "$MOUNTPOINT"; then [...] @@ -724,6 +1285,76 @@ pcrs() { fi } +# Marker helpers for TPM state that requires reset before reseal/generate paths. +tpm_reset_required_marker_path() { + printf %s "/tmp/secret/tpm_reset_required" +} + +tpm_reset_required_reason_path() { + printf %s "/tmp/secret/tpm_reset_required.reason" +} + +tpm_reset_required_source_path() { + printf %s "/tmp/secret/tpm_reset_required.source" +} + +tpm_reset_required_timestamp_path() { + printf %s "/tmp/secret/tpm_reset_required.timestamp" +} + +debug_tpm_reset_required_state() { + TRACE_FUNC + local marker reason source when + marker="$(tpm_reset_required_marker_path)" + reason="$(tpm_reset_required_reason_path)" + source="$(tpm_reset_required_source_path)" + when="$(tpm_reset_required_timestamp_path)" + + if [ -f "$marker" ]; then + DEBUG "TPM reset marker: PRESENT path=$marker" + DEBUG "TPM reset marker: reason=$(cat "$reason" 2>/dev/null || echo '')" + DEBUG "TPM reset marker: source=$(cat "$source" 2>/dev/null || echo '')" + DEBUG "TPM reset marker: timestamp=$(cat "$when" 2>/dev/null || echo '')" + else + DEBUG "TPM reset marker: ABSENT path=$marker" + fi +} + +set_tpm_reset_required() { + TRACE_FUNC + local reason source + reason="${1:-TPM state marked invalid by unknown caller}" + source="${2:-unknown}" + mkdir -p /tmp/secret || true + echo "$reason" >"$(tpm_reset_required_reason_path)" 2>/dev/null || true + echo "$source" >"$(tpm_reset_required_source_path)" 2>/dev/null || true + date -u "+%Y-%m-%d %H:%M:%S UTC" >"$(tpm_reset_required_timestamp_path)" 2>/dev/null || true + : >"$(tpm_reset_required_marker_path)" + WARN "TPM reset required: $reason" +} + +clear_tpm_reset_required() { + TRACE_FUNC + rm -f "$(tpm_reset_required_marker_path)" + rm -f "$(tpm_reset_required_reason_path)" + rm -f "$(tpm_reset_required_source_path)" + rm -f "$(tpm_reset_required_timestamp_path)" + STATUS_OK "TPM reset-required marker cleared" +} + +tpm_reset_required() { + TRACE_FUNC + local marker + marker="$(tpm_reset_required_marker_path)" + if [ -f "$marker" ]; then + DEBUG "tpm_reset_required: yes" + debug_tpm_reset_required_state + return 0 + fi + DEBUG "tpm_reset_required: no" + return 1 +} + confirm_totp() { TRACE_FUNC prompt="$1" @@ -740,7 +1371,7 @@ confirm_totp() { TOTP="NO TPM" elif [ "$half" != "$last_half" ]; then last_half=$half - TOTP=$(unseal-totp) || + TOTP=$(unseal-totp.sh) || recovery "TOTP code generation failed" fi @@ -765,6 +1396,24 @@ confirm_totp() { reseal_tpm_disk_decryption_key() { TRACE_FUNC + local GPG_KEY_COUNT + if tpm_reset_required; then + WARN "Cannot reseal TPM disk decryption key while TPM state is marked invalid. Reset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + # Resealing disk-unlock material eventually requires signing /boot updates; + # do not proceed if keyring is empty. + GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) + if [ "$GPG_KEY_COUNT" -eq 0 ]; then + DEBUG "Skipping TPM disk-key reseal: GPG keyring is empty (caller handles user guidance)" + return 1 + fi + + # only relevant for TPM2; TPM1 has no primary handle concept + if [ "$CONFIG_TPM2_TOOLS" = "y" ] && [ ! -f "/tmp/secret/primary.handle" ]; then + WARN "Cannot reseal TPM disk decryption key; no TPM primary handle. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) to reset the TPM first." + return 1 + fi #For robustness, exit early if LUKS TPM Disk Unlock Key is prohibited in board configs if [ "$CONFIG_TPM_DISK_UNLOCK_KEY" == "n" ]; then DEBUG "LUKS TPM Disk Unlock Key is prohibited in board configs" @@ -779,24 +1428,25 @@ reseal_tpm_disk_decryption_key() { fi if [ -s /boot/kexec_key_devices.txt ] || [ -s /boot/kexec_key_lvm.txt ]; then - NOTE "LUKS TPM sealed Disk Unlock Key secret needs to be resealed alongside TOTP/HOTP secret" - echo "Resealing LUKS TPM Disk Unlock Key to be unsealed by LUKS TPM Disk Unlock Key passphrase" - while ! kexec-seal-key /boot; do - warn "Recovery Disk Encryption key passphrase/TPM Owner Password may be invalid. Please try again" - done - NOTE "LUKS header hash changed under /boot/kexec_luks_hdr_hash.txt" - echo "Updating checksums and signing all files under /boot/kexec.sig" + STATUS "Validating TPM rollback counter before resealing" + preflight_rollback_counter_before_reseal + STATUS_OK "TPM rollback counter validated" + STATUS "Resealing TPM Disk Unlock Key alongside TOTP/HOTP secret" + if ! kexec-seal-key.sh /boot; then + DIE "Failed to reseal TPM Disk Unlock Key" + fi attempt=1 while ! update_checksums; do - warn "Attempt $attempt: Checksums were not signed. Preceding errors should explain possible causes" + WARN "Signing attempt $attempt/3 failed" if [ "$attempt" -ge 3 ]; then - die "Failed to sign checksums after 3 attempts" + DIE "Failed to sign boot hashes under /boot after 3 attempts" fi attempt=$((attempt + 1)) done - NOTE "Rebooting in 3 seconds to enable booting default boot option" + STATUS_OK "TPM Disk Unlock Key resealed and boot hashes signed" + STATUS "Rebooting to enable default boot option" sleep 3 - reboot + reboot.sh else DEBUG "No TPM disk decryption key to reseal" fi @@ -809,9 +1459,9 @@ enable_usb_storage() { TRACE_FUNC if ! lsmod | grep -q usb_storage; then timeout=0 - echo "Scanning for USB storage devices..." - insmod /lib/modules/usb-storage.ko >/dev/null 2>&1 || - die "usb_storage: module load failed" + STATUS "Scanning for USB storage devices" + insmod.sh /lib/modules/usb-storage.ko >/dev/null 2>&1 || + DIE "usb_storage: module load failed" while [[ $(list_usb_storage | wc -l) -eq 0 ]]; do [[ $timeout -ge 8 ]] && break sleep 1 @@ -911,7 +1561,7 @@ list_usb_storage() { # include the partitions instead - even if the kernel # hasn't detected the partitions yet. Such a device is # never usable directly, and this allows the "wait for - # disks" loop in mount-usb to correctly wait for the + # disks" loop in mount-usb.sh to correctly wait for the # partitions. if ! device_has_partitions "$b"; then # No partition table, include this device @@ -944,13 +1594,12 @@ prompt_tpm_owner_password() { return 0 fi - read -r -s -p $'\nTPM Owner Password: ' tpm_owner_password - echo + INPUT "TPM Owner Password:" -r -s tpm_owner_password # 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 owner_password under /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 owner_password under /tmp/secret/tpm_owner_password" } # Prompt for a new TPM Owner Password when resetting the TPM. @@ -963,20 +1612,17 @@ 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 -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 - + INPUT "New TPM Owner Password (2 words suggested, 1-32 characters max):" -r -s tpm_owner_password + INPUT "Repeat chosen TPM Owner Password:" -r -s tpm_owner_password2 if [ "$tpm_owner_password" != "$tpm_owner_password2" ]; then - echo - echo "Passphrases entered do not match. Try again!" + WARN "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/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/tpm_owner_password" } check_tpm_counter() { @@ -992,23 +1638,152 @@ check_tpm_counter() { 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" + DEBUG "$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" + WARN "TPM Owner Password is required to create a new TPM counter for /boot content rollback prevention" fi + # attempt to make a new counter, capturing any stderr for debugging + DEBUG "Invoking tpmr.sh counter_create with label $LABEL" + # run it, then record the exit status explicitly; the '!' operator + # cannot be used because it would hide the real return code. tpmr.sh counter_create \ - -pwdc '' \ - -la $LABEL | - tee /tmp/counter >/dev/null 2>&1 || - die "Unable to create TPM counter" + -pwdc "${tpm_password:-}" \ + -la $LABEL \ + >/tmp/counter 2> >(tee >(SINK_LOG "tpm counter_create stderr") >&2) + local rc=$? + if [ $rc -ne 0 ]; then + DEBUG "tpmr.sh counter_create failed with status $rc" + # don't tell the user to reset again; the TPM was just reset + DIE "Unable to create TPM counter; TPM appears to be in a bad state. Perform OEM Factory Reset / re-ownership and try again." + fi TPM_COUNTER=$(cut -d: -f1 /dev/null 2>&1; then + return 0 + fi + fi + return 1 +} + +# Validate rollback counter state before expensive operations. +# This is a non-mutating preflight intended to fail early when the configured +# rollback counter is clearly unusable. +# Parameters: +# $1 optional rollback file path (default: /boot/kexec_rollback.txt) +# $2 optional explicit counter id override +# $3 optional on-error mode: 'DIE' (default) or 'return' +preflight_rollback_counter_before_reseal() { + TRACE_FUNC + local rollback_file counter_id attrs_lc on_error error_file + rollback_file="${1:-/boot/kexec_rollback.txt}" + counter_id="$2" + on_error="${3:-DIE}" + local reset_required_marker="/tmp/secret/rollback_reset_required" + error_file="/tmp/rollback_preflight_error" + + fail_preflight() { + local message="$1" + mkdir -p /tmp/secret || true + : >"$reset_required_marker" + set_tpm_reset_required "$message" "preflight_rollback_counter_before_reseal" + if [ "$on_error" = "return" ]; then + echo "$message" >"$error_file" + return 1 + fi + DIE "$message" + } + + if [ "$CONFIG_TPM" != "y" ] || [ "$CONFIG_IGNORE_ROLLBACK" = "y" ]; then + DEBUG "Skipping rollback counter preflight: rollback checks are disabled" + return 0 + fi + + if [ -z "$counter_id" ]; then + counter_id="$(get_rollback_counter_id "$rollback_file")" + fi + if [ -z "$counter_id" ]; then + # If rollback metadata is missing on an already initialized system, + # this is an inconsistent TPM/boot state and should be handled before + # TOTP/HOTP recovery workflows. + if has_prior_boot_trust_metadata "$rollback_file"; then + fail_preflight "Boot integrity counter file missing. This means /boot was restored or swapped. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + DEBUG "Skipping rollback counter preflight: no counter id in $rollback_file (likely first-time initialization)" + return 0 + fi + + DEBUG "Preflight: validating rollback counter $counter_id before protected operations" + if ! tpmr.sh counter_read -ix "$counter_id" >/dev/null 2>&1; then + fail_preflight "TPM integrity counter cannot be read. Possible cause: TPM was swapped or reset. This could indicate a TPM swap attack. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + if attrs_lc="$(tpm2 nvreadpublic "0x$counter_id" 2>/dev/null | tr '[:upper:]' '[:lower:]')"; then + if [ -n "$attrs_lc" ]; then + if echo "$attrs_lc" | grep -q "ownerwrite" && ! echo "$attrs_lc" | grep -q "authwrite"; then + fail_preflight "TPM counter has invalid security policy. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + if ! echo "$attrs_lc" | grep -Eq "authwrite|ownerwrite"; then + fail_preflight "TPM counter is not writable. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + else + fail_preflight "TPM counter policy is corrupted. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + else + fail_preflight "Cannot read TPM counter policy. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + return 1 + fi + fi + + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + DEBUG "Preflight: rollback counter $counter_id is readable and has acceptable TPM2 write attributes" + else + DEBUG "Preflight: rollback counter $counter_id is readable on TPM1" + DEBUG "Preflight: post OEM Factory Reset / Re-Ownership, TOTP unseal may be unavailable until a new TOTP/HOTP secret is generated" fi } @@ -1019,75 +1794,176 @@ read_tpm_counter() { 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.sh counter_read -ix "$counter_id" | tee /tmp/counter-"$counter_id" >/dev/null 2>&1 || - die "Counter read failed for index $counter_id" + tpmr.sh counter_read -ix "$counter_id" >/tmp/counter-"$counter_id" || + DIE "Counter read failed for index $counter_id" fi DEBUG "Counter file /tmp/counter-$counter_id read successfully." } increment_tpm_counter() { TRACE_FUNC - local counter_id + local counter_id counter_present tpm_password increment_ok counter_id="$(echo "$1" | tr -d '\n')" - + tpm_password="$2" + counter_present="n" + increment_ok="n" + local reset_required_marker="/tmp/secret/rollback_reset_required" + + # Prefer explicit password, otherwise reuse cached TPM owner password. + if [ -z "$tpm_password" ] && [ -s /tmp/secret/tpm_owner_password ]; then + tpm_password="$(cat /tmp/secret/tpm_owner_password)" + DEBUG "increment_tpm_counter: using cached TPM owner password" + fi + + # TPM1 counter_increment requires owner auth in practice on this path. + # origin/master typically reached this with cached owner password already set, + # but the newer reseal/update flows can call this later in the session after + # that cache is absent. Prompt once and cache to avoid empty -pwdc failures. + if [ "$CONFIG_TPM2_TOOLS" != "y" ] && [ -z "$tpm_password" ]; then + WARN "TPM Owner Password is required to update rollback counter before signing updated boot hashes." + DEBUG "increment_tpm_counter: TPM1 path has no cached/provided owner password; prompting now" + prompt_tpm_owner_password + tpm_password="$tpm_owner_password" + DEBUG "increment_tpm_counter: TPM1 owner password obtained and cached" + fi + # Check if counter exists by reading it first + DEBUG "reading TPM counter $counter_id" if ! DO_WITH_DEBUG tpmr.sh 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" + counter_present="y" + fi + + # Try to increment the counter. We normally hide the verbose + # output of tpmr.sh commands to avoid overwhelming the console, but we + # must *not* swallow any interactive prompts. The previous implementation + # redirected the entire `tpmr.sh counter_create` invocation to a file and + # /dev/null, which meant that when the counter was missing the password + # prompt could not be seen by the user even though tpmr.sh printed it to the + # controlling terminal. Instead, capture just the stdout in a temporary + # file while still letting stdout appear on the console (and logging + # stderr to debug log). + DEBUG "incrementing TPM counter $counter_id" + + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + # TPM2 counters created with authwrite commonly require index auth (often + # empty auth) for nvincrement. Try that first, then owner auth fallback. + DEBUG "increment_tpm_counter: TPM2 trying index-auth nvincrement first" + if ( + set -o pipefail + DO_WITH_DEBUG --mask-position 5 \ + tpmr.sh counter_increment -ix "$counter_id" -pwdc "" \ + 2> >(SINK_LOG "tpm counter_increment stderr") | + tee /tmp/counter-"$counter_id" >/dev/null + ); then + increment_ok="y" + elif [ -n "$tpm_password" ]; then + DEBUG "increment_tpm_counter: TPM2 index-auth increment failed; trying owner-auth fallback" + if ( + set -o pipefail + DO_WITH_DEBUG --mask-position 5 \ + tpmr.sh counter_increment -ix "$counter_id" -pwdc "${tpm_password}" \ + 2> >(SINK_LOG "tpm counter_increment stderr") | + tee /tmp/counter-"$counter_id" >/dev/null + ); then + increment_ok="y" + fi + fi + else + # TPM1 path uses owner auth in practice. + if ( + set -o pipefail + DO_WITH_DEBUG --mask-position 5 \ + tpmr.sh counter_increment -ix "$counter_id" -pwdc "${tpm_password:-}" \ + 2> >(SINK_LOG "tpm counter_increment stderr") | + tee /tmp/counter-"$counter_id" >/dev/null + ); then + increment_ok="y" + fi fi - # Try to increment the counter - if ! DO_WITH_DEBUG tpmr.sh counter_increment -ix "$counter_id" -pwdc '' | - tee /tmp/counter-"$counter_id" >/dev/null 2>&1; then - + if [ "$increment_ok" != "y" ]; then + if [ "$counter_present" = "y" ]; then + mkdir -p /tmp/secret || true + : >"$reset_required_marker" + DIE "TPM rollback counter '$counter_id' is readable but not incrementable. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + fi + # 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.sh counter_create -pwdc '' -la 3135106223 >/tmp/new-counter 2>/dev/null; then + + # run counter_create but tee its stdout to a file so we still see + # the interactive prompt and any informational messages. + if ( + set -o pipefail + DO_WITH_DEBUG --mask-position 3 \ + tpmr.sh counter_create -pwdc "${tpm_password:-}" -la 3135106223 \ + 2> >(tee >(SINK_LOG "tpm counter_create stderr") >&2) | + tee /tmp/new-counter >/dev/null + ); then NEW_COUNTER=$(cut -d: -f1 TPM/TOTP/HOTP Options -> Reset the TPM) to clear the counter and allow a fresh one to be created." fi - + DEBUG "TPM counter incremented successfully for index $counter_id" } # Check detached signature on kexec boot params check_config() { TRACE_FUNC + local paramsdir="${1%%/}" + if [ ! -d /tmp/kexec ]; then mkdir /tmp/kexec || - die 'Failed to make kexec tmp dir' + DIE 'Failed to make kexec tmp dir' else rm -rf /tmp/kexec/* || - die 'Failed to empty kexec tmp dir' + DIE 'Failed to empty kexec tmp dir' fi - if [ ! -r $1/kexec.sig -a "$CONFIG_BASIC" != "y" ]; then - DEBUG "No $1/kexec.sig found" + DEBUG "check_config: checking $paramsdir (force=$2)" + + if [ ! -r "$paramsdir/kexec.sig" -a "$CONFIG_BASIC" != "y" ]; then + DEBUG "check_config: no $paramsdir/kexec.sig found, skipping signature check" return fi - if [ $(find $1/kexec*.txt | wc -l) -eq 0 ]; then - DEBUG "No $1/kexec*.txt found" + # Collect kexec*.txt files present in paramsdir + local param_files=() + for f in "$paramsdir"/kexec*.txt; do + [ -e "$f" ] || continue + param_files+=("$(basename "$f")") + done + DEBUG "check_config: ${#param_files[@]} kexec*.txt file(s) in $paramsdir: ${param_files[*]}" + + if [ ${#param_files[@]} -eq 0 ]; then + DEBUG "check_config: no kexec*.txt files found in $paramsdir, skipping" 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' + # Verify using relative filenames (cd into paramsdir) so the sha256sum + # output matches exactly what was produced during signing, where the same + # relative names were used. Absolute paths would differ between the + # signing staging dir and $paramsdir, causing a spurious mismatch. + STATUS "Verifying GPG signature on kexec boot params" + DEBUG "check_config: running (cd $paramsdir && sha256sum ${param_files[*]}) | gpgv.sh $paramsdir/kexec.sig" + if ! (cd "$paramsdir" && sha256sum "${param_files[@]}") | + gpgv.sh "$paramsdir/kexec.sig" - 2> >(SINK_LOG "gpgv kexec.sig"); then + DIE 'Invalid signature on kexec boot params' fi fi - INFO "+++ Found verified kexec boot params" - cp $1/kexec*.txt /tmp/kexec || - die "Failed to copy kexec boot params to tmp" + STATUS_OK "GPG signature on kexec boot params verified" + DEBUG "check_config: copying kexec*.txt from $paramsdir to /tmp/kexec" + cp "$paramsdir"/kexec*.txt /tmp/kexec || + DIE "Failed to copy kexec boot params to tmp" } # Replace a file in a ROM (add it if the file does not exist) @@ -1126,7 +2002,7 @@ replace_config() { secret_from_rom_hash() { local ROM_IMAGE="/tmp/coreboot-notpm.rom" - echo -e "\nTPM not detected; measuring ROM directly\n" 1>&2 + INFO "TPM not detected; measuring ROM directly" # Read the ROM if we haven't read it yet if [ ! -f "${ROM_IMAGE}" ]; then @@ -1136,9 +2012,38 @@ secret_from_rom_hash() { sha256sum "${ROM_IMAGE}" | cut -f1 -d ' ' | fromhex_plain } +# Refresh /boot hash of the TPM2 primary handle when available. +# This prevents a follow-up prompt to "set default boot" solely to rebuild +# kexec_primhdl_hash.txt after TPM reset/reseal flows. +refresh_tpm2_primary_handle_hash() { + TRACE_FUNC + local primhash_file="${1:-/boot/kexec_primhdl_hash.txt}" + + if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then + DEBUG "Skipping TPM2 primary handle hash refresh: CONFIG_TPM2_TOOLS != y" + return 0 + fi + + if [ ! -s /tmp/secret/primary.handle ]; then + DEBUG "Skipping TPM2 primary handle hash refresh: /tmp/secret/primary.handle not available" + return 0 + fi + + DEBUG "Refreshing TPM2 primary key handle hash into $primhash_file" + if ! DO_WITH_DEBUG sha256sum /tmp/secret/primary.handle >"$primhash_file"; then + WARN "Failed to refresh TPM2 primary key handle hash at $primhash_file" + return 1 + fi + + DEBUG "TPM2 primary key handle hash saved to $primhash_file" + return 0 +} + # Update the checksums of the files in /boot and sign them update_checksums() { TRACE_FUNC + local reset_required_marker="/tmp/secret/rollback_reset_required" + local signing_targets # ensure /boot mounted if ! grep -q /boot /proc/mounts; then mount -o ro /boot || @@ -1152,11 +2057,34 @@ 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" + DEBUG "add -r to kexec-sign-config.sh since CONFIG_IGNORE_ROLLBACK is not set" extparam=-r fi fi - if ! DO_WITH_DEBUG kexec-sign-config -p /boot -u $extparam; then + + # Keep this best-effort and run it before signing while /boot is RW. + # Running after kexec-sign-config.sh can fail because that path may remount + # /boot read-only before returning. + if ! refresh_tpm2_primary_handle_hash; then + WARN "Proceeding without refreshed TPM2 primary key handle hash" + fi + + signing_targets="$(find /boot/kexec*.txt 2>/dev/null | tr '\n' ' ')" + DEBUG "update_checksums: signing targets under /boot: ${signing_targets:-}" + DEBUG "update_checksums: rollback marker path is $reset_required_marker" + DEBUG "update_checksums: extparam='$extparam' CONFIG_TPM='${CONFIG_TPM:-}' CONFIG_IGNORE_ROLLBACK='${CONFIG_IGNORE_ROLLBACK:-}'" + DEBUG "update_checksums: signing is required because boot hashes under /boot changed (rollback counter and/or resealed secrets) and must be re-trusted" + + STATUS "Signing $CONFIG_BRAND_NAME boot hashes under /boot" + + # signing may prompt for TPM password; avoid DO_WITH_DEBUG which + # severs the controlling tty for the child process. + DEBUG "running kexec-sign-config.sh -p /boot -u $extparam" + rm -f "$reset_required_marker" + if ! kexec-sign-config.sh -p /boot -u $extparam; then + if [ -e "$reset_required_marker" ]; then + DIE "TPM rollback counter state is invalid for secure rollback protection. Reset TPM from GUI (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." + fi rv=1 else rv=0 @@ -1187,7 +2115,7 @@ escape_zero() { local echar="${2:-#}" local todo="" local echar_hex="$(echo -n "$echar" | xxd -p -c1)" - [ ${#echar_hex} -eq 2 ] || die "Invalid escape character $echar passed to escape_zero(). Programming error?!" + [ ${#echar_hex} -eq 2 ] || DIE "Invalid escape character $echar passed to escape_zero(). Programming error?!" echo -e -n "$prefix" xxd -p -c1 | tr -d '\n' | @@ -1244,7 +2172,7 @@ assert_signable() { find /boot -print0 >/tmp/signable.ref local del='\001-\037\134\177-\377' - LC_ALL=C tr -d "$del" /tmp/signable.del || die "Failed to execute tr." + LC_ALL=C tr -d "$del" /tmp/signable.del || DIE "Failed to execute tr." if ! cmp -s "/tmp/signable.ref" "/tmp/signable.del" &>/dev/null; then local user_out="/tmp/hash_output_mismatches" local add="Please investigate!" @@ -1309,9 +2237,9 @@ find_lvm_vg_name() { mkdir -p /tmp/root-hashes-gui # Try to query whether DEVICE is an LVM physical volume. On systems # without LVM the command may not exist; treat that like "not a PV". - if ! lvm pvs --noheadings -o vg_name "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/tmp/root-hashes-gui/lvm_err; then - # It's not an LVM PV, or lvm failed entirely. Log stderr for debugging. - DEBUG "lvm pvs failed for $DEVICE, stderr:" "$(cat /tmp/root-hashes-gui/lvm_err)" + if ! run_lvm pvs --noheadings -o vg_name "$DEVICE" >/tmp/root-hashes-gui/lvm_vg; then + # It's not an LVM PV, or lvm failed entirely. + DEBUG "lvm pvs failed for $DEVICE" # try any children shown by lsblk (handles LUKS containers with # internal partitions such as dm-0, dm-1 etc). if command -v lsblk >/dev/null 2>&1; then @@ -1319,9 +2247,12 @@ find_lvm_vg_name() { for part in $(lsblk -np -l -o NAME "$DEVICE" | tail -n +2); do [ -b "$part" ] || continue DEBUG "find_lvm_vg_name: testing child $part" - if lvm pvs --noheadings -o vg_name "$part" >/tmp/root-hashes-gui/lvm_vg 2>/tmp/root-hashes-gui/lvm_err; then + if run_lvm pvs --noheadings -o vg_name "$part" >/tmp/root-hashes-gui/lvm_vg; then VG="$(awk 'NF {print $1; exit}' /tmp/root-hashes-gui/lvm_vg)" - [ -n "$VG" ] && { echo "$VG"; return 0; } + [ -n "$VG" ] && { + echo "$VG" + return 0 + } fi done fi @@ -1355,7 +2286,7 @@ is_gpt_bios_grub() { number="${partname##*[!0-9]}" if [ -z "$number" ]; then DEBUG "cannot parse partition name '$partname'" - return 1 # not a recognised partition + return 1 # not a recognised partition fi device="${partname%"$number"}" @@ -1409,6 +2340,7 @@ mount_possible_boot_device() { fi if cryptsetup isLuks "$BOOT_DEV"; then DEBUG "$BOOT_DEV is a LUKS volume, skipping" + LUKS_PARTITION_DETECTED="y" return 1 fi if find_lvm_vg_name "$BOOT_DEV" >/dev/null; then @@ -1421,11 +2353,9 @@ mount_possible_boot_device() { # Check if the partition is small (less than 2MB, which is 4096 sectors) if [ "$sectors" -lt 4096 ]; then - TRACE_FUNC DEBUG "Partition $BOOT_DEV is very small, likely BIOS boot. Skipping mount." return 1 else - TRACE_FUNC DEBUG "Try mounting $BOOT_DEV as /boot" if mount -o ro "$BOOT_DEV" /boot >/dev/null 2>&1; then if ls -d /boot/grub* >/dev/null 2>&1; then @@ -1490,7 +2420,7 @@ detect_boot_device() { done # no valid boot device found - echo "Unable to locate /boot files on any mounted disk" + WARN "Unable to locate /boot files on any mounted disk" DEBUG "detect_boot_device: failed to find a bootable device" return 1 } @@ -1502,16 +2432,16 @@ scan_boot_options() { config="$2" option_file="$3" - if [ -r $option_file ]; then rm $option_file; fi - for i in $(find $bootdir -name "$config"); do - DO_WITH_DEBUG kexec-parse-boot "$bootdir" "$i" >>$option_file + if [ -r "$option_file" ]; then rm "$option_file"; fi + for i in $(find "$bootdir" -name "$config"); do + DO_WITH_DEBUG kexec-parse-boot.sh "$bootdir" "$i" >>"$option_file" done # FC29/30+ may use BLS format grub config files # https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault # only parse these if $option_file is still empty - if [ ! -s $option_file ] && [ -d "$bootdir/loader/entries" ]; then - for i in $(find $bootdir -name "$config"); do - kexec-parse-bls "$bootdir" "$i" "$bootdir/loader/entries" >>$option_file + if [ ! -s "$option_file" ] && [ -d "$bootdir/loader/entries" ]; then + for i in $(find "$bootdir" -name "$config"); do + kexec-parse-bls.sh "$bootdir" "$i" "$bootdir/loader/entries" >>"$option_file" done fi } @@ -1622,12 +2552,12 @@ trap run_at_exit_handlers EXIT # Helper function to generate diceware passphrase generate_passphrase() { usage_generate_passphrase() { - echo "Usage: generate_passphrase --dictionary|-d [--number_words|-n ] [--max_length|-m ] [--lowercase|-l]" - echo "Generates a passphrase using a Diceware dictionary." - echo " --dictionary|-d Path to the Diceware dictionary file (defaults to /etc/diceware_dictionaries/eff_short_wordlist_2_0.txt )." - echo " [--number_words|-n ] Number of words in the passphrase (default: 3)." - echo " [--max_length|-m ] Maximum size of the passphrase (default: 256)." - echo " [--lowercase|-l] Use lowercase words (default: false)." + DEBUG "Usage: generate_passphrase --dictionary|-d [--number_words|-n ] [--max_length|-m ] [--lowercase|-l]" + DEBUG "Generates a passphrase using a Diceware dictionary." + DEBUG " --dictionary|-d Path to the Diceware dictionary file (defaults to /etc/diceware_dictionaries/eff_short_wordlist_2_0.txt )." + DEBUG " [--number_words|-n ] Number of words in the passphrase (default: 3)." + DEBUG " [--max_length|-m ] Maximum size of the passphrase (default: 256)." + DEBUG " [--lowercase|-l] Use lowercase words (default: false)." } # Helper subfunction to get a random word from the dictionary @@ -1662,7 +2592,7 @@ generate_passphrase() { ;; --number_words | -n) if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -le 0 ]]; then - warn "Invalid number of words: $2" + WARN "generate_passphrase: invalid number of words: $2" usage_generate_passphrase return 1 fi @@ -1671,7 +2601,7 @@ generate_passphrase() { ;; --max_length | -m) if ! [[ "$2" =~ ^[0-9]+$ ]] || [[ "$2" -le 0 ]]; then - warn "Invalid maximum size: $2" + WARN "generate_passphrase: invalid maximum size: $2" usage_generate_passphrase return 1 fi @@ -1679,7 +2609,7 @@ generate_passphrase() { shift ;; *) - warn "Unknown parameter: $1" + WARN "generate_passphrase: unknown parameter: $1" usage_generate_passphrase return 1 ;; @@ -1689,7 +2619,7 @@ generate_passphrase() { # Validate dictionary file if [[ -z "$dictionary_file" || ! -f "$dictionary_file" ]]; then - warn "Dictionary file not found or not provided: $dictionary_file" + WARN "generate_passphrase: dictionary file not found or not provided: $dictionary_file" usage_generate_passphrase return 1 fi @@ -1749,7 +2679,7 @@ load_keymap() { # this might mean keys map unexpectedly. If this is # desired, update or clear the keymap setting to silence # the warning. - warn "Keymap $1 does not exist, continuing without keymap" + WARN "Keymap $1 does not exist, continuing without keymap" fi fi @@ -1760,26 +2690,63 @@ load_keymap() { fi } +# fail_unseal - called by unseal-hotp.sh and unseal-totp.sh on failure. +# If HEADS_NONFATAL_UNSEAL=y (set by callers that handle failure themselves, +# e.g. gui-init's integrity report), log at DEBUG and return 1 so the caller +# can decide what to do. Otherwise DIE, which is appropriate when the unseal +# script is run standalone and failure is unrecoverable. +fail_unseal() { + TRACE_FUNC + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal $(basename "$0") failure: $*" + return 1 + fi + DIE "$*" +} + # Show an updating UTC timestamp and optional TOTP on a single refreshed line # until the user presses the Escape key. Returns 0 after ESC pressed. # Function name: show_totp_until_esc - clearly indicates this displays the # TOTP code and waits for the user to press Escape to continue. show_totp_until_esc() { local now_str status_line current_totp ch - local last_totp_time=0 last_totp="" - printf "\n" # reserve a line for updates - - # Clear any pending keystrokes before we start displaying the TOTP. - # In particular, a stray Escape key from the previous passphrase - # prompt could be sitting in the input buffer and would cause the - # function to immediately return on the next iteration, confusing - # the user when they try to hit Esc again. Drain stdin until it's - # empty. - while IFS= read -r -t 0 -n 1 junk; do :; done - - # Poll frequently (200ms) for responsiveness, but only refresh the - # displayed timestamp/TOTP when the displayed second changes. Cache - # the TOTP for a short interval to avoid repeated unseal calls. + # totp_ever_unsealed: set to 1 on first successful unseal; used to detect + # mid-session secret wipe (e.g. another console entered recovery shell). + local last_totp_time=0 last_totp="" totp_ever_unsealed=0 + + # Use the same terminal the user is actively interacting with. + # HEADS_TTY is set by gui-init (after cttyhack) to the actual interactive + # terminal — both output (status line) and input (Esc / Enter detection) + # must use the same device. Falls back to stdout/stdin (file descriptor + # 1/0) when HEADS_TTY is not set so that callers' redirections are + # respected (same behaviour as the original pre-HEADS_TTY code). + local interactive_tty="${HEADS_TTY}" + + # Serial consoles (ttyS*, ttyUSB*, ttyAMA*) do not reliably support raw-mode + # single-character reads: bash's "read -n 1" puts the tty into raw mode via + # tcsetattr, but some serial line disciplines block indefinitely despite the + # -t timeout. On serial we accept Enter (line-mode read) instead of Esc. + local is_serial=0 + case "$interactive_tty" in + /dev/ttyS* | /dev/ttyUSB* | /dev/ttyAMA* | /dev/ttyO*) is_serial=1 ;; + esac + + if [ -n "$interactive_tty" ]; then + printf "\n" >"$interactive_tty" 2>/dev/null # reserve a line for updates + else + printf "\n" # reserve a line for updates + fi + + # Drain any pending keystrokes (e.g. a stray Esc from the previous prompt). + # Skip on serial: "read -n 1 -t 0" also uses raw mode and would block. + if [ "$is_serial" = "0" ]; then + if [ -n "$interactive_tty" ]; then + while IFS= read -r -t 0 -n 1 junk <"$interactive_tty" 2>/dev/null; do :; done + else + while IFS= read -r -t 0 -n 1 junk; do :; done + fi + fi + local last_sec=0 while :; do now_str=$(date -u '+%Y-%m-%d %H:%M:%S UTC') @@ -1787,14 +2754,19 @@ show_totp_until_esc() { now_epoch=$(date +%s) local now_sec=$now_epoch - # Refresh TOTP at most once every 1 second + # Refresh TOTP once per second for fresh validation. if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TOTP_SKIP_QRCODE" != "y" ]; then if [ $((now_epoch - last_totp_time)) -ge 1 ] || [ -z "$last_totp" ]; then - if current_totp=$(unseal-totp 2>/dev/null); then + if current_totp=$(unseal-totp.sh 2>/dev/null); then last_totp="$current_totp" last_totp_time=$now_epoch + totp_ever_unsealed=1 + elif [ "$totp_ever_unsealed" = "1" ]; then + # Previously succeeded but now fails: TPM secrets were wiped + # mid-session (e.g. another console entered the recovery shell). + DIE "TOTP secret no longer accessible: TPM secrets were wiped. Boot integrity cannot be confirmed." else - # If unseal fails, clear cached value so we retry later + # Never succeeded yet; clear and retry next second last_totp="" last_totp_time=0 fi @@ -1804,8 +2776,6 @@ show_totp_until_esc() { # Only update display when the second changes to avoid flicker if [ "$now_sec" -ne "$last_sec" ]; then last_sec=$now_sec - # Build an explicit TOTP field so it's clear when no code is - # available (initial state or unseal failure). local totp_field="" if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TOTP_SKIP_QRCODE" != "y" ]; then if [ -n "$last_totp" ]; then @@ -1814,20 +2784,51 @@ show_totp_until_esc() { totp_field=" | TOTP unavailable" fi fi - status_line="[$now_str]${totp_field} | Press Esc to continue..." - printf "\r%s\033[K" "$status_line" + if [ "$is_serial" = "1" ]; then + status_line="\033[1m[$now_str]${totp_field} | Press Enter to continue...\033[0m" + else + status_line="\033[1m[$now_str]${totp_field} | Press Esc to continue...\033[0m" + fi + if [ -n "$interactive_tty" ]; then + printf "\r%b\033[K" "$status_line" >"$interactive_tty" 2>/dev/null + else + printf "\r%b\033[K" "$status_line" + fi fi - # Short poll for keypress (200ms). If ESC pressed, exit and return 0. - if IFS= read -r -t 0.2 -n 1 ch; then - if [ "$ch" = $'\e' ]; then - # Print an extra blank line so the next prompt appears after - # an empty line (better UX before the passphrase prompt). - printf "\n\n" - return 0 + if [ "$is_serial" = "1" ]; then + # Line-mode read: no raw mode required; times out after 1 s. + # Any input (Enter) continues. + if [ -n "$interactive_tty" ]; then + if IFS= read -r -t 1 ch <"$interactive_tty" 2>/dev/null; then + printf "\n\n" >"$interactive_tty" 2>/dev/null + return 0 + fi + else + if IFS= read -r -t 1 ch; then + printf "\n\n" + return 0 + fi + fi + else + # Framebuffer: raw single-char poll (200 ms). ESC continues. + if [ -n "$interactive_tty" ]; then + if IFS= read -r -t 0.2 -n 1 ch <"$interactive_tty" 2>/dev/null; then + if [ "$ch" = $'\e' ]; then + printf "\n\n" >"$interactive_tty" 2>/dev/null + return 0 + fi + # Ignore other keys and continue polling + fi + else + if IFS= read -r -t 0.2 -n 1 ch; then + if [ "$ch" = $'\e' ]; then + printf "\n\n" + return 0 + fi + # Ignore other keys and continue polling + fi fi - # Ignore other keys and continue polling fi done } - diff --git a/initrd/etc/gui_functions.sh b/initrd/etc/gui_functions.sh index e7844e8e2..6e67058f2 100755 --- a/initrd/etc/gui_functions.sh +++ b/initrd/etc/gui_functions.sh @@ -1,10 +1,11 @@ #!/bin/bash # Shell functions for common operations using fbwhiptail -. /etc/functions +. /etc/functions.sh # Pause for the configured timeout before booting automatically. Returns 0 to # continue with automatic boot, nonzero if user interrupted. pause_automatic_boot() { + TRACE_FUNC if IFS= read -t "$CONFIG_AUTO_BOOT_TIMEOUT" -s -n 1 -r -p \ $'Automatic boot in '"$CONFIG_AUTO_BOOT_TIMEOUT"$' seconds unless interrupted by keypress...\n'; then return 1 # Interrupt automatic boot @@ -16,14 +17,14 @@ mount_usb() { TRACE_FUNC # Unmount any previous USB device if grep -q /media /proc/mounts; then - umount /media || die "Unable to unmount /media" + umount /media || DIE "Unable to unmount /media" fi # Mount the USB boot device - mount-usb && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) + mount-usb.sh && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) if [ $USB_FAILED -ne 0 ]; then whiptail_error --title 'USB Drive Missing' \ --msgbox "Insert your USB drive and press Enter to continue." 0 80 - mount-usb && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) + mount-usb.sh && USB_FAILED=0 || ([ $? -eq 5 ] && exit 1 || USB_FAILED=1) if [ $USB_FAILED -ne 0 ]; then whiptail_error --title 'ERROR: Mounting /media Failed' \ --msgbox "Unable to mount USB device" 0 80 @@ -33,28 +34,62 @@ mount_usb() { } # -- Display related functions -- + +# Rebuild "$@" into global _WHIPTAIL_ARGS, wrapping the body text argument +# (the one immediately following --msgbox, --yesno, --menu, --inputbox, etc.) +# through printf '%b' | fold -s -w 76 so \n escapes are expanded and long +# lines fit inside an 80-column dialog. All other arguments are passed +# through unchanged. Callers must not be called recursively. +_whiptail_preprocess_args() { + _WHIPTAIL_ARGS=() + local _wrap_next=0 _arg + for _arg in "$@"; do + if [ "$_wrap_next" = 1 ]; then + _WHIPTAIL_ARGS+=("$(printf '%b' "$_arg" | fold -s -w 76)") + _wrap_next=0 + else + _WHIPTAIL_ARGS+=("$_arg") + case "$_arg" in + --msgbox | --yesno | --menu | --inputbox | --passwordbox | --checklist | --radiolist) + _wrap_next=1 + ;; + esac + fi + done +} + # Produce a whiptail prompt with 'warning' background, works for fbwhiptail and newt whiptail_warning() { + TRACE_FUNC + _whiptail_preprocess_args "$@" if [ -x /bin/fbwhiptail ]; then - whiptail $BG_COLOR_WARNING "$@" + DEBUG "whiptail_warning: whiptail $BG_COLOR_WARNING $*" + whiptail $BG_COLOR_WARNING "${_WHIPTAIL_ARGS[@]}" else - env NEWT_COLORS="root=,$TEXT_BG_COLOR_WARNING" whiptail "$@" + DEBUG "whiptail_warning: NEWT_COLORS=root=,$TEXT_BG_COLOR_WARNING whiptail $*" + env NEWT_COLORS="root=,$TEXT_BG_COLOR_WARNING" whiptail "${_WHIPTAIL_ARGS[@]}" fi } # Produce a whiptail prompt with 'error' background, works for fbwhiptail and newt whiptail_error() { + TRACE_FUNC + _whiptail_preprocess_args "$@" if [ -x /bin/fbwhiptail ]; then - whiptail $BG_COLOR_ERROR "$@" + DEBUG "whiptail_error: whiptail $BG_COLOR_ERROR $*" + whiptail $BG_COLOR_ERROR "${_WHIPTAIL_ARGS[@]}" else - env NEWT_COLORS="root=,$TEXT_BG_COLOR_ERROR" whiptail "$@" + DEBUG "whiptail_error: NEWT_COLORS=root=,$TEXT_BG_COLOR_ERROR whiptail $*" + env NEWT_COLORS="root=,$TEXT_BG_COLOR_ERROR" whiptail "${_WHIPTAIL_ARGS[@]}" fi } # Produce a whiptail prompt of the given type - 'error', 'warning', or 'normal' whiptail_type() { + TRACE_FUNC local TYPE="$1" shift + DEBUG "whiptail_type: type=$TYPE args=$*" case "$TYPE" in error) whiptail_error "$@" @@ -63,7 +98,9 @@ whiptail_type() { whiptail_warning "$@" ;; normal) - whiptail "$@" + _whiptail_preprocess_args "$@" + DEBUG "whiptail_type: whiptail $*" + whiptail "${_WHIPTAIL_ARGS[@]}" ;; esac } @@ -71,6 +108,7 @@ whiptail_type() { # Create display text for a size in bytes in either MB or GB, unit selected # automatically, rounded to nearest display_size() { + TRACE_FUNC local size_bytes unit_divisor unit_symbol size_bytes="$1" @@ -90,6 +128,7 @@ display_size() { # Create display text for the size of a block device using MB or GB, rounded to # nearest display_block_device_size() { + TRACE_FUNC local block_dev disk_size_bytes block_dev="$1" @@ -153,7 +192,7 @@ file_selector() { whiptail --title "${MENU_TITLE}" \ --menu "${MENU_MSG} [1-$n, a to abort]:" 20 120 8 \ -- "${CHOICE_ARGS[@]}" \ - 2>/tmp/whiptail || die "Aborting" + 2>/tmp/whiptail || DIE "Aborting" option_index=$(cat /tmp/whiptail) @@ -203,19 +242,567 @@ show_system_info() { --msgbox "$msgbox_rm_tabs" 0 80 } +# Show measured integrity report including TOTP/HOTP status and /boot integrity. +report_integrity_measurements() { + TRACE_FUNC + local date_now hash_state msg menu_msg totp_state hotp_state signature_state sig_status sig_detail sig_guidance report_body report_option signing_key_state + + date_now=$(date "+%Y-%m-%d %H:%M:%S %Z") + totp_state="N/A" + hotp_state="N/A" + DEBUG "integrity report generated at $date_now" + STATUS "Preparing Measured Integrity Report - hashing and verifying /boot" + + if [ "$CONFIG_TPM" = "y" ]; then + totp_state="UNAVAILABLE" + if [ "$CONFIG_TPM2_TOOLS" != "y" ] || [ -f /tmp/secret/primary.handle ]; then + DEBUG "report_integrity_measurements: unsealing integrity TOTP from TPM" + if HEADS_NONFATAL_UNSEAL=y tpmr.sh unseal 4d47 0,1,2,3,4,7 312 /tmp/secret/integrity_totp_key >/dev/null 2>&1; then + truncate_max_bytes 20 /tmp/secret/integrity_totp_key >/dev/null 2>&1 + if totp /tmp/secret/integrity_totp 2>/dev/null; then + totp_state="$(cat /tmp/secret/integrity_totp 2>/dev/null)" + else + totp_state="ERROR" + fi + fi + fi + shred -n 10 -z -u /tmp/secret/integrity_totp_key /tmp/secret/integrity_totp 2>/dev/null + DEBUG "report_integrity_measurements: totp_state=$totp_state" + fi + + if [ -x /bin/hotp_verification ]; then + enable_usb + STATUS "Checking USB security dongle presence" + local _dongle_brand _hotp_info + _dongle_brand="$(detect_usb_security_dongle_branding)" + DEBUG "report_integrity_measurements: querying HOTP token info" + if _hotp_info="$(hotp_verification info 2>/dev/null)"; then + hotp_state="TOKEN PRESENT" + STATUS_OK "USB security dongle detected" + hotpkey_fw_display "$_hotp_info" "$_dongle_brand" + elif [ "$_dongle_brand" != "USB Security dongle" ]; then + hotp_state="TOKEN INCOMPATIBLE" + DEBUG "report_integrity_measurements: $_dongle_brand detected but HOTP verification failed" + else + hotp_state="TOKEN MISSING" + DEBUG "report_integrity_measurements: hotp_verification info failed, hotp_state=TOKEN MISSING" + fi + fi + + # Detached signature trust must be established before any hash files are trusted. + signature_state="UNVERIFIED" + if [ ! -r /boot/kexec.sig ]; then + signature_state="MISSING SIGNATURE FILE" + hash_state="UNTRUSTED (DETACHED SIGNATURE MISSING)" + DEBUG "report_integrity_measurements: /boot/kexec.sig is missing" + sig_detail="/boot/kexec.sig does not exist - /boot files cannot be verified as authentic." + sig_guidance="If unexpected, stop and restore a known-good /boot. If expected, choose: Investigate discrepancies -> Update checksums now." + elif detached_kexec_signature_valid /boot; then + signature_state="VERIFIED" + # detached_kexec_signature_valid confirms trust of kexec*.txt; load those trusted references. + check_config /boot force + TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" + TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" + if [ -r "$TMP_HASH_FILE" ] && [ -r "$TMP_TREE_FILE" ] && verify_checksums /boot n; then + hash_state="OK" + else + hash_state="ALTERED OR UNKNOWN" + fi + sig_detail="ROM-fused public key authenticated /boot/kexec.sig - all /boot files match the signed hashes." + sig_guidance="No signature fix needed." + else + sig_status="$(detached_kexec_signature_failure_status /boot)" + case "$sig_status" in + MALFORMED) + signature_state="SIGNATURE FILE IS BROKEN" + hash_state="UNTRUSTED (SIGNATURE FILE IS BROKEN)" + sig_detail="/boot/kexec.sig cannot be parsed - the file appears corrupted or truncated." + sig_guidance="If unexpected, stop and restore a known-good /boot. If expected, choose: Investigate discrepancies -> Update checksums now." + ;; + BAD) + signature_state="SIGNATURE DOES NOT MATCH BOOT FILES" + hash_state="UNTRUSTED (SIGNATURE DOES NOT MATCH FILES)" + sig_detail="The signature does not match the current /boot files - files may have been altered since last signed." + sig_guidance="If unexpected, stop and investigate tampering. If expected, choose: Investigate discrepancies -> Update checksums now." + ;; + UNKNOWN_KEY) + local _signer_info + _signer_info="$(detached_kexec_signature_signer_info)" + signature_state="SIGNED BY UNTRUSTED KEY" + hash_state="UNTRUSTED - content cannot be verified" + if [ -n "$_signer_info" ]; then + sig_detail="/boot was signed by an untrusted key (${_signer_info}). The files cannot be verified and must be treated as compromised. Possible causes: disk swap, /boot signed on a different machine, or firmware reflashed with a new key." + sig_guidance="Only re-sign if you can independently confirm /boot is in expected state, knowing it was signed by ${_signer_info}. If in doubt, restore /boot from a trusted backup. Do NOT re-sign blindly - that would bless a potentially compromised /boot. For intentional re-ownership or a fresh OS install, perform OEM Factory Reset / Re-Ownership." + else + sig_detail="/boot was signed by an untrusted key (signer identity could not be determined). The files cannot be verified and must be treated as compromised. Possible causes: disk swap, /boot signed on a different machine, or firmware reflashed with a new key." + sig_guidance="Treat /boot as compromised and restore from a trusted backup. Do NOT re-sign unverified files - that would bless a potentially compromised /boot. For intentional re-ownership or a fresh OS install, perform OEM Factory Reset / Re-Ownership." + fi + ;; + *) + signature_state="SIGNATURE CHECK FAILED" + hash_state="UNTRUSTED (DETACHED SIGNATURE INVALID)" + sig_detail="The signature check failed for an unknown reason." + sig_guidance="If this was NOT expected, stop and investigate. Only choose Update checksums after you trust the current /boot files." + ;; + esac + DEBUG "report_integrity_measurements: detached signature status=$sig_status detail=$(detached_kexec_signature_failure_detail_one_line)" + fi + INTEGRITY_REPORT_HASH_STATE="$hash_state" + + # Check signing key: try card immediately (USB already up); only prompt if not accessible. + # wait_for_gpg_card sets global gpg_output to the card-status output on success. + STATUS "Verifying OpenPGP signing key on USB security dongle" + enable_usb + gpg_output="" + local _card_detected=0 + if wait_for_gpg_card 2>/dev/null; then + _card_detected=1 + else + whiptail_type "$BG_COLOR_MAIN_MENU" --title 'Signing Card Check' \ + --msgbox "Please insert your OpenPGP signing card (USB security key), then press OK." 0 80 + if wait_for_gpg_card 2>/dev/null; then + _card_detected=1 + fi + fi + + # Determine signing key state from card-status output (gpg_output set by wait_for_gpg_card). + local _card_sig_fpr _rom_fprs signing_key_guidance + if [ "$_card_detected" -eq 0 ]; then + signing_key_state="NO DONGLE DETECTED" + signing_key_guidance="No USB security dongle detected. Insert the correct dongle and retry, or perform OEM Factory Reset / Re-Ownership." + else + _card_sig_fpr=$(echo "$gpg_output" | + awk -F: '/Signature key/ {gsub(/[[:space:]]/,"",$2); print $2; exit}') + if [ -z "$_card_sig_fpr" ] || [ "$_card_sig_fpr" = "[none]" ]; then + signing_key_state="DONGLE NOT PROVISIONED" + signing_key_guidance="USB security dongle is connected but has no signing key (unprovisioned or wiped). Provision the dongle with the signing subkey, or perform OEM Factory Reset / Re-Ownership to start fresh with a new key." + else + _rom_fprs=$(gpg --with-colons --list-keys 2>/dev/null | + awk -F: '/^fpr/ {print $10}') + if echo "$_rom_fprs" | grep -qF "$_card_sig_fpr"; then + signing_key_state="DONGLE MATCHES ROM-TRUSTED KEY" + signing_key_guidance="" + else + signing_key_state="DONGLE KEY NOT ROM-TRUSTED" + signing_key_guidance="USB security dongle has a signing key that does not match this firmware's trusted key. OEM Factory Reset / Re-Ownership is required to establish new trusted ownership." + fi + fi + fi + DEBUG "report_integrity_measurements: signing_key_state=$signing_key_state card_sig_fpr=${_card_sig_fpr:-none}" + + # Build display-friendly variants of TOTP/HOTP state for the report + local totp_display hotp_display + case "$totp_state" in + UNAVAILABLE) + totp_display="SEALED SECRET UNAVAILABLE - Reseal required (expected after TPM reset, re-ownership, or firmware update)" + ;; + ERROR) + totp_display="ERROR - TOTP calculation failed" + ;; + *) + totp_display="$totp_state" + ;; + esac + case "$hotp_state" in + "TOKEN MISSING") + hotp_display="TOKEN NOT CONNECTED" + ;; + "TOKEN PRESENT") + hotp_display="TOKEN CONNECTED (presence confirmed)" + ;; + "TOKEN INCOMPATIBLE") + hotp_display="TOKEN INCOMPATIBLE ($_dongle_brand does not support HOTP)" + ;; + *) + hotp_display="$hotp_state" + ;; + esac + + local action_guidance + if [ -n "$signing_key_guidance" ]; then + action_guidance="$signing_key_guidance" + else + action_guidance="$sig_guidance" + fi + report_body="Date: $date_now\nTOTP: $totp_display\nHOTP: $hotp_display\n\nBoot signature (/boot/kexec.sig): $signature_state\n$sig_detail\nBoot files: $hash_state\nDongle key: $signing_key_state\n\nAction: $action_guidance" + if [ "$hash_state" != "OK" ]; then + report_body="$report_body\n\nIf /boot integrity is not OK, investigate before sealing new secrets or performing TPM reset or re-ownership." + fi + DEBUG "report_integrity_measurements: totp=$totp_state hotp=$hotp_state signature=$signature_state hash=$hash_state" + DEBUG "report_integrity_measurements: signature_detail=$sig_detail" + DEBUG "report_integrity_measurements: signature_guidance=$sig_guidance signing_key_guidance=$signing_key_guidance" + DEBUG "report_integrity_measurements: INTEGRITY_REPORT_HASH_STATE=$INTEGRITY_REPORT_HASH_STATE" + if [ "$totp_state" = "UNAVAILABLE" ] && [ "$hash_state" = "OK" ] && [ "$signing_key_state" = "DONGLE MATCHES ROM-TRUSTED KEY" ]; then + DEBUG "report_integrity_measurements: TOTP unseal unavailable but /boot integrity is OK; reseal/update flows may proceed after user confirmation" + report_body="$report_body\n\nNote: /boot is intact - generate a new HOTP/TOTP secret to restore real-time boot attestation." + fi + msg="Measured Integrity Report\n\n$report_body" + # menu_msg omits the guidance paragraphs to keep the dialog within terminal height + menu_msg="Measured Integrity Report\n\nDate: $date_now\nTOTP: $totp_display\nHOTP: $hotp_display\n\nBoot signature (/boot/kexec.sig): $signature_state\n$sig_detail\nBoot files: $hash_state\nDongle key: $signing_key_state\n\nChoose an action:" + + if [ "$hash_state" = "OK" ] && [ "$signing_key_state" = "DONGLE MATCHES ROM-TRUSTED KEY" ]; then + whiptail_type $BG_COLOR_MAIN_MENU --title 'Measured Integrity Report' \ + --msgbox "$msg" 0 80 + return 0 + elif [ "$hash_state" = "OK" ] && [ "$signing_key_state" != "DONGLE MATCHES ROM-TRUSTED KEY" ]; then + # /boot is intact but no private key - direct path is OEM Factory Reset / Re-Ownership + while true; do + whiptail_type "$BG_COLOR_MAIN_MENU" --title 'Measured Integrity Report' \ + --menu "$msg" 0 80 2 \ + 'o' ' OEM Factory Reset / Re-Ownership -->' \ + 'c' ' Continue to main menu' \ + 2>/tmp/whiptail || return 0 + report_option=$(cat /tmp/whiptail) + case "$report_option" in + o) + INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh + return 0 + ;; + c | *) + return 0 + ;; + esac + done + fi + + if [ "$signing_key_state" = "DONGLE KEY NOT ROM-TRUSTED" ]; then + while true; do + whiptail_type $BG_COLOR_MAIN_MENU --title 'Measured Integrity Report' \ + --menu "$menu_msg" 0 80 4 \ + 'i' ' Investigate discrepancies -->' \ + 'r' ' Replace GPG key in current ROM and reflash' \ + 'o' ' OEM Factory Reset / Re-Ownership' \ + 'c' ' Continue' \ + 2>/tmp/whiptail || return 0 + report_option=$(cat /tmp/whiptail) + case "$report_option" in + i) + if investigate_integrity_discrepancies; then + report_integrity_measurements + return + fi + ;; + r) + gpg_replace_key_reflash + ;; + o) + INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh + return 0 + ;; + *) + return 0 + ;; + esac + done + else + while true; do + whiptail_type $BG_COLOR_MAIN_MENU --title 'Measured Integrity Report' \ + --menu "$menu_msg" 0 80 2 \ + 'i' ' Investigate discrepancies -->' \ + 'c' ' Continue' \ + 2>/tmp/whiptail || return 0 + report_option=$(cat /tmp/whiptail) + case "$report_option" in + i) + if investigate_integrity_discrepancies; then + report_integrity_measurements + return + fi + ;; + *) + return 0 + ;; + esac + done + fi +} + +investigate_integrity_discrepancies() { + TRACE_FUNC + local changed_files changed_count details sig_details sig_status + local sig_trust_state investigation_option inv_msg + + # Signature trust must be established first. If detached signature is not + # trusted, checksum success must not be treated as clean integrity. + sig_trust_state="untrusted" + if detached_kexec_signature_valid /boot; then + sig_trust_state="trusted" + fi + DEBUG "investigate_integrity_discrepancies: signature trust state=$sig_trust_state" + + if [ "$sig_trust_state" = "trusted" ]; then + check_config /boot force + TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" + TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt" + if verify_checksums /boot y; then + DEBUG "investigate_integrity_discrepancies: detached signature verified and verify_checksums returned success" + whiptail_type $BG_COLOR_MAIN_MENU --title 'Integrity Investigation' \ + --msgbox 'No integrity discrepancies are currently detected for /boot.' 0 80 + return 0 + fi + DEBUG "investigate_integrity_discrepancies: detached signature verified but verify_checksums reported discrepancies" + else + DEBUG "investigate_integrity_discrepancies: detached signature not trusted; treating /boot as untrusted regardless of checksum output" + fi + + if [ "$sig_trust_state" = "trusted" ]; then + changed_files=$(grep -v 'OK$' /tmp/hash_output 2>/dev/null | cut -f1 -d ':' | sed '/^$/d') + if [ -z "$changed_files" ] && [ -r /tmp/hash_output ]; then + changed_files=$(sed '/^$/d' /tmp/hash_output) + fi + DEBUG "investigate_integrity_discrepancies: raw changed_files list=$changed_files" + else + if [ ! -r /boot/kexec.sig ]; then + sig_details="Signature file is missing" + else + sig_status="$(detached_kexec_signature_failure_status /boot)" + sig_details="$(detached_kexec_signature_failure_detail_one_line)" + [ -n "$sig_details" ] || sig_details="Signature verification failed" + case "$sig_status" in + MALFORMED) + sig_details="Signature file is damaged or not a valid signature (${sig_details})" + ;; + BAD) + sig_details="Signature does not match current /boot files (${sig_details})" + ;; + UNKNOWN_KEY) + sig_details="Signature uses a key this firmware does not trust (${sig_details})" + ;; + *) + sig_details="Signature verification failed (${sig_details})" + ;; + esac + fi + changed_files="Signature problem: $sig_details" + DEBUG "investigate_integrity_discrepancies: signature issue details=$sig_details" + fi + + if [ -z "$changed_files" ]; then + whiptail_error --title 'Integrity Investigation' \ + --msgbox 'Integrity is not OK, but no detailed mismatch list is available.' 0 80 + return 1 + fi + + # details remains relative; user is told paths are under /boot + details=$(printf '%s\n' "$changed_files" | sort -u) + changed_count=$(printf '%s\n' "$details" | wc -l | tr -d ' ') + DEBUG "integrity: changed_count=$changed_count" + DEBUG "integrity: details=$details" + + if [ "$sig_trust_state" = "trusted" ]; then + inv_msg="Integrity mismatches were detected.\n\nDetached signature on /boot/kexec.sig verified successfully.\n\nChoose an action:" + else + inv_msg="Integrity mismatches were detected.\n\nDetached signature on /boot/kexec.sig could not be verified.\n\nTreat /boot as untrusted unless you explicitly expected these changes.\n\nChoose an action:" + fi + + while true; do + whiptail_error --title 'Integrity Investigation' \ + --menu "$inv_msg" 0 80 5 \ + 'd' ' Show mismatch details -->' \ + 's' ' Show detached signed output -->' \ + 'u' ' Update checksums now' \ + 'r' ' Drop to recovery shell (view discrepancies)' \ + 'c' ' Continue' \ + 2>/tmp/whiptail || return 1 + + investigation_option=$(cat /tmp/whiptail) + case "$investigation_option" in + s) + show_detached_signed_kexec_output + ;; + d) + if [ "$changed_count" -gt 12 ]; then + printf '%s\n' "$details" >/tmp/hash_output_mismatches + echo 'Type "q" to exit the list and return.' >>/tmp/hash_output_mismatches + whiptail_error --title 'Integrity Investigation' \ + --msgbox "${changed_count} discrepancy entries found.\n\nPress OK to review the full list." 0 80 + less /tmp/hash_output_mismatches + else + whiptail_error --title 'Integrity Investigation' \ + --msgbox "Discrepancy entries detected:\n\n${details}" 0 80 + fi + ;; + r) + local msg + msg=$'Integrity discrepancies detected (paths are under /boot):\n\n'"${details}"$'\n\nTo investigate:\n 1. remount /boot read-write:\n mount -o rw,remount /boot\n 2. edit files with vi (use :wq to save and exit) and save your changes\n 3. unsafe boot is still possible via the '"${CONFIG_BRAND_NAME}"$' menu: Options -> Boot Options -> Ignore tampering and force a boot\n while /boot remains untrusted\n 4. run reboot when done; '"${CONFIG_BRAND_NAME}"$' will re-audit on next boot\n\nBe cautious. If unsure, reinstall and restore from backups.' + recovery "$msg" + ;; + u) + prompt_update_checksums && return 0 + ;; + *) + return 0 + ;; + esac + done +} + +detached_kexec_signature_valid() { + TRACE_FUNC + local boot_dir="$1" + + [ -n "$boot_dir" ] || boot_dir="/boot" + boot_dir="${boot_dir%%/}" + + if [ "$CONFIG_BASIC" = "y" ]; then + return 1 + fi + + if [ ! -r "$boot_dir/kexec.sig" ]; then + DEBUG "detached_kexec_signature_valid: no $boot_dir/kexec.sig" + return 1 + fi + + # Collect full paths once; derive relative names via ##*/ where needed. + local kexec_txt_files=() + for f in "$boot_dir"/kexec*.txt; do + [ -e "$f" ] || continue + kexec_txt_files+=("$f") + done + if [ ${#kexec_txt_files[@]} -eq 0 ]; then + DEBUG "detached_kexec_signature_valid: no kexec*.txt files found under $boot_dir" + return 1 + fi + DEBUG "detached_kexec_signature_valid: ${#kexec_txt_files[@]} file(s) in $boot_dir: ${kexec_txt_files[*]##*/}" + + # Try relative filenames first (cd into boot_dir) to match the signing + # path format used by this branch's kexec-sign-config.sh (staging dir + relative names). + STATUS "Verifying /boot detached signature" + DEBUG "detached_kexec_signature_valid: running (cd $boot_dir && sha256sum ${kexec_txt_files[*]##*/}) | gpgv.sh $boot_dir/kexec.sig" + if (cd "$boot_dir" && sha256sum "${kexec_txt_files[@]##*/}") | + gpgv.sh "$boot_dir/kexec.sig" - >/tmp/integrity_sigcheck 2>&1; then + DEBUG "detached_kexec_signature_valid: signature valid (relative paths)" + mkdir -p /tmp/kexec + cp "$boot_dir"/kexec*.txt /tmp/kexec 2>/dev/null || true + return 0 + fi + DEBUG "detached_kexec_signature_valid: relative-path check failed; retrying with full paths (legacy format)" + DEBUG "$(sed -n '1,20p' /tmp/integrity_sigcheck)" + + # Backwards compatibility: the previous kexec-sign-config.sh signed with full + # paths (sha256sum /boot/kexec*.txt), not relative paths. A firmware upgrade + # must not invalidate an existing valid signature. + DEBUG "detached_kexec_signature_valid: running sha256sum ${kexec_txt_files[*]} | gpgv.sh $boot_dir/kexec.sig" + if sha256sum "${kexec_txt_files[@]}" | + gpgv.sh "$boot_dir/kexec.sig" - >/tmp/integrity_sigcheck 2>&1; then + DEBUG "detached_kexec_signature_valid: signature valid (full paths, legacy format)" + mkdir -p /tmp/kexec + cp "$boot_dir"/kexec*.txt /tmp/kexec 2>/dev/null || true + return 0 + fi + DEBUG "detached_kexec_signature_valid: both relative and full-path checks failed" + DEBUG "$(sed -n '1,20p' /tmp/integrity_sigcheck)" + return 1 +} + +detached_kexec_signature_failure_status() { + TRACE_FUNC + local boot_dir="$1" + + [ -n "$boot_dir" ] || boot_dir="/boot" + if [ ! -r "$boot_dir/kexec.sig" ]; then + echo "MISSING" + return 0 + fi + + if grep -Eiq 'no valid openpgp data found' /tmp/integrity_sigcheck 2>/dev/null; then + echo "MALFORMED" + return 0 + fi + if grep -Eiq 'bad signature' /tmp/integrity_sigcheck 2>/dev/null; then + echo "BAD" + return 0 + fi + if grep -Eiq 'no public key|can.t check signature: no public key' /tmp/integrity_sigcheck 2>/dev/null; then + echo "UNKNOWN_KEY" + return 0 + fi + + echo "INVALID" +} + +detached_kexec_signature_failure_detail_one_line() { + TRACE_FUNC + local line + + if [ ! -r /boot/kexec.sig ]; then + echo "/boot/kexec.sig is missing" + return 0 + fi + + line="$(grep -Eim1 'no valid openpgp data found|bad signature|no public key|can.t check signature' /tmp/integrity_sigcheck 2>/dev/null)" + if [ -z "$line" ]; then + line="$(sed -n '1p' /tmp/integrity_sigcheck 2>/dev/null)" + fi + + echo "$line" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g; s/^ //; s/ $//' +} + +detached_kexec_signature_signer_info() { + # Parse gpgv output in /tmp/integrity_sigcheck to extract signer key fingerprint + # and signing date. Returns empty string if not parseable. + # gpgv output for unknown key: + # gpgv: Signature made Wed Mar 11 19:53:41 2026 UTC + # gpgv: using RSA key 8E2364E3F305AACEDFFBB61C03E3D64DDA3E571B + # gpgv: Can't check signature: No public key + local date_str key_id + date_str="$(grep -im1 'signature made' /tmp/integrity_sigcheck 2>/dev/null | + sed 's/.*[Ss]ignature made[[:space:]]*//' | + sed 's/[[:space:]]*$//')" + key_id="$(grep -im1 'using .* key' /tmp/integrity_sigcheck 2>/dev/null | + sed 's/.*using [A-Za-z0-9]* key[[:space:]]*//' | + sed 's/[[:space:]]*$//')" + if [ -n "$key_id" ] && [ -n "$date_str" ]; then + echo "fingerprint $key_id, signed on $date_str, owner unknown (key not in firmware keyring)" + elif [ -n "$key_id" ]; then + echo "fingerprint $key_id, date unknown, owner unknown (key not in firmware keyring)" + fi +} + +show_detached_signed_kexec_output() { + TRACE_FUNC + local signed_files signed_count + + signed_files=$(find /tmp/kexec/kexec*.txt 2>/dev/null | sort) + if [ -z "$signed_files" ]; then + whiptail_error --title 'Signed Output' \ + --msgbox 'No verified detached signed output is available to display.' 0 80 + return 1 + fi + + : >/tmp/integrity_signed_output + for signed_file in $signed_files; do + echo "===== $(basename "$signed_file") =====" >>/tmp/integrity_signed_output + cat "$signed_file" >>/tmp/integrity_signed_output + echo >>/tmp/integrity_signed_output + done + + signed_count=$(wc -l >/tmp/integrity_signed_output + less /tmp/integrity_signed_output + else + whiptail_type $BG_COLOR_MAIN_MENU --title 'Signed Output' \ + --msgbox "$(cat /tmp/integrity_signed_output)" 0 80 + fi +} + # Get "Enable" or "Disable" to display in the configuration menu, based on a # setting value get_config_display_action() { + TRACE_FUNC [ "$1" = "y" ] && echo "Disable" || echo "Enable" } # Invert a config value invert_config() { + TRACE_FUNC [ "$1" = "y" ] && echo "n" || echo "y" } # Get "Enable" or "Disable" for a config that internally is inverted (because it # disables a behavior that is on by default). get_inverted_config_display_action() { + TRACE_FUNC get_config_display_action "$(invert_config "$1")" } diff --git a/initrd/etc/luks-functions.sh b/initrd/etc/luks-functions.sh index 43fc09aac..355f29ca9 100644 --- a/initrd/etc/luks-functions.sh +++ b/initrd/etc/luks-functions.sh @@ -1,14 +1,14 @@ #!/bin/bash # This script contains various functions related to LUKS (Linux Unified Key Setup) encryption management. -. /etc/functions -. /etc/gui_functions +. /etc/functions.sh +. /etc/gui_functions.sh . /tmp/config # List all LUKS devices on the system that are not USB list_local_luks_devices() { TRACE_FUNC - lvm vgscan 2>/dev/null || true + run_lvm vgscan 2>/dev/null || true blkid | cut -d ':' -f 1 | while read -r device; do DEBUG "Checking device: $device" if cryptsetup isLuks "$device"; then @@ -38,10 +38,9 @@ list_local_luks_devices() { prompt_luks_passphrase() { TRACE_FUNC while [[ ${#luks_current_Disk_Recovery_Key_passphrase} -lt 8 ]]; do - echo -e "\nEnter the LUKS Disk Recovery Key passphrase (At least 8 characters long):" - read -r luks_current_Disk_Recovery_Key_passphrase + INPUT "Enter the LUKS Disk Recovery Key passphrase (at least 8 characters):" -r luks_current_Disk_Recovery_Key_passphrase if [[ ${#luks_current_Disk_Recovery_Key_passphrase} -lt 8 ]]; then - echo -e "\nPassphrase must be at least 8 characters long. Please try again." + WARN "Passphrase must be at least 8 characters long. Please try again." unset luks_current_Disk_Recovery_Key_passphrase continue fi @@ -55,7 +54,7 @@ test_luks_passphrase() { DEBUG "Testing LUKS passphrase against all found LUKS containers" list_local_luks_devices >/tmp/luks_devices.txt if [ ! -s /tmp/luks_devices.txt ]; then - warn "No LUKS devices found" + WARN "No LUKS devices found" return 1 fi @@ -87,13 +86,13 @@ confirm_luks_partitions() { MSG="The following LUKS partitions can be unlocked:\n\n${LUKS}\n\nDo you want to use all of these partitions?" if [ -x /bin/whiptail ]; then if ! whiptail --title "Confirm LUKS Partitions" --yesno "$MSG" 0 80; then - die "User aborted the operation" + DIE "User aborted the operation" fi else - echo -e "$MSG" - read -p "Do you want to use all of these partitions? (y/n): " confirm + INFO "$MSG" + INPUT "Do you want to use all of these partitions? (y/n):" -n 1 -r confirm if [ "$confirm" != "y" ]; then - die "User aborted the operation" + DIE "User aborted the operation" fi fi DEBUG "User confirmed LUKS partitions: $LUKS" @@ -104,7 +103,7 @@ main_luks_selection() { TRACE_FUNC prompt_luks_passphrase if ! test_luks_passphrase; then - die "Passphrase test failed on all LUKS devices" + DIE "Passphrase test failed on all LUKS devices" fi confirm_luks_partitions DEBUG "Selected LUKS partitions: $LUKS" @@ -122,17 +121,12 @@ select_luks_container_size_percent() { "25" "25%" \ "50" "50%" \ "75" "75%" \ - 2> /tmp/luks_container_size_percent \ - || die "Error selecting LUKS container size percentage of device" + 2>/tmp/luks_container_size_percent || + DIE "Error selecting LUKS container size percentage of device" else #console prompt asking user to select ratio of device to use for LUKS container between: 10, 25, 50, 75 #console prompt returns the percentage of the device to use for LUKS container - echo "Select LUKS container size percentage of device:" - echo "1. 10%" - echo "2. 25%" - echo "3. 50%" - echo "4. 75%" - read -p "Choose your LUKS container size percentage of device [1-3]: " option_index + INPUT "Select LUKS container size percentage of device:\n 1. 10%\n 2. 25%\n 3. 50%\n 4. 75%\nChoice [1-4]:" -n 1 -r option_index if [ "$option_index" = "1" ]; then echo "10" >/tmp/luks_container_size_percent elif [ "$option_index" = "2" ]; then @@ -142,7 +136,7 @@ select_luks_container_size_percent() { elif [ "$option_index" = "4" ]; then echo "75" >/tmp/luks_container_size_percent else - die "Error selecting LUKS container size percentage of device" + DIE "Error selecting LUKS container size percentage of device" fi fi } @@ -166,22 +160,21 @@ interactive_prepare_thumb_drive() { #Parse parameters while [ $# -gt 0 ]; do case "$1" in - --device) - DEVICE=$2 - shift 2 - ;; - --percentage) - PERCENTAGE=$2 - shift 2 - ;; - --pass) - PASSPHRASE=$2 - shift 2 - ;; - *) - echo "usage: prepare_thumb_drive [--device device] [--percentage percentage] [--pass passphrase]" - return 1 - ;; + --device) + DEVICE=$2 + shift 2 + ;; + --percentage) + PERCENTAGE=$2 + shift 2 + ;; + --pass) + PASSPHRASE=$2 + shift 2 + ;; + *) + DIE "prepare_thumb_drive: unknown argument '$1' - usage: prepare_thumb_drive [--device device] [--percentage percentage] [--pass passphrase]" + ;; esac done @@ -195,47 +188,36 @@ interactive_prepare_thumb_drive() { #If no passphrase was provided, ask user to select passphrase for LUKS container #console based no whiptail while [[ ${#PASSPHRASE} -lt 8 ]]; do - { - echo -e "\nEnter passphrase for LUKS container (At least 8 characters long):" - #hide passphrase input from read command - read -r -s PASSPHRASE - #skip confirmation if passphrase is less then 8 characters long (continue) - if [[ ${#PASSPHRASE} -lt 8 ]]; then - echo -e "\nPassphrase must be at least 8 characters long. Please try again." - unset PASSPHRASE - continue - fi - #validate passphrase and ask user to re-enter if not at least 8 characters long - #confirm passphrase - echo -e "\nConfirm passphrase for LUKS container:" - #hide passphrase input from read command - read -r -s PASSPHRASE_CONFIRM - #compare passphrase and passphrase confirmation - if [ "$PASSPHRASE" != "$PASSPHRASE_CONFIRM" ]; then - echo -e "\nPassphrases do not match. Please try again." - unset PASSPHRASE - unset PASSPHRASE_CONFIRM - fi - - } + INPUT "Enter passphrase for LUKS container (at least 8 characters):" -r -s PASSPHRASE + if [[ ${#PASSPHRASE} -lt 8 ]]; then + WARN "Passphrase must be at least 8 characters long. Please try again." + unset PASSPHRASE + continue + fi + INPUT "Confirm passphrase for LUKS container:" -r -s PASSPHRASE_CONFIRM + if [ "$PASSPHRASE" != "$PASSPHRASE_CONFIRM" ]; then + WARN "Passphrases do not match. Please try again." + unset PASSPHRASE + unset PASSPHRASE_CONFIRM + fi done fi #If no device was provided, ask user to select device to partition if [ -z "$DEVICE" ]; then - #warn user to disconnect all external drives + #WARN user to disconnect all external drives if [ -x /bin/whiptail ]; then whiptail_warning --title "WARNING: Disconnect all external drives" --msgbox \ "WARNING: Please disconnect all external drives before proceeding.\n\nHit Enter to continue." 0 80 || - die "User cancelled wiping and repartitioning of $DEVICE" + DIE "User cancelled wiping and repartitioning of $DEVICE" else - echo -e -n "Warning: Please disconnect all external drives before proceeding.\n\nHit Enter to continue?" - read -r -p " [Y/n] " response + NOTE "Please disconnect all external drives before proceeding." + INPUT "Continue? [Y/n]:" -n 1 -r response #transform response to uppercase with bash parameter expansion response=${response^^} #continue if response different then uppercase N if [[ $response =~ ^(N)$ ]]; then - die "User cancelled wiping and repartitioning of $DEVICE" + DIE "User cancelled wiping and repartitioning of $DEVICE" fi fi @@ -249,18 +231,18 @@ interactive_prepare_thumb_drive() { if [ $(cat /tmp/devices.txt | wc -l) -gt 0 ]; then file_selector "/tmp/devices.txt" "Select device to partition" if [ "$FILE" == "" ]; then - die "Error: No device selected" + DIE "Error: No device selected" else DEVICE=$FILE fi else - die "Error: No device found" + DIE "Error: No device found" fi fi #Check if device is a block device if [ ! -b $DEVICE ]; then - die "Error: $DEVICE is not a block device" + DIE "Error: $DEVICE is not a block device" fi if [ -z "$PERCENTAGE" ]; then @@ -270,7 +252,7 @@ interactive_prepare_thumb_drive() { fi confirm_thumb_drive_format "$DEVICE" "$PERCENTAGE" || - die "User cancelled wiping and repartitioning of $DEVICE" + DIE "User cancelled wiping and repartitioning of $DEVICE" prepare_thumb_drive "$DEVICE" "$PERCENTAGE" "$PASSPHRASE" } @@ -295,17 +277,17 @@ confirm_thumb_drive_format() { DISK_SIZE_BYTES="$(blockdev --getsize64 "$DEVICE")" DISK_SIZE_DISPLAY="$(display_size "$DISK_SIZE_BYTES")" #Convert disk size to MB - DISK_SIZE_MB=$((DISK_SIZE_BYTES/1024/1024)) + DISK_SIZE_MB=$((DISK_SIZE_BYTES / 1024 / 1024)) #Calculate percentage of device in MB - LUKS_SIZE_MB="$((DISK_SIZE_BYTES*LUKS_PERCENTAGE/100/1024/1024))" + LUKS_SIZE_MB="$((DISK_SIZE_BYTES * LUKS_PERCENTAGE / 100 / 1024 / 1024))" MSG="WARNING: Wiping and repartitioning $DEVICE ($DISK_SIZE_DISPLAY) with $LUKS_SIZE_MB MB\n assigned to private LUKS ext4 partition,\n rest assigned to exFAT public partition.\n\nAre you sure you want to continue?" if [ -x /bin/whiptail ]; then whiptail_warning --title "WARNING: Wiping and repartitioning $DEVICE ($DISK_SIZE_DISPLAY)" --yesno \ "$MSG" 0 80 else - echo -e -n "$MSG" - read -r -p " [Y/n] " response + NOTE "$MSG" + INPUT "Continue? [Y/n]:" -n 1 -r response #transform response to uppercase with bash parameter expansion response=${response^^} #continue if response is Y, y, or empty, abort for anything else @@ -334,29 +316,29 @@ prepare_thumb_drive() { #Get disk size in bytes DISK_SIZE_BYTES="$(blockdev --getsize64 "$DEVICE")" #Calculate percentage of device in MB - PERCENTAGE_MB="$((DISK_SIZE_BYTES*PERCENTAGE/100/1024/1024))" + PERCENTAGE_MB="$((DISK_SIZE_BYTES * PERCENTAGE / 100 / 1024 / 1024))" - echo -e "Preparing $DEVICE with $PERCENTAGE_MB MB for private LUKS container while rest of device will be assigned to exFAT public partition...\n" - echo "Please wait..." + STATUS "Preparing $DEVICE: ${PERCENTAGE_MB}MB LUKS private + exFAT public partition" + STATUS "Please wait..." DEBUG "Creating empty DOS partition table on device through fdisk to start clean" - echo -e "o\nw\n" | fdisk $DEVICE >/dev/null 2>&1 || die "Error creating partition table" + echo -e "o\nw\n" | fdisk $DEVICE >/dev/null 2>&1 || DIE "Error creating partition table" DEBUG "partition device with two partitions: first one being the percent applied and rest for second partition through fdisk" - echo -e "n\np\n1\n\n+"$PERCENTAGE_MB"M\nn\np\n2\n\n\nw\n" | fdisk $DEVICE >/dev/null 2>&1 || die "Error partitioning device" + echo -e "n\np\n1\n\n+"$PERCENTAGE_MB"M\nn\np\n2\n\n\nw\n" | fdisk $DEVICE >/dev/null 2>&1 || DIE "Error partitioning device" DEBUG "cryptsetup luksFormat first partition with LUKS container aes-xts-plain64 cipher with sha256 hash and 512 bit key" DEBUG "Creating ${PERCENTAGE_MB}MB LUKS container on ${DEVICE}1..." DO_WITH_DEBUG cryptsetup --batch-mode -c aes-xts-plain64 -h sha256 -s 512 -y luksFormat ${DEVICE}1 \ - --key-file <(echo -n "${PASSPHRASE}") > /dev/null 2>&1 \ - || die "Error formatting LUKS container" + --key-file <(echo -n "${PASSPHRASE}") >/dev/null 2>&1 || + DIE "Error formatting LUKS container" DEBUG "Opening LUKS device and mapping under /dev/mapper/private..." - DO_WITH_DEBUG cryptsetup open ${DEVICE}1 private --key-file <(echo -n "${PASSPHRASE}") > /dev/null 2>&1 \ - || die "Error opening LUKS container" + DO_WITH_DEBUG cryptsetup open ${DEVICE}1 private --key-file <(echo -n "${PASSPHRASE}") >/dev/null 2>&1 || + DIE "Error opening LUKS container" DEBUG "Formatting LUKS container mapped under /dev/mapper/private as an ext4 partition..." - mke2fs -t ext4 -L private /dev/mapper/private >/dev/null 2>&1 || die "Error formatting LUKS container's ext4 filesystem" + mke2fs -t ext4 -L private /dev/mapper/private >/dev/null 2>&1 || DIE "Error formatting LUKS container's ext4 filesystem" DEBUG "Closing LUKS device /dev/mapper/private..." - cryptsetup close private > /dev/null 2>&1 || die "Error closing LUKS container" + cryptsetup close private >/dev/null 2>&1 || DIE "Error closing LUKS container" DEBUG "Formatting second partition ${DEVICE}2 with exfat filesystem..." - mkfs.exfat -L public ${DEVICE}2 >/dev/null 2>&1 || die "Error formatting second partition with exfat filesystem" - echo "Done." + mkfs.exfat -L public ${DEVICE}2 >/dev/null 2>&1 || DIE "Error formatting second partition with exfat filesystem" + STATUS_OK "Done." } # Select LUKS container @@ -367,7 +349,7 @@ select_luks_container() { LUKS=$(cut -d ' ' -f1 /boot/kexec_key_devices.txt) DEBUG "LUKS container device: $(echo $LUKS)" elif [ -z "$LUKS" ]; then - main_luks_selection + main_luks_selection fi } @@ -379,16 +361,14 @@ test_luks_current_disk_recovery_key_passphrase() { PRINTABLE_LUKS=$(echo $LUKS) + STATUS "$PRINTABLE_LUKS: Unlocking with LUKS Disk Recovery Key passphrase" if [ -z "$luks_current_Disk_Recovery_Key_passphrase" ]; then - echo -e "\nEnter the current LUKS Disk Recovery Key passphrase (Configured at OS installation or by OEM):" - read -r luks_current_Disk_Recovery_Key_passphrase - echo -n "$luks_current_Disk_Recovery_Key_passphrase" > /tmp/secret/luks_current_Disk_Recovery_Key_passphrase + INPUT "Enter the current LUKS Disk Recovery Key passphrase (configured at OS installation or by OEM):" -r luks_current_Disk_Recovery_Key_passphrase + echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase else - echo -n "$luks_current_Disk_Recovery_Key_passphrase" > /tmp/secret/luks_current_Disk_Recovery_Key_passphrase + echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase fi - echo -e "\n$PRINTABLE_LUKS: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." - for luks_container in $LUKS; do DEBUG "$luks_container: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." if ! cryptsetup open --test-passphrase "$luks_container" --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase; then @@ -401,7 +381,7 @@ test_luks_current_disk_recovery_key_passphrase() { luks_secrets_cleanup unset LUKS else - echo "$luks_container: unlocking LUKS container with current Disk Recovery Key passphrase successful" + STATUS_OK "$luks_container: unlocked with current Disk Recovery Key passphrase" export luks_current_Disk_Recovery_Key_passphrase fi done @@ -424,23 +404,10 @@ luks_reencrypt() { TRACE_FUNC DEBUG "luks_containers: ${luks_containers[@]}" - if [ -z "$luks_current_Disk_Recovery_Key_passphrase" ]; then - if [ -f /tmp/secret/luks_current_Disk_Recovery_Key_passphrase ]; then - luks_current_Disk_Recovery_Key_passphrase=$(cat /tmp/secret/luks_current_Disk_Recovery_Key_passphrase) - else - msg=$(echo -e "This will replace the encrypted container content and its LUKS Disk Recovery Key.\n\nThe passphrase associated with this key will be asked from the user under the following conditions:\n 1-Every boot if no Disk Unlock Key was added to the TPM\n 2-If the TPM fails (hardware failure)\n 3-If the firmware has been tampered with/modified by the user\n\nThis process requires you to type the current LUKS Disk Recovery Key passphrase and will delete the LUKS TPM Disk Unlock Key slot, if set up, by setting a default boot LUKS key slot (1) if present.\n\nAt the next prompt, you may be asked to select which file corresponds to the LUKS device container.\n\nHit Enter to continue." | fold -w 70 -s) - whiptail --title 'Reencrypt LUKS encrypted container ?' --msgbox "$msg" 0 80 - echo -e "\nEnter the current LUKS Disk Recovery Key passphrase:" - read -r -s luks_current_Disk_Recovery_Key_passphrase - echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase - fi - else - echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase - fi - for luks_container in "${luks_containers[@]}"; do - DEBUG "$luks_container: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." - if ! DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then + DEBUG "$luks_container: Test unlocking with current DRK passphrase..." + if ! DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" \ + --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then whiptail_error --title "$luks_container: Wrong current LUKS Disk Recovery Key passphrase?" --msgbox \ "If you previously changed it and do not remember it, you will have to reinstall the OS from an external drive.\n\nTo do so, place the ISO file and its signature file on root of an external drive, and select Options-> Boot from USB \n\nHit Enter to retry." 0 80 TRACE_FUNC @@ -453,21 +420,36 @@ luks_reencrypt() { continue fi - DEBUG "Test opening ${luks_container} successful. Now testing key slots to determine which holds master key" - DRK_KEYSLOT=-1 - DEBUG "$luks_container: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." - for i in $(seq 0 31); do - DEBUG "Testing key slot $i on $luks_container" - if DO_WITH_DEBUG cryptsetup open --test-passphrase $luks_container --key-slot $i --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then - DRK_KEYSLOT=$i - DEBUG "$luks_container: Found key-slot $DRK_KEYSLOT that can be unlocked with the current passphrase. breaking loop" + # Find the specific keyslot holding the DRK using luksDump (avoids + # brute-forcing all 32 slots). + DEBUG "$luks_container: identifying DRK key slot via luksDump" + luks_version=$(cryptsetup luksDump "$luks_container" | grep "^Version" | cut -d: -f2 | tr -d '[:space:]') + if [ "$luks_version" = "2" ]; then + ks_regex="^[[:space:]]+([0-9]+):[[:space:]]*luks2" + ks_sed='s/^[[:space:]]\+\([0-9]\+\):[[:space:]]*luks2/\1/g' + elif [ "$luks_version" = "1" ]; then + ks_regex="Key Slot ([0-9]+): ENABLED" + ks_sed='s/Key Slot \([0-9]\+\): ENABLED/\1/' + else + WARN "$luks_container: unsupported LUKS version '$luks_version', skipping" + continue + fi + mapfile -t used_keyslots < <(cryptsetup luksDump "$luks_container" | grep -E "$ks_regex" | sed "$ks_sed") + DEBUG "$luks_container: used keyslots: ${used_keyslots[*]}" + + DRK_KEYSLOT="" + for ks in "${used_keyslots[@]}"; do + DEBUG "$luks_container: testing keyslot $ks against DRK passphrase" + if DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" \ + --key-slot "$ks" \ + --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then + DRK_KEYSLOT="$ks" + DEBUG "$luks_container: DRK slot is $DRK_KEYSLOT" break - else - DEBUG "Key slot $i on $luks_container cannot be unlocked with the current passphrase" fi done - if [ $DRK_KEYSLOT -eq -1 ]; then + if [ -z "$DRK_KEYSLOT" ]; then whiptail_error --title "$luks_container: Wrong current LUKS Disk Recovery Key passphrase?" --msgbox \ "If you previously changed it and do not remember it, you will have to reinstall the OS from an external drive.\n\nTo do so, place the ISO file and its signature file on root of an external drive, and select Options-> Boot from USB \n\nHit Enter to retry." 0 80 TRACE_FUNC @@ -487,8 +469,8 @@ luks_reencrypt() { # --force-offline-reencrypt forces the reencryption to be done offline (no read/write operations on the device) # --disable-locks disables the lock feature of cryptsetup, which is enabled by default - echo -e "\nReencrypting $luks_container LUKS encrypted drive content with current Recovery Disk Key passphrase..." - warn "DO NOT POWER DOWN MACHINE, UNPLUG AC OR REMOVE BATTERY DURING REENCRYPTION PROCESS" + STATUS "Reencrypting $luks_container with current Recovery Disk Key passphrase" + WARN "DO NOT POWER DOWN MACHINE, UNPLUG AC OR REMOVE BATTERY DURING REENCRYPTION PROCESS" if ! DO_WITH_DEBUG cryptsetup reencrypt \ --perf-no_read_workqueue --perf-no_write_workqueue \ @@ -509,6 +491,8 @@ luks_reencrypt() { export LUKS fi done + + luks_tpm_reseal_prompt } # Function to change LUKS passphrase @@ -519,33 +503,27 @@ luks_change_passphrase() { luks_containers=($LUKS) TRACE_FUNC DEBUG "luks_containers: ${luks_containers[@]}" - # unset new passphrase to make sure the user enters it and knows what they are setting as the new passphrase! + # Prompt for new passphrase once before the per-container loop. + # test_luks_current_disk_recovery_key_passphrase already set and exported + # luks_current_Disk_Recovery_Key_passphrase and wrote the temp file. unset luks_new_Disk_Recovery_Key_passphrase - - for luks_container in "${luks_containers[@]}"; do - if [ -z "$luks_current_Disk_Recovery_Key_passphrase" ]; then - if [ -f /tmp/secret/luks_current_Disk_Recovery_Key_passphrase ]; then - luks_current_Disk_Recovery_Key_passphrase=$(cat /tmp/secret/luks_current_Disk_Recovery_Key_passphrase) - else - TRACE_FUNC - echo -e "\nEnter the current LUKS Disk Recovery Key passphrase (Configured at OS installation or by OEM):" - read -r luks_current_Disk_Recovery_Key_passphrase - fi - elif [ -z "$luks_new_Disk_Recovery_Key_passphrase" ]; then - whiptail --title 'Changing LUKS Disk Recovery Key passphrase' --msgbox \ - "Please choose a strong passphrase of your own.\n\n**DICEWARE passphrase methodology is STRONGLY ADVISED.**\n\nHit Enter to continue" 0 80 - - echo -e "\nEnter your desired replacement for the actual LUKS Disk Recovery Key passphrase (At least 8 characters long):" - while [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; do - read -r luks_new_Disk_Recovery_Key_passphrase - done + whiptail --title 'Changing LUKS Disk Recovery Key passphrase' --msgbox \ + "Please choose a strong passphrase of your own.\n\n**DICEWARE passphrase methodology is STRONGLY ADVISED.**\n\nHit Enter to continue" 0 80 + while [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; do + INPUT "Enter your new LUKS Disk Recovery Key passphrase (at least 8 characters):" -r luks_new_Disk_Recovery_Key_passphrase + if [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; then + WARN "Passphrase must be at least 8 characters long. Please try again." + unset luks_new_Disk_Recovery_Key_passphrase fi + done - echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase - echo -n "$luks_new_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_new_Disk_Recovery_Key_passphrase + echo -n "$luks_current_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_current_Disk_Recovery_Key_passphrase + echo -n "$luks_new_Disk_Recovery_Key_passphrase" >/tmp/secret/luks_new_Disk_Recovery_Key_passphrase - DEBUG "$luks_container: Test unlocking of LUKS encrypted drive content with current LUKS Disk Recovery Key passphrase..." - if ! DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then + for luks_container in "${luks_containers[@]}"; do + DEBUG "$luks_container: Test unlocking with current DRK passphrase..." + if ! DO_WITH_DEBUG cryptsetup open --test-passphrase "$luks_container" \ + --key-file /tmp/secret/luks_current_Disk_Recovery_Key_passphrase >/dev/null 2>&1; then whiptail_error --title "$luks_container: Wrong current LUKS Disk Recovery Key passphrase?" --msgbox \ "If you previously changed it and do not remember it, you will have to reinstall the OS from an external drive.\n\nTo do so, place the ISO file and its signature file on root of an external drive, and select Options-> Boot from USB \n\nHit Enter to retry." 0 80 TRACE_FUNC @@ -558,14 +536,14 @@ luks_change_passphrase() { continue fi - echo -e "\nChanging $luks_container LUKS encrypted disk passphrase to the new LUKS Disk Recovery Key passphrase..." + STATUS "Changing $luks_container LUKS passphrase to new Disk Recovery Key passphrase" if ! DO_WITH_DEBUG cryptsetup luksChangeKey "$luks_container" --key-file=/tmp/secret/luks_current_Disk_Recovery_Key_passphrase /tmp/secret/luks_new_Disk_Recovery_Key_passphrase; then whiptail_error --title 'Failed to change LUKS passphrase' --msgbox \ "Failed to change the passphrase for $luks_container.\nPlease try again." 0 80 continue fi - echo "Success changing passphrase for $luks_container." + STATUS_OK "Success: passphrase changed for $luks_container" done # Export the new passphrase if all containers were processed successfully @@ -573,6 +551,8 @@ luks_change_passphrase() { export luks_current_Disk_Recovery_Key_passphrase export luks_new_Disk_Recovery_Key_passphrase export LUKS + + luks_tpm_reseal_prompt } # Cleanup LUKS secrets @@ -588,3 +568,26 @@ luks_secrets_cleanup() { unset luks_new_Disk_Recovery_Key_passphrase unset LUKS } + +luks_tpm_reseal_prompt() { + # Warn user that TPM must be resealed before rebooting after LUKS changes + # Only prompt if TPM is enabled AND there's a disk unlock key to reseal + if [ "$CONFIG_TPM" = "y" ] && [ -s /boot/kexec_key_devices.txt ]; then + whiptail_warning --title 'TPM Reseal Required' \ + --menu "LUKS passphrase changed - you MUST generate new TOTP/HOTP secret to reseal the TPM.\n\nOtherwise the system will not boot on next reboot.\n\nWhat would you like to do?" 0 80 2 \ + 'g' ' Generate new TOTP/HOTP secret now' \ + 'r' ' Return to Options menu' \ + 2>/tmp/whiptail || return + local luks_passphrase_change_action + luks_passphrase_change_action=$(cat /tmp/whiptail) + case "$luks_passphrase_change_action" in + g) + # Call TPM/TOTP/HOTP Options menu directly to generate new secret + show_tpm_totp_hotp_options_menu + ;; + r) + return + ;; + esac + fi +} diff --git a/initrd/init b/initrd/init index 73bfde2f8..b5ce8e2a2 100755 --- a/initrd/init +++ b/initrd/init @@ -58,6 +58,18 @@ hwclock -l -s # import global functions . /etc/functions.sh +# Capture coreboot CBMEM console log before Heads extends any PCRs. +# This records everything firmware did (coreboot measurements, TPM init by +# coreboot, CBFS reads) so measuring_trace.log has the full chain from firmware +# through Heads. Written directly to avoid flooding the console via INFO. +if [ "$CONFIG_COREBOOT" = "y" ] && [ -x /bin/cbmem ]; then + { + echo "=== coreboot CBMEM console (captured before Heads PCR extensions) ===" + cbmem -L 2>/dev/null + echo "=== End coreboot CBMEM console ===" + } >>/tmp/measuring_trace.log +fi + # export user related content from cbfs if [ "$CONFIG_COREBOOT" = "y" ]; then /bin/cbfs-init.sh @@ -102,27 +114,27 @@ if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then TRACE_FUNC dmesg -n 8 DEBUG "Full debug output enabled from this point: output both in dmesg and on console (equivalent of passing debug to kernel cmdline)" - DEBUG "NOTE: DO_WITH_DEBUG std_err and std_out will be redirected to /tmp/debug.log" + DEBUG "DO_WITH_DEBUG std_err and std_out will be redirected to /tmp/debug.log" fi -# report if we are in quiet mode, tell user measurements logs available under /tmp/debug.log +# report if we are in quiet mode, tell user measurements logs available under /tmp/measuring_trace.log if [ "$CONFIG_QUIET_MODE" = "y" ]; then # check origin of quiet mode setting =y: if it is under /etc/config.user then early cbfs-init outputs are not suppressible # if it is under /etc/config then early cbfs-init outputs are suppressible if grep -q 'CONFIG_QUIET_MODE="y"' /etc/config 2>/dev/null; then - echo "Quiet mode enabled from board configuration: refer to '/tmp/debug.log' for boot measurements traces" >/dev/tty0 + NOTE "Quiet mode enabled from board configuration: refer to /tmp/measuring_trace.log for boot measurement traces" else - echo "Runtime applied Quiet mode: refer to '/tmp/debug.log' for additional boot measurements traces past this point" >/dev/tty0 - echo "To suppress earlier boot measurements traces, enable CONFIG_QUIET_MODE=y in your board configuration at build time." >/dev/tty0 + NOTE "Runtime applied Quiet mode: refer to /tmp/measuring_trace.log for boot measurement traces past this point" + NOTE "To suppress earlier boot measurement traces, enable CONFIG_QUIET_MODE=y in your board configuration at build time" fi # If CONFIG_QUIET_MODE enabled in board config but disabled from Config->Configuration Settings -# warn that early boot measurements output was suppressed prior of this point +# WARN that early boot measurements output was suppressed prior of this point elif [ "$CONFIG_QUIET_MODE" = "n" ]; then # if CONFIG_QUIET_MODE=n in /etc/config.user but CONFIG_QUIET_MODE=y in /etc/config then early cbfs-init outputs are suppressed # both needs to be checked to determine if early boot measurements traces were suppressed if grep -q 'CONFIG_QUIET_MODE="y"' /etc/config 2>/dev/null && grep -q 'CONFIG_QUIET_MODE="n"' /etc/config.user 2>/dev/null; then - echo "Early boot measurements traces were suppressed per CONFIG_QUIET_MODE=y in your board configuration at build time (/etc/config)" >/dev/tty0 - echo "Runtime applied Quiet mode disabled: refer to '/tmp/debug.log' for cbfs-init related traces prior of this point" >/dev/tty0 + NOTE "Early boot measurement traces were suppressed per CONFIG_QUIET_MODE=y in your board configuration at build time (/etc/config)" + NOTE "Runtime applied Quiet mode disabled: refer to /tmp/measuring_trace.log for cbfs-init related traces prior of this point" fi fi @@ -130,15 +142,11 @@ TRACE_FUNC # make sure we have sysctl requirements if [ ! -d /proc/sys ]; then - warn "BUG!!! The following requirements to apply runtime kernel tweaks are missing:" - warn "CONFIG_SYSCTL=y" - warn "CONFIG_PROC_SYSCTL=y" - warn "Please open an issue" + WARN "BUG: Kernel is missing CONFIG_SYSCTL=y / CONFIG_PROC_SYSCTL=y - runtime kernel tweaks cannot be applied. Please open an issue" fi if [ ! -e /proc/sys/vm/panic_on_oom ]; then - warn "BUG!!! Requirements to setup Panic when under Out Of Memory situation through PROC_SYSCTL are missing (panic_on_oom was not enabled)" - warn "Please open an issue" + WARN "BUG: panic_on_oom could not be enabled - CONFIG_PROC_SYSCTL requirements missing. Please open an issue" else DEBUG "Applying panic_on_oom setting to sysctl" echo 1 >/proc/sys/vm/panic_on_oom @@ -163,6 +171,7 @@ fi if [ "$CONFIG_TPM" = "y" ]; then # Initialize tpm2 encrypted sessions here + STATUS "Initializing TPM encrypted session" tpmr.sh startsession fi @@ -181,9 +190,13 @@ export GPG_TTY=/dev/console # Setup recovery serial shell if [ ! -z "$CONFIG_BOOT_RECOVERY_SERIAL" ]; then stty -F "$CONFIG_BOOT_RECOVERY_SERIAL" 115200 - pause_recovery 'Serial console recovery shell' \ - <"$CONFIG_BOOT_RECOVERY_SERIAL" \ - >"$CONFIG_BOOT_RECOVERY_SERIAL" 2>&1 & + # Run in a subshell so RECOVERY_TTY doesn't leak into init's environment. + # Use <> (read-write open) to avoid EOF/SIGHUP on the read side and so + # recovery() can reopen the TTY fresh for each bash respawn. + ( + RECOVERY_TTY="$CONFIG_BOOT_RECOVERY_SERIAL" + pause_recovery 'Serial console recovery shell' + ) <>"$CONFIG_BOOT_RECOVERY_SERIAL" >&0 2>&0 & fi # load USB modules for boards using a USB keyboard @@ -210,14 +223,14 @@ if [ "$boot_option" = "r" ]; then exit elif [ "$boot_option" = "o" ]; then # Launch OEM Factory Reset mode - echo -e "***** Entering OEM Factory Reset mode\n" >/dev/tty0 + STATUS "Entering OEM Factory Reset mode" oem-factory-reset.sh --mode oem # just in case... exit fi if [ "$CONFIG_BASIC" = "y" ]; then - echo -e "***** BASIC mode: tamper detection disabled\n" >/dev/tty0 + NOTE "BASIC mode: tamper detection disabled" fi # export firmware version @@ -242,6 +255,7 @@ fi # Perform board-specific init if present if [ -x /bin/board-init.sh ]; then + STATUS "Performing board-specific initialization" /bin/board-init.sh fi @@ -249,14 +263,14 @@ if [ ! -x "$CONFIG_BOOTSCRIPT" -a ! -x "$CONFIG_BOOTSCRIPT_NETWORK" ]; then recovery 'Boot script missing? Entering recovery shell' else if [ -x "$CONFIG_BOOTSCRIPT_NETWORK" ]; then - echo '***** Network Boot:' $CONFIG_BOOTSCRIPT_NETWORK + STATUS "Network Boot: $CONFIG_BOOTSCRIPT_NETWORK" $CONFIG_BOOTSCRIPT_NETWORK - echo '***** Network Boot Completed:' $CONFIG_BOOTSCRIPT_NETWORK + STATUS "Network Boot completed: $CONFIG_BOOTSCRIPT_NETWORK" # not blocking fi if [ -x "$CONFIG_BOOTSCRIPT" ]; then - echo '***** Normal boot:' $CONFIG_BOOTSCRIPT + STATUS "Normal boot: $CONFIG_BOOTSCRIPT" if [ -x /bin/setsid ] && [ -x /bin/agetty ]; then for console in $CONFIG_BOOT_EXTRA_TTYS; do @@ -264,8 +278,15 @@ else done fi - #Setup a control tty so that all terminals outputs correct tty when tty is called - exec cttyhack "$CONFIG_BOOTSCRIPT" + # Run the boot script under a respawn loop so that a DIE/exit inside + # gui-init (or any script it calls) does not kill PID 1 and oops the + # kernel. cttyhack is NOT exec'd — /init stays as PID 1. + while true; do + cttyhack "$CONFIG_BOOTSCRIPT" + _rc=$? + WARN "$CONFIG_BOOTSCRIPT exited (code $_rc) - respawning" + sleep 2 + done else # wait for boot via network to occur pause_recovery 'Override network boot. Entering recovery shell' diff --git a/initrd/mount-boot.sh b/initrd/mount-boot.sh index be02e08d8..18ea1da58 100755 --- a/initrd/mount-boot.sh +++ b/initrd/mount-boot.sh @@ -4,6 +4,8 @@ # the trusted key database, and execute it to mount # the /boot filesystem +. /etc/functions.sh + dev="$1" offset="$2" @@ -24,11 +26,7 @@ fi # dev_size_file="/sys/class/block/`basename $dev`/size" if [ ! -r "$dev_size_file" ]; then - echo >&2 '!!!!!' - echo >&2 '!!!!! $dev file $dev_size_file not found' - echo >&2 '!!!!! Dropping to recovery shell' - echo >&2 '!!!!!' - exit -1 + recovery "Device size file $dev_size_file not found" fi dev_blocks=`cat "$dev_size_file"` @@ -37,22 +35,14 @@ dev_blocks=`cat "$dev_size_file"` # Extract the signed file from the hard disk image # if ! dd if="$dev" of="$cmd_sig" bs=512 skip="`expr $dev_blocks - 1`" > /dev/null 2>&1; then - echo >&2 '!!!!!' - echo >&2 '!!!!! Boot block extraction failed' - echo >&2 '!!!!! Dropping to recovery shell' - echo >&2 '!!!!!' - exit -1 + recovery "Boot block extraction failed" fi # # Validate the file # -if ! gpgv --keyring /trustedkeys.gpg "$cmd_sig"; then - echo >&2 '!!!!!' - echo >&2 '!!!!! GPG signature on block failed' - echo >&2 '!!!!! Dropping to recovery shell' - echo >&2 '!!!!!' - exit -1 +if ! gpgv.sh --keyring /trustedkeys.gpg "$cmd_sig"; then + recovery "GPG signature on boot block failed" fi # diff --git a/initrd/sbin/config-dhcp.sh b/initrd/sbin/config-dhcp.sh index 6dcb8297b..a44b71848 100755 --- a/initrd/sbin/config-dhcp.sh +++ b/initrd/sbin/config-dhcp.sh @@ -1,6 +1,7 @@ #!/bin/bash # udhcpc script +. /etc/functions.sh [ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1 @@ -24,7 +25,7 @@ case "$1" in /sbin/ifconfig $interface $ip $BROADCAST $NETMASK if [ -n "$router" ] ; then - echo "deleting routers" + DEBUG "deleting routers" while route del default gw 0.0.0.0 dev $interface ; do : done @@ -37,7 +38,7 @@ case "$1" in echo -n > $RESOLV_CONF [ -n "$domain" ] && echo search $domain >> $RESOLV_CONF for i in $dns ; do - echo adding dns $i + DEBUG "adding dns $i" echo nameserver $i >> $RESOLV_CONF done ;; diff --git a/initrd/sbin/insmod.sh b/initrd/sbin/insmod.sh index 785d87726..9e89197fc 100755 --- a/initrd/sbin/insmod.sh +++ b/initrd/sbin/insmod.sh @@ -8,22 +8,22 @@ TRACE_FUNC -MODULE="$1" -shift +MODULE="$1"; shift if [ -z "$MODULE_PCR" ]; then MODULE_PCR=5 fi + if [ -z "$MODULE" ]; then - die "Usage: $0 module [args...]" + DIE "Usage: $0 module [args...]" fi if [ ! -r "$MODULE" ]; then - die "$MODULE: not found?" + DIE "$MODULE: not found?" fi -# Check if module is already loaded +# Check if module is already loaded # Transform module name changing _ for - and trailing .ko if present # Unify lsmod output to use - instead of _ for comparison module_name=$(basename "$MODULE" | sed 's/_/-/g' | sed 's/\.ko$//') @@ -45,20 +45,20 @@ if [ -z "$tpm_missing" ]; then # different PCR measurement. if [ -n "$*" ]; then TRACE_FUNC - INFO "Extending with module parameters and the module's content" + LOG "Extending with module parameters and the module's content" tpmr.sh extend -ix "$MODULE_PCR" -ic "$*" - tpmr.sh extend -ix "$MODULE_PCR" -if "$MODULE" || - die "$MODULE: tpm extend failed" + tpmr.sh extend -ix "$MODULE_PCR" -if "$MODULE" \ + || DIE "$MODULE: tpm extend failed" else TRACE_FUNC - INFO "No module parameters, extending only with the module's content" - tpmr.sh extend -ix "$MODULE_PCR" -if "$MODULE" || - die "$MODULE: tpm extend failed" + LOG "No module parameters, extending only with the module's content" + tpmr.sh extend -ix "$MODULE_PCR" -if "$MODULE" \ + || DIE "$MODULE: tpm extend failed" fi fi -# Since we have replaced the real insmod, we must invoke +# Since we have replaced the real insmod.sh, we must invoke # the busybox insmod via the original executable DEBUG "Loading $MODULE with busybox insmod" -busybox insmod "$MODULE" "$@" || - die "$MODULE: insmod failed" +busybox insmod "$MODULE" "$@" \ + || DIE "$MODULE: insmod failed" From eb84f1b52f8ad25f012eda2f5e96e8012251dfc3 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 31 Mar 2026 23:17:53 -0400 Subject: [PATCH 04/12] gui-init: add integrity gate for reseal/reset paths; add dongle branding This is the core commit of PR #2068. It introduces: Integrity gating (gui-init.sh): - gate_reseal_with_integrity_report(): blocks reseal/reset unless /boot integrity is confirmed; set INTEGRITY_GATE_REQUIRED=y on TOTP/HOTP failure to trigger the gate before any signing or secret-sealing - report_integrity_measurements(): shows /boot hash state (OK/CHANGED/UNKNOWN) - investigate_integrity_discrepancies(): guided flow when hashes mismatch, letting the user inspect changed files before deciding to re-sign - tpm_reset_required() guard in update_checksums() and gate_reseal_with_integrity_report(): forces TPM reset before signing if the rollback counter is broken/absent - LUKS_PARTITION_DETECTED reuse to distinguish "no /boot" from "no OS" and route user to the correct recovery path Dongle branding (gui-init.sh, oem-factory-reset.sh, gpg-gui.sh): - DONGLE_BRAND set from detect_usb_security_dongle_branding() (VID:PID); displayed in menu headers, HOTP prompts and error messages - hotpkey_fw_display() called for firmware version in HOTP prompts - oem-factory-reset.sh: STATUS messages use DONGLE_BRAND; adds Nitrokey 3 Secrets app reset GPG / kexec signing: - gpg-gui.sh: refactored to use shared gpg_functions.sh - kexec-seal-key.sh: LUKS DUK setup with per-device unlock validation, partial-device handling, and 3-attempt recovery - kexec-unseal-key.sh: STATUS/WARN output for unlock flow General: - config-gui.sh, flash-gui.sh: improved prompts and error handling - All scripts: tabs indentation, die() -> DIE() Signed-off-by: Thierry Laurion --- initrd/bin/basic-autoboot.sh | 2 +- initrd/bin/cbfs-init.sh | 50 +- initrd/bin/change-time.sh | 33 +- initrd/bin/config-gui.sh | 140 ++-- initrd/bin/flash-gui.sh | 252 +++--- initrd/bin/flash.sh | 24 +- initrd/bin/flashprog-kgpe-d16-openbmc.sh | 6 +- initrd/bin/generic-init.sh | 22 +- initrd/bin/gpg-gui.sh | 289 ++----- initrd/bin/gui-init-basic.sh | 321 ++++---- initrd/bin/gui-init.sh | 663 ++++++++++++---- initrd/bin/inject_firmware.sh | 2 +- initrd/bin/kexec-boot.sh | 28 +- initrd/bin/kexec-insert-key.sh | 61 +- initrd/bin/kexec-iso-init.sh | 43 +- initrd/bin/kexec-parse-bls.sh | 2 +- initrd/bin/kexec-parse-boot.sh | 2 +- initrd/bin/kexec-save-default.sh | 226 +++--- initrd/bin/kexec-save-key.sh | 49 +- initrd/bin/kexec-seal-key.sh | 261 +++--- initrd/bin/kexec-select-boot.sh | 170 ++-- initrd/bin/kexec-sign-config.sh | 153 +++- initrd/bin/kexec-unseal-key.sh | 12 +- initrd/bin/key-init.sh | 19 +- initrd/bin/lock_chip.sh | 5 +- initrd/bin/media-scan.sh | 65 +- initrd/bin/mount-usb.sh | 284 ++++--- initrd/bin/network-init-recovery.sh | 79 +- initrd/bin/oem-factory-reset.sh | 564 ++++++------- initrd/bin/qubes-measure-luks.sh | 8 +- initrd/bin/reboot.sh | 20 +- initrd/bin/root-hashes-gui.sh | 964 ++++++++++++----------- initrd/bin/seal-hotpkey.sh | 140 ++-- initrd/bin/seal-totp.sh | 25 +- initrd/bin/tpm-reset.sh | 4 +- initrd/bin/tpmr.sh | 276 +++++-- initrd/bin/uefi-init.sh | 24 +- initrd/bin/unpack_initramfs.sh | 4 +- initrd/bin/unseal-hotp.sh | 32 +- initrd/bin/unseal-totp.sh | 44 +- initrd/bin/usb-autoboot.sh | 13 +- initrd/bin/usb-init.sh | 4 +- initrd/bin/wget-measure.sh | 12 +- initrd/bin/wipe-totp.sh | 2 +- 44 files changed, 2995 insertions(+), 2404 deletions(-) diff --git a/initrd/bin/basic-autoboot.sh b/initrd/bin/basic-autoboot.sh index 060fc2f41..e44ded2d5 100755 --- a/initrd/bin/basic-autoboot.sh +++ b/initrd/bin/basic-autoboot.sh @@ -7,5 +7,5 @@ BOOT_MENU_OPTIONS=/tmp/basic-autoboot-options scan_boot_options /boot "grub.cfg" "$BOOT_MENU_OPTIONS" if [ -s "$BOOT_MENU_OPTIONS" ]; then - kexec-boot -b /boot -e "$(head -1 "$BOOT_MENU_OPTIONS")" + kexec-boot.sh -b /boot -e "$(head -1 "$BOOT_MENU_OPTIONS")" fi diff --git a/initrd/bin/cbfs-init.sh b/initrd/bin/cbfs-init.sh index 8b4e96e31..27b4601dc 100755 --- a/initrd/bin/cbfs-init.sh +++ b/initrd/bin/cbfs-init.sh @@ -2,12 +2,19 @@ set -e -o pipefail . /etc/functions.sh -# CBFS extraction and measurement -# This extraction and measurement cannot be suppressed by quiet mode, since -# config.user is not yet loaded at this point. -# To suppress this output, set CONFIG_QUIET_MODE=y needs be be set in /etc/config -# which is defined at build time under board configuration file to be part of initrd.cpio -# This script is called from initrd/init so really early in the boot process to put files in place in initramfs +# Board key and configuration injection from CBFS +# At build time, board-specific trusted keys, certificates, and configuration +# are injected into the firmware CBFS (heads/initrd/ namespace). cbfs-init +# extracts these into the running initramfs and measures each file into the +# TPM to establish the board's trust chain before anything else runs. +# +# cbfs-init runs before config.user is extracted and merged into /tmp/config, +# so logging here is governed by the board build-time /etc/config only. +# STATUS and STATUS_OK are always visible regardless of CONFIG_QUIET_MODE and +# are used for the summary bracket so the user always sees progress. +# Per-file detail is at DEBUG level (developer-facing). +# INFO calls (TPM PCR measurements) are suppressed in quiet mode - that is +# intentional, since quiet boards have CONFIG_QUIET_MODE=y in /etc/config. TRACE_FUNC @@ -18,31 +25,36 @@ fi if [ "$CONFIG_CBFS_VIA_FLASHPROG" = "y" ]; then # Use flashrom directly, because we don't have /tmp/config with params for flash.sh yet - /bin/flashprog -p internal --fmap -i COREBOOT -i FMAP -r /tmp/cbfs-init.rom && - CBFS_ARG=" -o /tmp/cbfs-init.rom" || - echo "Failed reading Heads configuration from flash! Some features may not be available." + STATUS "Reading board keys and configuration from SPI flash" + if /bin/flashprog -p internal --fmap -i COREBOOT -i FMAP -r /tmp/cbfs-init.rom; then + CBFS_ARG=" -o /tmp/cbfs-init.rom" + else + WARN "Failed to read board keys and configuration from SPI flash - some features may not be available" + fi fi # Load individual files -cbfsfiles=$(cbfs -t 50 -l $CBFS_ARG 2>/dev/null | grep "^heads/initrd/") +cbfsfiles=`cbfs -t 50 -l $CBFS_ARG 2>/dev/null | grep "^heads/initrd/"` -for cbfsname in $(echo $cbfsfiles); do +STATUS "Extracting GPG keyring, trustdb, and board configuration from firmware" +for cbfsname in `echo $cbfsfiles`; do filename=${cbfsname:12} if [ ! -z "$filename" ]; then - mkdir -p $(dirname $filename) || - die "$filename: mkdir failed" - INFO "Extracting CBFS file $cbfsname into $filename" - cbfs -t 50 $CBFS_ARG -r $cbfsname >"$filename" || - die "$filename: cbfs file read failed" + mkdir -p `dirname $filename` \ + || DIE "$filename: mkdir failed" + DEBUG "Extracting $cbfsname from firmware CBFS" + cbfs -t 50 $CBFS_ARG -r $cbfsname > "$filename" \ + || DIE "$filename: cbfs file read failed" if [ "$CONFIG_TPM" = "y" ]; then TRACE_FUNC - INFO "TPM: Extending PCR[$CONFIG_PCR] with filename $filename and then its content" + INFO "Measuring $filename into TPM PCR[$CONFIG_PCR]" # Measure both the filename and its content. This # ensures that renaming files or pivoting file content # will still affect the resulting PCR measurement. tpmr.sh extend -ix "$CONFIG_PCR" -ic "$filename" - tpmr.sh extend -ix "$CONFIG_PCR" -if "$filename" || - die "$filename: tpm extend failed" + tpmr.sh extend -ix "$CONFIG_PCR" -if "$filename" \ + || DIE "$filename: tpm extend failed" fi fi done +STATUS_OK "Board keys and configuration loaded from firmware" diff --git a/initrd/bin/change-time.sh b/initrd/bin/change-time.sh index b5d2a4ffe..f145a1ff0 100755 --- a/initrd/bin/change-time.sh +++ b/initrd/bin/change-time.sh @@ -1,30 +1,26 @@ #!/bin/bash #change time using hwclock and date -s +. /etc/functions.sh +. /tmp/config clear -echo "The system time is: $(date "+%Y-%m-%d %H:%M:%S %Z")" -echo -echo "Please enter the current date and time in UTC" -echo "To find the current date and time in UTC, please check https://time.is/UTC" -echo +STATUS "System time: $(date "+%Y-%m-%d %H:%M:%S %Z")" +STATUS "Please enter the current date and time in UTC" +INFO "To find the current UTC time: https://time.is/UTC" get_date () { local field_name min max field_name="$1" min="$2" max="$3" - echo -n "Enter the current $field_name [$min-$max]: " - read -r value - echo + INPUT "Enter the current $field_name [$min-$max]:" -r value #must be a number between $2 and $3 while [[ ! $value =~ ^[0-9]+$ ]] || [[ ${value#0} -lt $min ]] || [[ ${value#0} -gt $max ]]; do - echo "Please try again, it must be a number from $min to $max." - echo -n "Enter the current $field_name [$min-$max]: " - read -r value - echo + WARN "Please try again, it must be a number from $min to $max." + INPUT "Enter the current $field_name [$min-$max]:" -r value done # Pad with zeroes to length of maximum value. @@ -56,18 +52,13 @@ enter_time_and_change() } while ! enter_time_and_change; do - echo "Could not set the date to $year-$month-$day $hour:$min:$sec" - read -rp "Try again? [Y/n]: " try_again_confirm + WARN "Could not set the date to $year-$month-$day $hour:$min:$sec" + INPUT "Try again? [Y/n]:" -n 1 -r try_again_confirm if [ "${try_again_confirm^^}" = N ]; then exit 1 fi - echo done hwclock -w -echo "The system date has been sucessfully set to $year-$month-$day $hour:$min:$sec UTC" -echo - -echo "Press Enter to return to the menu" -echo -read -r nothing +STATUS_OK "System date set to $year-$month-$day $hour:$min:$sec UTC" +INPUT "Press Enter to return to the menu" diff --git a/initrd/bin/config-gui.sh b/initrd/bin/config-gui.sh index 4ca6b7a7b..fc1b7c3a7 100755 --- a/initrd/bin/config-gui.sh +++ b/initrd/bin/config-gui.sh @@ -76,7 +76,7 @@ while true; do # USB keyboard support always loads. [ "$CONFIG_USB_KEYBOARD_REQUIRED" != y ] && dynamic_config_options+=( 'K' " $(get_config_display_action "$CONFIG_USER_USB_KEYBOARD") USB keyboard" - ) + ) # Add keyboard keymap selection option only if loadkeys and keymaps exist if [ -x /bin/loadkeys ] && [ -d /usr/lib/kbd/keymaps ]; then @@ -160,7 +160,7 @@ while true; do set_config /etc/config.user "CONFIG_BOOT_DEV" "$SELECTED_FILE" combine_configs - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The /boot device was successfully changed to $SELECTED_FILE" 0 80 ;; "s") @@ -171,9 +171,9 @@ while true; do if (whiptail --title 'Update ROM?' \ --yesno "This will reflash your BIOS with the updated version\n\nDo you want to proceed?" 0 80); then /bin/flash.sh /tmp/config-gui.rom - whiptail --title 'BIOS Updated Successfully' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'BIOS Updated Successfully' \ --msgbox "BIOS updated successfully.\n\nIf your keys have changed, be sure to re-sign all files in /boot\nafter you reboot.\n\nPress Enter to reboot" 0 80 - /bin/reboot + /bin/reboot.sh else exit 0 fi @@ -204,11 +204,11 @@ while true; do # reset TPM if present if [ "$CONFIG_TPM" = "y" ]; then - /bin/tpm-reset + /bin/tpm-reset.sh fi - whiptail --title 'Configuration Reset Updated Successfully' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Configuration Reset Updated Successfully' \ --msgbox "Configuration reset and BIOS updated successfully.\n\nPress Enter to reboot" 0 80 - /bin/reboot + /bin/reboot.sh else exit 0 fi @@ -239,28 +239,23 @@ while true; do set_config /etc/config.user "CONFIG_ROOT_DEV" "$SELECTED_FILE" combine_configs - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The root device was successfully changed to $SELECTED_FILE" 0 80 ;; "D") CURRENT_OPTION="$(load_config_value CONFIG_ROOT_DIRLIST)" - # Separate from prior prompt history on the terminal with two blanks - echo -e "\n" - if [ -n "$CURRENT_OPTION" ]; then - echo -e "The current list of directories to hash is $CURRENT_OPTION" + INFO "The current list of directories to hash is $CURRENT_OPTION" fi - echo -e "Enter the new list of directories separated by spaces:" - echo -e "(Press enter with the list empty to cancel)" - read -r NEW_CONFIG_ROOT_DIRLIST + INPUT "Enter the new list of directories separated by spaces (empty to cancel):" -r NEW_CONFIG_ROOT_DIRLIST # strip any leading forward slashes NEW_CONFIG_ROOT_DIRLIST=$(echo $NEW_CONFIG_ROOT_DIRLIST | sed -e 's/^\///;s/ \// /g') #check if list empty if [ -z "$NEW_CONFIG_ROOT_DIRLIST" ]; then - whiptail --title 'Config change canceled' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change canceled' \ --msgbox "Root device directory change canceled by user" 0 80 break fi @@ -268,7 +263,7 @@ while true; do set_config /etc/config.user "CONFIG_ROOT_DIRLIST" "$NEW_CONFIG_ROOT_DIRLIST" combine_configs - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The root directories to hash was successfully changed to:\n$NEW_CONFIG_ROOT_DIRLIST" 0 80 ;; "B") @@ -294,7 +289,7 @@ while true; do fi fi - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The root device will be checked at each boot." 0 80 fi @@ -305,7 +300,7 @@ while true; do set_user_config "CONFIG_ROOT_CHECK_AT_BOOT" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "The root device will not be checked at each boot." 0 80 fi fi @@ -322,7 +317,7 @@ while true; do set_user_config "CONFIG_BASIC" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "$CONFIG_BRAND_NAME Basic mode enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi @@ -334,7 +329,7 @@ while true; do set_user_config "CONFIG_BASIC" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "$CONFIG_BRAND_NAME Basic mode has been disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -354,7 +349,7 @@ while true; do set_user_config "CONFIG_RESTRICTED_BOOT" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Restricted Boot mode enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi @@ -370,7 +365,7 @@ while true; do # Restricted Boot again might restore the firmware to an identical # state, and there would be no evidence that it had been temporarily # disabled. - if ! wipe-totp >/dev/null 2>/tmp/error; then + if ! wipe-totp.sh >/dev/null 2>/tmp/error; then ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error --title 'ERROR: erasing TOTP secret' \ --msgbox "Erasing TOTP Secret Failed\n\n${ERROR}" 0 80 @@ -390,9 +385,9 @@ while true; do replace_rom_file /tmp/config-gui.rom "heads/initrd/etc/config.user" "$FLASH_USER_CONFIG" /bin/flash.sh /tmp/config-gui.rom - whiptail --title 'BIOS Updated Successfully' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'BIOS Updated Successfully' \ --msgbox "BIOS updated successfully.\n\nIf your keys have changed, be sure to re-sign all files in /boot\nafter you reboot.\n\nPress Enter to reboot" 0 80 - /bin/reboot + /bin/reboot.sh fi fi ;; @@ -404,7 +399,7 @@ while true; do set_user_config "CONFIG_USE_BLOB_JAIL" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Firmware Blob Jail use has been enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi @@ -415,7 +410,7 @@ while true; do set_user_config "CONFIG_USE_BLOB_JAIL" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Firmware Blob Jail use has been disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -428,7 +423,7 @@ while true; do else current_msg="Currently boots automatically after $CONFIG_AUTO_BOOT_TIMEOUT seconds." fi - whiptail --title "Automatic Boot" \ + whiptail_type $BG_COLOR_MAIN_MENU --title "Automatic Boot" \ --menu "$CONFIG_BRAND_NAME can boot automatically. Select the amount of time to wait\nbefore booting.\n\n$current_msg" 0 80 10 \ "0" "Don't boot automatically" \ "1" "1 second" \ @@ -447,7 +442,7 @@ while true; do current_msg="$CONFIG_BRAND_NAME will boot automatically after $new_setting seconds." fi set_user_config "CONFIG_AUTO_BOOT_TIMEOUT" "$new_setting" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "$current_msg\nSave the config change and reboot for it to go into effect." 0 80 fi ;; @@ -461,7 +456,7 @@ while true; do set_user_config "CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Automatic default boot disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi else @@ -471,7 +466,7 @@ while true; do set_user_config "CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Automatic default boot enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -485,7 +480,7 @@ while true; do set_user_config "CONFIG_BASIC_USB_AUTOBOOT" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "USB automatic boot enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi else @@ -495,7 +490,7 @@ while true; do set_user_config "CONFIG_BASIC_USB_AUTOBOOT" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "USB automatic boot disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -508,7 +503,7 @@ while true; do set_user_config "CONFIG_AUTOMATIC_POWERON" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Automatic power-on enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi else @@ -523,7 +518,7 @@ while true; do # flash this change, we'll enable it again during boot. set_ec_poweron.sh n - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Automatic power-on disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -538,7 +533,7 @@ while true; do set_user_config "CONFIG_USER_USB_KEYBOARD" "y" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "USB Keyboard support has been enabled;\nsave the config change and reboot for it to go into effect." 0 80 fi @@ -549,7 +544,7 @@ while true; do set_user_config "CONFIG_USER_USB_KEYBOARD" "n" - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "USB Keyboard support has been disabled;\nsave the config change and reboot for it to go into effect." 0 80 fi fi @@ -560,7 +555,7 @@ while true; do while true; do # Guide user into finding which keyboard type he has - whiptail --title "Keyboard Layout Type" \ + whiptail_type $BG_COLOR_MAIN_MENU --title "Keyboard Layout Type" \ --menu "Look at the first row of your keyboard and select the layout type:" 0 60 3 \ "qwerty" "QWERTY (most common: US, UK, etc.)" \ "qwertz" "QWERTZ (German, Central Europe)" \ @@ -570,16 +565,16 @@ while true; do layout_choice=$(cat /tmp/whiptail) case "$layout_choice" in - "Cancel"|"") - break - ;; - "qwerty"|"qwertz"|"azerty") - BROWSE_DIR="$KEYMAP_ROOT/i386/$layout_choice" - ;; - *) - whiptail --title "Invalid selection" --msgbox "Invalid layout selection." 0 40 - break - ;; + "Cancel" | "") + break + ;; + "qwerty" | "qwertz" | "azerty") + BROWSE_DIR="$KEYMAP_ROOT/i386/$layout_choice" + ;; + *) + whiptail_error --title "Invalid selection" --msgbox "Invalid layout selection." 0 40 + break + ;; esac while true; do @@ -593,11 +588,11 @@ while true; do menu_entries+=("Cancel" "Cancel") if [ ${#menu_entries[@]} -le 2 ]; then - whiptail --title "No keymaps" --msgbox "No keymaps found in $BROWSE_DIR." 0 60 + whiptail_error --title "No keymaps" --msgbox "No keymaps found in $BROWSE_DIR." 0 60 break fi - whiptail --title "Select Keymap" \ + whiptail_type $BG_COLOR_MAIN_MENU --title "Select Keymap" \ --menu "Select a keymap file for $layout_choice layout.\n\n(Current: ${CURRENT_KEYMAP:-none})" 0 80 18 \ "${menu_entries[@]}" 2>/tmp/whiptail || break @@ -611,18 +606,13 @@ while true; do elif [[ "$choice" == *.map ]]; then SELECTED_KEYMAP="$BROWSE_DIR/$choice" load_keymap "$SELECTED_KEYMAP" - echo - echo "------------------------------------------------------------" - echo "Keymap loaded: $SELECTED_KEYMAP" - echo - echo "You can now test your keyboard layout in this shell." - echo "Press Enter when done testing to continue..." - echo "------------------------------------------------------------" - read -p $'\nTest your keymap now. Press Enter to continue:\n' dummy + STATUS_OK "Keymap loaded: $SELECTED_KEYMAP" + INFO "You can now test your keyboard layout in this shell." + INPUT "Test your keymap now. Press Enter to continue:" dummy if whiptail --title "Keep this keymap?" \ --yesno "Do you want to use this keymap?\n\n$SELECTED_KEYMAP" 0 70; then set_user_config "CONFIG_KEYBOARD_KEYMAP" "$SELECTED_KEYMAP" - whiptail --title "Keymap set" --msgbox "Keymap set to:\n\n$SELECTED_KEYMAP\n\nSave the config change and reboot for it to go into effect." 0 70 + whiptail_type $BG_COLOR_MAIN_MENU --title "Keymap set" --msgbox "Keymap set to:\n\n$SELECTED_KEYMAP\n\nSave the config change and reboot for it to go into effect." 0 70 break 2 fi load_keymap "$CURRENT_KEYMAP" @@ -641,23 +631,23 @@ while true; do output_choice=$(cat /tmp/whiptail) case "$output_choice" in - 0) - set_user_config "CONFIG_DEBUG_OUTPUT" "n" - set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "n" - set_user_config "CONFIG_QUIET_MODE" "y" - ;; - 1) - set_user_config "CONFIG_DEBUG_OUTPUT" "n" - set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "n" - set_user_config "CONFIG_QUIET_MODE" "n" - ;; - 2) - set_user_config "CONFIG_DEBUG_OUTPUT" "y" - set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "y" - set_user_config "CONFIG_QUIET_MODE" "n" - ;; + 0) + set_user_config "CONFIG_DEBUG_OUTPUT" "n" + set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "n" + set_user_config "CONFIG_QUIET_MODE" "y" + ;; + 1) + set_user_config "CONFIG_DEBUG_OUTPUT" "n" + set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "n" + set_user_config "CONFIG_QUIET_MODE" "n" + ;; + 2) + set_user_config "CONFIG_DEBUG_OUTPUT" "y" + set_user_config "CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT" "y" + set_user_config "CONFIG_QUIET_MODE" "n" + ;; esac - whiptail --title 'Config change successful' \ + whiptail_type $BG_COLOR_MAIN_MENU --title 'Config change successful' \ --msgbox "Output level changed.\nSave the config change and reboot for it to go into effect." 0 80 ;; esac diff --git a/initrd/bin/flash-gui.sh b/initrd/bin/flash-gui.sh index 4822b0ea6..1fb7c3a6b 100755 --- a/initrd/bin/flash-gui.sh +++ b/initrd/bin/flash-gui.sh @@ -8,9 +8,9 @@ set -e -o pipefail TRACE_FUNC if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then - whiptail_error --title 'Restricted Boot Active' \ - --msgbox "Disable Restricted Boot to flash new firmware." 0 80 - exit 1 + whiptail_error --title 'Restricted Boot Active' \ + --msgbox "Disable Restricted Boot to flash new firmware." 0 80 + exit 1 fi # Most boards use a .rom file as a "plain" update, contents of the BIOS flash @@ -20,7 +20,7 @@ UPDATE_PLAIN_EXT=rom # workflow (as-is, a .tgz could be inside that package in theory) but more work # would be needed to properly integrate it. if [ "${CONFIG_BOARD%_*}" = talos-2 ]; then - UPDATE_PLAIN_EXT=tgz + UPDATE_PLAIN_EXT=tgz fi # Check that a glob matches exactly one thing. If so, echoes the single value. @@ -33,140 +33,140 @@ fi # echo "Failed to find a ROM" >&2 # fi single_glob() { - if [ "$#" -eq 1 ] && [ -f "$1" ]; then - echo "$1" - else - return 1 - fi + if [ "$#" -eq 1 ] && [ -f "$1" ]; then + echo "$1" + else + return 1 + fi } while true; do - unset menu_choice - whiptail_type $BG_COLOR_MAIN_MENU --title "Firmware Management Menu" \ - --menu "Select the firmware function to perform\n\nRetaining settings copies existing settings to the new firmware:\n* Keeps your GPG keyring\n* Keeps changes to the default /boot device\n\nErasing settings uses the new firmware as-is:\n* Erases any existing GPG keyring\n* Restores firmware to default factory settings\n* Clears out /boot signatures\n\nIf you are just updating your firmware, you probably want to retain\nyour settings." 0 80 10 \ - 'f' ' Flash the firmware with a new ROM, retain settings' \ - 'c' ' Flash the firmware with a new ROM, erase settings' \ - 'x' ' Exit' \ - 2>/tmp/whiptail || recovery "GUI menu failed" + unset menu_choice + whiptail_type $BG_COLOR_MAIN_MENU --title "Firmware Management Menu" \ + --menu "Select the firmware function to perform\n\nRetaining settings copies existing settings to the new firmware:\n* Keeps your GPG keyring\n* Keeps changes to the default /boot device\n\nErasing settings uses the new firmware as-is:\n* Erases any existing GPG keyring\n* Restores firmware to default factory settings\n* Clears out /boot signatures\n\nIf you are just updating your firmware, you probably want to retain\nyour settings." 0 80 10 \ + 'f' ' Flash the firmware with a new ROM, retain settings' \ + 'c' ' Flash the firmware with a new ROM, erase settings' \ + 'x' ' Exit' \ + 2>/tmp/whiptail || recovery "GUI menu failed" - menu_choice=$(cat /tmp/whiptail) + menu_choice=$(cat /tmp/whiptail) - case "$menu_choice" in - "x") - exit 0 - ;; - f | c) - if (whiptail_warning --title 'Flash the BIOS with a new ROM' \ - --yesno "You will need to insert a USB drive containing your BIOS image (*.zip or\n*.$UPDATE_PLAIN_EXT).\n\nAfter you select this file, this program will reflash your BIOS.\n\nDo you want to proceed?" 0 80); then - mount_usb - if grep -q /media /proc/mounts; then - # 'find' parameters to match desired ROM extensions - FIND_ROM_EXTS=(\( -name "*.$UPDATE_PLAIN_EXT" -o -type f -name "*.zip" \)) - if [ "${CONFIG_BOARD%_*}" = talos-2 ]; then - # Show only *.tgz on talos-2 (lacks ZIP update package support) - FIND_ROM_EXTS=(-name "*.$UPDATE_PLAIN_EXT") - fi - # Media errors can cause this to fail (flash drive pulled, filesystem - # corruption, etc.) - if ! find /media ! -path '*/\.*' -type f "${FIND_ROM_EXTS[@]}" | sort >/tmp/filelist.txt; then - whiptail --title 'Unable to read USB drive' \ - --msgbox "The USB drive is not readable. Check the drive, reformat, or try a - \ndifferent drive." 16 60 - exit 1 - fi - file_selector "/tmp/filelist.txt" "Choose the ROM to flash" - if [ "$FILE" == "" ]; then - exit 1 - else - PKG_FILE=$FILE - fi + case "$menu_choice" in + "x") + exit 0 + ;; + f | c) + if (whiptail_warning --title 'Flash the BIOS with a new ROM' \ + --yesno "You will need to insert a USB drive containing your BIOS image (*.zip or\n*.$UPDATE_PLAIN_EXT).\n\nAfter you select this file, this program will reflash your BIOS.\n\nDo you want to proceed?" 0 80); then + mount_usb + if grep -q /media /proc/mounts; then + # 'find' parameters to match desired ROM extensions + FIND_ROM_EXTS=(\( -name "*.$UPDATE_PLAIN_EXT" -o -type f -name "*.zip" \)) + if [ "${CONFIG_BOARD%_*}" = talos-2 ]; then + # Show only *.tgz on talos-2 (lacks ZIP update package support) + FIND_ROM_EXTS=(-name "*.$UPDATE_PLAIN_EXT") + fi + # Media errors can cause this to fail (flash drive pulled, filesystem + # corruption, etc.) + if ! find /media ! -path '*/\.*' -type f "${FIND_ROM_EXTS[@]}" | sort -r >/tmp/filelist.txt; then + whiptail_error --title 'Unable to read USB drive' \ + --msgbox "The USB drive is not readable. Check the drive, reformat, or try a + \ndifferent drive." 0 80 + exit 1 + fi + file_selector "/tmp/filelist.txt" "Choose the ROM to flash" + if [ "$FILE" == "" ]; then + exit 1 + else + PKG_FILE=$FILE + fi - # Display the package file without the "/media/" prefix - PKG_FILE_DISPLAY="${PKG_FILE#"/media/"}" + # Display the package file without the "/media/" prefix + PKG_FILE_DISPLAY="${PKG_FILE#"/media/"}" - # Unzip the package - PKG_EXTRACT="/tmp/flash_gui/update_package" - rm -rf "$PKG_EXTRACT" - mkdir -p "$PKG_EXTRACT" + # Unzip the package + PKG_EXTRACT="/tmp/flash_gui/update_package" + rm -rf "$PKG_EXTRACT" + mkdir -p "$PKG_EXTRACT" - # is an update package provided? - if [ -z "${PKG_FILE##*.zip}" ]; then - # If extraction fails, delete everything and fall through to the - # integrity failure prompt. This is the most likely path if the ROM - # was actually corrupted in transit. Corrupting the ZIP in a way that - # still extracts is possible (the sha256sum detects this) but less - # likely. - unzip "$PKG_FILE" -d "$PKG_EXTRACT" || rm -rf "$PKG_EXTRACT" - # Older packages had /tmp/verified_rom hard-coded in the sha256sum.txt - # Remove that so it's a relative path to the ROM in the package. - # Ignore failure, if there is no sha256sum.txt the sha256sum will fail - sed -i -e 's| /tmp/verified_rom/\+| |g' "$PKG_EXTRACT/sha256sum.txt" || true - # check file integrity - if ! (cd "$PKG_EXTRACT" && sha256sum -cs sha256sum.txt); then - whiptail --title 'ROM Integrity Check Failed! ' \ - --msgbox "Integrity check failed in\n$PKG_FILE_DISPLAY.\nDid not flash.\n\nPlease check your file (e.g. re-download).\n" 16 60 - exit 1 - fi + # is an update package provided? + if [ -z "${PKG_FILE##*.zip}" ]; then + # If extraction fails, delete everything and fall through to the + # integrity failure prompt. This is the most likely path if the ROM + # was actually corrupted in transit. Corrupting the ZIP in a way that + # still extracts is possible (the sha256sum detects this) but less + # likely. + unzip "$PKG_FILE" -d "$PKG_EXTRACT" || rm -rf "$PKG_EXTRACT" + # Older packages had /tmp/verified_rom hard-coded in the sha256sum.txt + # Remove that so it's a relative path to the ROM in the package. + # Ignore failure, if there is no sha256sum.txt the sha256sum will fail + sed -i -e 's| /tmp/verified_rom/\+| |g' "$PKG_EXTRACT/sha256sum.txt" || true + # check file integrity + if ! (cd "$PKG_EXTRACT" && sha256sum -cs sha256sum.txt); then + whiptail_error --title 'ROM Integrity Check Failed! ' \ + --msgbox "Integrity check failed in\n$PKG_FILE_DISPLAY.\nDid not flash.\n\nPlease check your file (e.g. re-download).\n" 0 0 + exit 1 + fi - # The package must contain exactly one *.rom file, flash that. - if ! PACKAGE_ROM="$(single_glob "$PKG_EXTRACT/"*."$UPDATE_PLAIN_EXT")"; then - whiptail --title 'BIOS Image Not Found! ' \ - --msgbox "A BIOS image was not found in\n$PKG_FILE_DISPLAY.\n\nPlease check your file (e.g. re-download).\n" 16 60 - exit 1 - fi + # The package must contain exactly one *.rom file, flash that. + if ! PACKAGE_ROM="$(single_glob "$PKG_EXTRACT/"*."$UPDATE_PLAIN_EXT")"; then + whiptail_error --title 'BIOS Image Not Found! ' \ + --msgbox "A BIOS image was not found in\n$PKG_FILE_DISPLAY.\n\nPlease check your file (e.g. re-download).\n" 0 0 + exit 1 + fi - if ! whiptail_warning --title 'Flash ROM?' \ - --yesno "This will replace your current ROM with:\n\n$PKG_FILE_DISPLAY\n\nDo you want to proceed?" 0 80; then - exit 1 - fi + if ! whiptail_warning --title 'Flash ROM?' \ + --yesno "This will replace your current ROM with:\n\n$PKG_FILE_DISPLAY\n\nDo you want to proceed?" 0 0; then + exit 1 + fi - # Continue on using the verified ROM - ROM="$PACKAGE_ROM" - else - # talos-2 uses a .tgz file for its "plain" update, contains other parts as well, validated against hashes under flash.sh - # Skip prompt for hash validation for talos-2. Only method is through tgz or through bmc with individual parts - if [ "${CONFIG_BOARD%_*}" != talos-2 ]; then - # Though a plain ROM isn't a package, copy it to /tmp before doing - # anything, so we can be sure the media won't disappear or fail - # while flashing. - if ! cp "$PKG_FILE" "$PKG_EXTRACT/"; then - whiptail --title 'Failed to read ROM' \ - --msgbox "Failed to read ROM:\n$PKG_FILE_DISPLAY\n\nPlease check your file (e.g. re-download).\n" 16 60 - exit 1 - fi - ROM="$PKG_EXTRACT/$(basename "$PKG_FILE")" - ROM_HASH=$(sha256sum "$ROM" | awk '{print $1}') - if ! (whiptail_error --title 'Flash ROM without integrity check?' \ - --yesno "You have provided a *.$UPDATE_PLAIN_EXT file. The integrity of the file can not be\nchecked automatically for this file type.\n\nROM: $PKG_FILE_DISPLAY\nSHA256SUM: $ROM_HASH\n\nIf you do not know how to check the file integrity yourself,\nyou should use a *.zip file instead.\n\nIf the file is damaged, you will not be able to boot anymore.\nDo you want to proceed flashing without file integrity check?" 0 80); then - exit 1 - fi - else - #We are on talos-2, so we have a tgz file. We will pass it directly to flash.sh which will take care of it - ROM="$PKG_FILE" - fi - fi + # Continue on using the verified ROM + ROM="$PACKAGE_ROM" + else + # talos-2 uses a .tgz file for its "plain" update, contains other parts as well, validated against hashes under flash.sh + # Skip prompt for hash validation for talos-2. Only method is through tgz or through bmc with individual parts + if [ "${CONFIG_BOARD%_*}" != talos-2 ]; then + # Though a plain ROM isn't a package, copy it to /tmp before doing + # anything, so we can be sure the media won't disappear or fail + # while flashing. + if ! cp "$PKG_FILE" "$PKG_EXTRACT/"; then + whiptail_error --title 'Failed to read ROM' \ + --msgbox "Failed to read ROM:\n$PKG_FILE_DISPLAY\n\nPlease check your file (e.g. re-download).\n" 0 0 + exit 1 + fi + ROM="$PKG_EXTRACT/$(basename "$PKG_FILE")" + ROM_HASH=$(sha256sum "$ROM" | awk '{print $1}') + if ! (whiptail_error --title 'Flash ROM without integrity check?' \ + --yesno "You have provided a *.$UPDATE_PLAIN_EXT file. The integrity of the file can not be\nchecked automatically for this file type.\n\nROM: $PKG_FILE_DISPLAY\nSHA256SUM: $ROM_HASH\n\nIf you do not know how to check the file integrity yourself,\nyou should use a *.zip file instead.\n\nIf the file is damaged, you will not be able to boot anymore.\nDo you want to proceed flashing without file integrity check?" 0 0); then + exit 1 + fi + else + #We are on talos-2, so we have a tgz file. We will pass it directly to flash.sh which will take care of it + ROM="$PKG_FILE" + fi + fi - if [ "$menu_choice" == "c" ]; then - /bin/flash.sh -c "$ROM" - # after flash, /boot signatures are now invalid so go ahead and clear them - if ls /boot/kexec* >/dev/null 2>&1; then - ( - mount -o remount,rw /boot 2>/dev/null - rm /boot/kexec* 2>/dev/null - mount -o remount,ro /boot 2>/dev/null - ) - fi - else - /bin/flash.sh "$ROM" - fi - whiptail --title 'ROM Flashed Successfully' \ - --msgbox "$PKG_FILE_DISPLAY\n\nhas been flashed successfully.\n\nPress Enter to reboot\n" 0 80 - umount /media - /bin/reboot - fi - fi - ;; - esac + if [ "$menu_choice" == "c" ]; then + /bin/flash.sh -c "$ROM" + # after flash, /boot signatures are now invalid so go ahead and clear them + if ls /boot/kexec* >/dev/null 2>&1; then + ( + mount -o remount,rw /boot 2>/dev/null + rm /boot/kexec* 2>/dev/null + mount -o remount,ro /boot 2>/dev/null + ) + fi + else + /bin/flash.sh "$ROM" + fi + whiptail_type $BG_COLOR_MAIN_MENU --title 'ROM Flashed Successfully' \ + --msgbox "$PKG_FILE_DISPLAY\n\nhas been flashed successfully.\n\nPress Enter to reboot\n" 0 0 + umount /media + /bin/reboot.sh + fi + fi + ;; + esac done exit 0 diff --git a/initrd/bin/flash.sh b/initrd/bin/flash.sh index a57c4c45e..e769c15c6 100755 --- a/initrd/bin/flash.sh +++ b/initrd/bin/flash.sh @@ -6,17 +6,15 @@ set -e -o pipefail . /etc/functions.sh . /tmp/config -echo - TRACE_FUNC case "$CONFIG_FLASH_OPTIONS" in "" ) - die "ERROR: No flash options have been configured!\n\nEach board requires specific CONFIG_FLASH_OPTIONS options configured. It's unsafe to flash without them.\n\nAborting." + DIE "ERROR: No flash options have been configured!\n\nEach board requires specific CONFIG_FLASH_OPTIONS options configured. It's unsafe to flash without them.\n\nAborting." ;; * ) DEBUG "Flash options detected: $CONFIG_FLASH_OPTIONS" - echo "Board $CONFIG_BOARD detected with flash options configured. Continuing..." + INFO "Board $CONFIG_BOARD detected with flash options configured" ;; esac @@ -34,20 +32,20 @@ flash_rom() { fi # persist serial number from CBFS if cbfs.sh -r serial_number > /tmp/serial 2>/dev/null; then - echo "Persisting system serial" + STATUS "Persisting system serial" cbfs.sh -o /tmp/${CONFIG_BOARD}.rom -d serial_number 2>/dev/null || true cbfs.sh -o /tmp/${CONFIG_BOARD}.rom -a serial_number -f /tmp/serial fi # persist PCHSTRP9 from flash descriptor if [ "$CONFIG_BOARD" = "librem_l1um" ]; then - echo "Persisting PCHSTRP9" + STATUS "Persisting PCHSTRP9" $CONFIG_FLASH_OPTIONS -r /tmp/ifd.bin --ifd -i fd >/dev/null 2>&1 \ - || die "Failed to read flash descriptor" + || DIE "Failed to read flash descriptor" dd if=/tmp/ifd.bin bs=1 count=4 skip=292 of=/tmp/pchstrp9.bin >/dev/null 2>&1 dd if=/tmp/pchstrp9.bin bs=1 count=4 seek=292 of=/tmp/${CONFIG_BOARD}.rom conv=notrunc >/dev/null 2>&1 fi - warn "Do not power off computer. Updating firmware, this will take a few minutes" + WARN "Do not power off computer. Updating firmware, this will take a few minutes" $CONFIG_FLASH_OPTIONS -w /tmp/${CONFIG_BOARD}.rom 2>&1 \ || recovery "$ROM: Flash failed" fi @@ -69,7 +67,7 @@ else fi if [ ! -e "$ROM" ]; then - die "Usage: $0 [-c|-r] " + DIE "Usage: $0 [-c|-r] " fi if [ "$READ" -eq 0 ] && [ "${ROM##*.}" = tgz ]; then @@ -77,12 +75,12 @@ if [ "$READ" -eq 0 ] && [ "${ROM##*.}" = tgz ]; then rm -rf /tmp/verified_rom mkdir /tmp/verified_rom - tar -C /tmp/verified_rom -xf $ROM || die "Rom archive $ROM could not be extracted" + tar -C /tmp/verified_rom -xf $ROM || DIE "Rom archive $ROM could not be extracted" if ! (cd /tmp/verified_rom/ && sha256sum -cs sha256sum.txt); then - die "Provided tgz image did not pass hash verification" + DIE "Provided tgz image did not pass hash verification" fi - echo "Reading current flash and building an update image" + STATUS "Reading current flash and building update image" $CONFIG_FLASH_OPTIONS -r /tmp/flash.sh.bak \ || recovery "Read of flash has failed" @@ -97,7 +95,7 @@ if [ "$READ" -eq 0 ] && [ "${ROM##*.}" = tgz ]; then ROM=/tmp/flash.sh.bak else - die "$CONFIG_BOARD doesn't support tgz image format" + DIE "$CONFIG_BOARD doesn't support tgz image format" fi fi diff --git a/initrd/bin/flashprog-kgpe-d16-openbmc.sh b/initrd/bin/flashprog-kgpe-d16-openbmc.sh index 4b0e6fee3..2b9d8badf 100755 --- a/initrd/bin/flashprog-kgpe-d16-openbmc.sh +++ b/initrd/bin/flashprog-kgpe-d16-openbmc.sh @@ -5,14 +5,14 @@ TRACE_FUNC ROM="$1" if [ -z "$1" ]; then - die "Usage: $0 /media/kgpe-d16-openbmc.rom" + DIE "Usage: $0 /media/kgpe-d16-openbmc.rom" fi cp "$ROM" /tmp/kgpe-d16-openbmc.rom sha256sum /tmp/kgpe-d16-openbmc.rom flashprog --programmer="ast1100:spibus=2,cpu=reset" -c "S25FL128P......0" -w /tmp/kgpe-d16-openbmc.rom \ -|| die "$ROM: Flash failed" +|| DIE "$ROM: Flash failed" -warn "Reboot and hopefully it works" +WARN "Reboot and hopefully it works" exit 0 diff --git a/initrd/bin/generic-init.sh b/initrd/bin/generic-init.sh index 4f2071a86..35f17ddeb 100755 --- a/initrd/bin/generic-init.sh +++ b/initrd/bin/generic-init.sh @@ -4,15 +4,17 @@ . /etc/functions.sh . /tmp/config -mount_boot() { +mount_boot() +{ TRACE_FUNC # Mount local disk if it is not already mounted - if ! grep -q /boot /proc/mounts; then - mount -o ro /boot || - recovery "Unable to mount /boot" + if ! grep -q /boot /proc/mounts ; then + mount -o ro /boot \ + || recovery "Unable to mount /boot" fi } + # Confirm we have a good TOTP unseal and ask the user for next choice while true; do echo "y) Default boot" @@ -30,11 +32,9 @@ while true; do fi if [ "$totp_confirm" = "n" ]; then - echo "" - echo "To correct clock drift: 'date -s HH:MM:SS'" - echo "and save it to the RTC: 'hwclock -w'" - echo "then reboot and try again" - echo "" + INFO "To correct clock drift: date -s HH:MM:SS" + INFO "and save it to the RTC: hwclock -w" + INFO "then reboot and try again" recovery "TOTP mismatch" fi @@ -53,8 +53,8 @@ while true; do if [ "$totp_confirm" = "y" -o -n "$totp_confirm" ]; then # Try to boot the default mount_boot - DO_WITH_DEBUG kexec-select-boot.sh -b /boot -c "grub.cfg" || - recovery "Failed default boot" + DO_WITH_DEBUG kexec-select-boot.sh -b /boot -c "grub.cfg" \ + || recovery "Failed default boot" fi done diff --git a/initrd/bin/gpg-gui.sh b/initrd/bin/gpg-gui.sh index 3f8f4b37a..0ec4c3210 100755 --- a/initrd/bin/gpg-gui.sh +++ b/initrd/bin/gpg-gui.sh @@ -3,243 +3,68 @@ set -e -o pipefail . /etc/functions.sh . /etc/gui_functions.sh +. /etc/gpg_functions.sh . /tmp/config TRACE_FUNC -gpg_flash_rom() { - - if [ "$1" = "replace" ]; then - # clear local keyring - [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg - [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx - [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg - fi - - cat "$PUBKEY" | gpg --import - #update /.gnupg/trustdb.gpg to ultimately trust all user provided public keys - gpg --list-keys --fingerprint --with-colons |sed -E -n -e 's/^fpr:::::::::([0-9A-F]+):$/\1:6:/p' |gpg --import-ownertrust - gpg --update-trust - - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/pubring.kbx"); then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/pubring.kbx" - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/pubring.gpg"); then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/pubring.gpg" - if [ -e /.gnupg/pubring.gpg ];then - rm /.gnupg/pubring.gpg - fi - fi - fi - - #to be compatible with gpgv1 - if [ -e /.gnupg/pubring.kbx ];then - cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/pubring.kbx" -f /.gnupg/pubring.kbx - if [ -e /.gnupg/pubring.gpg ];then - rm /.gnupg/pubring.gpg - fi - fi - if [ -e /.gnupg/pubring.gpg ];then - cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/pubring.gpg" -f /.gnupg/pubring.gpg - fi - - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/trustdb.gpg") then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/trustdb.gpg" - fi - if [ -e /.gnupg/trustdb.gpg ]; then - cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/.gnupg/trustdb.gpg" -f /.gnupg/trustdb.gpg - fi - - #Remove old method owner trust exported file - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/.gnupg/otrust.txt") then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/.gnupg/otrust.txt" - fi - - # persist user config changes - if (cbfs.sh -o /tmp/gpg-gui.rom -l | grep -q "heads/initrd/etc/config.user") then - cbfs.sh -o /tmp/gpg-gui.rom -d "heads/initrd/etc/config.user" - fi - if [ -e /etc/config.user ]; then - cbfs.sh -o /tmp/gpg-gui.rom -a "heads/initrd/etc/config.user" -f /etc/config.user - fi - /bin/flash.sh /tmp/gpg-gui.rom - - if (whiptail --title 'BIOS Flashed 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 - if ! update_checksums ; then - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 - fi - else - /bin/reboot - fi - - whiptail --title 'Files in /boot Updated Successfully'\ - --msgbox "Checksums have been updated and /boot files signed.\n\nPress Enter to reboot" 0 80 - /bin/reboot - -} -gpg_post_gen_mgmt() { - GPG_GEN_KEY=`grep -A1 pub /tmp/gpg_card_edit_output | tail -n1 | sed -nr 's/^([ ])*//p'` - gpg --export --armor $GPG_GEN_KEY > "/tmp/${GPG_GEN_KEY}.asc" - if (whiptail --title 'Add Public Key to USB disk?' \ - --yesno "Would you like to copy the GPG public key you generated to a USB disk?\n\nYou may need it, if you want to use it outside of Heads later.\n\nThe file will show up as ${GPG_GEN_KEY}.asc" 0 80) then - mount_usb - mount -o remount,rw /media - cp "/tmp/${GPG_GEN_KEY}.asc" "/media/${GPG_GEN_KEY}.asc" - if [ $? -eq 0 ]; then - whiptail --title "The GPG Key Copied Successfully" \ - --msgbox "${GPG_GEN_KEY}.asc copied successfully." 0 80 - else - whiptail_error --title 'ERROR: Copy Failed' \ - --msgbox "Unable to copy ${GPG_GEN_KEY}.asc to /media" 0 80 - fi - umount /media - fi - if (whiptail --title 'Add Public Key to Running BIOS?' \ - --yesno "Would you like to add the GPG public key you generated to the BIOS?\n\nThis makes it a trusted key used to sign files in /boot\n\n" 0 80) then - /bin/flash.sh -r /tmp/gpg-gui.rom - if [ ! -s /tmp/gpg-gui.rom ]; then - whiptail_error --title 'ERROR: BIOS Read Failed!' \ - --msgbox "Unable to read BIOS" 0 80 - exit 1 - fi - PUBKEY="/tmp/${GPG_GEN_KEY}.asc" - gpg_flash_rom - fi -} - -gpg_add_key_reflash() { - if (whiptail --title 'GPG public key required' \ - --yesno "This requires you insert a USB drive containing:\n* Your GPG public key (*.key or *.asc)\n\nAfter you select this file, this program will copy and reflash your BIOS\n\nDo you want to proceed?" 0 80) then - mount_usb - if grep -q /media /proc/mounts ; then - find /media -name '*.key' > /tmp/filelist.txt - find /media -name '*.asc' >> /tmp/filelist.txt - file_selector "/tmp/filelist.txt" "Choose your GPG public key" - # bail if user didn't select a file - if [ "$FILE" = "" ]; then - return - else - PUBKEY=$FILE - fi - - /bin/flash.sh -r /tmp/gpg-gui.rom - if [ ! -s /tmp/gpg-gui.rom ]; then - whiptail_error --title 'ERROR: BIOS Read Failed!' \ - --msgbox "Unable to read BIOS" 0 80 - exit 1 - fi - - if (whiptail --title 'Update ROM?' \ - --yesno "This will reflash your BIOS with the updated version\n\nDo you want to proceed?" 0 80) then - gpg_flash_rom - else - exit 0 - fi - fi - fi -} while true; do - unset menu_choice - whiptail_type $BG_COLOR_MAIN_MENU --title "GPG Management Menu" \ - --menu 'Select the GPG function to perform' 0 80 10 \ - 'r' ' Add GPG key to running BIOS and reflash' \ - 'a' ' Add GPG key to standalone BIOS image and flash' \ - 'e' ' Replace GPG key(s) in the current ROM and reflash' \ - 'l' ' List GPG keys in your keyring' \ - 'p' ' Export public GPG key to USB drive' \ - 'g' ' Generate GPG keys manually on a USB security dongle' \ - 'x' ' Exit' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - menu_choice=$(cat /tmp/whiptail) - - case "$menu_choice" in - "x" ) - exit 0 - ;; - "a" ) - if (whiptail --title 'ROM and GPG public key required' \ - --yesno "This requires you insert a USB drive containing:\n* Your GPG public key (*.key or *.asc)\n* Your BIOS image (*.rom)\n\nAfter you select these files, this program will reflash your BIOS\n\nDo you want to proceed?" 0 80) then - mount_usb - if grep -q /media /proc/mounts ; then - find /media -name '*.key' > /tmp/filelist.txt - find /media -name '*.asc' >> /tmp/filelist.txt - file_selector "/tmp/filelist.txt" "Choose your GPG public key" - if [ "$FILE" == "" ]; then - return - else - PUBKEY=$FILE - fi - - find /media -name '*.rom' > /tmp/filelist.txt - file_selector "/tmp/filelist.txt" "Choose the ROM to load your key onto" - if [ "$FILE" == "" ]; then - return - else - ROM=$FILE - fi - cp "$ROM" /tmp/gpg-gui.rom - - if (whiptail_warning --title 'Flash ROM?' \ - --yesno "This will replace your old ROM with $ROM\n\nDo you want to proceed?" 0 80) then - gpg_flash_rom - else - exit 0 - fi - fi - fi - ;; - "r" ) - gpg_add_key_reflash - exit 0; - ;; - "e" ) - # clear local keyring - [ -e /.gnupg/pubring.gpg ] && rm /.gnupg/pubring.gpg - [ -e /.gnupg/pubring.kbx ] && rm /.gnupg/pubring.kbx - [ -e /.gnupg/trustdb.gpg ] && rm /.gnupg/trustdb.gpg - # add key and reflash - gpg_add_key_reflash - ;; - "l" ) - GPG_KEYRING=`gpg -k` - whiptail --title 'GPG Keyring' \ - --msgbox "${GPG_KEYRING}" 0 80 - ;; - "p" ) - if (whiptail --title 'Export Public Key(s) to USB drive?' \ - --yesno "Would you like to copy GPG public key(s) to a USB drive?\n\nThe file will show up as public-key.asc" 0 80) then - mount_usb - mount -o remount,rw /media - gpg --export --armor > "/tmp/public-key.asc" - cp "/tmp/public-key.asc" "/media/public-key.asc" - if [ $? -eq 0 ]; then - whiptail --title "The GPG Key Copied Successfully" \ - --msgbox "public-key.asc copied successfully." 0 80 - else - whiptail_error --title 'ERROR: Copy Failed' \ - --msgbox "Unable to copy public-key.asc to /media" 0 80 - fi - umount /media - fi - ;; - "g" ) - confirm_gpg_card - echo -e "\n\n\n\n" - echo "********************************************************************************" - echo "*" - echo "* INSTRUCTIONS:" - echo "* Type 'admin' and then 'generate' and follow the prompts to generate a GPG key." - echo "* Type 'quit' once you have generated the key to exit GPG." - echo "*" - echo "********************************************************************************" - gpg --card-edit > /tmp/gpg_card_edit_output - if [ $? -eq 0 ]; then - gpg_post_gen_mgmt - fi - ;; - esac + unset menu_choice + whiptail_type $BG_COLOR_MAIN_MENU --title "GPG Management Menu" \ + --menu 'Select the GPG function to perform' 0 80 10 \ + 'r' ' Add GPG key to running BIOS and reflash' \ + 'a' ' Add GPG key to standalone BIOS image and flash' \ + 'e' ' Replace GPG key(s) in the current ROM and reflash' \ + 'l' ' List GPG keys in your keyring' \ + 'p' ' Export public GPG key to USB drive' \ + 'g' ' Generate GPG keys manually on a USB security dongle' \ + 'x' ' Exit' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + menu_choice=$(cat /tmp/whiptail) + + case "$menu_choice" in + "x") + exit 0 + ;; + "a") + gpg_add_key_to_standalone_rom + ;; + "r") + gpg_add_key_reflash + exit 0 + ;; + "e") + gpg_replace_key_reflash + ;; + "l") + GPG_KEYRING=$(gpg -k) + whiptail_type $BG_COLOR_MAIN_MENU --title 'GPG Keyring' \ + --msgbox "${GPG_KEYRING}" 0 80 + ;; + "p") + if (whiptail_warning --title 'Export Public Key(s) to USB drive?' \ + --yesno "Would you like to copy GPG public key(s) to a USB drive?\n\nThe file will show up as public-key.asc" 0 80); then + if gpg_export_pubkey_to_usb; then + whiptail_type $BG_COLOR_MAIN_MENU --title "The GPG Key Copied Successfully" \ + --msgbox "public-key.asc copied successfully." 0 80 + else + whiptail_error --title 'ERROR: Copy Failed' \ + --msgbox "Unable to copy public-key.asc to /media" 0 80 + fi + fi + ;; + "g") + confirm_gpg_card + STATUS "INSTRUCTIONS:" + INFO "Type 'admin' then 'generate' and follow the prompts to generate a GPG key" + INFO "Type 'quit' once the key is generated to exit GPG" + gpg --card-edit >/tmp/gpg_card_edit_output + if [ $? -eq 0 ]; then + gpg_post_gen_mgmt + fi + ;; + esac done exit 0 diff --git a/initrd/bin/gui-init-basic.sh b/initrd/bin/gui-init-basic.sh index 1a6d8a875..51e06691d 100755 --- a/initrd/bin/gui-init-basic.sh +++ b/initrd/bin/gui-init-basic.sh @@ -1,7 +1,7 @@ #!/bin/bash # Boot from a local disk installation -BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} +BOARD_NAME=${CONFIG_BOARD_NAME:-${CONFIG_BOARD}} MAIN_MENU_TITLE="${BOARD_NAME} | $CONFIG_BRAND_NAME Basic Boot Menu" export BG_COLOR_MAIN_MENU="normal" @@ -9,168 +9,179 @@ export BG_COLOR_MAIN_MENU="normal" . /etc/gui_functions.sh . /tmp/config +# Detect the terminal — see detect_heads_tty in /etc/functions. +detect_heads_tty + # skip_to_menu is set if the user selects "continue to the main menu" from any # error, so we will indeed go to the main menu even if other errors occur. It's # reset when we reach the main menu so the user can retry from the main menu and # # see errors again. skip_to_menu="false" -mount_boot() { - TRACE_FUNC - # Mount local disk if it is not already mounted - while ! grep -q /boot /proc/mounts; do - # try to mount if CONFIG_BOOT_DEV exists - if [ -e "$CONFIG_BOOT_DEV" ]; then - mount -o ro $CONFIG_BOOT_DEV /boot - [[ $? -eq 0 ]] && continue - fi - - # CONFIG_BOOT_DEV doesn't exist or couldn't be mounted, so give user options - BG_COLOR_MAIN_MENU="error" - whiptail_error --title "ERROR: No Bootable OS Found!" \ - --menu " No bootable OS was found on the default boot device $CONFIG_BOOT_DEV. +mount_boot() +{ + TRACE_FUNC + # Mount local disk if it is not already mounted + while ! grep -q /boot /proc/mounts ; do + # try to mount if CONFIG_BOOT_DEV exists + if [ -e "$CONFIG_BOOT_DEV" ]; then + mount -o ro $CONFIG_BOOT_DEV /boot + [[ $? -eq 0 ]] && continue + fi + + # CONFIG_BOOT_DEV doesn't exist or couldn't be mounted, so give user options + BG_COLOR_MAIN_MENU="error" + whiptail_error --title "ERROR: No Bootable OS Found!" \ + --menu " No bootable OS was found on the default boot device $CONFIG_BOOT_DEV. How would you like to proceed?" 0 80 4 \ - 'b' ' Select a new boot device' \ - 'u' ' Boot from USB' \ - 'm' ' Continue to the main menu' \ - 'x' ' Exit to recovery shell' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - b) - config-gui.sh boot_device_select - if [ $? -eq 0 ]; then - # update CONFIG_BOOT_DEV - . /tmp/config - BG_COLOR_MAIN_MENU="normal" - fi - ;; - u) - exec /bin/usb-init.sh - ;; - m) - skip_to_menu="true" - break - ;; - *) - recovery "User requested recovery shell" - ;; - esac - done + 'b' ' Select a new boot device' \ + 'u' ' Boot from USB' \ + 'm' ' Continue to the main menu' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + b ) + config-gui.sh boot_device_select + if [ $? -eq 0 ]; then + # update CONFIG_BOOT_DEV + . /tmp/config + BG_COLOR_MAIN_MENU="normal" + fi + ;; + u ) + exec /bin/usb-init.sh + ;; + m ) + skip_to_menu="true" + break + ;; + * ) + recovery "User requested recovery shell" + ;; + esac + done } -prompt_auto_default_boot() { - TRACE_FUNC - echo -e "\n\n" - if pause_automatic_boot; then - echo -e "\n\nAttempting default boot...\n\n" - attempt_default_boot - fi +prompt_auto_default_boot() +{ + TRACE_FUNC + if pause_automatic_boot; then + STATUS "Attempting default boot" + attempt_default_boot + fi } -show_main_menu() { - TRACE_FUNC - date=$(date "+%Y-%m-%d %H:%M:%S %Z") - whiptail_type $BG_COLOR_MAIN_MENU --title "$MAIN_MENU_TITLE" \ - --menu "$date" 0 80 10 \ - 'd' ' Default boot' \ - 'o' ' Options -->' \ - 's' ' System Info' \ - 'p' ' Power Off' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - d) - attempt_default_boot - ;; - o) - show_options_menu - ;; - s) - show_system_info - ;; - p) - poweroff - ;; - esac +show_main_menu() +{ + TRACE_FUNC + date=`date "+%Y-%m-%d %H:%M:%S %Z"` + whiptail_type $BG_COLOR_MAIN_MENU --title "$MAIN_MENU_TITLE" \ + --menu "$date" 0 80 10 \ + 'd' ' Default boot' \ + 'o' ' Options -->' \ + 's' ' System Info' \ + 'p' ' Power Off' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + d ) + attempt_default_boot + ;; + o ) + show_options_menu + ;; + s ) + show_system_info + ;; + p ) + poweroff.sh + ;; + esac } -show_options_menu() { - TRACE_FUNC - whiptail_type $BG_COLOR_MAIN_MENU --title "$CONFIG_BRAND_NAME Basic Options" \ - --menu "" 0 80 10 \ - 'b' ' Boot Options -->' \ - 'c' ' Change configuration settings -->' \ - 'f' ' Flash/Update the BIOS -->' \ - 'x' ' Exit to recovery shell' \ - 'r' ' <-- Return to main menu' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - b) - show_boot_options_menu - ;; - c) - config-gui.sh - ;; - f) - flash-gui.sh - ;; - x) - recovery "User requested recovery shell" - ;; - r) ;; - esac +show_options_menu() +{ + TRACE_FUNC + whiptail_type $BG_COLOR_MAIN_MENU --title "$CONFIG_BRAND_NAME Basic Options" \ + --menu "" 0 80 10 \ + 'b' ' Boot Options -->' \ + 'c' ' Change configuration settings -->' \ + 'f' ' Flash/Update the BIOS -->' \ + 'x' ' Exit to recovery shell' \ + 'r' ' <-- Return to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + b ) + show_boot_options_menu + ;; + c ) + config-gui.sh + ;; + f ) + flash-gui.sh + ;; + x ) + recovery "User requested recovery shell" + ;; + r ) + ;; + esac } -show_boot_options_menu() { - TRACE_FUNC - whiptail_type $BG_COLOR_MAIN_MENU --title "Boot Options" \ - --menu "Select A Boot Option" 0 80 10 \ - 'm' ' Show OS boot menu' \ - 'u' ' USB boot' \ - 'r' ' <-- Return to main menu' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - - option=$(cat /tmp/whiptail) - case "$option" in - m) - # select a kernel from the menu - select_os_boot_option - ;; - u) - exec /bin/usb-init.sh - ;; - r) ;; - esac +show_boot_options_menu() +{ + TRACE_FUNC + whiptail_type $BG_COLOR_MAIN_MENU --title "Boot Options" \ + --menu "Select A Boot Option" 0 80 10 \ + 'm' ' Show OS boot menu' \ + 'u' ' USB boot' \ + 'r' ' <-- Return to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + m ) + # select a kernel from the menu + select_os_boot_option + ;; + u ) + exec /bin/usb-init.sh + ;; + r ) + ;; + esac } -select_os_boot_option() { - TRACE_FUNC - mount_boot - DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -i +select_os_boot_option() +{ + TRACE_FUNC + mount_boot + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -i } -attempt_default_boot() { - TRACE_FUNC - mount_boot - - DEFAULT_FILE=$(find /boot/kexec_default.*.txt 2>/dev/null | head -1) - # Basic by default boots automatically to the first menu option. This allows - # kernel updates to work in Basic by default without prompting to select a - # new default boot option. - if [ "$CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" != "y" ]; then - basic-autoboot.sh - elif [ -r "$DEFAULT_FILE" ]; then - DO_WITH_DEBUG kexec-select-boot.sh -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 - DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -i - fi +attempt_default_boot() +{ + TRACE_FUNC + mount_boot + + DEFAULT_FILE=`find /boot/kexec_default.*.txt 2>/dev/null | head -1` + # Basic by default boots automatically to the first menu option. This allows + # kernel updates to work in Basic by default without prompting to select a + # new default boot option. + if [ "$CONFIG_BASIC_NO_AUTOMATIC_DEFAULT" != "y" ]; then + basic-autoboot.sh + elif [ -r "$DEFAULT_FILE" ]; then + DO_WITH_DEBUG kexec-select-boot.sh -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 + DO_WITH_DEBUG kexec-select-boot.sh -m -b /boot -c "grub.cfg" -g -i + fi } # gui-init-basic start @@ -179,25 +190,25 @@ TRACE_FUNC # USB automatic boot (if configured) occurs before mounting /boot, this should # work even if no OS is installed if [ "$CONFIG_BASIC_USB_AUTOBOOT" = "y" ] && usb-autoboot.sh; then - # USB autoboot was offered and interrupted. Don't offer the default boot, - # go to the menu. - skip_to_menu=true + # USB autoboot was offered and interrupted. Don't offer the default boot, + # go to the menu. + skip_to_menu=true fi -if ! detect_boot_device; then - # can't determine /boot device or no OS installed, - # so fall back to interactive selection - mount_boot +if ! detect_boot_device ; then + # can't determine /boot device or no OS installed, + # so fall back to interactive selection + mount_boot fi if [ "$skip_to_menu" != "true" -a -n "$CONFIG_AUTO_BOOT_TIMEOUT" ]; then - prompt_auto_default_boot + prompt_auto_default_boot fi while true; do - TRACE_FUNC - skip_to_menu="false" - show_main_menu + TRACE_FUNC + skip_to_menu="false" + show_main_menu done recovery "Something failed during boot" diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index 64a24f040..d705afcd2 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -10,11 +10,21 @@ export BG_COLOR_MAIN_MENU="normal" . /etc/luks-functions.sh . /tmp/config +# Detect the terminal this gui-init session is running on. The user +# interacting with gui-init (via whiptail) is the source of truth for the +# "active" terminal — prompts, GPG/pinentry, and input all go to/from there. +# $(tty) works here because cttyhack (exec'd by /init) has already replaced +# fd0/1/2 with the correct console device before launching this script. +# Fall back to /sys/class/tty/console/active (last entry = preferred console, +# same source used by systemd and busybox cttyhack) when tty is unavailable. +detect_heads_tty + # skip_to_menu is set if the user selects "continue to the main menu" from any # error, so we will indeed go to the main menu even if other errors occur. It's # reset when we reach the main menu so the user can retry from the main menu and # # see errors again. skip_to_menu="false" +INTEGRITY_GATE_REQUIRED="n" mount_boot() { TRACE_FUNC @@ -22,34 +32,43 @@ mount_boot() { while ! grep -q /boot /proc/mounts; do # try to mount if CONFIG_BOOT_DEV exists if [ -e "$CONFIG_BOOT_DEV" ]; then - mount -o ro $CONFIG_BOOT_DEV /boot - [[ $? -eq 0 ]] && continue + if mount -o ro "$CONFIG_BOOT_DEV" /boot; then + continue + fi fi - # CONFIG_BOOT_DEV doesn't exist or couldn't be mounted, so give user options + # CONFIG_BOOT_DEV doesn't exist or couldn't be mounted, so give user options. + # LUKS_PARTITION_DETECTED is set by detect_boot_device (via mount_possible_boot_device) + # when it skips a LUKS partition -- reuse that result to distinguish + # "OS installed without separate /boot" from "no OS found at all". BG_COLOR_MAIN_MENU="error" - whiptail_error --title "ERROR: No Bootable OS Found!" \ - --menu " No bootable OS was found on the default boot device $CONFIG_BOOT_DEV. - How would you like to proceed?" 0 80 4 \ - 'b' ' Select a new boot device' \ + local boot_msg + if [ "${LUKS_PARTITION_DETECTED:-n}" = "y" ]; then + boot_msg="An encrypted OS was detected but no separate /boot partition was found.\n\n$CONFIG_BRAND_NAME requires a separate, unencrypted /boot partition.\n\nMost OS installers do not create this layout by default. Only DVD/live\nISOs that detect legacy boot (BIOS/CSM mode) will offer the correct\npartition scheme. Use 'Boot from USB' to boot a live ISO and reinstall\nyour OS with a separate /boot partition.\n\nHow would you like to proceed?" + else + boot_msg="No bootable OS was found on any disk.\n\n$CONFIG_BRAND_NAME requires a separate, unencrypted /boot partition\ncontaining grub configuration files.\n\nIf you are installing an OS for the first time, use 'Boot from USB' to\nboot a live ISO. Only DVD/live ISOs that detect legacy boot (BIOS/CSM)\nwill offer the correct partition scheme with a separate /boot.\n\nHow would you like to proceed?" + fi + whiptail_error --title "ERROR: No /boot Partition Found" \ + --menu "$boot_msg" 0 80 4 \ 'u' ' Boot from USB' \ + 'b' ' Select a new boot device' \ 'm' ' Continue to the main menu' \ 'x' ' Exit to recovery shell' \ 2>/tmp/whiptail || recovery "GUI menu failed" option=$(cat /tmp/whiptail) case "$option" in + u) + exec /bin/usb-init.sh + ;; b) - config-gui.sh boot_device_select - if [ $? -eq 0 ]; then + if config-gui.sh boot_device_select; then # update CONFIG_BOOT_DEV + # shellcheck source=/dev/null . /tmp/config BG_COLOR_MAIN_MENU="normal" fi ;; - u) - exec /bin/usb-init.sh - ;; m) skip_to_menu="true" break @@ -74,7 +93,7 @@ verify_global_hashes() { return 0 elif [[ ! -f "$TMP_HASH_FILE" || ! -f "$TMP_TREE_FILE" ]]; then if (whiptail_error --title 'ERROR: Missing File!' \ - --yesno "One of the files containing integrity information for /boot is missing!\n\nIf you are setting up heads for the first time or upgrading from an\nolder version, select Yes to create the missing files.\n\nOtherwise this could indicate a compromise and you should select No to\nreturn to the main menu.\n\nWould you like to create the missing files now?" 0 80); then + --yesno "One of the files containing integrity information for /boot is missing!\n\nIf you are setting up heads for the first time or upgrading from an older version, select Yes to create the missing files.\n\nOtherwise this could indicate a compromise and you should select No to return to the main menu.\n\nWould you like to create the missing files now?" 0 80); then if update_checksums; then BG_COLOR_MAIN_MENU="normal" return 0 @@ -91,13 +110,13 @@ verify_global_hashes() { # if files changed before package manager started, show stern warning if [ -f "$TMP_PACKAGE_TRIGGER_PRE" ]; then - PRE_CHANGED_FILES=$(grep '^CHANGED_FILES' $TMP_PACKAGE_TRIGGER_POST | cut -f 2 -d '=' | tr -d '"') + PRE_CHANGED_FILES=$(grep '^CHANGED_FILES' "$TMP_PACKAGE_TRIGGER_POST" | cut -f 2 -d '=' | tr -d '"') TEXT="The following files failed the verification process BEFORE package updates ran:\n${PRE_CHANGED_FILES}\n\nCompare against the files $CONFIG_BRAND_NAME has detected have changed:\n${CHANGED_FILES}\n\nThis could indicate a compromise!\n\nWould you like to update your checksums anyway?" # if files changed after package manager started, probably caused by package manager elif [ -f "$TMP_PACKAGE_TRIGGER_POST" ]; then - LAST_PACKAGE_LIST=$(grep -E "^(Install|Remove|Upgrade|Reinstall):" $TMP_PACKAGE_TRIGGER_POST) - UPDATE_INITRAMFS_PACKAGE=$(grep '^UPDATE_INITRAMFS_PACKAGE' $TMP_PACKAGE_TRIGGER_POST | cut -f 2 -d '=' | tr -d '"') + LAST_PACKAGE_LIST=$(grep -E "^(Install|Remove|Upgrade|Reinstall):" "$TMP_PACKAGE_TRIGGER_POST") + UPDATE_INITRAMFS_PACKAGE=$(grep '^UPDATE_INITRAMFS_PACKAGE' "$TMP_PACKAGE_TRIGGER_POST" | cut -f 2 -d '=' | tr -d '"') if [ "$UPDATE_INITRAMFS_PACKAGE" != "" ]; then TEXT="The following files failed the verification process AFTER package updates ran:\n${CHANGED_FILES}\n\nThis is likely due to package triggers in$UPDATE_INITRAMFS_PACKAGE.\n\nYou will need to update your checksums for all files in /boot.\n\nWould you like to update your checksums now?" @@ -115,68 +134,215 @@ verify_global_hashes() { less /tmp/hash_output_mismatches #move outdated hash mismatch list mv /tmp/hash_output_mismatches /tmp/hash_output_mismatch_old - TEXT="Would you like to update your checksums now?" + TEXT="${CHANGED_FILES_COUNT} files failed the verification process.\n\nThis could indicate a compromise!\n\nWould you like to investigate discrepancies or update your checksums now?" else - TEXT="The following files failed the verification process:\n\n${CHANGED_FILES}\n\nThis could indicate a compromise!\n\nWould you like to update your checksums now?" + TEXT="The following files failed the verification process:\n\n${CHANGED_FILES}\n\nThis could indicate a compromise!\n\nWould you like to investigate discrepancies or update your checksums now?" fi fi - if (whiptail_error --title 'ERROR: Boot Hash Mismatch' --yesno "$TEXT" 0 80); then - if update_checksums; then - BG_COLOR_MAIN_MENU="normal" - return 0 - else - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 - fi - fi - BG_COLOR_MAIN_MENU="error" - return 1 + local menu_text + menu_text="$TEXT" + while true; do + TRACE_FUNC + whiptail_error --title 'ERROR: Boot Hash Mismatch' \ + --menu "$menu_text\n\nChoose an action:" 0 80 3 \ + 'i' ' Investigate discrepancies -->' \ + 'u' ' Update checksums now' \ + 'm' ' Return to main menu' \ + 2>/tmp/whiptail || { + BG_COLOR_MAIN_MENU="error" + return 1 + } + + option=$(cat /tmp/whiptail) + case "$option" in + i) + investigate_integrity_discrepancies + ;; + u) + if update_checksums; then + BG_COLOR_MAIN_MENU="normal" + return 0 + else + whiptail_error --title 'ERROR' \ + --msgbox "Failed to update checksums / sign default config" 0 80 + fi + ;; + m | *) + BG_COLOR_MAIN_MENU="error" + return 1 + ;; + esac + done fi } prompt_update_checksums() { TRACE_FUNC + # Signing /boot with -r increments the TPM rollback counter. If the counter + # is broken or absent (tpm_reset_required), the increment will fail and DIE. + # The user must reset the TPM first; that flow re-creates the counter. + if [ "$CONFIG_TPM" = "y" ] && tpm_reset_required; then + whiptail_error --title 'TPM Reset Required' \ + --msgbox "Cannot sign /boot: TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM), then update checksums." 0 80 + return 1 + fi if (whiptail_warning --title 'Update Checksums and sign all files in /boot' \ --yesno "You have chosen to update the checksums and sign all of the files in /boot.\n\nThis means that you trust that these files have not been tampered with.\n\nYou will need your GPG key available, and this change will modify your disk.\n\nDo you want to continue?" 0 80); then - if ! update_checksums; then + if update_checksums; then + return 0 + else whiptail_error --title 'ERROR' \ --msgbox "Failed to update checksums / sign default config" 0 80 + return 1 + fi + fi + return 1 +} + +gate_reseal_with_integrity_report() { + TRACE_FUNC + local token_ok="y" + if tpm_reset_required; then + debug_tpm_reset_required_state + whiptail_error --title 'ERROR: TPM Reset Required' \ + --msgbox "TPM state is inconsistent for sealing/unsealing operations.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." 0 80 + return 1 + fi + + if [ "$INTEGRITY_GATE_REQUIRED" != "y" ]; then + DEBUG "Skipping integrity gate: no TOTP/HOTP failure context" + return 0 + fi + + INTEGRITY_REPORT_HASH_STATE="UNKNOWN" + report_integrity_measurements + local report_rc=$? + DEBUG "gate_reseal_with_integrity_report: report_integrity_measurements rc=$report_rc" + DEBUG "gate_reseal_with_integrity_report: INTEGRITY_REPORT_HASH_STATE=$INTEGRITY_REPORT_HASH_STATE" + if [ "$INTEGRITY_REPORT_HASH_STATE" != "OK" ]; then + DEBUG "returned from integrity report, now running investigation" + if ! investigate_integrity_discrepancies; then + DEBUG "investigation indicated problem, aborting gate" + return 1 fi + + DEBUG "gate_reseal_with_integrity_report: about to verify detached signature" + DEBUG "ls -l /boot/kexec.sig: $(ls -l /boot/kexec.sig 2>/dev/null || echo missing)" + if ! detached_kexec_signature_valid /boot; then + DEBUG "detached_kexec_signature_valid failed" + local sig_fail_msg + sig_fail_msg="Cannot proceed with sealing new secrets because /boot/kexec.sig could not be verified with your current keyring.\n\nTreat /boot as untrusted and recover ownership first." + whiptail_error --title 'ERROR: Signature Verification Failed' \ + --msgbox "$sig_fail_msg" 0 80 + return 1 + fi + else + DEBUG "gate_reseal_with_integrity_report: integrity is OK, skipping investigation and detached signature verification" fi + + if [ -x /bin/hotp_verification ]; then + token_ok="n" + while [ "$token_ok" != "y" ]; do + enable_usb + # wait_for_gpg_card already called release_scdaemon on success, + # starting the NK3 CCID teardown. This safety call covers the + # case where scdaemon was restarted between then and now. + release_scdaemon + STATUS "Checking USB security dongle presence before sealing" + DEBUG "gate_reseal_with_integrity_report: checking HOTP token presence" + if hotp_verification info >/dev/null 2>&1; then + token_ok="y" + STATUS_OK "USB security dongle present and accessible" + break + fi + DEBUG "gate_reseal_with_integrity_report: HOTP token not accessible" + if ! whiptail_warning --title "USB Security Dongle Required" \ + --yes-button "Retry" --no-button "Abort" \ + --yesno "Your USB security dongle must be present before sealing new secrets.\n\nInsert the dongle and choose Retry, or Abort." 0 80; then + return 1 + fi + done + fi + + if ! whiptail_warning --title 'Integrity Gate Passed' \ + --yesno "Integrity checks completed.\n\nProceed with TOTP/HOTP reseal action?" 0 80; then + return 1 + fi + INTEGRITY_GATE_REQUIRED="n" + return 0 } generate_totp_hotp() { TRACE_FUNC - tpm_owner_password="$1" # May be empty, will prompt if needed and empty + tpm_owner_passphrase="$1" # May be empty, will prompt if needed and empty + if [ "$CONFIG_TPM" = "y" ] && tpm_reset_required; then + debug_tpm_reset_required_state + whiptail_error --title 'ERROR: TPM Reset Required' \ + --msgbox "Cannot generate a new TPM-backed TOTP/HOTP secret while TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." 0 80 + return 1 + fi 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" + STATUS "Generating new HOTP secret" /bin/seal-hotpkey.sh || - die "Failed to generate HOTP secret" - elif echo -e "Generating new TOTP secret...\n\n" && /bin/seal-totp.sh "$BOARD_NAME" "$tpm_owner_password"; then - echo + DIE "Failed to generate HOTP secret" + elif STATUS "Generating new TOTP secret" && /bin/seal-totp.sh "$BOARD_NAME" "$tpm_owner_passphrase"; then 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 + INPUT "Once you have scanned the QR code, press Enter to configure your HOTP USB Security dongle (e.g. Librem Key or Nitrokey)" fi TRACE_FUNC - /bin/seal-hotpkey.sh || die "Failed to generate HOTP secret" + /bin/seal-hotpkey.sh || 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" - read + INPUT "Once you have scanned the QR code, press Enter to continue" fi fi - # clear screen - printf "\033c" + clear + else + # seal-totp.sh already printed an explanatory error (e.g. missing + # primary handle) and guided the user to reset the TPM. Don't add + # confusing generic warnings here, just propagate failure. + return 1 + fi +} + +prompt_missing_gpg_key_action() { + TRACE_FUNC + local retry_label retry_msg + if [ "$CONFIG_HAVE_GPG_KEY_BACKUP" = "y" ]; then + retry_label=' Retry (insert signing card or backup USB drive)' + retry_msg="Cannot sign /boot because no private GPG signing key is available (card not inserted, wiped, or key not set up).\n\nInsert your signing card or backup USB drive and retry.\n\nHow would you like to proceed?" else - warn "Unsealing TOTP/HOTP secret from previous sealed measurements failed" - warn 'Try "Generate new HOTP/TOTP secret" option if you updated firmware content' + retry_label=' Retry (after connecting the correct signing card)' + retry_msg="Cannot sign /boot because no private GPG signing key is available (card not inserted, wiped, or key not set up).\n\nIf you have the correct signing card, insert it and retry.\n\nHow would you like to proceed?" fi + whiptail_error --title "ERROR: GPG signing key unavailable" \ + --menu "$retry_msg" 0 80 4 \ + 'r' "$retry_label" \ + 'F' ' OEM Factory Reset / Re-Ownership' \ + 'm' ' Return to main menu' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + r) + return 0 + ;; + F) + oem-factory-reset.sh + ;; + x) + recovery "User requested recovery shell" + ;; + m | *) + return 1 + ;; + esac } update_totp() { @@ -187,38 +353,58 @@ update_totp() { if [ "$CONFIG_TPM" != "y" ]; then TOTP="NO TPM" else - TOTP=$(unseal-totp) + TOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-totp.sh) if [ $? -ne 0 ]; then + local totp_menu_text + INTEGRITY_GATE_REQUIRED="y" BG_COLOR_MAIN_MENU="error" if [ "$skip_to_menu" = "true" ]; then return 1 # Already asked to skip to menu from a prior error fi - DEBUG "CONFIG_TPM: $CONFIG_TPM" - DEBUG "CONFIG_TPM2_TOOLS: $CONFIG_TPM2_TOOLS" - DEBUG "Show PCRs" DEBUG "$(pcrs)" + totp_menu_text=$( + cat </tmp/whiptail || recovery "GUI menu failed" option=$(cat /tmp/whiptail) case "$option" in g) - if (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ + if tpm_reset_required; then + debug_tpm_reset_required_state + whiptail_error --title 'ERROR: TPM Reset Required' \ + --msgbox "Cannot generate a new TPM-backed TOTP/HOTP secret while TPM state is inconsistent.\n\nReset the TPM first (Options -> TPM/TOTP/HOTP Options -> Reset the TPM)." 0 80 + return 1 + elif gate_reseal_with_integrity_report && (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then - generate_totp_hotp && update_totp && BG_COLOR_MAIN_MENU="normal" && reseal_tpm_disk_decryption_key + if generate_totp_hotp; then + update_totp || true + BG_COLOR_MAIN_MENU="normal" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi fi ;; i) @@ -226,12 +412,16 @@ update_totp() { return 1 ;; p) - reset_tpm && update_totp && BG_COLOR_MAIN_MENU="normal" && reseal_tpm_disk_decryption_key + if gate_reseal_with_integrity_report && reset_tpm && update_totp && BG_COLOR_MAIN_MENU="normal"; then + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi ;; x) recovery "User requested recovery shell" ;; esac + else + INTEGRITY_GATE_REQUIRED="n" fi fi } @@ -239,47 +429,118 @@ update_totp() { update_hotp() { TRACE_FUNC HOTP="Unverified" - if [ -x /bin/hotp_verification ]; then - if ! hotp_verification info; then - if [ "$skip_to_menu" = "true" ]; then - return 1 # Already asked to skip to menu from a prior error - fi - if ! whiptail_warning \ - --title "WARNING: Please Insert Your $HOTPKEY_BRANDING" \ - --yes-button "Retry" --no-button "Skip" \ - --yesno "Your $HOTPKEY_BRANDING was not detected.\n\nPlease insert your $HOTPKEY_BRANDING" 0 80; then - HOTP="Error checking code, Insert $HOTPKEY_BRANDING and retry" - BG_COLOR_MAIN_MENU="warning" - return - fi + if [ ! -x /bin/hotp_verification ]; then + HOTP='N/A' + return + fi + + local hotp_token_info hotp_exit attempt + + # Ensure dongle is present; capture info for PIN counter display + if ! hotp_token_info="$(hotp_verification info)"; then + if [ "$skip_to_menu" = "true" ]; then + return 1 # Already asked to skip to menu from a prior error fi - HOTP=$(unseal-hotp) + if ! whiptail_warning \ + --title "WARNING: Please Insert Your $DONGLE_BRAND" \ + --yes-button "Retry" --no-button "Skip" \ + --yesno "Your $DONGLE_BRAND was not detected.\n\nPlease insert your $DONGLE_BRAND" 0 80; then + HOTP="Error checking code, Insert $DONGLE_BRAND and retry" + BG_COLOR_MAIN_MENU="warning" + return + fi + if ! hotp_token_info="$(hotp_verification info)"; then + HOTP="Error checking code, Insert $DONGLE_BRAND and retry" + BG_COLOR_MAIN_MENU="warning" + return + fi + fi + + # Show dongle firmware version with color coding so users know when to upgrade + hotpkey_fw_display "$hotp_token_info" "$DONGLE_BRAND" + + # Unseal HOTP secret from TPM once; if this fails don't proceed at all + HOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-hotp.sh) + if [ -z "$HOTP" ]; then + WARN "Unable to unseal HOTP secret from TPM" + HOTP="Error checking code, Insert $DONGLE_BRAND and retry" + BG_COLOR_MAIN_MENU="warning" + return + fi + + # Try HOTP check up to 3 times. + # Retries handle transient USB/timing failures; a definitive code mismatch + # (exit 4 or 7) breaks immediately since the same code won't verify again. + # PIN retry count is shown only before a retry so normal boots stay silent. + for attempt in 1 2 3; do # Don't output HOTP codes to screen, so as to make replay attacks harder hotp_verification check "$HOTP" - case "$?" in + hotp_exit=$? + case "$hotp_exit" in 0) HOTP="Success" BG_COLOR_MAIN_MENU="normal" + return ;; - 4 | 7) # 4: code was incorrect, 7: code was not a valid HOTP code at all + 4 | 7) # 4: code incorrect, 7: not a valid HOTP code — no point retrying same code HOTP="Invalid code" BG_COLOR_MAIN_MENU="error" + break ;; - *) - HOTP="Error checking code, Insert $HOTPKEY_BRANDING and retry" + 6) # EXIT_SLOT_NOT_PROGRAMMED — sealing was never completed or failed mid-way + HOTP="HOTP slot not configured" BG_COLOR_MAIN_MENU="warning" + break + ;; + *) + # Transient error (USB glitch etc.) — retry if attempts remain + if [ "$attempt" -lt 3 ]; then + WARN "HOTP check failed (attempt $attempt/3), retrying" + else + HOTP="Error checking code, Insert $DONGLE_BRAND and retry" + BG_COLOR_MAIN_MENU="warning" + fi ;; esac - else - HOTP='N/A' - fi + done - 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 + if [[ "$HOTP" = "HOTP slot not configured" ]]; then + WARN "$DONGLE_BRAND HOTP slot is not configured" + STATUS "Verify TOTP against your phone to confirm TPM is intact, then press Escape to continue" + show_totp_until_esc + whiptail_warning --title "HOTP Not Configured" \ + --menu "The HOTP slot on your $DONGLE_BRAND is not configured.\n\nThis can happen if HOTP sealing was interrupted (connection error, dongle removed during setup).\n\nPlease generate a new TOTP/HOTP secret to configure it." 0 80 2 \ + 'g' ' Generate new TOTP/HOTP secret' \ + 'x' ' Exit to recovery shell' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + g) + if gate_reseal_with_integrity_report && (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ + --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then + if generate_totp_hotp; then + update_totp || true + HOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-hotp.sh) + [ -n "$HOTP" ] && hotp_verification check "$HOTP" >/dev/null 2>&1 && HOTP="Success" + BG_COLOR_MAIN_MENU="normal" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi + fi + ;; + x) + recovery "User requested recovery shell" + ;; + esac + return + elif [[ "$HOTP" = "Invalid code" ]]; then + INTEGRITY_GATE_REQUIRED="y" + STATUS "HOTP failed - verify TOTP against your phone to confirm TPM integrity, then press Escape to continue" + show_totp_until_esc + local hotp_error_msg + hotp_error_msg="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?" 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 \ + --menu "$hotp_error_msg" 0 80 3 \ 'g' ' Generate new TOTP/HOTP secret' \ 'i' ' Ignore error and continue to main menu' \ 'x' ' Exit to recovery shell' \ @@ -288,9 +549,15 @@ update_hotp() { option=$(cat /tmp/whiptail) case "$option" in g) - if (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ + if gate_reseal_with_integrity_report && (whiptail_warning --title 'Generate new TOTP/HOTP secret' \ --yesno "This will erase your old secret and replace it with a new one!\n\nDo you want to proceed?" 0 80); then - generate_totp_hotp && BG_COLOR_MAIN_MENU="normal" && reseal_tpm_disk_decryption_key + if generate_totp_hotp; then + update_totp || true + HOTP=$(HEADS_NONFATAL_UNSEAL=y unseal-hotp.sh) + [ -n "$HOTP" ] && hotp_verification check "$HOTP" >/dev/null 2>&1 && HOTP="Success" + BG_COLOR_MAIN_MENU="normal" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi fi ;; i) @@ -300,6 +567,22 @@ update_hotp() { recovery "User requested recovery shell" ;; esac + elif [[ "$HOTP" = "Error checking code"* ]]; then + INTEGRITY_GATE_REQUIRED="y" + STATUS "HOTP verification failed after 3 retries - verify TOTP against your phone to confirm TPM integrity, then press Escape to continue" + show_totp_until_esc + whiptail_warning --title "HOTP Verification Failed" \ + --menu "The $DONGLE_BRAND could not be verified after multiple attempts.\n\nThis may indicate a USB connection issue or dongle problem.\n\nPlease insert your $DONGLE_BRAND and try again, or verify TOTP to continue." 0 80 2 \ + 'r' ' Retry HOTP verification' \ + 'i' ' Ignore and continue to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + option=$(cat /tmp/whiptail) + case "$option" in + r) update_hotp ;; + i) INTEGRITY_GATE_REQUIRED="n" ;; + esac + else + INTEGRITY_GATE_REQUIRED="n" fi } @@ -339,8 +622,10 @@ check_gpg_key() { if [ "$skip_to_menu" = "true" ]; then return 1 # Already asked to skip to menu from a prior error fi + local gpg_error_msg + gpg_error_msg="ERROR: $CONFIG_BRAND_NAME couldn't find any GPG keys in your keyring.\n\nIf this is the first time the system has booted, you should add a public GPG key to the BIOS now.\n\nIf you just reflashed a new BIOS, you'll need to add at least one public key to the keyring.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" whiptail_error --title "ERROR: GPG keyring empty!" \ - --menu "ERROR: $CONFIG_BRAND_NAME couldn't find any GPG keys in your keyring.\n\nIf this is the first time the system has booted,\nyou should add a public GPG key to the BIOS now.\n\nIf you just reflashed a new BIOS, you'll need to add at least one\npublic key to the keyring.\n\nIf you have not just reflashed your BIOS, THIS COULD INDICATE TAMPERING!\n\nHow would you like to proceed?" 0 80 4 \ + --menu "$gpg_error_msg" 0 80 4 \ 'g' ' Add a GPG key to the running BIOS' \ 'F' ' OEM Factory Reset / Re-Ownership' \ 'i' ' Ignore error and continue to main menu' \ @@ -369,9 +654,9 @@ check_gpg_key() { prompt_auto_default_boot() { TRACE_FUNC - echo -e "\nHOTP verification success\n\n" + STATUS_OK "HOTP verification success" if pause_automatic_boot; then - echo -e "\n\nAttempting default boot...\n\n" + STATUS "Attempting default boot" attempt_default_boot fi } @@ -403,7 +688,7 @@ show_main_menu() { show_system_info ;; p) - poweroff + poweroff.sh ;; esac } @@ -414,6 +699,7 @@ show_options_menu() { --menu "" 0 80 10 \ 'b' ' Boot Options -->' \ 't' ' TPM/TOTP/HOTP Options -->' \ + 'i' ' Investigate integrity discrepancies -->' \ 'h' ' Change system time' \ 'u' ' Update checksums and sign all files in /boot' \ 'c' ' Change configuration settings -->' \ @@ -435,6 +721,9 @@ show_options_menu() { t) show_tpm_totp_hotp_options_menu ;; + i) + investigate_integrity_discrepancies + ;; h) change-time.sh ;; @@ -451,7 +740,7 @@ show_options_menu() { gpg-gui.sh ;; F) - oem-factory-reset + oem-factory-reset.sh ;; C) luks_reencrypt @@ -510,10 +799,17 @@ show_tpm_totp_hotp_options_menu() { option=$(cat /tmp/whiptail) case "$option" in g) - generate_totp_hotp && reseal_tpm_disk_decryption_key + if gate_reseal_with_integrity_report && generate_totp_hotp; then + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + # If reseal did not reboot (no LUKS devices), refresh display so + # the user sees the new TOTP/HOTP state without a manual 'r' + update_totp && update_hotp || true + fi ;; r) - reset_tpm && reseal_tpm_disk_decryption_key + if gate_reseal_with_integrity_report && reset_tpm; then + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi ;; t) prompt_totp_mismatch @@ -534,31 +830,29 @@ reset_tpm() { TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then if (whiptail_warning --title 'Reset the TPM' \ - --yesno "This will clear the TPM and replace its Owner password with a new one!\n\nDo you want to proceed?" 0 80); then + --yesno "This will clear the TPM and replace its Owner passphrase with a new one!\n\nDo you want to proceed?" 0 80); then - if ! prompt_new_owner_password; then - echo "Press Enter to return to the menu..." - read - echo + if ! prompt_new_owner_passphrase; then + INPUT "Press Enter to return to the menu..." return 1 fi - tpmr.sh reset "$tpm_owner_password" + tpmr.sh reset "$tpm_owner_passphrase" # now that the TPM is reset, remove invalid TPM counter files mount_boot mount -o rw,remount /boot #TODO: this is really problematic, we should really remove the primary handle hash - INFO "Removing rollback and primary handle hashes under /boot" + STATUS "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" + check_tpm_counter /boot/kexec_rollback.txt "" "$tpm_owner_passphrase" || + DIE "Unable to find/create tpm counter" TRACE_FUNC @@ -566,29 +860,47 @@ reset_tpm() { DEBUG "TPM_COUNTER: $TPM_COUNTER" #TPM_COUNTER can be empty - increment_tpm_counter $TPM_COUNTER >/dev/null 2>&1 || - die "Unable to increment tpm counter" + increment_tpm_counter "$TPM_COUNTER" "$tpm_owner_passphrase" || + DIE "Unable to increment tpm counter" DO_WITH_DEBUG sha256sum /tmp/counter-$TPM_COUNTER >/boot/kexec_rollback.txt || - die "Unable to create rollback file" + 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 - if ! update_checksums; then - whiptail_error --title 'ERROR' \ - --msgbox "Failed to update checksums / sign default config" 0 80 + # USB is already initialized at startup; run gpg --card-status to populate key stub. + wait_for_gpg_card || true + while true; do + GPG_KEY_COUNT=$(gpg -K 2>/dev/null | wc -l) + if [ "$GPG_KEY_COUNT" -eq 0 ]; then + prompt_missing_gpg_key_action || return 1 + wait_for_gpg_card || true + else + STATUS_OK "TPM reset successful - updating /boot checksums and signatures" + if ! update_checksums; then + whiptail_error --title 'ERROR' \ + --msgbox "Failed to update checksums / sign default config" 0 80 + return 1 + fi + break fi - else - warn "TPM reset successful, but user chose not to update+sign /boot checksums. Rebooting" - reboot - fi + done mount -o ro,remount /boot - generate_totp_hotp "$tpm_owner_password" + # Reset completed and reseal prerequisites were rebuilt. + # Clear stale preflight marker before generating fresh TOTP/HOTP. + clear_tpm_reset_required + + if ! generate_totp_hotp "$tpm_owner_passphrase"; then + return 1 + fi + + if [ -s /boot/kexec_key_devices.txt ] || [ -s /boot/kexec_key_lvm.txt ]; then + STATUS_OK "TPM reset successful - resealing TPM Disk Unlock Key (DUK)" + reseal_tpm_disk_decryption_key || prompt_missing_gpg_key_action + fi else - echo "Returning to the main menu" + INFO "Returning to the main menu" fi else whiptail_error --title 'ERROR: No TPM Detected' --msgbox "This device does not have a TPM.\n\nPress OK to return to the Main Menu" 0 80 @@ -638,17 +950,15 @@ force_unsafe_boot() { # gui-init start TRACE_FUNC -# Use stored HOTP key branding -if [ -r /boot/kexec_hotp_key ]; then - HOTPKEY_BRANDING="$(cat /boot/kexec_hotp_key)" -else - HOTPKEY_BRANDING="HOTP USB Security dongle" -fi - if [ -x /bin/hotp_verification ]; then enable_usb fi +# Detect dongle branding from USB VID:PID -- must run AFTER enable_usb so lsusb +# can see the dongle (NK3 enumerates ~1 second after USB module load). +DONGLE_BRAND="$(detect_usb_security_dongle_branding)" +export DONGLE_BRAND + if detect_boot_device; then # /boot device with installed OS found clean_boot_check @@ -658,15 +968,104 @@ else mount_boot fi +# Fail early on rollback-counter inconsistencies before presenting TOTP/HOTP +# recovery prompts. This avoids guiding users into reseal flows when TPM +# rollback state is already invalid. +rollback_preflight_failed="n" +if ! preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="y" + BG_COLOR_MAIN_MENU="error" + preflight_error_msg="$(cat /tmp/rollback_preflight_error 2>/dev/null)" + if [ -z "$preflight_error_msg" ]; then + preflight_error_msg="TPM rollback counter state could not be validated." + fi + [ -n "$preflight_error_msg" ] && DEBUG "Rollback preflight failure: $preflight_error_msg" + + # Show the actual diagnostic directly so the user knows exactly why. + # Strip the "Reset TPM from GUI..." action guidance that fail_preflight appends + # since the menu already offers those actions. + preflight_reason="${preflight_error_msg%%. Reset TPM from GUI*}" + [ -z "$preflight_reason" ] && preflight_reason="TPM rollback counter state could not be validated." + + preflight_menu_text=$( + cat <' \ + 'o' ' OEM Factory Reset / Re-Ownership -->' \ + 't' ' Reset the TPM' \ + 'm' ' Continue to main menu' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + + option=$(cat /tmp/whiptail) + case "$option" in + i) + report_integrity_measurements + _preflight_report_shown="y" + export INTEGRITY_REPORT_ALREADY_SHOWN=1 + ;; + o) + oem-factory-reset.sh + if preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="n" + BG_COLOR_MAIN_MENU="normal" + fi + ;; + t) + if reset_tpm && preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then + rollback_preflight_failed="n" + BG_COLOR_MAIN_MENU="normal" + fi + ;; + m | *) + break + ;; + esac + if [ "$rollback_preflight_failed" = "y" ]; then + preflight_error_msg="$(cat /tmp/rollback_preflight_error 2>/dev/null)" + [ -n "$preflight_error_msg" ] && DEBUG "Rollback preflight failure: $preflight_error_msg" + fi + done +fi + # detect whether any GPG keys exist in the keyring, if not, initialize that first -check_gpg_key -# Even if GPG init fails, still try to update TOTP/HOTP so the main menu can -# show the correct status. -update_totp -update_hotp - -if [ "$HOTP" = "Success" -a -n "$CONFIG_AUTO_BOOT_TIMEOUT" ]; then - prompt_auto_default_boot +if [ "$rollback_preflight_failed" != "y" ]; then + check_gpg_key + # Even if GPG init fails, still try to update TOTP/HOTP so the main menu can + # show the correct status. + update_totp && update_hotp + + if [ "$HOTP" = "Success" -a -n "$CONFIG_AUTO_BOOT_TIMEOUT" ]; then + prompt_auto_default_boot + fi fi while true; do diff --git a/initrd/bin/inject_firmware.sh b/initrd/bin/inject_firmware.sh index ee1bfdddf..b7a5ca763 100755 --- a/initrd/bin/inject_firmware.sh +++ b/initrd/bin/inject_firmware.sh @@ -51,7 +51,7 @@ done # awk will happily pass through a binary file, so look for the match we want # before modifying init to ensure it's a shell script and not an ELF, etc. if ! grep -E -q '^exec run-init .*\$\{rootmnt\}' "$INITRD_ROOT/init"; then - warn "Can't apply firmware blob jail, unknown init script" + WARN "Can't apply firmware blob jail, unknown init script" exit 0 fi diff --git a/initrd/bin/kexec-boot.sh b/initrd/bin/kexec-boot.sh index 808597e1f..4043c3118 100755 --- a/initrd/bin/kexec-boot.sh +++ b/initrd/bin/kexec-boot.sh @@ -22,7 +22,7 @@ while getopts "b:e:r:a:o:fi" arg; do done if [ -z "$bootdir" -o -z "$entry" ]; then - die "Usage: $0 -b /boot -e 'kexec params|...|...'" + DIE "Usage: $0 -b /boot -e 'kexec params|...|...'" fi bootdir="${bootdir%%/}" @@ -47,7 +47,7 @@ fix_file_path() { filepath="$bootdir$firstval" if ! [ -r $filepath ]; then - die "Failed to find file $firstval" + DIE "Failed to find file $firstval" fi } @@ -92,7 +92,7 @@ do kexeccmd="$kexeccmd -l $filepath" DEBUG "kexeccmd= $kexeccmd" else - DEBUG "unknown kexectype!!!!" + DEBUG "unknown kexectype" kexeccmd="$kexeccmd -l $filepath" fi fi @@ -143,26 +143,25 @@ if [ "$adjusted_cmd_line" = "n" ]; then if [ "$kexectype" = "elf" ]; then kexeccmd="$kexeccmd --append=\"$cmdadd\"" else - die "Failed to add required kernel commands: $cmdadd" + DIE "Failed to add required kernel commands: $cmdadd" fi fi if [ "$dryrun" = "y" ]; then exit 0; fi -echo "Loading the new kernel:" -echo "$kexeccmd" +STATUS "Loading the new kernel" +DEBUG "kexec command: $kexeccmd" # DO_WITH_DEBUG captures the debug output from stderr to the log, we don't need # it on the console as well DO_WITH_DEBUG eval "$kexeccmd" 2>/dev/null \ -|| die "Failed to load the new kernel" +|| DIE "Failed to load the new kernel" if [ "$CONFIG_DEBUG_OUTPUT" = "y" ];then #Ask user if they want to continue booting without echoing back the input (-s) - read -s -n 1 -p "[DEBUG] Continue booting? [Y/n]: " debug_boot_confirm - echo + INPUT "[DEBUG] Continue booting? [Y/n]:" -s -n 1 debug_boot_confirm if [ "${debug_boot_confirm^^}" = N ]; then # abort - die "Boot aborted" + DIE "Boot aborted" fi fi @@ -171,8 +170,13 @@ if [ "$CONFIG_TPM" = "y" ]; then fi if [ -x /bin/io386 -a "$CONFIG_FINALIZE_PLATFORM_LOCKING" = "y" ]; then - lock_chip + lock_chip.sh fi -echo "Starting the new kernel" +if [ "$CONFIG_BRAND_NAME" = "Heads" ]; then + STATUS_OK "Heads firmware job done - handing off to your OS. Consider donating: https://opencollective.com/insurgo" + qrenc "https://opencollective.com/insurgo" +else + STATUS_OK "$CONFIG_BRAND_NAME firmware job done - starting your OS" +fi exec kexec -e diff --git a/initrd/bin/kexec-insert-key.sh b/initrd/bin/kexec-insert-key.sh index e26518c7e..883ec8897 100755 --- a/initrd/bin/kexec-insert-key.sh +++ b/initrd/bin/kexec-insert-key.sh @@ -11,26 +11,27 @@ TMP_KEY_LVM="/tmp/kexec/kexec_key_lvm.txt" INITRD="$1" if [ -z "$INITRD" ]; then - die "Usage: $0 /boot/initramfs... " + DIE "Usage: $0 /boot/initramfs... " fi if [ ! -r "$TMP_KEY_DEVICES" ]; then - die "No devices defined for disk encryption" + DIE "No devices defined for disk encryption" fi if [ -r "$TMP_KEY_LVM" ]; then # Activate the LVM volume group VOLUME_GROUP=$(cat $TMP_KEY_LVM) if [ -z "$TMP_KEY_LVM" ]; then - die "No LVM volume group defined for activation" + DIE "No LVM volume group defined for activation" fi - lvm vgchange -a y $VOLUME_GROUP || - die "$VOLUME_GROUP: unable to activate volume group" + run_lvm vgchange -a y $VOLUME_GROUP || + DIE "$VOLUME_GROUP: unable to activate volume group" fi # Measure the LUKS headers before we unseal the LUKS Disk Unlock Key from TPM -cat "$TMP_KEY_DEVICES" | cut -d\ -f1 | xargs /bin/qubes-measure-luks || - die "LUKS measure failed" +STATUS "Measuring LUKS headers" +cat "$TMP_KEY_DEVICES" | cut -d\ -f1 | xargs /bin/qubes-measure-luks.sh || + DIE "LUKS measure failed" # Unpack the initrd and fixup the crypttab # this is a hack to split it into two parts since @@ -42,72 +43,65 @@ mkdir -p "$INITRD_DIR/etc" if [ -e /boot/kexec_lukshdr_hash.txt ] && [ -e /tmp/luksDump.txt ]; then if ! cmp -s /boot/kexec_lukshdr_hash.txt /tmp/luksDump.txt >/dev/null 2>&1; then - #LUKS header hash part of detached signed hash digest under boot doesn't match qubes-measure-luks tmp file - warn "Encrypted disk keys have changed since the TPM Disk Unlock Key was sealed. If you did not make this change, the disk may be compromised" + #LUKS header hash part of detached signed hash digest under boot doesn't match qubes-measure-luks.sh tmp file + WARN "Encrypted disk keys have changed since the TPM Disk Unlock Key was sealed. If you did not make this change, the disk may be compromised" exit 1 else #LUKS header hash part of detached signed hash digest matches - echo "+++ Encrypted disk keys have not been changed since sealed in TPM Disk Unlock Key" - #TODO: remove "+++" with boot info helper when added, same with "!!!" currently for info. + STATUS_OK "Encrypted disk keys have not changed since sealed in TPM Disk Unlock Key" fi else - warn "Could not check for tampering of Encrypted disk keys" - warn "Re-seal the TPM Disk Unlock Key by re-selecting your default boot option to enable this check (Options -> Boot Options -> Show OS boot menu)." + WARN "Could not check for tampering of Encrypted disk keys" + WARN "Re-seal the TPM Disk Unlock Key by re-selecting your default boot option to enable this check (Options -> Boot Options -> Show OS boot menu)." fi # Attempt to unseal the Disk Unlock Key from the TPM # should we give this some number of tries? unseal_failed="n" -if ! kexec-unseal-key "$INITRD_DIR/secret.key"; then +if ! kexec-unseal-key.sh "$INITRD_DIR/secret.key"; then unseal_failed="y" - echo - echo "!!! Failed to unseal the TPM LUKS Disk Unlock Key" + WARN "Failed to unseal the TPM LUKS Disk Unlock Key" fi # Override PCR 4 so that user can't read the key TRACE_FUNC INFO "TPM: Extending PCR[4] to prevent any future secret unsealing" tpmr.sh extend -ix 4 -ic generic || - die 'Unable to scramble PCR' + DIE 'Unable to scramble PCR' # Check to continue if [ "$unseal_failed" = "y" ]; then confirm_boot="n" - read \ - -n 1 \ - -p "Do you wish to boot and use the LUKS Disk Recovery Key? [Y/n] " \ - confirm_boot - echo + INPUT "Do you wish to boot and use the LUKS Disk Recovery Key? [Y/n]:" -n 1 confirm_boot if [ "$confirm_boot" != 'y' \ -a "$confirm_boot" != 'Y' \ -a -n "$confirm_boot" ] \ ; then - die "!!! Aborting boot due to failure to unseal TPM Disk Unlock Key" + DIE "Aborting boot due to failure to unseal TPM Disk Unlock Key" fi fi -echo -echo '+++ Building initrd' +STATUS "Building initrd" # pad the initramfs (dracut doesn't pad the last gz blob) # without this the kernel init/initramfs.c fails to read # the subsequent uncompressed/compressed cpio dd if="$INITRD" of="$SECRET_CPIO" bs=512 conv=sync > /dev/null 2>&1 || - die "Failed to copy initrd to /tmp" + DIE "Failed to copy initrd to /tmp" if [ "$unseal_failed" = "n" ]; then - # kexec-save-default might have created crypttab overrides to be injected in initramfs through additional cpio + # kexec-save-default.sh might have created crypttab overrides to be injected in initramfs through additional cpio if [ -r "$bootdir/kexec_initrd_crypttab_overrides.txt" ]; then - echo "+++ $bootdir/kexec_initrd_crypttab_overrides.txt found..." - echo "+++ Preparing initramfs crypttab overrides as defined under $bootdir/kexec_initrd_crypttab_overrides.txt to be injected through cpio at next kexec call..." - # kexec-save-default has found crypttab files under initrd and saved them + DEBUG "$bootdir/kexec_initrd_crypttab_overrides.txt found" + DEBUG "Preparing initramfs crypttab overrides from $bootdir/kexec_initrd_crypttab_overrides.txt" + # kexec-save-default.sh has found crypttab files under initrd and saved them cat "$bootdir/kexec_initrd_crypttab_overrides.txt" | while read line; do crypttab_file=$(echo "$line" | awk -F ':' {'print $1'}) crypttab_entry=$(echo "$line" | awk -F ':' {'print $NF'}) # Replace each initrd crypttab file with modified entry containing /secret.key path mkdir -p "$INITRD_DIR/$(dirname $crypttab_file)" echo "$crypttab_entry" | tee -a "$INITRD_DIR/$crypttab_file" >/dev/null - echo "+++ initramfs's $crypttab_file will be overriden with: $crypttab_entry" + DEBUG "initramfs $crypttab_file will be overridden with: $crypttab_entry" done else # No crypttab files were found under selected default boot option's initrd file @@ -116,10 +110,10 @@ if [ "$unseal_failed" = "n" ]; then for crypttab_file in $crypttab_files; do mkdir -p "$INITRD_DIR/$(dirname $crypttab_file)" # overwrite crypttab to mirror behavior of seal-key - echo "+++ The following $crypttab_file overrides will be passed through concatenated secret/initrd.cpio at kexec call:" + DEBUG "The following $crypttab_file overrides will be injected via cpio at kexec:" for uuid in $(cat "$TMP_KEY_DEVICES" | cut -d\ -f2); do # NOTE: discard operation (TRIM) is activated by default if no crypptab found in initrd - echo "luks-$uuid UUID=$uuid /secret.key luks,discard" | tee -a "$INITRD_DIR/$crypttab_file" + echo "luks-$uuid UUID=$uuid /secret.key luks,discard" >> "$INITRD_DIR/$crypttab_file" done done fi @@ -128,3 +122,4 @@ if [ "$unseal_failed" = "n" ]; then find . -type f | cpio -H newc -o ) >>"$SECRET_CPIO" fi +STATUS_OK "Initrd prepared for kexec boot" diff --git a/initrd/bin/kexec-iso-init.sh b/initrd/bin/kexec-iso-init.sh index 73af82a15..fa7b85ce9 100755 --- a/initrd/bin/kexec-iso-init.sh +++ b/initrd/bin/kexec-iso-init.sh @@ -11,7 +11,7 @@ MOUNTED_ISO_PATH="$1" ISO_PATH="$2" DEV="$3" -echo '+++ Verifying ISO' +STATUS "Verifying ISO" # Verify the signature on the hashes ISOSIG="$MOUNTED_ISO_PATH.sig" if ! [ -r "$ISOSIG" ]; then @@ -22,36 +22,35 @@ ISO_PATH="${ISO_PATH##/}" if [ -r "$ISOSIG" ]; then # Signature found, verify it - gpgv --homedir=/etc/distro/ "$ISOSIG" "$MOUNTED_ISO_PATH" || - die 'ISO signature failed' - echo '+++ ISO signature verified' + gpgv.sh --homedir=/etc/distro/ "$ISOSIG" "$MOUNTED_ISO_PATH" \ + || DIE 'ISO signature failed' + STATUS_OK "ISO signature verified" else # No signature found, prompt user with warning - echo '+++ WARNING: No signature found for ISO' + WARN "No signature found for ISO" if [ -x /bin/whiptail ]; then if ! whiptail_warning --title 'UNSIGNED ISO WARNING' --yesno \ "WARNING: UNSIGNED ISO DETECTED\n\nThe selected ISO file:\n$MOUNTED_ISO_PATH\n\nDoes not have a detached signature (.sig or .asc file).\n\n\nThis means the integrity and authenticity of the ISO cannot be verified.\nBooting unsigned ISOs is potentially unsafe.\n\nDo you want to proceed with booting this unsigned ISO?" \ 0 80; then - die "Unsigned ISO boot cancelled by user" + DIE "Unsigned ISO boot cancelled by user" fi else - echo "WARNING: The selected ISO file does not have a detached signature" - echo "This means the integrity and authenticity cannot be verified" - echo "Booting unsigned ISOs is potentially unsafe" - read -n1 -p "Do you want to proceed anyway? (y/N): " response - echo + WARN "The selected ISO file does not have a detached signature" + WARN "Integrity and authenticity of the ISO cannot be verified" + WARN "Booting unsigned ISOs is potentially unsafe" + INPUT "Do you want to proceed anyway? (y/N):" -n 1 response if [ "$response" != "y" ] && [ "$response" != "Y" ]; then - die "Unsigned ISO boot cancelled by user" + DIE "Unsigned ISO boot cancelled by user" fi fi - echo '+++ Proceeding with unsigned ISO boot' + NOTE "Proceeding with unsigned ISO boot" fi -echo '+++ Mounting ISO and booting' -mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot || - die '$MOUNTED_ISO_PATH: Unable to mount /boot' +STATUS "Mounting ISO and booting" +mount -t iso9660 -o loop $MOUNTED_ISO_PATH /boot \ + || DIE '$MOUNTED_ISO_PATH: Unable to mount /boot' -DEV_UUID=$(blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2) +DEV_UUID=`blkid $DEV | tail -1 | tr " " "\n" | grep UUID | cut -d\" -f2` ADD="fromiso=/dev/disk/by-uuid/$DEV_UUID/$ISO_PATH img_dev=/dev/disk/by-uuid/$DEV_UUID iso-scan/filename=/${ISO_PATH} img_loop=$ISO_PATH iso=$DEV_UUID/$ISO_PATH" REMOVE="" @@ -60,20 +59,20 @@ check_config $paramsdir ADD_FILE=/tmp/kexec/kexec_iso_add.txt if [ -r $ADD_FILE ]; then - NEW_ADD=$(cat $ADD_FILE) + NEW_ADD=`cat $ADD_FILE` ADD=$(eval "echo \"$NEW_ADD\"") fi -echo "+++ Overriding standard ISO kernel arguments with additions: $ADD" +DEBUG "Overriding ISO kernel arguments with additions: $ADD" REMOVE_FILE=/tmp/kexec/kexec_iso_remove.txt if [ -r $REMOVE_FILE ]; then - NEW_REMOVE=$(cat $REMOVE_FILE) + NEW_REMOVE=`cat $REMOVE_FILE` REMOVE=$(eval "echo \"$NEW_REMOVE\"") fi -echo "+++ Overriding standard ISO kernel arguments with suppressions: $REMOVE" +DEBUG "Overriding ISO kernel arguments with suppressions: $REMOVE" # Call kexec and indicate that hashes have been verified DO_WITH_DEBUG kexec-select-boot.sh -b /boot -d /media -p "$paramsdir" \ -a "$ADD" -r "$REMOVE" -c "*.cfg" -u -i -die "Something failed in selecting boot" +DIE "Something failed in selecting boot" diff --git a/initrd/bin/kexec-parse-bls.sh b/initrd/bin/kexec-parse-bls.sh index 30f7ce69d..98b1a3020 100755 --- a/initrd/bin/kexec-parse-bls.sh +++ b/initrd/bin/kexec-parse-bls.sh @@ -9,7 +9,7 @@ blsdir="$3" kernelopts="" if [ -z "$bootdir" -o -z "$file" ]; then - die "Usage: $0 /boot /boot/grub/grub.cfg blsdir" + DIE "Usage: $0 /boot /boot/grub/grub.cfg blsdir" fi reset_entry() { diff --git a/initrd/bin/kexec-parse-boot.sh b/initrd/bin/kexec-parse-boot.sh index e5e382425..852bc00ee 100755 --- a/initrd/bin/kexec-parse-boot.sh +++ b/initrd/bin/kexec-parse-boot.sh @@ -8,7 +8,7 @@ bootdir="$1" file="$2" if [ -z "$bootdir" -o -z "$file" ]; then - die "Usage: $0 /boot /boot/grub/grub.cfg" + DIE "Usage: $0 /boot /boot/grub/grub.cfg" fi reset_entry() { diff --git a/initrd/bin/kexec-save-default.sh b/initrd/bin/kexec-save-default.sh index 0db91a955..551c934d9 100755 --- a/initrd/bin/kexec-save-default.sh +++ b/initrd/bin/kexec-save-default.sh @@ -16,7 +16,7 @@ while getopts "b:d:p:i:" arg; do done if [ -z "$bootdir" -o -z "$index" ]; then - die "Usage: $0 -b /boot -i menu_option " + DIE "Usage: $0 -b /boot -i menu_option " fi if [ -z "$paramsdev" ]; then @@ -38,7 +38,7 @@ PRIMHASH_FILE="$paramsdir/kexec_primhdl_hash.txt" KEY_DEVICES="$paramsdir/kexec_key_devices.txt" KEY_LVM="$paramsdir/kexec_key_lvm.txt" -lvm_suggest=$(lvm vgscan 2>/dev/null | awk -F '"' {'print $1'} | tail -n +2) +lvm_suggest=$(run_lvm vgscan 2>/dev/null | awk -F '"' {'print $1'} | tail -n +2) num_lvm=$(echo "$lvm_suggest" | wc -l) if [ "$num_lvm" -eq 1 ] && [ -n "$lvm_suggest" ]; then lvm_volume_group="$lvm_suggest" @@ -91,36 +91,41 @@ prompt_for_existing_encrypted_lvms_or_disks() { declare -a key_lvms_array while [ $selected_lvms_not_existing -ne 0 ]; do - { - # Read the user input and store it in a variable - read \ - -p "Encrypted LVMs? (choose between/all: $lvm_suggest): " \ - key_lvms - - # Split the user input by spaces and add each element to the array - IFS=' ' read -r -a key_lvms_array <<<"$key_lvms" - - # Loop through the array and check if each element is in the lvms_array - valid=1 - for lvm in "${key_lvms_array[@]}"; do - if [[ ! ${lvms_array[$lvm]+_} ]]; then - # If not found, set the flag to indicate invalid input - valid=0 - break - fi - done - - # If valid, set the flag to indicate valid input - if [[ $valid -eq 1 ]]; then - selected_lvms_not_existing=0 + # Read the user input and store it in a variable + INPUT "Encrypted LVMs? (type 'all' to select all, or space-separated subset of: $lvm_suggest):" -r key_lvms + + # 'all' expands to every discovered LVM + if [ "$key_lvms" = "all" ]; then + key_lvms="$lvm_suggest" + DEBUG "User chose 'all' LVMs: $key_lvms" + selected_lvms_not_existing=0 + continue + fi + + # Split the user input by spaces and validate each element + IFS=' ' read -r -a key_lvms_array <<<"$key_lvms" + + if [ ${#key_lvms_array[@]} -eq 0 ]; then + continue + fi + + valid=1 + for lvm in "${key_lvms_array[@]}"; do + if [[ ! ${lvms_array[$lvm]+_} ]]; then + valid=0 + break fi - } + done + + if [[ $valid -eq 1 ]]; then + selected_lvms_not_existing=0 + fi done elif [ "$num_lvms" -eq 1 ]; then - echo "Single Encrypted LVM found at $lvm_suggest." + INFO "Single Encrypted LVM found at $lvm_suggest" key_lvms=$lvm_suggest else - echo "No encrypted LVMs found." + DEBUG "No encrypted LVMs found" fi # Create an associative array to store the suggested devices and their paths @@ -140,36 +145,41 @@ prompt_for_existing_encrypted_lvms_or_disks() { declare -a key_devices_array while [ $selected_luksdevs_not_existing -ne 0 ]; do - { - # Read the user input and store it in a variable - read \ - -p "Encrypted devices? (choose between/all: $devices_suggest): " \ - key_devices - - # Split the user input by spaces and add each element to the array - IFS=' ' read -r -a key_devices_array <<<"$key_devices" - - # Loop through the array and check if each element is in the devices_array - valid=1 - for device in "${key_devices_array[@]}"; do - if [[ ! ${devices_array[$device]+_} ]]; then - # If not found, set the flag to indicate invalid input - valid=0 - break - fi - done - - # If valid, set the flag to indicate valid input - if [[ $valid -eq 1 ]]; then - selected_luksdevs_not_existing=0 + # Read the user input and store it in a variable + INPUT "Encrypted devices? (type 'all' to select all, or space-separated subset of: $devices_suggest):" -r key_devices + + # 'all' expands to every discovered LUKS device + if [ "$key_devices" = "all" ]; then + key_devices="$devices_suggest" + DEBUG "User chose 'all' LUKS devices: $key_devices" + selected_luksdevs_not_existing=0 + continue + fi + + # Split the user input by spaces and validate each element + IFS=' ' read -r -a key_devices_array <<<"$key_devices" + + if [ ${#key_devices_array[@]} -eq 0 ]; then + continue + fi + + valid=1 + for device in "${key_devices_array[@]}"; do + if [[ ! ${devices_array[$device]+_} ]]; then + valid=0 + break fi - } + done + + if [[ $valid -eq 1 ]]; then + selected_luksdevs_not_existing=0 + fi done elif [ "$num_devices" -eq 1 ]; then - echo "Single Encrypted Disk found at $devices_suggest." + INFO "Single Encrypted Disk found at $devices_suggest" key_devices=$devices_suggest else - echo "No encrypted devices found." + DEBUG "No encrypted devices found" fi DEBUG "Multiple LUKS devices selected: $key_devices" @@ -177,12 +187,12 @@ prompt_for_existing_encrypted_lvms_or_disks() { } if [ ! -r "$TMP_MENU_FILE" ]; then - die "No menu options available, please run kexec-select-boot" + DIE "No menu options available, please run kexec-select-boot.sh" fi entry=$(head -n $index $TMP_MENU_FILE | tail -1) if [ -z "$entry" ]; then - die "Invalid menu index $index" + DIE "Invalid menu index $index" fi save_key="n" @@ -193,11 +203,7 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ #check if $KEY_DEVICES file exists and is not empty if [ -r "$KEY_DEVICES" ] && [ -s "$KEY_DEVICES" ]; then DEBUG "LUKS TPM Disk Unlock Key was previously set up from $KEY_DEVICES" - read \ - -n 1 \ - -p "Do you want to reseal a Disk Unlock Key in the TPM [y/N]: " \ - change_key_confirm - echo + INPUT "Do you want to reseal a Disk Unlock Key in the TPM [y/N]:" -n 1 change_key_confirm if [ "$change_key_confirm" = "y" \ -o "$change_key_confirm" = "Y" ]; then @@ -219,12 +225,7 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ fi else DEBUG "No previous LUKS TPM Disk Unlock Key was set up, confirming to add a Disk Unlock Key (DUK) to the TPM" - read \ - -n 1 \ - -p "Do you wish to add a disk encryption key to the TPM [y/N]: " \ - add_key_confirm - #TODO: still not convinced: disk encryption key? decryption key? everywhere TPM Disk Unlock Key. Confusing even more? - echo + INPUT "Do you wish to add a LUKS TPM Disk Unlock Key (DUK) to the TPM [y/N]:" -n 1 add_key_confirm if [ "$add_key_confirm" = "y" \ -o "$add_key_confirm" = "Y" ]; then @@ -236,11 +237,7 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ if [ "$save_key" = "y" ]; then if [ -n "$old_key_devices" ] || [ -n "$old_lvm_volume_group" ]; then DEBUG "Previous LUKS TPM Disk Unlock Key was set up for $old_key_devices $old_lvm_volume_group" - read \ - -n 1 \ - -p "Do you want to reuse configured Encrypted LVM groups/Block devices? (Y/n):" \ - reuse_past_devices - echo + INPUT "Do you want to reuse configured Encrypted LVM groups/Block devices? (Y/n):" -n 1 reuse_past_devices if [ "$reuse_past_devices" = "y" ] || [ "$reuse_past_devices" = "Y" ] || [ -z "$reuse_past_devices" ]; then if [ -z "$key_devices" ] && [ -n "$old_key_devices" ]; then key_devices="$old_key_devices" @@ -263,8 +260,8 @@ if [ "$CONFIG_TPM" = "y" ] && [ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" != "y" ] && [ else save_key_params="$save_key_params $key_devices" fi - kexec-save-key $save_key_params || - die "Failed to save the LUKS TPM Disk Unlock Key" + kexec-save-key.sh $save_key_params || + DIE "Failed to save the LUKS TPM Disk Unlock Key" fi fi @@ -273,28 +270,47 @@ mount -o rw,remount $paramsdev if [ ! -d $paramsdir ]; then mkdir -p $paramsdir || - die "Failed to create params directory" + DIE "Failed to create params directory" fi +# All writes go to a staging directory; files are moved to their final +# locations only after kexec-sign-config.sh succeeds. This prevents a failed +# signing attempt from leaving /boot with updated config but no matching +# signature. +stagedir=$(mktemp -d /tmp/kexec-default-XXXXXX) +cleanup_stagedir() { rm -rf "$stagedir"; } +trap cleanup_stagedir EXIT +DEBUG "Staging directory created: $stagedir" + +# Seed staging with existing kexec*.txt so sign-config signs everything +for f in "$paramsdir"/kexec*.txt; do + [ -e "$f" ] && cp "$f" "$stagedir/" +done +# Seed staging with existing crypttab overrides if present +[ -e "$bootdir/kexec_initrd_crypttab_overrides.txt" ] && \ + cp "$bootdir/kexec_initrd_crypttab_overrides.txt" "$stagedir/" +DEBUG "Seeded $stagedir with existing config files from $paramsdir" + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then if [ -f /tmp/secret/primary.handle ]; then DEBUG "Hashing TPM2 primary key handle..." - sha256sum /tmp/secret/primary.handle > "$PRIMHASH_FILE" || - die "ERROR: Failed to Hash TPM2 primary key handle!" - DEBUG "TPM2 primary key handle hash saved to $PRIMHASH_FILE" + sha256sum /tmp/secret/primary.handle > "$stagedir/kexec_primhdl_hash.txt" || + DIE "ERROR: Failed to Hash TPM2 primary key handle!" + DEBUG "TPM2 primary key handle hash written to $stagedir/kexec_primhdl_hash.txt" else - die "ERROR: TPM2 primary key handle file does not exist!" + DIE "ERROR: TPM2 primary key handle file does not exist!" fi fi -rm $paramsdir/kexec_default.*.txt 2>/dev/null || true -echo "$entry" >$ENTRY_FILE +# Remove old kexec_default.*.txt from staging; new entry written below +rm "$stagedir"/kexec_default.*.txt 2>/dev/null || true +echo "$entry" >"$stagedir/kexec_default.$index.txt" ( - cd $bootdir && kexec-boot -b "$bootdir" -e "$entry" -f | - xargs sha256sum >$HASH_FILE -) || die "Failed to create hashes of boot files" -if [ ! -r $ENTRY_FILE -o ! -r $HASH_FILE ]; then - die "Failed to write default config" + cd $bootdir && kexec-boot.sh -b "$bootdir" -e "$entry" -f | + xargs sha256sum >"$stagedir/kexec_default_hashes.txt" +) || DIE "Failed to create hashes of boot files" +if [ ! -r "$stagedir/kexec_default.$index.txt" -o ! -r "$stagedir/kexec_default_hashes.txt" ]; then + DIE "Failed to write default config to staging" fi if [ "$save_key" = "y" ]; then @@ -302,15 +318,15 @@ if [ "$save_key" = "y" ]; then initrd_decompressed="/tmp/initrd_extract" mkdir -p "$initrd_decompressed" # Get initrd filename selected to be default initrd that OS could be using to configure LUKS on boot by deploying crypttab files - current_default_initrd=$(cat /boot/kexec_default_hashes.txt | grep initr | awk -F " " {'print $NF'} | sed 's/\.\//\/boot\//g') + current_default_initrd=$(cat "$stagedir/kexec_default_hashes.txt" | grep initr | awk -F " " {'print $NF'} | sed 's/\.\//\/boot\//g') - echo "+++ Extracting current selected default boot's $current_default_initrd to find crypttab files..." + DEBUG "Extracting $current_default_initrd to find crypttab files" unpack_initramfs.sh "$current_default_initrd" "$initrd_decompressed" crypttab_files=$(find "$initrd_decompressed" | grep crypttab 2>/dev/null) || true if [ ! -z "$crypttab_files" ]; then DEBUG "Found crypttab files in $current_default_initrd" - rm -f $bootdir/kexec_initrd_crypttab_overrides.txt || true + rm -f "$stagedir/kexec_initrd_crypttab_overrides.txt" || true #Parsing each crypttab file found echo "$crypttab_files" | while read crypttab_file; do @@ -324,20 +340,20 @@ if [ "$save_key" = "y" ]; then modified_crypttab_entries=$(echo "$current_crypttab_entries" | sed 's/none/\/secret.key/g') DEBUG "Modified crypttab entries $final_initrd_filepath:$modified_crypttab_entries" echo "$modified_crypttab_entries" | while read modified_crypttab_entry; do - echo "$final_initrd_filepath:$modified_crypttab_entry" >>$bootdir/kexec_initrd_crypttab_overrides.txt + echo "$final_initrd_filepath:$modified_crypttab_entry" >>"$stagedir/kexec_initrd_crypttab_overrides.txt" done done #insert current default boot's initrd crypttab locations into tracking file to be overwritten into initramfs at kexec-inject-key - echo "+++ The following OS crypttab file:entry were modified from default boot's initrd:" - cat $bootdir/kexec_initrd_crypttab_overrides.txt - echo "+++ Heads added /secret.key in those entries and saved them under $bootdir/kexec_initrd_crypttab_overrides.txt" - echo "+++ Those overrides will be part of detached signed digests and used to prepare cpio injected at kexec of selected default boot entry." + STATUS "The following OS crypttab entries were modified from default boot's initrd:" + cat "$stagedir/kexec_initrd_crypttab_overrides.txt" + STATUS_OK "Heads added /secret.key to those entries and saved overrides for signing" + DEBUG "Crypttab overrides will be included in signed digests and injected via cpio at kexec" else - echo "+++ No crypttab file found in extracted initrd. A generic crypttab will be generated" - if [ -e "$bootdir/kexec_initrd_crypttab_overrides.txt" ]; then - echo "+++ Removing $bootdir/kexec_initrd_crypttab_overrides.txt" - rm -f "$bootdir/kexec_initrd_crypttab_overrides.txt" + INFO "No crypttab found in initrd; a generic crypttab will be generated" + if [ -e "$stagedir/kexec_initrd_crypttab_overrides.txt" ]; then + DEBUG "Removing stale $stagedir/kexec_initrd_crypttab_overrides.txt (no crypttab found in initrd)" + rm -f "$stagedir/kexec_initrd_crypttab_overrides.txt" fi fi @@ -354,8 +370,22 @@ if [ "$CONFIG_TPM" = "y" ]; then fi fi if [ "$CONFIG_BASIC" != "y" ]; then - DO_WITH_DEBUG kexec-sign-config -p $paramsdir $extparam || - die "Failed to sign default config" + DO_WITH_DEBUG kexec-sign-config.sh -p "$stagedir" $extparam || + DIE "Failed to sign default config" fi +# Signing succeeded; move all staged files to their final locations. +# kexec-sign-config.sh may have remounted $paramsdev ro; remount rw for the moves. +mount -o rw,remount $paramsdev +rm "$paramsdir"/kexec_default.*.txt 2>/dev/null || true +for f in "$stagedir"/*; do + [ -e "$f" ] || continue + fname="$(basename "$f")" + if [ "$fname" = "kexec_initrd_crypttab_overrides.txt" ]; then + mv "$f" "$bootdir/$fname" + else + mv "$f" "$paramsdir/$fname" + fi +done +DEBUG "Staged files from $stagedir moved to $paramsdir (crypttab overrides to $bootdir)" # switch back to ro mode mount -o ro,remount $paramsdev diff --git a/initrd/bin/kexec-save-key.sh b/initrd/bin/kexec-save-key.sh index 890ffa6a5..19428057e 100755 --- a/initrd/bin/kexec-save-key.sh +++ b/initrd/bin/kexec-save-key.sh @@ -18,71 +18,74 @@ while getopts "sp:d:l:" arg; do esac done -DEBUG "kexec-save-key prior of parsing: paramsdir: $paramsdir, paramsdev: $paramsdev, lvm_volume_group: $lvm_volume_group" +DEBUG "kexec-save-key.sh prior of parsing: paramsdir: $paramsdir, paramsdev: $paramsdev, lvm_volume_group: $lvm_volume_group" shift $(expr $OPTIND - 1) key_devices="$@" -DEBUG "kexec-save-key: key_devices: $key_devices" +DEBUG "kexec-save-key.sh: key_devices: $key_devices" if [ -z "$paramsdir" ]; then - die "Usage: $0 [-s] -p /boot [-l qubes_dom0] [/dev/sda2 /dev/sda5 ...] " + DIE "Usage: $0 [-s] -p /boot [-l qubes_dom0] [/dev/sda2 /dev/sda5 ...] " fi if [ -z "$paramsdev" ]; then paramsdev="$paramsdir" - DEBUG "kexec-save-key: paramsdev modified to : $paramsdev" + DEBUG "kexec-save-key.sh: paramsdev modified to : $paramsdev" fi paramsdev="${paramsdev%%/}" paramsdir="${paramsdir%%/}" -DEBUG "kexec-save-key prior of last override: paramsdir: $paramsdir, paramsdev: $paramsdev, lvm_volume_group: $lvm_volume_group" +DEBUG "kexec-save-key.sh prior of last override: paramsdir: $paramsdir, paramsdev: $paramsdev, lvm_volume_group: $lvm_volume_group" if [ -n "$lvm_volume_group" ]; then - lvm vgchange -a y $lvm_volume_group || - die "Failed to activate the LVM group" + run_lvm vgchange -a y $lvm_volume_group || + DIE "Failed to activate the LVM group" for dev in /dev/$lvm_volume_group/*; do key_devices="$key_devices $dev" done fi if [ -z "$key_devices" ]; then - die "No devices specified for TPM key insertion" + DIE "No devices specified for TPM key insertion" fi # try to switch to rw mode -mount -o rw,remount $paramsdev +mount -o rw,remount "$paramsdev" -rm -f $paramsdir/kexec_key_lvm.txt || true +rm -f "$paramsdir/kexec_key_lvm.txt" || true if [ -n "$lvm_volume_group" ]; then - DEBUG "kexec-save-key saving under $paramsdir/kexec_key_lvm.txt : lvm_volume_group: $lvm_volume_group" - echo "$lvm_volume_group" >$paramsdir/kexec_key_lvm.txt || - die "Failed to write lvm group to key config " + DEBUG "kexec-save-key.sh saving under $paramsdir/kexec_key_lvm.txt : lvm_volume_group: $lvm_volume_group" + echo "$lvm_volume_group" >"$paramsdir/kexec_key_lvm.txt" || + DIE "Failed to write lvm group to key config" fi -rm -f $paramsdir/kexec_key_devices.txt || true +rm -f "$paramsdir/kexec_key_devices.txt" || true for dev in $key_devices; do DEBUG "Getting UUID for $dev" uuid=$(cryptsetup luksUUID "$dev" 2>/dev/null) || - die "Failed to get UUID for device $dev" + DIE "Failed to get UUID for device $dev" DEBUG "Saving under $paramsdir/kexec_key_devices.txt : dev: $dev, uuid: $uuid" - echo "$dev $uuid" >>$paramsdir/kexec_key_devices.txt || - die "Failed to add $dev:$uuid to key devices config" + echo "$dev $uuid" >>"$paramsdir/kexec_key_devices.txt" || + DIE "Failed to add $dev:$uuid to key devices config" done -kexec-seal-key $paramsdir || - die "Failed to save and generate LUKS TPM Disk Unlock Key" +# kexec-seal-key.sh tests the DRK passphrase, filters kexec_key_devices.txt to +# only the unlockable subset, then seals the DUK into TPM NVRAM. +kexec-seal-key.sh "$paramsdir" || + DIE "Failed to save and generate LUKS TPM Disk Unlock Key" 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" + DEBUG "kexec-save-key.sh: CONFIG_IGNORE_ROLLBACK is not set, will sign with -r" extparam=-r fi - # sign and auto-roll config counter - DO_WITH_DEBUG kexec-sign-config -p $paramsdir $extparam || - die "Failed to sign updated config" + # Sign the updated /boot — kexec_key_devices.txt now reflects only the + # devices that actually received a DUK (may be a subset of what was passed). + DO_WITH_DEBUG kexec-sign-config.sh -p "$paramsdir" $extparam || + DIE "Failed to sign updated config" fi # switch back to ro mode diff --git a/initrd/bin/kexec-seal-key.sh b/initrd/bin/kexec-seal-key.sh index 061542a28..e2b9ff741 100755 --- a/initrd/bin/kexec-seal-key.sh +++ b/initrd/bin/kexec-seal-key.sh @@ -6,146 +6,185 @@ set -e -o pipefail . /etc/functions.sh find_drk_key_slot() { - local temp_drk_key_slot="" + # Usage: find_drk_key_slot [ ...] + # Echoes the first keyslot on that the current DISK_RECOVERY_KEY_FILE + # can unlock. All dependencies are explicit; no outer-scope variables used. + local dev="$1" + shift local keyslot - for keyslot in "${luks_used_keyslots[@]}"; do - if [ -z "$temp_drk_key_slot" ]; then - DEBUG "Testing LUKS key slot $keyslot against $DISK_RECOVERY_KEY_FILE for Disk Recovery Key slot..." - if DO_WITH_DEBUG cryptsetup open --test-passphrase --key-slot "$keyslot" --key-file "$DISK_RECOVERY_KEY_FILE" "$dev"; then - temp_drk_key_slot="$keyslot" - DEBUG "Disk Recovery key slot is $temp_drk_key_slot" - break - fi + for keyslot in "$@"; do + DEBUG "Testing LUKS key slot $keyslot against $DISK_RECOVERY_KEY_FILE for Disk Recovery Key slot..." + if DO_WITH_DEBUG cryptsetup open --test-passphrase --key-slot "$keyslot" --key-file "$DISK_RECOVERY_KEY_FILE" "$dev"; then + DEBUG "Disk Recovery key slot is $keyslot" + echo "$keyslot" + return 0 fi done - - echo "$temp_drk_key_slot" + # No matching slot found; return 0 so set -e does not abort — caller checks + # for empty output. + return 0 } TPM_INDEX=3 TPM_SIZE=312 DUK_KEY_FILE="/tmp/secret/secret.key" -TPM_SEALED="/tmp/secret/secret.sealed" +# TPM_SEALED is written by tpmr.sh seal internally; not used directly here. DISK_RECOVERY_KEY_FILE="/tmp/secret/recovery.key" -. /etc/functions.sh . /tmp/config TRACE_FUNC paramsdir=$1 if [ -z "$paramsdir" ]; then - die "Usage $0 /boot" + DIE "Usage $0 /boot" fi KEY_DEVICES="$paramsdir/kexec_key_devices.txt" KEY_LVM="$paramsdir/kexec_key_lvm.txt" -key_devices=$(cat "$KEY_DEVICES" | cut -d\ -f1 | tr '\n' ' ') if [ ! -r "$KEY_DEVICES" ]; then - die "No devices defined for disk encryption" -else - DEBUG "Devices defined for disk encryption: $key_devices" + DIE "No devices defined for disk encryption" fi +key_devices=$(cut -d\ -f1 "$KEY_DEVICES" | tr '\n' ' ') +DEBUG "Devices defined for disk encryption: $key_devices" if [ -r "$KEY_LVM" ]; then # Activate the LVM volume group - VOLUME_GROUP=$(cat $KEY_LVM) + VOLUME_GROUP=$(<"$KEY_LVM") if [ -z "$VOLUME_GROUP" ]; then - die "No LVM volume group defined for activation" + DIE "No LVM volume group defined for activation" fi - lvm vgchange -a y $VOLUME_GROUP || - die "$VOLUME_GROUP: unable to activate volume group" + run_lvm vgchange -a y "$VOLUME_GROUP" || + DIE "$VOLUME_GROUP: unable to activate volume group" else DEBUG "No LVM volume group defined for activation" fi DEBUG "$(pcrs)" -# First, collect all the LUKS devices that need to be tested +# Ask for the DRK passphrase and test it against every selected device. +# Devices that cannot be unlocked are reported and skipped; DUK is set up +# only for the subset that the passphrase can actually unlock. luks_drk_passphrase_valid=0 attempts=0 -# Ask for the DRK passphrase first, before testing any devices +STATUS "Unlocking LUKS device(s) using the Disk Recovery Key passphrase" 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 + disk_recovery_key_passphrase="" + INPUT "Enter LUKS Disk Recovery Key (DRK) passphrase that can unlock $key_devices:" -r -s disk_recovery_key_passphrase 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 - + # Test passphrase against ALL devices without short-circuiting so the user + # sees the full picture (which devices worked, which did not). + unlockable_devices="" + failed_devices="" 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 + STATUS "Testing DRK passphrase against $dev..." + if cryptsetup open "$dev" --test-passphrase --key-file "$DISK_RECOVERY_KEY_FILE" >/dev/null 2>&1; then + STATUS_OK "$dev: unlocked successfully with the Disk Recovery Key passphrase" + unlockable_devices="$unlockable_devices $dev" else - echo "++++++ $dev: LUKS device unlocked successfully with the DRK passphrase" + WARN "$dev: cannot be unlocked with the provided passphrase" + failed_devices="$failed_devices $dev" fi done + unlockable_devices="${unlockable_devices# }" + failed_devices="${failed_devices# }" + DEBUG "kexec-seal-key.sh: unlockable='$unlockable_devices' failed='$failed_devices'" - if [ $all_devices_unlocked -eq 1 ]; then - luks_drk_passphrase_valid=1 - else + if [ -z "$unlockable_devices" ]; then + # No device could be unlocked — wrong passphrase entirely 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." + DIE "Failed to unlock any LUKS device with the provided passphrase after 3 attempts." + fi + WARN "None of the selected LUKS devices could be unlocked. Please try again." + continue + fi + + if [ -n "$failed_devices" ]; then + # Partial success: warn and ask the user to confirm skipping the failing devices + WARN "The following device(s) cannot be unlocked with the provided passphrase and will be skipped:" + WARN " $failed_devices" + WARN "DUK will be set up only for: $unlockable_devices" + confirm_partial="Y" + INPUT "Continue with only the unlockable devices? [Y/n]:" -n 1 -r confirm_partial + if [ "$confirm_partial" = "n" ] || [ "$confirm_partial" = "N" ]; then + attempts=$((attempts + 1)) + if [ $attempts -eq 3 ]; then + DIE "DUK setup cancelled: user declined partial device setup after 3 attempts." + fi + WARN "Please enter a passphrase valid for all desired devices, or reduce your device selection." + continue fi fi + + luks_drk_passphrase_valid=1 + key_devices="$unlockable_devices" done -# Now that all devices are verified with the DRK passphrase, proceed with DUK setup +# Build the filtered device list in /tmp; written to $KEY_DEVICES in the rw +# block near the end so all paramsdir writes happen in one mount window. +# kexec-save-key.sh pre-mounts /boot rw before calling us, but +# reseal_tpm_disk_decryption_key calls us directly with /boot still ro. +DEBUG "kexec-seal-key.sh: filtering $KEY_DEVICES to unlockable devices: $key_devices" +{ + for dev in $key_devices; do + grep "^$dev " "$KEY_DEVICES" || true + done +} > /tmp/kexec_key_devices_filtered.txt +if [ ! -s /tmp/kexec_key_devices_filtered.txt ]; then + DIE "kexec-seal-key.sh: filtered device list is empty, cannot continue" +fi + +# Proceed with DUK setup for the confirmed unlockable devices MIN_PASSPHRASE_LENGTH=12 attempts=0 while [ $attempts -lt 3 ]; do - read -r -s -p $'\nNew LUKS TPM Disk Unlock Key (DUK) passphrase for booting (minimum '"$MIN_PASSPHRASE_LENGTH"' characters): ' key_password - echo + key_password="" + INPUT "New LUKS TPM Disk Unlock Key (DUK) passphrase for booting (minimum $MIN_PASSPHRASE_LENGTH characters):" -r -s key_password if [ ${#key_password} -lt $MIN_PASSPHRASE_LENGTH ]; then attempts=$((attempts + 1)) - warn "Disk Unlock Key (DUK) passphrase is too short. Please try again." + 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 + key_password2="" + INPUT "Repeat LUKS TPM Disk Unlock Key (DUK) passphrase for booting:" -r -s key_password2 if [ "$key_password" != "$key_password2" ]; then attempts=$((attempts + 1)) - warn "Disk Unlock Key (DUK) passphrases do not match. Please try again." + 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..." + 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" +STATUS "Generating new randomized key of 128 characters for LUKS TPM Disk Unlock Key" dd \ if=/dev/urandom \ of="$DUK_KEY_FILE" \ bs=1 \ count=128 \ 2>/dev/null || - die "Unable to generate 128 random bytes" + DIE "Unable to generate random key of 128 characters" previous_luks_header_version=0 for dev in $key_devices; do # Check and store LUKS version of the devices to be used later luks_version=$(cryptsetup luksDump "$dev" | grep "Version" | cut -d: -f2 | tr -d '[:space:]') if [ "$luks_version" == "2" ] && [ "$previous_luks_header_version" == "1" ]; then - die "$dev: LUKSv2 device detected while LUKSv1 device was detected previously. Exiting..." + DIE "$dev: LUKSv2 device detected while LUKSv1 device was detected previously. Exiting..." fi if [ "$luks_version" == "1" ] && [ "$previous_luks_header_version" == "2" ]; then - die "$dev: LUKSv1 device detected while LUKSv2 device was detected previously. Exiting..." + DIE "$dev: LUKSv1 device detected while LUKSv2 device was detected previously. Exiting..." fi if [ "$luks_version" == "2" ]; then @@ -163,74 +202,76 @@ for dev in $key_devices; do previous_luks_header_version=1 DEBUG "$dev: LUKSv1 device detected" else - die "$dev: Unsupported LUKS version $luks_version" + DIE "$dev: Unsupported LUKS version $luks_version" fi - # drk_key_slot will be the slot number where the passphrase was tested against as valid. We will keep that slot - drk_key_slot="-1" - # Get all the key slots that are used on $dev - luks_used_keyslots=($(cryptsetup luksDump "$dev" | grep -E "$regex" | sed "$sed_command")) + mapfile -t 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) - - # If we didn't find the DRK key slot, we exit (this should never happen) - if [ "$drk_key_slot" == "-1" ]; then - die "$dev: Unable to find a key slot that can be unlocked with provided passphrase. Exiting..." + # Find the key slot that can be unlocked with the provided passphrase. + # Pass keyslots explicitly so find_drk_key_slot has no outer-scope deps. + drk_key_slot=$(find_drk_key_slot "$dev" "${luks_used_keyslots[@]}") + if [ -z "$drk_key_slot" ]; then + DIE "$dev: Unable to find a key slot that can be unlocked with provided passphrase. Exiting..." fi - # If the key slot is not the expected DUK or DRK key slot, we will ask the user to confirm the wipe + # Wipe all key slots except the DRK slot; the outer `if` already guarantees + # keyslot != drk_key_slot for every iteration, so inner re-checks are omitted. for keyslot in "${luks_used_keyslots[@]}"; do - if [ "$keyslot" != "$drk_key_slot" ]; then - #set wipe_desired to no by default - wipe_desired="no" + if [ "$keyslot" = "$drk_key_slot" ]; then + continue + fi - if [ "$keyslot" != "$drk_key_slot" ] && [ "$keyslot" == "1" ]; then - wipe_desired="yes" - DEBUG "LUKS key slot $keyslot not DRK. Will wipe this DUK key slot silently" - elif [ "$keyslot" != "$drk_key_slot" ] && [ "$keyslot" != "$duk_keyslot" ]; then - # 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' -n 1 -r - echo "" - # If user does not confirm, skip this slot - if [[ $REPLY =~ ^[Yy]$ ]]; then - wipe_desired="yes" - fi - elif [ "$keyslot" == "$duk_keyslot" ]; then - # If key slot is the expected DUK keyslot, we wipe it silently - DEBUG "LUKS key slot $keyslot is the expected DUK key slot. Will wipe this DUK key slot silently" + wipe_desired="no" + if [ "$keyslot" = "1" ]; then + # Slot 1 is the legacy DUK slot — wipe silently + wipe_desired="yes" + DEBUG "$dev: LUKS key slot $keyslot is legacy DUK slot, wiping silently" + elif [ "$keyslot" = "$duk_keyslot" ]; then + # Expected DUK slot — wipe silently to make room for the new key + wipe_desired="yes" + DEBUG "$dev: LUKS key slot $keyslot is the expected DUK slot, wiping silently" + else + # Unexpected occupied slot — ask before wiping + WARN "$dev: LUKS key slot $keyslot is occupied and not the expected DUK slot ($duk_keyslot)" + REPLY="N" + INPUT "Wipe key slot $keyslot on $dev? [y/N]:" -n 1 -r REPLY + if [[ $REPLY =~ ^[Yy]$ ]]; then wipe_desired="yes" fi + fi - if [ "$wipe_desired" == "yes" ] && [ "$keyslot" != "$drk_key_slot" ]; then - echo "++++++ $dev: Wiping LUKS key slot $keyslot" - DO_WITH_DEBUG cryptsetup luksKillSlot \ - --key-file "$DISK_RECOVERY_KEY_FILE" \ - $dev $keyslot || - warn "$dev: removal of LUKS slot $keyslot failed: Continuing" + if [ "$wipe_desired" = "yes" ]; then + # Hard guard: never wipe the DRK slot regardless of how wipe_desired was set. + if [ "$keyslot" = "$drk_key_slot" ]; then + DIE "$dev: BUG: attempted to wipe DRK key slot $drk_key_slot — aborting to prevent data loss" fi + STATUS "$dev: Wiping LUKS key slot $keyslot" + DO_WITH_DEBUG cryptsetup luksKillSlot \ + --key-file "$DISK_RECOVERY_KEY_FILE" \ + "$dev" "$keyslot" || + WARN "$dev: removal of LUKS slot $keyslot failed: Continuing" fi done - echo "++++++ $dev: Adding LUKS TPM Disk Unlock Key to LUKS key slot $duk_keyslot" + STATUS "$dev: Adding LUKS TPM Disk Unlock Key to key slot $duk_keyslot" DO_WITH_DEBUG cryptsetup luksAddKey \ --key-file "$DISK_RECOVERY_KEY_FILE" \ - --new-key-slot $duk_keyslot \ - $dev "$DUK_KEY_FILE" || - die "$dev: Unable to add LUKS TPM Disk Unlock Key to LUKS key slot $duk_keyslot" + --new-key-slot "$duk_keyslot" \ + "$dev" "$DUK_KEY_FILE" || + DIE "$dev: Unable to add LUKS TPM Disk Unlock Key to LUKS key slot $duk_keyslot" done # Now that we have setup the new keys, measure the PCRs # We don't care what ends up in PCR 6; we just want # to get the /tmp/luksDump.txt file. We use PCR16 # since it should still be zero -echo "$key_devices" | xargs /bin/qubes-measure-luks || - die "Unable to measure the LUKS headers" +STATUS "Measuring LUKS headers for TPM sealing policy" +echo "$key_devices" | xargs /bin/qubes-measure-luks.sh || + DIE "Unable to measure the LUKS headers" +STATUS "Reading current PCR values for TPM sealing policy" pcrf="/tmp/secret/pcrf.bin" tpmr.sh pcrread 0 "$pcrf" tpmr.sh pcrread -a 1 "$pcrf" @@ -238,7 +279,7 @@ tpmr.sh pcrread -a 2 "$pcrf" tpmr.sh pcrread -a 3 "$pcrf" # Note that PCR 4 needs to be set with the "normal-boot" path value, read it from event log. tpmr.sh calcfuturepcr 4 >>"$pcrf" -if [ "$CONFIG_USER_USB_KEYBOARD" = "y" -o -r /lib/modules/libata.ko -o -x /bin/hotp_verification ]; then +if [ "$CONFIG_USER_USB_KEYBOARD" = "y" ] || [ -r /lib/modules/libata.ko ] || [ -x /bin/hotp_verification ]; then DEBUG "Sealing LUKS TPM Disk Unlock Key with PCR5 involvement (additional kernel modules are loaded per board config)..." # Here, we take pcr 5 into consideration if modules are expected to be measured+loaded tpmr.sh pcrread -a 5 "$pcrf" @@ -253,17 +294,25 @@ tpmr.sh calcfuturepcr 6 "/tmp/luksDump.txt" >>"$pcrf" # We take into consideration user files in cbfs tpmr.sh pcrread -a 7 "$pcrf" -DO_WITH_DEBUG --mask-position 7 \ - tpmr.sh seal "$DUK_KEY_FILE" "$TPM_INDEX" 0,1,2,3,4,5,6,7 "$pcrf" \ - "$TPM_SIZE" "$key_password" || die "Unable to write LUKS TPM Disk Unlock Key to NVRAM" +# tpmr.sh seal may prompt for TPM owner password; avoid DO_WITH_DEBUG here so the +# prompt remains visible on console. tpmr.sh logs command details internally. +STATUS "Sealing LUKS TPM Disk Unlock Key into TPM NVRAM (this may take a moment)" +DEBUG "tpmr.sh seal $DUK_KEY_FILE $TPM_INDEX 0,1,2,3,4,5,6,7 $pcrf $TPM_SIZE " +tpmr.sh seal "$DUK_KEY_FILE" "$TPM_INDEX" 0,1,2,3,4,5,6,7 "$pcrf" \ + "$TPM_SIZE" "$key_password" || DIE "Unable to write LUKS TPM Disk Unlock Key to NVRAM" +STATUS_OK "LUKS TPM Disk Unlock Key sealed successfully" # should be okay if this fails shred -n 10 -z -u "$pcrf" 2>/dev/null || - warn "Failed to delete pcrf file - continuing" + WARN "Failed to delete pcrf file - continuing" shred -n 10 -z -u "$DUK_KEY_FILE" 2>/dev/null || - warn "Failed to delete key file - continuing" + WARN "Failed to delete key file - continuing" -mount -o rw,remount $paramsdir || warn "Failed to remount $paramsdir in RW - continuing" +mount -o rw,remount "$paramsdir" || WARN "Failed to remount $paramsdir in RW - continuing" +cp -f /tmp/kexec_key_devices_filtered.txt "$KEY_DEVICES" || + DIE "kexec-seal-key.sh: failed to update $KEY_DEVICES" +DEBUG "kexec-seal-key.sh: $KEY_DEVICES updated" +rm -f /tmp/kexec_key_devices_filtered.txt cp -f /tmp/luksDump.txt "$paramsdir/kexec_lukshdr_hash.txt" || - warn "Failed to copy LUKS header hashes to /boot - continuing" -mount -o ro,remount $paramsdir || warn "Failed to remount $paramsdir in RO - continuing" + WARN "Failed to copy LUKS header hashes to /boot - continuing" +mount -o ro,remount "$paramsdir" || WARN "Failed to remount $paramsdir in RO - continuing" diff --git a/initrd/bin/kexec-select-boot.sh b/initrd/bin/kexec-select-boot.sh index 56cbfbc1b..9f5a07543 100755 --- a/initrd/bin/kexec-select-boot.sh +++ b/initrd/bin/kexec-select-boot.sh @@ -20,30 +20,30 @@ 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 if [ -z "$bootdir" ]; then - die "Usage: $0 -b /boot" + DIE "Usage: $0 -b /boot" fi if [ -z "$paramsdev" ]; then @@ -64,28 +64,25 @@ if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then #PRIMHASH_FILE (normally /boot/kexec_primhdl_hash.txt) exists and is not empty sha256sum -c "$PRIMHASH_FILE" >/dev/null 2>&1 || { - echo "FATAL: Hash of TPM2 primary key handle mismatch!" - warn "If you have not intentionally regenerated TPM2 primary key," - warn "your system may have been compromised" + WARN "Hash of TPM2 primary key handle mismatch - if you have not intentionally regenerated the TPM2 primary key, your system may have been compromised" DEBUG "Hash of TPM2 primary key handle mismatched for $PRIMHASH_FILE" DEBUG "Contents of $PRIMHASH_FILE:" DEBUG "$(cat $PRIMHASH_FILE)" + DIE "Hash of TPM2 primary key handle mismatch ($PRIMHASH_FILE). If you did not intentionally regenerate the TPM2 primary key, this may indicate compromise." } else - warn "Hash of TPM2 primary key handle does not exist" - warn "Please rebuild the TPM2 primary key handle hash by setting a default OS to boot." - warn "Select Options-> Boot Options -> Show OS Boot Menu -> -> Make default" - #TODO: Simplify/Automatize TPM2 firmware upgrade process. Today: upgrade, reboot, reseal(type TPM Owner Password), resign, boot + WARN "Hash of TPM2 primary key handle does not exist - rebuild it by setting a default OS to boot: Options -> Boot Options -> Show OS Boot Menu -> pick OS -> Make default" + #TODO: Simplify/Automatize TPM2 firmware upgrade process. Today: upgrade, reboot, reseal(type TPM owner passphrase), resign, boot default_failed="y" DEBUG "Hash of TPM2 primary key handle does not exist under $PRIMHASH_FILE" fi fi verify_global_hashes() { - INFO "+++ Checking verified boot hash file " + STATUS "Checking verified boot hash file" # Check the hashes of all the files if verify_checksums "$bootdir" "$gui_menu"; then - INFO "+++ Verified boot hashes " + STATUS_OK "Verified boot hashes" valid_hash='y' valid_global_hash='y' else @@ -94,23 +91,23 @@ verify_global_hashes() { whiptail_error --title 'ERROR: Boot Hash Mismatch' \ --msgbox "The following files failed the verification process:\n${CHANGED_FILES}\nExiting to a recovery shell" 0 80 fi - die "$TMP_HASH_FILE: boot hash mismatch" + DIE "$TMP_HASH_FILE: boot hash mismatch" fi # If user enables it, check root hashes before boot as well if [[ "$CONFIG_ROOT_CHECK_AT_BOOT" = "y" && "$force_menu" == "n" ]]; then if root-hashes-gui.sh -c; then - echo "+++ Verified root hashes, continuing boot " + STATUS_OK "Verified root hashes, continuing boot" # if user re-signs, it wipes out saved options, so scan the boot directory and generate if [ ! -r "$TMP_MENU_FILE" ]; then scan_options fi else - # root-hashes-gui.sh handles the GUI error menu, just die here + # root-hashes-gui.sh handles the GUI error menu, just DIE here if [ "$gui_menu" = "y" ]; then whiptail_error --title 'ERROR: Root Hash Mismatch' \ --msgbox "The root hash check failed!\nExiting to a recovery shell" 0 80 fi - die "root hash mismatch, see /tmp/hash_output_mismatches for details" + DIE "root hash mismatch, see /tmp/hash_output_mismatches for details" fi fi } @@ -120,14 +117,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. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" + 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. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" + 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. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" + DIE "Invalid TPM counter state. Please reset TPM through the Heads menu: Options -> TPM/TOTP/HOTP Options -> Reset the TPM" valid_rollback="y" } @@ -136,42 +133,45 @@ first_menu="y" get_menu_option() { num_options=$(cat $TMP_MENU_FILE | wc -l) if [ $num_options -eq 0 ]; then - die "No boot options" + DIE "No boot options" fi if [ $num_options -eq 1 -a $first_menu = "y" ]; then option_index=1 elif [ "$gui_menu" = "y" ]; then - MENU_OPTIONS="" + MENU_OPTIONS=() n=0 while read option; do parse_option n=$(expr $n + 1) - name=$(echo $name | tr " " "_") - MENU_OPTIONS="$MENU_OPTIONS $n ${name} " + MENU_OPTIONS+=("$n" "$name") done <$TMP_MENU_FILE - whiptail --title "Select your boot option" \ + whiptail_type $BG_COLOR_MAIN_MENU --title "Select your boot option" \ --menu "Choose the boot option [1-$n, a to abort]:" 0 80 8 \ - -- $MENU_OPTIONS \ - 2>/tmp/whiptail || die "Aborting boot attempt" + -- "${MENU_OPTIONS[@]}" \ + 2>/tmp/whiptail || DIE "Aborting boot attempt" option_index=$(cat /tmp/whiptail) else - echo "+++ Select your boot option:" + STATUS "Select your boot option:" n=0 while read option; do parse_option n=$(expr $n + 1) - echo "$n. $name [$kernel]" + # Use the same device routing as INPUT so option lines and the + # prompt share the same unbuffered fd (HEADS_TTY when in gui-init + # context, stderr otherwise). Writing to stdout is wrong here + # because DO_WITH_DEBUG pipes stdout through tee for debug logging, + # making it fully buffered — the last option would appear after the + # INPUT prompt. + printf '%d. %s [%s]\n' "$n" "$name" "$kernel" >"${HEADS_TTY:-/dev/stderr}" done <$TMP_MENU_FILE - read \ - -p "Choose the boot option [1-$n, a to abort]: " \ - option_index + INPUT "Choose the boot option [1-$n, a to abort]:" -r option_index if [ "$option_index" = "a" ]; then - die "Aborting boot attempt" + DIE "Aborting boot attempt" fi fi first_menu="n" @@ -187,18 +187,14 @@ confirm_menu_option() { whiptail_warning --title "Confirm boot details" \ --menu "Confirm the boot details for $name:\n\n$(echo $kernel | fold -s -w 80) \n\n" 0 80 8 \ -- 'd' "${default_text}" 'y' "Boot one time" \ - 2>/tmp/whiptail || die "Aborting boot attempt" + 2>/tmp/whiptail || DIE "Aborting boot attempt" option_confirm=$(cat /tmp/whiptail) else - echo "+++ Please confirm the boot details for $name:" - echo $option - - read \ - -n 1 \ - -p "Confirm selection by pressing 'y', make default with 'd': " \ - option_confirm - echo + STATUS "Confirm boot details for $name:" + INFO "$option" + + INPUT "Confirm selection by pressing 'y', make default with 'd':" -n 1 option_confirm fi } @@ -208,11 +204,11 @@ parse_option() { } scan_options() { - INFO "+++ Scanning for unsigned boot options" + STATUS "Scanning for unsigned boot options" option_file="/tmp/kexec_options.txt" scan_boot_options "$bootdir" "$config" "$option_file" if [ ! -s $option_file ]; then - die "Failed to parse any boot options" + DIE "Failed to parse any boot options" fi if [ "$unique" = 'y' ]; then sort -r $option_file | uniq >$TMP_MENU_FILE @@ -223,28 +219,24 @@ scan_options() { save_default_option() { if [ "$gui_menu" != "y" ]; then - read \ - -n 1 \ - -p "Saving a default will modify the disk. Proceed? (Y/n): " \ - default_confirm - echo + INPUT "Saving a default will modify the disk. Proceed? (Y/n):" -n 1 default_confirm fi [ "$default_confirm" = "" ] && default_confirm="y" if [[ "$default_confirm" = "y" || "$default_confirm" = "Y" ]]; then - if kexec-save-default \ + if kexec-save-default.sh \ -b "$bootdir" \ -d "$paramsdev" \ -p "$paramsdir" \ -i "$option_index" \ ; then - echo "+++ Saved defaults to device" + STATUS_OK "Saved defaults to device" default_failed="n" force_menu="n" return else - echo "Failed to save defaults" + WARN "Failed to save defaults" fi fi @@ -265,17 +257,17 @@ default_select() { whiptail_error --title 'ERROR: Boot Entry Has Changed' \ --msgbox "The list of boot entries has changed\n\nPlease set a new default" 0 80 fi - warn "Boot entry has changed - please set a new default" + WARN "Boot entry has changed - please set a new default" return fi parse_option if [ "$CONFIG_BASIC" != "y" ]; then # Enforce that default option hashes are valid - INFO "+++ Checking verified default boot hash file " + STATUS "Checking verified default boot hash file" # Check the hashes of all the files if (cd $bootdir && sha256sum -c "$TMP_DEFAULT_HASH_FILE" >/tmp/hash_output); then - echo "+++ Verified default boot hashes " + STATUS_OK "Verified default boot hashes" valid_hash='y' else if [ "$gui_menu" = "y" ]; then @@ -286,9 +278,9 @@ default_select() { fi fi - echo "+++ Executing default boot for $name:" + STATUS "Executing default boot for $name" do_boot - warn "Failed to boot default option" + WARN "Failed to boot default option" } user_select() { @@ -315,7 +307,7 @@ user_select() { true else NOTE "Rebooting to start the new default option" - reboot + reboot.sh fi fi @@ -324,29 +316,29 @@ user_select() { do_boot() { if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_BOOT_REQ_ROLLBACK" = "y" ] && [ "$valid_rollback" = "n" ]; then - die "!!! Missing required rollback counter state" + DIE "Missing required rollback counter state" fi if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_BOOT_REQ_HASH" = "y" ] && [ "$valid_hash" = "n" ]; then - die "!!! Missing required boot hashes" + DIE "Missing required boot hashes" fi - if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_TPM" = "y" ] && [ -r "$TMP_KEY_DEVICES" ]; then - INITRD=$(kexec-boot -b "$bootdir" -e "$option" -i) || - die "!!! Failed to extract the initrd from boot option" + if [ "$CONFIG_BASIC" != y ] && [ "$CONFIG_TPM" = "y" ] && [ -r "$TMP_KEY_DEVICES" ] && [ "$force_boot" != "y" ]; then + INITRD=$(kexec-boot.sh -b "$bootdir" -e "$option" -i) || + DIE "Failed to extract the initrd from boot option" if [ -z "$INITRD" ]; then - die "!!! No initrd file found in boot option" + DIE "No initrd file found in boot option" fi - kexec-insert-key $INITRD || - die "!!! Failed to prepare TPM Disk Unlock Key for boot" + kexec-insert-key.sh $INITRD || + DIE "Failed to prepare TPM Disk Unlock Key for boot" - kexec-boot -b "$bootdir" -e "$option" \ + kexec-boot.sh -b "$bootdir" -e "$option" \ -a "$add" -r "$remove" -o "/tmp/secret/initrd.cpio" || - die "!!! Failed to boot w/ options: $option" + DIE "Failed to boot w/ options: $option" else - kexec-boot -b "$bootdir" -e "$option" -a "$add" -r "$remove" || - die "!!! Failed to boot w/ options: $option" + kexec-boot.sh -b "$bootdir" -e "$option" -a "$add" -r "$remove" || + DIE "Failed to boot w/ options: $option" fi } @@ -383,7 +375,7 @@ while true; do TRACE_FUNC INFO "TPM: Extending PCR[4] to prevent further secret unsealing" tpmr.sh extend -ix 4 -ic generic || - die "Failed to extend TPM PCR[4]" + DIE "Failed to extend TPM PCR[4]" fi fi @@ -400,7 +392,7 @@ while true; do verify_global_hashes if [ "$valid_global_hash" = "n" ]; then - die "Failed to verify global hashes" + DIE "Failed to verify global hashes" fi fi @@ -424,4 +416,4 @@ while true; do fi done -die "!!! Shouldn't get here" +DIE "Shouldn't get here" diff --git a/initrd/bin/kexec-sign-config.sh b/initrd/bin/kexec-sign-config.sh index 6b0cd1d87..cde9fae01 100755 --- a/initrd/bin/kexec-sign-config.sh +++ b/initrd/bin/kexec-sign-config.sh @@ -21,7 +21,7 @@ while getopts "p:c:ur" arg; do done if [ -z "$paramsdir" ]; then - die "Usage: $0 -p /boot [ -u | -c counter ]" + DIE "Usage: $0 -p /boot [ -u | -c counter ]" fi paramsdir="${paramsdir%%/}" @@ -34,28 +34,37 @@ mount -o remount,rw /boot DEBUG "Signing kexec parameters in $paramsdir, rollback=$rollback, update=$update, counter=$counter" -# update hashes in /boot before signing +# All writes go to a staging directory first; files are moved to their final +# locations only after signing succeeds. This prevents a failed signing +# attempt (wrong PIN, connection error, etc.) from leaving /boot with updated +# hash files but a stale or missing signature. +stagedir=$(mktemp -d /tmp/kexec-sign-XXXXXX) +cleanup_stagedir() { rm -rf "$stagedir"; } +trap cleanup_stagedir EXIT + +# Seed staging with any existing kexec*.txt from paramsdir so unchanged files +# are included when building param_files for signing. +for f in "$paramsdir"/kexec*.txt; do + [ -e "$f" ] && cp "$f" "$stagedir/" +done + +# update hashes in staging before signing if [ "$update" = "y" ]; then ( TRACE_FUNC - DEBUG "update=y: Updating kexec hashes in /boot" + DEBUG "update=y: Updating kexec hashes in staging dir $stagedir" cd /boot - find ./ -type f ! -path './kexec*' -print0 | xargs -0 sha256sum >/boot/kexec_hashes.txt + find ./ -type f ! -path './kexec*' -print0 | xargs -0 sha256sum >"$stagedir/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 + DEBUG "/boot/kexec_default_hashes.txt exists, updating in staging" + DEFAULT_FILES=$(cut -f3 -d ' ' "$stagedir/kexec_default_hashes.txt" fi #also save the file & directory structure to detect added files - print_tree >/boot/kexec_tree.txt - TRACE_FUNC + print_tree >"$stagedir/kexec_tree.txt" ) - [ $? -eq 0 ] || die "$paramsdir: Failed to update hashes." - - # Remove any package trigger log files - # We don't need them after the user decides to sign - rm -f /boot/kexec_package_trigger* + [ $? -eq 0 ] || DIE "$paramsdir: Failed to update hashes." fi if [ "$rollback" = "y" ]; then @@ -70,7 +79,7 @@ if [ "$rollback" = "y" ]; then # use existing tpm counter DO_WITH_DEBUG read_tpm_counter "$counter" >/dev/null 2>&1 || - die "$paramsdir: Unable to read tpm counter '$counter'" + DIE "$paramsdir: Unable to read tpm counter '$counter'" else DEBUG "rollback=y: counter was not provided: checking for existing TPM counter from TPM rollback_file=$rollback_file" TRACE_FUNC @@ -82,7 +91,7 @@ if [ "$rollback" = "y" ]; then 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" + 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 $TPM_COUNTER || + 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." + # read_tpm_counter doesn't prompt; silence its normal output if called in DEBUG context DO_WITH_DEBUG read_tpm_counter "$TPM_COUNTER" >/dev/null 2>&1 || - die "$paramsdir: TPM counter file '$incremented_counter_file' not found after incrementing." + DIE "$paramsdir: TPM counter file '$incremented_counter_file' not found after incrementing." fi 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" + # Write rollback file to staging; moved to paramsdir only on signing success + sha256sum "$incremented_counter_file" >"$stagedir/kexec_rollback.txt" || + DIE "$paramsdir: Unable to create rollback file in staging" fi TRACE_FUNC -param_files=$(find $paramsdir/kexec*.txt) -if [ -z "$param_files" ]; then - die "$paramsdir: No kexec parameter files to sign" + +# Collect the list of kexec*.txt files present in staging (relative names +# only, so the sha256sum output is path-independent and check_config can +# reproduce the same signed data from $paramsdir after the move). +param_files=() +for f in "$stagedir"/kexec*.txt; do + [ -e "$f" ] || continue + param_files+=( "$(basename "$f")" ) +done +if [ ${#param_files[@]} -eq 0 ]; then + DIE "$paramsdir: No kexec parameter files to sign" +fi +DEBUG "kexec-sign-config.sh: ${#param_files[@]} file(s) to sign from $stagedir: ${param_files[*]}" + +# before we even attempt to sign, make sure there is at least one public +# key available so the user gets a clear error instead of a mysterious gpg +# failure. +if [ "$(gpg -k 2>/dev/null | wc -l)" -eq 0 ]; then + DIE "$paramsdir: no public GPG keys in keyring; add one via Options --> GPG Options --> Add GPG key to running BIOS and reflash, or perform OEM Factory Reset / Re-Ownership." fi for tries in 1 2 3; do 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 + DEBUG "kexec-sign-config.sh: signing attempt ${tries}/3 begins after GPG card confirmation" + + # Public keys are not sufficient for signing. After cache_gpg_signing_pin, + # force discovery of a usable secret key identity and pass it explicitly + # to gpg, instead of relying on implicit default-key selection. + card_status_output=$(gpg --card-status 2>/dev/null || true) + SIGNING_KEY_ID=$(gpg --with-colons --list-secret-keys 2>/dev/null | awk -F: '$1=="sec"||$1=="ssb" {print $5; exit}') + if [ -z "$SIGNING_KEY_ID" ]; then + CARD_SIGNING_KEY_ID=$(echo "$card_status_output" | awk -F: '/Signature key/ {gsub(/[[:space:]]/,"",$2); print $2; exit}') + if [ -n "$CARD_SIGNING_KEY_ID" ]; then + SIGNING_KEY_ID="$CARD_SIGNING_KEY_ID" + DEBUG "kexec-sign-config.sh: using card-reported signing key id ${SIGNING_KEY_ID}" + fi + fi + if [ -z "$SIGNING_KEY_ID" ]; then + DIE "$paramsdir: no private signing key is available. A public key in keyring is not enough to sign. Insert/unlock the signing smartcard (or import private key backup material), then retry. If smartcard is inserted, ensure it contains a signing key and that GPG card status shows a non-empty 'Signature key'." + fi + DEBUG "kexec-sign-config.sh: using explicit signing key id ${SIGNING_KEY_ID}" + + # Sign using relative filenames (cd into stagedir) so the sha256sum + # output contains bare names like "kexec_hashes.txt" rather than the + # full staging path. check_config reproduces the same output by doing + # the same cd+sha256sum from $paramsdir after the files are moved there. + DEBUG "kexec-sign-config.sh: running sha256sum (relative) | gpg in $stagedir" + if (cd "$stagedir" && sha256sum "${param_files[@]}") | \ + gpg --local-user "$SIGNING_KEY_ID" \ + --pinentry-mode=loopback \ + --passphrase-file /tmp/secret/gpg_pin \ + --detach-sign -a >"$stagedir/kexec.sig" 2>/tmp/kexec-sign.log; then + # Signing succeeded — move all staged files to their final locations + DEBUG "kexec-sign-config.sh: signing succeeded; moving staged files to $paramsdir" + for f in "$stagedir"/*; do + [ -e "$f" ] || continue + DEBUG "kexec-sign-config.sh: mv $f -> $paramsdir/$(basename "$f")" + mv "$f" "$paramsdir/$(basename "$f")" + done + DEBUG "kexec-sign-config.sh: all staged files moved to $paramsdir" + + # Remove any package trigger log files now that the user has signed + rm -f /boot/kexec_package_trigger* + + # Validate the final config in paramsdir + DEBUG "kexec-sign-config.sh: calling check_config $paramsdir" + check_config "$paramsdir" # remount /boot as ro mount -o remount,ro /boot + STATUS_OK "Boot hashes signed successfully" exit 0 fi + + DEBUG "kexec-sign-config.sh: signing attempt ${tries}/3 failed" + if [ -r /tmp/kexec-sign.log ]; then + DEBUG "kexec-sign-config.sh: gpg signing stderr/stdout excerpt follows" + DEBUG "$(sed -n '1,40p' /tmp/kexec-sign.log)" + fi + + if grep -Eiq 'no default secret key|no secret key|secret key not available|signing failed: no secret key' /tmp/kexec-sign.log 2>/dev/null; then + DIE "$paramsdir: GPG signing failed because no private signing key is available to gpg. Confirm the expected signing key is present/unlocked on your smartcard or imported backup key material, then retry (Options --> GPG Options --> Add GPG key to running BIOS and reflash, or OEM Factory Reset / Re-Ownership)." + fi + + # Bad PIN after cache_gpg_signing_pin pre-validation is unexpected; clear + # and retry so the user can re-enter the correct PIN on the next attempt. + if grep -Eiq 'bad pin|wrong pin|incorrect pin|pin incorrect|pinentry.*cancel' /tmp/kexec-sign.log 2>/dev/null; then + if [ "$tries" -lt 3 ]; then + WARN "$paramsdir: GPG signing failed due to incorrect PIN (attempt $tries/3) - re-enter the correct PIN at the next prompt" + rm -f /tmp/secret/gpg_pin + continue + else + DIE "$paramsdir: GPG signing failed due to incorrect PIN after 3 attempts. Check remaining retries in the GPG card status menu; if retries are exhausted, unblock/reset with Admin PIN and try again." + fi + fi + + if grep -Eiq 'pin blocked|card is blocked|authentication failed' /tmp/kexec-sign.log 2>/dev/null; then + DIE "$paramsdir: GPG signing failed because smartcard PIN is blocked or authentication is denied. Unblock/reset the card PIN with Admin PIN (or use valid backup key material) and retry signing." + fi done # remount /boot as ro mount -o remount,ro /boot -die "$paramsdir: Unable to sign kexec hashes" +DIE "$paramsdir: Unable to sign kexec hashes" diff --git a/initrd/bin/kexec-unseal-key.sh b/initrd/bin/kexec-unseal-key.sh index 78aa5a464..50d07368a 100755 --- a/initrd/bin/kexec-unseal-key.sh +++ b/initrd/bin/kexec-unseal-key.sh @@ -30,20 +30,20 @@ for tries in 1 2 3; do # passphrase prompt. This gives the user context while they prepare to # type the LUKS passphrase. show_totp_until_esc - - read -r -s -p $'\nEnter LUKS TPM Disk Unlock Key passphrase (blank to abort): ' tpm_password - echo + STATUS "Unlocking LUKS with TPM Disk Unlock Key" + INPUT "Enter LUKS TPM Disk Unlock Key passphrase (blank to abort):" -r -s tpm_password if [ -z "$tpm_password" ]; then - die "Aborting unseal disk encryption key" + DIE "Aborting unseal disk encryption key" fi if DO_WITH_DEBUG --mask-position 6 \ tpmr.sh unseal "$TPM_INDEX" "0,1,2,3,4,5,6,7" "$TPM_SIZE" \ "$key_file" "$tpm_password"; then + STATUS_OK "TPM Disk Unlock Key unsealed" exit 0 fi - warn "Unable to unseal LUKS Disk Unlock Key from TPM" + WARN "Unable to unseal LUKS Disk Unlock Key from TPM" done -die "Retry count exceeded..." +DIE "Retry count exceeded..." diff --git a/initrd/bin/key-init.sh b/initrd/bin/key-init.sh index b26da2910..c139e3f26 100755 --- a/initrd/bin/key-init.sh +++ b/initrd/bin/key-init.sh @@ -20,16 +20,19 @@ 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 + # oem-factory-reset.sh 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" + STATUS "Importing user GPG keys" + 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 -gpg --homedir=/etc/distro/ --import /etc/distro/keys/* 2>/dev/null || warn "Importing distro keys failed" +# Import OS distribution signing keys used to authenticate ISO boots +STATUS "Loading OS distribution signing keys for ISO boot authentication" +gpg --homedir=/etc/distro/ --import /etc/distro/keys/* 2>/dev/null || WARN "Importing distro keys failed" #Set distro keys trust level to ultimate (trust anything that was signed with these keys) -gpg --homedir=/etc/distro/ --list-keys --fingerprint --with-colons|sed -E -n -e 's/^fpr:::::::::([0-9A-F]+):$/\1:6:/p' |gpg --homedir=/etc/distro/ --import-ownertrust 2>/dev/null || warn "Setting distro keys ultimate trust failed" -gpg --homedir=/etc/distro/ --update-trust 2>/dev/null || warn "Updating distro keys trust failed" +gpg --homedir=/etc/distro/ --list-keys --fingerprint --with-colons|sed -E -n -e 's/^fpr:::::::::([0-9A-F]+):$/\1:6:/p' |gpg --homedir=/etc/distro/ --import-ownertrust 2>/dev/null || WARN "Setting distro keys ultimate trust failed" +gpg --homedir=/etc/distro/ --update-trust 2>/dev/null || WARN "Updating distro keys trust failed" -# Add user's keys to the list of trusted keys for ISO signing -gpg --export | gpg --homedir=/etc/distro/ --import 2>/dev/null || warn "Adding user's keys to distro keys failed" +# Add user's key so self-signed ISOs can also be booted from USB +STATUS "Adding user GPG key as trusted for ISO signing" +gpg --export | gpg --homedir=/etc/distro/ --import 2>/dev/null || WARN "Adding user's keys to distro keys failed" diff --git a/initrd/bin/lock_chip.sh b/initrd/bin/lock_chip.sh index 8fb71401c..44a3003a4 100755 --- a/initrd/bin/lock_chip.sh +++ b/initrd/bin/lock_chip.sh @@ -19,9 +19,8 @@ if [ -n "$APM_CNT" -a -n "$FIN_CODE" ]; then # will become write protected in the range specified in the PR0 register. Once # the protection is set and locked, it cannot be disabled # until the next system reset. - echo "Finalizing chipset Write Protection through SMI PR0 lockdown call" + STATUS "Finalizing chipset write protection via SMI PR0 lockdown" io386 -o b -b x $APM_CNT $FIN_CODE else - echo "NOT Finalizing chipset" - echo "lock_chip called without valid APM_CNT and FIN_CODE defined under bin/lock_chip." + NOTE "NOT finalizing chipset - lock_chip.sh called without valid APM_CNT and FIN_CODE" fi diff --git a/initrd/bin/media-scan.sh b/initrd/bin/media-scan.sh index 12d8c0d9b..c41890def 100755 --- a/initrd/bin/media-scan.sh +++ b/initrd/bin/media-scan.sh @@ -8,26 +8,26 @@ set -e -o pipefail TRACE_FUNC #Booting from external media should be authenticated if supported -gpg_auth || die "GPG authentication failed" +gpg_auth || DIE "GPG authentication failed" # Unmount any previous boot device -if grep -q /boot /proc/mounts; then - umount /boot || - die "Unable to unmount /boot" +if grep -q /boot /proc/mounts ; then + umount /boot \ + || DIE "Unable to unmount /boot" fi -available_partitions="$(blkid | while read line; do echo $line | awk -F ":" {'print $1'}; done)" +available_partitions="$(blkid | while read line; do echo $line | awk -F ":" {'print $1'}; done )" if [ "$1" == "usb" ]; then # Mount the USB boot device - mount_usb || die "Unable to mount /media" + mount_usb || DIE "Unable to mount /media" elif $(echo $available_partitions | grep -q "$1"); then if grep -q /media /proc/mounts; then - umount /media || - die "Unable to unmount /media" + umount /media \ + || DIE "Unable to unmount /media" fi - mount "$1" /media || - die "Unable to mount $1 to /media" + mount "$1" /media \ + || DIE "Unable to mount $1 to /media" fi # Get USB boot device @@ -38,67 +38,68 @@ get_menu_option() { if [ -x /bin/whiptail ]; then MENU_OPTIONS="" n=0 - while read option; do - n=$(expr $n + 1) + while read option + do + n=`expr $n + 1` option=$(echo $option | tr " " "_") MENU_OPTIONS="$MENU_OPTIONS $n ${option}" - done /tmp/whiptail || die "Aborting boot attempt" + 2>/tmp/whiptail || DIE "Aborting boot attempt" option_index=$(cat /tmp/whiptail) else - echo "+++ Select your ISO boot option:" + STATUS "Select your ISO boot option:" n=0 - while read option; do - n=$(expr $n + 1) + while read option + do + n=`expr $n + 1` echo "$n. $option" - done /dev/null | sort -r >/tmp/iso_menu.txt || true -if [ $(cat /tmp/iso_menu.txt | wc -l) -gt 0 ]; then +find /media -name "*.iso" -type f 2>/dev/null | sort -r > /tmp/iso_menu.txt || true +if [ `cat /tmp/iso_menu.txt | wc -l` -gt 0 ]; then option_confirm="" - while [ -z "$option" -a "$option_index" != "s" ]; do + while [ -z "$option" -a "$option_index" != "s" ] + do get_menu_option done MOUNTED_ISO="$option" ISO="${option:7}" # remove /media/ to get device relative path - DO_WITH_DEBUG kexec-iso-init "$MOUNTED_ISO" "$ISO" "$USB_BOOT_DEV" + DO_WITH_DEBUG kexec-iso-init.sh "$MOUNTED_ISO" "$ISO" "$USB_BOOT_DEV" - die "Something failed in iso init" + DIE "Something failed in iso init" fi # No *.iso files on media, try ordinary bootable USB if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then - die "No ISO files found, bootable USB not allowed with Restricted Boot." + DIE "No ISO files found, bootable USB not allowed with Restricted Boot." fi -echo "!!! Could not find any ISO, trying bootable USB" +WARN "Could not find any ISO, trying bootable USB" # Attempt to pull verified config from device if [ -x /bin/whiptail ]; then DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u -g -s @@ -106,4 +107,4 @@ else DO_WITH_DEBUG kexec-select-boot.sh -b /media -c "*.cfg" -u -s fi -die "Something failed in selecting boot" +DIE "Something failed in selecting boot" diff --git a/initrd/bin/mount-usb.sh b/initrd/bin/mount-usb.sh index e4c4f252b..fb975ed56 100755 --- a/initrd/bin/mount-usb.sh +++ b/initrd/bin/mount-usb.sh @@ -7,7 +7,7 @@ TRACE_FUNC function usage() { - cat < <--device device> <--mountpoint mountpoint> <--pass passphrase> $0 --help @@ -25,46 +25,44 @@ DEVICE="" MOUNTPOINT="/media" PASS="" - #Only assign --mode, --device, --mountpoint and --pass parameters only if variables following them are not empty while [ $# -gt 0 ]; do - case "$1" in - --mode) - if [ -n "$2" ]; then - MODE="$2" - shift - shift - fi - ;; - --device) - if [ -n "$2" ]; then - DEVICE="$2" - shift - shift - fi - ;; - --mountpoint) - if [ -n "$2" ]; then - MOUNTPOINT="$2" - shift - shift - fi - ;; - --pass) - if [ -n "$2" ]; then - PASS="$2" - shift - shift - fi - ;; - *) - usage - exit 1 - ;; - esac + case "$1" in + --mode) + if [ -n "$2" ]; then + MODE="$2" + shift + shift + fi + ;; + --device) + if [ -n "$2" ]; then + DEVICE="$2" + shift + shift + fi + ;; + --mountpoint) + if [ -n "$2" ]; then + MOUNTPOINT="$2" + shift + shift + fi + ;; + --pass) + if [ -n "$2" ]; then + PASS="$2" + shift + shift + fi + ;; + *) + usage + exit 1 + ;; + esac done - #Show parameters content but not LUKS passphrase: if empty, show "empty", if provided, show "provided" DEBUG "Parameters: --mode=$MODE, --device=${DEVICE:-empty}, --mountpoint=$MOUNTPOINT, --pass=${PASS:+provided}" @@ -72,133 +70,125 @@ enable_usb enable_usb_storage if [ ! -d "$MOUNTPOINT" ]; then - DEBUG "Creating $MOUNTPOINT directory" - mkdir -p "$MOUNTPOINT" > /dev/null 2>&1 + DEBUG "Creating $MOUNTPOINT directory" + mkdir -p "$MOUNTPOINT" >/dev/null 2>&1 else - DEBUG "Cleaning $MOUNTPOINT directory" - umount "$MOUNTPOINT" > /dev/null 2>&1 || true + DEBUG "Cleaning $MOUNTPOINT directory" + umount "$MOUNTPOINT" >/dev/null 2>&1 || true fi - -list_usb_storage > /tmp/usb_block_devices +list_usb_storage >/tmp/usb_block_devices if [ -z "$(cat /tmp/usb_block_devices)" ]; then - if [ -x /bin/whiptail ]; then - whiptail_warning --title 'USB Drive Missing' \ - --msgbox "Insert your USB drive and press Enter to continue." 0 80 - else - echo "+++ USB Drive Missing! Insert your USB drive and press Enter to continue." - read - fi - sleep 1 - list_usb_storage > /tmp/usb_block_devices - if [ -z "$(cat /tmp/usb_block_devices)" ]; then - if [ -x /bin/whiptail ]; then - whiptail_error --title 'ERROR: USB Drive Missing' \ - --msgbox "USB Drive Missing! Aborting mount attempt.\n\nPress Enter to continue." 0 80 - else - echo "!!! ERROR: USB Drive Missing! Aborting mount. Press Enter to continue." - fi - exit 1 - fi + if [ -x /bin/whiptail ]; then + whiptail_warning --title 'USB Drive Missing' \ + --msgbox "Insert your USB drive and press Enter to continue." 0 80 + else + INPUT "USB Drive Missing! Insert your USB drive and press Enter to continue." + fi + sleep 1 + list_usb_storage >/tmp/usb_block_devices + if [ -z "$(cat /tmp/usb_block_devices)" ]; then + if [ -x /bin/whiptail ]; then + whiptail_error --title 'ERROR: USB Drive Missing' \ + --msgbox "USB Drive Missing! Aborting mount attempt.\n\nPress Enter to continue." 0 80 + else + DIE "USB Drive Missing! Aborting mount." + fi + exit 1 + fi fi USB_MOUNT_DEVICE="" # Check if the user has specified a USB device if [ -n "$DEVICE" ]; then - DEBUG "Checking if "$DEVICE" is a USB detected block device" - if grep -q "$DEVICE" /tmp/usb_block_devices; then - DEBUG "Selected device is a USB block device" - USB_MOUNT_DEVICE="$DEVICE" - else - die "ERROR: Selected $DEVICE is not a USB block device" - fi + DEBUG "Checking if "$DEVICE" is a USB detected block device" + if grep -q "$DEVICE" /tmp/usb_block_devices; then + DEBUG "Selected device is a USB block device" + USB_MOUNT_DEVICE="$DEVICE" + else + DIE "ERROR: Selected $DEVICE is not a USB block device" + fi else - # Check for the common case: a single USB disk with one partition - if [ $(cat /tmp/usb_block_devices | wc -l) -eq 1 ]; then - USB_MOUNT_DEVICE="$(cat /tmp/usb_block_devices)" - fi - # otherwise, let the user pick - if [ -z ${USB_MOUNT_DEVICE} ]; then - > /tmp/usb_disk_list - for i in $(cat /tmp/usb_block_devices); do - #appends label to the device name - echo $i $(blkid | grep $i | grep -o 'LABEL=".*"' | cut -f2 -d '"') >> /tmp/usb_disk_list - done - - if [ -x /bin/whiptail ]; then - MENU_OPTIONS="" - n=0 - while read option - do - n=$(expr $n + 1) - option=$(echo $option | tr " " "_") - MENU_OPTIONS="$MENU_OPTIONS $n ${option}" - done < /tmp/usb_disk_list - - MENU_OPTIONS="$MENU_OPTIONS a Abort" - whiptail --title "Select your USB disk" \ - --menu "Choose your USB disk [1-$n, a to abort]:" 0 80 8 \ - -- $MENU_OPTIONS \ - 2>/tmp/whiptail - if [ $? -ne 0 ]; then - die "ERROR: Selecting USB disk/partition aborted." - fi - option_index=$(cat /tmp/whiptail) - else - echo "+++ Select your USB disk:" - n=0 - while read option - do - n=$(expr $n + 1) - echo "$n. $option" - done < /tmp/usb_disk_list - - read \ - -p "Choose your USB disk [1-$n, a to abort]: " \ - option_index - fi - - if [ "$option_index" = "a" ]; then - exit 5 - fi - USB_MOUNT_DEVICE=$(head -n $option_index /tmp/usb_disk_list | tail -1 | sed 's/\ .*$//') - fi -fi + # Check for the common case: a single USB disk with one partition + if [ $(cat /tmp/usb_block_devices | wc -l) -eq 1 ]; then + USB_MOUNT_DEVICE="$(cat /tmp/usb_block_devices)" + fi + # otherwise, let the user pick + if [ -z ${USB_MOUNT_DEVICE} ]; then + >/tmp/usb_disk_list + for i in $(cat /tmp/usb_block_devices); do + #appends label to the device name + echo $i $(blkid | grep $i | grep -o 'LABEL=".*"' | cut -f2 -d '"') >>/tmp/usb_disk_list + done + + if [ -x /bin/whiptail ]; then + MENU_OPTIONS=() + n=0 + while read option; do + n=$(expr $n + 1) + MENU_OPTIONS+=("$n" "$option") + done /tmp/whiptail + if [ $? -ne 0 ]; then + DIE "ERROR: Selecting USB disk/partition aborted." + fi + option_index=$(cat /tmp/whiptail) + else + STATUS "Select your USB disk:" + n=0 + while read option; do + n=$(expr $n + 1) + printf '%d. %s\n' "$n" "$option" >"${HEADS_TTY:-/dev/stderr}" + done 1 >/dev/null - echo "Attempting to sync time with NTP server: $DNS_SERVER..." + STATUS "Attempting NTP time sync with $DNS_SERVER" if ! ntpd -d -N -n -q -p $DNS_SERVER; then - echo "NTP sync unsuccessful with DNS server" - echo "Attempting NTP time sync with pool.ntp.org..." + WARN "NTP sync unsuccessful with DNS server" + STATUS "Attempting NTP time sync with pool.ntp.org" if ! ntpd -d -d -N -n -q -p pool.ntp.org; then - echo "NTP sync unsuccessful." + WARN "NTP sync unsuccessful" else - echo "NTP time sync successful." + STATUS_OK "NTP time sync successful" fi fi - echo "Syncing hardware clock with system time in UTC/GMT timezone..." + STATUS "Syncing hardware clock with system time (UTC)" hwclock -w - echo "" date=$(date "+%Y-%m-%d %H:%M:%S %Z") - echo "Time: $date" + STATUS "Time: $date" fi fi fi @@ -134,7 +126,7 @@ if [ -n "$dev" ]; then if [ ! -d /etc/dropbear ]; then mkdir /etc/dropbear fi - echo "Starting dropbear ssh server..." + STATUS "Starting dropbear SSH server" # Make sure dropbear is not already running killall dropbear > /dev/null 2>&1 || true # Start dropbear with root login and log to stderr @@ -142,7 +134,6 @@ if [ -n "$dev" ]; then # -R create host keys dropbear -B -R fi - echo "" - echo "Network setup complete:" + STATUS_OK "Network setup complete" ifconfig $dev fi diff --git a/initrd/bin/oem-factory-reset.sh b/initrd/bin/oem-factory-reset.sh index 31aa89097..f70b03a03 100755 --- a/initrd/bin/oem-factory-reset.sh +++ b/initrd/bin/oem-factory-reset.sh @@ -6,6 +6,7 @@ set -o pipefail ## External files sourced . /etc/functions.sh . /etc/gui_functions.sh +. /etc/gpg_functions.sh . /etc/luks-functions.sh . /tmp/config @@ -33,7 +34,7 @@ GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" #Circumvent Librem Key/Nitrokey HOTP firmware bug https://github.com/osresearch/heads/issues/1167 MAX_HOTP_GPG_PIN_LENGTH=25 -# What are the Security components affected by custom passwords +# What are the Security components affected by custom passphrases CUSTOM_PASS_AFFECTED_COMPONENTS="" # Default GPG Algorithm is RSA @@ -56,7 +57,7 @@ handle_mode() { USER_PIN=$CUSTOM_SINGLE_PASS ADMIN_PIN=$CUSTOM_SINGLE_PASS TPM_PASS=$CUSTOM_SINGLE_PASS - # User doesn't know this password, really badger them to record it + # User doesn't know this passphrase, really badger them to record it MAKE_USER_RECORD_PASSPHRASES=y title_text="OEM Factory Reset Mode" @@ -66,13 +67,13 @@ handle_mode() { USER_PIN=$(generate_passphrase --number_words 2 --max_length $MAX_HOTP_GPG_PIN_LENGTH) ADMIN_PIN=$(generate_passphrase --number_words 2 --max_length $MAX_HOTP_GPG_PIN_LENGTH) TPM_PASS=$ADMIN_PIN - # User doesn't know this password, really badger them to record it + # User doesn't know this passphrase, really badger them to record it MAKE_USER_RECORD_PASSPHRASES=y title_text="User Re-Ownership Mode" ;; *) - warn "Unknown oem-factory-reset lauched mode, setting PINs to weak defaults" + WARN "Unknown oem-factory-reset.sh launched mode, setting PINs to weak defaults" USER_PIN=$USER_PIN_DEF ADMIN_PIN=$ADMIN_PIN_DEF TPM_PASS=$ADMIN_PIN_DEF @@ -101,7 +102,7 @@ if [[ -n "$MODE" ]]; then fi #Override RSA_KEY_LENGTH to 2048 bits for Canokey under qemu testing boards until canokey fixes -if [[ "$CONFIG_BOARD_NAME" == qemu-* ]]; then +if [[ "$CONFIG_BOARD_NAME" == qemu-* ]] && [[ "$DONGLE_BRAND" == "Canokey" ]]; then DEBUG "Overriding RSA_KEY_LENGTH to 2048 bits for Canokey under qemu testing boards" RSA_KEY_LENGTH=2048 fi @@ -114,11 +115,10 @@ SKIP_BOOT="n" ## functions -die() { - +DIE() { local msg=$1 if [ -n "$msg" ]; then - echo -e "\n$msg" + WARN "$msg" fi kill -s TERM $TOP_PID exit 1 @@ -127,14 +127,14 @@ die() { local_whiptail_error() { local msg=$1 if [ "$msg" = "" ]; then - die "whiptail error: An error msg is required" + DIE "whiptail error: An error msg is required" fi whiptail_error --msgbox "${msg}\n\n" $HEIGHT $WIDTH --title "Error" } whiptail_error_die() { local_whiptail_error "$@" - die + DIE } mount_boot() { @@ -144,7 +144,7 @@ mount_boot() { if ! grep -q /boot /proc/mounts; then # try to mount if CONFIG_BOOT_DEV exists if [ -e "$CONFIG_BOOT_DEV" ]; then - mount -o ro $CONFIG_BOOT_DEV /boot || die "Failed to mount $CONFIG_BOOT_DEV. Please change boot device under Configuration > Boot Device" + mount -o ro $CONFIG_BOOT_DEV /boot || DIE "Failed to mount $CONFIG_BOOT_DEV. Please change boot device under Configuration > Boot Device" fi fi } @@ -153,20 +153,19 @@ reset_nk3_secret_app() { TRACE_FUNC # Reset Nitrokey 3 Secrets app PIN with $ADMIN_PIN (default 12345678, or customised) - if lsusb | grep -q "20a0:42b2" && [ -x /bin/hotp_verification ]; then - echo - warn "Resetting Nitrokey 3's Secrets app with PIN. Physical presence (touch) will be required" + if [ "$DONGLE_BRAND" = "Nitrokey 3" ] && [ -x /bin/hotp_verification ]; then + STATUS "Resetting Nitrokey 3 Secrets app (physical touch will be required)" # TODO: change message when https://github.com/Nitrokey/nitrokey-hotp-verification/issues/41 is fixed # Reset Nitrokey 3 secret app with PIN # Do 3 attempts to reset Nitrokey 3 Secrets app if return code is 3 (no touch) for attempt in 1 2 3; do - if /bin/hotp_verification reset "${ADMIN_PIN}"; then - echo + if hotp_verification reset "${ADMIN_PIN}"; then + STATUS_OK "Nitrokey 3 Secrets app reset" return 0 else error_code=$? if [ $error_code -eq 3 ] && [ $attempt -lt 3 ]; then - whiptail --msgbox "Nitrokey 3 requires physical presence: touch the dongle when requested" $HEIGHT $WIDTH --title "Nk3 secrets app reset attempt: $attempt/3" + whiptail_warning --msgbox "Nitrokey 3 requires physical presence: touch the dongle when requested" $HEIGHT $WIDTH --title "Nk3 secrets app reset attempt: $attempt/3" else whiptail_error_die "Nitrokey 3's Secrets app reset failed with error:$error_code. Contact Nitrokey support" fi @@ -181,7 +180,7 @@ reset_nk3_secret_app() { generate_inmemory_RSA_master_and_subkeys() { TRACE_FUNC - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits master key..." + STATUS "Generating RSA ${RSA_KEY_LENGTH}-bit master key for $DONGLE_BRAND" # Generate GPG master key { echo "Key-Type: RSA" # RSA key @@ -199,7 +198,7 @@ generate_inmemory_RSA_master_and_subkeys() { whiptail_error_die "GPG Key generation failed!\n\n$ERROR" fi - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits signing subkey..." + STATUS "Generating RSA signing subkey for $DONGLE_BRAND" # Add signing subkey { echo addkey # add key in --edit-key mode @@ -216,7 +215,7 @@ generate_inmemory_RSA_master_and_subkeys() { whiptail_error_die "GPG Key signing subkey generation failed!\n\n$ERROR" fi - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits encryption subkey..." + STATUS "Generating RSA encryption subkey for $DONGLE_BRAND" #Add encryption subkey { echo addkey # add key in --edit-key mode @@ -233,7 +232,7 @@ generate_inmemory_RSA_master_and_subkeys() { whiptail_error_die "GPG Key encryption subkey generation failed!\n\n$ERROR" fi - echo "Generating GPG RSA ${RSA_KEY_LENGTH} bits authentication subkey..." + STATUS "Generating RSA authentication subkey for $DONGLE_BRAND" #Add authentication subkey { #Authentication subkey needs gpg in expert mode to select RSA custom mode (8) @@ -264,7 +263,7 @@ generate_inmemory_RSA_master_and_subkeys() { generate_inmemory_p256_master_and_subkeys() { TRACE_FUNC - echo "Generating GPG p256 bits master key..." + STATUS "Generating p256 master key for $DONGLE_BRAND" { echo "Key-Type: ECDSA" # ECDSA key echo "Key-Curve: nistp256" # ECDSA key curve @@ -285,7 +284,7 @@ generate_inmemory_p256_master_and_subkeys() { #Keep Master key fingerprint for add key calls MASTER_KEY_FP=$(gpg --list-secret-keys --with-colons | grep fpr | cut -d: -f10) - echo "Generating GPG nistp256 signing subkey..." + STATUS "Generating p256 signing subkey for $DONGLE_BRAND" { echo addkey # add key in --edit-key mode echo 11 # ECC own set capability @@ -300,7 +299,7 @@ generate_inmemory_p256_master_and_subkeys() { whiptail_error_die "Failed to add ECC nistp256 signing key to master key\n\n${ERROR_MSG}" fi - echo "Generating GPG nistp256 encryption subkey..." + STATUS "Generating p256 encryption subkey for $DONGLE_BRAND" { echo addkey echo 12 # ECC own set capability @@ -315,7 +314,7 @@ generate_inmemory_p256_master_and_subkeys() { whiptail_error_die "Failed to add ECC nistp256 encryption key to master key\n\n${ERROR_MSG}" fi - echo "Generating GPG nistp256 authentication subkey..." + STATUS "Generating p256 authentication subkey for $DONGLE_BRAND" { echo addkey # add key in --edit-key mode echo 11 # ECC own set capability @@ -346,11 +345,12 @@ keytocard_subkeys_to_smartcard() { #make sure usb ready and USB Security dongle ready to communicate with enable_usb enable_usb_storage - gpg --card-status >/dev/null 2>&1 || die "Error getting GPG card status" + STATUS "Accessing $DONGLE_BRAND OpenPGP smartcard" + gpg --card-status >/dev/null 2>&1 || DIE "Error getting GPG card status" gpg_key_factory_reset - echo "Moving subkeys to smartcard..." + STATUS "Moving subkeys to $DONGLE_BRAND" { echo "key 1" #Toggle on Signature key in --edit-key mode on local keyring echo "keytocard" #Move Signature key to smartcard @@ -378,6 +378,7 @@ keytocard_subkeys_to_smartcard() { ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key moving subkeys to smartcard failed!\n\n$ERROR" fi + STATUS_OK "Subkeys moved to smartcard" TRACE_FUNC } @@ -388,7 +389,73 @@ prompt_insert_to_be_wiped_thumb_drive() { #Whiptail warning about having only desired to be wiped thumb drive inserted whiptail_warning --title 'WARNING: Please insert the thumb drive to be wiped' \ --msgbox "The thumb drive will be WIPED next.\n\nPlease connect only the thumb drive to be wiped and disconnect others." 0 80 || - die "Error displaying warning about having only desired to be wiped thumb drive inserted" + DIE "Error displaying warning about having only desired to be wiped thumb drive inserted" +} + +set_card_identity() { + TRACE_FUNC + + # Determine which fields we have custom values for + local set_name=0 set_login=0 + local surname given + + # Name: skip if still the OEM default + if [ "$GPG_USER_NAME" != "OEM Key" ] && [ -n "$GPG_USER_NAME" ]; then + set_name=1 + # OpenPGP card stores surname and given name separately; + # gpg displays them as "given surname" + if [[ "$GPG_USER_NAME" == *" "* ]]; then + given="${GPG_USER_NAME% *}" + surname="${GPG_USER_NAME##* }" + else + surname="$GPG_USER_NAME" + given="" + fi + DEBUG "Will set cardholder name: surname='$surname' given='$given'" + else + DEBUG "Skipping cardholder name: no custom name set" + fi + + # Login: skip if still the auto-generated OEM default (oem-*@example.com) + if [ -n "$GPG_USER_MAIL" ] && [[ "$GPG_USER_MAIL" != oem-*@example.com ]]; then + set_login=1 + DEBUG "Will set login data: '$GPG_USER_MAIL'" + else + DEBUG "Skipping login data: no custom email set" + fi + + [ "$set_name" -eq 0 ] && [ "$set_login" -eq 0 ] && return + + STATUS "Setting identity fields on OpenPGP smartcard" + { + echo "admin" + if [ "$set_name" -eq 1 ]; then + echo "name" + echo "${surname}" + echo "${given}" + echo "${ADMIN_PIN_DEF}" + fi + if [ "$set_login" -eq 1 ]; then + echo "login" + echo "${GPG_USER_MAIL}" + echo "${ADMIN_PIN_DEF}" + fi + echo "quit" + } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit || + DIE "Failed to set identity fields on OpenPGP smartcard" + + local summary="" + [ "$set_name" -eq 1 ] && summary="${given:+$given }${surname}" + [ "$set_login" -eq 1 ] && summary="${summary:+$summary, }${GPG_USER_MAIL}" + STATUS_OK "Card identity set: $summary" + #TODO: set card `url` field and GPG key preferred keyserver after uploading to keys.openpgp.org + # Two separate operations needed: + # 1. card `url` — set via gpg --card-edit admin → url → + # 2. key `keyserver` preference — set via gpg --edit-key → keyserver → → save + # (applies to both on-card and in-memory key paths) + # Requires: network access in initrd, curl, and user email verification on keyserver. + # Note: keys.openpgp.org hides UID until owner verifies email — upload works but key + # is not searchable by email until verified from a normal OS session after provisioning. } #export master key and subkeys to thumbdrive's private LUKS contained partition @@ -419,24 +486,25 @@ export_master_key_subkeys_and_revocation_key_to_private_LUKS_container() { shift ;; *) - die "Error: unknown argument: $1" + DIE "Error: unknown argument: $1" ;; esac done - mount-usb --mode "$mode" --device "$device" --mountpoint "$mountpoint" --pass "$pass" || die "Error mounting thumb drive's private partition" + mount-usb.sh --mode "$mode" --device "$device" --mountpoint "$mountpoint" --pass "$pass" || DIE "Error mounting thumb drive's private partition" #Export master key and subkeys to thumb drive - DEBUG "Exporting master key and subkeys to private LUKS container's partition..." + STATUS "Exporting master key and subkeys to backup LUKS container" gpg --export-secret-key --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/privkey.sec || - die "Error exporting master key to private LUKS container's partition" + DIE "Error exporting master key to private LUKS container's partition" gpg --export-secret-subkeys --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/subkeys.sec || - die "Error exporting subkeys to private LUKS container's partition" + DIE "Error exporting subkeys to private LUKS container's partition" #copy whole keyring to thumb drive, including revocation key and trust database - cp -af ~/.gnupg "$mountpoint"/.gnupg || die "Error copying whole keyring to private LUKS container's partition" + cp -af ~/.gnupg "$mountpoint"/.gnupg || DIE "Error copying whole keyring to private LUKS container's partition" #Unmount private LUKS container's mount point - umount "$mountpoint" || die "Error unmounting private LUKS container's mount point" + umount "$mountpoint" || DIE "Error unmounting private LUKS container's mount point" + STATUS_OK "Master key and subkeys backed up to USB" TRACE_FUNC } @@ -464,16 +532,18 @@ export_public_key_to_thumbdrive_public_partition() { shift ;; *) - die "Error: unknown argument: $1" + DIE "Error: unknown argument: $1" ;; esac done #pass non-empty arguments to --pass, --mountpoint, --device, --mode - mount-usb --device "$device" --mode "$mode" --mountpoint "$mountpoint" || die "Error mounting thumb drive's public partition" + mount-usb.sh --device "$device" --mode "$mode" --mountpoint "$mountpoint" || DIE "Error mounting thumb drive's public partition" #TODO: reuse "Obtain GPG key ID" so that pubkey on public thumb drive partition is named after key ID - gpg --export --armor "${GPG_USER_MAIL}" >"$mountpoint"/pubkey.asc || die "Error exporting public key to thumb drive's public partition" - umount "$mountpoint" || die "Error unmounting thumb drive's public partition" + STATUS "Exporting public key to USB" + gpg --export --armor "${GPG_USER_MAIL}" >"$mountpoint"/pubkey.asc || DIE "Error exporting public key to thumb drive's public partition" + umount "$mountpoint" || DIE "Error unmounting thumb drive's public partition" + STATUS_OK "Public key exported to USB" TRACE_FUNC } @@ -506,16 +576,16 @@ select_thumb_drive_for_key_material() { # Obtain size of thumb drive to be wiped with fdisk disk_size_bytes="$(blockdev --getsize64 "$FILE")" if [ "$disk_size_bytes" -lt "$((128 * 1024 * 1024))" ]; then - warn "Thumb drive size is less than 128MB!" - warn "LUKS container needs to be at least 8MB!" - warn "If the next operation fails, try with a bigger thumb drive" + WARN "Thumb drive size is less than 128MB!" + WARN "LUKS container needs to be at least 8MB!" + WARN "If the next operation fails, try with a bigger thumb drive" fi select_luks_container_size_percent thumb_drive_luks_percent="$(cat /tmp/luks_container_size_percent)" if ! confirm_thumb_drive_format "$FILE" "$thumb_drive_luks_percent"; then - warn "Thumb drive wipe aborted by user!" + INFO "Thumb drive wipe aborted by user" continue fi @@ -523,12 +593,11 @@ select_thumb_drive_for_key_material() { thumb_drive=$FILE else #No USB storage device detected - warn "No USB storage device detected! Aborting OEM Factory Reset / Re-Ownership" + WARN "No USB storage device detected! Aborting OEM Factory Reset / Re-Ownership" sleep 3 - die "No USB storage device detected! User decided to not wipe any thumb drive" + DIE "No USB storage device detected! User decided to not wipe any thumb drive" fi done - thumb_drive_luks_percent="$(cat /tmp/luks_container_size_percent)" } #Wipe a thumb drive and export master key and subkeys to it @@ -558,7 +627,7 @@ gpg_key_factory_reset() { enable_usb # Factory reset GPG card - echo "GPG factory reset of USB Security dongle's OpenPGP smartcard..." + STATUS "GPG factory reset of $DONGLE_BRAND OpenPGP smartcard" { echo admin # admin menu echo factory-reset # factory reset smartcard @@ -572,16 +641,15 @@ gpg_key_factory_reset() { fi # If Nitrokey Storage is inserted, reset AES keys as well - if lsusb | grep -q "20a0:4109" && [ -x /bin/hotp_verification ]; then - DEBUG "Nitrokey Storage detected, resetting AES keys..." - /bin/hotp_verification regenerate ${ADMIN_PIN_DEF} - DEBUG "Restarting scdaemon to remove possible exclusive lock of dongle" - killall -9 scdaemon + if [ "$DONGLE_BRAND" = "Nitrokey Storage" ] && [ -x /bin/hotp_verification ]; then + STATUS "Resetting Nitrokey Storage AES keys" + hotp_verification regenerate ${ADMIN_PIN_DEF} + STATUS_OK "Nitrokey Storage AES keys reset" fi # Toggle forced sig (good security practice, forcing PIN request for each signature request) if gpg --card-status | grep "Signature PIN" | grep -q "not forced"; then - DEBUG "GPG toggling forcesig on since off..." + STATUS "Enabling forced signature PIN on smartcard" { echo admin # admin menu echo forcesig # toggle forcesig @@ -592,10 +660,12 @@ gpg_key_factory_reset() { ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key forcesig toggle on failed!\n\n$ERROR" fi + STATUS_OK "Forced signature PIN enabled" fi # use p256 for key generation if requested if [ "$GPG_ALGO" = "p256" ]; then + STATUS "Setting NIST-P256 key attributes on $DONGLE_BRAND" { echo admin # admin menu echo key-attr # key attributes @@ -612,11 +682,12 @@ gpg_key_factory_reset() { >/tmp/gpg_card_edit_output 2>&1 if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "Setting key to NIST-P256 in USB Security dongle failed." + whiptail_error_die "Setting key to NIST-P256 in $DONGLE_BRAND failed." fi + STATUS_OK "NIST-P256 key attributes set on $DONGLE_BRAND" # fallback to RSA key generation by default elif [ "$GPG_ALGO" = "RSA" ]; then - DEBUG "GPG setting RSA key length to ${RSA_KEY_LENGTH} bits..." + STATUS "Setting RSA ${RSA_KEY_LENGTH}-bit key attributes on $DONGLE_BRAND (may take a minute)" # Set RSA key length { echo admin @@ -634,8 +705,9 @@ gpg_key_factory_reset() { >/tmp/gpg_card_edit_output 2>&1 if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "Setting key attributed to RSA ${RSA_KEY_LENGTH} bits in USB Security dongle failed." + whiptail_error_die "Setting key attributed to RSA ${RSA_KEY_LENGTH} bits in $DONGLE_BRAND failed." fi + STATUS_OK "RSA ${RSA_KEY_LENGTH}-bit key attributes set on $DONGLE_BRAND" else #Unknown GPG_ALGO whiptail_error_die "Unknown GPG_ALGO: $GPG_ALGO" @@ -648,7 +720,11 @@ generate_OEM_gpg_keys() { TRACE_FUNC #This function simply generates subkeys in smartcard following smarcard config from gpg_key_factory_reset - echo "Generating GPG keys in USB Security dongle's OpenPGP smartcard..." + if [ "$GPG_ALGO" = "RSA" ]; then + STATUS "Generating RSA ${RSA_KEY_LENGTH}-bit keys on $DONGLE_BRAND" + else + STATUS "Generating p256 keys on $DONGLE_BRAND" + fi { echo admin # admin menu echo generate # generate keys @@ -671,6 +747,7 @@ generate_OEM_gpg_keys() { ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key automatic keygen failed!\n\n$ERROR" fi + STATUS_OK "GPG keys generated on $DONGLE_BRAND" TRACE_FUNC } @@ -678,7 +755,6 @@ generate_OEM_gpg_keys() { gpg_key_change_pin() { TRACE_FUNC - DEBUG "Changing GPG key PIN" # 1 = user PIN, 3 = admin PIN PIN_TYPE=$1 PIN_ORIG=${2} @@ -725,27 +801,30 @@ generate_checksums() { if [ "$CONFIG_TPM" = "y" ]; then if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then tpmr.sh counter_create \ - -pwdc '' \ + -pwdc "${TPM_PASS:-}" \ -la -3135106223 | tee /tmp/counter >/dev/null 2>&1 || 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" + # increment TPM counter so /tmp/counter-$TPM_COUNTER is populated, + # then persist rollback metadata under /boot for next-boot preflight. + increment_tpm_counter "$TPM_COUNTER" || + 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 + [ -s /tmp/counter-"$TPM_COUNTER" ] || + whiptail_error_die "TPM counter increment did not produce counter state for rollback file" + + # create rollback file + sha256sum /tmp/counter-"$TPM_COUNTER" >/boot/kexec_rollback.txt 2>/dev/null || + whiptail_error_die "Unable to create rollback file" 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 + ## needs to exist for initial call to unseal-hotp.sh echo "0" >/boot/kexec_hotp_counter fi fi @@ -755,7 +834,7 @@ generate_checksums() { set_default_boot_option fi - DEBUG "Generating hashes" + STATUS "Generating /boot file hashes" ( set -e -o pipefail cd /boot @@ -764,9 +843,16 @@ generate_checksums() { print_tree >/boot/kexec_tree.txt ) [ $? -eq 0 ] || whiptail_error_die "Error generating kexec hashes" - - param_files=$(find /boot/kexec*.txt) - [ -z "$param_files" ] && + STATUS_OK "/boot file hashes generated" + + # Collect relative basenames so sha256sum output is path-independent and + # matches what check_config produces when verifying (also uses cd+relative). + param_files=() + for f in /boot/kexec*.txt; do + [ -e "$f" ] || continue + param_files+=("$(basename "$f")") + done + [ ${#param_files[@]} -eq 0 ] && whiptail_error_die "No kexec parameter files to sign" if [ "$GPG_GEN_KEY_IN_MEMORY" = "y" -a "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "n" ]; then @@ -776,19 +862,21 @@ generate_checksums() { USER_PIN=$ADMIN_PIN fi - DEBUG "Detach-signing boot files under kexec.sig: ${param_files}" + DEBUG "oem-factory-reset.sh: ${#param_files[@]} file(s) to sign (relative): ${param_files[*]}" - if sha256sum $param_files 2>/dev/null | gpg --detach-sign \ + if (cd /boot && sha256sum "${param_files[@]}") 2>/dev/null | gpg --detach-sign \ --pinentry-mode loopback \ --passphrase-file <(echo -n "$USER_PIN") \ --digest-algo SHA256 \ -a \ >/boot/kexec.sig 2>/tmp/error; then + DEBUG "oem-factory-reset.sh: signing succeeded, running check_config /boot" # successful - update the validated params if ! check_config /boot >/dev/null 2>/tmp/error; then cat /tmp/error ret=1 else + STATUS_OK "/boot files signed and verified" ret=0 fi else @@ -818,14 +906,14 @@ set_default_boot_option() { rm $option_file 2>/dev/null # parse boot options from grub.cfg for i in $(find /boot -name "grub.cfg"); do - kexec-parse-boot "/boot" "$i" >>$option_file + kexec-parse-boot.sh "/boot" "$i" >>$option_file done # FC29/30+ may use BLS format grub config files # https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault # only parse these if $option_file is still empty if [ ! -s $option_file ] && [ -d "/boot/loader/entries" ]; then for i in $(find /boot -name "grub.cfg"); do - kexec-parse-bls "/boot" "$i" "/boot/loader/entries" >>$option_file + kexec-parse-bls.sh "/boot" "$i" "/boot/loader/entries" >>$option_file done fi [ ! -s $option_file ] && @@ -854,89 +942,25 @@ set_default_boot_option() { TRACE_FUNC } -report_integrity_measurements() { - TRACE_FUNC - - #check for GPG key in keyring - GPG_KEY_COUNT=$(gpg -k 2>/dev/null | wc -l) - if [ "$GPG_KEY_COUNT" -ne 0 ]; then - # Check and report TOTP - # update the TOTP code every thirty seconds - date=$(date "+%Y-%m-%d %H:%M:%S %Z") - seconds=$(date "+%s") - half=$(expr \( "$seconds" % 60 \) / 30) - if [ "$CONFIG_TPM" != "y" ]; then - TOTP="NO TPM" - elif [ "$half" != "$last_half" ]; then - last_half=$half - TOTP=$(unseal-totp) >/dev/null 2>&1 - fi - - # Check and report on HOTP status - if [ -x /bin/hotp_verification ]; then - HOTP="Unverified" - enable_usb - for attempt in 1 2 3; do - if ! hotp_verification info >/dev/null 2>&1; then - whiptail_warning --title "WARNING: Please insert your HOTP enabled USB Security dongle (Attempt $attempt/3)" --msgbox "Your HOTP enabled USB Security dongle was not detected.\n\nPlease remove it and insert it again." 0 80 - else - break - fi - done - - if [ $attempt -eq 3 ]; then - die "No HOTP enabled USB Security dongle detected. Please disable 'CONFIG_HOTPKEY' in the board config and rebuild." - fi - - # Don't output HOTP codes to screen, so as to make replay attacks harder - HOTP=$(unseal-hotp) >/dev/null 2>&1 - hotp_verification check $HOTP - case "$?" in - 0) - HOTP="Success" - ;; - 4) - HOTP="Invalid code" - BG_COLOR_MAIN_MENU="error" - ;; - *) - HOTP="Error checking code, Insert USB Security dongle and retry" - BG_COLOR_MAIN_MENU="warning" - ;; - esac - else - HOTP='N/A' - fi - # Check for detached signed digest and report on /boot integrity status - check_config /boot force - TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt" - - if (cd /boot && sha256sum -c "$TMP_HASH_FILE" >/tmp/hash_output); then - HASH="OK" - else - HASH="ALTERED" - fi - - #Show results - whiptail_type $BG_COLOR_MAIN_MENU --title "Measured Integrity Report" --msgbox "$date\nTOTP: $TOTP | HOTP: $HOTP\n/BOOT INTEGRITY: $HASH\n\nPress OK to continue or Ctrl+Alt+Delete to reboot" 0 80 - fi - - TRACE_FUNC -} - usb_security_token_capabilities_check() { TRACE_FUNC - echo -e "\nChecking for USB Security dongle...\n" - enable_usb + + # Always detect dongle branding from USB VID:PID — never read a stored file. + DONGLE_BRAND="$(detect_usb_security_dongle_branding)" + export DONGLE_BRAND + DEBUG "USB Security dongle detected: $DONGLE_BRAND" + INFO "Detected $DONGLE_BRAND" + STATUS "Checking $DONGLE_BRAND capabilities" + # ... first set board config preference if [ -n "$CONFIG_GPG_ALGO" ]; then GPG_ALGO=$CONFIG_GPG_ALGO DEBUG "Setting GPG_ALGO to (board-)configured: $CONFIG_GPG_ALGO" fi # ... overwrite with usb-token capability - if lsusb | grep -q "20a0:42b2"; then + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then GPG_ALGO="p256" DEBUG "Nitrokey 3 detected: Setting GPG_ALGO to: $GPG_ALGO" fi @@ -958,14 +982,14 @@ fi # show warning prompt if [ "$CONFIG_TPM" = "y" ]; then - TPM_STR=" * ERASE the TPM and own it with a password\n" + TPM_STR=" * ERASE the TPM and own it with a passphrase\n" else TPM_STR="" fi if ! whiptail_warning --yesno " This operation will automatically:\n $TPM_STR - * ERASE any keys or passwords on the GPG smart card,\n + * ERASE any keys or PINs on the GPG smart card,\n reset it to a factory state, generate new keys\n and optionally set custom PIN(s)\n * Add the new GPG key to the firmware and reflash it\n @@ -978,72 +1002,70 @@ fi #Make sure /boot is mounted if board config defines default mount_boot -# We show current integrity measurements status and time -report_integrity_measurements +# Show integrity report only when prior Heads trust metadata exists and it +# has not already been shown to the user (e.g. when called from the report menu). +if [ "${INTEGRITY_REPORT_ALREADY_SHOWN:-0}" = "1" ]; then + DEBUG "Skipping integrity report in OEM Factory Reset: already shown to user before this call" +elif has_prior_boot_trust_metadata /boot/kexec_rollback.txt; then + report_integrity_measurements +else + DEBUG "Skipping integrity report in OEM Factory Reset: no prior /boot trust metadata detected (fresh first-ownership path)" +fi # Clear the screen clear #Prompt user for use of default configuration options TRACE_FUNC -echo -e -n "Would you like to use default configuration options?\nIf N, you will be prompted for each option [Y/n]: " -read -n 1 use_defaults +INPUT "Would you like to use default configuration options? If N, you will be prompted for each option [Y/n]:" -n 1 use_defaults if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then #Give general guidance to user on how to answer prompts - echo - echo "****************************************************" - echo "**** Factory Reset / Re-Ownership Questionnaire ****" - echo "****************************************************" - echo "The following questionnaire will help you configure the security components of your system." - echo "Each prompt requires a single letter answer: eg. (Y/n)." - echo -e "If you don't know what to answer, pressing Enter will select the default answer for that prompt: eg. Y, above.\n" + STATUS "Factory Reset / Re-Ownership Questionnaire" + INFO "The following questionnaire will help you configure the security components of your system" + INFO "Each prompt requires a single letter answer (Y/n)" + INFO "Pressing Enter selects the default answer for each prompt" + TRACE_FUNC + DEBUG "Showing passphrase guidance: QR code from diceware.dmuth.org" + qrenc "https://diceware.dmuth.org/" + NOTE "Scan the QR code above for passphrase guidance (diceware.dmuth.org):" + NOTE "Recommended lengths: Disk Recovery Key: 6 words TPM Owner: 2 words GPG User PIN: 2 words" # Re-ownership of LUKS encrypted Disk: key, content and passphrase - echo -e -n "\n\nWould you like to change the current LUKS Disk Recovery Key passphrase?\n (Highly recommended if you didn't install the Operating System yourself, so that past configured passphrase would not permit to access content.\n Note that without re-encrypting disk, a backed up header could be restored to access encrypted content with old passphrase) [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to change the current LUKS Disk Recovery Key passphrase? (Highly recommended if you didn't install the OS yourself) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then luks_new_Disk_Recovery_Key_passphrase_desired=1 - echo -e "\n" fi - echo -e -n "Would you like to re-encrypt LUKS encrypted container and generate new LUKS Disk Recovery Key?\n (Highly recommended if you didn't install the operating system yourself: this would prevent any LUKS backed up header to be restored to access encrypted data) [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to re-encrypt LUKS container and generate new LUKS Disk Recovery Key? (Highly recommended if you didn't install the OS yourself) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then TRACE_FUNC test_luks_current_disk_recovery_key_passphrase luks_new_Disk_Recovery_Key_desired=1 - echo -e "\n" fi #Prompt to ask if user wants to generate GPG key material in memory or on smartcard - echo -e -n "Would you like to format an encrypted USB Thumb drive to store GPG key material?\n (Required to enable GPG authentication) [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to format an encrypted USB Thumb drive to store GPG key material? (Required to enable GPG authentication) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ] \ ; then GPG_GEN_KEY_IN_MEMORY="y" - echo " ++++ Master key and subkeys will be generated in memory, backed up to dedicated LUKS container +++" - echo -e -n "Would you like in-memory generated subkeys to be copied to USB Security dongle's OpenPGP smartcard?\n (Highly recommended so the smartcard is used on daily basis and backup is kept safe, but not required) [Y/n]: " - read -n 1 prompt_output - echo + INFO "Master key and subkeys will be generated in memory and backed up to a dedicated LUKS container" + INPUT "Would you like in-memory generated subkeys to be copied to $DONGLE_BRAND's OpenPGP smartcard? (Highly recommended) [Y/n]:" -n 1 prompt_output if [ "$prompt_output" == "n" \ -o "$prompt_output" == "N" ]; then - warn "Subkeys will NOT be copied to USB Security dongle's OpenPGP smartcard" - warn "Your GPG key material backup thumb drive should be cloned to a second thumb drive for redundancy for production environements" + NOTE "Subkeys will NOT be copied to $DONGLE_BRAND's OpenPGP smartcard" + NOTE "Your GPG key material backup thumb drive should be cloned to a second thumb drive for redundancy for production environments" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" else - echo "++++ Subkeys will be copied to USB Security dongle's OpenPGP smartcard ++++" - warn "Please keep your GPG key material backup thumb drive safe" + INFO "Subkeys will be copied to $DONGLE_BRAND's OpenPGP smartcard" + NOTE "Please keep your GPG key material backup thumb drive safe" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="y" fi else - echo "GPG key material will be generated on USB Security dongle's OpenPGP smartcard without backup" + INFO "GPG key material will be generated on $DONGLE_BRAND's OpenPGP smartcard without backup" GPG_GEN_KEY_IN_MEMORY="n" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" fi @@ -1056,7 +1078,7 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then CUSTOM_PASS_AFFECTED_COMPONENTS+="LUKS Disk Recovery Key passphrase\n" fi if [ "$CONFIG_TPM" = "y" ]; then - CUSTOM_PASS_AFFECTED_COMPONENTS+="TPM Owner Password\n" + CUSTOM_PASS_AFFECTED_COMPONENTS+="TPM Owner Passphrase\n" fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "y" ]; then CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Key material backup passphrase (Same as GPG Admin PIN)\n" @@ -1068,22 +1090,16 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then fi # Inform user of security components affected for the following prompts - echo - echo -e "The following Security Components will be configured with defaults or further chosen PINs/passwords: - $CUSTOM_PASS_AFFECTED_COMPONENTS\n" - - # Prompt to change default passwords - echo -e -n "Would you like to set a single custom password to all previously stated security components? [y/N]: " - read -n 1 prompt_output - echo + INFO "The following Security Components will be configured with defaults or further chosen PINs/passphrases: $CUSTOM_PASS_AFFECTED_COMPONENTS" + + # Prompt to change default passphrases + INPUT "Would you like to set a single custom passphrase to all previously stated security components? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then - echo -e "\nThe chosen custom password must be between 8 and $MAX_HOTP_GPG_PIN_LENGTH characters in length." + INFO "The chosen passphrase must be between 8 and $MAX_HOTP_GPG_PIN_LENGTH characters in length." while [[ ${#CUSTOM_SINGLE_PASS} -lt 8 ]] || [[ ${#CUSTOM_SINGLE_PASS} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - echo -e -n "Enter the custom password: " - read CUSTOM_SINGLE_PASS + INPUT "Enter the passphrase (8-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r CUSTOM_SINGLE_PASS done - echo TPM_PASS=${CUSTOM_SINGLE_PASS} USER_PIN=${CUSTOM_SINGLE_PASS} ADMIN_PIN=${CUSTOM_SINGLE_PASS} @@ -1093,40 +1109,34 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then luks_new_Disk_Recovery_Key_passphrase=${CUSTOM_SINGLE_PASS} fi - # The user knows this password, we don't need to badger them to + # The user knows this passphrase, we don't need to badger them to # record it MAKE_USER_RECORD_PASSPHRASES= else - echo -e -n "Would you like to set distinct PINs/passwords to configure previously stated security components? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to set distinct PINs/passphrases to configure previously stated security components? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then - echo -e "\nThe TPM Owner Password and Admin PIN must be at least 8, the User PIN at least 6 characters in length.\n" - echo + INFO "TPM Owner Passphrase and GPG Admin PIN must be at least 8 chars, GPG User PIN at least 6 chars." if [ "$CONFIG_TPM" = "y" ]; then + NOTE "TPM Owner Passphrase: sets TPM ownership. Recommended: 2 words" while [[ ${#TPM_PASS} -lt 8 ]]; do - echo -e -n "Enter desired TPM Owner Password: " - read TPM_PASS + INPUT "Enter desired TPM Owner Passphrase (min 8 chars):" -r TPM_PASS done fi + NOTE "GPG Admin PIN: management tasks on USB Security dongle, seal measurements under HOTP. 3 attempts max, locks Admin out. DO NOT FORGET. Recommended: 2 words" while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - echo -e -n "\nThis PIN should be between 6 to $MAX_HOTP_GPG_PIN_LENGTH characters in length.\n" - echo -e -n "Enter desired GPG Admin PIN: " - read ADMIN_PIN + INPUT "Enter desired GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r ADMIN_PIN done #USER PIN not required in case of GPG_GEN_KEY_IN_MEMORY not requested of if GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD is # That is, if keys were NOT generated in memory (on smartcard only) or # if keys were generated in memory but are to be moved from local keyring to smartcard if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then + NOTE "GPG User PIN: sign/encrypt content, sign hashes under Heads. 3 attempts max. DO NOT FORGET. Recommended: 2 words" while [[ ${#USER_PIN} -lt 6 ]] || [[ ${#USER_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - echo -e -n "\nThis PIN should be between 6 to $MAX_HOTP_GPG_PIN_LENGTH characters in length.\n" - echo -e -n "Enter desired GPG User PIN: " - read USER_PIN + INPUT "Enter desired GPG User PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r USER_PIN done fi - echo - # The user knows these passwords, we don't need to + # The user knows these passphrases, we don't need to # badger them to record them MAKE_USER_RECORD_PASSPHRASES= fi @@ -1134,48 +1144,35 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then if [ -n "$luks_new_Disk_Recovery_Key_passphrase_desired" -a -z "$luks_new_Disk_Recovery_Key_passphrase" ]; then # We catch here if changing LUKS Disk Recovery Key passphrase was desired - # but yet undone. This is if not being covered by the single password - echo -e "\nEnter desired replacement for current LUKS Disk Recovery Key passphrase (At least 8 characters long):" + # but yet undone. This is if not being covered by the single passphrase + NOTE "Disk Recovery Key Passphrase: required to unlock disk, setup TPM Disk Unlock Key, access data from any computer, unsafe boot. DO NOT FORGET. Recommended: 6 words" while [[ ${#luks_new_Disk_Recovery_Key_passphrase} -lt 8 ]]; do - { - read -r luks_new_Disk_Recovery_Key_passphrase - } + INPUT "Enter desired replacement for current LUKS Disk Recovery Key passphrase (min 8 chars):" -r luks_new_Disk_Recovery_Key_passphrase done #We test that current LUKS Disk Recovery Key passphrase is known prior of going further TRACE_FUNC test_luks_current_disk_recovery_key_passphrase - echo -e "\n" fi # Prompt to change default GnuPG key information - echo -e -n "Would you like to set custom user information for the GnuPG key? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to set custom user information for the GnuPG key? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then - echo -e "\n\n" - echo -e "We will generate a GnuPG (PGP) keypair identifiable with the following text form:" - echo -e "Real Name (Comment) email@address.org" + INFO "We will generate a GnuPG (PGP) keypair identifiable as: Real Name (Comment) email@address.org" - echo -e "\nEnter your Real Name (Optional):" - read -r GPG_USER_NAME + INPUT "Enter your Real Name (optional):" -r GPG_USER_NAME - echo -e "\nEnter your email@adress.org:" - read -r GPG_USER_MAIL + INPUT "Enter your email@address.org:" -r GPG_USER_MAIL while ! $(expr "$GPG_USER_MAIL" : '.*@' >/dev/null); do - { - echo -e "\nEnter your email@address.org:" - read -r GPG_USER_MAIL - } + INPUT "Invalid email - enter your email@address.org:" -r GPG_USER_MAIL done - echo -e "\nEnter Comment (Required: Use this to distinguish this key from others, e.g., its purpose or usage context. Must be 1-60 characters):" while true; do - read -r GPG_USER_COMMENT + INPUT "Enter Comment (1-60 chars, distinguishes this key, e.g. its purpose):" -r GPG_USER_COMMENT if [[ ${#GPG_USER_COMMENT} -ge 1 && ${#GPG_USER_COMMENT} -le 60 ]]; then break fi - echo -e "\nComment must be 1-60 characters long. Please try again:" + WARN "Comment must be 1-60 characters long. Please try again." done fi @@ -1193,9 +1190,7 @@ if [ "$ADMIN_PIN" == "" ]; then ADMIN_PIN=${ADMIN_PIN_DEF}; fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" ]; then # Prompt to insert USB drive if desired - echo -e -n "\nWould you like to export your public key to an USB drive? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Would you like to export your public key to a USB drive? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ] \ ; then @@ -1203,7 +1198,7 @@ if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" ]; then # mount USB over /media only if not already mounted if ! grep -q /media /proc/mounts; then # mount USB in rw - if ! mount-usb --mode rw 2>/tmp/error; then + if ! mount-usb.sh --mode rw 2>/tmp/error; then ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error_die "Unable to mount USB on /media:\n\n${ERROR}" fi @@ -1216,7 +1211,7 @@ if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" ]; then fi else GPG_EXPORT=0 - # needed for USB Security dongle below and is ensured via mount-usb in case of GPG_EXPORT=1 + # needed for USB Security dongle below and is ensured via mount-usb.sh in case of GPG_EXPORT=1 enable_usb fi fi @@ -1246,11 +1241,11 @@ killall gpg-agent >/dev/null 2>&1 || true rm -rf /.gnupg/*.kbx /.gnupg/*.gpg >/dev/null 2>&1 || true # detect and set /boot device -echo -e "\nDetecting and setting boot device...\n" +STATUS "Detecting and setting boot device" if ! detect_boot_device; then SKIP_BOOT="y" else - echo -e "Boot device set to $CONFIG_BOOT_DEV\n" + STATUS "Boot device set to $CONFIG_BOOT_DEV" fi # update configs @@ -1271,9 +1266,9 @@ elif [ -z "$luks_new_Disk_Recovery_Key_desired" -a -n "$luks_new_Disk_Recovery_K luks_change_passphrase fi -## reset TPM and set password +## reset TPM and set passphrase if [ "$CONFIG_TPM" = "y" ]; then - echo -e "\nResetting TPM...\n" + STATUS "Resetting TPM" tpmr.sh reset "$TPM_PASS" >/dev/null 2>/tmp/error fi if [ $? -ne 0 ]; then @@ -1297,7 +1292,7 @@ if [ "$GPG_GEN_KEY_IN_MEMORY" = "y" ]; then elif [ "$GPG_ALGO" == "p256" ]; then generate_inmemory_p256_master_and_subkeys else - die "Unsupported GPG_ALGO: $GPG_ALGO" + DIE "Unsupported GPG_ALGO: $GPG_ALGO" fi wipe_thumb_drive_and_copy_gpg_key_material "$thumb_drive" "$thumb_drive_luks_percent" set_user_config "CONFIG_HAVE_GPG_KEY_BACKUP" "y" @@ -1310,13 +1305,21 @@ else #Reset Nitrokey 3 secret app reset_nk3_secret_app #Generate GPG key and subkeys on smartcard only - echo -e "\nResetting USB Security dongle's OpenPGP smartcard with GPG...\n(this may take up to 3 minutes...)\n" + STATUS "Resetting USB Security dongle OpenPGP smartcard with GPG" + if [ "$GPG_ALGO" = "RSA" ]; then + NOTE "RSA key generation on $DONGLE_BRAND may take 10 or more minutes - please be patient" + fi gpg_key_factory_reset generate_OEM_gpg_keys fi -# Obtain GPG key ID -GPG_GEN_KEY=$(gpg --list-keys --with-colons | grep "^fpr" | cut -d: -f10 | head -n1) +# Set identity fields on the OpenPGP smartcard from collected identity info +if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" ] || [ "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then + set_card_identity +fi + +# Obtain GPG key ID without printing trustdb maintenance chatter to console +GPG_GEN_KEY=$(gpg --list-keys --with-colons 2>/dev/null | grep "^fpr" | cut -d: -f10 | head -n1) #Where to export the public key PUBKEY="/tmp/${GPG_GEN_KEY}.asc" @@ -1330,16 +1333,18 @@ fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then #Only apply smartcard PIN change if smartcard only or if keytocard op is expected next if [ "${USER_PIN}" != "${USER_PIN_DEF}" -o "${ADMIN_PIN}" != "${ADMIN_PIN_DEF}" ]; then - echo -e "\nChanging default GPG Admin PIN\n" + STATUS "Changing default GPG Admin PIN" gpg_key_change_pin "3" "${ADMIN_PIN_DEF}" "${ADMIN_PIN}" - echo -e "\nChanging default GPG User PIN\n" + STATUS_OK "GPG Admin PIN changed" + STATUS "Changing default GPG User PIN" gpg_key_change_pin "1" "${USER_PIN_DEF}" "${USER_PIN}" + STATUS_OK "GPG User PIN changed" fi fi ## export pubkey to USB if [ "$GPG_EXPORT" != "0" ]; then - echo -e "\nExporting generated key to USB...\n" + STATUS "Exporting generated key to USB" # copy to USB if ! cp "${PUBKEY}" "/media/${GPG_GEN_KEY}.asc" 2>/tmp/error; then ERROR=$(tail -n 1 /tmp/error | fold -s) @@ -1368,18 +1373,18 @@ fi # Do not attempt to flash the key to ROM if we are running in QEMU based on CONFIG_BOARD_NAME matching glob pattern containing qemu-* # We check for qemu-* instead of ^qemu- because CONFIG_BOARD_NAME could be renamed to UNTESTED-qemu-* in a probable future if [[ "$CONFIG_BOARD_NAME" == qemu-* ]]; then - warn "Skipping flash of GPG key to ROM because we are running in QEMU without internal flashing support." - warn "Please review boards/qemu*/qemu*.md documentation to extract public key from raw disk and inject at build time" - warn "Also review boards/qemu*/qemu*.config to tweak CONFIG_* options you might need to turn on/off manually at build time" + WARN "Skipping flash of GPG key to ROM because we are running in QEMU without internal flashing support." + WARN "Please review boards/qemu*/qemu*.md documentation to extract public key from raw disk and inject at build time" + WARN "Also review boards/qemu*/qemu*.config to tweak CONFIG_* options you might need to turn on/off manually at build time" else #We are not running in QEMU, so flash the key to ROM ## flash generated key to ROM # read current firmware; show all output and capture stderr for errors if echo "$CONFIG_FLASH_OPTIONS" | grep -q -- '--progress'; then - echo -e "\nReading current firmware (progress shown below)...\n" + STATUS "Reading current firmware (progress shown below)..." else - echo -e "\nReading current firmware...\n(this may take up to two minutes...)\n" + STATUS "Reading current firmware... (this may take up to two minutes)" fi if ! /bin/flash.sh -r /tmp/oem-setup.rom 2> >(tee /tmp/error >&2); then ERROR=$(tail -n 1 /tmp/error | fold -s) @@ -1414,7 +1419,7 @@ else fi # flash updated firmware image - echo -e "\nAdding generated key to current firmware and re-flashing...\n" + STATUS "Adding generated key to firmware and re-flashing" if ! /bin/flash.sh /tmp/oem-setup.rom 2>/tmp/error; then ERROR=$(tail -n 1 /tmp/error | fold -s) whiptail_error_die "Error flashing updated firmware image:\n\n$ERROR" @@ -1423,7 +1428,7 @@ fi ## sign files in /boot and generate checksums if [[ "$SKIP_BOOT" == "n" ]]; then - echo -e "\nUpdating checksums and signing all files in /boot...\n" + STATUS "Updating checksums and signing all files in /boot" generate_checksums fi @@ -1436,11 +1441,11 @@ if [ -n "$luks_new_Disk_Recovery_Key_passphrase" -o -n "$luks_new_Disk_Recovery_ fi if [ "$CONFIG_TPM" = "y" ]; then - passphrases+="TPM Owner Password: ${TPM_PASS}\n" + passphrases+="TPM Owner Passphrase: ${TPM_PASS}\n" fi -#if nk3 detected, we add the NK3 Secre App PIN. Detect by product ID -if lsusb | grep -q "20a0:42b2"; then +#if nk3 detected, we add the NK3 Secrets App PIN +if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then passphrases+="Nitrokey 3 Secrets app PIN: ${ADMIN_PIN}\n" fi @@ -1458,36 +1463,49 @@ fi # Show configured secrets in whiptail and loop until user confirms qr code was scanned while true; do - whiptail --msgbox "$(echo -e "$passphrases" | fold -w $((WIDTH - 5)))" \ + whiptail_type $BG_COLOR_MAIN_MENU --msgbox "$(echo -e "$passphrases" | fold -w $((WIDTH - 5)))" \ $HEIGHT $WIDTH --title "Configured secrets" if [ "$MAKE_USER_RECORD_PASSPHRASES" != y ]; then - # Passwords were user-supplied or not complex, we do not need to + # Passphrases were user-supplied or not complex, we do not need to # badger the user to record them break fi #Tell user to scan the QR code containing all configured secrets - echo -e "\nScan the QR code below to save the secrets to a secure location" + STATUS "Scan the QR code below to save the secrets to a secure location" qrenc "$(echo -e "$passphrases")" # Prompt user to confirm scanning of qrcode on console prompt not whiptail: y/n - echo -e -n "Please confirm you have scanned the QR code above and/or written down the secrets? [y/N]: " - read -n 1 prompt_output - echo + INPUT "Please confirm you have scanned the QR code above and/or written down the secrets? [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" -o "$prompt_output" == "Y" ]; then break fi done ## all done -- reboot -whiptail --msgbox " - OEM Factory Reset / Re-Ownership has completed successfully\n\n - After rebooting, you will need to generate new TOTP/HOTP secrets\n - when prompted in order to complete the setup process.\n\n - Press Enter to reboot.\n" \ +if [ "${CONFIG_TPM_DISK_UNLOCK_KEY:-n}" = "y" ]; then + boot_next_steps="Then open: Options -> Boot Options -> Show OS boot menu +and set a new default boot option. +This step also configures/reseals the TPM Disk Unlock Key (DUK). +" +else + boot_next_steps="Then open: Options -> Boot Options -> Show OS boot menu +and set a new default boot option. +" +fi + +completion_msg="OEM Factory Reset / Re-Ownership has completed successfully + +After rebooting, you will need to generate new TOTP/HOTP secrets +when prompted in order to complete the setup process. + +${boot_next_steps} +Press Enter to reboot." + +whiptail --msgbox "${completion_msg}" \ $HEIGHT $WIDTH --title "OEM Factory Reset / Re-Ownership Complete" # Clean LUKS secrets luks_secrets_cleanup unset luks_passphrase_changed -unset tpm_owner_password_changed +unset tpm_owner_passphrase_changed -reboot +reboot.sh diff --git a/initrd/bin/qubes-measure-luks.sh b/initrd/bin/qubes-measure-luks.sh index 6d3b3c2bb..7e2e53f46 100755 --- a/initrd/bin/qubes-measure-luks.sh +++ b/initrd/bin/qubes-measure-luks.sh @@ -4,22 +4,22 @@ . /etc/functions.sh TRACE_FUNC -DEBUG "Arguments passed to qubes-measure-luks: $@" +DEBUG "Arguments passed to qubes-measure-luks.sh: $@" # Measure the LUKS headers into PCR 6 for dev in "$@"; do DEBUG "Storing LUKS header for $dev into /tmp/lukshdr-$(echo "$dev" | sed 's/\//_/g')" cryptsetup luksHeaderBackup $dev \ --header-backup-file /tmp/lukshdr-$(echo "$dev" | sed 's/\//_/g') || - die "$dev: Unable to read LUKS header" + DIE "$dev: Unable to read LUKS header" done DEBUG "Hashing LUKS headers into /tmp/luksDump.txt" -sha256sum /tmp/lukshdr-* >/tmp/luksDump.txt || die "Unable to hash LUKS headers" +sha256sum /tmp/lukshdr-* >/tmp/luksDump.txt || DIE "Unable to hash LUKS headers" DEBUG "Removing /tmp/lukshdr-*" rm /tmp/lukshdr-* TRACE_FUNC INFO "TPM: Extending PCR[6] with hash of LUKS headers from /tmp/luksDump.txt" tpmr.sh extend -ix 6 -if /tmp/luksDump.txt || - die "Unable to extend PCR" + DIE "Unable to extend PCR" diff --git a/initrd/bin/reboot.sh b/initrd/bin/reboot.sh index 0f576b22d..c5d616d3d 100755 --- a/initrd/bin/reboot.sh +++ b/initrd/bin/reboot.sh @@ -17,13 +17,25 @@ echo u > /proc/sysrq-trigger # 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 - read -r -n 1 -s -p "Press any key to continue reboot or 'r' to go to recovery shell: " REPLY - echo + INPUT "Press any key to continue reboot or 'r' to go to recovery shell:" -r -n 1 -s REPLY if [ "$REPLY" = "r" ] || [ "$REPLY" = "R" ]; then recovery "Reboot call bypassed to go into recovery shell to debug" fi - DEBUG "DEBUG: TPM shutdown and filesystem operations complete" - read -r -p "Press Enter to issue final reboot syscall: " + DEBUG "TPM shutdown and filesystem operations complete" + INPUT "Press Enter to issue final reboot syscall:" +fi + +# On qemu-* boards, reboot is broken (q35 bug) - use poweroff instead. +# TODO: revisit when qemu q35 reboot is fixed upstream. +# Always offer a recovery shell first so state can be inspected. +if [[ "$CONFIG_BOARD_NAME" == qemu-* ]]; then + _reboot_choice="" + INPUT "QEMU board - press Enter to poweroff, or 'r' to open a recovery shell first:" -r -n 1 -s _reboot_choice + if [ "$_reboot_choice" = "r" ] || [ "$_reboot_choice" = "R" ]; then + recovery "Entering recovery shell before poweroff (QEMU board)" + fi + poweroff.sh + exit fi # Use busybox reboot explicitly (symlinks removed to avoid conflicts) diff --git a/initrd/bin/root-hashes-gui.sh b/initrd/bin/root-hashes-gui.sh index 9871ee6c9..4d4bb7fa0 100755 --- a/initrd/bin/root-hashes-gui.sh +++ b/initrd/bin/root-hashes-gui.sh @@ -15,267 +15,270 @@ ROOT_SUPPORTED_LAYOUT_MSG="Filesystem support in this build:\n- ext4 (ext2/ext3 export CONFIG_ROOT_DIRLIST_PRETTY=$(echo $CONFIG_ROOT_DIRLIST | sed -e 's/^/\//;s/ / \//g') show_unsupported_root_layout_and_die() { - local ACTION="$1" + local ACTION="$1" - whiptail_error --title 'ERROR: Unsupported Root Layout' \ - --msgbox "$ROOT_DETECT_UNSUPPORTED_REASON\n\n$ROOT_SUPPORTED_LAYOUT_MSG\n\nTry a supported root layout,\nor do not use root hashing,\nthen rerun $ACTION." 0 80 - die "$ROOT_DETECT_UNSUPPORTED_REASON" + whiptail_error --title 'ERROR: Unsupported Root Layout' \ + --msgbox "$ROOT_DETECT_UNSUPPORTED_REASON\n\n$ROOT_SUPPORTED_LAYOUT_MSG\n\nTry a supported root layout,\nor do not use root hashing,\nthen rerun $ACTION." 0 80 + DIE "$ROOT_DETECT_UNSUPPORTED_REASON" } update_root_checksums() { - TRACE_FUNC - if ! detect_root_device; then - if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then - show_unsupported_root_layout_and_die "root hash update" - fi - whiptail_error --title 'ERROR: No Valid Root Disk Found' \ - --msgbox "No Valid Root Disk Found" 0 80 - die "No Valid Root Disk Found" - fi - - # mount /boot RW - if ! grep -q /boot /proc/mounts ; then - if ! mount -o rw /boot; then - unmount_root_device - whiptail_error --title 'ERROR: Unable to mount /boot' \ - --msgbox "Unable to mount /boot" 0 80 - die "Unable to mount /boot" - fi - else - mount -o rw,remount /boot - fi - - DEBUG "calculating hashes for $CONFIG_ROOT_DIRLIST_PRETTY on $ROOT_MOUNT" - echo "+++ Calculating hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY " - # Intentional wordsplit - # shellcheck disable=SC2086 - (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' -print0 | xargs -0 sha256sum) >"${HASH_FILE}" - - # switch back to ro mode - mount -o ro,remount /boot - - update_checksums - - whiptail --title 'Root Hashes Updated and Signed' \ - --msgbox "All files in:\n$CONFIG_ROOT_DIRLIST_PRETTY\nhave been hashed and signed successfully" 0 80 - - unmount_root_device + TRACE_FUNC + if ! detect_root_device; then + if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then + show_unsupported_root_layout_and_die "root hash update" + fi + whiptail_error --title 'ERROR: No Valid Root Disk Found' \ + --msgbox "No Valid Root Disk Found" 0 80 + DIE "No Valid Root Disk Found" + fi + + # mount /boot RW + if ! grep -q /boot /proc/mounts; then + if ! mount -o rw /boot; then + unmount_root_device + whiptail_error --title 'ERROR: Unable to mount /boot' \ + --msgbox "Unable to mount /boot" 0 80 + DIE "Unable to mount /boot" + fi + else + mount -o rw,remount /boot + fi + + DEBUG "calculating hashes for $CONFIG_ROOT_DIRLIST_PRETTY on $ROOT_MOUNT" + STATUS "Calculating hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY" + # Intentional wordsplit + # shellcheck disable=SC2086 + (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' -print0 | xargs -0 sha256sum) >"${HASH_FILE}" + + # switch back to ro mode + mount -o ro,remount /boot + + update_checksums + + whiptail_type $BG_COLOR_MAIN_MENU --title 'Root Hashes Updated and Signed' \ + --msgbox "All files in:\n$CONFIG_ROOT_DIRLIST_PRETTY\nhave been hashed and signed successfully" 0 80 + + unmount_root_device } check_root_checksums() { - TRACE_FUNC - DEBUG "verifying existing hash file for $CONFIG_ROOT_DIRLIST_PRETTY" - if ! detect_root_device; then - if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then - show_unsupported_root_layout_and_die "root hash verification" - fi - whiptail_error --title 'ERROR: No Valid Root Disk Found' \ - --msgbox "No Valid Root Disk Found" 0 80 - die "No Valid Root Disk Found" - fi - - # mount /boot RO - if ! grep -q /boot /proc/mounts ; then - if ! mount -o ro /boot; then - unmount_root_device - whiptail_error --title 'ERROR: Unable to mount /boot' \ - --msgbox "Unable to mount /boot" 0 80 - die "Unable to mount /boot" - fi - fi - - # check that root hash file exists - if [ ! -f ${HASH_FILE} ]; then - if (whiptail_warning --title 'WARNING: No Root Hash File Found' \ - --yesno "\nIf you just enabled root hash checking feature, + TRACE_FUNC + DEBUG "verifying existing hash file for $CONFIG_ROOT_DIRLIST_PRETTY" + if ! detect_root_device; then + if [ -n "$ROOT_DETECT_UNSUPPORTED_REASON" ]; then + show_unsupported_root_layout_and_die "root hash verification" + fi + whiptail_error --title 'ERROR: No Valid Root Disk Found' \ + --msgbox "No Valid Root Disk Found" 0 80 + DIE "No Valid Root Disk Found" + fi + + # mount /boot RO + if ! grep -q /boot /proc/mounts; then + if ! mount -o ro /boot; then + unmount_root_device + whiptail_error --title 'ERROR: Unable to mount /boot' \ + --msgbox "Unable to mount /boot" 0 80 + DIE "Unable to mount /boot" + fi + fi + + # check that root hash file exists + if [ ! -f ${HASH_FILE} ]; then + if (whiptail_warning --title 'WARNING: No Root Hash File Found' \ + --yesno "\nIf you just enabled root hash checking feature, \nthen you need to create the initial hash file. \nOtherwise, This could be caused by tampering. \n - \nWould you like to create the hash file now?" 0 80) then - update_root_checksums - return 0 - else - DEBUG "Root hash file not created (user declined)" - exit 1 - fi - fi - - echo "+++ Checking root hash file signature " - if ! sha256sum `find /boot/kexec*.txt` | gpgv /boot/kexec.sig - > /tmp/hash_output; then - ERROR=`cat /tmp/hash_output` - whiptail_error --title 'ERROR: Signature Failure' \ - --msgbox "The signature check on hash files failed:\n${CHANGED_FILES}\nExiting to a recovery shell" 0 80 - unmount_root_device - die 'Invalid signature' - fi - - echo "+++ Checking for new files in $CONFIG_ROOT_DIRLIST_PRETTY " - (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*') | sort > /tmp/new_file_list - cut -d' ' -f3- ${HASH_FILE} | sort | diff -U0 - /tmp/new_file_list > /tmp/new_file_diff || new_files_found=y - if [ "$new_files_found" == "y" ]; then - grep -E -v '^[+-]{3}|[@]{2} ' /tmp/new_file_diff > /tmp/new_file_diff2 # strip any output that's not a file - mv /tmp/new_file_diff2 /tmp/new_file_diff - CHANGED_FILES_COUNT=$(wc -l /tmp/new_file_diff | cut -f1 -d ' ') - whiptail_error --title 'ERROR: Files Added/Removed in Root ' \ - --msgbox "${CHANGED_FILES_COUNT} files were added/removed in root!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return to the menu." 0 80 - - echo "Type \"q\" to exit the list and return to the menu." >> /tmp/new_file_diff - less /tmp/new_file_diff - else - echo "+++ Verified no files added/removed " - fi - - echo "+++ Checking hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY (this might take a while) " - if (cd $ROOT_MOUNT && sha256sum -c ${HASH_FILE} > /tmp/hash_output 2>/dev/null); then - echo "+++ Verified root hashes " - valid_hash='y' - unmount_root_device - - if [ "$new_files_found" == "y" ]; then - if (whiptail --title 'ERROR: New Files Added/Removed in Root' \ - --yesno "New files were added/removed in root. + \nWould you like to create the hash file now?" 0 80); then + update_root_checksums + return 0 + else + DEBUG "Root hash file not created (user declined)" + exit 1 + fi + fi + + STATUS "Checking root hash file signature" + # Use relative filenames (cd /boot) so sha256sum output matches what was + # produced during signing, which also uses relative names from a staging dir. + if ! (cd /boot && sha256sum kexec*.txt) | gpgv.sh /boot/kexec.sig - >/tmp/hash_output; then + ERROR=$(cat /tmp/hash_output) + whiptail_error --title 'ERROR: Signature Failure' \ + --msgbox "The signature check on hash files failed:\n${CHANGED_FILES}\nExiting to a recovery shell" 0 80 + unmount_root_device + DIE 'Invalid signature' + fi + + STATUS "Checking for new files in $CONFIG_ROOT_DIRLIST_PRETTY" + (cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*') | sort >/tmp/new_file_list + cut -d' ' -f3- ${HASH_FILE} | sort | diff -U0 - /tmp/new_file_list >/tmp/new_file_diff || new_files_found=y + if [ "$new_files_found" == "y" ]; then + grep -E -v '^[+-]{3}|[@]{2} ' /tmp/new_file_diff >/tmp/new_file_diff2 # strip any output that's not a file + mv /tmp/new_file_diff2 /tmp/new_file_diff + CHANGED_FILES_COUNT=$(wc -l /tmp/new_file_diff | cut -f1 -d ' ') + whiptail_error --title 'ERROR: Files Added/Removed in Root ' \ + --msgbox "${CHANGED_FILES_COUNT} files were added/removed in root!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return to the menu." 0 80 + + echo "Type \"q\" to exit the list and return to the menu." >>/tmp/new_file_diff + less /tmp/new_file_diff + else + STATUS_OK "Verified no files added or removed" + fi + + STATUS "Checking hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY (this may take a while)" + if (cd $ROOT_MOUNT && sha256sum -c ${HASH_FILE} >/tmp/hash_output 2>/dev/null); then + STATUS_OK "Verified root hashes" + valid_hash='y' + unmount_root_device + + if [ "$new_files_found" == "y" ]; then + if (whiptail --title 'ERROR: New Files Added/Removed in Root' \ + --yesno "New files were added/removed in root. \n \nThis could be caused by tampering or by routine software updates. \n \nIf you just updated the software on your system, then that is likely \nthe cause and you should update your file signatures. \n - \nWould you like to update your signatures now?" 0 80) then - - update_root_checksums - - return 0 - else - DEBUG "Signatures not updated (user declined after new-files warning)" - return 1 - fi - fi - return 0 - else - CHANGED_FILES=$(grep -v 'OK$' /tmp/hash_output | cut -f1 -d ':' | tee -a /tmp/hash_output_mismatches) - CHANGED_FILES_COUNT=$(wc -l /tmp/hash_output_mismatches | cut -f1 -d ' ') - whiptail_error --title 'ERROR: Root Hash Mismatch' \ - --msgbox "${CHANGED_FILES_COUNT} files failed the verification process!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return to the menu." 0 80 - unmount_root_device - - echo "Type \"q\" to exit the list and return to the menu." >> /tmp/hash_output_mismatches - less /tmp/hash_output_mismatches - - #move outdated hash mismatch list - mv /tmp/hash_output_mismatches /tmp/hash_output_mismatch_old - - if (whiptail --title 'ERROR: Root Hash Check Failed' \ - --yesno "The root hash check failed. + \nWould you like to update your signatures now?" 0 80); then + + update_root_checksums + + return 0 + else + DEBUG "Signatures not updated (user declined after new-files warning)" + return 1 + fi + fi + return 0 + else + CHANGED_FILES=$(grep -v 'OK$' /tmp/hash_output | cut -f1 -d ':' | tee -a /tmp/hash_output_mismatches) + CHANGED_FILES_COUNT=$(wc -l /tmp/hash_output_mismatches | cut -f1 -d ' ') + whiptail_error --title 'ERROR: Root Hash Mismatch' \ + --msgbox "${CHANGED_FILES_COUNT} files failed the verification process!\n\nHit OK to review the list of files.\n\nType \"q\" to exit the list and return to the menu." 0 80 + unmount_root_device + + echo "Type \"q\" to exit the list and return to the menu." >>/tmp/hash_output_mismatches + less /tmp/hash_output_mismatches + + #move outdated hash mismatch list + mv /tmp/hash_output_mismatches /tmp/hash_output_mismatch_old + + if (whiptail --title 'ERROR: Root Hash Check Failed' \ + --yesno "The root hash check failed. \n \nThis could be caused by tampering or by routine software updates. \n \nIf you just updated the software on your system, then that is likely \nthe cause and you should update your file signatures. \n - \nWould you like to update your signatures now?" 0 80) then - - update_root_checksums - return 0 - else - DEBUG "Signatures not updated (user declined after hash-check failure)" - return 1 - fi - fi + \nWould you like to update your signatures now?" 0 80); then + + update_root_checksums + return 0 + else + DEBUG "Signatures not updated (user declined after hash-check failure)" + return 1 + fi + fi } # Open an LVM volume group, then continue looking for more layers in the 'root' # logical volume. open_block_device_lvm() { - TRACE_FUNC - local VG="$1" - local LV MAPPER_VG MAPPER_LV name lvpath FIRST_LV_PREFERRED FIRST_LV_FALLBACK - - if ! lvm vgchange -ay "$VG"; then - DEBUG "Can't open LVM VG: $VG" - return 1 - fi - - # Prefer an LV named 'root' (used by Qubes), but fall back to any LV - # in the VG. This ensures Ubuntu-style names (e.g. ubuntu-vg/ubuntu-root) - # also work. - LV="/dev/$VG/root" - if ! [ -e "$LV" ]; then - MAPPER_VG="${VG//-/--}" - LV="/dev/mapper/${MAPPER_VG}-root" - fi - if ! [ -e "$LV" ]; then - FIRST_LV_PREFERRED="" - FIRST_LV_FALLBACK="" - DEBUG "LVM VG $VG has no 'root' LV, enumerating all LVs" - # list LV names and prefer root-like names - for name in $(lvm lvs --noheadings -o lv_name --separator ' ' "$VG" 2>/dev/null); do - # thin pool/metadata and swap-like LVs are not root filesystems - case "$name" in - *pool*|*tmeta*|*tdata*|*tpool*|swap*) - DEBUG "skipping LV name $name (not a root LV candidate)" - continue - ;; - esac - - lvpath="/dev/$VG/$name" - if ! [ -e "$lvpath" ]; then - MAPPER_LV="${name//-/--}" - lvpath="/dev/mapper/${VG//-/--}-${MAPPER_LV}" - fi - if [ -e "$lvpath" ]; then - case "$name" in - root|dom0|dom0-root|qubes_dom0|qubes_dom0-root|*dom0*root*|*root*) - [ -n "$FIRST_LV_PREFERRED" ] || FIRST_LV_PREFERRED="$lvpath" - DEBUG "preferred LV candidate $lvpath (name $name)" - ;; - *) - [ -n "$FIRST_LV_FALLBACK" ] || FIRST_LV_FALLBACK="$lvpath" - ;; - esac - fi - done - - if [ -n "$FIRST_LV_PREFERRED" ]; then - DEBUG "selecting preferred LV $FIRST_LV_PREFERRED in VG $VG" - LV="$FIRST_LV_PREFERRED" - elif [ -n "$FIRST_LV_FALLBACK" ]; then - DEBUG "falling back to first mountable LV $FIRST_LV_FALLBACK in VG $VG" - LV="$FIRST_LV_FALLBACK" - else - LV="" - fi - fi - if ! [ -e "$LV" ]; then - DEBUG "no usable LV found in VG $VG" - return 1 - fi - # Use selected LV - open_block_device_layers "$LV" + TRACE_FUNC + local VG="$1" + local LV MAPPER_VG MAPPER_LV name lvpath FIRST_LV_PREFERRED FIRST_LV_FALLBACK + + if ! run_lvm vgchange -ay "$VG"; then + DEBUG "Can't open LVM VG: $VG" + return 1 + fi + + # Prefer an LV named 'root' (used by Qubes), but fall back to any LV + # in the VG. This ensures Ubuntu-style names (e.g. ubuntu-vg/ubuntu-root) + # also work. + LV="/dev/$VG/root" + if ! [ -e "$LV" ]; then + MAPPER_VG="${VG//-/--}" + LV="/dev/mapper/${MAPPER_VG}-root" + fi + if ! [ -e "$LV" ]; then + FIRST_LV_PREFERRED="" + FIRST_LV_FALLBACK="" + DEBUG "LVM VG $VG has no 'root' LV, enumerating all LVs" + # list LV names and prefer root-like names + for name in $(run_lvm lvs --noheadings -o lv_name --separator ' ' "$VG" 2>/dev/null); do + # thin pool/metadata and swap-like LVs are not root filesystems + case "$name" in + # TODO: *tdata*, *tmeta*, *tpool* are redundant with *pool*; deduplicate + *pool* | *tmeta* | *tdata* | *tpool* | swap*) + DEBUG "skipping LV name $name (not a root LV candidate)" + continue + ;; + esac + + lvpath="/dev/$VG/$name" + if ! [ -e "$lvpath" ]; then + MAPPER_LV="${name//-/--}" + lvpath="/dev/mapper/${VG//-/--}-${MAPPER_LV}" + fi + if [ -e "$lvpath" ]; then + case "$name" in + root | dom0 | dom0-root | qubes_dom0 | qubes_dom0-root | *dom0*root* | *root*) + [ -n "$FIRST_LV_PREFERRED" ] || FIRST_LV_PREFERRED="$lvpath" + DEBUG "preferred LV candidate $lvpath (name $name)" + ;; + *) + [ -n "$FIRST_LV_FALLBACK" ] || FIRST_LV_FALLBACK="$lvpath" + ;; + esac + fi + done + + if [ -n "$FIRST_LV_PREFERRED" ]; then + DEBUG "selecting preferred LV $FIRST_LV_PREFERRED in VG $VG" + LV="$FIRST_LV_PREFERRED" + elif [ -n "$FIRST_LV_FALLBACK" ]; then + DEBUG "falling back to first mountable LV $FIRST_LV_FALLBACK in VG $VG" + LV="$FIRST_LV_FALLBACK" + else + LV="" + fi + fi + if ! [ -e "$LV" ]; then + DEBUG "no usable LV found in VG $VG" + return 1 + fi + # Use selected LV + open_block_device_layers "$LV" } # Open a LUKS device, then continue looking for more layers. open_block_device_luks() { - TRACE_FUNC - local DEVICE="$1" - local LUKSDEV - LUKSDEV="$(basename "$DEVICE")_crypt" - - # Open the LUKS device. This may prompt interactively for the passphrase, so - # hook it up to the console even if stdout/stdin have been redirected. - if ! cryptsetup open "$DEVICE" "$LUKSDEV"; then - DEBUG "Can't open LUKS volume: $DEVICE" - return 1 - fi - - # Inform LVM about any new physical volume inside this decrypted container. - # Some distributions (Fedora) require a vgscan before LVM will create nodes - # under /dev/mapper, otherwise our later search won't see the logical - # volumes. This is harmless on systems without lvm installed. - if command -v lvm >/dev/null 2>&1; then - DEBUG "running vgscan to populate /dev/mapper after unlocking LUKS" - lvm vgscan --mknodes >/dev/null 2>&1 || true - fi - - open_block_device_layers "/dev/mapper/$LUKSDEV" + TRACE_FUNC + local DEVICE="$1" + local LUKSDEV + LUKSDEV="$(basename "$DEVICE")_crypt" + + # Open the LUKS device. This may prompt interactively for the passphrase, so + # hook it up to the console even if stdout/stdin have been redirected. + if ! cryptsetup open "$DEVICE" "$LUKSDEV"; then + DEBUG "Can't open LUKS volume: $DEVICE" + return 1 + fi + + # Inform LVM about any new physical volume inside this decrypted container. + # Some distributions (Fedora) require a vgscan before LVM will create nodes + # under /dev/mapper, otherwise our later search won't see the logical + # volumes. This is harmless on systems without lvm installed. + if command -v lvm >/dev/null 2>&1; then + DEBUG "running vgscan to populate /dev/mapper after unlocking LUKS" + run_lvm vgscan --mknodes >/dev/null 2>&1 || true + fi + + open_block_device_layers "/dev/mapper/$LUKSDEV" } # Open block device layers to access /root recursively. If another layer (LUKS @@ -287,30 +290,30 @@ open_block_device_luks() { # it. It succeeds otherwise, even if no layers are recognized, because we # should try to mount the block device directly in that case. open_block_device_layers() { - TRACE_FUNC - local DEVICE="$1" - local VG - - if ! [ -e "$DEVICE" ]; then - DEBUG "Block device doesn't exit: $DEVICE" - # This shouldn't really happen, we thought we opened the last layer - # successfully. The call stack reveals what LUKS/LVM2 layers have been - # opened so far. - DEBUG_STACK - return 1 - fi - - # Try to open a LUKS layer - if cryptsetup isLuks "$DEVICE" &>/dev/null; then - open_block_device_luks "$DEVICE" || return 1 - # Try to open an LVM layer - elif VG="$(find_lvm_vg_name "$DEVICE")"; then - open_block_device_lvm "$VG" || return 1 - else - # The given block device exists but is not any layer we understand. Stop - # opening layers and try to mount it. - echo "$DEVICE" - fi + TRACE_FUNC + local DEVICE="$1" + local VG + + if ! [ -e "$DEVICE" ]; then + DEBUG "Block device doesn't exit: $DEVICE" + # This shouldn't really happen, we thought we opened the last layer + # successfully. The call stack reveals what LUKS/LVM2 layers have been + # opened so far. + DEBUG_STACK + return 1 + fi + + # Try to open a LUKS layer + if cryptsetup isLuks "$DEVICE" &>/dev/null; then + open_block_device_luks "$DEVICE" || return 1 + # Try to open an LVM layer + elif VG="$(find_lvm_vg_name "$DEVICE")"; then + open_block_device_lvm "$VG" || return 1 + else + # The given block device exists but is not any layer we understand. Stop + # opening layers and try to mount it. + echo "$DEVICE" + fi } # Try to open a block device as /root. open_block_device_layers() is used to @@ -319,279 +322,280 @@ open_block_device_layers() { # This function does not clean up anything if it is unsuccessful. Use # try_open_root_device() to also clean up when unsuccessful. open_root_device_no_clean_up() { - TRACE_FUNC - local DEVICE="$1" - local FS_DEVICE BLKID_OUT - - # Open LUKS/LVM and get the name of the block device that should contain the - # filesystem. If there are no LUKS/LVM layers, FS_DEVICE is just DEVICE. - FS_DEVICE="$(open_block_device_layers "$DEVICE")" || return 1 - - # Keep detection minimal for initrd: only require blkid to return some - # metadata before mount probing. TYPE is often unavailable in this initrd. - BLKID_OUT="$(blkid "$FS_DEVICE" 2>/dev/null || true)" - DEBUG "blkid output for $FS_DEVICE: $BLKID_OUT" - - # If blkid reports nothing at all, this is likely not a filesystem-bearing - # partition. Skip mount probing to avoid noisy kernel probe logs. - if [ -z "$BLKID_OUT" ]; then - ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer with no recognizable filesystem metadata." - DEBUG "Skipping $FS_DEVICE: blkid returned no filesystem metadata" - return 1 - fi - - # Mount the device - if ! mount -o ro "$FS_DEVICE" "$ROOT_MOUNT" &>/dev/null; then - ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer on $FS_DEVICE but it could not be mounted as root by this root-hash flow." - DEBUG "Can't mount filesystem on $FS_DEVICE from $DEVICE" - return 1 - fi - - # The filesystem must have all of the directories configured. (Intentional - # word-split) - # shellcheck disable=SC2086 - if ! (cd "$ROOT_MOUNT" && ls -d $CONFIG_ROOT_DIRLIST &>/dev/null); then - DEBUG "Root filesystem on $DEVICE lacks one of the configured directories: $CONFIG_ROOT_DIRLIST" - return 1 - fi - - # Root is mounted now and the directories are present - return 0 + TRACE_FUNC + local DEVICE="$1" + local FS_DEVICE BLKID_OUT + + # Open LUKS/LVM and get the name of the block device that should contain the + # filesystem. If there are no LUKS/LVM layers, FS_DEVICE is just DEVICE. + FS_DEVICE="$(open_block_device_layers "$DEVICE")" || return 1 + + # Keep detection minimal for initrd: only require blkid to return some + # metadata before mount probing. TYPE is often unavailable in this initrd. + BLKID_OUT="$(blkid "$FS_DEVICE" 2>/dev/null || true)" + DEBUG "blkid output for $FS_DEVICE: $BLKID_OUT" + + # If blkid reports nothing at all, this is likely not a filesystem-bearing + # partition. Skip mount probing to avoid noisy kernel probe logs. + if [ -z "$BLKID_OUT" ]; then + ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer with no recognizable filesystem metadata." + DEBUG "Skipping $FS_DEVICE: blkid returned no filesystem metadata" + return 1 + fi + + # Mount the device + if ! mount -o ro "$FS_DEVICE" "$ROOT_MOUNT" &>/dev/null; then + ROOT_DETECT_UNSUPPORTED_REASON="Found partition/layer on $FS_DEVICE but it could not be mounted as root by this root-hash flow." + DEBUG "Can't mount filesystem on $FS_DEVICE from $DEVICE" + return 1 + fi + + # The filesystem must have all of the directories configured. (Intentional + # word-split) + # shellcheck disable=SC2086 + if ! (cd "$ROOT_MOUNT" && ls -d $CONFIG_ROOT_DIRLIST &>/dev/null); then + DEBUG "Root filesystem on $DEVICE lacks one of the configured directories: $CONFIG_ROOT_DIRLIST" + return 1 + fi + + # Root is mounted now and the directories are present + return 0 } # If an LVM VG is open, close any layers within it, then close the LVM VG. close_block_device_lvm() { - TRACE_FUNC - local VG="$1" - # Deactivate the VG directly. This avoids recursive LV close probing noise - # for LV paths that are not PVs and matches the minimal initrd workflow. - lvm vgchange -an "$VG" || \ - DEBUG "Can't close LVM VG: $VG" + TRACE_FUNC + local VG="$1" + # Deactivate the VG directly. This avoids recursive LV close probing noise + # for LV paths that are not PVs and matches the minimal initrd workflow. + run_lvm vgchange -an "$VG" || + DEBUG "Can't close LVM VG: $VG" } # If a LUKS device is open, close any layers within the LUKS device, then close # the LUKS device. close_block_device_luks() { - TRACE_FUNC - local DEVICE="$1" - local LUKSDEV - LUKSDEV="$(basename "$DEVICE")_crypt" - - if [ -e "/dev/mapper/$LUKSDEV" ]; then - # Close inner layers before trying to close LUKS - close_block_device_layers "/dev/mapper/$LUKSDEV" - cryptsetup close "$LUKSDEV" || \ - DEBUG "Can't close LUKS volume: $LUKSDEV" - fi + TRACE_FUNC + local DEVICE="$1" + local LUKSDEV + LUKSDEV="$(basename "$DEVICE")_crypt" + + if [ -e "/dev/mapper/$LUKSDEV" ]; then + # Close inner layers before trying to close LUKS + close_block_device_layers "/dev/mapper/$LUKSDEV" + cryptsetup close "$LUKSDEV" || + DEBUG "Can't close LUKS volume: $LUKSDEV" + fi } # Close the root device, including unmounting the filesystem and closing all # layers. This can close a partially-opened device if an error occurs. close_block_device_layers() { - TRACE_FUNC - local DEVICE="$1" - local VG - - if ! [ -e "$DEVICE" ]; then - DEBUG "Block device doesn't exit: $DEVICE" - # Like in open_root_device(), this shouldn't really happen, show the layers - # up to this point via the call stack. - DEBUG_STACK - return 1 - fi - - if cryptsetup isLuks "$DEVICE"; then - close_block_device_luks "$DEVICE" - elif VG="$(find_lvm_vg_name "$DEVICE")"; then - close_block_device_lvm "$VG" - fi - # Otherwise, we've handled all the layers we understood, there's nothing left - # to do. + TRACE_FUNC + local DEVICE="$1" + local VG + + if ! [ -e "$DEVICE" ]; then + DEBUG "Block device doesn't exit: $DEVICE" + # Like in open_root_device(), this shouldn't really happen, show the layers + # up to this point via the call stack. + DEBUG_STACK + return 1 + fi + + if cryptsetup isLuks "$DEVICE"; then + close_block_device_luks "$DEVICE" + elif VG="$(find_lvm_vg_name "$DEVICE")"; then + close_block_device_lvm "$VG" + fi + # Otherwise, we've handled all the layers we understood, there's nothing left + # to do. } # Try to open the root device, and clean up if unsuccessful. open_root_device() { - TRACE_FUNC - if ! open_root_device_no_clean_up "$1"; then - close_root_device "$1" - return 1 - fi + TRACE_FUNC + if ! open_root_device_no_clean_up "$1"; then + close_root_device "$1" + return 1 + fi - return 0 + return 0 } # Close the root device, including unmounting the filesystem and closing all # layers. This can close a partially-opened device if an error occurs. This # never fails, if an error occurs it still tries to close anything it can. close_root_device() { - TRACE_FUNC - local DEVICE="$1" + TRACE_FUNC + local DEVICE="$1" - # Unmount the filesystem if it is mounted. If it is not mounted, ignore the - # failure. If it is mounted but can't be unmounted, this will fail and we - # will fail to close any LUKS/LVM layers too. - umount "$ROOT_MOUNT" &>/dev/null || true + # Unmount the filesystem if it is mounted. If it is not mounted, ignore the + # failure. If it is mounted but can't be unmounted, this will fail and we + # will fail to close any LUKS/LVM layers too. + umount "$ROOT_MOUNT" &>/dev/null || true - close_block_device_layers "$DEVICE" || true + close_block_device_layers "$DEVICE" || true } # detect and set /root device # mount /root if successful -detect_root_device() -{ - TRACE_FUNC - - echo "+++ Detecting root device " - - if [ ! -e $ROOT_MOUNT ]; then - mkdir -p $ROOT_MOUNT - fi - # Ensure nothing is opened/mounted - unmount_root_device - ROOT_DETECT_UNSUPPORTED_REASON="" - - # check $CONFIG_ROOT_DEV if set/valid - # run open_root_device with fd10 closed so external tools don't inherit it - if [ -e "$CONFIG_ROOT_DEV" ] && open_root_device "$CONFIG_ROOT_DEV" 10<&-; then - return 0 - fi - - # generate list of possible boot devices - fdisk -l 2>/dev/null | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" > /tmp/disklist - DEBUG "detect_root_device: initial disklist=$(cat /tmp/disklist | tr '\n' ' ')" - - # filter out extraneous options - > /tmp_root_device_list - while IFS= read -r -u 10 i; do - # remove block device from list if numeric partitions exist - DEV_NUM_PARTITIONS=$((`ls -1 $i* | wc -l`-1)) - DEBUG "detect_root_device: candidate $i has $DEV_NUM_PARTITIONS numeric partitions" - if [ ${DEV_NUM_PARTITIONS} -eq 0 ]; then - echo $i >> /tmp_root_device_list - else - ls $i* | tail -${DEV_NUM_PARTITIONS} >> /tmp_root_device_list - fi - done 10/dev/null | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" >/tmp/disklist + DEBUG "detect_root_device: initial disklist=$(cat /tmp/disklist | tr '\n' ' ')" + + # filter out extraneous options + >/tmp_root_device_list + while IFS= read -r -u 10 i; do + # remove block device from list if numeric partitions exist + DEV_NUM_PARTITIONS=$(($(ls -1 $i* | wc -l) - 1)) + DEBUG "detect_root_device: candidate $i has $DEV_NUM_PARTITIONS numeric partitions" + if [ ${DEV_NUM_PARTITIONS} -eq 0 ]; then + echo $i >>/tmp_root_device_list + else + ls $i* | tail -${DEV_NUM_PARTITIONS} >>/tmp_root_device_list + fi + done 10/tmp/whiptail || recovery "GUI menu failed" - else - whiptail --title "Root Disk Verification Menu" \ - --menu "This feature lets you detect tampering in files on your root disk.\n\nNo hash file has been created yet\n\nYou can create hashes for files in:\n $CONFIG_ROOT_DIRLIST_PRETTY\n\nAutomatic checks are ${AT_BOOT} at boot.\n\nSelect the function to perform:" 0 80 10 \ - 'u' ' Create root hashes' \ - 'x' ' Exit' \ - 2>/tmp/whiptail || recovery "GUI menu failed" - fi - - menu_choice=$(cat /tmp/whiptail) - - case "$menu_choice" in - "x" ) - exit 0 - ;; - "c" ) - check_root_checksums - if [ $? -eq 0 ]; then - whiptail --title 'Verified Root Hashes' \ - --msgbox "All files in $CONFIG_ROOT_DIRLIST_PRETTY passed the verification process" 0 80 - fi - ;; - "u" ) - update_root_checksums - ;; - esac + unset menu_choice + + # mount /boot RO to detect hash file + if ! grep -q /boot /proc/mounts; then + if ! mount -o ro /boot; then + unmount_root_device + whiptail_error --title 'ERROR: Unable to mount /boot' \ + --msgbox "Unable to mount /boot" 0 80 + DIE "Unable to mount /boot" + fi + fi + + if [ "$CONFIG_ROOT_CHECK_AT_BOOT" = "y" ]; then + AT_BOOT="enabled" + else + AT_BOOT="disabled" + fi + if [ -e "$HASH_FILE" ]; then + HASH_FILE_DATE=$(stat -c %y ${HASH_FILE}) + whiptail_type $BG_COLOR_MAIN_MENU --title "Root Disk Verification Menu" \ + --menu "This feature lets you detect tampering in files on your root disk.\n\nHash file last updated: ${HASH_FILE_DATE}\n\nYou can check and update hashes for files in:\n $CONFIG_ROOT_DIRLIST_PRETTY\n\nAutomatic checks are ${AT_BOOT} at boot.\n\nSelect the function to perform:" 0 80 10 \ + 'c' ' Check root hashes' \ + 'u' ' Update root hashes' \ + 'x' ' Exit' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + else + whiptail_type $BG_COLOR_MAIN_MENU --title "Root Disk Verification Menu" \ + --menu "This feature lets you detect tampering in files on your root disk.\n\nNo hash file has been created yet\n\nYou can create hashes for files in:\n $CONFIG_ROOT_DIRLIST_PRETTY\n\nAutomatic checks are ${AT_BOOT} at boot.\n\nSelect the function to perform:" 0 80 10 \ + 'u' ' Create root hashes' \ + 'x' ' Exit' \ + 2>/tmp/whiptail || recovery "GUI menu failed" + fi + + menu_choice=$(cat /tmp/whiptail) + + case "$menu_choice" in + "x") + exit 0 + ;; + "c") + check_root_checksums + if [ $? -eq 0 ]; then + whiptail_type $BG_COLOR_MAIN_MENU --title 'Verified Root Hashes' \ + --msgbox "All files in $CONFIG_ROOT_DIRLIST_PRETTY passed the verification process" 0 80 + fi + ;; + "u") + update_root_checksums + ;; + esac done exit 0 diff --git a/initrd/bin/seal-hotpkey.sh b/initrd/bin/seal-hotpkey.sh index bd32f16f2..10e93f544 100755 --- a/initrd/bin/seal-hotpkey.sh +++ b/initrd/bin/seal-hotpkey.sh @@ -6,7 +6,6 @@ HOTP_SECRET="/tmp/secret/hotp.key" HOTP_COUNTER="/boot/kexec_hotp_counter" -HOTP_KEY="/boot/kexec_hotp_key" mount_boot() { TRACE_FUNC @@ -22,20 +21,13 @@ mount_boot() { TRACE_FUNC -# 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)" -else - HOTPKEY_BRANDING="HOTP USB Security dongle" -fi - if [ "$CONFIG_TPM" = "y" ]; then DEBUG "Sealing HOTP secret reuses TOTP sealed secret..." tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET" || - die "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" + secret_from_rom_hash >"$HOTP_SECRET" || DIE "Reading ROM failed" fi # Store counter in file instead of TPM for now, as it conflicts with Heads @@ -44,12 +36,12 @@ fi mount_boot || exit 1 #check_tpm_counter $HOTP_COUNTER hotp \ -#|| die "Unable to find/create TPM counter" +#|| DIE "Unable to find/create TPM counter" #counter="$TPM_COUNTER" # #counter_value=$(read_tpm_counter $counter | cut -f2 -d ' ' | awk 'gsub("^000e","")') #if [ "$counter_value" == "" ]; then -# die "Unable to read HOTP counter" +# DIE "Unable to read HOTP counter" #fi #counter_value=$(printf "%d" 0x${counter_value}) @@ -58,6 +50,11 @@ counter_value=1 enable_usb +# Detect branding after USB is up so lsusb can see the device. +DONGLE_BRAND="$(detect_usb_security_dongle_branding)" +export DONGLE_BRAND +DEBUG "$DONGLE_BRAND detected via USB VID:PID" + TRACE_FUNC # Make sure no conflicting GPG related services are running, gpg-agent will respawn @@ -66,25 +63,18 @@ 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 if ! hotp_token_info="$(hotp_verification info)"; then - echo -e "\nInsert your $HOTPKEY_BRANDING and press Enter to configure it" - read + INPUT "Insert your $DONGLE_BRAND and press Enter to configure it" if ! hotp_token_info="$(hotp_verification info)"; then # don't leak key on failure shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null - die "Unable to find $HOTPKEY_BRANDING" + DIE "Unable to find $DONGLE_BRAND" fi fi -# Set HOTP USB Security dongle branding based on VID -if lsusb | grep -q "20a0:"; then - HOTPKEY_BRANDING="Nitrokey" -elif lsusb | grep -q "316d:"; then - HOTPKEY_BRANDING="Librem Key" -else - HOTPKEY_BRANDING="HOTP USB Security dongle" -fi - -DEBUG "HOTP USB Security dongle branding is $HOTPKEY_BRANDING" +# Re-detect branding now that the dongle is confirmed present. +DONGLE_BRAND="$(detect_usb_security_dongle_branding)" +export DONGLE_BRAND +DEBUG "$DONGLE_BRAND detected via USB VID:PID" # Truncate the secret if it is longer than the maximum HOTP secret truncate_max_bytes 20 "$HOTP_SECRET" @@ -99,20 +89,34 @@ gpg_key_create_time="${gpg_key_create_time:-0}" DEBUG "Signature key was created at $(date -d "@$gpg_key_create_time")" now_date="$(date '+%s')" -# Get the number of HOTP related PIN retry attempts remaining -# if nk3 detected by lsusb, use different regex to get admin counter -if lsusb | grep -q "20a0:42b2"; then - # Nitrokey 3: Secrets app PIN counter: 8 +# Get the number of HOTP related PIN retry attempts remaining. +# NK3 uses "Secrets app PIN counter"; all pre-NK3 devices use "Card counters: Admin". +if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then admin_pin_retries=$(echo "$hotp_token_info" | grep "Secrets app PIN counter:" | cut -d ':' -f 2 | tr -d ' ') prompt_message="Secrets app" else - # /dev/null 2>&1 - hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$HOTPKEY_BRANDING" + #hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$DONGLE_BRAND" >/dev/null 2>&1 + hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$DONGLE_BRAND" admin_pin_status="$?" fi if [ "$admin_pin_status" -ne 0 ]; then - # prompt user for PIN and retry - 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 - 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 + # prompt user for PIN; re-query counter before each attempt so the user + # sees the decremented count after a wrong PIN (same pattern as kexec-sign-config.sh) + for tries in 1 2 3; do + show_pin_retries + if [ "$tries" -eq 1 ]; then + INPUT "Enter your $DONGLE_BRAND $prompt_message PIN (attempt $tries/3):" -r -s admin_pin + else + INPUT "Wrong PIN - re-enter your $DONGLE_BRAND $prompt_message PIN (attempt $tries/3):" -r -s admin_pin + fi + if hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$DONGLE_BRAND"; then + break + fi + if [ "$tries" -eq 3 ]; then # don't leak key on failure shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null - if [ "$HOTPKEY_BRANDING" == "Nitrokey" ]; then - die "Setting HOTP secret failed, to reset $prompt_message PIN, redo Re-Ownership procedure, use the Nitrokey App 2 or contact Nitrokey support" - else - die "Setting HOTP secret failed" - fi + case "$DONGLE_BRAND" in + "Nitrokey Pro" | "Nitrokey Storage" | "Nitrokey 3") + DIE "Setting HOTP secret on $DONGLE_BRAND failed after 3 attempts. To reset $prompt_message PIN: redo Re-Ownership, or use Nitrokey App 2, or contact Nitrokey support." + ;; + "Librem Key") + DIE "Setting HOTP secret on $DONGLE_BRAND failed after 3 attempts. To reset $prompt_message PIN: redo Re-Ownership or contact Purism support." + ;; + *) + DIE "Setting HOTP secret failed after 3 attempts" + ;; + esac fi - fi + done else - # remind user to change admin password - warn "Default $prompt_message PIN detected. Please change this as soon as possible with Options > OEM Factory Reset / Re-Ownership" + # Default PIN was accepted — security reminder, not a fatal error. + # NOTE prints blank lines before/after and is always visible; no INPUT needed. + NOTE "Default $prompt_message PIN detected. Change it via Options --> OEM Factory Reset / Re-Ownership." fi # HOTP key no longer needed @@ -170,25 +185,20 @@ shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null # Make sure our counter is incremented ahead of the next check #increment_tpm_counter $counter > /dev/null \ -#|| die "Unable to increment tpm counter" +#|| DIE "Unable to increment tpm counter" #increment_tpm_counter $counter > /dev/null \ -#|| die "Unable to increment tpm counter" +#|| DIE "Unable to increment tpm counter" mount -o remount,rw /boot counter_value=$(expr $counter_value + 1) echo $counter_value >$HOTP_COUNTER || - die "Unable to create hotp counter file" - -# Store/overwrite HOTP USB Security dongle branding found out beforehand -echo $HOTPKEY_BRANDING >$HOTP_KEY || - die "Unable to store hotp key file" + DIE "Unable to create hotp counter file" #sha256sum /tmp/counter-$counter > $HOTP_COUNTER \ -#|| die "Unable to create hotp counter file" +#|| DIE "Unable to create hotp counter file" mount -o remount,ro /boot -echo -e "\n$HOTPKEY_BRANDING initialized successfully. Press Enter to continue." -read +STATUS_OK "$DONGLE_BRAND initialized successfully" exit 0 diff --git a/initrd/bin/seal-totp.sh b/initrd/bin/seal-totp.sh index 596d2f191..1edab24b9 100755 --- a/initrd/bin/seal-totp.sh +++ b/initrd/bin/seal-totp.sh @@ -20,16 +20,18 @@ TPM_PASSWORD="$2" TOTP_SECRET="/tmp/secret/totp.key" TOTP_SEALED="/tmp/secret/totp.sealed" +STATUS "Generating TOTP secret" dd \ if=/dev/urandom \ of="$TOTP_SECRET" \ count=1 \ bs=20 \ 2>/dev/null || - die "Unable to generate 20 random bytes" + DIE "Unable to generate 20 random bytes" secret="$(base32 <$TOTP_SECRET)" pcrf="/tmp/secret/pcrf.bin" +INFO "TPM: Reading PCR values for TOTP sealing policy" DEBUG "Sealing TOTP with actual state of PCR0-3" tpmr.sh pcrread 0 "$pcrf" tpmr.sh pcrread -a 1 "$pcrf" @@ -49,8 +51,23 @@ DEBUG "Sealing TOTP without PCR6 involvement (LUKS header consistency is not fir DEBUG "Sealing TOTP with actual state of PCR7 (User injected stuff in cbfs)" tpmr.sh pcrread -a 7 "$pcrf" #Make sure we clear the TPM Owner Password from memory in case it failed to be used to seal TOTP -tpmr.sh seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSWORD" || - die "Unable to write sealed secret to NVRAM from seal-totp" + +# if the board has TPM2 tools, check for the primary handle before +# attempting to seal; a missing handle is the most common reason for +# failure and we want to give the same message as unseal-totp.sh. +if [ "$CONFIG_TPM2_TOOLS" = "y" ] && [ ! -f "/tmp/secret/primary.handle" ]; then + DIE "Unable to seal TOTP secret; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) then generate a new TOTP secret." +fi + +# perform sealing via tpmr.sh. Failures may indicate missing primary handle +# or other TPM state issues. Avoid DO_WITH_DEBUG so interactive prompts +# (TPM owner password on TPM1) are not hidden from the user. +STATUS "Sealing TOTP secret to TPM NVRAM" +if ! tpmr.sh seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSWORD"; then + # tpmr.sh already logged details; guide user generically to reset TPM + DIE "Unable to seal TOTP secret to TPM NVRAM; reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) and try again." +fi +STATUS_OK "TOTP secret sealed to TPM successfully" #Make sure we clear TPM TOTP sealed if we succeed to seal TOTP shred -n 10 -z -u "$TOTP_SEALED" 2>/dev/null @@ -59,5 +76,5 @@ url="otpauth://totp/$HOST?secret=$secret" DEBUG "TOTP secret output on screen (both URL and QR code)" qrenc "$url" -echo "TOTP secret for manual input (device without camera): $secret" +STATUS "TOTP secret for manual input (device without camera): $secret" secret="" diff --git a/initrd/bin/tpm-reset.sh b/initrd/bin/tpm-reset.sh index 7348d107d..2c67e7471 100755 --- a/initrd/bin/tpm-reset.sh +++ b/initrd/bin/tpm-reset.sh @@ -1,9 +1,7 @@ #!/bin/bash . /etc/functions.sh -echo '*****' -echo '***** WARNING: This will erase all keys and secrets from the TPM' -echo '*****' +NOTE "This will erase all keys and secrets from the TPM" prompt_new_owner_password diff --git a/initrd/bin/tpmr.sh b/initrd/bin/tpmr.sh index c0a15c634..abe9a6dde 100755 --- a/initrd/bin/tpmr.sh +++ b/initrd/bin/tpmr.sh @@ -46,7 +46,7 @@ tpm2_password_hex() { echo "hex:$(echo -n "$1" | xxd -p | tr -d ' \n')" } -# usage: tpmr pcrread [-a] +# usage: tpmr.sh pcrread [-a] # Reads PCR binary data and writes to file. # -a: Append to file. Default is to overwrite. tpm2_pcrread() { @@ -196,11 +196,11 @@ $0 ~ pcr { replay_pcr() { TRACE_FUNC if [ -z "$2" ]; then - echo >&2 "No PCR number passed" + WARN "No PCR number passed" return fi if [ "$2" -ge 8 ]; then - echo >&2 "Illegal PCR number ($2)" + WARN "Illegal PCR number ($2)" return fi local log=$(cbmem -L) @@ -218,13 +218,13 @@ replay_pcr() { DEBUG "Replayed cbmem -L clean boot state of PCR=$pcr ALG=$alg : $replayed_pcr" # To manually introspect current PCR values: # PCR-2: - # tpmr calcfuturepcr 2 | xxd -p + # tpmr.sh calcfuturepcr 2 | xxd -p # PCR-4, in case of recovery shell (bash used for process substitution): - # bash -c "tpmr calcfuturepcr 4 <(echo -n recovery)" | xxd -p - # PCR-4, in case of normal boot passing through kexec-select-boot: - # bash -c "tpmr calcfuturepcr 4 <(echo -n generic)" | xxd -p + # bash -c "tpmr.sh calcfuturepcr 4 <(echo -n recovery)" | xxd -p + # PCR-4, in case of normal boot passing through kexec-select-boot.sh: + # bash -c "tpmr.sh calcfuturepcr 4 <(echo -n generic)" | xxd -p # PCR-5, depending on which modules are loaded for given board: - # tpmr calcfuturepcr 5 module0.ko module1.ko module2.ko | xxd -p + # tpmr.sh calcfuturepcr 5 module0.ko module1.ko module2.ko | xxd -p # PCR-6 and PCR-7: similar to 5, but with different files passed # (6: LUKS header, 7: user related cbfs files loaded from cbfs-init) } @@ -257,10 +257,8 @@ tpm2_extend() { esac done tpm2 pcrextend "$index:sha256=$hash" - INFO $(tpm2 pcrread "sha256:$index" 2>&1) - - TRACE_FUNC - DEBUG "TPM: Extended PCR[$index] with hash $hash" + LOG "TPM: PCR[$index] after extend: $(tpm2 pcrread "sha256:$index" 2>&1)" + LOG "TPM: Extended PCR[$index] with hash $hash" } tpm2_counter_read() { @@ -276,11 +274,15 @@ tpm2_counter_read() { ;; esac done - echo "$index: $(tpm2 nvread 0x$index | xxd -pc8)" + local hex_val + hex_val="$(tpm2 nvread 0x"$index" | xxd -pc8)" || return 1 + echo "$index: $hex_val" } tpm2_counter_inc() { TRACE_FUNC + local index pwd + local inc_args=() while true; do case "$1" in -ix) @@ -296,13 +298,18 @@ tpm2_counter_inc() { ;; esac done - tpm2 nvincrement "0x$index" >/dev/console - echo "$index: $(tpm2 nvread 0x$index | xxd -pc8)" + if [ -n "$pwd" ]; then + inc_args=(-C o -P "$(tpm2_password_hex "$pwd")") + fi + tpm2 nvincrement "${inc_args[@]}" "0x$index" >/dev/console || return 1 + local hex_val + hex_val="$(tpm2 nvread 0x"$index" | xxd -pc8)" || return 1 + echo "$index: $hex_val" } tpm1_counter_create() { TRACE_FUNC - # tpmr handles the TPM Owner Password (from cache or prompt), but all + # tpmr.sh handles the TPM Owner Password (from cache or prompt), but all # other parameters for TPM1 are passed directly, and TPM2 mimics the # TPM1 interface. prompt_tpm_owner_password @@ -315,13 +322,15 @@ tpm1_counter_create() { DEBUG "tpm1 stderr: $line" done <"$TMP_ERR_FILE" rm -f "$TMP_ERR_FILE" - die "Unable to create counter from tpm1_counter_create" + DIE "Unable to create counter from tpm1_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." fi rm -f "$TMP_ERR_FILE" } tpm2_counter_create() { TRACE_FUNC + pwd="" # owner password from argument + label="" # label argument while true; do case "$1" in -pwdc) @@ -337,15 +346,33 @@ tpm2_counter_create() { ;; esac done - prompt_tpm_owner_password + + # if caller supplied a password use it, otherwise prompt / use cache + if [ -n "$pwd" ]; then + tpm_owner_password="$pwd" + # cache it so other functions can reuse without prompting + mkdir -p "$SECRET_DIR" || true + echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password 2>/dev/null || true + else + prompt_tpm_owner_password + fi + rand_index="1$(dd if=/dev/urandom bs=1 count=3 2>/dev/null | xxd -pc3)" - tpm2 nvdefine -C o -s 8 -a "ownerread|authread|authwrite|nt=1" \ - -P "$(tpm2_password_hex "$(cat "/tmp/secret/tpm_owner_password")")" "0x$rand_index" >/dev/null 2>&1 || - { - DEBUG "Failed to create counter from tpm2_counter_create. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - die "Unable to create counter from tpm2_counter_create" - } + # capture stderr to a temp file for visibility + TMP_ERR_FILE=$(mktemp) + if ! tpm2 nvdefine -C o -s 8 -a "ownerread|authread|authwrite|nt=1" \ + -P "$(tpm2_password_hex "$tpm_owner_password")" "0x$rand_index" \ + 2>"$TMP_ERR_FILE" >/dev/null; then + DEBUG "Failed to create counter from tpm2_counter_create. Wiping /tmp/secret/tpm_owner_password" + shred -n 10 -z -u /tmp/secret/tpm_owner_password + # log the TPM2 stderr output + while IFS= read -r line; do + DEBUG "tpm2 stderr: $line" + done <"$TMP_ERR_FILE" + rm -f "$TMP_ERR_FILE" + DIE "Unable to create counter from tpm2_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + rm -f "$TMP_ERR_FILE" echo "$rand_index: (valid after an increment)" } @@ -354,15 +381,15 @@ tpm2_startsession() { mkdir -p "$SECRET_DIR" tpm2 flushcontext -Q \ --transient-object || - die "tpm2_flushcontext: unable to flush transient handles" + DIE "tpm2_flushcontext: unable to flush transient handles" tpm2 flushcontext -Q \ --loaded-session || - die "tpm2_flushcontext: unable to flush sessions" + DIE "tpm2_flushcontext: unable to flush sessions" tpm2 flushcontext -Q \ --saved-session || - die "tpm2_flushcontext: unable to flush saved session" + DIE "tpm2_flushcontext: unable to flush saved session" tpm2 readpublic -Q -c "$PRIMARY_HANDLE" -t "$PRIMARY_HANDLE_FILE" >/dev/null 2>&1 #TODO: do the right thing to not have to suppress "WARN: check public portion the tpmkey manually" see https://github.com/linuxboot/heads/pull/1630#issuecomment-2075120429 tpm2 startauthsession -Q -c "$PRIMARY_HANDLE_FILE" --hmac-session -S "$ENC_SESSION_FILE" >/dev/null 2>&1 @@ -407,7 +434,7 @@ tpm2_destroy() { # remove possible data occupying this handle tpm2 evictcontrol -Q -C p -c "$handle" 2>/dev/null || - die "Unable to evict secret from TPM NVRAM" + DIE "Unable to evict secret from TPM NVRAM" } # tpm1_destroy: Destroy a sealed file in the TPM. The mechanism differs by @@ -418,9 +445,9 @@ tpm1_destroy() { index="$1" # Index of the sealed file size="$2" # Size of zeroes to overwrite for TPM1 - dd if=/dev/zero bs="$size" count=1 of=/tmp/wipe-totp-zero >/dev/null 2>&1 - tpm nv_writevalue -in "$index" -if /tmp/wipe-totp-zero || - die "Unable to wipe sealed secret from TPM NVRAM" + dd if=/dev/zero bs="$size" count=1 of=/tmp/wipe-totp.sh-zero >/dev/null 2>&1 + tpm nv_writevalue -in "$index" -if /tmp/wipe-totp.sh-zero || + DIE "Unable to wipe sealed secret from TPM NVRAM" } # tpm2_seal: Seal a file against PCR values and, optionally, a password. @@ -439,6 +466,10 @@ tpm2_seal() { tpm_password="$7" # Owner password - will prompt if needed and not empty # TPM Owner Password is always needed for TPM2. + # bail early if the TPM hasn't been reset and we lack a primary handle. + if [ ! -f "$PRIMARY_HANDLE_FILE" ]; then + DIE "Unable to seal secret from TPM; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) and try again." + fi mkdir -p "$SECRET_DIR" bname="$(basename $file)" @@ -476,33 +507,50 @@ tpm2_seal() { # (The default is to allow either policy auth _or_ password auth. In # this case the policy includes the password, and we don't want to allow # the password on its own.) - tpm2 create -Q -C "$PRIMARY_HANDLE_FILE" \ + # mask hex of authorization password when supplied (it will be the + # last parameter if CREATE_PASS_ARGS is non-empty) + # tpm2 create: -u = public area output, -r = private (encrypted) area output. + # File extensions match: .pub for the public area, .priv for the private area. + DO_WITH_DEBUG --mask-position 18 tpm2 create -Q -C "$PRIMARY_HANDLE_FILE" \ -i "$file" \ - -u "$SECRET_DIR/$bname.priv" \ - -r "$SECRET_DIR/$bname.pub" \ + -u "$SECRET_DIR/$bname.pub" \ + -r "$SECRET_DIR/$bname.priv" \ -L "$AUTH_POLICY" \ -S "$DEC_SESSION_FILE" \ -a "fixedtpm|fixedparent|adminwithpolicy" \ "${CREATE_PASS_ARGS[@]}" - tpm2 load -Q -C "$PRIMARY_HANDLE_FILE" \ - -u "$SECRET_DIR/$bname.priv" -r "$SECRET_DIR/$bname.pub" \ + # tpm2 load: -u = public area input, -r = private (encrypted) area input. + DO_WITH_DEBUG tpm2 load -Q -C "$PRIMARY_HANDLE_FILE" \ + -u "$SECRET_DIR/$bname.pub" -r "$SECRET_DIR/$bname.priv" \ -c "$SECRET_DIR/$bname.seal.ctx" - prompt_tpm_owner_password - # remove possible data occupying this handle - tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_password")" \ - -c "$handle" 2>/dev/null || true - DO_WITH_DEBUG --mask-position 6 \ - tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_password")" \ - -c "$SECRET_DIR/$bname.seal.ctx" "$handle" || - { - DEBUG "Failed to write sealed secret to NVRAM from tpm2_seal. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - die "Unable to write sealed secret to TPM NVRAM" - } + # Retry loop: evictcontrol requires the TPM owner password; allow the + # user to re-enter it if it is wrong rather than dying immediately. + local evict_attempts=0 + while true; do + evict_attempts=$((evict_attempts + 1)) + prompt_tpm_owner_password + # remove possible data occupying this handle (failure is expected when + # the handle doesn't exist yet, so ignore errors) + DO_WITH_DEBUG --mask-position 6 tpm2 evictcontrol -Q -C o \ + -P "$(tpm2_password_hex "$tpm_owner_password")" \ + -c "$handle" >/dev/null 2>&1 || true + if DO_WITH_DEBUG --mask-position 6 \ + tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_password")" \ + -c "$SECRET_DIR/$bname.seal.ctx" "$handle" >/dev/null 2>&1; then + break + fi + DEBUG "tpm2_seal: evictcontrol failed (attempt $evict_attempts), wiping cached TPM owner password" + shred -n 10 -z -u /tmp/secret/tpm_owner_password 2>/dev/null + if [ "$evict_attempts" -ge 3 ]; then + DIE "Unable to write sealed secret to TPM NVRAM after $evict_attempts attempts" + fi + WARN "TPM owner password incorrect or TPM error - please try again (attempt $evict_attempts of 3)" + done } tpm1_seal() { TRACE_FUNC + local tmp_err_write tmp_err_define tmp_err_write_after_define file="$1" index="$2" pcrl="$3" #0,1,2,3,4,5,6,7 (does not include algorithm prefix) @@ -516,7 +564,7 @@ tpm1_seal() { POLICY_ARGS=() - DEBUG "tpm1_seal arguments: file=$file index=$index pcrl=$pcrl pcrf=$pcrf sealed_size=$sealed_size pass=$(mask_param "$pass") tpm_password=$(mask_param "$tpm_password")" + DEBUG "tpm1_seal arguments: file=$file index=$index pcrl=$pcrl pcrf=$pcrf sealed_size=$sealed_size pass=$(mask_param "$pass") tpm_owner_password=$(mask_param "$tpm_owner_password")" # If a password was given, add it to the policy arguments if [ "$pass" ]; then @@ -539,29 +587,54 @@ tpm1_seal() { -of "$sealed_file" \ -hk 40000000 \ "${POLICY_ARGS[@]}" + DEBUG "tpm1_seal: sealed blob created at $sealed_file (size=$(wc -c <"$sealed_file" 2>/dev/null || echo 0) bytes), target nv index=$index" # try it without the TPM Owner Password first - if ! tpm nv_writevalue -in "$index" -if "$sealed_file"; then + tmp_err_write="$(mktemp)" + if ! tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write"; then + while IFS= read -r line; do + DEBUG "tpm1_seal nv_writevalue(pre-define) stderr: $line" + done <"$tmp_err_write" + if grep -qi 'illegal index' "$tmp_err_write"; then + DEBUG "tpm1_seal: nv index $index is not defined yet (Illegal index); attempting nv_definespace" + fi + rm -f "$tmp_err_write" # to create an nvram space we need the TPM Owner Password # and the TPM physical presence must be asserted. # # The permissions are 0 since there is nothing special # about the sealed file tpm physicalpresence -s || - warn "Unable to assert physical presence" + { + WARN "Unable to assert physical presence" + } prompt_tpm_owner_password - tpm nv_definespace -in "$index" -sz "$sealed_size" \ - -pwdo "$tpm_owner_password" -per 0 || - warn "Unable to define TPM NVRAM space; trying anyway" + tmp_err_define="$(mktemp)" + if ! DO_WITH_DEBUG --mask-position 7 tpm nv_definespace -in "$index" -sz "$sealed_size" \ + -pwdo "$tpm_owner_password" -per 0 2>"$tmp_err_define"; then + while IFS= read -r line; do + DEBUG "tpm1_seal nv_definespace stderr: $line" + done <"$tmp_err_define" + WARN "Unable to define TPM NVRAM space; trying anyway" + fi + rm -f "$tmp_err_define" - tpm nv_writevalue -in "$index" -if "$sealed_file" || + tmp_err_write_after_define="$(mktemp)" + tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write_after_define" || { + while IFS= read -r line; do + DEBUG "tpm1_seal nv_writevalue(post-define) stderr: $line" + done <"$tmp_err_write_after_define" + rm -f "$tmp_err_write_after_define" DEBUG "Failed to write sealed secret to NVRAM from tpm1_seal. Wiping /tmp/secret/tpm_owner_password" shred -n 10 -z -u /tmp/secret/tpm_owner_password - die "Unable to write sealed secret to TPM NVRAM" + DIE "Unable to write sealed secret to TPM NVRAM" } + rm -f "$tmp_err_write_after_define" + else + rm -f "$tmp_err_write" fi } @@ -589,7 +662,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 the TPM to seal secret to TPM NVRAM" + WARN "No TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) before attempting to unseal a secret from TPM NVRAM" exit 1 fi @@ -624,6 +697,7 @@ tpm2_unseal() { tpm1_unseal() { TRACE_FUNC + local tmp_err_read index="$1" pcrl="$2" sealed_size="$3" @@ -639,22 +713,57 @@ tpm1_unseal() { rm -f "$sealed_file" - DO_WITH_DEBUG tpm nv_readvalue \ + # Read the sealed blob from NVRAM. `tpm nv_readvalue` prints a + # spurious warning about index size that we don't want on the console; + # capture stderr in the debug log instead. Password prompts are written + # directly to the tty and are unaffected by this redirection. + tmp_err_read="$(mktemp)" + if ! DO_WITH_DEBUG tpm nv_readvalue \ -in "$index" \ -sz "$sealed_size" \ - -of "$sealed_file" || - die "Unable to read sealed file from TPM NVRAM" + -of "$sealed_file" \ + 2>"$tmp_err_read"; then + while IFS= read -r line; do + DEBUG "tpm1_unseal nv_readvalue stderr: $line" + done <"$tmp_err_read" + rm -f "$tmp_err_read" + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal tpm1_unseal failure: unable to read sealed file from TPM NVRAM" + return 1 + fi + DIE "Unable to read sealed file from TPM NVRAM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." + fi + rm -f "$tmp_err_read" PASS_ARGS=() if [ "$pass" ]; then PASS_ARGS=(-pwdd "$pass") fi - tpm unsealfile \ - -if "$sealed_file" \ - -of "$file" \ - "${PASS_ARGS[@]}" \ - -hk 40000000 + if [ -n "$pass" ]; then + if ! DO_WITH_DEBUG --mask-position 7 tpm unsealfile \ + -if "$sealed_file" \ + -of "$file" \ + "${PASS_ARGS[@]}" \ + -hk 40000000; then + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal tpm1_unseal failure: unable to unseal TPM NVRAM blob" + return 1 + fi + DIE "Unable to unseal secret from TPM NVRAM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." + fi + else + if ! DO_WITH_DEBUG tpm unsealfile \ + -if "$sealed_file" \ + -of "$file" \ + -hk 40000000; then + if [ "$HEADS_NONFATAL_UNSEAL" = "y" ]; then + DEBUG "nonfatal tpm1_unseal failure: unable to unseal TPM NVRAM blob" + return 1 + fi + DIE "Unable to unseal secret from TPM NVRAM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." + fi + fi } # cache_owner_password @@ -692,25 +801,30 @@ tpm2_reset() { fi # 3. Re-own the TPM for Heads: set new owner and endorsement auth. - if ! DO_WITH_DEBUG tpm2 changeauth -c owner "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + # don't echo the owner password in debug logs + # hide the owner auth hex (argument index 4) + if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c owner "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to set owner auth" return 1 fi - if ! DO_WITH_DEBUG tpm2 changeauth -c endorsement "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + # mask endorsement password too (argument index 4) + if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c endorsement "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to set endorsement auth" return 1 fi # 4. Create and persist Heads primary key. - if ! DO_WITH_DEBUG tpm2 createprimary -C owner -g sha256 -G "${CONFIG_PRIMARY_KEY_TYPE:-rsa}" \ + # hide owner password during primary creation (argument index 11) + if ! DO_WITH_DEBUG --mask-position 11 tpm2 createprimary -C owner -g sha256 -G "${CONFIG_PRIMARY_KEY_TYPE:-rsa}" \ -c "$SECRET_DIR/primary.ctx" \ -P "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to create primary" return 1 fi - if ! DO_WITH_DEBUG tpm2 evictcontrol -C owner -c "$SECRET_DIR/primary.ctx" "$PRIMARY_HANDLE" \ + # and hide password when evicting the primary handle (argument index 8) + if ! DO_WITH_DEBUG --mask-position 8 tpm2 evictcontrol -C owner -c "$SECRET_DIR/primary.ctx" "$PRIMARY_HANDLE" \ -P "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to persist primary" shred -u "$SECRET_DIR/primary.ctx" >/dev/null 2>&1 @@ -794,19 +908,19 @@ tpm2_kexec_finalize() { # Flush sessions and transient objects tpm2 flushcontext -Q --transient-object || - warn "tpm2_flushcontext: unable to flush transient handles" + WARN "tpm2_flushcontext: unable to flush transient handles" tpm2 flushcontext -Q --loaded-session || - warn "tpm2_flushcontext: unable to flush sessions" + WARN "tpm2_flushcontext: unable to flush sessions" tpm2 flushcontext -Q --saved-session || - warn "tpm2_flushcontext: unable to flush saved session" + WARN "tpm2_flushcontext: unable to flush saved session" # Add a random passphrase to platform hierarchy to prevent TPM2 from # being cleared in the OS. # This passphrase is only effective before the next boot. - echo "Locking TPM2 platform hierarchy..." + STATUS "Locking TPM2 platform hierarchy" randpass=$(dd if=/dev/urandom bs=4 count=1 status=none 2>/dev/null | xxd -p) tpm2 changeauth -c platform "$randpass" || - warn "Failed to lock platform hierarchy of TPM2" + WARN "Failed to lock platform hierarchy of TPM2" } tpm2_shutdown() { @@ -819,8 +933,7 @@ tpm2_shutdown() { } if [ "$CONFIG_TPM" != "y" ]; then - echo >&2 "No TPM!" - exit 1 + DIE "No TPM!" fi # TPM1 - most commands forward directly to tpm, but some are still wrapped for @@ -883,8 +996,10 @@ if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then kexec_finalize) ;; # Nothing on TPM1. shutdown) ;; # Nothing on TPM1. *) - DEBUG "Direct translation from tpmr to tpm1 call" - DO_WITH_DEBUG exec tpm "$@" + # Keep passthrough raw here: callers decide whether to wrap with + # DO_WITH_DEBUG (and --mask-position when passing secrets). + DEBUG "Direct translation from tpmr.sh to tpm1 call" + exec tpm "$@" ;; esac exit 0 @@ -940,7 +1055,6 @@ shutdown) tpm2_shutdown "$@" ;; *) - echo "Command $subcmd not wrapped!" - exit 1 + DIE "Command $subcmd not wrapped!" ;; esac diff --git a/initrd/bin/uefi-init.sh b/initrd/bin/uefi-init.sh index 2870ae600..f95341f12 100755 --- a/initrd/bin/uefi-init.sh +++ b/initrd/bin/uefi-init.sh @@ -10,22 +10,22 @@ fi CONFIG_GUID="74696e69-6472-632e-7069-6f2f75736572" # copy EFI file named $CONFIG_GUID to /tmp, measure and extract -GUID=$(uefi -l | grep "^$CONFIG_GUID") +GUID=`uefi -l | grep "^$CONFIG_GUID"` -if [ -n "GUID" ]; then - echo "Loading $GUID from ROM" +if [ -n "$GUID" ]; then + STATUS "Loading $GUID from ROM" TMPFILE=/tmp/uefi.$$ - uefi -r $GUID | gunzip -c >$TMPFILE || - die "Failed to read config GUID from ROM" + uefi -r $GUID | gunzip -c > $TMPFILE \ + || DIE "Failed to read config GUID from ROM" if [ "$CONFIG_TPM" = "y" ]; then - tpmr.sh extend -ix "$CONFIG_PCR" -if $TMPFILE || - die "$filename: tpm extend failed" + INFO "TPM: Extending PCR[$CONFIG_PCR] with UEFI configuration" + tpmr.sh extend -ix "$CONFIG_PCR" -if $TMPFILE \ + || DIE "$GUID: tpm extend failed" fi - ( - cd / - cpio -iud <$TMPFILE 2>/dev/null - ) || - die "Failed to extract config GUID" + STATUS "Extracting UEFI configuration" + ( cd / ; cpio -iud < $TMPFILE 2>/dev/null ) \ + || DIE "Failed to extract config GUID" + STATUS_OK "UEFI configuration loaded" fi diff --git a/initrd/bin/unpack_initramfs.sh b/initrd/bin/unpack_initramfs.sh index 7604a4571..25f0b5caf 100755 --- a/initrd/bin/unpack_initramfs.sh +++ b/initrd/bin/unpack_initramfs.sh @@ -102,10 +102,10 @@ unpack_first_segment() { (zstd-decompress -d || true) | unpack_cpio ;; *) # unknown - die "Can't decompress initramfs archive, unknown type: $magic" + DIE "Can't decompress initramfs archive, unknown type: $magic" # The following are magic values for other compression formats # but not added because not tested. - # TODO: open an issue for unsupported magic number reported on die. + # TODO: open an issue for unsupported magic number reported on DIE. # #425a*) # bzip2 # DEBUG "archive segment $magic: bzip2" diff --git a/initrd/bin/unseal-hotp.sh b/initrd/bin/unseal-hotp.sh index 154f90007..e8f4691ed 100755 --- a/initrd/bin/unseal-hotp.sh +++ b/initrd/bin/unseal-hotp.sh @@ -11,7 +11,7 @@ mount_boot_or_die() { # 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" + DIE "Unable to mount /boot" fi } @@ -23,7 +23,7 @@ TRACE_FUNC mount_boot_or_die #check_tpm_counter $HOTP_COUNTER hotp \ -#|| die "Unable to find/create TPM counter" +#|| DIE "Unable to find/create TPM counter" #counter="$TPM_COUNTER" # #counter_value=$(read_tpm_counter $counter | cut -f2 -d ' ' | awk 'gsub("^000e","")') @@ -31,23 +31,37 @@ mount_boot_or_die #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" + fail_unseal "HOTP counter file not found. If you just reinstalled an OS, you need to reseal the HOTP secret" || exit 1 fi # Read the counter from the file -counter_value=$(cat $HOTP_COUNTER 2>/dev/null) +counter_value=$(cat "$HOTP_COUNTER" 2>/dev/null) if [ "$counter_value" == "" ]; then - die "Unable to read HOTP counter" + fail_unseal "Unable to read HOTP counter" || exit 1 fi #counter_value=$(printf "%d" 0x${counter_value}) if [ "$CONFIG_TPM" = "y" ]; then + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + # ensure primary handle exists before any TPM2 operation, to keep + # messaging consistent with unseal-totp.sh + if [ ! -f "/tmp/secret/primary.handle" ]; then + fail_unseal "Unable to unseal HOTP secret from TPM; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI)." || exit 1 + fi + fi DEBUG "Unsealing HOTP secret reuses TOTP sealed secret..." - tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET" || die "Unable to unseal HOTP secret" + # debug unseal too; no password argument + if ! DO_WITH_DEBUG tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$HOTP_SECRET"; then + if counter_readable; then + fail_unseal "Unable to unseal HOTP secret from TPM; TPM rollback counter intact. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 + else + fail_unseal "Unable to unseal HOTP secret from TPM; TPM rollback counter broken or missing, reset TPM (see Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and then generate a new secret." || exit 1 + fi + fi 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" + secret_from_rom_hash >"$HOTP_SECRET" || fail_unseal "Reading ROM failed" || exit 1 fi # Truncate the secret if it is longer than the maximum HOTP secret @@ -55,7 +69,7 @@ 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?' + fail_unseal 'Unable to compute HOTP hash?' || exit 1 fi shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null @@ -71,7 +85,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" + fail_unseal "Unable to create hotp counter file" || exit 1 mount -o remount,ro /boot exit 0 diff --git a/initrd/bin/unseal-totp.sh b/initrd/bin/unseal-totp.sh index c0d71808c..1a0dd00d8 100755 --- a/initrd/bin/unseal-totp.sh +++ b/initrd/bin/unseal-totp.sh @@ -5,17 +5,51 @@ TOTP_SECRET="/tmp/secret/totp.key" +fail_unseal_reset_required() { + TRACE_FUNC + # A TPM-side unseal failure generally indicates that reset/re-ownership is + # required before allowing reseal/generate workflows again. + set_tpm_reset_required "$*" "unseal-totp.sh:fail_unseal_reset_required" + DEBUG "fail_unseal_reset_required: reason='$*'" + fail_unseal "$@" +} + TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then - DO_WITH_DEBUG --mask-position 5 \ - tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET" || - die "Unable to unseal TOTP secret from TPM" + if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then + # if we are talking to TPM2, ensure the primary handle exists; TPM1 + # does not have the concept, so skip the check. + if [ ! -f "/tmp/secret/primary.handle" ]; then + fail_unseal_reset_required "Unable to unseal TOTP secret from TPM; no TPM primary handle. Reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI)." || exit 1 + fi + # show unseal invocation; there is no secret argument to mask + if ! DO_WITH_DEBUG \ + tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET"; then + # A TPM2 unseal failure with primary handle present is commonly a + # policy/PCR mismatch (for example after firmware updates). Keep this + # recoverable via reseal and do not force reset-required marker. + fail_unseal "Unable to unseal TOTP secret from TPM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 + fi + else + # TPM1 path: after reset/re-ownership, unseal failures here are best + # handled by resealing the secret from the GUI flow. + if ! DO_WITH_DEBUG tpmr.sh unseal 4d47 0,1,2,3,4,7 312 "$TOTP_SECRET"; then + fail_unseal "Unable to unseal TOTP secret from TPM. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 + fi + fi +fi + +if [ ! -s "$TOTP_SECRET" ]; then + fail_unseal "Unable to unseal TOTP secret from TPM; secret file $TOTP_SECRET is missing or empty. Use the GUI menu (Options -> TPM/TOTP/HOTP Options -> Generate new TOTP/HOTP secret) to reseal." || exit 1 fi -if ! DO_WITH_DEBUG totp -q <"$TOTP_SECRET"; then +# Run totp without DO_WITH_DEBUG: stdout is the TOTP code and must not be +# logged (security hazard - code in debug.log could be used to verify OTPs). +# Errors (stderr) are still captured for debugging. +if ! totp -q <"$TOTP_SECRET" 2> >(SINK_LOG "totp stderr"); then shred -n 10 -z -u "$TOTP_SECRET" 2>/dev/null - die 'Unable to compute TOTP hash?' + fail_unseal 'Unable to compute TOTP hash?' || exit 1 fi shred -n 10 -z -u "$TOTP_SECRET" 2>/dev/null diff --git a/initrd/bin/usb-autoboot.sh b/initrd/bin/usb-autoboot.sh index 8d63fbf7d..851f6ebe1 100755 --- a/initrd/bin/usb-autoboot.sh +++ b/initrd/bin/usb-autoboot.sh @@ -13,7 +13,7 @@ set -o pipefail # * No automatic boot was attempted - script returns nonzero. Continue with # normal automatic boot. -# These may die for failure, nonzero exit is correct (USB boot wasn't possible) +# These may DIE for failure, nonzero exit is correct (USB boot wasn't possible) enable_usb enable_usb_storage @@ -23,7 +23,7 @@ parse_boot_options() { BOOTDIR="$1" for i in $(find "$BOOTDIR" -name '*.cfg'); do - kexec-parse-boot "$BOOTDIR" "$i" + kexec-parse-boot.sh "$BOOTDIR" "$i" done } @@ -34,16 +34,15 @@ while read -u 4 -r USB_BLOCK_DEVICE; do USB_DEFAULT_BOOT="$(parse_boot_options /media | head -1)" if [ -n "$USB_DEFAULT_BOOT" ]; then # Boot automatically, unless the user interrupts. - echo -e "\n\n" - echo "Found bootable USB: $(echo "$USB_DEFAULT_BOOT" | cut -d '|' -f 1)" + STATUS "Found bootable USB: $(echo "$USB_DEFAULT_BOOT" | cut -d '|' -f 1)" if ! pause_automatic_boot; then # User interrupted, go to boot menu umount /media exit 0 fi - echo -e "\n\nBooting from USB...\n\n" - kexec-boot -b /media -e "$USB_DEFAULT_BOOT" - # If kexec-boot returned, the boot obviously did not occur, + STATUS "Booting from USB" + kexec-boot.sh -b /media -e "$USB_DEFAULT_BOOT" + # If kexec-boot.sh returned, the boot obviously did not occur, # return nonzero below so the normal OS boot will continue. fi umount /media diff --git a/initrd/bin/usb-init.sh b/initrd/bin/usb-init.sh index 2d09f651d..93cdfcec3 100755 --- a/initrd/bin/usb-init.sh +++ b/initrd/bin/usb-init.sh @@ -8,8 +8,10 @@ TRACE_FUNC if [ "$CONFIG_TPM" = "y" ]; then # Extend PCR4 as soon as possible + INFO "TPM: Extending PCR[4] for USB boot" tpmr.sh extend -ix 4 -ic usb fi -DO_WITH_DEBUG media-scan usb +STATUS "Scanning USB for boot media" +DO_WITH_DEBUG media-scan.sh usb recovery "Something failed during USB boot" diff --git a/initrd/bin/wget-measure.sh b/initrd/bin/wget-measure.sh index b1c1a9a6c..5d3f0c5f3 100755 --- a/initrd/bin/wget-measure.sh +++ b/initrd/bin/wget-measure.sh @@ -2,23 +2,17 @@ # get a file and extend a TPM PCR . /etc/functions.sh -die() { - TRACE_FUNC - echo >&2 "$@" - exit 1 -} - INDEX="$1" URL="$2" if [ -z "$INDEX" -o -z "$URL" ]; then - die "Usage: $0 pcr-index url" + DIE "Usage: $0 pcr-index url" fi -wget "$URL" || die "$URL: failed" +wget "$URL" || DIE "$URL: failed" FILE="`basename "$URL"`" -tpmr.sh extend -ix "$INDEX" -if "$FILE" || die "$FILE: tpm extend failed" +tpmr.sh extend -ix "$INDEX" -if "$FILE" || DIE "$FILE: tpm extend failed" diff --git a/initrd/bin/wipe-totp.sh b/initrd/bin/wipe-totp.sh index c7d142b77..220228ed7 100755 --- a/initrd/bin/wipe-totp.sh +++ b/initrd/bin/wipe-totp.sh @@ -10,5 +10,5 @@ TPM_SIZE=312 if [ "$CONFIG_TPM" = "y" ]; then tpmr.sh destroy "$TPM_NVRAM_SPACE" "$TPM_SIZE" \ - || die "Unable to wipe sealed secret" + || DIE "Unable to wipe sealed secret" fi From a21ce5f6d7a461d9361f95b0b35dd10d6a567b68 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Apr 2026 10:23:35 -0400 Subject: [PATCH 05/12] functions: add Canokey QEMU USB and Nitrokey 3 detection - Add Canokey QEMU USB (20a0:42d4) to detect_usb_security_dongle_branding() with debug logging - Display Nitrokey 3 firmware version in hotpkey_fw_display(); warn when firmware is below the minimum supported version - Re-detect DONGLE_BRAND in cache_gpg_signing_pin() after GPG card is confirmed present; fixes generic 'USB security dongle' label when dongle enumerates after the initial detection in gui-init.sh - Combine DONGLE_BRAND assignment and export into a single line Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 994cb8258..51c1e9fb9 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -489,6 +489,7 @@ pin_color() { # Sources: hotp-verification/src/device.c and targets/qemu.mk # USB Security dongle (OpenPGP smart card) VID:PID table: # 20a0:42b2 Nitrokey 3 (3A Mini / 3A NFC / 3C NFC - all share this PID) +# 20a0:42d4 Canokey QEMU # 20a0:4108 Nitrokey Pro / Pro 2 (Pro and Pro 2 share the same PID) # 20a0:4109 Nitrokey Storage / Storage 2 # 316d:4c4b Librem Key @@ -498,22 +499,34 @@ pin_color() { # 1050:0115 Yubikey 4/5 (OTP+U2F+CCID) - FIDO+CCID # 1050:0404 Yubikey 5 (FIDO+CCID) detect_usb_security_dongle_branding() { + TRACE_FUNC local lsusb_out lsusb_out="$(lsusb)" + DEBUG "lsusb output: $lsusb_out" # Check NK3 (42b2) before the broader 20a0 vendor match if echo "$lsusb_out" | grep -q "20a0:42b2"; then + DEBUG "Detected Nitrokey 3 (20a0:42b2)" echo "Nitrokey 3" + elif echo "$lsusb_out" | grep -q "20a0:42d4"; then + DEBUG "Detected Canokey QEMU (20a0:42d4)" + echo "Canokey" elif echo "$lsusb_out" | grep -q "20a0:4108"; then + DEBUG "Detected Nitrokey Pro (20a0:4108)" echo "Nitrokey Pro" elif echo "$lsusb_out" | grep -q "20a0:4109"; then + DEBUG "Detected Nitrokey Storage (20a0:4109)" echo "Nitrokey Storage" elif echo "$lsusb_out" | grep -q "316d:4c4b"; then + DEBUG "Detected Librem Key (316d:4c4b)" echo "Librem Key" elif echo "$lsusb_out" | grep -q "16d0:21dc"; then + DEBUG "Detected Canokey (16d0:21dc)" echo "Canokey" elif echo "$lsusb_out" | grep -q "1050:"; then + DEBUG "Detected Yubikey (1050:*)" echo "Yubikey" else + DEBUG "No known USB Security dongle detected" echo "USB Security dongle" fi } @@ -542,6 +555,14 @@ hotpkey_fw_display() { local app_ver app_ver="$(echo "$info" | grep "Firmware Secrets App:" | sed 's/.*: *//')" [ -n "$app_ver" ] && extras=" (Secrets App: ${app_ver})" + # Display Nitrokey 3 firmware version - check if below minimum + if [ "$(printf '%s\n' "$fw_ver" "$min_ver" | sort -V | head -1)" != "$min_ver" ]; then + NOTE "$branding firmware: \033[1;33m${fw_ver}\033[0m${extras} (minimum: ${min_ver}, latest known: ${latest_ver}) - upgrade recommended" + else + STATUS_OK "$branding firmware: ${fw_ver}${extras} (minimum: ${min_ver}, latest known: ${latest_ver})" + fi + touch /tmp/hotpkey_fw_shown + return elif echo "$info" | grep -q "Firmware:"; then # Nitrokey Pro / Storage / Librem Key: "Firmware: v0.15" # hotp_verification prefixes lines with a tab; omit ^ so the pattern matches. @@ -749,6 +770,9 @@ cache_gpg_signing_pin() { user_pin_retries=$(echo "$pin_retry_counters" | awk '{print $1}') admin_pin_retries=$(echo "$pin_retry_counters" | awk '{print $3}') + # Re-detect dongle branding after card is detected (may have been too early in gui-init.sh) + export DONGLE_BRAND="$(detect_usb_security_dongle_branding)" + echo >/dev/console 2>/dev/null STATUS "GPG User PIN retries remaining: $(pin_color "$user_pin_retries")${user_pin_retries}\033[0m" STATUS "GPG Admin PIN retries remaining: $(pin_color "$admin_pin_retries")${admin_pin_retries}\033[0m" From a02203e72caf7f3aa9b610c34b2d56a862f9c064 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 31 Mar 2026 21:41:42 -0400 Subject: [PATCH 06/12] boards/qemu, Makefile: update coreboot configs and fix build tracking - Add prod_quiet board variants to QEMU coreboot configs - Fix cpio dependency tracking to correctly rebuild initrd when root files change - Sync board configs and root file references Signed-off-by: Thierry Laurion --- ...port-for-everything-except-build-errors.md | 3 +- .gitignore | 2 + BOARDS_AND_TESTERS.md | 98 +-- CONTRIBUTING.md | 6 +- FAQ.md | 123 +--- Makefile | 36 +- README.md | 602 +----------------- WP_NOTES.md | 23 +- .../UNTESTED_talos-2/UNTESTED_talos-2.config | 2 +- ...-coreboot-fbwhiptail-tpm1-hotp-prod.config | 4 +- ...oot-fbwhiptail-tpm1-hotp-prod_quiet.config | 5 +- .../qemu-coreboot-fbwhiptail-tpm1-hotp.config | 5 +- .../qemu-coreboot-fbwhiptail-tpm1-prod.config | 6 +- ...coreboot-fbwhiptail-tpm1-prod_quiet.config | 97 +++ .../qemu-coreboot-fbwhiptail-tpm1.config | 2 +- ...-coreboot-fbwhiptail-tpm2-hotp-prod.config | 4 +- ...oot-fbwhiptail-tpm2-hotp-prod_quiet.config | 5 +- .../qemu-coreboot-fbwhiptail-tpm2-prod.config | 4 +- ...coreboot-fbwhiptail-tpm2-prod_quiet.config | 98 +++ .../qemu-coreboot-fbwhiptail-tpm2.config | 3 + ...mu-coreboot-whiptail-tpm1-hotp-prod.config | 5 +- .../qemu-coreboot-whiptail-tpm1-hotp.config | 1 + .../qemu-coreboot-whiptail-tpm1-prod.config | 4 +- ...mu-coreboot-whiptail-tpm2-hotp-prod.config | 2 +- .../qemu-coreboot-whiptail-tpm2-hotp.config | 2 +- .../qemu-coreboot-whiptail-tpm2-prod.config | 2 +- .../qemu-coreboot-whiptail-tpm2.config | 3 +- config/variation_to_defconfig.md | 73 +-- doc/build-freshness.md | 152 +++++ modules/coreboot | 12 +- targets/qemu.md | 197 +----- ...MAINTAINED_kgpe-d16_server-whiptail.config | 2 +- .../UNMAINTAINED_kgpe-d16_server.config | 2 +- ...D_kgpe-d16_workstation-usb_keyboard.config | 2 +- .../UNMAINTAINED_kgpe-d16_workstation.config | 2 +- .../UNMAINTAINED_qemu-linuxboot.config | 2 +- 36 files changed, 468 insertions(+), 1123 deletions(-) mode change 100644 => 120000 BOARDS_AND_TESTERS.md mode change 100644 => 120000 FAQ.md mode change 100644 => 120000 WP_NOTES.md create mode 100644 boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config create mode 100644 boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config mode change 100644 => 120000 config/variation_to_defconfig.md create mode 100644 doc/build-freshness.md mode change 100644 => 120000 targets/qemu.md diff --git a/.github/ISSUE_TEMPLATE/bug-report-for-everything-except-build-errors.md b/.github/ISSUE_TEMPLATE/bug-report-for-everything-except-build-errors.md index 576b5aab7..a516b6f0e 100644 --- a/.github/ISSUE_TEMPLATE/bug-report-for-everything-except-build-errors.md +++ b/.github/ISSUE_TEMPLATE/bug-report-for-everything-except-build-errors.md @@ -101,7 +101,8 @@ assignees: '' 4. What version of Heads/coreboot are you running? - Navigate to **Options → System Information** on the running device and paste the **full version string** here (including the git commit hash). - - Alternatively, provide the GitHub commit ID if building from source. + - Alternatively, provide the **exact ROM filename** (e.g. `heads-x230-20260327-202007-my-branch-v0.2.1-42-g0b9d8e4.rom`) — it encodes the build timestamp, branch, and commit and is the fastest way to identify your build. + - Or provide the GitHub commit ID if building from source. 5. In building the rom, where did you get the blobs? - [ ] No blobs required diff --git a/.gitignore b/.gitignore index 720f911aa..296b7091d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.asc *.bad *.bz2 *.cpio @@ -23,3 +24,4 @@ config/*.old crossgcc typescript* result +.claude/ diff --git a/BOARDS_AND_TESTERS.md b/BOARDS_AND_TESTERS.md deleted file mode 100644 index 37fd41977..000000000 --- a/BOARDS_AND_TESTERS.md +++ /dev/null @@ -1,97 +0,0 @@ -General information -== - -- **Intel CPU Generations:** [List of Intel processors](https://en.wikipedia.org/wiki/List_of_Intel_processors) - - **End of Servicing Updates (ESU Date)** [ESU table for Intel processors](https://www.intel.com/content/www/us/en/support/articles/000022396/processors.html) -- **AMD CPU Generations:** [List of AMD processors](https://en.wikipedia.org/wiki/AMD_processors) -- **Transient CPU Vulnerabilities:** [Transient execution CPU vulnerability](https://en.wikipedia.org/wiki/Transient_execution_CPU_vulnerability) - -**Note (as of 2025-05-29):** -- Intel CPUs from the 1st to 7th generations (Nehalem through Kaby Lake) have reached End-of-Life (EOL) status and no longer receive microcode updates. Consequently, these processors remain vulnerable to Spectre Variant 2 (CVE-2017-5715) and related speculative execution vulnerabilities. -- Some 8th generations (Kaby Lake Refresh) also reached EOL per Intel ESU. -- **Those boards names were renamed with EOL_ preceding their board names for users to be hinted by this at download/compilation/testing time** - -While software-based mitigations like Retpoline can reduce exposure to certain speculative execution attacks, their effectiveness is limited without corresponding microcode updates. Therefore, systems utilizing these older CPUs should be considered inherently vulnerable to Spectre Variant 2 and similar threats. - -Only mitigation is to make sure no secret is present in memory (trusted workflow) in parallel of untrusted workflows. -- This implies a single trusted workflow per boot session, ideally without any secrets remaining in memory—for example, running Tails from a live CD without providing it with any disk decryption passphrase. - - Poper OPSEC when running Tails: https://www.anarsec.guide/posts/tails - - The moment a secret resides in memory (e.g., a passphrase or private document), minimize its exposure by limiting its duration—reboot before switching tasks. - - Always prioritize security over convenience. When in doubt, reboot. - - Proper OPSEC for Memory use on QubesOS: https://www.anarsec.guide/posts/qubes/#appendix-opsec-for-memory-use - - Use disposable qubes as if you were running Tails: use distinct disposable qubes and for really short lived tasks: always consider disk decryption key in memory at risk! -**On systems affected by QSB-107 and lacking updated microcode, [any untrusted application running in a qube could potentially exfiltrate sensitive memory content at a rate of as fast as 5.6 KiB/s.](https://comsec.ethz.ch/research/microarch/branch-privilege-injection)** - - -Live list of community supported platform testers per last coreboot/linux version bump -== - -Heads is a community project, where boards under boards/* need to be tested by board owners when coreboot/linux version bumps happen prior of a Pull Request (PR) merge. -This list will be maintained per coreboot/linux version bumps PRs. - -Please see boards/BOARD_NAME/BOARD_NAME.config for HCL details. - ----- - -As per tracking issue for board testers: https://github.com/linuxboot/heads/issues/692, currently built CircleCI boards ROMs are: - -Laptops -== - -xx20 (Sandy Bridge: Intel 2nd Gen CPU) -=== -- [ ] t420 (xx20): @notgivenby @alexmaloteaux @akfhasodh @doob85 -- [ ] x220 (xx20): @srgrint @Thrilleratplay - -xx30 (Ivy Bridge: Intel 3rd Gen CPU) -=== -- [ ] t430 (xx30): @notgivenby @nestire @Thrilleratplay @alexmaloteaux @lsafd @bwachter(iGPU maximized) @shamen123 @eganonoa(iGPU) @nitrosimon @jans23 @icequbes1 (iGPU) @weyounsix (t430-dgpu) -- [ ] w530 (xx30): @eganonoa @zifxify @weyounsix (dGPU: w530-k2000m) @jnscmns (dGPU K1000M) @computer-user123 (w530 / w530 k2000: prefers iGPU) @tlaurion -- [ ] x230 (xx30): @nestire @tlaurion @merge @jan23 @MrChromebox @shamen123 @eganonoa @bwachter @Thrilleratplay @jnscmns -- [ ] x230-fhd/edp variant: @n4ru @computer-user123 (nitro caster board) @Tonux599 @househead @pcm720 (eDP 4.0 board and 1440p display) @doob85 -- [ ] t530 (xx30): @fhvyhjriur @3hhh (See: https://github.com/linuxboot/heads/issues/1682) - -xx4x (Haswell: Intel 4th Gen CPU) -=== -- [ ] t440p: @MattClifton76 @fhvyhjriur @ThePlexus @srgrint @akunterkontrolle @rbreslow -- [ ] w541 (similar of t440p): @gaspar-ilom @ResendeGHF - -xx8x (Kaby Lake Refresh: Intel 8th Gen Mobile : ESU ended 12/31/2024) -=== -- [ ] t480: @gaspar-ilom @doritos4mlady @MattClifton76 @notgivenby @akunterkontrolle -- [ ] t480s: @thickfont @kjkent @HarleyGodfrey @nestire - -Librem -=== -- [ ] Librem 13v2 (Sky Lake: Intel 6th Gen CPU): @JonathonHall-Purism -- [ ] Librem 15v3 (Sky Lake: Intel 6th Gen CPU): @JonathonHall-Purism -- [ ] Librem 15v4 (Kaby Lake: Intel 7th Gen CPU): @JonathonHall-Purism -- [ ] Librem 13v4 (Kaby Lake: Intel 7th Gen CPU): @JonathonHall-Purism -- [ ] Librem 14 (Comet Lake: Intel 10th Gen CPU): @JonathonHall-Purism -- [ ] Librem 11 (Jasper Lake: Intel 11th Gen Atom CPU): @JonathonHall-Purism - -Clevo -=== -- [ ] Nitropad NS50 (Alder Lake: Intel 12th Gen CPU): @daringer -- [ ] Novacustom NV4x (Alder Lake: Intel 12th Gen CPU): @tlaurion @daringer -- [ ] Novacustom v540tu (Meteor Lake: Intel Core Ultra 7 155H, Core Ultra Series 1 – 14th Gen Mobile): @tlaurion @daringer @mkopec -- [ ] Novacustom v560tu (Meteor Lake: Intel Core Ultra 7 155H, Core Ultra Series 1 – 14th Gen Mobile): @tlaurion @daringer @mkopec - - -Desktops / Servers -== -- [ ] Optiplex 7010/9010 SFF/DT (Ivy Bridge: Intel 3rd Gen CPU): @tlaurion(owns DT variant) -- [ ] HP Z220 CMT (Ivy Bridge: Intel 3rd Gen CPU): @d-wid -- [ ] KGPE-D16 (Bulldozer: AMD Family 15h CPU) – dropped in coreboot 4.12: @arhabd @Tonux599 @zifxify -- [ ] Librem L1UM v1 (Broadwell: Intel 5th Gen CPU): @JonathonHall-Purism -- [ ] Librem L1UM v2 (Coffee Lake: Intel 9th Gen CPU): @JonathonHall-Purism -- [ ] Librem mini v1 (Whiskey Lake: Intel 8th Gen CPU : ESU ends 03/31/2026): @JonathonHall-Purism -- [ ] Librem mini v2 (Comet Lake: Intel 10th Gen CPU): @JonathonHall-Purism -- [ ] Talos II (Power9, PPC64LE): @tlaurion (became untested, low community interest despite large investment) - -MSI ---- -- [ ] MSI PRO Z690-A (WIFI) (DDR4): **None** - Board is untested. -- [ ] MSI PRO Z690-A (WIFI) (DDR5): **None** - Board is untested. -- [ ] MSI PRO Z790-P (WIFI) (DDR4): **None** - Board is untested. -- [ ] MSI PRO Z790-P (WIFI) (DDR5): @Tonux599 \ No newline at end of file diff --git a/BOARDS_AND_TESTERS.md b/BOARDS_AND_TESTERS.md new file mode 120000 index 000000000..69794160f --- /dev/null +++ b/BOARDS_AND_TESTERS.md @@ -0,0 +1 @@ +doc/BOARDS_AND_TESTERS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2a797503..30cd7d263 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,9 +56,9 @@ If you're unsure about what kind of issue you're looking at or whether it's an a - Link to related issues or discussions. - Provide a clear description of the changes and their purpose. - Be responsive to feedback and prepared to make adjustments. -- **Important**: All commits to linuxboot/heads (*not heads-wiki!*) must be signed. - - For instructions, see: [Signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) - - If you won't GPG-sign your commits (GitHub signature doesn't count), they will get signed by a maintainer after a successful review, but it's strongly preferred you do it yourself. +- **Important**: All commits to linuxboot/heads (*not heads-wiki!*) must be: + - **GPG-signed** (`git commit -S`). For instructions, see: [Signing commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). If you won't GPG-sign your commits (GitHub signature doesn't count), they will get signed by a maintainer after a successful review, but it's strongly preferred you do it yourself. + - **Signed-off** (`git commit -s`) with a `Signed-off-by:` trailer for [DCO](https://developercertificate.org/) compliance. Commits missing this trailer will fail the DCO check in CI. ## GitHub Repositories diff --git a/FAQ.md b/FAQ.md deleted file mode 100644 index 1e2c1c609..000000000 --- a/FAQ.md +++ /dev/null @@ -1,122 +0,0 @@ -Frequently Asked Questions about Heads -=== - -Why replace UEFI with coreboot? ---- -While Intel's edk2 tree that is the base of UEFI firmware is open source, -the firmware that vendors install on their machines is proprietary and -closed source. Updates for bugs fixes or security vulnerabilities -are at the vendor's convenience; user specific enhancements are likely not -possible; and the code is not auditable. - -UEFI is much more complex than the BIOS that it replaced. It consists of -millions of lines of code and is an entire operating system, -with network device drivers, graphics, USB, TCP, https, etc, etc, etc. -All of these features represents increased "surface area" for attacks, -as well as unnecessary complexity in the boot process. - -coreboot is open source and focuses on just the code necessary to bring -the system up from reset. This minimal code base has a much smaller -surface area and is possible to audit. Additionally, self-help is -possible if custom features are required or if a security vulnerability -needs to be patched. - - -What's wrong with UEFI Secure Boot? ---- -Can't audit it, signing keys are controlled by vendors, -doesn't handle hand off in all cases, depends on possible leaked keys. - - -Why use Linux instead of vboot2? ---- -vboot2 is part of the coreboot tree and is used by Google in the -Chromebook system to provide boot time security by verifying the -hashes on the coreboot payload. This works well for the specialized -Chrome OS on the Chromebook, but is not as flexible as a measured -boot solution. - -By moving the verification into the boot scripts we're able to have -a much flexible verification system and use more common tools like PGP -to sign firmware stages. - - -What about Trusted GRUB? ---- -The mainline grub doesn't have support for TPM and signed kernels, but -there is a Trusted grub fork that does. Due to philosophical differences -the code might not be merged into the mainline. And due to problems -with secure boot (which Trusted Grub builds on), many distributions have -signed insecure kernels that bypass all of the protections secure -boot promised. - -Additionally, grub is closer to UEFI in that it must have device -drivers for all the different boot devices, as well as filesystems. -This duplicates the code that exists in the Linux kernel and has its -own attack surface. - -Using coreboot and Linux as a boot loader allows us to restrict -the signature validation to keys that we control. We also have one code -base for the device drivers in the Linux-as-a-boot-loader as well -as Linux in the operating system. - - -What is the concern with the Intel Management Engine? ---- -"Rootkit in your chipset", "x86 considered harmful", etc - - -How about the other embedded devices in the system? ---- -#goodbios, funtenna, etc. - - -Should we be concerned about the binary blobs? ---- -Maybe. x230 has very few (MRC) since it has native vga init. - - -Why use ancient Thinkpads instead of modern Macbooks? ---- -coreboot support, TPM, nice keyboards, cheap to experiment on. - -How likely are physical presence attacks vs remote software attacks? ---- -Who knows. - - -Defense in depth vs single layers ---- -Yes. - -is it worth doing the hardware modifications? ---- -Depends on your threat model. - - -Should I validate the TPMTOTP on every boot? ---- -Probably. I want to make it also do it at S3. - - -suspend vs shutdown? ---- -S3 is subject to cold boot attacks, although they are harder to -pull off on a Heads system since the boot devices are constrained. - -However, without tpmtotp in s3 it is hard to know if the system is in -a safe state when the xscreensaver lock screen comes up. Is it a fake -to deceive you and steal your login password? Maybe! It wouldn't get -your disk password, which is perhaps an improvement. - - -Disk key in TPM (LUKS TPM Disk Unlock Key) or user passphrase? ---- -Depends on your threat model. With the Disk Unlock Key in the TPM an -attacker would need to have the entire machine (or a backdoor in the TPM) -to get the key and their attempts to unlock it can be rate limited -by the TPM hardware. - -However, this ties the disk to that one machine (without having to -recover and type in the master key), which might be an unacceptable risk -for some users. diff --git a/FAQ.md b/FAQ.md new file mode 120000 index 000000000..61018a488 --- /dev/null +++ b/FAQ.md @@ -0,0 +1 @@ +doc/faq.md \ No newline at end of file diff --git a/Makefile b/Makefile index 860403189..d8d92bd1f 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,20 @@ GIT_STATUS := $(shell \ echo dirty ; \ fi) HEADS_GIT_VERSION := $(shell git describe --abbrev=7 --tags --dirty) +GIT_TIMESTAMP := $(shell git log -1 --format=%cd --date=format:'%Y%m%d-%H%M%S') +GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD | cut -c1-30) +# Release builds: HEAD is exactly on a tag AND working tree is clean. +# Dev builds: any untagged commit, commits ahead of a tag, or dirty tree. +# Dev filenames include timestamp + branch for traceability without +# polluting release filenames. +GIT_IS_RELEASE := $(shell git describe --exact-match --tags HEAD >/dev/null 2>&1 \ + && git diff --exit-code >/dev/null 2>&1 \ + && echo y || echo n) +ifeq ($(GIT_IS_RELEASE),y) +GIT_VERSION_SUFFIX := $(HEADS_GIT_VERSION) +else +GIT_VERSION_SUFFIX := $(GIT_TIMESTAMP)-$(GIT_BRANCH)-$(HEADS_GIT_VERSION) +endif # Override BRAND_NAME to set the name displayed in the UI, filenames, versions, etc. BRAND_NAME ?= Heads @@ -108,12 +122,12 @@ include $(CONFIG) # https://doc.coreboot.org/tutorial/managing_local_additions.html -include $(pwd)/site-local/config -CB_OUTPUT_BASENAME := $(shell echo $(BRAND_NAME) | tr A-Z a-z)-$(BOARD)-$(HEADS_GIT_VERSION) +CB_OUTPUT_BASENAME := $(shell echo $(BRAND_NAME) | tr A-Z a-z)-$(BOARD)-$(GIT_VERSION_SUFFIX) CB_OUTPUT_FILE := $(CB_OUTPUT_BASENAME).rom CB_OUTPUT_FILE_GPG_INJ := $(CB_OUTPUT_BASENAME)-gpg-injected.rom CB_BOOTBLOCK_FILE := $(CB_OUTPUT_BASENAME).bootblock CB_UPDATE_PKG_FILE := $(CB_OUTPUT_BASENAME).zip -LB_OUTPUT_FILE := linuxboot-$(BOARD)-$(HEADS_GIT_VERSION).rom +LB_OUTPUT_FILE := linuxboot-$(BOARD)-$(GIT_VERSION_SUFFIX).rom # Unless otherwise specified, we are building for heads CONFIG_HEADS ?= y @@ -830,8 +844,7 @@ endif $(build)/$(initrd_dir)/tools.cpio: \ $(initrd_bins) \ $(initrd_libs) \ - $(initrd_tools_dir)/etc/config \ - FORCE + $(initrd_tools_dir)/etc/config $(call do-cpio,$@,$(initrd_tools_dir)) @$(RM) -rf "$(initrd_tools_dir)" @@ -840,7 +853,7 @@ $(build)/$(initrd_dir)/tools.cpio: \ # Those defaults can be overriden by cbfs' config.user applied at init through cbfs-init. # To view overriden exports at runtime, simply run 'env' and review CONFIG_ exported variables # To view compilation time board's config; check /etc/config under recovery shell. -$(initrd_tools_dir)/etc/config: FORCE +$(initrd_tools_dir)/etc/config: $(CONFIG) @mkdir -p $(dir $@) $(call do,INSTALL,$(CONFIG), \ export \ @@ -864,12 +877,15 @@ $(initrd_tools_dir)/etc/config: FORCE # board.cpio is built from the board's initrd/ directory and contains # board-specific support scripts. -ifeq ($(wildcard $(pwd)/boards/$(BOARD)/initrd),) +# FORCE ensures the recipe always runs so do-cpio can show "UNCHANGED" when +# content is unchanged (efficient rebuild while maintaining consistent output) +BOARD_INITRD_FILES := $(shell find $(pwd)/boards/$(BOARD)/initrd -type f 2>/dev/null) +ifeq ($(BOARD_INITRD_FILES),) $(build)/$(initrd_dir)/board.cpio: # Only create a board.cpio if the board has a initrd directory cpio -H newc -o "$@" else -$(build)/$(initrd_dir)/board.cpio: FORCE +$(build)/$(initrd_dir)/board.cpio: $(BOARD_INITRD_FILES) FORCE $(call do-cpio,$@,$(pwd)/boards/$(BOARD)/initrd) endif @@ -877,7 +893,11 @@ endif # heads.cpio is built from the initrd directory in the Heads tree # This is heads security policies, executed by board's CONFIG_BOOTSCRIPT at init -$(build)/$(initrd_dir)/heads.cpio: FORCE +# FORCE ensures the recipe always runs so do-cpio can show "UNCHANGED" when +# content is unchanged (efficient rebuild while maintaining consistent output) +# Use find to get file list - regular files only, evaluated at make time +HEADS_INITRD_FILES := $(shell find $(pwd)/initrd -type f 2>/dev/null) +$(build)/$(initrd_dir)/heads.cpio: $(HEADS_INITRD_FILES) FORCE $(call do-cpio,$@,$(pwd)/initrd) # --- FINAL INITRD PACKAGING --- diff --git a/README.md b/README.md index 92b542b4b..a5ab085ca 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ -![Heads booting on an x230](https://user-images.githubusercontent.com/827570/156627927-7239a936-e7b1-4ffb-9329-1c422dc70266.jpeg) +# Heads: the other side of TAILS -Heads: the other side of TAILS -== +![Heads booting on an x230](https://user-images.githubusercontent.com/827570/156627927-7239a936-e7b1-4ffb-9329-1c422dc70266.jpeg) Heads is a configuration for laptops and servers that tries to bring more security to commodity hardware. Among its goals are: @@ -21,582 +20,49 @@ significant frustration. More information is available in [the 33C3 presentation of building "Slightly more secure systems"](https://trmm.net/Heads_33c3). -Documentation -=== -Please refer to [Heads-wiki](https://osresearch.net) for your Heads' documentation needs. - -Contributing -=== -We welcome contributions to the Heads project! Before contributing, please read our [Contributing Guidelines](CONTRIBUTING.md) for information on how to get started, submit issues, and propose changes. - - -Building heads with prebuilt and versioned docker images -== - -Heads now builds with Nix built docker images since https://github.com/linuxboot/heads/pull/1661. - -The short path to build Heads is to do what CircleCI would do (./docker_repro.sh under heads git cloned directory): -- Install Docker (docker-ce) for your OS by following Docker's official installation instructions: https://docs.docker.com/engine/install/ -- run `./docker_repro.sh make BOARD=XYZ` - -Note: `./docker_repro.sh` is the canonical, reproducible way to build and test Heads. The `docker_local_dev.sh` helper is intended for developers who need to modify the local image built from `flake.nix`/`flake.lock` and is not recommended for general testing. - -Important: the supported and tested workflow uses the provided Docker -wrappers (`./docker_repro.sh`, `./docker_local_dev.sh`, or -`./docker_latest.sh`). Host-side installation of QEMU, `swtpm`, or other -QEMU-related tooling is unnecessary for the standard workflow and is not -part of the tested configuration. Only advanced or edge-case workflows -may require installing those tools on the host (see `targets/qemu.md` -for guidance). - -The Docker images produced by our Nix build include QEMU -(`qemu-system-x86_64`), `swtpm` / `libtpms`, `canokey-qemu` (a virtual -OpenPGP smartcard), and other userspace tooling required to build and -test QEMU boards. If you use `./docker_repro.sh` you only need Docker on -the host (for example, `docker-ce`). For KVM acceleration the host -must expose `/dev/kvm` (load `kvm_intel` / `kvm_amd` as appropriate); -our wrapper scripts mount `/dev/kvm` automatically when it exists. - -If you plan to manage disk images or use `qemu-img` snapshots on the -host (outside containers), install the `qemu-utils` package locally -(which provides `qemu-img`). - -Inspecting and cleaning local Docker images ---- - -```bash -# List local images -docker images - -# Inspect a specific image (IDs, digests, repo tags) -docker image inspect - -# Remove a specific image -docker rmi - -# Remove all local images (destructive) -docker rmi -f $(docker images -aq) - -# Remove unused images/containers/networks/build cache (destructive) -docker system prune -a --volumes -``` - -Note: you may need to prefix commands with `sudo` depending on your Docker setup. - -QEMU disk snapshots with `qemu-img` ---- - -If you manage qcow2 disk images on the host, `qemu-img` can create, list, -restore, and delete snapshots. These examples assume a qcow2 disk image: - -```bash -# Create a snapshot -qemu-img snapshot -c clean root.qcow2 - -# List snapshots -qemu-img snapshot -l root.qcow2 - -# Restore (apply) a snapshot -qemu-img snapshot -a clean root.qcow2 - -# Delete a snapshot -qemu-img snapshot -d clean root.qcow2 - -# Optional: create an overlay backed by a base image -qemu-img create -f qcow2 -b base.qcow2 overlay.qcow2 -``` - -If you prefer to run these inside the container, prefix with -`./docker_repro.sh` (for example, `./docker_repro.sh qemu-img snapshot -l root.qcow2`). - -If you do not specify `USB_TOKEN` when running QEMU targets, the container will use the included `canokey-qemu` virtual token by default; set `USB_TOKEN` (or use `hostbus`/`hostport`/`vendorid,productid`) to forward a hardware token instead. - -Docker wrapper helper reference ---- - -Each wrapper now shows its own focused help (only the variables it actually uses). For the complete environment reference, run `docker/common.sh` directly: - -```bash -# Wrapper-specific help -./docker_repro.sh --help -./docker_latest.sh --help -./docker_local_dev.sh --help - -# Full environment variable reference (shared helper) -./docker/common.sh -``` - -The shared helper documents all supported environment variables (opt-ins and opt-outs) and defaults. Wrapper help is intentionally narrower so it only lists variables relevant to that wrapper. - -Wrapper options & environment variables ---- - -**All wrapper scripts** (`./docker_repro.sh`, `./docker_latest.sh`, `./docker_local_dev.sh`): -- `HEADS_MAINTAINER_DOCKER_IMAGE` — override the canonical maintainer's Docker image repository (default: `tlaurion/heads-dev-env`). Use this for local testing or if you maintain a fork. Example: `export HEADS_MAINTAINER_DOCKER_IMAGE="myuser/heads-dev-env"`. This affects reproducibility checks and default image references across all Docker wrapper scripts. - -- `HEADS_CHECK_REPRODUCIBILITY_REMOTE` — specify which remote image to compare against when verifying reproducibility (default: `${HEADS_MAINTAINER_DOCKER_IMAGE}:latest`). Use this to test against a specific tagged version instead of `:latest`. - ```bash - # Compare against a specific version - export HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" - HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh - ``` -- `HEADS_DISABLE_USB=1` — disable automatic USB passthrough and the - automatic USB cleanup (default: `0`). -- `HEADS_X11_XAUTH=1` — force mounting your `${HOME}/.Xauthority` into the container for X11 authentication. When set the helper will bypass programmatic Xauthority generation and mount your `${HOME}/.Xauthority` (if present); if the file is missing the helper will warn and will not attempt automatic cookie creation (GUI may fail). - -`./docker_local_dev.sh`: -- `HEADS_SKIP_DOCKER_REBUILD=1` — skip automatically rebuilding the local image when `flake.nix`/`flake.lock` are dirty -- `HEADS_CHECK_REPRODUCIBILITY=1` — **recommended for verifying reproducible builds**. After building/loading the local image, automatically compares its digest with the published maintainer image to verify reproducibility. Requires network access. By default compares against `${HEADS_MAINTAINER_DOCKER_IMAGE}:latest`. Use `HEADS_CHECK_REPRODUCIBILITY_REMOTE` to specify a different tag (e.g., `v0.2.7`). See the "Verifying reproducibility" section below for detailed examples and expected outputs. -- `HEADS_AUTO_INSTALL_NIX=1` — automatically attempt to download the Nix single-user installer when `nix` is missing (interactive prompt suppressed). - For supply-chain safety the helper will download the installer to a temporary file and print its SHA256; it will NOT execute the installer automatically unless the downloaded installer matches a pinned hash. The helper will also attempt to detect the installer version heuristically (when possible) and suggest the canonical releases URL (for example `https://releases.nixos.org/nix/nix-2.33.2/install.sha256`) so you can fetch the published sha and compare. To verify: +## Documentation - - Preferred: pin a release version (recommended): set `HEADS_NIX_INSTALLER_VERSION` to a release (for example `nix-2.33.2`). The helper will fetch `https://releases.nixos.org/nix/${HEADS_NIX_INSTALLER_VERSION}/install` and `install.sha256` and show both checksums for you to compare. To auto-run in trusted automation, set `HEADS_NIX_INSTALLER_SHA256` to the expected sha256 as well. +The `doc/` directory contains technical reference documentation for the +Heads codebase. Start here: - - Or compute-and-pin locally: run `./docker/fetch_nix_installer.sh --version nix-2.33.2` (or `--url`) to download the installer and print its sha256, then set `HEADS_NIX_INSTALLER_SHA256` to that value for automation. +| Document | What it covers | +| --- | --- | +| [doc/architecture.md](doc/architecture.md) | Component overview: coreboot, Linux payload, initrd, build system, configuration layers | +| [doc/security-model.md](doc/security-model.md) | Trust hierarchy, measured boot, TOTP/HOTP attestation, GPG boot signing, LUKS DUK, fail-closed design | +| [doc/boot-process.md](doc/boot-process.md) | Step-by-step boot flow: /init → gui-init → kexec-select-boot → OS handoff | +| [doc/tpm.md](doc/tpm.md) | PCR assignments, sealing policies, SRTM chain, board-specific TPM variations, developer config reference | +| [doc/ux-patterns.md](doc/ux-patterns.md) | GUI/UX conventions: whiptail wrappers, integrity report, error flows | +| [doc/config.md](doc/config.md) | Board and user configuration system | +| [doc/docker.md](doc/docker.md) | Reproducible build workflow using Docker | +| [doc/qemu.md](doc/qemu.md) | QEMU board targets for development and testing | +| [doc/wp-notes.md](doc/wp-notes.md) | Flash write-protection status per board | +| [doc/BOARDS_AND_TESTERS.md](doc/BOARDS_AND_TESTERS.md) | Supported boards and their maintainers/testers | - Otherwise verify the downloaded installer manually and run it yourself: `sh /path/to/installer --no-daemon`. -- `HEADS_AUTO_ENABLE_FLAKES=1` — automatically enable flakes by writing `experimental-features = nix-command flakes` to `$HOME/.config/nix/nix.conf` (interactive prompt suppressed) -- `HEADS_MIN_DISK_GB` — minimum free disk space in GB required on `/nix` (or `/` if `/nix` missing) for building (default: `50`) -- `HEADS_SKIP_DISK_CHECK=1` — skip the preflight disk-space check -- `HEADS_ALLOW_UNPINNED_LATEST=1` — when set, bypass the interactive warning that using `:latest` in `./docker_latest.sh` is a supply-chain risk (otherwise `:latest` requires confirmation unless `DOCKER_LATEST_DIGEST` is set or the wrapper can fall back to `DOCKER_REPRO_DIGEST` for the maintainer image) -- `DOCKER_REPRO_DIGEST` — pin the image used by `./docker_repro.sh` to an immutable digest: `tlaurion/heads-dev-env@` (recommended for reproducible and secure builds). Note: `DOCKER_REPRO_DIGEST` is *consumed by* `./docker_repro.sh` (via `resolve_docker_image` in `docker/common.sh`) and is the canonical way to pin the repro image for reproducible builds. +For user-facing documentation and guides, see [Heads-wiki](https://osresearch.net). -For details about selecting or forwarding a physical USB token to QEMU -(handled by the `USB_TOKEN` make variable), see `targets/qemu.md`. +## Contributing -Note: when USB passthrough is active the wrappers will detect processes that may be holding a USB token (for example `scdaemon` or `pcscd`). The wrapper will warn and, on interactive shells, give a 3s abort window before attempting to kill those processes to free the token. Set `HEADS_DISABLE_USB=1` to opt out of this automatic cleanup. - -Example: `HEADS_DISABLE_USB=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` - -Using Nix local dev environment / building docker images with Nix -== - -Under QubesOS? -=== -* Setup nix persistent layer under QubesOS (Thanks @rapenne-s !) - * https://dataswamp.org/~solene/2023-05-15-qubes-os-install-nix.html -* Install docker under QubesOS (imperfect old article of mine. Better somewhere?) - * https://gist.github.com/tlaurion/9113983bbdead492735c8438cd14d6cd - -Build docker from nix develop layer locally -=== - -#### Set up Nix and flakes - -* If you don't already have Nix, install it: - * `[ -d /nix ] || sh <(curl -L https://nixos.org/nix/install) --no-daemon` - * `. /home/user/.nix-profile/etc/profile.d/nix.sh` -* Enable flake support in nix - * `mkdir -p ~/.config/nix` - * `echo 'experimental-features = nix-command flakes' >>~/.config/nix/nix.conf` - -Notes on automation and requirements: - -- The `./docker_local_dev.sh` helper will attempt to ensure Nix and flakes are available when you run it interactively. If Nix is missing it can optionally install it for you and prompt to enable flakes; set `HEADS_AUTO_INSTALL_NIX=1` / `HEADS_AUTO_ENABLE_FLAKES=1` to suppress prompts. -- Building the Docker image and populating `/nix` can require significant disk space — we recommend at least **50 GB** free on `/nix` (or `/` if `/nix` is not present). Adjust via `HEADS_MIN_DISK_GB` or skip the check with `HEADS_SKIP_DISK_CHECK=1`. -- The Nix installer requires a downloader; either `curl` or `wget` must be available on the host. The helper will guide you to install one if neither is present. -- For reproducible builds prefer `./docker_repro.sh`; `./docker_local_dev.sh` is intended for development and will rebuild the local image when `flake.nix`/`flake.lock` are dirty (unless `HEADS_SKIP_DOCKER_REBUILD=1`). - -#### Build image - -* Have docker and Nix installed - -* Build nix developer local environment with flakes locked to specified versions - * Manual: `nix --print-build-logs --verbose build .#dockerImage && docker load < result` - * Helper: `./docker_local_dev.sh` will perform a conditional rebuild when `flake.nix`/`flake.lock` are dirty (unless `HEADS_SKIP_DOCKER_REBUILD=1`). - -Using `./docker_local_dev.sh` - -* `./docker_local_dev.sh` is a developer helper that ensures a local Nix-based Docker image (`linuxboot/heads:dev-env`) is available for interactive development. It performs a few preflight checks and interactive prompts to make the process easier: - - Ensures `nix` is installed and **flakes** are enabled; if missing it will prompt to install Nix and enable flakes. Set `HEADS_AUTO_INSTALL_NIX=1` and/or `HEADS_AUTO_ENABLE_FLAKES=1` to suppress prompts and proceed automatically. - - Requires either `curl` or `wget` to fetch the Nix installer; if neither is present the script will print how to install one and abort. - - Checks disk space on `/nix` (or `/` if `/nix` is absent); default minimum is **50 GB** (`HEADS_MIN_DISK_GB=50`) — override or skip the check with `HEADS_SKIP_DISK_CHECK=1`. - - If `flake.nix` or `flake.lock` are dirty (uncommitted changes), the helper will rebuild the local Docker image. To intentionally trigger a rebuild, make and keep changes to `flake.nix` (for example update an input or a harmless comment) or update `flake.lock`, then run `./docker_local_dev.sh`; the helper detects the dirty flake files and will rebuild automatically. To avoid an automatic rebuild, commit or stash your changes or set `HEADS_SKIP_DOCKER_REBUILD=1` to disable the check. - -On some hardened OSes, you may encounter problems with ptrace. -``` - > proot error: ptrace(TRACEME): Operation not permitted -``` -The most likely reason is that your [kernel.yama.ptrace_scope](https://www.kernel.org/doc/Documentation/security/Yama.txt) variable is too high and doesn't allow docker+nix to run properly. -You'll need to set kernel.yama.ptrace_scope to 1 while you build the heads binary. - -``` -sudo sysctl kernel.yama.ptrace_scope #show you the actual value, probably 2 or 3 -sudo sysctl -w kernel.yama.ptrace_scope=1 #setup the value to let nix+docker run properly -``` -(don't forget to put back the value you had after finishing build head) - -Done! - -Your local docker image "linuxboot/heads:dev-env" is ready to use, reproducible for the specific Heads commit used to build it, and will produce ROMs reproducible for that Heads commit ID. - -Jump into nix develop created docker image for interactive workflow -==== -There are three helpers designed for different use cases: - -| Script | Use Case | Reproducibility | When to Use | -|--------|----------|------------------|------------| -| `./docker_repro.sh` | **Canonical reproducible builds** | Pinned to immutable digest | **All users & maintainers**: Standard way to build Heads; matches CircleCI exactly; use for releases and critical builds | -| `./docker_local_dev.sh` | **Developer customization** | Local build may differ if flake changes | **Developers only**: Rebuilds from local `flake.nix`/`flake.lock` when dirty; useful for testing flake changes; use `HEADS_CHECK_REPRODUCIBILITY=1` to verify against published version | -| `./docker_latest.sh` | **Convenience** | Defaults to reproducible digest; may be unpinned if no digest is available | **Testing/convenience**: Uses latest published image; by default falls back to the reproducible digest (`DOCKER_REPRO_DIGEST`) when available (no confirmation needed). Runs unpinned only when no digest is configured, in which case it requires confirmation unless `HEADS_ALLOW_UNPINNED_LATEST=1` or `DOCKER_LATEST_DIGEST` is set. | - -**Recommendation by role**: -- **End users & QA**: Use `./docker_repro.sh` for all builds (ensures reproducibility and security) -- **Developers**: Use `./docker_local_dev.sh` when iterating on the build system or Nix flake, but verify reproducibility with `HEADS_CHECK_REPRODUCIBILITY=1` before committing -- **Maintainers**: Use `./docker_repro.sh` for official releases; use the maintenance workflow in [Maintenance notes on docker image](#maintenance-notes-on-docker-image) when updating the Docker image base version - -**Examples**: - -Use `./docker_repro.sh` for canonical, reproducible builds: -```bash -./docker_repro.sh make BOARD=x230-hotp-maximized -./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run -``` - -Use `./docker_local_dev.sh` when developing with the Nix flake (verify reproducibility before committing): -```bash -# Modify flake.nix for testing -./docker_local_dev.sh make BOARD=nitropad-nv41 - -# Before committing, verify the build is reproducible -HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh make BOARD=nitropad-nv41 -``` - -If you are already inside the container interactively, run `make BOARD=board_name` as usual. - -One such useful example is to build and test qemu board roms and test them through qemu/kvm/swtpm provided in the docker image. -Please refer to [qemu documentation](targets/qemu.md) for more information. - -Eg: -``` -./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 # Build rom, export public key to emulated usb storage from qemu runtime -./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=~/pubkey.asc inject_gpg # Inject pubkey into rom image -./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 USB_TOKEN=Nitrokey3NFC PUBKEY_ASC=~/pubkey.asc ROOT_DISK_IMG=~/qemu-disks/debian-9.cow2 INSTALL_IMG=~/Downloads/debian-9.13.0-amd64-xfce-CD-1.iso run # Install -``` - -Alternatively, you can use locally built docker image to build a board ROM image in a single call **but do not expect reproducible builds if not using versioned docker images as per CircleCI as per usage of `./docker_repro.sh`** - -Eg: -`./docker_local_dev.sh make BOARD=nitropad-nv41` +We welcome contributions to the Heads project! Before contributing, please read our [Contributing Guidelines](CONTRIBUTING.md) for information on how to get started, submit issues, and propose changes. +## Building Heads -Building with the published Docker image (recommended for reproducible builds) -==== +Heads builds inside a versioned Docker image. The supported and tested workflow uses the +provided Docker wrappers — no host-side QEMU or swtpm installation is needed. -The canonical, reproducible way to build Heads is to use `./docker_repro.sh`, which automatically pulls the pinned Docker image digest from `docker/DOCKER_REPRO_DIGEST` and ensures your builds match the CI environment exactly. +**Quick start** (requires [Docker CE](https://docs.docker.com/engine/install/)): -**For users**: ```bash ./docker_repro.sh make BOARD=x230-hotp-maximized ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run ``` -This will: -1. Resolve the canonical image digest from `docker/DOCKER_REPRO_DIGEST` (immutable, pinned to a specific version) -2. Pull the image if not present locally -3. Execute your build inside that exact Docker environment -4. Guarantee reproducibility: your ROM output will match official CircleCI builds for that commit - -**About the published image**: -- **Repository**: `tlaurion/heads-dev-env` on Docker Hub is the maintainer's canonical image (configurable via `HEADS_MAINTAINER_DOCKER_IMAGE`) -- **Versioning**: Tagged with version numbers (e.g., `v0.2.7`) for stability; `:latest` is mutable and not recommended -- **Pinning**: The repository file `docker/DOCKER_REPRO_DIGEST` pins an immutable digest (`tlaurion/heads-dev-env@sha256:...`) to ensure reproducibility -- **Trust**: As long as flake.nix and flake.lock are not modified locally, your build will produce identical digests, confirming integrity -- **Fork/Override**: To use a different image repository (e.g., for testing or forks), set `HEADS_MAINTAINER_DOCKER_IMAGE="youruser/your-image"` before running any Docker wrapper script - -Pinning the reproducible image ---- - -- `DOCKER_REPRO_DIGEST` — pin the image used by `./docker_repro.sh` to an immutable digest: `tlaurion/heads-dev-env@`. This environment variable (or the repository file `docker/DOCKER_REPRO_DIGEST`) is *consumed by* `./docker_repro.sh` via `resolve_docker_image()`; pinning ensures reproducible builds and mitigates supply-chain risk from mutable `:latest` tags. - -```bash -./docker_repro.sh make BOARD=x230-hotp-maximized -./docker_repro.sh make BOARD=nitropad-nv41 -``` - -Verifying reproducibility of locally-built Docker images ---- - -**Best practice**: Verify that your locally-built Docker image is reproducible by comparing its digest with the published maintainer image. - -The Heads project maintains the canonical `tlaurion/heads-dev-env` Docker image on Docker Hub (configurable via `HEADS_MAINTAINER_DOCKER_IMAGE` environment variable for forks or testing). As long as you do not modify `flake.nix` or `flake.lock`, your locally-built image **should produce an identical digest** to the published image, demonstrating that your build is fully reproducible. - -#### Quick reference - -| Scenario | Command | -|----------|---------| -| **Check against latest maintainer image** | `HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh` | -| **Check against specific version tag** | `HEADS_CHECK_REPRODUCIBILITY=1 HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" ./docker_local_dev.sh` | -| **Check fork maintainer's image** | `HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env" HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh` | -| **Standalone check (any time)** | `./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7` | - -#### Prerequisites - -You have either: -- Built a local Docker image with `./docker_local_dev.sh` (produces `linuxboot/heads:dev-env`), or -- Built from `nix build .#dockerImage` (results in `result` symlink loadable via `docker load`) - -#### Check reproducibility - -**Method 1: Automated check during build (recommended)** - -Enable reproducibility verification automatically during your build with `HEADS_CHECK_REPRODUCIBILITY=1`: - -```bash -# Verify against the default (maintainer's :latest image) -HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh - -# Example output when digests MATCH (reproducible build): -# === Reproducibility Check === -# Local image (linuxboot/heads:dev-env): sha256:5f890f3d1b6b57f9e567191695df003a2ee880f084f5dfe7a5633e3e8f937479 -# Remote image (tlaurion/heads-dev-env:latest): sha256:5f890f3d1b6b57f9e567191695df003a2ee880f084f5dfe7a5633e3e8f937479 -# ✓ MATCH: Local build is reproducible! -``` - -To test against a **specific version tag** instead of `:latest`: - -```bash -HEADS_CHECK_REPRODUCIBILITY=1 \ - HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" \ - ./docker_local_dev.sh - -# Example output when digests DIFFER (expected for different versions): -# === Reproducibility Check === -# Local image (linuxboot/heads:dev-env): sha256:5f890f3d1b6b57f9e567191695df003a2ee880f084f5dfe7a5633e3e8f937479 -# Remote image (tlaurion/heads-dev-env:v0.2.6): sha256:75af4c816a4a92ebdd0030c2e56ebf23c066858e08145ec1cc64a9e750a0031d -# ✗ MISMATCH: Local build differs from remote -# (This is expected if Nix/flake.lock versions differ or if uncommitted changes exist) -``` - -Note: Docker images can have two different identifiers: a local image ID and a registry manifest digest. If a local image has no `RepoDigests` entry, the reproducibility check will compare image IDs (and may pull the remote tag) instead of manifest digests to avoid false mismatches. This can happen for locally built images that have not been pulled from a registry. - -**Method 2: Standalone reproducibility check** - -Use the provided reproducibility checker script to compare hashes at any time: - -```bash -# Compare your local dev image with a published version -./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7 - -# Output (example of a match): -# ✓ SUCCESS: Digests match! -# Your local build is reproducible and identical to tlaurion/heads-dev-env:v0.2.7 -``` - -**Method 3: Manual digest inspection** - -Manually inspect the digest: - -```bash -# Get the digest of your local image (after docker load) -docker inspect --format='{{.Id}}' linuxboot/heads:dev-env -# Output: sha256:8ae7744cc8b4ff0e959aa6dfeeb40dbd40d20ac6fa1f7071dd21ec0c2d0f9f41 - -# Compare with the published image (will pull if needed) -docker pull tlaurion/heads-dev-env:v0.2.7 -docker inspect --format='{{.Id}}' tlaurion/heads-dev-env:v0.2.7 -# Output: sha256:8ae7744cc8b4ff0e959aa6dfeeb40dbd40d20ac6fa1f7071dd21ec0c2d0f9f41 -``` - -#### When digests should match - -✓ **Digests match** → Your build is **reproducible and trustworthy**; matches the maintainer's published image for that Nix snapshot. - -Your locally-built image **will** produce an identical digest to the published image when: -- `flake.nix` and `flake.lock` are **not modified** (i.e., repository is clean relative to these files) -- The same Nix version and dependencies are used -- Build runs on the same Nix store state - -✗ **Digests differ** → Expected in these cases: - -- You have uncommitted changes in `flake.nix` or `flake.lock` -- Different Nix version or Nix dependencies resolved differently on your system -- Using a different `nixpkgs` version than the locked one in `flake.lock` - -#### Trust model - -The `tlaurion/heads-dev-env` image on Docker Hub is the **maintainer's canonical build** and serves as the source of truth for reproducibility. By verifying that your locally-built image produces the same digest as the published `v0.2.7` (or current version), you confirm: - -1. **No tampering**: Your build environment has not been compromised -2. **Reproducibility**: The Heads build system is deterministic for your specific Nix snapshot -3. **Auditability**: You can map your build back to a specific published, reviewed version - -**Recommendation**: Always pin to a specific version tag (e.g., `tlaurion/heads-dev-env:v0.2.7`) rather than `:latest`, and verify the digest matches the published value before using it for critical builds. - -Maintenance notes on docker image -=== - -To update the Docker image to a new version (e.g., vx.y.z), follow these steps. This ensures reproducible builds with immutable digests. - -``` -# Set variables -docker_version="vx.y.z" -docker_hub_repo="tlaurion/heads-dev-env" - -# Update pinned packages to latest if needed, modify flake.nix as required -nix flake update - -# Commit flake changes -git add flake.nix flake.lock -git commit --signoff -m "Bump nix develop based docker image to $docker_version" - -# Verify reproducibility: ensure the local build matches (no further changes to flake files) -nix develop --ignore-environment --command true - -# Build the new Docker image -nix build .#dockerImage -docker load < result - -# Verify you can extract the digest (for fully reproducible builds, flake.nix/flake.lock must be committed) -docker inspect --format='{{.Id}}' linuxboot/heads:dev-env - -# Tag the image with the new version -docker tag linuxboot/heads:dev-env "$docker_hub_repo:$docker_version" - -# Push the new version to Docker Hub (requires push access) -docker push "$docker_hub_repo:$docker_version" - -# Capture the digest of the pushed image (use --yes to auto-pull) -new_digest=$(./docker/get_digest.sh -y "$docker_hub_repo:$docker_version" | tail -n1) -prev_digest=$(grep '^[^#]' docker/DOCKER_REPRO_DIGEST | head -n1) - -# Update the digest in the repository file -sed -i "s|$prev_digest|$new_digest|" docker/DOCKER_REPRO_DIGEST - -# Update the version comment in the repository file -sed -i "s|# Version: .*|# Version: $docker_version|" docker/DOCKER_REPRO_DIGEST - -# Update .circleci/config.yml to use the new digest and add version comments -# The first -e removes existing "# Docker image" comment lines. The second -e inserts a -# fresh "# Docker image: $docker_hub_repo:$docker_version" comment immediately above the -# matching "- image: $docker_hub_repo@" line while preserving indentation. -sed -i -e "/^[[:space:]]*# Docker image: /d" -e "/^[[:space:]]*- image: ${docker_hub_repo//\//\\/}@/ s|^\([[:space:]]*\)\(- image: ${docker_hub_repo//\//\\/}@\)|\\1# Docker image: $docker_hub_repo:$docker_version\n\\1\\2|" .circleci/config.yml - -# Commit the digest and config changes -git add docker/DOCKER_REPRO_DIGEST .circleci/config.yml -git commit --signoff -m "Pin docker image to digest for $docker_version" - -# Push the branch and create a PR for testing with CircleCI -git push origin docker/squash-docker-changes - -# After PR is merged and tested: -# Tag the tested version as latest (optional; use with caution, prefer explicit versioning) -# docker tag "$docker_hub_repo:$docker_version" "$docker_hub_repo:latest" -# docker push "$docker_hub_repo:latest" -``` - -**Maintainer checklist**: -1. **Reproducibility**: Before pushing, verify `nix build .#dockerImage` produces a deterministic result (flake.nix and flake.lock must be committed and clean). -2. **Digest verification**: After pushing, use `./docker/check_reproducibility.sh` to verify local and remote digests match, confirming the build is reproducible. -3. **Supply chain**: Pin the digest in `docker/DOCKER_REPRO_DIGEST` and `.circleci/config.yml` to ensure all builds reference an immutable, auditable image. -4. **Documentation**: Update the version comment in `docker/DOCKER_REPRO_DIGEST` so users know which image version is pinned. -5. **User migration**: When releasing a new version, communicate the new digest and version to users via release notes. +For full details — wrapper scripts, Nix local dev, reproducibility verification, and +maintainer workflow — see **[doc/docker.md](doc/docker.md)**. -**For forks and alternate maintainers**: -If you maintain a fork or want to test with a different Docker image repository, set `HEADS_MAINTAINER_DOCKER_IMAGE` before running any wrapper script: -```bash -# Example: use your own Docker image repository -export HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env" +For QEMU board testing see **[doc/qemu.md](doc/qemu.md)**. -# Now all scripts will reference your repository -./docker_local_dev.sh make BOARD=x230 -HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh +## General notes on reproducible builds -# Reproducibility check will compare against youruser/heads-dev-env:latest -# resolve_docker_image will use youruser/heads-dev-env as the base image -``` - -Maintenance tip: The repository file `docker/DOCKER_REPRO_DIGEST` pins the canonical reproducible image used by `./docker_repro.sh`, ensuring immutable, secure builds. - -Acceptable formats include `sha256:<64-hex>`, `sha256-<64-hex>` (normalized to `sha256:`), or just `<64-hex>` (normalized to `sha256:`). The helper will normalize these formats and produce an image reference like `tlaurion/heads-dev-env@sha256:`. - -If you need to pin the convenience `./docker_latest.sh` wrapper, set the `DOCKER_LATEST_DIGEST` environment variable locally; we do not maintain a `docker/DOCKER_LATEST_DIGEST` file in the repository because 'latest' is a user-level convenience and should be explicitly chosen. When `DOCKER_LATEST_DIGEST` is unset, `./docker_latest.sh` may fall back to `DOCKER_REPRO_DIGEST` only when the base image matches the maintainer repo; otherwise it will prompt before using an unpinned `:latest` unless `HEADS_ALLOW_UNPINNED_LATEST=1` is set in the environment. - -Example: obtain the immutable digest for a published image and use it to force `docker_latest.sh` to use an immutable image: - -```bash -# 1) Obtain the digest for a published image (exact repo:name:tag form is required) -# -# Tip: inspect tags on Docker Hub: https://hub.docker.com/layers/tlaurion/heads-dev-env/ -# Click a tag to see details (Content type, Digest (sha256:...), Size, Last updated). -# Use the shown tag name with docker pull, e.g.: -# docker pull tlaurion/heads-dev-env:v0.2.7 -# -# Example: pull the image and then obtain its digest locally -# docker pull tlaurion/heads-dev-env:v0.2.7 -# ./docker/get_digest.sh tlaurion/heads-dev-env:v0.2.7 -# -# Or: query the registry for the digest and optionally pull it when prompted -# ./docker/get_digest.sh tlaurion/heads-dev-env:v0.2.7 -# (the script will show the remote digest and ask if you want to pull the image to create a local repo@digest) -# -# Use -y to auto-pull and return the digest in one go: -# ./docker/get_digest.sh -y tlaurion/heads-dev-env:v0.2.7 - -./docker/get_digest.sh tlaurion/heads-dev-env:v0.2.7 -# Output (example): tlaurion/heads-dev-env@sha256:50a9110c...\nsha256:50a9110c... - -# 2) If the image is not present locally, the helper will offer to pull it so a local repo@digest is available. -# Use '-y' / '--yes' to skip the interactive prompt and pull automatically. -./docker/get_digest.sh -y tlaurion/heads-dev-env:latest - -# 3) Export the raw digest into the env var expected by the wrapper -export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:latest | tail -n1) - -# 4) Run the convenience wrapper using the pinned digest -DOCKER_LATEST_DIGEST=$DOCKER_LATEST_DIGEST ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 - -Note: when a digest is discovered, helpers print a concise summary to help auditing, for example: - - Image: tlaurion/heads-dev-env@sha256:50a9... - Digest: sha256:50a9... - Resolved from: local|registry API|env|file - Tip: export DOCKER_LATEST_DIGEST=sha256:50a9... - -This makes it easy to copy/pin digests or verify provenance. - -If you want to change what `./docker_latest.sh` uses as the "latest" image: -- For a temporary override: run `./docker/pin-and-run.sh -- ./docker_latest.sh ` to run the wrapper pinned to a specific digest. -- To set a local convenience env: `export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:vX.Y.Z | tail -n1)`. -- To change the canonical fallback used by the project: edit `docker/DOCKER_REPRO_DIGEST` with the desired digest and commit the change. - -# Convenience: helper to obtain a digest and run a wrapper pinned to that digest -# Example: obtains digest and runs the 'latest' wrapper pinned to that digest (explicit wrapper is recommended) -./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 -# Auto-pull and run (non-interactive) -./docker/pin-and-run.sh -y tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 - -# Shortcut: omit the wrapper and just provide the command — the helper will use the default './docker_latest.sh' -./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2 - -# Explicit wrapper flag: use -w/--wrapper to avoid ambiguity -./docker/pin-and-run.sh -w ./docker_repro.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2 - - -``` - -Alternative (manual) commands without the helper script: - -```bash -docker pull tlaurion/heads-dev-env:latest -# prints full repo@digest (if available) -docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest -# to get only the digest portion: -docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest | cut -d'@' -f2 -``` - -Notes: some registries or Docker versions may require `docker manifest inspect` or `skopeo inspect` to obtain an authoritative digest; the helper script tries `docker inspect` first, then `docker manifest inspect` when available. - -Update the appropriate file after publishing a new image to keep the repo in sync. - -Notes: -- Local builds can use ":latest" tag, which will use latest tested successful CircleCI run -- To reproduce CircleCI results, make sure to use the same versioned tag declared under .circleci/config.yml's "image:" - - - -General notes on reproducible builds -=== In order to build reproducible firmware images, Heads builds a specific version of gcc and uses it to compile the Linux kernel and various tools that go into the initrd. Unfortunately this means the first step is a @@ -631,8 +97,7 @@ although there Heads can `kexec` into any Linux or [multiboot](https://www.gnu.org/software/grub/manual/multiboot/multiboot.html) kernel. -Notes: ---- +### Notes * Building coreboot's cross compilers can take a while. Luckily this is only done once. * Builds are finally reproducible! The [reproduciblebuilds tag](https://github.com/osresearch/heads/issues?q=is%3Aopen+is%3Aissue+milestone%3Areproduciblebuilds) tracks any regressions. @@ -643,15 +108,14 @@ See the readme.md file in that folder * Building for the Librem 13 v2/v3 or Librem 15 v3/v4 requires binary blobs to be placed in the blobs/librem_skl folder. See the readme.md file in that folder -QEMU: ---- +### QEMU OS booting can be tested in QEMU using a software TPM. HOTP can be tested by forwarding a USB token from the host to the guest. -For more information and setup instructions, refer to the [qemu documentation](targets/qemu.md). +For more information and setup instructions, refer to the [qemu documentation](doc/qemu.md). + +### coreboot console messages -coreboot console messages ---- The coreboot console messages are stored in the CBMEM region and can be read by the Linux payload with the `cbmem --console | less` command. There is lots of interesting data about the state of the diff --git a/WP_NOTES.md b/WP_NOTES.md deleted file mode 100644 index 802b2aeeb..000000000 --- a/WP_NOTES.md +++ /dev/null @@ -1,22 +0,0 @@ -Flashrom was passed to flashprog under https://github.com/linuxboot/heads/pull/1769 - -Those are notes for @i-c-o-n and others wanting to move WP forward but track issues and users - -The problem with WP is that it is desired but even if partial write protection regions is present, WP is widely unused. - -Some random notes since support is incomplete (depends on chips, really) --QDPI is problematic for WP (same IO2 PIN) - - Might be turned on by chipset for ME read https://matrix.to/#/!pAlHOfxQNPXOgFGTmo:matrix.org/$NCNidoPsw1ze6zv3m2jlPuGuNrdlDQmDcU81If-q55A?via=matrix.org&via=nitro.chat&via=tchncs.de -- WP wanted, WP done, WP unused - - WP wanted https://github.com/flashrom/flashrom/issues/185 https://github.com/linuxboot/heads/issues/985 - - WP done: https://github.com/linuxboot/heads/issues/1741 https://github.com/linuxboot/heads/issues/1546 - - Documented https://docs.dasharo.com/variants/asus_kgpe_d16/spi-wp/ - - WP still unused - -Alternative, as suggested by @i-c-o-n is Chipset Platform Locking (PR0) which is enforced at platform's chipset level for a boot -- This is implemented and enforced on <= Haswell from this PR merged : https://github.com/linuxboot/heads/pull/1373 -- All Intel platforms have PR0 platform locking implemented prior to kexec call with this not yet upstreamed patch applied in all forks https://review.coreboot.org/c/coreboot/+/85278 -- Discussion point under flashrom-> flashprog PR under https://github.com/linuxboot/heads/pull/1769/files/f8eb0a27c3dcb17a8c6fcb85dd7f03e8513798ae#r1752395865 tagging @i-c-o-n - - -Not sure what is the way forward here, but lets keep this file in tree to track improvements over time. diff --git a/WP_NOTES.md b/WP_NOTES.md new file mode 120000 index 000000000..68f63d1b5 --- /dev/null +++ b/WP_NOTES.md @@ -0,0 +1 @@ +doc/wp-notes.md \ No newline at end of file diff --git a/boards/UNTESTED_talos-2/UNTESTED_talos-2.config b/boards/UNTESTED_talos-2/UNTESTED_talos-2.config index 8433881ca..f6245438c 100644 --- a/boards/UNTESTED_talos-2/UNTESTED_talos-2.config +++ b/boards/UNTESTED_talos-2/UNTESTED_talos-2.config @@ -51,7 +51,7 @@ export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y -export CONFIG_BOOTSCRIPT=/bin/talos-init +export CONFIG_BOOTSCRIPT=/bin/talos-init.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_KERNEL_REMOVE="quiet" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config index feff75db9..467fb9fab 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod/qemu-coreboot-fbwhiptail-tpm1-hotp-prod.config @@ -1,5 +1,5 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to @@ -91,7 +91,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config index 5d500ac68..14b268cc4 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config @@ -1,5 +1,6 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail +# This version requires a supported HOTP Security dongle (Nitrokey Pro/Storage or Librem Key) # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to @@ -91,7 +92,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config index 1d5b106a7..8db2985b1 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-hotp/qemu-coreboot-fbwhiptail-tpm1-hotp.config @@ -1,5 +1,6 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail +# This version requires a supported HOTP Security dongle (Nitrokey Pro/Storage or Librem Key) # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to @@ -42,7 +43,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -#CONFIG_MSRTOOLS=y +CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements #CONFIG_TPM2_TSS=y diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config index 017c136fc..bcb27d163 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-prod/qemu-coreboot-fbwhiptail-tpm1-prod.config @@ -1,5 +1,5 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail # # TPM can be used with a qemu software TPM (TIS, 1.2). export CONFIG_COREBOOT=y @@ -40,7 +40,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements #CONFIG_TPM2_TSS=y @@ -89,7 +89,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config new file mode 100644 index 000000000..14ee259d9 --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config @@ -0,0 +1,97 @@ +# Configuration for building a coreboot ROM that works in +# the qemu emulator in graphical mode thanks to FBWhiptail +# +# TPM can be used with a qemu software TPM (TIS, 1.2). +export CONFIG_COREBOOT=y +export CONFIG_COREBOOT_VERSION=25.09 +export CONFIG_LINUX_VERSION=6.1.8 + +CONFIG_COREBOOT_CONFIG=config/coreboot-qemu-tpm1-prod.config +CONFIG_LINUX_CONFIG=config/linux-qemu.config + +#Enable only one RESTRICTED/BASIC boot modes below to test them manually (we cannot inject config under QEMU (no internal flashing) +#export CONFIG_RESTRICTED_BOOT=y +#export CONFIG_BASIC=y + +#Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) +#export CONFIG_HAVE_GPG_KEY_BACKUP=y + +#On-demand hardware support (modules.cpio) +CONFIG_LINUX_USB=y +CONFIG_LINUX_E1000=y +#CONFIG_MOBILE_TETHERING=y +#Runtime on-demand additional hardware support (modules.cpio) +export CONFIG_LINUX_USB_COMPANION_CONTROLLER=y + + + +#Modules packed into tools.cpio +ifeq "$(CONFIG_UROOT)" "y" +CONFIG_BUSYBOX=n +else +#Modules packed into tools.cpio +CONFIG_CRYPTSETUP2=y +CONFIG_FLASHPROG=y +CONFIG_FLASHTOOLS=y +CONFIG_GPG2=y +CONFIG_KEXEC=y +CONFIG_UTIL_LINUX=y +CONFIG_LVM2=y +CONFIG_MBEDTLS=y +CONFIG_PCIUTILS=y +#Runtime tools to write to MSR +#CONFIG_MSRTOOLS=y +#Remote attestation support +# TPM2 requirements +#CONFIG_TPM2_TSS=y +#CONFIG_OPENSSL=y +#Remote Attestation common tools +CONFIG_POPT=y +CONFIG_QRENCODE=y +CONFIG_TPMTOTP=y +#HOTP based remote attestation for supported USB Security dongle +#With/Without TPM support +#CONFIG_HOTPKEY=y +#Nitrokey Storage admin tool (deprecated) +#CONFIG_NKSTORECLI=n +#GUI Support +#Console based Whiptail support(Console based, no FB): +#CONFIG_SLANG=y +#CONFIG_NEWT=y +#FBWhiptail based (Graphical): +CONFIG_CAIRO=y +CONFIG_FBWHIPTAIL=y +#Additional tools (tools.cpio): +#SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) +CONFIG_DROPBEAR=y +endif + +#Runtime configuration +#Automatically boot if HOTP is valid +export CONFIG_AUTO_BOOT_TIMEOUT=5 +#TPM2 requirements +#export CONFIG_TPM2_TOOLS=y +#export CONFIG_PRIMARY_KEY_TYPE=ecc +#TPM1 requirements +export CONFIG_TPM=y +#Enable DEBUG output +export CONFIG_DEBUG_OUTPUT=n +export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n +#Enable TPM2 pcap output under /tmp +export CONFIG_TPM2_CAPTURE_PCAP=n +#Enable quiet mode: technical information logged under /tmp/debug.log +export CONFIG_QUIET_MODE=y +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh +#text-based original init: +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh +export CONFIG_BOOT_REQ_HASH=n +export CONFIG_BOOT_REQ_ROLLBACK=n +export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" +export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" +export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm1-prod_quiet" +#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" + +export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" + +BOARD_TARGETS := qemu diff --git a/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config b/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config index bf56caf8d..b6eb72e79 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm1/qemu-coreboot-fbwhiptail-tpm1.config @@ -1,5 +1,5 @@ # Configuration for building a coreboot ROM that works in -# the qemu emulator in console mode thanks to Whiptail +# the qemu emulator in graphical mode thanks to FBWhiptail # # TPM can be used with a qemu software TPM (TIS, 1.2). export CONFIG_COREBOOT=y diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config index f40942c83..1eddb8e09 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod/qemu-coreboot-fbwhiptail-tpm2-hotp-prod.config @@ -41,7 +41,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y @@ -90,7 +90,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config index e310eacdf..84eae4d0d 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config @@ -17,6 +17,7 @@ CONFIG_LINUX_CONFIG=config/linux-qemu.config #Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) #export CONFIG_HAVE_GPG_KEY_BACKUP=y + #On-demand hardware support (modules.cpio) CONFIG_LINUX_USB=y CONFIG_LINUX_E1000=y @@ -41,7 +42,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y @@ -90,7 +91,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config b/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config index c8bbd6838..215b1be82 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-prod/qemu-coreboot-fbwhiptail-tpm2-prod.config @@ -40,7 +40,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y @@ -89,7 +89,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config b/boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config new file mode 100644 index 000000000..531b4f0a4 --- /dev/null +++ b/boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config @@ -0,0 +1,98 @@ +# Configuration for building a coreboot ROM that works in +# the qemu emulator in graphical mode thanks to FBWhiptail +# +# TPM can be used with a qemu software TPM (TIS, 2.0). +export CONFIG_COREBOOT=y +export CONFIG_COREBOOT_VERSION=25.09 +export CONFIG_LINUX_VERSION=6.1.8 + +CONFIG_COREBOOT_CONFIG=config/coreboot-qemu-tpm2-prod.config +CONFIG_LINUX_CONFIG=config/linux-qemu.config + +#Enable only one RESTRICTED/BASIC boot modes below to test them manually (we cannot inject config under QEMU (no internal flashing) +#export CONFIG_RESTRICTED_BOOT=y +#export CONFIG_BASIC=y + +#Enable HAVE_GPG_KEY_BACKUP to test GPG key backup drive (we cannot inject config under QEMU (no internal flashing)) +#export CONFIG_HAVE_GPG_KEY_BACKUP=y + + +#On-demand hardware support (modules.cpio) +CONFIG_LINUX_USB=y +CONFIG_LINUX_E1000=y +#CONFIG_MOBILE_TETHERING=y +#Runtime on-demand additional hardware support (modules.cpio) +export CONFIG_LINUX_USB_COMPANION_CONTROLLER=y + + + +#Modules packed into tools.cpio +ifeq "$(CONFIG_UROOT)" "y" +CONFIG_BUSYBOX=n +else +#Modules packed into tools.cpio +CONFIG_CRYPTSETUP2=y +CONFIG_FLASHPROG=y +CONFIG_FLASHTOOLS=y +CONFIG_GPG2=y +CONFIG_KEXEC=y +CONFIG_UTIL_LINUX=y +CONFIG_LVM2=y +CONFIG_MBEDTLS=y +CONFIG_PCIUTILS=y +#Runtime tools to write to MSR +#CONFIG_MSRTOOLS=y +#Remote attestation support +# TPM2 requirements +CONFIG_TPM2_TSS=y +CONFIG_OPENSSL=y +#Remote Attestation common tools +CONFIG_POPT=y +CONFIG_QRENCODE=y +CONFIG_TPMTOTP=y +#HOTP based remote attestation for supported USB Security dongle +#With/Without TPM support +#CONFIG_HOTPKEY=y +#Nitrokey Storage admin tool (deprecated) +#CONFIG_NKSTORECLI=n +#GUI Support +#Console based Whiptail support(Console based, no FB): +#CONFIG_SLANG=y +#CONFIG_NEWT=y +#FBWhiptail based (Graphical): +CONFIG_CAIRO=y +CONFIG_FBWHIPTAIL=y +#Additional tools (tools.cpio): +#SSH server (requires ethernet drivers, eg: CONFIG_LINUX_E1000E) +CONFIG_DROPBEAR=y +endif + +#Runtime configuration +#Automatically boot if HOTP is valid +export CONFIG_AUTO_BOOT_TIMEOUT=5 +#TPM2 requirements +export CONFIG_TPM2_TOOLS=y +export CONFIG_PRIMARY_KEY_TYPE=ecc +#TPM1 requirements +#export CONFIG_TPM=y +#Enable DEBUG output +export CONFIG_DEBUG_OUTPUT=n +export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n +#Enable TPM2 pcap output under /tmp +export CONFIG_TPM2_CAPTURE_PCAP=n +#Enable quiet mode: technical information logged under /tmp/debug.log +export CONFIG_QUIET_MODE=y +export CONFIG_BOOTSCRIPT=/bin/gui-init.sh +#text-based original init: +#export CONFIG_BOOTSCRIPT=/bin/generic-init.sh +export CONFIG_BOOT_REQ_HASH=n +export CONFIG_BOOT_REQ_ROLLBACK=n +export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" +export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" +export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" +export CONFIG_BOARD_NAME="qemu-coreboot-fbwhiptail-tpm2-prod_quiet" +#export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" + +export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" + +BOARD_TARGETS := qemu diff --git a/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config b/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config index 11c887cb7..b9604d74f 100644 --- a/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config +++ b/boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config @@ -79,6 +79,9 @@ export CONFIG_PRIMARY_KEY_TYPE=ecc export CONFIG_DEBUG_OUTPUT=y export CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y #Enable TPM2 pcap output under /tmp +# When enabled, tpmr writes TPM2 command/response capture to /tmp/tpm0.pcap +# (inside the Heads runtime). This can be inspected with Wireshark to debug +# TPM interaction similarly to a TPM bus sniffer. export CONFIG_TPM2_CAPTURE_PCAP=y #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=n diff --git a/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config b/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config index 964065cd6..b1087e3d0 100644 --- a/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm1-hotp-prod/qemu-coreboot-whiptail-tpm1-hotp-prod.config @@ -1,5 +1,6 @@ # Configuration for building a coreboot ROM that works in # the qemu emulator in console mode thanks to Whiptail +# This version requires a supported HOTP Security dongle (Nitrokey Pro/Storage or Librem Key) # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to @@ -42,7 +43,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements #CONFIG_TPM2_TSS=y @@ -91,7 +92,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm1-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm1-hotp-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config b/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config index 9a2bda98c..43343f326 100644 --- a/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config +++ b/boards/qemu-coreboot-whiptail-tpm1-hotp/qemu-coreboot-whiptail-tpm1-hotp.config @@ -1,5 +1,6 @@ # Configuration for building a coreboot ROM that works in # the qemu emulator in console mode thanks to Whiptail +# This version requires a supported HOTP Security dongle (Nitrokey Pro/Storage or Librem Key) # # TPM can be used with a qemu software TPM (TIS, 1.2). A Librem Key or # Nitrokey Pro can also be used by forwarding the USB device from the host to diff --git a/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config b/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config index c3e421afa..13bdb2412 100644 --- a/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm1-prod/qemu-coreboot-whiptail-tpm1-prod.config @@ -40,7 +40,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -CONFIG_MSRTOOLS=y +#CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements #CONFIG_TPM2_TSS=y @@ -89,7 +89,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm1" +export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm1-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config b/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config index 0fd679f59..6bf43bf69 100644 --- a/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm2-hotp-prod/qemu-coreboot-whiptail-tpm2-hotp-prod.config @@ -90,7 +90,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2-hotp" +export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2-hotp-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config b/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config index 6660907bd..b29f28eba 100644 --- a/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config +++ b/boards/qemu-coreboot-whiptail-tpm2-hotp/qemu-coreboot-whiptail-tpm2-hotp.config @@ -42,7 +42,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -#CONFIG_MSRTOOLS=y +CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y diff --git a/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config b/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config index ab57bb2c1..e6586de8e 100644 --- a/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config +++ b/boards/qemu-coreboot-whiptail-tpm2-prod/qemu-coreboot-whiptail-tpm2-prod.config @@ -89,7 +89,7 @@ export CONFIG_BOOT_REQ_ROLLBACK=n export CONFIG_BOOT_RECOVERY_SERIAL="/dev/ttyS0" export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" -export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2" +export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2-prod" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config b/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config index 17b2905c0..b71d97b17 100644 --- a/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config +++ b/boards/qemu-coreboot-whiptail-tpm2/qemu-coreboot-whiptail-tpm2.config @@ -41,7 +41,7 @@ CONFIG_LVM2=y CONFIG_MBEDTLS=y CONFIG_PCIUTILS=y #Runtime tools to write to MSR -#CONFIG_MSRTOOLS=y +CONFIG_MSRTOOLS=y #Remote attestation support # TPM2 requirements CONFIG_TPM2_TSS=y @@ -92,7 +92,6 @@ export CONFIG_BOOT_KERNEL_ADD="console=ttyS0 console=tty systemd.zram=0" export CONFIG_BOOT_KERNEL_REMOVE="quiet rhgb splash" export CONFIG_BOARD_NAME="qemu-coreboot-whiptail-tpm2" #export CONFIG_FLASH_OPTIONS="flashprog --progress --programmer internal" -#export CONFIG_AUTO_BOOT_TIMEOUT=5 export CONFIG_KEYBOARD_KEYMAP="/usr/lib/kbd/keymaps/i386/qwerty/us.map" diff --git a/config/variation_to_defconfig.md b/config/variation_to_defconfig.md deleted file mode 100644 index f6e10b8ed..000000000 --- a/config/variation_to_defconfig.md +++ /dev/null @@ -1,72 +0,0 @@ -# Variation to defconfig (cleaned) - -This file lists configuration items found to be inconsistent and/or removed when generating defconfig with `make BOARD=XYZ coreboot.save_in_defconfig_format_in_place` helper for different boards. - - -## Questionable configs - -These options are inconsistent across boards and should be reviewed. - -### Global - -```text -CONFIG_USE_OPTION_TABLE=y -CONFIG_STATIC_OPTION_TABLE=y -# CONFIG_USE_PC_CMOS_ALTCENTURY is not set -# CONFIG_DRIVERS_MTK_WIFI is not set -# CONFIG_DRIVERS_INTEL_WIFI is not set -# CONFIG_RAMINIT_ENABLE_ECC is not set -# CONFIG_TIMESTAMPS_ON_CONSOLE is not set -CONFIG_PCI_ALLOW_BUS_MASTER=y -``` - -### Specifics - -#### T480 - -```text -CONFIG_USE_LEGACY_8254_TIMER=y -``` - -## Removed undesirables - -The following lines were removed from specific board defconfig variations. Filenames (when present) are listed above their removed fragments. - -```text -config/coreboot-optiplex-7019_9010-maximized.config -CONFIG_TIMESTAMPS_ON_CONSOLE=y -config/coreboot-optiplex-7019_9010_TXT-maximized.config -IDEM -config/coreboot-qemu-tpm1-prod.config -# CONFIG_INCLUDE_CONFIG_FILE is not set -# CONFIG_CONSOLE_SERIAL is not set -# CONFIG_POST_DEVICE is not set -# CONFIG_POST_IO is not set -CONFIG_PCIEXP_ASPM=y -CONFIG_PCIEXP_HOTPLUG_BUSES=32 -CONFIG_PCIEXP_COMMON_CLOCK=y -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-qemu-tpm1.config -IDEM -config/coreboot-qemu-tpm2-prod.config -IDEM -config/coreboot-qemu-tpm2.config -IDEM -config/coreboot-t420-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-t430-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-t480-maximized.config -CONFIG_USE_LEGACY_8254_TIMER=y -CONFIG_PCIEXP_HOTPLUG=y -config/coreboot-w530-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-x220-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-x230-maximized-fhd_edp.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -config/coreboot-x230-maximized.config -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -# CONFIG_PCI_ALLOW_BUS_MASTER is not set -CONFIG_PCIEXP_HOTPLUG_IO=0x2000 -``` diff --git a/config/variation_to_defconfig.md b/config/variation_to_defconfig.md new file mode 120000 index 000000000..33edf299f --- /dev/null +++ b/config/variation_to_defconfig.md @@ -0,0 +1 @@ +../doc/variation-to-defconfig.md \ No newline at end of file diff --git a/doc/build-freshness.md b/doc/build-freshness.md new file mode 100644 index 000000000..d4fc74b55 --- /dev/null +++ b/doc/build-freshness.md @@ -0,0 +1,152 @@ +# Build Freshness Debugging Guide + +## The Problem + +Changes to source files in `initrd/` or other build dependencies were not being packed into `initrd.cpio.xz`, causing stale artifacts in the final ROM. The test system showed old commit hashes in `/tmp/config` even after rebuilding. + +## initrd.cpio.xz Composition + +The final initrd.cpio.xz is built from **6 separate cpio archives** (Makefile line 794-798): + +| CPIO | Source | Built by | +|------|--------|----------| +| `dev.cpio` | `blobs/dev.cpio` | Static (pre-built) | +| `modules.cpio` | Linux kernel modules | modules/linux | +| `tools.cpio` | Binaries + libraries + **/etc/config** | Makefile | +| `board.cpio` | Board-specific scripts | Makefile | +| `data.cpio` | Configurable data files | Makefile | +| `heads.cpio` | initrd/* scripts | Makefile | + +The final packaging rule: +```makefile +$(build)/$(initrd_dir)/initrd.cpio.xz: $(initrd-y) +``` + +## Build Flow + +### 1. Initrd Build (Makefile) + +``` +tools.cpio: binaries + libraries + /etc/config (from board .config) +board.cpio: boards/BOARD/initrd/* scripts +heads.cpio: initrd/* scripts (oem-factory-reset.sh, etc.) +data.cpio: module data files + +initrd.cpio.xz = cpio-clean(dev.cpio + modules.cpio + tools.cpio + board.cpio + data.cpio + heads.cpio) +``` + +**tools.cpio contains /etc/config**: +- Exports all CONFIG_* variables from board config +- GIT_HASH, GIT_STATUS, CONFIG_BOARD + +### 2. coreboot Build (modules/coreboot) + +``` +.build rule: depends on bzImage + initrd.cpio.xz +``` + +coreboot is configured with `CONFIG_LINUX_INITRD` pointing to initrd.cpio.xz. The initrd is embedded in the Linux kernel payload, not in CBFS. + +### 3. Final Output + +``` +$(BOARD)/$(CB_OUTPUT_FILE) = coreboot-VERSION/board/coreboot.rom (copied and renamed) +$(BOARD)/$(CB_UPDATE_PKG_FILE) = .rom + sha256sum.txt in a zip +``` + +## Dependency Chain + +The build system uses file dependencies + FORCE for consistent output: + +| Target | Dependencies | +|--------|--------------| +| `heads.cpio` | `$(HEADS_INITRD_FILES)` (variable with find results) + FORCE | +| `board.cpio` | `$(BOARD_INITRD_FILES)` (variable with find results) + FORCE | +| `tools.cpio` | `$(initrd_bins)`, `$(initrd_libs)`, `etc/config` | +| `etc/config` | `$(CONFIG)` | +| `initrd.cpio.xz` | `$(initrd-y)` (all cpio components) | +| `coreboot .build` | `bzImage`, `initrd.cpio.xz` | + +**Key insight**: Using `$(shell find ...)` directly in prerequisites causes Make to evaluate the file list ONCE at parse time. Instead, we use variable assignment: +```makefile +HEADS_INITRD_FILES := $(shell find $(pwd)/initrd -type f 2>/dev/null) +$(build)/$(initrd_dir)/heads.cpio: $(HEADS_INITRD_FILES) FORCE +``` + +This ensures the file list is re-evaluated each time Make runs, properly tracking source file changes. + +**Why FORCE?** Make may skip the recipe if it thinks the target is up-to-date based on file timestamps. FORCE ensures the recipe always runs so our do-cpio macro can use `cmp` to check if content actually changed. This provides: +1. **Consistent output** - always shows "CPIO" or "UNCHANGED" +2. **Efficient rebuilds** - actual filesystem write only happens when content differs + +Each target only rebuilds when its dependencies change: +- `cmp` checks if content actually changed before writing output +- Timestamps are preserved when content is identical + +## Verifying Freshness + +### Check if your changes are in the built initrd: + +```bash +# Extract initrd to temp directory +cd /tmp && rm -rf initrd_check && mkdir initrd_check +xz -dc < build/x86/BOARD/initrd.cpio.xz | cpio -idm -D /tmp/initrd_check + +# Check your file +grep "your_pattern" /tmp/initrd_check/path/to/file +``` + +### Check /etc/config (GIT_HASH, CONFIG_*): + +```bash +xz -dc < build/x86/BOARD/initrd.cpio.xz | cpio -idm -D /tmp/initrd_check +cat /tmp/initrd_check/etc/config | grep -E "GIT_HASH|CONFIG_BOARD" +``` + +### List all cpio contents: + +```bash +xz -dc < build/x86/BOARD/initrd.cpio.xz | cpio -it | head -30 +``` + +### Compare timestamps: + +```bash +# Source file +ls -la initrd/bin/oem-factory-reset.sh + +# Built initrd +ls -la build/x86/BOARD/initrd.cpio.xz + +# coreboot ROM +ls -la build/x86/BOARD/coreboot.rom +``` + +If source is newer but initrd.cpio.xz is older, it wasn't rebuilt. + +### Check what Makefile thinks is needed: + +```bash +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-hotp -n +``` + +## Building Fresh + +### Using Docker (Required) + +The build must run inside Docker to ensure proper permissions and dependencies: + +```bash +# Full rebuild +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-hotp + +# Force rebuild of initrd (touch source) +touch initrd/bin/oem-factory-reset.sh +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-hotp + +# Force complete rebuild of board artifacts +rm -rf build/x86/BOARD +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2-hotp +``` + +**Never run `make` directly** - it will fail due to permission issues on the build directory (owned by root from docker container). diff --git a/modules/coreboot b/modules/coreboot index c76e4da8d..2a6c4e53b 100644 --- a/modules/coreboot +++ b/modules/coreboot @@ -135,8 +135,8 @@ CONFIG_COREBOOT_CONFIG ?= config/coreboot-$(BOARD).config CONFIG_COREBOOT_LOCALVERSION ?= "$(BRAND_NAME)-$(HEADS_GIT_VERSION)" CONFIG_COREBOOT_SMBIOS_PRODUCT_NAME ?= $(BOARD) -# Ensure that touching the config file will force a rebuild -$(build)/$(coreboot_dir)/.configured: $(CONFIG_COREBOOT_CONFIG) +# Ensure that touching the config file or initrd will force a rebuild +$(build)/$(coreboot_dir)/.configured: $(CONFIG_COREBOOT_CONFIG) $(build)/$(BOARD)/initrd.cpio.xz # Select the coreboot version to use for the toolchain ifeq "$($(coreboot_module)_toolchain)" "" @@ -155,6 +155,14 @@ endif $(coreboot_module)_configure := \ mkdir -p "$(build)/$(coreboot_dir)"; \ + if [ -f "$(build)/$(coreboot_dir)/coreboot.rom" ]; then \ + initrd_hash=$$(sha256sum $(build)/$(BOARD)/initrd.cpio.xz | cut -d' ' -f1); \ + if [ ! -f "$(build)/$(coreboot_dir)/.initrd_hash" ] || [ "$$(cat $(build)/$(coreboot_dir)/.initrd_hash)" != "$$initrd_hash" ]; then \ + echo "INFO: initrd changed, cleaning coreboot board build dir to force rebuild with new version string"; \ + rm -rf $(build)/$(coreboot_dir)/*; \ + echo "$$initrd_hash" > $(build)/$(coreboot_dir)/.initrd_hash; \ + fi; \ + fi; \ $(call install_config,$(pwd)/$(CONFIG_COREBOOT_CONFIG),$(build)/$(coreboot_dir)/.config); \ sed -i '/^CONFIG_LOCALVERSION/d' $(build)/$(coreboot_dir)/.config; \ echo 'CONFIG_LOCALVERSION=$(CONFIG_COREBOOT_LOCALVERSION)' >> $(build)/$(coreboot_dir)/.config; \ diff --git a/targets/qemu.md b/targets/qemu.md deleted file mode 100644 index 0a7767f17..000000000 --- a/targets/qemu.md +++ /dev/null @@ -1,196 +0,0 @@ -qemu-coreboot-(fb)whiptail-tpmX(-hotp) boards -=== - -The `qemu-coreboot-fbwhiptail-tpm1-hotp` configuration (and their variants) permits testing of most features of Heads. - It requires a supported USB token (which will be reset for use with the VM, do not use a token needed for a - real machine). With KVM acceleration, speed is comparable to a real machine. If KVM is unavailable, - lightweight desktops are still usable. - -Heads is currently unable to reflash firmware within qemu, which means that OEM reset and re-ownership - cannot be fully performed within the VM. Instead, a GPG key can be injected in the Heads image from the - host during the build. - -The TPM and disks for this configuration are persisted in the build/qemu-coreboot-fbwhiptail-tpm1-hotp/ directory by default. - -Bootstrapping a working system -=== - -Important: The supported and tested workflow uses the provided Docker wrappers (`./docker_repro.sh`, `./docker_local_dev.sh`, or `./docker_latest.sh`). Host-side installation of QEMU, `swtpm`, or other QEMU-related tooling is unnecessary and is not part of the standard, supported workflow; only advanced or edge-case scenarios should install those tools on the host (see 'Troubleshooting' below for guidance). - -1. Install Docker - * Install Docker (docker-ce) for your OS by following Docker's official installation guide: https://docs.docker.com/engine/install/ - -Note: the Nix-built Docker images used by `./docker_repro.sh` include -QEMU (`qemu-system-x86_64`), `swtpm` / `libtpms`, `canokey-qemu` (a -virtual OpenPGP smartcard), and other userspace tooling required to -build and test QEMU targets. These images are intended to be -self-contained for QEMU testing; host-focused build instructions -(e.g., building `swtpm` on the host) were removed to avoid -divergence—use the Docker wrappers for the tested workflow. - -If you do not specify `USB_TOKEN` when running QEMU targets, the -container will use the included `canokey-qemu` virtual token by -default. To forward a hardware token from the host, set `USB_TOKEN` or -pass `hostbus`/`hostport`/`vendorid,productid` to the make invocation. - -If you plan to manage disk images or use `qemu-img` snapshots on the -host (outside the container), install the `qemu-utils` package locally -(which provides `qemu-img`). - - -2. Build Heads - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp` -3. Install OS - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp INSTALL_IMG=<~/heads/path_to_iso.iso> run` - * Lightweight desktops (XFCE, LXDE, etc.) are recommended, especially if KVM acceleration is not available (such nested in Qubes OS) - * When running nested in a qube, disable memory ballooning for the qube, or performance will be very poor. - * Include `QEMU_MEMORY_SIZE=6G` to set the guest's memory (`6G`, `8G`, etc.). The default is 4G to be conservative, but more may be needed depending on the guest OS. - * Include `QEMU_DISK_SIZE=30G` to set the guest's disk size, the default is `20G`. -4. Shut down and boot Heads with the USB token attached, proceed with OEM reset - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp USB_TOKEN= run` - * If you do not set `USB_TOKEN`, the included `canokey-qemu` virtual token will be used by default. - * For ``, use one of: - * `NitrokeyPro` - a Nitrokey Pro by VID/PID - * `NitrokeyStorage` - a Nitrokey Storage by VID/PID - * `Nitrokey3NFC` - a Nitrokey 3 by VID:PID - * `LibremKey` - a Librem Key by VID/PID - * `hostbus=#,hostport=#` - indicate a host bus and port (see qemu usb-host) - * `vendorid=#,productid=#` - indicate a device by VID/PID (decimal, see qemu usb-host) - * You _do_ need to export the GPG key to a USB disk, otherwise defaults are fine. - * Head will show an error saying it can't flash the firmware, continue - * Then Heads will indicate that there is no TOTP code yet, at this point shut down (Continue to main menu -> Power off) -5. Get the public key that was saved to the virtual USB flash drive - * `sudo mkdir /media/fd_heads_gpg` - * Attach the image and print the loop device in one step: - - sudo losetup --find --show --partscan ./build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw - - The command prints the loop device used (for example `/dev/loop0`) and the kernel will create partition nodes such as `/dev/loop0p1` and `/dev/loop0p2` when supported. - - Then mount the appropriate partition (usually the second/public partition): - - sudo mount /dev/loop0p2 /media/fd_heads_gpg # adjust based on the loop device reported above - - * Look in `/media/fd_heads_gpg` and copy the most recent public key - * `sudo umount /media/fd_heads_gpg` - * `sudo losetup --detach /dev/loop0` -6. Inject the GPG key into the Heads image and run again - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp PUBKEY_ASC= inject_gpg` - * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp USB_TOKEN=LibremKey PUBKEY_ASC= run` -7. Initialize the TPM - select "Reset the TPM" at the TOTP error prompt and follow prompts -8. Select "Default boot" and follow prompts to sign /boot for the first time and set a default boot option - -You can reuse an already created ROOT_DISK_IMG by passing its path at runtime. -Ex: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 run` - -Note: hardlinks are your friend. You can (should?) have qemu disk images kept somewhere (cp/mv) ~/qemu_img/test.qcow2 and do: - * `cp -alf ~/qemu_img/test.qcow2 ~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2` - -This way, if you accidentally wipe ~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2, the original is kept intact. -Also note that hardlinks share the same underlying data; modifications to one linked copy affect them all, and the filesystem maintains a link count to track how many references exist. - -`cp -alf` is basically creating a hardlink to destination overwriting it, and doesn't cost additional disk space. - -On a daily development cycle, usage looks like: -1. `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 inject_gpg` -2. `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 run` - -The first command builds the latest uncommitted/unsigned changes and injects the public key inside the ROM to be run by the second command. -To test across all qemu variants, one only has to change BOARD name and run the two previous commands, adapting `QEMU_MEMORY_SIZE=1G` or modifying the file directly under build dir to adapt to host resources. - - -Running via Docker wrappers -=== -We provide convenient wrapper scripts at the repository root that encapsulate Docker invocation and automatically handle common host integrations needed for QEMU runs. - -Wrapper comparison ---- - -| Script | Image | Use | -|---|---:|---| -| `docker_latest.sh` | Defaults to pinned digest when available | Convenience: run the latest published image | -| `docker_local_dev.sh` | `linuxboot/heads:dev-env` | Development: use local image built from the flake (rebuilds when flake files are dirty) | -| `docker_repro.sh` | Image pinned from `.circleci/config.yml` | Reproducible builds that match CircleCI | - -What the wrappers handle ---- - -Wrapper options: some runtime behavior is controlled via environment -variables documented in the repository README (see 'Wrapper options & -environment variables'). Wrapper scripts now have focused `--help` output -for their own variables, and `./docker/common.sh` prints the full -environment reference. Important ones are `HEADS_DISABLE_USB` -(set to `1` to disable automatic USB passthrough and cleanup) and -`HEADS_X11_XAUTH` (force mounting your `$HOME/.Xauthority`). - -Make variables such as `USB_TOKEN`, `PUBKEY_ASC`, `INSTALL_IMG`, -`QEMU_MEMORY_SIZE`, `QEMU_DISK_SIZE`, `ROOT_DISK_IMG`, `CPUS` and `V` -are forwarded to the `make` invocation and affect how -`targets/qemu.mk` runs QEMU. See `targets/qemu.mk` for token formats -and examples. - -Note: when USB passthrough is active the wrapper will warn and, on -interactive shells, give a 3s abort window before attempting to kill -processes that hold the token (e.g., `scdaemon`/`pcscd`) to free the -device; set `HEADS_DISABLE_USB=1` to opt out. - -- **KVM passthrough**: when `/dev/kvm` exists on the host the container is run with `/dev/kvm` mounted into the container, enabling KVM-accelerated QEMU. -- **X11 GUI support**: the wrappers mount the X11 socket and programmatically create a temporary Xauthority file (via `mktemp -t heads-docker-xauth-XXXXXX`, or `/tmp/.docker.xauth-` as fallback when mktemp is unavailable) when `xauth` is available; they fall back to mounting `${HOME}/.Xauthority` when needed and set `XAUTHORITY` inside the container so GTK/SDL QEMU windows work. The temp file is cleaned up automatically after `docker run` completes. - - To force mounting your `${HOME}/.Xauthority` regardless of socket detection, set `HEADS_X11_XAUTH=1`. -- **USB passthrough**: when host USB buses exist `/dev/bus/usb` is mounted into the container so VMs can access hardware tokens. To explicitly disable automatic USB passthrough set `HEADS_DISABLE_USB=1`. -- **USB token cleanup**: the wrappers attempt to detect and stop local GPG/toolstack processes (e.g., `scdaemon`, `pcscd`) which might hold USB tokens. Behavior notes: - - If `sudo` can be run without a password the cleanup runs silently. - - The cleanup avoids prompting for a password in non-interactive shells; it will prompt only when running interactively (attached to a TTY). To skip the cleanup entirely set `HEADS_DISABLE_USB=1`. -- **Convenience variables accepted by the wrappers**: `V=1` for verbose make output, `CPUS=N` to set parallelism for builds, and any `make` variables may be passed through to the container command. -- **Argument forwarding**: arguments given to the wrapper are forwarded directly to the container command (no special separator needed). For example: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run`. - -Environment variables reference ---- - -| Variable | Default | Effect | -|---|---:|---| -| `HEADS_DISABLE_USB` | `0` | When `1`, disable automatic USB passthrough and USB cleanup | -| `HEADS_X11_XAUTH` | `0` | When `1`, mount `${HOME}/.Xauthority` into the container (force usage even when a programmatic Xauthority would otherwise be created) | -| `HEADS_SKIP_DOCKER_REBUILD` | `0` | When `1`, skip rebuilding the local Docker image when `flake.nix`/`flake.lock` are dirty | -| `HEADS_AUTO_INSTALL_NIX` | `0` | When `1`, automatically attempt single-user Nix install if `nix` is missing (suppresses prompt) | -| `HEADS_AUTO_ENABLE_FLAKES` | `0` | When `1`, automatically enable flakes by writing to `$HOME/.config/nix/nix.conf` (suppresses prompt) | -| `HEADS_MIN_DISK_GB` | `50` | Minimum free disk in GB required on `/nix` or `/` before attempting rebuild | -| `HEADS_SKIP_DISK_CHECK` | `0` | When `1`, skip the disk-space preflight check | - -Examples ---- - -- Reproducible (uses image version from CircleCI config): - - `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` - - `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=pubkey.asc USB_TOKEN=Nitrokey3NFC inject_gpg` - - `HEADS_DISABLE_USB=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=pubkey.asc run` - - `HEADS_X11_XAUTH=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` - -- Local development image (uses locally built `linuxboot/heads:dev-env`): - - `./docker_local_dev.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2` - -- Published latest image (convenience): - - `./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` - -How I tested these wrappers (smoke checks) ---- - -- Minimal: `source docker/common.sh && build_docker_opts` — should print a short description and show flags such as `--device=/dev/kvm` when KVM is available and `-v /tmp/heads-docker-xauth-XXXXXX:...` (or `-v /tmp/.docker.xauth-:...` as fallback) when Xauthority was created. -- Functional (examples tested by PR author): see the tests in the PR body (Ubuntu, Debian, Fedora installer flows). Consider testing `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` locally to verify KVM+GTK behavior. - -Troubleshooting ---- - -- Quick checks: - - `echo $DISPLAY` — ensure `DISPLAY` is set on the host. - - `command -v xauth` — preferred for programmatic Xauthority cookies. - - `ls -l /dev/kvm` — verify `/dev/kvm` exists and is accessible. - - `groups | grep -q kvm` — confirm your user is in a group with access to KVM (or run with appropriate privileges). - - `source docker/common.sh && build_docker_opts` — inspect the options the wrapper will use without launching Docker. -- GUI issues: prefer installing `xauth` on the host so the wrappers can create a safe programmatic Xauthority file. As a last resort you can run `xhost +SI:localuser:root` (less secure). -- USB/GPG cleanup: if the cleanup is refusing to run due to non-interactive sudo, run the kill steps manually or set `HEADS_DISABLE_USB=1` to skip automatic cleanup. - -Notes ---- -- Ensure you have an X server available on the host; the wrappers forward `DISPLAY` automatically. -- If KVM is available but `/dev/kvm` is missing, load kernel modules (e.g., `kvm`, `kvm_intel`, `kvm_amd`) so `/dev/kvm` appears. diff --git a/targets/qemu.md b/targets/qemu.md new file mode 120000 index 000000000..cbe0d9d4e --- /dev/null +++ b/targets/qemu.md @@ -0,0 +1 @@ +../doc/qemu.md \ No newline at end of file diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config index fba4c743d..ca79a2af1 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server-whiptail/UNMAINTAINED_kgpe-d16_server-whiptail.config @@ -60,7 +60,7 @@ export CONFIG_TPM2_CAPTURE_PCAP=n export CONFIG_QUIET_MODE=y #export CONFIG_BOOTSCRIPT=/bin/generic-init.sh export CONFIG_BOOTSCRIPT=/bin/gui-init.sh -#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh #CONSOLE SELECTION #Single output to OpenBMC diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config index 79bd4b6d8..5ad337b7f 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_server/UNMAINTAINED_kgpe-d16_server.config @@ -52,7 +52,7 @@ export CONFIG_TPM2_CAPTURE_PCAP=n export CONFIG_QUIET_MODE=y #BOOT SCRIPT SELECTION export CONFIG_BOOTSCRIPT=/bin/generic-init.sh -#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh #CONSOLE SELECTION #Single output to OpenBMC diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config index 8c03f28ba..42c1de4eb 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard/UNMAINTAINED_kgpe-d16_workstation-usb_keyboard.config @@ -57,7 +57,7 @@ export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y export CONFIG_BOOTSCRIPT=/bin/gui-init.sh -#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh #CONSOLE SELECTION #Single output to OpenBMC diff --git a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config index 60080756c..49f7d131f 100644 --- a/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config +++ b/unmaintained_boards/UNMAINTAINED_kgpe-d16_workstation/UNMAINTAINED_kgpe-d16_workstation.config @@ -58,7 +58,7 @@ export CONFIG_TPM2_CAPTURE_PCAP=n #Enable quiet mode: technical information logged under /tmp/debug.log export CONFIG_QUIET_MODE=y export CONFIG_BOOTSCRIPT=/bin/gui-init.sh -#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +#export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh #CONSOLE SELECTION #Single output to OpenBMC diff --git a/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config b/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config index bfd3ba65b..ad202d5a6 100644 --- a/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config +++ b/unmaintained_boards/UNMAINTAINED_qemu-linuxboot/UNMAINTAINED_qemu-linuxboot.config @@ -33,7 +33,7 @@ CONFIG_LINUX_ATA=y CONFIG_LINUX_AHCI=y export CONFIG_BOOTSCRIPT=/bin/generic-init.sh -export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery +export CONFIG_BOOTSCRIPT_NETWORK=/bin/network-init-recovery.sh export CONFIG_BOOT_REQ_HASH=n export CONFIG_BOOT_REQ_ROLLBACK=n From 90460365246080599a886b31675ae0af6b094407 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Apr 2026 10:23:31 -0400 Subject: [PATCH 07/12] gui/init: skip integrity report for OEM reset path and improve boot messaging - Skip integrity report when OEM factory reset is called from TPM error menu - Improve boot messaging and fix console output Signed-off-by: Thierry Laurion --- initrd/bin/gui-init.sh | 2 +- initrd/init | 69 +++++++++++++++--------------------------- 2 files changed, 25 insertions(+), 46 deletions(-) diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index d705afcd2..910e22d33 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -1033,7 +1033,7 @@ EOF export INTEGRITY_REPORT_ALREADY_SHOWN=1 ;; o) - oem-factory-reset.sh + INTEGRITY_REPORT_ALREADY_SHOWN=1 oem-factory-reset.sh if preflight_rollback_counter_before_reseal /boot/kexec_rollback.txt "" return; then rollback_preflight_failed="n" BG_COLOR_MAIN_MENU="normal" diff --git a/initrd/init b/initrd/init index b5ce8e2a2..73bfde2f8 100755 --- a/initrd/init +++ b/initrd/init @@ -58,18 +58,6 @@ hwclock -l -s # import global functions . /etc/functions.sh -# Capture coreboot CBMEM console log before Heads extends any PCRs. -# This records everything firmware did (coreboot measurements, TPM init by -# coreboot, CBFS reads) so measuring_trace.log has the full chain from firmware -# through Heads. Written directly to avoid flooding the console via INFO. -if [ "$CONFIG_COREBOOT" = "y" ] && [ -x /bin/cbmem ]; then - { - echo "=== coreboot CBMEM console (captured before Heads PCR extensions) ===" - cbmem -L 2>/dev/null - echo "=== End coreboot CBMEM console ===" - } >>/tmp/measuring_trace.log -fi - # export user related content from cbfs if [ "$CONFIG_COREBOOT" = "y" ]; then /bin/cbfs-init.sh @@ -114,27 +102,27 @@ if [ "$CONFIG_DEBUG_OUTPUT" = "y" ]; then TRACE_FUNC dmesg -n 8 DEBUG "Full debug output enabled from this point: output both in dmesg and on console (equivalent of passing debug to kernel cmdline)" - DEBUG "DO_WITH_DEBUG std_err and std_out will be redirected to /tmp/debug.log" + DEBUG "NOTE: DO_WITH_DEBUG std_err and std_out will be redirected to /tmp/debug.log" fi -# report if we are in quiet mode, tell user measurements logs available under /tmp/measuring_trace.log +# report if we are in quiet mode, tell user measurements logs available under /tmp/debug.log if [ "$CONFIG_QUIET_MODE" = "y" ]; then # check origin of quiet mode setting =y: if it is under /etc/config.user then early cbfs-init outputs are not suppressible # if it is under /etc/config then early cbfs-init outputs are suppressible if grep -q 'CONFIG_QUIET_MODE="y"' /etc/config 2>/dev/null; then - NOTE "Quiet mode enabled from board configuration: refer to /tmp/measuring_trace.log for boot measurement traces" + echo "Quiet mode enabled from board configuration: refer to '/tmp/debug.log' for boot measurements traces" >/dev/tty0 else - NOTE "Runtime applied Quiet mode: refer to /tmp/measuring_trace.log for boot measurement traces past this point" - NOTE "To suppress earlier boot measurement traces, enable CONFIG_QUIET_MODE=y in your board configuration at build time" + echo "Runtime applied Quiet mode: refer to '/tmp/debug.log' for additional boot measurements traces past this point" >/dev/tty0 + echo "To suppress earlier boot measurements traces, enable CONFIG_QUIET_MODE=y in your board configuration at build time." >/dev/tty0 fi # If CONFIG_QUIET_MODE enabled in board config but disabled from Config->Configuration Settings -# WARN that early boot measurements output was suppressed prior of this point +# warn that early boot measurements output was suppressed prior of this point elif [ "$CONFIG_QUIET_MODE" = "n" ]; then # if CONFIG_QUIET_MODE=n in /etc/config.user but CONFIG_QUIET_MODE=y in /etc/config then early cbfs-init outputs are suppressed # both needs to be checked to determine if early boot measurements traces were suppressed if grep -q 'CONFIG_QUIET_MODE="y"' /etc/config 2>/dev/null && grep -q 'CONFIG_QUIET_MODE="n"' /etc/config.user 2>/dev/null; then - NOTE "Early boot measurement traces were suppressed per CONFIG_QUIET_MODE=y in your board configuration at build time (/etc/config)" - NOTE "Runtime applied Quiet mode disabled: refer to /tmp/measuring_trace.log for cbfs-init related traces prior of this point" + echo "Early boot measurements traces were suppressed per CONFIG_QUIET_MODE=y in your board configuration at build time (/etc/config)" >/dev/tty0 + echo "Runtime applied Quiet mode disabled: refer to '/tmp/debug.log' for cbfs-init related traces prior of this point" >/dev/tty0 fi fi @@ -142,11 +130,15 @@ TRACE_FUNC # make sure we have sysctl requirements if [ ! -d /proc/sys ]; then - WARN "BUG: Kernel is missing CONFIG_SYSCTL=y / CONFIG_PROC_SYSCTL=y - runtime kernel tweaks cannot be applied. Please open an issue" + warn "BUG!!! The following requirements to apply runtime kernel tweaks are missing:" + warn "CONFIG_SYSCTL=y" + warn "CONFIG_PROC_SYSCTL=y" + warn "Please open an issue" fi if [ ! -e /proc/sys/vm/panic_on_oom ]; then - WARN "BUG: panic_on_oom could not be enabled - CONFIG_PROC_SYSCTL requirements missing. Please open an issue" + warn "BUG!!! Requirements to setup Panic when under Out Of Memory situation through PROC_SYSCTL are missing (panic_on_oom was not enabled)" + warn "Please open an issue" else DEBUG "Applying panic_on_oom setting to sysctl" echo 1 >/proc/sys/vm/panic_on_oom @@ -171,7 +163,6 @@ fi if [ "$CONFIG_TPM" = "y" ]; then # Initialize tpm2 encrypted sessions here - STATUS "Initializing TPM encrypted session" tpmr.sh startsession fi @@ -190,13 +181,9 @@ export GPG_TTY=/dev/console # Setup recovery serial shell if [ ! -z "$CONFIG_BOOT_RECOVERY_SERIAL" ]; then stty -F "$CONFIG_BOOT_RECOVERY_SERIAL" 115200 - # Run in a subshell so RECOVERY_TTY doesn't leak into init's environment. - # Use <> (read-write open) to avoid EOF/SIGHUP on the read side and so - # recovery() can reopen the TTY fresh for each bash respawn. - ( - RECOVERY_TTY="$CONFIG_BOOT_RECOVERY_SERIAL" - pause_recovery 'Serial console recovery shell' - ) <>"$CONFIG_BOOT_RECOVERY_SERIAL" >&0 2>&0 & + pause_recovery 'Serial console recovery shell' \ + <"$CONFIG_BOOT_RECOVERY_SERIAL" \ + >"$CONFIG_BOOT_RECOVERY_SERIAL" 2>&1 & fi # load USB modules for boards using a USB keyboard @@ -223,14 +210,14 @@ if [ "$boot_option" = "r" ]; then exit elif [ "$boot_option" = "o" ]; then # Launch OEM Factory Reset mode - STATUS "Entering OEM Factory Reset mode" + echo -e "***** Entering OEM Factory Reset mode\n" >/dev/tty0 oem-factory-reset.sh --mode oem # just in case... exit fi if [ "$CONFIG_BASIC" = "y" ]; then - NOTE "BASIC mode: tamper detection disabled" + echo -e "***** BASIC mode: tamper detection disabled\n" >/dev/tty0 fi # export firmware version @@ -255,7 +242,6 @@ fi # Perform board-specific init if present if [ -x /bin/board-init.sh ]; then - STATUS "Performing board-specific initialization" /bin/board-init.sh fi @@ -263,14 +249,14 @@ if [ ! -x "$CONFIG_BOOTSCRIPT" -a ! -x "$CONFIG_BOOTSCRIPT_NETWORK" ]; then recovery 'Boot script missing? Entering recovery shell' else if [ -x "$CONFIG_BOOTSCRIPT_NETWORK" ]; then - STATUS "Network Boot: $CONFIG_BOOTSCRIPT_NETWORK" + echo '***** Network Boot:' $CONFIG_BOOTSCRIPT_NETWORK $CONFIG_BOOTSCRIPT_NETWORK - STATUS "Network Boot completed: $CONFIG_BOOTSCRIPT_NETWORK" + echo '***** Network Boot Completed:' $CONFIG_BOOTSCRIPT_NETWORK # not blocking fi if [ -x "$CONFIG_BOOTSCRIPT" ]; then - STATUS "Normal boot: $CONFIG_BOOTSCRIPT" + echo '***** Normal boot:' $CONFIG_BOOTSCRIPT if [ -x /bin/setsid ] && [ -x /bin/agetty ]; then for console in $CONFIG_BOOT_EXTRA_TTYS; do @@ -278,15 +264,8 @@ else done fi - # Run the boot script under a respawn loop so that a DIE/exit inside - # gui-init (or any script it calls) does not kill PID 1 and oops the - # kernel. cttyhack is NOT exec'd — /init stays as PID 1. - while true; do - cttyhack "$CONFIG_BOOTSCRIPT" - _rc=$? - WARN "$CONFIG_BOOTSCRIPT exited (code $_rc) - respawning" - sleep 2 - done + #Setup a control tty so that all terminals outputs correct tty when tty is called + exec cttyhack "$CONFIG_BOOTSCRIPT" else # wait for boot via network to occur pause_recovery 'Override network boot. Entering recovery shell' From 8fc87af25e3dc9225d3b9bc93c30345ff5589ff8 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Tue, 31 Mar 2026 21:41:29 -0400 Subject: [PATCH 08/12] doc: add comprehensive documentation and update GPG/dongle guides - Add documentation under doc/ for architecture, boot process, building, configuring keys, development, Docker, FAQ, GPG, keys, logging, prerequisites, QEMU, recovery shell, security model, TPM, UX patterns - gpg.md: add GPG Command Requirements section documenting scdaemon PIN caching behaviour and keytocard slot syntax; remove stale example showing ADMIN_PIN_DEF repeated for every subkey - configuring-keys.md: fix key generation step ordering (TPM reset before key generation, LUKS changes first, TOTP/HOTP sealing happens on first normal boot after reset -- not during OEM Factory Reset) Signed-off-by: Thierry Laurion --- doc/BOARDS_AND_TESTERS.md | 97 +++++ doc/architecture.md | 119 ++++++ doc/boot-process.md | 159 ++++++++ doc/build-artifacts.md | 120 ++++++ doc/configuring-keys.md | 175 +++++++++ doc/development.md | 106 ++++++ doc/docker.md | 687 ++++++++++++++++++++++++++++++++++ doc/faq.md | 122 ++++++ doc/gpg.md | 147 ++++++++ doc/keys.md | 115 ++++++ doc/logging.md | 341 ++++++++++++----- doc/prerequisites.md | 60 +++ doc/qemu.md | 209 +++++++++++ doc/recovery-shell.md | 94 +++++ doc/security-model.md | 484 ++++++++++++++++++++++++ doc/tpm.md | 399 ++++++++++++++++++++ doc/ux-patterns.md | 371 ++++++++++++++++++ doc/variation-to-defconfig.md | 72 ++++ doc/wp-notes.md | 22 ++ 19 files changed, 3814 insertions(+), 85 deletions(-) create mode 100644 doc/BOARDS_AND_TESTERS.md create mode 100644 doc/architecture.md create mode 100644 doc/boot-process.md create mode 100644 doc/build-artifacts.md create mode 100644 doc/configuring-keys.md create mode 100644 doc/development.md create mode 100644 doc/docker.md create mode 100644 doc/faq.md create mode 100644 doc/gpg.md create mode 100644 doc/keys.md create mode 100644 doc/prerequisites.md create mode 100644 doc/qemu.md create mode 100644 doc/recovery-shell.md create mode 100644 doc/security-model.md create mode 100644 doc/tpm.md create mode 100644 doc/ux-patterns.md create mode 100644 doc/variation-to-defconfig.md create mode 100644 doc/wp-notes.md diff --git a/doc/BOARDS_AND_TESTERS.md b/doc/BOARDS_AND_TESTERS.md new file mode 100644 index 000000000..37fd41977 --- /dev/null +++ b/doc/BOARDS_AND_TESTERS.md @@ -0,0 +1,97 @@ +General information +== + +- **Intel CPU Generations:** [List of Intel processors](https://en.wikipedia.org/wiki/List_of_Intel_processors) + - **End of Servicing Updates (ESU Date)** [ESU table for Intel processors](https://www.intel.com/content/www/us/en/support/articles/000022396/processors.html) +- **AMD CPU Generations:** [List of AMD processors](https://en.wikipedia.org/wiki/AMD_processors) +- **Transient CPU Vulnerabilities:** [Transient execution CPU vulnerability](https://en.wikipedia.org/wiki/Transient_execution_CPU_vulnerability) + +**Note (as of 2025-05-29):** +- Intel CPUs from the 1st to 7th generations (Nehalem through Kaby Lake) have reached End-of-Life (EOL) status and no longer receive microcode updates. Consequently, these processors remain vulnerable to Spectre Variant 2 (CVE-2017-5715) and related speculative execution vulnerabilities. +- Some 8th generations (Kaby Lake Refresh) also reached EOL per Intel ESU. +- **Those boards names were renamed with EOL_ preceding their board names for users to be hinted by this at download/compilation/testing time** + +While software-based mitigations like Retpoline can reduce exposure to certain speculative execution attacks, their effectiveness is limited without corresponding microcode updates. Therefore, systems utilizing these older CPUs should be considered inherently vulnerable to Spectre Variant 2 and similar threats. + +Only mitigation is to make sure no secret is present in memory (trusted workflow) in parallel of untrusted workflows. +- This implies a single trusted workflow per boot session, ideally without any secrets remaining in memory—for example, running Tails from a live CD without providing it with any disk decryption passphrase. + - Poper OPSEC when running Tails: https://www.anarsec.guide/posts/tails + - The moment a secret resides in memory (e.g., a passphrase or private document), minimize its exposure by limiting its duration—reboot before switching tasks. + - Always prioritize security over convenience. When in doubt, reboot. + - Proper OPSEC for Memory use on QubesOS: https://www.anarsec.guide/posts/qubes/#appendix-opsec-for-memory-use + - Use disposable qubes as if you were running Tails: use distinct disposable qubes and for really short lived tasks: always consider disk decryption key in memory at risk! +**On systems affected by QSB-107 and lacking updated microcode, [any untrusted application running in a qube could potentially exfiltrate sensitive memory content at a rate of as fast as 5.6 KiB/s.](https://comsec.ethz.ch/research/microarch/branch-privilege-injection)** + + +Live list of community supported platform testers per last coreboot/linux version bump +== + +Heads is a community project, where boards under boards/* need to be tested by board owners when coreboot/linux version bumps happen prior of a Pull Request (PR) merge. +This list will be maintained per coreboot/linux version bumps PRs. + +Please see boards/BOARD_NAME/BOARD_NAME.config for HCL details. + +---- + +As per tracking issue for board testers: https://github.com/linuxboot/heads/issues/692, currently built CircleCI boards ROMs are: + +Laptops +== + +xx20 (Sandy Bridge: Intel 2nd Gen CPU) +=== +- [ ] t420 (xx20): @notgivenby @alexmaloteaux @akfhasodh @doob85 +- [ ] x220 (xx20): @srgrint @Thrilleratplay + +xx30 (Ivy Bridge: Intel 3rd Gen CPU) +=== +- [ ] t430 (xx30): @notgivenby @nestire @Thrilleratplay @alexmaloteaux @lsafd @bwachter(iGPU maximized) @shamen123 @eganonoa(iGPU) @nitrosimon @jans23 @icequbes1 (iGPU) @weyounsix (t430-dgpu) +- [ ] w530 (xx30): @eganonoa @zifxify @weyounsix (dGPU: w530-k2000m) @jnscmns (dGPU K1000M) @computer-user123 (w530 / w530 k2000: prefers iGPU) @tlaurion +- [ ] x230 (xx30): @nestire @tlaurion @merge @jan23 @MrChromebox @shamen123 @eganonoa @bwachter @Thrilleratplay @jnscmns +- [ ] x230-fhd/edp variant: @n4ru @computer-user123 (nitro caster board) @Tonux599 @househead @pcm720 (eDP 4.0 board and 1440p display) @doob85 +- [ ] t530 (xx30): @fhvyhjriur @3hhh (See: https://github.com/linuxboot/heads/issues/1682) + +xx4x (Haswell: Intel 4th Gen CPU) +=== +- [ ] t440p: @MattClifton76 @fhvyhjriur @ThePlexus @srgrint @akunterkontrolle @rbreslow +- [ ] w541 (similar of t440p): @gaspar-ilom @ResendeGHF + +xx8x (Kaby Lake Refresh: Intel 8th Gen Mobile : ESU ended 12/31/2024) +=== +- [ ] t480: @gaspar-ilom @doritos4mlady @MattClifton76 @notgivenby @akunterkontrolle +- [ ] t480s: @thickfont @kjkent @HarleyGodfrey @nestire + +Librem +=== +- [ ] Librem 13v2 (Sky Lake: Intel 6th Gen CPU): @JonathonHall-Purism +- [ ] Librem 15v3 (Sky Lake: Intel 6th Gen CPU): @JonathonHall-Purism +- [ ] Librem 15v4 (Kaby Lake: Intel 7th Gen CPU): @JonathonHall-Purism +- [ ] Librem 13v4 (Kaby Lake: Intel 7th Gen CPU): @JonathonHall-Purism +- [ ] Librem 14 (Comet Lake: Intel 10th Gen CPU): @JonathonHall-Purism +- [ ] Librem 11 (Jasper Lake: Intel 11th Gen Atom CPU): @JonathonHall-Purism + +Clevo +=== +- [ ] Nitropad NS50 (Alder Lake: Intel 12th Gen CPU): @daringer +- [ ] Novacustom NV4x (Alder Lake: Intel 12th Gen CPU): @tlaurion @daringer +- [ ] Novacustom v540tu (Meteor Lake: Intel Core Ultra 7 155H, Core Ultra Series 1 – 14th Gen Mobile): @tlaurion @daringer @mkopec +- [ ] Novacustom v560tu (Meteor Lake: Intel Core Ultra 7 155H, Core Ultra Series 1 – 14th Gen Mobile): @tlaurion @daringer @mkopec + + +Desktops / Servers +== +- [ ] Optiplex 7010/9010 SFF/DT (Ivy Bridge: Intel 3rd Gen CPU): @tlaurion(owns DT variant) +- [ ] HP Z220 CMT (Ivy Bridge: Intel 3rd Gen CPU): @d-wid +- [ ] KGPE-D16 (Bulldozer: AMD Family 15h CPU) – dropped in coreboot 4.12: @arhabd @Tonux599 @zifxify +- [ ] Librem L1UM v1 (Broadwell: Intel 5th Gen CPU): @JonathonHall-Purism +- [ ] Librem L1UM v2 (Coffee Lake: Intel 9th Gen CPU): @JonathonHall-Purism +- [ ] Librem mini v1 (Whiskey Lake: Intel 8th Gen CPU : ESU ends 03/31/2026): @JonathonHall-Purism +- [ ] Librem mini v2 (Comet Lake: Intel 10th Gen CPU): @JonathonHall-Purism +- [ ] Talos II (Power9, PPC64LE): @tlaurion (became untested, low community interest despite large investment) + +MSI +--- +- [ ] MSI PRO Z690-A (WIFI) (DDR4): **None** - Board is untested. +- [ ] MSI PRO Z690-A (WIFI) (DDR5): **None** - Board is untested. +- [ ] MSI PRO Z790-P (WIFI) (DDR4): **None** - Board is untested. +- [ ] MSI PRO Z790-P (WIFI) (DDR5): @Tonux599 \ No newline at end of file diff --git a/doc/architecture.md b/doc/architecture.md new file mode 100644 index 000000000..638d36faf --- /dev/null +++ b/doc/architecture.md @@ -0,0 +1,119 @@ +# Heads Architecture + +Heads is a firmware distribution that replaces proprietary BIOS/UEFI with coreboot, a minimal +Linux kernel, and a security-focused initrd. It establishes a hardware root of trust, implements +measured boot via TPM, and verifies the OS boot environment before handing off control. + +See also: [security-model.md](security-model.md), [boot-process.md](boot-process.md), +[tpm.md](tpm.md) — detailed subsystem documentation. + +External reference: [deepwiki.com/linuxboot/heads](https://deepwiki.com/linuxboot/heads) — +validated against code in this repository. + +--- + +## Major components + +```text +┌─────────────────────────────────────────────────────┐ +│ SPI Flash ROM │ +│ ┌──────────────┐ ┌───────────────┐ ┌──────────┐ │ +│ │ coreboot │ │ Linux kernel │ │ initrd │ │ +│ │ (HW init + │→ │ (minimal, │→ │ (boot │ │ +│ │ PCR 2 SRTM) │ │ no initramfs)│ │ scripts)│ │ +│ └──────────────┘ └───────────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ + TPM (PCR values) OS kernel via kexec +``` + +### coreboot + +Replaces vendor firmware. Performs hardware initialization (memory training, PCIe, USB), +extends PCR 2 in the TPM with each firmware stage (bootblock → romstage → ramstage → +payload) as the Static Root of Trust for Measurement (SRTM), and launches the kernel +directly without a second-stage bootloader. + +### Linux kernel (payload) + +A minimal, stripped kernel compiled specifically for Heads. No initramfs — it boots directly +into the Heads initrd. Provides device drivers (TPM, USB, storage, network), filesystem +support, and the platform for the boot scripts. + +### initrd + +The root filesystem that runs at boot. Contains all Heads logic: configuration loading, +TPM operations, GPG verification, whiptail GUI, boot menu, LUKS key injection, and kexec +execution. Source lives in `initrd/`. + +--- + +## initrd subsystems + +| Subsystem | Key files | Purpose | +| --- | --- | --- | +| Init / boot flow | `initrd/init`, `initrd/bin/gui-init` | System initialization and main GUI loop | +| TPM abstraction | `initrd/bin/tpmr` | Unified TPM 1.2 / TPM 2.0 wrapper | +| Boot signing | `initrd/bin/kexec-sign-config` | GPG-sign /boot files, create checksums | +| Boot verification | `initrd/bin/kexec-select-boot` | Verify checksums, select and kexec the OS | +| LUKS key sealing | `initrd/bin/kexec-seal-key` | Seal disk encryption key to TPM | +| TOTP/HOTP | `initrd/bin/seal-totp`, `seal-hotpkey` | Seal attestation secrets to TPM | +| OEM reset | `initrd/bin/oem-factory-reset` | Full re-ownership: GPG, TPM, TOTP, checksums | +| Config GUI | `initrd/bin/config-gui.sh` | Runtime configuration menus | +| Functions lib | `initrd/etc/functions` | Shared utilities: logging, INPUT, TPM helpers | +| GUI lib | `initrd/etc/gui_functions` | Whiptail wrappers, integrity report | + +--- + +## Configuration system + +Three-layer hierarchy: + +1. **`/etc/config`** — Board defaults compiled into the ROM at build time +2. **`/etc/config.user`** — User overrides extracted from CBFS at runtime +3. **`/tmp/config`** — Combined result, sourced during boot + +`combine_configs()` in `initrd/etc/functions` merges these by concatenating +`/etc/config*` into `/tmp/config`. User settings in CBFS take precedence +because they appear last in the concatenation. + +Changes to user configuration are persisted by reflashing the ROM (CBFS operations). + +--- + +## Build system + +The top-level `Makefile` orchestrates: + +- Cross-compiler (`musl-cross-make`, target: `x86_64-linux-musl` or `powerpc64le-linux-musl`) +- Modules (coreboot, Linux, busybox, GPG, cryptsetup, kexec, LVM2, …) +- Six CPIO archives assembled into the initrd: + 1. `dev.cpio` — device nodes + 2. `modules.cpio` — kernel modules + 3. `tools.cpio` — userspace tools + configuration + 4. `board.cpio` — board-specific scripts + 5. `heads.cpio` — security scripts (`CONFIG_HEADS=y`) + 6. `data.cpio` — data files +- Final ROM image: coreboot ROM with Linux + initrd payload embedded + +Reproducible builds are achieved via Nix-pinned Docker images. See [docker.md](docker.md). + +--- + +## Supported architectures + +| Architecture | Target triplet | Example boards | +| --- | --- | --- | +| x86-64 | `x86_64-linux-musl` | ThinkPad, Librem, Dell OptiPlex, QEMU | +| PowerPC 64-bit LE | `powerpc64le-linux-musl` | Raptor Talos II | + +--- + +## Key design principles + +- **No network at boot** — all verification is local; no certificate authorities +- **Hardware root of trust** — coreboot in SPI flash is the trust anchor; coreboot extends measurements into the TPM +- **Fail-closed** — failed verification drops to authenticated recovery shell, not an unverified OS boot +- **Separation of duties** — the key that signs `/boot` lives on a hardware security dongle, never in the ROM +- **Auditability** — all source is open, builds are reproducible, ROM images are verifiable diff --git a/doc/boot-process.md b/doc/boot-process.md new file mode 100644 index 000000000..6491aaf86 --- /dev/null +++ b/doc/boot-process.md @@ -0,0 +1,159 @@ +# Heads Boot Process + +This document describes the complete boot flow from power-on to OS handoff. + +See also: [architecture.md](architecture.md) for component overview, +[tpm.md](tpm.md) for TPM PCR details, [security-model.md](security-model.md) +for the trust model. + +--- + +## Overview + +```text +Power-on + │ + ▼ +coreboot (SPI flash) + │ hardware init, SRTM measurement into PCR 2 + ▼ +Linux kernel (coreboot payload, no initramfs) + │ + ▼ +/init ← first userspace process + │ mount filesystems, load config, combine user overrides + ▼ +/bin/gui-init ← main interactive boot loop + │ TPM preflight, GPG key check, TOTP/HOTP attestation + ▼ +kexec-select-boot + │ verify /boot hashes + GPG signature, rollback counter + ▼ +kexec ← hands off to OS kernel +``` + +--- + +## Stage 1: /init + +`/init` is the first userspace process. It: + +1. Mounts virtual filesystems (`/dev`, `/proc`, `/sys`). +2. Loads board defaults from `/etc/config` and the functions library. +3. Runs `cbfs-init` to extract user configuration from CBFS into `/etc/config.user`. +4. Calls `combine_configs()` to merge all `/etc/config*` files into `/tmp/config`, + then sources `/tmp/config` so all subsequent scripts see the merged settings. +5. Checks for a quick `r` keypress (100 ms timeout) to drop to a recovery shell + before any GUI starts. +6. Execs `cttyhack $CONFIG_BOOTSCRIPT` (default: `/bin/gui-init`), which sets up + a controlling TTY and hands off to the boot script. + +### Config file merge + +```text +/etc/config (ROM, board defaults) +/etc/config.user (CBFS, user overrides) + │ + └─► combine_configs() ─► /tmp/config (runtime, sourced by all scripts) +``` + +User settings appear last in the concatenation and therefore override board +defaults. Changes are persisted by reflashing CBFS. + +--- + +## Stage 2: /bin/gui-init + +`gui-init` is the main interactive boot agent. It runs as an infinite loop and +handles all user interaction until the OS is handed off via kexec. + +### Initialization + +On startup, `gui-init` detects the controlling TTY (set by `cttyhack` in `/init`) +and exports it as `HEADS_TTY` and `GPG_TTY`. This ensures that all interactive +prompts and GPG operations reach the correct terminal regardless of stdout/stderr +redirections. + +### TPM rollback preflight + +Before showing any menu, `gui-init` verifies that the TPM rollback counter is +consistent with `/boot/kexec_rollback.txt`. An inconsistency indicates either a +TPM reset (expected: user must re-seal secrets) or an unexpected state (possible +tampering). On failure, the main menu background is set to error color and the +user is offered recovery options. + +### GPG key check (`check_gpg_key`) + +`gui-init` counts the keys in the GPG keyring. An empty keyring means no `/boot` +signature can be verified. The user must add a key or perform OEM Factory Reset +before booting. + +### TOTP generation (`update_totp`) + +`unseal-totp` retrieves the TOTP secret from TPM NVRAM and generates the current +30-second code. If the unseal fails (PCR mismatch, TPM reset, tampered firmware), +`INTEGRITY_GATE_REQUIRED` is set to `y`, which blocks all subsequent TPM secret +sealing until an integrity check passes. See [security-model.md](security-model.md). + +### HOTP / hardware token check (`update_hotp`) + +If a hardware HOTP token is present (`/bin/hotp_verification`), `gui-init` obtains +the HOTP secret (unsealed from TPM on boards with a TPM; derived from a ROM hash on +boards without one) and asks the token to verify the current code. Result codes: +`0` = success, `4` = wrong code, `7` = not a valid HOTP value. +See [security-model.md](security-model.md#hotp-on-boards-without-a-tpm-rom-hash-mode) +for the no-TPM path. + +### Auto-boot + +If HOTP succeeded and `CONFIG_AUTO_BOOT_TIMEOUT` is set, a countdown starts and +the default boot entry is selected automatically if the user does not intervene. + +### Main menu loop + +`show_main_menu` displays the current date, TOTP code, and HOTP status in the +menu title bar. The background color reflects the current integrity state +(normal / warning / error). Options: default boot, refresh TOTP/HOTP, options +menu, system info, power off. + +--- + +## Stage 3: kexec-select-boot + +Called from the boot menu. Responsible for final verification and OS handoff. + +### TPM2 primary key hash check + +For TPM2 systems, verifies the SHA-256 hash of the TPM2 primary key handle +against `/boot/kexec_primhdl_hash.txt` (if the file exists). A mismatch means +the TPM2 primary key was regenerated without updating the stored hash. + +### Boot hash verification (`verify_global_hashes`) + +`verify_checksums` checks the SHA-256 of every `/boot` file against +`kexec_hashes.txt`, then verifies `kexec.sig` with `gpgv`. A hash mismatch or +invalid signature causes `die` — there is no "boot anyway" path. + +Optionally, root partition hashes are also checked if `CONFIG_ROOT_CHECK_AT_BOOT=y`. + +### Rollback counter verification (`verify_rollback_counter`) + +The TPM monotonic counter index is read from `/boot/kexec_rollback.txt` and the +counter is read from the TPM. The SHA-256 of the counter file is then checked +against the hash stored in `kexec_rollback.txt`. Any discrepancy aborts the boot. + +### OS boot execution (`do_boot`) + +If a TPM-sealed LUKS Disk Unlock Key (DUK) is configured, `kexec-insert-key` +unseals the DUK and injects it into a minimal initrd prepended to the OS initrd. +The OS kernel then finds the key and unlocks LUKS without prompting the user. + +`kexec-boot` performs the final `kexec` system call to hand off to the OS kernel. + +--- + +## Recovery shell + +The recovery shell is an authenticated environment. Entering it extends TPM +PCR 4 with `"recovery"`, permanently invalidating TOTP/HOTP/LUKS unseal for +the rest of the boot session. See [tpm.md](tpm.md). diff --git a/doc/build-artifacts.md b/doc/build-artifacts.md new file mode 100644 index 000000000..c64d5f486 --- /dev/null +++ b/doc/build-artifacts.md @@ -0,0 +1,120 @@ +# Build Artifacts and ROM Filename Convention + +## Output Files + +A Heads build produces the following artifacts per board: + +| File | Purpose | +|------|---------| +| `.rom` | Full ROM image for external or internal flashing | +| `-gpg-injected.rom` | ROM with a GPG public key injected (post key-generation step) | +| `.bootblock` | coreboot bootblock only (board-specific use) | +| `.zip` | Update package: ROM + `sha256sum.txt`, used by `flash-gui.sh` for verified internal upgrades | +| `linuxboot--.rom` | LinuxBoot variant (where applicable) | + +## Filename Format + +The basename follows the pattern: + +``` +-- +``` + +Where `` differs between release and development builds. + +### Release Builds + +Condition: HEAD is exactly on a git tag **and** the working tree is clean. + +``` +heads-x230-v0.2.1.rom +``` + +`BRAND-BOARD-TAG` + +Release filenames are identical to the pre-timestamp convention and safe for +all downstream consumers including LVFS cabinet naming and OEM distribution. + +### Development Builds + +Condition: any untagged commit, commits ahead of a tag, a dirty working tree, +or a non-release branch. + +``` +heads-x230-20260327-202007-tpm_reseal_ux-feat-v0.2.1-42-g0b9d8e4-dirty.rom +``` + +`BRAND-BOARD-YYYYMMDD-HHMMSS-BRANCH-GITDESCRIBE` + +- **`YYYYMMDD-HHMMSS`** — timestamp of the last commit (UTC). Sorts + chronologically in file managers. `flash-gui.sh` reverse-sorts the ROM + list so the newest build appears first. +- **`BRANCH`** — git branch name at build time. Identifies which PR or + feature a binary corresponds to without consulting git. +- **`GITDESCRIBE`** — output of `git describe --abbrev=7 --tags --dirty` + (e.g. `v0.2.1-42-g0b9d8e4-dirty`). Pinpoints the exact commit. + +## Downstream Integration + +### Safe glob patterns + +```bash +# Always safe — board name is always the second component: +heads-${BOARD}-*.rom +heads-${BOARD}-*.zip + +# Breaks for dev builds — tag no longer follows board directly: +heads-${BOARD}-v*.rom # DON'T USE for dev artifact detection +``` + +### Parsing the filename structurally + +Branch names contain hyphens, so splitting on `-` is ambiguous for dev builds. +Parse by anchoring on the timestamp pattern instead: + +```bash +# Extract timestamp from a dev build filename: +basename="heads-x230-20260327-202007-my-feature-v0.2.1-42-gabc1234-dirty" +timestamp=$(echo "$basename" | grep -oP '\d{8}-\d{6}') +``` + +For release builds there is no timestamp; the third field is the tag directly. + +### fwupd / LVFS + +fwupd identifies firmware by **GUID**, not filename. The ROM filename inside +the cabinet (`.cab`) is not parsed for versioning purposes. The cabinet +metadata (``) carries the authoritative version string. + +Release ROM filenames (`heads-x230-v0.2.1.rom`) are unchanged from the +pre-timestamp convention, so existing LVFS submissions are unaffected. + +For LVFS pre-release / testing channels, the dev filename carries enough +information (timestamp + branch + git describe) to identify the exact build +without additional metadata. + +### CI / GitHub Actions + +Workflows that upload or download build artifacts should use the board-anchored +glob (`heads-${BOARD}-*.rom`) rather than the version-anchored form. + +The `.zip` update package follows the same naming convention as the `.rom` and +is the preferred artifact for internal upgrade workflows — it includes an +embedded `sha256sum.txt` that `flash-gui.sh` verifies before flashing. + +### Dasharo and other forks + +Forks that override `BRAND_NAME` in their build will see their brand name +substituted for `heads` in all filenames. The timestamp and branch logic +applies equally; no fork-specific changes are needed. + +## Build Variables (Makefile) + +| Variable | Value | Notes | +|----------|-------|-------| +| `HEADS_GIT_VERSION` | `git describe --abbrev=7 --tags --dirty` | Always set | +| `GIT_TIMESTAMP` | `YYYYMMDD-HHMMSS` of last commit | Always set | +| `GIT_BRANCH` | current branch name, truncated to 30 chars | Always set | +| `GIT_IS_RELEASE` | `y` or `n` | `y` only on clean exact tag | +| `GIT_VERSION_SUFFIX` | tag (release) or timestamp-branch-describe (dev) | Used in all output filenames | +| `CB_OUTPUT_BASENAME` | `--` | Base for all coreboot outputs | diff --git a/doc/configuring-keys.md b/doc/configuring-keys.md new file mode 100644 index 000000000..df2dcab9e --- /dev/null +++ b/doc/configuring-keys.md @@ -0,0 +1,175 @@ +# Configuring Keys: OEM Factory Reset / Re-Ownership + +This is the primary provisioning step after installing an OS or receiving a +new Heads-equipped device. It configures all security components in one pass. + +## Before You Start + +**Use a safe environment.** Passphrases are echoed to the screen during setup. + +**Prepare Diceware passphrases in advance.** Heads displays a QR code linking +to `osresearch.net/Configuring-Keys` when you enter the questionnaire — that +page lists recommended word counts per secret and links to EFF Diceware. +Using physical dice against a wordlist produces passphrases that are both +strong and memorable. +See [Keys](keys.md) for recommended lengths per secret. + +**You need:** +- A USB Security dongle with OpenPGP support (Nitrokey Pro 2, Nitrokey Storage 2, + Nitrokey 3, or Purism Librem Key for full HOTP support; YubiKey 5 for + OpenPGP-only). +- An OS installed on a dedicated `/boot` partition. +- Optionally: a USB thumb drive to back up GPG key material (recommended). + +## Entering OEM Factory Reset / Re-Ownership + +From the Heads main menu: `Options -> OEM Factory Reset / Re-Ownership`. + +The wizard first shows a warning describing what will be erased. Confirm to +continue. + +## Default vs. Custom Configuration + +`Would you like to use default configuration options? [Y/n]` + +**Answer N.** Accepting defaults leaves all security components at factory +PINs and passphrases (12345678 / 123456) which are publicly known. Custom +configuration only happens once per ownership; take the time to do it properly. + +When you answer N, Heads displays a **QR code for osresearch.net/Configuring-Keys** +— scan it with a phone for per-secret word-count guidance and EFF Diceware links. + +## Questionnaire + +### LUKS Disk Recovery Key Passphrase + +`Would you like to change the current LUKS Disk Recovery Key passphrase?` + +Answer **Y** if you did not install the OS yourself. The passphrase set at +OS install is unknown to you and may be known to the installer. + +### LUKS Re-encryption + +`Would you like to re-encrypt the LUKS container and generate a new LUKS Disk Recovery Key?` + +Answer **Y** if you did not install the OS yourself. Changing the passphrase +alone does not change the underlying encryption key — anyone with a LUKS header +backup from before could still decrypt with the old passphrase. Re-encryption +generates a new key and renders old header backups useless. + +### GPG Key Storage + +`Would you like to format an encrypted USB Thumb drive to store GPG key material?` + +- **Y** — Generates the GPG master key and subkeys in memory, backs them up + to an encrypted LUKS container on a USB thumb drive, then optionally copies + subkeys to the dongle. Recommended for production environments. +- **N** — Generates keys directly on the dongle's OpenPGP smartcard with no + off-card backup. Simpler but irreversible if the dongle is lost. + +If you answered Y: + +`Would you like in-memory generated subkeys to be copied to the USB Security dongle's OpenPGP smartcard?` + +Answer **Y** (recommended). Answering N leaves keys only on the backup drive; +clone it to a second drive for redundancy. + +### Passphrase Strategy + +`Would you like to set a single custom passphrase to all security components?` + +Not recommended — using one passphrase for everything means compromising one +secret compromises all. Useful only for OEM provisioning workflows. + +`Would you like to set distinct PINs/passphrases for each security component?` + +Answer **Y**. You will be prompted for: + +- **TPM Owner Passphrase** (min 8 chars) — protects TPM NVRAM ownership +- **GPG Admin PIN** (6-25 chars) — protects smartcard management operations +- **GPG User PIN** (6-25 chars) — protects signing and encryption operations + +### Custom GPG Key Identity + +`Would you like to set custom user information for the GnuPG key?` + +Answer **Y** if you plan to use the dongle for personal signing/encryption or +want the public key to be searchable on keyservers. + +- **Real Name** — your name; becomes the cardholder name on the smartcard +- **Email** — your email; becomes the login field on the smartcard and the + key UID email +- **Comment** — distinguishes this key (e.g. "USB Security dongle"); 1-60 chars + +## Key Generation + +After the questionnaire, Heads performs the following steps in order: + +1. Applies any requested LUKS passphrase or re-encryption changes +2. Resets the TPM with your chosen passphrase +3. Factory-resets the OpenPGP smartcard +4. Enables forced-signature PIN on the smartcard (good security practice) +5. Generates the GPG key (on-card or in-memory per your choice) +6. Backs up key material to the USB thumb drive (in-memory path only) +7. Copies subkeys to the dongle (in-memory path, if you chose to copy) +8. Sets the smartcard cardholder name and login fields from your identity info +9. Changes GPG Admin and User PINs to your chosen values +10. Adds the new public key to the firmware and reflashes the BIOS +11. Generates `/boot` hashes and signs them with the new key +12. Displays all provisioned secrets for confirmation + +After completing, Heads shows a **reboot prompt**. TOTP/HOTP secret +generation happens on the **first normal boot** after OEM reset — Heads detects +the TPM was cleared and guides you through the reseal process. + +RSA key generation on older dongles (Nitrokey Pro, Librem Key) may take +10 minutes or more — be patient. + +## Provisioned Secrets Summary + +At the end, Heads displays all provisioned secrets on screen and encodes them +in a QR code. **This is the last time these values are shown.** Write them +down or scan the QR code to a secure location before continuing. + +## After Provisioning + +### TOTP (smartphone) + +Scan the QR code into Google Authenticator, FreeOTP+, or a compatible app. +On subsequent boots Heads displays the current TOTP; compare it against +your phone. Requires correct UTC time set in `Options -> Time`. + +### HOTP (USB Security dongle) + +Heads seals the secret to the dongle automatically. On subsequent boots the +dongle verifies the HOTP code and shows a green LED (pass) or red LED (fail). + +### TPM Disk Unlock Key (optional) + +Go to `Options -> Boot Options`, select a default boot option, and answer +the prompts to seal a disk unlock key in the TPM. This requires your Disk +Recovery Key passphrase and GPG User PIN. On subsequent boots the TPM +releases the key automatically when PCRs match. + +## Adding an Existing GPG Key + +If you already have a provisioned USB Security dongle: + +1. Insert the dongle and the USB drive containing your public key. +2. Go to `Options -> GPG Management -> Add a GPG key to the running BIOS + reflash`. +3. Follow the steps. After reflashing, reboot. +4. Generate a new TOTP/HOTP secret when prompted. + +## Forgotten GPG User PIN + +From Recovery Shell with the dongle inserted: + +``` +gpg --change-pin +``` + +Enter the Admin PIN when prompted, then set a new User PIN. + +**Warning:** 3 consecutive wrong Admin PIN attempts permanently locks the +card. There is no recovery from an exhausted Admin PIN counter short of a +full factory reset of the OpenPGP applet (which destroys all keys on the card). diff --git a/doc/development.md b/doc/development.md new file mode 100644 index 000000000..b07ec8c13 --- /dev/null +++ b/doc/development.md @@ -0,0 +1,106 @@ +# Development Workflow + +## Commit Conventions + +All commits to `linuxboot/heads` must be: + +```bash +git commit -S -s -m "component: short description" +``` + +- **`-S`** — GPG-sign the commit (required; see [CONTRIBUTING.md](../CONTRIBUTING.md)) +- **`-s`** — add `Signed-off-by:` trailer for [DCO](https://developercertificate.org/) compliance (required; CI enforces this) + +### Message Format + +``` +component: short imperative description (72 chars max) + +Optional body explaining the why, not the what. Wrap at 72 chars. +Reference issues or PRs with #NNN. + +Signed-off-by: Your Name +``` + +- **Subject line**: imperative mood ("fix", "add", "remove", not "fixed"/"adds") +- **Component prefix**: the file or subsystem changed (`oem-factory-reset`, `tpmr`, `gui-init`, `Makefile`, `doc`, etc.) +- **Body**: explain motivation and context; the diff shows what changed + +### `Co-Authored-By` + +Add a `Co-Authored-By:` trailer only on commits whose **primary content is +collaborative documentation** (`doc/*.md` writing). Never add it to code +fixes, features, or refactors. + +``` +Co-Authored-By: Name +``` + +## Documentation: `doc/*.md` vs `heads-wiki` + +| Location | Purpose | Signing required | +|----------|---------|-----------------| +| `doc/*.md` in this repo | Developer-facing: architecture, patterns, internals, build conventions | Yes (same as all commits) | +| `linuxboot/heads-wiki` | User-facing: installation, configuration, how-to guides published at osresearch.net | No (lower bar for contribution) | + +Content should live in `doc/*.md` when it describes how the code works or how +to build/develop. Content should live in `heads-wiki` when it describes how a +user installs, configures, or operates a Heads-equipped device. + +Over time, `doc/*.md` and the wiki may overlap; the canonical user-facing +source is the wiki. + +## Build Artifacts + +See [build-artifacts.md](build-artifacts.md) for the full ROM filename +convention. Quick reference: + +```bash +# Release build (clean tag, e.g. v0.2.1): +heads-x230-v0.2.1.rom + +# Development build (any other state): +heads-x230-20260327-202007-my-feature-branch-v0.2.1-42-g0b9d8e4-dirty.rom +# ^timestamp ^branch name ^git describe +``` + +The timestamp sorts builds chronologically. The branch name identifies which +PR or feature a binary corresponds to without consulting git. + +When testing a development build, the ROM filename is your primary build +identifier — include it verbatim in bug reports and PR comments. + +## Testing Checklist + +When touching provisioning code (`oem-factory-reset`, `seal-hotpkey`, +`gui-init`): + +- [ ] Run a full OEM Factory Reset / Re-Ownership with custom identity (name + email) +- [ ] Verify `gpg --card-status` reflects cardholder name and login data +- [ ] Verify dongle branding shows correctly for the attached device +- [ ] Verify TOTP/HOTP sealing succeeds after reset +- [ ] Check `/boot` signing succeeds with the new GPG key + +When touching the Makefile or build system: + +- [ ] Verify dev build filename includes timestamp + branch +- [ ] Verify a locally-tagged clean commit produces the short filename +- [ ] Verify `.zip` package extracts and `sha256sum -c` passes + +## Coding Conventions + +### Shell scripts + +- All user-visible output through logging helpers: `STATUS`, `STATUS_OK`, + `INFO`, `NOTE`, `WARN`, `ERROR`, `DEBUG` (see [logging.md](logging.md)) +- Interactive prompts via `INPUT` only — never raw `read` +- All interactive text output routed through `>"${HEADS_TTY:-/dev/stderr}"` to + avoid interleaving with `DO_WITH_DEBUG` buffered stdout +- Terminology: **passphrase** for TPM/LUKS secrets; **PIN** for GPG smartcard + (OpenPGP spec); never "password" in user-facing text +- Diceware references when prompting users to choose passphrases + +### UX patterns + +See [ux-patterns.md](ux-patterns.md) for `INPUT`, `STATUS`/`STATUS_OK`, +`DO_WITH_DEBUG`, `HEADS_TTY` routing, and PIN caching conventions. diff --git a/doc/docker.md b/doc/docker.md new file mode 100644 index 000000000..4aac45164 --- /dev/null +++ b/doc/docker.md @@ -0,0 +1,687 @@ +# Heads Docker Build Environment + +Heads builds inside a versioned Docker image that provides a reproducible, hermetic build +environment. Docker images are built with Nix since +[PR #1661](https://github.com/linuxboot/heads/pull/1661). + +See also: [General reproducible-build notes](../README.md#general-notes-on-reproducible-builds), +[QEMU testing](qemu.md). + +--- + +## Quick start + +The short path to build Heads is to do what CircleCI does: + +- Install [Docker CE](https://docs.docker.com/engine/install/) for your OS +- Run `./docker_repro.sh make BOARD=XYZ` + +```bash +# Canonical, reproducible build (recommended for all users) +./docker_repro.sh make BOARD=x230-hotp-maximized + +# Build and run a QEMU board +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run +``` + +`./docker_repro.sh` is the canonical, reproducible way to build and test Heads. +`docker_local_dev.sh` is intended for developers who need to modify the local image built +from `flake.nix`/`flake.lock` and is not recommended for general testing. + +The supported and tested workflow uses the provided Docker wrappers +(`./docker_repro.sh`, `./docker_local_dev.sh`, or `./docker_latest.sh`). Host-side +installation of QEMU, `swtpm`, or other QEMU-related tooling is unnecessary for the +standard workflow and is not part of the tested configuration. Only advanced or edge-case +workflows may require installing those tools on the host (see [qemu.md](qemu.md)). + +The Docker images produced by our Nix build include QEMU (`qemu-system-x86_64`), +`swtpm` / `libtpms`, `canokey-qemu` (a virtual OpenPGP smartcard), and other userspace +tooling required to build and test QEMU boards. You only need Docker on the host. For KVM +acceleration expose `/dev/kvm` (load `kvm_intel` / `kvm_amd`); the wrapper scripts mount +it automatically when present. + +If you plan to manage disk images or use `qemu-img` snapshots on the host (outside +containers), install the `qemu-utils` package locally (which provides `qemu-img`). + +--- + +## Docker wrapper scripts + +Three wrappers cover different use cases: + +| Script | Use case | Reproducibility | When to use | +| --- | --- | --- | --- | +| `./docker_repro.sh` | **Canonical reproducible builds** | Pinned to immutable digest | **All users & maintainers**: Standard way to build Heads; matches CircleCI exactly; use for releases and critical builds | +| `./docker_local_dev.sh` | **Developer customization** | Local build may differ if flake changes | **Developers only**: Rebuilds from local `flake.nix`/`flake.lock` when dirty; use `HEADS_CHECK_REPRODUCIBILITY=1` to verify against published version | +| `./docker_latest.sh` | **Convenience** | Defaults to reproducible digest; may be unpinned if no digest is available | **Testing/convenience**: Uses latest published image; by default falls back to the reproducible digest (`DOCKER_REPRO_DIGEST`) when available (no confirmation needed). Runs unpinned only when no digest is configured, in which case it requires confirmation unless `HEADS_ALLOW_UNPINNED_LATEST=1` or `DOCKER_LATEST_DIGEST` is set. | + +**Recommendation by role**: + +- **End users & QA**: Use `./docker_repro.sh` for all builds (ensures reproducibility and security) +- **Developers**: Use `./docker_local_dev.sh` when iterating on the build system or Nix flake, + but verify reproducibility with `HEADS_CHECK_REPRODUCIBILITY=1` before committing +- **Maintainers**: Use `./docker_repro.sh` for official releases; see [Maintenance workflow](#maintenance-workflow) + +**Examples**: + +```bash +# Canonical builds +./docker_repro.sh make BOARD=x230-hotp-maximized +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run + +# Developer workflow (verify before committing) +./docker_local_dev.sh make BOARD=nitropad-nv41 +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh make BOARD=nitropad-nv41 +``` + +If you are already inside the container interactively, run `make BOARD=board_name` as usual. + +### QEMU workflow examples + +```bash +# Build ROM, then export public key to emulated USB storage at QEMU runtime +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 + +# Inject a GPG public key into the ROM image +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=~/pubkey.asc inject_gpg + +# Full install run with hardware token, disk image, and install ISO +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 \ + USB_TOKEN=Nitrokey3NFC \ + PUBKEY_ASC=~/pubkey.asc \ + ROOT_DISK_IMG=~/qemu-disks/debian-9.cow2 \ + INSTALL_IMG=~/Downloads/debian-9.13.0-amd64-xfce-CD-1.iso \ + run +``` + +If you do not specify `USB_TOKEN`, the container uses the included `canokey-qemu` virtual +token by default. Set `USB_TOKEN` (or use `hostbus`/`hostport`/`vendorid,productid`) to +forward a hardware token instead. See [qemu.md](qemu.md) for details. + +--- + +## Wrapper help and environment variables + +Each wrapper shows its own focused help (only variables it actually uses). For the complete +environment reference run `docker/common.sh` directly: + +```bash +# Wrapper-specific help +./docker_repro.sh --help +./docker_latest.sh --help +./docker_local_dev.sh --help + +# Full environment variable reference (shared helper) +./docker/common.sh +``` + +The shared helper documents all supported environment variables (opt-ins and opt-outs) and +defaults. Wrapper help is intentionally narrower so it only lists variables relevant to +that wrapper. + +### All wrapper scripts + +**`HEADS_MAINTAINER_DOCKER_IMAGE`** — override the canonical maintainer's Docker image +repository (default: `tlaurion/heads-dev-env`). Use this for local testing or if you +maintain a fork. Example: `export HEADS_MAINTAINER_DOCKER_IMAGE="myuser/heads-dev-env"`. +This affects reproducibility checks and default image references across all Docker wrapper +scripts. + +**`HEADS_CHECK_REPRODUCIBILITY_REMOTE`** — specify which remote image to compare against +when verifying reproducibility (default: `${HEADS_MAINTAINER_DOCKER_IMAGE}:latest`). Use +this to test against a specific tagged version instead of `:latest`. + +```bash +# Compare against a specific version +export HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh +``` + +**`HEADS_DISABLE_USB=1`** — disable automatic USB passthrough and the automatic USB +cleanup (default: `0`). + +**`HEADS_X11_XAUTH=1`** — force mounting your `${HOME}/.Xauthority` into the container +for X11 authentication. When set the helper will bypass programmatic Xauthority generation +and mount your `${HOME}/.Xauthority` (if present); if the file is missing the helper will +warn and will not attempt automatic cookie creation (GUI may fail). + +### `./docker_local_dev.sh` + +**`HEADS_SKIP_DOCKER_REBUILD=1`** — skip automatically rebuilding the local image when +`flake.nix`/`flake.lock` are dirty. + +**`HEADS_CHECK_REPRODUCIBILITY=1`** — **recommended for verifying reproducible builds**. +After building/loading the local image, automatically compares its digest with the +published maintainer image to verify reproducibility. Requires network access. By default +compares against `${HEADS_MAINTAINER_DOCKER_IMAGE}:latest`. Use +`HEADS_CHECK_REPRODUCIBILITY_REMOTE` to specify a different tag (e.g., `v0.2.7`). See +[Verifying reproducibility](#verifying-reproducibility) below for detailed examples. + +**`HEADS_AUTO_INSTALL_NIX=1`** — automatically attempt to download the Nix single-user +installer when `nix` is missing (interactive prompt suppressed). + +For supply-chain safety the helper will download the installer to a temporary file and +print its SHA256; it will NOT execute the installer automatically unless the downloaded +installer matches a pinned hash. The helper will also attempt to detect the installer +version heuristically (when possible) and suggest the canonical releases URL (for example +`https://releases.nixos.org/nix/nix-2.33.2/install.sha256`) so you can fetch the +published sha and compare. To verify: + +- **Preferred — pin a release version**: set `HEADS_NIX_INSTALLER_VERSION` to a release + (for example `nix-2.33.2`). The helper will fetch + `https://releases.nixos.org/nix/${HEADS_NIX_INSTALLER_VERSION}/install` and + `install.sha256` and show both checksums for you to compare. To auto-run in trusted + automation, set `HEADS_NIX_INSTALLER_SHA256` to the expected sha256 as well. + +- **Or compute-and-pin locally**: run + `./docker/fetch_nix_installer.sh --version nix-2.33.2` (or `--url`) to download the + installer and print its sha256, then set `HEADS_NIX_INSTALLER_SHA256` to that value for + automation. + + Otherwise verify the downloaded installer manually and run it yourself: + `sh /path/to/installer --no-daemon`. + +**`HEADS_AUTO_ENABLE_FLAKES=1`** — automatically enable flakes by writing +`experimental-features = nix-command flakes` to `$HOME/.config/nix/nix.conf` +(interactive prompt suppressed). + +**`HEADS_MIN_DISK_GB`** — minimum free disk space in GB required on `/nix` (or `/` if +`/nix` missing) for building (default: `50`). + +**`HEADS_SKIP_DISK_CHECK=1`** — skip the preflight disk-space check. + +### `./docker_latest.sh` + +**`HEADS_ALLOW_UNPINNED_LATEST=1`** — when set, bypass the interactive warning that using +`:latest` in `./docker_latest.sh` is a supply-chain risk (otherwise `:latest` requires +confirmation unless `DOCKER_LATEST_DIGEST` is set or the wrapper can fall back to +`DOCKER_REPRO_DIGEST` for the maintainer image). + +**`DOCKER_LATEST_DIGEST`** — pin the convenience wrapper to a specific immutable digest. + +### `./docker_repro.sh` + +**`DOCKER_REPRO_DIGEST`** — pin the image used by `./docker_repro.sh` to an immutable +digest: `tlaurion/heads-dev-env@` (recommended for reproducible and secure +builds). Note: `DOCKER_REPRO_DIGEST` is *consumed by* `./docker_repro.sh` via +`resolve_docker_image` in `docker/common.sh` and is the canonical way to pin the repro +image for reproducible builds. The repository file `docker/DOCKER_REPRO_DIGEST` contains +the pinned digest used by default. + +--- + +## USB token passthrough + +When USB passthrough is active the wrappers will detect processes that may be holding a +USB token (for example `scdaemon` or `pcscd`). The wrapper will warn and, on interactive +shells, give a **3-second abort window** before attempting to kill those processes to free +the token. Set `HEADS_DISABLE_USB=1` to opt out of this automatic cleanup. + +```bash +HEADS_DISABLE_USB=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run +``` + +For details about selecting or forwarding a physical USB token to QEMU (handled by the +`USB_TOKEN` make variable), see [qemu.md](qemu.md). + +--- + +## Managing local Docker images + +Note: you may need to prefix commands with `sudo` depending on your Docker setup. + +```bash +# List local images +docker images + +# Inspect a specific image (IDs, digests, repo tags) +docker image inspect + +# Remove a specific image +docker rmi + +# Remove all local images (destructive) +docker rmi -f $(docker images -aq) + +# Remove unused images/containers/networks/build cache (destructive) +docker system prune -a --volumes +``` + +--- + +## QEMU disk snapshots with `qemu-img` + +If you manage qcow2 disk images on the host, `qemu-img` can create, list, restore, and +delete snapshots. These examples assume a qcow2 disk image: + +```bash +# Create a snapshot +qemu-img snapshot -c clean root.qcow2 + +# List snapshots +qemu-img snapshot -l root.qcow2 + +# Restore (apply) a snapshot +qemu-img snapshot -a clean root.qcow2 + +# Delete a snapshot +qemu-img snapshot -d clean root.qcow2 + +# Optional: create an overlay backed by a base image +qemu-img create -f qcow2 -b base.qcow2 overlay.qcow2 +``` + +If you prefer to run these inside the container, prefix with `./docker_repro.sh`: + +```bash +./docker_repro.sh qemu-img snapshot -l root.qcow2 +``` + +--- + +## Building with the published Docker image + +The canonical, reproducible way to build Heads is to use `./docker_repro.sh`, which +automatically pulls the pinned Docker image digest from `docker/DOCKER_REPRO_DIGEST` and +ensures your builds match the CI environment exactly. + +```bash +./docker_repro.sh make BOARD=x230-hotp-maximized +./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run +``` + +This will: + +1. Resolve the canonical image digest from `docker/DOCKER_REPRO_DIGEST` (immutable, pinned to a specific version) +2. Pull the image if not present locally +3. Execute your build inside that exact Docker environment +4. Guarantee reproducibility: your ROM output will match official CircleCI builds for that commit + +**About the published image**: + +- **Repository**: `tlaurion/heads-dev-env` on Docker Hub is the maintainer's canonical image (configurable via `HEADS_MAINTAINER_DOCKER_IMAGE`) +- **Versioning**: Tagged with version numbers (e.g., `v0.2.7`) for stability; `:latest` is mutable and not recommended +- **Pinning**: The repository file `docker/DOCKER_REPRO_DIGEST` pins an immutable digest (`tlaurion/heads-dev-env@sha256:...`) to ensure reproducibility +- **Trust**: As long as `flake.nix` and `flake.lock` are not modified locally, your build will produce identical digests, confirming integrity +- **Fork/Override**: To use a different image repository, set `HEADS_MAINTAINER_DOCKER_IMAGE="youruser/your-image"` before running any Docker wrapper script + +`DOCKER_REPRO_DIGEST` (the environment variable or the repository file `docker/DOCKER_REPRO_DIGEST`) +is consumed by `./docker_repro.sh` via `resolve_docker_image()`; pinning ensures +reproducible builds and mitigates supply-chain risk from mutable `:latest` tags. + +--- + +## Using Nix for local development + +`./docker_local_dev.sh` is a developer helper that ensures a local Nix-based Docker image +(`linuxboot/heads:dev-env`) is available for interactive development. It performs preflight +checks and interactive prompts to make the process easier: + +- Ensures `nix` is installed and **flakes** are enabled; if missing it will prompt to + install Nix and enable flakes. Set `HEADS_AUTO_INSTALL_NIX=1` and/or + `HEADS_AUTO_ENABLE_FLAKES=1` to suppress prompts and proceed automatically. +- Requires either `curl` or `wget` to fetch the Nix installer; if neither is present the + script will print how to install one and abort. +- Checks disk space on `/nix` (or `/` if `/nix` is absent); default minimum is **50 GB** + (`HEADS_MIN_DISK_GB=50`) — override or skip the check with `HEADS_SKIP_DISK_CHECK=1`. +- If `flake.nix` or `flake.lock` are dirty (uncommitted changes), the helper will rebuild + the local Docker image. To intentionally trigger a rebuild, make and keep changes to + `flake.nix` (for example update an input or a harmless comment) or update `flake.lock`, + then run `./docker_local_dev.sh`; the helper detects the dirty flake files and will + rebuild automatically. To avoid an automatic rebuild, commit or stash your changes or + set `HEADS_SKIP_DOCKER_REBUILD=1` to disable the check. + +Notes on automation: + +- The `./docker_local_dev.sh` helper will attempt to ensure Nix and flakes are available + when you run it interactively. Set `HEADS_AUTO_INSTALL_NIX=1` / + `HEADS_AUTO_ENABLE_FLAKES=1` to suppress prompts. +- Building the Docker image and populating `/nix` can require significant disk space — at + least **50 GB** free on `/nix` (or `/` if `/nix` is not present). Adjust via + `HEADS_MIN_DISK_GB` or skip the check with `HEADS_SKIP_DISK_CHECK=1`. +- The Nix installer requires a downloader; either `curl` or `wget` must be available on + the host. The helper will guide you to install one if neither is present. +- For reproducible builds prefer `./docker_repro.sh`; `./docker_local_dev.sh` is intended + for development and will rebuild the local image when `flake.nix`/`flake.lock` are dirty + (unless `HEADS_SKIP_DOCKER_REBUILD=1`). + +### Set up Nix and flakes + +If you don't already have Nix, install it: + +```bash +[ -d /nix ] || sh <(curl -L https://nixos.org/nix/install) --no-daemon +. /home/user/.nix-profile/etc/profile.d/nix.sh +``` + +Enable flake support in nix: + +```bash +mkdir -p ~/.config/nix +echo 'experimental-features = nix-command flakes' >>~/.config/nix/nix.conf +``` + +### Build the local image + +```bash +# Manual +nix --print-build-logs --verbose build .#dockerImage && docker load < result + +# Via helper (rebuilds automatically when flake files are dirty) +./docker_local_dev.sh +``` + +Your local docker image `linuxboot/heads:dev-env` is ready to use, reproducible for the +specific Heads commit used to build it, and will produce ROMs reproducible for that commit ID. + +On some hardened OSes, you may encounter problems with ptrace: + +```text +> proot error: ptrace(TRACEME): Operation not permitted +``` + +The most likely reason is that your +[kernel.yama.ptrace_scope](https://www.kernel.org/doc/Documentation/security/Yama.txt) +variable is too high and doesn't allow docker+nix to run properly. You'll need to +temporarily set it to 1 while you build: + +```bash +sudo sysctl kernel.yama.ptrace_scope # show current value (probably 2 or 3) +sudo sysctl -w kernel.yama.ptrace_scope=1 # lower for the build +# ... build ... +sudo sysctl -w kernel.yama.ptrace_scope= # restore after +``` + +### Verify reproducibility before committing + +```bash +# Verify local image matches maintainer's latest +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh + +# Verify against a specific version +HEADS_CHECK_REPRODUCIBILITY=1 \ + HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" \ + ./docker_local_dev.sh +``` + +### Under QubesOS + +- [Setup Nix persistent layer under QubesOS](https://dataswamp.org/~solene/2023-05-15-qubes-os-install-nix.html) (Thanks @rapenne-s!) +- [Install Docker under QubesOS](https://gist.github.com/tlaurion/9113983bbdead492735c8438cd14d6cd) + +--- + +## Verifying reproducibility + +**Best practice**: Verify that your locally-built Docker image is reproducible by +comparing its digest with the published maintainer image. + +The Heads project maintains the canonical `tlaurion/heads-dev-env` Docker image on Docker +Hub (configurable via `HEADS_MAINTAINER_DOCKER_IMAGE` for forks or testing). As long as +you do not modify `flake.nix` or `flake.lock`, your locally-built image **should produce +an identical digest** to the published image, demonstrating that your build is fully +reproducible. + +### Quick reference + +| Scenario | Command | +| --- | --- | +| Check against latest maintainer image | `HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh` | +| Check against specific version tag | `HEADS_CHECK_REPRODUCIBILITY=1 HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" ./docker_local_dev.sh` | +| Check fork maintainer's image | `HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env" HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh` | +| Standalone check at any time | `./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7` | + +### Prerequisites + +You have either: + +- Built a local Docker image with `./docker_local_dev.sh` (produces `linuxboot/heads:dev-env`), or +- Built from `nix build .#dockerImage` (results in `result` symlink loadable via `docker load`) + +### Method 1: Automated check during build (recommended) + +Enable reproducibility verification automatically during your build with +`HEADS_CHECK_REPRODUCIBILITY=1`: + +```bash +# Verify against the default (maintainer's :latest image) +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh + +# Example output when digests MATCH (reproducible build): +# === Reproducibility Check === +# Local image (linuxboot/heads:dev-env): sha256:5f890f3d... +# Remote image (tlaurion/heads-dev-env:latest): sha256:5f890f3d... +# ✓ MATCH: Local build is reproducible! +``` + +To test against a **specific version tag** instead of `:latest`: + +```bash +HEADS_CHECK_REPRODUCIBILITY=1 \ + HEADS_CHECK_REPRODUCIBILITY_REMOTE="tlaurion/heads-dev-env:v0.2.7" \ + ./docker_local_dev.sh + +# Example output when digests DIFFER (expected for different versions): +# === Reproducibility Check === +# Local image (linuxboot/heads:dev-env): sha256:5f890f3d... +# Remote image (tlaurion/heads-dev-env:v0.2.6): sha256:75af4c81... +# ✗ MISMATCH: Local build differs from remote +# (This is expected if Nix/flake.lock versions differ or if uncommitted changes exist) +``` + +Note: Docker images can have two different identifiers: a local image ID and a registry +manifest digest. If a local image has no `RepoDigests` entry, the reproducibility check +will compare image IDs (and may pull the remote tag) instead of manifest digests to avoid +false mismatches. This can happen for locally built images that have not been pulled from +a registry. + +### Method 2: Standalone reproducibility check + +```bash +# Compare your local dev image with a published version +./docker/check_reproducibility.sh linuxboot/heads:dev-env tlaurion/heads-dev-env:v0.2.7 + +# Output (example of a match): +# ✓ SUCCESS: Digests match! +# Your local build is reproducible and identical to tlaurion/heads-dev-env:v0.2.7 +``` + +### Method 3: Manual digest inspection + +```bash +# Get the digest of your local image (after docker load) +docker inspect --format='{{.Id}}' linuxboot/heads:dev-env + +# Compare with the published image (will pull if needed) +docker pull tlaurion/heads-dev-env:v0.2.7 +docker inspect --format='{{.Id}}' tlaurion/heads-dev-env:v0.2.7 +``` + +### When digests should match + +✓ **Digests match** — your build is **reproducible and trustworthy**; matches the +maintainer's published image for that Nix snapshot. Happens when: + +- `flake.nix` and `flake.lock` are **not modified** (repository is clean relative to these files) +- The same Nix version and dependencies are used +- Build runs on the same Nix store state + +✗ **Digests differ** — expected when: + +- You have uncommitted changes in `flake.nix` or `flake.lock` +- Different Nix version or Nix dependencies resolved differently on your system +- Using a different `nixpkgs` version than the locked one in `flake.lock` + +### Trust model + +The `tlaurion/heads-dev-env` image on Docker Hub is the **maintainer's canonical build** +and serves as the source of truth for reproducibility. By verifying that your +locally-built image produces the same digest as the published version you confirm: + +1. **No tampering**: Your build environment has not been compromised +2. **Reproducibility**: The Heads build system is deterministic for your specific Nix snapshot +3. **Auditability**: You can map your build back to a specific published, reviewed version + +**Recommendation**: Always pin to a specific version tag (e.g., `tlaurion/heads-dev-env:v0.2.7`) +rather than `:latest`, and verify the digest matches the published value before using it +for critical builds. + +--- + +## Pinning `./docker_latest.sh` + +We do not maintain a `docker/DOCKER_LATEST_DIGEST` file in the repository because +`latest` is a user-level convenience and should be explicitly chosen. When +`DOCKER_LATEST_DIGEST` is unset, `./docker_latest.sh` may fall back to `DOCKER_REPRO_DIGEST` +only when the base image matches the maintainer repo; otherwise it will prompt before +using an unpinned `:latest` unless `HEADS_ALLOW_UNPINNED_LATEST=1` is set. + +```bash +# 1) Obtain the digest for a published image +# Tip: inspect tags on Docker Hub: https://hub.docker.com/layers/tlaurion/heads-dev-env/ +# +./docker/get_digest.sh tlaurion/heads-dev-env:v0.2.7 +# Output (example): tlaurion/heads-dev-env@sha256:50a9110c... + +# Auto-pull and return digest in one go: +./docker/get_digest.sh -y tlaurion/heads-dev-env:v0.2.7 + +# 2) Export and use the digest +export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:latest | tail -n1) +DOCKER_LATEST_DIGEST=$DOCKER_LATEST_DIGEST ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 +``` + +When a digest is discovered, helpers print a concise summary to help auditing: + +```text +Image: tlaurion/heads-dev-env@sha256:50a9... +Digest: sha256:50a9... +Resolved from: local|registry API|env|file +Tip: export DOCKER_LATEST_DIGEST=sha256:50a9... +``` + +To change what `./docker_latest.sh` uses as the "latest" image: + +- **Temporary override**: `./docker/pin-and-run.sh -- ./docker_latest.sh ` +- **Local convenience env**: `export DOCKER_LATEST_DIGEST=$(./docker/get_digest.sh tlaurion/heads-dev-env:vX.Y.Z | tail -n1)` +- **Canonical fallback**: edit `docker/DOCKER_REPRO_DIGEST` with the desired digest and commit + +```bash +# pin-and-run helper examples +./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 +./docker/pin-and-run.sh -y tlaurion/heads-dev-env:v0.2.7 -- ./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 + +# Omit the wrapper — helper defaults to './docker_latest.sh' +./docker/pin-and-run.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2 + +# Explicit wrapper flag (avoids ambiguity) +./docker/pin-and-run.sh -w ./docker_repro.sh tlaurion/heads-dev-env:v0.2.7 -- make BOARD=qemu-coreboot-fbwhiptail-tpm2 +``` + +Alternative manual commands without the helper: + +```bash +docker pull tlaurion/heads-dev-env:latest +# prints full repo@digest (if available) +docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest +# to get only the digest portion: +docker inspect --format='{{index .RepoDigests 0}}' tlaurion/heads-dev-env:latest | cut -d'@' -f2 +``` + +Notes: some registries or Docker versions may require `docker manifest inspect` or +`skopeo inspect` to obtain an authoritative digest; the helper script tries +`docker inspect` first, then `docker manifest inspect` when available. + +Acceptable digest formats for `DOCKER_REPRO_DIGEST` / `DOCKER_LATEST_DIGEST`: +`sha256:<64-hex>`, `sha256-<64-hex>`, or bare `<64-hex>` — all normalized to `sha256:`. + +--- + +## Maintenance workflow + +To update the Docker image to a new version (e.g., `vx.y.z`): + +```bash +docker_version="vx.y.z" +docker_hub_repo="tlaurion/heads-dev-env" + +# Update pinned packages to latest if needed, modify flake.nix as required +nix flake update + +# Commit flake changes +git add flake.nix flake.lock +git commit --signoff -m "Bump nix develop based docker image to $docker_version" + +# Verify reproducibility: ensure the local build matches (no further changes to flake files) +nix develop --ignore-environment --command true + +# Build the new Docker image +nix build .#dockerImage +docker load < result + +# Verify you can extract the digest (flake.nix/flake.lock must be committed) +docker inspect --format='{{.Id}}' linuxboot/heads:dev-env + +# Tag the image with the new version +docker tag linuxboot/heads:dev-env "$docker_hub_repo:$docker_version" + +# Push the new version to Docker Hub (requires push access) +docker push "$docker_hub_repo:$docker_version" + +# Capture the digest of the pushed image (use --yes to auto-pull) +new_digest=$(./docker/get_digest.sh -y "$docker_hub_repo:$docker_version" | tail -n1) +prev_digest=$(grep '^[^#]' docker/DOCKER_REPRO_DIGEST | head -n1) + +# Update the digest in the repository file +sed -i "s|$prev_digest|$new_digest|" docker/DOCKER_REPRO_DIGEST + +# Update the version comment in the repository file +sed -i "s|# Version: .*|# Version: $docker_version|" docker/DOCKER_REPRO_DIGEST + +# Update .circleci/config.yml (remove old comment, insert fresh one above the image line) +sed -i \ + -e "/^[[:space:]]*# Docker image: /d" \ + -e "/^[[:space:]]*- image: ${docker_hub_repo//\//\\/}@/ s|^\([[:space:]]*\)\(- image: ${docker_hub_repo//\//\\/}@\)|\\1# Docker image: $docker_hub_repo:$docker_version\n\\1\\2|" \ + .circleci/config.yml + +# Commit the digest and config changes +git add docker/DOCKER_REPRO_DIGEST .circleci/config.yml +git commit --signoff -m "Pin docker image to digest for $docker_version" + +# Push the branch and create a PR for testing with CircleCI +git push origin docker/squash-docker-changes + +# After PR is merged and tested, optionally tag as latest (use with caution) +# docker tag "$docker_hub_repo:$docker_version" "$docker_hub_repo:latest" +# docker push "$docker_hub_repo:latest" +``` + +### Maintainer checklist + +1. **Reproducibility**: Before pushing, verify `nix build .#dockerImage` produces a deterministic result (`flake.nix` and `flake.lock` must be committed and clean). +2. **Digest verification**: After pushing, use `./docker/check_reproducibility.sh` to verify local and remote digests match. +3. **Supply chain**: Pin digest in `docker/DOCKER_REPRO_DIGEST` and `.circleci/config.yml` to ensure all builds reference an immutable, auditable image. +4. **Documentation**: Update the version comment in `docker/DOCKER_REPRO_DIGEST` so users know which image version is pinned. +5. **User migration**: When releasing a new version, communicate the new digest and version in release notes. + +Notes: + +- Local builds can use `:latest` tag, which will use the latest tested successful CircleCI run +- To reproduce CircleCI results, make sure to use the same versioned tag declared under `.circleci/config.yml`'s `image:` + +### For forks and alternate maintainers + +```bash +export HEADS_MAINTAINER_DOCKER_IMAGE="youruser/heads-dev-env" + +# All scripts will now reference your repository +./docker_local_dev.sh make BOARD=x230 +HEADS_CHECK_REPRODUCIBILITY=1 ./docker_local_dev.sh + +# Reproducibility check compares against youruser/heads-dev-env:latest +# resolve_docker_image uses youruser/heads-dev-env as the base image +``` + +The repository file `docker/DOCKER_REPRO_DIGEST` pins the canonical reproducible image +used by `./docker_repro.sh`, ensuring immutable, secure builds. Update the appropriate +file after publishing a new image to keep the repo in sync. diff --git a/doc/faq.md b/doc/faq.md new file mode 100644 index 000000000..0dca32e7a --- /dev/null +++ b/doc/faq.md @@ -0,0 +1,122 @@ +Frequently Asked Questions about Heads +=== + +Why replace UEFI with coreboot? +--- +While Intel's edk2 tree that is the base of UEFI firmware is open source, +the firmware that vendors install on their machines is proprietary and +closed source. Updates for bugs fixes or security vulnerabilities +are at the vendor's convenience; user specific enhancements are likely not +possible; and the code is not auditable. + +UEFI is much more complex than the BIOS that it replaced. It consists of +millions of lines of code and is an entire operating system, +with network device drivers, graphics, USB, TCP, https, etc, etc, etc. +All of these features represents increased "surface area" for attacks, +as well as unnecessary complexity in the boot process. + +coreboot is open source and focuses on just the code necessary to bring +the system up from reset. This minimal code base has a much smaller +surface area and is possible to audit. Additionally, self-help is +possible if custom features are required or if a security vulnerability +needs to be patched. + + +What's wrong with UEFI Secure Boot? +--- +Can't audit it, signing keys are controlled by vendors, +doesn't handle hand off in all cases, depends on possible leaked keys. + + +Why use Linux instead of vboot2? +--- +vboot2 is part of the coreboot tree and is used by Google in the +Chromebook system to provide boot time security by verifying the +hashes on the coreboot payload. This works well for the specialized +Chrome OS on the Chromebook, but is not as flexible as a measured +boot solution. + +By moving the verification into the boot scripts we're able to have +a much flexible verification system and use more common tools like PGP +to sign firmware stages. + + +What about Trusted GRUB? +--- +The mainline grub doesn't have support for TPM and signed kernels, but +there is a Trusted grub fork that does. Due to philosophical differences +the code might not be merged into the mainline. And due to problems +with secure boot (which Trusted Grub builds on), many distributions have +signed insecure kernels that bypass all of the protections secure +boot promised. + +Additionally, grub is closer to UEFI in that it must have device +drivers for all the different boot devices, as well as filesystems. +This duplicates the code that exists in the Linux kernel and has its +own attack surface. + +Using coreboot and Linux as a boot loader allows us to restrict +the signature validation to keys that we control. We also have one code +base for the device drivers in the Linux-as-a-boot-loader as well +as Linux in the operating system. + + +What is the concern with the Intel Management Engine? +--- +"Rootkit in your chipset", "x86 considered harmful", etc + + +How about the other embedded devices in the system? +--- +#goodbios, funtenna, etc. + + +Should we be concerned about the binary blobs? +--- +Maybe. x230 has very few (MRC) since it has native vga init. + + +Why use ancient Thinkpads instead of modern Macbooks? +--- +coreboot support, TPM, nice keyboards, cheap to experiment on. + +How likely are physical presence attacks vs remote software attacks? +--- +Who knows. + + +Defense in depth vs single layers +--- +Yes. + +is it worth doing the hardware modifications? +--- +Depends on your threat model. + + +Should I validate the TPMTOTP on every boot? +--- +Probably. I want to make it also do it at S3. + + +suspend vs shutdown? +--- +S3 is subject to cold boot attacks, although they are harder to +pull off on a Heads system since the boot devices are constrained. + +However, without tpmtotp in s3 it is hard to know if the system is in +a safe state when the xscreensaver lock screen comes up. Is it a fake +to deceive you and steal your login password? Maybe! It wouldn't get +your disk passphrase, which is perhaps an improvement. + + +Disk key in TPM (LUKS TPM Disk Unlock Key) or user passphrase? +--- +Depends on your threat model. With the Disk Unlock Key in the TPM an +attacker would need to have the entire machine (or a backdoor in the TPM) +to get the key and their attempts to unlock it can be rate limited +by the TPM hardware. + +However, this ties the disk to that one machine (without having to +recover and type in the master key), which might be an unacceptable risk +for some users. diff --git a/doc/gpg.md b/doc/gpg.md new file mode 100644 index 000000000..6de0e55b4 --- /dev/null +++ b/doc/gpg.md @@ -0,0 +1,147 @@ +# GPG Key Management + +Heads uses a GPG key stored on the USB Security dongle's OpenPGP smartcard to +sign `/boot` contents and verify firmware integrity. + +## Key Generation + +Key generation happens automatically during `OEM Factory Reset / Re-Ownership`. +See [configuring-keys.md](configuring-keys.md) for the full provisioning flow. + +Two generation paths are available: + +**On-card (default):** keys are generated directly on the smartcard. +No off-card backup exists — losing the dongle means losing the key. + +**In-memory with backup:** master key and subkeys are generated in RAM, +backed up to an encrypted LUKS container on a USB thumb drive, then subkeys +are optionally copied to the dongle. Recommended for production environments. + +### GPG Command Requirements + +Heads uses GPG's `--command-fd` for interactive operations with smartcards. +The following patterns are critical for reliable operation: + +**For key generation on card:** +```bash +echo "generate" | gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit +``` + +**For keytocard operations:** +```bash +{ + echo "key 1" + echo "keytocard" + echo "1" # slot: 1=sign + echo "${ADMIN_PIN}" # local keyring passphrase + echo "${ADMIN_PIN_DEF}" # card admin PIN -- prompted once; scdaemon caches it + echo "key 1" + echo "key 2" + echo "keytocard" + echo "2" # slot: 2=enc + echo "${ADMIN_PIN}" # local keyring passphrase (card PIN already cached) + echo "key 2" + echo "key 3" + echo "keytocard" + echo "3" # slot: 3=auth + echo "${ADMIN_PIN}" # local keyring passphrase (card PIN still cached) + echo "key 3" + echo "save" +} | gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key "${GPG_MAIL}" +``` + +Key observations confirmed with GPG 2.4 + scdaemon: +- The card admin PIN is only requested during the **first** `keytocard` in a session. + scdaemon caches it; subsequent keytocard operations need only the local keyring passphrase. +- There is **no** "Really create?" confirmation prompt when using `--pinentry-mode=loopback` + for `addkey`. GPG issues `GET_HIDDEN passphrase.enter` (to unlock the master key) + **before** creating the subkey -- not after. + +**For signing with smartcard keys:** +```bash +gpg --detach-sign --pinentry-mode loopback --passphrase-file <(echo -n "$USER_PIN") --digest-algo SHA256 -a +``` + +Key requirements: +- Use `--command-fd=0` for interactive input +- Use `--pinentry-mode=loopback` for non-interactive PIN entry +- For `keytocard`, pass slot number (`1`, `2`, `3`) separately, not combined (`keytocard 1`) +- Do NOT use `--batch` with `keytocard` or `addkey` commands + +## Changing PINs + +### From Recovery Shell + +```bash +gpg --change-pin +``` + +Menu options: +- `1` — Change User PIN (requires current User PIN) +- `2` — Unblock User PIN (requires Admin PIN) +- `3` — Change Admin PIN (requires current Admin PIN) + +### PIN Retry Counters + +OpenPGP cards have separate retry counters for User PIN, Reset Code, and +Admin PIN. The factory state counter reads `3 0 3`: + +- `3` — User PIN attempts remaining +- `0` — Reset Code not configured (factory state; not exhausted) +- `3` — Admin PIN attempts remaining + +When a counter reaches 0 the corresponding PIN is blocked. A blocked User +PIN can be unblocked with the Admin PIN. A blocked Admin PIN **cannot be +recovered** — the card must be fully reset (destroying all keys). + +## Full Card Reset (last resort) + +If the Admin PIN is blocked or the card is in an unrecoverable state: + +```bash +gpg-connect-agent << 'EOF' +/hex +scd serialno +scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 81 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 +scd apdu 00 20 00 83 08 40 40 40 40 40 40 40 40 +scd apdu 00 e6 00 00 +scd apdu 00 44 00 00 +EOF +``` + +This sends deliberate wrong PINs to exhaust both the User and Admin PIN +counters, then issues the APDU sequence to fully reset the OpenPGP applet. +**All keys on the card are destroyed.** Run `OEM Factory Reset / Re-Ownership` +afterwards. + +## Adding an Existing Public Key + +If the dongle is already provisioned and you need to inject the matching +public key into the Heads firmware: + +1. Copy the public key (`.asc`) to a USB drive. +2. Insert the dongle and the USB drive. +3. From Heads: `Options -> GPG Management -> Add a GPG key to the running BIOS + reflash`. +4. Reboot. Generate a new TOTP/HOTP secret when prompted. + +## Nitrokey 3 Specifics + +- Supports NIST P-256 ECC keys in addition to RSA — significantly faster key + generation and signing. +- Secrets app (HOTP) PIN is separate from the OpenPGP card Admin PIN. +- Physical touch confirmation is required for some operations (initialize, + key generation). +- Secrets app PIN reset is available via `Options -> OEM Factory Reset / + Re-Ownership` without a full card reset. + +## TODO + +- Populate the GPG key's preferred keyserver field and the card `url` field + after uploading the public key to `keys.openpgp.org`. Requires network + access in the initrd. See `set_card_identity()` in `initrd/bin/oem-factory-reset`. diff --git a/doc/keys.md b/doc/keys.md new file mode 100644 index 000000000..d57a80fc9 --- /dev/null +++ b/doc/keys.md @@ -0,0 +1,115 @@ +# Keys and Secrets in Heads + +Heads uses several distinct secrets and keys, each protecting a different layer +of the system. Understanding what each one does helps in choosing appropriate +passphrases and in recovery scenarios. + +## TPM Owner Passphrase + +The TPM is "owned" by the user. Setting the owner passphrase clears all +existing NVRAM and spaces. An attacker who controls this passphrase can reseal +the TPMTOTP shared secret but cannot decrypt the disk without the Disk Recovery +Key passphrase. + +**Recommended length:** 2 Diceware words (1-32 characters, TPM hardware limit). + +## TPMTOTP / HOTP Shared Secret + +A random 20-byte value generated during OEM Factory Reset / Re-Ownership. + +- **TOTP (smartphone):** sealed into TPM NVRAM against PCR values; on each + boot Heads unseals it if PCRs match and displays the current TOTP code for + comparison against a phone app. Requires correct UTC time in Heads. +- **HOTP (USB Security dongle):** same secret sealed to dongle; dongle + verifies automatically and shows a green/red LED. No accurate time needed. + +A new secret must be generated after each firmware update. + +## TPM Counter Key + +Increment-only counters prevent rollback attacks. An attacker controlling +this key can only cause a denial-of-service by incrementing the counter. + +## GPG Admin PIN + +Protects management operations on the OpenPGP smartcard inside the USB +Security dongle. Required to seal HOTP measurements under Heads. + +- Locks after **3 consecutive wrong attempts** — do not forget it. +- Can be used to unblock a locked GPG User PIN via `gpg --change-pin`. +- **Recommended length:** 2 Diceware words (6-25 characters in Heads). + +## GPG User PIN + +Used to sign and encrypt content and for all user interactions with the USB +Security dongle. Heads prompts for this when signing `/boot` hashes. + +- Locks after **3 consecutive wrong attempts**. +- **Recommended length:** 2 Diceware words (6-25 characters in Heads). + +## Disk Recovery Key Passphrase + +The primary LUKS passphrase set at OS installation. Processed through PBKDF2 +(LUKS1) or Argon2 (LUKS2) to derive the actual disk encryption key. + +- Required to access encrypted data from any computer (without TPM). +- Required to set up or recover a TPM Disk Unlock Key. +- Required when "unsafe booting" — the OS prompts for it directly. +- **Recommended length:** 6 Diceware words. + +## TPM Disk Unlock Key Passphrase + +An additional LUKS key sealed in TPM NVRAM with PCR values for firmware, +kernel modules, and LUKS headers. Released only when Heads boots unmodified +from the expected firmware. + +- Ties the disk to one machine. +- In recovery mode PCRs will not match; use the Disk Recovery Key instead. +- After 3 failed unlock attempts Heads falls back to the Disk Recovery Key. +- **Recommended length:** 3 Diceware words. + +## Owner's GPG Key + +Generated during OEM Factory Reset / Re-Ownership. Private key lives on the +USB Security dongle's OpenPGP smartcard. Public key is fused into the Heads +firmware image and used to verify `/boot` signatures on every boot. + +## TPM PCR Map + +| PCR | Content | +|-----|---------| +| 0 | (reserved; populated by binary blobs where applicable for SRTM) | +| 1 | (reserved) | +| 2 | coreboot bootblock, ROM stage, RAM stage, Heads Linux kernel + initrd | +| 3 | (reserved) | +| 4 | Boot mode (0 during `/init`, then `recovery` or `normal-boot`) | +| 5 | Heads Linux kernel modules | +| 6 | Drive LUKS headers | +| 7 | Heads user-specific CBFS files (config.user, GPG keyring, etc.) | +| 16 | Used for TPM future-calc of LUKS header during DUK setup | + +Secrets sealed against PCRs 2, 4, 5, 6, 7. If any of these change +(firmware update, kernel module change, LUKS header change, config change) +unseal operations fail until secrets are re-sealed. + +## TPM Unseal Errors + +`Error Authentication failed (Incorrect Password) from TPM_Unseal` +— PCRs match but the passphrase is wrong (expected; just re-enter it). + +Any other TPM_Unseal error means the PCR measurements differ from when +secrets were sealed — potential tampering or an unsigned firmware update. + +Review the PCR2 TCPA event log from Recovery Shell: + +``` +cbmem -L +``` + +## LUKS Key Derivation + +Both the Disk Recovery Key passphrase and the TPM Disk Unlock Key passphrase +are processed through the LUKS key derivation function (PBKDF2 for LUKS1, +Argon2 for LUKS2) before being compared against the stored key slot. The +actual disk encryption key lives only in the LUKS header; passphrases never +directly encrypt disk data. diff --git a/doc/logging.md b/doc/logging.md index 7bfdea0ba..6a7be6cd3 100644 --- a/doc/logging.md +++ b/doc/logging.md @@ -3,56 +3,96 @@ Heads produces debug logging to aid development and troubleshooting. Logging is produced in scripts at a _log level_. -Users can set an _output level_ that controls how much output they see on the screen. +Users can set an _output level_ that controls how much output they see on the **screen**. -# Log Levels +**`/tmp/debug.log` always captures every log level regardless of output mode.** +This makes it a complete diagnostic artifact that can be shared with developers after any issue, +without requiring the user to reproduce the problem in debug mode first. +Console visibility is what varies by mode - the log file never loses information. + +## Log Levels In order from "most verbose" to "least verbose": -LOG > TRACE > DEBUG > INFO > (console) > NOTE > warn +LOG > TRACE > DEBUG > INFO > STATUS / STATUS_OK > NOTE > WARN > DIE -("console" level output is historical and should be replaced with INFO.) +("console" level output is historical and should be replaced with INFO or STATUS.) ## LOG LOG is for very detailed output or output with uncontrolled length. -It never goes to the screen, this always goes to the log file. -Usually, we dump outputs of commands like 'lsblk', 'lsusb', 'gpg --list-keys', etc. at LOG level (using DO_WITH_DEBUG or SINK_LOG), so we can tell the state of the system from a log submitted by a user. -We rarely want these on the console as they usually hide more relevant output with information that we already know. +It never goes to the screen. It always goes to debug.log. +Usually, we dump outputs of commands like `lsblk`, `lsusb`, `gpg --list-keys`, etc. at LOG level +(using `DO_WITH_DEBUG` or `SINK_LOG`), so we can tell the state of the system from a log submitted +by a user. We rarely want these on the console as they usually hide more relevant output. Use this in situations like: -* Dumping information about the state of the system for debugging. The output doesn't indicate any specific action/decision in Heads or a problem, it's just state relevant for troubleshooting the rest of the log. -* Tracing something that might be very long (including "we don't know how long this will be", even if it's sometimes short). Very long output isn't useful on the console, since you can't scroll back, and it hides more important information. -* The output is intended for debugging a specific topic, and usually unintersting otherwise. We want to be able to turn up output to DEBUG/TRACE when working on any topic without excessively filling the console with every topic's detailed output. + +* Dumping information about the state of the system for debugging. The output doesn't indicate any + specific action/decision in Heads or a problem - it's just state relevant for troubleshooting. +* Tracing something that might be very long. Very long output isn't useful on the console since you + can't scroll back, and it hides more important information. +* Output intended for debugging a specific topic that is usually uninteresting otherwise. ## TRACE TRACE is for following execution flow through Heads. -(TRACE_FUNC logs the current source location at TRACE level, you can use this when entering a function or script, this is much more common than using TRACE directly.) +(`TRACE_FUNC` logs the current source location at TRACE level. Use this when entering a function +or script - this is much more common than using TRACE directly.) You can also use TRACE to show parameter values to scripts or functions. -Since TRACE is for execution flow, show the unprocessed parameters as provided by the caller, not an interpreted version. -(This is uncommon though as it is very verbose, and we can also capture interesting call sites with DO_WITH_DEBUG.) +Since TRACE is for execution flow, show the unprocessed parameters as provided by the caller, not +an interpreted version. (This is uncommon as it is very verbose; we can also capture interesting +call sites with `DO_WITH_DEBUG`.) + +If you are tracing the result of a decision, consider using DEBUG instead. + +### Reading TRACE_FUNC output + +Each TRACE_FUNC call emits the full call chain leading to the current function. +The format is: + +```text +TRACE: caller(file:line) -> ... -> current_func(file:line) +``` -You can invoke TRACE to show specific execution flow when needed, but if you are tracing the result of a decision, consider using DEBUG instead. +The line number in each entry means something different depending on position: + +* **Non-last entries**: the line number is the **call site** - the line within that function where + it called the next function in the chain. +* **Last entry**: the line number is where **TRACE_FUNC itself** is called inside the current + function (typically the first line of the function body). + +Example - a `tpmr unseal` call triggered from `gui-init`: + +```text +TRACE: main(/init:0) -> main(/bin/gui-init:0) -> main(/bin/tpmr:0) -> main(/bin/tpmr:1037) -> tpm2_unseal(/bin/tpmr:635) +``` + +* `main(/init:0)` - `/init` is the root script; `:0` marks a cross-process boundary +* `main(/bin/gui-init:0)` - `gui-init` was launched by `/init` as a subprocess +* `main(/bin/tpmr:0)` - `tpmr` was launched by `gui-init` as a subprocess +* `main(/bin/tpmr:1037)` - line 1037 in `tpmr`'s `main` is the call site of `tpm2_unseal "$@"` +* `tpm2_unseal(/bin/tpmr:635)` - line 635 is where `TRACE_FUNC` is in `tpm2_unseal` Use this in situations like: + * Following control flow - use TRACE_FUNC when entering a script or function -* Showing the parameters used to invoke a script/function, when they are especially relevant and not excessively verbose +* Showing the parameters used to invoke a script/function, when especially relevant ## DEBUG DEBUG is for most log information that is relevant if you are a Heads developer. -Use DEBUG to highlight the decisions made in script logic, and the information that affects those decisions. -Generally, focus on decision points (if, else, case, while, for, etc.), because we can keep following straight-line execution without further tracing. +Use DEBUG to highlight the decisions made in script logic, and the information that affects those +decisions. Generally, focus on decision points (if, else, case, while, for, etc.), because we can +keep following straight-line execution without further tracing. -Decision points usually capture program behavior the best. -Show the information that is about to influence a decision (`DEBUG "Found ${#DEVS[@]} block devices: to check for LUKS:" "${DEVS[@]}"`) and/or the results of the decision (`DEBUG "${DEVS[$i]} is not a LUKS device, ignore it`). +Show the information that is about to influence a decision and/or the results of the decision. -Use DO_WITH_DEBUG to capture a particular command execution to the debug log. -The command and its arguments are captured at DEBUG level (as they usually indicate the decisions the command will make), and the command's stdout/stderr are captured at LOG level. -See DO_WITH_DEBUG for examples of usage. +Use `DO_WITH_DEBUG` to capture a particular command execution to the debug log. +The command and its arguments are captured at DEBUG level (as they usually indicate the decisions +the command will make), and the command's stdout/stderr are captured at LOG level. Use this in situations like: @@ -61,129 +101,260 @@ Use this in situations like: ## INFO -INFO is for contextual information that may be of interest to end users, but that is not required for use of Heads. -Users can control whether this is displayed on the console. +INFO is for contextual information that may be of interest to end users, but that is not required +for use of Heads. + +INFO always goes to debug.log. It is shown on the console in info and debug modes, and suppressed +from the console in quiet mode (where the log file serves as the post-mortem record). -Users might use this to troubleshoot Heads configuration or behavior, but this should not require knowledge of Heads implementation or developer experience. +Users might use this to troubleshoot Heads configuration or behavior, but this should not require +knowledge of Heads implementation or developer experience. For example: -* "Why can't I enable USB keyboard support?" `INFO "Not showing USB keyboard option, USB keyboard is always enabled for this board"` -* "Why isn't Heads booting automatically?" `INFO "Not booting automatically, automatic boot is disabled in user settings"` -* "Why didn't Heads prompt me for a password?" `INFO "Password has not been changed, using default"`) +* "Why can't I enable USB keyboard support?" `INFO "Not showing USB keyboard option, USB keyboard is always enabled for this board"` +* "Why isn't Heads booting automatically?" `INFO "Not booting automatically, automatic boot is disabled in user settings"` +* "Why didn't Heads prompt me for a password?" `INFO "Password has not been changed, using default"` These do not include highly technical details. -They can include configuration values or context, _but_ they should refer to configuration settings using the user-facing names in the configuration menus. +They can include configuration values or context, _but_ they should refer to configuration settings +using the user-facing names in the configuration menus. Use this in situations like: -* Showing very high level decision-making information, which is reasonably understandable for users not familiar with Heads implementation +* Showing very high level decision-making information, understandable for users not familiar with + Heads implementation * Explaining a behavior that could reasonably be unexpected for some users ## console -This level is historical, use INFO for this. -It is documented as there are still some occurrences in Heads, usually `echo`, `echo >&2`, or `echo >/dev/console`, each intended to produce output directly on the console. -The intent is the same as INFO. +This level is historical, use INFO or STATUS for this. +It is documented as there are still some occurrences in Heads, usually `echo`, `echo >&2`, or +`echo >/dev/console`, each intended to produce output directly on the console. -(This is different from `echo` used to produce output that might be captured by a caller, which is not logging at all.) +(This is different from `echo` used to produce output that might be captured by a caller, which is +not logging at all.) -Avoid using this, and change existing console output to INFO or another level. +Avoid using this, and change existing console output to INFO, STATUS, or another appropriate level. -## NOTE +## STATUS -NOTE is for contextual information explaining something that is _likely_ to be unexpected or confusing to users new to Heads. +STATUS is for action announcements - operations that are starting or in progress - that all users +must see regardless of output mode. -Unlike INFO, it cannot be hidden. Use this only if the behavior is likely to be unexpected or confusing to many users. If it is only possibly unexpected or uncommon that it is confusing, consider INFO instead. +A STATUS message typically precedes a STATUS_OK, WARN, or DIE: it announces the start of something +that has an outcome. If there is no outcome to report, consider INFO instead. -Do not overuse this above INFO. Adding too much output at NOTE causes users to ignore it, as there is too much output. +Use STATUS when an action is beginning or underway: -For example: +* "Verifying ISO" - a signature check is running (→ STATUS_OK or DIE follows) +* "Unlocking LUKS device(s) using the Disk Recovery Key passphrase" - an unlock is in progress (→ STATUS_OK or WARN follows) +* "Executing default boot for $name" - what is about to boot (→ WARN or DIE on failure) +* "GPG User PIN retries remaining: N" - state shown before an operation that will consume a PIN attempt + +Do NOT use STATUS for descriptions of what will happen based on a user's configuration choice +("Master key will be generated in memory") — use INFO for those. + +Unlike INFO, STATUS is always visible on the console in all output modes. +Unlike NOTE, STATUS does not sleep - it is for routine progress announcements. + +STATUS always goes to debug.log. + +## STATUS_OK + +STATUS_OK is for confirmed successful results - use it when reporting that an operation succeeded, +a verification passed, or a resource was confirmed available. + +Use STATUS_OK (not STATUS) for completed positive outcomes: + +* "ISO signature verified" - verification succeeded +* "LUKS device unlocked successfully" - unlock confirmed +* "GPG signature on kexec boot params verified" - integrity check passed +* "Heads firmware job done - starting your OS" - handoff complete + +STATUS_OK uses two signals so success is scannable without relying on either alone: + +* **`OK` text prefix** — readable in monochrome, on serial consoles, and with color vision deficiency +* **Bold green color** — instant visual scan for sighted users + +This follows the Linux/systemd `[OK]`/`[FAILED]` convention: always pair color with a text label. +The console renders `OK message` (with a leading space) in bold green; debug.log records it in plain text. + +## NOTE -* "Rebooting in 3 seconds to enable booting default boot option". Users probably don't expect the firmware to reboot to accomplish this behavior, this is unique to Heads. Without a message justifying the reboot, it would likely appear that the firmware faulted and reset unexpectedly. -* "Your GPG User PIN, followed by Enter key will be required [...]". GPG prompts are very confusing to users unfamiliar with GPG (which is most users). +NOTE is for contextual information explaining something that is _likely_ to be unexpected or +confusing to users new to Heads. -## warn +Unlike INFO, it cannot be hidden from the console. Use this only if the behavior is likely to be +unexpected or confusing to many users. If it is only possibly unexpected, consider INFO instead. -warn is for output that indicates a problem. We think the user should act on it, but we are able to continue, possibly with degraded functionality. -(This level and the utility function are lowercase, as they predate the other levels.) +Do not overuse this above INFO. Adding too much output at NOTE causes users to ignore it. -This is apppriate when _all_ of the following are true: +NOTE always goes to debug.log. -- there is a _likely_ problem -- we are able to continue, possibly with degraded functionality -- the warning is _actionable_ - there is a reasonable change that could silence the warning if this is intentional +Two specific patterns where NOTE is the right level: -**Do not overuse this.** Overuse of this level causes user to become accustomed to ignoring warnings. -This level only has value as long as it does not occur frequently, so users will notice warnings. +**Security reminders** — advice about consequences or risks the user should not overlook, +but that do not indicate a current problem: -Warnings must indicate a _likely_ problem. -(Not a rare or remote possibility of a problem.) +* "Please keep your GPG key material backup thumb drive safe" +* "Subkeys will NOT be copied to USB Security dongle" -Warnings are only appropriate if we're able to continue operating. -If we can't, consider prompting the user instead, since we cannot do what they asked. +**Hand-off to uncontrolled output** — when Heads is about to hand control to a tool it does not +own (gpg, cryptsetup, lvm, hardware firmware), and the user will interact directly with that +tool's prompts or output rather than Heads-formatted messages: -Warnings must be _actionable_. Only warn if there is a reasonable change the user can make to avoid the warning. +* "GPG User PIN required at next smartcard prompt" - the user will type into gpg's own PIN prompt +* "Nitrokey 3 requires physical presence: touch the dongle when prompted" - hardware-level event +* "Please authenticate with OpenPGP smartcard/backup media" - gpg auth flow follows For example: -* Warning when using default passphrases that are completely insecure is reasonable - the user has no security, and if they want that, they should use Basic mode. -* Warning when an unknown variable appears in config.user is not reasonable - there's no reasonable way for the user to address this. -# Output Levels +* "Proceeding with unsigned ISO boot" - booting without a verified signature is unexpected and + carries risk; the user needs to know it is happening deliberately. +* "TOTP secret no longer accessible: TPM secrets were wiped" - mid-session secret loss requires + immediate user attention. + +## WARN + +WARN is for output that indicates a problem. We think the user should act on it, but we are able +to continue, possibly with degraded functionality. + +This is appropriate when _all_ of the following are true: + +* there is a _likely_ problem +* we are able to continue, possibly with degraded functionality +* the warning is _actionable_ - there is a reasonable change that could silence the warning + +**Do not overuse this.** Overuse of this level causes users to become accustomed to ignoring +warnings. This level only has value as long as it does not occur frequently. + +Warnings must indicate a _likely_ problem (not a rare or remote possibility). +Warnings are only appropriate if we are able to continue operating. +Warnings must be _actionable_ - only WARN if there is a reasonable change the user can make. -Users can choose one of three output levels for extra console information. +WARN always goes to debug.log. -* None - Show no extra output. Only warnings appear on console. (Some 'console' level output appears that has not been addressed yet.) -* Info - Show information about operations in Heads. (INFO and below.) -* Debug - Show detailed information suitable for debugging Heads. (TRACE and below.) Log file captures all levels. +For example: + +* Warning when using default passphrases that are completely insecure is reasonable. +* Warning when an unknown variable appears in config.user is not reasonable - there's no reasonable + way for the user to address this. -TODO: Document what happens for kernel messages too. -This is more complex though since it is influenced by the board's config and user config differently (maybe we should improve that.) +## DIE -TODO: Document the variables that control these levels +DIE is for fatal errors from which Heads cannot recover. Execution stops after DIE. -## None - no extra output +DIE always goes to debug.log and is always shown on the console regardless of output mode. -| Sink | LOG | TRACE | DEBUG | INFO | console | NOTE | warn | -|-------------------------|-----|-------|-------|------|---------|------|------| -| Console (via /dev/kmsg) | | | | | Yes* | Yes | Yes | -| /tmp/debug.log | Yes | | | | | | | +## INPUT -* Most 'console' output should be changed to INFO, that content isn't intended to be displayed in quiet mode +INPUT is a direct replacement for the `echo "prompt"; read [flags] VAR` pattern. +It displays the prompt in **bold white** to visually distinguish interactive input requests from +progress/info messages. -No extra output is specified with: +Usage: `INPUT "prompt text" [read-flags] [VARNAME]` +```bash +# Instead of: +echo "Enter passphrase:" +read -r -s passphrase + +# Use: +INPUT "Enter passphrase:" -r -s passphrase ``` + +The prompt text and `INPUT:` label are always recorded in debug.log for tracing. +All read flags (`-r`, `-s`, `-n N`, etc.) and the variable name are passed through unchanged to `read`. + +INPUT displays the prompt with a trailing space and no newline, so the cursor lands immediately +after the prompt text on the same line — the user types on the same line, never on the next line. +A blank line is printed after the user's input to separate it from subsequent output. + +Do NOT use INPUT for yes/no confirmation dialogs inside whiptail GUI flows — use whiptail for those. +INPUT is appropriate for inline `[Y/n]` confirmations in terminal-mode scripts (recovery shell, +setup wizards, debug paths) where a full whiptail dialog would be out of place. + +## Output Levels + +Users can choose one of three output levels for console information. +**`/tmp/debug.log` always captures all levels regardless of the chosen output level.** + +* **Quiet** - Minimal console output. STATUS, NOTE, WARN and DIE always appear. INFO is suppressed. + Use this for production/unattended systems where the log file is the post-mortem record. +* **Info** - Show information about operations in Heads. INFO and above appear on console. + Use this for interactive use where the user is watching the screen. +* **Debug** - Show detailed information suitable for debugging Heads. TRACE and DEBUG also appear + on console. Use this when actively developing or diagnosing Heads. + +Console output styling - chosen for accessibility across color-deficiency types (WCAG 1.4.1: +color is never the sole signal; text prefixes carry meaning independently): + +| Level | Style | ANSI code | Rationale | +|-----------|--------------|--------------|---------------------------------------------------------------------------------------------------------------------| +| DIE | bold red | `\033[1;31m` | Red = universal danger signal; `!!! ERROR:` prefix is the semantic carrier | +| WARN | bold yellow | `\033[1;33m` | Most universally perceptible alert color across deuteranopia, protanopia, tritanopia | +| NOTE | italic white | `\033[3;37m` | White = highest-contrast neutral on dark consoles; italic separates NOTE from bold STATUS/WARN, no semantic hue | +| STATUS | bold only | `\033[1m` | In-progress actions - bold without hue readable in every terminal theme; `>>` prefix differentiates semantically | +| STATUS_OK | bold green | `\033[1;32m` | Confirmed success - green is universally understood as success; scannable at a glance against plain bold STATUS | +| INFO | green | `\033[0;32m` | Standard informational color; INFO is optional context, its absence on console is harmless | +| INPUT | bold white | `\033[1;37m` | Maximum contrast (21:1) on VGA/dark consoles; no color dependency, readable under all deficiency types | + +debug.log and /dev/kmsg always receive plain text without ANSI codes. + +All console output goes to **`/dev/console`** — the kernel console device, which follows +the `console=` kernel parameter and reaches whatever output the system was configured for +(serial port, framebuffer, BMC console, etc.) without requiring any process setup. +This means callers never need to care about redirections: a caller that does +`2>/tmp/whiptail` or `>/boot/kexec_tree.txt` will not accidentally capture log output. +Similarly, scripts that use stdout for a structured protocol can safely call STATUS, +STATUS_OK, and any other logging function — log output never appears on stdout. + +NOTE, WARN and DIE print a blank line before and after the message so they stand out visually +from surrounding output. STATUS and STATUS_OK do **not** — they are called frequently and blank +lines would make output very noisy. Use NOTE when a sleep and blank lines are needed. +INPUT displays the prompt inline (no leading blank line); the cursor stays on the same line as the prompt. + +### None / Quiet - minimal console output + +| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | +|----------------|-----|-------|-------|------|--------|-----------|------|------|-----| +| Console | | | | | Yes | Yes | Yes | Yes | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | + +Quiet output is specified with: + +```text CONFIG_DEBUG_OUTPUT=n CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n CONFIG_QUIET_MODE=y ``` -## Info +### Info -| Sink | LOG | TRACE | DEBUG | INFO | console | NOTE | warn | -|-------------------------|-----|-------|-------|------|---------|------|------| -| Console (via /dev/kmsg) | | | | Yes | Yes | Yes | Yes | -| /tmp/debug.log | Yes | | | | | | | +| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | +|----------------|-----|-------|-------|------|--------|-----------|------|------|-----| +| Console | | | | Yes | Yes | Yes | Yes | Yes | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Info output is enabled with: -``` +```text CONFIG_DEBUG_OUTPUT=n CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=n CONFIG_QUIET_MODE=n ``` -## Debug +### Debug -| Sink | LOG | TRACE | DEBUG | INFO | console | NOTE | warn | -|-------------------------|-----|-------|-------|------|---------|------|------| -| Console (via /dev/kmsg) | | Yes | Yes | Yes | Yes | Yes | Yes | -| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| Sink | LOG | TRACE | DEBUG | INFO | STATUS | STATUS_OK | NOTE | WARN | DIE | +|----------------|-----|-------|-------|------|--------|-----------|------|------|-----| +| Console | | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | +| /tmp/debug.log | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Debug output is enabled with: -``` +```text CONFIG_DEBUG_OUTPUT=y CONFIG_ENABLE_FUNCTION_TRACING_OUTPUT=y CONFIG_QUIET_MODE=n diff --git a/doc/prerequisites.md b/doc/prerequisites.md new file mode 100644 index 000000000..c6fa55815 --- /dev/null +++ b/doc/prerequisites.md @@ -0,0 +1,60 @@ +# Prerequisites + +## USB Security Dongles + +All USB Security dongles used with Heads must support the **OpenPGP card +applet**. FIDO2 and U2F are not used by Heads. + +HOTP verification requires a dongle with HOTP support and a compatible firmware +version. Without HOTP, Heads falls back to TPMTOTP (smartphone-based). + +| Dongle | OpenPGP | HOTP | Notes | +|--------|---------|------|-------| +| Nitrokey Pro 2 | Yes | Yes | Full support | +| Nitrokey Storage 2 | Yes | Yes | Full support | +| Nitrokey 3 | Yes | Yes | Full support; p256 ECC available | +| Purism Librem Key | Yes | Yes | Full support; rebranded NK Pro | +| YubiKey 5 Series | Yes | No | OpenPGP signing only; no HOTP | +| Nitrokey Pro (v1, fw < 0.8) | Yes | Limited | Older firmware may report no HOTP support; test before use | + +Heads detects dongle branding at runtime via USB VID:PID: + +| VID:PID | Dongle | +|---------|--------| +| `20a0:42b2` | Nitrokey 3 | +| `20a0:4108` | Nitrokey Pro | +| `20a0:4109` | Nitrokey Storage | +| `316d:4c4b` | Purism Librem Key | + +## HOTP vs. TPMTOTP + +**HOTP (recommended when available):** +- Heads generates HOTP codes and the dongle verifies them automatically. +- Pass = green LED, fail = red LED and boot halt. +- Does not require accurate time. + +**TPMTOTP (smartphone fallback):** +- Heads generates a TOTP code on screen; the user compares it against a phone + app (Google Authenticator, FreeOTP+, etc.). +- Requires correct UTC time set in `Options -> Time`. +- Less automated — relies on the user noticing a mismatch. + +## OS Requirements + +- A dedicated `/boot` partition (not `/boot` inside an LVM or btrfs subvolume + unless the board config supports it). +- LUKS-encrypted root (for TPM Disk Unlock Key functionality). + +## Supported Flashing Methods + +See board-specific configs under `boards/`. Most x86 boards support: + +- External SPI flashing (initial install) via `flashprog`. +- Internal flashing (upgrades) via `Options -> Flash/Update BIOS` for firmware + built after November 2023. + +Run from Recovery Shell to verify internal flash is unlocked: + +``` +flashprog -p internal +``` diff --git a/doc/qemu.md b/doc/qemu.md new file mode 100644 index 000000000..866e5ede0 --- /dev/null +++ b/doc/qemu.md @@ -0,0 +1,209 @@ +qemu-coreboot-(fb)whiptail-tpmX(-hotp) boards +=== + +The `qemu-coreboot-fbwhiptail-tpm1-hotp` configuration (and their variants) permits testing of most features of Heads. + It requires a supported USB token (which will be reset for use with the VM, do not use a token needed for a + real machine). With KVM acceleration, speed is comparable to a real machine. If KVM is unavailable, + lightweight desktops are still usable. + +Heads is currently unable to reflash firmware within qemu, which means that OEM reset and re-ownership + cannot be fully performed within the VM. Instead, a GPG key can be injected in the Heads image from the + host during the build. + +The TPM and disks for this configuration are persisted in the build/qemu-coreboot-fbwhiptail-tpm1-hotp/ directory by default. + +Bootstrapping a working system +=== + +Important: The supported and tested workflow uses the provided Docker wrappers (`./docker_repro.sh`, `./docker_local_dev.sh`, or `./docker_latest.sh`). Host-side installation of QEMU, `swtpm`, or other QEMU-related tooling is unnecessary and is not part of the standard, supported workflow; only advanced or edge-case scenarios should install those tools on the host (see 'Troubleshooting' below for guidance). + +1. Install Docker + * Install Docker (docker-ce) for your OS by following Docker's official installation guide: https://docs.docker.com/engine/install/ + +Note: the Nix-built Docker images used by `./docker_repro.sh` include +QEMU (`qemu-system-x86_64`), `swtpm` / `libtpms`, `canokey-qemu` (a +virtual OpenPGP smartcard), and other userspace tooling required to +build and test QEMU targets. These images are intended to be +self-contained for QEMU testing; host-focused build instructions +(e.g., building `swtpm` on the host) were removed to avoid +divergence—use the Docker wrappers for the tested workflow. + +If you do not specify `USB_TOKEN` when running QEMU targets, the +container will use the included `canokey-qemu` virtual token by +default. To forward a hardware token from the host, set `USB_TOKEN` or +pass `hostbus`/`hostport`/`vendorid,productid` to the make invocation. + +If you plan to manage disk images or use `qemu-img` snapshots on the +host (outside the container), install the `qemu-utils` package locally +(which provides `qemu-img`). + + +2. Build Heads + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp` +3. Install OS + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp INSTALL_IMG=<~/heads/path_to_iso.iso> run` + * Lightweight desktops (XFCE, LXDE, etc.) are recommended, especially if KVM acceleration is not available (such nested in Qubes OS) + * When running nested in a qube, disable memory ballooning for the qube, or performance will be very poor. + * Include `QEMU_MEMORY_SIZE=6G` to set the guest's memory (`6G`, `8G`, etc.). The default is 4G to be conservative, but more may be needed depending on the guest OS. + * Include `QEMU_DISK_SIZE=30G` to set the guest's disk size, the default is `20G`. +4. Shut down and boot Heads with the USB token attached, proceed with OEM reset + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp USB_TOKEN= run` + * If you do not set `USB_TOKEN`, the included `canokey-qemu` virtual token will be used by default. + * For ``, use one of: + * `NitrokeyPro` - a Nitrokey Pro by VID/PID + * `NitrokeyStorage` - a Nitrokey Storage by VID/PID + * `Nitrokey3NFC` - a Nitrokey 3 by VID:PID + * `LibremKey` - a Librem Key by VID/PID + * `hostbus=#,hostport=#` - indicate a host bus and port (see qemu usb-host) + * `vendorid=#,productid=#` - indicate a device by VID/PID (decimal, see qemu usb-host) + * You _do_ need to export the GPG key to a USB disk, otherwise defaults are fine. + * Head will show an error saying it can't flash the firmware, continue + * Then Heads will indicate that there is no TOTP code yet, at this point shut down (Continue to main menu -> Power off) +5. Get the public key that was saved to the virtual USB flash drive + * `sudo mkdir /media/fd_heads_gpg` + * Attach the image and print the loop device in one step: + + sudo losetup --find --show --partscan ./build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/usb_fd.raw + + The command prints the loop device used (for example `/dev/loop0`) and the kernel will create partition nodes such as `/dev/loop0p1` and `/dev/loop0p2` when supported. + + Then mount the appropriate partition (usually the second/public partition): + + sudo mount /dev/loop0p2 /media/fd_heads_gpg # adjust based on the loop device reported above + + * Look in `/media/fd_heads_gpg` and copy the most recent public key + * `sudo umount /media/fd_heads_gpg` + * `sudo losetup --detach /dev/loop0` +6. Inject the GPG key into the Heads image and run again + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp PUBKEY_ASC= inject_gpg` + * `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1-hotp USB_TOKEN=LibremKey PUBKEY_ASC= run` +7. Initialize the TPM - select "Reset the TPM" at the TOTP error prompt and follow prompts +8. Select "Default boot" and follow prompts to sign /boot for the first time and set a default boot option + +You can reuse an already created ROOT_DISK_IMG by passing its path at runtime. +Ex: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 run` + +Note: hardlinks are your friend. You can (should?) have qemu disk images kept somewhere (cp/mv) ~/qemu_img/test.qcow2 and do: + * `cp -alf ~/qemu_img/test.qcow2 ~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2` + +This way, if you accidentally wipe ~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2, the original is kept intact. +Also note that hardlinks share the same underlying data; modifications to one linked copy affect them all, and the filesystem maintains a link count to track how many references exist. + +`cp -alf` is basically creating a hardlink to destination overwriting it, and doesn't cost additional disk space. + +On a daily development cycle, usage looks like: +1. `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 inject_gpg` +2. `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm1 PUBKEY_ASC=~/pub_key_counterpart_of_usb_dongle.asc USB_TOKEN=NitrokeyStorage ROOT_DISK_IMG=~/heads/build/x86/qemu-coreboot-fbwhiptail-tpm1-hotp/root.qcow2 run` + +The first command builds the latest uncommitted/unsigned changes and injects the public key inside the ROM to be run by the second command. +To test across all qemu variants, one only has to change BOARD name and run the two previous commands, adapting `QEMU_MEMORY_SIZE=1G` or modifying the file directly under build dir to adapt to host resources. + + +Running via Docker wrappers +=== +We provide convenient wrapper scripts at the repository root that encapsulate Docker invocation and automatically handle common host integrations needed for QEMU runs. + +Wrapper comparison +--- + +| Script | Image | Use | +|---|---:|---| +| `docker_latest.sh` | Defaults to pinned digest when available | Convenience: run the latest published image | +| `docker_local_dev.sh` | `linuxboot/heads:dev-env` | Development: use local image built from the flake (rebuilds when flake files are dirty) | +| `docker_repro.sh` | Image pinned from `.circleci/config.yml` | Reproducible builds that match CircleCI | + +What the wrappers handle +--- + +Wrapper options: some runtime behavior is controlled via environment +variables documented in the repository README (see 'Wrapper options & +environment variables'). Wrapper scripts now have focused `--help` output +for their own variables, and `./docker/common.sh` prints the full +environment reference. Important ones are `HEADS_DISABLE_USB` +(set to `1` to disable automatic USB passthrough and cleanup) and +`HEADS_X11_XAUTH` (force mounting your `$HOME/.Xauthority`). + +Make variables such as `USB_TOKEN`, `PUBKEY_ASC`, `INSTALL_IMG`, +`QEMU_MEMORY_SIZE`, `QEMU_DISK_SIZE`, `ROOT_DISK_IMG`, `CPUS` and `V` +are forwarded to the `make` invocation and affect how +`targets/qemu.mk` runs QEMU. See `targets/qemu.mk` for token formats +and examples. + +Note: when USB passthrough is active the wrapper will warn and, on +interactive shells, give a 3s abort window before attempting to kill +processes that hold the token (e.g., `scdaemon`/`pcscd`) to free the +device; set `HEADS_DISABLE_USB=1` to opt out. + +- **KVM passthrough**: when `/dev/kvm` exists on the host the container is run with `/dev/kvm` mounted into the container, enabling KVM-accelerated QEMU. +- **X11 GUI support**: the wrappers mount the X11 socket and programmatically create a temporary Xauthority file (via `mktemp -t heads-docker-xauth-XXXXXX`, or `/tmp/.docker.xauth-` as fallback when mktemp is unavailable) when `xauth` is available; they fall back to mounting `${HOME}/.Xauthority` when needed and set `XAUTHORITY` inside the container so GTK/SDL QEMU windows work. The temp file is cleaned up automatically after `docker run` completes. + - To force mounting your `${HOME}/.Xauthority` regardless of socket detection, set `HEADS_X11_XAUTH=1`. +- **USB passthrough**: when host USB buses exist `/dev/bus/usb` is mounted into the container so VMs can access hardware tokens. To explicitly disable automatic USB passthrough set `HEADS_DISABLE_USB=1`. +- **USB token cleanup**: the wrappers attempt to detect and stop local GPG/toolstack processes (e.g., `scdaemon`, `pcscd`) which might hold USB tokens. Behavior notes: + - If `sudo` can be run without a password the cleanup runs silently. + - The cleanup avoids prompting for a password in non-interactive shells; it will prompt only when running interactively (attached to a TTY). To skip the cleanup entirely set `HEADS_DISABLE_USB=1`. +- **Convenience variables accepted by the wrappers**: `V=1` for verbose make output, `CPUS=N` to set parallelism for builds, and any `make` variables may be passed through to the container command. +- **Argument forwarding**: arguments given to the wrapper are forwarded directly to the container command (no special separator needed). For example: `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run`. + +Environment variables reference +--- + +| Variable | Default | Effect | +|---|---:|---| +| `HEADS_DISABLE_USB` | `0` | When `1`, disable automatic USB passthrough and USB cleanup | +| `HEADS_X11_XAUTH` | `0` | When `1`, mount `${HOME}/.Xauthority` into the container (force usage even when a programmatic Xauthority would otherwise be created) | +| `HEADS_SKIP_DOCKER_REBUILD` | `0` | When `1`, skip rebuilding the local Docker image when `flake.nix`/`flake.lock` are dirty | +| `HEADS_AUTO_INSTALL_NIX` | `0` | When `1`, automatically attempt single-user Nix install if `nix` is missing (suppresses prompt) | +| `HEADS_AUTO_ENABLE_FLAKES` | `0` | When `1`, automatically enable flakes by writing to `$HOME/.config/nix/nix.conf` (suppresses prompt) | +| `HEADS_MIN_DISK_GB` | `50` | Minimum free disk in GB required on `/nix` or `/` before attempting rebuild | +| `HEADS_SKIP_DISK_CHECK` | `0` | When `1`, skip the disk-space preflight check | + +Examples +--- + +- Reproducible (uses image version from CircleCI config): + - `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` + - `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=pubkey.asc USB_TOKEN=Nitrokey3NFC inject_gpg` + - `HEADS_DISABLE_USB=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 PUBKEY_ASC=pubkey.asc run` + - `HEADS_X11_XAUTH=1 ./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` + +- Local development image (uses locally built `linuxboot/heads:dev-env`): + - `./docker_local_dev.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2` + +- Published latest image (convenience): + - `./docker_latest.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` + +How I tested these wrappers (smoke checks) +--- + +- Minimal: `source docker/common.sh && build_docker_opts` — should print a short description and show flags such as `--device=/dev/kvm` when KVM is available and `-v /tmp/heads-docker-xauth-XXXXXX:...` (or `-v /tmp/.docker.xauth-:...` as fallback) when Xauthority was created. +- Functional (examples tested by PR author): see the tests in the PR body (Ubuntu, Debian, Fedora installer flows). Consider testing `./docker_repro.sh make BOARD=qemu-coreboot-fbwhiptail-tpm2 run` locally to verify KVM+GTK behavior. + +Troubleshooting +--- + +- Reuse provisioned canokey state across QEMU board build dirs: + - QEMU boards that use the default virtual token store canokey state at `build/x86//.canokey-file` (from `targets/qemu.mk`: `-device canokey,file=$(build)/$(BOARD)/.canokey-file`). + - After provisioning via Heads OEM reset/re-ownership in one QEMU board, you can copy that file into another QEMU board build directory to reuse the same virtual smartcard identity/public key material. + - Example: + - `cp build/x86/qemu-coreboot-fbwhiptail-tpm2/.canokey-file build/x86/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/.canokey-file` + - This is useful when troubleshooting TPM workflows while keeping the same token identity across variants. + +- TPM2 interaction capture (pcap) for debugging, similar to a bus sniffer: + - On TPM2 boards, set `export CONFIG_TPM2_CAPTURE_PCAP=y` in the board config. + - Heads `tpmr` then uses the pcap TCTI and writes captures to `/tmp/tpm0.pcap` inside the booted Heads environment. + - Save/copy that file from the guest (mount-usb --mode rw) and inspect it with Wireshark to analyze TPM command/response traffic. + - This is intended for TPM2 boards (for example the `qemu-coreboot-fbwhiptail-tpm2*` targets). + +- Quick checks: + - `echo $DISPLAY` — ensure `DISPLAY` is set on the host. + - `command -v xauth` — preferred for programmatic Xauthority cookies. + - `ls -l /dev/kvm` — verify `/dev/kvm` exists and is accessible. + - `groups | grep -q kvm` — confirm your user is in a group with access to KVM (or run with appropriate privileges). + - `source docker/common.sh && build_docker_opts` — inspect the options the wrapper will use without launching Docker. +- GUI issues: prefer installing `xauth` on the host so the wrappers can create a safe programmatic Xauthority file. As a last resort you can run `xhost +SI:localuser:root` (less secure). +- USB/GPG cleanup: if the cleanup is refusing to run due to non-interactive sudo, run the kill steps manually or set `HEADS_DISABLE_USB=1` to skip automatic cleanup. + +Notes +--- +- Ensure you have an X server available on the host; the wrappers forward `DISPLAY` automatically. +- If KVM is available but `/dev/kvm` is missing, load kernel modules (e.g., `kvm`, `kvm_intel`, `kvm_amd`) so `/dev/kvm` appears. diff --git a/doc/recovery-shell.md b/doc/recovery-shell.md new file mode 100644 index 000000000..fc94047ba --- /dev/null +++ b/doc/recovery-shell.md @@ -0,0 +1,94 @@ +# Recovery Shell + +The Recovery Shell is a full bash environment within the Heads initrd. It +gives direct access to block devices, GPG, TPM tools, and flash utilities. + +## Entering the Recovery Shell + +- At power-on: press `r` repeatedly during the Heads splash screen. +- From the Heads GUI: `Options -> Recovery Shell`. + +## Limitations + +The Recovery Shell boots with PCR 4 set to `recovery` instead of +`normal-boot`. This means: + +- **TPM-sealed secrets will not unseal** — PCRs no longer match. +- TOTP/HOTP sealing and TPM Disk Unlock Key creation/unsealing do not work. +- To perform seal/unseal operations return to the normal GUI boot. + +## Common Operations + +### Manual boot + +```bash +kexec-boot -b /boot -e 'foo|elf|kernel /vmlinuz|initrd /initrd.img|append root=/dev/whatever' +``` + +### Sign /boot after manual changes + +```bash +mount /dev/sdaX /boot +kexec-sign-config -p /boot +``` + +### Change GPG User PIN (locked out) + +With the dongle inserted: + +```bash +gpg --change-pin +``` + +Enter the Admin PIN when prompted, then set a new User PIN. + +### Read the TCPA event log (debug PCR mismatches) + +```bash +cbmem -L +``` + +Shows what was measured into each PCR during the current boot. Useful for +diagnosing unexpected TPM unseal failures. + +### Mount a USB drive + +```bash +mount-usb +``` + +Mounts the first detected USB partition at `/media`. For a specific device: + +```bash +mount-usb --device /dev/sdb1 --mode rw +``` + +### Flash firmware manually + +```bash +mount-usb +flashprog -p internal -w /media/heads-board-version.rom +``` + +Verify internal flash is unlocked first: + +```bash +flashprog -p internal +``` + +### Sign a detached ISO (for verified OS install from Recovery Shell) + +```bash +mount-usb --mode rw +cd /media +gpg --detach-sign +reboot +``` + +## After Recovery Shell Work + +If you modified `/boot` or reflashed firmware, return to the GUI and: + +1. Generate new TOTP/HOTP secret (`Options -> Generate new HOTP/TOTP secret`). +2. Update checksums and sign `/boot` (`Options -> Update checksums and sign all files in /boot`). +3. Optionally re-seal the TPM Disk Unlock Key by selecting a default boot option. diff --git a/doc/security-model.md b/doc/security-model.md new file mode 100644 index 000000000..2f16e7714 --- /dev/null +++ b/doc/security-model.md @@ -0,0 +1,484 @@ +# Heads Security Model + +This document describes the security architecture of Heads: how trust is +established, how integrity is verified at each boot, and how secrets are +protected. + +See also: [architecture.md](architecture.md), [tpm.md](tpm.md), +[boot-process.md](boot-process.md), [ux-patterns.md](ux-patterns.md). + +--- + +## Security Architecture Overview + +Heads implements a **cross-validated boot chain** where multiple security mechanisms +verify each other, preventing single points of failure. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ HEADS CROSS-VALIDATED BOOT CHAIN │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ SPI Flash │───▶│ TPM │───▶│ /boot │───▶│ OS │ │ +│ │ ROM │ │ PCRs │ │ GPG Sig │ │ Disk │ │ +│ │ (Hardware │ │ (Measured) │ │ (Signed) │ │ (LUKS) │ │ +│ │ RoT) │ │ │ │ │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ │ │ │ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ROLLBACK COUNTER (TPM NVRAM) │ │ +│ │ ┌───────────────┐ ┌───────────────┐ │ │ +│ │ │ TPM NVRAM │◀────────────▶│ /boot disk │ │ │ +│ │ │ (counter) │ 2-way │ kexec_ │ │ │ +│ │ │ │ binding │ rollback.txt │ │ │ +│ │ └───────────────┘ └───────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ TPM-SEALED SECRETS (NVRAM) │ │ +│ │ │ │ +│ │ ┌────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ TOTP/HOTP SHARED SECRET (NVRAM index 4d47) │ │ │ +│ │ │ 20 bytes random, sealed to PCRs 0,1,2,3,4,7 │ │ │ +│ │ │ │ │ │ +│ │ │ Same secret ──▶ TOTP ──▶ Phone authenticator app │ │ │ +│ │ │ └──▶ Reverse HOTP ──▶ USB dongle (verifies code) │ │ │ +│ │ └────────────────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌────────────────────────────────────────────────────────────────┐ │ │ +│ │ │ LUKS DUK (Disk Unlock Key) │ │ │ +│ │ │ 128 bytes random, sealed to PCRs 0,1,2,3,4,5,6,7 │ │ │ +│ │ │ - PCR 5: kernel modules, PCR 6: LUKS header │ │ │ +│ │ └────────────────────────────────────────────────────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Cross-Validation Matrix + +This table shows how each component verifies the others: + +| Component | Verifies | Verified By | Prevents | +|----------|----------|------------|----------| +| **SPI Flash ROM** | TPM PCRs | TPM measured boot | Firmware tampering | +| **TPM PCRs** | /boot files | TPM unseal policy | Firmware change | +| **/boot files** | GPG signature | ROM public key | Boot config tampering | +| **GPG signature** | Rollback counter hash | TPM (via PCRs) | Old /boot rollback | +| **Rollback counter** | TPM + /boot binding | Both must match | TPM/boot swap attack | +| **TOTP/HOTP** | PCR match (same secret) | TPM unseal | Firmware tampering | +| **LUKS DUK** | PCR + /boot | TPM unseal + sig | Disk theft | + +--- + +## Trust hierarchy + +The diagram below shows the standard TPM-based boot path. For boards without +TPM hardware, see [HOTP on boards without a TPM](#hotp-on-boards-without-a-tpm-rom-hash-mode). + +```text +SPI flash ROM (hardware root of trust) + │ + │ coreboot SRTM measures boot block + payload into PCR 2; PCRs 0,1,3 unused + ▼ +TPM PCR values (hardware-attested firmware state) + │ + │ Heads unseals TOTP/HOTP secret only when PCRs match expected values + ▼ +TOTP/HOTP code (proves firmware was not tampered since last seal) + │ + │ User verifies TOTP/HOTP matches the value on their phone/token + ▼ +User-approved boot (human-in-the-loop verification) + │ + │ GPG signature on /boot/kexec.sig verified against ROM-fused public key + ▼ +/boot integrity (OS bootloader, kernel, and initrd authenticated) + │ + │ LUKS DUK unsealed from TPM (only when PCRs match + /boot is signed) + ▼ +Decrypted OS disk (disk encryption key delivered without passphrase prompt) +``` + +--- + +## Hardware root of trust + +The trust anchor is the SPI flash ROM containing coreboot. Heads treats this +as the immutable starting point: + +- Coreboot measures firmware stages and the Linux payload into TPM PCR 2 (SRTM) before executing it. +- The Linux payload is embedded in the ROM (no network, no external media required). +- The ROM is physically write-protected on supported boards. See + [wp-notes.md](wp-notes.md) for current status. + +There is no certificate authority, no boot server, and no runtime network +access during the verified boot path. + +--- + +## Measured boot + +The **bootblock** (IBB — Initial Boot Block) is the Static Core Root of Trust +for Measurement (S-CRTM): the first code executed by the CPU, directly from +SPI flash, before anything else has run. All subsequent stages are measured +from it. + +Coreboot's measured boot (`CONFIG_TPM_MEASURED_BOOT=y`) measures the full +firmware chain into **PCR 2** (`CONFIG_PCR_SRTM=2`): + +```text +bootblock → romstage → ramstage → Heads Linux kernel + initrd (payload) +``` + +On boards with `CONFIG_TPM_MEASURED_BOOT=y` + `CONFIG_TPM_INIT_RAMSTAGE=y` +(the majority of maintained boards), ramstage initializes the TPM, reads each +prior stage from CBFS, and extends PCR 2. Older coreboot versions (4.11) used +`CONFIG_TPM_INIT=y` before this config key existed; some boards have no TPM +hardware. See [tpm.md](tpm.md) for the full breakdown. + +PCRs 0, 1, and 3 are unused — the `CONFIG_PCR_*` entries for those registers +are slot assignments for optional coreboot features that are not enabled. They +remain at zero and are anchored as zero in sealing policies. + +Heads extends additional PCRs during userspace boot: + +- **PCR 4** — boot mode tracking; see below +- **PCR 5** — each kernel module loaded via the `insmod` wrapper (binary + parameters) +- **PCR 6** — LUKS header dump (by `qubes-measure-luks`) before disk unlock +- **PCR 7** — each CBFS/UEFI file extracted from ROM (by `cbfs-init`/`uefi-init`) + +Heads extends PCR 4 further depending on execution path: + +- **Normal boot**: `calcfuturepcr 4` pre-computes the expected value and secrets + are sealed against it. +- **Recovery shell**: PCR 4 is extended with `"recovery"`, invalidating + normal-boot unsealing for the rest of the session. + +See [tpm.md](tpm.md) for the full PCR table and sealing policies. +For board-specific RoT configuration and the files that control each PCR, +see [tpm.md — Configuration reference for developers](tpm.md#configuration-reference-for-developers). + +--- + +## Boot attestation: TOTP and HOTP + +TOTP and HOTP share the **same 20-byte secret** sealed to TPM NVRAM (index 4d47). +The secret can only be unsealed when the firmware PCR state matches what was +recorded at seal time. A firmware change causes a PCR mismatch and unseal failure, +which the user observes as a TOTP/HOTP mismatch. + +### TOTP + +A 20-byte random secret is generated at OEM Factory Reset and sealed to +TPM NVRAM. At each boot, `unseal-totp` retrieves it and generates the current +30-second code. The user compares this against their authenticator app. + +### HOTP (Reverse HOTP) + +The same secret is used for HOTP. On supported dongles (Nitrokey Pro/Storage/3, +Librem Key), Heads uses **reverse HOTP** verification: + +1. Heads unseals the shared secret from TPM +2. Heads computes the HOTP code using secret + counter +3. Heads sends the computed code to the dongle +4. Dongle verifies the code matches its own computation and signals via LED: + - Green blinking: code verified + - Red blinking: code mismatch + +This is "reverse" because normally the dongle generates the code - here the +computer generates it and the dongle verifies. The dongle provides a tamper +signal independent of the display (visible before screen is initialized). + +The HOTP counter is stored in `/boot/kexec_hotp_counter` (a plain file, not +in TPM NVRAM). + +### HOTP on boards without a TPM (ROM-hash mode) + +On boards where `CONFIG_NO_TPM=y` (currently the Librem Mini, Librem Mini v2, +and Librem 11), there is no TPM to seal secrets against PCR values. Heads falls +back to a different HOTP secret derivation implemented in `secret_from_rom_hash` +in `initrd/etc/functions`: + +1. At seal time, `flash.sh` reads the full SPI ROM via flashrom/flashprog. +2. The SHA-256 of the ROM image is used directly as the HOTP secret. +3. The secret is programmed onto the USB security dongle. +4. At each boot, the ROM is read again, SHA-256 recomputed, and the HOTP code + sent to the dongle for comparison. A changed ROM produces a different hash, + a different code, and a dongle failure signal. + +The HOTP counter is stored in `/boot/kexec_hotp_counter` as a plain file +(not in TPM NVRAM, which does not exist on these platforms). + +**Known limitations of ROM-hash HOTP (publicly noted):** + +- The secret is **deterministic and derived from public data** — anyone with + physical access to read the ROM can derive the HOTP secret independently, + without owning the dongle. +- **No hardware platform binding**: the secret is not tied to the specific + hardware instance, only to ROM contents. +- ROM reading via flashrom/flashprog at every boot **expands attack surface** + and is slower than a TPM unseal. +- The counter file on `/boot` is not TPM-protected and could in principle be + manipulated to extend the HOTP window (the token accepts codes within a + ±5-count lookahead window). +- There is **no equivalent of TOTP** on these boards; time-based attestation + without a TPM is not implemented. +- LUKS disk encryption key sealing to TPM (DUK) is **not available**; disk + unlock requires the user's passphrase at every boot. + +The ROM-hash HOTP mode provides a weaker attestation model than the TPM-based +path. Its value is in detecting ROM modifications via the dongle's LED, but it +does not provide the same tamper-evident guarantees as TPM PCR sealing. + +### Attestation failure handling + +If TOTP or HOTP unseal fails, `INTEGRITY_GATE_REQUIRED` is set and all TPM +secret sealing operations are blocked until the integrity gate passes. +See [ux-patterns.md](ux-patterns.md#gate-before-sealing). + +--- + +## /boot integrity: GPG signatures + +All files in `/boot` are protected by a SHA-256 hash manifest and a GPG +detached signature (`kexec.sig`). + +### Signing (kexec-sign-config) + +When the user installs or updates the OS, `kexec-sign-config`: + +1. Hashes all non-`kexec*` files in `/boot` into `kexec_hashes.txt` and + generates a directory tree listing in `kexec_tree.txt`. +2. Signs the hash manifest with a GPG key, producing `kexec.sig`. +3. Increments the TPM rollback counter and stores the new counter hash in + `kexec_rollback.txt`. + +The signing key lives on a hardware security dongle (OpenPGP smartcard), +never in the ROM. Signing requires physical possession of the card and +knowledge of the card PIN. To reduce repeated PIN prompts within the same +session, Heads caches the validated User PIN in `/tmp/secret/gpg_pin` (mode +600, tmpfs; cleared at power-off). See +[ux-patterns.md — GPG User PIN caching](ux-patterns.md#gpg-user-pin-caching) +for the caching mechanism and its security properties. + +### Verification (kexec-select-boot) + +At each boot, `verify_global_hashes` in `kexec-select-boot` calls +`verify_checksums` and `check_config` to confirm that every `/boot` file +matches its stored hash and that `kexec.sig` is valid. A hash or signature +failure causes `die` — there is no "boot anyway" path. + +The ROM contains only the **public key**. Verification uses `gpgv` with +the ROM keyring; no private key material is needed at boot. + +--- + +## Rollback counter: TPM/boot binding + +The rollback counter creates a **two-way binding** between the TPM hardware +and the /boot disk, preventing swap attacks. + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ ROLLBACK COUNTER ATTACK PREVENTION │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ OLD TPM │ │ OLD /boot │ │ +│ │ │ │ │ │ +│ │ counter=5 │ │ hash=5 │ │ +│ │ (NVRAM) │ │ (disk) │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ ATTACK SCENARIO: Old TPM + Old /boot │ │ +│ │ │ │ +│ │ Attacker uses old TPM (counter=5) with old /boot │ │ +│ │ (hash=5). This would bypass security updates! │ │ +│ │ │ │ +│ │ → BLOCKED: TPM unseal requires current PCR values │ │ +│ │ → BLOCKED: GPG signature must match current /boot │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ NEW TPM │ │ OLD /boot │ │ +│ │ │ │ │ │ +│ │ counter=X │ │ hash=5 │ │ +│ │ (no secrets │ │ (disk) │ │ +│ │ sealed!) │ │ │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ ATTACK SCENARIO: New TPM + Old /boot │ │ +│ │ │ │ +│ │ Attacker swaps TPM. New TPM has no sealed secrets. │ │ +│ │ Old /boot has old counter hash. │ │ +│ │ │ │ +│ │ → BLOCKED: TOTP/HOTP/DUK unseal fails (no secrets) │ │ +│ │ → BLOCKED: Rollback counter mismatch detected │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### How the binding works + +1. **TPM stores counter**: A monotonic counter is created in TPM NVRAM at OEM Factory Reset +2. **/boot stores hash**: SHA-256 hash of counter value is stored in `/boot/kexec_rollback.txt` +3. **Counter increments on update**: Every `kexec-sign-config` run increments the counter and updates the hash +4. **Verification at boot**: `kexec-select-boot` verifies the counter matches the stored hash + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ ROLLBACK COUNTER LIFECYCLE │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ OEM FACTORY RESET NORMAL BOOT │ +│ ───────────────── ──────────── │ +│ │ +│ 1. Create counter in TPM NVRAM 1. Read counter from TPM │ +│ └─▶ counter_value = 0 │ │ +│ 2. Hash counter → /boot ▼ │ +│ └─▶ kexec_rollback.txt 2. Hash counter │ +│ contains hash of "0" │ │ +│ ▼ │ +│ 3. Compare with /boot hash │ +│ │ │ +│ ▼ │ +│ OS UPDATE 4. Match? → Continue │ +│ ────────── 5. Mismatch? → Die │ +│ │ +│ 1. kexec-sign-config runs TPM SEALED SECRETS │ +│ │ ────────────────── │ +│ ▼ TOTP/HOTP/DUK can only unseal │ +│ 2. Increment TPM counter if: │ +│ └─▶ counter_value = 1 - PCRs match seal policy │ +│ 3. Hash new value → /boot - TPM is the SAME TPM │ +│ └─▶ kexec_rollback.txt - /boot is the SAME /boot │ +│ updates hash of "1" │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### PCR binding in TPM sealing + +TPM-sealed secrets (TOTP, HOTP, LUKS DUK) are bound to specific PCR values, +creating additional hardware binding: + +```text +┌─────────────────────────────────────────────────────────────────────────────┐ +│ TPM SEALING PCR POLICIES │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ TOTP/HOTP Secret Seal Policy: PCRs 0,1,2,3,4,7 │ │ +│ │ │ │ +│ │ PCR 0,1,2,3: Platform configuration (unused but anchored as zero) │ │ +│ │ PCR 4: Boot mode (normal/recovery/usb) │ │ +│ │ PCR 7: CBFS/ROM files (user-injected) │ │ +│ │ │ │ +│ │ NOT included: PCR 5 (kernel modules), PCR 6 (LUKS headers) │ │ +│ │ → Allows disk updates without resealing TOTP/HOTP │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ LUKS DUK Seal Policy: PCRs 0,1,2,3,4,5,6,7 │ │ +│ │ │ │ +│ │ PCR 0,1,2,3: Platform configuration │ │ +│ │ PCR 4: Boot mode │ │ +│ │ PCR 5: Kernel modules (if loaded) │ │ +│ │ PCR 6: LUKS header (measured at seal time) │ │ +│ │ PCR 7: CBFS/ROM files │ │ +│ │ │ │ +│ │ Includes: PCR 5, PCR 6 → More restrictive │ │ +│ │ → Changing kernel modules or LUKS headers requires resealing DUK │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ PCR 16 (Scratch) │ │ +│ │ │ │ +│ │ Used internally for calcfuturepcr (pre-computing future values) │ │ +│ │ Not part of any sealing policy - purely for calculation │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Disk encryption: LUKS DUK + +The LUKS Disk Unlock Key (DUK) is a random binary key that: + +1. Is generated from `/dev/urandom` by `kexec-seal-key` (128 characters — 1024 bits of entropy). +2. Is sealed to TPM NVRAM with PCR policy `0,1,2,3,4,5,6,7`. +3. Is added as a LUKS key slot alongside the user's Disk Recovery Key (DRK). +4. At boot, `kexec-insert-key` unseals it and injects it into a minimal + initrd prepended to the OS initrd. The OS kernel unlocks LUKS without + prompting the user. + +If the TPM refuses to unseal (PCR mismatch, TPM reset), the OS falls back +to prompting for the DRK passphrase. The DRK is always a valid recovery path. + +--- + +## Integrity gate before sealing + +Before any operation that seals new TPM secrets, `gate_reseal_with_integrity_report` +in `gui-init` verifies: + +1. TPM is not in a reset-required state. +2. No prior TOTP/HOTP failure is recorded (`INTEGRITY_GATE_REQUIRED` is unset). +3. `/boot` hash verification passes. +4. `kexec.sig` is valid and signed by a key in the current keyring. +5. If HOTP is enabled: the USB security token is present. +6. User explicitly confirms proceeding. + +If any check fails, the sealing operation is aborted. This prevents new +secrets from being sealed against a potentially compromised `/boot`. + +For the UNKNOWN_KEY scenario and correct error messaging, see +[ux-patterns.md](ux-patterns.md#security-ux--integrity-report-and-unknown-keys). + +--- + +## OEM Factory Reset + +`oem-factory-reset` re-establishes full ownership of the device in five phases: + +1. **TPM reset** — clears the TPM owner hierarchy, removes all NVRAM indices, + and invalidates all sealed secrets. +2. **GPG key initialization** — generates new keys (in-memory RSA or ECC, or + on-smartcard) and configures the OpenPGP card PINs. The card PIN length + is limited to 25 characters due to a firmware constraint on supported tokens + (Librem Key / Nitrokey HOTP). +3. **TPM rollback counter creation** — creates a new monotonic counter and + stores its initial hash in `/boot/kexec_rollback.txt`. +4. **`/boot` signing** — hashes and GPG-signs the initial `/boot` state. +5. **TOTP/HOTP and LUKS DUK sealing** — TOTP/HOTP secrets are sealed + immediately; LUKS DUK sealing is performed by the user on the next boot + via the GUI menu. + +--- + +## Fail-closed design + +All verification failures are fatal by default: + +- GPG signature mismatch → `die` (recovery shell) +- Hash mismatch → `die` (recovery shell) +- TPM counter mismatch → `die` (recovery shell) +- TOTP unseal failure → error menu (no unattended boot) +- LUKS DUK unseal failure → OS prompts for DRK passphrase (no silent failure) + +There is no "continue anyway" path for integrity failures. diff --git a/doc/tpm.md b/doc/tpm.md new file mode 100644 index 000000000..2713ae002 --- /dev/null +++ b/doc/tpm.md @@ -0,0 +1,399 @@ +# Heads TPM Usage + +This document covers how Heads uses the TPM for measured boot, secret sealing, +rollback protection, and PCR extension. + +See also: [architecture.md](architecture.md), [boot-process.md](boot-process.md), [security-model.md](security-model.md). + +--- + +## tpmr — unified TPM abstraction + +`initrd/bin/tpmr` is a shell script wrapper that presents a single interface +over both TPM 1.2 (`tpm` / `trousers`) and TPM 2.0 (`tpm2-tools`). All Heads +scripts call `tpmr` rather than invoking `tpm` or `tpm2` directly. + +### PCR sizes + +| TPM version | Hash algorithm | PCR size | +| --- | --- | --- | +| TPM 1.2 | SHA-1 | 20 bytes | +| TPM 2.0 | SHA-256 | 32 bytes | + +### Subcommand surface + +| Subcommand | Description | +| --- | --- | +| `pcrread` | Read a PCR value | +| `pcrsize` | Print PCR byte size (20 or 32) | +| `calcfuturepcr` | Replay PCR extension to compute a future value | +| `extend` | Extend a PCR with a hash or file | +| `seal` | Seal a file to TPM NVRAM with a PCR policy | +| `unseal` | Unseal from TPM NVRAM | +| `startsession` | Start an authorization session (TPM2 only) | +| `counter_read` | Read a monotonic counter | +| `counter_increment` | Increment a monotonic counter | +| `counter_create` | Create a new monotonic counter | +| `destroy` | Destroy an NVRAM index | +| `reset` | Reset the TPM | +| `kexec_finalize` | Finalize PCR state before kexec (TPM2 only) | +| `shutdown` | Orderly shutdown (TPM2 only) | + +--- + +## PCR assignments + +### Who extends what + +`config/coreboot-*.config` defines slot assignments via `CONFIG_PCR_*` for +optional coreboot measured-boot features. Coreboot supports several modes: + +| coreboot mode | PCRs used | Status in Heads | +| --- | --- | --- | +| SRTM (Static Root of Trust for Measurement) | PCR 2 (`CONFIG_PCR_SRTM=2`) | **Active on all boards with TPM hardware and `CONFIG_TPM_MEASURED_BOOT=y`** | +| Boot mode measurement | PCR 1 (`CONFIG_PCR_BOOT_MODE=1`) | Not enabled | +| Hardware ID measurement | PCR 1 (`CONFIG_PCR_HWID=1`) | Not enabled | +| Runtime data | PCR 3 (`CONFIG_PCR_RUNTIME_DATA=3`) | Not enabled — coreboot's default slot for runtime data, but the feature is not activated in Heads; PCR 3 remains zero | +| Firmware version | PCR 10 (`CONFIG_PCR_FW_VER=10`) | Not enabled | + +### Root of Trust and SRTM chain + +Coreboot's measured boot establishes a **Core Root of Trust for Measurement +(CRTM)**. When the CRTM executes only once per power cycle — as it does on all +Heads boards — this is a **Static CRTM (S-CRTM)**, creating an SRTM chain. + +The **bootblock** (IBB — Initial Boot Block) is the S-CRTM: the first code +executed by the CPU after reset, directly from SPI flash. Its integrity is +guaranteed by hardware write-protection of the flash, not by a prior measurement. +Measured boot is independent of vboot and does not require vboot to be enabled. + +CBFS stages are measured as raw data before decompression; CBFS headers are +excluded from measurements. + +#### Standard path (boards with CONFIG_TPM_MEASURED_BOOT=y + CONFIG_TPM_INIT_RAMSTAGE=y) + +On the majority of Heads boards, the TPM chip is not initialized until ramstage +— the bootblock and romstage run before any TPM recording takes place. Once +ramstage initializes the TPM, coreboot's measured boot (`CONFIG_TPM_MEASURED_BOOT=y`) +reads each prior stage back from CBFS and extends PCR 2 (`CONFIG_PCR_SRTM=2`) +retroactively. The full chain recorded into PCR 2 is: + +```text +bootblock → romstage → ramstage → Heads Linux kernel + initrd (payload) +``` + +The gap between first CPU execution (bootblock) and first TPM recording (ramstage) +is covered by hardware write-protection of the SPI flash — the contents of those +stages cannot change without physical flash access. The bootblock is still the +S-CRTM; the TPM just begins recording later. + +After this chain is recorded, the TPM state reflects the complete firmware +stack. Any modification to any of these stages produces a different PCR 2 +value, causing unseal operations to fail. + +Under the active Heads configuration, only PCR 2 is extended by coreboot. +PCRs 0, 1, and 3 remain at zero and are anchored as zero in sealing policies. + +#### Boards with different or absent coreboot measured boot + +`CONFIG_TPM_MEASURED_BOOT` is the config key used in current coreboot versions. +Older coreboot releases (notably 4.11, used by KGPE-D16 and some Librem server +boards) used `CONFIG_TPM_INIT=y` before this key existed. The absence of +`CONFIG_TPM_MEASURED_BOOT` in an older-coreboot config does not automatically +mean measured boot is absent — it may use the older naming. + +Notable exceptions from the standard SRTM path: + +| Board family | coreboot fork/version | TPM situation | Notes | +| --- | --- | --- | --- | +| KGPE-D16 server/workstation variants | 4.11 (unmaintained) | `CONFIG_TPM_INIT=y` (old key); no `CONFIG_TPM_MEASURED_BOOT` | Pre-dates the current measured boot config naming | +| ThinkPad T520 | 4.22.01 (unmaintained) | `CONFIG_TPM_INIT_RAMSTAGE=y` but `CONFIG_TPM_MEASURED_BOOT` explicitly not set | TPM initialized but SRTM measurements disabled | +| Librem l1um (original) | purism fork (unmaintained) | `CONFIG_TPM_INIT=y` (old key); no `CONFIG_TPM_MEASURED_BOOT` | Purism fork; pre-dates current measured boot naming | +| Librem Mini, Librem Mini v2, Librem 11 | purism fork (maintained) | `CONFIG_NO_TPM=y` | No TPM hardware; falls back to ROM-hash HOTP mode (see below) | + +On boards where coreboot SRTM measurements are absent or uncertain, PCR 2 +remains at zero from coreboot's perspective. Heads still seals secrets to the +TPM (where a TPM exists), but the PCR 2 component of the seal offers no +firmware tamper detection. Boot integrity on these platforms relies on +write-protection of the flash and GPG-signed `/boot`. + +On boards with no TPM hardware, Heads uses ROM-hash HOTP as the sole +attestation mechanism. See +[security-model.md — HOTP on boards without a TPM](security-model.md#hotp-on-boards-without-a-tpm-rom-hash-mode) +for the mechanism and its known limitations. + +#### S-CRTM hardening (external hardware RoT) + +The software S-CRTM (bootblock measuring itself) has a known limitation: the +IBB is self-referential — it asserts its own integrity. To address this, +processor vendors provide external RoT mechanisms that validate the IBB via +hardware before execution: + +- **Intel BootGuard** — validates the bootblock against a signed manifest fused + into the CPU/PCH before any code runs +- **AMD Hardware Validated Boot (HVB)** — equivalent AMD mechanism + +These are hardware features of the platform, not coreboot configuration choices. +Where a board's CPU supports BootGuard or HVB, that hardware layer sits below +the coreboot SRTM chain and provides additional assurance for the S-CRTM +integrity. + +#### Intel TXT path (OptiPlex 7019/9010 TXT only) + +One board — the Dell OptiPlex configured with Intel Trusted Execution Technology +(`CONFIG_INTEL_TXT=y`, `CONFIG_TPM_MEASURED_BOOT_INIT_BOOTBLOCK=y`) — initializes +the TPM in the bootblock itself, closing the gap described above: the IBB measures +itself and then each subsequent stage, so measurements begin at the very first +stage. It also enables a **Dynamic Root of Trust for Measurement (DRTM)** path +via the Intel SINIT ACM, which allows a DRTM chain to be re-established within +a single power cycle with hardware-rooted trust. The PCR 2 SRTM chain is +unchanged; the TXT mechanism adds the DRTM capability on top of it. + +| PCR | Extended by | Content | +| --- | --- | --- | +| 0 | unused | Zero; anchored in sealing policies | +| 1 | unused | Zero; anchored in sealing policies | +| 2 | coreboot SRTM | Boot block, ROM stage, RAM stage, Heads Linux kernel + initrd | +| 3 | unused | Zero; anchored in sealing policies | +| 4 | Heads (`usb-init`, `kexec-insert-key`, `functions`) | Boot mode tracking: `"usb"` during USB init, `"generic"` after DUK unsealed, `"recovery"` when recovery shell entered | +| 5 | Heads `insmod` wrapper | Each loaded kernel module: parameters + binary content (default `MODULE_PCR=5`) | +| 6 | Heads `qubes-measure-luks` | LUKS header dump for each encrypted drive | +| 7 | Heads `cbfs-init`, `uefi-init` | Each CBFS/UEFI file: filename then content (default `CONFIG_PCR=7`) — covers `config.user`, GPG keyring, user CBFS files | +| 16 | `tpmr calcfuturepcr` (scratch use only) | Resettable debug PCR used as scratch pad during pre-computation of future PCR values; not part of any sealing policy | + +PCRs 0-3 are read at seal time and included in sealing policies. The zero +state of PCRs 0, 1, and 3 is intentional — any unexpected extension of those +PCRs (e.g. enabling an optional coreboot feature) would break the seal. + +### Sealing policies + +#### LUKS Disk Unlock Key (DUK) — kexec-seal-key + +The DUK is a 128-character random key (128 bytes from `/dev/urandom`, providing +1024 bits of entropy). It is added to a dedicated LUKS key slot and sealed to +TPM NVRAM with the policy below. + +| PCR | How obtained | Reason | +| --- | --- | --- | +| 0 | `pcrread` (current value) | Platform state at seal time | +| 1 | `pcrread` (current value) | Platform state at seal time | +| 2 | `pcrread` (current value) | coreboot SRTM measurement | +| 3 | `pcrread` (current value) | Platform state at seal time | +| 4 | `calcfuturepcr` | Pre-computed normal-boot path (before any USB init or recovery) | +| 5 | `pcrread` or `calcfuturepcr 5` | Actual if extra modules loaded; zeroed future value if no extra modules | +| 6 | `calcfuturepcr 6 /tmp/luksDump.txt` | Pre-computed LUKS header measurement | +| 7 | `pcrread` (current value) | User CBFS files | + +PCR 5 is conditional: if the board loads extra kernel modules (USB HID, +libata, HOTP token), the actual post-load PCR 5 value is used. If no extra +modules are loaded, `calcfuturepcr 5` computes the zeroed (never-extended) +future value. This means the seal is valid only for the expected module set. + +PCR 6 is pre-computed: `calcfuturepcr 6 /tmp/luksDump.txt` replays the +LUKS header extension to compute the expected post-measurement value. If +the LUKS header changes (key slot added/removed), the DUK unseal fails. + +#### TOTP/HOTP secret — seal-totp + +| PCR | Included | Reason | +| --- | --- | --- | +| 0 | Yes | Platform state | +| 1 | Yes | Platform state | +| 2 | Yes | coreboot SRTM measurement | +| 3 | Yes | Platform state | +| 4 | Yes | Pre-computed normal-boot value | +| 5 | **No** | Kernel modules are not firmware integrity attestation | +| 6 | **No** | LUKS header consistency is not firmware integrity attestation | +| 7 | Yes | User CBFS files | + +The narrower policy means a LUKS header change or different kernel module +set does not prevent TOTP from unsealing. TOTP/HOTP attests firmware and +ROM configuration integrity, not disk state. + +--- + +## PCR extension + +`tpmr extend -ix -ic ` extends a PCR with the hash of a +string. `-if ` extends with the hash of a file. + +`calcfuturepcr` replays the expected extend sequence to compute what a PCR +will contain after the normal boot path, without actually extending it. +This is used to seal secrets against a known-future PCR state (e.g. PCR 4 +after normal init, before any recovery shell entry). + +### Recovery PCR extension + +When a recovery shell is entered, `initrd/etc/functions` extends PCR 4 with +the string `"recovery"`. This permanently invalidates TOTP and LUKS DUK +unsealing for the rest of the boot session — the TPM will refuse to unseal +secrets that were sealed against the normal-boot PCR 4 value. + +### TPM event log + +Coreboot records each PCR extension into a TPM event log. Three log formats +are supported: coreboot-specific, TPM 1.2 spec, and TPM 2.0 spec. The log can +be inspected from an OS or recovery shell with: + +```text +cbmem -L +``` + +This is the authoritative record of what was measured into each PCR during +firmware boot. Useful for diagnosing unexpected PCR values or verifying that +a new board's SRTM chain matches expectations. + +--- + +## Rollback counter + +Heads uses a TPM monotonic counter stored in TPM NVRAM to detect rollback +attacks. The counter is incremented every time `/boot` is re-signed (i.e. +every time `kexec-sign-config` runs after an OS update). + +### What it protects against + +The rollback counter prevents **TPM swap attacks** and **/boot disk swap attacks**: + +1. **TPM swap**: An attacker swaps the TPM with a different one. The new TPM + doesn't have the sealed secrets (TOTP/HOTP/DUK) that are bound to the + original TPM's NVRAM. Even if the attacker has the original TPM, its PCR + values would be different from the current firmware state, so unseal would + fail. + +2. **Disk swap**: An attacker swaps the /boot disk with an older one. The old + disk has an older counter hash that doesn't match the current TPM counter + value. + +3. **Combined attack**: An attacker tries to use an old TPM with an old /boot + to bypass security updates or revert to a known-vulnerable state. + +### How it works + +The counter is stored **in the TPM** (NVRAM index `0x3135106223`), ensuring +hardware binding. A SHA-256 hash of the counter value is stored on **/boot** +(`/boot/kexec_rollback.txt`). This creates a two-way binding: + +- Cannot swap TPM without breaking /boot consistency +- Cannot swap /boot without breaking TPM consistency + +At boot, `verify_rollback_counter` in `kexec-select-boot` verifies the +counter hash matches. Before presenting TOTP/HOTP prompts, `preflight_rollback_counter_before_reseal` +validates the counter is readable from TPM, ensuring secrets can actually be +unsealed. + +The counter is created during OEM Factory Reset by `check_tpm_counter` in +`initrd/etc/functions`. + +### Counter state file + +`read_tpm_counter` in `initrd/etc/functions` reads the counter from the TPM +and writes the result to `/tmp/counter-`. The format is +`: `. + +`/boot/kexec_rollback.txt` stores the SHA-256 hash of that counter file. +At boot, `kexec-select-boot` reads the counter, hashes the file, and checks +it against the stored hash. Any discrepancy aborts the boot. + +### Rollback preflight: boot-time validation + +Before presenting TOTP/HOTP recovery prompts, `gui-init` calls +`preflight_rollback_counter_before_reseal` to confirm the rollback counter +is consistent. This catches TPM replacements, `/boot` disk swaps, and counter +corruption before any secrets are resealed. + +Failure conditions and their diagnostic messages: + +| Condition | Message shown to user | +| --- | --- | +| `/boot/kexec_rollback.txt` missing on initialized system | "Boot integrity counter file missing. This means /boot was restored or swapped." | +| Counter index unreadable from TPM | "TPM integrity counter cannot be read. Possible cause: TPM was swapped or reset. This could indicate a TPM swap attack." | +| TPM2: counter has `ownerwrite` but not `authwrite` | "TPM counter has invalid security policy." | +| TPM2: counter has neither `authwrite` nor `ownerwrite` | "TPM counter is not writable." | +| TPM2: counter attributes empty or unreadable | "TPM counter policy is corrupted." | + +The exact diagnostic message from `fail_preflight` is shown directly in the +error dialog — **not** a vague paraphrase. This tells the user and any support +context exactly which condition was detected. The action guidance ("Reset TPM +from GUI...") is stripped from the dialog since the menu already offers those +options. + +The user is offered four actions: show the integrity report, OEM Factory Reset, +Reset the TPM, or continue to the main menu. The dialog loops until the +counter passes preflight or the user chooses to continue. + +### Pipeline safety + +`tpmr counter_read` must be called with a direct redirect, not piped through +`tee`. Piping through `tee` hides `tpmr` failures because `||` checks the +exit status of `tee` (always 0), not `tpmr`. See +[ux-patterns.md](ux-patterns.md#tpm-counter-patterns) for the correct pattern. + +--- + +## TPM secret sealing internals (TPM2) + +TPM2 sealing uses NVRAM persistent objects with a combined PCR + optional +password policy: + +1. A policy session is started (`tpm2 startauthsession --policy-session`). +2. PCR values are bound to the session (`tpm2 policypcr`). +3. If a password is set, `tpm2 policyauthvalue` adds it to the policy. +4. The secret is stored in a persistent NVRAM handle. +5. At unseal time, the same policy session is reconstructed and + `tpm2 unseal` retrieves the plaintext. + +The primary handle file must exist before unsealing. If it is missing (after +a TPM reset), `tpm2_unseal` exits with a clear warning rather than producing +a confusing low-level error. + +--- + +## Configuration reference for developers + +The following table maps each configurable aspect of the RoT and PCR policy to +the file that controls it. Use this when adding a board, changing a sealing +policy, or investigating why a seal/unseal operation fails. + +| What you want to understand or change | Where to look | What to look for | +| --- | --- | --- | +| Which coreboot PCRs are active on a board | `config/coreboot-.config` | `CONFIG_PCR_SRTM`, `CONFIG_TPM_INIT_RAMSTAGE`, `CONFIG_TPM_MEASURED_BOOT_INIT_BOOTBLOCK`, `CONFIG_INTEL_TXT` | +| Which coreboot version / fork a board uses | `modules/coreboot` + `boards//` | `CONFIG_COREBOOT_VERSION` in board config selects the coreboot source defined in `modules/coreboot` | +| LUKS DUK sealing policy (which PCRs) | `initrd/bin/kexec-seal-key` | `tpmr seal` call and surrounding `pcrread` / `calcfuturepcr` calls; DEBUG comments explain each PCR | +| TOTP/HOTP sealing policy (which PCRs) | `initrd/bin/seal-totp` | `tpmr seal` call; DEBUG messages explain why PCR 5 and PCR 6 are excluded | +| PCR 4 (boot mode) tracking | `initrd/bin/usb-init`, `initrd/bin/kexec-insert-key`, `initrd/etc/functions` | `tpmr extend` calls with `"usb"`, `"generic"`, `"recovery"` | +| PCR 5 (kernel modules) | `initrd/sbin/insmod` | `MODULE_PCR` variable; default `MODULE_PCR=5`; each `insmod` extends PCR 5 | +| PCR 6 (LUKS header) | `initrd/bin/qubes-measure-luks` | `tpmr extend` call against `/tmp/luksDump.txt` | +| PCR 7 (CBFS / ROM files) | `initrd/bin/cbfs-init`, `initrd/bin/uefi-init` | `CONFIG_PCR` variable; default `CONFIG_PCR=7`; each extracted file extends PCR 7 | +| Rollback counter logic | `initrd/etc/functions` | `check_tpm_counter`, `read_tpm_counter`, `counter_increment` | + +### Adding a new board + +To verify that a new board's coreboot config matches the expected RoT: + +1. Check that `CONFIG_TPM_MEASURED_BOOT=y` and `CONFIG_PCR_SRTM=2` are set. + For boards using coreboot 4.11 or older forks, the equivalent older key is + `CONFIG_TPM_INIT=y`; confirm whether that version's measured boot is active. +2. Confirm `CONFIG_TPM_INIT_RAMSTAGE=y` (standard) or document why it differs. + If the board has no TPM hardware, verify `CONFIG_NO_TPM=y` is intentional and + note that TPM-based attestation (TOTP, LUKS DUK) will not be available. +3. Check that `CONFIG_PCR_BOOT_MODE`, `CONFIG_PCR_HWID`, `CONFIG_PCR_RUNTIME_DATA` + are set to their slot numbers but **not** enabled (no corresponding `=y` feature + flag). These are slot reservations; enabling them would extend PCRs 1 and 3, + breaking all existing seals on that board. +4. If the board uses Intel TXT, verify `CONFIG_INTEL_TXT=y` and + `CONFIG_TPM_MEASURED_BOOT_INIT_BOOTBLOCK=y` are intentional and document the + DRTM capability in the board's README. + +--- + +## TPM1 vs TPM2 differences + +| Feature | TPM 1.2 | TPM 2.0 | +| --- | --- | --- | +| PCR hash | SHA-1 (20 bytes) | SHA-256 (32 bytes) | +| Sealing | `tpm sealfile2` | `tpm2 nvdefine` + policy session | +| Counter | `tpm nv*` | `tpm2 nvincrement` | +| Auth sessions | Not used | Required for policy-based unseal | +| `kexec_finalize` | No-op | Extends PCRs, then `tpm2 shutdown` | +| `startsession` | No-op | Creates encryption session | diff --git a/doc/ux-patterns.md b/doc/ux-patterns.md new file mode 100644 index 000000000..731a0dfb8 --- /dev/null +++ b/doc/ux-patterns.md @@ -0,0 +1,371 @@ +# Heads UX Patterns + +This document describes the coding conventions for interactive UX in Heads initrd scripts. +See also: [logging.md](logging.md) for console/log output levels, and the +[Heads architecture reference](https://deepwiki.com/linuxboot/heads) for validated system context. + +--- + +## Whiptail dialogs + +All interactive dialogs use `whiptail` through one of three wrapper functions defined in +`initrd/etc/gui_functions`: + +| Wrapper | Background color | When to use | +|---|---|---| +| `whiptail_error` | Red | Errors, security warnings, irreversible states | +| `whiptail_warning` | Yellow/amber | Cautionary prompts, confirmations before risky actions | +| `whiptail_type $BG_COLOR` | Caller-supplied color | Normal menus, informational dialogs | + +**Never call `whiptail` directly** from initrd scripts — always go through a wrapper. +The wrappers handle color selection for both `fbwhiptail` (framebuffer) and `newt` (text) backends. + +### Message folding — centralized in `_whiptail_preprocess_args` + +`_whiptail_preprocess_args` in `initrd/etc/gui_functions` is the **single place** responsible +for expanding `\n` escape sequences and word-wrapping message text at 76 columns: + +```bash +_WHIPTAIL_ARGS+=("$(printf '%b' "$_arg" | fold -s -w 76)") +``` + +This runs automatically on the message argument (the string immediately after `--msgbox`, +`--yesno`, `--menu`, etc.) before it is passed to `whiptail`. + +**Callers must not pre-fold.** Pass raw strings with `\n` escape sequences directly to the +wrapper functions — `_whiptail_preprocess_args` handles everything: + +```bash +# CORRECT — raw string, \n escapes, no fold +whiptail_error --title 'ERROR' \ + --msgbox "Something failed.\n\nDetails here.\n\nChoose an action:" 0 80 + +# WRONG — double-folding, redundant pipe +local msg +msg="$(printf '%b' "Something failed.\n\nDetails here." | fold -s -w 76)" +whiptail_error --title 'ERROR' --msgbox "$msg" 0 80 +``` + +The 76-column wrap width leaves 2 columns of padding inside a standard 80-column dialog, +preventing text from being cut off at the dialog border. + +### Dialog structure + +Whiptail messages typically follow this layout: + +``` + + + + + +``` + +Keep the first paragraph short — it appears at the top of the box where vertical space is limited +and it must not wrap onto a third line at 76 columns. +The guidance paragraph is always the last line so it sits adjacent to the menu items or OK button. + +### Window sizing + +**fbwhiptail (framebuffer backend)** ignores height and width arguments +entirely — it always auto-sizes from content using its own internal layout +constants. Any values passed are silently discarded. + +**newt (text backend)** uses the height and width arguments: + +- `0` height triggers `guessSize()`, which computes the minimum height from + content. Use `0` for height in all dialogs. +- `0` width also triggers `guessSize()` for width — the dialog expands to fit + the longest content line. **Use `0` width only for dialogs that contain + dynamic strings of unpredictable length** (e.g. ROM filenames, file paths). +- For dialogs with static text, use a fixed width (typically `80`). This + produces a stable, readable layout in newt and is a no-op in fbwhiptail. + +In practice: + +```bash +# Dynamic content (ROM filename, file path) — width must fit at runtime: +whiptail_warning --title 'Flash ROM?' \ + --yesno "This will replace your current ROM with:\n\n$PKG_FILE_DISPLAY\n\nDo you want to proceed?" 0 0 + +# Static text — fixed width; fbwhiptail ignores it, newt uses it: +whiptail_error --title 'ERROR' \ + --msgbox "This device does not have a TPM.\n\nPress OK to return to the Main Menu" 0 80 +``` + +--- + +## `INPUT` — inline terminal prompts + +`INPUT` (defined in `initrd/etc/functions`) is the standard way to prompt the user for typed +input in non-whiptail contexts (e.g. recovery shell, passphrase entry, confirmation tokens). + +```bash +INPUT "prompt text" [read-flags] [VARNAME] + +# Examples: +INPUT "Enter new passphrase:" -r -s new_pass +INPUT "Enter TPM owner passphrase:" -r owner_pw +INPUT "Press Enter to continue" -r _ignored +``` + +**Cursor placement**: The prompt is printed with a trailing space and no newline (`printf '...' "$prompt"`). +The cursor lands on the same line as the prompt — the user types immediately after it. +Do not add `\n` or `echo` between the prompt and the `read`. + +**Device routing**: When `HEADS_TTY` is set (gui-init context after `cttyhack`), both prompt +output and `read` use that device — bypassing any stdout/stderr redirections the caller may have. +When `HEADS_TTY` is unset, the prompt goes to stderr and `read` uses stdin (serial recovery shell +convention). + +**Do not use INPUT for yes/no choices** — use `whiptail_warning --yesno` or +`whiptail_error --yesno` for those so the user has a clear graphical dialog. + +### Surviving a screen repaint — acknowledgment pattern + +`WARN`/`NOTE`/`STATUS` write to the terminal but do not block. When control +returns to a caller that repaints the screen (whiptail menu, gui-init loop), +those messages are immediately overwritten and the user never sees them. + +For security-critical notices that **must** be read, follow the logging call +with an `INPUT` acknowledgment: + +```bash +WARN "Default Admin PIN detected - your dongle is using factory defaults." +INPUT "Change secrets via Options > OEM Factory Reset / Re-Ownership. Press Enter to acknowledge." ignored +``` + +The `INPUT` call blocks until Enter is pressed, keeping the warning on screen +regardless of what the caller does next. + +--- + +## Security UX — integrity report and unknown keys + +### UNKNOWN_KEY / untrusted-key scenario + +When `/boot/kexec.sig` is signed by a key that is present in the GPG keyring but is **not** the +key that matches the currently inserted OpenPGP smartcard, the system cannot verify content integrity. + +The correct UX is: + +- **State clearly that /boot cannot be trusted** — do not frame this as merely "signed by a different key." +- **Do not offer re-signing as the primary action** — knowing the fingerprint, owner, and date of + the previous key is NOT sufficient to trust content. Re-signing would legitimize unknown changes. +- **Guide toward backup restoration or OEM Factory Reset** as the safe recovery path. +- **Re-signing is only valid** if the user can independently verify that the content of /boot is + exactly what they expected through an out-of-band means (e.g. comparing against a known-good + clean OS installation, not against the signature itself). + +### Show the actual diagnostic — do not paraphrase + +When an internal check fails and a reason is already available as a string, +show it directly to the user. Do **not** grep the message and replace it with +a vague summary — that discards the specific detail the user needs to act. + +```bash +# CORRECT — user sees exactly which counter and why +preflight_reason="${preflight_error_msg%%. Reset TPM from GUI*}" + +# WRONG — throws away counter ID and specific condition +if echo "$preflight_error_msg" | grep -qi "cannot be read"; then + preflight_reason="Stored TPM rollback metadata cannot be read." +fi +``` + +Strip action guidance from the displayed reason only when the menu already +offers those actions — this avoids duplication, not information loss. + +### Gate before sealing new secrets + +`gate_reseal_with_integrity_report` (`initrd/bin/gui-init`) must be called before any operation +that seals new TPM secrets. It verifies: +1. `/boot` integrity (file hashes) +2. Detached signature (`/boot/kexec.sig`) can be verified against the current keyring + +If either check fails, the user is shown an error and the sealing operation is aborted. +This prevents new TOTP/HOTP/DUK secrets from being sealed against a potentially compromised `/boot`. + +--- + +## GPG User PIN caching + +Heads signs `/boot` content using a GPG key. For OpenPGP smartcard keys, the +card's "force signature PIN" property (enabled by default on supported tokens) +requires the User PIN to be presented to the card for every signing operation. +Without caching, the user would be prompted on every `gpg --detach-sign` call +within the same session. + +To reduce PIN prompts (issue [#1955](https://github.com/linuxboot/heads/issues/1955)), +Heads caches the validated PIN for the session in `/tmp/secret/gpg_pin` +(mode 600, on tmpfs; cleared at power-off). + +### Architecture: loopback mode + +All GPG signing in Heads uses `--pinentry-mode=loopback` with +`--passphrase-file /tmp/secret/gpg_pin`. This means gpg-agent never calls +`pinentry` for signing operations — the PIN is supplied directly from the +cache file through the loopback channel. `initrd/.gnupg/gpg-agent.conf` +sets `allow-loopback-pinentry` to permit this. + +`confirm_gpg_card` in `initrd/etc/functions` is a thin wrapper around +`cache_gpg_signing_pin`, which implements both key paths below. + +### Priming the cache: test-sign in cache_gpg_signing_pin + +Both key paths prime the PIN cache **inside `cache_gpg_signing_pin`** (called +via `confirm_gpg_card`) via a validated test-sign before returning. The cache +is always populated before `kexec-sign-config` performs the actual signing. +On second and later calls in the same session, `[ -s /tmp/secret/gpg_pin ]` +triggers an early return with no prompting. + +**Smartcard (User PIN) path:** +`cache_gpg_signing_pin` reads the card status to display PIN retry counters, +then collects the User PIN via `INPUT` (Heads-controlled prompt). It performs +a test detach-sign using `--pinentry-mode=loopback --passphrase-file +<(printf '%s' "$sc_user_pin")` and verifies the signature. On success the PIN +is written to `/tmp/secret/gpg_pin` and `STATUS_OK "GPG User PIN cached for +this session"` is emitted. On bad PIN: clear input, WARN with updated retry +counter, retry (up to 3 attempts). The test-sign nonce is shredded on +completion. + +**Backup key (Admin PIN) path:** +`cache_gpg_signing_pin` collects the Admin PIN via `INPUT`, imports the +private subkeys with `--pinentry-mode=loopback --passphrase-file`, does a +test-sign with loopback, verifies the signature, then writes the validated +passphrase directly to `/tmp/secret/gpg_pin` and emits +`STATUS_OK "GPG Admin PIN cached for this session"`. + +### Bad PIN handling + +On bad-PIN signing failure inside `kexec-sign-config` or `gpg_auth`, callers +delete `/tmp/secret/gpg_pin` before retrying. The next call to `confirm_gpg_card` +finds an empty cache, runs the full test-sign flow, and re-prompts the user. + +### STATUS_OK on cache save + +`cache_gpg_signing_pin` emits `STATUS_OK` when the PIN is successfully cached: + +- Smartcard path: `STATUS_OK "GPG User PIN cached for this session"` +- Backup key path: `STATUS_OK "GPG Admin PIN cached for this session"` + +--- + +## Once-per-session display + +Some informational displays are useful on first occurrence but become noise if +repeated across multiple call sites in the same session. Guard these with a +session flag file under `/tmp`: + +```bash +some_display_function() { + [ -f /tmp/some_shown ] && return + # ... produce the display ... + touch /tmp/some_shown +} +``` + +`/tmp` is on tmpfs and is cleared at reboot, so the guard is automatically +lifted on the next boot. No cleanup code is needed. + +This pattern is used by `hotpkey_fw_display` in `initrd/etc/functions` to show +the USB security dongle firmware version at most once per session, regardless +of how many times the function is called from different code paths. + +### Color-coded version checks + +When displaying a version that has a known minimum, use the logging level that +matches the severity — do not embed raw ANSI codes in STATUS or produce two +separate messages for the same device: + +| Device | Condition | Function | Visual result | +| --- | --- | --- | --- | +| NK3 / Nitrokey Pro / Pro 2 | `fw_ver >= min_ver` | `STATUS_OK` | Bold green — firmware is current | +| NK3 / Nitrokey Pro / Pro 2 | `fw_ver < min_ver`, nitropy upgrade available | `NOTE` with inline `\033[1;33m` on the version | Yellow version — upgrade recommended | +| Nitrokey Pro / Pro 2 | `fw_ver < HOTPKEY_EXTERNAL_REPROGRAM_BELOW` | `NOTE` with inline `\033[1;31m` on the version | Red version — external programmer required | +| Nitrokey Storage | (any version) | `STATUS_OK` | Version shown; no min-ver comparison (Storage min ver TBD) | +| Librem Key | (any version) | `NOTE` | Version shown with advisory to contact Purism — never self-upgradeable | + +One message per device, color determined by the worst applicable condition. + +#### Nitrokey Pro / Pro 2 external-reprogram threshold + +`HOTPKEY_EXTERNAL_REPROGRAM_BELOW="v0.11"` in `initrd/etc/dongle-versions`. Firmware +v0.10 and earlier have no DFU bootloader and cannot be upgraded via nitropy; +the bootloader was introduced in v0.11 +([Nitrokey Pro firmware issue #95](https://github.com/Nitrokey/nitrokey-pro-firmware/issues/95)). +The physical hardware is unchanged — this is a firmware-only gap. Devices at +v0.10 or older continue to work normally; the red indicator informs the user +that an external programmer (e.g. SWD/JTAG) is required to flash firmware +up to the minimum recommended version. + +This threshold applies to Nitrokey Pro / Pro 2 only. Librem Key is never +self-upgradeable regardless of firmware version (always shown as NOTE +directing users to contact Purism support). Nitrokey Storage has a separate +firmware codebase and is not subject to this threshold. + +#### Parsing hotp_verification output + +`hotp_verification info` tab-indents all output lines (`\tFirmware: v0.15`). +Use `grep "Firmware:"` without `^` — the `^` anchor would never match a +tab-prefixed line. Also normalize `fw_ver` to add a `v` prefix if absent so +`sort -V` comparisons against the `v`-prefixed threshold values in +`dongle-versions` are consistent. + +--- + +## TPM counter patterns + +### Reading counters + +`read_tpm_counter` in `initrd/etc/functions` reads a TPM NV counter by index and writes the +output to `/tmp/counter-`. The format is `: `. + +**Pipeline exit status**: Never pipe `tpmr counter_read` through `tee` with `|| die` — the +`||` checks the exit status of `tee` (always 0), not `tpmr`. Use a direct redirect: + +```bash +# CORRECT — exit status of tpmr is captured +tpmr counter_read -ix "$counter_id" >/tmp/counter-"$counter_id" || die "..." + +# WRONG — || die checks tee's exit (always 0), tpmr failure is silent +tpmr counter_read -ix "$counter_id" | tee /tmp/counter-"$counter_id" >/dev/null || die "..." +``` + +### Counter reads in tpmr + +`tpm2_counter_read` and `tpm2_counter_inc` must propagate `tpm2 nvread` failure. Use a local +variable and explicit `|| return 1`: + +```bash +# CORRECT +local hex_val +hex_val="$(tpm2 nvread 0x"$index" | xxd -pc8)" || return 1 +echo "$index: $hex_val" + +# WRONG — echo always exits 0; partial/empty hex is silently written +echo "$index: $(tpm2 nvread 0x$index | xxd -pc8)" +``` + +--- + +## `HEADS_TTY` — terminal device routing + +`HEADS_TTY` is exported by `gui-init` and `gui-init-basic` after `cttyhack` sets up the +controlling terminal. It holds the path to the actual interactive terminal (e.g. `/dev/tty1` +or `/dev/ttyS0`). + +Scripts that output prompts or read interactive input should use `HEADS_TTY` when set: + +```bash +if [ -n "$HEADS_TTY" ]; then + printf '...' >"$HEADS_TTY" + read "$@" <"$HEADS_TTY" +else + printf '...' >&2 + read "$@" +fi +``` + +This ensures prompt/read always use the correct device regardless of how the caller has +redirected stdout/stderr (e.g. `2>/tmp/whiptail`). diff --git a/doc/variation-to-defconfig.md b/doc/variation-to-defconfig.md new file mode 100644 index 000000000..f6e10b8ed --- /dev/null +++ b/doc/variation-to-defconfig.md @@ -0,0 +1,72 @@ +# Variation to defconfig (cleaned) + +This file lists configuration items found to be inconsistent and/or removed when generating defconfig with `make BOARD=XYZ coreboot.save_in_defconfig_format_in_place` helper for different boards. + + +## Questionable configs + +These options are inconsistent across boards and should be reviewed. + +### Global + +```text +CONFIG_USE_OPTION_TABLE=y +CONFIG_STATIC_OPTION_TABLE=y +# CONFIG_USE_PC_CMOS_ALTCENTURY is not set +# CONFIG_DRIVERS_MTK_WIFI is not set +# CONFIG_DRIVERS_INTEL_WIFI is not set +# CONFIG_RAMINIT_ENABLE_ECC is not set +# CONFIG_TIMESTAMPS_ON_CONSOLE is not set +CONFIG_PCI_ALLOW_BUS_MASTER=y +``` + +### Specifics + +#### T480 + +```text +CONFIG_USE_LEGACY_8254_TIMER=y +``` + +## Removed undesirables + +The following lines were removed from specific board defconfig variations. Filenames (when present) are listed above their removed fragments. + +```text +config/coreboot-optiplex-7019_9010-maximized.config +CONFIG_TIMESTAMPS_ON_CONSOLE=y +config/coreboot-optiplex-7019_9010_TXT-maximized.config +IDEM +config/coreboot-qemu-tpm1-prod.config +# CONFIG_INCLUDE_CONFIG_FILE is not set +# CONFIG_CONSOLE_SERIAL is not set +# CONFIG_POST_DEVICE is not set +# CONFIG_POST_IO is not set +CONFIG_PCIEXP_ASPM=y +CONFIG_PCIEXP_HOTPLUG_BUSES=32 +CONFIG_PCIEXP_COMMON_CLOCK=y +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-qemu-tpm1.config +IDEM +config/coreboot-qemu-tpm2-prod.config +IDEM +config/coreboot-qemu-tpm2.config +IDEM +config/coreboot-t420-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-t430-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-t480-maximized.config +CONFIG_USE_LEGACY_8254_TIMER=y +CONFIG_PCIEXP_HOTPLUG=y +config/coreboot-w530-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-x220-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-x230-maximized-fhd_edp.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +config/coreboot-x230-maximized.config +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +# CONFIG_PCI_ALLOW_BUS_MASTER is not set +CONFIG_PCIEXP_HOTPLUG_IO=0x2000 +``` diff --git a/doc/wp-notes.md b/doc/wp-notes.md new file mode 100644 index 000000000..802b2aeeb --- /dev/null +++ b/doc/wp-notes.md @@ -0,0 +1,22 @@ +Flashrom was passed to flashprog under https://github.com/linuxboot/heads/pull/1769 + +Those are notes for @i-c-o-n and others wanting to move WP forward but track issues and users + +The problem with WP is that it is desired but even if partial write protection regions is present, WP is widely unused. + +Some random notes since support is incomplete (depends on chips, really) +-QDPI is problematic for WP (same IO2 PIN) + - Might be turned on by chipset for ME read https://matrix.to/#/!pAlHOfxQNPXOgFGTmo:matrix.org/$NCNidoPsw1ze6zv3m2jlPuGuNrdlDQmDcU81If-q55A?via=matrix.org&via=nitro.chat&via=tchncs.de +- WP wanted, WP done, WP unused + - WP wanted https://github.com/flashrom/flashrom/issues/185 https://github.com/linuxboot/heads/issues/985 + - WP done: https://github.com/linuxboot/heads/issues/1741 https://github.com/linuxboot/heads/issues/1546 + - Documented https://docs.dasharo.com/variants/asus_kgpe_d16/spi-wp/ + - WP still unused + +Alternative, as suggested by @i-c-o-n is Chipset Platform Locking (PR0) which is enforced at platform's chipset level for a boot +- This is implemented and enforced on <= Haswell from this PR merged : https://github.com/linuxboot/heads/pull/1373 +- All Intel platforms have PR0 platform locking implemented prior to kexec call with this not yet upstreamed patch applied in all forks https://review.coreboot.org/c/coreboot/+/85278 +- Discussion point under flashrom-> flashprog PR under https://github.com/linuxboot/heads/pull/1769/files/f8eb0a27c3dcb17a8c6fcb85dd7f03e8513798ae#r1752395865 tagging @i-c-o-n + + +Not sure what is the way forward here, but lets keep this file in tree to track improvements over time. From 3ca84e5ce8002a55019a7069dda794fc559ad1a1 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Wed, 1 Apr 2026 10:23:43 -0400 Subject: [PATCH 09/12] oem-factory-reset: improve dongle-aware UX, fix GPG operations, label NK3 Secrets app PIN - Auto-adjust RSA key size based on dongle type; show firmware version before reset; add firmware-aware RSA keygen timing guidance - Fix GPG signing failure by clearing scdaemon CCID lock before signing - Fix keytocard 'Invalid command': remove spurious echo arguments from RSA subkey generation and keytocard operations (scdaemon caches card admin PIN after first keytocard; stale ADMIN_PIN_DEF was landing at keyedit.prompt causing 'No user ID with index 12345678') - Fix set_card_identity sending ADMIN_PIN_DEF to cardedit.prompt after name/login commands (scdaemon caches admin PIN; no re-prompt needed) - Fix ECC P-256 encryption subkey generation: remove invalid 'echo Q' (option 12 skips capabilities menu, goes straight to curve selection) - Use DONGLE_BRAND variable in GPG User PIN prompt for consistent branding - Label ADMIN_PIN as 'NK3 Secrets app PIN / GPG Admin PIN' when Nitrokey 3 is detected, in all user-facing prompts and status messages Signed-off-by: Thierry Laurion --- doc/prerequisites.md | 2 +- doc/recovery-shell.md | 6 + initrd/bin/gui-init.sh | 2 +- initrd/bin/oem-factory-reset.sh | 280 ++++++++++++++++++++++++++------ initrd/bin/tpm-reset.sh | 2 +- initrd/etc/functions.sh | 26 +-- 6 files changed, 254 insertions(+), 64 deletions(-) diff --git a/doc/prerequisites.md b/doc/prerequisites.md index c6fa55815..1b9cd1893 100644 --- a/doc/prerequisites.md +++ b/doc/prerequisites.md @@ -12,7 +12,7 @@ version. Without HOTP, Heads falls back to TPMTOTP (smartphone-based). |--------|---------|------|-------| | Nitrokey Pro 2 | Yes | Yes | Full support | | Nitrokey Storage 2 | Yes | Yes | Full support | -| Nitrokey 3 | Yes | Yes | Full support; p256 ECC available | +| Nitrokey 3 | Yes | Yes | Full support; NIST P-256 ECC available | | Purism Librem Key | Yes | Yes | Full support; rebranded NK Pro | | YubiKey 5 Series | Yes | No | OpenPGP signing only; no HOTP | | Nitrokey Pro (v1, fw < 0.8) | Yes | Limited | Older firmware may report no HOTP support; test before use | diff --git a/doc/recovery-shell.md b/doc/recovery-shell.md index fc94047ba..c0ae078da 100644 --- a/doc/recovery-shell.md +++ b/doc/recovery-shell.md @@ -92,3 +92,9 @@ If you modified `/boot` or reflashed firmware, return to the GUI and: 1. Generate new TOTP/HOTP secret (`Options -> Generate new HOTP/TOTP secret`). 2. Update checksums and sign `/boot` (`Options -> Update checksums and sign all files in /boot`). 3. Optionally re-seal the TPM Disk Unlock Key by selecting a default boot option. + +## PIN Caching + +When exiting and re-entering the recovery shell, secrets are wiped and TTY is +re-detected on each iteration. This forces re-authentication (GPG PIN prompt) +on each entry, preventing cached credential reuse across shell sessions. diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index 910e22d33..f63867907 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -832,7 +832,7 @@ reset_tpm() { if (whiptail_warning --title 'Reset the TPM' \ --yesno "This will clear the TPM and replace its Owner passphrase with a new one!\n\nDo you want to proceed?" 0 80); then - if ! prompt_new_owner_passphrase; then + if ! prompt_new_owner_password; then INPUT "Press Enter to return to the menu..." return 1 fi diff --git a/initrd/bin/oem-factory-reset.sh b/initrd/bin/oem-factory-reset.sh index f70b03a03..f116c153c 100755 --- a/initrd/bin/oem-factory-reset.sh +++ b/initrd/bin/oem-factory-reset.sh @@ -1,6 +1,11 @@ #!/bin/bash # Automated setup of TPM, GPG keys, and disk +# TODO: Find a stronger mechanism for passing GPG commands that avoids the +# brittle --command-fd loop behavior. The current approach using +# "quit" relies on internal GPG behavior (keyedit.c:1510-1513, :2227-2229) +# and may break in future GPG versions. + set -o pipefail ## External files sourced @@ -10,6 +15,12 @@ set -o pipefail . /etc/luks-functions.sh . /tmp/config +# Reset background color - may be inherited as "error" from TPM error menu +BG_COLOR_MAIN_MENU="normal" + +# Allow firmware display in OEM reset context (flag may have been set during integrity report) +rm -f /tmp/hotpkey_fw_shown + TRACE_FUNC # use TERM to exit on error @@ -30,6 +41,7 @@ ADMIN_PIN_DEF=12345678 TPM_PASS_DEF=12345678 GPG_GEN_KEY_IN_MEMORY="n" GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD="n" +GPG_EXPORT=0 #Circumvent Librem Key/Nitrokey HOTP firmware bug https://github.com/osresearch/heads/issues/1167 MAX_HOTP_GPG_PIN_LENGTH=25 @@ -37,10 +49,11 @@ MAX_HOTP_GPG_PIN_LENGTH=25 # What are the Security components affected by custom passphrases CUSTOM_PASS_AFFECTED_COMPONENTS="" -# Default GPG Algorithm is RSA -# p256 also supported (TODO: nk3 supports RSA 4096 in secure element in firmare v1.7.1. Switch!? +# Default GPG Algorithm is RSA (key length set by RSA_KEY_LENGTH below) +# NIST P-256 also supported for Nitrokey 3 (chose NIST P-256 when RSA was not generated into secrets app) GPG_ALGO="RSA" -# Default RSA key length is 3072 bits for OEM key gen. 4096 are way longer to generate in smartcard +# Default RSA key length is 3072 bits for OEM key gen +# 4096 are way longer to generate in smartcard RSA_KEY_LENGTH=3072 # If we use complex generated passphrases, we will really try hard to make the @@ -49,6 +62,7 @@ MAKE_USER_RECORD_PASSPHRASES= # Function to handle --mode parameter handle_mode() { + TRACE_FUNC local mode=$1 case $mode in oem) @@ -125,6 +139,7 @@ DIE() { } local_whiptail_error() { + TRACE_FUNC local msg=$1 if [ "$msg" = "" ]; then DIE "whiptail error: An error msg is required" @@ -193,6 +208,8 @@ generate_inmemory_RSA_master_and_subkeys() { echo "Passphrase: ${ADMIN_PIN}" # Admin PIN echo "%commit" # Commit changes } | DO_WITH_DEBUG gpg --expert --batch --command-fd=0 --status-fd=1 --pinentry-mode=loopback --generate-key >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG on-card RSA key generation output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key generation failed!\n\n$ERROR" @@ -205,11 +222,12 @@ generate_inmemory_RSA_master_and_subkeys() { echo 4 # RSA (sign only) echo ${RSA_KEY_LENGTH} # Signing key size set to RSA_KEY_LENGTH echo 0 # No expiration date - echo ${ADMIN_PIN} # Local keyring admin pin - echo y # confirm + echo ${ADMIN_PIN} # Local keyring admin pin (passphrase requested before key creation, no confirm prompt) echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key "${GPG_USER_MAIL}" \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG RSA signing subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key signing subkey generation failed!\n\n$ERROR" @@ -222,11 +240,12 @@ generate_inmemory_RSA_master_and_subkeys() { echo 6 # RSA (encrypt only) echo ${RSA_KEY_LENGTH} # Encryption key size set to RSA_KEY_LENGTH echo 0 # No expiration date - echo ${ADMIN_PIN} # Local keyring admin pin - echo y # confirm + echo ${ADMIN_PIN} # Local keyring admin pin (passphrase requested before key creation, no confirm prompt) echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key "${GPG_USER_MAIL}" \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG RSA encryption subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key encryption subkey generation failed!\n\n$ERROR" @@ -246,24 +265,26 @@ generate_inmemory_RSA_master_and_subkeys() { echo Q # Quit echo ${RSA_KEY_LENGTH} # Authentication key size set to RSA_KEY_LENGTH echo 0 # No expiration date - echo ${ADMIN_PIN} # Local keyring admin pin - echo y # confirm + echo ${ADMIN_PIN} # Local keyring admin pin (passphrase requested before key creation, no confirm prompt) echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --expert --edit-key "${GPG_USER_MAIL}" \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG RSA authentication subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key authentication subkey generation failed!\n\n$ERROR" fi } -#Generate a gpg master key: no expiration date, p256 key (ECC) +#Generate a gpg master key: no expiration date, NIST P-256 key (ECC) #This key will be used to sign 3 subkeys: encryption, authentication and signing #The master key and subkeys will be copied to backup, and the subkeys moved from memory keyring to the smartcard generate_inmemory_p256_master_and_subkeys() { TRACE_FUNC - STATUS "Generating p256 master key for $DONGLE_BRAND" + STATUS "Generating NIST P-256 master key for $DONGLE_BRAND" + DEBUG "GPG batch key generation: Key-Type=ECDSA, Key-Curve=nistp256, Key-Usage=cert" { echo "Key-Type: ECDSA" # ECDSA key echo "Key-Curve: nistp256" # ECDSA key curve @@ -276,15 +297,17 @@ generate_inmemory_p256_master_and_subkeys() { echo "%commit" # Commit changes } | DO_WITH_DEBUG gpg --expert --batch --command-fd=0 --status-fd=1 --pinentry-mode=loopback --generate-key \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG p256 master key generation output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) - whiptail_error_die "GPG p256 Key generation failed!\n\n$ERROR" + whiptail_error_die "GPG NIST P-256 Key generation failed!\n\n$ERROR" fi #Keep Master key fingerprint for add key calls MASTER_KEY_FP=$(gpg --list-secret-keys --with-colons | grep fpr | cut -d: -f10) - STATUS "Generating p256 signing subkey for $DONGLE_BRAND" + STATUS "Generating NIST P-256 signing subkey for $DONGLE_BRAND" { echo addkey # add key in --edit-key mode echo 11 # ECC own set capability @@ -294,27 +317,30 @@ generate_inmemory_p256_master_and_subkeys() { echo ${ADMIN_PIN} # Local keyring admin pin echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key ${MASTER_KEY_FP} >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG p256 signing subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR_MSG=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Failed to add ECC nistp256 signing key to master key\n\n${ERROR_MSG}" fi - STATUS "Generating p256 encryption subkey for $DONGLE_BRAND" + STATUS "Generating NIST P-256 encryption subkey for $DONGLE_BRAND" { echo addkey echo 12 # ECC own set capability - echo Q # Quit echo 3 # P-256 echo 0 # No validity/expiration date echo ${ADMIN_PIN} # Local keyring admin pin echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key ${MASTER_KEY_FP} >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG p256 encryption subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR_MSG=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Failed to add ECC nistp256 encryption key to master key\n\n${ERROR_MSG}" fi - STATUS "Generating p256 authentication subkey for $DONGLE_BRAND" + STATUS "Generating NIST P-256 authentication subkey for $DONGLE_BRAND" { echo addkey # add key in --edit-key mode echo 11 # ECC own set capability @@ -326,6 +352,8 @@ generate_inmemory_p256_master_and_subkeys() { echo ${ADMIN_PIN} # Local keyring admin pin echo save # save changes and commit to keyring } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key ${MASTER_KEY_FP} >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG p256 authentication subkey output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR_MSG=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Failed to add ECC nistp256 authentication key to master key\n\n${ERROR_MSG}" @@ -356,24 +384,23 @@ keytocard_subkeys_to_smartcard() { echo "keytocard" #Move Signature key to smartcard echo "1" #Select Signature key key slot on smartcard echo "${ADMIN_PIN}" #Local keyring Subkey PIN - echo "${ADMIN_PIN_DEF}" #Smartcard Admin PIN - echo "0" #No expiration date + echo "${ADMIN_PIN_DEF}" #Smartcard Admin PIN (prompted once; scdaemon caches it for subsequent keytocard ops) echo "key 1" #Toggle off Signature key echo "key 2" #Toggle on Encryption key echo "keytocard" #Move Encryption key to smartcard echo "2" #Select Encryption key key slot on smartcard - echo "${ADMIN_PIN}" #Local keyring Subkey PIN - echo "${ADMIN_PIN_DEF}" #Smartcard Admin PIN + echo "${ADMIN_PIN}" #Local keyring Subkey PIN (card PIN already cached by scdaemon) echo "key 2" #Toggle off Encryption key echo "key 3" #Toggle on Authentication key echo "keytocard" #Move Authentication key to smartcard echo "3" #Select Authentication key slot on smartcard - echo "${ADMIN_PIN}" #Local keyring Subkey PIN - echo "${ADMIN_PIN_DEF}" #Smartcard Admin PIN + echo "${ADMIN_PIN}" #Local keyring Subkey PIN (card PIN still cached by scdaemon) echo "key 3" #Toggle off Authentication key echo "save" #Save changes and commit to keyring } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --edit-key "${GPG_USER_MAIL}" \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG keytocard output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key moving subkeys to smartcard failed!\n\n$ERROR" @@ -433,12 +460,13 @@ set_card_identity() { echo "name" echo "${surname}" echo "${given}" - echo "${ADMIN_PIN_DEF}" + # scdaemon caches the admin PIN from the preceding keytocard/generate + # session; name and login do not re-prompt for it fi if [ "$set_login" -eq 1 ]; then echo "login" echo "${GPG_USER_MAIL}" - echo "${ADMIN_PIN_DEF}" + # scdaemon admin PIN still cached; no re-prompt needed fi echo "quit" } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit || @@ -496,10 +524,18 @@ export_master_key_subkeys_and_revocation_key_to_private_LUKS_container() { #Export master key and subkeys to thumb drive STATUS "Exporting master key and subkeys to backup LUKS container" - gpg --export-secret-key --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/privkey.sec || + if gpg --export-secret-key --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/privkey.sec 2>/tmp/gpg_export_err; then + DEBUG "GPG master key export succeeded" + else + DEBUG "GPG master key export failed: $(cat /tmp/gpg_export_err)" DIE "Error exporting master key to private LUKS container's partition" - gpg --export-secret-subkeys --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/subkeys.sec || + fi + if gpg --export-secret-subkeys --armor --pinentry-mode loopback --passphrase="${pass}" "${GPG_USER_MAIL}" >"$mountpoint"/subkeys.sec 2>/tmp/gpg_export_err; then + DEBUG "GPG subkeys export succeeded" + else + DEBUG "GPG subkeys export failed: $(cat /tmp/gpg_export_err)" DIE "Error exporting subkeys to private LUKS container's partition" + fi #copy whole keyring to thumb drive, including revocation key and trust database cp -af ~/.gnupg "$mountpoint"/.gnupg || DIE "Error copying whole keyring to private LUKS container's partition" #Unmount private LUKS container's mount point @@ -541,7 +577,12 @@ export_public_key_to_thumbdrive_public_partition() { mount-usb.sh --device "$device" --mode "$mode" --mountpoint "$mountpoint" || DIE "Error mounting thumb drive's public partition" #TODO: reuse "Obtain GPG key ID" so that pubkey on public thumb drive partition is named after key ID STATUS "Exporting public key to USB" - gpg --export --armor "${GPG_USER_MAIL}" >"$mountpoint"/pubkey.asc || DIE "Error exporting public key to thumb drive's public partition" + if gpg --export --armor "${GPG_USER_MAIL}" >"$mountpoint"/pubkey.asc 2>/tmp/gpg_export_err; then + DEBUG "GPG public key export succeeded" + else + DEBUG "GPG public key export failed: $(cat /tmp/gpg_export_err)" + DIE "Error exporting public key to thumb drive's public partition" + fi umount "$mountpoint" || DIE "Error unmounting thumb drive's public partition" STATUS_OK "Public key exported to USB" @@ -635,6 +676,8 @@ gpg_key_factory_reset() { echo yes # confirm } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG factory-reset output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key factory reset failed!\n\n$ERROR" @@ -656,6 +699,8 @@ gpg_key_factory_reset() { echo ${ADMIN_PIN_DEF} # local keyring PIN } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG forcesig toggle output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "GPG Key forcesig toggle on failed!\n\n$ERROR" @@ -663,7 +708,7 @@ gpg_key_factory_reset() { STATUS_OK "Forced signature PIN enabled" fi - # use p256 for key generation if requested + # use NIST P-256 for key generation if requested if [ "$GPG_ALGO" = "p256" ]; then STATUS "Setting NIST-P256 key attributes on $DONGLE_BRAND" { @@ -680,6 +725,8 @@ gpg_key_factory_reset() { echo ${ADMIN_PIN_DEF} # local keyring PIN } | DO_WITH_DEBUG gpg --expert --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG NIST-P256 key-attr output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Setting key to NIST-P256 in $DONGLE_BRAND failed." @@ -703,6 +750,8 @@ gpg_key_factory_reset() { echo ${ADMIN_PIN_DEF} #Local keyring PIN } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=1 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG RSA key-attr output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output) whiptail_error_die "Setting key attributed to RSA ${RSA_KEY_LENGTH} bits in $DONGLE_BRAND failed." @@ -723,7 +772,7 @@ generate_OEM_gpg_keys() { if [ "$GPG_ALGO" = "RSA" ]; then STATUS "Generating RSA ${RSA_KEY_LENGTH}-bit keys on $DONGLE_BRAND" else - STATUS "Generating p256 keys on $DONGLE_BRAND" + STATUS "Generating NIST P-256 keys on $DONGLE_BRAND" fi { echo admin # admin menu @@ -738,6 +787,8 @@ generate_OEM_gpg_keys() { echo ${USER_PIN_DEF} # Default user PIN since we just factory reset } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG on-card key generation output: $(cat /tmp/gpg_card_edit_output)" #This outputs to console \ # "gpg: checking the trustdb" # "gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model" @@ -771,6 +822,8 @@ gpg_key_change_pin() { echo q } | DO_WITH_DEBUG gpg --command-fd=0 --status-fd=2 --pinentry-mode=loopback --card-edit \ >/tmp/gpg_card_edit_output 2>&1 + TRACE_FUNC + DEBUG "GPG PIN change output: $(cat /tmp/gpg_card_edit_output)" if [ $? -ne 0 ]; then ERROR=$(cat /tmp/gpg_card_edit_output | fold -s) whiptail_error_die "GPG Key PIN change failed!\n\n$ERROR" @@ -863,6 +916,8 @@ generate_checksums() { fi DEBUG "oem-factory-reset.sh: ${#param_files[@]} file(s) to sign (relative): ${param_files[*]}" + DEBUG "oem-factory-reset.sh: signing with USER_PIN='$USER_PIN' (length=${#USER_PIN})" + TRACE_FUNC if (cd /boot && sha256sum "${param_files[@]}") 2>/dev/null | gpg --detach-sign \ --pinentry-mode loopback \ @@ -880,6 +935,7 @@ generate_checksums() { ret=0 fi else + DEBUG "oem-factory-reset.sh: signing failed: $(cat /tmp/error)" cat /tmp/error ret=1 fi @@ -951,7 +1007,13 @@ usb_security_token_capabilities_check() { DONGLE_BRAND="$(detect_usb_security_dongle_branding)" export DONGLE_BRAND DEBUG "USB Security dongle detected: $DONGLE_BRAND" - INFO "Detected $DONGLE_BRAND" + # Only show generic "Detected" if no specific brand was identified + if [ "$DONGLE_BRAND" = "USB Security dongle" ]; then + INFO "Detected $DONGLE_BRAND" + else + # Specific brand detected - firmware version will be shown below + : + fi STATUS "Checking $DONGLE_BRAND capabilities" # ... first set board config preference @@ -960,14 +1022,35 @@ usb_security_token_capabilities_check() { DEBUG "Setting GPG_ALGO to (board-)configured: $CONFIG_GPG_ALGO" fi # ... overwrite with usb-token capability + # Nitrokey chose NIST P-256 when RSA was not generated into secrets app - TODO: review with lago changes + # Canokey and other dongles use default RSA (see default GPG_ALGO above) if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then GPG_ALGO="p256" DEBUG "Nitrokey 3 detected: Setting GPG_ALGO to: $GPG_ALGO" fi - #TODO: put everything related to USB Security dongle here + # Show firmware version for USB Security dongle + # Also capture firmware version for timing guidance in key generation message + # Wait for gpg card to be ready before hotp_verification + wait_for_gpg_card + DONGLE_FW_VERSION="" + if [ -x /bin/hotp_verification ]; then + if hotp_token_info="$(hotp_verification info 2>/dev/null)"; then + hotpkey_fw_display "$hotp_token_info" "$DONGLE_BRAND" + # Capture firmware version for timing guidance + if echo "$hotp_token_info" | grep -q "Firmware Nitrokey 3:"; then + DONGLE_FW_VERSION="$(echo "$hotp_token_info" | grep "Firmware Nitrokey 3:" | sed 's/.*: *//')" + elif echo "$hotp_token_info" | grep -q "Firmware:"; then + DONGLE_FW_VERSION="$(echo "$hotp_token_info" | grep "Firmware:" | sed 's/.*: *//')" + case "$DONGLE_FW_VERSION" in v*) ;; *) DONGLE_FW_VERSION="v$DONGLE_FW_VERSION" ;; esac + fi + DEBUG "Dongle firmware version: $DONGLE_FW_VERSION" + fi + fi } +# usb_security_token_capabilities_check now handles all USB Security dongle logic + ## main script start # check for args @@ -1029,13 +1112,13 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then DEBUG "Showing passphrase guidance: QR code from diceware.dmuth.org" qrenc "https://diceware.dmuth.org/" NOTE "Scan the QR code above for passphrase guidance (diceware.dmuth.org):" - NOTE "Recommended lengths: Disk Recovery Key: 6 words TPM Owner: 2 words GPG User PIN: 2 words" # Re-ownership of LUKS encrypted Disk: key, content and passphrase INPUT "Would you like to change the current LUKS Disk Recovery Key passphrase? (Highly recommended if you didn't install the OS yourself) [y/N]:" -n 1 prompt_output if [ "$prompt_output" == "y" \ -o "$prompt_output" == "Y" ]; then luks_new_Disk_Recovery_Key_passphrase_desired=1 + NOTE "Disk Recovery Key Passphrase: required to unlock disk, setup TPM Disk Unlock Key, access data from any computer, unsafe boot. DO NOT FORGET. Recommended: 6 words" fi INPUT "Would you like to re-encrypt LUKS container and generate new LUKS Disk Recovery Key? (Highly recommended if you didn't install the OS yourself) [y/N]:" -n 1 prompt_output @@ -1044,6 +1127,9 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then TRACE_FUNC test_luks_current_disk_recovery_key_passphrase luks_new_Disk_Recovery_Key_desired=1 + if [ "$luks_new_Disk_Recovery_Key_passphrase_desired" != "1" ]; then + NOTE "Disk Recovery Key Passphrase: required to unlock disk, setup TPM Disk Unlock Key, access data from any computer, unsafe boot. DO NOT FORGET. Recommended: 6 words" + fi fi #Prompt to ask if user wants to generate GPG key material in memory or on smartcard @@ -1081,9 +1167,17 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then CUSTOM_PASS_AFFECTED_COMPONENTS+="TPM Owner Passphrase\n" fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "y" ]; then - CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Key material backup passphrase (Same as GPG Admin PIN)\n" + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Key material backup passphrase (Same as NK3 Secrets app PIN / GPG Admin PIN)\n" + else + CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Key material backup passphrase (Same as GPG Admin PIN)\n" + fi + fi + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + CUSTOM_PASS_AFFECTED_COMPONENTS+="NK3 Secrets app PIN / GPG Admin PIN\n" + else + CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Admin PIN\n" fi - CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG Admin PIN\n" # Only show GPG User PIN as affected component if GPG_GEN_KEY_IN_MEMORY not requested or GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD is if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then CUSTOM_PASS_AFFECTED_COMPONENTS+="GPG User PIN\n" @@ -1123,10 +1217,17 @@ if [ "$use_defaults" == "n" -o "$use_defaults" == "N" ]; then INPUT "Enter desired TPM Owner Passphrase (min 8 chars):" -r TPM_PASS done fi - NOTE "GPG Admin PIN: management tasks on USB Security dongle, seal measurements under HOTP. 3 attempts max, locks Admin out. DO NOT FORGET. Recommended: 2 words" - while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do - INPUT "Enter desired GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r ADMIN_PIN - done + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + NOTE "NK3 Secrets app PIN / GPG Admin PIN: seals HOTP measurements and manages OpenPGP card. 3 attempts max. DO NOT FORGET. Recommended: 2 words" + while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do + INPUT "Enter desired NK3 Secrets app PIN / GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r ADMIN_PIN + done + else + NOTE "GPG Admin PIN: management tasks on USB Security dongle, seal measurements under HOTP. 3 attempts max, locks Admin out. DO NOT FORGET. Recommended: 2 words" + while [[ ${#ADMIN_PIN} -lt 6 ]] || [[ ${#ADMIN_PIN} -gt $MAX_HOTP_GPG_PIN_LENGTH ]]; do + INPUT "Enter desired GPG Admin PIN (6-${MAX_HOTP_GPG_PIN_LENGTH} chars):" -r ADMIN_PIN + done + fi #USER PIN not required in case of GPG_GEN_KEY_IN_MEMORY not requested of if GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD is # That is, if keys were NOT generated in memory (on smartcard only) or # if keys were generated in memory but are to be moved from local keyring to smartcard @@ -1229,14 +1330,32 @@ if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD #Now that USB Security dongle is detected, we can check its capabilities and limitations usb_security_token_capabilities_check + + # Adjust RSA key size based on dongle capabilities + # Yubikey: Uses faster onboard crypto, can handle 4096-bit RSA in reasonable time + # - Source: Yubikey 5 Series technical manual (~5s for 4096-bit RSA key gen) + # - Source: Yubico forum shows RSA-2048 at ~475ms, 4096-bit ~1-2s (https://forum.yubico.com/viewtopic9d4a.html?p=4515) + # Other dongles (Librem Key, Nitrokey Pro/Storage): Use slower STM32 chip + # - Source: Nitrokey Pro uses STM32F4, RSA is software-based, very slow + # - Source: User testing shows ~10 min for 3072-bit RSA on Librem Key v0.10 + # TODO: This 4096-bit change for Yubikey is untested - user requested to add with source verification + if [ "$DONGLE_BRAND" = "Yubikey" ]; then + DEBUG "Yubikey detected: using 4096-bit RSA key length (faster onboard crypto)" + RSA_KEY_LENGTH=4096 + elif [ "$DONGLE_BRAND" = "Canokey" ]; then + # Canokey has limited RSA key size support, use 2048-bit for reliability + DEBUG "Canokey detected: using 2048-bit RSA key length (limited key size support)" + RSA_KEY_LENGTH=2048 + fi fi assert_signable # Action time... -# clear gpg-agent cache so that next gpg calls doesn't have past keyring in memory -killall gpg-agent >/dev/null 2>&1 || true +# clear gpg-agent and scdaemon cache so that next gpg calls don't have stale state +# scdaemon holds exclusive CCID lock to dongle - must be killed to allow fresh card access +killall gpg-agent scdaemon >/dev/null 2>&1 || true # clear local keyring rm -rf /.gnupg/*.kbx /.gnupg/*.gpg >/dev/null 2>&1 || true @@ -1305,9 +1424,35 @@ else #Reset Nitrokey 3 secret app reset_nk3_secret_app #Generate GPG key and subkeys on smartcard only - STATUS "Resetting USB Security dongle OpenPGP smartcard with GPG" if [ "$GPG_ALGO" = "RSA" ]; then - NOTE "RSA key generation on $DONGLE_BRAND may take 10 or more minutes - please be patient" + DEBUG "RSA key length: $RSA_KEY_LENGTH bits" + if [ "$RSA_KEY_LENGTH" -ge 3072 ]; then + # Provide firmware-aware timing guidance + # Old Nitrokey Pro/Pro 2 firmware (< v0.15) and Librem Key (any version) + # have slower RSA key generation (around 10 minutes for 3072-bit) + # Yubikey uses faster onboard crypto, reasonable time even at 4096-bit + timing_msg="" + if [ "$DONGLE_BRAND" = "Yubikey" ]; then + # Yubikey handles 4096-bit RSA quickly (~5 seconds) + timing_msg="may take a minute or two" + elif [ "$DONGLE_BRAND" = "Librem Key" ]; then + timing_msg="may take several minutes (up to 10 minutes on older USB Security dongles)" + elif [ "$DONGLE_BRAND" = "Nitrokey Pro" ] || [ "$DONGLE_BRAND" = "Nitrokey Storage" ]; then + # Check if older firmware (before v0.15 had optimizations) + if [ -n "$DONGLE_FW_VERSION" ]; then + if [ "$(printf '%s\n' "$DONGLE_FW_VERSION" "v0.15" | sort -V | head -1)" != "v0.15" ]; then + timing_msg="may take several minutes (up to 10 minutes on older USB Security dongles)" + else + timing_msg="may take several minutes" + fi + else + timing_msg="may take several minutes" + fi + else + timing_msg="may take several minutes" + fi + NOTE "RSA ${RSA_KEY_LENGTH}-bit key generation on $DONGLE_BRAND ${timing_msg} - please be patient" + fi fi gpg_key_factory_reset generate_OEM_gpg_keys @@ -1333,9 +1478,17 @@ fi if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD" = "y" ]; then #Only apply smartcard PIN change if smartcard only or if keytocard op is expected next if [ "${USER_PIN}" != "${USER_PIN_DEF}" -o "${ADMIN_PIN}" != "${ADMIN_PIN_DEF}" ]; then - STATUS "Changing default GPG Admin PIN" + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + STATUS "Changing default NK3 Secrets app PIN / GPG Admin PIN" + else + STATUS "Changing default GPG Admin PIN" + fi gpg_key_change_pin "3" "${ADMIN_PIN_DEF}" "${ADMIN_PIN}" - STATUS_OK "GPG Admin PIN changed" + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then + STATUS_OK "NK3 Secrets app PIN / GPG Admin PIN changed" + else + STATUS_OK "GPG Admin PIN changed" + fi STATUS "Changing default GPG User PIN" gpg_key_change_pin "1" "${USER_PIN_DEF}" "${USER_PIN}" STATUS_OK "GPG User PIN changed" @@ -1343,14 +1496,23 @@ if [ "$GPG_GEN_KEY_IN_MEMORY" = "n" -o "$GPG_GEN_KEY_IN_MEMORY_COPY_TO_SMARTCARD fi ## export pubkey to USB +# Note: The thumb drive's public partition was already exported in +# wipe_thumb_drive_and_copy_gpg_key_material(). This block is for exporting +# to a DIFFERENT USB drive if user wants a separate copy (not the thumb drive). if [ "$GPG_EXPORT" != "0" ]; then - STATUS "Exporting generated key to USB" - # copy to USB - if ! cp "${PUBKEY}" "/media/${GPG_GEN_KEY}.asc" 2>/tmp/error; then - ERROR=$(tail -n 1 /tmp/error | fold -s) - whiptail_error_die "Key export error: unable to copy ${GPG_GEN_KEY}.asc to /media:\n\n$ERROR" + # The thumb drive is already unmounted at this point, so /media is not mounted + # Only attempt export if /media is actually mounted (different drive inserted) + if grep -q /media /proc/mounts 2>/dev/null; then + STATUS "Exporting generated key to USB" + if ! cp "${PUBKEY}" "/media/${GPG_GEN_KEY}.asc" 2>/tmp/error; then + ERROR=$(tail -n 1 /tmp/error | fold -s) + whiptail_error_die "Key export error: unable to copy ${GPG_GEN_KEY}.asc to /media:\n\n$ERROR" + fi + mount -o remount,ro /media 2>/dev/null + umount /media 2>/dev/null || true + else + INFO "Skipping separate USB export - public key already saved to thumb drive's public partition" fi - mount -o remount,ro /media 2>/dev/null fi # ensure key imported locally @@ -1508,4 +1670,22 @@ luks_secrets_cleanup unset luks_passphrase_changed unset tpm_owner_passphrase_changed +# Clean any stale files in /media from previous sessions (only when not mounted) +# This removes residual files from previous runs when /media wasn't mounted +if ! grep -q /media /proc/mounts 2>/dev/null; then + rm -rf /media/* 2>/dev/null || true +fi + +# Ensure /media is unmounted before reboot to prevent USB drive corruption +# Force unmount /media and close any LUKS mappings that might block it +umount /media 2>/dev/null || true +# Close any remaining LUKS mappings (these can block umount) +for dev in /dev/mapper/usb_mount_*; do + [ -e "$dev" ] && cryptsetup close "$(basename "$dev")" 2>/dev/null || true +done +# Sync to ensure all writes are flushed +sync +# Final attempt to unmount after closing LUKS +umount /media 2>/dev/null || true + reboot.sh diff --git a/initrd/bin/tpm-reset.sh b/initrd/bin/tpm-reset.sh index 2c67e7471..047d49ef0 100755 --- a/initrd/bin/tpm-reset.sh +++ b/initrd/bin/tpm-reset.sh @@ -5,4 +5,4 @@ NOTE "This will erase all keys and secrets from the TPM" prompt_new_owner_password -tpmr.sh reset "$tpm_owner_password" +tpmr.sh reset "$tpm_owner_passphrase" diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 51c1e9fb9..05e68806f 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -721,6 +721,10 @@ cache_gpg_signing_pin() { set_user_config "CONFIG_GPG_KEY_BACKUP_IN_USE" "y" DEBUG "CONFIG_GPG_KEY_BACKUP_IN_USE set; unmounting backup thumb drive" umount /media || DIE "Unable to unmount USB" + # Close any LUKS mapping that may have been opened + for dev in /dev/mapper/usb_mount_*; do + [ -e "$dev" ] && cryptsetup close "$(basename "$dev")" 2>/dev/null || true + done return else DEBUG "Backup key already in use this session (CONFIG_GPG_KEY_BACKUP_IN_USE=y); skipping mount" @@ -792,7 +796,7 @@ cache_gpg_signing_pin() { while [ "$sc_pin_tries" -lt 3 ]; do sc_pin_tries=$((sc_pin_tries + 1)) while [ -z "$sc_user_pin" ]; do - INPUT "Enter GPG User PIN for smartcard:" -r -s sc_user_pin + INPUT "Enter $DONGLE_BRAND User PIN:" -r -s sc_user_pin done if gpg --pinentry-mode=loopback \ --passphrase-file <(printf '%s' "$sc_user_pin") \ @@ -1606,7 +1610,7 @@ list_usb_storage() { } # Prompt for a TPM Owner Password if it is not already cached in /tmp/secret/tpm_owner_password. -# Sets tpm_owner_password variable reused in flow, and cache file used until recovery shell is accessed. +# Sets tpm_owner_passphrase variable reused in flow, and cache file used until recovery shell is accessed. # Tools should optionally accept a TPM password on the command line, since some flows need # it multiple times and only one prompt is ideal. prompt_tpm_owner_password() { @@ -1614,16 +1618,16 @@ prompt_tpm_owner_password() { if [ -s /tmp/secret/tpm_owner_password ]; then DEBUG "/tmp/secret/tpm_owner_password already cached in file. Reusing" - tpm_owner_password=$(cat /tmp/secret/tpm_owner_password) + tpm_owner_passphrase=$(cat /tmp/secret/tpm_owner_password) return 0 fi - INPUT "TPM Owner Password:" -r -s tpm_owner_password + INPUT "TPM Owner Password:" -r -s tpm_owner_passphrase # 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 owner_password under /tmp/secret/tpm_owner_password" + echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_password || DIE "Unable to cache TPM owner_password under /tmp/secret/tpm_owner_password" } # Prompt for a new TPM Owner Password when resetting the TPM. @@ -1633,12 +1637,12 @@ prompt_tpm_owner_password() { prompt_new_owner_password() { TRACE_FUNC local tpm_owner_password2 - tpm_owner_password=1 + tpm_owner_passphrase=1 tpm_owner_password2=2 - while [ "$tpm_owner_password" != "$tpm_owner_password2" ] || [ "${#tpm_owner_password}" -gt 32 ] || [ -z "$tpm_owner_password" ]; do - INPUT "New TPM Owner Password (2 words suggested, 1-32 characters max):" -r -s tpm_owner_password + while [ "$tpm_owner_passphrase" != "$tpm_owner_password2" ] || [ "${#tpm_owner_passphrase}" -gt 32 ] || [ -z "$tpm_owner_passphrase" ]; do + INPUT "New TPM Owner Password (2 words suggested, 1-32 characters max):" -r -s tpm_owner_passphrase INPUT "Repeat chosen TPM Owner Password:" -r -s tpm_owner_password2 - if [ "$tpm_owner_password" != "$tpm_owner_password2" ]; then + if [ "$tpm_owner_passphrase" != "$tpm_owner_password2" ]; then WARN "Passphrases entered do not match. Try again!" fi done @@ -1646,7 +1650,7 @@ prompt_new_owner_password() { # 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/tpm_owner_password" + echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_password || DIE "Unable to cache TPM password under /tmp/secret/tpm_owner_password" } check_tpm_counter() { @@ -1847,7 +1851,7 @@ increment_tpm_counter() { WARN "TPM Owner Password is required to update rollback counter before signing updated boot hashes." DEBUG "increment_tpm_counter: TPM1 path has no cached/provided owner password; prompting now" prompt_tpm_owner_password - tpm_password="$tpm_owner_password" + tpm_password="$tpm_owner_passphrase" DEBUG "increment_tpm_counter: TPM1 owner password obtained and cached" fi From c8ed01270d0c73b7cdd015304bbf7beb42274180 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Sat, 4 Apr 2026 15:25:10 -0400 Subject: [PATCH 10/12] recovery: clear secrets inside while loop to force PIN re-prompt on shell respawn Signed-off-by: Thierry Laurion --- initrd/etc/functions.sh | 42 ++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 05e68806f..d9b0f5855 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -893,33 +893,33 @@ gpg_auth() { recovery() { TRACE_FUNC - # Remove any temporary secret files, but recreate the directory so that new tools can use it. - - #safe to always be true. Otherwise "set -e" would make it exit here - shred -n 10 -z -u /tmp/secret/* 2>/dev/null || true - rm -rf /tmp/secret - mkdir -p /tmp/secret - - # ensure /tmp/config exists for recovery scripts that depend on it - touch /tmp/config - . /tmp/config - - # Log board and firmware/EC versions in one go. ec_version() will - # return an empty string if nothing is available, so the output is still - # well-formed even on systems without an EC version string. - DEBUG "Board $CONFIG_BOARD - version $(fw_version) EC_VER: $(ec_version)" - - if [ "$CONFIG_TPM" = "y" ]; then - INFO "TPM: Extending PCR[4] to prevent any further secret unsealing" - tpmr.sh extend -ix 4 -ic recovery - fi - if [ "$CONFIG_RESTRICTED_BOOT" = y ]; then NOTE "Restricted Boot enabled, recovery console disabled, rebooting in 5 seconds" sleep 5 /bin/reboot.sh fi while [ true ]; do + # Re-detect TTY on each iteration so INPUT uses the correct device + detect_heads_tty + + # Wipe secrets at start of each iteration to ensure fresh state + #safe to always be true. Otherwise "set -e" would make it exit here + shred -n 10 -z -u /tmp/secret/* 2>/dev/null || true + rm -rf /tmp/secret + mkdir -p /tmp/secret + + # ensure /tmp/config exists for recovery scripts that depend on it + touch /tmp/config + . /tmp/config + + # Log board and firmware/EC versions in one go + DEBUG "Board $CONFIG_BOARD - version $(fw_version) EC_VER: $(ec_version)" + + if [ "$CONFIG_TPM" = "y" ]; then + INFO "TPM: Extending PCR[4] to prevent any further secret unsealing" + tpmr.sh extend -ix 4 -ic recovery + fi + #Going to recovery shell should be authenticated if supported gpg_auth From e567b1282bd4a038d97d6de6a67011095c8eaa77 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Mon, 6 Apr 2026 16:38:07 -0400 Subject: [PATCH 11/12] mount-usb: auto-select LUKS partition when passphrase is provided When a passphrase is supplied (--pass) and multiple USB partitions are present, scan for the one LUKS partition and mount it automatically. This removes the need for the user to manually pick the correct partition when using the GPG key-material backup thumb drive, which always has two partitions: a LUKS-encrypted private partition and an exFAT public one. If exactly one LUKS partition is found it is selected silently; if zero or more than one LUKS partition is found the existing interactive menu is shown as before, so the behavior is unchanged for all other cases. Remove the now-redundant WARN in cache_gpg_signing_pin that instructed the user to select the encrypted LUKS partition manually. Signed-off-by: Thierry Laurion --- initrd/bin/mount-usb.sh | 17 +++++++++++++++++ initrd/etc/functions.sh | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/initrd/bin/mount-usb.sh b/initrd/bin/mount-usb.sh index fb975ed56..a22033108 100755 --- a/initrd/bin/mount-usb.sh +++ b/initrd/bin/mount-usb.sh @@ -113,6 +113,23 @@ else if [ $(cat /tmp/usb_block_devices | wc -l) -eq 1 ]; then USB_MOUNT_DEVICE="$(cat /tmp/usb_block_devices)" fi + # When a passphrase is provided and multiple devices are present, + # auto-select the LUKS partition (e.g. GPG backup drive: LUKS private + exFAT public). + # This avoids burdening the user with selecting the right partition. + if [ -z "$USB_MOUNT_DEVICE" ] && [ -n "$PASS" ]; then + luks_dev="" + luks_count=0 + while IFS= read -r dev; do + if cryptsetup isLuks "$dev" 2>/dev/null; then + luks_dev="$dev" + luks_count=$((luks_count + 1)) + fi + done /tmp/usb_disk_list diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index d9b0f5855..3217e3490 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -677,7 +677,6 @@ cache_gpg_signing_pin() { while [ -z "$gpg_admin_pin" ]; do INPUT "Please enter GPG Admin PIN needed to use the GPG backup thumb drive:" -r -s gpg_admin_pin done - WARN "Please select encrypted LUKS on GPG key material backup thumb drive (not public labeled one)" mount-usb.sh --pass "$gpg_admin_pin" || DIE "Unable to mount USB with provided GPG Admin PIN" DEBUG "USB backup thumb drive mounted; clearing card stubs and importing private subkeys" STATUS "Importing GPG private subkeys from backup thumb drive" From 0152068517177ac36313fd0be45dcdc058deeedd Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Mon, 6 Apr 2026 17:59:00 -0400 Subject: [PATCH 12/12] fix tpmr.sh: use tpm_owner_passphrase from prompt function The prompt_tpm_owner_password() function sets tpm_owner_passphrase variable, but tpm2_seal was using an unset tpm_owner_password variable instead. This caused evictcontrol to fail with auth error (0x9A2) since no passphrase was being passed to the TPM command. Also standardizes all user-facing strings and variables to use 'passphrase' instead of 'password' for TPM owner auth, including the cache file path. Fixes regression introduced in commit 16648ca4b9. Signed-off-by: Thierry Laurion --- initrd/bin/gui-init.sh | 3 +- initrd/bin/oem-factory-reset.sh | 3 +- initrd/bin/seal-hotpkey.sh | 22 ++- initrd/bin/seal-totp.sh | 6 +- initrd/bin/tpmr.sh | 283 ++++++++++++++++++-------------- initrd/etc/functions.sh | 106 ++++++------ 6 files changed, 225 insertions(+), 198 deletions(-) diff --git a/initrd/bin/gui-init.sh b/initrd/bin/gui-init.sh index f63867907..710b9d810 100755 --- a/initrd/bin/gui-init.sh +++ b/initrd/bin/gui-init.sh @@ -956,8 +956,7 @@ fi # Detect dongle branding from USB VID:PID -- must run AFTER enable_usb so lsusb # can see the dongle (NK3 enumerates ~1 second after USB module load). -DONGLE_BRAND="$(detect_usb_security_dongle_branding)" -export DONGLE_BRAND +detect_usb_security_dongle_branding if detect_boot_device; then # /boot device with installed OS found diff --git a/initrd/bin/oem-factory-reset.sh b/initrd/bin/oem-factory-reset.sh index f116c153c..2755cbb0b 100755 --- a/initrd/bin/oem-factory-reset.sh +++ b/initrd/bin/oem-factory-reset.sh @@ -1004,8 +1004,7 @@ usb_security_token_capabilities_check() { enable_usb # Always detect dongle branding from USB VID:PID — never read a stored file. - DONGLE_BRAND="$(detect_usb_security_dongle_branding)" - export DONGLE_BRAND + detect_usb_security_dongle_branding DEBUG "USB Security dongle detected: $DONGLE_BRAND" # Only show generic "Detected" if no specific brand was identified if [ "$DONGLE_BRAND" = "USB Security dongle" ]; then diff --git a/initrd/bin/seal-hotpkey.sh b/initrd/bin/seal-hotpkey.sh index 10e93f544..54aa76c8b 100755 --- a/initrd/bin/seal-hotpkey.sh +++ b/initrd/bin/seal-hotpkey.sh @@ -51,8 +51,7 @@ counter_value=1 enable_usb # Detect branding after USB is up so lsusb can see the device. -DONGLE_BRAND="$(detect_usb_security_dongle_branding)" -export DONGLE_BRAND +detect_usb_security_dongle_branding DEBUG "$DONGLE_BRAND detected via USB VID:PID" TRACE_FUNC @@ -72,8 +71,7 @@ if ! hotp_token_info="$(hotp_verification info)"; then fi # Re-detect branding now that the dongle is confirmed present. -DONGLE_BRAND="$(detect_usb_security_dongle_branding)" -export DONGLE_BRAND +detect_usb_security_dongle_branding DEBUG "$DONGLE_BRAND detected via USB VID:PID" # Truncate the secret if it is longer than the maximum HOTP secret @@ -109,13 +107,13 @@ hotpkey_fw_display "$hotp_token_info" "$DONGLE_BRAND" show_pin_retries() { local info info="$(hotp_verification info 2>/dev/null)" || true - if [ "$prompt_message" = "Secrets app" ]; then + if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then admin_pin_retries=$(echo "$info" | grep "Secrets app PIN counter:" | cut -d ':' -f 2 | tr -d ' ') else admin_pin_retries=$(echo "$info" | grep "Card counters: Admin" | grep -o 'Admin [0-9]*' | grep -o '[0-9]*') fi admin_pin_retries="${admin_pin_retries:-0}" - STATUS "$DONGLE_BRAND $prompt_message PIN retries remaining: $(pin_color "$admin_pin_retries")${admin_pin_retries}\033[0m" + STATUS "$DONGLE_BRAND GPG Admin PIN retries remaining: $(pin_color "$admin_pin_retries")${admin_pin_retries}\033[0m" } # Try using factory default admin PIN for 1 month following OEM reset to ease @@ -133,7 +131,7 @@ if [ "$((now_date - gpg_key_create_time))" -gt "$month_secs" ]; then elif [ "$admin_pin_retries" -lt 3 ]; then DEBUG "Not trying default PIN ($admin_pin): only $admin_pin_retries attempt(s) left" else - STATUS "Trying $prompt_message PIN to seal HOTP secret on $DONGLE_BRAND" + STATUS "Trying GPG Admin PIN to seal HOTP secret on $DONGLE_BRAND" # NK3 requires physical touch confirmation for the initialize operation if [ "$DONGLE_BRAND" = "Nitrokey 3" ]; then NOTE "Nitrokey 3 requires physical presence: touch the dongle when prompted" @@ -151,9 +149,9 @@ if [ "$admin_pin_status" -ne 0 ]; then for tries in 1 2 3; do show_pin_retries if [ "$tries" -eq 1 ]; then - INPUT "Enter your $DONGLE_BRAND $prompt_message PIN (attempt $tries/3):" -r -s admin_pin + INPUT "Enter your $DONGLE_BRAND GPG Admin PIN (attempt $tries/3):" -r -s admin_pin else - INPUT "Wrong PIN - re-enter your $DONGLE_BRAND $prompt_message PIN (attempt $tries/3):" -r -s admin_pin + INPUT "Wrong PIN - re-enter your $DONGLE_BRAND GPG Admin PIN (attempt $tries/3):" -r -s admin_pin fi if hotp_initialize "$admin_pin" $HOTP_SECRET $counter_value "$DONGLE_BRAND"; then break @@ -163,10 +161,10 @@ if [ "$admin_pin_status" -ne 0 ]; then shred -n 10 -z -u "$HOTP_SECRET" 2>/dev/null case "$DONGLE_BRAND" in "Nitrokey Pro" | "Nitrokey Storage" | "Nitrokey 3") - DIE "Setting HOTP secret on $DONGLE_BRAND failed after 3 attempts. To reset $prompt_message PIN: redo Re-Ownership, or use Nitrokey App 2, or contact Nitrokey support." + DIE "Setting HOTP secret on $DONGLE_BRAND failed after 3 attempts. To reset GPG Admin PIN: redo Re-Ownership, or use Nitrokey App 2, or contact Nitrokey support." ;; "Librem Key") - DIE "Setting HOTP secret on $DONGLE_BRAND failed after 3 attempts. To reset $prompt_message PIN: redo Re-Ownership or contact Purism support." + DIE "Setting HOTP secret on $DONGLE_BRAND failed after 3 attempts. To reset GPG Admin PIN: redo Re-Ownership or contact Purism support." ;; *) DIE "Setting HOTP secret failed after 3 attempts" @@ -177,7 +175,7 @@ if [ "$admin_pin_status" -ne 0 ]; then else # Default PIN was accepted — security reminder, not a fatal error. # NOTE prints blank lines before/after and is always visible; no INPUT needed. - NOTE "Default $prompt_message PIN detected. Change it via Options --> OEM Factory Reset / Re-Ownership." + NOTE "Default GPG Admin PIN detected. Change it via Options --> OEM Factory Reset / Re-Ownership." fi # HOTP key no longer needed diff --git a/initrd/bin/seal-totp.sh b/initrd/bin/seal-totp.sh index 1edab24b9..44a5c72e9 100755 --- a/initrd/bin/seal-totp.sh +++ b/initrd/bin/seal-totp.sh @@ -50,7 +50,7 @@ DEBUG "Sealing TOTP without PCR6 involvement (LUKS header consistency is not fir # pcr 7 is containing measurements of user injected stuff in cbfs DEBUG "Sealing TOTP with actual state of PCR7 (User injected stuff in cbfs)" tpmr.sh pcrread -a 7 "$pcrf" -#Make sure we clear the TPM Owner Password from memory in case it failed to be used to seal TOTP +#Make sure we clear the TPM Owner Passphrase from memory in case it failed to be used to seal TOTP # if the board has TPM2 tools, check for the primary handle before # attempting to seal; a missing handle is the most common reason for @@ -61,9 +61,9 @@ fi # perform sealing via tpmr.sh. Failures may indicate missing primary handle # or other TPM state issues. Avoid DO_WITH_DEBUG so interactive prompts -# (TPM owner password on TPM1) are not hidden from the user. +# (TPM owner passphrase on TPM1) are not hidden from the user. STATUS "Sealing TOTP secret to TPM NVRAM" -if ! tpmr.sh seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSWORD"; then +if ! tpmr.sh seal "$TOTP_SECRET" "$TPM_NVRAM_SPACE" 0,1,2,3,4,7 "$pcrf" 312 "" "$TPM_PASSPHRASE"; then # tpmr.sh already logged details; guide user generically to reset TPM DIE "Unable to seal TOTP secret to TPM NVRAM; reset the TPM (Options -> TPM/TOTP/HOTP Options -> Reset the TPM in the GUI) and try again." fi diff --git a/initrd/bin/tpmr.sh b/initrd/bin/tpmr.sh index abe9a6dde..264a15ffa 100755 --- a/initrd/bin/tpmr.sh +++ b/initrd/bin/tpmr.sh @@ -213,7 +213,7 @@ replay_pcr() { if [ "$alg" = "sha256" ]; then alg_digits=64; fi shift 2 replayed_pcr=$(extend_pcr_state $alg $(printf "%.${alg_digits}d" 0) \ - $(echo "$log" | awk -v alg=$alg -v pcr=$pcr -f <(echo $AWK_PROG)) $@) + $(echo "$log" | awk -v alg="$alg" -v pcr="$pcr" -f <(echo "$AWK_PROG")) "$@") echo $replayed_pcr | hex2bin DEBUG "Replayed cbmem -L clean boot state of PCR=$pcr ALG=$alg : $replayed_pcr" # To manually introspect current PCR values: @@ -309,32 +309,39 @@ tpm2_counter_inc() { tpm1_counter_create() { TRACE_FUNC - # tpmr.sh handles the TPM Owner Password (from cache or prompt), but all - # other parameters for TPM1 are passed directly, and TPM2 mimics the - # TPM1 interface. - prompt_tpm_owner_password - TMP_ERR_FILE=$(mktemp) - if ! tpm counter_create -pwdo "$(cat "/tmp/secret/tpm_owner_password")" "$@" 2>"$TMP_ERR_FILE"; then - DEBUG "Failed to create counter from tpm1_counter_create. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - # Log the contents of the temporary error file - while IFS= read -r line; do - DEBUG "tpm1 stderr: $line" - done <"$TMP_ERR_FILE" + local attempt=0 + while true; do + attempt=$((attempt + 1)) + prompt_tpm_owner_password + tpm_owner_passphrase="$(cat /tmp/secret/tpm_owner_passphrase)" + TMP_ERR_FILE=$(mktemp) + if tpm counter_create -pwdo "$tpm_owner_passphrase" "$@" 2>"$TMP_ERR_FILE"; then + rm -f "$TMP_ERR_FILE" + return 0 + fi + tmp_err_content="$(cat "$TMP_ERR_FILE")" rm -f "$TMP_ERR_FILE" - DIE "Unable to create counter from tpm1_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." - fi - rm -f "$TMP_ERR_FILE" + DEBUG "Failed attempt $attempt to create counter from tpm1_counter_create. Stderr: $tmp_err_content" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + if echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission'; then + if [ "$attempt" -ge 3 ]; then + DIE "Unable to create counter from tpm1_counter_create after 3 attempts. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + WARN "Counter creation failed (bad passphrase?). Retrying..." + else + DIE "Unable to create counter from tpm1_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + done } tpm2_counter_create() { TRACE_FUNC - pwd="" # owner password from argument - label="" # label argument + pass="" # owner passphrase from argument + label="" # label argument while true; do case "$1" in -pwdc) - pwd="$2" + pass="$2" shift 2 ;; -la) @@ -347,33 +354,44 @@ tpm2_counter_create() { esac done - # if caller supplied a password use it, otherwise prompt / use cache - if [ -n "$pwd" ]; then - tpm_owner_password="$pwd" - # cache it so other functions can reuse without prompting - mkdir -p "$SECRET_DIR" || true - echo -n "$tpm_owner_password" >/tmp/secret/tpm_owner_password 2>/dev/null || true - else - prompt_tpm_owner_password - fi - rand_index="1$(dd if=/dev/urandom bs=1 count=3 2>/dev/null | xxd -pc3)" - # capture stderr to a temp file for visibility - TMP_ERR_FILE=$(mktemp) - if ! tpm2 nvdefine -C o -s 8 -a "ownerread|authread|authwrite|nt=1" \ - -P "$(tpm2_password_hex "$tpm_owner_password")" "0x$rand_index" \ - 2>"$TMP_ERR_FILE" >/dev/null; then - DEBUG "Failed to create counter from tpm2_counter_create. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - # log the TPM2 stderr output - while IFS= read -r line; do - DEBUG "tpm2 stderr: $line" - done <"$TMP_ERR_FILE" + + local attempt=0 + while true; do + attempt=$((attempt + 1)) + if [ -n "$pass" ]; then + tpm_owner_passphrase="$pass" + mkdir -p "$SECRET_DIR" || true + echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_passphrase 2>/dev/null || true + else + prompt_tpm_owner_password + tpm_owner_passphrase="$tpm_owner_passphrase" + fi + + TMP_ERR_FILE=$(mktemp) + if tpm2 nvdefine -C o -s 8 -a "ownerread|authread|authwrite|nt=1" \ + -P "$(tpm2_password_hex "$tpm_owner_passphrase")" "0x$rand_index" \ + 2>"$TMP_ERR_FILE" >/dev/null; then + rm -f "$TMP_ERR_FILE" + echo "$rand_index: (valid after an increment)" + return 0 + fi + tmp_err_content="$(cat "$TMP_ERR_FILE")" rm -f "$TMP_ERR_FILE" - DIE "Unable to create counter from tpm2_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." - fi - rm -f "$TMP_ERR_FILE" - echo "$rand_index: (valid after an increment)" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + DEBUG "tpm2_counter_create attempt $attempt failed. Stderr: $tmp_err_content" + if echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission'; then + if [ -n "$pass" ]; then + DIE "Counter creation failed with provided passphrase. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + if [ "$attempt" -ge 3 ]; then + DIE "Unable to create counter from tpm2_counter_create after 3 attempts. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + WARN "Counter creation failed (bad passphrase?). Retrying..." + else + DIE "Unable to create counter from tpm2_counter_create. Please reset the TPM using the GUI menu (Options -> TPM/TOTP/HOTP Options -> Reset the TPM) and try again." + fi + done } tpm2_startsession() { @@ -461,10 +479,10 @@ tpm2_seal() { index="$2" pcrl="$3" #0,1,2,3,4,5,6,7 (does not include algorithm prefix) pcrf="$4" - sealed_size="$5" # Not used for TPM2 - pass="$6" # May be empty to seal with no password - tpm_password="$7" # Owner password - will prompt if needed and not empty - # TPM Owner Password is always needed for TPM2. + sealed_size="$5" # Not used for TPM2 + pass="$6" # May be empty to seal with no password + tpm_passphrase="$7" # Owner passphrase - will prompt if needed and not empty + # TPM Owner Passphrase is always needed for TPM2. # bail early if the TPM hasn't been reset and we lack a primary handle. if [ ! -f "$PRIMARY_HANDLE_FILE" ]; then @@ -524,28 +542,33 @@ tpm2_seal() { DO_WITH_DEBUG tpm2 load -Q -C "$PRIMARY_HANDLE_FILE" \ -u "$SECRET_DIR/$bname.pub" -r "$SECRET_DIR/$bname.priv" \ -c "$SECRET_DIR/$bname.seal.ctx" - # Retry loop: evictcontrol requires the TPM owner password; allow the - # user to re-enter it if it is wrong rather than dying immediately. - local evict_attempts=0 + + local attempt=0 while true; do - evict_attempts=$((evict_attempts + 1)) + attempt=$((attempt + 1)) prompt_tpm_owner_password - # remove possible data occupying this handle (failure is expected when - # the handle doesn't exist yet, so ignore errors) - DO_WITH_DEBUG --mask-position 6 tpm2 evictcontrol -Q -C o \ - -P "$(tpm2_password_hex "$tpm_owner_password")" \ - -c "$handle" >/dev/null 2>&1 || true + tpm_owner_passphrase="$tpm_owner_passphrase" + tmp_err_file="$(mktemp)" + tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_passphrase")" \ + -c "$handle" 2>"$tmp_err_file" || true if DO_WITH_DEBUG --mask-position 6 \ - tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_password")" \ - -c "$SECRET_DIR/$bname.seal.ctx" "$handle" >/dev/null 2>&1; then - break + tpm2 evictcontrol -Q -C o -P "$(tpm2_password_hex "$tpm_owner_passphrase")" \ + -c "$SECRET_DIR/$bname.seal.ctx" "$handle" 2>"$tmp_err_file"; then + rm -f "$tmp_err_file" + return 0 fi - DEBUG "tpm2_seal: evictcontrol failed (attempt $evict_attempts), wiping cached TPM owner password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password 2>/dev/null - if [ "$evict_attempts" -ge 3 ]; then - DIE "Unable to write sealed secret to TPM NVRAM after $evict_attempts attempts" + tmp_err_content="$(cat "$tmp_err_file")" + rm -f "$tmp_err_file" + DEBUG "Failed attempt $attempt to write sealed secret to NVRAM from tpm2_seal. Stderr: $tmp_err_content" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + if echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission'; then + if [ "$attempt" -ge 3 ]; then + DIE "Unable to write sealed secret to TPM NVRAM after 3 attempts. Reset the TPM and try again." + fi + WARN "Failed to write sealed secret (bad passphrase?). Retrying..." + else + DIE "Unable to write sealed secret to TPM NVRAM. Reset the TPM and try again." fi - WARN "TPM owner password incorrect or TPM error - please try again (attempt $evict_attempts of 3)" done } tpm1_seal() { @@ -556,17 +579,17 @@ tpm1_seal() { pcrl="$3" #0,1,2,3,4,5,6,7 (does not include algorithm prefix) pcrf="$4" sealed_size="$5" - pass="$6" # May be empty to seal with no password - tpm_owner_password="$7" # Owner password - will prompt if needed and not empty + pass="$6" # May be empty to seal with no password + tpm_owner_passphrase="$7" # Owner passphrase - will prompt if needed and not empty sealed_file="$SECRET_DIR/tpm1_seal_sealed.bin" at_exit cleanup_shred "$sealed_file" POLICY_ARGS=() - DEBUG "tpm1_seal arguments: file=$file index=$index pcrl=$pcrl pcrf=$pcrf sealed_size=$sealed_size pass=$(mask_param "$pass") tpm_owner_password=$(mask_param "$tpm_owner_password")" + DEBUG "tpm1_seal arguments: file=$file index=$index pcrl=$pcrl pcrf=$pcrf sealed_size=$sealed_size pass=$(mask_param "$pass") tpm_owner_passphrase=$(mask_param "$tpm_owner_passphrase")" - # If a password was given, add it to the policy arguments + # If a passphrase was given, add it to the policy arguments if [ "$pass" ]; then POLICY_ARGS+=(-pwdd "$pass") fi @@ -589,53 +612,61 @@ tpm1_seal() { "${POLICY_ARGS[@]}" DEBUG "tpm1_seal: sealed blob created at $sealed_file (size=$(wc -c <"$sealed_file" 2>/dev/null || echo 0) bytes), target nv index=$index" - # try it without the TPM Owner Password first - tmp_err_write="$(mktemp)" - if ! tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write"; then - while IFS= read -r line; do - DEBUG "tpm1_seal nv_writevalue(pre-define) stderr: $line" - done <"$tmp_err_write" - if grep -qi 'illegal index' "$tmp_err_write"; then - DEBUG "tpm1_seal: nv index $index is not defined yet (Illegal index); attempting nv_definespace" + local attempt=0 + while true; do + attempt=$((attempt + 1)) + tmp_err_write="$(mktemp)" + if tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write"; then + rm -f "$tmp_err_write" + return 0 fi + tmp_err_content="$(cat "$tmp_err_write")" rm -f "$tmp_err_write" - # to create an nvram space we need the TPM Owner Password - # and the TPM physical presence must be asserted. - # - # The permissions are 0 since there is nothing special - # about the sealed file + if ! echo "$tmp_err_content" | grep -qi 'illegal index'; then + DEBUG "tpm1_seal nv_writevalue(pre-define) stderr: $tmp_err_content" + DEBUG "Failed to write sealed secret to NVRAM from tpm1_seal (unexpected error). Wiping /tmp/secret/tpm_owner_passphrase" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + DIE "Unable to write sealed secret to TPM NVRAM" + fi + DEBUG "tpm1_seal: nv index $index is not defined yet (Illegal index); attempting nv_definespace" + tpm physicalpresence -s || { WARN "Unable to assert physical presence" } prompt_tpm_owner_password - + tpm_owner_passphrase="$(cat /tmp/secret/tpm_owner_passphrase)" tmp_err_define="$(mktemp)" if ! DO_WITH_DEBUG --mask-position 7 tpm nv_definespace -in "$index" -sz "$sealed_size" \ - -pwdo "$tpm_owner_password" -per 0 2>"$tmp_err_define"; then - while IFS= read -r line; do - DEBUG "tpm1_seal nv_definespace stderr: $line" - done <"$tmp_err_define" + -pwdo "$tpm_owner_passphrase" -per 0 2>"$tmp_err_define"; then + tmp_err_define_content="$(cat "$tmp_err_define")" + rm -f "$tmp_err_define" + DEBUG "tpm1_seal nv_definespace stderr: $tmp_err_define_content" WARN "Unable to define TPM NVRAM space; trying anyway" + else + rm -f "$tmp_err_define" fi - rm -f "$tmp_err_define" tmp_err_write_after_define="$(mktemp)" - tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write_after_define" || - { - while IFS= read -r line; do - DEBUG "tpm1_seal nv_writevalue(post-define) stderr: $line" - done <"$tmp_err_write_after_define" - rm -f "$tmp_err_write_after_define" - DEBUG "Failed to write sealed secret to NVRAM from tpm1_seal. Wiping /tmp/secret/tpm_owner_password" - shred -n 10 -z -u /tmp/secret/tpm_owner_password - DIE "Unable to write sealed secret to TPM NVRAM" - } + if tpm nv_writevalue -in "$index" -if "$sealed_file" 2>"$tmp_err_write_after_define"; then + rm -f "$tmp_err_write_after_define" + return 0 + fi + tmp_err_content="$(cat "$tmp_err_write_after_define")" rm -f "$tmp_err_write_after_define" - else - rm -f "$tmp_err_write" - fi + DEBUG "tpm1_seal nv_writevalue(post-define) stderr: $tmp_err_content" + DEBUG "Failed attempt $attempt to write sealed secret to NVRAM from tpm1_seal. Wiping /tmp/secret/tpm_owner_passphrase" + shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase + if echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission'; then + if [ "$attempt" -ge 3 ]; then + DIE "Unable to write sealed secret to TPM NVRAM after 3 attempts" + fi + WARN "Failed to write sealed secret (bad passphrase?). Retrying..." + else + DIE "Unable to write sealed secret to TPM NVRAM" + fi + done } # Unseal a file sealed by tpm2_seal. The PCR list must be provided, the @@ -766,25 +797,25 @@ tpm1_unseal() { fi } -# cache_owner_password -# Store the TPM owner password in SECRET_DIR for the current boot session. -# The original callers wrote the password to a file directly; the helper +# cache_owner_passphrase +# Store the TPM owner passphrase in SECRET_DIR for the current boot session. +# The original callers wrote the passphrase to a file directly; the helper # provides identical behaviour and keeps the code DRY. cache_owner_password() { TRACE_FUNC mkdir -p "$SECRET_DIR" - DEBUG "Caching TPM Owner Password to $SECRET_DIR/tpm_owner_password" - printf '%s' "$1" >"$SECRET_DIR/tpm_owner_password" + DEBUG "Caching TPM Owner Passphrase to $SECRET_DIR/tpm_owner_passphrase" + printf '%s' "$1" >"$SECRET_DIR/tpm_owner_passphrase" } # Reset a TPM2 device for Heads. (Previous versions in origin/master put the # comment about caching directly in this function, which is preserved below.) tpm2_reset() { TRACE_FUNC - tpm_owner_password="$1" - # output TPM Owner Password to a file to be reused in this boot session until recovery shell/reboot + tpm_owner_passphrase="$1" + # output TPM Owner Passphrase to a file to be reused in this boot session until recovery shell/reboot # (using cache_owner_password() to avoid duplicating the write logic) - cache_owner_password "$tpm_owner_password" + cache_owner_password "$tpm_owner_passphrase" # 1. Ensure TPM2_Clear is allowed: clear disableClear via platform hierarchy. # This makes future clears (Owner/Lockout) possible and avoids a 'no clear' stuck state. @@ -801,31 +832,31 @@ tpm2_reset() { fi # 3. Re-own the TPM for Heads: set new owner and endorsement auth. - # don't echo the owner password in debug logs + # don't echo the owner passphrase in debug logs # hide the owner auth hex (argument index 4) - if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c owner "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c owner "$(tpm2_password_hex "$tpm_owner_passphrase")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to set owner auth" return 1 fi - # mask endorsement password too (argument index 4) - if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c endorsement "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + # mask endorsement passphrase too (argument index 4) + if ! DO_WITH_DEBUG --mask-position 4 tpm2 changeauth -c endorsement "$(tpm2_password_hex "$tpm_owner_passphrase")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to set endorsement auth" return 1 fi # 4. Create and persist Heads primary key. - # hide owner password during primary creation (argument index 11) + # hide owner passphrase during primary creation (argument index 11) if ! DO_WITH_DEBUG --mask-position 11 tpm2 createprimary -C owner -g sha256 -G "${CONFIG_PRIMARY_KEY_TYPE:-rsa}" \ -c "$SECRET_DIR/primary.ctx" \ - -P "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + -P "$(tpm2_password_hex "$tpm_owner_passphrase")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to create primary" return 1 fi - # and hide password when evicting the primary handle (argument index 8) + # and hide passphrase when evicting the primary handle (argument index 8) if ! DO_WITH_DEBUG --mask-position 8 tpm2 evictcontrol -C owner -c "$SECRET_DIR/primary.ctx" "$PRIMARY_HANDLE" \ - -P "$(tpm2_password_hex "$tpm_owner_password")" >/dev/null 2>&1; then + -P "$(tpm2_password_hex "$tpm_owner_passphrase")" >/dev/null 2>&1; then LOG "tpm2_reset: unable to persist primary" shred -u "$SECRET_DIR/primary.ctx" >/dev/null 2>&1 return 1 @@ -840,7 +871,7 @@ tpm2_reset() { # * --max-tries=10: Allow 10 failures before lockout. This allows the # user to quickly "burst" 10 failures without significantly impacting # the rate allowed for a dictionary attacker. - # Most TPM2 flows ask for the TPM Owner Password 2-4 times, so this allows + # Most TPM2 flows ask for the TPM Owner Passphrase 2-4 times, so this allows # a handful of mistypes and some headroom for an expected unseal # failure if firmware is updated. # Remember that an auth failure is also counted any time an unclean @@ -856,8 +887,8 @@ tpm2_reset() { --max-tries=10 \ --recovery-time=3600 \ --lockout-recovery-time=0 \ - --auth="session:$ENC_SESSION_FILE" >/dev/null 2>&1 \ - || LOG "tpm2_reset: unable to set dictionary lockout parameters" + --auth="session:$ENC_SESSION_FILE" >/dev/null 2>&1 || + LOG "tpm2_reset: unable to set dictionary lockout parameters" # 6. Set a random DA lockout password so DA reset requires another TPM reset. # The default lockout password is empty, so we must set this, and we @@ -869,10 +900,10 @@ tpm2_reset() { tpm1_reset() { TRACE_FUNC - tpm_owner_password="$1" - # output tpm_owner_password to a file to be reused in this boot session until recovery shell/reboot + tpm_owner_passphrase="$1" + # output tpm_owner_passphrase to a file to be reused in this boot session until recovery shell/reboot # (using cache_owner_password() under the hood) - cache_owner_password "$tpm_owner_password" + cache_owner_password "$tpm_owner_passphrase" # 1. Request physical presence and enable TPM. DO_WITH_DEBUG tpm physicalpresence -s >/dev/null 2>&1 || LOG "tpm1_reset: unable to set physical presence" @@ -890,8 +921,8 @@ tpm1_reset() { # Re-enable after clear (some platforms require this). DO_WITH_DEBUG tpm physicalenable >/dev/null 2>&1 || LOG "tpm1_reset: unable to physicalenable after clear" - # 3. Take ownership with the new TPM owner password. - if ! DO_WITH_DEBUG --mask-position 3 tpm takeown -pwdo "$tpm_owner_password" >/dev/null 2>&1; then + # 3. Take ownership with the new TPM owner passphrase. + if ! DO_WITH_DEBUG --mask-position 3 tpm takeown -pwdo "$tpm_owner_passphrase" >/dev/null 2>&1; then LOG "tpm1_reset: tpm takeown failed after forceclear" return 1 fi @@ -998,7 +1029,7 @@ if [ "$CONFIG_TPM2_TOOLS" != "y" ]; then *) # Keep passthrough raw here: callers decide whether to wrap with # DO_WITH_DEBUG (and --mask-position when passing secrets). - DEBUG "Direct translation from tpmr.sh to tpm1 call" + DEBUG "Direct translation from tpmr.sh to tpm1 call" exec tpm "$@" ;; esac diff --git a/initrd/etc/functions.sh b/initrd/etc/functions.sh index 3217e3490..b3c9ff333 100644 --- a/initrd/etc/functions.sh +++ b/initrd/etc/functions.sh @@ -506,28 +506,28 @@ detect_usb_security_dongle_branding() { # Check NK3 (42b2) before the broader 20a0 vendor match if echo "$lsusb_out" | grep -q "20a0:42b2"; then DEBUG "Detected Nitrokey 3 (20a0:42b2)" - echo "Nitrokey 3" + export DONGLE_BRAND="Nitrokey 3" elif echo "$lsusb_out" | grep -q "20a0:42d4"; then DEBUG "Detected Canokey QEMU (20a0:42d4)" - echo "Canokey" + export DONGLE_BRAND="Canokey" elif echo "$lsusb_out" | grep -q "20a0:4108"; then DEBUG "Detected Nitrokey Pro (20a0:4108)" - echo "Nitrokey Pro" + export DONGLE_BRAND="Nitrokey Pro" elif echo "$lsusb_out" | grep -q "20a0:4109"; then DEBUG "Detected Nitrokey Storage (20a0:4109)" - echo "Nitrokey Storage" + export DONGLE_BRAND="Nitrokey Storage" elif echo "$lsusb_out" | grep -q "316d:4c4b"; then DEBUG "Detected Librem Key (316d:4c4b)" - echo "Librem Key" + export DONGLE_BRAND="Librem Key" elif echo "$lsusb_out" | grep -q "16d0:21dc"; then DEBUG "Detected Canokey (16d0:21dc)" - echo "Canokey" + export DONGLE_BRAND="Canokey" elif echo "$lsusb_out" | grep -q "1050:"; then DEBUG "Detected Yubikey (1050:*)" - echo "Yubikey" + export DONGLE_BRAND="Yubikey" else DEBUG "No known USB Security dongle detected" - echo "USB Security dongle" + export DONGLE_BRAND="USB Security dongle" fi } @@ -774,7 +774,7 @@ cache_gpg_signing_pin() { admin_pin_retries=$(echo "$pin_retry_counters" | awk '{print $3}') # Re-detect dongle branding after card is detected (may have been too early in gui-init.sh) - export DONGLE_BRAND="$(detect_usb_security_dongle_branding)" + detect_usb_security_dongle_branding echo >/dev/console 2>/dev/null STATUS "GPG User PIN retries remaining: $(pin_color "$user_pin_retries")${user_pin_retries}\033[0m" @@ -795,7 +795,7 @@ cache_gpg_signing_pin() { while [ "$sc_pin_tries" -lt 3 ]; do sc_pin_tries=$((sc_pin_tries + 1)) while [ -z "$sc_user_pin" ]; do - INPUT "Enter $DONGLE_BRAND User PIN:" -r -s sc_user_pin + INPUT "Enter $DONGLE_BRAND GPG User PIN:" -r -s sc_user_pin done if gpg --pinentry-mode=loopback \ --passphrase-file <(printf '%s' "$sc_user_pin") \ @@ -1608,48 +1608,48 @@ list_usb_storage() { done } -# Prompt for a TPM Owner Password if it is not already cached in /tmp/secret/tpm_owner_password. +# Prompt for a TPM Owner Passphrase if it is not already cached in /tmp/secret/tpm_owner_passphrase. # Sets tpm_owner_passphrase variable reused in flow, and cache file used until recovery shell is accessed. -# Tools should optionally accept a TPM password on the command line, since some flows need +# Tools should optionally accept a TPM passphrase on the command line, since some flows need # it multiple times and only one prompt is ideal. prompt_tpm_owner_password() { TRACE_FUNC - if [ -s /tmp/secret/tpm_owner_password ]; then - DEBUG "/tmp/secret/tpm_owner_password already cached in file. Reusing" - tpm_owner_passphrase=$(cat /tmp/secret/tpm_owner_password) + if [ -s /tmp/secret/tpm_owner_passphrase ]; then + DEBUG "/tmp/secret/tpm_owner_passphrase already cached in file. Reusing" + tpm_owner_passphrase=$(cat /tmp/secret/tpm_owner_passphrase) return 0 fi - INPUT "TPM Owner Password:" -r -s tpm_owner_passphrase + INPUT "TPM Owner Passphrase:" -r -s tpm_owner_passphrase - # Cache the password externally to be reused by who needs it - DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password" + # Cache the passphrase externally to be reused by who needs it + DEBUG "Caching TPM Owner Passphrase to /tmp/secret/tpm_owner_passphrase" mkdir -p /tmp/secret || DIE "Unable to create /tmp/secret" - echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_password || DIE "Unable to cache TPM owner_password under /tmp/secret/tpm_owner_password" + echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_passphrase || DIE "Unable to cache TPM owner_passphrase under /tmp/secret/tpm_owner_passphrase" } -# Prompt for a new TPM Owner Password when resetting the TPM. -# Returned in tpm_owner_passpword and cached under /tpm/secret/tpm_owner_password -# The password must be 1-32 characters and must be entered twice, +# Prompt for a new TPM Owner Passphrase when resetting the TPM. +# Returned in tpm_owner_passphrase and cached under /tmp/secret/tpm_owner_passphrase +# The passphrase must be 1-32 characters and must be entered twice, # the script will loop until this is met. prompt_new_owner_password() { TRACE_FUNC - local tpm_owner_password2 + local tpm_owner_passphrase2 tpm_owner_passphrase=1 - tpm_owner_password2=2 - while [ "$tpm_owner_passphrase" != "$tpm_owner_password2" ] || [ "${#tpm_owner_passphrase}" -gt 32 ] || [ -z "$tpm_owner_passphrase" ]; do - INPUT "New TPM Owner Password (2 words suggested, 1-32 characters max):" -r -s tpm_owner_passphrase - INPUT "Repeat chosen TPM Owner Password:" -r -s tpm_owner_password2 - if [ "$tpm_owner_passphrase" != "$tpm_owner_password2" ]; then + tpm_owner_passphrase2=2 + while [ "$tpm_owner_passphrase" != "$tpm_owner_passphrase2" ] || [ "${#tpm_owner_passphrase}" -gt 32 ] || [ -z "$tpm_owner_passphrase" ]; do + INPUT "New TPM Owner Passphrase (2 words suggested, 1-32 characters max):" -r -s tpm_owner_passphrase + INPUT "Repeat chosen TPM Owner Passphrase:" -r -s tpm_owner_passphrase2 + if [ "$tpm_owner_passphrase" != "$tpm_owner_passphrase2" ]; then WARN "Passphrases entered do not match. Try again!" fi done - # Cache the password externally to be reused by who needs it - DEBUG "Caching TPM Owner Password to /tmp/secret/tpm_owner_password" + # Cache the passphrase externally to be reused by who needs it + DEBUG "Caching TPM Owner Passphrase to /tmp/secret/tpm_owner_passphrase" mkdir -p /tmp/secret || DIE "Unable to create /tmp/secret" - echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_password || DIE "Unable to cache TPM password under /tmp/secret/tpm_owner_password" + echo -n "$tpm_owner_passphrase" >/tmp/secret/tpm_owner_passphrase || DIE "Unable to cache TPM passphrase under /tmp/secret/tpm_owner_passphrase" } check_tpm_counter() { @@ -1657,7 +1657,7 @@ check_tpm_counter() { TRACE_FUNC LABEL=${2:-3135106223} - tpm_password="$3" + tpm_passphrase="$3" # if the /boot.hashes file already exists, read the TPM counter ID # from it. if [ -r "$1" ]; then @@ -1666,9 +1666,9 @@ check_tpm_counter() { DEBUG "Extracted TPM_COUNTER: '$TPM_COUNTER' from $1" else DEBUG "$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" + # Warn user: TPM Owner Passphrase is required to create a new TPM counter + if [ ! -s /tmp/secret/tpm_owner_passphrase ]; then + WARN "TPM Owner Passphrase is required to create a new TPM counter for /boot content rollback prevention" fi # attempt to make a new counter, capturing any stderr for debugging @@ -1676,8 +1676,8 @@ check_tpm_counter() { # run it, then record the exit status explicitly; the '!' operator # cannot be used because it would hide the real return code. tpmr.sh counter_create \ - -pwdc "${tpm_password:-}" \ - -la $LABEL \ + -pwdc "${tpm_passphrase:-}" \ + -la "$LABEL" \ >/tmp/counter 2> >(tee >(SINK_LOG "tpm counter_create stderr") >&2) local rc=$? if [ $rc -ne 0 ]; then @@ -1829,29 +1829,29 @@ read_tpm_counter() { increment_tpm_counter() { TRACE_FUNC - local counter_id counter_present tpm_password increment_ok + local counter_id counter_present tpm_passphrase increment_ok counter_id="$(echo "$1" | tr -d '\n')" - tpm_password="$2" + tpm_passphrase="$2" counter_present="n" increment_ok="n" local reset_required_marker="/tmp/secret/rollback_reset_required" - # Prefer explicit password, otherwise reuse cached TPM owner password. - if [ -z "$tpm_password" ] && [ -s /tmp/secret/tpm_owner_password ]; then - tpm_password="$(cat /tmp/secret/tpm_owner_password)" - DEBUG "increment_tpm_counter: using cached TPM owner password" + # Prefer explicit passphrase, otherwise reuse cached TPM owner passphrase. + if [ -z "$tpm_passphrase" ] && [ -s /tmp/secret/tpm_owner_passphrase ]; then + tpm_passphrase="$(cat /tmp/secret/tpm_owner_passphrase)" + DEBUG "increment_tpm_counter: using cached TPM owner passphrase" fi # TPM1 counter_increment requires owner auth in practice on this path. - # origin/master typically reached this with cached owner password already set, + # origin/master typically reached this with cached owner passphrase already set, # but the newer reseal/update flows can call this later in the session after # that cache is absent. Prompt once and cache to avoid empty -pwdc failures. - if [ "$CONFIG_TPM2_TOOLS" != "y" ] && [ -z "$tpm_password" ]; then - WARN "TPM Owner Password is required to update rollback counter before signing updated boot hashes." - DEBUG "increment_tpm_counter: TPM1 path has no cached/provided owner password; prompting now" + if [ "$CONFIG_TPM2_TOOLS" != "y" ] && [ -z "$tpm_passphrase" ]; then + WARN "TPM Owner Passphrase is required to update rollback counter before signing updated boot hashes." + DEBUG "increment_tpm_counter: TPM1 path has no cached/provided owner passphrase; prompting now" prompt_tpm_owner_password - tpm_password="$tpm_owner_passphrase" - DEBUG "increment_tpm_counter: TPM1 owner password obtained and cached" + tpm_passphrase="$tpm_owner_passphrase" + DEBUG "increment_tpm_counter: TPM1 owner passphrase obtained and cached" fi # Check if counter exists by reading it first @@ -1887,12 +1887,12 @@ increment_tpm_counter() { tee /tmp/counter-"$counter_id" >/dev/null ); then increment_ok="y" - elif [ -n "$tpm_password" ]; then + elif [ -n "$tpm_passphrase" ]; then DEBUG "increment_tpm_counter: TPM2 index-auth increment failed; trying owner-auth fallback" if ( set -o pipefail DO_WITH_DEBUG --mask-position 5 \ - tpmr.sh counter_increment -ix "$counter_id" -pwdc "${tpm_password}" \ + tpmr.sh counter_increment -ix "$counter_id" -pwdc "${tpm_passphrase}" \ 2> >(SINK_LOG "tpm counter_increment stderr") | tee /tmp/counter-"$counter_id" >/dev/null ); then @@ -1904,7 +1904,7 @@ increment_tpm_counter() { if ( set -o pipefail DO_WITH_DEBUG --mask-position 5 \ - tpmr.sh counter_increment -ix "$counter_id" -pwdc "${tpm_password:-}" \ + tpmr.sh counter_increment -ix "$counter_id" -pwdc "${tpm_passphrase:-}" \ 2> >(SINK_LOG "tpm counter_increment stderr") | tee /tmp/counter-"$counter_id" >/dev/null ); then @@ -1927,7 +1927,7 @@ increment_tpm_counter() { if ( set -o pipefail DO_WITH_DEBUG --mask-position 3 \ - tpmr.sh counter_create -pwdc "${tpm_password:-}" -la 3135106223 \ + tpmr.sh counter_create -pwdc "${tpm_passphrase:-}" -la 3135106223 \ 2> >(tee >(SINK_LOG "tpm counter_create stderr") >&2) | tee /tmp/new-counter >/dev/null ); then