🏠 总目录📚 本教程 04 · 伪随机
📑 本页目录(点开跳转)

04 · 伪随机

25 分钟 | ⭐ "看起来随机"这句话怎么变严格


🎯 一句话

我们要用一个短种子造出一长串"随机"比特来代替 OTP 的密钥。 但"看起来随机"这个说法太模糊了 —— 这一章把它变成一个精确的、可证伪的定义。 而且这个定义会颠覆你对"随机性测试"的直觉。


🎲 一、先说清楚:什么不算随机性的标准

   一个序列:
   0 1 1 0 1 0 0 1 1 0 1 0 0 1 1 0 ...

   它通过了:
   ✅ 0 和 1 的个数几乎相等
   ✅ 卡方检验
   ✅ 游程检验(连续相同位的分布正常)
   ✅ 频谱检验
   ✅ NIST 随机性测试全套

   ⭐ 但如果我告诉你:它是 LFSR 生成的,
     那么看到前 2L 位,就能预测出后面【所有】位(第 5 章)

   → 它在统计上完美,在密码学上一文不值

🔑 本章第一课「统计随机」和「密码学安全」是两件完全不同的事。

统计测试问的是"分布对不对";密码学问的是"有没有任何高效算法能占到便宜"。 前者是有限的一组检查,后者是对所有可能的攻击算法量化


📐 二、PRG 的定义:不可区分性

定义:函数 $G: \{0,1\}^s \to \{0,1\}^\ell$($\ell \gg s$,即"拉伸") 是一个安全的 PRG,如果对任意多项式时间的判别器 $D$:

$$\Big|\Pr[D(G(U_s)) = 1] - \Pr[D(U_\ell) = 1]\Big| \le \text{negl}(s)$$

💡 人话翻译

把「G 拉出来的串」和「真随机串」放在你面前,你分不出哪个是哪个。 不管你用什么方法(任何多项式时间算法),猜对的概率都不比抛硬币好多少。

   想象一个游戏:

   裁判抛硬币决定:
   ├─ 正面 → 给你 G(随机种子) 的输出
   └─ 反面 → 给你一串真随机比特

   你看完之后猜是哪种。

   ⭐ 如果你的正确率不能显著超过 50%,G 就是安全的

💡 这个定义为什么这么设计

设计选择 为什么
"对任意判别器 D" 不是列举有限种测试,而是覆盖所有可能的攻击
"多项式时间" 承认无限算力必然能破(穷举种子),只要求高效攻击者做不到
"negl 可忽略" 优势必须比任何多项式的倒数还小,不能是 1% 这种"很小"

💡 注意这个定义的形状它把"安全"变成了一个可证伪的命题 —— 只要有人拿出一个具体的高效判别器 D,方案就死了。 🔗 这个"用不可区分性定义安全"的模式会在第 8 章再次出现, 那是整套教程的思维转折点。


🔮 三、Yao 定理:不可区分 ⟺ 不可预测

有另一个看起来更直观的定义:

Next-bit 不可预测性:给定输出的前 i−1 位, 任何高效算法都无法以显著优于 1/2 的概率猜中第 i 位。

Yao 定理(1982)

$$\text{不可区分} \iff \text{next-bit 不可预测}$$

💡 为什么这个定理很有用"不可区分"难验证(要对抗所有判别器), "不可预测"好验证得多(只要证明预测下一位很难)。

有了 Yao 定理,你可以证简单的那个,白得难的那个。

📐 证明思路:混合论证(想看再点)

难的方向是「不可预测 ⟹ 不可区分」,用混合论证(hybrid argument)

构造 $\ell+1$ 个分布:$H_i$ = 前 i 位来自 G,后 $\ell-i$ 位是真随机。

若 D 能以优势 ε 区分 $H_0$ 和 $H_\ell$,则由三角不等式, 必存在某个 i 使 D 能以优势 ≥ ε/ℓ 区分相邻的 $H_{i-1}$ 和 $H_i$

而这两个分布只在第 i 位上不同(一个来自 G,一个真随机)—— 所以这个 D 可以直接改造成一个第 i 位的预测器。∎

