TrustZone with ARMv8-M and the NXP LPC55S69-EVK

The ARM TrustZone is an optional secu=rity feature for Cortex-M33 which shall improve the security for embedded applications running on microcontroller as the NXP LPC55S69 (dual-core M33) on the LPC55S69-EVK.

NXP LPC55S69-EVK Board

NXP LPC55S69-EVK Board

As with anything, using and learning the TrustZone feature takes some time. ARM provides documentation on TrustZone, but it is not easy to apply it for an actual board or toolchain. The NXP MCUXpresso SDK comes with three examples for TrustZone on the LPC55S69-EVK, so I have investigated these examples to find out how it works and how I can use it in my application.

Software and Tools

I’m using the same setup as in my earlier article (“First Steps with the LPC55S69-EVK (Dual-Core ARM Cortex-M33 with Trustzone)“):

  • Windows 10 with MCUXpresso IDE 10.3.1 (Eclipse based with GNU toolchain for ARM Embedded)
  • MCUXpresso SDK V2.51. for LPC55S69

Most of the things presented in this article are applicable to any other Cortex-M33 environment with TrustZone.

TrustZone on ARMv8-M

As on the in the ARMv7-M, there is two basic modes the processor can be in:

  • Thread Mode: this mode is entered by reset or the usual mode in which the application runs. Code in Thread Mode can be executed in privileged (full access) or non-privileged (no restrictions imposed e.g. by an MPU (Memory Protection Unit)).
  • Interrupt or Handler Mode: this mode is executed with privileged level and this is where the interrupts are running.

TrustZone keeps that model and extends it. The basic concept of TrustZone on ARMv8-M is to separate the ‘untrusted’ from the ‘trusted’ parts on a microcontroller. With this division IP inside the trusted side can be protected while still allowing ‘untrusted’ software to run on the ‘untrusted’ side of the world. Each trusted and untrusted part can have different privileges, such as some hardware (GPIO ports, etc) only could be accessible from the trusted side, but not from the untrusted one.

💡 I recommend to read the ARM document about TrustZone.

Secure and Non-Secure World

Secure and Non-Secure World

While without TrustZone it is already possible to restrict memory access with an MPU, the TrustZone concept with ‘secure world’ and ‘non-secure world’ extends the concept to ‘secure’ or ‘trusted’ hardware or peripheral access. A non-secure function only can access secure hardware through an API which verifies if it is allowed to access the hardware through the secure world. So there are ways that the secure and non-secure parts can work together.

Similar to using an MPU, it means that there are several things to consider:

  • Setting security permissions for memory areas and accessing peripherals
  • Using secure and non-secure API and transfer functions
  • Ability to protect the secure world from debugging or memory read-out (reverse engineering)

MPU

The other important change in the ARMv8-M architecture that the size of an MPU region has now a granularity of 32 bytes. In ARMv7-M the size had to be a 2^N which I never understood and makes it not usable at all in real world applications (this is probably the reason the MPU is rarely used?).

SAU and IDAU

Because this all cannot be only implemented in the core (provided by ARM), there are extra settings needed on the implementation side by the vendor implementing the ARM core.

  • Secure Attribution Unit (SAU): this is inside the core/processor
  • Implementation Defined Attribution Unit (IDAU): this one is outside the processor

The SAU and IDAU work together and are used to grant/deny access to the system (peripherals, memory). Using the SAU+IDAU, the memory space gets separated into three kind:

  • Secure: Code, stack, data, … of the secure world
  • Not-Secure: Code, stack, data, … of the non-secure world
  • Non-Secure Callable: Entry to secure code with a secure gateway vector table

The important (and somewhat confusing) thing is that the SAU settings are first, and IDAU is used to make things ‘unsecure’:

  • SAU(secure) + IDAU(not secure) => secure
  • SAU(not secure) + IDAU(not secure) => not secure
  • SAU(non-secure callable) + IDAU(not secure) ==>  non-secure callable

Or in other words: by default things are secure, and with the IDAU the security level is set to a lower one.

Projects

Time to have a look at an example! The NXP MCUXpresso SDK already comes with an example showing how to call the non-secure land from the secure one. From the ‘Import SDK example(s) I can select examples demonstrating the TrustZone.

TrustZone Examples

TrustZone Examples

The ‘hello_world’ TrustZone example executes some code on the secure side and finally passes control to the non-secure side to execute the non-secure application. The example follows the pattern of a secure bootloader then calling the non-secure application to start.

