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完成。流程是这样的:

  1. Bootloader读取固件文件
  2. 从固件中提取签名数据
  3. 计算固件实际HASH值
  4. 用公钥解密签名,得到原始HASH值
  5. 对比两个HASH值
  6. 一致则启动,不一致则拒绝启动

我的建议:公钥一定要烧录到设备的OTP(一次性可编程)区域或安全存储区,防止被替换。我曾经见过一个案例,攻击者替换了设备中的公钥,然后用自己生成的私钥签名恶意固件,成功绕过了验签。

4.5 完整流程图

下面这张图把整个RSA签名流程串起来了,你可以对照着看:

RSA签名与验签完整流程 RSA签名与验签完整流程 签名方(开发环境) 固件文件 (firmware.bin) 计算SHA-256 HASH 私钥签名 (RSA) 私钥 签名数据 (firmware.sig) 验签方(设备端) 接收固件 + 签名 计算固件HASH 公钥解密签名 公钥 对比两个HASH值 一致 → 启动 不一致 → 拒绝 固件 + 签名传输 ⚠ 关键点: • 私钥必须安全存储,公钥可以公开但不可被篡改 • HASH算法(SHA-256)在签名和验签两端必须一致

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、私钥签名、公钥验签。每一步都有细节,但只要你把两端对齐了,联调就不会出大问题。