tools: support generating SEV secret injection tables

It is possible to build OVMF for SEV with an embedded Grub that can
fetch LUKS disk secrets. This adds support for injecting secrets in
the required format.

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-01-07 11:45:27 +00:00
parent 273c408899
commit b348f37445
2 changed files with 257 additions and 9 deletions

View File

@ -189,6 +189,46 @@ understand any configuration mistakes that have been made. If the
will be skipped. The result is that the validation will likely be reported as
failed.
Secret injection options
------------------------
These options provide a way to inject a secret if validation of the
launch measurement passes.
``--inject-secret ALIAS-OR-GUID:PATH``
Path to a file containing a secret to inject into the guest OS. Typical
usage would be to supply a password for unlocking the root filesystem
full disk encryption. ``ALIAS`` can be one of the well known secrets:
* ``luks-key`` - bytes to use as a key for unlocking a LUKS key slot.
GUID of ``736869e5-84f0-4973-92ec-06879ce3da0b``.
Alternatively ``GUID`` refers to an arbitrary UUID of the callers
choosing. The contents of ``PATH`` are defined by the requirements
of the associated GUID, and will used as-is without modification.
In particular be aware:
* Avoid unwanted trailing newline characters in ``PATH`` unless
mandated by the ``GUID``.
* Any trailing ``NUL`` byte must be explicitly included in ``PATH``
if mandated by the ``GUID``.
This argument can be repeated multiple times, provided a different
``GUID`` is given for each instance.
``--secret-header PATH``
Path to a file in which the injected secret header will be written in base64
format and later injected into the domain. This is required if there is no
connection to libvirt, otherwise the secret will be directly injected.
``--secret-payload PATH``
Path to a file in which the injected secret payload will be written in base64
format and later injected into the domain. This is required if there is no
connection to libvirt, otherwise the secret will be directly injected.
EXAMPLES
========
@ -263,6 +303,26 @@ automatically constructed VMSA:
--build-id 13 \
--policy 7
Validate the measurement of a SEV guest booting from disk and
inject a disk password on success:
::
# virt-dom-sev-validate \
--loader OVMF.sev.fd \
--tk this-guest-tk.bin \
--measurement Zs2pf19ubFSafpZ2WKkwquXvACx9Wt/BV+eJwQ/taO8jhyIj/F8swFrybR1fZ2ID \
--api-major 0 \
--api-minor 24 \
--build-id 13 \
--policy 3 \
--disk-password passwd.txt \
--secret-header secret-header.b64 \
--secret-payload secret-payload.b64
The ``secret-header.b64`` and ``secret-payload.b64`` files can now be sent to
the virtualization host for injection.
Fetch from remote libvirt
-------------------------
@ -323,6 +383,18 @@ automatically constructed VMSA:
--tk this-guest-tk.bin \
--domain fedora34x86_64
Validate the measurement of a SEV guest booting from disk and
inject a disk password on success:
::
# virt-dom-sev-validate \
--connect qemu+ssh://root@some.remote.host/system \
--loader OVMF.sev.fd \
--tk this-guest-tk.bin \
--domain fedora34x86_64 \
--disk-password passwd.txt
Fetch from local libvirt
------------------------
@ -373,6 +445,17 @@ automatically constructed VMSA:
--tk this-guest-tk.bin \
--domain fedora34x86_64
Validate the measurement of a SEV guest booting from disk and
inject a disk password on success:
::
# virt-dom-sev-validate \
--insecure \
--tk this-guest-tk.bin \
--domain fedora34x86_64 \
--disk-password passwd.txt
EXIT STATUS
===========

View File

