tpm2-util: support more than 8 branches in PolicyOR expressions (#43982)

TPM2_PolicyOR() takes at most eight branch digests (the TPM's
TPML_DIGEST is fixed at eight elements). pcrlock hits this limit as soon
as a PCR has more than eight predicted values, for example when several
kernels or boot loader versions are covered by a single policy. It then
fails with E2BIG.

Lift the limit by nesting PolicyORs into a balanced tree with a fanout
of eight, as the TCG TSS JSON policy format prescribes for more than
eight branches: the leaves are the actual branches and every layer above
them contains only PolicyORs, so all leaves end up at the same depth.
Each level is partitioned evenly, which avoids pointless single-branch
ORs.

The tree is materialized by a helper shared between the calculating
(tpm2_calculate_policy_or()) and the submitting (tpm2_policy_or()) side,
so sealing and unsealing derive the exact same digests from the same
branch list. For eight or fewer branches the previous flat code path is
used unchanged, which keeps existing policies and sealed PINs working.

Every extra level costs one more TPM round trip during unseal, hence the
nesting depth is capped at TPM2_POLICY_OR_MAX_DEPTH (five, i.e. up to
8^5 = 32768 alternatives per PCR). If that's not enough for SUSE, we can
discuss it :-) cc @aplanas

This also drops the hard "more than 8 alternatives per PCR" rejection
from the super PCR policy calculation and submission.

I was not able to run the integration testing yet with mkosi due to some
NixOS stuff, sorry!
This commit is contained in:
Paul Meyer authored and GitHub committed 2026-11-03 08:52:55 +01:00
commit 257be02c20
7 files changed
+657 -57

No files matched your search

-1
View File
@@ -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
+4
View File
@@ -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 <function>PolicyAuthorizeNV</function> policy).</para>
<para>If a single PCR has more than eight alternative values, the <function>PolicyOR</function> items are
nested into a balanced tree. The current implementation supports at most 5 nesting levels, i.e. up to
32768 alternatives per PCR.</para>
<para>Use tools such as
<citerefentry><refentrytitle>systemd-cryptenroll</refentrytitle><manvolnum>1</manvolnum></citerefentry>
or <citerefentry><refentrytitle>systemd-repart</refentrytitle><manvolnum>8</manvolnum></citerefentry> to
+5
View File
@@ -68,6 +68,11 @@
component. Example: <filename>650-kernel.pcrlock.d/6.5.5-200.fc38.x86_64.pcrlock</filename> and
<filename>650-kernel.pcrlock.d/6.5.7-100.fc38.x86_64.pcrlock</filename></para>
<para>Components may have any number of variants. If a PCR ends up with more than eight alternative
values, <command>systemd-pcrlock</command> expresses them in the TPM2 access policy as a balanced tree of
nested <function>PolicyOR</function> statements. Up to 8^5 = 32768 alternative values per PCR are
supported.</para>
<para>Use <command>systemd-pcrlock list-components</command> to list all pcrlock files currently
installed.</para>
+297 -55
View File
@@ -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, &current);
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;
+27
View File
@@ -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);
+299
View File
@@ -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);
+25 -1
View File
@@ -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