Tested as of: [3/9/24]
This guide will walkthrough setting up a basic linux image, emulating an NVMe device (PCIe) and testing SPDM over DOE all using QEMU. With everything setup, we will also take a look at using the Kernel SPDM implementation (ongoing work) to authenticate the NVMe device.
We need:
- Linux Kernel
- Linux Userspace/FS
- QEMU
- SPDM-utils
Notes: There is an in progress effort to add an SPDM requester support to Linux,
it's at a stage where it can be used as a requester to authenticate a device.
Note that there maybe bugs as it's a work in progress. If you don't have a
userspace SPDM requester to test the NVMe responder with. Using l1k's fork
with the kernel implementation is a good start.
Lets setup an isolated working directory:
$ mkdir qemu-spdm
$ cd qemu-spdm
# Linux Upstream
$ git clone https://github.com/torvalds/linux.git
# OR Linux Kernel with SPDM Requester Support (In development)
$ git clone https://github.com/l1k/linux.git
# We are using a fork of QEMU, but SPDM over DOE patches will be upstreamed soon
$ git clone https://github.com/qemu/qemu
# Pick one of the below tools
# spdm-utils -> libspdm Rust based responder/requestor
$ git clone --recursive https://github.com/westerndigitalcorporation/spdm-utils.git
# spdm-emu -> libspdm C based responder/requestor
$ git clone --recursive https://github.com/westerndigitalcorporation/spdm-utils.gitWe can start by compiling the kernel, we are going to be building an x86-64
basic kernel. But aarch64 should also work (although not tested).
$ cd linux
$ make defconfig
# Setup .config for QEMU
$ make kvm_guest.configWhen building the kernel with SPDM support
$ cd linux
$ git checkout spdm-future
$ make defconfig
# Setup .config for QEMU
$ make kvm_guest.config For both upstream and SPDM kernels: In the .config file, we need to enable NVME and some debug settings (optional).
- CONFIG_BLK_DEV_NVME=y
- CONFIG_KCOV=y
- CONFIG_CONFIGFS_FS=y
- CONFIG_SECURITYFS=y
For the SPDM kernel to enable CMA/SPDM Requester set:
- CONFIG_PCI_CMA=y
$ make olddefconfigTo make things easier, you can use the attached .config (this is missing
CONFIG_PCI_CMA=y) file in this repo. Just copy and paste it in the
linux directory.
Now to build it with
$ make -j32 # Use appropriate number of threadsNow to build QEMU, from the qemu-spdm top directory
$ cd qemu
$ mkdir build; cd build
# We are using SLIRP for networking (you will need libslirp-devel installed)
$ ../configure --target-list=x86_64-softmmu --enable-slirp
$ make -j32We are going to setup a filesystem using, the syskaller build script. It's a convenient way to setup a userspace.
We need to have debootstrap installed
$ sudo dnf install debootstrapNow to build the userspace/fs
$ cd qemu-spdm/linux
# You can make this elsewhere if preferred
$ mkdir image; cd image
# Get the build script
$ wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.shWe are going to run this with some options
- distribution=bookworm # Upto date glibc/pcilib maybe required for userspace DoE requesters (so use the latest debian image)
- feature=full # More features
- seek=32768 # Image size (32GB)
$ chmod +x create-image.sh
$ ./create-image.sh --distribution bookworm --feature full --seek 32768this will take awhile to cook. Go for a coffee run.
There are two tools we can use to achieve this, SPDM-utils (Rust based SPDM utility) and SPDM-emu. Pick one of the below, as for this application they both perform similarly.
SPDM utils is Rust based utility for linux that provides an implementation of an
SPDM responder & requester, it is based on libspdm. To learn more about how, spdm-utils
work, please refer to it's README. The below is a summary on how to build it.
$ cd qemu-spdm/spdm-utils/
# Build libspdm as a static library
$ cd third-party/libspdm/
$ mkdir build; cd build;
$ cmake -DARCH=x64 -DTOOLCHAIN=GCC -DTARGET=Debug -DCRYPTO=openssl -DENABLE_BINARY_BUILD=1 -DCOMPILED_LIBCRYPTO_PATH=/usr/lib/ -DCOMPILED_LIBSSL_PATH=/usr/lib/ -DDISABLE_TESTS=1 CFLAGS="-DLIBSPDM_E
NABLE_CAPABILITY_CHUNK_CAP=1" ..
$ make all
$ cd ../../../
# build spdm-utils using cargo
$ cargo buildThe SPDM implementation depends on spdm-emu or known to QEMU as OpenSPDM.
Let's build and set this up.
$ cd qemu-spdm/spdm-emu
$ git submodule init; git submodule update --recursive
$ mkdir build; cd build
$ cmake -DARCH=x64 -DTOOLCHAIN=GCC -DTARGET=Debug -DCRYPTO=openssl ..
$ make -j32
$ make copy_sample_key # Copy certificates, required for SPDM authentication.We need create a file that QEMU can use as the NVMe namespace for the emulated NVMe device.
$ cd qemu-spdm/linux/image
$ dd if=/dev/zero of=blknvme bs=1M count=2096 # 2GB NNMe DriveWe have everything setup. But QEMU depends on spdm-emu or spdm-utils for it's SPDM
implementation. So let's start that first, pick the one you chose to use:
$ cd qemu-spdm/spdm-emu/build/bin
# Must be ran from within `/bin` to keep the relative path to the certs accurate
$ ./spdm_responder_emu --trans PCI_DOE
spdm_responder_emu version 0.1
trans - 0x2
context_size - 0x2458
Platform server listening on port 2323$ cd qemu-spdm/spdm-utils/
$ ./target/debug/spdm_utils --qemu-server responseNow to run QEMU, create a run.sh for convenience
$ cd qemu-spdm/linux
$ vim run.shCopy the following in:
#!/bin/bash
echo "Starting QEMU..."
../qemu/build/qemu-system-x86_64 \
-m 4G \
-smp 8 \
-kernel $1/arch/x86/boot/bzImage \
-append "console=ttyS0 root=/dev/sda earlyprintk=serial net.ifnames=0 nokaslr" \
-drive file=$2/bookworm.img,format=raw \
-drive file=$2/blknvme,if=none,id=mynvme,format=raw \
-device nvme,drive=mynvme,serial=deadbeef,spdm_port=2323 \
-net user,host=10.0.2.10,hostfwd=tcp:127.0.0.1:10021-:22 \
-net nic,model=e1000 \
-enable-kvm \
-nographic \
-pidfile vm.pid \
-machine q35 \
2>&1 | tee vm.logTo summarise, we are virtualizing a q35 machine (this is the only x86-64
machine type to support PCIe). The machine emulates an NVMe controller that
points blknvme (file we made) for it's namespace storage. The spdm_port=2323
option to the NVMe device tells us on which port to look for spdm-responder.
spdm-emu and spdm-utils both currently use port 2323.
If an spdm-responder (spdm-emu or spdm-utils) is not started prior to QEMU,
QEMU will fail to start with an appropriate error.
Finally run:
$ chmod u+x run.sh
$ ./run.sh . image
...
....
....
Debian GNU/Linux 12 syzkaller ttyS0
syzkaller login:You can login with root as the user.
From the host, you can SSH to the guest for a better console experience with
$ cd qemu-spdm/linux
$ ssh -i image/bookworm.id_rsa -p 10021 -o "StrictHostKeyChecking no" root@localhostFirst install some tools
$ apt install nvme-cli pciutilsCheck that the NVMe device is detected with
$ nvme list
Node Generic SN Model Namespace Usage Format
--------------------- --------------------- -------------------- ---------------------------------------- --------- -------------------------- ----------------
/dev/nvme0n1 /dev/ng0n1 deadbeef QEMU NVMe Ctrl 1 2.20 GB / 2.20 GB 512 B + 0 B Check that the device shows DOE support with.
lspci -vvv | grep DOE
...
DOECap: IntSup+
DOECtl: IntEn-
DOESta: Busy- IntSta- Error- ObjectReady-That's it for setting up, we now have a functional NVMe drive that emulates
SPDM (Responder device). You can now start testing against the device. The next
section discusses using the kernel SPDM requester to test this. For this to work,
make sure you built and use the correct kernel from (l1k's linux fork).
At boot, the kernel will attempt to authenticate the NVME responder, and likely
fail as we haven't specified root certificates for the slot0..N in the .cma
keyring. Verify this with
$ dmesg | grep SPDM
[ 1.212647] pci 0000:00:03.0: SPDM: Root certificate for slot 0 not found in .cma keyring: DMTF libspdm ECP384 CA
[ 1.397389] pci 0000:00:03.0: SPDM: Root certificate for slot 1 not found in .cma keyring: DMTF libspdm ECP384 CA
[ 1.582627] pci 0000:00:03.0: SPDM: Root certificate for slot 2 not found in .cma keyring: DMTF libspdm ECP384 CANow, in user-space we can attempt to re-authenticate after adding
the required certificates to the .cma keyring.
You can copy across certificates from the host to the vm using scp, for example:
scp -i image/bookworm.id_rsa -P 10021 -o "StrictHostKeyChecking no" -rp /some/where/certs/ root@localhost:/root/And now on the VM let's add a certificate to the .cma keyring. We are going to
use keyctl for this. Can be installed with apt-get install keyutils on the VM.
First find the keyring id/desc,
$ cat /proc/keys/
...
.
0f229c7d I------ 1 perm 1f0f0000 0 0 keyring .cma: empty
.
...Now add the certificate to the keyring 0x0f229c7d with.
$ keyctl padd asymmetric "" 0x0f229c7d < ca.cert.der
868775432
# See certs in the keyring with
$ keyctl show 0x0f229c7d
Keyring
253926525 ---lswrv 0 0 keyring: .cma
868775432 --als--v 0 0 \_ asymmetric: NAME: VALWe can now attempt to re-authenticate with, ensure that you have the correct
path to the NVMe PCI device (0000:00:03.0 in this case).
echo re > /sys/devices/pci0000\:00/0000\:00\:03.0/authenticatedThis will start the authentication process.
The certificates used and generated by libspdm currently do not conform the CMA/SPDM rules mandated
by PCIe Base Specification 6.0.1 - Section 6.31.3. It is expected(when TCG manifests are not used),
"A Subject Alternative Name extension with a name formatted as defined below".
- [othername:UTF8STRING:PCISIG:<Common Name>:<Serial Number>].
Since the kernel SPDM requester uses DOE over PCIe, the leaf certificate format must conform to this requirement.
This can be achieved with specifying the following in an openssl.cnf file:
- subjectAltName = otherName:2.23.147;UTF8:Vendor=1b36:Device=0010:CC=010802:REV=02:SSVID=1af4:SSID=1100
Then generating the certificate chain. See the below example with SPDM-emu.
--- ../../../libspdm/unit_test/sample_key/openssl.cnf 2023-09-23 13:47:18.234787000 +0200
+++ ../openssl.cnf 2023-09-23 13:49:02.251162000 +0200
@@ -10,13 +10,11 @@
basicConstraints = critical,CA:false
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectKeyIdentifier = hash
-subjectAltName = otherName:1.3.6.1.4.1.412.274.1;UTF8:ACME:WIDGET:1234567890
+#subjectAltName = otherName:1.3.6.1.4.1.412.274.1;UTF8:ACME:WIDGET:1234567890
+subjectAltName = otherName:2.23.147;UTF8:Vendor=1b36:Device=0010:CC=010802:REV=02:SSVID=1af4:SSID=1100
extendedKeyUsage = critical, serverAuth, clientAuth, OCSPSigning
-1.3.6.1.4.1.412.274.6 = ASN1:SEQUENCE:id_spdm_cert_oids
-[id_spdm_cert_oids]
-field1 = SEQUENCE:id_spdm_cert_oid
-[id_spdm_cert_oid]
-field1 = OID:1.3.6.1.4.1.412.274.2
+1.3.6.1.4.1.412.274.6 = ASN1:OID:1.3.6.1.4.1.412.274.2
+2.23.147 = ASN1:OID:2.23.147
[v3_end_with_spdm_req_rsp_eku]
basicConstraints = critical,CA:falseThen regenerating the certificate chain as:
$ cd ecp384
$ openssl req -nodes -newkey ec:param.pem -keyout end_responder.key -out end_responder.req -sha384 -batch -subj "/CN=DMTF libspdm ECP384 responder cert"
$ openssl x509 -req -in end_responder.req -out end_responder.cert -CA inter.cert -CAkey inter.key -sha384 -days 3650 -set_serial 3 -extensions v3_end -extfile ../openssl.cnf
$ openssl asn1parse -in end_responder.cert -out end_responder.cert.der
cat ca.cert.der inter.cert.der end_responder.cert.der > bundle_responder.certchain.derNote that you will need to:
$ make copy_sample_key # Copy certificates, required for SPDM authentication.`To copy over the new certificates to the SPDM-emu working directory.