OP-TEE + YOLOv8:从“加密权重”到“内存中解密并推理”的完整实战记录
目标:把
yolov8n.pt这类 PyTorch 权重文件在生产环境中“加密存储”,运行时通过 OP-TEE 在 Secure World 解密得到明文,然后在 Normal World 里不落盘直接加载推理,最终只输出推理结果图片bus_pridect.jpg。
本文是我一次完整踩坑/修复/跑通的实践笔记,尽量把关键逻辑讲清楚,并把“真的能跑起来”的代码技巧写出来。你可以把它当作一篇从零到一的可复现教程。
1. 最终目录里有哪些文件?它们分别干什么
在设备上 /test 目录最终只保留这 4 个文件(这是我希望的“最小可交付形态”):
-
bus.jpg:输入测试图片 -
yolo8_kdf.enc:加密后的模型权重密文(由主机生成后拷到设备) -
ca_yolo8:Client App(CA),运行在 Normal World,负责:- 读密文
- 调用 TA 解密
- 把解密结果放进 RAM(memfd)
- 拉起 Python/Ultralytics 做推理
- 把结果保存为
bus_pridect.jpg - 清理
runs/目录
-
bus_pridect.jpg:最终产物(只想保留这个)
文件级逻辑图(不谈函数细节,只谈文件流向)
(Host 侧生成)
yolov8n.pt (明文模型)
|
| openssl AES-256-CBC + PKCS7
v
yolo8_kdf.enc (密文) ----scp----> /test/yolo8_kdf.enc
|
| CA: ca_yolo8 读取
v
调用 OP-TEE TA 解密
|
| 明文只进 RAM (memfd)
v
Ultralytics YOLO 推理 bus.jpg
|
v
输出 bus_pridect.jpg
(删除 runs/ 目录)
2. 核心设计目标(为什么要这么做)
我想实现的安全目标非常明确:
- 模型不以明文形式长期落盘:磁盘上只有
.enc密文。 - 密钥不出 Secure World:Normal World 只能拿到“解密后的字节流”,拿不到 key/iv。
- 推理加载不依赖落盘:Ultralytics/torch 的加载逻辑默认是 “从路径读文件”,所以我需要一个“看起来像路径,但内容来自内存”的办法。
- 推理后清理痕迹:只保留结果图片,其它临时目录(例如
runs/)都删掉。
这是一种典型的“TEE 解密 + REE 推理”的工程折中:
- Secure World 负责 密钥保护 和 解密
- Normal World 负责 重计算/推理(因为把整个 PyTorch 推理搬到 Secure World 通常不可行且性能更差)
3. 加密算法到底是什么?(你问得最多的点)
你用的命令是:
openssl enc -aes-256-cbc \
-K "$KEY_HEX" \
-iv "$IV_HEX" \
-in "$PLAINTEXT" \
-out "$OUT"
这代表:
-
AES-256-CBC:
- AES 对称加密
- 密钥长度 256 bit(32 字节)
- 工作模式 CBC
-
PKCS#7 padding:
openssl enc默认会启用 PKCS#7 padding(除非你加-nopad)- 作用是把明文补齐到 16 字节的整数倍
所以“加密算法”可以准确说成:
AES-256-CBC with PKCS#7 padding(OpenSSL enc 实现)
重要细节:
-
yolov8n.pt的大小 不一定是 16 的倍数。 -
你在主机上测过:
- 明文大小
6549796,mod16 = 4 - 因此加密时会补齐
12字节 padding - 密文大小会变成
6549808(补齐到 16 的倍数)
- 明文大小
-
解密后如果你不做 unpad,会得到尾部多出来的 padding 数据,导致文件不是合法
.pt(zip)格式。
这解释了为什么 TA 里做 PKCS7 unpad 是必须的。
4. Key/IV 从哪里来?(KDF + PTA 的核心逻辑)
我最终采用的方式是:
-
Secure World 有一个 seed(hello_seed),来自 EKB/硬件/受保护存储
-
PTA 根据 seed 派生:
key = SHA256(seed || "hello-aes-key")取 32 字节iv = SHA256(seed || "hello-aes-iv")取前 16 字节
-
TA 调用 PTA 拿到 key/iv
-
TA 执行 AES-CBC 解密
-
TA 执行 PKCS7 unpad
为什么你看到“前 4 字节对得上”但还是可能解密错?
因为日志只打印了 key[0..3] 和 iv[0..3],只能说明“开头对”,但无法证明完整 32/16 字节完全一致。
我在 shell 脚本里增加了完整字节打印,最终确认 key/iv 全部一致:
printf "[+] key bytes = %s\n" "$(echo "$KEY_HEX" | sed -E 's/(..)/\1 /g')"
printf "[+] iv bytes = %s\n" "$(echo "$IV_HEX" | sed -E 's/(..)/\1 /g')"
此外还需要保证 label 字符串严格一致(大小写/数字都不能错):
- 你一开始把 label 用成
yolo-aes-key/yolo-aes-iv,后来改成yolo8-aes-key/yolo8-aes-iv或hello-aes-key/hello-aes-iv,只要两边一致就行。 - PTA 里写死的是:
"hello-aes-key"和"hello-aes-iv",那脚本就必须用同样的。
5. TA 里真正“正确”的解密逻辑长什么样
5.1 AES-CBC 解密用 NOPAD
因为 padding 是我们自己做/自己去掉,所以 TA 解密用:
TEE_ALG_AES_CBC_NOPAD
这是对的。
5.2 PKCS7 unpad 的关键点
-
block size 永远是 16,不要改(AES block size 固定 128 bit)
-
你看到
padding value=12是正常的(因为6549796 mod 16 = 4,需要补12) -
你之前看到过
padding value=159,那就说明“解密出来的末尾字节不是合法 padding”,本质原因是:- key/iv/密文不匹配
- 或者密文被破坏
- 或者加密时用了
-nopad但明文不是 16 的倍数(会直接报错) - 或者误把加密输出/传输的文件搞混(比如 scp 了旧文件)
最终你跑通的日志是:
decrypt ok raw_len=6549808PKCS7 padding value=12unpad success out_len=6549796
这说明 TA 解密链路是对的。
6. CA 侧的大坑:为什么 TEEC_AllocateSharedMemory(out) 会失败?
你遇到过典型错误:
TEEC_AllocateSharedMemory(out) failed: 0xffff000c
这个错误在很多平台上意味着:
- 共享内存分配失败(太大/系统限制/共享内存池不足)
解决办法:
-
不用
TEEC_AllocateSharedMemory去“让 OP-TEE 分配一大块共享内存” -
改为:
- Normal World 自己用
posix_memalign分配一块内存(页对齐) - 用
TEEC_RegisterSharedMemory注册这块内存给 OP-TEE
- Normal World 自己用
你最后的 CA 代码里采用了这条路线,这是很稳妥的做法。
7. 关键难点:Ultralytics/YOLO 只接受“路径”,不接受“纯内存字节”
你踩到的报错非常典型:
TypeError: model='/proc/self/fd/6' is not a supported model format
原因是 Ultralytics 会根据 文件后缀 判断格式(.pt, .onnx, .engine 等)。
/proc/self/fd/6 没有 .pt 后缀,所以它拒绝。
7.1 解决技巧:memfd + symlink + .pt 后缀
核心套路是:
- CA 在内存里创建 memfd
- 把解密出的
.pt字节写入 memfd(仍然在 RAM) - 清除
FD_CLOEXEC(重要!否则 exec 后 fd 会被关闭) - fork + exec 一个 python3
- python 进程里创建一个临时目录(推荐
/dev/shm) os.symlink('/proc/self/fd/<fd>', '<tmp>/yolov8n.pt')- 用这个带后缀的路径
YOLO(link)加载
这样 Ultralytics 看到的是一个“真实路径,且以 .pt 结尾”,但内容来自内存。
这就是本文最核心的“实战技巧”。
8. 你问的“明文到底在哪里?”——一次讲清楚
以下是你给出的核心片段(我用文字解释每一步“明文在哪儿”):
if (fdf >= 0) (void)fcntl(fd, F_SETFD, fdf & ~FD_CLOEXEC);
if (write_all(fd, out_mem, out_len) < 0) err(1, "write(memfd) failed");
if (lseek(fd, 0, SEEK_SET) < 0) err(1, "lseek(memfd) failed");
pid_t pid = fork();
if (pid == 0) {
char fd_arg[32];
snprintf(fd_arg, sizeof(fd_arg), "%d", fd);
const char *py =
"... os.symlink(f'/proc/self/fd/{fd}', link) ...\n"
"m=YOLO(link)\n"
"m.predict(source=img, save=True, save_txt=True)\n";
char *const args[] = { "python3", "-c", py, fd_arg, img_path, NULL };
execvp("python3", args);
}
8.1 out_mem:Normal World 的 RAM
out_mem是你用posix_memalign分配的内存- TA 解密后写回到
out_mem - 所以解密后的明文模型字节最先在 out_mem
8.2 memfd:仍然是 RAM
fd = memfd_create(...)创建的是一个匿名内存文件- 内核把它当作“文件对象”,但数据存在 RAM(swap/内存管理层面)
- 你执行
write(fd, out_mem, out_len)后,明文就被复制进了 memfd
这一步的好处:给 Ultralytics 一个“可被文件 API 读取”的对象。
8.3 /proc/self/fd/:是 memfd 的“文件视图”
/proc/self/fd/<fd>是 Linux 提供的“fd 的路径映射”- 读取这个路径就等于读那个 fd
- 但它没有
.pt后缀
8.4 symlink:只是在文件系统里建一个“名字”,数据不落盘
os.symlink('/proc/self/fd/<fd>', '/dev/shm/.../yolov8n.pt')- 创建的是符号链接,不会把模型内容写入磁盘
- 只是给这个 fd 起了个“看起来像 pt 文件”的名字
8.5 最终清理:擦掉内存
memset(out_mem, 0, out_cap)擦掉第一份明文close(fd)关闭 memfd,内核可回收- python 里删 symlink 和临时目录
所以总结一句话:
明文模型在整个流程中只存在于 Normal World 的 RAM(out_mem + memfd),不会作为真实
.pt文件写入持久存储。
9. 结果文件整理:只保留 bus_pridect.jpg,删除 runs/
Ultralytics 默认会把结果写到:
runs/detect/predict/bus.jpg
你想要:
- 把它复制/移动到
/test/bus_pridect.jpg - 删除整个
runs/
你之前遇到过:
Invalid cross-device link
那是因为你尝试 rename/硬链接跨不同挂载点(比如 /dev/shm 到 /test)。
正确做法:用 copy,再删除源。
在 Python 里可以这么做:
import shutil, os
src = 'runs/detect/predict/bus.jpg'
dst = '/test/bus_pridect.jpg'
shutil.copy2(src, dst)
shutil.rmtree('runs', ignore_errors=True)
如果你在 CA 里做,也可以 system("cp ...") + system("rm -rf runs"),但更推荐 Python 内部完成,避免 shell 依赖。

