11

Trusted Execution Environment

An important step in the technological evolution of microcontroller hardware architecture has been recently achieved with the introduction of a domain separation mechanism, which is already present in other architectures, where it is usually referred to as a Trusted Execution Environment, or TEE.

TEE is an abstraction that provides two or more separated execution domains, or “worlds”, with different capabilities and permissions to access devices, resources, and peripherals.

Isolating the execution environment of one or more software components and modules, also generally known as sandboxing, consists of limiting their view of the system, without impacting their performance and normal operation. This is a requirement for many use cases and domains in computer science, and not only for increasing the security of embedded systems.

Similar hardware-assisted isolation mechanisms in other domains are the building blocks of the cloud server infrastructure as we know it today, in the form of virtualization extensions and security isolation mechanisms that allow us to run multiple “guest” virtual machines or containers simultaneously on the same hardware.

The concepts and technologies analyzed in this chapter are as follows:

  • Sandboxing
  • TrustZone-M
  • System resources separation
  • Building and running the example

By the end of this chapter, you will have learned about TEE and how to configure and use TrustZone-M on Cortex-M microcontrollers to obtain two separate execution domains.

Technical requirements

In order to run the proposed example available in this book’s repository, an STM32L552 microcontroller is required. The TrustZone-M technology is only supported by the newest family of ARM Cortex-M microcontrollers. The STM32L552 is a Cortex-M33, fully supporting TrustZone-M, which makes it a convenient and affordable choice for taking our first steps when learning about this technology.

The code files for this chapter are available at https://github.com/PacktPublishing/Embedded-Systems-Architecture-Second-Edition/tree/main/Chapter11.

Sandboxing

Sandboxing is a generic concept in computer security that refers to a set of hardware and software measures that limit the “view” of the system for one or more of its components, to restrict the area of the system affected by accidental malfunctions or purposedly forged malicious attacks and prevent them from spreading across the entire system. Sandboxing can have different forms and implementations, which may or may not leverage specific hardware functionalities to improve safety and effectiveness.

By the term TEE, we refer to those sandbox mechanisms that involve the CPU keeping track of the secure status of the running code at all times, without significantly impacting the performance of the running application. Due to these TEE mechanisms being intrinsically bonded to the CPU design, TEE behavior, management, and communication models in sandboxes differ across heterogeneous platforms and heavily depend on the architecture. Moreover, TEEs can be used for different purposes, often in combination with cryptography to preserve the integrity and authenticity of software through a hardware root of trust.

In 2005, Intel implemented the first virtualization instructions (Intel VT) for x86 processors to run isolated virtual machine code natively (as opposed to emulating the CPU in a dedicated process on the host machine), by providing the hardware-assisted virtualization of the core components (CPU, RAM, and peripherals). Intel CPUs limit the access of the guest virtual machines to the real hardware using an extension of the existing hierarchical protection domains, often simply called rings, already used for kernel/userspace separation.

Virtual machines are not the only use case of TEE on x86 processors. Intel Software Guard Extensions (SGX) is a set of security-related instructions present in many x86 CPUs, protecting specific memory regions, or enclaves, from unauthorized access. While these instructions have been recently removed from consumer Intel CPUs, they are still present in specific microprocessors in the cloud and enterprise hardware segment. SGX can be used for several purposes, such as providing a secure vault to hide secret keys to be used securely by the applications. Originally, however, they were introduced to fulfill the specific task of implementing Digital Rights Management (DRM) on PCs, which would have enforced copyright protection on media and proprietary software content by authorizing access to the protected content only to pre-authorized, signed software applications. In this setup, the adversaries that TEE protects the system against are the final users themselves.

Later on, AMD added vendor-specific architecture extensions to their CPUs, grouped into a technology called Secure Encrypted Virtualization (SEV). In addition to providing a sandbox for running virtual machines managed by a hypervisor, SEV uses hardware-assisted encryption to ensure the confidentiality of the content of single memory pages, and even CPU registers, during execution.

The Intel architectures, however, were not the first ones to introduce CPU-assisted, built-in, secure extensions. ARM started research on trusted computing in the early 2000s and finally announced support for a technology called TrustZone in 2003. Modern ARM microprocessors, such as those in the Cortex-A family, support a technology called TrustZone-A, which implements two separate Secure (S) and Non-Secure (NS) worlds, the latter having a restricted pre-configured view on the actual system, while the former is capable of accessing all the hardware resources directly.

To find the first microcontrollers implementing TEE, we have to look at the recently designed RISC-V architecture. Both microprocessors and microcontrollers within the RISC-V families offer complete sandboxes separate from each other, in both 32-bit and 64-bit architectures that implement “S” or “U” extensions. Each hardware-assisted container provides a subset of the resources available on the system and runs its own firmware.

Finally, the newest family of microcontrollers by ARM, the ARMv8-M family, includes the extensions and the microcode needed to implement isolation between secure and non-secure execution domains, based on the existing and well-oiled TrustZone technology design. This feature is called TrustZone-M, and it is the specific technology that we will be focusing on in more detail later in this chapter. ARMv8-M is a direct evolution of the ARMv7-M family of microcontrollers that have been used as a reference platform in all the previous chapters of this book.

