MD5、SHA-1、SHA-256、SHA-512 到底该选哪个

先给结论:完整性校验用 SHA-256,数字签名和证书用 SHA-256 或 SHA-384,密码一律用 Argon2id 或 bcrypt,MD5 和 SHA-1 从任何安全相关的场景里删掉。下面说为什么。

几个算法的现状

算法 输出位 HEX 长度 安全状态 还能用在哪
MD5 128 32 已可构造碰撞 仅限非安全用途(如缓存键)
SHA-1 160 40 已出现实际碰撞 基本无处可用,Git 内部仍在用
SHA-256 256 64 安全 通用首选
SHA-512 512 128 安全 需要更长输出,64 位平台上通常更快
SHA3-256 256 64 安全 结构不同于 SHA-2,用于算法多样性备份

MD5 的死亡时间线很明确:1996 年被发现设计缺陷,2004 年王小云团队给出可行的碰撞构造方法,2008 年研究者利用 MD5 碰撞伪造出一个能被浏览器信任的 CA 证书。到这一步,它就不能再出现在任何需要抗碰撞的场景里。

SHA-1 同理:2005 年理论攻击复杂度降到约 2^69,2017 年 Google 与 CWI 发布 SHAttered,用两个内容不同但 SHA-1 相同的 PDF 证明了实际碰撞可行;2020 年又出现了更实用的选择前缀碰撞。主流浏览器和 CA 早已停止签发 SHA-1 证书。

抗碰撞和抗原像不是一回事

判断一个哈希还能不能用,要看你需要哪个性质:

  • 抗原像(preimage):给定哈希值,找不到原文。密码存储依赖这个。
  • 抗碰撞(collision):找不到两个不同原文产生相同哈希。文件校验、证书签名依赖这个。

MD5 和 SHA-1 被攻破的是抗碰撞,抗原像目前仍然困难。但工程上不用赌:一旦抗碰撞没了,任何依赖它的构造都不可信,而绝大多数场景本来就需要抗碰撞。

校验文件该用哪个

下载镜像、安装包时对比官方发布的校验值,用 SHA-256 就足够:

# Linux
sha256sum ubuntu-24.04.iso

# macOS
shasum -a 256 ubuntu-24.04.iso
import hashlib

def sha256_file(path):
    h = hashlib.sha256()
    with open(path, 'rb') as f:
        for chunk in iter(lambda: f.read(1024 * 1024), b''):
            h.update(chunk)
    return h.hexdigest()

分块读取是必要的——一次性 read() 一个几 GB 的文件会打爆内存。

一个容易被忽略的前提:校验值必须来自独立于下载源的渠道。同一个 HTTPS 页面上同时提供安装包和它的 SHA-256,只能防传输损坏和镜像站出错,防不了这个页面本身被篡改。PGP 签名或独立的发布公告才是真正的信任链。

密码绝对不能用这些算法

MD5、SHA-1、SHA-256、SHA-512 都是快哈希,设计目标就是每秒算几百万次。用它们存密码等于给离线暴力破解开绿灯,加盐也只能防彩虹表,防不住 GPU 穷举。

密码必须用专门的慢哈希(带可调代价因子、抗并行的 KDF):

算法 特点 备注
Argon2id 抗 GPU/ASIC,可调内存与迭代 新系统首选
scrypt 内存硬 成熟,适合没有 Argon2 库时
bcrypt 支持广泛 输入长度上限 72 字节
PBKDF2-HMAC-SHA256 兼容性最好 只能调迭代次数,抗 GPU 较弱

别自己拼 HMAC

另一个高频错误是写 hash(secret + message)。SHA-256 属于 Merkle–Damgård 结构,存在长度扩展攻击:攻击者只要知道 hash(secret || message) 和 message 的长度,就能算出 hash(secret || message || 填充 || 附加数据),全程不需要知道 secret。

正确做法是用 HMAC:

import hmac, hashlib

hmac.new(b'secret-key', b'message', hashlib.sha256).hexdigest()
const crypto = require('node:crypto');
crypto.createHmac('sha256', 'secret-key').update('message').digest('hex');

顺手记一个可用的对照值,用来验证你的实现没写错:

import hashlib
hashlib.sha256(b'hello').hexdigest()
# '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'

一句话选型表

你要做的事 用什么
校验下载文件 SHA-256
API 请求签名 HMAC-SHA256
存用户密码 Argon2id,退而求其次 bcrypt
生成缓存键、去重指纹 SHA-256;MD5 仅在明确非安全用途时
数字签名、证书 SHA-256 / SHA-384
自定义构造需抗长度扩展 HMAC-SHA256,或 SHA3-256 / BLAKE3

输出多长才够:生日攻击

哈希的"位长"不等于"抗碰撞强度"。按生日悖论,找到任意一对碰撞只需要大约 2^(n/2) 次尝试,而不是 2^n:

输出位 抗原像强度 抗碰撞强度 现状
128(MD5) 约 2^128 约 2^64 碰撞已被实际构造
160(SHA-1) 约 2^160 约 2^80 碰撞已被实际构造
256(SHA-256) 约 2^256 约 2^128 安全
512(SHA-512) 约 2^512 约 2^256 安全

128 位输出的 MD5 抗碰撞只有 64 位量级,这正是它最先被打穿的原因。选算法时按"抗碰撞强度不低于 128 位"来挑,就自然落在 SHA-256 及以上。

顺带说一句:CRC32、Adler-32 这类校验和不是哈希。它们设计用来检测传输误码,构造碰撞是随手就能做到的事,既不能用于任何安全目的,也不适合做去重指纹——32 位输出在百万级数据上就会出现碰撞。

自己动手试试

相关工具