第2章:TEE架构概览——GlobalPlatform标准与内部组件
做TEE开发这些年,我最大的感受是:不懂架构就动手,迟早要踩坑。记得我刚接触TEE时,上来就翻代码、调接口,结果被各种术语和组件关系搞得晕头转向。后来老老实实把GlobalPlatform标准啃了一遍,才真正看清了全局。
这一章,我们就来聊聊TEE的整体架构。说白了,就是搞清楚TEE里面到底有什么、它们怎么配合、以及GlobalPlatform这个组织为什么这么重要。
2.1 GlobalPlatform TEE标准——为什么绕不开它?
你可能会问:TEE又不是只有一家在做,为什么非要跟着GlobalPlatform(GP)的标准走?
嗯,这个问题我当年也问过。后来在项目里对接过三四种不同厂商的TEE,才明白GP标准的价值——它定义了TEE的“通用语言”。没有这个标准,每个厂商的TEE接口都不一样,上层应用得针对每个平台单独适配,那开发效率就太低了。
GP TEE标准的核心目标:
- 定义TEE与REE(Rich Execution Environment,即普通操作系统)之间的通信机制
- 规范TEE内部组件的接口和行为
- 保证不同厂商的TEE实现可以运行相同的TA(Trusted Application,可信应用)
- 提供安全存储、密码学服务等基础功能的标准化接口
GP标准把TEE分成了几个关键层次,我习惯用一张图来理解:
我的经验:刚开始看GP标准文档时,别试图一口气读完。我建议先看 GPD_TEE_Internal_Core_API_Specification 和 GPD_TEE_Client_API_Specification 这两份,它们覆盖了90%的日常开发场景。
2.2 TEE内部组件详解
好,现在我们聚焦到TEE内部。一个完整的TEE系统,至少包含以下几个核心组件。我按从底层到上层的顺序来讲。
2.2.1 TEE内核——整个系统的“心脏”
TEE内核说白了就是一个精简的安全操作系统。它不像Linux那样功能齐全,但该有的都有:任务调度、内存管理、中断处理、进程间通信。
我在项目中遇到过一个问题:某个TA在运行时会莫名其妙地崩溃,查了两天才发现是TEE内核的栈空间配置太小,递归调用把栈撑爆了。嗯,这种问题在REE侧很少见,但在TEE里很常见——因为资源太有限了。
| 特性 | TEE内核 | 普通OS内核 |
|---|---|---|
| 代码体积 | 通常 < 500KB | 几MB到几十MB |
| 内存占用 | 几MB到几十MB | 几百MB到几GB |
| 调度策略 | 简单优先级 + 时间片 | CFS / 实时调度 |
| 安全模型 | 最小权限 + 白名单 | 用户态/内核态隔离 |
| 中断处理 | 快速FIQ处理为主 | 复杂IRQ分发 |
2.2.2 TEE API——CA和TA的“桥梁”
GP标准定义了两套API,我习惯叫它们“一内一外”:
- TEE Client API(外部API):运行在REE侧,供CA调用。负责建立会话、传输数据、调用TA命令。
- TEE Internal API(内部API):运行在TEE侧,供TA调用。提供密码学、安全存储、时钟等基础服务。
举个例子,一个支付CA要调用TA做签名,流程是这样的:
// REE侧:CA调用Client API
TEEC_Context context;
TEEC_Session session;
TEEC_Operation op;
TEEC_InitializeContext(NULL, &context);
TEEC_OpenSession(&context, &session, &uuid,
TEEC_LOGIN_PUBLIC, NULL, NULL, NULL);
// 准备签名数据
op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT,
TEEC_MEMREF_TEMP_OUTPUT,
TEEC_NONE, TEEC_NONE);
op.params[0].tmpref.buffer = input_data;
op.params[0].tmpref.size = input_size;
op.params[1].tmpref.buffer = output_data;
op.params[1].tmpref.size = &output_size;
TEEC_InvokeCommand(&session, CMD_SIGN, &op, NULL);
TEEC_CloseSession(&session);
TEEC_FinalizeContext(&context);
注意:CA和TA之间的数据传输,一定要考虑共享内存的大小限制。我曾经遇到过CA传了一个1MB的缓冲区给TA,结果TA侧内存不够直接崩溃。后来我们改成了分块传输,才解决了问题。
2.2.3 安全存储——数据保护的“保险柜”
安全存储是TEE最常用的功能之一。说白了,就是让TA能把密钥、证书这类敏感数据存起来,而且只有TA自己才能读。
GP标准定义的安全存储,底层通常依赖两种机制:
- RPMB(Replay Protected Memory Block):利用eMMC/UFS的物理安全分区,硬件防重放攻击。
- 安全文件系统:在REE侧的文件系统上,用TEE密钥加密存储。数据是密文,REE看不到内容。
我个人习惯在项目初期就确定用哪种方案。RPMB安全性高,但容量小、速度慢;安全文件系统容量大,但依赖TEE密钥管理。没有绝对的好坏,看场景选。
2.2.4 安全时钟——可信的“时间证人”
安全时钟这个组件,很多人容易忽略。但它在很多场景下至关重要——比如证书有效期校验、许可证管理、防回滚保护。
安全时钟和普通时钟最大的区别是:它不能被REE侧篡改。你想想看,如果攻击者把系统时间改回2019年,那过期的证书是不是就能用了?这就是为什么TEE需要自己的可信时间源。
实际项目中,安全时钟通常来自:
- 硬件RTC(实时时钟),通过安全世界独占访问
- 安全芯片(如SE)提供的可信时间
- 网络时间协议(NTP)的签名时间戳(需要联网)
避坑指南:我曾经在一个项目里,TA依赖安全时钟做license校验。结果发现设备断电后,RTC电池没电了,时间重置到了1970年。从那以后,我每次都会加一个“时间合理性检查”——比如判断当前时间是否在固件编译日期之后。
2.3 组件之间的协作关系
这几个组件不是孤立的,它们配合起来才能干活。我画个简单的调用链:
CA (REE侧)
│
├─ 调用 TEEC_OpenSession() → TEE Client API
│ │
│ ├─ REE驱动 → SMC陷入 → TEE内核
│ │ │
│ │ ├─ TEE内核加载TA镜像
│ │ ├─ 验证TA签名
│ │ └─ 创建TA实例
│ │
│ └─ 返回会话句柄给CA
│
├─ 调用 TEEC_InvokeCommand() → TEE Client API
│ │
│ ├─ 参数传递到TA
│ ├─ TA调用 TEE Internal API
│ │ ├─ TEE_AllocateMemory() 分配安全内存
│ │ ├─ TEE_ReadObjectData() 从安全存储读取密钥
│ │ ├─ TEE_GetSystemTime() 获取安全时钟
│ │ └─ TEE_AsymmetricSignDigest() 执行签名
│ │
│ └─ 结果返回给CA
│
└─ 调用 TEEC_CloseSession() 关闭会话
这个流程看起来简单,但每个环节都可能出问题。比如TA加载失败、共享内存不足、安全存储读写超时——我在调试时都遇到过。所以我的建议是:先把每个组件的边界测清楚,再集成。
2.4 本章小结
这一章我们聊了TEE的整体架构。核心就三件事:
- GlobalPlatform标准定义了TEE的通用框架,让不同厂商的TEE能跑同样的TA
- TEE内部组件包括内核、API、安全存储、安全时钟,各司其职
- 组件协作通过Client API和Internal API完成CA到TA的完整调用链