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:
</varlistentry>
<varlistentry>
- <term><option>--sign=<replaceable>BOOL</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>
+ <term><option>--sign=<replaceable>MODE</replaceable></option></term>
+
+ <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>
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 {
#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;
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) {
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;
};
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++;
}
}
- 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;
int context_sign_report(
Context *context,
sd_json_variant *report,
+ ReportSignMode mode,
sd_json_format_flags_t format_flags,
FILE *output) {
int r;
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
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");
int context_sign_report_as_string(
Context *context,
sd_json_variant *report,
+ ReportSignMode mode,
sd_json_format_flags_t format_flags,
char **ret) {
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;
#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);
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 {
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;
#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"
#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"
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);
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,
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;
.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");
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;
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;
}
#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,
#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."),
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));
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."),