The rest of this chapter will refer exclusively to TrustZone-M and how to configure and develop components in an embedded system, implementing TEE on the ARMv8-M family of microcontrollers. The term TrustZone from now on will refer specifically to the TrustZone-M technology.

TrustZone-M

ARMv7-M cores, such as the Cortex-M0+ and Cortex-M4 microcontrollers, have dominated the embedded market for decades and are still the most popular choice for many embedded system designs. Although there have been a number of changes and additions, the new Cortex-M23 and Cortex-M33 cores, as well as the newer M35P and M55, have inherited and expanded many of the successful features of the Cortex-M0, Cortex-M4, and Cortex-M7 microcontrollers.

In a typical TrustZone use case, multiple actors may be involved in the distinct phases of software development. The owner of a device may provide a base system, already equipped with all the software authorized to run in the secure world. This would still leave the possibility for a system integrator to customize the non-secure part but with a restricted view of the system, which depends on the configuration of the resources allowed by the secure domain. The system integrator in this case receives a system that is partially locked, with TrustZone enabled and flash memory protections in place to protect its integrity. The secure software provided supervises the execution of any custom software in the non-secure domain, while preserving the resources mapped in the secure world and limiting access from the running application. System integrators without authorized access to the secure execution domain can still run privileged or non-privileged software in the non-secure world, thus including operating systems and device drivers that access the interfaces authorized, either directly, or with some assistance from the secure supervisor.

The example associated with this chapter can be compiled and run on the reference platform. This example is based on the bootloader example introduced in Chapter 4, The Boot-Up Procedure. This is due to the similarity of the structure of the TrustZone-based solution that we want to describe, because the software for the two execution domains is shipped into separate binaries. In the TrustZone case, the separation between bootloader code executing in the secure domain and staging an application running in the non-secure world will help us understand the elements and tools used to build, configure, and run the components on a real system.

The next subsection contains a description of the reference platform, and then we will briefly introduce the execution model behind the secure and non-secure domains, which will then bring us to a deeper analysis of the TrustZone-M units and controllers regulating resource separation on a system.

Reference platform

The microcontroller used for reference in the examples is the STM32L552, a Cortex-M33 CPU that can be found on development boards in the convenient Nucleo-144 format. The STM32L5 series of microcontrollers may be considered the closest evolution of the older STM32F4 series, targeting the same slice of the market by combining low-power modes with high performance. For this reason, a microcontroller in this series has been selected in this chapter as a reference platform for the examples provided. Most of the concepts and the components of the TrustZone-M technology that will be described are, however, applicable to all the microcontrollers in the ARMv8-M family available from ST Microelectronics and several other chip manufacturers.

On the STM32L552ZE, the CPU clock can be configured to run at 110 MHz. The microcontroller is equipped with 256 KB of SRAM, divided into two banks SRAM1 and SRAM2 mapped into separate regions. 512 KB of flash memory can be used as one contiguous space or configured as two separate banks. The ST microcontroller provides platform-specific libraries and tools that are not part of the provided examples, which as usual are based on a fresh implementation that begins with an understanding of the documentation. The only exception to this approach in the example we are going to introduce is the use of the STM32 programming command-line interface, STM32_Programmer_CLI, which can be used to display the current value of the programmable option bytes, just by connecting through the ST-Link debugger on board to the PC with a USB cable and running the following:

STM32_Programmer_CLI -c port=swd -ob displ

This tool will be useful to set the option bytes required to turn TrustZone on and off and set up other options for separating areas of flash memory. Values for option bytes are stored in non-volatile memory, and the values will be retained after the board has been powered off.

Important note

Modifying some of the option bytes accessible through a programmer tool may be irreversible and, in some cases, brick your device. Please refer to the reference manual and application notes of your microcontroller before changing any option.

One of the option bytes contains the TZEN flag, which should be disabled per the factory default. Only when TrustZone-M has been configured will we then enable it on the target to upload and run the example. The bootloader part in the secure world will be responsible for setting up the environment for the application, installed as a different binary, and executing it in the non-secure domain. We will then demonstrate the transitions between the two worlds by introducing new ARMv8 assembly instructions introduced for this purpose.

In the next subsection, we will introduce the extensions included in the ARMv8-M architecture for executing code and controlling execution domains. These extensions are generic and included in all the ARMv8-M microcontrollers that support TrustZone, and are the core component for the execution in separate domains.

Secure and non-secure execution domains

In Chapter 10, Parallel Tasks and Scheduling, we learned that resource separation among threads and between threads and the operating system is possible, with the help of memory segmentation. In the ARMv8-m family of microcontrollers, TrustZone-M is often referred to as a security extension because it does, in fact, add one additional level of privilege separation between software components that are running on the target. These security extensions do not replace the existing thread separation we implemented earlier in the safe version of the scheduler. Instead, they introduce an additional security mode on top of the existing separation.

Similar to how an OS running without these extensions enforces a separation between thread mode and privileged mode, and can set boundaries for accessing memory-mapped areas using a MPU, TrustZone-M adds Secure (S) and Non-Secure (NS) execution domains (or “worlds”) with CPU-controlled access to the single resources.

