From: Ivan Kruglov Date: Fri, 19 Jun 2026 10:18:15 +0000 (-0700) Subject: tpm2: consider SHA384 and SHA512 PCR banks in tpm2_get_best_pcr_bank() X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2acfa5b294e34dd876fed16d9c048597bffbd224;p=thirdparty%2Fsystemd.git tpm2: consider SHA384 and SHA512 PCR banks in tpm2_get_best_pcr_bank() tpm2_get_best_pcr_bank() only ever considered SHA256 and SHA1, both in the firmware-reported LoaderTpm2ActivePcrBanks path and in the capability-based guesswork. On a TPM whose only active bank is SHA384 it returned -EOPNOTSUPP, which for units ordered before sysinit.target (systemd-pcrextend/systemd-pcrproduct.service) propagates and hangs the boot, even with --graceful (that only covers "no TPM present"). Introduce a preference table (SHA256 > SHA384 > SHA512 > SHA1) and select from it. SHA256 stays top for backwards compatibility; SHA384 is preferred over SHA512 as it produces a shorter digest and so uses less of the bounded TPM event log; SHA1 is the last resort. SM3/SHA3 are excluded as we cannot compute those digests in software. The legacy unseal path in tpm2_unseal() re-derives the bank for old enrollments that did not record one (pcr_bank == UINT16_MAX). Those were sealed by code that only picked SHA256 or SHA1, so the wider preference could now re-derive a stronger bank the secret was never bound to and silently fail to unseal. To stay compatible, the selection takes the preference list as a parameter, and a _legacy() variant restricted to {SHA256, SHA1} is used for that path; fresh-sealing callers keep the full preference. Co-developed-by: Claude Opus 4.8 --- diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 2e2c5d03b5a..d718824376b 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -52,6 +52,52 @@ #include "unaligned.h" #include "virt.h" +/* The PCR banks we are willing to bind policies to, in order of preference. We keep SHA256 as the top + * preference for backwards compatibility (systems that already bind to it keep using the same bank, so + * existing TPM2-sealed secrets remain valid), then prefer SHA384 over SHA512: SHA384 is computed in 64-bit + * words like SHA512 but produces a shorter digest, so it takes up less room in the TPM event log, whose + * memory is usually bounded. SHA512 comes next, and we only fall back to the weak SHA1 as a last resort. */ +static const uint16_t tpm2_pcr_bank_preference[] = { + TPM2_ALG_SHA256, + TPM2_ALG_SHA384, + TPM2_ALG_SHA512, + TPM2_ALG_SHA1, +}; + +/* The reduced preference list used when re-deriving the bank for legacy enrollments that did not record one + * (see tpm2_get_best_pcr_bank_legacy()). Such enrollments were sealed by code that only ever considered + * SHA256 and SHA1, so they can only be bound to one of those two banks. We must restrict the choice to exactly this set. */ +static const uint16_t tpm2_pcr_bank_preference_legacy[] = { + TPM2_ALG_SHA256, + TPM2_ALG_SHA1, +}; + +static int pcr_bank_from_efi_active( + const uint16_t *banks, + size_t n_banks, + uint32_t active_banks, + uint16_t *ret) { + + assert(banks); + assert(ret); + + FOREACH_ARRAY(bank, banks, n_banks) + if (BIT_SET(active_banks, *bank)) { + *ret = *bank; + return 0; + } + + return -EOPNOTSUPP; +} + +int tpm2_pcr_bank_from_efi_active(uint32_t active_banks, uint16_t *ret) { + return pcr_bank_from_efi_active(tpm2_pcr_bank_preference, ELEMENTSOF(tpm2_pcr_bank_preference), active_banks, ret); +} + +int tpm2_pcr_bank_from_efi_active_legacy(uint32_t active_banks, uint16_t *ret) { + return pcr_bank_from_efi_active(tpm2_pcr_bank_preference_legacy, ELEMENTSOF(tpm2_pcr_bank_preference_legacy), active_banks, ret); +} + #if HAVE_TPM2 static DLSYM_PROTOTYPE(Esys_Create) = NULL; static DLSYM_PROTOTYPE(Esys_CreateLoaded) = NULL; @@ -2863,9 +2909,23 @@ static int tpm2_bank_has24(const TPMS_PCR_SELECTION *selection) { return valid; } -int tpm2_get_best_pcr_bank( +static char* pcr_bank_preference_to_string(const uint16_t *banks, size_t n_banks) { + _cleanup_free_ char *s = NULL; + + assert(banks); + + FOREACH_ARRAY(bank, banks, n_banks) + if (!strextend_with_separator(&s, ", ", tpm2_hash_alg_to_string(*bank))) + return NULL; + + return TAKE_PTR(s); +} + +static int get_best_pcr_bank( Tpm2Context *c, uint32_t pcr_mask, + const uint16_t *banks, + size_t n_banks, TPMI_ALG_HASH *ret) { TPMI_ALG_HASH supported_hash = 0, hash_with_valid_pcr = 0; @@ -2873,6 +2933,8 @@ int tpm2_get_best_pcr_bank( assert(c); assert(ret); + assert(banks); + assert(n_banks > 0); uint32_t efi_banks; r = efi_get_active_pcr_banks(&efi_banks); @@ -2887,14 +2949,18 @@ int tpm2_get_best_pcr_bank( else if (efi_banks == 0) log_debug("Boot loader set the LoaderTpm2ActivePcrBanks EFI variable to zero to indicate that TPM support is not available in the firmware. We'll have to guess the used PCR banks."); else { - if (BIT_SET(efi_banks, TPM2_ALG_SHA256)) - *ret = TPM2_ALG_SHA256; - else if (BIT_SET(efi_banks, TPM2_ALG_SHA1)) - *ret = TPM2_ALG_SHA1; - else - return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Firmware reports neither SHA1 nor SHA256 PCR banks, cannot operate."); + r = pcr_bank_from_efi_active(banks, n_banks, efi_banks, ret); + if (r == -EOPNOTSUPP) { + _cleanup_free_ char *bank_list = pcr_bank_preference_to_string(banks, n_banks); + return log_debug_errno(r, "Firmware reports none of the PCR banks we can use (%s), cannot operate.", strna(bank_list)); + } + if (r < 0) + return r; - log_debug("Picked best PCR bank %s based on firmware reported banks.", tpm2_hash_alg_to_string(*ret)); + if (*ret == TPM2_ALG_SHA1) + log_notice("Firmware reports SHA1 as the best active PCR bank, binding policy to it. This reduces the security level substantially."); + else + log_debug("Picked best PCR bank %s based on firmware reported banks.", tpm2_hash_alg_to_string(*ret)); return 0; } @@ -2904,75 +2970,65 @@ int tpm2_get_best_pcr_bank( return 0; } - FOREACH_TPMS_PCR_SELECTION_IN_TPML_PCR_SELECTION(selection, &c->capability_pcrs) { - TPMI_ALG_HASH hash = selection->hash; + /* Walk our banks in order of preference. Because the list is already sorted best-first, the first + * bank that qualifies is the most preferred one — no ranking needed. */ + FOREACH_ARRAY(bank, banks, n_banks) { int good; - /* For now we are only interested in the SHA1 and SHA256 banks */ - if (!IN_SET(hash, TPM2_ALG_SHA256, TPM2_ALG_SHA1)) + /* Skip banks the TPM doesn't expose with a full set of 24 PCRs. */ + if (!tpm2_tpml_pcr_selection_has_mask(&c->capability_pcrs, *bank, TPM2_PCRS_MASK)) continue; - r = tpm2_bank_has24(selection); - if (r < 0) - return r; - if (!r) - continue; + /* First supported bank we reach is the most preferred supported one. */ + if (supported_hash == 0) + supported_hash = *bank; - good = tpm2_pcr_mask_good(c, hash, pcr_mask); + good = tpm2_pcr_mask_good(c, *bank, pcr_mask); if (good < 0) return good; - - if (hash == TPM2_ALG_SHA256) { - supported_hash = TPM2_ALG_SHA256; - if (good) { - /* Great, SHA256 is supported and has initialized PCR values, we are done. */ - hash_with_valid_pcr = TPM2_ALG_SHA256; - break; - } - } else { - assert(hash == TPM2_ALG_SHA1); - - if (supported_hash == 0) - supported_hash = TPM2_ALG_SHA1; - - if (good && hash_with_valid_pcr == 0) - hash_with_valid_pcr = TPM2_ALG_SHA1; + if (good) { + /* First supported bank with initialized PCRs is the most preferred such bank; we + * cannot do any better, so we are done. */ + hash_with_valid_pcr = *bank; + break; } } - /* We preferably pick SHA256, but only if its PCRs are initialized or neither the SHA1 nor the SHA256 - * PCRs are initialized. If SHA256 is not supported but SHA1 is and its PCRs are too, we prefer - * SHA1. - * - * We log at LOG_NOTICE level whenever we end up using the SHA1 bank or when the PCRs we bind to are - * not initialized. */ - - if (hash_with_valid_pcr == TPM2_ALG_SHA256) { - assert(supported_hash == TPM2_ALG_SHA256); - log_debug("TPM2 device supports SHA256 PCR bank and SHA256 PCRs are valid, yay!"); - *ret = TPM2_ALG_SHA256; - } else if (hash_with_valid_pcr == TPM2_ALG_SHA1) { - if (supported_hash == TPM2_ALG_SHA256) - log_notice("TPM2 device supports both SHA1 and SHA256 PCR banks, but only SHA1 PCRs are valid, falling back to SHA1 bank. This reduces the security level substantially."); - else { - assert(supported_hash == TPM2_ALG_SHA1); - log_notice("TPM2 device lacks support for SHA256 PCR bank, but SHA1 bank is supported and SHA1 PCRs are valid, falling back to SHA1 bank. This reduces the security level substantially."); - } + /* Prefer a bank whose selected PCRs are actually initialized. If none of the supported banks has + * initialized PCRs we still proceed with the most preferred supported bank, but the resulting PCR + * policy is then effectively unenforced. */ - *ret = TPM2_ALG_SHA1; - } else if (supported_hash == TPM2_ALG_SHA256) { - log_notice("TPM2 device supports SHA256 PCR bank but none of the selected PCRs are valid! Firmware apparently did not initialize any of the selected PCRs. Proceeding anyway with SHA256 bank. PCR policy effectively unenforced!"); - *ret = TPM2_ALG_SHA256; - } else if (supported_hash == TPM2_ALG_SHA1) { - log_notice("TPM2 device lacks support for SHA256 bank, but SHA1 bank is supported, but none of the selected PCRs are valid! Firmware apparently did not initialize any of the selected PCRs. Proceeding anyway with SHA1 bank. PCR policy effectively unenforced!"); - *ret = TPM2_ALG_SHA1; - } else + if (hash_with_valid_pcr != 0) { + if (hash_with_valid_pcr == TPM2_ALG_SHA1) + log_notice("Only the weak SHA1 PCR bank has initialized PCRs, binding policy to it. This reduces the security level substantially."); + else if (hash_with_valid_pcr != supported_hash) + log_notice("Preferred %s PCR bank has uninitialized PCRs, binding policy to the %s bank instead.", + tpm2_hash_alg_to_string(supported_hash), tpm2_hash_alg_to_string(hash_with_valid_pcr)); + else + log_debug("Binding policy to %s PCR bank with initialized PCRs.", tpm2_hash_alg_to_string(hash_with_valid_pcr)); + + *ret = hash_with_valid_pcr; + } else if (supported_hash != 0) { + log_notice("TPM2 device supports the %s PCR bank but none of the selected PCRs are initialized! Firmware apparently did not measure into any of them. Proceeding anyway, but the PCR policy is effectively unenforced!", + tpm2_hash_alg_to_string(supported_hash)); + *ret = supported_hash; + } else { + _cleanup_free_ char *bank_list = pcr_bank_preference_to_string(banks, n_banks); return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "TPM2 module supports neither SHA1 nor SHA256 PCR banks, cannot operate."); + "TPM2 module supports none of the PCR banks we can use (%s), cannot operate.", strna(bank_list)); + } return 0; } +int tpm2_get_best_pcr_bank(Tpm2Context *c, uint32_t pcr_mask, TPMI_ALG_HASH *ret) { + return get_best_pcr_bank(c, pcr_mask, tpm2_pcr_bank_preference, ELEMENTSOF(tpm2_pcr_bank_preference), ret); +} + +int tpm2_get_best_pcr_bank_legacy(Tpm2Context *c, uint32_t pcr_mask, TPMI_ALG_HASH *ret) { + return get_best_pcr_bank(c, pcr_mask, tpm2_pcr_bank_preference_legacy, ELEMENTSOF(tpm2_pcr_bank_preference_legacy), ret); +} + int tpm2_get_good_pcr_banks( Tpm2Context *c, uint32_t pcr_mask, @@ -5814,9 +5870,9 @@ int tpm2_unseal(Tpm2Context *c, return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Number of provided known policy hashes (%zu) does not match policy requirements (%zu or 0).", n_known_policy_hash, n_shards); /* Older code did not save the pcr_bank, and unsealing needed to detect the best pcr bank to use, - * so we need to handle that legacy situation. */ + * so we need to handle that legacy situation. Use the legacy variant, which only ever picks SHA256 or SHA1. */ if (pcr_bank == UINT16_MAX) { - r = tpm2_get_best_pcr_bank(c, hash_pcr_mask|pubkey_pcr_mask, &pcr_bank); + r = tpm2_get_best_pcr_bank_legacy(c, hash_pcr_mask|pubkey_pcr_mask, &pcr_bank); if (r < 0) return r; } diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 4d62702ce1f..922cc439de9 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -159,6 +159,8 @@ bool tpm2_test_parms(Tpm2Context *c, TPMI_ALG_PUBLIC alg, const TPMU_PUBLIC_PARM int tpm2_get_good_pcr_banks(Tpm2Context *c, uint32_t pcr_mask, TPMI_ALG_HASH **ret_banks); int tpm2_get_good_pcr_banks_strv(Tpm2Context *c, uint32_t pcr_mask, char ***ret); int tpm2_get_best_pcr_bank(Tpm2Context *c, uint32_t pcr_mask, TPMI_ALG_HASH *ret); +/* Like tpm2_get_best_pcr_bank(), but restricted to SHA256/SHA1 for re-deriving the bank of legacy enrollments */ +int tpm2_get_best_pcr_bank_legacy(Tpm2Context *c, uint32_t pcr_mask, TPMI_ALG_HASH *ret); const char* tpm2_userspace_log_path(void); const char* tpm2_firmware_log_path(void); @@ -482,6 +484,12 @@ int tpm2_parse_luks2_json(sd_json_variant *v, int *ret_keyslot, uint32_t *ret_ha #define TPM2_ALG_RSA 0x1 #endif +/* Picks the most preferred PCR bank (SHA256 > SHA384 > SHA512 > SHA1) out of the firmware-reported active + * banks bitmask. Defined unconditionally (no TPM2 libraries required) so it can be unit tested. */ +int tpm2_pcr_bank_from_efi_active(uint32_t active_banks, uint16_t *ret); +/* Like tpm2_pcr_bank_from_efi_active(), but restricted to SHA256/SHA1, for re-deriving the bank of legacy enrollments */ +int tpm2_pcr_bank_from_efi_active_legacy(uint32_t active_banks, uint16_t *ret); + int tpm2_hash_alg_to_size(uint16_t alg); const char* tpm2_hash_alg_to_string(uint16_t alg) _const_;