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: 10 additions & 2 deletions doc/build/signing/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,23 @@ 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
- ``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. A relative path
resolves against the application source directory when the file exists there, and
through ``APPLICATION_CONFIG_DIR`` / the west workspace topdir otherwise
- :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, see the
:zephyr:code-sample:`mcuboot_signing_keys` sample.

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
14 changes: 14 additions & 0 deletions doc/releases/release-notes-4.5.rst
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,20 @@ Other notable changes
* Removed the ``samples/net/wifi/test_certs/rsa2k`` enterprise test
certificates (DES-encrypted private keys). Use ``rsa2k_no_des`` instead.

* MCUboot

* ``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. A relative key path that resolves under the
application source directory is now anchored there, so the embedded public key
and the application signing key stay in sync; paths resolved elsewhere are
unchanged. See :ref:`build-signing` and the
:zephyr:code-sample:`mcuboot_signing_keys` sample.

..
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
56 changes: 56 additions & 0 deletions samples/sysbuild/mcuboot_signing_keys/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)

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

test_sysbuild()

target_sources(app PRIVATE src/main.c)

if(CONFIG_BOOTLOADER_MCUBOOT AND NOT CONFIG_MCUBOOT_GENERATE_UNSIGNED_IMAGE)
message(WARNING
"mcuboot_signing_keys is a demonstration sample: it also signs the "
"application with the production and an unknown key (zephyr.signed.prod.hex "
"and zephyr.signed.unknown.hex) using MCUboot's public TEST keys. A real "
"build must never hold the production private key -- do not copy this "
"signing step into a production application."
)

dt_nodelabel(slot1 NODELABEL "slot1_partition" REQUIRED)
dt_prop(slot_size PATH "${slot1}" PROPERTY "reg" INDEX 1 REQUIRED)

set(image ${ZEPHYR_BINARY_DIR}/${KERNEL_NAME})
set(imgtool_sign ${PYTHON_EXECUTABLE} ${ZEPHYR_MCUBOOT_MODULE_DIR}/scripts/imgtool.py sign
Comment thread
JPHutchins marked this conversation as resolved.
--version=${CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION}
--header-size=${CONFIG_ROM_START_OFFSET}
--slot-size=${slot_size}
--overwrite-only
--align=1
)

# Match the bootloader's hash/signature scheme (as cmake/mcuboot.cmake does).
if(CONFIG_MCUBOOT_BOOTLOADER_SIGNATURE_TYPE_PURE)
list(APPEND imgtool_sign --pure)
elseif(CONFIG_MCUBOOT_BOOTLOADER_USES_SHA512)
list(APPEND imgtool_sign --sha 512)
endif()

# ${image}.hex is a POST_BUILD byproduct of the Zephyr ELF target; depend on
# the target so the hex exists on every generator, not just Ninja.
add_custom_command(
OUTPUT ${image}.signed.prod.hex ${image}.signed.unknown.hex
COMMAND ${imgtool_sign}
--key=${ZEPHYR_MCUBOOT_MODULE_DIR}/root-ed25519-2.pem
${image}.hex ${image}.signed.prod.hex
COMMAND ${imgtool_sign}
--key=${ZEPHYR_MCUBOOT_MODULE_DIR}/root-ed25519-unknown.pem
${image}.hex ${image}.signed.unknown.hex
DEPENDS ${logical_target_for_zephyr_elf} ${image}.hex
COMMENT "Signing demo images with the production and unknown keys"
)
add_custom_target(mcuboot_signing_keys_demo_images ALL
DEPENDS ${image}.signed.prod.hex ${image}.signed.unknown.hex
)
endif()
229 changes: 229 additions & 0 deletions samples/sysbuild/mcuboot_signing_keys/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
.. zephyr:code-sample:: mcuboot_signing_keys
:name: MCUboot multiple signing keys with sysbuild

Build a development bootloader that boots images signed by either a
local development key or a remote production key.

