4. ONNX模型签名与验签:数字签名基础(RSA/ECDSA)
模型部署到生产环境后,最怕什么?
我个人最怕的是模型被人偷偷换了。你想想看,辛辛苦苦训练了几个月的模型,推理效果刚刚的,结果被人篡改了一下,输出全乱套了。更可怕的是,如果模型被植入了后门,那后果不堪设想。
所以这一章,我们来聊聊怎么给ONNX模型加上「数字签名」。说白了,就是给模型盖个章,证明它是你发布的、没有被改过。
4.1 数字签名是个啥?
数字签名不是加密。加密是为了不让别人看,签名是为了证明「这是我写的,而且没被改过」。我经常跟团队新人说:加密是锁门,签名是盖章。
数字签名有两个核心作用:
- 身份认证:证明这个模型确实是你签发的
- 完整性校验:证明模型没有被篡改过
嗯,这里要注意:签名本身不保护模型内容的机密性。如果你既要保密又要防篡改,那就得先加密再签名,或者用签密方案。
4.2 RSA vs ECDSA:选哪个?
目前主流的签名算法就两个:RSA和ECDSA。我在项目中两种都用过,说说我的感受。
| 特性 | RSA | ECDSA |
|---|---|---|
| 密钥长度 | 2048位起步 | 256位就够用 |
| 签名速度 | 较慢 | 快 |
| 验签速度 | 快 | 较快 |
| 安全性 | 成熟可靠 | 同等安全强度下密钥更短 |
| 兼容性 | 几乎所有平台都支持 | 部分老旧平台不支持 |
我个人习惯:如果是边缘设备部署,优先选ECDSA。因为密钥短、签名快,对资源受限的设备更友好。如果是云端部署,RSA也没问题,反正服务器算力充足。
4.3 对ONNX模型进行签名
签名流程其实不复杂。核心思路是:对模型的二进制内容做哈希,然后用私钥对哈希值签名。
具体怎么做?我直接上代码。
import hashlib
import onnx
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization
# 1. 生成RSA密钥对
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
# 2. 加载ONNX模型
model = onnx.load("model.onnx")
model_bytes = model.SerializeToString()
# 3. 计算模型哈希
model_hash = hashlib.sha256(model_bytes).digest()
# 4. 用私钥签名
signature = private_key.sign(
model_hash,
padding.PKCS1v15(),
hashes.SHA256()
)
# 5. 将签名嵌入模型(作为元数据)
meta = model.metadata_props.add()
meta.key = "signature"
meta.value = signature.hex()
# 6. 保存签名后的模型
onnx.save(model, "model_signed.onnx")
这里有个细节:签名要放在模型的metadata里。ONNX的metadata是专门用来存这些附加信息的,不会影响模型的计算图。
4.4 签名验证流程
验签是签名的逆过程。部署端拿到模型后,需要做以下几步:
- 从模型metadata中提取签名
- 重新计算模型的哈希值
- 用公钥验证签名是否匹配
代码实现如下:
import onnx
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
def verify_model_signature(model_path, public_key):
# 1. 加载模型
model = onnx.load(model_path)
model_bytes = model.SerializeToString()
# 2. 提取签名
signature_hex = None
for prop in model.metadata_props:
if prop.key == "signature":
signature_hex = prop.value
break
if signature_hex is None:
raise ValueError("模型没有签名!")
# 3. 计算哈希
model_hash = hashlib.sha256(model_bytes).digest()
# 4. 验签
try:
public_key.verify(
bytes.fromhex(signature_hex),
model_hash,
padding.PKCS1v15(),
hashes.SHA256()
)
return True
except Exception as e:
print(f"验签失败:{e}")
return False
为什么验签要放在部署端?因为部署环境是模型被攻击的最后一道防线。模型在传输过程中、存储过程中都可能被篡改,只有在加载到内存之前做验签,才能确保模型是安全的。
4.5 防止模型被篡改
签名能防篡改,但前提是公钥是安全的。如果攻击者替换了你的公钥,那他就可以用自己的私钥签名一个恶意模型,然后你的验签流程照样能通过。
所以,公钥的存储和分发才是关键。我见过几种做法:
- 硬编码在代码里:简单粗暴,但代码被反编译就完了
- 放在安全芯片里:硬件级保护,成本高
- 从远程服务器获取:需要网络,且服务器本身要可信
- 集成在部署框架中:比如ONNX Runtime的custom operator
我个人比较推荐的做法是:公钥放在部署框架的配置文件中,并对配置文件本身做完整性校验。这样既灵活又相对安全。
核心要点总结:
- 数字签名 ≠ 加密,签名是为了防篡改和身份认证
- RSA成熟稳定,ECDSA适合资源受限场景
- 签名要基于模型的原始二进制哈希
- 验签必须在模型加载到内存之前完成
- 公钥的安全存储比签名算法本身更重要
嗯,这一章的内容就这些。下一章我们会聊更高级的话题——ONNX模型的加密部署。到时候会讲到怎么把整个模型加密,运行时再解密加载。感兴趣的话,我们继续往下走。