抖动熵:调整配置

Jitterentropy 库由 Stephan Mueller 编写,在 https://github.com/smuellerDD/jitterentropy-library 上提供,在 http://www.chronox.de/jent.html 上提供记录。在 Zircon 中,它用作简单的熵源来为系统 CPRNG 播种种子。

关于抖动熵的基本配置选项的配套文档介绍了从根本上影响抖动熵运行方式的两个选项。本文档将介绍用于控制抖动速度及其收集到的熵的数值参数,但并未从根本上改变其操作原则。此外,还介绍了如何测试各种参数以及在输出中查找的内容(例如,是添加对新设备的支持,还是更全面地优化参数)。

抖动参数的简述

以下可调参数用于控制抖动熵的运行速度及其收集熵的速度:

kernel.jitterentropy.ll

ll”表示“LFSR 循环”。抖动熵会在生成噪声的过程中,使用 LFSR(故意使 LFSR 实现效率低下)来操控 CPU。内循环使 LFSR 移动 64 次;外循环重复 kernel.jitterentropy.ll 次。

根据我的经验,LFSR 代码会显著减慢抖动熵,但并不会生成很多熵。我在 RPi3 和 qemu-arm64 上进行了测试,结果在性质上类似,但尚未在 x86 上进行测试。在调谐时需要考虑这一点:使用较少的 LFSR 循环往往会提高整体性能。

请注意,设置 kernel.jitterentropy.ll=0 会导致抖动熵以“随机”方式选择 LFSR 循环的数量。如基本配置文档中所述,我不建议使用 kernel.jitterentropy.ll=0

kernel.jitterentropy.ml

ml”表示“内存访问循环”。抖动熵会遍历中等大的 RAM 块,并读取和写入每个字节。分块的大小和访问模式由以下两个参数控制。内存访问循环会重复 kernel.jitterentropy.ml 次。

根据我的经验,内存访问循环是原始熵的良好来源。同样,到目前为止 我只在 RPi3 和 qemu-arm64 上测试过

kernel.jitterentropy.ll 非常相似,如果您设置了 kernel.jitterentropy.ml=0,那么抖动将为内存访问循环计数选择一个“随机”值。我也建议不要这样做。

kernel.jitterentropy.bs

bs”表示“块大小”。Jitterentropy 将其 RAM 块划分成此大小的块。内存访问循环从块 0 的字节 0 开始,接着是块 1 的“字节 -1”(实际上是块 0 的最后一个字节),接着是块 2 的“字节 -2”(即块 1 的倒数第二个字节),依此类推。此模式可确保每个字节都会命中,并且大多数访问会进入不同的块。

我通常根据缓存行的大小使用 kernel.jitterentropy.bs=64 测试了抖动熵。我尚未测试在某些/所有平台上是否有更好的选择。

kernel.jitterentropy.bc

bc”表示“块数”。抖动熵会在其内存访问循环中使用这么多的 RAM 块,每个块的大小均为 kernel.jitterentropy.bs

由于我选择 kernel.jitterentropy.bs=64,因此通常选择 kernel.jitterentropy.bc=1024。这意味着使用 64KB 的 RAM,这足以使 L1 缓存溢出。

jent_memaccess 前面的注释中的 jitterentropy 源代码建议选择块大小和计数,以便使用的 RAM 大于 L1。令人困惑的是,上行抖动熵中的默认值(块大小 = 32,块数 = 64)不够大,无法溢出 L1。

调整过程

其基本原理很简单:在特定的目标设备上,尝试为参数设置不同的值。为每个参数集收集大量数据(理想情况下为 1MB 左右),然后运行 NIST 测试套件来分析数据。 确定哪些参数每单位时间的熵最佳。绘制熵样本所用的时间会记录在受测系统上。

一项复杂功能是内置于抖动熵的启动测试。在执行一些基本分析(主要是确保时钟是单调的并且具有足够高的分辨率和可变性)之后,这将绘制并舍弃 400 个样本。对于每组参数,更为准确的测试会重新启动两次:一次是为了收集大约 1MB 的数据进行分析,另一次是为了使用“适当”的熵(根据第一阶段的熵估算值计算得出,并采用适当的安全外边距等方法)启动了两次。请参阅“确定 entropy_per_1000_bytes 统计信息”。测试的第二阶段会模拟真实的启动,包括启动测试。完成第二阶段后,选择启动最快的参数集。当然,每个阶段的测试应重复几次以减少随机变化。

确定 entropy_per_1000_bytes 统计信息

kernel/lib/crypto/include/lib/crypto/entropy/collector.h 中的 crypto::entropy::Collector 接口需要其实例化中的参数 entropy_per_1000_bytes。与 jitterentropy 相关的值目前硬编码在 kernel/lib/crypto/entropy/jitterentropy_collector.cpp 中。此值旨在测量抖动产生的数据的每个字节中包含多少最小熵(由于字节不是独立且均匀分布的,因此这将小于 8 位)。“每 1000 个字节”部分只是让您指定熵的小数位,例如“0.123 位 / 字节”,而无需进行小数运算(因为内核代码中不允许使用 float,定点算术会令人感到困惑)。