Overview
********

This sample demonstrates a common use of a multi-key
``SB_CONFIG_BOOT_SIGNATURE_KEY_FILE``: a **development bootloader**
that accepts images signed with either the team's development key or
the production key, while production bootloaders (built separately)
accept only the production key.

``SB_CONFIG_BOOT_SIGNATURE_KEY_FILE`` takes a single PEM path *or* a
comma-separated list of PEMs. MCUboot embeds the public half of
every key in the list and, at boot, accepts an image signed by any one
of them. There is no fixed limit on the number of keys; this sample
uses two because the development/production split is the most common
case.

The security-critical property modelled here is that the development
workstation **never holds the production private key**. The production
team signs release images on a locked-down build server and
distributes only the public half of their signing key to development
workstations. That public half is embedded into development
bootloaders as a list entry — no private material is required or
present.

Keys used by this sample
========================

``SB_CONFIG_BOOT_SIGNATURE_KEY_FILE`` is set to a two-entry list:

- **First entry** — ``${ZEPHYR_MCUBOOT_MODULE_DIR}/root-ed25519.pem``,
a publicly-known MCUboot test key used here in place of a developer's
own signing key. The first entry is also the key the **application**
is signed with, so it must be a key pair.
- **Second entry** — :file:`keys/prod_pubkey.pem`, a PEM file
containing **only the public half** of a keypair. It was extracted
from MCUboot's ``root-ed25519-2.pem`` test key with
``imgtool getpub --encoding pem``, simulating what a dev workstation
would receive from a production team. Every list entry past the first
is verification-only and **must** be public-only; the build enforces
this with ``imgtool keyinfo --require public``.

:file:`sysbuild.conf` references the first key from the MCUboot module
directory with ``${ZEPHYR_MCUBOOT_MODULE_DIR}`` and the second with a
plain relative path. Sysbuild expands CMake variable references and
resolves relative signing-key paths against the application source
directory before forwarding the list to the MCUboot child image.

All keys here derive from MCUboot's publicly-available test keys and
are insecure. Substitute your own ED25519 keys when adopting this
pattern for production.

Files
=====

- :file:`sysbuild.conf` — enables MCUboot with ED25519 signing and sets
``SB_CONFIG_BOOT_SIGNATURE_KEY_FILE`` to the ``dev,prod`` key list.
- :file:`keys/prod_pubkey.pem` — public-only PEM shipped with the
sample.
- :file:`sysbuild/mcuboot.conf` — raises the MCUboot log level to debug
so the signature verification is visible at boot.
- :file:`prj.conf` — enables the Zephyr logging subsystem in the
application.
- :file:`CMakeLists.txt` — builds the application and, as a
demonstration aid, emits the production- and unknown-key signed images
used below.

Building and running
********************

.. zephyr-app-commands::
:tool: west
:zephyr-app: samples/sysbuild/mcuboot_signing_keys
:board: nrf52840dk/nrf52840
:goals: build flash
:west-args: --sysbuild
:compact:

The default build signs the application with the first (development)
key. With MCUboot debug logging enabled, the console shows MCUboot
selecting an embedded key and validating the image before chain-loading
it. Expected (abridged) output:

.. code-block:: console

*** Booting MCUboot ***
I: Starting bootloader
...
D: bootutil_img_validate: EXPECTED_KEY_TLV == 1
D: bootutil_find_key
D: bootutil_img_validate: EXPECTED_SIG_TLV == 36
D: bootutil_verify_sig: ED25519 key_id 0
...
I: Jumping to the first image slot
*** Booting Zephyr OS ***
[00:00:00.000,000] <inf> app: Address of sample 0xc000
[00:00:00.000,000] <inf> app: Hello mcuboot signing keys! nrf52840dk

