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.
This commit is contained in:
Lennart Poettering
2026-03-26 16:11:34 +01:00
parent 6718ba1769
commit 056c21aaeb
12 changed files with 333 additions and 0 deletions
+4
View 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',
+63
View File
@@ -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>
+1
View File
@@ -123,6 +123,7 @@ Packages=
sed
socat
strace
swtpm
tar
tree
util-linux
@@ -63,6 +63,7 @@ Packages=
softhsm
squashfs-tools
stress-ng
swtpm-tools
tpm2-tools
veritysetup
vim-common
@@ -71,6 +71,7 @@ Packages=
softhsm2
squashfs-tools
stress-ng
swtpm-tools
tgt
tpm2-tools
tzdata
+1
View File
@@ -16,4 +16,5 @@ Packages=
findutils
grep
sed
swtpm
tar
@@ -7,6 +7,7 @@ Distribution=|fedora
[Content]
PrepareScripts=%D/mkosi/mkosi.conf.d/centos-fedora/systemd.prepare
Packages=
swtpm-tools
tpm2-tools
VolatilePackages=
@@ -9,6 +9,7 @@ PrepareScripts=%D/mkosi/mkosi.conf.d/debian-ubuntu/systemd.prepare
Packages=
btrfs-progs
tpm2-tools
swtpm-tools
VolatilePackages=
libsystemd-shared
+9
View 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'),
+221
View File
@@ -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);
+4
View 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'],
+26
View File
@@ -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