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);