diff --git a/man/systemd-report.xml b/man/systemd-report.xml index 560d21c3479..2c3ff710eeb 100644 --- a/man/systemd-report.xml +++ b/man/systemd-report.xml @@ -197,6 +197,19 @@ + + + + + If enabled, the report generated by generate or uploaded by + upload 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 + io.systemd.Report.Signer.Sign() on any sockets found under + /run/systemd/report.sign/. Defaults to off. + + + diff --git a/src/libsystemd/sd-varlink/test-varlink-idl.c b/src/libsystemd/sd-varlink/test-varlink-idl.c index 65407415fb2..0eda9287636 100644 --- a/src/libsystemd/sd-varlink/test-varlink-idl.c +++ b/src/libsystemd/sd-varlink/test-varlink-idl.c @@ -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, diff --git a/src/report/meson.build b/src/report/meson.build index e64f74ee4ef..5c6cc416305 100644 --- a/src/report/meson.build +++ b/src/report/meson.build @@ -7,6 +7,7 @@ executables += [ 'sources' : files( 'report.c', 'report-generate.c', + 'report-sign.c', 'report-upload.c', ), }, diff --git a/src/report/report-generate.c b/src/report/report-generate.c index edac44a0eaa..69c58e715fd 100644 --- a/src/report/report-generate.c +++ b/src/report/report-generate.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; } diff --git a/src/report/report-sign.c b/src/report/report-sign.c new file mode 100644 index 00000000000..150547dcf47 --- /dev/null +++ b/src/report/report-sign.c @@ -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; +} diff --git a/src/report/report-sign.h b/src/report/report-sign.h new file mode 100644 index 00000000000..c5264252b91 --- /dev/null +++ b/src/report/report-sign.h @@ -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); diff --git a/src/report/report-upload.c b/src/report/report-upload.c index 17ef327db01..e21c0044e63 100644 --- a/src/report/report-upload.c +++ b/src/report/report-upload.c @@ -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); +} diff --git a/src/report/report.c b/src/report/report.c index c3af61f01dd..242cd97befa 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -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) diff --git a/src/report/report.h b/src/report/report.h index 51d55344f96..0b8873a4ee4 100644 --- a/src/report/report.h +++ b/src/report/report.h @@ -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, diff --git a/src/shared/meson.build b/src/shared/meson.build index 9359b4fee0e..0dd733100ab 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -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', diff --git a/src/shared/varlink-io.systemd.Report.Signer.c b/src/shared/varlink-io.systemd.Report.Signer.c new file mode 100644 index 00000000000..4c40285b46b --- /dev/null +++ b/src/shared/varlink-io.systemd.Report.Signer.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); diff --git a/src/shared/varlink-io.systemd.Report.Signer.h b/src/shared/varlink-io.systemd.Report.Signer.h new file mode 100644 index 00000000000..f9a918dbc53 --- /dev/null +++ b/src/shared/varlink-io.systemd.Report.Signer.h @@ -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; diff --git a/src/shared/varlink-io.systemd.Report.Uploader.c b/src/shared/varlink-io.systemd.Report.Uploader.c new file mode 100644 index 00000000000..ace9108b2e0 --- /dev/null +++ b/src/shared/varlink-io.systemd.Report.Uploader.c @@ -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); diff --git a/src/shared/varlink-io.systemd.Report.Uploader.h b/src/shared/varlink-io.systemd.Report.Uploader.h new file mode 100644 index 00000000000..19e1c78314a --- /dev/null +++ b/src/shared/varlink-io.systemd.Report.Uploader.h @@ -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; diff --git a/tmpfiles.d/systemd.conf.in b/tmpfiles.d/systemd.conf.in index f601cb87f9d..786b86d0069 100644 --- a/tmpfiles.d/systemd.conf.in +++ b/tmpfiles.d/systemd.conf.in @@ -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 -