
Software security controls are only as strong as the hardware layer beneath them. An attacker who can bypass the boot sequence, read flash directly or inject a voltage glitch to skip an authentication check defeats every software protection you built, regardless of how correct that code is. This article covers embedded hardware security from the ground up: locking debug interfaces, building a secure boot chain of trust, choosing and using trusted storage hardware, authenticating devices with certificates and Physical Unclonable Functions (PUFs), and monitoring for hardware-level tampering at runtime. Code examples target STM32 and ESP32, the most widely deployed Cortex-M and Xtensa platforms, but the principles apply to any embedded target.
Every software security control ultimately executes on hardware. The processor that verifies a firmware signature, the flash controller that enforces read protection, the memory that holds cryptographic keys: all of these are physical objects that an attacker with physical access can interact with directly. Software cannot defend against an attacker who desolds the flash chip and reads it on a programmer. Firmware cannot prevent a voltage glitch that causes the processor to skip a single instruction at a precisely chosen moment. A debug port left accessible on the PCB (Printed Circuit Board) renders every authentication layer in the application irrelevant.
Embedded hardware security addresses this by pushing controls down to a level the software cannot reach: immutable ROM bootloaders, OTP (One-Time Programmable) configuration bits, hardware-enforced memory regions, secure elements that never expose key material, and physical detection circuits that respond to tampering autonomously. These controls form the hardware root of trust that everything else depends on. Without them, the security of the entire device rests on the assumption that no one will ever touch it physically, which is not an assumption you can make for any device that ships to the field.
Debug interfaces are the most commonly exploited hardware access path in embedded devices. JTAG (Joint Test Action Group) and SWD (Serial Wire Debug) provide complete control over the processor: read and write any memory address, halt execution, set breakpoints, inspect registers and flash arbitrary firmware. UART consoles provide shell access to Linux-based devices and boot interrupt access on bare-metal targets. All of these are essential during development and catastrophic in production if left enabled.
STM32 microcontrollers implement debug and flash access control through the RDP (Readout Protection) option byte. There are three levels:
| RDP Level | Option Byte Value | Debug Access | Flash Readback | Reversible |
|---|---|---|---|---|
| Level 0 | 0xAA | Full JTAG/SWD | Fully readable | Yes |
| Level 1 | 0xBB | No debug access | Not readable via debug | Yes (erases flash) |
| Level 2 | 0xCC | Permanently disabled | Permanently blocked | No (irreversible) |
Level 1 is the minimum for any production device. It prevents firmware extraction via JTAG but allows the device to be reflashed after erasing all flash contents, which is useful for legitimate repair or refurbishment. Level 2 permanently and irrevocably disables the JTAG/SWD interface and blocks all flash readback. It should be used for high-security applications where the value of the device’s security properties justifies the inability to debug or recover a production unit.
/* Reading and setting STM32 RDP level programmatically from firmware.
Useful for a production provisioning routine that verifies security
configuration is correct before the device leaves the factory.
On STM32F4, the option bytes are at address 0x1FFFC000. */
#include "stm32f4xx_hal.h"
typedef enum {
RDP_LEVEL_0 = 0xAA, /* Open: full debug access */
RDP_LEVEL_1 = 0xBB, /* Protected: no debug, erasable */
RDP_LEVEL_2 = 0xCC /* Locked: permanently no debug */
} RdpLevel;
/* Read the current RDP level from option bytes */
RdpLevel read_rdp_level(void) {
FLASH_OBProgramInitTypeDef ob_init;
HAL_FLASHEx_OBGetConfig(&ob_init);
switch (ob_init.RDPLevel) {
case OB_RDP_LEVEL_0: return RDP_LEVEL_0;
case OB_RDP_LEVEL_1: return RDP_LEVEL_1;
case OB_RDP_LEVEL_2: return RDP_LEVEL_2;
default: return RDP_LEVEL_0;
}
}
/* Set RDP to Level 1. Call this during factory provisioning.
WARNING: Once set to Level 2, this operation is irreversible.
The device will reset after option byte programming to apply the change. */
HAL_StatusTypeDef set_rdp_level_1(void) {
FLASH_OBProgramInitTypeDef ob_init = {0};
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
ob_init.OptionType = OPTIONBYTE_RDP;
ob_init.RDPLevel = OB_RDP_LEVEL_1;
HAL_StatusTypeDef status = HAL_FLASHEx_OBProgram(&ob_init);
if (status == HAL_OK) {
/* Trigger system reset to apply the new option byte value */
HAL_FLASH_OB_Launch();
}
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
return status;
}
ESP32 uses eFuse bits for security configuration. Unlike STM32 option bytes, eFuse bits are individually one-way: once a bit is burned to 1, it cannot be returned to 0. The security-relevant eFuse fields are:
# Reading and burning ESP32 eFuses using the esptool / espefuse utility.
# Run on the host machine connected to the ESP32 over USB/UART.
# WARNING: eFuse burns are permanent and irreversible.
# Step 1: Read current eFuse state (safe, read-only)
python -m espefuse --port /dev/ttyUSB0 summary
# Step 2: Verify JTAG_DISABLE and DOWNLOAD_DIS before burning
# Look for these fields in the summary output:
# JTAG_DISABLE = 0 (0 = enabled, 1 = disabled)
# DOWNLOAD_DIS = 0 (0 = enabled, 1 = disabled)
# Step 3: Enable flash encryption (sets FLASH_CRYPT_CNT bit 0)
# This is done automatically by the ESP-IDF secure boot process,
# but can also be done manually for testing:
python -m espefuse --port /dev/ttyUSB0 burn_efuse FLASH_CRYPT_CNT 1
# Step 4: Disable JTAG permanently (production devices only)
python -m espefuse --port /dev/ttyUSB0 burn_efuse JTAG_DISABLE 1
# Step 5: Disable ROM download mode (critical: must be done for production)
python -m espefuse --port /dev/ttyUSB0 burn_efuse DOWNLOAD_DIS 1
# Always verify the burn succeeded before shipping:
python -m espefuse --port /dev/ttyUSB0 summary | grep -E "JTAG|DOWNLOAD|FLASH"
eFuse and option byte protection prevents software-level debug access. Physical access to unpopulated JTAG headers or accessible test point pads can still provide a path for hardware attack. On production PCBs: depopulate all debug connector footprints, fill JTAG test point vias with solder, cover them with soldermask, and use tamper-evident enclosure seals over any remaining access points. Security researchers routinely find and exploit JTAG headers on consumer IoT devices within minutes of opening the enclosure, because manufacturers design for manufacturability and forget to remove the debug access for production.
Beyond preventing debug readback, flash memory protection controls which software can write to which flash regions. Write protection is the defence against an application-level bug or a compromised application overwriting the bootloader, the stored public key or the security configuration.
On STM32F4 devices, individual flash sectors can be write-protected through option bytes independently of RDP level. The bootloader sector (typically Sector 0) should always be write-protected so that application code, regardless of how badly it is compromised, cannot overwrite the boot verification logic:
/* Write-protect flash Sector 0 (bootloader) on STM32F4.
This is independent of RDP level and survives firmware updates.
Once write-protected, the sector can only be written after
explicitly unlocking option bytes, which requires physical access
or a deliberate firmware update step. */
HAL_StatusTypeDef write_protect_bootloader_sector(void) {
FLASH_OBProgramInitTypeDef ob_init = {0};
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
ob_init.OptionType = OPTIONBYTE_WRP;
ob_init.WRPState = OB_WRPSTATE_ENABLE;
ob_init.WRPSector = OB_WRP_SECTOR_0; /* Bootloader sector */
ob_init.Banks = FLASH_BANK_1;
HAL_StatusTypeDef status = HAL_FLASHEx_OBProgram(&ob_init);
if (status == HAL_OK) {
HAL_FLASH_OB_Launch(); /* Reset to apply */
}
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
return status;
}
On ESP32 with flash encryption enabled, the entire SPI flash contents are encrypted with AES-256 using a key stored in eFuse. Reading the flash chip directly with a programmer yields only ciphertext. The decryption happens transparently inside the ESP32 SoC during flash read operations, so the plaintext is never present on the SPI bus between the chip and the external flash. This defeats bus probing attacks as well as chip desoldering attacks.
Microcontroller vendors have added progressively more hardware security features over the past decade. Understanding what your chosen chip provides is the first step in designing a hardware security architecture, because software cannot compensate for features the silicon does not have.
TrustZone for Cortex-M (also called Armv8-M Security Extension) creates two isolated execution domains in hardware: the Secure World and the Non-Secure World. Code and data in the Secure World is inaccessible to the Non-Secure World at the hardware level, even in the same processor core. Key management, authentication logic, cryptographic operations and security-critical state run in the Secure World. The application, network stack and user-facing logic run in the Non-Secure World.
The boundary between worlds is enforced by the hardware SAU (Security Attribution Unit) and IDAU (Implementation-Defined Attribution Unit). A Non-Secure World function cannot call Secure World code directly: it must use a defined NSC (Non-Secure Callable) entry point. This means a vulnerability in the application layer cannot directly access the key store even if it achieves arbitrary code execution in the Non-Secure World.
Chips with TrustZone include: STM32L5, STM32U5, nRF9160, nRF5340 and the Renesas RA6M4 family. For new designs with a security requirement, choosing a TrustZone-capable device is strongly recommended over compensating entirely in software.
Hardware crypto accelerators matter for security in three ways beyond raw speed:
A secure element is a dedicated tamper-resistant security IC connected to the main processor over I2C, SPI or a single-wire protocol. It stores cryptographic keys internally, performs operations (signing, ECDH (Elliptic Curve Diffie-Hellman) key agreement, symmetric encryption) internally, and returns only the result. The private key never leaves the secure element, even in response to software commands. Common options for embedded IoT designs:
| Secure Element | Interface | Key Operations | Certifications | Typical Use |
|---|---|---|---|---|
| Microchip ATECC608B | I2C, Single-Wire | ECDSA P-256, ECDH, AES-128, SHA-256 | FIPS 140-2 (planned), Common Criteria EAL2+ | IoT device identity, TLS client auth |
| NXP SE050 | I2C | RSA up to 4096-bit, ECC, AES, 3DES | CC EAL6+ | High-security IoT, payment |
| Infineon OPTIGA Trust M | I2C | ECDSA P-256/P-384, RSA, AES, SHA | CC EAL6+ | Industrial IoT, smart home |
| STMicro STSAFE-A110 | I2C | ECDSA P-256/P-384, SHA | CC EAL5+ | Automotive, industrial |
Secure boot is the mechanism that establishes trust in the software running on the device by cryptographically verifying each component before it executes. Without secure boot, an attacker who can write to flash, whether through an exploited OTA update handler, an open debug port, or physical flash access, can run arbitrary code on the device at the next power cycle.
Every secure boot implementation starts with a root of trust: a piece of code or data that is assumed to be correct without any prior verification, because there is nothing above it in the chain to verify it. In embedded systems, the root of trust is the ROM bootloader, code burned into read-only memory at the silicon level that cannot be modified after manufacture. This code is the one component an attacker cannot replace through software means, which is why it must be the starting point of every verification.
The root of trust holds (or has access via OTP memory to) the root public key: the cryptographic public key against which the first-stage bootloader signature is checked. This public key is typically burned into OTP or eFuse memory at factory time as part of the device provisioning process.
The chain of trust extends the root of trust through each boot stage by having each verified stage verify the next one before passing execution to it:
A chain is only as strong as its weakest link. A secure boot implementation that verifies the application firmware but not the first-stage bootloader is broken: an attacker can replace the first-stage bootloader with a version that skips the application verification step, installing unsigned firmware while the ROM bootloader continues to run normally because the first-stage bootloader it verified has not changed.
The policy on verification failure must be defined and implemented explicitly. Three common approaches:
ESP32 secure boot V2 uses RSA-PSS 3072-bit signatures (or ECDSA on ESP32-C3 and later) to verify the bootloader and application images. The process is managed through the ESP-IDF menuconfig and eFuse programming. Here is the complete setup sequence:
# ESP32 Secure Boot V2 setup using ESP-IDF.
# Run all commands from within an activated ESP-IDF environment.
# Prerequisites: ESP-IDF v4.3 or later, OpenSSL for key generation.
# --- Step 1: Generate the secure boot signing key ---
# The private key is used to sign firmware images at build time.
# Keep this key offline and in secure storage. Losing it means you
# cannot sign future firmware updates for devices with this key burned.
openssl genrsa -out secure_boot_signing_key.pem 3072
# Extract the public key for verification (this is what gets burned to eFuse)
openssl rsa -in secure_boot_signing_key.pem -pubout \
-out secure_boot_verification_key.pem
# --- Step 2: Configure ESP-IDF for secure boot ---
# In menuconfig: Security features -> Enable hardware Secure Boot in bootloader
# Set: Secure Boot V2 enabled
# Set: Sign binaries during build
# Set: Secure boot private signing key path = secure_boot_signing_key.pem
idf.py menuconfig
# --- Step 3: Build and flash with secure boot ---
# The build system automatically signs the bootloader and application.
idf.py build
idf.py -p /dev/ttyUSB0 flash
# On first boot, the ESP32 ROM bootloader detects that ABS_DONE_0 is not set,
# hashes and burns the public key digest to eFuse, then sets ABS_DONE_0.
# All subsequent boots verify the bootloader signature against this burned digest.
# --- Step 4: Verify eFuse state after first boot ---
python -m espefuse --port /dev/ttyUSB0 summary | grep -E "ABS_DONE|SECURE_BOOT"
# Expected: ABS_DONE_0 = 1 (secure boot stage 1 enabled)
# --- Step 5: Sign firmware updates for OTA delivery ---
# All subsequent firmware images must be signed with the same private key.
espsecure.py sign_data \
--version 2 \
--keyfile secure_boot_signing_key.pem \
--output firmware_signed.bin \
build/firmware.bin
STM32 secure boot is implemented using STM32CubeBootloader or a custom first-stage bootloader that calls HAL flash and cryptographic APIs. The simpler approach for most designs is the STM32 SBSFU (Secure Boot and Secure Firmware Update) reference implementation provided in STM32CubeExpansion packages:
# STM32 secure boot key generation and image signing workflow.
# Uses the STM32CubeExpansion_SBSFU package signing scripts.
# The signing scripts use Python with the pycryptodome library.
# Install the signing tool dependency
pip install pycryptodome
# --- Step 1: Generate an ECDSA P-256 keypair for firmware signing ---
# This uses the STM32CubeExpansion signing script format.
# The private key (.pem) stays with the signing infrastructure.
# The public key is compiled into the first-stage bootloader.
python SBSFU/Utilities/KeysGenerator/generatekeys.py \
--key-type ECDSA_P256 \
--priv-key signing_key_private.pem \
--pub-key signing_key_public.pem
# --- Step 2: Compile the public key into the SBSFU bootloader ---
# The SBSFU Makefile reads signing_key_public.pem and converts it to
# a C byte array embedded in the bootloader image. Rebuild the bootloader
# after key generation.
make -C SBSFU/STM32CubeExpansion_SBSFU/Projects/NUCLEO-L476RG/2_Images_SBSFU
# --- Step 3: Sign the application firmware image ---
python SBSFU/Utilities/PostProcessing/sign.py \
--layout SBSFU/Projects/NUCLEO-L476RG/linker/mapping_sbsfu.ld \
--key signing_key_private.pem \
--input build/application.bin \
--output build/application_signed.bin
# --- Step 4: Program the device (bootloader + signed application) ---
STM32_Programmer_CLI -c port=SWD -w SBSFU_Build/SBSFU.bin 0x08000000
STM32_Programmer_CLI -c port=SWD -w build/application_signed.bin 0x08020000
# --- Step 5: Set RDP to Level 1 and write-protect bootloader sector ---
# Done via STM32_Programmer_CLI option byte programming or
# the firmware provisioning routine shown earlier in this article.
STM32_Programmer_CLI -c port=SWD -ob RDP=0xBB WRP1A_STRT=0x0 WRP1A_END=0x7
Anti-rollback prevents an attacker from installing an older, signed firmware version that contains a vulnerability that was patched in a later release. The attack scenario is: a vulnerability is found in firmware version 1.0.0, patched in version 1.0.1, and the attacker forces a downgrade to version 1.0.0 to re-expose the vulnerability. All versions are validly signed, so signature verification alone does not prevent this.
The defence is a monotonic counter stored in OTP or eFuse memory. Every firmware image carries a minimum version field in its signed header. The bootloader reads the current counter from OTP and rejects any firmware image whose minimum version is lower than the counter value. When a security patch is released, the counter is incremented: all devices that install the patch burn the new counter value, and they will never accept the older firmware again.
/* Anti-rollback check in the first-stage bootloader.
FW_HEADER_ADDR is the start of the signed firmware image header in flash.
The anti-rollback counter is read from OTP memory at OTP_COUNTER_ADDR.
The firmware version field in the signed header is trusted because the
header is part of the signed image: modifying it invalidates the signature. */
#define OTP_COUNTER_ADDR 0x1FFF7800 /* STM32F4 OTP area base address */
#define ROLLBACK_COUNTER_OFFSET 4 /* Byte offset within OTP block */
typedef struct __attribute__((packed)) {
uint32_t magic; /* 0xDEADBEEF sanity check */
uint32_t version; /* Firmware version number */
uint32_t min_version; /* Anti-rollback: minimum accepted version */
uint32_t image_size; /* Size of firmware image in bytes */
uint8_t signature[64]; /* ECDSA P-256 signature over all above + image */
} FirmwareHeader;
typedef enum {
ROLLBACK_CHECK_OK,
ROLLBACK_CHECK_REJECTED,
ROLLBACK_CHECK_OTP_ERROR
} RollbackCheckResult;
RollbackCheckResult check_anti_rollback(const FirmwareHeader *header) {
/* Read current anti-rollback counter from OTP memory */
const uint32_t *otp_counter =
(const uint32_t *)(OTP_COUNTER_ADDR + ROLLBACK_COUNTER_OFFSET);
uint32_t current_min_version = *otp_counter;
/* OTP unwritten bytes read as 0xFFFFFFFF; treat as version 0 */
if (current_min_version == 0xFFFFFFFF) {
current_min_version = 0;
}
/* Reject firmware whose minimum version is below the counter */
if (header->min_version < current_min_version) {
log_security_event(SEC_EVENT_ROLLBACK_ATTEMPT,
header->min_version, current_min_version);
return ROLLBACK_CHECK_REJECTED;
}
return ROLLBACK_CHECK_OK;
}
/* Burn a new anti-rollback counter value to OTP after a successful update.
This is called only after the new firmware has booted successfully.
WARNING: This operation is irreversible. */
HAL_StatusTypeDef burn_rollback_counter(uint32_t new_min_version) {
HAL_StatusTypeDef status;
HAL_FLASH_Unlock();
/* Write the new minimum version to OTP memory */
status = HAL_FLASH_Program(
FLASH_TYPEPROGRAM_WORD,
OTP_COUNTER_ADDR + ROLLBACK_COUNTER_OFFSET,
new_min_version
);
HAL_FLASH_Lock();
return status;
}
Trusted storage provides hardware-backed protection for cryptographic keys and sensitive credentials. The right choice depends on your security requirement, budget and available hardware interfaces.
OTP and eFuse memory are write-once storage regions on the microcontroller itself. Once a bit is programmed to 1, it cannot be returned to 0. They are used for: the root public key hash for secure boot verification, device unique identifiers, security configuration flags (JTAG disable, flash encryption enable, download mode disable) and symmetric root keys for key derivation.
OTP and eFuse are not designed for general-purpose secret storage. The bit capacity is small (typically 1 to 4 KB), the programming is irreversible, and on most devices the stored values are readable by the processor during normal execution. Their primary value is immutability: an attacker cannot modify the root public key or the security configuration flags through software means.
For larger volumes of sensitive data (device certificates, WiFi credentials, application keys), an encrypted flash partition provides a software-accessible but hardware-encrypted storage region. On ESP32, the NVS partition encrypted by the hardware flash encryption engine is the standard approach. On STM32 with an external secure flash device (IS25LP, MX25L with security register), the security register provides a small (512-byte) region with write-once protection suitable for key storage.
Connecting an external secure element over I2C is the highest practical security level for standard embedded IoT hardware. The ATECC608B is the most widely used option. Here is how to perform an ECDSA P-256 signing operation using the ATECC608B with the cryptoauthlib library, without the private key ever being accessible to the host processor:
/* ECDSA P-256 signing using Microchip ATECC608B secure element.
The private key was generated inside the ATECC608B at provisioning time
and has never been readable. The host processor only provides the
message hash and receives the signature. */
#include "cryptoauthlib.h"
#include "atca_basic.h"
/* Key slot 0 is configured for P-256 private key storage at provisioning.
Configuration is locked in the ATECC608B configuration zone. */
#define SIGNING_KEY_SLOT 0
typedef enum {
SIGN_RESULT_OK,
SIGN_RESULT_HASH_ERROR,
SIGN_RESULT_ATCA_ERROR
} SignResult;
SignResult sign_with_secure_element(const uint8_t *message,
size_t message_len,
uint8_t *signature_out) {
uint8_t message_hash[32];
ATCA_STATUS status;
/* Step 1: Hash the message using the secure element's SHA-256 engine */
status = atcab_sha(message_len, message, message_hash);
if (status != ATCA_SUCCESS) {
return SIGN_RESULT_HASH_ERROR;
}
/* Step 2: Sign the hash using the private key in slot 0.
The private key is used internally by the ATECC608B.
The host processor never sees the private key. */
status = atcab_sign(SIGNING_KEY_SLOT, message_hash, signature_out);
if (status != ATCA_SUCCESS) {
return SIGN_RESULT_ATCA_ERROR;
}
/* signature_out now contains a 64-byte ECDSA P-256 signature (r || s) */
return SIGN_RESULT_OK;
}
Device authentication answers the question: before this device connects to the network or cloud infrastructure, how does the server know it is a genuine, authorised device rather than a clone, a counterfeit or a rogue device introduced by an attacker?
X.509 certificate-based mutual TLS is the standard for device authentication in production IoT deployments. Each device carries a unique X.509 client certificate whose private key is stored in the secure element. During the TLS handshake, the device presents its certificate and proves possession of the private key via the TLS client authentication mechanism. The server validates the certificate against a manufacturer CA (Certificate Authority) certificate.
The provisioning workflow:
AWS IoT Core, Azure IoT Hub and Google Cloud IoT all support X.509 mutual TLS device authentication with this workflow. It provides strong device identity that is tied to hardware (the private key in the secure element) rather than to credentials that can be copied.
For simpler deployments without a full PKI (Public Key Infrastructure), a challenge-response protocol proves device identity without transmitting the secret. The server sends a random nonce (number used once). The device signs the nonce with its private key (or computes an HMAC using a shared secret). The server verifies the signature or HMAC. A valid response proves the device holds the correct key without exposing it.
/* Challenge-response authentication using HMAC-SHA256.
The device and server share a device-unique secret key provisioned at factory.
The server sends a 32-byte random challenge. The device computes HMAC-SHA256
over the challenge and sends back the 32-byte MAC. The server independently
computes the same HMAC and compares. */
#include "mbedtls/md.h"
#define HMAC_KEY_LEN 32 /* 256-bit shared secret */
#define CHALLENGE_LEN 32 /* 256-bit random nonce */
#define RESPONSE_LEN 32 /* HMAC-SHA256 output */
/* The device's HMAC key is loaded from secure storage at startup.
It is never stored in plaintext in the firmware binary. */
static uint8_t g_device_hmac_key[HMAC_KEY_LEN]; /* Loaded from secure storage */
typedef enum {
AUTH_RESPONSE_OK,
AUTH_RESPONSE_ERROR
} AuthResponseResult;
AuthResponseResult compute_challenge_response(const uint8_t *challenge,
uint8_t *response_out) {
mbedtls_md_context_t ctx;
const mbedtls_md_info_t *md_info;
int ret;
md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
mbedtls_md_init(&ctx);
ret = mbedtls_md_setup(&ctx, md_info, 1); /* 1 = HMAC mode */
if (ret != 0) goto cleanup;
ret = mbedtls_md_hmac_starts(&ctx, g_device_hmac_key, HMAC_KEY_LEN);
if (ret != 0) goto cleanup;
ret = mbedtls_md_hmac_update(&ctx, challenge, CHALLENGE_LEN);
if (ret != 0) goto cleanup;
ret = mbedtls_md_hmac_finish(&ctx, response_out);
cleanup:
mbedtls_md_free(&ctx);
return (ret == 0) ? AUTH_RESPONSE_OK : AUTH_RESPONSE_ERROR;
}
A PUF (Physical Unclonable Function) generates a device-unique identity from the physical variations introduced during the semiconductor manufacturing process. Every chip has minute, uncontrollable differences in transistor threshold voltages, wire resistance and oxide thickness. These differences are consistent for a given chip across temperature and voltage variation, but differ between chips even from the same wafer and the same manufacturing run.
A PUF circuit exploits these variations to produce a device fingerprint: given a specific input challenge, the PUF circuit produces an output response that is stable for this device and statistically uncorrelated to the response of any other device to the same challenge. The critical security property is that the fingerprint cannot be cloned: even the chip manufacturer cannot reproduce the same PUF response in a different chip because it arises from genuinely random physical variation.
The most common use is key generation without key storage. Rather than generating a key, storing it in flash and protecting the stored value, a PUF-based system derives the key from the device’s physical fingerprint on demand. The key exists in RAM only while it is being used, and is reproduced from the PUF response rather than from stored data whenever it is needed. An attacker who extracts the flash chip finds no key material because none is stored there.
The NXP LPC55S69 and the STM32MP1 both include on-die PUF circuits. Infineon’s OPTIGA Trust X and several SRAM PUF IP providers (Intrinsic ID) offer PUF-based key generation for microcontrollers that do not have native PUF hardware.
PUF responses are not perfectly stable. Environmental factors (extreme temperature, aging, ionising radiation) can cause some response bits to flip. Production PUF systems use error correction codes (BCH or Reed-Solomon) applied to the raw PUF response to reconstruct the same key reliably across operating conditions. The helper data required for error correction is stored in non-volatile memory but reveals nothing useful about the key without the physical PUF response.
Hardware monitoring detects physical attack attempts in progress rather than after the fact. The goal is to sense that something is wrong and respond before the attacker completes their objective.
Detection without response is useless. When a tamper condition is detected, the firmware must respond immediately and irreversibly to prevent the attack from succeeding. The three-tier response model:
/* Tamper response handler: called from GPIO ISR when tamper switch triggers,
or from a monitoring task when voltage/temperature anomaly is detected.
Response must be immediate: complete within one ISR execution window.
The zeroization of keys is the critical step - it must complete even if
the device is immediately powered off after triggering. */
#include "secure_storage.h" /* Platform-specific key storage API */
#include "system_control.h" /* Platform-specific reset/lockdown API */
/* Tamper event severity levels */
typedef enum {
TAMPER_ALERT = 1, /* Log and notify, continue operation */
TAMPER_LOCKDOWN = 2, /* Disable sensitive functions, require re-auth */
TAMPER_ERASE = 3 /* Zeroize all keys and enter permanent lockdown */
} TamperResponse;
/* Called from GPIO ISR on tamper switch activation.
Severity 3 (ERASE) is used for physical breach of enclosure. */
void __attribute__((interrupt)) tamper_isr(void) {
/* Immediately respond at highest severity for physical breach */
handle_tamper_event(TAMPER_ERASE);
EXTI->PR = TAMPER_EXTI_LINE; /* Clear interrupt pending bit */
}
void handle_tamper_event(TamperResponse severity) {
/* Always log the event first, even if subsequent steps fail */
log_tamper_event_to_protected_log(severity);
if (severity >= TAMPER_LOCKDOWN) {
/* Disable network interfaces to prevent data exfiltration */
disable_all_network_interfaces();
/* Disable any active sessions */
invalidate_all_sessions();
}
if (severity >= TAMPER_ERASE) {
/* Zeroize all cryptographic key material.
This must use secure_zero (not memset) to prevent
compiler optimisation eliminating the wipe. */
secure_storage_zeroize_all_keys();
/* Invalidate the device certificate to prevent future authentication */
secure_storage_invalidate_device_cert();
/* Enter permanent lockdown: device requires factory re-provisioning */
system_enter_permanent_lockdown();
/* Halt: do not return to any application code */
while (1) { /* Permanent halt */ }
}
}
A watchdog timer (WDT) is a hardware counter that resets the device if the firmware does not reset the counter within a defined period. Its primary purpose is availability: recovering from firmware crashes and infinite loops. It is also a meaningful security control because many attack scenarios cause the firmware to hang: a successful fault injection that sends execution to an unexpected address, a buffer overflow that corrupts the stack, or a timing attack that enters an unintended tight loop.
Configuring the watchdog correctly for security means: enabling it as early as possible in the boot sequence (not after full application initialisation), using the shortest timeout that normal operation can comfortably meet, using an independent watchdog (IWDG on STM32, TWDT on ESP32) that cannot be disabled by application software once started, and ensuring the watchdog reset handler logs the reset reason so anomalous resets are visible in telemetry.
/* Independent Watchdog configuration on STM32.
The IWDG runs on the LSI (Low Speed Internal) oscillator (~32kHz)
independent of the main system clock and application code.
Once started, it cannot be stopped by software. */
#include "stm32f4xx_hal.h"
static IWDG_HandleTypeDef hiwdg;
/* Initialize IWDG with approximately 2-second timeout.
Prescaler 256 and reload 250: (256 / 32000) * 250 ≈ 2.0 seconds.
Call this during early boot, before any user-facing code runs. */
HAL_StatusTypeDef watchdog_init(void) {
hiwdg.Instance = IWDG;
hiwdg.Init.Prescaler = IWDG_PRESCALER_256;
hiwdg.Init.Reload = 250;
return HAL_IWDG_Init(&hiwdg);
}
/* Reset the watchdog. Must be called at least once per ~1.8 seconds.
Call this from the main application loop or a dedicated watchdog task.
If the firmware hangs (attack, crash, infinite loop), the IWDG
expires and resets the device. */
void watchdog_kick(void) {
HAL_IWDG_Refresh(&hiwdg);
}
/* Check the reset cause at startup to detect abnormal watchdog resets.
Log watchdog resets for security monitoring: frequent watchdog resets
can indicate ongoing fault injection attacks. */
bool was_reset_by_watchdog(void) {
return (__HAL_RCC_GET_FLAG(RCC_FLAG_IWDGRST) != 0);
}
Runtime attestation allows a remote server to verify that a device is running the expected firmware version, with the expected configuration, and has not been tampered with since it left the factory. It extends the boot-time integrity check from a one-time verification into an ongoing assurance that is verifiable remotely.
The attestation process:
This mechanism catches compromised devices that passed initial boot verification but have subsequently been modified (through an exploited OTA path, a compromised update server, or physical access after deployment). The attestation report signed by the secure element’s key cannot be forged by application code even if the application layer is completely compromised, because the secure element performs the signing operation independently of the main processor.
Embedded hardware security is the layer that gives every software control its actual meaning. An encryption scheme is only as strong as the hardware protecting the key. A signature verification is only as reliable as the chain of trust that starts at the immutable ROM bootloader. A secure provisioning process only holds if the debug interface is locked before the device leaves the factory. The controls covered here, from STM32 RDP option bytes and ESP32 eFuse configuration to ATECC608B secure element key operations, PUF-based identity and conductive mesh tamper detection, form a hierarchy that starts at the silicon and extends through the full device lifecycle. Building this hierarchy requires decisions at component selection time, at PCB layout time, at factory provisioning time and at firmware design time. Leave any layer incomplete and the layers above it inherit the gap. Get all of them right and you have a device that is genuinely difficult to compromise even for a well-resourced attacker with physical access.






