关键词:OP-TEE / Trusted Application (TA) / libteec / AES-GCM 分块解密 / 密钥不出 TEE / 推理后明文销毁
目标一句话:模型静态存储永远是密文;运行时由 TEE 掌控密钥与解密;明文只短暂存在于推理窗口,随后可执行“可验证的擦除(best-effort)”。
0. 你为什么需要这套方案
很多设备侧 AI 部署的痛点是:
- 模型文件(
.onnx/.engine/.bin)一旦以明文形式落盘,被拷走就能复用。 - 即使模型加密了,如果解密密钥在 Normal World(REE)里,仍可能被调试/内存 dump/逆向提取。
- 推理框架往往要求“文件路径”或“大块 buffer”,一不小心就让明文长期驻留。
OP-TEE 的价值在于:把密钥与关键解密逻辑放到 Secure World,把 REE 降级为“搬运密文 + 触发解密 + 执行推理”。
1. 威胁模型与边界(必须说清楚)
这套方案重点解决三类现实威胁:
- 离线拷贝与静态分析:攻击者拿到文件系统镜像/分区/OTA 包,无法直接拿到明文模型。
- 应用层逆向:攻击者分析 REE 应用,拿不到 DEK(数据密钥),因为 DEK 不出 TEE。
- 运行后驻留:推理结束后尽量清理明文模型(内存清零/临时文件删除)。
但也要坦诚它的边界:
- 若对手具备内核级/物理级能力(DMA、内核模块、hypervisor、JTAG 等),在推理期间抓 REE 内存仍可能拿到明文(因为大多数推理发生在 REE)。
- 若你要对抗这种级别,需要进一步:加密内存/加密显存硬件、IOMMU、TEE 内推理/安全协处理器、度量启动、内核完整性保护等。
结论:OP-TEE 方案是“显著抬高门槛”的工程解,而不是“绝对不可破”的魔法盾。

