Concepts behind the ARMv8-M TrustZone

Using GCC CMSE (Cortex-M Security Extensions) with ARM Cortex-M33 and M23 Cores

Introduction & Motivation

This article is the result of my first steps with the new nRF9160 System-in-Package of Nordic Semiconductor. This SiP can be used to create IoT sensor nodes with cellular connection to the internet. It integrates a modem for the new “IoT optimized” low-power variants of the LTE standard namely NB.IoT  and LTE-M which is targeted to replace traditional M2M connectivity using 2G networks.

Conceptual the SiP is very interesting to us at Lobaro because it breaks with the widespread concept of having one application microprocessor talking over serial line using AT commands with a separate cellular modem. Beside this it uses the latest ARM Cortex-M33 CPU core with new security features targeting especially secure IoT applications. The LTE modem is directly integrated into the SiP and communicating with the application processor using interprocessor communication based on shared memory. This tight coupling allows for better low power optimization and smaller PCB footprints beside easier securing the IoT node.

The Cortex-M33 processor inside the nRF9160 uses the new ARMv8-M architecture which offers a new feature called “ARM TrustZone“. The following article reflects my interpretation of the underlying concepts and their practical application using the GNU ARM GCC compiler and its CMSE (=Cortex M Security Extensions) Features. Since there is currently (May 2019) not a lot of information to find online I hope this article may help you to have a quicker start into the topic than I had.

Although I am using the nRF9160 Cortex-M33 SiP most concepts provided in the following are also valid for other ARMv8-M enabled Cortex-M33 and Cortex-M23 devices of different silicon vendors. The same holds for using other ARM compilers than GCC.

Cortex-M33 core, (c) ARM®

Secure & Non Secure CPU Operating States

Additional to the classical Thread (normal Code) and Handler (IRQ/Exception code) CPU operating states the ARMv8-M architecture introduces secure and non-secure attributes. After reset the CPU always starts in secure mode and can access all memory regions and use any peripheral without restrictions. This is like the normal operating mode of older architectures like the Cortex-M4 or Cortex-M3. Your ARMv8-M based CPU may stay in this mode without using the TrustZone features but then the creation of secure applications becomes harder. You have to double check on every piece of your code, because if there is a bug an (remote) attacker may gain access to your complete system and / or stored credentials.

The idea of the TrustZone is to hide security relevant code, data and peripherals in a small region of the device. This region will be called “secure” whereas the other parts of the device will be treated as (potentially) “non-secure”. The partition of memory regions, interrupts and peripherals into secure and non-secure is up to you. Normally your first lines of code after power-on will perform this decisions based on your application needs. In the “Secure Partition Manager” code example for the nRF9160 Nordic decided to use a flash memory partition of 256kB secure and 768kB non-secure.

The CPU core maintains two banked versions (secure / non-secure) of execution relevant registers e.g. the stack pointers and the system control block (SCB). The current CPU security mode defines which register variant will be used. The banking mechanism is similar present for some peripherals (e.g. SysTick) which are implemented twice in hardware. The secure program can always access both versions. After setting up the non-secure stack pointer and the location of the non-secure vector address the secure boot code uses a function pointer to the vector table of the non-secure firmware. From this point the CPU runs the non-secure firmware.

The non-secure firmware must not be aware of the existence of the secure firmware. It may be “normally” developed and deployed like on older cores with its linker-script pointing only to the non-secure memory regions defined. After jumping in the non secure firmware the CPU runs in non-secure mode and can not directly access any secure code, data or peripherals anymore without triggering a security exception. The only valid way that persists to access secure functions from non-secure code is the invocation of special gateway / entry / veneer functions the secure side may provide. The rest of this article demonstrates how this works in detail using the ARM GCC and its implementation of the CMSE (Cortex M Security-Extensions).

Non-secure callable (NSC) memory regions and the secure gateway (SG) ASM instruction

To allow non-secure -> secure function calls some (tiny) part of the secure flash region must be configured as “Non-secure callable” (NSC) in the secure firmware code. On the nRF9160 this is done by using the SPU peripheral.

As described before non-secure code can not call functions in the secure memory domain without rising an exception in the ARMv8-M core. By marking a small part of the secure memory as NSC this restriction is lowered in the way that calls / branches into locations containing the “SG” (Secure Gateway) assembly instruction as first opcode are now allowed. From the point after the SG instruction the program will take another branch into the secure functions implementation lying in the more restrict secure memory, e.g. not marked as NSC. Calling a secure function from non-secure code like this is a two step process with chained jumps first into NSC marked memory (with SG instruction) and then moving on with a jump into the actual function body in secure memory. By using this process the entries to the secure memory are separated from the rest of the secure memory. The reason not to use the SG instruction directly at the beginning of functions in the secure memory and avoiding the two step process is explained by the ARM technical support here: Reasons for introducing the NSC regions.

Defining non-secure callable functions in secure firmware

Luckily ARMv8-M compilers hide the previous described two step process from the programmer when calling a secure function from the non-secure firmware. A secure function designed as entry point for non-secure firmware has to be marked with the nonsecure entry attribute after its declaration in the secure firmware:

// some c file of secure firmware project defining veneer gateway functions 
// must compiled with -mcmse gcc flag (!) 
#include "arm_cmse.h" 
__attribute__((cmse_nonsecure_entry)) void ControlCriticalIO(){ 
// do some critical things 
}

This instructs the compiler to automatically generate two code parts for the above described process if compiling with the “-mcmse” ARM gcc flag.

// GCC COMPILER flags used during secure side building
arm-none-eabi-gcc -o secureFirmware.elf -mcmse [...]