I have tweaked and replicated the projects discussed in this article, you can find them on GitHub: https://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/MCUXpresso/LPC55S69-EVK

The ‘ns’ (non-secure) and ‘s’ secure projects work together. Using secure and non-secure application parts do not make things simpler, and there seems not to be a lot of documentation about this topic. So I investigated that ‘hello world’ example to better understand how it works.

I have configured both to use the newlib (nano) semihost library:

Semihost settings

Semihost settings

For both project, set the SDK Debug Console to ‘Semihost console’:

Setting Semihost Console

Setting Semihost Console

I have both the secure and non-secure projects configured for using the semihost console, but a real UART could be used too.

Both projects are configured to use the Cortex-M33 (this is a setting in the compiler and Linker):

M33 Architecture

M33 Architecture setting

Non-Secure Side

The non-secure project is configured in the compiler and linker settings as ‘Non-Secure’:

TrustZone Project Settings

TrustZone Project Settings

There is a setting to prevent debugging:

Prevent Debugging

Prevent Debugging

The non-secure application links in an object file which is part of the secure application:

Linking CMSE Lib Object File

Linking CMSE Lib Object File

💡 This means that the ‘secure’ project has to be built first.

This is for the ‘secure gateway library’ which is built in the secure project using the –cmse-implib and –out-implib linker commands:

From https://sourceware.org/binutils/docs/ld/ARM.html:

The ‘–cmse-implib’ option requests that the import libraries specified by the ‘–out-implib’ and ‘–in-implib’ options are secure gateway import libraries, suitable for linking a non-secure executable against secure code as per ARMv8-M Security Extensions.

Secure gateway library linker command

Secure gateway library linker command

The ‘hello_world_ns’ program is linked to address 0x10000: the vector table and code gets placed at this address:

Non-Secure Memory Settings

Non-Secure Memory Settings

Secure Application

On the secure side the compiler and linker settings for TrustZone are set to ‘secure’:

Secure Linker and Compiler Settings

Secure Linker and Compiler Settings

The program and vector table is loaded at 0x1000’0000 with a ‘veneer’ table loaded at 0x1000’fe00. More about this later…

Secure Memory Allocation

Secure Memory Allocation

Debug

The non-secure application can be flashed to the device like this:

Program to Flash

Program to Flash

This basically is as if the new (non-secure) application has been programmed using a bootloader or similar way to update the application.

To be able to debug the second (non-secure) from the secure application, I have to load the symbols for it in the debugger. The secure one can now be debugged as usual:

Debug secure application

Debug secure application

In order to debug the non-secure application code when debugging the secure one, I have to add the symbols to the debugger. I can do this by editing the debug/launch configuration. Double-click on the .launch file or open the debug configuration with Run > Debug Configurations, then use the ‘Edit Scripts’ in the Debugger tab:

Edit Scripts

Edit Scripts

Add the following to load the symbols of the other project using the add-symbol-file gdb command. Adapt the path as needed, I have the other project at the same directory level.

add-symbol-file ../LPC55S69_hello_world_ns/Debug/LPC55S69_hello_world_ns.axf 0x10000

to tell the debugger that the symbols of that application are loaded at the address 0x10000. Insert that after the ${load} command:

Adding Symbols after Load

Running the application produces the following output:

Console Output

Console Output

Security State Transitions

The ARMv8-M architecture has added instructions to transition between the security states. For example the BLXNX instruction is used to call a non-secure function from the secure world:

Security State Transition

Security State Transition (Source: ARM, Trustzone technology for ARMv8-M Architecture)

Calling a Non-Secure Function from the Secure World

The main() of the secure application is like below. It could be the base of a bootloader which jumps to the non-secure loaded application at address 0x1’0000:

 
#define NON_SECURE_START          0x00010000

/* typedef for non-secure callback functions */
typedef void (*funcptr_ns) (void) __attribute__((cmse_nonsecure_call));

