Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions doc/build/signing/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,22 @@ Notes on the above commands:

For more information on these and other related configuration options, see:

- ``SB_CONFIG_BOOTLOADER_MCUBOOT``: build the application for loading by MCUboot
- ``SB_CONFIG_BOOT_SIGNATURE_KEY_FILE``: the key file to use when signing images. If you have
your own key, change this appropriately
- :kconfig:option:`SB_CONFIG_BOOTLOADER_MCUBOOT`: build the application for loading by MCUboot
- :kconfig:option:`SB_CONFIG_BOOT_SIGNATURE_KEY_FILE`: the key file, or a comma-separated list of
key files, to use when signing images. If you have your own key, change this appropriately.
When a list is given, MCUboot embeds the public half of every key and accepts an image
signed with any of them; the first entry also signs the application, and every entry
past the first must be a public-only PEM of the same signature type
- :kconfig:option:`CONFIG_MCUBOOT_EXTRA_IMGTOOL_ARGS`: optional additional command line arguments
for ``imgtool``
- :kconfig:option:`CONFIG_MCUBOOT_GENERATE_CONFIRMED_IMAGE`: also generate a confirmed image,
which may be more useful for flashing in production environments than the OTA-able default image
- On Windows, if you get "Access denied" issues, the recommended fix is to run
``pip3 install imgtool``, then retry with a pristine build directory.

For a worked example of a multi-key bootloader exercised end-to-end under QEMU, see the
:zephyr_file:`tests/boot/mcuboot_multiple_keys` test.

If your ``west flash`` :ref:`runner <west-runner>` uses an image format supported by imgtool, you
should see something like this on your device's serial console when you run
``west flash -d build-hello-signed``:
Expand Down
10 changes: 10 additions & 0 deletions doc/releases/release-notes-4.5.rst
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,16 @@ Other notable changes
* Removed the ``samples/net/wifi/test_certs/rsa2k`` enterprise test
certificates (DES-encrypted private keys). Use ``rsa2k_no_des`` instead.

* MCUboot

* :kconfig:option:`SB_CONFIG_BOOT_SIGNATURE_KEY_FILE` now accepts a comma-separated list of
key files, embedding the public half of each in the MCUboot bootloader. When more
than one key is given, MCUboot accepts an image signed with any of them -- the
typical use is a development bootloader that boots both development- and
production-signed images, while production bootloaders embed only the production
key. The first entry is the key the application is signed with and the rest are
verification-only public keys. See :ref:`build-signing`.

..
Any more descriptive subsystem or driver changes. Do you really want to write
a paragraph or is it enough to link to the api/driver/Kconfig/board page above?
2 changes: 1 addition & 1 deletion modules/Kconfig.mcuboot
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ config MCUBOOT_SIGNATURE_KEY_FILE
The existence of bin and hex files depends on CONFIG_BUILD_OUTPUT_BIN
and CONFIG_BUILD_OUTPUT_HEX.

