tpm2: Improve how NvPCR protection works.

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.
This commit is contained in:
Chris Coulson
2026-07-28 08:35:39 +02:00
committed by Paul Meyer
parent 0d92898e85
commit f0f3687700
14 changed files with 935 additions and 641 deletions
+17
View File
@@ -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:
+21 -16
View 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)
-11
View File
@@ -235,17 +235,6 @@
<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>
-15
View File
@@ -79,21 +79,6 @@
<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>
+18
View File
@@ -232,6 +232,24 @@
<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>
-2
View 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);
-7
View 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)
+589 -516
View File
File diff suppressed because it is too large Load Diff
+4 -3
View 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);
+10
View 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;
+82 -27
View 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();
+55
View File
@@ -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(
[
+29 -5
View File
@@ -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)
+79 -8
View File
@@ -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() {