How-To-Tutorials · October 11, 2025

How to Implement ARM TrustZone for Secure Firmware Updates in STM32

how to implement arm trustzone for secure firmware updates in stm32

Why TrustZone Matters for Firmware Updates

Firmware updates over the air (or even over USB) are a massive attack surface. If someone can tamper with your update image or hijack the update process, they own your device. ARM TrustZone gives you hardware-enforced isolation on STM32 microcontrollers—a "secure world" that the normal application code literally cannot touch. You can put your bootloader, crypto keys, and update verification logic in that secure world, and even if your application firmware gets compromised, the update path stays protected.

This guide covers setting up TrustZone on an STM32 with secure/non-secure partitioning, building a secure bootloader for firmware updates, and testing the whole flow. We're targeting the STM32L5 or STM32U5 series since those have proper TrustZone-M support (Cortex-M33 based). The older STM32F7/H7 parts don't have TrustZone—don't confuse their MPU-based isolation with the real thing.

Prerequisites

  • Working knowledge of STM32 development and the HAL library.
  • Understanding of ARM Cortex-M33 architecture basics (secure/non-secure states, SAU, IDAU).
  • Familiarity with public-key cryptography concepts (signing, verification).
  • A TrustZone-capable STM32 board: STM32L552, STM32L562, STM32U575, or STM32U585 Nucleo/Discovery.
  • STM32CubeIDE v1.16+ installed and configured.

Parts/Tools

  • STM32L5 or STM32U5 development board with TrustZone support (Cortex-M33 based).
  • STM32CubeIDE v1.16+ (includes STM32CubeMX integration).
  • STM32CubeProgrammer for option byte configuration and flash programming.
  • ST-Link debugger (built into most Nucleo boards).
  • MCUboot or STM32 Secure Boot and Secure Firmware Update (SBSFU) package for production-ready bootloader reference.
  • OpenSSL or similar tool for generating signing keys.

