diff --git a/.github/workflows/sim.yaml b/.github/workflows/sim.yaml index 3811c4ed16..3edbfcb4cc 100644 --- a/.github/workflows/sim.yaml +++ b/.github/workflows/sim.yaml @@ -44,6 +44,7 @@ jobs: - "sig-rsa validate-primary-slot ram-load multiimage" - "sig-rsa validate-primary-slot direct-xip multiimage" - "sig-ecdsa hw-rollback-protection multiimage" + - "overwrite-only delta-dfu,sig-ecdsa overwrite-only delta-dfu,sig-ecdsa overwrite-only delta-dfu hw-rollback-protection" - "sig-ed25519 sig-second-key" # Logical sectors: swap bookkeeping in fixed 4K units # independent of the physical page layout. Covers each diff --git a/boot/bootutil/CMakeLists.txt b/boot/bootutil/CMakeLists.txt index 5388828ee0..998cf1d868 100644 --- a/boot/bootutil/CMakeLists.txt +++ b/boot/bootutil/CMakeLists.txt @@ -22,6 +22,7 @@ target_sources(bootutil src/bootutil_img_security_cnt.c src/bootutil_misc.c src/bootutil_area.c + src/delta.c src/bootutil_loader.c src/bootutil_public.c src/caps.c diff --git a/boot/bootutil/include/bootutil/caps.h b/boot/bootutil/include/bootutil/caps.h index 3227f7e49c..ddaa812238 100644 --- a/boot/bootutil/include/bootutil/caps.h +++ b/boot/bootutil/include/bootutil/caps.h @@ -54,6 +54,7 @@ uint32_t bootutil_get_caps(void); #define BOOTUTIL_CAP_HW_ROLLBACK_PROT (1<<18) #define BOOTUTIL_CAP_ECDSA_P384 (1<<19) #define BOOTUTIL_CAP_SWAP_USING_OFFSET (1<<20) +#define BOOTUTIL_CAP_DELTA_DFU (1<<21) /* * Query the number of images this bootloader is configured for. This diff --git a/boot/bootutil/include/bootutil/image.h b/boot/bootutil/include/bootutil/image.h index 3d103f8daa..50606093dc 100644 --- a/boot/bootutil/include/bootutil/image.h +++ b/boot/bootutil/include/bootutil/image.h @@ -81,6 +81,7 @@ extern "C" { #define IMAGE_F_COMPRESSED_LZMA1 0x00000200 #define IMAGE_F_COMPRESSED_LZMA2 0x00000400 #define IMAGE_F_COMPRESSED_ARM_THUMB_FLT 0x00000800 +#define IMAGE_F_DELTA 0x00001000 /* * ECSDA224 is with NIST P-224 @@ -119,13 +120,13 @@ extern "C" { #define IMAGE_TLV_DEPENDENCY 0x40 /* Image depends on other image */ #define IMAGE_TLV_SEC_CNT 0x50 /* security counter */ #define IMAGE_TLV_BOOT_RECORD 0x60 /* measured boot record */ -/* The following flags relate to compressed images and are for the decompressed image data */ +/* The following flags relate to transformed images and their output image data */ #define IMAGE_TLV_DECOMP_SIZE 0x70 /* Decompressed image size excluding header/TLVs */ -#define IMAGE_TLV_DECOMP_SHA 0x71 /* - * Decompressed image shaX hash, this field must match - * the format and size of the raw slot (compressed) - * shaX hash +#define IMAGE_TLV_OUTPUT_SHA 0x71 /* + * Output image shaX hash, this field must match the + * format and size of the input image shaX hash */ +#define IMAGE_TLV_DECOMP_SHA IMAGE_TLV_OUTPUT_SHA /* Compatibility alias */ #define IMAGE_TLV_DECOMP_SIGNATURE 0x72 /* * Decompressed image signature, this field must match * the format and size of the raw slot (compressed) @@ -134,6 +135,7 @@ extern "C" { #define IMAGE_TLV_COMP_DEC_SIZE 0x73 /* Compressed decrypted image size */ #define IMAGE_TLV_UUID_VID 0x74 /* Vendor unique identifier */ #define IMAGE_TLV_UUID_CID 0x75 /* Device class unique identifier */ +#define IMAGE_TLV_DELTA_BASE_SHA 0x76 /* SHA of image the delta applies to */ /* * vendor reserved TLVs at xxA0-xxFF, * where xx denotes the upper byte @@ -199,6 +201,8 @@ STRUCT_PACKED image_tlv { #define MUST_DECOMPRESS(fap, idx, hdr) \ (flash_area_get_id(fap) == FLASH_AREA_IMAGE_SECONDARY(idx) && IS_COMPRESSED(hdr)) +#define IS_DELTA(hdr) ((hdr)->ih_flags & IMAGE_F_DELTA) + _Static_assert(sizeof(struct image_header) == IMAGE_HEADER_SIZE, "struct image_header not required size"); diff --git a/boot/bootutil/src/bootutil_priv.h b/boot/bootutil/src/bootutil_priv.h index 77bce24ec1..2e89149208 100644 --- a/boot/bootutil/src/bootutil_priv.h +++ b/boot/bootutil/src/bootutil_priv.h @@ -357,6 +357,10 @@ int boot_copy_region(struct boot_loader_state *state, #endif bool boot_status_is_reset(const struct boot_status *bs); +#ifdef MCUBOOT_DELTA_DFU +int boot_delta_apply(struct boot_loader_state *state, struct boot_status *bs); +#endif + #ifdef MCUBOOT_ENC_IMAGES int boot_write_enc_keys(const struct flash_area *fap, const struct boot_status *bs); bool boot_read_enc_key(const struct flash_area *fap, uint8_t slot, diff --git a/boot/bootutil/src/caps.c b/boot/bootutil/src/caps.c index f1cf63c150..3804ea9443 100644 --- a/boot/bootutil/src/caps.c +++ b/boot/bootutil/src/caps.c @@ -83,6 +83,9 @@ uint32_t bootutil_get_caps(void) #if defined(MCUBOOT_HW_ROLLBACK_PROT) res |= BOOTUTIL_CAP_HW_ROLLBACK_PROT; #endif +#if defined(MCUBOOT_DELTA_DFU) + res |= BOOTUTIL_CAP_DELTA_DFU; +#endif return res; } diff --git a/boot/bootutil/src/delta.c b/boot/bootutil/src/delta.c new file mode 100644 index 0000000000..608846d0f1 --- /dev/null +++ b/boot/bootutil/src/delta.c @@ -0,0 +1,970 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + */ + +#include +#include +#include +#include +#include + +#include "flash_map_backend/flash_map_backend.h" +#include "bootutil/boot_hooks.h" +#include "bootutil/bootutil_log.h" +#include "bootutil/crypto/sha.h" +#include "bootutil/fault_injection_hardening.h" +#include "bootutil/image.h" +#include "bootutil_loader.h" +#include "bootutil_priv.h" + +BOOT_LOG_MODULE_DECLARE(mcuboot); + +#ifdef MCUBOOT_DELTA_DFU + +#define BOOT_DELTA_MAGIC 0x314c444d /* "MDL1" */ +#define BOOT_DELTA_VERSION 1 +#define BOOT_DELTA_HEADER_SIZE 32 +#define BOOT_DELTA_F_RESTORE 0x00000001 + +#ifndef MCUBOOT_DELTA_SECTOR_BUF_SIZE +#define MCUBOOT_DELTA_SECTOR_BUF_SIZE 4096 +#endif + +#if BOOT_MAX_ALIGN > 1024 +#define DELTA_STREAM_BUF_SZ BOOT_MAX_ALIGN +#else +#define DELTA_STREAM_BUF_SZ 1024 +#endif + +struct boot_delta_header { + uint32_t magic; + uint16_t version; + uint16_t header_size; + uint32_t target_size; + uint32_t write_size; + uint32_t record_count; + uint32_t block_size; + uint32_t flags; + uint32_t base_size; +}; + +struct boot_delta_record { + uint32_t offset; + uint32_t size; +}; + +static uint32_t +boot_delta_align_up(uint32_t value, uint32_t align) +{ + return (value + align - 1) & ~(align - 1); +} + +static bool +boot_delta_is_power_of_two(uint32_t value) +{ + return value != 0 && (value & (value - 1)) == 0; +} + +static int +boot_delta_read_prot_tlv(const struct image_header *hdr, + const struct flash_area *fap, uint16_t tlv_type, + uint8_t *out, uint16_t out_len) +{ + struct image_tlv_iter it; + uint32_t off; + uint16_t len; + int rc; + + rc = bootutil_tlv_iter_begin(&it, hdr, fap, tlv_type, true); + if (rc != 0) { + return rc; + } + + rc = bootutil_tlv_iter_next(&it, &off, &len, NULL); + if (rc != 0) { + return -1; + } + + if (len != out_len) { + return -1; + } + + return LOAD_IMAGE_DATA(hdr, fap, off, out, out_len); +} + +static int +boot_delta_image_end(const struct image_header *hdr, + const struct flash_area *fap, uint32_t *image_end) +{ + struct image_tlv_iter it = {0}; + int rc; + + rc = bootutil_tlv_iter_begin(&it, hdr, fap, IMAGE_TLV_DELTA_BASE_SHA, true); + if (rc != 0) { + return rc; + } + + *image_end = it.tlv_end; + return 0; +} + +static int +boot_delta_image_hash(struct boot_loader_state *state, int slot, uint8_t *hash) +{ + TARGET_STATIC uint8_t tmpbuf[BOOT_TMPBUF_SZ]; + const struct flash_area *fap = BOOT_IMG_AREA(state, slot); + struct image_header *hdr = boot_img_hdr(state, slot); + + return bootutil_img_hash(state, hdr, fap, tmpbuf, BOOT_TMPBUF_SZ, hash, NULL, 0); +} + +static bool +boot_delta_record_fits(uint32_t offset, uint32_t size, uint32_t limit) +{ + return size != 0 && offset <= limit && size <= (limit - offset); +} + +static bool +boot_delta_has_restore(const struct boot_delta_header *delta) +{ + return (delta->flags & BOOT_DELTA_F_RESTORE) != 0; +} + +static int +boot_delta_copy_record(const struct flash_area *fap_secondary, + const struct flash_area *fap_primary, + uint32_t data_off, const struct boot_delta_record *rec) +{ + TARGET_STATIC uint8_t buf[DELTA_STREAM_BUF_SZ] __attribute__((aligned(4))); + uint32_t copied = 0; + int rc; + + rc = boot_erase_region(fap_primary, rec->offset, rec->size, false); + if (rc != 0) { + return BOOT_EFLASH; + } + + while (copied < rec->size) { + uint32_t chunk = rec->size - copied; + + if (chunk > sizeof(buf)) { + chunk = sizeof(buf); + } + + rc = flash_area_read(fap_secondary, data_off + copied, buf, chunk); + if (rc != 0) { + return BOOT_EFLASH; + } + + rc = flash_area_write(fap_primary, rec->offset + copied, buf, chunk); + if (rc != 0) { + return BOOT_EFLASH; + } + + copied += chunk; + MCUBOOT_WATCHDOG_FEED(); + } + + return 0; +} + +static int +boot_delta_read_record(const struct image_header *patch_hdr, + const struct flash_area *fap_secondary, + uint32_t payload_end, uint32_t off, + const struct boot_delta_header *delta, + struct boot_delta_record *rec, uint32_t *new_data_off, + uint32_t *old_data_off, uint32_t *next_off) +{ + uint32_t data_size; + uint32_t data_end; + int rc; + + (void)patch_hdr; + (void)delta; + + if (!boot_delta_record_fits(off, sizeof(*rec), payload_end)) { + return -1; + } + + rc = LOAD_IMAGE_DATA(patch_hdr, fap_secondary, off, rec, sizeof(*rec)); + if (rc != 0) { + return BOOT_EFLASH; + } + + *new_data_off = off + sizeof(*rec); + if (rec->size > (UINT32_MAX / 2)) { + return -1; + } + data_size = rec->size * 2; + *old_data_off = *new_data_off + rec->size; + + if (!boot_delta_record_fits(*new_data_off, data_size, payload_end)) { + return -1; + } + + data_end = *new_data_off + data_size; + *next_off = boot_delta_align_up(data_end, 4); + + return 0; +} + +static int +boot_delta_record_covers_erase_sectors(const struct flash_area *fap_primary, + const struct boot_delta_record *rec) +{ + uint32_t off = rec->offset; + uint32_t end = rec->offset + rec->size; + int rc; + + while (off < end) { + struct flash_sector sector; + uint32_t sector_off; + uint32_t sector_size; + + rc = flash_area_get_sector(fap_primary, off, §or); + if (rc != 0) { + return rc; + } + + sector_off = flash_sector_get_off(§or); + sector_size = flash_sector_get_size(§or); + if (sector_size == 0 || off != sector_off || + sector_size > (end - sector_off)) { + return -1; + } + + off = sector_off + sector_size; + } + + return off == end ? 0 : -1; +} + +static int +boot_delta_validate_record(const struct flash_area *fap_primary, + const struct boot_delta_header *delta, + uint32_t write_align, + const struct boot_delta_record *rec, + uint32_t prev_end) +{ + if (write_align == 0) { + return -1; + } + + if (!boot_delta_record_fits(rec->offset, rec->size, flash_area_get_size(fap_primary)) || + !boot_delta_record_fits(rec->offset, rec->size, delta->write_size)) { + return -1; + } + + if (rec->offset < prev_end) { + return -1; + } + + if ((rec->offset % write_align) != 0 || (rec->size % write_align) != 0) { + return -1; + } + + if (device_requires_erase(fap_primary) && + boot_delta_record_covers_erase_sectors(fap_primary, rec) != 0) { + return -1; + } + + return 0; +} + +static int +boot_delta_validate_records(struct boot_loader_state *state, + const struct boot_delta_header *delta) +{ + const struct flash_area *fap_secondary = BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY); + const struct flash_area *fap_primary = BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY); + const struct image_header *patch_hdr = boot_img_hdr(state, BOOT_SLOT_SECONDARY); + const uint32_t payload_start = patch_hdr->ih_hdr_size; + const uint32_t payload_end = payload_start + patch_hdr->ih_img_size; + const uint32_t write_align = flash_area_align(fap_primary); + uint32_t off = payload_start + delta->header_size; + uint32_t prev_end = 0; + uint32_t i; + int rc; + + if (write_align == 0 || (delta->block_size % write_align) != 0) { + return -1; + } + + for (i = 0; i < delta->record_count; i++) { + struct boot_delta_record rec; + uint32_t new_data_off; + uint32_t old_data_off; + uint32_t next_off; + + rc = boot_delta_read_record(patch_hdr, fap_secondary, payload_end, off, + delta, &rec, &new_data_off, &old_data_off, + &next_off); + if (rc != 0) { + return rc; + } + (void)new_data_off; + (void)old_data_off; + + rc = boot_delta_validate_record(fap_primary, delta, write_align, &rec, prev_end); + if (rc != 0) { + return rc; + } + + prev_end = rec.offset + rec.size; + off = next_off; + } + + return off == payload_end ? 0 : -1; +} + +static int +boot_delta_apply_records_direct(struct boot_loader_state *state, + const struct boot_delta_header *delta, + bool restore) +{ + const struct flash_area *fap_secondary = BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY); + const struct flash_area *fap_primary = BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY); + const struct image_header *patch_hdr = boot_img_hdr(state, BOOT_SLOT_SECONDARY); + const uint32_t payload_start = patch_hdr->ih_hdr_size; + const uint32_t payload_end = payload_start + patch_hdr->ih_img_size; + const uint32_t write_align = flash_area_align(fap_primary); + uint32_t off = payload_start + delta->header_size; + uint32_t prev_end = 0; + uint32_t i; + int rc; + + for (i = 0; i < delta->record_count; i++) { + struct boot_delta_record rec; + uint32_t new_data_off; + uint32_t old_data_off; + uint32_t next_off; + uint32_t source_off; + + rc = boot_delta_read_record(patch_hdr, fap_secondary, payload_end, off, + delta, &rec, &new_data_off, &old_data_off, + &next_off); + if (rc != 0) { + return rc; + } + + rc = boot_delta_validate_record(fap_primary, delta, write_align, &rec, prev_end); + if (rc != 0) { + return BOOT_EBADIMAGE; + } + prev_end = rec.offset + rec.size; + + source_off = restore ? old_data_off : new_data_off; + rc = boot_delta_copy_record(fap_secondary, fap_primary, source_off, &rec); + if (rc != 0) { + return rc; + } + + off = next_off; + } + + return off == payload_end ? 0 : BOOT_EBADIMAGE; +} + +static int +boot_delta_overlay_sector(struct boot_loader_state *state, + const struct boot_delta_header *delta, + uint32_t sector_off, uint32_t sector_size, + uint8_t *sector_buf, bool *touched, + bool restore) +{ + const struct flash_area *fap_secondary = BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY); + const struct flash_area *fap_primary = BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY); + const struct image_header *patch_hdr = boot_img_hdr(state, BOOT_SLOT_SECONDARY); + const uint32_t payload_start = patch_hdr->ih_hdr_size; + const uint32_t payload_end = payload_start + patch_hdr->ih_img_size; + const uint32_t sector_end = sector_off + sector_size; + const uint32_t write_align = flash_area_align(fap_primary); + uint32_t off = payload_start + delta->header_size; + uint32_t prev_end = 0; + uint32_t i; + int rc; + + *touched = false; + + for (i = 0; i < delta->record_count; i++) { + struct boot_delta_record rec; + uint32_t new_data_off; + uint32_t old_data_off; + uint32_t source_off; + uint32_t next_off; + uint32_t rec_end; + uint32_t overlap_start; + uint32_t overlap_end; + uint32_t overlap_len; + + rc = boot_delta_read_record(patch_hdr, fap_secondary, payload_end, off, + delta, &rec, &new_data_off, &old_data_off, + &next_off); + if (rc != 0) { + return rc; + } + + rc = boot_delta_validate_record(fap_primary, delta, write_align, &rec, prev_end); + if (rc != 0) { + return BOOT_EBADIMAGE; + } + rec_end = rec.offset + rec.size; + prev_end = rec_end; + + if (rec.offset >= sector_end || rec_end <= sector_off) { + off = next_off; + continue; + } + + if (!*touched) { + rc = flash_area_read(fap_primary, sector_off, sector_buf, sector_size); + if (rc != 0) { + return BOOT_EFLASH; + } + *touched = true; + } + + overlap_start = rec.offset > sector_off ? rec.offset : sector_off; + overlap_end = rec_end < sector_end ? rec_end : sector_end; + overlap_len = overlap_end - overlap_start; + source_off = restore ? old_data_off : new_data_off; + + rc = flash_area_read(fap_secondary, + source_off + (overlap_start - rec.offset), + sector_buf + (overlap_start - sector_off), + overlap_len); + if (rc != 0) { + return BOOT_EFLASH; + } + + off = next_off; + } + + return off == payload_end ? 0 : BOOT_EBADIMAGE; +} + +static int +boot_delta_write_sector(const struct flash_area *fap_primary, uint32_t sector_off, + uint32_t sector_size, const uint8_t *sector_buf) +{ + uint32_t written = 0; + int rc; + + rc = boot_erase_region(fap_primary, sector_off, sector_size, false); + if (rc != 0) { + return BOOT_EFLASH; + } + + while (written < sector_size) { + uint32_t chunk = sector_size - written; + + if (chunk > DELTA_STREAM_BUF_SZ) { + chunk = DELTA_STREAM_BUF_SZ; + } + + rc = flash_area_write(fap_primary, sector_off + written, + sector_buf + written, chunk); + if (rc != 0) { + return BOOT_EFLASH; + } + + written += chunk; + MCUBOOT_WATCHDOG_FEED(); + } + + return 0; +} + +static int +boot_delta_apply_records_with_erase(struct boot_loader_state *state, + const struct boot_delta_header *delta, + uint8_t *sector_buf, + bool restore) +{ + const struct flash_area *fap_primary = BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY); + uint32_t off = 0; + int rc; + + while (off < delta->write_size) { + struct flash_sector sector; + uint32_t sector_off; + uint32_t sector_size; + bool touched; + + rc = flash_area_get_sector(fap_primary, off, §or); + if (rc != 0) { + return BOOT_EFLASH; + } + + sector_off = flash_sector_get_off(§or); + sector_size = flash_sector_get_size(§or); + if (sector_size == 0 || sector_size > MCUBOOT_DELTA_SECTOR_BUF_SIZE) { + BOOT_LOG_ERR("Delta sector buffer too small: sector=%" PRIu32 ", buffer=%zu", + sector_size, (size_t)MCUBOOT_DELTA_SECTOR_BUF_SIZE); + return BOOT_EBADIMAGE; + } + + rc = boot_delta_overlay_sector(state, delta, sector_off, sector_size, + sector_buf, &touched, restore); + if (rc != 0) { + return rc; + } + + if (touched) { + rc = boot_delta_write_sector(fap_primary, sector_off, sector_size, + sector_buf); + if (rc != 0) { + return rc; + } + } + + if (sector_off + sector_size <= off) { + return BOOT_EBADIMAGE; + } + off = sector_off + sector_size; + } + + return 0; +} + +static int +boot_delta_apply_records(struct boot_loader_state *state, + const struct boot_delta_header *delta, + uint8_t *sector_buf, + bool restore) +{ + if (device_requires_erase(BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY))) { + return boot_delta_apply_records_with_erase(state, delta, sector_buf, restore); + } + + return boot_delta_apply_records_direct(state, delta, restore); +} + +static int +boot_delta_validate_reconstructed_target(struct boot_loader_state *state, + const uint8_t *expected_hash) +{ + TARGET_STATIC uint8_t tmpbuf[BOOT_TMPBUF_SZ]; + uint8_t hash[IMAGE_HASH_SIZE]; + struct image_header *hdr = boot_img_hdr(state, BOOT_SLOT_PRIMARY); + const struct flash_area *fap = BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY); + FIH_DECLARE(fih_rc, FIH_FAILURE); + int rc; + + rc = boot_read_image_header(state, BOOT_SLOT_PRIMARY, hdr, NULL); + if (rc != 0 || !boot_check_header_valid(state, BOOT_SLOT_PRIMARY) || IS_DELTA(hdr)) { + return BOOT_EBADIMAGE; + } + + rc = boot_delta_image_hash(state, BOOT_SLOT_PRIMARY, hash); + if (rc != 0) { + return rc; + } + + if (memcmp(hash, expected_hash, IMAGE_HASH_SIZE) != 0) { + BOOT_LOG_ERR("Delta reconstructed image hash mismatch"); + return BOOT_EBADIMAGE; + } + + FIH_CALL(bootutil_img_validate, fih_rc, state, hdr, fap, tmpbuf, BOOT_TMPBUF_SZ, + NULL, 0, NULL); + if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) { + return BOOT_EBADIMAGE; + } + + return 0; +} + +static int +boot_delta_clear_primary_trailer(struct boot_loader_state *state, + uint8_t *sector_buf) +{ + const struct flash_area *fap = BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY); + const uint32_t area_size = flash_area_get_size(fap); + const uint32_t trailer_off = boot_swap_info_off(fap) + BOOT_MAX_ALIGN; + struct flash_sector sector; + uint32_t sector_off; + uint32_t sector_size; + uint32_t preserved_size; + int rc; + + if (!device_requires_erase(fap)) { + return boot_scramble_region(fap, trailer_off, area_size - trailer_off, false); + } + + rc = flash_area_get_sector(fap, trailer_off, §or); + if (rc != 0) { + return BOOT_EFLASH; + } + + sector_off = flash_sector_get_off(§or); + sector_size = flash_sector_get_size(§or); + if (sector_size == 0 || sector_size > MCUBOOT_DELTA_SECTOR_BUF_SIZE || + sector_off > trailer_off || sector_size > (area_size - sector_off)) { + return BOOT_EBADIMAGE; + } + + preserved_size = trailer_off - sector_off; + if (preserved_size > sector_size || + (area_size - trailer_off) > (sector_size - preserved_size)) { + return BOOT_EBADIMAGE; + } + + rc = flash_area_read(fap, sector_off, sector_buf, sector_size); + if (rc != 0) { + return BOOT_EFLASH; + } + + rc = flash_area_erase(fap, sector_off, sector_size); + if (rc != 0) { + return BOOT_EFLASH; + } + + if (preserved_size == 0) { + return 0; + } + + rc = flash_area_write(fap, sector_off, sector_buf, preserved_size); + return rc == 0 ? 0 : BOOT_EFLASH; +} + +static int +boot_delta_clear_secondary_trailer(struct boot_loader_state *state) +{ + const struct flash_area *fap = BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY); + const uint32_t area_size = flash_area_get_size(fap); + const uint32_t trailer_off = boot_swap_info_off(fap) + BOOT_MAX_ALIGN; + size_t last_sector; + + if (!device_requires_erase(fap)) { + return boot_scramble_region(fap, trailer_off, area_size - trailer_off, false); + } + + last_sector = boot_img_num_sectors(state, BOOT_SLOT_SECONDARY) - 1; + return boot_scramble_region(fap, + boot_img_sector_off(state, BOOT_SLOT_SECONDARY, + last_sector), + boot_img_sector_size(state, BOOT_SLOT_SECONDARY, + last_sector), + false); +} + +static int +boot_delta_scramble_secondary(struct boot_loader_state *state) +{ + const struct flash_area *fap_secondary = BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY); + int rc; + + rc = boot_scramble_region(fap_secondary, + boot_img_sector_off(state, BOOT_SLOT_SECONDARY, 0), + boot_img_sector_size(state, BOOT_SLOT_SECONDARY, 0), false); + if (rc != 0) { + return rc; + } + + return boot_delta_clear_secondary_trailer(state); +} + +static int +boot_delta_read_header(const struct image_header *patch_hdr, + const struct flash_area *fap_secondary, + struct boot_delta_header *delta) +{ + return LOAD_IMAGE_DATA(patch_hdr, fap_secondary, patch_hdr->ih_hdr_size, + delta, BOOT_DELTA_HEADER_SIZE); +} + +static int +boot_delta_validate_header(struct boot_loader_state *state, + const struct image_header *patch_hdr, + const struct boot_delta_header *delta) +{ + uint32_t write_align = flash_area_align(BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY)); + + if (delta->magic != BOOT_DELTA_MAGIC || + delta->version != BOOT_DELTA_VERSION || + delta->target_size == 0 || + delta->header_size != BOOT_DELTA_HEADER_SIZE || + delta->header_size > patch_hdr->ih_img_size || + delta->write_size < delta->target_size || + delta->write_size > flash_area_get_size(BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY)) || + !boot_delta_is_power_of_two(delta->block_size) || + !boot_delta_is_power_of_two(write_align) || + (delta->write_size % write_align) != 0 || + (delta->flags & ~BOOT_DELTA_F_RESTORE) != 0 || + !boot_delta_has_restore(delta) || + delta->base_size == 0 || + delta->write_size < delta->base_size) { + return BOOT_EBADIMAGE; + } + + return 0; +} + +static int +boot_delta_validate_patch_storage(struct boot_loader_state *state, + const struct image_header *patch_hdr, + const struct boot_delta_header *delta) +{ + const struct flash_area *fap_primary = + BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY); + const struct flash_area *fap_secondary = + BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY); + uint32_t image_end; + uint32_t primary_limit; + uint32_t secondary_limit; + size_t last_sector; + int rc; + + if (device_requires_erase(fap_primary)) { + if (boot_img_num_sectors(state, BOOT_SLOT_PRIMARY) == 0) { + return BOOT_EBADIMAGE; + } + + last_sector = boot_img_num_sectors(state, BOOT_SLOT_PRIMARY) - 1; + primary_limit = + boot_img_sector_off(state, BOOT_SLOT_PRIMARY, last_sector); + } else { + primary_limit = boot_swap_info_off(fap_primary); + } + if (delta->write_size > primary_limit) { + return BOOT_EBADIMAGE; + } + + rc = boot_delta_image_end(patch_hdr, fap_secondary, &image_end); + if (rc != 0) { + return BOOT_EBADIMAGE; + } + + if (device_requires_erase(fap_secondary)) { + if (boot_img_num_sectors(state, BOOT_SLOT_SECONDARY) == 0) { + return BOOT_EBADIMAGE; + } + + last_sector = boot_img_num_sectors(state, BOOT_SLOT_SECONDARY) - 1; + secondary_limit = + boot_img_sector_off(state, BOOT_SLOT_SECONDARY, last_sector); + } else { + secondary_limit = boot_swap_info_off(fap_secondary); + } + + return image_end <= secondary_limit ? 0 : BOOT_EBADIMAGE; +} + +static bool +boot_delta_should_stage_restore(const struct boot_delta_header *delta, + const struct boot_status *bs) +{ + return bs->swap_type == BOOT_SWAP_TYPE_TEST && boot_delta_has_restore(delta); +} + +static int +boot_delta_stage_restore(struct boot_loader_state *state, uint8_t *sector_buf) +{ + const struct flash_area *fap_primary = BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY); + int rc; + + /* + * Keep the secondary TEST marker intact until the primary REVERT marker is + * durable. A reset before the final secondary cleanup therefore retries the + * forward delta instead of losing both recovery states. + */ + rc = boot_delta_clear_primary_trailer(state, sector_buf); + if (rc != 0) { + return rc; + } + + rc = boot_write_magic(fap_primary); + if (rc != 0) { + return rc; + } + + rc = boot_write_copy_done(fap_primary); + if (rc != 0) { + return rc; + } + + return boot_delta_clear_secondary_trailer(state); +} + +static int +boot_delta_finish_restore(struct boot_loader_state *state) +{ + const struct flash_area *fap_primary = + BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY); + int rc; + + /* + * Commit the restored base by setting image_ok. This is an aligned trailer + * write and does not erase image bytes from the primary's final sector. + */ + rc = boot_write_image_ok(fap_primary); + if (rc != 0) { + return rc; + } + + return boot_delta_scramble_secondary(state); +} + +int +boot_delta_apply(struct boot_loader_state *state, struct boot_status *bs) +{ + TARGET_STATIC uint8_t sector_buf[MCUBOOT_DELTA_SECTOR_BUF_SIZE] + __attribute__((aligned(4))); + const struct flash_area *fap_secondary = BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY); + const struct image_header *patch_hdr = boot_img_hdr(state, BOOT_SLOT_SECONDARY); + struct boot_delta_header delta; + uint8_t expected_base_hash[IMAGE_HASH_SIZE]; + uint8_t expected_target_hash[IMAGE_HASH_SIZE]; + uint8_t actual_primary_hash[IMAGE_HASH_SIZE]; + const uint8_t *expected_start_hash; + const uint8_t *expected_end_hash; + uint32_t image_size; + bool restore = false; + bool stage_restore = false; + bool target_ready = false; + int rc; + + if (!IS_DELTA(patch_hdr)) { + return BOOT_EBADIMAGE; + } + + restore = bs->swap_type == BOOT_SWAP_TYPE_REVERT; + BOOT_LOG_INF("Image %d %s delta secondary slot -> primary slot", + BOOT_CURR_IMG(state), restore ? "restoring" : "applying"); + + rc = boot_delta_read_prot_tlv(patch_hdr, fap_secondary, + IMAGE_TLV_DELTA_BASE_SHA, + expected_base_hash, sizeof(expected_base_hash)); + if (rc != 0) { + BOOT_LOG_ERR("Delta base hash TLV missing"); + return BOOT_EBADIMAGE; + } + + rc = boot_delta_read_prot_tlv(patch_hdr, fap_secondary, + IMAGE_TLV_OUTPUT_SHA, + expected_target_hash, sizeof(expected_target_hash)); + if (rc != 0) { + BOOT_LOG_ERR("Delta target hash TLV missing"); + return BOOT_EBADIMAGE; + } + + if (patch_hdr->ih_img_size < BOOT_DELTA_HEADER_SIZE) { + BOOT_LOG_ERR("Delta payload too small"); + return BOOT_EBADIMAGE; + } + + rc = boot_delta_read_header(patch_hdr, fap_secondary, &delta); + if (rc != 0) { + return BOOT_EFLASH; + } + + rc = boot_delta_validate_header(state, patch_hdr, &delta); + if (rc != 0) { + BOOT_LOG_ERR("Invalid delta header"); + return BOOT_EBADIMAGE; + } + + rc = boot_delta_validate_patch_storage(state, patch_hdr, &delta); + if (rc != 0) { + BOOT_LOG_ERR("Delta image data overlaps a trailer sector"); + return BOOT_EBADIMAGE; + } + + if (restore && !boot_delta_has_restore(&delta)) { + BOOT_LOG_ERR("Delta restore data missing"); + return BOOT_EBADIMAGE; + } + stage_restore = boot_delta_should_stage_restore(&delta, bs); + + rc = boot_delta_validate_records(state, &delta); + if (rc != 0) { + BOOT_LOG_ERR("Invalid delta records"); + return BOOT_EBADIMAGE; + } + + rc = boot_delta_image_hash(state, BOOT_SLOT_PRIMARY, actual_primary_hash); + if (rc != 0) { + return rc; + } + + expected_start_hash = restore ? expected_target_hash : expected_base_hash; + expected_end_hash = restore ? expected_base_hash : expected_target_hash; + + if (memcmp(actual_primary_hash, expected_end_hash, IMAGE_HASH_SIZE) == 0) { + rc = boot_delta_validate_reconstructed_target(state, expected_end_hash); + if (rc == 0) { + BOOT_LOG_INF("Delta target image already reconstructed"); + target_ready = true; + } else { + BOOT_LOG_INF("Delta image hash complete but validation failed; rewriting records"); + } + } else if (memcmp(actual_primary_hash, expected_start_hash, IMAGE_HASH_SIZE) != 0) { + BOOT_LOG_INF("Delta interrupted; restoring base image before retry"); + rc = boot_delta_apply_records(state, &delta, sector_buf, true); + if (rc != 0) { + return rc; + } + + rc = boot_delta_validate_reconstructed_target(state, expected_base_hash); + if (rc != 0) { + BOOT_LOG_ERR("Delta base restore failed"); + return rc; + } + + if (restore) { + target_ready = true; + } + } + + if (!target_ready) { + rc = boot_delta_apply_records(state, &delta, sector_buf, restore); + if (rc != 0) { + return rc; + } + } + + rc = boot_delta_validate_reconstructed_target(state, expected_end_hash); + if (rc != 0) { + return rc; + } + +#ifdef MCUBOOT_HW_ROLLBACK_PROT + if (!restore && !stage_restore) { + rc = boot_update_security_counter(state, BOOT_SLOT_PRIMARY, BOOT_SLOT_PRIMARY); + if (rc != 0) { + BOOT_LOG_ERR("Security counter update failed after delta update: %d", rc); + return rc; + } + } +#endif + + if (restore) { + rc = boot_delta_finish_restore(state); + } else if (stage_restore) { + rc = boot_delta_stage_restore(state, sector_buf); + } else { + rc = boot_delta_scramble_secondary(state); + } + if (rc != 0) { + return rc; + } + +#if defined(MCUBOOT_OVERWRITE_ONLY) && !defined(MCUBOOT_OVERWRITE_ONLY_FAST) + image_size = restore ? delta.base_size : delta.target_size; +#else + rc = boot_read_image_size(state, BOOT_SLOT_PRIMARY, &image_size); + if (rc != 0) { + image_size = restore ? delta.base_size : delta.target_size; + } +#endif + + (void)image_size; + + return BOOT_HOOK_CALL(boot_copy_region_post_hook, 0, BOOT_CURR_IMG(state), + BOOT_IMG_AREA(state, BOOT_SLOT_PRIMARY), image_size); +} + +#endif /* MCUBOOT_DELTA_DFU */ diff --git a/boot/bootutil/src/loader.c b/boot/bootutil/src/loader.c index 6b0e84a03e..0f4411beeb 100644 --- a/boot/bootutil/src/loader.c +++ b/boot/bootutil/src/loader.c @@ -663,12 +663,18 @@ boot_validate_slot(struct boot_loader_state *state, int slot, #if defined(MCUBOOT_VERIFY_IMG_ADDRESS) && !defined(MCUBOOT_ENC_IMAGES) || \ defined(MCUBOOT_CHECK_HEADER_LOAD_ADDRESS) +#ifdef MCUBOOT_DELTA_DFU + bool is_delta_image = IS_DELTA(boot_img_hdr(state, slot)); +#else + bool is_delta_image = false; +#endif + /* Verify that the image in the secondary slot has a reset address * located in the primary slot. This is done to avoid users incorrectly * overwriting an application written to the incorrect slot. * This feature is only supported by ARM platforms. */ - if (fap == BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY)) { + if (fap == BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY) && !is_delta_image) { struct image_header *secondary_hdr = boot_img_hdr(state, slot); uint32_t internal_img_addr = 0; /* either the reset handler addres or the image beginning addres */ uint32_t min_addr; @@ -962,6 +968,12 @@ boot_copy_image(struct boot_loader_state *state, struct boot_status *bs) fap_secondary_slot = BOOT_IMG_AREA(state, BOOT_SLOT_SECONDARY); assert(fap_secondary_slot != NULL); +#ifdef MCUBOOT_DELTA_DFU + if (IS_DELTA(boot_img_hdr(state, BOOT_SLOT_SECONDARY))) { + return boot_delta_apply(state, bs); + } +#endif + sect_count = boot_img_num_sectors(state, BOOT_SLOT_PRIMARY); for (sect = 0, size = 0; sect < sect_count; sect++) { this_size = boot_img_sector_size(state, BOOT_SLOT_PRIMARY, sect); diff --git a/boot/zephyr/CMakeLists.txt b/boot/zephyr/CMakeLists.txt index df5e607e52..583617cbf2 100644 --- a/boot/zephyr/CMakeLists.txt +++ b/boot/zephyr/CMakeLists.txt @@ -156,6 +156,12 @@ else() ${BOOT_DIR}/bootutil/src/caps.c ) + if(CONFIG_BOOT_DELTA_DFU) + zephyr_sources( + ${BOOT_DIR}/bootutil/src/delta.c + ) + endif() + if(CONFIG_BOOT_SWAP_USING_MOVE) zephyr_sources( ${BOOT_DIR}/bootutil/src/swap_move.c diff --git a/boot/zephyr/Kconfig b/boot/zephyr/Kconfig index 3e8e7d5b99..79a7fe7110 100644 --- a/boot/zephyr/Kconfig +++ b/boot/zephyr/Kconfig @@ -628,6 +628,31 @@ config BOOT_UPGRADE_ONLY of swapping them. This prevents the fallback recovery, but uses a much simpler code path. +config BOOT_DELTA_DFU + bool "Apply delta update images" + depends on BOOT_UPGRADE_ONLY + depends on !BOOT_ENCRYPT_IMAGE + depends on !BOOT_SIGNATURE_TYPE_PURE + help + If y, MCUboot can accept signed delta images in the secondary slot. A + delta image is transport-neutral: any DFU path that writes it to the + secondary slot and marks it pending can trigger the update. During boot, + MCUboot verifies the delta image, restores any interrupted partial apply + back to the base image, patches the primary slot, then validates the + reconstructed target image before booting it. + +config BOOT_DELTA_DFU_SECTOR_BUFFER_SIZE + int "Delta DFU sector buffer size" + depends on BOOT_DELTA_DFU + range 256 262144 + default 4096 + help + Size of the temporary RAM buffer used to preserve unchanged bytes + while applying a delta record to flash devices that require explicit + erase before write. This must be at least as large as the biggest + primary-slot erase sector touched by a delta update or by the primary + trailer transition. + config BOOT_SWAP_USING_OFFSET bool "Swap using offset mode without scratch partition" help diff --git a/boot/zephyr/delta_dfu.conf b/boot/zephyr/delta_dfu.conf new file mode 100644 index 0000000000..2324ef4416 --- /dev/null +++ b/boot/zephyr/delta_dfu.conf @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_BOOT_UPGRADE_ONLY=y +CONFIG_BOOT_DELTA_DFU=y diff --git a/boot/zephyr/include/mcuboot_config/mcuboot_config.h b/boot/zephyr/include/mcuboot_config/mcuboot_config.h index 2ae16b7576..23b341748b 100644 --- a/boot/zephyr/include/mcuboot_config/mcuboot_config.h +++ b/boot/zephyr/include/mcuboot_config/mcuboot_config.h @@ -199,6 +199,11 @@ #define MCUBOOT_DECOMPRESS_IMAGES #endif +#ifdef CONFIG_BOOT_DELTA_DFU +#define MCUBOOT_DELTA_DFU +#define MCUBOOT_DELTA_SECTOR_BUF_SIZE CONFIG_BOOT_DELTA_DFU_SECTOR_BUFFER_SIZE +#endif + /* Invoke hashing functions directly on storage device. This requires the device * be able to map storage to address space or RAM. */ diff --git a/docs/compression_format.md b/docs/compression_format.md index 78397da7c8..7f175581da 100644 --- a/docs/compression_format.md +++ b/docs/compression_format.md @@ -152,7 +152,7 @@ The following Type-Length-Values (TLVs) are used in the context of decompressed images: - `DECOMP_SIZE (0x70)`: Specifies the size of the decompressed image. -- `DECOMP_SHA (0x71)`: Contains the hash of the decompressed image. +- `OUTPUT_SHA (0x71)`: Contains the hash of the decompressed image. - `DECOMP_SIGNATURE (0x72)`: Holds the signature of either the hash or the entire image. diff --git a/docs/delta_dfu.md b/docs/delta_dfu.md new file mode 100644 index 0000000000..2a7e4a443f --- /dev/null +++ b/docs/delta_dfu.md @@ -0,0 +1,149 @@ +# Delta DFU + +Delta DFU lets MCUboot accept a signed patch image in the secondary slot and +apply it to the current primary image. The patch image is a normal MCUboot image +with the `IMAGE_F_DELTA` header flag set. It is transport-neutral: any DFU +transport that can write the patch image to the secondary slot and mark it +pending can use it. + +Delta images are supported by the overwrite-only boot path. They are not +compressed images and they do not use the decompression path. + +## Image Format + +The delta image payload starts with this little-endian header: + +```c +struct boot_delta_header { + uint32_t magic; /* "MDL1" */ + uint16_t version; /* 1 */ + uint16_t header_size; /* sizeof(struct boot_delta_header) */ + uint32_t target_size; /* reconstructed signed target image size */ + uint32_t write_size; /* primary-slot span covered by the patch */ + uint32_t record_count; + uint32_t block_size; + uint32_t flags; + uint32_t base_size; +}; +``` + +The header is followed by `record_count` records: + +```c +struct boot_delta_record { + uint32_t offset; + uint32_t size; + uint8_t new_data[size]; + uint8_t old_data[size]; +}; +``` + +Each record replaces `size` bytes at `offset` in the primary slot. Records are +strictly ordered and non-overlapping, and record data is padded to 4 bytes in +the patch payload. + +Delta records contain the old bytes from the base image. The old bytes let +MCUboot recover from interrupted updates by restoring the touched regions back +to the signed base image before retrying the forward apply. + +The patch image contains protected TLVs with the hash of the expected base +image and the hash of the reconstructed target image: + +- `IMAGE_TLV_DELTA_BASE_SHA` +- `IMAGE_TLV_OUTPUT_SHA` + +The regular image SHA authenticates the delta payload itself, so it cannot also +identify the base image. + +During boot, MCUboot validates the signed delta image. If the active primary +hash already matches `IMAGE_TLV_OUTPUT_SHA`, MCUboot validates the complete +target image before finishing the update metadata. A matching hash with an +incomplete or damaged validation TLV causes the affected records to be written +again. If the hash matches `IMAGE_TLV_DELTA_BASE_SHA`, MCUboot applies the +records to the primary slot. If the hash matches neither value, MCUboot treats +the previous delta apply as interrupted, restores the touched regions using the +old bytes, validates the base image, and retries the update. + +## Zephyr Configuration + +Enable delta DFU in the Zephyr MCUboot image with: + +```text +CONFIG_BOOT_UPGRADE_ONLY=y +CONFIG_BOOT_DELTA_DFU=y +``` + +For sysbuild, select overwrite-only mode at the sysbuild level: + +```text +SB_CONFIG_MCUBOOT_MODE_OVERWRITE_ONLY=y +``` + +On flash devices that require erase before write, delta records must cover whole +primary-slot erase sectors and the delta write span must end before the final +primary-slot erase sector. That sector is reserved for recovery state so its +marker is never lost while image records are being restored. Configure the RAM buffer with +`CONFIG_BOOT_DELTA_DFU_SECTOR_BUFFER_SIZE`; it must be at least as large as the +largest primary-slot erase sector touched by a delta update or by the primary +trailer transition. + +## Creating Delta Images + +Create the current signed base image and the full signed target image normally. +Then create the delta image by passing the signed base image to `imgtool sign`: + +```bash +./scripts/imgtool.py sign \ + --version 2.0.0+0 \ + --header-size 0x800 \ + --slot-size 729088 \ + --overwrite-only \ + --align 1 \ + --key root-rsa-2048.pem \ + --delta-base app-base.signed.bin \ + --delta-block-size 4096 \ + app-target.bin \ + app-target.delta.signed.bin +``` + +`--delta-block-size` controls the comparison granularity. It must be a positive +power-of-two multiple of 4 and should be a multiple of the target flash write +alignment. On flash devices that require erase before write, use the primary +slot erase sector size so every changed record can be safely restored after an +interruption. The reconstructed base and target images must end before the final +primary-slot erase sector, and the signed delta image and its TLVs must end +before the final secondary-slot erase sector. MCUboot uses those sectors for +durable recovery state. `imgtool` reserves at least `--delta-block-size` bytes +at the end of both slots for this purpose. If either final erase sector is +larger, use that larger size as `--delta-block-size`; MCUboot rejects a delta +whose write span or patch overlaps a reserved sector. + +Delta images always include old bytes for each changed record. This makes the +delta payload larger than a forward-only patch, but it is required for +interruption recovery and is still usually smaller than keeping a full second +image slot. + +Each changed region stores both the new bytes and the old bytes, plus record +metadata and padding. A delta that touches many scattered regions can therefore +cost about 2x the changed bytes and can exceed the size of a plain overwrite +image. Compare the generated delta size with the full signed target image before +shipping a delta update. + +## Restore Behavior + +Delta restore uses the same secondary slot that received the update; it does not +require a transport-specific path. When a delta image is marked as a test update, +MCUboot applies the forward records and validates the new primary image. It then +clears the reserved primary trailer sector, writes durable primary restore +state, and only then erases the secondary-slot trailer sector. The signed delta +payload remains in the secondary slot, outside that final erase sector. + +If the new application confirms itself, the primary trailer `image_ok` flag is +set and the restore is cancelled. If the device reboots before confirmation, +MCUboot sees the primary trailer state, validates the same signed delta image in +the secondary slot, applies the old bytes from each record, validates the +restored image against `IMAGE_TLV_DELTA_BASE_SHA`, and commits the restored base +with `image_ok` before invalidating the patch. A reset before the primary restore +state is durable leaves the secondary test marker intact, so MCUboot retries the +forward apply. A reset during either forward apply or restore is recovered by +restoring the base records and replaying the requested direction. diff --git a/docs/design.md b/docs/design.md index 245b90f12e..55ccebdde6 100644 --- a/docs/design.md +++ b/docs/design.md @@ -99,6 +99,11 @@ struct image_tlv { #define IMAGE_F_ENCRYPTED_AES256 0x00000008 /* Encrypted using AES256. */ #define IMAGE_F_NON_BOOTABLE 0x00000010 /* Split image app. */ #define IMAGE_F_RAM_LOAD 0x00000020 +#define IMAGE_F_ROM_FIXED 0x00000100 +#define IMAGE_F_COMPRESSED_LZMA1 0x00000200 +#define IMAGE_F_COMPRESSED_LZMA2 0x00000400 +#define IMAGE_F_COMPRESSED_ARM_THUMB_FLT 0x00000800 +#define IMAGE_F_DELTA 0x00001000 /* * Image trailer TLV types. @@ -126,19 +131,22 @@ struct image_tlv { #define IMAGE_TLV_DEPENDENCY 0x40 /* Image depends on other image */ #define IMAGE_TLV_SEC_CNT 0x50 /* security counter */ #define IMAGE_TLV_BOOT_RECORD 0x60 /* measured boot record */ -/* The following flags relate to compressed images and are for the decompressed image data */ +/* The following flags relate to transformed images and their output image data */ #define IMAGE_TLV_DECOMP_SIZE 0x70 /* Decompressed image size excluding header/TLVs */ -#define IMAGE_TLV_DECOMP_SHA 0x71 /* - * Decompressed image shaX hash, this field must match - * the format and size of the raw slot (compressed) - * shaX hash +#define IMAGE_TLV_OUTPUT_SHA 0x71 /* + * Output image shaX hash, this field must match the + * format and size of the input image shaX hash */ +#define IMAGE_TLV_DECOMP_SHA IMAGE_TLV_OUTPUT_SHA /* Compatibility alias */ #define IMAGE_TLV_DECOMP_SIGNATURE 0x72 /* * Decompressed image signature, this field must match * the format and size of the raw slot (compressed) * signature */ #define IMAGE_TLV_COMP_DEC_SIZE 0x73 /* Compressed decrypted image size */ +#define IMAGE_TLV_UUID_VID 0x74 /* Vendor unique identifier */ +#define IMAGE_TLV_UUID_CID 0x75 /* Device class unique identifier */ +#define IMAGE_TLV_DELTA_BASE_SHA 0x76 /* SHA of image the delta applies to */ /* * vendor reserved TLVs at xxA0-xxFF, * where xx denotes the upper byte @@ -149,8 +157,6 @@ struct image_tlv { * ... * 0xffa0 - 0xfffe */ -#define IMAGE_TLV_UUID_VID 0x80 /* Vendor unique identifier */ -#define IMAGE_TLV_UUID_CID 0x81 /* Device class unique identifier */ ``` Optional type-length-value records (TLVs) containing image metadata are placed @@ -167,6 +173,14 @@ The `ih_hdr_size` field indicates the length of the header, and therefore the offset of the image itself. This field provides for backwards compatibility in case of changes to the format of the image header. +Delta update images set the `IMAGE_F_DELTA` header flag. Their payload is a +version-1 reversible patch stream rather than a bootable firmware body, and +MCUboot applies that stream to the active primary image during an overwrite-only +update. Delta images carry protected `IMAGE_TLV_DELTA_BASE_SHA` and +`IMAGE_TLV_OUTPUT_SHA` entries to bind the signed patch to the exact base +image it applies to and the exact target image it reconstructs. See +[Delta DFU](delta_dfu.md) for the payload and recovery-state formats. + ## [TLV allow list](#tlv-allow) While reading unprotected TLVs from an image, MCUboot will try to match TLVs diff --git a/docs/imgtool.md b/docs/imgtool.md index dfc59e2e16..c5d17f8a37 100644 --- a/docs/imgtool.md +++ b/docs/imgtool.md @@ -158,6 +158,15 @@ primary slot and adds a header and trailer that the bootloader is expecting: type. Will fall back without image compression automatically if the compression increases the image size. + --delta-base filename Create a robust signed reversible delta image + against an already-signed base image instead + of a full update image. + --delta-block-size INTEGER Granularity, in bytes, used when comparing + base and target images for --delta-base. + Use the target flash write alignment, or an + erase-block size that covers primary records + and reserves the primary and secondary + trailer blocks. --encrypt-keylen [128|256] When encrypting the image using AES, select a 128 bit or 256 bit key len. -E, --encrypt filename Encrypt image using the provided public key. @@ -202,6 +211,19 @@ about internals of image generated with this option can be found here This isn't fully supported on the embedded side but can be utilised when project is built on top of the mcuboot. +The `--delta-base` option creates a robust signed reversible delta update image +instead of a full target image. The base input must be the signed image +currently running from the primary slot. The target input is signed in memory, +compared against the signed base image, and emitted as a signed delta image +containing changed blocks plus the previous contents of those blocks. The old +bytes make the delta image larger, but they are required for interruption +recovery and allow MCUboot to revert an unconfirmed test update without keeping +a full backup image in the secondary slot. +On explicit-erase flash, `--delta-block-size` also reserves that much space at +the end of the primary and secondary slots so neither the reconstructed image +nor the signed patch can overlap a trailer sector used for recovery state. +See [Delta DFU](delta_dfu.md) for the image format and bootloader requirements. + The `--slot-size` argument is required and used to check that the firmware does not overflow into the swap status area (metadata). If swap upgrades are not being used, `--overwrite-only` can be passed to avoid adding the swap diff --git a/docs/index.md b/docs/index.md index 4f62d0cb2d..2a94c8a884 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,6 +39,7 @@ The MCUboot documentation is composed of the following pages: - [Release notes](release-notes.md) - [Bootloader design](design.md) - [Encrypted images](encrypted_images.md) +- [Delta DFU](delta_dfu.md) - [imgtool](imgtool.md) - image signing and key management - [ECDSA](ecdsa.md) - information about ECDSA signature formats - [Custom crypto backend](custom_crypto.md) - plugging in a custom crypto library diff --git a/docs/release-notes.d/delta-dfu.md b/docs/release-notes.d/delta-dfu.md new file mode 100644 index 0000000000..af9f2d8fc5 --- /dev/null +++ b/docs/release-notes.d/delta-dfu.md @@ -0,0 +1,8 @@ +- bootutil: added overwrite-only delta DFU support with robust signed reversible + protocol-v1 patch images. +- bootutil: delta DFU records include old bytes so interrupted delta applies + and unconfirmed updates can be restored and retried across resets. +- imgtool: added `--delta-base` and `--delta-block-size` for generating signed + delta images. +- simulator: added reset injection for forward apply and restore, target + validation repair, and security-counter confirmation coverage. diff --git a/scripts/imgtool/delta.py b/scripts/imgtool/delta.py new file mode 100644 index 0000000000..bc439cbef5 --- /dev/null +++ b/scripts/imgtool/delta.py @@ -0,0 +1,153 @@ +# SPDX-License-Identifier: Apache-2.0 + +import os.path +import struct +from dataclasses import dataclass + +from intelhex import IntelHex + +from .image import IMAGE_MAGIC, TLV_INFO_MAGIC, TLV_PROT_INFO_MAGIC, align_up + +DELTA_MAGIC = 0x314c444d # "MDL1" +DELTA_VERSION = 1 +DELTA_FLAG_RESTORE = 0x00000001 +DELTA_HEADER_SIZE = 32 +DELTA_HEADER = ''}[endian] + + +def _image_layout(data, endian): + e = _struct_endian(endian) + hdr = struct.unpack(e + 'IIHHII', data[:20]) + magic = hdr[0] + hdr_size = hdr[2] + prot_tlv_size = hdr[3] + img_size = hdr[4] + + if magic != IMAGE_MAGIC: + raise ValueError('base image does not contain a valid MCUboot header') + + hash_end = hdr_size + img_size + prot_tlv_size + if hash_end > len(data): + raise ValueError('image is truncated before the protected TLV area') + + total_end = hash_end + if total_end + 4 <= len(data): + tlv_magic, tlv_tot = struct.unpack(e + 'HH', data[total_end:total_end + 4]) + if tlv_magic == TLV_INFO_MAGIC: + total_end += tlv_tot + elif tlv_magic == TLV_PROT_INFO_MAGIC: + raise ValueError('unexpected protected TLV at unprotected TLV offset') + + if total_end > len(data): + raise ValueError('image is truncated before the unprotected TLV area ends') + + return hash_end, total_end + + +def image_hash(data, hash_algorithm, endian): + hash_end, _ = _image_layout(data, endian) + digest = hash_algorithm() + digest.update(data[:hash_end]) + return digest.digest() + + +def image_core(data, endian): + _, total_end = _image_layout(data, endian) + return data[:total_end] + + +def build_delta(base_image, target_image, block_size, erased_val, hash_algorithm, + endian): + if block_size <= 0 or block_size % 4 != 0 or (block_size & (block_size - 1)) != 0: + raise ValueError('--delta-block-size must be a positive power-of-two multiple of 4') + + base_core = image_core(base_image, endian) + target_core = image_core(target_image, endian) + compare_size = align_up(max(len(base_core), len(target_core)), block_size) + erased = bytes([erased_val]) + base_cmp = base_core.ljust(compare_size, erased) + target_cmp = target_core.ljust(compare_size, erased) + records = [] + run_offset = None + run_new_data = bytearray() + run_old_data = bytearray() + + def flush_run(): + nonlocal run_offset + + if run_offset is None: + return + + records.append((run_offset, bytes(run_new_data), bytes(run_old_data))) + run_offset = None + run_new_data.clear() + run_old_data.clear() + + for off in range(0, compare_size, block_size): + base_block = base_cmp[off:off + block_size] + target_block = target_cmp[off:off + block_size] + + if base_block == target_block: + flush_run() + continue + + if run_offset is None: + run_offset = off + + run_new_data.extend(target_block) + run_old_data.extend(base_block) + + flush_run() + + payload = bytearray(struct.pack( + DELTA_HEADER, + DELTA_MAGIC, + DELTA_VERSION, + DELTA_HEADER_SIZE, + len(target_core), + compare_size, + len(records), + block_size, + DELTA_FLAG_RESTORE, + len(base_core), + )) + + for off, new_data, old_data in records: + payload.extend(struct.pack(DELTA_RECORD, off, len(new_data))) + payload.extend(new_data) + payload.extend(old_data) + payload.extend(bytes(align_up(len(payload), 4) - len(payload))) + + return DeltaPayload( + payload=bytes(payload), + base_hash=image_hash(base_core, hash_algorithm, endian), + target_hash=image_hash(target_core, hash_algorithm, endian), + target_size=len(target_core), + write_size=compare_size, + record_count=len(records), + ) diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py index 06d9caf082..d84c54d8ad 100755 --- a/scripts/imgtool/image.py +++ b/scripts/imgtool/image.py @@ -66,6 +66,7 @@ 'COMPRESSED_LZMA1': 0x0000200, 'COMPRESSED_LZMA2': 0x0000400, 'COMPRESSED_ARM_THUMB': 0x0000800, + 'DELTA': 0x0001000, } TLV_VALUES = { @@ -89,10 +90,12 @@ 'BOOT_RECORD': 0x60, 'DECOMP_SIZE': 0x70, 'DECOMP_SHA': 0x71, + 'OUTPUT_SHA': 0x71, 'DECOMP_SIGNATURE': 0x72, 'COMP_DEC_SIZE' : 0x73, 'UUID_VID': 0x74, 'UUID_CID': 0x75, + 'DELTA_BASE_SHA': 0x76, } TLV_SIZE = 4 @@ -417,6 +420,20 @@ def load_compressed(self, data, compression_header): self.payload = bytes([0] * self.header_size) + \ self.payload + def load_payload(self, data): + """Load an already-built payload from a byte buffer.""" + self.payload = data + self.image_size = len(self.payload) + + if self.header_size > 0: + if self.pad_header: + if self.base_addr: + self.base_addr -= self.header_size + self.payload = bytes([self.erased_val] * self.header_size) + \ + self.payload + else: + self.payload = bytes([0] * self.header_size) + self.payload + def save(self, path, hex_addr=None): """Save an image from a given file""" ext = os.path.splitext(path)[1][1:].lower() @@ -464,14 +481,16 @@ def check_header(self): "Header padding was not requested and image does not start with zeros" ) - def check_trailer(self): + def check_trailer(self, minimum_trailer_size=0): if self.slot_size > 0: tsize = self._trailer_size(self.align, self.max_sectors, self.overwrite_only, self.enckey, self.save_enctlv, self.enctlv_len) + tsize = max(tsize, minimum_trailer_size) padding = self.slot_size - (len(self.payload) + tsize) if padding < 0: - msg = f"Image size (0x{len(self.payload):x}) + trailer (0x{tsize:x}) exceeds " \ + msg = f"Image size (0x{len(self.payload):x}) + reserved trailer area " \ + f"(0x{tsize:x}) exceeds " \ f"requested size 0x{self.slot_size:x}" raise click.UsageError(msg) @@ -517,7 +536,7 @@ def create(self, key, public_key_format, enckey, dependencies=None, compression_type=None, encrypt_keylen=128, clear=False, fixed_sig=None, pub_key=None, vector_to_sign=None, user_sha='auto', hmac_sha='auto', is_pure=False, keep_comp_size=False, - dont_encrypt=False): + dont_encrypt=False, delta_tlvs=None): self.enckey = enckey # key decides on sha, then pub_key; of both are none default is used @@ -594,6 +613,9 @@ def create(self, key, public_key_format, enckey, dependencies=None, if compression_tlvs is not None: for value in compression_tlvs.values(): protected_tlv_size += TLV_SIZE + len(value) + if delta_tlvs is not None: + for value in delta_tlvs.values(): + protected_tlv_size += TLV_SIZE + len(value) if custom_tlvs is not None: for value in custom_tlvs.values(): protected_tlv_size += TLV_SIZE + len(value) @@ -620,6 +642,8 @@ def create(self, key, public_key_format, enckey, dependencies=None, compression_flags = IMAGE_F['COMPRESSED_LZMA2'] if compression_type == "lzma2armthumb": compression_flags |= IMAGE_F['COMPRESSED_ARM_THUMB'] + if delta_tlvs is not None: + compression_flags |= IMAGE_F['DELTA'] # This adds the header to the payload as well if encrypt_keylen == 256: self.add_header(enckey, protected_tlv_size, compression_flags, 256) @@ -658,6 +682,10 @@ def create(self, key, public_key_format, enckey, dependencies=None, for tag, value in compression_tlvs.items(): prot_tlv.add(tag, value) + if delta_tlvs is not None: + for tag, value in delta_tlvs.items(): + prot_tlv.add(tag, value) + if self.vid is not None: vid = parse_uuid(uuid.NAMESPACE_DNS, self.vid) payload = struct.pack(e + '16s', vid) diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py index a9d9acbc43..d789cbf11f 100755 --- a/scripts/imgtool/main.py +++ b/scripts/imgtool/main.py @@ -28,7 +28,7 @@ import click import imgtool.keys as keys -from imgtool import image, imgtool_version +from imgtool import delta, image, imgtool_version from imgtool.dumpinfo import dump_imginfo from imgtool.version import decode_version @@ -432,6 +432,16 @@ def convert(self, value, param, ctx): help='Enable image compression using specified type. ' 'Will fall back without image compression automatically ' 'if the compression increases the image size.') +@click.option('--delta-base', metavar='filename', + type=click.Path(exists=True, dir_okay=False), + help='Create a robust signed reversible delta image against an ' + 'already-signed base image instead of a full update image.') +@click.option('--delta-block-size', default='16', type=BasedIntParamType(), + help='Granularity, in bytes, used when comparing base and ' + 'target images for --delta-base. Use the target flash write ' + 'alignment, or an erase-block size that covers primary ' + 'records and reserves the primary and secondary trailer ' + 'blocks.') @click.option('-c', '--clear', required=False, is_flag=True, default=False, help='Output a non-encrypted image with encryption capabilities,' 'so it can be installed in the primary slot, and encrypted ' @@ -516,7 +526,8 @@ def convert(self, value, param, ctx): help='Unique image class identifier, format: (|)') def sign(key, public_key_format, align, version, pad_sig, header_size, pad_header, slot_size, pad, confirm, test, max_sectors, overwrite_only, - endian, encrypt_keylen, encrypt, compression, infile, outfile, + endian, encrypt_keylen, encrypt, compression, delta_base, delta_block_size, + infile, outfile, dependencies, load_addr, hex_addr, erased_val, save_enctlv, security_counter, boot_record, custom_tlv, custom_tlv_file, rom_fixed, max_align, clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, hmac_sha, is_pure, @@ -600,7 +611,80 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, 'Pure signatures, currently, enforces preferred hash algorithm, ' 'and forbids sha selection by user.') - if compression in ["lzma2", "lzma2armthumb"]: + if delta_base is not None: + delta_block_size = int(delta_block_size) + if compression != "disabled": + raise click.UsageError('--delta-base cannot be combined with --compression') + if encrypt: + raise click.UsageError('--delta-base cannot be combined with --encrypt') + if fix_sig: + raise click.UsageError('--delta-base does not support --fix-sig') + if vector_to_sign: + raise click.UsageError('--delta-base does not support --vector-to-sign') + if is_pure: + raise click.UsageError('--delta-base does not support --pure signatures') + + target_img = image.Image(version=decode_version(version), + header_size=header_size, pad_header=pad_header, + pad=False, confirm=False, test=False, align=int(align), + slot_size=slot_size, max_sectors=max_sectors, + overwrite_only=overwrite_only, endian=endian, + load_addr=load_addr, rom_fixed=rom_fixed, + erased_val=erased_val, save_enctlv=save_enctlv, + security_counter=security_counter, max_align=max_align, + non_bootable=non_bootable, vid=vid, cid=cid) + target_img.load(infile) + target_img.create(key, public_key_format, None, dependencies, boot_record, + custom_tlvs, None, None, int(encrypt_keylen), clear, + baked_signature, pub_key, None, user_sha=user_sha, + hmac_sha=hmac_sha, is_pure=is_pure) + target_img.check_trailer(delta_block_size) + + check_key = key if key is not None else pub_key + hash_algorithm, _ = image.key_and_user_sha_to_alg_and_tlv( + check_key, user_sha, is_pure) + try: + delta_payload = delta.build_delta( + delta.load_image_bytes(delta_base), + bytes(target_img.payload), + delta_block_size, + target_img.erased_val, + hash_algorithm, + endian) + except ValueError as err: + raise click.UsageError(str(err)) from None + + if delta_payload.write_size + delta_block_size > slot_size: + raise click.UsageError( + f"Delta write span (0x{delta_payload.write_size:x}) + " + f"reserved trailer area (0x{delta_block_size:x}) exceeds " + f"requested size 0x{slot_size:x}") + + delta_tlvs = { + "DELTA_BASE_SHA": delta_payload.base_hash, + "OUTPUT_SHA": delta_payload.target_hash, + } + img = image.Image(version=decode_version(version), + header_size=header_size, pad_header=pad_header, + pad=pad, confirm=confirm, test=test, align=int(align), + slot_size=slot_size, max_sectors=max_sectors, + overwrite_only=overwrite_only, endian=endian, + load_addr=load_addr, rom_fixed=rom_fixed, + erased_val=erased_val, save_enctlv=save_enctlv, + security_counter=security_counter, max_align=max_align, + non_bootable=False, vid=vid, cid=cid) + img.load_payload(delta_payload.payload) + img.create(key, public_key_format, None, dependencies, boot_record, + custom_tlvs, None, None, int(encrypt_keylen), clear, + None, pub_key, None, user_sha=user_sha, hmac_sha=hmac_sha, + is_pure=is_pure, delta_tlvs=delta_tlvs) + img.check_trailer(delta_block_size) + print(f"delta target image size: {delta_payload.target_size} bytes") + print(f"delta write span: {delta_payload.write_size} bytes") + print(f"delta record count: {delta_payload.record_count}") + print("delta restore data: included") + print(f"delta payload size: {len(delta_payload.payload)} bytes") + elif compression in ["lzma2", "lzma2armthumb"]: img.create(key, public_key_format, enckey, dependencies, boot_record, custom_tlvs, compression_tlvs, None, int(encrypt_keylen), clear, baked_signature, pub_key, vector_to_sign, user_sha=user_sha, @@ -631,9 +715,9 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, print(f"original image size: {uncompressed_size} bytes") compression_tlvs["DECOMP_SIZE"] = struct.pack( img.get_struct_endian() + 'L', img.image_size) - compression_tlvs["DECOMP_SHA"] = img.image_hash + compression_tlvs["OUTPUT_SHA"] = img.image_hash compression_tlvs_size = len(compression_tlvs["DECOMP_SIZE"]) - compression_tlvs_size += len(compression_tlvs["DECOMP_SHA"]) + compression_tlvs_size += len(compression_tlvs["OUTPUT_SHA"]) if img.get_signature(): compression_tlvs["DECOMP_SIGNATURE"] = img.get_signature() compression_tlvs_size += len(compression_tlvs["DECOMP_SIGNATURE"]) diff --git a/scripts/tests/test_delta.py b/scripts/tests/test_delta.py new file mode 100644 index 0000000000..14c19e2b3c --- /dev/null +++ b/scripts/tests/test_delta.py @@ -0,0 +1,178 @@ +# SPDX-License-Identifier: Apache-2.0 + +import struct +from pathlib import Path + +from click.testing import CliRunner +from imgtool import delta, image +from imgtool.main import imgtool + +VERSION_BASE = '1.0.0' +VERSION_TARGET = '2.0.0' +HEADER_SIZE = 0x200 +SLOT_SIZE = 0x20000 + + +def sign_image(in_file: Path, out_file: Path, version: str, + *extra_args: str, slot_size: int = SLOT_SIZE): + runner = CliRunner() + result = runner.invoke( + imgtool, + [ + 'sign', + str(in_file), + str(out_file), + f'--header-size={HEADER_SIZE}', + f'--slot-size={slot_size}', + f'--version={version}', + '--pad-header', + *extra_args, + ], + ) + assert result.exit_code == 0, result.output + assert out_file.exists() + return result + + +def image_flags(path: Path) -> int: + data = path.read_bytes() + return struct.unpack(' bytes: + patch = patch_signed.read_bytes() + hdr_size, img_size = struct.unpack('= target_size + assert not reverse or flags & delta.DELTA_FLAG_RESTORE + + base_core = delta.image_core(base_signed.read_bytes(), 'little') + out = bytearray(base_core.ljust(write_size, b'\xff')) + off = delta_header_size + + for _ in range(record_count): + rec_off, rec_size = struct.unpack(delta.DELTA_RECORD, patch_payload[off:off + 8]) + off += 8 + new_data_off = off + old_data_off = new_data_off + rec_size + source_off = old_data_off if reverse else new_data_off + out[rec_off:rec_off + rec_size] = patch_payload[source_off:source_off + rec_size] + off += rec_size + off += rec_size + off = image.align_up(off, 4) + + assert off == len(patch_payload) + return bytes(out[:base_size if reverse else target_size]) + + +def test_delta_image_reconstructs_signed_target_and_base(tmp_path: Path): + base_raw = tmp_path / 'base.bin' + target_raw = tmp_path / 'target.bin' + base_signed = tmp_path / 'base-signed.bin' + target_signed = tmp_path / 'target-signed.bin' + patch_signed = tmp_path / 'target-delta.bin' + + base = bytearray(b'A' * 8192) + target = bytearray(base) + target[1024:1040] = b'delta-dfu-target' + target[4096:4112] = b'mcuboot-delta!!' + + base_raw.write_bytes(base) + target_raw.write_bytes(target) + + sign_image(base_raw, base_signed, VERSION_BASE) + sign_image(target_raw, target_signed, VERSION_TARGET) + result = sign_image( + target_raw, + patch_signed, + VERSION_TARGET, + f'--delta-base={base_signed}', + '--delta-block-size=16', + '--overwrite-only', + ) + + assert 'delta payload size:' in result.output + assert 'delta restore data: included' in result.output + assert 'delta write span:' in result.output + assert image_flags(patch_signed) & image.IMAGE_F['DELTA'] + assert patch_signed.stat().st_size < target_signed.stat().st_size + assert apply_delta(base_signed, patch_signed) == \ + delta.image_core(target_signed.read_bytes(), 'little') + assert apply_delta(target_signed, patch_signed, reverse=True) == \ + delta.image_core(base_signed.read_bytes(), 'little') + + +def test_delta_image_fails_when_patch_exceeds_slot(tmp_path: Path): + base_raw = tmp_path / 'base.bin' + target_raw = tmp_path / 'target.bin' + base_signed = tmp_path / 'base-signed.bin' + target_signed = tmp_path / 'target-signed.bin' + patch_signed = tmp_path / 'target-delta.bin' + runner = CliRunner() + + base_raw.write_bytes(b'A' * (70 * 1024)) + target_raw.write_bytes(b'B' * (70 * 1024)) + + sign_image(base_raw, base_signed, VERSION_BASE, '--overwrite-only') + sign_image(target_raw, target_signed, VERSION_TARGET, '--overwrite-only') + assert target_signed.stat().st_size < SLOT_SIZE + + result = runner.invoke( + imgtool, + [ + 'sign', + str(target_raw), + str(patch_signed), + f'--header-size={HEADER_SIZE}', + f'--slot-size={SLOT_SIZE}', + f'--version={VERSION_TARGET}', + '--pad-header', + f'--delta-base={base_signed}', + '--delta-block-size=16', + '--overwrite-only', + ], + ) + + assert result.exit_code != 0 + assert 'exceeds requested size' in result.output + assert not patch_signed.exists() + + +def test_delta_image_reserves_secondary_trailer_sector(tmp_path: Path): + base_raw = tmp_path / 'base.bin' + target_raw = tmp_path / 'target.bin' + base_signed = tmp_path / 'base-signed.bin' + patch_signed = tmp_path / 'target-delta.bin' + runner = CliRunner() + slot_size = 0x3000 + + base_raw.write_bytes(b'A' * 2048) + target_raw.write_bytes(b'B' * 2048) + sign_image(base_raw, base_signed, VERSION_BASE, '--overwrite-only', + slot_size=slot_size) + + result = runner.invoke( + imgtool, + [ + 'sign', + str(target_raw), + str(patch_signed), + f'--header-size={HEADER_SIZE}', + f'--slot-size={slot_size}', + f'--version={VERSION_TARGET}', + '--pad-header', + f'--delta-base={base_signed}', + '--delta-block-size=4096', + '--overwrite-only', + ], + ) + + assert result.exit_code != 0 + assert 'reserved trailer area' in result.output + assert not patch_signed.exists() diff --git a/sim/Cargo.toml b/sim/Cargo.toml index cb206e0437..31ab8ca0b9 100644 --- a/sim/Cargo.toml +++ b/sim/Cargo.toml @@ -17,6 +17,7 @@ sig-p384 = ["mcuboot-sys/sig-p384"] sig-ed25519 = ["mcuboot-sys/sig-ed25519"] sig-second-key = ["mcuboot-sys/sig-second-key"] overwrite-only = ["mcuboot-sys/overwrite-only"] +delta-dfu = ["mcuboot-sys/delta-dfu"] swap-offset = ["mcuboot-sys/swap-offset"] swap-move = ["mcuboot-sys/swap-move"] validate-primary-slot = ["mcuboot-sys/validate-primary-slot"] diff --git a/sim/mcuboot-sys/Cargo.toml b/sim/mcuboot-sys/Cargo.toml index 26e98cddaf..73de21f065 100644 --- a/sim/mcuboot-sys/Cargo.toml +++ b/sim/mcuboot-sys/Cargo.toml @@ -42,6 +42,9 @@ sig-second-key = [] # Overwrite only upgrade overwrite-only = [] +# Delta DFU support for overwrite-only upgrades +delta-dfu = [] + # Swap using offset mode swap-offset = [] diff --git a/sim/mcuboot-sys/build.rs b/sim/mcuboot-sys/build.rs index cb7d449ca3..c584304ba0 100644 --- a/sim/mcuboot-sys/build.rs +++ b/sim/mcuboot-sys/build.rs @@ -21,6 +21,7 @@ fn main() { let sig_ed25519 = env::var("CARGO_FEATURE_SIG_ED25519").is_ok(); let sig_second_key = env::var("CARGO_FEATURE_SIG_SECOND_KEY").is_ok(); let overwrite_only = env::var("CARGO_FEATURE_OVERWRITE_ONLY").is_ok(); + let delta_dfu = env::var("CARGO_FEATURE_DELTA_DFU").is_ok(); let swap_move = env::var("CARGO_FEATURE_SWAP_MOVE").is_ok(); let swap_offset = env::var("CARGO_FEATURE_SWAP_OFFSET").is_ok(); let validate_primary_slot = @@ -357,6 +358,20 @@ fn main() { conf.conf.define("MCUBOOT_OVERWRITE_ONLY", None); } + if delta_dfu { + if !overwrite_only { + panic!("Delta DFU requires overwrite-only"); + } + if enc_rsa || enc_aes256_rsa || enc_kw || enc_aes256_kw || enc_ec256 || + enc_ec256_mbedtls || enc_aes256_ec256 || enc_x25519 || enc_aes256_x25519 || + custom_enc_crypto { + panic!("Delta DFU does not support encrypted images"); + } + conf.conf.define("MCUBOOT_DELTA_DFU", None); + conf.conf.define("MCUBOOT_DELTA_SECTOR_BUF_SIZE", Some("65536")); + conf.file("../../boot/bootutil/src/delta.c"); + } + if swap_offset { conf.conf.define("MCUBOOT_SWAP_USING_OFFSET", None); } else if swap_move { diff --git a/sim/src/caps.rs b/sim/src/caps.rs index 2b98ab6f3b..782b91749b 100644 --- a/sim/src/caps.rs +++ b/sim/src/caps.rs @@ -31,6 +31,7 @@ pub enum Caps { HwRollbackProtection = (1 << 18), EcdsaP384 = (1 << 19), SwapUsingOffset = (1 << 20), + DeltaDfu = (1 << 21), } impl Caps { diff --git a/sim/src/image.rs b/sim/src/image.rs index d5fa19f2de..c48769ec44 100644 --- a/sim/src/image.rs +++ b/sim/src/image.rs @@ -18,6 +18,7 @@ use rand::{ Rng, RngCore, SeedableRng, rngs::SmallRng, }; +use ring::digest; use std::{ collections::{BTreeMap, HashSet}, io::{Cursor, Write}, mem, rc::Rc, slice }; @@ -395,6 +396,91 @@ impl ImagesBuilder { images } + pub fn try_make_delta_image(self) -> Option { + self.try_make_delta_image_with_size(ImageSize::Given(8192)) + } + + pub fn try_make_large_delta_image(self) -> Option { + self.try_make_delta_image_with_size(ImageSize::LargestDelta) + } + + fn try_make_delta_image_with_size(self, image_size: ImageSize) -> Option { + assert!(Caps::DeltaDfu.present()); + assert!(Caps::OverwriteUpgrade.present()); + + let ImagesBuilder { + mut flash, + areadesc, + slots, + ram, + } = self; + let ram_for_install = ram.clone(); + let mut images = Vec::with_capacity(slots.len()); + + for (image_num, slots) in slots.into_iter().enumerate() { + let dep = BoringDep::new(image_num, &NO_DEPS); + if let ImageSize::LargestDelta = image_size { + let trailer_sector = areadesc + .get_area_sectors(primary_flash_id(image_num)) + .unwrap() + .last() + .unwrap() + .size as usize; + if slots[0].len <= trailer_sector + 4096 { + return None; + } + } + let payload_seed = slots[0].base_off; + let primaries = install_image_with_payload_seed( + &mut flash, &areadesc, &slots, 0, image_size, &ram_for_install, + &dep, ImageManipulation::None, Some(0), payload_seed); + let target_size = match image_size { + ImageSize::Given(size) => ImageSize::Given(size), + ImageSize::LargestDelta => { + if slots[1].len < slots[0].len { + return None; + } + ImageSize::Given(u32::from_le_bytes( + primaries.plain[12..16].try_into().unwrap()) as usize) + } + ImageSize::Largest | ImageSize::Oversized => unreachable!(), + }; + let upgrades = install_image_with_payload_seed( + &mut flash, &areadesc, &slots, 1, target_size, &ram_for_install, + &dep, ImageManipulation::None, Some(1), payload_seed); + + if !install_delta_image(&mut flash, &areadesc, image_num, &slots, + &primaries, &upgrades, &dep, Some(1)) { + return None; + } + + images.push(OneImage { + slots, + primaries, + upgrades, + }); + } + + install_ptable(&mut flash, &areadesc); + let mut images = Images { + flash, + areadesc, + images, + total_count: None, + ram, + }; + + for image in &images.images { + mark_upgrade(&mut images.flash, &image.slots[1]); + } + + let total_count = images.run_basic_delta_upgrade() + .expect("Unable to perform basic delta upgrade"); + c::reset_security_counters(); + images.total_count = Some(total_count); + Some(images) + } + pub fn make_bad_secondary_slot_image(self, img_manipulation : ImageManipulation) -> Images { let mut bad_flash = self.flash; let ram = self.ram.clone(); // TODO: Avoid this clone. @@ -824,6 +910,18 @@ impl Images { } } + pub fn run_basic_delta_upgrade(&self) -> Option { + let (flash, total_count) = self.try_upgrade(None, true); + info!("Total delta flash operation count={}", total_count); + + if !self.verify_images(&flash, 0, 1) { + warn!("Image mismatch after delta boot"); + None + } else { + Some(total_count) + } + } + pub fn run_bootstrap(&self) -> bool { let mut flash = self.flash.clone(); let mut fails = 0; @@ -976,6 +1074,242 @@ impl Images { fails > 0 } + pub fn run_delta_with_fails(&self) -> bool { + if !Caps::DeltaDfu.present() { + return false; + } + + let total_flash_ops = self.total_count.unwrap(); + let mut fails = 0; + + if skip_slow_test() { + return false; + } + + for i in 1 ..= total_flash_ops { + info!("Try delta interruption at {}", i); + c::reset_security_counters(); + let (flash, _count) = self.try_upgrade(Some(i), true); + if !self.verify_images(&flash, 0, 1) { + warn!("Delta primary slot FAIL at step {} of {}", i, total_flash_ops); + fails += 1; + } + + if !self.delta_security_counters_are(0) { + warn!("Delta security counter changed before confirmation at step {}", i); + fails += 1; + } + + let mut reverted_flash = flash; + if !c::boot_go(&mut reverted_flash, &self.areadesc, None, None, false).success() { + warn!("Delta revert failed after interruption {}", i); + fails += 1; + } else if !self.verify_images(&reverted_flash, 0, 0) { + warn!("Delta revert did not restore the base image after interruption {}", i); + fails += 1; + } + + if !self.delta_security_counters_are(0) { + warn!("Delta security counter changed after revert at step {}", i); + fails += 1; + } + } + + c::reset_security_counters(); + let mut staged_flash = self.flash.clone(); + if !c::boot_go(&mut staged_flash, &self.areadesc, None, None, false).success() { + warn!("Unable to stage target image for delta revert interruption testing"); + fails += 1; + } else { + let mut count_flash = staged_flash.clone(); + let mut counter = 0; + if !c::boot_go(&mut count_flash, &self.areadesc, + Some(&mut counter), None, false).success() { + warn!("Unable to count delta revert flash operations"); + fails += 1; + } else { + let total_revert_ops = -counter; + for i in 1 ..= total_revert_ops { + info!("Try delta revert interruption at {}", i); + c::reset_security_counters(); + let mut flash = staged_flash.clone(); + let mut stop = i; + if !c::boot_go(&mut flash, &self.areadesc, + Some(&mut stop), None, false).interrupted() { + warn!("Delta revert did not stop at step {} of {}", + i, total_revert_ops); + fails += 1; + continue; + } + + if !c::boot_go(&mut flash, &self.areadesc, None, None, false).success() { + warn!("Delta revert did not resume at step {} of {}", + i, total_revert_ops); + fails += 1; + } else if !self.verify_images(&flash, 0, 0) { + warn!("Delta revert recovery did not restore the base image at step {} of {}", + i, total_revert_ops); + fails += 1; + } + + if !self.delta_security_counters_are(0) { + warn!("Delta security counter changed during revert at step {}", i); + fails += 1; + } + } + } + } + + c::reset_security_counters(); + if fails > 0 { + error!("{} delta interruption checks failed", fails); + } + + fails > 0 + } + + pub fn run_delta_large_image_revert(&self) -> bool { + if !Caps::DeltaDfu.present() { + return false; + } + + let mut flash = self.flash.clone(); + let mut fails = 0; + + if !c::boot_go(&mut flash, &self.areadesc, None, None, false).success() { + warn!("Failed large delta upgrade"); + fails += 1; + } else if !self.verify_images(&flash, 0, 1) { + warn!("Large delta upgrade did not reconstruct the target image"); + fails += 1; + } + + if !c::boot_go(&mut flash, &self.areadesc, None, None, false).success() { + warn!("Failed large delta revert"); + fails += 1; + } else if !self.verify_images(&flash, 0, 0) { + warn!("Large delta revert did not restore the base image"); + fails += 1; + } + + if fails > 0 { + error!("Error testing large delta image revert"); + } + + fails > 0 + } + + pub fn run_delta_repairs_invalid_target_tlv(&self) -> bool { + if !Caps::DeltaDfu.present() { + return false; + } + + let mut flash = self.flash.clone(); + let mut fails = 0; + + if !c::boot_go(&mut flash, &self.areadesc, None, None, false).success() { + warn!("Failed initial delta upgrade for target repair test"); + return true; + } + + for image in &self.images { + mark_upgrade(&mut flash, &image.slots[1]); + + let slot = &image.slots[0]; + let hash_end = image_hash_end(&image.upgrades.plain[..image.upgrades.size]); + let corrupt_off = slot.base_off + hash_end + 8; + let dev = flash.get_mut(&slot.dev_id).unwrap(); + let align = dev.align(); + let write_off = corrupt_off & !(align - 1); + let mut buf = vec![0; align]; + dev.read(write_off, &mut buf).unwrap(); + buf[corrupt_off - write_off] ^= 1; + dev.set_verify_writes(false); + dev.write(write_off, &buf).unwrap(); + dev.set_verify_writes(true); + } + + if !c::boot_go(&mut flash, &self.areadesc, None, None, false).success() { + warn!("Failed to repair a delta target with a damaged validation TLV"); + fails += 1; + } else if !self.verify_images(&flash, 0, 1) { + warn!("Delta target validation TLV was not reconstructed"); + fails += 1; + } + + if fails > 0 { + error!("Error testing delta target validation repair"); + } + + fails > 0 + } + + pub fn run_delta_confirm_updates_security_counter(&self) -> bool { + if !Caps::DeltaDfu.present() || !Caps::HwRollbackProtection.present() { + return false; + } + + let mut flash = self.flash.clone(); + let mut fails = 0; + + c::reset_security_counters(); + for image_index in 0 .. self.images.len() { + c::set_security_counter(image_index as u32, 0); + } + + if !c::boot_go(&mut flash, &self.areadesc, None, None, false).success() { + warn!("Failed initial delta boot"); + fails += 1; + } + + if !self.verify_images(&flash, 0, 1) { + warn!("Delta primary slot did not contain the target image"); + fails += 1; + } + + for image_index in 0 .. self.images.len() { + let counter_val = c::get_security_counter(image_index as u32); + if counter_val != 0 { + warn!("Counter for image {} changed before confirmation: {}", + image_index, counter_val); + fails += 1; + } + } + + for image in &self.images { + mark_confirmed_primary(&mut flash, &image.slots[0]); + } + + if !c::boot_go(&mut flash, &self.areadesc, None, None, false).success() { + warn!("Failed confirmed delta boot"); + fails += 1; + } + + for image_index in 0 .. self.images.len() { + let counter_val = c::get_security_counter(image_index as u32); + if counter_val != 1 { + warn!("Counter for image {} was not updated after confirmation: {}", + image_index, counter_val); + fails += 1; + } + } + + c::reset_security_counters(); + + if fails > 0 { + error!("Error testing confirmed delta security counter update"); + } + + fails > 0 + } + + fn delta_security_counters_are(&self, expected: u32) -> bool { + !Caps::HwRollbackProtection.present() || + (0 .. self.images.len()).all(|image_index| { + c::get_security_counter(image_index as u32) == expected + }) + } + pub fn run_perm_with_random_fails(&self, total_fails: usize) -> bool { if !Caps::modifies_flash() { return false; @@ -2050,13 +2384,15 @@ fn show_flash(flash: &dyn Flash) { println!(); } -#[derive(Debug)] +#[derive(Copy, Clone, Debug)] enum ImageSize { /// Make the image the specified given size. #[allow(dead_code)] Given(usize), /// Make the image as large as it can be for the partition/device. Largest, + /// Reserve the final erase sector for delta recovery state. + LargestDelta, /// Make the image quite larger than it can be for the partition/device/ Oversized, } @@ -2172,6 +2508,235 @@ fn compute_largest_image_size(dev: &dyn Flash, areadesc: &AreaDesc, slots: &[Slo slot_len - hdr_size - trailer - tlv_len - padding } +fn compute_largest_delta_image_size(dev: &dyn Flash, areadesc: &AreaDesc, + slots: &[SlotInfo], slot_ind: usize, + hdr_size: usize, tlv: &dyn ManifestGen) -> usize { + let slot_len = slots[0].len; + let slot_end = slots[0].base_off + slot_len; + let trailer_sector = dev.sector_iter() + .find(|sector| sector.base + sector.size == slot_end) + .unwrap() + .size; + let trailer = std::cmp::max(image_largest_trailer(dev, areadesc, &slots[slot_ind]), + trailer_sector); + let tlv_len = tlv.estimate_size(); + + slot_len.saturating_sub(hdr_size + trailer + tlv_len) +} + +struct SimDeltaPayload { + payload: Vec, + base_hash: Vec, + target_hash: Vec, +} + +const DELTA_MAGIC: u32 = 0x314c444d; +const DELTA_VERSION: u16 = 1; +const DELTA_HEADER_SIZE: u16 = 32; +const DELTA_FLAG_RESTORE: u32 = 0x00000001; + +fn image_hash_end(data: &[u8]) -> usize { + let hdr_size = u16::from_le_bytes(data[8..10].try_into().unwrap()) as usize; + let protect_tlv_size = u16::from_le_bytes(data[10..12].try_into().unwrap()) as usize; + let img_size = u32::from_le_bytes(data[12..16].try_into().unwrap()) as usize; + + hdr_size + img_size + protect_tlv_size +} + +fn image_core_end(data: &[u8]) -> usize { + let hash_end = image_hash_end(data); + + if hash_end + 4 <= data.len() { + let tlv_magic = u16::from_le_bytes(data[hash_end..hash_end + 2].try_into().unwrap()); + let tlv_total = u16::from_le_bytes(data[hash_end + 2..hash_end + 4].try_into().unwrap()) + as usize; + + if tlv_magic == 0x6907 { + return hash_end + tlv_total; + } + } + + hash_end +} + +fn image_hash(data: &[u8]) -> Vec { + let hash_end = image_hash_end(data); + let algorithm = if cfg!(feature = "sig-p384") { + &digest::SHA384 + } else { + &digest::SHA256 + }; + + digest::digest(algorithm, &data[..hash_end]).as_ref().to_vec() +} + +fn image_core(data: &[u8]) -> &[u8] { + &data[..image_core_end(data)] +} + +fn build_delta_payload(base: &ImageData, target: &ImageData, block_size: usize, + erased_val: u8) -> SimDeltaPayload { + assert!(block_size > 0 && block_size % 4 == 0 && (block_size & (block_size - 1)) == 0); + + let base_image = &base.plain[..base.size]; + let target_image = &target.plain[..target.size]; + let base_core = image_core(base_image); + let target_core = image_core(target_image); + let compare_size = align_up(std::cmp::max(base_core.len(), target_core.len()) as u32, + block_size as u32) as usize; + let mut base_cmp = vec![erased_val; compare_size]; + let mut target_cmp = vec![erased_val; compare_size]; + let mut records: Vec<(usize, Vec, Vec)> = vec![]; + let mut run_offset: Option = None; + let mut run_new_data: Vec = vec![]; + let mut run_old_data: Vec = vec![]; + + base_cmp[..base_core.len()].copy_from_slice(base_core); + target_cmp[..target_core.len()].copy_from_slice(target_core); + + for off in (0..compare_size).step_by(block_size) { + let base_block = &base_cmp[off..off + block_size]; + let target_block = &target_cmp[off..off + block_size]; + + if base_block == target_block { + if let Some(start) = run_offset.take() { + records.push((start, std::mem::take(&mut run_new_data), + std::mem::take(&mut run_old_data))); + } + continue; + } + + if run_offset.is_none() { + run_offset = Some(off); + } + run_new_data.extend_from_slice(target_block); + run_old_data.extend_from_slice(base_block); + } + + if let Some(start) = run_offset.take() { + records.push((start, run_new_data, run_old_data)); + } + + let mut payload = vec![]; + payload.write_u32::(DELTA_MAGIC).unwrap(); + payload.write_u16::(DELTA_VERSION).unwrap(); + payload.write_u16::(DELTA_HEADER_SIZE).unwrap(); + payload.write_u32::(target_core.len() as u32).unwrap(); + payload.write_u32::(compare_size as u32).unwrap(); + payload.write_u32::(records.len() as u32).unwrap(); + payload.write_u32::(block_size as u32).unwrap(); + payload.write_u32::(DELTA_FLAG_RESTORE).unwrap(); + payload.write_u32::(base_core.len() as u32).unwrap(); + + for (off, new_data, old_data) in records { + payload.write_u32::(off as u32).unwrap(); + payload.write_u32::(new_data.len() as u32).unwrap(); + payload.extend_from_slice(&new_data); + payload.extend_from_slice(&old_data); + while payload.len() % 4 != 0 { + payload.push(0); + } + } + + SimDeltaPayload { + payload, + base_hash: image_hash(base_image), + target_hash: image_hash(target_image), + } +} + +fn primary_flash_id(image_num: usize) -> FlashId { + match image_num { + 0 => FlashId::Image0, + 1 => FlashId::Image2, + _ => panic!("More than 2 images not supported"), + } +} + +fn secondary_flash_id(image_num: usize) -> FlashId { + match image_num { + 0 => FlashId::Image1, + 1 => FlashId::Image3, + _ => panic!("More than 2 images not supported"), + } +} + +fn primary_delta_block_size(areadesc: &AreaDesc, image_num: usize) -> usize { + areadesc.get_area_sectors(primary_flash_id(image_num)).unwrap() + .iter() + .map(|sector| sector.size as usize) + .max() + .unwrap() +} + +fn install_delta_image(flash: &mut SimMultiFlash, areadesc: &AreaDesc, + image_num: usize, slots: &[SlotInfo], + base: &ImageData, target: &ImageData, deps: &dyn Depender, + security_counter: Option) -> bool { + const HDR_SIZE: usize = 32; + const SIM_DELTA_SECTOR_BUF_SIZE: usize = 65536; + + let (block_size, erased_val) = { + let dev = flash.get(&slots[0].dev_id).unwrap(); + (primary_delta_block_size(areadesc, image_num), dev.erased_val()) + }; + if block_size > SIM_DELTA_SECTOR_BUF_SIZE { + return false; + } + + let delta = build_delta_payload(base, target, block_size, erased_val); + let slot = &slots[1]; + let offset = slot.base_off; + let dev_id = slot.dev_id; + let trailer_sector_size = areadesc + .get_area_sectors(secondary_flash_id(image_num)) + .unwrap() + .last() + .unwrap() + .size as usize; + let trailer_sector_off = slot.len - trailer_sector_size; + let dev = flash.get_mut(&dev_id).unwrap(); + let mut tlv: Box = Box::new(make_tlv(SigningKey::Primary)); + + tlv.set_security_counter(security_counter); + tlv.set_delta_hashes(delta.base_hash, delta.target_hash); + + let header = ImageHeader { + magic: tlv.get_magic(), + load_addr: 0, + hdr_size: HDR_SIZE as u16, + protect_tlv_size: tlv.protect_size(), + img_size: delta.payload.len() as u32, + flags: tlv.get_flags(), + ver: deps.my_version(offset, slot.index), + _pad2: 0, + }; + + let mut b_header = [0; HDR_SIZE]; + b_header[..32].clone_from_slice(header.as_raw()); + + tlv.add_bytes(&b_header); + tlv.add_bytes(&delta.payload); + let mut b_tlv = tlv.make_tlv(); + + let mut buf = vec![]; + buf.append(&mut b_header.to_vec()); + buf.extend_from_slice(&delta.payload); + buf.append(&mut b_tlv); + + while buf.len() % dev.align() != 0 { + buf.push(dev.erased_val()); + } + + if buf.len() > trailer_sector_off { + return false; + } + + dev.erase(offset, slot.len).unwrap(); + dev.write(offset, &buf).unwrap(); + true +} + /// Install a "program" into the given image. This fakes the image header, or at least all of the /// fields used by the given code. Returns a copy of the image that was written. fn install_image(flash: &mut SimMultiFlash, areadesc: &AreaDesc, slots: &[SlotInfo], @@ -2191,6 +2756,33 @@ fn install_image(flash: &mut SimMultiFlash, areadesc: &AreaDesc, slots: &[SlotIn ) } +fn install_image_with_payload_seed( + flash: &mut SimMultiFlash, + areadesc: &AreaDesc, + slots: &[SlotInfo], + slot_ind: usize, + len: ImageSize, + ram: &RamData, + deps: &dyn Depender, + img_manipulation: ImageManipulation, + security_counter: Option, + payload_seed: usize, +) -> ImageData { + install_image_with_key_and_payload_seed( + flash, + areadesc, + slots, + slot_ind, + len, + ram, + deps, + img_manipulation, + security_counter, + SigningKey::Primary, + Some(payload_seed), + ) +} + fn install_image_with_key( flash: &mut SimMultiFlash, areadesc: &AreaDesc, @@ -2202,6 +2794,34 @@ fn install_image_with_key( img_manipulation: ImageManipulation, security_counter: Option, signing_key: SigningKey, +) -> ImageData { + install_image_with_key_and_payload_seed( + flash, + areadesc, + slots, + slot_ind, + len, + ram, + deps, + img_manipulation, + security_counter, + signing_key, + None, + ) +} + +fn install_image_with_key_and_payload_seed( + flash: &mut SimMultiFlash, + areadesc: &AreaDesc, + slots: &[SlotInfo], + slot_ind: usize, + len: ImageSize, + ram: &RamData, + deps: &dyn Depender, + img_manipulation: ImageManipulation, + security_counter: Option, + signing_key: SigningKey, + payload_seed: Option, ) -> ImageData { let slot = &slots[slot_ind]; let mut offset = slot.base_off; @@ -2257,6 +2877,8 @@ fn install_image_with_key( ImageSize::Given(size) => size, ImageSize::Largest => compute_largest_image_size(dev, areadesc, slots, slot_ind, HDR_SIZE, tlv.as_ref()), + ImageSize::LargestDelta => compute_largest_delta_image_size( + dev, areadesc, slots, slot_ind, HDR_SIZE, tlv.as_ref()), ImageSize::Oversized => { let largest_img_sz = compute_largest_image_size(dev, areadesc, slots, slot_ind, HDR_SIZE, tlv.as_ref()); @@ -2284,7 +2906,7 @@ fn install_image_with_key( // The core of the image itself is just pseudorandom data. let mut b_img = vec![0; len]; - splat(&mut b_img, offset); + splat(&mut b_img, payload_seed.unwrap_or(offset)); // Add some information at the start of the payload to make it easier // to see what it is. This will fail if the image itself is too small. @@ -2755,6 +3377,15 @@ fn mark_permanent_upgrade(flash: &mut SimMultiFlash, slot: &SlotInfo) { dev.write(off, &ok).unwrap(); } +fn mark_confirmed_primary(flash: &mut SimMultiFlash, slot: &SlotInfo) { + let dev = flash.get_mut(&slot.dev_id).unwrap(); + let align = dev.align(); + let mut ok = vec![dev.erased_val(); align]; + ok[0] = 1u8; + let off = slot.trailer_off + c::boot_max_align() * 3; + dev.write(off, &ok).unwrap(); +} + // Drop some pseudo-random gibberish onto the data. fn splat(data: &mut [u8], seed: usize) { let mut seed_block = [0u8; 32]; diff --git a/sim/src/tlv.rs b/sim/src/tlv.rs index f1263d9a39..91397f086d 100644 --- a/sim/src/tlv.rs +++ b/sim/src/tlv.rs @@ -62,6 +62,8 @@ pub enum TlvKinds { ENCX25519 = 0x33, DEPENDENCY = 0x40, SECCNT = 0x50, + OutputSha = 0x71, + DeltaBaseSha = 0x76, } #[allow(dead_code, non_camel_case_types)] @@ -71,6 +73,7 @@ pub enum TlvFlags { ENCRYPTED_AES128 = 0x04, ENCRYPTED_AES256 = 0x08, RAM_LOAD = 0x20, + DELTA = 0x1000, } /// A generator for manifests. The format of the manifest can be either a @@ -118,6 +121,9 @@ pub trait ManifestGen { /// Sets the ignore_ram_load_flag so that can be validated when it is missing, /// it will not load successfully. fn set_ignore_ram_load_flag(&mut self); + + /// Add protected TLVs that bind a delta image to its base and target images. + fn set_delta_hashes(&mut self, base_hash: Vec, target_hash: Vec); } /// Selects which signing key to use when generating the TLV signature. @@ -151,6 +157,8 @@ pub struct TlvGen { ignore_ram_load_flag: bool, /// Which signing key to use. signing_key: SigningKey, + /// Base and target image hashes for delta images. + delta_hashes: Option, } #[derive(Debug)] @@ -159,6 +167,12 @@ struct Dependency { version: ImageVersion, } +#[derive(Clone, Debug)] +struct DeltaHashes { + base: Vec, + target: Vec, +} + impl TlvGen { /// Builder: select which signing key the generator will use. Has no /// effect on non-signing TLV kinds. @@ -358,11 +372,16 @@ impl ManifestGen for TlvGen { /// Retrieve the header flags for this configuration. This can be called at any time. fn get_flags(&self) -> u32 { // For the RamLoad case, add in the flag for this feature. + let mut flags = self.flags; + if Caps::RamLoad.present() && !self.ignore_ram_load_flag { - self.flags | (TlvFlags::RAM_LOAD as u32) - } else { - self.flags + flags |= TlvFlags::RAM_LOAD as u32; + } + if self.delta_hashes.is_some() { + flags |= TlvFlags::DELTA as u32; } + + flags } /// Add bytes to the covered hash. @@ -372,14 +391,20 @@ impl ManifestGen for TlvGen { fn protect_size(&self) -> u16 { let mut size = 0; - if !self.dependencies.is_empty() || (Caps::HwRollbackProtection.present() && self.security_cnt.is_some()) { + // add space for each dependency. + size += (self.dependencies.len() as u16) * + (4 + std::mem::size_of::() as u16); + if Caps::HwRollbackProtection.present() && self.security_cnt.is_some() { + size += 4 + 4; + } + if self.delta_hashes.is_some() { + let delta_hashes = self.delta_hashes.as_ref().unwrap(); + size += (4 + delta_hashes.base.len() as u16) + + (4 + delta_hashes.target.len() as u16); + } + if size != 0 { // include the TLV area header. size += 4; - // add space for each dependency. - size += (self.dependencies.len() as u16) * (4 + std::mem::size_of::() as u16); - if Caps::HwRollbackProtection.present() && self.security_cnt.is_some() { - size += 4 + 4; - } } size } @@ -489,6 +514,15 @@ impl ManifestGen for TlvGen { protected_tlv.write_u32::(self.security_cnt.unwrap() as u32).unwrap(); } + if let Some(delta_hashes) = &self.delta_hashes { + protected_tlv.write_u16::(TlvKinds::DeltaBaseSha as u16).unwrap(); + protected_tlv.write_u16::(delta_hashes.base.len() as u16).unwrap(); + protected_tlv.extend_from_slice(&delta_hashes.base); + protected_tlv.write_u16::(TlvKinds::OutputSha as u16).unwrap(); + protected_tlv.write_u16::(delta_hashes.target.len() as u16).unwrap(); + protected_tlv.extend_from_slice(&delta_hashes.target); + } + assert_eq!(size, protected_tlv.len() as u16, "protected TLV length incorrect"); } @@ -888,6 +922,13 @@ impl ManifestGen for TlvGen { fn set_ignore_ram_load_flag(&mut self) { self.ignore_ram_load_flag = true; } + + fn set_delta_hashes(&mut self, base_hash: Vec, target_hash: Vec) { + self.delta_hashes = Some(DeltaHashes { + base: base_hash, + target: target_hash, + }); + } } include!("rsa_pub_key-rs.txt"); diff --git a/sim/tests/core.rs b/sim/tests/core.rs index 516ce61b5c..7317b3f755 100644 --- a/sim/tests/core.rs +++ b/sim/tests/core.rs @@ -58,6 +58,34 @@ sim_test!(basic_revert, make_image(&NO_DEPS, true), run_basic_revert()); sim_test!(revert_with_fails, make_image(&NO_DEPS, false), run_revert_with_fails()); sim_test!(perm_with_fails, make_image(&NO_DEPS, true), run_perm_with_fails()); sim_test!(perm_with_random_fails, make_image(&NO_DEPS, true), run_perm_with_random_fails(5)); +#[cfg(feature = "delta-dfu")] +test_shell!(delta_with_fails, r, { + if let Some(image) = r.try_make_delta_image() { + dump_image(&image, "delta_with_fails"); + assert!(!image.run_delta_with_fails()); + } +}); +#[cfg(feature = "delta-dfu")] +test_shell!(delta_large_image_revert, r, { + if let Some(image) = r.try_make_large_delta_image() { + dump_image(&image, "delta_large_image_revert"); + assert!(!image.run_delta_large_image_revert()); + } +}); +#[cfg(feature = "delta-dfu")] +test_shell!(delta_repairs_invalid_target_tlv, r, { + if let Some(image) = r.try_make_delta_image() { + dump_image(&image, "delta_repairs_invalid_target_tlv"); + assert!(!image.run_delta_repairs_invalid_target_tlv()); + } +}); +#[cfg(all(feature = "delta-dfu", feature = "hw-rollback-protection"))] +test_shell!(delta_confirm_updates_security_counter, r, { + if let Some(image) = r.try_make_delta_image() { + dump_image(&image, "delta_confirm_updates_security_counter"); + assert!(!image.run_delta_confirm_updates_security_counter()); + } +}); sim_test!(norevert, make_image(&NO_DEPS, true), run_norevert()); sim_test!(oversized_secondary_slot, make_oversized_secondary_slot_image(), run_fail_upgrade_primary_intact()); #[cfg(feature = "check-load-addr")]