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
63 changes: 57 additions & 6 deletions boot/zephyr/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -405,10 +405,18 @@ if(CONFIG_MCUBOOT_SERIAL)
endif()

if(NOT CONFIG_BOOT_SIGNATURE_USING_KMU AND NOT CONFIG_BOOT_SIGNATURE_KEY_FILE STREQUAL "")
# Extract primary key; split on comma only when BOOT_SIGNATURE_KEY_FILE_MULTI is enabled.
if(CONFIG_BOOT_SIGNATURE_KEY_FILE_MULTI)
string(REPLACE "," ";" _mcuboot_key_files "${CONFIG_BOOT_SIGNATURE_KEY_FILE}")
else()
set(_mcuboot_key_files "${CONFIG_BOOT_SIGNATURE_KEY_FILE}")
endif()
list(GET _mcuboot_key_files 0 _primary_key_path)

# CONF_FILE points to the KConfig configuration files of the bootloader.
foreach (filepath ${CONF_FILE})
file(READ ${filepath} temp_text)
string(FIND "${temp_text}" ${CONFIG_BOOT_SIGNATURE_KEY_FILE} match)
string(FIND "${temp_text}" ${_primary_key_path} match)
if (${match} GREATER_EQUAL 0)
if (NOT DEFINED CONF_DIR)
get_filename_component(CONF_DIR ${filepath} DIRECTORY)
Expand All @@ -418,13 +426,17 @@ if(NOT CONFIG_BOOT_SIGNATURE_USING_KMU AND NOT CONFIG_BOOT_SIGNATURE_KEY_FILE ST
endif()
endforeach()

if(IS_ABSOLUTE ${CONFIG_BOOT_SIGNATURE_KEY_FILE})
set(KEY_FILE ${CONFIG_BOOT_SIGNATURE_KEY_FILE})
# Expand CMake variables (e.g. ${CMAKE_CURRENT_LIST_DIR}) after scanning
# CONF_FILE with the raw text, so the scan still matches the .conf content.
string(CONFIGURE "${_primary_key_path}" _primary_key_path)

if(IS_ABSOLUTE ${_primary_key_path})
set(KEY_FILE ${_primary_key_path})
elseif((DEFINED CONF_DIR) AND
(EXISTS ${CONF_DIR}/${CONFIG_BOOT_SIGNATURE_KEY_FILE}))
set(KEY_FILE ${CONF_DIR}/${CONFIG_BOOT_SIGNATURE_KEY_FILE})
(EXISTS ${CONF_DIR}/${_primary_key_path}))
set(KEY_FILE ${CONF_DIR}/${_primary_key_path})
else()
set(KEY_FILE ${MCUBOOT_DIR}/${CONFIG_BOOT_SIGNATURE_KEY_FILE})
set(KEY_FILE ${MCUBOOT_DIR}/${_primary_key_path})
endif()
message("MCUBoot bootloader key file: ${KEY_FILE}")

Expand Down Expand Up @@ -463,6 +475,45 @@ if(NOT CONFIG_BOOT_SIGNATURE_USING_KMU AND NOT CONFIG_BOOT_SIGNATURE_KEY_FILE ST
DEPENDS ${KEY_FILE}
)
zephyr_library_sources(${GENERATED_PUBKEY})

list(LENGTH _mcuboot_key_files _mcuboot_key_count)
target_compile_definitions(app PRIVATE MCUBOOT_SIGN_KEY_COUNT=${_mcuboot_key_count})

