From: Lennart Poettering Date: Mon, 15 Jun 2026 08:39:40 +0000 (+0200) Subject: ci: add test for the new logic X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9ebbbfd00e5da549f72813f1436c280180a21bf8;p=thirdparty%2Fsystemd.git ci: add test for the new logic --- diff --git a/test/units/TEST-74-AUX-UTILS.report.sh b/test/units/TEST-74-AUX-UTILS.report.sh index 48bbc4841a8..f758d24030d 100755 --- a/test/units/TEST-74-AUX-UTILS.report.sh +++ b/test/units/TEST-74-AUX-UTILS.report.sh @@ -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