Authenticate raw device tree via signed HDR_DEVICE_TREE_DIGEST TLV (Fenrir #7998)

pull/858/head
David Garske 2026-08-10 16:53:38 -07:00 committed by Daniele Lacamera
parent a79bace772
commit c8faa5adc2
17 changed files with 783 additions and 43 deletions

3
.gitignore vendored
View File

@ -170,6 +170,9 @@ tools/unit-tests/unit-tpm-rsa-exp
tools/unit-tests/unit-image-nopart
tools/unit-tests/unit-image-sha3-384
tools/unit-tests/unit-image-sha384
tools/unit-tests/unit-image-dts
tools/unit-tests/unit-image-dts-sha384
tools/unit-tests/unit-image-dts-sha3-384
tools/unit-tests/unit-store-sbrk
tools/unit-tests/unit-tpm-blob
tools/unit-tests/unit-update-disk

View File

@ -46,9 +46,11 @@ NO_XIP=1
# QSPI partition layout (16 MB total) - sized for a full Linux kernel + DTB
# pair so the same layout also works for bare-metal payloads.
# 0x000000 - 0x07FFFF BOOT.BIN (FSBL + wolfboot, 512 KB)
# 0x080000 - 0x0FFFFF DTS_BOOT (signed DTB, 512 KB - Linux only)
# 0x080000 - 0x0FFFFF DTS_BOOT (raw DTB, 512 KB - Linux only; authenticated
# via the boot image HDR_DEVICE_TREE_DIGEST
# TLV, see 'sign --dts')
# 0x100000 - 0x6FFFFF BOOT_A (~6 MB primary)
# 0x700000 - 0x77FFFF DTS_UPD (signed update DTB, 512 KB - Linux only)
# 0x700000 - 0x77FFFF DTS_UPD (raw update DTB, 512 KB - Linux only)
# 0x780000 - 0xDFFFFF UPDATE_B (~6.5 MB update)
# 0xE00000 - 0xE0FFFF SWAP (64 KB scratch)
WOLFBOOT_PARTITION_BOOT_ADDRESS=0x00100000

View File

@ -293,6 +293,25 @@ Provides a value to be set with a custom tag
altered without breaking the image signature. Example:
`--cmdline "root=/dev/mmcblk0p2 rw rootwait console=ttyTCU0,115200"`.
* `--dts filename`: Binds a raw (non-FIT) device tree blob to the firmware image. The sign
tool hashes exactly the first `fdt_totalsize` bytes of the `.dtb` (validating the FDT magic
and version the same way the bootloader does) with the image hash algorithm and stores the
digest as a signature-covered TLV using the wolfBoot-reserved tag `HDR_DEVICE_TREE_DIGEST`
(0x35). At boot, the non-FIT MMU path (`src/update_ram.c`) hashes the DTB it loads from the
raw DTS partition or `hal_get_dts_address()` and compares it against this digest before
handing the tree to the kernel; a mismatch triggers `wolfBoot_panic()`. This prevents an
attacker who can write the DTS flash region from altering `/chosen/bootargs` or other
kernel-visible policy while leaving the signed kernel intact. Example: `--dts board.dtb`.
Note: the DTB and the kernel image are signed together, so re-signing the kernel is required
whenever the device tree changes. A DTB delivered inside a signed FIT image is already
covered by the FIT's signature and does not need `--dts`.
Enforcement of a *missing* digest is opt-in for backward compatibility: a firmware image that
carries the digest is always verified, but a raw DTB with no `HDR_DEVICE_TREE_DIGEST` only
warns and boots unless wolfBoot is built with `WOLFBOOT_REQUIRE_SIGNED_DTB=1` (see
`options.mk`), which makes the missing digest a hard failure. Adopt `--dts` on every raw-DTB
payload first, then set `WOLFBOOT_REQUIRE_SIGNED_DTB=1` to fail closed.
The 65524-byte maximum is the largest TLV value the wolfBoot header parser can walk
past when locating the fields that follow it, such as the signature.
@ -301,8 +320,8 @@ Provides a value to be set with a custom tag
must be built with a matching `IMAGE_HEADER_SIZE`, or it will fail to locate the firmware
image at boot.
Note: all options, including `--cmdline` and the `--custom-tlv*` options, must appear **before**
the positional `image key version` arguments.
Note: all options, including `--cmdline`, `--dts`, and the `--custom-tlv*` options, must appear
**before** the positional `image key version` arguments.
#### Three-steps signing using external provisioning tools

View File

@ -4332,7 +4332,7 @@ Key options in `config/examples/zynq7000.config`:
- `MMU=1 ELF=1` - lets the same image boot Linux or bare-metal. `do_boot` always emits the ARM Linux boot ABI (`r0=0`, `r1=~0`, `r2=DTB_phys`, `r3=0`) on this target, which bare-metal apps simply ignore. `MMU=1` enables `update_ram.c`'s DTB-load codepath and pulls in `src/fdt.o`; wolfBoot does not manage page tables (it inherits FSBL's flat 1:1 DDR mapping). `ELF=1` lets wolfBoot understand ELF inputs (e.g. `vmlinux`) and load only their LOAD segments. Cost over a strictly bare-metal-only build: ~5 KB extra wolfBoot binary (31 KB -> 36 KB).
- `EXT_FLASH=1` - QSPI as external flash via `XQspiPs`
- `WOLFBOOT_LOAD_ADDRESS=0x10000000` - DDR offset 256 MB, where the verified app is staged before `do_boot`. Must be **above** wolfBoot's own region (`0x04000000`-`0x040FFFFF`) because `src/update_ram.c` enforces `dst > _end`.
- `WOLFBOOT_LOAD_DTS_ADDRESS=0x11000000` - DDR offset 272 MB, where a DTB read out of `PART_DTS_BOOT` would be relocated. Ignored for bare-metal payloads and for the appended-DTB Linux flow (where the DTB lives at the end of the signed kernel image).
- `WOLFBOOT_LOAD_DTS_ADDRESS=0x11000000` - DDR offset 272 MB, where a DTB read out of `PART_DTS_BOOT` would be relocated. Ignored for bare-metal payloads and for the appended-DTB Linux flow (where the DTB lives at the end of the signed kernel image). A DTB loaded this way (raw `PART_DTS_BOOT` or `hal_get_dts_address()`, i.e. not inside a signed FIT) is authenticated against the boot image's `HDR_DEVICE_TREE_DIGEST` TLV before the kernel sees it: sign the kernel with `sign --dts <board.dtb>` (see `docs/Signing.md`). When the image carries the digest it is always verified and a mismatch panics. A raw DTB with no digest only warns and boots by default (backward compatible); build with `WOLFBOOT_REQUIRE_SIGNED_DTB=1` to make a missing digest a hard failure once every raw-DTB payload is signed with `--dts`.
- `WOLFBOOT_PARTITION_BOOT_ADDRESS=0x00100000` - 16 MB QSPI layout below
- `CROSS_COMPILE=arm-none-eabi-`

