4. 消息完整性:HMAC原理、数字签名算法与签名伪造攻击面分析
聊到消息完整性,很多同学第一反应就是哈希。但哈希只能防意外篡改,防不了中间人。你想想看,如果攻击者把消息和哈希值一起换了,接收方根本察觉不到。所以我们需要一种带密钥的完整性校验机制——HMAC 和数字签名,才是真正能对抗恶意攻击的手段。
4.1 HMAC:带密钥的哈希消息认证码
HMAC 的核心思想很简单:把密钥混进哈希计算里。只有持有相同密钥的双方才能验证消息是否被篡改过。我在项目中遇到过好几次,有人直接用 hash(message + key) 这种拼接方式,结果被长度扩展攻击打得满地找牙。HMAC 的设计就是为了堵死这类漏洞。
HMAC 的标准计算流程(RFC 2104):
- 如果密钥长度大于哈希块大小(如 SHA-256 是 64 字节),先对密钥做哈希压缩。
- 构造 ipad(0x36 重复填充)和 opad(0x5c 重复填充)。
- 计算
H((K ⊕ opad) || H((K ⊕ ipad) || message))。
为什么这么设计?说白了就是通过两次哈希和异或操作,把密钥的影响扩散到整个计算过程中。即使攻击者知道 H(K || message) 的结果,也无法伪造出合法的 HMAC。
| 特性 | 普通哈希 | HMAC |
|---|---|---|
| 抗长度扩展 | ❌ 易受攻击 | ✅ 天然免疫 |
| 需要密钥 | ❌ 不需要 | ✅ 必须 |
| 抗碰撞要求 | 高 | 相对宽松 |
| 典型应用 | 文件校验 | API 认证、TLS |
我的习惯:在微服务内部通信中,我倾向于用 HMAC-SHA256 做请求签名。性能开销小,实现简单,而且不需要 PKI 基础设施。但注意——密钥分发是个大坑,千万别硬编码在代码里。
4.2 数字签名算法:RSA、ECDSA、EdDSA
HMAC 的问题是通信双方必须共享同一个密钥。在开放网络里,这几乎不可能。数字签名解决了这个问题:私钥签名,公钥验签。任何持有你公钥的人都能验证签名,但只有你能生成签名。
4.2.1 RSA 签名
RSA 签名基于大整数分解难题。流程是:先对消息做哈希,然后用私钥对哈希值做 RSA 加密(实际是模幂运算)。验签时用公钥解密,比对哈希值。
嗯,这里要注意——RSA 签名有填充方案。PKCS#1 v1.5 填充虽然古老,但至今仍广泛使用。PSS 填充更安全,我建议新系统直接上 PSS。
我曾经踩过的坑:RSA 签名长度固定,跟密钥长度一样。2048 位 RSA 签名就是 256 字节。这在某些带宽敏感场景下是个问题。另外,RSA 验签速度比 ECDSA 慢不少,高并发场景要提前压测。
4.2.2 ECDSA 签名
ECDSA 基于椭圆曲线离散对数难题。同样的安全强度下,密钥和签名都比 RSA 短得多。256 位 ECDSA 签名只有 64 字节,安全性却相当于 3072 位 RSA。
但 ECDSA 有个致命弱点——它需要高质量的随机数。我在项目中见过一次事故:某设备因为随机数生成器故障,导致两次签名使用了相同的随机数 k。攻击者直接恢复了私钥。那次事故之后,我要求所有 ECDSA 实现必须做 k 值确定性生成(RFC 6979)。
4.2.3 EdDSA 签名
EdDSA(特别是 Ed25519)是我个人最喜欢的签名算法。它基于扭曲爱德华曲线,设计上就避免了 ECDSA 的很多坑:
- 确定性签名:不需要随机数生成器,彻底杜绝 k 值重用问题。
- 抗侧信道:实现时天然抵抗时序攻击。
- 性能优秀:签名和验签都比 ECDSA 快 2-3 倍。
- 签名短:64 字节,公钥 32 字节。
| 对比维度 | RSA-2048 | ECDSA P-256 | Ed25519 |
|---|---|---|---|
| 公钥长度 | 256 字节 | 32 字节 | 32 字节 |
| 签名长度 | 256 字节 | 64 字节 | 64 字节 |
| 签名速度 | 慢 | 中 | 快 |
| 验签速度 | 中 | 快 | 快 |
| 随机数依赖 | 无 | 强依赖 | 无 |
| 量子安全 | ❌ | ❌ | ❌ |
4.3 签名伪造攻击面分析
签名算法本身是安全的,但实现和协议层面的漏洞才是真正的重灾区。我整理了几个最常见的攻击面:
4.3.1 哈希长度扩展攻击
这个攻击专门针对 H(secret || message) 这种构造。攻击者可以在不知道 secret 的情况下,基于已知的哈希值计算出 H(secret || message || padding || extra)。HMAC 的设计就是为了彻底杜绝这种攻击。
4.3.2 随机数重用攻击(针对 ECDSA)
前面提到了,ECDSA 的 k 值一旦重用,私钥直接暴露。这不是理论攻击——2010 年索尼 PS3 的 ECDSA 签名实现就因为这个被攻破。我建议所有 ECDSA 实现都采用 RFC 6979 的确定性 k 值生成。
4.3.3 签名 malleability(可塑性)攻击
ECDSA 签名 (r, s) 中,(r, s) 和 (r, n-s) 都是有效签名。攻击者可以修改签名而不影响验证。这在某些协议中可能导致双花或重放问题。解决方案是强制 s 为较小值(即 s ≤ n/2)。
4.3.4 实现侧信道攻击
签名算法中的条件分支、非恒定时间比较、缓存访问模式都可能泄露密钥信息。我记得有一次审计某嵌入式设备的 RSA 实现,发现它的模幂运算时间跟密钥比特位强相关——用简单的时序分析就能恢复私钥。
避坑指南:永远不要自己实现密码算法。用经过审计的库,比如 libsodium、OpenSSL、BoringSSL。自己实现的签名验证代码,十有八九有漏洞。
4.3.5 协议层面的签名绕过
有时候攻击者不需要破解签名算法,而是利用协议设计缺陷绕过签名验证:
- 签名未覆盖关键字段:攻击者可以修改未签名的字段而不影响签名有效性。
- 签名验证逻辑错误:比如只验证了签名存在性,没验证签名正确性。
- 重放攻击:没有 nonce 或时间戳,攻击者可以重复使用旧签名。
- 密钥混淆攻击:攻击者用自己的公钥替换合法公钥,然后用自己私钥签名。
我的经验:设计签名协议时,遵循「签名覆盖所有语义字段」原则。包括消息类型、时间戳、发送方、接收方、消息体。任何遗漏的字段都可能成为攻击入口。
4.4 小结
消息完整性不是简单的哈希就能搞定的。HMAC 解决了对称场景下的认证问题,数字签名解决了非对称场景下的不可否认性问题。选型时,我个人的优先级是:Ed25519 > RSA-PSS > ECDSA。但无论选哪种,实现质量和协议设计才是真正的防线。
记住:密码算法是数学上安全的,但系统是人写的。攻击者不会去破解 RSA-2048,他们会找你的随机数生成器漏洞、签名验证逻辑漏洞、或者干脆直接偷私钥。把基础打牢,别给攻击者留后门。