TrustZone 安全处理环境(SPE)

概述

Ameba SoC 采用 Arm TrustZone 技术实现硬件级安全隔离,在芯片内部建立 SPE (Secure Processing Environment,安全处理环境)。通过 TrustZone 机制,系统在物理层面将安全固件与普通应用程序隔离。当 TrustZone 保护机制使能并锁定后,即使非安全环境下的代码获得最高系统运行特权,也无法绕过底层硬件限制去窃取或篡改安全资源。

概念解析

在深入了解底层硬件机制之前,开发者需先理解以下软件层面的固件分类及其在 Ameba 异构多核架构中的对应关系:

异构多核架构:不同的处理器核心(Master)具有不同的物理安全属性:

  • Secure CPU(如 KM4TZ、CA32):具备完整的 TrustZone 硬件扩展,可在 Secure World 和 Non-Secure World 之间动态切换,承担系统安全策略的管理职责。

  • Non-Secure CPU(如 KM4NS、KM0、KR4、DSP):不具备安全扩展,硬件层面固定运行于 Non-Secure 状态,其发出的所有总线访问请求均默认带有“非安全”标签。

固件类型划分:在 Ameba SDK 中,系统固件被划分为两个隔离域:

  • 安全固件 (Secure Firmware):运行于 Secure World,可访问全系统的所有资源(包括安全与非安全资源),主要负责管理核心密钥、执行加解密运算、安全启动验证以及配置系统安全边界。安全固件包括 image1 和 image3 两部分:image1 为 KM4TZ 内核的 Bootloader,在 Secure 模式下执行固件加载与校验;image3 为独立安全固件,提供安全服务。两者均运行于 Secure World。

  • 普通固件 (Normal Firmware):在 Ameba SDK 中称为 image2,运行于 Non-Secure World,仅能访问被划分为非安全的内存和外设,或通过专用的 NSC (Non-Secure Callable) 接口向安全世界请求安全服务。常规的 RTOS、网络协议栈、业务逻辑均运行于此。

RDP 读保护:为进一步提升 image3(安全固件)的安全性,Ameba 引入了 RDP (Read Protection,读保护) 机制。TrustZone 实现运行时隔离,而 RDP 保障静态存储安全:

  • 固件加密存储:安全固件在 Flash 中以加密形式存储,运行时由硬件自动解密执行。

  • 防止物理读取:即使攻击者获取 Flash 芯片,也无法直接读取安全固件的明文内容。

  • 防止固件克隆:加密固件与芯片内部 OTP 密钥绑定,固件无法被复制到其他芯片运行。

硬件隔离机制

Arm TrustZone 通过在系统总线的发起端、传输端、接收端三个核心节点部署拦截机制,建立完整的硬件保护链路:

../../_images/trustzone_bus_arch.svg

1. 发起端 (Master):安全状态判定

系统中所有主设备(Master)和从设备(Slave)均挂载于 AXI 总线。当 Master 发起内存或外设访问时,其安全属性判定逻辑如下:

  • CPU 主设备(如 KM4TZ):根据当前 CPU 运行状态(Secure 或 Non-Secure),结合其内部的 IDAU(固化的安全地址映射)和 SAU(软件可配置的安全属性单元),自动为本次访问标记相应的安全标签。

  • 不具备安全扩展的主设备(如 KR4、DSP):此类主设备硬件层面固定运行于 Non-Secure 状态,其发出的所有总线访问请求均默认带有"非安全"标签,无法发出 Secure 请求。

  • 具备安全扩展的主设备(如 GDMA、Crypto Engine 等 IP):此类外设没有 SAU 单元,其安全属性完全依赖专属配置寄存器设定。若需发出 Secure 请求,必须由 Secure CPU(KM4TZ)预先配置其安全控制寄存器,赋予 Secure 权限。否则该设备发出的所有总线请求都是 Non-Secure 请求。

2. 传输端 (Bus):安全属性传递

AXI 总线协议包含专门的标准信号位用于传递访问的特权与安全属性,其中核心信号为 AxPROT[1]

  • AxPROT[1] = 0:表示安全访问(Secure Access)

  • AxPROT[1] = 1:表示非安全访问(Non-Secure Access)

无论发起者是 CPU 还是 DMA,AXI 总线互联矩阵均会携带此硬件信号,将其透传至目标外设。

3. 接收端 (Slave):访问权限拦截

当带有 AxPROT 标记的请求抵达目标之前,需经过硬件检查器的最终校验:

  • 内存(SRAM/Flash/DRAM):由 MPC(Memory Protection Controller)守护。MPC 内部记录各段物理内存的安全属性划分,若非安全请求尝试访问被标记为安全的内存块,MPC 将在硬件层面直接拦截访问并报错。

  • 普通外设(Peripherals):由 PPC(Peripheral Protection Controller)守护,控制某个外设(如某组 UART 或 SPI)是否仅限安全世界访问。

  • 自带安全鉴权的外设:部分 IP 自身硬件具备解析 AxPROT 信号的能力,无需外挂 PPC,可在其内部寄存器级别实现细粒度的安全控制。

系统启动与安全锁定

为确保上述硬件检查器(MPC/PPC 等)的配置不被恶意篡改,Ameba 采用严格的安全启动与锁定流程。