if(_mcuboot_key_count GREATER 1)
# Additional verification keys (index >= 1) from comma-separated list.
# Keys are comma-separated (semicolons do not survive sysbuild).
list(SUBLIST _mcuboot_key_files 1 -1 _extra_keys)
set(_key_index 1)
foreach(_key_path IN LISTS _extra_keys)
string(CONFIGURE "${_key_path}" _key_path)
if(IS_ABSOLUTE ${_key_path})
set(_resolved_key_path ${_key_path})
elseif((DEFINED CONF_DIR) AND (EXISTS ${CONF_DIR}/${_key_path}))
set(_resolved_key_path ${CONF_DIR}/${_key_path})
else()
set(_resolved_key_path ${MCUBOOT_DIR}/${_key_path})
endif()
set(_generated_pubkey ${ZEPHYR_BINARY_DIR}/autogen-pubkey-${_key_index}.c)
add_custom_command(
OUTPUT ${_generated_pubkey}
COMMAND ${PYTHON_EXECUTABLE} ${MCUBOOT_DIR}/scripts/imgtool.py
keyinfo --key ${_resolved_key_path} --require public
COMMAND
${PYTHON_EXECUTABLE}
${MCUBOOT_DIR}/scripts/imgtool.py
getpub
-k
${_resolved_key_path}
--name-suffix _${_key_index}
> ${_generated_pubkey}
DEPENDS ${_resolved_key_path}
)
zephyr_library_sources(${_generated_pubkey})
math(EXPR _key_index "${_key_index} + 1")
endforeach()
endif()
else()
target_compile_definitions(app PRIVATE MCUBOOT_SIGN_KEY_COUNT=1)
endif()

if(CONFIG_BOOT_ENCRYPTION_KEY_FILE AND NOT CONFIG_BOOT_ENCRYPTION_KEY_FILE STREQUAL "")
Expand Down
49 changes: 40 additions & 9 deletions boot/zephyr/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -503,21 +503,52 @@ config NCS_BOOT_SIGNATURE_USING_ITS
if !BOOT_SIGNATURE_USING_KMU && !NCS_BOOT_SIGNATURE_USING_ITS

config BOOT_SIGNATURE_KEY_FILE
string "PEM key file"
string "PEM key file (or comma-separated list on nrf91)"
depends on !BOOT_SIGNATURE_TYPE_NONE
default "root-ec-p256.pem" if BOOT_SIGNATURE_TYPE_ECDSA_P256
default "root-ed25519.pem" if BOOT_SIGNATURE_TYPE_ED25519
default "root-rsa-3072.pem" if BOOT_SIGNATURE_TYPE_RSA && BOOT_SIGNATURE_TYPE_RSA_LEN=3072
default "root-rsa-2048.pem" if BOOT_SIGNATURE_TYPE_RSA && BOOT_SIGNATURE_TYPE_RSA_LEN=2048
default ""
help
You can use either absolute or relative path.
In case relative path is used, the build system assumes that it starts
from the directory where the MCUBoot KConfig configuration file is
located. If the key file is not there, the build system uses relative
path that starts from the MCUBoot repository root directory.
The key file will be parsed by imgtool's getpub command and a .c source
with the public key information will be written in a format expected by
MCUboot.
Path to a signing/verification key PEM, or a comma-separated
list of PEMs (e.g. "prod_pub.pem,dev_pub.pem") to embed multiple
verification keys in the bootloader. Only the public-key bytes are
ever embedded in the bootloader image regardless of which form is
passed in. Comma-separated lists are only supported on nrf91 series
(requires BOOT_SIGNATURE_KEY_FILE_MULTI).

The first entry may be either a keypair PEM or a public-only PEM.
A keypair is required only if the same file is also used with
`imgtool sign`; a public-only PEM is sufficient (and preferred)
when image signing is performed elsewhere with the private half
held under separate custody. When a private key is used, only
the public half is embedded in the bootloader.

Subsequent entries (positions past the first) must be public-only
PEMs; the build will fail at CMake time otherwise. This guards the
intended workflow: a development bootloader that accepts both
production-signed images (verified against the prod public key,
whose private half stays under release-team custody) and
development-signed images (verified against the dev public key).
All entries must use the same BOOT_SIGNATURE_TYPE. Multi-key mode
is mutually exclusive with BOOT_HW_KEY, BOOT_SIGNATURE_USING_KMU,
NCS_BOOT_SIGNATURE_USING_ITS, and BOOT_BYPASS_KEY_MATCH.

Each entry can be an absolute or relative path. Relative paths are
resolved first against the directory of the Kconfig config file that
references the key (as found by scanning CONF_FILE), then against the
MCUboot repository root. Each file is parsed by imgtool's getpub
command and a .c source with the public key information is written
in a format expected by MCUboot.

