]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tpm2: Improve how NvPCR protection works.
authorChris Coulson <chris.coulson@amutable.com>
Tue, 23 Jun 2026 19:58:17 +0000 (20:58 +0100)
committerPaul Meyer <katexochen0@gmail.com>
Tue, 28 Jul 2026 06:35:39 +0000 (08:35 +0200)
NV indexes created in the storage hierarchy can be undefined and
redefined with TPM owner auth. Because of this, NvPCRs need some way
to prevent them from being redfined in a way that allows spoof
measurements to be replayed.

The current approach requires knowledge of a secret ("anchor secret")
in order to derive the initial NvPCR measurement and to derive a
measurement to an existing PCR (9). The credential is protected by the
TPM with a PCR policy. Without access to the credential, it's not
possible to replay measurements to a newly defined NvPCR without
breaking the binding with the measurement in PCR 9. However, this
approach has a couple of issues:

- The credential is currently only protected by PCR11. As it's not
  protected by the rest of the boot chain, it's possible to boot other
  operating systems in order to replay the PCR11 measurements and
  recover the secret. Note that as the NvPCR anchoring happens in early
  boot, the credential is stored in the ESP.
- Someone with privileged access to a system can just create a new
  credential containing a known secret and store this in /var/lib and
  the ESP. The NvPCRs are anchored with this known secret on subsequent
  boots, and therefore the measurements can no longer be trusted.
  Imagine the scenario where privileged access is theoretically possible
  as a result of some vulnerability. After upgrading the system to fix
  this vulnerability, the system should be able to attest that it is
  now in a good state. However, if an adversary were able to use their
  priviliges to replace the credential, they are able to obtain
  persistence and the NvPCR measurements are no longer trustworthy.

This PR changes things to take a different approach. Instead of
requiring knowledge of a secret, the NvPCRs are now created in a way
that requires a policy to be satisfied for writing. The write policy has
2 branches:
- TPM2_PolicyNvWritten(true), which can be satisified without any
  further authorization if the NvPCR has already been extended.
- TPM2_PolicyAuthorize(pcrPubKey, SHA256("nvpcr-init")) which can be
  satisfied with a signed PCR policy, and must be used to perform the
  initial extend to a NvPCR.

The intention here is that the signed PCR policy that can be used to
authorize the initial extend to the NvPCR can only be satisfied during
early boot. During later boot phases, this signed PCR policy must not be
valid. This means that if a NvPCR is undefined and redefined, it won't
be possible to satisfy its write policy in order to able to perform the
initial extend.

In order to anchor the NvPCRs and prevent them from being undefined and
then redefined with a different policy that does allow them to be
extended, the names of the NvPCRs are measured to PCR9. Verifiers must
check that the names of attested NvPCRs match the measurements in PCR9.

This uses the PCR signing key from the currently booted UKI to create
the NvPCRs. If this changes between boots, then tpm2-setup automatically
recreates new NvPCRs with an updated write policy to reflect this. I've
tried to be careful to not undefine arbitrary NV indexes in this case,
so it checks that the existing NV index looks like a NvPCR (ie, it has
the expected attributes) before undefining it.

I did originally try to preserve the old behaviour for existing systems,
but it makes things a lot more complicated. As the new implementation
already creates new NvPCRs when the PCR signing key changes, I ended up
just automatically upgrading the old NvPCRs as well. Again, I check here
that any existing NV index looks like an old style NvPCR (ie, it has the
expected attributes) before undefining it.

I did notice that the initial NvPCR measurement isn't going into the
log. I don't know if that was an intentional choice, but I've preserved
that behaviour in this PR.

This also adds a new option to ukify (--sign-initrd-pcrs) which creates
signed policies (one per PCR bank) that can only be satisfied from the
initrd. These policies are used for initializing the NvPCRs, but can also
be used for protecting TPM2 keyslots enrolled with systemd-cryptenroll
(by using the --tpm2-public-key-policyref=initrd option).

There is one outstanding issue. The NvPCR definitions support different
algorithms, but the use of PolicyAuthorize means that they can only support
SHA-256 for now. This is because the signed policy algorithm must match
the name algorithm, and some additional work is required to support
signed PCR policies for algorithms other than SHA256. I've left a note in
tpm2_nvpcr_initialize that details what's required, and I'll take a look
at that in a subsequent PR.

14 files changed:
NEWS
docs/TPM2_PCR_MEASUREMENTS.md
man/systemd-pcrphase.service.xml
man/systemd-tpm2-setup.service.xml
man/ukify.xml
src/cryptsetup/cryptsetup.c
src/pcrextend/pcrextend.c
src/shared/tpm2-util.c
src/shared/tpm2-util.h
src/test/test-tpm2.c
src/tpm2-setup/tpm2-setup.c
src/ukify/test/test_ukify.py
src/ukify/ukify.py
test/units/TEST-70-TPM2.nvpcr.sh

diff --git a/NEWS b/NEWS
index 963acfdf628b37f4e23aec6fdf97f6228aa84dc9..2c760d481614963ee7244dad22552252b263319e 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -64,6 +64,23 @@ CHANGES WITH 262:
           IPv4ProxyARP=yes but has no effect if IPv4ProxyARP= has been set to
           false. This mirrors the behavior of IPv6ProxyNDPAddress=.
 
+        Changes in the TPM Subsystem:
+
+        * The way that NvPCRs are anchored has been improved. They are now
+          created with a write policy which only permits the initial extend to
+          be performed from the initrd environment, and no longer rely on a
+          secret that is sealed to the TPM (the "anchor secret"). For NvPCRs to
+          work, all NvPCR definitions (/usr/lib/nvpcr/*.nvpcr) must now be
+          shipped in the UKI. The UKI must also embed a signed PCR policy that
+          is bound to the initrd boot phase and with the "initrd" policy
+          reference. This can be created by supplying the --sign-initrd-pcrs
+          option to ukify. NvPCRs that are not defined or initialized in the
+          initrd will fail to initialize lazily during runtime if an attempt is
+          made to extend them with systemd-pcrextend. Existing NvPCRs that were
+          created with a previous systemd version will be automatically
+          upgraded, and the existing "anchor secret" stored in /var/lib and the
+          ESP/XBOOTLDR will be removed.
+
 CHANGES WITH 261:
 
         Announcements of Future Feature Removals and Incompatible Changes:
index 87d8d3b6757340c0bcac72ee5eadab938ec6423e..b249db0e8269cf6ddd88b3bf5a16c0b65f44f141 100644 (file)
@@ -90,16 +90,21 @@ fields are:
 There's one complication: these NV indexes (like any NV indexes) can be deleted
 by anyone with access to the TPM, and then be recreated. This could be used to
 reset the NvPCRs to zero during runtime, which defeats the whole point of
-them. Our way out: we measure a secret as first thing after creation into the
-NvPCRs. (Or actually, we measure a per-NvPCR secret we derive from a system
-secret via an HMAC of the NvPCR name and the NV index handle). This "anchoring"
-secret is stored in `/run/` + `/var/lib/` + ESP/XBOOTLDR (the latter encrypted
-as credential, locked to the TPM), to make it available at the whole runtime of
-the OS. It's only accessible to privileged processes with access to the
-TPM. Due to this, any process with access to the TPM and read access to any of
-the storage locations of the anchor secret is considered part of the TCB, as
-they are able to replay the NvPCR with their own content at will, so due care
-must be employed when designing a system that uses this feature.
+them. To prevent this, we extend the name of each NvPCR to PCR 9 (the name in this
+context is a cryptographic hash of the public attributes of the NV index), and we
+only permit the first extend (write) to an NvPCR to be performed from the initrd.
+If a NvPCR is recreated later on during runtime, it is not possible to reinitialize
+it by performing the first write in order to replay arbitrary measurements. This
+is achieved by using a write policy on the NV index with 2 branches. One of these
+branches permits writing without any further authorization if the NV index has
+previously been written to. The other branch has to be satisfied in order to
+perform the first write, and this other branch is bound to a signed PCR policy that
+can only be satisfied from the initrd, using the kernel boot PCR (11). This works
+because booting an operating system that doesn't correctly extend this PCR can be
+detected via changes to other PCRs, and recreating a NvPCR with a different write
+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.
 
 ## PCR Measurements Made by `systemd-boot` (UEFI)
 
@@ -282,12 +287,12 @@ The `systemd-tpm2-setup-early.service` service initializes any NvPCRs defined vi
 9.
 
 → **Measured hash** covers the string `nvpcr-init:`, suffixed by the NvPCR
-name, suffixed by `:0x`, suffixed by the NV Index handle (formatted in
-hexadecimal), suffixed by a colon, suffixed by the hash function used, in
-lowercase (i.e. `sha256` or so), suffixed by a colon, and finally suffixed by
-the state of the NvPCR after its initialization with the anchor measurement, in
-hexadecimal. Example:
-`nvpcr-init:hardware:0x1d10200:sha256:de3857f637c61e82f02e3722e1b207585fe9711045d863238904be8db10683f2`
+readable name, suffixed by `:0x`, suffixed by the NV Index handle (formatted in
+hexadecimal), suffixed by a colon, and finally suffixed by the TPM name of the
+NvPCR in hexadecimal (where the TPM name is the cryptographic hash of the NV
+index public attributes, prefixed by the name algorithm - eg, `000b` for
+`sha256`). Example:
+`nvpcr-init:hardware:0x1d10200:000bff27bc66b6eda11bccd3a0ca07664ae4c360bcc3d7ef019d0b19cb5fa4067541`
 
 ## PCR/NvPCR Measurements Made by `systemd-pcrextend` (Userspace)
 
index c4716f9ab1f17836b61b8284b2ebe6deb2502776..c604a8465a3dd86ebd901901bee482259fff37ad 100644 (file)
         <xi:include href="version-info.xml" xpointer="v253"/></listitem>
       </varlistentry>
 
-      <varlistentry>
-        <term><option>--early</option></term>
-
-        <listitem><para>Selects early-boot mode. Specifically this means that the NvPCR anchor secret is not
-        attempted to be written into <filename>/var/lib/</filename> and the boot loader partition in addition
-        to <filename>/run/</filename>. (Unlike the latter the former are generally not mounted and writable
-        during early boot or in the initrd.)</para>
-
-        <xi:include href="version-info.xml" xpointer="v259"/></listitem>
-      </varlistentry>
-
       <varlistentry>
         <term><option>--machine-id</option></term>
 
index 43c28bc6a22d296edfff289218dee4af10fd1750..d0c33d70b34d091bdc401cb137e84f61b561e4b1 100644 (file)
         <listitem><para>Definition files for NvPCRs.</para>
         <xi:include href="version-info.xml" xpointer="v259"/></listitem>
       </varlistentry>
-
-      <varlistentry>
-        <term><filename>/run/credentials/@encrypted/nvpcr-anchor.*</filename></term>
-
-        <listitem><para>Encrypted NvPCR anchor secret, received into the system via system credentials.</para>
-        <xi:include href="version-info.xml" xpointer="v259"/></listitem>
-      </varlistentry>
-
-      <varlistentry>
-        <term><filename>$BOOT/loader/credentials/nvpcr-anchor.*.cred</filename></term>
-
-        <listitem><para>Encrypted NvPCR anchor secret, to be picked up by the kernel loader stub,
-        i.e. <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>.</para>
-        <xi:include href="version-info.xml" xpointer="v259"/></listitem>
-      </varlistentry>
     </variablelist>
   </refsect1>
 
index f1eb00617a25ad3015330f5611b7652f76639ba6..2c20ca7106058ca785825aea8cc33dc415f31f67 100644 (file)
           <xi:include href="version-info.xml" xpointer="v258"/></listitem>
         </varlistentry>
 
+        <varlistentry>
+          <term><option>--sign-initrd-pcrs</option></term>
+          <term><option>--no-sign-initrd-pcrs</option></term>
+
+          <listitem><para>Enable or disable signing of additional PCR policies that are bound to the initrd
+          phase (after <literal>enter-initrd</literal> and before <literal>leave-initrd</literal>) and the
+          <literal>initrd</literal> policy reference. These additional signed policies are consumed by
+          <citerefentry><refentrytitle>systemd-tpm2-setup-early.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+          to authorize the initialization of NvPCRs. They are also useful for enrolling TPM keyslots to encrypted
+          volumes that can only be unlocked from the initrd with
+          <citerefentry><refentrytitle>systemd-cryptenroll</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+          by using the <option>--tpm2-public-key-policyref=initrd</option> option. The extra signatures reuse
+          the first <varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option>, hence one must be
+          specified. Defaults to false.</para>
+
+          <xi:include href="version-info.xml" xpointer="v262"/></listitem>
+        </varlistentry>
+
         <varlistentry>
           <term><option>--section=<replaceable>NAME</replaceable>:<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
           <term><option>--section=<replaceable>NAME</replaceable>:text|binary<optional>@<replaceable>PATH</replaceable></optional></option></term>
index 6aedf12df5636b94ad4cd2630dbcc922f74c65a0..dcd830e44341f1298de0c3ede8f22bf4cd904129 100644 (file)
@@ -1145,7 +1145,6 @@ static int measure_keyslot(
                         arg_tpm2_measure_keyslot_nvpcr,
                         &IOVEC_MAKE_STRING(s),
                         /* secret= */ NULL,
-                        /* sync_secondary_anchor= */ false,
                         TPM2_EVENT_KEYSLOT,
                         s);
         if (r < 0)