int main(void)
{
    funcptr_ns ResetHandler_ns;

    /* Init board hardware. */
    /* attach main clock divide to FLEXCOMM0 (debug console) */
    CLOCK_AttachClk(BOARD_DEBUG_UART_CLK_ATTACH);

    BOARD_InitPins();
    BOARD_BootClockFROHF96M();
    BOARD_InitDebugConsole();

    PRINTF("Hello from secure world!\r\n");
 
    /* Set non-secure main stack (MSP_NS) */
    __TZ_set_MSP_NS(*((uint32_t *)(NON_SECURE_START)));
 
    /* Set non-secure vector table */
    SCB_NS->VTOR = NON_SECURE_START;
    
    /* Get non-secure reset handler */
    ResetHandler_ns = (funcptr_ns)(*((uint32_t *)((NON_SECURE_START) + 4U)));
     
    /* Call non-secure application */
    PRINTF("Entering normal world.\r\n");
    /* Jump to normal world */
    ResetHandler_ns();
    while (1)
    {
        /* This point should never be reached */
    }
}

The line with

__TZ_set_MSP_NS(*((uint32_t *)(NON_SECURE_START)));

loads the non-secure MSP (Main Stack Pointer). The debugger nicely shows both the secure and non-secure registers which are ‘banked’:

non-secure MSP

non-secure MSP

The following will call the non-secure world from the secure one:

ResetHandler_ns();

This a function pointer with the cmse_nonsecure_call attribute:

 
/* typedef for non-secure callback functions */
typedef void (*funcptr_ns) (void) __attribute__((cmse_nonsecure_call));

Non-Secure functions can only be called from the secure world using function pointers, as a result dividing the secure from t

Behind that function call there are several assembly instructions executed. It clears the LSB of the function address and clears the FPU Single Precision registers, or any registers which could contain ‘secret’ information. At the end it calls the library function __gnu_cmse_nonsecure_call:

non-secure call sequence

non-secure call sequence

The __gnu_cmse_nonsecure_call does push the registers and does more register cleaning and uses the BLXNS assembly instruction to finally enter the non-secure world:

__gnu_cmse_nonsecure_call

__gnu_cmse_nonsecure_call

So there are quite a few instructions to be executed to make that transition.

Calling the Secure World from the non-secure World

Calling a secure function from the non-secure side uses an intermediate step (Non-secure Callable):

armv8_m_architecture_trustzone_technology_100690_0101_00_en

Calling secure Function from non-secure side (Source: ARM, Trustzone technology for ARMv8-M Architecture)

In the example the non-secure world is calling a printf function (DbgConsole_Printf_NSE) which is located in the secure world:

Calling printf from the non-secure world

The secure functions which are callable from the non-secure world hae to be marked with the cmse_nonsecure_entry attribute:

💡 CMSE stands for Cortex-M (ARMv8-M) Security Extension

Function with cmse_nonsecure_entry attribute

Function with cmse_nonsecure_entry attribute

So how does the non-secure world know how to call this function? The answer is that the linker prepares everything to make it possible. For this the non-secure application has to link an object file (or ‘library’) with the ‘veneer’ functions:

Linking CMSE Lib Object File

Linking CMSE Lib Object File

This object file (or library) is created with the following linker setting on the secure side:

--cmse-implib --out-implib=hello_world_s_CMSE_lib.o
Secure gateway library linker command

Secure gateway library linker command

So let’s follow the code from the non-secure to the secure world: The assembly calls a ‘veneer’ function:

calling printf veneer

calling printf veneer

The veneer is a simply ‘trampoline’ function which loads the address for the ‘non-secure callable’ and does a BX to that address:

BX to non-secure callable

BX to non-secure callable

The ‘secure non-callable’ area is in the ‘secure world’ with a SG instruction as the first one to be executed, followed by a branch.

SG Instruction in non-secure callable region

SG Instruction in non-secure callable region

The SG (Secure Gateway) instruction switches to the secure state followed by the B (Branch) instruction to the secure function itself:

Executing Secure Function

Executing Secure Function

Compared to calling the unsecure side from the secure world this was rather fast. The clearing of all the registers because they can contain secret information is done just before the BXNS returns to the non-secure state:

Clearing registers on return to non-secure state

Clearing registers on return to non-secure state

SAU Setup

So how is the protection configured? For this the SAU (Secure Attribution Unit) is configured which only can be done on the secure side.

The example uses the following secure and non-secure code and data areas:

#define CODE_FLASH_START_NS         0x00010000  
#define CODE_FLASH_SIZE_NS          0x00062000
#define CODE_FLASH_START_NSC        0x1000FE00
#define CODE_FLASH_SIZE_NSC         0x200
#define DATA_RAM_START_NS           0x20008000
#define DATA_RAM_SIZE_NS            0x0002B000

