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:
@@ -7,6 +7,7 @@ executables += [
|
||||
'sources' : files(
|
||||
'report.c',
|
||||
'report-generate.c',
|
||||
'report-sign.c',
|
||||
'report-upload.c',
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
+39
-16
@@ -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(¶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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user