MD5 是一种 128 位密码学哈希函数,于 1992 年标准化。它速度快、历史使用广泛,但实际碰撞攻击已经证明其不适合需要碰撞抗性的安全决策。关键不是 MD5 字符串“看起来是否随机”,而是哈希原语是否匹配威胁模型,以及摘要是否通过可信独立渠道获得认证。
核心要点
- MD5 将任意字节映射为 16 字节摘要,通常显示为 32 个十六进制字符;编码不是安全属性。
- MD5 的碰撞抗性已经失效,但这不等于碰撞会立即泄露输入、反推出摘要,或让攻击者凭任意密码登录。
- 不要将 MD5 用于密码存储、数字签名、证书、安全令牌、MAC 或对抗攻击者的文件真实性验证。
- SHA-256 是通用哈希,不是密码 KDF。密码需要 Argon2id、scrypt、bcrypt 或 PBKDF2,并配合独立盐值和调优成本。
- 裸校验和只有在参考摘要通过独立可信渠道获得时,才能发现部分意外损坏;它不能认证攻击者可替换的下载文件。
- 共享密钥完整性使用 HMAC,公开验证使用数字签名,软件分发使用经过认证的包和签名体系。
哈希能做什么
密码学哈希是确定性函数:
digest = H(message)
它输出固定大小的结果,接受任意长度输入,并希望让多类攻击变得困难。这些属性必须区分:
| 属性 | 问题 | MD5 状态 |
|---|---|---|
| 原像抗性 | 能否为指定摘要找到消息? | 未知有实用通用破解,但 MD5 已弃用 |
| 第二原像抗性 | 已知一条消息,能否找到另一条同摘要消息? | 不能作为安全设计前提 |
| 碰撞抗性 | 能否找到任意两条同摘要的不同消息? | 已被实用的选择前缀碰撞研究破坏 |
| 长度扩展抗性 | 没有密钥能否扩展摘要计算? | 裸 MD5 具有 Merkle–Damgård 长度扩展行为 |
碰撞抗性不等于保密、加密、认证或授权。摘要本身不会自动泄露输入,但输入空间很小或可预测时,仍然可以被穷举或字典搜索。
MD5 的高层工作原理
MD5 对消息填充后按 512 位分块处理,通过模加、布尔函数和循环移位,在四轮中更新四个 32 位状态字,最终输出 128 位状态。这解释了它的固定长度和速度,但不能使该结构适合新的安全设计。
不要根据文字说明在生产环境重新实现 MD5。应使用经过审查的密码学库,并把遗留 MD5 支持限制在有迁移计划的互操作边界。
碰撞攻击意味着什么
碰撞是两段不同字节串 m1 和 m2 满足:
MD5(m1) = MD5(m2)
研究人员已经展示了实用的碰撞和选择前缀碰撞技术。当系统对攻击者可影响的内容计算 MD5,并把相等当作身份、批准或签名语义证明时,风险尤其明显。
这不等于“找到一个与用户 MD5 相同的密码”。密码存储主要受到快速穷举、重复密码、无盐数据库和撞库攻击影响;碰撞攻击和密码破解的目标不同,控制措施也不同。
MD5 不能用于哪些场景
密码
不要用 MD5、SHA-1,甚至不要直接用快速 SHA-256 存储密码。快速哈希方便攻击者离线测试大量猜测。应使用 Argon2id、scrypt、bcrypt 或 PBKDF2-HMAC 等密码 KDF,并配置:
- 每个密码独立随机生成的盐;
- 根据生产硬件调优的工作因子;
- 可版本化的参数记录,以便未来重新哈希;
- 认证边界的恒定时间验证和速率限制。
SHA-256 可以作为 KDF 内部或其他协议用途的一部分,但单独使用不是密码存储替代方案。
签名与证书
不要在新的数字签名、证书、软件包或更新信任链中使用 MD5 作为摘要。签名是在签名方案和密钥策略下认证摘要;如果摘要允许碰撞,签名所代表的语义可能被破坏。
安全令牌与 MAC
不要把裸 MD5 摘要当作令牌、密码重置码或完整性标签。共享密钥场景应使用现代哈希构造的 HMAC,密钥独立生成。HMAC 不等于在消息前后拼接密钥再计算哈希。
对抗攻击者的文件真实性
下载旁边发布的 MD5 值,如果摘要通过可信渠道独立传递,可以发现意外损坏。但攻击者若能同时替换文件和 MD5,或利用碰撞,就不会被阻止。软件发布应使用签名清单、可信传输或以 SHA-256 等现代摘要为组成部分的完整校验协议。
受限的遗留用途
当碰撞抗性明确无关且存在独立安全控制时,MD5 可能仍处于受限兼容位置:
- 遗留协议中的意外传输错误检测;
- 非对抗缓存中定位逐字节相同对象;
- 迁移期间比较旧记录,最终身份仍用更强哈希或字节比较确认。
即使在这些场景,MD5 也不是唯一标识符。碰撞、截断、规范化差异和编码错误都可能造成错误等价。新内容寻址系统应使用现代哈希并定义规范化字节。
可复现的遗留校验
下面的示例用于互操作或意外损坏检测,不提供认证:
printf 'abc' | md5sum
# 900150983cd24fb0d6963f7d28e17f72 -
# 新的通用摘要应优先使用这一类算法。
printf 'abc' | sha256sum
from hashlib import md5, sha256
payload = b"abc"
legacy_digest = md5(payload, usedforsecurity=False).hexdigest()
modern_digest = sha256(payload).hexdigest()
print(legacy_digest, modern_digest)
部分运行时会在安全模式禁用 MD5,或要求显式的遗留标志。不要为了让新的安全功能运行而移除这种保护。
大文件应流式读取,而不是一次性载入内存:
from hashlib import sha256
def sha256_file(path: str, chunk_size: int = 1024 * 1024) -> str:
digest = sha256()
with open(path, "rb") as source:
while chunk := source.read(chunk_size):
digest.update(chunk)
return digest.hexdigest()
摘要必须与从认证发布渠道获得的值比较。摘要匹配本身不能证明来源。
迁移与威胁建模
盘点每一个 MD5 用途并分类:
- 密码验证器;
- 签名或证书摘要;
- 令牌/MAC 或重置码;
- 对抗攻击者的文件或软件包校验;
- 非安全校验和;
- 缓存或去重键。
根据边界替换安全用途:登录或重置时重新哈希密码,重新签发签名和证书,轮换令牌和密钥,并发布签名清单。遗留校验和应记录“只防意外损坏”的威胁模型;需要防篡改时增加更强的认证机制。
不要静默把 MD5 换成 SHA-256 后宣称向后兼容。应记录输入字节、规范化、编码、算法、摘要长度、来源修订和迁移状态。
哈希、HMAC、KDF 与签名
| 需求 | 合适原语 | 提供什么 |
|---|---|---|
| 通用摘要 | SHA-256/SHA-512 或当前批准的哈希 | 在其设计假设下提供摘要和碰撞抗性 |
| 密码存储 | Argon2id、scrypt、bcrypt 或 PBKDF2-HMAC | 高成本、带盐的密码验证 |
| 共享密钥完整性 | HMAC-SHA-256 或批准的 MAC/AEAD | 对持有密钥者的认证 |
| 公开验证 | Ed25519/ECDSA/RSA-PSS 签名 | 使用公钥和签名密钥策略进行验证 |
| 保密与完整性 | AES-GCM 或 ChaCha20-Poly1305 等 AEAD | 加密及认证密文 |
密码存储、授权或加密需求选择 SHA-256,仍然属于概念类别错误。
常见问题
碰撞能让攻击者反推出 MD5 吗?
不能。碰撞是两条不同输入拥有同一摘要;反推摘要是原像问题,恢复密码通常是离线猜测问题。MD5 的碰撞抗性已失效,且速度不适合密码存储,因此不应用于新的安全设计。
MD5 可以检查下载文件吗?
只有在参考摘要通过可信独立渠道传递,并且威胁模型只考虑意外损坏时才可以。软件、更新或敌对网络应使用签名元数据和现代摘要。
SHA-256 适合存储密码吗?
直接使用不适合。SHA-256 很快,攻击者可以测试大量猜测。应使用带盐且调优成本的 Argon2id 等密码 KDF。
MD5 可以作为唯一 ID 或缓存键吗?
它可以作为非对抗缓存的遗留提示,但不是保证唯一的标识符。需要身份时使用数据库 ID、符合用途的 UUID,或现代内容哈希加字节比较。
HMAC-MD5 是裸 MD5 的替代方案吗?
不要在新设计中引入它。应迁移到现代批准哈希的 HMAC,并遵循协议的密钥、截断、防重放和验证规则。
一手来源
- RFC 1321:MD5 Message-Digest Algorithm
- RFC 6151:MD5 安全注意事项更新
- RFC 2104:HMAC
- RFC 8018:基于密码的密码学规范
- NIST SP 800-63B:数字身份指南
- CWI:SHA-1 首个完整碰撞
总结
MD5 是一种碰撞抗性已失效的历史快速摘要,不是通用安全方案。只有在明确记录“无需防恶意篡改”的遗留边界中保留它。新系统应按需求选择原语:现代哈希用于摘要,密码 KDF 用于密码,HMAC 用于共享密钥完整性,签名用于公开验证,AEAD 用于带完整性的保密。