Within each of those worlds, it is still possible to implement privileged/thread separation by using the existing mechanism based on the CONTROL bit. Each security world can have its own privileged and non-privileged execution modes. An OS running in the NS world can still use the classic privilege separation that has been inherited from the previous ARMv7-M architecture. This creates a total of four available execution contexts that can be followed simultaneously by the CPU, summarized in this table as a combination of domain and privilege levels:

Secure world

Non-secure world

Secure privileged execution

Non-secure privileged execution

Secure thread execution

Non-secure thread execution

Table 11.1 – Available execution modes in secure/non-secure domains

As we pointed out in Chapter 10, Parallel Tasks and Scheduling, the Cortex-M4 provides two separate stack pointers (MSP and PSP) to keep track of the different contexts when executing threads or kernel code. In the Cortex-M33, there is a total of four different stack pointers, MSP_S, PSP_S, MSP_NS, and PSP_NS. Each stack pointer is aliased into the actual SP register during execution, depending on the current domain and context.

A very convenient feature has been added to the ARMv8-M architecture when MAIN extensions are present on the CPU, as in our reference platform. Each one of the four stack pointers has a corresponding stack pointer limit (SPLIM) register (called MSPLIM_S, PSPLIM_S, MSPLIM_NS, and PSPLIM_NS respectively). These registers indicate the lower limit for the stack pointer value in the four cases. This is in fact an effective countermeasure to the issues analyzed in the Stack overflows subsection in Chapter 5, Memory Management. The CPU will constantly check at runtime that the stack never grows past its lower limit in memory by generating an exception when this happens. This mechanism provides a better hardware-assisted way to protect memory from accidental stack overflows and collisions with other memory regions than the one proposed in the examples from Chapter 5, Memory Management, where we introduced a guard region between the two memory areas assigned to heap and stack.

We already analyzed how to switch between execution modes, and how setting or clearing the CONTROL bit while returning from system calls plays a role in the transactions between privilege and thread execution modes. The mechanisms for switching between secure and non-secure executions are implemented via specific assembly instructions, which we will explain later after introducing resource separation between secure and non-secure worlds.

To better understand what system resources the software running in the non-secure world may or may not access, the next section will go into detail about the different possibilities provided by the TrustZone-M controller modules to isolate and separate hardware resources.

System resources separation

When TrustZone-M is enabled, all areas mapped in memory, including RAM, peripherals, and even FLASH storage, receive a new security attribute. Besides the secure and non-secure domains, a security attribute may assume a third value, Non-Secure Callable (NSC). This last attribute defines special regions of memory used to implement transactions from the non-secure world to the secure world through a specific mechanism, which will be explained in the last section, Building and running the example. An NSC area is used to offer secure APIs that act like system calls with new powers. The secure domain exposes service routines that can perform specific controlled actions while accessing secure resources from its non-secure counterpart.

Security attributes and memory regions

Cortex-M33 microcontrollers offer various levels of protection. The combination of the effects of those levels determines which of the memory-mapped areas associated with a resource on the system are available to both execution domains and which of them are only accessible from the secure world.

Enabling TrustZone-M will also duplicate the representation of some of the system resources. The flash memory usually mapped from the start of the address 0x08000000 has an alias in the region 0x0C000000, which is used to access the same storage from the secure world. Many system registers are “banked” and have secure and non-secure versions at separate memory locations. For example, the GPIOA controller is mapped to the address 0x42020000 when TrustZone is disabled. When TrustZone is enabled, the same address is used by software running in the non-secure domain if the GPIOA controller is accessible from the non-secure world. However, software running in the secure domain will use the same controller mapped from the start of the address 0x52020000. The same banking applies to many other registers in the peripheral region, which have secure and non-secure versions of the same registers mapped into two separate regions.

Before being processed by other TrustZone-aware components, each memory access is monitored and filtered by two units responsible for configuring the attributes. These are the Security Attribution Unit (SAU) and Implementation-Defined Attribution Unit (IDAU). These units affect the accessibility of the entire memory mapping regardless of the type of resource associated with each region. While the SAU is configurable through a set of registers, the IDAU contains hardcoded mappings enforced by the chip manufacturer. The combination of the attributes of IDAU and SAU influences the accessibility of each memory-mapped region, the following in particular:

  • Regions mapped as secure by IDAU are not influenced by SAU attributes and will always stay mapped as secure
  • Regions mapped as NSC by IDAU can be secure or NSC, based on the SAU attribute
  • Regions mapped as non-secure by IDAU will follow the SAU mapping

The combination of the attributes and the resulting mapping for each region is summarized in the following table:

IDAU attribute

SAU attribute

Resulting attribute

Secure

Secure, NSC, or Non-secure

Secure

NSC

Secure

Secure

Non-secure

Secure

Secure

NSC

NSC or Non-secure

NSC

Non-secure

NSC

NSC

Non-secure

Non-secure

Non-secure

Table 11.2 – A combination of IDAU and SAU attributes

By default, our IDAU in the STM32L552 reference platform enforces the secure/NSC mapping of a few key regions:

  • The flash memory mapping in secure space, starting at the address 0x0C000000
  • The second SRAM bank, SRAM2, mapped from the start of the address 0x30000000
  • The memory between 0x50000000 and 0x5FFFFFFF, reserved for secure peripherals’ control and configuration

