test: verify snp attestation report

Sign a system report through the io.systemd.Report pipeline in the
SNP guest and export the resulting signed report to the host. The host
takes the role of a remote verifier: the tsm signature record must carry
the digest of the signed report bytes, that digest must be embedded in
the REPORT_DATA field of the enclosed SNP attestation report, and the
attestation report must verify against the AMD certificate chain with
snpguest, which is added to the Fedora test image. Certificates must be
pre-provisioned on the host in a sirectory named 'snp-certs' and bound
into the test container via $TEST_BIND_DIRS.

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
This commit is contained in:
Paul Meyer committed 2026-10-05 13:14:36 +02:00
1 parent 7471c5b02d
commit 36a0a64017
6 files changed
+111 -1

No files matched your search

+2
View File
@@ -16,4 +16,6 @@ Packages=
kernel-modules-internal
rpmautospec
scsi-target-utils
# Required for TEST-94-COCO (verifying SNP attestation reports)
snpguest
util-linux-script
+11
View File
@@ -179,6 +179,17 @@ meson test
└─ guest-test-runner.sh
```
Verifying SNP attestation reports requires the AMD certificate chain, fetched
once on the host into a directory named `snp-certs` and passed to the test via
`TEST_BIND_DIRS=/path/to/snp-certs`. The VCEK is per-machine and derived from
an attestation report of this host, e.g. one generated with `snpguest report`
in a manually booted SNP guest:
```shell
$ snpguest fetch ca pem snp-certs <processor-model>
$ snpguest fetch vcek pem snp-certs <attestation-report>
```
## Running the integration tests without building systemd from source
If you want to run the integration tests against prebuilt systemd packages,
+6 -1
View File
@@ -32,14 +32,19 @@ trap at_exit EXIT
# - creds_vmspawn: credential transport by vmspawn is working (via initrd for SNP)
# - creds_cmdline: credential transport via cmdline is working (cmdline is measured on SNP)
# - tsm_signer_varlink_snp: the tsm report signer yields an SNP attestation report echoing the digest
# - report_pipeline: signing a report via io.systemd.Report; the result is verified below, host-side
vmspawn_boot_coco "$MACHINE-honest" "$COCO_TYPE" "$WORKDIR/honest" \
'detect_virt creds_vmspawn creds_cmdline tsm_signer_varlink_snp' \
'detect_virt creds_vmspawn creds_cmdline tsm_signer_varlink_snp report_pipeline' \
--image="$IMAGE_DIR/image.raw" \
--linux="$IMAGE_DIR/image.vmlinuz" \
--initrd="$IMAGE_DIR/image.initrd" \
--set-credential="$COCO_CRED_TRUSTED_ID:$COCO_CRED_TRUSTED_VALUE" \
selinux=0 systemd.firstboot=no rw \
"systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE"
# The report_pipeline check exported the signed report; verify it as a real verifier would.
test -s "$WORKDIR/honest/artifacts/$COCO_ARTIFACT_SIGNED_REPORT"
coco_verify_snp_signed_report "$WORKDIR/honest/artifacts/$COCO_ARTIFACT_SIGNED_REPORT" "$WORKDIR/honest"
echo "SEV-SNP direct-boot guest correctly reported confidential virtualization and trusted credentials"
# Offensive test:
+5
View File
@@ -15,6 +15,11 @@ fi
# Name of the guest self-check unit lib.sh injects.
COCO_GUEST_UNIT="coco-guest.service"
# Artifacts the guest checks export for host-side verification, by file name:
# written to $COCO_ARTIFACTS_DIR by the guest (see guest-test-runner.sh)
# collected into WORKDIR/artifacts on the host (see lib.sh)
COCO_ARTIFACT_SIGNED_REPORT="signed-report.seq"
# Credentials by channel
COCO_CRED_TRUSTED_ID="coco.trusted"
COCO_CRED_TRUSTED_VALUE="trusted-via-vmspawn"
+17
View File
@@ -131,3 +131,20 @@ testcase_coco_tsm_signer_varlink_snp() {
test -d /sys/kernel/config/tsm/report
assert_eq "$(ls -A /sys/kernel/config/tsm/report)" ""
}
# Sign a real system report through the io.systemd.Report pipeline and export the result (the
# report plus its signature records) for the host to check, as a real verifier would. No assertions
# here beyond a produced document: what the signatures must prove is decided on the untrusted side.
testcase_coco_report_pipeline() {
local report_seq="${COCO_ARTIFACTS_DIR:?}/$COCO_ARTIFACT_SIGNED_REPORT"
systemctl start systemd-report.socket
# The reply carries the signed report as base64-encoded JSON-SEQ: the first record is the
# report itself, each further record is one signer backend's signature.
varlinkctl call /run/systemd/io.systemd.Report io.systemd.Report.GenerateSigned \
'{"matches":["io.systemd.Manager.UnitsTotal"]}' |
jq -er .reportData | base64 -d >"$report_seq"
test -s "$report_seq"
}
+70
View File
@@ -9,6 +9,9 @@ if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then
exit 1
fi
# util.sh (assert_*) lives one level up in the shared units dir.
# shellcheck source=test/units/util.sh
. "$(dirname "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")")/util.sh"
# shellcheck source=test/units/TEST-94-COCO/fixtures.sh
. "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/fixtures.sh"
@@ -78,6 +81,73 @@ _coco_collect_artifacts() {
done <"$results"
}
# coco_verify_snp_report REPORT
# Verify the SNP attestation report REPORT with snpguest: the report parses, the AMD certificate
# chain (ARK -> ASK -> VCEK) is valid, and the report is signed by the VCEK. The certificates must
# be pre-provisioned in a host directory named 'snp-certs', bound in via $TEST_BIND_DIRS: the
# harness runs the container without network, and the AMD KDS rate-limits hard enough that
# fetching them per test run would be flaky anyway.
coco_verify_snp_report() {
local report="${1:?}"
local certs_dir=/work/snp-certs
# snpguest is part of the test image (see mkosi.conf.d/fedora): its absence is a broken image,
# not a reason to silently skip the verification.
if ! command -v snpguest >/dev/null; then
echo "snpguest is not installed, cannot verify the SNP attestation report" >&2
return 1
fi
snpguest display report "$report"
if [[ ! -d "$certs_dir" ]]; then
echo "no AMD certificates at $certs_dir; fetch them once on the host" \
"(snpguest fetch ca/vcek) and pass the directory via \$TEST_BIND_DIRS" >&2
return 1
fi
snpguest verify certs "$certs_dir"
snpguest verify attestation "$certs_dir" "$report"
}
# coco_verify_snp_signed_report REPORT_SEQ WORKDIR
# Check a signed report produced by io.systemd.Report's GenerateSigned the way a remote verifier
# would: the tsm signature record must carry the digest of the signed report bytes, that digest must
# be embedded in the REPORT_DATA field of the enclosed SNP attestation report, and the attestation
# report must verify against the AMD certificate chain (see coco_verify_snp_report).
coco_verify_snp_signed_report() {
local report_seq="${1:?}" workdir="${2:?}"
local message="$workdir/message.bin"
local report_file="$workdir/attestation-report.bin"
local sig digest report
# The first JSON-SEQ record is the report itself: exactly the byte sequence that got signed,
# including the leading record separator (0x1e) and the trailing newline, so 'head -n1'
# reproduces it verbatim. Each further record is one signer backend's signature.
head -n1 "$report_seq" >"$message"
tr -d '\036' <"$message" | jq -e '.mediaType == "application/vnd.io.systemd.report"' >/dev/null
digest="$(sha256sum "$message" | cut -d' ' -f1)"
# Pick the tsm backend's signature record (other backends may be enabled too).
sig="$(tail -n +2 "$report_seq" | tr -d '\036' | jq -c 'select(.mechanism == "tsm")')"
test -n "$sig"
assert_eq "$(jq -r .mediaType <<<"$sig")" "application/vnd.io.systemd.report.signature"
assert_eq "$(jq -r .sha256 <<<"$sig")" "$digest"
assert_eq "$(jq -r '.data.provider' <<<"$sig")" "sev_guest"
jq -r '.data.outblob' <<<"$sig" | base64 -d >"$report_file"
# The attestation report must bind the report digest, not some other value. Hex-encode the
# report so fields can be sliced by string offset (2 hex chars per byte).
report="$(od -An -vtx1 <"$report_file" | tr -d ' \n')"
assert_eq "$((${#report} / 2))" "1184"
# REPORT_DATA sits at byte offset 0x50: the digest, zero-padded to 64 bytes.
assert_eq "${report:2*0x50:64}" "$digest"
assert_eq "${report:2*0x70:64}" "$(printf '00%.0s' {1..32})"
coco_verify_snp_report "$report_file"
}
# vmspawn_boot_coco MACHINE COCO_TYPE WORKDIR TESTCASES [systemd-vmspawn args...]
# Boot a confidential guest with systemd-vmspawn, running the coco guest test runner. TESTCASES is the
# space-separated list of guest checks (testcase_coco_* names without the prefix).