tools: support automatically constructing SEV-ES vCPU state

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: Cole Robinson <crobinso@redhat.com>
Reviewed-by: Ján Tomko <jtomko@redhat.com>
Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
This commit is contained in:
Daniel P. Berrangé 2022-10-05 13:08:17 +01:00
parent 3e7b7da9e0
commit 676df5b358
2 changed files with 512 additions and 0 deletions

View File

@ -245,6 +245,24 @@ Validate the measurement of a SEV-ES SMP guest booting from disk:
--build-id 13 \ --build-id 13 \
--policy 7 --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 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 \ --tk this-guest-tk.bin \
--domain fedora34x86_64 --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 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 \ --tk this-guest-tk.bin \
--domain fedora34x86_64 --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 EXIT STATUS
=========== ===========

View File

@ -42,6 +42,7 @@ import hmac
import logging import logging
import re import re
import socket import socket
from struct import pack
import sys import sys
import traceback import traceback
from uuid import UUID from uuid import UUID
@ -72,6 +73,427 @@ class InvalidStateException(Exception):
pass 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 Programmers 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): class GUIDTable(abc.ABC):
GUID_LEN = 16 GUID_LEN = 16
@ -242,6 +664,26 @@ class ConfidentialVM(object):
log.debug("VMSA CPU 1(sha256): %s", log.debug("VMSA CPU 1(sha256): %s",
sha256(self.vmsa_cpu1).hexdigest()) 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): def get_cpu_state(self):
if self.num_cpus is None: if self.num_cpus is None:
raise UnsupportedUsageException( raise UnsupportedUsageException(
@ -514,6 +956,12 @@ def parse_command_line():
help='VMSA state for the boot CPU') help='VMSA state for the boot CPU')
vmconfig.add_argument('--vmsa-cpu1', '-1', vmconfig.add_argument('--vmsa-cpu1', '-1',
help='VMSA state for the additional CPUs') 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', vmconfig.add_argument('--tik',
help='TIK file for domain') help='TIK file for domain')
vmconfig.add_argument('--tek', vmconfig.add_argument('--tek',
@ -577,6 +1025,20 @@ def check_usage(args):
raise UnsupportedUsageException( raise UnsupportedUsageException(
"--initrd/--cmdline require --kernel") "--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): def attest(args):
if args.domain is None: if args.domain is None:
@ -617,6 +1079,11 @@ def attest(args):
if args.vmsa_cpu1 is not None: if args.vmsa_cpu1 is not None:
cvm.load_vmsa_cpu1(args.vmsa_cpu1) 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: if args.domain is not None:
cvm.load_domain(args.connect, cvm.load_domain(args.connect,
args.domain, args.domain,