Blame SOURCES/ovmf-vars-generator

c4e3b2
#!/bin/python3
c4e3b2
# Copyright (C) 2017 Red Hat
c4e3b2
# Authors:
c4e3b2
# - Patrick Uiterwijk <puiterwijk@redhat.com>
c4e3b2
# - Kashyap Chamarthy <kchamart@redhat.com>
c4e3b2
#
c4e3b2
# Licensed under MIT License, for full text see LICENSE
c4e3b2
#
c4e3b2
# Purpose: Launch a QEMU guest and enroll ithe UEFI keys into an OVMF
c4e3b2
#          variables ("VARS") file.  Then boot a Linux kernel with QEMU.
c4e3b2
#          Finally, perform a check to verify if Secure Boot
c4e3b2
#          is enabled.
c4e3b2
c4e3b2
from __future__ import print_function
c4e3b2
c4e3b2
import argparse
c4e3b2
import os
c4e3b2
import logging
c4e3b2
import tempfile
c4e3b2
import shutil
c4e3b2
import string
c4e3b2
import subprocess
c4e3b2
c4e3b2
c4e3b2
def strip_special(line):
c4e3b2
    return ''.join([c for c in str(line) if c in string.printable])
c4e3b2
c4e3b2
c4e3b2
def generate_qemu_cmd(args, readonly, *extra_args):
c4e3b2
    if args.disable_smm:
c4e3b2
        machinetype = 'pc'
c4e3b2
    else:
c4e3b2
        machinetype = 'q35,smm=on'
c4e3b2
    machinetype += ',accel=%s' % ('kvm' if args.enable_kvm else 'tcg')
c4e3b2
c4e3b2
    if args.oem_string is None:
c4e3b2
        oemstrings = []
c4e3b2
    else:
c4e3b2
        oemstring_values = [
c4e3b2
            ",value=" + s.replace(",", ",,") for s in args.oem_string ]
c4e3b2
        oemstrings = [
c4e3b2
            '-smbios',
c4e3b2
            "type=11" + ''.join(oemstring_values) ]
c4e3b2
c4e3b2
    return [
c4e3b2
        args.qemu_binary,
c4e3b2
        '-machine', machinetype,
c4e3b2
        '-display', 'none',
c4e3b2
        '-no-user-config',
c4e3b2
        '-nodefaults',
c4e3b2
        '-m', '768',
c4e3b2
        '-smp', '2,sockets=2,cores=1,threads=1',
c4e3b2
        '-chardev', 'pty,id=charserial1',
c4e3b2
        '-device', 'isa-serial,chardev=charserial1,id=serial1',
c4e3b2
        '-global', 'driver=cfi.pflash01,property=secure,value=%s' % (
c4e3b2
            'off' if args.disable_smm else 'on'),
c4e3b2
        '-drive',
c4e3b2
        'file=%s,if=pflash,format=raw,unit=0,readonly=on' % (
c4e3b2
            args.ovmf_binary),
c4e3b2
        '-drive',
c4e3b2
        'file=%s,if=pflash,format=raw,unit=1,readonly=%s' % (
c4e3b2
            args.out_temp, 'on' if readonly else 'off'),
c4e3b2
        '-serial', 'stdio'] + oemstrings + list(extra_args)
c4e3b2
c4e3b2
c4e3b2
def download(url, target, suffix, no_download):
c4e3b2
    istemp = False
c4e3b2
    if target and os.path.exists(target):
c4e3b2
        return target, istemp
c4e3b2
    if not target:
c4e3b2
        temped = tempfile.mkstemp(prefix='qosb.', suffix='.%s' % suffix)
c4e3b2
        os.close(temped[0])
c4e3b2
        target = temped[1]
c4e3b2
        istemp = True
c4e3b2
    if no_download:
c4e3b2
        raise Exception('%s did not exist, but downloading was disabled' %
c4e3b2
                        target)
