随机数生成:真随机数生成器与伪随机数生成器,熵源选择
密钥安全的第一道关口,就是随机数的质量。你想想看,如果密钥是用一个可预测的序列生成的,那加密还有什么意义?这章我就专门聊聊随机数的生成——怎么区分真随机和伪随机,怎么选熵源,怎么在工程里落地。
我在做安全审计时见过不少事故,说白了都是随机数没整明白。
3.1 随机数的本质与密码学要求
密码学里说的随机数,和你在程序里用的 Math.random() 完全不是一回事。
它得满足三个硬性条件:
- 不可预测性——就算知道过去输出的所有随机数,也没法推断出下一个。
- 均匀分布——每一位是0或1的概率基本相等,不能有偏。
- 独立性——任何两个随机数之间统计上无关。
实践中,真正能达到这三条的只有真随机数生成器(TRNG)。
核心原则:密钥材料必须来自不可预测的熵源。任何算法生成的伪随机数,如果种子被泄露,就是假的。
3.2 真随机数生成器(TRNG)
TRNG 从物理过程中提取随机性。比如:
- 热噪声(电阻的约翰逊噪声)
- 时钟抖动(晶振的微小漂移)
- 量子效应(光子偏振)
- 用户行为(鼠标移动、键盘时序)
我记得有一次做嵌入式密钥生成,芯片内置的 TRNG 采样率不够,导致密钥生成速度卡脖子。最后我们外挂了一个专用熵源芯片,才解决问题。
TRNG 的优点是真正的不可预测,但缺点是输出速率低、对硬件依赖强。
3.3 伪随机数生成器(PRNG)
PRNG 用确定性算法把短种子扩展成看似随机的长序列。密码学里用的叫 CSPRNG(密码学安全伪随机数生成器),比如:
- Yarrow(FreeBSD 的 /dev/random)
- ChaCha20 CSPRNG(Linux 5.6+ 的内核随机池)
- Fortuna(在 Yarrow 基础上改进)
| 特性 | TRNG | CSPRNG |
|---|---|---|
| 熵来源 | 物理过程 | 种子(来自于TRNG) |
| 输出速率 | 慢(<100 Mbps) | 快(>1 Gbps) |
| 可预测性 | 不可预测 | 种子未知时不可预测 |
| 硬件依赖 | 高 | 低 |
| 典型用途 | 种子生成、一次性密钥 | 批量密钥、会话密钥 |
你在工程里碰到的 /dev/urandom 和 openssl rand,本质都是 CSPRNG。它们会定期从内核熵池里补充新熵。
千万别把 rand() 或 mt_rand() 用在密钥生成上。它们不是 CSPRNG,种子空间小到可以被暴力破解。我曾经见过一个项目用 time(NULL) 做种子,结果所有密钥都在同一秒内重复——简直是给攻击者送大礼。
3.4 熵源的选择与评估
熵源的质量直接决定密钥安全。我习惯按这三级评估:
- 硬件熵源——专用 TRNG 芯片、CPU 内置 DRNG(Intel RDRAND),最好用,但要注意后门风险。
- 系统熵源——Linux 的
/dev/random(阻塞模式)和/dev/urandom(非阻塞)。我建议优先用/dev/urandom,除非在系统刚启动时种子不够。 - 混合熵源——组合多个物理源(比如鼠标 + 麦克风 + 网络延迟),通过 Hash 提取公共熵。
怎么评估熵源够不够?看这个:
熵估值:理论上每比特输出应至少包含 0.5 位的真实熵。常用工具如 ent、Dieharder、NIST SP 800-90B 测试套件。我每次换硬件平台,都会先跑一遍 NIST 测试,卡住就不放行。
我曾经被一个问题坑过:某 ARM 板的硬件熵源在低功耗模式下输出全零。后来做了一票否决机制——连续采样 8 次如果全是 0 或 1,直接报错,不留隐含陷阱。
3.5 工程实践中的关键原则
- 从不直接使用 TRNG 输出做密钥——经过 CSPRNG 混合,消除硬件偏置。
- 定期 reseed——Linux 内核每秒会 reseed 一次,但如果你自己实现 CSPRNG,建议每生成 1MB 随机数就触发一次。
- 禁止使用系统时间、PID、MAC 地址等作为唯一种子——这些都能被对手猜到。
一个小技巧:在应用层,可以调用 getrandom() 系统调用(Linux 3.17+),它比 /dev/urandom 更安全,而且不会因为文件描述符耗尽而出错。我新写的代码现在都用它。
下面这张图总结了我理解的随机数生成流程。记住:熵源是根,种子是干,CSPRNG 是枝,密钥是果。
最后提醒一句:别在随机数上耍小聪明。用现成的系统 CSPRNG,配好熵源监控,这就是最稳妥的做法。我上个月还在审查一个代码,发现有人自己实现了一个 LCG 算法生成密钥……当时我就直接打回了。千万别重造劣质轮子。