From 89bfa9239e7092fb4cce7ef64bbbb68eefbe5fd2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Nov 2025 22:34:27 +0100 Subject: [PATCH 1/9] tpm2-util: add missing entry in string table Follow-up for: e5a2e7866572614e66cbf6c1d3969128562d9552 --- src/shared/tpm2-util.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index da74ca06a5..356c3e4909 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6425,6 +6425,7 @@ static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MA [TPM2_EVENT_VOLUME_KEY] = "volume-key", [TPM2_EVENT_MACHINE_ID] = "machine-id", [TPM2_EVENT_PRODUCT_ID] = "product-id", + [TPM2_EVENT_KEYSLOT] = "keyslot", }; DEFINE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType); From d70296bb563338f59d3be998a3049ad74ebd7a32 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Nov 2025 22:35:30 +0100 Subject: [PATCH 2/9] tpm2-setup: measure information about NvPCR initialization to PCR 9 This locks down NvPCR initilization a bit more: we'll measure each initialization of an NvPCR into PCR 9, thus chaining the NvPCRs to the PCR set. After all NvPCRs are initialized we measure a barrier into PCR 9 as well. This ensures that later additions of NvPCRs are clearly recognizable and distuingishable from those done at boot. --- docs/TPM2_PCR_MEASUREMENTS.md | 16 ++++++++++++++++ src/shared/tpm2-util.c | 32 ++++++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 1 + 3 files changed, 49 insertions(+) diff --git a/docs/TPM2_PCR_MEASUREMENTS.md b/docs/TPM2_PCR_MEASUREMENTS.md index abd280c004..7b29069a7e 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 diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 356c3e4909..ad61fd1d8b 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6426,6 +6426,7 @@ static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MA [TPM2_EVENT_MACHINE_ID] = "machine-id", [TPM2_EVENT_PRODUCT_ID] = "product-id", [TPM2_EVENT_KEYSLOT] = "keyslot", + [TPM2_EVENT_NVPCR_INIT] = "nvpcr-init", }; DEFINE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType); @@ -7368,6 +7369,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..8dfe87af07 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -144,6 +144,7 @@ typedef enum Tpm2UserspaceEventType { TPM2_EVENT_MACHINE_ID, TPM2_EVENT_PRODUCT_ID, TPM2_EVENT_KEYSLOT, + TPM2_EVENT_NVPCR_INIT, _TPM2_USERSPACE_EVENT_TYPE_MAX, _TPM2_USERSPACE_EVENT_TYPE_INVALID = -EINVAL, } Tpm2UserspaceEventType; From 86dc140b9fe632aff129be91d54877a857dc8db5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 13 Nov 2025 14:46:24 +0100 Subject: [PATCH 3/9] pcrextend: allow setting the event type for the event log on the command line This makes the tool more powerful as we can invoke it for any type of measurement correctly --- man/systemd-pcrphase.service.xml | 11 +++++++++++ src/pcrextend/pcrextend.c | 20 +++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml index b95007dfcb..7832e10f85 100644 --- a/man/systemd-pcrphase.service.xml +++ b/man/systemd-pcrphase.service.xml @@ -240,6 +240,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/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; From 867e64737a1761e313c371abfb43ab2c04b9e568 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 13 Nov 2025 14:47:57 +0100 Subject: [PATCH 4/9] units: measure a separator event into PCR 9 after completing NvPCR initialization We do this in a separate service (rather than inside of systemd-tpm2-setup), since we want failures of this measurement to result in an instant reboot, like for most our measurements. Failures to initialize nvpcrs, or allocate an SRK are somewhat OK (and more likely), as long as this separator communicates clearly where they have to have taken place, if they worked. --- docs/TPM2_PCR_MEASUREMENTS.md | 9 +++++++++ man/rules/meson.build | 1 + man/systemd-pcrphase.service.xml | 5 +++++ src/shared/tpm2-util.c | 15 ++++++++------- src/shared/tpm2-util.h | 1 + units/meson.build | 5 +++++ units/systemd-pcrnvdone.service.in | 24 ++++++++++++++++++++++++ 7 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 units/systemd-pcrnvdone.service.in diff --git a/docs/TPM2_PCR_MEASUREMENTS.md b/docs/TPM2_PCR_MEASUREMENTS.md index 7b29069a7e..b05739a8c7 100644 --- a/docs/TPM2_PCR_MEASUREMENTS.md +++ b/docs/TPM2_PCR_MEASUREMENTS.md @@ -260,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..6ffc088453 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1106,6 +1106,7 @@ manpages = [ 'systemd-pcrfs-root.service', 'systemd-pcrfs@.service', 'systemd-pcrmachine.service', + 'systemd-pcrnvdone.service', 'systemd-pcrphase-initrd.service', 'systemd-pcrphase-sysinit.service'], 'ENABLE_BOOTLOADER HAVE_OPENSSL HAVE_TPM2'], diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml index 7832e10f85..1d543fe403 100644 --- a/man/systemd-pcrphase.service.xml +++ b/man/systemd-pcrphase.service.xml @@ -24,6 +24,7 @@ systemd-pcrproduct.service systemd-pcrfs-root.service systemd-pcrfs@.service + systemd-pcrnvdone.service systemd-pcrextend Measure boot phases, machine ID, product UUID and file system identity into TPM PCRs and NvPCRs @@ -35,6 +36,7 @@ systemd-pcrmachine.service systemd-pcrfs-root.service systemd-pcrfs@.service + systemd-pcrnvdone.service /usr/lib/systemd/systemd-pcrextend STRING @@ -54,6 +56,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 diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index ad61fd1d8b..1603b5f316 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6420,13 +6420,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_KEYSLOT] = "keyslot", - [TPM2_EVENT_NVPCR_INIT] = "nvpcr-init", + [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); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 8dfe87af07..59b7ed9984 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -145,6 +145,7 @@ typedef enum Tpm2UserspaceEventType { 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 From 7643e4a89ca12049e65ad139160c7b762e060ee2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Nov 2025 23:34:52 +0100 Subject: [PATCH 5/9] tpm2-util: use LoaderTpm2ActivePcrBanks efi var when figuring out best+good banks to use We nowadays have clear reporting which PCR banks the firmware is using via LoaderTpm2ActivePcrBanks, hence rely on that. --- src/shared/efi-api.c | 65 ++++++++++++++++++++++++++++++++++-------- src/shared/efi-api.h | 1 + src/shared/tpm2-util.c | 44 ++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 12 deletions(-) diff --git a/src/shared/efi-api.c b/src/shared/efi-api.c index bbc410ca50..608001b6e2 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,59 @@ 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 = UINT32_MAX; 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; + if (cache == UINT32_MAX) { + _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"); + + /* 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; } - 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 1603b5f316..de7e3d9728 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -2697,6 +2697,26 @@ 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 (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 +2804,30 @@ 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 { + 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; From b2b8fb810509442ceca33555d47a04ff527b3da7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 13 Nov 2025 09:30:43 +0100 Subject: [PATCH 6/9] boot: report missing GetActivePcrBanks() call in formware as UINT32_MAX PCR bank mask Fixes: #39150 --- src/boot/measure.c | 4 ++-- src/shared/efi-api.c | 44 +++++++++++++++++++++++++++--------------- src/shared/tpm2-util.c | 8 ++++++-- 3 files changed, 36 insertions(+), 20 deletions(-) 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/shared/efi-api.c b/src/shared/efi-api.c index 608001b6e2..ad9cbab74e 100644 --- a/src/shared/efi-api.c +++ b/src/shared/efi-api.c @@ -525,10 +525,15 @@ int efi_get_boot_options(uint16_t **ret_options) { int efi_get_active_pcr_banks(uint32_t *ret) { #if ENABLE_EFI - static uint32_t cache = UINT32_MAX; + static uint32_t cache = 0; + static bool cache_valid = false; int r; - if (cache == UINT32_MAX) { + /* 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) @@ -539,22 +544,29 @@ int efi_get_active_pcr_banks(uint32_t *ret) { if (r < 0) return log_debug_errno(r, "Failed to parse LoaderTpm2ActivePcrBanks variable: %m"); - /* 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 }, - }; + 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); + uint32_t tcg_bits = 0; + FOREACH_ELEMENT(t, table) + SET_FLAG(tcg_bits, t->tcg, efi_bits & t->efi); - cache = tcg_bits; + cache = tcg_bits; + } + + cache_valid = true; } if (ret) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index de7e3d9728..c74162302c 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -2705,7 +2705,9 @@ int tpm2_get_best_pcr_bank( /* 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 { + } 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)) @@ -2812,7 +2814,9 @@ int tpm2_get_good_pcr_banks( /* 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 { + } 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; From a84202edc59bbdc969d60423b4a360a5bbc65946 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 13 Nov 2025 09:42:43 +0100 Subject: [PATCH 7/9] man: document that ConditionSecurity=tpm2 means full UEFI/PC Client profile support TPM2 support is not too useful if the firmware doesn't actually use it for the boot chain, hence we require the full PC client profile support. Let's make that clear in the docs. Fixes: #38939 --- man/systemd.unit.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From d51599bbed47db64137a046f6c3675709010c410 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 13 Nov 2025 15:33:47 +0100 Subject: [PATCH 8/9] units: systemd-pcrproduct.service measures into an NvPCR, fix that in Description= Follow-up for: 0196abbd10331f89bd5ca7abc39225852dff7406 --- units/systemd-pcrproduct.service.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 45c305f9b8fae4e1f7706b6d4c374d588fb6ea38 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Nov 2025 22:06:53 +0100 Subject: [PATCH 9/9] man: add pcrproduct to man page header, too --- man/rules/meson.build | 3 ++- man/systemd-pcrphase.service.xml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/man/rules/meson.build b/man/rules/meson.build index 6ffc088453..667d538d4b 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1108,7 +1108,8 @@ manpages = [ '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 1d543fe403..6b5ff05c3d 100644 --- a/man/systemd-pcrphase.service.xml +++ b/man/systemd-pcrphase.service.xml @@ -34,6 +34,7 @@ systemd-pcrphase-sysinit.service systemd-pcrphase-initrd.service systemd-pcrmachine.service + systemd-pcrproduct.service systemd-pcrfs-root.service systemd-pcrfs@.service systemd-pcrnvdone.service