config BOOT_SIGNATURE_KEY_FILE_MULTI
bool
default y
depends on SOC_SERIES_NRF91
help
Enable comma-separated multi-key support in BOOT_SIGNATURE_KEY_FILE.
Restricted to nrf91 series; cannot be enabled for other SoC families.

endif

Expand Down
47 changes: 30 additions & 17 deletions boot/zephyr/keys.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,40 @@
* provides via the compiler command line).
*/
#include <mcuboot_config/mcuboot_config.h>
#include <zephyr/sys/util.h>

#if !defined(MCUBOOT_HW_KEY)
#if defined(MCUBOOT_SIGN_RSA) || defined(MCUBOOT_SIGN_EC256) || defined(MCUBOOT_SIGN_ED25519)
#define HAVE_KEYS

#ifndef MCUBOOT_SIGN_KEY_COUNT
#error "MCUBOOT_SIGN_KEY_COUNT must be defined by the build system"
#endif

#define _BOOT_KEY_CAT(a, b) a##b
#define BOOT_KEY_CAT(a, b) _BOOT_KEY_CAT(a, b)

#if defined(MCUBOOT_SIGN_RSA)
extern const unsigned char rsa_pub_key[];
extern unsigned int rsa_pub_key_len;
# define BOOT_KEY_PRIMARY rsa_pub_key
#elif defined(MCUBOOT_SIGN_EC256)
extern const unsigned char ecdsa_pub_key[];
extern unsigned int ecdsa_pub_key_len;
# define BOOT_KEY_PRIMARY ecdsa_pub_key
#elif defined(MCUBOOT_SIGN_ED25519)
extern const unsigned char ed25519_pub_key[];
extern unsigned int ed25519_pub_key_len;
# define BOOT_KEY_PRIMARY ed25519_pub_key
#endif

#define BOOT_KEY_NAME(N) BOOT_KEY_CAT(BOOT_KEY_PRIMARY, BOOT_KEY_CAT(_, N))

#define BOOT_KEY_DECL_AT(i, _) \
extern const unsigned char BOOT_KEY_NAME(UTIL_INC(i))[]; \
extern unsigned int BOOT_KEY_CAT(BOOT_KEY_NAME(UTIL_INC(i)), _len);

#define BOOT_KEY_ENTRY_AT(i, _) \
{ .key = BOOT_KEY_NAME(UTIL_INC(i)), \
.len = &BOOT_KEY_CAT(BOOT_KEY_NAME(UTIL_INC(i)), _len) },

extern const unsigned char BOOT_KEY_PRIMARY[];
extern unsigned int BOOT_KEY_CAT(BOOT_KEY_PRIMARY, _len);
LISTIFY(UTIL_DEC(MCUBOOT_SIGN_KEY_COUNT), BOOT_KEY_DECL_AT, ())
#endif

/*
Expand All @@ -51,19 +71,12 @@ extern unsigned int ed25519_pub_key_len;
#if defined(HAVE_KEYS)
const struct bootutil_key bootutil_keys[] = {
{
#if defined(MCUBOOT_SIGN_RSA)
.key = rsa_pub_key,
.len = &rsa_pub_key_len,
#elif defined(MCUBOOT_SIGN_EC256)
.key = ecdsa_pub_key,
.len = &ecdsa_pub_key_len,
#elif defined(MCUBOOT_SIGN_ED25519)
.key = ed25519_pub_key,
.len = &ed25519_pub_key_len,
#endif
.key = BOOT_KEY_PRIMARY,
.len = &BOOT_KEY_CAT(BOOT_KEY_PRIMARY, _len),
},
LISTIFY(UTIL_DEC(MCUBOOT_SIGN_KEY_COUNT), BOOT_KEY_ENTRY_AT, ())
};
const int bootutil_key_cnt = 1;
const int bootutil_key_cnt = sizeof(bootutil_keys) / sizeof(bootutil_keys[0]);
#endif /* HAVE_KEYS */
#else
unsigned int pub_key_len;
Expand Down
24 changes: 24 additions & 0 deletions docs/imgtool.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ output it as a C data structure. You can replace or insert this code
into the key file. However, when the `MCUBOOT_HW_KEY` config option is
enabled, this last step is unnecessary and can be skipped.

