4. RSA签名流程:从密钥生成到验签落地
RSA签名,说白了就是给固件盖一个「防伪章」。你想想看,设备在升级时怎么知道收到的固件是官方发布的,而不是被篡改过的?RSA签名就是干这个的。
我个人习惯把RSA签名拆成四个步骤来理解:生成密钥对 → 计算HASH → 私钥签名 → 公钥验签。每一步都有坑,咱们一个一个说。
核心要点:RSA签名不是加密整个固件,而是加密固件的HASH值。这样既保证了完整性验证,又避免了加解密大文件的性能开销。
4.1 生成RSA密钥对
密钥对包含两个部分:私钥(自己留着,打死也不能给别人)和公钥(可以公开,放到设备里)。
我记得刚入行时,有个同事把私钥直接提交到了Git仓库……嗯,那天的复盘会开了三个小时。从此以后,我们团队定了个规矩:私钥必须用HSM(硬件安全模块)或者至少用加密文件存储。
生成2048位RSA密钥对的命令很简单:
# 生成私钥(PKCS#1格式)
openssl genrsa -out private_key.pem 2048
# 从私钥提取公钥
openssl rsa -in private_key.pem -pubout -out public_key.pem
# 查看密钥信息
openssl rsa -in private_key.pem -text -noout
这里有个关键点:密钥长度。我建议至少用2048位,1024位已经被证明不够安全了。4096位更安全,但签名和验签速度会慢一些,在资源受限的嵌入式设备上尤其明显。
| 密钥长度 | 安全等级 | 签名速度 | 验签速度 | 推荐场景 |
|---|---|---|---|---|
| 1024位 | 不推荐(已破解) | 快 | 快 | 仅用于旧设备兼容 |
| 2048位 | 安全(推荐) | 中等 | 中等 | 大多数嵌入式设备 |
| 4096位 | 高安全 | 慢 | 慢 | 服务器端或高安全场景 |
我的经验:在STM32这类MCU上,2048位RSA验签大约需要100-300ms(取决于主频和硬件加速器)。如果设备对启动时间有严格要求,可以考虑使用ECC(椭圆曲线)签名,密钥更短,速度更快。
4.2 计算固件HASH值
签名之前,先要对固件做一次HASH计算。为什么?因为RSA签名算法本身不适合处理大块数据,而且签名HASH值比签名整个固件要高效得多。
我常用的HASH算法是SHA-256。曾经有个项目用了SHA-1,结果被安全审计打回来了……从那以后,我默认都用SHA-256。
# 计算固件文件的SHA-256 HASH值
openssl dgst -sha256 -binary firmware.bin > firmware.hash
# 查看HASH值(十六进制)
openssl dgst -sha256 firmware.bin
这里要注意一个细节:HASH计算的范围。有些固件格式包含头部信息(版本号、长度、校验和等),签名时到底要不要包含头部?
我个人习惯的做法是:对整个固件文件(包括头部)做HASH。这样任何字节的改动都会被检测到。如果头部信息允许单独修改(比如版本号),那就需要设计更复杂的签名方案,比如只签名固件数据部分,头部单独做完整性保护。
曾经踩过的坑:有一次,我们计算HASH时用了文本模式读取固件文件,结果Windows下把0x0A自动转换成了0x0D 0x0A,导致HASH值对不上。记住:一定要用二进制模式读取固件文件。
4.3 使用私钥签名
签名过程:用私钥对固件的HASH值进行加密,生成签名数据。这个签名数据会附加到固件中,或者单独存储。
# 使用私钥对HASH值进行签名
openssl dgst -sha256 -sign private_key.pem -out firmware.sig firmware.bin
# 查看签名数据(二进制)
hexdump -C firmware.sig | head -20
签名输出的长度取决于密钥长度:2048位RSA签名输出固定为256字节,4096位为512字节。这个长度是固定的,跟固件大小无关。
我在项目中遇到过一个问题:签名时没有指定HASH算法,结果OpenSSL用了默认的SHA-1。验签时设备端用的是SHA-256,两边对不上。所以,签名和验签的HASH算法必须一致,这个看起来简单,但真的很容易忽略。
签名数据格式:RSA签名输出是PKCS#1 v1.5格式(默认)或PSS格式。PKCS#1 v1.5兼容性好,但PSS更安全。我建议新项目直接用PSS模式:openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sign private_key.pem -out firmware.sig firmware.bin
4.4 使用公钥验签
验签是签名的逆过程:用公钥解密签名数据,得到原始的HASH值,然后与固件实际HASH值对比。如果一致,说明固件是完整的、未被篡改的。
# 使用公钥验签
openssl dgst -sha256 -verify public_key.pem -signature firmware.sig firmware.bin
# 输出:Verified OK 或 Verification Failure
在嵌入式设备上,验签通常由Bootloader完成。流程是这样的:
- Bootloader读取固件文件
- 从固件中提取签名数据
- 计算固件实际HASH值
- 用公钥解密签名,得到原始HASH值
- 对比两个HASH值
- 一致则启动,不一致则拒绝启动
我的建议:公钥一定要烧录到设备的OTP(一次性可编程)区域或安全存储区,防止被替换。我曾经见过一个案例,攻击者替换了设备中的公钥,然后用自己生成的私钥签名恶意固件,成功绕过了验签。
4.5 完整流程图
下面这张图把整个RSA签名流程串起来了,你可以对照着看:
4.6 常见问题与避坑指南
做RSA签名联调时,我遇到过不少问题,挑几个典型的说说:
- HASH算法不一致:签名端用SHA-256,验签端用SHA-1,结果永远对不上。检查两端代码中的算法标识。
- 填充模式不匹配:PKCS#1 v1.5和PSS两种模式不兼容。如果设备端只支持PKCS#1 v1.5,签名端也必须用同样的模式。
- 字节序问题:有些嵌入式平台是大端,有些是小端。签名数据在传输时要注意字节序转换。
- 公钥格式:OpenSSL导出的公钥是PEM格式(Base64编码),设备端可能需要原始二进制格式(DER)。记得做格式转换。
曾经踩过的大坑:有一次,我们在签名时用了OpenSSL的默认参数,但设备端的RSA库要求签名数据必须包含完整的DER编码。结果两边解析出来的签名长度不一样——一个256字节,一个270多字节。后来发现是签名输出格式的问题。解决方案:在签名时指定-keyform DER,或者在设备端兼容两种格式。
嗯,RSA签名流程就这些。记住四个步骤:生成密钥对、计算HASH、私钥签名、公钥验签。每一步都有细节,但只要你把两端对齐了,联调就不会出大问题。