从物理熵源到随机性检测:TRNG真随机数生成器实战解析

📅 2026/8/27 2:44:50
从物理熵源到随机性检测:TRNG真随机数生成器实战解析
“随机数”这个词在工程圈里藏着不少坑。很多人觉得在代码里调用Math.random()、rand()拿到的东西就是随机数实际上你拿到的往往是确定性算法生成的伪随机序列。真正的随机必须靠物理世界来提供。True Random Number GenerationTRNG要解决的核心问题就是从物理噪声里提取不可预测的熵而不是让数学公式去“模拟”随机。这篇博文我结合自己从零搭建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搭一个低成本TRNG3.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, timeout1) 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, errorsignore)) 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:i64]).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放进量产设备时买一颗经过认证的独立安全芯片绝对比继续调模拟电路更划算那是真金白银换来的经验。