#!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-2.1+ # pylint: disable=missing-docstring,redefined-outer-name,invalid-name # pylint: disable=unused-import,import-outside-toplevel,useless-else-on-loop # pylint: disable=consider-using-with,wrong-import-position,unspecified-encoding import base64 import json import os import pathlib import re import shutil import subprocess import sys import tempfile try: import pytest except ImportError: sys.exit(77) try: # pyflakes: noqa import pefile # noqa except ImportError: 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 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_parse_args_minimal(): 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(): 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.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( ['/ARG1', '/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].offset is None 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].offset is None assert opts.sections[1].measure is False 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_error(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 @pytest.fixture(scope='session') def kernel_initrd(): 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]}" except (KeyError, IndexError): pass return [linux, 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, tmpdir): if kernel_initrd is None: pytest.skip('linux+initrd not found') output = f'{tmpdir}/basic.efi' opts = ukify.parse_args(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]) def test_sections(kernel_initrd, tmpdir): if kernel_initrd is None: pytest.skip('linux+initrd not found') output = f'{tmpdir}/basic.efi' opts = ukify.parse_args([ *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', dump, re.MULTILINE) 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') uname = ukify.Uname.scrape(kernel_initrd[0]) assert re.match(r'\d+\.\d+\.\d+', uname) def test_efi_signing(kernel_initrd, tmpdir): 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'{tmpdir}/signed.efi' opts = ukify.parse_args([ *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 def test_pcr_signing(kernel_initrd, tmpdir): if kernel_initrd is None: pytest.skip('linux+initrd not found') if os.getuid() != 0: pytest.skip('must be root to access tpm2') if subprocess.call(['systemd-creds', 'has-tpm2', '-q']) != 0: pytest.skip('tpm2 is not available') 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'{tmpdir}/signed.efi' opts = ukify.parse_args([ *kernel_initrd, f'--output={output}', '--uname=1.2.3', '--cmdline=ARG1 ARG2 ARG3', '--os-release=ID=foobar\n', '--pcr-banks=sha1', # use sha1 as that is most likely to be supported f'--pcrpkey={pub.name}', f'--pcr-public-key={pub.name}', f'--pcr-private-key={priv.name}', ]) 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', 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}={tmpdir}/out.{n}' for n in ( 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline')), output, tmpdir / 'dummy', ], text=True) assert open(tmpdir / 'out.pcrpkey').read() == open(pub.name).read() assert open(tmpdir / 'out.osrel').read() == 'ID=foobar\n' assert open(tmpdir / 'out.uname').read() == '1.2.3' assert open(tmpdir / 'out.cmdline').read() == 'ARG1 ARG2 ARG3' sig = open(tmpdir / 'out.pcrsig').read() sig = json.loads(sig) assert list(sig.keys()) == ['sha1'] assert len(sig['sha1']) == 4 # four items for four phases def test_pcr_signing2(kernel_initrd, tmpdir): if kernel_initrd is None: pytest.skip('linux+initrd not found') if os.getuid() != 0: pytest.skip('must be root to access tpm2') if subprocess.call(['systemd-creds', 'has-tpm2', '-q']) != 0: pytest.skip('tpm2 is not available') 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'{tmpdir}/microcode', 'wb') as microcode: microcode.write(b'1234567890') output = f'{tmpdir}/signed.efi' opts = ukify.parse_args([ kernel_initrd[0], microcode.name, kernel_initrd[1], f'--output={output}', '--uname=1.2.3', '--cmdline=ARG1 ARG2 ARG3', '--os-release=ID=foobar\n', '--pcr-banks=sha1', # use sha1 as that is most likely to be supported 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 ]) 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', dump, re.MULTILINE) subprocess.check_call([ 'objcopy', *(f'--dump-section=.{n}={tmpdir}/out.{n}' for n in ( 'pcrpkey', 'pcrsig', 'osrel', 'uname', 'cmdline', 'initrd')), output, tmpdir / 'dummy', ], text=True) assert open(tmpdir / 'out.pcrpkey').read() == open(pub2.name).read() assert open(tmpdir / 'out.osrel').read() == 'ID=foobar\n' assert open(tmpdir / 'out.uname').read() == '1.2.3' assert open(tmpdir / 'out.cmdline').read() == 'ARG1 ARG2 ARG3' assert open(tmpdir / 'out.initrd', 'rb').read(10) == b'1234567890' sig = open(tmpdir / 'out.pcrsig').read() sig = json.loads(sig) assert list(sig.keys()) == ['sha1'] assert len(sig['sha1']) == 6 # six items for six phases paths if __name__ == '__main__': pytest.main([__file__, '-v'])