@@ -2885,7 +2884,6 @@ static int verb_detach(int argc, char *argv[], uintptr_t _data, void *userdata)
 static int run(int argc, char *argv[]) {
         int r;
 
-        LIBBLKID_NOTE(recommended);
         LIBCRYPTO_NOTE(recommended);
         LIBCRYPTSETUP_NOTE(required);
         LIBFIDO2_NOTE(suggested);
index b32a5c708b74faf9d333d4682976413cfd2833be..6c87f244f8bcd3431ba5c1d318eb6eaa7a6b6a1d 100644 (file)
@@ -37,7 +37,6 @@ static UserRecord *arg_login = NULL;
 static uint32_t arg_pcr_mask = 0;
 static char *arg_nvpcr_name = NULL;
 static bool arg_varlink = false;
-static bool arg_early = false;
 static Tpm2UserspaceEventType arg_event_type = _TPM2_USERSPACE_EVENT_TYPE_INVALID;
 
 STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
@@ -177,11 +176,6 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
                         break;
                 }
 
-                OPTION_LONG("early", NULL,
-                            "Run in early boot mode, without access to /var/"):
-                        arg_early = true;
-                        break;
-
                 OPTION_LONG("event-type", "TYPE",
                             "Event type to include in the event log"):
                         if (streq(opts.arg, "help"))
@@ -379,7 +373,6 @@ static int extend_nvpcr_now(
                         name,
                         &IOVEC_MAKE(data, size),
                         /* secret= */ NULL,
-                        /* sync_secondary_anchor= */ !arg_early,
                         event,
                         safe);
         if (r == -ENOBUFS)
index d0758efdc99a78ca08eb359650c9cda330c9dd8c..cf7620ae03cad74d29a2947becb8281cfc9c1026 100644 (file)
@@ -8,8 +8,6 @@
 #include "alloc-util.h"
 #include "ansi-color.h"
 #include "bitfield.h"
-#include "boot-entry.h"
-#include "chase.h"
 #include "constants.h"
 #include "creds-util.h"
 #include "crypto-util.h"
@@ -124,6 +122,7 @@ static DLSYM_PROTOTYPE(Esys_PolicyAuthValue) = NULL;
 static DLSYM_PROTOTYPE(Esys_PolicyAuthorize) = NULL;
 static DLSYM_PROTOTYPE(Esys_PolicyAuthorizeNV) = NULL;
 static DLSYM_PROTOTYPE(Esys_PolicyGetDigest) = NULL;
+static DLSYM_PROTOTYPE(Esys_PolicyNvWritten) = NULL;
 static DLSYM_PROTOTYPE(Esys_PolicyOR) = NULL;
 static DLSYM_PROTOTYPE(Esys_PolicyPCR) = NULL;
 static DLSYM_PROTOTYPE(Esys_PolicySigned) = NULL;
@@ -201,6 +200,7 @@ static int dlopen_tpm2_esys(int log_level) {
                         DLSYM_ARG(Esys_PolicyAuthorize),
                         DLSYM_ARG(Esys_PolicyAuthorizeNV),
                         DLSYM_ARG(Esys_PolicyGetDigest),
+                        DLSYM_ARG(Esys_PolicyNvWritten),
                         DLSYM_ARG(Esys_PolicyOR),
                         DLSYM_ARG(Esys_PolicyPCR),
                         DLSYM_ARG(Esys_PolicySigned),
@@ -4174,13 +4174,11 @@ int tpm2_make_policy_session(
         int r;
 
         assert(c);
-        assert(primary);
-        assert(encryption_session);
         assert(ret_session);
 
-        if (!tpm2_is_encryption_session(c, encryption_session))
+        if (encryption_session && !tpm2_is_encryption_session(c, encryption_session))
                 return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
-                                       "Missing encryption session");
+                                       "Invalid encryption session");
 
         log_debug("Starting policy session.");
 
@@ -4191,9 +4189,9 @@ int tpm2_make_policy_session(
 
         rc = sym_Esys_StartAuthSession(
                         c->esys_context,
-                        primary->esys_handle,
+                        primary ? primary->esys_handle : ESYS_TR_NONE,
                         ESYS_TR_NONE,
-                        encryption_session->esys_handle,
+                        encryption_session ? encryption_session->esys_handle : ESYS_TR_NONE,
                         ESYS_TR_NONE,
                         ESYS_TR_NONE,
                         NULL,
@@ -4532,6 +4530,74 @@ int tpm2_policy_auth_value(
         return tpm2_get_policy_digest(c, session, ret_policy_digest);
 }
 
+/* Extend 'digest' with the PolicyNvWritten calculated hash. */
+int tpm2_calculate_policy_nv_written(bool written_set, TPM2B_DIGEST *digest) {
+        TPM2_CC command = TPM2_CC_PolicyNvWritten;
+        TSS2_RC rc;
+        int r;
+
+        assert(digest);
+        assert(digest->size == SHA256_DIGEST_SIZE);
+
+        r = dlopen_tpm2(LOG_DEBUG);
+        if (r < 0)
+                return r;
+
+        uint8_t buf[sizeof(command)];
+        size_t offset = 0;
+
+        rc = sym_Tss2_MU_TPM2_CC_Marshal(command, buf, sizeof(buf), &offset);
+        if (rc != TSS2_RC_SUCCESS)
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                       "Failed to marshal PolicyNvWritten command: %s", sym_Tss2_RC_Decode(rc));
+
+        if (offset != sizeof(command))
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                       "Offset 0x%zx wrong after marshalling PolicyNvWritten command", offset);
+
+        uint8_t written_byte = written_set ? TPM2_YES : TPM2_NO;
+
+        struct iovec data[] = {
+                IOVEC_MAKE(buf, offset),
+                IOVEC_MAKE(&written_byte, sizeof(written_byte)),
+        };
+        r = tpm2_digest_many(TPM2_ALG_SHA256, digest, data, ELEMENTSOF(data), /* extend= */ true);
+        if (r < 0)
+                return r;
+
+        tpm2_log_debug_digest(digest, "PolicyNvWritten calculated digest");
+
+        return 0;
+}
+
+int tpm2_policy_nv_written(
+                Tpm2Context *c,
+                const Tpm2Handle *session,
+                bool written_set,
+                TPM2B_DIGEST **ret_policy_digest) {
+
+        TSS2_RC rc;
+
+        assert(c);
+        assert(session);
+
+        log_debug("Submitting NvWritten policy.");
+
+        rc = sym_Esys_PolicyNvWritten(
+                        c->esys_context,
+                        session->esys_handle,
+                        ESYS_TR_NONE,
+                        ESYS_TR_NONE,
+                        ESYS_TR_NONE,
+                        written_set ? TPM2_YES : TPM2_NO);
+        if (rc != TSS2_RC_SUCCESS)
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                       "Failed to add NvWritten policy to TPM: %s",
+                                       sym_Tss2_RC_Decode(rc));
+
+        return tpm2_get_policy_digest(c, session, ret_policy_digest);
+}
+
 /* Extend 'digest' with the PolicySigned calculated hash. */
 int tpm2_calculate_policy_signed(TPM2B_DIGEST *digest, const TPM2B_NAME *name) {
         TPM2_CC command = TPM2_CC_PolicySigned;
@@ -7054,6 +7120,51 @@ int tpm2_write_policy_nv_index(
         return 0;
 }
 
+#if HAVE_OPENSSL
+/* Calculates the write policy of a NvPCR, which is TPM2_PolicyAuthorize OR TPM2_PolicyNvWritten(true),
+ * where the authorized policy is bound to the supplied public key and policy ref. Returns the overall write
+ * policy, and optionally, the PolicyAuthorize branch digest (which the extend path needs later on in order
+ * to reconstruct the PolicyOR). */
+static int tpm2_nvpcr_calculate_write_policy(
+                const TPM2B_PUBLIC *public,
+                const char *pubkey_policy_ref,
+                TPM2B_DIGEST *ret_authorize_policy,
+                TPM2B_DIGEST *ret_write_policy) {
+
+        int r;
+
+        assert(public);
+        assert(ret_write_policy);
+
+        TPM2B_NONCE policy_ref;
+        tpm2_make_policy_ref(pubkey_policy_ref, &policy_ref);
+
+        /* Branch order matters here and must match the reconstruction in tpm2_nvpcr_open_write_session. */
+        TPM2B_DIGEST branches[2] = {
+                TPM2B_DIGEST_MAKE(NULL, SHA256_DIGEST_SIZE), /* PolicyAuthorize branch */
+                TPM2B_DIGEST_MAKE(NULL, SHA256_DIGEST_SIZE), /* PolicyNvWritten branch */
+        };
+
+        r = tpm2_calculate_policy_authorize(public, &policy_ref, &branches[0]);
+        if (r < 0)
+                return r;
+
+        r = tpm2_calculate_policy_nv_written(/* written_set= */ true, &branches[1]);
+        if (r < 0)
+                return r;
+
+        TPM2B_DIGEST write_policy = TPM2B_DIGEST_MAKE(NULL, SHA256_DIGEST_SIZE);
+        r = tpm2_calculate_policy_or(branches, ELEMENTSOF(branches), &write_policy);
+        if (r < 0)
+                return r;
+
+        if (ret_authorize_policy)
+                *ret_authorize_policy = branches[0];
+        *ret_write_policy = write_policy;
+        return 0;
+}
+#endif
+
 int tpm2_define_data_nv_index(
                 Tpm2Context *c,
                 const Tpm2Handle *session,
@@ -7191,20 +7302,148 @@ int tpm2_undefine_nv_index(
 }
 
 #if HAVE_OPENSSL
+
+static int tpm2_reuse_or_redefine_nvpcr_nv_index(
+                Tpm2Context *c,
+                const Tpm2Handle *session,
+                bool exhausted,
+                TPM2_HANDLE nv_index,
+                const TPM2B_NV_PUBLIC *nv_public_info,
+                Tpm2Handle **ret_nv_handle) {
+
+        int r;
+        TSS2_RC rc;
+
+        assert(c);
+        assert(nv_public_info);
+        assert(ret_nv_handle);
+
+        log_debug("Checking if there is an existing NV index 0x%" PRIx32 " that can be reused as the requested NvPCR.", nv_index);
+
+        _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public_real = NULL;
+        _cleanup_(Esys_Freep) TPM2B_NAME *nv_name_real = NULL;
+        _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+        r = tpm2_nv_index_to_handle(
+                        c,
+                        nv_index,
+                        session,
+                        &nv_public_real,
+                        &nv_name_real,
+                        &handle);
+        if (r <= 0)
+                return log_debug_errno(r < 0 ? r : SYNTHETIC_ERRNO(ENOENT),
+                                       "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
+         * 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);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to compute expected NvPCR index name");
+        if (memcmp_nn(nv_name_expected.name, nv_name_expected.size, nv_name_real->name, nv_name_real->size) == 0) {
+                log_debug("NV index 0x%" PRIx32 " for NvPCR already exists and not yet initialized, reusing.", nv_index);
+                *ret_nv_handle = TAKE_PTR(handle);
+                return 0;
+        }
+
+        /* If space for this orderly type is exhausted, don't try to redefine the index if the one we're
+         * going to create has a size that is larger than the one we need to undefine. Note that this only
+         * considers the data size of the 2 indices. Whilst the size of the authorization policy and the
+         * maximum size of the authorization value are dependent on the name algorithm, the reference TPM
+         * implementation stores the TPMS_NV_PUBLIC and TPM2B_AUTH structures using a fixed size. */
+        if (exhausted &&
+            (nv_public_info->nvPublic.attributes & TPMA_NV_ORDERLY) == (nv_public_real->nvPublic.attributes & TPMA_NV_ORDERLY) &&
+                nv_public_info->nvPublic.dataSize > nv_public_real->nvPublic.dataSize)
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS),
+                                       "NV space is exhausted and undefining the existing NV index 0x%" PRIx32 " won't "
+                                       "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. */
+        const TPMA_NV old_expected_attrs =
+                TPMA_NV_CLEAR_STCLEAR |
+                TPMA_NV_OWNERWRITE |
+                TPMA_NV_AUTHWRITE |
+                TPMA_NV_OWNERREAD |
+                TPMA_NV_AUTHREAD |
+                (TPM2_NT_EXTEND << TPMA_NV_TPM2_NT_SHIFT);
+        if ((((nv_public_real->nvPublic.attributes ^ nv_public_info->nvPublic.attributes) & ~(TPMA_NV_WRITTEN | TPMA_NV_ORDERLY)) != 0) &&
+                (((nv_public_real->nvPublic.attributes ^ old_expected_attrs) & ~(TPMA_NV_WRITTEN | TPMA_NV_ORDERLY)) != 0))
+                return log_debug_errno(SYNTHETIC_ERRNO(EEXIST),
+                                        "Existing nvindex 0x%" PRIx32 " does not have the attributes expected for a NvPCR.", nv_index);
+
+        /* At this point, the existing index looks like a NvPCR - either one that was authenticated using a
+         * previous signing key or an old style NvPCR. We'll attempt to replace it with a new one. As we are
+         * replacing an existing one, we do this regardless of whether a previous call indicated that NV
+         * space is exhausted because it might still work. */
+
+        log_debug("Public info for nvindex 0x%" PRIx32 " looks like an existing NvPCR, replacing.", nv_index);
+
+        /* Undefine the existing index. */
+        r = tpm2_undefine_nv_index(c, session, nv_index, handle);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to undefine existing nvindex 0x%" PRIx32 ".", nv_index);
+
+        /* Try to create the replacement index. */
+        _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL;
+        r = tpm2_handle_new(c, &new_handle);
+        if (r < 0)
+                return r;
+
+        new_handle->flush = false;
+
+        rc = sym_Esys_NV_DefineSpace(
+                        c->esys_context,
+                        /* authHandle= */ ESYS_TR_RH_OWNER,
+                        /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD,
+                        /* shandle2= */ ESYS_TR_NONE,
+                        /* shandle3= */ ESYS_TR_NONE,
+                        /* auth= */ NULL,
+                        nv_public_info,
+                        &new_handle->esys_handle);
+        if (rc == TPM2_RC_NV_SPACE)
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS),
+                                       "NV index space on TPM exhausted, cannot allocate NvPCR.");
+        if (rc != TSS2_RC_SUCCESS)
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                       "Failed to allocate NvPCR index: %s", sym_Tss2_RC_Decode(rc));
+
+        *ret_nv_handle = TAKE_PTR(new_handle);
+        return 1;
+}
+
 static int tpm2_define_nvpcr_nv_index(
                 Tpm2Context *c,
                 const Tpm2Handle *session,
                 TPM2_HANDLE nv_index,
                 TPMI_ALG_HASH algorithm,
+                const TPM2B_PUBLIC *public,
+                const char *pubkey_policy_ref,
                 bool orderly,
+                TPM2B_DIGEST *ret_authorize_policy,
                 Tpm2Handle **ret_nv_handle) {
 
         TSS2_RC rc;
         int r;
 
         assert(c);
+        assert(public);
+
+        /* Allocates an nvindex to use as a "fake" PCR. We call these "NvPCR" in our codebase. The index is
+         * created to require a policy for writing, so that the first write (which sets TPMA_NV_WRITTEN)
+         * requires a signed PolicyAuthorize authorization, while all subsequent writes can be performed using
+         * a PolicyNvWritten(true) branch without any further authorization. The intention here is that a
+         * NvPCR can only be defined and initialized for as long as the authorized policy can be satisfied
+         * (ie, during an early boot phase). Once the authorized policy can no longer be satisfied (ie, after
+         * the early boot has ended), it is no longer possible to redefine and initialize the same NvPCR in
+         * order to spoof its measurements. */
 
-        /* Allocates an nvindex to use as a "fake" PCR. We call these "NvPCR" in our codebase */
+        log_debug("Allocating NvPCR index 0x%" PRIx32 ".", nv_index);
 
         if (algorithm == 0)
                 algorithm = TPM2_ALG_SHA256;
@@ -7224,6 +7463,14 @@ static int tpm2_define_nvpcr_nv_index(
                 "/run/systemd/tpm2-nv-space-exhausted-orderly" :
                 "/run/systemd/tpm2-nv-space-exhausted-non-orderly";
 
+        /* Calculate the write policy, which is TPM2_PolicyAuthorize() OR TPM2_PolicyNvWritten(true). The
+         * PolicyAuthorize() branch is used for the first write, but we need to retain the digest of that so
+         * that we can reconstruct the digests used for the TPM2_PolicyOR() assertion on subsequent writes. */
+        TPM2B_DIGEST authorize_policy, write_policy;
+        r = tpm2_nvpcr_calculate_write_policy(public, pubkey_policy_ref, &authorize_policy, &write_policy);
+        if (r < 0)
+                return r;
+
         TPM2B_NV_PUBLIC public_info = {
                 .size = sizeof_field(TPM2B_NV_PUBLIC, nvPublic),
                 .nvPublic = {
@@ -7231,108 +7478,109 @@ static int tpm2_define_nvpcr_nv_index(
                         .nameAlg = algorithm,
                         .attributes = TPMA_NV_CLEAR_STCLEAR |
                                       (orderly ? TPMA_NV_ORDERLY : 0) |
-                                      TPMA_NV_OWNERWRITE |
-                                      TPMA_NV_AUTHWRITE |
+                                      TPMA_NV_POLICYWRITE |
                                       TPMA_NV_OWNERREAD |
                                       TPMA_NV_AUTHREAD |
                                       (TPM2_NT_EXTEND << TPMA_NV_TPM2_NT_SHIFT),
                         .dataSize = digest_size,
+                        .authPolicy = write_policy,
                 },
         };
 
-        bool exhausted;
-        if (access(exhausted_flag, F_OK) < 0) {
-                if (errno != ENOENT)
-                        log_debug_errno(errno, "Failed to check whether %s exists, assuming it does not: %m", exhausted_flag);
+        if (access(exhausted_flag, F_OK) == 0) {
+                log_debug("TPM NV index space previously found exhausted (%s exists), refusing to allocate %s NvPCR, but checking if it already exists.",
+                          exhausted_flag, orderly ? "orderly" : "non-orderly");
 
-                _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL;
-                r = tpm2_handle_new(c, &new_handle);
+                _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+                r = tpm2_reuse_or_redefine_nvpcr_nv_index(c, session, /* exhausted= */ true, nv_index, &public_info, &handle);
+                if (r == -ENOENT)
+                        r = -ENOBUFS;
                 if (r < 0)
-                        return r;
+                        return log_debug_errno(r, "No existing NvPCR index 0x%" PRIx32 " and NV space is exhausted.", nv_index);
 
-                new_handle->flush = false; /* This is a persistent NV index, don't flush hence */
+                log_debug("NV index 0x%" PRIx32 " successfully %s for NvPCR.", nv_index, r == 0 ? "reused" : "reallocated");
 
-                rc = sym_Esys_NV_DefineSpace(
-                                c->esys_context,
-                                /* authHandle= */ ESYS_TR_RH_OWNER,
-                                /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD,
-                                /* shandle2= */ ESYS_TR_NONE,
-                                /* shandle3= */ ESYS_TR_NONE,
-                                /* auth= */ NULL,
-                                &public_info,
-                                &new_handle->esys_handle);
-                if (rc == TPM2_RC_NV_SPACE) {
-                        /* Remember that we ran out of NV index space for this orderly mode, so that we don't keep
-                         * retrying the (doomed) allocation until reboot. */
-                        r = touch(exhausted_flag);
-                        if (r < 0)
-                                log_debug_errno(r, "Failed to create %s flag file, ignoring: %m", exhausted_flag);
+                if (ret_authorize_policy)
+                        *ret_authorize_policy = authorize_policy;
+                if (ret_nv_handle)
+                        *ret_nv_handle = TAKE_PTR(handle);
 
-                        return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS),
-                                               "NV index space on TPM exhausted, cannot allocate NvPCR.");
-                }
-                if (rc == TSS2_RC_SUCCESS) {
-                        log_debug("NV Index 0x%" PRIx32 " successfully allocated.", nv_index);
+                return r;
+        }
 
-                        if (ret_nv_handle)
-                                *ret_nv_handle = TAKE_PTR(new_handle);
+        if (errno != ENOENT)
+                log_debug_errno(errno, "Failed to check whether %s exists, assuming it does not: %m", exhausted_flag);
 
-                        return 1;
-                }
-                if (rc != TPM2_RC_NV_DEFINED)
-                        return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
-                                               "Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc));
+        _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL;
+        r = tpm2_handle_new(c, &new_handle);
+        if (r < 0)
+                return r;
 
-                log_debug("NV index 0x%" PRIx32 " already registered.", nv_index);
-                exhausted = false;
-        } else {
-                log_debug("TPM NV index space previously found exhausted (%s exists), refusing to allocate %s NvPCR, but checking if it already exists.",
-                          exhausted_flag, orderly ? "orderly" : "non-orderly");
-                exhausted = true;
-        }
+        new_handle->flush = false; /* This is a persistent NV index, don't flush hence */
 
-        /* We either got told that this NV index already exists or we didn't even try to allocate it, because
-         * it failed before. Let's get information about it, in the hope it exists. */
+        rc = sym_Esys_NV_DefineSpace(
+                        c->esys_context,
+                        /* authHandle= */ ESYS_TR_RH_OWNER,
+                        /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD,
+                        /* shandle2= */ ESYS_TR_NONE,
+                        /* shandle3= */ ESYS_TR_NONE,
+                        /* auth= */ NULL,
+                        &public_info,
+                        &new_handle->esys_handle);
+        if (rc == TPM2_RC_NV_SPACE) {
+                /* Remember that we ran out of NV index space for this orderly mode, so that we don't keep
+                 * retrying the (doomed) allocation until reboot. */
+                r = touch(exhausted_flag);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to create %s flag file, ignoring: %m", exhausted_flag);
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS),
+                                        "NV index space on TPM exhausted, cannot allocate NvPCR.");
+        }
+        if (rc == TSS2_RC_SUCCESS) {
+                log_debug("NV index 0x%" PRIx32 " successfully allocated for NvPCR.", nv_index);
 
-        _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public_real = NULL;
-        _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL;
-        r = tpm2_nv_index_to_handle(
-                        c,
-                        nv_index,
-                        session,
-                        &nv_public_real,
-                        /* ret_name= */ NULL,
-                        &new_handle);
-        if (r <= 0) {
-                if (exhausted)
-                        return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS), "Unable to acquire NvPCR and space exhaustion was indicated before.");
+                if (ret_authorize_policy)
+                        *ret_authorize_policy = authorize_policy;
+                if (ret_nv_handle)
+                        *ret_nv_handle = TAKE_PTR(new_handle);
 