启动时序

  1. Secure CPU 率先唤醒:芯片上电或复位后,具备最高权限的 Secure CPU(KM4TZ)率先启动,此时系统处于 Secure World。

  2. 初始化安全边界:运行于 KM4TZ 的 Bootloader(image1)读取开发者预设的配置,初始化 SAU,并配置全系统的 MPC 和 PPC,完成安全边界的划分。

  3. 执行配置锁定(Lock):所有安全边界划分完毕后,KM4TZ 执行锁定操作。

锁定机制

锁定操作指向安全控制硬件(如 MPC 的 IDAU_LOCK 或 PPC 的 PPC_LOCK)的特定寄存器写入锁定标志位。一旦执行锁定:

  • 该硬件组件的所有安全配置寄存器将被物理冻结。

  • 不仅 Non-Secure World 无法修改配置,即使 Secure World 的 KM4TZ 也无法再进行修改。

  • 唯一的解锁途径是对系统进行硬件级复位(Reset)。

在 Ameba SDK 的默认实现中,Bootloader(image1)配置 MPC 和 PPC 时会锁定 MPC,而 PPC 保持未锁定状态。 这意味着 MPC 的内存安全划分在启动后不可更改,而 PPC 的外设归属配置可在运行时由安全固件动态调整。

架构特点

基于上述硬件隔离机制与启动锁定流程,Ameba SoC 依托 Arm TrustZone 技术建立的 SPE 安全处理环境具备以下特点:

  • 硬件隔离:隔离机制深植于芯片物理层,所有访问拦截均由底层硬件控制器直接裁决。

  • 零性能损耗:硬件级别的安全属性校验与实际数据地址传输并行处理。开启 TrustZone 保护并拦截非法访问几乎不增加系统总线延迟,实现高安全性与高性能兼顾。

  • 配置灵活:系统并未在出厂时固定所有内存和外设安全边界。开发者可以根据产品业务需求调整安全与非安全内存大小,并自由定义每个外设节点的安全属性,适配多样化的应用场景。

备注

Cortex-M 安全服务

本节将介绍跨世界调用(Cross-World Call)机制以及外设中断安全属性的配置方法。 通过合理使用这些机制,开发者可以在确保系统核心资产安全的前提下,实现安全世界(Secure World)与非安全世界(Non-Secure World)之间的高效协同。 此外,外设中断的安全属性也需要正确配置,确保关键外设的中断只能被安全世界响应。

NSC 调用机制

业务场景:实现安全与非安全世界的双向通信

在实际开发中,功能逻辑经常需要跨越 TrustZone 边界。例如:非安全侧的 RTOS 应用需要调用安全世界的 AES 算法库完成加密(非安全调用安全);或者安全侧的底层驱动处理完核心逻辑后,需要触发非安全侧的回调函数来记录日志(安全调用非安全)。 由于硬件隔离的限制,非安全代码无法直接寻址和调用安全函数。为了安全、受控地满足双向通信需求,TrustZone 引入了 NSC(Non-Secure Callable)机制。

机制与原理

编译器会为带有特殊标记的安全函数生成一层 安全网关跳板(Secure Gateway Veneers),这些跳板指令被集中放置在一个特殊的 NSC 内存区域中。 当非安全代码想要调用安全服务时,必须先跳转到 NSC 区域的跳板指令,由跳板指令将处理器状态切换为“安全”后,再跳转到实际的安全函数体执行。 执行完毕后,硬件会自动将状态恢复并返回非安全世界。 这种机制确保了非安全侧只能访问被明确暴露的 API,而无法恶意窥探安全世界的其他代码或数据。

配置与代码实操

以下代码展示了一个完整的跨世界交互流程:非安全侧注册回调函数,并触发安全侧服务; 安全侧处理后,再跨界回调非安全侧函数。

../../_images/secure_from_non_secure.svg

步骤 1:安全侧暴露 API 与回调注册(安全世界代码)

在安全世界中,必须使用 NS_ENTRY 宏(底层为 cmse_nonsecure_entry)声明函数,使其暴露给非安全世界。同时需结合 IMAGE3_ENTRY_SECTION 确保函数入口被放置在 NSC 内存段。

/* secure_src.c */

/* 1. 声明跨界回调指针类型,需使用 cmse_nonsecure_call 属性 */
typedef rdp_callback_t __attribute__((cmse_nonsecure_call)) rdp_ns_callback_t;

static rdp_ns_callback_t *g_rdp_callback = NULL;

/* 2. 暴露给非安全侧的 API:接收并注册非安全回调函数 */
IMAGE3_ENTRY_SECTION
NS_ENTRY void rdp_service_init(rdp_callback_t *callback) {
    if (callback != NULL) {
        /* 使用 cmse_nsfptr_create 转换非安全函数指针 */
        g_rdp_callback = (rdp_ns_callback_t *)cmse_nsfptr_create(callback);
    }
}

/* 3. 暴露给非安全侧的 API:触发安全服务,并由安全侧调用非安全回调 */
IMAGE3_ENTRY_SECTION
NS_ENTRY uint32_t rdp_callback_process(uint32_t idx) {
    if (g_rdp_callback == NULL) return 0;

    uint32_t secure_data = 0; /* ... 获取或处理安全数据的逻辑省略 ... */

    /* 跨界调用:安全世界调用非安全世界的回调函数 */
    return g_rdp_callback(secure_data);
}

步骤 2:非安全侧分配安全栈并调用(非安全世界代码)

非安全侧在调用任何 NSC 函数之前,必须首先为当前任务分配安全栈,否则会触发硬件异常。

../../_images/non_secure_from_secure.svg
/* example_rdp_service.c */

