From: Paul Meyer Date: Fri, 17 Jul 2026 14:53:50 +0000 (+0200) Subject: report: replace boolean --sign with signing modes X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=156fb14b6a71ec083af2647fbd3b4950c28732e1;p=thirdparty%2Fsystemd.git report: replace boolean --sign with signing modes Turn --sign=BOOL into --sign=no|best-effort|require-one|require-all, making the multi-signer aggregation policy explicit: best-effort never fails on signing, require-one requires at least one signature, and require-all requires every signer to succeed (an empty reply, i.e. a signer opting out, counts as failure). Signed reports are always emitted as a JSON-SEQ stream. The mode is also exposed as an input to the io.systemd.Report.GenerateSigned Varlink method. Signed-off-by: Paul Meyer --- diff --git a/NEWS b/NEWS index efa5d46419f..c810f22c932 100644 --- a/NEWS +++ b/NEWS @@ -107,6 +107,19 @@ CHANGES WITH 262: upgraded, and the existing "anchor secret" stored in /var/lib and the ESP/XBOOTLDR will be removed. + Changes in systemd-report: + + * The "generate" and "upload" commands of systemd-report can now + cryptographically sign the reports they produce. Signatures are + acquired from backend Varlink signers linked into + /run/systemd/report.sign/. The new "--sign=" option selects the + signing policy: "no" (the default) emits an unsigned report; + "best-effort" attaches whatever signatures can be acquired but never + fails; "require-one" requires at least one signature; and + "require-all" requires every available signer to succeed. A signed + report is emitted as a JSON-SEQ stream: the report object followed by + one signature object per acquired signature. + CHANGES WITH 261: Announcements of Future Feature Removals and Incompatible Changes: diff --git a/man/systemd-report.xml b/man/systemd-report.xml index 2c3ff710eeb..f3d21526efb 100644 --- a/man/systemd-report.xml +++ b/man/systemd-report.xml @@ -199,14 +199,56 @@ - - - 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. + + + Controls whether and how the report generated by generate or + uploaded by upload is cryptographically signed. Signatures are acquired by calling + io.systemd.Report.Signer.Sign() on each socket found under + /run/systemd/report.sign/. + + When signing is requested (any value other than ) the report is emitted as a + JSON-SEQ stream: the report object comes first, followed by one signature object per acquired signature + (possibly none, in mode), each covering the precise binary representation + of the report object. With the report is emitted as a single plain JSON object. + Takes one of the following values: + + + + + Do not sign the report; it is emitted as a single plain JSON object. This is the + default. + + + + + + + Never fail because of signing. Any acquired signatures are included. The operation + succeeds even if none could be acquired, in which case the JSON-SEQ stream simply contains no + signature objects. + + + + + + + Require at least one signature. Individual signers that fail or return no signature + are tolerated, but the operation fails if not a single signature could be acquired. + + + + + + + Require every signer to succeed with at least one signature. The operation fails + if no signer is present, if any signer returns an error, or if any signer returns no signature. + + + + + + + Defaults to . diff --git a/src/report/report-generate.c b/src/report/report-generate.c index 7dc46bf2188..06a05bd565b 100644 --- a/src/report/report-generate.c +++ b/src/report/report-generate.c @@ -51,8 +51,8 @@ int context_generate_report(Context *context) { if (r < 0) return r; - if (arg_sign) { - r = context_sign_report(context, report, arg_json_format_flags, /* output= */ NULL); + if (arg_sign_mode != REPORT_SIGN_NO) { + r = context_sign_report(context, report, arg_sign_mode, arg_json_format_flags, /* output= */ NULL); if (r < 0) return r; } else { diff --git a/src/report/report-sign.c b/src/report/report-sign.c index aabf2a55833..27665fe25f6 100644 --- a/src/report/report-sign.c +++ b/src/report/report-sign.c @@ -12,12 +12,22 @@ #include "report.h" #include "report-sign.h" #include "sha256.h" +#include "string-table.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 +static const char* const report_sign_mode_table[_REPORT_SIGN_MODE_MAX] = { + [REPORT_SIGN_NO] = "no", + [REPORT_SIGN_BEST_EFFORT] = "best-effort", + [REPORT_SIGN_REQUIRE_ONE] = "require-one", + [REPORT_SIGN_REQUIRE_ALL] = "require-all", +}; + +DEFINE_STRING_TABLE_LOOKUP(report_sign_mode, ReportSignMode); + typedef struct Signature { char *mechanism; sd_json_variant *data; @@ -26,7 +36,11 @@ typedef struct Signature { typedef struct SignatureList { Signature *signatures; size_t n_signatures; - int result; + + size_t n_replies; /* replies received (one per contacted socket) */ + size_t n_errors; /* replies that were errors */ + size_t n_empty; /* replies OK but with zero signatures (opt-out) */ + int fatal_error; /* first unrecoverable errno (e.g. OOM); fails every mode */ } SignatureList; static void signature_done(Signature *s) { @@ -58,18 +72,23 @@ static int execute_dir_reply( assert(link); + sl->n_replies++; + /* 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 (r < 0) { + log_warning_errno(r, "Failed to extract service name from '%s': %m", p); + sl->n_errors++; + return 0; + } 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); + log_warning("Signing via Varlink service '%s' failed: %s", p, error_id); + sl->n_errors++; + return 0; } _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL; @@ -80,21 +99,28 @@ static int execute_dir_reply( }; r = sd_json_dispatch(reply, dispatch_table, /* flags= */ 0, &array); - if (r < 0) - return log_error_errno(r, "Failed to dispatch method reply: %m"); + if (r < 0) { + log_warning_errno(r, "Failed to parse reply from Varlink service '%s': %m", p); + sl->n_errors++; + return 0; + } 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(); + if (!GREEDY_REALLOC(sl->signatures, sl->n_signatures + 1)) { + RET_GATHER(sl->fatal_error, log_oom()); + return 0; + } Signature *i = sl->signatures + sl->n_signatures; i->mechanism = strdup(sn); - if (!i->mechanism) - return log_oom(); + if (!i->mechanism) { + RET_GATHER(sl->fatal_error, log_oom()); + return 0; + } i->data = sd_json_variant_ref(s); sl->n_signatures++; @@ -103,9 +129,10 @@ static int execute_dir_reply( } } - if (n == 0) + if (n == 0) { + sl->n_empty++; log_info("Mechanism '%s' succeeded, but returned no signatures.", p); - else + } else log_info("Successfully acquired %zu signatures from '%s'", n, p); return 0; @@ -114,6 +141,7 @@ static int execute_dir_reply( int context_sign_report( Context *context, sd_json_variant *report, + ReportSignMode mode, sd_json_format_flags_t format_flags, FILE *output) { int r; @@ -121,6 +149,9 @@ int context_sign_report( assert(context); assert(report); + if (mode == REPORT_SIGN_NO) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing to sign report with signing disabled."); + /* 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 @@ -156,17 +187,38 @@ int context_sign_report( REPORT_SIGN_TIMEOUT_USEC, execute_dir_reply, /* userdata= */ &sl); - if (jobs < 0) + /* A missing signer directory (-ENOENT) just means no signing backends are installed; that is benign + * and surfaces below as "no signatures acquired". Any other enumeration failure is fatal. */ + if (jobs < 0 && jobs != -ENOENT) 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 (sl.fatal_error < 0) + return log_error_errno(sl.fatal_error, "Failed to collect signatures: %m"); + + switch (mode) { + case REPORT_SIGN_REQUIRE_ALL: + if (sl.n_errors > 0) + return log_error_errno(SYNTHETIC_ERRNO(EIO), + "Signing mode '%s' requested, but %zu of %zu signing mechanisms failed.", + report_sign_mode_to_string(mode), sl.n_errors, sl.n_replies); + if (sl.n_empty > 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), + "Signing mode '%s' requested, but %zu of %zu signing mechanisms produced no signature.", + report_sign_mode_to_string(mode), sl.n_empty, sl.n_replies); + _fallthrough_; + case REPORT_SIGN_REQUIRE_ONE: + if (sl.n_signatures == 0) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), + "Signing mode '%s' requested, but no signatures could be acquired via '%s'.", + report_sign_mode_to_string(mode), REPORT_SIGN_DIR); + break; + + case REPORT_SIGN_BEST_EFFORT: + break; /* never fails; may emit zero signatures */ + + default: + assert_not_reached(); + } if (fputs(text, output) == EOF) return log_error_errno(errno, "Failed to write report: %m"); @@ -202,6 +254,7 @@ int context_sign_report( int context_sign_report_as_string( Context *context, sd_json_variant *report, + ReportSignMode mode, sd_json_format_flags_t format_flags, char **ret) { @@ -217,7 +270,7 @@ int context_sign_report_as_string( if (!f) return log_oom(); - r = context_sign_report(context, report, format_flags, f); + r = context_sign_report(context, report, mode, format_flags, f); if (r < 0) return r; diff --git a/src/report/report-sign.h b/src/report/report-sign.h index c5264252b91..e219fa6cec7 100644 --- a/src/report/report-sign.h +++ b/src/report/report-sign.h @@ -3,6 +3,18 @@ #include "report.h" -int context_sign_report(Context *context, sd_json_variant *report, sd_json_format_flags_t format_flags, FILE *output); +DECLARE_STRING_TABLE_LOOKUP(report_sign_mode, ReportSignMode); -int context_sign_report_as_string(Context *context, sd_json_variant *report, sd_json_format_flags_t format_flags, char **ret); +int context_sign_report( + Context *context, + sd_json_variant *report, + ReportSignMode mode, + sd_json_format_flags_t format_flags, + FILE *output); + +int context_sign_report_as_string( + Context *context, + sd_json_variant *report, + ReportSignMode mode, + sd_json_format_flags_t format_flags, + char **ret); diff --git a/src/report/report-upload.c b/src/report/report-upload.c index 4178e8de33b..bb694301da3 100644 --- a/src/report/report-upload.c +++ b/src/report/report-upload.c @@ -68,11 +68,12 @@ static int http_upload_report(Context *context, sd_json_variant *report) { if (r < 0) return r; - /* Upload a JSON report in text form as a single JSON object, instead of a JSON-SEQ list. */ + /* Upload the report in text form: a single JSON object when unsigned, or a JSON-SEQ stream (the + * report followed by zero or more signature objects) when signing is enabled. */ _cleanup_free_ char *text = NULL; - if (arg_sign) { - r = context_sign_report_as_string(context, report, /* format_flags= */ 0, &text); + if (arg_sign_mode != REPORT_SIGN_NO) { + r = context_sign_report_as_string(context, report, arg_sign_mode, /* format_flags= */ 0, &text); if (r < 0) return r; } else { @@ -232,10 +233,10 @@ static int varlink_upload_report(Context *context, sd_json_variant *report) { assert(report); _cleanup_(sd_json_variant_unrefp) sd_json_variant *params = NULL; - if (arg_sign) { + if (arg_sign_mode != REPORT_SIGN_NO) { _cleanup_free_ char *buf = NULL; - r = context_sign_report_as_string(context, report, /* format_flags= */ 0, &buf); + r = context_sign_report_as_string(context, report, arg_sign_mode, /* format_flags= */ 0, &buf); if (r < 0) return r; diff --git a/src/report/report.c b/src/report/report.c index 8b756e0c2d2..82559fff38e 100644 --- a/src/report/report.c +++ b/src/report/report.c @@ -10,6 +10,7 @@ #include "dlopen-note.h" #include "format-table.h" #include "help-util.h" +#include "json-util.h" #include "log.h" #include "main-func.h" #include "options.h" @@ -23,6 +24,7 @@ #include "runtime-scope.h" #include "set.h" #include "sort-util.h" +#include "string-table.h" #include "string-util.h" #include "strv.h" #include "time-util.h" @@ -46,7 +48,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; +ReportSignMode arg_sign_mode = REPORT_SIGN_NO; STATIC_DESTRUCTOR_REGISTER(arg_url, freep); STATIC_DESTRUCTOR_REGISTER(arg_key, freep); @@ -779,6 +781,28 @@ static int verb_list_sources(int argc, char *argv[], uintptr_t _data, void *user return 0; } +/* String table mapping the io.systemd.Report SignMode enum values (camelCase, per Varlink conventions) onto + * ReportSignMode. This is an explicit allowlist of the modes valid for GenerateSigned: unlike the CLI's + * report_sign_mode_from_string() it deliberately omits "no" (use the Generate method for unsigned reports). */ +static const char* const report_sign_varlink_mode_table[_REPORT_SIGN_MODE_MAX] = { + [REPORT_SIGN_BEST_EFFORT] = "bestEffort", + [REPORT_SIGN_REQUIRE_ONE] = "requireOne", + [REPORT_SIGN_REQUIRE_ALL] = "requireAll", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(report_sign_varlink_mode, ReportSignMode); + +static JSON_DISPATCH_ENUM_DEFINE(json_dispatch_report_sign_varlink_mode, ReportSignMode, report_sign_varlink_mode_from_string); + +typedef struct GenerateParameters { + char **matches; + ReportSignMode sign_mode; +} GenerateParameters; + +static void generate_parameters_done(GenerateParameters *p) { + strv_free(p->matches); +} + static int vl_method_generate_internal( sd_varlink *link, sd_json_variant *parameters, @@ -789,14 +813,21 @@ static int vl_method_generate_internal( assert(link); assert(parameters); - _cleanup_strv_free_ char **input_matches = NULL; + _cleanup_(generate_parameters_done) GenerateParameters p = { + .sign_mode = _REPORT_SIGN_MODE_INVALID, + }; - static const sd_json_dispatch_field dispatch_table[] = { - { "matches", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, 0, 0 }, + static const sd_json_dispatch_field dispatch_table_unsigned[] = { + { "matches", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, voffsetof(p, matches), SD_JSON_NULLABLE }, + {} + }; + static const sd_json_dispatch_field dispatch_table_signed[] = { + { "matches", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, voffsetof(p, matches), SD_JSON_NULLABLE }, + { "mode", SD_JSON_VARIANT_STRING, json_dispatch_report_sign_varlink_mode, voffsetof(p, sign_mode), SD_JSON_NULLABLE }, {} }; - r = sd_varlink_dispatch(link, parameters, dispatch_table, &input_matches); + r = sd_varlink_dispatch(link, parameters, sign ? dispatch_table_signed : dispatch_table_unsigned, &p); if (r != 0) return r; @@ -804,7 +835,7 @@ static int vl_method_generate_internal( .action = ACTION_GENERATE, }; - r = parse_metrics_matches(input_matches, &context.matches); + r = parse_metrics_matches(p.matches, &context.matches); if (r < 0) return sd_varlink_error_invalid_parameter_name(link, "matches"); @@ -822,10 +853,14 @@ static int vl_method_generate_internal( return r; if (sign) { + /* Supply the default when 'mode' was absent. */ + if (p.sign_mode < 0) + p.sign_mode = REPORT_SIGN_REQUIRE_ONE; + /* 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); + r = context_sign_report_as_string(&context, report, p.sign_mode, /* format_flags= */ 0, &s); if (r < 0) return r; @@ -1007,10 +1042,11 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) { 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; + OPTION_LONG("sign", "MODE", + "Sign the report: no, best-effort, require-one, require-all"): + arg_sign_mode = report_sign_mode_from_string(opts.arg); + if (arg_sign_mode < 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse --sign= mode '%s'.", opts.arg); break; } diff --git a/src/report/report.h b/src/report/report.h index 8d1c687c954..4ed1e8fab65 100644 --- a/src/report/report.h +++ b/src/report/report.h @@ -9,11 +9,20 @@ #define REPORT_CERT_FILE CERTIFICATE_ROOT "/certs/systemd-report.pem" #define REPORT_TRUST_FILE CERTIFICATE_ROOT "/ca/trusted.pem" +typedef enum ReportSignMode { + REPORT_SIGN_NO, + REPORT_SIGN_BEST_EFFORT, + REPORT_SIGN_REQUIRE_ONE, + REPORT_SIGN_REQUIRE_ALL, + _REPORT_SIGN_MODE_MAX, + _REPORT_SIGN_MODE_INVALID = -EINVAL, +} ReportSignMode; + 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; +extern ReportSignMode arg_sign_mode; typedef enum Action { ACTION_LIST_METRICS, diff --git a/src/shared/varlink-io.systemd.Report.c b/src/shared/varlink-io.systemd.Report.c index 38f2f780bd7..1b936fd5f85 100644 --- a/src/shared/varlink-io.systemd.Report.c +++ b/src/shared/varlink-io.systemd.Report.c @@ -2,6 +2,15 @@ #include "varlink-io.systemd.Report.h" +static SD_VARLINK_DEFINE_ENUM_TYPE( + SignMode, + SD_VARLINK_FIELD_COMMENT("Emit whatever signatures are available, never fail on signing."), + SD_VARLINK_DEFINE_ENUM_VALUE(bestEffort), + SD_VARLINK_FIELD_COMMENT("Require at least one signature; tolerate individual signer failures. (Default.)"), + SD_VARLINK_DEFINE_ENUM_VALUE(requireOne), + SD_VARLINK_FIELD_COMMENT("Require every signing mechanism to succeed with a signature."), + SD_VARLINK_DEFINE_ENUM_VALUE(requireAll)); + 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."), @@ -13,6 +22,8 @@ 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("How to handle multiple signers. Defaults to requireOne if unset."), + SD_VARLINK_DEFINE_INPUT_BY_TYPE(mode, SignMode, 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)); @@ -20,6 +31,8 @@ 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("Policy for how multiple signers are aggregated."), + &vl_type_SignMode, 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."),