The actual function body will be placed by the linker in the secure .text section as usual, but the part with the SG and branch instructions will be placed in a special section called “.gnu.sgstubs” or similar for non GNU C compilers.

The small redirect functions in the NSC region are also called “veneer” functions or “SG stups”, because they are so small doing only one SG instruction and a branch. The linker file of the secure firmware must place any veneer code in the secure memory region which the secure firmware will declare as NSC as part of its secure boot process before jumping in the non-secure firmware.

// Linkerscript Section for TrustZone Secure Gateway veneers
.gnu.sgstubs : ALIGN (32)
{
    . = ALIGN(32);
    _start_sg = .;
    *(.gnu.sgstubs*)
    . = ALIGN(32);
    _end_sg = .;
} > FLASH-REGION-WITH-NSC-ENABLED

Calling secure functions from non-secure firmware (NS->S)

Normally the secure firmware will be developed and flashed independently of the non-secure firmware image. To use any functionality of the secure firmware a header file (.h) describing the callable secure side functions must be included in the non-secure sources. Beside this the non-secure firmware has to know where the veneer entry point stubs have been placed before in the NSC parts of the secure memory. This is done by linking a special object file (e.g. CMSE_importLib.o) to the non-secure firmware containing the needed address information. This object file must have been generated during the secure firmware linking process using the following cmse linker options:

// GCC LINKER flags used during SECURE side linking
arm-none-eabi-gcc -Xlinker --cmse-implib -Xlinker --out-implib=CMSE_importLib.o -Xlinker --sort-section=alignment [...]

The optional “–in-implib=CMSE_importLib.o” may be used to keep entries already defined in older versions of the object file at the exact same memory location also in the new version. This enables additions to the secure image without the need of updating the non-secure image as well (which may have been linked against an older version of the import lib before).

Using the .h file and linking against CMSE_importLib.o object the non-secure firmware can now invoke the ControlCriticalIO() function from the example above.

// GCC LINKER flags used during non-secure side linking
arm-none-eabi-gcc -o non-secureFirmware.elf CMSE_importLib.o [...]

Calling non-secure (callback) functions from secure firmware (S->NS)

This article demonstrated how ARM TrustZone can be used to create two isolated firmware parts connected by well defined tiny gateway / veneer functions. These allow functions of the secure firmware to be called from the non-secure firmware.

The other direction, meaning the secure firmware calling a non-secure function, works out of the box, if the non-secure function address is somehow given to secure code. Normally this is done by defining secure gateway functions with a non-secure callback pointer as parameter:

// some c file of secure firmware project defining veneer gateway functions
// must compiled with -mcmse gcc flag (!)
#include "arm_cmse.h"
typedef void (*funcptr_ns) (void) __attribute__((cmse_nonsecure_call));
void ControlCriticalIO(funcptr_ns callback_fn) __attribute__((cmse_nonsecure_entry)){ 
 funcptr_ns cb = callback_fn; // save volatile pointer from non-secure code 
 
 // check if given pointer to non-secure memory is actually non-secure as expected
 cb = cmse_check_address_range(cb, sizeof(cb), CMSE_NONSECURE);
  
 if (cb != 0) {
    /* do some critical things e.g. use other secure functions */
    cb(); // invoke non-secure call back function
 }else {
   // do nothing if pointer is incorrect
 }
}

The “nonsecure_call” attribute is needed to instruct the secure compiler to clear the non banked general purpose registers before jumping back in the non-secure code since not doing so would be a potential security risk. Beside this it instructs the compiler to clear the LSB of the function-pointers address in the underlying branch assembler instruction. By doing so the CPU will transit from secure to non-secure state within the branch.

Because the given non-secure callback has to be treated like a volatile variable, which content may be changed by non-secure interrupts, the function-pointer copy “cb” is needed. Before invocation of the callback its pointer has to be checked for pointing to non-secure memory only. If it would point to secure memory it would be a security risk too.

Cortex-M33 core register, (c) ARM®

Hints and further reading

Keep in mind that the linkerscript for the secure side must consider the section for the veneer functions and the linkerscript for the non secure side must reflect the memory partitions made in the secure firmware. If including non-secure callable functions in an archive (*.a) file it must linked with the “whole-archive” linker option to the secure firmware. Only this way the CMSE import lib will include the needed veneer functions.

A few intricacies of writing ARMv8-M Secure code – ARM Limited
SAM-L11-Security-ReferenceGuide-AN-DS70005365A.pdf – Microchip Technology
Trustzone with ARMv8-m and the NXP lpc55s69-evk – Erich Styger
CM33 freeRTOS Example

written by Theodor Rohde (CEO of Lobaro GmbH) on May 4th 2019.

4 Kommentare
  1. Daniel Oliveira sagte:

    Hi Theodor,

    Thank you for this post and the detailed explanation on Armv8-M architecture with security extensions. Btw, do you have any gcc-based projects on github related to this subject?

    Greetings,
    Daniel

  2. Tobias sagte:

    Hello Daniel,
    we plan to release a minimal example for the nRF9160 using GCC CMSE extensions and FreeRTOS. This is planned to be as alternative environment for the SiP. I think this will take aprox. 1 month from now.

    Greetings
    Theo

  3. Neculai Agavriloaie sagte:

    Hi Theodor,

    After about 20 years of projecting with MSP430, I wanted to make a change.
    In my new project, I want to use Trustzone technology and I really needed to understand how it works.

    This page is the only place where the mode of operation is very simple and clearly described.

    Greetings,
    Neculai

Kommentare sind deaktiviert.