c4e3b2
    import requests
c4e3b2
    logging.debug('Downloading %s to %s', url, target)
c4e3b2
    r = requests.get(url, stream=True)
c4e3b2
    with open(target, 'wb') as f:
c4e3b2
        for chunk in r.iter_content(chunk_size=1024):
c4e3b2
            if chunk:
c4e3b2
                f.write(chunk)
c4e3b2
    return target, istemp
c4e3b2
c4e3b2
c4e3b2
def enroll_keys(args):
c4e3b2
    shutil.copy(args.ovmf_template_vars, args.out_temp)
c4e3b2
c4e3b2
    logging.info('Starting enrollment')
c4e3b2
c4e3b2
    cmd = generate_qemu_cmd(
c4e3b2
        args,
c4e3b2
        False,
c4e3b2
        '-drive',
c4e3b2
        'file=%s,format=raw,if=none,media=cdrom,id=drive-cd1,'
c4e3b2
        'readonly=on' % args.uefi_shell_iso,
c4e3b2
        '-device',
c4e3b2
        'ide-cd,drive=drive-cd1,id=cd1,'
c4e3b2
        'bootindex=1')
c4e3b2
    p = subprocess.Popen(cmd,
c4e3b2
        stdin=subprocess.PIPE,
c4e3b2
        stdout=subprocess.PIPE,
c4e3b2
        stderr=subprocess.STDOUT)
c4e3b2
    logging.info('Performing enrollment')
c4e3b2
    # Wait until the UEFI shell starts (first line is printed)
c4e3b2
    read = p.stdout.readline()
c4e3b2
    if b'char device redirected' in read:
c4e3b2
        read = p.stdout.readline()
c4e3b2
    # Skip passed QEMU warnings, like the following one we see in Ubuntu:
c4e3b2
    # qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
c4e3b2
    while b'qemu-system-x86_64: warning:' in read:
c4e3b2
        read = p.stdout.readline()
c4e3b2
    if args.print_output:
c4e3b2
        print(strip_special(read), end='')
c4e3b2
        print()
c4e3b2
    # Send the escape char to enter the UEFI shell early
c4e3b2
    p.stdin.write(b'\x1b')
c4e3b2
    p.stdin.flush()
c4e3b2
    # And then run the following three commands from the UEFI shell:
c4e3b2
    # change into the first file system device; install the default
c4e3b2
    # keys and certificates, and reboot
c4e3b2
    p.stdin.write(b'fs0:\r\n')
c4e3b2
    p.stdin.write(b'EnrollDefaultKeys.efi\r\n')
c4e3b2
    p.stdin.write(b'reset -s\r\n')
c4e3b2
    p.stdin.flush()
c4e3b2
    while True:
c4e3b2
        read = p.stdout.readline()
c4e3b2
        if args.print_output:
c4e3b2
            print('OUT: %s' % strip_special(read), end='')
c4e3b2
            print()
c4e3b2
        if b'info: success' in read:
c4e3b2
            break
c4e3b2
    p.wait()
c4e3b2
    if args.print_output:
c4e3b2
        print(strip_special(p.stdout.read()), end='')
c4e3b2
    logging.info('Finished enrollment')
c4e3b2
c4e3b2
c4e3b2
def test_keys(args):
c4e3b2
    logging.info('Grabbing test kernel')
c4e3b2
    kernel, kerneltemp = download(args.kernel_url, args.kernel_path,
c4e3b2
                                  'kernel', args.no_download)
c4e3b2
c4e3b2
    logging.info('Starting verification')
c4e3b2
    try:
c4e3b2
        cmd = generate_qemu_cmd(
c4e3b2
            args,
c4e3b2
            True,
c4e3b2
            '-append', 'console=tty0 console=ttyS0,115200n8',
c4e3b2
            '-kernel', kernel)
