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:
+70
-40
@@ -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;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.. SPDX-License-Identifier: GPL-2.0+
|
||||
|
||||
test_fit_verity_sign
|
||||
====================
|
||||
|
||||
.. automodule:: test_fit_verity_sign
|
||||
:synopsis:
|
||||
:member-order: bysource
|
||||
:members:
|
||||
:undoc-members:
|
||||
@@ -209,6 +209,11 @@ typically be obtained from its output.
|
||||
The ``digest`` and ``salt`` byte arrays correspond to the hex-encoded
|
||||
``Root hash`` and ``Salt`` printed by ``veritysetup format``.
|
||||
|
||||
When the configuration is signed, ``digest`` and ``salt`` are covered by
|
||||
the configuration signature (see :doc:`signature`), so the roothash
|
||||
cannot be swapped out for a matching one without invalidating the
|
||||
signature.
|
||||
|
||||
Optional boolean properties (when present, they are collected and appended
|
||||
as dm-verity optional parameters with hyphens converted to underscores):
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ however, U-Boot does not read 'hashed-nodes'. Instead it rebuilds the node
|
||||
list from the configuration's own image references (kernel, fdt, ramdisk,
|
||||
etc.), since 'hashed-nodes' is not itself covered by the signature. The
|
||||
rebuilt list always includes the root node, the configuration node, each
|
||||
referenced image node and its hash/cipher subnodes.
|
||||
referenced image node and its hash, cipher and dm-verity subnodes.
|
||||
|
||||
The image is walked in order and each tag processed as follows:
|
||||
|
||||
|
||||
@@ -1897,6 +1897,29 @@ struct image_region *fit_region_make_list(const void *fit,
|
||||
struct fdt_region *fdt_regions, int count,
|
||||
struct image_region *region);
|
||||
|
||||
/**
|
||||
* fit_config_get_signed_nodes() - Build the list of nodes covered by a config
|
||||
* signature
|
||||
*
|
||||
* Collects the paths of the nodes that the configuration signature is
|
||||
* computed over: the root node, the configuration node, and for each image
|
||||
* referenced by the configuration its node, its hash subnodes and its cipher
|
||||
* and dm-verity subnodes. The result is the same node list used when creating
|
||||
* and verifying the signature, and is suitable for passing to
|
||||
* fdt_find_regions().
|
||||
*
|
||||
* @fit: FIT blob
|
||||
* @conf_noffset: Configuration node offset
|
||||
* @node_inc: Array to fill with pointers to packed path strings
|
||||
* @max_nodes: Number of entries in @node_inc
|
||||
* @buf: Buffer for the packed null-terminated path strings
|
||||
* @buf_len: Size of @buf
|
||||
* Return: number of entries written to @node_inc, or -ve on error
|
||||
*/
|
||||
int fit_config_get_signed_nodes(const void *fit, int conf_noffset,
|
||||
char **node_inc, int max_nodes,
|
||||
char *buf, int buf_len);
|
||||
|
||||
static inline int fit_image_check_target_arch(const void *fdt, int node)
|
||||
{
|
||||
#ifndef USE_HOSTCC
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
# Copyright 2026 Daniel Golle <daniel@makrotopia.org>
|
||||
|
||||
"""Verify that the dm-verity roothash is covered by the FIT configuration
|
||||
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. That roothash must therefore be part of the signed
|
||||
region of the configuration, otherwise an attacker can replace both the
|
||||
filesystem and the roothash while keeping the configuration signature valid.
|
||||
|
||||
This test signs a configuration referencing a filesystem image that carries a
|
||||
``dm-verity`` subnode, then flips one byte of the roothash and of the salt and
|
||||
checks that verification rejects the image. A control tampering a byte that is
|
||||
known to be signed confirms that the check is able to detect a broken region.
|
||||
|
||||
The FIT pairs a signed configuration with a filesystem image carrying a
|
||||
``dm-verity`` subnode:
|
||||
|
||||
.. code-block:: devicetree
|
||||
|
||||
images {
|
||||
rootfs-1 {
|
||||
data = /incbin/("rootfs.bin");
|
||||
type = "filesystem";
|
||||
compression = "none";
|
||||
hash-1 {
|
||||
algo = "sha256";
|
||||
};
|
||||
dm-verity {
|
||||
algo = "sha256";
|
||||
data-block-size = <4096>;
|
||||
hash-block-size = <4096>;
|
||||
num-data-blocks = <16>;
|
||||
hash-start-block = <16>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
configurations {
|
||||
conf-1 {
|
||||
kernel = "kernel-1";
|
||||
loadables = "rootfs-1";
|
||||
signature-1 {
|
||||
algo = "sha256,rsa2048";
|
||||
key-name-hint = "dev";
|
||||
sign-images = "kernel", "loadables";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
mkimage builds the dm-verity hash tree when assembling the image and records
|
||||
the resulting roothash and salt in the ``dm-verity`` subnode; fit_check_sign
|
||||
must reject an image where either was modified after signing.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import utils
|
||||
|
||||
# 16 blocks of 4096 bytes, matching num-data-blocks/data-block-size below.
|
||||
ROOTFS_SIZE = 16 * 4096
|
||||
|
||||
ITS = '''
|
||||
/dts-v1/;
|
||||
/ {
|
||||
description = "verity roothash signing coverage test";
|
||||
#address-cells = <1>;
|
||||
|
||||
images {
|
||||
kernel-1 {
|
||||
description = "kernel";
|
||||
data = /incbin/("kernel.bin");
|
||||
type = "kernel";
|
||||
arch = "arm64";
|
||||
os = "linux";
|
||||
compression = "none";
|
||||
load = <0x40000000>;
|
||||
entry = <0x40000000>;
|
||||
hash-1 { algo = "sha256"; };
|
||||
};
|
||||
rootfs-1 {
|
||||
description = "rootfs";
|
||||
data = /incbin/("rootfs.bin");
|
||||
type = "filesystem";
|
||||
arch = "arm64";
|
||||
compression = "none";
|
||||
hash-1 { algo = "sha256"; };
|
||||
dm-verity {
|
||||
algo = "sha256";
|
||||
data-block-size = <4096>;
|
||||
hash-block-size = <4096>;
|
||||
num-data-blocks = <16>;
|
||||
hash-start-block = <16>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
configurations {
|
||||
default = "conf-1";
|
||||
conf-1 {
|
||||
description = "signed config";
|
||||
kernel = "kernel-1";
|
||||
loadables = "rootfs-1";
|
||||
signature-1 {
|
||||
algo = "sha256,rsa2048";
|
||||
key-name-hint = "dev";
|
||||
sign-images = "kernel", "loadables";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
'''
|
||||
|
||||
VERITY_NODE = '/images/rootfs-1/dm-verity'
|
||||
ROOTFS_HASH_NODE = '/images/rootfs-1/hash-1'
|
||||
|
||||
|
||||
def flip_prop_byte(ubman, fit, node, prop):
|
||||
"""Flip the first byte of a byte-array property in a FIT, in place.
|
||||
|
||||
The property is rewritten with the same length so that no node is
|
||||
relaid out and the signed regions keep their offsets.
|
||||
"""
|
||||
val = utils.run_and_log(ubman, 'fdtget -t bx %s %s %s' % (fit, node, prop))
|
||||
bytelist = val.split()
|
||||
bytelist[0] = '%x' % (int(bytelist[0], 16) ^ 0xff)
|
||||
utils.run_and_log(ubman, 'fdtput -t bx %s %s %s %s' %
|
||||
(fit, node, prop, ' '.join(bytelist)))
|
||||
|
||||
|
||||
@pytest.mark.boardspec('sandbox')
|
||||
@pytest.mark.buildconfigspec('fit_signature')
|
||||
@pytest.mark.requiredtool('dtc')
|
||||
@pytest.mark.requiredtool('fdtget')
|
||||
@pytest.mark.requiredtool('fdtput')
|
||||
@pytest.mark.requiredtool('openssl')
|
||||
@pytest.mark.requiredtool('veritysetup')
|
||||
def test_fit_verity_roothash_signed(ubman):
|
||||
"""The dm-verity roothash must be inside the signed configuration region."""
|
||||
tmpdir = os.path.join(ubman.config.result_dir, 'verity-sign') + '/'
|
||||
if not os.path.exists(tmpdir):
|
||||
os.makedirs(tmpdir)
|
||||
mkimage = ubman.config.build_dir + '/tools/mkimage'
|
||||
fit_check_sign = ubman.config.build_dir + '/tools/fit_check_sign'
|
||||
dtc_args = '-I dts -O dtb -i %s' % tmpdir
|
||||
its = tmpdir + 'verity.its'
|
||||
fit = tmpdir + 'verity.itb'
|
||||
dtb = tmpdir + 'control.dtb'
|
||||
|
||||
# Signing key and empty control dtb to receive the public key.
|
||||
utils.run_and_log(ubman, 'openssl genpkey -algorithm RSA -out %sdev.key '
|
||||
'-pkeyopt rsa_keygen_bits:2048 '
|
||||
'-pkeyopt rsa_keygen_pubexp:65537' % tmpdir)
|
||||
utils.run_and_log(ubman, 'openssl req -batch -new -x509 -key %sdev.key '
|
||||
'-out %sdev.crt' % (tmpdir, tmpdir))
|
||||
with open(tmpdir + 'control.dts', 'w') as f:
|
||||
f.write('/dts-v1/; / { model = "verity-test"; };\n')
|
||||
utils.run_and_log(ubman, 'dtc -O dtb -o %s %scontrol.dts' % (dtb, tmpdir))
|
||||
|
||||
# Payloads. The rootfs must be a whole number of data blocks so mkimage can
|
||||
# build the dm-verity hash tree and compute the roothash.
|
||||
with open(tmpdir + 'rootfs.bin', 'wb') as f:
|
||||
f.write(b'R' * ROOTFS_SIZE)
|
||||
with open(tmpdir + 'kernel.bin', 'wb') as f:
|
||||
f.write(b'KERNEL')
|
||||
|
||||
with open(its, 'w') as f:
|
||||
f.write(ITS)
|
||||
|
||||
# Build and sign. -E keeps the (large) rootfs external, as on a real device.
|
||||
utils.run_and_log(ubman, [mkimage, '-D', dtc_args, '-E', '-f', its,
|
||||
'-k', tmpdir, '-K', dtb, '-r', fit])
|
||||
|
||||
# Baseline: the freshly signed image must verify.
|
||||
utils.run_and_log(ubman, [fit_check_sign, '-f', fit, '-k', dtb])
|
||||
|
||||
# Control: tampering a byte that is signed (the filesystem image hash value)
|
||||
# must be detected. This proves the check can fail.
|
||||
control = tmpdir + 'control.itb'
|
||||
utils.run_and_log(ubman, 'cp %s %s' % (fit, control))
|
||||
flip_prop_byte(ubman, control, ROOTFS_HASH_NODE, 'value')
|
||||
utils.run_and_log_expect_exception(
|
||||
ubman, [fit_check_sign, '-f', control, '-k', dtb],
|
||||
1, 'Failed to verify required signature')
|
||||
|
||||
# Roothash: tampering the dm-verity digest must be rejected. If the digest
|
||||
# is outside the signed region this check passes and boot is compromised.
|
||||
tampered = tmpdir + 'tamper-digest.itb'
|
||||
utils.run_and_log(ubman, 'cp %s %s' % (fit, tampered))
|
||||
flip_prop_byte(ubman, tampered, VERITY_NODE, 'digest')
|
||||
utils.run_and_log_expect_exception(
|
||||
ubman, [fit_check_sign, '-f', tampered, '-k', dtb],
|
||||
1, 'Failed to verify required signature')
|
||||
|
||||
# Salt: likewise, the salt feeds the dm-verity target and must be signed.
|
||||
tampered = tmpdir + 'tamper-salt.itb'
|
||||
utils.run_and_log(ubman, 'cp %s %s' % (fit, tampered))
|
||||
flip_prop_byte(ubman, tampered, VERITY_NODE, 'salt')
|
||||
utils.run_and_log_expect_exception(
|
||||
ubman, [fit_check_sign, '-f', tampered, '-k', dtb],
|
||||
1, 'Failed to verify required signature')
|
||||
+68
-26
@@ -1183,6 +1183,41 @@ static const char *fit_config_get_image_list(const void *fit, int noffset,
|
||||
return default_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* fit_config_add_node() - Add a node's path to a list of nodes to hash
|
||||
*
|
||||
* @fit: Pointer to the FIT format image header
|
||||
* @noffset: Offset of the node whose path should be added
|
||||
* @node_inc: List of nodes to add to
|
||||
* @conf_name Configuration-node name, child of /configurations node (only
|
||||
* used for error messages)
|
||||
* @sig_name Signature-node name (only used for error messages)
|
||||
* @iname: Name of image being processed (e.g. "kernel-1" (only used
|
||||
* for error messages)
|
||||
*/
|
||||
static int fit_config_add_node(const void *fit, int noffset,
|
||||
struct strlist *node_inc, const char *conf_name,
|
||||
const char *sig_name, const char *iname)
|
||||
{
|
||||
char path[200];
|
||||
int ret;
|
||||
|
||||
ret = fdt_get_path(fit, noffset, path, sizeof(path));
|
||||
if (ret < 0) {
|
||||
fprintf(stderr,
|
||||
"Failed to get path for image '%s' in configuration '%s/%s': %s\n",
|
||||
iname, conf_name, sig_name, fdt_strerror(ret));
|
||||
return -ENOENT;
|
||||
}
|
||||
if (strlist_add(node_inc, path)) {
|
||||
fprintf(stderr, "Out of memory processing configuration '%s/%s'\n",
|
||||
conf_name, sig_name);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* fit_config_add_hash() - Add a list of nodes to hash for an image
|
||||
*
|
||||
@@ -1202,16 +1237,14 @@ static int fit_config_add_hash(const void *fit, int image_noffset,
|
||||
struct strlist *node_inc, const char *conf_name,
|
||||
const char *sig_name, const char *iname)
|
||||
{
|
||||
char path[200];
|
||||
int noffset;
|
||||
int hash_count;
|
||||
int ret;
|
||||
|
||||
ret = fdt_get_path(fit, image_noffset, path, sizeof(path));
|
||||
if (ret < 0)
|
||||
goto err_path;
|
||||
if (strlist_add(node_inc, path))
|
||||
goto err_mem;
|
||||
ret = fit_config_add_node(fit, image_noffset, node_inc, conf_name,
|
||||
sig_name, iname);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
/* Add all this image's hashes */
|
||||
hash_count = 0;
|
||||
@@ -1223,11 +1256,10 @@ static int fit_config_add_hash(const void *fit, int image_noffset,
|
||||
if (strncmp(name, FIT_HASH_NODENAME,
|
||||
strlen(FIT_HASH_NODENAME)))
|
||||
continue;
|
||||
ret = fdt_get_path(fit, noffset, path, sizeof(path));
|
||||
if (ret < 0)
|
||||
goto err_path;
|
||||
if (strlist_add(node_inc, path))
|
||||
goto err_mem;
|
||||
ret = fit_config_add_node(fit, noffset, node_inc, conf_name,
|
||||
sig_name, iname);
|
||||
if (ret)
|
||||
return ret;
|
||||
hash_count++;
|
||||
}
|
||||
|
||||
@@ -1249,24 +1281,34 @@ static int fit_config_add_hash(const void *fit, int image_noffset,
|
||||
fdt_strerror(noffset));
|
||||
return -EIO;
|
||||
}
|
||||
ret = fdt_get_path(fit, noffset, path, sizeof(path));
|
||||
if (ret < 0)
|
||||
goto err_path;
|
||||
if (strlist_add(node_inc, path))
|
||||
goto err_mem;
|
||||
ret = fit_config_add_node(fit, noffset, node_inc, conf_name,
|
||||
sig_name, iname);
|
||||
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) {
|
||||
fprintf(stderr,
|
||||
"Failed to get dm-verity node in configuration '%s/%s' image '%s': %s\n",
|
||||
conf_name, sig_name, iname,
|
||||
fdt_strerror(noffset));
|
||||
return -EIO;
|
||||
}
|
||||
ret = fit_config_add_node(fit, noffset, node_inc, conf_name,
|
||||
sig_name, iname);
|
||||
if (ret)
|
||||
return ret;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
err_mem:
|
||||
fprintf(stderr, "Out of memory processing configuration '%s/%s'\n", conf_name,
|
||||
sig_name);
|
||||
return -ENOMEM;
|
||||
|
||||
err_path:
|
||||
fprintf(stderr, "Failed to get path for image '%s' in configuration '%s/%s': %s\n",
|
||||
iname, conf_name, sig_name, fdt_strerror(ret));
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user