diff --git a/.github/workflows/test-compressed-simulator.yml b/.github/workflows/test-compressed-simulator.yml new file mode 100644 index 0000000000..b4e05771c9 --- /dev/null +++ b/.github/workflows/test-compressed-simulator.yml @@ -0,0 +1,165 @@ +name: Compressed images - test with simulator target + +on: + push: + branches: [ 'master', 'main', 'release/**' ] + pull_request: + branches: [ '*' ] + +jobs: + compressed_simulator_tests: + runs-on: ubuntu-latest + container: + image: ghcr.io/wolfssl/wolfboot-ci-sim:v1.0 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + # GZIP_SMALL selects a different Huffman decoder in src/gzip.c, so + # both have to be exercised and not merely built. + sign: [ ED25519, ECC256 ] + gzip_small: [ 0, 1 ] + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: make clean + run: | + make distclean + + - name: Select config + run: | + cp config/examples/sim-compressed-ram.config .config + sed -i "s/^SIGN?=.*/SIGN?=${{ matrix.sign }}/" .config + sed -i "s/^GZIP_SMALL=.*//" .config + echo "GZIP_SMALL=${{ matrix.gzip_small }}" >> .config + + - name: Build tools + run: | + make -C tools/keytools && make -C tools/bin-assemble + + - name: Build wolfboot.elf and test-app/image.elf + run: | + make clean && make + + - name: Assemble the compressed A/B slots + run: | + make test-sim-compressed-ram-with-update + + - name: Run the compressed image simulator test + run: | + ./tools/scripts/sim-compressed-ram.sh + + compressed_encrypted_simulator_tests: + runs-on: ubuntu-latest + container: + image: ghcr.io/wolfssl/wolfboot-ci-sim:v1.0 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + # Each cipher has its own block size, which the per-slot keystream + # stride is derived from, so all three have to run. + cipher: [ chacha, aes128, aes256 ] + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: make clean + run: | + make distclean + + - name: Select config + run: | + cp config/examples/sim-compressed-ram-enc.config .config + case "${{ matrix.cipher }}" in + aes128) echo "ENCRYPT_WITH_AES128=1" >> .config ;; + aes256) echo "ENCRYPT_WITH_AES256=1" >> .config ;; + esac + + - name: Build tools + run: | + make -C tools/keytools && make -C tools/bin-assemble + + - name: Build wolfboot.elf and test-app/image.elf + run: | + make clean && make + + - name: Assemble the encrypted compressed A/B slots + run: | + make test-sim-compressed-ram-enc-with-update + + - name: Run the encrypted compressed image simulator test + run: | + ./tools/scripts/sim-compressed-ram-enc.sh + + compressed_cross_build: + runs-on: ubuntu-latest + container: + image: ghcr.io/wolfssl/wolfboot-ci-arm:v1.0 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + config: [ stm32h7-compressed-ram, stm32f407-compressed-ram ] + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Select config + run: | + cp config/examples/${{ matrix.config }}.config .config + + - name: Build + run: | + make clean && make keysclean && make + + # A link that succeeds but places the vector table somewhere other + # than the load address boots into garbage, and nothing catches that + # without hardware. Assert the layout from the ELF instead. + - name: Check the application is linked for the load address + run: | + LOAD=$(grep -E '^WOLFBOOT_LOAD_ADDRESS\??=' .config | tail -1 | cut -d= -f2) + SIZE=$(grep -E '^WOLFBOOT_RAMBOOT_MAX_SIZE\??=' .config | tail -1 | cut -d= -f2) + echo "load address $LOAD, region size $SIZE" + # .text must start exactly at the load address, with LMA == VMA + arm-none-eabi-objdump -h test-app/image.elf | \ + awk -v want=$(printf "%08x" $((LOAD))) ' + $2==".text" { if ($4!=want || $5!=want) { + printf "`.text` at vma %s lma %s, expected %s\n",$4,$5,want; exit 1 } + print "ok: .text at " $4 " (lma " $5 ")" }' + # word 0 of the image is the initial stack pointer and must be the + # top of the load region + SP=$(od -An -t x4 -N 4 test-app/image.bin | tr -d ' ') + WANT_SP=$(printf "%08x" $((LOAD + SIZE))) + if [ "$SP" != "$WANT_SP" ]; then + echo "initial SP is 0x$SP, expected 0x$WANT_SP"; exit 1 + fi + echo "ok: initial SP 0x$SP" + + - name: Check wolfBoot does not overlap the load region + run: | + LOAD=$(grep -E '^WOLFBOOT_LOAD_ADDRESS\??=' .config | tail -1 | cut -d= -f2) + END=$(arm-none-eabi-nm wolfboot.elf | awk '$3=="_end" {print $1}') + # Only meaningful when wolfBoot's RAM and the load region share an + # address space (STM32F4); on the H7 they are different SRAMs. + if [ $((0x$END)) -ge $((LOAD)) ] && \ + [ $((0x$END)) -lt $((LOAD + 0x100000)) ]; then + echo "wolfBoot _end 0x$END is inside the load region at $LOAD"; exit 1 + fi + echo "ok: wolfBoot _end 0x$END clear of the load region at $LOAD" diff --git a/.gitignore b/.gitignore index 3a07d3512c..29c7291703 100644 --- a/.gitignore +++ b/.gitignore @@ -453,6 +453,7 @@ bootmode1/ # Emulator test: generated linker scripts test-app/emu-test-apps/*/target.ld +test-app/*.gz # AMD/Xilinx Versal test artifacts uart_log*.txt diff --git a/arch.mk b/arch.mk index 15c5aebb36..1d69cc2c2c 100644 --- a/arch.mk +++ b/arch.mk @@ -3177,9 +3177,22 @@ else endif endif else - ifeq ($(DUALBANK_SWAP),1) + ifeq ($(COMPRESSED_RAM_SLOTS),1) + # Compressed A/B slots, execute from RAM. Both slots hold a signed, + # gzip-compressed image; the RAM loader picks the highest valid version, + # verifies the signature over the compressed bytes and inflates to + # WOLFBOOT_LOAD_ADDRESS. There is no swap or install step, so the slot + # roles are decided per boot by version, as for the hardware bank swap. + ifeq ($(DUALBANK_SWAP),1) + $(error COMPRESSED_RAM_SLOTS=1 is incompatible with DUALBANK_SWAP=1) + endif CFLAGS+=-DWOLFBOOT_DUALBOOT - UPDATE_OBJS:=src/update_flash_hwswap.o + UPDATE_OBJS:=src/update_ram.o + else + ifeq ($(DUALBANK_SWAP),1) + CFLAGS+=-DWOLFBOOT_DUALBOOT + UPDATE_OBJS:=src/update_flash_hwswap.o + endif endif endif diff --git a/config/examples/sim-compressed-ram-enc.config b/config/examples/sim-compressed-ram-enc.config new file mode 100644 index 0000000000..0abafe51f8 --- /dev/null +++ b/config/examples/sim-compressed-ram-enc.config @@ -0,0 +1,31 @@ +ARCH=sim +TARGET=sim +SIGN?=ED25519 +HASH?=SHA256 +WOLFBOOT_SMALL_STACK?=0 +SPI_FLASH=0 +DEBUG=1 + +# Compressed A/B slots, executed from RAM, encrypted at rest. +COMPRESSED_RAM_SLOTS=1 +COMPRESSED_ENCRYPT=1 +GZIP=1 + +# sizes should be multiple of system page size +WOLFBOOT_PARTITION_SIZE=0x40000 +WOLFBOOT_SECTOR_SIZE=0x1000 +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x100000 +WOLFBOOT_PARTITION_SWAP_ADDRESS=0x180000 + +# See sim-compressed-ram.config for why the load region sits above wolfboot.elf. +WOLFBOOT_LOAD_ADDRESS=0x1000000 +WOLFBOOT_RAMBOOT_MAX_SIZE=0x40000 + +# An encrypted slot cannot be verified in place, so it is decrypted into a +# staging region first. Must be disjoint from the load region, or the +# decompressor would overwrite its own input. +WOLFBOOT_COMPRESSED_STAGE_ADDRESS=0x1100000 + +# required for keytools +WOLFBOOT_FIXED_PARTITIONS=1 diff --git a/config/examples/sim-compressed-ram.config b/config/examples/sim-compressed-ram.config new file mode 100644 index 0000000000..620546662f --- /dev/null +++ b/config/examples/sim-compressed-ram.config @@ -0,0 +1,34 @@ +ARCH=sim +TARGET=sim +SIGN?=ED25519 +HASH?=SHA256 +WOLFBOOT_SMALL_STACK?=0 +SPI_FLASH=0 +DEBUG=1 + +# Compressed A/B slots, executed from RAM. Both slots hold a signed gzip +# stream; wolfBoot verifies the compressed bytes in place, inflates to +# WOLFBOOT_LOAD_ADDRESS and boots there. +COMPRESSED_RAM_SLOTS=1 +GZIP=1 + +# sizes should be multiple of system page size +WOLFBOOT_PARTITION_SIZE=0x40000 +WOLFBOOT_SECTOR_SIZE=0x1000 +WOLFBOOT_PARTITION_BOOT_ADDRESS=0x80000 +# if on external flash, it should be multiple of system page size +WOLFBOOT_PARTITION_UPDATE_ADDRESS=0x100000 +WOLFBOOT_PARTITION_SWAP_ADDRESS=0x180000 + +# The simulator's do_boot() writes a whole partition from the load address, +# so the load region must be at least WOLFBOOT_PARTITION_SIZE. +# +# WOLFBOOT_ORIGIN is 0 here (simulator flash lives at the runtime +# sim_ram_base), so the loader's bootloader-overlap guard degenerates to +# "below _end is suspect". wolfboot.elf is a non-PIE host binary loaded at +# 0x400000, so keep the load region above it. +WOLFBOOT_LOAD_ADDRESS=0x1000000 +WOLFBOOT_RAMBOOT_MAX_SIZE=0x40000 + +# required for keytools +WOLFBOOT_FIXED_PARTITIONS=1 diff --git a/config/examples/stm32f407-compressed-ram.config b/config/examples/stm32f407-compressed-ram.config new file mode 100644 index 0000000000..f34b864cb7 --- /dev/null +++ b/config/examples/stm32f407-compressed-ram.config @@ -0,0 +1,44 @@ +# STM32F407-DISCOVERY with compressed A/B images executed from RAM. +# +# The F407 has one contiguous 128KB SRAM shared between wolfBoot and the +# inflated application, so hal/stm32f4_ramboot.ld caps wolfBoot at the +# bottom 16KB and the application occupies the remaining 112KB. That is the +# ceiling on this part: the CCM RAM at 0x10000000 cannot be used, because +# on the F4 it is reachable only by the data bus and instruction fetch from +# it faults. +# +# GZIP_SMALL=1 selects the bit-serial Huffman decoder: a few KB less code +# and no static tables, which matters more than throughput on a 1MB part. +# +# Note that hal/stm32f4.c has no debug UART, so DEBUG=1 only buys symbols +# here: there is no console for the "Inflating ... done" progress line. +# Inspect the load region over SWD instead, or add a UART to the HAL. +ARCH?=ARM +TARGET?=stm32f4 +SIGN?=ED25519 +HASH?=SHA256 +DEBUG?=1 +VTOR?=1 +SPMATH?=1 + +COMPRESSED_RAM_SLOTS?=1 +GZIP?=1 +GZIP_SMALL?=1 + +WOLFBOOT_PARTITION_SIZE?=0x20000 +WOLFBOOT_SECTOR_SIZE?=0x20000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x08020000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x08040000 +# Unused in this mode (there is no swap or install step), but still +# required by the keytools and the partition trailer layout. +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x08060000 + +# SRAM above wolfBoot's 16KB region. 0x400-aligned for the SCB->VTOR write. +WOLFBOOT_LOAD_ADDRESS?=0x20004000 +WOLFBOOT_RAMBOOT_MAX_SIZE?=0x1C000 + +CLOCK_SPEED?=160000000 +STM32_PLLM?=8 +STM32_PLLN?=336 +STM32_PLLP?=2 +STM32_PLLQ?=7 diff --git a/config/examples/stm32f439zi-compressed-ram.config b/config/examples/stm32f439zi-compressed-ram.config new file mode 100644 index 0000000000..c3d252d333 --- /dev/null +++ b/config/examples/stm32f439zi-compressed-ram.config @@ -0,0 +1,66 @@ +# NUCLEO-F439ZI with compressed A/B images executed from RAM. +# +# The F439ZI has 2MB of flash and 256KB of SRAM, of which 192KB is +# contiguous at 0x20000000 (SRAM1 112KB + SRAM2 16KB + SRAM3 64KB). +# hal/stm32f4_ramboot.ld caps wolfBoot at the bottom 16KB, leaving 176KB +# for the inflated application. The 64KB CCM at 0x10000000 cannot be used: +# on the F4 it is reachable only by the data bus, so an instruction fetch +# from it faults. +# +# Console is USART3 on PD8/PD9, the ST-LINK virtual COM port on the +# NUCLEO-144 boards, via hal/uart/uart_drv_stm32f4.c. +ARCH?=ARM +TARGET?=stm32f4 +SIGN?=ED25519 +HASH?=SHA256 +DEBUG?=1 +DEBUG_UART?=1 +CFLAGS_EXTRA+=-DUSE_UART3 +VTOR?=1 +SPMATH?=1 + +COMPRESSED_RAM_SLOTS?=1 +GZIP?=1 +# Required at a 16KB bootloader RAM region: the table-driven decoder needs +# about 19KB of .bss for its Huffman tables and fails to link. +GZIP_SMALL?=1 + +WOLFBOOT_PARTITION_SIZE?=0x20000 +WOLFBOOT_SECTOR_SIZE?=0x20000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x08020000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x08040000 +# Unused in this mode (there is no swap or install step), but still +# required by the keytools and the partition trailer layout. +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x08060000 + +# SRAM above wolfBoot's 16KB region. 0x400-aligned for the SCB->VTOR write. +# This uses all the remaining SRAM, which is the right choice when the slots +# are not encrypted; the encrypted variant below has to give some back. +WOLFBOOT_LOAD_ADDRESS?=0x20004000 +WOLFBOOT_RAMBOOT_MAX_SIZE?=0x2C000 + +# Encryption of both slots at rest. Commented out because it needs a key +# provisioned on the device first: hal_get_image_encrypt_key() reads +# WOLFBOOT_COMPRESSED_KEY_ADDRESS, expecting the key followed by the nonce, +# and rejects an erased or all-zero region. Override that hook in the HAL to +# take the key from OTP or a secure element rather than flash. +# +# An encrypted slot cannot be verified in place, so it is decrypted into a +# staging region first, and that region must be disjoint from the load +# region. On this part the 176KB above wolfBoot therefore has to be split: +# 112KB of load region and a 64KB staging region at the top of SRAM, which +# bounds the compressed image to 64KB. +# +# Sign slot B with --encrypt-iv-offset $((WOLFBOOT_PARTITION_SIZE / 64)) for +# ChaCha20, or / 16 for AES-CTR, so the two slots never share a keystream. +#COMPRESSED_ENCRYPT?=1 +#WOLFBOOT_RAMBOOT_MAX_SIZE?=0x1C000 +#WOLFBOOT_COMPRESSED_STAGE_ADDRESS?=0x20020000 +#WOLFBOOT_COMPRESSED_KEY_ADDRESS?=0x080E0000 + +# HSE 8MHz from the ST-LINK MCO -> 168MHz SYSCLK, PCLK1 42MHz +CLOCK_SPEED?=168000000 +STM32_PLLM?=8 +STM32_PLLN?=336 +STM32_PLLP?=2 +STM32_PLLQ?=7 diff --git a/config/examples/stm32h7-compressed-ram.config b/config/examples/stm32h7-compressed-ram.config new file mode 100644 index 0000000000..856abd690b --- /dev/null +++ b/config/examples/stm32h7-compressed-ram.config @@ -0,0 +1,44 @@ +# STM32H7 (NUCLEO-H743ZI) with compressed A/B images executed from RAM. +# +# Both slots hold a signed gzip stream; wolfBoot verifies the compressed +# bytes in place in flash, inflates to AXI SRAM and boots there. Each slot +# only has to hold the compressed image, so A/B redundancy costs roughly +# half the flash of two uncompressed slots. +# +# wolfBoot itself runs from DTCM (hal/stm32h7.ld), which is a different RAM +# than the D1 AXI SRAM used for the load region, so the two cannot collide. +ARCH?=ARM +TARGET?=stm32h7 +SIGN?=ECC256 +HASH?=SHA256 +DEBUG?=1 +DEBUG_UART?=1 +VTOR?=1 +NO_ASM?=0 +EXT_FLASH?=0 +SPI_FLASH?=0 +QSPI_FLASH?=0 +OCTOSPI_FLASH?=0 +ALLOW_DOWNGRADE?=0 +NVM_FLASH_WRITEONCE?=0 +WOLFBOOT_VERSION?=1 +V?=0 +SPMATH?=1 +RAM_CODE?=0 +DUALBANK_SWAP?=0 + +COMPRESSED_RAM_SLOTS?=1 +GZIP?=1 +GZIP_SMALL?=0 + +WOLFBOOT_PARTITION_SIZE?=0xD0000 +WOLFBOOT_SECTOR_SIZE?=0x20000 +WOLFBOOT_PARTITION_BOOT_ADDRESS?=0x8020000 +WOLFBOOT_PARTITION_UPDATE_ADDRESS?=0x80F0000 +# Unused in this mode (there is no swap or install step), but still +# required by the keytools and the partition trailer layout. +WOLFBOOT_PARTITION_SWAP_ADDRESS?=0x81C0000 + +# D1 AXI SRAM (512KB). 0x400-aligned for the SCB->VTOR write in do_boot(). +WOLFBOOT_LOAD_ADDRESS?=0x24000000 +WOLFBOOT_RAMBOOT_MAX_SIZE?=0x60000 diff --git a/docs/Compression.md b/docs/Compression.md new file mode 100644 index 0000000000..6cbbe68a3a --- /dev/null +++ b/docs/Compression.md @@ -0,0 +1,149 @@ +# Compressed firmware images + +wolfBoot can boot a gzip-compressed, signed firmware image. Both A/B slots hold the image in compressed form, and wolfBoot decompresses the selected one into RAM and executes it from there. Because each slot only has to hold the compressed image, A/B redundancy costs roughly half the flash of two uncompressed slots, and rollback is preserved: the other slot still holds the previous compressed image. + +This is enabled with `COMPRESSED_RAM_SLOTS=1`. It is a different mechanism from [delta updates](firmware_update.md), which shrink the update that has to be transferred but still install into a same-size partition, and from [encrypted partitions](encrypted_partitions.md), which protect the update partition at rest. + +## Is this the mode you want + +**RAM, not flash, is the binding constraint here.** The image is executed from the load region, so the largest application this mode can run is the size of that region, no matter how much flash the compressed slots save. On a part with far more execute-in-place flash than contiguous SRAM - which is most microcontrollers - that ceiling is lower than what an ordinary uncompressed A/B layout already allows, and the freed flash cannot be spent on the application. + +Taking an STM32H743 with the layout in `config/examples/stm32h7.config` as the example: two uncompressed 832KB partitions give the application 832KB, while this mode caps it at the 384KB load region that `config/examples/stm32h7-compressed-ram.config` configures, or about 512KB at the very best, the size of D1 AXI SRAM. + +So this mode is the right choice when: + +- the application is small relative to SRAM, and halving the flash spent on A/B redundancy is worth more than execute-in-place; +- the slots are to stay encrypted at rest, so that no plaintext firmware ever exists in flash (see [Encryption](#encryption) below); +- the target already executes from RAM for other reasons. + +It is the wrong choice when the application is large and the part executes in place from internal flash. Nothing in wolfBoot converts a compressed slot into an execute-in-place image today; that is a separate update mode, and until it exists an execute-in-place target that wants a smaller update partition should use [delta updates](firmware_update.md). + +## How it works + +The signature covers the **compressed** bytes. On each boot wolfBoot: + +1. Selects the slot with the highest version that carries a valid image, exactly as the dual-boot updater does. +2. Verifies the integrity and signature of the compressed image, in place, without decompressing it. A corrupt payload is therefore rejected before the decompressor ever sees it. +3. Rejects the image if its uncompressed length does not fit the configured load region. +4. Inflates the payload to `WOLFBOOT_LOAD_ADDRESS` and cross-checks the number of bytes produced against the signed manifest. +5. Jumps to `WOLFBOOT_LOAD_ADDRESS`. + +Decompression of an authenticated input needs no separate signature over the output: DEFLATE is deterministic, the gzip trailer binds the output length and its CRC-32, and the manifest's `HDR_IMG_UNCOMPRESSED_SIZE` is signature-covered and checked against what was actually produced. + +There is no swap partition, no scratch sector and no install step. An update is applied by writing the new compressed image into the inactive slot and triggering an update; the slot roles are decided per boot by version. `WOLFBOOT_PARTITION_SWAP_ADDRESS` is unused in this mode but is still required by the keytools and the partition trailer layout. + +## Manifest fields + +Two TLVs are added to the image header (see [firmware_image.md](firmware_image.md)): + +| Tag | Name | Length | Contents | +|-----|------|--------|----------| +| 0x08 | `HDR_IMG_COMPRESSION` | 2 | Algorithm id, little-endian. `0x0001` is gzip/DEFLATE. | +| 0x09 | `HDR_IMG_UNCOMPRESSED_SIZE` | 4 | Uncompressed length in bytes, little-endian. | + +Compression is detected by the presence of `HDR_IMG_COMPRESSION`, not by a bit in `HDR_IMG_TYPE`. A manifest that carries the tag with an unknown algorithm, or without a valid uncompressed-size tag, is rejected rather than booted as a raw payload. + +## Preparing an image + +The `sign` tool does not compress. Compress the image with your own tooling and sign the result, so the bytes wolfBoot verifies are byte-identical to the bytes you produced: + +```sh +gzip -9 -n test-app/image.bin +./tools/keytools/sign --compressed test-app/image.bin.gz wolfboot_signing_private_key.der 1 +``` + +This produces `test-app/image_v1_signed_compressed.bin`. Use `-n` so gzip does not embed the original file name and timestamp, which would make the output non-reproducible; the tool warns if it finds them. + +The tool validates the gzip container, takes the uncompressed length from its ISIZE trailer, and prints the ratio it achieved. It rejects anything that is not a single DEFLATE-method gzip stream. + +`--encrypt` composes as usual and runs after signing, producing `..._signed_compressed_and_encrypted.bin`. + +## Build options + +| Option | Meaning | +|--------|---------| +| `COMPRESSED_RAM_SLOTS=1` | Enable compressed A/B slots executed from RAM. Implies `GZIP=1` and selects the RAM loader. | +| `WOLFBOOT_LOAD_ADDRESS` | Where the image is inflated and executed. Required. | +| `WOLFBOOT_RAMBOOT_MAX_SIZE` | Capacity of the load region. Required: it is what bounds the decompressor. | +| `GZIP_SMALL=1` | Use the bit-serial Huffman decoder: a few KB less code and no static tables, at a fraction of the table-driven decoder's rate. | +| `COMPRESSED_ENCRYPT=1` | Keep both slots encrypted at rest. Requires `WOLFBOOT_COMPRESSED_STAGE_ADDRESS`. See below. | + +The following are rejected at build time because they cannot work with this mode: `DELTA_UPDATES` (there is nothing to patch before inflating), `WOLFBOOT_ELF`, `DUALBANK_SWAP`, `ENCRYPT` (use `COMPRESSED_ENCRYPT` instead), a non-memory-mapped slot (`EXT_FLASH` with `NO_XIP`), and signing a wolfBoot self-update with `--compressed` (wolfBoot cannot inflate itself). + +## Sizing the load region + +The whole uncompressed image must fit in the load region, and the region must not overlap wolfBoot's own code, data or stack. + +`WOLFBOOT_LOAD_ADDRESS` must be aligned to 0x400 on Arm. `do_boot()` writes it to `SCB->VTOR`, whose `TBLOFF` field has no low bits, so a misaligned value would be silently truncated and the core would fetch vectors from the wrong place. This is checked at build time. + +The table-driven gzip decoder needs about 19KB of `.bss` for its Huffman tables. On a part where wolfBoot's RAM region is smaller than that, the link fails with a `region RAM overflowed` error; use `GZIP_SMALL=1`, which needs none of those tables. + +On a target whose caches wolfBoot enables, the HAL must override the weak `hal_cache_sync_range()` to clean the data cache and invalidate the instruction cache over the inflated image, or the core may execute stale instructions. + +## Rollback behaviour + +Two different failures have different outcomes, and the distinction matters: + +- **A slot that fails verification** is abandoned and wolfBoot boots the other slot. This covers a corrupt or incompletely written image and is the property the A/B arrangement exists for. +- **A slot that boots but is never confirmed** with `wolfBoot_success()` is retried. On the next boot `wolfBoot_dualboot_candidate()` will only abandon it in favour of the other slot if that is not a downgrade. With the default `ALLOW_DOWNGRADE=0`, an unconfirmed *newer* image keeps being retried and logs `Rollback to lower version not allowed`. Set `ALLOW_DOWNGRADE=1` if you want an unconfirmed image to fall back to an older one. This is the same policy the hardware-bank-swap updater applies. + +`wolfBoot_success()` confirms the slot that is actually on trial, not `PART_BOOT`. In this mode nothing remaps the booted slot the way a hardware bank swap does, so confirming `PART_BOOT` unconditionally would leave a freshly booted update in `IMG_STATE_TESTING` for ever. + +Note that `wolfBoot_current_firmware_version()` still reports the version in `PART_BOOT`, which is not necessarily the running image. This applies to every `WOLFBOOT_DUALBOOT` configuration without a hardware bank remap, not just to compressed images. An application that needs to know which slot it is running from should compare both slot versions and states itself. + +## Limitations + +wolfBoot inflates exactly one gzip member, so a concatenated multi-member `.gz` would boot only the first one. `gzip` over a single file never produces one, and the manifest length cross-check catches it if it happens. + +The compressed slot must be directly readable by the CPU, because it is verified and inflated in place. A slot on a non-memory-mapped external flash (`EXT_FLASH=1` with `NO_XIP=1`) would have to be staged into RAM first, and the only staging region wolfBoot has is the load region itself, which is where the decompressor writes. That combination is therefore rejected at build time rather than left to overwrite its own input mid-stream. + +## Encryption + +`COMPRESSED_ENCRYPT=1` keeps both slots encrypted at rest and decrypts on the boot path. ChaCha20-256 is the default; `ENCRYPT_WITH_AES128=1` or `ENCRYPT_WITH_AES256=1` select AES-CTR instead. + +This is a separate layer from `ENCRYPT=1`, which is rejected alongside `COMPRESSED_RAM_SLOTS=1`. That one hooks the external flash read/write interface, so it requires `EXT_FLASH` or `MMU` and cannot serve slots in internal flash; it keys its stream from the update partition's base address and classifies only the update and swap partitions; and it consumes its key once per update, erasing it in `wolfBoot_success()`. A pair of compressed slots stays encrypted for the life of the device and exchanges roles by version, so it needs a long-lived key and decryption on the boot path. `src/compress_crypt.c` provides that, in the way `src/update_disk.c` already does for disk images. + +### The key + +The image key is obtained through `hal_get_image_encrypt_key()`. The weak default reads a dedicated provisioning region at `WOLFBOOT_COMPRESSED_KEY_ADDRESS`, holding the key followed by the nonce, and rejects an unprogrammed or zeroized region. The region must be outside both slots, because wolfBoot never erases it and the slots are rewritten by updates. + +Override the hook in the HAL to take the key from OTP, a PUF, a secure element or wolfHSM, so the plaintext key never exists in flash at all. All key material is zeroized on every exit path, including failures, and before the jump to the image. + +### Per-slot keystreams + +Both slots are encrypted under the same key, so they must never share a keystream: the XOR of two slots encrypted at the same counter reveals the XOR of the two firmware versions. Each slot is therefore given its own keystream region, one partition's worth of cipher blocks apart (`WOLFBOOT_COMPRESSED_IV_SLOT_STRIDE`, derived from the layout by default). The existing `FALLBACK_IV_OFFSET` in `libwolfboot.c` exists for the same reason on the swap path. + +The signing tool has to match, so the artifact is produced for the slot it will be written to: + +```sh +# slot A (the BOOT partition): the default, offset 0 +./tools/keytools/sign --compressed --encrypt enc_key.der image.bin.gz key.der 1 + +# slot B (the UPDATE partition): one partition's worth of blocks +# ChaCha20 blocks are 64 bytes, AES-CTR blocks are 16 +./tools/keytools/sign --compressed --encrypt enc_key.der \ + --encrypt-iv-offset $((WOLFBOOT_PARTITION_SIZE / 64)) \ + image.bin.gz key.der 2 +``` + +An artifact written to the wrong slot decrypts to garbage, so its manifest magic fails and the slot reads as empty; wolfBoot then boots the other slot rather than attempting to verify or inflate it. `--encrypt-iv-offset` defaults to 0, so an invocation without it is byte-for-byte what previous releases produced. + +### Staging + +An encrypted slot cannot be verified in place, so `WOLFBOOT_COMPRESSED_STAGE_ADDRESS` is required and must be disjoint from the load region. The slot is decrypted into the staging region, the signature is verified over the decrypted compressed bytes, and the payload is then inflated from staging to the load region. Two regions of a partition each are therefore needed in RAM, on top of the application itself. + +Note that with encryption enabled the application cannot read its own version through `wolfBoot_current_firmware_version()` at all: the manifest in flash is ciphertext and `libwolfboot` linked into the application has no image key. + +## Examples + +`config/examples/sim-compressed-ram.config` runs the whole flow under the simulator. `tools/scripts/sim-compressed-ram.sh` asserts slot selection, confirmation on the booted slot, fallback from a corrupt slot, and rejection of an image too large to inflate: + +```sh +cp config/examples/sim-compressed-ram.config .config +make keysclean && make && make test-sim-compressed-ram-with-update +./tools/scripts/sim-compressed-ram.sh +``` + +`config/examples/stm32h7-compressed-ram.config` boots an STM32H7 from D1 AXI SRAM at `0x24000000`, leaving wolfBoot in DTCM so the two RAMs cannot collide. + +`config/examples/stm32f407-compressed-ram.config` shows the tight case. The STM32F407 has a single contiguous 128KB SRAM shared between wolfBoot and the application, so `hal/stm32f4_ramboot.ld` caps wolfBoot at the bottom 16KB and the application occupies the remaining 112KB. That is the ceiling on this part: the CCM RAM at `0x10000000` cannot be used, because on the F4 it is reachable only by the data bus and an instruction fetch from it faults. `GZIP_SMALL=1` is required at that bootloader region size. diff --git a/docs/README.md b/docs/README.md index cbe7a1f33a..de0ecf1871 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ See also: [wolfBoot Product Overview](https://www.wolfssl.com/products/wolfboot/ - [**azure_keyvault.md**](./azure_keyvault.md) - Using Azure Key Vault for key management and signing with wolfBoot. - [**CMake.md**](./CMake.md) - CMake-based build setup, presets, toolchains, and tips for building wolfBoot. - [**compile.md**](./compile.md) - How to build/compile wolfBoot (toolchains, options, typical steps). +- [**Compression.md**](./Compression.md) - Compressed firmware images: gzip-compressed A/B slots executed from RAM. - [**encrypted_partitions.md**](./encrypted_partitions.md) - Creating and managing encrypted firmware/data partitions. - [**firmware_image.md**](./firmware_image.md) - wolfBoot firmware image format, layout, and metadata. - [**firmware_update.md**](./firmware_update.md) - Update flow: slots, verification, rollback, and recovery. diff --git a/docs/Signing.md b/docs/Signing.md index e904c560e9..c62c52af86 100644 --- a/docs/Signing.md +++ b/docs/Signing.md @@ -212,6 +212,18 @@ For complete documentation of the self-header feature — including configuration, update flow, and runtime verification — see [firmware_update.md](firmware_update.md#self-header-persisting-the-bootloader-manifest). +#### Compressed images + +A gzip-compressed image can be signed with the `--compressed` option: + + * `--compressed` the input file is an already-gzipped image. The sign tool does not compress; it + validates the gzip container, records the algorithm and the uncompressed length in the manifest, + and signs the compressed bytes. Produce the input with `gzip -9 -n `. + +The output is named `_v_signed_compressed.bin`. This option requires the bootloader to be +built with `COMPRESSED_RAM_SLOTS=1`, and cannot be combined with `--delta` or with a wolfBoot +self-update. See [Compression.md](Compression.md). + #### Encryption using a symmetric key Although signed to be authenticated, by default the image is not encrypted and diff --git a/docs/compile.md b/docs/compile.md index 7f646e03f7..13f63bcd24 100644 --- a/docs/compile.md +++ b/docs/compile.md @@ -206,6 +206,16 @@ differences between the old firmware to replace, currently running on the target For more information and examples, see the [firmware update](firmware_update.md) section. +### Compressed images + +wolfBoot can boot a gzip-compressed image, keeping both A/B slots compressed and decompressing the +selected one into RAM. To enable this feature, compile with `COMPRESSED_RAM_SLOTS=1` and set +`WOLFBOOT_LOAD_ADDRESS` and `WOLFBOOT_RAMBOOT_MAX_SIZE`. + +The sign tool does not compress: compress the image yourself and pass the `--compressed` option. + +For more information and examples, see the [compression](Compression.md) section. + ### Enable debug symbols To debug the bootloader, simply compile with `DEBUG=1`. The size of the bootloader will increase diff --git a/docs/encrypted_partitions.md b/docs/encrypted_partitions.md index c977a95f14..dd6b13aa9f 100644 --- a/docs/encrypted_partitions.md +++ b/docs/encrypted_partitions.md @@ -216,6 +216,16 @@ which will produce as output the file `test-app/image_v24_signed_and_encrypted.b When used in combination with delta updates, encryption works the same way as in full-update mode. The final delta image is encrypted with the selected algorithm. +### Encryption of compressed images + +Compressed A/B slots (`COMPRESSED_RAM_SLOTS=1`) use `COMPRESSED_ENCRYPT=1` rather than +`ENCRYPT=1`, and the build rejects the combination. This mechanism works at the external flash +interface, requires `EXT_FLASH` or `MMU`, and consumes its key once per update, none of which fits a +pair of slots that stay encrypted for the life of the device and exchange roles by version. The +compressed variant has its own long-lived key and per-slot keystreams; see +[Compression.md](Compression.md). + + ### Encryption of self-updates When used in combination with bootloader 'self' updates, the encryption algorithm must be configured to run from RAM. diff --git a/docs/firmware_image.md b/docs/firmware_image.md index 2daded06ab..d257c5481d 100644 --- a/docs/firmware_image.md +++ b/docs/firmware_image.md @@ -51,6 +51,11 @@ Each **Type** has a different meaning, and integrate information about the firmw - A 'firmware signature' Tag (type: 0x0020, size: 64 Bytes) used to validate the signature stored with the firmware against a known public key - A 'firmware type' Tag (type: 0x0030, size: 2 Bytes) used to identify the type of firmware, and the authentication mechanism in use. +A compressed image carries two additional Tags (see [Compression.md](Compression.md)): + + - A 'compression' Tag (type: 0x0008, size: 2 Bytes) indicating the algorithm used to compress the payload. 0x0001 is gzip/DEFLATE. The presence of this Tag is what marks the payload as compressed. + - An 'uncompressed size' Tag (type: 0x0009, size: 4 Bytes) indicating the length of the payload once decompressed. The bootloader uses it to reject an image too large for its load region before decompressing, and to cross-check the number of bytes actually produced. + A 'public key hint digest' tag is transmitted in the header (type: 0x10, size:32 Bytes). This tag contains the SHA digest of the public key used by the signing tool. The bootloader may use this field to locate the correct public key in case of multiple keys available. diff --git a/hal/sim.c b/hal/sim.c index 469e1d3593..9ac81a3940 100644 --- a/hal/sim.c +++ b/hal/sim.c @@ -559,6 +559,20 @@ int hal_flash_erase(uintptr_t address, int len) return 0; } +#ifdef WOLFBOOT_COMPRESSED_ENCRYPT +#include "compress_crypt.h" +/* Simulator image key: the same value tools/test.mk gives the signer. */ +int hal_get_image_encrypt_key(uint8_t *key, uint8_t *nonce) +{ + static const char test_key[] = "0123456789abcdef0123456789abcdef"; + static const char test_nonce[] = "0123456789abcdef"; + + memcpy(key, test_key, COMPRESS_CRYPT_KEY_SIZE); + memcpy(nonce, test_nonce, COMPRESS_CRYPT_NONCE_SIZE); + return 0; +} +#endif + void hal_init(void) { int ret; @@ -580,6 +594,60 @@ void hal_init(void) } #endif /* EXT_FLASH */ +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS + /* The load address is a plain constant, not rebased onto sim_ram_base, + * so map it fixed. do_boot() writes a whole partition from it. */ + { + void *load_region; + int map_flags = MAP_PRIVATE | MAP_ANONYMOUS; +#ifdef MAP_FIXED_NOREPLACE + map_flags |= MAP_FIXED_NOREPLACE; +#endif + if ((size_t)WOLFBOOT_RAMBOOT_MAX_SIZE < + (WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE)) { + wolfBoot_printf("WOLFBOOT_RAMBOOT_MAX_SIZE must be at least one " + "partition for the simulator\n"); + exit(-1); + } + load_region = mmap((void*)(uintptr_t)WOLFBOOT_LOAD_ADDRESS, + (size_t)WOLFBOOT_RAMBOOT_MAX_SIZE, PROT_READ | PROT_WRITE, + map_flags, -1, 0); + if ((load_region == MAP_FAILED) || + (load_region != (void*)(uintptr_t)WOLFBOOT_LOAD_ADDRESS)) { + wolfBoot_printf("failed to map the RAM load region at %p\n", + (void*)(uintptr_t)WOLFBOOT_LOAD_ADDRESS); + exit(-1); + } + wolfBoot_printf("Simulator assigned RAM load region to base %p " + "(%zu bytes)\n", load_region, + (size_t)WOLFBOOT_RAMBOOT_MAX_SIZE); + } +#ifdef WOLFBOOT_COMPRESSED_ENCRYPT + /* Staging for the decrypted, still compressed, slot. */ + { + void *stage_region; + int map_flags = MAP_PRIVATE | MAP_ANONYMOUS; +#ifdef MAP_FIXED_NOREPLACE + map_flags |= MAP_FIXED_NOREPLACE; +#endif + stage_region = mmap( + (void*)(uintptr_t)WOLFBOOT_COMPRESSED_STAGE_ADDRESS, + (size_t)WOLFBOOT_PARTITION_SIZE, PROT_READ | PROT_WRITE, + map_flags, -1, 0); + if ((stage_region == MAP_FAILED) || + (stage_region != + (void*)(uintptr_t)WOLFBOOT_COMPRESSED_STAGE_ADDRESS)) { + wolfBoot_printf("failed to map the staging region at %p\n", + (void*)(uintptr_t)WOLFBOOT_COMPRESSED_STAGE_ADDRESS); + exit(-1); + } + wolfBoot_printf("Simulator assigned staging region to base %p " + "(%zu bytes)\n", stage_region, + (size_t)WOLFBOOT_PARTITION_SIZE); + } +#endif /* WOLFBOOT_COMPRESSED_ENCRYPT */ +#endif /* WOLFBOOT_COMPRESSED_RAM_SLOTS */ + #ifdef DUALBANK_SWAP sim_dualbank_register_load(); #endif diff --git a/hal/stm32f4_ramboot.ld b/hal/stm32f4_ramboot.ld new file mode 100644 index 0000000000..84ed286bdd --- /dev/null +++ b/hal/stm32f4_ramboot.ld @@ -0,0 +1,57 @@ +/* COMPRESSED_RAM_SLOTS=1: wolfBoot shares one SRAM with the inflated image, so + * cap it at 16KB. END_STACK must stay below WOLFBOOT_LOAD_ADDRESS. */ +MEMORY +{ + FLASH (rx) : ORIGIN = 0x08000000, LENGTH = @BOOTLOADER_PARTITION_SIZE@ + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00004000 +} + +SECTIONS +{ + .text : + { + _start_text = .; + KEEP(*(.isr_vector)) + *(.text*) + *(.rodata*) + . = ALIGN(4); + _end_text = .; + } > FLASH + + .edidx : + { + . = ALIGN(4); + *(.ARM.exidx*) + } > FLASH + + _stored_data = .; + .data : AT (_stored_data) + { + _start_data = .; + KEEP(*(.data*)) + . = ALIGN(4); + KEEP(*(.ramcode)) + . = ALIGN(4); + _end_data = .; + } > RAM + + .bss (NOLOAD) : + { + _start_bss = .; + __bss_start__ = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + _end_bss = .; + __bss_end__ = .; + _end = .; + } > RAM + . = ALIGN(4); +} + +_wolfboot_partition_boot_address = @WOLFBOOT_PARTITION_BOOT_ADDRESS@; +_wolfboot_partition_size = @WOLFBOOT_PARTITION_SIZE@; +_wolfboot_partition_update_address = @WOLFBOOT_PARTITION_UPDATE_ADDRESS@; +_wolfboot_partition_swap_address = @WOLFBOOT_PARTITION_SWAP_ADDRESS@; + +END_STACK = ORIGIN(RAM) + LENGTH(RAM); diff --git a/hal/uart/uart_drv_stm32f4.c b/hal/uart/uart_drv_stm32f4.c index c2ea800de1..0acecc78f2 100644 --- a/hal/uart/uart_drv_stm32f4.c +++ b/hal/uart/uart_drv_stm32f4.c @@ -48,6 +48,14 @@ #define CLOCK_SPEED (168000000) #endif +#ifndef BAUD_RATE +#define BAUD_RATE (115200) +#endif + +/* APB == SYSCLK here: clock_pll_on() writes the 4-bit AHB encoding into the + * 3-bit PPRE fields, so both decode as /1 (RCC_CFGR 0x0001200A on F439ZI). */ +#define UART_PCLK (CLOCK_SPEED) + /* Common GPIO Config */ #define GPIO_MODE_AF (2) @@ -166,8 +174,8 @@ int uart_init(uint32_t bitrate, uint8_t data, char parity, uint8_t stop) /* Configure for TX + RX */ UART_CR1 |= (UART_CR1_TX_ENABLE | UART_CR1_RX_ENABLE); - /* Configure clock */ - UART_BRR = CLOCK_SPEED / bitrate; + /* BRR is the 16x-oversampled divisor: a rounded PCLK/baud. */ + UART_BRR = (UART_PCLK + (bitrate / 2)) / bitrate; /* Configure data bits */ if (data == 8) @@ -202,6 +210,18 @@ int uart_init(uint32_t bitrate, uint8_t data, char parity, uint8_t stop) #ifdef DEBUG_UART void uart_write(const char *buf, unsigned int len) { + /* Nothing calls the initializer above on the console path, so the first + * print used to hang. Skipped under UART_FLASH: loader.c inits it. */ +#ifndef UART_FLASH + static int uart_ready = 0; + + if (!uart_ready) { + if (uart_init(BAUD_RATE, 8, 'N', 1) != 0) { + return; + } + uart_ready = 1; + } +#endif while (len--) { uart_tx(*buf); buf++; diff --git a/include/compress_crypt.h b/include/compress_crypt.h new file mode 100644 index 0000000000..2abb506719 --- /dev/null +++ b/include/compress_crypt.h @@ -0,0 +1,74 @@ +/* compress_crypt.h + * + * Decryption of compressed A/B slots, for the RAM loader. + * + * + * 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 + */ +#ifndef WOLFBOOT_COMPRESS_CRYPT_H +#define WOLFBOOT_COMPRESS_CRYPT_H + +#include +#include "wolfboot/wolfboot.h" + +#ifdef WOLFBOOT_COMPRESSED_ENCRYPT + +/* The ENCRYPT_*_SIZE aliases in wolfboot.h are scoped to EXT_ENCRYPTED, so + * select from the unconditional base constants instead. */ +#if defined(ENCRYPT_WITH_CHACHA) + #define COMPRESS_CRYPT_BLOCK_SIZE ENCRYPT_BLOCK_SIZE_CHACHA + #define COMPRESS_CRYPT_KEY_SIZE ENCRYPT_KEY_SIZE_CHACHA + #define COMPRESS_CRYPT_NONCE_SIZE ENCRYPT_NONCE_SIZE_CHACHA +#elif defined(ENCRYPT_WITH_AES128) + #define COMPRESS_CRYPT_BLOCK_SIZE ENCRYPT_BLOCK_SIZE_AES + #define COMPRESS_CRYPT_KEY_SIZE ENCRYPT_KEY_SIZE_AES128 + #define COMPRESS_CRYPT_NONCE_SIZE ENCRYPT_NONCE_SIZE_AES +#elif defined(ENCRYPT_WITH_AES256) + #define COMPRESS_CRYPT_BLOCK_SIZE ENCRYPT_BLOCK_SIZE_AES + #define COMPRESS_CRYPT_KEY_SIZE ENCRYPT_KEY_SIZE_AES256 + #define COMPRESS_CRYPT_NONCE_SIZE ENCRYPT_NONCE_SIZE_AES +#else + #error "WOLFBOOT_COMPRESSED_ENCRYPT needs ChaCha20, AES128 or AES256" +#endif + +/* Long-lived image key, which must outlive wolfBoot_success() and live + * outside both slots. Weak default reads WOLFBOOT_COMPRESSED_KEY_ADDRESS. */ +int hal_get_image_encrypt_key(uint8_t *key, uint8_t *nonce); + +/* Fetch the key and initialize the cipher. Idempotent. */ +int compress_crypt_init(void); + +/* Zeroize all key material. Safe when uninitialized. */ +void compress_crypt_clear(void); + +/* Scrub a buffer, so the loader need not pull in the wolfCrypt headers. */ +void compress_crypt_scrub(void *p, uint32_t len); + +/* Decrypt IMAGE_HEADER_SIZE bytes from the start of a slot. The caller + * checks the magic. part selects the slot's keystream region. */ +int compress_crypt_header(int part, const uint8_t *src, uint8_t *dst); + +/* Decrypt a slot's header and len payload bytes into dst, which must hold + * IMAGE_HEADER_SIZE + len. len comes from the decrypted header. */ +int compress_crypt_body(int part, const uint8_t *src, uint8_t *dst, + uint32_t len); + +#endif /* WOLFBOOT_COMPRESSED_ENCRYPT */ + +#endif /* WOLFBOOT_COMPRESS_CRYPT_H */ diff --git a/include/hal.h b/include/hal.h index be24b7f719..d2853e2a85 100644 --- a/include/hal.h +++ b/include/hal.h @@ -146,6 +146,12 @@ void hal_prepare_boot(void); * never silently runs DMA through write-back memory. */ int hal_dma_set_noncached(uintptr_t start, uintptr_t end); +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS +/* Sync caches over a freshly inflated image before the jump to it. The weak + * default does nothing; a HAL that enables caches must override it. */ +void hal_cache_sync_range(void *addr, size_t len); +#endif + #ifdef DUALBANK_SWAP void hal_flash_dualbank_swap(void); #endif diff --git a/include/wolfboot/wolfboot.h b/include/wolfboot/wolfboot.h index 0ca0301dec..6834ff6dbf 100644 --- a/include/wolfboot/wolfboot.h +++ b/include/wolfboot/wolfboot.h @@ -289,6 +289,12 @@ extern "C" { #define HDR_IMG_DELTA_BASE 0x05 #define HDR_IMG_DELTA_SIZE 0x06 #define HDR_IMG_DELTA_BASE_HASH 0x07 +/* uint16 LE algorithm id. Presence is the signal: the HDR_IMG_TYPE 0x00F0 + * nibble is equality-tested, so it cannot carry an orthogonal flag. */ +#define HDR_IMG_COMPRESSION 0x08 +/* uint32 LE uncompressed length, from the gzip ISIZE trailer at sign time + * so the signature binds it. Bounds the inflate. */ +#define HDR_IMG_UNCOMPRESSED_SIZE 0x09 #define HDR_PUBKEY 0x10 #define HDR_SECONDARY_CIPHER 0x11 #define HDR_SECONDARY_PUBKEY 0x12 @@ -309,6 +315,9 @@ extern "C" { #define HDR_DEVICE_TREE_DIGEST 0x35 #define HDR_PADDING 0xFF +/* HDR_IMG_COMPRESSION algorithm ids */ +#define HDR_COMP_GZIP 0x0001 /* RFC 1952 gzip / 1951 DEFLATE */ + /* Auth Key types */ #define AUTH_KEY_NONE 0x00 #define AUTH_KEY_ED25519 0x01 @@ -656,6 +665,53 @@ extern "C" { #define WOLFBOOT_TRAILER_SECTORS 1 #endif +/* Compressed A/B images; roles decided per boot by version, as for the + * bank-swap updater. Bootloader only: target.h is included under WOLFBOOT. */ +#if defined(WOLFBOOT_COMPRESSED_RAM_SLOTS) && defined(__WOLFBOOT) + #ifndef WOLFBOOT_GZIP + #error "WOLFBOOT_COMPRESSED_RAM_SLOTS requires GZIP=1 (src/gzip.c)" + #endif + #ifndef WOLFBOOT_DUALBOOT + #error "WOLFBOOT_COMPRESSED_RAM_SLOTS requires WOLFBOOT_DUALBOOT" + #endif + #if defined(WOLFBOOT_NO_LOAD_ADDRESS) || !defined(WOLFBOOT_LOAD_ADDRESS) + #error "WOLFBOOT_COMPRESSED_RAM_SLOTS requires WOLFBOOT_LOAD_ADDRESS" + #endif + #ifndef WOLFBOOT_RAMBOOT_MAX_SIZE + #error "WOLFBOOT_COMPRESSED_RAM_SLOTS requires WOLFBOOT_RAMBOOT_MAX_SIZE" + #endif + #ifdef DELTA_UPDATES + #error "DELTA_UPDATES is not compatible with WOLFBOOT_COMPRESSED_RAM_SLOTS" + #endif + #ifdef EXT_ENCRYPTED + #error "ENCRYPT=1 is not compatible with COMPRESSED_RAM_SLOTS" + #endif + #ifdef WOLFBOOT_ELF + #error "WOLFBOOT_ELF is not supported with WOLFBOOT_COMPRESSED_RAM_SLOTS" + #endif + #if defined(WOLFBOOT_ORIGIN) && defined(BOOTLOADER_PARTITION_SIZE) && \ + ((WOLFBOOT_LOAD_ADDRESS + 0) >= (WOLFBOOT_ORIGIN + 0)) && \ + ((WOLFBOOT_LOAD_ADDRESS + 0) < \ + (WOLFBOOT_ORIGIN + 0 + BOOTLOADER_PARTITION_SIZE + 0)) + #error "WOLFBOOT_LOAD_ADDRESS falls inside the bootloader partition" + #endif + /* VTOR.TBLOFF has no low bits: an unaligned value is truncated and the + * core fetches vectors from the wrong place. */ + #if defined(ARCH_ARM) && !defined(NO_VTOR) && \ + (((WOLFBOOT_LOAD_ADDRESS + 0) & 0x3FF) != 0) + #error "WOLFBOOT_LOAD_ADDRESS must be 0x400-aligned (VTOR)" + #endif +#endif /* WOLFBOOT_COMPRESSED_RAM_SLOTS */ + +#if defined(WOLFBOOT_COMPRESSED_ENCRYPT) && defined(__WOLFBOOT) + #ifndef WOLFBOOT_COMPRESSED_RAM_SLOTS + #error "WOLFBOOT_COMPRESSED_ENCRYPT requires WOLFBOOT_COMPRESSED_RAM_SLOTS" + #endif + #ifndef WOLFBOOT_COMPRESSED_STAGE_ADDRESS + #error "COMPRESSED_ENCRYPT needs a STAGE_ADDRESS" + #endif +#endif /* WOLFBOOT_COMPRESSED_ENCRYPT */ + #define PART_BOOT 0 #define PART_UPDATE 1 #define PART_SWAP 2 @@ -732,6 +788,7 @@ int wolfBoot_clear_trailer(uint8_t part); int wolfBoot_fallback_is_possible(void); int wolfBoot_dualboot_candidate(void); +int wolfBoot_dualboot_select(uint32_t boot_v, uint32_t update_v); int wolfBoot_dualboot_candidate_addr(void**); int wolfBoot_get_partition_state(uint8_t part, uint8_t *st); diff --git a/options.mk b/options.mk index 930a7f1872..cebcf3cdf7 100644 --- a/options.mk +++ b/options.mk @@ -1186,6 +1186,80 @@ ifeq ($(GZIP),1) endif endif +# COMPRESSED_RAM_SLOTS=1 boots a gzip-compressed, wolfBoot-signed image from +# either A/B slot. The signature covers the compressed bytes; wolfBoot +# verifies them in place, inflates to WOLFBOOT_LOAD_ADDRESS and boots there. +# Both slots stay compressed, so A/B redundancy costs roughly half the flash +# of two uncompressed slots, and rollback is unchanged: the other slot still +# holds the previous compressed image. The update strategy object +# (src/update_ram.o) is selected in arch.mk, which is included first. +COMPRESSED_RAM_SLOTS ?= 0 +COMPRESSED_ENCRYPT ?= 0 +ifeq ($(COMPRESSED_RAM_SLOTS),1) + CFLAGS+=-DWOLFBOOT_COMPRESSED_RAM_SLOTS + # The inflater is mandatory; pull it in if the GZIP gate above did not. + ifneq ($(GZIP),1) + GZIP := 1 + OBJS += src/gzip.o + CFLAGS+=-DWOLFBOOT_GZIP + ifeq ($(GZIP_SMALL),1) + CFLAGS+=-DWOLFBOOT_GZIP_SMALL + endif + endif + ifeq ($(strip $(WOLFBOOT_LOAD_ADDRESS)),) + $(error COMPRESSED_RAM_SLOTS=1 requires WOLFBOOT_LOAD_ADDRESS) + endif + ifeq ($(strip $(WOLFBOOT_RAMBOOT_MAX_SIZE)),) + $(error COMPRESSED_RAM_SLOTS=1 requires WOLFBOOT_RAMBOOT_MAX_SIZE to bound the inflate destination) + endif + ifeq ($(DELTA_UPDATES),1) + $(error DELTA_UPDATES is incompatible with COMPRESSED_RAM_SLOTS) + endif + ifeq ($(ENCRYPT),1) + $(error ENCRYPT=1 is not supported with COMPRESSED_RAM_SLOTS: see docs/Compression.md) + endif + # Targets whose RAM load region is carved out of the same SRAM wolfBoot + # runs from need a reduced bootloader region, or wolfBoot's stack lands + # inside the inflated image. Opt in by adding hal/_ramboot.ld. + ifneq ($(wildcard hal/$(TARGET)_ramboot.ld),) + LSCRIPT_IN=hal/$(TARGET)_ramboot.ld + endif + # COMPRESSED_ENCRYPT=1 keeps both slots encrypted at rest and + # decrypts on the boot path. Deliberately does NOT set EXT_ENCRYPTED: that + # layer hooks the external flash interface, so it needs EXT_FLASH or MMU + # and cannot serve slots in internal flash, and its key is consumed once + # per update. See src/compress_crypt.c. + ifeq ($(COMPRESSED_ENCRYPT),1) + CFLAGS+=-DWOLFBOOT_COMPRESSED_ENCRYPT + OBJS+=src/compress_crypt.o + ifeq ($(strip $(WOLFBOOT_COMPRESSED_STAGE_ADDRESS)),) + $(error COMPRESSED_ENCRYPT=1 requires WOLFBOOT_COMPRESSED_STAGE_ADDRESS: an encrypted slot cannot be verified in place) + endif + CFLAGS+=-DWOLFBOOT_COMPRESSED_STAGE_ADDRESS=$(WOLFBOOT_COMPRESSED_STAGE_ADDRESS) + ifneq ($(WOLFBOOT_COMPRESSED_KEY_ADDRESS),) + CFLAGS+=-DWOLFBOOT_COMPRESSED_KEY_ADDRESS=$(WOLFBOOT_COMPRESSED_KEY_ADDRESS) + endif + ifneq ($(WOLFBOOT_COMPRESSED_IV_SLOT_STRIDE),) + CFLAGS+=-DWOLFBOOT_COMPRESSED_IV_SLOT_STRIDE=$(WOLFBOOT_COMPRESSED_IV_SLOT_STRIDE) + endif + ifeq ($(ENCRYPT_WITH_AES128),1) + CFLAGS+=-DWOLFSSL_AES_COUNTER -DWOLFSSL_AES_DIRECT + CFLAGS+=-DENCRYPT_WITH_AES128 -DWOLFSSL_AES_128 + WOLFCRYPT_OBJS+=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/aes.o + else + ifeq ($(ENCRYPT_WITH_AES256),1) + CFLAGS+=-DWOLFSSL_AES_COUNTER -DWOLFSSL_AES_DIRECT + CFLAGS+=-DENCRYPT_WITH_AES256 -DWOLFSSL_AES_256 + WOLFCRYPT_OBJS+=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/aes.o + else + ENCRYPT_WITH_CHACHA=1 + WOLFCRYPT_OBJS+=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/chacha.o + CFLAGS+=-DENCRYPT_WITH_CHACHA -DHAVE_CHACHA + endif + endif + endif +endif + # FIT_RAMDISK=1 enables FIT ramdisk (initramfs) extraction and DTB # /chosen/linux,initrd-{start,end} fixup. Compressed (gzip) ramdisks # decompress through the same path when GZIP=1. diff --git a/src/compress_crypt.c b/src/compress_crypt.c new file mode 100644 index 0000000000..22fb324f27 --- /dev/null +++ b/src/compress_crypt.c @@ -0,0 +1,267 @@ +/* compress_crypt.c + * + * Decryption of compressed A/B slots, for the RAM loader. + * + * + * 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 + */ + +/* Separate from EXT_ENCRYPTED: that needs EXT_FLASH or MMU and erases its + * key in wolfBoot_success(). Decrypt only; slots are never written here. */ + +#include +#include + +#include "image.h" +#include "printf.h" +#include "compress_crypt.h" +#include "wolfboot/wolfboot.h" + +#ifdef WOLFBOOT_COMPRESSED_ENCRYPT + +#include "wolfssl/wolfcrypt/settings.h" +#ifdef ENCRYPT_WITH_CHACHA + #include "wolfssl/wolfcrypt/chacha.h" +#else + #include "wolfssl/wolfcrypt/aes.h" +#endif +#define WOLFSSL_MISC_INCLUDED /* allow misc.c code to be inlined */ +#include +#include /* for ForceZero */ + +/* The body starts at IMAGE_HEADER_SIZE; a fractional block index there would + * desynchronise the keystream from the signing tool's. */ +#if (IMAGE_HEADER_SIZE % COMPRESS_CRYPT_BLOCK_SIZE) != 0 + #error "IMAGE_HEADER_SIZE must be a multiple of the cipher block size" +#endif + +/* Keystream blocks per slot. Both share a key, so a shared keystream would + * leak the XOR of the two firmware versions; one partition is disjoint. */ +#ifndef WOLFBOOT_COMPRESSED_IV_SLOT_STRIDE +#define WOLFBOOT_COMPRESSED_IV_SLOT_STRIDE \ + (WOLFBOOT_PARTITION_SIZE / COMPRESS_CRYPT_BLOCK_SIZE) +#endif + +#ifdef ENCRYPT_WITH_CHACHA +static ChaCha cc_cipher; +#else +static Aes cc_cipher; +#endif + +static uint8_t cc_nonce[COMPRESS_CRYPT_NONCE_SIZE]; +static int cc_ready = 0; +#ifndef ENCRYPT_WITH_CHACHA +/* Separate from cc_ready: AesInit can succeed where the key set-up fails. */ +static int cc_aes_inited = 0; +#endif + +/* Default key source: a provisioning region outside both slots. Override in + * the HAL for OTP, a PUF, a secure element or wolfHSM. */ +int __attribute__((weak)) hal_get_image_encrypt_key(uint8_t *key, + uint8_t *nonce) +{ +#ifdef WOLFBOOT_COMPRESSED_KEY_ADDRESS + const uint8_t *src = (const uint8_t *)(uintptr_t) + (WOLFBOOT_COMPRESSED_KEY_ADDRESS); + uint32_t i; + uint8_t acc_or = 0x00; + uint8_t acc_and = 0xFF; + + memcpy(key, src, COMPRESS_CRYPT_KEY_SIZE); + memcpy(nonce, src + COMPRESS_CRYPT_KEY_SIZE, COMPRESS_CRYPT_NONCE_SIZE); + + /* Reject an erased or zeroed region. No early exit, so neither the trip + * count nor the branch depends on key bytes. */ + for (i = 0; i < COMPRESS_CRYPT_KEY_SIZE; i++) { + acc_or |= key[i]; + acc_and &= key[i]; + } + if ((acc_or == 0x00) || (acc_and == 0xFF)) { + ForceZero(key, COMPRESS_CRYPT_KEY_SIZE); + ForceZero(nonce, COMPRESS_CRYPT_NONCE_SIZE); + return -1; + } + return 0; +#else + (void)key; + (void)nonce; + /* No region and no HAL override: fail rather than use an unset key. */ + return -1; +#endif +} + +int compress_crypt_init(void) +{ + uint8_t key[COMPRESS_CRYPT_KEY_SIZE]; + int ret; + + if (cc_ready) { + return 0; + } + memset(key, 0, sizeof(key)); + memset(cc_nonce, 0, sizeof(cc_nonce)); + + ret = hal_get_image_encrypt_key(key, cc_nonce); + if (ret != 0) { + wolfBoot_printf("No image encryption key available\n"); + goto exit; + } + + memset(&cc_cipher, 0, sizeof(cc_cipher)); +#ifdef ENCRYPT_WITH_CHACHA + ret = wc_Chacha_SetKey(&cc_cipher, key, (uint32_t)sizeof(key)); +#else + /* Sets devId, without which a hardware AES callback registered on + * WOLFBOOT_DEVID_CRYPT would silently not engage. */ + ret = wc_AesInit(&cc_cipher, NULL, WOLFBOOT_DEVID_CRYPT); + if (ret != 0) { + wolfBoot_printf("Image cipher init failed (%d)\n", ret); + goto exit; + } + cc_aes_inited = 1; + /* CTR: same transform both ways, so key for encryption. */ + ret = wc_AesSetKeyDirect(&cc_cipher, key, (word32)sizeof(key), cc_nonce, + AES_ENCRYPTION); +#endif + if (ret != 0) { + wolfBoot_printf("Image cipher init failed (%d)\n", ret); + goto exit; + } + cc_ready = 1; + +exit: + ForceZero(key, sizeof(key)); + if (ret != 0) { + compress_crypt_clear(); + } + return ret; +} + +void compress_crypt_clear(void) +{ +#ifndef ENCRYPT_WITH_CHACHA + if (cc_aes_inited) { + wc_AesFree(&cc_cipher); + cc_aes_inited = 0; + } +#endif + ForceZero(&cc_cipher, sizeof(cc_cipher)); + ForceZero(cc_nonce, sizeof(cc_nonce)); + cc_ready = 0; +} + +void compress_crypt_scrub(void *p, uint32_t len) +{ + if ((p != NULL) && (len > 0)) { + ForceZero(p, len); + } +} + +/* First keystream block belonging to a slot. */ +static uint32_t cc_slot_base(int part) +{ + if (part == PART_BOOT) { + return 0; + } + return (uint32_t)(WOLFBOOT_COMPRESSED_IV_SLOT_STRIDE); +} + +static void cc_set_iv(int part, uint32_t block) +{ + uint32_t ctr = cc_slot_base(part) + block; +#ifdef ENCRYPT_WITH_CHACHA + /* The ChaCha20 32-bit block counter is the block index. */ + (void)wc_Chacha_SetIV(&cc_cipher, cc_nonce, ctr); +#else + uint8_t iv[COMPRESS_CRYPT_NONCE_SIZE]; + int i; + + /* Add the block index to the big-endian 128-bit counter. Fixed trip + * count, so the carry cannot leak nonce bytes through timing. */ + memcpy(iv, cc_nonce, sizeof(iv)); + for (i = (int)sizeof(iv) - 1; i >= 0; i--) { + uint32_t sum = (uint32_t)iv[i] + (ctr & 0xFF); + iv[i] = (uint8_t)(sum & 0xFF); + ctr >>= 8; + ctr += (sum >> 8); + } + (void)wc_AesSetIV(&cc_cipher, iv); + ForceZero(iv, sizeof(iv)); +#endif +} + +/* Decrypt len bytes from the given block offset within the slot. */ +static int cc_decrypt_range(int part, const uint8_t *src, uint8_t *dst, + uint32_t block_off, uint32_t len) +{ + uint32_t off = 0; + + if (!cc_ready) { + return -1; + } + /* Re-anchor per block, so a short final block and a non-contiguous + * source both stay in step with the signing tool's keystream. */ + while (off < len) { + uint32_t chunk = len - off; + int ret; + + if (chunk > (uint32_t)COMPRESS_CRYPT_BLOCK_SIZE) { + chunk = (uint32_t)COMPRESS_CRYPT_BLOCK_SIZE; + } + cc_set_iv(part, block_off + (off / COMPRESS_CRYPT_BLOCK_SIZE)); +#ifdef ENCRYPT_WITH_CHACHA + ret = wc_Chacha_Process(&cc_cipher, dst + off, src + off, chunk); +#else + ret = wc_AesCtrEncrypt(&cc_cipher, dst + off, src + off, chunk); +#endif + if (ret != 0) { + return -1; + } + off += chunk; + } + return 0; +} + +int compress_crypt_header(int part, const uint8_t *src, uint8_t *dst) +{ + if ((src == NULL) || (dst == NULL)) { + return -1; + } + if (compress_crypt_init() != 0) { + return -1; + } + return cc_decrypt_range(part, src, dst, 0, (uint32_t)IMAGE_HEADER_SIZE); +} + +int compress_crypt_body(int part, const uint8_t *src, uint8_t *dst, + uint32_t len) +{ + /* Checks the arguments and initializes the cipher for both halves. */ + if (compress_crypt_header(part, src, dst) != 0) { + return -1; + } + /* Exactly len bytes: rounding up to the tool's padded block would + * write past the staging buffer. */ + return cc_decrypt_range(part, src + IMAGE_HEADER_SIZE, + dst + IMAGE_HEADER_SIZE, + (uint32_t)IMAGE_HEADER_SIZE / (uint32_t)COMPRESS_CRYPT_BLOCK_SIZE, + len); +} + +#endif /* WOLFBOOT_COMPRESSED_ENCRYPT */ diff --git a/src/libwolfboot.c b/src/libwolfboot.c index 9ef6d737b8..958ffec47b 100644 --- a/src/libwolfboot.c +++ b/src/libwolfboot.c @@ -1653,6 +1653,49 @@ void RAMFUNCTION wolfBoot_update_trigger(void) void RAMFUNCTION wolfBoot_success(void) { uint8_t st = IMG_STATE_SUCCESS; +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS + { + uint8_t boot_st = IMG_STATE_NEW; + uint8_t update_st = IMG_STATE_NEW; + int confirm_update = 0; + + /* Nothing remaps the booted slot here the way a bank swap does, so + * confirming PART_BOOT would strand a booted update in TESTING. */ + if (wolfBoot_get_partition_state(PART_BOOT, &boot_st) != 0) { + boot_st = IMG_STATE_NEW; + } + if (wolfBoot_get_partition_state(PART_UPDATE, &update_st) != 0) { + update_st = IMG_STATE_NEW; + } + if (update_st == IMG_STATE_TESTING) { + if (boot_st != IMG_STATE_TESTING) { + confirm_update = 1; + } +#ifdef WOLFBOOT_FIXED_PARTITIONS + /* Both testing means a corrupted trailer: take the newer. Under + * encryption both read 0, falling through to PART_BOOT. */ + else if (wolfBoot_get_image_version(PART_UPDATE) > + wolfBoot_get_image_version(PART_BOOT)) { + confirm_update = 1; + } +#endif /* WOLFBOOT_FIXED_PARTITIONS */ + } + if (confirm_update) { + /* Test the macro directly, so a target without external flash + * folds these calls away rather than failing to link. */ + if (FLAGS_UPDATE_EXT()) { + ext_flash_unlock(); + wolfBoot_set_partition_state(PART_UPDATE, st); + ext_flash_lock(); + } else { + hal_flash_unlock(); + wolfBoot_set_partition_state(PART_UPDATE, st); + hal_flash_lock(); + } + return; + } + } +#endif if (FLAGS_BOOT_EXT()) { ext_flash_unlock(); wolfBoot_set_partition_state(PART_BOOT, st); @@ -2534,17 +2577,13 @@ uint16_t wolfBoot_get_image_type(uint8_t part) * Returns -1 if no valid candidate is found. * */ -int wolfBoot_dualboot_candidate(void) +/* Split from the version lookup so a caller reading versions another way, + * from decrypted headers say, shares these anti-rollback rules. */ +int wolfBoot_dualboot_select(uint32_t boot_v, uint32_t update_v) { int candidate = PART_BOOT; int fallback_possible = 0; - uint32_t boot_v, update_v; uint8_t p_state; - /* Find the candidate */ - boot_v = wolfBoot_current_firmware_version(); - update_v = wolfBoot_update_firmware_version(); - - wolfBoot_printf("Versions: Boot %d, Update %d\n", boot_v, update_v); /* -1 means no images available */ if ((boot_v == 0) && (update_v == 0)) @@ -2578,6 +2617,18 @@ int wolfBoot_dualboot_candidate(void) } return candidate; } + +int wolfBoot_dualboot_candidate(void) +{ + uint32_t boot_v, update_v; + + boot_v = wolfBoot_current_firmware_version(); + update_v = wolfBoot_update_firmware_version(); + + wolfBoot_printf("Versions: Boot %d, Update %d\n", boot_v, update_v); + + return wolfBoot_dualboot_select(boot_v, update_v); +} #else static int wolfBoot_current_firmware_version(void) diff --git a/src/update_ram.c b/src/update_ram.c index c40e5e2c88..bb8b372821 100644 --- a/src/update_ram.c +++ b/src/update_ram.c @@ -42,6 +42,12 @@ #ifdef WOLFBOOT_ELF #include "elf.h" #endif +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS +#include "gzip.h" +#endif +#ifdef WOLFBOOT_COMPRESSED_ENCRYPT +#include "compress_crypt.h" +#endif #if defined(WOLFBOOT_ZYNQMP_FSBL) && defined(MMU) #include "../hal/zynqmp_atf.h" #endif @@ -64,6 +70,12 @@ extern uint8_t _end[]; /* linker symbol: end of wolfBoot BSS */ #define WOLFBOOT_USE_RAMBOOT #endif +#if defined(WOLFBOOT_COMPRESSED_RAM_SLOTS) && defined(WOLFBOOT_USE_RAMBOOT) +/* RAMBOOT stages the slot to the inflate destination, so inflate would eat + * its own input, and the path below is compiled out entirely. */ + #error "WOLFBOOT_COMPRESSED_RAM_SLOTS does not support a staged slot yet" +#endif + #ifdef WOLFBOOT_USE_RAMBOOT /* Function to load image from flash to ram */ @@ -173,6 +185,215 @@ int wolfBoot_ramboot(struct wolfBoot_image *img, uint8_t *src, uint8_t *dst) } #endif /* WOLFBOOT_USE_RAMBOOT */ +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS + +/* 1 = gzip, 0 = none, -1 = unusable. Unusable must not fall through to + * booting raw: that would be a jump into DEFLATE data. */ +static int wolfBoot_image_compression(struct wolfBoot_image *img, + uint32_t *uncompressed_sz) +{ + uint8_t *tlv = NULL; + uint16_t len; + uint16_t algo; + + len = wolfBoot_get_header(img, HDR_IMG_COMPRESSION, &tlv); + if ((len == 0) || (tlv == NULL)) { + return 0; + } + if (len != 2) { + return -1; + } + algo = (uint16_t)tlv[0] | ((uint16_t)tlv[1] << 8); + if (algo != HDR_COMP_GZIP) { + return -1; + } + len = wolfBoot_get_header(img, HDR_IMG_UNCOMPRESSED_SIZE, &tlv); + if ((len != 4) || (tlv == NULL)) { + return -1; + } + *uncompressed_sz = (uint32_t)tlv[0] | ((uint32_t)tlv[1] << 8) | + ((uint32_t)tlv[2] << 16) | ((uint32_t)tlv[3] << 24); + return 1; +} + +#ifdef WOLFBOOT_COMPRESSED_ENCRYPT + +/* Plaintext header of whichever slot is being examined. */ +static uint8_t compressed_hdr[IMAGE_HEADER_SIZE]; + +static uint8_t *compressed_slot_base(int part) +{ + if (part == PART_BOOT) { + return (uint8_t *)WOLFBOOT_PARTITION_BOOT_ADDRESS; + } + return (uint8_t *)WOLFBOOT_PARTITION_UPDATE_ADDRESS; +} + +/* Version of an encrypted slot, 0 if undecryptable. Slot selection + * compares versions, which are ciphertext until the key is applied. */ +static uint32_t compressed_slot_version(int part) +{ + uint32_t magic; + + if (compress_crypt_header(part, compressed_slot_base(part), + compressed_hdr) != 0) { + return 0; + } + memcpy(&magic, compressed_hdr, sizeof(magic)); + if (magic != WOLFBOOT_MAGIC) { + return 0; + } + return wolfBoot_get_blob_version(compressed_hdr); +} + +/* Decrypt the slot into staging: an encrypted slot cannot be verified in + * place the way an unencrypted one is. */ +static int compressed_stage_slot(int part) +{ + uint32_t magic; + uint32_t len; + + if (compress_crypt_header(part, compressed_slot_base(part), + compressed_hdr) != 0) { + wolfBoot_printf("Cannot decrypt slot %d header\n", part); + return -1; + } + memcpy(&magic, compressed_hdr, sizeof(magic)); + if (magic != WOLFBOOT_MAGIC) { + wolfBoot_printf("No valid image in slot %d\n", part); + return -1; + } + len = wolfBoot_image_size(compressed_hdr); + /* Bound the not-yet-authenticated length before it drives the copy. */ + if ((len == 0) || + (len > (uint32_t)(WOLFBOOT_PARTITION_SIZE - IMAGE_HEADER_SIZE))) { + wolfBoot_printf("Invalid encrypted image size %u in slot %d\n", + (unsigned)len, part); + return -1; + } + if (compress_crypt_body(part, compressed_slot_base(part), + (uint8_t *)(uintptr_t)WOLFBOOT_COMPRESSED_STAGE_ADDRESS, + len) != 0) { + wolfBoot_printf("Cannot decrypt slot %d\n", part); + return -1; + } + return 0; +} + +#endif /* WOLFBOOT_COMPRESSED_ENCRYPT */ + +/* Drop key material on paths that skip the normal teardown: panic() spins, + * so the key would otherwise sit in .bss readable over a debug port. */ +static void compressed_wipe_secrets(void) +{ +#ifdef WOLFBOOT_COMPRESSED_ENCRYPT + compress_crypt_clear(); + compress_crypt_scrub(compressed_hdr, (uint32_t)sizeof(compressed_hdr)); +#endif +} + +/* Empty by default: wolfBoot leaves caches off on most targets. A HAL that + * enables them must override this or the core may fetch stale code. */ +void __attribute__((weak)) hal_cache_sync_range(void *addr, size_t len) +{ + (void)addr; + (void)len; +} + +/* Inflate to the execution address. 1 = staged, 0 = uncompressed so the + * caller copies it, -1 = reject this image and try the other slot. */ +static int wolfBoot_stage_compressed(struct wolfBoot_image *img, + uint32_t *load_address) +{ + uint32_t uncompressed_sz = 0; + uint32_t out_len = 0; + uint8_t *src; + uintptr_t dst_lo; + uintptr_t dst_hi; + int comp; + int ret; + + comp = wolfBoot_image_compression(img, &uncompressed_sz); + if (comp < 0) { + wolfBoot_printf("Unsupported or malformed compression TLV\n"); + return -1; + } + if (comp == 0) { + /* Uncompressed is legitimate here, but still bound it: fw_size is + * signed, the region capacity is a property of this build. */ + if (img->fw_size > (uint32_t)WOLFBOOT_RAMBOOT_MAX_SIZE) { + wolfBoot_printf("Image size %u exceeds RAM load capacity %u\n", + (unsigned)img->fw_size, + (unsigned)WOLFBOOT_RAMBOOT_MAX_SIZE); + return -1; + } + return 0; + } + + src = img->fw_base; + dst_lo = (uintptr_t)load_address; + dst_hi = dst_lo + (uintptr_t)WOLFBOOT_RAMBOOT_MAX_SIZE; + + /* Signed length, but this build's capacity: an image signed for a + * target with more RAM must be rejected, not overrun the region. */ + if ((uncompressed_sz == 0) || + (uncompressed_sz > (uint32_t)WOLFBOOT_RAMBOOT_MAX_SIZE)) { + wolfBoot_printf("Uncompressed size %u exceeds RAM load capacity " + "%u\n", (unsigned)uncompressed_sz, + (unsigned)WOLFBOOT_RAMBOOT_MAX_SIZE); + return -1; + } + + /* The output buffer is the LZ77 window, so an overlap lets inflate eat + * its own input. Fixed by the layout, so both slots fail alike. */ + if (ramboot_region_overlap(dst_lo, dst_hi, (uintptr_t)src, + (uintptr_t)src + img->fw_size)) { + wolfBoot_printf("Error: compressed source %p-%p overlaps inflate " + "destination %p-%p\n", (void*)src, + (void*)((uintptr_t)src + img->fw_size), + (void*)dst_lo, (void*)dst_hi); + compressed_wipe_secrets(); + wolfBoot_panic(); + return -1; + } +#if defined(__WOLFBOOT) && defined(WOLFBOOT_ORIGIN) + if (ramboot_region_overlap(dst_lo, dst_lo + uncompressed_sz, + (uintptr_t)(WOLFBOOT_ORIGIN), (uintptr_t)_end)) { + wolfBoot_printf("Error: inflate destination overlaps wolfBoot\n"); + compressed_wipe_secrets(); + wolfBoot_panic(); + return -1; + } +#endif + + wolfBoot_printf("Inflating %u bytes from %p to RAM at %p (%u bytes)...", + (unsigned)img->fw_size, src, load_address, + (unsigned)uncompressed_sz); + /* out_max is the real capacity, not the manifest's claim; the claim is + * cross-checked below, keeping the two independent. */ + ret = wolfBoot_gunzip(src, img->fw_size, (uint8_t*)load_address, + (uint32_t)WOLFBOOT_RAMBOOT_MAX_SIZE, &out_len); + if (ret != 0) { + wolfBoot_printf("FAILED (%d)\n", ret); + return -1; + } + /* The gzip trailer binds out_len; this binds it to the manifest. */ + if (out_len != uncompressed_sz) { + wolfBoot_printf("Inflated %u bytes, manifest says %u\n", + (unsigned)out_len, (unsigned)uncompressed_sz); + return -1; + } + wolfBoot_printf("done\n"); + /* The ELF loader and FIT parser take fw_size as their capacity. */ + img->fw_size = out_len; + hal_cache_sync_range((void*)load_address, (size_t)out_len); + return 1; +} + + +#endif /* WOLFBOOT_COMPRESSED_RAM_SLOTS */ + + #ifdef WOLFBOOT_UBOOT_LEGACY /* Validate a 64-byte U-Boot legacy image header (image_header_t). * @@ -290,6 +511,9 @@ void RAMFUNCTION wolfBoot_start(void) #endif uint32_t *load_address = NULL; uint32_t *source_address = NULL; +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS + int staged = 0; /* set once inflated, so the plain copy is skipped */ +#endif #ifdef HAVE_PARTITION_TRAILERS uint8_t p_state; #endif @@ -333,8 +557,21 @@ void RAMFUNCTION wolfBoot_start(void) wolfBoot_invalidate_hdr_cache(); #endif #if defined(WOLFBOOT_DUALBOOT) && defined(WOLFBOOT_FIXED_PARTITIONS) + #ifdef WOLFBOOT_COMPRESSED_ENCRYPT + /* Same policy as wolfBoot_dualboot_candidate() on decrypted + * headers. */ + if (active < 0) { + uint32_t boot_v = compressed_slot_version(PART_BOOT); + uint32_t update_v = compressed_slot_version(PART_UPDATE); + + wolfBoot_printf("Versions: Boot %d, Update %d\n", + boot_v, update_v); + active = wolfBoot_dualboot_select(boot_v, update_v); + } + #else if (active < 0) active = wolfBoot_dualboot_candidate(); + #endif if (active == PART_BOOT) source_address = (uint32_t*)WOLFBOOT_PARTITION_BOOT_ADDRESS; else @@ -349,6 +586,9 @@ void RAMFUNCTION wolfBoot_start(void) #endif if (active < 0) { /* panic if no images available */ wolfBoot_printf("No valid image found!\n"); +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS + compressed_wipe_secrets(); /* versions were read, key expanded */ +#endif wolfBoot_panic(); break; } @@ -373,11 +613,18 @@ void RAMFUNCTION wolfBoot_start(void) if (ret != 0) { goto backup_on_failure; } + #elif defined(WOLFBOOT_COMPRESSED_ENCRYPT) + ret = compressed_stage_slot(active); + if (ret != 0) { + goto backup_on_failure; + } + load_address = (uint32_t*)(uintptr_t)WOLFBOOT_COMPRESSED_STAGE_ADDRESS; #else load_address = source_address; #endif #if !defined(WOLFBOOT_FIXED_PARTITIONS) || \ - defined(WOLFBOOT_USE_RAMBOOT) + defined(WOLFBOOT_USE_RAMBOOT) || \ + defined(WOLFBOOT_COMPRESSED_ENCRYPT) ret = wolfBoot_open_image_address(&os_image, (uint8_t*)load_address); #else ret = wolfBoot_open_image(&os_image, active); @@ -419,6 +666,15 @@ void RAMFUNCTION wolfBoot_start(void) #error missing WOLFBOOT_LOAD_ADDRESS or XIP #endif wolfBoot_printf("Successfully selected image in part: %d\n", active); +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS + /* Inside the loop, so a rejection falls back to the other slot + * without jumping back into it. */ + ret = wolfBoot_stage_compressed(&os_image, load_address); + if (ret < 0) { + goto backup_on_failure; + } + staged = (ret == 1); +#endif break; } @@ -428,6 +684,9 @@ void RAMFUNCTION wolfBoot_start(void) /* panic if authentication fails and no backup */ if (!wolfBoot_fallback_is_possible()) { wolfBoot_printf("Impossible recovery with fallback.\n"); +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS + compressed_wipe_secrets(); +#endif wolfBoot_panic(); break; } @@ -441,6 +700,9 @@ void RAMFUNCTION wolfBoot_start(void) * boot. */ wolfBoot_printf( "Both images failed verification; no valid image to boot.\n"); +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS + compressed_wipe_secrets(); +#endif wolfBoot_panic(); break; } @@ -566,7 +828,12 @@ void RAMFUNCTION wolfBoot_start(void) #endif #ifndef WOLFBOOT_USE_RAMBOOT - /* copy image to RAM */ +#ifdef WOLFBOOT_COMPRESSED_RAM_SLOTS + /* Already inflated to load_address in the loop above. */ + if (!staged) +#endif + { + /* copy image to RAM, bounded by wolfBoot_stage_compressed() above */ #if defined(EXT_FLASH) && defined(NO_XIP) wolfBoot_printf("Loading flash image from %p to RAM at %p (%d bytes)\n", os_image.fw_base, load_address, os_image.fw_size); @@ -586,6 +853,7 @@ void RAMFUNCTION wolfBoot_start(void) os_image.fw_base, load_address, os_image.fw_size); memcpy((void*)load_address, os_image.fw_base, os_image.fw_size); #endif + } #endif /* !WOLFBOOT_USE_RAMBOOT */ #ifdef WOLFBOOT_ELF @@ -838,6 +1106,11 @@ void RAMFUNCTION wolfBoot_start(void) #ifdef ENCRYPT_PKCS11 pkcs11_crypto_deinit(); #endif +#ifdef WOLFBOOT_COMPRESSED_ENCRYPT + /* The booted image must not find the key in wolfBoot's bss. */ + compress_crypt_clear(); + compress_crypt_scrub(compressed_hdr, (uint32_t)sizeof(compressed_hdr)); +#endif #ifndef TZEN if (hal_flash_protect(WOLFBOOT_ORIGIN, BOOTLOADER_PARTITION_SIZE) < 0) { diff --git a/test-app/ARM-ramboot.ld b/test-app/ARM-ramboot.ld new file mode 100644 index 0000000000..8877af0863 --- /dev/null +++ b/test-app/ARM-ramboot.ld @@ -0,0 +1,59 @@ +/* Linked for execution from RAM: no XIP region, and the vector table must + * come first because do_boot() reads SP and entry from the load address. */ +MEMORY +{ + RAM (rwx) : ORIGIN = @WOLFBOOT_LOAD_ADDRESS@, + LENGTH = @WOLFBOOT_RAMBOOT_MAX_SIZE@ +} + +SECTIONS +{ + .text : + { + _start_text = .; + KEEP(*(.isr_vector)) + *(.init) + *(.fini) + *(.text*) + *(.rodata*) + . = ALIGN(4); + _end_text = .; + } > RAM + + .edidx : + { + . = ALIGN(4); + *(.ARM.exidx*) + } > RAM + + _stored_data = .; + + /* No AT(): LMA equals VMA, so the startup copy is a no-op. */ + .data : + { + _start_data = .; + KEEP(*(.data*)) + . = ALIGN(4); + KEEP(*(.ramcode)) + . = ALIGN(4); + _end_data = .; + } > RAM + + .bss (NOLOAD) : + { + _start_bss = .; + *(.bss*) + *(COMMON) + . = ALIGN(4); + _end_bss = .; + _end = .; + } > RAM +} + +_wolfboot_partition_boot_address = @WOLFBOOT_PARTITION_BOOT_ADDRESS@; +_wolfboot_partition_size = @WOLFBOOT_PARTITION_SIZE@; +_wolfboot_partition_update_address = @WOLFBOOT_PARTITION_UPDATE_ADDRESS@; +_wolfboot_partition_swap_address = @WOLFBOOT_PARTITION_SWAP_ADDRESS@; + +PROVIDE(_start_heap = _end); +PROVIDE(_end_stack = ORIGIN(RAM) + LENGTH(RAM)); diff --git a/test-app/Makefile b/test-app/Makefile index 38fb4fc7f2..8d53e60cba 100644 --- a/test-app/Makefile +++ b/test-app/Makefile @@ -1794,6 +1794,16 @@ clean: $(Q)rm -rf $(WOLFSSL_LOCAL_OBJDIR) $(WOLFTPM_LOCAL_OBJDIR) \ $(WOLFHSM_LOCAL_OBJDIR) $(WOLFIP_LOCAL_OBJDIR) mcxw_ns +# COMPRESSED_RAM_SLOTS=1 means the application is inflated to +# WOLFBOOT_LOAD_ADDRESS and executed there, so it is linked for RAM rather +# than for its slot in flash. Assigned after the per-target templates above +# so it wins for any ARM target. +ifeq ($(ARCH),ARM) + ifeq ($(COMPRESSED_RAM_SLOTS),1) + LSCRIPT_TEMPLATE:=ARM-ramboot.ld + endif +endif + $(LSCRIPT): $(LSCRIPT_TEMPLATE) FORCE $(Q)printf "%d" $(WOLFBOOT_PARTITION_BOOT_ADDRESS) > .wolfboot-offset $(Q)printf "%d" $(WOLFBOOT_PARTITION_SIZE) > .partition-size @@ -1819,6 +1829,7 @@ endif sed -e "s/@WOLFBOOT_TEST_APP_SIZE@/`cat .app-size`/g" | \ sed -e "s/@WOLFBOOT_TEST_APP_ADDRESS@/`cat .entry-point`/g" | \ sed -e "s/@WOLFBOOT_LOAD_ADDRESS@/$(WOLFBOOT_LOAD_ADDRESS)/g" | \ + sed -e "s/@WOLFBOOT_RAMBOOT_MAX_SIZE@/$(WOLFBOOT_RAMBOOT_MAX_SIZE)/g" | \ sed -e "s/@WOLFBOOT_PARTITION_BOOT_ADDRESS@/$(WOLFBOOT_PARTITION_BOOT_ADDRESS)/g" | \ sed -e "s/@WOLFBOOT_PARTITION_SIZE@/$(WOLFBOOT_PARTITION_SIZE)/g" | \ sed -e "s/@WOLFBOOT_PARTITION_UPDATE_ADDRESS@/$(WOLFBOOT_PARTITION_UPDATE_ADDRESS)/g" | \ diff --git a/tools/config.mk b/tools/config.mk index f9eac2bb53..8d1c3afdb5 100644 --- a/tools/config.mk +++ b/tools/config.mk @@ -151,6 +151,8 @@ CONFIG_VARS:= ARCH TARGET SIGN HASH MCUXSDK MCUXPRESSO MCUXPRESSO_CPU MCUXPRESSO XMSS_PARAMS \ ELF BIG_ENDIAN \ GZIP FIT_RAMDISK \ + COMPRESSED_RAM_SLOTS COMPRESSED_ENCRYPT \ + WOLFBOOT_RAMBOOT_MAX_SIZE \ WATCHDOG WATCHDOG_TIMEOUT_MS WATCHDOG_BASE WATCHDOG_CLK_HZ \ WATCHDOG_HANDOFF \ NXP_CUSTOM_DCD NXP_CUSTOM_DCD_OBJS \ diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 097dce8eb8..29fcada3a4 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -43,6 +43,7 @@ #include #include #include +#include #include "wolfboot/version.h" @@ -188,6 +189,10 @@ static inline int fp_truncate(FILE *f, size_t len) #define HDR_IMG_DELTA_INVERSE 0x15 #define HDR_IMG_DELTA_INVERSE_SIZE 0x16 +#define HDR_IMG_COMPRESSION 0x08 +#define HDR_IMG_UNCOMPRESSED_SIZE 0x09 +#define HDR_COMP_GZIP 0x0001 + #define HDR_IMG_TYPE_AUTH_MASK 0xFF00 #define HDR_IMG_TYPE_AUTH_NONE 0xFF00 #define HDR_IMG_TYPE_WOLFBOOT 0x0000 @@ -410,6 +415,14 @@ struct cmd_options { uint32_t policy_sz; uint8_t partition_id; uint32_t custom_tlvs; + /* --compressed: the payload is an already-gzipped image. The tool does + * not compress; these are read back from the gzip container. */ + uint32_t uncompressed_sz; + uint32_t compressed_sz; + /* Starting CTR block, so each slot gets its own keystream. 0 keeps + * the historical output byte for byte. */ + uint32_t encrypt_iv_offset; + int compressed; struct cmd_tlv { uint16_t tag; uint16_t len; @@ -1501,6 +1514,108 @@ static int dts_hash_file(const char *file, int hash_algo, uint8_t *out, * against header_required_size() without the auto-grow exit(1) path. */ static uint32_t test_last_header_idx; +/* Add a block offset to a big-endian 128-bit CTR counter. Must match + * cc_set_iv() in src/compress_crypt.c or the keystreams diverge. */ +static void aes_iv_add_blocks(uint8_t *iv, uint32_t ivSz, uint32_t blocks) +{ + int i; + + for (i = (int)ivSz - 1; i >= 0; i--) { + uint32_t sum = (uint32_t)iv[i] + (blocks & 0xFF); + iv[i] = (uint8_t)(sum & 0xFF); + blocks >>= 8; + blocks += (sum >> 8); + } +} + +/* Validate an RFC 1952 container and read its ISIZE trailer; 0 or -1. The + * tool does not compress: the operator runs "gzip -9 -n" and signs that. */ +static int gzip_probe(const char *path, uint32_t *isize, uint32_t *csize) +{ + FILE *f; + uint8_t hdr[GZIP_HEADER_MIN_SIZE]; + uint8_t trailer[GZIP_TRAILER_SIZE]; + long fsize; + int ret = -1; + + f = fopen(path, "rb"); + if (f == NULL) { + fprintf(stderr, "Error: cannot open compressed image '%s': %s\n", + path, strerror(errno)); + return -1; + } + if (fseek(f, 0, SEEK_END) != 0) { + goto cleanup; + } + fsize = ftell(f); + if (fsize < (long)(GZIP_HEADER_MIN_SIZE + GZIP_TRAILER_SIZE)) { + fprintf(stderr, "Error: '%s' is too short to be a gzip stream\n", path); + goto cleanup; + } + if (fseek(f, 0, SEEK_SET) != 0) { + goto cleanup; + } + if (fread(hdr, 1, sizeof(hdr), f) != sizeof(hdr)) { + fprintf(stderr, "Error: cannot read gzip header of '%s'\n", path); + goto cleanup; + } + if ((hdr[0] != GZIP_MAGIC_ID1) || (hdr[1] != GZIP_MAGIC_ID2)) { + fprintf(stderr, "Error: '%s' is not a gzip stream " + "(use: gzip -9 -n )\n", path); + goto cleanup; + } + if (hdr[2] != GZIP_CM_DEFLATE) { + fprintf(stderr, "Error: '%s' uses gzip method %u; only DEFLATE (%u) " + "is supported\n", path, (unsigned)hdr[2], + (unsigned)GZIP_CM_DEFLATE); + goto cleanup; + } + /* The inflater rejects these, so refuse them here not at boot. */ + if ((hdr[3] & GZIP_FLG_RESERVED) != 0) { + fprintf(stderr, "Error: '%s' sets reserved gzip header flags\n", path); + goto cleanup; + } + if (fseek(f, fsize - (long)GZIP_TRAILER_SIZE, SEEK_SET) != 0) { + goto cleanup; + } + if (fread(trailer, 1, sizeof(trailer), f) != sizeof(trailer)) { + fprintf(stderr, "Error: cannot read gzip trailer of '%s'\n", path); + goto cleanup; + } + /* trailer = CRC32 (ignored here, checked at boot) then ISIZE, both LE */ + *isize = (uint32_t)trailer[4] | ((uint32_t)trailer[5] << 8) | + ((uint32_t)trailer[6] << 16) | ((uint32_t)trailer[7] << 24); + *csize = (uint32_t)fsize; + + if (*isize == 0) { + fprintf(stderr, "Error: '%s' declares a zero uncompressed size\n", + path); + goto cleanup; + } + /* ISIZE is mod 2^32; refuse rather than wrap silently. */ + if (*isize > 0x7FFFFFFFU) { + fprintf(stderr, "Error: '%s' declares an uncompressed size of %u " + "bytes\n", path, (unsigned)*isize); + goto cleanup; + } + if (*isize < *csize) { + fprintf(stderr, "Warning: '%s' is larger than its uncompressed " + "payload (%u > %u); the payload may already be compressed\n", + path, (unsigned)*csize, (unsigned)*isize); + } + if ((hdr[3] & GZIP_FLG_FNAME) != 0) { + fprintf(stderr, "Warning: '%s' embeds the original file name and " + "timestamp; use 'gzip -n' for reproducible images\n", path); + } + /* One member only: a concatenated stream would boot just the first, + * which the loader's length cross-check catches. */ + ret = 0; + +cleanup: + fclose(f); + return ret; +} + static uint32_t header_required_size(int is_diff, uint32_t cert_chain_sz, uint32_t secondary_key_sz) { @@ -1532,6 +1647,13 @@ static uint32_t header_required_size(int is_diff, uint32_t cert_chain_sz, } } + if (CMD.compressed) { + header_size_align_4(&idx); + header_size_append_tag(&idx, 2); + header_size_align_4(&idx); + header_size_append_tag(&idx, 4); + } + for (i = 0; i < CMD.custom_tlvs; i++) { header_size_align_8(&idx); header_size_append_tag(&idx, CMD.custom_tlv[i].len); @@ -1620,7 +1742,7 @@ 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) || - (CMD.dts_file != NULL)) { + (CMD.dts_file != NULL) || CMD.compressed) { uint32_t hdr_cert_chain_sz = 0; uint32_t required_space; @@ -1778,6 +1900,16 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, } } + if (CMD.compressed) { + /* Append pad bytes, so fields are 4-byte aligned */ + ALIGN_4(header_idx); + header_append_tag_u16(header, &header_idx, HDR_IMG_COMPRESSION, + HDR_COMP_GZIP); + ALIGN_4(header_idx); + header_append_tag_u32(header, &header_idx, HDR_IMG_UNCOMPRESSED_SIZE, + CMD.uncompressed_sz); + } + /* Add custom TLVs */ if (CMD.custom_tlvs > 0) { uint32_t i; @@ -2529,7 +2661,7 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, goto failure; #endif wc_Chacha_SetKey(&cha, key, sizeof(key)); - wc_Chacha_SetIV(&cha, iv, 0); + wc_Chacha_SetIV(&cha, iv, CMD.encrypt_iv_offset); for (pos = 0; pos < fsize; pos += encBlockSz) { int fread_retval; fread_retval = (int)fread(buf, 1, encBlockSz, f); @@ -2542,6 +2674,9 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, } else if ((CMD.encrypt == ENC_AES128) || (CMD.encrypt == ENC_AES256)) { Aes aes_e; wc_AesInit(&aes_e, NULL, 0); + if (CMD.encrypt_iv_offset != 0) { + aes_iv_add_blocks(iv, ivSz, CMD.encrypt_iv_offset); + } wc_AesSetKeyDirect(&aes_e, key, keySz, iv, AES_ENCRYPTION); for (pos = 0; pos < fsize; pos += encBlockSz) { int fread_retval; @@ -3642,6 +3777,14 @@ int main(int argc, char** argv) } CMD.delta = 1; CMD.delta_base_file = argv[++i]; + } else if (strcmp(argv[i], "--compressed") == 0) { + CMD.compressed = 1; + } else if (strcmp(argv[i], "--encrypt-iv-offset") == 0) { + if (argc <= (i + 1)) { + fprintf(stderr, "Missing --encrypt-iv-offset argument\n"); + exit(16); + } + CMD.encrypt_iv_offset = (uint32_t)strtoul(argv[++i], NULL, 0); } else if (strcmp(argv[i], "--no-base-sha") == 0) { CMD.no_base_sha = 1; } @@ -3982,22 +4125,60 @@ int main(int argc, char** argv) exit(16); } + if (CMD.compressed && CMD.delta) { + fprintf(stderr, "Error: --compressed excludes --delta\n"); + exit(16); + } + if (CMD.compressed && CMD.self_update) { + fprintf(stderr, "Error: --compressed cannot be used for a wolfBoot " + "self-update: wolfBoot cannot inflate itself\n"); + exit(16); + } + if (CMD.compressed && !CMD.header_only) { + /* Fail before any key material is touched. */ + if (gzip_probe(CMD.image_file, &CMD.uncompressed_sz, + &CMD.compressed_sz) != 0) { + exit(16); + } + } + memset(buf, 0, sizeof(buf)); strncpy((char*)buf, CMD.image_file, sizeof(buf)-1); - tmpstr = strrchr((char*)buf, '.'); - if (tmpstr) { - *tmpstr = '\0'; /* null terminate at last "." */ + if (CMD.compressed) { + /* Strip .gz so the trim below does not stem "image.bin.gz" to + * "image.bin". */ + size_t blen = strlen((char*)buf); + if ((blen > 3) && (strcmp((char*)buf + blen - 3, ".gz") == 0)) { + buf[blen - 3] = '\0'; + } + } + /* Basename only: a dot in a parent directory would otherwise truncate + * the path, which stripping .gz above makes reachable. */ + { + char *base = strrchr((char*)buf, '/'); +#ifdef _WIN32 + char *base_bs = strrchr((char*)buf, '\\'); + if ((base_bs != NULL) && ((base == NULL) || (base_bs > base))) { + base = base_bs; + } +#endif + tmpstr = strrchr((base != NULL) ? (base + 1) : (char*)buf, '.'); + if (tmpstr) { + *tmpstr = '\0'; /* null terminate at last "." */ + } } { const char* artifact = - CMD.header_only ? "header" : (CMD.sha_only ? "digest" : "signed"); + CMD.header_only ? "header" : (CMD.sha_only ? "digest" : + (CMD.compressed ? "signed_compressed" : "signed")); snprintf(CMD.output_image_file, sizeof(CMD.output_image_file) - 1, "%s_v%s_%s.bin", (char*)buf, CMD.fw_version, artifact); } snprintf(CMD.output_encrypted_image_file, sizeof(CMD.output_encrypted_image_file), - "%s_v%s_signed_and_encrypted.bin", + CMD.compressed ? "%s_v%s_signed_compressed_and_encrypted.bin" : + "%s_v%s_signed_and_encrypted.bin", (char*)buf, CMD.fw_version); printf("Update type: %s\n", @@ -4016,6 +4197,14 @@ int main(int argc, char** argv) break; } printf("Input image: %s\n", CMD.image_file); + if (CMD.compressed) { + printf("Compression: gzip (%u -> %u bytes)\n", + (unsigned)CMD.uncompressed_sz, (unsigned)CMD.compressed_sz); + } + if ((CMD.encrypt != ENC_OFF) && (CMD.encrypt_iv_offset != 0)) { + printf("Encryption IV offset: %u blocks\n", + (unsigned)CMD.encrypt_iv_offset); + } printf("Selected cipher: %s\n", sign_str); printf("Selected hash : %s\n", hash_str); if (CMD.sign != NO_SIGN) { diff --git a/tools/scripts/sim-compressed-ram-enc.sh b/tools/scripts/sim-compressed-ram-enc.sh new file mode 100755 index 0000000000..90eb2fc400 --- /dev/null +++ b/tools/scripts/sim-compressed-ram-enc.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# Simulated test for compressed A/B images that are also encrypted at rest. +# +# Run after: +# cp config/examples/sim-compressed-ram-enc.config .config +# make keysclean && make && make test-sim-compressed-ram-enc-with-update + +set -uo pipefail + +if [ ! -f ".config" ]; then + echo "Missing .config. Run make config first." >&2 + exit 1 +fi + +if ! grep -Eq '^(COMPRESSED_ENCRYPT(\?|)=1)' .config; then + echo "COMPRESSED_ENCRYPT=1 is required for this simulation." >&2 + exit 1 +fi + +if [ ! -x "./wolfboot.elf" ]; then + echo "wolfboot.elf not found. Build the simulator first." >&2 + exit 1 +fi + +cfg() { + grep -E "^$1\??=" .config | tail -1 | cut -d= -f2 +} + +PART_SIZE=$(( $(cfg WOLFBOOT_PARTITION_SIZE) )) +BOOT_OFF=$(( $(cfg WOLFBOOT_PARTITION_BOOT_ADDRESS) )) +UPDATE_OFF=$(( $(cfg WOLFBOOT_PARTITION_UPDATE_ADDRESS) )) +LOAD_ADDR=$(cfg WOLFBOOT_LOAD_ADDRESS) +STAGE_ADDR=$(cfg WOLFBOOT_COMPRESSED_STAGE_ADDRESS) +V1=test-app/image_v1_signed_compressed_and_encrypted.bin +V2=test-app/image_v2_signed_compressed_and_encrypted.bin + +fail() { echo "FAIL: $*" >&2; exit 1; } +ok() { echo " ok: $*"; } + +reset_flash() { + make test-sim-compressed-ram-enc-with-update >/dev/null 2>&1 || \ + fail "could not rebuild the simulated flash" +} + +# --------------------------------------------------------------------------- +echo "1. slots are ciphertext at rest" +# --------------------------------------------------------------------------- +reset_flash +# A plaintext slot would start with the "WOLF" manifest magic. Neither slot +# may, or the image is not actually encrypted on flash. +for off in "$BOOT_OFF" "$UPDATE_OFF"; do + magic=$(dd if=internal_flash.dd bs=1 skip="$off" count=4 2>/dev/null) + if [ "$magic" = "WOLF" ]; then + fail "slot at offset $off is stored in the clear" + fi +done +ok "neither slot exposes the manifest magic on flash" + +# --------------------------------------------------------------------------- +echo "2. the two slots do not share a keystream" +# --------------------------------------------------------------------------- +# Both slots hold the same application compressed with the same key, so if +# they shared a keystream the XOR of the two would collapse. This is what +# the per-slot IV offset exists to prevent. +python3 -I -c ' +import sys +a=open(sys.argv[1],"rb").read() +b=open(sys.argv[2],"rb").read() +n=min(len(a),len(b)) +if n == 0: + print("empty artifact"); sys.exit(1) +if a[:n] == b[:n]: + print("slot ciphertexts are identical"); sys.exit(1) +x = bytes(a[i]^b[i] for i in range(n)) +if x == bytes(n): + print("slot keystreams are shared"); sys.exit(1) +same = sum(1 for i in range(n) if a[i]==b[i]) +# Distinct keystreams agree only by chance, about 1 byte in 256. +if same > n // 64: + print("ciphertexts agree on %d/%d bytes, more than chance" % (same,n)) + sys.exit(1) +' "$V1" "$V2" || fail "slot keystream separation check failed" +ok "slot ciphertexts are independent" + +# --------------------------------------------------------------------------- +echo "3. an encrypted compressed image boots" +# --------------------------------------------------------------------------- +out=$(timeout 60 ./wolfboot.elf get_version 2>&1) +grep -q "Successfully selected image in part: 1" <<<"$out" || \ + fail "did not select the UPDATE slot:\n$out" +# The inflate source is the staging base plus the manifest header, so +# compare numerically against the staging window rather than a literal. +src=$(grep -oE "Inflating [0-9]+ bytes from 0x[0-9a-fA-F]+" <<<"$out" | \ + grep -oE '0x[0-9a-fA-F]+$' | tail -1) +[ -n "$src" ] || fail "no inflate line in output:\n$out" +if [ $((src)) -lt $((STAGE_ADDR)) ] || \ + [ $((src)) -ge $((STAGE_ADDR + PART_SIZE)) ]; then + fail "inflated from $src, outside the staging region at $STAGE_ADDR" +fi +grep -q "Booting at $LOAD_ADDR" <<<"$out" || fail "did not boot from RAM:\n$out" +ok "decrypted to staging, verified, inflated and booted" + +# --------------------------------------------------------------------------- +echo "4. an update is confirmed on the slot that actually booted" +# --------------------------------------------------------------------------- +reset_flash +timeout 60 ./wolfboot.elf update_trigger >/dev/null 2>&1 +st=$(timeout 60 ./wolfboot.elf get_state 2>/dev/null | tail -1) +[ "$st" = "10" ] || fail "expected UPDATE in IMG_STATE_TESTING (10), got '$st'" +st=$(timeout 60 ./wolfboot.elf success get_state 2>/dev/null | tail -1) +[ "$st" = "00" ] || fail "expected UPDATE in IMG_STATE_SUCCESS (00), got '$st'" +ok "confirmation landed on the update slot" + +# --------------------------------------------------------------------------- +echo "5. a slot encrypted for the wrong keystream is not booted" +# --------------------------------------------------------------------------- +# Re-sign the update without the slot offset. Its header then decrypts to +# garbage, so it must read as absent and the device must stay on slot A +# rather than attempting to verify or inflate it. +reset_flash +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT +cp test-app/image.elf.gz "$tmpdir/wrong.gz" +SIGN_FLAGS="--$(echo "$(cfg SIGN)" | tr 'A-Z' 'a-z') --$(echo "$(cfg HASH)" | tr 'A-Z' 'a-z')" +CIPHER=--chacha +grep -Eq '^ENCRYPT_WITH_AES128(\?|)=1' .config && CIPHER=--aes128 +grep -Eq '^ENCRYPT_WITH_AES256(\?|)=1' .config && CIPHER=--aes256 +./tools/keytools/sign $SIGN_FLAGS --compressed $CIPHER \ + --encrypt "${COMPRESSED_ENC_KEY:-/tmp/compressed_enc_key.der}" \ + "$tmpdir/wrong.gz" wolfboot_signing_private_key.der 2 >/dev/null 2>&1 || \ + fail "could not sign the mismatched image" +./tools/bin-assemble/bin-assemble internal_flash.dd \ + 0 wolfboot.bin \ + "$BOOT_OFF" "$V1" \ + "$UPDATE_OFF" "$tmpdir/wrong_v2_signed_compressed_and_encrypted.bin" \ + $(cfg WOLFBOOT_PARTITION_SWAP_ADDRESS) erased_sec.dd >/dev/null 2>&1 || \ + fail "could not assemble the mismatched image" + +out=$(timeout 60 ./wolfboot.elf get_version 2>&1) +grep -q "Versions: Boot 1, Update 0" <<<"$out" || \ + fail "mismatched slot was not reported as absent:\n$out" +grep -q "Successfully selected image in part: 0" <<<"$out" || \ + fail "did not stay on the BOOT slot:\n$out" +ok "mismatched slot reads as absent and slot A boots" + +echo +echo "Encrypted compressed image simulation successful." +exit 0 diff --git a/tools/scripts/sim-compressed-ram.sh b/tools/scripts/sim-compressed-ram.sh new file mode 100755 index 0000000000..c179a20c5d --- /dev/null +++ b/tools/scripts/sim-compressed-ram.sh @@ -0,0 +1,155 @@ +#!/bin/bash +# Simulated end-to-end test for compressed A/B images executed from RAM. +# +# Run after: +# cp config/examples/sim-compressed-ram.config .config +# make keysclean && make && make test-sim-compressed-ram-with-update + +set -uo pipefail + +if [ ! -f ".config" ]; then + echo "Missing .config. Run make config first." >&2 + exit 1 +fi + +if ! grep -Eq '^(COMPRESSED_RAM_SLOTS(\?|)=1)' .config; then + echo "COMPRESSED_RAM_SLOTS=1 is required for this simulation." >&2 + exit 1 +fi + +if [ ! -x "./wolfboot.elf" ]; then + echo "wolfboot.elf not found. Build the simulator first." >&2 + exit 1 +fi + +cfg() { + grep -E "^$1\??=" .config | tail -1 | cut -d= -f2 +} + +PART_SIZE=$(( $(cfg WOLFBOOT_PARTITION_SIZE) )) +UPDATE_OFF=$(( $(cfg WOLFBOOT_PARTITION_UPDATE_ADDRESS) )) +LOAD_ADDR=$(cfg WOLFBOOT_LOAD_ADDRESS) +RAMBOOT_MAX=$(( $(cfg WOLFBOOT_RAMBOOT_MAX_SIZE) )) +V1=test-app/image_v1_signed_compressed.bin +V2=test-app/image_v2_signed_compressed.bin +# The sign tool cannot auto-detect the algorithm from a raw .der key, so +# pass it explicitly the way tools/test.mk does via SIGN_OPTIONS. +SIGN_FLAGS="--$(echo "$(cfg SIGN)" | tr 'A-Z' 'a-z') --$(echo "$(cfg HASH)" | tr 'A-Z' 'a-z')" + +fail() { echo "FAIL: $*" >&2; exit 1; } +ok() { echo " ok: $*"; } + +reset_flash() { + make test-sim-compressed-ram-with-update >/dev/null 2>&1 || \ + fail "could not rebuild the simulated flash" +} + +# --------------------------------------------------------------------------- +echo "1. compressed slots are smaller than an uncompressed A/B pair" +# --------------------------------------------------------------------------- +for f in "$V1" "$V2"; do + [ -f "$f" ] || fail "$f not found; run make test-sim-compressed-ram-with-update" + sz=$(stat -c%s "$f") + # The whole point of the feature: each slot must hold the image in + # well under half a partition, or compression bought nothing. + if [ "$sz" -ge $((PART_SIZE / 2)) ]; then + fail "$f is $sz bytes, not below half a partition ($((PART_SIZE / 2)))" + fi + ok "$(basename "$f") is $sz bytes (partition is $PART_SIZE)" +done + +# --------------------------------------------------------------------------- +echo "2. fresh boot selects the highest version and inflates it" +# --------------------------------------------------------------------------- +reset_flash +out=$(timeout 60 ./wolfboot.elf get_version 2>&1) +grep -q "Successfully selected image in part: 1" <<<"$out" || \ + fail "did not select the UPDATE slot:\n$out" +grep -qE "Inflating [0-9]+ bytes from .* to RAM at $LOAD_ADDR" <<<"$out" || \ + fail "did not inflate to the load address:\n$out" +grep -q "Booting at $LOAD_ADDR" <<<"$out" || fail "did not boot from RAM:\n$out" +ok "selected UPDATE, inflated to $LOAD_ADDR and booted from RAM" + +# --------------------------------------------------------------------------- +echo "3. an update is confirmed on the slot that actually booted" +# --------------------------------------------------------------------------- +# Regression guard for wolfBoot_success(): without the active-slot fix it +# confirms PART_BOOT, leaving the booted UPDATE image in IMG_STATE_TESTING +# for ever, and wolfBoot_dualboot_candidate() then discards it. +reset_flash +timeout 60 ./wolfboot.elf update_trigger >/dev/null 2>&1 +st=$(timeout 60 ./wolfboot.elf get_state 2>/dev/null | tail -1) +[ "$st" = "10" ] || fail "expected UPDATE in IMG_STATE_TESTING (10), got '$st'" +ok "booted update slot is marked TESTING" + +st=$(timeout 60 ./wolfboot.elf success get_state 2>/dev/null | tail -1) +[ "$st" = "00" ] || fail "expected UPDATE in IMG_STATE_SUCCESS (00), got '$st'" +ok "wolfBoot_success() confirmed the update slot" + +st=$(timeout 60 ./wolfboot.elf get_state 2>/dev/null | tail -1) +[ "$st" = "00" ] || fail "confirmation did not persist, got '$st'" +out=$(timeout 60 ./wolfboot.elf get_version 2>&1) +grep -q "Successfully selected image in part: 1" <<<"$out" || \ + fail "stopped booting the confirmed update slot:\n$out" +ok "confirmation persists across reboot" + +# --------------------------------------------------------------------------- +echo "4. a corrupt compressed payload falls back to the other slot" +# --------------------------------------------------------------------------- +# The signature covers the compressed bytes, so corruption must be caught +# by verification and never reach the inflater. +reset_flash +python3 -I -c ' +import sys +off = int(sys.argv[1]) + 256 + 0x1000 +with open("internal_flash.dd", "r+b") as f: + f.seek(off); b = f.read(1) + f.seek(off); f.write(bytes([b[0] ^ 0xFF])) +' "$UPDATE_OFF" || fail "could not corrupt the update slot" + +out=$(timeout 60 ./wolfboot.elf get_version 2>&1) +grep -qE "Checking integrity\.\.\.FAILED|Verifying signature\.\.\.FAILED" <<<"$out" || \ + fail "corruption was not caught by verification:\n$out" +grep -q "Successfully selected image in part: 0" <<<"$out" || \ + fail "did not fall back to the BOOT slot:\n$out" +[ "$(tail -1 <<<"$out")" = "1" ] || fail "fallback did not boot v1:\n$out" +ok "verification rejected the corrupt slot and v1 booted" + +# --------------------------------------------------------------------------- +echo "5. a signed image too large to inflate is rejected" +# --------------------------------------------------------------------------- +# The uncompressed length is signature-covered, but the destination capacity +# is a property of the build, so an oversized image must be refused rather +# than allowed to run off the end of the load region. +reset_flash +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT +cp test-app/image.elf "$tmpdir/big.elf" +# Pad with zeros: compresses to almost nothing, so the slot still fits +# while the uncompressed size exceeds WOLFBOOT_RAMBOOT_MAX_SIZE. +dd if=/dev/zero bs=1024 count=$((RAMBOOT_MAX / 1024)) >> "$tmpdir/big.elf" 2>/dev/null +gzip -9 -n -c "$tmpdir/big.elf" > "$tmpdir/big.elf.gz" +PRIVATE_KEY=$(ls wolfboot_signing_private_key.der 2>/dev/null || echo "") +[ -n "$PRIVATE_KEY" ] || fail "signing key not found" +./tools/keytools/sign $SIGN_FLAGS --compressed "$tmpdir/big.elf.gz" \ + "$PRIVATE_KEY" 3 >/dev/null 2>&1 || fail "could not sign the oversized image" +./tools/bin-assemble/bin-assemble internal_flash.dd \ + 0 wolfboot.bin \ + $(cfg WOLFBOOT_PARTITION_BOOT_ADDRESS) "$V1" \ + "$UPDATE_OFF" "$tmpdir/big_v3_signed_compressed.bin" \ + $(cfg WOLFBOOT_PARTITION_SWAP_ADDRESS) erased_sec.dd >/dev/null 2>&1 || \ + fail "could not assemble the oversized image" + +out=$(timeout 60 ./wolfboot.elf get_version 2>&1) +grep -q "exceeds RAM load capacity" <<<"$out" || \ + fail "oversized image was not rejected:\n$out" +# The size is a property of the image, not of the build, so the other slot +# must still boot rather than the device halting. +grep -q "Successfully selected image in part: 0" <<<"$out" || \ + fail "did not fall back to the BOOT slot:\n$out" +[ "$(tail -1 <<<"$out")" = "1" ] || fail "fallback did not boot v1:\n$out" +ok "oversized image rejected and v1 booted instead" + +echo +echo "Compressed image simulation successful." +exit 0 diff --git a/tools/test.mk b/tools/test.mk index 8f39c6e66c..34d5413e2f 100644 --- a/tools/test.mk +++ b/tools/test.mk @@ -199,6 +199,73 @@ test-sim-internal-flash-with-update: wolfboot.bin test-app/image.elf FORCE $$(($(WOLFBOOT_PARTITION_UPDATE_ADDRESS)-$(ARCH_FLASH_OFFSET))) test-app/image_v$(TEST_UPDATE_VERSION)_signed.bin \ $$(($(WOLFBOOT_PARTITION_SWAP_ADDRESS)-$(ARCH_FLASH_OFFSET))) erased_sec.dd +test-sim-compressed-ram-with-update: wolfboot.bin test-app/image.elf FORCE + # Two compressed slots: v1 in BOOT, v2 in UPDATE. wolfBoot picks the + # highest valid version, inflates it to WOLFBOOT_LOAD_ADDRESS and boots + # there, so neither slot is ever installed into the other. + $(Q)cp test-app/image.elf test-app/image.bak.elf + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=16 oflag=append conv=notrunc + $(Q)gzip -9 -n -c test-app/image.elf > test-app/image.elf.gz + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) --compressed \ + test-app/image.elf.gz $(PRIVATE_KEY) 1 + $(Q)cp test-app/image.bak.elf test-app/image.elf + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=16 oflag=append conv=notrunc + $(Q)gzip -9 -n -c test-app/image.elf > test-app/image.elf.gz + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) --compressed \ + test-app/image.elf.gz $(PRIVATE_KEY) $(TEST_UPDATE_VERSION) + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_SECTOR_SIZE))) count=1 2>/dev/null $(INVERSION) > erased_sec.dd + $(Q)$(BINASSEMBLE) internal_flash.dd \ + 0 wolfboot.bin \ + $$(($(WOLFBOOT_PARTITION_BOOT_ADDRESS) - $(ARCH_FLASH_OFFSET))) test-app/image_v1_signed_compressed.bin \ + $$(($(WOLFBOOT_PARTITION_UPDATE_ADDRESS)-$(ARCH_FLASH_OFFSET))) test-app/image_v$(TEST_UPDATE_VERSION)_signed_compressed.bin \ + $$(($(WOLFBOOT_PARTITION_SWAP_ADDRESS)-$(ARCH_FLASH_OFFSET))) erased_sec.dd + +# Compressed A/B slots, encrypted at rest. Each slot's ciphertext must use +# its own keystream region, so v2 (which lands in the UPDATE slot) is signed +# with the matching --encrypt-iv-offset. The stride is one partition's worth +# of cipher blocks, which is what src/compress_crypt.c derives by default. +# Cipher geometry for the slot keystream stride and the secret file layout: +# ChaCha20 is a 64-byte block with a 32-byte key and 12-byte nonce, AES-CTR +# a 16-byte block with a 16- or 32-byte key and a 16-byte IV. +COMPRESSED_ENC_KEY?=/tmp/compressed_enc_key.der +ifeq ($(ENCRYPT_WITH_AES128),1) + COMPRESSED_ENC_BLOCK?=16 + COMPRESSED_ENC_SECRET?=0123456789abcdef0123456789abcdef + COMPRESSED_ENC_CIPHER?=--aes128 +else +ifeq ($(ENCRYPT_WITH_AES256),1) + COMPRESSED_ENC_BLOCK?=16 + COMPRESSED_ENC_SECRET?=0123456789abcdef0123456789abcdef0123456789abcdef + COMPRESSED_ENC_CIPHER?=--aes256 +else + COMPRESSED_ENC_BLOCK?=64 + COMPRESSED_ENC_SECRET?=0123456789abcdef0123456789abcdef0123456789ab + COMPRESSED_ENC_CIPHER?=--chacha +endif +endif + +test-sim-compressed-ram-enc-with-update: wolfboot.bin test-app/image.elf FORCE + $(Q)printf "$(COMPRESSED_ENC_SECRET)" > $(COMPRESSED_ENC_KEY) + $(Q)cp test-app/image.elf test-app/image.bak.elf + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=16 oflag=append conv=notrunc + $(Q)gzip -9 -n -c test-app/image.elf > test-app/image.elf.gz + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) --compressed \ + $(COMPRESSED_ENC_CIPHER) --encrypt $(COMPRESSED_ENC_KEY) \ + test-app/image.elf.gz $(PRIVATE_KEY) 1 + $(Q)cp test-app/image.bak.elf test-app/image.elf + $(Q)dd if=/dev/urandom of=test-app/image.elf bs=1k count=16 oflag=append conv=notrunc + $(Q)gzip -9 -n -c test-app/image.elf > test-app/image.elf.gz + $(Q)$(SIGN_ENV) $(SIGN_TOOL) $(SIGN_OPTIONS) --compressed \ + $(COMPRESSED_ENC_CIPHER) --encrypt $(COMPRESSED_ENC_KEY) \ + --encrypt-iv-offset $$(($(WOLFBOOT_PARTITION_SIZE) / $(COMPRESSED_ENC_BLOCK))) \ + test-app/image.elf.gz $(PRIVATE_KEY) $(TEST_UPDATE_VERSION) + $(Q)dd if=/dev/zero bs=$$(($(WOLFBOOT_SECTOR_SIZE))) count=1 2>/dev/null $(INVERSION) > erased_sec.dd + $(Q)$(BINASSEMBLE) internal_flash.dd \ + 0 wolfboot.bin \ + $$(($(WOLFBOOT_PARTITION_BOOT_ADDRESS) - $(ARCH_FLASH_OFFSET))) test-app/image_v1_signed_compressed_and_encrypted.bin \ + $$(($(WOLFBOOT_PARTITION_UPDATE_ADDRESS)-$(ARCH_FLASH_OFFSET))) test-app/image_v$(TEST_UPDATE_VERSION)_signed_compressed_and_encrypted.bin \ + $$(($(WOLFBOOT_PARTITION_SWAP_ADDRESS)-$(ARCH_FLASH_OFFSET))) erased_sec.dd + test-sim-internal-flash-with-delta-update: # This target calls test-sim-internal-flash-with-update with the delta option # The delta option causes the sign tool to produce image_v2_signed_diff.bin instead of image_v2_signed.bin diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index f06f10ece0..284764a4ae 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -68,7 +68,7 @@ TESTS:=unit-parser unit-parser-large-header unit-fdt unit-extflash unit-string \ unit-update-flash-self-update \ unit-update-flash-self-update-ecc \ unit-nsc-update \ - unit-update-flash-enc unit-update-flash-enc-full unit-update-flash-enc-intswap unit-update-ram unit-update-ram-fit unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-custom-trailer-nopart unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ + unit-update-flash-enc unit-update-flash-enc-full unit-update-flash-enc-intswap unit-update-ram unit-update-ram-fit unit-update-ram-uboot unit-update-ram-enc unit-update-ram-enc-nopart unit-update-ram-nofixed unit-update-ram-nofixed-noramboot unit-update-ram-noramboot unit-update-ram-custom-trailer unit-compressed-tlv unit-custom-trailer-nopart unit-update-flash-hwswap unit-pkcs11_store unit-psa_store unit-wolfhsm_flash_hal unit-disk \ unit-update-disk unit-update-disk-confirm unit-update-disk-golden unit-update-disk-fsp unit-update-disk-oob unit-update-disk-fit unit-update-disk-fit-ramdisk 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-image-dts \ unit-image-dts-sha384 unit-image-dts-sha3-384 unit-store-sbrk \ @@ -415,6 +415,15 @@ unit-update-ram-fit:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUT -DWOLFBOOT_DTS_BOOT_ADDRESS=0xCF000000 \ -DWOLFBOOT_DTS_UPDATE_ADDRESS=0xCF100000 \ -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE +# Compressed-image manifest reader and the shared slot-selection policy. +# No NO_XIP here: that would turn on WOLFBOOT_USE_RAMBOOT, which compressed +# images reject because the staging and inflate regions would coincide. +unit-compressed-tlv:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN \ + -DUNIT_TEST_AUTH -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED \ + -DWOLFBOOT_DUALBOOT -DWOLFBOOT_COMPRESSED_RAM_SLOTS -DWOLFBOOT_GZIP \ + -DWOLFBOOT_RAMBOOT_MAX_SIZE=WOLFBOOT_PARTITION_SIZE \ + -DWOLFBOOT_ORIGIN=MOCK_ADDRESS_BOOT \ + -DBOOTLOADER_PARTITION_SIZE=WOLFBOOT_PARTITION_SIZE unit-update-ram:CFLAGS+=-DMOCK_PARTITIONS -DWOLFBOOT_NO_SIGN -DUNIT_TEST_AUTH \ -DWOLFBOOT_HASH_SHA256 -DPRINTF_ENABLED -DEXT_FLASH -DPART_UPDATE_EXT \ -DPART_SWAP_EXT -DPART_BOOT_EXT -DWOLFBOOT_DUALBOOT -DNO_XIP \ @@ -1208,6 +1217,10 @@ unit-update-flash-enc-intswap: ../../include/target.h unit-update-flash.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/chacha.c \ $(CFLAGS) $(LDFLAGS) +unit-compressed-tlv: ../../include/target.h unit-compressed-tlv.c + gcc -o $@ unit-compressed-tlv.c ../../src/image.c ../../src/gzip.c \ + $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) + unit-update-ram: ../../include/target.h unit-update-ram.c gcc -o $@ unit-update-ram.c ../../src/image.c $(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha256.c $(CFLAGS) $(LDFLAGS) diff --git a/tools/unit-tests/unit-compressed-tlv.c b/tools/unit-tests/unit-compressed-tlv.c new file mode 100644 index 0000000000..9dbced0227 --- /dev/null +++ b/tools/unit-tests/unit-compressed-tlv.c @@ -0,0 +1,313 @@ +/* unit-compressed-tlv.c + * + * Unit tests for the compressed-image manifest reader in update_ram.c and + * for the slot selection policy shared with the encrypted loader path. + * + * 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 + */ +#ifndef WOLFBOOT_HASH_SHA256 + #define WOLFBOOT_HASH_SHA256 +#endif +#define IMAGE_HEADER_SIZE 256 +#define MOCK_ADDRESS_UPDATE 0xCC000000 +#define MOCK_ADDRESS_BOOT 0xCD000000 +#define MOCK_ADDRESS_SWAP 0xCE000000 + +#define NO_FORK 1 + +#include "target.h" + +static __thread unsigned char wolfboot_ram[2 * WOLFBOOT_PARTITION_SIZE + + IMAGE_HEADER_SIZE]; +#define WOLFBOOT_LOAD_ADDRESS (((uintptr_t)wolfboot_ram + IMAGE_HEADER_SIZE)) + +#include +#include +#include +#include +#include +#include +#include "user_settings.h" +#include "wolfboot/wolfboot.h" +#include "libwolfboot.c" +#include "update_ram.c" +#include +#include "unit-mock-flash.c" + +const char *argv0; + +Suite *wolfboot_suite(void); + +int wolfBoot_staged_ok = 0; + +void do_boot(const uint32_t *address) +{ + (void)address; + wolfBoot_staged_ok++; +} + +int hal_flash_protect(haladdr_t address, int len) +{ + (void)address; + (void)len; + return 0; +} + +/* Manifest construction helpers */ + +static uint8_t test_hdr[IMAGE_HEADER_SIZE]; +static struct wolfBoot_image test_img; + +static uint32_t hdr_idx; + +static void hdr_reset(void) +{ + memset(test_hdr, 0xFF, sizeof(test_hdr)); + memcpy(test_hdr, "WOLF", 4); + /* image size field, unused by the reader under test */ + memset(test_hdr + 4, 0, 4); + hdr_idx = 8; + + memset(&test_img, 0, sizeof(test_img)); + test_img.hdr = test_hdr; + test_img.not_ext = 1; + test_img.hdr_ok = 1; + test_img.part = PART_BOOT; +} + +static void hdr_add(uint16_t tag, uint16_t len, const uint8_t *val) +{ + test_hdr[hdr_idx++] = (uint8_t)(tag & 0xFF); + test_hdr[hdr_idx++] = (uint8_t)(tag >> 8); + test_hdr[hdr_idx++] = (uint8_t)(len & 0xFF); + test_hdr[hdr_idx++] = (uint8_t)(len >> 8); + if ((val != NULL) && (len > 0)) { + memcpy(test_hdr + hdr_idx, val, len); + } + hdr_idx += len; +} + +static void hdr_add_u16(uint16_t tag, uint16_t v) +{ + uint8_t b[2]; + b[0] = (uint8_t)(v & 0xFF); + b[1] = (uint8_t)(v >> 8); + hdr_add(tag, 2, b); +} + +static void hdr_add_u32(uint16_t tag, uint32_t v) +{ + uint8_t b[4]; + b[0] = (uint8_t)(v & 0xFF); + b[1] = (uint8_t)((v >> 8) & 0xFF); + b[2] = (uint8_t)((v >> 16) & 0xFF); + b[3] = (uint8_t)((v >> 24) & 0xFF); + hdr_add(tag, 4, b); +} + +/* wolfBoot_image_compression() */ + +/* No compression TLV: an uncompressed image, which must be reported as + * "none" (0) rather than as an error, so the plain path still runs. */ +START_TEST(test_compression_absent) +{ + uint32_t sz = 0xA5A5A5A5; + + hdr_reset(); + hdr_add_u32(HDR_VERSION, 1); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz), 0); +} +END_TEST + +/* A well-formed gzip manifest. Also pins the little-endian decode of the + * uncompressed length, which is what bounds the inflate. */ +START_TEST(test_compression_gzip_ok) +{ + uint32_t sz = 0; + + hdr_reset(); + hdr_add_u16(HDR_IMG_COMPRESSION, HDR_COMP_GZIP); + hdr_add_u32(HDR_IMG_UNCOMPRESSED_SIZE, 0x00029A28); /* 170536 */ + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz), 1); + ck_assert_uint_eq(sz, 0x00029A28); +} +END_TEST + +/* Unknown algorithm must be -1, never a fall-through to booting raw: + * that would be a jump into DEFLATE data. */ +START_TEST(test_compression_unknown_algo) +{ + uint32_t sz = 0; + + hdr_reset(); + hdr_add_u16(HDR_IMG_COMPRESSION, 0x00FE); + hdr_add_u32(HDR_IMG_UNCOMPRESSED_SIZE, 1024); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz), -1); +} +END_TEST + +/* Wrong length on the algorithm tag. */ +START_TEST(test_compression_bad_algo_len) +{ + uint32_t sz = 0; + uint8_t one = 1; + + hdr_reset(); + hdr_add(HDR_IMG_COMPRESSION, 1, &one); + hdr_add_u32(HDR_IMG_UNCOMPRESSED_SIZE, 1024); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz), -1); +} +END_TEST + +/* Compression declared but no uncompressed size: the loader would have + * nothing to bound the inflate with. */ +START_TEST(test_compression_missing_size) +{ + uint32_t sz = 0; + + hdr_reset(); + hdr_add_u16(HDR_IMG_COMPRESSION, HDR_COMP_GZIP); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz), -1); +} +END_TEST + +/* Size tag present but the wrong width. */ +START_TEST(test_compression_bad_size_len) +{ + uint32_t sz = 0; + uint8_t two[2] = { 0x00, 0x04 }; + + hdr_reset(); + hdr_add_u16(HDR_IMG_COMPRESSION, HDR_COMP_GZIP); + hdr_add(HDR_IMG_UNCOMPRESSED_SIZE, 2, two); + ck_assert_int_eq(wolfBoot_image_compression(&test_img, &sz), -1); +} +END_TEST + +/* ramboot_region_overlap(), the guard the simulator cannot reach */ + +/* The output buffer is the LZ77 window, so overlap lets inflate eat its own + * input. Unreachable from the sim's layout, so only a unit test covers it. */ +START_TEST(test_overlap_detection) +{ + uintptr_t dst_lo = 0x20004000; + uintptr_t dst_hi = dst_lo + 0x1C000; + + /* source entirely below the destination */ + ck_assert_int_eq(ramboot_region_overlap(dst_lo, dst_hi, + 0x08020000, 0x08040000), 0); + /* source entirely above */ + ck_assert_int_eq(ramboot_region_overlap(dst_lo, dst_hi, + 0x20030000, 0x20040000), 0); + /* source straddling the start of the destination */ + ck_assert_int_ne(ramboot_region_overlap(dst_lo, dst_hi, + dst_lo - 0x100, dst_lo + 0x100), 0); + /* source inside the destination: staged into the load region */ + ck_assert_int_ne(ramboot_region_overlap(dst_lo, dst_hi, + dst_lo + 0x1000, dst_lo + 0x2000), 0); + /* source exactly equal to the destination */ + ck_assert_int_ne(ramboot_region_overlap(dst_lo, dst_hi, + dst_lo, dst_hi), 0); + /* adjacent but not overlapping, at both ends */ + ck_assert_int_eq(ramboot_region_overlap(dst_lo, dst_hi, + dst_hi, dst_hi + 0x1000), 0); + ck_assert_int_eq(ramboot_region_overlap(dst_lo, dst_hi, + dst_lo - 0x1000, dst_lo), 0); +} +END_TEST + +/* wolfBoot_dualboot_select() */ + +/* The policy reads partition state once both slots hold an image. These + * partitions are memory mapped, so an anonymous mapping is enough. */ +static int prepare_mock_partitions(void) +{ + void *p; + size_t len = WOLFBOOT_PARTITION_SIZE + IMAGE_HEADER_SIZE; + + p = mmap((void *)(uintptr_t)MOCK_ADDRESS_BOOT, len, + PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, + -1, 0); + if (p != (void *)(uintptr_t)MOCK_ADDRESS_BOOT) { + return -1; + } + memset(p, 0xFF, len); + p = mmap((void *)(uintptr_t)MOCK_ADDRESS_UPDATE, len, + PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, + -1, 0); + if (p != (void *)(uintptr_t)MOCK_ADDRESS_UPDATE) { + return -1; + } + memset(p, 0xFF, len); + return 0; +} + +/* Pins the version-arithmetic half of the policy shared with the encrypted + * loader; that half needs no partition state. */ +START_TEST(test_dualboot_select_versions) +{ + ck_assert_int_eq(prepare_mock_partitions(), 0); + + /* no images at all */ + ck_assert_int_eq(wolfBoot_dualboot_select(0, 0), -1); + /* only the update slot has an image */ + ck_assert_int_eq(wolfBoot_dualboot_select(0, 5), PART_UPDATE); + /* only the boot slot */ + ck_assert_int_eq(wolfBoot_dualboot_select(5, 0), PART_BOOT); + /* newer update wins */ + ck_assert_int_eq(wolfBoot_dualboot_select(1, 2), PART_UPDATE); + /* older update loses */ + ck_assert_int_eq(wolfBoot_dualboot_select(2, 1), PART_BOOT); + /* equal versions keep the boot slot */ + ck_assert_int_eq(wolfBoot_dualboot_select(3, 3), PART_BOOT); +} +END_TEST + +Suite *wolfboot_suite(void) +{ + Suite *s = suite_create("wolfBoot-compressed-tlv"); + TCase *tc = tcase_create("compressed manifest"); + + tcase_add_test(tc, test_compression_absent); + tcase_add_test(tc, test_compression_gzip_ok); + tcase_add_test(tc, test_compression_unknown_algo); + tcase_add_test(tc, test_compression_bad_algo_len); + tcase_add_test(tc, test_compression_missing_size); + tcase_add_test(tc, test_compression_bad_size_len); + tcase_add_test(tc, test_overlap_detection); + tcase_add_test(tc, test_dualboot_select_versions); + suite_add_tcase(s, tc); + return s; +} + +int main(int argc, char *argv[]) +{ + int number_failed; + SRunner *sr; + + argv0 = argv[0]; + (void)argc; + + sr = srunner_create(wolfboot_suite()); + srunner_set_fork_status(sr, CK_NOFORK); + srunner_run_all(sr, CK_NORMAL); + number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + return (number_failed == 0) ? 0 : 1; +} diff --git a/tools/unit-tests/unit-sign-encrypted-output.c b/tools/unit-tests/unit-sign-encrypted-output.c index 8d8de172e8..b18be16a11 100644 --- a/tools/unit-tests/unit-sign-encrypted-output.c +++ b/tools/unit-tests/unit-sign-encrypted-output.c @@ -851,7 +851,7 @@ START_TEST(test_header_required_size_covers_all_branches) static const int sign_opts[] = { NO_SIGN, SIGN_ED25519, SIGN_ECC256, SIGN_RSA2048 }; static const int hash_opts[] = { HASH_SHA256, HASH_SHA384, HASH_SHA3 }; - int s, h, policy, no_ts, is_diff, has_dts; + int s, h, policy, no_ts, is_diff, has_dts, compressed; ck_assert_ptr_nonnull(mkdtemp(tempdir)); @@ -881,6 +881,7 @@ START_TEST(test_header_required_size_covers_all_branches) for (no_ts = 0; no_ts <= 1; no_ts++) { for (is_diff = 0; is_diff <= 1; is_diff++) { for (has_dts = 0; has_dts <= 1; has_dts++) { + for (compressed = 0; compressed <= 1; compressed++) { uint32_t required; uint32_t idx; int ret; @@ -913,6 +914,12 @@ START_TEST(test_header_required_size_covers_all_branches) if (is_diff) { CMD.no_base_sha = 1; } + /* Any size will do: gzip_probe() runs in + * main(), not in the writer. */ + if (compressed && !is_diff) { + CMD.compressed = 1; + CMD.uncompressed_sz = 0x1234; + } /* Model first, on the clean CMD we just set. */ required = header_required_size(is_diff, 0, 0); @@ -925,6 +932,7 @@ START_TEST(test_header_required_size_covers_all_branches) ck_assert_uint_le(idx, required); unlink(output_path); } + } } } }