ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

NIST SP800-90B熵评估源码解析:从最小熵到随机数质量检测

NIST SP800-90B熵评估源码解析:从最小熵到随机数质量检测 简介NIST SP800-90B熵评估源代码是依据NIST标准开发的随机数生成器RNG质量检测工具面向密码学工程师、安全测试人员及研究人员用于量化硬件与软件随机源的熵水平确保其满足安全应用不可预测性要求。该资源压缩包共21个文件、2.07MB其中13个Python脚本构成核心测试框架4个bin文件提供典型随机数样本另有PDF用户指南、Markdown说明和docx笔记便于阅读和参照整个包结构清晰可直接在Python环境中运行。源码完整实现了NIST SP800-90B标准中的近似熵、最小熵等核心指标涵盖数据预处理、直方图分析、χ²检验、阈值判定等环节并针对IID与非IID序列提供不同评估模块帮助深入理解熵评估原理与实现细节。已有1201人学习该资源配套文档与示例数据可以辅助快速验证也可作为二次开发自定义RNG测试工具的基础。 说实话我第一次拿到 NIST SP800-90B 熵评估源代码的时候第一感觉是这不是一个能让你一键生成漂亮报告的工具而是一台专门用来拆穿伪随机的显微镜。你觉得自己在设计一个非常精妙的随机数发生器数据看起来也很随机但把样本喂进这套评估流程之后它会用一堆你平时根本不会关注的统计指标把你的熵源按在地上反复摩擦。尤其是那个取最小值的设计逻辑简直就是在告诉你哪怕你 9 个指标都过了只要 1 个指标拉胯你的熵源在标准眼里就不合格。这篇内容就是基于我研究和使用这套源代码的完整记录。我会从标准到底在测什么、源码的结构怎么读、实际运行怎么操作、再到最容易翻车的几个坑完整拆一遍。如果你正在做硬件安全芯片、密码模块、物联网设备的随机数发生器或者只是对熵这个概念好奇这篇都适用。1. 为什么几乎所有随机数接口都绕不开这份代码先聊点背景。你做的任何一个跟安全沾边的系统只要涉及密钥生成、签名、加密随机数背后都藏着一个硬件或者软件熵源。大部分工程师嘴上说着我用的是真随机数发生器但实际上没人说得清这个熵源到底有多真。SP800-90B 这套标准就是为了给这种行为画一条线熵源的输出到底有多少不可预测性用最小熵来量化而且给了你一套配套的评估代码。这套代码就是 NIST 官方发布的实现不是第三方爱好者写的是标准正文的可执行版本。很多人的误区是我只要用了硬件 RNG它就是随机的不需要测。这个想法在我刚接触这块的时候也有过。但真实场景是一个环振采样出来的比特流可能因为电压波动、温度漂移、工艺偏差出现严重的周期性。人眼看不出来但卡方检验一下就会原形毕露。更麻烦的是有些芯片的熵源在后处理之前根本达不到预期熵值而你在系统里用的其实是后处理之后的数据这会带来一种虚假的安全感。SP800-90B 的评估源代码就是帮你把这些问题暴露出来的。另外这套代码的出现解决了一个非常实际的合规问题以前各个厂商自己做随机数检测用的测试五花八门报告格式也不统一。有了官方实现之后厂商可以在同一个尺子下评估自己的熵源评测机构也有了一个默认参照物。你不需要自己发明算法直接用官方代码跑数据然后解释报告。这一点对于做密码模块认证的团队来说价值非常直接。回到代码本身。NIST 在 GitHub 上有一个专门的仓库里面包含 Python 版和 C 版两套实现。Python 版适合做分析、调参和快速验证C 版适合做大规模批量测试或者嵌入到自动化产线。无论你选哪套源代码的核心评估逻辑是一致的。2. 读懂源码前先要搞清的事它在测什么、用什么指标2.1 最小熵才是密码学关心的那个熵源码里各种函数算来算去最后落到报告上的核心指标是最小熵不是香农熵。这两个概念的区别值得说一下。香农熵描述的是整个分布的平均不确定性公式是 -Σp·log2(p)它对分布的整体形态敏感。而最小熵只关心分布中最可能出现的那个值到底有多大概率公式是 -log2(p_max)。为什么密码学用最小熵而不是香农熵想象一个极端的场景某个熵源输出 0 的概率是 0.5输出 1 到 255 的概率各约 0.002。这个分布的香农熵接近 8 比特看起来非常漂亮。但攻击者只需要猜你最可能输出什么他有 50% 的概率猜中 0。真正决定安全性的是攻击者猜中最可能值的概率而不是平均不确定性。所以 SP800-90B 用最小熵来定义熵源的质量源码里所有估计器最终都指向这个指标。理解了这一点你就知道看报告时应该盯哪个数字了。源码输出的最小熵有两种格式每样本最小熵和每比特最小熵。如果你的样本是 8 比特那每比特最小熵 每样本最小熵 / 8。很多厂商喜欢报每样本的比特数很高的数字但评估机构真正关心的是每比特最小熵是否达到了宣称的安全强度。2.2 评估的主线先测 IID再决定用哪套估计器源码的评估流程分为两条路径。第一步它会对你的数据集做 IID 测试也就是判断这些样本在统计上是否可以视为独立同分布。如果数据通过了 IID 测试就走 IID 熵估计的路径如果没通过就走非 IID 的 10 种估计器路径。为什么这个判定如此关键因为 IID 假设直接决定了你能用多简单的模型来估计熵。如果数据是 IID 的那么每个样本的分布就是整个数据集的分布你只需要统计每个取值出现的频率就够了。但如果数据不是 IID 的样本之间可能存在相关性、马尔可夫依赖、长程关联这时候用一个简单的频率分布去算熵会严重高估真实熵。官方源码中IID 测试不是单个测试而是一组测试的组合。包括卡方检验、置换检验、游程检验、最长游程检验等每个测试从不同角度找数据中的非随机模式。这些测试的源码实现逻辑我在这里不逐行贴代码量很大但关键点是官方代码使用的是随机重采样策略也就是说它会对原始数据进行多次随机打乱然后比较打乱前后的统计量。这里就引出了很多新手第一次运行时会被吓到的一个现象同一个数据文件跑两次 IID 测试结果可能不一样。因为随机重采样过程引入了随机性。你第一次跑可能判定为 IID第二次跑可能判定为非 IID。这不是代码 bug而是这种统计方法的内生特性。官方代码对此的处理方式是让你增加轮数来降低误判概率我后面会详细说。2.3 非 IID 路径里的 10 个估计器为什么那么不近人情如果数据被判为非 IID源码会启动 10 种不同的熵估计器包括最常用值估计、碰撞估计、马尔可夫估计、压缩估计、元组估计、最长重复子串估计、偏度估计、自相关估计等。这 10 种估计器的共同特点是它们从不同的角度寻找数据中的可预测性而且最终取的是最小值。这个取最小值的设计是标准的核心哲学在密码学里你宁可低估熵也不能高估。如果你的数据在马尔可夫估计器下暴露出了很强的状态依赖哪怕其他 9 个估计器都给出接近 8 比特的结果最终报告也只会按马尔可夫估计器给的那个低值来算。我在看代码的时候觉得这个设计非常像审计的逻辑——审计师不会因为你大部分账目没问题就放过有问题的那个科目他只会按最坏情况来评估风险。源码中每个估计器的实现各有侧重。比如碰撞估计器它统计的是数据中重复值出现的间隔或者首次出现重复的位置如果数据有很强的结构性碰撞发生的频率会显著偏离随机预期。马尔可夫估计器则把数据看成一种状态转移过程统计从某个状态转移到另一个状态的频率分布如果某些转移概率非常高说明下一个输出可以被上一位输出预测。压缩估计器更直接它对数据做压缩压缩率越低说明可预测性越高熵越低。实际操作中这 10 个估计器跑起来非常耗时。以 Python 版为例对 100 万个样本做完整的非 IID 评估可能耗时数小时甚至更久具体取决于机器的 CPU 性能和样本位数。这也是为什么官方仓库里还保留了 C 版本——生产环境下没人愿意等 Python 慢慢跑。3. 官方评估工具的代码结构与核心流程拆解3.1 源码骨架两个入口一条数据管线从代码结构上看这套评估工具非常清晰。主入口有两个脚本一个是 iid_test对应 IID 路径另一个是 noniid_test对应非 IID 路径。输入都是一个原始数据文件输出是评估报告和计算出的最小熵值。两者共用一个数据读取模块用来解析不同格式的样本数据——既支持 ASCII 格式的每行一个十进制样本也支持二进制格式的原始字节流。我建议第一次读代码时不要扎进某个估计算法里而是先把数据流理清楚。整个工具的核心流程是读数据文件解析样本判断样本的位宽和格式然后根据入口进入对应的评估路径。IID 路径内部会先跑一组假设检验然后根据检验结果决定是否进入 IID 熵估计非 IID 路径则直接运行全部 10 个估计器最后汇总出最小值。理解和把握这个主线比背下每个函数的实现更有用因为这套代码的复杂度在于多个估计器之间的逻辑关系而不是单行代码本身。3.2 核心函数级别的实现思路如果要挑最简单的熵估计器来理解代码最常用值估计Most Common Value是最合适的一个。它的核心逻辑非常直白def most_common_value_estimate(samples, bits_per_sample): sample_counts {} for s in samples: sample_counts[s] sample_counts.get(s, 0) 1 p_max max(sample_counts.values()) / len(samples) min_entropy_per_sample -math.log2(p_max) min_entropy_per_bit min_entropy_per_sample / bits_per_sample return min_entropy_per_sample, min_entropy_per_bit这个代码几乎就是最小熵定义的直接翻译统计每个取值出现的频率找到最高频率作为 p_max取负对数。虽然这段代码很短但它是整个工具中最重要的逻辑之一因为无论数据经过多么复杂的相关性分析最常用值估计给出的结果永远是一个不可绕过的锚点。其他估计器就要复杂得多。碰撞估计器需要把数据分块然后在每个块内找首次重复的位置计算碰撞时间的分布。马尔可夫估计器需要维护一个状态转移矩阵矩阵的大小和样本的位数相关如果你样本是 8 比特那就是 256×256 的矩阵。压缩估计器甚至内部接了一个通用的压缩算法来估算数据的压缩率。在读源码的时候我发现一个很值得注意的细节各个估计器并不是直接对原始数据做计算而是使用了不同的归约方式。比如元组估计器会把数据按 2 元组、3 元组重新组织然后统计不同元组的出现频率。马尔可夫估计器则把数据看成一种状态序列统计的是状态跳转的规律。这种设计导致同一份数据在不同估计器下可能呈现出完全不同的可预测性特征而最终取最小值从逻辑上是说只要任何一个视角暴露了数据的非随机性我们就不应该盲目信任它。3.3 初始化与随机性为什么两次运行结果会跳源码中还有一个非常隐晦但重要的地方很多内部测试不是确定性的。特别是 IID 测试中的置换检验它通过打乱数据顺序来构造零假设下的统计分布而这个打乱过程用到了伪随机数生成器。换句话说评估随机数的工具本身内部也在用随机数。这个设计在密码学工具里是一个充满哲学意味的循环但在工程上确实引入了实际影响。同一个样本文件你用 Python 版跑两次非 IID 评估如果某个估计器的结果恰好落在阈值附近两次输出可能在边界上出现不一致。这会导致你在写评估报告的时候很难解释为什么同一组数据昨天跑的结果和今天跑的结果不一样。解决思路有两个层面。第一官方代码支持设置随机种子你在命令行里固定种子之后结果就可以复现。第二即使不设种子你也要理解这种波动的合理范围。如果某个估计器的结果在多次运行中始终稳定在某个值附近那没问题。但如果每次运行的结果都有较大的跳跃你需要怀疑数据量是否足够或者数据本身存在某种非平稳特性。稳妥的做法是正式报告数据时固定随机种子并把多次运行的结果都记录在案。4. 实际操作从采集数据到拿到一份可信报告的完整过程4.1 数据采集阶段的两个硬指标在运行任何评估代码之前你要解决的是数据从哪来。SP800-90B 标准对数据量有明确的硬性要求至少采集 100 万个样本。这个数字不是随便定的因为很多统计测试的置信度依赖于样本量。100 万个 8 比特样本大约是 1MB 数据采集起来并不困难但关键是要在真实工作条件下采集而不是在实验室里给它一个最理想的电压和温度环境。另一个硬指标是样本位宽。官方工具支持不同位宽的样本但你必须明确告知工具你的样本是多少比特。我遇到过有人把 8 比特的样本用 1 比特的模式去解析导致结果完全失真。如果你的硬件熵源输出的原始数据是模拟信号经过比较器之后得到的比特流你可以按 1 比特样本评估如果硬件输出的是 32 位或者 64 位寄存器值你需要决定是直接按原始位宽评估还是先截断成 8 比特。不同的位宽选择会让评估结果相差非常大。数据采集时还有一个容易犯的错误直接拿后处理过的数据做评估。如果你的熵源后面接了哈希函数、CRC 或者 LFSR 做后处理那么你采集到的数据是后处理输出而不是原始熵源输出。评估原始熵源的时候你必须绕过后处理逻辑直接抓取原始随机比特。评估后处理后的输出也不是不行但那是另一套标准和逻辑。官方源代码本身不会区分这两者它只会机械地分析你给它的数据。这个区分需要你自己来做。4.2 命令行运行从 ASCII 文件到报告数据准备好之后运行工具本身并不复杂。假设你有一个文本文件 random_data.txt每行一个 0 到 255 的十进制数代表 8 比特样本那么 IID 测试的命令大概是这样的python iid_test.py -i random_data.txt --bits 8 --verbose跑完之后终端会输出每个子测试的统计量和 p 值最后给出是否通过 IID 的结论。如果结论是数据是 IID 的工具会接着给出基于 IID 假设的最小熵估计。如果你想强制走非 IID 路径不管 IID 测试的结果是什么就直接运行python noniid_test.py -i random_data.txt --bits 8 --verbose这个命令会跑完全部 10 个估计器输出每个估计器的最小熵结果然后告诉你最终采用的最小值是多少。这里有一个细节值得注意即使你的数据通过了 IID 测试标准也允许你用非 IID 路径的结果作为最终评估值。这在工程上是一个保守但更安全的选择因为 IID 测试本身也可能犯第二类错误把非 IID 的数据误判为 IID。在正式合规报告中很多团队会直接走非 IID 路径省去 IID 判定的争议。4.3 输出报告怎么解读运行结束后除了终端输出官方工具还会生成一份 JSON 或者文本格式的详细报告里面记录了每个估计器的参数和结果。看报告时我最关注的是两个数字每样本最小熵和每比特最小熵。假设你的样本是 8 比特每样本最小熵是 6.4那么每比特最小熵就是 0.8。这意味着每个输出比特只有 0.8 比特的不可预测性。如果你要用这个熵源生成一个 128 比特的密钥你需要从熵源采集至少 128 / 0.8 160 比特的原始输出再做提取和压缩才能保证密钥有 128 比特的安全强度。实际工作中我遇到过一些团队拿到报告只读每样本最小熵看到 6.4 就觉得挺高直接忽略了每比特的换算。这种做法非常危险因为采样位数越高每比特熵值往往越低。你的熵源可能每样本给出了 6.4 比特的熵但这只是 8 比特之一换算到每比特可能只有 0.6 甚至 0.3。密码模块设计时真正关心的是能不能从每比特输出中榨出足够的安全强度。4.4 关于随机种子和重复性为了报告的可复现性我强烈建议正式评估时固定随机种子。官方代码提供了相关参数你在命令行里指定一个固定值比如 42然后所有内部置换检验和重采样过程都会使用这个种子。我自己的习惯是对同一份数据固定种子跑 3 次记录每次的结果再换一个种子跑 3 次对比波动范围。如果不同种子下的评估结果差异非常小比如最小熵在小数点后两位内浮动那说明数据量足够评估结果可信。如果差异明显那大概率是你的数据本身存在边缘状态需要采集更多的数据或者检查采集过程是否稳定。5. 兼容性陷阱与误用场景我踩过的几个坑5.1 位宽不匹配最隐蔽的错误我踩过的第一个大坑就是位宽。当时我从一个 FPGA 上的环形振荡器熵源采集了一批数据硬件输出的是 32 位寄存器值。为了省事我直接把这批数据当成 8 比特样本喂给评估工具。结果跑出来最小熵非常低几乎接近 0。排查了很久才发现32 位寄存器值被当成 8 比特解析之后低 8 位的模式被强行切割重组原本的时序信息被彻底打乱统计测试自然能检测出大量异常。更麻烦的是这种错误不会产生报错信息工具会照常运行并输出一个看似合理的结果但这个结果毫无意义。正确的做法是采集原始寄存器值后根据实际的熵源设计来决定评估粒度。如果我的目标是评估每个输出比特的熵那应该把 32 位值拆成 4 个 8 比特样本或者直接按照硬件输出的位宽来配置工具。这一步看似简单但一旦搞错后面所有分析都是白做。5.2 时序采样引入的自相关不是硬件有问题是你的采集方法有问题另一个让我印象深刻的坑与数据采集的方式有关。当时我在用 MCU 的 ADC 采样一个噪声源为了加快速度我在循环里连续读取 ADC 寄存器。评估结果显示自相关估计器给出的熵极低。原因不是噪声源本身太差而是我连续读取 ADC 时上一次采样对下一次采样有残留影响——ADC 的采样电容还没来得及完全放电就被拿来采了下一个值。这种时钟同步采样引入了样本之间的相关性。解决方法是给每次采样之间加入足够长的间隔或者使用过采样技术把多个样本合并处理。官方评估工具本身不会告诉你这个坑它只会冷酷地暴露你的数据质量问题。这是一个通用的工程问题采集数据本身的行为可能会污染数据。如果你用中断或者 DMA 连续采集一定要检查样本之间是否存在时间关联。一个简单的方法是把数据文件画成时间序列图如果看到明显的周期性波形那采集过程就有问题。5.3 数据非平稳性不能只测一段数据就说合格很多人在做熵源评估时只采集一段数据跑个报告看到最小熵达到要求就认为大功告成。这在标准眼里是不够的。熵源在不同温度、电压、工艺角下的表现可能完全不同。一个在常温下评估合格的熵源在高温下可能因为运放增益变化而输出退化。SP800-90B 的评估源代码在设计上虽然只接受一个数据文件作为输入但这个工具本身隐含了一个假设你的数据是在某种特定条件下采集的。为了符合实际部署的需求你需要对多个条件分别采集数据分别评估然后取最坏情况下的最小熵作为设计指标。我在实际项目中的做法是至少在常温、高温、低温三种温度下各采集一组数据每组 100 万到 500 万个样本然后分别跑非 IID 评估。最终报告里采用三组结果中最低的每比特最小熵作为这个熵源的设计值。这样做虽然耗时但避免了在极端工况下被认证实验室打回的风险。5.4 只信最小值但也要看估计器的分布最后想提醒一点虽然标准要求取 10 个估计器的最小值但你在看报告时不要只盯着那个最小值还要看其他估计器的分布。如果只有一个估计器给出极低值其他 9 个都很高那可能意味着数据中存在一种特定类型的相关性而这个相关性可能是可以修复的。比如若是马尔可夫估计器给出的熵特别低那就说明样本之间的状态转移有很强的规律你可以尝试增加采样间隔或者引入位反转、异或等去相关处理再重新评估。反过来如果 10 个估计器给出的结果普遍偏低那问题可能出在熵源本身的物理噪声强度不够这时候做后处理可能是唯一的补救办法。后处理本质上是用经典的密码学操作来压榨原始熵但从评估的角度看你依然要保证原始熵源的熵率不能太低否则后处理算法也无法凭空创造熵。6. 从评估到落地源代码之外的工程化建议6.1 把评估融入研发流程而不是事后补救很多硬件团队的习惯是芯片流片回来之后跑一下官方工具发现熵不够然后手忙脚乱地打补丁。但其实评估这件事完全可以前置到设计阶段。在 FPGA 原型验证阶段你就可以把熵源模块单独拉出来采集数据跑 SP800-90B 的评估工具。这时候发现问题改 RTL 的成本远低于流片后发现问题。我在 FPGA 验证阶段就会把这一套评估脚本集成到自动化的回归测试里每次修改熵源设计后重新跑一遍评估。这套流程看起来多花了一点时间但长期看能省掉大量返工成本。集成的思路其实很简单。官方工具提供了命令行接口你可以写一个简单的脚本自动完成数据采集、格式转换、调用评估工具、解析输出、和上一次结果做对比。如果最小熵低于预设阈值就报警。这样每次代码改动之后熵源质量是否退化就变成了一目了然的事情。6.2 关于后处理的一个实践教训后处理这个词在随机数发生器领域有点被神化了。很多人认为只要在熵源后面挂一个 SHA-256任何垃圾输入都能变成安全的随机数。这句话在密钥生成场景下没问题但在熵源评估场景下会栽跟头。SP800-90B 评估源代码的默认对象是原始熵源输出。如果你的数据是经过哈希后处理的那你评估的其实是后处理器的输出而不是熵源本身的熵。评估后处理输出本身没有意义因为后处理器是确定性的它不会增加熵只是将输入熵分散到更多的输出比特中。如果原始熵源只有 0.1 比特/样本的熵无论你用什么后处理算法都不可能凭空把它变成 8 比特/样本的熵。所以在设计上你需要明确区分两条路径一条是评估原始熵源一条是评估后处理器输出。前者用来证明你的物理噪声源有足够的熵后者用来验证你的后处理算法没有引入额外的偏置。官方工具更适合前者后者你需要额外的统计测试来辅助判断。6.3 健康检测和实时监测评估工具只能离线分析数据但它给在线健康检测提供了很好的设计参考。SP800-90B 本身也提到了连续健康测试的要求也就是熵源在正常工作时要周期性地检查输出数据是否存在异常。一个简单有效的方法是从熵源输出中定期采集一小段数据快速计算基本统计量比如最常用值的频率、自相关函数、运行均值等。这些指标不需要像完整评估那样严谨但可以快速捕捉熵源的急性退化。用完整评估工具做体检用轻量统计做日常监测这个组合在工程上非常实用。我在实际产品里实现过一个方案每秒钟从熵源采样 1024 个字节计算每个字节的均值、方差和最常用值频率如果这些指标偏离基线超过一定阈值就触发健康测试失败并停止从熵源提取数据。这套逻辑就是借鉴了 SP800-90B 评估代码中熵估计器的思路只是做了大幅简化以适应实时性要求。6.4 关于工具本身最后的个人体会我再回头说说这套官方源代码。它最大的价值不在于那些统计公式而在于它把熵源质量从一个模糊的概念变成了一个可测量、可比较、可认证的数字。你对这套代码的理解越深你对熵源设计的认知就越清晰。我自己在刚开始接触的时候犯过一个方向性的错误我试图通过调整采集数据的格式、改变样本解析方式让评估结果看起来更好。后来我意识到这本质上是在自欺欺人。评估代码是无情的它不关心你如何解析数据它只关心你送给它的比特流是否真的不可预测。与其纠结怎样让报告更好看不如老老实实优化熵源的物理设计把采集电路做好把采样时序调对然后让代码给出它该给的结果。现在每当我拿到一个新的熵源模块第一件事就是跑一遍完整的非 IID 评估然后把这 10 个估计器的结果全部摆出来看一遍。哪个估计器低就去看对应的物理机制是什么。这不仅是一个工具的使用经验也是一种工程思维的训练。推荐每一个做随机数相关开发的人都去仔细啃一遍这套源码哪怕你最终不用它来出报告它也会改变你评价随机性的方式。本文还有配套的精品资源点击获取
返回列表