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:
committed by
Paul Meyer
parent
0d92898e85
commit
f0f3687700
@@ -64,6 +64,23 @@ CHANGES WITH 262:
|
|||||||
IPv4ProxyARP=yes but has no effect if IPv4ProxyARP= has been set to
|
IPv4ProxyARP=yes but has no effect if IPv4ProxyARP= has been set to
|
||||||
false. This mirrors the behavior of IPv6ProxyNDPAddress=.
|
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:
|
CHANGES WITH 261:
|
||||||
|
|
||||||
Announcements of Future Feature Removals and Incompatible Changes:
|
Announcements of Future Feature Removals and Incompatible Changes:
|
||||||
|
|||||||
@@ -90,16 +90,21 @@ fields are:
|
|||||||
There's one complication: these NV indexes (like any NV indexes) can be deleted
|
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
|
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
|
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
|
them. To prevent this, we extend the name of each NvPCR to PCR 9 (the name in this
|
||||||
NvPCRs. (Or actually, we measure a per-NvPCR secret we derive from a system
|
context is a cryptographic hash of the public attributes of the NV index), and we
|
||||||
secret via an HMAC of the NvPCR name and the NV index handle). This "anchoring"
|
only permit the first extend (write) to an NvPCR to be performed from the initrd.
|
||||||
secret is stored in `/run/` + `/var/lib/` + ESP/XBOOTLDR (the latter encrypted
|
If a NvPCR is recreated later on during runtime, it is not possible to reinitialize
|
||||||
as credential, locked to the TPM), to make it available at the whole runtime of
|
it by performing the first write in order to replay arbitrary measurements. This
|
||||||
the OS. It's only accessible to privileged processes with access to the
|
is achieved by using a write policy on the NV index with 2 branches. One of these
|
||||||
TPM. Due to this, any process with access to the TPM and read access to any of
|
branches permits writing without any further authorization if the NV index has
|
||||||
the storage locations of the anchor secret is considered part of the TCB, as
|
previously been written to. The other branch has to be satisfied in order to
|
||||||
they are able to replay the NvPCR with their own content at will, so due care
|
perform the first write, and this other branch is bound to a signed PCR policy that
|
||||||
must be employed when designing a system that uses this feature.
|
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)
|
## PCR Measurements Made by `systemd-boot` (UEFI)
|
||||||
|
|
||||||
@@ -282,12 +287,12 @@ The `systemd-tpm2-setup-early.service` service initializes any NvPCRs defined vi
|
|||||||
9.
|
9.
|
||||||
|
|
||||||
→ **Measured hash** covers the string `nvpcr-init:`, suffixed by the NvPCR
|
→ **Measured hash** covers the string `nvpcr-init:`, suffixed by the NvPCR
|
||||||
name, suffixed by `:0x`, suffixed by the NV Index handle (formatted in
|
readable name, suffixed by `:0x`, suffixed by the NV Index handle (formatted in
|
||||||
hexadecimal), suffixed by a colon, suffixed by the hash function used, in
|
hexadecimal), suffixed by a colon, and finally suffixed by the TPM name of the
|
||||||
lowercase (i.e. `sha256` or so), suffixed by a colon, and finally suffixed by
|
NvPCR in hexadecimal (where the TPM name is the cryptographic hash of the NV
|
||||||
the state of the NvPCR after its initialization with the anchor measurement, in
|
index public attributes, prefixed by the name algorithm - eg, `000b` for
|
||||||
hexadecimal. Example:
|
`sha256`). Example:
|
||||||
`nvpcr-init:hardware:0x1d10200:sha256:de3857f637c61e82f02e3722e1b207585fe9711045d863238904be8db10683f2`
|
`nvpcr-init:hardware:0x1d10200:000bff27bc66b6eda11bccd3a0ca07664ae4c360bcc3d7ef019d0b19cb5fa4067541`
|
||||||
|
|
||||||
## PCR/NvPCR Measurements Made by `systemd-pcrextend` (Userspace)
|
## PCR/NvPCR Measurements Made by `systemd-pcrextend` (Userspace)
|
||||||
|
|
||||||
|
|||||||
@@ -235,17 +235,6 @@
|
|||||||
<xi:include href="version-info.xml" xpointer="v253"/></listitem>
|
<xi:include href="version-info.xml" xpointer="v253"/></listitem>
|
||||||
</varlistentry>
|
</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>
|
<varlistentry>
|
||||||
<term><option>--machine-id</option></term>
|
<term><option>--machine-id</option></term>
|
||||||
|
|
||||||
|
|||||||
@@ -79,21 +79,6 @@
|
|||||||
<listitem><para>Definition files for NvPCRs.</para>
|
<listitem><para>Definition files for NvPCRs.</para>
|
||||||
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
|
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
|
||||||
</varlistentry>
|
</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>
|
</variablelist>
|
||||||
</refsect1>
|
</refsect1>
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,24 @@
|
|||||||
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
|
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
|
||||||
</varlistentry>
|
</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>
|
<varlistentry>
|
||||||
<term><option>--section=<replaceable>NAME</replaceable>:<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
|
<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>
|
<term><option>--section=<replaceable>NAME</replaceable>:text|binary<optional>@<replaceable>PATH</replaceable></optional></option></term>
|
||||||
|
|||||||
@@ -1145,7 +1145,6 @@ static int measure_keyslot(
|
|||||||
arg_tpm2_measure_keyslot_nvpcr,
|
arg_tpm2_measure_keyslot_nvpcr,
|
||||||
&IOVEC_MAKE_STRING(s),
|
&IOVEC_MAKE_STRING(s),
|
||||||
/* secret= */ NULL,
|
/* secret= */ NULL,
|
||||||
/* sync_secondary_anchor= */ false,
|
|
||||||
TPM2_EVENT_KEYSLOT,
|
TPM2_EVENT_KEYSLOT,
|
||||||
s);
|
s);
|
||||||
if (r < 0)
|
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[]) {
|
static int run(int argc, char *argv[]) {
|
||||||
int r;
|
int r;
|
||||||
|
|
||||||
LIBBLKID_NOTE(recommended);
|
|
||||||
LIBCRYPTO_NOTE(recommended);
|
LIBCRYPTO_NOTE(recommended);
|
||||||
LIBCRYPTSETUP_NOTE(required);
|
LIBCRYPTSETUP_NOTE(required);
|
||||||
LIBFIDO2_NOTE(suggested);
|
LIBFIDO2_NOTE(suggested);
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ static UserRecord *arg_login = NULL;
|
|||||||
static uint32_t arg_pcr_mask = 0;
|
static uint32_t arg_pcr_mask = 0;
|
||||||
static char *arg_nvpcr_name = NULL;
|
static char *arg_nvpcr_name = NULL;
|
||||||
static bool arg_varlink = false;
|
static bool arg_varlink = false;
|
||||||
static bool arg_early = false;
|
|
||||||
static Tpm2UserspaceEventType arg_event_type = _TPM2_USERSPACE_EVENT_TYPE_INVALID;
|
static Tpm2UserspaceEventType arg_event_type = _TPM2_USERSPACE_EVENT_TYPE_INVALID;
|
||||||
|
|
||||||
STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
|
STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
|
||||||
@@ -177,11 +176,6 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
OPTION_LONG("early", NULL,
|
|
||||||
"Run in early boot mode, without access to /var/"):
|
|
||||||
arg_early = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
OPTION_LONG("event-type", "TYPE",
|
OPTION_LONG("event-type", "TYPE",
|
||||||
"Event type to include in the event log"):
|
"Event type to include in the event log"):
|
||||||
if (streq(opts.arg, "help"))
|
if (streq(opts.arg, "help"))
|
||||||
@@ -379,7 +373,6 @@ static int extend_nvpcr_now(
|
|||||||
name,
|
name,
|
||||||
&IOVEC_MAKE(data, size),
|
&IOVEC_MAKE(data, size),
|
||||||
/* secret= */ NULL,
|
/* secret= */ NULL,
|
||||||
/* sync_secondary_anchor= */ !arg_early,
|
|
||||||
event,
|
event,
|
||||||
safe);
|
safe);
|
||||||
if (r == -ENOBUFS)
|
if (r == -ENOBUFS)
|
||||||
|
|||||||
+620
-547
File diff suppressed because it is too large
Load Diff
@@ -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)
|
#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_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_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_acquire_anchor_secret(struct iovec *ret, bool sync_secondary);
|
int tpm2_nvpcr_initialize(Tpm2Context *c, const Tpm2Handle *session, const char *name);
|
||||||
int tpm2_nvpcr_initialize(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *anchor_secret);
|
|
||||||
int tpm2_nvpcr_read(Tpm2Context *c, const Tpm2Handle *session, const char *name, struct iovec *ret, uint32_t *ret_nv_index, uint64_t *ret_priority);
|
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);
|
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_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_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_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_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);
|
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_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_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(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_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);
|
int tpm2_calculate_policy_pcr(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, TPM2B_DIGEST *digest);
|
||||||
|
|||||||
@@ -998,6 +998,16 @@ TEST(calculate_policy_auth_value) {
|
|||||||
assert_se(digest_check(&d, "759ebd5ed65100e0b4aa2d04b4b789c2672d92ecc9cdda4b5fa16a303132e008"));
|
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) {
|
TEST(calculate_policy_authorize) {
|
||||||
TPM2B_PUBLIC public;
|
TPM2B_PUBLIC public;
|
||||||
TPM2B_DIGEST d;
|
TPM2B_DIGEST d;
|
||||||
|
|||||||
+82
-27
@@ -2,13 +2,16 @@
|
|||||||
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sysexits.h>
|
#include <sysexits.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "sd-messages.h"
|
#include "sd-messages.h"
|
||||||
|
|
||||||
#include "alloc-util.h"
|
#include "alloc-util.h"
|
||||||
|
#include "boot-entry.h"
|
||||||
#include "build.h"
|
#include "build.h"
|
||||||
#include "conf-files.h"
|
#include "conf-files.h"
|
||||||
#include "constants.h"
|
#include "constants.h"
|
||||||
|
#include "creds-util.h"
|
||||||
#include "crypto-util.h"
|
#include "crypto-util.h"
|
||||||
#include "dlopen-note.h"
|
#include "dlopen-note.h"
|
||||||
#include "errno-util.h"
|
#include "errno-util.h"
|
||||||
@@ -22,7 +25,9 @@
|
|||||||
#include "mkdir.h"
|
#include "mkdir.h"
|
||||||
#include "options.h"
|
#include "options.h"
|
||||||
#include "parse-util.h"
|
#include "parse-util.h"
|
||||||
|
#include "path-util.h"
|
||||||
#include "pretty-print.h"
|
#include "pretty-print.h"
|
||||||
|
#include "sd-id128.h"
|
||||||
#include "set.h"
|
#include "set.h"
|
||||||
#include "sort-util.h"
|
#include "sort-util.h"
|
||||||
#include "string-util.h"
|
#include "string-util.h"
|
||||||
@@ -368,10 +373,71 @@ static int setup_srk(void) {
|
|||||||
return 0;
|
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 {
|
typedef struct SetupNvPCRContext {
|
||||||
Tpm2Context *tpm2_context;
|
Tpm2Context *tpm2_context;
|
||||||
struct iovec anchor_secret;
|
size_t n_already, n_initialized, n_failed, n_skipped;
|
||||||
size_t n_already, n_anchored, n_failed, n_skipped;
|
|
||||||
bool nv_space_exhausted; /* Set once the TPM ran out of NV index space, so we skip the rest. */
|
bool nv_space_exhausted; /* Set once the TPM ran out of NV index space, so we skip the rest. */
|
||||||
Set *done;
|
Set *done;
|
||||||
} SetupNvPCRContext;
|
} SetupNvPCRContext;
|
||||||
@@ -379,7 +445,6 @@ typedef struct SetupNvPCRContext {
|
|||||||
static void setup_nvpcr_context_done(SetupNvPCRContext *c) {
|
static void setup_nvpcr_context_done(SetupNvPCRContext *c) {
|
||||||
assert(c);
|
assert(c);
|
||||||
|
|
||||||
iovec_done_erase(&c->anchor_secret);
|
|
||||||
c->tpm2_context = tpm2_context_unref(c->tpm2_context);
|
c->tpm2_context = tpm2_context_unref(c->tpm2_context);
|
||||||
c->done = set_free(c->done);
|
c->done = set_free(c->done);
|
||||||
}
|
}
|
||||||
@@ -405,19 +470,7 @@ static int setup_nvpcr_one(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 == -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);
|
|
||||||
}
|
|
||||||
if (r == -EOPNOTSUPP) {
|
if (r == -EOPNOTSUPP) {
|
||||||
c->n_failed++;
|
c->n_failed++;
|
||||||
return log_struct_errno(LOG_ERR, r,
|
return log_struct_errno(LOG_ERR, r,
|
||||||
@@ -436,11 +489,11 @@ static int setup_nvpcr_one(
|
|||||||
}
|
}
|
||||||
if (r < 0) {
|
if (r < 0) {
|
||||||
c->n_failed++;
|
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)
|
if (r > 0)
|
||||||
c->n_anchored++;
|
c->n_initialized++;
|
||||||
else
|
else
|
||||||
c->n_already++;
|
c->n_already++;
|
||||||
|
|
||||||
@@ -474,6 +527,12 @@ static int setup_nvpcr(void) {
|
|||||||
_cleanup_(setup_nvpcr_context_done) SetupNvPCRContext c = {};
|
_cleanup_(setup_nvpcr_context_done) SetupNvPCRContext c = {};
|
||||||
int r;
|
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;
|
_cleanup_strv_free_ char **l = NULL;
|
||||||
r = conf_files_list_nulstr(
|
r = conf_files_list_nulstr(
|
||||||
&l,
|
&l,
|
||||||
@@ -524,23 +583,17 @@ static int setup_nvpcr(void) {
|
|||||||
RET_GATHER(ret, setup_nvpcr_one(&c, e->name));
|
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)
|
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);
|
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)
|
if (c.n_failed > 0)
|
||||||
log_warning("%zu NvPCRs failed to initialize, proceeding anyway.", c.n_failed);
|
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)
|
if (c.n_already == 0)
|
||||||
log_info("%zu NvPCRs initialized.", c.n_anchored);
|
log_info("%zu NvPCRs initialized.", c.n_initialized);
|
||||||
else
|
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)
|
} else if (c.n_already > 0)
|
||||||
log_info("%zu NvPCRs already initialized.", c.n_already);
|
log_info("%zu NvPCRs already initialized.", c.n_already);
|
||||||
else if (c.n_failed == 0 && c.n_skipped == 0)
|
else if (c.n_failed == 0 && c.n_skipped == 0)
|
||||||
@@ -614,6 +667,8 @@ static int run(int argc, char *argv[]) {
|
|||||||
|
|
||||||
umask(0022);
|
umask(0022);
|
||||||
|
|
||||||
|
(void) cleanup_old_nvpcr_creds();
|
||||||
|
|
||||||
/* Execute all jobs, and then return unlisted errors preferably, and listed errors
|
/* Execute all jobs, and then return unlisted errors preferably, and listed errors
|
||||||
* (i.e. EX_UNAVAILABLE, EX_CANTCREAT, EX_PROTOCOL) otherwise. */
|
* (i.e. EX_UNAVAILABLE, EX_CANTCREAT, EX_PROTOCOL) otherwise. */
|
||||||
r = setup_srk();
|
r = setup_srk();
|
||||||
|
|||||||
@@ -272,6 +272,8 @@ def test_parse_args_many():
|
|||||||
'--no-measure',
|
'--no-measure',
|
||||||
'--policy-digest',
|
'--policy-digest',
|
||||||
'--no-policy-digest',
|
'--no-policy-digest',
|
||||||
|
'--sign-initrd-pcrs',
|
||||||
|
'--no-sign-initrd-pcrs',
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert opts.linux == pathlib.Path('/ARG1')
|
assert opts.linux == pathlib.Path('/ARG1')
|
||||||
@@ -294,6 +296,7 @@ def test_parse_args_many():
|
|||||||
assert opts.output == pathlib.Path('OUTPUT')
|
assert opts.output == pathlib.Path('OUTPUT')
|
||||||
assert opts.measure is False
|
assert opts.measure is False
|
||||||
assert opts.policy_digest is False
|
assert opts.policy_digest is False
|
||||||
|
assert opts.sign_initrd_pcrs is False
|
||||||
|
|
||||||
|
|
||||||
def test_parse_sections():
|
def test_parse_sections():
|
||||||
@@ -1126,6 +1129,58 @@ def test_pcr_signing3(kernel_initrd, tmp_path):
|
|||||||
shutil.rmtree(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):
|
def test_key_cert_generation(tmp_path):
|
||||||
opts = ukify.parse_args(
|
opts = ukify.parse_args(
|
||||||
[
|
[
|
||||||
|
|||||||
+29
-5
@@ -291,8 +291,9 @@ class UkifyConfig:
|
|||||||
pcrpkey: Optional[Path]
|
pcrpkey: Optional[Path]
|
||||||
pcrsig: Union[str, Path, None]
|
pcrsig: Union[str, Path, None]
|
||||||
join_pcrsig: Optional[Path]
|
join_pcrsig: Optional[Path]
|
||||||
phase_path_groups: Optional[list[str]]
|
phase_path_groups: Optional[list[list[str]]]
|
||||||
policyrefs: Optional[list[str]]
|
policyrefs: Optional[list[str]]
|
||||||
|
sign_initrd_pcrs: bool
|
||||||
policy_digest: bool
|
policy_digest: bool
|
||||||
profile: Optional[str]
|
profile: Optional[str]
|
||||||
sb_cert: Union[str, Path, None]
|
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
|
# Raise if any of the keys and certs are found on disk
|
||||||
paths: Iterator[Union[str, Path, None]] = itertools.chain(
|
paths: Iterator[Union[str, Path, None]] = itertools.chain(
|
||||||
(opts.sb_key, opts.sb_cert),
|
(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:
|
for path in paths:
|
||||||
if path and Path(path).exists():
|
if path and Path(path).exists():
|
||||||
@@ -742,7 +746,8 @@ def combine_signatures(pcrsigs: list[dict[str, str]]) -> str:
|
|||||||
|
|
||||||
def key_path_groups(
|
def key_path_groups(
|
||||||
opts: UkifyConfig,
|
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:
|
if not opts.pcr_private_keys:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -761,6 +766,17 @@ def key_path_groups(
|
|||||||
fillvalue=None,
|
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:
|
def pe_strip_section_name(name: bytes) -> str:
|
||||||
return name.rstrip(b'\x00').decode()
|
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),
|
*(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}']
|
extra = [f'--private-key={priv_key}']
|
||||||
if opts.signing_engine is not None:
|
if opts.signing_engine is not None:
|
||||||
assert pub_key or cert
|
assert pub_key or cert
|
||||||
@@ -1685,7 +1701,7 @@ def generate_keys(opts: UkifyConfig) -> None:
|
|||||||
|
|
||||||
work = True
|
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()
|
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)
|
print(f'Writing private key for PCR signing to {priv_key}', file=sys.stderr)
|
||||||
@@ -2287,6 +2303,12 @@ CONFIG_ITEMS = [
|
|||||||
action=argparse.BooleanOptionalAction,
|
action=argparse.BooleanOptionalAction,
|
||||||
help='print systemd-measure policy digests for the UKI',
|
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(
|
ConfigItem(
|
||||||
'--json',
|
'--json',
|
||||||
choices=('pretty', 'short', 'off'),
|
choices=('pretty', 'short', 'off'),
|
||||||
@@ -2481,6 +2503,8 @@ def finalize_options(opts: argparse.Namespace) -> None:
|
|||||||
raise ValueError('--phases= specifications must match --pcr-private-key=')
|
raise ValueError('--phases= specifications must match --pcr-private-key=')
|
||||||
if n_policyrefs is not None and n_policyrefs != n_pcr_priv:
|
if n_policyrefs is not None and n_policyrefs != n_pcr_priv:
|
||||||
raise ValueError('--policyref= specifications must match --pcr-private-key=')
|
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)
|
opts.cmdline = resolve_at_path(opts.cmdline)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ set -o pipefail
|
|||||||
export SYSTEMD_LOG_LEVEL=debug
|
export SYSTEMD_LOG_LEVEL=debug
|
||||||
SD_PCREXTEND="/usr/lib/systemd/systemd-pcrextend"
|
SD_PCREXTEND="/usr/lib/systemd/systemd-pcrextend"
|
||||||
SD_TPM2SETUP="/usr/lib/systemd/systemd-tpm2-setup"
|
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
|
if [[ ! -x "${SD_PCREXTEND:?}" ]] || [[ ! -x "${SD_MEASURE:?}" ]] || ! tpm_has_pcr sha256 11; then
|
||||||
echo "$SD_PCREXTEND or PCR sysfs files not found, skipping PCR extension tests"
|
echo "$SD_PCREXTEND, $SD_MEASURE or PCR sysfs files not found, skipping PCR extension tests"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -21,9 +22,13 @@ at_exit() {
|
|||||||
jq --seq --slurp </run/log/systemd/tpm2-measure.log
|
jq --seq --slurp </run/log/systemd/tpm2-measure.log
|
||||||
fi
|
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 -rf /run/nvpcr /tmp/nvpcr
|
||||||
rm -f /var/tmp/nvpcr.raw /run/verity.d/test-70-nvpcr.crt
|
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
|
trap at_exit EXIT
|
||||||
@@ -33,23 +38,42 @@ trap at_exit EXIT
|
|||||||
# QEMU guests.
|
# QEMU guests.
|
||||||
run_tpm2_setup() {
|
run_tpm2_setup() {
|
||||||
local rc=0
|
local rc=0
|
||||||
"$SD_TPM2SETUP" || rc=$?
|
"$SD_TPM2SETUP" --early=yes || rc=$?
|
||||||
[[ "$rc" -eq 0 || "$rc" -eq 69 ]]
|
[[ "$rc" -eq 0 || "$rc" -eq 69 ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Temporarily override sd-pcrextend's sanity checks
|
# Temporarily override sd-pcrextend's sanity checks
|
||||||
export SYSTEMD_FORCE_MEASURE=1
|
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
|
mkdir -p /run/nvpcr
|
||||||
|
|
||||||
cat >/run/nvpcr/test.nvpcr <<EOF
|
cat >/run/nvpcr/test.nvpcr <<EOF
|
||||||
{"name":"test","algorithm":"sha256","nvIndex":30474762}
|
{"name":"test","algorithm":"sha256","nvIndex":30474762}
|
||||||
EOF
|
EOF
|
||||||
run_tpm2_setup
|
run_tpm2_setup
|
||||||
test -f /run/systemd/nvpcr/test.anchor
|
test -f /run/systemd/nvpcr/test.auth
|
||||||
/usr/lib/systemd/systemd-pcrextend --nvpcr=test schrumpel
|
# The initial measurement is all zeroes
|
||||||
# To calculate the current value we need the anchor measurement
|
DIGEST_BASE_UNINITIALIZED="0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
DIGEST_BASE="$(cat /run/systemd/nvpcr/test.anchor)"
|
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_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_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')"
|
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)"
|
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"
|
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
|
# 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, so it must report the default (1000).
|
||||||
PRIORITY_DEFAULT="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .priority')"
|
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)"
|
ZZZ_LINE="$(echo "$SETUP_LOG" | grep -n "Setting up NvPCR 'zzz'" | cut -d: -f1)"
|
||||||
test "$ZZZ_LINE" -lt "$AAA_LINE"
|
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.
|
# Test the --login= mode and the 'login' NvPCR, used in production by systemd-pcrlogin@.service.
|
||||||
if [[ -f /usr/lib/nvpcr/login.nvpcr ]]; then
|
if [[ -f /usr/lib/nvpcr/login.nvpcr ]]; then
|
||||||
login_nvpcr_value() {
|
login_nvpcr_value() {
|
||||||
|
|||||||
Reference in New Issue
Block a user