[libvirt PATCH v4 09/12] tools: support generating SEV secret injection tables

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



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: Ján Tomko <jtomko@xxxxxxxxxx>
Signed-off-by: Daniel P. Berrangé <berrange@xxxxxxxxxx>
---
 docs/manpages/virt-qemu-sev-validate.rst |  83 ++++++++++
 tools/virt-qemu-sev-validate             | 183 +++++++++++++++++++++--
 2 files changed, 257 insertions(+), 9 deletions(-)

diff --git a/docs/manpages/virt-qemu-sev-validate.rst b/docs/manpages/virt-qemu-sev-validate.rst
index eef8a0281c..24cdbb6d92 100644
--- a/docs/manpages/virt-qemu-sev-validate.rst
+++ b/docs/manpages/virt-qemu-sev-validate.rst
@@ -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@xxxxxxxxxxxxxxxx/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
 ===========
 
diff --git a/tools/virt-qemu-sev-validate b/tools/virt-qemu-sev-validate
index 37f6f65bac..712a4e4593 100755
--- a/tools/virt-qemu-sev-validate
+++ b/tools/virt-qemu-sev-validate
@@ -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:
-- 
2.37.3




[Index of Archives]     [Virt Tools]     [Libvirt Users]     [Lib OS Info]     [Fedora Users]     [Fedora Desktop]     [Fedora SELinux]     [Big List of Linux Books]     [Yosemite News]     [KDE Users]     [Fedora Tools]

  Powered by Linux