diff --git a/boot/Kconfig b/boot/Kconfig index ae6f09a6ede..e1114aea843 100644 --- a/boot/Kconfig +++ b/boot/Kconfig @@ -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 diff --git a/boot/bootm.c b/boot/bootm.c index 4836d6b2d41..ec74873b503 100644 --- a/boot/bootm.c +++ b/boot/bootm.c @@ -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); diff --git a/boot/image-board.c b/boot/image-board.c index 005d60caf5c..265f29d44ff 100644 --- a/boot/image-board.c +++ b/boot/image-board.c @@ -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"); diff --git a/boot/image-fit.c b/boot/image-fit.c index 884a5105df5..1cf8e7be957 100644 --- a/boot/image-fit.c +++ b/boot/image-fit.c @@ -21,8 +21,11 @@ extern void *aligned_alloc(size_t alignment, size_t size); #else #include +#include #include +#include #include +#include #include #include #include @@ -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 */ diff --git a/configs/sandbox64_defconfig b/configs/sandbox64_defconfig index 5bf6146b1d0..f5d5b21e733 100644 --- a/configs/sandbox64_defconfig +++ b/configs/sandbox64_defconfig @@ -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 diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig index ba800f7d19d..ff4a6eb285a 100644 --- a/configs/sandbox_defconfig +++ b/configs/sandbox_defconfig @@ -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 diff --git a/configs/sandbox_flattree_defconfig b/configs/sandbox_flattree_defconfig index a14dd5beb31..4ad2bb01673 100644 --- a/configs/sandbox_flattree_defconfig +++ b/configs/sandbox_flattree_defconfig @@ -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 diff --git a/doc/usage/fit/dm-verity.rst b/doc/usage/fit/dm-verity.rst new file mode 100644 index 00000000000..800a18fceae --- /dev/null +++ b/doc/usage/fit/dm-verity.rst @@ -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 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 `` 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. diff --git a/doc/usage/fit/index.rst b/doc/usage/fit/index.rst index 6c78d8584ed..d17582b1d64 100644 --- a/doc/usage/fit/index.rst +++ b/doc/usage/fit/index.rst @@ -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 diff --git a/include/hexdump.h b/include/hexdump.h index f2ca4793d69..5cb48d79efe 100644 --- a/include/hexdump.h +++ b/include/hexdump.h @@ -7,7 +7,11 @@ #ifndef HEXDUMP_H #define HEXDUMP_H +#ifdef USE_HOSTCC +#include +#else #include +#endif #include 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++); diff --git a/include/image.h b/include/image.h index 34efac6056d..7b16284257a 100644 --- a/include/image.h +++ b/include/image.h @@ -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 * diff --git a/test/boot/Makefile b/test/boot/Makefile index 89538d4f0a6..d98f212b243 100644 --- a/test/boot/Makefile +++ b/test/boot/Makefile @@ -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 diff --git a/test/boot/fit_verity.c b/test/boot/fit_verity.c new file mode 100644 index 00000000000..7459a9d6f81 --- /dev/null +++ b/test/boot/fit_verity.c @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Tests for FIT dm-verity cmdline generation + * + * Copyright 2026 Daniel Golle + */ + +#include +#include +#include + +#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); diff --git a/test/cmd_ut.c b/test/cmd_ut.c index 44e5fdfdaa6..d1b376f617c 100644 --- a/test/cmd_ut.c +++ b/test/cmd_ut.c @@ -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"), diff --git a/test/py/tests/test_fit_verity.py b/test/py/tests/test_fit_verity.py new file mode 100644 index 00000000000..f1b6262ed0e --- /dev/null +++ b/test/py/tests/test_fit_verity.py @@ -0,0 +1,175 @@ +# SPDX-License-Identifier: GPL-2.0+ +# +# Copyright 2026 Daniel Golle + +""" +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') diff --git a/tools/fit_image.c b/tools/fit_image.c index 1dbc14c63e4..b53088bf783 100644 --- a/tools/fit_image.c +++ b/tools/fit_image.c @@ -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); diff --git a/tools/image-host.c b/tools/image-host.c index 8b550af0dc1..8f1e7be4066 100644 --- a/tools/image-host.c +++ b/tools/image-host.c @@ -12,8 +12,12 @@ #include #include #include +#include #include +#include +#include + #if CONFIG_IS_ENABLED(FIT_SIGNATURE) #include #include @@ -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