diff --git a/docs/manpages/meson.build b/docs/manpages/meson.build index b5556996a4..84b2e247e9 100644 --- a/docs/manpages/meson.build +++ b/docs/manpages/meson.build @@ -20,6 +20,7 @@ docs_man_files = [ { 'name': 'virt-qemu-run', 'section': '1', 'install': conf.has('WITH_QEMU') }, { 'name': 'virt-qemu-qmp-proxy', 'section': '1', 'install': conf.has('WITH_QEMU') }, { 'name': 'virt-xml-validate', 'section': '1', 'install': true }, + { 'name': 'virt-qemu-sev-validate', 'section': '1', 'install': conf.has('WITH_QEMU') }, { 'name': 'libvirt-guests', 'section': '8', 'install': conf.has('WITH_LIBVIRTD') }, { 'name': 'libvirtd', 'section': '8', 'install': conf.has('WITH_LIBVIRTD') }, diff --git a/docs/manpages/virt-qemu-sev-validate.rst b/docs/manpages/virt-qemu-sev-validate.rst new file mode 100644 index 0000000000..0f6c64ba90 --- /dev/null +++ b/docs/manpages/virt-qemu-sev-validate.rst @@ -0,0 +1,207 @@ +====================== +virt-qemu-sev-validate +====================== + +-------------------------------------------- +validate a domain AMD SEV launch measurement +-------------------------------------------- + +:Manual section: 1 +:Manual group: Virtualization Support + +.. contents:: + +SYNOPSIS +======== + + +``virt-qemu-sev-validate`` [*OPTIONS*] + + +DESCRIPTION +=========== + +This program validates the reported measurement for a domain launched with AMD +SEV. If the program exits with a status of zero, the guest owner can be +confident that their guest OS is running under the protection offered by the +SEV / SEV-ES platform. + +Note that the level of protection varies depending on the AMD SEV platform +generation and describing the differences is outside the scope of this +document. + +For the results of this program to be considered trustworthy, it is required to +be run on a machine that is already trusted by the guest owner. This could be a +machine that the guest owner has direct physical control over, or it could be +another virtual machine protected by AMD SEV that has already had its launch +measurement validated. Running this program on the virtualization host will not +produce an answer that can be trusted. + +OPTIONS +======= + +Common options +-------------- + +``-h``, ``--help`` + +Display command line help usage then exit. + +``-d``, ``--debug`` + +Show debug information while running + +``-q``, ``--quiet`` + +Don't print information about the attestation result. + +Guest state options +------------------- + +These options provide information about the state of the guest that needs its +boot attested. + +``--measurement BASE64-STRING`` + +The launch measurement reported by the hypervisor of the domain to be validated. +The measurement must be 48 bytes of binary data encoded as a base64 string. + +``--api-major VERSION`` + +The SEV API major version of the hypervisor the domain is running on. + +``--api-minor VERSION`` + +The SEV API major version of the hypervisor the domain is running on. + +``--build-id ID`` + +The SEV build ID of the hypervisor the domain is running on. + +``--policy POLiCY`` + +The policy bitmask associated with the session launch data of the domain to be +validated. + +Guest config options +-------------------- + +These options provide items needed to calculate the expected domain launch +measurement. This will then be compared to the reported launch measurement. + +``-f PATH``, ``--firmware=PATH`` + +Path to the firmware loader binary. This is the EDK2 build that knows how to +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. + +``--tik PATH`` + +TIK file for domain. This file must be exactly 16 bytes in size and contains the +unique transport integrity key associated with the domain session launch data. +This is mutually exclusive with the ``--tk`` argument. + +``--tek PATH`` + +TEK file for domain. This file must be exactly 16 bytes in size and contains the +unique transport encryption key associated with the domain session launch data. +This is mutually exclusive with the ``--tk`` argument. + +``--tk PATH`` + +TEK/TIK combined file for the domain. This file must be exactly 32 bytes in +size, with the first 16 bytes containing the TEK and the last 16 bytes +containing the TIK. This is mutually exclusive with the ``--tik`` and ``--tek`` +arguments. + +EXAMPLES +======== + +Fully offline execution +----------------------- + +This scenario allows a measurement to be securely validated in a completely +offline state without any connection to the hypervisor host. All required +data items must be provided as command line parameters. This usage model is +considered secure, because all input data is provided by the user. + +Validate the measurement of a SEV guest booting from disk: + +:: + + # virt-qemu-sev-validate \ + --firmware 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 + +EXIT STATUS +=========== + +Upon successful attestation of the launch measurement, an exit status of 0 will +be set. + +Upon failure to attest the launch measurement one of the following codes will +be set: + +* **1** - *Guest measurement did not validate* + + Assuming the inputs to this program are correct, the virtual machine launch + has been compromised and it should not be trusted henceforth. + +* **2** - *Usage scenario cannot be supported* + + The way in which this program has been invoked prevent it from being able to + validate the launch measurement. + +* **3** - *unexpected error occurred in the code* + + A logic flaw in this program means it is unable to complete the validation of + the measurement. This is a bug which should be reported to the maintainers. + +AUTHOR +====== + +Daniel P. Berrangé + + +BUGS +==== + +Please report all bugs you discover. This should be done via either: + +#. the mailing list + + `https://libvirt.org/contact.html `_ + +#. the bug tracker + + `https://libvirt.org/bugs.html `_ + +Alternatively, you may report bugs to your software distributor / vendor. + + +COPYRIGHT +========= + +Copyright (C) 2022 by Red Hat, Inc. + + +LICENSE +======= + +``virt-qemu-sev-validate`` is distributed under the terms of the GNU LGPL v2.1+. +This is free software; see the source for copying conditions. There +is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR +PURPOSE + + +SEE ALSO +======== + +virsh(1), `SEV launch security usage `_, +`https://www.libvirt.org/ `_ diff --git a/libvirt.spec.in b/libvirt.spec.in index ac5bf7b865..eb8ebbdd3f 100644 --- a/libvirt.spec.in +++ b/libvirt.spec.in @@ -2185,7 +2185,9 @@ exit 0 %if %{with_qemu} %files client-qemu %{_mandir}/man1/virt-qemu-qmp-proxy.1* +%{_mandir}/man1/virt-qemu-sev-validate.1* %{_bindir}/virt-qemu-qmp-proxy +%{_bindir}/virt-qemu-sev-validate %endif %files libs -f %{name}.lang diff --git a/tools/meson.build b/tools/meson.build index 20509906af..c41c619af4 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -299,6 +299,11 @@ if conf.has('WITH_SANLOCK') ) endif +if conf.has('WITH_QEMU') + install_data('virt-qemu-sev-validate', + install_dir: bindir) +endif + if conf.has('WITH_LIBVIRTD') configure_file( input: 'libvirt-guests.sh.in', diff --git a/tools/virt-qemu-sev-validate b/tools/virt-qemu-sev-validate new file mode 100755 index 0000000000..7ff54e7623 --- /dev/null +++ b/tools/virt-qemu-sev-validate @@ -0,0 +1,263 @@ +#!/usr/bin/python3 +# +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# Validates a guest AMD SEV launch measurement +# +# A general principle in writing this tool is that it must calculate the +# expected measurement based entirely on information it receives on the CLI +# from the guest owner. +# +# It cannot generally trust information obtained from the guest XML or from the +# virtualization host OS. The main exceptions are: +# +# - The guest measurement +# +# This is a result of cryptographic operation using a shared secret known +# only to the guest owner and SEV platform, not the host OS. +# +# - The guest policy +# +# This is encoded in the launch session blob that is encrypted with a shared +# secret known only to the guest owner and SEV platform, not the host OS. It +# is impossible for the host OS to maliciously launch a guest with different +# policy and the user provided launch session blob. +# +# CAVEAT: the user must ALWAYS create a launch blob with freshly generated +# TIK/TEK for every new VM. Re-use of the same TIK/TEK for multiple VMs +# is insecure. +# +# - The SEV API version / build ID +# +# This does not have an impact on the security of the measurement, unless +# the guest owner needs a guarantee that the host is not using specific +# firmware versions with known flaws. +# + +import argparse +from base64 import b64decode +from hashlib import sha256 +import hmac +import logging +import sys +import traceback + +log = logging.getLogger() + + +class AttestationFailedException(Exception): + pass + + +class UnsupportedUsageException(Exception): + pass + + +class ConfidentialVM(object): + + def __init__(self, + measurement=None, + api_major=None, + api_minor=None, + build_id=None, + policy=None): + self.measurement = measurement + self.api_major = api_major + self.api_minor = api_minor + self.build_id = build_id + self.policy = policy + + self.firmware = None + self.tik = None + self.tek = None + + def load_tik_tek(self, tik_path, tek_path): + with open(tik_path, 'rb') as fh: + self.tik = fh.read() + log.debug("TIK(hex): %s", self.tik.hex()) + + if len(self.tik) != 16: + raise UnsupportedUsageException( + "Expected 16 bytes in TIK file, but got %d" % len(self.tik)) + + with open(tek_path, 'rb') as fh: + self.tek = fh.read() + log.debug("TEK(hex): %s", self.tek.hex()) + + if len(self.tek) != 16: + raise UnsupportedUsageException( + "Expected 16 bytes in TEK file, but got %d" % len(self.tek)) + + def load_tk(self, tk_path): + with open(tk_path, 'rb') as fh: + tk = fh.read() + + if len(tk) != 32: + raise UnsupportedUsageException( + "Expected 32 bytes in TIK/TEK file, but got %d" % len(tk)) + + self.tek = tk[0:16] + self.tik = tk[16:32] + log.debug("TIK(hex): %s", self.tik.hex()) + log.debug("TEK(hex): %s", self.tek.hex()) + + def load_firmware(self, firmware_path): + with open(firmware_path, 'rb') as fh: + self.firmware = fh.read() + log.debug("Firmware(sha256): %s", sha256(self.firmware).hexdigest()) + + # Get the full set of measured launch data for the domain + # + # The measured data that the guest is initialized with is the concatenation + # of the following: + # + # - The firmware blob + def get_measured_data(self): + measured_data = self.firmware + log.debug("Measured-data(sha256): %s", + sha256(measured_data).hexdigest()) + return measured_data + + # Get the reported and computed launch measurements for the domain + # + # AMD Secure Encrypted Virtualization API , section 6.5: + # + # measurement = HMAC(0x04 || API_MAJOR || API_MINOR || BUILD || + # GCTX.POLICY || GCTX.LD || MNONCE; GCTX.TIK) + # + # Where GCTX.LD covers all the measured data the guest is initialized with + # per get_measured_data(). + def get_measurements(self): + measurement = b64decode(self.measurement) + reported = measurement[0:32] + nonce = measurement[32:48] + + measured_data = self.get_measured_data() + msg = ( + bytes([0x4]) + + self.api_major.to_bytes(1, 'little') + + self.api_minor.to_bytes(1, 'little') + + self.build_id.to_bytes(1, 'little') + + self.policy.to_bytes(4, 'little') + + sha256(measured_data).digest() + + nonce + ) + log.debug("Measured-msg(hex): %s", msg.hex()) + + computed = hmac.new(self.tik, msg, 'sha256').digest() + + log.debug("Measurement reported(hex): %s", reported.hex()) + log.debug("Measurement computed(hex): %s", computed.hex()) + + return reported, computed + + def attest(self): + reported, computed = self.get_measurements() + + if reported != computed: + raise AttestationFailedException( + "Measurement does not match, VM is not trustworthy") + + +def parse_command_line(): + parser = argparse.ArgumentParser( + description='Validate guest AMD SEV launch measurement') + parser.add_argument('--debug', '-d', action='store_true', + help='Show debug information') + parser.add_argument('--quiet', '-q', action='store_true', + help='Do not display status') + + # Arguments related to the state of the launched guest + vmstate = parser.add_argument_group("Virtual machine launch state") + vmstate.add_argument('--measurement', '-m', required=True, + help='Measurement for the running domain') + vmstate.add_argument('--api-major', type=int, required=True, + help='SEV API major version for the running domain') + vmstate.add_argument('--api-minor', type=int, required=True, + help='SEV API minor version for the running domain') + vmstate.add_argument('--build-id', type=int, required=True, + help='SEV build ID for the running domain') + vmstate.add_argument('--policy', type=int, required=True, + help='SEV policy for the running domain') + + # Arguments related to calculation of the expected launch measurement + vmconfig = parser.add_argument_group("Virtual machine config") + vmconfig.add_argument('--firmware', '-f', required=True, + help='Path to the firmware binary') + vmconfig.add_argument('--tik', + help='TIK file for domain') + vmconfig.add_argument('--tek', + help='TEK file for domain') + vmconfig.add_argument('--tk', + help='TEK/TIK combined file for domain') + + return parser.parse_args() + + +# Sanity check the set of CLI args specified provide enough info for us to do +# the job +def check_usage(args): + if args.tk is not None: + if args.tik is not None or args.tek is not None: + raise UnsupportedUsageException( + "--tk is mutually exclusive with --tek/--tik") + else: + if args.tik is None or args.tek is None: + raise UnsupportedUsageException( + "Either --tk or both of --tek/--tik are required") + + +def attest(args): + cvm = ConfidentialVM(measurement=args.measurement, + api_major=args.api_major, + api_minor=args.api_minor, + build_id=args.build_id, + policy=args.policy) + + cvm.load_firmware(args.firmware) + + if args.tk is not None: + cvm.load_tk(args.tk) + else: + cvm.load_tik_tek(args.tik, args.tek) + + cvm.attest() + + if not args.quiet: + print("OK: Looks good to me") + +def main(): + args = parse_command_line() + if args.debug: + logging.basicConfig(level="DEBUG") + formatter = logging.Formatter("[%(levelname)s]: %(message)s") + handler = log.handlers[0] + handler.setFormatter(formatter) + + try: + check_usage(args) + + attest(args) + + sys.exit(0) + except AttestationFailedException as e: + if args.debug: + traceback.print_tb(e.__traceback__) + if not args.quiet: + print("ERROR: %s" % e, file=sys.stderr) + sys.exit(1) + except UnsupportedUsageException as e: + if args.debug: + traceback.print_tb(e.__traceback__) + if not args.quiet: + print("ERROR: %s" % e, file=sys.stderr) + sys.exit(2) + except Exception as e: + if args.debug: + traceback.print_tb(e.__traceback__) + if not args.quiet: + print("ERROR: %s" % e, file=sys.stderr) + sys.exit(3) + +if __name__ == "__main__": + main()