4、签名算法详解(RSA):从原理到联调实战
RSA 是固件签名领域的老大哥。虽然 ECC 这些年势头很猛,但 RSA 的生态太成熟了,工具链、库、硬件加速器几乎全覆盖。我个人习惯在 Legacy 平台或客户明确要求 RSA 时,首选它。这一章我们把它掰开揉碎,讲清楚原理、密钥长度、填充方案和签名验签流程。
核心要点:RSA 签名不是「加密」,而是用私钥对摘要做数学变换。验签是用公钥还原摘要并比对。理解这个本质,后面联调才不会跑偏。
4.1 RSA 算法原理——一句话讲透
RSA 的安全性建立在「大整数分解」的数学难题上。给你两个大素数 p 和 q,乘起来得到 n 很容易;但给你 n,反推出 p 和 q 几乎不可能。
签名过程说白了就是:
- 私钥签名:s = m^d mod n(m 是填充后的摘要,d 是私钥指数)
- 公钥验签:m' = s^e mod n(e 是公钥指数,m' 应与原始 m 一致)
嗯,这里要注意:RSA 签名用的是私钥,验签用的是公钥。和加密正好反过来。我在项目中遇到过新手把签名和加密搞混,结果联调时怎么都对不上——后来发现是 API 调用反了。
我的习惯:在代码里把签名函数命名为 rsa_sign_with_privkey(),验签命名为 rsa_verify_with_pubkey(),从命名上杜绝混淆。
4.2 密钥长度选择:2048 还是 4096?
这是每次做方案选型都要吵一轮的问题。我直接给结论:
| 密钥长度 | 安全等级 | 签名耗时(典型 MCU) | 签名体积 | 推荐场景 |
|---|---|---|---|---|
| 2048 位 | ≈ 112 位对称密钥 | 50~200 ms | 256 字节 | IoT 设备、资源受限 MCU |
| 4096 位 | ≈ 128 位对称密钥 | 200~800 ms | 512 字节 | 网关、服务器、高安全要求场景 |
我个人建议:绝大多数固件签名场景,2048 位足够了。 为什么?
- 签名耗时在 MCU 上已经是个瓶颈。4096 位的签名操作,在一些低主频芯片上能卡到 1 秒以上,用户升级时体验很差。
- 签名体积翻倍。固件包本身可能只有几百 KB,签名占 512 字节倒不算大,但 OTA 传输和存储都要考虑。
- 安全边际:2048 位在 2030 年之前被攻破的概率极低。除非你的产品要服役 20 年,或者客户是军方/金融级,否则没必要上 4096。
我曾经踩过的坑:有个项目选了 4096 位密钥,结果目标芯片的 RSA 硬件加速器只支持 2048 位。软件模拟 4096 位签名,一次要 3 秒多,最后不得不换芯片。所以选密钥长度前,一定先查芯片的 RSA 引擎规格。
4.3 填充方案:PKCS#1 v1.5 vs PSS
RSA 签名不能直接对原始摘要做模幂运算,必须先填充。填充方案决定了安全性和兼容性。
4.3.1 PKCS#1 v1.5——老牌选手
这是最经典的填充方案,格式固定:
EM = 0x00 || 0x01 || PS || 0x00 || DigestInfo
- PS:填充字节,全是 0xFF,长度补齐到密钥长度
- DigestInfo:哈希算法 OID + 摘要值
优点:几乎所有 RSA 库都支持,兼容性最好。缺点:存在已知的理论攻击(Bleichenbacher 攻击),虽然实际利用条件苛刻,但安全审计时会被揪出来。
4.3.2 PSS——现代推荐
PSS(Probabilistic Signature Scheme)是更安全的替代方案。它引入了随机盐值(salt),每次签名结果都不一样。
EM = maskedDB || H || 0xBC
其中 maskedDB 包含盐值和哈希值,经过掩码函数处理
PSS 有严格的安全证明,没有已知的有效攻击。我现在的项目,只要库支持,一律用 PSS。
| 对比项 | PKCS#1 v1.5 | PSS |
|---|---|---|
| 安全性 | 有理论攻击 | 安全证明完备 |
| 签名确定性 | 确定(相同输入相同输出) | 概率性(每次不同) |
| 兼容性 | 极好 | 较好(需库支持) |
| 推荐场景 | Legacy 系统、互操作性要求高 | 新设计、安全敏感场景 |
我的选择策略:如果是和第三方平台联调,先问对方支持哪种。如果对方说「都行」,我选 PSS。如果对方只支持 v1.5,那就用 v1.5,但会在设计文档里注明安全风险。
4.4 签名与验签流程——手把手拆解
下面这张图把整个流程串起来了。你想想看,从固件二进制到签名文件,再到设备端验签,每一步都有明确的输入输出。
4.4.1 签名端(固件发布方)步骤
- 计算哈希:对固件二进制做 SHA-256,得到 32 字节摘要。
- 填充:按选定的填充方案(PSS 或 v1.5)对摘要进行编码,补齐到密钥长度(2048 位 = 256 字节)。
- 私钥签名:用私钥对填充后的数据做模幂运算,得到签名值。
- 打包:将原始固件、签名值、签名算法标识、公钥哈希等打包成升级文件。
4.4.2 验签端(设备端)步骤
- 解析:从升级包中提取固件和签名值。
- 计算哈希:对提取出的固件做同样的 SHA-256,得到摘要。
- 公钥验签:用公钥对签名值做模幂运算,得到填充后的摘要;再反向解析出原始摘要。
- 比对:将解析出的摘要与本地计算的摘要比对。一致则验签通过,否则拒绝升级。
我曾经遇到的一个隐蔽问题:设备端验签时,公钥是从 Flash 某个固定偏移读取的。有一次产线烧录时把公钥地址写错了,结果设备读到了一串 0xFF,验签永远失败。排查了两天才发现。所以我的建议是:公钥存储后一定要做一次自校验,比如存一份公钥的哈希,启动时先验公钥完整性。
4.5 代码示例——OpenSSL 命令行实战
联调时最常用的就是 OpenSSL 命令行。下面给出一套完整的签名和验签命令,你可以直接复制到终端里跑。
4.5.1 生成密钥对
# 生成 2048 位私钥
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
# 从私钥导出公钥
openssl pkey -in private.pem -pubout -out public.pem
4.5.2 签名(以 PSS 为例)
# 对固件 firmware.bin 做 PSS 签名
openssl dgst -sha256 -sigopt rsa_padding_mode:pss \
-sigopt rsa_pss_saltlen:32 \
-sign private.pem -out firmware.sig firmware.bin
4.5.3 验签
# 验签
openssl dgst -sha256 -sigopt rsa_padding_mode:pss \
-sigopt rsa_pss_saltlen:32 \
-verify public.pem -signature firmware.sig firmware.bin
如果输出 Verified OK,说明验签通过。如果输出 Verification Failure,检查一下:
- 公钥是否和私钥匹配?
- 填充参数是否一致(PSS 的 saltlen 必须相同)?
- 哈希算法是否一致?
联调小技巧:我习惯在签名时把填充参数写进一个 JSON 元数据文件,和设备端代码里的参数做交叉比对。这样联调时一旦失败,先查元数据,不用反复猜参数。
4.6 避坑指南——我踩过的三个坑
- 坑一:哈希算法不匹配。签名端用 SHA-256,设备端代码里写死了 SHA-1。验签永远失败。后来我在固件头里加了一个字段标明哈希算法,设备端根据字段动态选择。
- 坑二:PSS saltlen 不一致。OpenSSL 默认 saltlen 是 32,但有些库默认是 20。联调前先确认双方 saltlen 一致,或者在代码里显式指定。
- 坑三:公钥格式问题。设备端通常需要 DER 格式的裸公钥,而 OpenSSL 默认输出 PEM。记得用
openssl pkey -in public.pem -outform DER -pubout -out public.der转换。