c4e3b2
        p = subprocess.Popen(cmd,
c4e3b2
            stdin=subprocess.PIPE,
c4e3b2
            stdout=subprocess.PIPE,
c4e3b2
            stderr=subprocess.STDOUT)
c4e3b2
        logging.info('Performing verification')
c4e3b2
        while True:
c4e3b2
            read = p.stdout.readline()
c4e3b2
            if args.print_output:
c4e3b2
                print('OUT: %s' % strip_special(read), end='')
c4e3b2
                print()
c4e3b2
            if b'Secure boot disabled' in read:
c4e3b2
                raise Exception('Secure Boot was disabled')
c4e3b2
            elif b'Secure boot enabled' in read:
c4e3b2
                logging.info('Confirmed: Secure Boot is enabled')
c4e3b2
                break
c4e3b2
            elif b'Kernel is locked down from EFI secure boot' in read:
c4e3b2
                logging.info('Confirmed: Secure Boot is enabled')
c4e3b2
                break
c4e3b2
        p.kill()
c4e3b2
        if args.print_output:
c4e3b2
            print(strip_special(p.stdout.read()), end='')
c4e3b2
        logging.info('Finished verification')
c4e3b2
    finally:
c4e3b2
        if kerneltemp:
c4e3b2
            os.remove(kernel)
c4e3b2
c4e3b2
c4e3b2
def parse_args():
c4e3b2
    parser = argparse.ArgumentParser()
c4e3b2
    parser.add_argument('output', help='Filename for output vars file')
c4e3b2
    parser.add_argument('--out-temp', help=argparse.SUPPRESS)
c4e3b2
    parser.add_argument('--force', help='Overwrite existing output file',
c4e3b2
                        action='store_true')
c4e3b2
    parser.add_argument('--print-output', help='Print the QEMU guest output',
c4e3b2
                        action='store_true')
c4e3b2
    parser.add_argument('--verbose', '-v', help='Increase verbosity',
c4e3b2
                        action='count')
c4e3b2
    parser.add_argument('--quiet', '-q', help='Decrease verbosity',
c4e3b2
                        action='count')
c4e3b2
    parser.add_argument('--qemu-binary', help='QEMU binary path',
c4e3b2
                        default='/usr/bin/qemu-system-x86_64')
c4e3b2
    parser.add_argument('--enable-kvm', help='Enable KVM acceleration',
c4e3b2
                        action='store_true')
c4e3b2
    parser.add_argument('--ovmf-binary', help='OVMF secureboot code file',
c4e3b2
                        default='/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd')
c4e3b2
    parser.add_argument('--ovmf-template-vars', help='OVMF empty vars file',
c4e3b2
                        default='/usr/share/edk2/ovmf/OVMF_VARS.fd')
c4e3b2
    parser.add_argument('--uefi-shell-iso', help='Path to uefi shell iso',
c4e3b2
                        default='/usr/share/edk2/ovmf/UefiShell.iso')
c4e3b2
    parser.add_argument('--skip-enrollment',
c4e3b2
                        help='Skip enrollment, only test', action='store_true')
c4e3b2
    parser.add_argument('--skip-testing',
c4e3b2
                        help='Skip testing generated "VARS" file',
c4e3b2
                        action='store_true')
c4e3b2
    parser.add_argument('--kernel-path',
c4e3b2
                        help='Specify a consistent path for kernel')
c4e3b2
    parser.add_argument('--no-download', action='store_true',
c4e3b2
                        help='Never download a kernel')
c4e3b2
    parser.add_argument('--fedora-version',
c4e3b2
                        help='Fedora version to get kernel for checking',
c4e3b2
                        default='27')
c4e3b2
    parser.add_argument('--kernel-url', help='Kernel URL',
c4e3b2
                        default='https://download.fedoraproject.org/pub/fedora'
c4e3b2
                                '/linux/releases/%(version)s/Everything/x86_64'
c4e3b2
                                '/os/images/pxeboot/vmlinuz')
