拿到一个随机数生成器,或者自己写完一段PRNG代码,第一件事是什么?我一般不做功能自测,而是直接丢给 NIST 随机数测试软件,跑一遍那十几个统计测试项,看它能不能撑住 100 万 bit 的序列检验。NIST STS(Statistical Test Suite)在随机性评估领域基本就是默认标准,不管是做密码学模块、硬件真随机数源(TRNG)、还是嵌入式系统的熵源验证,最后拿出手的报告总得有几行 P-value 和通过率。这篇教程我会从下载渠道、编译安装、参数选择到报告解读全部过一遍,兼顾刚入门的新手和已经在项目里用过的老手,争取不让你走我当年走过的弯路。
这个工具能解决的问题很直接:随机数“看起来乱”和“统计学上合格”完全是两码事。你在终端里打印一串数字觉得挺随机,但拿到频数检测、游程检测、块内最大游程检测里跑一圈,可能立刻露馅。NIST SP 800-22 定义的这套测试套件,就是用来判断一个二进制序列是否具备随机序列应有的统计特征,评估对象包括密码算法产生的伪随机数、硬件 RNG 采样数据以及各类熵源输出。适合参考这篇教程的人包括:正在做安全模块开发的软件工程师、需要验证硬件熵源质量的嵌入式开发者、研究随机数算法的学生,以及任何需要在项目验收文档里附上一份正式随机性测试报告的人。
1. 随机数测试这件事,为什么绕不开 NIST
1.1 一个统计测试套件如何判断“随机”
判断“随机”本质上是个逆否命题思维:我们没法证明一个序列绝对随机,但可以检验它“不随机”的程度。NIST STS 的做法是设计一系列统计量,每个统计量对应随机序列某方面的特征,然后计算待测序列在这些统计量上出现的概率,即 P-value。如果 P-value 小于预设阈值(默认 0.01),就认为这一项测试失败,说明序列在该维度上偏离了随机性预期。
整套测试从频数检测(Frequency Test)开始。这个测试逻辑最简单,就是看整个序列里 0 和 1 的比例是否接近 1:1。一个随机序列里出现 60% 的 1 和 40% 的 0,直觉上就觉得不对劲,频数检测就是把这个直觉量化。做完频数检测后,还有针对相邻比特关系的游程检测(Runs Test),它统计连续相同比特的段长度分布,检验序列中是否出现过长的连续 0 或连续 1。再往后还有块内频数检测、离散傅里叶变换检测、非重叠模板匹配检测等一共 15 项,各自关注随机序列的不同切面。把这些测试全跑完,基本就能覆盖常见的随机性缺陷类型。
这里有个重要概念:这些测试不是孤立看的。单项测试通过不说明随机性合格,但多项测试同时失败基本能确定序列有问题。NIST 官方建议的阈值是显著性水平 α = 0.01,也就是说,即使一个序列真随机,理论上也会有约 1% 的概率在某项测试中被误判为失败。所以当你看到某个测试项 P-value 略小于 0.01 时,别急着下结论,通常需要多跑几组样本看整体通过率。
1.2 别把 STS 和 SP 800-22B 搞混了
很多人第一次接触 NIST 随机数测试时,会在两个文档之间绕晕:一个是 NIST SP 800-22,全称 “A Statistical Test Suite for Random and Pseudorandom Number Generators for Cryptographic Applications”,这是测试套件的算法规范文档;另一个是 NIST SP 800-22B,晚一些发布的,它更像是一个应用指南,用来指导用户“如何选择合适的测试项并解释结果”。我们在工程里实际编译运行的那个命令行工具,就是基于 SP 800-22 规范实现的 STS 套件。
关于版本,我得强调一下:目前你在各种博客上看到的截图,多是早年的 1.6 或 2.0 版本,界面和输出格式都比较旧。现阶段最新的主版本是 2.1.2,对应 SP 800-22 Rev 1a 的算法。下载源码后编译得到的可执行文件是assess,这个程序就是全套测试的入口。2.1.2 版本修复了老版本里 Makefile 找不到 libm.a 的问题,还改进了部分测试项的边界处理,强烈建议直接用 2.1.2,不要再用老版本折腾。下载方式我在下一节展开说。
2. 最新版下载与环境准备
2.1 从哪个渠道拿到最新版最靠谱
NIST 官网的 CSRC 页面虽然还挂着 SP 800-22 的资料,但那个下载链接更新频率不高,时不时还会因为站点改版导致链接失效。我自己现在习惯去 GitHub 拿源码,在 GitHub 上搜索 “NIST STS” 或 “sp800-22” 就能找到若干镜像仓库。需要注意仓库是否包含了完整的sts-2.1.2目录结构和文档,有些仓库只放了源码没放论文 PDF,不影响编译,但建议把 SP 800-22 规范文件也下下来,报告里引用时用得上。
如果你用的是 Linux 发行版,有些系统的软件仓库里也能搜到相关包,比如apt-cache search nist在部分源里会出现r-cran-randomfields这类同名但完全无关的包,别下错。最稳妥的方式还是 Git clone 源码:
git clone https://github.com/InsaneMonster/NIST-Randomness-Testsuite.git cd NIST-Randomness-Testsuite/sts-2.1.2如果你访问 GitHub 不稳定,也可以用我后面讲到的办法,去其他镜像站下载打包好的源码压缩包。总之,别下载来路不明的编译好的二进制文件,这种工具类软件最好从源码编译,既能确认没有被人动过手脚,又能根据自己系统环境调整编译参数。
2.2 Windows 环境准备:WSL 还是原生编译
Windows 下跑 NIST STS 有几个选择。最简单的是启用 WSL(Windows Subsystem for Linux),在 Ubuntu 子系统里直接编译运行,这样路径和依赖问题都少,还能用 Linux 下的 shell 脚本处理测试数据。我实测过,WSL2 下编译和运行完全正常,性能损失可以忽略。
另一个选择是在 Windows 上装 MinGW 或 Cygwin 环境做原生编译。这条路比较折腾,主要是 Makefile 里的-lm链接选项在 Windows 下可能找不到对应的数学库,需要手动调整。如果你只是要快速出结果,建议别跟这个环境较劲,直接用 WSL 或者虚拟机。
还有一条路:用 Docker。你把 sts-2.1.2 源码挂载进 Ubuntu 容器,在容器里编译运行,本地只保留测试数据和输出报告。这个方式适合需要多次、批量测试的工程场景,环境隔离干净,换机器也方便。我在团队内部推荐的就是这个方案,后续想要封装成自动化测试流水线也更容易。
2.3 Linux 环境准备与依赖检查
在 Linux 下编译前需要确认系统装了gcc、make和标准库。Debian/Ubuntu 系可以这样检查:
sudo apt update sudo apt install build-essential gcc --version make --version只需要这几个基础工具,STS 没有其他复杂的第三方依赖。不过有一点要注意:老版本的 STS(比如 1.6)在部分 64 位系统上编译时,make会因为缺少libm.a静态库而报错。2.1.2 已经在 Makefile 里处理了这个问题,正常情况下直接make就能过。
如果编译过程中弹出fatal error: gmp.h: No such file or directory,这通常意味着你 clone 的仓库还附带了一些扩展模块。STS 主程序不需要 GMP 库,我建议忽略多余模块,只进入sts-2.1.2目录编译主程序即可。
3. 编译安装全流程
3.1 编译命令详解
进入sts-2.1.2目录后,直接执行:
make终端会开始编译各个测试模块,最后在obj/目录下生成一堆.o文件,并在当前目录生成assess可执行文件。整个过程在一分钟内能跑完。如果你在非常老的操作系统上遇到-lm找不到,可以手动把源码包里的lib/libm.a复制到系统库目录再重新编译:
sudo cp lib/libm.a /usr/lib/libm.a make clean make这条命令在旧教程里出现频率极高,但 2.1.2 里一般用不到了。我把它保留在这里,是因为仍然有人在老系统的兼容模式下会踩到同样的坑。如果你的系统不需要,跳过这步。
编译完成后,可以顺手把assess拷贝到/usr/local/bin,方便之后在任何目录直接调用:
sudo cp assess /usr/local/bin/3.2 验证安装是否成功
编译完成后别急着跑测试,先确认assess能正常启动。在命令行输入:
./assess正常情况下程序会进入交互式界面,先显示一串版本信息,然后进入测试参数设置流程。如果终端只显示一行Could not open file...,先忽略,那是程序在找默认文件,通常不影响启动流程。要强制退出交互界面,直接按Ctrl+C就行。
如果提示segmentation fault或者error while loading shared libraries,大概率是环境还是有问题。前者常见于内存不足或者输入数据格式不该传的情况下发生了越界,后者通常是缺少动态库。解决办法在我第 6 节的问题排查表里有整理。
3.3 常见的编译报错与处理
编译过程中最常见的三类报错和应对方案如下:
| 错误现象 | 常见原因 | 解决办法 |
|---|---|---|
make: gcc: Command not found | 系统没有装 gcc | sudo apt install build-essential或sudo yum groupinstall "Development Tools" |
cannot find -lm | 缺少数学库或链接路径不对 | 检查libm.so是否存在,尝试sudo apt install libc6-dev,必要时手工指定-L/usr/lib/x86_64-linux-gnu |
undefined reference to ... | 源码目录不完整或者编译缓存异常 | make clean后重新make,确认 clone 的仓库包含了全部.c和.h文件 |
做完这步,工具本身已就绪,接下来关键是搞清楚怎么把待测数据喂给它,并理解运行参数的含义。
4. 使用流程与参数详解
4.1 准备测试数据
NIST STS 测试的单位是二进制序列。它支持两种输入文件格式:ASCII 字符流和二进制位流。ASCII 格式的文件里,每个字节是一个字符'0'或'1',文件内容就是一堆010101...这样的文本。二进制格式则直接按位存放,每 8 个 bit 拼成一个字节,文件大小更小,读取效率也更高。assess会自动识别文件格式,判断依据是文件开头的字节内容是字符 48/49 还是其他二进制值。
为了讲清楚两种格式的区别,我写个 Python 示例,生成 100 万 bit 的测试数据:
import os import random # 方法一:生成 ASCII 字符流 n = 1_000_000 with open("random_ascii.txt", "w") as f: for _ in range(n): f.write(str(random.getrandbits(1))) # 文件大小约 1 MB # 方法二:生成二进制位流 with open("random_bin.dat", "wb") as f: f.write(os.urandom(n // 8)) # 文件大小约 125 KB这里有个细节:当你使用默认的流方式测试时,assess要求输入文件至少包含待测bit长度 × 数据流数量个比特。举个例子,如果你设置 bit length 为 100 万、streams 为 10,那就需要 1000 万 bit 的数据。很多人在这一步报错,提示数据不足,就是因为文件长度不够。一个常见做法是先用dd或 Python 脚本从系统熵源一次性生成足够大的原始数据文件,再按需分割。
4.2 运行 assess 并逐项配置参数
准备好数据文件后,启动测试:
./assess 1000000这里的1000000指每个子序列的比特长度。程序随即进入交互式配置界面,按提示操作即可。我整理了一份参数交互流程,方便你对照着看:
| 交互提示 | 输入值 | 说明 |
|---|---|---|
Provide input (in bits): | 1000000 | 单个测试序列的 bit 数,命令行参数已传入时这里仍会显示 |
Statistical Testing...后询问How many bit streams? | 10 | 要测试的独立序列数量,recommend 不少于 10 |
Input file format: | 0 或 1 | 0 表示二进制位流,1 表示 ASCII 十六进制字节流,这里容易混淆,见下文 |
后续测试项选择,如Select Test (0-15): | 0 | 0 代表执行全套测试,也可以输入具体测试项的编号组合 |
这里最容易被坑的就是Input file format的选择。菜单里的说明文字并不直观,它实际上在问:你的输入文件是原始的二进制 bit 文件,还是用 ASCII 字符表示十六进制数值的文件?我建议统一使用“直接生成的随机 bit 文件”,对应格式选 0。如果你在 Windows 上用文本编辑器新建了文件,里面写了字符串101010,那这个文件其实是 ASCII 字符流,格式要选 1,并且文件内容每个字符必须是'0'或'1',不能有空格和换行。选错格式最典型的症状是:程序跑得很“顺利”,但所有测试项的 P-value 全是 0,或者直接 segment fault。
4.3 修改测试项、运行频率等细节
在Select Test (0-15)这一步,输入不同编号可以选择要执行的测试。编号对应关系如下:
| 编号 | 测试项 | 备注 |
|---|---|---|
| 1 | Frequency Test | 推荐必测 |
| 2 | Block Frequency Test | 推荐必测 |
| 3 | Runs Test | 推荐必测 |
| 4 | Longest Run Ones in a Block | 推荐必测 |
| 5 | Binary Matrix Rank Test | 对序列长度有要求 |
| 6 | DFT Spectral Test | 推荐必测 |
| 7 | Non-overlapping Template Matching | 耗时较长 |
| 8 | Overlapping Template Matching | 耗时较长 |
| 9 | Maurer Universal Statistical Test | 需要足够长序列 |
| 10 | Linear Complexity Test | 对序列长度有要求 |
| 11 | Serial Test | 推荐必测 |
| 12 | Approximate Entropy Test | 推荐必测 |
| 13 | Cumulative Sums Test | 推荐必测 |
| 14 | Random Excursions Test | 需要序列足够长 |
| 15 | Random Excursions Variant Test | 需要序列足够长 |
输入0表示执行所有测试,程序会继续问你是否要调整默认参数,比如块长度、模板长度等等。如果你第一次使用,直接回车接受默认值即可。想批量跑多个样本时,也支持非交互式参数传入,核心命令可以这样组合:
./assess 1000000 -i-i表示忽略交互参数,直接使用默认配置。这个方式适合脚本化调用,但要注意-i模式仍然需要你事先按前面交互流程设置过一次参数,它会把配置缓存到experiments/AlgorithmTesting/目录下的某个配置文件中。
5. 测试结果解读
5.1 finalAnalysisReport.txt 与 summaryReport.txt
测试跑完后,assess会在当前目录生成experiments/AlgorithmTesting/文件夹,里面有两个关键输出:finalAnalysisReport.txt和summaryReport.txt。前者保存的是每个测试项、每个数据流的详细 P-value 列表,后者则是一个压缩版摘要。
finalAnalysisReport.txt的格式大致长这样:
RESULTS FOR THE UNIFORMITY OF P-VALUES AND THE PROPORTION OF PASSING SEQUENCES -------------------------------------------------------------------------------- C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 P-VALUE PROPORTION STATISTICAL TEST 0 1 2 0 1 1 3 1 0 1 0.534146 10/10 Frequency 1 0 1 1 2 1 0 2 1 1 0.739918 10/10 BlockFrequency前面 10 列 C1 到 C10 是 P-value 的分布区间计数。一个合格的结果里,这 10 个区间的计数值应该大致均匀,不会出现某个区间集中了异常多的样本。紧跟着的 P-value 列是均匀性检验的 P-value,最后一列PROPORTION表示通过比例,例如10/10意味着 10 个数据流全部通过该项测试。
5.2 判定标准与细节
判定测试是否通过主要看两个指标:一是每个子序列的 P-value 是否大于 0.01,二是PROPORTION是否落在可接受范围内。对于 10 个数据流的情况,至少应通过 9 个,也就是比例不低于 9/10。如果通过比例过低,基本可以断定待测序列存在明显的随机性缺陷。
还有一点容易被忽略:finalAnalysisReport里的结果只有在“每个测试项都有足够数量的 P-value 参与均匀性计算”时才是可靠的。NIST 文档建议每个测试项至少收集 55 个 P-value,即至少跑 55 个数据流,才能对整体均匀性下结论。我们的日常实践里,如果只是快速自测,10 个流可以接受;但如果是为了出正式报告,至少跑 100 个数据流,每个 100 万 bit。
另外,summaryReport.txt只列出通过/失败的汇总,没有 P-value 明细。我习惯先看 summary,如果某项失败,再去 finalAnalysisReport 里精确查找是哪个数据流拖了后腿。正式交付报告时,两个文件最好都附上。
5.3 报告如何输出到自定义目录
默认输出在experiments/AlgorithmTesting/,但很多项目里你希望在特定目录保存报告。可以手动把experiments路径打包,或者做一个软链接指向自己的数据目录。我常用的做法是在跑测试前先把输出目录切换出来:
mkdir -p /data/rng_test_result ln -sfn /data/rng_test_result experiments/AlgorithmTesting ./assess 1000000 -i这个 symlink 的方式最简单,不用改源码。注意切换目录后,如果跑过多次测试,finalAnalysisReport.txt会被新结果覆盖,如果你需要保留历史记录,每次跑完后立刻重命名备份。
6. 常见问题与排查技巧实录
6.1 编译和运行时的环境类错误
我在多个平台上编译运行过 STS,遇到的环境类问题可以归纳成一张速查表:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
make报错ld: cannot find -lm | 老版本 STS 的 Makefile 缺陷 | 参考 3.1 节的 libm.a 处理办法 |
运行./assess直接segment fault | 内存不足或输入文件格式选错 | 用ulimit -a查看内存限制,确认数据文件格式匹配 |
| 交互界面卡在参数输入阶段 | 终端编码问题 | 把终端语言环境设为 C,即export LANG=C后重跑 |
| Non-overlapping Template 测试耗时异常久 | 序列过长或模板数太多 | 适当减少测试流数量,或用-i默认参数跑 |
这些问题的共同特征是“程序本身没错,但环境和配置不匹配”,排查时把眼光放到输入数据和系统环境上,别一开始就怀疑源码。
6.2 数据格式与测试结果异常
如果测试跑完了但结果异常,先确认数据生成和读取格式一致。我曾经遇到一个案例:同事用openssl rand生成了二进制随机数,但保存文件时用xxd转成了十六进制文本,然后在assess里选格式 1(ASCII 十六进制),导致测试一直报“数据太少”。原因是 ASCII 十六进制格式下,每个字节只解析一个十六进制字符,实际参与测试的 bit 数缩水了一半。
还有一个高频问题:测试所有 P-value 都是 0。这通常不是随机源的问题,而是文件里除了0和1之外还有其他字节,比如换行符\n。解决办法是用管道直接去掉所有空白字符,或者生成数据时确认每行没有换行:
tr -d '\n' < random_raw.txt > random_clean.txt另外,如果你用 Python 的random模块生成测试数据,注意getrandbits(1)在 Python 的全局随机种子下产生的序列质量其实足够高,用来测试工具本身没问题。但正式评估你自己的随机源时,还是应该使用实际待测设备的输出,不要混入系统伪随机数,否则测出来的结果不能代表待测对象的真实质量。
6.3 测试数量与效率的取舍
STS 运行速度不算快,尤其是 Non-overlapping Template 和 Serial 测试,在 100 万 bit、10 个流的情况下,可能需要几分钟到十几分钟。如果跑 100 个流做正式报告,时间会显著拉长,这时候合理规划测试顺序很重要。
我的建议是先只跑核心快速项(Frequency、Block Frequency、Runs、Longest Run、Cumulative Sums),这些测试耗时短,能快速暴露严重缺陷。快速项全部通过后,再跑全套测试,节约时间。命令行里选择测试项时可以直接输入一组编号,比如123456表示只跑前六项,不需要每一项单独确认。
另外,不要在一个体积巨大的文件上反复跑同一套测试。更好的做法是先把大文件切成若干个 100 万 bit 的小文件,再统一批量处理。写个简单的 shell 循环就可以完成:
for i in $(seq 1 10); do head -c 125000 /dev/urandom > sample_${i}.dat ./assess 1000000 -i < sample_${i}.dat cp experiments/AlgorithmTesting/finalAnalysisReport.txt report_${i}.txt done注意我这里用了/dev/urandom做演示,这只是为了说明自动化流程。正式测试必须替换成你自己要验证的随机源输出,否则整份报告只是对 Linux 内核熵池的测试,没有任何工程意义。
在实际项目中,我最看重的其实不是单个样本的 P-value,而是同一个随机源在不同批次数据下结果的稳定性。偶尔一项测试失败可能是统计波动,但同一项测试在多个批次中反复失败,那基本就是随机源存在结构性缺陷。想当年我第一次用这个工具时,以为跑完一遍全是 PASS 就万事大吉,后来把测试流从 10 提到 100,才发现某个自研 PRNG 在串行测试上时好时坏,差点带着隐患上线。这个经验后来也成了我给团队定的规矩:凡是随机性相关模块验收,数据流数量不能少于 100,而且至少要复测三次取一致的结论。希望你在自己的项目里起步时就养成这个习惯,省掉后面返工的麻烦。