-                return log_debug_errno(r < 0 ? r : SYNTHETIC_ERRNO(ENOTRECOVERABLE),
-                                       "Failed to acquire handle to NV index 0x%" PRIx32 ".", nv_index);
+                return 1;
         }
+        if (rc != TPM2_RC_NV_DEFINED)
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                        "Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc));
 
-        log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", nv_index);
+        log_debug("NV index 0x%" PRIx32 " already registered.", nv_index);
 
-        if (nv_public_real->size < endoffsetof_field(TPMS_NV_PUBLIC, attributes) + sizeof_field(TPMS_NV_PUBLIC, dataSize) ||
-            nv_public_real->nvPublic.nvIndex != public_info.nvPublic.nvIndex ||
-            nv_public_real->nvPublic.nameAlg != public_info.nvPublic.nameAlg ||
-            ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~(TPMA_NV_WRITTEN|TPMA_NV_ORDERLY)) != 0 ||
-            nv_public_real->nvPublic.dataSize != public_info.nvPublic.dataSize)
-                return log_debug_errno(SYNTHETIC_ERRNO(EEXIST),
-                                       "Public data of nvindex 0x%" PRIx32 " does not match our expectations.", nv_index);
+        _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+        r = tpm2_reuse_or_redefine_nvpcr_nv_index(c, session, /* exhausted= */ false, nv_index, &public_info, &handle);
+        if (r == -ENOBUFS) {
+                /* Remember that we ran out of NV index space for this orderly mode, so that we don't keep
+                 * retrying the (doomed) allocation until reboot. */
+                r = touch(exhausted_flag);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to create %s flag file, ignoring: %m", exhausted_flag);
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS),
+                                        "NV index space on TPM exhausted, cannot re-allocate NvPCR.");
+        }
+        if (r < 0)
+                return r;
 
-        log_debug("Public info for nvindex 0x%" PRIx32 " checks out, using.", nv_index);
+        log_debug("NV index 0x%" PRIx32 " successfully %s for NvPCR.", nv_index, r == 0 ? "reused" : "reallocated");
 
+        if (ret_authorize_policy)
+                *ret_authorize_policy = authorize_policy;
         if (ret_nv_handle)
-                *ret_nv_handle = TAKE_PTR(new_handle);
+                *ret_nv_handle = TAKE_PTR(handle);
 
-        return 0;
+        return r;
 }
 
 static int tpm2_extend_nvpcr_nv_index(
                 Tpm2Context *c,
                 TPM2_HANDLE nv_index,
                 const Tpm2Handle *nv_handle,
+                const Tpm2Handle *policy_session,
                 const struct iovec *digest) {
 
         TPM2_RC rc;
@@ -7340,6 +7588,7 @@ static int tpm2_extend_nvpcr_nv_index(
         assert(c);
         assert(nv_index);
         assert(nv_handle);
+        assert(policy_session);
         assert(iovec_is_set(digest));
 
         if (digest->iov_len > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer))
@@ -7354,10 +7603,13 @@ static int tpm2_extend_nvpcr_nv_index(
                         c->esys_context,
                         /* authHandle= */ nv_handle->esys_handle,
                         /* nvIndex= */ nv_handle->esys_handle,
-                        /* shandle1= */ ESYS_TR_PASSWORD,
+                        /* shandle1= */ policy_session->esys_handle,
                         /* shandle2= */ ESYS_TR_NONE,
                         /* shandle3= */ ESYS_TR_NONE,
                         &buf);
+        if (rc == TPM2_RC_PCR_CHANGED)
+                return log_debug_errno(SYNTHETIC_ERRNO(ESTALE),
+                                       "PCR changed while extending NV index.");
         if (rc != TSS2_RC_SUCCESS)
                 return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
                                        "Failed to extend NV index: %s", sym_Tss2_RC_Decode(rc));
@@ -7370,6 +7622,7 @@ static int tpm2_extend_nvpcr_nv_index(
 
         return 0;
 }
