用 OP-TEE 给 AI 模型“上锁”:密文存储、TEE 解密放行、推理后销毁(实战可落地)

AI 镜像开发实战征文活动 10w+人浏览 222人参与

部署运行你感兴趣的模型镜像

关键词: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. 威胁模型与边界(必须说清楚)

这套方案重点解决三类现实威胁:

  1. 离线拷贝与静态分析:攻击者拿到文件系统镜像/分区/OTA 包,无法直接拿到明文模型。
  2. 应用层逆向:攻击者分析 REE 应用,拿不到 DEK(数据密钥),因为 DEK 不出 TEE
  3. 运行后驻留:推理结束后尽量清理明文模型(内存清零/临时文件删除)。

但也要坦诚它的边界:

  • 若对手具备内核级/物理级能力(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 objectTEE_TYPE_AES)。
  • REE 只发密文 chunk + nonce/tag,让 TA 输出明文 chunk 到共享内存。

4.2 分块解密输出位置的权衡

  • 更安全:解密到内存(不要落盘)。Linux 可用 memfd_create() 创建匿名文件,兼容很多“需要文件”的 runtime。
  • 更易兼容:解密到临时文件(/tmptmpfs)。但要承认“彻底擦除”在 SSD/日志型文件系统上不保证。

本文提供两套:

  1. 临时文件方式(兼容优先):更容易接入 TensorRT/ORT。
  2. memfd 方式(安全优先):明文不落盘。

4.3 销毁策略要分两边讲

  • TEE 侧销毁(强保证):释放 transient object,清除会话上下文。
  • REE 侧销毁(best-effort):清零 buffer、关闭 fd、删除临时文件;尽量使用 memfd。

5. TA(Secure World)实战代码:解包 DEK + 分块解密 + Wipe

下面代码给出一个“最小但工程合理”的 TA 结构:

  • CMD_INIT_KEYS:初始化/加载 KEK
  • CMD_UNWRAP_DEK:解包 DEK(DEK 不回传 REE)
  • CMD_DECRYPT_CHUNK:解密一个 chunk
  • CMD_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 都是这样)。

流程:

  1. 解密 chunk 写入 /tmp/model.plain(尽量在 tmpfs)。

  2. runtime 加载并推理。

  3. 推理后:

    • best-effort 覆写文件内容(不保证彻底)
    • fsync(best-effort)
    • unlink 删除
  4. 通知 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. 常见坑与工程经验(决定你能不能真正上线)

  1. nonce 重用是灾难:AES-GCM 下同 key 重用 nonce 会直接破坏安全性。分块时要保证每块 nonce 唯一。
  2. 完整性必须验证:GCM 的 tag 验证失败必须清零输出,避免“半明文”。
  3. 不要把 DEK 返回 REE:哪怕只返回一次,也会在调试/崩溃/日志里留下痕迹。
  4. 尽量避免落盘:否则“销毁”只能 best-effort。
  5. 性能优化点:chunk 大小建议 256KB~2MB 之间测试;太小 syscall/TEE 往返开销大,太大影响内存峰值。

10. 你可以直接复用的“实战用例模板”

适配任何模型格式:ONNX、TensorRT engine、自研权重二进制。

  • 模型以 model.enc 形式分发/落盘。
  • 运行时 REE 调 TA:UNWRAP_DEKDECRYPT_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 的更优落地。

如果你把以下三项告诉我:

  1. 你模型当前格式(ONNX/TensorRT/自研)和加载方式(路径/内存/fd);
  2. 你希望的 model.enc chunk 组织(每块 tag/整文件 tag/是否需要随机访问);
  3. 你的设备平台(例如 Jetson/IMX/瑞芯微/高通等)以及 OP-TEE 的存储后端(RPMB/REE FS);

我可以把本文代码进一步补齐到“可直接编译跑通”的工程包(含 TA manifest、Makefile、打包加密工具、memfd 版本、以及完整的 header/chunk 解析代码)。

您可能感兴趣的与本文相关的镜像

Facefusion

Facefusion

AI应用

FaceFusion是全新一代AI换脸工具,无需安装,一键运行,可以完成去遮挡,高清化,卡通脸一键替换,并且Nvidia/AMD等显卡全平台支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值