From 23b4c833a4e1c0dd38b649dc5791c12cac91b403 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 9 Mar 2026 13:06:42 +0100 Subject: [PATCH 01/12] fileio: add WRITE_DATA_FILE_MODE_0400 mode --- src/basic/fileio.c | 2 +- src/basic/fileio.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 66d06484dc..7edf54edf3 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -1706,7 +1706,7 @@ int write_data_file_atomic_at( return r; } - r = fchmod_umask(fd, 0644); + r = fchmod_umask(fd, FLAGS_SET(flags, WRITE_DATA_FILE_MODE_0400) ? 0400 : 0644); if (r < 0) return r; diff --git a/src/basic/fileio.h b/src/basic/fileio.h index 3e2372c4dd..274fdfbd7c 100644 --- a/src/basic/fileio.h +++ b/src/basic/fileio.h @@ -166,6 +166,7 @@ int fopen_mode_to_flags(const char *mode); typedef enum WriteDataFileFlags { WRITE_DATA_FILE_MKDIR_0755 = 1 << 0, + WRITE_DATA_FILE_MODE_0400 = 1 << 1, } WriteDataFileFlags; int write_data_file_atomic_at(int dir_fd, const char *path, const struct iovec *iovec, WriteDataFileFlags flags); From 6718ba1769184d3e7ebe06801d44d55e30e92ac8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 9 Mar 2026 18:52:20 +0100 Subject: [PATCH 02/12] gpt-auto-generator: generate an initrd ESP mount if it makes sense We need to store state persistently for the software TPM (i.e. the root key). But given that TPMs are generally used to unlock the rootfs, this storage cannot be on the rootfs. Hence let's use the ESP instead, as the next best thing, that is guaranteed to exist during early boot, given we just were booted from it. This defines automatic logic for this, but does not cause the ESP mount job to be enqueued (since typically we don't actually want that mounted), this is left for the actual services that needs to be done. Note that the mount here is set up quite differently from the one from the host: since initrds are short-lived anyway, it seemed pointless to use autofs. Moreover this uses a fixed place to mount the ESP, inspired by the /sysroot/ + /sysusr/ mount naming. All that to simplify things a bit for the consumers (which is mostly swtpm) --- man/systemd-gpt-auto-generator.xml | 13 +++- src/gpt-auto-generator/gpt-auto-generator.c | 73 ++++++++++++++++++--- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/man/systemd-gpt-auto-generator.xml b/man/systemd-gpt-auto-generator.xml index e267fc9528..6494a79d22 100644 --- a/man/systemd-gpt-auto-generator.xml +++ b/man/systemd-gpt-auto-generator.xml @@ -75,6 +75,13 @@ discovery based on the boot loader reported ESP which is also enabled if no root= parameter is specified at all. (The latter relies on systemd-udevd.service's /dev/gpt-auto-root block device symlink generation). + + It will also generate a unit file mounting the EFI System Partition (ESP) to + /sysefi/, if applicable. Unlike the file systems mentioned above this mount is not + activated by default however, but can be pulled in by services requiring ESP access from within the + initrd. Note that this mount point is initrd-specific and does not make use of autofs. The ESP is + typically mounted at a different place and via autofs once the system transitions out of the + initrd. @@ -179,7 +186,7 @@ SD_GPT_ESP c12a7328-f81f-11d2-ba4b-00a0c93ec93b EFI System Partition (ESP) - /efi/ or /boot/ + /efi/ or /boot/ once the system transitioned out of the initrd, /sysefi/ before The first partition with this type UUID located on the same disk as the root partition is mounted to /boot/ or /efi/, see below. @@ -259,7 +266,9 @@ The ESP is mounted to /boot/ if that directory exists and is not used for XBOOTLDR, and otherwise to /efi/. Same as for /boot/, an - automount unit is used. The mount point will be created if necessary. + automount unit is used. The mount point will be created if necessary. These apply once the system + transitioned out of the initrd phase. Before that, if components in the initrd require ESP access, it + will be mounted to /sysefi/. No configuration is created for mount points that are configured in fstab5 or when diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 4fd92a6057..a87b514080 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -307,7 +307,8 @@ static int add_mount( MountPointFlags flags, const char *options, const char *description, - const char *post) { + const char *post, + const char *conflicts) { _cleanup_free_ char *unit = NULL, *crypto_what = NULL, *opts_filtered = NULL; _cleanup_fclose_ FILE *f = NULL; @@ -377,6 +378,12 @@ static int add_mount( if (r < 0) return r; + if (conflicts) + fprintf(f, + "Conflicts=%1$s\n" + "Before=%1$s\n", + conflicts); + fprintf(f, "\n" "[Mount]\n" @@ -493,7 +500,8 @@ static int add_partition_mount( (STR_IN_SET(id, "root", "var") ? MOUNT_MEASURE : 0), /* by default measure rootfs and /var, since they contain the "identity" of the system */ options, description, - SPECIAL_LOCAL_FS_TARGET); + SPECIAL_LOCAL_FS_TARGET, + /* conflicts= */ NULL); } static int add_partition_swap(DissectedPartition *p) { @@ -582,7 +590,8 @@ static int add_automount( flags, options, description, - /* post= */ NULL); + /* post= */ NULL, + /* conflicts= */ NULL); if (r < 0) return r; @@ -919,7 +928,8 @@ static int add_root_mount(void) { MOUNT_MEASURE, options, "Root Partition", - in_initrd() ? SPECIAL_INITRD_ROOT_FS_TARGET : SPECIAL_LOCAL_FS_TARGET); + in_initrd() ? SPECIAL_INITRD_ROOT_FS_TARGET : SPECIAL_LOCAL_FS_TARGET, + /* conflicts= */ NULL); #else return 0; #endif @@ -995,7 +1005,8 @@ static int add_usr_mount(void) { /* flags= */ 0, options, "/usr/ Partition", - in_initrd() ? SPECIAL_INITRD_USR_FS_TARGET : SPECIAL_LOCAL_FS_TARGET); + in_initrd() ? SPECIAL_INITRD_USR_FS_TARGET : SPECIAL_LOCAL_FS_TARGET, + /* conflicts= */ NULL); if (r < 0) return r; @@ -1009,7 +1020,8 @@ static int add_usr_mount(void) { MOUNT_VALIDATEFS, "bind", "/usr/ Partition (Final)", - SPECIAL_INITRD_FS_TARGET); + SPECIAL_INITRD_FS_TARGET, + /* conflicts= */ NULL); if (r < 0) return r; } @@ -1017,6 +1029,49 @@ static int add_usr_mount(void) { return 0; } +static int add_early_esp_mount(void) { + int r; + + /* Early ESP discovery is a bit different than the other mounts here: it's purely about the initrd, + * and goes away during the transition to the host (where it might likely be mounted again, but then + * via autofs, hence lazily). Moreover, the location is fixed → /sysefi/, i.e. we do not bother with + * XBOOTLDR vs. ESP for this. Also, the mount is not pulled in by default, but is expected to be + * pulled in by the component that uses it. + * + * The initial usecase for this is software TPM that needs a place to store its state before the root + * file system can be mounted. + * + * Or in other words: this is much simpler, more focussed on a short-lived boot-time operation than + * the regular logic during later boot. */ + + if (!in_initrd()) + return 0; + + if (!is_efi_boot()) + return 0; + + _cleanup_free_ char *options = NULL; + r = partition_pick_mount_options( + PARTITION_ESP, + "vfat", + /* rw= */ true, + /* discard= */ false, + &options, + /* ret_ms_flags= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to pick ESP mount options: %m"); + + return add_mount("esp", + "/dev/disk/by-designator/esp", + "/sysefi/", + "vfat", + MOUNT_RW, + options, + "EFI System Partition (Early)", + /* post= */ NULL, + /* conflicts= */ "initrd-switch-root.target"); +} + static int process_loader_partitions(DissectedPartition *esp, DissectedPartition *xbootldr) { sd_id128_t loader_uuid; int r; @@ -1190,7 +1245,7 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat return 0; /* Disable root disk logic if there's a root= value specified (unless it happens to be - * "gpt-auto" or "gpt-auto-force") */ + * "gpt-auto", "gpt-auto-force", "dissect", "dissect-force") */ arg_auto_root = parse_gpt_auto_root("root=", value); assert(arg_auto_root >= 0); @@ -1244,9 +1299,6 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat if (proc_cmdline_value_missing(key, value)) return 0; - /* Disable root disk logic if there's a root= value specified (unless it happens to be - * "gpt-auto" or "gpt-auto-force") */ - arg_auto_usr = parse_gpt_auto_root("mount.usr=", value); assert(arg_auto_usr >= 0); @@ -1325,6 +1377,7 @@ static int run(const char *dest, const char *dest_early, const char *dest_late) r = 0; RET_GATHER(r, add_root_mount()); RET_GATHER(r, add_usr_mount()); + RET_GATHER(r, add_early_esp_mount()); RET_GATHER(r, add_mounts()); return r; From 056c21aaebce6f7fd83ffe6a1784ff2692dc8744 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 9 Mar 2026 13:06:58 +0100 Subject: [PATCH 03/12] tpm2: add "systemd-tpm2-swtpm" wrapper for "swtpm" For TPM-less systems it's sometimes valuable to have a fill-in software TPM running from early boot on, so that TPM-based functionality can "just work" and rely on TPM semantics, even if it's at a substantially weaker security level. This adds a wrapper around swtpm. It's a binary that chainloads swtpm but does a few preparatory steps and integrates into systemd's logic otherwise. All this is then exposed as systemd-tpm2-swtpm.service. The service is not hooked into much yet, that is added in later commits. --- man/rules/meson.build | 4 + man/systemd-tpm2-swtpm.service.xml | 63 +++++ mkosi/mkosi.conf | 1 + mkosi/mkosi.conf.d/centos-fedora/mkosi.conf | 1 + mkosi/mkosi.conf.d/debian-ubuntu/mkosi.conf | 1 + mkosi/mkosi.initrd.conf/mkosi.conf | 1 + .../mkosi.conf.d/centos-fedora.conf | 1 + .../mkosi.conf.d/debian-ubuntu.conf | 1 + src/tpm2-setup/meson.build | 9 + src/tpm2-setup/tpm2-swtpm.c | 221 ++++++++++++++++++ units/meson.build | 4 + units/systemd-tpm2-swtpm.service.in | 26 +++ 12 files changed, 333 insertions(+) create mode 100644 man/systemd-tpm2-swtpm.service.xml create mode 100644 src/tpm2-setup/tpm2-swtpm.c create mode 100644 units/systemd-tpm2-swtpm.service.in diff --git a/man/rules/meson.build b/man/rules/meson.build index 5c14fb626b..0b5e438200 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1237,6 +1237,10 @@ manpages = [ '8', ['systemd-tpm2-setup', 'systemd-tpm2-setup-early.service'], 'ENABLE_BOOTLOADER'], + ['systemd-tpm2-swtpm.service', + '8', + ['systemd-tpm2-swtpm'], + 'ENABLE_BOOTLOADER'], ['systemd-tty-ask-password-agent', '1', [], ''], ['systemd-udev-settle.service', '8', [], ''], ['systemd-udevd.service', diff --git a/man/systemd-tpm2-swtpm.service.xml b/man/systemd-tpm2-swtpm.service.xml new file mode 100644 index 0000000000..3111a782c7 --- /dev/null +++ b/man/systemd-tpm2-swtpm.service.xml @@ -0,0 +1,63 @@ + + + + + + + + systemd-tpm2-swtpm.service + systemd + + + + systemd-tpm2-swtpm.service + 8 + + + + systemd-tpm2-swtpm.service + systemd-tpm2-swtpm + Provide a fallback software TPM + + + + systemd-tpm2-swtpm.service + /usr/lib/systemd/systemd-tpm2-swtpm + + + + Description + + The systemd-tpm2-swtpm.service provides fallback software TPM functionality, + intended for use in environments where a discrete or firmware TPM ("hardware TPM") is not available. It is + pulled into the boot process by + systemd-tpm2-generator8 + if a hardware TPM is not available, and the system is configured to provide a software TPM in that case. + + Note that a software TPM provides only very weak security properties compared to a hardware TPM, + and hence should only be used as a fallback mechanism if a hardware TPM is not available but TPM + semantics are desired. This service ultimately wraps + swtpm8. + + If the boot secret /.extra/boot-secret (in the initrd) or + /run/systemd/stub/boot-secret (on the host) is available the software TPM NVRAM + storage is encrypted with this key. See + systemd-stub7 for + details. + + The TPM NVRAM storage is placed on the EFI System Partition as it needs to be accessible during + very early boot-up, in particular before the root file system is decrypted and mounted. + + + + See Also + + systemd1 + systemd-tpm2-generator8 + swtpm8 + systemd-stub7 + + + diff --git a/mkosi/mkosi.conf b/mkosi/mkosi.conf index 80c4e59390..8bbf964664 100644 --- a/mkosi/mkosi.conf +++ b/mkosi/mkosi.conf @@ -123,6 +123,7 @@ Packages= sed socat strace + swtpm tar tree util-linux diff --git a/mkosi/mkosi.conf.d/centos-fedora/mkosi.conf b/mkosi/mkosi.conf.d/centos-fedora/mkosi.conf index fbbc6a90bf..416e71ba32 100644 --- a/mkosi/mkosi.conf.d/centos-fedora/mkosi.conf +++ b/mkosi/mkosi.conf.d/centos-fedora/mkosi.conf @@ -63,6 +63,7 @@ Packages= softhsm squashfs-tools stress-ng + swtpm-tools tpm2-tools veritysetup vim-common diff --git a/mkosi/mkosi.conf.d/debian-ubuntu/mkosi.conf b/mkosi/mkosi.conf.d/debian-ubuntu/mkosi.conf index f024eae204..80dc87213a 100644 --- a/mkosi/mkosi.conf.d/debian-ubuntu/mkosi.conf +++ b/mkosi/mkosi.conf.d/debian-ubuntu/mkosi.conf @@ -71,6 +71,7 @@ Packages= softhsm2 squashfs-tools stress-ng + swtpm-tools tgt tpm2-tools tzdata diff --git a/mkosi/mkosi.initrd.conf/mkosi.conf b/mkosi/mkosi.initrd.conf/mkosi.conf index 1c73f3a328..207b15f98c 100644 --- a/mkosi/mkosi.initrd.conf/mkosi.conf +++ b/mkosi/mkosi.initrd.conf/mkosi.conf @@ -16,4 +16,5 @@ Packages= findutils grep sed + swtpm tar diff --git a/mkosi/mkosi.initrd.conf/mkosi.conf.d/centos-fedora.conf b/mkosi/mkosi.initrd.conf/mkosi.conf.d/centos-fedora.conf index 1a971625bf..a35ccb4a0e 100644 --- a/mkosi/mkosi.initrd.conf/mkosi.conf.d/centos-fedora.conf +++ b/mkosi/mkosi.initrd.conf/mkosi.conf.d/centos-fedora.conf @@ -7,6 +7,7 @@ Distribution=|fedora [Content] PrepareScripts=%D/mkosi/mkosi.conf.d/centos-fedora/systemd.prepare Packages= + swtpm-tools tpm2-tools VolatilePackages= diff --git a/mkosi/mkosi.initrd.conf/mkosi.conf.d/debian-ubuntu.conf b/mkosi/mkosi.initrd.conf/mkosi.conf.d/debian-ubuntu.conf index 7f2566e993..1e5e894237 100644 --- a/mkosi/mkosi.initrd.conf/mkosi.conf.d/debian-ubuntu.conf +++ b/mkosi/mkosi.initrd.conf/mkosi.conf.d/debian-ubuntu.conf @@ -9,6 +9,7 @@ PrepareScripts=%D/mkosi/mkosi.conf.d/debian-ubuntu/systemd.prepare Packages= btrfs-progs tpm2-tools + swtpm-tools VolatilePackages= libsystemd-shared diff --git a/src/tpm2-setup/meson.build b/src/tpm2-setup/meson.build index a862e7239c..bac29cbbdc 100644 --- a/src/tpm2-setup/meson.build +++ b/src/tpm2-setup/meson.build @@ -22,6 +22,15 @@ executables += [ 'HAVE_TPM2', ], }, + libexec_template + { + 'name' : 'systemd-tpm2-swtpm', + 'sources' : files('tpm2-swtpm.c'), + 'conditions' : [ + 'ENABLE_BOOTLOADER', + 'HAVE_OPENSSL', + 'HAVE_TPM2', + ], + }, generator_template + { 'name' : 'systemd-tpm2-generator', 'sources' : files('tpm2-generator.c'), diff --git a/src/tpm2-setup/tpm2-swtpm.c b/src/tpm2-setup/tpm2-swtpm.c new file mode 100644 index 0000000000..9522420d86 --- /dev/null +++ b/src/tpm2-setup/tpm2-swtpm.c @@ -0,0 +1,221 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "sd-daemon.h" + +#include "alloc-util.h" +#include "chase.h" +#include "errno-util.h" +#include "escape.h" +#include "fd-util.h" +#include "fileio.h" +#include "find-esp.h" +#include "fs-util.h" +#include "hexdecoct.h" +#include "hmac.h" +#include "initrd-util.h" +#include "iovec-util.h" +#include "log.h" +#include "main-func.h" +#include "path-lookup.h" +#include "path-util.h" +#include "sha256.h" +#include "stat-util.h" +#include "string-util.h" +#include "strv.h" +#include "swtpm-util.h" + +#define BOOT_SECRET_SIZE 32U + +static int load_boot_secret(struct iovec *ret) { + _cleanup_(iovec_done_erase) struct iovec buf = {}; + int r; + + const char *bs = in_initrd() ? "/.extra/boot-secret" : "/run/systemd/stub/boot-secret"; + r = read_full_file_full( + AT_FDCWD, + bs, + UINT64_MAX, + BOOT_SECRET_SIZE, + READ_FULL_FILE_SECURE|READ_FULL_FILE_VERIFY_REGULAR, + /* bind_name= */ NULL, + (char**) &buf.iov_base, + &buf.iov_len); + if (r == -ENOENT) { + log_warning_errno(r, "Boot secret (%s) not found, not encrypting software TPM state!", bs); + *ret = (struct iovec) {}; + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to read '%s': %m", bs); + + if (buf.iov_len < BOOT_SECRET_SIZE) + return log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Boot secret too short, refusing."); + + *ret = TAKE_STRUCT(buf); + return 1; +} + +static int prepare_secret(const char *runtime_dir, char **ret) { + int r; + + assert(runtime_dir); + assert(ret); + + _cleanup_(iovec_done_erase) struct iovec boot_secret = {}; + r = load_boot_secret(&boot_secret); + if (r < 0) + return r; + if (r == 0) { + *ret = NULL; + return 0; + } + + /* Derive a suitable swtpm specific secret */ + static const char tag[] = "systemd swtpm tag v1"; + uint8_t secret[SHA256_DIGEST_SIZE]; + CLEANUP_ERASE(secret); + hmac_sha256(boot_secret.iov_base, + boot_secret.iov_len, + tag, + strlen(tag), + secret); + + _cleanup_free_ char *p = path_join(runtime_dir, "secret"); + if (!p) + return log_oom(); + + assert_cc(sizeof(secret) >= 16); /* swtpm only wants a 16 byte key */ + _cleanup_(erase_and_freep) char *h = hexmem(secret, 16); + if (!h) + return log_oom(); + + r = write_data_file_atomic_at(XAT_FDROOT, p, &IOVEC_MAKE_STRING(h), WRITE_DATA_FILE_MODE_0400); + if (r < 0) + return log_error_errno(r, "Failed to write secret file: %m"); + + *ret = TAKE_PTR(p); + return 1; +} + +static int setup_swtpm(const char *state_dir, int state_fd, const char *secret) { + int r; + + assert(state_dir); + assert(state_fd >= 0); + + /* Sets up the state directory via swtpm_setup */ + + if (in_initrd()) { + /* In the initrd remove previous transient state */ + r = RET_NERRNO(unlinkat(state_fd, "tpm2-00.volatilestate", /* flags= */ 0)); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to remove 'tpm2-00.volatilestate': %m"); + } + + r = dir_is_empty_at(state_fd, /* path= */ NULL, /* ignore_hidden_or_backup= */ false); + if (r < 0) + return log_error_errno(r, "Failed to check if TPM state directory is empty: %m"); + if (r == 0) { + log_debug("TPM state directory is already populated, not manufacturing a TPM."); + return 0; + } + + if (!in_initrd()) + return log_error_errno(SYNTHETIC_ERRNO(ESTALE), "swtpm TPM state directory has not been initialized in the initrd, refusing."); + + log_debug("TPM state directory is unpopulated, manufacturing a TPM."); + + return manufacture_swtpm(state_dir, secret); +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + _cleanup_free_ char *runtime_dir = NULL; + r = runtime_directory(RUNTIME_SCOPE_SYSTEM, "systemd/swtpm", &runtime_dir); + if (r < 0) + return log_error_errno(r, "Unable to determine runtime directory: %m"); + + _cleanup_free_ char *swtpm = NULL; + r = find_executable("swtpm", &swtpm); + if (r < 0) + return log_error_errno(r, "Failed to find 'swtpm' binary: %m"); + + _cleanup_free_ char *_esp = NULL; + const char *esp; + if (in_initrd()) + /* The early ESP support uses only a single mount point, we do not need to search for it. */ + esp = "/sysefi"; + else { + r = find_esp_and_warn( + /* root= */ NULL, + /* path= */ NULL, + /* unprivileged_mode= */ false, + &_esp); + if (r == -ENOKEY) /* This one find_esp_and_warn() doesn't actually log about. */ + return log_error_errno(r, "No ESP discovered."); + if (r < 0) + return r; + esp = _esp; + } + + _cleanup_free_ char *state_dir = NULL; + _cleanup_close_ int state_fd = -EBADF; + r = chase("/loader/swtpm", + esp, CHASE_PREFIX_ROOT|CHASE_TRIGGER_AUTOFS|CHASE_PROHIBIT_SYMLINKS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, + &state_dir, + &state_fd); + if (r < 0) + return log_error_errno(r, "Failed to open swtpm state directory in ESP: %m"); + + _cleanup_(unlink_and_freep) char *secret = NULL; + r = prepare_secret(runtime_dir, &secret); + if (r < 0) + return r; + + r = setup_swtpm(state_dir, state_fd, secret); + if (r < 0) + return r; + + _cleanup_strv_free_ char **args = + strv_new(swtpm, + "chardev", + "--vtpm-proxy", + "--tpm2", + /* Make sure that in the initrd swtpm never sends TPM2_Shutdown() for us, we want to + * be able to stop the daemon after all temporarily during the initrd→host + * transition. */ + in_initrd() ? "--flags=startup-clear,disable-auto-shutdown" : "--flags=startup-clear"); + if (!args) + return log_oom(); + + if (strv_extendf(&args, "--ctrl=type=unixio,path=%s/socket", runtime_dir) < 0) + return log_oom(); + + if (secret && strv_extendf(&args, "--key=file=%s,format=hex,mode=aes-cbc,remove=true", secret) < 0) + return log_oom(); + + if (strv_extendf(&args, "--tpmstate=dir=%s,mode=0600", state_dir) < 0) + return log_oom(); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *cmd = quote_command_line(args, SHELL_ESCAPE_EMPTY); + + log_debug("Chain-loading: %s", strnull(cmd)); + } + + /* Ideally swtpm could send this itself, but for now let's accept it like this. */ + // FIXME: remove this once swtpm 0.11 is released and hit all relevant distros. Then bump version + // requirements. + (void) sd_notify(/* unset_environment= */ true, "READY=1"); + + /* NB: if the execve() succeeds it's swtpm's job to actually unlink the secret file */ + execv(swtpm, args); + return log_error_errno(errno, "Failed to chainload swtpm: %m"); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/units/meson.build b/units/meson.build index ca17237dd0..1d0e145287 100644 --- a/units/meson.build +++ b/units/meson.build @@ -633,6 +633,10 @@ units = [ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], 'symlinks' : ['sysinit.target.wants/'], }, + { + 'file' : 'systemd-tpm2-swtpm.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + }, { 'file' : 'systemd-pcrlock-make-policy.service.in', 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], diff --git a/units/systemd-tpm2-swtpm.service.in b/units/systemd-tpm2-swtpm.service.in new file mode 100644 index 0000000000..10856f70d9 --- /dev/null +++ b/units/systemd-tpm2-swtpm.service.in @@ -0,0 +1,26 @@ +# 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=Fallback Software TPM +Documentation=man:systemd-tpm2-swtpm.service(8) +DefaultDependencies=no +After=systemd-sysusers.service +Wants=modprobe@tpm_vtpm_proxy.service +After=modprobe@tpm_vtpm_proxy.service +Before=tpm2.target sysinit.target + +[Service] +Type=notify +RuntimeDirectory=systemd/swtpm +ExecStart={{LIBEXECDIR}}/systemd-tpm2-swtpm +# Write out volatile state (so that we can read it back after the initrd transition +ExecStop=swtpm_ioctl --unix %t/systemd/swtpm/socket -v +# Initiate graceful shutdown +ExecStop=swtpm_ioctl --unix %t/systemd/swtpm/socket -s From 750795d1030fa5155224676d6e38554a35c6f076 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 9 Mar 2026 13:08:28 +0100 Subject: [PATCH 04/12] tpm2-generator: if requested run things with an swtpm We want to start the software TPM fallback only if no real hw is evailable and if the user opts-in to this behaviour. Add a generator that drives all this, based on kernel command line configuration. --- man/kernel-command-line.xml | 10 +++ man/systemd-tpm2-generator.xml | 9 +++ src/tpm2-setup/tpm2-generator.c | 114 +++++++++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 10 deletions(-) diff --git a/man/kernel-command-line.xml b/man/kernel-command-line.xml index 98673e0a51..1292bbfbee 100644 --- a/man/kernel-command-line.xml +++ b/man/kernel-command-line.xml @@ -782,6 +782,16 @@ + + systemd.tpm2_software_fallback= + + Controls whether to start a fallback software TPM service in case a hardware TPM is + not available, implemented by + systemd-tpm2-generator8. + + + + systemd.factory_reset= diff --git a/man/systemd-tpm2-generator.xml b/man/systemd-tpm2-generator.xml index 2e22b99b5a..b45cf29be8 100644 --- a/man/systemd-tpm2-generator.xml +++ b/man/systemd-tpm2-generator.xml @@ -45,6 +45,14 @@ for it yet. The latter might be useful in environments where a suitable TPM2 driver for the available hardware is not available. + The kernel command line option (which takes a + boolean argument, defaulting to false) may be used to enable an automatic software TPM fallback in case a + hardware TPM is not detected and + swtpm8 is + available. This pulls in the + systemd-tpm2-swtpm.service8 + service. + systemd-tpm2-generator implements systemd.generator7. @@ -55,6 +63,7 @@ systemd1 systemd.special7 kernel-command-line7 + systemd-tpm2-swtpm.service8 diff --git a/src/tpm2-setup/tpm2-generator.c b/src/tpm2-setup/tpm2-generator.c index 043e0fd4b7..65f450daaf 100644 --- a/src/tpm2-setup/tpm2-generator.c +++ b/src/tpm2-setup/tpm2-generator.c @@ -1,8 +1,14 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include + +#include "dropin.h" +#include "efivars.h" #include "generator.h" +#include "initrd-util.h" #include "log.h" #include "parse-util.h" +#include "path-util.h" #include "proc-cmdline.h" #include "special.h" #include "tpm2-util.h" @@ -15,6 +21,7 @@ static const char *arg_dest = NULL; static int arg_tpm2_wait = -1; /* tri-state: negative → don't know */ +static bool arg_tpm2_software_fallback = false; static int parse_proc_cmdline_item(const char *key, const char *value, void *data) { int r; @@ -27,12 +34,19 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat log_warning_errno(r, "Failed to parse 'systemd.tpm2_wait=' kernel command line argument, ignoring: %s", value); else arg_tpm2_wait = r; + + } else if (proc_cmdline_key_streq(key, "systemd.tpm2_software_fallback")) { + r = value ? parse_boolean(value) : 1; + if (r < 0) + log_warning_errno(r, "Failed to parse 'systemd.tpm2_software_fallback=' kernel command line argument, ignoring: %s", value); + else + arg_tpm2_software_fallback = r; } return 0; } -static int generate_tpm_target_symlink(void) { +static int generate_tpm_target_symlink(Tpm2Support support, bool software_fallback_enabled) { int r; if (arg_tpm2_wait == 0) { @@ -40,9 +54,7 @@ static int generate_tpm_target_symlink(void) { return 0; } - if (arg_tpm2_wait < 0) { - Tpm2Support support = tpm2_support(); - + if (arg_tpm2_wait < 0 && !software_fallback_enabled) { if (FLAGS_SET(support, TPM2_SUPPORT_DRIVER)) { log_debug("Not generating tpm2.target synchronization point, as TPM2 device is already present."); return 0; @@ -52,11 +64,6 @@ static int generate_tpm_target_symlink(void) { log_debug("Not generating tpm2.target synchronization point, as firmware reports no TPM2 present."); return 0; } - - if (!FLAGS_SET(support, TPM2_SUPPORT_SYSTEM|TPM2_SUPPORT_SUBSYSTEM|TPM2_SUPPORT_LIBRARIES)) { - log_debug("Not generating tpm2.target synchronization point, as userspace support for TPM2 is not complete."); - return 0; - } } r = generator_add_symlink(arg_dest, SPECIAL_SYSINIT_TARGET, "wants", SYSTEM_DATA_UNIT_DIR "/" SPECIAL_TPM2_TARGET); @@ -66,6 +73,93 @@ static int generate_tpm_target_symlink(void) { return 0; } +static int generate_swtpm_symlink(Tpm2Support support) { + int r; + + if (!arg_tpm2_software_fallback) + return 0; + + if (FLAGS_SET(support, TPM2_SUPPORT_DRIVER) || FLAGS_SET(support, TPM2_SUPPORT_FIRMWARE)) { + log_debug("Not generating software TPM units, as a TPM2 device is otherwise available."); + return 0; + } + + if (!is_efi_boot()) { /* We need the ESP to store the TPM state. */ + log_warning("TPM software fallback requested but not booted in EFI mode, not pulling in software TPM unit."); + return 0; + } + + r = find_executable("swtpm", /* ret_filename= */ NULL); + if (r == -ENOENT) { + log_warning("TPM software fallback requested but swtpm not available, not pulling in software TPM unit."); + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to determine if 'swtpm' is available: %m"); + + r = generator_add_symlink(arg_dest, SPECIAL_SYSINIT_TARGET, "wants", SYSTEM_DATA_UNIT_DIR "/systemd-tpm2-swtpm.service"); + if (r < 0) + return log_error_errno(r, "Failed to hook in systemd-tpm2-swtpm.service: %m"); + + if (in_initrd()) + /* Order + pull in the early ESP mount so that swtpm has a place to store its data. */ + r = write_drop_in( + arg_dest, + "systemd-tpm2-swtpm.service", + 50, "esp", + "# Automatically generated by systemd-tpm2-generator\n\n" + "[Unit]\n" + "Wants=sysefi.mount\n" + "After=sysefi.mount\n"); + else + /* Order (but not pull in) the regular ESP automount so that swtpm has a place to store its + * data. Note that it might be mounted to two different places depending on the existence of + * XBOOTLDR, hence order after both. */ + r = write_drop_in( + arg_dest, + "systemd-tpm2-swtpm.service", + 50, "esp", + "# Automatically generated by systemd-tpm2-generator\n\n" + "[Unit]\n" + "After=boot.automount efi.automount\n"); + if (r < 0) + return log_error_errno(r, "Failed to hook ESP mount before systemd-tpm2-swtpm.service: %m"); + + return 1; /* Tell caller we now created swtpm units */ +} + +static int generate_now(void) { + int r; + + /* Let's shortcut things before we check for TPM2 support if no one cares anyway */ + if (arg_tpm2_wait == 0 && !arg_tpm2_software_fallback) { + log_debug("Not generating tpm2.target synchronization point or activating software TPM, as turned off via kernel command line."); + return 0; + } + + /* We are supposed to sync on TPM or do a software fallback, let's first and unconditionally validate this makes sense at + * all, i.e. if we have a suitable kernel+userspace. */ + Tpm2Support support = tpm2_support(); + if (!FLAGS_SET(support, TPM2_SUPPORT_SYSTEM|TPM2_SUPPORT_SUBSYSTEM|TPM2_SUPPORT_LIBRARIES)) { + + /* Raise log level if things were explicitly configured */ + log_full((arg_tpm2_wait > 0 || + arg_tpm2_software_fallback) ? LOG_NOTICE : LOG_DEBUG, + "Not generating tpm2.target synchronization point or activating software TPM, as userspace support for TPM2 is not complete."); + return 0; + } + + r = generate_swtpm_symlink(support); + if (r < 0) + return r; + + r = generate_tpm_target_symlink(support, /* software_fallback_enabled= */ r > 0); + if (r < 0) + return r; + + return 0; +} + static int run(const char *dest, const char *dest_early, const char *dest_late) { int r; @@ -75,7 +169,7 @@ static int run(const char *dest, const char *dest_early, const char *dest_late) if (r < 0) log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m"); - return generate_tpm_target_symlink(); + return generate_now(); } DEFINE_MAIN_GENERATOR_FUNCTION(run); From ca03d178a077a7705c58619fcc83fb3b90845fdb Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 9 Mar 2026 18:51:49 +0100 Subject: [PATCH 05/12] tree-wide: relax TPM available checks for many cases In many cases it's essential to know if the firmware supports a TPM, but in others we should accept it if the firmware doesn't have TPM support, in particular if we want to run the OS with a software TPM. Hence, add tpm2_is_mostly_supported() as function similar to tpm2_is_fully_supported(), with the only difference that the former doesn't insist on a firmware supported TPM. Then, change a number of users over to this (but not all). --- src/analyze/analyze-nvpcrs.c | 2 +- src/analyze/analyze-pcrs.c | 4 ++-- src/pcrextend/pcrextend.c | 2 +- src/shared/creds-util.c | 2 +- src/shared/tpm2-util.h | 4 ++++ src/tpm2-setup/tpm2-setup.c | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/analyze/analyze-nvpcrs.c b/src/analyze/analyze-nvpcrs.c index 68e7acb33a..56b5c9a204 100644 --- a/src/analyze/analyze-nvpcrs.c +++ b/src/analyze/analyze-nvpcrs.c @@ -56,7 +56,7 @@ int verb_nvpcrs(int argc, char *argv[], uintptr_t _data, void *userdata) { _cleanup_(table_unrefp) Table *table = NULL; int r; - bool have_tpm2 = tpm2_is_fully_supported(); + bool have_tpm2 = tpm2_is_mostly_supported(); if (!have_tpm2) log_notice("System lacks full TPM2 support, not showing NvPCR state."); diff --git a/src/analyze/analyze-pcrs.c b/src/analyze/analyze-pcrs.c index f98f4a8d50..7e3ddde800 100644 --- a/src/analyze/analyze-pcrs.c +++ b/src/analyze/analyze-pcrs.c @@ -101,8 +101,8 @@ int verb_pcrs(int argc, char *argv[], uintptr_t _data, void *userdata) { const char *alg = NULL; int r; - if (!tpm2_is_fully_supported()) - log_notice("System lacks full TPM2 support, not showing PCR state."); + if (!tpm2_is_mostly_supported()) + log_notice("System lacks sufficient TPM2 support, not showing PCR state."); else { r = get_pcr_alg(&alg); if (r < 0) diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index c319ddd0f8..c0b111a096 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -531,7 +531,7 @@ static int run(int argc, char *argv[]) { if (arg_event_type >= 0) event = arg_event_type; - if (arg_graceful && !tpm2_is_fully_supported()) { + if (arg_graceful && !tpm2_is_mostly_supported()) { log_notice("No complete TPM2 support detected, exiting gracefully."); return EXIT_SUCCESS; } diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 9c093181c7..8071629c17 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -894,7 +894,7 @@ int encrypt_credential_and_warn( * container tpm2_support will detect this, and will return a different flag combination of * TPM2_SUPPORT_FULL, effectively skipping the use of TPM2 when inside one. */ - try_tpm2 = tpm2_is_fully_supported(); + try_tpm2 = tpm2_is_mostly_supported(); if (!try_tpm2) log_debug("System lacks TPM2 support or running in a container, not attempting to use TPM2."); } else diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 841f33b8de..2f5d8632de 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -496,6 +496,7 @@ typedef enum Tpm2Support { /* Combined flags for generic (i.e. not tool-specific) support */ TPM2_SUPPORT_FULL = TPM2_SUPPORT_API|TPM2_SUPPORT_LIBTSS2_ALL, + TPM2_SUPPORT_SOFTWARE = TPM2_SUPPORT_FULL & ~TPM2_SUPPORT_FIRMWARE, /* Same, just without PC firmware support */ } Tpm2Support; Tpm2Support tpm2_support_full(Tpm2Support mask); @@ -505,6 +506,9 @@ static inline Tpm2Support tpm2_support(void) { static inline bool tpm2_is_fully_supported(void) { return tpm2_support() == TPM2_SUPPORT_FULL; } +static inline bool tpm2_is_mostly_supported(void) { + return (tpm2_support() & TPM2_SUPPORT_SOFTWARE) == TPM2_SUPPORT_SOFTWARE; +} int verb_has_tpm2_generic(bool quiet); diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index d243f199e9..92a4bfa12a 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -516,7 +516,7 @@ static int run(int argc, char *argv[]) { if (r <= 0) return r; - if (arg_graceful && !tpm2_is_fully_supported()) { + if (arg_graceful && !tpm2_is_mostly_supported()) { log_notice("No complete TPM2 support detected, exiting gracefully."); return EXIT_SUCCESS; } From cd911bec6eec21a2cc775c98bf599ea38cb1fa1f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 9 Mar 2026 18:53:09 +0100 Subject: [PATCH 06/12] core: introduce ConditionSecurity=measured-os So far we always conditioned our TPM magic on the UKI having detected TPM support in the firmware. This is a bit limiting when we want to support a software TPM that is not visible to the firmware. Hence let's split this up, and add a separate control that can be set via the kernel command line. However, as before, let's by default inherit the firmare TPM discovery state into it, to retain the current behaviour unless overriden. With this in place, boot with "systemd.tpm2_measured_os=1 systemd.tpm2_software_fallback=1" on the kernel cmdline to get the swtpm fallback and then a measured OS based on it. --- man/kernel-command-line.xml | 12 +++++++++ man/systemd.unit.xml | 4 +++ src/bootctl/bootctl-status.c | 10 ++++++++ src/cryptsetup/cryptsetup.c | 8 +++--- src/fstab-generator/fstab-generator.c | 4 +-- src/gpt-auto-generator/gpt-auto-generator.c | 14 ++++++----- .../hibernate-resume-generator.c | 2 +- src/pcrextend/pcrextend.c | 6 ++--- src/shared/condition.c | 2 ++ src/shared/efi-loader.c | 25 +++++++++++++++++++ src/shared/efi-loader.h | 1 + units/systemd-pcrextend.socket | 2 +- units/systemd-pcrfs-root.service.in | 2 +- units/systemd-pcrfs@.service.in | 2 +- units/systemd-pcrmachine.service.in | 2 +- units/systemd-pcrnvdone.service.in | 2 +- .../systemd-pcrphase-factory-reset.service.in | 2 +- units/systemd-pcrphase-initrd.service.in | 2 +- ...md-pcrphase-storage-target-mode.service.in | 2 +- units/systemd-pcrphase-sysinit.service.in | 2 +- units/systemd-pcrphase.service.in | 2 +- units/systemd-pcrproduct.service.in | 2 +- units/systemd-tpm2-clear.service.in | 2 +- units/systemd-tpm2-setup-early.service.in | 2 +- units/systemd-tpm2-setup.service.in | 2 +- 25 files changed, 86 insertions(+), 30 deletions(-) diff --git a/man/kernel-command-line.xml b/man/kernel-command-line.xml index 1292bbfbee..088ce24154 100644 --- a/man/kernel-command-line.xml +++ b/man/kernel-command-line.xml @@ -792,6 +792,18 @@ + + systemd.tpm2_measured_os= + + Controls whether to execute various boot and runtime TPM PCR measurements. Takes a + boolean argument. If not specified explicitly this behaviour is enabled automatically in case + systemd-stub7 is + used and it succeeded in doing pre-boot measurements of the booted UKI, and otherwise + disabled. + + + + systemd.factory_reset= diff --git a/man/systemd.unit.xml b/man/systemd.unit.xml index 37022ecc1c..8bbff2f210 100644 --- a/man/systemd.unit.xml +++ b/man/systemd.unit.xml @@ -1605,6 +1605,10 @@ measured-uki Unified Kernel Image with PCR 11 Measurements, as per systemd-stub7. + + measured-os + OS PCR measurements enabled. This is typically equivalent to measured-uki, however may also be set explicitly via the systemd.tpm2_measured_os= kernel command line switch, see kernel-command-line7 for details. The various system services doing boot and runtime measurements are conditioned on this flag. + diff --git a/src/bootctl/bootctl-status.c b/src/bootctl/bootctl-status.c index 4184e3d424..178bffb365 100644 --- a/src/bootctl/bootctl-status.c +++ b/src/bootctl/bootctl-status.c @@ -477,6 +477,16 @@ int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { printf(" Measured UKI: %sfailed%s (%m)\n", ansi_highlight_red(), ansi_normal()); } + k = efi_measured_os(LOG_DEBUG); + if (k > 0) + printf(" Measured OS: %syes%s\n", ansi_highlight_green(), ansi_normal()); + else if (k == 0) + printf(" Measured OS: no\n"); + else { + errno = -k; + printf(" Measured OS: %sfailed%s (%m)\n", ansi_highlight_red(), ansi_normal()); + } + k = efi_get_reboot_to_firmware(); if (k > 0) printf(" Boot into FW: %sactive%s\n", ansi_highlight_yellow(), ansi_normal()); diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index bda9a8cc84..64cd3813b8 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -1030,11 +1030,11 @@ static int measure_volume_key( return 0; } - r = efi_measured_uki(LOG_WARNING); + r = efi_measured_os(LOG_WARNING); if (r < 0) return r; if (r == 0) { - log_debug("Kernel stub did not measure kernel image into the expected PCR, skipping userspace volume key measurement, too."); + log_debug("OS measurements not explicitly requested and kernel stub did not measure kernel image into the expected PCR, skipping userspace volume key measurement, too."); return 0; } @@ -1109,11 +1109,11 @@ static int measure_keyslot( } #if HAVE_TPM2 - r = efi_measured_uki(LOG_WARNING); + r = efi_measured_os(LOG_WARNING); if (r < 0) return r; if (r == 0) { - log_debug("Kernel stub did not measure kernel image into the expected PCR, skipping userspace key slot measurement, too."); + log_debug("OS measurements not explicitly requested and kernel stub did not measure kernel image into the expected PCR, skipping userspace key slot measurement, too."); return 0; } diff --git a/src/fstab-generator/fstab-generator.c b/src/fstab-generator/fstab-generator.c index d60db7e9c1..1bfebf3c40 100644 --- a/src/fstab-generator/fstab-generator.c +++ b/src/fstab-generator/fstab-generator.c @@ -672,9 +672,9 @@ static int add_mount( } if (flags & MOUNT_PCRFS) { - r = efi_measured_uki(LOG_WARNING); + r = efi_measured_os(LOG_WARNING); if (r == 0) - log_debug("Kernel stub did not measure kernel image into PCR, skipping userspace measurement, too."); + log_debug("OS measurements not explicitly requested and kernel stub did not measure kernel image into PCR, skipping userspace measurement, too."); else if (r > 0) { r = generator_hook_up_pcrfs(dest, where, target_unit); if (r < 0) diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index a87b514080..6716a8d1aa 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -115,11 +115,13 @@ static int add_cryptsetup( return log_oom(); } - r = efi_measured_uki(LOG_WARNING); - if (r > 0) + r = efi_measured_os(LOG_WARNING); + if (r > 0) { /* Enable TPM2 based unlocking automatically, if we have a TPM. See #30176. */ if (!strextend_with_separator(&options, ",", "tpm2-device=auto")) return log_oom(); + } else if (r == 0) + log_debug("Will not enable TPM based unlocking of volume '%s', OS measurements are not explicitly requested and not booted via systemd-stub with measurements enabled.", id); if (FLAGS_SET(flags, MOUNT_MEASURE)) { /* We only measure the root volume key into PCR 15 if we are booted with sd-stub (i.e. in a @@ -130,7 +132,7 @@ static int add_cryptsetup( if (!strextend_with_separator(&options, ",", "tpm2-measure-pcr=yes,tpm2-measure-keyslot-nvpcr=yes")) return log_oom(); if (r == 0) - log_debug("Will not measure volume key of volume '%s', not booted via systemd-stub with measurements enabled.", id); + log_debug("Will not measure volume key of volume '%s', as OS measurements are not explicitly requested and not booted via systemd-stub with measurements enabled.", id); } r = generator_write_cryptsetup_service_section(f, id, what, NULL, options); @@ -240,11 +242,11 @@ static int add_veritysetup( return log_oom(); if (FLAGS_SET(flags, MOUNT_MEASURE)) { - r = efi_measured_uki(LOG_WARNING); + r = efi_measured_os(LOG_WARNING); if (r > 0 && !strextend_with_separator(&options, ",", "tpm2-measure-nvpcr=yes")) return log_oom(); - if (r == 0) - log_debug("Will not measure root hash/signature of volume '%s', not booted via systemd-stub with measurements enabled.", id); + else if (r == 0) + log_debug("Will not measure root hash/signature of volume '%s', OS measurements not explicitly requested and not booted via systemd-stub with measurements enabled.", id); } r = generator_write_veritysetup_service_section( diff --git a/src/hibernate-resume/hibernate-resume-generator.c b/src/hibernate-resume/hibernate-resume-generator.c index 79c7d41bb4..998c8e84d9 100644 --- a/src/hibernate-resume/hibernate-resume-generator.c +++ b/src/hibernate-resume/hibernate-resume-generator.c @@ -86,7 +86,7 @@ static int add_dissected_swap_cryptsetup(void) { r = generator_write_cryptsetup_service_section( f, "swap", DISSECTED_SWAP_LUKS_DEVICE, /* key_file= */ NULL, - efi_measured_uki(LOG_DEBUG) > 0 ? "tpm2-device=auto" : NULL); + efi_measured_os(LOG_DEBUG) > 0 ? "tpm2-device=auto" : NULL); if (r < 0) return r; diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index c0b111a096..8ec0a733c6 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -536,12 +536,12 @@ static int run(int argc, char *argv[]) { return EXIT_SUCCESS; } - /* Skip logic if sd-stub is not used, after all PCR 11 might have a very different purpose then. */ - r = efi_measured_uki(LOG_ERR); + /* Skip logic if measured OS functionality is not enabled. */ + r = efi_measured_os(LOG_ERR); if (r < 0) return r; if (r == 0) { - log_info("Kernel stub did not measure kernel image into PCR %i, skipping userspace measurement, too.", TPM2_PCR_KERNEL_BOOT); + log_info("OS measurements not explicitly requested and kernel stub did not measure kernel image into PCR %i, skipping userspace measurement, too.", TPM2_PCR_KERNEL_BOOT); return EXIT_SUCCESS; } diff --git a/src/shared/condition.c b/src/shared/condition.c index 903662edf1..dd720c55bd 100644 --- a/src/shared/condition.c +++ b/src/shared/condition.c @@ -741,6 +741,8 @@ static int condition_test_security(Condition *c, char **env) { return detect_confidential_virtualization() > 0; if (streq(c->parameter, "measured-uki")) return efi_measured_uki(LOG_DEBUG); + if (streq(c->parameter, "measured-os")) + return efi_measured_os(LOG_DEBUG); return false; } diff --git a/src/shared/efi-loader.c b/src/shared/efi-loader.c index 1f4fc665c0..ce10a44d34 100644 --- a/src/shared/efi-loader.c +++ b/src/shared/efi-loader.c @@ -8,6 +8,7 @@ #include "log.h" #include "parse-util.h" #include "path-util.h" +#include "proc-cmdline.h" #include "stat-util.h" #include "string-util.h" #include "strv.h" @@ -312,6 +313,30 @@ int efi_measured_uki(int log_level) { #endif } +int efi_measured_os(int log_level) { +#if ENABLE_EFI + static int cached = -1; + int r; + + /* Returns if we shall enable our measurement machinery */ + + if (cached >= 0) + return cached; + + bool b; + r = proc_cmdline_get_bool("systemd.tpm2_measured_os", /* flags= */ 0, &b); + if (r < 0) + log_debug_errno(r, "Failed to parse systemd.tpm2_measured_os= kernel command line argument, ignoring: %m"); + else if (r > 0) + return (cached = b); + + /* If nothing is explicitly configured, just assume that if we booted with a measured UKI we also want a measured OS */ + return (cached = efi_measured_uki(log_level)); +#else + return log_full_errno(log_level, SYNTHETIC_ERRNO(EOPNOTSUPP), "Compiled without support for EFI"); +#endif +} + int efi_loader_get_config_timeout_one_shot(usec_t *ret) { #if ENABLE_EFI _cleanup_free_ char *v = NULL; diff --git a/src/shared/efi-loader.h b/src/shared/efi-loader.h index 5b614cd0a7..abf8bdc49e 100644 --- a/src/shared/efi-loader.h +++ b/src/shared/efi-loader.h @@ -15,6 +15,7 @@ int efi_loader_get_features(uint64_t *ret); int efi_stub_get_features(uint64_t *ret); int efi_measured_uki(int log_level); +int efi_measured_os(int log_level); int efi_loader_get_config_timeout_one_shot(usec_t *ret); int efi_loader_update_entry_one_shot_cache(char **cache, struct stat *cache_stat); diff --git a/units/systemd-pcrextend.socket b/units/systemd-pcrextend.socket index d429150eda..0f4ab11e2f 100644 --- a/units/systemd-pcrextend.socket +++ b/units/systemd-pcrextend.socket @@ -13,7 +13,7 @@ Documentation=man:systemd-pcrextend(8) DefaultDependencies=no After=tpm2.target Before=sockets.target -ConditionSecurity=measured-uki +ConditionSecurity=measured-os [Socket] ListenStream=/run/systemd/io.systemd.PCRExtend diff --git a/units/systemd-pcrfs-root.service.in b/units/systemd-pcrfs-root.service.in index f774c4c8bf..88551d7ed0 100644 --- a/units/systemd-pcrfs-root.service.in +++ b/units/systemd-pcrfs-root.service.in @@ -15,7 +15,7 @@ Conflicts=shutdown.target After=tpm2.target systemd-pcrmachine.service Before=shutdown.target ConditionPathExists=!/etc/initrd-release -ConditionSecurity=measured-uki +ConditionSecurity=measured-os FailureAction=reboot-force [Service] diff --git a/units/systemd-pcrfs@.service.in b/units/systemd-pcrfs@.service.in index 3d18fe4d30..38cc41976f 100644 --- a/units/systemd-pcrfs@.service.in +++ b/units/systemd-pcrfs@.service.in @@ -16,7 +16,7 @@ Conflicts=shutdown.target After=%i.mount tpm2.target systemd-pcrfs-root.service Before=shutdown.target ConditionPathExists=!/etc/initrd-release -ConditionSecurity=measured-uki +ConditionSecurity=measured-os FailureAction=reboot-force [Service] diff --git a/units/systemd-pcrmachine.service.in b/units/systemd-pcrmachine.service.in index ea2561ef79..d97afa6965 100644 --- a/units/systemd-pcrmachine.service.in +++ b/units/systemd-pcrmachine.service.in @@ -15,7 +15,7 @@ Conflicts=shutdown.target After=tpm2.target Before=sysinit.target shutdown.target ConditionPathExists=!/etc/initrd-release -ConditionSecurity=measured-uki +ConditionSecurity=measured-os FailureAction=reboot-force [Service] diff --git a/units/systemd-pcrnvdone.service.in b/units/systemd-pcrnvdone.service.in index e0dd9a8820..7593dedfed 100644 --- a/units/systemd-pcrnvdone.service.in +++ b/units/systemd-pcrnvdone.service.in @@ -14,7 +14,7 @@ DefaultDependencies=no Conflicts=shutdown.target After=systemd-tpm2-setup-early.service systemd-tpm2-setup.service Before=sysinit.target shutdown.target -ConditionSecurity=measured-uki +ConditionSecurity=measured-os ConditionPathExists=!/etc/initrd-release FailureAction=reboot-force diff --git a/units/systemd-pcrphase-factory-reset.service.in b/units/systemd-pcrphase-factory-reset.service.in index 5dbcb0f53f..2efd8830d3 100644 --- a/units/systemd-pcrphase-factory-reset.service.in +++ b/units/systemd-pcrphase-factory-reset.service.in @@ -14,7 +14,7 @@ DefaultDependencies=no Conflicts=shutdown.target After=tpm2.target Before=shutdown.target factory-reset.target -ConditionSecurity=measured-uki +ConditionSecurity=measured-os FailureAction=reboot-force [Service] diff --git a/units/systemd-pcrphase-initrd.service.in b/units/systemd-pcrphase-initrd.service.in index 5aba32128c..cbb8331470 100644 --- a/units/systemd-pcrphase-initrd.service.in +++ b/units/systemd-pcrphase-initrd.service.in @@ -15,7 +15,7 @@ Conflicts=shutdown.target initrd-switch-root.target After=tpm2.target Before=sysinit.target cryptsetup-pre.target cryptsetup.target shutdown.target initrd-switch-root.target systemd-sysext.service ConditionPathExists=/etc/initrd-release -ConditionSecurity=measured-uki +ConditionSecurity=measured-os FailureAction=reboot-force [Service] diff --git a/units/systemd-pcrphase-storage-target-mode.service.in b/units/systemd-pcrphase-storage-target-mode.service.in index 52b53e5b81..b4330c560f 100644 --- a/units/systemd-pcrphase-storage-target-mode.service.in +++ b/units/systemd-pcrphase-storage-target-mode.service.in @@ -15,7 +15,7 @@ Conflicts=shutdown.target After=tpm2.target Before=shutdown.target ConditionPathExists=/etc/initrd-release -ConditionSecurity=measured-uki +ConditionSecurity=measured-os FailureAction=reboot-force [Service] diff --git a/units/systemd-pcrphase-sysinit.service.in b/units/systemd-pcrphase-sysinit.service.in index 4a01279159..aa4d364098 100644 --- a/units/systemd-pcrphase-sysinit.service.in +++ b/units/systemd-pcrphase-sysinit.service.in @@ -15,7 +15,7 @@ Conflicts=shutdown.target After=sysinit.target tpm2.target Before=basic.target shutdown.target ConditionPathExists=!/etc/initrd-release -ConditionSecurity=measured-uki +ConditionSecurity=measured-os FailureAction=reboot-force [Service] diff --git a/units/systemd-pcrphase.service.in b/units/systemd-pcrphase.service.in index 43459a2fcc..b2f925d40f 100644 --- a/units/systemd-pcrphase.service.in +++ b/units/systemd-pcrphase.service.in @@ -13,7 +13,7 @@ Documentation=man:systemd-pcrphase.service(8) After=remote-fs.target remote-cryptsetup.target tpm2.target Before=systemd-user-sessions.service ConditionPathExists=!/etc/initrd-release -ConditionSecurity=measured-uki +ConditionSecurity=measured-os FailureAction=reboot-force [Service] diff --git a/units/systemd-pcrproduct.service.in b/units/systemd-pcrproduct.service.in index 09e446c2a0..2562dea18f 100644 --- a/units/systemd-pcrproduct.service.in +++ b/units/systemd-pcrproduct.service.in @@ -16,7 +16,7 @@ After=tpm2.target Before=sysinit.target shutdown.target RequiresMountsFor=/var/lib/systemd/nvpcr ConditionPathExists=!/etc/initrd-release -ConditionSecurity=measured-uki +ConditionSecurity=measured-os [Service] Type=oneshot diff --git a/units/systemd-tpm2-clear.service.in b/units/systemd-tpm2-clear.service.in index a47d99ac8e..501846180c 100644 --- a/units/systemd-tpm2-clear.service.in +++ b/units/systemd-tpm2-clear.service.in @@ -22,7 +22,7 @@ ConditionPathExists=/sys/class/tpm/tpm0/ppi/request # derive here from the fact that UKIs are used. Because if they do they are OK # with our SRK initialization and our PCR measurements, and hence should also # be OK with our TPM resets. -ConditionSecurity=measured-uki +ConditionSecurity=measured-os [Service] Type=oneshot diff --git a/units/systemd-tpm2-setup-early.service.in b/units/systemd-tpm2-setup-early.service.in index ce1ee94cc4..6b7ef34b8c 100644 --- a/units/systemd-tpm2-setup-early.service.in +++ b/units/systemd-tpm2-setup-early.service.in @@ -14,7 +14,7 @@ DefaultDependencies=no Conflicts=shutdown.target After=tpm2.target systemd-pcrphase-initrd.service Before=sysinit.target shutdown.target -ConditionSecurity=measured-uki +ConditionSecurity=measured-os ConditionPathExists=!/run/systemd/tpm2-srk-public-key.pem [Service] diff --git a/units/systemd-tpm2-setup.service.in b/units/systemd-tpm2-setup.service.in index dff516832d..4593211c1e 100644 --- a/units/systemd-tpm2-setup.service.in +++ b/units/systemd-tpm2-setup.service.in @@ -15,7 +15,7 @@ Conflicts=shutdown.target After=tpm2.target systemd-tpm2-setup-early.service systemd-remount-fs.service Before=sysinit.target shutdown.target RequiresMountsFor=/var/lib/systemd -ConditionSecurity=measured-uki +ConditionSecurity=measured-os ConditionPathExists=!/etc/initrd-release [Service] From 1494cb04ea145f8ce706413f05a28cadc93059f1 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 10 Mar 2026 08:04:52 +0100 Subject: [PATCH 07/12] pcrlock: don't fail if firmware measurements aren't available With swtpm in place we now commonly have systems where TPM is available during runtime, but not in the firmware. Handle that nicely. --- src/pcrlock/pcrlock.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index ddae43dc54..c940ab01e8 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -899,6 +899,10 @@ static int event_log_load_firmware(EventLog *el) { path = tpm2_firmware_log_path(); r = read_full_file(path, (char**) &buf, &bufsize); + if (r == -ENOENT) { + log_notice("No '%s' file, assuming TPM without firmware support.", path); + return 0; + } if (r < 0) return log_error_errno(r, "Failed to open TPM2 event log '%s': %m", path); From 96bb950ffa8b606518b104a109ed7f0c1bfb2bce Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 10 Mar 2026 08:07:25 +0100 Subject: [PATCH 08/12] pcrlock: deal with firmwares which understand TPM but where no TPM is available This is a potentially common case in VMs: firmwares might know the concept of TPMs, but the hardware is not enabled in the specific VM. Let's handle this case nicely. --- src/shared/tpm2-util.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 47a6a309dd..05ea7c47be 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -2899,6 +2899,8 @@ int tpm2_get_best_pcr_bank( log_debug("Boot loader didn't set the LoaderTpm2ActivePcrBanks EFI variable or EFI support is unavailable, 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 (efi_banks == 0) + log_debug("Boot loader set the LoaderTpm2ActivePcrBanks EFI variable to zero to indicate that TPM support is not available in the firmware. We'll have to guess the used PCR banks."); else { if (BIT_SET(efi_banks, TPM2_ALG_SHA256)) *ret = TPM2_ALG_SHA256; @@ -3008,6 +3010,8 @@ int tpm2_get_good_pcr_banks( log_debug("Boot loader didn't set the LoaderTpm2ActivePcrBanks EFI variable or EFI support is unavailable, 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 (efi_banks == 0) + log_debug("Boot loader set the LoaderTpm2ActivePcrBanks EFI variable to zero to indicate that TPM support is not available in the firmware. We'll have to guess the used PCR banks."); else { FOREACH_ARRAY(hash, tpm2_hash_algorithms, TPM2_N_HASH_ALGORITHMS) { if (!BIT_SET(efi_banks, *hash)) From 3b20cc4526e8068474d4c9b1f9eaa49cc6afcaa4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 10 Mar 2026 18:17:04 +0100 Subject: [PATCH 09/12] creds-util: only lock against public key PCR stuff if we are booted with UEFI supporting TPMs The UKI public key PCR stuff only works if we get PCR measurements from the pre-boot environment, hence automatically disable the logic by default if we don't have that. --- src/shared/creds-util.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 8071629c17..e7db1ff7ef 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -901,7 +901,10 @@ int encrypt_credential_and_warn( try_tpm2 = CRED_KEY_REQUIRES_TPM2(with_key); if (try_tpm2) { - if (CRED_KEY_WANTS_TPM2_PK(with_key) || CRED_KEY_REQUIRES_TPM2_PK(with_key)) { + /* If the firmware does not support TPMs, then UKI measurements are not going to work, hence + * PCR 11 public key stuff cannot work. Because of that, if PK is only wanted (but not + * required) we won't try it. */ + if ((CRED_KEY_WANTS_TPM2_PK(with_key) && tpm2_is_fully_supported()) || CRED_KEY_REQUIRES_TPM2_PK(with_key)) { /* Load public key for PCR policies, if one is specified, or explicitly requested */ @@ -926,6 +929,8 @@ int encrypt_credential_and_warn( if (r < 0) return log_error_errno(r, "Could not find best pcr bank: %m"); + log_debug("Selected literal PCR mask: 0x%x, PK PCR mask: 0x%x", tpm2_hash_pcr_mask, tpm2_pubkey_pcr_mask); + TPML_PCR_SELECTION tpm2_hash_pcr_selection; tpm2_tpml_pcr_selection_from_mask(tpm2_hash_pcr_mask, tpm2_pcr_bank, &tpm2_hash_pcr_selection); From 7c27f9f59455b199e12976c76b28b708c525b55c Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 10 Mar 2026 08:09:52 +0100 Subject: [PATCH 10/12] pcrextend: measure another separator at boot This has been requested previously for PCR 7 (#40567), but let's do that for all firmware owned PCRs, since some firmwares forget to measure their own separator. Let's hence measure our own guranteed one. Fixes: #40567 --- man/rules/meson.build | 1 + man/systemd-pcrphase.service.xml | 10 +++- src/pcrextend/pcrextend.c | 57 +++++++++++-------- src/pcrlock/meson.build | 1 + .../pcrlock.d/750-os-separator.pcrlock | 1 + src/shared/tpm2-util.c | 1 + src/shared/tpm2-util.h | 1 + test/units/TEST-70-TPM2.pcrextend.sh | 1 - test/units/TEST-70-TPM2.pcrlock.sh | 12 ++++ units/meson.build | 5 ++ units/systemd-pcrosseparator.service.in | 28 +++++++++ units/systemd-tpm2-setup-early.service.in | 2 +- 12 files changed, 93 insertions(+), 27 deletions(-) create mode 100644 src/pcrlock/pcrlock.d/750-os-separator.pcrlock create mode 100644 units/systemd-pcrosseparator.service.in diff --git a/man/rules/meson.build b/man/rules/meson.build index 0b5e438200..911a68543e 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1138,6 +1138,7 @@ manpages = [ 'systemd-pcrfs@.service', 'systemd-pcrmachine.service', 'systemd-pcrnvdone.service', + 'systemd-pcrosseparator.service', 'systemd-pcrphase-initrd.service', 'systemd-pcrphase-sysinit.service', 'systemd-pcrproduct.service'], diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml index 6b5ff05c3d..b1049ea916 100644 --- a/man/systemd-pcrphase.service.xml +++ b/man/systemd-pcrphase.service.xml @@ -21,6 +21,7 @@ systemd-pcrphase-sysinit.service systemd-pcrphase-initrd.service systemd-pcrmachine.service + systemd-pcrosseparator.service systemd-pcrproduct.service systemd-pcrfs-root.service systemd-pcrfs@.service @@ -34,6 +35,7 @@ systemd-pcrphase-sysinit.service systemd-pcrphase-initrd.service systemd-pcrmachine.service + systemd-pcrosseparator.service systemd-pcrproduct.service systemd-pcrfs-root.service systemd-pcrfs@.service @@ -53,6 +55,11 @@ (see machine-id5) into PCR 15. + systemd-pcrosseparator.service is a system service that measures the word + os-separator into PCRs 0-7, 9, 12-14. This acts as additional separator measurement + separating pre-boot (i.e. firmware + bootloader) measurements from OS measurements, and in particular + "seals" off the firmware PCRs. + systemd-pcrproduct.service is a system service that measures the firmware product UUID (as provided by one of SMBIOS, Devicetree, …) into a NvPCR named hardware. @@ -168,7 +175,8 @@ Takes the index of the PCR to extend. If or are specified defaults to 15, otherwise (and unless - is specified) defaults to 11. May not be combined with + is specified) defaults to 11. May be specified multiple times, in order + to measure to multiple PCRs at the same time. May not be combined with . diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index 8ec0a733c6..2a087ec8f8 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -29,7 +29,7 @@ static char **arg_banks = NULL; static char *arg_file_system = NULL; static bool arg_machine_id = false; static bool arg_product_id = false; -static unsigned arg_pcr_index = UINT_MAX; +static uint32_t arg_pcr_mask = 0; static char *arg_nvpcr_name = NULL; static bool arg_varlink = false; static bool arg_early = false; @@ -139,11 +139,16 @@ static int parse_argv(int argc, char *argv[]) { } case ARG_PCR: + if (isempty(optarg)) { + arg_pcr_mask = 0; + break; + } + r = tpm2_pcr_index_from_string(optarg); if (r < 0) return log_error_errno(r, "Failed to parse PCR index: %s", optarg); - arg_pcr_index = r; + arg_pcr_mask |= INDEX_TO_MASK(uint32_t, r); break; case ARG_NVPCR: @@ -213,7 +218,7 @@ static int parse_argv(int argc, char *argv[]) { if (!!arg_file_system + arg_machine_id + arg_product_id > 1) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system=, --machine-id, --product-id may not be combined."); - if (arg_pcr_index != UINT_MAX && arg_nvpcr_name) + if (arg_pcr_mask != 0 && arg_nvpcr_name) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--pcr= and --nvpcr= may not be combined."); r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); @@ -221,11 +226,11 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); if (r > 0) arg_varlink = true; - else if (arg_pcr_index == UINT_MAX && !arg_nvpcr_name) { - arg_pcr_index = - (arg_file_system || arg_machine_id) ? TPM2_PCR_SYSTEM_IDENTITY : /* → PCR 15 */ - !arg_product_id ? TPM2_PCR_KERNEL_BOOT : /* → PCR 11 */ - UINT_MAX; + else if (arg_pcr_mask == 0 && !arg_nvpcr_name) { + arg_pcr_mask = + (arg_file_system || arg_machine_id) ? INDEX_TO_MASK(uint32_t, TPM2_PCR_SYSTEM_IDENTITY) : /* → PCR 15 */ + !arg_product_id ? INDEX_TO_MASK(uint32_t, TPM2_PCR_KERNEL_BOOT) : /* → PCR 11 */ + 0; r = free_and_strdup_warn(&arg_nvpcr_name, arg_product_id ? "hardware" : NULL); if (r < 0) @@ -235,7 +240,7 @@ static int parse_argv(int argc, char *argv[]) { return 1; } -static int determine_banks(Tpm2Context *c, unsigned target_pcr_nr) { +static int determine_banks(Tpm2Context *c, uint32_t target_pcr_mask) { _cleanup_strv_free_ char **l = NULL; int r; @@ -244,7 +249,7 @@ static int determine_banks(Tpm2Context *c, unsigned target_pcr_nr) { if (!strv_isempty(arg_banks)) /* Explicitly configured? Then use that */ return 0; - r = tpm2_get_good_pcr_banks_strv(c, UINT32_C(1) << target_pcr_nr, &l); + r = tpm2_get_good_pcr_banks_strv(c, target_pcr_mask, &l); if (r < 0) return log_error_errno(r, "Could not verify pcr banks: %m"); @@ -275,7 +280,7 @@ static int escape_and_truncate_data(const void *data, size_t size, char **ret) { } static int extend_pcr_now( - unsigned pcr, + uint32_t pcr_mask, const void *data, size_t size, Tpm2UserspaceEventType event) { @@ -283,11 +288,13 @@ static int extend_pcr_now( _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; int r; + assert(pcr_mask != 0); + r = tpm2_context_new_or_warn(arg_tpm2_device, &c); if (r < 0) return r; - r = determine_banks(c, pcr); + r = determine_banks(c, pcr_mask); if (r < 0) return r; if (strv_isempty(arg_banks)) /* Still none? */ @@ -302,18 +309,20 @@ static int extend_pcr_now( if (escape_and_truncate_data(data, size, &safe) < 0) return log_oom(); - log_debug("Measuring '%s' into PCR index %u, banks %s.", safe, pcr, joined_banks); + BIT_FOREACH(pcr, pcr_mask) { + log_debug("Measuring '%s' into PCR index %i, banks %s.", safe, pcr, joined_banks); - r = tpm2_pcr_extend_bytes(c, arg_banks, pcr, &IOVEC_MAKE(data, size), /* secret= */ NULL, event, safe); - if (r < 0) - return log_error_errno(r, "Could not extend PCR: %m"); + r = tpm2_pcr_extend_bytes(c, arg_banks, pcr, &IOVEC_MAKE(data, size), /* secret= */ NULL, event, safe); + if (r < 0) + return log_error_errno(r, "Could not extend PCR: %m"); - log_struct(LOG_INFO, - LOG_MESSAGE_ID(SD_MESSAGE_TPM_PCR_EXTEND_STR), - LOG_MESSAGE("Extended PCR index %u with '%s' (banks %s).", pcr, safe, joined_banks), - LOG_ITEM("MEASURING=%s", safe), - LOG_ITEM("PCR=%u", pcr), - LOG_ITEM("BANKS=%s", joined_banks)); + log_struct(LOG_INFO, + LOG_MESSAGE_ID(SD_MESSAGE_TPM_PCR_EXTEND_STR), + LOG_MESSAGE("Extended PCR index %i with '%s' (banks %s).", pcr, safe, joined_banks), + LOG_ITEM("MEASURING=%s", safe), + LOG_ITEM("PCR=%i", pcr), + LOG_ITEM("BANKS=%s", joined_banks)); + } return 0; } @@ -435,7 +444,7 @@ static int vl_method_extend(sd_varlink *link, sd_json_variant *parameters, sd_va if (r == -ENOENT) return sd_varlink_error(link, "io.systemd.PCRExtend.NoSuchNvPCR", NULL); } else - r = extend_pcr_now(p.pcr, extend_iovec->iov_base, extend_iovec->iov_len, p.event_type); + r = extend_pcr_now(INDEX_TO_MASK(uint32_t, p.pcr), extend_iovec->iov_base, extend_iovec->iov_len, p.event_type); if (r < 0) return r; @@ -548,7 +557,7 @@ static int run(int argc, char *argv[]) { if (arg_nvpcr_name) r = extend_nvpcr_now(arg_nvpcr_name, word, strlen(word), event); else - r = extend_pcr_now(arg_pcr_index, word, strlen(word), event); + r = extend_pcr_now(arg_pcr_mask, word, strlen(word), event); if (r < 0) return r; diff --git a/src/pcrlock/meson.build b/src/pcrlock/meson.build index f7fa3d18d9..ff2b0f0cb2 100644 --- a/src/pcrlock/meson.build +++ b/src/pcrlock/meson.build @@ -27,6 +27,7 @@ install_data('pcrlock.d/500-separator.pcrlock.d/600-0xffffffff.pcrlock', install install_data('pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/300-present.pcrlock', install_dir : pcrlockdir / '700-action-efi-exit-boot-services.pcrlock.d') install_data('pcrlock.d/700-action-efi-exit-boot-services.pcrlock.d/600-absent.pcrlock', install_dir : pcrlockdir / '700-action-efi-exit-boot-services.pcrlock.d') install_data('pcrlock.d/750-enter-initrd.pcrlock', install_dir : pcrlockdir) +install_data('pcrlock.d/750-os-separator.pcrlock', install_dir : pcrlockdir) install_data('pcrlock.d/770-nvpcr-separator.pcrlock', install_dir : pcrlockdir) install_data('pcrlock.d/800-leave-initrd.pcrlock', install_dir : pcrlockdir) install_data('pcrlock.d/850-sysinit.pcrlock', install_dir : pcrlockdir) diff --git a/src/pcrlock/pcrlock.d/750-os-separator.pcrlock b/src/pcrlock/pcrlock.d/750-os-separator.pcrlock new file mode 100644 index 0000000000..aaeba174d1 --- /dev/null +++ b/src/pcrlock/pcrlock.d/750-os-separator.pcrlock @@ -0,0 +1 @@ +{"records":[{"pcr":0,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":1,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":2,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":3,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":4,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":5,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":6,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":7,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":9,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":12,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":13,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]},{"pcr":14,"digests":[{"hashAlg":"sha512","digest":"c2c4de82bbe2dd787a6bdca1014b9557313f258e18aaee2c9c3e2f9c79e526d66283db9aef5d7aeed3ffecd628ecc045716583eecd23ca73eee63f299a8dc2cc"},{"hashAlg":"sha384","digest":"5f2a9be7b55475435f46c343bb8c224db427d4fc7cdca42ac02e50d61c67a8bf0b707f7e1527a5f65175efdf8b56ba24"},{"hashAlg":"sha256","digest":"ff5b9d73dad709633ae76adf444012b57e913a12ed7403c3931145862f35f841"},{"hashAlg":"sha1","digest":"0d3751a5a6707fb8918dfbe6b0d9853ca6c313c4"}]}]} diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 05ea7c47be..fbf87d8d5a 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6680,6 +6680,7 @@ static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MA [TPM2_EVENT_NVPCR_SEPARATOR] = "nvpcr-separator", [TPM2_EVENT_DM_VERITY] = "dm-verity", [TPM2_EVENT_IMDS_USERDATA] = "imds-userdata", + [TPM2_EVENT_OS_SEPARATOR] = "os-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 2f5d8632de..5ada96e8e1 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -149,6 +149,7 @@ typedef enum Tpm2UserspaceEventType { TPM2_EVENT_NVPCR_SEPARATOR, TPM2_EVENT_DM_VERITY, TPM2_EVENT_IMDS_USERDATA, + TPM2_EVENT_OS_SEPARATOR, _TPM2_USERSPACE_EVENT_TYPE_MAX, _TPM2_USERSPACE_EVENT_TYPE_INVALID = -EINVAL, } Tpm2UserspaceEventType; diff --git a/test/units/TEST-70-TPM2.pcrextend.sh b/test/units/TEST-70-TPM2.pcrextend.sh index fbb6b30a12..14808f0763 100755 --- a/test/units/TEST-70-TPM2.pcrextend.sh +++ b/test/units/TEST-70-TPM2.pcrextend.sh @@ -53,7 +53,6 @@ fi (! "$SD_PCREXTEND" --bank= foo) (! "$SD_PCREXTEND" --tpm2-device= foo) (! "$SD_PCREXTEND" --tpm2-device=/dev/null foo) -(! "$SD_PCREXTEND" --pcr= foo) (! "$SD_PCREXTEND" --pcr=-1 foo) (! "$SD_PCREXTEND" --pcr=1024 foo) (! "$SD_PCREXTEND" --foo=bar) diff --git a/test/units/TEST-70-TPM2.pcrlock.sh b/test/units/TEST-70-TPM2.pcrlock.sh index d90b4bc99f..71f2ac53d7 100755 --- a/test/units/TEST-70-TPM2.pcrlock.sh +++ b/test/units/TEST-70-TPM2.pcrlock.sh @@ -42,6 +42,18 @@ PCRS="1+2+3+4+5+16" # (as the PCR values simply won't match the log). rm -f /run/log/systemd/tpm2-measure.log +# Add the os-separator measurements, they should be the only measurements that touch pcr 0…6 done from userspace. +RS=$'\x1e' +cat >/run/log/systemd/tpm2-measure.log < Date: Thu, 26 Mar 2026 11:16:10 +0100 Subject: [PATCH 11/12] units: make use of nvpcrs only after the NV anchor completion measurement is done This makes sure we don't use the "hardware" or "verity" nvpcrs before the NV anchor measurement is done. This is mostly to avoid confusing output, and to indirectly ensure the nvpcr allocation in tpm2-setup is the load bearing one, but it should not be load bearing for security afaics. --- units/systemd-pcrnvdone.service.in | 2 +- units/systemd-pcrproduct.service.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/units/systemd-pcrnvdone.service.in b/units/systemd-pcrnvdone.service.in index 7593dedfed..bbd0e66e60 100644 --- a/units/systemd-pcrnvdone.service.in +++ b/units/systemd-pcrnvdone.service.in @@ -13,7 +13,7 @@ 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 +Before=sysinit.target cryptsetup-pre.target cryptsetup.target shutdown.target ConditionSecurity=measured-os ConditionPathExists=!/etc/initrd-release FailureAction=reboot-force diff --git a/units/systemd-pcrproduct.service.in b/units/systemd-pcrproduct.service.in index 2562dea18f..1b121416a9 100644 --- a/units/systemd-pcrproduct.service.in +++ b/units/systemd-pcrproduct.service.in @@ -12,7 +12,7 @@ Description=TPM NvPCR Product ID Measurement Documentation=man:systemd-pcrproduct.service(8) DefaultDependencies=no Conflicts=shutdown.target -After=tpm2.target +After=tpm2.target systemd-pcrnvdone.service Before=sysinit.target shutdown.target RequiresMountsFor=/var/lib/systemd/nvpcr ConditionPathExists=!/etc/initrd-release From 1e254ad1bd857ebead192dab27b2610a4185358f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 10 Mar 2026 17:12:23 +0100 Subject: [PATCH 12/12] update TODO --- TODO | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/TODO b/TODO index 5855e36390..687d15b5ba 100644 --- a/TODO +++ b/TODO @@ -133,6 +133,14 @@ Features: * add service file setting to force the fwmark (a la SO_MARK) to some value, so that we can allowlist certain services for imds this way. +* lock down swtpm a bit to make it harder to extract keys from it as it is + running. i.e. make ptracing + termination hard from the outside. also run + swtpm as unpriv user (not trivial, probably requires patch swtpm, as it needs + to allocate vtpm device), to lock it down from the inside. + +* once swtpm's sd_notify() support has landed in the distributions, remove the + invocation in tpm2-swtpm.c and let swtpm handle it. + * make systemd work nicely without /bin/sh, logins and associated shell tools around - make sure debug shell service (sushell) has a nice failure mode, prints a message and reboots - varlink interface for "systemctl start" and friends @@ -156,13 +164,6 @@ Features: * on first login of a user, measure its identity to some nvpcr -* optionally spawn an swtpm instance if a system doesn't have a native tpm, do - it via the tpm generator - -* add a secret key logic to sd-stub, that uses early-boot efi variables for - storing, that can be used as fallback logic for tpm-less systems for disk - encryption, and swtpm state encryption. - * sd-lldp: pick up 802.3 maximum frame size/mtu, to be able to detect jumbo frame capable networks @@ -562,7 +563,7 @@ Features: service into the early boot, waiting for the DMI and network device to show up. -* Add UKI profile conditioning so that profles are only available if secure +* Add UKI profile conditioning so that profiles are only available if secure boot is turned off, or only on. similar, add conditions on TPM availability, network boot, and other conditions.