2. 端到端架构
核心思路:KEK + DEK 两级密钥体系,DEK 用来加密模型数据,KEK 用来包裹 DEK。
- KEK(Key Encryption Key):长期密钥,只在 TEE 内存在(安全存储或 HUK 派生)。
- DEK(Data Encryption Key):模型数据密钥,随机生成;永远不回传 REE。
数据与调用流可以用一张图理解:
生产端 / 打包端
明文模型 ──(DEK: AES-GCM)──► model.enc(密文)
▲
│ wrapped_DEK = Wrap(KEK, DEK)
│
┌─────┴─────┐
│ OP-TEE │
REE Client ──►│ TA │◄── KEK(安全存储/HUK 派生)
(搬运密文) └─────┬─────┘
│ 分块解密(DEK 仅在 TA 内)
▼
REE 推理框架加载/推理
│
▼
推理完成:REE/TEE 双侧销毁
3. 文件格式设计(model.enc):强烈推荐“分块独立认证”
为什么要分块?因为模型可能很大(几十 MB 到几百 MB),你希望:
- 流式解密:不用把整模型一次性解密到内存。
- 随机访问:有些 runtime 会跳读;分块更灵活。
- 更强完整性:每块都有 tag,篡改可定位。
推荐的最小格式:
- Header:魔数、版本、chunk_size、wrapped_DEK、包裹用 nonce/tag、以及可选 AAD。
- Body:由若干 chunk 组成,每个 chunk:
[nonce(12) | tag(16) | ciphertext(chunk)]。
示例结构(仅示意):
#define MAGIC 0x4D4F444C // 'MODL'
typedef struct __attribute__((packed)) {
uint32_t magic;
uint16_t version;
uint16_t alg; // 1 = AES-GCM
uint32_t header_len;
uint32_t chunk_size; // e.g. 1MB
uint32_t wrapped_dek_len;
uint8_t wrapped_dek[64];
uint8_t wrap_nonce[12];
uint8_t wrap_tag[16];
// 可选:model_id、aad_hash、签名等
} model_enc_header_t;
实战建议:把
model_id/版本号/TA UUID/设备标识派生信息作为 AAD 或派生输入,做到“换设备/换 TA/换版本不可解”。
4. OP-TEE 设计要点(高含金量的地方)
4.1 DEK 明文不出 TEE
常见误区:TA 解包 DEK 后把 DEK 返回给 REE 再解密模型。这等于“把钥匙递给小偷”。
正确做法:
- TA 内把 DEK 导入为 transient key object(
TEE_TYPE_AES)。 - REE 只发密文 chunk + nonce/tag,让 TA 输出明文 chunk 到共享内存。
4.2 分块解密输出位置的权衡
- 更安全:解密到内存(不要落盘)。Linux 可用
memfd_create()创建匿名文件,兼容很多“需要文件”的 runtime。 - 更易兼容:解密到临时文件(
/tmp、tmpfs)。但要承认“彻底擦除”在 SSD/日志型文件系统上不保证。
本文提供两套:
- 临时文件方式(兼容优先):更容易接入 TensorRT/ORT。
- memfd 方式(安全优先):明文不落盘。
4.3 销毁策略要分两边讲
- TEE 侧销毁(强保证):释放 transient object,清除会话上下文。
- REE 侧销毁(best-effort):清零 buffer、关闭 fd、删除临时文件;尽量使用 memfd。
5. TA(Secure World)实战代码:解包 DEK + 分块解密 + Wipe
下面代码给出一个“最小但工程合理”的 TA 结构:
CMD_INIT_KEYS:初始化/加载 KEKCMD_UNWRAP_DEK:解包 DEK(DEK 不回传 REE)CMD_DECRYPT_CHUNK:解密一个 chunkCMD_WIPE:销毁会话内敏感信息
5.1 命令定义
// ta_model_protect.h
#pragma once
#define TA_MODEL_PROTECT_UUID \
{ 0x12345678, 0x1234, 0x5678, { 0x12,0x34,0x56,0x78,0x90,0xab,0xcd,0xef } }
#define CMD_INIT_KEYS 0x00000001
#define CMD_UNWRAP_DEK 0x00000002
#define CMD_DECRYPT_CHUNK 0x00000003
#define CMD_WIPE 0x00000004
5.2 会话上下文 + 安全清零
#include <tee_internal_api.h>
#include <tee_internal_api_extensions.h>
#include "ta_model_protect.h"
typedef struct {
TEE_ObjectHandle dek_obj; // DEK transient object(只在会话内)
uint32_t dek_len;
bool dek_ready;
} session_ctx_t;
static void secure_bzero(void *p, size_t n) {
if (!p || !n) return;
volatile uint8_t *vp = (volatile uint8_t*)p;
while (n--) *vp++ = 0;
}
TEE_Result TA_OpenSessionEntryPoint(uint32_t pt, TEE_Param p[4], void **s) {
(void)pt; (void)p;
session_ctx_t *ctx = TEE_Malloc(sizeof(*ctx), TEE_MALLOC_FILL_ZERO);
if (!ctx) return TEE_ERROR_OUT_OF_MEMORY;
ctx->dek_obj = TEE_HANDLE_NULL;
*s = ctx;
return TEE_SUCCESS;
}
void TA_CloseSessionEntryPoint(void *s) {
session_ctx_t *ctx = (session_ctx_t*)s;
if (!ctx) return;
if (ctx->dek_obj != TEE_HANDLE_NULL) {
TEE_FreeTransientObject(ctx->dek_obj);
ctx->dek_obj = TEE_HANDLE_NULL;
}
secure_bzero(ctx, sizeof(*ctx));
TEE_Free(ctx);
}
5.3 KEK:示例用安全存储持久化
真实产品更建议:用 HUK(硬件唯一密钥)派生 KEK。这里用安全存储演示可跑通的版本。
static TEE_Result load_or_create_kek(uint8_t kek[32], uint32_t *kek_len) {
const char *id = "MODEL_KEK_V1";
const uint32_t id_len = 12;
TEE_ObjectHandle h = TEE_HANDLE_NULL;
TEE_Result res = TEE_OpenPersistentObject(TEE_STORAGE_PRIVATE,
id, id_len,
TEE_DATA_FLAG_ACCESS_READ,
&h);
if (res == TEE_SUCCESS) {
uint32_t r = 0;
res = TEE_ReadObjectData(h, kek, 32, &r);
TEE_CloseObject(h);
if (res == TEE_SUCCESS && r == 32) {
*kek_len = 32;
return TEE_SUCCESS;
}
}
TEE_GenerateRandom(kek, 32);
*kek_len = 32;
res = TEE_CreatePersistentObject(TEE_STORAGE_PRIVATE,
id, id_len,
TEE_DATA_FLAG_ACCESS_WRITE | TEE_DATA_FLAG_OVERWRITE,
TEE_HANDLE_NULL,
kek, 32,
&h);
if (res == TEE_SUCCESS) TEE_CloseObject(h);
return res;
}
static TEE_Result cmd_init_keys(void) {
uint8_t kek[32];
uint32_t len = 0;
TEE_Result res = load_or_create_kek(kek, &len);
secure_bzero(kek, sizeof(kek));
return res;
}
5.4 解包 DEK(AES-GCM 包裹示例)
- 输入:
wrapped_dek,以及wrap_nonce(12)+wrap_tag(16) - 输出:把 DEK 导入 transient object,不返回 DEK
static TEE_Result unwrap_dek_gcm(const uint8_t *wrapped, uint32_t wrapped_len,
const uint8_t *nonce, uint32_t nonce_len,
const uint8_t *tag, uint32_t tag_len,
uint8_t *out_dek, uint32_t *out_len) {
uint8_t kek[32];
uint32_t kek_len = 0;
TEE_Result res = load_or_create_kek(kek, &kek_len);
if (res != TEE_SUCCESS) return res;
// KEK -> transient object
TEE_ObjectHandle kek_obj = TEE_HANDLE_NULL;
res = TEE_AllocateTransientObject(TEE_TYPE_AES, kek_len * 8, &kek_obj);
if (res != TEE_SUCCESS) { secure_bzero(kek, sizeof(kek)); return res; }
TEE_Attribute attr;
TEE_InitRefAttribute(&attr, TEE_ATTR_SECRET_VALUE, kek, kek_len);
res = TEE_PopulateTransientObject(kek_obj, &attr, 1);
secure_bzero(kek, sizeof(kek));
if (res != TEE_SUCCESS) { TEE_FreeTransientObject(kek_obj); return res; }
// AES-GCM 解密 wrapped(dek)
TEE_OperationHandle op = TEE_HANDLE_NULL;
res = TEE_AllocateOperation(&op, TEE_ALG_AES_GCM, TEE_MODE_DECRYPT, kek_len * 8);
if (res != TEE_SUCCESS) { TEE_FreeTransientObject(kek_obj); return res; }
res = TEE_SetOperationKey(op, kek_obj);
TEE_FreeTransientObject(kek_obj);
if (res != TEE_SUCCESS) { TEE_FreeOperation(op); return res; }
res = TEE_AEInit(op, nonce, nonce_len, tag_len * 8, 0, 0);
if (res != TEE_SUCCESS) { TEE_FreeOperation(op); return res; }
uint32_t pt_len = *out_len;
res = TEE_AEDecryptFinal(op, wrapped, wrapped_len, out_dek, &pt_len, tag, tag_len);
TEE_FreeOperation(op);
if (res == TEE_SUCCESS) *out_len = pt_len;
return res;
}
static TEE_Result cmd_unwrap_dek(uint32_t pt, TEE_Param p[4], void *s) {
session_ctx_t *ctx = (session_ctx_t*)s;
const uint32_t exp = TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_INPUT,
TEE_PARAM_TYPE_MEMREF_INPUT,
TEE_PARAM_TYPE_VALUE_OUTPUT,
TEE_PARAM_TYPE_NONE);
if (pt != exp) return TEE_ERROR_BAD_PARAMETERS;
const uint8_t *wrapped = p[0].memref.buffer;
uint32_t wrapped_len = p[0].memref.size;
const uint8_t *nt = p[1].memref.buffer;
uint32_t nt_len = p[1].memref.size;
if (nt_len != (12 + 16)) return TEE_ERROR_BAD_PARAMETERS;
const uint8_t *nonce = nt;
const uint8_t *tag = nt + 12;
uint8_t dek[32];
uint32_t dek_len = sizeof(dek);
TEE_Result res = unwrap_dek_gcm(wrapped, wrapped_len, nonce, 12, tag, 16, dek, &dek_len);
if (res != TEE_SUCCESS) { secure_bzero(dek, sizeof(dek)); return res; }
// 存入会话 transient object
if (ctx->dek_obj != TEE_HANDLE_NULL) {
TEE_FreeTransientObject(ctx->dek_obj);
ctx->dek_obj = TEE_HANDLE_NULL;
}
res = TEE_AllocateTransientObject(TEE_TYPE_AES, dek_len * 8, &ctx->dek_obj);
if (res != TEE_SUCCESS) { secure_bzero(dek, sizeof(dek)); return res; }
TEE_Attribute a;
TEE_InitRefAttribute(&a, TEE_ATTR_SECRET_VALUE, dek, dek_len);
res = TEE_PopulateTransientObject(ctx->dek_obj, &a, 1);
secure_bzero(dek, sizeof(dek));
if (res != TEE_SUCCESS) {
TEE_FreeTransientObject(ctx->dek_obj);
ctx->dek_obj = TEE_HANDLE_NULL;
return res;
}
ctx->dek_len = dek_len;
ctx->dek_ready = true;
p[2].value.a = 1; // 会话内句柄(示例)
p[2].value.b = dek_len;
return TEE_SUCCESS;
}
5.5 分块解密(每块 nonce/tag)
static TEE_Result cmd_decrypt_chunk(uint32_t pt, TEE_Param p[4], void *s) {
session_ctx_t *ctx = (session_ctx_t*)s;
const uint32_t exp = TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_INPUT,
TEE_PARAM_TYPE_MEMREF_INPUT,
TEE_PARAM_TYPE_MEMREF_OUTPUT,
TEE_PARAM_TYPE_VALUE_INPUT);
if (pt != exp) return TEE_ERROR_BAD_PARAMETERS;
if (!ctx->dek_ready || ctx->dek_obj == TEE_HANDLE_NULL) return TEE_ERROR_SECURITY;
const uint8_t *ct = p[0].memref.buffer;
uint32_t ct_len = p[0].memref.size;
const uint8_t *nt = p[1].memref.buffer;
uint32_t nt_len = p[1].memref.size;
if (nt_len != (12 + 16)) return TEE_ERROR_BAD_PARAMETERS;
const uint8_t *nonce = nt;
const uint8_t *tag = nt + 12;
uint8_t *ptbuf = p[2].memref.buffer;
uint32_t pt_len = p[2].memref.size;
uint32_t chunk_index = p[3].value.a;
(void)chunk_index;
TEE_OperationHandle op = TEE_HANDLE_NULL;
TEE_Result res = TEE_AllocateOperation(&op, TEE_ALG_AES_GCM, TEE_MODE_DECRYPT, ctx->dek_len * 8);
if (res != TEE_SUCCESS) return res;
res = TEE_SetOperationKey(op, ctx->dek_obj);
if (res != TEE_SUCCESS) { TEE_FreeOperation(op); return res; }
res = TEE_AEInit(op, nonce, 12, 16 * 8, 0, 0);
if (res != TEE_SUCCESS) { TEE_FreeOperation(op); return res; }
// 可选:把 header_hash / model_id / chunk_index 作为 AAD,增强绑定
// TEE_AEUpdateAAD(op, aad, aad_len);
res = TEE_AEDecryptFinal(op, ct, ct_len, ptbuf, &pt_len, tag, 16);
TEE_FreeOperation(op);
if (res == TEE_SUCCESS) {
p[2].memref.size = pt_len;
} else {
// 防止留下半截明文
secure_bzero(ptbuf, p[2].memref.size);
}
return res;
}
5.6 Wipe:推理后销毁(TEE 侧强保证)
static TEE_Result cmd_wipe(void *s) {
session_ctx_t *ctx = (session_ctx_t*)s;
if (!ctx) return TEE_SUCCESS;
if (ctx->dek_obj != TEE_HANDLE_NULL) {
TEE_FreeTransientObject(ctx->dek_obj);
ctx->dek_obj = TEE_HANDLE_NULL;
}
ctx->dek_ready = false;
ctx->dek_len = 0;
return TEE_SUCCESS;
}
5.7 Invoke 分发
TEE_Result TA_InvokeCommandEntryPoint(void *s, uint32_t cmd,
uint32_t pt, TEE_Param p[4]) {
switch (cmd) {
case CMD_INIT_KEYS: return cmd_init_keys();
case CMD_UNWRAP_DEK: return cmd_unwrap_dek(pt, p, s);
case CMD_DECRYPT_CHUNK: return cmd_decrypt_chunk(pt, p, s);
case CMD_WIPE: return cmd_wipe(s);
default: return TEE_ERROR_NOT_SUPPORTED;
}
}
6. REE(Normal World)实战代码:解密输出 + 推理 + 销毁
6.1 REE 侧安全清零
static void secure_bzero(void *p, size_t n) {
if (!p || !n) return;
volatile unsigned char *vp = (volatile unsigned char*)p;
while (n--) *vp++ = 0;
}
6.2 关键点:REE 永远拿不到 DEK
REE 只做:
- 读 header 得到
wrapped_dek+wrap_nonce/tag。 - 调
CMD_UNWRAP_DEK,让 TA 在会话内保存 DEK。 - 循环读 chunk:把
ct + nonce/tag发给 TA,拿回pt。
(调用代码与参数组织略,保持与前一版一致即可——核心在“DEK 不回传”。)
7. 两种“返回后销毁”落地方式
你要求的重点是:解密后返回给 REE 使用,推理完成后销毁。这里给两套真实工程里常用的方案。
7.1 方案一:解密到临时文件(兼容优先)
适用:你的 runtime 只能从路径加载(很多 TensorRT/某些 SDK 都是这样)。
流程:
-
解密 chunk 写入
/tmp/model.plain(尽量在 tmpfs)。 -
runtime 加载并推理。
-
推理后:
- best-effort 覆写文件内容(不保证彻底)
fsync(best-effort)unlink删除
-
通知 TA
CMD_WIPE。
⚠️ 重要说明:SSD 磨损均衡、日志文件系统会让“覆写即抹除”不可靠,所以这只是 best-effort。能不用落盘就不要落盘。
7.2 方案二:解密到 memfd(安全优先,强烈推荐)
适用:你的 runtime 能从 文件描述符 或 内存 buffer 加载;或者你能改造加载入口。
memfd_create() 会创建一个匿名的、只存在内存的文件对象:
- 没有路径,不会出现在文件系统里。
- 你可以像文件一样
write()明文模型。 - 推理完成后
close(fd)即释放。
伪代码思路:
int fd = memfd_create("model", MFD_CLOEXEC);
// 循环解密 chunk,write(fd, pt, pt_len)
// lseek(fd, 0, SEEK_SET)
// runtime_load_from_fd(fd)
// 推理
close(fd); // 释放
secure_bzero(buffers);
TEEC_InvokeCommand(... CMD_WIPE ...);
如果你的推理框架只接受路径,可以考虑:
- 是否支持从内存加载(部分 ORT 可从 buffer)。
- 是否能用
/proc/self/fd/<fd>这种形式“伪装路径”。(依赖框架实现)
8. “销毁”到底要做哪些动作(清单化)
为了让方案可审计、可复现,我建议把销毁动作写成 checklist:
8.1 TEE 侧(必须做)
-
TEE_FreeTransientObject(ctx->dek_obj) -
ctx->dek_ready=false; ctx->dek_len=0; - 清理所有临时 buffer(
secure_bzero) - 会话关闭时再次兜底清理
8.2 REE 侧(尽力而为,优先级从高到低)
- 优先使用 memfd/纯内存,不落盘
- chunk 明文用完立即
secure_bzero(pt) - 推理结束释放 runtime 持有的 buffer(如果你能拿到 handle)
- 如果落盘:覆写 + fsync + unlink(best-effort)
- 禁止日志打印明文/禁止 core dump(
ulimit -c 0等)
9. 常见坑与工程经验(决定你能不能真正上线)
- nonce 重用是灾难:AES-GCM 下同 key 重用 nonce 会直接破坏安全性。分块时要保证每块 nonce 唯一。
- 完整性必须验证:GCM 的 tag 验证失败必须清零输出,避免“半明文”。
- 不要把 DEK 返回 REE:哪怕只返回一次,也会在调试/崩溃/日志里留下痕迹。
- 尽量避免落盘:否则“销毁”只能 best-effort。
- 性能优化点:chunk 大小建议 256KB~2MB 之间测试;太小 syscall/TEE 往返开销大,太大影响内存峰值。
10. 你可以直接复用的“实战用例模板”
适配任何模型格式:ONNX、TensorRT engine、自研权重二进制。
- 模型以
model.enc形式分发/落盘。 - 运行时 REE 调 TA:
UNWRAP_DEK→DECRYPT_CHUNK*N → 推理 →WIPE。 - 密钥只在 TEE;明文只存在于推理窗口;推理后 REE/TEE 双侧销毁。
11. 如果你要把它做成“更硬”的商业级方案
下面是从“能跑”到“能打”的升级路线:
- HUK 派生 KEK(设备绑定):换设备无法解密。
- 将 model_id/版本/TA UUID/设备标识作为 AAD:防替换、防降级。
- 模型签名验证(TA 内或 REE+TEE 协作):确保密文没被替换。
- 度量启动/远程证明:在可信启动链上运行推理服务。
- 速率限制/审计:TA 记录解密次数和时间窗,异常行为直接拒绝。
12. 结语
这篇实战的关键“含金量”在于:
- 密钥不出 TEE(DEK 永不回传),这是模型保护的底线。
- 分块 AES-GCM(每块 nonce/tag),兼顾安全与性能。
- 推理后销毁分成 TEE 强保证 + REE best-effort,并给出 memfd 的更优落地。
如果你把以下三项告诉我:
- 你模型当前格式(ONNX/TensorRT/自研)和加载方式(路径/内存/fd);
- 你希望的
model.encchunk 组织(每块 tag/整文件 tag/是否需要随机访问); - 你的设备平台(例如 Jetson/IMX/瑞芯微/高通等)以及 OP-TEE 的存储后端(RPMB/REE FS);
我可以把本文代码进一步补齐到“可直接编译跑通”的工程包(含 TA manifest、Makefile、打包加密工具、memfd 版本、以及完整的 header/chunk 解析代码)。

被折叠的 条评论
为什么被折叠?



