4. TA与CA通信机制:会话建立、内存共享、命令处理、参数传递
各位同学,今天我们来聊聊TEE开发里最基础也最关键的一环——CA(Client Application,客户端应用)和TA(Trusted Application,可信应用)怎么“对上话”。
说白了,CA跑在富执行环境(REE),TA跑在可信执行环境(TEE)。一个在普通世界,一个在安全世界。它们之间怎么传数据、怎么发命令、怎么共享内存?这就是本章要解决的问题。
我在做第一个TEE项目时,最头疼的就是CA和TA的通信。明明两边代码都写对了,就是调不通。后来才发现,是会话建立的上下文没搞清楚。嗯,咱们一步步来。
4.1 会话建立:CA与TA的“握手”
CA和TA通信的第一步,就是建立会话。你可以把它想象成打电话——先拨号,对方接起来,才能开始说话。
在GP TEE(GlobalPlatform TEE)标准中,会话建立的核心API是TEEC_OpenSession。CA调用这个接口,TEE框架会做以下几件事:
- 检查TA是否已加载到TEE中
- 如果未加载,从安全存储或镜像中加载TA
- 验证TA的签名和完整性
- 分配会话ID,建立CA与TA之间的逻辑通道
- 返回操作结果给CA
关键点:会话建立时,CA需要提供TA的UUID(Universally Unique Identifier)。这个UUID是TA的唯一标识,就像身份证号。UUID错了,会话肯定建立失败。
我曾经遇到过一个坑:CA和TA的UUID明明一样,但就是连不上。查了半天,发现是UUID的字节序搞反了。GP规范里UUID是大端序,但有些平台默认是小端序。嗯,这种细节最容易出问题。
4.2 内存共享:数据交换的“高速公路”
会话建立后,CA和TA需要交换数据。但问题来了——REE和TEE的内存空间是隔离的。CA不能直接访问TA的内存,反之亦然。
解决方案是什么?共享内存。
GP TEE定义了两种内存共享方式:
| 类型 | 描述 | 适用场景 |
|---|---|---|
| 临时内存 | CA分配一块内存,通过TEEC_RegisterSharedMemory注册到TEE |
小数据量、低频传输 |
| 预分配内存 | 在TA加载时预先分配一块固定大小的共享内存 | 大数据量、高频传输 |
我个人习惯用临时内存。原因很简单——灵活。你想想看,CA端可以按需分配和释放,不用提前规划大小。但要注意,共享内存的物理地址必须在TEE可访问的范围内。有些平台会限制共享内存的起始地址和大小对齐。
小技巧:共享内存的分配建议使用大页(如2MB对齐),这样可以减少TLB miss,提升性能。我在一个视频解码的项目里,把共享内存从4KB对齐改成2MB对齐后,吞吐量提升了约15%。
4.3 命令处理:CA如何“指挥”TA
会话建立、内存共享都准备好了,接下来就是发命令。CA通过TEEC_InvokeCommand调用TA中的特定命令。
每个TA都会实现一个命令分发函数,通常叫TA_InvokeCommandEntryPoint。这个函数根据命令ID,分发到不同的处理逻辑。
举个例子:
// TA端的命令处理入口
TEE_Result TA_InvokeCommandEntryPoint(
void *sessionContext,
uint32_t commandID,
uint32_t paramTypes,
TEE_Param params[4])
{
switch (commandID) {
case CMD_ENCRYPT:
return handle_encrypt(params);
case CMD_DECRYPT:
return handle_decrypt(params);
case CMD_GEN_KEY:
return handle_gen_key(params);
default:
return TEE_ERROR_BAD_PARAMETERS;
}
}
CA端调用时,需要指定命令ID和参数:
// CA端调用TA命令
TEEC_Operation op;
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_ENCRYPT, &op, &error);
这里要注意paramTypes的编码。它用4个字节表示4个参数的类型,每个参数占2位。我刚开始写的时候,经常把类型编码搞错,导致TA端解析参数失败。后来我写了一个宏来生成paramTypes,再也没出过问题。
4.4 参数传递:数据怎么“过墙”
CA和TA之间的参数传递,不是简单的memcpy。因为两个世界的内存隔离,参数需要经过TEE框架的“搬运”。
GP TEE定义了四种参数类型:
| 类型 | 值 | 说明 |
|---|---|---|
TEEC_NONE |
0 | 无参数 |
TEEC_VALUE_INPUT |
1 | 输入值(4字节) |
TEEC_VALUE_OUTPUT |
2 | 输出值(4字节) |
TEEC_VALUE_INOUT |
3 | 输入输出值 |
TEEC_MEMREF_TEMP_INPUT |
5 | 临时内存输入 |
TEEC_MEMREF_TEMP_OUTPUT |
6 | 临时内存输出 |
TEEC_MEMREF_TEMP_INOUT |
7 | 临时内存输入输出 |
TEEC_MEMREF_WHOLE |
12 | 整个共享内存区域 |
TEEC_MEMREF_PARTIAL_INPUT |
13 | 共享内存部分区域输入 |
TEEC_MEMREF_PARTIAL_OUTPUT |
14 | 共享内存部分区域输出 |
TEEC_MEMREF_PARTIAL_INOUT |
15 | 共享内存部分区域输入输出 |
你可能会问:为什么要有这么多类型?
原因很简单——性能。值类型(VALUE)只传4字节,适合小数据,比如密钥索引、状态码。内存引用类型(MEMREF)传大数据,但需要经过TEE框架的拷贝或映射。选对了类型,性能能差一个数量级。
避坑指南:我曾经在传输国密SM4的密钥时,用了TEEC_VALUE_INPUT。结果密钥是16字节,VALUE类型只能传4字节,密钥被截断了。嗯,这个bug查了我整整一天。记住:密钥、证书、大块数据,一定要用MEMREF类型。
4.5 整体流程:一张图看懂
说了这么多,咱们用一张图把整个流程串起来:
从这张图可以看出,整个通信流程是单向发起、双向交互的。CA主动发起会话,TA被动响应。所有数据交换都经过TEE框架的管控,保证了安全性。
4.6 实战建议:少走弯路
最后,分享几个我在项目中积累的经验:
- 会话复用:不要每次调用命令都建立新会话。会话建立的开销很大(尤其是TA加载和验证),建议在CA启动时建立一次会话,程序退出时关闭。
- 参数类型匹配:CA和TA两端的
paramTypes必须完全一致。我见过太多因为类型不匹配导致的崩溃。 - 内存对齐:共享内存的起始地址建议按缓存行对齐(64字节),避免伪共享问题。
- 错误处理:每个API调用都要检查返回值。TEE环境下的错误往往不会直接崩溃,而是静默失败,排查起来非常痛苦。
个人习惯:我通常在CA端封装一个通信层,把TEEC_OpenSession、TEEC_InvokeCommand、TEEC_CloseSession包装成简单的接口。这样上层业务代码不用关心TEE的细节,也方便后续切换不同的TEE实现。