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
14 changes: 12 additions & 2 deletions doc/build/signing/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,25 @@
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 is also the key the application is signed with,
and every entry past the first must be a public-only PEM (all keys must use the same
signature type). Typical use: development bootloaders that should boot both production-

Check failure on line 45 in doc/build/signing/index.rst

View workflow job for this annotation

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

SphinxLint

doc/build/signing/index.rst:45 Line ends with dangling hyphen (dangling-hyphen) Check Sphinx/reStructuredText files with sphinx-lint.
and development-signed images, while production bootloaders embed only the production key
- :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 embedding more than one verification key — a development
bootloader that boots both development- and production-signed images while never
holding the production private key — 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
9 changes: 9 additions & 0 deletions doc/releases/release-notes-4.5.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,15 @@ 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 first
entry is the key the application is signed with and the rest are verification-only
public keys. 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?
6 changes: 5 additions & 1 deletion modules/Kconfig.mcuboot
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,18 @@ 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)
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
for your application which is to be loaded by MCUboot. The MCUboot
config option can be a relative path from the MCUboot repository
root.)

The application is always signed with a single key. When the MCUboot
bootloader embeds several verification keys (BOOT_SIGNATURE_KEY_FILE is
a comma-separated list), this is the first entry of that list.

If left empty and MCUBOOT_GENERATE_UNSIGNED_IMAGE is not set, you
must sign and prepare the Zephyr binaries manually to be bootable
from MCUboot.
Expand Down
52 changes: 52 additions & 0 deletions samples/sysbuild/mcuboot_signing_keys/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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)

# Demonstration aid: re-sign the built image with extra keys so multi-key
# acceptance can be checked by flashing a prebuilt hex instead of re-signing by
# hand. The signing parameters mirror the build's own overwrite-only ED25519
# signing (see cmake/mcuboot.cmake).
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
--version=${CONFIG_MCUBOOT_IMGTOOL_SIGN_VERSION}
--header-size=${CONFIG_ROM_START_OFFSET}
--slot-size=${slot_size}

Check failure on line 32 in samples/sysbuild/mcuboot_signing_keys/CMakeLists.txt

View workflow job for this annotation

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

CMakeStyle

samples/sysbuild/mcuboot_signing_keys/CMakeLists.txt:32 CMakeStyle See https://docs.zephyrproject.org/latest/contribute/style/cmake.html for more details.
--overwrite-only
--align=1

Check failure on line 34 in samples/sysbuild/mcuboot_signing_keys/CMakeLists.txt

View workflow job for this annotation

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

CMakeStyle

samples/sysbuild/mcuboot_signing_keys/CMakeLists.txt:34 CMakeStyle See https://docs.zephyrproject.org/latest/contribute/style/cmake.html for more details.
Comment on lines +32 to +34
)

# Re-sign the built image (a declared build byproduct) with each demo key.
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

Check failure on line 44 in samples/sysbuild/mcuboot_signing_keys/CMakeLists.txt

View workflow job for this annotation

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

CMakeStyle

samples/sysbuild/mcuboot_signing_keys/CMakeLists.txt:44 CMakeStyle See https://docs.zephyrproject.org/latest/contribute/style/cmake.html for more details.
${image}.hex ${image}.signed.unknown.hex
DEPENDS ${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()
246 changes: 246 additions & 0 deletions samples/sysbuild/mcuboot_signing_keys/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
.. 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 the canonical real-world 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 standing in for the 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-

Check failure on line 137 in samples/sysbuild/mcuboot_signing_keys/README.rst

View workflow job for this annotation

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

SphinxLint

samples/sysbuild/mcuboot_signing_keys/README.rst:137 Line ends with dangling hyphen (dangling-hyphen) Check Sphinx/reStructuredText files with sphinx-lint.
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`.

The security property this build demonstrates
=============================================

A developer with only :file:`keys/prod_pubkey.pem` (no production
private key) can produce a bootloader that accepts production-signed
images. They cannot themselves produce such an image — only the
production team, who holds the private half, can. This is the
separation of duties a multi-key bootloader is designed to enforce.

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. (This sample additionally re-signs demo copies with the other
keys, as described under *Verifying acceptance*, purely to make the
multi-key behavior easy to observe.)

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.

The two bootloaders are deliberately asymmetric: a **development**
bootloader also lists the production public key, so it accepts both
development- and production-signed images, whereas a **production**
bootloader lists only the production key and so rejects
development-signed images. That asymmetry is the point — production
units run only production firmware.

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-----
5 changes: 5 additions & 0 deletions samples/sysbuild/mcuboot_signing_keys/prj.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Enable the Zephyr logging subsystem in the application so its boot banner is
# emitted through the same console as MCUboot's verification output. Immediate
# mode keeps the application's log lines ordered with the bootloader's.
CONFIG_LOG=y
CONFIG_LOG_MODE_IMMEDIATE=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!(.*)"
Loading
Loading