☰
OP-TEE_my_test 从零跑通:QEMU 环境搭建与 TA/CA 最小验证闭环
2026/10/14 16:25:27 网站建设 项目流程

简介:OP-TEE_my_test 是一份面向嵌入式安全开发者的实践型代码包,聚焦 ARM 架构下可信执行环境 OP-TEE 中自定义 Trustlet(TA)与 Client Application(CA)的集成与测试。资源共 19 个文件,压缩包约 15KB,以 Makefile、.mk、Android.mk 等构建脚本为主,配合 .c 源码、.h 头文件、.msc 消息序列图及 README 说明,完整呈现 TA 与 CA 的目录组织与编译配置方式。已有 2338 人学习下载,适合具备一定 C 语言与 Linux 构建基础、希望理解 Secure World 与 Normal World 通信机制的开发者。通过该资源可掌握 TA 属性定义、CA 调用接口、OP-TEE 构建系统修改及 QEMU 部署验证等关键环节,并借助消息序列图理清 open session、invoke command 与 close session 的交互流程,为嵌入式安全应用开发提供可复用的工程参考。

1. 从零跑通 OP-TEE_my_test:一个可信执行环境的最小验证闭环

OP-TEE_my_test 这个标题,第一次看到的人大概率会愣一下——它不像某个正式项目名,更像某位工程师在本地建了个目录随手敲下的名字。但恰恰是这种命名,暴露了它真正的用途:一个用来验证 OP-TEE 环境是否跑通、TA(Trusted Application)能否正常加载、CA(Client Application)与安全世界通信是否顺畅的最小测试工程。如果你正在做 ARM 平台的可信执行环境开发,或者需要在 SoC 上落地安全存储、密钥管理、安全支付这类场景,这个测试工程就是你绕不开的第一道门槛。它不解决业务问题,它解决的是「我的环境到底能不能用」这个问题。很多人卡在这一步:编译过了,但 TA 加载失败;TA 加载了,但 CA 调用返回 0xFFFF0001;一切看似正常,但xtest跑不过。这篇笔记就围绕这个最小闭环展开,把环境搭建、TA/CA 编写、编译加载、调试排查的完整路径拆开讲清楚。

2. OP-TEE 的软硬件分层与最小系统选型

2.1 为什么不能直接在应用层调 OP-TEE

OP-TEE 的架构决定了它不是一个库,而是一套完整的安全世界运行时。它运行在 ARM 的 Secure World,与 Normal World 的 Linux 内核通过 SMC(Secure Monitor Call)指令交互。这意味着你的 CA 程序不能像调普通动态库那样直接dlopen一个 TA,而是必须经过/dev/tee0设备节点,由内核态的 OP-TEE 驱动转发请求到安全世界。

这个分层带来的直接后果是:任何一环缺失,整个链路就断了。常见做法是先在 QEMU 上跑通完整链路,再迁移到真实硬件。QEMU 的好处是它模拟了 ARM TrustZone 的完整环境,包括 ATF(ARM Trusted Firmware)、OP-TEE OS、Linux 内核和根文件系统,你不需要真实的开发板就能验证 TA/CA 的交互逻辑。

我一般会建议按这个顺序选型:

层级QEMU 方案真实硬件方案选型理由
安全世界 OSOP-TEE OS (QEMU 平台)OP-TEE OS (厂商移植版)QEMU 版开箱即用,硬件版需确认厂商是否提供
固件ATF + OP-TEE厂商 BL31 + OP-TEEQEMU 自带 ATF,硬件需确认 BL31 是否集成
内核Linux 5.x + OP-TEE driver厂商内核 + OP-TEE driver主线内核已含 OP-TEE driver,硬件需确认 config
用户态optee_client + xtest同左标准工具链,版本需与 OS 匹配

选 QEMU 的核心原因是:你可以在半天内跑通一个完整的 TA 加载和调用流程,而不需要等硬件、等厂商 BSP、等驱动适配。对于 OP-TEE_my_test 这种验证性工程,时间应该花在理解 TA/CA 交互协议上,而不是花在板子起不来上。

2.2 最小系统的三个必调参数

