report: add support for optionally signing reports

These signatures can be provided by arbitrary Varlink backends symlinked
into /run/systemd/report/sign.

Also add a proper Varlink IDL for the upload interface, since we are
extended it now.
This commit is contained in:
Lennart Poettering
2026-06-14 18:46:19 +02:00
parent 0d9026c736
commit 3c2f7c6002
15 changed files with 369 additions and 19 deletions

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>

View File

@@ -43,6 +43,8 @@
#include "varlink-io.systemd.PCRExtend.h"
#include "varlink-io.systemd.PCRLock.h"
#include "varlink-io.systemd.Repart.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 +218,8 @@ TEST(parse_format) {
&vl_interface_io_systemd_PCRExtend,
&vl_interface_io_systemd_PCRLock,
&vl_interface_io_systemd_Repart,
&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,

View File

@@ -7,6 +7,7 @@ executables += [
'sources' : files(
'report.c',
'report-generate.c',
'report-sign.c',
'report-upload.c',
),
},

View File

@@ -5,6 +5,7 @@
#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) {
@@ -27,6 +28,12 @@ int context_build_report(Context *context, sd_json_variant **ret) {
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;
}
@@ -43,9 +50,15 @@ int context_generate_report(Context *context) {
if (r < 0)
return r;
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");
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;
}

229
src/report/report-sign.c Normal file
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
src/report/report-sign.h Normal file
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);

View File

@@ -7,6 +7,7 @@
#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"
@@ -57,11 +58,10 @@ static size_t output_callback(char *buf,
}
#endif
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);
@@ -70,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",
@@ -144,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);
@@ -218,20 +225,25 @@ static int execute_dir_reply(
return 0;
}
int context_upload_report(Context *context) {
static int varlink_upload_report(Context *context, sd_json_variant *report) {
int r;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL;
r = context_build_report(context, &report);
if (r < 0)
return 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");
@@ -255,3 +267,14 @@ int context_upload_report(Context *context) {
log_debug("Upload via %s finished successfully.", REPORT_UPLOAD_DIR);
return 0;
}
int context_upload_report(Context *context) {
int r;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *report = NULL;
r = context_build_report(context, &report);
if (r < 0)
return r;
return (arg_url ? http_upload_report : varlink_upload_report)(context, report);
}

View File

@@ -43,6 +43,7 @@ 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);
@@ -874,6 +875,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)

View File

@@ -13,6 +13,7 @@ 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,

View File

@@ -241,6 +241,8 @@ shared_sources = files(
'varlink-io.systemd.PCRExtend.c',
'varlink-io.systemd.PCRLock.c',
'varlink-io.systemd.Repart.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',

View File

@@ -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);

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_Signer;

View File

@@ -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);

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_Uploader;

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 -