UX improvements: TPM reseal (HOTP/TOTP/DUK) adds integrity report; detects disk/tpm swap and guide user into action, add terminal colors and guidance! Reduced quiet noise#2068
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves Heads’ TPM reseal UX by adding an integrity “gate” (TOTP/HOTP + /boot verification) and better detection/handling of TPM/disk swap or rollback-counter inconsistencies, plus some QEMU-focused debugging/documentation updates.
Changes:
- Add measured integrity reporting + discrepancy investigation flows, and integrate them into reseal/reset paths in the GUI.
- Improve TPM rollback-counter handling (preflight validation, clearer error guidance, better prompt visibility).
- Replace fdisk-based disk display with a sysfs-based helper and add QEMU troubleshooting/debug tips (including TPM2 pcap capture).
Reviewed changes
Copilot reviewed 8 out of 20 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| targets/qemu.md | Adds QEMU troubleshooting notes (Canokey state reuse, TPM2 pcap capture). |
| initrd/etc/gui_functions | Adds integrity report + investigation UI helpers; system info now uses disk_info_sysfs. |
| initrd/etc/functions | Adds trace stack, rollback-counter preflight helpers, sysfs disk info helper, and multiple TPM/boot-device related adjustments. |
| initrd/bin/unseal-totp | Improves TPM2 primary-handle error handling and adds nonfatal mode support. |
| initrd/bin/unseal-hotp | Improves TPM2 primary-handle + rollback-state-aware error handling and adds nonfatal mode support. |
| initrd/bin/tpmr | Improves TPM2 counter increment auth handling, counter-create UX, and TPM2 seal/unseal messaging. |
| initrd/bin/seal-totp | Adds TPM2 primary-handle precheck + clearer sealing failure guidance. |
| initrd/bin/root-hashes-gui.sh | Improves tracing/debugging and adds more flexible LVM LV selection/cleanup. |
| initrd/bin/oem-system-info-xx30 | Switches disk listing to disk_info_sysfs to avoid fdisk/busybox limitations. |
| initrd/bin/oem-factory-reset | Adjusts TPM counter increment handling and removes duplicated integrity report implementation. |
| initrd/bin/kexec-sign-config | Changes TPM counter increment handling and adds a pre-check for empty GPG keyring; modifies signing pipeline. |
| initrd/bin/kexec-select-boot | Hard-fails on TPM2 primary handle hash mismatch with a stronger warning. |
| initrd/bin/kexec-seal-key | Tweaks passphrase prompts/formatting for improved UX. |
| initrd/bin/gui-init | Adds integrity gate + rollback-counter preflight UX and integrates investigation/report flows. |
| boards/qemu-coreboot-fbwhiptail-tpm2/qemu-coreboot-fbwhiptail-tpm2.config | Documents TPM2 pcap capture option in board config. |
| boards/qemu-coreboot-fbwhiptail-tpm2-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-prod_quiet.config | Adds a new “prod_quiet” QEMU TPM2 board config. |
| boards/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm2-hotp-prod_quiet.config | Adjusts board name and minor formatting. |
| boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config | Adds a new “prod_quiet” QEMU TPM1 board config. |
| boards/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-hotp-prod_quiet.config | Adjusts board name. |
| .gitignore | Ignores *.asc files. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
3f855b8 to
3f2fe25
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 20 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
3f2fe25 to
a1e063a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 19 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
boards/qemu-coreboot-fbwhiptail-tpm1-prod_quiet/qemu-coreboot-fbwhiptail-tpm1-prod_quiet.config
Outdated
Show resolved
Hide resolved
b905930 to
8be0849
Compare
8be0849 to
5b6ab4f
Compare
6a1c15a to
b698631
Compare
|
Will merge in the next days :) |
b698631 to
cfa3585
Compare
…ID:PID
Add detect_hotpkey_branding() to etc/functions. It matches the connected
device by USB VID:PID (source: hotp-verification/src/device.c and
targets/qemu.mk) and returns the specific model name:
20a0:42b2 Nitrokey 3 (3A Mini / 3A NFC / 3C NFC — all share this PID)
20a0:4108 Nitrokey Pro (Pro and Pro 2 share the same PID)
20a0:4109 Nitrokey Storage (Storage and Storage 2 share the same PID)
316d:4c4b Librem Key
Previously the VID-only check (grep "20a0:") matched all Nitrokey devices
and returned the generic "Nitrokey" label regardless of model.
seal-hotpkey:
- Replace the VID-only first-setup detection block with a case statement
that reuses specific stored branding ("Nitrokey Pro", "Nitrokey Storage",
"Nitrokey 3", "Librem Key") and calls detect_hotpkey_branding() only
when the stored value is absent or a legacy generic name
- Replace lsusb NK3 VID:PID check with [ "$HOTPKEY_BRANDING" = "Nitrokey 3" ]
for the PIN counter and physical-touch prompts (avoids redundant lsusb call)
- Provide device-specific DIE messages: Nitrokey directs to Nitrokey App 2
or Nitrokey support; Librem Key directs to Purism support
gui-init:
- After loading branding from /boot/kexec_hotp_key, fall back to
detect_hotpkey_branding() when the stored value is absent or generic,
so menus show the actual model name even for existing installations
dongle-versions:
- Rename HOTPKEY_REPROGRAM_BELOW to HOTPKEY_EXTERNAL_REPROGRAM_BELOW to
make the variable's meaning self-documenting
- Clarify that HOTPKEY_EXTERNAL_REPROGRAM_BELOW applies to Nitrokey Pro /
Pro 2 only: Librem Key is never self-upgradeable (no nitropy path exists
regardless of firmware version); Nitrokey Storage has a separate firmware
codebase and is not subject to this threshold
- Add HOTPKEY_STORAGE_MIN_VER="" placeholder; Storage minimum firmware
version is not yet confirmed upstream (TODO)
- Add VID:PID annotations to each device section header
hotpkey_fw_display():
- Librem Key: always NOTE with firmware version and advisory to contact
Purism support — remove the STATUS_OK green path that implied the
firmware was acceptable/upgradeable when Librem Key has no self-upgrade
path at any version
- Nitrokey Storage: show STATUS_OK with firmware version; skip min-ver
comparison until HOTPKEY_STORAGE_MIN_VER is confirmed upstream
- Nitrokey Pro / Pro 2: retain green/yellow/red logic; update upgrade
command from "nitropy pro" to "nitropy nk pro"
doc/ux-patterns.md:
- Update color-coding table to include device column and Storage/Librem
Key rows with their specific (non-comparative) display behaviour
- Update section heading and add note clarifying which devices
HOTPKEY_EXTERNAL_REPROGRAM_BELOW applies to
Signed-off-by: Thierry Laurion <insurgo@riseup.net>
oem-factory-reset: - Detect HOTPKEY_BRANDING in usb_security_token_capabilities_check() and export it; replace all remaining lsusb VID:PID checks with HOTPKEY_BRANDING comparisons (NK3 42b2, Storage 4109, NK3 summary section) - reset_nk3_secret_app: WARN -> STATUS; add STATUS_OK on success - gpg_key_factory_reset: add STATUS/STATUS_OK around Nitrokey Storage AES reset, forcesig toggle, and key-attr (p256/RSA); RSA key-attr STATUS includes device name; patience NOTE is RSA-only (NK3/p256 is fast) - generate_OEM_gpg_keys: add RSA-conditional patience NOTE naming the device; add STATUS_OK after success - keytocard_subkeys_to_smartcard: add STATUS before gpg --card-status; RSA-conditional patience NOTE; STATUS_OK after subkeys moved - export_master_key_subkeys: DEBUG -> STATUS; add STATUS_OK - export_public_key_to_thumbdrive_public_partition: add STATUS/STATUS_OK - gpg_key_change_pin: remove redundant DEBUG (call sites already have STATUS); add STATUS_OK at each call site - generate_checksums: DEBUG "Generating hashes" -> STATUS; STATUS_OK after hashes generated and after /boot signed and verified - Main flow: RSA-conditional patience NOTE naming the device before factory reset + key generation block gui-init: - reset_tpm: add STATUS "Resetting TPM" before tpmr reset - generate_totp_hotp: add STATUS_OK after each success path kexec-select-boot: - gui_menu path: replace string accumulation + tr " " "_" with a bash array so boot option names with spaces display correctly in whiptail menus Signed-off-by: Thierry Laurion <insurgo@riseup.net>
When using the backup thumb drive for signing, cache_gpg_signing_pin previously called mount-usb without --device, which listed all USB partitions and required the user to manually identify and select the LUKS-encrypted private partition. The backup created by oem-factory-reset always has a known layout: partition 1 is the LUKS-encrypted private container and partition 2 is the public exFAT partition. Replace the manual partition selection with auto-detection using list_usb_storage disks (whole-device list) + cryptsetup isLuks scan: 1. List USB disks (whole devices, not partitions) 2. Auto-select if only one disk; prompt to choose disk if multiple 3. Scan up to 4 partitions with cryptsetup isLuks to find the LUKS one 4. Pass --device <detected_partition> to mount-usb The user now only picks the USB disk (not the partition), and only when multiple drives are connected; single-drive attachment requires no selection at all. Also remove a now-redundant duplicate read of /tmp/luks_container_size_percent after the selection loop in select_thumb_drive_for_key_material (value already captured inside). Signed-off-by: Thierry Laurion <insurgo@riseup.net>
DO_WITH_DEBUG pipes stdout through tee for debug logging, making stdout fully buffered. The numbered option list (echo to stdout) accumulated in the pipe buffer while INPUT's prompt wrote directly to /dev/console via HEADS_TTY, causing the INPUT prompt to appear before the last option(s) flushed. Write the option list entries directly to /dev/console (same device STATUS uses) so all output in the text path - header, option list, and INPUT prompt - goes through the same unbuffered fd with no ordering surprises. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
When DO_WITH_DEBUG wraps a call, stdout is piped through tee for debug
logging, making it fully buffered. INPUT writes directly to HEADS_TTY
(or stderr), bypassing that pipe. Any bare echo/printf to stdout in the
same call context arrives after the INPUT prompt, producing interleaved
output.
Apply the HEADS_TTY routing pattern (documented in doc/ux-patterns.md)
everywhere interactive numbered lists are printed:
kexec-select-boot: replace echo >/dev/console with
printf >"${HEADS_TTY:-/dev/stderr}" in the text-mode boot option list.
Also switch the whiptail path from string accumulation + tr to a bash
array so option names with spaces are preserved correctly.
mount-usb: same two fixes -- printf >"${HEADS_TTY:-/dev/stderr}" for the
text-mode disk list, and bash array for the whiptail path.
oem-factory-reset: remove two bare echo calls after INPUT -n 1 prompts;
INPUT already emits the trailing newline to HEADS_TTY, so the bare
echo was redundant and went to the wrong fd.
Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…or fw display report_integrity_measurements was passing the branding string from /boot/kexec_hotp_key to hotpkey_fw_display. That file is written at seal time and can be stale if a different dongle was used last, causing the wrong device name to be shown (e.g. Nitrokey Pro labelled as Librem Key). At the point hotpkey_fw_display is called the device is confirmed present (hotp_verification info just succeeded) and HOTPKEY_BRANDING is already exported from detect_hotpkey_branding(). Use it directly, with detect_hotpkey_branding() as a fallback in case it is unset. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…tp_key reads /boot/kexec_hotp_key stored the dongle branding string written at seal time. Reading it back is unreliable: the file is stale when a different dongle is used, causing wrong labels (e.g. Nitrokey Pro shown as Librem Key). Remove all reads of /boot/kexec_hotp_key for branding purposes and replace them with detect_hotpkey_branding() which queries lsusb VID:PID at runtime: - gui-init: replace file-read + case guard with a single detect_hotpkey_branding() call. - oem-factory-reset: same; always re-detect rather than inheriting a potentially stale exported value. - seal-hotpkey: remove file-read at startup and the case block that reused it; re-detect after the dongle is confirmed present via hotp_verification info; remove the write of branding to the file. - gui_functions: already fixed in previous commit to use HOTPKEY_BRANDING; included here to ensure the variable is always set by detect_hotpkey_branding() before report_integrity_measurements calls hotpkey_fw_display. The HOTP_KEY variable and its write are removed from seal-hotpkey since nothing reads the file any longer. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
detect_hotpkey_branding() calls lsusb, which requires USB to be enumerated. Both scripts were calling it before enable_usb, so lsusb returned nothing and the fallback 'HOTP USB Security dongle' was used for the rest of the session. Move the detect_hotpkey_branding() call to after enable_usb in both scripts so lsusb can see the device. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
cfa3585 to
899f2de
Compare
af5043b to
1b3c484
Compare
Add set_card_identity() to populate the card's Name-of-cardholder and
Login-data fields from the same GPG_USER_NAME/GPG_USER_MAIL values
entered during the re-ownership questionnaire. Both fields default
to OEM placeholders ("OEM Key", "oem-<timestamp>@example.com"); the
function is a no-op for those, so cards provisioned without custom
identity info are unaffected.
The two operations are issued in a single gpg --card-edit session
(admin PIN required once for `name`; `login` reuses the cached PIN).
Signed-off-by: Thierry Laurion <insurgo@riseup.net>
Consistently distinguish secret types across all user-facing prompts: - TPM Owner Password -> TPM Owner Passphrase (not a PIN; arbitrary length) - GPG Admin/User stay as "PIN" (OpenPGP card spec terminology) - LUKS secrets already used "passphrase" - Generic "password" in whiptail/INPUT prompts -> "passphrase" Also show a QR code at the start of the re-ownership questionnaire pointing to osresearch.net/Configuring-Keys which gives per-credential word-count recommendations (DRK: 6 words, TPM Owner: 2 words, GPG PIN: 2 words) and links to EFF Diceware. The TPM passphrase prompt now says "Diceware 2+ words" as a concrete suggestion. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…l, gpg Pull wiki content (osresearch.net) into the repo as doc/*.md files so documentation and code live together. Five files that had no local counterpart: - keys.md: all key/secret types, PCR map, LUKS derivation, unseal errors - configuring-keys.md: OEM Factory Reset / Re-Ownership step-by-step guide - prerequisites.md: USB dongle compatibility table, HOTP vs TPMTOTP, OS requirements - recovery-shell.md: limitations, common operations, post-recovery checklist - gpg.md: key generation paths, PIN management, full card reset, NK3 notes Terminology updated throughout to match current code: TPM Owner Passphrase (not password), GPG Admin/User PIN, Diceware references. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Thierry Laurion <insurgo@riseup.net>
Add GIT_TIMESTAMP (YYYYMMDD-HHMMSS from last commit) to all output filenames so ROM files sort chronologically in file managers and the build timestamp is unambiguous without parsing git describe output. Before: heads-x230-v0.2.1-42-g1234567.rom After: heads-x230-20260327-200305-v0.2.1-42-g1234567.rom Applies to .rom, -gpg-injected.rom, .bootblock, .zip, and linuxboot-.rom. Signed-off-by: Thierry Laurion <insurgo@riseup.net> doc, .github: document commit conventions, dev workflow, ROM filename in bug reports - doc/development.md: commit format (-S -s, imperative, component prefix), Co-Authored-By policy, doc/*.md vs heads-wiki split, build artifact quick reference, testing checklist, shell/UX coding conventions - bug report template: prompt reporters to include the exact ROM filename as the primary build identifier (encodes timestamp, branch, commit) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Thierry Laurion <insurgo@riseup.net>
…e_selector Dialogs in flash-gui.sh that display $PKG_FILE_DISPLAY (dev-build ROM filenames up to 100+ characters) use 0 0 so they auto-size to fit in newt (text backend). fbwhiptail (framebuffer backend) ignores height and width entirely and always auto-sizes from content regardless. Static-text dialogs retain fixed widths (0 80, 0 40/60/70) for a stable layout in newt; these are a no-op in fbwhiptail. file_selector now shows only the basename of each file in the menu label instead of the full /media/... path, preventing long paths from overflowing the menu column on both backends. Document the rule in doc/ux-patterns.md: use 0 0 only for dialogs with unpredictable-length dynamic content; use fixed widths for static text. Explain the fbwhiptail/newt backend behavior difference. Signed-off-by: Thierry Laurion <insurgo@riseup.net>
- functions: two WARN strings still said "TPM Owner Password" instead of "TPM Owner Passphrase" (create_tpm_counter and increment_tpm_counter) - configuring-keys.md: QR code description still referenced EFF Diceware wordlist; now correctly names osresearch.net/Configuring-Keys - ux-patterns.md: window sizing section still advised "0 80"; updated to "0 0" with explanation of fbwhiptail vs newt behaviour - build-artifacts.md: GIT_BRANCH table entry missing 30-char truncation note Signed-off-by: Thierry Laurion <insurgo@riseup.net>
Complete the passphrase/PIN terminology pass started in the previous commit: rename all internal variables, function names, cache file paths, and code comments that used "password" for TPM/LUKS secrets. - tpmr: tpm2_passphrase_hex, cache_owner_passphrase, tpm_passphrase var, /tmp/secret/tpm_owner_passphrase cache path - functions: prompt_tpm_owner_passphrase, prompt_new_owner_passphrase, WARN strings updated to "TPM Owner Passphrase" - kexec-seal-key: key_passphrase / key_passphrase2 variables - gui-init, oem-factory-reset: call sites updated to match new names - doc/faq.md, doc/keys.md: "disk password" -> "disk passphrase" Terminology: TPM/LUKS secrets are passphrases; GPG smartcard operations use PINs (OpenPGP spec). The --passwordbox whiptail flag is unchanged (external API). Signed-off-by: Thierry Laurion <insurgo@riseup.net>
d216954 to
7168e9c
Compare
Summary
Improve TPM/TOTP/HOTP recovery and reseal behavior by adding integrity-first
gating, clearer failure handling, and stronger rollback preflight checks.
run_lvmwrapper and switch runtime scripts to reduce harmless LVM noise*.asc) in.gitignoredoc/logging.md); all wrappers colorized with ANSI escape charactersDongle branding: always from USB VID:PID at runtime
/boot/kexec_hotp_key-- the stored branding file caused stale labels (Librem Key shown as Nitrokey and vice versa after dongle swaps)detect_hotpkey_branding()is now called afterenable_usbin every code path (gui-init,seal-hotpkey,oem-factory-reset,gui_functions)20a0:42b2->NK3,20a0:4108->NK Pro,20a0:4109->NK Storage,316d:4c4b->Librem KeyHEADS_TTY output routing
>"${HEADS_TTY:-/dev/stderr}"to matchINPUT's routing and avoid interleaving whenDO_WITH_DEBUGwraps stdout through a buffered tee pipekexec-select-bootandmount-usbtext-mode option listsMENU_OPTIONS+=()instead of string accumulation withtr " " "_"GPG PIN caching for signing
cache_gpg_signing_pin(called viaconfirm_gpg_card) validates the PIN with a test-sign before caching in/tmp/secret/gpg_pin(mode 600, tmpfs)--pinentry-mode=loopback --passphrase-file-- gpg-agent never calls pinentry for signingOpenPGP card identity fields from re-ownership questionnaire
set_card_identity()inoem-factory-resetsets the card Name of cardholder and Login data fields fromGPG_USER_NAMEandGPG_USER_MAILcollected during re-ownership"OEM Key",oem-*@example.com)gpg --card-editsession; admin PIN authenticated once forname, reused forloginPassphrase/PIN terminology
qrenc "https://www.eff.org/dice") shown at questionnaire start when user answers N to defaultsprompt_new_owner_passwordprompt updated to say "Diceware 2+ words"LUKS auto-detection on GPG key backup USB
cache_gpg_signing_pinno longer asks the user to manually select a LUKS partition; usescryptsetup isLuksauto-detection loopROM filename timestamps for dev builds
GIT_TIMESTAMP(YYYYMMDD-HHMMSSfrom last commit) embedded in output filenames for dev builds onlyheads-x230-v0.2.1.romheads-x230-20260327-200305-v0.2.1-42-g1234567-dirty.romflash-gui.shsorts ROM file list newest-first; file selector shows basename only in menuDocumentation
Five new
doc/*.mdfiles pulled from osresearch.net wiki into the repo:doc/keys.md- all key/secret types, PCR map, LUKS derivation, unseal error meaningsdoc/configuring-keys.md- OEM Factory Reset / Re-Ownership step-by-step guidedoc/prerequisites.md- USB dongle compatibility table, HOTP vs TPMTOTP, OS requirementsdoc/recovery-shell.md- entry methods, limitations, common operationsdoc/gpg.md- key generation paths, PIN counters, full card reset APDU sequence, NK3 notesWorkflow change
CC @wessel-novacustom -- There were reports of Heads not providing integrity checks prior to resealing TOTP/HOTP, so that users are confident about the state of /boot before resealing TOTP/HOTP/DUK (which resigns /boot content).
Normal workflow after upgrading firmware while /boot unchanged
Normal non-HOTP boot workflow requesting TPM DUK
Other corner cases
TPM reset from OS?
Similar to above, but pushes for TPM Reset since TPM reseal won't work


Replaced GPG key / mismatch from USB security dongle
Testing of these corner cases is still needed (too much time invested already).
Test plan
Tested: simulating or real firmware upgrade from master to this PR CI created ROM artifacts 03/11/2026
oearly at boot still generates a single random diceware passphrase shared for all security componentsAdditional test cases (v560tu)
set_card_identity(): oem-factory-reset with custom name + email; verifygpg --card-statusshows cardholder name and login dataset_card_identity(): login-only path (no name change); verify admin PIN prompted correctlyset_card_identity(): without custom info; verify silent no-opKnown limitations / follow-up
urland GPG keykeyserverpreference not yet set (requires network in initrd)Generated with Claude Code