The VMSA files contain the expected CPU register state for the VM. Their
content varies based on a few pieces of the stack
- AMD CPU architectural initial state
- KVM hypervisor VM CPU initialization
- QEMU userspace VM CPU initialization
- AMD CPU SKU (family/model/stepping)
The first three pieces of information we can obtain through code
inspection. The last piece of information we can take on the command
line. This allows a user to validate a SEV-ES guest merely by providing
the CPU SKU information, using --cpu-family, --cpu-model,
--cpu-stepping. This avoids the need to obtain or construct VMSA files
directly.
Reviewed-by: Ján Tomko <jtomko(a)redhat.com>
Signed-off-by: Daniel P. Berrangé <berrange(a)redhat.com>
---
docs/manpages/virt-qemu-sev-validate.rst | 45 +++
tools/virt-qemu-sev-validate | 467 +++++++++++++++++++++++
2 files changed, 512 insertions(+)
diff --git a/docs/manpages/virt-qemu-sev-validate.rst
b/docs/manpages/virt-qemu-sev-validate.rst
index d51969d0b2..34ef328fe8 100644
--- a/docs/manpages/virt-qemu-sev-validate.rst
+++ b/docs/manpages/virt-qemu-sev-validate.rst
@@ -245,6 +245,24 @@ Validate the measurement of a SEV-ES SMP guest booting from disk:
--build-id 13 \
--policy 7
+Validate the measurement of a SEV-ES SMP guest booting from disk, with
+automatically constructed VMSA:
+
+::
+
+ # virt-dom-sev-validate \
+ --firmware OVMF.sev.fd \
+ --num-cpus 2 \
+ --cpu-family 23 \
+ --cpu-model 49 \
+ --cpu-stepping 0 \
+ --tk this-guest-tk.bin \
+ --measurement Zs2pf19ubFSafpZ2WKkwquXvACx9Wt/BV+eJwQ/taO8jhyIj/F8swFrybR1fZ2ID \
+ --api-major 0 \
+ --api-minor 24 \
+ --build-id 13 \
+ --policy 7
+
Fetch from remote libvirt
-------------------------
@@ -291,6 +309,20 @@ Validate the measurement of a SEV-ES SMP guest booting from disk:
--tk this-guest-tk.bin \
--domain fedora34x86_64
+Validate the measurement of a SEV-ES SMP guest booting from disk, with
+automatically constructed VMSA:
+
+::
+
+ # virt-dom-sev-validate \
+ --connect qemu+ssh://root@some.remote.host/system \
+ --firmware OVMF.sev.fd \
+ --cpu-family 23 \
+ --cpu-model 49 \
+ --cpu-stepping 0 \
+ --tk this-guest-tk.bin \
+ --domain fedora34x86_64
+
Fetch from local libvirt
------------------------
@@ -332,6 +364,19 @@ Validate the measurement of a SEV-ES SMP guest booting from disk:
--tk this-guest-tk.bin \
--domain fedora34x86_64
+Validate the measurement of a SEV-ES SMP guest booting from disk, with
+automatically constructed VMSA:
+
+::
+
+ # virt-dom-sev-validate \
+ --insecure \
+ --cpu-family 23 \
+ --cpu-model 49 \
+ --cpu-stepping 0 \
+ --tk this-guest-tk.bin \
+ --domain fedora34x86_64
+
EXIT STATUS
===========
diff --git a/tools/virt-qemu-sev-validate b/tools/virt-qemu-sev-validate
index 6003ba2929..ef8fa6fa27 100755
--- a/tools/virt-qemu-sev-validate
+++ b/tools/virt-qemu-sev-validate
@@ -42,6 +42,7 @@ import hmac
import logging
import re
import socket
+from struct import pack
import sys
import traceback
from uuid import UUID
@@ -72,6 +73,427 @@ class InvalidStateException(Exception):
pass
+class Field(object):
+ U8 = 0
+ U16 = 2
+ U32 = 4
+ U64 = 8
+
+ SCALAR = 0
+ BITMASK = 1
+ ARRAY = 2
+
+ def __init__(self, name, size, fmt, value, order):
+ self.name = name
+ self.size = size
+ self.value = value
+ self.fmt = fmt
+ self.order = order
+
+
+class Struct(object):
+ def __init__(self, size):
+ self._fields = {}
+ self.size = size
+
+ def register_field(self, name, size, fmt=Field.SCALAR, defvalue=0):
+ self._fields[name] = Field(name, size, fmt,
+ defvalue, len(self.fields))
+
+ @property
+ def fields(self):
+ return sorted(self._fields.values(), key=lambda f: f.order)
+
+ def __getattr__(self, name):
+ return self._fields[name]
+
+ def __setattr__(self, name, value):
+ if name in ["_fields", "size"]:
+ super().__setattr__(name, value)
+ else:
+ self._fields[name].value = value
+
+ def binary_format(self):
+ fmt = ["<"]
+ datalen = 0
+ for field in self.fields:
+ if field.size == Field.U8:
+ if field.fmt == Field.ARRAY:
+ datalen += len(field.value)
+ fmt += ["%dB" % len(field.value)]
+ else:
+ datalen += 1
+ fmt += ["B"]
+ elif field.size == Field.U16:
+ datalen += 2
+ fmt += ["H"]
+ elif field.size == Field.U32:
+ datalen += 4
+ fmt += ["L"]
+ elif field.size == Field.U64:
+ datalen += 8
+ fmt += ["Q"]
+
+ pad = self.size - datalen
+ assert self.size >= 1
+ fmt += ["%dB" % pad]
+
+ return "".join(fmt), pad
+
+ def pack(self):
+ fmt, pad = self.binary_format()
+
+ values = []
+ for field in self.fields:
+ if field.size == Field.U8 and field.fmt == Field.ARRAY:
+ for _, k in enumerate(field.value):
+ values.append(k)
+ else:
+ values.append(field.value)
+ values.extend([0] * pad)
+
+ return pack(fmt, *values)
+
+
+class VMSA(Struct):
+ ATTR_G_SHIFT = 23
+ ATTR_G_MASK = (1 << ATTR_G_SHIFT)
+ ATTR_B_SHIFT = 22
+ ATTR_B_MASK = (1 << ATTR_B_SHIFT)
+ ATTR_L_SHIFT = 21
+ ATTR_L_MASK = (1 << ATTR_L_SHIFT)
+ ATTR_AVL_SHIFT = 20
+ ATTR_AVL_MASK = (1 << ATTR_AVL_SHIFT)
+ ATTR_P_SHIFT = 15
+ ATTR_P_MASK = (1 << ATTR_P_SHIFT)
+ ATTR_DPL_SHIFT = 13
+ ATTR_DPL_MASK = (3 << ATTR_DPL_SHIFT)
+ ATTR_S_SHIFT = 12
+ ATTR_S_MASK = (1 << ATTR_S_SHIFT)
+ ATTR_TYPE_SHIFT = 8
+ ATTR_TYPE_MASK = (15 << ATTR_TYPE_SHIFT)
+ ATTR_A_MASK = (1 << 8)
+
+ ATTR_CS_MASK = (1 << 11)
+ ATTR_C_MASK = (1 << 10)
+ ATTR_R_MASK = (1 << 9)
+
+ ATTR_E_MASK = (1 << 10)
+ ATTR_W_MASK = (1 << 9)
+
+ def __init__(self):
+ super().__init__(4096)
+
+ # From Linux arch/x86/include/asm/svm.h, we're unpacking the
+ # struct vmcb_save_area
+
+ self.register_field("es_selector", Field.U16)
+ self.register_field("es_attrib", Field.U16, Field.BITMASK)
+ self.register_field("es_limit", Field.U32)
+ self.register_field("es_base", Field.U64)
+
+ self.register_field("cs_selector", Field.U16)
+ self.register_field("cs_attrib", Field.U16, Field.BITMASK)
+ self.register_field("cs_limit", Field.U32)
+ self.register_field("cs_base", Field.U64)
+
+ self.register_field("ss_selector", Field.U16)
+ self.register_field("ss_attrib", Field.U16, Field.BITMASK)
+ self.register_field("ss_limit", Field.U32)
+ self.register_field("ss_base", Field.U64)
+
+ self.register_field("ds_selector", Field.U16)
+ self.register_field("ds_attrib", Field.U16, Field.BITMASK)
+ self.register_field("ds_limit", Field.U32)
+ self.register_field("ds_base", Field.U64)
+
+ self.register_field("fs_selector", Field.U16)
+ self.register_field("fs_attrib", Field.U16, Field.BITMASK)
+ self.register_field("fs_limit", Field.U32)
+ self.register_field("fs_base", Field.U64)
+
+ self.register_field("gs_selector", Field.U16)
+ self.register_field("gs_attrib", Field.U16, Field.BITMASK)
+ self.register_field("gs_limit", Field.U32)
+ self.register_field("gs_base", Field.U64)
+
+ self.register_field("gdtr_selector", Field.U16)
+ self.register_field("gdtr_attrib", Field.U16, Field.BITMASK)
+ self.register_field("gdtr_limit", Field.U32)
+ self.register_field("gdtr_base", Field.U64)
+
+ self.register_field("ldtr_selector", Field.U16)
+ self.register_field("ldtr_attrib", Field.U16, Field.BITMASK)
+ self.register_field("ldtr_limit", Field.U32)
+ self.register_field("ldtr_base", Field.U64)
+
+ self.register_field("idtr_selector", Field.U16)
+ self.register_field("idtr_attrib", Field.U16, Field.BITMASK)
+ self.register_field("idtr_limit", Field.U32)
+ self.register_field("idtr_base", Field.U64)
+
+ self.register_field("tr_selector", Field.U16)
+ self.register_field("tr_attrib", Field.U16, Field.BITMASK)
+ self.register_field("tr_limit", Field.U32)
+ self.register_field("tr_base", Field.U64)
+
+ self.register_field("reserved_1",
+ Field.U8, Field.ARRAY, bytearray([0] * 43))
+
+ self.register_field("cpl", Field.U8)
+
+ self.register_field("reserved_2",
+ Field.U8, Field.ARRAY, bytearray([0] * 4))
+
+ self.register_field("efer", Field.U64)
+
+ self.register_field("reserved_3",
+ Field.U8, Field.ARRAY, bytearray([0] * 104))
+
+ self.register_field("xss", Field.U64)
+ self.register_field("cr4", Field.U64)
+ self.register_field("cr3", Field.U64)
+ self.register_field("cr0", Field.U64)
+ self.register_field("dr7", Field.U64)
+ self.register_field("dr6", Field.U64)
+ self.register_field("rflags", Field.U64)
+ self.register_field("rip", Field.U64)
+
+ self.register_field("reserved_4",
+ Field.U8, Field.ARRAY, bytearray([0] * 88))
+
+ self.register_field("rsp", Field.U64)
+
+ self.register_field("reserved_5",
+ Field.U8, Field.ARRAY, bytearray([0] * 24))
+
+ self.register_field("rax", Field.U64)
+ self.register_field("star", Field.U64)
+ self.register_field("lstar", Field.U64)
+ self.register_field("cstar", Field.U64)
+ self.register_field("sfmask", Field.U64)
+ self.register_field("kernel_gs_base", Field.U64)
+ self.register_field("sysenter_cs", Field.U64)
+ self.register_field("sysenter_esp", Field.U64)
+ self.register_field("sysenter_eip", Field.U64)
+ self.register_field("cr2", Field.U64)
+
+ self.register_field("reserved_6",
+ Field.U8, Field.ARRAY, bytearray([0] * 32))
+
+ self.register_field("g_pat", Field.U64)
+ self.register_field("dbgctl", Field.U64)
+ self.register_field("br_from", Field.U64)
+ self.register_field("br_to", Field.U64)
+ self.register_field("last_excp_from", Field.U64)
+ self.register_field("last_excp_to", Field.U64)
+
+ self.register_field("reserved_7",
+ Field.U8, Field.ARRAY, bytearray([0] * 72))
+
+ self.register_field("spec_ctrl", Field.U32)
+
+ self.register_field("reserved_7b",
+ Field.U8, Field.ARRAY, bytearray([0] * 4))
+
+ self.register_field("pkru", Field.U32)
+
+ self.register_field("reserved_7a",
+ Field.U8, Field.ARRAY, bytearray([0] * 20))
+
+ self.register_field("reserved_8", Field.U64) # rax duplicate
+
+ self.register_field("rcx", Field.U64)
+ self.register_field("rdx", Field.U64, Field.BITMASK)
+ self.register_field("rbx", Field.U64)
+
+ self.register_field("reserved_9", Field.U64) # rsp duplicate
+
+ self.register_field("rbp", Field.U64)
+ self.register_field("rsi", Field.U64)
+ self.register_field("rdi", Field.U64)
+ self.register_field("r8", Field.U64)
+ self.register_field("r9", Field.U64)
+ self.register_field("r10", Field.U64)
+ self.register_field("r11", Field.U64)
+ self.register_field("r12", Field.U64)
+ self.register_field("r13", Field.U64)
+ self.register_field("r14", Field.U64)
+ self.register_field("r15", Field.U64)
+
+ self.register_field("reserved_10",
+ Field.U8, Field.ARRAY, bytearray([0] * 16))
+
+ self.register_field("sw_exit_code", Field.U64)
+ self.register_field("sw_exit_info_1", Field.U64)
+ self.register_field("sw_exit_info_2", Field.U64)
+ self.register_field("sw_scratch", Field.U64)
+
+ self.register_field("reserved_11",
+ Field.U8, Field.ARRAY, bytearray([0] * 56))
+
+ self.register_field("xcr0", Field.U64)
+ self.register_field("valid_bitmap",
+ Field.U8, Field.ARRAY, bytearray([0] * 16))
+ self.register_field("x87_state_gpa",
+ Field.U64)
+
+ def amd64_cpu_init(self):
+ # AMD64 Architecture Programmer’s Manual
+ # Volume 2: System Programming.
+ #
+ # 14.1.3 Processor Initialization State
+ #
+ # Values after INIT
+
+ self.cr0 = (1 << 4)
+ self.rip = 0xfff0
+
+ self.cs_selector = 0xf000
+ self.cs_base = 0xffff0000
+ self.cs_limit = 0xffff
+
+ self.ds_limit = 0xffff
+
+ self.es_limit = 0xffff
+ self.fs_limit = 0xffff
+ self.gs_limit = 0xffff
+ self.ss_limit = 0xffff
+
+ self.gdtr_limit = 0xffff
+ self.idtr_limit = 0xffff
+
+ self.ldtr_limit = 0xffff
+ self.tr_limit = 0xffff
+
+ self.dr6 = 0xffff0ff0
+ self.dr7 = 0x0400
+ self.rflags = 0x2
+ self.xcr0 = 0x1
+
+ def kvm_cpu_init(self):
+ # svm_set_cr4() sets guest X86_CR4_MCE bit if host
+ # has X86_CR4_MCE enabled
+ self.cr4 = 0x40
+
+ # svm_set_efer sets guest EFER_SVME (Secure Virtual Machine enable)
+ self.efer = 0x1000
+
+ # init_vmcb + init_sys_seg() sets
+ # SVM_SELECTOR_P_MASK | SEG_TYPE_LDT
+ self.ldtr_attrib = 0x0082
+
+ # init_vmcb + init_sys_seg() sets
+ # SVM_SELECTOR_P_MASK | SEG_TYPE_BUSY_TSS16
+ self.tr_attrib = 0x0083
+
+ # kvm_arch_vcpu_create() in arch/x86/kvm/x86.c
+ self.g_pat = 0x0007040600070406
+
+ def qemu_cpu_init(self):
+ # Based on logic in x86_cpu_reset()
+ #
+ # file target/i386/cpu.c
+
+ def attr(mask):
+ return (mask >> VMSA.ATTR_TYPE_SHIFT)
+
+ self.ldtr_attrib = attr(VMSA.ATTR_P_MASK |
+ (2 << VMSA.ATTR_TYPE_SHIFT))
+ self.tr_attrib = attr(VMSA.ATTR_P_MASK |
+ (11 << VMSA.ATTR_TYPE_SHIFT))
+ self.cs_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_CS_MASK |
+ VMSA.ATTR_R_MASK |
+ VMSA.ATTR_A_MASK)
+ self.ds_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+ self.es_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+ self.ss_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+ self.fs_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+ self.gs_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+
+ self.g_pat = 0x0007040600070406
+
+ def cpu_sku(self, family, model, stepping):
+ stepping &= 0xf
+ model &= 0xff
+ family &= 0xfff
+
+ self.rdx.value = stepping
+
+ if family > 0xf:
+ self.rdx.value |= 0xf00 | ((family - 0x0f) << 20)
+ else:
+ self.rdx.value |= family << 8
+
+ self.rdx.value |= ((model & 0xf) << 4) | ((model >> 4) <<
16)
+
+ def reset_addr(self, reset_addr):
+ reset_cs = reset_addr & 0xffff0000
+ reset_ip = reset_addr & 0x0000ffff
+
+ self.rip.value = reset_ip
+ self.cs_base.value = reset_cs
+
+
+class OVMF(object):
+
+ OVMF_TABLE_FOOTER_GUID = UUID("96b582de-1fb2-45f7-baea-a366c55a082d")
+ SEV_INFO_BLOCK_GUID = UUID("00f771de-1a7e-4fcb-890e-68c77e2fb44e")
+
+ def __init__(self):
+ self.entries = {}
+
+ def load(self, content):
+ expect = OVMF.OVMF_TABLE_FOOTER_GUID.bytes_le
+ actual = content[-48:-32]
+ if expect != actual:
+ raise Exception("OVMF footer GUID not found")
+
+ tablelen = int.from_bytes(content[-50:-48], byteorder='little')
+
+ if tablelen == 0:
+ raise Exception("OVMF tables zero length")
+
+ table = content[-(32 + tablelen):-50]
+
+ self.parse_table(table)
+
+ def parse_table(self, data):
+ while len(data) > 0:
+ entryuuid = UUID(bytes_le=data[-16:])
+ entrylen = int.from_bytes(data[-18:-16], byteorder='little')
+ entrydata = data[-entrylen:-18]
+
+ self.entries[str(entryuuid)] = entrydata
+
+ data = data[0:-entrylen]
+
+ def reset_addr(self):
+ if str(OVMF.SEV_INFO_BLOCK_GUID) not in self.entries:
+ raise Exception("SEV info block GUID not found")
+
+ reset_addr = int.from_bytes(
+ self.entries[str(OVMF.SEV_INFO_BLOCK_GUID)], "little")
+ return reset_addr
+
+
class GUIDTable(abc.ABC):
GUID_LEN = 16
@@ -242,6 +664,26 @@ class ConfidentialVM(object):
log.debug("VMSA CPU 1(sha256): %s",
sha256(self.vmsa_cpu1).hexdigest())
+ def build_vmsas(self, family, model, stepping):
+ ovmf = OVMF()
+ ovmf.load(self.firmware)
+
+ vmsa = VMSA()
+ vmsa.amd64_cpu_init()
+ vmsa.kvm_cpu_init()
+ vmsa.qemu_cpu_init()
+
+ vmsa.cpu_sku(family, model, stepping)
+
+ self.vmsa_cpu0 = vmsa.pack()
+ log.debug("VMSA CPU 0(sha256): %s",
+ sha256(self.vmsa_cpu0).hexdigest())
+
+ vmsa.reset_addr(ovmf.reset_addr())
+ self.vmsa_cpu1 = vmsa.pack()
+ log.debug("VMSA CPU 1(sha256): %s",
+ sha256(self.vmsa_cpu1).hexdigest())
+
def get_cpu_state(self):
if self.num_cpus is None:
raise UnsupportedUsageException(
@@ -514,6 +956,12 @@ def parse_command_line():
help='VMSA state for the boot CPU')
vmconfig.add_argument('--vmsa-cpu1', '-1',
help='VMSA state for the additional CPUs')
+ vmconfig.add_argument('--cpu-family', type=int,
+ help='Hypervisor host CPU family number')
+ vmconfig.add_argument('--cpu-model', type=int,
+ help='Hypervisor host CPU model number')
+ vmconfig.add_argument('--cpu-stepping', type=int,
+ help='Hypervisor host CPU stepping number')
vmconfig.add_argument('--tik',
help='TIK file for domain')
vmconfig.add_argument('--tek',
@@ -577,6 +1025,20 @@ def check_usage(args):
raise UnsupportedUsageException(
"--initrd/--cmdline require --kernel")
+ sku = [args.cpu_family, args.cpu_model, args.cpu_stepping]
+ if sku.count(None) == len(sku):
+ if args.vmsa_cpu1 is not None and args.vmsa_cpu0 is None:
+ raise UnsupportedUsageException(
+ "VMSA for additional CPU also requires VMSA for boot CPU")
+ else:
+ if args.vmsa_cpu0 is not None or args.vmsa_cpu1 is not None:
+ raise UnsupportedUsageException(
+ "VMSA files are mutually exclusive with CPU SKU")
+
+ if sku.count(None) != 0:
+ raise UnsupportedUsageException(
+ "CPU SKU needs family, model and stepping for SEV-ES domain")
+
def attest(args):
if args.domain is None:
@@ -617,6 +1079,11 @@ def attest(args):
if args.vmsa_cpu1 is not None:
cvm.load_vmsa_cpu1(args.vmsa_cpu1)
+ if args.cpu_family is not None:
+ cvm.build_vmsas(args.cpu_family,
+ args.cpu_model,
+ args.cpu_stepping)
+
if args.domain is not None:
cvm.load_domain(args.connect,
args.domain,
--
2.37.3