]>
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 | ||
a65627a8 TL |
204 | def test_ovmf_4m(self): |
205 | q = Qemu.QemuCommand( | |
206 | QemuEfiMachine.OVMF_Q35, | |
207 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
208 | ) | |
209 | self.run_cmd_check_shell(q.command) | |
210 | ||
211 | def test_ovmf_4m_secboot(self): | |
212 | q = Qemu.QemuCommand( | |
213 | QemuEfiMachine.OVMF_Q35, | |
214 | variant=QemuEfiVariant.SECBOOT, | |
215 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
216 | ) | |
217 | self.run_cmd_check_shell(q.command) | |
218 | ||
219 | def test_ovmf_4m_ms(self): | |
220 | q = Qemu.QemuCommand( | |
221 | QemuEfiMachine.OVMF_Q35, | |
222 | variant=QemuEfiVariant.MS, | |
223 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
224 | ) | |
225 | self.run_cmd_check_shell(q.command) | |
226 | ||
227 | def test_ovmf_snakeoil(self): | |
228 | q = Qemu.QemuCommand( | |
229 | QemuEfiMachine.OVMF_Q35, | |
230 | variant=QemuEfiVariant.SNAKEOIL, | |
231 | ) | |
232 | self.run_cmd_check_shell(q.command) | |
233 | ||
234 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
235 | def test_ovmf_4m_ms_secure_boot_signed(self): | |
236 | q = Qemu.QemuCommand( | |
237 | QemuEfiMachine.OVMF_Q35, | |
238 | variant=QemuEfiVariant.MS, | |
239 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
240 | ) | |
d7274593 TL |
241 | grub = get_local_grub_path('X64', signed=True) |
242 | shim = get_local_shim_path('X64', signed=True) | |
243 | iso = GrubShellBootableIsoImage('X64', shim, grub) | |
a65627a8 TL |
244 | q.add_disk(iso.path) |
245 | self.run_cmd_check_secure_boot(q.command, 'x64', True) | |
246 | ||
247 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
248 | def test_ovmf_4m_ms_secure_boot_unsigned(self): | |
249 | q = Qemu.QemuCommand( | |
250 | QemuEfiMachine.OVMF_Q35, | |
251 | variant=QemuEfiVariant.MS, | |
252 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
253 | ) | |
d7274593 TL |
254 | grub = get_local_grub_path('X64', signed=False) |
255 | shim = get_local_shim_path('X64', signed=False) | |
256 | iso = GrubShellBootableIsoImage('X64', shim, grub) | |
257 | q.add_disk(iso.path) | |
258 | self.run_cmd_check_secure_boot(q.command, 'x64', False) | |
259 | ||
260 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
261 | def test_ovmf_snakeoil_secure_boot_signed(self): | |
262 | q = Qemu.QemuCommand( | |
263 | QemuEfiMachine.OVMF_Q35, | |
264 | variant=QemuEfiVariant.SNAKEOIL, | |
265 | ) | |
266 | shim = SignedBinary( | |
267 | get_local_shim_path('X64', signed=False), | |
268 | "/usr/share/ovmf/PkKek-1-snakeoil.key", | |
269 | "/usr/share/ovmf/PkKek-1-snakeoil.pem", | |
270 | "snakeoil", | |
271 | ) | |
272 | grub = SignedBinary( | |
273 | get_local_grub_path('X64', signed=False), | |
274 | "/usr/share/ovmf/PkKek-1-snakeoil.key", | |
275 | "/usr/share/ovmf/PkKek-1-snakeoil.pem", | |
276 | "snakeoil", | |
277 | ) | |
278 | iso = GrubShellBootableIsoImage('X64', shim.path, grub.path) | |
279 | q.add_disk(iso.path) | |
280 | self.run_cmd_check_secure_boot(q.command, 'x64', True) | |
281 | ||
282 | @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only") | |
283 | def test_ovmf_snakeoil_secure_boot_unsigned(self): | |
284 | q = Qemu.QemuCommand( | |
285 | QemuEfiMachine.OVMF_Q35, | |
286 | variant=QemuEfiVariant.SNAKEOIL, | |
287 | flash_size=QemuEfiFlashSize.DEFAULT, | |
288 | ) | |
289 | grub = get_local_grub_path('X64', signed=False) | |
290 | shim = get_local_shim_path('X64', signed=False) | |
291 | iso = GrubShellBootableIsoImage('X64', shim, grub) | |
a65627a8 TL |
292 | q.add_disk(iso.path) |
293 | self.run_cmd_check_secure_boot(q.command, 'x64', False) | |
294 | ||
295 | def test_ovmf32_4m_secboot(self): | |
296 | q = Qemu.QemuCommand( | |
297 | QemuEfiMachine.OVMF32, | |
298 | variant=QemuEfiVariant.SECBOOT, | |
299 | flash_size=QemuEfiFlashSize.SIZE_4MB, | |
300 | ) | |
301 | self.run_cmd_check_shell(q.command) | |
302 | ||
d7298091 TL |
303 | def test_riscv64(self): |
304 | q = Qemu.QemuCommand(QemuEfiMachine.RISCV64) | |
305 | self.run_cmd_check_shell(q.command) | |
a65627a8 TL |
306 | |
307 | if __name__ == '__main__': | |
308 | unittest.main(verbosity=2) |