]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
ci: add test for the new logic
authorLennart Poettering <lennart@amutable.com>
Mon, 15 Jun 2026 08:39:40 +0000 (10:39 +0200)
committerLennart Poettering <lennart@amutable.com>
Fri, 19 Jun 2026 03:23:18 +0000 (05:23 +0200)
test/units/TEST-74-AUX-UTILS.report.sh

index 48bbc4841a8f555b63841947d9cc2a5943db9b72..f758d24030db5848756494ebc7aadf4d6f5a4c18 100755 (executable)
@@ -109,7 +109,9 @@ CERTDIR=$(mktemp -d)
 at_exit() {
     set +e
     systemctl stop fake-report-server fake-report-server-tls
-    rm -rf "$CERTDIR"
+    systemctl stop systemd-report.socket systemd-report-files.socket systemd-report-sign-plain.socket
+    rm -f /run/systemd/report.files/testreportfile /run/systemd/report.files/binaryfile
+    rm -rf "$CERTDIR" "${SIGN_WORK:-}"
 }
 trap at_exit EXIT
 
@@ -132,3 +134,108 @@ systemctl status fake-report-server-tls
 
 "$REPORT" upload --url=https://localhost:8090/ --key=- --trust="$CERTDIR/server.crt" \
           --extra-header='Authorization: Bearer magic string'
