summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--boot/image-fit-sig.c108
-rw-r--r--doc/develop/pytest/test_fit_verity_sign.rst10
-rw-r--r--doc/usage/fit/dm-verity.rst5
-rw-r--r--doc/usage/fit/signature.rst2
-rw-r--r--include/image.h23
-rw-r--r--test/boot/fit_verity.c200
-rw-r--r--test/py/tests/test_fit_verity_sign.py203
-rw-r--r--tools/image-host.c94
8 files changed, 579 insertions, 66 deletions
diff --git a/boot/image-fit-sig.c b/boot/image-fit-sig.c
index fe7ca6e4ab5..a0c50bba4cf 100644
--- a/boot/image-fit-sig.c
+++ b/boot/image-fit-sig.c
@@ -231,13 +231,10 @@ int fit_image_verify_required_sigs(const void *fit, int image_noffset,
}
/**
- * 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.
+ * fit_config_add_node() - Append one node's path to the hashed-node list
*
* @fit: FIT blob
- * @image_noffset: Image node offset (e.g. /images/kernel-1)
+ * @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
@@ -246,23 +243,51 @@ int fit_image_verify_required_sigs(const void *fit, int image_noffset,
* @buf_len: Total size of @buf
* Return: 0 on success, -ve on error
*/
-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)
+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 noffset, hash_count, ret, len;
+ int ret, len;
if (*count >= max_nodes)
return -ENOSPC;
-
- ret = fdt_get_path(fit, image_noffset, buf + *buf_used,
- buf_len - *buf_used);
+ 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 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)
+ * @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_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;
+
+ 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;
for (noffset = fdt_first_subnode(fit, image_noffset);
@@ -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;
diff --git a/doc/develop/pytest/test_fit_verity_sign.rst b/doc/develop/pytest/test_fit_verity_sign.rst
new file mode 100644
index 00000000000..94a39b08952
--- /dev/null
+++ b/doc/develop/pytest/test_fit_verity_sign.rst
@@ -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:
diff --git a/doc/usage/fit/dm-verity.rst b/doc/usage/fit/dm-verity.rst
index 800a18fceae..76030c751ae 100644
--- a/doc/usage/fit/dm-verity.rst
+++ b/doc/usage/fit/dm-verity.rst
@@ -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):
diff --git a/doc/usage/fit/signature.rst b/doc/usage/fit/signature.rst
index da08cc75c3a..64bada2f58f 100644
--- a/doc/usage/fit/signature.rst
+++ b/doc/usage/fit/signature.rst
@@ -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:
diff --git a/include/image.h b/include/image.h
index 4149ebbcce9..6edcb1995bf 100644
--- a/include/image.h
+++ b/include/image.h
@@ -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
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/py/tests/test_fit_verity_sign.py b/test/py/tests/test_fit_verity_sign.py
new file mode 100644
index 00000000000..3c75ef8558c
--- /dev/null
+++ b/test/py/tests/test_fit_verity_sign.py
@@ -0,0 +1,203 @@
+# SPDX-License-Identifier: GPL-2.0
+# Copyright 2026 Daniel Golle <[email protected]>
+
+"""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)))
+
+
[email protected]('fit_signature')
[email protected]('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')
diff --git a/tools/image-host.c b/tools/image-host.c
index 8f1e7be4066..16a5ad6c22d 100644
--- a/tools/image-host.c
+++ b/tools/image-host.c
@@ -1184,6 +1184,41 @@ static const char *fit_config_get_image_list(const void *fit, int noffset,
}
/**
+ * 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
*
* This adds a list of paths to image nodes (as referred to by a particular
@@ -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;
}
- return 0;
-
-err_mem:
- fprintf(stderr, "Out of memory processing configuration '%s/%s'\n", conf_name,
- sig_name);
- return -ENOMEM;
+ /*
+ * 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;
+ }
-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;
+ return 0;
}
/**