
Security in embedded firmware is no longer optional. As devices connect to networks, handle sensitive data, and control physical systems, they become targets. The constraints of embedded systems — limited compute, no secure OS, long lifecycles — make traditional security approaches difficult. This article covers practical hardening techniques that work within those constraints.
Before writing code, define what you’re protecting against. A medical device faces different threats than a smart light bulb. Common embedded threat vectors:
Document your threat model. It drives every architectural decision.
Secure boot establishes a chain of trust from power-on reset to application code. Each stage verifies the next before execution.
+--------------------------+| ROM Bootloader (Root) | <-- Immutable, in silicon| Verifies: Stage 1 sig |+------------+-------------+|v+--------------------------+| Stage 1 Bootloader | <-- Signed by OEM key| Verifies: Stage 2 sig || Initializes: MPU, clock |+------------+-------------+|v+--------------------------+| Stage 2 / Application | <-- Signed by OEM key| Verifies: Config, data || Enforces: MPU regions |+--------------------------+
Implementation requirements:
Common pitfalls:
Encryption protects IP and prevents firmware analysis. Two approaches:
The MCU decrypts firmware on-the-fly during execution (e.g., STM32 OTFDEC, ESP32 flash encryption). Keys never leave the chip.
// STM32 OTFDEC example: configure external flash encrypted region// Note: OTFDEC inherently uses AES-128 CTR modeOTFDEC_RegionConfigTypeDef config = {.StartAddress = 0x90000000, // OCTOSPI external flash start.EndAddress = 0x9007FFFF, // OCTOSPI external flash end.Nonce = {0x11223344, 0x55667788}, // 64-bit nonce for AES-CTR.Version = 0x1234 // 16-bit firmware version};// Configure region 1 without locking configuration// Decryption keys are loaded separately (e.g., via HAL_OTFDEC_RegionSetKey)HAL_OTFDEC_RegionConfig(&hotfdec, OTFDEC_REGION_1, &config, OTFDEC_REG_CONFIGR_LOCK_DISABLE);HAL_OTFDEC_RegionEnable(&hotfdec, OTFDEC_REGION_1);
OTA payloads encrypted with AES-GCM or ChaCha20-Poly1305. The device decrypts only after signature verification.
+------------------+ +------------------+| OTA Server | | Device || | | || Firmware v1.2 | | 1. Verify sig || AES-GCM encrypt | | 2. Decrypt || Sign (ECDSA) |---->| 3. Write flash || Package: | | 4. Update ctr || {ct, tag, nonce,| | 5. Reboot || sig, version} | | |+------------------+ +------------------+
Memory Protection Units (MPU) on Cortex-M or MMUs on Cortex-A enforce isolation. Define regions early — before untrusted code runs.
+--------------------------+ 0x00000000| Bootloader (RO, XN=0) | Execute only, no write| Size: 64 KB |+--------------------------+| Secure Config (RO, XN=1)| Keys, certs, no execute| Size: 16 KB |+--------------------------+| Application Code (RX) | Execute + read| Size: Variable |+--------------------------+| App Data (RW, XN=1) | Read/write, no execute| Stack (RW, XN=1) |+--------------------------+| Peripherals (RW, XN=1) | Device memory attributes| Size: 512 MB |+--------------------------+ 0xFFFFFFFF
// Example: MPU region for secure key storage (no execute, privileged read-only)MPU_Region_InitTypeDef region = {.Enable = MPU_REGION_ENABLE,.Number = MPU_REGION_NUMBER_1,.BaseAddress = SECURE_KEY_BASE,.Size = MPU_REGION_SIZE_16KB,.SubRegionDisable = 0,.TypeExtField = MPU_TEX_LEVEL_0,.AccessPermission = MPU_REGION_PRIV_RO, // Privileged read-only, user no-access.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE,.IsShareable = MPU_ACCESS_NOT_SHAREABLE,.IsCacheable = MPU_ACCESS_NOT_CACHEABLE,.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE,};HAL_MPU_ConfigRegion(®ion);
Key principles:
// STM32: Disable SWD/JTAG by reconfiguring pins as analog// Note: This is a software defense. Use RDP Level 2 for hardware protection.GPIO_InitTypeDef GPIO_InitStruct = {0};__HAL_RCC_GPIOA_CLK_ENABLE();GPIO_InitStruct.Pin = GPIO_PIN_13 | GPIO_PIN_14; // SWDIO, SWCLKGPIO_InitStruct.Mode = GPIO_MODE_ANALOG;GPIO_InitStruct.Pull = GPIO_NOPULL;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
For permanent disable, blow OTP fuses (e.g., STM32 RDP Level 2, NXP SJC_DISABLE). Warning: RDP Level 2 is irreversible.
Network-facing parsers are the #1 exploit vector. Use bounded parsers, avoid scanf/strcpy/sprintf.
// Bad: unboundedchar buf[64];strcpy(buf, network_input); // Overflow!// Good: bounded with explicit lengthsize_t len = strnlen(network_input, sizeof(buf) - 1);memcpy(buf, network_input, len);buf[len] = '\0';// Better: use a safe parsing library (e.g., nanopb for Protobuf, tinycbor for CBOR)// Pseudocode — consult your library's API for exact function signaturesif (parse(network_input, input_len, &value) != OK) return ERROR_INVALID_FORMAT;
Enable compiler stack protection:
# GCC/Clang (arm-none-eabi)-fstack-protector-strong
On hosted platforms (Linux/RTOS with glibc), additionally use -D_FORTIFY_SOURCE=2 for hardened libc functions. Note: _FORTIFY_SOURCE requires libc support — bare-metal newlib-nano does not fully implement it. On Cortex-M, combine stack canaries with MPU stack guard regions for hardware-enforced protection.
Never store keys in plain flash. Options in order of preference:
// Example: Using ATECC608A for ECDSA signingATCA_STATUS status = atcab_sign(PRIVATE_KEY_SLOT, digest, signature);if (status != ATCA_SUCCESS) return ERROR_CRYPTO_FAILURE;// Private key never exposed to MCU firmware
A robust OTA system requires:
| Property | Implementation |
|---|---|
| Authenticity | ECDSA signature on full payload |
| Integrity | SHA-256 hash verified before write |
| Confidentiality | AES-GCM encryption of payload |
| Freshness | Monotonic version counter, timestamp |
| Atomicity | A/B partition scheme |
| Rollback protection | Reject versions <= current |
| Recovery | Fallback to known-good partition |
Flash Memory Map:+--------------------------+ 0x08000000| Bootloader (immutable) | 64 KB+--------------------------+| Slot A (active) | 512 KB| Firmware v1.2 |+--------------------------+| Slot B (standby) | 512 KB| Firmware v1.3 (new) |+--------------------------+| Metadata / Config | 32 KB| {active_slot: A, || version_A: 1.2, || version_B: 1.3, || rollback_ctr: 5} |+--------------------------+
Update flow:
pending_slot = Bpending_slot, verifies Slot B, swaps activeUse hardware TRNG (True Random Number Generator), not PRNG. Seed any CSPRNG from TRNG.
// STM32 RNG exampleuint32_t random32;if (HAL_RNG_GenerateRandomNumber(&hrng, &random32) != HAL_OK) {Error_Handler(); // Clock or seed error — do not proceed}// For larger buffers, generate in a loop (32 bits at a time)for (size_t i = 0; i < word_count; i++) {if (HAL_RNG_GenerateRandomNumber(&hrng, &buffer[i]) != HAL_OK) {Error_Handler();}}
| Purpose | Algorithm | Parameters |
|---|---|---|
| Signatures | Ed25519 / ECDSA P-256 | Ed25519: inherently deterministic (RFC 8032); ECDSA: deterministic nonce per RFC 6979 |
| Key Exchange | X25519 / ECDH P-256 | Ephemeral keys |
| Symmetric Encryption | AES-256-GCM / ChaCha20-Poly1305 | 96-bit nonce, never reuse |
| Hashing | SHA-256 / SHA-384 | — |
| KDF | HKDF-SHA256 | Salt from TRNG |
| Password Hashing | Argon2id / PBKDF2 | Argon2id (m=32KB) or PBKDF2-HMAC-SHA256 |
Avoid: RSA < 2048, SHA-1, MD5, ECB mode, static nonces, custom crypto.
On devices handling secrets, use constant-time implementations. Most secure elements handle this internally. For software crypto, use libraries with constant-time guarantees (e.g., mbedTLS, wolfSSL, libsodium).
libfoo==1.2.3, not ^1.2.3)grype, trivy, or syft in CIEven hardened systems can be compromised. Prepare:
Embedded firmware security is a system property, not a checklist item. The most effective defenses layer together:
Start with threat modeling. Implement secure boot first — everything else builds on it. Use hardware security features (MPU, TRNG, secure elements) rather than software-only approximations. Test with fault injection and side-channel analysis if the threat model warrants it.
Quick Links
Legal Stuff




