diff --git a/LICENSE.qosb b/LICENSE.qosb new file mode 100644 index 0000000..9849381 --- /dev/null +++ b/LICENSE.qosb @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Patrick Uiterwijk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/edk2.spec b/edk2.spec index f0c5022..2e5598f 100644 --- a/edk2.spec +++ b/edk2.spec @@ -3,17 +3,14 @@ %define TOOLCHAIN GCC5 %define OPENSSL_VER 1.1.1g -%global qosb_version 20200228-gitc3e16b3 %global softfloat_version 20180726-gitb64af41 -# Enable this to skip secureboot enrollment, if problems pop up -%global skip_enroll 0 - %define qosb_testing 0 - %ifarch x86_64 %define qosb_testing 1 %endif +%define qemu_binary /usr/bin/qemu-system-x86_64 + %if 0%{?fedora:1} %define cross 1 %endif @@ -53,21 +50,21 @@ URL: http://www.tianocore.org Source0: edk2-%{GITCOMMIT}.tar.xz Source1: ovmf-whitepaper-c770f8c.txt Source2: openssl-rhel-bdd048e929dcfcf2f046d74e812e0e3d5fc58504.tar.xz -#Source3: https://github.com/puiterwijk/qemu-ovmf-secureboot/archive/v{qosb_version}/qemu-ovmf-secureboot-{qosb_version}.tar.gz -Source3: qemu-ovmf-secureboot-%{qosb_version}.tar.xz -Source4: softfloat-%{softfloat_version}.tar.xz -Source5: RedHatSecureBootPkKek1.pem -Source11: build-iso.sh +Source3: ovmf-vars-generator +Source4: LICENSE.qosb +Source5: RedHatSecureBootPkKek1.pem -# Fedora-specific JSON "descriptor files" -Source14: 40-edk2-ovmf-x64-sb-enrolled.json -Source15: 50-edk2-ovmf-x64-sb.json -Source16: 60-edk2-ovmf-x64.json -Source17: 40-edk2-ovmf-ia32-sb-enrolled.json -Source18: 50-edk2-ovmf-ia32-sb.json -Source19: 60-edk2-ovmf-ia32.json -Source20: 70-edk2-aarch64-verbose.json -Source21: 70-edk2-arm-verbose.json +# Fedora specific sources +Source50: softfloat-%{softfloat_version}.tar.xz +Source51: build-iso.sh +Source52: 40-edk2-ovmf-x64-sb-enrolled.json +Source53: 50-edk2-ovmf-x64-sb.json +Source54: 60-edk2-ovmf-x64.json +Source55: 40-edk2-ovmf-ia32-sb-enrolled.json +Source56: 50-edk2-ovmf-ia32-sb.json +Source57: 60-edk2-ovmf-ia32.json +Source58: 70-edk2-aarch64-verbose.json +Source59: 70-edk2-arm-verbose.json # non-upstream patches Patch0008: 0008-BaseTools-do-not-build-BrotliCompress-RH-only.patch @@ -126,18 +123,16 @@ BuildRequires: findutils # These are for QOSB BuildRequires: python3-requests BuildRequires: qemu-system-x86 -%if %{?qosb_testing} -# This is used for testing the enrollment: builds are run in a chroot, lacking -# a kernel. The testing is only performed on x86_64 for now, but we can't make -# the BuildRequires only on a specific arch, as that'd come through in the SRPM -# NOTE: The actual enrollment needs to happen in all builds for all architectures, -# because OVMF is built as noarch, which means that koji enforces that the build -# results don't actually differ per arch, and then it picks a random arches' build -# for the actual RPM. -BuildRequires: kernel-core -%endif BuildRequires: make +%if %{qosb_testing} +# For verifying SB enablement in the above variable store template, we need a +# guest kernel that prints "Secure boot enabled". +BuildRequires: kernel-core >= 4.18.0-161 +BuildRequires: rpmdevtools +%endif + + %description EDK II is a development code base for creating UEFI drivers, applications and firmware images. @@ -239,26 +234,25 @@ git config am.keepcr true # -D is passed to %%setup to not delete the existing archive dir %autosetup -T -D -n edk2-%{GITCOMMIT} -S git_am -# copy whitepaper into place -cp -a -- %{SOURCE1} . +cp -a -- %{SOURCE1} %{SOURCE3} . # extract openssl into place tar -C CryptoPkg/Library/OpensslLib -a -f %{SOURCE2} -x # extract softfloat into place -tar -xf %{SOURCE4} --strip-components=1 --directory ArmPkg/Library/ArmSoftFloatLib/berkeley-softfloat-3/ +tar -xf %{SOURCE50} --strip-components=1 --directory ArmPkg/Library/ArmSoftFloatLib/berkeley-softfloat-3/ -# Extract QOSB -tar -xf %{SOURCE3} -mv qemu-ovmf-secureboot-%{qosb_version}/README.md README.qosb -mv qemu-ovmf-secureboot-%{qosb_version}/LICENSE LICENSE.qosb - -# Extract OEM string from the RH cert, as described here -# https://bugzilla.tianocore.org/show_bug.cgi?id=1747#c2 +# Format the Red Hat-issued certificate that is to be enrolled as both Platform +# Key and first Key Exchange Key, as an SMBIOS OEM String. This means stripping +# the PEM header and footer, and prepending the textual representation of the +# GUID that identifies this particular OEM String to "EnrollDefaultKeys.efi", +# plus the separator ":". For details, see +# comments 2, 7, 14. sed \ -e 's/^-----BEGIN CERTIFICATE-----$/4e32566d-8e9e-4f52-81d3-5bb9715f9727:/' \ -e '/^-----END CERTIFICATE-----$/d' \ - %{_sourcedir}/RedHatSecureBootPkKek1.pem \ -| tr -d '\n' \ -> PkKek1.oemstr + %{SOURCE5} \ + > PkKek1.oemstr + +cp -a -- %{SOURCE4} . %build @@ -325,20 +319,15 @@ cp Build/Ovmf3264/*/X64/Shell.efi ovmf/ cp Build/Ovmf3264/*/X64/EnrollDefaultKeys.efi ovmf sh %{_sourcedir}/build-iso.sh ovmf/ -%if !%{skip_enroll} -python3 qemu-ovmf-secureboot-%{qosb_version}/ovmf-vars-generator \ - --qemu-binary /usr/bin/qemu-system-x86_64 \ - --ovmf-binary ovmf/OVMF_CODE.secboot.fd \ +# Enroll the default certificates in a separate variable store template. +%{__python3} ovmf-vars-generator --verbose --verbose \ + --qemu-binary %{qemu_binary} \ + --ovmf-binary ovmf/OVMF_CODE.secboot.fd \ --ovmf-template-vars ovmf/OVMF_VARS.fd \ - --uefi-shell-iso ovmf/UefiShell.iso \ - --oem-string "$(< PkKek1.oemstr)" \ + --uefi-shell-iso ovmf/UefiShell.iso \ + --oem-string "$(< PkKek1.oemstr)" \ --skip-testing \ ovmf/OVMF_VARS.secboot.fd -%else -# This isn't going to actually give secureboot, but makes json files happy -# if we need to test disabling ovmf-vars-generator -cp ovmf/OVMF_VARS.fd ovmf/OVMF_VARS.secboot.fd -%endif %endif @@ -386,25 +375,25 @@ dd of="arm/vars-template-pflash.raw" if="/dev/zero" bs=1M count=64 %check -%if 0%{?build_ovmf_x64:1} -%if 0%{?qosb_testing} -%if !%{skip_enroll} -KERNELPATH="$(find /lib/modules -name vmlinuz | head -1)" -python3 qemu-ovmf-secureboot-%{qosb_version}/ovmf-vars-generator \ - --qemu-binary /usr/bin/qemu-system-x86_64 \ - --ovmf-binary ovmf/OVMF_CODE.secboot.fd \ - --ovmf-template-vars ovmf/OVMF_VARS.fd \ - --uefi-shell-iso ovmf/UefiShell.iso \ - --skip-enrollment \ - --print-output \ - --no-download \ - -vv \ - --kernel-path "$KERNELPATH" \ - ovmf/OVMF_VARS.secboot.fd -%endif -%endif -%endif +%if %{qosb_testing} +# Of the installed host kernels, boot the one with the highest Version-Release +# under OVMF, and check if it prints "Secure boot enabled". +KERNEL_PKG=$(rpm -q kernel-core | rpmdev-sort | tail -n 1) +KERNEL_IMG=$(rpm -q -l $KERNEL_PKG | egrep '^/lib/modules/[^/]+/vmlinuz$') + +%{__python3} ovmf-vars-generator --verbose --verbose \ + --qemu-binary %{qemu_binary} \ + --ovmf-binary ovmf/OVMF_CODE.secboot.fd \ + --ovmf-template-vars ovmf/OVMF_VARS.fd \ + --uefi-shell-iso ovmf/UefiShell.iso \ + --kernel-path $KERNEL_IMG \ + --skip-enrollment \ + --no-download \ + ovmf/OVMF_VARS.secboot.fd + +# endif qosb_testing +%endif %install @@ -490,8 +479,7 @@ done %py_byte_compile %{python3} %{buildroot}%{_datadir}/edk2/Python %endif - -install qemu-ovmf-secureboot-%{qosb_version}/ovmf-vars-generator %{buildroot}%{_bindir} +install -p ovmf-vars-generator %{buildroot}%{_bindir} %files tools @@ -533,7 +521,6 @@ install qemu-ovmf-secureboot-%{qosb_version}/ovmf-vars-generator %{buildroot}%{_ %files qosb %license LICENSE.qosb -%doc README.qosb %{_bindir}/ovmf-vars-generator %if 0%{?build_ovmf_x64:1} diff --git a/ovmf-vars-generator b/ovmf-vars-generator new file mode 100755 index 0000000..111e438 --- /dev/null +++ b/ovmf-vars-generator @@ -0,0 +1,295 @@ +#!/bin/python3 +# Copyright (C) 2017 Red Hat +# Authors: +# - Patrick Uiterwijk +# - Kashyap Chamarthy +# +# Licensed under MIT License, for full text see LICENSE +# +# Purpose: Launch a QEMU guest and enroll ithe UEFI keys into an OVMF +# variables ("VARS") file. Then boot a Linux kernel with QEMU. +# Finally, perform a check to verify if Secure Boot +# is enabled. + +from __future__ import print_function + +import argparse +import os +import logging +import tempfile +import shutil +import string +import subprocess + + +def strip_special(line): + return ''.join([c for c in str(line) if c in string.printable]) + + +def generate_qemu_cmd(args, readonly, *extra_args): + if args.disable_smm: + machinetype = 'pc' + else: + machinetype = 'q35,smm=on' + machinetype += ',accel=%s' % ('kvm' if args.enable_kvm else 'tcg') + + if args.oem_string is None: + oemstrings = [] + else: + oemstring_values = [ + ",value=" + s.replace(",", ",,") for s in args.oem_string ] + oemstrings = [ + '-smbios', + "type=11" + ''.join(oemstring_values) ] + + return [ + args.qemu_binary, + '-machine', machinetype, + '-display', 'none', + '-no-user-config', + '-nodefaults', + '-m', '768', + '-smp', '2,sockets=2,cores=1,threads=1', + '-chardev', 'pty,id=charserial1', + '-device', 'isa-serial,chardev=charserial1,id=serial1', + '-global', 'driver=cfi.pflash01,property=secure,value=%s' % ( + 'off' if args.disable_smm else 'on'), + '-drive', + 'file=%s,if=pflash,format=raw,unit=0,readonly=on' % ( + args.ovmf_binary), + '-drive', + 'file=%s,if=pflash,format=raw,unit=1,readonly=%s' % ( + args.out_temp, 'on' if readonly else 'off'), + '-serial', 'stdio'] + oemstrings + list(extra_args) + + +def download(url, target, suffix, no_download): + istemp = False + if target and os.path.exists(target): + return target, istemp + if not target: + temped = tempfile.mkstemp(prefix='qosb.', suffix='.%s' % suffix) + os.close(temped[0]) + target = temped[1] + istemp = True + if no_download: + raise Exception('%s did not exist, but downloading was disabled' % + target) + import requests + logging.debug('Downloading %s to %s', url, target) + r = requests.get(url, stream=True) + with open(target, 'wb') as f: + for chunk in r.iter_content(chunk_size=1024): + if chunk: + f.write(chunk) + return target, istemp + + +def enroll_keys(args): + shutil.copy(args.ovmf_template_vars, args.out_temp) + + logging.info('Starting enrollment') + + cmd = generate_qemu_cmd( + args, + False, + '-drive', + 'file=%s,format=raw,if=none,media=cdrom,id=drive-cd1,' + 'readonly=on' % args.uefi_shell_iso, + '-device', + 'ide-cd,drive=drive-cd1,id=cd1,' + 'bootindex=1') + p = subprocess.Popen(cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + logging.info('Performing enrollment') + # Wait until the UEFI shell starts (first line is printed) + read = p.stdout.readline() + if b'char device redirected' in read: + read = p.stdout.readline() + # Skip passed QEMU warnings, like the following one we see in Ubuntu: + # qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5] + while b'qemu-system-x86_64: warning:' in read: + read = p.stdout.readline() + if args.print_output: + print(strip_special(read), end='') + print() + # Send the escape char to enter the UEFI shell early + p.stdin.write(b'\x1b') + p.stdin.flush() + # And then run the following three commands from the UEFI shell: + # change into the first file system device; install the default + # keys and certificates, and reboot + p.stdin.write(b'fs0:\r\n') + p.stdin.write(b'EnrollDefaultKeys.efi\r\n') + p.stdin.write(b'reset -s\r\n') + p.stdin.flush() + while True: + read = p.stdout.readline() + if args.print_output: + print('OUT: %s' % strip_special(read), end='') + print() + if b'info: success' in read: + break + p.wait() + if args.print_output: + print(strip_special(p.stdout.read()), end='') + logging.info('Finished enrollment') + + +def test_keys(args): + logging.info('Grabbing test kernel') + kernel, kerneltemp = download(args.kernel_url, args.kernel_path, + 'kernel', args.no_download) + + logging.info('Starting verification') + try: + cmd = generate_qemu_cmd( + args, + True, + '-append', 'console=tty0 console=ttyS0,115200n8', + '-kernel', kernel) + p = subprocess.Popen(cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + logging.info('Performing verification') + while True: + read = p.stdout.readline() + if args.print_output: + print('OUT: %s' % strip_special(read), end='') + print() + if b'Secure boot disabled' in read: + raise Exception('Secure Boot was disabled') + elif b'Secure boot enabled' in read: + logging.info('Confirmed: Secure Boot is enabled') + break + elif b'Kernel is locked down from EFI secure boot' in read: + logging.info('Confirmed: Secure Boot is enabled') + break + p.kill() + if args.print_output: + print(strip_special(p.stdout.read()), end='') + logging.info('Finished verification') + finally: + if kerneltemp: + os.remove(kernel) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('output', help='Filename for output vars file') + parser.add_argument('--out-temp', help=argparse.SUPPRESS) + parser.add_argument('--force', help='Overwrite existing output file', + action='store_true') + parser.add_argument('--print-output', help='Print the QEMU guest output', + action='store_true') + parser.add_argument('--verbose', '-v', help='Increase verbosity', + action='count') + parser.add_argument('--quiet', '-q', help='Decrease verbosity', + action='count') + parser.add_argument('--qemu-binary', help='QEMU binary path', + default='/usr/bin/qemu-system-x86_64') + parser.add_argument('--enable-kvm', help='Enable KVM acceleration', + action='store_true') + parser.add_argument('--ovmf-binary', help='OVMF secureboot code file', + default='/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd') + parser.add_argument('--ovmf-template-vars', help='OVMF empty vars file', + default='/usr/share/edk2/ovmf/OVMF_VARS.fd') + parser.add_argument('--uefi-shell-iso', help='Path to uefi shell iso', + default='/usr/share/edk2/ovmf/UefiShell.iso') + parser.add_argument('--skip-enrollment', + help='Skip enrollment, only test', action='store_true') + parser.add_argument('--skip-testing', + help='Skip testing generated "VARS" file', + action='store_true') + parser.add_argument('--kernel-path', + help='Specify a consistent path for kernel') + parser.add_argument('--no-download', action='store_true', + help='Never download a kernel') + parser.add_argument('--fedora-version', + help='Fedora version to get kernel for checking', + default='27') + parser.add_argument('--kernel-url', help='Kernel URL', + default='https://download.fedoraproject.org/pub/fedora' + '/linux/releases/%(version)s/Everything/x86_64' + '/os/images/pxeboot/vmlinuz') + parser.add_argument('--disable-smm', + help=('Don\'t restrict varstore pflash writes to ' + 'guest code that executes in SMM. Use this ' + 'option only if your OVMF binary doesn\'t have ' + 'the edk2 SMM driver stack built into it ' + '(possibly because your QEMU binary lacks SMM ' + 'emulation). Note that without restricting ' + 'varstore pflash writes to guest code that ' + 'executes in SMM, a malicious guest kernel, ' + 'used for testing, could undermine Secure ' + 'Boot.'), + action='store_true') + parser.add_argument('--oem-string', + help=('Pass the argument to the guest as a string in ' + 'the SMBIOS Type 11 (OEM Strings) table. ' + 'Multiple occurrences of this option are ' + 'collected into a single SMBIOS Type 11 table. ' + 'A pure ASCII string argument is strongly ' + 'suggested.'), + action='append') + args = parser.parse_args() + args.kernel_url = args.kernel_url % {'version': args.fedora_version} + + validate_args(args) + return args + + +def validate_args(args): + if (os.path.exists(args.output) + and not args.force + and not args.skip_enrollment): + raise Exception('%s already exists' % args.output) + + if args.skip_enrollment and not os.path.exists(args.output): + raise Exception('%s does not yet exist' % args.output) + + verbosity = (args.verbose or 1) - (args.quiet or 0) + if verbosity >= 2: + logging.basicConfig(level=logging.DEBUG) + elif verbosity == 1: + logging.basicConfig(level=logging.INFO) + elif verbosity < 0: + logging.basicConfig(level=logging.ERROR) + else: + logging.basicConfig(level=logging.WARN) + + if args.skip_enrollment: + args.out_temp = args.output + else: + temped = tempfile.mkstemp(prefix='qosb.', suffix='.vars') + os.close(temped[0]) + args.out_temp = temped[1] + logging.debug('Temp output: %s', args.out_temp) + + +def move_to_dest(args): + shutil.copy(args.out_temp, args.output) + os.remove(args.out_temp) + + +def main(): + args = parse_args() + if not args.skip_enrollment: + enroll_keys(args) + if not args.skip_testing: + test_keys(args) + if not args.skip_enrollment: + move_to_dest(args) + if args.skip_testing: + logging.info('Created %s' % args.output) + else: + logging.info('Created and verified %s' % args.output) + else: + logging.info('Verified %s', args.output) + + +if __name__ == '__main__': + main() diff --git a/sources b/sources index 38b6e11..ee5edd1 100644 --- a/sources +++ b/sources @@ -1,5 +1,4 @@ SHA512 (softfloat-20180726-gitb64af41.tar.xz) = f079debd1bfcc0fe64329a8947b0689ef49246793edcdd28a2879f6550c652b0cf0f53ac4f6f5ab61ac4f7933972e0019d0ab63eb9931b6884c2909f3a5ead30 -SHA512 (qemu-ovmf-secureboot-20200228-gitc3e16b3.tar.xz) = 123889b9277adda472035f72e4836b6fe8e0cd8e2e87d28400bbc846ea1308378fc7aae413d463e0c1bfda096d85e51be100eb8d7dfb0738707c3412f2855711 SHA512 (openssl-rhel-a75722161d20fd632f8875585d3aa066ec5fea93.tar.xz) = f88284054e33921fbebfad973abe83b83528e88b4b07b21b0c4999c07e7cb95270817144122209f4bf992deb96684d9fd2e871c63aa563cf39d14b087decf907 SHA512 (edk2-e1999b264f1f.tar.xz) = 323e28ae205e0aa670674e1bd39845f942331348da34a3c48a9ec7da1d3e617b0abf1c52347c818ea84e7218b74760cbeb4ba761656919748f799b984ab04e6a SHA512 (openssl-rhel-bdd048e929dcfcf2f046d74e812e0e3d5fc58504.tar.xz) = d6c0be28cefaa7993f479aecf45f5523e10aab388a92c3e4add55d6ff8483cc01fba367be62f6e9a23bfad7b46986befd3eca0d0e89b984049719645e37d52df