mirror of
https://github.com/systemd/systemd.git
synced 2026-06-30 19:57:29 +00:00
tpm2: add SWTPM fallback test, fixes and hardening (#42722)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
25
test/integration-tests/TEST-92-TPM2-SWTPM/meson.build
Normal file
25
test/integration-tests/TEST-92-TPM2-SWTPM/meson.build
Normal file
@@ -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',
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -104,6 +104,7 @@ foreach dirname : [
|
||||
'TEST-89-RESOLVED-MDNS',
|
||||
'TEST-90-RESTRICT-FSACCESS',
|
||||
'TEST-91-LIVEUPDATE',
|
||||
'TEST-92-TPM2-SWTPM',
|
||||
]
|
||||
subdir(dirname)
|
||||
endforeach
|
||||
|
||||
101
test/units/TEST-92-TPM2-SWTPM.sh
Executable file
101
test/units/TEST-92-TPM2-SWTPM.sh
Executable file
@@ -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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user