4. 密码学基础(下):数字签名、消息认证码与密钥派生函数
好,我们接着往下聊。上一章我们把哈希、对称加密和非对称加密的底子打好了。这一章,咱们要解决几个更实际的问题:怎么证明消息是你发的?怎么保证消息没被篡改?怎么从一堆随机数里安全地派生出密钥?
这三个问题,对应着三个核心工具:数字签名、消息认证码(MAC)、密钥派生函数(KDF)。在MCU-Boot里,它们一个都不能少。
4.1 数字签名:你是谁?你说了算不算?
数字签名,说白了就是现实世界签名的电子版。但它的安全性要高得多——你没法伪造一个RSA签名,就像你没法凭空变出私钥一样。
4.1.1 核心流程:签名与验签
签名过程很简单:
- 哈希:对消息做一次哈希,得到摘要。
- 加密:用私钥对摘要加密,得到签名。
- 打包:把签名和原始消息一起发出去。
验签过程反过来:
- 拆包:收到消息和签名。
- 哈希:对收到的消息再做一次哈希。
- 解密:用公钥解密签名,得到原始摘要。
- 比对:两个摘要一致,验签通过。
关键点:私钥签名,公钥验签。私钥只有你自己有,所以签名不可抵赖。公钥谁都可以有,所以任何人都能验证。
4.1.2 ECDSA vs RSA-PSS:选哪个?
在MCU-Boot里,最常见的两种签名算法就是ECDSA和RSA-PSS。我个人的习惯是:资源受限选ECDSA,兼容性优先选RSA-PSS。
| 对比项 | ECDSA | RSA-PSS |
|---|---|---|
| 密钥长度 | 短(256位 ≈ 32字节) | 长(2048位 ≈ 256字节) |
| 签名速度 | 快 | 慢 |
| 验签速度 | 较快 | 快 |
| 安全性基础 | 椭圆曲线离散对数 | 大整数分解 |
| 典型应用 | IoT、嵌入式 | Web、PKI |
我在一个STM32H7的项目里用过ECDSA P-256。签名只需要几毫秒,验签更快。但要注意,ECDSA的随机数生成必须足够好——如果随机数重复,私钥就暴露了。嗯,这里有个坑,我后面会讲。
4.1.3 避坑指南:ECDSA的随机数陷阱
我曾经在一个产品里遇到过ECDSA签名偶尔失败的问题。排查了半天,发现是随机数生成器在启动初期熵不够,导致两次签名用了相同的随机数k。攻击者只要拿到两个签名,就能算出私钥。
解决方案:使用硬件随机数生成器(TRNG),或者在签名前做一次熵池刷新。别偷懒用伪随机数。
4.2 消息认证码(HMAC):你俩的小秘密
数字签名能证明身份,但有个问题——它太慢了。如果你只是想确认消息在传输过程中没被改过,而且你和对方共享一个密钥,那用消息认证码(MAC)更合适。
HMAC(Hash-based Message Authentication Code)是最常见的MAC实现。它把密钥和消息混在一起做哈希,输出一个固定长度的认证码。
4.2.1 HMAC的工作原理
HMAC的计算公式看起来有点绕,但核心思想很简单:
HMAC(K, m) = H( (K' ⊕ opad) || H( (K' ⊕ ipad) || m ) )
其中:
- K':密钥经过填充后的版本
- ipad:内填充(0x36重复)
- opad:外填充(0x5C重复)
- H:哈希函数(如SHA-256)
你想想看,为什么要做两次哈希?因为这样可以防止长度扩展攻击。SHA-256本身有这个漏洞,但HMAC通过内外两次哈希把它堵死了。
4.2.2 HMAC在MCU-Boot里的典型用法
在安全启动里,HMAC通常用来做固件完整性校验。比如:
- BootROM里预置一个密钥。
- 固件烧录时,计算整个固件的HMAC值,附加在固件尾部。
- 启动时,BootROM重新计算HMAC,比对是否一致。
小技巧:HMAC的密钥不要硬编码在源码里。我习惯把它放在OTP(一次性可编程)区域,或者从芯片唯一ID派生出来。这样即使固件被反编译,攻击者也拿不到密钥。
4.3 密钥派生函数(KDF):从种子到密钥
最后一个话题是KDF。你可能会有这样的需求:从一个主密钥(比如用户输入的密码)派生出多个子密钥,分别用于加密、签名、认证等不同用途。
直接拿主密钥去干所有事,是不安全的。万一某个子密钥泄露了,主密钥也跟着完蛋。KDF就是用来解决这个问题的。
4.3.1 KDF的核心流程
一个标准的KDF流程包含三个步骤:
- 输入:主密钥、盐值(Salt)、上下文信息(Info)、目标长度。
- 迭代:通过多次哈希或HMAC计算,增加攻击者的破解成本。
- 输出:派生出的子密钥。
最常用的KDF是HKDF(HMAC-based Key Derivation Function)。它分两步:
- 提取(Extract):用HMAC把输入熵浓缩成一个固定长度的伪随机密钥。
- 扩展(Expand):用HMAC把这个密钥扩展成任意长度的子密钥序列。
4.3.2 实战:用HKDF派生多个密钥
假设你有一个256位的主密钥,需要派生出三个128位的子密钥:一个用于固件加密,一个用于HMAC校验,一个用于会话密钥。
// 伪代码示例
master_key = [0x01, 0x02, ..., 0x20]; // 32字节
salt = [0xA1, 0xB2, ..., 0xC3]; // 16字节随机数
// 第一步:提取
prk = hkdf_extract(salt, master_key); // 输出32字节
// 第二步:扩展
key_enc = hkdf_expand(prk, "enc-key", 16); // 16字节
key_mac = hkdf_expand(prk, "mac-key", 16); // 16字节
key_session = hkdf_expand(prk, "session-key", 16); // 16字节
注意,这里的"enc-key"、"mac-key"就是上下文信息(Info)。它们保证了即使两个子密钥长度相同,实际值也不同。
重要:盐值(Salt)必须是随机且唯一的。如果盐值重复,派生出的密钥就可能相同。我在一个OTA升级项目里吃过这个亏——两次升级用了相同的盐值,导致加密密钥重复,攻击者可以解密固件。
4.4 本章知识体系总览
为了让你更直观地理解这三个工具的关系,我画了一张图:
4.5 小结
这一章我们聊了三个工具:
- 数字签名:用私钥签名,公钥验签。ECDSA快但依赖随机数,RSA-PSS慢但兼容性好。
- HMAC:共享密钥下的消息完整性校验。比签名快,但需要提前协商密钥。
- KDF:从主密钥安全派生子密钥。HKDF是首选,盐值必须随机。
在MCU-Boot里,这三个工具通常配合使用:签名用来验证固件来源,HMAC用来校验固件完整性,KDF用来管理各种密钥。你想想看,少了任何一个,攻击者都能找到突破口。
个人建议:刚开始做安全启动时,别想着自己实现这些算法。用现成的库(比如mbedTLS、OpenSSL)就好。我见过太多人自己写HMAC结果写错了填充逻辑——嗯,那还不如不写。