4、可信执行环境(TEE)入门:对比TEE与REE,介绍ARM TrustZone技术,以及如何在TEE中隔离密钥运算
好,咱们进入第四章。前面几章聊了密钥隔离的必要性和基础概念,这一章咱们得动点真格的了——聊聊可信执行环境(TEE)。
说实话,我早年做物联网安全方案时,遇到的最大痛点就是:密钥存哪儿都不放心。存Flash里?容易被读。存外部SE(安全芯片)?成本高、通信链路又长。直到我接触了ARM TrustZone,才觉得这事儿终于有了一个“性价比不错”的解法。
这一章,我会从REE和TEE的对比讲起,然后深入TrustZone的硬件架构,最后手把手带你看看怎么在TEE里把密钥运算隔离起来。
4.1 REE vs TEE:两个世界,一个芯片
先问个问题:你手机里跑的那个Android或Linux系统,安全吗?
答案很残酷——不安全。为什么呢?因为它的攻击面太大了。一个App漏洞、一个内核提权、一个USB调试接口被打开,攻击者就能拿到整个系统的控制权。这个“大而全”的环境,我们叫它REE(Rich Execution Environment,富执行环境)。
那TEE是什么?说白了,就是在同一颗芯片上,用硬件强行隔离出另一个“小世界”。这个小世界有自己的内存、自己的外设、自己的操作系统(通常是微内核),而且和REE是物理隔离的。
我习惯用一个比喻:REE就像一个大商场,人来人往,谁都能进;TEE就像商场里的金库,只有拿着特殊钥匙的保安才能进去。
下面这个表格,是我在项目里经常拿来跟客户解释的,你一看就明白:
| 对比维度 | REE(富执行环境) | TEE(可信执行环境) |
|---|---|---|
| 操作系统 | Linux、Android、RTOS(功能丰富) | Trusty、OP-TEE、QSEE(微内核,功能精简) |
| 安全等级 | 低,攻击面大 | 高,硬件隔离 |
| 内存访问 | 可访问所有非安全内存 | 只能访问安全内存(TZASC控制) |
| 外设访问 | 可访问非安全外设 | 可访问安全外设(如安全GPIO、密钥存储器) |
| 典型用途 | 用户界面、网络通信、文件系统 | 密钥存储、指纹比对、支付认证、DRM |
| 启动流程 | 由TEE验证后启动 | 最先启动,验证REE |
核心要点:TEE不是软件层面的沙箱,而是硬件层面的隔离。攻击者即使攻破了REE的root权限,也无法直接读取TEE内存里的密钥。
4.2 ARM TrustZone:硬件隔离的基石
讲TEE,绕不开ARM TrustZone。这是ARM从Cortex-A系列开始引入的硬件安全扩展。我当年第一次看TrustZone的白皮书时,觉得它设计得真巧妙——它不是在芯片外面加了个锁,而是在芯片内部画了一条线。
这条线,叫NS位(Non-Secure bit)。
TrustZone把CPU、内存、总线、外设都分成了两个世界:
- 安全世界(Secure World):运行TEE OS和可信应用(TA)。
- 非安全世界(Non-Secure World):运行REE OS和普通应用(CA)。
CPU通过一个叫Monitor Mode(监控模式)的特殊模式来切换这两个世界。切换的指令是SMC(Secure Monitor Call)。
嗯,这里要注意:切换是有代价的。每次从REE切到TEE,都要保存上下文、刷新TLB、切换页表。所以别指望在TEE里跑高频循环——那会慢得让你怀疑人生。
下面这张图,是我用SVG画的TrustZone核心架构,你一看就懂:
4.3 在TEE中隔离密钥运算
好了,理论讲完了,咱们来点实际的。怎么在TEE里做密钥运算?
我以OP-TEE(开源TEE OS)为例,给你拆解一下流程。OP-TEE是目前物联网设备上用得最多的TEE方案之一,我在好几个智能门锁和车联网项目里都用过它。
4.3.1 整体架构:CA与TA的通信
在TEE里做密钥运算,涉及两个角色:
- CA(Client Application,客户端应用):运行在REE侧,负责发起请求。比如你的支付App。
- TA(Trusted Application,可信应用):运行在TEE侧,负责执行安全操作。比如密钥签名、解密。
通信流程是这样的:
- CA通过TEE Client API发起一个
TEEC_InvokeCommand调用。 - 这个调用触发
SMC指令,CPU从REE切换到TEE。 - TEE OS收到请求,找到对应的TA,把参数传给它。
- TA执行密钥运算(比如用私钥签名),结果通过共享内存返回给CA。
避坑指南:我曾经在一个项目里,把TA的堆栈设得太小,结果密钥运算时栈溢出了,整个TEE直接panic。后来我养成了习惯:TA的堆栈至少给8KB,复杂运算给16KB。
4.3.2 代码示例:在TA中生成并存储密钥
下面是一个简化版的TA代码,演示如何在TEE里生成一个RSA密钥对,并把私钥安全存储起来:
// TA入口函数:处理来自CA的命令
TEE_Result TA_InvokeCommandEntryPoint(void *sessionContext,
uint32_t cmdID,
uint32_t paramTypes,
TEE_Param params[4])
{
TEE_Result res;
TEE_ObjectHandle keyObj;
TEE_Attribute attr;
switch (cmdID) {
case CMD_GEN_RSA_KEY:
// 1. 分配临时密钥对象
res = TEE_AllocateTransientObject(TEE_TYPE_RSA_KEYPAIR,
RSA_KEY_BITS, &keyObj);
if (res != TEE_SUCCESS) {
EMSG("分配密钥对象失败: 0x%x", res);
return res;
}
// 2. 设置密钥属性(可导出公钥,私钥不可导出)
TEE_InitRefAttribute(&attr, TEE_ATTR_RSA_MODULUS,
NULL, 0);
TEE_InitValueAttribute(&attr, TEE_ATTR_RSA_EXPONENT,
65537, 0);
// 3. 生成密钥对(私钥永远不出TEE)
res = TEE_GenerateKey(keyObj, RSA_KEY_BITS, &attr, 1);
if (res != TEE_SUCCESS) {
EMSG("生成密钥失败: 0x%x", res);
TEE_FreeTransientObject(keyObj);
return res;
}
// 4. 将密钥持久化存储到安全存储区
res = TEE_OpenPersistentObject(TEE_STORAGE_PRIVATE,
"rsa_key_1",
TEE_DATA_FLAG_ACCESS_WRITE,
&keyObj);
if (res == TEE_SUCCESS) {
TEE_WriteObjectData(keyObj, &keyObj, sizeof(keyObj));
TEE_CloseObject(keyObj);
}
// 5. 返回公钥给CA(私钥留在TEE里)
// ... 导出公钥并写入params ...
TEE_FreeTransientObject(keyObj);
return TEE_SUCCESS;
default:
return TEE_ERROR_BAD_PARAMETERS;
}
}
重要提醒:上面的代码里,私钥是通过TEE_GenerateKey在TEE内部生成的,私钥的明文永远不会离开TEE内存。CA拿到的只有公钥。这是密钥隔离的核心原则——私钥不出TEE。
4.3.3 密钥运算的隔离效果
你想想看,当密钥运算在TEE里执行时,REE侧的攻击者能看到什么?
- 看不到私钥明文:私钥存在TEE的安全内存里,REE的页表根本映射不到。
- 看不到运算中间值:RSA签名时的模幂运算结果,都在TEE的寄存器里。
- 只能看到输入和输出:CA传进去的是明文数据,拿回来的是签名结果。
说白了,TEE给密钥运算提供了一个“黑箱”。你往里扔数据,它给你出结果,但你看不到里面怎么算的。
4.4 实际项目中的注意事项
最后,我结合自己的踩坑经历,给你列几条实战建议:
实战清单:
- 共享内存要小心:CA和TA通过共享内存传数据,这块内存REE和TEE都能读。千万别把密钥明文放共享内存里传——我见过有人这么干,结果被DMA攻击读走了。
- TA的签名要验证:TEE在加载TA时,会验证TA的数字签名。确保你的TA是用正确的私钥签名的,否则TEE会拒绝加载。
- 防侧信道攻击:TEE虽然隔离了内存,但CPU的缓存、分支预测等侧信道仍然可能泄露信息。做密钥运算时,尽量用常数时间算法。
- 别忘了回滚保护:TEE的固件和TA版本要防回滚。攻击者可能刷一个旧版本TA,利用已知漏洞窃取密钥。