Merge patch series "fit: dm-verity support"

Daniel Golle <daniel@makrotopia.org> says:

This series adds dm-verity support to U-Boot's FIT image infrastructure.
It is the first logical subset of the larger OpenWrt boot method series
posted as an RFC in February 2026 [1], extracted here for independent
review and merging.

OpenWrt's firmware model embeds a read-only squashfs or erofs root
filesystem directly inside a uImage.FIT container as a FILESYSTEM-type
loadable FIT image. At boot the kernel maps this sub-image directly from
the underlying block device via the fitblk driver (/dev/fit0, /dev/fit1,
...), the goal is that the bootloader never even copies it to RAM.

dm-verity enables the kernel to verify the integrity of those mapped
filesystems at read time, with a Merkle hash tree stored contiguously in
the same sub-image just after the data. Two kernel command-line
parameters are required:

  dm-mod.create=   -- the device-mapper target table for the verity device
  dm-mod.waitfor=  -- a comma-separated list of block devices to wait for
                      before dm-init sets up the targets (needed when fitblk
                      probes late, e.g. because it depends on NVMEM
                      calibration data)

The FIT dm-verity node schema was upstreamed into the flat-image-tree
specification [2], which this implementation tries to follow exactly.

The runtime feature is guarded behind CONFIG_FIT_VERITY. If not
enabled the resulting binary size remains unchanged. If enabled the
binary size increases by about 3kB.

[1] previous submissions:
    RFC: https://www.mail-archive.com/u-boot@lists.denx.de/msg565945.html
    v1:  https://www.mail-archive.com/u-boot@lists.denx.de/msg569472.html
    v2:  https://www.mail-archive.com/u-boot@lists.denx.de/msg570599.html
    v3:  https://www.mail-archive.com/u-boot@lists.denx.de/msg573223.html
    v4:  https://www.mail-archive.com/u-boot@lists.denx.de/msg574000.html

[2] flat-image-tree dm-verity node spec:
    795fd5fd7f

Link: https://lore.kernel.org/r/cover.1778887196.git.daniel@makrotopia.org
This commit is contained in:
Tom Rini
2026-05-27 13:44:20 -06:00
17 changed files with 1783 additions and 12 deletions

View File

@@ -142,6 +142,26 @@ config FIT_CIPHER
Enable the feature of data ciphering/unciphering in the tool mkimage
and in the u-boot support of the FIT image.
config FIT_VERITY
bool "dm-verity boot parameter generation from FIT metadata"
depends on FIT && OF_LIBFDT
help
When a FIT configuration contains loadable sub-images of type
IH_TYPE_FILESYSTEM with a dm-verity subnode, this option enables
building the dm-mod.create= and dm-mod.waitfor= kernel
command-line parameters from the verity metadata
(data-block-size, hash-block-size, num-data-blocks,
hash-start-block, algorithm, digest, salt) stored in the FIT.
The generated parameters reference /dev/fitN block devices that
Linux's uImage.FIT block driver assigns to loadable sub-images.
During FIT parsing (BOOTM_STATE_FINDOTHER), verity cmdline
fragments are stored in struct bootm_headers and automatically
appended to the bootargs environment variable during
BOOTM_STATE_OS_PREP. This works from both the bootm command
and BOOTSTD bootmeths.
config FIT_VERBOSE
bool "Show verbose messages when FIT images fail"
help

View File

