密钥生成算法与安全随机数

为什么要关注密钥生成?

很多人觉得密钥生成嘛,调个库就行了。

嗯,理论上是这样。但我在生产环境里见过太多因为随机数出问题导致密钥被破解的案例。说白了,密钥是边界系统的第一道防线,生成这一步垮了,后面所有加密都是纸上谈兵。

你想想看,如果你用的随机数发生器不够强,攻击者就能预测出你下一把密钥长什么样。那我建议你先停下来认真看看这一节。

对称密钥:随机就是一切

AES-256 的密钥是 32 字节。这 32 个字节怎么来?必须来自密码学安全的随机数发生器(CSPRNG)。

我个人习惯用 os.urandom(32) 直接拿系统的熵池输出。Python 里也可以这么写:

import os
aes_key = os.urandom(32)  # 256-bit 密钥
print(aes_key.hex())

有个坑:千万别用 random.getrandbits(256)。那个是伪随机数,不适合做密钥。我见过一个团队直接把随机库的种子设为固定时间戳,结果所有节点的 AES 密钥都一样……还好是在测试环境发现的。

警告:生产环境永远不要用语言内建的标准随机库(如 Python 的 random、Java 的 java.util.Random)生成密钥。它们不是为加密设计的。

对称密钥还有一种生成方式叫密钥派生(KDF)。比如你从用户的密码派生出一个 AES 密钥,这时候要用 PBKDF2 或 Argon2。我在做身份认证模块时经常用:

from hashlib import pbkdf2_hmac
import os

salt = os.urandom(16)
key = pbkdf2_hmac('sha256', b'user_password', salt, 100000, dklen=32)

迭代次数一定要够。我一般设 10 万次以上,有些场景用 30 万次。防暴力破解就靠这个。

非对称密钥:素数挑选与曲线参数

RSA 密钥生成需要找两个大素数。我刚开始做的时候觉得这不就是筛法加概率检验嘛,直到有一次在生产环境里生成 4096 位 RSA 密钥,花了将近两分钟——那台设备 CPU 太弱。

我个人建议用 OpenSSL 或 libsodium 的封装,别自己手写。举个例子:

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend

private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=4096,
    backend=default_backend()
)

公钥指数用 65537 是业界共识,别改。我见过有人贪快用 3,结果被低指数攻击直接拿下。

ECC 这边更依赖安全随机数。椭圆曲线的私钥就是一个随机整数,范围在曲线的阶之内。如果随机数有偏差,攻击者可以还原私钥。我记得有一年某知名开源库因为随机数生成器忘了加熵,导致 ECDSA 签名重复使用了同一个 k 值,私钥被当场算出来——这就是经典的「随机数重复攻击」。

混合密钥生成:TLS 握手中的临时密钥

实战中很少只用一种密钥。TLS 1.3 握手时,客户端和服务端各自生成临时 ECDHE 密钥对,然后通过 DH 交换算出会话密钥。这个过程要求每次握手都用新的随机数,不能缓存。

我踩过一个坑:在嵌入式设备上,因为没有硬件熵源,PRNG 初始化时种子太弱,导致每次重启后生成的临时密钥序列完全一致。攻击者只要抓一次握手就能预测后续所有会话的密钥。后来我在初始化脚本里强制混入网络延迟、传感器噪声等多源熵,问题才解决。

核心原则:临时密钥(ephemeral keys)必须每次握手重新生成,且熵源必须独立、不可预测。

安全随机数生成器:TRNG vs PRNG

很多人混淆这两个概念。我画了一张图帮你理清关系:

密钥生成与随机数流转结构 物理熵源 (硬件噪声/热噪声) OS 熵池 (/dev/urandom) CSPRNG (加密安全伪随机) 对称密钥 非对称密钥 临时密钥 文件/数据库加密 数字签名/证书 TLS/SSH 会话 关键说明 ① 所有随机数必须最终来自物理熵源; ② CSPRNG 每次输出后必须更新内部状态,防止回溯。

TRNG(真随机数发生器)靠硬件噪声,速度慢但不可预测。PRNG(伪随机数发生器)靠算法扩展熵,速度快,但种子必须真随机。CSPRNG 是带密码学安全属性的 PRNG,比如 Linux 的 /dev/urandom。

特性 TRNG CSPRNG
熵来源 物理随机过程 种子从 TRNG 或熵池获得
输出速率 低(几十 Kbps) 高(可达 Gbps)
可预测性 理论上不可预测 种子未知时不可预测
典型场景 熵种子、硬件安全模块 批量密钥生成、TLS 握手
我的做法:在边界系统的密钥生成模块里,我会用 TRNG 定期补充熵池,然后让 CSPRNG 输出所有业务密钥。每隔一小时从 TRNG 拿 256 比特喂给 CSPRNG 做 reseed。

常见陷阱与避坑指南

  1. 种子重用:我曾经遇到一个系统,每次重启后 PRNG 种子相同,导致所有密钥一样。解决方案是每次启动时强制读取硬件熵源。
  2. 拒绝服务:PRNG 在熵池耗尽时阻塞。我记得有一次线上故障,因为 /dev/random 读不到足够的熵,密钥生成进程卡死。后来全部改用 /dev/urandom(除非你非常明确需要阻塞)。
  3. 密钥强度不足:生成 RSA 密钥时 key_size 低于 2048,或者 ECC 曲线选择 secp256r1 以下。我个人建议至少 2048 位 RSA 或 P-384 曲线。
  4. 调试环境不小心暴露:我在日志里见过有人把私钥的十六进制直接打印出来。日志收集系统会存很久,这是巨大的风险。
警告:永远不要在日志、异常堆栈、监控指标中输出私钥或种子值。即使脱敏后也不行,攻击者可以通过模式分析猜出部分内容。

一个实战检查清单

  • 确认所有密钥生成都调用 CSPRNG(如 os.urandom、secrets._bytes)。
  • 检查种子来源:如果容器化部署,是否每个容器有独立熵源?(建议挂载 --privileged 或使用 vTPM。)
  • PRNG 自动 reseed 间隔不应超过 1 小时。
  • 生成非对称密钥时,优先使用 OpenSSL 或 BoringSSL 的库,不要手写素数生成。
  • 测试环境与生产环境的随机数方案必须一致,不能测试用伪随机而生产用真随机(我曾经因此错过一个熵不足的 bug)。

最后说一句:密钥生成这件事,说白了就是「用真随机种子,喂给安全的算法,输出高熵的密钥」。听起来简单,但每个环节都能出幺蛾子。我建议你在系统部署前专门写一个随机数验证脚本,跑 10 万次采样做卡方检验,确认没有偏差。这一步花不了 10 分钟,但能省很多事。