diff --git a/TODO.md b/TODO.md index ed7c40c2dbb..8b8451c9212 100644 --- a/TODO.md +++ b/TODO.md @@ -1989,7 +1989,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - pre-calc cryptsetup root key measurement - maybe make systemd-repart generate .pcrlock for old and new GPT header in /run? - - Add support for more than 8 branches per PCR OR - add "systemd-pcrlock lock-kernel-current" or so which synthesizes .pcrlock policy from currently booted kernel/event log, to close gap for first boot for pre-built images diff --git a/man/systemd-pcrlock.xml b/man/systemd-pcrlock.xml index 24f9978eb6b..f5c116cd4d7 100644 --- a/man/systemd-pcrlock.xml +++ b/man/systemd-pcrlock.xml @@ -74,6 +74,10 @@ then stored in an NV index in the TPM2. This may be used to then lock secrets (such as disk encryption keys) to these policies (via a TPM2 PolicyAuthorizeNV policy). + If a single PCR has more than eight alternative values, the PolicyOR items are + nested into a balanced tree. The current implementation supports at most 5 nesting levels, i.e. up to + 32768 alternatives per PCR. + Use tools such as systemd-cryptenroll1 or systemd-repart8 to diff --git a/man/systemd.pcrlock.xml b/man/systemd.pcrlock.xml index d27d13b9617..8d06ef20851 100644 --- a/man/systemd.pcrlock.xml +++ b/man/systemd.pcrlock.xml @@ -68,6 +68,11 @@ component. Example: 650-kernel.pcrlock.d/6.5.5-200.fc38.x86_64.pcrlock and 650-kernel.pcrlock.d/6.5.7-100.fc38.x86_64.pcrlock + Components may have any number of variants. If a PCR ends up with more than eight alternative + values, systemd-pcrlock expresses them in the TPM2 access policy as a balanced tree of + nested PolicyOR statements. Up to 8^5 = 32768 alternative values per PCR are + supported. + Use systemd-pcrlock list-components to list all pcrlock files currently installed. diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 10e3b843664..b8349463ec3 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -5498,55 +5498,80 @@ int tpm2_policy_authorize_nv( return tpm2_get_policy_digest(c, session, ret_policy_digest); } -int tpm2_policy_or( - Tpm2Context *c, - const Tpm2Handle *session, - const TPM2B_DIGEST *branches, size_t n_branches, - TPM2B_DIGEST **ret_policy_digest) { +/* TPM2_PolicyOR() takes at most TPM2_POLICY_OR_MAX_BRANCHES digests. To support more alternatives we nest + * ORs into a balanced tree with that fanout. All leaves are at the same depth and the upper levels contain + * only ORs, as the TCG TSS JSON policy format requires. The exact partition below is part of the on-disk + * policy format: sealing (tpm2_calculate_policy_or()) and unsealing (tpm2_policy_or()) must derive the same + * tree forever, so group sizes and ordering must not change. */ - TPML_DIGEST hash_list; - TSS2_RC rc; - - assert(c); - assert(session); - - if (n_branches > ELEMENTSOF(hash_list.digests)) - return -EOPNOTSUPP; - - log_debug("Submitting OR policy."); - - hash_list = (TPML_DIGEST) { - .count = n_branches, - }; - - memcpy(hash_list.digests, branches, n_branches * sizeof(TPM2B_DIGEST)); - - if (DEBUG_LOGGING) - for (size_t i = 0; i < hash_list.count; i++) { - _cleanup_free_ char *h = hexmem(hash_list.digests[i].buffer, hash_list.digests[i].size); - log_debug("Submitting OR Branch #%zu: %s", i, h); - } - - rc = sym_Esys_PolicyOR( - c->esys_context, - session->esys_handle, - ESYS_TR_NONE, - ESYS_TR_NONE, - ESYS_TR_NONE, - &hash_list); - if ((rc & ~(TPM2_RC_N_MASK|TPM2_RC_P)) == TPM2_RC_VALUE) /* Return a recognizable error if none of the OR branches matched */ - return log_debug_errno(SYNTHETIC_ERRNO(ENOANO), - "None of the PolicyOR branches matched the current policy state."); - if (rc != TSS2_RC_SUCCESS) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), - "Failed to add OR policy to TPM: %s", - sym_Tss2_RC_Decode(rc)); - - return tpm2_get_policy_digest(c, session, ret_policy_digest); +/* Number of ORs needed to combine 'n' nodes into one level up. */ +static size_t tpm2_policy_or_tree_n_groups(size_t n) { + return DIV_ROUND_UP(n, TPM2_POLICY_OR_MAX_BRANCHES); } -/* Extend 'digest' with the PolicyOR calculated hash. */ -int tpm2_calculate_policy_or(const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST *digest) { +/* Number of nested OR levels needed to collapse 'n' branches into one digest. A flat OR (n <= 8) has + * depth 1, i.e. one level, and the tree has depth+1 levels in total including the leaves. Because the fanout + * is TPM2_POLICY_OR_MAX_BRANCHES, depth 'd' supports up to TPM2_POLICY_OR_MAX_BRANCHES^d alternatives, i.e. + * TPM2_POLICY_OR_MAX_DEPTH = 5 levels allow up to 8^5 = 32768 alternatives per PCR. */ +static size_t tpm2_policy_or_tree_depth(size_t n) { + size_t depth = 0; + + while (n > 1) { + n = tpm2_policy_or_tree_n_groups(n); + depth++; + } + + return depth; +} + +/* Whether a PolicyOR expression over 'n' branches stays within TPM2_POLICY_OR_MAX_DEPTH levels. Callers use + * this to reject overly large branch lists before they build them, so that the offending PCR can be named + * and no work is wasted on the TPM round trips preceding it. */ +static bool tpm2_policy_or_branches_supported(size_t n) { + return n > 0 && tpm2_policy_or_tree_depth(n) <= TPM2_POLICY_OR_MAX_DEPTH; +} + +/* Distribute 'n' nodes evenly over tpm2_policy_or_tree_n_groups(n) groups, with the first n%g groups getting + * one extra node. This avoids single-branch ORs. Return the slice [*ret_start, *ret_start + *ret_size) held + * by group 'group'. */ +static void tpm2_policy_or_tree_group_slice(size_t n, size_t group, size_t *ret_start, size_t *ret_size) { + size_t g, base, rem; + + assert(n > 0); + assert(ret_start); + assert(ret_size); + + g = tpm2_policy_or_tree_n_groups(n); + base = n / g; + rem = n % g; + + *ret_size = base + (group < rem); + *ret_start = group < rem ? group * (base + 1) : + rem * (base + 1) + (group - rem) * base; +} + +/* Inverse of tpm2_policy_or_tree_group_slice(): return which group of a level with 'n' nodes holds 'index'. */ +static size_t tpm2_policy_or_tree_group_of(size_t n, size_t index) { + size_t g, base, rem, first_big; + + assert(n > 0); + assert(index < n); + + g = tpm2_policy_or_tree_n_groups(n); + base = n / g; + rem = n % g; + first_big = rem * (base + 1); + + return index < first_big ? index / (base + 1) : + rem + (index - first_big) / base; +} + +/* Extend 'digest' with the PolicyOR calculated hash, for a single flat OR with 1…8 branches. */ +static int tpm2_calculate_policy_or_flat( + const TPM2B_DIGEST *branches, + size_t n_branches, + TPM2B_DIGEST *digest) { + TPM2_CC command = TPM2_CC_PolicyOR; TSS2_RC rc; int r; @@ -5556,10 +5581,10 @@ int tpm2_calculate_policy_or(const TPM2B_DIGEST *branches, size_t n_branches, TP if (n_branches == 0) return -EINVAL; + if (n_branches > TPM2_POLICY_OR_MAX_BRANCHES) + return -E2BIG; if (n_branches == 1) log_warning("PolicyOR with a single branch submitted, this is weird."); - if (n_branches > 8) - return -E2BIG; r = dlopen_tpm2(LOG_ERR); if (r < 0) @@ -5602,6 +5627,216 @@ int tpm2_calculate_policy_or(const TPM2B_DIGEST *branches, size_t n_branches, TP return 0; } +/* A fully materialized balanced PolicyOR tree: levels[0] are the n_level[0] leaves, and the last level holds + * exactly one digest (the root). Intermediate levels hold the flat OR of each group of the level below. */ +typedef struct Tpm2PolicyORTree { + TPM2B_DIGEST **levels; + size_t *n_level; + size_t n_levels; +} Tpm2PolicyORTree; + +static void tpm2_policy_or_tree_done(Tpm2PolicyORTree *t) { + if (!t) + return; + + if (t->levels) + for (size_t i = 0; i < t->n_levels; i++) + free(t->levels[i]); + + free(t->levels); + free(t->n_level); + *t = (Tpm2PolicyORTree) {}; +} + +static int tpm2_policy_or_tree_build( + const TPM2B_DIGEST *branches, + size_t n_branches, + Tpm2PolicyORTree *ret) { + + size_t depth; + int r; + + assert(branches); + assert(n_branches > TPM2_POLICY_OR_MAX_BRANCHES); + assert(ret); + + depth = tpm2_policy_or_tree_depth(n_branches); + if (depth > TPM2_POLICY_OR_MAX_DEPTH) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), + "PolicyOR tree for %zu branches exceeds %u levels.", + n_branches, TPM2_POLICY_OR_MAX_DEPTH); + + _cleanup_(tpm2_policy_or_tree_done) Tpm2PolicyORTree tree = { + .n_levels = depth + 1, + }; + + tree.levels = new0(TPM2B_DIGEST*, tree.n_levels); + tree.n_level = new0(size_t, tree.n_levels); + if (!tree.levels || !tree.n_level) + return log_oom_debug(); + + tree.n_level[0] = n_branches; + tree.levels[0] = newdup(TPM2B_DIGEST, branches, n_branches); + if (!tree.levels[0]) + return log_oom_debug(); + + for (size_t i = 0; i + 1 < tree.n_levels; i++) { + size_t n_groups = tpm2_policy_or_tree_n_groups(tree.n_level[i]); + + tree.n_level[i + 1] = n_groups; + tree.levels[i + 1] = new0(TPM2B_DIGEST, n_groups); + if (!tree.levels[i + 1]) + return log_oom_debug(); + + for (size_t g = 0; g < n_groups; g++) { + size_t start, size; + + tree.levels[i + 1][g].size = SHA256_DIGEST_SIZE; + + tpm2_policy_or_tree_group_slice(tree.n_level[i], g, &start, &size); + r = tpm2_calculate_policy_or_flat( + tree.levels[i] + start, + size, + &tree.levels[i + 1][g]); + if (r < 0) + return r; + } + } + + *ret = TAKE_STRUCT(tree); + return 0; +} + +/* Submit a single flat PolicyOR (1…8 branches) to the TPM. */ +static int tpm2_policy_or_submit_flat( + Tpm2Context *c, + const Tpm2Handle *session, + const TPM2B_DIGEST *branches, + size_t n_branches) { + + TPML_DIGEST hash_list; + TSS2_RC rc; + + assert_cc(ELEMENTSOF(((TPML_DIGEST) {}).digests) == TPM2_POLICY_OR_MAX_BRANCHES); + + assert(c); + assert(session); + assert(n_branches <= TPM2_POLICY_OR_MAX_BRANCHES); + + log_debug("Submitting OR policy."); + + hash_list = (TPML_DIGEST) { + .count = n_branches, + }; + + memcpy(hash_list.digests, branches, n_branches * sizeof(TPM2B_DIGEST)); + + if (DEBUG_LOGGING) + for (size_t i = 0; i < hash_list.count; i++) { + _cleanup_free_ char *h = hexmem(hash_list.digests[i].buffer, hash_list.digests[i].size); + log_debug("Submitting OR Branch #%zu: %s", i, h); + } + + rc = sym_Esys_PolicyOR( + c->esys_context, + session->esys_handle, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + &hash_list); + if (TPM2_RC_IS_POLICY_OR_NO_MATCH(rc)) /* Return a recognizable error if none of the OR branches matched */ + return log_debug_errno(SYNTHETIC_ERRNO(ENOANO), + "None of the PolicyOR branches matched the current policy state."); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to add OR policy to TPM: %s", + sym_Tss2_RC_Decode(rc)); + + return 0; +} + +int tpm2_policy_or( + Tpm2Context *c, + const Tpm2Handle *session, + const TPM2B_DIGEST *branches, size_t n_branches, + TPM2B_DIGEST **ret_policy_digest) { + + int r; + + assert(c); + assert(session); + + if (n_branches <= TPM2_POLICY_OR_MAX_BRANCHES) { + r = tpm2_policy_or_submit_flat(c, session, branches, n_branches); + if (r < 0) + return r; + + return tpm2_get_policy_digest(c, session, ret_policy_digest); + } + + _cleanup_(tpm2_policy_or_tree_done) Tpm2PolicyORTree tree = {}; + r = tpm2_policy_or_tree_build(branches, n_branches, &tree); + if (r < 0) + return r; + + /* The immediately preceding policy command (typically PolicyPCR) left the session digest equal to + * one of our leaves. Find it, then walk from it up to the root, one OR per level. */ + _cleanup_(Esys_Freep) TPM2B_DIGEST *current = NULL; + r = tpm2_get_policy_digest(c, session, ¤t); + if (r < 0) + return r; + + size_t index = SIZE_MAX; + for (size_t i = 0; i < tree.n_level[0]; i++) + if (memcmp_nn(tree.levels[0][i].buffer, tree.levels[0][i].size, + current->buffer, current->size) == 0) { + index = i; + break; + } + + if (index == SIZE_MAX) + return log_debug_errno(SYNTHETIC_ERRNO(ENOANO), + "None of the PolicyOR branches matched the current policy state."); + + for (size_t i = 0; i + 1 < tree.n_levels; i++) { + size_t group, start, size; + + /* PolicyOR can only fail with TPM2_RC_VALUE if the session digest is not one of the submitted + * digests. Every slice we submit does contain it: level 0's slice contains the leaf we just + * matched, and after each OR the session digest is exactly levels[i + 1][group], which the + * slice for the next level contains by construction. */ + group = tpm2_policy_or_tree_group_of(tree.n_level[i], index); + tpm2_policy_or_tree_group_slice(tree.n_level[i], group, &start, &size); + + r = tpm2_policy_or_submit_flat(c, session, tree.levels[i] + start, size); + if (r < 0) + return r; + + index = group; + } + + return tpm2_get_policy_digest(c, session, ret_policy_digest); +} + +/* Extend 'digest' with the PolicyOR calculated hash. */ +int tpm2_calculate_policy_or(const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST *digest) { + assert(digest); + assert(digest->size == SHA256_DIGEST_SIZE); + + if (n_branches <= TPM2_POLICY_OR_MAX_BRANCHES) + return tpm2_calculate_policy_or_flat(branches, n_branches, digest); + + _cleanup_(tpm2_policy_or_tree_done) Tpm2PolicyORTree tree = {}; + int r; + + r = tpm2_policy_or_tree_build(branches, n_branches, &tree); + if (r < 0) + return r; + + *digest = tree.levels[tree.n_levels - 1][0]; + return 0; +} + /* Extend 'digest' with the PolicyPCR calculated hash. */ int tpm2_calculate_policy_pcr( const Tpm2PCRValue *pcr_values, @@ -10170,17 +10405,20 @@ int tpm2_calculate_policy_super_pcr( /* Now deal with the PCRs for which we have variants, i.e. more than one allowed values */ for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) { _cleanup_free_ TPM2B_DIGEST *pcr_policy_digest_variants = NULL; - size_t n_pcr_policy_digest_variants = 0; + size_t n_pcr_policy_digest_variants = 0, n_variants; Tpm2PCRPredictionResult *banks; if (!BIT_SET(prediction->pcrs, pcr)) continue; - if (ordered_set_size(prediction->results[pcr]) <= 1) /* We only care for PCRs with 2 or more variants in this loop */ + n_variants = ordered_set_size(prediction->results[pcr]); + if (n_variants <= 1) /* We only care for PCRs with 2 or more variants in this loop */ continue; - if (ordered_set_size(prediction->results[pcr]) > 8) - return log_error_errno(SYNTHETIC_ERRNO(E2BIG), "PCR policies with more than 8 alternatives per PCR are currently not supported."); + if (!tpm2_policy_or_branches_supported(n_variants)) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), + "PCR %" PRIu32 " has %zu predicted values, which needs more than the %u nested PolicyOR levels we support.", + pcr, n_variants, TPM2_POLICY_OR_MAX_DEPTH); ORDERED_SET_FOREACH(banks, prediction->results[pcr]) { /* Start from the super PCR policy from the previous PCR we looked at so far. */ @@ -10211,7 +10449,6 @@ int tpm2_calculate_policy_super_pcr( } assert_se(n_pcr_policy_digest_variants >= 2); - assert_se(n_pcr_policy_digest_variants <= 8); /* Now combine all our variant into one OR policy */ r = tpm2_calculate_policy_or( @@ -10284,8 +10521,13 @@ int tpm2_policy_super_pcr( continue; n_branches = ordered_set_size(prediction->results[pcr]); - if (n_branches < 1 || n_branches > 8) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Number of variants per PCR not in range 1…8"); + if (n_branches < 1) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "PCR %" PRIu32 " has no predicted values.", pcr); + if (!tpm2_policy_or_branches_supported(n_branches)) + return log_error_errno(SYNTHETIC_ERRNO(E2BIG), + "PCR %" PRIu32 " has %zu predicted values, which needs more than the %u nested PolicyOR levels we support.", + pcr, n_branches, TPM2_POLICY_OR_MAX_DEPTH); if (n_branches == 1) /* Single choice PCRs are already covered by the loop above */ continue; diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 609d2aa93c7..c1d488c2579 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -348,6 +348,33 @@ int tpm2_policy_authorize_nv(Tpm2Context *c, const Tpm2Handle *session, const Tp * moved while the policy was submitted, i.e. if something extended a PCR in the meantime. That's transient, * callers are supposed to restart the policy session and try again. */ int tpm2_policy_pcr(Tpm2Context *c, const Tpm2Handle *session, const TPML_PCR_SELECTION *pcr_selection, TPM2B_DIGEST **ret_policy_digest); + +/* TPM2_PolicyOR() accepts at most TPM2_POLICY_OR_MAX_BRANCHES branches (the TPM's TPML_DIGEST is fixed at 8 + * elements). To support more alternatives the PolicyOR helpers nest ORs into a balanced tree with that + * fanout. TPM2_POLICY_OR_MAX_DEPTH caps the nesting depth, and hence the number of alternatives per PCR at + * TPM2_POLICY_OR_MAX_BRANCHES^TPM2_POLICY_OR_MAX_DEPTH = 8^5 = 32768. The cap ensures unsealing never needs + * an unbounded number of TPM round trips. */ +#define TPM2_POLICY_OR_MAX_BRANCHES 8U +#define TPM2_POLICY_OR_MAX_DEPTH 5U + +/* TPM2_PolicyOR() has a single parameter, pHashList. When the TPM rejects the command it does so with + * TPM_RC_VALUE on that parameter: a format-one code with TPM_RC_P set and the 1-based parameter number in N. + * Classify those fields explicitly instead of masking N away the way we do for other commands: TPM_RC_S + * shares bit 11 with the top bit of N, so masking N away hides the session bit too and would turn a + * session-related (or otherwise unrelated) TPM_RC_VALUE into a bogus "no branch matched". */ +static inline bool TPM2_RC_IS_POLICY_OR_NO_MATCH(TSS2_RC rc) { + if (!(rc & TPM2_RC_P)) /* A handle- or session-related error, or not parameter-related at all */ + return false; + if (rc & TPM2_RC_S) /* Session-related, hence not about pHashList */ + return false; + if ((rc & ~(TPM2_RC_N_MASK | TPM2_RC_P)) != TPM2_RC_VALUE) + return false; + + /* N is the parameter number, i.e. 1 for PolicyOR's only parameter. TPMs that leave it at 0 are + * tolerated. */ + return ((rc & TPM2_RC_N_MASK) >> 8) <= 1; +} + int tpm2_policy_or(Tpm2Context *c, const Tpm2Handle *session, const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST **ret_policy_digest); int tpm2_policy_super_pcr(Tpm2Context *c, const Tpm2Handle *session, const Tpm2PCRPrediction *prediction, uint16_t algorithm); int tpm2_policy_signed_hmac_sha256(Tpm2Context *c, const Tpm2Handle *session, const Tpm2Handle *hmac_key_handle, const struct iovec *hmac_key, TPM2B_DIGEST **ret_policy_digest); diff --git a/src/test/test-tpm2.c b/src/test/test-tpm2.c index d7964ec5af1..d6c8fa0ee6d 100644 --- a/src/test/test-tpm2.c +++ b/src/test/test-tpm2.c @@ -6,6 +6,7 @@ #include "hexdecoct.h" #include "iovec-util.h" #include "memory-util.h" +#include "ordered-set.h" #include "random-util.h" #include "tests.h" #include "tpm2-util.h" @@ -1257,6 +1258,191 @@ TEST(calculate_policy_pcr) { assert_se(digest_check(&d, "7481fd1b116078eb3ac2456e4ad542c9b46b9b8eb891335771ca8e7c8f8e4415")); } +/* Fill a digest with a value that is distinct for every index, so that grouping two different indices + * together can never accidentally compare equal. */ +static void fill_indexed_digest(TPM2B_DIGEST *digest, size_t index) { + uint64_t state = index; + + *digest = (TPM2B_DIGEST) { .size = SHA256_DIGEST_SIZE }; + + for (size_t i = 0; i < SHA256_DIGEST_SIZE; i++) { + /* The first sizeof(uint64_t) bytes encode the index verbatim (independent of the host's + * size_t width, so the digests are identical on every architecture, such as armhf). The + * remaining bytes are a cheap expansion. */ + digest->buffer[i] = i < sizeof(uint64_t) ? (uint8_t) ((uint64_t) index >> (8 * i)) : (uint8_t) state; + + /* This is a full-period LCG mod 2^64, the constants are taken from the PCG reference + * implementation. */ + state = state * UINT64_C(6364136223846793005) + UINT64_C(1442695040888963407); + } +} + +static size_t sha256_index(void) { + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) + if (tpm2_hash_algorithms[i] == TPM2_ALG_SHA256) + return i; + + assert_not_reached(); +} + +/* Independently reconstruct the balanced PolicyOR tree the implementation is supposed to build, using only + * flat ORs (n <= 8) for each group. This pins down the group sizes and their order. */ +static int policy_or_tree_reference(const TPM2B_DIGEST *leaves, size_t n, TPM2B_DIGEST *ret) { + _cleanup_free_ TPM2B_DIGEST *level = NULL; + size_t n_level = n; + + assert_se(leaves); + assert_se(n > 0); + assert_se(ret); + + level = newdup(TPM2B_DIGEST, leaves, n); + if (!level) + return -ENOMEM; + + /* Collapse one level at a time until a single flat OR (n <= 8) remains. */ + while (n_level > TPM2_POLICY_OR_MAX_BRANCHES) { + size_t g = DIV_ROUND_UP(n_level, TPM2_POLICY_OR_MAX_BRANCHES), + base = n_level / g, rem = n_level % g; + _cleanup_free_ TPM2B_DIGEST *next = NULL; + + next = new0(TPM2B_DIGEST, g); + if (!next) + return -ENOMEM; + + for (size_t i = 0, off = 0; i < g; i++) { + size_t size = base + (i < rem); + int r; + + assert_se(size >= 2); /* No degenerate single-branch ORs */ + assert_se(size <= TPM2_POLICY_OR_MAX_BRANCHES); + + next[i].size = SHA256_DIGEST_SIZE; + r = tpm2_calculate_policy_or(level + off, size, &next[i]); + if (r < 0) + return r; + + off += size; + } + + free_and_replace(level, next); + n_level = g; + } + + return tpm2_calculate_policy_or(level, n_level, ret); +} + +TEST(calculate_policy_or) { + TPM2B_DIGEST leaves[65], reference, actual; + + for (size_t i = 0; i < ELEMENTSOF(leaves); i++) + fill_indexed_digest(leaves + i, i); + + /* The empty input must be rejected before the flat/tree split. */ + actual = (TPM2B_DIGEST) { .size = SHA256_DIGEST_SIZE }; + ASSERT_ERROR(tpm2_calculate_policy_or(leaves, 0, &actual), EINVAL); + + /* For n<=8 the flat form must be used unchanged; for n>8 the result must match the balanced tree + * reference implementation. */ + const size_t ns[] = { 1, 2, 5, 8, 9, 16, 17, 64, 65 }; + FOREACH_ELEMENT(n, ns) { + reference = (TPM2B_DIGEST) { .size = SHA256_DIGEST_SIZE }; + ASSERT_OK(policy_or_tree_reference(leaves, *n, &reference)); + + actual = (TPM2B_DIGEST) { .size = SHA256_DIGEST_SIZE }; + ASSERT_OK(tpm2_calculate_policy_or(leaves, *n, &actual)); + + ASSERT_EQ(actual.size, reference.size); + ASSERT_EQ(memcmp(actual.buffer, reference.buffer, actual.size), 0); + + /* Pin the flat form and one nested result to the on-disk format, independently of the + * shared implementation. */ + if (*n == 8) + ASSERT_TRUE(digest_check(&actual, "338aff94ac60da78125690c17895df113c5598f65d500dc028fca20feeaa5ed5")); + if (*n == 9) + ASSERT_TRUE(digest_check(&actual, "806fed0eaea4bd5a7cba19d21781c4d1e099e80607f76814d46ac0812c623312")); + } + + /* Branch order is significant. */ + TPM2B_DIGEST reversed[9]; + for (size_t i = 0; i < ELEMENTSOF(reversed); i++) + reversed[i] = leaves[ELEMENTSOF(reversed) - 1 - i]; + + ASSERT_OK(tpm2_calculate_policy_or(leaves, ELEMENTSOF(reversed), &actual)); + ASSERT_OK(tpm2_calculate_policy_or(reversed, ELEMENTSOF(reversed), &reference)); + ASSERT_EQ(actual.size, reference.size); + ASSERT_NE(memcmp(actual.buffer, reference.buffer, actual.size), 0); + + /* Depth cap: 8^TPM2_POLICY_OR_MAX_DEPTH branches are fine, one more is not. */ + size_t max_branches = 1; + for (size_t i = 0; i < TPM2_POLICY_OR_MAX_DEPTH; i++) + max_branches *= TPM2_POLICY_OR_MAX_BRANCHES; + + _cleanup_free_ TPM2B_DIGEST *many = new0(TPM2B_DIGEST, max_branches + 1); + ASSERT_NOT_NULL(many); + for (size_t i = 0; i < max_branches + 1; i++) + fill_indexed_digest(many + i, i); + + actual = (TPM2B_DIGEST) { .size = SHA256_DIGEST_SIZE }; + ASSERT_OK(tpm2_calculate_policy_or(many, max_branches, &actual)); + ASSERT_ERROR(tpm2_calculate_policy_or(many, max_branches + 1, &actual), E2BIG); +} + +TEST(tpm2_rc_is_policy_or_no_match) { + /* How the TCG reference implementation reports "no digest in pHashList matched": TPM_RC_VALUE on + * parameter 1. That is the one response the PolicyOR helpers may translate into ENOANO. */ + ASSERT_TRUE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_VALUE + TPM2_RC_P + TPM2_RC_1)); + ASSERT_TRUE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_VALUE + TPM2_RC_P)); + + /* TPM_RC_S shares bit 11 with the top bit of N, so a session-related TPM_RC_VALUE must not read as a + * branch mismatch - which is exactly what masking N away would do. */ + ASSERT_FALSE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_VALUE + TPM2_RC_S + TPM2_RC_1)); + ASSERT_FALSE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_VALUE + TPM2_RC_P + (UINT32_C(8) << 8))); + + /* Not pHashList. */ + ASSERT_FALSE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_VALUE + TPM2_RC_P + TPM2_RC_2)); + + /* Handle-related rather than parameter-related, i.e. P is clear. Format-zero codes have bit 8 set + * (TPM_RC_VER1 == TPM_RC_1), so requiring P also keeps those out. */ + ASSERT_FALSE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_VALUE + TPM2_RC_1)); + ASSERT_FALSE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_COMMAND_CODE)); + + /* Right parameter, different error number. */ + ASSERT_FALSE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_SIZE + TPM2_RC_P + TPM2_RC_1)); + ASSERT_FALSE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_HASH + TPM2_RC_P + TPM2_RC_1)); + ASSERT_FALSE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_HANDLE + TPM2_RC_P + TPM2_RC_1)); + ASSERT_FALSE(TPM2_RC_IS_POLICY_OR_NO_MATCH(TPM2_RC_SUCCESS)); +} + +TEST(calculate_policy_super_pcr_too_many_variants) { + /* A PCR with more alternatives than the depth cap allows must be rejected per PCR, with E2BIG, + * rather than surfacing as a generic PolicyOR tree failure somewhere down the call chain. */ + size_t max_branches = 1; + for (size_t i = 0; i < TPM2_POLICY_OR_MAX_DEPTH; i++) + max_branches *= TPM2_POLICY_OR_MAX_BRANCHES; + + _cleanup_(tpm2_pcr_prediction_done) Tpm2PCRPrediction prediction = { + .pcrs = UINT32_C(1) << 16, + }; + + for (size_t i = 0; i < max_branches + 1; i++) { + _cleanup_free_ Tpm2PCRPredictionResult *result = new0(Tpm2PCRPredictionResult, 1); + ASSERT_NOT_NULL(result); + + fill_indexed_digest(result->hash + sha256_index(), i); + + ASSERT_OK(ordered_set_ensure_put( + &prediction.results[16], + &tpm2_pcr_prediction_result_hash_ops, + result)); + TAKE_PTR(result); + } + + ASSERT_EQ(ordered_set_size(prediction.results[16]), max_branches + 1); + + TPM2B_DIGEST digest = { .size = SHA256_DIGEST_SIZE }; + ASSERT_ERROR(tpm2_calculate_policy_super_pcr(&prediction, TPM2_ALG_SHA256, &digest), E2BIG); +} + static void check_srk_rsa_template(TPMT_PUBLIC *template) { assert_se(template->type == TPM2_ALG_RSA); assert_se(template->nameAlg == TPM2_ALG_SHA256); @@ -2905,6 +3091,117 @@ static void check_get_session_audit_digest(Tpm2Context *c) { check_attest_signature(&template, sig); } +/* Exercise the runtime side of the balanced PolicyOR tree: a freshly started policy session has an all-zero + * policy digest, so make that one of the branches and add enough further ones to push tpm2_policy_or() onto + * the nested code path. Its result must match the offline calculation. */ +static void check_policy_or_tree_runtime(Tpm2Context *c) { + assert(c); + + TEST_LOG_FUNC(); + + _cleanup_(tpm2_handle_freep) Tpm2Handle *session = NULL; + ASSERT_OK(tpm2_make_policy_session( + c, + /* primary= */ NULL, + /* encryption_session= */ NULL, + &session)); + + TPM2B_DIGEST branches[65] = {}; + for (size_t i = 0; i < ELEMENTSOF(branches); i++) + fill_indexed_digest(branches + i, i + 1); + /* Index 60 maps to a non-zero group on the first two levels, so the nested walk's group remapping is + * exercised against the TPM. */ + zero(branches[60].buffer); + + _cleanup_(Esys_Freep) TPM2B_DIGEST *tpm_digest = NULL; + ASSERT_OK(tpm2_policy_or(c, session, branches, ELEMENTSOF(branches), &tpm_digest)); + + TPM2B_DIGEST calculated = { .size = SHA256_DIGEST_SIZE }; + ASSERT_OK(tpm2_calculate_policy_or(branches, ELEMENTSOF(branches), &calculated)); + + ASSERT_EQ(tpm_digest->size, calculated.size); + ASSERT_EQ(memcmp(tpm_digest->buffer, calculated.buffer, calculated.size), 0); + + /* Another fresh session again starts from an all-zero digest. If none of the branches matches it, + * the leaf lookup must fail with ENOANO, which pcrlock relies on to detect tampering. */ + session = tpm2_handle_free(session); + ASSERT_OK(tpm2_make_policy_session( + c, + /* primary= */ NULL, + /* encryption_session= */ NULL, + &session)); + + TPM2B_DIGEST no_match[65]; + for (size_t i = 0; i < ELEMENTSOF(no_match); i++) + fill_indexed_digest(no_match + i, i + 1); + + _cleanup_(Esys_Freep) TPM2B_DIGEST *no_match_digest = NULL; + ASSERT_ERROR(tpm2_policy_or(c, session, no_match, ELEMENTSOF(no_match), &no_match_digest), ENOANO); +} + +/* Same for the super PCR policy, which chains a PolicyPCR and a (possibly nested) PolicyOR per PCR. We + * submit the policy to a live session and read the resulting digest back by extending it once more with + * PolicyAuthValue, which is a pure function of the previous digest. */ +static void check_policy_super_pcr_tree(Tpm2Context *c) { + assert(c); + + TEST_LOG_FUNC(); + + /* Read the current value of PCR 16 ("debug"), so the prediction covers the actual TPM state. */ + TPML_PCR_SELECTION selection; + tpm2_tpml_pcr_selection_from_mask(UINT32_C(1) << 16, TPM2_ALG_SHA256, &selection); + + _cleanup_free_ Tpm2PCRValue *pcr_values = NULL; + size_t n_pcr_values = 0; + ASSERT_OK(tpm2_pcr_read(c, &selection, &pcr_values, &n_pcr_values)); + ASSERT_EQ(n_pcr_values, 1u); + ASSERT_EQ(pcr_values[0].index, 16u); + ASSERT_EQ(pcr_values[0].hash, TPM2_ALG_SHA256); + + /* 65 alternatives for PCR 16, the first of which is the current value, so the PolicyOR tree is three + * levels deep. */ + _cleanup_(tpm2_pcr_prediction_done) Tpm2PCRPrediction prediction = { + .pcrs = UINT32_C(1) << 16, + }; + + for (size_t i = 0; i < 65; i++) { + _cleanup_free_ Tpm2PCRPredictionResult *result = new0(Tpm2PCRPredictionResult, 1); + ASSERT_NOT_NULL(result); + + if (i == 0) + result->hash[sha256_index()] = pcr_values[0].value; + else + fill_indexed_digest(&result->hash[sha256_index()], i + 1); + + ASSERT_OK(ordered_set_ensure_put( + &prediction.results[16], + &tpm2_pcr_prediction_result_hash_ops, + result)); + TAKE_PTR(result); + } + + ASSERT_EQ(ordered_set_size(prediction.results[16]), 65u); + + /* Compute the digest the unsealing side pins. */ + TPM2B_DIGEST expected = { .size = SHA256_DIGEST_SIZE }; + ASSERT_OK(tpm2_calculate_policy_super_pcr(&prediction, TPM2_ALG_SHA256, &expected)); + + _cleanup_(tpm2_handle_freep) Tpm2Handle *session = NULL; + ASSERT_OK(tpm2_make_policy_session( + c, + /* primary= */ NULL, + /* encryption_session= */ NULL, + &session)); + ASSERT_OK(tpm2_policy_super_pcr(c, session, &prediction, TPM2_ALG_SHA256)); + + _cleanup_(Esys_Freep) TPM2B_DIGEST *tpm_digest = NULL; + ASSERT_OK(tpm2_policy_auth_value(c, session, &tpm_digest)); + ASSERT_OK(tpm2_calculate_policy_auth_value(&expected)); + + ASSERT_EQ(tpm_digest->size, expected.size); + ASSERT_EQ(memcmp(tpm_digest->buffer, expected.buffer, expected.size), 0); +} + TEST_RET(tests_which_require_tpm) { _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; int r = 0; @@ -2932,6 +3229,8 @@ TEST_RET(tests_which_require_tpm) { check_nv_certify(c); check_certify(c); check_get_session_audit_digest(c); + check_policy_or_tree_runtime(c); + check_policy_super_pcr_tree(c); #if HAVE_OPENSSL r = check_calculate_seal(c); diff --git a/test/units/TEST-70-TPM2.pcrlock.sh b/test/units/TEST-70-TPM2.pcrlock.sh index 61018cd942f..1109b068300 100755 --- a/test/units/TEST-70-TPM2.pcrlock.sh +++ b/test/units/TEST-70-TPM2.pcrlock.sh @@ -46,7 +46,7 @@ at_exit() { "$SD_PCRLOCK" unlock-raw --pcrlock=/var/lib/pcrlock.d/920-test70.pcrlock fi - rm -rf /tmp/fakexbootldr /var/lib/pcrlock.d/123-empty.pcrlock.d /run/systemd/system/systemd-pcrlock.socket.d + rm -rf /tmp/fakexbootldr /var/lib/pcrlock.d/123-empty.pcrlock.d /var/lib/pcrlock.d/930-many.pcrlock.d /run/systemd/system/systemd-pcrlock.socket.d if [[ -n "${img:-}" ]]; then rm -f "$img" "$img".private.pem "$img".public.pem "$img".pcrsign fi @@ -196,6 +196,30 @@ echo -n test70-take-two | "$SD_PCRLOCK" lock-raw --pcrlock=/var/lib/pcrlock.d/92 systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json,headless systemd-cryptsetup detach pcrlock +# Now exercise the PolicyOR tree code path: more than 8 alternatives for a PCR. Add a component with 9 +# variants touching PCR 16, rebuild the policy (the current PCR 16 state still matches the policy generated +# above, so the recovery PIN is not needed), then extend PCR 16 with the 9th variant and unlock again. A +# flat PolicyOR could only hold 8 of these. The 9th variant is in the second group, so the nested walk also +# has to submit a group slice with a non-zero start offset. +mkdir -p /var/lib/pcrlock.d/930-many.pcrlock.d +for i in $(seq -w 1 9); do + echo -n "test70-many-$i" | "$SD_PCRLOCK" lock-raw \ + --pcrlock="/var/lib/pcrlock.d/930-many.pcrlock.d/${i}.pcrlock" --pcr=16 +done + +"$SD_PCRLOCK" predict --pcr="$PCRS" + +# The extra variants for PCR 16 must actually end up in the prediction, otherwise this block silently stops +# covering the nested PolicyOR code path. +"$SD_PCRLOCK" predict --json=short --pcr="$PCRS" | \ + jq -e '[to_entries[] | .value[] | select(.pcr == 16) | .values | length] | length > 0 and all(. > 8)' >/dev/null + +"$SD_PCRLOCK" make-policy --pcr="$PCRS" --force + +"$SD_PCREXTEND" --pcr=16 test70-many-9 +systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json,headless +systemd-cryptsetup detach pcrlock + # Now combined pcrlock and signed PCR # Generate key pair openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "$img".private.pem