4、模型文件格式与结构:常见模型格式(如ONNX、PyTorch、TensorFlow)及其存储特点
在模型加密的实操链路中,第一步往往不是直接编写加密代码,而是先弄清楚“要加密的对象到底是什么”。模型文件格式决定了参数在磁盘上的排列方式、元数据的组织逻辑、算子的描述方法以及运行时加载的入口。如果对格式理解不透彻,后续的权重保护、算子混淆、加密容器设计都可能建立在错误的地基上。本章将系统拆解ONNX、PyTorch(Pickle/Zip)、TensorFlow(SavedModel/Checkpoint)三种主流格式的存储结构,并分析它们在加密场景下的关键差异。
上图展示了三种主流格式在存储结构上的核心差异。ONNX以Protobuf二进制整体序列化,PyTorch基于Zip+Pickle的Python对象持久化,TensorFlow则采用多文件目录结构。理解这些差异是选择加密策略的前提——例如,对ONNX可以整体加密后运行时解密加载,而对PyTorch则更适合在张量层面做细粒度保护。
4.1 ONNX格式:静态图与Protobuf序列化
核心原理
ONNX(Open Neural Network Exchange)采用Google Protobuf作为序列化协议,将模型的计算图、算子属性、权重参数统一编码为二进制流。一个ONNX文件本质上是一个ModelProto对象,内部包含GraphProto(计算图)、NodeProto(算子节点)、TensorProto(权重张量)以及版本信息、生产者信息等元数据。由于是静态图描述,ONNX文件中的权重直接以原始字节形式内嵌在TensorProto中,不依赖任何Python运行时。
存储结构拆解
| 层级 | 组件 | 存储内容 | 加密关注点 |
|---|---|---|---|
| 文件级 | ModelProto | ir_version、opset_import、producer_name | 整体文件可被加密或签名 |
| 图级 | GraphProto | 节点列表、输入输出张量名、初始值 | 图结构暴露算子逻辑 |
| 节点级 | NodeProto | op_type、attribute、输入输出名称 | 算子类型可被混淆替换 |
| 权重级 | TensorProto | raw_data(原始字节)、dims、data_type | 权重可直接提取并篡改 |
实战场景:ONNX模型加密的两种路径
在实际项目中,ONNX模型常被用于边缘设备或跨框架推理。加密时有两种典型路径:路径一是整体文件加密,将整个.onnx文件用AES-256-GCM加密,运行时在内存中解密并加载,适用于对文件分发管控严格的场景;路径二是权重张量级加密,仅对TensorProto中的raw_data字段进行加密,保留图结构明文,这样攻击者即使看到图结构也无法获取有效权重。路径二的优势在于可以配合模型剪枝或量化,只保护关键层。
常见误区
一个常见误区是认为ONNX文件“已经是二进制了,所以天然安全”。实际上,ONNX的二进制格式有公开的解析库(如onnx Python包),任何人都可以轻松读取权重。另一个误区是混淆“加密”与“编码”——Base64编码或简单的字节异或不是加密,只能增加一点点逆向成本,无法抵御专业攻击。
4.2 PyTorch格式:Zip容器与Pickle序列化
核心原理
PyTorch的.pt或.pth文件采用Zip归档格式,内部包含一个或多个Pickle序列化的Python对象。当使用torch.save(model.state_dict(), "model.pth")时,实际是将一个OrderedDict(包含所有张量的键值对)通过Pickle协议序列化后写入Zip文件。PyTorch的torch.load()在加载时会自动解压Zip并反序列化Pickle对象。这种设计使得PyTorch模型与Python运行时深度耦合——加载过程需要执行Pickle字节码,这既是灵活性来源,也是安全漏洞的温床。
存储结构拆解
model.pth (Zip归档)
├── [Content_Types].xml # 可选的Opc内容类型声明
├── data.pkl # Pickle序列化的状态字典(权重+元数据)
├── data/ # 可选目录,存放额外数据
│ └── 0 # 按索引存储的张量数据
└── zipcomment # 可选的注释信息
关键点在于data.pkl中的Pickle数据流。Pickle协议允许序列化任意Python对象,包括自定义类实例、函数引用甚至可执行代码。这意味着一个恶意构造的.pth文件可以在torch.load()时执行任意代码——这就是著名的Pickle反序列化漏洞。
实战场景:PyTorch模型的安全加载方案
假设你负责一个AI推理服务的部署,需要加载一个来自第三方团队的PyTorch模型。直接使用torch.load()存在巨大风险。推荐的做法是:
- 使用
weights_only=True参数(PyTorch 2.0+),限制Pickle只加载基本张量类型,拒绝自定义类。 - 自定义Unpickler,重写
find_class()方法,白名单允许的类。 - 先解压Zip,检查文件列表,确认没有可疑的
.py或.so文件。 - 在沙箱环境中先加载一次,观察是否有异常网络请求或文件操作。
state_dict中的每个张量转换为字节流,使用ChaCha20-Poly1305加密后存入自定义Zip容器,同时将密钥存储在硬件安全模块(HSM)或安全飞地中。加载时,在内存中解密并重建张量,整个过程不落盘明文。
常见误区
很多开发者误以为torch.save的模型文件“已经压缩过了”,所以体积小就安全。实际上Zip压缩只是无损压缩,不提供任何保密性。另一个误区是使用pickle.dumps后自己加个简单的XOR混淆就认为安全——这种方案在攻击者拿到样本后几分钟内就能被破解。
4.3 TensorFlow格式:SavedModel与Checkpoint
核心原理
TensorFlow的模型存储分为两大体系:Checkpoint(用于训练中断恢复)和SavedModel(用于部署推理)。SavedModel是一个目录结构,包含saved_model.pb(图定义)、variables/(变量数据)、assets/(辅助文件)。变量数据以分片形式存储在variables.index和variables.data-00000-of-00001中,采用TensorFlow自定义的二进制格式。与PyTorch不同,SavedModel不依赖Python对象,而是通过C++运行时加载,因此更适合作业环境部署。
存储结构拆解
| 路径 | 内容 | 格式 | 加密策略 |
|---|---|---|---|
| saved_model.pb | GraphDef(计算图) | Protobuf二进制 | 可整体加密或签名 |
| variables/variables.index | 变量名到分片偏移的映射 | Protobuf文本/二进制 | 可加密索引表 |
| variables/variables.data-* | 权重张量原始字节 | 自定义二进制分片 | 核心加密对象 |
| assets/ | 词表、配置文件等 | 任意格式 | 按需加密 |
实战场景:TensorFlow Serving中的模型保护
在TensorFlow Serving场景中,模型文件通常存放在共享存储上。一个实际案例是:某金融公司需要将风控模型部署到多个边缘节点,但担心模型被运维人员复制泄露。解决方案是:
- 在模型导出时,对
variables.data-*文件使用AES-256-GCM加密,密钥由KMS管理。 - 编写自定义的
SessionBundle加载器,在加载模型前先解密变量文件到内存临时文件(使用memfd_create避免落盘)。 - 加载完成后立即删除临时文件,并锁定进程内存防止swap泄露。
该方案将模型泄露风险从“文件被复制即泄露”降低到“需要同时获取密文和密钥”的级别,显著提升了安全性。
常见误区
一个常见误区是只加密saved_model.pb而忽略variables/目录。实际上,图结构本身的价值远低于权重参数——攻击者即使拿到明文图结构,没有权重也无法复现模型功能。另一个误区是使用TensorFlow自带的tf.io.write_file配合简单加密,但这种方式容易在临时文件中留下明文副本。
4.4 格式对比与加密选型决策
核心原理
三种格式的差异本质上是“静态图vs动态图”、“Python对象vs纯数据”、“单文件vs多文件”的差异。这些差异直接决定了加密的可行性和效率。
对比分析表
| 维度 | ONNX | PyTorch | TensorFlow |
|---|---|---|---|
| 序列化方式 | Protobuf | Pickle | Protobuf+自定义分片 |
| 运行时依赖 | 无Python依赖 | 强依赖Python | C++运行时 |
| 权重存储 | 内嵌于TensorProto | Pickle内嵌或独立文件 | 独立分片文件 |
| 加密粒度 | 文件级/张量级 | 张量级/对象级 | 变量级/文件级 |
| 反序列化风险 | 低(纯数据) | 高(可执行代码) | 低(纯数据) |
| 推荐加密方案 | AES-GCM整体加密 | ChaCha20张量加密+安全加载器 | 变量文件加密+自定义Loader |
决策流程
在实际项目中,可以按照以下流程选择加密策略:
- 确认部署环境:如果是嵌入式设备,优先选ONNX;如果是Python服务,PyTorch更自然;如果是TensorFlow Serving,则用SavedModel。
- 评估威胁模型:如果只是防止普通用户复制,轻量级混淆即可;如果面对专业逆向工程师,必须使用强加密+硬件密钥。
- 测试性能损耗:加密解密带来的延迟是否在可接受范围内?对于实时推理,建议使用硬件加速的AES-NI指令集。
- 设计密钥管理:密钥不能硬编码在代码中,应使用KMS、HSM或环境变量注入。
案例复盘:一个混合加密方案
某AI公司需要交付一个OCR模型给客户,客户环境是内网隔离的Windows服务器,没有GPU。模型使用PyTorch训练,但客户要求提供ONNX格式以便集成到C#系统中。项目组面临两个挑战:一是模型精度在转换后略有下降,二是客户要求模型文件不能泄露到外部。
最终方案是:
- 将PyTorch模型转换为ONNX,并记录精度差异(从98.2%降至97.9%,可接受)。
- 使用AES-256-GCM加密整个ONNX文件,密钥由客户提供的硬件加密狗存储。
- 在C#端使用
OnnxRuntime加载前,调用C++/CLI封装的解密函数,在内存中完成解密。 - 增加模型文件哈希校验,防止被替换。
该方案上线后运行稳定,解密耗时约80ms(模型大小120MB),对推理性能影响小于5%。复盘经验是:加密方案必须与客户的实际IT环境匹配——如果客户没有硬件加密狗,就需要改用软件密钥+白盒加密方案。