应通过使用 NIST 测试套件分析随机数据样本来确定该值,如熵质量测试文档中所述。该测试套件可生成最小熵的估算值;在我的经验中,同一 RNG 的重复测试的变化幅度为十分之一(当熵值大约为每字节数据 0.5 位时,这一点非常显著!)。从测试套件中获得良好且一致的结果后,应用安全系数(例如,将熵估算值除以 2),并更新 entropy_per_1000_bytes 的值(请记得乘以 1000)。

请注意,最终可能应该在某个位置配置 entropy_per_1000_bytes,而不是在 jitterentropy_collector.cpp 中对其进行硬编码。内核 cmdline 甚至预处理器符号都可以运行。

关于测试脚本的备注

scripts/entropy-test/jitterentropy/test-tunable 脚本可自动处理大型测试矩阵的循环。其缺点是,测试会在一台机器上按顺序运行,因此 (1) 错误会阻碍测试流水线,因此需要监督;(2) 机器会不断地重新启动而不是冷启动(再加上是 netboot-reboot),这很可能使测试混淆。尽管如此,它还是比手动一千次手动关闭电源/开启电源还好!

一些快乐的备注:

  1. 在 netboot 中,该脚本在等待 netcp 成功导出数据文件时使 bootserver 保持开启状态。如果系统挂起,您可以将其关闭再重新打开,现有的启动服务器进程将重启失败的测试。

  2. 如果测试要运行(假设)16 个参数组合,每次运行 10 次,结果将如下所示:

    测试 # 0: ml = 1 ll = 1 b ll = 1 b ll = 1 b ll = 1 b ll = 1 b ll = 1 b ll = 1 b ll = 1 1 ml = 1 ll = 1 bc = 1 bs = 64 测试 # 2: ml = 1 ll = 1 bc = 32 bs = 1 test

    (输出文件从 0 开始编号,所以我在上面从 0 开始。)

    因此,如果测试 17 失败,您可以删除测试 16 和 17,然后重新运行每个测试的 9 次迭代。您至少可以保留第一次迭代的完整结果。理论上,测试可以更智能,也可以保留测试 16 中的现有结果,但当前的 Shell 脚本就没有那么复杂。

这些脚本不会像我在上面的“调整过程”部分中建议的两阶段进行。这当然是可行的,但现有的脚本并不具备那么复杂。

开放性问题

我们有多信任低熵极端值?

我们有可能通过选择较小的参数值来尽可能提高每单位时间的熵。当然,最极端的地方是 ll=1, ml=1, bs=1, bc=1,但即使是像 ll=1, ml=1, bs=64, bc=32 这样的内容,也是我所想的示例。其中部分顾虑在于测试套件中的可变性:如果假设测试在每个字节的熵不超过 0.2 位以内,而如果它们报告的每个字节报告的是 0.15 位的熵,那么我们该如何解释?希望连续运行同一测试几百次可以得到明确的模态值,但依赖这个低估值的准确性仍然存在一定的风险。

NIST 出版物指出(第 1302 行,第 35 页,第 2 份草稿)指出 Estimator“当每个样本的熵大于 0.1 时效果良好”。这个值相当低,因此在实践中应该不会造成问题。尽管如此,存在下限这一事实意味着我们可能应该在它周围留下一个相当保守的信封。

“基于设备”的参数是否是最佳参数选择?

显然,在不同架构或不同硬件上,实际的“每字节熵位数”指标存在显著差异。大多数系统是否在使用类似的参数值时都是最优的(这样我们只需将这些值硬编码到 kernel/lib/crypto/entropy/jitterentropy_collector.cpp 中)有可能吗?或者,我们是否需要将参数放入 MDI 或预处理器宏,以便我们针对每个平台(或合适的任何粒度级别)使用不同的默认值。

我们甚至可以以足够精细的方式记录最优参数吗?

我在前面提到过,但我们的目标之一是“x86”,即在任何 x86 PC 上运行。当然,x86 PC 也有很多可能。即使我们在 build 中添加了 JITTERENTROPY_LL_VALUE 等预处理器符号并在 kernel/project/target/pc-x86.mk 中进行了自定义,我们能否为所有 PC 选择一个合适的值?

如果不能,有哪些选择?

  1. 我们可以基于在运行时可访问的值(例如确切的 CPU 型号、核心内存大小、缓存行大小等)存储对照表。这看起来相当笨重。或许,如果我们能找到一两个简单属性(例如“CPU 核心频率”和“L1 缓存大小”),可能会使它相对不太糟糕。

  2. 我们可以尝试一种自适应方法:监控熵流的质量,并根据情况实时调整参数。如果我们想要信任它,需要进行大量测试并获得理由。

  3. 在大多数设备上,我们可以接受“足够好”的参数,并可以选择通过内核 cmdline 或类似机制进行调优。这似乎是我最有可能得到的结果。我预计“足够好”的参数将易于找到,并且不会造成干扰足以证明使用极端解决方案。