/* 1. 实现非安全侧的回调处理函数 */
static uint32_t my_callback(uint32_t data) {
    /* ... 处理来自安全世界的数据逻辑省略 ... */
    return data;
}

/* 2. 非安全侧任务执行流程 */
static void rdp_demo(void) {
    /* 【关键】为当前非安全任务分配安全上下文(安全栈)
     * 必须在调用任何安全侧 API 前执行! */
    rtos_create_secure_context(1024);

    /* 调用 NSC API:将非安全回调注册到安全世界 */
    rdp_service_init(my_callback);

    /* 调用 NSC API:触发安全服务,安全侧会回调 my_callback */
    uint32_t result = rdp_callback_process(0);

    rtos_task_delete(NULL);
}

SDK 提供了完整的参考例程,路径位于 {SDK}\example\peripheral\raw\RDP\rdp_service

小心

  • 安全栈分配:通过 rtos_task_create() 创建的非安全任务默认不分配安全栈。由于 NSC 安全函数的执行必须依赖安全栈,因此非安全任务在调用安全函数前,必须显式调用 rtos_create_secure_context() 分配空间,否则将引发 Hard Fault。

  • Menuconfig 使能:开发 TrustZone 功能前,须在 Menuconfig 中开启 CONFIG_TRUSTZONE 选项。若未开启,支持 Security 功能的 CPU 会将所有代码默认运行在安全世界。

中断安全属性配置

业务场景:保护核心外设中断不被接管

当客户在安全世界运行核心的 Secure Timer,或使用加密引擎处理敏感数据时,必须确保这些外设产生的中断只能由安全世界响应。 如果中断权限是开放的,非安全侧的 OS 就可能恶意接管该中断,或在关键时刻误清除中断标志,从而导致系统崩溃或敏感数据泄露。

底层机制与原理

TrustZone 硬件支持为每个独立的中断源(IRQ)配置安全属性。当硬件触发中断时,CPU 会根据该属性进行派发:

  • 安全中断:强制跳转到安全世界的向量表,由安全 ISR 处理。非安全世界完全无法拦截、屏蔽或篡改。

  • 非安全中断:跳转到非安全世界的向量表,由非安全 ISR 处理。

配置与代码实操

默认情况下,系统中未特别配置的中断均属于“非安全”属性。为了保护关键外设,开发者必须在安全世界的初始化代码中,显式调用核心 API 将其划归为安全中断。

/* 安全世界的外设中断初始化代码 */
void Secure_Timer_Init(void) {
    /* 1. 配置 Timer 硬件相关寄存器(省略)... */

    /* 2. 将特定中断源(如 TIMER5_IRQn)强行设置为安全属性
     * 注意:NVIC_ClearTargetState() 是 TrustZone 专属 API */
    NVIC_ClearTargetState(TIMER5_IRQn);

    /* 3. 注册安全 ISR 并使能中断 */
    InterruptRegister(Secure_Timer_ISR, TIMER5_IRQn, NULL, 5);
    InterruptEn(TIMER5_IRQn, 5);
}

备注

NVIC_ClearTargetState() 涉及系统核心安全权限,仅能在安全世界(Secure World)中被调用。若在非安全侧调用将被硬件拦截。

MPC 与 PPC 配置

在 TrustZone 系统中,安全边界不仅存在于代码执行层面,还需要在硬件总线层面进行隔离保护。 MPC 负责对物理内存区域进行安全属性划分,PPC 负责对外设进行安全属性控制。两者在总线从设备端实时拦截非法访问请求,确保安全资源不会被非安全主设备或非安全代码窃取或篡改。

在实际开发中,开发者经常需要调整内存区域或外设的安全属性。例如,在安全世界与非安全世界之间共享一块内存缓冲区,或将某个 I2C 接口独占给安全世界使用。本节将介绍 MPC 和 PPC 的配置方法。

MPC 配置

业务场景:跨世界共享内存数据

假设客户在 SRAM 特定区域中划定了一块 8KB 的缓存区,用于存放非安全端采集到的数据包,随后交由安全端解密。 为了让非安全侧的 OS 和 DMA 能够顺利将数据搬运到这块内存而不触发总线异常(Bus Fault),我们需要将这块特定内存的访问权限从"仅安全读写"放宽为"非安全可读写"。

底层机制与原理

MPC 负责为物理内存区域划分安全属性。它在总线层面实时拦截非法访问:如果是安全内存,非安全主设备的读写请求会触发总线错误。

系统启动时,Bootloader 会读取一份预设的 MPC 配置表并写入硬件寄存器。MPC Entry 的起始和结束地址须为 4K 对齐(硬件忽略地址低 12 位),每个 MPC 最多支持 8 个 Entry。未被配置覆盖的地址区间,默认均视为 Secure(安全)属性。

配置与代码实操

要修改内存属性,开发者需要直接修改 SDK 源码中的配置表文件 ameba_boot_trustzonecfg.c,在对应的 mpc_config 数组中插入一条新的 Entry。

RTL8721Dx:

github source code

配置规则:

  1. 定位到目标物理内存所属的 MPC 数组(见下方对应关系表)

  2. 在数组结尾的 0xFFFFFFFF (End Flag)之前插入新条目

  3. 确保配置项指定为 MPC_NS (非安全)和 MPC_RW (可读写)

  4. 新增 Entry 后,需将 End Flag 移动到新位置

代码示例:在默认配置基础上,将 0x20010000 - 0x20011FFF 这 8K 地址空间设置为非安全可读写:

