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