4. 密码学基础(下):数字签名、消息认证码与密钥派生函数

好,我们接着往下聊。上一章我们把哈希、对称加密和非对称加密的底子打好了。这一章,咱们要解决几个更实际的问题:怎么证明消息是你发的?怎么保证消息没被篡改?怎么从一堆随机数里安全地派生出密钥?

这三个问题,对应着三个核心工具:数字签名、消息认证码(MAC)、密钥派生函数(KDF)。在MCU-Boot里,它们一个都不能少。

4.1 数字签名:你是谁?你说了算不算?

数字签名,说白了就是现实世界签名的电子版。但它的安全性要高得多——你没法伪造一个RSA签名,就像你没法凭空变出私钥一样。

4.1.1 核心流程:签名与验签

签名过程很简单:

  1. 哈希:对消息做一次哈希,得到摘要。
  2. 加密:用私钥对摘要加密,得到签名。
  3. 打包:把签名和原始消息一起发出去。

验签过程反过来:

  1. 拆包:收到消息和签名。
  2. 哈希:对收到的消息再做一次哈希。
  3. 解密:用公钥解密签名,得到原始摘要。
  4. 比对:两个摘要一致,验签通过。

关键点:私钥签名,公钥验签。私钥只有你自己有,所以签名不可抵赖。公钥谁都可以有,所以任何人都能验证。

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通常用来做固件完整性校验。比如:

  1. BootROM里预置一个密钥。
  2. 固件烧录时,计算整个固件的HMAC值,附加在固件尾部。
  3. 启动时,BootROM重新计算HMAC,比对是否一致。

小技巧:HMAC的密钥不要硬编码在源码里。我习惯把它放在OTP(一次性可编程)区域,或者从芯片唯一ID派生出来。这样即使固件被反编译,攻击者也拿不到密钥。

4.3 密钥派生函数(KDF):从种子到密钥

最后一个话题是KDF。你可能会有这样的需求:从一个主密钥(比如用户输入的密码)派生出多个子密钥,分别用于加密、签名、认证等不同用途。

直接拿主密钥去干所有事,是不安全的。万一某个子密钥泄露了,主密钥也跟着完蛋。KDF就是用来解决这个问题的。

4.3.1 KDF的核心流程

一个标准的KDF流程包含三个步骤:

  1. 输入:主密钥、盐值(Salt)、上下文信息(Info)、目标长度。
  2. 迭代:通过多次哈希或HMAC计算,增加攻击者的破解成本。
  3. 输出:派生出的子密钥。

最常用的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 本章知识体系总览

为了让你更直观地理解这三个工具的关系,我画了一张图:

密码学基础(下)知识体系 密码学基础(下)核心工具 数字签名 消息认证码 密钥派生函数 ECDSA RSA-PSS HMAC-SHA256 HKDF MCU-Boot 典型应用场景 数字签名 → 固件来源认证 | HMAC → 固件完整性校验 | KDF → 密钥分层管理 三者协同:签名保证身份,MAC保证完整,KDF保证密钥安全 缺一不可,共同构成安全启动的密码学基石

4.5 小结

这一章我们聊了三个工具:

  • 数字签名:用私钥签名,公钥验签。ECDSA快但依赖随机数,RSA-PSS慢但兼容性好。
  • HMAC:共享密钥下的消息完整性校验。比签名快,但需要提前协商密钥。
  • KDF:从主密钥安全派生子密钥。HKDF是首选,盐值必须随机。

在MCU-Boot里,这三个工具通常配合使用:签名用来验证固件来源,HMAC用来校验固件完整性,KDF用来管理各种密钥。你想想看,少了任何一个,攻击者都能找到突破口。

个人建议:刚开始做安全启动时,别想着自己实现这些算法。用现成的库(比如mbedTLS、OpenSSL)就好。我见过太多人自己写HMAC结果写错了填充逻辑——嗯,那还不如不写。