💡 "混合论证"是密码学证明里最常用的工具: 把一个大差距拆成一串小差距,说明至少有一步的差距不小


🌱 四、种子从哪来:熵源

   ⚠️ PRG 再安全,种子可预测的话一切归零

   种子必须来自【真正的物理随机】:
   ├─ 硬件噪声(热噪声、振荡器抖动)
   ├─ CPU 指令 RDRAND / RDSEED
   ├─ 操作系统熵池:/dev/urandom、getrandom()、CryptGenRandom
   └─ 用户输入的时间抖动(鼠标、键盘)

该用什么,不该用什么

# ❌ 绝对不要:这是给模拟和游戏用的,可预测
import random
key = random.randbytes(32)          # 💀 Mersenne Twister,看到 624 个输出即可完全还原

# ✅ 正确
import secrets
key = secrets.token_bytes(32)       # ⭐ 走操作系统的 CSPRNG

import os
key = os.urandom(32)                # ⭐ 同样正确
语言 ✅ 该用 ❌ 别用
Python secretsos.urandom random
Java SecureRandom Random
JavaScript crypto.getRandomValues Math.random
Go crypto/rand math/rand
C/C++ getrandom() / BCryptGenRandom rand()

💥 真实事故 —— 三个都是同一个错误: - Debian OpenSSL(2006–2008):一行"清理未初始化内存"的改动, 让熵源只剩进程 ID → 可能的 SSH 密钥只剩 32768 个,两年后才被发现。 - Android Bitcoin 钱包(2013)SecureRandom 初始化有缺陷, 导致签名用的 k 值重复 → 私钥被算出来,钱被盗第 17 章会讲原理)。 - 各类嵌入式设备:开机时熵池是空的,生成的密钥高度重复 —— 有研究扫描全网发现成千上万台设备共用同一把 RSA 私钥

🔑 共同的教训随机数是密码系统里最安静的失效点。 它坏掉的时候什么都不会报错 —— 加密照常工作,只是不再安全。


🏗️ 五、实践中的 PRG 长什么样

   理论上:G 是一个抽象的拉伸函数

   工程上:几乎总是【用分组密码或哈希在 CTR 模式下跑】

   例:CTR_DRBG(NIST 标准)
       输出 = AES_K(1) ‖ AES_K(2) ‖ AES_K(3) ‖ ...
                    ↑ 计数器递增

   例:ChaCha20 的密钥流生成
       输出 = ChaCha20(K, nonce, 0) ‖ ChaCha20(K, nonce, 1) ‖ ...

🔗 第 7 章 CTR 模式讲的就是这个结构。 "把分组密码当 PRG 用"是现代对称加密的核心套路。

⚠️ 一个容易被忽略的性质:前向安全

   如果攻击者【某一刻】拿到了 PRG 的内部状态,
   他能不能算出【之前】的输出?

   ❌ 简单的 CTR 结构:能(状态就是 K 和计数器,往回倒即可)
   ✅ 带 reseed / 状态单向更新的设计:不能 ⭐

   → 这就是为什么 /dev/urandom 会周期性地混入新熵

🔗 和站内其他章的关系

相关的地方 和这一章的关系
第 3 章:OTP 密钥太长 PRG 就是为了解决它
第 3 章:计算安全的妥协 PRG 的定义就是这个妥协的形式化
第 5 章 LFSR 统计完美但密码学上失败的典型
第 8 章 IND-CPA 同一个"不可区分"模式 ⭐
Kaggle · 数据泄露与外部数据 对抗验证」训一个二分类器区分训练集和测试集——那就是本章定义里的判别器 D,AUC≈0.5 就是"不可区分"
模型上线之后 · 版本回溯与可复现 ⚠️ 同一个 random,两个相反的目标:那边固定种子是要"任何人都能复现",这里的要求恰恰是"任何人都不能预测"
AI 基础设施 · 训练稳定性与故障恢复 checkpoint 清单里那一行「随机数状态」——那边把 PRG 状态当资产存下来,在这里存下来就等于泄露密钥(本章的前向安全) ⭐

