diff --git a/docs/TPM2_PCR_MEASUREMENTS.md b/docs/TPM2_PCR_MEASUREMENTS.md
index abd280c004..b05739a8c7 100644
--- a/docs/TPM2_PCR_MEASUREMENTS.md
+++ b/docs/TPM2_PCR_MEASUREMENTS.md
@@ -199,6 +199,22 @@ initrd" in UTF-16.
→ **Measured hash** covers the per-UKI sysext cpio archive (which is generated
on-the-fly by `systemd-stub`).
+## PCR Measurements Made by `systemd-tpm2-setup` (Userspace)
+
+### PCR 9, NvPCR Initializations
+
+The `systemd-tpm2-setup.service` service initializes any NvPCRs defined via
+`*.nvpcr` files. For each initialized NvPCR it will measure an event into PCR
+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`
+
## PCR/NvPCR Measurements Made by `systemd-pcrextend` (Userspace)
### PCR 11, boot phases
@@ -244,6 +260,15 @@ colon-separated strings, identifying the file system type, UUID, label as well
as the GPT partition entry UUID, entry type UUID and entry label (in UTF-8,
without trailing NUL bytes).
+### PCR 9, NvPCR initialization separator
+
+After completion of `systemd-tpm2-setup.service` (which initializes all NvPCRs
+and measures their initial state) at arly boot the `systemd-pcrnvdone.service`
+service will measure a separator event into PCR 9, isolating the early-boot
+NvPCR initializations from any later additions.
+
+→ **Measured hash** covers the string `nvpcr-separator`.
+
## PCR/NvPCR Measurements Made by `systemd-cryptsetup` (Userspace)
### PCR 15, volume key
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 26eddb7791..667d538d4b 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -1106,8 +1106,10 @@ manpages = [
'systemd-pcrfs-root.service',
'systemd-pcrfs@.service',
'systemd-pcrmachine.service',
+ 'systemd-pcrnvdone.service',
'systemd-pcrphase-initrd.service',
- 'systemd-pcrphase-sysinit.service'],
+ 'systemd-pcrphase-sysinit.service',
+ 'systemd-pcrproduct.service'],
'ENABLE_BOOTLOADER HAVE_OPENSSL HAVE_TPM2'],
['systemd-portabled.service', '8', ['systemd-portabled'], 'ENABLE_PORTABLED'],
['systemd-poweroff.service',
diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml
index b95007dfcb..6b5ff05c3d 100644
--- a/man/systemd-pcrphase.service.xml
+++ b/man/systemd-pcrphase.service.xml
@@ -24,6 +24,7 @@
systemd-pcrproduct.servicesystemd-pcrfs-root.servicesystemd-pcrfs@.service
+ systemd-pcrnvdone.servicesystemd-pcrextendMeasure boot phases, machine ID, product UUID and file system identity into TPM PCRs and NvPCRs
@@ -33,8 +34,10 @@
systemd-pcrphase-sysinit.servicesystemd-pcrphase-initrd.servicesystemd-pcrmachine.service
+ systemd-pcrproduct.servicesystemd-pcrfs-root.servicesystemd-pcrfs@.service
+ systemd-pcrnvdone.service/usr/lib/systemd/systemd-pcrextendSTRING
@@ -54,6 +57,9 @@
product UUID (as provided by one of SMBIOS, Devicetree, …) into a NvPCR named
hardware.
+ systemd-pcrnvdone.service is a system service that measures a separator event
+ into PCR 9 once all NvPCRs have completed initialization.
+
systemd-pcrfs-root.service and systemd-pcrfs@.service are
services that measure file system identity information (i.e. mount point, file system type, label and
UUID, partition label and UUID) into PCR 15. systemd-pcrfs-root.service does so for
@@ -240,6 +246,17 @@
+
+
+
+ Set the event log event type for this measurement. Pass help for a
+ list of currently defined identifiers. Defaults to an appropriate value for
+ , , , and
+ otherwise to phase.
+
+
+
+
diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml
index 7f552a687f..d434434e62 100644
--- a/man/systemd.unit.xml
+++ b/man/systemd.unit.xml
@@ -1586,7 +1586,8 @@
tpm2
- Trusted Platform Module 2.0 (TPM2)
+ Trusted Platform Module 2.0 (TPM2) (with full UEFI support, including the TCG PC Client
+ Platform Firmware Profile)cvm
diff --git a/src/boot/measure.c b/src/boot/measure.c
index b1433ff1b5..22129cb87d 100644
--- a/src/boot/measure.c
+++ b/src/boot/measure.c
@@ -198,8 +198,8 @@ uint32_t tpm_get_active_pcr_banks(void) {
/* GetActivePcrBanks() was added only in version 1.1 of the spec */
if (version.Major < 1 || (version.Major == 1 && version.Minor < 1)) {
- log_debug("TCG protocol too old for GetActivePcrBanks(), claiming no active banks.");
- return 0;
+ log_debug("TCG protocol too old for GetActivePcrBanks(), returning wildcard bank information.");
+ return UINT32_MAX;
}
uint32_t active_pcr_banks = 0;
diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c
index 18d51ca214..7af7d3211e 100644
--- a/src/pcrextend/pcrextend.c
+++ b/src/pcrextend/pcrextend.c
@@ -15,6 +15,7 @@
#include "parse-argument.h"
#include "pcrextend-util.h"
#include "pretty-print.h"
+#include "string-table.h"
#include "string-util.h"
#include "strv.h"
#include "tpm2-pcr.h"
@@ -32,6 +33,7 @@ static unsigned arg_pcr_index = UINT_MAX;
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);
STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
@@ -65,6 +67,7 @@ static int help(int argc, char *argv[], void *userdata) {
" --machine-id Measure machine ID into PCR 15\n"
" --product-id Measure SMBIOS product ID into NvPCR 'hardware'\n"
" --early Run in early boot mode, without access to /var/\n"
+ " --event-type=TYPE Event type to include in the event log\n"
"\nSee the %2$s for details.\n",
program_invocation_short_name,
link,
@@ -88,6 +91,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_MACHINE_ID,
ARG_PRODUCT_ID,
ARG_EARLY,
+ ARG_EVENT_TYPE,
};
static const struct option options[] = {
@@ -102,6 +106,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "machine-id", no_argument, NULL, ARG_MACHINE_ID },
{ "product-id", no_argument, NULL, ARG_PRODUCT_ID },
{ "early", no_argument, NULL, ARG_EARLY },
+ { "event-type", required_argument, NULL, ARG_EVENT_TYPE },
{}
};
@@ -189,6 +194,15 @@ static int parse_argv(int argc, char *argv[]) {
arg_early = true;
break;
+ case ARG_EVENT_TYPE:
+ if (streq(optarg, "help"))
+ return DUMP_STRING_TABLE(tpm2_userspace_event_type, Tpm2UserspaceEventType, _TPM2_USERSPACE_EVENT_TYPE_MAX);
+
+ arg_event_type = tpm2_userspace_event_type_from_string(optarg);
+ if (arg_event_type < 0)
+ return log_error_errno(arg_event_type, "Failed to parse --event-type= argument: %s", optarg);
+ break;
+
case '?':
return -EINVAL;
@@ -446,7 +460,7 @@ static int vl_server(void) {
static int run(int argc, char *argv[]) {
_cleanup_free_ char *word = NULL;
- Tpm2UserspaceEventType event;
+ Tpm2UserspaceEventType event = _TPM2_USERSPACE_EVENT_TYPE_INVALID;
int r;
log_setup();
@@ -506,6 +520,10 @@ static int run(int argc, char *argv[]) {
event = TPM2_EVENT_PHASE;
}
+ /* Override with explicitly configured event type */
+ if (arg_event_type >= 0)
+ event = arg_event_type;
+
if (arg_graceful && !tpm2_is_fully_supported()) {
log_notice("No complete TPM2 support detected, exiting gracefully.");
return EXIT_SUCCESS;
diff --git a/src/shared/efi-api.c b/src/shared/efi-api.c
index bbc410ca50..ad9cbab74e 100644
--- a/src/shared/efi-api.c
+++ b/src/shared/efi-api.c
@@ -15,8 +15,14 @@
#include "stat-util.h"
#include "stdio-util.h"
#include "string-util.h"
+#include "tpm2-util.h"
#include "utf8.h"
+#define EFI_TCG2_BOOT_HASH_ALG_SHA1 0x01
+#define EFI_TCG2_BOOT_HASH_ALG_SHA256 0x02
+#define EFI_TCG2_BOOT_HASH_ALG_SHA384 0x04
+#define EFI_TCG2_BOOT_HASH_ALG_SHA512 0x08
+
#define LOAD_OPTION_ACTIVE 0x00000001
#define MEDIA_DEVICE_PATH 0x04
#define MEDIA_HARDDRIVE_DP 0x01
@@ -517,24 +523,71 @@ int efi_get_boot_options(uint16_t **ret_options) {
#endif
}
+int efi_get_active_pcr_banks(uint32_t *ret) {
#if ENABLE_EFI
-static int loader_has_tpm2(void) {
- _cleanup_free_ char *active_pcr_banks = NULL;
- uint32_t active_pcr_banks_value;
+ static uint32_t cache = 0;
+ static bool cache_valid = false;
int r;
- r = efi_get_variable_string(EFI_LOADER_VARIABLE_STR("LoaderTpm2ActivePcrBanks"), &active_pcr_banks);
- if (r < 0) {
- if (r != -ENOENT)
- log_debug_errno(r, "Failed to read LoaderTpm2ActivePcrBanks variable: %m");
- return r;
+ /* Returns the enabled PCR banks as bitmask, as reported by firmware. If the bitmask is returned as
+ * UINT32_MAX, the firmware supports the TCG protocol, but in a version too old to report this
+ * information. */
+
+ if (!cache_valid) {
+ _cleanup_free_ char *active_pcr_banks = NULL;
+ r = efi_get_variable_string(EFI_LOADER_VARIABLE_STR("LoaderTpm2ActivePcrBanks"), &active_pcr_banks);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to read LoaderTpm2ActivePcrBanks variable: %m");
+
+ uint32_t efi_bits;
+ r = safe_atou32_full(active_pcr_banks, 16, &efi_bits);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to parse LoaderTpm2ActivePcrBanks variable: %m");
+
+ if (efi_bits == UINT32_MAX)
+ /* UINT32_MAX means that the firmware API doesn't implement GetActivePcrBanks() and caller must guess */
+ cache = UINT32_MAX;
+ else {
+ /* EFI TPM protocol uses different bit values for the hash algorithms, let's convert */
+ static const struct {
+ uint32_t efi;
+ uint32_t tcg;
+ } table[] = {
+ { EFI_TCG2_BOOT_HASH_ALG_SHA1, 1U << TPM2_ALG_SHA1 },
+ { EFI_TCG2_BOOT_HASH_ALG_SHA256, 1U << TPM2_ALG_SHA256 },
+ { EFI_TCG2_BOOT_HASH_ALG_SHA384, 1U << TPM2_ALG_SHA384 },
+ { EFI_TCG2_BOOT_HASH_ALG_SHA512, 1U << TPM2_ALG_SHA512 },
+ };
+
+ uint32_t tcg_bits = 0;
+ FOREACH_ELEMENT(t, table)
+ SET_FLAG(tcg_bits, t->tcg, efi_bits & t->efi);
+
+ cache = tcg_bits;
+ }
+
+ cache_valid = true;
}
- r = safe_atou32_full(active_pcr_banks, 16, &active_pcr_banks_value);
- if (r < 0)
- return log_debug_errno(r, "Failed to parse LoaderTpm2ActivePcrBanks variable: %m");
+ if (ret)
+ *ret = cache;
- return active_pcr_banks_value != 0;
+ return 0;
+#else
+ return -EOPNOTSUPP;
+#endif
+}
+
+#if ENABLE_EFI
+static int loader_has_tpm2(void) {
+ uint32_t active_pcr_banks;
+ int r;
+
+ r = efi_get_active_pcr_banks(&active_pcr_banks);
+ if (r < 0)
+ return r;
+
+ return active_pcr_banks != 0;
}
#endif
diff --git a/src/shared/efi-api.h b/src/shared/efi-api.h
index 6fb3efb88a..98c55f8a38 100644
--- a/src/shared/efi-api.h
+++ b/src/shared/efi-api.h
@@ -18,6 +18,7 @@ int efi_get_boot_order(uint16_t **ret_order);
int efi_set_boot_order(const uint16_t *order, size_t n);
int efi_get_boot_options(uint16_t **ret_options);
+int efi_get_active_pcr_banks(uint32_t *ret);
bool efi_has_tpm2(void);
sd_id128_t efi_guid_to_id128(const void *guid);
diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c
index da74ca06a5..c74162302c 100644
--- a/src/shared/tpm2-util.c
+++ b/src/shared/tpm2-util.c
@@ -2697,6 +2697,28 @@ int tpm2_get_best_pcr_bank(
assert(c);
assert(ret);
+ uint32_t efi_banks;
+ r = efi_get_active_pcr_banks(&efi_banks);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return r;
+
+ /* If variable is not set use guesswork below */
+ log_debug("Boot loader didn't set the LoaderTpm2ActivePcrBanks EFI variable, we have to guess the used PCR banks.");
+ } else if (efi_banks == UINT32_MAX)
+ log_debug("Boot loader set the LoaderTpm2ActivePcrBanks EFI variable to indicate that the GetActivePcrBanks() API is not available in the firmware. We have to guess the used PCR banks.");
+ else {
+ if (BIT_SET(efi_banks, TPM2_ALG_SHA256))
+ *ret = TPM2_ALG_SHA256;
+ else if (BIT_SET(efi_banks, TPM2_ALG_SHA1))
+ *ret = TPM2_ALG_SHA1;
+ else
+ return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Firmware reports neither SHA1 nor SHA256 PCR banks, cannot operate.");
+
+ log_debug("Picked best PCR bank %s based on firmware reported banks.", tpm2_hash_alg_to_string(*ret));
+ return 0;
+ }
+
if (pcr_mask == 0) {
log_debug("Asked to pick best PCR bank but no PCRs selected we could derive this from. Defaulting to SHA256.");
*ret = TPM2_ALG_SHA256; /* if no PCRs are selected this doesn't matter anyway... */
@@ -2784,6 +2806,32 @@ int tpm2_get_good_pcr_banks(
assert(c);
assert(ret);
+ uint32_t efi_banks;
+ r = efi_get_active_pcr_banks(&efi_banks);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return r;
+
+ /* If the variable is not set we have to guess via the code below */
+ log_debug("Boot loader didn't set the LoaderTpm2ActivePcrBanks EFI variable, we have to guess the used PCR banks.");
+ } else if (efi_banks == UINT32_MAX)
+ log_debug("Boot loader set the LoaderTpm2ActivePcrBanks EFI variable to indicate that the GetActivePcrBanks() API is not available in the firmware. We have to guess the used PCR banks.");
+ else {
+ FOREACH_ARRAY(hash, tpm2_hash_algorithms, TPM2_N_HASH_ALGORITHMS) {
+ if (!BIT_SET(efi_banks, *hash))
+ continue;
+
+ if (!GREEDY_REALLOC(good_banks, n_good_banks+1))
+ return log_oom_debug();
+
+ good_banks[n_good_banks++] = *hash;
+ }
+
+ log_debug("Found %zu initialized TPM2 banks reported by firmware.", n_good_banks);
+ *ret = TAKE_PTR(good_banks);
+ return (int) n_good_banks;
+ }
+
FOREACH_TPMS_PCR_SELECTION_IN_TPML_PCR_SELECTION(selection, &c->capability_pcrs) {
TPMI_ALG_HASH hash = selection->hash;
@@ -6420,11 +6468,14 @@ static int json_dispatch_tpm2_algorithm(const char *name, sd_json_variant *varia
}
static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MAX] = {
- [TPM2_EVENT_PHASE] = "phase",
- [TPM2_EVENT_FILESYSTEM] = "filesystem",
- [TPM2_EVENT_VOLUME_KEY] = "volume-key",
- [TPM2_EVENT_MACHINE_ID] = "machine-id",
- [TPM2_EVENT_PRODUCT_ID] = "product-id",
+ [TPM2_EVENT_PHASE] = "phase",
+ [TPM2_EVENT_FILESYSTEM] = "filesystem",
+ [TPM2_EVENT_VOLUME_KEY] = "volume-key",
+ [TPM2_EVENT_MACHINE_ID] = "machine-id",
+ [TPM2_EVENT_PRODUCT_ID] = "product-id",
+ [TPM2_EVENT_KEYSLOT] = "keyslot",
+ [TPM2_EVENT_NVPCR_INIT] = "nvpcr-init",
+ [TPM2_EVENT_NVPCR_SEPARATOR] = "nvpcr-separator",
};
DEFINE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType);
@@ -7367,6 +7418,37 @@ int tpm2_nvpcr_initialize(
return log_debug_errno(r, "Failed to write anchor file: %m");
tpm2_userspace_log_clean(log_fd);
+ log_fd = safe_close(log_fd);
+
+ /* 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
+ * (i.e. possibly some overly verbose Grub stuff, as well as all initrds – those generated on-the-fly
+ * and those prepared beforehand – mangled into one), quite differently from all other PCRs we could
+ * use. Moreover PCR 11 already contains most stuff from PCR 9, as it contains the same data
+ * (i.e. initrds) in a more sensible fashion, clearly separated from on-the-fly generated ones. Note
+ * that we only do all this measurement stuff if we are booted as UKI, and hence when PCR 11 is
+ * available, but PCR 9 is not predictable. */
+ _cleanup_strv_free_ char **banks = NULL;
+ r = tpm2_get_good_pcr_banks_strv(c, UINT32_C(1) << TPM2_PCR_KERNEL_INITRD, &banks);
+ if (r < 0)
+ return log_error_errno(r, "Could not verify PCR banks: %m");
+
+ _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)
+ return log_oom();
+
+ r = tpm2_pcr_extend_bytes(
+ c,
+ banks,
+ TPM2_PCR_KERNEL_INITRD,
+ &IOVEC_MAKE_STRING(word),
+ /* secret= */ NULL,
+ TPM2_EVENT_NVPCR_INIT,
+ word);
+ if (r < 0)
+ return log_error_errno(r, "Could not extend PCR %i: %m", TPM2_PCR_KERNEL_INITRD);
+
return 1;
#else /* HAVE_OPENSSL */
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled.");
diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h
index abf8adfdaf..59b7ed9984 100644
--- a/src/shared/tpm2-util.h
+++ b/src/shared/tpm2-util.h
@@ -144,6 +144,8 @@ typedef enum Tpm2UserspaceEventType {
TPM2_EVENT_MACHINE_ID,
TPM2_EVENT_PRODUCT_ID,
TPM2_EVENT_KEYSLOT,
+ TPM2_EVENT_NVPCR_INIT,
+ TPM2_EVENT_NVPCR_SEPARATOR,
_TPM2_USERSPACE_EVENT_TYPE_MAX,
_TPM2_USERSPACE_EVENT_TYPE_INVALID = -EINVAL,
} Tpm2UserspaceEventType;
diff --git a/units/meson.build b/units/meson.build
index bd788f6d0b..8e5b645f91 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -581,6 +581,11 @@ units = [
'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
'symlinks' : ['sysinit.target.wants/'],
},
+ {
+ 'file' : 'systemd-pcrnvdone.service.in',
+ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
+ 'symlinks' : ['sysinit.target.wants/'],
+ },
{
'file' : 'systemd-tpm2-clear.service.in',
'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
diff --git a/units/systemd-pcrnvdone.service.in b/units/systemd-pcrnvdone.service.in
new file mode 100644
index 0000000000..e0dd9a8820
--- /dev/null
+++ b/units/systemd-pcrnvdone.service.in
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=TPM PCR NvPCR Initialization Separator
+Documentation=man:systemd-pcrnvdone.service(8)
+DefaultDependencies=no
+Conflicts=shutdown.target
+After=systemd-tpm2-setup-early.service systemd-tpm2-setup.service
+Before=sysinit.target shutdown.target
+ConditionSecurity=measured-uki
+ConditionPathExists=!/etc/initrd-release
+FailureAction=reboot-force
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart={{LIBEXECDIR}}/systemd-pcrextend --graceful --pcr=kernel-initrd --event-type=nvpcr-separator nvpcr-separator
diff --git a/units/systemd-pcrproduct.service.in b/units/systemd-pcrproduct.service.in
index a70fff19df..737ebff3bc 100644
--- a/units/systemd-pcrproduct.service.in
+++ b/units/systemd-pcrproduct.service.in
@@ -8,7 +8,7 @@
# (at your option) any later version.
[Unit]
-Description=TPM PCR Product ID Measurement
+Description=TPM NvPCR Product ID Measurement
Documentation=man:systemd-pcrproduct.service(8)
DefaultDependencies=no
Conflicts=shutdown.target