When embedding more than one signing-verification key in the same image
(for example, a Zephyr build with a multi-key
`CONFIG_BOOT_SIGNATURE_KEY_FILE` list), pass `--name-suffix` to
distinguish the emitted symbol names:

./scripts/imgtool.py getpub -k dev-key.pem --name-suffix _2

emits `<shortname>_pub_key_2[]` and `<shortname>_pub_key_2_len` (the
same suffix is applied by `getpubhash` for the lang-c encoding). The
option is accepted only for the `lang-c` / `lang-rust` encodings; using
it with `--encoding pem` or `--encoding raw` is rejected.

## [Inspecting key kind](#inspecting-key-kind)

For build-system use, `imgtool keyinfo` reports whether a PEM contains
private material (`private`) or only public material (`public`):

./scripts/imgtool.py keyinfo -k some-key.pem

Pair with `--require private` or `--require public` to exit non-zero
when the kind does not match. The Zephyr port uses this to enforce that
every verification-only key passed via `CONFIG_BOOT_SIGNATURE_KEY_FILE`
past the first entry is a public-only PEM.

## [Signing images](#signing-images)

Image signing takes an image in binary or Intel Hex format intended for the
Expand Down
48 changes: 46 additions & 2 deletions docs/readme-zephyr.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,52 @@ the public key in a format usable by the C compiler.
The generated public key is saved in `build/zephyr/autogen-pubkey.h`, which is included
by the `boot/zephyr/keys.c`.

Currently, the Zephyr RTOS port limits its support to one keypair at the time,
although MCUboot's key management infrastructure supports multiple keypairs.
``CONFIG_BOOT_SIGNATURE_KEY_FILE`` accepts either a keypair PEM or a
public-key-only PEM: only the public key is consumed during the build.
This enables production flows in which the signing private key is held
by a release team and only an exported public key is provided to
bootloader builders. Signing images (`imgtool sign`) requires the
private key.

The Zephyr port supports embedding multiple verification keys in the
bootloader. `CONFIG_BOOT_SIGNATURE_KEY_FILE` accepts a single PEM path or
a comma-separated list, e.g.
`"prod_pubkey.pem,dev_pubkey.pem"` or
`"\${CMAKE_CURRENT_LIST_DIR}/prod_pubkey.pem,\${CMAKE_CURRENT_LIST_DIR}/dev_pubkey.pem"`.
Only the public-key bytes are ever embedded in the bootloader image,
regardless of which form is passed in. All entries must use the same
`BOOT_SIGNATURE_TYPE`, and multi-key mode is mutually exclusive with
`BOOT_HW_KEY`, `BOOT_SIGNATURE_USING_KMU`, `NCS_BOOT_SIGNATURE_USING_ITS`,
and `BOOT_BYPASS_KEY_MATCH`.
The first entry **may** be a keypair PEM or a public-only PEM. A
keypair is required only if the same file is also used with
`imgtool sign`; otherwise a public-only PEM is sufficient — and
preferred, since private material embedded in the bootloader image is
recoverable from flash. Subsequent entries (positions past the first)
**must** be public-only PEMs; the build is rejected at CMake time
otherwise (via `imgtool keyinfo --require public`).

### Custody model

The feature is for separating signing custody from verification custody.

| Key | Custody | Distribution | Blast radius if lost |
| ------------------------- | ------------------------------------------ | ----------------------------------------------------------------- | --------------------------------------------------------------------- |
| Production private | HSM, signing ceremony, release team only | Never leaves the HSM | Catastrophic: attacker can sign images that boot on the prod fleet |
| Production public | N/A (public material) | Embedded in dev bootloaders so dev units can verify prod images | None: public by design |
| Development private | Loosely held by engineers | On dev workstations / dev signing infra | Bounded: only authorizes firmware on non-deployed dev hardware |

