From 1b1900a6f3162b3f16b6779bdcca9ec0f9aa11e9 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Tue, 23 Jun 2026 14:40:51 +0200 Subject: [PATCH 1/6] test: add TEST-92-TPM2-SWTPM for the software TPM fallback Boot a VM in EFI mode without a hardware/firmware TPM and with systemd.tpm2_software_fallback=yes, so systemd-tpm2-generator manufactures a software TPM on the ESP in the initrd and chainloads swtpm. Assert the service starts, the vtpm-proxy device shows up, and a systemd-creds TPM2 seal/unseal round-trip works. Then reboot and confirm the sealed secret still unseals, i.e. the TPM state persisted on the ESP across the reboot. Signed-off-by: Paul Meyer --- .../TEST-92-TPM2-SWTPM/meson.build | 25 ++++++++ test/integration-tests/meson.build | 1 + test/units/TEST-92-TPM2-SWTPM.sh | 61 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 test/integration-tests/TEST-92-TPM2-SWTPM/meson.build create mode 100755 test/units/TEST-92-TPM2-SWTPM.sh diff --git a/test/integration-tests/TEST-92-TPM2-SWTPM/meson.build b/test/integration-tests/TEST-92-TPM2-SWTPM/meson.build new file mode 100644 index 0000000000..917ad55bb7 --- /dev/null +++ b/test/integration-tests/TEST-92-TPM2-SWTPM/meson.build @@ -0,0 +1,25 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +integration_tests += [ + integration_test_template + { + 'name' : fs.name(meson.current_source_dir()), + # The test reboots to verify TPM state persists across boots, so keep storage around. + 'storage' : 'persistent', + 'configuration' : integration_test_template['configuration'] + { + 'wants' : '@0@ tpm2.target'.format(integration_test_template['configuration']['wants']), + 'after' : '@0@ tpm2.target'.format(integration_test_template['configuration']['after']), + }, + 'vm' : true, + # We need to boot in EFI mode (with an initrd) so that an ESP exists for the software TPM to + # store its state on and so that systemd-tpm2-generator runs in the initrd. + 'firmware' : 'auto', + # Crucially, do NOT attach a hardware/firmware TPM: that's what makes the generator fall back + # to the software TPM, which is what this test exercises. + 'tpm' : false, + 'cmdline' : integration_test_template['cmdline'] + [ + # Opt into the software TPM fallback (off by default). + 'systemd.tpm2_software_fallback=yes', + 'systemd.default_device_timeout_sec=300', + ], + }, +] diff --git a/test/integration-tests/meson.build b/test/integration-tests/meson.build index 867fdecda2..3991b11e79 100644 --- a/test/integration-tests/meson.build +++ b/test/integration-tests/meson.build @@ -104,6 +104,7 @@ foreach dirname : [ 'TEST-89-RESOLVED-MDNS', 'TEST-90-RESTRICT-FSACCESS', 'TEST-91-LIVEUPDATE', + 'TEST-92-TPM2-SWTPM', ] subdir(dirname) endforeach diff --git a/test/units/TEST-92-TPM2-SWTPM.sh b/test/units/TEST-92-TPM2-SWTPM.sh new file mode 100755 index 0000000000..7793a7f58c --- /dev/null +++ b/test/units/TEST-92-TPM2-SWTPM.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# Exercises the software TPM fallback (systemd-tpm2-swtpm.service) across a reboot. The VM boots in EFI mode +# without a hardware/firmware TPM and with "systemd.tpm2_software_fallback=yes" (see the test's meson.build), +# so systemd-tpm2-generator manufactures a software TPM on the ESP in the initrd and chainloads swtpm. +# +# boot 0: the TPM is manufactured in the initrd; seal a secret to it and stash the blob. +# boot 1: the TPM state persisted on the ESP across the reboot, so the secret still unseals. +# +# See systemd-tpm2-swtpm.service(8). + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +CRED=/var/lib/systemd-tpm2-swtpm-test.cred +PLAINTEXT="swtpm round-trip" + +if [[ -n "${ASAN_OPTIONS:-}" ]]; then + # swtpm_setup is not built with sanitizers, but does NSS lookups that pull in the ASan-instrumented + # libnss_systemd.so, which aborts with "ASan runtime does not come first". Skip under sanitizers. + echo "swtpm_setup does not work under sanitizers, skipping the test" | tee --append /skipped + exit 77 +fi + +if [[ ! -x /usr/lib/systemd/systemd-tpm2-swtpm ]] || ! command -v swtpm >/dev/null || [[ ! -d /sys/firmware/efi ]]; then + echo "Software TPM prerequisites missing, skipping the test" | tee --append /skipped + exit 77 +fi + +assert_swtpm_up() { + systemctl is-active systemd-tpm2-swtpm.service + timeout 30 bash -c 'until [[ -c /dev/tpmrm0 ]]; do sleep 1; done' + test -c /dev/tpm0 + # No firmware TPM here, so has-tpm2 reports "partial"; assert the software driver is present. + assert_in '\+driver' "$(systemd-analyze has-tpm2 || :)" +} + +case "$REBOOT_COUNT" in + 0) + assert_swtpm_up + # Seal a secret to the software TPM and keep the blob for the next boot. + echo -n "$PLAINTEXT" >/tmp/swtpm-plaintext + systemd-creds encrypt --name= --with-key=tpm2 /tmp/swtpm-plaintext "$CRED" + systemd-creds decrypt --name= "$CRED" - | cmp /tmp/swtpm-plaintext - + systemctl_final reboot + exec sleep infinity + ;; + 1) + assert_swtpm_up + # Persistence: the TPM state survived the reboot on the ESP, so the blob still unseals. + echo -n "$PLAINTEXT" >/tmp/swtpm-plaintext + systemd-creds decrypt --name= "$CRED" - | cmp /tmp/swtpm-plaintext - + touch /testok + ;; + *) + assert_not_reached + ;; +esac From df2baac2b9a54ccc1043c71b190e1b684bb19cfc Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Tue, 23 Jun 2026 14:46:24 +0200 Subject: [PATCH 2/6] tpm2: stop the software TPM before the ESP is unmounted on shutdown swtpm keeps its state on the ESP (--tpmstate=dir=) and thus holds it busy for as long as it runs, but nothing ensured it was stopped before the ESP was unmounted on shutdown, leaving boot.mount failing to unmount. Two things were missing: - systemd-tpm2-swtpm.service has DefaultDependencies=no, which strips the implicit shutdown.target membership, so it was torn down late rather than stopped in an ordered manner. Add Conflicts=/Before=shutdown.target, as the sibling systemd-tpm2-setup{,-early}.service units already do. - The generator only ordered the service After=boot.automount/efi.automount. Ordering after the .automount units is enough for start-up, but only an ordering against the actual .mount units makes the service stop (releasing the ESP) before the file system is unmounted. Add boot.mount/efi.mount to the After= line; this is a no-op at start-up, as the mount has no job of its own there (it is triggered on access via the automount). Signed-off-by: Paul Meyer --- src/tpm2-setup/tpm2-generator.c | 8 ++++++-- units/systemd-tpm2-swtpm.service.in | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/tpm2-setup/tpm2-generator.c b/src/tpm2-setup/tpm2-generator.c index 65f450daaf..5a4a88bf9f 100644 --- a/src/tpm2-setup/tpm2-generator.c +++ b/src/tpm2-setup/tpm2-generator.c @@ -114,14 +114,18 @@ static int generate_swtpm_symlink(Tpm2Support support) { 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. */ + * XBOOTLDR, hence order after both. We also order after the .mount units (not just the + * .automount units): ordering after the automount is enough for start-up, but only an + * ordering against the actual mount unit ensures swtpm is stopped (releasing the ESP) before + * the file system is unmounted on shutdown. This is a no-op at start-up, as the mount has no + * job of its own there (it is triggered on access via the automount). */ 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"); + "After=boot.automount efi.automount boot.mount efi.mount\n"); if (r < 0) return log_error_errno(r, "Failed to hook ESP mount before systemd-tpm2-swtpm.service: %m"); diff --git a/units/systemd-tpm2-swtpm.service.in b/units/systemd-tpm2-swtpm.service.in index 10856f70d9..5ca946248c 100644 --- a/units/systemd-tpm2-swtpm.service.in +++ b/units/systemd-tpm2-swtpm.service.in @@ -11,10 +11,11 @@ Description=Fallback Software TPM Documentation=man:systemd-tpm2-swtpm.service(8) DefaultDependencies=no +Conflicts=shutdown.target After=systemd-sysusers.service Wants=modprobe@tpm_vtpm_proxy.service After=modprobe@tpm_vtpm_proxy.service -Before=tpm2.target sysinit.target +Before=tpm2.target sysinit.target shutdown.target [Service] Type=notify From 2f1c3b8afb555fb05925cc284b28a5fa50d02ff9 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Wed, 17 Jun 2026 18:03:55 +0200 Subject: [PATCH 3/6] units: harden systemd-tpm2-swtpm.service Lock down the software TPM service: restrict the runtime directory (which holds the AES key sealing swtpm's state) to 0700, and apply the usual sandboxing (NoNewPrivileges, MemoryDenyWriteExecute, ProtectSystem-adjacent Protect*/Restrict* knobs, PrivateNetwork, PrivateTmp, a @system-service syscall filter, etc.). A few common knobs can't be used here: the service must keep CAP_SYS_ADMIN (needed for the ioctl that creates the vtpm proxy device on /dev/vtpmx), and it needs runtime access to the ESP and its backing block device at a path only known at runtime, which rules out PrivateDevices=, DevicePolicy=, ProtectSystem= and User=/DynamicUser=. Signed-off-by: Paul Meyer --- units/systemd-tpm2-swtpm.service.in | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/units/systemd-tpm2-swtpm.service.in b/units/systemd-tpm2-swtpm.service.in index 5ca946248c..e9127f34c9 100644 --- a/units/systemd-tpm2-swtpm.service.in +++ b/units/systemd-tpm2-swtpm.service.in @@ -18,10 +18,32 @@ After=modprobe@tpm_vtpm_proxy.service Before=tpm2.target sysinit.target shutdown.target [Service] -Type=notify -RuntimeDirectory=systemd/swtpm +CapabilityBoundingSet=CAP_SYS_ADMIN 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 +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateNetwork=yes +PrivateTmp=disconnected +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectProc=invisible +RestrictAddressFamilies=AF_UNIX AF_NETLINK +RestrictNamespaces=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +RuntimeDirectory=systemd/swtpm +RuntimeDirectoryMode=0700 +SystemCallArchitectures=native +SystemCallErrorNumber=EPERM +SystemCallFilter=@system-service +Type=notify +UMask=0077 From 47b6a2062150bff5199afe661518ffb86b65e41f Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Thu, 25 Jun 2026 13:27:21 +0200 Subject: [PATCH 4/6] fileio: add write_string_filef_at() Signed-off-by: Paul Meyer --- src/basic/fileio.c | 20 ++++++++++++++++++++ src/basic/fileio.h | 1 + 2 files changed, 21 insertions(+) diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 1f2143e885..d7b4a02cbb 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -400,6 +400,26 @@ int write_string_filef( return write_string_file(fn, p, flags); } +int write_string_filef_at( + int dir_fd, + const char *fn, + WriteStringFileFlags flags, + const char *format, ...) { + + _cleanup_free_ char *p = NULL; + va_list ap; + int r; + + va_start(ap, format); + r = vasprintf(&p, format, ap); + va_end(ap); + + if (r < 0) + return -ENOMEM; + + return write_string_file_at(dir_fd, fn, p, flags); +} + int write_base64_file_at( int dir_fd, const char *fn, diff --git a/src/basic/fileio.h b/src/basic/fileio.h index 5e6e3de1fe..d10b04c907 100644 --- a/src/basic/fileio.h +++ b/src/basic/fileio.h @@ -56,6 +56,7 @@ static inline int write_string_file(const char *fn, const char *line, WriteStrin return write_string_file_at(AT_FDCWD, fn, line, flags); } int write_string_filef(const char *fn, WriteStringFileFlags flags, const char *format, ...) _printf_(3, 4); +int write_string_filef_at(int dir_fd, const char *fn, WriteStringFileFlags flags, const char *format, ...) _printf_(4, 5); int write_base64_file_at(int dir_fd, const char *fn, const struct iovec *data, WriteStringFileFlags flags); From abf96165a6ecf22e26a39e1ec95fc65859b6b542 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Wed, 24 Jun 2026 08:58:10 +0200 Subject: [PATCH 5/6] tpm2: write swtpm config files atomically via the state directory fd Open the swtpm state directory once and write the three config files relative to that fd with WRITE_STRING_FILE_ATOMIC, rather than by path with a plain truncating write. Writing atomically ensures a crash or a concurrent reader never observes a half-written config file, and operating through a single directory fd lets later steps reuse it. Signed-off-by: Paul Meyer --- src/shared/swtpm-util.c | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/shared/swtpm-util.c b/src/shared/swtpm-util.c index 156a5422df..8ad221a996 100644 --- a/src/shared/swtpm-util.c +++ b/src/shared/swtpm-util.c @@ -135,6 +135,10 @@ int manufacture_swtpm(const char *state_dir, const char *secret) { assert(state_dir); + _cleanup_close_ int state_dir_fd = xopenat(AT_FDCWD, state_dir, O_RDONLY|O_DIRECTORY|O_CLOEXEC); + if (state_dir_fd < 0) + return log_error_errno(state_dir_fd, "Failed to open TPM state directory '%s': %m", state_dir); + _cleanup_free_ char *swtpm_setup = NULL; r = find_executable("swtpm_setup", &swtpm_setup); if (r < 0) @@ -151,9 +155,9 @@ int manufacture_swtpm(const char *state_dir, const char *secret) { if (!localca_conf) return log_oom(); - r = write_string_filef( - localca_conf, - WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE|WRITE_STRING_FILE_MKDIR_0755, + r = write_string_filef_at( + state_dir_fd, "swtpm-localca.conf", + WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC, "statedir = %1$s\n" "signingkey = %1$s/signing-private-key.pem\n" "issuercert = %1$s/issuer-certificate.pem\n" @@ -166,12 +170,12 @@ int manufacture_swtpm(const char *state_dir, const char *secret) { if (!localca_options) return log_oom(); - r = write_string_file( - localca_options, + r = write_string_file_at( + state_dir_fd, "swtpm-localca.options", "--platform-manufacturer systemd\n" "--platform-version 2.1\n" "--platform-model swtpm\n", - WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE|WRITE_STRING_FILE_MKDIR_0755); + WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); if (r < 0) return log_error_errno(r, "Failed to write swtpm-localca.options: %m"); @@ -184,9 +188,9 @@ int manufacture_swtpm(const char *state_dir, const char *secret) { if (!setup_conf) return log_oom(); - r = write_string_filef( - setup_conf, - WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_TRUNCATE|WRITE_STRING_FILE_MKDIR_0755, + r = write_string_filef_at( + state_dir_fd, "swtpm_setup.conf", + WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC, "create_certs_tool = %1$s\n" "create_certs_tool_config = %2$s\n" "create_certs_tool_options = %3$s\n", From e3ebdb8ec57c976db393723dbd969cde3165d0e7 Mon Sep 17 00:00:00 2001 From: Paul Meyer Date: Tue, 23 Jun 2026 16:07:34 +0200 Subject: [PATCH 6/6] tpm2: re-manufacture software TPM when state dir is incomplete setup_swtpm() decided whether a software TPM had already been manufactured by checking whether the state directory was empty. But manufacture_swtpm() writes swtpm's config files before forking swtpm_setup, so an interrupted manufacture leaves the directory non-empty yet without a usable TPM. The next boot then mistook it for a complete TPM and started swtpm against a broken state directory. Keying off a swtpm state file like tpm2-00.permall is no better, as swtpm_setup gives no guarantee any single one is written atomically or last. Instead, have manufacture_swtpm() write a marker (.manufactured) as its very last step, once swtpm_setup has exited successfully, and gate on it: re-manufacture when it is missing in the initrd, and refuse rather than start a broken TPM outside it. Signed-off-by: Paul Meyer --- src/shared/swtpm-util.c | 17 ++++++++++-- src/shared/swtpm-util.h | 5 ++++ src/tpm2-setup/tpm2-swtpm.c | 24 ++++++++++++----- test/units/TEST-92-TPM2-SWTPM.sh | 44 ++++++++++++++++++++++++++++++-- 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/shared/swtpm-util.c b/src/shared/swtpm-util.c index 8ad221a996..1fa0ad3725 100644 --- a/src/shared/swtpm-util.c +++ b/src/shared/swtpm-util.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include "sd-json.h" @@ -8,6 +9,7 @@ #include "escape.h" #include "fd-util.h" #include "fileio.h" +#include "fs-util.h" #include "json-util.h" #include "log.h" #include "memfd-util.h" @@ -17,6 +19,7 @@ #include "string-util.h" #include "strv.h" #include "swtpm-util.h" +#include "sync-util.h" static int swtpm_find_best_profile(const char *swtpm_setup, char **ret) { int r; @@ -135,9 +138,9 @@ int manufacture_swtpm(const char *state_dir, const char *secret) { assert(state_dir); - _cleanup_close_ int state_dir_fd = xopenat(AT_FDCWD, state_dir, O_RDONLY|O_DIRECTORY|O_CLOEXEC); + _cleanup_close_ int state_dir_fd = open(state_dir, O_RDONLY|O_DIRECTORY|O_CLOEXEC); if (state_dir_fd < 0) - return log_error_errno(state_dir_fd, "Failed to open TPM state directory '%s': %m", state_dir); + return log_error_errno(errno, "Failed to open TPM state directory '%s': %m", state_dir); _cleanup_free_ char *swtpm_setup = NULL; r = find_executable("swtpm_setup", &swtpm_setup); @@ -238,5 +241,15 @@ int manufacture_swtpm(const char *state_dir, const char *secret) { _exit(EXIT_FAILURE); } + /* Persist swtpm_setup's freshly created TPM state before writing the completion marker. */ + r = syncfs_path(state_dir_fd, NULL); + if (r < 0) + return log_error_errno(r, "Failed to sync TPM state directory: %m"); + + /* Marker, written last, signals that manufacturing completed successfully. */ + _cleanup_close_ int marker_fd = xopenat(state_dir_fd, SWTPM_MANUFACTURED_MARKER, O_WRONLY|O_CREAT|O_CLOEXEC|O_NOFOLLOW); + if (marker_fd < 0) + return log_error_errno(marker_fd, "Failed to write '%s' marker: %m", SWTPM_MANUFACTURED_MARKER); + return 0; } diff --git a/src/shared/swtpm-util.h b/src/shared/swtpm-util.h index 9c1c737721..8897d17126 100644 --- a/src/shared/swtpm-util.h +++ b/src/shared/swtpm-util.h @@ -1,4 +1,9 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +/* Marker file written into the TPM state directory once swtpm_setup has successfully created the TPM state. + * It is written last, so its presence reliably means a complete TPM was manufactured, rather than a manufacture + * that was interrupted halfway through. */ +#define SWTPM_MANUFACTURED_MARKER ".manufactured" + int manufacture_swtpm(const char *state_dir, const char *secret); diff --git a/src/tpm2-setup/tpm2-swtpm.c b/src/tpm2-setup/tpm2-swtpm.c index db5ec09312..33d5defd06 100644 --- a/src/tpm2-setup/tpm2-swtpm.c +++ b/src/tpm2-setup/tpm2-swtpm.c @@ -20,8 +20,8 @@ #include "main-func.h" #include "path-lookup.h" #include "path-util.h" +#include "rm-rf.h" #include "sha256.h" -#include "stat-util.h" #include "string-util.h" #include "strv.h" #include "swtpm-util.h" @@ -116,18 +116,28 @@ static int setup_swtpm(const char *state_dir, int state_fd, const char *secret) 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."); + /* manufacture_swtpm() writes its marker only after swtpm_setup has fully created the TPM state. + * If the marker file is missing, the existing state is incomplete and recreation is needed. */ + r = RET_NERRNO(faccessat(state_fd, SWTPM_MANUFACTURED_MARKER, F_OK, AT_SYMLINK_NOFOLLOW)); + if (r >= 0) { + log_debug("TPM state directory holds a fully manufactured TPM, not manufacturing a TPM."); return 0; } + if (r != -ENOENT) + return log_error_errno(r, "Failed to check for TPM manufacture marker: %m"); 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."); + /* Cleanup incomplete state before recreating. */ + _cleanup_close_ int wipe_fd = fd_reopen(state_fd, O_RDONLY|O_DIRECTORY|O_CLOEXEC); + if (wipe_fd < 0) + return log_error_errno(wipe_fd, "Failed to reopen swtpm state directory: %m"); + r = rm_rf_children(TAKE_FD(wipe_fd), REMOVE_PHYSICAL, /* root_dev= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to clear incomplete swtpm state directory: %m"); + + log_debug("TPM state directory holds no fully manufactured TPM, manufacturing a TPM."); return manufacture_swtpm(state_dir, secret); } diff --git a/test/units/TEST-92-TPM2-SWTPM.sh b/test/units/TEST-92-TPM2-SWTPM.sh index 7793a7f58c..8e2c7d1143 100755 --- a/test/units/TEST-92-TPM2-SWTPM.sh +++ b/test/units/TEST-92-TPM2-SWTPM.sh @@ -3,12 +3,16 @@ set -eux set -o pipefail -# Exercises the software TPM fallback (systemd-tpm2-swtpm.service) across a reboot. The VM boots in EFI mode +# Exercises the software TPM fallback (systemd-tpm2-swtpm.service) across reboots. The VM boots in EFI mode # without a hardware/firmware TPM and with "systemd.tpm2_software_fallback=yes" (see the test's meson.build), # so systemd-tpm2-generator manufactures a software TPM on the ESP in the initrd and chainloads swtpm. # # boot 0: the TPM is manufactured in the initrd; seal a secret to it and stash the blob. -# boot 1: the TPM state persisted on the ESP across the reboot, so the secret still unseals. +# boot 1: the TPM state persisted on the ESP across the reboot, so the secret still unseals. Then mimic a +# manufacture that was interrupted before it completed (drop everything but the config files, so the +# ".manufactured" marker is gone) and reboot. +# boot 2: setup_swtpm() must notice the missing marker and re-manufacture, rather than mistaking the +# leftover config files for a complete TPM and starting swtpm against a stateless directory. # # See systemd-tpm2-swtpm.service(8). @@ -17,6 +21,8 @@ set -o pipefail CRED=/var/lib/systemd-tpm2-swtpm-test.cred PLAINTEXT="swtpm round-trip" +# Marker (SWTPM_MANUFACTURED_MARKER) that manufacture_swtpm() indicates completion with. +MARKER=.manufactured if [[ -n "${ASAN_OPTIONS:-}" ]]; then # swtpm_setup is not built with sanitizers, but does NSS lookups that pull in the ASan-instrumented @@ -38,6 +44,15 @@ assert_swtpm_up() { assert_in '\+driver' "$(systemd-analyze has-tpm2 || :)" } +# Locate swtpm's state directory on the ESP. +swtpm_state_dir() { + local d + for d in /boot/loader/swtpm /efi/loader/swtpm; do + [[ -d "$d" ]] && { echo "$d"; return 0; } + done + return 1 +} + case "$REBOOT_COUNT" in 0) assert_swtpm_up @@ -53,6 +68,31 @@ case "$REBOOT_COUNT" in # Persistence: the TPM state survived the reboot on the ESP, so the blob still unseals. echo -n "$PLAINTEXT" >/tmp/swtpm-plaintext systemd-creds decrypt --name= "$CRED" - | cmp /tmp/swtpm-plaintext - + + # Mimic a manufacture interrupted after swtpm began writing TPM state but before the marker: stop + # swtpm, drop everything except the three config files, then leave a partial/corrupt state file + # behind. swtpm_setup --not-overwrite would refuse to recreate that, so recovery must clear it first. + statedir="$(swtpm_state_dir)" + systemctl stop systemd-tpm2-swtpm.service + find "$statedir" -mindepth 1 -maxdepth 1 -type f \ + ! -name swtpm-localca.conf ! -name swtpm-localca.options ! -name swtpm_setup.conf -delete + echo "corrupt" >"$statedir/tpm2-00.permall" + test -e "$statedir/swtpm_setup.conf" + test ! -e "$statedir/$MARKER" + systemctl_final reboot + exec sleep infinity + ;; + 2) + # setup_swtpm() must have re-manufactured instead of trusting the leftover config files: the marker is + # back and swtpm_setup re-ran swtpm_localca, recreating issuer-certificate.pem. The TPM must also work. + # Regression test for keying re-manufacture off an incomplete state directory. + assert_swtpm_up + statedir="$(swtpm_state_dir)" + test -e "$statedir/$MARKER" + test -e "$statedir/issuer-certificate.pem" + echo -n "$PLAINTEXT" >/tmp/swtpm-plaintext + systemd-creds encrypt --name= --with-key=tpm2 /tmp/swtpm-plaintext /tmp/swtpm-new.cred + systemd-creds decrypt --name= /tmp/swtpm-new.cred - | cmp /tmp/swtpm-plaintext - touch /testok ;; *)