固件签名基础:数字签名原理、哈希算法与非对称加密
各位好,欢迎来到第二章。上一章我们聊了固件安全的整体威胁模型,今天要扎进去讲一个最核心的防御手段——固件签名。
说实话,我刚入行那会儿,觉得签名就是个「加个密」的事儿。直到有一次,我在一个 IoT 项目里发现,固件明明有签名,但设备照样被刷了恶意版本。查了半天,原来是签名算法选错了,哈希碰撞概率高得吓人。嗯,从那以后,我对签名这件事再也不敢马虎了。
这一章,我会把数字签名的三个基石——哈希算法、非对称加密、签名与验签流程——掰开揉碎讲清楚。你想想看,没有这些基础,后面证书链联动根本玩不转。
2.1 数字签名到底在干什么?
先问个问题:你收到一个固件包,怎么确认它真的是厂商发的,而且没被改过?
数字签名就是干这个的。它不加密固件内容,而是给固件打一个「防伪标签」。这个标签只有厂商能生成,但任何人都能验证。
核心逻辑:
- 签名 = 私钥加密(哈希(固件))
- 验签 = 公钥解密(签名) == 哈希(固件)
说白了,就是「私钥签名,公钥验签」。私钥只有厂商有,公钥可以公开。
我个人的习惯是,把签名想象成「数字印章」。你盖个章,别人拿你的印泥对比一下就知道真假。但数字世界比物理世界严格得多——伪造一个物理印章可能,伪造一个 RSA 签名?几乎不可能。
2.2 哈希算法:固件的「指纹」
签名之前,第一步是对固件做哈希。为什么?因为固件可能很大,几 MB 甚至几百 MB,直接用非对称加密去算,性能扛不住。哈希能把任意长度的数据,压缩成一个固定长度的摘要(比如 256 位)。
哈希算法有几个关键特性:
- 单向性:从摘要反推原始数据,理论上不可能。
- 抗碰撞性:很难找到两个不同的输入,产生相同的摘要。
- 雪崩效应:输入改一个比特,摘要完全变样。
我在项目中遇到过最坑的事,就是有人用了 MD5 做固件签名。MD5 的碰撞攻击早在 2004 年就被公开了,两个不同固件能算出同一个 MD5 值。你想想看,攻击者完全可以伪造一个恶意固件,但签名校验照样通过。所以,MD5 和 SHA-1 现在绝对不能用。
避坑指南:
我曾经在一个遗留项目里看到固件签名还在用 SHA-1,理由是「兼容旧设备」。我直接建议客户做 OTA 升级,把哈希算法强制切换到 SHA-256。SHA-1 的碰撞攻击成本已经低到几千美元,对于固件安全来说,这就是个定时炸弹。
2.2.1 SHA-256 vs SHA-384:怎么选?
目前工业界最常用的两个哈希算法是 SHA-256 和 SHA-384。它们都属于 SHA-2 家族,安全性经过十几年验证,没问题。
| 特性 | SHA-256 | SHA-384 |
|---|---|---|
| 输出长度 | 256 位(32 字节) | 384 位(48 字节) |
| 安全强度 | 128 位 | 192 位 |
| 性能 | 较快 | 较慢(约慢 30-40%) |
| 适用场景 | 大多数嵌入式设备 | 高安全等级、证书链 |
我个人建议:普通 IoT 设备用 SHA-256 就够了。性能开销小,安全强度 128 位,对于固件签名来说绰绰有余。但如果你在做车规级或者金融级设备,或者签名要跟证书链联动(后面章节会讲),那 SHA-384 更稳妥——它跟 ECC P-384 曲线是黄金搭档。
小技巧:
在嵌入式 MCU 上做哈希计算时,注意用硬件加速模块。很多芯片(比如 STM32、NXP i.MX)内置了 SHA 硬件引擎,算一次 1MB 固件的哈希,从几十毫秒降到几百微秒。别傻乎乎用软件库去算,功耗和时延都扛不住。
2.3 非对称加密:公钥与私钥的博弈
哈希搞定了,接下来就是签名本身。非对称加密是签名的核心——用私钥签名,用公钥验签。
目前主流的两大阵营是 RSA 和 ECC。我两个都用过,各有优劣,下面细说。
2.3.1 RSA:老牌劲旅
RSA 基于大整数分解难题。简单说,两个大质数相乘很容易,但把乘积分解回质数,难到爆炸。
- 密钥长度:通常 2048 位或 4096 位。1024 位已经被破解,别用了。
- 签名速度:私钥签名较慢,公钥验签较快。
- 签名大小:等于密钥长度。2048 位 RSA 签名就是 256 字节。
我在一个智能门锁项目里用过 RSA-2048。当时 MCU 主频只有 200MHz,验签一次要 80 毫秒。用户开门时等 80 毫秒?体验太差了。后来我换成了 ECC,验签时间降到了 10 毫秒以内。
注意:
RSA 的私钥操作(签名)比公钥操作(验签)慢得多。如果你的设备需要频繁签名(比如 OTA 服务器),RSA 的 CPU 开销会很高。但验签场景(比如设备端)RSA 反而有优势,因为公钥指数通常选 65537,计算量小。
2.3.2 ECC:后起之秀
ECC 基于椭圆曲线离散对数难题。同样的安全强度下,ECC 的密钥长度比 RSA 短得多。
| 安全强度 | RSA 密钥长度 | ECC 密钥长度 |
|---|---|---|
| 128 位 | 3072 位 | 256 位(P-256) |
| 192 位 | 7680 位 | 384 位(P-384) |
| 256 位 | 15360 位 | 521 位(P-521) |
你看,ECC 用 256 位就能达到 RSA 3072 位的安全强度。这意味着:
- 签名更小:ECC 签名通常 64 字节(P-256),而 RSA 签名 256 字节。
- 计算更快:尤其是私钥操作,ECC 比 RSA 快一个数量级。
- 存储更省:密钥和证书占用的 Flash 空间更小。
我个人现在做新项目,首选 ECC P-256 或 P-384。除非客户有强制要求(比如某些政府项目指定 RSA),否则我不会再碰 RSA。原因很简单:嵌入式设备的资源太金贵了,能省一点是一点。
选型建议:
如果你的 MCU 没有硬件加速模块,ECC 的曲线运算可能会比较慢。这时候可以评估一下 RSA-2048,因为它的公钥验签可以用小指数优化,纯软件实现也不慢。但如果有硬件加速(很多芯片支持 ECC 点乘),那闭眼选 ECC。
2.4 签名与验签流程:一张图说清楚
理论讲完了,我们来看实际流程。下面这张 SVG 图,把固件签名和验签的完整链路画了出来。
流程其实不复杂:
- 签名方:对固件做哈希 → 用私钥加密哈希 → 得到签名值。
- 传输:固件 + 签名值一起发给设备。
- 验签方:对收到的固件做同样的哈希 → 用公钥解密签名 → 比对两个哈希值。
如果哈希一致,说明固件没被篡改,而且确实是私钥持有者签的。不一致?直接拒绝启动。
关键点:
签名不是加密固件本身,而是加密固件的哈希。这样做有两个好处:一是性能好(哈希计算快),二是签名值固定长度(不管固件多大,签名都是 64 或 256 字节)。
2.5 实际项目中的代码示例
光说不练假把式。下面我用 Python 演示一下 ECC P-256 签名和验签。实际嵌入式开发中会用 C 语言加 mbedTLS 或 OpenSSL,但原理完全一样。
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import (
encode_dss_signature,
decode_dss_signature
)
# 1. 生成 ECC P-256 密钥对
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
# 2. 固件数据(模拟)
firmware = b"Hello, this is my firmware v2.1.0"
# 3. 签名:先哈希,再用私钥签名
signature = private_key.sign(
firmware,
ec.ECDSA(hashes.SHA256())
)
print(f"签名值长度: {len(signature)} 字节") # 通常是 64 字节
# 4. 验签:用公钥验证
try:
public_key.verify(
signature,
firmware,
ec.ECDSA(hashes.SHA256())
)
print("验签通过!固件未被篡改。")
except Exception as e:
print(f"验签失败: {e}")
这段代码里,ec.ECDSA(hashes.SHA256()) 指定了签名算法是 ECDSA,哈希用 SHA-256。实际项目中,你还需要处理签名值的编码格式(DER 或 RAW),以及把公钥固化到设备 BootROM 里。
经验之谈:
我在一个量产项目里踩过坑:公钥存储在 Flash 里,但没做写保护。攻击者通过调试接口把公钥换成了自己的,然后用自己的私钥签名恶意固件。所以,公钥必须放在只读区域,最好熔断 OTP 或者用安全元件存储。否则签名算法再强也没用。
2.6 本章小结
这一章我们覆盖了固件签名的三个核心:
- 哈希算法:SHA-256 是主流,SHA-384 用于高安全场景。MD5/SHA-1 已经废了,别碰。
- 非对称加密:ECC 比 RSA 更省资源,新项目优先考虑 ECC P-256 或 P-384。
- 签名流程:先哈希再签名,验签时比对摘要。流程简单,但密钥存储是命门。