TEE-TA学习轨迹第八篇:optee_os源码下TA分析之-app_secrets

📅 2026/6/30 23:33:39
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_hdrstruct 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_payloadstruct plaintext_payload { uint32_t client_login; // 调用方 CA 的登录类型 TEE_UUID client_uuid; // 调用方 CA 的 UUID uint8_t data[]; // 柔性数组用户传入的原始敏感数据 };核心设计加密时把「CA 身份信息」和「用户敏感数据」绑定在一起加密解封时校验身份实现「谁密封、谁解封」的访问控制。二、底层能力密钥派生与密码运算1. 设备唯一密钥派生derive_unique_keystatic 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_encryptstatic 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.生成随机 IVTEE_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_decryptstatic 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_cryptstatic 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_secretCA 调用方式输入参数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_SUCCESSCA 即可拿到可安全存储的 Blob。对CA的价值CA 可以把任何敏感数据密码、令牌、私钥片段、配置项传给 TA得到一个「只能在本设备、被本CA解封」的加密包放心存入 Android 本地存储、SP、数据库等非安全区域。服务2解封秘密TA_APPSECRETS_CMD_UNSEAL_SECRET对应处理函数 unseal_secretCA 调用方式输入参数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 (3TA返回, 2TEE内核, 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_Pssw0rd_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 字段可以直接定位错误来源origin3错误由 TA 业务逻辑返回如身份不匹配、篡改校验失败origin2错误由 OP-TEE 内核返回如 TA 加载失败、资源不足origin1错误由 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 文件执行测试