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