report: add a simple software-backed signing backend

This commit is contained in:
Lennart Poettering
2026-06-19 05:23:18 +02:00
parent 3c2f7c6002
commit 2640a3b0e2
9 changed files with 550 additions and 0 deletions
+5
View File
@@ -1185,6 +1185,11 @@ manpages = [
['systemd-remount-fs.service', '8', ['systemd-remount-fs'], ''],
['systemd-repart', '8', ['systemd-repart.service'], 'ENABLE_REPART'],
['systemd-report', '1', [], ''],
['systemd-report-sign-plain@.service',
'8',
['systemd-report-sign-plain',
'systemd-report-sign-plain.socket'],
'HAVE_OPENSSL'],
['systemd-resolved.service', '8', ['systemd-resolved'], 'ENABLE_RESOLVE'],
['systemd-rfkill.service',
'8',
@@ -0,0 +1,66 @@
<?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-plain_.service" conditional='HAVE_OPENSSL'
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>systemd-report-sign-plain@.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-report-sign-plain@.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-report-sign-plain@.service</refname>
<refname>systemd-report-sign-plain.socket</refname>
<refname>systemd-report-sign-plain</refname>
<refpurpose>Sign system reports with a local software key</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-report-sign-plain@.service</filename></para>
<para><filename>systemd-report-sign-plain.socket</filename></para>
<para><filename>/usr/lib/systemd/systemd-report-sign-plain</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-report-sign-plain@.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>plain</filename>.</para>
<para>The service is socket-activated (one instance per connection) via
<filename>systemd-report-sign-plain.socket</filename>, and signs the digest passed to it with a local
software key. Signatures are made with the Ed25519 (EdDSA) algorithm.</para>
<para>On first use a key pair is generated automatically and stored below
<filename>/var/lib/systemd/report.sign.plain/</filename>: the private key in
<filename>local.private</filename> (PKCS#8 PEM, readable only by the root user) and the public key in
<filename>local.public</filename> (PEM, world readable).</para>
<para>The public key is also exported as a system report metric.</para>
</refsect1>
<!-- Note: we do not document the command line switches here. The systemd-report-sign-plain 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
@@ -33,6 +33,7 @@ enable systemd-nsresourced.socket
enable systemd-pstore.service
enable systemd-report-basic.socket
enable systemd-report-cgroup.socket
enable systemd-report-sign-plain.socket
enable systemd-resolved.service
enable systemd-sysext.service
enable systemd-timesyncd.service
+10
View File
@@ -27,4 +27,14 @@ executables += [
'report-cgroup-server.c',
),
},
libexec_template + {
'name' : 'systemd-report-sign-plain',
'conditions' : [
'HAVE_OPENSSL',
],
'sources' : files(
'report-sign-plain.c',
),
'dependencies' : libopenssl_cflags,
},
]
+403
View File
@@ -0,0 +1,403 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/file.h>
#include <unistd.h>
#include "sd-json.h"
#include "sd-varlink.h"
#include "alloc-util.h"
#include "build.h"
#include "crypto-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-table.h"
#include "fs-util.h"
#include "help-util.h"
#include "iovec-util.h"
#include "json-util.h"
#include "lock-util.h"
#include "log.h"
#include "main-func.h"
#include "metrics.h"
#include "options.h"
#include "stat-util.h"
#include "tmpfile-util.h"
#include "varlink-io.systemd.Metrics.h"
#include "varlink-io.systemd.Report.Signer.h"
#include "varlink-util.h"
#define REPORT_SIGN_PLAIN_DIR "/var/lib/systemd/report.sign.plain"
#define REPORT_SIGN_PLAIN_PRIVATE_KEY "local.private"
#define REPORT_SIGN_PLAIN_PUBLIC_KEY "local.public"
#define METRIC_IO_SYSTEMD_SIGN_PLAIN_PREFIX "io.systemd.Report.SignPlain."
static int load_key(int dir_fd, EVP_PKEY **ret) {
_cleanup_fclose_ FILE *f = NULL;
struct stat st;
int r;
assert(dir_fd >= 0);
assert(ret);
r = xfopenat(dir_fd, REPORT_SIGN_PLAIN_PRIVATE_KEY, "re", /* open_flags= */ 0, &f);
if (r == -ENOENT) {
*ret = NULL;
return 0;
}
if (r < 0)
return log_error_errno(r, "Failed to open private key file '%s/%s': %m",
REPORT_SIGN_PLAIN_DIR, REPORT_SIGN_PLAIN_PRIVATE_KEY);
if (fstat(fileno(f), &st) < 0)
return log_error_errno(errno, "Failed to stat private key file: %m");
r = stat_verify_regular(&st);
if (r < 0)
return log_error_errno(r, "Private key file is not a regular file: %m");
if (st.st_uid != 0 || (st.st_mode & 0077) != 0)
return log_error_errno(SYNTHETIC_ERRNO(EPERM),
"Private key file '%s/%s' is accessible by more than the root user, refusing.",
REPORT_SIGN_PLAIN_DIR, REPORT_SIGN_PLAIN_PRIVATE_KEY);
_cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = sym_PEM_read_PrivateKey(f, /* x= */ NULL, /* cb= */ NULL, /* u= */ NULL);
if (!pkey)
return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to load private key from '%s/%s'.",
REPORT_SIGN_PLAIN_DIR, REPORT_SIGN_PLAIN_PRIVATE_KEY);
log_debug("Successfully loaded private key from '%s/%s'.", REPORT_SIGN_PLAIN_DIR, REPORT_SIGN_PLAIN_PRIVATE_KEY);
*ret = TAKE_PTR(pkey);
return 1;
}
static int write_key_file(int dir_fd, EVP_PKEY *pkey, bool private_key, const char *name, mode_t mode) {
_cleanup_fclose_ FILE *f = NULL;
_cleanup_free_ char *temp = NULL;
int r;
assert(dir_fd >= 0);
assert(pkey);
assert(name);
r = fopen_temporary_at(dir_fd, name, &f, &temp);
if (r < 0)
return log_error_errno(r, "Failed to open key file '%s/%s' for writing: %m", REPORT_SIGN_PLAIN_DIR, name);
CLEANUP_TMPFILE_AT(dir_fd, temp);
if (private_key)
r = sym_PEM_write_PrivateKey(f, pkey, /* enc= */ NULL, /* kstr= */ NULL, /* klen= */ 0, /* cb= */ NULL, /* u= */ NULL);
else
r = sym_PEM_write_PUBKEY(f, pkey);
if (r <= 0)
return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to write %s key.", private_key ? "private" : "public");
if (fchmod(fileno(f), mode) < 0)
return log_error_errno(errno, "Failed to adjust key file access mode: %m");
r = fflush_sync_and_check(f);
if (r < 0)
return log_error_errno(r, "Failed to write key file: %m");
f = safe_fclose(f);
/* Never overwrite an existing private key: we must not destroy it. The public key is derived from the
* private one and carries no secret, hence may be refreshed unconditionally. */
if (private_key)
r = rename_noreplace(dir_fd, temp, dir_fd, name);
else
r = RET_NERRNO(renameat(dir_fd, temp, dir_fd, name));
if (r < 0) {
if (private_key && r == -EEXIST)
return log_error_errno(r, "Private key file '%s/%s' appeared while generating a new one, refusing to overwrite it.",
REPORT_SIGN_PLAIN_DIR, name);
return log_error_errno(r, "Failed to move key file '%s/%s' into place: %m", REPORT_SIGN_PLAIN_DIR, name);
}
return 0;
}
static int generate_key(int dir_fd, EVP_PKEY **ret) {
int r;
assert(dir_fd >= 0);
assert(ret);
/* Generate a new Ed25519 key pair. */
_cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = sym_EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, /* e= */ NULL);
if (!ctx)
return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to allocate Ed25519 key generation context.");
if (sym_EVP_PKEY_keygen_init(ctx) <= 0)
return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to initialize Ed25519 key generation context.");
log_info("Generating Ed25519 key pair for signing reports.");
_cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
if (sym_EVP_PKEY_keygen(ctx, &pkey) <= 0)
return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to generate Ed25519 key pair.");
log_info("Successfully generated Ed25519 key pair.");
/* Write out the private key (PKCS#8 PEM; this includes the public key too), readable by root only. */
r = write_key_file(dir_fd, pkey, /* private_key= */ true, REPORT_SIGN_PLAIN_PRIVATE_KEY, 0400);
if (r < 0)
return r;
/* Write out the public key too, as a convenience for verification, world readable. */
r = write_key_file(dir_fd, pkey, /* private_key= */ false, REPORT_SIGN_PLAIN_PUBLIC_KEY, 0444);
if (r < 0)
return r;
r = RET_NERRNO(fsync(dir_fd));
if (r < 0)
return log_error_errno(r, "Failed to sync directory '%s': %m", REPORT_SIGN_PLAIN_DIR);
*ret = TAKE_PTR(pkey);
return 1;
}
static int acquire_key(EVP_PKEY **ret) {
int r;
assert(ret);
/* Open (creating if necessary) and exclusively lock the key directory, so that loading/generating the
* key is safe against parallel invocations (we are spawned once per connection). The directory is
* created with mode 0700, so that nobody else can pre-create it with looser permissions. */
_cleanup_close_ int dir_fd = xopenat_lock_full(
AT_FDCWD,
REPORT_SIGN_PLAIN_DIR,
O_CLOEXEC|O_DIRECTORY|O_CREAT,
/* xopen_flags= */ 0,
/* mode= */ 0700,
LOCK_BSD,
LOCK_EX);
if (dir_fd < 0)
return log_error_errno(dir_fd, "Failed to open and lock directory '%s': %m", REPORT_SIGN_PLAIN_DIR);
/* First try to load the key off disk. If loading fails for any reason other than the key not
* existing yet (e.g. it is malformed or has bad permissions) we propagate the error and refuse to
* touch the existing file. Only if the key is genuinely missing do we generate a fresh one. */
r = load_key(dir_fd, ret);
if (r != 0)
return r;
return generate_key(dir_fd, ret);
}
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, 0, SD_JSON_MANDATORY },
{ "algorithm", SD_JSON_VARIANT_STRING, NULL, 0, SD_JSON_MANDATORY },
{}
};
_cleanup_(iovec_done) struct iovec digest = {};
int r;
assert(link);
assert(parameters);
/* Signing is privileged: we share the Varlink server (and possibly the socket) with the unprivileged
* metrics interface, so we cannot rely on a server-wide access check and must gate this method
* ourselves. Only root (i.e. ourselves) may request signatures. */
r = varlink_check_privileged_peer(link);
if (r < 0)
return r;
r = sd_varlink_dispatch(link, parameters, dispatch_table, &digest);
if (r != 0)
return r;
if (!iovec_is_set(&digest))
return sd_varlink_error_invalid_parameter_name(link, "digest");
_cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
r = acquire_key(&pkey);
if (r < 0)
return r;
/* Sign the provided digest directly. We pass a NULL message digest algorithm, which for a suitable
* signing algorithm such as Ed25519 means the input bytes are signed as-is, without hashing them
* first. The caller already passes us a digest, hence we must not hash it again. */
_cleanup_(iovec_done) struct iovec signature = {};
r = digest_and_sign(/* md= */ NULL, pkey, digest.iov_base, digest.iov_len, &signature.iov_base, &signature.iov_len);
if (r < 0)
return log_error_errno(r, "Failed to sign digest: %m");
_cleanup_free_ char *pubkey = NULL;
r = openssl_pubkey_to_pem(pkey, &pubkey);
if (r < 0)
return log_error_errno(r, "Failed to extract public key: %m");
/* Return a single signature object, carrying the raw Ed25519 (EdDSA) signature and the public key it
* was made with, in PEM form. */
return sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR(
"data",
SD_JSON_BUILD_ARRAY(
SD_JSON_BUILD_OBJECT(
JSON_BUILD_PAIR_IOVEC_BASE64("signature", &signature),
SD_JSON_BUILD_PAIR_STRING("key", pubkey)))));
}
static int public_key_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) {
int r;
assert(mf && mf->name);
assert(link);
/* Expose the public key the signer uses also as metrics. This has the nice effect that the signature
* will cover the public key, in the sense of a self-signed certificate. We acquire it through the
* very same logic as the Sign() method, i.e. we load it off disk, or generate it on first use if it
* is still missing. */
_cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
r = acquire_key(&pkey);
if (r < 0)
return r;
_cleanup_free_ char *pubkey = NULL;
r = openssl_pubkey_to_pem(pkey, &pubkey);
if (r < 0)
return log_error_errno(r, "Failed to extract public key: %m");
return metric_build_send_string(
mf,
link,
/* object= */ NULL,
pubkey,
/* fields= */ NULL);
}
static const MetricFamily metric_family_table[] = {
{
METRIC_IO_SYSTEMD_SIGN_PLAIN_PREFIX "PublicKey",
"PEM-encoded public key used to sign system reports with the local software key",
METRIC_FAMILY_TYPE_STRING,
.generate = public_key_generate,
},
{}
};
static int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return metrics_method_list(metric_family_table, link, parameters, flags, userdata);
}
static int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
return metrics_method_describe(metric_family_table, link, parameters, flags, userdata);
}
static int vl_server(void) {
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL;
int r;
r = DLOPEN_LIBCRYPTO(LOG_ERR, SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED);
if (r < 0)
return r;
/* No server-wide access restriction: the metrics interface is meant to be reachable unprivileged,
* while the signing method enforces root access on its own (see vl_method_sign()). */
r = varlink_server_new(&vs, /* flags= */ 0, /* userdata= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
/* We serve two interfaces off the same server: the signing API and a metrics provider that exposes our
* public key. Either is reachable regardless of which socket the connection arrived on. */
r = sd_varlink_server_add_interface_many(
vs,
&vl_interface_io_systemd_Report_Signer,
&vl_interface_io_systemd_Metrics);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interfaces: %m");
r = sd_varlink_server_bind_method_many(
vs,
"io.systemd.Report.Signer.Sign", vl_method_sign,
"io.systemd.Metrics.List", vl_method_list_metrics,
"io.systemd.Metrics.Describe", vl_method_describe_metrics);
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("Sign system reports with a local software key.");
help_section("Options");
r = table_print_or_warn(options);
if (r < 0)
return r;
help_man_page_reference("systemd-report-sign-plain@.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);
+3
View File
@@ -1615,6 +1615,9 @@ int digest_and_sign(
if (!mdctx)
return log_openssl_errors("Failed to create new EVP_MD_CTX");
/* Note that a NULL 'md' (message digest algorithm) means to sign the provided data directly, without
* hashing it first, as long as a suitable signing algorithm is used that supports this, such as
* Ed25519 (PureEdDSA). For such algorithms callers may pass an already calculated digest as input. */
if (sym_EVP_DigestSignInit(mdctx, NULL, md, NULL, privkey) != 1) {
/* Distro security policies often disable support for SHA-1. Let's return a recognizable
* error for that case. */
+8
View File
@@ -731,6 +731,14 @@ units = [
},
{ 'file' : 'systemd-report-cgroup.socket' },
{ 'file' : 'systemd-report-cgroup@.service.in' },
{
'file' : 'systemd-report-sign-plain.socket',
'conditions' : ['HAVE_OPENSSL'],
},
{
'file' : 'systemd-report-sign-plain@.service.in',
'conditions' : ['HAVE_OPENSSL'],
},
{
'file' : 'systemd-resolved.service.in',
'conditions' : ['ENABLE_RESOLVE'],
+33
View File
@@ -0,0 +1,33 @@
# 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 (Local Software Key)
Documentation=man:systemd-report-sign-plain@.service(8)
DefaultDependencies=no
Before=sockets.target shutdown.target
Conflicts=shutdown.target
WantsMountsFor=/var/lib/systemd/report.sign.plain
# The same binary serves two interfaces off one socket: an unprivileged metrics provider exposing our public
# key, and the privileged signing API. The actual socket node hence lives in the world-accessible metrics
# directory /run/systemd/report/, and the signing entrypoint is a symlink into the root-only
# /run/systemd/report.sign/ directory. Access to the signing method itself is restricted to root by the
# service (see vl_method_sign()); the directory permissions are merely an additional barrier.
[Socket]
ListenStream=/run/systemd/report/io.systemd.Report.SignPlain
Symlinks=/run/systemd/report.sign/plain
FileDescriptorName=varlink
SocketMode=0666
Accept=yes
MaxConnectionsPerSource=16
RemoveOnStop=yes
[Install]
WantedBy=sockets.target
@@ -0,0 +1,21 @@
# 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 (Local Software Key)
Documentation=man:systemd-report-sign-plain@.service(8)
DefaultDependencies=no
Conflicts=shutdown.target
Before=shutdown.target
WantsMountsFor=/var/lib/systemd/report.sign.plain
[Service]
StateDirectory=systemd/report.sign.plain
StateDirectoryMode=0700
ExecStart={{LIBEXECDIR}}/systemd-report-sign-plain