数字签名基础:RSA与ECDSA签名验签流程

数字签名这东西,说白了就是给固件盖个「防伪章」。你想想看,设备启动时怎么知道拿到的固件是官方发布的,而不是被篡改过的?靠的就是签名和验签这套机制。

我在嵌入式安全这行干了快十年,见过太多因为签名没做好导致设备被攻破的案例。有一次客户的产品在OTA升级时被中间人劫持,整批设备都被植入了后门——嗯,从那以后我对签名流程的要求就变得特别苛刻。

数字签名解决了什么问题?

先理清一个概念:签名 ≠ 加密。签名要保证的是三点:

  • 完整性:固件内容没有被改过
  • 真实性:确实是你信任的发布方签的
  • 不可否认性:发布方不能事后说「这不是我签的」

我习惯把签名比作「手写签名+防伪水印」的结合体。别人可以复印你的签名,但复印出来的签名在验签时就会露馅。

RSA签名:经典但吃资源

RSA签名基于大整数分解难题。流程其实不复杂:

签名过程(私钥持有者做):

  1. 对固件做哈希运算,得到摘要 H
  2. 用私钥对 H 做 RSA 加密(实际是模幂运算),得到签名 S
  3. 把固件 + 签名打包发布

验签过程(设备端做):

  1. 收到固件后,同样做哈希得到 H'
  2. 用公钥对签名 S 做 RSA 解密,得到 H
  3. 比较 H 和 H',一致则通过

这里有个坑:RSA签名不是直接对固件做加密,而是对哈希值做操作。为什么?因为RSA模幂运算太慢了,对几百KB的固件直接签名,设备端根本扛不住。

我曾经踩过的坑: 有个项目用了1024位的RSA密钥,验签一次要300多毫秒。后来换到2048位,时间直接翻倍到700毫秒。对于启动时间要求严格的产品,这个延迟是致命的。所以选RSA密钥长度时,一定要在安全性和性能之间找平衡。

ECDSA签名:短密钥,高性能

ECDSA(椭圆曲线数字签名算法)是RSA的现代替代方案。它的核心优势是:同样安全强度下,密钥长度短得多。

安全等级 RSA密钥长度 ECDSA密钥长度 签名大小
80位 1024位 160位 320位
112位 2048位 224位 448位
128位 3072位 256位 512位
192位 7680位 384位 768位

你看这个表就明白了:要达到128位安全强度,RSA需要3072位密钥,而ECDSA只要256位。在嵌入式设备上,这差距意味着存储空间和计算时间的巨大差异。

ECDSA的签名流程和RSA不太一样:

ECDSA签名过程:

  1. 对固件做哈希,得到摘要 H
  2. 生成一个随机数 k,计算椭圆曲线上的点 (x, y) = k * G
  3. 计算 r = x mod n(n是曲线的阶)
  4. 计算 s = k⁻¹ * (H + r * d) mod n(d是私钥)
  5. 签名就是 (r, s) 对

ECDSA验签过程:

  1. 对固件做哈希,得到 H'
  2. 计算 w = s⁻¹ mod n
  3. 计算 u1 = H' * w mod n, u2 = r * w mod n
  4. 计算点 P = u1 * G + u2 * Q(Q是公钥)
  5. 如果 P.x mod n == r,验签通过

说实话,ECDSA的数学比RSA复杂不少。但作为工程师,你不需要完全理解椭圆曲线群论,只要记住:ECDSA的签名是(r, s)对,验签时用公钥恢复出r',和签名里的r比较。

我个人习惯: 在资源受限的MCU上,优先选ECDSA P-256曲线。它比RSA-2048快2-3倍,签名大小只有64字节,而且安全性完全够用。不过要注意,ECDSA对随机数质量要求很高——如果随机数生成器有缺陷,私钥可能被泄露。

两种算法的对比与选型

