Hardware Security and Secure Boot

MuhammadMuhammadEmbedded Security6 days ago10 Views

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.

Why Hardware Security Is the Foundation

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 Port Lockdown: JTAG, SWD and UART

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: Flash Readout Protection (RDP)

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: eFuse-Based Security Configuration

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:

  • ABS_DONE_0: Enables secure boot verification for the first bootloader stage.
  • ABS_DONE_1: Enables secure boot verification for the second stage.
  • JTAG_DISABLE: Permanently disables JTAG access to the processor.
  • DOWNLOAD_DIS: Disables the ROM download mode (the hardware bootloader that accepts firmware over UART). Essential: without this burned, an attacker can hold GPIO0 low at boot and reflash the device over UART regardless of other protections.
  • FLASH_CRYPT_CNT: Controls flash encryption. When the least significant bit is 1, flash encryption is enabled.
# 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"

PCB-Level Measures

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.

Flash Memory Protection

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.

Modern Chip Security Features

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 (ARM Cortex-M33 and Above)

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 Cryptography Accelerators

Hardware crypto accelerators matter for security in three ways beyond raw speed:

  • A hardware AES engine is significantly harder to attack via power analysis than a software AES implementation, because the power consumption pattern of dedicated silicon is less correlated to the key bytes being processed.
  • A hardware TRNG (True Random Number Generator) draws entropy from thermal noise or other physical entropy sources, producing genuinely unpredictable values. A software PRNG (Pseudo-Random Number Generator) seeded from a timer or ADC reading can be predicted by an attacker who knows the seed source.
  • Hardware acceleration reduces the CPU time spent on cryptographic operations, making it feasible to use stronger algorithms and longer key sizes without impacting real-time performance.

Secure Elements

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: Chain of Trust from ROM to Application

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.

The Root of Trust

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

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:

  1. ROM bootloader (trust anchor): Executes from immutable ROM. Loads the first-stage bootloader from flash. Verifies its digital signature using the root public key from OTP memory. Halts if verification fails.
  2. First-stage bootloader (verified by ROM): Trusted after verification. Initialises hardware, loads the application firmware image. Verifies its digital signature using the public key embedded within the first-stage bootloader (or from OTP). Halts or enters recovery mode if verification fails.
  3. Application firmware (verified by first-stage bootloader): Trusted after verification. Runs the device’s application logic. May optionally verify other components (OS kernel modules, application plugins) before loading them.

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.

What Happens on Verification Failure

The policy on verification failure must be defined and implemented explicitly. Three common approaches:

  • Halt: The device stops and does not proceed. Appropriate for safety-critical devices where operating with unverified firmware is more dangerous than not operating at all.
  • Recovery mode: The device enters a minimal, constrained mode that accepts a signed firmware update over a local interface (USB, UART with physical presence required). Allows field recovery without a complete device replacement.
  • Rollback to last known good: With dual-bank flash layout, the device boots the previous firmware version if the new one fails verification. The previous version is only overwritten after the new version has been verified and has run successfully for a defined period.

Implementing Secure Boot on ESP32

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

Implementing Secure Boot on STM32

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 Protection

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 Methods

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

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.

Encrypted Flash Partitions

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.

Secure Elements

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

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?

Certificate-Based Authentication

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:

  1. At the factory, generate a key pair inside the device’s secure element. The private key never leaves the device.
  2. Export the device’s public key from the secure element and submit a CSR (Certificate Signing Request) to the manufacturer’s CA.
  3. The CA issues a signed X.509 certificate containing the device’s public key, a unique device identifier and a validity period.
  4. Store the signed certificate in the device’s encrypted flash partition.
  5. Register the device’s certificate thumbprint in the cloud backend’s device registry.

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.

Challenge-Response Authentication

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;
}

Physical Unclonable Functions

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.

How PUFs Are Used in Embedded Security

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 Limitations

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.

Monitoring Hardware-Level Changes

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.

Tamper Detection Circuit Types

  • Mechanical tamper switches: A microswitch connected to a GPIO that changes state when the enclosure is opened. The simplest and cheapest option. Can be defeated by inserting a probe before opening, or by holding the switch in the closed position. Appropriate for consumer devices where the attack model does not include sophisticated physical adversaries.
  • Light sensors: A photodiode inside a sealed enclosure that detects the change in ambient light when the enclosure is opened. Harder to defeat than a mechanical switch because it detects any breach of the enclosure, not just operation of a specific switch.
  • Conductive mesh: A grid of fine conductive traces covering the sensitive area of the PCB or the interior surface of the enclosure. Any drilling, milling or penetration attempt breaks one or more traces, triggering detection. Used in payment terminals, HSMs and high-security industrial controllers. Requires dedicated tamper detection ASIC to monitor the mesh continuously.
  • Voltage and clock monitors: Sensors that detect supply voltage outside the normal operating range (indicating a voltage glitching attempt) or clock frequency outside the normal range (indicating a clock glitching attempt). Several STM32 devices include built-in brownout detection. The ESP32 includes a power glitch detector that can trigger a system reset.
  • Temperature sensors: Extreme temperature manipulation (freezing DRAM to slow bit decay for cold boot attacks, or heating to affect threshold voltages) can be detected by monitoring the on-die temperature sensor against expected operating ranges.

Tamper Response Implementation

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 */ }
    }
}

Watchdog Timers as a Security Control

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

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:

  1. The device periodically measures its own state: hashes the running firmware image, reads the security configuration register, checks the OTP counter value and assembles this into an attestation report.
  2. The report is signed by the device’s attestation key, stored in the secure element.
  3. The signed report is sent to the attestation server over a secure channel.
  4. The server verifies the signature, compares the firmware hash against the expected hash for the claimed firmware version, and confirms the device configuration matches the expected profile.
  5. Devices that fail attestation (wrong firmware hash, disabled security features, abnormal configuration) are quarantined from the network and flagged for investigation.

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.

Conclusion

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.

Leave a reply

Loading Next Post...
Follow
Search Trending
Popular Now
Loading

Signing-in 3 seconds...

Signing-up 3 seconds...