Files
buildroot/support/testing/tests/package/test_uboot_tools.py
James Hilliard 928cc5dc5c package/uboot-tools: fix host FIT signature support
U-Boot host tools use the tools configuration namespace when code calls
CONFIG_IS_ENABLED(). With USE_HOSTCC, CONFIG_IS_ENABLED(FIT_SIGNATURE)
resolves to CONFIG_TOOLS_FIT_SIGNATURE, while
CONFIG_VAL(FIT_SIGNATURE_MAX_SIZE) resolves to
CONFIG_TOOLS_FIT_SIGNATURE_MAX_SIZE.

The host-uboot-tools package only generates the old CONFIG_FIT_SIGNATURE
define. This makes tools/Makefile include fit_check_sign, but the host code
sees FIT signature support as disabled and the OpenSSL-backed signing and
verification objects are omitted. As a result, mkimage accepts a signature
node without writing its value or injecting a required public key.
fit_check_sign then has no required key and reports success without
checking the configuration signature.

A FIT-support-only configuration never exercises this path, which is why
the existing hash-only runtime test still passes.

Generate the tools FIT signature options needed by the host code and pass
CONFIG_TOOLS_LIBCRYPTO=y so the OpenSSL-backed signing, verification and
cipher objects are selected. Keep CONFIG_FIT_SIGNATURE=y in the make
options because U-Boot tools/Makefile still uses it to build
fit_check_sign.

Extend TestHostUbootTools to create an RSA-signed FIT, require a 256-byte
configuration signature and a required public key, verify the FIT, corrupt
the signature, and require verification to fail.

Signed-off-by: James Hilliard <james.hilliard1@gmail.com>
[Julien: add comments in runtime test]
Signed-off-by: Julien Olivain <ju.o@free.fr>
2026-07-29 20:38:07 +02:00

160 lines
5.8 KiB
Python

