Skip to content

Authenticate the raw device tree against the signed firmware image - #852

Draft
dgarske wants to merge 1 commit into
wolfSSL:masterfrom
dgarske:fenrir_7998_dtb_auth
Draft

Authenticate the raw device tree against the signed firmware image#852
dgarske wants to merge 1 commit into
wolfSSL:masterfrom
dgarske:fenrir_7998_dtb_auth

Conversation

@dgarske

@dgarske dgarske commented Aug 11, 2026

Copy link
Copy Markdown
Member

Fenrir 7998

Problem

For non-FIT MMU boots, wolfBoot_start() loads a separate raw device tree (DTB) after the OS image is authenticated and hands it to the kernel with no check of its own -- only an FDT magic/version parse. An attacker who can write the DTS flash region can swap in a structurally valid tree that changes /chosen/bootargs or other kernel-visible policy while the signed kernel stays intact: a secure-boot bypass. A DTB inside a signed FIT is unaffected (it is covered by the FIT signature); this only concerns the raw PART_DTS_BOOT / hal_get_dts_address() path.

Fix

Bind the raw DTB to the firmware image with a signature-covered digest and verify it before the tree reaches the kernel.

  • Sign time: sign --dts <board.dtb> hashes exactly the first fdt_totalsize bytes of the device tree with the image hash algorithm and stores the result as a new signature-covered TLV, HDR_DEVICE_TREE_DIGEST (tag 0x35) -- mirroring the existing --cmdline / HDR_CMDLINE pattern. The signer applies at least the checks the bootloader's fdt_check_header() applies.
  • Boot time: wolfBoot_start() snapshots the digest from the verified image header, loads the raw DTB, hashes it, and rejects a mismatch with wolfBoot_panic() (fault-hardened compare). The digest binds the tree to that firmware version, so an unrelated or rolled-back DTB cannot pair with it.

Enforcement is backward compatible: a DTB that carries the digest is always verified and a mismatch always panics; a raw DTB with no digest only warns and boots unless the build opts in with WOLFBOOT_REQUIRE_SIGNED_DTB=1 (plumbed through options.mk with a $(warning)). Existing MMU targets that boot an unsigned raw DTB keep working until they adopt sign --dts.

Raw-DTB load path: the pre-existing PART_IS_EXT(&os_image) proxy left the raw DTS partition unreachable on EXT_FLASH + NO_XIP (RAMBOOT) targets. hal_get_dts_address() remains the primary source (unchanged for memory-mapped targets); when it has no usable address, the DTB is read directly from external flash at WOLFBOOT_DTS_BOOT_ADDRESS. Both paths clamp the DTB to WOLFBOOT_DTS_MAX_SIZE before copying and check every ext_flash_read length, so an attacker-inflated fdt_totalsize or a short read cannot overflow or forward a partial tree.

Usage

./tools/keytools/sign --ecc256 --sha256 --dts board.dtb kernel.elf priv.der 1

Stage the same board.dtb in the raw DTS region (WOLFBOOT_DTS_BOOT_ADDRESS). Build with WOLFBOOT_REQUIRE_SIGNED_DTB=1 to fail closed once every raw-DTB payload is signed. A DTB inside a signed FIT does not need --dts.

Testing

Software (all passing):

  • unit-image-dts / -sha384 / -sha3-384 -- wolfBoot_verify_dts_digest() and wolfBoot_hash_buffer() for each hash: match accepts, tamper/wrong/short/NULL reject.
  • unit-sign-dts.py -- sign side across SHA256/384/3: correct HDR_DEVICE_TREE_DIGEST; bad-magic/truncated/unsupported-version/short-body rejected with a clean non-zero exit (guards the fixed double-free, previously SIGABRT); --dts+--delta rejected; missing-arg handled.
  • Regression: unit-image, unit-update-ram*, unit-update-disk*, unit-fdt green; ZynqMP firmware builds clean for both SHA3 and SHA256.