/* ameba_boot_trustzonecfg.c */
const TZ_CFG_TypeDef mpc1_config[MPC_ENTRYS_NUM] =                  /* Security configuration for sram S1/S2 */
{
//  Start                       End                 CTRL
   {0x20000000,                 0x20006FFF,         MPC_RW | MPC_NS},   /* entry0: MSP_NS, ... */
   {0x20008000,                 0x20008FFF,         MPC_RW | MPC_NS},   /* entry1: KM0_RTOS_STATIC_0_NS */
   {(u32)__km4_bd_ram_start__,  0x20100000 - 1,     MPC_RW | MPC_NS},   /* entry2: BD_RAM_NS*/

   /* 在此处插入客户自定义的 8KB 非安全共享内存区间 */
   {0x20010000,                 0x20012000 - 1,     MPC_RW | MPC_NS},   /* entry3: User Shared Buffer (NS) */
   {0xFFFFFFFF,                 0xFFFFFFFF,         MPC_RW | MPC_NS},   /* entry4: End Flag (标志配置结束) */
   {0xFFFFFFFF,                 0xFFFFFFFF,         MPC_RW | MPC_NS},   /* entry5: TODO */
   {0xFFFFFFFF,                 0xFFFFFFFF,         MPC_RW | MPC_NS},   /* entry6: TODO */
   {0xFFFFFFFF,                 0xFFFFFFFF,         MPC_RW | MPC_NS},   /* entry7: TODO */
};

每个芯片的 MPC 的 Entry 数量有所差别。各芯片 MPC 介绍如下:

RTL8721Dx:
  • mpc1_config:SRAM

  • mpc2_config:PSRAM

MPC 锁定机制:每组 MPC 均有独立的 lock 寄存器(如 IDAU1_LOCK),用于锁定配置防止运行时被篡改:

  • 当向 lock 寄存器写入 1 时,对应的 MPC 配置被锁定

  • 锁定后,该 MPC 的所有寄存器(包括 lock 寄存器本身)均无法再被修改,直至系统复位

  • 锁定后也无法通过写入 0 来解锁,唯一的解锁方式是系统复位

出于安全性考虑,默认 SDK KM4TZ Bootloader 在初始化完上述 MPC 配置后,会自动将硬件的 MPC_LOCK 寄存器置位。

PPC 配置

业务场景:独占外设控制权

与保护内存类似,客户可能需要将某个特定的 I2C 接口用于连接外部硬件安全芯片。 这种情况下,必须彻底禁止非安全主设备对该外设寄存器的任何读写操作。

底层机制与原理

PPC 负责外设总线级别的安全过滤:

  • 当外设配置为 安全(Secure) 时,硬件仅放行安全侧主设备的访问请求,非安全侧的读写会被直接硬件拦截

  • 当外设配置为 非安全(Non-Secure) 时,两侧代码均可自由访问

PPC 在上电时,Bootloader 对于部分外设自动进行默认的初始化配置。用户也可以在运行时在安全世界通过 API 修改配置项。

配置与代码实操

Bootloader 上电时会为各类外设提供一套默认的初始配置。如果开发者需要调整某个外设的归属权,可以在安全世界的初始化阶段(如 main() 函数早期),通过 SDK 提供的 API 动态修改。

/* 安全世界初始化代码 */
#include "ameba_soc.h"

void Secure_Peripheral_Protection_Init(void) {
    // 调用 TZ_ConfigSlaveSecurity 调整外设权限
    // 示例 1: 将 UART0 配置为仅安全世界可访问(ENABLE = Secure 状态)
    TZ_ConfigSlaveSecurity(UART0_DEV, ENABLE);

    // 示例 2: 将 I2C1 开放给非安全世界使用(DISABLE = Non-Secure 状态)
    // TZ_ConfigSlaveSecurity(I2C1_DEV, DISABLE);

    // 可选: 手动锁定 PPC 配置(见下方锁定机制说明)
    // HAL_WRITE32(SYSTEM_CTRL_BASE, REG_PPC_LOCK, 1);
}

各个 IC 外设列表及具体宏定义如下:

RTL8721Dx:

函数类型 TZ_ConfigSlaveSecurity(PPC_PeripheralId Perip, u32 Status)

  • Status 可以选择为 SECURE 或者 NON_SECURE

  • PPC_PeripheralId 定义在 component/soc/amebadplus/fwlib/include/ameba_trustzone.h , Perip 可选的枚举值如下表所示:

外设名称

PPC_PeripheralId

WIFI

SECFG_WIFI_CFG

BT

SECFG_BT_CFG

SECURE_ENGINE

SECFG_SECURE_ENGINE

GDMA0

SECFG_GDMA0_CFG

PPE

SECFG_PPE_CFG

SDIO

SECFG_SDIO_CFG

SPI0

SECFG_SPI0

SPI1

SECFG_SPI1

PSRAM_PHY

SECFG_PSRAM_PHY

PSRAM_SPIC_USERMODE

SECFG_PSRAM_SPIC_USERMODE

SPIC_USERMODE

SECFG_SPIC_USERMODE

QSPI

SECFG_QSPI

SPORT0_I2S

SECFG_SPORT0_I2S

SPORT1_I2S

SECFG_SPORT1_I2S

OTPC

SECFG_OTPC_CFG

SYSON

SECFG_SYSON

UART0

SECFG_UART0

UART1

SECFG_UART1

UART2_BT

SECFG_UART2_BT

UART3_LOG

SECFG_UART3_LOG

GPIOA_B

