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:
- Firmware images must be cryptographically verified before execution
- OTA updates must be atomic — either the whole update lands or the previous version runs
- The bootloader itself must be immutable and protected from corruption
- Rollback must be possible if an update fails validation
Architecture Overview
I split the STM32 flash into three regions:
- Bootloader (0x08000000) — Read-protected, never updated OTA. Contains verification logic and the public key.
- Slot A (0x08008000) — Primary application slot.
- 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:
- Checks the magic bytes in both slots
- Verifies the ECDSA signature of the selected slot
- Compares version numbers — picks the newest valid image
- Jumps to the application's reset handler
OTA Update Flow
When the Pico2W receives a new firmware image over MQTT, it:
- Writes the image to Slot B via UART to the STM32
- The STM32 bootloader verifies the signature before committing
- If valid, sets a flag indicating a new image is available in Slot B
- On next reset, the bootloader swaps active slots
- If Slot B fails verification, the device continues running Slot A — no bricking
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.