]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tpm2: add "systemd-tpm2-swtpm" wrapper for "swtpm"
authorLennart Poettering <lennart@amutable.com>
Mon, 9 Mar 2026 12:06:58 +0000 (13:06 +0100)
committerLennart Poettering <lennart@amutable.com>
Thu, 26 Mar 2026 15:11:34 +0000 (16:11 +0100)
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.

12 files changed:
man/rules/meson.build
man/systemd-tpm2-swtpm.service.xml [new file with mode: 0644]
mkosi/mkosi.conf
mkosi/mkosi.conf.d/centos-fedora/mkosi.conf
mkosi/mkosi.conf.d/debian-ubuntu/mkosi.conf
mkosi/mkosi.initrd.conf/mkosi.conf
mkosi/mkosi.initrd.conf/mkosi.conf.d/centos-fedora.conf
mkosi/mkosi.initrd.conf/mkosi.conf.d/debian-ubuntu.conf
src/tpm2-setup/meson.build
src/tpm2-setup/tpm2-swtpm.c [new file with mode: 0644]
units/meson.build
units/systemd-tpm2-swtpm.service.in [new file with mode: 0644]

index 5c14fb626bb42ed95799d0787d64726a08859d9b..0b5e438200f7aa7c1285ef4b8a7a5d585a28de6f 100644 (file)
@@ -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 (file)
index 0000000..3111a78
--- /dev/null
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+<refentry id="systemd-tpm2-swtpm.service" conditional='ENABLE_BOOTLOADER HAVE_TPM2'
+          xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <refentryinfo>
+    <title>systemd-tpm2-swtpm.service</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>systemd-tpm2-swtpm.service</refentrytitle>
+    <manvolnum>8</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>systemd-tpm2-swtpm.service</refname>
+    <refname>systemd-tpm2-swtpm</refname>
+    <refpurpose>Provide a fallback software TPM</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <para><filename>systemd-tpm2-swtpm.service</filename></para>
+    <para><filename>/usr/lib/systemd/systemd-tpm2-swtpm</filename></para>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para>The <filename>systemd-tpm2-swtpm.service</filename> 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
+    <citerefentry><refentrytitle>systemd-tpm2-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+    if a hardware TPM is not available, and the system is configured to provide a software TPM in that case.</para>
+
+    <para>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
+    <citerefentry><refentrytitle>swtpm</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
+
+    <para>If the boot secret <filename>/.extra/boot-secret</filename> (in the initrd) or
+    <filename>/run/systemd/stub/boot-secret</filename> (on the host) is available the software TPM NVRAM
+    storage is encrypted with this key. See
+    <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry> for
+    details.</para>
+
+    <para>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.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para><simplelist type="inline">
+      <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd-tpm2-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>swtpm</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
+    </simplelist></para>
+  </refsect1>
+</refentry>
index 80c4e59390c957986d12eba6a24fa4960f7dfa2d..8bbf964664e858aaf2305029427c502e50a227d9 100644 (file)
@@ -123,6 +123,7 @@ Packages=
         sed
         socat
         strace
+        swtpm
         tar
         tree
         util-linux
index fbbc6a90bf91c73d79a25c53b61798b89c921548..416e71ba32175b2066ff0b159056e67d07452cc4 100644 (file)
@@ -63,6 +63,7 @@ Packages=
         softhsm
         squashfs-tools
         stress-ng
+        swtpm-tools
         tpm2-tools
         veritysetup
         vim-common
index f024eae204d0f9a8c90a20d3672fcab429f76486..80dc87213a4eb05e8d2f0b6765721ca5a96ec887 100644 (file)
@@ -71,6 +71,7 @@ Packages=
         softhsm2
         squashfs-tools
         stress-ng
+        swtpm-tools
         tgt
         tpm2-tools
         tzdata
index 1c73f3a328440db5cd17fc8045eddc13aa10daca..207b15f98c903405962808c97037cf843c0eb998 100644 (file)
@@ -16,4 +16,5 @@ Packages=
         findutils
         grep
         sed
+        swtpm
         tar
index 1a971625bfe7f884c9fd3b8d7d0a0cbb6d0ad717..a35ccb4a0e446a876b511715246f8edc5c047a6a 100644 (file)
@@ -7,6 +7,7 @@ Distribution=|fedora
 [Content]
 PrepareScripts=%D/mkosi/mkosi.conf.d/centos-fedora/systemd.prepare
 Packages=
+        swtpm-tools
         tpm2-tools
 
 VolatilePackages=
index 7f2566e9938d2c4879260d6b32803432b5087791..1e5e8942373bdbd299cead1860e8fc1f31ce5dca 100644 (file)
@@ -9,6 +9,7 @@ PrepareScripts=%D/mkosi/mkosi.conf.d/debian-ubuntu/systemd.prepare
 Packages=
         btrfs-progs
         tpm2-tools
+        swtpm-tools
 
 VolatilePackages=
         libsystemd-shared
index a862e7239cc6ba2f3f55a12379699b0f11b967d6..bac29cbbdcabe20eb3199edd96665ab27e4c608d 100644 (file)
@@ -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 (file)
index 0000000..9522420
--- /dev/null
@@ -0,0 +1,221 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#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);
index ca17237dd0b16793514f5d6df943ef099c8d22a4..1d0e145287e61eb6e484afd5f1716d83398ea9ed 100644 (file)
@@ -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 (file)
index 0000000..10856f7
--- /dev/null
@@ -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