本文档介绍了我们如何测试用于为 Zircon CPRNG 提供种子的熵源的质量。
理论方面的担忧
从广义上讲,有时很容易通过识别数字流中的模式来判断该数字流是否随机。我们无法确定这些数字是否真正随机。目前最先进的方法似乎是对数据运行多项统计检验,并希望检测到任何可利用的弱点。
如果随机数并非完全随机(即分布不均匀,或者序列中的数字之间存在一些有限的相关性),那么随机性检验问题会变得更加困难。非完全随机数的流仍包含一些随机性,但很难确定其随机程度。
就我们的目的而言,衡量非完全随机数流中包含多少随机性的一个好方法是最小熵。这与信息论中使用的香农熵有关,但始终取较小的值。最小熵用于控制我们可以从熵源可靠提取的随机性程度;请参阅随机性提取器#提取器的正式定义。
从实践角度来看,我们可以使用美国 NIST SP800-90B 中描述的测试套件来分析熵源的随机样本。有关测试的原型实现,请参阅 NIST SP800-90B 熵评估。该套件将一个示例数据文件(例如,1 MB 的随机字节)作为输入。此测试套件的优点在于,它可以处理不完美的 RNG,并报告随机数据流的每个字节中包含的最小熵的估计值。
测试未处理数据的重要性
从熵源提取熵后,我们会以“安全”的方式将其混合到 CPRNG 中,从而基本上消除熵源原始随机字节流中可检测到的相关性和分布不完美性。在实际生成要使用的随机数时,这样做非常重要,但在测试熵源本身时,我们必须避免这种混合和处理阶段。
为了直观地说明为什么在测试实际熵源时测试未处理的数据非常重要,我们进行了一项实验。它应可在安装了 OpenSSL 的任何现代 Linux 系统上运行。
head -c 1000000 /dev/zero >zero.bin
openssl enc -aes-256-ctr -in zero.bin -out random.bin -nosalt -k "password"
此命令从 /dev/zero 中获取 100 万个字节,通过 AES-256 对其进行加密,使用安全系数低的密码且不使用 salt(当然,这是一种糟糕的加密方案!)。输出看起来像是良好的随机数据,这表明 AES 正在按预期运行,但也说明了从处理后的数据估计熵内容的风险:/dev/zero 和“password”加在一起提供的熵约为 0 位,但我们的测试对生成的数据过于乐观!
如需查看与 Zircon 相关的更具体的示例,请考虑 jitterentropy(CPU-Jitter-NPTRNG 中讨论的 RNG)。Jitterentropy 从 CPU 时序的变化中提取熵。未处理的数据是指运行某个 CPU 和内存密集型代码块所花费的时间(以纳秒为单位)。当然,这些时间数据并非完全随机:它们围绕某个平均值波动。每个单独的数据样本可能为若干位(例如 64 位整数),但仅贡献 1 位或更少的最小熵。
完整的 jitterentropy RNG 代码会获取多个原始时间数据样本,并通过 LFSR(以及其他方式)将它们处理为单个随机输出。如果我们测试处理后的输出,会发现实际时序变化和 LFSR 都会产生明显的随机性。我们只想关注时间变化,因此应测试原始时间样本。请注意,可以通过 kernel.jitterentropy.raw 命令行开启和关闭 jitterentropy 的内置处理。
质量测试实现
如上所述,NIST 测试套件将包含随机字节的文件作为输入。我们在 Zircon 系统(可能在顶部有一个精简的 Fuchsia 层)上收集这些字节,然后通常将它们导出到功能更强大的工作站来运行测试套件。
启动时测试
我们的某些熵源是在启动期间(在用户空间启动之前)读取的。
为了在真实环境中测试这些熵源,我们在启动期间运行测试。相关代码位于 kernel/lib/crypto/entropy/quality\_test.cpp 中,但基本思路是,内核在前期启动期间(在 VMM 启动之前,因此在无法分配 VMO 之前)分配一个大型静态缓冲区来保存测试数据。之后,数据会被复制到 VMO 中,然后 VMO 会传递给 userboot 和 devmgr,并在 /boot/kernel/debug/entropy.bin 中显示为伪文件。用户空间应用可以读取此文件并导出数据(例如,通过复制到持久性存储空间或使用网络)。
从理论上讲,您应该能够使用 scripts/entropy-test/make-parallel 构建启用了熵收集器测试的 Zircon,然后应该能够使用脚本 scripts/entropy-test/run-boot-test 运行单个启动时测试。run-boot-test 脚本主要用于被其他脚本调用,因此在某些方面还不够完善(例如,其大多数实参都是通过命令行选项(如 -a x86-64)传递的,但许多此类“选项”实际上是必需的)。
假设 run-boot-test 脚本成功运行,它应该会在输出目录中生成两个文件:entropy.000000000.bin 和 entropy.000000000.meta。第一个是收集自熵源的原始数据,第二个是一个简单的文本文件,其中每行都是一个键值对。键是与 /[a-zA-Z0-9_-]+/ 匹配的单个字词,值以与 /[ \t]+/ 匹配的空格分隔。此文件可以通过 Bash 中的 read、Python 中的 str.split() 或 C 中的 scanf(请注意缓冲区溢出问题)轻松解析。
实际上,我担心这些脚本中会出现位腐烂,因此接下来的几个部分将记录脚本应该执行的操作,以便在脚本损坏时更轻松地手动运行测试或修复脚本。
启动时测试:构建
由于启动时熵测试需要永久预留大块内存(用于临时的预 VMM 缓冲区),因此我们通常不会将熵测试模式构建到内核中。通过在 build 时传递 ENABLE_ENTROPY_COLLECTOR_TEST 标志来启用测试,例如,通过添加以下行
EXTERNAL_DEFINES += ENABLE_ENTROPY_COLLECTOR_TEST=1
到 local.mk。目前,还有一个 build-time 常量 ENTROPY_COLLECTOR_TEST_MAXLEN(如果提供),它是静态分配的缓冲区的大小。如果未指定,则默认值为 1 MiB。
启动时测试:配置
启动时测试通过内核命令行控制。相关命令行是 kernel.entropy-test.*,记录在内核命令行中。
某些熵源(尤其是 jitterentropy)的参数值可以通过内核命令行进行调整。同样,如需了解更多详情,请参阅内核命令行。
启动时测试:正在运行
只要传递了正确的内核 cmdline,启动时间测试就会在启动期间自动运行(如果 cmdline 存在问题,系统会改为输出错误消息)。测试在 RNG 初始化的第一阶段(在 LK_INIT_LEVEL_PLATFORM_EARLY 发生)之前运行,该阶段发生在 VMM 堆被启动之前不久。如果运行大型测试,启动速度通常会明显变慢。例如,从 rpi3 上的 jitterentropy 收集 128kB 的数据可能需要大约一分钟,具体取决于参数值。
运行时测试
当前粗略的想法:只有内核可以触发 hwrng 读取。为了进行测试,用户空间会发出内核命令(例如 k hwrng test),并附带一些参数来指定测试源和长度。内核将随机字节收集到 /boot/kernel/debug/entropy.bin 处的现有 VMO 支持的伪文件中,并假设该文件是可安全写入的。目前未实现;因缺少用户空间 HWRNG 驱动程序而被阻止。可以先测试 VMO 重写机制。
测试数据导出
测试数据保存在受测 Zircon 系统中的 /boot/kernel/debug/entropy.bin 中。到目前为止,我通常通过 netcp 手动导出数据文件。其他选项包括:如果您使用正确的 Fuchsia 软件包进行构建,则为 scp;或者保存到持久性存储空间。
运行 NIST 测试套件
NIST 测试套件有三个入口点(截至 2016 年 10 月 25 日提交的版本):iid_main.py、noniid_main.py 和 restart.py。这两个“主要”脚本会完成大部分工作。iid_main.py 脚本适用于生成独立同分布数据样本的熵源。大多数测试都是为了验证 iid 条件。许多熵源不是 iid,因此 noniid_main.py 测试实现了几种不需要 iid 数据的熵估计器。
请注意,NIST 代码库中的测试二进制文件是不含 Shebang 行的 Python 脚本,因此在调用这些脚本时,您可能需要在命令行中显式调用 python3。
前两个脚本需要两个实参,这两个实参都是必需的:要读取的数据文件,以及每个样本的有效位数(如果小于 8,则每个字节仅使用低 N 位)。它们可以选择接受 -v 标志以生成详细输出,或接受 -h 标志以获取帮助。
noniid_main.py 还可以选择接受 -u <int> 标志,该标志可以减少第二个必需实参中传递的 N 值以下的位数。我不完全确定为何提供此标志;它在功能上似乎是冗余的,但传递它确实会略微改变详细输出。我猜测,之所以提供此数据,是因为非 iid 马尔可夫检验仅适用于最多 6 位的样本,因此对于 7 位或 8 位的数据集,此检验会将其缩减为低 6 位。相比之下,所有 IID 测试都可以在 8 位样本上运行。
iid_main.py 脚本的调用示例:
python3 -- $FUCHSIA_DIR/third_party/sp800-90b-entropy-assessment/iid_main.py -v /path/to/datafile.bin 8restart.py 脚本采用相同的两个实参,外加第三个实参:之前运行 iid_main.py 或 noniid_main.py 返回的最小熵估计值。本文档未介绍重新开始测试。目前,请参阅 NIST SP800-90B 了解更多详情。
未来方向
自动化
最好能自动执行构建、配置和运行质量测试的流程。首先,应能轻松编写 shell 脚本来执行这些步骤。更好的做法是使用测试基础架构自动运行熵收集器质量测试,这主要是为了减少测试代码中的位腐烂。如果自动化失败,我们就必须依靠人工来定期运行测试(或在测试中断时修复测试)。