]> git.ipfire.org Git - thirdparty/systemd.git/blob - src/ukify/test/test_ukify.py
test_ukify: formatting
[thirdparty/systemd.git] / src / ukify / test / test_ukify.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: LGPL-2.1-or-later
3
4 # pylint: disable=unused-import,import-outside-toplevel,useless-else-on-loop
5 # pylint: disable=consider-using-with,wrong-import-position,unspecified-encoding
6 # pylint: disable=protected-access,redefined-outer-name
7
8 import base64
9 import json
10 import os
11 import pathlib
12 import re
13 import shutil
14 import subprocess
15 import sys
16 import tempfile
17 import textwrap
18
19 try:
20 import pytest
21 except ImportError as e:
22 print(str(e), file=sys.stderr)
23 sys.exit(77)
24
25 try:
26 # pyflakes: noqa
27 import pefile # noqa
28 except ImportError as e:
29 print(str(e), file=sys.stderr)
30 sys.exit(77)
31
32 # We import ukify.py, which is a template file. But only __version__ is
33 # substituted, which we don't care about here. Having the .py suffix makes it
34 # easier to import the file.
35 sys.path.append(os.path.dirname(__file__) + '/..')
36 import ukify
37
38 build_root = os.getenv('PROJECT_BUILD_ROOT')
39 arg_tools = ['--tools', build_root] if build_root else []
40
41 def systemd_measure():
42 opts = ukify.create_parser().parse_args(arg_tools)
43 return ukify.find_tool('systemd-measure', opts=opts)
44
45 def test_guess_efi_arch():
46 arch = ukify.guess_efi_arch()
47 assert arch in ukify.EFI_ARCHES
48
49 def test_shell_join():
50 assert ukify.shell_join(['a', 'b', ' ']) == "a b ' '"
51
52 def test_round_up():
53 assert ukify.round_up(0) == 0
54 assert ukify.round_up(4095) == 4096
55 assert ukify.round_up(4096) == 4096
56 assert ukify.round_up(4097) == 8192
57
58 def test_namespace_creation():
59 ns = ukify.create_parser().parse_args(())
60 assert ns.linux is None
61 assert ns.initrd is None
62
63 def test_config_example():
64 ex = ukify.config_example()
65 assert '[UKI]' in ex
66 assert 'Splash = BMP' in ex
67
68 def test_apply_config(tmp_path):
69 config = tmp_path / 'config1.conf'
70 config.write_text(textwrap.dedent(
71 f'''
72 [UKI]
73 Linux = LINUX
74 Initrd = initrd1 initrd2
75 initrd3
76 Cmdline = 1 2 3 4 5
77 6 7 8
78 OSRelease = @some/path1
79 DeviceTree = some/path2
80 Splash = some/path3
81 Uname = 1.2.3
82 EFIArch=arm
83 Stub = some/path4
84 PCRBanks = sha512,sha1
85 SigningEngine = engine1
86 SecureBootPrivateKey = some/path5
87 SecureBootCertificate = some/path6
88 SignKernel = no
89
90 [PCRSignature:NAME]
91 PCRPrivateKey = some/path7
92 PCRPublicKey = some/path8
93 Phases = {':'.join(ukify.KNOWN_PHASES)}
94 '''))
95
96 ns = ukify.create_parser().parse_args(['build'])
97 ns.linux = None
98 ns.initrd = []
99 ukify.apply_config(ns, config)
100
101 assert ns.linux == pathlib.Path('LINUX')
102 assert ns.initrd == [pathlib.Path('initrd1'),
103 pathlib.Path('initrd2'),
104 pathlib.Path('initrd3')]
105 assert ns.cmdline == '1 2 3 4 5\n6 7 8'
106 assert ns.os_release == '@some/path1'
107 assert ns.devicetree == pathlib.Path('some/path2')
108 assert ns.splash == pathlib.Path('some/path3')
109 assert ns.efi_arch == 'arm'
110 assert ns.stub == pathlib.Path('some/path4')
111 assert ns.pcr_banks == ['sha512', 'sha1']
112 assert ns.signing_engine == 'engine1'
113 assert ns.sb_key == 'some/path5'
114 assert ns.sb_cert == 'some/path6'
115 assert ns.sign_kernel is False
116
117 assert ns._groups == ['NAME']
118 assert ns.pcr_private_keys == [pathlib.Path('some/path7')]
119 assert ns.pcr_public_keys == [pathlib.Path('some/path8')]
120 assert ns.phase_path_groups == [['enter-initrd:leave-initrd:sysinit:ready:shutdown:final']]
121
122 ukify.finalize_options(ns)
123
124 assert ns.linux == pathlib.Path('LINUX')
125 assert ns.initrd == [pathlib.Path('initrd1'),
126 pathlib.Path('initrd2'),
127 pathlib.Path('initrd3')]
128 assert ns.cmdline == '1 2 3 4 5 6 7 8'
129 assert ns.os_release == pathlib.Path('some/path1')
130 assert ns.devicetree == pathlib.Path('some/path2')
131 assert ns.splash == pathlib.Path('some/path3')
132 assert ns.efi_arch == 'arm'
133 assert ns.stub == pathlib.Path('some/path4')
134 assert ns.pcr_banks == ['sha512', 'sha1']
135 assert ns.signing_engine == 'engine1'
136 assert ns.sb_key == 'some/path5'
137 assert ns.sb_cert == 'some/path6'
138 assert ns.sign_kernel is False
139
140 assert ns._groups == ['NAME']
141 assert ns.pcr_private_keys == [pathlib.Path('some/path7')]
142 assert ns.pcr_public_keys == [pathlib.Path('some/path8')]
143 assert ns.phase_path_groups == [['enter-initrd:leave-initrd:sysinit:ready:shutdown:final']]
144
145 def test_parse_args_minimal():
146 with pytest.raises(ValueError):
147 ukify.parse_args([])
148
149 opts = ukify.parse_args('arg1 arg2'.split())
150 assert opts.linux == pathlib.Path('arg1')
151 assert opts.initrd == [pathlib.Path('arg2')]
152 assert opts.os_release in (pathlib.Path('/etc/os-release'),
153 pathlib.Path('/usr/lib/os-release'))
154
155 def test_parse_args_many_deprecated():
156 opts = ukify.parse_args(
157 ['/ARG1', '///ARG2', '/ARG3 WITH SPACE',
158 '--cmdline=a b c',
159 '--os-release=K1=V1\nK2=V2',
160 '--devicetree=DDDDTTTT',
161 '--splash=splash',
162 '--pcrpkey=PATH',
163 '--uname=1.2.3',
164 '--stub=STUBPATH',
165 '--pcr-private-key=PKEY1',
166 '--pcr-public-key=PKEY2',
167 '--pcr-banks=SHA1,SHA256',
168 '--signing-engine=ENGINE',
169 '--secureboot-private-key=SBKEY',
170 '--secureboot-certificate=SBCERT',
171 '--sign-kernel',
172 '--no-sign-kernel',
173 '--tools=TOOLZ///',
174 '--output=OUTPUT',
175 '--measure',
176 '--no-measure',
177 ])
178 assert opts.linux == pathlib.Path('/ARG1')
179 assert opts.initrd == [pathlib.Path('/ARG2'), pathlib.Path('/ARG3 WITH SPACE')]
180 assert opts.cmdline == 'a b c'
181 assert opts.os_release == 'K1=V1\nK2=V2'
182 assert opts.devicetree == pathlib.Path('DDDDTTTT')
183 assert opts.splash == pathlib.Path('splash')
184 assert opts.pcrpkey == pathlib.Path('PATH')
185 assert opts.uname == '1.2.3'
186 assert opts.stub == pathlib.Path('STUBPATH')
187 assert opts.pcr_private_keys == [pathlib.Path('PKEY1')]
188 assert opts.pcr_public_keys == [pathlib.Path('PKEY2')]
189 assert opts.pcr_banks == ['SHA1', 'SHA256']
190 assert opts.signing_engine == 'ENGINE'
191 assert opts.sb_key == 'SBKEY'
192 assert opts.sb_cert == 'SBCERT'
193 assert opts.sign_kernel is False
194 assert opts.tools == [pathlib.Path('TOOLZ/')]
195 assert opts.output == pathlib.Path('OUTPUT')
196 assert opts.measure is False
197
198 def test_parse_args_many():
199 opts = ukify.parse_args(
200 ['build',
201 '--linux=/ARG1',
202 '--initrd=///ARG2',
203 '--initrd=/ARG3 WITH SPACE',
204 '--cmdline=a b c',
205 '--os-release=K1=V1\nK2=V2',
206 '--devicetree=DDDDTTTT',
207 '--splash=splash',
208 '--pcrpkey=PATH',
209 '--uname=1.2.3',
210 '--stub=STUBPATH',
211 '--pcr-private-key=PKEY1',
212 '--pcr-public-key=PKEY2',
213 '--pcr-banks=SHA1,SHA256',
214 '--signing-engine=ENGINE',
215 '--secureboot-private-key=SBKEY',
216 '--secureboot-certificate=SBCERT',
217 '--sign-kernel',
218 '--no-sign-kernel',
219 '--tools=TOOLZ///',
220 '--output=OUTPUT',
221 '--measure',
222 '--no-measure',
223 ])
224 assert opts.linux == pathlib.Path('/ARG1')
225 assert opts.initrd == [pathlib.Path('/ARG2'), pathlib.Path('/ARG3 WITH SPACE')]
226 assert opts.cmdline == 'a b c'
227 assert opts.os_release == 'K1=V1\nK2=V2'
228 assert opts.devicetree == pathlib.Path('DDDDTTTT')
229 assert opts.splash == pathlib.Path('splash')
230 assert opts.pcrpkey == pathlib.Path('PATH')
231 assert opts.uname == '1.2.3'
232 assert opts.stub == pathlib.Path('STUBPATH')
233 assert opts.pcr_private_keys == [pathlib.Path('PKEY1')]
234 assert opts.pcr_public_keys == [pathlib.Path('PKEY2')]
235 assert opts.pcr_banks == ['SHA1', 'SHA256']
236 assert opts.signing_engine == 'ENGINE'
237 assert opts.sb_key == 'SBKEY'
238 assert opts.sb_cert == 'SBCERT'
239 assert opts.sign_kernel is False
240 assert opts.tools == [pathlib.Path('TOOLZ/')]
241 assert opts.output == pathlib.Path('OUTPUT')
242 assert opts.measure is False
243
244 def test_parse_sections():
245 opts = ukify.parse_args(
246 ['build',
247 '--linux=/ARG1',
248 '--initrd=/ARG2',
249 '--section=test:TESTTESTTEST',
250 '--section=test2:@FILE',
251 ])
252
253 assert opts.linux == pathlib.Path('/ARG1')
254 assert opts.initrd == [pathlib.Path('/ARG2')]
255 assert len(opts.sections) == 2
256
257 assert opts.sections[0].name == 'test'
258 assert isinstance(opts.sections[0].content, pathlib.Path)
259 assert opts.sections[0].tmpfile
260 assert opts.sections[0].measure is False
261
262 assert opts.sections[1].name == 'test2'
263 assert opts.sections[1].content == pathlib.Path('FILE')
264 assert opts.sections[1].tmpfile is None
265 assert opts.sections[1].measure is False
266
267 def test_config_priority(tmp_path):
268 config = tmp_path / 'config1.conf'
269 # config: use pesign and give certdir + certname
270 config.write_text(textwrap.dedent(
271 f'''
272 [UKI]
273 Linux = LINUX
274 Initrd = initrd1 initrd2
275 initrd3
276 Cmdline = 1 2 3 4 5
277 6 7 8
278 OSRelease = @some/path1
279 DeviceTree = some/path2
280 Splash = some/path3
281 Uname = 1.2.3
282 EFIArch = arm
283 Stub = some/path4
284 PCRBanks = sha512,sha1
285 SigningEngine = engine1
286 SecureBootSigningTool = pesign
287 SecureBootCertificateDir = some/path5
288 SecureBootCertificateName = some/name1
289 SignKernel = no
290
291 [PCRSignature:NAME]
292 PCRPrivateKey = some/path7
293 PCRPublicKey = some/path8
294 Phases = {':'.join(ukify.KNOWN_PHASES)}
295 '''))
296
297 # args: use sbsign and give key + cert, should override pesign
298 opts = ukify.parse_args(
299 ['build',
300 '--linux=/ARG1',
301 '--initrd=///ARG2',
302 '--initrd=/ARG3 WITH SPACE',
303 '--cmdline= a b c ',
304 '--os-release=K1=V1\nK2=V2',
305 '--devicetree=DDDDTTTT',
306 '--splash=splash',
307 '--pcrpkey=PATH',
308 '--uname=1.2.3',
309 '--stub=STUBPATH',
310 '--pcr-private-key=PKEY1',
311 '--pcr-public-key=PKEY2',
312 '--pcr-banks=SHA1,SHA256',
313 '--signing-engine=ENGINE',
314 '--signtool=sbsign',
315 '--secureboot-private-key=SBKEY',
316 '--secureboot-certificate=SBCERT',
317 '--sign-kernel',
318 '--no-sign-kernel',
319 '--tools=TOOLZ///',
320 '--output=OUTPUT',
321 '--measure',
322 ])
323
324 ukify.apply_config(opts, config)
325 ukify.finalize_options(opts)
326
327 assert opts.linux == pathlib.Path('/ARG1')
328 assert opts.initrd == [pathlib.Path('initrd1'),
329 pathlib.Path('initrd2'),
330 pathlib.Path('initrd3'),
331 pathlib.Path('/ARG2'),
332 pathlib.Path('/ARG3 WITH SPACE')]
333 assert opts.cmdline == 'a b c'
334 assert opts.os_release == 'K1=V1\nK2=V2'
335 assert opts.devicetree == pathlib.Path('DDDDTTTT')
336 assert opts.splash == pathlib.Path('splash')
337 assert opts.pcrpkey == pathlib.Path('PATH')
338 assert opts.uname == '1.2.3'
339 assert opts.stub == pathlib.Path('STUBPATH')
340 assert opts.pcr_private_keys == [pathlib.Path('PKEY1'),
341 pathlib.Path('some/path7')]
342 assert opts.pcr_public_keys == [pathlib.Path('PKEY2'),
343 pathlib.Path('some/path8')]
344 assert opts.pcr_banks == ['SHA1', 'SHA256']
345 assert opts.signing_engine == 'ENGINE'
346 assert opts.signtool == 'sbsign' # from args
347 assert opts.sb_key == 'SBKEY' # from args
348 assert opts.sb_cert == 'SBCERT' # from args
349 assert opts.sb_certdir == 'some/path5' # from config
350 assert opts.sb_cert_name == 'some/name1' # from config
351 assert opts.sign_kernel is False
352 assert opts.tools == [pathlib.Path('TOOLZ/')]
353 assert opts.output == pathlib.Path('OUTPUT')
354 assert opts.measure is True
355
356 def test_help(capsys):
357 with pytest.raises(SystemExit):
358 ukify.parse_args(['--help'])
359 out = capsys.readouterr()
360 assert '--section' in out.out
361 assert not out.err
362
363 def test_help_display(capsys):
364 with pytest.raises(SystemExit):
365 ukify.parse_args(['inspect', '--help'])
366 out = capsys.readouterr()
367 assert '--section' in out.out
368 assert not out.err
369
370 def test_help_error_deprecated(capsys):
371 with pytest.raises(SystemExit):
372 ukify.parse_args(['a', 'b', '--no-such-option'])
373 out = capsys.readouterr()
374 assert not out.out
375 assert '--no-such-option' in out.err
376 assert len(out.err.splitlines()) == 1
377
378 def test_help_error(capsys):
379 with pytest.raises(SystemExit):
380 ukify.parse_args(['build', '--no-such-option'])
381 out = capsys.readouterr()
382 assert not out.out
383 assert '--no-such-option' in out.err
384 assert len(out.err.splitlines()) == 1
385
386 @pytest.fixture(scope='session')
387 def kernel_initrd():
388 opts = ukify.create_parser().parse_args(arg_tools)
389 bootctl = ukify.find_tool('bootctl', opts=opts)
390 if bootctl is None:
391 return None
392
393 try:
394 text = subprocess.check_output([bootctl, 'list', '--json=short'],
395 text=True)
396 except subprocess.CalledProcessError:
397 return None
398
399 items = json.loads(text)
400
401 for item in items:
402 try:
403 linux = f"{item['root']}{item['linux']}"
404 initrd = f"{item['root']}{item['initrd'][0].split(' ')[0]}"
405 except (KeyError, IndexError):
406 continue
407 return ['--linux', linux, '--initrd', initrd]
408 else:
409 return None
410
411 def test_check_splash():
412 try:
413 # pyflakes: noqa
414 import PIL # noqa
415 except ImportError:
416 pytest.skip('PIL not available')
417
418 with pytest.raises(OSError):
419 ukify.check_splash(os.devnull)
420
421 def test_basic_operation(kernel_initrd, tmp_path):
422 if kernel_initrd is None:
423 pytest.skip('linux+initrd not found')
424
425 output = f'{tmp_path}/basic.efi'
426 opts = ukify.parse_args([
427 'build',
428 *kernel_initrd,
429 f'--output={output}',
430 ])
431 try:
432 ukify.check_inputs(opts)
433 except OSError as e:
434 pytest.skip(str(e))
435
436 ukify.make_uki(opts)
437
438 # let's check that objdump likes the resulting file
439 subprocess.check_output(['objdump', '-h', output])
440
441 shutil.rmtree(tmp_path)
442
443 def test_sections(kernel_initrd, tmp_path):
444 if kernel_initrd is None:
445 pytest.skip('linux+initrd not found')
446
447 output = f'{tmp_path}/basic.efi'
448 opts = ukify.parse_args([
449 'build',
450 *kernel_initrd,
451 f'--output={output}',
452 '--uname=1.2.3',
453 '--cmdline=ARG1 ARG2 ARG3',
454 '--os-release=K1=V1\nK2=V2\n',
455 '--section=.test:CONTENTZ',
456 ])
457
458 try:
459 ukify.check_inputs(opts)
460 except OSError as e:
461 pytest.skip(str(e))
462
463 ukify.make_uki(opts)
464
465 # let's check that objdump likes the resulting file
466 dump = subprocess.check_output(['objdump', '-h', output], text=True)
467
468 for sect in 'text osrel cmdline linux initrd uname test'.split():
469 assert re.search(fr'^\s*\d+\s+\.{sect}\s+[0-9a-f]+', dump, re.MULTILINE)
470
471 shutil.rmtree(tmp_path)
472
473 def test_addon(tmp_path):
474 output = f'{tmp_path}/addon.efi'
475 args = [
476 'build',
477 f'--output={output}',
478 '--cmdline=ARG1 ARG2 ARG3',
479 """--sbat=sbat,1,foo
480 foo,1
481 bar,2
482 """,
483 '--section=.test:CONTENTZ',
484 """--sbat=sbat,1,foo
485 baz,3
486 """
487 ]
488 if stub := os.getenv('EFI_ADDON'):
489 args += [f'--stub={stub}']
490 expected_exceptions = ()
491 else:
492 expected_exceptions = (FileNotFoundError,)
493
494 opts = ukify.parse_args(args)
495 try:
496 ukify.check_inputs(opts)
497 except expected_exceptions as e:
498 pytest.skip(str(e))
499
500 ukify.make_uki(opts)
501
502 # let's check that objdump likes the resulting file
503 dump = subprocess.check_output(['objdump', '-h', output], text=True)
504
505 for sect in 'text cmdline test sbat'.split():
506 assert re.search(fr'^\s*\d+\s+\.{sect}\s+[0-9a-f]+', dump, re.MULTILINE)
507
508 pe = pefile.PE(output, fast_load=True)
509 found = False
510
511 for section in pe.sections:
512 if section.Name.rstrip(b"\x00").decode() == ".sbat":
513 assert found is False
514 split = section.get_data().rstrip(b"\x00").decode().splitlines()
515 assert split == ["sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md", "foo,1", "bar,2", "baz,3"]
516 found = True
517
518 assert found is True
519
520 def unbase64(filename):
521 tmp = tempfile.NamedTemporaryFile()
522 base64.decode(filename.open('rb'), tmp)
523 tmp.flush()
524 return tmp
525
526 def test_uname_scraping(kernel_initrd):
527 if kernel_initrd is None:
528 pytest.skip('linux+initrd not found')
529
530 assert kernel_initrd[0] == '--linux'
531 uname = ukify.Uname.scrape(kernel_initrd[1])
532 assert re.match(r'\d+\.\d+\.\d+', uname)
533
534 @pytest.mark.parametrize("days", [365*10, None])
535 def test_efi_signing_sbsign(days, kernel_initrd, tmp_path):
536 if kernel_initrd is None:
537 pytest.skip('linux+initrd not found')
538 if not shutil.which('sbsign'):
539 pytest.skip('sbsign not found')
540
541 ourdir = pathlib.Path(__file__).parent
542 cert = unbase64(ourdir / 'example.signing.crt.base64')
543 key = unbase64(ourdir / 'example.signing.key.base64')
544
545 output = f'{tmp_path}/signed.efi'
546 args = [
547 'build',
548 *kernel_initrd,
549 f'--output={output}',
550 '--uname=1.2.3',
551 '--cmdline=ARG1 ARG2 ARG3',
552 f'--secureboot-certificate={cert.name}',
553 f'--secureboot-private-key={key.name}',
554 ]
555 if days is not None:
556 args += [f'--secureboot-certificate-validity={days}']
557
558 opts = ukify.parse_args(args)
559
560 try:
561 ukify.check_inputs(opts)
562 except OSError as e:
563 pytest.skip(str(e))
564
565 ukify.make_uki(opts)
566
567 if shutil.which('sbverify'):
568 # let's check that sbverify likes the resulting file
569 dump = subprocess.check_output([
570 'sbverify',
571 '--cert', cert.name,
572 output,
573 ], text=True)
574
575 assert 'Signature verification OK' in dump
576
577 shutil.rmtree(tmp_path)
578
579 def test_efi_signing_pesign(kernel_initrd, tmp_path):
580 if kernel_initrd is None:
581 pytest.skip('linux+initrd not found')
582 if not shutil.which('pesign'):
583 pytest.skip('pesign not found')
584
585 nss_db = f'{tmp_path}/nss_db'
586 name = 'Test_Secureboot'
587 author = 'systemd'
588
589 subprocess.check_call(['mkdir', '-p', nss_db])
590 cmd = f'certutil -N --empty-password -d {nss_db}'.split(' ')
591 subprocess.check_call(cmd)
592 cmd = f'efikeygen -d {nss_db} -S -k -c CN={author} -n {name}'.split(' ')
593 subprocess.check_call(cmd)
594
595 output = f'{tmp_path}/signed.efi'
596 opts = ukify.parse_args([
597 'build',
598 *kernel_initrd,
599 f'--output={output}',
600 '--uname=1.2.3',
601 '--signtool=pesign',
602 '--cmdline=ARG1 ARG2 ARG3',
603 f'--secureboot-certificate-name={name}',
604 f'--secureboot-certificate-dir={nss_db}',
605 ])
606
607 try:
608 ukify.check_inputs(opts)
609 except OSError as e:
610 pytest.skip(str(e))
611
612 ukify.make_uki(opts)
613
614 # let's check that sbverify likes the resulting file
615 dump = subprocess.check_output([
616 'pesign', '-S',
617 '-i', output,
618 ], text=True)
619
620 assert f"The signer's common name is {author}" in dump
621
622 shutil.rmtree(tmp_path)
623
624 def test_inspect(kernel_initrd, tmp_path, capsys):
625 if kernel_initrd is None:
626 pytest.skip('linux+initrd not found')
627 if not shutil.which('sbsign'):
628 pytest.skip('sbsign not found')
629
630 ourdir = pathlib.Path(__file__).parent
631 cert = unbase64(ourdir / 'example.signing.crt.base64')
632 key = unbase64(ourdir / 'example.signing.key.base64')
633
634 output = f'{tmp_path}/signed2.efi'
635 uname_arg='1.2.3'
636 osrel_arg='Linux'
637 cmdline_arg='ARG1 ARG2 ARG3'
638 opts = ukify.parse_args([
639 'build',
640 *kernel_initrd,
641 f'--cmdline={cmdline_arg}',
642 f'--os-release={osrel_arg}',
643 f'--uname={uname_arg}',
644 f'--output={output}',
645 f'--secureboot-certificate={cert.name}',
646 f'--secureboot-private-key={key.name}',
647 ])
648
649 ukify.check_inputs(opts)
650 ukify.make_uki(opts)
651
652 opts = ukify.parse_args(['inspect', output])
653 ukify.inspect_sections(opts)
654
655 text = capsys.readouterr().out
656
657 expected_osrel = f'.osrel:\n size: {len(osrel_arg)}'
658 assert expected_osrel in text
659 expected_cmdline = f'.cmdline:\n size: {len(cmdline_arg)}'
660 assert expected_cmdline in text
661 expected_uname = f'.uname:\n size: {len(uname_arg)}'
662 assert expected_uname in text
663
664 expected_initrd = '.initrd:\n size:'
665 assert expected_initrd in text
666 expected_linux = '.linux:\n size:'
667 assert expected_linux in text
668
669 shutil.rmtree(tmp_path)
670
671 def test_pcr_signing(kernel_initrd, tmp_path):
672 if kernel_initrd is None:
673 pytest.skip('linux+initrd not found')
674 if systemd_measure() is None:
675 pytest.skip('systemd-measure not found')
676
677 ourdir = pathlib.Path(__file__).parent
678 pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
679 priv = unbase64(ourdir / 'example.tpm2-pcr-private.pem.base64')
680
681 output = f'{tmp_path}/signed.efi'
682 args = [
683 'build',
684 *kernel_initrd,
685 f'--output={output}',
686 '--uname=1.2.3',
687 '--cmdline=ARG1 ARG2 ARG3',
688 '--os-release=ID=foobar\n',
689 '--pcr-banks=sha1', # use sha1 because it doesn't really matter
690 f'--pcr-private-key={priv.name}',
691 ] + arg_tools
692
693 # If the public key is not explicitly specified, it is derived
694 # automatically. Let's make sure everything works as expected both when the
695 # public keys is specified explicitly and when it is derived from the
696 # private key.
697 for extra in ([f'--pcrpkey={pub.name}', f'--pcr-public-key={pub.name}'], []):
698 opts = ukify.parse_args(args + extra)
699 try:
700 ukify.check_inputs(opts)
701 except OSError as e:
702 pytest.skip(str(e))
703
704 ukify.make_uki(opts)
705
706 # let's check that objdump likes the resulting file
707 dump = subprocess.check_output(['objdump', '-h', output], text=True)
708
709 for sect in 'text osrel cmdline linux initrd uname pcrsig'.split():
710 assert re.search(fr'^\s*\d+\s+\.{sect}\s+[0-9a-f]+', dump, re.MULTILINE)
711
712 # objcopy fails when called without an output argument (EPERM).
713 # It also fails when called with /dev/null (file truncated).
714 # It also fails when called with /dev/zero (because it reads the
715 # output file, infinitely in this case.)
716 # So let's just call it with a dummy output argument.
717 subprocess.check_call([
718 'objcopy',
719 *(f'--dump-section=.{n}={tmp_path}/out.{n}' for n in (
720 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline')),
721 output,
722 tmp_path / 'dummy',
723 ],
724 text=True)
725
726 assert open(tmp_path / 'out.pcrpkey').read() == open(pub.name).read()
727 assert open(tmp_path / 'out.osrel').read() == 'ID=foobar\n'
728 assert open(tmp_path / 'out.uname').read() == '1.2.3'
729 assert open(tmp_path / 'out.cmdline').read() == 'ARG1 ARG2 ARG3'
730 sig = open(tmp_path / 'out.pcrsig').read()
731 sig = json.loads(sig)
732 assert list(sig.keys()) == ['sha1']
733 assert len(sig['sha1']) == 4 # four items for four phases
734
735 shutil.rmtree(tmp_path)
736
737 def test_pcr_signing2(kernel_initrd, tmp_path):
738 if kernel_initrd is None:
739 pytest.skip('linux+initrd not found')
740 if systemd_measure() is None:
741 pytest.skip('systemd-measure not found')
742
743 ourdir = pathlib.Path(__file__).parent
744 pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
745 priv = unbase64(ourdir / 'example.tpm2-pcr-private.pem.base64')
746 pub2 = unbase64(ourdir / 'example.tpm2-pcr-public2.pem.base64')
747 priv2 = unbase64(ourdir / 'example.tpm2-pcr-private2.pem.base64')
748
749 # simulate a microcode file
750 with open(f'{tmp_path}/microcode', 'wb') as microcode:
751 microcode.write(b'1234567890')
752
753 output = f'{tmp_path}/signed.efi'
754 assert kernel_initrd[0] == '--linux'
755 opts = ukify.parse_args([
756 'build',
757 *kernel_initrd[:2],
758 f'--initrd={microcode.name}',
759 *kernel_initrd[2:],
760 f'--output={output}',
761 '--uname=1.2.3',
762 '--cmdline=ARG1 ARG2 ARG3',
763 '--os-release=ID=foobar\n',
764 '--pcr-banks=sha1',
765 f'--pcrpkey={pub2.name}',
766 f'--pcr-public-key={pub.name}',
767 f'--pcr-private-key={priv.name}',
768 '--phases=enter-initrd enter-initrd:leave-initrd',
769 f'--pcr-public-key={pub2.name}',
770 f'--pcr-private-key={priv2.name}',
771 '--phases=sysinit ready shutdown final', # yes, those phase paths are not reachable
772 ] + arg_tools)
773
774 try:
775 ukify.check_inputs(opts)
776 except OSError as e:
777 pytest.skip(str(e))
778
779 ukify.make_uki(opts)
780
781 # let's check that objdump likes the resulting file
782 dump = subprocess.check_output(['objdump', '-h', output], text=True)
783
784 for sect in 'text osrel cmdline linux initrd uname pcrsig'.split():
785 assert re.search(fr'^\s*\d+\s+\.{sect}\s+[0-9a-f]+', dump, re.MULTILINE)
786
787 subprocess.check_call([
788 'objcopy',
789 *(f'--dump-section=.{n}={tmp_path}/out.{n}' for n in (
790 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline', 'initrd')),
791 output,
792 tmp_path / 'dummy',
793 ],
794 text=True)
795
796 assert open(tmp_path / 'out.pcrpkey').read() == open(pub2.name).read()
797 assert open(tmp_path / 'out.osrel').read() == 'ID=foobar\n'
798 assert open(tmp_path / 'out.uname').read() == '1.2.3'
799 assert open(tmp_path / 'out.cmdline').read() == 'ARG1 ARG2 ARG3'
800 assert open(tmp_path / 'out.initrd', 'rb').read(10) == b'1234567890'
801
802 sig = open(tmp_path / 'out.pcrsig').read()
803 sig = json.loads(sig)
804 assert list(sig.keys()) == ['sha1']
805 assert len(sig['sha1']) == 6 # six items for six phases paths
806
807 shutil.rmtree(tmp_path)
808
809 def test_key_cert_generation(tmp_path):
810 opts = ukify.parse_args([
811 'genkey',
812 f"--pcr-public-key={tmp_path / 'pcr1.pub.pem'}",
813 f"--pcr-private-key={tmp_path / 'pcr1.priv.pem'}",
814 '--phases=enter-initrd enter-initrd:leave-initrd',
815 f"--pcr-public-key={tmp_path / 'pcr2.pub.pem'}",
816 f"--pcr-private-key={tmp_path / 'pcr2.priv.pem'}",
817 '--phases=sysinit ready',
818 f"--secureboot-private-key={tmp_path / 'sb.priv.pem'}",
819 f"--secureboot-certificate={tmp_path / 'sb.cert.pem'}",
820 ])
821 assert opts.verb == 'genkey'
822 ukify.check_cert_and_keys_nonexistent(opts)
823
824 pytest.importorskip('cryptography')
825
826 ukify.generate_keys(opts)
827
828 if not shutil.which('openssl'):
829 return
830
831 for key in (tmp_path / 'pcr1.priv.pem',
832 tmp_path / 'pcr2.priv.pem',
833 tmp_path / 'sb.priv.pem'):
834 out = subprocess.check_output([
835 'openssl', 'rsa',
836 '-in', key,
837 '-text',
838 '-noout',
839 ], text = True)
840 assert 'Private-Key' in out
841 assert '2048 bit' in out
842
843 for pub in (tmp_path / 'pcr1.pub.pem',
844 tmp_path / 'pcr2.pub.pem'):
845 out = subprocess.check_output([
846 'openssl', 'rsa',
847 '-pubin',
848 '-in', pub,
849 '-text',
850 '-noout',
851 ], text = True)
852 assert 'Public-Key' in out
853 assert '2048 bit' in out
854
855 out = subprocess.check_output([
856 'openssl', 'x509',
857 '-in', tmp_path / 'sb.cert.pem',
858 '-text',
859 '-noout',
860 ], text = True)
861 assert 'Certificate' in out
862 assert 'Issuer: CN = SecureBoot signing key on host' in out
863
864 if __name__ == '__main__':
865 sys.exit(pytest.main(sys.argv))