forked from rhuefi/qemu-ovmf-secureboot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ovmf-vars-generator
executable file
·298 lines (267 loc) · 11 KB
/
ovmf-vars-generator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
#!/bin/python3
# Copyright (C) 2017 Red Hat
# Authors:
# - Patrick Uiterwijk <[email protected]>
# - Kashyap Chamarthy <[email protected]>
#
# 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-reboot',
'-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 and len(read):
print('OUT: %s' % strip_special(read), end='')
print()
if b'info: success' in read:
break
elif b'Reset with <null string>' 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 and len(read):
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://archives.fedoraproject.org/pub'
'/archive/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()