summaryrefslogtreecommitdiffstats
path: root/src/ukify/test/test_ukify.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/ukify/test/test_ukify.py')
-rwxr-xr-xsrc/ukify/test/test_ukify.py876
1 files changed, 876 insertions, 0 deletions
diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py
new file mode 100755
index 0000000..5866447
--- /dev/null
+++ b/src/ukify/test/test_ukify.py
@@ -0,0 +1,876 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+# pylint: disable=unused-import,import-outside-toplevel,useless-else-on-loop
+# pylint: disable=consider-using-with,wrong-import-position,unspecified-encoding
+# pylint: disable=protected-access,redefined-outer-name
+
+import base64
+import json
+import os
+import pathlib
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import textwrap
+
+try:
+ import pytest
+except ImportError as e:
+ print(str(e), file=sys.stderr)
+ sys.exit(77)
+
+try:
+ # pyflakes: noqa
+ import pefile # noqa
+except ImportError as e:
+ print(str(e), file=sys.stderr)
+ sys.exit(77)
+
+# We import ukify.py, which is a template file. But only __version__ is
+# substituted, which we don't care about here. Having the .py suffix makes it
+# easier to import the file.
+sys.path.append(os.path.dirname(__file__) + '/..')
+import ukify
+
+build_root = os.getenv('PROJECT_BUILD_ROOT')
+try:
+ slow_tests = bool(int(os.getenv('SYSTEMD_SLOW_TESTS', '1')))
+except ValueError:
+ slow_tests = True
+
+arg_tools = ['--tools', build_root] if build_root else []
+
+def systemd_measure():
+ opts = ukify.create_parser().parse_args(arg_tools)
+ return ukify.find_tool('systemd-measure', opts=opts)
+
+def test_guess_efi_arch():
+ arch = ukify.guess_efi_arch()
+ assert arch in ukify.EFI_ARCHES
+
+def test_shell_join():
+ assert ukify.shell_join(['a', 'b', ' ']) == "a b ' '"
+
+def test_round_up():
+ assert ukify.round_up(0) == 0
+ assert ukify.round_up(4095) == 4096
+ assert ukify.round_up(4096) == 4096
+ assert ukify.round_up(4097) == 8192
+
+def test_namespace_creation():
+ ns = ukify.create_parser().parse_args(())
+ assert ns.linux is None
+ assert ns.initrd is None
+
+def test_config_example():
+ ex = ukify.config_example()
+ assert '[UKI]' in ex
+ assert 'Splash = BMP' in ex
+
+def test_apply_config(tmp_path):
+ config = tmp_path / 'config1.conf'
+ config.write_text(textwrap.dedent(
+ f'''
+ [UKI]
+ Linux = LINUX
+ Initrd = initrd1 initrd2
+ initrd3
+ Cmdline = 1 2 3 4 5
+ 6 7 8
+ OSRelease = @some/path1
+ DeviceTree = some/path2
+ Splash = some/path3
+ Uname = 1.2.3
+ EFIArch=arm
+ Stub = some/path4
+ PCRBanks = sha512,sha1
+ SigningEngine = engine1
+ SecureBootPrivateKey = some/path5
+ SecureBootCertificate = some/path6
+ SignKernel = no
+
+ [PCRSignature:NAME]
+ PCRPrivateKey = some/path7
+ PCRPublicKey = some/path8
+ Phases = {':'.join(ukify.KNOWN_PHASES)}
+ '''))
+
+ ns = ukify.create_parser().parse_args(['build'])
+ ns.linux = None
+ ns.initrd = []
+ ukify.apply_config(ns, config)
+
+ assert ns.linux == pathlib.Path('LINUX')
+ assert ns.initrd == [pathlib.Path('initrd1'),
+ pathlib.Path('initrd2'),
+ pathlib.Path('initrd3')]
+ assert ns.cmdline == '1 2 3 4 5\n6 7 8'
+ assert ns.os_release == '@some/path1'
+ assert ns.devicetree == pathlib.Path('some/path2')
+ assert ns.splash == pathlib.Path('some/path3')
+ assert ns.efi_arch == 'arm'
+ assert ns.stub == pathlib.Path('some/path4')
+ assert ns.pcr_banks == ['sha512', 'sha1']
+ assert ns.signing_engine == 'engine1'
+ assert ns.sb_key == 'some/path5'
+ assert ns.sb_cert == 'some/path6'
+ assert ns.sign_kernel is False
+
+ assert ns._groups == ['NAME']
+ assert ns.pcr_private_keys == [pathlib.Path('some/path7')]
+ assert ns.pcr_public_keys == [pathlib.Path('some/path8')]
+ assert ns.phase_path_groups == [['enter-initrd:leave-initrd:sysinit:ready:shutdown:final']]
+
+ ukify.finalize_options(ns)
+
+ assert ns.linux == pathlib.Path('LINUX')
+ assert ns.initrd == [pathlib.Path('initrd1'),
+ pathlib.Path('initrd2'),
+ pathlib.Path('initrd3')]
+ assert ns.cmdline == '1 2 3 4 5 6 7 8'
+ assert ns.os_release == pathlib.Path('some/path1')
+ assert ns.devicetree == pathlib.Path('some/path2')
+ assert ns.splash == pathlib.Path('some/path3')
+ assert ns.efi_arch == 'arm'
+ assert ns.stub == pathlib.Path('some/path4')
+ assert ns.pcr_banks == ['sha512', 'sha1']
+ assert ns.signing_engine == 'engine1'
+ assert ns.sb_key == 'some/path5'
+ assert ns.sb_cert == 'some/path6'
+ assert ns.sign_kernel is False
+
+ assert ns._groups == ['NAME']
+ assert ns.pcr_private_keys == [pathlib.Path('some/path7')]
+ assert ns.pcr_public_keys == [pathlib.Path('some/path8')]
+ assert ns.phase_path_groups == [['enter-initrd:leave-initrd:sysinit:ready:shutdown:final']]
+
+def test_parse_args_minimal():
+ with pytest.raises(ValueError):
+ ukify.parse_args([])
+
+ opts = ukify.parse_args('arg1 arg2'.split())
+ assert opts.linux == pathlib.Path('arg1')
+ assert opts.initrd == [pathlib.Path('arg2')]
+ assert opts.os_release in (pathlib.Path('/etc/os-release'),
+ pathlib.Path('/usr/lib/os-release'))
+
+def test_parse_args_many_deprecated():
+ opts = ukify.parse_args(
+ ['/ARG1', '///ARG2', '/ARG3 WITH SPACE',
+ '--cmdline=a b c',
+ '--os-release=K1=V1\nK2=V2',
+ '--devicetree=DDDDTTTT',
+ '--splash=splash',
+ '--pcrpkey=PATH',
+ '--uname=1.2.3',
+ '--stub=STUBPATH',
+ '--pcr-private-key=PKEY1',
+ '--pcr-public-key=PKEY2',
+ '--pcr-banks=SHA1,SHA256',
+ '--signing-engine=ENGINE',
+ '--secureboot-private-key=SBKEY',
+ '--secureboot-certificate=SBCERT',
+ '--sign-kernel',
+ '--no-sign-kernel',
+ '--tools=TOOLZ///',
+ '--output=OUTPUT',
+ '--measure',
+ '--no-measure',
+ ])
+ assert opts.linux == pathlib.Path('/ARG1')
+ assert opts.initrd == [pathlib.Path('/ARG2'), pathlib.Path('/ARG3 WITH SPACE')]
+ assert opts.cmdline == 'a b c'
+ assert opts.os_release == 'K1=V1\nK2=V2'
+ assert opts.devicetree == pathlib.Path('DDDDTTTT')
+ assert opts.splash == pathlib.Path('splash')
+ assert opts.pcrpkey == pathlib.Path('PATH')
+ assert opts.uname == '1.2.3'
+ assert opts.stub == pathlib.Path('STUBPATH')
+ assert opts.pcr_private_keys == [pathlib.Path('PKEY1')]
+ assert opts.pcr_public_keys == [pathlib.Path('PKEY2')]
+ assert opts.pcr_banks == ['SHA1', 'SHA256']
+ assert opts.signing_engine == 'ENGINE'
+ assert opts.sb_key == 'SBKEY'
+ assert opts.sb_cert == 'SBCERT'
+ assert opts.sign_kernel is False
+ assert opts.tools == [pathlib.Path('TOOLZ/')]
+ assert opts.output == pathlib.Path('OUTPUT')
+ assert opts.measure is False
+
+def test_parse_args_many():
+ opts = ukify.parse_args(
+ ['build',
+ '--linux=/ARG1',
+ '--initrd=///ARG2',
+ '--initrd=/ARG3 WITH SPACE',
+ '--cmdline=a b c',
+ '--os-release=K1=V1\nK2=V2',
+ '--devicetree=DDDDTTTT',
+ '--splash=splash',
+ '--pcrpkey=PATH',
+ '--uname=1.2.3',
+ '--stub=STUBPATH',
+ '--pcr-private-key=PKEY1',
+ '--pcr-public-key=PKEY2',
+ '--pcr-banks=SHA1,SHA256',
+ '--signing-engine=ENGINE',
+ '--secureboot-private-key=SBKEY',
+ '--secureboot-certificate=SBCERT',
+ '--sign-kernel',
+ '--no-sign-kernel',
+ '--tools=TOOLZ///',
+ '--output=OUTPUT',
+ '--measure',
+ '--no-measure',
+ ])
+ assert opts.linux == pathlib.Path('/ARG1')
+ assert opts.initrd == [pathlib.Path('/ARG2'), pathlib.Path('/ARG3 WITH SPACE')]
+ assert opts.cmdline == 'a b c'
+ assert opts.os_release == 'K1=V1\nK2=V2'
+ assert opts.devicetree == pathlib.Path('DDDDTTTT')
+ assert opts.splash == pathlib.Path('splash')
+ assert opts.pcrpkey == pathlib.Path('PATH')
+ assert opts.uname == '1.2.3'
+ assert opts.stub == pathlib.Path('STUBPATH')
+ assert opts.pcr_private_keys == [pathlib.Path('PKEY1')]
+ assert opts.pcr_public_keys == [pathlib.Path('PKEY2')]
+ assert opts.pcr_banks == ['SHA1', 'SHA256']
+ assert opts.signing_engine == 'ENGINE'
+ assert opts.sb_key == 'SBKEY'
+ assert opts.sb_cert == 'SBCERT'
+ assert opts.sign_kernel is False
+ assert opts.tools == [pathlib.Path('TOOLZ/')]
+ assert opts.output == pathlib.Path('OUTPUT')
+ assert opts.measure is False
+
+def test_parse_sections():
+ opts = ukify.parse_args(
+ ['build',
+ '--linux=/ARG1',
+ '--initrd=/ARG2',
+ '--section=test:TESTTESTTEST',
+ '--section=test2:@FILE',
+ ])
+
+ assert opts.linux == pathlib.Path('/ARG1')
+ assert opts.initrd == [pathlib.Path('/ARG2')]
+ assert len(opts.sections) == 2
+
+ assert opts.sections[0].name == 'test'
+ assert isinstance(opts.sections[0].content, pathlib.Path)
+ assert opts.sections[0].tmpfile
+ assert opts.sections[0].measure is False
+
+ assert opts.sections[1].name == 'test2'
+ assert opts.sections[1].content == pathlib.Path('FILE')
+ assert opts.sections[1].tmpfile is None
+ assert opts.sections[1].measure is False
+
+def test_config_priority(tmp_path):
+ config = tmp_path / 'config1.conf'
+ # config: use pesign and give certdir + certname
+ config.write_text(textwrap.dedent(
+ f'''
+ [UKI]
+ Linux = LINUX
+ Initrd = initrd1 initrd2
+ initrd3
+ Cmdline = 1 2 3 4 5
+ 6 7 8
+ OSRelease = @some/path1
+ DeviceTree = some/path2
+ Splash = some/path3
+ Uname = 1.2.3
+ EFIArch = arm
+ Stub = some/path4
+ PCRBanks = sha512,sha1
+ SigningEngine = engine1
+ SecureBootSigningTool = pesign
+ SecureBootCertificateDir = some/path5
+ SecureBootCertificateName = some/name1
+ SignKernel = no
+
+ [PCRSignature:NAME]
+ PCRPrivateKey = some/path7
+ PCRPublicKey = some/path8
+ Phases = {':'.join(ukify.KNOWN_PHASES)}
+ '''))
+
+ # args: use sbsign and give key + cert, should override pesign
+ opts = ukify.parse_args(
+ ['build',
+ '--linux=/ARG1',
+ '--initrd=///ARG2',
+ '--initrd=/ARG3 WITH SPACE',
+ '--cmdline= a b c ',
+ '--os-release=K1=V1\nK2=V2',
+ '--devicetree=DDDDTTTT',
+ '--splash=splash',
+ '--pcrpkey=PATH',
+ '--uname=1.2.3',
+ '--stub=STUBPATH',
+ '--pcr-private-key=PKEY1',
+ '--pcr-public-key=PKEY2',
+ '--pcr-banks=SHA1,SHA256',
+ '--signing-engine=ENGINE',
+ '--signtool=sbsign',
+ '--secureboot-private-key=SBKEY',
+ '--secureboot-certificate=SBCERT',
+ '--sign-kernel',
+ '--no-sign-kernel',
+ '--tools=TOOLZ///',
+ '--output=OUTPUT',
+ '--measure',
+ ])
+
+ ukify.apply_config(opts, config)
+ ukify.finalize_options(opts)
+
+ assert opts.linux == pathlib.Path('/ARG1')
+ assert opts.initrd == [pathlib.Path('initrd1'),
+ pathlib.Path('initrd2'),
+ pathlib.Path('initrd3'),
+ pathlib.Path('/ARG2'),
+ pathlib.Path('/ARG3 WITH SPACE')]
+ assert opts.cmdline == 'a b c'
+ assert opts.os_release == 'K1=V1\nK2=V2'
+ assert opts.devicetree == pathlib.Path('DDDDTTTT')
+ assert opts.splash == pathlib.Path('splash')
+ assert opts.pcrpkey == pathlib.Path('PATH')
+ assert opts.uname == '1.2.3'
+ assert opts.stub == pathlib.Path('STUBPATH')
+ assert opts.pcr_private_keys == [pathlib.Path('PKEY1'),
+ pathlib.Path('some/path7')]
+ assert opts.pcr_public_keys == [pathlib.Path('PKEY2'),
+ pathlib.Path('some/path8')]
+ assert opts.pcr_banks == ['SHA1', 'SHA256']
+ assert opts.signing_engine == 'ENGINE'
+ assert opts.signtool == 'sbsign' # from args
+ assert opts.sb_key == 'SBKEY' # from args
+ assert opts.sb_cert == 'SBCERT' # from args
+ assert opts.sb_certdir == 'some/path5' # from config
+ assert opts.sb_cert_name == 'some/name1' # from config
+ assert opts.sign_kernel is False
+ assert opts.tools == [pathlib.Path('TOOLZ/')]
+ assert opts.output == pathlib.Path('OUTPUT')
+ assert opts.measure is True
+
+def test_help(capsys):
+ with pytest.raises(SystemExit):
+ ukify.parse_args(['--help'])
+ out = capsys.readouterr()
+ assert '--section' in out.out
+ assert not out.err
+
+def test_help_display(capsys):
+ with pytest.raises(SystemExit):
+ ukify.parse_args(['inspect', '--help'])
+ out = capsys.readouterr()
+ assert '--section' in out.out
+ assert not out.err
+
+def test_help_error_deprecated(capsys):
+ with pytest.raises(SystemExit):
+ ukify.parse_args(['a', 'b', '--no-such-option'])
+ out = capsys.readouterr()
+ assert not out.out
+ assert '--no-such-option' in out.err
+ assert len(out.err.splitlines()) == 1
+
+def test_help_error(capsys):
+ with pytest.raises(SystemExit):
+ ukify.parse_args(['build', '--no-such-option'])
+ out = capsys.readouterr()
+ assert not out.out
+ assert '--no-such-option' in out.err
+ assert len(out.err.splitlines()) == 1
+
+@pytest.fixture(scope='session')
+def kernel_initrd():
+ opts = ukify.create_parser().parse_args(arg_tools)
+ bootctl = ukify.find_tool('bootctl', opts=opts)
+ if bootctl is None:
+ return None
+
+ try:
+ text = subprocess.check_output([bootctl, 'list', '--json=short'],
+ text=True)
+ except subprocess.CalledProcessError:
+ return None
+
+ items = json.loads(text)
+
+ for item in items:
+ try:
+ linux = f"{item['root']}{item['linux']}"
+ initrd = f"{item['root']}{item['initrd'][0].split(' ')[0]}"
+ except (KeyError, IndexError):
+ continue
+ return ['--linux', linux, '--initrd', initrd]
+ else:
+ return None
+
+def test_check_splash():
+ try:
+ # pyflakes: noqa
+ import PIL # noqa
+ except ImportError:
+ pytest.skip('PIL not available')
+
+ with pytest.raises(OSError):
+ ukify.check_splash(os.devnull)
+
+def test_basic_operation(kernel_initrd, tmp_path):
+ if kernel_initrd is None:
+ pytest.skip('linux+initrd not found')
+
+ output = f'{tmp_path}/basic.efi'
+ opts = ukify.parse_args([
+ 'build',
+ *kernel_initrd,
+ f'--output={output}',
+ ])
+ try:
+ ukify.check_inputs(opts)
+ except OSError as e:
+ pytest.skip(str(e))
+
+ ukify.make_uki(opts)
+
+ # let's check that objdump likes the resulting file
+ subprocess.check_output(['objdump', '-h', output])
+
+ shutil.rmtree(tmp_path)
+
+def test_sections(kernel_initrd, tmp_path):
+ if kernel_initrd is None:
+ pytest.skip('linux+initrd not found')
+
+ output = f'{tmp_path}/basic.efi'
+ opts = ukify.parse_args([
+ 'build',
+ *kernel_initrd,
+ f'--output={output}',
+ '--uname=1.2.3',
+ '--cmdline=ARG1 ARG2 ARG3',
+ '--os-release=K1=V1\nK2=V2\n',
+ '--section=.test:CONTENTZ',
+ ])
+
+ try:
+ ukify.check_inputs(opts)
+ except OSError as e:
+ pytest.skip(str(e))
+
+ ukify.make_uki(opts)
+
+ # let's check that objdump likes the resulting file
+ dump = subprocess.check_output(['objdump', '-h', output], text=True)
+
+ for sect in 'text osrel cmdline linux initrd uname test'.split():
+ assert re.search(fr'^\s*\d+\s+\.{sect}\s+[0-9a-f]+', dump, re.MULTILINE)
+
+ shutil.rmtree(tmp_path)
+
+def test_addon(tmp_path):
+ output = f'{tmp_path}/addon.efi'
+ args = [
+ 'build',
+ f'--output={output}',
+ '--cmdline=ARG1 ARG2 ARG3',
+ """--sbat=sbat,1,foo
+foo,1
+bar,2
+""",
+ '--section=.test:CONTENTZ',
+ """--sbat=sbat,1,foo
+baz,3
+"""
+ ]
+ if stub := os.getenv('EFI_ADDON'):
+ args += [f'--stub={stub}']
+ expected_exceptions = ()
+ else:
+ expected_exceptions = (FileNotFoundError,)
+
+ opts = ukify.parse_args(args)
+ try:
+ ukify.check_inputs(opts)
+ except expected_exceptions as e:
+ pytest.skip(str(e))
+
+ ukify.make_uki(opts)
+
+ # let's check that objdump likes the resulting file
+ dump = subprocess.check_output(['objdump', '-h', output], text=True)
+
+ for sect in 'text cmdline test sbat'.split():
+ assert re.search(fr'^\s*\d+\s+\.{sect}\s+[0-9a-f]+', dump, re.MULTILINE)
+
+ pe = pefile.PE(output, fast_load=True)
+ found = False
+
+ for section in pe.sections:
+ if section.Name.rstrip(b"\x00").decode() == ".sbat":
+ assert found is False
+ split = section.get_data().rstrip(b"\x00").decode().splitlines()
+ assert split == ["sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md", "foo,1", "bar,2", "baz,3"]
+ found = True
+
+ assert found is True
+
+
+def unbase64(filename):
+ tmp = tempfile.NamedTemporaryFile()
+ base64.decode(filename.open('rb'), tmp)
+ tmp.flush()
+ return tmp
+
+
+def test_uname_scraping(kernel_initrd):
+ if kernel_initrd is None:
+ pytest.skip('linux+initrd not found')
+
+ assert kernel_initrd[0] == '--linux'
+ uname = ukify.Uname.scrape(kernel_initrd[1])
+ assert re.match(r'\d+\.\d+\.\d+', uname)
+
+@pytest.mark.skipif(not slow_tests, reason='slow')
+def test_efi_signing_sbsign(kernel_initrd, tmp_path):
+ if kernel_initrd is None:
+ pytest.skip('linux+initrd not found')
+ if not shutil.which('sbsign'):
+ pytest.skip('sbsign not found')
+
+ ourdir = pathlib.Path(__file__).parent
+ cert = unbase64(ourdir / 'example.signing.crt.base64')
+ key = unbase64(ourdir / 'example.signing.key.base64')
+
+ output = f'{tmp_path}/signed.efi'
+ opts = ukify.parse_args([
+ 'build',
+ *kernel_initrd,
+ f'--output={output}',
+ '--uname=1.2.3',
+ '--cmdline=ARG1 ARG2 ARG3',
+ f'--secureboot-certificate={cert.name}',
+ f'--secureboot-private-key={key.name}',
+ ])
+
+ try:
+ ukify.check_inputs(opts)
+ except OSError as e:
+ pytest.skip(str(e))
+
+ ukify.make_uki(opts)
+
+ if shutil.which('sbverify'):
+ # let's check that sbverify likes the resulting file
+ dump = subprocess.check_output([
+ 'sbverify',
+ '--cert', cert.name,
+ output,
+ ], text=True)
+
+ assert 'Signature verification OK' in dump
+
+ shutil.rmtree(tmp_path)
+
+@pytest.mark.skipif(not slow_tests, reason='slow')
+def test_efi_signing_pesign(kernel_initrd, tmp_path):
+ if kernel_initrd is None:
+ pytest.skip('linux+initrd not found')
+ if not shutil.which('pesign'):
+ pytest.skip('pesign not found')
+
+ nss_db = f'{tmp_path}/nss_db'
+ name = 'Test_Secureboot'
+ author = 'systemd'
+
+ subprocess.check_call(['mkdir', '-p', nss_db])
+ cmd = f'certutil -N --empty-password -d {nss_db}'.split(' ')
+ subprocess.check_call(cmd)
+ cmd = f'efikeygen -d {nss_db} -S -k -c CN={author} -n {name}'.split(' ')
+ subprocess.check_call(cmd)
+
+ output = f'{tmp_path}/signed.efi'
+ opts = ukify.parse_args([
+ 'build',
+ *kernel_initrd,
+ f'--output={output}',
+ '--uname=1.2.3',
+ '--signtool=pesign',
+ '--cmdline=ARG1 ARG2 ARG3',
+ f'--secureboot-certificate-name={name}',
+ f'--secureboot-certificate-dir={nss_db}',
+ ])
+
+ try:
+ ukify.check_inputs(opts)
+ except OSError as e:
+ pytest.skip(str(e))
+
+ ukify.make_uki(opts)
+
+ # let's check that sbverify likes the resulting file
+ dump = subprocess.check_output([
+ 'pesign', '-S',
+ '-i', output,
+ ], text=True)
+
+ assert f"The signer's common name is {author}" in dump
+
+ shutil.rmtree(tmp_path)
+
+def test_inspect(kernel_initrd, tmp_path, capsys):
+ if kernel_initrd is None:
+ pytest.skip('linux+initrd not found')
+ if not shutil.which('sbsign'):
+ pytest.skip('sbsign not found')
+
+ ourdir = pathlib.Path(__file__).parent
+ cert = unbase64(ourdir / 'example.signing.crt.base64')
+ key = unbase64(ourdir / 'example.signing.key.base64')
+
+ output = f'{tmp_path}/signed2.efi'
+ uname_arg='1.2.3'
+ osrel_arg='Linux'
+ cmdline_arg='ARG1 ARG2 ARG3'
+
+ args = [
+ 'build',
+ *kernel_initrd,
+ f'--cmdline={cmdline_arg}',
+ f'--os-release={osrel_arg}',
+ f'--uname={uname_arg}',
+ f'--output={output}',
+ ]
+ if slow_tests:
+ args += [
+ f'--secureboot-certificate={cert.name}',
+ f'--secureboot-private-key={key.name}',
+ ]
+
+ opts = ukify.parse_args(args)
+
+ ukify.check_inputs(opts)
+ ukify.make_uki(opts)
+
+ opts = ukify.parse_args(['inspect', output])
+ ukify.inspect_sections(opts)
+
+ text = capsys.readouterr().out
+
+ expected_osrel = f'.osrel:\n size: {len(osrel_arg)}'
+ assert expected_osrel in text
+ expected_cmdline = f'.cmdline:\n size: {len(cmdline_arg)}'
+ assert expected_cmdline in text
+ expected_uname = f'.uname:\n size: {len(uname_arg)}'
+ assert expected_uname in text
+
+ expected_initrd = '.initrd:\n size:'
+ assert expected_initrd in text
+ expected_linux = '.linux:\n size:'
+ assert expected_linux in text
+
+ shutil.rmtree(tmp_path)
+
+@pytest.mark.skipif(not slow_tests, reason='slow')
+def test_pcr_signing(kernel_initrd, tmp_path):
+ if kernel_initrd is None:
+ pytest.skip('linux+initrd not found')
+ if systemd_measure() is None:
+ pytest.skip('systemd-measure not found')
+
+ ourdir = pathlib.Path(__file__).parent
+ pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
+ priv = unbase64(ourdir / 'example.tpm2-pcr-private.pem.base64')
+
+ output = f'{tmp_path}/signed.efi'
+ args = [
+ 'build',
+ *kernel_initrd,
+ f'--output={output}',
+ '--uname=1.2.3',
+ '--cmdline=ARG1 ARG2 ARG3',
+ '--os-release=ID=foobar\n',
+ '--pcr-banks=sha1', # use sha1 because it doesn't really matter
+ f'--pcr-private-key={priv.name}',
+ ] + arg_tools
+
+ # If the public key is not explicitly specified, it is derived automatically. Let's make sure everything
+ # works as expected both when the public keys is specified explicitly and when it is derived from the
+ # private key.
+ for extra in ([f'--pcrpkey={pub.name}', f'--pcr-public-key={pub.name}'], []):
+ opts = ukify.parse_args(args + extra)
+ try:
+ ukify.check_inputs(opts)
+ except OSError as e:
+ pytest.skip(str(e))
+
+ ukify.make_uki(opts)
+
+ # let's check that objdump likes the resulting file
+ dump = subprocess.check_output(['objdump', '-h', output], text=True)
+
+ for sect in 'text osrel cmdline linux initrd uname pcrsig'.split():
+ assert re.search(fr'^\s*\d+\s+\.{sect}\s+[0-9a-f]+', dump, re.MULTILINE)
+
+ # objcopy fails when called without an output argument (EPERM).
+ # It also fails when called with /dev/null (file truncated).
+ # It also fails when called with /dev/zero (because it reads the
+ # output file, infinitely in this case.)
+ # So let's just call it with a dummy output argument.
+ subprocess.check_call([
+ 'objcopy',
+ *(f'--dump-section=.{n}={tmp_path}/out.{n}' for n in (
+ 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline')),
+ output,
+ tmp_path / 'dummy',
+ ],
+ text=True)
+
+ assert open(tmp_path / 'out.pcrpkey').read() == open(pub.name).read()
+ assert open(tmp_path / 'out.osrel').read() == 'ID=foobar\n'
+ assert open(tmp_path / 'out.uname').read() == '1.2.3'
+ assert open(tmp_path / 'out.cmdline').read() == 'ARG1 ARG2 ARG3'
+ sig = open(tmp_path / 'out.pcrsig').read()
+ sig = json.loads(sig)
+ assert list(sig.keys()) == ['sha1']
+ assert len(sig['sha1']) == 4 # four items for four phases
+
+ shutil.rmtree(tmp_path)
+
+@pytest.mark.skipif(not slow_tests, reason='slow')
+def test_pcr_signing2(kernel_initrd, tmp_path):
+ if kernel_initrd is None:
+ pytest.skip('linux+initrd not found')
+ if systemd_measure() is None:
+ pytest.skip('systemd-measure not found')
+
+ ourdir = pathlib.Path(__file__).parent
+ pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
+ priv = unbase64(ourdir / 'example.tpm2-pcr-private.pem.base64')
+ pub2 = unbase64(ourdir / 'example.tpm2-pcr-public2.pem.base64')
+ priv2 = unbase64(ourdir / 'example.tpm2-pcr-private2.pem.base64')
+
+ # simulate a microcode file
+ with open(f'{tmp_path}/microcode', 'wb') as microcode:
+ microcode.write(b'1234567890')
+
+ output = f'{tmp_path}/signed.efi'
+ assert kernel_initrd[0] == '--linux'
+ opts = ukify.parse_args([
+ 'build',
+ *kernel_initrd[:2],
+ f'--initrd={microcode.name}',
+ *kernel_initrd[2:],
+ f'--output={output}',
+ '--uname=1.2.3',
+ '--cmdline=ARG1 ARG2 ARG3',
+ '--os-release=ID=foobar\n',
+ '--pcr-banks=sha1',
+ f'--pcrpkey={pub2.name}',
+ f'--pcr-public-key={pub.name}',
+ f'--pcr-private-key={priv.name}',
+ '--phases=enter-initrd enter-initrd:leave-initrd',
+ f'--pcr-public-key={pub2.name}',
+ f'--pcr-private-key={priv2.name}',
+ '--phases=sysinit ready shutdown final', # yes, those phase paths are not reachable
+ ] + arg_tools)
+
+ try:
+ ukify.check_inputs(opts)
+ except OSError as e:
+ pytest.skip(str(e))
+
+ ukify.make_uki(opts)
+
+ # let's check that objdump likes the resulting file
+ dump = subprocess.check_output(['objdump', '-h', output], text=True)
+
+ for sect in 'text osrel cmdline linux initrd uname pcrsig'.split():
+ assert re.search(fr'^\s*\d+\s+\.{sect}\s+[0-9a-f]+', dump, re.MULTILINE)
+
+ subprocess.check_call([
+ 'objcopy',
+ *(f'--dump-section=.{n}={tmp_path}/out.{n}' for n in (
+ 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline', 'initrd')),
+ output,
+ tmp_path / 'dummy',
+ ],
+ text=True)
+
+ assert open(tmp_path / 'out.pcrpkey').read() == open(pub2.name).read()
+ assert open(tmp_path / 'out.osrel').read() == 'ID=foobar\n'
+ assert open(tmp_path / 'out.uname').read() == '1.2.3'
+ assert open(tmp_path / 'out.cmdline').read() == 'ARG1 ARG2 ARG3'
+ assert open(tmp_path / 'out.initrd', 'rb').read(10) == b'1234567890'
+
+ sig = open(tmp_path / 'out.pcrsig').read()
+ sig = json.loads(sig)
+ assert list(sig.keys()) == ['sha1']
+ assert len(sig['sha1']) == 6 # six items for six phases paths
+
+ shutil.rmtree(tmp_path)
+
+def test_key_cert_generation(tmp_path):
+ opts = ukify.parse_args([
+ 'genkey',
+ f"--pcr-public-key={tmp_path / 'pcr1.pub.pem'}",
+ f"--pcr-private-key={tmp_path / 'pcr1.priv.pem'}",
+ '--phases=enter-initrd enter-initrd:leave-initrd',
+ f"--pcr-public-key={tmp_path / 'pcr2.pub.pem'}",
+ f"--pcr-private-key={tmp_path / 'pcr2.priv.pem'}",
+ '--phases=sysinit ready',
+ f"--secureboot-private-key={tmp_path / 'sb.priv.pem'}",
+ f"--secureboot-certificate={tmp_path / 'sb.cert.pem'}",
+ ])
+ assert opts.verb == 'genkey'
+ ukify.check_cert_and_keys_nonexistent(opts)
+
+ pytest.importorskip('cryptography')
+
+ ukify.generate_keys(opts)
+
+ if not shutil.which('openssl'):
+ return
+
+ for key in (tmp_path / 'pcr1.priv.pem',
+ tmp_path / 'pcr2.priv.pem',
+ tmp_path / 'sb.priv.pem'):
+ out = subprocess.check_output([
+ 'openssl', 'rsa',
+ '-in', key,
+ '-text',
+ '-noout',
+ ], text = True)
+ assert 'Private-Key' in out
+ assert '2048 bit' in out
+
+ for pub in (tmp_path / 'pcr1.pub.pem',
+ tmp_path / 'pcr2.pub.pem'):
+ out = subprocess.check_output([
+ 'openssl', 'rsa',
+ '-pubin',
+ '-in', pub,
+ '-text',
+ '-noout',
+ ], text = True)
+ assert 'Public-Key' in out
+ assert '2048 bit' in out
+
+ out = subprocess.check_output([
+ 'openssl', 'x509',
+ '-in', tmp_path / 'sb.cert.pem',
+ '-text',
+ '-noout',
+ ], text = True)
+ assert 'Certificate' in out
+ assert re.search(r'Issuer: CN\s?=\s?SecureBoot signing key on host', out)
+
+if __name__ == '__main__':
+ sys.exit(pytest.main(sys.argv))