密钥派生函数(KDF):HKDF、PBKDF2、scrypt 与 Argon2 深度对比
概述
密钥派生函数(Key Derivation Function, KDF)是密码学中最基础也最容易被忽视的组件之一。它的核心任务是:从一个可能不够均匀、不够长的输入密钥材料(Keying Material)中,派生出密码学安全的密钥。
为什么需要 KDF?因为密码学算法(AES、SM4、HMAC 等)要求密钥具有特定的长度和均匀分布的随机性,而现实中的密钥来源往往不满足这些要求:
- 用户密码:长度不一、熵值低
- Diffie-Hellman 共享秘密:长度固定但可能不够均匀
- 主密钥:需要派生出多个用途不同的子密钥
KDF 的分类
根据应用场景和安全目标,KDF 可以分为两大类:
1. 基于伪随机函数的 KDF(PRF-based KDF)
适用于高熵输入(如 DH 共享秘密、主密钥),目标是密钥分离和扩展。
代表:HKDF(RFC 5869)
2. 基于密码的 KDF(Password-based KDF, PBKDF)
适用于低熵输入(如用户密码),目标是增加计算成本以抵抗暴力破解。
代表:PBKDF2(RFC 8018)、scrypt(RFC 7914)、Argon2(RFC 9106)
HKDF:基于 HMAC 的密钥派生
设计背景
HKDF(HMAC-based Key Derivation Function)由 Krawczyk 在 2010 年提出,定义于 RFC 5869。它是最简洁、最通用的 KDF 设计,被 TLS 1.3、Signal 协议、WireGuard 等广泛采用。
两阶段结构
HKDF 由两个阶段组成:
阶段 1:HKDF-Extract(提取)
PRK = HMAC-Hash(salt, IKM)IKM(Input Keying Material):输入密钥材料salt:可选的盐值(不提供则使用全零串)PRK(Pseudo-Random Key):提取出的伪随机密钥
阶段 2:HKDF-Expand(扩展)
T(0) = empty string
T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
...
OKM = T(1) | T(2) | ... 截断到所需长度info:可选的上下文信息(用于密钥分离)OKM(Output Keying Material):输出的派生密钥
info 值可以从同一个 PRK 派生出多个独立的密钥。安全特性
- 安全性归约:HKDF 的安全性归约到底层 PRF(HMAC)的安全性
- 密钥分离:不同的
info值保证派生密钥的独立性 - 前向安全:即使某个派生密钥泄露,也无法反推 PRK 或其他派生密钥
国密场景中的 HKDF
在国密体系中,HKDF 可以使用 SM3 作为底层哈希函数:
PRK = HMAC-SM3(salt, IKM)
T(i) = HMAC-SM3(PRK, T(i-1) | info | i)GM/T 0003.4-2012(SM2 密钥交换协议)中定义的密钥派生函数与 HKDF-Expand 结构类似,使用 SM3 哈希迭代生成密钥数据。
PBKDF2:基于密码的密钥派生
设计背景
PBKDF2(Password-Based Key Derivation Function 2)定义于 PKCS#5 v2.0(RFC 8018),是最广泛使用的密码哈希函数。
算法结构
DK = PBKDF2(PRF, Password, Salt, c, dkLen)其中:
PRF:伪随机函数(通常为 HMAC-SHA256 或 HMAC-SM3)Password:用户密码Salt:盐值(推荐 ≥128 位)c:迭代次数(推荐 ≥600,000 次,OWASP 2023 建议)dkLen:派生密钥长度
U₁ = PRF(Password, Salt || INT(32, i))
U₂ = PRF(Password, U₁)
...
U_c = PRF(Password, U_{c-1})
T_i = U₁ ⊕ U₂ ⊕ ... ⊕ U_c
DK = T₁ || T₂ || ... 截断到 dkLen安全分析
PBKDF2 通过迭代增加计算成本,但存在一个根本性弱点:内存硬度不足。
攻击者可以使用 ASIC 或 GPU 并行计算,因为每次迭代只需要存储前一次的结果(O(1) 内存)。这使得 PBKDF2 在面对大规模并行攻击时防护能力有限。
参数选择建议
| 安全级别 | 迭代次数 | 适用场景 |
|---|---|---|
| 最低 | 100,000 | 遗留系统 |
| 推荐 | 600,000 | 一般应用(OWASP 2023) |
| 高安全 | 1,000,000+ | 敏感数据保护 |
scrypt:内存困难型 KDF
设计背景
scrypt 由 Colin Percival 于 2009 年提出,定义于 RFC 7914。它专门设计来抵抗 ASIC/GPU 攻击,通过内存困难性(Memory-hardness)大幅提高攻击者的硬件成本。
算法结构
scrypt(N, r, p, Password, Salt, dkLen)核心参数:
N:CPU/内存成本参数(必须是 2 的幂,推荐 2^17 = 131072)r:块大小参数(推荐 8)p:并行化参数(推荐 1)
- 使用 PBKDF2 生成初始块 B
- 对 B 中的每个块 V_i,执行 N 次迭代:
V_i = ScryptROMix(B_i, N)- ScryptROMix 维护一个大小为 N 的数组 V,每次迭代都随机读写数组元素
DK = PBKDF2(Password, B, 1, dkLen)内存困难性原理
scrypt 的核心思想是:计算过程需要大量内存,且内存访问模式是伪随机的。
- 内存需求:128 × N × r 字节(N=2^17, r=8 时约 128MB)
- 攻击者如果想减少内存使用,就必须重新计算丢失的块,这会增加计算量
- ASIC 的优势在于并行计算,但内存是物理资源,无法通过增加核心数来分摊
与国密结合
scrypt 可以与 SM3 结合使用:
import hashlib
# 使用 HMAC-SM3 替代 HMAC-SHA256
def sm3_prf(password, salt):
return hmac.new(password, salt, hashlib.sm3).digest()Argon2:密码哈希竞赛冠军
设计背景
Argon2 是 2015 年密码哈希竞赛(PHC)的获胜者,定义于 RFC 9106。它综合了 scrypt 的内存困难性和新的数据依赖内存访问模式,是目前最推荐的密码哈希算法。
三种变体
| 变体 | 特点 | 适用场景 |
|---|---|---|
| Argon2d | 数据依赖内存访问,抗 GPU/ASIC | 加密货币、无侧信道风险场景 |
| Argon2i | 数据独立内存访问,抗侧信道 | 密码哈希(推荐) |
| Argon2id | 混合模式(前几轮独立,后几轮依赖) | 通用推荐 |
算法结构
Argon2(type, version, m, t, p, Password, Salt, ...)核心参数:
type:Argon2d/Argon2i/Argon2idm:内存大小(KB),推荐 65536(64MB)t:迭代次数,推荐 3p:并行度,推荐 4
- 初始化:用密码和盐值填充初始块
- 填充内存:生成 m/4 个 1KB 块,每个块依赖前一个块
- 迭代混合:进行 t 轮混合,每轮对块进行 Blake2b 哈希
- 最终化:对最后一个块进行哈希得到输出
安全优势
- 可证明安全:Argon2 的安全性可以归约到 Blake2b 的安全性
- 灵活配置:可以独立调整内存、时间和并行度
- 抗多种攻击:同时抵抗 GPU、ASIC 和侧信道攻击
OWASP 2023 推荐参数
Argon2id: m=65536 KB, t=3, p=4
Argon2id: m=131072 KB, t=4, p=4(高安全)四种 KDF 深度对比
| 维度 | HKDF | PBKDF2 | scrypt | Argon2 |
|---|---|---|---|---|
| 输入类型 | 高熵密钥 | 低熵密码 | 低熵密码 | 低熵密码 |
| 内存困难性 | 无 | 无 | 有 | 有 |
| 抗 GPU/ASIC | 弱 | 弱 | 中 | 强 |
| 抗侧信道 | 不适用 | 不适用 | 弱 | 强(Argon2i/id) |
| 计算速度 | 极快 | 快(可调) | 中 | 中(可调) |
| 内存需求 | O(1) | O(1) | O(N×r) | O(m) |
| 标准化 | RFC 5869 | RFC 8018 | RFC 7914 | RFC 9106 |
| 国密适配 | HMAC-SM3 | HMAC-SM3 | 可适配 | 可适配 |
| 典型用途 | TLS 密钥派生 | 密码存储 | 密码存储 | 密码存储(首选) |
工程选型指南
场景 1:从 DH/SM2 共享秘密派生会话密钥
推荐:HKDF
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
# 国密场景:使用 SM3
# 注意:Python cryptography 库原生不支持 SM3,需要使用 gmssl 库
hkdf = HKDF(
algorithm=hashes.SM3(), # 需要 gmssl 或自定义实现
length=32,
salt=None,
info=b"session-key-v1",
)
key = hkdf.derive(shared_secret)场景 2:用户密码存储
推荐:Argon2id
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # 迭代次数
memory_cost=65536, # 64MB
parallelism=4,
hash_len=32,
salt_len=16,
)
hash = ph.hash("user_password")
ph.verify(hash, "user_password")场景 3:从主密钥派生多个子密钥
推荐:HKDF(不同 info 值)
# 派生加密密钥
enc_key = hkdf_derive(master_key, info=b"encryption")
# 派生 MAC 密钥
mac_key = hkdf_derive(master_key, info=b"authentication")
# 派生 IV
iv_key = hkdf_derive(master_key, info=b"iv-generation")场景 4:国密合规场景
在需要满足国密合规要求的系统中:
- 密钥派生:使用 GM/T 0003.4-2012 定义的 KDF(基于 SM3)
- 密码存储:使用 PBKDF2-HMAC-SM3 或 Argon2(底层使用 SM3)
- 随机数:使用 GM/T 0005-2012 定义的随机数生成器
常见误区
误区 1:直接用哈希函数作为 KDF
# ❌ 错误:直接用 SHA-256/SM3 哈希
key = sha256(password + salt)
# ✅ 正确:使用专用 KDF
key = hkdf(password, salt, info)直接用哈希函数缺少密钥分离能力,且对低熵输入没有保护。
误区 2:盐值复用
# ❌ 错误:所有用户使用相同盐值
key = pbkdf2(password, fixed_salt)
# ✅ 正确:每个用户使用随机盐值
salt = os.urandom(16)
key = pbkdf2(password, salt)盐值的作用是防止彩虹表攻击,复用盐值等于没有盐值。
误区 3:迭代次数过低
# ❌ 错误:迭代次数太低
key = pbkdf2(password, salt, iterations=1000)
# ✅ 正确:使用推荐的迭代次数
key = pbkdf2(password, salt, iterations=600000)迭代次数直接决定暴力破解的成本。
总结
密钥派生函数是密码学基础设施的基石。选择合适的 KDF 需要综合考虑:
- 输入熵值:高熵用 HKDF,低熵用 PBKDF2/scrypt/Argon2
- 安全需求:一般场景用 PBKDF2,高安全用 Argon2id
- 性能约束:资源受限环境用 PBKDF2,一般环境用 Argon2id
- 合规要求:国密场景需要适配 SM3 哈希函数
参考来源
- RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
- RFC 8018: PKCS #5: Password-Based Cryptography Specification Version 2.1
- RFC 7914: The scrypt Password-Based Key Derivation Function
- RFC 9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work
- GM/T 0003.4-2012: SM2 公钥密码算法 第4部分:密钥交换协议
- GM/T 0005-2012: 随机性检测规范
- OWASP Password Storage Cheat Sheet (2023)
相关实践
- 如需了解密码学安全随机数生成的工程实践,请参阅《密码学安全随机数生成:从 /dev/urandom 到 CSPRNG 工程实践》
- 如需了解国密算法在 JWT 中的应用,请参阅《国密算法在 JWT 中的应用:SM2-SM3 签名与 SM4-CBC 加密实战》