Skip to content

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

Merged
danielinux merged 1 commit into
wolfSSL:masterfrom
dgarske:fenrir_7998_dtb_auth
Aug 13, 2026
Merged

Authenticate the raw device tree against the signed firmware image#852
danielinux merged 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.

Hardware: confirmed end-to-end on a ZCU102 (ZynqMP, xczu9eg). Boot chain FSBL -> PMUFW -> BL31 -> wolfBoot (EL2) from QSPI, with the signed app at 0x800000 and the raw DTB at 0x7B0000; wolfBoot reads the DTB from external flash and authenticates it against the app's HDR_DEVICE_TREE_DIGEST before boot.

  • Matching DTB: Verifying signature...done / Firmware Valid / DTB digest verified / Booting at 0x10000000 -> app runs (Application running successfully!).
  • Tampered DTB (one flipped byte, digest no longer matches the signed app): DTB digest mismatch - rejecting / wolfBoot: PANIC! -- refuses to boot.

The raw DTB loads via the external-flash fallback here (hal_get_dts_address() returns a flash offset that is not memory-mapped under NO_XIP, so it is ignored -- "DTB parse/size check failed - ignoring" -- and the ext-flash path reads and authenticates it). SHA3-384 hash config, RSA-4096 image signing.

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 Outdated
Comment thread src/update_ram.c
@dgarske
dgarske force-pushed the fenrir_7998_dtb_auth branch from bdf1bd7 to 9482c5f Compare August 12, 2026 18:02
@dgarske
dgarske marked this pull request as ready for review August 12, 2026 18:02
@dgarske dgarske assigned danielinux and wolfSSL-Bot and unassigned dgarske Aug 12, 2026
@dgarske
dgarske requested a review from danielinux August 12, 2026 20:15
@danielinux
danielinux merged commit c8faa5a into wolfSSL:master Aug 13, 2026
409 checks passed
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.

4 participants