+
 #endif
 
 int tpm2_read_nv_index(
@@ -8108,56 +8361,184 @@ int tpm2_nvpcr_get_index(const char *name, uint32_t *ret_nv_index, uint64_t *ret
         return 0;
 }
 
-static int 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) {
-
 #if HAVE_OPENSSL
-        _cleanup_close_ int log_fd = -EBADF;
+static int tpm2_nvpcr_load_pcr_public_key(
+                const char *path,
+                TPM2B_PUBLIC *ret_public,
+                struct iovec *ret_fingerprint) {
+
         int r;
 
-        assert(c);
-        assert(name);
-        assert(iovec_is_valid(data));
-        assert(iovec_is_valid(secret));
+        assert(ret_public);
+        assert(ret_fingerprint);
 
-        r = dlopen_libcrypto(LOG_DEBUG);
+        _cleanup_(iovec_done) struct iovec pubkey = {};
+        r = tpm2_load_pcr_public_key(path, &pubkey.iov_base, &pubkey.iov_len);
         if (r < 0)
-                return r;
+                return log_debug_errno(r, "Failed to load PCR public key for NvPCR: %m");
 
-        _cleanup_(nvpcr_data_done) NvPCRData p = {};
-        r = nvpcr_data_load(name, &p);
+        TPM2B_PUBLIC public;
+        r = tpm2_tpm2b_public_from_pem(pubkey.iov_base, pubkey.iov_len, &public);
         if (r < 0)
-                return r;
+                return log_debug_errno(r, "Failed to convert PCR public key to TPM2B_PUBLIC: %m");
 
-        /* Open + lock the log file *before* we start measuring, so that no one else can come between our log
-         * and our measurement and change either */
-        log_fd = tpm2_userspace_log_open();
+        _cleanup_(iovec_done) struct iovec fp = {};
+        r = tpm2_tpm2b_public_to_fingerprint(&public, &fp.iov_base, &fp.iov_len);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to calculate fingerprint of PCR public key: %m");
 
-        /* Check if this NvPCR is already anchored */
-        const char *anchor_fname = strjoina("/run/systemd/nvpcr/", name, ".anchor");
-        if (faccessat(AT_FDCWD, anchor_fname, F_OK, AT_SYMLINK_NOFOLLOW) < 0) {
-                if (errno != ENOENT)
-                        return log_debug_errno(errno, "Failed to check if '%s' exists: %m", anchor_fname);
+        *ret_public = public;
+        *ret_fingerprint = TAKE_STRUCT(fp);
+        return 0;
+}
 
-                return log_debug_errno(SYNTHETIC_ERRNO(ENETDOWN), "NvPCR '%s' not anchored yet, refusing.", name);
-        }
+/* Reads the stored PolicyAuthorize branch digest of a previously initialized NvPCR. This is written by
+ * tpm2_nvpcr_initialize() and is needed to reconstruct the TPM2_PolicyOR for runtime extends. */
+static int tpm2_nvpcr_read_authorize_policy(const char *name, TPM2B_DIGEST *ret) {
+        int r;
 
-        const char *an = tpm2_hash_alg_to_string(p.algorithm);
-        if (!an)
-                return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported algorithm for NvPCR, refusing.");
+        assert(name);
+        assert(ret);
 
-        const EVP_MD *implementation;
-        assert_se(implementation = sym_EVP_get_digestbyname(an));
+        const char *fname = strjoina("/run/systemd/nvpcr/", name, ".auth");
 
-        _cleanup_(iovec_done) struct iovec digest = {
-                .iov_len = sym_EVP_MD_get_size(implementation),
-        };
+        _cleanup_free_ char *h = NULL;
+        r = read_one_line_file(fname, &h);
+        if (r == -ENOENT)
+                return log_debug_errno(SYNTHETIC_ERRNO(ENETDOWN), "NvPCR '%s' not initialized yet, refusing.", name);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to read NvPCR authorize policy '%s': %m", fname);
+
+        _cleanup_free_ void *d = NULL;
+        size_t d_size;
+        r = unhexmem(strstrip(h), &d, &d_size);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to decode NvPCR authorize policy from '%s': %m", fname);
+        if (d_size != SHA256_DIGEST_SIZE)
+                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "NvPCR authorize policy in '%s' has unexpected size.", fname);
+
+        *ret = TPM2B_DIGEST_MAKE(d, d_size);
+        return 0;
+}
+
+/* Opens and executes a policy session that satisfies an NvPCR's write policy, leaving it ready for a
+ * subsequent extend. If 'signature_json' is provided, the PolicyAuthorize branch is used (for the
+ * initializing write that sets TPMA_NV_WRITTEN); otherwise the PolicyNvWritten(true) branch is used (for
+ * all later writes). */
+static int tpm2_nvpcr_open_write_session(
+                Tpm2Context *c,
+                const TPM2B_PUBLIC *public,
+                const char *pubkey_policy_ref,
+                uint32_t pubkey_pcr_mask,
+                const struct iovec *fingerprint,
+                sd_json_variant *signature_json,
+                const TPM2B_DIGEST *authorize_policy,
+                Tpm2Handle **ret_session) {
+
+        int r;
+
+        assert(c);
+        assert(authorize_policy);
+        assert(ret_session);
+
+        _cleanup_(tpm2_handle_freep) Tpm2Handle *session = NULL;
+        r = tpm2_make_policy_session(c, /* primary= */ NULL, /* encryption_session= */ NULL, &session);
+        if (r < 0)
+                return r;
+
+        if (signature_json) {
+                assert(public);
+                assert(fingerprint);
+
+                /* Initializing write: satisfy the PolicyAuthorize branch using the signed PCR policy. */
+                TPML_PCR_SELECTION pcr_selection;
+                tpm2_tpml_pcr_selection_from_mask(pubkey_pcr_mask, TPM2_ALG_SHA256, &pcr_selection);
+
+                r = tpm2_policy_authorize(
+                                c,
+                                session,
+                                &pcr_selection,
+                                public,
+                                pubkey_policy_ref,
+                                fingerprint->iov_base, fingerprint->iov_len,
+                                signature_json,
+                                /* ret_policy_digest= */ NULL);
+                if (r < 0)
+                        return r;
+        } else {
+                /* Subsequent write: the NvPCR is already written, so the PolicyNvWritten(true) branch can be
+                 * satisfied without any authorization. */
+                r = tpm2_policy_nv_written(c, session, /* written_set= */ true, /* ret_policy_digest= */ NULL);
+                if (r < 0)
+                        return r;
+        }
+
+        /* Branch order must match tpm2_nvpcr_calculate_write_policy(). */
+        TPM2B_DIGEST branches[2] = {
+                *authorize_policy,
+                TPM2B_DIGEST_MAKE(NULL, SHA256_DIGEST_SIZE), /* PolicyNvWritten branch */
+        };
+        r = tpm2_calculate_policy_nv_written(/* written_set= */ true, &branches[1]);
+        if (r < 0)
+                return r;
+
+        r = tpm2_policy_or(c, session, branches, ELEMENTSOF(branches), /* ret_policy_digest= */ NULL);
+        if (r < 0)
+                return r;
+
+        *ret_session = TAKE_PTR(session);
+        return 0;
+}
+#endif
+
+static int 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) {
+
+#if HAVE_OPENSSL
+        _cleanup_close_ int log_fd = -EBADF;
+        int r;
+
+        assert(c);
+        assert(name);
+        assert(iovec_is_valid(data));
+        assert(iovec_is_valid(secret));
+
+        r = dlopen_libcrypto(LOG_DEBUG);
+        if (r < 0)
+                return r;
+
+        _cleanup_(nvpcr_data_done) NvPCRData p = {};
+        r = nvpcr_data_load(name, &p);
+        if (r < 0)
+                return r;
+
+        /* Open + lock the log file *before* we start measuring, so that no one else can come between our log
+         * and our measurement and change either */
+        log_fd = tpm2_userspace_log_open();
+
+        /* Read the stored PolicyAuthorize branch digest. Its presence also tells us this NvPCR has been
+         * initialized; we need it to reconstruct the TPM2_PolicyOR below. */
+        TPM2B_DIGEST authorize_policy;
+        r = tpm2_nvpcr_read_authorize_policy(name, &authorize_policy);
+        if (r < 0)
+                return r;
+
+        const char *an = tpm2_hash_alg_to_string(p.algorithm);
+        if (!an)
+                return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported algorithm for NvPCR, refusing.");
+
+        const EVP_MD *implementation;
+        assert_se(implementation = sym_EVP_get_digestbyname(an));
+
+        _cleanup_(iovec_done) struct iovec digest = {
+                .iov_len = sym_EVP_MD_get_size(implementation),
+        };
 
         digest.iov_base = malloc(digest.iov_len);
         if (!digest.iov_base)
@@ -8188,12 +8569,28 @@ static int nvpcr_extend_bytes(
 
         log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", p.nv_index);
 
+        /* Open a policy session that satisfies the write policy via the PolicyNvWritten(true) branch
+         * (the index was already written during initialization). */
+        _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL;
+        r = tpm2_nvpcr_open_write_session(
+                        c,
+                        /* public= */ NULL,
+                        /* pubkey_policy_ref= */ NULL,
+                        /* pubkey_pcr_mask= */ 0,
+                        /* fingerprint= */ NULL,
+                        /* signature_json= */ NULL,
+                        &authorize_policy,
+                        &policy_session);
+        if (r < 0)
+                return r;
+
         bool reset_marker = tpm2_userspace_log_dirty(log_fd) >= 0;
 
         r = tpm2_extend_nvpcr_nv_index(
                         c,
                         p.nv_index,
                         nv_handle,
+                        policy_session,
                         &digest);
         if (r < 0)
                 return r;
@@ -8227,7 +8624,6 @@ int tpm2_nvpcr_extend_bytes(
                 const char *name,
                 const struct iovec *data,
                 const struct iovec *secret,
-                bool sync_secondary_anchor,
                 Tpm2UserspaceEventType event_type,
                 const char *description) {
 
@@ -8237,389 +8633,15 @@ int tpm2_nvpcr_extend_bytes(
         if (r != -ENETDOWN)
                 return r;
 
-        /* The NvPCR isn't anchored yet, i.e. systemd-tpm2-setup hasn't run.
-         * Anchor it now and extend again. */
-
-        _cleanup_(iovec_done_erase) struct iovec anchor_secret = {};
-        r = tpm2_nvpcr_acquire_anchor_secret(&anchor_secret, sync_secondary_anchor);
-        if (r < 0)
-                return log_debug_errno(r, "Failed to acquire anchor secret for NvPCR '%s': %m", name);
-
-        r = tpm2_nvpcr_initialize(c, session, name, &anchor_secret);
+        /* The NvPCR isn't initialized yet, i.e. systemd-tpm2-setup hasn't run.
+         * Initialize it now and extend again. */
+        r = tpm2_nvpcr_initialize(c, session, name);
         if (r < 0)
-                return log_debug_errno(r, "Failed to initialize NvPCR '%s' with anchor secret: %m", name);
+                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_nvpcr_write_anchor_secret(
-                const char *dir,
-                const char *fname,
-                const struct iovec *credential) {
-
-        int r;
-
-        assert(dir);
-        assert(fname);
-        assert(iovec_is_set(credential));
-
-        /* Writes the encrypted credential of the anchor secret to directory 'dir' and file 'fname' */
-
-        _cleanup_close_ int dfd = -EBADF;
-        r = chase(dir, /* root= */ NULL, CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, /* ret_path= */ NULL, &dfd);
-        if (r < 0)
-                return log_error_errno(r, "Failed to create '%s' directory: %m", dir);
-
-        _cleanup_free_ char *joined = path_join(dir, fname);
-        if (!joined)
-                return log_oom();
-
-        _cleanup_(iovec_done) struct iovec existing = {};
-        r = read_full_file_full(
-                        dfd,
-                        fname,
-                        /* offset= */ UINT64_MAX,
-                        CREDENTIAL_ENCRYPTED_SIZE_MAX,
-                        READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER,
-                        /* bind_name= */ NULL,
-                        (char**) &existing.iov_base,
-                        &existing.iov_len);
-        if (r < 0) {
-                if (r != -ENOENT)
-                        return log_error_errno(r, "Failed to read '%s' file: %m", joined);
-        } else if (iovec_equal(&existing, credential)) {
-                log_debug("Anchor secret file '%s' already matches expectations, not updating.", joined);
-                return 0;
-        } else
-                log_notice("Anchor secret file '%s' different from current anchor secret, updating.", joined);
-
-        r = write_base64_file_at(
-                        dfd,
-                        fname,
-                        credential,
-                        WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_SYNC);
-        if (r < 0)
-                return log_error_errno(r, "Failed to write anchor secret file to '%s': %m", joined);
-
-        log_info("Successfully written anchor secret to '%s'.", joined);
-        return 1;
-}
-
-static int tpm2_nvpcr_write_anchor_secret_to_var(const struct iovec *credential) {
-        return tpm2_nvpcr_write_anchor_secret("/var/lib/systemd/nvpcr", "nvpcr-anchor.cred", credential);
-}
-
-static int tpm2_nvpcr_write_anchor_secret_to_boot(const struct iovec *credential) {
-        int r;
-
-        assert(iovec_is_set(credential));
-
-        _cleanup_free_ char *dir = NULL;
-        r = get_global_boot_credentials_path(&dir);
-        if (r < 0)
-                return r;
-        if (r == 0) {
-                log_debug("No XBOOTLDR/ESP partition found, not writing boot anchor secret file.");
-                return 0;
-        }
-
-        sd_id128_t machine_id;
-        r = sd_id128_get_machine(&machine_id);
-        if (r < 0)
-                return log_error_errno(r, "Failed to read machine ID: %m");
-
-        BootEntryTokenType entry_token_type = BOOT_ENTRY_TOKEN_AUTO;
-        _cleanup_free_ char *entry_token = NULL;
-        r = boot_entry_token_ensure(
-                        /* root= */ NULL,
-                        /* conf_root= */ NULL,
-                        machine_id,
-                        /* machine_id_is_random= */ false,
-                        &entry_token_type,
-                        &entry_token);
-        if (r < 0)
-                return r;
-
-        _cleanup_free_ char *fname = strjoin("nvpcr-anchor.", entry_token, ".cred");
-        if (!fname)
-                return log_oom();
-
-        if (!filename_is_valid(fname))
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name '%s' would not be a valid file name, refusing.", fname);
-
-        return tpm2_nvpcr_write_anchor_secret(dir, fname, credential);
-}
-
-static int tpm2_nvpcr_acquire_anchor_secret_from_var(struct iovec *ret_credential) {
-        int r;
-
-        assert(ret_credential);
-
-        r = read_full_file_full(
-                        AT_FDCWD,
-                        "/var/lib/systemd/nvpcr/nvpcr-anchor.cred",
-                        /* offset= */ UINT64_MAX,
-                        CREDENTIAL_ENCRYPTED_SIZE_MAX,
-                        READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER|READ_FULL_FILE_VERIFY_REGULAR,
-                        /* bind_name= */ NULL,
-                        (char**) &ret_credential->iov_base,
-                        &ret_credential->iov_len);
-        if (r == -ENOENT) {
-                log_debug_errno(r, "No '/var/lib/systemd/nvpcr/nvpcr-anchor.cred' file.");
-                *ret_credential = (struct iovec) {};
-                return 0;
-        }
-        if (r < 0)
-                return log_error_errno(r, "Failed to read '/var/lib/systemd/nvpcr/nvpcr-anchor.cred': %m");
-
-        return 1;
-}
-
-static int tpm2_nvpcr_acquire_anchor_secret_from_credential(struct iovec *ret_credential, struct iovec *ret_secret) {
-        int r;
-
-        assert(ret_credential);
-        assert(ret_secret);
-
-        /* We need the anchor secret before the first measurement into an NvPCR. That means very early. Hence
-         * we'll try to pass it into the system via the system credentials logic. Because we must expect a
-         * multi-boot scenario it's hard to know which secret to use for which system. Hence we'll just try
-         * to unlock all of the available ones, until we can decrypt one of them, and then we'll use that. */
-
-        const char *dp;
-        r = get_encrypted_system_credentials_dir(&dp);
-        if (r < 0)
-                return log_error_errno(r, "Failed to get encrypted system credentials directory: %m");
-
-        /* Define early, so that it is definitely initialized, even if we take "goto not_found" branch below. */
-        _cleanup_free_ DirectoryEntries *de = NULL;
-
-        _cleanup_close_ int dfd = open(dp, O_CLOEXEC|O_DIRECTORY);
-        if (dfd < 0) {
-                if (errno == ENOENT) {
-                        log_debug("No encrypted system credentials passed.");
-                        goto not_found;
-                }
-
-                return log_error_errno(errno, "Failed to open system credentials directory.");
-        }
-
-        r = readdir_all(dfd, RECURSE_DIR_IGNORE_DOT, &de);
-        if (r < 0)
-                return log_error_errno(r, "Failed to enumerate system credentials: %m");
-
-        FOREACH_ARRAY(i, de->entries, de->n_entries) {
-                _cleanup_(iovec_done) struct iovec credential = {};
-                struct dirent *d = *i;
-
-                if (!startswith_no_case(d->d_name, "nvpcr-anchor.")) /* VFAT is case-insensitive, hence don't be too strict here */
-                        continue;
-
-                r = read_full_file_full(
-                                dfd,
-                                d->d_name,
-                                /* offset= */ UINT64_MAX,
-                                CREDENTIAL_ENCRYPTED_SIZE_MAX,
-                                READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER,
-                                /* bind_name= */ NULL,
-                                (char**) &credential.iov_base,
-                                &credential.iov_len);
-                if (r == -ENOENT)
-                        continue;
-                if (r < 0) {
-                        log_warning_errno(r, "Failed to read anchor secret file '%s/%s', skipping: %m", dp, d->d_name);
-                        continue;
-                }
-
-                r = decrypt_credential_and_warn(
-                                "nvpcr-anchor.cred",
-                                now(CLOCK_REALTIME),
-                                /* tpm2_device= */ NULL,
-                                /* tpm2_signature_path= */ NULL,
-                                /* uid= */ UID_INVALID,
-                                &credential,
-                                /* flags= */ 0,
-                                ret_secret);
-                if (r < 0)
-                        log_debug_errno(r, "Failed to decrypt anchor secret file '%s' passed in as system credential, skipping: %m", d->d_name);
-                else {
-                        *ret_credential = TAKE_STRUCT(credential);
-                        return 1;
-                }
-        }
-
-        log_debug("No suitable anchor secret passed as system credential.");
-
-not_found:
-        *ret_credential = (struct iovec) {};
-        *ret_secret = (struct iovec) {};
-        return 0;
-}
-#endif
-
-#define ANCHOR_SECRET_SIZE 4096U
-
-int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary) {
-#if HAVE_OPENSSL
-        _cleanup_close_ int fd = -EBADF;
-        int r;
-
-        /* Acquires the anchor secret. We store it in a credential. The primary location (and primary truth)
-         * for it is /run/systemd/nvpcr/ (i.e. volatile) [this file also doubles as lock file for the whole
-         * logic]. But something has to place it there once. We do keep two copies of it: one in
-         * /var/lib/systemd/nvpcr/, which is the persistent place for it, but which is only available at late
-         * boot, potentially. And one in the ESP/XBOOTLDR which will make it available in the initrd
-         * already via system credentials. */
-
-        _cleanup_close_ int dfd = open_mkdir("/run/systemd/nvpcr", O_CLOEXEC, 0755);
-        if (dfd < 0)
-                return log_error_errno(dfd, "Failed to open directory '/run/systemd/nvpcr': %m");
-
-        /* Use restrictive access mode of 0600. Not because the data inside needs to be kept inaccessible
-         * (it's encrypted, hence that'd be fine), but because we need to lock it, and unprivileged clients
-         * shouldn't be permitted to lock it. */
-        fd = openat(dfd, "nvpcr-anchor.cred", O_RDWR|O_CLOEXEC|O_CREAT|O_NOCTTY, 0644);
-        if (fd < 0)
-                return log_error_errno(errno, "Failed to open anchor secret: %m");
-
-        r = lock_generic(fd, LOCK_BSD, LOCK_SH);
-        if (r < 0)
-                return log_error_errno(r, "Failed to lock anchor secret file: %m");
-
-        struct stat st;
-        if (fstat(fd, &st) < 0)
-                return log_error_errno(errno, "Failed to stat() anchor secret: %m");
-
-        r = stat_verify_regular(&st);
-        if (r < 0)
-                return log_error_errno(r, "Anchor secret file is not a regular file: %m");
-
-        if (st.st_size == 0) {
-                /* If this is not initialized yet, then let's update the lock to an exclusive lock */
-                r = lock_generic(fd, LOCK_BSD, LOCK_EX);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to upgrade lock on anchor secret file: %m");
-
-                /* Refresh size info, in case someone else has initialized it by now */
-                if (fstat(fd, &st) < 0)
-                        return log_error_errno(errno, "Failed to stat() anchor secret: %m");
-        }
-
-        bool copy_to_var = true, copy_to_boot = true;
-
-        _cleanup_(iovec_done) struct iovec credential = {};
-        _cleanup_(iovec_done_erase) struct iovec secret = {};
-        if (st.st_size == 0) { /* No initialized yet? */
-
-                /* Check if we have a secret in /var/lib/systemd/nvpcr/. If so, import the secret from there */
-                if (!sync_secondary) {
-                        r = tpm2_nvpcr_acquire_anchor_secret_from_var(&credential);
-                        if (r < 0)
-                                return r;
-                        if (r > 0)
-                                copy_to_var = false; /* We read the secret from /var/, hence we don't have to copy it there. */
-                }
-
-                /* Did the copy_source logic work? If not, let's search for the secret among passed system credentials. */
-                if (!iovec_is_set(&credential)) {
-                        r = tpm2_nvpcr_acquire_anchor_secret_from_credential(&credential, &secret);
-                        if (r < 0)
-                                return r;
-                        if (r > 0)
-                                copy_to_boot = false; /* We read the secret from the boot partition, hence we don't have to copy it there. */
-                }
-
-                /* Did the copy_source or system credential logic work? If not, let's generate a new random one */
-                if (!iovec_is_set(&credential)) {
-                        r = crypto_random_bytes_allocate_iovec(ANCHOR_SECRET_SIZE, &secret);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to acquire entropy for anchor secret: %m");
-
-                        r = encrypt_credential_and_warn(
-                                        _CRED_AUTO_TPM2,
-                                        "nvpcr-anchor.cred",
-                                        now(CLOCK_REALTIME),
-                                        /* not_after= */ USEC_INFINITY,
-                                        /* tpm2_device= */ NULL,
-                                        /* tpm2_hash_pcr_mask= */ 0,
-                                        /* tpm2_pubkey_path= */ NULL,
-                                        /* tpm2_pubkey_pcrs= */ UINT32_MAX,
-                                        /* uid= */ UID_INVALID,
-                                        &secret,
-                                        /* flags= */ 0,
-                                        &credential);
-                        if (r < 0)
-                                return r;
-                }
-
-                _cleanup_free_ char *encoded = NULL;
-                ssize_t n = base64mem_full(credential.iov_base, credential.iov_len, 79, &encoded);
-                if (n < 0)
-                        return log_error_errno(n, "Failed to base64 encode credential: %m");
-
-                if (!strextend(&encoded, "\n"))
-                        return log_oom();
-
-                n++;
-
-                r = loop_write(fd, encoded, n);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to write anchor secret to disk: %m");
-        } else {
-                /* The file was already initialized? Then just read it. */
-                r = read_full_file_full(
-                                fd,
-                                /* filename= */ NULL,
-                                /* offset= */ UINT64_MAX,
-                                CREDENTIAL_ENCRYPTED_SIZE_MAX,
-                                READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER,
-                                /* bind_name= */ NULL,
-                                (char**) &credential.iov_base,
-                                &credential.iov_len);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to read anchor secret file: %m");
-        }
-
-        /* if we don't have the plaintext secret yet, then decrypt it now. */
-        if (!iovec_is_set(&secret)) {
-                assert(iovec_is_set(&credential));
-
-                r = decrypt_credential_and_warn(
-                                "nvpcr-anchor.cred",
-                                now(CLOCK_REALTIME),
-                                /* tpm2_device= */ NULL,
-                                /* tpm2_signature_path= */ NULL,
-                                /* uid= */ UID_INVALID,
-                                &credential,
-                                /* flags= */ 0,
-                                &secret);
-                if (r < 0)
-                        return r;
-        }
-
-        if (sync_secondary) {
-                if (copy_to_var) {
-                        r = tpm2_nvpcr_write_anchor_secret_to_var(&credential);
-                        if (r < 0)
-                                return r;
-                }
-
-                if (copy_to_boot) {
-                        r = tpm2_nvpcr_write_anchor_secret_to_boot(&credential);
-                        if (r < 0)
-                                return r;
-                }
-        }
-
-        if (ret)
-                *ret = TAKE_STRUCT(secret);
-        return 0;
-#else /* HAVE_OPENSSL */
-        return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled.");
-#endif
-}
-
 #if HAVE_OPENSSL
 static int tpm2_context_can_nvindex(Tpm2Context *c) {
         int r;
@@ -8658,14 +8680,21 @@ static int tpm2_context_can_nvindex(Tpm2Context *c) {
 }
 #endif
 
+/* Policy reference for the PolicyAuthorize assertion that guards the first write to an NvPCR. */
+#define NVPCR_INIT_POLICY_REF "initrd"
+
+/* PCR mask for the policy that guards the first write to an NvPCR, via the signed policy and PolicyAuthorize. */
+#define NVPCR_PUBKEY_PCRMASK (UINT32_C(1) << TPM2_PCR_KERNEL_BOOT)
+
+/* 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
+
 int tpm2_nvpcr_initialize(
                 Tpm2Context *c,
                 const Tpm2Handle *session,
-                const char *name,
-                const struct iovec *anchor_secret) {
+                const char *name) {
 
 #if HAVE_OPENSSL
-        TPM2_RC rc;
         int r;
 
         assert(c);
@@ -8682,114 +8711,144 @@ int tpm2_nvpcr_initialize(
         if (r < 0)
                 return r;
 
-        /* Open + lock the log file *before* we check for the *.anchor flag file. */
+        /* Open + lock the log file *before* we check for the *.auth flag file. */
         _cleanup_close_ int log_fd = tpm2_userspace_log_open();
 
         _cleanup_close_ int dfd = open_mkdir("/run/systemd/nvpcr", O_CLOEXEC, 0755);
         if (dfd < 0)
                 return log_debug_errno(dfd, "Failed to open directory '/run/systemd/nvpcr': %m");
 
-        const char *anchor_fname = strjoina(name, ".anchor");
-        if (faccessat(dfd, anchor_fname, F_OK, AT_SYMLINK_NOFOLLOW) < 0) {
+        const char *auth_fname = strjoina(name, ".auth");
+        if (faccessat(dfd, auth_fname, F_OK, AT_SYMLINK_NOFOLLOW) < 0) {
                 if (errno != ENOENT)
-                        return log_debug_errno(errno, "Failed to check if /run/systemd/nvpcr/%s exists: %m", anchor_fname);
+                        return log_debug_errno(errno, "Failed to check if /run/systemd/nvpcr/%s exists: %m", auth_fname);
         } else {
-                log_debug("NvPCR '%s' is already anchored.", name);
+                log_debug("NvPCR '%s' is already initialized.", name);
                 return 0;
         }
 
-        if (!iovec_is_set(anchor_secret))
-                return log_debug_errno(SYNTHETIC_ERRNO(EUNATCH), "Need anchor secret.");
-
-        const char *an = tpm2_hash_alg_to_string(p.algorithm);
-        if (!an)
-                return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported algorithm for NvPCR, refusing.");
-
         r = dlopen_libcrypto(LOG_DEBUG);
         if (r < 0)
                 return r;
 
-        const EVP_MD *implementation;
-        assert_se(implementation = sym_EVP_get_digestbyname(an));
-
-        int digest_size = sym_EVP_MD_get_size(implementation);
-        assert_se(digest_size > 0);
+        /* 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:
+         * - 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.
+         * - the name algorithm is plumbed through to tpm2_make_policy_session, tpm2_policy_authorize and
+         *   find_signature.
+         * - find_signature filters on the new policy algorithm field.
+         *
+         * Note that the policy algorithm is distinct from the signing algorithm. For PolicyAuthorize
+         * signatures, the signature algorithm has to match the name algorithm of the signing key, but this
+         * doesn't have to be the same as the algorithm of the signed policy. Signature algorithms are
+         * currently hardcoded to SHA256 too. */
+        if (p.algorithm != TPM2_ALG_SHA256)
+                return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Only SHA256 is currently supported for NvPCR, refusing.");
+
+        int digest_size = tpm2_hash_alg_to_size(p.algorithm);
+        if (digest_size < 0)
+                return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported algorithm for NvPCR, refusing.");
 
         if ((size_t) digest_size > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer))
                 return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Hash function result too large for TPM, refusing.");
 
-        /* Put together a buffer consisting if the nvindex number and the NvPCR name, that we can calculate an HMAC() off, see below */
-        size_t hmac_buffer_size = sizeof(le32_t) + strlen(p.name);
-        _cleanup_free_ void* hmac_buffer = malloc(hmac_buffer_size);
-        if (!hmac_buffer)
-                return log_oom_debug();
-
-        *(le32_t*) hmac_buffer = htole32(p.nv_index);
-        memcpy((uint8_t*) hmac_buffer + sizeof(le32_t), name, strlen(name));
-
-        TPM2B_MAX_NV_BUFFER buf = {
-                .size = digest_size,
-        };
-        CLEANUP_ERASE(buf);
-
-        /* We measure HMAC(anchor_secret, name) into the NvPCR to anchor it on our secret. */
-        if (!sym_HMAC(implementation, anchor_secret->iov_base, anchor_secret->iov_len, hmac_buffer, hmac_buffer_size, buf.buffer, NULL))
-                return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to calculate HMAC of data to measure.");
+        /* Load the PCR public key. Its signatures authorize the initializing write to the NvPCR via the
+         * PolicyAuthorize branch of the write policy. */
+        TPM2B_PUBLIC public;
+        _cleanup_(iovec_done) struct iovec fingerprint = {};
+        r = tpm2_nvpcr_load_pcr_public_key(/* path= */ NULL, &public, &fingerprint);
+        if (r < 0)
+                return r;
 
+        TPM2B_DIGEST authorize_policy;
         _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL;
         r = tpm2_define_nvpcr_nv_index(
                         c,
                         session,
                         p.nv_index,
                         p.algorithm,
+                        &public,
+                        NVPCR_INIT_POLICY_REF,
                         p.orderly,
+                        &authorize_policy,
                         &nv_handle);
         if (r < 0)
                 return r;
 
         log_debug("Successfully acquired handle to NV index 0x%" PRIx32 ".", p.nv_index);
 
-        bool reset_marker = tpm2_userspace_log_dirty(log_fd) >= 0;
-        rc = sym_Esys_NV_Extend(
-                        c->esys_context,
-                        /* authHandle= */ nv_handle->esys_handle,
-                        /* nvIndex= */ nv_handle->esys_handle,
-                        /* shandle1= */ ESYS_TR_PASSWORD,
-                        /* shandle2= */ ESYS_TR_NONE,
-                        /* shandle3= */ ESYS_TR_NONE,
-                        &buf);
-        if (rc != TSS2_RC_SUCCESS)
-                return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
-                                       "Failed to extend NV index: %s", sym_Tss2_RC_Decode(rc));
+        /* Load the signed PCR policy, which authorizes the initializing write. */
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *signature_json = NULL;
+        r = tpm2_load_pcr_signature(/* path= */ NULL, &signature_json);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to load PCR signature for NvPCR initialization: %m");
 
-        log_debug("Successfully extended NvPCR '%s' with anchor secret.", name);
+        bool reset_marker;
+        for (unsigned i = RETRY_NVPCR_INIT_MAX;; i--) {
+                /* Open a policy session to perform the initializing write. */
+                _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL;
+                r = tpm2_nvpcr_open_write_session(
+                                c,
+                                &public,
+                                NVPCR_INIT_POLICY_REF,
+                                NVPCR_PUBKEY_PCRMASK,
+                                &fingerprint,
+                                signature_json,
+                                &authorize_policy,
+                                &policy_session);
+                if (r < 0)
+                        return r;
 
-        /* Now pre-calculate the initial measurement of an "anchor" secret. This makes sure that others
-         * cannot delete and reproduce the same fake PCR, unless they also know the "anchor" secret. */
-        TPM2B_DIGEST start = { /* initialize to zero */
-                .size = digest_size,
-        };
-        r = tpm2_digest_buffer(
-                        p.algorithm,
-                        &start,
-                        buf.buffer,
-                        buf.size,
-                        /* extend= */ true);
-        if (r < 0)
-                return log_debug_errno(r, "Failed to calculate initial value: %m");
+                _cleanup_free_ void *zero = malloc0(digest_size);
+                if (!zero)
+                        return log_oom_debug();
+
+                reset_marker = tpm2_userspace_log_dirty(log_fd) >= 0;
+
+                r = tpm2_extend_nvpcr_nv_index(
+                                c,
+                                p.nv_index,
+                                nv_handle,
+                                policy_session,
+                                &IOVEC_MAKE(zero, digest_size));
+                if (r == -ESTALE) {
+                        /* PCR values changed under our feet. This shouldn't happen from userspace
+                         * measurements because we hold the measurement log lock, but the kernel can still
+                         * perform IMA measurements. */
+                        tpm2_userspace_log_clean(log_fd, reset_marker);
+                        if (i > 0)
+                                continue;
+                }
+                if (r < 0)
+                        return r;
 
-        /* Now create the anchor flag file */
-        _cleanup_free_ char *h = hexmem(start.buffer, start.size);
+                log_debug("Successfully performed initializing write to NvPCR '%s'.", name);
+                break;
+        }
+
+        /* Persist the PolicyAuthorize branch digest. The extend path needs it to reconstruct the
+         * TPM2_PolicyOR, and its presence doubles as an indication that this NvPCR is initialized. */
+        _cleanup_free_ char *h = hexmem(authorize_policy.buffer, authorize_policy.size);
         if (!h)
                 return log_oom_debug();
 
-        r = write_string_file_at(dfd, anchor_fname, h, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC);
+        r = write_string_file_at(dfd, auth_fname, h, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC);
         if (r < 0)
-                return log_debug_errno(r, "Failed to write anchor file: %m");
+                return log_debug_errno(r, "Failed to write auth file: %m");
 
         (void) tpm2_userspace_log_clean(log_fd, reset_marker);
         log_fd = safe_close(log_fd);
 
+        /* Acquire the name of the newly created index. This is done after the first write when the
+         * TPMA_NV_WRITTEN flag is already set. */
+        _cleanup_(Esys_Freep) TPM2B_NAME *nv_name = NULL;
+        r = tpm2_get_name(c, nv_handle, &nv_name);
+        if (r < 0)
+                return r;
+
         /* Now also measure the initialization into PCR 9, so that there's a trace of it in regular PCRs. You
          * might wonder why PCR 9? Well, we have very few PCRs available, and PCR 9 appears to be the least
          * bad for this. It typically contains stuff that in our world is hard to predict anyway
@@ -8804,8 +8863,12 @@ int tpm2_nvpcr_initialize(
         if (r < 0)
                 return log_error_errno(r, "Could not verify PCR banks: %m");
 
+        _cleanup_free_ char *nv_name_h = hexmem(nv_name->name, nv_name->size);
+        if (!nv_name_h)
+                return log_oom_debug();
+
         _cleanup_free_ char *word = NULL;
-        if (asprintf(&word, "nvpcr-init:%s:0x%x:%s:%s", name, p.nv_index, tpm2_hash_alg_to_string(p.algorithm), h) < 0)
+        if (asprintf(&word, "nvpcr-init:%s:0x%x:%s", name, p.nv_index, nv_name_h) < 0)
                 return log_oom();
 
         r = tpm2_pcr_extend_bytes(
@@ -8847,14 +8910,14 @@ int tpm2_nvpcr_read(
         if (r < 0)
                 return r;
 
-        /* Check if the NvPCR is already anchored */
-        const char *anchor_fname = strjoina("/run/systemd/nvpcr/", name, ".anchor");
-        r = access_nofollow(anchor_fname, F_OK);
+        /* Check if the NvPCR is already initialized */
+        const char *auth_fname = strjoina("/run/systemd/nvpcr/", name, ".auth");
+        r = access_nofollow(auth_fname, F_OK);
         if (r < 0) {
                 if (r != -ENOENT)
-                        return log_debug_errno(r, "Failed to check if '%s' exists: %m", anchor_fname);
+                        return log_debug_errno(r, "Failed to check if '%s' exists: %m", auth_fname);
 
-                /* valid, but not anchored */
+                /* valid, but not initialized */
                 *ret_value = (struct iovec) {};
                 if (ret_nv_index)
                         *ret_nv_index = p.nv_index;
@@ -10640,6 +10703,7 @@ int tpm2_load_pcr_signature(const char *path, sd_json_variant **ret) {
 }
 
 int tpm2_load_pcr_public_key(const char *path, void **ret_pubkey, size_t *ret_pubkey_size) {
+        _cleanup_strv_free_ char **search = NULL;
         _cleanup_free_ char *discovered_path = NULL;
         _cleanup_fclose_ FILE *f = NULL;
         int r;
@@ -10647,10 +10711,19 @@ int tpm2_load_pcr_public_key(const char *path, void **ret_pubkey, size_t *ret_pu
         /* Tries to load a PCR public key file. Takes an absolute path, a simple file name or NULL. In the
          * latter two cases searches in /etc/, /usr/lib/, /run/, as usual. */
 
-        if (!path)
+        search = strv_new(CONF_PATHS("systemd"));
+        if (!search)
+                return log_oom_debug();
+
+        if (!path) {
                 path = "tpm2-pcr-public-key.pem";
 
-        r = search_and_fopen(path, "re", NULL, (const char**) CONF_PATHS_STRV("systemd"), &f, &discovered_path);
+                if (in_initrd())
+                        if (strv_extend(&search, "/.extra") < 0)
+                                return log_oom_debug();
+        }
+
+        r = search_and_fopen(path, "re", NULL, (const char**) search, &f, &discovered_path);
         if (r < 0)
                 return log_debug_errno(r, "Failed to find TPM PCR public key file '%s': %m", path);
 
index d8da991784bf60a53be36c37bae5ccd5a712bf09..b2f89f3a5d02e8119a8eeba9e35ec3cf44c86e93 100644 (file)
@@ -188,9 +188,8 @@ int tpm2_pcr_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, cons
 #define TPM2_NVPCR_PRIORITY_DEFAULT UINT64_C(1000)
 
 int tpm2_nvpcr_get_index(const char *name, uint32_t *ret_nv_index, uint64_t *ret_priority);
-int tpm2_nvpcr_extend_bytes(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *data, const struct iovec *secret, bool sync_secondary_anchor, Tpm2UserspaceEventType event_type, const char *description);
-int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary);
-int tpm2_nvpcr_initialize(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *anchor_secret);
+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 tpm2_nvpcr_initialize(Tpm2Context *c, const Tpm2Handle *session, const char *name);
 int tpm2_nvpcr_read(Tpm2Context *c, const Tpm2Handle *session, const char *name, struct iovec *ret, uint32_t *ret_nv_index, uint64_t *ret_priority);
 
 uint32_t tpm2_tpms_pcr_selection_to_mask(const TPMS_PCR_SELECTION *s);
@@ -316,6 +315,7 @@ int tpm2_set_auth_binary(Tpm2Context *c, const Tpm2Handle *handle, const TPM2B_A
 int tpm2_make_policy_session(Tpm2Context *c, const Tpm2Handle *primary, const Tpm2Handle *encryption_session, Tpm2Handle **ret_session);
 
 int tpm2_policy_auth_value(Tpm2Context *c, const Tpm2Handle *session, TPM2B_DIGEST **ret_policy_digest);
+int tpm2_policy_nv_written(Tpm2Context *c, const Tpm2Handle *session, bool written_set, TPM2B_DIGEST **ret_policy_digest);
 int tpm2_policy_authorize_nv(Tpm2Context *c, const Tpm2Handle *session, const Tpm2Handle *nv_handle, TPM2B_DIGEST **ret_policy_digest);
 int tpm2_policy_pcr(Tpm2Context *c, const Tpm2Handle *session, const TPML_PCR_SELECTION *pcr_selection, TPM2B_DIGEST **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);
@@ -326,6 +326,7 @@ int tpm2_calculate_pubkey_name(const TPMT_PUBLIC *public, TPM2B_NAME *ret_name);
 int tpm2_calculate_nv_index_name(const TPMS_NV_PUBLIC *nvpublic, TPM2B_NAME *ret_name);
 
 int tpm2_calculate_policy_auth_value(TPM2B_DIGEST *digest);
+int tpm2_calculate_policy_nv_written(bool written_set, TPM2B_DIGEST *digest);
 int tpm2_calculate_policy_authorize(const TPM2B_PUBLIC *public, const TPM2B_NONCE *policy_ref, TPM2B_DIGEST *digest);
 int tpm2_calculate_policy_authorize_nv(const TPM2B_NV_PUBLIC *public, TPM2B_DIGEST *digest);
 int tpm2_calculate_policy_pcr(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, TPM2B_DIGEST *digest);
index 2b8c6fb051b4539da0432db9827d5c15d7316d96..b565a08de1c9b10f4ecaedfdf66a2031fffd6351 100644 (file)
@@ -998,6 +998,16 @@ TEST(calculate_policy_auth_value) {
         assert_se(digest_check(&d, "759ebd5ed65100e0b4aa2d04b4b789c2672d92ecc9cdda4b5fa16a303132e008"));
 }
 
+TEST(calculate_policy_nv_written) {
+        TPM2B_DIGEST d;
+
+        digest_init(&d, "0000000000000000000000000000000000000000000000000000000000000000");
+        ASSERT_OK_ZERO(tpm2_calculate_policy_nv_written(false, &d));
+        ASSERT_TRUE(digest_check(&d, "3c326323670e28ad37bd57f63b4cc34d26ab205ef22f275c58d47fab2485466e"));
+        ASSERT_OK_ZERO(tpm2_calculate_policy_nv_written(true, &d));
+        ASSERT_TRUE(digest_check(&d, "af595f3b5b4930da0d29921745c970bea2d8e6cdb9aa7cc40ce4916d0dc185fa"));
+}
+
 TEST(calculate_policy_authorize) {
         TPM2B_PUBLIC public;
         TPM2B_DIGEST d;
index 3c6cd2aeee89a3ba703a8b2c339234b9b923d93a..2b247efd236823cff79a269918b3c8b9a66c5716 100644 (file)
@@ -2,13 +2,16 @@
 
 #include <sys/stat.h>
 #include <sysexits.h>
+#include <unistd.h>
 
 #include "sd-messages.h"
 
 #include "alloc-util.h"
+#include "boot-entry.h"
 #include "build.h"
 #include "conf-files.h"
 #include "constants.h"
+#include "creds-util.h"
 #include "crypto-util.h"
 #include "dlopen-note.h"
 #include "errno-util.h"
@@ -22,7 +25,9 @@
 #include "mkdir.h"
 #include "options.h"
 #include "parse-util.h"
+#include "path-util.h"
 #include "pretty-print.h"
+#include "sd-id128.h"
 #include "set.h"
 #include "sort-util.h"
 #include "string-util.h"
@@ -368,10 +373,71 @@ static int setup_srk(void) {
         return 0;
 }
 
+static int cleanup_one_old_nvpcr_cred(const char *path) {
+        int r;
+
+        assert(path);
+
+        r = RET_NERRNO(unlink(path));
+        if (r == -ENOENT)
+                return 0;
+        if (r < 0)
+                return log_debug_errno(r, "Failed to unlink old NvPCR anchor secret credential '%s', ignoring: %m", path);
+
+        log_debug("Removed old NvPCR anchor secret credential '%s'.", path);
+        return 0;
+}
+
+static int cleanup_old_nvpcr_creds(void) {
+        int r;
+
+        if (arg_early)
+                return 0;
+
+        int j = cleanup_one_old_nvpcr_cred("/var/lib/systemd/nvpcr/nvpcr-anchor.cred");
+
+        _cleanup_free_ char *dir = NULL;
+        r = get_global_boot_credentials_path(&dir);
+        if (r <= 0)
+                return r;
+
+        sd_id128_t machine_id;
+        r = sd_id128_get_machine(&machine_id);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to read machine ID, not removing old NvPCR anchor secret boot credential: %m");
+
+        BootEntryTokenType entry_token_type = BOOT_ENTRY_TOKEN_AUTO;
+        _cleanup_free_ char *entry_token = NULL;
+        r = boot_entry_token_ensure(
+                        /* root= */ NULL,
+                        /* conf_root= */ NULL,
+                        machine_id,
+                        /* machine_id_is_random= */ false,
+                        &entry_token_type,
+                        &entry_token);
+        if (r < 0)
+                return r;
+
+        _cleanup_free_ char *fname = strjoin("nvpcr-anchor.", entry_token, ".cred");
+        if (!fname)
+                return log_oom();
+
+        if (!filename_is_valid(fname))
+                return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+                                       "'%s' is not a valid file name, not removing old NvPCR anchor secret boot credential.", fname);
+
+        _cleanup_free_ char *joined = path_join(dir, fname);
+        if (!joined)
+                return log_oom();
+
+        RET_GATHER(j, cleanup_one_old_nvpcr_cred(joined));
+
+        return j;
+}
+
 typedef struct SetupNvPCRContext {
         Tpm2Context *tpm2_context;
-        struct iovec anchor_secret;
-        size_t n_already, n_anchored, n_failed, n_skipped;
+        size_t n_already, n_initialized, n_failed, n_skipped;
         bool nv_space_exhausted; /* Set once the TPM ran out of NV index space, so we skip the rest. */
         Set *done;
 } SetupNvPCRContext;
@@ -379,7 +445,6 @@ typedef struct SetupNvPCRContext {
 static void setup_nvpcr_context_done(SetupNvPCRContext *c) {
         assert(c);
 
-        iovec_done_erase(&c->anchor_secret);
         c->tpm2_context = tpm2_context_unref(c->tpm2_context);
         c->done = set_free(c->done);
 }
@@ -405,19 +470,7 @@ static int setup_nvpcr_one(
                 return 0;
         }
 
-        r = tpm2_nvpcr_initialize(c->tpm2_context, /* session= */ NULL, name, &c->anchor_secret);
-        if (r == -EUNATCH) {
-                assert(!iovec_is_set(&c->anchor_secret));
-
-                /* If we get EUNATCH this means we actually need to initialize this NvPCR
-                 * now, and haven't provided the anchor secret yet. Hence acquire it now. */
-
-                r = tpm2_nvpcr_acquire_anchor_secret(&c->anchor_secret, /* sync_secondary= */ !arg_early);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to acquire anchor secret: %m");
-
-                r = tpm2_nvpcr_initialize(c->tpm2_context, /* session= */ NULL, name, &c->anchor_secret);
-        }
+        r = tpm2_nvpcr_initialize(c->tpm2_context, /* session= */ NULL, name);
         if (r == -EOPNOTSUPP) {
                 c->n_failed++;
                 return log_struct_errno(LOG_ERR, r,
@@ -436,11 +489,11 @@ static int setup_nvpcr_one(
         }
         if (r < 0) {
                 c->n_failed++;
-                return log_error_errno(r, "Failed to extend NvPCR index with anchor secret: %m");
+                return log_error_errno(r, "Failed to initialize NvPCR index: %m");
         }
 
         if (r > 0)
-                c->n_anchored++;
+                c->n_initialized++;
         else
                 c->n_already++;
 
@@ -474,6 +527,12 @@ static int setup_nvpcr(void) {
         _cleanup_(setup_nvpcr_context_done) SetupNvPCRContext c = {};
         int r;
 
+        /* NvPCRs can only be initialized from the initrd. */
+        if (!arg_early) {
+                log_debug("Skipping NvPCR setup in later boot phase.");
+                return 0;
+        }
+
         _cleanup_strv_free_ char **l = NULL;
         r = conf_files_list_nulstr(
                         &l,
@@ -524,23 +583,17 @@ static int setup_nvpcr(void) {
                 RET_GATHER(ret, setup_nvpcr_one(&c, e->name));
         }
 
-        if (c.n_already > 0 && c.n_anchored == 0 && !arg_early)
-                /* If we didn't anchor anything right now, but we anchored something earlier, then it might
-                 * have happened in the initrd, and thus the anchor ID was not committed to /var/ or the ESP
-                 * yet. Hence, let's explicitly do so now, to catch up. */
-                RET_GATHER(ret, tpm2_nvpcr_acquire_anchor_secret(/* ret= */ NULL, /* sync_secondary= */ true));
-
         if (c.nv_space_exhausted)
                 log_notice("Skipped %zu lowest-priority NvPCR(s) because the TPM's NV index space is exhausted, proceeding anyway.", c.n_skipped);
 
         if (c.n_failed > 0)
                 log_warning("%zu NvPCRs failed to initialize, proceeding anyway.", c.n_failed);
 
-        if (c.n_anchored > 0) {
+        if (c.n_initialized > 0) {
                 if (c.n_already == 0)
-                        log_info("%zu NvPCRs initialized.", c.n_anchored);
+                        log_info("%zu NvPCRs initialized.", c.n_initialized);
                 else
-                        log_info("%zu NvPCRs initialized. (%zu NvPCRs were already initialized.)", c.n_anchored, c.n_already);
+                        log_info("%zu NvPCRs initialized. (%zu NvPCRs were already initialized.)", c.n_initialized, c.n_already);
         } else if (c.n_already > 0)
                 log_info("%zu NvPCRs already initialized.", c.n_already);
         else if (c.n_failed == 0 && c.n_skipped == 0)
@@ -614,6 +667,8 @@ static int run(int argc, char *argv[]) {
 
         umask(0022);
 
+        (void) cleanup_old_nvpcr_creds();
+
         /* Execute all jobs, and then return unlisted errors preferably, and listed errors
          * (i.e. EX_UNAVAILABLE, EX_CANTCREAT, EX_PROTOCOL) otherwise. */
         r = setup_srk();
index 58ad37c752dd6c39c55e3574b40798a3b83d69cb..d61a7924ffdb933de4461c4d2dd37a58ea88dffe 100755 (executable)
@@ -272,6 +272,8 @@ def test_parse_args_many():
             '--no-measure',
             '--policy-digest',
             '--no-policy-digest',
+            '--sign-initrd-pcrs',
+            '--no-sign-initrd-pcrs',
         ]
     )
     assert opts.linux == pathlib.Path('/ARG1')
@@ -294,6 +296,7 @@ def test_parse_args_many():
     assert opts.output == pathlib.Path('OUTPUT')
     assert opts.measure is False
     assert opts.policy_digest is False
+    assert opts.sign_initrd_pcrs is False
 
 
 def test_parse_sections():
@@ -1126,6 +1129,58 @@ def test_pcr_signing3(kernel_initrd, tmp_path):
     shutil.rmtree(tmp_path)
 
 
+def test_pcr_signing_initrd_pcrs(kernel_initrd, tmp_path):
+    if kernel_initrd is None:
+        pytest.skip('linux+initrd not found')
+    try:
+        systemd_measure()
+    except ValueError:
+        pytest.skip('systemd-measure not found')
+
+    ourdir = pathlib.Path(__file__).parent
+    pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
+    priv = unbase64(ourdir / 'example.tpm2-pcr-private.pem.base64')
+
+    output = f'{tmp_path}/signed.efi'
+    args = [
+        'build',
+        *kernel_initrd,
+        f'--output={output}',
+        '--uname=1.2.3',
+        '--cmdline=ARG1 ARG2 ARG3',
+        '--os-release=ID=foobar\n',
+        '--pcr-banks=sha384',  # sha1 might not be allowed, use something else
+        f'--pcr-private-key={priv.name}',
+        f'--pcr-public-key={pub.name}',
+        '--sign-initrd-pcrs',
+    ] + arg_tools
+
+    opts = ukify.parse_args(args)
+    try:
+        ukify.check_inputs(opts)
+    except OSError as e:
+        pytest.skip(str(e))
+
+    ukify.make_uki(opts)
+
+    subprocess.check_call(
+        ['objcopy', f'--dump-section=.pcrsig={tmp_path}/out.pcrsig', output, tmp_path / 'dummy'],
+        text=True,
+    )
+
+    sig = json.loads(open(tmp_path / 'out.pcrsig').read())
+    assert list(sig.keys()) == ['sha384']
+    assert len(sig['sha384']) == 5  # five items for five phase paths
+    assert 'ref' not in sig['sha384'][0]
+    assert 'ref' not in sig['sha384'][1]
+    assert 'ref' not in sig['sha384'][2]
+    assert 'ref' not in sig['sha384'][3]
+    assert 'ref' in sig['sha384'][4]
+    assert sig['sha384'][4]['ref'] == 'initrd'
+
+    shutil.rmtree(tmp_path)
+
+
 def test_key_cert_generation(tmp_path):
     opts = ukify.parse_args(
         [
index e6f35ee86bab49f5a14848dc053d7768b24eaa2c..82d0c8676e8b819bd0c6561b7516f8fa362ac36e 100755 (executable)
@@ -291,8 +291,9 @@ class UkifyConfig:
     pcrpkey: Optional[Path]
     pcrsig: Union[str, Path, None]
     join_pcrsig: Optional[Path]
-    phase_path_groups: Optional[list[str]]
+    phase_path_groups: Optional[list[list[str]]]
     policyrefs: Optional[list[str]]
+    sign_initrd_pcrs: bool
     policy_digest: bool
     profile: Optional[str]
     sb_cert: Union[str, Path, None]
@@ -702,7 +703,10 @@ def check_cert_and_keys_nonexistent(opts: UkifyConfig) -> None:
     # Raise if any of the keys and certs are found on disk
     paths: Iterator[Union[str, Path, None]] = itertools.chain(
         (opts.sb_key, opts.sb_cert),
-        *((priv_key, pub_key, cert) for priv_key, pub_key, cert, _, _ in key_path_groups(opts)),
+        *(
+            (priv_key, pub_key, cert)
+            for priv_key, pub_key, cert, _, _ in key_path_groups(opts, include_extra=False)
+        ),
     )
     for path in paths:
         if path and Path(path).exists():
@@ -742,7 +746,8 @@ def combine_signatures(pcrsigs: list[dict[str, str]]) -> str:
 
 def key_path_groups(
     opts: UkifyConfig,
-) -> Iterator[tuple[str, Optional[str], Optional[str], Optional[str], Optional[str]]]:
+    include_extra: bool,
+) -> Iterator[tuple[str, Optional[str], Optional[str], Optional[list[str]], Optional[str]]]:
     if not opts.pcr_private_keys:
         return
 
@@ -761,6 +766,17 @@ def key_path_groups(
         fillvalue=None,
     )
 
+    # When requested, emit an extra signing group that reuses the first PCR signing key to produce
+    # a signed PCR policyy that can only be used from the initrd.
+    if opts.sign_initrd_pcrs and include_extra:
+        yield (
+            opts.pcr_private_keys[0],
+            pub_keys[0] if pub_keys else None,
+            certs[0] if certs else None,
+            ['enter-initrd'],
+            'initrd',
+        )
+
 
 def pe_strip_section_name(name: bytes) -> str:
     return name.rstrip(b'\x00').decode()
@@ -879,7 +895,7 @@ def call_systemd_measure(uki: UKI, opts: UkifyConfig, profile_start: int = 0) ->
                 *(f'--bank={bank}' for bank in banks),
             ]
 
-            for priv_key, pub_key, cert, group, ref in key_path_groups(opts):
+            for priv_key, pub_key, cert, group, ref in key_path_groups(opts, include_extra=True):
                 extra = [f'--private-key={priv_key}']
                 if opts.signing_engine is not None:
                     assert pub_key or cert
@@ -1685,7 +1701,7 @@ def generate_keys(opts: UkifyConfig) -> None:
 
         work = True
 
-    for priv_key, pub_key, _, _, _ in key_path_groups(opts):
+    for priv_key, pub_key, _, _, _ in key_path_groups(opts, include_extra=False):
         priv_key_pem, pub_key_pem = generate_priv_pub_key_pair()
 
         print(f'Writing private key for PCR signing to {priv_key}', file=sys.stderr)
@@ -2287,6 +2303,12 @@ CONFIG_ITEMS = [
         action=argparse.BooleanOptionalAction,
         help='print systemd-measure policy digests for the UKI',
     ),
+    ConfigItem(
+        '--sign-initrd-pcrs',
+        action=argparse.BooleanOptionalAction,
+        help='additionally sign PCR policies that can only be satisfied from the initrd',
+        config_key='UKI/SignInitrdPCRs',
+    ),
     ConfigItem(
         '--json',
         choices=('pretty', 'short', 'off'),
@@ -2481,6 +2503,8 @@ def finalize_options(opts: argparse.Namespace) -> None:
         raise ValueError('--phases= specifications must match --pcr-private-key=')
     if n_policyrefs is not None and n_policyrefs != n_pcr_priv:
         raise ValueError('--policyref= specifications must match --pcr-private-key=')
+    if opts.sign_initrd_pcrs and not n_pcr_priv:
+        raise ValueError('--sign-initrd-pcrs requires at least one --pcr-private-key=')
 
     opts.cmdline = resolve_at_path(opts.cmdline)
 
index 2c1c7b0e3ef0a191648c67914e3c1c0e2f573cd5..7666b4ec71e56954f2f7aefdab0400f08a9c417c 100755 (executable)
@@ -9,9 +9,10 @@ set -o pipefail
 export SYSTEMD_LOG_LEVEL=debug
 SD_PCREXTEND="/usr/lib/systemd/systemd-pcrextend"
 SD_TPM2SETUP="/usr/lib/systemd/systemd-tpm2-setup"
+SD_MEASURE="/usr/lib/systemd/systemd-measure"
 
-if [[ ! -x "${SD_PCREXTEND:?}" ]] || ! tpm_has_pcr sha256 11; then
-    echo "$SD_PCREXTEND or PCR sysfs files not found, skipping PCR extension tests"
+if [[ ! -x "${SD_PCREXTEND:?}" ]] || [[ ! -x "${SD_MEASURE:?}" ]] || ! tpm_has_pcr sha256 11; then
+    echo "$SD_PCREXTEND, $SD_MEASURE or PCR sysfs files not found, skipping PCR extension tests"
     exit 0
 fi
 
@@ -21,9 +22,13 @@ at_exit() {
         jq --seq --slurp </run/log/systemd/tpm2-measure.log
     fi
 
+    mv -f /run/systemd/tpm2-pcr-public-key.pem.bak /run/systemd/tpm2-pcr-public-key.pem
+    mv -f /run/systemd/tpm2-pcr-signature.json.bak /run/systemd/tpm2-pcr-signature.json
+    rm -f /tmp/tpm2-pcr-private-key.pem
     rm -rf /run/nvpcr /tmp/nvpcr
     rm -f /var/tmp/nvpcr.raw /run/verity.d/test-70-nvpcr.crt
-    rm -f /run/systemd/nvpcr/test.anchor /run/systemd/nvpcr/test2.anchor /run/systemd/nvpcr/aaa.anchor /run/systemd/nvpcr/zzz.anchor
+    rm -f /run/systemd/nvpcr/test.auth /run/systemd/nvpcr/test2.auth /run/systemd/nvpcr/aaa.auth /run/systemd/nvpcr/zzz.auth
+    rm -f /tmp/test.policy
 }
 
 trap at_exit EXIT
@@ -33,23 +38,42 @@ trap at_exit EXIT
 # QEMU guests.
 run_tpm2_setup() {
     local rc=0
-    "$SD_TPM2SETUP" || rc=$?
+    "$SD_TPM2SETUP" --early=yes || rc=$?
     [[ "$rc" -eq 0 || "$rc" -eq 69 ]]
 }
 
 # Temporarily override sd-pcrextend's sanity checks
 export SYSTEMD_FORCE_MEASURE=1
 
+# Temporarily override the PCR signing key and PCR signatures to create a signed
+# PCR policy that's valid for the current state
+mv /run/systemd/tpm2-pcr-public-key.pem /run/systemd/tpm2-pcr-public-key.pem.bak
+# XXX: This gets removed in TEST-70-TPM2.measure.sh, but the intention here is to preserve the original one.
+touch /run/systemd/tpm2-pcr-signature.json
+mv /run/systemd/tpm2-pcr-signature.json /run/systemd/tpm2-pcr-signature.json.bak
+openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "/tmp/tpm2-pcr-private-key.pem"
+openssl rsa -pubout -in "/tmp/tpm2-pcr-private-key.pem" -out "/run/systemd/tpm2-pcr-public-key.pem"
+"$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="initrd" >"/run/systemd/tpm2-pcr-signature.json"
+
 mkdir -p /run/nvpcr
 
 cat >/run/nvpcr/test.nvpcr <<EOF
 {"name":"test","algorithm":"sha256","nvIndex":30474762}
 EOF
 run_tpm2_setup
-test -f /run/systemd/nvpcr/test.anchor
-/usr/lib/systemd/systemd-pcrextend --nvpcr=test schrumpel
-# To calculate the current value we need the anchor measurement
-DIGEST_BASE="$(cat /run/systemd/nvpcr/test.anchor)"
+test -f /run/systemd/nvpcr/test.auth
+# The initial measurement is all zeroes
+DIGEST_BASE_UNINITIALIZED="0000000000000000000000000000000000000000000000000000000000000000"
+DIGEST_INITIAL_MEASUREMENT="0000000000000000000000000000000000000000000000000000000000000000"
+DIGEST_BASE_EXPECTED=$(echo "$DIGEST_BASE_UNINITIALIZED$DIGEST_INITIAL_MEASUREMENT" | tr '[:lower:]' '[:upper:]' | basenc --base16 -d | openssl dgst -sha256 -hex -r | cut -d' ' -f1)
+DIGEST_BASE="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .value')"
+test "$DIGEST_BASE" = "$DIGEST_BASE_EXPECTED"
+
+ANCHOR_MEASUREMENT_STRING_EXPECTED="nvpcr-init:test:0x1d1020a:$(tpm2_nvreadpublic 0x01d1020a | awk '/name:/{print $2; exit}')"
+ANCHOR_MEASUREMENT_STRING=$(jq --seq --slurp -r '[.[] | select(.content.eventType=="nvpcr-init") | .content.string] | last' </run/log/systemd/tpm2-measure.log)
+test "$ANCHOR_MEASUREMENT_STRING" = "$ANCHOR_MEASUREMENT_STRING_EXPECTED"
+
+"$SD_PCREXTEND" --nvpcr=test schrumpel
 DIGEST_MEASURED="$(echo -n "schrumpel" | openssl dgst -sha256 -hex -r | cut -d' ' -f1)"
 DIGEST_EXPECTED="$(echo "$DIGEST_BASE$DIGEST_MEASURED" | tr '[:lower:]' '[:upper:]' | basenc --base16 -d | openssl dgst -sha256 -hex -r | cut -d' ' -f1)"
 DIGEST_ACTUAL="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .value')"
@@ -65,6 +89,46 @@ DIGEST_MEASURED2="$(echo -n "schnurz" | openssl dgst -sha256 -hex -r | cut -d' '
 DIGEST_EXPECTED2="$(echo "$DIGEST_EXPECTED$DIGEST_MEASURED2" | tr '[:lower:]' '[:upper:]' | basenc --base16 -d | openssl dgst -sha256 -hex -r | cut -d' ' -f1)"
 test "$DIGEST_ACTUAL2" = "$DIGEST_EXPECTED2"
 
+# Make sure that systemd-tpm2-setup recognizes the pre-existing NV index as valid, to simulate
+# what we expect on a fresh boot.
+POLICY=$(tpm2_nvreadpublic 0x01d1020a | awk '/authorization policy:/{print $3; exit}')
+NAME_EXPECTED=$(tpm2_nvreadpublic 0x01d1020a | awk '/name:/{print $2; exit}')
+echo "$POLICY" | basenc --base16 -d >/tmp/test.policy
+rm -f /run/systemd/nvpcr/test.auth
+tpm2_nvundefine -C o 0x01d1020a
+tpm2_nvdefine -C o -s 32 -g sha256 -a "policywrite|ownerread|authread|orderly|clear_stclear|nt=extend" -L /tmp/test.policy 0x01d1020a
+run_tpm2_setup
+test -f /run/systemd/nvpcr/test.auth
+DIGEST_ACTUAL3="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .value')"
+test "$DIGEST_ACTUAL3" = "$DIGEST_BASE_EXPECTED"
+NAME_ACTUAL=$(tpm2_nvreadpublic 0x01d1020a | awk '/name:/{print $2; exit}')
+test "$NAME_ACTUAL" = "$NAME_EXPECTED"
+
+# Make sure that systemd-tpm2-setup is able to replace a pre-existing NvPCR index associated
+# with a different PCR signing key.
+OLD_POLICY="0000000000000000000000000000000000000000000000000000000000000000"
+echo "$OLD_POLICY" | basenc --base16 -d >/tmp/test.policy
+rm -f /run/systemd/nvpcr/test.auth
+tpm2_nvundefine -C o 0x01d1020a
+tpm2_nvdefine -C o -s 32 -g sha256 -a "policywrite|ownerread|authread|orderly|clear_stclear|nt=extend" -L /tmp/test.policy 0x01d1020a
+run_tpm2_setup
+test -f /run/systemd/nvpcr/test.auth
+DIGEST_ACTUAL4="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .value')"
+test "$DIGEST_ACTUAL4" = "$DIGEST_BASE_EXPECTED"
+NAME_ACTUAL=$(tpm2_nvreadpublic 0x01d1020a | awk '/name:/{print $2; exit}')
+test "$NAME_ACTUAL" = "$NAME_EXPECTED"
+
+# Make sure that systemd-tpm2-setup is able to upgrade old-style NvPCRs automatically.
+rm -f /run/systemd/nvpcr/test.auth
+tpm2_nvundefine -C o 0x01d1020a
+tpm2_nvdefine -C o -s 32 -g sha256 -a "ownerwrite|authwrite|ownerread|authread|orderly|clear_stclear|nt=extend" 0x01d1020a
+run_tpm2_setup
+test -f /run/systemd/nvpcr/test.auth
+DIGEST_ACTUAL5="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .value')"
+test "$DIGEST_ACTUAL5" = "$DIGEST_BASE_EXPECTED"
+NAME_ACTUAL=$(tpm2_nvreadpublic 0x01d1020a | awk '/name:/{print $2; exit}')
+test "$NAME_ACTUAL" = "$NAME_EXPECTED"
+
 # Verify the 'priority' field round-trips through the JSON definition. The 'test' NvPCR above sets no
 # priority, so it must report the default (1000).
 PRIORITY_DEFAULT="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .priority')"
@@ -92,6 +156,13 @@ 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 we can't redefine an NvPCR once we've exitted early boot by extending PCR11.
+rm -f /run/systemd/nvpcr/test.auth
+tpm2_nvundefine -C o 0x01d1020a
+"$SD_PCREXTEND" --pcr 11 "foo"
+SETUP_LOG="$(rc=0; $SD_TPM2SETUP --early=yes 2>&1 || rc=$?; [[ "$rc" -ne 0 ]] && [[ "$rc" -ne 69 ]])"
+grep -F "Failed to initialize NvPCR index: Device not a stream" <<<"$SETUP_LOG" >/dev/null
+
 # 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() {