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 <katexochen0@gmail.com>
This commit is contained in:
Paul Meyer
2026-08-02 15:13:35 +09:00
committed by Yu Watanabe
parent bc9870a923
commit 156fb14b6a
9 changed files with 232 additions and 53 deletions
+13
View File
@@ -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:
+49 -7
View File
@@ -199,14 +199,56 @@
</varlistentry>
<varlistentry>
<term><option>--sign=<replaceable>BOOL</replaceable></option></term>
<term><option>--sign=<replaceable>MODE</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>
<listitem><para>Controls whether and how the report generated by <command>generate</command> or
uploaded by <command>upload</command> is cryptographically signed. Signatures are acquired by calling
<function>io.systemd.Report.Signer.Sign()</function> on each socket found under
<filename>/run/systemd/report.sign/</filename>.</para>
<para>When signing is requested (any value other than <option>no</option>) 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 <option>best-effort</option> mode), each covering the precise binary representation
of the report object. With <option>no</option> the report is emitted as a single plain JSON object.
Takes one of the following values:</para>
<variablelist>
<varlistentry>
<term><option>no</option></term>
<listitem><para>Do not sign the report; it is emitted as a single plain JSON object. This is the
default.</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
<varlistentry>
<term><option>best-effort</option></term>
<listitem><para>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.</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
<varlistentry>
<term><option>require-one</option></term>
<listitem><para>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.</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
<varlistentry>
<term><option>require-all</option></term>
<listitem><para>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.
</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
</variablelist>
<para>Defaults to <option>no</option>.</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
+2 -2
View File
@@ -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 {
+78 -25
View File
@@ -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;
+14 -2
View File
@@ -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);
+6 -5
View File
@@ -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;
+47 -11
View File
@@ -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;
}
+10 -1
View File
@@ -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,
+13
View File
@@ -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."),