import hashlib
import re
import subprocess
import zlib
from pathlib import Path
import infra.basetest
EXAMPLE_ITS = Path(__file__).parent / "test_uboot_tools/example.its"
KEY_DTS = Path(__file__).parent / "test_uboot_tools/key.dts"
SIGNED_ITS = Path(__file__).parent / "test_uboot_tools/signed.its"
def get_hashes(output: str) -> dict[str, str]:
"""Get hashes from dumpimage output (assumes exactly one image in FIT)"""
pat = re.compile(
r"Hash algo:\s+(\S+)$\s+Hash value:\s+(\w+)$", flags=re.MULTILINE)
return dict(pat.findall(output))
def calc_hashes(data: bytes) -> dict[str, str]:
"""Calculate the hashes dumpimage should report for a given blob"""
return {
"crc32": f"{zlib.crc32(data):08x}",
"md5": hashlib.md5(data).hexdigest(),
"sha1": hashlib.sha1(data).hexdigest(),
"sha256": hashlib.sha256(data).hexdigest(),
"sha384": hashlib.sha384(data).hexdigest(),
"sha512": hashlib.sha512(data).hexdigest(),
}
class TestUbootTools(infra.basetest.BRTest):
rootfs_overlay = \
infra.filepath("tests/package/test_uboot_tools")
config = infra.basetest.BASIC_TOOLCHAIN_CONFIG + \
f"""
BR2_PACKAGE_UBOOT_TOOLS=y
BR2_PACKAGE_UBOOT_TOOLS_DUMPIMAGE=y
BR2_PACKAGE_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_UBOOT_TOOLS_MKIMAGE=y
BR2_ROOTFS_OVERLAY="{rootfs_overlay}"
BR2_TARGET_ROOTFS_CPIO=y
# BR2_TARGET_ROOTFS_TAR is not set
"""
def test_run(self):
ramdisk = Path(self.builddir) / "images/rootfs.cpio"
self.emulator.boot(arch="armv5",
kernel="builtin",
options=["-initrd", str(ramdisk)])
self.emulator.login()
self.assertRunOk(f"mkimage -f /{EXAMPLE_ITS.name} /tmp/test.fit")
cmd = "dumpimage -l /tmp/test.fit"
out, exit_code = self.emulator.run(cmd, -1)
self.assertEqual(
exit_code,
0,
"\nFailed to run: {}\noutput was:\n{}".format(cmd, ' '+'\n '.join(out))
)
reported = get_hashes("\n".join(out))
expected = calc_hashes(EXAMPLE_ITS.read_bytes())
for h in expected:
with self.subTest(hash=h):
self.assertEqual(expected[h], reported[h])
# Python does not have built-in CRC16 support, just check it is present
self.assertIn("crc16-ccitt", reported)
class TestHostUbootTools(infra.basetest.BRHostPkgTest):
hostpkgs = ["host-uboot-tools"]
config = infra.basetest.BASIC_TOOLCHAIN_CONFIG + \
"""
BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
"""
def test_run(self):
cmd = ["host/bin/mkimage", "-f", str(EXAMPLE_ITS), "test.fit"]
infra.run_cmd_on_host(self.builddir, cmd)
cmd = ["host/bin/dumpimage", "-l", "test.fit"]
reported = get_hashes(infra.run_cmd_on_host(self.builddir, cmd))
expected = calc_hashes(EXAMPLE_ITS.read_bytes())
for h in expected:
with self.subTest(hash=h):
self.assertEqual(expected[h], reported[h])
# Python does not have built-in CRC16 support, just check it is present
self.assertIn("crc16-ccitt", reported)
# See U-Boot FIT Signature Verification documentation:
# https://source.denx.de/u-boot/u-boot/-/blob/v2026.07/doc/usage/fit/signature.rst
keydir = Path(self.builddir) / "keys"
keydir.mkdir(exist_ok=True)
# We generate a prublic/private key pair to sign the image.
cmd = [
"host/bin/openssl", "req", "-batch", "-new", "-x509", "-nodes",
"-newkey", "rsa:2048", "-keyout", str(keydir / "dev.key"),
"-out", str(keydir / "dev.crt"), "-subj", "/CN=Buildroot FIT test",
]
infra.run_cmd_on_host(self.builddir, cmd)
# We compile the empty dtb that will be used to store the
# public key.
cmd = [
"host/bin/dtc", "-I", "dts", "-O", "dtb", "-p", "0x1000",
"-o", "test-key.dtb", str(KEY_DTS),
]
infra.run_cmd_on_host(self.builddir, cmd)
# We sign the image and write the public key in our key
# storage dtb.
cmd = [
"host/bin/mkimage", "-f", str(SIGNED_ITS), "-k", str(keydir),
"-K", "test-key.dtb", "-r", "signed.fit",
]
infra.run_cmd_on_host(self.builddir, cmd)
# We check there is a signature present in the FIT image.
cmd = [
"host/bin/fdtget", "-t", "bx", "signed.fit",
"/configurations/config-1/signature-1", "value",
]
signature = infra.run_cmd_on_host(self.builddir, cmd).split()
self.assertEqual(len(signature), 256)
# We check the key is marked as required for the configuration.
cmd = [
"host/bin/fdtget", "-t", "s", "test-key.dtb",
"/signature/key-dev", "required",
]
required = infra.run_cmd_on_host(self.builddir, cmd).strip()
self.assertEqual(required, "conf")
# We actually check the signature is valid.
cmd = [
"host/bin/fit_check_sign", "-f", "signed.fit",
"-k", "test-key.dtb",
]
infra.run_cmd_on_host(self.builddir, cmd)
# We "corrupt" the signature, by setting it to zero.
cmd = [
"host/bin/fdtput", "-t", "bx", "signed.fit",
"/configurations/config-1/signature-1", "value", "00",
]
infra.run_cmd_on_host(self.builddir, cmd)
# We check again the siganute, and expect a failure.
cmd = [
"host/bin/fit_check_sign", "-f", "signed.fit",
"-k", "test-key.dtb",
]
with self.assertRaises(subprocess.CalledProcessError):
infra.run_cmd_on_host(self.builddir, cmd)