diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 1f2143e8854..d7b4a02cbb2 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 5e6e3de1fef..d10b04c9078 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); diff --git a/src/shared/swtpm-util.c b/src/shared/swtpm-util.c index 156a5422df9..1fa0ad37255 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,6 +138,10 @@ int manufacture_swtpm(const char *state_dir, const char *secret) { assert(state_dir); + _cleanup_close_ int state_dir_fd = open(state_dir, O_RDONLY|O_DIRECTORY|O_CLOEXEC); + if (state_dir_fd < 0) + 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); if (r < 0) @@ -151,9 +158,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 +173,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 +191,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", @@ -234,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 9c1c7377218..8897d17126a 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-generator.c b/src/tpm2-setup/tpm2-generator.c index 65f450daaff..5a4a88bf9f4 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/src/tpm2-setup/tpm2-swtpm.c b/src/tpm2-setup/tpm2-swtpm.c index db5ec09312b..33d5defd060 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/integration-tests/TEST-92-TPM2-SWTPM/meson.build b/test/integration-tests/TEST-92-TPM2-SWTPM/meson.build new file mode 100644 index 00000000000..917ad55bb73 --- /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 867fdecda2d..3991b11e79c 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 00000000000..8e2c7d11432 --- /dev/null +++ b/test/units/TEST-92-TPM2-SWTPM.sh @@ -0,0 +1,101 @@ +#!/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 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. 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). + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +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 + # 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 || :)" +} + +# 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 + # 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 - + + # 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 + ;; + *) + assert_not_reached + ;; +esac diff --git a/units/systemd-tpm2-swtpm.service.in b/units/systemd-tpm2-swtpm.service.in index 10856f70d9e..e9127f34c90 100644 --- a/units/systemd-tpm2-swtpm.service.in +++ b/units/systemd-tpm2-swtpm.service.in @@ -11,16 +11,39 @@ 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 -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