The ``key_id`` on the ``bootutil_verify_sig`` line is the runtime proof of
*which* embedded key was used. ``bootutil_find_key`` hashes the image's key
TLV against each embedded key and returns its index, then the signature is
verified with that key. Here ``key_id 0`` is the first list entry
(``root-ed25519.pem``, emitted as ``autogen-pubkey.c``); an image signed
with the second key validates against ``key_id 1``
(:file:`keys/prod_pubkey.pem`, emitted as ``autogen-pubkey-1.c``).

What the build produces
***********************

After ``west build``, the bootloader and three signed application
images exist side by side:

- :file:`build/mcuboot/zephyr/zephyr.hex` — the MCUboot bootloader
with the public half of **every key in the list embedded** (the first
from ``root-ed25519.pem`` and the second from
:file:`keys/prod_pubkey.pem`). Inspect
:file:`build/mcuboot/zephyr/autogen-pubkey.c` (the first key) and
:file:`autogen-pubkey-1.c` (the second; the trailing index disambiguates
the emitted C symbol names) to see the embedded byte arrays. Neither
file contains private key material — both are pure public-key data.
- :file:`build/mcuboot_signing_keys/zephyr/zephyr.signed.hex` — the
application signed with the **first (development)** key. This is the
image ``west flash`` programs by default.
- :file:`build/mcuboot_signing_keys/zephyr/zephyr.signed.prod.hex` and
:file:`build/mcuboot_signing_keys/zephyr/zephyr.signed.unknown.hex` —
the same application re-signed with the production key and with an
unrecognized key. The sample's :file:`CMakeLists.txt` emits these so
multi-key acceptance can be checked by flashing a prebuilt image. They
are a **demonstration shortcut** that signs with MCUboot's public test
keys; see the note under *Verifying acceptance*.

``west flash`` programs the bootloader and the default
(development-signed) application automatically (the flash order is
recorded in
:file:`build/domains.yaml`); the production- and unknown-signed copies
are flashed on demand, as shown under *Verifying acceptance*. For a single
combined hex, enable :kconfig:option:`SB_CONFIG_MERGED_HEX_FILES` in
:file:`sysbuild.conf` and sysbuild will emit
:file:`build/merged_<board_target>.hex`. See
:ref:`sysbuild_merged_hex_files`.

Which key signs the application?
********************************

The application's default image (:file:`zephyr.signed.hex`) is signed
at build time by imgtool using the **first** list entry (forwarded to
``CONFIG_MCUBOOT_SIGNATURE_KEY_FILE``). The bootloader uses the
remaining embedded keys only for verification — a development
workstation should only hold the private key it is authorized to sign
with.

To change which key signs the application, make the desired private key
the first entry of ``SB_CONFIG_BOOT_SIGNATURE_KEY_FILE`` and rebuild —
the local development key on a developer's machine, or the production
key on the release build server.

Verifying acceptance of the second key
**************************************

The build already produced
:file:`build/mcuboot_signing_keys/zephyr/zephyr.signed.prod.hex` — the
application signed with the production key. Flash it in place of the
default image to confirm the bootloader accepts the **second** embedded
key. ``--domain`` reflashes only the application slot, leaving the
bootloader in place:

.. code-block:: console

west flash -d build --domain mcuboot_signing_keys \
--hex-file build/mcuboot_signing_keys/zephyr/zephyr.signed.prod.hex

The bootloader verifies and chain-loads it identically, but the debug
log now reports ``bootutil_verify_sig: ED25519 key_id 1`` — the
*second* embedded key — instead of ``key_id 0``. Seeing the ``key_id``
change from 0 to 1 between the development- and production-signed
images is the runtime confirmation that both keys are embedded and that
MCUboot selects the matching one per image.

.. important::

Producing a production-signed image requires the production *private*
key, which a development workstation must not have. This sample can
emit :file:`zephyr.signed.prod.hex` only because it uses MCUboot's
publicly-available test keys — it is a demonstration shortcut. In a
real deployment the release team signs production images on their own
build server and hands the development team the finished binary; the
production private key never reaches the developer's machine.

