2、密码学基础(一):哈希函数与数字签名
各位同学,今天我们来啃一块硬骨头——密码学基础。别紧张,我不会把数学公式甩你脸上。咱们从工程实践的角度,把哈希函数和数字签名这两个东西彻底搞明白。
我做了十几年嵌入式安全,见过太多因为密码学基础不牢导致的翻车事故。有一次,某款IoT设备因为用了MD5做固件校验,结果被攻击者伪造了升级包,整批设备都得召回。嗯,从那以后,我对哈希算法的选择就特别较真。
2.1 哈希函数:数据的「指纹」
哈希函数,说白了就是一个单向映射。你给它任意长度的数据,它吐出一个固定长度的摘要。这个摘要就像数据的指纹——理论上,找不到两个不同的输入能产生相同的输出。
我个人习惯把哈希函数比作「碎纸机+照相机」的组合:你把文件塞进去,它粉碎、压缩,最后给你一张独一无二的照片。但你不可能从照片还原出原文件。
2.1.1 SHA-256 的核心特性
SHA-256 是 SHA-2 家族的一员,输出 256 位(32 字节)的摘要。为什么选它?三个字:够安全。
- 单向性(抗原像性):给定 y = H(x),你几乎不可能反推出 x。我在项目中测试过,即使用 GPU 暴力破解,算到地老天荒也未必能还原一个 128 位的输入。
- 抗碰撞性:找不到两个不同的 x₁ 和 x₂ 使得 H(x₁) = H(x₂)。SHA-256 的碰撞概率低到可以忽略——你被陨石砸中的概率都比这个高。
- 雪崩效应:输入改一个比特,输出大约一半的比特会翻转。这个特性对签名安全至关重要。
- 固定输出长度:不管输入是 1 字节还是 1GB,输出永远是 32 字节。这对嵌入式设备的内存管理非常友好。
重要提醒:SHA-256 不是加密算法,是摘要算法。它没有密钥,也不可逆。很多新手把哈希叫「加密」,这是概念错误。
2.1.2 SHA-256 的工作流程(简化版)
我不打算把 64 轮压缩函数的细节全讲一遍,但核心流程你得知道:
- 填充:在消息末尾补一个 '1' 位,然后补 '0' 位,直到长度模 512 等于 448。最后附加 64 位的原始长度信息。
- 分块:将填充后的消息分成 512 位(64 字节)的块。
- 初始化:设置 8 个 32 位的初始哈希值(H₀~H₇),这些是固定的常数。
- 压缩:对每个块执行 64 轮迭代运算,更新哈希值。
- 输出:将最终的 H₀~H₇ 拼接成 256 位的摘要。
你想想看,这个过程保证了即使输入只差一个比特,输出也天差地别。我在调试一个固件签名工具时,就靠这个特性定位了一个字节的校验错误——SHA-256 摘要对不上,说明固件被篡改过。
工程建议:在嵌入式设备上,如果 CPU 没有硬件 SHA 加速器,可以考虑用 mbedTLS 或 WolfSSL 的软件实现。它们对 RAM 和 Flash 的占用都经过优化,Cortex-M4 上跑一次 SHA-256 大约耗时 0.5~2ms(取决于输入长度)。
2.2 数字签名:谁签的?改没改?
哈希函数解决了「数据有没有被改」的问题,但没解决「数据是谁发的」的问题。数字签名就是干这个的。
数字签名的核心思想很简单:用私钥签名,用公钥验签。私钥只有你一个人有,所以签名不可抵赖。公钥公开,谁都可以验证。
2.2.1 RSA 签名:老牌劲旅
RSA 签名基于大整数分解难题。流程是这样的:
- 密钥生成:选两个大素数 p 和 q,计算 n = p × q。然后选一个公钥指数 e(常用 65537),计算私钥 d。
- 签名:对消息的哈希值 H(m) 做私钥运算:s = H(m)^d mod n。
- 验签:用公钥 (n, e) 计算 s^e mod n,看结果是否等于 H(m)。
我在一个车联网项目里用过 RSA-2048 做固件签名。说实话,RSA 的优点是算法成熟、实现简单,但缺点也很明显——签名太长(2048 位 = 256 字节),而且验签速度慢。
避坑指南:我曾经见过一个团队直接用教科书式的 RSA 签名,没有加 PSS 填充。结果被选择密文攻击打穿了。记住,RSA 签名一定要用 PKCS#1 v2.2 的 PSS 模式,或者至少用 v1.5 的 EMSA-PKCS1-v1_5 编码。
2.2.2 ECDSA 签名:后起之秀
ECDSA 基于椭圆曲线离散对数难题。同样的安全强度下,ECDSA 的密钥和签名都比 RSA 短得多。
举个例子:RSA-3072 的安全等级 ≈ ECDSA-256。但 RSA 签名是 384 字节,ECDSA 签名只有 64 字节。在嵌入式设备上,这差距就是能不能塞进一个 Flash 扇区的问题。
ECDSA 的签名流程:
- 生成一个随机数 k,计算椭圆曲线点 R = k × G(G 是基点)。
- 计算 r = R.x mod n(n 是曲线的阶)。
- 计算 s = k⁻¹ × (H(m) + d × r) mod n,其中 d 是私钥。
- 签名就是 (r, s) 对。
验签时,验签者用公钥 Q 计算 u₁ × G + u₂ × Q,看结果点的 x 坐标是否等于 r。
嗯,这里要注意:ECDSA 的随机数 k 必须每次都不一样,而且必须是真随机。我曾经在审计一个开源项目时发现,他们用了一个固定的 k 值——结果私钥被直接算出来了。这就是著名的「索尼 PS3 签名密钥泄露事件」的翻版。
2.2.3 RSA vs ECDSA:怎么选?
| 对比维度 | RSA-2048 | ECDSA-256 (secp256r1) |
|---|---|---|
| 安全等级 | 112 位 | 128 位 |
| 公钥长度 | 256 字节 | 64 字节(未压缩) |
| 签名长度 | 256 字节 | 64 字节 |
| 签名速度 | 慢(私钥操作) | 快 |
| 验签速度 | 快(公钥操作) | 较快 |
| 硬件加速 | 广泛支持 | 现代 MCU 普遍支持 |
| 抗量子攻击 | 弱 | 弱(两者都不抗量子) |
我个人建议:如果是新项目,优先选 ECDSA。除非你的硬件只有 RSA 加速器,或者需要兼容遗留系统。别为了「熟悉」而选 RSA,ECDSA 在嵌入式场景下优势太明显了。
2.3 从哈希到签名:构建可信链条
现在我们把两个概念串起来。一个典型的固件签名流程是这样的:
- 计算固件镜像的 SHA-256 摘要。
- 用私钥对摘要进行签名(RSA 或 ECDSA)。
- 将签名附加到固件包中,一起发布。
- 设备端收到固件后,先计算本地 SHA-256 摘要。
- 用公钥验签,比对摘要是否一致。
你想想看,这个流程同时解决了两个问题:
- 哈希保证固件没被篡改(完整性)。
- 签名保证固件来自合法发布者(真实性)。
这就是「可信闭环」的起点。没有这个基础,后面讲的远程证明、安全启动都是空中楼阁。
核心结论:SHA-256 做「体检」,数字签名做「身份认证」。两者缺一不可。
2.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以把它当作一个思维导图来理解:
这张图把本章的知识点都串起来了。左边是哈希函数,右边是数字签名,底部是两者的结合点。你可以把它截图保存,后面几章还会反复用到这些概念。
学习建议:别急着背算法细节。先理解「为什么需要哈希」和「为什么需要签名」这两个问题。原理通了,具体实现只是查文档的事。