4. 完整性与防篡改核心概念

聊到消息完整性,很多同学第一反应就是哈希。但哈希只能防意外篡改,防不了中间人。你想想看,如果攻击者把消息和哈希值一起换了,接收方根本察觉不到。所以我们需要一种带密钥的完整性校验机制——HMAC 和数字签名,才是真正能对抗恶意攻击的手段。

4.1 HMAC:带密钥的哈希消息认证码

HMAC 的核心思想很简单:把密钥混进哈希计算里。只有持有相同密钥的双方才能验证消息是否被篡改过。我在项目中遇到过好几次,有人直接用 hash(message + key) 这种拼接方式,结果被长度扩展攻击打得满地找牙。HMAC 的设计就是为了堵死这类漏洞。

HMAC 的标准计算流程(RFC 2104):

  1. 如果密钥长度大于哈希块大小(如 SHA-256 是 64 字节),先对密钥做哈希压缩。
  2. 构造 ipad(0x36 重复填充)和 opad(0x5c 重复填充)。
  3. 计算 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 字节
签名速度 慢 中 快
验签速度 中 快 快
随机数依赖 无 强依赖 无
量子安全 ❌ ❌ ❌
数字签名算法选型决策树 需要数字签名? 兼容性优先 → RSA 性能优先 → 椭圆曲线 填充方案:PSS > v1.5 有可靠随机源 → ECDSA 无随机源依赖 → EdDSA 选型建议 • 新系统优先选 Ed25519:安全、快速、实现简单 • 需要兼容旧系统时选 RSA-PSS,密钥至少 2048 位 • 避免 ECDSA 除非你确信随机数生成器质量

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,他们会找你的随机数生成器漏洞、签名验证逻辑漏洞、或者干脆直接偷私钥。把基础打牢,别给攻击者留后门。