View File

@ -280,6 +280,10 @@ downgrades, compile with `ALLOW_DOWNGRADE=1`.
Warning: this option will disable version checking before the updates, thus exposing the system to potential
forced downgrade attacks.
### Require an authenticated device tree (raw-DTB targets)
On non-FIT MMU targets that load a raw device tree from flash, wolfBoot authenticates the DTB against the `HDR_DEVICE_TREE_DIGEST` TLV bound to the signed kernel (`sign --dts <board.dtb>`, see `docs/Signing.md`). A DTB carrying the digest is always verified; a raw DTB with no digest only warns and boots by default. Compile with `WOLFBOOT_REQUIRE_SIGNED_DTB=1` to make a missing digest a hard failure (fail-closed) once every raw-DTB payload is signed with `--dts`.
### Enable optional support for external flash memory
WolfBoot can be compiled with the makefile option `EXT_FLASH=1`. When the external flash support is

View File

@ -2591,7 +2591,12 @@ uint64_t hal_get_timer_us(void)
void* hal_get_dts_address(void)
{
#ifdef WOLFBOOT_DTS_BOOT_ADDRESS
#ifdef NO_XIP
/* QSPI is not memory-mapped: WOLFBOOT_DTS_BOOT_ADDRESS is a flash offset,
* not a CPU-dereferenceable address. Return NULL so update_ram.c reads and
* authenticates the DTB from external flash instead of dereferencing it. */
return NULL;
#elif defined(WOLFBOOT_DTS_BOOT_ADDRESS)
return (void*)WOLFBOOT_DTS_BOOT_ADDRESS;
#elif defined(WOLFBOOT_LOAD_DTS_ADDRESS)
return (void*)WOLFBOOT_LOAD_DTS_ADDRESS;

View File

@ -826,9 +826,10 @@ void ext_flash_unlock(void) { }
#ifdef MMU
/* Memory-mapped DTB fallback. Not used on Zynq-7000 with EXT_FLASH=1: the
* signed DTB is opened as PART_DTS_BOOT in update_ram.c and read out of
* QSPI via ext_flash_check_read. Return NULL so the fallback path is a
* no-op when the DTS partition is missing. */
* DTB is opened as PART_DTS_BOOT in update_ram.c and read out of QSPI via
* ext_flash_check_read, then authenticated against the boot image's
* HDR_DEVICE_TREE_DIGEST TLV before the kernel sees it. Return NULL so the
* fallback path is a no-op when the DTS partition is missing. */
void *hal_get_dts_address(void)
{
return NULL;

View File

@ -1712,6 +1712,11 @@ int image_CT_compare(const uint8_t *expected, const uint8_t *actual,
uint32_t len);
int wolfBoot_hardened_CT_compare(const uint8_t *expected, const uint8_t *actual,
uint32_t len);
#if defined(MMU) || defined(WOLFBOOT_FDT)
int wolfBoot_get_dts_size(void *dts_addr);
int wolfBoot_verify_dts_digest(const uint8_t *expected_digest,
const void *dts_addr, uint32_t dts_size);
#endif
int wolfBoot_open_image(struct wolfBoot_image *img, uint8_t part);
#ifdef EXT_FLASH
int wolfBoot_open_image_external(struct wolfBoot_image* img, uint8_t part, uint8_t* addr);

View File

@ -239,6 +239,9 @@ extern "C" {
* (0x0030-0xFEFE). Signature-covered, unlike an external cmdline file; consumed
* by the EFI targets and passed to the kernel via LoadOptions. */
#define HDR_CMDLINE 0x0034
/* Signature-covered digest of a raw (non-FIT) device tree, binding it to this
* image. Length = image hash size (WOLFBOOT_SHA_DIGEST_SIZE). */
#define HDR_DEVICE_TREE_DIGEST 0x35
#define HDR_PADDING 0xFF
/* Auth Key types */

View File

@ -919,6 +919,16 @@ ifeq ($(ALLOW_DOWNGRADE),1)
CFLAGS+= -D"ALLOW_DOWNGRADE"
endif
# Raw (non-FIT) device tree authentication (see docs/Signing.md '--dts'). A
# device tree bound to the image via HDR_DEVICE_TREE_DIGEST is always verified;
# WOLFBOOT_REQUIRE_SIGNED_DTB additionally makes a missing digest a hard failure
# (fail-closed) instead of a warning, once every raw-DTB payload is signed with
# 'sign --dts'.
ifeq ($(WOLFBOOT_REQUIRE_SIGNED_DTB),1)
$(warning WOLFBOOT_REQUIRE_SIGNED_DTB=1 makes wolfBoot panic on a raw device tree that carries no authenticated HDR_DEVICE_TREE_DIGEST; sign every raw DTB payload with 'sign --dts' first or the target will not boot)
CFLAGS+= -D"WOLFBOOT_REQUIRE_SIGNED_DTB"
endif
ifeq ($(WOLFBOOT_SKIP_BOOT_VERIFY),1)
ifneq ($(WOLFBOOT_SELF_HEADER),1)
$(error WOLFBOOT_SKIP_BOOT_VERIFY=1 requires WOLFBOOT_SELF_HEADER=1)

View File

@ -1549,6 +1549,92 @@ int wolfBoot_get_dts_size(void *dts_addr)
return ret;
}
/* Hash a raw buffer with the configured image hash (explicit per-algorithm API,
* since the generic update_hash macro's SHA3 mapping is wrong). 0 on success. */
static int wolfBoot_hash_buffer(const void *buf, uint32_t len, uint8_t *out)
{
const uint8_t *p = (const uint8_t *)buf;
int ret;
#if defined(WOLFBOOT_HASH_SHA256)
wc_Sha256 ctx;
ret = wc_InitSha256_ex(&ctx, NULL, WOLFBOOT_DEVID_HASH);
if (ret == 0) {
while (len > 0) {
uint32_t sz = (len < WOLFBOOT_SHA_BLOCK_SIZE) ?
len : (uint32_t)WOLFBOOT_SHA_BLOCK_SIZE;
ret = wc_Sha256Update(&ctx, p, sz);
if (ret != 0)
break;
p += sz;
len -= sz;
}
if (ret == 0)
ret = wc_Sha256Final(&ctx, out);
wc_Sha256Free(&ctx);
}
#elif defined(WOLFBOOT_HASH_SHA384)
wc_Sha384 ctx;
ret = wc_InitSha384_ex(&ctx, NULL, WOLFBOOT_DEVID_HASH);
if (ret == 0) {
while (len > 0) {
uint32_t sz = (len < WOLFBOOT_SHA_BLOCK_SIZE) ?
len : (uint32_t)WOLFBOOT_SHA_BLOCK_SIZE;
ret = wc_Sha384Update(&ctx, p, sz);
if (ret != 0)
break;
p += sz;
len -= sz;
}
if (ret == 0)
ret = wc_Sha384Final(&ctx, out);
wc_Sha384Free(&ctx);
}
#elif defined(WOLFBOOT_HASH_SHA3_384)
wc_Sha3 ctx;
ret = wc_InitSha3_384(&ctx, NULL, WOLFBOOT_DEVID_HASH);
if (ret == 0) {
while (len > 0) {
uint32_t sz = (len < WOLFBOOT_SHA_BLOCK_SIZE) ?
len : (uint32_t)WOLFBOOT_SHA_BLOCK_SIZE;
ret = wc_Sha3_384_Update(&ctx, p, sz);
if (ret != 0)
break;
p += sz;
len -= sz;
}
if (ret == 0)
ret = wc_Sha3_384_Final(&ctx, out);
wc_Sha3_384_Free(&ctx);
}
#else
(void)p;
ret = -1;
#endif
return (ret == 0) ? 0 : -1;
}
/* Verify a raw DTB against a firmware-bound digest (from the image's
* HDR_DEVICE_TREE_DIGEST TLV, captured by the caller since the load may reuse
* the image struct). Returns 0 on match, -1 on mismatch/bad args/hash error. */
int wolfBoot_verify_dts_digest(const uint8_t *expected_digest,
const void *dts_addr, uint32_t dts_size)
{
uint8_t calc[WOLFBOOT_SHA_DIGEST_SIZE];
if (expected_digest == NULL || dts_addr == NULL || dts_size == 0)
return -1;
if (wolfBoot_hash_buffer(dts_addr, dts_size, calc) != 0)
return -1;
if (wolfBoot_hardened_CT_compare(expected_digest, calc,
WOLFBOOT_SHA_DIGEST_SIZE) != 0) {
return -1;
}
return 0;
}
#endif /* MMU || WOLFBOOT_FDT */
#ifdef WOLFBOOT_FIXED_PARTITIONS

View File

@ -46,11 +46,22 @@
#endif
extern void hal_flash_dualbank_swap(void);
extern int wolfBoot_get_dts_size(void *dts_addr);
/* DTS helpers declared in include/image.h under (MMU || WOLFBOOT_FDT). */
extern uint32_t kernel_load_addr;
extern uint32_t dts_load_addr;
#if defined(MMU) || defined(WOLFBOOT_FDT)
/* Bounds for the attacker-influenced fdt_totalsize before relocating a DTB.
* MIN is the FDT v17 header size (also enforced by the signer): fdt_check_header
* validates magic/version but not totalsize, so a crafted header with a tiny
* totalsize must be rejected rather than loaded/forwarded as a partial tree. */
#ifndef WOLFBOOT_DTS_MAX_SIZE
#define WOLFBOOT_DTS_MAX_SIZE (1024U * 1024U)
#endif
#define WOLFBOOT_DTS_MIN_SIZE (40U)
#endif
#if defined(__WOLFBOOT) && defined(WOLFBOOT_LOAD_ADDRESS)
extern uint8_t _end[]; /* linker symbol: end of wolfBoot BSS */
#endif
@ -292,6 +303,14 @@ void RAMFUNCTION wolfBoot_start(void)
#endif
#ifdef MMU
uint32_t dts_size = 0;
/* HDR_DEVICE_TREE_DIGEST snapshot, taken before the raw DTB is loaded. */
uint8_t dts_digest[WOLFBOOT_SHA_DIGEST_SIZE];
uint8_t *dts_tlv = NULL;
uint16_t dts_tlv_len = 0;
int dts_digest_present = 0; /* 0 absent, 1 valid, -1 malformed */
#if defined(EXT_FLASH) && defined(WOLFBOOT_DTS_BOOT_ADDRESS)
uint8_t dts_hdr[64]; /* FDT header peek (fdt_check_header needs >= 40) */
#endif
#endif
#if defined(WOLFBOOT_ZYNQMP_FSBL) && defined(MMU)
/* When wolfBoot is the FSBL, the boot FIT carries an "atf" (BL31)
@ -573,6 +592,22 @@ backup_on_failure:
#endif
#ifdef MMU
/* Snapshot the digest from the verified header before os_image can be
* reused for the DTS partition below. (FIT DTBs are covered by the FIT.) */
dts_tlv_len = wolfBoot_get_header(&os_image, HDR_DEVICE_TREE_DIGEST,
&dts_tlv);
if (dts_tlv_len != 0 && dts_tlv != NULL) {
if (dts_tlv_len == WOLFBOOT_SHA_DIGEST_SIZE) {
memcpy(dts_digest, dts_tlv, WOLFBOOT_SHA_DIGEST_SIZE);
dts_digest_present = 1; /* present and well-formed */
}
else {
/* A present-but-malformed digest TLV must not silently downgrade
* to an unauthenticated DTB boot; treat it as a hard failure. */
dts_digest_present = -1;
}
}
/* Is this a Flattened uImage Tree (FIT) image (FDT format) */
if (wolfBoot_get_dts_size(load_address) > 0) {
void* fit = (void*)load_address;
@ -638,39 +673,77 @@ backup_on_failure:
#endif
}
else {
/* Load DTS to RAM */
#ifdef EXT_FLASH
if (PART_IS_EXT(&os_image) &&
wolfBoot_open_image(&os_image, PART_DTS_BOOT) >= 0) {
dts_addr = (uint8_t*)WOLFBOOT_LOAD_DTS_ADDRESS;
dts_size = (uint32_t)os_image.fw_size;
wolfBoot_printf("Loading DTS (size %lu) to RAM at %08lx\n",
(long unsigned int)dts_size, (long unsigned int)dts_addr);
ext_flash_check_read((uintptr_t)os_image.fw_base,
(uint8_t*)dts_addr, dts_size);
/* Prefer the HAL's memory-mapped DTB (unchanged for XIP targets); fall
* back to external flash at WOLFBOOT_DTS_BOOT_ADDRESS when the HAL has
* no usable address (NULL, or a flash offset on NO_XIP targets). */
dts_addr = hal_get_dts_address();
if (dts_addr != NULL) {
ret = wolfBoot_get_dts_size(dts_addr);
if (ret < (int)WOLFBOOT_DTS_MIN_SIZE ||
(uint32_t)ret > WOLFBOOT_DTS_MAX_SIZE) {
wolfBoot_printf("DTB parse/size check failed - ignoring\n");
dts_addr = NULL; /* never forward an unvalidated address */
}
else {
dts_size = (uint32_t)ret;
memcpy((void*)WOLFBOOT_LOAD_DTS_ADDRESS, dts_addr, dts_size);
dts_addr = (uint8_t*)WOLFBOOT_LOAD_DTS_ADDRESS;
}
}
else
#endif /* EXT_FLASH */
{
dts_addr = hal_get_dts_address();
if (dts_addr) {
ret = wolfBoot_get_dts_size(dts_addr);
if (ret < 0) {
wolfBoot_printf("Failed parsing DTB to load\n");
/* Allow failure, continue booting */
}
else {
/* relocate DTS to RAM */
uint8_t* dts_dst = (uint8_t*)WOLFBOOT_LOAD_DTS_ADDRESS;
#if defined(EXT_FLASH) && defined(WOLFBOOT_DTS_BOOT_ADDRESS)
if (dts_addr == NULL) {
/* Peek the FDT header for the size, clamp it, then read the body.
* Each ext_flash_read length is checked so a short/failed read
* never yields a partial or oversized tree. */
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 >= (int)WOLFBOOT_DTS_MIN_SIZE &&
(uint32_t)ret <= WOLFBOOT_DTS_MAX_SIZE) {
dts_size = (uint32_t)ret;
wolfBoot_printf("Loading DTB (size %d) from %p to RAM at %p\n",
dts_size, dts_addr, (void*)WOLFBOOT_LOAD_DTS_ADDRESS);
memcpy(dts_dst, dts_addr, dts_size);
dts_addr = dts_dst;
if (ext_flash_read((uintptr_t)WOLFBOOT_DTS_BOOT_ADDRESS,
(uint8_t*)WOLFBOOT_LOAD_DTS_ADDRESS, (int)dts_size)
== (int)dts_size)
dts_addr = (uint8_t*)WOLFBOOT_LOAD_DTS_ADDRESS;
else
dts_size = 0;
}
}
}
#endif /* EXT_FLASH && WOLFBOOT_DTS_BOOT_ADDRESS */
/* Authenticate the raw DTB before boot. dts_size == 0 with a non-NULL
* address (e.g. a zeroed fdt totalsize) is rejected. A bound digest is
* always enforced; a missing one only panics under
* WOLFBOOT_REQUIRE_SIGNED_DTB, so unsigned raw-DTB targets keep booting
* until they adopt 'sign --dts'. */
if (dts_addr != NULL) {
if (dts_size == 0) {
wolfBoot_printf("DTB has zero size - rejecting\n");
wolfBoot_panic();
}
if (dts_digest_present == 1) {
if (wolfBoot_verify_dts_digest(dts_digest, dts_addr, dts_size)
!= 0) {
wolfBoot_printf("DTB digest mismatch - rejecting\n");
wolfBoot_panic();
}
wolfBoot_printf("DTB digest verified\n");
}
else if (dts_digest_present < 0) {
wolfBoot_printf("Malformed DTB digest TLV - rejecting\n");
wolfBoot_panic();
}
else {
#ifdef WOLFBOOT_REQUIRE_SIGNED_DTB
wolfBoot_printf("No DTB digest - rejecting\n");
wolfBoot_panic();
#else
wolfBoot_printf("Warning: DTB not authenticated (sign --dts)\n");
#endif
}
}
}
#endif /* MMU */

View File

@ -163,6 +163,8 @@ static inline int fp_truncate(FILE *f, size_t len)
#define HDR_POLICY_SIGNATURE 0x21
#define HDR_SECONDARY_SIGNATURE 0x22
#define HDR_CERT_CHAIN 0x23
/* HDR_DEVICE_TREE_DIGEST comes from wolfboot/wolfboot.h (included above),
* matching how HDR_CMDLINE is consumed. */
#define HDR_SHA256_LEN 32
@ -389,6 +391,7 @@ struct cmd_options {
const char *encrypt_key_file;
const char *delta_base_file;
const char *cert_chain_file;
const char *dts_file;
int no_base_sha;
char output_image_file[PATH_MAX];
char output_diff_file[PATH_MAX];
@ -1333,6 +1336,147 @@ static uint32_t header_digest_size(int hash_algo)
}
}
/* Hash the first fdt_totalsize bytes of a DTB with the image hash algorithm
* (the same span the bootloader hashes). Applies at least fdt_check_header()'s
* checks (magic, version range); intentionally stricter. Writes digest to out,
* length to out_sz. Returns 0 on success, -1 on error. */
static int dts_hash_file(const char *file, int hash_algo, uint8_t *out,
uint32_t *out_sz)
{
/* FDT header (big-endian, 40B): magic@0, totalsize@4, version@0x14,
* last_comp_version@0x18. */
const uint32_t FDT_MAGIC = 0xd00dfeedU;
const uint32_t FDT_HDR_SIZE = 40U;
const uint32_t FDT_FIRST_VER = 0x10U;
const uint32_t FDT_LAST_COMP_VER = 0x11U;
FILE *f;
uint8_t hdr[40];
uint8_t rbuf[4096];
uint32_t magic, total, version, last_comp, remain;
long fsz;
size_t rd, want;
int ret = -1;
f = fopen(file, "rb");
if (f == NULL) {
fprintf(stderr, "Cannot open device tree file %s: %s\n",
file, strerror(errno));
return -1;
}
if (fread(hdr, 1, sizeof(hdr), f) != sizeof(hdr)) {
fprintf(stderr, "Device tree file %s too small for an FDT header\n",
file);
fclose(f);
return -1;
}
magic = ((uint32_t)hdr[0] << 24) | ((uint32_t)hdr[1] << 16) |
((uint32_t)hdr[2] << 8) | (uint32_t)hdr[3];
total = ((uint32_t)hdr[4] << 24) | ((uint32_t)hdr[5] << 16) |
((uint32_t)hdr[6] << 8) | (uint32_t)hdr[7];
version = ((uint32_t)hdr[0x14] << 24) | ((uint32_t)hdr[0x15] << 16) |
((uint32_t)hdr[0x16] << 8) | (uint32_t)hdr[0x17];
last_comp = ((uint32_t)hdr[0x18] << 24) | ((uint32_t)hdr[0x19] << 16) |
((uint32_t)hdr[0x1A] << 8) | (uint32_t)hdr[0x1B];
if (magic != FDT_MAGIC) {
fprintf(stderr, "Not a valid device tree (bad FDT magic): %s\n", file);
fclose(f);
return -1;
}
if (total < FDT_HDR_SIZE || version < FDT_FIRST_VER ||
last_comp > FDT_LAST_COMP_VER) {
fprintf(stderr, "Unsupported device tree (version %u, comp %u, "
"totalsize %u): %s -- the bootloader would reject it\n",
version, last_comp, total, file);
fclose(f);
return -1;
}
/* A file shorter than fdt_totalsize can't be hashed as declared (reject);
* a longer one (trailing padding) is hashed over the declared span (warn). */
fseek(f, 0, SEEK_END);
fsz = ftell(f);
if (fsz >= 0 && (uint32_t)fsz < total) {
fprintf(stderr, "Device tree file %s is %ld bytes but its FDT totalsize "
"is %u (truncated)\n", file, fsz, total);
fclose(f);
return -1;
}
if (fsz >= 0 && (uint32_t)fsz != total) {
fprintf(stderr, "Warning: device tree file %s is %ld bytes but its FDT "
"totalsize is %u; hashing the first %u bytes\n",
file, fsz, total, total);
}
if (fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
return -1;
}
remain = total;
if (hash_algo == HASH_SHA256) {
#ifndef NO_SHA256
wc_Sha256 sha;
ret = wc_InitSha256_ex(&sha, NULL, INVALID_DEVID);
if (ret == 0) {
while (remain > 0) {
want = (remain < sizeof(rbuf)) ? remain : sizeof(rbuf);
rd = fread(rbuf, 1, want, f);
if (rd == 0) { ret = -1; break; }
ret = wc_Sha256Update(&sha, rbuf, (word32)rd);
if (ret != 0) break;
remain -= (uint32_t)rd;
}
if (ret == 0)
ret = wc_Sha256Final(&sha, out);
wc_Sha256Free(&sha);
}
*out_sz = HDR_SHA256_LEN;
#endif
}
else if (hash_algo == HASH_SHA384) {
#ifndef NO_SHA384
wc_Sha384 sha;
ret = wc_InitSha384_ex(&sha, NULL, INVALID_DEVID);
if (ret == 0) {
while (remain > 0) {
want = (remain < sizeof(rbuf)) ? remain : sizeof(rbuf);
rd = fread(rbuf, 1, want, f);
if (rd == 0) { ret = -1; break; }
ret = wc_Sha384Update(&sha, rbuf, (word32)rd);
if (ret != 0) break;
remain -= (uint32_t)rd;
}
if (ret == 0)
ret = wc_Sha384Final(&sha, out);
wc_Sha384Free(&sha);
}
*out_sz = HDR_SHA384_LEN;
#endif
}
else if (hash_algo == HASH_SHA3) {
#ifdef WOLFSSL_SHA3
wc_Sha3 sha;
ret = wc_InitSha3_384(&sha, NULL, INVALID_DEVID);
if (ret == 0) {
while (remain > 0) {
want = (remain < sizeof(rbuf)) ? remain : sizeof(rbuf);
rd = fread(rbuf, 1, want, f);
if (rd == 0) { ret = -1; break; }
ret = wc_Sha3_384_Update(&sha, rbuf, (word32)rd);
if (ret != 0) break;
remain -= (uint32_t)rd;
}
if (ret == 0)
ret = wc_Sha3_384_Final(&sha, out);
wc_Sha3_384_Free(&sha);
}
*out_sz = HDR_SHA3_384_LEN;
#endif
}
fclose(f);
return ret;
}
static uint32_t header_required_size(int is_diff, uint32_t cert_chain_sz,
uint32_t secondary_key_sz)
{
@ -1369,6 +1513,11 @@ static uint32_t header_required_size(int is_diff, uint32_t cert_chain_sz,
header_size_append_tag(&idx, CMD.custom_tlv[i].len);
}
if (CMD.dts_file != NULL && digest_sz > 0U) {
header_size_align_8(&idx);
header_size_append_tag(&idx, digest_sz);
}
if (cert_chain_sz > 0U) {
header_size_align_8(&idx);
header_size_append_tag(&idx, cert_chain_sz);
@ -1446,7 +1595,8 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz,
/* Check certificate chain file size before allocating header, and adjust
* header size if needed */
if ((CMD.cert_chain_file != NULL) || (CMD.custom_tlvs > 0)) {
if ((CMD.cert_chain_file != NULL) || (CMD.custom_tlvs > 0) ||
(CMD.dts_file != NULL)) {
uint32_t hdr_cert_chain_sz = 0;
uint32_t required_space;
@ -1517,7 +1667,7 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz,
image_sz = ftell(f);
fseek(f, 0, SEEK_SET);
fclose(f);
f = NULL;
f = NULL; /* avoid a double fclose() if a later step jumps to 'failure' */
/* Append Magic header (spells 'WOLF') */
header_append_u32(header, &header_idx, WOLFBOOT_MAGIC);
@ -1626,6 +1776,21 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz,
}
}
/* Signature-covered digest of a raw (non-FIT) device tree. */
if (CMD.dts_file != NULL) {
uint8_t dts_digest[48]; /* max digest */
uint32_t dts_digest_sz = 0;
if (dts_hash_file(CMD.dts_file, CMD.hash_algo, dts_digest,
&dts_digest_sz) != 0 || dts_digest_sz == 0) {
printf("Error hashing device tree file %s\n", CMD.dts_file);
goto failure;
}
ALIGN_8(header_idx);
header_append_tag(header, &header_idx, HDR_DEVICE_TREE_DIGEST,
(uint16_t)dts_digest_sz, dts_digest);
printf("Device tree digest (%u bytes) bound to image\n", dts_digest_sz);
}
/* Read certificate chain if provided */
if (CMD.cert_chain_file != NULL) {
const size_t cert_chain_tlv_hdr_sz = 4;
@ -3447,6 +3612,14 @@ int main(int argc, char** argv)
CMD.custom_tlv[p].buffer = NULL;
CMD.custom_tlvs++;
i += 3;
} else if (strcmp(argv[i], "--dts") == 0) {
if (argc < (i + 2)) {
fprintf(stderr, "Missing device tree file for --dts.\n");
exit(16);
}
/* Hashed in make_header_ex() once the hash algo is known. */
CMD.dts_file = argv[i + 1];
i += 1;
} else if (strcmp(argv[i], "--custom-tlv-buffer") == 0) {
int p = CMD.custom_tlvs;
uint16_t tag, len;
@ -3705,6 +3878,13 @@ int main(int argc, char** argv)
CMD.fw_version = argv[i+2];
}
/* --dts can grow the header after patch_inv_off is computed, so the delta
* inverse offset would be stale. Reject the combination. */
if (CMD.dts_file != NULL && CMD.delta) {
fprintf(stderr, "Error: --dts cannot be combined with --delta\n");
exit(16);
}
memset(buf, 0, sizeof(buf));
strncpy((char*)buf, CMD.image_file, sizeof(buf)-1);
tmpstr = strrchr((char*)buf, '.');

View File

@ -113,11 +113,15 @@ else
exit 1
fi
echo "Mode : RAW DTB (zImage signed alone, DTB staged separately at PART_DTS_BOOT)"
echo "Mode : RAW DTB (zImage signed alone, DTB staged separately in flash"
echo " at WOLFBOOT_DTS_BOOT_ADDRESS)"
echo "zImage: $ZIMAGE ($KSIZE bytes)"
echo "DTB : $DTB ($DSIZE bytes)"
echo "Signing kernel as PART_BOOT v$VERSION ..."
$SIGN_TOOL --ecc256 --sha256 "$ZIMAGE" "$KEY" "$VERSION"
# --dts binds the raw DTB's digest to the signed kernel (HDR_DEVICE_TREE_DIGEST),
# so wolfBoot authenticates the device tree before booting. Build wolfBoot with
# WOLFBOOT_REQUIRE_SIGNED_DTB=1 to make a missing digest fatal.
$SIGN_TOOL --ecc256 --sha256 --dts "$DTB" "$ZIMAGE" "$KEY" "$VERSION"
SIGNED_OUT="${ZIMAGE%.*}_v${VERSION}_signed.bin"
[ -f "$SIGNED_OUT" ] || SIGNED_OUT="${ZIMAGE}_v${VERSION}_signed.bin"

View File

@ -63,7 +63,8 @@ TESTS:=unit-parser unit-fdt unit-extflash unit-string unit-spi-flash unit-aes128
unit-update-flash-self-update \
unit-update-flash-enc unit-update-ram unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-noramboot unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \
unit-update-disk unit-update-disk-oob unit-update-disk-fit unit-multiboot unit-boot-x86-fsp unit-loader-tpm-init unit-qspi-flash unit-fwtpm-stub unit-tpm-rsa-exp \
unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-store-sbrk \
unit-image-nopart unit-image-sha384 unit-image-sha3-384 unit-image-dts \
unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \
unit-tpm-blob unit-policy-create unit-policy-sign unit-rot-auth unit-sdhci-response-bits \
unit-sdhci-disk-unaligned unit-sign-encrypted-output \
unit-sign-hybrid-keyload \
@ -147,6 +148,7 @@ run: $(TESTS)
python3 unit-sign-custom-tlv-le.py || exit 1
python3 unit-sign-custom-tlv-large.py || exit 1
python3 unit-sign-custom-tlv-pubkey-der.py || exit 1
python3 unit-sign-dts.py || exit 1
WOLFCRYPT_SRC:=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha.c \
@ -579,6 +581,26 @@ unit-image-sha3-384: ../../include/target.h unit-image.c unit-common.c
$(CFLAGS) $(WOLFCRYPT_CFLAGS) -DUNIT_IMAGE_KEYHASH_ONLY \
-DWOLFBOOT_HASH_SHA3_384 $(LDFLAGS)
# Exercises the raw-DTB authentication helper wolfBoot_verify_dts_digest()
# (Fenrir #7998). WOLFBOOT_FDT compiles the DTS helpers in image.c, which pull
# in fdt.c for wolfBoot_get_dts_size(). The sha384/sha3-384 variants cover the
# corresponding wolfBoot_hash_buffer() branches used by real MMU targets.
unit-image-dts: ../../include/target.h unit-image.c unit-common.c $(WOLFCRYPT_SRC)
gcc -o $@ unit-image.c unit-common.c $(WOLFCRYPT_SRC) ../../src/fdt.c \
$(CFLAGS) $(WOLFCRYPT_CFLAGS) -DWOLFBOOT_FDT $(LDFLAGS)
unit-image-dts-sha384: ../../include/target.h unit-image.c unit-common.c $(WOLFCRYPT_SRC)
gcc -o $@ unit-image.c unit-common.c $(WOLFCRYPT_SRC) ../../src/fdt.c \
$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha512.c \
$(CFLAGS) $(WOLFCRYPT_CFLAGS) -DWOLFBOOT_FDT -DUNIT_IMAGE_DTS_ONLY \
-DWOLFBOOT_HASH_SHA384 $(LDFLAGS)
unit-image-dts-sha3-384: ../../include/target.h unit-image.c unit-common.c $(WOLFCRYPT_SRC)
gcc -o $@ unit-image.c unit-common.c $(WOLFCRYPT_SRC) ../../src/fdt.c \
$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha3.c \
$(CFLAGS) $(WOLFCRYPT_CFLAGS) -DWOLFBOOT_FDT -DUNIT_IMAGE_DTS_ONLY \
-DWOLFBOOT_HASH_SHA3_384 $(LDFLAGS)
unit-image-rsa: CFLAGS += -DWOLFBOOT_SIGN_RSA2048
unit-image-rsa: ../../include/target.h unit-image.c unit-common.c
gcc -o $@ unit-image.c unit-common.c $(WOLFCRYPT_SRC) \

View File

@ -1105,6 +1105,74 @@ START_TEST(test_open_image_address_without_partitions_rejects_oversized_fw_size)
END_TEST
#endif
#if defined(WOLFBOOT_FDT) || defined(MMU)
/* Exercises wolfBoot_verify_dts_digest(), the raw-DTB authentication used by
* the non-FIT MMU boot path in src/update_ram.c (Fenrir #7998). The reference
* digest is computed with whichever WOLFBOOT_HASH the build selected, so the
* SHA256, SHA384 and SHA3-384 variants of wolfBoot_hash_buffer() are all
* covered (see the unit-image-dts* Makefile targets). */
START_TEST(test_verify_dts_digest)
{
uint8_t dtb[256];
uint8_t tampered[256];
uint8_t good[WOLFBOOT_SHA_DIGEST_SIZE];
unsigned int i;
for (i = 0; i < sizeof(dtb); i++)
dtb[i] = (uint8_t)(i * 7U + 1U);
/* Reference digest computed the same way the signer does (image hash over
* the DTB bytes), using the configured hash algorithm. */
#if defined(WOLFBOOT_HASH_SHA256)
{
wc_Sha256 sha;
ck_assert_int_eq(wc_InitSha256_ex(&sha, NULL, INVALID_DEVID), 0);
ck_assert_int_eq(wc_Sha256Update(&sha, dtb, sizeof(dtb)), 0);
ck_assert_int_eq(wc_Sha256Final(&sha, good), 0);
wc_Sha256Free(&sha);
}
#elif defined(WOLFBOOT_HASH_SHA384)
{
wc_Sha384 sha;
ck_assert_int_eq(wc_InitSha384_ex(&sha, NULL, INVALID_DEVID), 0);
ck_assert_int_eq(wc_Sha384Update(&sha, dtb, sizeof(dtb)), 0);
ck_assert_int_eq(wc_Sha384Final(&sha, good), 0);
wc_Sha384Free(&sha);
}
#elif defined(WOLFBOOT_HASH_SHA3_384)
{
wc_Sha3 sha;
ck_assert_int_eq(wc_InitSha3_384(&sha, NULL, INVALID_DEVID), 0);
ck_assert_int_eq(wc_Sha3_384_Update(&sha, dtb, sizeof(dtb)), 0);
ck_assert_int_eq(wc_Sha3_384_Final(&sha, good), 0);
wc_Sha3_384_Free(&sha);
}
#else
#error "test_verify_dts_digest: no supported WOLFBOOT_HASH selected"
#endif
/* Matching digest -> accept. */
ck_assert_int_eq(wolfBoot_verify_dts_digest(good, dtb, sizeof(dtb)), 0);
/* Tampered DTB (single flipped byte) -> reject. */
memcpy(tampered, dtb, sizeof(dtb));
tampered[100] ^= 0xFFU;
ck_assert_int_eq(
wolfBoot_verify_dts_digest(good, tampered, sizeof(tampered)), -1);
/* Wrong expected digest -> reject. */
good[0] ^= 0xFFU;
ck_assert_int_eq(wolfBoot_verify_dts_digest(good, dtb, sizeof(dtb)), -1);
good[0] ^= 0xFFU;
/* Bad arguments -> reject. */
ck_assert_int_eq(wolfBoot_verify_dts_digest(NULL, dtb, sizeof(dtb)), -1);
ck_assert_int_eq(wolfBoot_verify_dts_digest(good, NULL, sizeof(dtb)), -1);
ck_assert_int_eq(wolfBoot_verify_dts_digest(good, dtb, 0), -1);
}
END_TEST
#endif /* WOLFBOOT_FDT || MMU */
Suite *wolfboot_suite(void)
{
@ -1119,6 +1187,16 @@ Suite *wolfboot_suite(void)
return s;
#endif
#if defined(UNIT_IMAGE_DTS_ONLY) && (defined(WOLFBOOT_FDT) || defined(MMU))
/* Only the raw-DTB digest test. Used by the sha384/sha3-384 variants,
* whose non-SHA256 hash config would break the other unit-image tests. */
TCase* tcase_dts_only = tcase_create("dts_digest");
tcase_set_timeout(tcase_dts_only, 20);
tcase_add_test(tcase_dts_only, test_verify_dts_digest);
suite_add_tcase(s, tcase_dts_only);
return s;
#endif
#ifdef UNIT_IMAGE_HYBRID_ONLY
TCase* tcase_hybrid_auth = tcase_create("hybrid_auth");
tcase_set_timeout(tcase_hybrid_auth, 20);
@ -1196,6 +1274,13 @@ Suite *wolfboot_suite(void)
#endif
suite_add_tcase(s, tcase_open_image);
#endif
#if defined(WOLFBOOT_FDT) || defined(MMU)
TCase* tcase_dts_digest = tcase_create("dts_digest");
tcase_set_timeout(tcase_dts_digest, 20);
tcase_add_test(tcase_dts_digest, test_verify_dts_digest);
suite_add_tcase(s, tcase_dts_digest);
#endif
return s;
}

View File

@ -0,0 +1,238 @@
#!/usr/bin/env python3
# unit-sign-dts.py
#
# Regression test for the C sign tool "--dts" device-tree binding
# (make_header_ex()/dts_hash_file() in tools/keytools/sign.c, Fenrir #7998).
#
# Covers:
# 1. A valid DTB is bound to the image: the HDR_DEVICE_TREE_DIGEST TLV
# (tag 0x35) holds the image-hash (SHA256) of exactly the first
# fdt_totalsize bytes of the .dtb.
# 2. A bad-magic file is rejected with a clean non-zero exit -- NOT a signal
# (the dangling-FILE double free previously aborted with SIGABRT).
# 3. A truncated FDT (valid magic, shorter than the 40-byte header) is
# rejected cleanly, matching what the bootloader's fdt_check_header()
# would reject.
#
# Copyright (C) 2026 wolfSSL Inc.
#
# This file is part of wolfBoot.
#
# wolfBoot is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# wolfBoot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
import hashlib
import os
import struct
import subprocess
import sys
import tempfile
HDR_PADDING = 0xFF
HDR_DEVICE_TREE_DIGEST = 0x35
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(THIS_DIR, "..", ".."))
SIGN = os.path.join(ROOT, "tools", "keytools", "sign")
def skip(msg):
print("SKIP unit-sign-dts: " + msg)
sys.exit(0)
def fail(msg):
print("FAIL unit-sign-dts: " + msg)
sys.exit(1)
def find_tlv_bytes(data, want_type, scan_end):
"""Return raw value bytes for a TLV entry (mirrors wolfBoot_find_header)."""
p = 8 # skip 4-byte magic + 4-byte image size
while p + 4 <= scan_end:
htype = data[p] | (data[p + 1] << 8)
if htype == 0:
break
if data[p] == HDR_PADDING or (p & 1) != 0:
p += 1
continue
length = data[p + 2] | (data[p + 3] << 8)
if htype == want_type:
if p + 4 + length > scan_end:
return None
return bytes(data[p + 4:p + 4 + length])
p += 4 + length
return None
def ensure_sign():
if os.path.exists(SIGN):
return True
try:
subprocess.run(["make", "sign"],
cwd=os.path.join(ROOT, "tools", "keytools"),
check=True, capture_output=True, text=True)
except (subprocess.CalledProcessError, OSError):
return False
return os.path.exists(SIGN)
def make_ed25519_key(path):
"""Write a 64-byte raw ed25519 key (seed + public) as expected by sign."""
try:
from cryptography.hazmat.primitives.asymmetric.ed25519 import \
Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization
except Exception:
return False
seed = b"\x42" * 32
sk = Ed25519PrivateKey.from_private_bytes(seed)
pub = sk.public_key().public_bytes(serialization.Encoding.Raw,
serialization.PublicFormat.Raw)
with open(path, "wb") as f:
f.write(seed + pub)
return True
def make_fdt(total):
"""Build a minimal valid FDT blob of 'total' bytes (v17 header)."""
hdr = bytearray(40)
struct.pack_into(">I", hdr, 0x00, 0xd00dfeed) # magic
struct.pack_into(">I", hdr, 0x04, total) # totalsize
struct.pack_into(">I", hdr, 0x08, 0x38) # off_dt_struct
struct.pack_into(">I", hdr, 0x0C, 0x38) # off_dt_strings
struct.pack_into(">I", hdr, 0x10, 0x28) # off_mem_rsvmap
struct.pack_into(">I", hdr, 0x14, 0x11) # version 17
struct.pack_into(">I", hdr, 0x18, 0x10) # last_comp_version 16
blob = bytes(hdr) + b"\x00" * (total - len(hdr))
return blob
# (hash flag, hashlib factory, digest length) for each supported algorithm.
HASHES = [
("--sha256", hashlib.sha256, 32),
("--sha384", hashlib.sha384, 48),
("--sha3", (lambda b: hashlib.sha3_384(b)), 48),
]
def run_dts(work, key, dtb_path, hash_flag="--sha256", image=None):
"""Run 'sign --ed25519 <hash_flag> --dts <dtb> <image> <key> 1'."""
if image is None:
image = os.path.join(work, "fw.bin")
with open(image, "wb") as f:
f.write(b"\x11" * 2048)
cmd = [SIGN, "--ed25519", hash_flag, "--dts", dtb_path, image, key, "1"]
return subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True), image
def main():
if not ensure_sign():
skip("sign tool not available")
with tempfile.TemporaryDirectory() as work:
key = os.path.join(work, "ed25519.der")
if not make_ed25519_key(key):
skip("python 'cryptography' module unavailable")
# 1. Valid DTB -> digest TLV present and correct, for every hash
# algorithm (covers dts_hash_file's sha256/sha384/sha3-384 branches).
# A sign failure here is a real regression, so fail() (not skip()).
total = 128
dtb = os.path.join(work, "good.dtb")
blob = make_fdt(total)
with open(dtb, "wb") as f:
f.write(blob + b"\xAA" * 16) # trailing padding beyond totalsize
for hflag, hfn, hlen in HASHES:
r, image = run_dts(work, key, dtb, hflag)
if "unsupported" in (r.stderr or "").lower() and hflag == "--sha3":
# SHA3 not compiled into this sign build; skip only this case.
continue
if r.returncode != 0:
fail("sign %s --dts failed (rc=%d): %s"
% (hflag, r.returncode, r.stderr.strip()))
signed = image.replace(".bin", "_v1_signed.bin")
if not os.path.exists(signed):
fail("sign %s --dts produced no signed image" % hflag)
with open(signed, "rb") as f:
data = f.read(2048)
got = find_tlv_bytes(data, HDR_DEVICE_TREE_DIGEST, len(data))
expect = hfn(blob).digest() # first totalsize bytes only
if got is None:
fail("%s: HDR_DEVICE_TREE_DIGEST (0x35) TLV not found" % hflag)
if len(got) != hlen:
fail("%s: digest TLV len %d, expected %d"
% (hflag, len(got), hlen))
if got != expect:
fail("%s: digest TLV mismatch: got %s expected %s"
% (hflag, got.hex(), expect.hex()))
# 2. Bad magic -> clean non-zero exit, never a signal (SIGABRT was the
# double-free symptom). subprocess returncode < 0 means killed by
# signal -N.
bad = os.path.join(work, "bad.dtb")
with open(bad, "wb") as f:
f.write(b"not-an-fdt-file-but-long-enough-to-fill-40-bytes!!!")
r, _ = run_dts(work, key, bad)
if r.returncode < 0:
fail("bad-magic --dts died with signal %d (double free?)"
% (-r.returncode))
if r.returncode == 0:
fail("bad-magic --dts unexpectedly succeeded")
# 3. Truncated FDT (valid magic, < 40-byte header) -> clean rejection.
trunc = os.path.join(work, "trunc.dtb")
with open(trunc, "wb") as f:
f.write(struct.pack(">II", 0xd00dfeed, 0x10)) # 8 bytes only
r, _ = run_dts(work, key, trunc)
if r.returncode < 0:
fail("truncated --dts died with signal %d" % (-r.returncode))
if r.returncode == 0:
fail("truncated --dts unexpectedly succeeded")
# 4. Unsupported FDT version (full 40B header, valid magic, version 15)
# -> rejected (the version/last_comp check the bootloader also does).
badver = os.path.join(work, "badver.dtb")
vblob = bytearray(make_fdt(128))
struct.pack_into(">I", vblob, 0x14, 0x0F) # version 15 < 0x10
with open(badver, "wb") as f:
f.write(bytes(vblob))
r, _ = run_dts(work, key, badver)
if r.returncode <= 0:
fail("unsupported-version --dts should be rejected (rc=%d)"
% r.returncode)
# 5. Truncated body: totalsize declares more than the file holds ->
# rejected up front, not hashed.
short = os.path.join(work, "short.dtb")
with open(short, "wb") as f:
f.write(make_fdt(4096)[:512]) # header says 4096, file is 512
r, _ = run_dts(work, key, short)
if r.returncode <= 0:
fail("short-body --dts should be rejected (rc=%d)" % r.returncode)
# 6. Missing --dts argument -> clean usage error, never a crash.
r = subprocess.run([SIGN, "--ed25519", "--sha256", "--dts"],
cwd=ROOT, capture_output=True, text=True)
if r.returncode < 0:
fail("--dts with no argument died with signal %d" % (-r.returncode))
if r.returncode == 0:
fail("--dts with no argument unexpectedly succeeded")
print("PASS unit-sign-dts")
sys.exit(0)
if __name__ == "__main__":
main()