数字签名基础:非对称加密原理、哈希函数的作用、签名生成与验证流程

各位同学,欢迎来到第四章。前面我们聊了信任根和Boot ROM的硬件基础,但光有硬件还不够——你得让Boot ROM能验证下一级镜像是不是真的、有没有被篡改。这个验证的核心,就是数字签名。

数字签名这东西,说白了就是现实世界里「签名+封条」的电子版。你写封信,签上名,再拿火漆封上——别人拆开一看,签名对得上,封条没破,就知道信是你写的、内容没动过。数字签名干的也是这两件事:身份认证和完整性保护。

这一章我会把背后的原理拆开来讲。非对称加密、哈希函数、签名流程,一个一个过。嗯,都是后面讲代码签名、安全启动时绕不开的基础。

4.1 非对称加密:公钥和私钥的「夫妻档」

先说说非对称加密。对称加密大家可能熟悉——一把钥匙开一把锁,加密解密用同一个密钥。但问题来了:你俩怎么安全地交换这把钥匙?

非对称加密换了个思路。它给你一对密钥:私钥(自己藏好)和公钥(随便发)。私钥签名的东西,公钥能验证;公钥加密的东西,只有私钥能解开。就像一把锁配两把钥匙,但功能不一样。

我个人习惯把私钥比作「你的电子手印」——绝对不能给别人。公钥呢,就像你的电话号码,谁都可以知道。

核心要点:在数字签名场景里,我们只用私钥签名,公钥验签。私钥从不用于加密,公钥也从不用于解密签名。别搞混了。

常见的非对称算法有RSA、ECDSA、Ed25519。嵌入式里用得最多的是RSA-2048和ECDSA-P256。为什么?

  • RSA:成熟、广泛支持,但密钥长、运算慢。适合对性能要求不苛刻的场景。
  • ECDSA:同样安全强度下密钥短得多(256位 vs 2048位),签名也小。我最近几个项目都用的ECDSA,Boot ROM里省了不少空间。
  • Ed25519:更新、更快、更安全,但硬件支持还不算普及。
算法 密钥长度 签名大小 验签速度 硬件支持
RSA-2048 2048位 256字节 慢 非常广泛
ECDSA-P256 256位 64字节 快 广泛
Ed25519 256位 64字节 极快 有限

避坑指南:我曾经在一个项目里选了RSA-4096,觉得「越长越安全」。结果Boot ROM的验签时间飙到了300多毫秒,用户等得直骂娘。后来换成ECDSA-P256,验签只要十几毫秒。选算法时一定要考虑硬件加速和实时性要求。

4.2 哈希函数:给数据打「指纹」

非对称加密有个问题——它算得慢。你想想看,一个固件镜像可能几兆甚至几十兆,用私钥逐字节签名?那得等到天荒地老。

所以我们需要哈希函数。哈希函数能把任意长度的数据,压缩成一个固定长度的「摘要」(也叫哈希值)。这个摘要就像数据的指纹——哪怕你改了一个比特,指纹都会变得面目全非。

好的哈希函数有三个特性:

  1. 单向性:从哈希值推不出原始数据。说白了就是「只进不出」。
  2. 抗碰撞性:找不到两个不同的数据有相同的哈希值。理论上存在,但计算上不可行。
  3. 雪崩效应:输入改一点点,输出天翻地覆。

嵌入式里常用的哈希算法是SHA-256。输出32字节,不长不短,刚好够用。SHA-1已经被攻破了,别再用。SHA-3虽然更新,但硬件加速还没普及。

注意:哈希函数不是加密。你不能「解密」哈希值。哈希是单向的,加密是双向的。这两个概念经常被新人搞混,我面试时至少有一半的人答不对。

4.3 签名生成与验证:完整流程拆解

好了,现在把非对称加密和哈希函数拼起来,就是完整的数字签名流程。我分两步讲。

4.3.1 签名生成(在安全环境下做)

这一步通常由开发者的构建服务器或HSM(硬件安全模块)完成。流程如下:

  1. 对原始数据(比如固件镜像)计算哈希值,得到摘要。
  2. 用私钥对摘要进行签名运算,生成签名数据。
  3. 把原始数据和签名打包在一起,分发给设备。

你可能会问:为什么不直接签名原始数据?因为慢。哈希把数据压缩成固定长度,私钥只需要处理这几十个字节,效率高得多。

// 伪代码:签名生成
data = read_firmware("bootloader.bin")
digest = SHA256(data)          // 第一步:计算哈希
signature = RSA_SIGN(digest, private_key)  // 第二步:签名
package = data + signature     // 第三步:打包

4.3.2 签名验证(在Boot ROM里做)

这一步由设备的Boot ROM在启动时执行。流程是对称的:

  1. 从包中取出原始数据,计算哈希值,得到摘要A。
  2. 从包中取出签名,用公钥验签,得到摘要B。
  3. 比较摘要A和摘要B。一致则通过,不一致则拒绝启动。
// 伪代码:签名验证(在Boot ROM中执行)
package = read_from_flash()
data = extract_data(package)
signature = extract_signature(package)

digest_A = SHA256(data)                    // 第一步:计算哈希
digest_B = RSA_VERIFY(signature, public_key) // 第二步:验签

if (digest_A == digest_B) {
    boot_ok()   // 验证通过,启动
} else {
    boot_fail() // 验证失败,拒绝
}

关键点:公钥必须存储在Boot ROM里,或者由Boot ROM的信任根派生出来。如果公钥可以被篡改,那整个签名体系就形同虚设。这就是为什么我们之前花那么多篇幅讲硬件信任根——它保证了公钥的不可篡改性。

4.4 一张图看懂整个流程

下面这张SVG图把签名生成和验证的完整流程画出来了。左边是签名方(开发者),右边是验证方(设备Boot ROM)。

数字签名生成与验证流程 数字签名生成与验证流程 签名方(开发者) 原始数据(固件镜像) SHA-256 哈希运算 摘要(32字节) 私钥签名运算 打包:数据 + 签名 验证方(Boot ROM) 接收数据 + 签名 拆包:分离数据和签名 SHA-256 哈希运算 摘要A(本地计算) 公钥验签 → 摘要B 比较摘要A == 摘要B? 一致 → 启动 ✅ | 不一致 → 拒绝 ❌ 公钥存储在Boot ROM中 由硬件信任根保护

4.5 常见问题与避坑

讲完了原理,我分享几个实际项目中踩过的坑。

坑一:哈希算法不匹配。 我曾经接手过一个项目,签名方用的SHA-256,Boot ROM里验签时用的SHA-1。结果可想而知——永远验不过。排查了两天才发现。所以签名和验签用的哈希算法必须一致,这个要在设计文档里写死。

坑二:公钥存储位置不对。 有人把公钥放在Flash里,觉得「反正签名验证会检查完整性」。但你想过没有——如果攻击者同时篡改了固件和公钥呢?公钥必须放在Boot ROM里,或者由信任根派生。这是底线。

坑三:签名长度检查。 验签时一定要检查签名数据的长度是否合法。我见过一个案例,攻击者传了个空签名,结果验签函数直接返回成功——因为代码没做长度校验。嗯,从那以后我写验签代码时,第一件事就是检查签名长度。

4.6 小结

这一章我们拆解了数字签名的三个核心要素:

  • 非对称加密:私钥签名,公钥验签。算法选型要兼顾安全和性能。
  • 哈希函数:把任意数据压缩成固定长度的摘要,提高签名效率。
  • 签名流程:先哈希后签名,先验签后比较。流程对称,但角色不同。