从物理熵源到随机性检测:TRNG真随机数生成器实战解析
2026/9/22 10:18:11 网站建设 项目流程

“随机数”这个词,在工程圈里藏着不少坑。很多人觉得在代码里调用Math.random()rand()拿到的东西就是随机数,实际上你拿到的往往是确定性算法生成的伪随机序列。真正的随机,必须靠物理世界来提供。True Random Number Generation(TRNG)要解决的核心问题,就是从物理噪声里提取不可预测的熵,而不是让数学公式去“模拟”随机。这篇博文我结合自己从零搭建TRNG的实操经历,把核心概念、硬件方案、检测方法和日常踩坑一次性讲清楚。适合做嵌入式、安全开发,以及所有对“随机数从哪来”这个问题好奇的朋友。

先说结论:TRNG不是魔法,它只是把环境里的随机物理量变成比特。热噪声、半导体PN结击穿、时钟抖动,这些不起眼的物理现象,在合适的电路设计下,都能变成源源不断的随机源。下面我从最常见的问题切入——为什么伪随机数不够用。

1. 从伪随机到真随机:TRNG核心概念

1.1 伪随机数为什么会“不够真”

伪随机数生成器(PRNG)本质是一个确定性算法。无论是早年的线性同余发生器,还是现在常见的梅森旋转、ChaCha20,它们都有一个共同点:给定相同的种子(seed),一定产生相同的序列。这意味着伪随机数的“随机性”完全被种子限定死了。

举个容易理解的例子:你掷一枚硬币,结果是正面还是反面,取决于硬币的物理运动过程,没人能提前算出来。但伪随机数更像是“复印机”,它把一份随机样本无限复制、变形。如果攻击者知道了你的种子,或者种子空间太小可以被穷举,那整个随机序列就等于透明的。早年间一些系统用系统时间当种子,结果密钥被爆破,就是因为种子可预测。

这不是说PRNG没用,在高吞吐、可复现性要求高的场景(比如蒙特卡洛模拟调试)它非常合适。但凡是涉及安全、涉及不可预测性的地方——密钥生成、nonce、抽奖、防作弊——只用PRNG是不够的,因为种子的熵质量决定了整个系统的安全上限。这正是TRNG登场的原因。

1.2 TRNG的熵源:热噪声、雪崩击穿与时钟抖动

TRNG的“真”来自物理层的随机源。和我们直觉相反,宏观世界里“无规律”往往源于微观过程。最常见的熵源有以下几种:

  • 半导体热噪声:导体内部载流子随机运动产生的微小电压波动,特点是带宽宽、幅度低。
  • PN结雪崩击穿噪声:将PN结反向偏置到接近击穿电压,载流子像雪崩一样倍增,输出幅度较大的白噪声。这是业余自制TRNG最常用的方案。
  • 振荡器相位抖动:晶体振荡器或环形振荡器的每个周期起始点存在微小抖动,用高频振荡器去采样低频信号,就能得到随机比特。
  • 放射性衰变或量子效应:这些属于“硬核”熵源,专业级设备使用,业余很少碰。

这些物理过程天然不可预测,不依赖任何算法,所以只要熵源本身没有被外部影响控制,输出就是真正随机的。关键是,我们要通过电路把这种微观噪声“放大”到可观测的电压范围,再交给模数转换器采样。

1.3 真实场景里TRNG和PRNG如何配合

很多人以为TRNG出现以后PRNG就该被淘汰,其实不是。实际工程里最常见的架构是“熵种子 + CSPRNG”:TRNG负责产出少量高质量随机种子,内核的密码学安全伪随机数生成器(比如Linux的getrandom、Windows的BCryptGenRandom)再用这个种子生成大量随机字节。

为什么绕这一圈?因为硬件TRNG的熵率往往不高。一个自制TRNG每秒能产生几千比特高质量的随机位就算不错,但上层应用可能每秒需要几MB随机数据。用TRNG种子去“喂”CSPRNG,既能大幅提高随机流吞吐,又能保证随机性源头不可预测。Intel CPU里的RDRAND/RDSEED其实就是这个思路,RDSEED直接给种子,RDRAND内部再用CSPRNG扩展。

