Files
u-boot-krane/test/boot/fit_verity.c
T
Daniel GolleandTom Rini fe9877c7d9 test: fit: verify dm-verity roothash is covered by the config signature
A dm-verity protected filesystem image is not hashed by U-Boot; its
integrity is delegated to the kernel, which trusts the roothash taken
from the FIT dm-verity subnode. For that chain of trust to hold, the
roothash (and salt) must be part of the region covered by the
configuration signature, otherwise an attacker can replace both the
filesystem and the roothash while keeping the signature valid.

Add two independent checks of this property:

 - test/py/tests/test_fit_verity_sign.py signs a configuration that
   references a filesystem image carrying a dm-verity subnode, then
   confirms that tampering the roothash or the salt is rejected by
   fit_check_sign. A control that tampers a byte known to be signed
   proves the check can fail. A matching page is added under
   doc/develop/pytest/ so the module documentation is rendered with
   the rest of the generated docs.

 - test/boot/fit_verity.c gains a runtime unit test that builds the
   exact node list the configuration signature is computed over,
   turns it into hashed regions and checks both that the roothash
   bytes fall inside a signed region and that tampering them changes
   the hash. It needs no private key, so it also runs on real devices
   and uses the same hash path a device would.

To let the unit test build the signed-region node list, rename the
config node-list helper to fit_config_get_signed_nodes(), make it
non-static and declare it in image.h.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
Reviewed-by: Simon Glass <sjg@chromium.org>
2026-08-10 12:32:41 -06:00

507 lines
15 KiB
C

// SPDX-License-Identifier: GPL-2.0+
/*
* Tests for FIT dm-verity cmdline generation
*
* Copyright 2026 Daniel Golle <daniel@makrotopia.org>
*/
#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>
#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);
#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 */