密码学安全随机数生成:CSPRNG 原理与熵源管理
为什么随机数对密码学至关重要
密码学中几乎所有安全协议的正确性和安全性,都依赖于随机数的质量。密钥生成需要随机数,初始化向量(IV)需要随机数,Nonce 需要随机数,挑战-应答协议中的挑战值需要随机数,甚至零知识证明中的模拟器也需要随机数。如果随机数可被预测或重复,整个密码系统的安全性将轰然倒塌。
历史上因随机数质量问题导致的安全事故屡见不鲜:
- 2012 年 Android 比特币钱包漏洞:Java
SecureRandom实现中熵源不足,导致生成的 ECDSA 私钥可被恢复,大量比特币被盗。 - 2010 年 Sony PS3 ECDSA 密钥泄露:Sony 在实现 ECDSA 签名时使用了固定的 k 值(而非随机数),导致私钥被直接计算出来。
- Debian OpenSSL 漏洞(2008):一个错误的代码注释移除了熵源中的大部分随机性,导致生成的密钥空间从 $2^{128}$ 骤降至约 $2^{15}$,所有使用该版本 OpenSSL 生成的密钥均不安全。
随机数的分类与定义
真随机数生成器(TRNG)
真随机数生成器(True Random Number Generator)从物理现象中提取随机性,其输出在理论上不可预测。常见的物理熵源包括:
| 熵源类型 | 物理原理 | 典型速率 |
|---|---|---|
| 热噪声(Johnson-Nyquist 噪声) | 电阻中电子的热运动 | ~Mbps |
| 振荡器抖动(Jitter) | 两个自由振荡器的相位差 | ~Mbps |
| 半导体击穿噪声 | 齐纳二极管的雪崩击穿 | ~Mbps |
| 鼠标/键盘时序 | 用户输入的微观时序差异 | ~bps |
| 磁盘寻道时间 | 机械硬盘的旋转/寻道抖动 | ~kbps |
| 大气噪声 | 无线电频段的电磁噪声 | ~kbps |
伪随机数生成器(PRNG)
伪随机数生成器(Pseudorandom Number Generator)使用确定性算法,从一个较短的种子(seed)扩展为较长的输出序列。其安全性完全依赖于种子的保密性和随机性。
PRNG 的核心安全性质是不可区分性(indistinguishability):对于任何多项式时间的敌手,PRNG 的输出与真正的均匀随机串在计算上不可区分。
密码学安全伪随机数生成器(CSPRNG)
CSPRNG 是满足密码学安全要求的 PRNG,其形式化定义如下:
定义(CSPRNG):设 $G: \{0,1\}^n \rightarrow \{0,1\}^{l(n)}$ 是一个确定性多项式时间算法,其中 $l(n) > n$。如果对于所有概率多项式时间区分器 $D$,有:>
$$\left| \Pr[D(G(U_n)) = 1] - \Pr[D(U_{l(n)}) = 1] \right| \leq \text{negl}(n)$$>
其中 $U_n$ 表示 $n$ 位均匀随机串,$\text{negl}(n)$ 是可忽略函数,则称 $G$ 是一个密码学安全的伪随机数生成器。与普通 PRNG 不同,CSPRNG 还需要满足两个额外的安全性质:
后向安全性(Backward Security / Break-in Recovery):即使攻击者在某一时刻获取了 CSPRNG 的内部状态,也无法恢复之前输出的随机数。
前向安全性(Forward Security):即使攻击者获取了当前内部状态,在状态更新后,之前的状态信息不会帮助预测未来的输出。
CSPRNG 的安全模型
不可预测性(Unpredictability)
CSPRNG 的核心安全要求是下一比特不可预测性(next-bit unpredictability):给定前 $k$ 个输出比特,任何多项式时间算法无法以显著优于 $1/2$ 的概率预测第 $k+1$ 个比特。
Yao 在 1982 年证明了以下等价性定理:
Yao 定理:一个 PRNG 满足下一比特不可预测性,当且仅当它是计算上不可区分的(即满足 CSPRNG 的定义)。
状态泄露扩展(State Compromise Extension)
在实际系统中,CSPRNG 的内部状态可能因侧信道攻击、内存泄露等原因被部分或全部泄露。因此,现代 CSPRNG 设计需要考虑状态泄露后的恢复能力:
- 完全恢复型:在获取新的熵输入后,CSPRNG 的安全性自动恢复。
- 有限恢复型:只能保证未来输出的安全性,无法恢复过去输出的安全性。
- 无恢复型:一旦状态泄露,所有后续输出均不可信。
主流 CSPRNG 算法架构
基于分组密码的 CSPRNG
利用 AES 等分组密码算法构造 CSPRNG 是最经典的方法之一。
CTR_DRBG(Counter Mode DRBG):NIST SP 800-90A 推荐的方案之一,使用 AES-CTR 模式生成伪随机比特流。
工作流程:
- 初始化:使用种子密钥 $K$ 和计数器 $V$
- 生成:对 $V+1, V+2, \ldots$ 进行 AES 加密,输出密文块
- 更新:生成一定量输出后,用新的熵源更新 $(K, V)$
基于 SM4 的 CTR_DRBG:在国密体系中,可以使用 SM4 替代 AES 实现 CTR_DRBG,其原理和流程完全相同。
基于哈希函数的 CSPRNG
Hash_DRBG:NIST SP 800-90A 推荐的另一方案,基于哈希函数(如 SHA-256)构造。
工作流程:
- 初始化:$V = \text{Hash}(\text{seed})$,$C = \text{Hash}(V \| 0x00)$
- 生成:输出 $\text{Hash}(V)$,然后 $V = V + 1$
- 更新:$V = \text{Hash}(V \| C \| \text{new\_entropy})$
基于数论的 CSPRNG
Blum Blum Shub(BBS):基于二次剩余问题的困难性。
$$x_{i+1} = x_i^2 \mod N$$
其中 $N = pq$ 是两个大素数的乘积。输出取 $x_i$ 的最低有效位。BBS 的安全性可归约到整数分解问题的困难性,但速度较慢,主要用于理论分析。
ANSI X9.31 PRNG:基于 3DES 或 AES,使用当前时间值和内部状态生成随机数。曾广泛用于金融行业,但因潜在弱点已被 NIST SP 800-90A 方案取代。
Linux 内核 CSPRNG
Linux 内核的随机数子系统是 CSPRNG 工程实现的典范,其架构经历了多次演进:
早期设计(< 3.17):
/dev/random:阻塞式,估计熵池中的熵量,熵不足时阻塞/dev/urandom:非阻塞式,熵不足时仍输出(依赖 CSPRNG 的安全性)
- 使用 ChaCha20 算法作为 CSPRNG 的核心
- 引入 CRNG(ChaCha20 Random Number Generator)
getrandom()系统调用:在熵池初始化完成前阻塞,初始化后行为与/dev/urandom相同/dev/random和/dev/urandom在熵池初始化后行为一致
- 输入池(Input Pool):收集来自硬件、设备驱动、中断时序等的熵
- 阻塞池(Blocking Pool):用于
/dev/random的阻塞语义 - CRNG:基于 ChaCha20 的 CSPRNG,从阻塞池播种
熵源管理
熵的度量
信息论中,熵(Entropy)度量随机变量的不确定性。对于离散随机变量 $X$,Shannon 熵定义为:
$$H(X) = -\sum_{x \in \mathcal{X}} P(X = x) \log_2 P(X = x)$$
密码学中更常用的是最小熵(Min-Entropy):
$$H_\infty(X) = -\log_2 \left( \max_{x \in \mathcal{X}} P(X = x) \right)$$
最小熵给出了最坏情况下的不确定性度量,是密码学安全分析的标准工具。
熵源的健康测试
NIST SP 800-90B 规定了熵源的健康测试要求:
重复计数测试(Repetition Count Test):检测熵源是否陷入固定值输出。如果在连续 $N$ 个样本中出现相同的值,则报警。
自适应比例测试(Adaptive Proportion Test):检测熵源中某个值的出现频率是否异常偏高。
熵池管理策略
Linux 内核的熵估计:
- 每个熵源贡献的熵量由驱动程序声明
- 内核维护一个熵计数器(entropy count)
- 当熵计数器达到阈值(通常为 256 位)时,认为熵池已充分播种
- 使用 XOR 操作将新熵混合到熵池中
- 使用 LFSR(线性反馈移位寄存器)进行扩散
- 现代设计使用密码学哈希函数进行混合
NIST SP 800-90 系列标准
NIST(美国国家标准与技术研究院)的 SP 800-90 系列标准是 CSPRNG 领域最权威的标准体系:
| 标准 | 内容 | 状态 |
|---|---|---|
| SP 800-90A | 随机数生成机制(DRBG) | 2015 年修订版 |
| SP 800-90B | 熵源规范 | 2018 年版 |
| SP 800-90C | RNG 整体架构设计 | 草案阶段 |
- Hash_DRBG:基于 SHA-2 系列哈希函数
- HMAC_DRBG:基于 HMAC 构造
- CTR_DRBG:基于 AES 分组密码
- GM/T 0005-2012《随机性检测规范》
- GM/T 0006-2012《密码应用标识规范》
- 在国密体系中,CTR_DRBG 和 Hash_DRBG 可以使用 SM4 和 SM3 替代 AES 和 SHA-256
低熵环境下的挑战
嵌入式设备
嵌入式设备(如 IoT 传感器、智能卡)通常缺乏丰富的物理熵源:
- 没有键盘/鼠标等用户交互设备
- 没有机械硬盘
- 启动时系统状态高度可预测
- 在制造阶段注入唯一性密钥(Unique ID)作为初始种子
- 利用 SRAM 上电时的随机初始状态
- 采集 ADC(模数转换器)的最低位噪声
- 使用专用硬件 TRNG 模块
虚拟机
虚拟机面临更严重的熵不足问题:
- 虚拟硬件的确定性行为减少了可用熵源
- 虚拟机克隆导致初始状态完全相同
- 快照恢复后熵池状态回退
- 使用 virtio-rng 设备从宿主机传递熵
- 利用 CPU 的 RDRAND/RDSEED 指令(如果虚拟化层透传)
- 在云环境中使用云厂商提供的熵服务(如 AWS Nitro Enclaves 的熵源)
- 使用
haveged或jitterentropy等软件熵增强工具
容器环境
Docker 等容器共享宿主机的熵池,通常不会出现熵不足问题,但需要注意:
- 容器内的
/dev/random和/dev/urandom实际上是宿主机的设备 - 大量容器同时启动时可能短暂消耗熵池
- 建议使用
getrandom()系统调用而非直接读取设备文件
CSPRNG 的安全使用原则
播种原则
- 种子长度:种子应至少提供 128 位安全强度(即至少 128 位最小熵)
- 种子唯一性:每次系统启动应使用不同的种子
- 种子保密性:种子应存储在安全区域,防止泄露
- 持续重播种:在获取新熵后应定期重播种
使用禁忌
- 禁止自行设计 CSPRNG 算法:应使用经过标准认证的实现
- 禁止使用非密码学 PRNG:如
rand()、Math.random()、Linear Congruential Generator等 - 禁止重复使用种子:每次生成密钥应使用新的随机种子
- 禁止截断输出:CSPRNG 的输出应完整使用,不应截断后作为密钥
常见错误
// 错误:使用时间作为随机种子
srand(time(NULL));
int key = rand(); // 完全不安全!
// 正确:使用 CSPRNG
#include <openssl/rand.h>
unsigned char key[32];
RAND_bytes(key, sizeof(key)); // 使用 OpenSSL 的 CSPRNG国密体系中的随机数生成
在国密(GM)体系中,随机数生成需要满足以下要求:
- GM/T 0005-2012:规定了随机性检测的统计测试方法,包括频数测试、块内频数测试、游程测试、矩阵秩测试等 15 项测试
- GM/T 0003.3-2012:SM2 算法标准第 3 部分(密钥交换协议)中规定了密钥生成时对随机数的要求
- GM/T 0004-2012:SM3 密码杂凑算法标准
- GM/T 0009-2012:SM2 密码算法使用规范中对随机数的要求
相关实践
- 密码学安全随机数生成:从 /dev/urandom 到 CSPRNG 工程实践 — 深入讲解 Linux、OpenSSL、Java、Go 等平台的 CSPRNG 实现与最佳实践