The SAU sets all the regions as secure upon reset and is disabled by default. To execute non-secure code, we must define at least two non-secure regions within the intervals allowed by the IDAU configuration.

In our example, we initialize a few memory areas to allow access from the applications, before enabling SAU. SAU is controlled through four main 32-bit registers:

  • SAU_CTRL (SAU control): Used to activate SAU. It contains a flag to “invert” the logic of the SAU filter, by setting all the memory regions as non-secure.
  • SAU_RNR (SAU region number register): Contains the region number to select at the beginning of the configuration procedure for the memory regions. Further writes to SAU_RBAR and SAU_RLAR will refer to this numbered region.
  • SAU_RBAR (SAU region base address register): Indicates the base address of the region that we want to configure.
  • SAU_RLAR (SAU region limit address register): Contains the end address of the region to configure. The least significant 5 bits are reserved for flags. Bit 1, when on, indicates that the region is secure or non-secure callable. Bit 0 enables the region and indicates that its configuration is complete.

In the following example code, you can find the sau_init_region convenience function. Given a region identifier, base address, end address, and secure bit value, it will set all the register values accordingly:

static void sau_init_region(uint32_t region,
    uint32_t start_addr,
    uint32_t end_addr,
    int secure)
{
  uint32_t secure_flag = 0;
  if (secure)
      secure_flag = SAU_REG_SECURE;
  SAU_RNR = region & SAU_REGION_MASK;
  SAU_RBAR = start_addr & SAU_ADDR_MASK;
  SAU_RLAR = (end_addr & SAU_ADDR_MASK)
      | secure_flag | SAU_REG_ENABLE;
}

This function is called by the secure_world_init initialization function to map the four SAU regions that we want to configure for this example, which are, specifically, the following:

  • Region 0: An non-secure callable section, where our secure API that is callable from the non-secure application will be stored. In this example, we expose a single function called nsc_blue_led_toggle, which will be the only way that the application can access an otherwise secure-only GPIO, wired to the blue LED on the Nucleo board.
  • Region 1: An non-secure area in the flash, starting from the address 0x08040000. This is where the code of our non-secure application will reside.
  • Region 2: An non-secure part of the SRAM1 bank that can be used by the non-secure application for the stack and variables. This is a necessary step to ensure the application can access RAM addresses.
  • Region 3: The non-secure peripheral control mapped at the address 0x40000000, including the non-secure GPIO controllers. This area will be accessed by the non-secure application to set the system clock and control the green led in the example.

The code for the SAU initialization in the example is the following:

static void secure_world_init(void)
{
  /* Non-secure callable: NSC functions area */
  sau_init_region(0, 0x0C001000, 0x0C001FFF, 1);
  /* Non-secure: application flash area */
  sau_init_region(1, 0x08040000, 0x0804FFFF, 0);
  /* Non-secure RAM region in SRAM1 */
  sau_init_region(2, 0x20018000, 0x2002FFFF, 0);
  /* Non-secure: internal peripherals */
  sau_init_region(3, 0x40000000, 0x4FFFFFFF, 0);

The code in the tail of this function activates the SAU and enables a specific handler that detects secure faults:

  /* Enable SAU */
  SAU_CTRL = SAU_INIT_CTRL_ENABLE;
  /* Enable securefault handler */
  SCB_SHCSR |= SCB_SHCSR_SECUREFAULT_EN;
}

By default, enabling SAU would mark all regions as secure, so each region configuration trims an non-secure or non-secure callable “window” within the addressable memory space. Region 0 is the only region marked with the NSC flag in our example configuration, which means that NSC code (explained later) will be installed here by the secure application. Regions 1, 2, and 3 are the only memory areas that may be accessed when running in the non-secure domain with TrustZone-M enabled.

As previously mentioned, IDAU/SAU is just the first level of filters for the TrustZone-M protection mechanisms. Flash memory and RAM are protected by additional secure gates, which can be block-based or watermark-based. The STM32L552 microcontroller is equipped with a Global TrustZone Controller (GTZC), which includes one watermark-based gate controller for the flash and one block-based to define secure/non-secure RAM blocks.

Flash memory and secure watermarks

On the target platform, flash memory can be configured to be mapped as a single, contiguous space, or split in half by activating a dual bank configuration. For the sake of our TrustZone-M example, we will keep the flash memory in a single bank.

In this configuration, when TrustZone is enabled, we can assign an non-secure area in the higher half of the contiguous flash memory space, starting at the address 0x08040000. When the flash is divided into two banks, each bank can configure its own independent secure watermark. The flash area in between the start/end addresses is marked as secure, and everything left outside of the marks is non-secure. The secure area in each bank is delimited by the value of the option bytes, SECWMx_PSTRT and SECWMx_PEND. If the delimiters overlap – that is, when the value of SECWMx_PEND is bigger than that of SECWMx_PSTRT – the entire area is marked as non-secure.

Their value can be modified using the programmer tool provided, as shown here:

STM32_Programmer_CLI -c port=swd -ob SECWM1_PSTRT=0
   SECWM1_PEND=0x39

