tpm2: support SHA384/SHA512 PCR banks in tpm2_get_best_pcr_bank() (#42538)
`tpm2_get_best_pcr_bank()` only ever considered the SHA256 and SHA1 banks (both the `LoaderTpm2ActivePcrBanks` path and the capability guesswork). On a TPM whose only active bank is SHA384 it returned `-EOPNOTSUPP`, breaking sealing/enrollment (cryptenroll, credential encryption, legacy unseal). The restriction looks like a historical simplification — `efi_get_active_pcr_banks()` already decodes SHA384/SHA512 and `tpm2_hash_algorithms[]` already lists them. This PR introduces an explicit preference table (SHA256 > SHA512 > SHA384 > SHA1) and selects from it. SHA256 stays the top preference for backwards compatibility, so existing systems keep using the same bank and the legacy unseal-guess in `tpm2_unseal()` stays consistent; SHA384/SHA512 are only chosen when SHA256 is unavailable, SHA1 remains the last resort. Behavior for existing SHA256/SHA1 systems is unchanged. Includes a unit test for the bank-preference logic. Related to https://github.com/systemd/systemd/pull/42537
This commit is contained in:
+118
-62
@@ -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. */
|
||||
/* 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. */
|
||||
|
||||
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.");
|
||||
}
|
||||
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 = 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
|
||||
*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;
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -46,6 +46,70 @@ TEST(tpm2_pcr_index_from_string) {
|
||||
assert_se(tpm2_pcr_index_from_string("24") == -EINVAL);
|
||||
}
|
||||
|
||||
TEST(tpm2_pcr_bank_from_efi_active) {
|
||||
uint16_t bank;
|
||||
|
||||
/* SHA256 is the top preference whenever it is active. */
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active((1u << TPM2_ALG_SHA1) | (1u << TPM2_ALG_SHA256) | (1u << TPM2_ALG_SHA384) | (1u << TPM2_ALG_SHA512), &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA256);
|
||||
|
||||
/* Without SHA256, SHA384 is preferred over SHA512 (shorter digest, less TPM event log space), and
|
||||
* both win over SHA1. */
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active((1u << TPM2_ALG_SHA1) | (1u << TPM2_ALG_SHA384) | (1u << TPM2_ALG_SHA512), &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA384);
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active((1u << TPM2_ALG_SHA1) | (1u << TPM2_ALG_SHA512), &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA512);
|
||||
|
||||
/* SHA384-only firmware must resolve, not fail. */
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active(1u << TPM2_ALG_SHA384, &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA384);
|
||||
|
||||
/* Single-bank cases pick the obvious bank. */
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active(1u << TPM2_ALG_SHA256, &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA256);
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active(1u << TPM2_ALG_SHA512, &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA512);
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active(1u << TPM2_ALG_SHA1, &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA1);
|
||||
|
||||
/* No bank we are willing to use -> -EOPNOTSUPP. Empty mask, or only a bank we cannot hash in
|
||||
* software (SM3_256, TCG algorithm id 0x12). */
|
||||
ASSERT_ERROR(tpm2_pcr_bank_from_efi_active(0, &bank), EOPNOTSUPP);
|
||||
ASSERT_ERROR(tpm2_pcr_bank_from_efi_active(1u << 0x12, &bank), EOPNOTSUPP);
|
||||
}
|
||||
|
||||
TEST(tpm2_pcr_bank_from_efi_active_legacy) {
|
||||
uint16_t bank;
|
||||
|
||||
/* The legacy variant re-derives the bank for old enrollments that did not record one. Such secrets
|
||||
* could only ever have been sealed against SHA256 or SHA1, so the choice MUST stay restricted to
|
||||
* those two banks regardless of which stronger banks the firmware reports as active — otherwise we'd
|
||||
* re-derive a bank the secret was never bound to and silently fail to unseal. */
|
||||
|
||||
/* SHA256 stays the top preference. */
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active_legacy((1u << TPM2_ALG_SHA1) | (1u << TPM2_ALG_SHA256) | (1u << TPM2_ALG_SHA384) | (1u << TPM2_ALG_SHA512), &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA256);
|
||||
|
||||
/* The crucial backwards-compatibility case: with SHA384/SHA512 active but no SHA256, the legacy
|
||||
* variant must fall back to SHA1, NOT pick the stronger SHA384 the way the non-legacy variant does
|
||||
* (compare the SHA384 result in the test above). */
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active_legacy((1u << TPM2_ALG_SHA1) | (1u << TPM2_ALG_SHA384) | (1u << TPM2_ALG_SHA512), &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA1);
|
||||
|
||||
/* Single-bank cases for the two banks we accept. */
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active_legacy(1u << TPM2_ALG_SHA256, &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA256);
|
||||
ASSERT_OK(tpm2_pcr_bank_from_efi_active_legacy(1u << TPM2_ALG_SHA1, &bank));
|
||||
ASSERT_EQ(bank, TPM2_ALG_SHA1);
|
||||
|
||||
/* Banks the legacy variant never binds to -> -EOPNOTSUPP, even when active. A secret could not have
|
||||
* been sealed against these by the old code, so there is nothing to re-derive. */
|
||||
ASSERT_ERROR(tpm2_pcr_bank_from_efi_active_legacy(1u << TPM2_ALG_SHA384, &bank), EOPNOTSUPP);
|
||||
ASSERT_ERROR(tpm2_pcr_bank_from_efi_active_legacy(1u << TPM2_ALG_SHA512, &bank), EOPNOTSUPP);
|
||||
ASSERT_ERROR(tpm2_pcr_bank_from_efi_active_legacy((1u << TPM2_ALG_SHA384) | (1u << TPM2_ALG_SHA512), &bank), EOPNOTSUPP);
|
||||
ASSERT_ERROR(tpm2_pcr_bank_from_efi_active_legacy(0, &bank), EOPNOTSUPP);
|
||||
}
|
||||
|
||||
TEST(tpm2_util_pbkdf2_hmac_sha256) {
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user