From b20e4b7f46763afe8e35e4d2de37db9aa0152345 Mon Sep 17 00:00:00 2001 From: Thierry Laurion Date: Fri, 15 May 2026 21:03:45 -0400 Subject: [PATCH 1/5] initrd: fix TPM1 counter auth regression and defend lock cascade failure PR #2068 (tpm_reseal_ux-integrity_report-detect_disk_and_tpm_swap, merged at d3d80530) changed increment_tpm_counter from hardcoded -pwdc '' (empty counter auth) to -pwdc "${tpm_passphrase:-}" (owner passphrase from cache/prompt), but left check_tpm_counter using empty -pwdc when called from kexec-sign-config.sh without a $3 passphrase argument. This caused every counter increment to compute SHA1(owner_pass) while the counter was created with SHA1("") - persistent TPM_AUTH_FAIL. Per TCG TPM Main Spec Part 3, TPM_CreateCounter uses owner auth (-pwdo) but TPM_IncrementCounter uses the counter's own authData, not the owner password. The correct design for Heads' rollback counter is empty auth: rollback security comes from the signed /boot/kexec_rollback.txt and TPM sealing, not counter access control. The repeated auth failures (3 per boot x ~5 boots via the _tpm_auth_retry loop) triggered TPM 1.2 dictionary-attack lockout (TPM_DEFEND_LOCK_RUNNING), which persisted through forceclear on some implementations, causing tpm takeown to fail and TPM reset to abort - a cascade failure from the counter auth mismatch. Changes: - initrd/bin/tpmr.sh (_tpm_auth_retry, tpm2_counter_inc, tpm2_seal, tpm1_seal): add 'defend' and '0x98e|0x149' to auth detection grep patterns so defend lock and TPM2 RC codes are treated as retryable auth failures rather than fatal errors - initrd/bin/tpmr.sh (tpm1_reset): detect defend lock after takeown failure and cycle physical presence to clear the lock state before retrying; full AC power cycle remains the fallback if software presence is insufficient - initrd/bin/tpmr.sh (tpm1_counter_increment): detect -pwdc '' and call tpm directly, bypassing _tpm_auth_retry which injected the owner passphrase. Use || return to survive set -e on expected auth failure. - initrd/etc/functions.sh (check_tpm_counter): pass -pwdc '' instead of -pwdc "${tpm_passphrase:-}" so counters use SHA1("") per TCG spec. Document that $3 is intentionally ignored. - initrd/etc/functions.sh (increment_tpm_counter): try -pwdc '' first for TPM1. If that fails on a readable counter (created by PR #2068 era code), prompt for owner passphrase and retry as migration fallback with clear WARN explaining the one-time migration and TPM reset option. - initrd/etc/functions.sh (increment_tpm_counter): remove the TPM1-specific owner-passphrase prompt block added by PR #2068 - initrd/etc/functions.sh (increment_tpm_counter): DIE-path fallback counter_create: -pwdc '' for consistency - initrd/bin/oem-factory-reset.sh: counter_create -pwdc '' for consistency with the empty-auth design - doc/tpm.md: document TPM1 boot chain, tpmtotp tool selection, auth retry patterns, defend lock recovery, and physical presence Signed-off-by: Thierry Laurion --- doc/tpm.md | 96 ++++++++++++++++++++++++++++++++- initrd/bin/oem-factory-reset.sh | 2 +- initrd/bin/tpmr.sh | 75 +++++++++++++++++++++----- initrd/etc/functions.sh | 44 +++++++++------ 4 files changed, 185 insertions(+), 32 deletions(-) diff --git a/doc/tpm.md b/doc/tpm.md index 90f7ec064..f20bf0f8f 100644 --- a/doc/tpm.md +++ b/doc/tpm.md @@ -10,8 +10,35 @@ See also: [architecture.md](architecture.md), [boot-process.md](boot-process.md) ## tpmr — unified TPM abstraction `initrd/bin/tpmr.sh` 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.sh` rather than invoking `tpm` or `tpm2` directly. +over both TPM 1.2 and TPM 2.0. All Heads scripts call `tpmr.sh` rather than +invoking TPM tools directly. + +### Boot chain and TPM tool selection + +```text +initrd/init (PID 1) + └─ CONFIG_BOOTSCRIPT → /bin/gui-init.sh [board config] + ├─ source /etc/functions.sh [shared TPM helpers] + ├─ source /etc/gui_functions.sh [whiptail wrappers] + └─ calls initrd/bin/tpmr.sh [TPM abstraction] + ├─ TPM1: calls `tpm` (tpmtotp util/tpm) [CONFIG_TPM2_TOOLS != y] + │ modules/tpmtotp → output: totp hotp qrenc util/tpm + │ + └─ TPM2: calls tpm2_* (tpm2-tools) [CONFIG_TPM2_TOOLS=y] + modules/tpm2-tss + modules/tpm2-tools +``` + +TPM1 support comes exclusively from the `tpmtotp` module (`modules/tpmtotp`), +which builds `util/tpm` as part of its outputs. This binary is installed to +the initrd as `tpm` and supports subcommands such as `physicalpresence`, +`forceclear`, `takeown -pwdo`, `counter_create`, `counter_increment`, etc. + +TPM2 support comes from `modules/tpm2-tss` (TSS software stack) and +`modules/tpm2-tools` (command-line tools like `tpm2_nvdefine`, +`tpm2_getcap`, `tpm2_nvincrement`). + +Both TPM1 and TPM2 boards may also enable `CONFIG_TPMTOTP=y` for the +`totp` and `hotp` utilities, which are independent of the TPM version. ### PCR sizes @@ -398,3 +425,68 @@ To verify that a new board's coreboot config matches the expected RoT: | Auth sessions | Not used | Required for policy-based unseal | | `kexec_finalize` | No-op | Extends PCRs, then `tpm2 shutdown` | | `startsession` | No-op | Creates encryption session | + +### TPM1 auth retry and error detection + +`_tpm_auth_retry()` in `initrd/bin/tpmr.sh` provides shared retry logic for +both TPM1 and TPM2 operations that need authorization. On auth failure +(wrong passphrase), the passphrase cache is shredded and the user is +re-prompted up to 3 times before giving up. + +Auth failure is detected by grepping the command output for known error +patterns. TPM1 (tpmtotp) errors go to stdout via `printf()` with +`TPM_GetErrMsg()` strings. TPM2 (tpm2-tools) errors go to stderr via +`LOG_ERR()` and may include raw TPM response codes. + +| Pattern | Type | TPM version | Example error | +| --- | --- | --- | --- | +| `authorization|auth|bad|permission` | English words | TPM1+TPM2 | `TPM_AUTHFAIL`, `bad passphrase` | +| `defend` | English word | TPM1 | `Defend lock running` | +| `0x98e|0x149` | Hex codes | TPM2 | `TPM2_RC_AUTH_FAIL`, `TPM2_RC_NV_AUTHORIZATION` | + +### TPM1 reset defend lock + +`TPM_DEFEND_LOCK_RUNNING` (`tpm_error.h`: `TPM_BASE + TPM_NON_FATAL + 3`) +is a standard TPM 1.2 error raised when the TPM's dictionary-attack +protection is active. After too many failed authorization attempts, the +TPM enters a time-out period and refuses all authorization operations — +including `tpm takeown` even after a successful `tpm forceclear` +(forceclear clears the owner but not the dictionary attack counter on +some implementations). + +tpmtotp's `tpm takeown` outputs: +``` +Error Defend lock running from TPM_TakeOwnership +``` + +`tpm1_reset()` in `initrd/bin/tpmr.sh` detects "defend lock" in the +`takeown` output and cycles physical presence (`physicaldisable` / +`physicalenable` / `physicalpresence` / `physicalsetdeactivated`) to +reset the TPM state machine and clear the lock on chips that honour +software presence. `TPM_ResetLockValue` (in tpmtotp's `util/resetlockvalue.c`) +exists but requires owner auth — after forceclear there is no owner, +so it cannot be used. + +If the cycling also fails, only a full AC power cycle (not just reboot) +will clear the defend lock. The timeout duration is chip-specific and +not documented in the tpmtotp source. + +### TPM1 physical presence + +TPM1.2 forceclear requires physical presence to be asserted. The +`tpm1_reset()` function does this with `tpm physicalpresence -s` (software +presence). On some platforms (e.g., Dell OptiPlex, some Infineon TPMs), +software physical presence may not work — the TPM firmware only accepts +hardware-asserted presence (GPIO set by BIOS). In that case, `forceclear` +returns success but may not fully reset the TPM, or `takeown` may fail +with unexpected errors. + +When software physical presence fails, the LOG shows: +``` +tpm1_reset: unable to set physical presence +``` + +This is logged but not fatal — `tpm forceclear` is still attempted. +If the TPM firmware ignores software physical presence, the reset fails +and the user must use the platform's hardware TPM reset mechanism +(typically a BIOS option or jumper). diff --git a/initrd/bin/oem-factory-reset.sh b/initrd/bin/oem-factory-reset.sh index ece2f8f59..5dc315965 100755 --- a/initrd/bin/oem-factory-reset.sh +++ b/initrd/bin/oem-factory-reset.sh @@ -868,7 +868,7 @@ generate_checksums() { if [ "$CONFIG_TPM" = "y" ]; then if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then tpmr.sh counter_create \ - -pwdc "${TPM_PASS:-}" \ + -pwdc '' \ -la -3135106223 | tee /tmp/counter >/dev/null 2>&1 || whiptail_error_die "Unable to create TPM counter" diff --git a/initrd/bin/tpmr.sh b/initrd/bin/tpmr.sh index 46f4581d8..00e5bf379 100755 --- a/initrd/bin/tpmr.sh +++ b/initrd/bin/tpmr.sh @@ -354,7 +354,7 @@ tpm2_counter_inc() { rm -f "$tmp_err_file" shred -n 10 -z -u /tmp/secret/tpm_owner_passphrase 2>/dev/null || true DEBUG "tpm2_counter_inc attempt $attempt failed. Stderr: $tmp_err_content" - if ! echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission|0x98e|0x149'; then + if ! echo "$tmp_err_content" | grep -qiE 'authorization|auth|bad|permission|defend|0x98e|0x149'; then DIE "Can't increment TPM counter for $index, access denied." fi WARN "Authentication failed, retrying..." @@ -370,16 +370,26 @@ tpm2_counter_inc() { # Caching: prompt_tpm_owner_password reuses cached passphrase if available. # On auth failure the cache is shredded; next prompt will ask the user. # +# Error stream selection: +# TPM1 (tpmtotp): errors go to stdout via printf() — capture stdout+stderr +# TPM2 (tpm2-tools): errors go to stderr via LOG_ERR() — capture stderr only +# +# Auth detection grep patterns: +# English words — TPM1 (TPM_GetErrMsg returns "Authentication failed...") +# — TPM2 (tpm2-tools LOG_ERR returns "TPM2_RC_AUTH_FAIL...") +# defend — TPM1 "Defend lock running" (TPM_DEFEND_LOCK_RUNNING) +# 0x98e, 0x149 — TPM2 raw hex codes (TPM2_RC_AUTH_FAIL, TPM2_RC_NV_AUTHORIZATION) +# # Usage: _tpm_auth_retry