]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
report: replace boolean --sign with signing modes
authorPaul Meyer <katexochen0@gmail.com>
Fri, 17 Jul 2026 14:53:50 +0000 (16:53 +0200)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Sun, 2 Aug 2026 06:13:35 +0000 (15:13 +0900)
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>
NEWS
man/systemd-report.xml
src/report/report-generate.c
src/report/report-sign.c
src/report/report-sign.h
src/report/report-upload.c
src/report/report.c
src/report/report.h
src/shared/varlink-io.systemd.Report.c

diff --git a/NEWS b/NEWS
index efa5d46419fde97c182b88ff8d33aec3f42feba5..c810f22c93215a2259478c6c5fe455e9e48ac8dc 100644 (file)
--- 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:
index 2c3ff710eeb8710dd46bc48f57b61df4a9a725d7..f3d21526efb8c2248123389c3b9501d1e6c4d68d 100644 (file)
       </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>
index 7dc46bf2188d0707241a92a596f90c8f2a4cdd9f..06a05bd565bc93eefa42125589eb8e82e9f0380a 100644 (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 {
index aabf2a558335a6b1d2b5d0e883ebdbd595308581..27665fe25f6f7f7f832799a0c5705af05b580417 100644 (file)
 #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;
 
index c5264252b9128c6b141f44fe835da6b3de7d4bfd..e219fa6cec7ef34dc4cf888968fd23002bb891b5 100644 (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);
index 4178e8de33b53cc80a933d6671c19e9a2cf1ebda..bb694301da350d55af7f9efd965ccad2168787e5 100644 (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;
 
index 8b756e0c2d2829811bf5e1bb7626dc71056babe2..82559fff38efdbea7cbbd5e02cf1c1946ae00eca 100644 (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;
                 }
 
index 8d1c687c954593567deedd98fc9dee840ea62ca0..4ed1e8fab65134299bc1cbc20e4bf2f56293ae5b 100644 (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,
index 38f2f780bd7fc765438973e1551de8d88683fe6b..1b936fd5f850e15abf2ad7523b48c648eb6e768e 100644 (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."),