Notes for reviewers

  • The digest TLV is placed before the hash/signature TLVs, so it is covered by the image signature (verified: two different DTBs yield two different image digests).
  • The digest is verified on the pristine DTB before any hal_dts_fixup() runs; the helper takes a pre-captured digest because the DTS load may reuse os_image.

@dgarske dgarske self-assigned this Aug 11, 2026
Copilot AI lite review requested due to automatic review settings August 11, 2026 18:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens secure boot for non-FIT MMU targets by cryptographically binding a raw DTB (loaded from a separate flash region) to the signed firmware image, preventing DTB tampering (e.g., /chosen/bootargs) while keeping the kernel image intact.

Changes:

  • Adds a new signature-covered TLV (HDR_DEVICE_TREE_DIGEST, tag 0x35) and implements sign --dts <dtb> to hash/sign the DTB’s declared fdt_totalsize span.
  • Updates the MMU raw-DTB boot path to load the DTB safely (size clamp + checked reads) and verify its digest, with optional fail-closed enforcement via WOLFBOOT_REQUIRE_SIGNED_DTB=1.
  • Adds/extends unit tests and documentation for the new signing and boot-time verification behavior.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tools/unit-tests/unit-sign-dts.py New regression test for sign --dts DTB hashing/TLV emission and failure modes
tools/unit-tests/unit-image.c Adds unit test for wolfBoot_verify_dts_digest() across supported hashes
tools/unit-tests/Makefile Adds new unit-test targets for DTB digest verification and runs unit-sign-dts.py
tools/scripts/zynq7000/prepare_linux.sh Updates RAW-DTB signing flow to use --dts and documents enforcement option
tools/keytools/sign.c Implements --dts option and DTB hashing into a signature-covered TLV
src/update_ram.c Loads raw DTB via HAL or ext-flash fallback and enforces digest verification policy
src/image.c Adds wolfBoot_hash_buffer() + wolfBoot_verify_dts_digest() helper
options.mk Adds WOLFBOOT_REQUIRE_SIGNED_DTB build option with warning
include/wolfboot/wolfboot.h Defines HDR_DEVICE_TREE_DIGEST tag
include/image.h Exposes DTS helper prototypes under `MMU
hal/zynq7000.c Updates comments to reflect new DTB authentication behavior
docs/Targets.md Documents raw-DTB authentication and enforcement behavior for relevant targets
docs/Signing.md Documents new --dts option, behavior, and constraints
docs/compile.md Documents WOLFBOOT_REQUIRE_SIGNED_DTB for raw-DTB targets
config/examples/zynq7000.config Updates partition comments to reflect raw DTB + digest authentication
.gitignore Ignores new unit-test binaries
Suppressed comments (1)

src/update_ram.c:673

  • The external-flash DTB path treats any positive fdt_totalsize as valid. Since fdt_check_header() doesn’t bound totalsize, a crafted header with totalsize < 40 would be accepted and only that short prefix would be loaded/forwarded. Enforce the same minimum header size used by the signer (40 bytes).
            ret = ext_flash_read((uintptr_t)WOLFBOOT_DTS_BOOT_ADDRESS,
                    dts_hdr, (int)sizeof(dts_hdr));
            if (ret == (int)sizeof(dts_hdr)) {
                ret = wolfBoot_get_dts_size(dts_hdr);
                if (ret > 0 && (uint32_t)ret <= WOLFBOOT_DTS_MAX_SIZE) {

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/keytools/sign.c
Comment on lines +1347 to +1348
fprintf(stderr, "Device tree file %s is %ld bytes but its FDT totalsize "
"is %u (truncated): %s\n", file, fsz, total, file);
Comment thread src/update_ram.c
Comment on lines +651 to +657
dts_addr = hal_get_dts_address();
if (dts_addr != NULL) {
ret = wolfBoot_get_dts_size(dts_addr);
if (ret < 0 || (uint32_t)ret > WOLFBOOT_DTS_MAX_SIZE) {
wolfBoot_printf("DTB parse/size check failed - ignoring\n");
dts_addr = NULL; /* never forward an unvalidated address */
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants