4. TA与CA开发入门:从架构到第一个Hello World

好,咱们进入第四章。这一章我打算聊聊TA和CA的开发入门。说实话,很多初学者一上来就被TrustZone的“双世界”概念搞晕了。其实没那么复杂——你想想看,它就是一个安全世界和一个普通世界,两个世界各自跑各自的程序,通过特定的通道通信。

我个人习惯把TA和CA的关系比作“服务端”和“客户端”。CA在普通世界发起请求,TA在安全世界处理请求。嗯,这个比喻虽然不完全准确,但用来入门足够了。

4.1 TA与CA的架构关系

先看一张图,把整体架构理清楚。

TA与CA架构关系图 普通世界(Normal World) 安全世界(Secure World) CA应用 (客户端应用) TEE Client API (GP标准接口) 普通OS (Linux/Android) 硬件层 (ARM TrustZone) TA应用 (可信应用) TEE Internal API (GP标准接口) 安全OS (TEE内核) 安全硬件 (安全存储/密码加速器) SMC调用 CA通过TEE Client API发起请求,TA通过TEE Internal API提供服务 两者通过SMC指令完成世界切换

这张图里,左侧是普通世界,右侧是安全世界。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
安全等级 低(不可信环境) 高(可信环境)
小提示:GP TEE API是跨平台的。你在一家芯片上写的TA,理论上可以移植到另一家支持GP标准的TEE上。当然,实际移植时还得处理底层差异——我吃过这个亏。

4.3 CA开发核心流程

写一个CA,本质上就是调用TEE Client API的几个函数。流程很固定:

  1. 初始化上下文:调用TEEC_InitializeContext,建立与TEE的连接
  2. 打开会话:调用TEEC_OpenSession,指定要连接的TA的UUID
  3. 发送命令:调用TEEC_InvokeCommand,传递命令ID和参数
  4. 关闭会话:调用TEEC_CloseSession,释放资源
  5. 释放上下文:调用TEEC_FinalizeContext,断开与TEE的连接

你看,就五步。我刚开始学的时候,总觉得少了点什么——是不是太简单了?其实TrustZone的复杂度不在API调用上,而在安全设计和内存管理上。API本身很清爽。

4.4 TA开发核心流程

TA的开发稍微复杂一点,因为它要处理来自CA的请求,还要管理安全世界的内存和密码运算。核心流程如下:

  1. 实现入口函数:TA_CreateEntryPoint、TA_OpenSessionEntryPoint、TA_InvokeCommandEntryPoint、TA_CloseSessionEntryPoint、TA_DestroyEntryPoint
  2. 注册命令处理:在TA_InvokeCommandEntryPoint中根据命令ID分发处理
  3. 处理参数:通过TEE_Param结构体接收CA传来的参数
  4. 返回结果:将处理结果写回参数,返回TEEC_SUCCESS或其他错误码
注意:TA运行在安全世界,内存和栈空间都非常有限。我曾经在TA里开了一个1MB的局部数组,结果TEE直接崩溃。后来查了半天才发现是栈溢出。TA的栈通常只有几KB,大内存要用TEE_Malloc分配。

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)&param_types;
    (void)&params;
    (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;
}
关键点:CA和TA通过TEE_Param结构体传递数据。参数类型由paramTypes指定,可以是值传递(VALUE)或内存引用(MEMREF)。内存引用又分输入、输出和输入输出三种。这个设计很巧妙——它让TEE能控制内存访问权限,防止TA越界读写。

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/目录下。

调试技巧:TA的调试很麻烦,因为它跑在安全世界,不能直接用printf。我一般用TEE的日志机制——在TA里调用IMSG()或DMSG()宏,日志会通过安全监控输出到普通世界的串口。嗯,这个在开发阶段特别有用。

4.7 常见问题与避坑

最后,分享几个我踩过的坑:

  • UUID不匹配:CA和TA的UUID必须完全一致,否则会话打不开。我曾经复制粘贴时漏了一个字节,查了整整一天。
  • 参数类型错误:paramTypes定义错了,TA拿到的数据就是乱的。建议用TEEC_PARAM_TYPES宏,别手写。
  • 内存越界:TA的栈很小,别在栈上开大数组。用TEE_Malloc分配堆内存。
  • 忘记初始化:TEE_Param结构体一定要memset清零,否则未初始化的字段会导致奇怪的问题。