This option should contain a path to the same file as the
This option should contain a path to the same file as the (first)
Comment thread
JPHutchins marked this conversation as resolved.
BOOT_SIGNATURE_KEY_FILE option in your MCUboot .config. The path
may be absolute or relative to the west workspace topdir. (The MCUboot
config option is used for the MCUboot bootloader image; this option is
Expand Down
44 changes: 44 additions & 0 deletions share/sysbuild/cmake/modules/sysbuild_extensions.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,50 @@ function(set_config_int image setting value)
set_property(TARGET ${image} APPEND_STRING PROPERTY CONFIG "${setting}=${value}\n")
endfunction()

# Usage:
# sysbuild_mcuboot_resolve_signature_key_files(<out_var> <key_files>)
#
# Normalize a BOOT_SIGNATURE_KEY_FILE value -- a single path or a comma-separated
# list -- for forwarding to an image: strip surrounding whitespace from each
# entry and warn on (and skip) an empty entry (a stray comma). Entries are
# forwarded verbatim for each consumer to resolve, and stay comma-separated
# (';' would not survive set_config_string() or -D overrides).
function(sysbuild_mcuboot_resolve_signature_key_files out_var key_files)
string(REPLACE "," ";" key_list "${key_files}")
set(resolved "")
# IN LISTS keeps empty elements (unlike unquoted expansion, which drops them),
# so a stray/leading/trailing/double comma is warned about below instead of
# silently collapsing the key set.
foreach(key_path IN LISTS key_list)
string(STRIP "${key_path}" key_path)
if(key_path STREQUAL "")
message(WARNING
"Empty entry in a BOOT_SIGNATURE_KEY_FILE list (\"${key_files}\"); "
"check for a stray, leading, or trailing comma."
)
continue()
endif()
list(APPEND resolved "${key_path}")
endforeach()
string(REPLACE ";" "," resolved "${resolved}")
set(${out_var} "${resolved}" PARENT_SCOPE)
endfunction()

Comment thread
JPHutchins marked this conversation as resolved.
# Usage:
# sysbuild_mcuboot_application_signature_key_file(<out_var> <key_files>)
#
# Set <out_var> to the key the application is signed with: the first entry of
# the resolved <key_files> list (the MCUboot bootloader embeds the public half
# of every entry; the application is signed with exactly one).
function(sysbuild_mcuboot_application_signature_key_file out_var key_files)
sysbuild_mcuboot_resolve_signature_key_files(resolved "${key_files}")
if(NOT resolved STREQUAL "")
string(REPLACE "," ";" resolved "${resolved}")
list(GET resolved 0 resolved)
endif()
set(${out_var} "${resolved}" PARENT_SCOPE)
endfunction()

# Usage:
# sysbuild_add_subdirectory(<source_dir> [<binary_dir>])
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
# on a firmware updater image.

set_config_bool(${ZCMAKE_APPLICATION} CONFIG_BOOTLOADER_MCUBOOT "${SB_CONFIG_BOOTLOADER_MCUBOOT}")

sysbuild_mcuboot_application_signature_key_file(
application_signature_key_file "${SB_CONFIG_BOOT_SIGNATURE_KEY_FILE}"
)
set_config_string(${ZCMAKE_APPLICATION} CONFIG_MCUBOOT_SIGNATURE_KEY_FILE
"${SB_CONFIG_BOOT_SIGNATURE_KEY_FILE}"
"${application_signature_key_file}"
)
set_config_string(${ZCMAKE_APPLICATION} CONFIG_MCUBOOT_ENCRYPTION_KEY_FILE
"${SB_CONFIG_BOOT_ENCRYPTION_KEY_FILE}"
Expand Down
6 changes: 5 additions & 1 deletion share/sysbuild/image_configurations/MAIN_image_default.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
# on the main Zephyr image.

set_config_bool(${ZCMAKE_APPLICATION} CONFIG_BOOTLOADER_MCUBOOT "${SB_CONFIG_BOOTLOADER_MCUBOOT}")

sysbuild_mcuboot_application_signature_key_file(
application_signature_key_file "${SB_CONFIG_BOOT_SIGNATURE_KEY_FILE}"
)
set_config_string(${ZCMAKE_APPLICATION} CONFIG_MCUBOOT_SIGNATURE_KEY_FILE
"${SB_CONFIG_BOOT_SIGNATURE_KEY_FILE}"
"${application_signature_key_file}"
)
set_config_string(${ZCMAKE_APPLICATION} CONFIG_MCUBOOT_ENCRYPTION_KEY_FILE
"${SB_CONFIG_BOOT_ENCRYPTION_KEY_FILE}"
Expand Down
5 changes: 4 additions & 1 deletion share/sysbuild/images/bootloader/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ if(SB_CONFIG_BOOTLOADER_MCUBOOT)
# Link the footer size output of this MCUboot build with the main sysbuild image
set_target_properties(${image} PROPERTIES MCUBOOT_FOOTER_UPDATE_IMAGES ${DEFAULT_IMAGE})

set_config_string(${image} CONFIG_BOOT_SIGNATURE_KEY_FILE "${SB_CONFIG_BOOT_SIGNATURE_KEY_FILE}")
sysbuild_mcuboot_resolve_signature_key_files(
resolved_key_files "${SB_CONFIG_BOOT_SIGNATURE_KEY_FILE}"
)
set_config_string(${image} CONFIG_BOOT_SIGNATURE_KEY_FILE "${resolved_key_files}")

if(SB_CONFIG_MCUBOOT_DIRECT_XIP_GENERATE_VARIANT)
ExternalZephyrVariantProject_Add(
Expand Down
8 changes: 6 additions & 2 deletions share/sysbuild/images/bootloader/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,17 @@ config BOOT_SIGNATURE_TYPE_ED25519
endchoice

config BOOT_SIGNATURE_KEY_FILE
string "Signing PEM key file" if !BOOT_SIGNATURE_TYPE_NONE
string "Signing PEM key file(s) (or comma-separated list)" if !BOOT_SIGNATURE_TYPE_NONE
default "$(ZEPHYR_MCUBOOT_MODULE_DIR)/root-ec-p256.pem" if BOOT_SIGNATURE_TYPE_ECDSA_P256
default "$(ZEPHYR_MCUBOOT_MODULE_DIR)/root-ed25519.pem" if BOOT_SIGNATURE_TYPE_ED25519
default "$(ZEPHYR_MCUBOOT_MODULE_DIR)/root-rsa-2048.pem" if BOOT_SIGNATURE_TYPE_RSA
default ""
help
Absolute path to signing key file to use with MCUBoot.
Path to the signing key file, or a comma-separated list of key files
to embed more than one verification key in the bootloader (an image
signed with any of them is accepted). The first entry also signs the
application; every later entry is verification-only and must be a
public-only PEM of the same signature type.

config SUPPORT_BOOT_ENCRYPTION
bool
Expand Down
10 changes: 10 additions & 0 deletions tests/boot/mcuboot_multiple_keys/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright (c) 2026 Intercreate, Inc.
#
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(mcuboot_multiple_keys)

target_sources(app PRIVATE src/main.c)
36 changes: 36 additions & 0 deletions tests/boot/mcuboot_multiple_keys/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
MCUboot Multiple Signing Keys
#############################

Runtime acceptance test for an MCUboot bootloader built with more than one
verification key: a single bootloader embeds two keys and boots an application
signed by *either* one, exercised entirely under QEMU.

:kconfig:option:`SB_CONFIG_BOOT_SIGNATURE_KEY_FILE` takes a comma-separated list
of keys. MCUboot embeds the public half of each; the application is signed with
the first. This test models a *development* bootloader that trusts both a
development and a production key, using keys shipped with MCUboot:

* ``key_id 0`` -- ``root-ed25519.pem``, the development key whose private half
signs the application in the default build;
* ``key_id 1`` -- ``root-ed25519-2-pub.pem``, the production key's public half
only: the bootloader trusts production-signed images without ever holding the
production private key.

The two scenarios differ only in how the application is signed:

* **key0** boots the application as built -- signed with the development key.
* **key1** re-signs the built application with the production private key
(``root-ed25519-2.pem``) *after* the build and boots that, modelling a
production custodian who signs the release binary out-of-band. The re-sign
reuses the build's own imgtool invocation; see ``pytest/test_multiple_keys.py``.

How the QEMU Test Works
*******************************

A ``harness: pytest`` test (``pytest/test_multiple_keys.py``) starts QEMU with
two ``-device loader`` entries -- MCUboot's hex and the signed application
preloaded into slot0 -- and reads the console. With
:kconfig:option:`CONFIG_MCUBOOT_LOG_LEVEL_DBG` MCUboot prints
``bootutil_verify_sig: ED25519 key_id N``, the runtime proof of which embedded
key validated the image; the test asserts the expected ``key_id`` and that the
application banner then prints from the primary slot.
12 changes: 12 additions & 0 deletions tests/boot/mcuboot_multiple_keys/boards/mps2_an385.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright (c) 2026 Intercreate, Inc.
#
# SPDX-License-Identifier: Apache-2.0

CONFIG_FLASH=y
CONFIG_FLASH_SIMULATOR=y

CONFIG_RETAINED_MEM=y
CONFIG_RETENTION=y
CONFIG_RETAINED_MEM_ZEPHYR_RAM=y
CONFIG_RETENTION_BOOTLOADER_INFO=y
CONFIG_RETENTION_BOOTLOADER_INFO_TYPE_MCUBOOT=y
106 changes: 106 additions & 0 deletions tests/boot/mcuboot_multiple_keys/boards/mps2_an385.overlay
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2026 Intercreate, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*
* mps2/an385 memory map for the MCUboot RAM_LOAD fixture, and the single
* source of the flash-simulator layout: sysbuild/mcuboot.overlay includes
* this file so both images agree on the partition table.
*
* This is the application image. MCUboot copies slot0 into the sram0 exec
* region defined here and jumps to it. sram0 is declared before the FlashSim
* and RetainedMem regions (also mmio-sram) so it stays mmio-sram instance 0.
* The image slots use zephyr,mapped-partition, so flash_sim0's inherited
* fixed-partitions node is deleted first.
*
* Adapted from github.com/intercreate/smp-server-fixtures
*/

/delete-node/ &sram0;
/delete-node/ &{/sim_flash_controller/flash_sim@0/partitions};

/ {
chosen {
zephyr,flash = &flash0;
zephyr,console = &uart0;
zephyr,bootloader-info = &boot_info0;

/delete-property/ zephyr,code-partition;
};

sram0: memory@20240000 {
compatible = "mmio-sram";
reg = <0x20240000 0x001bf000>;
};

flash_sim_region: memory@20040000 {
compatible = "zephyr,memory-region", "mmio-sram";
reg = <0x20040000 0x00200000>; /* 2 MB sim-flash backing store */
ranges = <0x0 0x20040000 0x00200000>;
zephyr,memory-region = "FlashSim";
status = "okay";
#address-cells = <1>;
#size-cells = <1>;
};

sram_retained: memory@203ffc00 {
compatible = "zephyr,memory-region", "mmio-sram";
reg = <0x203ffc00 0x00000400>;
ranges = <0x0 0x203ffc00 0x00000400>;
zephyr,memory-region = "RetainedMem";
status = "okay";
#address-cells = <1>;
#size-cells = <1>;

retainedmem {
compatible = "zephyr,retained-ram";
status = "okay";
ranges;
#address-cells = <1>;
#size-cells = <1>;

boot_info0: boot_info@0 {
compatible = "zephyr,retention";
status = "okay";
reg = <0x0 0x100>;
};
};
};
};

&sim_flash_controller {
memory-region = <&flash_sim_region>;
};

&flash_sim0 {
reg = <0x00000000 0x00200000>;
erase-block-size = <4096>;
write-block-size = <4>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0x0 0x0 0x00200000>;

partitions {
ranges;
#address-cells = <1>;
#size-cells = <1>;

boot_partition: partition@0 {
compatible = "zephyr,mapped-partition";
label = "mcuboot";
reg = <0x00000000 0x00010000>;
};

slot0_partition: partition@10000 {
compatible = "zephyr,mapped-partition";
label = "image-0";
reg = <0x00010000 0x00080000>;
};

slot1_partition: partition@90000 {
compatible = "zephyr,mapped-partition";
label = "image-1";
reg = <0x00090000 0x00080000>;
};
};
};
1 change: 1 addition & 0 deletions tests/boot/mcuboot_multiple_keys/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# nothing here
39 changes: 39 additions & 0 deletions tests/boot/mcuboot_multiple_keys/pytest/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) 2026 Intercreate, Inc.
#
# SPDX-License-Identifier: Apache-2.0

import os
import sys

import pytest

# Add the directory to PYTHONPATH
zephyr_base = os.getenv("ZEPHYR_BASE")
if zephyr_base:
sys.path.insert(
0, os.path.join(zephyr_base, "scripts", "pylib", "pytest-twister-harness", "src")
)
else:
raise OSError("ZEPHYR_BASE environment variable is not set")

pytest_plugins = [
"twister_harness.plugin",
]


def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption(
"--expected-key-id",
type=int,
required=True,
help="key_id MCUboot is expected to validate the application against",
)
Comment thread
JPHutchins marked this conversation as resolved.
parser.addoption(
"--resign-key",
default="",
help=(
"filename of a key (resolved against the build's signing-key "
"directory) to re-sign the application with before loading it, "
"modelling a production custodian signing the release binary"
),
)
Loading