4、模型文件格式与结构:常见模型格式(如ONNX、PyTorch、TensorFlow)及其存储特点

在模型加密的实操链路中,第一步往往不是直接编写加密代码,而是先弄清楚“要加密的对象到底是什么”。模型文件格式决定了参数在磁盘上的排列方式、元数据的组织逻辑、算子的描述方法以及运行时加载的入口。如果对格式理解不透彻,后续的权重保护、算子混淆、加密容器设计都可能建立在错误的地基上。本章将系统拆解ONNX、PyTorch(Pickle/Zip)、TensorFlow(SavedModel/Checkpoint)三种主流格式的存储结构,并分析它们在加密场景下的关键差异。

常见模型格式存储结构与加密切入点 ONNX • Protobuf 序列化 • 静态图 + 权重内嵌 • 算子级元数据 • 适合跨框架部署 加密点:整体文件加密/签名 或转ONNX后做算子混淆 PyTorch • Zip 容器 (Pickle) • 动态图 + Python对象 • 权重/结构分离存储 • 易被反序列化攻击 加密点:自定义Zip加密容器 或权重张量级AES加密 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运行时。

存储结构拆解

层级组件存储内容加密关注点
文件级ModelProtoir_version、opset_import、producer_name整体文件可被加密或签名
图级GraphProto节点列表、输入输出张量名、初始值图结构暴露算子逻辑
节点级NodeProtoop_type、attribute、输入输出名称算子类型可被混淆替换
权重级TensorProtoraw_data(原始字节)、dims、data_type权重可直接提取并篡改

实战场景:ONNX模型加密的两种路径

在实际项目中,ONNX模型常被用于边缘设备或跨框架推理。加密时有两种典型路径:路径一是整体文件加密,将整个.onnx文件用AES-256-GCM加密,运行时在内存中解密并加载,适用于对文件分发管控严格的场景;路径二是权重张量级加密,仅对TensorProto中的raw_data字段进行加密,保留图结构明文,这样攻击者即使看到图结构也无法获取有效权重。路径二的优势在于可以配合模型剪枝或量化,只保护关键层。

风险提示:ONNX的Protobuf序列化是确定性的,同一模型多次序列化结果完全一致。这意味着攻击者可以通过对比不同版本的ONNX文件,定位权重字节的偏移位置,从而实施针对性攻击。因此,单纯依赖格式本身的“隐蔽性”是不安全的,必须配合加密算法。

常见误区

一个常见误区是认为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()存在巨大风险。推荐的做法是:

  1. 使用weights_only=True参数(PyTorch 2.0+),限制Pickle只加载基本张量类型,拒绝自定义类。
  2. 自定义Unpickler,重写find_class()方法,白名单允许的类。
  3. 先解压Zip,检查文件列表,确认没有可疑的.py或.so文件。
  4. 在沙箱环境中先加载一次,观察是否有异常网络请求或文件操作。
加密实践:对于PyTorch模型,推荐采用“张量级加密”方案。将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.pbGraphDef(计算图)Protobuf二进制可整体加密或签名
variables/variables.index变量名到分片偏移的映射Protobuf文本/二进制可加密索引表
variables/variables.data-*权重张量原始字节自定义二进制分片核心加密对象
assets/词表、配置文件等任意格式按需加密

实战场景:TensorFlow Serving中的模型保护

在TensorFlow Serving场景中,模型文件通常存放在共享存储上。一个实际案例是:某金融公司需要将风控模型部署到多个边缘节点,但担心模型被运维人员复制泄露。解决方案是:

  1. 在模型导出时,对variables.data-*文件使用AES-256-GCM加密,密钥由KMS管理。
  2. 编写自定义的SessionBundle加载器,在加载模型前先解密变量文件到内存临时文件(使用memfd_create避免落盘)。
  3. 加载完成后立即删除临时文件,并锁定进程内存防止swap泄露。

该方案将模型泄露风险从“文件被复制即泄露”降低到“需要同时获取密文和密钥”的级别,显著提升了安全性。

常见误区

一个常见误区是只加密saved_model.pb而忽略variables/目录。实际上,图结构本身的价值远低于权重参数——攻击者即使拿到明文图结构,没有权重也无法复现模型功能。另一个误区是使用TensorFlow自带的tf.io.write_file配合简单加密,但这种方式容易在临时文件中留下明文副本。

经验总结:选择加密方案时,先问三个问题:①模型在什么运行时加载?②攻击者的能力假设是什么(能否拿到文件、能否调试进程)?③性能损耗的容忍度是多少?ONNX适合整体加密,PyTorch适合张量级加密,TensorFlow适合变量文件加密+自定义加载器。

4.4 格式对比与加密选型决策

核心原理

三种格式的差异本质上是“静态图vs动态图”、“Python对象vs纯数据”、“单文件vs多文件”的差异。这些差异直接决定了加密的可行性和效率。

对比分析表

维度ONNXPyTorchTensorFlow
序列化方式ProtobufPickleProtobuf+自定义分片
运行时依赖无Python依赖强依赖PythonC++运行时
权重存储内嵌于TensorProtoPickle内嵌或独立文件独立分片文件
加密粒度文件级/张量级张量级/对象级变量级/文件级
反序列化风险低(纯数据)高(可执行代码)低(纯数据)
推荐加密方案AES-GCM整体加密ChaCha20张量加密+安全加载器变量文件加密+自定义Loader

决策流程

在实际项目中,可以按照以下流程选择加密策略:

  1. 确认部署环境:如果是嵌入式设备,优先选ONNX;如果是Python服务,PyTorch更自然;如果是TensorFlow Serving,则用SavedModel。
  2. 评估威胁模型:如果只是防止普通用户复制,轻量级混淆即可;如果面对专业逆向工程师,必须使用强加密+硬件密钥。
  3. 测试性能损耗:加密解密带来的延迟是否在可接受范围内?对于实时推理,建议使用硬件加速的AES-NI指令集。
  4. 设计密钥管理:密钥不能硬编码在代码中,应使用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环境匹配——如果客户没有硬件加密狗,就需要改用软件密钥+白盒加密方案。

重要提醒:无论选择哪种格式和加密方案,都不要忽略“侧信道攻击”风险。例如,攻击者可以通过监控内存访问模式来推测权重值,或者通过计时攻击获取密钥信息。对于高安全场景,建议结合同态加密或安全多方计算,但这会带来巨大的性能开销,需谨慎权衡。