diff options
Diffstat (limited to 'test/py')
| -rw-r--r-- | test/py/tests/test_efi_secboot/conftest.py | 8 | ||||
| -rw-r--r-- | test/py/tests/test_efi_secboot/test_authvar.py | 47 | ||||
| -rw-r--r-- | test/py/tests/test_fit_import_data.py | 89 | ||||
| -rw-r--r-- | test/py/tests/test_fit_verity_sign.py | 203 | ||||
| -rw-r--r-- | test/py/tests/test_load_sandbox.py | 51 | ||||
| -rw-r--r-- | test/py/tests/test_net.py | 94 | ||||
| -rw-r--r-- | test/py/tests/test_semihosting/conftest.py | 4 | ||||
| -rw-r--r-- | test/py/tests/test_semihosting/test_load_semihosting.py | 38 | ||||
| -rw-r--r-- | test/py/tests/test_trace.py | 5 |
9 files changed, 533 insertions, 6 deletions
diff --git a/test/py/tests/test_efi_secboot/conftest.py b/test/py/tests/test_efi_secboot/conftest.py index 76b8f9fa0a3..0755497d46d 100644 --- a/test/py/tests/test_efi_secboot/conftest.py +++ b/test/py/tests/test_efi_secboot/conftest.py @@ -96,6 +96,14 @@ def efi_boot_env(request, ubman): check_call('cd %s; %ssign-efi-sig-list -t "2020-04-05" -c KEK.crt -k KEK.key dbx db.esl dbx_db.auth' % (mnt_point, EFITOOLS_PATH), shell=True) + # dbt (with TEST_db certificate) + check_call('cd %s; %ssign-efi-sig-list -t "2020-04-05" -c KEK.crt -k KEK.key dbt db.esl dbt.auth' + % (mnt_point, EFITOOLS_PATH), + shell=True) + # dbr (with TEST_db certificate) + check_call('cd %s; %ssign-efi-sig-list -t "2020-04-05" -c KEK.crt -k KEK.key dbr db.esl dbr.auth' + % (mnt_point, EFITOOLS_PATH), + shell=True) # Copy image check_call('cp %s/lib/efi_loader/helloworld.efi %s' % diff --git a/test/py/tests/test_efi_secboot/test_authvar.py b/test/py/tests/test_efi_secboot/test_authvar.py index 7b45f8fb814..a41e9eb9204 100644 --- a/test/py/tests/test_efi_secboot/test_authvar.py +++ b/test/py/tests/test_efi_secboot/test_authvar.py @@ -279,3 +279,50 @@ class TestEfiAuthVar(object): output = ubman.run_command( 'printenv -e SetupMode') assert '00000000: 01' in output + + def test_efi_var_auth6(self, ubman, efi_boot_env): + """ + Test Case 6 - Default GUID of signature database variables + """ + ubman.restart_uboot() + disk_img = efi_boot_env + with ubman.log.section('Test Case 6a'): + # Test Case 6a, install signature database variables in setup + # mode without -guid + output = ubman.run_command_list([ + 'host bind 0 %s' % disk_img, + 'printenv -e SetupMode']) + assert '00000000: 01' in ''.join(output) + + for var in ('db', 'dbx', 'dbt', 'dbr'): + output = ubman.run_command_list([ + 'fatload host 0:1 4000000 %s.auth' % var, + 'setenv -e -nv -bs -rt -at -i 4000000:$filesize %s' % var, + 'printenv -e -n -guid d719b2cb-3d3a-4596-a3bc-dad00e67656f %s' % var]) + assert 'Failed to set EFI variable' not in ''.join(output) + assert '%s:' % var in ''.join(output) + + with ubman.log.section('Test Case 6b'): + # Test Case 6b, variables must not exist under the global + # variable GUID + for var in ('db', 'dbx', 'dbt', 'dbr'): + output = ubman.run_command( + 'printenv -e -n -guid 8be4df61-93ca-11d2-aa0d-00e098032b8c %s' % var) + assert '\"%s\" not defined' % var in output + + with ubman.log.section('Test Case 6c'): + # Test Case 6c, PK and KEK get the global variable GUID by + # default. Enrolling PK leaves setup mode, so this must come + # after the signature database enrollment above. + for var in ('PK', 'KEK'): + output = ubman.run_command_list([ + 'fatload host 0:1 4000000 %s.auth' % var, + 'setenv -e -nv -bs -rt -at -i 4000000:$filesize %s' % var, + 'printenv -e -n -guid 8be4df61-93ca-11d2-aa0d-00e098032b8c %s' % var]) + assert 'Failed to set EFI variable' not in ''.join(output) + assert '%s:' % var in ''.join(output) + + for var in ('PK', 'KEK'): + output = ubman.run_command( + 'printenv -e -n -guid d719b2cb-3d3a-4596-a3bc-dad00e67656f %s' % var) + assert '\"%s\" not defined' % var in output diff --git a/test/py/tests/test_fit_import_data.py b/test/py/tests/test_fit_import_data.py new file mode 100644 index 00000000000..efbbad14262 --- /dev/null +++ b/test/py/tests/test_fit_import_data.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright 2026 Canonical Ltd. +# +# Test mkimage import of external data in fit_import_data() + +"""Regression test for stale per-image state in fit_import_data(). + +The import loop used to keep the data pointer and external property name +of the previous image, so an image node carrying data-size but neither +data-offset nor data-position imported the previous image's data and +then made mkimage abort without printing any diagnostic. Such a node +must be skipped by the import and reported by the later processing +stages instead. +""" + +import os +import subprocess + +import pytest + +import fit_util + +BASE_ITS = ''' +/dts-v1/; + +/ { + description = "import-data test"; + + images { + kernel-1 { + description = "first kernel"; + data = /incbin/("%(kernel1)s"); + type = "kernel"; + arch = "sandbox"; + os = "linux"; + compression = "none"; + load = <0x40000>; + entry = <0x40000>; + }; + kernel-2 { + description = "second kernel"; + data = /incbin/("%(kernel2)s"); + type = "kernel"; + arch = "sandbox"; + os = "linux"; + compression = "none"; + load = <0x80000>; + entry = <0x80000>; + }; + }; + + configurations { + default = "conf-1"; + conf-1 { + kernel = "kernel-1"; + }; + }; +}; +''' + + [email protected]('sandbox') [email protected]('dtc') [email protected]('fdtput') +def test_fit_import_data_missing_offset(ubman): + """An image with data-size but no data-offset must not inherit data""" + mkimage = os.path.join(ubman.config.build_dir, 'tools/mkimage') + params = { + 'kernel1': fit_util.make_kernel(ubman, 'imp-kernel1.bin', 'first'), + 'kernel2': fit_util.make_kernel(ubman, 'imp-kernel2.bin', 'second'), + } + its = fit_util.make_its(ubman, BASE_ITS, params, 'imp.its') + itb = fit_util.make_fname(ubman, 'imp.itb') + + result = subprocess.run([mkimage, '-E', '-f', its, itb], + capture_output=True, text=True) + assert result.returncode == 0, result.stderr + + # Remove the offset so that only data-size is left on kernel-2 + subprocess.run(['fdtput', '-d', itb, '/images/kernel-2', 'data-offset'], + check=True) + + # Re-processing must skip the malformed image in the import, so that + # the hashing stage reports it; previously the stale pointer made the + # import write kernel-1's data into kernel-2 and abort silently + result = subprocess.run([mkimage, '-F', itb], + capture_output=True, text=True) + assert result.returncode != 0 + assert "Can't get image data/size" in result.stderr diff --git a/test/py/tests/test_fit_verity_sign.py b/test/py/tests/test_fit_verity_sign.py new file mode 100644 index 00000000000..3c75ef8558c --- /dev/null +++ b/test/py/tests/test_fit_verity_sign.py @@ -0,0 +1,203 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright 2026 Daniel Golle <[email protected]> + +"""Verify that the dm-verity roothash is covered by the FIT configuration +signature. + +A dm-verity protected filesystem image is not hashed by U-Boot; its integrity +is delegated to the kernel, which trusts the roothash taken from the FIT +``dm-verity`` subnode. That roothash must therefore be part of the signed +region of the configuration, otherwise an attacker can replace both the +filesystem and the roothash while keeping the configuration signature valid. + +This test signs a configuration referencing a filesystem image that carries a +``dm-verity`` subnode, then flips one byte of the roothash and of the salt and +checks that verification rejects the image. A control tampering a byte that is +known to be signed confirms that the check is able to detect a broken region. + +The FIT pairs a signed configuration with a filesystem image carrying a +``dm-verity`` subnode: + +.. code-block:: devicetree + + images { + rootfs-1 { + data = /incbin/("rootfs.bin"); + type = "filesystem"; + compression = "none"; + hash-1 { + algo = "sha256"; + }; + dm-verity { + algo = "sha256"; + data-block-size = <4096>; + hash-block-size = <4096>; + num-data-blocks = <16>; + hash-start-block = <16>; + }; + }; + }; + + configurations { + conf-1 { + kernel = "kernel-1"; + loadables = "rootfs-1"; + signature-1 { + algo = "sha256,rsa2048"; + key-name-hint = "dev"; + sign-images = "kernel", "loadables"; + }; + }; + }; + +mkimage builds the dm-verity hash tree when assembling the image and records +the resulting roothash and salt in the ``dm-verity`` subnode; fit_check_sign +must reject an image where either was modified after signing. +""" + +import os +import pytest +import utils + +# 16 blocks of 4096 bytes, matching num-data-blocks/data-block-size below. +ROOTFS_SIZE = 16 * 4096 + +ITS = ''' +/dts-v1/; +/ { + description = "verity roothash signing coverage test"; + #address-cells = <1>; + + images { + kernel-1 { + description = "kernel"; + data = /incbin/("kernel.bin"); + type = "kernel"; + arch = "arm64"; + os = "linux"; + compression = "none"; + load = <0x40000000>; + entry = <0x40000000>; + hash-1 { algo = "sha256"; }; + }; + rootfs-1 { + description = "rootfs"; + data = /incbin/("rootfs.bin"); + type = "filesystem"; + arch = "arm64"; + compression = "none"; + hash-1 { algo = "sha256"; }; + dm-verity { + algo = "sha256"; + data-block-size = <4096>; + hash-block-size = <4096>; + num-data-blocks = <16>; + hash-start-block = <16>; + }; + }; + }; + + configurations { + default = "conf-1"; + conf-1 { + description = "signed config"; + kernel = "kernel-1"; + loadables = "rootfs-1"; + signature-1 { + algo = "sha256,rsa2048"; + key-name-hint = "dev"; + sign-images = "kernel", "loadables"; + }; + }; + }; +}; +''' + +VERITY_NODE = '/images/rootfs-1/dm-verity' +ROOTFS_HASH_NODE = '/images/rootfs-1/hash-1' + + +def flip_prop_byte(ubman, fit, node, prop): + """Flip the first byte of a byte-array property in a FIT, in place. + + The property is rewritten with the same length so that no node is + relaid out and the signed regions keep their offsets. + """ + val = utils.run_and_log(ubman, 'fdtget -t bx %s %s %s' % (fit, node, prop)) + bytelist = val.split() + bytelist[0] = '%x' % (int(bytelist[0], 16) ^ 0xff) + utils.run_and_log(ubman, 'fdtput -t bx %s %s %s %s' % + (fit, node, prop, ' '.join(bytelist))) + + [email protected]('sandbox') [email protected]('fit_signature') [email protected]('dtc') [email protected]('fdtget') [email protected]('fdtput') [email protected]('openssl') [email protected]('veritysetup') +def test_fit_verity_roothash_signed(ubman): + """The dm-verity roothash must be inside the signed configuration region.""" + tmpdir = os.path.join(ubman.config.result_dir, 'verity-sign') + '/' + if not os.path.exists(tmpdir): + os.makedirs(tmpdir) + mkimage = ubman.config.build_dir + '/tools/mkimage' + fit_check_sign = ubman.config.build_dir + '/tools/fit_check_sign' + dtc_args = '-I dts -O dtb -i %s' % tmpdir + its = tmpdir + 'verity.its' + fit = tmpdir + 'verity.itb' + dtb = tmpdir + 'control.dtb' + + # Signing key and empty control dtb to receive the public key. + utils.run_and_log(ubman, 'openssl genpkey -algorithm RSA -out %sdev.key ' + '-pkeyopt rsa_keygen_bits:2048 ' + '-pkeyopt rsa_keygen_pubexp:65537' % tmpdir) + utils.run_and_log(ubman, 'openssl req -batch -new -x509 -key %sdev.key ' + '-out %sdev.crt' % (tmpdir, tmpdir)) + with open(tmpdir + 'control.dts', 'w') as f: + f.write('/dts-v1/; / { model = "verity-test"; };\n') + utils.run_and_log(ubman, 'dtc -O dtb -o %s %scontrol.dts' % (dtb, tmpdir)) + + # Payloads. The rootfs must be a whole number of data blocks so mkimage can + # build the dm-verity hash tree and compute the roothash. + with open(tmpdir + 'rootfs.bin', 'wb') as f: + f.write(b'R' * ROOTFS_SIZE) + with open(tmpdir + 'kernel.bin', 'wb') as f: + f.write(b'KERNEL') + + with open(its, 'w') as f: + f.write(ITS) + + # Build and sign. -E keeps the (large) rootfs external, as on a real device. + utils.run_and_log(ubman, [mkimage, '-D', dtc_args, '-E', '-f', its, + '-k', tmpdir, '-K', dtb, '-r', fit]) + + # Baseline: the freshly signed image must verify. + utils.run_and_log(ubman, [fit_check_sign, '-f', fit, '-k', dtb]) + + # Control: tampering a byte that is signed (the filesystem image hash value) + # must be detected. This proves the check can fail. + control = tmpdir + 'control.itb' + utils.run_and_log(ubman, 'cp %s %s' % (fit, control)) + flip_prop_byte(ubman, control, ROOTFS_HASH_NODE, 'value') + utils.run_and_log_expect_exception( + ubman, [fit_check_sign, '-f', control, '-k', dtb], + 1, 'Failed to verify required signature') + + # Roothash: tampering the dm-verity digest must be rejected. If the digest + # is outside the signed region this check passes and boot is compromised. + tampered = tmpdir + 'tamper-digest.itb' + utils.run_and_log(ubman, 'cp %s %s' % (fit, tampered)) + flip_prop_byte(ubman, tampered, VERITY_NODE, 'digest') + utils.run_and_log_expect_exception( + ubman, [fit_check_sign, '-f', tampered, '-k', dtb], + 1, 'Failed to verify required signature') + + # Salt: likewise, the salt feeds the dm-verity target and must be signed. + tampered = tmpdir + 'tamper-salt.itb' + utils.run_and_log(ubman, 'cp %s %s' % (fit, tampered)) + flip_prop_byte(ubman, tampered, VERITY_NODE, 'salt') + utils.run_and_log_expect_exception( + ubman, [fit_check_sign, '-f', tampered, '-k', dtb], + 1, 'Failed to verify required signature') diff --git a/test/py/tests/test_load_sandbox.py b/test/py/tests/test_load_sandbox.py new file mode 100644 index 00000000000..8d28a630e76 --- /dev/null +++ b/test/py/tests/test_load_sandbox.py @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright 2026 Free Mobile - Vincent Jardin + +"""Regression test for `load sandbox - <addr> <file>`. + +Exercises the null_dev_desc_ok dispatch added in +"fs: dispatch null_dev_desc_ok filesystems before block lookup". + +It is the counterpart of test_load_semihosting.py +""" + +import os +import pytest + + [email protected](scope='session') +def sandbox_fixture(u_boot_config): + """Host-staged fixture file read by `load sandbox`.""" + path = os.path.join(u_boot_config.persistent_data_dir, + 'sandbox-fstype.txt') + with open(path, 'w', encoding='utf-8') as f: + f.write('Das U-Boot\n') # 11 bytes, same as test_hostfs.py / semihosting + yield path + os.remove(path) + + [email protected]('sandbox') +def test_sandbox_load(ubman, sandbox_fixture): + """Run `load sandbox - <addr> <file>` and check the bytes.""" + response = ubman.run_command( + f'load sandbox - $loadaddr {sandbox_fixture}') + + # Fixture is "Das U-Boot\n" (11 bytes). + assert '11 bytes read' in response + + # crc32("Das U-Boot\n") -- identical to the semihosting / hostfs checks. + response = ubman.run_command('crc32 $loadaddr $filesize') + assert '==> 60cfccfc' in response + + [email protected]('sandbox') +def test_sandbox_load_offset(ubman, sandbox_fixture): + """Run the [bytes] [pos] variant through the same dispatch.""" + response = ubman.run_command( + f'load sandbox - $loadaddr {sandbox_fixture} 4 6') + # bytes=4 pos=6 over "Das U-Boot\n" -> "Boot". + assert '4 bytes read' in response + + # crc32("Boot") + response = ubman.run_command('crc32 $loadaddr $filesize') + assert '==> e6df01fa' in response diff --git a/test/py/tests/test_net.py b/test/py/tests/test_net.py index 27cdd73fd49..a2007c2fd3a 100644 --- a/test/py/tests/test_net.py +++ b/test/py/tests/test_net.py @@ -59,6 +59,19 @@ For example: 'fnu': 'ubtest-upload.bin', } + # Details regarding a file that may be written to U-Boot using the tftpsrv + # command. This variable may be omitted or set to None if tftpsrv testing + # is not possible or desired. The test uses host-side curl TFTP support to + # upload a generated file to U-Boot. The optional tftpsrv_url entry may be + # used when the host must use a forwarded address instead of U-Boot's + # ipaddr value. + env__net_tftpsrv_file = { + 'fn': 'ubtest-tftpsrv.bin', + 'addr': 0x10000000, + 'size': 4096, + 'timeout': 50000, + } + # Details regarding a file that may be read from a NFS server. This variable # may be omitted or set to None if NFS testing is not possible or desired. env__net_nfs_readable_file = { @@ -89,6 +102,8 @@ import utils import uuid import datetime import re +import tempfile +import zlib net_set_up = False net6_set_up = False @@ -460,3 +475,82 @@ def test_net_tftpput(ubman): output = ubman.run_command("crc32 $fileaddr $filesize") assert expected_tftpb_crc in output + + [email protected]("cmd_crc32") [email protected]("cmd_tftpsrv") [email protected]("curl") +def test_net_tftpsrv(ubman): + """Test the tftpsrv command. + + A file is generated on the host, uploaded to U-Boot using TFTP and then + validated in U-Boot using its size and CRC32. + + The details of the file to upload are provided by the boardenv_* file; + see the comment at the beginning of this file. + """ + + if not net_set_up: + pytest.skip("Network not initialized") + + f = ubman.config.env.get("env__net_tftpsrv_file", None) + if not f: + pytest.skip("No tftpsrv file to write") + + curl_version = utils.run_and_log(ubman, ["curl", "--version"]) + if "tftp" not in curl_version.split(): + pytest.skip("curl does not support TFTP") + + addr = f.get("addr", None) + if not addr: + addr = utils.find_ram_base(ubman) + + timeout = f.get("timeout", ubman.p.timeout) + timeout_secs = max(1, (timeout + 999) // 1000) + size = f.get("size", 4096) + fn = f.get("fn", "ubtest-tftpsrv.bin") + url = f.get("tftpsrv_url", None) + data = bytes([i % 251 for i in range(size)]) + crc = "%08x" % (zlib.crc32(data) & 0xffffffff) + + ip = ubman.run_command("echo $ipaddr").strip() + if not ip: + pytest.skip("No U-Boot IP address") + if not url: + url = "tftp://%s/%s" % (ip, fn) + + with tempfile.NamedTemporaryFile() as tmp: + tmp.write(data) + tmp.flush() + + done = False + with ubman.temporary_timeout(timeout): + try: + ubman.run_command("tftpsrv %x" % addr, + wait_for_prompt=False) + ubman.wait_for("Listening for TFTP transfer") + utils.run_and_log( + ubman, + [ + "curl", + "--fail", + "--max-time", + str(timeout_secs), + "--upload-file", + tmp.name, + url, + ], + ) + ubman.wait_for("Bytes transferred = %d" % size) + ubman.wait_for(ubman.prompt) + done = True + finally: + if not done: + ubman.ctrlc() + ubman.drain_console() + + output = ubman.run_command("echo $filesize") + assert "%x" % size in output + + output = ubman.run_command("crc32 $fileaddr $filesize") + assert crc in output diff --git a/test/py/tests/test_semihosting/conftest.py b/test/py/tests/test_semihosting/conftest.py index b00d8f4ea9c..6b7f3f3c2d9 100644 --- a/test/py/tests/test_semihosting/conftest.py +++ b/test/py/tests/test_semihosting/conftest.py @@ -6,9 +6,9 @@ import os import pytest [email protected](scope='session') [email protected](scope='function') def semihosting_data(u_boot_config): - """Set up a file system to be used in semihosting tests + """Set up a new file for each semihosting test Args: u_boot_config -- U-Boot configuration. diff --git a/test/py/tests/test_semihosting/test_load_semihosting.py b/test/py/tests/test_semihosting/test_load_semihosting.py new file mode 100644 index 00000000000..7c2eb72c69a --- /dev/null +++ b/test/py/tests/test_semihosting/test_load_semihosting.py @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: GPL-2.0+ +# Copyright 2026 Free Mobile - Vincent Jardin + +"""Regression test for `load semihosting - <addr> <file>`. + +Companion to test_hostfs.py: same fixture, same crc32, different +fstype routing: +see the doc/usage/cmd/load.rst "Null-block-device interfaces" section. +""" + +import pytest + + [email protected]('semihosting') +def test_semihosting_load(ubman, semihosting_data): + """Run `load semihosting - <addr> <file>` and check the bytes.""" + response = ubman.run_command( + f'load semihosting - $loadaddr {semihosting_data}') + + # Fixture is "Das U-Boot\n" (11 bytes). + assert '11 bytes read' in response + + # crc32("Das U-Boot\n") + response = ubman.run_command('crc32 $loadaddr $filesize') + assert '==> 60cfccfc' in response + + [email protected]('semihosting') +def test_semihosting_load_offset(ubman, semihosting_data): + """Run the [bytes] [pos] variant through the same dispatch.""" + response = ubman.run_command( + f'load semihosting - $loadaddr {semihosting_data} 4 6') + # bytes=4 pos=6 over "Das U-Boot\n" -> "Boot". + assert '4 bytes read' in response + + # crc32("Boot") + response = ubman.run_command('crc32 $loadaddr $filesize') + assert '==> e6df01fa' in response diff --git a/test/py/tests/test_trace.py b/test/py/tests/test_trace.py index 36a3c4e8fe9..a68851facc4 100644 --- a/test/py/tests/test_trace.py +++ b/test/py/tests/test_trace.py @@ -145,8 +145,6 @@ def check_function(ubman, fname, proftool, map_fname, trace_dat): out = utils.run_and_log(ubman, ['sh', '-c', cmd]) # Format: - # u-boot-1 0..... 60.805596: function: initf_malloc - # u-boot-1 0..... 60.805597: function: initf_malloc # u-boot-1 0..... 60.805601: function: initf_bootstage # u-boot-1 0..... 60.805607: function: initf_bootstage @@ -162,7 +160,7 @@ def check_function(ubman, fname, proftool, map_fname, trace_dat): # Check for some expected functions if ubman.config.buildconfig.get('config_trace_early'): - assert 'initf_malloc' in vals.keys() + assert 'initf_upl' in vals.keys() assert 'initr_watchdog' in vals.keys() assert 'initr_dm' in vals.keys() @@ -193,7 +191,6 @@ def check_funcgraph(ubman, fname, proftool, map_fname, trace_dat): out = utils.run_and_log(ubman, ['sh', '-c', cmd]) # First look for this: - # u-boot-1 0..... 282.101360: funcgraph_entry: 0.004 us | initf_malloc(); # ... # u-boot-1 0..... 282.101369: funcgraph_entry: | initf_bootstage() { # u-boot-1 0..... 282.101369: funcgraph_entry: | bootstage_init() { |