在 QEMU 上跑 OP-TEE,有三个参数直接决定你能不能进到安全世界:

第一个是CFG_TEE_CORE_LOG_LEVEL。这个参数控制 OP-TEE OS 的日志级别,默认是 1(仅错误),调试时建议改成 3 或 4。日志会通过串口输出,你能看到 TA 加载时的详细过程,包括 TA 的 UUID、加载地址、签名验证结果。如果 TA 加载失败,这里会直接告诉你原因。

第二个是CFG_TEE_TA_LOG_LEVEL。这是 TA 内部的日志级别,默认也是 1。当你的 TA 代码里有EMSG或DMSG输出时,这个参数决定它们是否可见。调试 TA 逻辑时,把它设成 4,能看到 TA 内部每一步的执行情况。

第三个是CFG_TEE_CLIENT_LOG_LEVEL。这是 CA 侧 optee_client 库的日志级别。当 CA 调用TEEC_OpenSession返回错误时,这个日志能告诉你错误发生在哪一层——是驱动层、是 SMC 调用层、还是 TA 内部返回的错误码。

这三个参数在编译 OP-TEE OS、optee_client 和 TA 时分别生效,缺一不可。很多新手只改了 OS 的日志级别,结果 TA 内部的DMSG死活看不到,就是因为漏了CFG_TEE_TA_LOG_LEVEL。

2.3 从源码到镜像:QEMU 环境的构建步骤

假设你已经装好了 ARM 交叉编译工具链(gcc-arm-linux-gnueabihf和aarch64-linux-gnu-gcc),并且有基本的 Linux 内核编译经验。以下是构建 QEMU OP-TEE 环境的最小步骤:

# 1. 创建工作目录 mkdir -p ~/optee-qemu && cd ~/optee-qemu # 2. 拉取 OP-TEE 官方构建仓库(使用 repo 工具) repo init -u https://github.com/OP-TEE/manifest.git -m qemu_v8.xml repo sync -j4 # 3. 安装依赖(Ubuntu 环境) sudo apt-get install android-tools-adb android-tools-fastboot \ autoconf automake bc bison build-essential cscope curl \ device-tree-compiler expect flex ftp-upload gdisk \ iasl libattr1-dev libc6:i386 libcap-dev libfdt-dev \ libftdi-dev libglib2.0-dev libgmp3-dev libhidapi-dev \ libmpc-dev libncurses5-dev libpixman-1-dev libssl-dev \ libtool make mtools netcat python3-crypto python3-pip \ python3-pyelftools python3-serial python3-wand \ rsync unzip uuid-dev xdg-utils xterm xz-utils zlib1g-dev # 4. 编译完整环境(首次编译约 30-60 分钟) make -j$(nproc) all # 5. 启动 QEMU make run

编译完成后,QEMU 会启动一个完整的 ARM 虚拟平台,包括 ATF、OP-TEE OS、Linux 内核和 BusyBox 根文件系统。登录后你会看到optee相关的设备节点已经就绪:

# 在 QEMU 的 Linux 终端中执行 ls -l /dev/tee* # 应输出: # crw------- 1 root root 10, 60 Jan 1 00:00 /dev/tee0 # crw------- 1 root root 10, 61 Jan 1 00:00 /dev/teepriv0

/dev/tee0是 CA 与 OP-TEE 通信的主设备节点,/dev/teepriv0是特权设备节点,通常用于 TEE Supplicant。如果这两个节点不存在,说明内核态的 OP-TEE 驱动没有加载成功,需要检查内核 config 中CONFIG_TEE和CONFIG_OPTEE是否开启。

2.4 验证 OP-TEE 是否真正跑通

设备节点存在不代表 OP-TEE 真正可用。最直接的验证方式是跑xtest:

# 在 QEMU 的 Linux 终端中执行 xtest # 如果 OP-TEE 正常,会输出大量测试用例的执行结果 # 最后一行通常是: # 12345 subtests of 12345 passed

如果xtest跑不过,或者卡在某个用例上,说明 OP-TEE 的某个子系统有问题。常见的是xtest在regression_1000或regression_2000系列失败,这通常指向 TA 加载或内存管理的问题。此时需要打开 OP-TEE OS 的详细日志,重新编译并启动 QEMU,观察串口输出中 TA 加载时的错误信息。

3. 编写 OP-TEE_my_test 的 TA 与 CA

3.1 TA 的最小骨架与 UUID 生成

TA 是运行在安全世界的程序,它有自己的入口函数、堆栈和内存空间。一个最小的 TA 需要包含以下部分:

/* my_test_ta.c - OP-TEE_my_test 的 TA 实现 */ #include <tee_internal_api.h> #include <tee_internal_api_extensions.h> /* TA 的 UUID,必须全局唯一,由 uuidgen 生成 */ #define TA_MY_TEST_UUID \ { 0x12345678, 0x9abc, 0xdef0, \ { 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0 } } /* TA 的入口函数,OP-TEE OS 加载 TA 时调用 */ TEE_Result TA_CreateEntryPoint(void) { DMSG("TA_CreateEntryPoint: OP-TEE_my_test TA loaded"); return TEE_SUCCESS; } /* TA 的退出函数,TA 被卸载时调用 */ void TA_DestroyEntryPoint(void) { DMSG("TA_DestroyEntryPoint: OP-TEE_my_test TA unloaded"); } /* 打开会话,CA 调用 TEEC_OpenSession 时触发 */ TEE_Result TA_OpenSessionEntryPoint(uint32_t param_types, TEE_Param params[4], void **sess_ctx) { uint32_t exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_NONE, TEE_PARAM_TYPE_NONE, TEE_PARAM_TYPE_NONE, TEE_PARAM_TYPE_NONE); if (param_types != exp_param_types) return TEE_ERROR_BAD_PARAMETERS; (void)&params; (void)&sess_ctx; DMSG("TA_OpenSessionEntryPoint: session opened"); return TEE_SUCCESS; } /* 关闭会话 */ void TA_CloseSessionEntryPoint(void *sess_ctx) { (void)&sess_ctx; DMSG("TA_CloseSessionEntryPoint: session closed"); } /* 命令处理函数,CA 调用 TEEC_InvokeCommand 时触发 */ TEE_Result TA_InvokeCommandEntryPoint(void *sess_ctx, uint32_t cmd_id, uint32_t param_types, TEE_Param params[4]) { (void)&sess_ctx; (void)&param_types; (void)&params; switch (cmd_id) { case 0: /* 自定义命令:返回一个固定值 */ DMSG("TA_InvokeCommandEntryPoint: cmd 0 invoked"); return TEE_SUCCESS; default: return TEE_ERROR_NOT_SUPPORTED; } }

这段代码里最关键的是TA_MY_TEST_UUID。UUID 是 CA 找到 TA 的唯一标识,如果 CA 里的 UUID 和 TA 里的不一致,TEEC_OpenSession会直接返回TEEC_ERROR_ITEM_NOT_FOUND。生成 UUID 的常见做法是用uuidgen命令,然后把输出填进宏定义。注意 UUID 的格式是 8-4-4-4-12 的十六进制,在 C 代码里需要按TEE_UUID结构体的字段拆分。

另一个容易翻车的地方是param_types的校验。OP-TEE 对参数类型有严格检查,如果 CA 传的参数类型和 TA 期望的不一致,TA 会直接返回TEE_ERROR_BAD_PARAMETERS。新手经常在这里卡住,因为错误码不够直观,需要对照 OP-TEE 的文档逐个排查。

3.2 CA 侧调用流程与参数传递

CA 是运行在 Normal World 的用户态程序,它通过libteec库与 OP-TEE 通信。以下是最小 CA 的实现:

/* my_test_ca.c - OP-TEE_my_test 的 CA 实现 */ #include <stdio.h> #include <string.h> #include <tee_client_api.h> /* 必须与 TA 中的 UUID 完全一致 */ #define TA_MY_TEST_UUID \ { 0x12345678, 0x9abc, 0xdef0, \ { 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0 } } int main(void) { TEEC_Context ctx; TEEC_Session sess; TEEC_Operation op; TEEC_Result res; uint32_t err_origin; /* 1. 初始化 TEE 上下文,连接 /dev/tee0 */ res = TEEC_InitializeContext(NULL, &ctx); if (res != TEEC_SUCCESS) { printf("TEEC_InitializeContext failed: 0x%x\n", res); return 1; } /* 2. 打开与 TA 的会话 */ memset(&op, 0, sizeof(op)); op.paramTypes = TEEC_PARAM_TYPES(TEEC_NONE, TEEC_NONE, TEEC_NONE, TEEC_NONE); res = TEEC_OpenSession(&ctx, &sess, &(const TEEC_UUID)TA_MY_TEST_UUID, TEEC_LOGIN_PUBLIC, NULL, &op, &err_origin); if (res != TEEC_SUCCESS) { printf("TEEC_OpenSession failed: 0x%x, origin: 0x%x\n", res, err_origin); TEEC_FinalizeContext(&ctx); return 1; } /* 3. 调用 TA 的命令 0 */ memset(&op, 0, sizeof(op)); op.paramTypes = TEEC_PARAM_TYPES(TEEC_NONE, TEEC_NONE, TEEC_NONE, TEEC_NONE); res = TEEC_InvokeCommand(&sess, 0, &op, &err_origin); if (res != TEEC_SUCCESS) { printf("TEEC_InvokeCommand failed: 0x%x, origin: 0x%x\n", res, err_origin); } else { printf("OP-TEE_my_test: command 0 executed successfully\n"); } /* 4. 关闭会话并释放上下文 */ TEEC_CloseSession(&sess); TEEC_FinalizeContext(&ctx); return 0; }

CA 侧的核心是TEEC_OpenSession和TEEC_InvokeCommand两个调用。TEEC_OpenSession的第三个参数是 TA 的 UUID,第四个参数是登录类型,TEEC_LOGIN_PUBLIC表示不需要用户身份验证。如果你的 TA 需要访问安全存储或硬件密钥,可能需要改成TEEC_LOGIN_USER或其他类型。

err_origin是一个容易被忽略的参数。当TEEC_OpenSession或TEEC_InvokeCommand返回错误时,err_origin会告诉你错误发生在哪一层:TEEC_ORIGIN_COMMS表示通信层错误,TEEC_ORIGIN_TEE表示安全世界内部错误,TEEC_ORIGIN_TRUSTED_APP表示 TA 内部返回的错误。这个信息对排查问题至关重要。

3.3 编译脚本与 TA 签名

TA 在加载到安全世界之前需要签名,OP-TEE 使用 RSA 或 ECDSA 对 TA 进行签名验证。在 QEMU 环境中,通常使用测试密钥。以下是一个最小的 TA 编译 Makefile:

# Makefile - OP-TEE_my_test TA 编译 CFG_TEE_TA_LOG_LEVEL ?= 4 CFG_TEE_CORE_LOG_LEVEL ?= 3 # TA 的源文件和 UUID BINARY = my_test_ta OBJS = my_test_ta.o # 包含 OP-TEE TA 的公共 Makefile include $(TA_DEV_KIT_DIR)/mk/ta_dev_kit.mk

编译时需要指定TA_DEV_KIT_DIR,它指向 OP-TEE OS 编译后生成的 TA 开发套件目录:

# 在 OP-TEE_my_test 目录下执行 make TA_DEV_KIT_DIR=~/optee-qemu/optee_os/out/arm/export-ta_arm32 \ CROSS_COMPILE=arm-linux-gnueabihf- \ CFG_TEE_TA_LOG_LEVEL=4

编译成功后会生成my_test_ta.ta文件,这是一个签名后的 TA 二进制。把它拷贝到 QEMU 根文件系统的/lib/optee_armtz/目录下,CA 就能通过 UUID 找到它。

CA 的编译更简单,只需要链接libteec:

arm-linux-gnueabihf-gcc -o my_test_ca my_test_ca.c \ -I~/optee-qemu/optee_client/out/export/usr/include \ -L~/optee-qemu/optee_client/out/export/usr/lib \ -lteec

把my_test_ca和my_test_ta.ta一起拷贝到 QEMU 的根文件系统,运行 CA,如果一切正常,你会看到:

OP-TEE_my_test: command 0 executed successfully

同时在 QEMU 的串口日志中,能看到 TA 内部的DMSG输出,证明 TA 确实被加载并执行了。

4. 避坑与排查:OP-TEE_my_test 最常见的五类翻车

4.1 TA 加载失败,返回 0xFFFF0002

现象:CA 调用TEEC_OpenSession返回0xFFFF0002,err_origin为TEEC_ORIGIN_COMMS。

原因:这个错误码对应TEE_ERROR_ITEM_NOT_FOUND,意思是 OP-TEE 在/lib/optee_armtz/目录下找不到与 UUID 匹配的 TA。常见原因有三个:TA 文件没有拷贝到正确目录、TA 文件名与 UUID 不匹配、TA 签名验证失败。

解决:首先确认 TA 文件确实在/lib/optee_armtz/下,并且文件名是<uuid>.ta的格式。OP-TEE 加载 TA 时,会先根据 UUID 查找文件名,然后验证签名。如果签名验证失败,日志中会有TA signature verification failed的输出。此时需要检查签名密钥是否与 OP-TEE OS 中内置的公钥匹配。在 QEMU 环境中,通常使用默认的测试密钥,如果你自己生成了新密钥,需要重新编译 OP-TEE OS 并替换公钥。

4.2 TA 加载成功但 InvokeCommand 返回 0xFFFF0006

现象:TEEC_OpenSession成功,但TEEC_InvokeCommand返回0xFFFF0006。

原因:这个错误码对应TEE_ERROR_BAD_PARAMETERS,意思是 CA 传递的参数类型或数量与 TA 期望的不一致。OP-TEE 对参数有严格校验,param_types的每一位对应一个参数的类型,如果 CA 声明了TEEC_VALUE_INPUT但 TA 期望TEEC_NONE,就会直接返回这个错误。

解决:对照 TA 中TA_InvokeCommandEntryPoint的exp_param_types和 CA 中op.paramTypes的设置,确保两者完全一致。注意TEE_PARAM_TYPES宏的参数顺序是param0, param1, param2, param3,而TEEC_PARAM_TYPES的顺序相同。如果 TA 期望TEE_PARAM_TYPE_VALUE_INPUT,CA 必须设置TEEC_VALUE_INPUT,并且op.params[0].value.a和op.params[0].value.b需要正确赋值。

4.3 串口日志中看不到 TA 的 DMSG 输出

现象:TA 加载成功,CA 调用也返回成功,但 QEMU 串口日志中看不到 TA 内部的DMSG输出。

原因:TA 的日志级别由CFG_TEE_TA_LOG_LEVEL控制,这个参数在编译 TA 时生效,而不是在编译 OP-TEE OS 时生效。很多新手只改了 OS 的日志级别,漏掉了 TA 的编译参数。

解决:在编译 TA 时显式指定CFG_TEE_TA_LOG_LEVEL=4,并确保 TA 的 Makefile 中包含了这个变量。另外,TA 的日志输出会经过 OP-TEE OS 的日志系统,如果 OS 的CFG_TEE_CORE_LOG_LEVEL太低,TA 的日志也可能被过滤掉。建议两个参数都设成 4,确保日志完整输出。

4.4 QEMU 启动后 /dev/tee0 不存在

现象:QEMU 启动成功,Linux 终端可用,但/dev/tee0设备节点不存在。

原因:内核态的 OP-TEE 驱动没有加载。可能的原因包括:内核 config 中没有开启CONFIG_OPTEE、设备树中没有 OP-TEE 节点、OP-TEE driver 的 probe 失败。

解决:首先检查内核 config,确认CONFIG_TEE=y和CONFIG_OPTEE=y已开启。然后检查设备树中是否有optee节点,QEMU 的qemu_v8.dts中应该包含:

firmware { optee { compatible = "linaro,optee-tz"; method = "smc"; }; };

如果设备树正确但驱动仍然 probe 失败,查看内核启动日志中的 OP-TEE 相关输出,通常会提示optee: probe failed或optee: unable to register driver。这可能是 ATF 没有正确初始化 TrustZone 环境,需要检查 ATF 的编译配置。

4.5 xtest 在 regression_1000 系列失败

