前言

关于随机数测试,有两份比较常用的标准,一份是国密局的随机数检测规范,一份是NIST的测试标准

国密局标准参看GB/T 32915,NIST标准可以参看 https://nvlpubs.nist.gov/nistpubs/legacy/sp/nistspecialpublication800-22r1a.pdf

两者大体上一致,存在几个用例的区别,另外NIST除了统计P值总数,还统计P值的分布

NIST还提供了一个测试程序,可以从这里获取 https://csrc.nist.gov/projects/random-bit-generation/documentation-and-software

关于随机数

随机数特性

随机性

一个理想的随机二进制比特流,可由一个完全"公正无偏"的通过抛掷正反面得到,其中正面记为0,反面记为1。每一掷得到0和1的概率都是确切的1/2。而且每一次的结果之间都相互独立,即先前的结果不会对未来的结果产生影响

使用像“公正硬币”这样的理想随机数发生器是不实际的,但是这样一个理想的发生器产生的输出可以作为我们测试评估其他随机数发生器的基准。

不可预测性

加密应用的随机数需要不可预测。对于伪随机数发生器,在不知道种子的情况下,就算知道先前的随机数序列也应该无法预测接下来输出,也即前向不可预测性。同时后向不可预测性也是要求的,即无法通过已知的产生的值,得出种子。

由于伪随机数算法往往已知,确保前向不可预测性,在得到种子的方法上需要注意,种子本身要不可预测。

产生方法

RNG(随机数发生器)

使用熵源和熵蒸馏函数。其中熵蒸馏函数用来克服熵源的任何非随机性缺陷。熵源一般基于物理现象由硬件产生,得到真随机数。特别地,像我们即将评估的量子随机数发生器,就是利用了半导体的量子效应。

RNG产生的结果可以直接作为随机数使用,但前提是已经满足了严格的随机性标准。也可以将得到的结果再喂给PRNG,或者将几个PNRG的结果进行结合,以提高随机数的随机性。

PRNG(伪随机数发生器)

提供输入的种子,通过数值运算,得到伪随机数。伪随机数发生器的输出是种子的一个确定性结果,即喂给相同的种子的情况下输出永远一样。这也是为什么称之为“伪随机数”的原因。所以输入的种子本身需要满足随机性和不可预测性。

伪随机发生器便于重现,产生速度快,有其自身的优点。

测试

随机数的测试,基于随机性假设设计测试用例进行统计性的验证。每一个统计测试用例用来验证一个特定的假设。根据测试的统计结果决定是假设成立还是不成立。在随机性假设下,统计量会满足一个参考分布,从这个参考分布上确定一个临界值(比如99%)。测试的时候,将测试结果同这个临界值进行比较,如果通过小于临界值则认为假设成立,反之则认为不成立。

考虑到假设不成立本身的概率很小,需要测试多个样本(至少在alpha值倒数的量级),通过统计所有样本的结果,最终决定测试是否通过。

可能的统计测试是无穷的,评估任何一种特定的pattern是否存在。没有那个有穷的测试集合是完备的,所以在测试结果的解释上要谨慎。

测试用例

频数检测

目的是检测待测试二进制序列中,“0”和“1” 数目是否近似相等。如果是,则认为序列是随机的。

块内频数检测

目的是确定在待测序列中,所有非重叠的长度为M位的块内的“0”和“1”的数目是否表现为随机分布。如果是,则序列是随机的。

游程检测

目的是确定待测序列中,总的游程数目是否如真随机序列期望的那样。如果是,则序列是随机的。

块内最长游程检测

目的是确定待测序列中,最长“1”游程的长度是否与真随机序列中最长“1”游程的长度近似一致。如果是,则序列是随机的。

矩阵秩检测

目的是检测待测序列中,固定长度子序列的线性相关性。如果线性相关性较小,则序列是随机的。

离散傅里叶变换检测

目的是通过检测待测序列的周期性质,并与真随机序列周期性质相比较,通过它们之间的偏离程度来确定待测序列随机性。如果偏离程度较小,序列是随机的。(超过95%阈值的峰数是否显著异于5%)

非重叠模板匹配检测

目的是检测待测序列中,子序列是否与太多的非重叠模板相匹配。太多就意味着待测序列是非随机的。

重叠模板匹配检测

目的是统计待测序列中,特定长度的连续“1”的数目,是否与真随机序列的情况偏离太大。太大是非随机的。

通用统计检测

目的是检测待测序列是否能在信息不丢失的情况下被明显压缩。一个不可被明显压缩的序列是随机的。(匹配pattern(压缩序列长度相关的度量)之间的比特数)

线性复杂度检测

目的是确定待测序列是否足够复杂,如果是,则序列是随机的。

重叠子序列检测

目的是确定待测序列所有可能的m位比特的组合子串出现的次数是否与真随机序列中的情况近似相同,如果是,则序列是随机的。每个pattern的概率相等

近似熵检测

目的是通过比较m位比特串与m+1位比特串在待测序列中出现的频度,再与正态分布的序列中的情况相对比,从而确定随机性。

累加和检测

目的确定待测序列中的部分和是否太大或太小。太大或太小都是非随机的。

随机游走检测

目的是确定在一次随机游走过程中,某个特定状态出现的次数为K的cycle个数是否远远超过真随机序列中的情况。如果是,则序列是非随机的。

随机游走变量检测

目的是检测待测序列中,某一特定状态在一个游走过程中出现的总次数与真随机序列的偏离程度。如果偏离程度较大,则序列是非随机的。

>以下几个国密特有

游程分布检测

目的是确定待测序列中,游程的分布情况是或否与真随机序列近似。如果偏离程度较大,则序列是非随机的。

扑克检测

目的是检测待测序列中,子序列是否与太多的非重叠模板相匹配。太多就意味着待测序列是非随机的

二元推导检测

将初始序列中相邻两比特依次做异或操作得到新序列,这样在第k次二元推导的序列中0和1的数量是否接近一致。如果与真随机序列相比接近,则认为是随机的。

自相关检测

检测待测序列与将其自身逻辑左移d位后的新序列的关联程度,如果关联程度与真随机序列相比偏高,则认为是非随机的。

测试步骤

准备测试数据

使用待测的(伪)随机数发生器,产生足够长度的随机数序列。(样本参数见附录)

在NIST的测试源码中,添加4个国密特有的测试用例

扑克测试,游程分布测试,二元推导测试,自相关测试

编译源码得到测试程序

在sts-2.1.2目录下make即可

运行测试程序,选择待测试数据,按照NIST测试用例和测试参数进行设置

执行测试程序,$ ./assess 1000000
2. 输入0,选择从文件读入随机数
3. 输入随机数路径及文件名,例如data/data1.bin
4. 输入0,选择NIST测试模式
5. 输入2,选择测试所有NIST用例
6. 输入0,NIST用例使用默认参数即可(如果需要修改参数输入相应的编号进行修改)
7. 输入1000,选择测试的样本数量
8. 输入1,选择输入的文件类型为二进制(选择0表示全是01的ASCII文件)
9. 按回车即开始测试

测试结束之后,分析测试结果,看是否满足预期要求

运行测试程序,选择待测试数据,按照国密规范选择对应测试用例,并进行参数设置

执行测试程序 $ ./assess 1000000
2. 输入0,选择从文件读入随机数
3. 输入随机数路径及文件名,例如data/data1.bin
4. 输入1,选择国密测试模式,固定为国密的测试参数
5. 输入3,选择测试所有GM用例
6. 输入1000,选择测试的样本数量
7. 输入1,选择输入的文件类型为二进制(选择0表示全是01的ASCII文件)
8. 按回车即开始测试

测试结果之后,分析测试结果,看是否满足预期要求

结果分析

通过测试的样本比例(NIST标准)

根据样本总个数和显著性水平,计算判定用例通过的最小样本个数
对于样本数为1000,显著性水平为0.01的情况,通过的比例需要在0.98以上
即1000的样本需要有980个通过。

P值均匀分布(仅NIST)

对每个测试样本的P值结果,按照0.1的区间间隔进行数量统计,计算P值统计结果的P值。
如果最终的P值大于0.0001,则认为是均匀分布的。

附录

国密和NIST随机性测试参数设置表

序号 检测项目 国密参数 NIST参数
1 块内频数测试 m=100 m=128
2 扑克测试(国密) m=4; m=8 /
3 重叠子序列测试 m=2; m=5 m=16
4 块内最长游程测试 m=10000 m=10000
5 二元推导测试(国密) k=3; k=7 /
6 自相关测试(国密) d=1, 2, 8, 16 /
7 矩阵秩测试 M=Q=32 M=Q=32
8 近似熵测试 m=2; m=5 m=10
9 线性复杂度测试 m=500 m=500
10 非重叠模板测试(NIST) / m=9
11 重叠模板测试(NIST) / m=9
12 通用测试 L=7,Q=1280 L=7,Q=1280

显著性水平α=0.01

样本参数

参数
样本长度 10^6 bits
样本个数 1000

随机数样本生成方法

openssl随机数样本生成方法(1.0.2o)

openssl rand -out data.openssl 1000000000

用C语言中rand函数生成随机数样本(glibc版本2.27)

./c_rand 1024 data.c_rand 1000000000

其中c_rand程序的源码c_rand.c如下

#include <stdlib.h>
#include <stdio.h>

#define buffer 1024

int main(int argc, char *argv[])
{
	int j, r, nbytes;
	unsigned int seed;
	FILE* fp = NULL;
	size_t res = 0;
	if (argc != 4) {
	fprintf(stderr, "Usage: %s <seed> <rand_file> <nbytes>
", argv[0]);
	return -1;
 	}

	seed = atoi(argv[1]);
	nbytes = atoi(argv[3]);
	srand(seed);

	unsigned char* out = new unsigned char[buffer];
	unsigned char* cur = out;
	if (!(fp = fopen(argv[2], "wb")))
	{
 		printf("file %s open fail!", argv[2]);
 		delete[] out;
 		return -1;
	}

	for (j=0;j<nbytes; ++j) {
		r =  rand()%256;
		*cur++ = *((unsigned char*)(&r));
		if((!(( j + 1 ) % buffer)) || ( j + 1 == nbytes))
		{
			size_t num = j % buffer + 1;
      		res = fwrite(out, 1, num, fp);
      		if (res != num)
      		{
        		fclose(fp);
        		delete[] out;
        		printf("file %s write fail!", argv[2]);
        		return -1;
			}
      			cur = out;
		}
	}
		fclose(fp);
		delete[] out;
		return 0;
}

采集握手中ClientHello随机数

生成随机数命令:

openssl s_server -engine ./engine.so -cert test/rsa-ext.pem -key test/rsa.key -CAfile ca/rsa-ca.pem -www -accept 8888

./ssl -h 192.168.1.10 -p 8888 -s 128000000 -f binary -o data.handshake -m tls1_2

采集硬件加密卡生成的随机数样本

前置条件:完成硬件安装,驱动安装,编译openssl 引擎得到engine.so

openssl rand -engine ./engine.so -out data.cryptocard 1000000000