report: add support for signing reports via varlink backends + make report a varlink service (#42595)
This adds the following: 1. systemd-report gains a new --sign= option, taking a boolean. If true, this makes systemd-report generate + systemd-report upload generate a signed report, instead of a regular one. The signatures are collected from Varlink-based backends. 2. One such backend is added which does a simple Ed21159 based signing scheme. 3. this adds a new metrics source which just reports text files symlinked into a special dir as metrics. This is used to report the Ed21159 public key as metric, by default, if it exists. 4. finally, systemd-report itself is turned into a varlink service. this is useful for example for extracting a report from a system coming in via the varlink/http bridge. I thought a long time about the format of signing of reports. Initially i intended to do this like homed's user record signing, i.e. require normalization of the record, then normalize the record, and write it out in dense form, since the result. Finally insert the resulting hash into the user record itself. People have pointed me to the inherent messiness of signing JSON this way though, as it requires any participant that wishes to sign/authenticate records this way to implement the exact same normalization/formatting rules, and in particular in the area of floating point numbers (of which metrics presumably will have many) this is quite problematic. This signing hence goes a different way. instead of expecting signer+verifier to independently come to the same normalized text form of the json data, let's instead output a JSON-SEQ sequence, where the first object is the report, and any subsequent objects are one signature each. the signatures are supposed to cover the precise binary representation of the first element in the JSON-SEQ stream. (i.e. from the RS to the NL). or in other words: a verifier would receive the JSON-SEQ stream, split it up before each RS. Then it would leave object 1 unparsed for the moment, and parse objects 2…n. It would then authenticate object 1's precise binary representation with objects 2…n. Once that checks out, it would parse object 1, and use it as report.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user