10. 关键代码技巧清单(踩坑后总结)
这一节是“经验提炼”,你以后再做类似工程会非常省时间。
10.1 大块共享内存:优先 RegisterSharedMemory
TEEC_AllocateSharedMemory可能因为共享内存池限制失败posix_memalign + TEEC_RegisterSharedMemory更稳
10.2 memfd 一定要清 FD_CLOEXEC
否则 exec 后 fd 被关闭,python 进程读不到。
int fdf = fcntl(fd, F_GETFD);
fcntl(fd, F_SETFD, fdf & ~FD_CLOEXEC);
10.3 Ultralytics 必须看到“.pt”后缀
/proc/self/fd/<fd>不带后缀会被拒绝- 用 symlink 起一个
yolov8n.pt
10.4 PKCS7 unpad 一定做在 TA 侧
- block size 固定 16
- padding value 应该在 1…16
10.5 输出结果要 copy,不要 rename(跨挂载点)
shutil.copy2最稳
10.6 推理结束后擦内存 & 禁 core dump
setrlimit(RLIMIT_CORE, 0)prctl(PR_SET_DUMPABLE, 0)(可选)memset擦 out_mem/in_mem
11. 最小可用的“加密/解密”脚本(与 PTA 完全一致)
11.1 加密脚本(Host 侧生成 yolo8_kdf.enc)
#!/bin/bash
set -e
SEED=hello_seed.bin
PLAINTEXT=yolov8n.pt
OUT=yolo8_kdf.enc
KEY_LABEL="hello-aes-key"
IV_LABEL="hello-aes-iv"
KEY_HEX=$(cat "$SEED" <(echo -n "$KEY_LABEL") | sha256sum | awk '{print $1}')
IV_HEX=$(cat "$SEED" <(echo -n "$IV_LABEL") | sha256sum | awk '{print substr($1,1,32)}')
echo "[+] AES-256 key = $KEY_HEX"
echo "[+] AES IV = $IV_HEX"
printf "[+] key bytes = %s\n" "$(echo "$KEY_HEX" | sed -E 's/(..)/\1 /g')"
printf "[+] iv bytes = %s\n" "$(echo "$IV_HEX" | sed -E 's/(..)/\1 /g')"
openssl enc -aes-256-cbc \
-K "$KEY_HEX" \
-iv "$IV_HEX" \
-in "$PLAINTEXT" \
-out "$OUT"
echo "[✓] Generated $OUT"
stat -c"[✓] Size: %s bytes" "$OUT"
11.2 解密脚本(用于对照验证)
#!/bin/bash
set -e
SEED=hello_seed.bin
IN=yolo8_kdf.enc
OUT=yolov8n_decrept.pt
KEY_LABEL="hello-aes-key"
IV_LABEL="hello-aes-iv"
KEY_HEX=$(cat "$SEED" <(echo -n "$KEY_LABEL") | sha256sum | awk '{print $1}')
IV_HEX=$(cat "$SEED" <(echo -n "$IV_LABEL") | sha256sum | awk '{print substr($1,1,32)}')
openssl enc -d -aes-256-cbc \
-K "$KEY_HEX" \
-iv "$IV_HEX" \
-in "$IN" \
-out "$OUT"
echo "[✓] Decrypted to $OUT"
如果 yolov8n_decrept.pt 和原始 yolov8n.pt 完全一致(你已经验证 hexdump 头一致),说明 Host 侧加密/解密链路没问题。
12. 你当前的进展总结(可以直接写在报告/邮件里)
- 我已经完成 YOLOv8 权重的加密存储(AES-256-CBC + PKCS7),设备上只保存密文
yolo8_kdf.enc。 - CA 能通过 OP-TEE 调用 TA/PTA,使用 Secure World 派生的 key/iv 完成解密并正确 PKCS7 unpad。
- 解密后的模型字节不落盘,保持在 RAM 中(out_mem + memfd),通过
/proc/self/fd/<fd>+ symlink 方式让 Ultralytics 以.pt路径形式加载。 - 推理可输出结果图片,并可以把
runs/detect/predict/bus.jpg复制为最终交付文件bus_pridect.jpg,随后删除runs/。
13. 下一步可以继续优化的方向(可选)
如果你要进一步做“更强保护”,可以考虑:
- 模型分段解密/流式解密:避免一次性把全部明文暴露在 REE 大块内存
- 推理框架侧改造:让 Ultralytics/torch 支持 file-like 对象(减少 symlink 依赖)
- 更强的加密模式:比如 AES-GCM(带完整性校验),防止密文被篡改后仍尝试加载
- 更严格的 REE 防护:禁止 ptrace、限制 /proc 访问、SELinux/AppArmor、容器隔离等
结语
这次实战最核心的成果是把“TEE 解密 + REE 内存加载推理”这个完整链路跑通,并把它收敛到 4 个文件:
bus.jpgyolo8_kdf.encca_yolo8bus_pridect.jpg
后续只要把 CA 的收尾逻辑再精炼一下(自动复制结果、自动删 runs、擦内存),就可以形成一个很像“产品功能”的闭环。
17

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