@ -36,16 +36,19 @@
import abc
import argparse
from base64 import b64decode
from base64 import b64decode, b64encode
from hashlib import sha256
import hmac
import logging
import os
import re
import socket
from struct import pack
import sys
import traceback
from uuid import UUID
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from lxml import etree
import libvirt
@ -579,7 +582,46 @@ class KernelTable(GUIDTable):
return entries
class ConfidentialVM(object):
class SecretsTable(GUIDTable):
TABLE_GUID = UUID('{1e74f542-71dd-4d66-963e-ef4287ff173b}').bytes_le
GUID_ALIASES = {
"luks-key": UUID('{736869e5-84f0-4973-92ec-06879ce3da0b}')
}
def __init__(self):
super().__init__(guid=self.TABLE_GUID,
lenlen=4)
self.secrets = {}
def load_secret(self, alias_or_guid, path):
guid = None
if re.match(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
alias_or_guid):
guid = UUID(alias_or_guid)
else:
if alias_or_guid not in self.GUID_ALIASES:
raise UnsupportedUsageException(
"Secret alias '%s' is not known" % alias_or_guid)
guid = self.GUID_ALIASES[alias_or_guid]
if guid in self.secrets:
raise UnsupportedUsageException(
"Secret for GUID %s already loaded" % guid)
with open(path, 'rb') as fh:
self.secrets[guid] = fh.read()
def entries(self):
entries = bytes([])
for guid, value in self.secrets.items():
entries += self.build_entry(guid.bytes_le, value, 4)
return entries
class ConfidentialVM(abc.ABC):
POLICY_BIT_SEV_ES = 2
POLICY_VAL_SEV_ES = (1 << POLICY_BIT_SEV_ES)
@ -605,6 +647,7 @@ class ConfidentialVM(object):
self.vmsa_cpu1 = None
self.kernel_table = KernelTable()
self.secrets_table = SecretsTable()
def is_sev_es(self):
return self.policy & self.POLICY_VAL_SEV_ES
@ -757,6 +800,82 @@ class ConfidentialVM(object):
raise AttestationFailedException(
"Measurement does not match, VM is not trustworthy")
def build_secrets(self):
measurement, _ = self.get_measurements()
iv = os.urandom(16)
secret_table = self.secrets_table.build()
cipher = Cipher(algorithms.AES(self.tek), modes.CTR(iv))
enc = cipher.encryptor()
secret_table_ciphertext = (enc.update(secret_table) +
enc.finalize())
flags = 0
##
# Table 55. LAUNCH_SECRET Packet Header Buffer
##
header = (
flags.to_bytes(4, byteorder='little') +
iv
)
# AMD Secure Encrypted Virtualization API , section 6.6
#
# hdrmac = HMAC(0x01 || FLAGS || IV || GUEST_LENGTH ||
# TRANS_LENGTH || DATA ||
# MEASURE; GCTX.TIK)
#
msg = (
bytes([0x01]) +
flags.to_bytes(4, byteorder='little') +
iv +
len(secret_table).to_bytes(4, byteorder='little') +
len(secret_table).to_bytes(4, byteorder='little') +
secret_table_ciphertext +
measurement
)
h = hmac.new(self.tik, msg, 'sha256')
header = (
flags.to_bytes(4, byteorder='little') +
iv +
h.digest()
)
header64 = b64encode(header).decode('utf8')
secret64 = b64encode(secret_table_ciphertext).decode('utf8')
log.debug("Header: %s (%d bytes)", header64, len(header))
log.debug("Secret: %s (%d bytes)",
secret64, len(secret_table_ciphertext))
return header64, secret64
@abc.abstractmethod
def inject_secrets(self):
pass
class OfflineConfidentialVM(ConfidentialVM):
def __init__(self,
secret_header=None,
secret_payload=None,
**kwargs):
super().__init__(**kwargs)
self.secret_header = secret_header
self.secret_payload = secret_payload
def inject_secrets(self):
header64, secret64 = self.build_secrets()
with open(self.secret_header, "wb") as fh:
fh.write(header64.encode('utf8'))
with open(self.secret_payload, "wb") as fh:
fh.write(secret64.encode('utf8'))
class LibvirtConfidentialVM(ConfidentialVM):
def __init__(self, **kwargs):
@ -944,6 +1063,14 @@ class LibvirtConfidentialVM(ConfidentialVM):
cpu_stepping = int(sig[0].get("stepping"))
self.build_vmsas(cpu_family, cpu_model, cpu_stepping)
def inject_secrets(self):
header64, secret64 = self.build_secrets()
params = {"sev-secret": secret64,
"sev-secret-header": header64}
self.dom.setLaunchSecurityState(params, 0)
self.dom.resume()
def parse_command_line():
parser = argparse.ArgumentParser(
@ -1006,6 +1133,15 @@ def parse_command_line():
vmconn.add_argument('--ignore-config', '-g', action='store_true',
help='Do not attempt to sanity check the guest config')
# Arguments related to secret injection
inject = parser.add_argument_group("Secret injection parameters")
inject.add_argument('--inject-secret', '-s', action='append', default=[],
help='ALIAS-OR-GUID:PATH file containing secret to inject')
inject.add_argument('--secret-payload',
help='Path to file to write secret data payload to')
inject.add_argument('--secret-header',
help='Path to file to write secret data header to')
return parser.parse_args()
@ -1046,6 +1182,15 @@ def check_usage(args):
raise UnsupportedUsageException(
"Either --firmware or --domain is required")
if len(args.inject_secret) > 0:
if args.secret_header is None:
raise UnsupportedUsageException(
"Either --secret-header or --domain is required")
if args.secret_payload is None:
raise UnsupportedUsageException(
"Either --secret-payload or --domain is required")
if args.kernel is None:
if args.initrd is not None or args.cmdline is not None:
raise UnsupportedUsageException(
@ -1065,15 +1210,22 @@ def check_usage(args):
raise UnsupportedUsageException(
"CPU SKU needs family, model and stepping for SEV-ES domain")
secret = [args.secret_payload, args.secret_header]
if secret.count(None) > 0 and secret.count(None) != len(secret):
raise UnsupportedUsageException(
"Both --secret-payload and --secret-header are required")
def attest(args):
if args.domain is None:
cvm = ConfidentialVM(measurement=args.measurement,
api_major=args.api_major,
api_minor=args.api_minor,
build_id=args.build_id,
policy=args.policy,
num_cpus=args.num_cpus)
cvm = OfflineConfidentialVM(measurement=args.measurement,
api_major=args.api_major,
api_minor=args.api_minor,
build_id=args.build_id,
policy=args.policy,
num_cpus=args.num_cpus,
secret_header=args.secret_header,
secret_payload=args.secret_payload)
else:
cvm = LibvirtConfidentialVM(measurement=args.measurement,
api_major=args.api_major,
@ -1117,10 +1269,23 @@ def attest(args):
args.ignore_config)
cvm.attest()
if not args.quiet:
print("OK: Looks good to me")
for secret in args.inject_secret:
bits = secret.split(":")
if len(bits) != 2:
raise UnsupportedUsageException(
"Expecting ALIAS-OR-GUID:PATH for injected secret")
cvm.secrets_table.load_secret(bits[0], bits[1])
if len(args.inject_secret) > 0:
cvm.inject_secrets()
if not args.quiet:
print("OK: Injected %d secrets" % len(args.inject_secret))
def main():
args = parse_command_line()
if args.debug: