熵质量测试

本文档介绍了如何测试用于播种锆石 CPRNG 的熵源的质量。

理论问题

大致说来,有时可以通过识别其中的某种模式来轻松辨别出一串数字不是随机的。我们无法保证这些数字是否确实随机。先进的技术似乎会对数据运行多次统计测试,并希望检测到任何可利用的漏洞。

如果随机数字不是完全随机的(当其分布不均匀,或者序列中数字之间的相关性有限时),测试随机性问题会变得更加困难。不完美的随机数字流仍然包含一些随机性,但很难确定它的随机性。

就我们的目的而言,最小熵可以很好地衡量非完全随机数字流中包含多少随机性。这与信息理论中使用的香农熵相关,但其值始终较小。最小熵控制着我们可以从熵源中可靠地提取多少随机性;如需查看示例,请参阅 https://en.wikipedia.org/wiki/Randomness_extractor#Formal_definition_of_extractors

从实际的角度来看,我们可以使用 US NIST SP800-90B 中所述的测试套件来分析来自熵源的随机样本。https://github.com/usnistgov/SP800-90B_EntropyAssessment 中提供了这些测试的原型实现。该套件将样本数据文件(例如 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 对其进行加密,使用安全系数低的密码且不加盐(当然,这是一种糟糕的加密方案!)。输出看起来是优质的随机数据表明 AES 在按预期运行,但这表明了根据处理后的数据估算熵内容的风险:/dev/zero 和“password”共同提供了约 0 位的熵,但我们的测试对结果数据更为乐观!

如需与 Zircon 相关的更具体示例,请考虑使用抖动熵(http://www.chronox.de/jent/doc/CPU-Jitter-NPTRNG.html 中讨论的 RNG)。抖动熵会从 CPU 时间的变化中获取熵。未处理的数据是指运行特定 CPU 密集型和内存密集型代码块所花费的时间(以纳秒为单位)。当然,这些时间数据并非完全随机:它们以一个平均值为中心,但有一些波动。每个单独的数据样本可能是几个位(例如一个 64 位整数),但所贡献的最小熵不超过 1 位。

完整的抖动熵 RNG 代码会获取多个原始时间数据样本,并将它们处理成单个随机输出(通过通过 LFSR 进行移位等方式)。如果测试经过处理的输出,我们会从实际时间变化和 LFSR 中看到明显随机性。我们希望仅关注时间变化,因此应该测试原始时间样本。请注意,可通过 kernel.jitterentropy.raw cmdline 开启和关闭 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.binentropy.000000000.meta。第一个是从熵源收集的原始数据,第二个是简单的文本文件,其中每行是一个键值对。键是与 /[a-zA-Z0-9_-]+/ 匹配的单个单词,值由与 /[ \t]+/ 匹配的空格分隔。此文件可通过 Bash 中的 read、Python 中的 str.split() 或 C 中的 scanf(对于缓冲区溢出)轻松解析。

在实践中,我很担心这些脚本中的位错误,所以接下来的几个部分会记录脚本应该做什么,从而更轻松地手动运行测试或在脚本损坏时修复这些脚本。

启动时测试:构建

由于启动时熵测试需要永久预留大块内存块(用于 VMM 前的临时缓冲区),因此我们通常不会将熵测试模式内置到内核中。如需启用这些测试,请在构建时传递 ENABLE_ENTROPY_COLLECTOR_TEST 标志,例如,添加一行

EXTERNAL_DEFINES += ENABLE_ENTROPY_COLLECTOR_TEST=1

local.mk。目前,还有一个构建时常量 ENTROPY_COLLECTOR_TEST_MAXLEN,该常量(如果提供)是静态分配的缓冲区的大小。如果未指定,则默认值为 1MiB。

启动时测试:配置

启动时测试通过内核 cmdline 进行控制。相关 cmdline 为 kernel.entropy-test.*,详见 kernel_cmdline.md

某些熵源(尤其是抖动熵)具有可通过内核 cmdline 调整的参数值。同样,请参阅 kernel_cmdline.md,了解更多详情。

启动时测试:正在运行

只要传递了正确的内核 cmdline,启动期间测试就会自动在启动期间运行(如果 cmdline 出现问题,系统将改为输出错误消息)。这些测试将在 RNG 种子的第一阶段(在 LK_INIT_LEVEL_PLATFORM_EARLY 发生)之前运行,此时 VMM 产生堆之前不久。如果运行大型测试,则启动通常会明显变慢。例如,在 rpi3 上从抖动熵收集 128 kB 的数据可能需要大约一分钟时间,具体取决于参数值。

运行时测试

TODO(https://fxbug.dev/42098992):讨论实际的用户模式测试流程

目前的粗略思路:只有内核可以触发 hwrng 读取。为了进行测试,用户空间会发出一条内核命令(例如 k hwrng test),并使用一些参数来指定测试来源和长度。内核会将随机字节收集到位于 /boot/kernel/debug/entropy.bin 的现有由 VMO 支持的伪文件中,并假设这可以安全写入。目前未实现;因缺少用户空间 HWRNG 驱动程序而被屏蔽。可以先测试 VMO 重写机制。

测试数据导出

测试数据保存在被测 Zircon 系统的 /boot/kernel/debug/entropy.bin 中。到目前为止,我通常已经通过 netcp 手动导出数据文件。其他选项包括 scp(如果您使用正确的 Fuchsia 软件包构建)或保存到永久性存储空间。

运行 NIST 测试套件

注意:NIST 测试实际上尚未在 Fuchsia 中镜像。目前,您需要从位于 https://github.com/usnistgov/SP800-90B_EntropyAssessment 的代码库克隆测试。

NIST 测试套件有三个入口点(截至 2016 年 10 月 25 日提交的版本):iid_main.pynoniid_main.pyrestart.py。两个“主”脚本执行大部分工作。iid_main.py 脚本适用于生成独立、同分布的数据样本的熵源。大多数测试用于验证 iid 条件。许多熵源都不会具有 iid,因此 noniid_main.py 测试会实现多个不需要 iid 数据的熵 Estimator。

请注意,NIST 代码库中的测试二进制文件是不含 shebang 行的 Python 脚本,因此在调用它们时,您可能需要在命令行上明确调用 python

前两个脚本采用两个参数,这两个参数都是强制性的:要读取的数据文件和每个样本的有效位数量(如果小于 8,则仅使用每个字节的低 N 位)。它们可以选择接受 -v 标志以生成详细输出,或者接受 -h 以获取帮助。

noniid_main.py 还可以选择接受 -u <int> 标记,该标记可以减少在第二个必需参数中传递的 N 值的位数。我不太清楚为什么提供此标志;这似乎在功能上是多余的,但传递它确实会略微改变详细输出。我最好的猜测是,之所以提供这种测试,是因为非 iid 马尔可夫测试仅适用于最多 6 位的样本,因此对于此测试,7 位或 8 位数据集将缩减至低 6 位。相反,所有 iid 测试都可以在 8 位样本上运行。

iid_main.py 脚本的调用示例:

python2 -- $FUCHSIA_DIR/third_party/sp800-90b-entropy-assessment/iid_main.py -v /path/to/datafile.bin 8

restart.py 脚本接受相同的两个参数,外加第三个参数:上次运行 iid_main.pynoniid_main.py 时返回的最小熵估算值。本文档不介绍重启测试。目前,请参阅 NIST SP800-90B 了解更多详情。

未来路线

自动化

最好能够自动执行构建、配置和运行质量测试的流程。首先,编写 Shell 脚本以执行这些步骤应该非常简单。更好的方法是使用测试基础架构自动运行熵收集器质量测试,这主要是为了减少测试代码中的位损坏。如果自动化失败,我们必须依靠人工定期运行测试(或在测试中断时修复测试)。