3. 固件签名机制:数字签名原理、RSA与ECDSA签名算法、固件签名流程设计

说到固件保护,签名机制是绕不开的核心防线。我入行那会儿,很多设备连签名校验都没有,烧个固件进去就能跑。后来出了几次严重的OTA攻击事件,大家才意识到——没有签名的固件,就像没上锁的房门。

这一章,我们就把数字签名的原理、两种主流算法(RSA和ECDSA)的选型对比,以及实际项目中怎么设计签名流程,一次性讲透。

3.1 数字签名:它到底解决了什么问题?

数字签名不是加密。加密是为了不让别人看,签名是为了证明“这东西是我发的,而且没被改过”。

你想想看,固件从开发机传到设备,中间可能经过服务器、CDN、下载工具,任何一个环节被篡改,设备刷进去的就是恶意代码。签名机制要保证三点:

  • 完整性:固件内容没有被篡改
  • 真实性:固件确实来自合法的发布者
  • 不可否认性:发布者不能事后抵赖说“这不是我签的”

实现方式其实不复杂:发布者用私钥对固件的哈希值签名,设备端用公钥验证签名。私钥只有发布者有,公钥可以公开。只要签名验证通过,固件就是可信的。

核心逻辑一句话:私钥签名,公钥验签。私钥泄露,一切归零。

3.2 RSA vs ECDSA:两种签名算法的实战对比

目前嵌入式领域最常用的签名算法就是RSA和ECDSA。我在项目中两种都用过,各有优劣,选型时得看具体场景。

3.2.1 RSA:老牌劲旅,成熟稳定

RSA基于大整数分解的数学难题。简单说,两个大质数相乘很容易,但把乘积分解回去极难。RSA签名就是利用这个不对称性。

RSA的优点很明显:

  • 算法成熟,几乎所有加密库都支持
  • 验签速度快,适合资源受限的设备
  • 密钥长度灵活(1024/2048/4096位)

但缺点也突出:

  • 签名体积大(2048位RSA签名是256字节)
  • 生成密钥对和签名速度慢
  • 密钥越长,性能开销越大

我的经验:在MCU上做RSA-2048验签,一般需要几十到几百毫秒。如果设备对启动时间敏感,这个延迟得提前评估。

3.2.2 ECDSA:后起之秀,短小精悍

ECDSA基于椭圆曲线离散对数难题。同样的安全强度下,ECDSA的密钥长度和签名体积都比RSA小得多。

举个例子:

  • RSA-2048的安全强度 ≈ ECDSA-256
  • RSA签名256字节 vs ECDSA签名64字节
  • RSA公钥256字节 vs ECDSA公钥32字节

ECDSA的缺点:

  • 验签比RSA慢(尤其在没有硬件加速的MCU上)
  • 对随机数质量要求极高——随机数重复会直接泄露私钥
  • 实现复杂度更高,容易踩坑

我曾经踩过的坑:某项目用了ECDSA,但随机数生成器在启动初期熵不够,导致签名重复。幸好测试阶段发现了,否则私钥就暴露了。后来我们强制在签名前先做熵池刷新。

3.2.3 选型对比表

对比维度 RSA-2048 ECDSA-256
安全强度 112位 128位
签名体积 256字节 64字节
公钥体积 256字节 32字节
签名速度 慢 快
验签速度 快 慢
随机数依赖 低 高
硬件加速支持 广泛 逐渐普及

我个人习惯:如果设备有硬件加速或验签次数少,优先用ECDSA;如果设备性能弱但需要频繁验签,RSA更稳妥。

3.3 固件签名流程设计:从开发到量产

理论说完了,咱们看看实际项目中怎么落地。下面是我在多个产品中验证过的签名流程。

3.3.1 整体流程

固件签名不是单点操作,而是一条完整的信任链。我把它分成三个阶段:

  1. 开发阶段:生成密钥对,安全存储私钥
  2. 构建阶段:对固件计算哈希,用私钥签名,打包固件+签名+公钥证书
  3. 部署阶段:设备验签,通过则启动,失败则拒绝

下面这张图展示了完整的签名与验证流程:

固件签名与验证流程 固件签名与验证流程 签名端(开发/构建服务器) 固件二进制(.bin / .hex) 计算哈希值(SHA-256) 私钥签名 → 生成签名值 打包:固件 + 签名 + 公钥证书 安全通道 验证端(目标设备) 接收固件包 提取签名 + 公钥证书 用公钥验签 通过 → 启动 失败 → 拒绝 说明:私钥必须离线安全存储,公钥可嵌入固件或使用证书链 验签失败时,设备应进入恢复模式,不可执行未签名固件

3.3.2 密钥管理:最容易被忽视的环节

算法选得再好,密钥管理一塌糊涂,整个安全体系就白搭。我见过不止一个项目,私钥直接放在Git仓库里——这跟把银行卡密码贴在ATM机上没区别。

几个关键原则:

  • 私钥离线存储:只在专用的签名服务器上使用,不联网
  • 密钥分级:开发密钥、测试密钥、生产密钥严格分开
  • 定期轮换:生产密钥建议每年更换一次
  • 访问控制:签名操作需要多人审批,不能一个人说了算

3.3.3 验签的工程实现要点

设备端的验签代码,我建议放在启动流程的最早期,最好在Flash驱动初始化之后、主程序加载之前。这样即使固件被篡改,恶意代码也没有机会执行。

一个典型的验签伪代码:

// 固件签名验证伪代码
int verify_firmware(const uint8_t* firmware, uint32_t size,
                    const uint8_t* signature, uint32_t sig_len,
                    const uint8_t* public_key, uint32_t key_len) {
    uint8_t hash[32];
    
    // 1. 计算固件哈希
    sha256(firmware, size, hash);
    
    // 2. 验签
    if (ecdsa_verify(hash, 32, signature, sig_len, public_key, key_len) != 0) {
        return -1;  // 验签失败
    }
    
    // 3. 验签通过,跳转到固件入口
    jump_to_firmware(firmware);
    return 0;
}

注意:验签失败后,设备不能静默跳过。我见过一个产品,验签失败只是打印了日志,然后继续启动——攻击者直接替换了固件,设备照跑不误。正确的做法是:验签失败就进入安全恢复模式,或者直接停机。

3.4 签名方案的选型建议

说了这么多,到底怎么选?我根据项目经验总结了几条:

  • 资源极度受限(如8位MCU):用RSA-1024或HMAC(如果信任链能保证)
  • 主流ARM Cortex-M系列:ECDSA-256,配合硬件加速更好
  • 高性能应用处理器(如Linux设备):RSA-2048或ECDSA-256都行,看生态支持
  • 安全等级要求极高(如金融、车规):RSA-4096或ECDSA-384,配合HSM硬件

嗯,这里要特别提醒:不要自己实现签名算法。用现成的加密库(mbedTLS、OpenSSL、MicroECC),它们经过了大量安全审计,比自己写的靠谱得多。

我的习惯:每次发布新固件,我都会在签名后做一次完整的验签测试——用正确的签名验一次,再用篡改过的固件验一次,确保失败路径也正确。这个习惯帮我发现过好几次签名脚本的bug。

固件签名不是银弹,但它是整个启动信任链的基石。没有签名,后续的加密存储、安全启动、远程证明都无从谈起。下一层我们要聊的是如何把签名和启动流程结合起来,形成完整的信任链——不过那是后面的事了。