第7篇:SECoreBin------安全引擎核心详解
Secure Engine(安全引擎,简称 SE)是 X-CUBE-SBSFU 中最核心、也是最难理解的部分。它像一个"银行金库"------存放密码运算所需的所有秘密(密钥),通过严格的访问控制机制(CallGate)只向外界暴露有限的合法操作,杜绝任何越权访问。本篇将深入剖析 SE 的设计理念、核心源码和运行机制。
1. Secure Engine 的设计理念
1.1 什么是可信执行环境(TEE)?
在嵌入式安全领域,可信执行环境(Trusted Execution Environment, TEE) 是指:
在同一个处理器上,通过硬件隔离机制创建的一个与普通执行环境(REE)隔离的独立运行区域。TEE 中的代码和数据即使面对被攻破的普通操作系统或应用程序也无法被非法访问。
X-CUBE-SBSFU 在 STM32G4 上实现了一个轻量级软件 TEE,使用 ARMv7-M MPU(Memory Protection Unit)而非 TrustZone(STM32G4 不支持 TrustZone)来实现隔离。
1.2 SE 在 SBSFU 中的角色
┌──────────────────────────────────────────────────────────┐
│ STM32G474RE Flash │
│ │
│ ┌───────────────────────┐ ← 0x08000000 (系统复位入口) │
│ │ 向量表 (0x200 B) │ │
│ ├───────────────────────┤ ← 0x08000200 │
│ │ │ │
│ │ SE Core (可信环境) │ 大小: ~24 KB │
│ │ ┌─────────────┐ │ │
│ │ │ CallGate入口 │ │ ← 0x08000204 (SE_CallGate) │
│ │ │ SE_Key_Data │ │ ← 0x08000400 (PCROP保护) │
│ │ │ SE Core代码 │ │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ ├───────────────────────┤ ← 0x08006000 │
│ │ SE 接口层 (~1.5KB) │ se_interface_application.o │
│ │ │ se_interface_bootloader.o │
│ ├───────────────────────┤ ← 0x08006A00 │
│ │ HDP 代码 (~256B) │ 安全内存激活 (RAM执行) │
│ ├───────────────────────┤ ← 0x08006B00 │
│ │ │ │
│ │ SBSFU (~38 KB) │ 状态机 + Loader + MPU配置 │
│ │ │ │
│ └───────────────────────┘ ← 0x0800FFFF │
│ │
│ ┌───────────────────────┐ ← 0x08010000 │
│ │ UserApp Active Slot │ 用户应用程序 │
│ │ (216 KB) │ │
│ └───────────────────────┘ ← 0x08045FFF │
└──────────────────────────────────────────────────────────┘
SE 的角色可以概括为三个词:存储秘密、执行运算、拒绝非法访问。
| 角色 | 说明 |
|---|---|
| 密钥保管者 | AES 密钥、ECDSA 公钥存储在 SE 专属 Flash 区域,受 PCROP 和 MPU 双重保护 |
| 密码运算器 | 固件解密(AES256-CBC)、签名验证(ECDSA P-384)、哈希计算(SHA384)均在 SE 内部执行 |
| 访问守卫 | 任何对 SE 的调用必须经过 CallGate,CallGate 严格验证调用来源和参数合法性 |
1.3 SE 与普通代码的隔离
UserApp (用户模式)
│
│ se_interface_application.o
│ (位于 SE 接口区,可被 UserApp 调用)
▼
SE 接口层 (特权模式,但不可访问 SE 内部)
│
│ SE_CallGate()
│ (唯一入口,验证调用来源)
▼
╔══════════════════════╗
║ SE Core (特权模式) ║ ← MPU 保护:外界不可直接访问
║ ║
║ SE_CallGateService() ║ ← 根据服务 ID 分发
║ ├─ AES 解密 ║
║ ├─ ECDSA 验签 ║
║ ├─ SHA384 哈希 ║
║ ├─ Flash 读写 ║
║ └─ ... ║
╚══════════════════════╝
2. SE 的核心文件解析
2.1 文件关系图
se_crypto_config.h (加密方案配置)
│
▼
se_crypto_bootloader.c ← 根据 SECBOOT_CRYPTO_SCHEME 条件编译
│ 实现: SE_APP_GetActiveFwInfo()
│ SE_APP_ValidateFw()
│ SE_APP_GetActiveFwState()
│
├──→ 使用 se_crypto_common.c (通用加密操作)
│ ├── SE_CRYPTO_Decrypt_Init/Append/Finish()
│ ├── SE_CRYPTO_AuthenticateFW_Init/Append/Finish()
│ └── SE_CRYPTO_Authenticate_Metadata()
│
├──→ 使用 se_low_level.c (底层接口)
│ ├── SE_LL_FLASH_Read/Write/Erase()
│ ├── SE_LL_Buffer_in_ram()
│ └── SE_LL_MPU_Init()
│
├──→ 链接 STM32_Cryptographic.lib (预编译加密库)
│ ├── AES (aes.h)
│ ├── ECDSA (ecc.h)
│ └── SHA (hash.h)
│
└──→ 链接 se_key.s (预编译生成的密钥汇编)
├── SE_ReadKey_1() ← AES-256 密钥 (32字节)
└── SE_ReadKey_1_Pub() ← ECDSA P-384 公钥 (96字节)
2.2 se_crypto_bootloader.c ------ 加密方案启动加载器
这是 SE 中最"业务化"的文件,它实例化了加密方案的模板,实现以下三个核心函数:
c
// 函数1: 获取活动固件头信息
SE_ErrorStatus SE_APP_GetActiveFwInfo(
uint32_t slot_number, // 槽位编号 (1/2/3)
SE_APP_ActiveFwInfo_t *p_FwInfo // 输出: 固件信息结构体
);
// 函数2: 验证固件签名和完整性
SE_ErrorStatus SE_APP_ValidateFw(
uint32_t slot_number, // 槽位编号
SE_APP_ActiveFwInfo_t *p_FwInfo // 固件信息
);
// 函数3: 获取固件状态
SE_ErrorStatus SE_APP_GetActiveFwState(
uint32_t slot_number, // 槽位编号
SE_FwStateTypeDef *p_fw_state // 输出: 固件状态
);
SE_APP_ValidateFw() 的完整验证流程:
输入: 槽位编号 (slot_number)
│
▼
1. 读取 Active Slot 起始地址的固件头
SE_IMG_Read(p_header, active_slot_addr, sizeof(SE_FwRawHeaderTypeDef))
│
▼
2. 验证固件头签名 (ECDSA P-384)
SE_CRYPTO_Authenticate_Metadata(p_header)
│ ├─ 用 ECDSA P-384 公钥验证 HeaderSignature 字段
│ └─ 失败 → 返回 SE_ERROR
│
▼
3. 验证固件体完整性
SE_CRYPTO_AuthenticateFW_Init(p_header, SE_FW_IMAGE_COMPLETE)
│ ├─ 逐块读取加密固件
│ ├─ AES256-CBC 解密
│ ├─ SHA384 哈希累计
│ └─ SE_CRYPTO_AuthenticateFW_Finish()
│ ├─ 比较计算出的 SHA384 与头部的 FwTag
│ └─ 不匹配 → 返回 SE_ERROR
│
▼
4. 检查固件状态 (如果 ENABLE_IMAGE_STATE_HANDLING)
SE_IMG_GetActiveFwState(slot_number, &fw_state)
│ ├─ 状态为 INVALID → 拒绝启动
│ └─ 状态为 VALID/SELFTEST/NEW → 允许启动
│
▼
输出: SE_SUCCESS / SE_ERROR
2.3 se_crypto_common.c ------ 通用加密操作
这个文件位于 Middlewares 层,定义了所有加密方案共用的密码原语操作:
| 函数 | 功能 |
|---|---|
SE_CRYPTO_Decrypt_Init() |
初始化 AES 解密上下文(加载密钥和 IV) |
SE_CRYPTO_Decrypt_Append() |
逐块追加解密数据 |
SE_CRYPTO_Decrypt_Finish() |
完成解密(处理 PKCS#7 填充) |
SE_CRYPTO_AuthenticateFW_Init() |
初始化固体验证(SHA384 + 分块解密) |
SE_CRYPTO_AuthenticateFW_Append() |
逐块追加固体验证数据 |
SE_CRYPTO_AuthenticateFW_Finish() |
完成验证(比较最终哈希) |
SE_CRYPTO_Authenticate_Metadata() |
验证固件头签名(ECDSA 验签) |
这些函数调用底层的加密库(STM32_Cryptographic,预编译的 .a/.lib 文件),实现具体的 AES 和 ECDSA 算法。
3. CallGate 机制详解
3.1 为什么需要 CallGate?
问题:SE 代码受 MPU 保护,外界代码不能直接调用 SE 内部函数。
解决方案:提供一个受控的"单一入口点"------
就像银行只有一个柜台窗口,所有业务必须通过窗口办理。
你不能翻过柜台直接进入金库。
CallGate 是 SE 暴露给外界的唯一合法入口。它不是一个简单的函数调用,而是一个精心设计的访问控制机制,包含:
- 来源验证:确认调用者来自合法的 SE 接口区域
- 参数验证:检查指针是否在合法内存范围内
- 防故障注入:对关键检查做双重验证
- 权限提升:进入 SE 时以特权模式执行
- 栈切换:从 SBSFU 栈切换到 SE 专用栈
- 向量表切换:SE 使用自己的中断向量表
3.2 CallGate 的工作流程
用 ASCII 流程图展示完整的调用链路(基于 se_callgate.c 实际源码):
SBSFU / UserApp (调用者)
│
│ 1) 将服务 ID 写入 R0
│ 将状态指针写入 R1
│ 将 PRIMASK 值写入 R2
│ 将其他参数压入栈
│
▼
SE_CallGate(SE_FunctionIDTypeDef eID, SE_StatusTypeDef *peSE_Status,
uint32_t PrimaskParam, ...)
│ ↑
│ 这个函数位于 │ 注意: SE_CallGate 本身不在 MPU 保护区内!
│ SE IF 区域 │ 它位于 SE_IF_REGION_ROM_START + 4 (0x08000204)
│ │ 这个地址是 SE 给外界的"公开入口"
│
├─ 2) 获取 LR (链接寄存器,确认调用来源)
│ LR = get_LR() // 返回地址必须在 SE_IF 区内
│
├─ 3) ENTER_PROTECTED_AREA()
│ 进入 MPU 保护区(关闭 SE 区域的用户模式访问权限)
│
├─ 4) IS_CALLER_SE_IF()
│ ┌─────────────────────────────────────────┐
│ │ if (LR < SE_IF_REGION_ROM_START) │
│ │ NVIC_SystemReset(); // 非法调用者! │
│ │ if (LR > SE_IF_REGION_ROM_END) │
│ │ NVIC_SystemReset(); // 非法调用者! │
│ └─────────────────────────────────────────┘
│ **关键设计**: 发现非法访问后立即系统复位,
│ 而不是返回错误------防止攻击者修改 LR 尝试再次调用
│
├─ 5) 双重验证指针合法性 (防御故障注入攻击)
│ Check 1: peSE_Status 指针在 RAM 中
│ Check 2: peSE_Status 指针不在 SE RAM 中
│ Check 3: (再次) IS_CALLER_SE_IF()
│ Check 4: (再次) peSE_Status 指针在 RAM 中
│ Check 5: (再次) peSE_Status 指针不在 SE RAM 中
│
├─ 6) 中断处理检查(如果启用了 IT_MANAGEMENT)
│ 如果 eID == SE_EXIT_INTERRUPT:
│ 恢复之前保存的用户上下文(栈指针、PRIMASK 等)
│ 返回到被中断的用户代码
│
├─ 7) 切换向量表到 SE 自己的向量表
│ SCB->VTOR = (uint32_t)&SeVectorsTable;
│ (仅在 SFU_ISOLATE_SE_WITH_FIREWALL 或 CKS_ENABLED 时)
│
├─ 8) 解析可变参数
│ va_start(arguments, PrimaskParam);
│
├─ 9) 调用 SE_CallGateService()(服务分发)
│ ┌─────────────────────────────────────┐
│ │ switch (eID) { │
│ │ case SE_INIT_ID: │ 初始化 SE(设置时钟)
│ │ case SE_CRYPTO_LL_DECRYPT_INIT: │ 解密初始化
│ │ case SE_CRYPTO_LL_DECRYPT_APPEND: │ 解密追加数据
│ │ case SE_CRYPTO_LL_DECRYPT_FINISH: │ 解密完成
│ │ case SE_CRYPTO_LL_AUTHENTICATE... │ 认证固件
│ │ case SE_CRYPTO_HL_AUTHENTICATE... │ 验证固件头签名
│ │ case SE_APP_GET_ACTIVE_FW_INFO: │ 获取固件信息
│ │ case SE_APP_VALIDATE_FW: │ 验证固件
│ │ case SE_APP_GET_FW_STATE: │ 获取固件状态
│ │ case SE_IMG_READ / WRITE / ERASE: │ Flash 操作
│ │ case SE_IMG_GET_FW_STATE: │ 获取镜像状态
│ │ case SE_IMG_SET_FW_STATE: │ 设置镜像状态
│ │ case SE_LOCK_RESTRICT_SERVICES: │ 锁定 SE 服务
│ │ case SE_EXTFLASH_DECRYPT_INIT: │ 外部Flash解密
│ │ case SE_SYS_SAVE_DISABLE_IRQ: │ 保存禁用IRQ
│ │ case SE_SYS_RESTORE_ENABLE_IRQ: │ 恢复IRQ
│ │ default: │ 非法 ID → 系统复位
│ │ } │
│ └─────────────────────────────────────┘
│
├─ 10) 恢复向量表
│ SCB->VTOR = AppliVectorsAddr;
│
├─ 11) EXIT_PROTECTED_AREA()
│ 退出 MPU 保护区
│
▼
返回给调用者 (SE_SUCCESS / SE_ERROR)
3.3 CallGate 的服务 ID 定义
从 se_callgate.h 中可以看到完整的服务 ID 列表:
c
// 通用服务
#define SE_INIT_ID (0x00UL) // SE 初始化
// 加密底层服务 (仅 Bootloader)
#define SE_CRYPTO_LL_DECRYPT_INIT_ID (0x04UL) // 解密初始化
#define SE_CRYPTO_LL_DECRYPT_APPEND_ID (0x05UL) // 解密追加
#define SE_CRYPTO_LL_DECRYPT_FINISH_ID (0x06UL) // 解密完成
#define SE_CRYPTO_LL_AUTHENTICATE_FW_INIT_ID (0x07UL) // 认证初始化
#define SE_CRYPTO_LL_AUTHENTICATE_FW_APPEND_ID (0x08UL) // 认证追加
#define SE_CRYPTO_LL_AUTHENTICATE_FW_FINISH_ID (0x09UL) // 认证完成
// 加密高层服务
#define SE_CRYPTO_HL_AUTHENTICATE_METADATA (0x10UL) // 验证固件头签名
// 应用层服务
#define SE_APP_GET_ACTIVE_FW_INFO (0x20UL) // 获取固件信息
#define SE_APP_VALIDATE_FW (0x21UL) // 验证固件
#define SE_APP_GET_FW_STATE (0x22UL) // 获取固件状态
// Flash 镜像操作
#define SE_IMG_READ (0x92UL) // 读 Flash 受保护区
#define SE_IMG_WRITE (0x93UL) // 写 Flash 受保护区
#define SE_IMG_ERASE (0x94UL) // 擦除 Flash 受保护区
#define SE_IMG_GET_FW_STATE (0x95UL) // 获取镜像状态
#define SE_IMG_SET_FW_STATE (0x96UL) // 设置镜像状态
// 锁定服务
#define SE_LOCK_RESTRICT_SERVICES (0x100UL) // 锁定受限服务
// KMS 服务 (密钥管理)
#define SE_MW_ADDON_KMS_MSB (0x10000000U)
3.4 CallGate 的防攻击设计
在 se_callgate.c 的代码中,你可以看到几个明确的防御模式:
1. 双重检查(Double Check)防故障注入:
c
// 第一次检查
IS_CALLER_SE_IF();
if (SE_LL_Buffer_in_ram(peSE_Status, sizeof(*peSE_Status)) != SE_SUCCESS)
NVIC_SystemReset();
// 第二次检查(防止故障注入绕过第一次检查)
IS_CALLER_SE_IF(); // 再次验证调用源
if (SE_LL_Buffer_in_ram(peSE_Status, sizeof(*peSE_Status)) != SE_SUCCESS)
NVIC_SystemReset(); // 再次验证指针
2. 失败即复位,不返回错误码:
c
// 发现任何异常,不是 return SE_ERROR,而是直接复位
NVIC_SystemReset();
因为 return SE_ERROR 会恢复被攻击者篡改的栈帧,可能被利用来劫持控制流。
3. 服务锁定后不可逆:
c
// SE_LOCK_RESTRICT_SERVICES 只能被调用一次
static SE_LockStatus SE_LockRestrictedServices = SE_UNLOCKED; // 初始未锁定
case SE_LOCK_RESTRICT_SERVICES:
SE_LockRestrictedServices = SE_LOCKED; // 锁定后无法解锁
SE_LL_CORE_Cleanup(); // 清理 SE RAM 中的敏感数据
SE_LL_Lock_Keys(); // 锁定密钥
4. 密钥存储机制
4.1 密钥在哪里?
密钥存储在 SE 的专用密钥区域,受 PCROP 保护:
Flash 地址空间:
0x08000000 ┌──────────────────────┐
│ 向量表 │ 200 bytes
0x08000200 ├──────────────────────┤
│ SE_CallGate 入口 │ 4 bytes
0x08000204 ├──────────────────────┤
│ CallGate 代码区 │ 508 bytes
0x08000400 ├──────────────────────┤ ← SE_KEY_REGION_ROM_START
│ │
│ 密钥数据区 │ 512 bytes (最小 PCROP 粒度)
│ (.SE_Key_Data 段) │ PCROP 保护: 数据只能被 CPU 取指访问
│ │
0x08000600 ├──────────────────────┤ ← SE_KEY_REGION_ROM_END + 1
│ SE Startup 代码 │ 256 bytes
0x08000700 ├──────────────────────┤
│ SE Core 代码 │ ~23 KB
... ...
双重保护机制:
| 保护层 | 机制 | 效果 |
|---|---|---|
| 静态保护 | PCROP(Proprietary Code Read-Out Protection) | Flash 中的密钥区只能被 CPU 指令取指访问,不能通过数据读取(D-Code 总线) |
| 运行时保护 | MPU(Memory Protection Unit) | SE 密钥区配置为特权模式只读、用户模式不可访问 |
4.2 密钥如何被读取?
密钥通过专用函数读取,这些函数定义在 se_key.h 中,实现在 se_key.s(汇编文件)中:
c
// se_key.h 中的声明
void SE_ReadKey_1(uint8_t* buffer); // AES256 密钥 (32 bytes)
void SE_ReadKey_1_Pub(uint8_t* buffer); // ECDSA P-384 公钥 (96 bytes)
void SE_ReadKey_2(uint8_t* buffer); // OEM2 密钥 (可选)
void SE_ReadKey_2_Pub(uint8_t* buffer); // OEM2 公钥 (可选)
void SE_ReadKey_3(uint8_t* buffer); // OEM3 密钥 (可选)
void SE_ReadKey_3_Pub(uint8_t* buffer); // OEM3 公钥 (可选)
这些函数的汇编实现如下(以实际 se_key.s 为例,本项目使用 ECDSA P-384 + AES256-CBC):
asm
AREA |.SE_Key_Data|, CODE ; 放在 .SE_Key_Data 段(密钥区)
EXPORT SE_ReadKey_1 ; AES256 密钥读取函数 (32字节)
SE_ReadKey_1
PUSH {R1-R5} ; 保存工作寄存器
MOVW R1, #0x454f ; 密钥字节 0-1 (低16位,小端序)
MOVT R1, #0x5f4d ; 密钥字节 2-3 (高16位)
MOVW R2, #0x454b ; 密钥字节 4-5
MOVT R2, #0x5f59 ; 密钥字节 6-7
MOVW R3, #0x4f43 ; 密钥字节 8-9
MOVT R3, #0x504d ; 密钥字节 10-11
MOVW R4, #0x4e41 ; 密钥字节 12-13
MOVT R4, #0x3159 ; 密钥字节 14-15
STM R0, {R1-R4} ; 写入 16 字节到 *R0(调用者缓冲区)
ADD R0, R0, #16 ; 缓冲区指针 += 16
; ... 继续写入后续 16 字节 (AES-256 共 32 字节) ...
POP {R1-R5} ; 恢复寄存器
BX LR ; 返回
EXPORT SE_ReadKey_1_Pub ; ECDSA P-384 公钥读取函数 (96字节)
SE_ReadKey_1_Pub
PUSH {R1-R5}
MOVW R1, #0xf02 ; 公钥字节 0-1
MOVT R1, #0x3d9c ; 公钥字节 2-3
MOVW R2, #0x8bbb
MOVT R2, #0x60f1
MOVW R3, #0x290c
MOVT R3, #0x2313
MOVW R4, #0x2b12
MOVT R4, #0xa106
STM R0, {R1-R4}
ADD R0, R0, #16
; ... 重复 6 次 (共 96 字节 = 6 × 16) ...
POP {R1-R5}
BX LR
END
4.3 密钥混淆(Obfuscation)
密钥在 Flash 中不以连续的字节数组形式存在。每个密钥字节对被嵌入到 MOVW/MOVT 指令的立即数字段中:
MOVW/MOVT 指令在 Flash 中的实际编码 (每条指令 4 字节,Thumb-2):
┌──────────────────────────────────────┐
│ 0xF44F 0145 │ MOVW R1, #0x454f → [0x4f, 0x45]
│ 0xF2C5 010D │ MOVT R1, #0x5f4d → [0x4d, 0x5f]
│ 0xF44F 024B │ MOVW R2, #0x454b → [0x4b, 0x45]
│ 0xF2C5 0209 │ MOVT R2, #0x5f59 → [0x59, 0x5f]
│ ... │
└──────────────────────────────────────┘
攻击者在 Flash dump 中看到的是:
4F F4 4F 45 C5 F2 4D 5F 4F F4 4B 45 C5 F2 59 5F ...
而不是连续的密钥字节:
4F 45 4D 5F 4B 45 59 5F ...
真正有用的密钥字节隐藏在每条指令的立即数字段中,
与 Thumb-2 操作码混在一起,静态分析难以提取。
这是轻量级混淆 (不是真正的加密),但它能有效防止简单的二进制扫描和 strings 等工具的直接提取。
4.4 密钥保护层次总结
┌───────────────────────────────────────────┐
│ 层次1: 混淆 (Obfuscation) │
│ 密钥分散在汇编立即数中,不是连续字节 │
│ ┌─────────────────────────────────────┐ │
│ │ 层次2: PCROP (专有代码读保护) │ │
│ │ CPU 可以执行代码但不能读取数据 │ │
│ │ 调试器完全不能访问 │ │
│ │ ┌───────────────────────────────┐ │ │
│ │ │ 层次3: MPU (运行时内存保护) │ │ │
│ │ │ 用户模式下完全不可访问 │ │ │
│ │ │ 只能通过 CallGate 间接调用 │ │ │
│ │ │ ┌─────────────────────────┐ │ │ │
│ │ │ │ 层次4: RDP (芯片读保护) │ │ │ │
│ │ │ │ Level 1: 调试器连接 │ │ │ │
│ │ │ │ 自动触发全片擦除 │ │ │ │
│ │ │ └─────────────────────────┘ │ │ │
│ │ └───────────────────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└───────────────────────────────────────────┘
5. 加密方案配置(se_crypto_config.h)
5.1 SECBOOT_CRYPTO_SCHEME 宏
c
// 文件: 2_Images_SECoreBin/Inc/se_crypto_config.h
/* 当前选择的加密方案 */
#define SECBOOT_CRYPTO_SCHEME SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384
/* 5 种可选方案 */
#define SECBOOT_ECCDSA_WITHOUT_ENCRYPT_SHA256 (1U)
#define SECBOOT_ECCDSA_WITH_AES128_CBC_SHA256 (2U)
#define SECBOOT_AES128_GCM_AES128_GCM_AES128_GCM (3U)
#define SECBOOT_ECCDSA_WITHOUT_ENCRYPT_SHA384 (4U)
#define SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384 (5U)
5.2 方案对比表
| 特性 | 方案(1) | 方案(2) | 方案(3) | 方案(4) | 方案(5) |
|---|---|---|---|---|---|
| 固件加密 | 无 | AES-128-CBC | AES-128-GCM | 无 | AES-256-CBC |
| 固件完整性 | SHA256 | SHA256 | GCM Tag | SHA384 | SHA384 |
| 固件头签名 | ECDSA P-256 | ECDSA P-256 | AES-GCM | ECDSA P-384 | ECDSA P-384 |
| AES密钥长度 | N/A | 16字节 | 16字节 | N/A | 32字节 |
| ECDSA公钥长度 | 64字节 | 64字节 | N/A | 96字节 | 96字节 |
| 哈希输出长度 | 32字节 | 32字节 | 16字节 | 48字节 | 48字节 |
| 固件头大小 | 192字节 | 192字节 | 192字节 | 232字节 | 232字节 |
| 安全性等级 | 中 | 中高 | 中高 | 高 | 最高 |
| Flash/RAM开销 | 最小 | 中等 | 中等 | 中等 | 最大 |
| 适用场景 | 内网设备 | 一般IoT | 资源受限 | 高安全需求 | 最高安全需求 |
5.3 如何选择加密方案
开始
│
┌────────────────┼────────────────┐
▼ ▼ ▼
固件需要加密? 固件需要加密? 固件需要加密?
否 是 是
│ │ │
▼ ▼ ▼
只有认证需求 认证+加密+完整性 认证+加密+完整性
│ │ │
┌────┴────┐ ┌────┴────┐ ┌───┴────┐
▼ ▼ ▼ ▼ ▼ ▼
标准安全 高安全 标准安全 高安全 仅对称 混合
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
方案(1) 方案(4) 方案(2) 方案(5) 方案(3) 方案(5)
SHA256 SHA384 AES128 AES256 AES128 AES256+
P-256 P-384 +P-256 +P-384 GCM P-384
5.4 切换加密方案的步骤
步骤1: 修改 se_crypto_config.h 中的 SECBOOT_CRYPTO_SCHEME
步骤2: 生成新密钥 (如果换了方案)
python prepareimage.py keygen -k new_key.bin -t aes-cbc -s 256
python prepareimage.py keygen -k new_ecc.txt -t ecdsa-p384
步骤3: 将新密钥文件复制到 SECoreBin/Binary/
步骤4: 按顺序全部重新编译 SECoreBin → SBSFU → UserApp
步骤5: 重新烧录设备
6. 固件元数据结构(se_def_metadata.h)
6.1 SE_FwRawHeaderTypeDef 结构体
本项目使用的加密方案(方案5)对应的头部结构:
c
// se_def_metadata.h 中针对 SECBOOT_ECCDSA_WITH_AES256_CBC_SHA384 的定义
typedef struct
{
uint8_t SFUMagic[4U]; // [0-3] 魔数 "SFU1"
uint16_t ProtocolVersion; // [4-5] 协议版本
uint16_t FwVersion; // [6-7] 固件版本号
uint32_t FwSize; // [8-11] 固件大小(字节)
uint32_t PartialFwOffset; // [12-15] 差分固件偏移
uint32_t PartialFwSize; // [16-19] 差分固件大小
uint8_t FwTag[48]; // [20-67] SHA384 固件哈希 (48字节)
uint8_t PartialFwTag[48]; // [68-115]差分固件哈希 (48字节)
uint8_t InitVector[16]; // [116-131] AES-256-CBC 的 IV (16字节)
uint8_t Reserved[4]; // [132-135] 保留字段
uint8_t HeaderSignature[96]; // [136-231] ECDSA P-384 头签名 (96字节)
uint8_t FwImageState[3][32]; // [232-327] 固件状态 (3×32字节)
uint8_t PrevHeaderFingerprint[32];// [328-359] 前一个固件头指纹 (32字节)
} SE_FwRawHeaderTypeDef;
// sizeof(SE_FwRawHeaderTypeDef) = 360
6.2 头部大小计算
头部总大小: sizeof(SE_FwRawHeaderTypeDef) = 360 bytes
头部组成 (本项目 ECDSA P-384 + AES-256-CBC + SHA-384):
┌───────────────────────┬──────────┬────────────────────────┐
│ 部分 │ 大小 │ 说明 │
├───────────────────────┼──────────┼────────────────────────┤
│ 认证区 (SFUMagic.. │ 136 bytes| 魔数+版本+大小+哈希+IV等 │
│ Reserved) │ │ 被 ECDSA 签名保护 │
├───────────────────────┼──────────┼────────────────────────┤
│ HeaderSignature │ 96 bytes| ECDSA P-384 签名 │
│ │ │ 对认证区的签名 │
├───────────────────────┼──────────┼────────────────────────┤
│ FwImageState │ 96 bytes| 3×32 固件状态 │
│ │ │ 不在签名范围内 │
├───────────────────────┼──────────┼────────────────────────┤
│ PrevHeaderFingerprint │ 32 bytes| 前一头部的指纹 │
│ │ │ 不在签名范围内 │
├───────────────────────┼──────────┼────────────────────────┤
│ 合计 │ 360 bytes| 对齐到 8 字节边界 │
└───────────────────────┴──────────┴────────────────────────┘
6.3 各字段含义详解
| 字段 | 大小 | 值/说明 |
|---|---|---|
SFUMagic |
4B | "SFU1"/"SFU2"/"SFU3",标识槽位编号 |
ProtocolVersion |
2B | 协议版本,当前为 4 |
FwVersion |
2B | 固件版本号,由 postbuild.bat 的第 5 个参数指定 |
FwSize |
4B | 解密后固件的实际字节数 |
PartialFwOffset |
4B | 差分固件在完整固件中的偏移位置 |
PartialFwSize |
4B | 差分固件的大小 |
FwTag |
48B | SHA384(解密后的固件)。设备端验证时重新计算比较 |
PartialFwTag |
48B | SHA384(差分固件) |
InitVector |
16B | AES-CBC 加密的初始化向量 |
HeaderSignature |
96B | ECDSA P-384 对认证区的签名 |
FwImageState |
96B | 3 个 32 字节状态字,编码固件当前状态 |
PrevHeaderFingerprint |
32B | 前一个固件头指纹,用于防回滚攻击 |
6.4 固件状态编码
固件镜像有 5 种可能的状态(定义在 se_def_metadata.h):
c
// 三种 32 字节状态字的编码:
// FWIMG_STATE_INVALID : 32*0x00, 32*0x00, 32*0x00 // 无效
// FWIMG_STATE_VALID : 32*0xFF, 32*0x00, 32*0x00 // 有效
// FWIMG_STATE_VALID_ALL: 32*0xFF, 32*0x55, 32*0x00 // 主槽位有效,连锁验证
// FWIMG_STATE_SELFTEST : 32*0xFF, 32*0xFF, 32*0x00 // 自检中
// FWIMG_STATE_NEW : 32*0xFF, 32*0xFF, 32*0xFF // 新镜像,待首次验证
状态转换图:
NEW ──(首次启动)──→ SELFTEST ──(自检通过)──→ VALID
│ │ │
│ └──(自检失败)──→ INVALID │
│ │
└───────────────────────────────────────────────┘
(如果 ENABLE_IMAGE_STATE_HANDLING 未开启)
VALID ──(新固件被安装)──→ NEW (新镜像被安装,旧镜像保持 VALID)
│ │
│ └──(首次启动)──→ SELFTEST
│ │
│ ┌──(自检通过)──┘
│ │
│ ▼
│ VALID
│
└──(正常运行)──────────────────→ VALID (保持不变)
7. 数据初始化(data_init.c)
7.1 SE 独立的 .data 和 .bss 段
SE 拥有自己独立的 .data 和 .bss 段,独立于 SBSFU 的全局数据区。这是因为 SE 的 RAM 区域被 MPU 保护,SBSFU 的启动代码(__main 或 _start)不会初始化 SE 的数据段。
c
// data_init.c
// SE 提供的外部符号(由链接器定义)
extern uint32_t Image$$SE_region_RAM$$RW$$Base; // SE .data 在 RAM 中的起始地址
extern uint32_t Load$$SE_region_RAM$$RW$$Base; // SE .data 在 Flash 中的加载地址
extern uint32_t Image$$SE_region_RAM$$RW$$Length; // SE .data 的长度
extern uint32_t Image$$SE_region_RAM$$ZI$$Base; // SE .bss 在 RAM 中的起始地址
extern uint32_t Image$$SE_region_RAM$$ZI$$Length; // SE .bss 的长度
// 将 .data 从 Flash 复制到 RAM
void LoopCopyDataInit(void)
{
uint8_t* src = (uint8_t*)&data_rom; // Flash 中的初始化数据
uint8_t* dst = (uint8_t*)&data_ram; // RAM 中的目标
uint32_t len = (uint32_t)&data_rom_length;
for (uint32_t i = 0; i < len; i++)
dst[i] = src[i];
}
// 将 .bss 清零
void LoopFillZerobss(void)
{
uint8_t* dst = (uint8_t*)&bss;
uint32_t len = (uint32_t)&bss_length;
for (uint32_t i = 0; i < len; i++)
dst[i] = 0;
}
// SE 数据初始化的统一入口
void __arm_data_init(void) {
LoopFillZerobss(); // 先清零 .bss
LoopCopyDataInit(); // 再复制 .data
}
// SE 数据清理(用于退出前擦除敏感数据)
void __arm_clean_data(void) {
LoopFillZerobss(); // 清零 .bss
LoopCleanDataInit(); // 清零 .data(擦除密钥等敏感信息)
}
7.2 为什么需要独立的初始化?
SBSFU 启动流程:
SystemInit() → __main()
├─ 初始化 SBSFU 的 .data 和 .bss
├─ 调用 main()
│ └─ SE_Startup()
│ ├─ __arm_data_init() ← 初始化 SE 专属 .data/.bss
│ └─ 设置 SE 系统时钟
└─ ...
如果 SE 不独立初始化:
- SE 的全局变量可能包含上一次运行的残留值(安全风险!)
- SE 的初始化值可能来自错误的 Flash 地址
- BSS 中可能残留加密操作后的敏感密钥数据
8. 底层接口(se_low_level.c)
8.1 功能分类
SE 的底层接口负责处理 MCU 特定的硬件操作:
| 函数类别 | 代表函数 | 功能 |
|---|---|---|
| Flash 操作 | SE_LL_FLASH_Read() |
从 Flash 读取数据到缓冲区 |
SE_LL_FLASH_Write() |
将数据写入 Flash(双字对齐) | |
SE_LL_FLASH_Erase() |
擦除 Flash 页(2KB 页大小) | |
| 安全配置 | SE_LL_MPU_Init() |
初始化 SE 区域的 MPU 保护 |
SE_LL_PCROP_Init() |
初始化 PCROP 保护 | |
SE_LL_Lock_Keys() |
锁定密钥(退出 SE 前调用) | |
| 缓冲区检查 | SE_LL_Buffer_in_ram() |
检查缓冲区是否在合法 RAM 范围内 |
SE_LL_Buffer_in_SBSFU_ram() |
检查缓冲区是否在 SBSFU RAM 范围内 | |
SE_LL_Buffer_part_of_SE_ram() |
检查缓冲区是否在 SE RAM 内(禁止!) | |
| 系统操作 | SE_LL_CRC_Config() |
恢复 CRC 外设配置 |
| 清理 | SE_LL_CORE_Cleanup() |
清理 SE RAM,擦除敏感数据 |
8.2 缓冲区检查的重要性
c
// 攻击者可能传递一个指向 SE 内部 RAM 的指针,
// 试图通过 SE 服务函数将恶意数据写入 SE 的受保护 RAM
// 防御: 严格的缓冲区位置检查
if (SE_LL_Buffer_part_of_SE_ram(peSE_Status, sizeof(*peSE_Status)) == SE_SUCCESS) {
NVIC_SystemReset(); // 攻击者试图将状态写入 SE 内部 → 直接复位!
}
SE 通过严格的缓冲区地址检查,防止调用者通过传递"指向 SE 内部 RAM 的指针"来攻击 SE 的受保护内存。
总结
Secure Engine 是 X-CUBE-SBSFU 安全体系的基石。它通过以下机制构建了一个"设备内部的安全岛":
- 物理隔离:PCROP + MPU 双重保护,使密钥和可信代码无法被外部读取
- 逻辑隔离:CallGate 单一入口,服务 ID 白名单,双重参数验证
- 防御纵深:失败即复位、双重检查防故障注入、服务锁定后不可逆
- 加密灵活性 :通过
SECBOOT_CRYPTO_SCHEME宏一键切换 5 种方案 - 数据独立:SE 拥有独立的数据段初始化,不依赖外部信任
核心理念:
- 信任最小化:只有 SE 持有密钥和执行加密操作
- 深度防御:PCROP + MPU + CallGate 三层保护
- 单向访问:通过 CallGate 只能调用特定服务,不能随意跳转
理解 SE 的设计,你就理解了嵌入式安全系统中"可信计算基(TCB)"的实现方式。下一篇文章我们将解析 SBSFU------它如何作为"调度者"使用 SE 提供的安全服务,实现完整的安全启动流程。