5 TPM tweaks (#39712)

Fixes: #38939
Fixes: #39150
This commit is contained in:
Lennart Poettering
2025-11-14 23:54:45 +01:00
committed by GitHub
13 changed files with 253 additions and 23 deletions

View File

@@ -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

View File

@@ -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',

View File

@@ -24,6 +24,7 @@
<refname>systemd-pcrproduct.service</refname>
<refname>systemd-pcrfs-root.service</refname>
<refname>systemd-pcrfs@.service</refname>
<refname>systemd-pcrnvdone.service</refname>
<refname>systemd-pcrextend</refname>
<refpurpose>Measure boot phases, machine ID, product UUID and file system identity into TPM PCRs and NvPCRs</refpurpose>
</refnamediv>
@@ -33,8 +34,10 @@
<para><filename>systemd-pcrphase-sysinit.service</filename></para>
<para><filename>systemd-pcrphase-initrd.service</filename></para>
<para><filename>systemd-pcrmachine.service</filename></para>
<para><filename>systemd-pcrproduct.service</filename></para>
<para><filename>systemd-pcrfs-root.service</filename></para>
<para><filename>systemd-pcrfs@.service</filename></para>
<para><filename>systemd-pcrnvdone.service</filename></para>
<para><filename>/usr/lib/systemd/systemd-pcrextend</filename> <optional><replaceable>STRING</replaceable></optional></para>
</refsynopsisdiv>
@@ -54,6 +57,9 @@
product UUID (as provided by one of SMBIOS, Devicetree, …) into a NvPCR named
<literal>hardware</literal>.</para>
<para><filename>systemd-pcrnvdone.service</filename> is a system service that measures a separator event
into PCR 9 once all NvPCRs have completed initialization.</para>
<para><filename>systemd-pcrfs-root.service</filename> and <filename>systemd-pcrfs@.service</filename> 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. <filename>systemd-pcrfs-root.service</filename> does so for
@@ -240,6 +246,17 @@
<xi:include href="version-info.xml" xpointer="v253"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--event-type=</option></term>
<listitem><para>Set the event log event type for this measurement. Pass <literal>help</literal> for a
list of currently defined identifiers. Defaults to an appropriate value for
<option>--machine-id</option>, <option>--product-id</option>, <option>--file-system=</option>, and
otherwise to <literal>phase</literal>.</para>
<xi:include href="version-info.xml" xpointer="v259"/></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />

View File

@@ -1586,7 +1586,8 @@
</row>
<row>
<entry>tpm2</entry>
<entry>Trusted Platform Module 2.0 (TPM2)</entry>
<entry>Trusted Platform Module 2.0 (TPM2) (with full UEFI support, including the TCG PC Client
Platform Firmware Profile)</entry>
</row>
<row>
<entry>cvm</entry>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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.");

View File

@@ -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;

View File

@@ -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'],

View File

@@ -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

View File

@@ -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