In single-bank mode, each flash sector is 4096 B. By setting these option bytes, we are marking the first 64 sectors (from 0x00 to 0x39) as secure, which leaves the other half of the flash, starting from the address 0x08040000, to be used by the non-secure application in our example. The programmer tool, launched with the -ob displ option, will show the following:

   Secure Area 1:
     SECWM1_PSTRT : 0x0  (0x8000000)
     SECWM1_PEND  : 0x39  (0x8039000)

GTZC configuration and block-based SRAM protection

An additional gate to control access is present in the TrustZone controller on the reference platform. The block-based gate component of the GTZC allows us to configure the secure-only bit to portions of SRAM. SRAM on STM32L552 is divided into two main banks:

  • SRAM1: 192 KB of RAM mapped at the address 0x08000000
  • SRAM2: 64 KB of RAM mapped at the address 0x30000000 and set as NSC in IDAU

In our example, we are marking the higher half of SRAM1, starting at the address 0x2018000, as non-secure. To do so, the GTZC provides two sets of registers, one for each bank, to configure the block-based gate to each page in RAM. Each block represents 25 6B, and each 32-bit register, by holding one secure bit per block, can map 32 pages, also defined as an 8 KB superblock. 24 registers are required to map the 24 superblocks for a total of 192 KB in SRAM1, and only 8 are required to map the 64 KB area in SRAM2.

Like for the SAU initialization, once again the approach taken in the example relies on a convenient macro that, given a memory bank, the superblock number, and its register value, calculates the address for the right register that refers to the superblock and generates the right assignment statement:

#define SET_GTZC_MPCBBx_S_VCTR(bank,n,val) 
(*((volatile uint32_t *)(GTZC_MPCBB##bank##_S_VCTR_BASE )
           + n ))= val

This way we can easily configure block-based gates of contiguous regions within a loop. The secure-world application example uses the following function to configure the block-based gates for the two banks:

 static void gtzc_init(void)
{
   int i;
  /* Configure lower half of SRAM1 as secure */
   for (i = 0; i < 12; i++) {
       SET_GTZC_MPCBBx_S_VCTR(1, i, 0xFFFFFFFF);
   }
   /* Configure upper half of SRAM1 as non-secure */
   for (i = 12; i < 24; i++) {
       SET_GTZC_MPCBBx_S_VCTR(1, i, 0x0);
   }
  /* Configure SRAM2 as secure */
   for (i = 0; i < 8; i++) {
       SET_GTZC_MPCBBx_S_VCTR(2, i, 0xFFFFFFFF);
   }
}

We now have everything required to run the simplest non-secure application on the system; we have defined the non-secure areas in SAU, set the watermark for the separation of the flash memory, and finally, set the block-based gates to enable non-secure access to the higher half of SRAM1.

There is, however, another aspect that deserves attention, and that is the possibility of configuring secure access to peripherals.

Configuring secure access to peripherals

On the reference platforms, peripherals are divided into two categories:

  • Securable peripherals: Peripherals are not directly connected to a local bus, but through a gate system controlled by the TrustZone Secure Controller (TZSC)
  • TrustZone-aware peripherals: These are peripherals that integrate with TrustZone mechanisms – for example, by offering separate interfaces to access their resources, depending on the execution domain

For the first category of peripherals, the configuration of the secure access and privileged access within secure and non-secure domains can be configured through the TZSC registers within the GTZC. At system startup, all devices are set as secure by default, so to enable access to UART, I2C, timers, and other peripherals, it will be necessary to turn off the secure bit associated with the specific controller.

TrustZone-aware peripherals have banked registers for both secure domains. In the next example, we configure three GPIO controllers (GPIOA, GPIOB, and GPIOC), which are connected to the LEDs on the Nucleo-144 board, via pins C7 (the green LED), B7 (the blue LED), and A9 (the red LED). The GPIO controller registers, when TrustZone is enabled, are banked into two regions. You will notice in the example code the difference between the two LED driver interfaces in the secure and non-secure applications. In the secure version of led.h, we define the following address base for the GPIO controller registers:

#define GPIOA_BASE 0x52020000
#define GPIOB_BASE 0x52020400
#define GPIOC_BASE 0x52020800

The same controllers, in the non-secure world application, are mapped in the non-secure peripheral address space:

#define GPIOA_BASE 0x42020000
#define GPIOB_BASE 0x42020400
#define GPIOC_BASE 0x42020800

This ensures that the GPIO configuration is accessible only through the interface assigned to the non-secure space when running in the non-secure domain.

Additionally, each GPIO controller provides an interface to secure each single controlled pin. This is achieved through a write-only register controlling the secure and non-secure access with a flag corresponding to each pin. The register is called GPIOx_SECCFG and is located at an offset of 0x24 in each GPIO controller space. This is only accessible for writing when running in a secure domain.

In the example, we define functions to set/clear the secure bit for each GPIO pin connected to the three LEDs. For example, we can set the secure state of the red LED, before staging the non-secure application to disallow changing the LED state in the application by calling red_led_secure(1), which is implemented as follows:

void red_led_secure(int onoff)
{
  if (onoff)
      GPIOA_SECCFG |= (1 << RED_LED);
  else
      GPIOA_SECCFG &= ~(1 << RED_LED);
}

Our secure-world example application in fact restricts access to the blue and red LED before staging while allowing access to the green LED:

    red_led_secure(1);
    green_led_secure(0);
    blue_led_secure(1);

After the domain switch, the non-secure application will attempt to turn on all three LEDs, but only the green one will actually be turned on, and the other will stay off because access through the non-secure interface is gated by the SECCFG bit set in the secure world and has no effect on the GPIO.

Blinking the blue LED, however, will still be done using a special non-secure callable interface, explained in the Inter-domain transitions subsection in the next section.

After configuring all the securable and TrustZone-aware peripherals, we are finally ready to build and install the firmware images for the two domains and observe their effects on the system.

Building and running the example

Finally, we are putting all we have learned about TrustZone-M into practice, by activating the option flags needed to enable TrustZone-M and running the two software components associated with the execution domains.

Enabling TrustZone-M

By default, TrustZone-M is turned off on our microcontroller when it is in its factory state. Turning on TrustZone is a one-way operation, but it is typically not irreversible unless combined with other hardware-assisted protection mechanisms that make it impossible to disable it when the embedded system is deployed. Disabling TrustZone once enabled, however, requires a more complex procedure than just clearing one bit in a register.

Important note

Please refer to your microcontroller’s reference manual and application notes, and ensure that you understand the procedure and the consequences of enabling or attempting to disable TrustZone-M on your device.

On the reference platform, to enable TrustZone, we set the associated flag in the option bytes via the following command:

STM32_Programmer_CLI -c port=swd mode=hotplug -ob TZEN=1

Once TrustZone has been enabled, we can build and install the secure firmware. The next subsection highlights some important aspects to consider when building the secure part of the system.

Secure application entry point

The regions defined in the secure-world linker script reflect the system resources as seen by the secure firmware. We allocate a RAM region covering the lower half of the SRAM1 bank:

    RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00018000

Our .text and .data LMSes end up in the FLASH region, mapped to its secure-domain address:

    FLASH (rx) : ORIGIN = 0x0C000000, LENGTH = 0x1000

For our simple example, 4 KB is enough to store the bootloader image. Additionally, we define a non-secure callable area, which will contain the implementation of our secure stubs. This is an area dedicated to accessing secure APIs from the non-secure world through pre-defined inter-domain special function calls:

    FLASH_NSC(rx): ORIGIN = 0x0C001000, LENGTH = 0x1000

The entry point of the secure application on the reference platform is hardcoded in the option bytes. Before installing our image, we must ensure that the option bytes for SECBOOTADD0 are configured to point to the address 0x0C000000, which is the beginning of the flash memory in the secure system view. If, for any reason, the value has been previously modified, it can be restored via the following command:

STM32_Programmer_CLI -c port=swd mode=hotplug -ob SECBOOTADD0=0x180000

This is because the granularity of SECBOOTADD0 is 128 bytes, so setting a value of 0x180000 will result in a pointer to the address 0x0C000000.

This last value completes the setup of option bytes, so we are finally ready to build and install the secure application.

A list of the option bytes and their values, assigned in order to configure the target run of the example code, is provided in the repository of this book, in the Chapter11/option-bytes.txt file.

Compiling and linking secure-world applications

If you look in the Makefile for the secure-world application, you will notice two new flags have been introduced in the build process. gcc requires us to use the -mcmse flag to indicate that we are compiling secure code for a TrustZone system. By adding this flag, we are telling the compiler to generate Secure Gateway (SG) stubs for the functions that are marked as non-secure callable. When compiling sources marked as secure, gcc allows specific attributes to mark our secure API calls. In our example, we define a secure API call that will allow non-secure code to toggle the value of the GPIO pin connected to the blue LED. The non-secure callable function is defined in the secure application code; in the case of the example, it is contained in the nsc_led.c file:

void __attribute__((cmse_nonsecure_entry))
    nsc_blue_led_toggle(void)
{
  if ((GPIOB_ODR & (1 << BLUE_LED)) == (1 << BLUE_LED))
    blue_led_off();
  else
    blue_led_on();
}

The __attribute__((cmse_nonsecure_entry)) compiler attribute tells gcc to generate the SG stub for this function. The FLASH_NSC section that we defined in the linker script is used to store the SG stubs for the secure API that we configure. The SG stubs are automatically placed in a section called .gnu.sgstubs, which we place in the FLASH_NSC region in the example linker script:

.gnu.sgstubs :
{
  . = ALIGN(4);
  *(.gnu.sgstubs*)   /* Secure Gateway stubs */
  . = ALIGN(4);
} >FLASH_NSC

The extra linker flags, --cmse-implib and --out-implib=led_cmse.o, have a different purpose that does not directly affect the secure domain. When linking the secure application, by adding these flags we are asking the linker to create a new object file, which will not be linked in the final secure application. This new object file instead will be linked in the non-secure world application and contains the veneers for the secure API. These veneers prepare the jump from non-secure to non-secure callable world. In other words, this new file, led_cmse.o, is the non-secure world counterpart implementation of the secure calls through a non-secure callable SG stub. The veneers are generated by the linker and contain the code needed to jump to the non-secure callable stub. To recap, to build the secure application, we need to introduce two specific set of flags:

  • The–mcmse compile time flag, which tells gcc that we are generating secure code for TrustZone and enables SG stubs for non-secure entry points
  • The–cmse-implib and –out-implib=… linker flags, which tell the linker to generate veneers in object file formats, which in turn will be linked to the non-secure domain to access the associated secure API calls

Once built using make, the secure firmware image can be uploaded to the device flash, using the following command:

STM32_Programmer_CLI -c port=swd -d bootloader.bin 0x0C000000

The microcontroller flash is now populated with the secure firmware, our enhanced bootloader that is ready to set up all the parameters in the TrustZone controller and stage the non-secure application. The obvious next step is to compile and install the non-secure world counterpart.

Compiling and linking non-secure applications

The linker script for our non-secure application defines the boundaries for the world as seen from the non-secure execution domain. Secure and NSC regions are not reachable from here. Our view on the flash memory is restricted to its upper half, and the accessible RAM is limited to the upper half of the SRAM1 bank. The target.ld linker script in the non-secure application defines these regions as follows:

FLASH (rx) : ORIGIN = 0x08040000, LENGTH = 256K
RAM (rwx) :  ORIGIN = 0x20018000, LENGTH = 96K

From this point onward, the build process is similar to building normal applications with no support for TrustZone. Unlike its secure counterpart, non-secure applications do not require any special compiler or linker flags.

The noticeable exception consists of the extra object file generated by the secure application build process, which allows the non-secure application to briefly interact with the secure world. The contract between the secure and non-secure domains consists of the secure API defined by the secure world. In our example, we have only defined one single secure function, nsc_blue_led_toggle. The object file containing the veneers (called cmse_led.o in our example), automatically generated when compiling the code for the secure domain, is linked within the non-secure application, and it is in fact the code that satisfies the symbol dependency in the secure application for these special symbols. We will explore the details of this procedure in the next subsection, Inter-domain transitions.

Once the non-secure application has been built by running make, we upload the non-secure firmware image into the internal flash of the target, starting from the address 0x08040000:

STM32_Programmer_CLI -c port=swd -d image.bin 0x08040000

We will now take a closer look at the transitions from secure to non-secure domains and vice versa, to understand how new ARMv8-M instructions are involved in the transition operations and how these should be used in such cases.

Inter-domain transitions

When our secure world example bootloader is ready to stage the non-secure world application, we can notice some differences in the assembly that prepares the CPU registers and executes the jump to the non-secure domain. First, the VTOR system register is banked when TrustZone is enabled. This means that there are two separate registers that hold the offset for the vector table, one for each execution domain – VTOR_S and VTOR_NS, for secure and non-secure domains, respectively. Before jumping into the entry point for non-secure world code, the VTOR_NS register should contain the offset of the interrupt vector for the non-secure world application. As we know, the IV sits at the beginning of the binary image, so the following assignment in the bootloader’s main procedure ensures that eventually our non-secure domain code will be able to execute interrupt service routines:

  /* Update IV */
  VTOR_NS = ((uint32_t)app_IV);

After this system register is set, we acquire the two important pointers needed for staging, similar to how we would do for bootloaders without TrustZone-M capabilities, like the one proposed in Chapter 4, The Boot-Up Procedure. These pointers, stored in the first two 32-bit words of the non-secure application binary image, are the initial stack pointer and the actual entry point containing the address of the isr_reset handler respectively. We read these two addresses into local stack variables before staging:

  app_end_stack =
     (*((uint32_t *)(NS_WORLD_ENTRY_ADDRESS)));
  app_entry =
     (void *)(*((uint32_t *)(NS_WORLD_ENTRY_ADDRESS + 4)));

In our example, we size the stack area in advance for the non-secure application, calculating the lowest address allowed for the stack as follows:

  app_stack_limit = app_end_stack - MAX_NS_STACK_SIZE;

We then assign this value to the MSPLIM_NS register. MSPLIM_NS is a special register, so as usual we must use the msr instruction:

  asm volatile("msr msplim_ns, %0" ::"r"(app_stack_limit));

We then set the value for the new stack pointer, which will replace SP once the domain transition is complete:

  asm volatile("msr msp_ns, %0" ::"r"(app_end_stack));

The actual jump to non-secure code is where things differ a lot from our previous bootloader introduced in Chapter 4, The Boot-Up Procedure. First, we must ensure that the address for the jump is adjusted to comply with the convention used in ARMv8 transitions. The value we read from the binary image into the app_entry local variable is in fact odd, which is the classic requirement when assigning a new value to the PC register when jumping within the same domain – for example, when using mov pc, ... instructions in ARMv7-M, such as the one in the example bootloader from Chapter 4, The Boot-Up Procedure. In ARMv8-M, the instruction that executes the jump and the domain transition into the non-secure world at the same time, is blxns. However, when invoking blxns or any other instruction that implies a jump to a non-secure address, we must ensure that the destination address for the jump has its least significant bit turned off. For this reason, we decrease the value of app_entry by one before executing blxns:

  /* Jump to non-secure app_entry */
  asm volatile("mov r12, %0" ::"r"
     ((uint32_t)app_entry - 1));
  asm volatile("blxns   r12" );

This is the last instruction executed in the secure domain before finally staging our non-secure application. If we use the debugger to check the values of the registers while stepping through these last instructions, we can see the values of the CPU registers being updated, and then finally, the SP register will point to the new context in the non-secure domain.

From this point onward, any attempt to jump back into the secure domain is, of course, not allowed and will generate an exception. However, as we have previously mentioned, the purpose of the functions placed in the NSC region is to provide temporary and controlled execution of secure functions from the non-secure domain.

In our example, before transitioning to the non-secure execution domain, we impose some limitation on access to the GPIO lines associated with the three LEDs, by setting the corresponding bits in the GPIOx_SECCFG register, as explained in the Configuring secure access to peripherals subsection previously in this chapter.

When both images composing the example are uploaded to the target platform, we can power-cycle and observe the effects by looking at the three LEDs. After rebooting, we should see the red LED that will be turned on at startup and kept on while the secure code is running in the bootloader. After spinning for an arbitrary number of cycles, to give us enough time to inspect the LED status, the red LED will then be turned off and secured. The blue LED is secured too, through the blue_led_secure(1) call executed before staging. The green LED is not secured and can be accessed normally in the non-secure domain.

When the non-secure application starts, we can see the green LED constantly on and the blue LED rapidly blinking. The latter is only possible thanks to the fact that non-secure applications can access a function within the secure APIs.

We can have a look at the assembly generated for this function by running arm-none-eabi-objdump –D on the secure-world elf file. We immediately notice that the non-secure callable function stub generated is in fact a short procedure placed at the beginning of the non-callable section:

0c001000 <nsc_blue_led_toggle>:
c001000:   e97f e97f   sg
c001004:   f7ff bdd2   b.w c000bac
              <__acle_se_nsc_blue_led_toggle>

The most interesting part of the code running in the NSC area is the use of the special assembly instruction, sg, which is a new instruction introduced in ARMv8-M with the specific purpose of implementing secure calls from non-secure domains. This instruction prepares the branching to an secure call in the secure flash space, and it is only legal when it is executed from an non-secure callable area.

Also, note that the real implementation is in fact contained in the __acle_se_nsc_blue_led_toggle function, generated by the compiler and placed in the S region of the flash.

The assembly code generated by the veneer for nsc_blue_led_toggle, as seen by disassembling the non-secure application in the same way after including the generated object in the final image, should look like the following:

080408e8 <__nsc_blue_led_toggle_veneer>:
80408e8:   f85f f000   ldr.w   pc, [pc]    ; 80408ec 
                       <__nsc_blue_led_toggle_veneer+0x4>
80408ec:   0c001001

The conclusion of the procedure of calling an secure function from the non-secure domain is in the tail of the actual implementation of the secure function toggling the blue LED, __acle_se_nsc_blue_led_toggle:

c000bee:   4774        bxns    lr

This should already look familiar to us, as it is in fact the non-linked version of the bxlns instruction that we have seen before, performing a jump to the return address of the non-secure veneer stored in the link register, while also transitioning back to the non-secure domain. The following checklist is a recap of the steps involved when a secure function is called from the NS execution domain in this chapter’s example:

  1. non-secure world code calls the veneer for nsc_blue_led_toggle, which is implemented in the cmse_leds.o object that is generated when compiling the secure code and linked to the non-secure application.
  2. The veneer knows the SG stub location in the NSC region. This region is accessible for execution from the secure world, while being placed in a specific region inside the secure firmware. The veneer then proceeds to jump to the SG stub.
  3. The SG stub calls the sg instruction, initiating the transition to the secure world, and then jumps to the actual implementation, __acle_se_nsc_blue_led_toggle. This now executes in the secure domain, performing the requested action (in our example, this is toggling the value of the GPIO line connected to the blue LED).
  4. When the procedure terminates, the secure function performs a transition back to the non-secure world by using the bxns instruction, while at the same time jumping back to the address of the original caller in the non-secure world.

Despite its simplicity, our example shows how to configure and use all the features needed to separate the two execution domains, as well as the mechanisms to be used to implement the interactions between the two worlds. The design of these interactions in the secure domain will determine the capabilities offered to non-secure applications. The boundaries and the interface for the transitions act like a contract between the two parts, which is enforced by the hardware itself, thanks to TrustZone-M.

Summary

ARMv8-M is the newest architecture defined by ARM for modern microcontrollers. It extends and completes the capabilities of its predecessor, ARMv7-M, by integrating several new features. The most important improvement for this novel architecture design is the possibility to implement a TEE by separating the execution domains and creating a sandboxed environment to execute non-secure applications.

In real-life scenarios, this gives flexibility to the deployment of applications from different providers, with distinct levels of trust regarding accessing features and resources on a system.

In this last chapter, we have analyzed the mechanisms available in the TrustZone-M technology. TrustZone-M can be activated on ARMv8-M systems for the purpose of integrating a powerful, hardware-assisted solution, aimed to protect system components from any access that has not been explicitly authorized by a system supervisor component running in the secure domain.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset