TEE-TA学习轨迹第八篇:optee_os源码下TA分析之-app_secrets
应用严格遵循 GlobalPlatform TEE Internal API 规范,是典型的「设备绑定+身份绑定」型安全密封 TA。
一、基础定义:常量与核心数据结构
1. 宏常量定义
#define IV_SIZE 12 // AES-GCM 推荐 IV 长度(96bit) #define TAG_SIZE 16 // GCM 认证标签长度(128bit) #define MAX_BUF_SIZE 4096 // 单条命令最大处理数据量 #define AS_BLOB_VERSION 1 // 密封 Blob 格式版本号 #define AS_MAGIC 0x41534543 // Blob 魔数(ASCII: ASEC)- 作用:统一加密参数、限制内存使用、做格式合法性校验;魔数和版本号用于解封时快速识别非法/不兼容的 Blob。
- 对CA的意义:CA 传入的明文不能超过 MAX_BUF_SIZE - 密封开销,否则会被直接拒绝。
2. 核心数据结构
① 密封后输出结构secret_blob_hdr
struct secret_blob_hdr { uint32_t magic; // 魔数,固定 0x41534543 uint32_t version; // Blob 格式版本 uint8_t iv[IV_SIZE]; // 本次加密随机生成的 IV uint8_t tag[TAG_SIZE]; // AES-GCM 认证标签 uint8_t encrypted_payload[]; // 柔性数组:加密后的载荷 };这是 CA 拿到的「密封产物」的完整结构,CA 可以直接把整个结构体当作二进制数据存入文件系统、数据库等非安全存储区。
② 加密前明文载荷结构plaintext_payload
struct plaintext_payload { uint32_t client_login; // 调用方 CA 的登录类型 TEE_UUID client_uuid; // 调用方 CA 的 UUID uint8_t data[]; // 柔性数组:用户传入的原始敏感数据 };核心设计:加密时把「CA 身份信息」和「用户敏感数据」绑定在一起加密,解封时校验身份,实现「谁密封、谁解封」的访问控制。
二、底层能力:密钥派生与密码运算
1. 设备唯一密钥派生derive_unique_key
static TEE_Result derive_unique_key(void *key, size_t key_size, const void *extra, size_t extra_size) { static const TEE_UUID system_uuid = PTA_SYSTEM_UUID; // 打开系统 PTA 会话 res = TEE_OpenTASession(&system_uuid, ...); // 调用派生 TA 唯一密钥的命令 res = TEE_InvokeTACommand(sess, ..., PTA_SYSTEM_DERIVE_TA_UNIQUE_KEY, ...); TEE_CloseTASession(sess); return res; }代码逻辑与安全意义
- 调用系统内置 PTA(伪 TA,运行在内核态)的 DERIVE_TA_UNIQUE_KEY 接口;
- 基于设备硬件根密钥 HUK+当前 TA 的 UUID+ 传入的 extra 因子,派生出唯一密钥;
- 传入的 extra 固定为字符串 "sealing",确保该密钥仅用于密封/解封场景,和其他业务密钥隔离。
对CA的价值
- 设备绑定:只有同一台设备的同一个 TA,才能派生出相同的密钥;Blob 拷贝到其他设备无法解密。
- 密钥零硬编码:密钥全程在安全世界内派生、使用,不会出现在源码、固件、存储中。
2. AES-GCM 加密实现huk_ae_encrypt
static TEE_Result huk_ae_encrypt(TEE_OperationHandle crypto_op, const uint8_t *in, size_t in_sz, uint8_t *out, size_t *out_sz)逐段执行逻辑
1.初始化 Blob 头部:写入魔数、版本号,标记这是合法的密封格式。
2.获取调用方身份:
res = TEE_GetPropertyAsIdentity(TEE_PROPSET_CURRENT_CLIENT, "gpd.client.identity", &id);调用 GP 标准接口,安全获取当前调用 TA 的 CA 的身份(登录类型 + UUID),身份信息来自 OP-TEE 内核,CA 无法伪造。
3.生成随机 IV:TEE_GenerateRandom(hdr->iv, IV_SIZE),每次加密使用全新随机 IV,防止重放攻击。
4.初始化 AE 运算:设置 IV、标签长度,将 magic、version 作为AAD(附加认证数据)参与认证计算——头部数据不加密,但被完整性保护,篡改后标签校验直接失败。
5.构造明文载荷:把「CA 身份 + 用户明文」拼接成完整的 plaintext_payload。
6.执行 GCM 加密:TEE_AEEncryptFinal 输出密文和认证标签,写入 Blob 对应位置。
7.安全清理:memzero_explicit 强制清零临时明文缓冲区,防止编译器优化掉清零操作,避免敏感数据残留在安全世界内存中。
3. AES-GCM 解密实现huk_ae_decrypt
static TEE_Result huk_ae_decrypt(TEE_OperationHandle crypto_op, const uint8_t *in, size_t in_sz, uint8_t *out, size_t *out_sz)逐段执行逻辑
- 前置合法性校验:先检查魔数、版本号,非法格式直接返回 TEE_ERROR_SECURITY,不进入密码运算,减少攻击面。
- 初始化解密运算:使用 Blob 中的 IV,同样把魔数、版本号作为 AAD。
- 解密+完整性校验:TEE_AEDecryptFinal 同时完成密文解密和标签校验;只要 Blob 任何一位被篡改,标签校验失败,直接返回安全错误,不会输出明文。
- 解密成功后输出原始明文载荷(包含 CA 身份 + 用户数据),后续由上层做身份校验。
4. 加解密统一入口huk_crypt
static TEE_Result huk_crypt(TEE_OperationMode mode, const uint8_t *in, size_t in_sz, uint8_t *out, size_t *out_sz)这是密封/解封的公共底层封装,完整执行「分配资源 → 派生密钥 → 创建密码对象 → 执行加解密 → 全链路资源清理」的流程。
关键安全细节
- 内存安全:所有临时缓冲区都用 TEE_MALLOC_FILL_ZERO 分配,分配即清零;使用后强制清零再释放。
- 整数溢出防护:所有长度计算都用 ADD_OVERFLOW / SUB_OVERFLOW 宏检查溢出,溢出直接返回安全错误,杜绝堆溢出漏洞。
- 密钥零残留:派生出来的密钥 huk_key,在函数退出前必须 memzero_explicit 清零,即使 TA 后续被攻击,内存中也不会残留明文密钥。
- 算法固定:强制使用 TEE_ALG_AES_GCM,不允许 CA 指定算法,避免弱算法风险。
三、对外服务:CA 可调用的两个命令接口
TA 通过 TA_InvokeCommandEntryPoint 做命令分发,CA 端通过 TEEC_InvokeCommand 传入命令 ID,即可调用对应服务。
服务1:密封秘密TA_APPSECRETS_CMD_SEAL_SECRET
对应处理函数 seal_secret
CA 调用方式
- 输入参数:params[0] = 明文敏感数据(内存引用)
- 输出参数:params[1] = 密封后的完整 Blob(内存引用)
代码执行流程
1.参数合法性校验
if (types != TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_INPUT, TEE_PARAM_TYPE_MEMREF_OUTPUT, ...)) return TEE_ERROR_BAD_PARAMETERS;严格校验参数类型,不符合 GP 规范直接拒绝,防止非法参数触发异常。
2.长度校验:输入数据不能超过上限,输出缓冲区不足时返回 TEE_ERROR_SHORT_BUFFER 并写入所需长度——CA 可以先传空缓冲区查询大小,再分配足够内存二次调用,这是 TEE 标准的缓冲区协商模式。
3.调用底层加密:huk_crypt(TEE_MODE_ENCRYPT, ...) 完成身份绑定 + AES-GCM 加密。
4.返回结果:设置输出缓冲区的实际大小,返回 TEE_SUCCESS,CA 即可拿到可安全存储的 Blob。
对CA的价值
CA 可以把任何敏感数据(密码、令牌、私钥片段、配置项)传给 TA,得到一个「只能在本设备、被本CA解封」的加密包,放心存入 Android 本地存储、SP、数据库等非安全区域。
服务2:解封秘密TA_APPSECRETS_CMD_UNSEAL_SECRET
对应处理函数 unseal_secret
CA 调用方式
- 输入参数:params[0] = 之前密封得到的 Blob
- 输出参数:params[1] = 解密后的原始明文
代码执行流程
- 参数与长度校验:同密封接口,同时校验 Blob 最小长度必须大于头部大小。
- 获取当前调用者身份:再次调用 TEE_GetPropertyAsIdentity 拿到当前 CA 的真实身份。
- 调用底层解密:huk_crypt(TEE_MODE_DECRYPT, ...) 完成完整性校验和解密,得到 plaintext_payload 明文载荷。
- 核心:身份绑定校验
if (payload->client_login != id.login || TEE_MemCompare(&payload->client_uuid, &id.uuid, sizeof(TEE_UUID))) { DMSG("Client identity mismatch"); res = TEE_ERROR_SECURITY; goto out; }对比「密封时写入的CA身份」和「当前调用的CA身份」,完全一致才允许解封。
- 即使别的 CA 拿到了 Blob 文件,调用解封接口也会直接被拒绝;
- 即使 CA 被篡改、伪造身份,OP-TEE 内核返回的真实身份也不会变,校验无法绕过。
- 提取用户数据:从载荷中取出原始敏感数据,拷贝到 CA 的输出缓冲区。
- 安全清理:清零解密后的临时内存,释放资源后返回。
对CA的价值
CA 可以安全地取回自己之前密封的敏感数据,同时保证:
- Blob 被篡改 → 解密失败;
- 其他应用/CA 拿到 Blob → 身份校验失败,无法解密;
- Blob 被拷贝到其他设备 → 密钥不匹配,无法解密。
四、TA 生命周期入口(GP 标准接口)
这份 TA 实现了 GlobalPlatform 规定的 5 个标准入口函数:
// TA 首次被加载到安全世界时调用,全局初始化 TEE_Result TA_CreateEntryPoint(void) { return TEE_SUCCESS; } // TA 被卸载前调用,全局资源清理 void TA_DestroyEntryPoint(void) {} // CA 打开会话时调用,会话级初始化 TEE_Result TA_OpenSessionEntryPoint(...) { return TEE_SUCCESS; } // CA 关闭会话时调用,会话级清理 void TA_CloseSessionEntryPoint(void *sess) {} // CA 调用命令时的分发入口 TEE_Result TA_InvokeCommandEntryPoint(..., uint32_t cmd, ...) { switch (cmd) { case TA_APPSECRETS_CMD_SEAL_SECRET: return seal_secret(pt, params); case TA_APPSECRETS_CMD_UNSEAL_SECRET: return unseal_secret(pt, params); default: return TEE_ERROR_NOT_SUPPORTED; } }- 该 TA 是无状态设计:不保存会话上下文、不缓存密钥,每次调用都是独立的,安全性更高,不会因为会话残留导致信息泄露。
- 不支持的命令 ID 直接返回错误,避免非法命令探测。
五、总结:CA 获得的完整安全能力
从 CA 开发者视角,这份 TA 最终提供了两项可直接调用的安全服务:
命令ID | 服务名称 | CA 输入 | CA 输出 | 核心安全保障 |
TA_APPSECRETS_CMD_SEAL_SECRET | 敏感数据密封 | 明文敏感数据 | 加密后的密封 Blob | 设备绑定、AES-GCM 机密性+完整性、CA身份嵌入 |
TA_APPSECRETS_CMD_UNSEAL_SECRET | 敏感数据解封 | 密封 Blob | 原始明文数据 | 完整性校验、CA身份强绑定、设备绑定、防篡改 |
典型 Android 应用场景
- 应用登录令牌、支付密钥的本地安全存储;
- 多应用隔离场景下,每个应用自己密封自己的敏感数据,互不访问;
- 替代 Android Keystore 的自定义密封方案,可灵活扩展业务逻辑。
五、CA 完整源码app_secrets.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <tee_client_api.h> #define APP_SECRETS_TA_UUID \ { 0x5ca4d9d9, 0xdee4, 0x47f4, \ { 0x97, 0x7a, 0x7e, 0xad, 0xc0, 0x60, 0xe5, 0x2c } } /* * Seal secret using hardware unique TA specific key * * [in] memref[0] Plain secret * [out] memref[1] Sealed secret datablob */ #define TA_APPSECRETS_CMD_SEAL_SECRET 0x0 /* * Unseal secret using hardware unique TA specific key * * [in] memref[0] Sealed secret datablob * [out] memref[1] Plain secret */ #define TA_APPSECRETS_CMD_UNSEAL_SECRET 0x1 /* TA 唯一标识符 UUID,必须与 user_ta_header_defines.h 中 TA_UUID 一字不差 */ #define TA_APPSECRETS_UUID \ { 0x5ca4d9d9, 0xdee4, 0x47f4, \ { 0x97, 0x7a, 0x7e, 0xad, 0xc0, 0x60, 0xe5, 0x2c } } /********************************************************************* * 辅助工具:打印 TEE 错误信息 *********************************************************************/ static void print_teec_error(TEEC_Result res, const char *op_name, uint32_t origin) { printf("[ERROR] %s failed\n", op_name); printf(" Error code: 0x%08x\n", res); printf(" Error origin: %u (3=TA返回, 2=TEE内核, 1=驱动)\n", origin); } /********************************************************************* * 服务1:调用 TA 密封敏感数据 * 输入:明文数据指针 + 长度 * 输出:密封后的 Blob 指针 + 长度(调用方需自行 free) *********************************************************************/ static TEEC_Result ta_seal_secret(TEEC_Session *session, const uint8_t *plain_in, size_t plain_len, uint8_t **sealed_out, size_t *sealed_len) { TEEC_Result res; TEEC_Operation op; uint32_t ret_origin = 0; size_t required_size = 0; memset(&op, 0, sizeof(op)); /* 参数类型:第0个=输入内存引用, 第1个=输出内存引用, 后两个未使用 */ op.paramTypes = TEEC_PARAM_TYPES( TEEC_MEMREF_TEMP_INPUT, TEEC_MEMREF_TEMP_OUTPUT, TEEC_NONE, TEEC_NONE ); /* ---------- 第一步:查询密封所需缓冲区大小 ---------- */ op.params[0].tmpref.buffer = (void *)plain_in; op.params[0].tmpref.size = plain_len; op.params[1].tmpref.buffer = NULL; op.params[1].tmpref.size = 0; res = TEEC_InvokeCommand(session, TA_APPSECRETS_CMD_SEAL_SECRET, &op, &ret_origin); /* 正常情况下会返回 SHORT_BUFFER,同时带出所需大小 */ if (res != TEEC_ERROR_SHORT_BUFFER) { print_teec_error(res, "Query seal size", ret_origin); return res; } required_size = op.params[1].tmpref.size; /* ---------- 第二步:分配内存,正式执行密封 ---------- */ *sealed_out = (uint8_t *)malloc(required_size); if (!*sealed_out) { printf("[ERROR] Allocate sealed buffer failed\n"); return TEEC_ERROR_OUT_OF_MEMORY; } op.params[1].tmpref.buffer = *sealed_out; op.params[1].tmpref.size = required_size; res = TEEC_InvokeCommand(session, TA_APPSECRETS_CMD_SEAL_SECRET, &op, &ret_origin); if (res != TEEC_SUCCESS) { print_teec_error(res, "Seal secret", ret_origin); free(*sealed_out); *sealed_out = NULL; return res; } *sealed_len = op.params[1].tmpref.size; return TEEC_SUCCESS; } /********************************************************************* * 服务2:调用 TA 解封敏感数据 * 输入:密封 Blob 指针 + 长度 * 输出:明文数据指针 + 长度(调用方需自行 free) *********************************************************************/ static TEEC_Result ta_unseal_secret(TEEC_Session *session, const uint8_t *sealed_in, size_t sealed_len, uint8_t **plain_out, size_t *plain_len) { TEEC_Result res; TEEC_Operation op; uint32_t ret_origin = 0; size_t required_size = 0; memset(&op, 0, sizeof(op)); op.paramTypes = TEEC_PARAM_TYPES( TEEC_MEMREF_TEMP_INPUT, TEEC_MEMREF_TEMP_OUTPUT, TEEC_NONE, TEEC_NONE ); /* ---------- 第一步:查询解封所需缓冲区大小 ---------- */ op.params[0].tmpref.buffer = (void *)sealed_in; op.params[0].tmpref.size = sealed_len; op.params[1].tmpref.buffer = NULL; op.params[1].tmpref.size = 0; res = TEEC_InvokeCommand(session, TA_APPSECRETS_CMD_UNSEAL_SECRET, &op, &ret_origin); if (res != TEEC_ERROR_SHORT_BUFFER) { print_teec_error(res, "Query unseal size", ret_origin); return res; } required_size = op.params[1].tmpref.size; /* ---------- 第二步:分配内存,正式执行解封 ---------- */ *plain_out = (uint8_t *)malloc(required_size); if (!*plain_out) { printf("[ERROR] Allocate plain buffer failed\n"); return TEEC_ERROR_OUT_OF_MEMORY; } op.params[1].tmpref.buffer = *plain_out; op.params[1].tmpref.size = required_size; res = TEEC_InvokeCommand(session, TA_APPSECRETS_CMD_UNSEAL_SECRET, &op, &ret_origin); if (res != TEEC_SUCCESS) { print_teec_error(res, "Unseal secret", ret_origin); free(*plain_out); *plain_out = NULL; return res; } *plain_len = op.params[1].tmpref.size; return TEEC_SUCCESS; } /********************************************************************* * 主函数:完整演示密封 + 解封 + 一致性校验 *********************************************************************/ int main(void) { TEEC_Result res; TEEC_Context teec_ctx; TEEC_Session teec_sess; TEEC_UUID ta_uuid = TA_APPSECRETS_UUID; uint32_t ret_origin = 0; /* 测试用敏感数据 */ const char *test_secret = "My_P@ssw0rd_123!_sensitive_data"; uint8_t *sealed_data = NULL; size_t sealed_len = 0; uint8_t *unsealed_data = NULL; size_t unsealed_len = 0; printf("===== AppSecrets TA 调用演示 =====\n"); printf("原始明文: %s\n\n", test_secret); /* 1. 初始化 TEE 上下文(与驱动建立连接) */ res = TEEC_InitializeContext(NULL, &teec_ctx); if (res != TEEC_SUCCESS) { print_teec_error(res, "TEEC_InitializeContext", 0); return -1; } /* 2. 打开 TA 会话 * 注意:登录类型会影响 TA 端获取的 client 身份 * 密封和解封必须使用相同的登录方式,否则身份校验失败 */ res = TEEC_OpenSession(&teec_ctx, &teec_sess, &ta_uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, &ret_origin); if (res != TEEC_SUCCESS) { print_teec_error(res, "TEEC_OpenSession", ret_origin); goto exit_ctx; } printf("[OK] 成功打开 AppSecrets TA 会话\n\n"); /* 3. 调用密封服务 */ res = ta_seal_secret(&teec_sess, (const uint8_t *)test_secret, strlen(test_secret), &sealed_data, &sealed_len); if (res != TEEC_SUCCESS) goto exit_sess; printf("[OK] 密封成功,密封后大小: %zu 字节\n", sealed_len); printf("密封数据前16字节(HEX): "); for (size_t i = 0; i < (sealed_len > 16 ? 16 : sealed_len); i++) printf("%02x ", sealed_data[i]); printf("\n\n"); /* 4. 调用解封服务 */ res = ta_unseal_secret(&teec_sess, sealed_data, sealed_len, &unsealed_data, &unsealed_len); if (res != TEEC_SUCCESS) goto exit_seal; printf("[OK] 解封成功,明文大小: %zu 字节\n", unsealed_len); printf("解封后明文: %.*s\n\n", (int)unsealed_len, unsealed_data); /* 5. 一致性校验 */ if (unsealed_len == strlen(test_secret) && memcmp(unsealed_data, test_secret, unsealed_len) == 0) { printf("校验通过:密封-解封前后数据完全一致\n"); } else { printf("校验失败:数据不匹配\n"); } exit_seal: free(sealed_data); free(unsealed_data); exit_sess: TEEC_CloseSession(&teec_sess); exit_ctx: TEEC_FinalizeContext(&teec_ctx); return res == TEEC_SUCCESS ? 0 : -1; }六、关键代码说明
1. 接口严格对齐 TA 端
- 参数类型完全匹配:统一使用 TEEC_MEMREF_TEMP_INPUT + TEEC_MEMREF_TEMP_OUTPUT,对应 TA 端的 MEMREF_INPUT + MEMREF_OUTPUT,不会触发 TEE_ERROR_BAD_PARAMETERS。
- 两次调用机制:先传空缓冲区查询所需大小,分配内存后再正式调用,完全适配 TA 端的 TEE_ERROR_SHORT_BUFFER 协商逻辑。
2. 身份绑定注意事项
TA 端会把调用方 CA 的身份(登录类型 + UUID)绑定到密封数据中,因此:
- 密封和解封必须使用相同的登录类型(示例中为 TEEC_LOGIN_PUBLIC);
- 如果切换登录方式(如 TEEC_LOGIN_USER、TEEC_LOGIN_APPLICATION),之前密封的数据会因身份不匹配无法解封。
3. 错误定位
ret_origin 字段可以直接定位错误来源:
- origin=3:错误由 TA 业务逻辑返回(如身份不匹配、篡改校验失败);
- origin=2:错误由 OP-TEE 内核返回(如 TA 加载失败、资源不足);
- origin=1:错误由 Linux 驱动返回(如设备节点异常、SMC 调用失败)。
七、编译与运行方法
1. 交叉编译命令(适配你的 aarch64 QEMU 环境)
aarch64-linux-gnu-gcc app_secrets.c -o app_secrets \ -I../optee_client/out/arm64/export/usr/include \ -L../optee_client/out/arm64/export/usr/lib \ -lteec- -I 指定 libteec 头文件路径
- -L 指定 libteec 库文件路径
- -lteec 链接 OP-TEE 客户端库
2. 部署到根文件系统
# 回到宿主机挂载镜像 cd firmware mount bootdisk.img rootfs_mnt # 把编译好的 CA 复制到 /root 目录 cp app_secrets_ca rootfs_mnt/root/ chmod +x rootfs_mnt/root/app_secrets # 安全卸载 sync && umount rootfs_mnt3. QEMU 内运行前提
- 确保 tee-supplicant & 已后台启动;
- 确保 /lib/optee_armtz/ 下存在正确命名的 TA 文件(UUID 命名的 .ta 文件);
- 执行测试:
