4. TA与CA开发入门:从架构到第一个Hello World
好,咱们进入第四章。这一章我打算聊聊TA和CA的开发入门。说实话,很多初学者一上来就被TrustZone的“双世界”概念搞晕了。其实没那么复杂——你想想看,它就是一个安全世界和一个普通世界,两个世界各自跑各自的程序,通过特定的通道通信。
我个人习惯把TA和CA的关系比作“服务端”和“客户端”。CA在普通世界发起请求,TA在安全世界处理请求。嗯,这个比喻虽然不完全准确,但用来入门足够了。
4.1 TA与CA的架构关系
先看一张图,把整体架构理清楚。
这张图里,左侧是普通世界,右侧是安全世界。CA跑在普通世界,TA跑在安全世界。它们之间通过GP TEE标准API通信,底层依赖SMC(安全监控调用)完成世界切换。
我在项目中遇到过一个问题:有人把TA直接当普通程序写,结果一调用就崩溃。为什么?因为TA不能直接访问普通世界的资源,反过来也一样。两个世界是隔离的,这是TrustZone的核心安全假设。
4.2 GP TEE API标准
GP(GlobalPlatform)TEE标准定义了TA和CA的编程接口。说白了,就是告诉你怎么写CA、怎么写TA,以及它们之间怎么通信。
标准主要分两部分:
- TEE Client API:给CA用的,运行在普通世界
- TEE Internal API:给TA用的,运行在安全世界
我整理了一个对比表,方便你理解两者的差异:
| 对比项 | TEE Client API | TEE Internal API |
|---|---|---|
| 使用方 | CA(客户端应用) | TA(可信应用) |
| 运行世界 | 普通世界 | 安全世界 |
| 核心功能 | 打开会话、发送命令、关闭会话 | 处理命令、访问安全存储、密码运算 |
| 关键函数 | TEEC_InitializeContext、TEEC_OpenSession、TEEC_InvokeCommand | TEE_OpenSession、TEE_InvokeCommand、TEE_AllocateOperation |
| 安全等级 | 低(不可信环境) | 高(可信环境) |
4.3 CA开发核心流程
写一个CA,本质上就是调用TEE Client API的几个函数。流程很固定:
- 初始化上下文:调用TEEC_InitializeContext,建立与TEE的连接
- 打开会话:调用TEEC_OpenSession,指定要连接的TA的UUID
- 发送命令:调用TEEC_InvokeCommand,传递命令ID和参数
- 关闭会话:调用TEEC_CloseSession,释放资源
- 释放上下文:调用TEEC_FinalizeContext,断开与TEE的连接
你看,就五步。我刚开始学的时候,总觉得少了点什么——是不是太简单了?其实TrustZone的复杂度不在API调用上,而在安全设计和内存管理上。API本身很清爽。
4.4 TA开发核心流程
TA的开发稍微复杂一点,因为它要处理来自CA的请求,还要管理安全世界的内存和密码运算。核心流程如下:
- 实现入口函数:TA_CreateEntryPoint、TA_OpenSessionEntryPoint、TA_InvokeCommandEntryPoint、TA_CloseSessionEntryPoint、TA_DestroyEntryPoint
- 注册命令处理:在TA_InvokeCommandEntryPoint中根据命令ID分发处理
- 处理参数:通过TEE_Param结构体接收CA传来的参数
- 返回结果:将处理结果写回参数,返回TEEC_SUCCESS或其他错误码
4.5 第一个Hello World TA
光说不练假把式。咱们写一个真正的Hello World TA。这个TA只做一件事:接收CA传来的字符串,加上"Hello: "前缀,再返回给CA。
先看TA的代码:
// hello_world_ta.c
#include <tee_internal_api.h>
#include <tee_internal_api_extensions.h>
#define TA_HELLO_WORLD_UUID \
{ 0x12345678, 0x1234, 0x1234, \
{ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0 } }
#define CMD_HELLO_WORLD 0
TEE_Result TA_CreateEntryPoint(void) {
// TA加载时的初始化
return TEE_SUCCESS;
}
void TA_DestroyEntryPoint(void) {
// TA卸载时的清理
}
TEE_Result TA_OpenSessionEntryPoint(uint32_t param_types,
TEE_Param params[4], void **session_ctx) {
// 会话打开时的处理
(void)¶m_types;
(void)¶ms;
(void)&session_ctx;
return TEE_SUCCESS;
}
void TA_CloseSessionEntryPoint(void *session_ctx) {
// 会话关闭时的处理
(void)&session_ctx;
}
TEE_Result TA_InvokeCommandEntryPoint(void *session_ctx,
uint32_t cmd_id, uint32_t param_types, TEE_Param params[4]) {
(void)&session_ctx;
if (cmd_id != CMD_HELLO_WORLD) {
return TEE_ERROR_BAD_PARAMETERS;
}
// 获取输入参数
// params[0] 是输入字符串
// params[1] 是输出缓冲区
char *input = (char *)params[0].memref.buffer;
size_t input_len = params[0].memref.size;
char *output = (char *)params[1].memref.buffer;
size_t output_len = params[1].memref.size;
// 构造输出: "Hello: " + input
const char *prefix = "Hello: ";
size_t prefix_len = strlen(prefix);
if (output_len < prefix_len + input_len + 1) {
return TEE_ERROR_SHORT_BUFFER;
}
memcpy(output, prefix, prefix_len);
memcpy(output + prefix_len, input, input_len);
output[prefix_len + input_len] = '\0';
return TEE_SUCCESS;
}
再看CA的代码:
// hello_world_ca.c
#include <tee_client_api.h>
#define TA_HELLO_WORLD_UUID \
{ 0x12345678, 0x1234, 0x1234, \
{ 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0 } }
#define CMD_HELLO_WORLD 0
int main(void) {
TEEC_Context ctx;
TEEC_Session session;
TEEC_Operation op;
TEEC_Result res;
uint32_t err_origin;
// 1. 初始化上下文
res = TEEC_InitializeContext(NULL, &ctx);
if (res != TEEC_SUCCESS) {
printf("初始化上下文失败: 0x%x\n", res);
return -1;
}
// 2. 打开会话
TEEC_UUID uuid = TA_HELLO_WORLD_UUID;
res = TEEC_OpenSession(&ctx, &session, &uuid,
TEEC_LOGIN_PUBLIC, NULL, NULL, &err_origin);
if (res != TEEC_SUCCESS) {
printf("打开会话失败: 0x%x, 错误来源: %u\n", res, err_origin);
TEEC_FinalizeContext(&ctx);
return -1;
}
// 3. 准备参数
char input[] = "World!";
char output[64] = {0};
memset(&op, 0, sizeof(op));
op.paramTypes = TEEC_PARAM_TYPES(
TEEC_MEMREF_TEMP_INPUT, // params[0]: 输入
TEEC_MEMREF_TEMP_OUTPUT, // params[1]: 输出
TEEC_NONE, // params[2]: 未使用
TEEC_NONE // params[3]: 未使用
);
op.params[0].tmpref.buffer = input;
op.params[0].tmpref.size = strlen(input) + 1;
op.params[1].tmpref.buffer = output;
op.params[1].tmpref.size = sizeof(output);
// 4. 发送命令
res = TEEC_InvokeCommand(&session, CMD_HELLO_WORLD, &op, &err_origin);
if (res != TEEC_SUCCESS) {
printf("调用命令失败: 0x%x\n", res);
} else {
printf("TA返回: %s\n", output);
}
// 5. 关闭会话和上下文
TEEC_CloseSession(&session);
TEEC_FinalizeContext(&ctx);
return 0;
}
4.6 编译与部署
编译TA和CA需要不同的工具链。TA用交叉编译链,目标平台是TEE;CA用普通编译链,目标平台是Linux或Android。
我一般用这样的Makefile结构:
# Makefile
CROSS_COMPILE ?= arm-linux-gnueabihf-
TA_CC = $(CROSS_COMPILE)gcc
CA_CC = gcc
TA_CFLAGS = -fPIC -I$(TEEC_EXPORT)/include
TA_LDFLAGS = -L$(TEEC_EXPORT)/lib -lteec
CA_CFLAGS = -I$(TEEC_EXPORT)/include
CA_LDFLAGS = -L$(TEEC_EXPORT)/lib -lteec
all: hello_world.ta hello_world_ca
hello_world.ta: hello_world_ta.c
$(TA_CC) $(TA_CFLAGS) -shared -o $@ $^ $(TA_LDFLAGS)
hello_world_ca: hello_world_ca.c
$(CA_CC) $(CA_CFLAGS) -o $@ $^ $(CA_LDFLAGS)
clean:
rm -f hello_world.ta hello_world_ca
部署时,TA要放到TEE的文件系统中,CA放到普通世界的文件系统中。具体路径取决于你用的TEE实现——OP-TEE的话,TA放在/lib/optee_armtz/目录下。
4.7 常见问题与避坑
最后,分享几个我踩过的坑:
- UUID不匹配:CA和TA的UUID必须完全一致,否则会话打不开。我曾经复制粘贴时漏了一个字节,查了整整一天。
- 参数类型错误:paramTypes定义错了,TA拿到的数据就是乱的。建议用TEEC_PARAM_TYPES宏,别手写。
- 内存越界:TA的栈很小,别在栈上开大数组。用TEE_Malloc分配堆内存。
- 忘记初始化:TEE_Param结构体一定要memset清零,否则未初始化的字段会导致奇怪的问题。