+
+# -----------------------------------------------------------------------------
+# Test the systemd-report-files@.service metric provider: files dropped into one
+# of the report.files directories are exposed verbatim as io.systemd.Files.*
+# metrics, keyed by their file name.
+mkdir -p /run/systemd/report.files
+REPORT_FILE_CONTENT="hello from the report files metric provider"
+printf '%s' "$REPORT_FILE_CONTENT" >/run/systemd/report.files/testreportfile
+
+systemctl start systemd-report-files.socket
+varlinkctl info /run/systemd/report/io.systemd.Files
+varlinkctl list-methods /run/systemd/report/io.systemd.Files
+
+# The metric value must be the exact file contents.
+files_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.Files io.systemd.Metrics.List {})"
+files_value() { echo "$files_metrics" | jq --seq -r "select(.name == \"$1\") | .value"; }
+[ "$(files_value io.systemd.Files.testreportfile)" = "$REPORT_FILE_CONTENT" ]
+
+# Describe must list the very same metric, of type string.
+files_describe="$(varlinkctl call --more /run/systemd/report/io.systemd.Files io.systemd.Metrics.Describe {})"
+echo "$files_describe" | jq --seq -r 'select(.name == "io.systemd.Files.testreportfile") | .type' | grep -wx string >/dev/null
+
+# Files that aren't valid UTF-8 text must be skipped silently (the value is a
+# JSON string, so only text files can be reported).
+printf '\xff\xfe binary garbage' >/run/systemd/report.files/binaryfile
+files_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.Files io.systemd.Metrics.List {})"
+[ -z "$(files_value io.systemd.Files.binaryfile)" ]
+rm -f /run/systemd/report.files/binaryfile
+
+# -----------------------------------------------------------------------------
+# Test report signing through the plain software backend, driven entirely via
+# the io.systemd.Report Varlink interface (the GenerateSigned method). The plain
+# backend is only built/installed with OpenSSL support, so skip if unavailable.
+if ! systemctl cat systemd-report-sign-plain.socket &>/dev/null; then
+    echo "systemd-report-sign-plain.socket is not installed, skipping report signing test."
+    exit 0
+fi
+
+systemctl start systemd-report.socket
+systemctl start systemd-report-sign-plain.socket
+
+SIGN_WORK="$(mktemp -d)"
+
+# Ask systemd-report to generate a *signed* report over Varlink. The reply
+# carries the signed report as base64-encoded JSON-SEQ data.
+varlinkctl call /run/systemd/io.systemd.Report io.systemd.Report.GenerateSigned \
+    '{"matches":["io.systemd.Manager.UnitsTotal"]}' | jq -r .reportData | base64 -d >"$SIGN_WORK/report.seq"
+
+# The first JSON-SEQ record is the report itself. This is exactly the byte
+# sequence that got signed, including the leading record separator (0x1e) and
+# the trailing newline, so 'head -n1' reproduces it verbatim.
+head -n1 "$SIGN_WORK/report.seq" >"$SIGN_WORK/message.bin"
+tr -d '\036' <"$SIGN_WORK/message.bin" | jq -e '.mediaType == "application/vnd.io.systemd.report"' >/dev/null
+
+# The remaining record(s) are signature objects. With only the plain backend
+# enabled there is exactly one, with mechanism "plain".
+sig_json="$(tail -n +2 "$SIGN_WORK/report.seq" | tr -d '\036')"
+[ "$(echo "$sig_json" | jq -r .mechanism)" = "plain" ]
+[ "$(echo "$sig_json" | jq -r .mediaType)" = "application/vnd.io.systemd.report.signature" ]
+
+# The sha256 recorded in the signature must match the digest of the report bytes.
+[ "$(echo "$sig_json" | jq -r .sha256)" = "$(sha256sum "$SIGN_WORK/message.bin" | cut -d' ' -f1)" ]
+
+# Extract the embedded public key and the raw Ed25519 signature.
+echo "$sig_json" | jq -r .data.key >"$SIGN_WORK/pub.pem"
+echo "$sig_json" | jq -r .data.signature | base64 -d >"$SIGN_WORK/sig.bin"
+
+# The signature is made over the SHA256 digest bytes directly (Ed25519, the
+# signer does not pre-hash), so verify the raw digest against the signature.
+openssl dgst -sha256 -binary "$SIGN_WORK/message.bin" >"$SIGN_WORK/digest.bin"
+openssl pkeyutl -verify -pubin -inkey "$SIGN_WORK/pub.pem" -rawin \
+    -in "$SIGN_WORK/digest.bin" -sigfile "$SIGN_WORK/sig.bin"
+
+# The public key embedded in the report must be the very key the plain backend
+# generated and persisted on disk.
+diff <(openssl pkey -pubin -in "$SIGN_WORK/pub.pem" -pubout) \
+     <(openssl pkey -pubin -in /var/lib/systemd/report.sign.plain/local.public -pubout)
+
+# A tampered report must no longer verify against the signature.
+printf 'x' >>"$SIGN_WORK/message.bin"
+openssl dgst -sha256 -binary "$SIGN_WORK/message.bin" >"$SIGN_WORK/digest-bad.bin"
+if openssl pkeyutl -verify -pubin -inkey "$SIGN_WORK/pub.pem" -rawin \
+    -in "$SIGN_WORK/digest-bad.bin" -sigfile "$SIGN_WORK/sig.bin"; then
+    echo "Signature verification unexpectedly succeeded for tampered report" >&2
+    exit 1
+fi
+
+# -----------------------------------------------------------------------------
+# The plain backend also acts as a metric provider, exposing its public signing
+# key as the io.systemd.Report.SignPlain.PublicKey metric. This is served off the
+# very same socket as the signer, reachable in the metrics directory under
+# /run/systemd/report/ (the signer endpoint is just a symlink into the root-only
+# /run/systemd/report.sign/ directory pointing at the same socket).
+varlinkctl info /run/systemd/report/io.systemd.Report.SignPlain
+varlinkctl list-methods /run/systemd/report/io.systemd.Report.SignPlain
+
+# The exposed public key must be the very key persisted on disk.
+sign_metrics="$(varlinkctl call --more /run/systemd/report/io.systemd.Report.SignPlain io.systemd.Metrics.List {})"
+echo "$sign_metrics" | jq --seq -r 'select(.name == "io.systemd.Report.SignPlain.PublicKey") | .value' >"$SIGN_WORK/metric-pub.pem"
+diff <(openssl pkey -pubin -in "$SIGN_WORK/metric-pub.pem" -pubout) \
+     <(openssl pkey -pubin -in /var/lib/systemd/report.sign.plain/local.public -pubout)
+
+# Describe must list the very same metric, of type string.
+varlinkctl call --more /run/systemd/report/io.systemd.Report.SignPlain io.systemd.Metrics.Describe {} |
+    jq --seq -r 'select(.name == "io.systemd.Report.SignPlain.PublicKey") | .type' | grep -wx string >/dev/null