diff --git a/.gitignore b/.gitignore index b6817d2b06..dc682de501 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/config/examples/zynq7000.config b/config/examples/zynq7000.config index 16e9e01d0b..72d2622939 100644 --- a/config/examples/zynq7000.config +++ b/config/examples/zynq7000.config @@ -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 diff --git a/docs/Signing.md b/docs/Signing.md index 19030e4fcd..1ea97f14e1 100644 --- a/docs/Signing.md +++ b/docs/Signing.md @@ -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 diff --git a/docs/Targets.md b/docs/Targets.md index 71dea72ce2..e8e8eaa81b 100644 --- a/docs/Targets.md +++ b/docs/Targets.md @@ -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 ` (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-` diff --git a/docs/compile.md b/docs/compile.md index d7be860560..dac1d4c4e7 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -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 `, 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 diff --git a/hal/zynq.c b/hal/zynq.c index 7717e866d9..95203c7c50 100644 --- a/hal/zynq.c +++ b/hal/zynq.c @@ -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; diff --git a/hal/zynq7000.c b/hal/zynq7000.c index d7370c3897..53e018e6d6 100644 --- a/hal/zynq7000.c +++ b/hal/zynq7000.c @@ -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; diff --git a/include/image.h b/include/image.h index 040a235e00..bf59138a21 100644 --- a/include/image.h +++ b/include/image.h @@ -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); diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index 19c45666ec..19fb54e7e5 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -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 */ diff --git a/options.mk b/options.mk index 0f340fa856..c97fd26325 100644 --- a/options.mk +++ b/options.mk @@ -918,6 +918,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) diff --git a/src/image.c b/src/image.c index 1af07da248..50458ab9ea 100644 --- a/src/image.c +++ b/src/image.c @@ -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 diff --git a/src/update_ram.c b/src/update_ram.c index e83dad81f8..0dac446536 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -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 @@ void RAMFUNCTION wolfBoot_start(void) #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,37 +673,75 @@ void RAMFUNCTION wolfBoot_start(void) #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 } } } diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 7f8022aaf2..9e808c65c4 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -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, '.'); diff --git a/tools/scripts/zynq7000/prepare_linux.sh b/tools/scripts/zynq7000/prepare_linux.sh index b5adbfa86d..9eaf5e8663 100755 --- a/tools/scripts/zynq7000/prepare_linux.sh +++ b/tools/scripts/zynq7000/prepare_linux.sh @@ -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" diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 1e629f3d88..adef137b7a 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -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) \ diff --git a/tools/unit-tests/unit-image.c b/tools/unit-tests/unit-image.c index be99438880..1ae990701f 100644 --- a/tools/unit-tests/unit-image.c +++ b/tools/unit-tests/unit-image.c @@ -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; } diff --git a/tools/unit-tests/unit-sign-dts.py b/tools/unit-tests/unit-sign-dts.py new file mode 100644 index 0000000000..84721344bc --- /dev/null +++ b/tools/unit-tests/unit-sign-dts.py @@ -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 --dts 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()