diff options
| author | Tom Rini <[email protected]> | 2026-05-27 13:44:20 -0600 |
|---|---|---|
| committer | Tom Rini <[email protected]> | 2026-05-27 13:44:20 -0600 |
| commit | 746a986fe247fc0b3d52ad7ae7027e0a6d57d12c (patch) | |
| tree | 9fd87b6016fb21d043deab6fffabb8ddad9fd36c /test | |
| parent | 8d5f30b52f7c800c2177188fc4d331fb7af2c46a (diff) | |
| parent | 89d3c1fe1b0fb5db15fce96a7e6db7885ebf240e (diff) | |
Merge patch series "fit: dm-verity support"
Daniel Golle <[email protected]> says:
This series adds dm-verity support to U-Boot's FIT image infrastructure.
It is the first logical subset of the larger OpenWrt boot method series
posted as an RFC in February 2026 [1], extracted here for independent
review and merging.
OpenWrt's firmware model embeds a read-only squashfs or erofs root
filesystem directly inside a uImage.FIT container as a FILESYSTEM-type
loadable FIT image. At boot the kernel maps this sub-image directly from
the underlying block device via the fitblk driver (/dev/fit0, /dev/fit1,
...), the goal is that the bootloader never even copies it to RAM.
dm-verity enables the kernel to verify the integrity of those mapped
filesystems at read time, with a Merkle hash tree stored contiguously in
the same sub-image just after the data. Two kernel command-line
parameters are required:
dm-mod.create= -- the device-mapper target table for the verity device
dm-mod.waitfor= -- a comma-separated list of block devices to wait for
before dm-init sets up the targets (needed when fitblk
probes late, e.g. because it depends on NVMEM
calibration data)
The FIT dm-verity node schema was upstreamed into the flat-image-tree
specification [2], which this implementation tries to follow exactly.
The runtime feature is guarded behind CONFIG_FIT_VERITY. If not
enabled the resulting binary size remains unchanged. If enabled the
binary size increases by about 3kB.
[1] previous submissions:
RFC: https://www.mail-archive.com/[email protected]/msg565945.html
v1: https://www.mail-archive.com/[email protected]/msg569472.html
v2: https://www.mail-archive.com/[email protected]/msg570599.html
v3: https://www.mail-archive.com/[email protected]/msg573223.html
v4: https://www.mail-archive.com/[email protected]/msg574000.html
[2] flat-image-tree dm-verity node spec:
https://github.com/open-source-firmware/flat-image-tree/commit/795fd5fd7f0121d0cb03efb1900aafc61c704771
Link: https://lore.kernel.org/r/[email protected]
Diffstat (limited to 'test')
| -rw-r--r-- | test/boot/Makefile | 1 | ||||
| -rw-r--r-- | test/boot/fit_verity.c | 306 | ||||
| -rw-r--r-- | test/cmd_ut.c | 2 | ||||
| -rw-r--r-- | test/py/tests/test_fit_verity.py | 175 |
4 files changed, 484 insertions, 0 deletions
diff --git a/test/boot/Makefile b/test/boot/Makefile index 89538d4f0a6..d98f212b243 100644 --- a/test/boot/Makefile +++ b/test/boot/Makefile @@ -15,6 +15,7 @@ endif ifdef CONFIG_SANDBOX obj-$(CONFIG_$(PHASE_)CMDLINE) += bootm.o endif +obj-$(CONFIG_$(PHASE_)FIT_VERITY) += fit_verity.o obj-$(CONFIG_MEASURED_BOOT) += measurement.o ifdef CONFIG_OF_LIVE diff --git a/test/boot/fit_verity.c b/test/boot/fit_verity.c new file mode 100644 index 00000000000..7459a9d6f81 --- /dev/null +++ b/test/boot/fit_verity.c @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Tests for FIT dm-verity cmdline generation + * + * Copyright 2026 Daniel Golle <[email protected]> + */ + +#include <image.h> +#include <test/test.h> +#include <test/ut.h> + +#define FIT_VERITY_TEST(_name, _flags) UNIT_TEST(_name, _flags, fit_verity) + +/* FIT blob buffer size — generous to avoid FDT_ERR_NOSPACE */ +#define FIT_BUF_SIZE 4096 + +/* Test digest (32 bytes = sha256) */ +static const u8 test_digest[32] = { + 0x8e, 0x67, 0x91, 0x63, 0x7f, 0x93, 0xcb, 0xb8, + 0x1f, 0xc4, 0x52, 0x99, 0xe2, 0x03, 0xcb, 0xe8, + 0x5c, 0xa2, 0xe4, 0x7a, 0x38, 0xf5, 0x05, 0x1b, + 0xdd, 0xee, 0xce, 0x92, 0xd7, 0xb1, 0xc9, 0xf9, +}; + +/* Test salt (32 bytes) */ +static const u8 test_salt[32] = { + 0xaa, 0x7b, 0x11, 0xf8, 0xdb, 0x8f, 0xe2, 0xe5, + 0xbf, 0xd4, 0xec, 0xa1, 0xd1, 0x8a, 0x22, 0xb5, + 0xde, 0x7e, 0xa3, 0x9d, 0x2e, 0x1b, 0x93, 0xbb, + 0x72, 0x72, 0xce, 0x0c, 0x6c, 0xa3, 0xcc, 0x8e, +}; + +/** + * build_verity_fit() - construct a minimal FIT blob with dm-verity metadata + * @buf: output buffer (at least FIT_BUF_SIZE bytes) + * @num_loadables: number of filesystem loadables to create (1 or 2) + * + * Builds a FIT blob containing: + * - /images/rootfsN with type="filesystem" and a dm-verity subnode + * - /configurations/conf-1 referencing the loadable(s) + * + * Return: configuration node offset, or -ve on error + */ +static int build_verity_fit(void *buf, int num_loadables) +{ + int images_node, conf_node, confs_node, img_node, verity_node; + fdt32_t val; + int ret, i; + char name[32]; + /* + * Build the loadables string list. FDT stringlists are concatenated + * NUL-terminated strings. E.g. "rootfs0\0rootfs1\0" + */ + char loadables[128]; + int loadables_len = 0; + + ret = fdt_create_empty_tree(buf, FIT_BUF_SIZE); + if (ret) + return ret; + + /* /images */ + images_node = fdt_add_subnode(buf, 0, "images"); + if (images_node < 0) + return images_node; + + for (i = 0; i < num_loadables; i++) { + snprintf(name, sizeof(name), "rootfs%d", i); + + img_node = fdt_add_subnode(buf, images_node, name); + if (img_node < 0) + return img_node; + + ret = fdt_setprop_string(buf, img_node, FIT_TYPE_PROP, + "filesystem"); + if (ret) + return ret; + + verity_node = fdt_add_subnode(buf, img_node, + FIT_VERITY_NODENAME); + if (verity_node < 0) + return verity_node; + + ret = fdt_setprop_string(buf, verity_node, + FIT_VERITY_ALGO_PROP, "sha256"); + if (ret) + return ret; + + val = cpu_to_fdt32(4096); + ret = fdt_setprop(buf, verity_node, FIT_VERITY_DBS_PROP, + &val, sizeof(val)); + if (ret) + return ret; + + ret = fdt_setprop(buf, verity_node, FIT_VERITY_HBS_PROP, + &val, sizeof(val)); + if (ret) + return ret; + + val = cpu_to_fdt32(100); + ret = fdt_setprop(buf, verity_node, FIT_VERITY_NBLK_PROP, + &val, sizeof(val)); + if (ret) + return ret; + + val = cpu_to_fdt32(100); + ret = fdt_setprop(buf, verity_node, FIT_VERITY_HBLK_PROP, + &val, sizeof(val)); + if (ret) + return ret; + + ret = fdt_setprop(buf, verity_node, FIT_VERITY_DIGEST_PROP, + test_digest, sizeof(test_digest)); + if (ret) + return ret; + + ret = fdt_setprop(buf, verity_node, FIT_VERITY_SALT_PROP, + test_salt, sizeof(test_salt)); + if (ret) + return ret; + + /* Append to loadables stringlist */ + loadables_len += snprintf(loadables + loadables_len, + sizeof(loadables) - loadables_len, + "%s", name) + 1; + } + + /* /configurations/conf-1 */ + confs_node = fdt_add_subnode(buf, 0, "configurations"); + if (confs_node < 0) + return confs_node; + + conf_node = fdt_add_subnode(buf, confs_node, "conf-1"); + if (conf_node < 0) + return conf_node; + + ret = fdt_setprop(buf, conf_node, FIT_LOADABLE_PROP, + loadables, loadables_len); + if (ret) + return ret; + + return conf_node; +} + +/* Test: single dm-verity loadable produces correct cmdline fragments */ +static int fit_verity_test_single(struct unit_test_state *uts) +{ + char buf[FIT_BUF_SIZE]; + struct bootm_headers images; + int conf_noffset; + + conf_noffset = build_verity_fit(buf, 1); + ut_assert(conf_noffset >= 0); + + memset(&images, 0, sizeof(images)); + ut_assertok(fit_verity_build_cmdline(buf, conf_noffset, &images)); + + /* dm_mod_create should contain the target spec for rootfs0 */ + ut_assertnonnull(images.dm_mod_create); + ut_assert(strstr(images.dm_mod_create, "rootfs0,,,")); + ut_assert(strstr(images.dm_mod_create, "verity 1")); + ut_assert(strstr(images.dm_mod_create, "/dev/fit0")); + ut_assert(strstr(images.dm_mod_create, "4096 4096 100 100")); + ut_assert(strstr(images.dm_mod_create, "sha256")); + /* Check hex-encoded digest prefix */ + ut_assert(strstr(images.dm_mod_create, "8e6791637f93cbb8")); + /* Check hex-encoded salt prefix */ + ut_assert(strstr(images.dm_mod_create, "aa7b11f8db8fe2e5")); + + /* dm_mod_waitfor should reference /dev/fit0 */ + ut_assertnonnull(images.dm_mod_waitfor); + ut_asserteq_str("/dev/fit0", images.dm_mod_waitfor); + + fit_verity_free(&images); + ut_assertnull(images.dm_mod_create); + ut_assertnull(images.dm_mod_waitfor); + + return 0; +} +FIT_VERITY_TEST(fit_verity_test_single, 0); + +/* Test: FIT with no dm-verity subnode returns 0, pointers stay NULL */ +static int fit_verity_test_no_verity(struct unit_test_state *uts) +{ + char buf[FIT_BUF_SIZE]; + struct bootm_headers images; + int conf_node, images_node, img_node, confs_node; + int ret; + + ret = fdt_create_empty_tree(buf, FIT_BUF_SIZE); + ut_assertok(ret); + + images_node = fdt_add_subnode(buf, 0, "images"); + ut_assert(images_node >= 0); + + img_node = fdt_add_subnode(buf, images_node, "rootfs"); + ut_assert(img_node >= 0); + ut_assertok(fdt_setprop_string(buf, img_node, FIT_TYPE_PROP, + "filesystem")); + /* No dm-verity subnode */ + + confs_node = fdt_add_subnode(buf, 0, "configurations"); + ut_assert(confs_node >= 0); + conf_node = fdt_add_subnode(buf, confs_node, "conf-1"); + ut_assert(conf_node >= 0); + ut_assertok(fdt_setprop_string(buf, conf_node, FIT_LOADABLE_PROP, + "rootfs")); + + memset(&images, 0, sizeof(images)); + ut_asserteq(0, fit_verity_build_cmdline(buf, conf_node, &images)); + ut_assertnull(images.dm_mod_create); + ut_assertnull(images.dm_mod_waitfor); + + return 0; +} +FIT_VERITY_TEST(fit_verity_test_no_verity, 0); + +/* Test: two dm-verity loadables produce combined cmdline */ +static int fit_verity_test_two_loadables(struct unit_test_state *uts) +{ + char buf[FIT_BUF_SIZE]; + struct bootm_headers images; + int conf_noffset; + + conf_noffset = build_verity_fit(buf, 2); + ut_assert(conf_noffset >= 0); + + memset(&images, 0, sizeof(images)); + ut_assertok(fit_verity_build_cmdline(buf, conf_noffset, &images)); + + /* Both targets should appear, separated by ";" */ + ut_assertnonnull(images.dm_mod_create); + ut_assert(strstr(images.dm_mod_create, "rootfs0,,,")); + ut_assert(strstr(images.dm_mod_create, ";rootfs1,,,")); + ut_assert(strstr(images.dm_mod_create, "/dev/fit0")); + ut_assert(strstr(images.dm_mod_create, "/dev/fit1")); + + /* dm_mod_waitfor should list both devices */ + ut_assertnonnull(images.dm_mod_waitfor); + ut_assert(strstr(images.dm_mod_waitfor, "/dev/fit0")); + ut_assert(strstr(images.dm_mod_waitfor, "/dev/fit1")); + + fit_verity_free(&images); + return 0; +} +FIT_VERITY_TEST(fit_verity_test_two_loadables, 0); + +/* Test: invalid block size (not power of two) returns -EINVAL */ +static int fit_verity_test_bad_blocksize(struct unit_test_state *uts) +{ + char buf[FIT_BUF_SIZE]; + struct bootm_headers images; + int images_node, conf_node, confs_node, img_node, verity_node; + fdt32_t val; + int ret; + + ret = fdt_create_empty_tree(buf, FIT_BUF_SIZE); + ut_assertok(ret); + + images_node = fdt_add_subnode(buf, 0, "images"); + ut_assert(images_node >= 0); + + img_node = fdt_add_subnode(buf, images_node, "rootfs"); + ut_assert(img_node >= 0); + ut_assertok(fdt_setprop_string(buf, img_node, FIT_TYPE_PROP, + "filesystem")); + + verity_node = fdt_add_subnode(buf, img_node, FIT_VERITY_NODENAME); + ut_assert(verity_node >= 0); + + ut_assertok(fdt_setprop_string(buf, verity_node, + FIT_VERITY_ALGO_PROP, "sha256")); + + /* 3000 is not a power of two */ + val = cpu_to_fdt32(3000); + ut_assertok(fdt_setprop(buf, verity_node, FIT_VERITY_DBS_PROP, + &val, sizeof(val))); + val = cpu_to_fdt32(4096); + ut_assertok(fdt_setprop(buf, verity_node, FIT_VERITY_HBS_PROP, + &val, sizeof(val))); + + val = cpu_to_fdt32(100); + ut_assertok(fdt_setprop(buf, verity_node, FIT_VERITY_NBLK_PROP, + &val, sizeof(val))); + ut_assertok(fdt_setprop(buf, verity_node, FIT_VERITY_HBLK_PROP, + &val, sizeof(val))); + + ut_assertok(fdt_setprop(buf, verity_node, FIT_VERITY_DIGEST_PROP, + test_digest, sizeof(test_digest))); + ut_assertok(fdt_setprop(buf, verity_node, FIT_VERITY_SALT_PROP, + test_salt, sizeof(test_salt))); + + confs_node = fdt_add_subnode(buf, 0, "configurations"); + ut_assert(confs_node >= 0); + conf_node = fdt_add_subnode(buf, confs_node, "conf-1"); + ut_assert(conf_node >= 0); + ut_assertok(fdt_setprop_string(buf, conf_node, FIT_LOADABLE_PROP, + "rootfs")); + + memset(&images, 0, sizeof(images)); + ut_asserteq(-EINVAL, fit_verity_build_cmdline(buf, conf_node, &images)); + ut_assertnull(images.dm_mod_create); + ut_assertnull(images.dm_mod_waitfor); + + return 0; +} +FIT_VERITY_TEST(fit_verity_test_bad_blocksize, 0); diff --git a/test/cmd_ut.c b/test/cmd_ut.c index 44e5fdfdaa6..d1b376f617c 100644 --- a/test/cmd_ut.c +++ b/test/cmd_ut.c @@ -59,6 +59,7 @@ SUITE_DECL(env); SUITE_DECL(exit); SUITE_DECL(fdt); SUITE_DECL(fdt_overlay); +SUITE_DECL(fit_verity); SUITE_DECL(font); SUITE_DECL(hush); SUITE_DECL(lib); @@ -86,6 +87,7 @@ static struct suite suites[] = { SUITE(exit, "shell exit and variables"), SUITE(fdt, "fdt command"), SUITE(fdt_overlay, "device tree overlays"), + SUITE(fit_verity, "FIT dm-verity cmdline generation"), SUITE(font, "font command"), SUITE(hush, "hush behaviour"), SUITE(lib, "library functions"), diff --git a/test/py/tests/test_fit_verity.py b/test/py/tests/test_fit_verity.py new file mode 100644 index 00000000000..f1b6262ed0e --- /dev/null +++ b/test/py/tests/test_fit_verity.py @@ -0,0 +1,175 @@ +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright 2026 Daniel Golle <[email protected]> + +""" +Test mkimage dm-verity Merkle-tree generation + +Build a minimal .its with a dm-verity subnode (user-provided properties only), +run mkimage -E, and verify that the computed properties (digest, salt, +num-data-blocks, hash-start-block) are written into the resulting FIT. +The computed digest is then re-verified by running ``veritysetup verify`` +against the external data section of the .itb. + +This test does not run the sandbox. It only exercises the host tool 'mkimage'. +Requires 'veritysetup' from the cryptsetup package on the build host. +""" + +import os +import struct +import pytest +import utils + +ITS_TEMPLATE = """\ +/dts-v1/; + +/ { + description = "dm-verity test"; + #address-cells = <1>; + + images { + rootfs { + description = "test filesystem"; + data = /incbin/("./rootfs.bin"); + type = "filesystem"; + arch = "sandbox"; + compression = "none"; + + dm-verity { + algo = "sha256"; + data-block-size = <%d>; + hash-block-size = <%d>; + }; + }; + }; + + configurations { + default = "conf-1"; + conf-1 { + description = "test config"; + loadables = "rootfs"; + }; + }; +}; +""" + +def _fdt_totalsize(path): + """Read the totalsize field from an FDT header (offset 4, big-endian u32).""" + with open(path, 'rb') as f: + magic, totalsize = struct.unpack('>II', f.read(8)) + assert magic == 0xd00dfeed, f'not an FDT: magic={magic:#x}' + return totalsize + + +def _run_round_trip(ubman, tempdir, data_block_size, hash_block_size): + """Build a FIT with dm-verity, verify written properties, re-verify with veritysetup.""" + mkimage = ubman.config.build_dir + '/tools/mkimage' + + rootfs_file = os.path.join(tempdir, 'rootfs.bin') + its_file = os.path.join(tempdir, 'image.its') + fit_file = os.path.join(tempdir, 'image.itb') + + # 64 data blocks of 0xa5 + num_blocks = 64 + data_size = data_block_size * num_blocks + with open(rootfs_file, 'wb') as f: + f.write(bytes([0xa5]) * data_size) + + with open(its_file, 'w') as f: + f.write(ITS_TEMPLATE % (data_block_size, hash_block_size)) + + dtc_args = f'-I dts -O dtb -i {tempdir}' + utils.run_and_log(ubman, + [mkimage, '-E', '-D', dtc_args, '-f', its_file, fit_file]) + + def fdt_get(node, prop): + val = utils.run_and_log(ubman, f'fdtget {fit_file} {node} {prop}') + return val.strip() + + def fdt_get_hex(node, prop): + val = utils.run_and_log(ubman, f'fdtget -tbx {fit_file} {node} {prop}') + return ''.join(b.zfill(2) for b in val.strip().split()) + + verity_path = '/images/rootfs/dm-verity' + + assert fdt_get(verity_path, 'algo') == 'sha256' + assert int(fdt_get(verity_path, 'data-block-size')) == data_block_size + assert int(fdt_get(verity_path, 'hash-block-size')) == hash_block_size + + nblk = int(fdt_get(verity_path, 'num-data-blocks')) + assert nblk == num_blocks, f'num-data-blocks {nblk} != {num_blocks}' + + hblk = int(fdt_get(verity_path, 'hash-start-block')) + # With --no-superblock, hash-start-block = data_size / hash-block-size + assert hblk == data_size // hash_block_size, \ + f'hash-start-block {hblk} != {data_size // hash_block_size}' + + digest = fdt_get_hex(verity_path, 'digest') + assert len(digest) == 64 and digest != '0' * 64 + salt = fdt_get_hex(verity_path, 'salt') + assert len(salt) == 64 + + # Re-verify the digest with veritysetup against the .itb's external data. + # With -E, image data sits after the FIT FDT at (fdt_totalsize + data-offset). + data_offset = int(fdt_get('/images/rootfs', 'data-offset')) + data_size_full = int(fdt_get('/images/rootfs', 'data-size')) + ext_pos = _fdt_totalsize(fit_file) + data_offset + expanded = os.path.join(tempdir, 'expanded.bin') + with open(fit_file, 'rb') as src, open(expanded, 'wb') as dst: + src.seek(ext_pos) + dst.write(src.read(data_size_full)) + + utils.run_and_log(ubman, [ + 'veritysetup', 'verify', expanded, expanded, digest, + '--no-superblock', + f'--data-block-size={data_block_size}', + f'--hash-block-size={hash_block_size}', + f'--data-blocks={nblk}', + '--hash=sha256', + f'--salt={salt}', + f'--hash-offset={data_size}', + ]) + + [email protected]('dtc') [email protected]('fdtget') [email protected]('veritysetup') [email protected]('data_block_size,hash_block_size,subdir', [ + (4096, 4096, 'verity-equal'), + (4096, 1024, 'verity-unequal'), +]) +def test_mkimage_verity(ubman, data_block_size, hash_block_size, subdir): + """mkimage writes correct dm-verity properties and the digest verifies. + + Run with matching and mismatched block sizes so the + ``hash-start-block != num-data-blocks`` path is exercised. + """ + tempdir = os.path.join(ubman.config.result_dir, subdir) + os.makedirs(tempdir, exist_ok=True) + _run_round_trip(ubman, tempdir, data_block_size, hash_block_size) + + [email protected]('dtc') [email protected]('veritysetup') +def test_mkimage_verity_requires_external(ubman): + """mkimage rejects dm-verity without -E with the expected diagnostic.""" + + mkimage = ubman.config.build_dir + '/tools/mkimage' + tempdir = os.path.join(ubman.config.result_dir, 'verity_no_ext') + os.makedirs(tempdir, exist_ok=True) + + rootfs_file = os.path.join(tempdir, 'rootfs.bin') + its_file = os.path.join(tempdir, 'image.its') + fit_file = os.path.join(tempdir, 'image.itb') + + with open(rootfs_file, 'wb') as f: + f.write(bytes([0xa5]) * 4096 * 8) + + with open(its_file, 'w') as f: + f.write(ITS_TEMPLATE % (4096, 4096)) + + dtc_args = f'-I dts -O dtb -i {tempdir}' + utils.run_and_log_expect_exception( + ubman, + [mkimage, '-D', dtc_args, '-f', its_file, fit_file], + 1, 'dm-verity requires external data') |
