密钥生成策略:随机数生成器、熵源、生成算法选择
密钥生成的本质,就是让系统“猜不出”的随机比特流。你可能觉得这很简单——不就是调个 rand() 吗?
我早年接手过一个硬件钱包项目,团队直接用 libc 的 rand() 生成 ECDSA 私钥。结果呢?测试时发现每次设备上电后生成的密钥序列都一模一样。嗯,这就是典型的“伪随机数当真随机用”的翻车案例。
这一章我们就把密钥生成策略拆成三块:随机数生成器、熵源、生成算法。这三者的关系,你可以想象一个流水线:熵源是原材料,生成器是加工车间,算法是质检标准。任何一个环节出问题,最终密钥都是脆弱的。
随机数生成器的类型与选择
随机数生成器分两大类:真随机数生成器 (TRNG) 和 伪随机数生成器 (PRNG)。
TRNG 依赖物理过程——比如电阻热噪声、时钟抖动、量子效应。它输出的是“真随机”,但速度慢,而且硬件成本高。我在做金融级 HSM 项目时,芯片里集成的 TRNG 模块每秒钟只能产出几十 KB 的随机数,完全不够对称密钥批量生成用的。
PRNG 呢?它用确定性的算法(比如 HMAC_DRBG、CTR_DRBG)从一段种子密钥扩展出无限长的伪随机序列。速度快,但一旦种子泄漏或者状态被逆向,整个序列就废了。
| 特性 | TRNG | PRNG |
|---|---|---|
| 熵来源 | 物理噪声 | 种子(来自 TRNG 或前一轮状态) |
| 输出速度 | 慢(~KB/s 级别) | 快(~MB/s 甚至 GB/s) |
| 可预测性 | 理论上不可预测 | 种子未知时不可预测 |
| 典型场景 | 种子生成、一次性密钥、CA 根密钥 | 会话密钥、对称密钥批量生成 |
实用原则:永远不要用 TRNG 直接生成大量对称密钥。你应该用 TRNG 产生一个高熵种子(比如 256 位),然后交给 PRNG 扩展。很多 SDK 内部就是这么做的——NIST SP 800-90A 标准化的 DRBG 机制就是最佳实践。
熵源的重要性与评估
说白了,熵源就是密钥生成的“米”。米不好,再好的厨子也做不出好饭。
我见过最离谱的案例:某物联网芯片为了省成本,直接用芯片内部的一个自由运行计数器的低几位作为熵源。结果因为每次启动时上电时序几乎一致,计数器初值也差不多,生成的密钥只有两种变体。嗯,那批设备基本等于没有加密。
评估熵源质量,我习惯看三个指标:
- 熵率(bits/bit):每个比特携带多少真随机信息。理想值接近 1.0。
- 输出不可预测性:即使攻击者知道之前的所有输出,也无法推断下一比特。
- 稳定性:温度、电压、老化等环境下,熵率不会剧烈下降。
如果你在 Linux 上开发,/dev/random 和 /dev/urandom 就是典型的熵源接口。前者会阻塞等待内核熵池充足,后者永不阻塞但可能用残留熵。我个人的建议:密钥种子务必从 /dev/random 获取,非关键随机数可以用 /dev/urandom。
小技巧:在容器或虚拟化环境中,内核熵池经常被耗尽。我曾经遇到 K8s 中 Pod 启动后 /dev/random 一直阻塞的坑。解决方案是用 haveged 或 rng-tools 注入硬件熵,或者从宿主机的 /dev/urandom 读取种子。
生成算法与标准化
算法选择这块,大部分现代加密 SDK 已经帮你封装好了,但你要理解它背后的逻辑。
主流标准是 NIST SP 800-90A 定义的三种 DRBG:
- HMAC_DRBG:基于 HMAC,安全性强但计算开销略大。适合对安全极度敏感的场景。
- CTR_DRBG:基于分组密码 (如 AES-256),速度快。大多数 SDK 的默认选项。
- Hash_DRBG:基于哈希函数,现在用得少了,因为它对状态管理的容错性不如前两者。
另外,RFC 6979 定义了一种确定性 ECDSA 签名 与密钥生成无关 吗?等等——这里容易混淆。RFC 6979 解决的是签名时 k 值随机性问题,不是密钥生成。但它的思想可以借鉴:用 HMAC 结合私钥和消息摘要产生确定性随机数,避免 RNG 故障。
// OpenSSL 生成 ECC 密钥对示例(底层使用了 DRBG):
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL);
EVP_PKEY_keygen_init(ctx);
EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, NID_X9_62_prime256v1);
EVP_PKEY *pkey = NULL;
EVP_PKEY_keygen(ctx, &pkey);
// 密钥自动由 SDK 内部 DRBG 生成,种子来自 OS 熵源
踩坑回忆:我曾经在某国产芯片上调用 OpenSSL 生成的 RSA 密钥,发现每次生成的素数因子竟然相同——因为该芯片的硬件 TRNG 模块被关闭了,但 RAND_bytes() 并未报错。从那以后,我强制要求所有 SDK 密钥生成后做一次已知密钥测试:RAND_status() 加上检验函数。
密钥生成参数对比
| 算法场景 | 推荐生成器 | 最少安全强度 | 常用熵源 |
|---|---|---|---|
| AES-256 对称密钥 | CTR_DRBG (AES-256) | 256 位 | /dev/random 或 TRNG 种子 |
| RSA-4096 私钥 | HMAC_DRBG (SHA-512) | 4096 位 | 硬件 TRNG + /dev/random |
| ECDSA P-384 | CTR_DRBG (AES-256) | 384 位 | /dev/urandom + 额外熵混合 |
| 一次性会话密钥 | PRNG (快速模式) | 128 位 | /dev/urandom |
核心总结:密钥生成不是“调一个 API”那么简单。你需要想清楚三件事——熵源可信吗?生成算法经过 NIST 认证吗?种子更新机制到位了吗?把这三根柱子立稳,密钥的安全根基才算夯实。