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 签名流程:一步步来

签名流程,说白了就是给固件“盖章”的过程。我把它拆成五个步骤:

  1. 计算固件哈希:用选定的哈希算法(比如SHA-256)对固件二进制文件计算摘要。
  2. 签名摘要:用发布者的私钥对摘要进行非对称加密,生成签名数据。
  3. 打包固件:将原始固件、签名数据、公钥证书(或公钥指纹)打包成一个升级包。
  4. 添加元数据:在升级包头部写入固件版本、设备型号、签名算法标识等信息。
  5. 分发:将完整的升级包上传到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 验签流程:设备端怎么验证?

验签是签名的逆过程。设备收到升级包后,需要验证它是不是合法的。流程如下:

  1. 解析升级包:提取出原始固件、签名数据和元数据。
  2. 计算本地哈希:用同样的哈希算法对原始固件计算摘要。
  3. 解密签名:用预置的公钥解密签名数据,得到原始的哈希值。
  4. 比对哈希:比较本地计算的哈希和解密得到的哈希是否一致。
  5. 判断结果:如果一致,验签通过;否则拒绝升级。

代码示例:

// 验签端流程
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安全就成功了一大半。

下一章我会深入讲密钥管理,包括密钥生成、存储和轮换策略。到时候我会分享一个我踩过的坑——密钥过期导致整个产品线无法升级的惨痛教训。咱们下章见。