哈希函数(Hash Function)是密码学中最基础也最重要的原语之一。它将任意长度的输入数据映射为固定长度的输出值(称为哈希值或摘要),具备单向性、抗碰撞性和确定性等核心性质。现代密码学中,哈希函数无处不在:从HTTPS协议的证书验证到比特币的工作量证明,从用户口令的安全存储到文件的完整性校验,哈希函数构成了数字世界信任的基础设施。
通俗理解:哈希函数就像一台"数字碎纸机"。无论你放进去的是一张纸还是一本书,它都会将其粉碎成固定大小的纸屑(哈希值),并且这个过程不可逆——你无法从纸屑还原出原始文档。同时,两本不同的书几乎不可能产生完全相同的纸屑图案。
一个安全的密码学哈希函数 需要满足以下三个安全性要求:
给定哈希值 ,找到任意满足 的输入 在计算上不可行。
通俗理解:你可以从 计算出 ,但无法从 反推出 。
给定输入 ,找到另一个不同的输入 使得 在计算上不可行。
通俗理解:如果有人给了你一个文件及其哈希值,你不能伪造另一个不同内容但哈希值相同的文件。
找到任意两个不同的输入 使得 在计算上不可行。
通俗理解:你无法故意制造两份不同内容但哈希值相同的文件。
下表以 位输出的哈希函数为例,对比不同安全性要求下的攻击复杂度:
| 安全性质 | 攻击类型 | 暴力破解复杂度 | 128位 | 256位 | 512位 |
|---|---|---|---|---|---|
| 抗原像性 | 原像攻击 | ||||
| 抗第二原像性 | 第二原像攻击 | ||||
| 抗碰撞性 | 生日攻击 |
关键洞察:由于生日悖论,碰撞攻击的复杂度仅为 ,远低于原像攻击的 。这就是为什么MD5(128位输出)的碰撞攻击复杂度仅为 ,在1996年就被认为不再安全。这也是为什么现代应用要求至少256位输出的哈希函数。
生日悖论告诉我们:在23个人中,至少两人同一天生日的概率超过50%。同样的原理适用于哈希碰撞。
对于 位哈希函数,计算 个不同输入时发生至少一次碰撞的概率约为:
具体数值计算:对于SHA-256(,):
| 尝试次数 | 碰撞概率 |
|---|---|
| 约 (极小) | |
| 约 | |
| 约 39.3% | |
| 约 86.5% |
也就是说,如果有人计算 个SHA-256哈希值(这需要远超当前宇宙算力的计算资源),才有约39%的概率产生一次碰撞。
| 年代 | 算法 | 输出长度 | 安全状态 | 关键事件 |
|---|---|---|---|---|
| 1990 | MD4 | 128 bit | ❌ 已破解 | 1995年首次碰撞攻击 |
| 1992 | MD5 | 128 bit | ❌ 已破解 | 2004年王小云提出碰撞攻击 |
| 1995 | SHA-0 | 160 bit | ❌ 已破解 | 发布不久即发现缺陷 |
| 1995 | SHA-1 | 160 bit | ⚠️ 已废除 | 2017年Google展示SHAttered碰撞攻击 |
| 2002 | SHA-256/512 | 256/512 bit | ✅ 仍然安全 | NIST标准,使用最广泛 |
| 2008 | SHA-3 (Keccak) | 可变 | ✅ 仍然安全 | NIST竞赛胜出,海绵结构 |
| 2012 | BLAKE2 | 可变 | ✅ 仍然安全 | 比SHA-3更快 |
| 2020 | BLAKE3 | 可变 | ✅ 仍然安全 | 单周期哈希,性能极优 |
SHA-256是SHA-2家族中最常用的一员,输出256位(32字节)的哈希值。其核心结构是Merkle-Damgård构造:
原始消息 M (任意长度)
│
▼
┌─────────────┐
│ 填充 │ ← 添加 '1' 位 → 添加 '0' 位 → 添加64位长度
└──────┬──────┘
│
▼
512-bit块 1 ──► ┌──────────────┐
│ 压缩函数 f │ ←── 初始向量 IV (8个32bit字)
└──────┬───────┘
│
512-bit块 2 ──► ┌──────────────┐
│ 压缩函数 f │
└──────┬───────┘
│
... ...
│
512-bit块 N ──► ┌──────────────┐
│ 压缩函数 f │
└──────┬───────┘
│
▼
256位哈希值 (8 × 32bit)
SHA-256使用前8个素数的平方根的小数部分取前32位作为初始值:
| 寄存器 | 素数 | 平方根小数前32位 | 十六进制值 |
|---|---|---|---|
| 2 | 0x6a09e667 |
||
| 3 | 0xbb67ae85 |
||
| 5 | 0x3c6ef372 |
||
| 7 | 0xa54ff53a |
||
| 11 | 0x510e527f |
||
| 13 | 0x9b05688c |
||
| 17 | 0x1f83d9ab |
||
| 19 | 0x5be0cd19 |
SHA-256的每一轮压缩函数对当前8个寄存器值进行操作。每一轮处理消息调度表中的第 个字:
给定当前状态 ,一轮操作如下:
其中:
为了直观展示,我们计算空字符串 "" 的SHA-256值。严格按FIPS 180-4标准计算:
输入:""(空字符串,0字节)
步骤1:填充
原始消息长度 bit。填充要求:先加一个 '1' 位,然后加 个 '0' 位,最后加64位长度表示,使总长度是512的倍数。
解得 。
所以填充后的消息为:
1 // 1 bit '1'
后面加447个 '0'
后面加64位长度表示:0x00000000 00000000 (长度 = 0 bit)
填充结果:一个完整的512位数据块。
步骤2:初始化
步骤3:迭代64轮
经过64轮压缩计算后,最终输出:
这是空字符串的标准SHA-256哈希值,你可以用任何SHA-256工具验证。
| 输入 | SHA-256哈希值(十六进制) |
|---|---|
""(空字符串) |
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
"a" |
ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb |
"abc" |
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad |
"hello" |
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 |
"The quick brown fox jumps over the lazy dog" |
d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592 |
雪崩效应检验:将最后一个输入改为
"The quick brown fox jumps over the lazy dog."(仅加了句号),哈希值变为ef537f25c895bfa782526529a9b63d97aa631564d5d789c2b765448c8635fb6c。输入仅变化1个字符,输出中的一半以上比特位都发生了翻转。
SHA-3采用与SHA-2完全不同的海绵构造(Sponge Construction),而非Merkle-Damgård构造。这一根本区别使SHA-3天然免疫于长度扩展攻击。
吸收阶段 挤压阶段
┌─────────────────┬──────────────────┐
│ │ │
▼ ▼ ▼
r ┌────┬────┬────┐ ┌────┬────┬────┐ ┌────┬────┐
───►│ f │ f │ f │►│ f │ f │ f │►│ f │ f │►...
├────┼────┼────┤ ├────┼────┼────┤ ├────┼────┤
c │ │ │ │ │ │ │ │ │ │ │
└────┴────┴────┘ └────┴────┴────┘ └────┴────┘
r = 速率 (rate) — 与输入/输出交互的部分
c = 容量 (capacity) — 内部状态,不直接输出,决定安全强度
f = Keccak-f 置换函数
| 参数 | SHA3-224 | SHA3-256 | SHA3-384 | SHA3-512 |
|---|---|---|---|---|
| 输出长度 | 224 bit | 256 bit | 384 bit | 512 bit |
| 安全强度 | 112 bit | 128 bit | 192 bit | 256 bit |
| 速率 | 1152 bit | 1088 bit | 832 bit | 576 bit |
| 容量 | 448 bit | 512 bit | 768 bit | 1024 bit |
安全强度由容量 决定:抗碰撞性为 ,抗原像性为 。
Keccak-f 是SHA-3的核心,对1600位状态进行多轮置换。状态可以看作一个 的三维数组:
状态表示:A[x][y][z]
x, y ∈ {0, 1, 2, 3, 4}
z ∈ {0, 1, ..., 63} (64位字)
总大小:5 × 5 × 64 = 1600 bit
每轮包括5个步骤:
SHA-3的轮数与状态宽度对应:1600位状态经过 轮(其中 ,)。
| 特性 | SHA-256 | SHA3-256 |
|---|---|---|
| 构造 | Merkle-Damgård | 海绵构造 |
| 输出长度 | 256 bit | 256 bit |
| 安全强度(碰撞) | 128 bit | 128 bit |
| 安全强度(原像) | 256 bit | 256 bit |
| 长度扩展攻击 | ❌ 易受攻击 | ✅ 免疫 |
| 硬件实现效率 | 中等 | 优秀 |
| 软件实现效率 | 优秀 | 中等 |
| 量子安全强度 | 128 bit(Grover算法) | 128 bit |
BLAKE2是SHA-3竞赛决赛选手BLAKE的优化版本。它没有入选NIST标准,但由于其出色的性能,被广泛采用。
与SHA-2/3的性能对比(Intel Skylake架构,单线程,消息长度4096字节):
| 哈希函数 | 吞吐量 | 每字节周期数 | 优势领域 |
|---|---|---|---|
| BLAKE2b | 1.22 GB/s | 0.82 cpb | 通用哈希 |
| BLAKE2s | 0.88 GB/s | 1.14 cpb | 32位平台 |
| SHA-256 | 0.39 GB/s | 2.55 cpb | 标准兼容性 |
| SHA3-256 | 0.28 GB/s | 3.57 cpb | 安全强度 |
数据来源:BLAKE2官方基准测试(Skylake i7-6700K, 4.0GHz)
BLAKE2b的吞吐量是SHA-256的3倍以上,是SHA3-256的4倍以上。
BLAKE3(2020年发布)是BLAKE系列的最新一代,采用了Merkle树结构,实现了:
BLAKE3多线程性能(AMD Ryzen 5950X, 16核):
| 核心数 | 吞吐量 | 加速比 |
|---|---|---|
| 1 | 1.3 GB/s | 1.0× |
| 2 | 2.5 GB/s | 1.9× |
| 4 | 4.8 GB/s | 3.7× |
| 8 | 9.1 GB/s | 7.0× |
| 16 | 16.1 GB/s | 12.4× |
实际应用案例:
b2sum 和 b3sum通用哈希函数(如SHA-256)设计为快速计算,这恰恰是口令哈希的大忌。攻击者可以用GPU每秒计算数十亿次SHA-256,轻松暴力破解用户口令。
口令哈希函数通过引入计算成本参数,显著降低攻击者的破解速率。
| 特性 | bcrypt | scrypt | Argon2id |
|---|---|---|---|
| 发布时间 | 1999 | 2009 | 2015(PHC胜出) |
| 内存硬 | ❌ 否 | ✅ 是 | ✅ 是 |
| CPU硬 | ✅ 是 | ✅ 是 | ✅ 是 |
| 并行抵抗 | ❌ 弱 | ✅ 强 | ✅ 强 |
| 可调参数 | 轮数 | N/r/p(内存/并行) | m/t/p(内存/时间/并行) |
Argon2id(推荐版本)结合了Argon2i(数据无关)和Argon2d(数据相关)的优点。
参数配置建议(OWASP推荐):
| 安全级别 | 内存 | 迭代 | 并行 | 单次验签耗时 |
|---|---|---|---|---|
| 低 | 19 MiB | 2 | 1 | ~0.05s |
| 中 | 78 MiB | 3 | 1 | ~0.2s |
| 高 | 256 MiB | 4 | 2 | ~1.0s |
| 极高 | 1024 MiB | 5 | 4 | ~4.0s |
攻击者成本对比(以高级别配置为例):
假设攻击者使用8块RTX 4090 GPU:
| 哈希函数 | 每秒尝试次数 | 破解8字符口令() | 注意 |
|---|---|---|---|
| MD5 | ~1000亿/秒 | ~0.03秒 | 瞬间破解 |
| SHA-256 | ~350亿/秒 | ~0.1秒 | 瞬间破解 |
| bcrypt (cost=12) | ~7万/秒 | ~11年 | 量级提升 |
| scrypt (N=2^20) | ~2000/秒 | ~39万年 | 内存瓶颈 |
| Argon2id (m=256MB,t=4) | ~200/秒 | ~390万年 | 安全边界 |
# 安装:pip install argon2-cffi
from argon2 import PasswordHasher, Type
ph = PasswordHasher(
time_cost=4, # t = 4 次迭代
memory_cost=256*1024, # m = 256 MiB
parallelism=2, # p = 2 个并行线程
hash_len=32, # 输出32字节哈希
salt_len=16, # 16字节随机盐
type=Type.ID # 使用Argon2id
)
# 存储口令(自动生成随机盐)
hash_str = ph.hash("my_secure_password")
# 输出示例:$argon2id$v=19$m=262144,t=4,p=2$c29tZXNhbHQ$...
# 验证口令
try:
ph.verify(hash_str, "my_secure_password")
print("✓ 口令正确")
except:
print("✗ 口令错误")
核心实践:始终使用随机盐(至少16字节)并采用迭代哈希(至少10000次迭代或等效成本)。通用哈希函数(如SHA-256)不适合直接用于口令存储。
MD5碰撞攻击(王小云,2004):
给定任意初始消息 ,攻击者可以在约 次MD5运算内找到两个不同的消息 使得 。这比理论上限 低数十亿倍。
实际危害:2008年,研究人员利用MD5碰撞创建了一个伪造的CA证书,使攻击者可以冒充任意网站。这是直接导致浏览器厂商停止信任MD5签名的关键事件。
SHA-1碰撞——SHAttered攻击(Google,2017):
Google展示了两个不同的PDF文件具有相同的SHA-1哈希值:
# 两个PDF文件具有相同的SHA-1哈希值
# SHA1("shattered-1.pdf") = SHA1("shattered-2.pdf")
# = 38762cf7f55934b34d179ae6a4c80cadccbb7f0a
此次攻击需要约 次SHA-1运算,成本约11万美元(使用Google Cloud的110个GPU)。
Merkle-Damgård构造(如SHA-256)存在一个固有缺陷:给定 ,不知 也能计算 。
攻击场景:服务器使用 SHA256(secret || message) 作为MAC。攻击者可以构造一个新的合法MAC而不需要知道 secret。
防御:使用HMAC、SHA-3(海绵构造)或BLAKE2(内部使用HAIFA构造)。
| 算法 | 最佳攻击复杂度 | 理论安全强度 | 攻击复杂度相对理论的比例 | 当前状态 |
|---|---|---|---|---|
| MD5 | 倍更弱 | ❌ 完全不可用 | ||
| SHA-1 | 倍更弱 | ❌ 已废除 | ||
| SHA-256 | (预期) | 无显著改进 | ✅ 仍然安全 | |
| SHA3-256 | (预期) | 无显著改进 | ✅ 仍然安全 | |
| BLAKE2b | (预期) | 无显著改进 | ✅ 仍然安全 | |
| BLAKE3 | (预期) | 无显著改进 | ✅ 仍然安全 |
哈希函数是数字签名的核心组件。签名过程先对消息进行哈希,再对哈希值签名:
消息 M ──► SHA-256 ──► hash(M) ──► 私钥加密 ──► 签名
验证过程:
签名 ──► 公钥解密 ──► hash'
──► 比较 ──► 通过/失败
消息 M ──► SHA-256 ──► hash(M)
这样做的好处是可以对任意长度的消息签名,且效率远高于直接对消息签名。
比特币使用双重SHA-256(SHA-256(SHA-256(block)))作为工作量证明的核心。矿工需要找到一个随机数(nonce)使得区块哈希值小于目标值:
| 年份 | 比特币全网算力 | 难度目标前导零位数 | 平均每块耗时 |
|---|---|---|---|
| 2009 | ~10 MH/s | 约8位 | 10 min |
| 2013 | ~10 TH/s | 约16位 | 10 min |
| 2017 | ~10 EH/s | 约20位 | 10 min |
| 2024 | ~600 EH/s | 约29位 | 10 min |
以太坊原本使用Keccak-256(SHA-3前身),现在ETH 2.0转为PoS后不再需要哈希挖矿。
文件下载时使用的哈希校验:
# 下载并验证文件
$ wget https://example.com/ubuntu-24.04.iso
$ wget https://example.com/ubuntu-24.04.iso.sha256
$ sha256sum --check ubuntu-24.04.iso.sha256
ubuntu-24.04.iso: OK
HMAC(Hash-based Message Authentication Code)是使用哈希函数构建MAC的标准方案:
其中 重复, 重复, 是密码学上处理后的密钥。
HMAC的安全性证明:如果底层哈希函数是伪随机函数族,则HMAC是安全的。这比简单的 H(key || message) 方案安全得多。
在数据结构和算法中,哈希函数用于构建哈希表和布隆过滤器:
布隆过滤器可以近似判断某个元素是否在一个集合中:
from hashlib import sha256, md5
import bitarray
class BloomFilter:
def __init__(self, size: int, num_hashes: int):
self.bits = bitarray.bitarray(size)
self.bits.setall(0)
self.size = size
self.num_hashes = num_hashes
def _hashes(self, item: str):
"""使用SHA-256和MD5生成两个基础哈希值,再通过线性组合派生更多"""
h1 = int(sha256(item.encode()).hexdigest(), 16)
h2 = int(md5(item.encode()).hexdigest(), 16)
return [(h1 + i * (h2 or 1)) % self.size for i in range(self.num_hashes)]
def add(self, item: str):
for h in self._hashes(item):
self.bits[h] = 1
def contains(self, item: str) -> bool:
return all(self.bits[h] for h in self._hashes(item))
# 示例:使用1MB空间,7个哈希函数
bf = BloomFilter(size=8_000_000, num_hashes=7)
for url in ["https://evil.com"]:
bf.add(url)
print(f"已知恶意URL: {bf.contains('https://evil.com')}") # True
print(f"未知URL: {bf.contains('https://safe.com')}") # 极大概率False
测试环境:Intel Core i7-12700K, DDR5-5600, Ubuntu 22.04
| 消息大小 | SHA-256 | SHA3-256 | BLAKE2b | BLAKE3 |
|---|---|---|---|---|
| 64 B | 185 MB/s | 135 MB/s | 220 MB/s | 240 MB/s |
| 1 KB | 520 MB/s | 360 MB/s | 850 MB/s | 900 MB/s |
| 64 KB | 680 MB/s | 420 MB/s | 1.1 GB/s | 1.3 GB/s |
| 1 MB | 720 MB/s | 430 MB/s | 1.2 GB/s | 1.4 GB/s |
| 256 MB | 730 MB/s | 435 MB/s | 1.21 GB/s | 1.42 GB/s |
| 算法 | 输出长度 | 碰撞安全 | 性能(1MB, 单线程) | 推荐场景 |
|---|---|---|---|---|
| SHA-224 | 224 bit | 112 bit | 780 MB/s | 兼容性(较少用) |
| SHA-256 | 256 bit | 128 bit | 730 MB/s | 通用推荐 |
| SHA-384 | 384 bit | 192 bit | 520 MB/s | 高级安全 |
| SHA-512 | 512 bit | 256 bit | 480 MB/s | 最大安全 |
| SHA3-256 | 256 bit | 128 bit | 435 MB/s | 抗长度扩展 |
| BLAKE2b | 可变 | 最高256 bit | 1210 MB/s | 高性能需求 |
| BLAKE3 | 可变 | 最高256 bit | 1420 MB/s | 超高性能+多线程 |
需要哈希函数?
│
├── 口令存储?──► Argon2id (推荐) 或 scrypt
│
├── 需要最高性能?──► BLAKE3 (多线程) 或 BLAKE2b (单线程)
│
├── HMAC/数字签名?──► SHA-256 (广泛兼容) 或 BLAKE2b (性能)
│
├── 需要抗长度扩展?──► SHA3-256 或 BLAKE2b
│
└── 区块链/PoW?──► 按协议要求 (比特币SHA-256d, 以太坊Keccak-256)
H(key || message) 构造MAC| ❌ 错误做法 | 问题 | ✅ 正确做法 |
|---|---|---|
SHA256(password) |
无盐,可被彩虹表破解 | Argon2id(password, salt) |
SHA256(key + message) |
长度扩展攻击 | HMAC-SHA256(key, message) |
MD5(file) 校验文件 |
可构造碰撞 | SHA256(file) 或 BLAKE3(file) |
| 用SHA-256哈希数百万URL | 哈希碰撞概率累积 | 使用Bloom Filter |
| 相同的盐用于所有用户 | 一个盐泄露影响全局 | 每个用户独立随机盐 |
量子计算机对哈希函数的威胁主要来自Grover算法,可以将抗原像攻击的复杂度从 降低到 。这意味着:
好消息是:
结论:哈希函数的量子安全准备就是增加输出长度——从256位升级到384位或512位。所有当前标准化的哈希函数在量子时代仍然可用,只是需要更大的安全边际。