维度 RSA ECDSA
签名速度 慢(私钥操作) 快(私钥操作)
验签速度 快(公钥操作) 中等
密钥长度 长(2048-4096位) 短(256-384位)
签名大小 与密钥同长 密钥长度的2倍
硬件加速 广泛支持 现代芯片支持
抗量子攻击 弱 弱(需后量子算法)

选型时我一般这么判断:如果设备有硬件RSA加速器,而且启动时间要求不苛刻,RSA-2048是稳妥选择。如果追求性能或存储空间紧张,ECDSA P-256是更好的选择。

签名流程的完整视图

下面这张图把整个签名验签流程串起来了,我建议你仔细看看——尤其是哈希运算的位置,很多人会搞错。

数字签名与验签完整流程 签名方(私钥持有者) 固件镜像 哈希运算(SHA-256) 私钥签名(RSA/ECDSA) 固件 + 签名打包 传输/存储 验签方(设备端) 收到固件 + 签名 哈希运算(SHA-256) 公钥验签(RSA/ECDSA) 比较哈希值 一致 → 启动固件 不一致 → 拒绝启动 ⚠ 关键点: 签名方和验签方使用相同的哈希算法。公钥必须安全存储(通常烧录在OTP或eFuse中)。

实际项目中的注意事项

光懂算法原理还不够,落地时有一堆细节要处理。我列几个常见的坑:

我曾经遇到的一个典型问题: 某款IoT设备使用RSA-2048签名,但验签时总是失败。排查了两天才发现——签名方用的哈希是SHA-256,但设备端固件里写死了SHA-1。哈希算法不一致,验签当然过不了。所以签名和验签的哈希算法必须严格对齐。

  • 公钥存储:公钥必须防篡改。我建议烧录到一次性可编程存储器(OTP)或eFuse中,写进去就锁死,防止被替换。
  • 签名格式:RSA签名通常用PKCS#1 v1.5或PSS填充。PSS更安全,但兼容性差一些。ECDSA签名要注意DER编码和原始(r,s)格式的区别。
  • 防重放攻击:签名本身不防重放。如果攻击者截获了一个旧版本的合法固件,设备还是会验签通过。解决方案是在固件头里加入版本号或时间戳,验签时一并检查。
  • 性能优化:如果设备启动时间要求严格,可以考虑「先验签头部,再流式验签主体」的策略,不用等整个固件加载完再验。

我的一个小技巧: 开发阶段用OpenSSL生成测试密钥对,生产环境一定要用HSM(硬件安全模块)或安全芯片生成和保管私钥。私钥一旦泄露,整个签名体系就废了。

代码示例:用OpenSSL做RSA签名验签

下面是一个简化的示例,展示实际开发中怎么操作:

// 签名方(PC端)
#include <openssl/rsa.h>
#include <openssl/sha.h>

RSA *rsa = RSA_generate_key(2048, RSA_F4, NULL, NULL);
unsigned char hash[SHA256_DIGEST_LENGTH];
unsigned char signature[256];
unsigned int sig_len;

SHA256(firmware, firmware_len, hash);
RSA_sign(NID_sha256, hash, SHA256_DIGEST_LENGTH, 
         signature, &sig_len, rsa);

// 验签方(设备端)
RSA *pub_rsa = RSA_new();
// 从OTP加载公钥...
unsigned char hash_dev[SHA256_DIGEST_LENGTH];
SHA256(received_firmware, fw_len, hash_dev);

int result = RSA_verify(NID_sha256, hash_dev, SHA256_DIGEST_LENGTH,
                        received_sig, sig_len, pub_rsa);
if (result == 1) {
    // 验签通过,启动固件
} else {
    // 验签失败,拒绝启动
}

ECDSA的代码结构类似,只是函数名换成ECDSA_sign和ECDSA_verify。嗯,这里要注意:ECDSA的签名长度不是固定的,取决于曲线参数,所以签名缓冲区要提前算好最大长度。