数字签名基础:非对称加密原理、哈希函数的作用、签名生成与验证流程
各位同学,欢迎来到第四章。前面我们聊了信任根和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 哈希函数:给数据打「指纹」
非对称加密有个问题——它算得慢。你想想看,一个固件镜像可能几兆甚至几十兆,用私钥逐字节签名?那得等到天荒地老。
所以我们需要哈希函数。哈希函数能把任意长度的数据,压缩成一个固定长度的「摘要」(也叫哈希值)。这个摘要就像数据的指纹——哪怕你改了一个比特,指纹都会变得面目全非。
好的哈希函数有三个特性:
- 单向性:从哈希值推不出原始数据。说白了就是「只进不出」。
- 抗碰撞性:找不到两个不同的数据有相同的哈希值。理论上存在,但计算上不可行。
- 雪崩效应:输入改一点点,输出天翻地覆。
嵌入式里常用的哈希算法是SHA-256。输出32字节,不长不短,刚好够用。SHA-1已经被攻破了,别再用。SHA-3虽然更新,但硬件加速还没普及。
注意:哈希函数不是加密。你不能「解密」哈希值。哈希是单向的,加密是双向的。这两个概念经常被新人搞混,我面试时至少有一半的人答不对。
4.3 签名生成与验证:完整流程拆解
好了,现在把非对称加密和哈希函数拼起来,就是完整的数字签名流程。我分两步讲。
4.3.1 签名生成(在安全环境下做)
这一步通常由开发者的构建服务器或HSM(硬件安全模块)完成。流程如下:
- 对原始数据(比如固件镜像)计算哈希值,得到摘要。
- 用私钥对摘要进行签名运算,生成签名数据。
- 把原始数据和签名打包在一起,分发给设备。
你可能会问:为什么不直接签名原始数据?因为慢。哈希把数据压缩成固定长度,私钥只需要处理这几十个字节,效率高得多。
// 伪代码:签名生成
data = read_firmware("bootloader.bin")
digest = SHA256(data) // 第一步:计算哈希
signature = RSA_SIGN(digest, private_key) // 第二步:签名
package = data + signature // 第三步:打包
4.3.2 签名验证(在Boot ROM里做)
这一步由设备的Boot ROM在启动时执行。流程是对称的:
- 从包中取出原始数据,计算哈希值,得到摘要A。
- 从包中取出签名,用公钥验签,得到摘要B。
- 比较摘要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)。
4.5 常见问题与避坑
讲完了原理,我分享几个实际项目中踩过的坑。
坑一:哈希算法不匹配。 我曾经接手过一个项目,签名方用的SHA-256,Boot ROM里验签时用的SHA-1。结果可想而知——永远验不过。排查了两天才发现。所以签名和验签用的哈希算法必须一致,这个要在设计文档里写死。
坑二:公钥存储位置不对。 有人把公钥放在Flash里,觉得「反正签名验证会检查完整性」。但你想过没有——如果攻击者同时篡改了固件和公钥呢?公钥必须放在Boot ROM里,或者由信任根派生。这是底线。
坑三:签名长度检查。 验签时一定要检查签名数据的长度是否合法。我见过一个案例,攻击者传了个空签名,结果验签函数直接返回成功——因为代码没做长度校验。嗯,从那以后我写验签代码时,第一件事就是检查签名长度。
4.6 小结
这一章我们拆解了数字签名的三个核心要素:
- 非对称加密:私钥签名,公钥验签。算法选型要兼顾安全和性能。
- 哈希函数:把任意数据压缩成固定长度的摘要,提高签名效率。
- 签名流程:先哈希后签名,先验签后比较。流程对称,但角色不同。