tpm2-util: fall back to a PCR policy for NvPCR initialization

If the booted UKI carries no PCR public key or no signed policy with
the "initrd" reference, initialize NvPCRs in the initrd with a
PolicyPCR init policy bound to the current value of PCR 11 instead of
not initializing them at all.

The resulting NvPCR names depend on PCR 11 and hence change with every
UKI update, so this is only useful where PCR 11 can be predicted.

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
This commit is contained in:
Paul Meyer committed 2026-10-13 20:41:53 +02:00
1 parent 13c417c1fa
commit 7d37482ce4
6 files changed
+338 -136

No files matched your search

+10
View File
@@ -58,6 +58,16 @@ CHANGES WITH 263 in spe:
available, for example because they ship new or changed transfer
files. systemd-sysupdate-update.service now uses --loop=16.
Changes in the TPM Subsystem:
* If the booted UKI carries no PCR public key or no signed PCR policy
with the "initrd" policy reference, NvPCRs are now initialized in the
initrd with a write policy bound to the current value of PCR 11,
instead of not being initialized at all. The names of such NvPCRs,
which are measured into PCR 9, change whenever the UKI changes, so
this is mostly useful where the value of PCR 11 in the initrd is
predictable.
CHANGES WITH 262:
Announcements of Future Feature Removals and Incompatible Changes:
+14
View File
@@ -106,6 +106,20 @@ policy or different attributes that permit initialization during runtime can be
detected because the name of the NV index will not match the name that was
previously measured to PCR 9.
If the booted UKI carries no PCR public key, or no signed PCR policy with the
`initrd` policy reference for it, the branch for the first write is instead a
`TPM2_PolicyPCR` assertion bound to the value of the kernel boot PCR (11) at the
time the NvPCR is initialized. This fallback is only used in the initrd, where
PCR 11 has a value that can no longer be reached once the initrd has been left.
The same considerations as above apply, but the name of the NvPCR then depends
on the value of PCR 11 in the initrd, and hence changes whenever the UKI changes.
A verifier has to calculate the expected name from the expected value of PCR 11
in the initrd (e.g. as calculated by `systemd-measure calculate
--phase=enter-initrd`). This is hence only useful where that value can be
predicted. Note that on systems that are not booted via a UKI, PCR 11 does not
cover the kernel and initrd, and the name of the NvPCR hence does not depend on
them.
## PCR Measurements Made by `systemd-boot` (UEFI)
### PCR 1, `EV_EVENT_TAG`, SMBIOS information
+4 -2
View File
@@ -34,8 +34,10 @@
<para><filename>systemd-tpm2-setup.service</filename> and
<filename>systemd-tpm2-setup-early.service</filename> are services that generate the Storage Root Key
(SRK) and Endorsement Key (EK) if they have not been generated yet, and stores them in the TPM. If
NvPCRs (additional PCR registers in TPM NV Indexes) are defined, these are initialized with the
anchoring secret.</para>
NvPCRs (additional PCR registers in TPM NV Indexes) are defined, these are initialized. NvPCRs can only
be initialized from the initrd. Their initial write is authorized by the signed PCR policy with the
<literal>initrd</literal> policy reference embedded in the booted UKI, or, if there is none, by a PCR
policy bound to the current value of PCR 11.</para>
<para>The services will store the public key of the SRK key pair in a PEM file in
<filename>/run/systemd/tpm2-srk-public-key.pem</filename> and
+4 -8
View File
@@ -365,8 +365,10 @@ static int extend_nvpcr_now(
secret,
event,
safe);
if (IN_SET(r, -ENOBUFS, -ENETDOWN))
return r; /* NV space exhausted or NvPCR not initialized; let caller handle gracefully */
if (r == -ENOBUFS)
return r; /* NV space exhausted; let caller handle gracefully */
if (r == -ENETDOWN)
return log_error_errno(r, "NvPCR '%s' is not initialized, cannot extend it.", name);
if (r < 0)
return log_error_errno(r, "Could not extend NvPCR: %m");
@@ -599,12 +601,6 @@ static int run(int argc, char *argv[]) {
log_notice_errno(r, "TPM2 cannot be used for measurement (no usable PCR bank, missing device, or missing crypto support), skipping gracefully.");
return EXIT_SUCCESS;
}
if (r == -ENETDOWN) {
/* The NvPCR couldn't be initialized in this boot (e.g. because the booted kernel image
* carries no suitable PCR signature), hence there's nothing to extend. */
log_notice_errno(r, "NvPCR '%s' is not initialized, skipping gracefully.", arg_nvpcr_name);
return EXIT_SUCCESS;
}
if (r == -ENOBUFS) {
log_notice_errno(r, "TPM NV index space is exhausted, NvPCR '%s' could not be initialized, skipping gracefully.", arg_nvpcr_name);
return EXIT_SUCCESS;
+213 -93
View File
@@ -7997,7 +7997,7 @@ static int tpm2_reuse_or_redefine_nvpcr_nv_index(
"Failed to acquire handle to NV index 0x%" PRIx32 ".", nv_index);
/* Check if the existing index has the same name as the one we're trying to define. We expect
* this on any system where this NvPCR was created with the current pubkey on a previous
* this on any system where this NvPCR was created with the same init policy on a previous
* boot, and it should always be uninitialized (ie, TPMA_NV_WRITTEN unset) at this point. */
TPM2B_NAME nv_name_expected;
r = tpm2_calculate_nv_index_name(&nv_public_info->nvPublic, &nv_name_expected);
@@ -8022,11 +8022,12 @@ static int tpm2_reuse_or_redefine_nvpcr_nv_index(
"free sufficient space to allocate the NvPCR.", nv_index);
/* Check if the existing index looks like it might have been a NvPCR - it might have a
* policy that's different because the signing key for the PolicyAuthorize assertion changed,
* or it might be an old style NvPCR. In this case, we'll undefine it and create a new one.
* This handles updates from old style NvPCRs, and rotation of the authorized policy signing
* key. We just check the attributes here - we know that the nvIndex is the same, and the
* dataSize for a valid extend index matches the size of nameAlg so we don't need to check that. */
* policy that's different because its init policy changed (the signing key for the
* PolicyAuthorize assertion was rotated, the PCR value bound by a PolicyPCR init policy changed,
* or the init policy switched between the two), or it might be an old style NvPCR. In this case,
* we'll undefine it and create a new one. We just check the attributes here - we know that the
* nvIndex is the same, and the dataSize for a valid extend index matches the size of nameAlg so
* we don't need to check that. */
const TPMA_NV old_expected_attrs =
TPMA_NV_CLEAR_STCLEAR |
TPMA_NV_OWNERWRITE |
@@ -8079,6 +8080,11 @@ static int tpm2_reuse_or_redefine_nvpcr_nv_index(
return 1;
}
/* Flag files marking that the TPM's NV index space was found exhausted in this boot, separately for orderly
* and non-orderly NvPCRs. */
#define NVPCR_SPACE_EXHAUSTED_FLAG_ORDERLY "/run/systemd/tpm2-nv-space-exhausted-orderly"
#define NVPCR_SPACE_EXHAUSTED_FLAG_NON_ORDERLY "/run/systemd/tpm2-nv-space-exhausted-non-orderly"
static int tpm2_define_nvpcr_nv_index(
Tpm2Context *c,
const Tpm2Handle *session,
@@ -8120,8 +8126,8 @@ static int tpm2_define_nvpcr_nv_index(
* file in /run/, with a separate file for orderly and non-orderly NvPCRs, since the two draw on
* different TPM resources (RAM-backed vs. NVRAM-backed). */
const char *exhausted_flag = orderly ?
"/run/systemd/tpm2-nv-space-exhausted-orderly" :
"/run/systemd/tpm2-nv-space-exhausted-non-orderly";
NVPCR_SPACE_EXHAUSTED_FLAG_ORDERLY :
NVPCR_SPACE_EXHAUSTED_FLAG_NON_ORDERLY;
/* Calculate the write policy, which is init_policy OR TPM2_PolicyNvWritten(true). The init_policy
* branch is used for the first write and can only be satisfied during early boot. */
@@ -9107,6 +9113,14 @@ static int tpm2_nvpcr_read_init_policy(const char *name, TPM2B_DIGEST *ret) {
return 0;
}
/* The kind of policy guarding the initializing write to an NvPCR. */
typedef enum NvPCRInitPolicyType {
NVPCR_INIT_POLICY_SIGNED,
NVPCR_INIT_POLICY_PCR,
_NVPCR_INIT_POLICY_MAX,
_NVPCR_INIT_POLICY_INVALID = -EINVAL,
} NvPCRInitPolicyType;
/* Policy reference for the PolicyAuthorize assertion that guards the first write to an NvPCR. */
#define NVPCR_INIT_POLICY_REF "initrd"
@@ -9116,8 +9130,11 @@ static int tpm2_nvpcr_read_init_policy(const char *name, TPM2B_DIGEST *ret) {
/* The init policy of an NvPCR, i.e. the branch of its write policy that guards the initializing write,
* together with everything needed to satisfy it. */
typedef struct NvPCRInitPolicy {
NvPCRInitPolicyType type;
TPM2B_DIGEST digest;
TPML_PCR_SELECTION pcr_selection;
/* type NVPCR_INIT_POLICY_SIGNED only */
TPM2B_PUBLIC public;
struct iovec fingerprint;
sd_json_variant *signature_json;
@@ -9168,27 +9185,40 @@ static int tpm2_nvpcr_open_init_session(
assert(c);
assert(init);
assert(iovec_is_set(&init->fingerprint));
assert(init->signature_json);
assert(ret_session);
r = tpm2_make_policy_session(c, /* primary= */ NULL, /* encryption_session= */ NULL, &session);
if (r < 0)
return r;
/* tpm2_policy_authorize() takes a non-const selection. */
pcr_selection = init->pcr_selection;
switch (init->type) {
case NVPCR_INIT_POLICY_SIGNED:
assert(iovec_is_set(&init->fingerprint));
assert(init->signature_json);
/* Satisfy the PolicyAuthorize branch using the signed PCR policy. */
r = tpm2_policy_authorize(
c,
session,
&pcr_selection,
&init->public,
NVPCR_INIT_POLICY_REF,
init->fingerprint.iov_base, init->fingerprint.iov_len,
init->signature_json,
/* ret_policy_digest= */ NULL);
/* tpm2_policy_authorize() takes a non-const selection. */
pcr_selection = init->pcr_selection;
/* Satisfy the PolicyAuthorize branch using the signed PCR policy. */
r = tpm2_policy_authorize(
c,
session,
&pcr_selection,
&init->public,
NVPCR_INIT_POLICY_REF,
init->fingerprint.iov_base, init->fingerprint.iov_len,
init->signature_json,
/* ret_policy_digest= */ NULL);
break;
case NVPCR_INIT_POLICY_PCR:
/* Satisfy the PolicyPCR branch with the current PCR values. */
r = tpm2_policy_pcr(c, session, &init->pcr_selection, /* ret_policy_digest= */ NULL);
break;
default:
assert_not_reached();
}
if (r < 0)
return r;
@@ -9351,35 +9381,6 @@ static int nvpcr_extend_bytes(
#endif
}
int tpm2_nvpcr_extend_bytes(
Tpm2Context *c,
const Tpm2Handle *session,
const char *name,
const struct iovec *data,
const struct iovec *secret,
Tpm2UserspaceEventType event_type,
const char *description) {
int r;
r = nvpcr_extend_bytes(c, session, name, data, secret, event_type, description);
if (r != -ENETDOWN)
return r;
/* The NvPCR isn't initialized yet, i.e. systemd-tpm2-setup hasn't run.
* Initialize it now and extend again. This can only run in the initrd. */
if (!in_initrd())
return log_debug_errno(r, "NvPCR is not initialized and lazy initialization is only available in the initrd, refusing.");
r = tpm2_nvpcr_initialize(c, session, name);
if (r == -ENOKEY) /* Can't be initialized in this boot, report it as not initialized. */
return -ENETDOWN;
if (r < 0)
return log_debug_errno(r, "Failed to initialize NvPCR '%s': %m", name);
return nvpcr_extend_bytes(c, session, name, data, secret, event_type, description);
}
#if HAVE_OPENSSL
static int tpm2_context_can_nvindex(Tpm2Context *c) {
int r;
@@ -9418,6 +9419,47 @@ static int tpm2_context_can_nvindex(Tpm2Context *c) {
}
#endif
int tpm2_nvpcr_extend_bytes(
Tpm2Context *c,
const Tpm2Handle *session,
const char *name,
const struct iovec *data,
const struct iovec *secret,
Tpm2UserspaceEventType event_type,
const char *description) {
int r;
r = nvpcr_extend_bytes(c, session, name, data, secret, event_type, description);
if (r != -ENETDOWN)
return r;
/* The NvPCR isn't initialized yet, i.e. systemd-tpm2-setup hasn't run.
* Initialize it now and extend again. This can only run in the initrd. */
if (!in_initrd()) {
/* systemd-tpm2-setup skips NvPCRs if the TPM doesn't support them or ran out of NV index
* space, report these cases the same way instead of as a missing initialization. */
#if HAVE_OPENSSL
if (tpm2_context_can_nvindex(c) == 0)
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"TPM2 device does not support NvPCRs, NvPCR '%s' is not initialized.", name);
if (access(NVPCR_SPACE_EXHAUSTED_FLAG_ORDERLY, F_OK) >= 0 ||
access(NVPCR_SPACE_EXHAUSTED_FLAG_NON_ORDERLY, F_OK) >= 0)
return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS),
"TPM NV index space was exhausted in this boot, NvPCR '%s' is not initialized.", name);
#endif
return log_debug_errno(r, "NvPCR is not initialized and lazy initialization is only available in the initrd, refusing.");
}
r = tpm2_nvpcr_initialize(c, session, name);
if (r < 0)
return log_debug_errno(r, "Failed to initialize NvPCR '%s': %m", name);
return nvpcr_extend_bytes(c, session, name, data, secret, event_type, description);
}
/* The maximum number of times to try initializing a NvPCR before failing if PCR values change under our feet. */
#define RETRY_NVPCR_INIT_MAX 30u
@@ -9445,6 +9487,104 @@ static int tpm2_nvpcr_calculate_signed_init_policy(
*ret = init_policy;
return 0;
}
/* Calculates the init policy for an NvPCR protected by the current PCR values:
* PolicyPCR over 'sel' with the values read from the TPM now. */
static int tpm2_nvpcr_calculate_pcr_init_policy(
Tpm2Context *c,
const TPML_PCR_SELECTION *sel,
TPM2B_DIGEST *ret) {
_cleanup_free_ Tpm2PCRValue *values = NULL;
size_t n;
TPM2B_DIGEST init_policy;
int r;
assert(c);
assert(sel);
assert(ret);
r = tpm2_pcr_read(c, sel, &values, &n);
if (r < 0)
return r;
if (n != 1)
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"TPM returned no value for the selected PCR, cannot calculate NvPCR init policy.");
init_policy = TPM2B_DIGEST_MAKE(NULL, SHA256_DIGEST_SIZE);
r = tpm2_calculate_policy_pcr(values, n, &init_policy);
if (r < 0)
return r;
tpm2_log_debug_pcr_value(&values[0], "PCR value for NvPCR init policy");
*ret = init_policy;
return 0;
}
/* Loads the signed init policy of NvPCR 'name' from the booted UKI. Returns -ENOKEY if no PCR public key,
* signature file, or "initrd" signature for the key is available, in which case 'ret' is left untouched. */
static int tpm2_nvpcr_load_signed_init_policy(
const char *name,
const TPML_PCR_SELECTION *pcr_selection,
NvPCRInitPolicy *ret) {
_cleanup_(nvpcr_init_policy_done) NvPCRInitPolicy p = {
.type = NVPCR_INIT_POLICY_SIGNED,
};
int r;
assert(name);
assert(pcr_selection);
assert(ret);
p.pcr_selection = *pcr_selection;
/* Load the PCR public key. Its signatures authorize the initializing write to the NvPCR via the
* PolicyAuthorize branch of the write policy. */
r = tpm2_nvpcr_load_pcr_public_key(/* path= */ NULL, &p.public, &p.fingerprint);
if (r == -ENOENT)
/* Avoid failing systemd-tpm2-setup if there is no PCR public key attached to the UKI. */
return log_debug_errno(SYNTHETIC_ERRNO(ENOKEY),
"No PCR public key available, cannot initialize NvPCR '%s'.", name);
if (r < 0)
return r;
/* Load the signed PCR policy, which authorizes the initializing write. */
r = tpm2_load_pcr_signature(/* path= */ NULL, &p.signature_json);
if (r == -ENOENT)
/* Avoid failing systemd-tpm2-setup if there are no PCR signatures attached to the UKI. */
return log_debug_errno(SYNTHETIC_ERRNO(ENOKEY),
"No signed PCR policy available, cannot initialize NvPCR '%s'.", name);
if (r < 0)
return log_debug_errno(r, "Failed to load PCR signature for NvPCR initialization: %m");
/* Check that there's a signed policy for the initializing write at all before we allocate anything
* on the TPM. This avoids failing systemd-tpm2-setup if there are no policies signed for the "initrd"
* policy reference. We don't check the policy digest here: if there is a matching policy but it doesn't
* match the current PCR state, or its signature is invalid, then that's a real error which we
* want to report below. */
r = find_signature(
p.signature_json,
&p.pcr_selection,
p.fingerprint.iov_base, p.fingerprint.iov_len,
NVPCR_INIT_POLICY_REF,
/* policy= */ NULL, /* policy_size= */ 0,
/* ret_signature= */ NULL, /* ret_signature_size= */ NULL);
if (r == -ENOSTR)
return log_debug_errno(SYNTHETIC_ERRNO(ENOKEY),
"No signed PCR policy with reference '%s' available, cannot initialize NvPCR '%s'.",
NVPCR_INIT_POLICY_REF, name);
if (r < 0)
return r;
r = tpm2_nvpcr_calculate_signed_init_policy(&p.public, &p.digest);
if (r < 0)
return r;
*ret = TAKE_STRUCT(p);
return 0;
}
#endif
int tpm2_nvpcr_initialize(
@@ -9492,9 +9632,9 @@ int tpm2_nvpcr_initialize(
if (r < 0)
return r;
/* XXX: As NvPCRs use a write policy with PolicyAuthorize, the name algorithm and signed policy
* algorithm must match. Currently, signed policies are restricted to SHA256, so reject anything else
* for now. This can only be relaxed when:
/* XXX: The init policy of a NvPCR (the signed policy for PolicyAuthorize, the PCR digest for
* PolicyPCR) must use the name algorithm of the NV index. Currently, both are restricted to SHA256,
* so reject anything else for now. This can only be relaxed when:
* - The various tpm2_calculate_policy_* and tpm2_digest_* functions support more than SHA256.
* - Signed policies have a new policy algorithm field in the JSON metadata.
* - systemd-measure has a new argument to select the policy algorithm.
@@ -9517,47 +9657,21 @@ int tpm2_nvpcr_initialize(
return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Hash function result too large for TPM, refusing.");
_cleanup_(nvpcr_init_policy_done) NvPCRInitPolicy init = {};
TPML_PCR_SELECTION pcr_selection;
/* Load the PCR public key. Its signatures authorize the initializing write to the NvPCR via the
* PolicyAuthorize branch of the write policy. */
r = tpm2_nvpcr_load_pcr_public_key(/* path= */ NULL, &init.public, &init.fingerprint);
if (r == -ENOENT)
/* Avoid failing systemd-tpm2-setup if there is no PCR public key attached to the UKI. */
return log_debug_errno(SYNTHETIC_ERRNO(ENOKEY),
"No PCR public key available, cannot initialize NvPCR '%s'.", name);
if (r < 0)
return r;
tpm2_tpml_pcr_selection_from_mask(NVPCR_INIT_PCRMASK, TPM2_ALG_SHA256, &pcr_selection);
/* Load the signed PCR policy, which authorizes the initializing write. */
r = tpm2_load_pcr_signature(/* path= */ NULL, &init.signature_json);
if (r == -ENOENT)
/* Avoid failing systemd-tpm2-setup if there are no PCR signatures attached to the UKI. */
return log_debug_errno(SYNTHETIC_ERRNO(ENOKEY),
"No signed PCR policy available, cannot initialize NvPCR '%s'.", name);
if (r < 0)
return log_debug_errno(r, "Failed to load PCR signature for NvPCR initialization: %m");
/* Check that there's a signed policy for the initializing write at all before we allocate anything
* on the TPM. This avoids failing systemd-tpm2-setup if there are no policies signed for the "initrd"
* policy reference. We don't check the policy digest here: if there is a matching policy but it doesn't
* match the current PCR state, or its signature is invalid, then that's a real error which we
* want to report below. */
tpm2_tpml_pcr_selection_from_mask(NVPCR_INIT_PCRMASK, TPM2_ALG_SHA256, &init.pcr_selection);
r = find_signature(
init.signature_json,
&init.pcr_selection,
init.fingerprint.iov_base, init.fingerprint.iov_len,
NVPCR_INIT_POLICY_REF,
/* policy= */ NULL, /* policy_size= */ 0,
/* ret_signature= */ NULL, /* ret_signature_size= */ NULL);
if (r == -ENOSTR)
return log_debug_errno(SYNTHETIC_ERRNO(ENOKEY),
"No signed PCR policy with reference '%s' available, cannot initialize NvPCR '%s'.",
NVPCR_INIT_POLICY_REF, name);
if (r < 0)
return r;
r = tpm2_nvpcr_calculate_signed_init_policy(&init.public, &init.digest);
/* Prefer the signed init policy from the booted UKI. Without one, fall back to a PolicyPCR bound to
* the current PCR value, which is only reachable in the initrd, so only do that there. */
r = tpm2_nvpcr_load_signed_init_policy(name, &pcr_selection, &init);
if (r == -ENOKEY && in_initrd()) {
log_debug_errno(r, "No signed init policy for NvPCR '%s', falling back to PCR policy.", name);
init = (NvPCRInitPolicy) {
.type = NVPCR_INIT_POLICY_PCR,
.pcr_selection = pcr_selection,
};
r = tpm2_nvpcr_calculate_pcr_init_policy(c, &pcr_selection, &init.digest);
}
if (r < 0)
return r;
@@ -9580,6 +9694,12 @@ int tpm2_nvpcr_initialize(
/* Open a policy session to perform the initializing write. */
_cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL;
r = tpm2_nvpcr_open_init_session(c, &init, &policy_session);
if (r == -ENOANO && init.type == NVPCR_INIT_POLICY_PCR)
/* The index is bound to the old PCR value, so retrying can't help. The next attempt
* redefines it. */
return log_debug_errno(r,
"PCR %i changed since the init policy of NvPCR '%s' was calculated, refusing initialization.",
TPM2_PCR_KERNEL_BOOT, name);
if (r == -EUCLEAN) {
/* A PCR was extended while we submitted the policy, so this session is unusable.
* Same situation as below, just observed while building the policy rather than
+93 -33
View File
@@ -29,6 +29,7 @@ at_exit() {
rm -f /var/tmp/nvpcr.raw /run/verity.d/test-70-nvpcr.crt
rm -f /run/systemd/nvpcr/test.auth /run/systemd/nvpcr/test2.auth /run/systemd/nvpcr/aaa.auth /run/systemd/nvpcr/zzz.auth /run/systemd/nvpcr/unauth.auth /run/systemd/nvpcr/unauth2.auth
rm -f /tmp/test.policy
rm -f /run/systemd/tpm2-nv-space-exhausted-orderly
}
trap at_exit EXIT
@@ -157,11 +158,12 @@ AAA_LINE="$(echo "$SETUP_LOG" | grep -n "Setting up NvPCR 'aaa'" | cut -d: -f1)"
ZZZ_LINE="$(echo "$SETUP_LOG" | grep -n "Setting up NvPCR 'zzz'" | cut -d: -f1)"
test "$ZZZ_LINE" -lt "$AAA_LINE"
# Verify that systemd-tpm2-setup skips (rather than fails) NvPCR initialization if the booted kernel
# image carries no PCR public key or no signed PCR policy with the "initrd" reference, and that it
# doesn't allocate the NV index in that case. systemd-pcrextend must then treat the NvPCR as not
# Verify the handling of a booted kernel image that carries no PCR public key or no signed PCR policy
# with the "initrd" reference. Outside the initrd, systemd-tpm2-setup skips (rather than fails) NvPCR
# initialization and doesn't allocate the NV index, and systemd-pcrextend treats the NvPCR as not
# initialized. 'unauth2' sorts after 'unauth', so once 'unauth' couldn't be initialized it must be
# skipped without another initialization attempt.
# skipped without another initialization attempt. In the initrd, the NvPCRs are initialized with a
# PolicyPCR init policy bound to the current value of PCR 11 instead.
cat >/run/nvpcr/unauth.nvpcr <<EOF
{"name":"unauth","algorithm":"sha256","nvIndex":30474774}
EOF
@@ -170,59 +172,87 @@ cat >/run/nvpcr/unauth2.nvpcr <<EOF
EOF
cp /run/systemd/tpm2-pcr-signature.json /tmp/tpm2-pcr-signature.json.valid
# Takes the expected reason for skipping the initialization, as logged by tpm2_nvpcr_initialize().
check_nvpcr_unauth_skipped() {
# Reads a hex string from stdin and prints the SHA256 digest of the bytes it encodes.
sha256_hex() {
tr -d '\n' | tr '[:lower:]' '[:upper:]' | basenc --base16 -d | openssl dgst -sha256 -hex -r | cut -d' ' -f1
}
nv_index_policy() {
tpm2_nvreadpublic "${1:?}" | awk '/authorization policy:/{print tolower($3); exit}'
}
# Checks that the NV index carries the write policy of an NvPCR initialized with the PolicyPCR init
# policy for the current value of PCR 11, i.e. PolicyOR(PolicyPCR(sha256:11), PolicyNvWritten(YES)).
check_nvpcr_pcr_policy() {
local nv_index="${1:?}"
local zero pcr_policy written_policy
zero="$(printf '00%.0s' {1..32})"
# TPM2_CC_PolicyPCR, TPML_PCR_SELECTION for sha256:11, digest of the PCR value
pcr_policy="$(sha256_hex <<<"${zero}0000017F00000001000B03000800$(sha256_hex </sys/class/tpm/tpm0/pcr-sha256/11)")"
# TPM2_CC_PolicyNvWritten, YES
written_policy="$(sha256_hex <<<"${zero}0000018F01")"
# TPM2_CC_PolicyOR, init branch first
[[ "$(nv_index_policy "$nv_index")" == "$(sha256_hex <<<"${zero}00000171${pcr_policy}${written_policy}")" ]]
}
# Takes the expected reason for not using a signed init policy, as logged by tpm2_nvpcr_initialize().
check_nvpcr_unauth() {
local reason="${1:?}"
local expected="$reason, cannot initialize NvPCR 'unauth'."
local log rc=0
local log
# systemd-tpm2-setup returns EX_CONFIG if the NvPCR initialization was skipped for this reason
log="$(SYSTEMD_IN_INITRD=1 "$SD_TPM2SETUP" --early=yes 2>&1)" || rc=$?
[[ "$rc" -eq 78 ]]
grep -F "$expected" <<<"$log" >/dev/null
grep -F "Skipped 2 NvPCR(s) because NvPCR initialization is not authorized in this boot" <<<"$log" >/dev/null
grep -F "skipping allocation of NvPCR 'unauth2'." <<<"$log" >/dev/null
(! grep -F "cannot initialize NvPCR 'unauth2'" <<<"$log" >/dev/null)
test ! -f /run/systemd/nvpcr/unauth.auth
test ! -f /run/systemd/nvpcr/unauth2.auth
(! tpm2_nvreadpublic 0x01d10216)
(! tpm2_nvreadpublic 0x01d10217)
(! "$SD_PCREXTEND" --nvpcr=unauth foo)
"$SD_PCREXTEND" --graceful --nvpcr=unauth foo
(! "$SD_PCREXTEND" --graceful --nvpcr=unauth foo)
log="$(varlinkctl call /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend.Extend '{"nvpcr":"unauth","text":"foo"}' 2>&1 || :)"
grep -F "io.systemd.PCRExtend.NoSuchNvPCR" <<<"$log" >/dev/null
# In the initrd, systemd-pcrextend attempts to lazily initialize the NvPCR, which must fail in the
# same way and be reported as the NvPCR not being initialized.
rc=0
log="$(SYSTEMD_IN_INITRD=1 "$SD_PCREXTEND" --nvpcr=unauth foo 2>&1)" || rc=$?
[[ "$rc" -ne 0 ]]
# In the initrd, systemd-pcrextend lazily initializes the NvPCR with the PolicyPCR init policy, and
# anchors it in PCR 9 like any other NvPCR.
log="$(SYSTEMD_IN_INITRD=1 "$SD_PCREXTEND" --nvpcr=unauth foo 2>&1)"
grep -F "$expected" <<<"$log" >/dev/null
log="$(SYSTEMD_IN_INITRD=1 "$SD_PCREXTEND" --graceful --nvpcr=unauth foo 2>&1)"
grep -F "$expected" <<<"$log" >/dev/null
grep -F "NvPCR 'unauth' is not initialized, skipping gracefully." <<<"$log" >/dev/null
log="$(SYSTEMD_IN_INITRD=1 varlinkctl call /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend.Extend '{"nvpcr":"unauth","text":"foo"}' 2>&1 || :)"
grep -F "$expected" <<<"$log" >/dev/null
grep -F "io.systemd.PCRExtend.NoSuchNvPCR" <<<"$log" >/dev/null
test ! -f /run/systemd/nvpcr/unauth.auth
(! tpm2_nvreadpublic 0x01d10216)
grep -F "No signed init policy for NvPCR 'unauth', falling back to PCR policy." <<<"$log" >/dev/null
test -f /run/systemd/nvpcr/unauth.auth
check_nvpcr_pcr_policy 0x01d10216
log="$(jq --seq --slurp -r '[.[] | select(.content.eventType=="nvpcr-init") | .content.string] | last' </run/log/systemd/tpm2-measure.log)"
[[ "$log" == "nvpcr-init:unauth:0x1d10216:$(tpm2_nvreadpublic 0x01d10216 | awk '/name:/{print $2; exit}')" ]]
# Subsequent extends use the PolicyNvWritten branch, hence work outside the initrd too.
"$SD_PCREXTEND" --nvpcr=unauth foo
rm -f /run/systemd/nvpcr/unauth.auth
tpm2_nvundefine -C o 0x01d10216
# Same for systemd-tpm2-setup in the initrd, which initializes both NvPCRs.
SYSTEMD_IN_INITRD=1 run_tpm2_setup
test -f /run/systemd/nvpcr/unauth.auth
test -f /run/systemd/nvpcr/unauth2.auth
check_nvpcr_pcr_policy 0x01d10216
check_nvpcr_pcr_policy 0x01d10217
"$SD_PCREXTEND" --nvpcr=unauth foo
"$SD_PCREXTEND" --nvpcr=unauth2 foo
rm -f /run/systemd/nvpcr/unauth.auth /run/systemd/nvpcr/unauth2.auth
tpm2_nvundefine -C o 0x01d10216
tpm2_nvundefine -C o 0x01d10217
}
# No PCR public key
mv /run/systemd/tpm2-pcr-public-key.pem /tmp/tpm2-pcr-public-key.pem
check_nvpcr_unauth_skipped "No PCR public key available"
check_nvpcr_unauth "No PCR public key available"
mv /tmp/tpm2-pcr-public-key.pem /run/systemd/tpm2-pcr-public-key.pem
# No PCR signature file
rm -f /run/systemd/tpm2-pcr-signature.json
check_nvpcr_unauth_skipped "No signed PCR policy available"
check_nvpcr_unauth "No signed PCR policy available"
# PCR signature without a policy with the "initrd" reference
"$SD_MEASURE" sign --current --bank sha256 --private-key="/tmp/tpm2-pcr-private-key.pem" --public-key="/run/systemd/tpm2-pcr-public-key.pem" --phase=":" --policyref="foo" >"/run/systemd/tpm2-pcr-signature.json"
check_nvpcr_unauth_skipped "No signed PCR policy with reference 'initrd' available"
check_nvpcr_unauth "No signed PCR policy with reference 'initrd' available"
"$SD_MEASURE" sign --current --bank sha256 --private-key="/tmp/tpm2-pcr-private-key.pem" --public-key="/run/systemd/tpm2-pcr-public-key.pem" --phase=":" >"/run/systemd/tpm2-pcr-signature.json"
check_nvpcr_unauth_skipped "No signed PCR policy with reference 'initrd' available"
check_nvpcr_unauth "No signed PCR policy with reference 'initrd' available"
# Outside the initrd, NvPCR initialization must be skipped even with a valid PCR public key and signed
# PCR policy, as the policy can no longer be satisfied once the initrd has been left. This happens if
@@ -241,11 +271,17 @@ test ! -f /run/systemd/nvpcr/unauth2.auth
(! tpm2_nvreadpublic 0x01d10216)
(! tpm2_nvreadpublic 0x01d10217)
(! SYSTEMD_IN_INITRD=0 "$SD_PCREXTEND" --nvpcr=unauth foo)
SYSTEMD_IN_INITRD=0 "$SD_PCREXTEND" --graceful --nvpcr=unauth foo
(! SYSTEMD_IN_INITRD=0 "$SD_PCREXTEND" --graceful --nvpcr=unauth foo)
VARLINK_LOG="$(SYSTEMD_IN_INITRD=0 varlinkctl call /usr/lib/systemd/systemd-pcrextend io.systemd.PCRExtend.Extend '{"nvpcr":"unauth","text":"foo"}' 2>&1 || :)"
grep -F "io.systemd.PCRExtend.NoSuchNvPCR" <<<"$VARLINK_LOG" >/dev/null
test ! -f /run/systemd/nvpcr/unauth.auth
(! tpm2_nvreadpublic 0x01d10216)
# If the TPM's NV index space ran out in this boot, systemd-tpm2-setup skips the remaining NvPCRs
# gracefully, so systemd-pcrextend must do so as well.
touch /run/systemd/tpm2-nv-space-exhausted-orderly
(! SYSTEMD_IN_INITRD=0 "$SD_PCREXTEND" --nvpcr=unauth foo)
SYSTEMD_IN_INITRD=0 "$SD_PCREXTEND" --graceful --nvpcr=unauth foo
rm -f /run/systemd/tpm2-nv-space-exhausted-orderly
# A policy with the "initrd" reference but an invalid signature must still fail
jq --arg sig "$(openssl rand -base64 256 | tr -d '\n')" '.sha256[].sig = $sig' </tmp/tpm2-pcr-signature.json.valid >/run/systemd/tpm2-pcr-signature.json
@@ -256,10 +292,18 @@ test ! -f /run/systemd/nvpcr/unauth2.auth
tpm2_nvundefine -C o 0x01d10216
tpm2_nvundefine -C o 0x01d10217
# Leave behind an NV index initialized with the PolicyPCR init policy in a previous boot (i.e. without
# its .auth file), which must be redefined once a signed init policy is available.
rm -f /run/systemd/tpm2-pcr-signature.json
SYSTEMD_IN_INITRD=1 "$SD_PCREXTEND" --nvpcr=unauth foo
check_nvpcr_pcr_policy 0x01d10216
rm -f /run/systemd/nvpcr/unauth.auth
# And with the valid signature restored, the NvPCRs get initialized
cp /tmp/tpm2-pcr-signature.json.valid /run/systemd/tpm2-pcr-signature.json
run_tpm2_setup
test -f /run/systemd/nvpcr/unauth.auth
(! check_nvpcr_pcr_policy 0x01d10216)
test -f /run/systemd/nvpcr/unauth2.auth
"$SD_PCREXTEND" --nvpcr=unauth foo
"$SD_PCREXTEND" --nvpcr=unauth2 foo
@@ -274,6 +318,22 @@ tpm2_nvundefine -C o 0x01d1020a
SETUP_LOG="$(rc=0; SYSTEMD_IN_INITRD=1 $SD_TPM2SETUP --early=yes 2>&1 || rc=$?; [[ "$rc" -ne 0 ]] && [[ "$rc" -ne 69 ]] && [[ "$rc" -ne 78 ]])"
grep -F "Failed to initialize NvPCR index: Device not a stream" <<<"$SETUP_LOG" >/dev/null
# The PolicyPCR init policy is bound to the value of PCR 11 at initialization. Once PCR 11 changed, a new
# initialization redefines the NV index with a policy for the new value.
echo '{"name":"unauth","algorithm":"sha256","nvIndex":30474774}' >/run/nvpcr/unauth.nvpcr
rm -f /run/systemd/tpm2-pcr-signature.json
SYSTEMD_IN_INITRD=1 "$SD_PCREXTEND" --nvpcr=unauth foo
check_nvpcr_pcr_policy 0x01d10216
OLD_POLICY="$(nv_index_policy 0x01d10216)"
"$SD_PCREXTEND" --pcr 11 "foo"
rm -f /run/systemd/nvpcr/unauth.auth
SYSTEMD_IN_INITRD=1 "$SD_PCREXTEND" --nvpcr=unauth foo
check_nvpcr_pcr_policy 0x01d10216
[[ "$(nv_index_policy 0x01d10216)" != "$OLD_POLICY" ]]
cp /tmp/tpm2-pcr-signature.json.valid /run/systemd/tpm2-pcr-signature.json
rm -f /run/nvpcr/unauth.nvpcr /run/systemd/nvpcr/unauth.auth
tpm2_nvundefine -C o 0x01d10216
# Test the --login= mode and the 'login' NvPCR, used in production by systemd-pcrlogin@.service.
if [[ -f /usr/lib/nvpcr/login.nvpcr ]]; then
login_nvpcr_value() {