When doing direct kernel boot we need to include the kernel, initrd and cmdline in the measurement. Signed-off-by: Daniel P. Berrangé <berrange@xxxxxxxxxx> --- docs/manpages/virt-qemu-sev-validate.rst | 43 +++++++++ tools/virt-qemu-sev-validate | 108 ++++++++++++++++++++++- 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/docs/manpages/virt-qemu-sev-validate.rst b/docs/manpages/virt-qemu-sev-validate.rst index ec875028cf..0aee1db76d 100644 --- a/docs/manpages/virt-qemu-sev-validate.rst +++ b/docs/manpages/virt-qemu-sev-validate.rst @@ -102,6 +102,20 @@ initialize AMD SEV. For the validation to be trustworthy it important that the firmware build used has no support for loading non-volatile variables from NVRAM, even if NVRAM is expose to the guest. +``-k PATH``, ``--kernel=PATH`` + +Path to the kernel binary if doing direct kernel boot. + +``-r PATH``, ``--initrd=PATH`` + +Path to the initrd binary if doing direct kernel boot. Defaults to zero length +content if omitted. + +``-e STRING``, ``--cmdline=STRING`` + +String containing any kernel command line parameters used during boot of the +domain. Defaults to the empty string if omitted. + ``--tik PATH`` TIK file for domain. This file must be exactly 16 bytes in size and contains the @@ -182,6 +196,22 @@ Validate the measurement of a SEV guest booting from disk: --build-id 13 \ --policy 3 +Validate the measurement of a SEV guest with direct kernel boot: + +:: + + # virt-dom-sev-validate \ + --firmware OVMF.sev.fd \ + --kernel vmlinuz-5.11.12 \ + --initrd initramfs-5.11.12 \ + --cmdline "root=/dev/vda1" \ + --tk this-guest-tk.bin \ + --measurement Zs2pf19ubFSafpZ2WKkwquXvACx9Wt/BV+eJwQ/taO8jhyIj/F8swFrybR1fZ2ID \ + --api-major 0 \ + --api-minor 24 \ + --build-id 13 \ + --policy 3 + Fetch from remote libvirt ------------------------- @@ -202,6 +232,19 @@ Validate the measurement of a SEV guest booting from disk: --tk this-guest-tk.bin \ --domain fedora34x86_64 +Validate the measurement of a SEV guest with direct kernel boot: + +:: + + # virt-dom-sev-validate \ + --connect qemu+ssh://root@xxxxxxxxxxxxxxxx/system \ + --firmware OVMF.sev.fd \ + --kernel vmlinuz-5.11.12 \ + --initrd initramfs-5.11.12 \ + --cmdline "root=/dev/vda1" \ + --tk this-guest-tk.bin \ + --domain fedora34x86_64 + Fetch from local libvirt ------------------------ diff --git a/tools/virt-qemu-sev-validate b/tools/virt-qemu-sev-validate index d3a4d5f26b..deef34628b 100755 --- a/tools/virt-qemu-sev-validate +++ b/tools/virt-qemu-sev-validate @@ -34,6 +34,7 @@ # firmware versions with known flaws. # +import abc import argparse from base64 import b64decode from hashlib import sha256 @@ -43,6 +44,7 @@ import re import socket import sys import traceback +from uuid import UUID from lxml import etree import libvirt @@ -70,6 +72,86 @@ class InvalidStateException(Exception): pass +class GUIDTable(abc.ABC): + GUID_LEN = 16 + + def __init__(self, guid, lenlen=2): + self.guid = guid + self.lenlen = lenlen + + @abc.abstractmethod + def entries(self): + pass + + def build_entry(self, guid, payload, lenlen): + dummylen = int(0).to_bytes(lenlen, 'little') + entry = bytearray(guid + dummylen + payload) + + lenle = len(entry).to_bytes(lenlen, 'little') + entry[self.GUID_LEN:(self.GUID_LEN + lenlen)] = lenle + + return bytes(entry) + + def build(self): + payload = self.entries() + + if len(payload) == 0: + return bytes([]) + + dummylen = int(0).to_bytes(self.lenlen, 'little') + table = bytearray(self.guid + dummylen + payload) + + guidlen = len(table).to_bytes(self.lenlen, 'little') + table[self.GUID_LEN:(self.GUID_LEN + self.lenlen)] = guidlen + + pad = 16 - (len(table) % 16) + table += bytes([0]) * pad + + log.debug("Table(hex): %s", bytes(table).hex()) + return bytes(table) + + +class KernelTable(GUIDTable): + + TABLE_GUID = UUID('{9438d606-4f22-4cc9-b479-a793-d411fd21}').bytes_le + KERNEL_GUID = UUID('{4de79437-abd2-427f-b835-d5b1-72d2045b}').bytes_le + INITRD_GUID = UUID('{44baf731-3a2f-4bd7-9af1-41e2-9169781d}').bytes_le + CMDLINE_GUID = UUID('{97d02dd8-bd20-4c94-aa78-e771-4d36ab2a}').bytes_le + + def __init__(self): + super().__init__(guid=self.TABLE_GUID, + lenlen=2) + + self.kernel = None + self.initrd = sha256(bytes([])).digest() + self.cmdline = sha256(bytes([0])).digest() + + def load_kernel(self, path): + with open(path, "rb") as fh: + self.kernel = sha256(fh.read()).digest() + + def load_initrd(self, path): + with open(path, "rb") as fh: + self.initrd = sha256(fh.read()).digest() + + def load_cmdline(self, val): + self.cmdline = sha256(val.encode("utf8") + bytes([0])).digest() + + def entries(self): + entries = bytes([]) + if self.kernel is None: + return entries + + log.debug("Kernel(sha256): %s", self.kernel.hex()) + log.debug("Initrd(sha256): %s", self.initrd.hex()) + log.debug("Cmdline(sha256): %s", self.cmdline.hex()) + entries += self.build_entry(self.CMDLINE_GUID, self.cmdline, 2) + entries += self.build_entry(self.INITRD_GUID, self.initrd, 2) + entries += self.build_entry(self.KERNEL_GUID, self.kernel, 2) + + return entries + + class ConfidentialVM(object): def __init__(self, @@ -88,6 +170,8 @@ class ConfidentialVM(object): self.tik = None self.tek = None + self.kernel_table = KernelTable() + def load_tik_tek(self, tik_path, tek_path): with open(tik_path, 'rb') as fh: self.tik = fh.read() @@ -129,8 +213,10 @@ class ConfidentialVM(object): # of the following: # # - The firmware blob + # - The kernel GUID table def get_measured_data(self): - measured_data = self.firmware + measured_data = (self.firmware + + self.kernel_table.build()) log.debug("Measured-data(sha256): %s", sha256(measured_data).hexdigest()) return measured_data @@ -303,6 +389,12 @@ def parse_command_line(): vmconfig = parser.add_argument_group("Virtual machine config") vmconfig.add_argument('--firmware', '-f', help='Path to the firmware binary') + vmconfig.add_argument('--kernel', '-k', + help='Path to the kernel binary') + vmconfig.add_argument('--initrd', '-r', + help='Path to the initrd binary') + vmconfig.add_argument('--cmdline', '-e', + help='Cmdline string booted with') vmconfig.add_argument('--tik', help='TIK file for domain') vmconfig.add_argument('--tek', @@ -361,6 +453,11 @@ def check_usage(args): raise UnsupportedUsageException( "Either --firmware or --domain is required") + if args.kernel is None: + if args.initrd is not None or args.cmdline is not None: + raise UnsupportedUsageException( + "--initrd/--cmdline require --kernel") + def attest(args): if args.domain is None: @@ -384,6 +481,15 @@ def attest(args): else: cvm.load_tik_tek(args.tik, args.tek) + if args.kernel is not None: + cvm.kernel_table.load_kernel(args.kernel) + + if args.initrd is not None: + cvm.kernel_table.load_initrd(args.initrd) + + if args.cmdline is not None: + cvm.kernel_table.load_cmdline(args.cmdline) + if args.domain is not None: cvm.load_domain(args.connect, args.domain, -- 2.37.3