2. 固件签名与验签:数字签名原理、哈希算法选择、签名流程与验签流程
好,咱们进入正题。上一章聊了OTA的整体架构,这一章我重点说说固件签名与验签。说白了,这就是给固件加一把“电子锁”,确保它在传输和存储过程中没被人动过手脚。
你想想看,如果固件在下载过程中被篡改了,设备刷入恶意代码,后果不堪设想。我早年做智能门锁项目时就遇到过类似问题——当时没做签名校验,结果测试阶段发现固件被中间人替换了,门锁直接“罢工”。从那以后,签名验签就成了我设计OTA方案时的铁律。
2.1 数字签名原理:为什么需要它?
数字签名,本质上是一种非对称加密的应用。它解决三个核心问题:
- 完整性:固件有没有被篡改?
- 真实性:固件是不是来自合法的发布者?
- 不可否认性:发布者不能抵赖自己签过这个固件。
原理其实不复杂。我习惯用一个比喻来解释:
你把固件想象成一封信。签名就是用你的私钥在信上盖一个“印章”。别人收到信后,用你的公钥来验证这个印章是不是真的。如果印章对得上,说明信没被拆过,而且确实是你发的。
嗯,这里要注意:私钥必须绝对保密。一旦泄露,整个信任体系就崩塌了。我在一个工业网关项目里,客户把私钥直接硬编码在编译脚本里,结果被内部员工泄露了...后来我们花了整整两周才完成密钥轮换。
2.2 哈希算法选择:别选错了
签名之前,通常先对固件做哈希运算。为什么?因为直接对几百兆的固件做非对称加密,性能扛不住。哈希算法能把任意长度的数据压缩成固定长度的摘要,然后再对这个摘要签名。
哈希算法的选择,我建议遵循以下原则:
| 算法 | 摘要长度 | 安全性 | 我的建议 |
|---|---|---|---|
| MD5 | 128位 | 已破解,不推荐 | 千万别用,碰撞攻击太容易了 |
| SHA-1 | 160位 | 已过时,有理论攻击 | 别用了,我见过有人因此被审计卡住 |
| SHA-256 | 256位 | 安全,目前主流 | 首选,资源消耗适中 |
| SHA-384/512 | 384/512位 | 更安全,但开销大 | 对安全性要求极高的场景使用 |
核心建议:对于绝大多数IoT设备,SHA-256是性价比最高的选择。既保证了安全性,又不会给MCU带来太大负担。
我曾经在一个资源受限的蓝牙设备上,被迫使用SHA-1,因为SHA-256的计算时间超过了OTA的超时窗口。后来我们优化了算法实现,用硬件加速模块才解决了问题。所以,选算法时一定要考虑目标平台的算力。
2.3 签名流程:一步步来
签名流程,说白了就是给固件“盖章”的过程。我把它拆成五个步骤:
- 计算固件哈希:用选定的哈希算法(比如SHA-256)对固件二进制文件计算摘要。
- 签名摘要:用发布者的私钥对摘要进行非对称加密,生成签名数据。
- 打包固件:将原始固件、签名数据、公钥证书(或公钥指纹)打包成一个升级包。
- 添加元数据:在升级包头部写入固件版本、设备型号、签名算法标识等信息。
- 分发:将完整的升级包上传到OTA服务器。
代码示例(伪代码,展示核心逻辑):
// 签名端流程
function signFirmware(firmwareBinary, privateKey):
// 1. 计算哈希
hash = SHA256(firmwareBinary)
// 2. 签名
signature = RSA_Sign(privateKey, hash)
// 3. 打包
package = {
firmware: firmwareBinary,
signature: signature,
algorithm: "SHA256withRSA",
version: "2.1.0"
}
return package
我的小技巧:签名时最好在哈希计算前加入一个“盐值”(比如固件版本号),这样可以防止重放攻击。我习惯把版本号拼在固件数据前面再算哈希。
2.4 验签流程:设备端怎么验证?
验签是签名的逆过程。设备收到升级包后,需要验证它是不是合法的。流程如下:
- 解析升级包:提取出原始固件、签名数据和元数据。
- 计算本地哈希:用同样的哈希算法对原始固件计算摘要。
- 解密签名:用预置的公钥解密签名数据,得到原始的哈希值。
- 比对哈希:比较本地计算的哈希和解密得到的哈希是否一致。
- 判断结果:如果一致,验签通过;否则拒绝升级。
代码示例:
// 验签端流程
function verifyFirmware(package, publicKey):
// 1. 解析
firmware = package.firmware
signature = package.signature
algorithm = package.algorithm
// 2. 计算本地哈希
localHash = SHA256(firmware)
// 3. 解密签名
decryptedHash = RSA_Decrypt(publicKey, signature)
// 4. 比对
if localHash == decryptedHash:
return VERIFY_PASS
else:
return VERIFY_FAIL
注意! 公钥必须安全地存储在设备上。我见过有人把公钥放在外部Flash里,结果被攻击者替换了。正确的做法是:将公钥烧录在一次性可编程存储器(OTP)或安全元件中,并且做防读保护。
嗯,这里还要提一个坑:验签失败后的处理逻辑。我曾经在一个项目中,设备验签失败后只是打印了一条日志,然后继续执行升级。结果可想而知——设备变砖了。正确的做法是:验签失败立即终止升级流程,并上报错误状态。
2.5 防回滚与签名的关系
签名和防回滚是两兄弟,但很多人容易搞混。签名保证固件没被篡改,防回滚保证固件版本不会倒退到有漏洞的旧版本。
我通常的做法是:在签名数据中包含版本号信息。这样,设备在验签时不仅能验证固件的完整性,还能检查版本号是否大于当前运行版本。一举两得。
举个例子:
// 签名时包含版本号
dataToSign = version + firmwareBinary
signature = RSA_Sign(privateKey, SHA256(dataToSign))
// 验签时提取版本号
extractedVersion = package.version
if extractedVersion <= currentVersion:
reject("版本过低,拒绝升级")
总结一下:签名验签是OTA安全的基石。选对哈希算法、保护好私钥、在设备端安全存储公钥、验签失败坚决不升级——做到这四点,你的OTA安全就成功了一大半。
下一章我会深入讲密钥管理,包括密钥生成、存储和轮换策略。到时候我会分享一个我踩过的坑——密钥过期导致整个产品线无法升级的惨痛教训。咱们下章见。