4. 密钥生成安全:别在第一步就埋下炸弹

密钥生成是整个密码体系的第一道门。门要是没装好,后面再强的算法、再安全的存储都白搭。我见过太多项目,密钥生成时随手一写,结果安全审计直接亮红灯。

说白了,随机数质量决定了密钥的不可预测性。你想想看,如果攻击者能猜到你的密钥是怎么生成的,那加密就形同虚设。

4.1 随机数来源:用对熵池,别用rand()

很多人图省事,直接用C语言的rand()或者Java的Math.random()来生成密钥。这是天大的坑。rand()是伪随机数生成器,种子固定的话序列可复现。我曾在一个嵌入式项目中看到有人用srand(time(NULL))生成AES密钥,结果时间戳只有秒级精度,暴力破解的范围瞬间缩小到几十万种组合。

正确的做法是什么?

  • 操作系统级熵源:Linux的/dev/urandom(非阻塞,够用)、/dev/random(阻塞,真随机但慢);Windows的CryptGenRandom。
  • 专用硬件:TEE、HSM内置的TRNG(真随机数发生器)。
  • 混合方案:熵池 + DRBG(确定性随机数生成器),如NIST SP 800-90A/B/C推荐的CTR_DRBG、Hash_DRBG。

曾经踩过的坑:我在一个物联网模块上用了芯片内部的ADC噪声作为熵源,但ADC采样模式固定,噪声分布居然是有规律的周期性抖动。攻击者用几千次采样就重建了熵模型。从那以后我坚持只用经过认证的硬件TRNG或操作系统级接口。

4.2 密钥生成流程:五步到位

我个人习惯把密钥生成拆成五个阶段,每一步都做检查。表格里列清楚了:

步骤 操作 常见错误
1. 熵收集 从至少两个独立熵源混合 只用一个弱熵源
2. 去偏处理 使用Von Neumann去偏器或Hash提取 直接使用原始随机比特
3. 生成密钥种子 通过PBKDF2/Argon2等KDF拉伸熵 不拉伸,直接截取
4. 派生目标密钥 使用HKDF或标准派生函数 复用种子直接作为密钥
5. 质量验证 做重复检查、统计测试 跳过验证

核心原则:密钥生成器必须是不可预测且不可重复的。给同一输入,永远不能产生相同密钥。

4.3 伪随机数生成器的正确用法

就算用上了好的熵源,如果伪随机数生成器(PRNG)实现不对,照样出事。我建议遵循这几个要点:

  • 必须加密安全:只用CSPRNG(如getrandom()、BCryptGenRandom),别用普通PRNG。
  • 种子一次性:每次启动进程或虚拟机都重新播种,别把种子持久化到文件。
  • 定期重播种:长时间运行的服务器应该每隔一段时间或每生成一定量的随机数后,从熵源重新播种。
  • 避免fork后重复:进程fork后,子进程如果立即读取相同PRNG状态,会得到相同序列。一定要在fork后重新播种或使用独立的PRNG实例。

小技巧:在Linux上,我习惯用getrandom()系统调用,如果它返回-1且错误码是EINTR,就重试。这个函数会自动混合熵池,比从/dev/urandom读更安全。

4.4 密钥生成中的典型陷阱

  1. 用时间/进程ID作为随机种子——太容易被猜到。
  2. 单链式随机:只用rand() % 256生成字节,分布极不均匀。
  3. 复用计数器作为密钥:有些人为了省事,把序号直接当密钥,那等于没加密。
  4. 在虚拟化环境中取熵:我遇到过虚拟机重启后熵池被重置,导致前后密钥相同的情况。必须在VM内额外混入物理熵源。
  5. 忽略后量子安全:虽然现在还没到PQC时代,但不妨开始用双熵源(经典 + 量子安全随机数),为未来迁移留余地。

真实案例:某个支付终端固件升级后,所有新设备的RSA密钥都是一样的。原因很简单——熵源初始化代码在boot loader阶段被砍掉了,生成器只用了一个硬编码种子。结果整个批次可以互相解密。我当时做渗透测试时发现的,厂商差点把我拉黑。

4.5 知识体系:密钥生成安全全景

下面这张图把本章核心知识点串起来,你看一眼就能记住。

密钥生成安全全景 密钥生成安全 熵源收集 去偏 / KDF拉伸 派生目标密钥 质量验证 弱种子(时间/PID) fork后重复序列 单熵源依赖 跳过统计测试 核心:不可预测 + 不可重复 → 高质量密钥

4.6 实操建议:三行代码保平安

如果你用的是现代语言的标准库,其实不需要自己造轮子。下面三个例子都是经过安全审计的:

// C (Linux):从 /dev/urandom 读32字节
int fd = open("/dev/urandom", O_RDONLY);
uint8_t key[32];
if (read(fd, key, sizeof(key)) != sizeof(key)) { /* 错误处理 */ }
close(fd);

# Python:使用 secrets 模块
import secrets
key = secrets._bytes(32)

// Java:使用 SecureRandom
SecureRandom sr = new SecureRandom();
byte[] key = new byte[32];
sr.nextBytes(key);

一定要检查read的返回值!我见过有人没检查,结果读取了0字节,密钥全是零。那叫一个尴尬。

4.7 质量验证:别偷懒

生成完密钥后,至少做三件事:

  • 全零检查:密钥不能是全0或全1,也不该有固定模式。
  • 随机性测试:局部的比特分布是否均匀?用NIST SP 800-22里的简单频次测试就能筛掉很多问题。
  • 去重检查:如果系统生成大量密钥(比如IoT设备批量产线),必须确保没有重复。我在生产线加过一个简单的布隆过滤器,命中率控制得很好。

嗯,密钥生成这件事说大不大,说小不小。但每次出安全事故,十有八九都能追溯到最开始那几步。把本章的原则刻进你的代码习惯里,后面的路就好走了。