From bd04510a63659b7bd8e8ce28e759b0118e542a8a Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Fri, 24 Mar 2023 21:26:01 +0000 Subject: [PATCH] Add support for OVMF hashes Some times we may not want to have to keep the full OVMF binary handy when we try to validate a measurement. Most of the data we extract out of the OVMF binary is static, except for its actual hashes. So let's introduce a new mode that generates a precalculated launch digest that contains the SNP ld hash at the point where we fully ingested OVMF's contents: $ sev-snp-measure --mode snp:ovmf-hash --ovmf OVMF.fd cab7e[...] In addition, add a new optional parameter that a user can then use to consume the hash instead of recalculating it from their OVMF binary at hand: $ sev-snp-measure --mode snp --vcpus=1 --vcpu-type=EPYC-v4 \ --ovmf=OVMF.fd.old --snp-ovmf-hash cab7e[...] d5269[...] which is identical to the full calculation with the new OVMF.fd: $ sev-snp-measure --mode snp --vcpus=1 --vcpu-type=EPYC-v4 --ovmf=OVMF.fd d5269[...] With this in place, we no longer need to copy full OVMF binaries around. Instead, for almost all OVMF binaries in existence today, we can merely share their hash values and are still able to validate the measurement correctness. Signed-off-by: Alexander Graf --- README.md | 26 ++++++++++++++++++++++++-- sevsnpmeasure/cli.py | 9 +++++++-- sevsnpmeasure/gctx.py | 4 ++-- sevsnpmeasure/guest.py | 24 +++++++++++++++++++++--- tests/test_guest.py | 40 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ef98e53..0f8827b 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,11 @@ Clone the Github repo and run the script directly from the local directory: ``` $ sev-snp-measure --help -usage: sev-snp-measure [-h] [--version] [-v] --mode {sev,seves,snp} [--vcpus N] +usage: sev-snp-measure [-h] [--version] [-v] --mode {sev,seves,snp,snp:ovmf-hash} [--vcpus N] [--vcpu-type CPUTYPE] [--vcpu-sig VALUE] [--vcpu-family FAMILY] [--vcpu-model MODEL] [--vcpu-stepping STEPPING] --ovmf PATH [--kernel PATH] [--initrd PATH] [--append CMDLINE] [--output-format {hex,base64}] + [--snp-ovmf-hash HASH] Calculate AMD SEV/SEV-ES/SEV-SNP guest launch measurement @@ -39,7 +40,7 @@ optional arguments: -h, --help show this help message and exit --version show program's version number and exit -v, --verbose - --mode {sev,seves,snp} + --mode {sev,seves,snp,snp:ovmf-hash} Guest mode --vcpus N Number of guest vcpus --vcpu-type CPUTYPE Type of guest vcpu (EPYC, EPYC-v1, EPYC-v2, EPYC-IBPB, EPYC-v3, EPYC-v4, @@ -56,6 +57,7 @@ optional arguments: --append CMDLINE Kernel command line to calculate hash from (use with --kernel) --output-format {hex,base64} Measurement output format + --snp-ovmf-hash HASH Precalculated hash of the OVMF binary (hex string) ``` For example: @@ -93,6 +95,26 @@ example, the following 3 invocations are identical: 2. `sev-snp-measure --vcpu-sig=0x800f12 ...` 3. `sev-snp-measure --vcpu-family=23 --vcpu-model=1 --vcpu-stepping=2 ...` +## Precalculated OVMF hashes + +The SEV-SNP digest gets generated in multiple steps that each have a digest as output. With that digest output, you can stop at any of these steps and continue generation of the full digest later. These are the steps: + +1. OVMF +2. (optional) -kernel, -initrd, -append arguments +3. Initial state of all vCPUs + +In situations where only minor OVMF changes happen, you may not want to copy the full OVMF binary to the validation system. In these situations, you can cut digest calculation after the `OVMF` step and use its hash instead of the full binary. + +To generate a hash, use the `--mode snp:ovmf-hash` parameter: + + $ sev-snp-measure --mode snp:ovmf-hash --ovmf OVMF.fd + cab7e085874b3acfdbe2d96dcaa3125111f00c35c6fc9708464c2ae74bfdb048a198cb9a9ccae0b3e5e1a33f5f249819 + +On a different machine that only has access to an older but compatible OVMF binary, you can then ingest the hash again to generate a full measurement: + + $ sev-snp-measure --mode snp --vcpus=1 --vcpu-type=EPYC-v4 --ovmf=OVMF.fd.old --ovmf-hash cab7e[...] + d52697c3e056fb8d698d19cc29adfbed5a8ec9170cb9eb63c2ac957d22b4eb647e25780162036d063a0cf418b8830acc + ## Related projects * libvirt tools: [virt-dom-sev-validate](https://gitlab.com/berrange/libvirt/-/blob/lgtm/tools/virt-dom-sev-validate.py), diff --git a/sevsnpmeasure/cli.py b/sevsnpmeasure/cli.py index 5d3ad65..6f93221 100644 --- a/sevsnpmeasure/cli.py +++ b/sevsnpmeasure/cli.py @@ -23,7 +23,7 @@ def main() -> int: description='Calculate AMD SEV/SEV-ES/SEV-SNP guest launch measurement') parser.add_argument('--version', action='version', version=f'%(prog)s {VERSION}') parser.add_argument('-v', '--verbose', action='store_true') - parser.add_argument('--mode', choices=['sev', 'seves', 'snp'], help='Guest mode', required=True) + parser.add_argument('--mode', choices=['sev', 'seves', 'snp', 'snp:ovmf-hash'], help='Guest mode', required=True) parser.add_argument('--vcpus', metavar='N', type=int, help='Number of guest vcpus', default=None) parser.add_argument('--vcpu-type', metavar='CPUTYPE', choices=list(vcpu_types.CPU_SIGS.keys()), help=f"Type of guest vcpu ({', '.join(vcpu_types.CPU_SIGS.keys())})", @@ -41,8 +41,13 @@ def main() -> int: parser.add_argument('--append', metavar='CMDLINE', help='Kernel command line to calculate hash from (use with --kernel)') parser.add_argument('--output-format', choices=['hex', 'base64'], help='Measurement output format', default='hex') + parser.add_argument('--snp-ovmf-hash', metavar='HASH', help='Precalculated hash of the OVMF binary (hex string)') args = parser.parse_args() + if args.mode == 'snp:ovmf-hash': + print(guest.calc_snp_ovmf_hash(args.ovmf).hex()) + return 0 + if args.mode != 'sev' and args.vcpus is None: parser.error(f"missing --vcpus N in guest mode '{args.mode}'") @@ -58,7 +63,7 @@ def main() -> int: parser.error(f"missing --vcpu-type or --vcpu-sig or --vcpu-family in guest mode '{args.mode}'") sev_mode = SevMode.from_str(args.mode) - ld = guest.calc_launch_digest(sev_mode, args.vcpus, vcpu_sig, args.ovmf, args.kernel, args.initrd, args.append) + ld = guest.calc_launch_digest(sev_mode, args.vcpus, vcpu_sig, args.ovmf, args.kernel, args.initrd, args.append, args.snp_ovmf_hash) if args.output_format == "hex": measurement = ld.hex() diff --git a/sevsnpmeasure/gctx.py b/sevsnpmeasure/gctx.py index 84607ab..4ca4238 100644 --- a/sevsnpmeasure/gctx.py +++ b/sevsnpmeasure/gctx.py @@ -39,8 +39,8 @@ class GCTX(object): # 51 are cleared. VMSA_GPA = 0xFFFFFFFFF000 - def __init__(self): - self._ld = ZEROS + def __init__(self, seed: bytes = ZEROS): + self._ld = seed def ld(self) -> bytes: return self._ld diff --git a/sevsnpmeasure/guest.py b/sevsnpmeasure/guest.py index 2a3212a..2b2bd2c 100644 --- a/sevsnpmeasure/guest.py +++ b/sevsnpmeasure/guest.py @@ -15,9 +15,12 @@ def calc_launch_digest(mode: SevMode, vcpus: int, vcpu_sig: int, ovmf_file: str, - kernel: str, initrd: str, append: str) -> bytes: + kernel: str, initrd: str, append: str, snp_ovmf_hash_str: str = '') -> bytes: + if snp_ovmf_hash_str and mode != SevMode.SEV_SNP: + raise ValueError("SNP OVMF hash only works with SNP") + if mode == SevMode.SEV_SNP: - return snp_calc_launch_digest(vcpus, vcpu_sig, ovmf_file, kernel, initrd, append) + return snp_calc_launch_digest(vcpus, vcpu_sig, ovmf_file, kernel, initrd, append, snp_ovmf_hash_str) elif mode == SevMode.SEV_ES: return seves_calc_launch_digest(vcpus, vcpu_sig, ovmf_file, kernel, initrd, append) elif mode == SevMode.SEV: @@ -38,11 +41,26 @@ def snp_update_metadata_pages(gctx, ovmf) -> None: raise ValueError("unknown OVMF metadata section type") -def snp_calc_launch_digest(vcpus: int, vcpu_sig: int, ovmf_file: str, kernel: str, initrd: str, append: str) -> bytes: +def calc_snp_ovmf_hash(ovmf_file: str) -> bytes: ovmf = OVMF(ovmf_file) gctx = GCTX() gctx.update_normal_pages(ovmf.gpa(), ovmf.data()) + return gctx.ld() + + +def snp_calc_launch_digest(vcpus: int, vcpu_sig: int, ovmf_file: str, kernel: str, initrd: str, append: str, ovmf_hash_str: str) -> bytes: + + gctx = GCTX() + ovmf = OVMF(ovmf_file) + + # Allow users to provide a precalculated OVMF hash. + # Ignores the contents of the OVMF file in front of us. + if ovmf_hash_str: + ovmf_hash = bytearray.fromhex(ovmf_hash_str) + gctx = GCTX(seed = ovmf_hash) + else: + gctx.update_normal_pages(ovmf.gpa(), ovmf.data()) if kernel: sev_hashes_table_gpa = ovmf.sev_hashes_table_gpa() diff --git a/tests/test_guest.py b/tests/test_guest.py index c64b829..c3d135b 100644 --- a/tests/test_guest.py +++ b/tests/test_guest.py @@ -11,6 +11,46 @@ class TestGuest(unittest.TestCase): + # Test of we can generate a good OVMF hash + def test_snp_ovmf_hash_gen(self): + ovmf_hash = 'cab7e085874b3acfdbe2d96dcaa3125111f00c35c6fc9708464c2ae74bfdb048a198cb9a9ccae0b3e5e1a33f5f249819' + ld = guest.calc_launch_digest( + SevMode.SEV_SNP, + 1, + vcpu_types.CPU_SIGS["EPYC-v4"], + "tests/fixtures/ovmf_suffix.bin", + "/dev/null", + "/dev/null", + "", + snp_ovmf_hash_str = ovmf_hash) + self.assertEqual( + ld.hex(), + '6a23d4774a60f6238506b531e0cb60a698a198db100476f6' + 'fadb724f60c144bed9c71a3903b9ca425ff82b376c381b33') + + # Test of we can a full LD from the OVMF hash + def test_snp_ovmf_hash_full(self): + ovmf_hash = guest.calc_snp_ovmf_hash("tests/fixtures/ovmf_suffix.bin").hex() + self.assertEqual( + ovmf_hash, + '4ef91bfd7241908300ac19305a694753cbc8db28104f356f' + 'd7860cc7b4119db285ce80586c19bd358a731d5267cee60e') + + ld = guest.calc_launch_digest( + SevMode.SEV_SNP, + 1, + vcpu_types.CPU_SIGS["EPYC-v4"], + "tests/fixtures/ovmf_suffix.bin", + "/dev/null", + "/dev/null", + "", + snp_ovmf_hash_str = ovmf_hash) + + self.assertEqual( + ld.hex(), + '38859e76ac5fa5009c8249eb2f44dafb33a2a1f41efd65ce' + 'b13f042864abab87d018dc64da21628b320a98642f25ae6c') + def test_snp(self): ld = guest.calc_launch_digest( SevMode.SEV_SNP,