]>
Commit | Line | Data |
---|---|---|
a65627a8 TL |
1 | #!/usr/bin/env python3 |
2 | # | |
d7274593 | 3 | # Copyright 2019-2022 Canonical Ltd. |
a65627a8 TL |
4 | # Authors: |
5 | # - dann frazier <dann.frazier@canonical.com> | |
6 | # | |
7 | # This program is free software: you can redistribute it and/or modify it | |
8 | # under the terms of the GNU General Public License version 3, as published | |
9 | # by the Free Software Foundation. | |
10 | # | |
11 | # This program is distributed in the hope that it will be useful, but WITHOUT | |
12 | # ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, | |
13 | # SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
14 | # General Public License for more details. | |
15 | # | |
16 | # You should have received a copy of the GNU General Public License along with | |
17 | # this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | # | |
19 | ||
20 | import enum | |
d7274593 | 21 | import os |
a65627a8 TL |
22 | import pexpect |
23 | import subprocess | |
24 | import sys | |
d7274593 | 25 | import time |
a65627a8 TL |
26 | import unittest |
27 | ||
28 | from UEFI.Filesystems import GrubShellBootableIsoImage | |
d7274593 | 29 | from UEFI.SignedBinary import SignedBinary |
a65627a8 TL |
30 | from UEFI.Qemu import QemuEfiMachine, QemuEfiVariant, QemuEfiFlashSize |
31 | from UEFI import Qemu | |
32 | ||
33 | DPKG_ARCH = subprocess.check_output( | |
34 | ['dpkg', '--print-architecture'] | |
35 | ).decode().rstrip() | |
36 | ||
d7274593 TL |
37 | EfiArchToGrubArch = { |
38 | 'X64': "x86_64", | |
39 | 'AA64': "arm64", | |
40 | } | |
41 | ||
42 | TEST_TIMEOUT = 120 | |
43 | ||
44 | ||
45 | def get_local_grub_path(efi_arch, signed=False): | |
46 | grub_subdir = "%s-efi" % EfiArchToGrubArch[efi_arch.upper()] | |
47 | ext = "efi" | |
48 | if signed: | |
49 | grub_subdir = f"{grub_subdir}-signed" | |
50 | ext = f"{ext}.signed" | |
51 | ||
52 | grub_path = os.path.join( | |
53 | os.path.sep, 'usr', 'lib', 'grub', | |
54 | '%s' % (grub_subdir), | |
55 | "" if signed else "monolithic", | |
56 | 'grub%s.%s' % (efi_arch.lower(), ext) | |
57 | ) | |
58 | return grub_path | |
59 | ||
60 | ||
61 | def get_local_shim_path(efi_arch, signed=False): | |
62 | ext = 'efi' | |
63 | if signed: | |
64 | ext = f"{ext}.signed" | |
65 | shim_path = os.path.join( | |
66 | os.path.sep, 'usr', 'lib', 'shim', | |
67 | 'shim%s.%s' % (efi_arch.lower(), ext) | |
68 | ) | |
69 | return shim_path | |
70 | ||
a65627a8 TL |
71 | |
72 | class BootToShellTest(unittest.TestCase): | |
73 | debug = True | |
74 | ||
d7274593 TL |
75 | def setUp(self): |
76 | self.startTime = time.time() | |
77 | ||
78 | def tearDown(self): | |
79 | t = time.time() - self.startTime | |
80 | sys.stdout.write("%s runtime: %.3fs\n" % (self.id(), t)) | |
81 | ||
a65627a8 | 82 | def run_cmd_check_shell(self, cmd): |
d7274593 | 83 | child = pexpect.spawn(' '.join(cmd), encoding='UTF-8') |
a65627a8 TL |
84 | |
85 | if self.debug: | |
d7274593 | 86 | child.logfile = sys.stdout |
a65627a8 TL |
87 | try: |
88 | while True: | |
89 | i = child.expect( | |
90 | [ | |
91 | 'Press .* or any other key to continue', | |
92 | 'Shell> ' | |
93 | ], | |
d7274593 | 94 | timeout=TEST_TIMEOUT, |
a65627a8 TL |
95 | ) |
96 | if i == 0: | |
97 | child.sendline('\x1b') | |
98 | continue | |
99 | if i == 1: | |
100 | child.sendline('reset -s\r') | |
101 | continue | |
102 | except pexpect.EOF: | |
d7274593 TL |
103 | child.close() |
104 | if child.exitstatus != 0: | |
105 | self.fail("ERROR: exit code %d\n" % (child.exitstatus)) | |
a65627a8 TL |
106 | except pexpect.TIMEOUT as err: |
107 | self.fail("%s\n" % (err)) | |
108 | ||
109 | def run_cmd_check_secure_boot(self, cmd, efiarch, should_verify): | |
110 | class State(enum.Enum): | |
111 | PRE_EXEC = 1 | |
112 | POST_EXEC = 2 | |
113 | ||
d7274593 | 114 | child = pexpect.spawn(' '.join(cmd), encoding='UTF-8') |
a65627a8 TL |
115 | |
116 | if self.debug: | |
d7274593 | 117 | child.logfile = sys.stdout |
a65627a8 TL |
118 | try: |
119 | state = State.PRE_EXEC | |
120 | while True: | |
121 | i = child.expect( | |
122 | [ | |
123 | 'Press .* or any other key to continue', | |
124 | 'Shell> ', | |
125 | "FS0:\\\\> ", | |
126 | 'grub> ', | |
127 | 'Command Error Status: Access Denied', | |
128 | ], | |
d7274593 | 129 | timeout=TEST_TIMEOUT, |
a65627a8 TL |
130 | ) |
131 | if i == 0: | |
132 | child.sendline('\x1b') | |
133 | continue | |
134 | if i == 1: | |
135 | child.sendline('fs0:\r') | |
136 | continue | |
137 | if i == 2: | |
138 | if state == State.PRE_EXEC: | |
139 | child.sendline(f'\\efi\\boot\\boot{efiarch}.efi\r') | |
140 | state = State.POST_EXEC | |
141 | elif state == State.POST_EXEC: | |
142 | child.sendline('reset -s\r') | |
143 | continue | |
144 | if i == 3: | |
145 | child.sendline('halt\r') | |
146 | verified = True | |
147 | continue | |
148 | if i == 4: | |
149 | verified = False | |
150 | continue | |
d7274593 TL |
151 | except pexpect.EOF: |
152 | child.close() | |
153 | if child.exitstatus != 0: | |
154 | self.fail("ERROR: exit code %d\n" % (child.exitstatus)) | |
a65627a8 TL |
155 | except pexpect.TIMEOUT as err: |
156 | self.fail("%s\n" % (err)) | |
a65627a8 TL |
157 | self.assertEqual(should_verify, verified) |
158 | ||
159 | def test_aavmf(self): | |
160 | q = Qemu.QemuCommand(QemuEfiMachine.AAVMF) | |
161 | self.run_cmd_check_shell(q.command) | |
162 | ||
163 | @unittest.skipUnless(DPKG_ARCH == 'arm64', "Requires grub-efi-arm64") | |
d7274593 TL |
164 | @unittest.skipUnless( |
165 | subprocess.run( | |
166 | ['dpkg-vendor', '--derives-from', 'Ubuntu'] | |
167 | ).returncode == 0, | |
168 | "Debian does not provide a signed shim for arm64, see #992073" | |
169 | ) | |
a65627a8 TL |
170 | def test_aavmf_ms_secure_boot_signed(self): |
171 | q = Qemu.QemuCommand( | |
172 | QemuEfiMachine.AAVMF, | |
173 | variant=QemuEfiVariant.MS, | |
174 | ) | |
d7274593 TL |
175 | grub = get_local_grub_path('AA64', signed=True) |
176 | shim = get_local_shim_path('AA64', signed=True) | |
177 | iso = GrubShellBootableIsoImage('AA64', shim, grub) | |
a65627a8 TL |
178 | q.add_disk(iso.path) |
179 | self.run_cmd_check_secure_boot(q.command, 'aa64', True) | |
180 | ||
181 | @unittest.skipUnless(DPKG_ARCH == 'arm64', "Requires grub-efi-arm64") | |
182 | def test_aavmf_ms_secure_boot_unsigned(self): | |
183 | q = Qemu.QemuCommand( | |
184 | QemuEfiMachine.AAVMF, | |
185 | variant=QemuEfiVariant.MS, | |
186 | ) | |
d7274593 TL |
187 | grub = get_local_grub_path('AA64', signed=False) |
188 | shim = get_local_shim_path('AA64', signed=False) | |
189 | iso = GrubShellBootableIsoImage('AA64', shim, grub) | |
a65627a8 TL |
190 | q.add_disk(iso.path) |
191 | self.run_cmd_check_secure_boot(q.command, 'aa64', False) | |
192 | ||
193 | def test_aavmf_snakeoil(self): | |
194 | q = Qemu.QemuCommand( | |
195 | QemuEfiMachine.AAVMF, | |
196 | variant=QemuEfiVariant.SNAKEOIL, | |
197 | ) | |
198 | self.run_cmd_check_shell(q.command) | |
199 | ||
200 | def test_aavmf32(self): | |
201 | q = Qemu.QemuCommand(QemuEfiMachine.AAVMF32) | |
202 | self.run_cmd_check_shell(q.command) | |
203 | ||
204 | def test_ovmf_pc(self): | |
205 | q = Qemu.QemuCommand( | |
206 | QemuEfiMachine.OVMF_PC, flash_size=QemuEfiFlashSize.SIZE_2MB, | |
207 | ) | |
208 | self.run_cmd_check_shell(q.command) | |
209 | ||
210 | def test_ovmf_q35(self): | |
211 | q = Qemu.QemuCommand( | |
212 | QemuEfiMachine.OVMF_Q35, flash_size=QemuEfiFlashSize.SIZE_2MB, | |
213 | ) | |
214 | self.run_cmd_check_shell(q.command) | |
215 | ||
216 | def test_ovmf_secboot(self): | |
217 | q = Qemu.QemuCommand( | |
218 | QemuEfiMachine.OVMF_Q35, | |
219 | variant=QemuEfiVariant.SECBOOT, | |
220 | flash_size=QemuEfiFlashSize.SIZE_2MB, | |
221 | ) | |
222 | self.run_cmd_check_shell(q.command) | |
223 | ||
224 | def test_ovmf_ms(self): | |
225 | q = Qemu.QemuCommand( | |
226 | QemuEfiMachine.OVMF_Q35, | |
227 | variant=QemuEfiVariant.MS, | |
228 | flash_size=QemuEfiFlashSize.SIZE_2MB, | |
229 | ) | |
230 | self.run_cmd_check_shell(q.command) | |
231 | ||
232 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
233 | def test_ovmf_ms_secure_boot_signed(self): | |
234 | q = Qemu.QemuCommand( | |
235 | QemuEfiMachine.OVMF_Q35, | |
236 | variant=QemuEfiVariant.MS, | |
237 | flash_size=QemuEfiFlashSize.SIZE_2MB, | |
238 | ) | |
d7274593 TL |
239 | grub = get_local_grub_path('X64', signed=True) |
240 | shim = get_local_shim_path('X64', signed=True) | |
241 | iso = GrubShellBootableIsoImage('X64', shim, grub) | |
a65627a8 TL |
242 | q.add_disk(iso.path) |
243 | self.run_cmd_check_secure_boot(q.command, 'x64', True) | |
244 | ||
245 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
246 | def test_ovmf_ms_secure_boot_unsigned(self): | |
247 | q = Qemu.QemuCommand( | |
248 | QemuEfiMachine.OVMF_Q35, | |
249 | variant=QemuEfiVariant.MS, | |
250 | flash_size=QemuEfiFlashSize.SIZE_2MB, | |
251 | ) | |
d7274593 TL |
252 | grub = get_local_grub_path('X64', signed=False) |
253 | shim = get_local_shim_path('X64', signed=False) | |
254 | iso = GrubShellBootableIsoImage('X64', shim, grub) | |
a65627a8 TL |
255 | q.add_disk(iso.path) |
256 | self.run_cmd_check_secure_boot(q.command, 'x64', False) | |
257 | ||
258 | def test_ovmf_4m(self): | |
259 | q = Qemu.QemuCommand( | |
260 | QemuEfiMachine.OVMF_Q35, | |
261 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
262 | ) | |
263 | self.run_cmd_check_shell(q.command) | |
264 | ||
265 | def test_ovmf_4m_secboot(self): | |
266 | q = Qemu.QemuCommand( | |
267 | QemuEfiMachine.OVMF_Q35, | |
268 | variant=QemuEfiVariant.SECBOOT, | |
269 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
270 | ) | |
271 | self.run_cmd_check_shell(q.command) | |
272 | ||
273 | def test_ovmf_4m_ms(self): | |
274 | q = Qemu.QemuCommand( | |
275 | QemuEfiMachine.OVMF_Q35, | |
276 | variant=QemuEfiVariant.MS, | |
277 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
278 | ) | |
279 | self.run_cmd_check_shell(q.command) | |
280 | ||
281 | def test_ovmf_snakeoil(self): | |
282 | q = Qemu.QemuCommand( | |
283 | QemuEfiMachine.OVMF_Q35, | |
284 | variant=QemuEfiVariant.SNAKEOIL, | |
285 | ) | |
286 | self.run_cmd_check_shell(q.command) | |
287 | ||
288 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
289 | def test_ovmf_4m_ms_secure_boot_signed(self): | |
290 | q = Qemu.QemuCommand( | |
291 | QemuEfiMachine.OVMF_Q35, | |
292 | variant=QemuEfiVariant.MS, | |
293 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
294 | ) | |
d7274593 TL |
295 | grub = get_local_grub_path('X64', signed=True) |
296 | shim = get_local_shim_path('X64', signed=True) | |
297 | iso = GrubShellBootableIsoImage('X64', shim, grub) | |
a65627a8 TL |
298 | q.add_disk(iso.path) |
299 | self.run_cmd_check_secure_boot(q.command, 'x64', True) | |
300 | ||
301 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
302 | def test_ovmf_4m_ms_secure_boot_unsigned(self): | |
303 | q = Qemu.QemuCommand( | |
304 | QemuEfiMachine.OVMF_Q35, | |
305 | variant=QemuEfiVariant.MS, | |
306 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
307 | ) | |
d7274593 TL |
308 | grub = get_local_grub_path('X64', signed=False) |
309 | shim = get_local_shim_path('X64', signed=False) | |
310 | iso = GrubShellBootableIsoImage('X64', shim, grub) | |
311 | q.add_disk(iso.path) | |
312 | self.run_cmd_check_secure_boot(q.command, 'x64', False) | |
313 | ||
314 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
315 | def test_ovmf_snakeoil_secure_boot_signed(self): | |
316 | q = Qemu.QemuCommand( | |
317 | QemuEfiMachine.OVMF_Q35, | |
318 | variant=QemuEfiVariant.SNAKEOIL, | |
319 | ) | |
320 | shim = SignedBinary( | |
321 | get_local_shim_path('X64', signed=False), | |
322 | "/usr/share/ovmf/PkKek-1-snakeoil.key", | |
323 | "/usr/share/ovmf/PkKek-1-snakeoil.pem", | |
324 | "snakeoil", | |
325 | ) | |
326 | grub = SignedBinary( | |
327 | get_local_grub_path('X64', signed=False), | |
328 | "/usr/share/ovmf/PkKek-1-snakeoil.key", | |
329 | "/usr/share/ovmf/PkKek-1-snakeoil.pem", | |
330 | "snakeoil", | |
331 | ) | |
332 | iso = GrubShellBootableIsoImage('X64', shim.path, grub.path) | |
333 | q.add_disk(iso.path) | |
334 | self.run_cmd_check_secure_boot(q.command, 'x64', True) | |
335 | ||
336 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
337 | def test_ovmf_snakeoil_secure_boot_unsigned(self): | |
338 | q = Qemu.QemuCommand( | |
339 | QemuEfiMachine.OVMF_Q35, | |
340 | variant=QemuEfiVariant.SNAKEOIL, | |
341 | flash_size=QemuEfiFlashSize.DEFAULT, | |
342 | ) | |
343 | grub = get_local_grub_path('X64', signed=False) | |
344 | shim = get_local_shim_path('X64', signed=False) | |
345 | iso = GrubShellBootableIsoImage('X64', shim, grub) | |
a65627a8 TL |
346 | q.add_disk(iso.path) |
347 | self.run_cmd_check_secure_boot(q.command, 'x64', False) | |
348 | ||
349 | def test_ovmf32_4m_secboot(self): | |
350 | q = Qemu.QemuCommand( | |
351 | QemuEfiMachine.OVMF32, | |
352 | variant=QemuEfiVariant.SECBOOT, | |
353 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
354 | ) | |
355 | self.run_cmd_check_shell(q.command) | |
356 | ||
357 | ||
358 | if __name__ == '__main__': | |
359 | unittest.main(verbosity=2) |