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:
@@ -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,
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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(¶ms,
|
||||
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;
|
||||
}
|
||||
@@ -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
@@ -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(¶ms,
|
||||
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(¶ms,
|
||||
SD_JSON_BUILD_PAIR_BASE64("reportData", buf, strlen(buf)));
|
||||
} else
|
||||
r = sd_json_buildo(¶ms,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user