As a negative check, flash
:file:`build/mcuboot_signing_keys/zephyr/zephyr.signed.unknown.hex`,
signed with a key the bootloader does not embed:

.. code-block:: console

west flash -d build --domain mcuboot_signing_keys \
--hex-file build/mcuboot_signing_keys/zephyr/zephyr.signed.unknown.hex

MCUboot should reject it. With the debug logging this sample enables,
``bootutil_find_key`` matches none of the embedded keys, so **no**
``bootutil_verify_sig: key_id`` line is printed at all; the rejection
is reported as ``Image in the primary slot is not valid!`` and the
application is not booted. Re-flash the development image with
``west flash -d build`` to return to a bootable state.

How the sample proves the public-only property
**********************************************

After a default build, inspect :file:`build/mcuboot/zephyr/.config`
and :file:`build/mcuboot/zephyr/autogen-pubkey-1.c`:

- ``CONFIG_BOOT_SIGNATURE_KEY_FILE`` ends with
:file:`keys/prod_pubkey.pem` — a PEM file whose only content is
``-----BEGIN PUBLIC KEY-----``.
- ``autogen-pubkey-1.c`` contains a valid ED25519 public key byte
array. No private-key material was involved at any point.

You can confirm the custody roles of any PEM directly::

imgtool keyinfo -k keys/prod_pubkey.pem # -> public
imgtool keyinfo -k <your dev key>.pem # -> private

Attempting to ``imgtool sign`` with the sample's
:file:`prod_pubkey.pem` fails (as it should — signing requires a
private key).
3 changes: 3 additions & 0 deletions samples/sysbuild/mcuboot_signing_keys/keys/prod_pubkey.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----

Check warning on line 1 in samples/sysbuild/mcuboot_signing_keys/keys/prod_pubkey.pem

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

Copyright missing

samples/sysbuild/mcuboot_signing_keys/keys/prod_pubkey.pem:1 File has no SPDX-FileCopyrightText header, consider adding one. Check SPDX headers and copyright lines with the reuse Python API.

Check warning on line 1 in samples/sysbuild/mcuboot_signing_keys/keys/prod_pubkey.pem

View workflow job for this annotation

GitHub Actions / Run compliance checks on patch series (PR)

License missing

samples/sysbuild/mcuboot_signing_keys/keys/prod_pubkey.pem:1 File has no SPDX-License-Identifier header, consider adding one. Check SPDX headers and copyright lines with the reuse Python API.
MCowBQYDK2VwAyEAw7zfMHZOH120DYkuDQ6rBJwwBk55qO2293kuRpom2nc=
-----END PUBLIC KEY-----
2 changes: 2 additions & 0 deletions samples/sysbuild/mcuboot_signing_keys/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Enable logging so the application's banner reaches the console.
CONFIG_LOG=y
18 changes: 18 additions & 0 deletions samples/sysbuild/mcuboot_signing_keys/sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
sample:
description: Sample demonstrating MCUboot with multiple accepted signing keys
name: mcuboot signing keys
tests:
sample.sysbuild.mcuboot_signing_keys:
sysbuild: true
platform_allow:
- nrf52840dk/nrf52840
- frdm_k64f
integration_platforms:
- nrf52840dk/nrf52840
tags: mcuboot
harness: console
harness_config:
type: multi_line
regex:
- "Address of sample(.*)"
- "Hello mcuboot signing keys!(.*)"
18 changes: 18 additions & 0 deletions samples/sysbuild/mcuboot_signing_keys/src/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Intercreate, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*/

#include <zephyr/kernel.h>
#include <zephyr/linker/linker-defs.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(app, LOG_LEVEL_INF);

int main(void)
{
LOG_INF("Address of sample %p", (void *)__rom_region_start);
LOG_INF("Hello mcuboot signing keys! %s", CONFIG_BOARD);
return 0;
}
Loading
Loading