现象:xtest运行到regression_1000系列时失败,错误信息通常是TA_OpenSession failed或InvokeCommand failed。

原因:regression_1000系列测试的是 TA 的加载和基本通信,失败通常意味着 OP-TEE 的 TA 加载机制有问题。常见原因是 TA 的签名密钥不匹配、TA 的依赖库缺失、或者 OP-TEE OS 的版本与xtest的版本不兼容。

解决:首先确认xtest和 OP-TEE OS 来自同一个 manifest 版本,版本不匹配是常见问题。然后检查/lib/optee_armtz/下的 TA 文件是否完整,特别是xtest依赖的bta_*系列 TA。如果签名密钥有问题,重新编译 OP-TEE OS 和所有 TA,确保使用同一套密钥。最后,如果问题仍然存在,打开 OP-TEE OS 的CFG_TEE_CORE_LOG_LEVEL=4,观察 TA 加载时的详细日志,通常会直接指出失败原因。

5. 从 OP-TEE_my_test 到真实业务 TA 的进阶路径

跑通 OP-TEE_my_test 只是第一步,它验证的是环境可用性。真正落地业务时,你需要在这个骨架上叠加安全存储、密钥管理、加密运算等能力。我一般会按这个顺序推进:

第一步是接入安全存储。OP-TEE 提供了TEE_OpenPersistentObject和TEE_WriteObjectData等 API,用于在 RPMB 或普通文件系统中存储安全数据。在 QEMU 环境中,安全存储通常映射到根文件系统的一个目录下。你需要先在 TA 中调用TEE_OpenPersistentObject创建一个对象,然后写入数据,再在 CA 中通过命令触发读写。这一步的关键是理解 OP-TEE 的对象 ID 和属性标志,比如TEE_DATA_FLAG_ACCESS_READ和TEE_DATA_FLAG_ACCESS_WRITE必须正确设置,否则会返回TEE_ERROR_ACCESS_CONFLICT。

第二步是接入硬件密钥。如果你的 SoC 有硬件密钥模块(如 OTP 或 HUK),OP-TEE 可以通过TEE_GenerateKey或TEE_DeriveKey从硬件密钥派生会话密钥。这一步需要厂商的 BSP 支持,QEMU 环境中通常用软件模拟。我一般会先在 QEMU 上验证密钥派生逻辑,再迁移到硬件。迁移时需要注意,硬件密钥的派生算法和参数可能与软件模拟不同,需要对照厂商文档调整。

第三步是做性能验证。OP-TEE 的 SMC 调用有固定开销,每次 CA 到 TA 的切换大约在几十微秒量级。如果你的业务需要频繁调用 TA,比如每秒钟几千次加密操作,这个开销会累积成瓶颈。我一般会在 TA 中实现批量处理接口,一次调用处理多个数据块,减少 SMC 切换次数。另外,TA 内部的堆栈大小有限,默认通常是 8KB 或 16KB,如果处理大块数据,需要在 TA 的 Makefile 中调整TA_STACK_SIZE和TA_HEAP_SIZE。

第四步是验证签名和回滚保护。真实业务中,TA 的签名密钥不能是测试密钥,必须使用生产密钥。同时,OP-TEE 支持 TA 的版本回滚保护,通过TEE_CheckRevocation等 API 可以防止旧版本 TA 被加载。这一步在 QEMU 上验证时,需要手动生成生产密钥对,并重新编译 OP-TEE OS 和所有 TA。我踩过的坑是:生产密钥的权限设置不当,导致 OP-TEE OS 编译时无法读取私钥,编译直接失败。后来我把密钥文件的权限改成 600,并且确保编译用户有读取权限,问题才解决。

最后说一个我自己的习惯:每次修改 TA 或 CA 后,先跑一遍xtest的regression_1000系列,确认基础通信没有退化,再跑自己的测试用例。这个习惯帮我省了很多次「改了 A 结果 B 挂了」的后悔药。OP-TEE 的调试链路比较长,从 CA 到内核驱动到 ATF 到安全世界,任何一环出问题都会表现为上层的一个错误码。保持一个最小可用的基线测试,是快速定位问题的关键。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询