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

说到固件签名,我得先讲个真实教训。几年前我接手一个IoT设备项目,固件升级没做签名校验。结果呢?产品上市三个月,就被黑客用伪造固件批量刷机,变成了挖矿肉鸡。那场面,真是惨不忍睹。从那以后,我对签名机制的敬畏心就刻在骨子里了。

说白了,数字签名就是给固件贴上一张「防伪标签」。这张标签只有你(固件发布者)能贴,但任何人都能验证。它解决的是三个核心问题:完整性(固件没被改过)、真实性(确实是官方发布的)、不可否认性(你不能赖账说没发过这个版本)。

核心概念速览

  • 私钥:只有你持有,用来生成签名。丢了就完了。
  • 公钥:公开的,任何人都能用它验证签名。
  • 签名:对固件哈希值用私钥加密后的结果。
  • 验证:用公钥解密签名,跟固件哈希比对。

3.1 数字签名原理:先搞懂流程

我习惯把签名过程拆成三步走。你想想看,就像你给重要文件盖公章:

  1. 哈希摘要:对固件二进制做SHA-256哈希,得到一个固定长度的摘要。这一步是为了把任意大小的固件压缩成「指纹」。
  2. 私钥加密:用你的私钥对这个哈希摘要进行加密运算,生成签名数据。
  3. 打包发布:把原始固件 + 签名数据 + 公钥证书(可选)一起打包发布。

验证端收到固件后,做反向操作:

  1. 对收到的固件重新计算哈希。
  2. 用公钥解密签名,得到原始哈希。
  3. 两个哈希比对。一致?固件可信。不一致?直接拒绝启动。

我的经验:哈希算法一定要选对。SHA-1已经废了,别用。我个人项目里统一用SHA-256,安全性和性能平衡得最好。SHA-512虽然更安全,但在低端MCU上跑起来太慢,不划算。

这里我画了一张流程图,把签名和验证的完整路径展示出来。你看一眼就明白了:

固件签名与验证流程 固件签名与验证流程 签名端(发布者) 原始固件 SHA-256 哈希运算 哈希摘要 (32字节) 私钥签名 (RSA/ECDSA) 签名数据 固件 + 签名 打包发布 验证端(设备端) 接收固件 + 签名 重新计算 SHA-256 哈希 公钥解密签名 哈希比对 ✅ 一致 → 启动 ❌ 不一致 → 拒绝 ⚠️ 关键安全要点 私钥必须离线存储,绝不能放在编译服务器或代码仓库里。 公钥在设备端要烧写在一次性写入的OTP区域,防止被篡改。

3.2 RSA vs ECDSA:选哪个?

这是我在项目里被问得最多的问题。RSA和ECDSA都能做签名,但性格完全不同。我直接给你对比表,一目了然:

对比维度 RSA ECDSA
安全强度 2048位 ≈ 112位安全级别 256位 ≈ 128位安全级别
密钥长度 2048位(256字节) 256位(32字节)
签名长度 256字节(固定) 64字节(固定)
签名速度 较慢(私钥操作) 较快
验证速度 较快(公钥操作) 较慢
硬件支持 几乎所有MCU都支持 较新MCU才支持
推荐场景 资源充足的设备 资源受限的IoT设备

避坑指南:我曾经在一个项目里选了RSA 1024位,觉得够用了。结果第二年NIST就建议淘汰1024位,搞得我们紧急升级到2048位,固件体积暴涨,Flash差点塞不下。现在我的底线是:RSA至少2048位,ECDSA至少256位。别贪便宜用短密钥,省那点空间不够你擦屁股的。

3.3 签名验证流程:代码怎么写?

理论说完了,咱们上点干货。我拿一个实际项目里的代码片段给你看。这是设备端用mbedTLS做ECDSA签名验证的核心逻辑:

#include <mbedtls/pk.h>
#include <mbedtls/sha256.h>
#include <mbedtls/ecdsa.h>

int verify_firmware_signature(
    const uint8_t *firmware, size_t fw_len,
    const uint8_t *signature, size_t sig_len,
    const uint8_t *public_key, size_t key_len)
{
    int ret;
    mbedtls_pk_context pk;
    mbedtls_sha256_context sha256_ctx;
    uint8_t hash[32];
    
    // 1. 初始化公钥上下文
    mbedtls_pk_init(&pk);
    ret = mbedtls_pk_parse_public_key(&pk, public_key, key_len);
    if (ret != 0) {
        printf("公钥解析失败: -0x%04X\n", -ret);
        goto cleanup;
    }
    
    // 2. 计算固件SHA-256哈希
    mbedtls_sha256_init(&sha256_ctx);
    mbedtls_sha256_starts(&sha256_ctx, 0);  // 0 = SHA-256, 1 = SHA-224
    mbedtls_sha256_update(&sha256_ctx, firmware, fw_len);
    mbedtls_sha256_finish(&sha256_ctx, hash);
    mbedtls_sha256_free(&sha256_ctx);
    
    // 3. 验证签名
    ret = mbedtls_pk_verify(
        &pk, 
        MBEDTLS_MD_SHA256,
        hash, sizeof(hash),
        signature, sig_len
    );
    
    if (ret == 0) {
        printf("✅ 签名验证通过!固件可信。\n");
    } else {
        printf("❌ 签名验证失败!固件可能被篡改。\n");
    }

cleanup:
    mbedtls_pk_free(&pk);
    return ret;
}

我的习惯:验证签名之前,我总会先检查固件头部的一个魔数(Magic Number)。比如前4个字节固定为0xDEADBEEF。这能快速过滤掉明显无效的固件,避免在垃圾数据上跑签名验证浪费CPU。算是个小优化,但在量产线上能省不少时间。

3.4 实际部署中的几个坑

嗯,这里要注意。签名机制不是写完代码就完事了。部署环节有几个坑,我一个个说:

  • 公钥存储位置:公钥必须放在设备的安全存储区,比如OTP(一次性可编程)区域或者安全元件(SE)里。别放Flash里,黑客改个字节就能绕过验证。我曾经见过一个产品,公钥放在外部SPI Flash里,结果被替换公钥后直接刷入恶意固件。
  • 签名验证时机:我建议在Bootloader阶段就做验证,而不是等系统启动后再做。因为一旦恶意固件跑起来了,它可能篡改验证逻辑本身。记住:信任链要从最底层开始。
  • 回滚攻击防护:签名只能保证固件是官方发布的,但防不住黑客拿旧版本固件(有已知漏洞)来刷机。解决方案是加一个版本号计数器,只允许升级,不允许降级。我在一个车联网项目里吃过这个亏,后来老老实实加了防回滚机制。
  • 密钥管理流程:私钥的保管是个大问题。我见过有人把私钥放在Git仓库里,结果代码泄露后整个产品线都得换密钥。正确的做法是:私钥放在离线HSM(硬件安全模块)里,签名操作在HSM内部完成,私钥永远不离开硬件。

总结一下:数字签名是固件安全的第一道防线。RSA和ECDSA各有优劣,选型时要综合考虑MCU性能、Flash空间和安全需求。验证流程要放在Bootloader里,公钥要锁死在OTP里,私钥要锁死在HSM里。这三条做到了,你的固件安全就及格了。