第11章 安全随机数生成:/dev/urandom与OpenSSL随机数、C语言实现安全种子
随机数这东西,看着简单,坑却不少。
我早年做嵌入式安全模块时,就吃过随机数的亏。产品上线后,某次安全审计发现我们的密钥生成用了rand()——嗯,标准库那个伪随机函数。审计报告直接给了个「严重」级别漏洞。从那以后,我对随机数的态度就变了:普通随机数用来做游戏可以,用来做加密,那是找死。
11.1 为什么不能用 rand() 做加密?
先说说rand()的问题。它是个线性同余生成器,说白了就是:
next = (current * A + C) % M
一旦你知道了当前值,下一个值就能算出来。我在项目中见过有人用srand(time(NULL))做种子,然后生成密钥——你想想看,时间戳的范围是有限的,暴力枚举几万种可能,密钥就破了。
rand()、random() 或任何非密码学安全的伪随机数生成器。
11.2 真正的安全随机数从哪里来?
安全随机数的核心,是熵。熵来自物理世界的不可预测事件:鼠标移动、键盘敲击间隔、磁盘读写时间、硬件噪声……操作系统把这些收集起来,存到一个熵池里。
Linux 下有两个接口:
- /dev/random:阻塞式。熵不够时就等着,直到收集到足够的熵才返回。适合生成长期密钥(比如 GPG 密钥对)。
- /dev/urandom:非阻塞式。熵不够时用内部算法扩展,永远不阻塞。适合大多数场景,包括 TLS 会话密钥、IV 向量等。
我个人习惯:99% 的场景用 /dev/urandom。为什么?因为 /dev/random 在熵耗尽时会卡住,服务器在高并发下可能因此 hang 住。我曾经在生产环境遇到过,一个 HTTPS 服务因为 /dev/random 阻塞,握手延迟飙到十几秒——那场面,真是惨不忍睹。
11.3 C 语言读取 /dev/urandom
直接上代码。这是我最常用的封装:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
/**
* 从 /dev/urandom 读取安全随机字节
* 返回 0 成功,-1 失败
*/
int secure_random_bytes(void *buf, size_t len) {
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0) {
return -1;
}
size_t total_read = 0;
ssize_t n;
while (total_read < len) {
n = read(fd, (char*)buf + total_read, len - total_read);
if (n <= 0) {
// 这里要注意:EINTR 表示被信号中断,需要重试
if (errno == EINTR) {
continue;
}
close(fd);
return -1;
}
total_read += n;
}
close(fd);
return 0;
}
// 使用示例:生成 32 字节的密钥
int main() {
unsigned char key[32];
if (secure_random_bytes(key, sizeof(key)) == 0) {
printf("安全密钥生成成功\n");
// 这里可以继续使用 key
} else {
fprintf(stderr, "随机数生成失败\n");
}
return 0;
}
注意几个细节:
- 循环读取:read() 可能不会一次读完所有字节,必须循环直到读满。
- 处理 EINTR:信号中断时 read 返回 -1,errno 为 EINTR,这时要重试,不能直接报错。
- 关闭文件描述符:用完就关,别泄漏。
11.4 OpenSSL 的随机数接口
如果你项目里已经用了 OpenSSL,那就直接用它的随机数函数,省事又安全。
#include <openssl/rand.h>
// 生成 16 字节随机数
unsigned char iv[16];
if (RAND_bytes(iv, sizeof(iv)) != 1) {
// 失败处理
fprintf(stderr, "RAND_bytes 失败\n");
unsigned long err = ERR_get_error();
// 可以打印错误信息
}
// 生成 32 字节密钥
unsigned char key[32];
if (RAND_priv_bytes(key, sizeof(key)) != 1) {
fprintf(stderr, "RAND_priv_bytes 失败\n");
}
这里有两个函数:
| 函数 | 用途 | 说明 |
|---|---|---|
RAND_bytes() |
通用安全随机数 | 适合 IV、nonce、会话密钥等 |
RAND_priv_bytes() |
私钥相关随机数 | 更安全,但更慢,适合长期私钥 |
我个人建议:生成会话密钥、IV 用 RAND_bytes;生成 RSA/DSA 私钥用 RAND_priv_bytes。别搞反了,RAND_priv_bytes 内部会做额外的清理和隔离,性能开销大一些。
11.5 安全种子:如何给随机数生成器播种?
有些场景下,你需要自己管理随机数生成器的种子。比如在嵌入式系统里,可能没有 /dev/urandom,或者 OpenSSL 还没初始化好。
OpenSSL 的种子接口是这样的:
#include <openssl/rand.h>
// 添加熵到 OpenSSL 的随机数池
unsigned char entropy[] = {
0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f,
// ... 至少 32 字节的熵
};
RAND_add(entropy, sizeof(entropy), 4.0);
// 第三个参数是熵的估计值(单位:字节),这里估计有 4 字节的熵
// 检查是否已经足够安全
if (RAND_status() != 1) {
fprintf(stderr, "随机数状态不安全\n");
// 需要继续添加熵
}
这里有个容易踩的坑:熵估计值不要虚报。如果你从同一个时间戳取了 32 字节,实际熵可能只有 2-3 比特,但你报了 32,OpenSSL 会以为已经足够安全了——实际上你的密钥可能只有几十种可能性。
11.6 知识体系:安全随机数生成全景
下面这张图,是我自己总结的安全随机数选型逻辑:
11.7 实战建议与避坑总结
最后,把我这些年踩过的坑和总结的经验列一下:
- 永远不要自己写随机数生成器。密码学随机数生成器需要对抗各种攻击,自己写的几乎一定有漏洞。
- 种子来源要多样化。如果必须手动播种,混合多个来源:时间戳(纳秒级)、/dev/urandom 读取、硬件传感器噪声、网络延迟抖动等。
- 生成密钥后立即清零种子。种子用完后用
memset()配合volatile或explicit_bzero()清掉,防止被内存 dump 出来。 - 检查返回值。不管是
RAND_bytes还是read(),一定要检查返回值。我曾经见过代码里RAND_bytes返回 0 但程序继续往下跑,结果密钥全是 0。 - 测试随机性。可以用
dieharder或TestU01工具测试你的随机数质量。虽然 /dev/urandom 和 OpenSSL 的随机数通常没问题,但如果你在特殊硬件上(比如某些 ARM 开发板),最好验证一下。
一句话总结: 加密场景用 RAND_bytes() 或 /dev/urandom,非加密场景才用 rand()。种子要多样,用完要清,返回值要检查。
嗯,安全随机数这块,说难不难,说简单也不简单。关键是要养成习惯——每次写随机数代码时,先问自己一句:这个随机数被攻击者猜到了会怎样?如果答案是「会出事」,那就老老实实用安全接口。
公众号:蓝海资料掘金营,微信 deep3321