test: coco credential transport on snp (#43827)
This commit is contained in:
@@ -160,12 +160,19 @@ as root. The host must have coco enabled in firmware and in the kernel (for SEV-
|
|||||||
e.g. `kvm_amd.sev_snp=1`, with `/dev/sev` accessible). On a host without the requested
|
e.g. `kvm_amd.sev_snp=1`, with `/dev/sev` accessible). On a host without the requested
|
||||||
technology, or when running in `mkosi vm` mode, the tests are skipped.
|
technology, or when running in `mkosi vm` mode, the tests are skipped.
|
||||||
|
|
||||||
|
Inside each guest, `guest-test-runner.sh` runs the guest-side checks and reports
|
||||||
|
their results back to the host over a vsock socket, while the guest's overall
|
||||||
|
pass/fail is relayed separately through its exit status. The host side collects
|
||||||
|
both to produce the subtest result.
|
||||||
|
|
||||||
```
|
```
|
||||||
meson test
|
meson test
|
||||||
└─ integration-test-wrapper.py
|
└─ integration-test-wrapper.py
|
||||||
└─ mkosi boot ──► systemd-nspawn container
|
└─ mkosi boot ──► systemd-nspawn container
|
||||||
└─ TEST-94-COCO.sh
|
└─ TEST-94-COCO.sh
|
||||||
└─ systemd-vmspawn --coco=<type> ──► confidential VM
|
└─ TEST-94-COCO.*.sh
|
||||||
|
└─ systemd-vmspawn --coco=<type> ──► confidential VM
|
||||||
|
└─ guest-test-runner.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running the integration tests without building systemd from source
|
## Running the integration tests without building systemd from source
|
||||||
|
|||||||
+21
-82
@@ -1,20 +1,23 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
#
|
#
|
||||||
# Launch a confidential VM with systemd-vmspawn --coco= and verify the guest reports the expected
|
# Confidential Computing test.
|
||||||
# confidential-virtualization type. Runs only on coco-capable hardware: the harness --coco=any flag gates
|
|
||||||
# the run (skipped otherwise), binds /dev/kvm + the coco device(s) into this boot-mode container, and
|
|
||||||
# passes the resolved technology via $COCO_TYPE.
|
|
||||||
#
|
#
|
||||||
# The confidential guest boots the same mkosi test image. A one-shot unit injected into the guest checks
|
# These only run on coco hardware and if the test environment isn't already virtualized, see
|
||||||
# systemd-detect-virt --cvm and reports the verdict via systemd's exit-status notification over vsock
|
# the harness --coco= flag and the documentation in test/integration-tests/README.md.
|
||||||
# (SuccessAction=exit): the guest PID1 sends EXIT_STATUS to vmspawn's vmm notify socket and vmspawn exits
|
#
|
||||||
# with it.
|
# Subtests are split per platform and launch path to reduce the number of required boots.
|
||||||
|
# Test code runs on both sides:
|
||||||
|
# - host: test vmspawn by running confidential guests. TEST-94-COCO/lib.sh provides helpers.
|
||||||
|
# - guest: test guest side component coco behavior. Assertions via TEST-94-COCO/guest-test{,-runner}.sh.
|
||||||
|
|
||||||
set -eux
|
set -eux
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
# shellcheck source=test/units/util.sh
|
# shellcheck source=test/units/util.sh
|
||||||
. "$(dirname "$0")"/util.sh
|
. "$(dirname "$0")"/util.sh
|
||||||
|
# shellcheck source=test/units/test-control.sh
|
||||||
|
. "$(dirname "$0")"/test-control.sh
|
||||||
|
|
||||||
if [[ -v ASAN_OPTIONS ]]; then
|
if [[ -v ASAN_OPTIONS ]]; then
|
||||||
echo "vmspawn launches QEMU which doesn't work under ASan, skipping" | tee --append /skipped
|
echo "vmspawn launches QEMU which doesn't work under ASan, skipping" | tee --append /skipped
|
||||||
@@ -31,83 +34,19 @@ if ! find_qemu_binary; then
|
|||||||
exit 77
|
exit 77
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The integration-test-wrapper resolves the host's coco technology on the
|
# The harness binds the guest image artifacts read-only at /work/vm-images.
|
||||||
# bare-metal host (where /sys/module is readable) and passes it via $COCO_TYPE.
|
export IMAGE_DIR=/work/vm-images
|
||||||
|
if [[ ! -f "$IMAGE_DIR/image.raw" || ! -f "$IMAGE_DIR/image.vmlinuz" || ! -f "$IMAGE_DIR/image.initrd" ]]; then
|
||||||
|
echo "image artifacts not found in $IMAGE_DIR, skipping" | tee --append /skipped
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The integration-test-wrapper resolves the host's coco technology (where /sys/module is readable) and
|
||||||
|
# passes it via $COCO_TYPE.
|
||||||
if [[ -z "${COCO_TYPE:-}" ]]; then
|
if [[ -z "${COCO_TYPE:-}" ]]; then
|
||||||
echo "COCO_TYPE not set (run via the integration harness with --coco), skipping" | tee --append /skipped
|
echo "COCO_TYPE not set (run via the integration harness with --coco), skipping" | tee --append /skipped
|
||||||
exit 77
|
exit 77
|
||||||
fi
|
fi
|
||||||
echo "Host coco type: $COCO_TYPE"
|
echo "Host coco type: $COCO_TYPE"
|
||||||
|
|
||||||
# The harness binds the guest image read-only at /work/vm-images (the 'vm-images' test option).
|
run_subtests_and_exit
|
||||||
IMAGE_DIR=/work/vm-images
|
|
||||||
if [[ ! -f "$IMAGE_DIR/image.raw" || ! -f "$IMAGE_DIR/image.vmlinuz" || ! -f "$IMAGE_DIR/image.initrd" ]]; then
|
|
||||||
echo "guest image not found in $IMAGE_DIR, skipping" | tee --append /skipped
|
|
||||||
exit 77
|
|
||||||
fi
|
|
||||||
echo "Guest image directory: $IMAGE_DIR"
|
|
||||||
|
|
||||||
MACHINE="coco-guest-$$"
|
|
||||||
WORKDIR="$(mktemp -d)"
|
|
||||||
at_exit() {
|
|
||||||
set +e
|
|
||||||
machinectl terminate "$MACHINE" 2>/dev/null || :
|
|
||||||
rm -rf "$WORKDIR"
|
|
||||||
}
|
|
||||||
trap at_exit EXIT INT TERM
|
|
||||||
|
|
||||||
# One-shot unit for the guest: verify systemd-detect-virt --cvm matches the type vmspawn launched.
|
|
||||||
# Created via the systemd.extra-unit.* credential and pulled into the boot by a multi-user.target drop-in.
|
|
||||||
GUEST_PASS=123
|
|
||||||
GUEST_UNIT="$(cat <<'EOF'
|
|
||||||
[Unit]
|
|
||||||
Description=coco guest self-check
|
|
||||||
After=basic.target
|
|
||||||
SuccessAction=exit
|
|
||||||
SuccessActionExitStatus=@GUEST_PASS@
|
|
||||||
FailureAction=exit
|
|
||||||
FailureActionExitStatus=1
|
|
||||||
[Service]
|
|
||||||
Type=oneshot
|
|
||||||
ExecStart=bash -c 'test "$(systemd-detect-virt --cvm)" = "@COCO_TYPE@"'
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
GUEST_UNIT="${GUEST_UNIT//@COCO_TYPE@/$COCO_TYPE}"
|
|
||||||
GUEST_UNIT="${GUEST_UNIT//@GUEST_PASS@/$GUEST_PASS}"
|
|
||||||
|
|
||||||
GUEST_DROPIN="$(cat <<'EOF'
|
|
||||||
[Unit]
|
|
||||||
Wants=coco-guest.service
|
|
||||||
After=coco-guest.service
|
|
||||||
EOF
|
|
||||||
)"
|
|
||||||
|
|
||||||
# Launch the confidential guest and capture its console output.
|
|
||||||
# Direct linux boot should work on all coco platforms (the only boot path supported by SNP).
|
|
||||||
vmspawn_rc=0
|
|
||||||
timeout -k 30 300 systemd-vmspawn \
|
|
||||||
--machine="$MACHINE" \
|
|
||||||
--coco="$COCO_TYPE" \
|
|
||||||
--ram=1G \
|
|
||||||
--ephemeral \
|
|
||||||
--image="$IMAGE_DIR/image.raw" \
|
|
||||||
--linux="$IMAGE_DIR/image.vmlinuz" \
|
|
||||||
--initrd="$IMAGE_DIR/image.initrd" \
|
|
||||||
--tpm=no \
|
|
||||||
--console=read-only \
|
|
||||||
--set-credential="systemd.extra-unit.coco-guest.service:$GUEST_UNIT" \
|
|
||||||
--set-credential="systemd.unit-dropin.multi-user.target:$GUEST_DROPIN" \
|
|
||||||
selinux=0 systemd.firstboot=no rw \
|
|
||||||
2>&1 | tee "$WORKDIR/console.log" || vmspawn_rc="${PIPESTATUS[0]}"
|
|
||||||
if [[ "$vmspawn_rc" -eq 124 || "$vmspawn_rc" -eq 137 ]]; then
|
|
||||||
echo "systemd-vmspawn was killed by timeout (exit $vmspawn_rc)" >&2
|
|
||||||
cat "$WORKDIR/console.log" >&2
|
|
||||||
exit 1
|
|
||||||
elif [[ "$vmspawn_rc" -ne "$GUEST_PASS" ]]; then
|
|
||||||
echo "guest coco self-check did not pass: vmspawn exit $vmspawn_rc (expected $GUEST_PASS)" >&2
|
|
||||||
cat "$WORKDIR/console.log" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Guest correctly reported confidential virtualization: $COCO_TYPE"
|
|
||||||
|
|
||||||
touch /testok
|
|
||||||
|
|||||||
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#
|
||||||
|
# SEV-SNP, direct kernel boot: launch confidential guests with --linux=/--initrd=.
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# shellcheck source=test/units/TEST-94-COCO/lib.sh
|
||||||
|
. "$(dirname "$0")"/TEST-94-COCO/lib.sh
|
||||||
|
|
||||||
|
if [[ "${COCO_TYPE:?}" != "sev-snp" ]]; then
|
||||||
|
echo "host coco type is '$COCO_TYPE', not sev-snp, skipping"
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
|
||||||
|
MACHINE="coco-snp-direct-$$"
|
||||||
|
WORKDIR="$(mktemp -d)"
|
||||||
|
mkdir -p "$WORKDIR/honest" "$WORKDIR/hostile"
|
||||||
|
at_exit() {
|
||||||
|
set +e
|
||||||
|
jobs -p | xargs -r kill 2>/dev/null
|
||||||
|
machinectl terminate "$MACHINE-honest" 2>/dev/null || :
|
||||||
|
machinectl terminate "$MACHINE-hostile" 2>/dev/null || :
|
||||||
|
rm -rf "$WORKDIR"
|
||||||
|
}
|
||||||
|
trap at_exit EXIT
|
||||||
|
|
||||||
|
# General test case for SNP direct boot, covers:
|
||||||
|
#
|
||||||
|
# - detect_virt: systemd-detect-virt --cvm detects SNP as the CVM type
|
||||||
|
# - 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)
|
||||||
|
vmspawn_boot_coco "$MACHINE-honest" "$COCO_TYPE" "$WORKDIR/honest" 'detect_virt creds_vmspawn creds_cmdline' \
|
||||||
|
--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"
|
||||||
|
echo "SEV-SNP direct-boot guest correctly reported confidential virtualization and trusted credentials"
|
||||||
|
|
||||||
|
# Offensive test:
|
||||||
|
#
|
||||||
|
# - creds_hostile_*: ensure the guest isn't tricked into consuming untrusted/unmeasured credentials.
|
||||||
|
QEMU_EXTRA=(
|
||||||
|
-smbios "type=11,value=io.systemd.credential:$COCO_CRED_SMBIOS_ID=$COCO_CRED_SMBIOS_VALUE"
|
||||||
|
-smbios "type=11,value=io.systemd.credential:$COCO_CRED_TRUSTED_ID=$COCO_CRED_SMBIOS_VALUE"
|
||||||
|
-fw_cfg "name=opt/io.systemd.credentials/$COCO_CRED_FWCFG_ID,string=$COCO_CRED_FWCFG_VALUE"
|
||||||
|
)
|
||||||
|
|
||||||
|
SYSTEMD_VMSPAWN_QEMU_EXTRA="${QEMU_EXTRA[*]}" \
|
||||||
|
vmspawn_boot_coco "$MACHINE-hostile" "$COCO_TYPE" "$WORKDIR/hostile" \
|
||||||
|
'detect_virt creds_vmspawn creds_hostile_fwcfg creds_hostile_snp_smbios creds_hostile_shadow' \
|
||||||
|
--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
|
||||||
|
echo "SEV-SNP direct-boot guest correctly ignored the host-injected credentials"
|
||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#
|
||||||
|
# TDX, direct kernel boot: launch a confidential guest with --linux=/--initrd=.
|
||||||
|
|
||||||
|
set -eux
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
# shellcheck source=test/units/TEST-94-COCO/lib.sh
|
||||||
|
. "$(dirname "$0")"/TEST-94-COCO/lib.sh
|
||||||
|
|
||||||
|
if [[ "${COCO_TYPE:?}" != "tdx" ]]; then
|
||||||
|
echo "host coco type is '$COCO_TYPE', not tdx, skipping"
|
||||||
|
exit 77
|
||||||
|
fi
|
||||||
|
|
||||||
|
MACHINE="coco-tdx-direct-$$"
|
||||||
|
WORKDIR="$(mktemp -d)"
|
||||||
|
at_exit() {
|
||||||
|
set +e
|
||||||
|
jobs -p | xargs -r kill 2>/dev/null
|
||||||
|
machinectl terminate "$MACHINE" 2>/dev/null || :
|
||||||
|
rm -rf "$WORKDIR"
|
||||||
|
}
|
||||||
|
trap at_exit EXIT
|
||||||
|
|
||||||
|
vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" 'detect_virt' \
|
||||||
|
--image="$IMAGE_DIR/image.raw" \
|
||||||
|
--linux="$IMAGE_DIR/image.vmlinuz" \
|
||||||
|
--initrd="$IMAGE_DIR/image.initrd" \
|
||||||
|
selinux=0 systemd.firstboot=no rw
|
||||||
|
echo "TDX direct-boot guest correctly reported confidential virtualization"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# Every constant here is consumed by whichever side sources this file, never by this file itself.
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
#
|
||||||
|
# Fixtures for the confidential computing integration test (TEST-94-COCO). Shared by the host-side
|
||||||
|
# boot scenarios, which deliver these credentials over trusted and untrusted channels alike, and by
|
||||||
|
# the guest-side checks, which assert where each one ended up, so the two sides cannot drift apart.
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then
|
||||||
|
echo >&2 "This file should not be executed directly"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Name of the guest self-check unit lib.sh injects.
|
||||||
|
COCO_GUEST_UNIT="coco-guest.service"
|
||||||
|
|
||||||
|
# Credentials by channel
|
||||||
|
COCO_CRED_TRUSTED_ID="coco.trusted"
|
||||||
|
COCO_CRED_TRUSTED_VALUE="trusted-via-vmspawn"
|
||||||
|
COCO_CRED_CMDLINE_ID="coco.cmdline"
|
||||||
|
COCO_CRED_CMDLINE_VALUE="trusted-via-cmdline"
|
||||||
|
COCO_CRED_SMBIOS_ID="coco.hostile.smbios"
|
||||||
|
COCO_CRED_SMBIOS_VALUE="injected-via-smbios"
|
||||||
|
COCO_CRED_FWCFG_ID="coco.hostile.fwcfg"
|
||||||
|
COCO_CRED_FWCFG_VALUE="injected-via-fwcfg"
|
||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
#
|
||||||
|
# Runs inside the confidential guest launched by TEST-94-COCO.sh. Runs the requested guest-side checks
|
||||||
|
# (testcase_coco_* from guest-test.sh), ships a per-check result record to the host over the vsock
|
||||||
|
# result socket, and lets the injected unit's SuccessAction/FailureAction relay the aggregate verdict
|
||||||
|
# to the host via vmspawn's exit status.
|
||||||
|
set -eux
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
_COCO_DIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
# util.sh (assert_*) lives one level up in the shared units dir.
|
||||||
|
# shellcheck source=test/units/util.sh
|
||||||
|
. "$(dirname "$_COCO_DIR")/util.sh"
|
||||||
|
# shellcheck source=test/units/TEST-94-COCO/guest-test.sh
|
||||||
|
. "$_COCO_DIR/guest-test.sh"
|
||||||
|
|
||||||
|
# Ship the accumulated result records to the host's vsock listener (CID 2 = host). Best-effort: the
|
||||||
|
# aggregate verdict travels independently via the unit's exit status, so a vsock hiccup never masks a
|
||||||
|
# pass/fail — it only drops the per-check breakdown.
|
||||||
|
_coco_ship_results() {
|
||||||
|
local records="${1:?}"
|
||||||
|
[[ -n "${COCO_RESULT_PORT:-}" ]] || return 0
|
||||||
|
socat -u "OPEN:$records,rdonly" "VSOCK-CONNECT:2:$COCO_RESULT_PORT" ||
|
||||||
|
echo >&2 "WARNING: failed to ship coco results over vsock port $COCO_RESULT_PORT"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the guest-side checks named by $COCO_TESTCASES (testcase_coco_* names without the prefix),
|
||||||
|
# record pass/fail per check, and ship the records. Each named check is something its boot scenario
|
||||||
|
# exists to prove, so an unknown name or an empty request fails the aggregate rather than letting the
|
||||||
|
# boot pass without having verified anything.
|
||||||
|
_coco_run_tests() {
|
||||||
|
local testcases testcase rc result failed=0 records
|
||||||
|
records="$(mktemp)"
|
||||||
|
|
||||||
|
read -ra testcases <<<"${COCO_TESTCASES:-}"
|
||||||
|
if [[ "${#testcases[@]}" -eq 0 ]]; then
|
||||||
|
echo >&2 "No coco testcases requested via \$COCO_TESTCASES"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
testcases=("${testcases[@]/#/testcase_coco_}")
|
||||||
|
|
||||||
|
for testcase in "${testcases[@]}"; do
|
||||||
|
if ! declare -F "$testcase" >/dev/null; then
|
||||||
|
echo >&2 "Requested coco testcase '$testcase' is not defined"
|
||||||
|
printf 'ID=%s RESULT=missing\n' "$testcase" >>"$records"
|
||||||
|
failed=1
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Subshell so a testcase's RETURN trap can't fire twice (see run_testcases in test-control.sh).
|
||||||
|
# Bash disables 'set -e' throughout a function called in a context that tests its exit status,
|
||||||
|
# so '("$testcase") || rc=$?' would let a testcase run past its first failed assertion and
|
||||||
|
# report only its last one. Reading $? from a standalone call avoids that context; the inner
|
||||||
|
# 'set -e' re-arms what the surrounding 'set +e' turned off.
|
||||||
|
set +e
|
||||||
|
(set -e; "$testcase")
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
if [[ "$rc" -eq 0 ]]; then
|
||||||
|
result=pass
|
||||||
|
else
|
||||||
|
result=fail
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
echo "coco check $testcase: $result (rc=$rc)"
|
||||||
|
printf 'ID=%s RESULT=%s\n' "$testcase" "$result" >>"$records"
|
||||||
|
done
|
||||||
|
|
||||||
|
_coco_ship_results "$records"
|
||||||
|
return "$failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
_coco_run_tests
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
# shellcheck shell=bash
|
||||||
|
#
|
||||||
|
# Guest-side checks for the confidential computing integration test (TEST-94-COCO). Sourced by
|
||||||
|
# guest-test-runner.sh inside the confidential guest; each testcase_coco_* function is one
|
||||||
|
# sub-subtest, and a boot scenario names the checks it requires via $COCO_TESTCASES.
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then
|
||||||
|
echo >&2 "This file should not be executed directly"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck source=test/units/TEST-94-COCO/fixtures.sh
|
||||||
|
. "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/fixtures.sh"
|
||||||
|
|
||||||
|
# _coco_assert_cred_absent ID
|
||||||
|
# Assert no credential named ID reached the guest at all, neither trusted nor encrypted.
|
||||||
|
_coco_assert_cred_absent() {
|
||||||
|
local id="${1:?}"
|
||||||
|
local system_creds_dir=/run/credentials/@system
|
||||||
|
local encrypted_creds_dir=/run/credentials/@encrypted
|
||||||
|
|
||||||
|
assert_fail test -e "$system_creds_dir/$id"
|
||||||
|
assert_fail test -e "$encrypted_creds_dir/$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
# _coco_assert_smbios_injected ID
|
||||||
|
# Assert the guest received an SMBIOS type #11 OEM string carrying credential ID. The scenario
|
||||||
|
# injected it, so a miss means the injection was lost on the way in and concluding anything from the
|
||||||
|
# credential's absence afterwards would be meaningless.
|
||||||
|
_coco_assert_smbios_injected() {
|
||||||
|
local id="${1:?}" raw
|
||||||
|
|
||||||
|
[[ -d /sys/firmware/dmi/entries ]] || modprobe dmi-sysfs || :
|
||||||
|
|
||||||
|
for raw in /sys/firmware/dmi/entries/11-*/raw; do
|
||||||
|
[[ -e "$raw" ]] || continue
|
||||||
|
# The entry is a blob of NUL-separated strings: --text to search it anyway, --fixed-strings
|
||||||
|
# because credential names contain dots. Redirect rather than -q, which would exit early and
|
||||||
|
# risk SIGPIPE on the reader.
|
||||||
|
if grep --text --fixed-strings "io.systemd.credential:$id=" "$raw" >/dev/null; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "FAIL: no SMBIOS OEM string for credential '$id' in the guest's DMI type #11 tables" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assert the guest observes the confidential-virtualization technology the host launched it with.
|
||||||
|
# COCO_TYPE is provided by the injected guest unit (Environment=, see lib.sh).
|
||||||
|
testcase_coco_detect_virt() {
|
||||||
|
assert_eq "$(systemd-detect-virt --cvm)" "${COCO_TYPE:?}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assert a credential handed to systemd-vmspawn --set-credential lands in the trusted bucket.
|
||||||
|
#
|
||||||
|
# SNP: creds should be delivered via the initrd cpio channel.
|
||||||
|
testcase_coco_creds_vmspawn() {
|
||||||
|
assert_eq "$(systemd-creds --system cat "$COCO_CRED_TRUSTED_ID")" "$COCO_CRED_TRUSTED_VALUE"
|
||||||
|
assert_not_in "$COCO_CRED_TRUSTED_ID" "$(</proc/cmdline)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assert a credential passed on the kernel command line lands in the trusted bucket.
|
||||||
|
#
|
||||||
|
# Credentials passed on the cmdline should be accepted on every coco platform.
|
||||||
|
testcase_coco_creds_cmdline() {
|
||||||
|
assert_in "systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE" "$(</proc/cmdline)"
|
||||||
|
assert_eq "$(systemd-creds --system cat "$COCO_CRED_CMDLINE_ID")" "$COCO_CRED_CMDLINE_VALUE"
|
||||||
|
}
|
||||||
|
|
||||||
|
# fw_cfg is unmeasured on every platform and import_credentials_qemu() drops it for any confidential
|
||||||
|
# guest, so this applies to SNP and TDX alike.
|
||||||
|
testcase_coco_creds_hostile_fwcfg() {
|
||||||
|
local fwcfg_dir=/sys/firmware/qemu_fw_cfg/by_name
|
||||||
|
|
||||||
|
[[ -d "$fwcfg_dir" ]] || modprobe qemu_fw_cfg || :
|
||||||
|
|
||||||
|
assert_ok test -e "$fwcfg_dir/opt/io.systemd.credentials/$COCO_CRED_FWCFG_ID/raw"
|
||||||
|
_coco_assert_cred_absent "$COCO_CRED_FWCFG_ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
# SNP only: the SMBIOS tables are not covered by the launch measurement, so
|
||||||
|
# import_credentials_smbios() bails out. Under TDX they are measured into RTMR0 and thus trusted.
|
||||||
|
testcase_coco_creds_hostile_snp_smbios() {
|
||||||
|
_coco_assert_smbios_injected "$COCO_CRED_SMBIOS_ID"
|
||||||
|
_coco_assert_cred_absent "$COCO_CRED_SMBIOS_ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
# A host-injected SMBIOS string must not displace a credential the measured channel already
|
||||||
|
# delivered: boot-channel credentials import first and names are never overwritten, on every
|
||||||
|
# platform. The probe proves the overwrite was attempted.
|
||||||
|
testcase_coco_creds_hostile_shadow() {
|
||||||
|
_coco_assert_smbios_injected "$COCO_CRED_TRUSTED_ID"
|
||||||
|
assert_eq "$(systemd-creds --system cat "$COCO_CRED_TRUSTED_ID")" "$COCO_CRED_TRUSTED_VALUE"
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||||
|
# shellcheck shell=bash
|
||||||
|
#
|
||||||
|
# Host-side helpers for the confidential computing integration test (TEST-94-COCO).
|
||||||
|
# Sourced by the TEST-94-COCO.sh driver.
|
||||||
|
|
||||||
|
if [[ "${BASH_SOURCE[0]}" -ef "$0" ]]; then
|
||||||
|
echo >&2 "This file should not be executed directly"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck source=test/units/TEST-94-COCO/fixtures.sh
|
||||||
|
. "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/fixtures.sh"
|
||||||
|
|
||||||
|
# Exit status the guest test runner reports on success (its unit's SuccessActionExitStatus=). vmspawn
|
||||||
|
# forwards the guest PID1's EXIT_STATUS over the vsock notify socket and exits with it.
|
||||||
|
_COCO_GUEST_PASS=123
|
||||||
|
# vsock port the host listens on for the guest's per-check result records, distinct from vmspawn's own
|
||||||
|
# (kernel-assigned) notify port. The host binds CID_ANY:PORT; the guest connects to CID 2 (host):PORT.
|
||||||
|
_COCO_RESULT_PORT=4321
|
||||||
|
|
||||||
|
# _coco_guest_unit COCO_TYPE UNITS_DIR TESTCASES
|
||||||
|
# Emit the one-shot unit that runs the guest test runner: passes the coco type, result port and
|
||||||
|
# the requested checks via the environment, and relays the aggregate verdict via
|
||||||
|
# SuccessAction/EXIT_STATUS.
|
||||||
|
_coco_guest_unit() {
|
||||||
|
local coco_type="${1:?}" units_dir="${2:?}" testcases="${3:?}"
|
||||||
|
cat <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=coco guest self-check
|
||||||
|
After=basic.target
|
||||||
|
SuccessAction=exit
|
||||||
|
SuccessActionExitStatus=$_COCO_GUEST_PASS
|
||||||
|
FailureAction=exit
|
||||||
|
FailureActionExitStatus=1
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
Environment=COCO_TYPE=$coco_type
|
||||||
|
Environment=COCO_RESULT_PORT=$_COCO_RESULT_PORT
|
||||||
|
Environment="COCO_TESTCASES=$testcases"
|
||||||
|
ExecStart=$units_dir/guest-test-runner.sh
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# _coco_guest_dropin
|
||||||
|
# Emit the multi-user.target drop-in that pulls the guest self-check unit into the boot.
|
||||||
|
_coco_guest_dropin() {
|
||||||
|
cat <<EOF
|
||||||
|
[Unit]
|
||||||
|
Wants=$COCO_GUEST_UNIT
|
||||||
|
After=$COCO_GUEST_UNIT
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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).
|
||||||
|
# The caller supplies the boot-path-specific launch args — --image/--linux/--initrd and the guest
|
||||||
|
# kernel command line — plus any extra flags via "$@", so each per-boot-path scenario
|
||||||
|
# stays configurable. Per-check records shipped by the guest are collected over a vsock
|
||||||
|
# socket into WORKDIR/results and echoed; returns 0 iff the guest reported the success
|
||||||
|
# aggregate, otherwise dumps the console.
|
||||||
|
vmspawn_boot_coco() {
|
||||||
|
local machine="${1:?}" coco_type="${2:?}" workdir="${3:?}" testcases="${4:?}"
|
||||||
|
shift 4
|
||||||
|
|
||||||
|
local units_dir results console guest_unit guest_dropin rc=0 listener_pid=""
|
||||||
|
units_dir="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||||
|
results="$workdir/results"
|
||||||
|
console="$workdir/console.log"
|
||||||
|
: >"$results"
|
||||||
|
|
||||||
|
guest_unit="$(_coco_guest_unit "$coco_type" "$units_dir" "$testcases")"
|
||||||
|
guest_dropin="$(_coco_guest_dropin)"
|
||||||
|
|
||||||
|
# Collect the per-check records the guest ships over vsock (single connection, guest closes when
|
||||||
|
# done). Best-effort: the aggregate exit status below is the authoritative pass/fail, so a vsock
|
||||||
|
# hiccup only costs the per-check breakdown, never the verdict.
|
||||||
|
socat -u "VSOCK-LISTEN:$_COCO_RESULT_PORT" "OPEN:$results,creat" &
|
||||||
|
listener_pid=$!
|
||||||
|
|
||||||
|
timeout -k 30 300 systemd-vmspawn \
|
||||||
|
--machine="$machine" \
|
||||||
|
--coco="$coco_type" \
|
||||||
|
--ram=1G \
|
||||||
|
--ephemeral \
|
||||||
|
--tpm=no \
|
||||||
|
--console=read-only \
|
||||||
|
--set-credential="systemd.extra-unit.$COCO_GUEST_UNIT:$guest_unit" \
|
||||||
|
--set-credential="systemd.unit-dropin.multi-user.target:$guest_dropin" \
|
||||||
|
"$@" \
|
||||||
|
2>&1 | tee "$console" || rc="${PIPESTATUS[0]}"
|
||||||
|
|
||||||
|
kill "$listener_pid" 2>/dev/null || :
|
||||||
|
wait "$listener_pid" 2>/dev/null || :
|
||||||
|
|
||||||
|
if [[ -s "$results" ]]; then
|
||||||
|
echo "coco guest per-check results:"
|
||||||
|
cat "$results"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$rc" -eq 124 || "$rc" -eq 137 ]]; then
|
||||||
|
echo "systemd-vmspawn was killed by timeout (exit $rc)" >&2
|
||||||
|
cat "$console" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$rc" -ne "$_COCO_GUEST_PASS" ]]; then
|
||||||
|
echo "coco guest test runner failed: vmspawn exit $rc (expected $_COCO_GUEST_PASS)" >&2
|
||||||
|
cat "$console" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -84,6 +84,8 @@ _show_summary() {(
|
|||||||
_record_subtest_rc() {
|
_record_subtest_rc() {
|
||||||
local subtest="${1:?}" rc="${2:?}"
|
local subtest="${1:?}" rc="${2:?}"
|
||||||
|
|
||||||
|
subtest="${subtest##*/}"
|
||||||
|
|
||||||
if [[ $rc -eq $_SUBTEST_SKIP_RC ]]; then
|
if [[ $rc -eq $_SUBTEST_SKIP_RC ]]; then
|
||||||
echo "Subtest $subtest skipped"
|
echo "Subtest $subtest skipped"
|
||||||
_SKIPPED_TESTS+=("$subtest")
|
_SKIPPED_TESTS+=("$subtest")
|
||||||
|
|||||||
Reference in New Issue
Block a user