pax_global_header00006660000000000000000000000064133167010310014505gustar00rootroot0000000000000052 comment=69cee8b86ca4077b0de83fb936874b8d7884fd81 qemu-ovmf-secureboot-1.1.3/000077500000000000000000000000001331670103100155735ustar00rootroot00000000000000qemu-ovmf-secureboot-1.1.3/.circleci/000077500000000000000000000000001331670103100174265ustar00rootroot00000000000000qemu-ovmf-secureboot-1.1.3/.circleci/Dockerfile000066400000000000000000000002071331670103100214170ustar00rootroot00000000000000FROM fedora:latest RUN dnf install -y git openssh \ edk2-ovmf qemu-system-x86 \ python2 python2-requests \ python3 python3-requests qemu-ovmf-secureboot-1.1.3/.circleci/config.yml000066400000000000000000000017321331670103100214210ustar00rootroot00000000000000version: 2 jobs: build: docker: - image: puiterwijk/qemu-ovmf-secureboot:testenv steps: - checkout - run: name: run simple version with python2 command: | python2 ./ovmf-vars-generator --verbose --print-output --kernel-path vmlinuz output2.vars - run: name: run simple version with python3 command: | python3 ./ovmf-vars-generator --verbose --print-output --kernel-path vmlinuz output3.vars - run: name: run enrollment-only command: | python3 ./ovmf-vars-generator --verbose --print-output --kernel-path vmlinuz outputsplit.vars --skip-testing - run: name: run testing-only command: | python3 ./ovmf-vars-generator --verbose --print-output --kernel-path vmlinuz outputsplit.vars --skip-enrollment - store_artifacts: path: output2.vars - store_artifacts: path: output3.vars qemu-ovmf-secureboot-1.1.3/.gitignore000066400000000000000000000022051331670103100175620ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ qemu-ovmf-secureboot-1.1.3/CONTRIBUTING000066400000000000000000000044671331670103100174400ustar00rootroot00000000000000This project requires the Developer Certificate of Origin (DCO) process to be followed. The DCO is an attestation attached to every contribution made by every developer, signifying that the developer licenses their contribution under the open source license governing the file(s) being modified. In the commit message of the contribution, simply add a Signed-off-by statement, which signifies agreement to the text at http://developercertificate.org, reproduced below: Developer Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 1 Letterman Drive Suite D4700 San Francisco, CA, 94129 Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. The DCO requires a signoff message in the following format to appear on each commit: Signed-off-by: Your Name If you set your user.name and user.email git configs, you can sign your commit automatically with git commit -s. qemu-ovmf-secureboot-1.1.3/LICENSE000066400000000000000000000020621331670103100166000ustar00rootroot00000000000000MIT 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. qemu-ovmf-secureboot-1.1.3/README.md000066400000000000000000000055731331670103100170640ustar00rootroot00000000000000# QEMU, OVMF and Secure Boot ## Description and usage Script to generate an OVMF variables ("VARS") file with default Secure Boot keys enrolled. (And verify that it works.) Simplest working invocation of the script is: $ ./ovmf-vars-generator output-VARS.fd But, a more tedious variant where you can invoke the script with custom paths and URLs: $ ./ovmf-vars-generator \ --ovmf-binary /usr/share/edk2/ovmf/OVMF_CODE.secboot.fd \ --uefi-shell-iso /usr/share/edk2/ovmf/UefiShell.iso \ --ovmf-template-vars /usr/share/edk2/ovmf/OVMF_VARS.fd \ --fedora-version 27 \ --kernel-path /tmp/qosb.kernel \ --kernel-url https://download.fedoraproject.org/pub/fedora/linux/releases/27/Everything/x86_64/os/images/pxeboot/vmlinuz \ --initrd-path /tmp/qosb.initrd \ --initrd-url https://download.fedoraproject.org/pub/fedora/linux/releases/27/Everything/x86_64/os/images/pxeboot/initrd.img \ another-output-VARS.fd This script does the following, in that order: (1) Launches a QEMU guest with the UefiShell.iso as a CD-ROM. (2) Automatically enrolls the cryptographic keys in the UEFI shell. (3) Finally, downloads a Fedora Kernel and 'initrd' file and boots into it, & confirms Secure Boot is really applied. Alternatively: You can also verify that Secure Boot is enabled properly in a full virtual machine by explicitly running `dmesg`, and grepping for "secure" string. On a recent Fedora QEMU+KVM virtual machine, it looks as follows: (fedora-vm)$ dmesg | grep -i secure [ 0.000000] Secure boot enabled and kernel locked down [ 3.261277] EFI: Loaded cert 'Fedora Secure Boot CA: fde32599c2d61db1bf5807335d7b20e4cd963b42' linked to '.builtin_trusted_keys' ## What certificates and keys are enrolled? The following certificates and keys are enrolled by the tool: - As *Platform Key*, and as one of the two *Key Exchange Keys* that we set up, the `EnrollDefaultKeys.efi` binary on both Fedora and RHEL, uses the same digital certificate called `Red Hat Secure Boot (PK/KEK key 1)/emailAddress=secalert@redhat.com`, and Red Hat's Product Security team has the private key for it. - The certificate that is enrolled as the second *Key Exchange Key* is called `Microsoft Corporation KEK CA 2011`. Updates to the authenticated dbx (basically, "blacklist") variable, periodically released at http://www.uefi.org/revocationlistfile , are signed such that the signature chain ends in this certificate. The update can be installed in the guest Linux OS with the `dbxtool` utility. - Then, the authenticated `db` variable gets the following two cetificates: `Microsoft Windows Production PCA 2011` (for accepting Windows 8, Windows Server 2012 R2, etc boot loaders), and `Microsoft Corporation UEFI CA 2011` (for verifying the `shim` binary, and PCI expansion ROMs). qemu-ovmf-secureboot-1.1.3/ovmf-vars-generator000077500000000000000000000235121331670103100214300ustar00rootroot00000000000000#!/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') return [ args.qemu_binary, '-machine', machinetype, '-display', 'none', '-no-user-config', '-nodefaults', '-m', '256', '-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'] + 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() 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') 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()