diff --git a/config/examples/aurix-tc375-hsm-monolithic-update-wolfHSM-certs-rsa4096.config b/config/examples/aurix-tc375-hsm-monolithic-update-wolfHSM-certs-rsa4096.config new file mode 100644 index 0000000000..eacdfd7252 --- /dev/null +++ b/config/examples/aurix-tc375-hsm-monolithic-update-wolfHSM-certs-rsa4096.config @@ -0,0 +1,47 @@ +ARCH?=AURIX_TC3 +TARGET?=aurix_tc3xx +AURIX_TC3_HSM=1 +SIGN?=RSA4096 +HASH?=SHA256 +DEBUG?=0 +WOLFBOOT_VERSION?=1 +V?=0 +SPMATH?=1 +RAM_CODE?=1 +EXT_FLASH?=1 +EXT_BOOT=1 +EXT_UPDATE=1 +EXT_SWAP=1 +FLAGS_INVERT=1 +FLASH_MULTI_SECTOR_ERASE=1 +DEBUG_UART=1 +PRINTF_ENABLED=1 + +# wolfHSM options +WOLFHSM_SERVER=1 + +# Cert chain options +CERT_CHAIN_VERIFY=1 + +# RSA4096 cert chains need the larger header and stack +WOLFBOOT_HUGE_STACK=1 +IMAGE_HEADER_SIZE=4096 + +# self-header feature (persist header in external flash) +WOLFBOOT_SELF_HEADER=1 +SELF_HEADER_EXT=1 + +# Monolithic updates +SELF_UPDATE_MONOLITHIC=1 + +# Enable wolfBoot hooks +WOLFBOOT_HOOK_LOADER_POSTINIT=1 +WOLFBOOT_HOOKS_FILE=../wolfBoot-callbacks/wolfboot_hooks.c + +ARCH_FLASH_OFFSET=0x80028000 +WOLFBOOT_SECTOR_SIZE=0x4000 +WOLFBOOT_PARTITION_SIZE=0x30000 +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80038000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x80068000 +WOLFBOOT_PARTITION_SWAP_ADDRESS=0x80098000 +WOLFBOOT_PARTITION_SELF_HEADER_ADDRESS=0x8009C000 diff --git a/config/examples/sim-self-update-monolithic.config b/config/examples/sim-self-update-monolithic.config new file mode 100644 index 0000000000..a2d0597c46 --- /dev/null +++ b/config/examples/sim-self-update-monolithic.config @@ -0,0 +1,20 @@ +ARCH=sim +TARGET=sim +SIGN?=ED25519 +HASH?=SHA256 +WOLFBOOT_SMALL_STACK?=0 +SPI_FLASH=0 +DEBUG=1 +RAM_CODE=1 +SELF_UPDATE_MONOLITHIC=1 +WOLFBOOT_VERSION=1 + +# sizes should be multiple of system page size +WOLFBOOT_PARTITION_SIZE=0x40000 +WOLFBOOT_SECTOR_SIZE=0x1000 +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x20000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x60000 +WOLFBOOT_PARTITION_SWAP_ADDRESS=0xA0000 + +# required for keytools +WOLFBOOT_FIXED_PARTITIONS=1 diff --git a/docs/firmware_update.md b/docs/firmware_update.md index 971edcc845..b0cc6b0af9 100644 --- a/docs/firmware_update.md +++ b/docs/firmware_update.md @@ -200,6 +200,120 @@ regular signed image at the bootloader origin. See [Signing.md](Signing.md#header-only-output-wolfboot-self-header) for more detail on the `--header-only` sign tool option. +#### Monolithic updates + +The self-update mechanism can be used to update both the bootloader +**and** the application firmware in a single operation. Because +`wolfBoot_self_update()` copies `fw_size` bytes from the update image to +`ARCH_FLASH_OFFSET`, a payload that is larger than the bootloader region +will spill into the contiguous BOOT partition, overwriting whatever +application image was there previously. + +To enable this behavior, set `SELF_UPDATE_MONOLITHIC=1` in your build +configuration. The payload should be constructed by concatenating the +new bootloader binary with the new signed application image and signing +the result as a wolfBoot self-update. Note that the user must ensure that padding +is supplied such that the header of the new signed app image will be located +at an offset of `WOLFBOOT_PARTITION_BOOT_ADDRESS` from the base of the binary. + + +The following pseudo-shell-script demonstrates how to use standard CLI tools to +build this padded image, where `$PRIVATE_KEY`, `$ARCH_FLASH_OFFSET` and +`$WOLFBOOT_PARTITION_BOOT_ADDRESS` are the wolfBoot config variables that +correspond to your platform + +```sh +# Sign your app image as v2 for inclusion in the monolithic payload. This generates test-app/image_v2_signed.bin +tools/keytools/sign test-app/image.bin $(PRIVATE_KEY) 2 + +# Create padded wolfboot v2 binary file (0xFF fill to exact bootloader region size) +# Bootloader region = $WOLFBOOT_PARTITION_BOOT_ADDRESS - $ARCH_FLASH_OFFSET +dd if=/dev/zero bs=$$(($WOLFBOOT_PARTITION_BOOT_ADDRESS - $ARCH_FLASH_OFFSET)) count=1 2>/dev/null | tr '\000' '\377' > wolfboot_v2_padded.bin +dd if=wolfboot.bin of=wolfboot_v2_padded.bin conv=notrunc 2>/dev/null + +# Concatenate padded bootloader v2 + signed app v2 to form the monolithic payload +cat wolfboot_v2_padded.bin test-app/image_v2_signed.bin > monolithic_payload.bin +# Sign the monolithic payload as a wolfBoot self-update v2 +tools/keytools/sign --wolfboot-update monolithic_payload.bin $PRIVATE_KEY 2 +``` + +After the self-update completes, flash looks like: + +``` +ARCH_FLASH_OFFSET WOLFBOOT_PARTITION_BOOT_ADDRESS + | | + v v + [ new bootloader bytes | padding | new signed app image ] + <-------- fw_size -------------------------------------------> +``` + +##### Self-header interaction + +When `WOLFBOOT_SELF_HEADER` is enabled, the persisted header retains the +`fw_size`, hash and signature exactly as the signing tool produced them. +The hash covers the **entire** monolithic payload — both the bootloader +bytes and the nested application image. Later calls to +`wolfBoot_open_self()` / `wolfBoot_verify_integrity()` will re-hash +`fw_size` bytes starting at `ARCH_FLASH_OFFSET`, spanning into the BOOT +partition. + +##### Restrictions + +- **Not power-fail safe.** Like all self-updates, a monolithic update + erases the bootloader region and writes in-place. An interruption + during the write leaves the device unbootable. + +- **Not revertible.** There is no swap or rollback mechanism. The old + bootloader and application are destroyed during the update. + +- **Locks bootloader verification to a specific application version.** + Because the self-header hash covers the full monolithic image, any + independent application update will invalidate the persisted + self-header. To maintain a valid self-header, both components must + always be updated together as a single monolithic payload. + +- **Payload must fit in the UPDATE partition.** The signed monolithic + image (header + bootloader + signed application) plus the 5-byte + `pBOOT` trailer must not exceed `WOLFBOOT_PARTITION_SIZE`. + +##### Simulator test + +A simulator test is provided in `tools/test.mk` to exercise this use case: + +``` +cp config/examples/sim-self-update-monolithic.config .config +make clean && make +make test-sim-self-update-monolithic +``` + +#### Skipping boot image verification + +When wolfBoot is used together with the [self-header](#self-header-persisting-the-bootloader-manifest) +and [monolithic updates](#monolithic-updates) features, an external verifier such as +[wolfHSM](wolfHSM.md) can verify the combined bootloader+application payload before wolfBoot runs. +In this scenario, wolfBoot's own boot-time verification is redundant and can be skipped as a +performance optimization. + +Setting `WOLFBOOT_SKIP_BOOT_VERIFY=1` in the build configuration disables both the integrity (hash) +and authenticity (signature) checks that wolfBoot normally performs on the boot image at startup. + +**WARNING: This option completely disables boot-time firmware verification. It is only safe to use +when ALL of the following conditions are met:** + +- The self-header feature is enabled, so the bootloader manifest is persisted alongside the + application image +- Monolithic updates are enabled, so the bootloader and application are always updated together as a + single payload +- An external entity (e.g. an HSM running wolfHSM) is guaranteed to verify the full monolithic + payload before wolfBoot boots + +**Using this option outside of this specific scenario removes all boot-time authenticity and integrity +guarantees and is not secure.** + +Note that this option only affects verification of the boot image at startup. Firmware updates +staged in the update partition are still fully verified (signature and integrity) before being +installed, regardless of this setting. + ### Incremental updates (aka: 'delta' updates) wolfBoot supports incremental updates, based on a specific older version. The sign tool diff --git a/options.mk b/options.mk index ff57622796..c05bdb8bad 100644 --- a/options.mk +++ b/options.mk @@ -74,6 +74,12 @@ ifeq ($(WOLFBOOT_TPM_SEAL),1) endif endif +## Monolithic self-update: erase covers fw_size so the payload can span +## the bootloader region into the contiguous boot partition. +ifeq ($(SELF_UPDATE_MONOLITHIC),1) + CFLAGS+=-DWOLFBOOT_SELF_UPDATE_MONOLITHIC +endif + ## Persist wolfBoot self header at fixed address ## Invariants and defaults are enforced in wolfboot.h ifeq ($(WOLFBOOT_SELF_HEADER),1) @@ -681,6 +687,10 @@ ifeq ($(ALLOW_DOWNGRADE),1) CFLAGS+= -D"ALLOW_DOWNGRADE" endif +ifeq ($(WOLFBOOT_SKIP_BOOT_VERIFY),1) + CFLAGS+=-D"WOLFBOOT_SKIP_BOOT_VERIFY" +endif + ifeq ($(NVM_FLASH_WRITEONCE),1) CFLAGS+= -D"NVM_FLASH_WRITEONCE" endif diff --git a/src/update_disk.c b/src/update_disk.c index cdf815cd91..3f5c0917cd 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -424,6 +424,7 @@ void RAMFUNCTION wolfBoot_start(void) } os_image.fw_base = (uint8_t*)load_address; +#ifndef WOLFBOOT_SKIP_BOOT_VERIFY wolfBoot_printf("Checking image integrity..."); BENCHMARK_START(); if (wolfBoot_verify_integrity(&os_image) != 0) { @@ -445,6 +446,10 @@ void RAMFUNCTION wolfBoot_start(void) failures = 0; break; /* Success case */ } +#else + failures = 0; + break; /* Skip verification, boot directly */ +#endif } while (failures < MAX_FAILURES); if (failures) { diff --git a/src/update_flash.c b/src/update_flash.c index 054837d898..0b3f3a4824 100644 --- a/src/update_flash.c +++ b/src/update_flash.c @@ -67,11 +67,19 @@ static uint8_t buffer[FLASHBUFFER_SIZE] XALIGNED(4); #endif -static void RAMFUNCTION wolfBoot_erase_bootloader(void) +static void RAMFUNCTION wolfBoot_erase_bootloader(uint32_t len) { - uint32_t len = WOLFBOOT_PARTITION_BOOT_ADDRESS - ARCH_FLASH_OFFSET; +#ifdef WOLFBOOT_SELF_UPDATE_MONOLITHIC + /* Erase the full write range (rounded up to sector boundary) so that + * a monolithic payload that spills past the bootloader region into the + * contiguous boot partition lands on erased flash. */ + len = ((len + WOLFBOOT_SECTOR_SIZE - 1) / + WOLFBOOT_SECTOR_SIZE) * WOLFBOOT_SECTOR_SIZE; +#else + (void)len; + len = WOLFBOOT_PARTITION_BOOT_ADDRESS - ARCH_FLASH_OFFSET; +#endif hal_flash_erase(ARCH_FLASH_OFFSET, len); - } #include @@ -154,7 +162,7 @@ static void RAMFUNCTION wolfBoot_self_update(struct wolfBoot_image *src) #endif hal_flash_unlock(); - wolfBoot_erase_bootloader(); + wolfBoot_erase_bootloader(src->fw_size); #ifdef EXT_FLASH if (PART_IS_EXT(src)) { while (pos < src->fw_size) { @@ -1345,6 +1353,7 @@ void RAMFUNCTION wolfBoot_start(void) wolfBoot_printf("Booting version: 0x%x\n", wolfBoot_get_blob_version(boot.hdr)); +#ifndef WOLFBOOT_SKIP_BOOT_VERIFY if (bootRet < 0 || (wolfBoot_verify_integrity(&boot) < 0) || (wolfBoot_verify_authenticity(&boot) < 0) @@ -1376,6 +1385,11 @@ void RAMFUNCTION wolfBoot_start(void) } } PART_SANITY_CHECK(&boot); +#else + if (bootRet < 0) { + wolfBoot_panic(); + } +#endif #ifdef WOLFBOOT_ELF_FLASH_SCATTER unsigned long entry; diff --git a/src/update_flash_hwswap.c b/src/update_flash_hwswap.c index ec3816e5da..7006208562 100644 --- a/src/update_flash_hwswap.c +++ b/src/update_flash_hwswap.c @@ -50,10 +50,12 @@ void RAMFUNCTION wolfBoot_start(void) boot_panic(); for (;;) { - if ((wolfBoot_open_image(&fw_image, active) < 0) || - (wolfBoot_verify_integrity(&fw_image) < 0) || - (wolfBoot_verify_authenticity(&fw_image) < 0)) { - + if ((wolfBoot_open_image(&fw_image, active) < 0) +#ifndef WOLFBOOT_SKIP_BOOT_VERIFY + || (wolfBoot_verify_integrity(&fw_image) < 0) + || (wolfBoot_verify_authenticity(&fw_image) < 0) +#endif + ) { /* panic if authentication fails and no backup */ if (!wolfBoot_fallback_is_possible()) boot_panic(); diff --git a/src/update_ram.c b/src/update_ram.c index d415fc7602..3aba3dfde0 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -191,6 +191,7 @@ void RAMFUNCTION wolfBoot_start(void) goto backup_on_failure; } +#ifndef WOLFBOOT_SKIP_BOOT_VERIFY /* Verify image integrity (hash check) */ wolfBoot_printf("Checking integrity..."); BENCHMARK_START(); @@ -210,6 +211,7 @@ void RAMFUNCTION wolfBoot_start(void) goto backup_on_failure; } BENCHMARK_END("done"); +#endif { /* Success - integrity and signature valid */ diff --git a/tools/test.mk b/tools/test.mk index c177bdfb53..7218dced6d 100644 --- a/tools/test.mk +++ b/tools/test.mk @@ -266,6 +266,42 @@ test-sim-self-update: wolfboot.bin FORCE @# Verify dummy payload was written to bootloader region, indicating the self update swapped images as expected $(Q)cmp -n $$(wc -c < dummy_update.bin | awk '{print $$1}') dummy_update.bin internal_flash.dd && echo "=== Self-update test PASSED ===" +# Test monolithic self-update mechanism using simulator. A monolithic self-update +# updates both the bootloader AND the boot partition application image in a single +# operation by crafting a payload that spans the bootloader region and spills into +# the contiguous boot partition. +test-sim-self-update-monolithic: wolfboot.bin test-app/image_v1_signed.bin FORCE + @echo "=== Simulator Monolithic Self-Update Test ===" + @# Create dummy bootloader (0xAA pattern, exactly bootloader region size: WOLFBOOT_PARTITION_BOOT_ADDRESS - ARCH_FLASH_OFFSET) + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_PARTITION_BOOT_ADDRESS) - $(ARCH_FLASH_OFFSET))) count=1 2>/dev/null | tr '\000' '\252' > monolithic_dummy_bl.bin + @# Concatenate dummy bootloader + signed app image to form monolithic payload + $(Q)cat monolithic_dummy_bl.bin test-app/image_v1_signed.bin > monolithic_payload.bin + @# Sign monolithic payload as wolfBoot self-update v2 + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) --wolfboot-update monolithic_payload.bin $(PRIVATE_KEY) 2 + @# Create update partition with signed monolithic image and "pBOOT" trailer + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_PARTITION_SIZE))) count=1 2>/dev/null | tr '\000' '\377' > update_part.dd + $(Q)dd if=monolithic_payload_v2_signed.bin of=update_part.dd bs=1 conv=notrunc + $(Q)printf "pBOOT" | dd of=update_part.dd bs=1 seek=$$(($(WOLFBOOT_PARTITION_SIZE) - 5)) conv=notrunc + @# Create erased boot and swap partitions + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_PARTITION_SIZE))) count=1 2>/dev/null | tr '\000' '\377' > boot_part.dd + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_SECTOR_SIZE))) count=1 2>/dev/null | tr '\000' '\377' > erased_sec.dd + @# Assemble flash: wolfboot.bin at 0, empty boot partition, update partition, swap + $(Q)$(BINASSEMBLE) internal_flash.dd \ + 0 wolfboot.bin \ + $$(($(WOLFBOOT_PARTITION_BOOT_ADDRESS) - $(ARCH_FLASH_OFFSET))) boot_part.dd \ + $$(($(WOLFBOOT_PARTITION_UPDATE_ADDRESS) - $(ARCH_FLASH_OFFSET))) update_part.dd \ + $$(($(WOLFBOOT_PARTITION_SWAP_ADDRESS) - $(ARCH_FLASH_OFFSET))) erased_sec.dd + @# Run simulator - self-update fires, copies monolithic payload to offset 0 + $(Q)./wolfboot.elf get_version || true + @# Verify bootloader region contains 0xAA pattern (dummy bootloader was written) + $(Q)cmp -n $$(($(WOLFBOOT_PARTITION_BOOT_ADDRESS) - $(ARCH_FLASH_OFFSET))) monolithic_dummy_bl.bin internal_flash.dd + @echo " Bootloader region 0xAA pattern: PASSED" + @# Extract nested app from boot partition and verify it matches original signed app + $(Q)dd if=internal_flash.dd bs=1 skip=$$(($(WOLFBOOT_PARTITION_BOOT_ADDRESS) - $(ARCH_FLASH_OFFSET))) count=$$(wc -c < test-app/image_v1_signed.bin | awk '{print $$1}') of=nested_app.dd 2>/dev/null + $(Q)cmp nested_app.dd test-app/image_v1_signed.bin + @echo " Nested app at boot partition: PASSED" + @echo "=== Monolithic Self-Update Test PASSED ===" + # Test self-header cryptographic verification (hash + signature validation) # # Verifies that an application can cryptographically verify the bootloader using