From 8efe6ad75f88525928994240f6485988650169ff Mon Sep 17 00:00:00 2001 From: Michael Eskowitz Date: Wed, 2 Jul 2025 09:35:41 -0400 Subject: [PATCH 1/2] scripts: imgtool: Add script to combine multiple image as single MCUBoot multi image support mode requires each image be procedded individually that requires multiple signature check during boot. If there be 4 image it will require 4 times signature validatation. This feature increase boot time and depend on the project long boot time may not be expected. To provide a solution for this case we propose to - Generate each image as regular - Concatanate them and re generate a main mcuboot image. ------------------------- | Header | ------------------------- | SubImage (optional) | | (Header+Data+Footer) | ------------------------- | SubImage (optional) | | (Header+Data+Footer) | ------------------------- | ..... | ------------------------- | Footer | ------------------------- During boot time if top level image be validated sub image can be just copied to the target location without to recheck them. To provide this feature there will be two commit 1- Provide a script that combine images 2- Update mcuboot source code to process subimages This commit is for #1, in this commit we are adding a script which called as combine_images.py. The usage of the script is: python combine_images.py --config combine_images.yaml --imgtool imgtool --output combine_images.yaml file is added as reference file it need to be updated as per of the project need. Signed-off-by: Sadik Ozer Signed-off-by: Michael Eskowitz Michael.Eskowitz@analog.com --- scripts/combine_images.py | 178 ++++++++++++++++++++++++++++++++++++ scripts/combine_images.yaml | 53 +++++++++++ 2 files changed, 231 insertions(+) create mode 100644 scripts/combine_images.py create mode 100644 scripts/combine_images.yaml diff --git a/scripts/combine_images.py b/scripts/combine_images.py new file mode 100644 index 0000000000..45c1f8aefe --- /dev/null +++ b/scripts/combine_images.py @@ -0,0 +1,178 @@ +#! /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 + + +# Function definitions + + +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): + header = None + image = [] + imgtype = 0 + global package_name + + for key in app_pack: + if key.endswith("_pack"): + imagename, imagetype = parse_app_pack(app_pack[key], name) + image += [imagename] + if key.lower() == "header": + header = app_pack[key] + if key.lower() == "image": + image_path = os.path.join(config_path, app_pack[key]) + verify_file_exists(image_path) + image += [image_path] + if key.lower() == "outputfile": + name = app_pack[key] + + # Exit if header or image are not specified + if (header is None) or (image is None): + return (None, 0) + + operation = 0 + + cmd = [] + if img_tool.endswith('.py'): + cmd += [sys.executable] + + cmd += [img_tool] + cmd += ["sign"] + + for key in header: + if key == "align": + cmd += ["--align", str(header[key])] + elif key == "aes-gcm-key": + operation += 2 + gcm_key_path = os.path.join(config_path, header[key]) + verify_file_exists(gcm_key_path) + cmd += ["--aes-gcm-key", gcm_key_path] + elif key == "aes-kw-key": + operation += 2 + kw_key_path = os.path.join(config_path, header[key]) + verify_file_exists(kw_key_path) + cmd += ["--aes-kw-key", kw_key_path] + elif key == "header-size": + cmd += ["--header-size", hex(header[key])] + elif key == "load-addr": + cmd += ["--load-addr", hex(header[key])] + elif key == "pad-header": + if header[key] is True: + cmd += ["--pad-header"] + elif key == "private_signing_key": + operation += 1 + private_key_path = os.path.join(config_path, header[key]) + verify_file_exists(private_key_path) + cmd += ["--key", private_key_path] + elif key == "public-key-format": + cmd += ["--public-key-format", header[key]] + elif key == "slot-size": + cmd += ["--slot-size", hex(header[key])] + elif key == "version": + cmd += ["--version", header[key]] + else: + print("Unknown argument: " + key) + + # If there are multiple input files they must be combined for signing or encryption + if len(image) > 1: + combined_images = os.path.join(output_dir, name + ".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] + + if operation > 1: + image_output = os.path.join(output_dir, name + "_signed_encrypted.bin") + else: + image_output = os.path.join(output_dir, name + "_signed.bin") + + 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, imgtype) + + +if __name__ == "__main__": + main() diff --git a/scripts/combine_images.yaml b/scripts/combine_images.yaml new file mode 100644 index 0000000000..98bec2d3e6 --- /dev/null +++ b/scripts/combine_images.yaml @@ -0,0 +1,53 @@ +#! /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. + +combined_app_pack: + outputfile: combined + + header: + private_signing_key: ../root-rsa-2048.pem + header-size: 0x400 + align: 4 + load-addr: 0x20080000 + pad-header: yes + version: 1.0.0 + slot-size: 0x40000 + + image1_pack: + outputfile: image1 + header: + private_signing_key: ../root-rsa-2048.pem + header-size: 0x400 + align: 4 + load-addr: 0x20010000 + pad-header: yes + version: 1.0.0 + slot-size: 0x10000 + image: image1.bin + + image2_pack: + outputfile: image2 + header: + private_signing_key: ../root-rsa-2048.pem + header-size: 0x400 + align: 4 + load-addr: 0x20020000 + pad-header: yes + version: 1.0.0 + slot-size: 0x10000 + image: image2.bin From ae6d9cc0bd58331a64de3f1ea8dac50d76985076 Mon Sep 17 00:00:00 2001 From: Sadik Ozer Date: Wed, 24 Sep 2025 16:34:57 +0300 Subject: [PATCH 2/2] boot: bootutils: Add image in image support This is source code update to provide images in images feature support. As mentioned in prev commit: MCUBoot multi image support mode requires each image be procedded individually that requires multiple signature check during boot. If there be 4 image it will require 4 times signature validatation. This feature increase boot time and depend on the project long boot time may not be expected. In this commit the loader.c file update to search subimages and copy them in the related load_address. By this solution: - Image update will be handled as requlary - Signature check will be executed for combined image - Boot time will be decreased by eliminating multiple signature check This featue only support for (BOOT_IMAGE_NUMBER == 1 && MCUBOOT_RAM_LOAD) case. Signed-off-by: Sadik Ozer --- boot/bootutil/src/loader.c | 132 +++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/boot/bootutil/src/loader.c b/boot/bootutil/src/loader.c index 9769428b4d..7cd771fd84 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: * @@ -3067,6 +3068,130 @@ context_boot_go(struct boot_loader_state *state, struct boot_rsp *rsp) } #endif /* MCUBOOT_DIRECT_XIP || MCUBOOT_RAM_LOAD */ + +#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) { + BOOT_LOG_INF("IMAGE_MAGIC not correct."); + 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; + + /* 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. */ + 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, so it migth be single image return 0 */ + rc = 0; + 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; + } + + return rc; +} +#endif // #if (BOOT_IMAGE_NUMBER == 1) && defined(MCUBOOT_RAM_LOAD) + /** * Prepares the booting process. This function moves images around in flash as * appropriate, and tells you what address to boot from. @@ -3083,6 +3208,13 @@ boot_go(struct boot_rsp *rsp) boot_state_clear(NULL); FIH_CALL(context_boot_go, fih_rc, &boot_data, rsp); + +#if (BOOT_IMAGE_NUMBER == 1) && defined(MCUBOOT_RAM_LOAD) + if (FIH_EQ(fih_rc, FIH_SUCCESS)) { + (void) process_sub_images(rsp->br_hdr->ih_load_addr, rsp); + } +#endif + FIH_RET(fih_rc); }