所以你要明白:TRNG和PRNG不是二选一,而是合作。理解这一点,后面做方案选型时思路就清楚了。

2. TRNG硬件方案选型解析

2.1 CPU内置硬件RNG:现成的TRNG模块

如果你不想自己焊电路,最省事的TRNG其实就在你口袋里。现代CPU和SoC普遍内置了硬件真随机数模块。

Intel/AMD桌面处理器有RDRAND和RDSEED指令。RDRAND通过rand指令直接从CPU硬件RNG读取随机数,内部混合了多种熵源并做了后处理;RDSEED则更“原始”,输出的是从熵源直接提取的种子,适合喂给CSPRNG。ARM平台的TrustZone也内置TRNG,树莓派博通芯片、手机SoC里都有类似模块。

在Linux下,内核的/dev/random/dev/urandom会从这些硬件熵源收集熵。你甚至可以手动验证一下树莓派是否有硬件RNG:查看/dev/hwrng是否存在,或者用rng-tools把硬件熵导入内核熵池。

但CPU内置TRNG也有短板:你无法完全掌控它的熵源质量,偶尔会发生固件层被爆出漏洞的事件。所以安全级别要求极高的厂商,往往会自己做TRNG或采购独立安全芯片,而不是完全信任CPU内置模块。

2.2 自制TRNG的常见电路方案对比

动手玩家更关心的,是能不能用几十块钱的零件搭一个TRNG。我试过三种主流方案,各有优劣。

方案实现难度硬件成本熵率稳定性
反向偏置PN结雪崩噪声较高中等,需认真布线
环形振荡器相位抖动较高低(逻辑门即可)受温度和电源影响大
ADC悬空引脚采样极低不稳定,适合入门

第一种方案最经典:用一个Zener二极管或普通二极管的反向击穿区产生雪崩噪声,经过运放放大后送给ADC采样。适合模拟电路基础一般的朋友,调试起来也直观。第二种方案用数字逻辑门做成环形振荡器,再通过另一个低频振荡器去“打拍子”,纯数字电路实现,但需要仔细处理亚稳态和抖动采样,新手容易踩坑。第三种方案最简单,但是噪声幅度小、频率低,随机性偏差大,更适合用来验证原理和跑通流程。

2.3 选型思考:入门、工程与产品级

选哪种方案,取决于你的目标。如果你是第一次接触TRNG,我推荐从Arduino ADC方案入门,半小时就能跑起来,先把“熵→比特→检测”的链路跑通,再谈优化。如果要追求工程实用,Zener雪崩方案比较合适,它抗干扰能力强,输出熵率也更高。

如果要做正式产品,我的建议是别自己在模拟电路上死磕,直接购买带安全认证的TRNG芯片或模块,比如Microchip的ATECC608B、Macrochip的MCP2200? 不对,ATECC608B内部有合格随机数发生器,通过AEC-Q100认证。这类芯片出厂前已经过了熵源校准和检测,软件接入也简单,产品过认证时能省掉一堆麻烦。

我自己的体会是:自制TRNG的最大价值不是替代硬件芯片,而是让你理解熵源电路、采样策略和后处理的每一层细节。真到产品级,时间成本和风险控制远比省几块钱更重要。

3. 实操:基于Arduino搭一个低成本TRNG

3.1 材料清单与电路连接

下面分享一个我实测过多次的方案,核心思路是“Zener雪崩噪声 + 运放放大 + Arduino ADC采样”。材料清单如下:

  • Arduino Nano或Uno一块
  • Zener二极管一只,型号1N4733A(稳压值5.1V),或者普通1N4148反向接也可以
  • 运放一枚:LM358或TL072,我建议TL072,输入阻抗高、噪声特性更好
  • 电阻若干:10KΩ、100KΩ、1MΩ各几个
  • 电容:0.1uF瓷片电容、10uF电解电容各一个
  • 9V电池供电给运放,或者用双电源。如果Arduino供5V,运放用单电源5V也能工作,但动态范围小一些

连接逻辑大致是:9V电池正极接电阻后接到Zener阴极,Zener阳极接GND,让Zener工作在反向击穿区;Zener两端并联一个0.1uF电容滤掉高频毛刺;噪声信号经电容耦合到运放的同相输入端;运放接成10倍到100倍放大电路,输出接到Arduino的A0引脚。整个电路建议用面包板搭,但信号线尽量短。