In the example this is configured in BOARD_InitTrustZone(). The following setting configures a region for the non-secure FLASH execution:

 
    /* Configure SAU region 0 - Non-secure FLASH for CODE execution*/
    /* Set SAU region number */
    SAU->RNR = 0;
    /* Region base address */   
    SAU->RBAR = (CODE_FLASH_START_NS & SAU_RBAR_BADDR_Msk);
    /* Region end address */
    SAU->RLAR = ((CODE_FLASH_START_NS + CODE_FLASH_SIZE_NS-1) & SAU_RLAR_LADDR_Msk) | 
                 /* Region memory attribute index */
                 ((0U >> SAU_RLAR_NSC_Pos) & SAU_RLAR_NSC_Msk) |
                 /* Enable region */
                 ((1U >> SAU_RLAR_ENABLE_Pos) & SAU_RLAR_ENABLE_Msk); 

The IDAU (Implementation Defined Attribution Unit) is optional and is intended to provide a default access memory map (secure, non-secure and non-secure-callable) which can be overwritten by the SAU.

Summary

It probably will take me some more time to understand the details of the ARMv8-M security extensions.There are more details to explore such as secure peripheral access or how to protect memory areas. In a nutshell, it allows to partition the device into ‘secure’/trusted and ‘unsecure’/not-trusted and divides the memory map into secure, non-secure and non-secure-callable with the addition of MPU and controlled access to peripherals. Plus there is the ability to control the level of debugging to prevent reverse engineering.

With the NXP MCUXpresso SDK and IDE plus the LPC55S69 board I have a working environment I can use for my experiments. I like the approach that basically the non-secure application does need to know about the fact that it is running in a secure environment, unless it wants to call functions of the secure world.

I have now FreeRTOS working on the LPC55xx with the FreeRTOS port for M33, but I’m using it in the ‘non-secure’ world. My goal is to get the RTOS running on the secure side. Not sure yet how exactly this will look like, but that’s a good use case I want to explore in the next week if time permits.

Happy Securing 🙂

Links

