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-19 05:23:18 +02:00
parent 0d9026c736
commit 3c2f7c6002
15 changed files with 369 additions and 19 deletions
+1
View File
@@ -7,6 +7,7 @@ executables += [
'sources' : files(
'report.c',
'report-generate.c',
'report-sign.c',
'report-upload.c',
),
},
+16 -3
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
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);
+39 -16
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);
}
+7
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)
+1
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,