diff options
Diffstat (limited to 'test/boot')
| -rw-r--r-- | test/boot/Makefile | 2 | ||||
| -rw-r--r-- | test/boot/fit_verity.c | 200 | ||||
| -rw-r--r-- | test/boot/image.c | 68 | ||||
| -rw-r--r-- | test/boot/measurement.c | 2 | ||||
| -rw-r--r-- | test/boot/vbe_read_fit.c | 224 |
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); |