Steps

  1. Set Up the Development Environment
    1. Install STM32CubeIDE v1.16+ from the STMicroelectronics website. The integrated CubeMX handles TrustZone project generation directly.
    2. When creating a new project, select your specific MCU (e.g., STM32L552ZE). CubeIDE will ask whether to create a TrustZone project—say yes. This generates two sub-projects: one for secure code, one for non-secure.
    3. Install the matching STM32L5 or STM32U5 firmware package through the CubeIDE package manager. You'll need the HAL drivers for both secure and non-secure contexts.
  2. Configure TrustZone Partitioning
    1. Open the .ioc file in CubeMX and go to the Security section. You need to define which flash pages and SRAM regions belong to the secure world vs. non-secure.
    2. Configure the SAU (Security Attribution Unit) regions. Typically, you'll mark the first portion of flash as secure (for your bootloader and crypto functions) and the remainder as non-secure (for your application).
    3. Set up GTZC (Global TrustZone Controller) to protect specific peripherals. Your crypto accelerator, RNG, and flash controller should stay in the secure world.
    4. Watch out: the option bytes must have TZEN=1 to enable TrustZone. Use STM32CubeProgrammer to set this. Once TZEN is set, you can't easily go back without a full chip erase, so be deliberate about it.
    5. Generate the project code. CubeMX creates the partition configuration in the secure project's partition_stm32l5xx.h file.
  3. Implement Secure and Non-Secure Code
    1. The secure project runs first at boot. It initializes the SAU, configures security boundaries, and then jumps to the non-secure application. Your secure firmware handles all sensitive operations.
    2. Define Non-Secure Callable (NSC) functions—these are the gateway between worlds. The non-secure app calls these to request secure services:
      /* In the secure project - exported via veneer table */
      CMSE_NS_ENTRY void SECURE_VerifyFirmwareSignature(
          uint8_t *fw_data, uint32_t fw_size,
          uint8_t *signature, uint32_t *result)
      {
          /* Verify ECDSA signature using secure-world keys */
          *result = verify_ecdsa_p256(fw_data, fw_size,
                                      signature, stored_public_key);
      }
    3. The non-secure project contains your actual application. It calls secure functions through the NSC interface but never directly accesses secure memory:
      /* In the non-secure project */
      void CheckAndApplyUpdate(void)
      {
          uint32_t verify_result = 0;
          /* Call into secure world for signature check */
          SECURE_VerifyFirmwareSignature(
              update_buffer, update_size,
              update_signature, &verify_result);
      
          if (verify_result == SIGNATURE_VALID) {
              SECURE_ApplyFirmwareUpdate(update_buffer, update_size);
          }
      }
    4. The key insight: your signing keys and verification logic live entirely in the secure world. Even if an attacker gains code execution in the non-secure application, they can't extract the keys or bypass signature verification.
  4. Build the Secure Bootloader
    1. The bootloader runs in the secure world before anything else. On every boot, it checks whether a new firmware image is pending in a staging area of flash.
    2. Here's the core bootloader flow:
      void SecureBootloader_Main(void)
      {
          /* Initialize secure peripherals */
          HAL_Init();
          SystemClock_Config();
          SAU_and_GTZC_Init();
      
          /* Check for pending firmware update */
          if (FW_UpdatePending()) {
              uint8_t *staged_fw = (uint8_t *)FW_STAGING_ADDRESS;
              uint32_t fw_size = GetStagedFirmwareSize();
      
              if (VerifySignature(staged_fw, fw_size) == SIG_OK) {
                  EraseAppRegion();
                  CopyFirmware(staged_fw, APP_ADDRESS, fw_size);
                  ClearUpdateFlag();
              } else {
                  /* Signature failed - discard the update */
                  ClearUpdateFlag();
              }
          }
      
          /* Boot into non-secure application */
          JumpToNonSecureApp(NS_APP_ADDRESS);
      }
    3. For production systems, look into MCUboot or ST's SBSFU reference implementation. They handle rollback protection, version checking, and dual-bank swap mechanisms that you really don't want to build from scratch.
    4. Tip: always verify the firmware signature before erasing the existing application. If verification fails, you still have a working system. If you erase first and then verification fails, you've bricked the device.
  5. Test the Implementation
    1. Connect your board via the ST-Link. Flash the secure project first, then the non-secure project. Order matters—the secure binary must be in place before the non-secure one tries to boot.
    2. Verify the security boundaries by intentionally trying to read secure memory from the non-secure context. You should get a HardFault (SecureFault). If you don't, your SAU configuration is wrong.
    3. Test the update flow: stage a properly signed firmware image, reboot, and confirm the bootloader picks it up and applies it. Then try with a corrupted signature—the bootloader should reject it and boot the existing firmware.
    4. Use the SWD debugger in secure-aware mode (STM32CubeIDE supports this) to step through both secure and non-secure code during development.

Troubleshooting

  • SecureFault on boot:
    • Your SAU regions are misconfigured. Double-check that the non-secure application's address range is properly marked as non-secure in the SAU setup.
    • Make sure the vector table address for the non-secure app is aligned correctly (must be on a 512-byte boundary for most STM32L5/U5 parts).
  • Non-secure app can't call secure functions:
    • Verify that NSC functions are in the correct memory region (the NSC region defined in your linker script).
    • Check that the veneer table is exported properly. CubeMX generates a secure_nsc.h header—make sure the non-secure project includes it.
  • Firmware update applies but app won't boot afterward:
    • The staged image might be targeting the wrong flash address. Verify your linker script's FLASH origin matches where the bootloader copies the firmware.
    • Check that the update image was built for the non-secure context with the correct vector table offset.
  • Can't debug after enabling TZEN:
    • TrustZone restricts debug access by default. You may need to configure RDP (Read Protection) level and debug permissions through option bytes in STM32CubeProgrammer. During development, keep RDP at level 0.

Summary

TrustZone on STM32 Cortex-M33 parts gives you a real hardware security boundary for firmware updates. The secure world owns the keys and the verification logic, the bootloader validates everything before applying it, and the non-secure application can't tamper with any of it. For production, build on top of MCUboot or ST's SBSFU rather than rolling your own—the dual-bank swap, rollback protection, and anti-rollback counter logic alone will save you weeks of development time. Start with the STM32L5 or U5 Nucleo board, get the secure/non-secure partition working, and layer on the update mechanism once you're confident the boundary is solid.