c4e3b2
    parser.add_argument('--disable-smm',
c4e3b2
                        help=('Don\'t restrict varstore pflash writes to '
c4e3b2
                              'guest code that executes in SMM. Use this '
c4e3b2
                              'option only if your OVMF binary doesn\'t have '
c4e3b2
                              'the edk2 SMM driver stack built into it '
c4e3b2
                              '(possibly because your QEMU binary lacks SMM '
c4e3b2
                              'emulation). Note that without restricting '
c4e3b2
                              'varstore pflash writes to guest code that '
c4e3b2
                              'executes in SMM, a malicious guest kernel, '
c4e3b2
                              'used for testing, could undermine Secure '
c4e3b2
                              'Boot.'),
c4e3b2
                        action='store_true')
c4e3b2
    parser.add_argument('--oem-string',
c4e3b2
                        help=('Pass the argument to the guest as a string in '
c4e3b2
                              'the SMBIOS Type 11 (OEM Strings) table. '
c4e3b2
                              'Multiple occurrences of this option are '
c4e3b2
                              'collected into a single SMBIOS Type 11 table. '
c4e3b2
                              'A pure ASCII string argument is strongly '
c4e3b2
                              'suggested.'),
c4e3b2
                        action='append')
c4e3b2
    args = parser.parse_args()
c4e3b2
    args.kernel_url = args.kernel_url % {'version': args.fedora_version}
c4e3b2
c4e3b2
    validate_args(args)
c4e3b2
    return args
c4e3b2
c4e3b2
c4e3b2
def validate_args(args):
c4e3b2
    if (os.path.exists(args.output)
c4e3b2
            and not args.force
c4e3b2
            and not args.skip_enrollment):
c4e3b2
        raise Exception('%s already exists' % args.output)
c4e3b2
c4e3b2
    if args.skip_enrollment and not os.path.exists(args.output):
c4e3b2
        raise Exception('%s does not yet exist' % args.output)
c4e3b2
c4e3b2
    verbosity = (args.verbose or 1) - (args.quiet or 0)
c4e3b2
    if verbosity >= 2:
c4e3b2
        logging.basicConfig(level=logging.DEBUG)
c4e3b2
    elif verbosity == 1:
c4e3b2
        logging.basicConfig(level=logging.INFO)
c4e3b2
    elif verbosity < 0:
c4e3b2
        logging.basicConfig(level=logging.ERROR)
c4e3b2
    else:
c4e3b2
        logging.basicConfig(level=logging.WARN)
c4e3b2
c4e3b2
    if args.skip_enrollment:
c4e3b2
        args.out_temp = args.output
c4e3b2
    else:
c4e3b2
        temped = tempfile.mkstemp(prefix='qosb.', suffix='.vars')
c4e3b2
        os.close(temped[0])
c4e3b2
        args.out_temp = temped[1]
c4e3b2
        logging.debug('Temp output: %s', args.out_temp)
c4e3b2
c4e3b2
c4e3b2
def move_to_dest(args):
c4e3b2
    shutil.copy(args.out_temp, args.output)
c4e3b2
    os.remove(args.out_temp)
c4e3b2
c4e3b2
c4e3b2
def main():
c4e3b2
    args = parse_args()
c4e3b2
    if not args.skip_enrollment:
c4e3b2
        enroll_keys(args)
c4e3b2
    if not args.skip_testing:
c4e3b2
        test_keys(args)
c4e3b2
    if not args.skip_enrollment:
c4e3b2
        move_to_dest(args)
c4e3b2
        if args.skip_testing:
c4e3b2
            logging.info('Created %s' % args.output)
c4e3b2
        else:
c4e3b2
            logging.info('Created and verified %s' % args.output)
c4e3b2
    else:
c4e3b2
        logging.info('Verified %s', args.output)
c4e3b2
c4e3b2
c4e3b2
if __name__ == '__main__':
c4e3b2
    main()