test: coco credential rejection on snp

Not all channels used for credential transport in systemd are trusted
on SNP: while cmdline and initrd are measured and should be used,
smbios and fwcfg are untrusted and the guest should ignore creds from
those delivery paths.

Implement an offensive credential test that tries to inject credentials
via those untrusted channels and check the guest won't consume those
credentials.

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
This commit is contained in:
Paul Meyer
2026-09-25 08:15:24 +02:00
parent 28a1975396
commit d6f96ac311
3 changed files with 88 additions and 3 deletions
+24 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later # SPDX-License-Identifier: LGPL-2.1-or-later
# #
# SEV-SNP, direct kernel boot: launch a confidential guest with --linux=/--initrd=. # SEV-SNP, direct kernel boot: launch confidential guests with --linux=/--initrd=.
set -eux set -eux
set -o pipefail set -o pipefail
@@ -16,10 +16,12 @@ fi
MACHINE="coco-snp-direct-$$" MACHINE="coco-snp-direct-$$"
WORKDIR="$(mktemp -d)" WORKDIR="$(mktemp -d)"
mkdir -p "$WORKDIR/honest" "$WORKDIR/hostile"
at_exit() { at_exit() {
set +e set +e
jobs -p | xargs -r kill 2>/dev/null jobs -p | xargs -r kill 2>/dev/null
machinectl terminate "$MACHINE" 2>/dev/null || : machinectl terminate "$MACHINE-honest" 2>/dev/null || :
machinectl terminate "$MACHINE-hostile" 2>/dev/null || :
rm -rf "$WORKDIR" rm -rf "$WORKDIR"
} }
trap at_exit EXIT trap at_exit EXIT
@@ -29,7 +31,7 @@ trap at_exit EXIT
# - detect_virt: systemd-detect-virt --cvm detects SNP as the CVM type # - 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_vmspawn: credential transport by vmspawn is working (via initrd for SNP)
# - creds_cmdline: credential transport via cmdline is working (cmdline is measured on SNP) # - creds_cmdline: credential transport via cmdline is working (cmdline is measured on SNP)
vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" 'detect_virt creds_vmspawn creds_cmdline' \ vmspawn_boot_coco "$MACHINE-honest" "$COCO_TYPE" "$WORKDIR/honest" 'detect_virt creds_vmspawn creds_cmdline' \
--image="$IMAGE_DIR/image.raw" \ --image="$IMAGE_DIR/image.raw" \
--linux="$IMAGE_DIR/image.vmlinuz" \ --linux="$IMAGE_DIR/image.vmlinuz" \
--initrd="$IMAGE_DIR/image.initrd" \ --initrd="$IMAGE_DIR/image.initrd" \
@@ -37,3 +39,22 @@ vmspawn_boot_coco "$MACHINE" "$COCO_TYPE" "$WORKDIR" 'detect_virt creds_vmspawn
selinux=0 systemd.firstboot=no rw \ selinux=0 systemd.firstboot=no rw \
"systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE" "systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE"
echo "SEV-SNP direct-boot guest correctly reported confidential virtualization and trusted credentials" 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"
+4
View File
@@ -20,3 +20,7 @@ COCO_CRED_TRUSTED_ID="coco.trusted"
COCO_CRED_TRUSTED_VALUE="trusted-via-vmspawn" COCO_CRED_TRUSTED_VALUE="trusted-via-vmspawn"
COCO_CRED_CMDLINE_ID="coco.cmdline" COCO_CRED_CMDLINE_ID="coco.cmdline"
COCO_CRED_CMDLINE_VALUE="trusted-via-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"
+60
View File
@@ -13,6 +13,40 @@ fi
# shellcheck source=test/units/TEST-94-COCO/fixtures.sh # shellcheck source=test/units/TEST-94-COCO/fixtures.sh
. "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/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. # 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). # COCO_TYPE is provided by the injected guest unit (Environment=, see lib.sh).
testcase_coco_detect_virt() { testcase_coco_detect_virt() {
@@ -34,3 +68,29 @@ testcase_coco_creds_cmdline() {
assert_in "systemd.set_credential=$COCO_CRED_CMDLINE_ID:$COCO_CRED_CMDLINE_VALUE" "$(</proc/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" 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"
}