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框架会做以下几件事:

  1. 检查TA是否已加载到TEE中
  2. 如果未加载,从安全存储或镜像中加载TA
  3. 验证TA的签名和完整性
  4. 分配会话ID,建立CA与TA之间的逻辑通道
  5. 返回操作结果给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通信流程 CA (REE侧) TA (TEE侧) 1. TEEC_OpenSession UUID + 上下文 加载/验证TA 返回会话ID 2. 注册共享内存 内存地址 + 大小 映射到TEE空间 3. TEEC_InvokeCommand 命令ID + 参数 执行命令逻辑 返回结果 + 输出数据 4. 处理返回数据 5. TEEC_CloseSession 关闭会话 释放资源 说明:CA和TA之间的所有通信都经过TEE框架,保证安全隔离

从这张图可以看出,整个通信流程是单向发起、双向交互的。CA主动发起会话,TA被动响应。所有数据交换都经过TEE框架的管控,保证了安全性。

4.6 实战建议:少走弯路

最后,分享几个我在项目中积累的经验:

  • 会话复用:不要每次调用命令都建立新会话。会话建立的开销很大(尤其是TA加载和验证),建议在CA启动时建立一次会话,程序退出时关闭。
  • 参数类型匹配:CA和TA两端的paramTypes必须完全一致。我见过太多因为类型不匹配导致的崩溃。
  • 内存对齐:共享内存的起始地址建议按缓存行对齐(64字节),避免伪共享问题。
  • 错误处理:每个API调用都要检查返回值。TEE环境下的错误往往不会直接崩溃,而是静默失败,排查起来非常痛苦。

个人习惯:我通常在CA端封装一个通信层,把TEEC_OpenSession、TEEC_InvokeCommand、TEEC_CloseSession包装成简单的接口。这样上层业务代码不用关心TEE的细节,也方便后续切换不同的TEE实现。