summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorTom Rini <[email protected]>2026-05-27 13:44:20 -0600
committerTom Rini <[email protected]>2026-05-27 13:44:20 -0600
commit746a986fe247fc0b3d52ad7ae7027e0a6d57d12c (patch)
tree9fd87b6016fb21d043deab6fffabb8ddad9fd36c /test
parent8d5f30b52f7c800c2177188fc4d331fb7af2c46a (diff)
parent89d3c1fe1b0fb5db15fce96a7e6db7885ebf240e (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/Makefile1
-rw-r--r--test/boot/fit_verity.c306
-rw-r--r--test/cmd_ut.c2
-rw-r--r--test/py/tests/test_fit_verity.py175
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]('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]('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')