Embedded

Building a Secure Bootloader with Chain-of-Trust on STM32

Amine El Omari

Every embedded device that goes into the field needs a mechanism to update its firmware. But if anyone can flash anything onto your MCU, you don't have a product — you have a liability. This post walks through how I designed and implemented a secure bootloader on STM32 that establishes a full chain-of-trust from power-on to application execution.

The Problem

I was building a dual-MCU system for vehicle telemetry: an STM32 handling real-time CAN/OBD-II acquisition and a Pico2W managing cloud connectivity via MQTT. Both needed OTA update capability, but the attack surface was significant — the device would be physically accessible, connected to a vehicle CAN bus, and exposing an MQTT endpoint to the internet.

The core requirements were:

Architecture Overview

I split the STM32 flash into three regions:

  1. Bootloader (0x08000000) — Read-protected, never updated OTA. Contains verification logic and the public key.
  2. Slot A (0x08008000) — Primary application slot.
  3. Slot B (0x08040000) — Secondary slot for OTA staging. The bootloader swaps or selects between slots.

Chain-of-Trust Implementation

The chain starts with the hardware. STM32's Option Bytes let you configure read-out protection (RDP Level 1) and write-protection on the bootloader sector. This means even if someone connects a debug probe, they can't read the bootloader binary or overwrite it.

On top of that, each firmware image is signed using ECDSA (P-256) with a private key that never leaves the build machine. The corresponding public key is embedded in the bootloader. The image format is straightforward:

| Field            | Size     | Description                          |
|------------------|----------|--------------------------------------|
| Magic            | 4 bytes  | 0xEMU1 (validation marker)           |
| Version          | 4 bytes  | Semantic version (major.minor.patch) |
| Length           | 4 bytes  | Firmware binary size                 |
| Firmware Binary  | N bytes  | The actual application               |
| Signature        | 64 bytes | ECDSA signature over all above fields|
        

On boot, the bootloader:

  1. Checks the magic bytes in both slots
  2. Verifies the ECDSA signature of the selected slot
  3. Compares version numbers — picks the newest valid image
  4. Jumps to the application's reset handler

OTA Update Flow

When the Pico2W receives a new firmware image over MQTT, it:

Lessons Learned

The biggest challenge wasn't the cryptography — it was flash management. STM32 flash sectors aren't all the same size, and the F4 series has large 128KB sectors that make it expensive to erase. I had to be very careful about which sectors held the bootloader metadata vs. the application slots.

If you're designing a secure bootloader, start by mapping your flash sectors on paper before writing a single line of code. The memory layout determines everything else.

Another gotcha: the jump from bootloader to application requires the interrupt vector table to be relocated. Forgetting SCB->VTOR = APP_ADDRESS; is the kind of bug that costs you a full afternoon.

What's Next

The current implementation uses a single signing key. The next iteration will support key rotation — embedding multiple public keys and implementing a key revocation mechanism using a monotonic counter in the STM32's backup registers. This way, if a key is ever compromised, we can push an update that blacklists it without replacing hardware.

I'm also looking at adding hardware-level tamper detection using the STM32's tamper detection peripheral, which can erase sensitive data if the device is opened or the supply voltage is manipulated.