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