SECFG_GPIOA_B

ADC

SECFG_ADC

CAP_TOUCH

SECFG_CAP_TOUCH

KEY_SCAN

SECFG_KEY_SCAN

IPC

SECFG_IPC

DBG_TIMER

SECFG_DBG_TIMER

PMC_TIMER_0_1

SECFG_PMC_TIMER_0_1

TIMER0_7_BASIC

SECFG_TIMER0_7_BASIC

TIMER8_9_PULSE_PWM_TIMER10_11

SECFG_TIMER8_9_PULSE_PWM_TIMER10_11

TRNG_PORT1_PORT2

SECFG_TRNG_PORT1_PORT2

RXI300

SECFG_RXI300

RSIP

SECFG_RSIP

LEDC

SECFG_LEDC

PDM

SECFG_PDM

IR

SECFG_IR

I2C0

SECFG_I2C0

I2C1

SECFG_I2C1

PPC 锁定机制: 与 MPC 的开机自动锁定不同,默认 Bootloader 未使能 PPC_LOCK 锁,因此代码可以在运行时动态切换外设权限。 但在产品量产阶段,建议客户在安全初始化完成所有 TZ_ConfigSlaveSecurity 调用后,手动将 PPC_LOCK 置 1 固化配置,防止运行期间遭到恶意篡改。

  • 当向 PPC_LOCK 写入 1 时,PPC 配置被锁定

  • 锁定后,PPC 寄存器无法再被修改,直至系统复位

  • 锁定后也无法通过写入 0 来解锁,唯一的解锁方式是系统复位

特殊外设的安全控制

以下外设内部实现了更细致的安全管理机制,可对寄存器或功能进行粒度更细的访问控制:

  • 定时器(Timer)

  • 真随机数生成器(TRNG)

  • 看门狗(Watchdog)

  • 一次性可编程存储器(OTP)

  • 通用 DMA(GDMA)

  • 加密引擎(SHA/AES)

建议将上述外设的 PPC 属性配置为非安全,利用外设内部的安全控制策略进行精细化管理,以获得更高的灵活性。具体配置方法请参考对应章节的用户手册。

TrustZone 布局配置

本章介绍 TrustZone 的地址映射机制,帮助开发者理解安全代码与非安全代码如何通过不同的地址窗口访问同一块物理内存。

了解 IDAU 和 SAU 的工作原理,有助于理解总线地址的划分方式。在大多数情况下,SDK 已根据芯片的内存布局自动配置好这些参数,开发者无需手动调整。

本章还会介绍一种实际场景:当安全固件体积增加导致编译报出 "region is full" 错误时,如何通过调整安全区域大小来解决问题。

SAU IDAU 联合仲裁

TrustZone 通过 IDAU 和 SAU 两个属性单元协同工作,为 Secure CPU 的总线地址空间设置安全属性。 SAU / IDAU 仅监控来自 CPU 的访问地址,不监控其他总线主设备(如 DMA)。本节介绍两者的工作机制及联合仲裁规则。

IDAU:硬件固化的基础映射

IDAU(Implementation-Defined Attribution Unit)由芯片厂商在硬件设计阶段固化,提供固定的安全属性映射。IDAU 使用地址 bit[28] 区分安全属性:

  • bit[28] = 0:非安全地址空间

  • bit[28] = 1:安全地址空间

以 SRAM 和 PSRAM 为例,地址空间划分如下:

  • SRAM

    • 0x2000_0000 ~ 0x2FFF_FFFF:非安全窗口(bit[28] = 0)

    • 0x3000_0000 ~ 0x3FFF_FFFF:安全窗口(bit[28] = 1)

  • PSRAM

    • 0x6000_0000 ~ 0x6FFF_FFFF:非安全窗口(bit[28] = 0)

    • 0x7000_0000 ~ 0x7FFF_FFFF:安全窗口(bit[28] = 1)

关键点:安全窗口和非安全窗口映射到同一块物理内存,但访问时携带的安全属性不同。

SAU:软件可配置的属性单元

SAU(Security Attribution Unit)可通过软件配置,为地址空间分配安全或非安全属性。

SAU 的特点:

  • 系统上电时,SAU 默认将整个地址空间设置为安全属性

  • SAU 提供多个 Entry,可将指定地址空间配置为非安全(NS)或非安全可调用(NSC)

  • 安全属性级别:S(安全,最高)> NSC(非安全可调用,中等)> NS(非安全,最低)

访问属性的判定规则

当 CPU 访问某个地址时,该访问是 Secure 还是 Non-Secure 的判定流程如下:

  1. IDAU 判定:根据地址 bit[28] 给出硬件固化的安全属性

  2. SAU 判定:根据软件配置给出安全属性

  3. 联合仲裁:取两者中较高的安全级别作为最终结果

安全属性级别:S(安全)> NSC(非安全可调用)> NS(非安全)

../../_images/idau_conj_with_sau.svg

IDAU 与 SAU 联合仲裁规则

备注

系统上电时,SAU 默认将整个地址空间设置为安全,因此启动阶段所有地址访问都是安全的。

SAU 配置文件位置

SAU 配置根据实际的内存布局(Layout)自动生成:

在大多数应用场景下,开发者无需手动修改 SAU 配置。不同 IC 的默认 TrustZone 布局请参考 RAM 布局与配置

开发阶段:安全区域大小配置

业务场景:安全固件体积膨胀导致编译失败

