OP-TEE + YOLOv8:从“加密权重”到“内存中解密并推理”的完整实战记录

编程达人挑战赛·第6期 10w+人浏览 212人参与

OP-TEE + YOLOv8:从“加密权重”到“内存中解密并推理”的完整实战记录

目标:把 yolov8n.pt 这类 PyTorch 权重文件在生产环境中“加密存储”,运行时通过 OP-TEESecure World 解密得到明文,然后在 Normal World不落盘直接加载推理,最终只输出推理结果图片 bus_pridect.jpg

本文是我一次完整踩坑/修复/跑通的实践笔记,尽量把关键逻辑讲清楚,并把“真的能跑起来”的代码技巧写出来。你可以把它当作一篇从零到一的可复现教程。


1. 最终目录里有哪些文件?它们分别干什么

在设备上 /test 目录最终只保留这 4 个文件(这是我希望的“最小可交付形态”):

  • bus.jpg:输入测试图片

  • yolo8_kdf.enc加密后的模型权重密文(由主机生成后拷到设备)

  • ca_yolo8Client 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. 核心设计目标(为什么要这么做)

我想实现的安全目标非常明确:

  1. 模型不以明文形式长期落盘:磁盘上只有 .enc 密文。
  2. 密钥不出 Secure World:Normal World 只能拿到“解密后的字节流”,拿不到 key/iv。
  3. 推理加载不依赖落盘:Ultralytics/torch 的加载逻辑默认是 “从路径读文件”,所以我需要一个“看起来像路径,但内容来自内存”的办法。
  4. 推理后清理痕迹:只保留结果图片,其它临时目录(例如 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 的倍数

  • 你在主机上测过:

    • 明文大小 6549796mod16 = 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-ivhello-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=6549808
  • PKCS7 padding value=12
  • unpad success out_len=6549796

这说明 TA 解密链路是对的。


6. CA 侧的大坑:为什么 TEEC_AllocateSharedMemory(out) 会失败?

你遇到过典型错误:

  • TEEC_AllocateSharedMemory(out) failed: 0xffff000c

这个错误在很多平台上意味着:

  • 共享内存分配失败(太大/系统限制/共享内存池不足)

解决办法

  • 不用 TEEC_AllocateSharedMemory 去“让 OP-TEE 分配一大块共享内存”

  • 改为:

    1. Normal World 自己用 posix_memalign 分配一块内存(页对齐)
    2. TEEC_RegisterSharedMemory 注册这块内存给 OP-TEE

你最后的 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 后缀

核心套路是:

  1. CA 在内存里创建 memfd
  2. 把解密出的 .pt 字节写入 memfd(仍然在 RAM)
  3. 清除 FD_CLOEXEC(重要!否则 exec 后 fd 会被关闭)
  4. fork + exec 一个 python3
  5. python 进程里创建一个临时目录(推荐 /dev/shm
  6. os.symlink('/proc/self/fd/<fd>', '<tmp>/yolov8n.pt')
  7. 用这个带后缀的路径 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. 下一步可以继续优化的方向(可选)

如果你要进一步做“更强保护”,可以考虑:

  1. 模型分段解密/流式解密:避免一次性把全部明文暴露在 REE 大块内存
  2. 推理框架侧改造:让 Ultralytics/torch 支持 file-like 对象(减少 symlink 依赖)
  3. 更强的加密模式:比如 AES-GCM(带完整性校验),防止密文被篡改后仍尝试加载
  4. 更严格的 REE 防护:禁止 ptrace、限制 /proc 访问、SELinux/AppArmor、容器隔离等

结语

这次实战最核心的成果是把“TEE 解密 + REE 内存加载推理”这个完整链路跑通,并把它收敛到 4 个文件:

  • bus.jpg
  • yolo8_kdf.enc
  • ca_yolo8
  • bus_pridect.jpg

后续只要把 CA 的收尾逻辑再精炼一下(自动复制结果、自动删 runs、擦内存),就可以形成一个很像“产品功能”的闭环。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值