Building CRA-ready STM32H5 controllers¶
Secure boot and signed firmware updates, end to end, on an STM32H5. OEMiRoT, ST's immutable first stage built from MCUboot, verifies the secure and the non-secure application image, and every signing key stays inside the HSM.
The Cyber Resilience Act's Annex I requires that a product protects the integrity of its software and that security updates can be installed securely. On an STM32H5 both come down to the same discipline: each application image is signed, the bootloader that verifies it cannot be changed, and the keys it verifies against are provisioned once into the chip.
How to read this
This is an example configuration, not a design guide. It shows which platform operations sign the images of ST's own reference setup (STM32CubeH5, the OEMiRoT projects for the STM32H573I-DK) and what the device verifies, so that a manufacturer with a design of its own can see which calls fulfil which part.
Secure boot across the STM32 range, and why the H5¶
ST's families differ in what they offer as a first stage, and that decides what gets signed and how. The older families have software-only bootloaders; the current ones carry a root of trust in silicon or in locked flash. The same two operations serve the other families where the image is an MCUboot image.
| Families | First stage | Images signed with |
|---|---|---|
| F0 to F7, G0/G4, L0 to L4, WB | None in silicon. A software bootloader in flash (ST's legacy SBSFU, or MCUboot), protected only by readout protection | SignMcuBoot for MCUboot |
| L5, U5, WBA | MCUboot (TF-M's BL2), locked by a boot lock | SignMcuBoot |
| H5 (OEMiRoT also on H7RS, U0, U3, C5) | OEMiRoT: MCUboot in user flash, made immutable by the option bytes, keys in the chip. On the parts with the cryptographic accelerator, such as the H573, also STiRoT, ST's own immutable code | SignMcuBoot (OEMiRoT); DigestSigning (STiRoT) |
| N6 | Boot ROM with its own first-stage format, MCUboot above it | DigestSigning, then SignMcuBoot |
| MP1, MP2 (Linux) | Boot ROM with a key hash in OTP, then TF-A, OP-TEE, U-Boot | Not covered by this guide |
This guide is the highlighted row. The H5 is its example because it is where ST's current designs go: every OEM-owned stack ST ships for it is MCUboot-based with ECDSA P-256, the keys live in the chip rather than in flash, and the image format is one the platform already signs for other vendors.
Secure boot on the STM32H5: ST's two first stages¶
The STM32H5 has no public-key secure boot in its ROM. ST ships two ways to anchor the chain instead, and a product picks one of them at design time. They differ in who owns the first stage and in which image format the first stage verifies; that decides which LAAVAT operation signs.
| Path | What boots what | When a product picks it | LAAVAT operation |
|---|---|---|---|
| OEMiRoT alone | OEMiRoT (MCUboot, OEM keys, made immutable by the option bytes) → secure application → non-secure application | The OEM owns the bootloader and the keys; exists on every H5 part; images are plain MCUboot. The bootloader itself is not updatable in the field | SignMcuBoot |
| STiRoT | STiRoT (ST's own immutable code in system flash, on the parts with the cryptographic accelerator) → up to two application images in STiRoT's format, signed and encrypted. ST also allows an updatable OEM bootloader in between (OEMuRoT, which is the OEMiRoT build in a second configuration), for OEMs that need their own boot logic and must be able to update it | No OEM boot code, certified by ST; the path behind Secure Manager on the H573. Equally normal; the OEM owns only the keys | DigestSigning for the STiRoT-format image; SignMcuBoot for anything an OEMuRoT verifies |
In both, exactly one MCUboot is involved: OEMiRoT, or its OEMuRoT configuration under STiRoT.
This guide shows OEMiRoT, for three reasons: it exists on every H5 part, its
images are in MCUboot's plain image format, which SignMcuBoot signs directly,
and the OEM owns the bootloader as well as the keys. The STiRoT path is just as
normal on the H573; its images need the two-step with DigestSigning — see
digest signing. On the older STM32
families without TrustZone (F4, L4 and similar) the application image is the
same and is signed with the same operation, but the verifying key sits in flash
behind readout protection, which is not a root of trust.
The chain¶
One bootloader and two application images. Trust starts in OEMiRoT, which the option bytes make immutable; it verifies both images, then starts the secure one, which starts the non-secure one. The two images are the two halves of a TrustZone application, not two stages: ST's reference build splits the application into a secure side and a non-secure side, each with its own key. One ECDSA P-256 key per image, both on the platform.
graph TD
O["<b>OEMiRoT</b><br/>stage 1 · locked flash<br/>MCUboot, keys in OBKeys"]
S["<b>Secure application</b><br/>stage 2a<br/>secure world"]
N["<b>Non-secure application</b><br/>stage 2b<br/>non-secure world"]
O -->|"verifies with the S key"| S
O -->|"verifies with the NS key"| N
S -.->|starts| N
OEMiRoT verifies both images before anything runs; the secure side then hands control to the non-secure side. The two public keys go into the chip as OBKeys at the factory; the debug-authentication secret stays with the OEM — see What goes on the device. The images in this example are signed, not encrypted.
The two images can also be one
ST's "full secure" configuration of the same project builds the application as a single image verified with a single key, with or without TrustZone. The product then has one operation instead of two; everything else on this page is unchanged.
LAAVAT's part in each link¶
| Link | The device verifies with | LAAVAT signs with | Where the public half goes |
|---|---|---|---|
| OEMiRoT → secure application | ECDSA P-256 against the secure authentication key in OBKeys; SHA-256 of the image; security counter | SignMcuBoot, secure application operation |
OBKeys, at the factory |
| OEMiRoT → non-secure application | The same, against the non-secure authentication key | SignMcuBoot, non-secure application operation |
OBKeys, at the factory |
| Field update | The new image in the download slot, verified the same way before it replaces the old one; a lower security counter is refused | The same two operations | Already on the device |
Core property — HSM-resident keys
The two ECDSA P-256 private keys that anchor this chain live in the HSM and never leave it. What the device holds is the matching pair of public keys, written into the OBKeys once, at the factory. OEMiRoT compares the hash of the key named in each image with what it holds, then verifies the signature. LAAVAT is not in the loop at boot or at update time. See key custody.
The LAAVAT product¶
One product, two SignMcuBoot operations, one ECDSA P-256 key each. The
platform runs imgtool itself with the HSM-held key; the build host sends the
unsigned image and the parameters.
Example product definition¶
{
"name": "STM32H573 controller",
"description": "STM32H573 controller: OEMiRoT verifies the secure and non-secure application images",
"productType": "Production",
"enabled": true,
"caInfo": [], "rndKeys": [],
"productOperations": [
{ "name": "STM32H5 secure application signing",
"description": "Sign the OEMiRoT secure application image",
"operationType": "SignMcuBoot",
"token": { "name": "STM32H5 S app signing key", "description": "ECDSA P-256, secure image",
"keyType": "ECDSAP256" },
"approvalRule": { "name": "Release", "description": "Unattended release signing",
"allowedGroups": ["<release-group-uuid>"],
"approvalGroups": [], "blanketGroups": ["<release-group-uuid>"] } },
{ "name": "STM32H5 non-secure application signing",
"description": "Sign the OEMiRoT non-secure application image",
"operationType": "SignMcuBoot",
"token": { "name": "STM32H5 NS app signing key", "description": "ECDSA P-256, non-secure image",
"keyType": "ECDSAP256" },
"approvalRule": { "name": "Release", "description": "Unattended release signing",
"allowedGroups": ["<release-group-uuid>"],
"approvalGroups": [], "blanketGroups": ["<release-group-uuid>"] } }
]
}
signing-tool -n test.ini product add -T stm32h573-controller.json
# approve it in the console, then fetch each operation's public key (one call per operation); the factory provisions them
signing-tool -n test.ini product getpubkey -P <product-uuid> --operid <s-app-operation-uuid> -O OEMiRoT_Authentication_S.pem
signing-tool -n test.ini product getpubkey -P <product-uuid> --operid <ns-app-operation-uuid> -O OEMiRoT_Authentication_NS.pem
Both operations are blanket-approved for the release group in this example, so
the release pipeline runs unattended; an approval group on one or both is a
one-line change in the template. The config file test.ini is set up once, as
described in providing the token.
Signing the release artifacts¶
Secure and non-secure application images — SignMcuBoot¶
ST's project builds two binaries: rot_tz_s_app.bin (the secure side) and
rot_tz_ns_app.bin (the non-secure side). Each is signed with its own
operation, so a release is two signing requests. The steps below show the
secure image; the non-secure image goes through the same three steps with
slot_size 0xA0000 and the non-secure operation.
The imgtool parameters in the request are ST's own: they are the values in
ROT_Provisioning/OEMiROT/Images/OEMiROT_S_Code_Image.xml, the file ST's
signing tool (Trusted Package Creator, part of STM32CubeProgrammer) reads when
it signs the same image with a local key. Every field is described in
MCUboot signing.
1. Package what imgtool needs. A .tgz with the unsigned image and a
request.json carrying the imgtool parameters:
{ "images": [ {
"filename": "rot_tz_s_app.bin", "output": "rot_tz_s_app_sign.bin",
"align": "16", "version": "1.0.0", "header_size": "0x400", "slot_size": "0x6000",
"pad_header": true, "pad": true, "confirm": false, "overwrite_only": true,
"max_sectors": "0", "max_align": "8",
"security_counter": "auto", "public_key_format": "hash" } ] }
tar -czf s-app.tar.gz request.json rot_tz_s_app.bin
2. Submit to the MCUboot operation. The platform signs asynchronously: the request is created and becomes Ready a few seconds later with blanket approval.
signing-tool -n test.ini imagesigning add MCUBoot -P <product-uuid> --operid <s-app-operation-uuid> \
-N "controller 1.0.0 secure app" -D "OEMiRoT secure image" -F s-app.tar.gz
Request sent. Request ID: <request-uuid>, state: Created
3. Download and extract the signed image. --wait waits until the request
is Ready, then downloads it.
signing-tool -n test.ini imagesigning get -I <request-uuid> --wait --skipBase64 -O s-app-signed.tar.gz
tar -xzf s-app-signed.tar.gz # rot_tz_s_app_sign.bin
Then the same for rot_tz_ns_app.bin with the non-secure operation. The two
signed images are what the factory flashes first and what every update ships.
Delivering an update to the device and writing it into the download slot is
the manufacturer's update mechanism and is not covered here.
Before shipping, check the result as described under Verifying before shipping; the byte comparison against ST's own tool is the check that matters.
Two values to get right¶
| Field | Value | Reason |
|---|---|---|
public_key_format |
"hash" |
OEMiRoT compares the hash of the key in the image (KEYHASH) with the OBKeys. The platform's default is full, which embeds the whole public key, so the field has to be set |
max_align |
"8" with align "16" |
ST's MCUboot fork compares the image trailer against the classic MCUboot magic, although the H5 flash writes in 16-byte words. Upstream imgtool, which the platform runs, switches to a newer trailer magic whenever align is 16 unless max_align is 8. The image verifies with imgtool either way; OEMiRoT only accepts the classic magic. ST's own tool does not need this field |
Every other value is ST's. confirm: false is in the request because ST does
not confirm images.
What goes on the device¶
At the factory, the two public keys are written into the chip's option-byte keys (OBKeys). OEMiRoT reads them from there at every boot; nothing else on the device holds key material for the signatures. The private keys stay in the HSM.
The OBKeys are written as a file that ST's Trusted Package Creator builds from
OEMiRoT_Config.xml. The XML names the key files; for the two signing keys
those are the public keys fetched with product getpubkey above. They reach
the factory as PEM files; they are public, so what matters is only that the
factory has the right ones, which the key hashes printed by product getpubkey
let it confirm.
STM32TrustedPackageCreator_CLI -obk ROT_Provisioning/OEMiROT/Config/OEMiRoT_Config.xml
# -> Binary/OEMiRoT_Config.obk, written into the chip with STM32CubeProgrammer during provisioning
| In the OBKeys | Comes from | Who keeps the secret |
|---|---|---|
| Secure and non-secure signing public keys | The platform (product getpubkey) |
The HSM holds the private keys |
| OEM encryption key | The OEM; ST's provisioning file always has this slot. Unused in this example, which does not encrypt images | The OEM |
| Debug-authentication password or key | The OEM, through Trusted Package Creator | The OEM; without it, a Closed chip cannot be reopened for debugging |
Only the first row comes from the platform.
After provisioning, the product state is set to Closed, which locks the option bytes and the OBKeys. Closed can be reverted only through debug authentication with the OEM's secret, and the reversal erases the chip; Locked, the final state, is permanent.
Verifying before shipping¶
With ST's imgtool and the upstream imgtool¶
Two tools, the same check. imgtool is the upstream MCUboot tool
(pip install imgtool). stm32-mw-mcuboot is ST's fork of MCUboot, the code
OEMiRoT is built from; it is on GitHub and inside STM32CubeH5 under
Middlewares/Third_Party/mcuboot, and its scripts/imgtool.py is the imgtool
Trusted Package Creator runs. Both verify the signed image against the
operation's public key:
imgtool verify -k OEMiRoT_Authentication_S.pem rot_tz_s_app_sign.bin
python3 stm32-mw-mcuboot/scripts/imgtool.py verify -k OEMiRoT_Authentication_S.pem rot_tz_s_app_sign.bin
python3 stm32-mw-mcuboot/scripts/imgtool.py dumpinfo rot_tz_s_app_sign.bin # header, TLVs, trailer
Against the image ST's own tool produces¶
Sign the same binary once with ST's imgtool and a local test key, using the
parameters from ST's XML, and compare. The two files must be identical except
for the key hash and the signature bytes: same header fields, same payload,
same security counter, same TLV sequence (SHA256, KEYHASH, ECDSASIG),
same digest, same padding and trailer. That comparison is what caught the
max_align difference; a signature check alone would not have.
python3 stm32-mw-mcuboot/scripts/imgtool.py sign -k test_key.pem -e little --pad -S 0x6000 -H 0x400 \
--pad-header --overwrite-only -v 1.0.0 -s auto --align 16 -M 0 rot_tz_s_app.bin reference.bin
cmp -l reference.bin rot_tz_s_app_sign.bin | wc -l # differences only inside KEYHASH and ECDSASIG
Where off-hardware verification stops¶
Worth stating plainly, because a passing test suite is easy to over-read:
| Claim | Off-hardware | What would close the gap |
|---|---|---|
| Both image signatures are cryptographically correct for OEMiRoT | Checked on the build host: imgtool verify (upstream and ST's fork) accepts both images with the operations' public keys |
— |
| The images are in the format OEMiRoT expects | Checked on the build host: byte-identical to ST's own tool output except key hash and signature | — |
| A tampered image or a wrong key is rejected | Checked on the build host: both imgtools reject a flipped payload byte, a flipped TLV byte and the other operation's key | — |
| The OBKeys file carries the platform's public keys | Checked on the build host: the file Trusted Package Creator produces contains both public keys | — |
| The provisioning flow puts the keys in the chip and closes it | Not tested | Provisioning on an STM32H573I-DK |
| OEMiRoT boots the images, installs an update, refuses a rollback | Not tested | Boot on an STM32H573I-DK |
Device-side requirements: STM32CubeH5 with its OEMiRoT projects, ST's MCUboot
fork (stm32-mw-mcuboot) for its imgtool, and STM32CubeProgrammer, which
contains Trusted Package Creator. The two application binaries used here were
built from ST's STM32CubeIDE project on the command line with the GNU Arm
toolchain; the IDE itself is not needed for signing.
Mapping to the CRA¶
| Annex I requirement | Covered by |
|---|---|
| Integrity of the software and its configuration is protected; unauthorised modification is detected | OEMiRoT verifies both images with ECDSA P-256 against keys in OBKeys; the product state locks the option bytes |
| Security updates: the product can be updated | MCUboot download slots with a security counter against rollback; each release signed with a recorded, group-restricted operation |
| Confidentiality of data and code where the risk assessment requires it | Out of scope: this example signs plain images. ST's optional image encryption is not produced by this flow |
| Access control for privileged functions | Debug authentication; signing restricted to groups; every signature on record |
Building a device this way does not by itself make a product CRA compliant. The Annex I mapping sets out which requirements the platform supports and which remain yours.
Related¶
- Building CRA-ready nRF devices — the same
SignMcuBootflow on a Nordic MCUboot chain - MCUboot signing — every
request.jsonfield - MCUboot example flow — a single-image walkthrough, from product creation to verification
- Digest signing with detached signature — signs a digest computed elsewhere, as the STiRoT path needs; not used in this example
- Trusted device identities — if the controller also needs a device certificate
- Security and architecture — where the keys live and who can reach them
References¶
- ST wiki: Secure Boot for STM32H5 — the first stages and the option bytes that lock them
- ST wiki: OEMiRoT and OEMuRoT for STM32H5
- STM32CubeH5 — the
OEMiRoT projects and
ROT_Provisioning; and stm32-mw-mcuboot — ST's MCUboot fork and its imgtool - MCUboot design — image format, TLVs, trailer and security counter
- UM2238: STM32 Trusted Package Creator — OBKeys and debug-authentication file generation