随着业务演进,客户在开发过程中添加了大量的私钥处理逻辑和核心安全算法代码(存放在 image3 中)。这经常会导致编译阶段报出 Secure RAM 空间不足的链接错误(Linker Error: region is full)。此时,开发者需要从非安全区的配额中,划拨出更多的内存给安全区域。

底层机制与原理

TrustZone 使能后,安全固件(image3)的代码、数据和栈需要分配到安全内存区域。SDK 通过以下方式控制安全区域的大小:

  • TZ_S_SIZE:存放核心安全代码和数据区,通过 Kconfig 配置(单位为 KB),默认值为 44KB

  • TZ_NSC_SIZE:专门存放生成的 Secure Gateway Veneers(跳转指令表),定义于 linker 脚本

  • TZ_ENTRY_SIZE:存放使用 IMAGE3_ENTRY_SECTION 声明的 NSC 函数实体本身,定义于 linker 脚本

为确保 MPC 正确工作,上述三个区域的总大小须为 4KB 的整数倍。

备注

NSC 区域(TZ_NSC_SIZE)和 Entry 区域(TZ_ENTRY_SIZE)在正常业务场景下无需调整。NSC 区域仅用于跳转指令表,不应在此处存放大量安全业务代码。

配置方法

针对不同平台的具体修改文件及步骤如下:

RTL8721Dx:

配置步骤

  1. 选择安全固件运行位置

    执行 ameba.py menuconfig,按路径 CONFIG TrustZone > CONFIG Link Option > IMG3(SecureImage) running on PSRAM or SRAM? 选择 image3 的运行位置。

  2. 调整安全区域大小

    执行 ameba.py menuconfig,按路径 CONFIG TrustZone > TrustZone Secure Image Size (KB) 配置安全区域大小:

    配置项

    描述

    TZ_S_SIZE

    Secure 区域:存储安全函数实现,非安全代码只能通过 NSC 函数间接调用,默认值为 44KB

    配置完成后,需确保安全区域总大小为 4KB 的整数倍,以满足 MPC 的 4K 对齐要求。

    备注

    NSC 区域(TZ_NSC_SIZE)仅用于存储编译器生成的 Secure Gateway Veneers 跳转指令表,Entry 区域(TZ_ENTRY_SIZE)用于存储 NSC 函数实体。正常业务场景下无需修改这两个区域大小。如确需调整,可修改 ameba_layout.ld 文件中的对应宏定义:

    RTL8721Dx:

    github source code

备注

关于 NSC 与 ENTRY 的空间分布限制:

TZ_NSC_SIZE 区域中的每个 NSC Entry 由 sg 和 b.w 两条指令共同组成。b.w 指令的编码限制了目标跳转地址不能过远(例如 0x2XXX_XXXX 跳到 0x3XXX_XXXX 地址)。 因此需要在 TZ_NSC_SIZE 区域附近分配 TZ_ENTRY_SIZE 安全区域来存放 NSC 函数。更多细节请参考 Cortex-M 安全服务

实战案例:ECDSA 安全签名服务

本节将基于 MbedTLS 加密库,演示一个 TrustZone 应用场景:私钥隔离与安全签名服务。 完整的工程代码位于 SDK 的 {SDK}\example\peripheral\raw\RDP\rdp_ecdsa 目录。

业务需求与核心痛点

在物联网安全中,设备的“私钥”是最高机密。如果将数字签名的逻辑放在非安全世界(OS 侧)运行,黑客一旦攻破系统漏洞,就能轻易窃取内存中的私钥,从而伪造设备身份。

TrustZone 解决方案:利用 NSC 跨世界调用机制,我们将私钥的生成、存储以及 ECDSA 签名算法全部封存在安全世界(Secure World)中。 非安全世界只需提供“待签名的数据”并准备好接收结果的空缓存。安全世界完成签名后,仅将“签名结果”和“公钥”返回给非安全世界。这样,即使非安全侧被完全攻破,黑客也无法接触到真实的私钥。

交互设计与调用流程

为了在两个世界间传递多个参数(消息、签名结果缓存、公钥缓存等),最佳实践是定义一个共享的数据结构,通过传递结构体指针来完成交互。

完整的调用流程如下:

  1. 非安全侧:准备好明文消息,并分配好用于存放签名结果的 Buffer,将其打包进请求结构体。

  2. 非安全侧:跨世界调用安全侧暴露的 NSC API,传入结构体指针。

  3. 安全侧:校验参数,提取明文,使用被隔离保护的私钥进行 ECDSA 签名计算。

  4. 安全侧:将计算得出的签名和公钥写入结构体指定的 Buffer 中,返回非安全侧。

  5. 非安全侧:使用获取到的公钥和签名,在非安全环境中对消息进行验签。

核心代码实操

1. 定义跨世界通信的共享数据结构(头文件)

双方需要一个共同认识的数据结构来进行参数传递。

/* ecdsa_sign_service.h */
typedef struct {
    const unsigned char *message;       /* [输入] 待签名的明文消息 */
    size_t message_len;                 /* [输入] 消息长度 */
    unsigned char *signature;           /* [输出] 用于存放签名的 Buffer */
    size_t *sig_len;                    /* [输出] 实际生成的签名长度 */
    unsigned char *public_key_raw;      /* [输出] 用于存放公钥的 Buffer */
    size_t *pub_key_len;                /* [输出] 实际公钥长度 */
} ecdsa_sign_req_t;

2. 安全世界的 NSC 服务封装(Secure World)

在安全侧,我们将 NSC 入口函数与真正的密码学运算逻辑解耦。NSC 函数仅负责参数校验和请求路由。

