4. 真随机数发生器(TRNG):原理、熵源设计与随机性测试

真随机数发生器,圈里人习惯叫 TRNG。它跟伪随机数发生器(PRNG)最大的区别在哪?说白了,TRNG 的随机性来源于物理过程——热噪声、抖动、亚稳态这些不可预测的物理现象。而 PRNG 再厉害,本质也是个确定性的算法,种子一旦泄露,整个序列就完了。

我在安全芯片项目里吃过 TRNG 的亏。第一次流片回来,随机数测试 FIPS 140-2 死活过不了,查了两个月才发现是熵源电路的电源噪声被同步了。嗯,从那以后我对 TRNG 的设计就格外较真。

4.1 TRNG 的核心架构

一个完整的 TRNG,通常由三部分组成:

  • 熵源(Entropy Source):产生原始随机信号,是 TRNG 的根基
  • 数字化电路(Digitizer):把模拟的随机信号转成 0/1 比特流
  • 后处理(Post-Processing):消除偏差、提取纯随机性

你想想看,如果熵源本身不够随机,后面做再多处理也是白搭。所以我个人习惯把 70% 的精力放在熵源设计上。

核心原则:TRNG 的安全性不取决于算法复杂度,而取决于物理熵源的质量。一个简单的环形振荡器 TRNG,只要熵源设计得当,比一个花哨的量子 TRNG 更可靠。

TRNG 核心架构 熵源 热噪声 / 抖动 / 亚稳态 数字化电路 采样 / 比较 / 量化 后处理 去偏 / 提取 / 加密 随机数输出 熵源质量决定 TRNG 安全性上限 后处理只能消除偏差,不能增加熵

4.2 常见熵源设计

我见过不下十种熵源方案,真正能在量产中稳定工作的其实就几种。下面是我个人比较推荐的方案对比:

熵源类型 基本原理 速率 抗攻击能力 面积开销
环形振荡器 振荡器相位抖动采样 中(~Mbps) 中等 小
亚稳态采样 触发器亚稳态判决 低(~kbps) 高 极小
热噪声放大 电阻热噪声 + 放大器 高(~10Mbps) 高 大
量子噪声 光子散粒噪声 极高 极高 极大(需外部器件)

我的建议:对于大多数安全 SoC,环形振荡器 + 亚稳态混合方案是最务实的。纯环形振荡器容易被电源噪声注入攻击,纯亚稳态速率太低。两者结合,既保证速率又提升抗攻击能力。

4.3 环形振荡器 TRNG 详解

这是最经典的 TRNG 结构。原理其实不复杂:

  1. 用奇数个反相器构成环形振荡器,输出一个方波信号
  2. 这个方波的周期不是固定的——受热噪声和闪烁噪声影响,每个周期都有微小的抖动
  3. 用另一个低频时钟去采样这个抖动信号
  4. 采到的值就是随机比特

我曾经犯过一个低级错误:把振荡器的电源跟数字核心共用。结果数字电路一跑起来,振荡器的抖动被电源噪声同步了,随机性大幅下降。后来我强制要求 TRNG 用独立的 LDO 供电,问题才解决。

避坑指南:环形振荡器 TRNG 最怕「确定性抖动」。如果振荡器的抖动主要来自电源噪声或衬底耦合,而不是热噪声,那输出的随机性就是假的。我曾经在测试中遇到过——看起来随机数测试能过,但换到不同工艺角就挂了。后来发现是振荡器结构对工艺太敏感。

4.4 亚稳态 TRNG

这个方案很有意思。它利用的是触发器的亚稳态特性——当建立时间或保持时间不满足时,触发器输出会进入一个不确定状态,最终稳定到 0 或 1 的概率各占 50%。

我参与过一个项目,用亚稳态 TRNG 做密钥生成。设计很简单:两个反相器交叉耦合,再加一个使能信号。面积只有几个门,但随机性非常好。不过要注意:

  • 亚稳态的解析时间需要精确控制,太短会偏向 0 或 1
  • 温度变化会影响亚稳态窗口,需要做温度补偿
  • 老化工况下,器件失配会引入偏差

关键参数:亚稳态 TRNG 的「解析时间窗口」通常需要控制在 50-200ps 之间。太窄了采样不到亚稳态,太宽了输出偏向严重。我习惯在仿真时跑 10000 次 Monte Carlo,确保 3-sigma 范围内偏差小于 1%。

4.5 后处理技术

熵源出来的原始比特流,通常不是完美的。常见的问题有:

  • 偏差(Bias):0 和 1 的比例不是 50:50
  • 相关性(Correlation):相邻比特之间存在依赖
  • 周期性(Periodicity):存在重复模式

后处理就是用来解决这些问题的。我常用的方案:

后处理方法 原理 适用场景 吞吐损失
Von Neumann 去偏 丢弃 00/11,保留 01/10 偏差大但无相关性 ~75%
XOR 压缩 多比特异或 相关性较强 ~50%
LFSR 后处理 线性反馈移位寄存器 通用场景 ~30%
加密哈希 SHA-256 / AES 压缩 高安全场景 ~90%

我的经验:不要一上来就用加密哈希做后处理。先看看熵源质量——如果偏差在 5% 以内,一个简单的 Von Neumann 去偏就够了。哈希虽然安全,但吞吐损失太大,而且会引入额外的功耗和面积。我一般只在 FIPS 140-2 Level 3 以上的场景才用哈希后处理。

4.6 随机性测试

设计完 TRNG,怎么验证它真的随机?光靠仿真不够,必须上标准测试。我常用的测试套件:

  • NIST SP 800-22:15 项统计测试,行业标准
  • FIPS 140-2:4 项基本测试,适合快速筛查
  • Dieharder:更严格的测试集,适合深度验证
  • TestU01:学术圈常用,覆盖更全面

我个人习惯的测试流程:

  1. 在线测试:芯片运行时实时监测,发现异常立即报警
  2. 离线测试:采集 1M 比特以上数据,跑 NIST 全套测试
  3. 工况测试:在高温、低温、高电压、低电压下重复测试
  4. 老化测试:加速老化后再次测试,确保寿命期内稳定

重要提醒:随机性测试通过 ≠ TRNG 安全。我见过一个案例——TRNG 在正常工况下所有测试都过了,但攻击者用电磁注入干扰后,输出变成了可预测的序列。所以除了统计测试,还要做「主动攻击测试」:尝试用电源噪声、时钟毛刺、激光照射等手段攻击 TRNG,看它是否还能保持随机性。

4.7 一个完整的 TRNG 设计示例

下面是我在一个 IoT 安全芯片中用过的 TRNG 代码片段。它用环形振荡器做熵源,加上 Von Neumann 去偏:

// 环形振荡器 TRNG 核心
module trng_core (
    input  wire        clk,      // 采样时钟
    input  wire        rst_n,    // 复位
    input  wire        enable,   // 使能
    output wire [7:0]  rnd_data, // 随机字节
    output wire        valid     // 数据有效
);

    // 环形振荡器:3级反相器
    wire osc_out;
    ring_osc #(.STAGES(3)) u_osc (
        .enable(enable),
        .osc_out(osc_out)
    );

    // 亚稳态采样
    reg [1:0] sample_ff;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sample_ff <= 2'b0;
        end else begin
            sample_ff <= {sample_ff[0], osc_out};
        end
    end

    // Von Neumann 去偏
    wire raw_bit = sample_ff[1];
    reg  prev_bit;
    reg  valid_bit;
    reg  [7:0] shift_reg;
    reg  [2:0] bit_cnt;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            prev_bit <= 1'b0;
            valid_bit <= 1'b0;
            shift_reg <= 8'b0;
            bit_cnt   <= 3'b0;
        end else begin
            // Von Neumann: 01 -> 0, 10 -> 1, 00/11 -> 丢弃
            if (raw_bit != prev_bit) begin
                valid_bit <= 1'b1;
                shift_reg <= {shift_reg[6:0], raw_bit};
                bit_cnt   <= bit_cnt + 1'b1;
            end else begin
                valid_bit <= 1'b0;
            end
            prev_bit <= raw_bit;
        end
    end

    assign rnd_data = shift_reg;
    assign valid    = (bit_cnt == 3'b111) & valid_bit;

endmodule

设计要点:这个代码里我用了两级采样(sample_ff)来避免亚稳态传播。Von Neumann 去偏的逻辑很简单——只保留相邻比特不同的情况。这样即使原始比特有 60:40 的偏差,输出也能恢复到接近 50:50。实测下来,这个设计在 0.18um 工艺下能跑到 2Mbps,面积只有 0.01mm²。

4.8 常见问题与调试方法

做 TRNG 设计,总会遇到各种奇怪的问题。我整理了几个高频问题:

现象 可能原因 排查方法
NIST 测试全挂 熵源未工作 / 采样时钟同步 用示波器看振荡器波形,检查是否有周期性
偏差 > 10% 器件失配 / 温度影响 跑 Monte Carlo 仿真,检查工艺角
高温下失效 振荡器频率漂移 加温度补偿电路,或改用温度不敏感结构
攻击后输出可预测 缺少主动检测机制 加电源毛刺检测、频率监测

调试技巧:我习惯在 TRNG 输出端加一个「健康监测」模块——实时统计 0 和 1 的计数,如果连续 1024 个比特中偏差超过 15%,就触发告警并重新初始化熵源。这个模块面积很小,但能避免很多线上问题。

嗯,TRNG 设计就是这样——看起来简单,但坑很多。核心就一句话:尊重物理,敬畏随机。熵源是根,后处理是叶,测试是眼。根扎稳了,叶子自然茂盛。