这里有个重要的注意事项:电源不能直接用Arduino的5V给Zener供电,因为5V不足以让常规Zener进入稳定击穿区。用9V电池能给Zener提供足够的反向偏压,同时电池本身比开关电源更少毛刺,噪声源更干净。实测下来,用USB供电的噪声信号里能明显看到周期性干扰。

3.2 固件代码与采样策略

电路搭好后,Arduino代码并不复杂。核心逻辑就是不断读A0引脚的ADC值,把低比特提取出来。为什么不直接读整个8位?因为ADC的高位大多反映了信号的平均电压,而真正的随机抖动主要落在最低的一位或两位上。

const int pinNoise = A0; void setup() { pinMode(pinNoise, INPUT); // 115200波特率,把数据快速推送到串口 Serial.begin(115200); } void loop() { int raw = analogRead(pinNoise); // 只取最低位,这是受噪声影响最明显的位置 int bit = raw & 1; // 输出字符'0'或'1',方便串口监视器观察 Serial.print((char)('0' + bit)); // 采样之间的间隔不能太短,避免ADC连续采样结果相关 delayMicroseconds(800); }

你可以先运行这段代码,打开串口监视器,应该看到一串0和1。如果看起来全是“01010101”这种规律模式,说明采样间隔或电路有问题。

为了让随机性更好,建议加一个最简单的解偏置步骤:连续采两个比特,只保留“01”或“10”这两种情况,分别输出0和1,丢弃“00”和“11”。这就是著名的von Neumann解偏置器,可以有效消除直流偏置。缺点是吞吐量降到原来的四分之一左右,但对一把密钥的生成足够用了。

void loop() { int a = analogRead(pinNoise) & 1; delayMicroseconds(200); int b = analogRead(pinNoise) & 1; if (a != b) { // 01 -> 0, 10 -> 1 Serial.print(a == 0 ? '0' : '1'); } }

3.3 数据采集与格式化

要验证TRNG是否合格,光盯着串口监视器看是不够的,得采集一批数据交给统计检测工具。这里我用Python脚本接收串口数据,把字符流保存下来。

import serial with serial.Serial('COM3', 115200, timeout=1) as ser: with open('trng_bits.txt', 'w') as f: while True: data = ser.read(4096) if not data: break f.write(data.decode('utf-8', errors='ignore')) f.flush()

注意默认串口工具会一个字符一个字符地发,如果没有数据流控制,容易丢字符。波特率建议不要设太高,115200是常用档位。如果你的TRNG输出很慢,可以采用“攒一批再发”的方式,在Arduino里每积攒64字节再打包发送,减少串口中断次数。

收集到足够的字符后,要转成二进制文件。NIST检测套件喜欢处理无格式的比特流,所以我们需要把ASCII的'0'/'1'转成真正的字节位。

# 将trng_bits.txt中的'0'/'1'转换为二进制文件 with open('trng_bits.txt', 'r') as fin, open('trng_entropy.bin', 'wb') as fout: bits = fin.read().replace('\r', '').replace('\n', '') # 截断到8的整数倍 bits = bits[:len(bits) - len(bits) % 8] fout.write(int(bits, 2).to_bytes(len(bits) // 8, 'big'))

如果你读取时本身就攒成了字节数组,也可以直接写二进制,省去转换这一步。二进制文件建议收集至少1百万比特,也就是125KB左右,NIST检测跑起来才比较有统计意义。

4. 随机性检测:怎么证明你的TRNG足够“真”

4.1 为什么必须跑统计检测

别急着拍胸脯说“我用了Zener噪声所以一定是真随机”。物理世界的熵源往往带有偏置、相关性、间歇性,直接采样的比特流很可能不满足均匀独立分布的要求。判断一个TRNG是否合格,必须依靠统计检测工具,而不是肉眼观察。

最常用的标准是NIST SP 800-22,它由美国国家标准技术研究院发布,包含15项统计测试,从频率、游程、FFT、序列相关等多个维度评估随机性。除此之外,还有Dieharder、TestU01、PractRand等工具。我的习惯是先用NIST STS跑一遍大文件,再用PractRand做更严格的长序列检测,两道关卡都过了才放心。

4.2 NIST SP 800-22检测套件实操

NIST STS的使用流程是:下载源码、编译、把二进制文件放进去、运行交互命令。具体步骤我简单记录一下。

git clone https://github.com/StephanMueller/NIST-STS # 或者从NIST官网下载 cd NIST-STS make -f Makefile ./assess 1000000

运行后会出现交互菜单,按提示选择0开始新测试,输入被测文件名、序列数量等参数。如果你只有一个独立文件,序列数量填1,比特长度为1000000。它会生成一个报告文件,通常叫做finalAnalysisReport.txt,里面有每项测试的结果。

需要注意,assess后面的参数是每个被测序列的比特长度,而不是总比特数。如果你准备了100万个比特且只想测一整段,那就输入1000000;如果你想测10段各10万比特,就要输入100000,并设置序列数为10。我第一次用的时候把这搞混了,测试结果惨不忍睹,还以为自己的电路坏了。

检测结果里最关键的两个指标是P-value和Proportion。P-value如果小于0.01,该项测试就判定失败;Proportion则反映了多段序列中有多少段通过了频率测试,一般要求不低于96%。

4.3 结果解读与后处理

如果你的自建TRNG第一次没通过NIST检测,不要慌,这是常态。Zener噪声电路的直流偏置、ADC量化非线性都可能引入偏差。测试失败通常不是熵源彻底不行,而是输出比特流没有经过“熵提取”和“清洗”。

常见的后处理手段有两个。第一个就是前面提到的von Neumann解偏置,思路简单,但吞吐低。第二个是用密码学哈希函数做熵提取,把原始有偏的比特流送进SHA-256,取输出的一部分。这种方法能同时消除偏置和相关,工程上常用。

下面是一段Python后处理示例,每64字节原始熵做一次SHA256,取前16字节作为输出。

import hashlib import sys with open('trng_entropy.bin', 'rb') as fin: data = fin.read() out = bytearray() for i in range(0, len(data) - 63, 64): h = hashlib.sha256(data[i:i+64]).digest() out.extend(h[:16]) with open('trng_purified.bin', 'wb') as fout: fout.write(out)

哈希后处理的前提是原始熵源必须有足够的熵,否则“垃圾进垃圾出”。判断熵够不够,可以用ent工具或估算最小熵。一般来说,自制ADC噪声方案的原始比特流熵往往偏低,用SHA-256处理后能显著改善检测结果,但不能完全替代硬件改进。

4.4 NIST测试项速查表

测试名考察目标
Frequency (Monobit)0和1的比例是否接近
Runs Test0和1的游程分布是否合理
Longest Run最长游程是否异于随机
FFT Test序列是否存在周期性
Serial Test相邻比特模式是否均匀
Approximate Entropy局部模式的可预测性
Cumulative Sums累积偏移是否异常

表格里只列了部分,全套15项要跑完需要一些时间。建议做TRNG时先把这7个核心项跑通,再跑完整套。

5. 常见问题与排查技巧实录

5.1 0/1比例严重偏斜

我最早搭的Zener电路采集出来的数据大概有63%的比特是1,这个偏斜非常明显,直接导致NIST频率测试不通过。排查后发现原因有三层:一是Zener击穿电压波动导致平均电平抬高;二是ADC输入阻抗和运放输出阻抗不匹配,造成基线漂移;三是采样时间固定,电流噪声没有充分随机化。

解决办法并不复杂:先检查运放输出对地电压,如果明显偏离2.5V中心点,就在放大级输出端串一个隔直电容,让交流噪声通过而直流偏置被滤掉。然后再加von Neumann解偏置,或者用哈希后处理兜底。两招都上,最终检测通过率就有了保障。

5.2 采样数据节奏异常,出现明显周期

有段时间我串口收到的比特流,每隔几个字节就出现一段固定的“010101”模式,像时钟信号混进来了。最后用示波器一看,发现Arduino板上的16MHz晶振信号通过地线耦合进了运放输入。这是因为我的面包板布线太乱,运放输入引脚离晶振走线太近,电磁干扰被放大了。

排查思路:先断开Zener,把运放输入端直接短路到GND,看输出是否有周期性信号;如果没有,说明噪声源环节没问题,问题出在外部耦合。再把运放输入端用一根短线接到干净地线,尽可能缩短信号路径。我后来把整个噪声源电路单独隔离在一小块区域,模拟电源和数字电源分开接,周期性模式基本消失。

5.3 噪声源漂移与环境干扰

AD采集到的信号幅度会随着温度、湿度缓慢漂移,这是Zener击穿噪声方案的固有特性。冬天开暖气后,我采集到的平均电平比秋天高了不少,导致后续处理阈值需要重新校准。解决方法是写一个自动校准循环:每隔一段时间采样1000次,动态计算中位数,把采样点相对于中位数的正负作为随机位,而不是直接看最低比特。

这种方式能有效抵抗直流漂移。因为无论平均电压怎么变,只要采集值围绕中位数上下波动,就能提取出相对均匀的随机位。另外,把电路装进一个金属屏蔽盒里,也能明显降低手机信号、Wi-Fi等射频干扰造成的突发扰动。注意屏蔽盒要和设备的GND连通,否则反而会变成天线。

6. TRNG的应用场景与落地建议

6.1 密钥生成与安全协议

TRNG最刚需的场景是密码学。无论是TLS握手用的临时密钥、加密文件的对称密钥,还是初始化向量(IV)、随机nonce,都需要不可预测的随机数。如果随机数可预测,攻击者就能构造出同样的密钥,整个加密体系瞬间崩溃。

现代操作系统其实已经帮我们兜底:Linux的/dev/urandom会在系统启动时收集硬件中断、磁盘IO、网络包等熵源,混合后用CSPRNG输出。但对专业的嵌入式设备来说,系统运行的确定性很强,没有键盘鼠标事件、没有网络流量,熵池可能长期处于低熵状态。这时单独挂一个TRNG模块,直接给内核提供熵补充,是很多安全网关设备的标准做法。

6.2 抽奖、游戏与公平随机

游戏开箱、直播抽奖、体育竞猜,这些场景的随机性直接关系到用户信任。如果后台用固定种子的伪随机数,用户一旦摸清规律就能预判结果,这相当于把运营方架在火上烤。用TRNG生成抽奖序列,每次结果都无法从历史数据推导,能从根本上消除“暗箱操作”的质疑。

这里有个实际案例:我帮朋友做过一个线下活动抽奖程序,结果就是用Arduino TRNG给后端CSPRNG提供种子,再生成唯一的中奖号码。虽然技术上稍微绕了一点,但对外可以理直气壮地说“物理真随机”,用户宣传效果和信任度完全不一样。

6.3 科学模拟与统计抽样

蒙特卡洛模拟、统计抽样、机器学习中的随机初始化,这些场景对随机数的要求没有密码学那么高,但同样存在“伪随机周期性”影响结果的风险。用TRNG产生的独立样本能降低结果偏差,尤其当模拟规模很大时,伪随机序列的周期属性可能被放大。

当然,纯科学实验为了可复现性,往往希望用固定种子,这时PRNG是更好的选择。所以科学场景的TRNG应用更多是“混合制”:先用TRNG产生一次性种子,然后记录种子并保存,后续复现用同一个种子跑PRNG,兼顾随机性和可复现性。

7. 最后分享几点个人体会

从零搭一个TRNG,最让我意外的是“熵”这个东西的脆弱性。电路里一个不显眼的接地回路、一行不合适的循环延时,都可能把假随机模式重新带回来。真正可靠的TRNG,是靠物理源、采样策略、后处理和统计验证四层防线一起撑起来的,任何一层偷懒都会在检测报告里现原形。

另外一个实用建议:如果你的目标是快速做出一个“够用”的真随机源,不要过度纠缠在自制电路的细节上。直接采用主流的做法——低成本TRNG只负责产种子,具体随机流交给CSPRNG扩展。这样既绕开了自制TRNG吞吐低的痛点,又保证了随机源不可预测。等有一天你需要把TRNG放进量产设备时,买一颗经过认证的独立安全芯片绝对比继续调模拟电路更划算,那是真金白银换来的经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询