揭秘 /dev/urandom 的误区
多年来,开发者社区中一直流传着一个持久的误区:认为 /dev/urandom 是不安全的,而所有的加密用途都应该使用 /dev/random。这种误解通常源于对 Linux man pages 的草率阅读,导致开发者认为通过选择非阻塞设备是在牺牲安全性来换取速度。
实际上,两者之间的区别在很大程度上是可用性问题,而非安全性问题。通过理解 Linux 内核随机数生成的底层机制可以发现,对于几乎所有的实际用途,/dev/urandom 不仅足够,而且是首选。
核心误解:真随机 vs. 伪随机
许多开发者认为 /dev/random 提供的是“真”随机性(直接源自环境噪声),而 /dev/urandom 提供的是安全性较低的“伪随机”数。这是对 Linux 内核运作方式的根本误解。
计算安全性 vs. 信息论安全性
要理解为什么这种区别是一个误区,我们必须首先区分两种安全类型:
- 信息论安全性 (Information-Theoretic Security): 这是“金标准”,即拥有无限计算能力的对手也无法破解加密。一次一密 (One-time pad) 就是这里的主要例子。由于其极度的不切实际性,这在实际软件工程中几乎从不被使用。
- 计算安全性 (Computational Security): 这是 AES、RSA 和椭圆曲线加密 (Elliptic Curve cryptography) 所使用的安全级别。它假设虽然可能存在某种解法,但即使动用世界上所有的计算机,花费的时间也会超过宇宙的寿命才能找到它。
由于几乎所有的加密库 (OpenSSL, GnuTLS) 和算法 (AES, RSA) 提供的都只是计算安全性,因此,在为种子 (seed) 坚持要求“真”随机性的同时,却使用计算安全的算法进行加密,这在逻辑上是不自洽。如果底层的分组密码或哈希函数被破解了,那么随机数的来源将是你最不需要担心的问题。
Linux RNG 实际上是如何工作的
有一种普遍的观点认为 /dev/random 直接从熵池中提取数据,并在熵池为空时发生阻塞,而 /dev/urandom 则会切换到质量较低的 PRNG。这是错误的。
CSPRNG 架构
/dev/random 和 /dev/urandom 都是由同一个密码学安全伪随机数生成器 (CSPRNG) 提供的。这两个设备提供的每一个随机比特位都是通过这个 CSPRNG 处理的。
- 在 Linux 4.8 之前: 两个设备使用相同的内部池和 CSPRNG。唯一的区别在于,如果内核对可用熵的估计值低于某个阈值,
/dev/random就会发生阻塞。 - 从 Linux 4.8 开始: 架构发生了演进,但基本原则保持不变:
/dev/urandom提供的是由熵池进行种子的 CSPRNG 输出。
至关重要的是,“熵计数”是一个估计值,而不是精确的测量。因为这个估计值可能是保守的或不准确的,所以 /dev/random 的阻塞并不真正保证“更多”的安全性;它只保证了系统会等待。
阻塞的危险
为了所谓的安全性而选择 /dev/random 会引入一个重大的风险:可用性。
当 /dev/random 发生阻塞时,你的应用程序就会停止。这可能导致生产环境中的灾难性故障,例如 Web 服务器在等待临时会话密钥时挂起。正如一位开发者在社区讨论中提到的:
"偶尔页面加载会阻塞数秒……我发现了一个阻塞的进程,果然它是在读取 /dev/random 时被阻塞了。"
除了技术故障外,阻塞还会诱发危险的权宜之计。当开发者遇到系统无法启动或服务器挂起时,他们可能会倾向于通过补丁去掉 random() 调用,或者干脆禁用 SSL,仅仅为了让系统运行起来,这比使用 /dev/urandom 带来的安全漏洞要大得多。
应对“熵不足”的恐惧
支持 /dev/random 最常见的论点之一是,当熵“耗尽”时,/dev/urandom 会变得不安全。这是一个稻草人谬误。
在密码学中,一旦 CSPRNG 使用足够的熵(通常是 256 位)进行了种子初始化,它就可以在无限的时间内产生计算安全的随机数流。内核执行的持续重新种子化 (re-seeding) 并不是因为生成器“耗尽”了随机性,而是为了提供“自愈”特性。如果攻击者以某种方式破坏了 RNG 的内部状态,注入新鲜的熵最终会使状态重新变得不可预测。
最终结论:使用 /dev/urandom
虽然 man pages 建议 /dev/random 应该用于长寿命密钥(如 GPG 或 SSH 密钥),但大多数密码学家认为这是一种不必要的预防措施。正如 Daniel Bernstein (djb) 所指出的,如果我们在无法确定性地将一个 256 位的种子扩展为无穷无尽的不可预测密钥流的同时,又相信我们可以使用单个密钥来安全地加密许多消息,这本身就是一个矛盾。
最佳实践总结
- 对于几乎所有的用例: 使用
/dev/urandom。它是计算安全的,且不会阻塞你的应用程序。 - 在系统启动时: 请注意,Linux 的
/dev/urandom可能在内核收集到足够的初始熵之前就提供数字。大多数现代发行版通过种子文件来缓解这个问题,但在虚拟化环境(如克隆的 VM)中,请确保在克隆或恢复检查点之后正确地进行 RNG 种子初始化。 - 避免使用 /dev/random 除非你正在实现一种信息论安全的算法(你几乎肯定不是)。