Merge patch series "boot: fit: authenticate the dm-verity roothash"

Daniel Golle <daniel@makrotopia.org> says:

A signed FIT configuration can delegate the integrity of a (potentially
large) root filesystem image to the kernel's dm-verity instead of having
U-Boot hash the whole payload at boot: the FIT carries a "dm-verity"
subnode with the roothash, salt and block parameters, U-Boot passes the
roothash to Linux through the dm-mod.create bootargs, and dm-verity then
validates the filesystem block by block against it.

For that to be safe the roothash has to be trusted, and in a signed
configuration the only thing that establishes trust is the configuration
signature. The roothash was not covered by it. fit_config_add_hash()
collected the image node, its hash subnodes and its cipher subnode into
the signed region, but not the dm-verity subnode, so the roothash, the
sole integrity anchor for the filesystem, was left unsigned.

The result is a verified-boot bypass for the root filesystem: an
attacker who can rewrite the boot medium can replace the filesystem,
recompute a matching dm-verity tree, write the new roothash into the
unsigned dm-verity subnode, and the configuration signature still
verifies. dm-verity then faithfully validates the malicious filesystem
against the attacker's roothash.

This series closes the gap.

Link: https://lore.kernel.org/r/cover.1785276461.git.daniel@makrotopia.org
This commit is contained in:
Tom Rini
2026-08-10 12:37:16 -06:00
8 changed files with 580 additions and 67 deletions
+70 -40
View File
@@ -230,11 +230,42 @@ int fit_image_verify_required_sigs(const void *fit, int image_noffset,
return 0;
}
/**
* fit_config_add_node() - Append one node's path to the hashed-node list
*
* @fit: FIT blob
* @noffset: Offset of the node whose path should be added
* @node_inc: Array of path pointers to fill
* @count: Pointer to current count (updated on return)
* @max_nodes: Maximum entries in @node_inc
* @buf: Buffer for packed path strings
* @buf_used: Pointer to bytes used in @buf (updated on return)
* @buf_len: Total size of @buf
* Return: 0 on success, -ve on error
*/
static int fit_config_add_node(const void *fit, int noffset, char **node_inc,
int *count, int max_nodes, char *buf,
int *buf_used, int buf_len)
{
int ret, len;
if (*count >= max_nodes)
return -ENOSPC;
ret = fdt_get_path(fit, noffset, buf + *buf_used, buf_len - *buf_used);
if (ret < 0)
return -ENOENT;
len = strlen(buf + *buf_used) + 1;
node_inc[(*count)++] = buf + *buf_used;
*buf_used += len;
return 0;
}
/**
* fit_config_add_hash() - Add hash nodes for one image to the node list
*
* Adds the image path, all its hash-* subnode paths, and its cipher
* subnode path (if present) to the packed buffer.
* Adds the image path, all its hash-* subnode paths, and its cipher and
* dm-verity subnode paths (each if present) to the packed buffer.
*
* @fit: FIT blob
* @image_noffset: Image node offset (e.g. /images/kernel-1)
@@ -250,18 +281,12 @@ static int fit_config_add_hash(const void *fit, int image_noffset,
char **node_inc, int *count, int max_nodes,
char *buf, int *buf_used, int buf_len)
{
int noffset, hash_count, ret, len;
int noffset, hash_count, ret;
if (*count >= max_nodes)
return -ENOSPC;
ret = fdt_get_path(fit, image_noffset, buf + *buf_used,
buf_len - *buf_used);
if (ret < 0)
return -ENOENT;
len = strlen(buf + *buf_used) + 1;
node_inc[(*count)++] = buf + *buf_used;
*buf_used += len;
ret = fit_config_add_node(fit, image_noffset, node_inc, count,
max_nodes, buf, buf_used, buf_len);
if (ret)
return ret;
/* Add all this image's hash subnodes */
hash_count = 0;
@@ -273,15 +298,10 @@ static int fit_config_add_hash(const void *fit, int image_noffset,
if (strncmp(name, FIT_HASH_NODENAME,
strlen(FIT_HASH_NODENAME)))
continue;
if (*count >= max_nodes)
return -ENOSPC;
ret = fdt_get_path(fit, noffset, buf + *buf_used,
buf_len - *buf_used);
if (ret < 0)
return -ENOENT;
len = strlen(buf + *buf_used) + 1;
node_inc[(*count)++] = buf + *buf_used;
*buf_used += len;
ret = fit_config_add_node(fit, noffset, node_inc, count,
max_nodes, buf, buf_used, buf_len);
if (ret)
return ret;
hash_count++;
}
@@ -296,26 +316,36 @@ static int fit_config_add_hash(const void *fit, int image_noffset,
if (noffset != -FDT_ERR_NOTFOUND) {
if (noffset < 0)
return -EIO;
if (*count >= max_nodes)
return -ENOSPC;
ret = fdt_get_path(fit, noffset, buf + *buf_used,
buf_len - *buf_used);
if (ret < 0)
return -ENOENT;
len = strlen(buf + *buf_used) + 1;
node_inc[(*count)++] = buf + *buf_used;
*buf_used += len;
ret = fit_config_add_node(fit, noffset, node_inc, count,
max_nodes, buf, buf_used, buf_len);
if (ret)
return ret;
}
/*
* Add this image's dm-verity node if present. Its roothash is the
* only integrity anchor for a dm-verity filesystem image, so it must
* be covered by the configuration signature.
*/
noffset = fdt_subnode_offset(fit, image_noffset, FIT_VERITY_NODENAME);
if (noffset != -FDT_ERR_NOTFOUND) {
if (noffset < 0)
return -EIO;
ret = fit_config_add_node(fit, noffset, node_inc, count,
max_nodes, buf, buf_used, buf_len);
if (ret)
return ret;
}
return 0;
}
/**
* fit_config_get_hash_list() - Build the list of nodes to hash
* fit_config_get_signed_nodes() - Build the list of nodes to hash
*
* Works through every image referenced by the configuration and collects the
* node paths: root + config + all referenced images with their hash and
* cipher subnodes.
* node paths: root + config + all referenced images with their hash,
* cipher and dm-verity subnodes.
*
* Properties known not to be image references (description, compatible,
* default, load-only) are skipped, so any new image type is covered by default.
@@ -328,9 +358,9 @@ static int fit_config_add_hash(const void *fit, int image_noffset,
* @buf_len: Size of @buf
* Return: number of entries in @node_inc, or -ve on error
*/
static int fit_config_get_hash_list(const void *fit, int conf_noffset,
char **node_inc, int max_nodes,
char *buf, int buf_len)
int fit_config_get_signed_nodes(const void *fit, int conf_noffset,
char **node_inc, int max_nodes,
char *buf, int buf_len)
{
const char *conf_name;
int image_count;
@@ -470,9 +500,9 @@ static int fit_config_check_sig(const void *fit, int noffset, int conf_noffset,
}
/* Build the node list from the config, ignoring hashed-nodes */
count = fit_config_get_hash_list(fit, conf_noffset,
node_inc, IMAGE_MAX_HASHED_NODES,
hash_buf, sizeof(hash_buf));
count = fit_config_get_signed_nodes(fit, conf_noffset,
node_inc, IMAGE_MAX_HASHED_NODES,
hash_buf, sizeof(hash_buf));
if (count < 0) {
*err_msgp = "Failed to build hash node list";
return -1;