From eca9a20b3ba690a9627eded545eccfe7f5540289 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 2 Jul 2026 20:09:16 +0200 Subject: [PATCH 1/3] armored: harden fw_base against fault injection --- include/image.h | 54 ++++++++++++++++++++++++++++++++++++++++++++++ src/image.c | 8 +++---- src/update_flash.c | 5 +++-- 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/include/image.h b/include/image.h index 2d9aefa16e..77eaee6af6 100644 --- a/include/image.h +++ b/include/image.h @@ -173,6 +173,7 @@ struct wolfBoot_image { uint32_t sha_ok; uint32_t canary_FEEDCAFE; uint32_t not_sha_ok; + uintptr_t not_fw_base; /* complement of fw_base, for FI hardening */ uint32_t not_ext; /* image is no longer external */ }; @@ -229,6 +230,18 @@ static void NOINLINEFUNCTION wolfBoot_image_clear_sha_ok( img->not_sha_ok = 1UL; } +/** + * Records the image entry base together with its complement, so that a single + * fault on the pointer that do_boot() jumps through can be detected by + * FW_BASE_SANITY_CHECK() before the branch is taken. + */ +static void NOINLINEFUNCTION wolfBoot_image_set_fw_base( + struct wolfBoot_image *img, void *base) +{ + img->fw_base = (uint8_t *)base; + img->not_fw_base = ~(uintptr_t)base; +} + /** * Final sanity check, performed just before do_boot, or before starting an * update that has been verified. @@ -757,6 +770,40 @@ static void NOINLINEFUNCTION wolfBoot_image_clear_sha_ok( asm volatile("cmp r2, #0xFFFFFFFE":::"cc"); \ asm volatile("bne .-12") +/** + * Hardened assertion that the image entry base is consistent with its stored + * complement (fw_base == ~not_fw_base), performed immediately before do_boot() + * dereferences fw_base. A single fault on either the pointer or the loads + * fails the check safely (spins) instead of redirecting the boot jump. + */ +#define FW_BASE_SANITY_CHECK(p) \ + /* Redundant set of r2=0 */ \ + asm volatile("mov r2, #0":::"r2"); \ + asm volatile("mov r2, #0":::"r2"); \ + asm volatile("mov r2, #0":::"r2"); \ + asm volatile("mov r2, #0":::"r2"); \ + asm volatile("mov r2, #0":::"r2"); \ + /* r2 = fw_base, r0 = ~not_fw_base (== expected fw_base) */ \ + asm volatile("mov r2, %0" ::"r"((uintptr_t)(p)->fw_base):"r2"); \ + asm volatile("mov r0, %0" ::"r"((p)->not_fw_base):"r0"); \ + asm volatile("mvn r0, r0":::"r0"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("bne ."); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("bne .-4"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("bne .-8"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("cmp r2, r0":::"cc"); \ + asm volatile("bne .-12") + /** * ECC / Ed / PQ signature verification. * Those verify functions set an additional value 'p_res' @@ -1521,6 +1568,11 @@ static void UNUSEDFUNCTION wolfBoot_image_clear_sha_ok( { img->sha_ok = 0; } +static void UNUSEDFUNCTION wolfBoot_image_set_fw_base( + struct wolfBoot_image *img, void *base) +{ + img->fw_base = (uint8_t *)base; +} #define likely(x) (x) #define unlikely(x) (x) @@ -1560,6 +1612,8 @@ static void UNUSEDFUNCTION wolfBoot_image_clear_sha_ok( if ((p)->sha_ok != 1) \ wolfBoot_panic() +#define FW_BASE_SANITY_CHECK(p) do{} while(0) + #define CONFIRM_MASK_VALID(id, mask) \ if ((mask & (1UL << id)) != (1UL << id)) \ wolfBoot_panic() diff --git a/src/image.c b/src/image.c index 97841b660a..3de4022cff 100644 --- a/src/image.c +++ b/src/image.c @@ -1424,7 +1424,7 @@ int wolfBoot_open_image_address(struct wolfBoot_image *img, uint8_t *image) } #endif img->hdr_ok = 1; - img->fw_base = img->hdr + IMAGE_HEADER_SIZE; + wolfBoot_image_set_fw_base(img, img->hdr + IMAGE_HEADER_SIZE); #ifdef EXT_FLASH img->hdr_cache = image; #endif @@ -1488,7 +1488,7 @@ int wolfBoot_open_image(struct wolfBoot_image *img, uint8_t part) if (part == PART_SWAP) { img->hdr = (void*)WOLFBOOT_PARTITION_SWAP_ADDRESS; img->hdr_ok = 1; - img->fw_base = img->hdr; + wolfBoot_image_set_fw_base(img, img->hdr); img->fw_size = WOLFBOOT_SECTOR_SIZE; return 0; } @@ -1507,7 +1507,7 @@ int wolfBoot_open_image(struct wolfBoot_image *img, uint8_t part) if (ret < 0) return -1; img->hdr_ok = 1; - img->fw_base = img->hdr; + wolfBoot_image_set_fw_base(img, img->hdr); img->fw_size = (uint32_t)ret; return 0; } @@ -1620,7 +1620,7 @@ int wolfBoot_open_self_address(struct wolfBoot_image* img, uint8_t* hdr, return -1; } #endif - img->fw_base = image; + wolfBoot_image_set_fw_base(img, image); img->part = PART_SELF; img->hdr_ok = 1; diff --git a/src/update_flash.c b/src/update_flash.c index 9e433c260b..b81d525105 100644 --- a/src/update_flash.c +++ b/src/update_flash.c @@ -1270,7 +1270,7 @@ static int RAMFUNCTION wolfBoot_update(int fallback_allowed) wolfBoot_printf( "Scattered image correctly verified. Setting entry point to %lx\n", entry); - boot.fw_base = (void*)entry; + wolfBoot_image_set_fw_base(&boot, (void*)entry); #endif /* Direct Swap without power fail safety */ @@ -1618,7 +1618,7 @@ void RAMFUNCTION wolfBoot_start(void) wolfBoot_printf( "Scattered image correctly verified. Setting entry point to %lx\n", entry); - boot.fw_base = (void*)entry; + wolfBoot_image_set_fw_base(&boot, (void*)entry); #endif @@ -1650,6 +1650,7 @@ void RAMFUNCTION wolfBoot_start(void) #endif #ifndef WOLFBOOT_SKIP_BOOT_VERIFY PART_SANITY_CHECK(&boot); + FW_BASE_SANITY_CHECK(&boot); #endif do_boot((void *)boot.fw_base); } From 6708438024cde60c09ec21ce9a37cc980195b2b8 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 2 Jul 2026 20:11:46 +0200 Subject: [PATCH 2/3] armored: use hardened digest compare in elf and delta paths --- include/image.h | 2 ++ src/image.c | 22 +++++++++++++++++++++- src/update_flash.c | 3 ++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/include/image.h b/include/image.h index 77eaee6af6..50aa405217 100644 --- a/include/image.h +++ b/include/image.h @@ -1625,6 +1625,8 @@ static void UNUSEDFUNCTION wolfBoot_image_set_fw_base( /* Defined in image.c */ 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); 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/src/image.c b/src/image.c index 3de4022cff..8012387a68 100644 --- a/src/image.c +++ b/src/image.c @@ -71,6 +71,25 @@ int NOINLINEFUNCTION image_CT_compare( return (diff != 0U) ? 1 : 0; } +/** + * Fault-hardened equality check around image_CT_compare(): the constant-time + * comparison is run twice and a match is reported only when both independent + * calls agree. A single instruction-skip fault can subvert at most one of the + * two calls (or one of the two result checks), so a genuine mismatch is still + * detected. Returns 0 when equal, non-zero otherwise. + */ +int NOINLINEFUNCTION wolfBoot_hardened_CT_compare( + const uint8_t *expected, const uint8_t *actual, uint32_t len) +{ + volatile int r1 = image_CT_compare(expected, actual, len); + volatile int r2 = image_CT_compare(expected, actual, len); + if (r1 != 0) + return -1; + if (r2 != 0) + return -1; + return 0; +} + #if defined(WOLFBOOT_CERT_CHAIN_VERIFY) && \ (defined(WOLFBOOT_ENABLE_WOLFHSM_CLIENT) || \ defined(WOLFBOOT_ENABLE_WOLFHSM_SERVER)) @@ -2111,7 +2130,8 @@ int wolfBoot_check_flash_image_elf(uint8_t part, unsigned long* entry_out) /* Finalize SHA calculation */ final_hash(&ctx, calc_digest); - if (image_CT_compare(exp_digest, calc_digest, WOLFBOOT_SHA_DIGEST_SIZE) != 0) { + if (wolfBoot_hardened_CT_compare(exp_digest, calc_digest, + WOLFBOOT_SHA_DIGEST_SIZE) != 0) { wolfBoot_printf("ELF: [CHECK] SHA verification FAILED\n"); wolfBoot_printf( "ELF: [CHECK] Expected %02x%02x%02x%02x%02x%02x%02x%02x\n", diff --git a/src/update_flash.c b/src/update_flash.c index b81d525105..28d3c8d6cc 100644 --- a/src/update_flash.c +++ b/src/update_flash.c @@ -709,7 +709,8 @@ static int wolfBoot_delta_update(struct wolfBoot_image *boot, cur_v, delta_base_v); ret = -1; } else if (!resume && delta_base_hash && - image_CT_compare(base_hash, delta_base_hash, base_hash_sz) != 0) { + wolfBoot_hardened_CT_compare(base_hash, delta_base_hash, + base_hash_sz) != 0) { /* Wrong base image digest, cannot apply delta patch */ wolfBoot_printf("Delta Base hash mismatch\n"); ret = -1; From 195610647c236cccf29a5d05d2488cc957c2813c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Thu, 2 Jul 2026 20:33:07 +0200 Subject: [PATCH 3/3] armored: address review on hardened compare and fw_base check --- include/image.h | 55 ++++++++++++++++++++++++++----------------------- src/image.c | 10 ++++----- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/include/image.h b/include/image.h index 50aa405217..5357ffa8e3 100644 --- a/include/image.h +++ b/include/image.h @@ -777,32 +777,35 @@ static void NOINLINEFUNCTION wolfBoot_image_set_fw_base( * fails the check safely (spins) instead of redirecting the boot jump. */ #define FW_BASE_SANITY_CHECK(p) \ - /* Redundant set of r2=0 */ \ - asm volatile("mov r2, #0":::"r2"); \ - asm volatile("mov r2, #0":::"r2"); \ - asm volatile("mov r2, #0":::"r2"); \ - asm volatile("mov r2, #0":::"r2"); \ - asm volatile("mov r2, #0":::"r2"); \ - /* r2 = fw_base, r0 = ~not_fw_base (== expected fw_base) */ \ - asm volatile("mov r2, %0" ::"r"((uintptr_t)(p)->fw_base):"r2"); \ - asm volatile("mov r0, %0" ::"r"((p)->not_fw_base):"r0"); \ - asm volatile("mvn r0, r0":::"r0"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("bne ."); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("bne .-4"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("bne .-8"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("cmp r2, r0":::"cc"); \ - asm volatile("bne .-12") + do { \ + /* Single asm block so the compiler cannot allocate over r0/r2 between \ + * steps: r2 = fw_base, r0 = ~not_fw_base (== expected fw_base), then \ + * a redundant self-trapping comparison that spins on any mismatch. */ \ + asm volatile( \ + "mov r2, %[fwb]\n\t" \ + "mov r0, %[nfwb]\n\t" \ + "mvn r0, r0\n\t" \ + "cmp r2, r0\n\t" \ + "cmp r2, r0\n\t" \ + "cmp r2, r0\n\t" \ + "bne .\n\t" \ + "cmp r2, r0\n\t" \ + "cmp r2, r0\n\t" \ + "cmp r2, r0\n\t" \ + "bne .-4\n\t" \ + "cmp r2, r0\n\t" \ + "cmp r2, r0\n\t" \ + "cmp r2, r0\n\t" \ + "bne .-8\n\t" \ + "cmp r2, r0\n\t" \ + "cmp r2, r0\n\t" \ + "cmp r2, r0\n\t" \ + "bne .-12\n\t" \ + : \ + : [fwb] "r" ((uintptr_t)(p)->fw_base), \ + [nfwb] "r" ((uintptr_t)(p)->not_fw_base) \ + : "r0", "r2", "cc"); \ + } while (0) /** * ECC / Ed / PQ signature verification. diff --git a/src/image.c b/src/image.c index 8012387a68..6e24f3daf9 100644 --- a/src/image.c +++ b/src/image.c @@ -83,11 +83,11 @@ int NOINLINEFUNCTION wolfBoot_hardened_CT_compare( { volatile int r1 = image_CT_compare(expected, actual, len); volatile int r2 = image_CT_compare(expected, actual, len); - if (r1 != 0) - return -1; - if (r2 != 0) - return -1; - return 0; + /* Combine both results without branching: non-zero if either independent + * comparison reported a mismatch. This preserves image_CT_compare()'s 0/1 + * return semantics and avoids data-dependent control flow, while a single + * fault can still subvert at most one of the two calls. */ + return (r1 | r2); } #if defined(WOLFBOOT_CERT_CHAIN_VERIFY) && \