/* secure_ecdsa_service.c */

/* 内部实际的签名工作函数(包含 MbedTLS 复杂逻辑,不对非安全侧暴露) */
static int ecdsa_do_sign(const unsigned char *message, size_t message_len,
                         unsigned char *signature, size_t *sig_len,
                         unsigned char *public_key_raw, size_t *pub_key_len) {
    /* 1. 初始化 MbedTLS 上下文,生成/加载受保护的私钥 (略) */
    /* 2. 计算明文 Hash (略) */
    /* 3. 使用 mbedtls_ecdsa_write_signature 进行签名 (略) */
    /* 4. 将签名结果和公钥拷贝回输入参数指向的内存中 */
    return 0; // 返回成功
}

/* 暴露给非安全侧的 NSC 网关函数 */
IMAGE3_ENTRY_SECTION
NS_ENTRY int ecdsa_secure_sign(ecdsa_sign_req_t *req) {
    /* 【关键步骤】:指针与参数合法性校验 */
    if (req == NULL || req->message == NULL || req->signature == NULL ||
        req->sig_len == NULL || req->public_key_raw == NULL || req->pub_key_len == NULL) {
        RTK_LOGE(TAG, "Invalid request parameters\n");
        return -1;
    }

    /* 参数校验通过后,将工作委托给内部安全函数 */
    return ecdsa_do_sign(req->message, req->message_len,
                         req->signature, req->sig_len,
                         req->public_key_raw, req->pub_key_len);
}

3. 非安全世界的服务调用(Non-Secure World)

非安全世界的任务在调用签名服务前,必须申请足够大的安全栈,随后组装请求并调用 NSC 函数。

/* example_rdp_ecdsa.c */

static void ecdsa_tz_demo(void) {
    int ret;
    const char *message = "Hello from Non-Secure World!";
    unsigned char signature[MBEDTLS_ECDSA_MAX_LEN];
    size_t sig_len = 0;
    unsigned char public_key_raw[65];
    size_t pub_key_len = 0;
    ecdsa_sign_req_t req;

    /* 1. 【核心】为当前任务分配足够的安全栈
     * 密码学运算(尤其是非对称加密)在安全侧需要大量栈空间 */
    rtos_create_secure_context(4096);

    /* 2. 组装请求结构体,提供输入数据和输出 Buffer */
    req.message = (const unsigned char *)message;
    req.message_len = strlen(message);
    req.signature = signature;
    req.sig_len = &sig_len;
    req.public_key_raw = public_key_raw;
    req.pub_key_len = &pub_key_len;

    /* 3. 跨世界调用:请求安全世界进行签名 */
    RTK_LOGI(TAG, "Calling secure world for signing...\n");
    ret = ecdsa_secure_sign(&req);
    if (ret != 0) {
        RTK_LOGE(TAG, "Secure signing failed!\n");
        goto end;
    }

    /* 4. 拿到安全世界返回的签名和公钥后,在非安全侧进行验签确认 */
    ret = ecdsa_ns_verify((const unsigned char *)message, strlen(message),
                          signature, sig_len, public_key_raw, pub_key_len);
    if (ret == 0) {
        RTK_LOGI(TAG, "Sign in Secure World, Verify in Non-Secure World: SUCCESS!\n");
    }

end:
    rtos_task_delete(NULL);
}

小心

  1. 加大安全栈的分配:由于 MbedTLS 等加密库在运行时会消耗大量栈内存,非安全任务在调用复杂的安全服务前,rtos_create_secure_context(size) 的 size 参数建议分配 2048 字节 或 4096 字节,否则极易导致安全侧栈溢出(Stack Overflow)。

  2. 警惕指针漏洞(Secure 指针校验):在产品级代码中,安全侧不仅要判断指针是否为 NULL,还应使用硬件提供的地址校验机制(如 cmse_check_address_range),确保非安全侧传入的指针确实指向非安全内存,防止黑客通过恶意构造的指针诱导安全侧篡改自身内存数据。

Cortex-A 安全服务

在 Ameba 多核 SoC 架构中,Cortex-A (CA32) 核心运行宏内核 OS(如 Linux/大型 RTOS), 采用 SMC(Secure Monitor Call)机制,通过触发 CPU 特权异常来实现 TrustZone 世界切换。

本节介绍 Cortex-A 核心的 SMC 安全调用机制,以及 ATF (Arm Trusted Firmware) 框架下的安全服务实现。

SMC 跨世界调用机制

业务场景:Cortex-A 非安全世界访问安全资源

运行在 Cortex-A 非安全世界(EL0/EL1)的应用层或内核驱动,通常无法直接寻址受 TrustZone 保护的内存。当它们需要执行以下操作时,必须跨越硬件隔离边界:

  • 密钥获取:读取保存在 OTP/eFuse 中的设备根密钥。

  • 硬件加解密:请求安全世界的加密引擎(Crypto Engine)处理敏感数据。

  • 电源与状态管理:通过 PSCI 规范唤醒/挂起 CPU 核心。

为此,Arm 架构提供了 SMC 指令,作为从非安全世界敲开安全世界大门的唯一“合法标准接口”。

机制与原理

