report: add support for signing reports via varlink backends + make report a varlink service (#42595)

This adds the following:

1. systemd-report gains a new --sign= option, taking a boolean. If true,
this makes systemd-report generate + systemd-report upload generate a
signed report, instead of a regular one. The signatures are collected
from Varlink-based backends.
2. One such backend is added which does a simple Ed21159 based signing
scheme.
3. this adds a new metrics source which just reports text files
symlinked into a special dir as metrics. This is used to report the
Ed21159 public key as metric, by default, if it exists.
4. finally, systemd-report itself is turned into a varlink service. this
is useful for example for extracting a report from a system coming in
via the varlink/http bridge.

I thought a long time about the format of signing of reports. Initially
i intended to do this like homed's user record signing, i.e. require
normalization of the record, then normalize the record, and write it out
in dense form, since the result. Finally insert the resulting hash into
the user record itself. People have pointed me to the inherent messiness
of signing JSON this way though, as it requires any participant that
wishes to sign/authenticate records this way to implement the exact same
normalization/formatting rules, and in particular in the area of
floating point numbers (of which metrics presumably will have many) this
is quite problematic.

This signing hence goes a different way. instead of expecting
signer+verifier to independently come to the same normalized text form
of the json data, let's instead output a JSON-SEQ sequence, where the
first object is the report, and any subsequent objects are one signature
each. the signatures are supposed to cover the precise binary
representation of the first element in the JSON-SEQ stream. (i.e. from
the RS to the NL).

or in other words: a verifier would receive the JSON-SEQ stream, split
it up before each RS. Then it would leave object 1 unparsed for the
moment, and parse objects 2…n. It would then authenticate object 1's
precise binary representation with objects 2…n. Once that checks out, it
would parse object 1, and use it as report.
This commit is contained in:
Lennart Poettering
2026-06-19 20:37:25 +02:00
committed by GitHub
38 changed files with 1846 additions and 174 deletions
+27 -65
View File
@@ -128,17 +128,36 @@ SPDX-License-Identifier: LGPL-2.1-or-later
## Features
- **report:**
- implement signer for TPM2 that adds a quote + event log excerpt as signing
object. should include a TPM timestamp, and some "generation ID" provided
by an orchestrator to guarantee freshness.
- implement metrics provider in logind: report number of active
sessions, and number of sessions since boot.
- implement metrics provider in journald that reports number of log messages
received since boot, by log priority
- implement metrics provider in journalctl, that simply reports the most
recent 10 emergency log msgs on the system
- allow to compile statically (together with the basic and cgroup
backends)
- make sure backends can also be invoked via forking off
- allow metrics providers to indicate which reported values mean
"nothing"/"invalid"/"zero"/"please-suppress". Then use that to reduce noise
in systemd-report output.
- teach cgroup metrics provider to expose PSI information
- implement metrics provider that reports local IP addresses, and bound open
IP ports
- implement current system load via metrics
- metrics from pid1: suppress metrics form units that are inactive and have nothing to report
- pass filtering hints to services, so that they can also be applied server-side, not just client side
- add "hint-suppress-zero" flag (which suppresses all metrics which are zero)
- add "hint-object" parameter (which only queries info about certain object)
- make systemd-report a varlink service
- bootctl set-tries for setting retry counters on boot entries
- report: allow to compile statically (together with the basic and cgroup
backends)
- report: make sure backends can also be invoked via forking off
- report: backend that extracts 10 most recent log msgs of a certain priority
- implement enough of PCP in a new sd-pcp-client library that networkd can use
to punch holes for wireguard into common NAT routers.
to punch holes for wireguard into common NAT routers. use that in networkd. alternatively: just use libjuice
- measure an uapi16 manifest of /etc/ during early boot (so that
pre-initialized /etc/ can be detected when systems are enrolled into some
@@ -223,8 +242,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
traffic of all sockets marked via the new varlink socket xattrs. Use BPF for
all of that of course.
- systemd-report: implement signing via callout varlink dir
- add tooling for generating dictionary-based hostnames
- do not pull dbus daemon/broker anymore, instead lazy activate it. Given how
@@ -237,12 +254,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
hence we might as well associate the title with the table itself, and
streamline a few things.
- allow metrics to indicate which values mean
"nothing"/"invalid"/"zero"/"please-suppress". Then use that to reduce noise
in systemd-report output.
- cgroup-metrics: add per-cgroup PSI metrics
- sysupdate: offer reading transfer files/components/features optionally from
some JSON fragment rather than transfer files, so that we can update it
independently from any DDI, and it needs no activation cycle. Why? so that
@@ -319,16 +330,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
retriggers the fs is was invoked for, which causes the udev rules to rerun
that assemble the btrfs raid, but this time force degraded assembly.
- add a report backend that simply exposes a bunch of static files that are
symlinked to some dir {/run,/etc/,/var/lib/}systemd/report-files/ or so as
facts. Use that for exposing SSH keys and suchlike.
- report generators for:
- ip addresses
- imds address
- tpm event log
- open IP ports
- a way for container managers to turn off getty starting via $container_headless= or so...
- add "conditions" for bls type 1 and type 2 profiles that allow suppressing
@@ -889,15 +890,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- ddi must be listed as block device fstype
- define a generic "report" varlink interface, which services can implement to
provide health/statistics data about themselves. then define a dir somewhere
in /run/ where components can bind such sockets. Then make journald, logind,
and pid1 itself implement this and expose various stats on things there. Then
issue parallel calls to these interfaces from the systemd-report tool,
combine into one json document, and include measurement logs and tpm
quote. tpm quote should protect the json doc via the nonce field
studd. Allow shipping this off elsewhere for analyze.
- define a JSON format for units, separating out unit definitions from unit
runtime state. Then, expose it:
@@ -1667,28 +1659,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- man: the documentation of Restart= currently is very misleading and suggests the tools from ExecStartPre= might get restarted.
- maybe add a "systemd-report" tool, that generates a TPM2-backed "report" of
current system state, i.e. a combination of PCR information, local system
time and TPM clock, running services, recent high-priority log
messages/coredumps, system load/PSI, signed by the local TPM chip, to form an
enhanced remote attestation quote. Use case: a simple orchestrator could use
this: have the report tool upload these reports every 3min somewhere. Then
have the orchestrator collect these reports centrally over a 3min time
window, and use them to determine what which node should now start/stop what,
and generate a small confext for each node, that uses Uphold= to pin services
on each node. The confext would be encrypted using the asymmetric encryption
proposed above, so that it can only be activated on the specific host, if the
software is in a good state, and within a specific time frame. Then run a
loop on each node that sends report to orchestrator and then sysupdate to
update confext. Orchestrator would be stateless, i.e. operate on desired
config and collected reports in the last 3min time window only, and thus can
be trivially scaled up since all instances of the orchestrator should come to
the same conclusions given the same inputs of reports/desired workload info.
Could also be used to deliver Wireguard secrets and thus to clients, thus
permitting zero-trust networking: secrets are rolled over via confext updates,
and via the time window TPM logic invalidated if node doesn't keep itself
updated, or becomes corrupted in some way.
- maybe add a new standard slice where process that are started in the initrd
and stick around for the whole system runtime (i.e. root fs storage daemons,
the bpf loader daemon discussed above, and such) are placed. maybe
@@ -2241,14 +2211,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- report: have something that requests cloud workload identity bearer tokens
and includes it in the report
- **report:**
- plug "facts" into systemd-report too, i.e. stuff that is more static, such as hostnames, ssh keys and so on.
- pass filtering hints to services, so that they can also be applied server-side, not just client side
- metrics from pid1: suppress metrics form units that are inactive and have nothing to report
- add "hint-suppress-zero" flag (which suppresses all metrics which are zero)
- add "hint-object" parameter (which only queries info about certain object)
- make systemd-report a varlink service
- Reset TPM2 DA bit on each successful boot
- **resolved:**
+10
View File
@@ -1185,6 +1185,16 @@ manpages = [
['systemd-remount-fs.service', '8', ['systemd-remount-fs'], ''],
['systemd-repart', '8', ['systemd-repart.service'], 'ENABLE_REPART'],
['systemd-report', '1', [], ''],
['systemd-report-files@.service',
'8',
['systemd-report-files',
'systemd-report-files.socket'],
''],
['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',
+72
View File
@@ -0,0 +1,72 @@
<?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-files_.service" xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>systemd-report-files@.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-report-files@.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-report-files@.service</refname>
<refname>systemd-report-files.socket</refname>
<refname>systemd-report-files</refname>
<refpurpose>Report the contents of files as system report metrics</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-report-files@.service</filename></para>
<para><filename>systemd-report-files.socket</filename></para>
<para><filename>/usr/lib/systemd/systemd-report-files</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-report-files@.service</command> is a metrics source for
<citerefentry><refentrytitle>systemd-report</refentrytitle><manvolnum>1</manvolnum></citerefentry>. It
reports the contents of a configurable set of files as metrics, by implementing the
<constant>io.systemd.Metrics</constant> Varlink interface on a socket linked into the
<filename>/run/systemd/report/</filename> directory.</para>
<para>The files to report are picked up from <filename>report.files/</filename> subdirectories of the
usual configuration directories, i.e. from
<filename>/etc/systemd/report.files/</filename>,
<filename>/run/systemd/report.files/</filename>,
<filename>/var/lib/systemd/report.files/</filename>,
<filename>/usr/local/lib/systemd/report.files/</filename> and
<filename>/usr/lib/systemd/report.files/</filename>. Typically these directories contain symlinks pointing
to the actual files in the file system whose contents shall be reported. Entries with the same name in an
earlier directory override those in a later one.</para>
<para>Each entry is reported as a metric named
<literal>io.systemd.Files.<replaceable>NAME</replaceable></literal>, where
<replaceable>NAME</replaceable> is the name of the entry (which must be a valid metric field name). The
metric value is the verbatim contents of the file. Files that cannot be read (for example because a
symlink is dangling) are skipped, as are files whose contents are not valid UTF-8 text.</para>
<para>Note that access to this service is unprivileged, but it runs privileged. This means take care when
symlinking files into the various <filename>report.files/</filename> directories: this service might
provide read access even if the client does not have the required direct access rights itself.</para>
</refsect1>
<!-- Note: we do not document the command line switches here. The systemd-report-files binary itself should
not be invoked by users, but only as a service. Use systemd-report(1) to acquire metrics. -->
<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>
@@ -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>
+13
View File
@@ -197,6 +197,19 @@
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--sign=<replaceable>BOOL</replaceable></option></term>
<listitem><para>If enabled, the report generated by <command>generate</command> or uploaded by
<command>upload</command> is cryptographically signed. In this mode the report is emitted as a
JSON-SEQ stream: the report object comes first, followed by one or more signature objects that
cover the precise binary representation of the report object. Signatures are acquired by calling
<function>io.systemd.Report.Signer.Sign()</function> on any sockets found under
<filename>/run/systemd/report.sign/</filename>. Defaults to off.</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
+3
View File
@@ -33,6 +33,9 @@ enable systemd-nsresourced.socket
enable systemd-pstore.service
enable systemd-report-basic.socket
enable systemd-report-cgroup.socket
enable systemd-report-files.socket
enable systemd-report-sign-plain.socket
enable systemd-report.socket
enable systemd-resolved.service
enable systemd-sysext.service
enable systemd-timesyncd.service
@@ -43,6 +43,9 @@
#include "varlink-io.systemd.PCRExtend.h"
#include "varlink-io.systemd.PCRLock.h"
#include "varlink-io.systemd.Repart.h"
#include "varlink-io.systemd.Report.h"
#include "varlink-io.systemd.Report.Signer.h"
#include "varlink-io.systemd.Report.Uploader.h"
#include "varlink-io.systemd.Resolve.h"
#include "varlink-io.systemd.Resolve.Hook.h"
#include "varlink-io.systemd.Resolve.Monitor.h"
@@ -216,6 +219,9 @@ TEST(parse_format) {
&vl_interface_io_systemd_PCRExtend,
&vl_interface_io_systemd_PCRLock,
&vl_interface_io_systemd_Repart,
&vl_interface_io_systemd_Report,
&vl_interface_io_systemd_Report_Signer,
&vl_interface_io_systemd_Report_Uploader,
&vl_interface_io_systemd_Resolve,
&vl_interface_io_systemd_Resolve_Hook,
&vl_interface_io_systemd_Resolve_Monitor,
+1 -1
View File
@@ -282,7 +282,7 @@ ssize_t varlink_execute_directory(
FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) {
struct dirent *de = *dp;
if (de->d_type != DT_SOCK)
if (!IN_SET(de->d_type, DT_SOCK, DT_LNK))
continue;
t++;
+19
View File
@@ -6,6 +6,8 @@ executables += [
'public' : true,
'sources' : files(
'report.c',
'report-generate.c',
'report-sign.c',
'report-upload.c',
),
},
@@ -25,4 +27,21 @@ executables += [
'report-cgroup-server.c',
),
},
libexec_template + {
'name' : 'systemd-report-files',
'sources' : files(
'report-files.c',
'report-files-server.c',
),
},
libexec_template + {
'name' : 'systemd-report-sign-plain',
'conditions' : [
'HAVE_OPENSSL',
],
'sources' : files(
'report-sign-plain.c',
),
'dependencies' : libopenssl_cflags,
},
]
+103
View File
@@ -0,0 +1,103 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-varlink.h"
#include "build.h"
#include "format-table.h"
#include "help-util.h"
#include "log.h"
#include "main-func.h"
#include "options.h"
#include "report-files.h"
#include "varlink-io.systemd.Metrics.h"
#include "varlink-util.h"
static int vl_server(void) {
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *vs = NULL;
int r;
r = varlink_server_new(&vs, /* flags= */ 0, /* 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_Metrics);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = sd_varlink_server_bind_method_many(
vs,
"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("Report the contents of files as system report metrics.");
help_section("Options");
r = table_print_or_warn(options);
if (r < 0)
return r;
help_man_page_reference("systemd-report-files@.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);
+193
View File
@@ -0,0 +1,193 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-json.h"
#include "sd-varlink.h"
#include "alloc-util.h"
#include "chase.h"
#include "conf-files.h"
#include "fd-util.h"
#include "fileio.h"
#include "log.h"
#include "metrics.h"
#include "path-util.h"
#include "report-files.h"
#include "string-util.h"
#include "strv.h"
#include "utf8.h"
#include "varlink-idl-util.h"
/* Upper bounds, to protect against pathologically large directories or files. */
#define REPORT_FILES_MAX 1024U
#define REPORT_FILE_SIZE_MAX (4U * 1024U * 1024U)
/* The directories we look for files to report in. This is the usual CONF_PATHS() set (/etc/, /run/,
* /usr/local/lib/, /usr/lib/), plus an extra directory below /var/lib/ for persistent local additions. Files
* (typically symlinks to the actual files to report) dropped into any of these are reported as metrics, keyed
* by their name. Entries in earlier directories override identically named ones in later directories. */
static const char* const report_files_dirs[] = {
"/etc/systemd/report.files",
"/run/systemd/report.files",
"/var/lib/systemd/report.files",
"/usr/local/lib/systemd/report.files",
"/usr/lib/systemd/report.files",
NULL,
};
static MetricFamily* metric_family_array_free(MetricFamily *families) {
if (!families)
return NULL;
/* The array is NULL-name terminated. We own the name/description strings. */
for (MetricFamily *mf = families; mf->name; mf++) {
free((char*) mf->name);
free((char*) mf->description);
}
return mfree(families);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(MetricFamily*, metric_family_array_free);
static int file_metric_generate(const MetricFamily *mf, sd_varlink *link, void *userdata) {
int r;
assert(mf && mf->name);
assert(link);
/* Recover the file name from the metric family name: it is the part following the interface prefix.
* We look it up across our directories again (rather than caching the path found while building the
* metric list), and read the first instance we find, matching the precedence used at enumeration. */
const char *field = startswith(mf->name, METRIC_IO_SYSTEMD_FILES_PREFIX);
assert(field);
_cleanup_free_ char *buf = NULL, *discovered_path = NULL;
size_t size = 0;
STRV_FOREACH(d, report_files_dirs) {
_cleanup_free_ char *path = path_join(*d, field);
if (!path)
return log_oom();
_cleanup_free_ char *resolved = NULL;
_cleanup_close_ int fd = chase_and_open(path, /* root= */ NULL, CHASE_MUST_BE_REGULAR, O_RDONLY|O_CLOEXEC, &resolved);
if (fd == -ENOENT) /* Not in this directory (or dangling symlink): try the next one. */
continue;
if (fd < 0) {
log_warning_errno(fd, "Failed to open '%s', skipping: %m", path);
return 0;
}
r = read_full_file_full(
fd,
/* filename= */ NULL,
/* offset= */ UINT64_MAX,
REPORT_FILE_SIZE_MAX,
READ_FULL_FILE_FAIL_WHEN_LARGER,
/* bind_name= */ NULL,
&buf,
&size);
if (r < 0) {
log_warning_errno(r, "Failed to read '%s', skipping: %m", path);
return 0;
}
discovered_path = TAKE_PTR(resolved);
break;
}
if (!discovered_path) {
log_debug("File for metric '%s' unavailable, skipping.", mf->name);
return 0;
}
/* Metric values are JSON strings, so we can only report text files. Skip anything that isn't valid,
* NUL-free UTF-8. */
if (memchr(buf, 0, size) || !utf8_is_valid(buf)) {
log_debug("File for metric '%s' is not valid UTF-8 text, skipping.", mf->name);
return 0;
}
return metric_build_send_string(mf, link, discovered_path, buf, /* fields= */ NULL);
}
static int build_file_metrics(MetricFamily **ret) {
_cleanup_(metric_family_array_freep) MetricFamily *families = NULL;
_cleanup_strv_free_ char **files = NULL;
size_t n = 0;
int r;
assert(ret);
/* Enumerate the files to report across all our directories, deduplicated by name. The entry name is
* used as the metric field name. */
r = conf_files_list_strv(&files, /* suffix= */ NULL, /* root= */ NULL, CONF_FILES_REGULAR, report_files_dirs);
if (r < 0)
return log_error_errno(r, "Failed to enumerate report files: %m");
STRV_FOREACH(f, files) {
_cleanup_free_ char *base = NULL;
r = path_extract_filename(*f, &base);
if (r < 0)
return log_error_errno(r, "Failed to extract file name from '%s': %m", *f);
/* The name becomes the metric field name, so it must be a valid one. */
if (!varlink_idl_field_name_is_valid(base)) {
log_debug("Report file '%s' does not have a valid metric field name, skipping.", *f);
continue;
}
if (n >= REPORT_FILES_MAX) {
log_warning("More than %u report files found, not reporting the rest.", REPORT_FILES_MAX);
break;
}
_cleanup_free_ char *name = strjoin(METRIC_IO_SYSTEMD_FILES_PREFIX, base);
_cleanup_free_ char *description = strjoin("Contents of the '", base, "' report file");
if (!name || !description)
return log_oom();
/* Room for the new entry plus the NULL-name terminator. */
if (!GREEDY_REALLOC(families, n + 2))
return log_oom();
families[n++] = (MetricFamily) {
.name = TAKE_PTR(name),
.description = TAKE_PTR(description),
.type = METRIC_FAMILY_TYPE_STRING,
.generate = file_metric_generate,
};
families[n] = (MetricFamily) {}; /* terminator */
}
/* The metrics helpers expect a valid, terminated array even when empty. */
if (!families) {
families = new0(MetricFamily, 1);
if (!families)
return log_oom();
}
*ret = TAKE_PTR(families);
return 0;
}
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
int r;
_cleanup_(metric_family_array_freep) MetricFamily *families = NULL;
r = build_file_metrics(&families);
if (r < 0)
return r;
return metrics_method_list(families, link, parameters, flags, /* userdata= */ NULL);
}
int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
int r;
_cleanup_(metric_family_array_freep) MetricFamily *families = NULL;
r = build_file_metrics(&families);
if (r < 0)
return r;
return metrics_method_describe(families, link, parameters, flags, /* userdata= */ NULL);
}
+9
View File
@@ -0,0 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "shared-forward.h"
#define METRIC_IO_SYSTEMD_FILES_PREFIX "io.systemd.Files."
int vl_method_list_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
int vl_method_describe_metrics(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
+64
View File
@@ -0,0 +1,64 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-json.h"
#include "log.h"
#include "report.h"
#include "report-generate.h"
#include "report-sign.h"
#include "time-util.h"
int context_build_report(Context *context, sd_json_variant **ret) {
int r;
/* Convert the variant array to a JSON report. */
assert(context);
assert(ret);
usec_t ts = now(CLOCK_REALTIME);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL;
r = sd_json_buildo(&report,
SD_JSON_BUILD_PAIR_STRING("mediaType", "application/vnd.io.systemd.report"),
SD_JSON_BUILD_PAIR("timestamp",
SD_JSON_BUILD_STRING(FORMAT_TIMESTAMP_STYLE(ts, TIMESTAMP_UTC))),
SD_JSON_BUILD_PAIR("metrics",
SD_JSON_BUILD_VARIANT_ARRAY(context->metrics, context->n_metrics)));
if (r < 0)
return log_error_errno(r, "Failed to build JSON data: %m");
/* Normalize the report, to make signing more robust (note that we sign a specific binary formatting
* of it though, this is hence not load bearing, but still useful) */
r = sd_json_variant_normalize(&report);
if (r < 0)
return log_error_errno(r, "Failed to normalize report JSON: %m");
*ret = TAKE_PTR(report);
return 0;
}
int context_generate_report(Context *context) {
int r;
assert(context);
/* Make a structured report and either print it or upload it. */
_cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL;
r = context_build_report(context, &report);
if (r < 0)
return r;
if (arg_sign) {
r = context_sign_report(context, report, arg_json_format_flags, /* output= */ NULL);
if (r < 0)
return r;
} else {
r = sd_json_variant_dump(report, arg_json_format_flags, /* f= */ NULL, /* prefix= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to dump json object: %m");
}
return 0;
}
+8
View File
@@ -0,0 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "report.h"
int context_build_report(Context *context, sd_json_variant **ret);
int context_generate_report(Context *context);
+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);
+229
View File
@@ -0,0 +1,229 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-json.h"
#include "sd-varlink.h"
#include "alloc-util.h"
#include "errno-util.h"
#include "json-util.h"
#include "log.h"
#include "memstream-util.h"
#include "path-util.h"
#include "report.h"
#include "report-sign.h"
#include "sha256.h"
#include "time-util.h"
#include "varlink-util.h"
#define REPORT_SIGN_DIR "/run/systemd/report.sign"
#define REPORT_SIGN_TIMEOUT_USEC USEC_PER_MINUTE
typedef struct Signature {
char *mechanism;
sd_json_variant *data;
} Signature;
typedef struct SignatureList {
Signature *signatures;
size_t n_signatures;
int result;
} SignatureList;
static void signature_done(Signature *s) {
assert(s);
s->data = sd_json_variant_unref(s->data);
s->mechanism = mfree(s->mechanism);
}
static void signature_list_done(SignatureList *sl) {
assert(sl);
FOREACH_ARRAY(s, sl->signatures, sl->n_signatures)
signature_done(s);
sl->signatures = mfree(sl->signatures);
sl->n_signatures = 0;
}
static int execute_dir_reply(
sd_varlink *link,
sd_json_variant *reply,
const char *error_id,
sd_varlink_reply_flags_t flags,
void *userdata) {
SignatureList *sl = ASSERT_PTR(userdata);
int r;
assert(link);
/* Get the socket name */
const char *p = ASSERT_PTR(sd_varlink_get_description(link));
_cleanup_free_ char *sn = NULL;
r = path_extract_filename(p, &sn);
if (r < 0)
return log_error_errno(r, "Failed to extract service name from '%s': %m", p);
if (error_id) {
r = sd_varlink_error_to_errno(error_id, reply);
RET_GATHER(sl->result, r);
return log_error_errno(r, "Signing via Varlink service '%s' failed: %s", p, error_id);
}
_cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL;
static const sd_json_dispatch_field dispatch_table[] = {
{ "data", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant, /* offset= */ 0, /* flags= */ 0 },
{},
};
r = sd_json_dispatch(reply, dispatch_table, /* flags= */ 0, &array);
if (r < 0)
return log_error_errno(r, "Failed to dispatch method reply: %m");
size_t n = 0;
if (array) {
sd_json_variant *s;
JSON_VARIANT_ARRAY_FOREACH(s, array) {
if (!GREEDY_REALLOC(sl->signatures, sl->n_signatures + 1))
return log_oom();
Signature *i = sl->signatures + sl->n_signatures;
i->mechanism = strdup(sn);
if (!i->mechanism)
return log_oom();
i->data = sd_json_variant_ref(s);
sl->n_signatures++;
n++;
}
}
if (n == 0)
log_info("Mechanism '%s' succeeded, but returned no signatures.", p);
else
log_info("Successfully acquired %zu signatures from '%s'", n, p);
return 0;
}
int context_sign_report(
Context *context,
sd_json_variant *report,
sd_json_format_flags_t format_flags,
FILE *output) {
int r;
assert(context);
assert(report);
/* When generating a signed report we switch to JSON-SEQ. We'll put the report as first object in the
* stream, and then signature objects after it, that cover the precise binary representation of the
* first object. We normalize the report JSON first, but this is not load bearing, as the signature
* is about the binary representation of the JSON object sent over the wire, not the JSON object
* itself. */
if (!output)
output = stdout;
/* For the report itself we'll use the normalized, dense formatting, in order to make things as
* reproducible as possible. */
_cleanup_free_ char *text = NULL;
r = sd_json_variant_format(report, SD_JSON_FORMAT_SEQ, &text);
if (r < 0)
return log_error_errno(r, "Failed to format JSON data: %m");
uint8_t digest[SHA256_DIGEST_SIZE];
sha256_direct(text, strlen(text), digest);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL;
r = sd_json_buildo(&params,
SD_JSON_BUILD_PAIR_HEX("digest", digest, sizeof(digest)),
SD_JSON_BUILD_PAIR_STRING("algorithm", "SHA256"));
if (r < 0)
return log_error_errno(r, "Failed to build JSON data: %m");
_cleanup_(signature_list_done) SignatureList sl = {};
ssize_t jobs = varlink_execute_directory(
REPORT_SIGN_DIR,
"io.systemd.Report.Signer.Sign",
params,
/* more= */ false,
REPORT_SIGN_TIMEOUT_USEC,
execute_dir_reply,
/* userdata= */ &sl);
if (jobs < 0)
return log_error_errno(jobs, "Failed to execute signing via '%s': %m", REPORT_SIGN_DIR);
if (jobs == 0)
return log_error_errno(SYNTHETIC_ERRNO(ENOPKG),
"No signing mechanism found via '%s'.", REPORT_SIGN_DIR);
if (sl.result < 0)
/* The details were printed at error level by execute_dir_reply above. */
return log_debug_errno(sl.result, "Signing via '%s' failed: %m", REPORT_SIGN_DIR);
if (sl.n_signatures == 0)
return log_debug_errno(SYNTHETIC_ERRNO(ENOPKG),
"No signatures acquired via '%s'.", REPORT_SIGN_DIR);
if (fputs(text, output) == EOF)
return log_error_errno(errno, "Failed to write report: %m");
/* For the signatures we can use the requested formattting */
format_flags |= SD_JSON_FORMAT_SEQ;
format_flags &= ~SD_JSON_FORMAT_OFF;
FOREACH_ARRAY(s, sl.signatures, sl.n_signatures) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *sig = NULL;
r = sd_json_buildo(&sig,
SD_JSON_BUILD_PAIR_STRING("mediaType", "application/vnd.io.systemd.report.signature"),
SD_JSON_BUILD_PAIR_STRING("mechanism", s->mechanism),
SD_JSON_BUILD_PAIR_HEX("sha256", digest, sizeof(digest)),
JSON_BUILD_PAIR_VARIANT_NON_EMPTY("data", s->data));
if (r < 0)
return log_error_errno(r, "Failed to build JSON data: %m");
r = sd_json_variant_normalize(&sig);
if (r < 0)
return log_error_errno(r, "Failed to normalize JSON object: %m");
r = sd_json_variant_dump(sig, format_flags, output, /* prefix= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to dump json object: %m");
}
log_debug("Signing via '%s' finished successfully.", REPORT_SIGN_DIR);
return 0;
}
int context_sign_report_as_string(
Context *context,
sd_json_variant *report,
sd_json_format_flags_t format_flags,
char **ret) {
int r;
assert(context);
assert(report);
assert(ret);
_cleanup_(memstream_done) MemStream ms = {};
FILE *f = memstream_init(&ms);
if (!f)
return log_oom();
r = context_sign_report(context, report, format_flags, f);
if (r < 0)
return r;
r = memstream_finalize(&ms, ret, /* ret_size= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to finalize memory stream: %m");
return 0;
}
+8
View File
@@ -0,0 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "report.h"
int context_sign_report(Context *context, sd_json_variant *report, sd_json_format_flags_t format_flags, FILE *output);
int context_sign_report_as_string(Context *context, sd_json_variant *report, sd_json_format_flags_t format_flags, char **ret);
+37 -46
View File
@@ -6,6 +6,9 @@
#include "errno-util.h"
#include "log.h"
#include "report.h"
#include "report-generate.h"
#include "report-sign.h"
#include "report-upload.h"
#include "string-util.h"
#include "strv.h"
#include "time-util.h"
@@ -13,11 +16,12 @@
#include "varlink-util.h"
#include "version.h"
#define REPORT_UPLOAD_DIR "/run/systemd/report.upload"
#define SERVER_ANSWER_MAX (1U * 1024U * 1024U)
#if HAVE_LIBCURL
#include "curl-util.h"
#define SERVER_ANSWER_MAX (1*1024*1024u)
static size_t output_callback(char *buf,
size_t size,
size_t nmemb,
@@ -54,31 +58,10 @@ static size_t output_callback(char *buf,
}
#endif
static int build_json_report(Context *context, sd_json_variant **ret) {
/* Convert the variant array to a JSON report. */
assert(context);
assert(ret);
usec_t ts = now(CLOCK_REALTIME);
int r;
r = sd_json_buildo(ret,
SD_JSON_BUILD_PAIR_STRING("mediaType", "application/vnd.io.systemd.report"),
SD_JSON_BUILD_PAIR("timestamp",
SD_JSON_BUILD_STRING(FORMAT_TIMESTAMP_STYLE(ts, TIMESTAMP_UTC))),
SD_JSON_BUILD_PAIR("metrics",
SD_JSON_BUILD_VARIANT_ARRAY(context->metrics, context->n_metrics)));
if (r < 0)
return log_error_errno(r, "Failed to build JSON data: %m");
return 0;
}
static int http_upload_collected(Context *context, sd_json_variant *report) {
static int http_upload_report(Context *context, sd_json_variant *report) {
#if HAVE_LIBCURL
_cleanup_(curl_slist_free_allp) struct curl_slist *header = NULL;
char error[CURL_ERROR_SIZE] = {};
_cleanup_free_ char *json = NULL;
int r;
r = DLOPEN_CURL(LOG_DEBUG, SD_ELF_NOTE_DLOPEN_PRIORITY_REQUIRED);
@@ -87,9 +70,16 @@ static int http_upload_collected(Context *context, sd_json_variant *report) {
/* Upload a JSON report in text form as a single JSON object, instead of a JSON-SEQ list. */
r = sd_json_variant_format(report, /* flags= */ 0, &json);
if (r < 0)
return log_error_errno(r, "Failed to format JSON data: %m");
_cleanup_free_ char *text = NULL;
if (arg_sign) {
r = context_sign_report_as_string(context, report, /* format_flags= */ 0, &text);
if (r < 0)
return r;
} else {
r = sd_json_variant_format(report, /* flags= */ 0, &text);
if (r < 0)
return log_error_errno(r, "Failed to format JSON data: %m");
}
r = curl_append_to_header(&header,
STRV_MAKE("Content-Type: application/json",
@@ -161,7 +151,7 @@ static int http_upload_collected(Context *context, sd_json_variant *report) {
if (!easy_setopt(curl, LOG_ERR, CURLOPT_URL, arg_url))
return -EXFULL;
if (!easy_setopt(curl, LOG_ERR, CURLOPT_POSTFIELDS, json))
if (!easy_setopt(curl, LOG_ERR, CURLOPT_POSTFIELDS, text))
return -EXFULL;
CURLcode code = sym_curl_easy_perform(curl);
@@ -235,15 +225,25 @@ static int execute_dir_reply(
return 0;
}
static int upload_collected(Context *context, sd_json_variant *report) {
static int varlink_upload_report(Context *context, sd_json_variant *report) {
int r;
if (arg_url)
return http_upload_collected(context, report);
assert(context);
assert(report);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL;
r = sd_json_buildo(&params,
SD_JSON_BUILD_PAIR_VARIANT("report", report));
if (arg_sign) {
_cleanup_free_ char *buf = NULL;
r = context_sign_report_as_string(context, report, /* format_flags= */ 0, &buf);
if (r < 0)
return r;
r = sd_json_buildo(&params,
SD_JSON_BUILD_PAIR_BASE64("reportData", buf, strlen(buf)));
} else
r = sd_json_buildo(&params,
SD_JSON_BUILD_PAIR_VARIANT("report", report));
if (r < 0)
return log_error_errno(r, "Failed to build JSON data: %m");
@@ -268,22 +268,13 @@ static int upload_collected(Context *context, sd_json_variant *report) {
return 0;
}
/* Make a structured report and either print it or upload it. */
int report_collected(Context *context) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL;
int context_upload_report(Context *context) {
int r;
r = build_json_report(context, &report);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL;
r = context_build_report(context, &report);
if (r < 0)
return r;
if (context->action == ACTION_UPLOAD)
return upload_collected(context, report);
/* Just print the report for now. */
assert(context->action == ACTION_GENERATE);
r = sd_json_variant_dump(report, arg_json_format_flags, /* f= */ NULL, /* prefix= */ NULL);
if (r < 0)
return log_error_errno(r, "Failed to dump json object: %m");
return 0;
return (arg_url ? http_upload_report : varlink_upload_report)(context, report);
}
+6
View File
@@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "report.h"
int context_upload_report(Context *context);
+219 -56
View File
@@ -16,6 +16,9 @@
#include "path-lookup.h"
#include "recurse-dir.h"
#include "report.h"
#include "report-generate.h"
#include "report-sign.h"
#include "report-upload.h"
#include "runtime-scope.h"
#include "set.h"
#include "sort-util.h"
@@ -23,6 +26,8 @@
#include "strv.h"
#include "time-util.h"
#include "varlink-idl-util.h"
#include "varlink-io.systemd.Report.h"
#include "varlink-util.h"
#include "verbs.h"
#include "web-util.h"
@@ -33,7 +38,6 @@
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
static char **arg_matches = NULL;
sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF|SD_JSON_FORMAT_PRETTY_AUTO|SD_JSON_FORMAT_COLOR_AUTO;
char *arg_url = NULL;
char *arg_key = NULL;
@@ -41,8 +45,8 @@ char *arg_cert = NULL;
char *arg_trust = NULL;
char **arg_extra_headers = NULL;
usec_t arg_network_timeout_usec = TIMEOUT_USEC;
bool arg_sign = false;
STATIC_DESTRUCTOR_REGISTER(arg_matches, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_url, freep);
STATIC_DESTRUCTOR_REGISTER(arg_key, freep);
STATIC_DESTRUCTOR_REGISTER(arg_cert, freep);
@@ -70,6 +74,7 @@ static void context_done(Context *context) {
context->event = sd_event_unref(context->event);
context->link_infos = set_free(context->link_infos);
context->matches = strv_free(context->matches);
sd_json_variant_unref_many(context->metrics, context->n_metrics);
context->metrics = NULL;
context->n_metrics = 0;
@@ -190,13 +195,13 @@ static Verdict metrics_verdict(LinkInfo *li, sd_json_variant *metric) {
/* Check it against any specified matches */
bool matches;
if (strv_isempty(arg_matches))
if (strv_isempty(li->context->matches))
matches = true;
else {
matches = false;
/* Allow exact matches or prefix matches */
STRV_FOREACH(i, arg_matches)
STRV_FOREACH(i, li->context->matches)
if (streq(metric_name, *i) ||
metric_startswith_prefix(metric_name, *i)) {
matches = true;
@@ -232,6 +237,8 @@ static int on_query_reply(
log_warning("Varlink connection to '%s' disconnected prematurely, ignoring.", li->name);
else if (streq(error_id, SD_VARLINK_ERROR_TIMEOUT))
log_warning("Varlink connection to '%s' timed out, ignoring.", li->name);
else if (streq(error_id, "io.systemd.Metrics.NoSuchMetric"))
log_debug("Varlink connection to '%s' reported no more metrics, ignoring.", li->name);
else
log_warning("Varlink error from '%s', ignoring: %s", li->name, error_id);
@@ -488,28 +495,33 @@ static int output_collected(Context *context) {
return 0;
}
static int parse_metrics_matches(char **matches) {
static int parse_metrics_matches(char **input, char ***ret) {
int r;
STRV_FOREACH(i, matches) {
assert(ret);
_cleanup_strv_free_ char **matches = NULL;
STRV_FOREACH(i, input) {
r = metrics_name_valid(*i);
if (r < 0)
return log_error_errno(r, "Failed to determine if '%s' is a valid metric name: %m", *i);
if (!r && !varlink_idl_interface_name_is_valid(*i))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Match is not a valid family name or prefix: %s", *i);
if (strv_extend(&arg_matches, *i) < 0)
if (strv_extend(&matches, *i) < 0)
return log_oom();
}
strv_sort_uniq(arg_matches);
strv_sort_uniq(matches);
*ret = TAKE_PTR(matches);
return 0;
}
static bool test_service_matches(const char *service) {
static bool test_service_matches(const char *service, char **matches) {
assert(service);
if (strv_isempty(arg_matches))
if (strv_isempty(matches))
return true;
/* Only contact services whose name is either a prefix of any of the specified metrics families, or
@@ -522,7 +534,7 @@ static bool test_service_matches(const char *service) {
* it should also be fine to specify a full metric name, and then go directly to the relevant services, and ask for matching metrics.
*/
STRV_FOREACH(i, arg_matches) {
STRV_FOREACH(i, matches) {
if (streq(service, *i))
return true;
@@ -534,7 +546,7 @@ static bool test_service_matches(const char *service) {
return false;
}
static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
static int readdir_sources(char **matches, char **ret_directory, DirectoryEntries **ret) {
int r;
assert(ret_directory);
@@ -570,7 +582,7 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
if (!varlink_idl_interface_name_is_valid(d->d_name))
continue;
if (!test_service_matches(d->d_name))
if (!test_service_matches(d->d_name, matches))
continue;
de->entries[m++] = *i;
@@ -584,6 +596,51 @@ static int readdir_sources(char **ret_directory, DirectoryEntries **ret) {
return m > 0;
}
static int context_collect_metrics(Context *context) {
int r;
/* Contacts all known metrics sources, issues the appropriate Varlink call on each and runs the
* event loop until all replies came in. Expects the caller to have set up context->event
* beforehand. The collected metrics end up in context->metrics. */
assert(context);
assert(context->event);
_cleanup_free_ DirectoryEntries *de = NULL;
_cleanup_free_ char *sources_path = NULL;
r = readdir_sources(context->matches, &sources_path, &de);
if (r < 0)
return r;
if (r == 0)
return 0;
FOREACH_ARRAY(i, de->entries, de->n_entries) {
struct dirent *d = *i;
if (set_size(context->link_infos) >= METRICS_LINKS_MAX) {
context->n_skipped_sources++;
break;
}
_cleanup_free_ char *p = path_join(sources_path, d->d_name);
if (!p)
return log_oom();
(void) call_collect(context, d->d_name, p);
}
context->n_contacted_sources = set_size(context->link_infos);
if (context->n_contacted_sources == 0)
return 0;
r = sd_event_loop(context->event);
if (r < 0)
return log_error_errno(r, "Failed to run event loop: %m");
return 1;
}
VERB_FULL(verb_metrics, "metrics", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_LIST_METRICS,
"Acquire list of metrics and their values");
VERB_FULL(verb_metrics, "describe", "[MATCH…]", VERB_ANY, VERB_ANY, 0, ACTION_DESCRIBE_METRICS,
@@ -605,67 +662,55 @@ static int verb_metrics(int argc, char *argv[], uintptr_t data, void *userdata)
* objects. In the report format, we return a single JSON object, so don't do this. */
arg_json_format_flags |= SD_JSON_FORMAT_SEQ;
r = parse_metrics_matches(argv + 1);
if (r < 0)
return r;
_cleanup_(context_done) Context context = {
.action = action,
};
size_t n_skipped_sources = 0;
_cleanup_free_ DirectoryEntries *de = NULL;
_cleanup_free_ char *sources_path = NULL;
r = readdir_sources(&sources_path, &de);
r = parse_metrics_matches(argv + 1, &context.matches);
if (r < 0)
return r;
if (r > 0) {
r = sd_event_default(&context.event);
if (r < 0)
return log_error_errno(r, "Failed to get event loop: %m");
r = sd_event_set_signal_exit(context.event, true);
if (r < 0)
return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m");
r = sd_event_default(&context.event);
if (r < 0)
return log_error_errno(r, "Failed to get event loop: %m");
FOREACH_ARRAY(i, de->entries, de->n_entries) {
struct dirent *d = *i;
r = sd_event_set_signal_exit(context.event, true);
if (r < 0)
return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m");
if (set_size(context.link_infos) >= METRICS_LINKS_MAX) {
n_skipped_sources++;
break;
}
_cleanup_free_ char *p = path_join(sources_path, d->d_name);
if (!p)
return log_oom();
(void) call_collect(&context, d->d_name, p);
}
}
if (set_isempty(context.link_infos)) {
r = context_collect_metrics(&context);
if (r < 0)
return r;
if (r == 0) {
if (arg_legend)
log_info("No metrics sources found.");
} else {
assert(context.event);
switch (action) {
r = sd_event_loop(context.event);
if (r < 0)
return log_error_errno(r, "Failed to run event loop: %m");
if (IN_SET(action, ACTION_GENERATE, ACTION_UPLOAD))
r = report_collected(&context);
else
case ACTION_LIST_METRICS:
case ACTION_DESCRIBE_METRICS:
r = output_collected(&context);
break;
case ACTION_GENERATE:
r = context_generate_report(&context);
break;
case ACTION_UPLOAD:
r = context_upload_report(&context);
break;
default:
assert_not_reached();
}
if (r < 0)
return r;
}
if (n_skipped_sources > 0)
if (context.n_skipped_sources > 0)
return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN),
"Too many metrics sources, only %u sources contacted, %zu sources skipped.",
set_size(context.link_infos), n_skipped_sources);
"Too many metrics sources, only %zu sources contacted, %zu sources skipped.",
context.n_contacted_sources, context.n_skipped_sources);
if (context.n_invalid_metrics > 0)
return log_warning_errno(SYNTHETIC_ERRNO(EUCLEAN),
"%zu metrics are not valid.",
@@ -687,7 +732,7 @@ static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *user
_cleanup_free_ char *sources_path = NULL;
_cleanup_free_ DirectoryEntries *de = NULL;
r = readdir_sources(&sources_path, &de);
r = readdir_sources(/* matches= */ NULL, &sources_path, &de);
if (r < 0)
return r;
if (r > 0)
@@ -733,6 +778,110 @@ static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *user
return 0;
}
static int vl_method_generate_internal(
sd_varlink *link,
sd_json_variant *parameters,
bool sign) {
int r;
assert(link);
assert(parameters);
_cleanup_strv_free_ char **input_matches = NULL;
static const sd_json_dispatch_field dispatch_table[] = {
{ "matches", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, 0, 0 },
{}
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &input_matches);
if (r != 0)
return r;
_cleanup_(context_done) Context context = {
.action = ACTION_GENERATE,
};
r = parse_metrics_matches(input_matches, &context.matches);
if (r < 0)
return sd_varlink_error_invalid_parameter_name(link, "matches");
r = sd_event_new(&context.event);
if (r < 0)
return log_error_errno(r, "Failed to allocate event loop: %m");
r = context_collect_metrics(&context);
if (r < 0)
return r;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL;
r = context_build_report(&context, &report);
if (r < 0)
return r;
if (sign) {
/* Use compact JSON formatting (no pretty/color/seq flags), matching the on-the-wire format
* used for uploads. context_sign_report() adds the JSON-SEQ record separators itself. */
_cleanup_free_ char *s = NULL;
r = context_sign_report_as_string(&context, report, /* format_flags= */ 0, &s);
if (r < 0)
return r;
return sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_BASE64("reportData", s, strlen(s)));
}
return sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_VARIANT("report", report));
}
static int vl_method_generate(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
return vl_method_generate_internal(link, parameters, /* sign= */ false);
}
static int vl_method_generate_signed(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
return vl_method_generate_internal(link, parameters, /* sign= */ true);
}
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_MYSELF_ONLY|SD_VARLINK_SERVER_ROOT_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);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = sd_varlink_server_bind_method_many(
vs,
"io.systemd.Report.Generate", vl_method_generate,
"io.systemd.Report.GenerateSigned", vl_method_generate_signed);
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) {
int r;
@@ -856,6 +1005,12 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
if (strv_extend(&arg_extra_headers, opts.arg) < 0)
return log_oom();
break;
OPTION_LONG("sign", "BOOL", "Sign the generated report."):
r = parse_boolean_argument("--sign", opts.arg, &arg_sign);
if (r < 0)
return r;
break;
}
if ((arg_url || arg_key || arg_cert || arg_trust || arg_extra_headers) && !HAVE_LIBCURL)
@@ -871,6 +1026,14 @@ static int run(int argc, char *argv[]) {
log_setup();
/* If invoked as a socket-activated Varlink service (Accept=yes), act as the io.systemd.Report
* server instead of running the command line interface. */
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 vl_server();
r = parse_argv(argc, argv, &args);
if (r <= 0)
return r;
+3 -5
View File
@@ -9,12 +9,11 @@
#define REPORT_CERT_FILE CERTIFICATE_ROOT "/certs/systemd-report.pem"
#define REPORT_TRUST_FILE CERTIFICATE_ROOT "/ca/trusted.pem"
#define REPORT_UPLOAD_DIR "/run/systemd/report.upload"
extern sd_json_format_flags_t arg_json_format_flags;
extern char *arg_url, *arg_key, *arg_cert, *arg_trust;
extern char **arg_extra_headers;
extern usec_t arg_network_timeout_usec;
extern bool arg_sign;
typedef enum Action {
ACTION_LIST_METRICS,
@@ -30,11 +29,10 @@ typedef struct Context {
Action action;
sd_event *event;
Set *link_infos;
char **matches; /* Metric families to include, or NULL/empty for all */
sd_json_variant **metrics; /* Collected metrics for sorting */
size_t n_metrics, n_skipped_metrics, n_invalid_metrics;
size_t n_metrics, n_skipped_metrics, n_invalid_metrics, n_contacted_sources, n_skipped_sources;
int upload_result;
struct iovec_wrapper upload_answer;
} Context;
int report_collected(Context *context);
+3
View File
@@ -1627,6 +1627,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. */
+3
View File
@@ -241,6 +241,9 @@ shared_sources = files(
'varlink-io.systemd.PCRExtend.c',
'varlink-io.systemd.PCRLock.c',
'varlink-io.systemd.Repart.c',
'varlink-io.systemd.Report.c',
'varlink-io.systemd.Report.Signer.c',
'varlink-io.systemd.Report.Uploader.c',
'varlink-io.systemd.Resolve.c',
'varlink-io.systemd.Resolve.Hook.c',
'varlink-io.systemd.Resolve.Monitor.c',
@@ -0,0 +1,19 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "varlink-io.systemd.Report.Signer.h"
static SD_VARLINK_DEFINE_METHOD(
Sign,
SD_VARLINK_FIELD_COMMENT("The digest of the data to sign."),
SD_VARLINK_DEFINE_INPUT(digest, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("The digest algorithm used for the digest field above. For now this is always SHA256, but this might be changed eventually."),
SD_VARLINK_DEFINE_INPUT(algorithm, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("An array of signature objects"),
SD_VARLINK_DEFINE_OUTPUT(data, SD_VARLINK_OBJECT, SD_VARLINK_ARRAY));
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_Report_Signer,
"io.systemd.Report.Signer",
SD_VARLINK_INTERFACE_COMMENT("Backend API for signing reports. This interface shall be implemented by services linked into /run/systemd/report.sign/."),
SD_VARLINK_SYMBOL_COMMENT("Sign a report, identified by a digest. This may return zero, one or more signatures, as appropriate. For example some backend might have multiple keys or algorithms available, that are appropriate, in which case it can generate multiple signatures."),
&vl_method_Sign);
@@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-varlink-idl.h"
extern const sd_varlink_interface vl_interface_io_systemd_Report_Signer;
@@ -0,0 +1,17 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "varlink-io.systemd.Report.Uploader.h"
static SD_VARLINK_DEFINE_METHOD(
Upload,
SD_VARLINK_FIELD_COMMENT("Report data as JSON variant. Either this field or reportData (below) have to be specified. This mode is used if signing is not used, and hence a precise binary formatting of the JSON data is not relevant."),
SD_VARLINK_DEFINE_INPUT(report, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Report data in Base64. Either this field or report (above) have to be specified. This mode is used if signing is enabled, as a precise binary formatting of the JSON data is important to authenticate the signature. This data contains a JSON-SEQ compliant stream of objects, the first being the report, the following ones signature objects."),
SD_VARLINK_DEFINE_INPUT(reportData, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_Report_Uploader,
"io.systemd.Report.Uploader",
SD_VARLINK_INTERFACE_COMMENT("Backend API for uploading reports. This interface shall be implemented by services linked into /run/systemd/report.upload/"),
SD_VARLINK_SYMBOL_COMMENT("Upload a report now."),
&vl_method_Upload);
@@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-varlink-idl.h"
extern const sd_varlink_interface vl_interface_io_systemd_Report_Uploader;
+26
View File
@@ -0,0 +1,26 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "varlink-io.systemd.Report.h"
static SD_VARLINK_DEFINE_METHOD(
Generate,
SD_VARLINK_FIELD_COMMENT("Selects which metrics to include in the report, as an array of metric family names or prefixes thereof. If unset or empty all available metrics are included. This matches the [MATCH…] arguments of the systemd-report command line tool."),
SD_VARLINK_DEFINE_INPUT(matches, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The generated report as a JSON object. This mode does not sign the report, hence a precise binary formatting of the JSON data is not relevant."),
SD_VARLINK_DEFINE_OUTPUT(report, SD_VARLINK_OBJECT, 0));
static SD_VARLINK_DEFINE_METHOD(
GenerateSigned,
SD_VARLINK_FIELD_COMMENT("Selects which metrics to include in the report, as an array of metric family names or prefixes thereof. If unset or empty all available metrics are included. This matches the [MATCH…] arguments of the systemd-report command line tool."),
SD_VARLINK_DEFINE_INPUT(matches, SD_VARLINK_STRING, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("The generated, signed report in Base64. A precise binary formatting of the JSON data is important to authenticate the signature. This data contains a JSON-SEQ compliant stream of objects, the first being the report, the following ones signature objects."),
SD_VARLINK_DEFINE_OUTPUT(reportData, SD_VARLINK_STRING, 0));
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_Report,
"io.systemd.Report",
SD_VARLINK_INTERFACE_COMMENT("Frontend API for generating system reports. This interface is implemented by systemd-report, which aggregates the metrics exposed by the io.systemd.Metrics services linked into /run/systemd/report/."),
SD_VARLINK_SYMBOL_COMMENT("Generate a report and return it as a JSON object."),
&vl_method_Generate,
SD_VARLINK_SYMBOL_COMMENT("Generate a signed report and return it Base64 encoded."),
&vl_method_GenerateSigned);
+6
View File
@@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-varlink-idl.h"
extern const sd_varlink_interface vl_interface_io_systemd_Report;
+108 -1
View File
@@ -109,7 +109,9 @@ CERTDIR=$(mktemp -d)
at_exit() {
set +e
systemctl stop fake-report-server fake-report-server-tls
rm -rf "$CERTDIR"
systemctl stop systemd-report.socket systemd-report-files.socket systemd-report-sign-plain.socket
rm -f /run/systemd/report.files/testreportfile /run/systemd/report.files/binaryfile
rm -rf "$CERTDIR" "${SIGN_WORK:-}"
}
trap at_exit EXIT
@@ -132,3 +134,108 @@ systemctl status fake-report-server-tls
"$REPORT" upload --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \
--extra-header='Authorization: Bearer magic string'
# -----------------------------------------------------------------------------
# Test the systemd-report-files@.service metric provider: files dropped into one
# of the report.files directories are exposed verbatim as io.systemd.Files.*
# metrics, keyed by their file name.
mkdir -p /run/systemd/report.files
REPORT_FILE_CONTENT="hello from the report files metric provider"
printf '%s' "$REPORT_FILE_CONTENT" >/run/systemd/report.files/testreportfile
systemctl start systemd-report-files.socket
varlinkctl info /run/systemd/report/io.systemd.Files
varlinkctl list-methods /run/systemd/report/io.systemd.Files
# The metric value must be the exact file contents.
files_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.Files io.systemd.Metrics.List {})"
files_value() { echo "$files_metrics" | jq --seq -r "select(.name == \"$1\") | .value"; }
[ "$(files_value io.systemd.Files.testreportfile)" = "$REPORT_FILE_CONTENT" ]
# Describe must list the very same metric, of type string.
files_describe="$(varlinkctl call --more /run/systemd/report/io.systemd.Files io.systemd.Metrics.Describe {})"
echo "$files_describe" | jq --seq -r 'select(.name == "io.systemd.Files.testreportfile") | .type' | grep -wx string >/dev/null
# Files that aren't valid UTF-8 text must be skipped silently (the value is a
# JSON string, so only text files can be reported).
printf '\xff\xfe binary garbage' >/run/systemd/report.files/binaryfile
files_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.Files io.systemd.Metrics.List {})"
[ -z "$(files_value io.systemd.Files.binaryfile)" ]
rm -f /run/systemd/report.files/binaryfile
# -----------------------------------------------------------------------------
# Test report signing through the plain software backend, driven entirely via
# the io.systemd.Report Varlink interface (the GenerateSigned method). The plain
# backend is only built/installed with OpenSSL support, so skip if unavailable.
if ! systemctl cat systemd-report-sign-plain.socket &>/dev/null; then
echo "systemd-report-sign-plain.socket is not installed, skipping report signing test."
exit 0
fi
systemctl start systemd-report.socket
systemctl start systemd-report-sign-plain.socket
SIGN_WORK="$(mktemp -d)"
# Ask systemd-report to generate a *signed* report over Varlink. The reply
# carries the signed report as base64-encoded JSON-SEQ data.
varlinkctl call /run/systemd/io.systemd.Report io.systemd.Report.GenerateSigned \
'{"matches":["io.systemd.Manager.UnitsTotal"]}' | jq -r .reportData | base64 -d >"$SIGN_WORK/report.seq"
# The first JSON-SEQ record is the report itself. This is exactly the byte
# sequence that got signed, including the leading record separator (0x1e) and
# the trailing newline, so 'head -n1' reproduces it verbatim.
head -n1 "$SIGN_WORK/report.seq" >"$SIGN_WORK/message.bin"
tr -d '\036' <"$SIGN_WORK/message.bin" | jq -e '.mediaType == "application/vnd.io.systemd.report"' >/dev/null
# The remaining record(s) are signature objects. With only the plain backend
# enabled there is exactly one, with mechanism "plain".
sig_json="$(tail -n +2 "$SIGN_WORK/report.seq" | tr -d '\036')"
[ "$(echo "$sig_json" | jq -r .mechanism)" = "plain" ]
[ "$(echo "$sig_json" | jq -r .mediaType)" = "application/vnd.io.systemd.report.signature" ]
# The sha256 recorded in the signature must match the digest of the report bytes.
[ "$(echo "$sig_json" | jq -r .sha256)" = "$(sha256sum "$SIGN_WORK/message.bin" | cut -d' ' -f1)" ]
# Extract the embedded public key and the raw Ed25519 signature.
echo "$sig_json" | jq -r .data.key >"$SIGN_WORK/pub.pem"
echo "$sig_json" | jq -r .data.signature | base64 -d >"$SIGN_WORK/sig.bin"
# The signature is made over the SHA256 digest bytes directly (Ed25519, the
# signer does not pre-hash), so verify the raw digest against the signature.
openssl dgst -sha256 -binary "$SIGN_WORK/message.bin" >"$SIGN_WORK/digest.bin"
openssl pkeyutl -verify -pubin -inkey "$SIGN_WORK/pub.pem" -rawin \
-in "$SIGN_WORK/digest.bin" -sigfile "$SIGN_WORK/sig.bin"
# The public key embedded in the report must be the very key the plain backend
# generated and persisted on disk.
diff <(openssl pkey -pubin -in "$SIGN_WORK/pub.pem" -pubout) \
<(openssl pkey -pubin -in /var/lib/systemd/report.sign.plain/local.public -pubout)
# A tampered report must no longer verify against the signature.
printf 'x' >>"$SIGN_WORK/message.bin"
openssl dgst -sha256 -binary "$SIGN_WORK/message.bin" >"$SIGN_WORK/digest-bad.bin"
if openssl pkeyutl -verify -pubin -inkey "$SIGN_WORK/pub.pem" -rawin \
-in "$SIGN_WORK/digest-bad.bin" -sigfile "$SIGN_WORK/sig.bin"; then
echo "Signature verification unexpectedly succeeded for tampered report" >&2
exit 1
fi
# -----------------------------------------------------------------------------
# The plain backend also acts as a metric provider, exposing its public signing
# key as the io.systemd.Report.SignPlain.PublicKey metric. This is served off the
# very same socket as the signer, reachable in the metrics directory under
# /run/systemd/report/ (the signer endpoint is just a symlink into the root-only
# /run/systemd/report.sign/ directory pointing at the same socket).
varlinkctl info /run/systemd/report/io.systemd.Report.SignPlain
varlinkctl list-methods /run/systemd/report/io.systemd.Report.SignPlain
# The exposed public key must be the very key persisted on disk.
sign_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.Report.SignPlain io.systemd.Metrics.List {})"
echo "$sign_metrics" | jq --seq -r 'select(.name == "io.systemd.Report.SignPlain.PublicKey") | .value' >"$SIGN_WORK/metric-pub.pem"
diff <(openssl pkey -pubin -in "$SIGN_WORK/metric-pub.pem" -pubout) \
<(openssl pkey -pubin -in /var/lib/systemd/report.sign.plain/local.public -pubout)
# Describe must list the very same metric, of type string.
varlinkctl call --more /run/systemd/report/io.systemd.Report.SignPlain io.systemd.Metrics.Describe {} |
jq --seq -r 'select(.name == "io.systemd.Report.SignPlain.PublicKey") | .type' | grep -wx string >/dev/null
+1
View File
@@ -19,6 +19,7 @@ d$ /run/systemd/users 0755 root root -
d /run/systemd/machines 0755 root root -
d$ /run/systemd/shutdown 0755 root root -
d /run/systemd/dissect-root 0000 root root -
d /run/systemd/report.sign 0700 root root -
d /run/log 0755 root root -
+12
View File
@@ -633,6 +633,8 @@ units = [
'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'],
'symlinks' : ['sysinit.target.wants/'],
},
{ 'file' : 'systemd-report.socket' },
{ 'file' : 'systemd-report@.service.in' },
{ 'file' : 'systemd-report-basic.socket' },
{ 'file' : 'systemd-report-basic@.service.in' },
{
@@ -731,6 +733,16 @@ units = [
},
{ 'file' : 'systemd-report-cgroup.socket' },
{ 'file' : 'systemd-report-cgroup@.service.in' },
{ 'file' : 'systemd-report-files.socket' },
{ 'file' : 'systemd-report-files@.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'],
+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=Report Files Metrics Socket
Documentation=man:systemd-report-files@.service(8)
DefaultDependencies=no
Before=sockets.target shutdown.target
Conflicts=shutdown.target
[Socket]
ListenStream=/run/systemd/report/io.systemd.Files
FileDescriptorName=varlink
SocketMode=0666
Accept=yes
MaxConnectionsPerSource=16
RemoveOnStop=yes
[Install]
WantedBy=sockets.target
+18
View File
@@ -0,0 +1,18 @@
# 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 Files Metrics
Documentation=man:systemd-report-files@.service(8)
DefaultDependencies=no
Conflicts=shutdown.target
Before=shutdown.target
[Service]
ExecStart={{LIBEXECDIR}}/systemd-report-files
+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
+24
View File
@@ -0,0 +1,24 @@
# 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=System Report Socket
Documentation=man:systemd-report(1)
DefaultDependencies=no
Before=sockets.target
[Socket]
ListenStream=/run/systemd/io.systemd.Report
FileDescriptorName=varlink
SocketMode=0600
Accept=yes
MaxConnectionsPerSource=16
RemoveOnStop=yes
[Install]
WantedBy=sockets.target
+18
View File
@@ -0,0 +1,18 @@
# 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=System Report
Documentation=man:systemd-report(1)
DefaultDependencies=no
Conflicts=shutdown.target
Before=shutdown.target
[Service]
ExecStart={{LIBEXECDIR}}/systemd-report