summaryrefslogtreecommitdiff
path: root/test/boot
diff options
context:
space:
mode:
Diffstat (limited to 'test/boot')
-rw-r--r--test/boot/Makefile2
-rw-r--r--test/boot/fit_verity.c200
-rw-r--r--test/boot/image.c68
-rw-r--r--test/boot/measurement.c2
-rw-r--r--test/boot/vbe_read_fit.c224
5 files changed, 494 insertions, 2 deletions
diff --git a/test/boot/Makefile b/test/boot/Makefile
index e6aa0ab7d3e..59a87028704 100644
--- a/test/boot/Makefile
+++ b/test/boot/Makefile
@@ -22,7 +22,7 @@ obj-$(CONFIG_$(PHASE_)FIT_VERITY) += fit_verity.o
obj-$(CONFIG_MEASURED_BOOT) += measurement.o
ifdef CONFIG_OF_LIVE
-obj-$(CONFIG_BOOTMETH_VBE_SIMPLE) += vbe_simple.o
+obj-$(CONFIG_BOOTMETH_VBE_SIMPLE) += vbe_simple.o vbe_read_fit.o
endif
obj-$(CONFIG_BOOTMETH_VBE) += vbe_fixup.o
diff --git a/test/boot/fit_verity.c b/test/boot/fit_verity.c
index 7459a9d6f81..4b5db839085 100644
--- a/test/boot/fit_verity.c
+++ b/test/boot/fit_verity.c
@@ -6,6 +6,11 @@
*/
#include <image.h>
+#include <fdt_region.h>
+#include <malloc.h>
+#include <linux/kernel.h>
+#include <linux/libfdt.h>
+#include <u-boot/hash-checksum.h>
#include <test/test.h>
#include <test/ut.h>
@@ -304,3 +309,198 @@ static int fit_verity_test_bad_blocksize(struct unit_test_state *uts)
return 0;
}
FIT_VERITY_TEST(fit_verity_test_bad_blocksize, 0);
+
+#if CONFIG_IS_ENABLED(FIT_SIGNATURE)
+/**
+ * build_signed_verity_fit() - build a FIT with a signable verity config
+ * @buf: output buffer (at least FIT_BUF_SIZE bytes)
+ *
+ * Like build_verity_fit(), but the filesystem image also carries a hash
+ * subnode (required for a configuration to be signable) so the config's
+ * signed-region node list can be built with fit_config_get_signed_nodes().
+ *
+ * Return: configuration node offset, or -ve on error
+ */
+static int build_signed_verity_fit(void *buf)
+{
+ int images_node, confs_node, conf_node, img_node, hash_node, verity_node;
+ fdt32_t val;
+ int ret;
+
+ ret = fdt_create_empty_tree(buf, FIT_BUF_SIZE);
+ if (ret)
+ return ret;
+
+ images_node = fdt_add_subnode(buf, 0, "images");
+ if (images_node < 0)
+ return images_node;
+
+ img_node = fdt_add_subnode(buf, images_node, "rootfs");
+ if (img_node < 0)
+ return img_node;
+ ret = fdt_setprop_string(buf, img_node, FIT_TYPE_PROP, "filesystem");
+ if (ret)
+ return ret;
+
+ hash_node = fdt_add_subnode(buf, img_node, "hash-1");
+ if (hash_node < 0)
+ return hash_node;
+ ret = fdt_setprop_string(buf, hash_node, FIT_ALGO_PROP, "sha256");
+ if (ret)
+ return ret;
+ ret = fdt_setprop(buf, hash_node, FIT_VALUE_PROP, test_digest,
+ sizeof(test_digest));
+ 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;
+ 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;
+
+ 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_string(buf, conf_node, FIT_LOADABLE_PROP, "rootfs");
+ if (ret)
+ return ret;
+
+ return conf_node;
+}
+
+/*
+ * Test: the dm-verity roothash and salt are inside the region covered by the
+ * configuration signature.
+ *
+ * A dm-verity filesystem image is not hashed by U-Boot; its integrity is
+ * delegated to the kernel, which trusts the roothash from the FIT dm-verity
+ * subnode. That roothash must therefore be part of the signed region, so that
+ * an attacker cannot swap both the filesystem and the roothash while keeping
+ * the configuration signature valid.
+ *
+ * This checks the property without a private key, so it also runs on real
+ * devices: it builds the exact node list the signature is computed over
+ * (fit_config_get_signed_nodes), turns it into hashed regions, and verifies both
+ * that the roothash bytes fall inside a region and that tampering them changes
+ * the hash. It uses the same hash path a device would (crypto accelerated where
+ * available).
+ */
+static int fit_verity_test_roothash_signed(struct unit_test_state *uts)
+{
+ char buf[FIT_BUF_SIZE];
+ char *node_inc[32];
+ char path_buf[256];
+ char region_path[256];
+ struct fdt_region fdt_regions[64];
+ struct image_region *region = NULL;
+ int conf_node, verity_node;
+ int count, i, digest_len;
+ const void *digest;
+ ulong digest_off, region_off;
+ bool covered = false;
+ u8 hash_clean[32], hash_tampered[32], hash_control[32];
+
+ conf_node = build_signed_verity_fit(buf);
+ ut_assert(conf_node >= 0);
+
+ verity_node = fdt_path_offset(buf, "/images/rootfs/dm-verity");
+ ut_assert(verity_node >= 0);
+
+ /* Build the node list the configuration signature is computed over. */
+ count = fit_config_get_signed_nodes(buf, conf_node, node_inc,
+ ARRAY_SIZE(node_inc), path_buf,
+ sizeof(path_buf));
+ ut_assert(count > 0);
+
+ /*
+ * Turn the node list into hashed regions. No exclude list is needed:
+ * the excluded properties (data, data-size, data-offset,
+ * data-position) never include the dm-verity digest or salt, so the
+ * coverage answer is the same with or without it.
+ */
+ count = fdt_find_regions(buf, node_inc, count, NULL, 0, fdt_regions,
+ ARRAY_SIZE(fdt_regions) - 1, region_path,
+ sizeof(region_path), 0);
+ ut_assert(count > 0);
+ /* Region array exhausted: mirror the bound fit_config_check_sig() enforces. */
+ ut_assert(count < ARRAY_SIZE(fdt_regions) - 1);
+
+ region = fit_region_make_list(buf, fdt_regions, count, NULL);
+ ut_assertnonnull(region);
+
+ digest = fdt_getprop(buf, verity_node, FIT_VERITY_DIGEST_PROP,
+ &digest_len);
+ ut_assertnonnull(digest);
+ ut_assert(digest_len > 0);
+ digest_off = (ulong)((const char *)digest - (const char *)buf);
+
+ /*
+ * Control: the hash covers a non-empty region and reacts to a change
+ * inside it. Flip a byte of the (signed) image hash value and confirm
+ * the computed hash differs, proving the region set and hash work.
+ */
+ ut_assertok(hash_calculate("sha256", region, count, hash_clean));
+ for (i = 0; i < count; i++) {
+ region_off = (ulong)((const char *)region[i].data -
+ (const char *)buf);
+ if (digest_off >= region_off &&
+ digest_off + digest_len <= region_off + region[i].size) {
+ covered = true;
+ break;
+ }
+ }
+
+ /* The roothash must be covered by the configuration signature. */
+ ut_assert(covered);
+
+ /*
+ * Tampering the roothash must change the signed hash. Only the digest
+ * is flipped here; salt sits in the same dm-verity node, so coverage
+ * of one implies coverage of the other.
+ */
+ buf[digest_off] ^= 0xff;
+ ut_assertok(hash_calculate("sha256", region, count, hash_tampered));
+ buf[digest_off] ^= 0xff;
+ ut_assert(memcmp(hash_clean, hash_tampered, sizeof(hash_clean)) != 0);
+
+ /* Sanity: with the byte restored the hash matches the clean value. */
+ ut_assertok(hash_calculate("sha256", region, count, hash_control));
+ ut_asserteq_mem(hash_clean, hash_control, sizeof(hash_clean));
+
+ free(region);
+ return 0;
+}
+FIT_VERITY_TEST(fit_verity_test_roothash_signed, 0);
+#endif /* FIT_SIGNATURE */
diff --git a/test/boot/image.c b/test/boot/image.c
index 4df7b17ce88..2c6d9dcbc22 100644
--- a/test/boot/image.c
+++ b/test/boot/image.c
@@ -8,8 +8,76 @@
#include <image.h>
#include <test/ut.h>
+#include <linux/libfdt.h>
#include "bootstd_common.h"
+/* Test that the default configuration breaks best-match ties */
+static int test_fit_conf_find_compat(struct unit_test_state *uts)
+{
+ char fdt[256], fit[1024];
+ int confs, images, node;
+ int ret;
+
+ /* control devicetree with a two-entry compatible list */
+ ut_assertok(fdt_create_empty_tree(fdt, sizeof(fdt)));
+ ut_assertok(fdt_appendprop_string(fdt, 0, "compatible",
+ "test,board-a"));
+ ut_assertok(fdt_appendprop_string(fdt, 0, "compatible",
+ "test,fallback"));
+
+ /* FIT with two configurations matching the same compatible */
+ ut_assertok(fdt_create_empty_tree(fit, sizeof(fit)));
+ images = fdt_add_subnode(fit, 0, "images");
+ ut_assert(images >= 0);
+ confs = fdt_add_subnode(fit, 0, "configurations");
+ ut_assert(confs >= 0);
+ ut_assertok(fdt_setprop_string(fit, confs, FIT_DEFAULT_PROP, "conf-2"));
+ /*
+ * fdt_add_subnode() inserts before existing subnodes: create conf-2
+ * first so that conf-1 ends up listed first, like an .its compiled
+ * with the configurations in that order
+ */
+ node = fdt_add_subnode(fit, confs, "conf-2");
+ ut_assert(node >= 0);
+ ut_assertok(fdt_setprop_string(fit, node, "compatible",
+ "test,board-a"));
+ node = fdt_add_subnode(fit, confs, "conf-1");
+ ut_assert(node >= 0);
+ ut_assertok(fdt_setprop_string(fit, node, "compatible",
+ "test,board-a"));
+ confs = fdt_path_offset(fit, "/configurations");
+ node = fdt_first_subnode(fit, confs);
+ ut_asserteq_str("conf-1", fdt_get_name(fit, node, NULL));
+
+ /* on a tie, the default configuration wins */
+ ret = fit_conf_find_compat(fit, fdt);
+ ut_assert(ret > 0);
+ ut_asserteq_str("conf-2", fdt_get_name(fit, ret, NULL));
+
+ /* without a default, the first listed configuration wins */
+ confs = fdt_path_offset(fit, "/configurations");
+ ut_assertok(fdt_delprop(fit, confs, FIT_DEFAULT_PROP));
+ confs = fdt_path_offset(fit, "/configurations");
+ ut_assertnull((void *)fdt_getprop(fit, confs, FIT_DEFAULT_PROP, NULL));
+ ret = fit_conf_find_compat(fit, fdt);
+ ut_assert(ret > 0);
+ ut_asserteq_str("conf-1", fdt_get_name(fit, ret, NULL));
+
+ /* a strictly better match still beats the default */
+ confs = fdt_path_offset(fit, "/configurations");
+ ut_assertok(fdt_setprop_string(fit, confs, FIT_DEFAULT_PROP, "conf-2"));
+ confs = fdt_path_offset(fit, "/configurations");
+ node = fdt_subnode_offset(fit, confs, "conf-2");
+ ut_assertok(fdt_setprop_string(fit, node, "compatible",
+ "test,fallback"));
+ ret = fit_conf_find_compat(fit, fdt);
+ ut_assert(ret > 0);
+ ut_asserteq_str("conf-1", fdt_get_name(fit, ret, NULL));
+
+ return 0;
+}
+BOOTSTD_TEST(test_fit_conf_find_compat, 0);
+
/* Test of image phase */
static int test_image_phase(struct unit_test_state *uts)
{
diff --git a/test/boot/measurement.c b/test/boot/measurement.c
index 71f503f1567..85a01f1fec8 100644
--- a/test/boot/measurement.c
+++ b/test/boot/measurement.c
@@ -9,9 +9,9 @@
#include <bootm.h>
#include <env.h>
#include <malloc.h>
+#include <mapmem.h>
#include <test/test.h>
#include <test/ut.h>
-#include <asm/io.h>
#define MEASUREMENT_TEST(_name, _flags) \
UNIT_TEST(_name, _flags, measurement)
diff --git a/test/boot/vbe_read_fit.c b/test/boot/vbe_read_fit.c
new file mode 100644
index 00000000000..f67de0e7165
--- /dev/null
+++ b/test/boot/vbe_read_fit.c
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Bounds-check tests for vbe_read_fit()
+ *
+ * vbe_read_fit() pulls a firmware-phase FIT from a trusted firmware area
+ * on a block device. The external-data location and size carried in the
+ * FIT image node are attacker-controllable when the firmware area is on
+ * mutable boot media, so vbe_read_fit() must reject FITs whose external
+ * data extends past @area_size before issuing the follow-up blk_read().
+ *
+ * These tests build small synthetic FITs with deliberately out-of-range
+ * values and confirm vbe_read_fit() returns -E2BIG for each.
+ *
+ * Copyright 2026 Canonical Ltd.
+ * Written by Aristo Chen <[email protected]>
+ */
+
+#include <blk.h>
+#include <dm.h>
+#include <image.h>
+#include <memalign.h>
+#include <mmc.h>
+#include <test/test.h>
+#include <test/ut.h>
+#include <linux/libfdt.h>
+#include "bootstd_common.h"
+#include "../../boot/vbe_common.h"
+
+/*
+ * The synthetic FIT is written to mmc1 starting at block TEST_FIT_BLK.
+ * bootstd_setup_for_tests() uses blocks 4 and 6 (see bootstd_common.h);
+ * block 16 leaves a comfortable gap.
+ */
+#define TEST_FIT_BLK 16
+#define TEST_FIT_OFF ((ulong)TEST_FIT_BLK * MMC_MAX_BLOCK_LEN)
+#define TEST_AREA_SIZE 0x1000
+
+/**
+ * build_fit() - Build a minimal external-data FIT for vbe_read_fit()
+ *
+ * The FIT advertises a single firmware image whose @data-position and
+ * @data-size are passed in directly. Both values are attacker-controlled
+ * in the real threat model.
+ *
+ * @buf: Destination buffer (must be at least 512 bytes)
+ * @buf_size: Size of @buf
+ * @data_position: Value written to the image's data-position property
+ * @data_size: Value written to the image's data-size property
+ * Returns: 0 on success, libfdt error otherwise
+ */
+static int build_fit(void *buf, size_t buf_size, u32 data_position,
+ u32 data_size)
+{
+ int ret;
+
+ ret = fdt_create(buf, buf_size);
+ if (ret)
+ return ret;
+ ret = fdt_finish_reservemap(buf);
+ if (ret)
+ return ret;
+
+ ret = fdt_begin_node(buf, "");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_DESC_PROP, "vbe-read-fit test");
+ if (ret)
+ return ret;
+ ret = fdt_property_u32(buf, FIT_TIMESTAMP_PROP, 0);
+ if (ret)
+ return ret;
+
+ ret = fdt_begin_node(buf, "images");
+ if (ret)
+ return ret;
+ ret = fdt_begin_node(buf, "u-boot");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_DESC_PROP, "U-Boot");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_TYPE_PROP, "firmware");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_ARCH_PROP, "sandbox");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_OS_PROP, "u-boot");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_PHASE_PROP, "u-boot");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_COMP_PROP, "none");
+ if (ret)
+ return ret;
+ ret = fdt_property_u32(buf, FIT_DATA_POSITION_PROP, data_position);
+ if (ret)
+ return ret;
+ ret = fdt_property_u32(buf, FIT_DATA_SIZE_PROP, data_size);
+ if (ret)
+ return ret;
+ ret = fdt_end_node(buf); /* u-boot */
+ if (ret)
+ return ret;
+ ret = fdt_end_node(buf); /* images */
+ if (ret)
+ return ret;
+
+ ret = fdt_begin_node(buf, "configurations");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_DEFAULT_PROP, "conf-1");
+ if (ret)
+ return ret;
+ ret = fdt_begin_node(buf, "conf-1");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, "compatible", "sandbox");
+ if (ret)
+ return ret;
+ ret = fdt_property_string(buf, FIT_FIRMWARE_PROP, "u-boot");
+ if (ret)
+ return ret;
+ ret = fdt_end_node(buf); /* conf-1 */
+ if (ret)
+ return ret;
+ ret = fdt_end_node(buf); /* configurations */
+ if (ret)
+ return ret;
+
+ ret = fdt_end_node(buf); /* root */
+ if (ret)
+ return ret;
+
+ return fdt_finish(buf);
+}
+
+/**
+ * place_fit_on_mmc() - Write a synthetic FIT to mmc1 and return its blk dev
+ *
+ * @uts: Unit test state
+ * @fit: FIT image to write
+ * @blkp: On success, receives the block udevice for mmc1
+ * Returns: 0 on success, -ve on error
+ */
+static int place_fit_on_mmc(struct unit_test_state *uts, const void *fit,
+ struct udevice **blkp)
+{
+ ALLOC_CACHE_ALIGN_BUFFER(u8, blkbuf, MMC_MAX_BLOCK_LEN);
+ struct udevice *mmc;
+ struct blk_desc *desc;
+ size_t fit_size = fdt_totalsize(fit);
+ size_t pos;
+ int blknum = TEST_FIT_BLK;
+
+ ut_assertok(uclass_get_device(UCLASS_MMC, 1, &mmc));
+ desc = blk_get_by_device(mmc);
+ if (!desc)
+ return log_msg_ret("desc", -ENODEV);
+
+ for (pos = 0; pos < fit_size; pos += MMC_MAX_BLOCK_LEN, blknum++) {
+ size_t this_blk = min(fit_size - pos,
+ (size_t)MMC_MAX_BLOCK_LEN);
+
+ memset(blkbuf, '\0', MMC_MAX_BLOCK_LEN);
+ memcpy(blkbuf, (const u8 *)fit + pos, this_blk);
+ if (blk_dwrite(desc, blknum, 1, blkbuf) != 1)
+ return log_msg_ret("wr", -EIO);
+ }
+ *blkp = desc->bdev;
+
+ return 0;
+}
+
+/*
+ * data-position points past area_size: vbe_read_fit() must reject the
+ * FIT with -E2BIG before issuing the external-data blk_read().
+ */
+static int vbe_read_fit_oob_position(struct unit_test_state *uts)
+{
+ u8 fit[1024] __aligned(8);
+ struct udevice *blk;
+ ulong load_addr = 0, len = 0;
+ char *name = NULL;
+ int ret;
+
+ ut_assertok(build_fit(fit, sizeof(fit),
+ TEST_AREA_SIZE + 0x10, 0x40));
+ ut_assertok(place_fit_on_mmc(uts, fit, &blk));
+
+ ret = vbe_read_fit(blk, TEST_FIT_OFF, TEST_AREA_SIZE,
+ NULL, &load_addr, &len, &name);
+ ut_asserteq(-E2BIG, ret);
+
+ return 0;
+}
+
+BOOTSTD_TEST(vbe_read_fit_oob_position, UTF_DM | UTF_SCAN_FDT);
+
+/*
+ * data-position is inside the area but data-size pushes the end past
+ * area_size: vbe_read_fit() must reject the FIT with -E2BIG.
+ */
+static int vbe_read_fit_oversize_data(struct unit_test_state *uts)
+{
+ u8 fit[1024] __aligned(8);
+ struct udevice *blk;
+ ulong load_addr = 0, len = 0;
+ char *name = NULL;
+ int ret;
+
+ ut_assertok(build_fit(fit, sizeof(fit),
+ 0x400, TEST_AREA_SIZE));
+ ut_assertok(place_fit_on_mmc(uts, fit, &blk));
+
+ ret = vbe_read_fit(blk, TEST_FIT_OFF, TEST_AREA_SIZE,
+ NULL, &load_addr, &len, &name);
+ ut_asserteq(-E2BIG, ret);
+
+ return 0;
+}
+
+BOOTSTD_TEST(vbe_read_fit_oversize_data, UTF_DM | UTF_SCAN_FDT);