✅ 检查点

  1. 为什么"通过了 NIST 随机性测试"不代表密码学安全?
  2. PRG 的安全定义是什么?用人话说一遍。
  3. 这个定义为什么要说"对任意判别器"而不是列举几种测试?
  4. Yao 定理说了什么?它为什么有用?
  5. 混合论证的基本思路是什么?
  6. random.randbytessecrets.token_bytes 的区别?为什么前者致命?
  7. 举一个随机数失效导致的真实事故,说明它的教训。
  8. 什么是 PRG 的前向安全?
👀 答案
  1. 因为统计测试是有限的一组检查(分布对不对),而密码学要求的是没有任何高效算法能占到便宜。LFSR 输出能通过全套统计测试,但看到前 2L 位就能预测后面所有位。
  2. 任意多项式时间判别器 D,D 区分 G 的输出和真随机串的优势可忽略。人话:把 G 拉出来的串和真随机串放你面前,你分不出哪个是哪个
  3. 因为列举测试只能覆盖你想到的攻击;"对任意判别器"覆盖所有可能的攻击,并且让"安全"成为一个可证伪的命题——有人拿出一个具体的高效 D,方案就死了。
  4. 不可区分 ⟺ next-bit 不可预测。有用是因为不可区分难验证(要对抗所有判别器),不可预测好验证——证简单的那个,白得难的那个。
  5. 构造一串中间分布 H₀(全真随机)到 H_ℓ(全来自 G),相邻两个只差一位。若能以优势 ε 区分两端,则必有某相邻对能以 ε/ℓ 区分,而它可以直接改造成第 i 位的预测器。
  6. randomMersenne Twister,为模拟和游戏设计,看到 624 个输出就能完全还原内部状态secrets 走操作系统的 CSPRNG。
  7. Debian OpenSSL:熵源被误删只剩进程 ID,可能的 SSH 密钥只剩 32768 个,两年才发现。教训:随机数是最安静的失效点——坏了不报错,加密照常工作,只是不再安全。
  8. 攻击者拿到当前内部状态后,无法回推出之前的输出。简单 CTR 结构不满足(往回倒计数器即可),需要状态单向更新或周期性 reseed。

🛑 可以停在这里

走神救援

第一课:「统计随机」≠「密码学安全」——LFSR 输出能通过 NIST 全套统计测试,但看到前 2L 位就能预测后面所有位。统计测试是有限的一组检查,密码学要求没有任何高效算法能占便宜PRG 定义(不可区分性):对任意多项式时间判别器 D,区分 G(种子) 和真随机串的优势可忽略;人话:两串放你面前你分不出哪个是哪个。三个设计要点:"任意判别器"覆盖所有攻击(而非列举测试)、"多项式时间"承认无限算力必破、"negl"要求优势比任何多项式倒数还小。⭐这个定义让"安全"变成可证伪命题——拿出一个具体高效的 D 方案就死。Yao 定理:不可区分 ⟺ next-bit 不可预测——有用是因为不可区分难验证、不可预测好验证,证简单的白得难的;证明用混合论证(把大差距拆成一串小差距,必有一步差距不小)。种子必须来自真物理熵:⚠️ Python 用 secrets/os.urandom 绝不用 random(Mersenne Twister,624个输出即可还原);Java SecureRandom 不用 Random,JS crypto.getRandomValues 不用 Math.random。💥 三大事故同一根因:Debian OpenSSL(熵只剩进程ID,SSH密钥只剩32768种,两年才发现)、Android 比特币钱包(k 值重复导致私钥被算出)、嵌入式设备开机熵池为空(全网成千上万台共用同一把 RSA 私钥)→ ⭐随机数是密码系统里最安静的失效点,坏了不报错。工程上 PRG 几乎总是用分组密码在 CTR 模式下跑(CTR_DRBG、ChaCha20)。前向安全:拿到当前状态能否回推之前输出——简单 CTR 不行,需要状态单向更新或 reseed。

下一节 👉 05-流密码与Nonce铁律.md

打卡记录保存在你的浏览器里,首页能看到总进度