@@ -243,6 +243,13 @@ static int boot_get_kernel(const char *addr_fit, struct bootm_headers *images,
static int bootm_start(void)
{
/*
* Free dm-verity allocations from a prior boot attempt before
* zeroing the structure. The pointers are guaranteed to be valid
* or NULL: .bss is zero-initialised, and memset() below zeroes
* them again after every boot.
*/
fit_verity_free(&images);
memset((void *)&images, 0, sizeof(images));
images.verify = env_get_yesno("verify");
@@ -1071,6 +1078,12 @@ int bootm_run_states(struct bootm_info *bmi, int states)
/* For Linux OS do all substitutions at console processing */
if (images->os.os == IH_OS_LINUX)
flags = BOOTM_CL_ALL;
ret = fit_verity_apply_bootargs(images);
if (ret) {
printf("dm-verity bootargs failed (err=%d)\n", ret);
ret = CMD_RET_FAILURE;
goto err;
}
ret = bootm_process_cmdline_env(flags);
if (ret) {
printf("Cmdline setup failed (err=%d)\n", ret);

View File

@@ -810,6 +810,11 @@ int boot_get_loadable(struct bootm_headers *images)
fit_loadable_process(img_type, img_data, img_len);
}
fit_img_result = fit_verity_build_cmdline(buf, conf_noffset,
images);
if (fit_img_result < 0)
return fit_img_result;
break;
default:
printf("The given image format is not supported (corrupt?)\n");

View File

@@ -21,8 +21,11 @@
extern void *aligned_alloc(size_t alignment, size_t size);
#else
#include <linux/compiler.h>
#include <linux/log2.h>
#include <linux/sizes.h>
#include <env.h>
#include <errno.h>
#include <hexdump.h>
#include <log.h>
#include <mapmem.h>
#include <asm/io.h>
@@ -235,6 +238,39 @@ static void fit_image_print_data(const void *fit, int noffset, const char *p,
}
}
static __maybe_unused void fit_image_print_dm_verity(const void *fit,
int noffset,
const char *p)
{
#if defined(USE_HOSTCC) || CONFIG_IS_ENABLED(FIT_VERITY)
const char *algo;
const uint8_t *bin;
int len, i;
algo = fdt_getprop(fit, noffset, FIT_VERITY_ALGO_PROP, NULL);
if (algo)
printf("%s Verity algo: %s\n", p, algo);
bin = fdt_getprop(fit, noffset, FIT_VERITY_DIGEST_PROP,
&len);
if (bin && len > 0) {
printf("%s Verity hash: ", p);
for (i = 0; i < len; i++)
printf("%02x", bin[i]);
printf("\n");
}
bin = fdt_getprop(fit, noffset, FIT_VERITY_SALT_PROP,
&len);
if (bin && len > 0) {
printf("%s Verity salt: ", p);
for (i = 0; i < len; i++)
printf("%02x", bin[i]);
printf("\n");
}
#endif
}
/**
* fit_image_print_verification_data() - prints out the hash/signature details
* @fit: pointer to the FIT format image header
@@ -263,6 +299,11 @@ static void fit_image_print_verification_data(const void *fit, int noffset,
strlen(FIT_SIG_NODENAME))) {
fit_image_print_data(fit, noffset, p, "Sign");
}
#if defined(USE_HOSTCC) || CONFIG_IS_ENABLED(FIT_VERITY)
else if (!strcmp(name, FIT_VERITY_NODENAME)) {
fit_image_print_dm_verity(fit, noffset, p);
}
#endif
}
/**
@@ -2589,3 +2630,299 @@ out:
return fdt_noffset;
}
#endif
#if !defined(USE_HOSTCC) && CONFIG_IS_ENABLED(FIT_VERITY)
static const char *const verity_opt_props[] = {
FIT_VERITY_OPT_RESTART,
FIT_VERITY_OPT_PANIC,
FIT_VERITY_OPT_RERR,
FIT_VERITY_OPT_PERR,
FIT_VERITY_OPT_ONCE,
};
/**
* fit_verity_build_target() - build one dm-verity target specification
* @fit: pointer to the FIT blob
* @img_noffset: image node offset containing the dm-verity subnode
* @loadable_idx: index of this loadable (for /dev/fitN)
* @uname: unit name of the image
* @separator: true if a ";" prefix is needed (not the first target)
* @buf: output buffer, or NULL to measure only
* @bufsize: size of @buf (ignored when @buf is NULL)
*
* Parses all dm-verity properties from the image's ``dm-verity`` child
* node and writes (or measures) a dm target specification string of the
* form used by the ``dm-mod.create`` kernel parameter.
*
* Return: number of characters that would be written (excluding '\0'),
* or -ve errno on error (e.g. missing mandatory property)
*/
static int fit_verity_build_target(const void *fit, int img_noffset,
int loadable_idx, const char *uname,
bool separator, char *buf, int bufsize)
{
const char *algorithm;
const u8 *digest_raw, *salt_raw;
const fdt32_t *val;
char *digest_hex = NULL, *salt_hex = NULL, *opt_buf = NULL;
int verity_node;
unsigned int data_block_size, hash_block_size;
int num_data_blocks, hash_start_block;
u64 data_sectors;
int digest_len, salt_len;
int opt_count, opt_off, opt_buf_size;
int len;
int i;
verity_node = fdt_subnode_offset(fit, img_noffset, FIT_VERITY_NODENAME);
if (verity_node < 0)
return -ENOENT;
/* Mandatory u32 properties */
val = fdt_getprop(fit, verity_node, FIT_VERITY_DBS_PROP, NULL);
if (!val)
return -EINVAL;
data_block_size = fdt32_to_cpu(*val);
val = fdt_getprop(fit, verity_node, FIT_VERITY_HBS_PROP, NULL);
if (!val)
return -EINVAL;
hash_block_size = fdt32_to_cpu(*val);
val = fdt_getprop(fit, verity_node, FIT_VERITY_NBLK_PROP, NULL);
if (!val)
return -EINVAL;
num_data_blocks = fdt32_to_cpu(*val);
val = fdt_getprop(fit, verity_node, FIT_VERITY_HBLK_PROP, NULL);
if (!val)
return -EINVAL;
hash_start_block = fdt32_to_cpu(*val);
if (data_block_size < 512U || !is_power_of_2(data_block_size) ||
hash_block_size < 512U || !is_power_of_2(hash_block_size) ||
!num_data_blocks)
return -EINVAL;
/* Mandatory string */
algorithm = fdt_getprop(fit, verity_node, FIT_VERITY_ALGO_PROP, NULL);
if (!algorithm)
return -EINVAL;
/* Mandatory byte arrays */
digest_raw = fdt_getprop(fit, verity_node, FIT_VERITY_DIGEST_PROP,
&digest_len);
if (!digest_raw || digest_len <= 0)
return -EINVAL;
salt_raw = fdt_getprop(fit, verity_node, FIT_VERITY_SALT_PROP,
&salt_len);
if (!salt_raw || salt_len <= 0)
return -EINVAL;
/* Hex-encode digest and salt into dynamically sized buffers */
digest_hex = malloc(digest_len * 2 + 1);
salt_hex = malloc(salt_len * 2 + 1);
if (!digest_hex || !salt_hex) {
len = -ENOMEM;
goto out;
}
*bin2hex(digest_hex, digest_raw, digest_len) = '\0';
*bin2hex(salt_hex, salt_raw, salt_len) = '\0';
data_sectors = (u64)num_data_blocks * ((u64)data_block_size / 512);
/* Compute space needed for optional boolean properties */
opt_buf_size = 1; /* NUL terminator */
for (i = 0; i < ARRAY_SIZE(verity_opt_props); i++)
opt_buf_size += strlen(verity_opt_props[i]) + 1;
opt_buf = malloc(opt_buf_size);
if (!opt_buf) {
len = -ENOMEM;
goto out;
}
/* Collect optional boolean properties */
opt_count = 0;
opt_off = 0;
opt_buf[0] = '\0';
for (i = 0; i < ARRAY_SIZE(verity_opt_props); i++) {
if (fdt_getprop(fit, verity_node,
verity_opt_props[i], NULL)) {
const char *s = verity_opt_props[i];
int slen = strlen(s);
if (opt_off)
opt_buf[opt_off++] = ' ';
/* Copy with hyphen-to-underscore conversion */
while (slen-- > 0) {
opt_buf[opt_off++] =
(*s == '-') ? '_' : *s;
s++;
}
opt_buf[opt_off] = '\0';
opt_count++;
}
}
/* Emit (or measure) the target spec */
len = snprintf(buf, buf ? bufsize : 0,
"%s%s,,, ro,0 %llu verity 1 /dev/fit%d /dev/fit%d %u %u %d %d %s %s %s",
separator ? ";" : "", uname,
(unsigned long long)data_sectors, loadable_idx, loadable_idx,
data_block_size, hash_block_size,
num_data_blocks, hash_start_block,
algorithm, digest_hex, salt_hex);
if (opt_count) {
int extra = snprintf(buf ? buf + len : NULL,
buf ? bufsize - len : 0,
" %d %s", opt_count, opt_buf);
len += extra;
}
out:
free(digest_hex);
free(salt_hex);
free(opt_buf);
return len;
}
int fit_verity_build_cmdline(const void *fit, int conf_noffset,
struct bootm_headers *images)
{
int images_noffset;
int dm_create_len = 0, dm_waitfor_len = 0;
char *dm_create = NULL, *dm_waitfor = NULL;
const char *uname;
int loadable_idx;
int found = 0;
int ret = 0;
images_noffset = fdt_path_offset(fit, FIT_IMAGES_PATH);
if (images_noffset < 0)
return 0;
for (loadable_idx = 0;
(uname = fdt_stringlist_get(fit, conf_noffset,
FIT_LOADABLE_PROP,
loadable_idx, NULL));
loadable_idx++) {
int img_noffset, need;
u8 img_type;
char *tmp;
img_noffset = fdt_subnode_offset(fit, images_noffset, uname);
if (img_noffset < 0)
continue;
if (fit_image_get_type(fit, img_noffset, &img_type) ||
img_type != IH_TYPE_FILESYSTEM)
continue;
/* Measure first, then allocate and write */
need = fit_verity_build_target(fit, img_noffset,
loadable_idx, uname,
found > 0, NULL, 0);
if (need == -ENOENT)
continue; /* no dm-verity subnode -- fine */
if (need < 0) {
log_err("FIT: broken dm-verity metadata in '%s'\n",
uname);
ret = need;
goto err;
}
tmp = realloc(dm_create, dm_create_len + need + 1);
if (!tmp) {
ret = -ENOMEM;
goto err;
}
dm_create = tmp;
fit_verity_build_target(fit, img_noffset, loadable_idx,
uname, found > 0,
dm_create + dm_create_len,
need + 1);
dm_create_len += need;
/* Grow dm_waitfor buffer */
need = snprintf(NULL, 0, "%s/dev/fit%d",
dm_waitfor_len ? "," : "",
loadable_idx);
tmp = realloc(dm_waitfor, dm_waitfor_len + need + 1);
if (!tmp) {
ret = -ENOMEM;
goto err;
}
dm_waitfor = tmp;
sprintf(dm_waitfor + dm_waitfor_len, "%s/dev/fit%d",
dm_waitfor_len ? "," : "",
loadable_idx);
dm_waitfor_len += need;
found++;
}
if (found) {
/* Transfer ownership to the bootm_headers */
images->dm_mod_create = dm_create;
images->dm_mod_waitfor = dm_waitfor;
} else {
free(dm_create);
free(dm_waitfor);
}
return 0;
err:
free(dm_create);
free(dm_waitfor);
return ret;
}
/**
* fmt used by both the measurement and the actual write of bootargs.
* Shared to guarantee they stay in sync.
*/
#define VERITY_BOOTARGS_FMT "%s%sdm-mod.create=\"%s\" dm-mod.waitfor=\"%s\""
int fit_verity_apply_bootargs(const struct bootm_headers *images)
{
const char *existing;
char *newargs;
int len;
if (!images->dm_mod_create)
return 0;
existing = env_get("bootargs");
if (!existing)
existing = "";
/* Measure */
len = snprintf(NULL, 0, VERITY_BOOTARGS_FMT,
existing, existing[0] ? " " : "",
images->dm_mod_create, images->dm_mod_waitfor);
newargs = malloc(len + 1);
if (!newargs)
return -ENOMEM;
snprintf(newargs, len + 1, VERITY_BOOTARGS_FMT,
existing, existing[0] ? " " : "",
images->dm_mod_create, images->dm_mod_waitfor);
env_set("bootargs", newargs);
free(newargs);
return 0;
}
void fit_verity_free(struct bootm_headers *images)
{
free(images->dm_mod_create);
free(images->dm_mod_waitfor);
images->dm_mod_create = NULL;
images->dm_mod_waitfor = NULL;
}
#endif /* FIT_VERITY */

View File

@@ -18,6 +18,7 @@ CONFIG_EFI_RT_VOLATILE_STORE=y
CONFIG_BUTTON_CMD=y
CONFIG_FIT=y
CONFIG_FIT_SIGNATURE=y
CONFIG_FIT_VERITY=y
CONFIG_FIT_VERBOSE=y
CONFIG_LEGACY_IMAGE_FORMAT=y
CONFIG_BOOTSTAGE=y

View File

@@ -22,6 +22,7 @@ CONFIG_BUTTON_CMD=y
CONFIG_FIT=y
CONFIG_FIT_SIGNATURE=y
CONFIG_FIT_CIPHER=y
CONFIG_FIT_VERITY=y
CONFIG_FIT_VERBOSE=y
CONFIG_BOOTMETH_ANDROID=y
CONFIG_BOOTMETH_RAUC=y

View File

@@ -14,6 +14,7 @@ CONFIG_EFI_CAPSULE_CRT_FILE="board/sandbox/capsule_pub_key_good.crt"
CONFIG_BUTTON_CMD=y
CONFIG_FIT=y
CONFIG_FIT_SIGNATURE=y
CONFIG_FIT_VERITY=y
CONFIG_FIT_VERBOSE=y
CONFIG_LEGACY_IMAGE_FORMAT=y
CONFIG_BOOTSTAGE=y

304
doc/usage/fit/dm-verity.rst Normal file
View File

@@ -0,0 +1,304 @@
.. SPDX-License-Identifier: GPL-2.0+
FIT dm-verity Boot Parameters
=============================
Introduction
------------
Linux's dm-verity device-mapper target provides transparent integrity
checking of block devices using a Merkle tree. It is commonly used to
protect read-only root filesystems such as SquashFS images.
When a FIT image packages the root filesystem as a loadable sub-image of
type ``filesystem`` (``IH_TYPE_FILESYSTEM``), the verity metadata can be
stored alongside the image data in a ``dm-verity`` subnode. U-Boot reads
this metadata at boot time and generates the kernel command-line parameters
that Linux needs to activate the verity target, eliminating the need for
an initramfs or userspace helper to set up dm-verity.
This feature is enabled by ``CONFIG_FIT_VERITY`` (see ``boot/Kconfig``).
Prerequisites
-------------
* **Linux uImage.FIT block driver** the kernel must include the FIT block
driver that exposes loadable sub-images as ``/dev/fit0``, ``/dev/fit1``,
etc. The driver assigns device numbers in the order loadables appear in
the FIT configuration.
* **dm-verity support in the kernel** ``CONFIG_DM_VERITY`` must be
enabled so the kernel can process the ``dm-mod.create=`` parameter.
* **CONFIG_FIT_VERITY** enabled in U-Boot.
How it works
------------
The implementation is split into a **build** phase and an **apply** phase,
both of which run automatically within the ``bootm`` state machine. No boot
method needs to call verity functions explicitly.
**Build phase** (``BOOTM_STATE_FINDOTHER````boot_get_loadable()``)
1. After all loadable sub-images have been loaded,
``fit_verity_build_cmdline()`` iterates the configuration's
``loadables`` list.
2. For each loadable that is an ``IH_TYPE_FILESYSTEM`` image **and**
contains a ``dm-verity`` child node, a dm-verity target specification is
built by the helper ``fit_verity_build_target()``.
3. The dm-verity target references ``/dev/fitN``, where *N* is the
zero-based index of the loadable in the configuration. This matches the
numbering used by the Linux FIT block driver.
4. The resulting fragments are stored in ``struct bootm_headers``:
``images->dm_mod_create``
The full dm-verity target table. Multiple targets are separated by ``;``.
``images->dm_mod_waitfor``
Comma-separated list of ``/dev/fitN`` devices so the kernel waits for
the underlying FIT block devices to appear before activating
device-mapper.
**Apply phase** (``BOOTM_STATE_OS_PREP``)
5. Just before ``bootm_process_cmdline_env()`` processes the ``bootargs``
environment variable, ``fit_verity_apply_bootargs()`` appends the
``dm-mod.create=`` and ``dm-mod.waitfor=`` parameters.
**Bootmeth integration**
Because the fragments are stored in ``struct bootm_headers``, a boot
method can check ``fit_verity_active(images)`` between bootm state
invocations. A typical pattern splits ``bootm_run_states()`` into two
calls -- one for ``START|FINDOS|FINDOTHER|LOADOS`` and one for
``OS_PREP|OS_GO`` -- and inspects ``fit_verity_active()`` in
between to decide whether to add a ``root=`` parameter pointing at the
dm-verity device.
FIT image source (.its) example
-------------------------------
Below is a minimal ``.its`` file showing a kernel and a dm-verity-protected
root filesystem packaged as a FIT. Only the three user-provided properties
(``algo``, ``data-block-size``, ``hash-block-size``) are included; ``mkimage``
computes and fills in ``digest``, ``salt``, ``num-data-blocks``, and
``hash-start-block`` automatically (see `Generating verity metadata`_ below)::
/dts-v1/;
/ {
description = "Kernel + dm-verity rootfs";
#address-cells = <1>;
images {
kernel {
description = "Linux kernel";
data = /incbin/("./Image.gz");
type = "kernel";
arch = "arm64";
os = "linux";
compression = "gzip";
load = <0x44000000>;
entry = <0x44000000>;
hash-1 {
algo = "sha256";
};
};
fdt {
description = "Device tree blob";
data = /incbin/("./board.dtb");
type = "flat_dt";
arch = "arm64";
compression = "none";
hash-1 {
algo = "sha256";
};
};
rootfs {
description = "SquashFS root filesystem";
data = /incbin/("./rootfs.squashfs");
type = "filesystem";
arch = "arm64";
compression = "none";
hash-1 {
algo = "sha256";
};
dm-verity {
algo = "sha256";
data-block-size = <4096>;
hash-block-size = <4096>;
};
};
};
configurations {
default = "config-1";
config-1 {
description = "Boot with dm-verity rootfs";
kernel = "kernel";
fdt = "fdt";
loadables = "rootfs";
};
};
};
With this configuration U-Boot produces a kernel command line similar to::
dm-mod.create="rootfs,,, ro,0 <data_sectors> verity 1 \
/dev/fit0 /dev/fit0 4096 4096 3762 3762 sha256 \
8e6791637f93cbb81fc45299e203cbe85ca2e47a38f5051bddeece92d7b1c9f9 \
aa7b11f8db8fe2e5bfd4eca1d18a22b5de7ea39d2e1b93bb7272ce0c6ca3cc8e" \
dm-mod.waitfor=/dev/fit0
dm-verity subnode properties
----------------------------
User-provided properties (required in the ``.its``):
.. list-table::
:header-rows: 1
:widths: 20 15 65
* - Property
- Type
- Description
* - ``algo``
- string
- Hash algorithm name, e.g. ``"sha256"``.
* - ``data-block-size``
- u32
- Data block size in bytes (>= 512, typically 4096).
* - ``hash-block-size``
- u32
- Hash block size in bytes (>= 512, typically 4096).
Computed properties (filled in by ``mkimage``):
.. list-table::
:header-rows: 1
:widths: 20 15 65
* - Property
- Type
- Description
* - ``num-data-blocks``
- u32
- Number of data blocks in the filesystem image (computed from the
image size and ``data-block-size``).
* - ``hash-start-block``
- u32
- Offset in ``hash-block-size``-sized blocks from the start of the
sub-image to the root block of the hash tree.
* - ``digest``
- byte array
- Root hash of the Merkle tree, stored as raw bytes. Length must match
the output size of ``algo``.
* - ``salt``
- byte array
- Salt used when computing the Merkle tree, stored as raw bytes.
These values are the same ones produced by ``veritysetup format`` and can
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``.
Optional boolean properties (when present, they are collected and appended
as dm-verity optional parameters with hyphens converted to underscores):
.. list-table::
:header-rows: 1
:widths: 30 70
* - Property
- Description
* - ``restart-on-corruption``
- Restart the system on data corruption.
* - ``panic-on-corruption``
- Panic the system on data corruption.
* - ``restart-on-error``
- Restart the system on I/O error.
* - ``panic-on-error``
- Panic the system on I/O error.
* - ``check-at-most-once``
- Verify data blocks only on first read.
Generating verity metadata
--------------------------
``mkimage`` automates the entire process. When it encounters a
``dm-verity`` subnode, it:
1. Writes the embedded image data to a temporary file.
2. Runs ``veritysetup format`` with the user-supplied algorithm and
block sizes.
3. Parses ``Root hash`` and ``Salt`` from ``veritysetup`` stdout.
4. Reads the expanded content (original data + Merkle hash tree) back
into an in-memory buffer and removes the temporary file. The
external-data section written to the .itb file uses this buffer in
place of the original ``data`` property.
5. Writes the computed ``digest``, ``salt``, ``num-data-blocks``, and
``hash-start-block`` properties into the ``dm-verity`` subnode.
Images with ``dm-verity`` subnodes **must** use external data layout
(``mkimage -E``). ``mkimage`` will abort with an error if ``-E`` is
not specified.
Usage::
# Create the filesystem image
mksquashfs rootfs/ rootfs.squashfs -comp xz
# Build the FIT (dm-verity is computed automatically); align each
# external-data section to the block size of the underlying storage
# (see the alignment note below).
mkimage -E -B 0x1000 -f image.its image.itb
``veritysetup`` (from the cryptsetup_ package) must be installed on
the build host.
.. _cryptsetup: https://gitlab.com/cryptsetup/cryptsetup
.. note::
``veritysetup format`` is invoked with ``--no-superblock``, so no
on-disk superblock is written between the data and hash regions.
The Merkle hash tree is appended directly to the image data within
the FIT external data section. ``hash-start-block`` is therefore
computed as ``data_size / hash-block-size`` (the offset of the hash
region in units of ``hash-block-size``). When ``data-block-size``
equals ``hash-block-size`` this happens to equal ``num-data-blocks``.
.. note::
The Linux ``fitblk`` driver currently requires each ``filesystem``
sub-image to start and end on block boundaries of the underlying
block device (typically 512 bytes, sometimes 4 KiB for eMMC or NVMe
with 4 KiB native sectors). Use ``mkimage -B <align>`` to pad
external-data sections to that boundary; ``-B 0x1000`` is a safe
default for the storage in common use.
This alignment requirement comes from the kernel-side ``fitblk``
driver to avoid unaligned-access fix-up overhead in block I/O, and
is **independent** of the dm-verity ``data-block-size`` and
``hash-block-size`` properties -- those describe the block sizes
used by the device-mapper verity target itself, not storage
alignment.
Kconfig
-------
``CONFIG_FIT_VERITY``
Depends on ``CONFIG_FIT`` and ``CONFIG_OF_LIBFDT``.
When enabled, ``fit_verity_build_cmdline()`` and
``fit_verity_apply_bootargs()`` are compiled into the boot path.
When disabled, the functions are static inlines returning 0, so there
is no code-size impact. Works with both the ``bootm`` command and
BOOTSTD boot methods.

View File

@@ -11,6 +11,7 @@ images that it reads and boots. Documentation about FIT is available in
:maxdepth: 1
beaglebone_vboot
dm-verity
howto
kernel_fdt
kernel_fdts_compressed

View File

@@ -7,7 +7,11 @@
#ifndef HEXDUMP_H
#define HEXDUMP_H
#ifdef USE_HOSTCC
#include <ctype.h>
#else
#include <linux/ctype.h>
#endif
#include <linux/types.h>
enum dump_prefix_t {
@@ -20,7 +24,7 @@ extern const char hex_asc[];
#define hex_asc_lo(x) hex_asc[((x) & 0x0f)]
#define hex_asc_hi(x) hex_asc[((x) & 0xf0) >> 4]
static inline char *hex_byte_pack(char *buf, u8 byte)
static inline char *hex_byte_pack(char *buf, uint8_t byte)
{
*buf++ = hex_asc_hi(byte);
*buf++ = hex_asc_lo(byte);
@@ -52,7 +56,7 @@ static inline int hex_to_bin(char ch)
*
* Return 0 on success, -1 in case of bad input.
*/
static inline int hex2bin(u8 *dst, const char *src, size_t count)
static inline int hex2bin(uint8_t *dst, const char *src, size_t count)
{
while (count--) {
int hi = hex_to_bin(*src++);

View File

@@ -396,7 +396,19 @@ struct bootm_headers {
ulong cmdline_start;
ulong cmdline_end;
struct bd_info *kbd;
#endif
#if CONFIG_IS_ENABLED(FIT_VERITY)
/*
* dm-verity kernel command-line fragments, populated during FIT
* parsing by fit_verity_build_cmdline(). Bootmeths can check
* fit_verity_active() between bootm states, and
* fit_verity_apply_bootargs() appends these to the "bootargs"
* env var during BOOTM_STATE_OS_PREP.
*/
char *dm_mod_create;
char *dm_mod_waitfor;
#endif /* FIT_VERITY */
#endif /* !USE_HOSTCC */
int verify; /* env_get("verify")[0] != 'n' */
@@ -756,6 +768,72 @@ int fit_image_load(struct bootm_headers *images, ulong addr,
int arch, int image_ph_type, int bootstage_id,
enum fit_load_op load_op, ulong *datap, ulong *lenp);
#if !defined(USE_HOSTCC) && CONFIG_IS_ENABLED(FIT_VERITY)
/**
* fit_verity_build_cmdline() - build dm-verity cmdline from FIT metadata
* @fit: pointer to the FIT blob
* @conf_noffset: configuration node offset in @fit
* @images: bootm headers; dm_mod_create / dm_mod_waitfor are
* populated on success
*
* Called automatically from boot_get_loadable() during FIT parsing.
* For each IH_TYPE_FILESYSTEM loadable with a dm-verity subnode,
* builds the corresponding dm target specification.
*
* Return: 0 on success, -ve errno on error
*/
int fit_verity_build_cmdline(const void *fit, int conf_noffset,
struct bootm_headers *images);
/**
* fit_verity_apply_bootargs() - append dm-verity params to bootargs env
* @images: bootm headers with dm-verity cmdline fragments
*
* Called from BOOTM_STATE_OS_PREP before bootm_process_cmdline_env().
*
* Return: 0 on success, -ve errno on error
*/
int fit_verity_apply_bootargs(const struct bootm_headers *images);
/**
* fit_verity_active() - check whether dm-verity targets were found
* @images: bootm headers
*
* Return: true if at least one dm-verity target was built
*/
static inline bool fit_verity_active(const struct bootm_headers *images)
{
return !!images->dm_mod_create;
}
/**
* fit_verity_free() - free dm-verity cmdline allocations
* @images: bootm headers
*/
void fit_verity_free(struct bootm_headers *images);
#else /* !FIT_VERITY */
static inline int fit_verity_build_cmdline(const void *fit, int conf_noffset,
struct bootm_headers *images)
{
return 0;
}
static inline int fit_verity_apply_bootargs(const struct bootm_headers *images)
{
return 0;
}
static inline bool fit_verity_active(const struct bootm_headers *images)
{
return false;
}
static inline void fit_verity_free(struct bootm_headers *images) {}
#endif /* FIT_VERITY */
/**
* image_locate_script() - Locate the raw script in an image
*
@@ -1079,6 +1157,23 @@ int booti_setup(ulong image, ulong *relocated_addr, ulong *size,
#define FIT_CIPHER_NODENAME "cipher"
#define FIT_ALGO_PROP "algo"
/* dm-verity node */
#define FIT_VERITY_NODENAME "dm-verity"
#define FIT_VERITY_ALGO_PROP "algo"
#define FIT_VERITY_DBS_PROP "data-block-size"
#define FIT_VERITY_HBS_PROP "hash-block-size"
#define FIT_VERITY_NBLK_PROP "num-data-blocks"
#define FIT_VERITY_HBLK_PROP "hash-start-block"
#define FIT_VERITY_DIGEST_PROP "digest"
#define FIT_VERITY_SALT_PROP "salt"
/* dm-verity error-handling modes (optional boolean property names) */
#define FIT_VERITY_OPT_RESTART "restart-on-corruption"
#define FIT_VERITY_OPT_PANIC "panic-on-corruption"
#define FIT_VERITY_OPT_RERR "restart-on-error"
#define FIT_VERITY_OPT_PERR "panic-on-error"
#define FIT_VERITY_OPT_ONCE "check-at-most-once"
/* image node */
#define FIT_DATA_PROP "data"
#define FIT_DATA_POSITION_PROP "data-position"
@@ -1332,6 +1427,24 @@ int fit_add_verification_data(const char *keydir, const char *keyfile,
const char *cmdname, const char *algo_name,
struct image_summary *summary);
#ifdef USE_HOSTCC
/**
* fit_verity_get_expanded() - look up the cached dm-verity expanded buffer
*
* After mkimage has run veritysetup on a FILESYSTEM image, the original
* data concatenated with the Merkle hash tree is cached in memory keyed
* by image name. fit_extract_data() retrieves it to write the external
* data section without having to re-read a temporary file from disk.
*
* @name: image unit name (FDT node name under /images)
* @data: output -- pointer to cached buffer (do NOT free; lifetime
* ends when mkimage exits)
* @size: output -- size of @data in bytes
* Return: 0 if a cache entry exists for @name, -ENOENT otherwise
*/
int fit_verity_get_expanded(const char *name, const void **data, size_t *size);
#endif /* USE_HOSTCC */
/**
* fit_image_verify_with_data() - Verify an image with given data
*

View File

@@ -15,6 +15,7 @@ endif
ifdef CONFIG_SANDBOX
obj-$(CONFIG_$(PHASE_)CMDLINE) += bootm.o
endif
obj-$(CONFIG_$(PHASE_)FIT_VERITY) += fit_verity.o
obj-$(CONFIG_MEASURED_BOOT) += measurement.o
ifdef CONFIG_OF_LIVE

306
test/boot/fit_verity.c Normal file
View File

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

View File

@@ -59,6 +59,7 @@ SUITE_DECL(env);
SUITE_DECL(exit);
SUITE_DECL(fdt);
SUITE_DECL(fdt_overlay);
SUITE_DECL(fit_verity);
SUITE_DECL(font);
SUITE_DECL(hush);
SUITE_DECL(lib);
@@ -86,6 +87,7 @@ static struct suite suites[] = {
SUITE(exit, "shell exit and variables"),
SUITE(fdt, "fdt command"),
SUITE(fdt_overlay, "device tree overlays"),
SUITE(fit_verity, "FIT dm-verity cmdline generation"),
SUITE(font, "font command"),
SUITE(hush, "hush behaviour"),
SUITE(lib, "library functions"),

View File

@@ -0,0 +1,175 @@
# SPDX-License-Identifier: GPL-2.0+
#
# Copyright 2026 Daniel Golle <daniel@makrotopia.org>
"""
Test mkimage dm-verity Merkle-tree generation
Build a minimal .its with a dm-verity subnode (user-provided properties only),
run mkimage -E, and verify that the computed properties (digest, salt,
num-data-blocks, hash-start-block) are written into the resulting FIT.
The computed digest is then re-verified by running ``veritysetup verify``
against the external data section of the .itb.
This test does not run the sandbox. It only exercises the host tool 'mkimage'.
Requires 'veritysetup' from the cryptsetup package on the build host.
"""
import os
import struct
import pytest
import utils
ITS_TEMPLATE = """\
/dts-v1/;
/ {
description = "dm-verity test";
#address-cells = <1>;
images {
rootfs {
description = "test filesystem";
data = /incbin/("./rootfs.bin");
type = "filesystem";
arch = "sandbox";
compression = "none";
dm-verity {
algo = "sha256";
data-block-size = <%d>;
hash-block-size = <%d>;
};
};
};
configurations {
default = "conf-1";
conf-1 {
description = "test config";
loadables = "rootfs";
};
};
};
"""
def _fdt_totalsize(path):
"""Read the totalsize field from an FDT header (offset 4, big-endian u32)."""
with open(path, 'rb') as f:
magic, totalsize = struct.unpack('>II', f.read(8))
assert magic == 0xd00dfeed, f'not an FDT: magic={magic:#x}'
return totalsize
def _run_round_trip(ubman, tempdir, data_block_size, hash_block_size):
"""Build a FIT with dm-verity, verify written properties, re-verify with veritysetup."""
mkimage = ubman.config.build_dir + '/tools/mkimage'
rootfs_file = os.path.join(tempdir, 'rootfs.bin')
its_file = os.path.join(tempdir, 'image.its')
fit_file = os.path.join(tempdir, 'image.itb')
# 64 data blocks of 0xa5
num_blocks = 64
data_size = data_block_size * num_blocks
with open(rootfs_file, 'wb') as f:
f.write(bytes([0xa5]) * data_size)
with open(its_file, 'w') as f:
f.write(ITS_TEMPLATE % (data_block_size, hash_block_size))
dtc_args = f'-I dts -O dtb -i {tempdir}'
utils.run_and_log(ubman,
[mkimage, '-E', '-D', dtc_args, '-f', its_file, fit_file])
def fdt_get(node, prop):
val = utils.run_and_log(ubman, f'fdtget {fit_file} {node} {prop}')
return val.strip()
def fdt_get_hex(node, prop):
val = utils.run_and_log(ubman, f'fdtget -tbx {fit_file} {node} {prop}')
return ''.join(b.zfill(2) for b in val.strip().split())
verity_path = '/images/rootfs/dm-verity'
assert fdt_get(verity_path, 'algo') == 'sha256'
assert int(fdt_get(verity_path, 'data-block-size')) == data_block_size
assert int(fdt_get(verity_path, 'hash-block-size')) == hash_block_size
nblk = int(fdt_get(verity_path, 'num-data-blocks'))
assert nblk == num_blocks, f'num-data-blocks {nblk} != {num_blocks}'
hblk = int(fdt_get(verity_path, 'hash-start-block'))
# With --no-superblock, hash-start-block = data_size / hash-block-size
assert hblk == data_size // hash_block_size, \
f'hash-start-block {hblk} != {data_size // hash_block_size}'
digest = fdt_get_hex(verity_path, 'digest')
assert len(digest) == 64 and digest != '0' * 64
salt = fdt_get_hex(verity_path, 'salt')
assert len(salt) == 64
# Re-verify the digest with veritysetup against the .itb's external data.
# With -E, image data sits after the FIT FDT at (fdt_totalsize + data-offset).
data_offset = int(fdt_get('/images/rootfs', 'data-offset'))
data_size_full = int(fdt_get('/images/rootfs', 'data-size'))
ext_pos = _fdt_totalsize(fit_file) + data_offset
expanded = os.path.join(tempdir, 'expanded.bin')
with open(fit_file, 'rb') as src, open(expanded, 'wb') as dst:
src.seek(ext_pos)
dst.write(src.read(data_size_full))
utils.run_and_log(ubman, [
'veritysetup', 'verify', expanded, expanded, digest,
'--no-superblock',
f'--data-block-size={data_block_size}',
f'--hash-block-size={hash_block_size}',
f'--data-blocks={nblk}',
'--hash=sha256',
f'--salt={salt}',
f'--hash-offset={data_size}',
])
@pytest.mark.requiredtool('dtc')
@pytest.mark.requiredtool('fdtget')
@pytest.mark.requiredtool('veritysetup')
@pytest.mark.parametrize('data_block_size,hash_block_size,subdir', [
(4096, 4096, 'verity-equal'),
(4096, 1024, 'verity-unequal'),
])
def test_mkimage_verity(ubman, data_block_size, hash_block_size, subdir):
"""mkimage writes correct dm-verity properties and the digest verifies.
Run with matching and mismatched block sizes so the
``hash-start-block != num-data-blocks`` path is exercised.
"""
tempdir = os.path.join(ubman.config.result_dir, subdir)
os.makedirs(tempdir, exist_ok=True)
_run_round_trip(ubman, tempdir, data_block_size, hash_block_size)
@pytest.mark.requiredtool('dtc')
@pytest.mark.requiredtool('veritysetup')
def test_mkimage_verity_requires_external(ubman):
"""mkimage rejects dm-verity without -E with the expected diagnostic."""
mkimage = ubman.config.build_dir + '/tools/mkimage'
tempdir = os.path.join(ubman.config.result_dir, 'verity_no_ext')
os.makedirs(tempdir, exist_ok=True)
rootfs_file = os.path.join(tempdir, 'rootfs.bin')
its_file = os.path.join(tempdir, 'image.its')
fit_file = os.path.join(tempdir, 'image.itb')
with open(rootfs_file, 'wb') as f:
f.write(bytes([0xa5]) * 4096 * 8)
with open(its_file, 'w') as f:
f.write(ITS_TEMPLATE % (4096, 4096))
dtc_args = f'-I dts -O dtb -i {tempdir}'
utils.run_and_log_expect_exception(
ubman,
[mkimage, '-D', dtc_args, '-f', its_file, fit_file],
1, 'dm-verity requires external data')

View File

@@ -40,10 +40,10 @@ static int fit_estimate_hash_sig_size(struct image_tool_params *params, const ch
return -EIO;
/*
* Walk the FIT image, looking for nodes named hash* and
* signature*. Since the interesting nodes are subnodes of an
* image or configuration node, we are only interested in
* those at depth exactly 3.
* Walk the FIT image, looking for nodes named hash*,
* signature*, and dm-verity. Since the interesting nodes are
* subnodes of an image or configuration node, we are only
* interested in those at depth exactly 3.
*
* The estimate for a hash node is based on a sha512 digest
* being 64 bytes, with another 64 bytes added to account for
@@ -55,6 +55,10 @@ static int fit_estimate_hash_sig_size(struct image_tool_params *params, const ch
* account for fdt overhead and the various other properties
* (hashed-nodes etc.) that will also be filled in.
*
* For a dm-verity node the small metadata properties (digest,
* salt, two u32s and a temp-file path) are written into the
* FDT by fit_image_process_verity().
*
* One could try to be more precise in the estimates by
* looking at the "algo" property and, in the case of
* configuration signatures, the sign-images property. Also,
@@ -76,6 +80,18 @@ static int fit_estimate_hash_sig_size(struct image_tool_params *params, const ch
if (signing && !strncmp(name, FIT_SIG_NODENAME, strlen(FIT_SIG_NODENAME)))
estimate += 1024;
if (!strcmp(name, FIT_VERITY_NODENAME)) {
if (!params->external_data) {
fprintf(stderr,
"%s: dm-verity requires external data (-E)\n",
params->cmdname);
munmap(fdt, sbuf.st_size);
close(fd);
return -EINVAL;
}
estimate += 256;
}
}
munmap(fdt, sbuf.st_size);
@@ -470,6 +486,41 @@ static int fit_write_images(struct image_tool_params *params, char *fdt)
return 0;
}
/**
* fit_copy_image_data() - copy image data, using cached verity expansion
* @fdt: FIT blob
* @node: image node offset
* @buf: destination buffer
* @buf_ptr: write offset within @buf
* @data: embedded image data (used when no dm-verity expansion exists)
* @lenp: in/out: on entry, length of @data; on exit, bytes written
*
* When fit_image_process_verity() has run, the expanded image data
* (original + hash tree) is cached in memory. Look it up by image name
* and copy from the cached buffer rather than the embedded ``data``
* property; fall back to @data otherwise.
*
* Return: 0 on success
*/
static int fit_copy_image_data(void *fdt, int node, void *buf,
int buf_ptr, const void *data, int *lenp)
{
const char *image_name = fdt_get_name(fdt, node, NULL);
const void *vdata;
size_t vsize;
if (image_name &&
!fit_verity_get_expanded(image_name, &vdata, &vsize)) {
memcpy(buf + buf_ptr, vdata, vsize);
*lenp = vsize;
return 0;
}
memcpy(buf + buf_ptr, data, *lenp);
return 0;
}
/**
* fit_write_configs() - Write out a list of configurations to the FIT
*
@@ -653,6 +704,8 @@ static int fit_extract_data(struct image_tool_params *params, const char *fname)
int node;
int align_size = 0;
int len = 0;
int verity_extra = 0;
int orig_len;
fd = mmap_fdt(params->cmdname, fname, 0, &fdt, &sbuf, false, false);
if (fd < 0)
@@ -686,11 +739,34 @@ static int fit_extract_data(struct image_tool_params *params, const char *fname)
align_size += 4;
}
/*
* When dm-verity is active the external data for an image is
* larger than the embedded data property (original + hash tree).
* Walk images once more and consult the in-memory cache for the
* actual expanded size.
*/
fdt_for_each_subnode(node, fdt, images) {
const char *image_name;
const void *vdata;
size_t vsize;
orig_len = 0;
if (fdt_subnode_offset(fdt, node, FIT_VERITY_NODENAME) < 0)
continue;
image_name = fdt_get_name(fdt, node, NULL);
if (!image_name ||
fit_verity_get_expanded(image_name, &vdata, &vsize))
continue;
fdt_getprop(fdt, node, FIT_DATA_PROP, &orig_len);
if ((int)vsize > orig_len)
verity_extra += (int)vsize - orig_len;
}
/*
* Allocate space to hold the image data we will extract,
* extral space allocate for image alignment to prevent overflow.
*/
buf = calloc(1, fit_size + align_size);
buf = calloc(1, fit_size + align_size + verity_extra);
if (!buf) {
ret = -ENOMEM;
goto err_munmap;
@@ -721,7 +797,10 @@ static int fit_extract_data(struct image_tool_params *params, const char *fname)
data = fdt_getprop(fdt, node, FIT_DATA_PROP, &len);
if (!data)
continue;
memcpy(buf + buf_ptr, data, len);
ret = fit_copy_image_data(fdt, node, buf, buf_ptr, data, &len);
if (ret)
goto err_munmap;
debug("Extracting data size %x\n", len);
ret = fdt_delprop(fdt, node, FIT_DATA_PROP);

View File

@@ -12,8 +12,12 @@
#include <bootm.h>
#include <fdt_region.h>
#include <image.h>
#include <hexdump.h>
#include <version.h>
#include <sys/stat.h>
#include <sys/wait.h>
#if CONFIG_IS_ENABLED(FIT_SIGNATURE)
#include <openssl/pem.h>
#include <openssl/evp.h>
@@ -626,6 +630,376 @@ int fit_image_cipher_data(const char *keydir, void *keydest,
image_noffset, cipher_node_offset, data, size, cmdname);
}
/*
* In-memory cache of dm-verity expanded buffers (original data followed
* by the Merkle hash tree), keyed by image unit name. Populated by
* fit_image_process_verity() and consumed by fit_extract_data() /
* fit_copy_image_data() so that the expanded content never leaves the
* mkimage process address space.
*/
struct fit_verity_blob {
char *name;
void *data;
size_t size;
struct fit_verity_blob *next;
};
static struct fit_verity_blob *fit_verity_blobs;
/* Stash a malloc'd expanded buffer; takes ownership of @data on success. */
static int fit_verity_stash(const char *name, void *data, size_t size)
{
struct fit_verity_blob *b;
b = calloc(1, sizeof(*b));
if (!b)
return -ENOMEM;
b->name = strdup(name);
if (!b->name) {
free(b);
return -ENOMEM;
}
b->data = data;
b->size = size;
b->next = fit_verity_blobs;
fit_verity_blobs = b;
return 0;
}
int fit_verity_get_expanded(const char *name, const void **data, size_t *size)
{
struct fit_verity_blob *b;
for (b = fit_verity_blobs; b; b = b->next) {
if (!strcmp(b->name, name)) {
*data = b->data;
*size = b->size;
return 0;
}
}
return -ENOENT;
}
/**
* fit_image_process_verity() - Run veritysetup and fill dm-verity properties
*
* Extracts the embedded image data to a temporary file, runs
* ``veritysetup format`` to generate the Merkle hash tree (appended to the
* same file), parses Root hash / Salt from its stdout, and writes the
* computed properties (digest, salt, num-data-blocks, hash-start-block)
* back into the FIT dm-verity subnode.
*
* The expanded data (original data + hash tree) is read back into a
* malloc'd buffer and stashed in an in-memory cache keyed by @image_name
* via fit_verity_stash(). The same buffer is returned through
* @expanded_data / @expanded_size so that hash and signature subnodes
* can be computed over the complete image; the returned pointer is a
* *view* of the cached buffer and must not be freed by the caller.
* fit_extract_data() later retrieves the same buffer via
* fit_verity_get_expanded() to write the external data section.
*
* @fit: FIT blob (read-write)
* @image_name: image unit name (for diagnostics)
* @verity_noffset: dm-verity subnode offset
* @data: embedded image data
* @data_size: size of @data in bytes
* @expanded_data: output -- malloc'd buffer with expanded content
* @expanded_size: output -- size of @expanded_data
* Return: 0 on success, -ve on error (-ENOSPC when the FIT blob is full)
*/
static int fit_image_process_verity(void *fit, const char *image_name,
int verity_noffset,
const void *data, size_t data_size,
void **expanded_data, size_t *expanded_size)
{
const char *algo_prop;
char algo[64];
const fdt32_t *val;
unsigned int data_block_size, hash_block_size;
uint32_t num_data_blocks;
size_t hash_offset;
uint32_t hash_start_block;
char tmpfile[] = "/tmp/mkimage-verity-XXXXXX";
char dbs_arg[32], hbs_arg[32], algo_arg[80], hoff_arg[40];
int pipefd[2];
pid_t pid;
FILE *fp;
char line[256];
char *colon, *value, *end;
char root_hash_hex[256] = {0};
char salt_hex[256] = {0};
uint8_t digest_bin[FIT_MAX_HASH_LEN];
uint8_t salt_bin[FIT_MAX_HASH_LEN];
int digest_len = 0, salt_len = 0;
void *expanded = NULL;
struct stat st;
int fd, ret;
*expanded_data = NULL;
*expanded_size = 0;
algo_prop = fdt_getprop(fit, verity_noffset, FIT_VERITY_ALGO_PROP,
NULL);
if (!algo_prop) {
fprintf(stderr,
"Missing '%s' in dm-verity node of '%s'\n",
FIT_VERITY_ALGO_PROP, image_name);
return -EINVAL;
}
/* Local copy -- the FDT pointer goes stale after fdt_setprop(). */
snprintf(algo, sizeof(algo), "%s", algo_prop);
val = fdt_getprop(fit, verity_noffset, FIT_VERITY_DBS_PROP, NULL);
if (!val) {
fprintf(stderr,
"Missing '%s' in dm-verity node of '%s'\n",
FIT_VERITY_DBS_PROP, image_name);
return -EINVAL;
}
data_block_size = fdt32_to_cpu(*val);
val = fdt_getprop(fit, verity_noffset, FIT_VERITY_HBS_PROP, NULL);
if (!val) {
fprintf(stderr,
"Missing '%s' in dm-verity node of '%s'\n",
FIT_VERITY_HBS_PROP, image_name);
return -EINVAL;
}
hash_block_size = fdt32_to_cpu(*val);
if (data_block_size < 512 || (data_block_size & (data_block_size - 1)) ||
hash_block_size < 512 || (hash_block_size & (hash_block_size - 1))) {
fprintf(stderr,
"Block sizes must be >= 512 and a power of two in dm-verity node of '%s'\n",
image_name);
return -EINVAL;
}
if (data_size % data_block_size) {
fprintf(stderr,
"Image '%s' size %zu not a multiple of data-block-size %d\n",
image_name, data_size, data_block_size);
return -EINVAL;
}
if (data_size / data_block_size > UINT32_MAX ||
data_size / hash_block_size > UINT32_MAX) {
fprintf(stderr,
"Image '%s' too large for dm-verity (> 2^32 blocks)\n",
image_name);
return -EINVAL;
}
num_data_blocks = data_size / data_block_size;
hash_offset = data_size;
fd = mkstemp(tmpfile);
if (fd < 0) {
fprintf(stderr, "Can't create temp file: %s\n",
strerror(errno));
return -EIO;
}
if (write(fd, data, data_size) != (ssize_t)data_size) {
fprintf(stderr, "Can't write temp file: %s\n",
strerror(errno));
ret = -EIO;
goto err_unlink;
}
close(fd);
fd = -1;
/*
* Invoke veritysetup via fork/execvp -- no shell, so each argument
* goes verbatim to the binary and the algo string cannot inject
* additional commands no matter how crafted the .its is.
*/
snprintf(algo_arg, sizeof(algo_arg), "--hash=%s", algo);
snprintf(dbs_arg, sizeof(dbs_arg), "--data-block-size=%u",
data_block_size);
snprintf(hbs_arg, sizeof(hbs_arg), "--hash-block-size=%u",
hash_block_size);
snprintf(hoff_arg, sizeof(hoff_arg), "--hash-offset=%zu", hash_offset);
if (pipe(pipefd) < 0) {
fprintf(stderr, "Can't create pipe: %s\n", strerror(errno));
ret = -EIO;
goto err_unlink;
}
pid = fork();
if (pid < 0) {
fprintf(stderr, "Can't fork: %s\n", strerror(errno));
close(pipefd[0]);
close(pipefd[1]);
ret = -EIO;
goto err_unlink;
}
if (pid == 0) {
/* child: redirect stdout+stderr to pipe write-end, then exec */
char *argv[] = {
"veritysetup", "format", tmpfile, tmpfile,
"--no-superblock", algo_arg, dbs_arg, hbs_arg,
hoff_arg, NULL,
};
close(pipefd[0]);
if (dup2(pipefd[1], STDOUT_FILENO) < 0 ||
dup2(pipefd[1], STDERR_FILENO) < 0)
_exit(127);
close(pipefd[1]);
execvp(argv[0], argv);
fprintf(stderr, "Can't exec veritysetup: %s\n",
strerror(errno));
_exit(127);
}
/* parent: parse key: value lines from veritysetup stdout */
close(pipefd[1]);
fp = fdopen(pipefd[0], "r");
if (!fp) {
fprintf(stderr, "Can't fdopen veritysetup pipe: %s\n",
strerror(errno));
close(pipefd[0]);
waitpid(pid, NULL, 0);
ret = -EIO;
goto err_unlink;
}
while (fgets(line, sizeof(line), fp)) {
colon = strchr(line, ':');
if (!colon)
continue;
value = colon + 1;
while (*value == ' ' || *value == '\t')
value++;
end = value + strlen(value) - 1;
while (end > value && (*end == '\n' || *end == '\r' ||
*end == ' '))
*end-- = '\0';
if (!strncmp(line, "Root hash:", 10))
snprintf(root_hash_hex, sizeof(root_hash_hex),
"%s", value);
else if (!strncmp(line, "Salt:", 5))
snprintf(salt_hex, sizeof(salt_hex), "%s", value);
}
fclose(fp);
if (waitpid(pid, &ret, 0) < 0 || !WIFEXITED(ret) ||
WEXITSTATUS(ret) != 0) {
fprintf(stderr, "veritysetup failed for '%s'\n", image_name);
ret = -EIO;
goto err_unlink;
}
if (!root_hash_hex[0] || !salt_hex[0]) {
fprintf(stderr, "Failed to parse veritysetup output for '%s'\n",
image_name);
ret = -EIO;
goto err_unlink;
}
digest_len = strlen(root_hash_hex) / 2;
salt_len = strlen(salt_hex) / 2;
if (digest_len > (int)sizeof(digest_bin) ||
salt_len > (int)sizeof(salt_bin)) {
fprintf(stderr, "Hash/salt too long for '%s'\n", image_name);
ret = -EINVAL;
goto err_unlink;
}
if (hex2bin(digest_bin, root_hash_hex, digest_len) ||
hex2bin(salt_bin, salt_hex, salt_len)) {
fprintf(stderr, "Invalid hex in veritysetup output for '%s'\n",
image_name);
ret = -EINVAL;
goto err_unlink;
}
if (stat(tmpfile, &st)) {
fprintf(stderr, "Can't stat temp file: %s\n",
strerror(errno));
ret = -EIO;
goto err_unlink;
}
expanded = malloc(st.st_size);
if (!expanded) {
ret = -ENOMEM;
goto err_unlink;
}
fd = open(tmpfile, O_RDONLY);
if (fd < 0 || read(fd, expanded, st.st_size) != st.st_size) {
fprintf(stderr, "Can't read back temp file: %s\n",
strerror(errno));
ret = -EIO;
goto err_free;
}
close(fd);
fd = -1;
/* Temp file is no longer needed -- expanded buffer lives in memory. */
unlink(tmpfile);
/* hash tree starts immediately after data (no superblock) */
hash_start_block = hash_offset / hash_block_size;
ret = fdt_setprop(fit, verity_noffset, FIT_VERITY_DIGEST_PROP,
digest_bin, digest_len);
if (ret) {
ret = (ret == -FDT_ERR_NOSPACE) ? -ENOSPC : -EIO;
goto err_free;
}
ret = fdt_setprop(fit, verity_noffset, FIT_VERITY_SALT_PROP,
salt_bin, salt_len);
if (ret) {
ret = (ret == -FDT_ERR_NOSPACE) ? -ENOSPC : -EIO;
goto err_free;
}
ret = fdt_setprop_u32(fit, verity_noffset, FIT_VERITY_NBLK_PROP,
num_data_blocks);
if (ret) {
ret = (ret == -FDT_ERR_NOSPACE) ? -ENOSPC : -EIO;
goto err_free;
}
ret = fdt_setprop_u32(fit, verity_noffset, FIT_VERITY_HBLK_PROP,
hash_start_block);
if (ret) {
ret = (ret == -FDT_ERR_NOSPACE) ? -ENOSPC : -EIO;
goto err_free;
}
/*
* Stash the expanded buffer in the in-process cache; fit_extract_data()
* looks it up via fit_verity_get_expanded() to populate the external
* data section. On success the cache takes ownership of @expanded.
*/
ret = fit_verity_stash(image_name, expanded, st.st_size);
if (ret)
goto err_free;
*expanded_data = expanded;
*expanded_size = st.st_size;
return 0;
err_free:
free(expanded);
err_unlink:
if (fd >= 0)
close(fd);
unlink(tmpfile);
return ret;
}
/**
* fit_image_add_verification_data() - calculate/set verig. data for image node
*
@@ -652,6 +1026,8 @@ int fit_image_cipher_data(const char *keydir, void *keydest,
*
* For signature details, please see doc/usage/fit/signature.rst
*
* For dm-verity details, please see doc/usage/fit/dm-verity.rst
*
* @keydir Directory containing *.key and *.crt files (or NULL)
* @keydest FDT Blob to write public keys into (NULL if none)
* @fit: Pointer to the FIT format image header
@@ -667,9 +1043,16 @@ int fit_image_add_verification_data(const char *keydir, const char *keyfile,
const char *cmdname, const char* algo_name)
{
const char *image_name;
const char *node_name;
const void *data;
size_t size;
/*
* View pointer into the dm-verity cache (owned by image-host.c).
* Do not free; the cache lives until mkimage exits.
*/
void *verity_data = NULL;
int noffset;
int ret;
/* Get image data and data length */
if (fit_image_get_emb_data(fit, image_noffset, &data, &size)) {
@@ -679,13 +1062,38 @@ int fit_image_add_verification_data(const char *keydir, const char *keyfile,
image_name = fit_get_name(fit, image_noffset, NULL);
/* Process all hash subnodes of the component image node */
/*
* Pass 1 -- dm-verity: run veritysetup to produce the Merkle
* hash tree and fill in computed metadata. The expanded
* content (original data + hash tree) is returned in
* verity_data so that pass 2 hashes the complete image.
*/
for (noffset = fdt_first_subnode(fit, image_noffset);
noffset >= 0;
noffset = fdt_next_subnode(fit, noffset)) {
const char *node_name;
int ret = 0;
if (!strcmp(fit_get_name(fit, noffset, NULL),
FIT_VERITY_NODENAME)) {
ret = fit_image_process_verity(fit, image_name,
noffset,
data, size,
&verity_data,
&size);
if (ret)
return ret;
if (verity_data)
data = verity_data;
break;
}
}
/*
* Pass 2 -- hashes and signatures: compute over the (possibly
* expanded) image data.
*/
for (noffset = fdt_first_subnode(fit, image_noffset);
noffset >= 0;
noffset = fdt_next_subnode(fit, noffset)) {
ret = 0;
/*
* Check subnode name, must be equal to "hash" or "signature".
* Multiple hash nodes require unique unit node