25 thoughts on “TrustZone with ARMv8-M and the NXP LPC55S69-EVK

  1. Hi Erich,

    I am running some examples from the SDK_2.5.1_LPCXpresso55S69_GCCARMEmbedded for the LPC55S69-EVK related to trustzone (Secure Faults example). For each example I created my own Makefile with only the strictly necessary source files (.c, .h, .ld, .S) – which I accomplish by exploring the CMAKE build mechanism (dependencies, compiler flags, etc) provided for each example. I am able to compile the trustzone-based examples without running to any issue, and I already tested them on the board by flashing through a J-link probe and a gdb session.

    My next step was to create a makefile-based project on MCUXpressoIDE, since I wanted to use the debugger interface provided by the tool (I followed your own tutorial https://mcuoneclipse.com/2017/07/22/tutorial-makefile-projects-with-eclipse/). For both secure and non-secure applications I was able once again to build them correctly using my makefile (no issues to link the secure gateway library generated during the build of the secure app).

    After this process, I run into a issue while trying to initiate a debug session (I am using the on-board debugger). I simply flash the NS app (as exemplified on this post) using the LinkServer and once I try to debug the secure app (which I already loaded with the non-secure symbols) I run to an error while the probe is trying to flash:

    “Target error from Commit Flash write: Em(12). Target rejected debug access at location 0x1000A000
    GDB stub (crt_emu_cm_redlink) terminating – GDB protocol problem: Pipe has been closed by GDB.”

    I should mentioned that the memory on project settings was the layout of the example provided in the SDK which is intended for the toolchain MCUXpresso IDE (SDK_2.5.1_LPCXpresso55S69_MCUXpressoIDE):

    Do you have any idea what step or mistake can I be doing? I understand that this maybe an out-of-scope question in the context of this post.

    Makefile (secure): https://codeshare.io/5eLVyr
    Makefile (non-secure): https://codeshare.io/adlwrR
    Linker Script (secure): https://codeshare.io/anMzWj
    Linker Script (non-secure): https://codeshare.io/G8opjd

    Like

      • Hi Erich,

        After reading this post https://mcuoneclipse.com/2019/05/18/internal-and-external-debug-options-for-the-nxp-lpc55s69-evk-board/, I tried to flash the on-board LPC-Link2 debug probe as a SEGGER J-Link. Since I didn’t had any success with my previous problem, I reprogram the board back to the LPC-Link2/CMSIS-DAP firmware. Which I observed is that now the Link2 boot LED keeps blinking, which according to the LCP55S69-EVK User-Guide reflects that the boot process is failing.

        “LED DS2 is the Link2 MCU BOOT0_LED indicator. This LED reflects the state of Link2 MCU pin P1_1. When the boot process fails, DS2 will toggle at a 1 Hz rate for 60 seconds. After 60 seconds, the Link2 MCU is reset.”

        I used the LPCScrypt_2.1.0_842 and batch file “Program LPC-Link2 with CMSIS-DAP” and the output showed a successful process:

        “Booting LPCScrypt target with “LPCScrypt_227.bin.hdr” LPCScrypt target booted.
        Programming LPCXpresso V2/V3 with “LPC432x_IAP_CMSIS_DAP_V5_224.bin”
        – LPCXpresso V2/V3 programmed successfully and has the unique ID: RA4BQIQ
        – To use: remove DFU link and reboot.
        Connect Next Board then press Space (or CTRL-C to Quit)”

        Like

        • I don’t have access to a board right now, so nothing I could directly check. But could you use an external debug probe, e.g. a LPC-Link2, a P&E or SEGGER one? Very often if the onboard one does not work, it is possible to talk to the device with an external probe.

          Like

  2. Hello Erich,

    Thank you for the great tutorial. I am a real fan of what you have done over the years.
    I have tried the instructions provided in this tutorial but somehow when I try to debug the application, the debugger seems to go in a lockup of some kind. I was able to run a full debug session of your earlier tutorial using the Binky led (LPC55S69) project and all was fine then. I also cloned your github project and once again it all worked well with the secure and non-secure code execution. I am not sure about what is happening here. There might be a missing step in the project’s setting? currently, no messages get printed on the console, and gdb does not stop on any breakpoints that I have set (even the secure reset handler).

    Kindly,

    David.

    Like

    • Hi David,
      In any case, I recommend using an external debug probe if things are failing. Are you powering the board properly (there is a dedicated USB port for power)?
      The other thing might be that the program is stuck between reset handler and main(). Disable ‘run to main’ in the launch configuration so you can step through the startup code. If this does not help, try to erase the chip using the flash programmer icon in the toolbar.

      Like

      • Thank you for your prompt response Erich.

        I haven’t tried your suggestions yet. For the moment, the projects (hello_world_ns/s) that I have imported from your github repository work just fine. I can single step through the secure and non-secure code. I seem to have difficulties following the steps on your post. I’ll try your instructions once again to see if I missed something.

        You mentioned you made a couple of tweaks to the files imported from the SDK sample project. Is there anything significant? I think I have noticed a couple of variations on the debug launch configuration files.

        Kindly,

        David.

        Like

  3. Thanks for the information Daniel, Erich. I think I am edging closer to a result.
    One place where I got confused is when I added the line: add-symbol-file ../LPC55S69_hello_world_ns/Debug/LPC55S69_hello_world_ns.axf 0x10000 to the script.
    I had done that in the hello_world_ns debug configuration. I think what mislead me is the screenshot: “- Debug secure application” that showed the hello_word_ns project selected instead of the hello_world_s. I am now trying to create a project from scratch instead of using the SDK sample. So far I can compile/link and execute the secure application but as soon as I enter the non-secure world and attempt an interactive debug session the code hangs. Clearly, I have a bit more testing to do.

    Like

  4. Hi Erich,

    While reviewing once more this post, I was wondering what happens in a real scenario to the secure function itself (__acle_se_DbgConsole_Printf_NSE). How can we protect its ‘secrecy’ to prevent the disassemble of it? I am thinking in a situation that someones is able to develop non-secure applications and as access to a set of secure API functions. I assume that we are able to see the secure function code since we have access to both secure and non-secure projects, right?

    Like

    • The ‘standard’ TrustZone feature is basically control who is able to call code in the ‘trusted’ area, basically a call gate keeper. To prevent the the non-trusted code to read/disassembe the secure code, you have to restrict the memory access similar to the MPU. There is yet another level of security (which I have not explored yet): the ability to restrict debugging. To my understanding you would need some kind of certificate/key to debug it. Otherwise as soon as you have debug access, you have access to the whole memory map.

      Like

  5. Pingback: Investigating ARM Cortex® M33 core with TrustZone® – running TrustZone® example projects in MCUXpresso IDE | MCU on Eclipse

What do you think?

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.