Cortex-A 的 SMC 机制主要依赖 CPU 寄存器(x0 ~ x7)传递参数和返回值。完整执行流如下:

  1. 触发异常:非安全世界的代码执行 SMC 汇编指令,并将“服务编号(Function ID)”存入 x0 寄存器,业务参数存入 x1~x7

  2. 陷入监视器:CPU 立即触发同步异常,强行切换至最高的 EL3 (安全监视器级别)。

  3. 路由分发:运行在 EL3 的 ATF (Arm Trusted Firmware) 接管系统,解析 x0 中的 ID,将请求路由给 BL32 中的 Secure Service 进行处理。

  4. 处理与返回:安全服务执行完毕后,将结果写入 x0~x3 寄存器,并通过 ERET 指令将 CPU 降级交还给非安全世界。

ATF 与 Secure Service 软件框架

ARM Trusted Firmware (ATF) 提供了 Cortex-A 安全世界软件的参考实现:

  • BL1/BL2:安全启动阶段,加载并验证后续固件

  • Secure Monitor (BL31):常驻 EL3,处理 SMC 调用和世界切换

  • Secure Service (BL32):安全代码,承载用户自定义安全服务(如密钥管理、加密操作等)

ATF 遵循 Arm 标准接口规范:

配置与代码实操

基于 ARM SMCC (SMC Calling Convention) 规范,厂商自定义服务的 Function ID 必须落在 0x8200_0000 ~ 0x8200_FFFF 区间内。 SDK 提供的完整参考工程位于 {SDK}\example\peripheral\raw\CA32Trustzone\

步骤 1:安全世界接收与处理(BL32 Secure Service 侧)

在安全世界中,我们需要注册一个 Handler 来拦截对应的 Function ID,处理业务,并利用 SMC_RET 宏将结果塞回寄存器。

/* rtk_svc_setup.c - 运行于安全世界 (BL32 Secure Service) */

/* 定义符合 SMCC 规范的自定义服务 ID */
#define RTK_SMC_TEST_SERVICE  0x82000001

/* SMC 异常处理分发中心 */
static uintptr_t rtk_smc_handler(uint32_t smc_fid,
                                 u_register_t x1, u_register_t x2,
                                 u_register_t x3, u_register_t x4,
                                 void *cookie, void *handle,
                                 u_register_t flags)
{
    uint32_t status = 0;
    uint32_t secure_data = 0;

    switch (smc_fid) {
        case RTK_SMC_TEST_SERVICE:
            /* --- 在此执行您的安全业务逻辑 --- */
            /* 示例:根据入参 x1 执行对应操作,并产出安全数据 */
            if (x1 == 0 /* SECURE_REG_READ */) {
                secure_data = 0x5A5A; // 模拟从安全内存读取到的机密数据
                status = 0;           // 0 表示成功
            } else {
                status = -1;          // 失败状态
            }

            /* SMC_RET2 宏:将状态码存入 x0(a0),数据存入 x1(a1) 并返回非安全世界 */
            SMC_RET2(handle, status, secure_data);
            break;

        default:
            /* 未知 ID 拒绝处理 */
            SMC_RET1(handle, SMC_UNK);
    }
}

步骤 2:非安全世界发起调用(OS / App 侧)

在非安全侧,只需包含标准的 arm_smccc_res 结构体,并调用封装好的 arm_smccc_smc 接口,即可触发上述流程。

/* example_CA32Trustzone.c - 运行于非安全世界 (EL0/EL1) */
#include "ameba_soc.h"

/* 标准 ARM SMC 返回结果结构体(映射到 x0~x3 寄存器) */
struct arm_smccc_res {
    unsigned long a0;  /* 对应 x0: 通常用作状态码 */
    unsigned long a1;  /* 对应 x1: 业务数据 1 */
    unsigned long a2;  /* 对应 x2: 业务数据 2 */
    unsigned long a3;  /* 对应 x3: 业务数据 3 */
};

/* 触发 SMC 的底层汇编封装接口 */
extern void __arm_smccc_smc(unsigned long a0, unsigned long a1,
                            unsigned long a2, unsigned long a3,
                            unsigned long a4, unsigned long a5,
                            unsigned long a6, unsigned long a7,
                            struct arm_smccc_res *res, void *quirk);
#define arm_smccc_smc(...) __arm_smccc_smc(__VA_ARGS__, NULL)

/* 业务层调用示例 */
static void invoke_secure_test(void)
{
    struct arm_smccc_res res;
    unsigned long cmd = 0; /* SECURE_REG_READ */

    /* 发起调用:
     * 参数 1 (a0) = Function ID (0x82000001)
     * 参数 2 (a1) = cmd (0)
     * 参数 9      = 用于接收返回值的结构体指针 */
    arm_smccc_smc(0x82000001, cmd, 0, 0, 0, 0, 0, 0, &res);

    /* 解析从安全世界带回的结果 */
    if (res.a0 == 0) {
        printf("SMC Call Success! Secure Value: 0x%lx\n", res.a1);
    } else {
        printf("SMC Call Failed! Error code: %lu\n", res.a0);
    }
}

警告

  1. 当 CPU 通过 SMC 进入监控模式时,所有中断请求将被屏蔽,直至返回非安全态后才会恢复中断处理。这意味着:

    • SMC 调用期间系统无法响应任何外部中断(包括高优先级中断)

    • 安全服务代码必须尽快执行并返回,避免长时间占用 CPU

    • 涉及大量数据传输或耗时操作的场景,可以转移到 Cortex-M (KM4TZ) 的 Image3 安全世界中实现,通过非安全回调通知 CA32 完成状态,从而不影响 CA32 的实时性

  2. 新增服务时确保函数 ID 不与 ATF 保留 ID 或其他 SiP 服务冲突

参考资源