Production bootloaders should embed only the production public key, so
that production units boot only production-signed images. Development
bootloaders embed both the production public key and the development
public key, so a dev unit can boot a production-signed image (verified
against the prod public key) without re-signing, while still allowing
engineers to flash development-signed images.

The bootloader's existing key-matching logic (`bootutil_find_key()`)
hashes the image's KEYHASH TLV against every embedded key and accepts
the first match. There is no per-key behaviour: multi-key mode is purely
about accepting more than one valid signer.

Once MCUboot is built, this new keypair file (`mykey.pem` in this
example) can be used to sign images.
Expand Down
12 changes: 12 additions & 0 deletions docs/signed_images.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ be useful when you want to prevent production units from booting
development images, but want development units to be able to boot
both production images and development images.

On Zephyr, `CONFIG_BOOT_SIGNATURE_KEY_FILE` accepts a comma-separated
list of PEMs. Only the public-key bytes are embedded regardless of which
form is passed in. The first entry may be a keypair PEM (needed only if
the same file is also fed to `imgtool sign`) or a public-only PEM
(preferred when signing happens elsewhere); subsequent entries must be
public-only. The intended use is to separate signing custody (a
production private key, held only by a release team) from verification
custody (the production public key, embedded in development bootloaders
so dev units can boot prod-signed images). See
[readme-zephyr.md](readme-zephyr.md) for the threat-model table and a
worked example.

For an alternative solution when the public key(s) doesn't need to be
included in the bootloader, see the [design](design.md) document.

Expand Down
3 changes: 3 additions & 0 deletions root-ed25519-2-pub.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAw7zfMHZOH120DYkuDQ6rBJwwBk55qO2293kuRpom2nc=
-----END PUBLIC KEY-----
18 changes: 9 additions & 9 deletions scripts/imgtool/keys/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,28 @@ def _emit_raw(self, encoded_bytes, file):
# raw binary data, can be for example io.BytesIO
file.write(encoded_bytes)

def emit_c_public(self, file=sys.stdout):
def emit_c_public(self, file=sys.stdout, name_suffix: str = ""):
self._emit(
header=f"const unsigned char {self.shortname()}_pub_key[] = {{"
header=f"const unsigned char {self.shortname()}_pub_key{name_suffix}[] = {{"
,
trailer="};",
encoded_bytes=self.get_public_bytes(),
indent=" ",
len_format=f"const unsigned int {self.shortname()}_pub_key_len = {{}};"
len_format=f"const unsigned int {self.shortname()}_pub_key{name_suffix}_len = {{}};"
,
file=file)

def emit_c_public_hash(self, file=sys.stdout):
def emit_c_public_hash(self, file=sys.stdout, name_suffix: str = ""):
digest = Hash(SHA256())
digest.update(self.get_public_bytes())
self._emit(
header=f"const unsigned char {self.shortname()}_pub_key_hash[] = {{"
header=f"const unsigned char {self.shortname()}_pub_key_hash{name_suffix}[] = {{"
,
trailer="};",
encoded_bytes=digest.finalize(),
indent=" ",
len_format=f"const unsigned int {self.shortname()}_pub_key_hash_len = {{}};"
,
len_format=("const unsigned int "
f"{self.shortname()}_pub_key_hash{name_suffix}_len = {{}};"),
file=file)

def emit_raw_public(self, file=sys.stdout):
Expand All @@ -91,9 +91,9 @@ def emit_raw_public_hash(self, file=sys.stdout):
digest.update(self.get_public_bytes())
self._emit_raw(digest.finalize(), file=file)

def emit_rust_public(self, file=sys.stdout):
def emit_rust_public(self, file=sys.stdout, name_suffix: str = ""):
self._emit(
header=f"static {self.shortname().upper()}_PUB_KEY: &[u8] = &["
header=f"static {self.shortname().upper()}_PUB_KEY{name_suffix.upper()}: &[u8] = &["
,
trailer="];",
encoded_bytes=self.get_public_bytes(),
Expand Down
Loading
Loading