diff --git a/man/rules/meson.build b/man/rules/meson.build index a3d8500cc6..b37eb5f19f 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -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', diff --git a/man/systemd-report-sign-tsm@.service.xml b/man/systemd-report-sign-tsm@.service.xml new file mode 100644 index 0000000000..61d67b24a9 --- /dev/null +++ b/man/systemd-report-sign-tsm@.service.xml @@ -0,0 +1,73 @@ + + + + + + + + systemd-report-sign-tsm@.service + systemd + + + + systemd-report-sign-tsm@.service + 8 + + + + systemd-report-sign-tsm@.service + systemd-report-sign-tsm.socket + systemd-report-sign-tsm + Sign system reports with a confidential-computing attestation report + + + + systemd-report-sign-tsm@.service + systemd-report-sign-tsm.socket + /usr/lib/systemd/systemd-report-sign-tsm + + + + Description + + systemd-report-sign-tsm@.service is a system service that signs system reports + generated by + systemd-report1. It is + a signing backend for the logic of that tool: it implements the + io.systemd.Report.Signer.Sign() Varlink method and is reached via a socket linked into + the /run/systemd/report.sign/ directory, named tsm. + + The service is socket-activated (one instance per connection) via + systemd-report-sign-tsm.socket. 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 /sys/kernel/config/tsm/report/. The digest passed to it is + embedded into the report as its run-time provided data (inblob), cryptographically + binding the attestation report to the system report being signed. + + The returned signature carries the binary attestation report (the outblob), the + name of the TSM provider that generated it (for example sev_guest for AMD SEV-SNP or + tdx_guest for Intel TDX), and, where the provider supplies them, supplemental + certificate (auxblob) and manifest (manifestblob) data. A verifier + selects the appropriate validation logic based on the provider field. + + 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. + + + + + + See Also + + systemd1 + systemd-report1 + + + + diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset index a00643f684..005280cd94 100644 --- a/presets/90-systemd.preset +++ b/presets/90-systemd.preset @@ -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 diff --git a/src/report/meson.build b/src/report/meson.build index c3e7cf959e..d0a316c689 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -44,4 +44,10 @@ executables += [ ), 'dependencies' : libopenssl_cflags, }, + libexec_template + { + 'name' : 'systemd-report-sign-tsm', + 'sources' : files( + 'report-sign-tsm.c', + ), + }, ] diff --git a/src/report/report-sign-tsm.c b/src/report/report-sign-tsm.c new file mode 100644 index 0000000000..0929fbbc0b --- /dev/null +++ b/src/report/report-sign-tsm.c @@ -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); diff --git a/src/shared/meson.build b/src/shared/meson.build index a3684ade1e..8e874cb99d 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -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', diff --git a/src/shared/tsm-report.c b/src/shared/tsm-report.c new file mode 100644 index 0000000000..d10eb7fdbf --- /dev/null +++ b/src/shared/tsm-report.c @@ -0,0 +1,214 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include + +#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; +} diff --git a/src/shared/tsm-report.h b/src/shared/tsm-report.h new file mode 100644 index 0000000000..6b7d63ac60 --- /dev/null +++ b/src/shared/tsm-report.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#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); diff --git a/units/meson.build b/units/meson.build index 47c48bcabc..ea59dac424 100644 --- a/units/meson.build +++ b/units/meson.build @@ -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'], diff --git a/units/systemd-report-sign-tsm.socket b/units/systemd-report-sign-tsm.socket new file mode 100644 index 0000000000..1756dde8cf --- /dev/null +++ b/units/systemd-report-sign-tsm.socket @@ -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 diff --git a/units/systemd-report-sign-tsm@.service.in b/units/systemd-report-sign-tsm@.service.in new file mode 100644 index 0000000000..6e2f4805dc --- /dev/null +++ b/units/systemd-report-sign-tsm@.service.in @@ -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