report: add systemd-report-sign-tsm backend (#42683)

Add tsm_report_acquire(), a thin wrapper around the kernel's
/sys/kernel/config/tsm/report/ configfs interface for fetching a
confidential-computing attestation report (SEV-SNP, TDX, ...), including
a caller supplied input.

A report signing backend that returns a confidential-computing
attestation report obtained via configfs-tsm. Implements
io.systemd.Report.Signer.Sign(): embeds the digest as the report's
inblob and returns the outblob (plus provider and any aux/manifest
blobs). Wired up as the "tsm" mechanism with a socket-activated service.
This commit is contained in:
Lennart Poettering
2026-06-22 23:02:58 +02:00
committed by GitHub
11 changed files with 580 additions and 0 deletions
+5
View File
@@ -1196,6 +1196,11 @@ manpages = [
['systemd-report-sign-plain',
'systemd-report-sign-plain.socket'],
'HAVE_OPENSSL'],
['systemd-report-sign-tsm@.service',
'8',
['systemd-report-sign-tsm',
'systemd-report-sign-tsm.socket'],
''],
['systemd-resolved.service', '8', ['systemd-resolved'], 'ENABLE_RESOLVE'],
['systemd-rfkill.service',
'8',
+73
View File
@@ -0,0 +1,73 @@
<?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-report-sign-tsm_.service"
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>systemd-report-sign-tsm@.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-report-sign-tsm@.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-report-sign-tsm@.service</refname>
<refname>systemd-report-sign-tsm.socket</refname>
<refname>systemd-report-sign-tsm</refname>
<refpurpose>Sign system reports with a confidential-computing attestation report</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-report-sign-tsm@.service</filename></para>
<para><filename>systemd-report-sign-tsm.socket</filename></para>
<para><filename>/usr/lib/systemd/systemd-report-sign-tsm</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-report-sign-tsm@.service</command> is a system service that signs system reports
generated by
<citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry>. It is
a signing backend for the <option>--sign=</option> logic of that tool: it implements the
<function>io.systemd.Report.Signer.Sign()</function> Varlink method and is reached via a socket linked into
the <filename>/run/systemd/report.sign/</filename> directory, named <filename>tsm</filename>.</para>
<para>The service is socket-activated (one instance per connection) via
<filename>systemd-report-sign-tsm.socket</filename>. Rather than signing with a local key, it obtains a
hardware-backed attestation report from the platform's Trusted Security Module (TSM) through the kernel's
configfs-TSM interface at <filename>/sys/kernel/config/tsm/report/</filename>. The digest passed to it is
embedded into the report as its run-time provided data (<literal>inblob</literal>), cryptographically
binding the attestation report to the system report being signed.</para>
<para>The returned signature carries the binary attestation report (the <literal>outblob</literal>), the
name of the TSM provider that generated it (for example <literal>sev_guest</literal> for AMD SEV-SNP or
<literal>tdx_guest</literal> for Intel TDX), and, where the provider supplies them, supplemental
certificate (<literal>auxblob</literal>) and manifest (<literal>manifestblob</literal>) data. A verifier
selects the appropriate validation logic based on the provider field.</para>
<para>This backend is only functional inside a confidential virtual machine whose kernel exposes the
configfs-TSM interface (such as AMD SEV-SNP or Intel TDX guests). On systems where the interface is
unavailable the signing operation is reported as unsupported and the mechanism is skipped. The service
keeps no persistent state.</para>
</refsect1>
<!-- Note: we do not document the command line switches here. The systemd-report-sign-tsm binary itself
should not be invoked by users, but only as a service. Use systemd-report(1) to generate signed
reports. -->
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
</refentry>
+1
View File
@@ -35,6 +35,7 @@ enable systemd-report-basic.socket
enable systemd-report-cgroup.socket
enable systemd-report-files.socket
enable systemd-report-sign-plain.socket
enable systemd-report-sign-tsm.socket
enable systemd-report.socket
enable systemd-resolved.service
enable systemd-sysext.service
+6
View File
@@ -44,4 +44,10 @@ executables += [
),
'dependencies' : libopenssl_cflags,
},
libexec_template + {
'name' : 'systemd-report-sign-tsm',
'sources' : files(
'report-sign-tsm.c',
),
},
]
+167
View File
@@ -0,0 +1,167 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-json.h"
#include "sd-varlink.h"
#include "build.h"
#include "format-table.h"
#include "help-util.h"
#include "iovec-util.h"
#include "json-util.h"
#include "log.h"
#include "main-func.h"
#include "options.h"
#include "sha256.h"
#include "string-util.h"
#include "tsm-report.h"
#include "varlink-io.systemd.Report.Signer.h"
#include "varlink-util.h"
typedef struct SignParameters {
struct iovec digest;
const char *algorithm;
} SignParameters;
static void sign_parameters_done(SignParameters *p) {
iovec_done(&p->digest);
}
static int vl_method_sign(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
static const sd_json_dispatch_field dispatch_table[] = {
{ "digest", SD_JSON_VARIANT_STRING, json_dispatch_unhex_iovec, offsetof(SignParameters, digest), SD_JSON_MANDATORY },
{ "algorithm", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(SignParameters, algorithm), SD_JSON_MANDATORY },
{}
};
_cleanup_(sign_parameters_done) SignParameters sp = {};
_cleanup_(tsm_report_freep) TsmReport *report = NULL;
int r;
assert(link);
assert(parameters);
r = sd_varlink_dispatch(link, parameters, dispatch_table, &sp);
if (r != 0)
return r;
if (!streq(sp.algorithm, "SHA256"))
return sd_varlink_error_invalid_parameter_name(link, "algorithm");
if (sp.digest.iov_len != SHA256_DIGEST_SIZE)
return sd_varlink_error_invalid_parameter_name(link, "digest");
uint8_t report_data[TSM_REPORT_DATA_SIZE] = {};
memcpy(report_data, sp.digest.iov_base, sp.digest.iov_len);
r = tsm_report_acquire(&IOVEC_MAKE(report_data, sizeof report_data), /* options= */ NULL, &report);
if (IN_SET(r, -EOPNOTSUPP, -ENXIO))
/* Returning an error will fail the whole report signing, so we return
* no signature instead which will be ignored by the aggregator. */
return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_EMPTY_ARRAY("data"));
if (r < 0)
return log_error_errno(r, "Failed to acquire TSM report: %m");
return sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR("data",
SD_JSON_BUILD_ARRAY(
SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_STRING("provider", report->provider),
JSON_BUILD_PAIR_IOVEC_BASE64("outblob", &report->outblob),
SD_JSON_BUILD_PAIR_CONDITION(iovec_is_set(&report->auxblob),
"auxblob", JSON_BUILD_IOVEC_BASE64(&report->auxblob)),
SD_JSON_BUILD_PAIR_CONDITION(iovec_is_set(&report->manifestblob),
"manifestblob", JSON_BUILD_IOVEC_BASE64(&report->manifestblob))))));
}
static int vl_server(void) {
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL;
int r;
r = varlink_server_new(&vs, SD_VARLINK_SERVER_ROOT_ONLY|SD_VARLINK_SERVER_MYSELF_ONLY, /* userdata= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
r = sd_varlink_server_add_interface(vs, &vl_interface_io_systemd_Report_Signer);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = sd_varlink_server_bind_method(vs, "io.systemd.Report.Signer.Sign", vl_method_sign);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
r = sd_varlink_server_loop_auto(vs);
if (r < 0)
return log_error_errno(r, "Failed to run Varlink event loop: %m");
return 0;
}
static int help(void) {
_cleanup_(table_unrefp) Table *options = NULL;
int r;
r = option_parser_get_help_table(&options);
if (r < 0)
return r;
help_cmdline("[OPTIONS...]");
help_abstract("Get an attestation report via configfs Trusted Security Module (TSM) "
"that includes the hash of the system report.");
help_section("Options");
r = table_print_or_warn(options);
if (r < 0)
return r;
help_man_page_reference("systemd-report-sign-tsm@.service", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
int r;
assert(argc >= 0);
assert(argv);
OptionParser opts = { argc, argv };
FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
OPTION_COMMON_VERSION:
return version();
}
if (option_parser_get_n_args(&opts) > 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"This program takes no arguments.");
r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT);
if (r < 0)
return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m");
if (r == 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"This program can only run as a Varlink service.");
return 1;
}
static int run(int argc, char *argv[]) {
int r;
log_setup();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
return vl_server();
}
DEFINE_MAIN_FUNCTION(run);
+1
View File
@@ -208,6 +208,7 @@ shared_sources = files(
'tomoyo-util.c',
'tpm2-util.c',
'tpm2-event-log.c',
'tsm-report.c',
'udev-util.c',
'unit-file.c',
'user-record-nss.c',
+214
View File
@@ -0,0 +1,214 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fcntl.h>
#include <unistd.h>
#include "sd-id128.h"
#include "alloc-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-util.h"
#include "fs-util.h"
#include "io-util.h"
#include "iovec-util.h"
#include "log.h"
#include "parse-util.h"
#include "process-util.h"
#include "stdio-util.h"
#include "tsm-report.h"
#define TSM_REPORT_PATH "/sys/kernel/config/tsm/report"
TsmReport *tsm_report_free(TsmReport *report) {
if (!report)
return NULL;
free(report->provider);
iovec_done(&report->outblob);
iovec_done(&report->auxblob);
iovec_done(&report->manifestblob);
return mfree(report);
}
static int read_generation(int entry_fd, uint64_t *ret) {
_cleanup_free_ char *s = NULL;
int r;
assert(entry_fd >= 0);
assert(ret);
/* The kernel bumps this counter on every option write to the entry. We snapshot it before writing
* and re-check it after reading the report to detect a writer racing us on this entry. */
r = read_one_line_file_at(entry_fd, "generation", &s);
if (r < 0)
return log_debug_errno(r, "Failed to read 'generation' attribute: %m");
r = safe_atou64(s, ret);
if (r < 0)
return log_debug_errno(r, "Failed to parse 'generation' attribute: %m");
return 0;
}
static int tsm_report_fill(
int entry_fd,
const struct iovec *report_data,
const TsmReportOptions *options,
TsmReport **ret) {
_cleanup_close_ int inblob_fd = -EBADF;
_cleanup_(tsm_report_freep) TsmReport *report = NULL;
_cleanup_free_ char *floor = NULL;
bool has_privlevel = false, has_floor = false;
unsigned privlevel = 0, privlevel_floor = 0;
int r;
assert(entry_fd >= 0);
assert(report_data);
assert(ret);
r = read_one_line_file_at(entry_fd, "privlevel_floor", &floor);
if (r >= 0) {
r = safe_atou(floor, &privlevel_floor);
if (r < 0)
return log_debug_errno(r, "Failed to parse 'privlevel_floor' attribute: %m");
has_floor = true;
} else if (r != -ENOENT)
return log_debug_errno(r, "Failed to read 'privlevel_floor' attribute: %m");
/* -ENOENT: provider has no privlevel concept, leave it unset. */
if (options && options->privlevel_set) {
if (!has_floor)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
"TSM provider does not support 'privlevel'.");
if (options->privlevel < privlevel_floor)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
"Requested privlevel %u is below the provider's floor %u.",
options->privlevel, privlevel_floor);
privlevel = options->privlevel;
has_privlevel = true;
} else if (has_floor) {
privlevel = privlevel_floor;
has_privlevel = true;
}
/* Snapshot the write-generation counter before touching any input. Note that tracking generations
* is defense in-depth, as we are already operating on a private directory per report request. */
uint64_t generation;
r = read_generation(entry_fd, &generation);
if (r < 0)
return r;
/* Write inputs. Each successful option write bumps the kernel's generation by one. */
if (has_privlevel) {
char privlvl_buf[DECIMAL_STR_MAX(unsigned)];
xsprintf(privlvl_buf, "%u", privlevel);
r = write_string_file_at(entry_fd, "privlevel",
privlvl_buf,
WRITE_STRING_FILE_DISABLE_BUFFER);
if (r < 0)
return log_debug_errno(r, "Failed to write 'privlevel' attribute: %m");
generation++;
}
inblob_fd = openat(entry_fd, "inblob", O_WRONLY|O_CLOEXEC);
if (inblob_fd < 0)
return log_debug_errno(errno, "Failed to open 'inblob' attribute: %m");
r = loop_write(inblob_fd, report_data->iov_base, report_data->iov_len);
if (r < 0)
return log_debug_errno(r, "Failed to write 'inblob' attribute: %m");
inblob_fd = safe_close(inblob_fd); /* configfs commits the buffered write only on close. */
generation++;
/* Read output. */
report = new0(TsmReport, 1);
if (!report)
return log_oom_debug();
r = read_full_file_at(entry_fd, "outblob",
(char**) &report->outblob.iov_base, &report->outblob.iov_len);
if (r < 0)
return log_debug_errno(r, "Failed to read 'outblob' attribute: %m");
r = read_one_line_file_at(entry_fd, "provider", &report->provider);
if (r < 0)
return log_debug_errno(r, "Failed to read 'provider' attribute: %m");
r = read_full_file_at(entry_fd, "auxblob",
(char**) &report->auxblob.iov_base, &report->auxblob.iov_len);
if (r < 0 && r != -ENOENT) /* auxblob is optional */
return log_debug_errno(r, "Failed to read 'auxblob' attribute: %m");
r = read_full_file_at(entry_fd, "manifestblob",
(char**) &report->manifestblob.iov_base, &report->manifestblob.iov_len);
if (r < 0 && r != -ENOENT) /* manifestblob is optional */
return log_debug_errno(r, "Failed to read 'manifestblob' attribute: %m");
uint64_t generation_now;
r = read_generation(entry_fd, &generation_now);
if (r < 0)
return r;
if (generation_now != generation)
return log_debug_errno(SYNTHETIC_ERRNO(EBUSY),
"TSM report generation changed during acquisition (%" PRIu64 " != %" PRIu64 "), concurrent access?",
generation_now, generation);
*ret = TAKE_PTR(report);
return 0;
}
int tsm_report_acquire(
const struct iovec *report_data,
const TsmReportOptions *options,
TsmReport **ret) {
_cleanup_close_ int report_fd = -EBADF, entry_fd = -EBADF;
_cleanup_free_ char *name = NULL;
sd_id128_t rnd;
int r;
assert(ret);
if (!report_data || !iovec_is_set(report_data))
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Report data can't be empty");
if (report_data->iov_len != TSM_REPORT_DATA_SIZE)
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Report data must be %u bytes.",
TSM_REPORT_DATA_SIZE);
report_fd = open(TSM_REPORT_PATH, O_DIRECTORY|O_CLOEXEC|O_RDONLY);
if (report_fd < 0) {
if (errno == ENOENT)
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"configfs-tsm interface not available at " TSM_REPORT_PATH ".");
return log_debug_errno(errno, "Failed to open " TSM_REPORT_PATH ": %m");
}
/* Private, unique entry name so we don't race with other callers.
* PID is included for attribution. */
r = sd_id128_randomize(&rnd);
if (r < 0)
return log_debug_errno(r, "Failed to generate report entry name: %m");
r = asprintf(&name, "systemd-report-" PID_FMT "-%s", getpid_cached(), SD_ID128_TO_STRING(rnd));
if (r < 0)
return log_oom_debug();
entry_fd = open_mkdir_at(report_fd, name, O_EXCL|O_RDONLY|O_CLOEXEC, 0700);
if (entry_fd < 0)
return log_debug_errno(entry_fd, "Failed to create TSM report entry: %m");
r = tsm_report_fill(entry_fd, report_data, options, ret);
/* Remove the entry regardless of success/failure. */
if (unlinkat(report_fd, name, AT_REMOVEDIR) < 0)
log_debug_errno(errno, "Failed to remove TSM report entry '%s', ignoring: %m", name);
return r;
}
+38
View File
@@ -0,0 +1,38 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <sys/uio.h>
#include "shared-forward.h"
#define TSM_REPORT_DATA_SIZE 64U
/* Optional knobs. Mirrors the write-only attributes other than inblob.
* Zero-initialize ({}) and set only what you need. NULL options == all defaults. */
typedef struct TsmReportOptions {
unsigned privlevel; /* e.g. SEV-SNP VMPL */
bool privlevel_set; /* privlevel_floor is used when unset */
/* service_provider currently not supported. */
} TsmReportOptions;
/* Result. Mirrors the read-only attributes. */
typedef struct TsmReport {
char *provider; /* e.g. "sev_guest", "tdx_guest" */
struct iovec outblob; /* the attestation report */
struct iovec auxblob; /* optional, unset if empty (e.g. SEV cert_table) */
struct iovec manifestblob; /* optional, unset if empty */
} TsmReport;
TsmReport *tsm_report_free(TsmReport *report);
DEFINE_TRIVIAL_CLEANUP_FUNC(TsmReport*, tsm_report_free);
/* Acquire an attestation report via configfs-tsm.
* report_data: mandatory inblob to include in the report, TSM_REPORT_DATA_SIZE bytes
* options: optional, NULL for defaults
* ret: result, freed with tsm_report_free()
* Returns -EOPNOTSUPP if the configfs-tsm interface is absent, -ENXIO if it is present
* but no provider is registered. Other negative errnos are real failures. */
int tsm_report_acquire(
const struct iovec *report_data,
const TsmReportOptions *options,
TsmReport **ret);
+2
View File
@@ -747,6 +747,8 @@ units = [
'file' : 'systemd-report-sign-plain@.service.in',
'conditions' : ['HAVE_OPENSSL'],
},
{ 'file' : 'systemd-report-sign-tsm.socket' },
{ 'file' : 'systemd-report-sign-tsm@.service.in' },
{
'file' : 'systemd-resolved.service.in',
'conditions' : ['ENABLE_RESOLVE'],
+27
View File
@@ -0,0 +1,27 @@
# 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=Report Signing Socket (configfs-tsm)
Documentation=man:systemd-report-sign-tsm@.service(8)
DefaultDependencies=no
Before=sockets.target shutdown.target
Conflicts=shutdown.target
ConditionSecurity=cvm
[Socket]
ListenStream=/run/systemd/report.sign/tsm
FileDescriptorName=varlink
SocketMode=0600
Accept=yes
MaxConnectionsPerSource=16
RemoveOnStop=yes
[Install]
WantedBy=sockets.target
+46
View File
@@ -0,0 +1,46 @@
# 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=Report Signing Service (configfs-tsm)
Documentation=man:systemd-report-sign-tsm@.service(8)
DefaultDependencies=no
Before=shutdown.target
Conflicts=shutdown.target
Wants=sys-kernel-config.mount
After=sys-kernel-config.mount
[Service]
ExecStart={{LIBEXECDIR}}/systemd-report-sign-tsm
CapabilityBoundingSet=
DeviceAllow=
IPAddressDeny=any
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
PrivateDevices=yes
PrivateIPC=yes
PrivateNetwork=yes
PrivateTmp=disconnected
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectSystem=strict
ReadWritePaths=-/sys/kernel/config/tsm/report
RestrictAddressFamilies=AF_UNIX
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RuntimeMaxSec=5min
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service