diff --git a/boot/bootutil/src/loader.c b/boot/bootutil/src/loader.c index 9769428b4d..07b6c93475 100644 --- a/boot/bootutil/src/loader.c +++ b/boot/bootutil/src/loader.c @@ -5,6 +5,7 @@ * Copyright (c) 2016-2019 JUUL Labs * Copyright (c) 2019-2023 Arm Limited * Copyright (c) 2024-2025 Nordic Semiconductor ASA + * Portions Copyright (c) 2025 Analog Devices Inc. * * Original license: * @@ -2990,6 +2991,138 @@ boot_update_hw_rollback_protection(struct boot_loader_state *state) #endif } + +#if (BOOT_IMAGE_NUMBER == 1) && defined(MCUBOOT_RAM_LOAD) + +static int +read_image_info(uint32_t addr, struct image_header *hdr, + uint32_t *total_size, uint32_t *footer_size) +{ + struct image_tlv_info info; + uint32_t off; + uint32_t protect_tlv_size; + + memcpy(hdr, (unsigned char *)addr, sizeof(struct image_header)); + if (hdr->ih_magic != IMAGE_MAGIC) { + return BOOT_EBADIMAGE; + } + + off = BOOT_TLV_OFF(hdr); + + memcpy(&info, (unsigned char *)(addr + off), sizeof(info)); + + protect_tlv_size = hdr->ih_protect_tlv_size; + if (info.it_magic == IMAGE_TLV_PROT_INFO_MAGIC) { + if (protect_tlv_size != info.it_tlv_tot) { + return BOOT_EBADIMAGE; + } + + memcpy(&info, (unsigned char *)(addr + off + info.it_tlv_tot), sizeof(info)); + } else if (protect_tlv_size != 0) { + return BOOT_EBADIMAGE; + } + + if (info.it_magic != IMAGE_TLV_INFO_MAGIC) { + return BOOT_EBADIMAGE; + } + + *footer_size = protect_tlv_size + info.it_tlv_tot; + *total_size = off + *footer_size; + + return 0; +} + +/** + * Check the main image and find sub images, then copy them to the target addr. + * Set boot image address with the first image that found in the list. + * + * ------------------------- + * | Header | + * ------------------------- + * | SubImage (optional) | + * | (Header+Data+Footer) | + * ------------------------- + * | SubImage (optional) | + * | (Header+Data+Footer) | + * ------------------------- + * | ..... | + * ------------------------- + * | Footer | + * ------------------------- + * + * @param addr Image start address + * @param rsp On success, indicates how booting should occur. + * + * @return 0 on success; nonzero on failure. + */ +static int +process_sub_images(uint32_t addr, struct boot_rsp *rsp) +{ + int rc = 0; + bool first_subimage = true; + uint32_t main_image_size; + struct image_header hdr; + uint32_t img_total_size; + uint32_t img_footer_size; + uint32_t subimg_count = 0; + + /* read main image info */ + rc = read_image_info(addr, &hdr, &img_total_size, &img_footer_size); + if (rc != 0) { + /* No valid image header, main image format not correct. */ + BOOT_LOG_INF("Image header read failed."); + return rc; + } + + /* Set main image size */ + main_image_size = img_total_size; + /* Decrease image header size and footer size */ + main_image_size -= (hdr.ih_hdr_size + img_footer_size); + + /* Pass image header */ + addr += hdr.ih_hdr_size; + + while (main_image_size) { + /* read sub image info */ + rc = read_image_info(addr, &hdr, &img_total_size, &img_footer_size); + if (rc != 0) { + /* No valid sub-image header */ + if (subimg_count == 0) { + /* it is single image return 0 */ + rc = 0; + } else { + BOOT_LOG_INF("Sub image header read failed."); + } + break; + } + + /* copy image to target addr */ + if (hdr.ih_flags & IMAGE_F_RAM_LOAD) { + /* + * For heterogenous system that have multi core on same IC. + * Assuming main core that execute MCUBoot able to access other cores ITCM/DTCM + */ + memcpy((unsigned char *)(hdr.ih_load_addr), (unsigned char *)addr, img_total_size); + BOOT_LOG_INF("Copying image from 0x%x to 0x%x is succeeded.", addr, hdr.ih_load_addr); + } + + /* Execute first sub image */ + if ((first_subimage) && !(hdr.ih_flags & IMAGE_F_NON_BOOTABLE)) { + first_subimage = false; + rsp->br_hdr = (struct image_header *)hdr.ih_load_addr; + } + + /* go next image */ + main_image_size -= img_total_size; + addr += img_total_size; + + ++subimg_count; /* Increase number of sub image */ + } + + return rc; +} +#endif // #if (BOOT_IMAGE_NUMBER == 1) && defined(MCUBOOT_RAM_LOAD) + fih_ret context_boot_go(struct boot_loader_state *state, struct boot_rsp *rsp) { @@ -3055,6 +3188,10 @@ context_boot_go(struct boot_loader_state *state, struct boot_rsp *rsp) fill_rsp(state, rsp); +#if (BOOT_IMAGE_NUMBER == 1) && defined(MCUBOOT_RAM_LOAD) + rc = process_sub_images(rsp->br_hdr->ih_load_addr, rsp); +#endif + close: boot_close_all_flash_areas(state); diff --git a/docs/combine_images.md b/docs/combine_images.md new file mode 100644 index 0000000000..cba9712357 --- /dev/null +++ b/docs/combine_images.md @@ -0,0 +1,58 @@ +# [combine_images Extensions to MCUboot for Multi-Core SoC Image Distribution](#combine_images_for_multi-core_SoC_image_distribution) + +In multi-core system-on-chip based platforms, each processor core typically runs separate firmware +as every core may have distinct responsibilities. Packaging and distributing firmware images for +these systems presents a challenge as multiple independent images must be bundled, versioned, and +delivered as a single update while preserving the ability to secure and authenticate each image. The +combine_images extensions to MCUboot allow for the bundling and distribution of multiple images +together as an MCUboot AppPack. + +# [AppPack Structure](#apppack_structure) + +The purpose of this method to handle multiple images in a single image pack is to decrease the number +of signature checks required during bootup. An example AppPack layout is shown in the image below. +This example contains the standard MCUboot header and trailer surrounding a number of images. Each +of the enclosed images may itself be an AppPack. + +![AppPack Structure](./images/AppPack.png) + +# [AppPack Generation](#apppack_generation) + +## [combine_images.py](#combine_images.py) + +In order to generate an AppPack you will need to call the combine_images.py script. + + Usage: python combine_images.py --config [yaml file] --imgtool [path to imgtool] --output [output directory] + + Options: + --config [yaml file] yaml file specifying content of the AppPack to + be generated. The yaml file will specify input + binaries, output file names and the path to any + signing keys. + --imgtool [path to imgtool] Path to ImgTool which can be either a python file + or binary. The provided ImgTool is called to + package binaries and create an AppPack structure + defined according to the specified yaml file. + --output [output directory] Path to an directory which will be used to store + any intermediary file products and the final + AppPack itself. + +## [combine_images.yaml](#combine_images.yaml) + +The combine_images.py script requires a yaml config file to define the layout of the desired AppPack. +The structure of the yaml file is shown below. In this example there is a top level AppPack which +may or may not contain an inputfile. The top level AppPack may also contain some number of nested +AppPacks which can contain additional AppPacks itself. + + [AppPack Name]_pack: + # Optional: top-level infline can be omitted if not needed + infile: [filename] + + # Required + outfile: [filename] + params: [imgtool parameter string] + + [Nested AppPack Name]_pack: + infile: [filename] + outfile: [filename] + params: [imgtool parameter string] diff --git a/docs/images/AppPack.png b/docs/images/AppPack.png new file mode 100644 index 0000000000..4083faf475 Binary files /dev/null and b/docs/images/AppPack.png differ diff --git a/docs/index.md b/docs/index.md index 4b36dc389a..374ef9e5a1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,6 +40,7 @@ The MCUboot documentation is composed of the following pages: - [Bootloader design](design.md) - [Encrypted images](encrypted_images.md) - [imgtool](imgtool.md) - image signing and key management +- [Combine Images](combine_images.md) - Combine images feature - [ECDSA](ecdsa.md) - information about ECDSA signature formats - [Serial Recovery](serial_recovery.md) - MCUmgr as the serial recovery protocol - Usage instructions: diff --git a/scripts/combine_images.py b/scripts/combine_images.py new file mode 100644 index 0000000000..e5523046a9 --- /dev/null +++ b/scripts/combine_images.py @@ -0,0 +1,137 @@ +#! /usr/bin/env python3 +# +# Copyright (C) 2025 Analog Devices, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + +import argparse +import os +import pathlib +import subprocess +import sys + +import yaml +import shutil + + +def main(): + global img_tool + global output_dir + global config_path + + parser = argparse.ArgumentParser(description="Create application package", allow_abbrev=False) + parser.add_argument('--config', help="The path to config yaml file", required=True) + parser.add_argument('--imgtool', help="The path to ImgTool", required=True) + parser.add_argument('--output', help="Output directory", required=True) + + args = parser.parse_args() + + if not os.path.isfile(args.config): + print(f"Error: The config file '{args.config}' does not exist") + return + + config_path = os.path.dirname(os.path.abspath(args.config)) + print(f"config_path: {config_path}") + + with open(args.config) as config_file: + config = yaml.safe_load(config_file) + + img_tool = args.imgtool + if img_tool.endswith('.py'): + if not os.path.exists(img_tool): # Is python file exist? + print(f"Error: The '{img_tool}' not found") + return + else: + if not shutil.which(img_tool): # Is binary file exist? + print(f"Error: The '{img_tool}' not found in the path") + return + + + output_dir = args.output + os.makedirs(output_dir, exist_ok=True) + + parse_app_pack(config, None) + + print(f"\nCreated {package_name}") + + +def verify_file_exists(filename): + filepath = pathlib.Path(filename) + + if not filepath.exists(): + print("ERROR: File " + filename + " not found") + sys.exit(1) + + +def parse_app_pack(app_pack, name): + params = None + image = [] + global package_name + + for key in app_pack: + if key.endswith("_pack"): + imagename = parse_app_pack(app_pack[key], name) + image += [imagename] + if key.lower() == "params": + params = app_pack[key] + if key.lower() == "infile": + if app_pack[key] != "": + image_path = os.path.join(config_path, app_pack[key]) + verify_file_exists(image_path) + image += [image_path] + if key.lower() == "outfile": + name = app_pack[key] + + # Exit if params or image are not specified + if (params is None) or (image is None): + return None + + cmd = [] + if img_tool.endswith('.py'): + cmd += [sys.executable] + + cmd += [img_tool] + cmd += ["sign"] + cmd += params.split() + + # Combine images for parent package (top level package) + if len(image) > 1: + combined_images = os.path.join(output_dir, "combined.bin") + + with open(combined_images, 'wb') as outfile: + for fname in image: + with open(fname, 'rb') as infile: + outfile.write(infile.read()) + infile.close() + outfile.close() + + image_input = combined_images + else: + image_input = image[0] + + image_output = os.path.join(output_dir, name) + + cmd += [image_input] + cmd += [image_output] + + print(f'Calling imgtool to generate file {image_output}') + package_name = image_output + subprocess.run(cmd) + + return image_output + + +if __name__ == "__main__": + main() diff --git a/scripts/combine_images.yaml b/scripts/combine_images.yaml new file mode 100644 index 0000000000..77f865f8e7 --- /dev/null +++ b/scripts/combine_images.yaml @@ -0,0 +1,36 @@ +# +# Copyright (C) 2025 Analog Devices, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed 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. + + +img0_pack: + outfile: combined_signed.bin + params: "--key ../root-rsa-2048.pem --header-size 0x20 --align 4 --load-addr 0x20080000 --pad-header --version 1.0.0 --slot-size 0x100000" + + img1_pack: + infile: img1.bin + outfile: img1_signed.bin + params: "--key ../root-rsa-2048.pem --header-size 0x20 --align 4 --load-addr 0x20010000 --pad-header --version 1.0.0 --slot-size 0x10000" + + img2_pack: + infile: img2.bin + outfile: img2_signed.bin + params: "--key ../root-rsa-2048.pem --header-size 0x20 --align 4 --load-addr 0x20020000 --pad-header --version 1.0.0 --slot-size 0x10000" + + img3_pack: + infile: img3.bin + outfile: img3_signed.bin + params: "--key ../root-rsa-2048.pem --header-size 0x20 --align 4 --load-addr 0x20030000 --pad-header --version 1.0.0 --slot-size 0x10000"