From c9b33c74792ba480cfb6d497fb73c16d4211db2e Mon Sep 17 00:00:00 2001 From: Tomasz Chyrowicz Date: Thu, 9 Oct 2025 15:05:21 +0200 Subject: [PATCH 1/4] boot: Add MCUboot manifest TLV Add a possibility to attach a basic manifest with expected digests to an image. Alter the image verification logic, so only digests specified by the manifest are allowed on the device. Signed-off-by: Tomasz Chyrowicz --- boot/bootutil/include/bootutil/image.h | 1 + .../include/bootutil/mcuboot_manifest.h | 99 +++++++++++++++ boot/bootutil/src/bootutil_priv.h | 12 ++ boot/bootutil/src/image_validate.c | 118 +++++++++++++++++- boot/zephyr/Kconfig | 25 ++++ .../include/mcuboot_config/mcuboot_config.h | 8 ++ docs/design.md | 5 +- 7 files changed, 265 insertions(+), 3 deletions(-) create mode 100644 boot/bootutil/include/bootutil/mcuboot_manifest.h diff --git a/boot/bootutil/include/bootutil/image.h b/boot/bootutil/include/bootutil/image.h index 3d103f8da..63dbac696 100644 --- a/boot/bootutil/include/bootutil/image.h +++ b/boot/bootutil/include/bootutil/image.h @@ -134,6 +134,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_MANIFEST 0x76 /* Transaction manifest */ /* * vendor reserved TLVs at xxA0-xxFF, * where xx denotes the upper byte diff --git a/boot/bootutil/include/bootutil/mcuboot_manifest.h b/boot/bootutil/include/bootutil/mcuboot_manifest.h new file mode 100644 index 000000000..2f0100640 --- /dev/null +++ b/boot/bootutil/include/bootutil/mcuboot_manifest.h @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __MCUBOOT_MANIFEST_H__ +#define __MCUBOOT_MANIFEST_H__ + +/** + * @file mcuboot_manifest.h + * + * @note This file is only used when MCUBOOT_MANIFEST_UPDATES is enabled. + */ + +#include +#include "bootutil/bootutil.h" +#include "bootutil/crypto/sha.h" + +#ifndef __packed +#define __packed __attribute__((__packed__)) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** Manifest structure for image updates. */ +struct mcuboot_manifest { + uint32_t format; + uint32_t image_count; + /* Skip a digest of the MCUBOOT_MANIFEST_IMAGE_NUMBER image. */ + uint8_t image_hash[MCUBOOT_IMAGE_NUMBER - 1][IMAGE_HASH_SIZE]; +} __packed; + +/** + * @brief Check if the specified manifest has the correct format. + * + * @param[in] manifest The reference to the manifest structure. + * + * @return true on success. + */ +static inline bool bootutil_verify_manifest(const struct mcuboot_manifest *manifest) +{ + if (manifest == NULL) { + return false; + } + + /* Currently only the simplest manifest format is supported */ + if (manifest->format != 0x1) { + return false; + } + + if (manifest->image_count != MCUBOOT_IMAGE_NUMBER - 1) { + return false; + } + + return true; +} + +/** + * @brief Get the image hash from the manifest. + * + * @param[in] manifest The reference to the manifest structure. + * @param[in] image_index The index of the image to get the hash for. + * Must be in range <0, MCUBOOT_IMAGE_NUMBER - 1>, but + * must not be equal to MCUBOOT_MANIFEST_IMAGE_NUMBER. + * + * @return true if hash matches with the manifest, false otherwise. + */ +static inline bool bootutil_verify_manifest_image_hash(const struct mcuboot_manifest *manifest, + const uint8_t *exp_hash, uint32_t image_index) +{ + if (!bootutil_verify_manifest(manifest)) { + return false; + } + + if (image_index >= MCUBOOT_IMAGE_NUMBER) { + return false; + } + + if (image_index < MCUBOOT_MANIFEST_IMAGE_NUMBER) { + if (memcmp(exp_hash, manifest->image_hash[image_index], IMAGE_HASH_SIZE) == 0) { + return true; + } + } else if (image_index > MCUBOOT_MANIFEST_IMAGE_NUMBER) { + if (memcmp(exp_hash, manifest->image_hash[image_index - 1], IMAGE_HASH_SIZE) == 0) { + return true; + } + } + + return false; +} + +#ifdef __cplusplus +} +#endif + +#endif /* __MCUBOOT_MANIFEST_H__ */ diff --git a/boot/bootutil/src/bootutil_priv.h b/boot/bootutil/src/bootutil_priv.h index 14c56cd21..3b442c99a 100644 --- a/boot/bootutil/src/bootutil_priv.h +++ b/boot/bootutil/src/bootutil_priv.h @@ -44,6 +44,10 @@ #include "bootutil/enc_key.h" #endif +#ifdef MCUBOOT_MANIFEST_UPDATES +#include "bootutil/mcuboot_manifest.h" +#endif /* MCUBOOT_MANIFEST_UPDATES */ + #ifdef __cplusplus extern "C" { #endif @@ -271,6 +275,14 @@ struct boot_loader_state { #endif } slot_usage[BOOT_IMAGE_NUMBER]; #endif /* MCUBOOT_DIRECT_XIP || MCUBOOT_RAM_LOAD */ + +#if defined(MCUBOOT_MANIFEST_UPDATES) + struct mcuboot_manifest manifest[BOOT_NUM_SLOTS]; + bool manifest_valid[BOOT_NUM_SLOTS]; +#if defined(MCUBOOT_SWAP_USING_SCRATCH) || defined(MCUBOOT_SWAP_USING_MOVE) || defined(MCUBOOT_SWAP_USING_OFFSET) + enum boot_slot matching_manifest[BOOT_IMAGE_NUMBER][BOOT_NUM_SLOTS]; +#endif +#endif }; struct boot_sector_buffer { diff --git a/boot/bootutil/src/image_validate.c b/boot/bootutil/src/image_validate.c index 0639aac10..3c1c5ec7f 100644 --- a/boot/bootutil/src/image_validate.c +++ b/boot/bootutil/src/image_validate.c @@ -50,6 +50,10 @@ BOOT_LOG_MODULE_DECLARE(mcuboot); #include "bootutil/mcuboot_uuid.h" #endif /* MCUBOOT_UUID_VID || MCUBOOT_UUID_CID */ +#ifdef MCUBOOT_MANIFEST_UPDATES +#include "bootutil/mcuboot_manifest.h" +#endif /* MCUBOOT_MANIFEST_UPDATES */ + #ifdef MCUBOOT_ENC_IMAGES #include "bootutil/enc_key.h" #endif @@ -206,7 +210,7 @@ bootutil_img_validate(struct boot_loader_state *state, { #if (defined(EXPECTED_KEY_TLV) && defined(MCUBOOT_HW_KEY)) || \ (defined(EXPECTED_SIG_TLV) && defined(MCUBOOT_BUILTIN_KEY)) || \ - defined(MCUBOOT_HW_ROLLBACK_PROT) || \ + defined(MCUBOOT_HW_ROLLBACK_PROT) || defined(MCUBOOT_MANIFEST_UPDATES) || \ defined(MCUBOOT_UUID_VID) || defined(MCUBOOT_UUID_CID) int image_index = (state == NULL ? 0 : BOOT_CURR_IMG(state)); #endif @@ -244,6 +248,11 @@ bootutil_img_validate(struct boot_loader_state *state, uint32_t img_security_cnt = 0; FIH_DECLARE(security_counter_valid, FIH_FAILURE); #endif +#ifdef MCUBOOT_MANIFEST_UPDATES + bool manifest_found = false; + bool manifest_valid = false; + uint8_t slot = (flash_area_get_id(fap) == FLASH_AREA_IMAGE_SECONDARY(image_index) ? 1 : 0); +#endif #ifdef MCUBOOT_UUID_VID struct image_uuid img_uuid_vid = {0x00}; FIH_DECLARE(uuid_vid_valid, FIH_FAILURE); @@ -356,6 +365,69 @@ bootutil_img_validate(struct boot_loader_state *state, goto out; } +#ifdef MCUBOOT_MANIFEST_UPDATES + if (image_index == MCUBOOT_MANIFEST_IMAGE_NUMBER) { + if (!state->manifest_valid[slot]) { + /* Manifest TLV must be processed before any of the image's hash TLV. */ + BOOT_LOG_ERR("bootutil_img_validate: image rejected, manifest not found before " + "image %d hash", image_index); + rc = -1; + goto out; + } + /* Manifest image does not have hash in the manifest. */ + image_hash_valid = 1; + break; + } +#if defined(MCUBOOT_SWAP_USING_SCRATCH) || defined(MCUBOOT_SWAP_USING_MOVE) || \ + defined(MCUBOOT_SWAP_USING_OFFSET) + state->matching_manifest[image_index][slot] = BOOT_SLOT_NONE; + /* Try to match with the primary manifest first. */ + if (state->manifest_valid[BOOT_SLOT_PRIMARY]) { + if (bootutil_verify_manifest_image_hash(&state->manifest[BOOT_SLOT_PRIMARY], hash, + image_index)) { + state->matching_manifest[image_index][slot] = BOOT_SLOT_PRIMARY; + } + } + + /* Try to match with the secondary manifest if not matched with the primary. */ + if(state->matching_manifest[image_index][slot] == BOOT_SLOT_NONE && + state->manifest_valid[BOOT_SLOT_SECONDARY]) { + if (bootutil_verify_manifest_image_hash(&state->manifest[BOOT_SLOT_SECONDARY], hash, + image_index)) { + state->matching_manifest[image_index][slot] = BOOT_SLOT_SECONDARY; + } + } + + /* No matching manifest found. */ + if (state->matching_manifest[image_index][slot] == BOOT_SLOT_NONE) { + BOOT_LOG_ERR( + "bootutil_img_validate: image rejected, no valid manifest for image %d slot %d", + image_index, slot); + rc = -1; + goto out; + } else { + BOOT_LOG_INF("bootutil_img_validate: image %d slot %d matches manifest in slot %d", + image_index, slot, state->matching_manifest[image_index][slot]); + } +#else /* MCUBOOT_SWAP_USING_SCRATCH || MCUBOOT_SWAP_USING_MOVE || MCUBOOT_SWAP_USING_OFFSET */ + /* Manifest image for a given slot must precede any of other images. */ + if (!state->manifest_valid[slot]) { + /* Manifest TLV must be processed before any of the image's hash TLV. */ + BOOT_LOG_ERR("bootutil_img_validate: image rejected, no valid manifest for slot %d", + slot); + rc = -1; + goto out; + } + + /* Any image, not described by the manifest is considered as invalid. */ + if (!bootutil_verify_manifest_image_hash(&state->manifest[slot], hash, image_index)) { + BOOT_LOG_ERR( + "bootutil_img_validate: image rejected, hash does not match manifest contents"); + FIH_SET(fih_rc, FIH_FAILURE); + goto out; + } +#endif /* MCUBOOT_SWAP_USING_SCRATCH || MCUBOOT_SWAP_USING_MOVE || MCUBOOT_SWAP_USING_OFFSET */ +#endif /* MCUBOOT_MANIFEST_UPDATES */ image_hash_valid = 1; break; } @@ -484,6 +556,43 @@ bootutil_img_validate(struct boot_loader_state *state, break; } #endif /* MCUBOOT_HW_ROLLBACK_PROT */ +#ifdef MCUBOOT_MANIFEST_UPDATES + case IMAGE_TLV_MANIFEST: + { + /* There can be only one manifest and must be a part of image with specific index. */ + if (manifest_found || image_index != MCUBOOT_MANIFEST_IMAGE_NUMBER || + len != sizeof(struct mcuboot_manifest)) { + BOOT_LOG_ERR( + "bootutil_img_validate: image %d slot %d rejected, unexpected manifest TLV", + image_index, slot); + rc = -1; + goto out; + } + + manifest_found = true; + + rc = LOAD_IMAGE_DATA(hdr, fap, off, &state->manifest[slot], + sizeof(struct mcuboot_manifest)); + if (rc) { + BOOT_LOG_ERR("bootutil_img_validate: slot %d rejected, unable to load manifest", + slot); + goto out; + } + + manifest_valid = bootutil_verify_manifest(&state->manifest[slot]); + if (!manifest_valid) { + BOOT_LOG_ERR("bootutil_img_validate: slot %d rejected, invalid manifest contents", + slot); + rc = -1; + goto out; + } + + /* The image's manifest has been successfully verified. */ + state->manifest_valid[slot] = true; + BOOT_LOG_INF("bootutil_img_validate: slot %d manifest verified", slot); + break; + } +#endif #ifdef MCUBOOT_UUID_VID case IMAGE_TLV_UUID_VID: { @@ -564,6 +673,13 @@ bootutil_img_validate(struct boot_loader_state *state, } #endif +#ifdef MCUBOOT_MANIFEST_UPDATES + if (image_index == MCUBOOT_MANIFEST_IMAGE_NUMBER && (!manifest_found || !manifest_valid)) { + BOOT_LOG_ERR("bootutil_img_validate: slot %d rejected, manifest missing or invalid", slot); + rc = -1; + goto out; + } +#endif #ifdef MCUBOOT_UUID_VID if (FIH_NOT_EQ(uuid_vid_valid, FIH_SUCCESS)) { rc = -1; diff --git a/boot/zephyr/Kconfig b/boot/zephyr/Kconfig index e2ad52fc4..d3793b058 100644 --- a/boot/zephyr/Kconfig +++ b/boot/zephyr/Kconfig @@ -1072,6 +1072,31 @@ config MCUBOOT_HW_DOWNGRADE_PREVENTION_COUNTER_LIMITED endchoice +config MCUBOOT_MANIFEST_UPDATES + bool "Enable transactional updates" + select EXPERIMENTAL + help + If y, enables support for transactional updates using manifests. + This allows multiple images to be updated atomically. The manifest + is a separate TLV which contains a list of images to update and + their expected hash values. The manifest TLV is a part of an image + that is signed to prevent tampering. + The manifest must be transferred as part of the image with index 0. + It can be a dedicated image, or part of an existing image. + If the second option is selected, all updates must contain an update + for image 0. + +if MCUBOOT_MANIFEST_UPDATES + +config MCUBOOT_MANIFEST_IMAGE_NUMBER + int "Number of image that must include manifest" + default 0 + range 0 UPDATEABLE_IMAGE_NUMBER + help + Specifies the index of the image that must include the manifest. + +endif # MCUBOOT_MANIFEST_UPDATES + config MCUBOOT_UUID_VID bool "Expect vendor unique identifier in image's TLV" help diff --git a/boot/zephyr/include/mcuboot_config/mcuboot_config.h b/boot/zephyr/include/mcuboot_config/mcuboot_config.h index d4be9d412..9cc35f13a 100644 --- a/boot/zephyr/include/mcuboot_config/mcuboot_config.h +++ b/boot/zephyr/include/mcuboot_config/mcuboot_config.h @@ -233,6 +233,14 @@ #define MCUBOOT_HW_ROLLBACK_PROT_COUNTER_LIMITED #endif +#ifdef CONFIG_MCUBOOT_MANIFEST_UPDATES +#define MCUBOOT_MANIFEST_UPDATES + +#ifdef CONFIG_MCUBOOT_MANIFEST_IMAGE_NUMBER +#define MCUBOOT_MANIFEST_IMAGE_NUMBER CONFIG_MCUBOOT_MANIFEST_IMAGE_NUMBER +#endif /* CONFIG_MCUBOOT_MANIFEST_IMAGE_NUMBER */ +#endif /* CONFIG_MCUBOOT_MANIFEST_UPDATES */ + #ifdef CONFIG_MCUBOOT_UUID_VID #define MCUBOOT_UUID_VID #endif diff --git a/docs/design.md b/docs/design.md index 484066d60..ab532c0f4 100755 --- a/docs/design.md +++ b/docs/design.md @@ -150,8 +150,9 @@ struct image_tlv { * ... * 0xffa0 - 0xfffe */ -#define IMAGE_TLV_UUID_VID 0x80 /* Vendor unique identifier */ -#define IMAGE_TLV_UUID_CID 0x81 /* Device class unique identifier */ +#define IMAGE_TLV_UUID_VID 0x74 /* Vendor unique identifier */ +#define IMAGE_TLV_UUID_CID 0x75 /* Device class unique identifier */ +#define IMAGE_TLV_MANIFEST 0x76 /* Transaction manifest */ ``` Optional type-length-value records (TLVs) containing image metadata are placed From f9a047c83cf7d7ad013020cce8a078f7e0844ebe Mon Sep 17 00:00:00 2001 From: Tomasz Chyrowicz Date: Thu, 9 Oct 2025 17:51:17 +0200 Subject: [PATCH 2/4] imgtool: Add a possibility to attach manifest TLV Add a simple logic that allows to attach a manifest TLV to an image. Signed-off-by: Tomasz Chyrowicz --- scripts/imgtool/image.py | 86 ++++++++++++++++++++++++++++++++++++++-- scripts/imgtool/main.py | 9 +++-- 2 files changed, 89 insertions(+), 6 deletions(-) diff --git a/scripts/imgtool/image.py b/scripts/imgtool/image.py index 803aff69e..39fda393f 100755 --- a/scripts/imgtool/image.py +++ b/scripts/imgtool/image.py @@ -39,6 +39,7 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat from intelhex import IntelHex +from yaml import safe_load as yaml_safe_load from . import keys from . import version as versmod @@ -93,6 +94,7 @@ 'COMP_DEC_SIZE' : 0x73, 'UUID_VID': 0x74, 'UUID_CID': 0x75, + 'MANIFEST': 0x76, } TLV_SIZE = 4 @@ -282,6 +284,73 @@ def parse_uuid(namespace, value): return uuid_bytes +class Manifest: + def __init__(self, endian, path): + self.path = path + self.format = 1 + self.data = None + self.config = None + self.endian = endian + self.load() + + def load(self): + try: + with open(self.path) as f: + self.config = yaml_safe_load(f) + format = self.config.get('format', 0) + if isinstance(format, str) and format.isdigit(): + format = int(format) + if format != self.format: + raise click.UsageError(f"Unsupported manifest format: {format}") + + # Encode manifest format + e = STRUCT_ENDIAN_DICT[self.endian] + self.data = struct.pack(e + 'I', format) + + # Encode number of images/hashes + n_images = len(self.config.get('images', [])) + self.data += struct.pack(e + 'I', n_images) + + # Encode each image hash + exp_hash_len = None + for image in self.config.get('images', []): + if 'path' not in image and 'hash' not in image: + raise click.UsageError( + "Manifest image entry must contain either 'path' or 'hash'") + + # Encode hash, based on the signed image path + if 'path' in image: + (result, version, digest, _) = Image.verify(image['path'], None) + if result != VerifyResult.OK: + raise click.UsageError(f"Failed to verify image: {image['path']}") + + if exp_hash_len is None: + exp_hash_len = len(digest) + elif exp_hash_len != len(digest): + raise click.UsageError("All image hashes must have the same length") + self.data += struct.pack(e + f'{exp_hash_len}s', digest) + + # Encode RAW image hash + if 'hash' in image: + if exp_hash_len is None: + exp_hash_len = len(bytes.fromhex(image['hash'])) + elif exp_hash_len != len(bytes.fromhex(image['hash'])): + raise click.UsageError("All image hashes must have the same length") + self.data += struct.pack(e + f'{exp_hash_len}s', bytes.fromhex(image['hash'])) + + except FileNotFoundError: + raise click.UsageError(f"Manifest file {self.path} not found") from None + + def encode(self): + if self.data is None: + raise click.UsageError("Manifest data is empty") + return self.data + + def __len__(self): + return len(self.data) if self.data is not None else 0 + + def __repr__(self): + return f"" class Image: @@ -291,7 +360,7 @@ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE, overwrite_only=False, endian="little", load_addr=0, rom_fixed=None, erased_val=None, save_enctlv=False, security_counter=None, max_align=None, - non_bootable=False, vid=None, cid=None): + non_bootable=False, vid=None, cid=None, manifest=None): if load_addr and rom_fixed: raise click.UsageError("Can not set rom_fixed and load_addr at the same time") @@ -323,6 +392,7 @@ def __init__(self, version=None, header_size=IMAGE_HEADER_SIZE, self.non_bootable = non_bootable self.vid = vid self.cid = cid + self.manifest = Manifest(endian=endian, path=manifest) if manifest is not None else None if self.max_align == DEFAULT_MAX_ALIGN: self.boot_magic = bytes([ @@ -352,7 +422,7 @@ def __repr__(self): return "".format( + payloadlen=0x{:x}, vid={}, cid={}, manifest={}>".format( self.version, self.header_size, self.security_counter, @@ -366,7 +436,8 @@ def __repr__(self): self.__class__.__name__, len(self.payload), self.vid, - self.cid) + self.cid, + self.manifest) def load(self, path): """Load an image from a given file""" @@ -556,6 +627,11 @@ def create(self, key, public_key_format, enckey, dependencies=None, # = 4 + 16 = 20 Bytes protected_tlv_size += TLV_SIZE + 16 + if self.manifest is not None: + # Size of the MANIFEST TLV: header ('HH') + payload (len(manifest)) + # = 4 + len(manifest) Bytes + protected_tlv_size += TLV_SIZE + len(self.manifest.encode()) + if sw_type is not None: if len(sw_type) > MAX_SW_TYPE_LENGTH: msg = f"'{sw_type}' is too long ({len(sw_type)} characters) for sw_type. Its " \ @@ -671,6 +747,10 @@ def create(self, key, public_key_format, enckey, dependencies=None, payload = struct.pack(e + '16s', cid) prot_tlv.add('UUID_CID', payload) + if self.manifest is not None: + payload = self.manifest.encode() + prot_tlv.add('MANIFEST', payload) + if custom_tlvs is not None: for tag, value in custom_tlvs.items(): prot_tlv.add(tag, value) diff --git a/scripts/imgtool/main.py b/scripts/imgtool/main.py index 3074234e2..01631f2cd 100755 --- a/scripts/imgtool/main.py +++ b/scripts/imgtool/main.py @@ -450,13 +450,15 @@ def convert(self, value, param, ctx): help='Unique vendor identifier, format: (|') @click.option('--cid', default=None, required=False, help='Unique image class identifier, format: (|)') +@click.option('--manifest', default=None, required=False, + help='Path to the update manifest file') 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, dependencies, load_addr, hex_addr, erased_val, save_enctlv, security_counter, boot_record, custom_tlv, rom_fixed, max_align, clear, fix_sig, fix_sig_pubkey, sig_out, user_sha, hmac_sha, is_pure, - vector_to_sign, non_bootable, vid, cid): + vector_to_sign, non_bootable, vid, cid, manifest): if confirm or test: # Confirmed but non-padded images don't make much sense, because @@ -469,7 +471,8 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, 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) + non_bootable=non_bootable, vid=vid, cid=cid, + manifest=manifest) compression_tlvs = {} img.load(infile) key = load_key(key) if key else None @@ -540,7 +543,7 @@ def sign(key, public_key_format, align, version, pad_sig, header_size, load_addr=load_addr, rom_fixed=rom_fixed, erased_val=erased_val, save_enctlv=save_enctlv, security_counter=security_counter, max_align=max_align, - vid=vid, cid=cid) + vid=vid, cid=cid, manifest=manifest) compression_filters = [ {"id": lzma.FILTER_LZMA2, "preset": comp_default_preset, "dict_size": comp_default_dictsize, "lp": comp_default_lp, From 89bb9fab7c581a3b2214ac4c3ef30ba51a6ed2ea Mon Sep 17 00:00:00 2001 From: Tomasz Chyrowicz Date: Wed, 15 Oct 2025 17:54:12 +0200 Subject: [PATCH 3/4] bootutil: Add manifest-based loader for Direct XIP Add a loader variant that is capable of booting images, based on a simple manifest. Signed-off-by: Tomasz Chyrowicz --- boot/bootutil/CMakeLists.txt | 1 + boot/bootutil/src/loader.c | 4 + boot/bootutil/src/loader_manifest_xip.c | 630 ++++++++++++++++++++++++ boot/zephyr/CMakeLists.txt | 20 + 4 files changed, 655 insertions(+) create mode 100644 boot/bootutil/src/loader_manifest_xip.c diff --git a/boot/bootutil/CMakeLists.txt b/boot/bootutil/CMakeLists.txt index b6abb7fff..6d74578d8 100644 --- a/boot/bootutil/CMakeLists.txt +++ b/boot/bootutil/CMakeLists.txt @@ -33,6 +33,7 @@ target_sources(bootutil src/image_rsa.c src/image_validate.c src/loader.c + src/loader_manifest_xip.c src/swap_misc.c src/swap_move.c src/swap_scratch.c diff --git a/boot/bootutil/src/loader.c b/boot/bootutil/src/loader.c index 6d93753a0..ded7d949c 100644 --- a/boot/bootutil/src/loader.c +++ b/boot/bootutil/src/loader.c @@ -51,6 +51,8 @@ #include "bootutil/mcuboot_status.h" #include "bootutil_loader.h" +#ifndef MCUBOOT_MANIFEST_UPDATES + #ifdef MCUBOOT_ENC_IMAGES #include "bootutil/enc_key.h" #endif @@ -2495,3 +2497,5 @@ uint32_t boot_get_state_secondary_offset(struct boot_loader_state *state, return 0; } #endif + +#endif /* !MCUBOOT_MANIFEST_UPDATES */ diff --git a/boot/bootutil/src/loader_manifest_xip.c b/boot/bootutil/src/loader_manifest_xip.c new file mode 100644 index 000000000..858fdb82b --- /dev/null +++ b/boot/bootutil/src/loader_manifest_xip.c @@ -0,0 +1,630 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright (c) 2016-2020 Linaro LTD + * Copyright (c) 2016-2019 JUUL Labs + * Copyright (c) 2019-2023 Arm Limited + * Copyright (c) 2024-2025 Nordic Semiconductor ASA + * + * Original license: + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * This file provides an interface to the manifest-based boot loader. + * Functions defined in this file should only be called while the boot loader is + * running. + */ + +#include +#include +#include +#include +#include +#include "flash_map_backend/flash_map_backend.h" +#include "mcuboot_config/mcuboot_config.h" +#include "bootutil/bootutil.h" +#include "bootutil/bootutil_public.h" +#include "bootutil/image.h" +#include "bootutil_priv.h" +#include "bootutil/bootutil_log.h" +#include "bootutil/security_cnt.h" +#include "bootutil/fault_injection_hardening.h" +#include "bootutil/boot_hooks.h" +#include "bootutil_loader.h" + +#if defined(MCUBOOT_MANIFEST_UPDATES) && defined(MCUBOOT_DIRECT_XIP) +#include "bootutil/mcuboot_manifest.h" + +#if defined(MCUBOOT_DIRECT_XIP) && defined(MCUBOOT_DECOMPRESS_IMAGES) +#error "Image decompression is not supported when MCUBOOT_DIRECT_XIP is selected." +#endif /* MCUBOOT_DIRECT_XIP && MCUBOOT_DECOMPRESS_IMAGES */ + +#ifdef MCUBOOT_ENC_IMAGES +#include "bootutil/enc_key.h" +#endif + +BOOT_LOG_MODULE_DECLARE(mcuboot); + +static struct boot_loader_state boot_data; + +#if defined(MCUBOOT_SERIAL_IMG_GRP_SLOT_INFO) || defined(MCUBOOT_DATA_SHARING) +static struct image_max_size image_max_sizes[BOOT_IMAGE_NUMBER] = {0}; +#endif + +#if BOOT_MAX_ALIGN > 1024 +#define BUF_SZ BOOT_MAX_ALIGN +#else +#define BUF_SZ 1024 +#endif + +struct boot_loader_state *boot_get_loader_state(void) +{ + return &boot_data; +} + +#if defined(MCUBOOT_SERIAL_IMG_GRP_SLOT_INFO) || defined(MCUBOOT_DATA_SHARING) +struct image_max_size *boot_get_image_max_sizes(void) +{ + return image_max_sizes; +} +#endif + +/** + * Fills rsp to indicate how booting should occur. + * + * @param state Boot loader status information. + * @param rsp boot_rsp struct to fill. + */ +static void +fill_rsp(struct boot_loader_state *state, struct boot_rsp *rsp) +{ + uint32_t active_slot; + + /* Always boot from the first image. */ + BOOT_CURR_IMG(state) = 0; + active_slot = state->slot_usage[BOOT_CURR_IMG(state)].active_slot; + + rsp->br_flash_dev_id = flash_area_get_device_id(BOOT_IMG_AREA(state, active_slot)); + rsp->br_image_off = boot_img_slot_off(state, active_slot); + rsp->br_hdr = boot_img_hdr(state, active_slot); +} + +#if defined(MCUBOOT_DIRECT_XIP) +/** + * Check if image in slot has been set with specific ROM address to run from + * and whether the slot starts at that address. + * + * @returns 0 if IMAGE_F_ROM_FIXED flag is not set; + * 0 if IMAGE_F_ROM_FIXED flag is set and ROM address specified in + * header matches the slot address; + * 1 if IMF_F_ROM_FIXED flag is set but ROM address specified in header + * does not match the slot address. + */ +static bool +boot_rom_address_check(struct boot_loader_state *state) +{ + uint32_t active_slot; + const struct image_header *hdr; + uint32_t f_off; + + active_slot = state->slot_usage[BOOT_CURR_IMG(state)].active_slot; + hdr = boot_img_hdr(state, active_slot); + f_off = boot_img_slot_off(state, active_slot); + + if (hdr->ih_flags & IMAGE_F_ROM_FIXED && hdr->ih_load_addr != f_off) { + BOOT_LOG_WRN("Image in %s slot at 0x%x has been built for offset 0x%x"\ + ", skipping", + active_slot == 0 ? "primary" : "secondary", f_off, + hdr->ih_load_addr); + + /* The image is not bootable from this slot. */ + return 1; + } + + return 0; +} +#endif + +/* + * Check that there is a valid image in a slot + * + * @returns + * FIH_SUCCESS if image was successfully validated + * FIH_NO_BOOTABLE_IMAGE if no bootloable image was found + * FIH_FAILURE on any errors + */ +static fih_ret +boot_validate_slot(struct boot_loader_state *state, int slot, + struct boot_status *bs, int expected_swap_type) +{ + const struct flash_area *fap; + struct image_header *hdr; + FIH_DECLARE(fih_rc, FIH_FAILURE); + + BOOT_LOG_DBG("boot_validate_slot: slot %d, expected_swap_type %d", + slot, expected_swap_type); + (void)expected_swap_type; + + fap = BOOT_IMG_AREA(state, slot); + assert(fap != NULL); + + hdr = boot_img_hdr(state, slot); + if (boot_check_header_erased(state, slot) || (hdr->ih_flags & IMAGE_F_NON_BOOTABLE)) { + /* No bootable image in slot; continue booting from the primary slot. */ + fih_rc = FIH_NO_BOOTABLE_IMAGE; + goto out; + } + + if (!boot_check_header_valid(state, slot)) { + fih_rc = FIH_FAILURE; + } else { + BOOT_HOOK_CALL_FIH(boot_image_check_hook, FIH_BOOT_HOOK_REGULAR, + fih_rc, BOOT_CURR_IMG(state), slot); + if (FIH_EQ(fih_rc, FIH_BOOT_HOOK_REGULAR)) { + FIH_CALL(boot_check_image, fih_rc, state, bs, slot); + } + } + + if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) { + if ((slot != BOOT_SLOT_PRIMARY) || ARE_SLOTS_EQUIVALENT()) { + boot_scramble_slot(fap, slot); + /* Image is invalid, erase it to prevent further unnecessary + * attempts to validate and boot it. + */ + } + +#if !defined(__BOOTSIM__) + BOOT_LOG_ERR("Image in the %s slot is not valid!", + (slot == BOOT_SLOT_PRIMARY) ? "primary" : "secondary"); +#endif + fih_rc = FIH_NO_BOOTABLE_IMAGE; + goto out; + } + +out: + FIH_RET(fih_rc); +} + +/** + * Opens all flash areas and checks which contain an image with a valid header. + * + * @param state Boot loader status information. + * + * @return 0 on success; nonzero on failure. + */ +static int +boot_get_slot_usage(struct boot_loader_state *state) +{ + uint32_t slot; + int rc; + + IMAGES_ITER(BOOT_CURR_IMG(state)) { + /* Attempt to read an image header from each slot. */ + rc = boot_read_image_headers(state, false, NULL); + if (rc != 0) { + BOOT_LOG_WRN("Failed reading image headers."); + return rc; + } + + /* Check headers in all slots */ + for (slot = 0; slot < BOOT_NUM_SLOTS; slot++) { + if (boot_check_header_valid(state, slot)) { + state->slot_usage[BOOT_CURR_IMG(state)].slot_available[slot] = true; + BOOT_LOG_IMAGE_INFO(slot, boot_img_hdr(state, slot)); + } else { + state->slot_usage[BOOT_CURR_IMG(state)].slot_available[slot] = false; + BOOT_LOG_INF("Image %d %s slot: Image not found", + BOOT_CURR_IMG(state), + (slot == BOOT_SLOT_PRIMARY) + ? "Primary" : "Secondary"); + } + } + + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = BOOT_SLOT_NONE; + } + + return 0; +} + +/** + * Finds the slot containing the image with the highest version number for the + * current image. + * + * @param state Boot loader status information. + * + * @return BOOT_SLOT_NONE if no available slot found, number of + * the found slot otherwise. + */ +static uint32_t +find_slot_with_highest_version(struct boot_loader_state *state) +{ + uint32_t slot; + uint32_t candidate_slot = BOOT_SLOT_NONE; + int rc; + + for (slot = 0; slot < BOOT_NUM_SLOTS; slot++) { + if (state->slot_usage[BOOT_CURR_IMG(state)].slot_available[slot]) { + if (candidate_slot == BOOT_SLOT_NONE) { + candidate_slot = slot; + } else { + rc = boot_compare_version( + &boot_img_hdr(state, slot)->ih_ver, + &boot_img_hdr(state, candidate_slot)->ih_ver); + if (rc == 1) { + /* The version of the image being examined is greater than + * the version of the current candidate. + */ + candidate_slot = slot; + } + } + } + } + + return candidate_slot; +} + +#ifdef MCUBOOT_HAVE_LOGGING +/** + * Prints the state of the loaded images. + * + * @param state Boot loader status information. + */ +static void +print_loaded_images(struct boot_loader_state *state) +{ + uint32_t active_slot; + + IMAGES_ITER(BOOT_CURR_IMG(state)) { + active_slot = state->slot_usage[BOOT_CURR_IMG(state)].active_slot; + + BOOT_LOG_INF("Image %d loaded from the %s slot", + BOOT_CURR_IMG(state), + (active_slot == BOOT_SLOT_PRIMARY) ? + "primary" : "secondary"); + } +} +#endif + +#if (defined(MCUBOOT_DIRECT_XIP) && defined(MCUBOOT_DIRECT_XIP_REVERT)) +/** + * Checks whether the active slot of the current image was previously selected + * to run. Erases the image if it was selected but its execution failed, + * otherwise marks it as selected if it has not been before. + * + * @param state Boot loader status information. + * + * @return 0 on success; nonzero on failure. + */ +static int +boot_select_or_erase(struct boot_loader_state *state) +{ + const struct flash_area *fap = NULL; + int rc; + uint32_t active_slot; + struct boot_swap_state* active_swap_state; + + active_slot = state->slot_usage[BOOT_CURR_IMG(state)].active_slot; + + fap = BOOT_IMG_AREA(state, active_slot); + assert(fap != NULL); + + active_swap_state = &(state->slot_usage[BOOT_CURR_IMG(state)].swap_state); + + memset(active_swap_state, 0, sizeof(struct boot_swap_state)); + rc = boot_read_swap_state(fap, active_swap_state); + assert(rc == 0); + + if (active_swap_state->magic != BOOT_MAGIC_GOOD) { + /* Image was not selected for test. Skip slot. */ + return -1; + } + + if (active_swap_state->copy_done == BOOT_FLAG_SET && + active_swap_state->image_ok != BOOT_FLAG_SET) { + /* + * A reboot happened without the image being confirmed at + * runtime or its trailer is corrupted/invalid. Erase the image + * to prevent it from being selected again on the next reboot. + */ + BOOT_LOG_DBG("Erasing faulty image in the %s slot.", + (active_slot == BOOT_SLOT_PRIMARY) ? "primary" : "secondary"); + rc = boot_scramble_region(fap, 0, flash_area_get_size(fap), false); + assert(rc == 0); + rc = -1; + } else { + if (active_swap_state->copy_done != BOOT_FLAG_SET) { + if (active_swap_state->copy_done == BOOT_FLAG_BAD) { + BOOT_LOG_DBG("The copy_done flag had an unexpected value. Its " + "value was neither 'set' nor 'unset', but 'bad'."); + } + /* + * Set the copy_done flag, indicating that the image has been + * selected to boot. It can be set in advance, before even + * validating the image, because in case the validation fails, the + * entire image slot will be erased (including the trailer). + */ + rc = boot_write_copy_done(fap); + if (rc != 0) { + BOOT_LOG_WRN("Failed to set copy_done flag of the image in " + "the %s slot.", (active_slot == BOOT_SLOT_PRIMARY) ? + "primary" : "secondary"); + rc = 0; + } + } + } + + return rc; +} +#endif /* MCUBOOT_DIRECT_XIP && MCUBOOT_DIRECT_XIP_REVERT */ + +/** + * Tries to load and validate a single slot. + * + * @param state Boot loader status information. + * + * @return 0 on success; nonzero on failure. + */ +static fih_ret +boot_load_and_validate_current_image(struct boot_loader_state *state) +{ + int rc; + fih_ret fih_rc; + uint32_t active_slot = state->slot_usage[BOOT_CURR_IMG(state)].active_slot; + + if (active_slot == BOOT_SLOT_NONE) { + FIH_RET(FIH_FAILURE); + } + + BOOT_LOG_INF("Loading image %d from slot %d", BOOT_CURR_IMG(state), active_slot); + +#ifdef MCUBOOT_DIRECT_XIP + rc = boot_rom_address_check(state); + if (rc != 0) { + FIH_RET(FIH_FAILURE); + } +#endif /* MCUBOOT_DIRECT_XIP */ + +#if defined(MCUBOOT_DIRECT_XIP_REVERT) + /* The manifest binds images together. The act of validating the manifest + * image implies that the other images are also validated. + * Skip this step and ignore those flags for other images, so a sudden power + * loss after confirming some of the images does not result in partially + * confirmed state. + */ + if (BOOT_CURR_IMG(state) == MCUBOOT_MANIFEST_IMAGE_NUMBER) { + rc = boot_select_or_erase(state); + if (rc != 0) { + FIH_RET(FIH_FAILURE); + } + } +#endif /* MCUBOOT_DIRECT_XIP_REVERT */ + + FIH_CALL(boot_validate_slot, fih_rc, state, active_slot, NULL, 0); + if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) { + /* Image is invalid. */ + FIH_RET(FIH_FAILURE); + } + + FIH_RET(FIH_SUCCESS); +} + +/** + * Tries to load a slot for all the images with validation. + * + * @param state Boot loader status information. + * + * @return 0 on success; nonzero on failure. + */ +fih_ret +boot_load_and_validate_images(struct boot_loader_state *state) +{ + uint32_t active_slot; + int rc; + fih_ret fih_rc; + + while (true) { +#if (BOOT_IMAGE_NUMBER > 1) + BOOT_CURR_IMG(state) = MCUBOOT_MANIFEST_IMAGE_NUMBER; +#endif + rc = BOOT_HOOK_FIND_SLOT_CALL(boot_find_next_slot_hook, BOOT_HOOK_REGULAR, + state, BOOT_CURR_IMG(state), &active_slot); + if (rc == BOOT_HOOK_REGULAR) { + active_slot = find_slot_with_highest_version(state); + } + if (active_slot == BOOT_SLOT_NONE) { + BOOT_LOG_ERR("No more manifest slots available"); + FIH_RET(FIH_FAILURE); + } + + /* Save the number of the active manifest slot. */ + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = active_slot; + + FIH_CALL(boot_load_and_validate_current_image, fih_rc, state); + if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) { + state->slot_usage[MCUBOOT_MANIFEST_IMAGE_NUMBER].slot_available[active_slot] = false; + state->slot_usage[MCUBOOT_MANIFEST_IMAGE_NUMBER].active_slot = BOOT_SLOT_NONE; + BOOT_LOG_INF("No valid manifest in slot %d", active_slot); + continue; + } + + BOOT_LOG_INF("Try to validate images using manifest in slot %d", active_slot); + +#if BOOT_IMAGE_NUMBER > 1 + /* Go over all other images and try to load one */ + IMAGES_ITER(BOOT_CURR_IMG(state)) { + /* Skip the image with manifest - it's been already verified. */ + if (BOOT_CURR_IMG(state) == MCUBOOT_MANIFEST_IMAGE_NUMBER) { + continue; + } + + /* Check if there is a matching slot available. */ + if (!state->slot_usage[BOOT_CURR_IMG(state)].slot_available[active_slot]) { + /* Invalidate manifest */ + FIH_SET(fih_rc, FIH_FAILURE); + break; + } + + /* Save the number of the active slot. */ + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = active_slot; + + FIH_CALL(boot_load_and_validate_current_image, fih_rc, state); + if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) { + state->slot_usage[BOOT_CURR_IMG(state)].slot_available[active_slot] = false; + state->slot_usage[BOOT_CURR_IMG(state)].active_slot = BOOT_SLOT_NONE; + /* Invalidate manifest */ + break; + } + } +#endif + + if (FIH_EQ(fih_rc, FIH_SUCCESS)) { + /* All images have been loaded and validated successfully. */ + break; + } + + BOOT_LOG_DBG("Manifest in slot %d is invalid", active_slot); + + /* Invalidate manifest */ + state->slot_usage[MCUBOOT_MANIFEST_IMAGE_NUMBER].slot_available[active_slot] = false; + state->slot_usage[MCUBOOT_MANIFEST_IMAGE_NUMBER].active_slot = BOOT_SLOT_NONE; + } + + FIH_RET(FIH_SUCCESS); +} + +/** + * Updates the security counter for the current image. + * + * @param state Boot loader status information. + * + * @return 0 on success; nonzero on failure. + */ +static int +boot_update_hw_rollback_protection(struct boot_loader_state *state) +{ +#ifdef MCUBOOT_HW_ROLLBACK_PROT + int rc; + + /* Update the stored security counter with the newer (active) image's + * security counter value. + */ +#if (defined(MCUBOOT_DIRECT_XIP) && defined(MCUBOOT_DIRECT_XIP_REVERT)) + /* When the 'revert' mechanism is enabled in direct-xip or RAM load mode, + * the security counter can be increased only after reboot, if the image + * has been confirmed at runtime (the image_ok flag has been set). + * This way a 'revert' can be performed when it's necessary. + */ + if (state->slot_usage[BOOT_CURR_IMG(state)].swap_state.image_ok == BOOT_FLAG_SET) { +#endif + rc = boot_update_security_counter(state, + state->slot_usage[BOOT_CURR_IMG(state)].active_slot, + state->slot_usage[BOOT_CURR_IMG(state)].active_slot); + if (rc != 0) { + BOOT_LOG_ERR("Security counter update failed after image %d validation.", + BOOT_CURR_IMG(state)); + return rc; + } +#if (defined(MCUBOOT_DIRECT_XIP) && defined(MCUBOOT_DIRECT_XIP_REVERT)) + } +#endif + + return 0; + +#else /* MCUBOOT_HW_ROLLBACK_PROT */ + (void) (state); + return 0; +#endif +} + +fih_ret +context_boot_go(struct boot_loader_state *state, struct boot_rsp *rsp) +{ + int rc; + FIH_DECLARE(fih_rc, FIH_FAILURE); + + rc = boot_open_all_flash_areas(state); + if (rc != 0) { + goto out; + } + + rc = boot_get_slot_usage(state); + if (rc != 0) { + goto close; + } + + FIH_CALL(boot_load_and_validate_images, fih_rc, state); + if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) { + FIH_SET(fih_rc, FIH_FAILURE); + goto close; + } + + IMAGES_ITER(BOOT_CURR_IMG(state)) { + rc = boot_update_hw_rollback_protection(state); + if (rc != 0) { + FIH_SET(fih_rc, FIH_FAILURE); + goto close; + } + + rc = boot_add_shared_data(state, + (uint8_t)state->slot_usage[BOOT_CURR_IMG(state)].active_slot); + if (rc != 0) { + FIH_SET(fih_rc, FIH_FAILURE); + goto close; + } + } + + /* All image loaded successfully. */ +#ifdef MCUBOOT_HAVE_LOGGING + print_loaded_images(state); +#endif + + fill_rsp(state, rsp); + +close: + boot_close_all_flash_areas(state); + +out: + if (rc != 0) { + FIH_SET(fih_rc, FIH_FAILURE); + } + + FIH_RET(fih_rc); +} + +/** + * Prepares the booting process. This function moves images around in flash as + * appropriate, and tells you what address to boot from. + * + * @param rsp On success, indicates how booting should occur. + * + * @return FIH_SUCCESS on success; nonzero on failure. + */ +fih_ret +boot_go(struct boot_rsp *rsp) +{ + FIH_DECLARE(fih_rc, FIH_FAILURE); + + boot_state_clear(NULL); + + FIH_CALL(context_boot_go, fih_rc, &boot_data, rsp); + FIH_RET(fih_rc); +} + +#endif /* MCUBOOT_MANIFEST_UPDATES && MCUBOOT_DIRECT_XIP */ diff --git a/boot/zephyr/CMakeLists.txt b/boot/zephyr/CMakeLists.txt index 1813ea321..5bbb09aac 100644 --- a/boot/zephyr/CMakeLists.txt +++ b/boot/zephyr/CMakeLists.txt @@ -157,6 +157,26 @@ elseif(CONFIG_BOOT_FIRMWARE_LOADER) ${BOOT_DIR}/zephyr/firmware_loader.c ) zephyr_library_include_directories(${BOOT_DIR}/bootutil/src) +elseif(CONFIG_MCUBOOT_MANIFEST_UPDATES) + zephyr_library_sources( + ${BOOT_DIR}/bootutil/src/loader_manifest_xip.c + ${BOOT_DIR}/bootutil/src/swap_misc.c + ${BOOT_DIR}/bootutil/src/caps.c + ) + + if(CONFIG_BOOT_SWAP_USING_MOVE) + zephyr_library_sources( + ${BOOT_DIR}/bootutil/src/swap_move.c + ) + elseif(CONFIG_BOOT_SWAP_USING_OFFSET) + zephyr_library_sources( + ${BOOT_DIR}/bootutil/src/swap_offset.c + ) + else() + zephyr_library_sources( + ${BOOT_DIR}/bootutil/src/swap_scratch.c + ) + endif() else() zephyr_library_sources( ${BOOT_DIR}/bootutil/src/loader.c From 937c80ac4a4f7a22ddc117ebabe3c83151adfd5c Mon Sep 17 00:00:00 2001 From: Tomasz Chyrowicz Date: Tue, 4 Nov 2025 16:00:23 +0100 Subject: [PATCH 4/4] doc: Add manifest conceptual description Add a short description about motivation behind manifest-based updates and the Direct XIP mode of operation if the manifests are enabled. Signed-off-by: Tomasz Chyrowicz --- docs/design.md | 118 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/docs/design.md b/docs/design.md index ab532c0f4..d278dbeaf 100755 --- a/docs/design.md +++ b/docs/design.md @@ -4,6 +4,7 @@ - Copyright (c) 2017-2020 Linaro LTD - Copyright (c) 2017-2019 JUUL Labs - Copyright (c) 2019-2024 Arm Limited + - Copyright (c) 2025 Nordic Semiconductor ASA - Original license: @@ -987,6 +988,123 @@ strategy but there is no need for Scratch area. + Boot the loaded slot of image 0. +### [Multiple image boot using manifest](#multiple-image-boot-using-manifest) + + +Deployments that use multiple images typically require strict control over +versions of firmware components. +There is a dependency TLV that can be used to specify dependencies between +semantic versions of multiple components. However, since these only describe +a minimally compatible version of a counterpart component, there is no mechanism +to enforce a specific revision of the other image. +Therefore, the publisher must ensure that all combinations that satisfy the +dependencies are compatible with each other and are tested before deploying +a new version of the firmware bundle. +One way to simplify the process is to add dependencies to all parts that +enforce the latest revision of all other parts of the bundle. +This effectively enforces equality between the revisions of all parts. +This solution becomes even more problematic when using Direct-XIP mode - since +the bootloader typically only chain-loads the next stage, the next stage must +select and boot the correct slot of the next part of the firmware. +Although the dependencies ensure that the next part with a specific +version is present on the device, there is no guarantee in which slot it +was validated. +Again, the publisher may use different version numbers for the same firmware +created for different slots, but this raises the question of whether there is +a better way to manage dependencies between images. + +The bootloader manifest is an additional, protected TLV that serves as the sole +source of information about the compatibility and bootability of the multipart +firmware. The basic rules for all types of manifests are: + + * The manifest must be protected (directly or indirectly) by a cryptographic + signature. + * There must be exactly one selected image that can provide the manifest TLV. + * There must be only one manifest per slot. + * Processing of the manifest must validate the full functional set of firmware + components. + * If the manifest does not describe a part of the firmware, it must be + considered invalid. + * Each update candidate must provide a new manifest. + + +``` + +-------------------+ + +----------------->| Mainfest | + | +===================+ + | |+-----------------+| + | || format || + | |+-----------------+| + | || image count || + | |+-----------------+| + | +------------|| digest(Image 1) || + | | |+-----------------+| + | | || digest(Image 2) ||-------------+ + | | |+-----------------+| | + | | +-------------------+ | + | | | + +-----------------+ | | +-----------------+ +-----------------+ | + | Manifest image | | | | Image 1 | | Image 2 | | + +=================+ | | +=================+ +=================+ | + |+---------------+| | | |+---------------+| |+---------------+| | + || header || | | || header || || header || | + |+---------------+| | | |+---------------+| |+---------------+| | + || manifest TLV ||-+ | || firmware || || firmware || | + |+---------------+| | |+---------------+| |+---------------+| | + || digest TLV || +->|| digest TLV || || digest TLV ||<-+ + |+---------------+| |+---------------+| |+---------------+| + || signature TLV || || signature TLV || || signature TLV || + |+---------------+| |+---------------+| |+---------------+| + +-----------------+ +-----------------+ +-----------------+ +``` + +The manifest TLV has a format field that allows for the development of complex +boot logic in the future. The default manifest is structured as a list of +digests of firmware parts. + +The manifest is transferred as a protected TLV in a dedicated "Manifest image." +This image is updated using the same mechanisms as regular images. + +The manifest image may contain firmware - if so, this part of the firmware must +be updated with every firmware update. + +The bootloader's behavior changes once manifest-based updates +and booting are enabled. + +Boot process for Direct-XIP modes: + ++ Loop 1. Until all images are loaded and validated against the active manifest + 1. Subloop 1. Iterate over the manifest image slots + + Does any of the slots contain a manifest? + + Yes: + + Select the newer manifest. + + Copy it to the bootloader state. + + Validate the manifest image (integrity and security check). + + If validation fails mark the active manifest slot as + unavailable and try the other slot. + + No: Return with an error. + + 2. Subloop 2. Iterate over all images except manifest image + + Does the current image contain a valid header in the same slot + as the selected (active) manifest? + + Yes: Is the image valid (integrity and security check) and its + digest matches the manifest? + + Yes: Skip to the next image. + + No: + + Mark the active manifest slot as unavailable. + + Restart main loop. + + No: + + Mark the active manifest slot as unavailable. + + Restart main loop. + ++ Loop 2. Iterate over all images + + Increase the security counter if needed. + + Do the measured boot and the data sharing if needed. + ++ Boot the loaded slot of image 0. + +Manifest-based updates and booting for other modes are not yet implemented. + ## [Image swapping](#image-swapping) The bootloader swaps the contents of the two image slots for two reasons: