抖动 (Jitterentropy):基本配置

抖动库由 Stephan Mueller 编写,位于 https://github.com/smuellerDD/jitterentropy-library;如需查看相关说明,请访问 http://www.chronox.de/jent.html.在 Zircon 中,它被用作简单的熵 为系统 CPRNG 播种。

本文档介绍并分析了 Cloud Storage 的两个(独立的)配置选项, 抖动:

  1. 是否在噪声中使用可变的伪随机迭代次数 生成函数。
  2. 是否使用抖动的内部抖动对原始噪声样本进行后处理 处理例程。

之所以考虑这些基本配置选项,是因为 使用抖动方法的过程。我把它们与可调参数对比 (例如,如果未选择循环计数,则使用精确值 或内部使用的暂存内存大小 抖动),因为可调参数不会对 即抖动收集熵的大小 。

我在本文档的最后就是了完整的结论,但总的来说,我认为 我们应避免在选择伪随机迭代编号和使用 抖动后处理数据。

抖动的简要说明

该作者的文档以 HTML 形式提供,网址为: http://www.chronox.de/jent/doc/CPU-Jitter-NPTRNG.html,或 PDF 格式: http://www.chronox.de/jent/doc/CPU-Jitter-NPTRNG.pdf.简而言之,该库 从 CPU 指令时间变化中收集随机位。

抖动会维持随机状态,即一个 64 位的数字, 受许多抖动函数的影响,最终可用作 输出随机性。

有两个噪声源,它们都是运行速度相对较慢的区块 用于测量精确运行时的代码(使用系统时钟,大约需要 纳秒分辨率)。完成这些代码块的确切时间将 变化。我们会对这些时间进行测试,以确保其不可预测;而不能 测试结果(包括 令人鼓舞。但请注意,本文档的目的并非 证明我们估计的抖动样本中的最小熵是合理的,不过, 讨论上面列出的两种配置选项

第一个用作噪声源的代码块是 CPU 密集型 LFSR 循环,在 jent_lfsr_time 函数。 LFSR 逻辑的重复次数由 kernel.jitterentropy.ll cmdline(“ll”代表“LFSR 循环”)。如果为 ll = 0, 使用伪随机计数,否则使用 ll 的值。 查看源代码,发现外部循环根据 ll 参数。内部循环将 LFSR 前进 64 步,每次执行 XOR 运算 最近的时间样本中的一个位。将时间样本传递至 通过这种方式,LFSR 起到一个处理步骤,通常 随机时间步。如 熵质量测试文档中,请务必注意 测试 CPU 时间的熵内容时,跳过此处理 变体。同样的情况是,启用处理功能会增加 有时可能会出现可疑量的熵估算(请参阅 “处理原始样本的影响”部分)。

第二个噪声源是内存访问循环, jent_memaccess 函数。 根据 kernel.jitterentropy.ml 重复执行内存访问循环 cmdline(“ml”表示“内存循环”),其中值 0 同样会激活 伪随机循环计数,并且任何非零值会覆盖伪随机值 计数。实际内存访问循环的每次迭代都会读取和写入 相对较大的内存块,分为 kernel.jitterentropy.bc 对很多 每个块的大小为 kernel.jitterentropy.bs 个字节。在我加载对象时 为 bc = 1024bs = 64;最新的默认值 均应记录在 cmdline 文档。为便于比较, 抖动源代码为 bc = 64bs = 32此处定义。 根据 jent_memaccess 函数上方的注释,总内存大小 应大于目标计算机的 L1 缓存大小。令人困惑 bc = 64bs = 32 产生的内存大小为 2048 字节, 甚至比大多数 L1 缓存要小(我找不到任何具有超过 0 个字节的 CPU) 但小于 4KB)。使用 bs = 64bc = 1024 会生成 64KB 该内存通常足以溢出 L1 数据缓存。

选项 1:伪随机循环计数

抖动最初设计为两个噪声生成函数 运行伪随机数的次数。具体而言, jent_loop_shuffle 函数 混用 (1) 从高分辨率时钟读取的时间和 (2) 抖动的内部随机状态,以此来决定运行次数 噪声来源。

我们添加了覆盖这些伪随机循环计数的功能,并测试了 抖动性能。结果如下: 我们将在本单元后面的部分 “伪随机循环计数的影响”部分, 但总而言之:统计测试表明,伪随机循环 计数增加的熵远远超出预期。这让我不信任 这些较高的熵计数,因此建议使用较低的估计值, 优先考虑确定性循环计数而非伪随机。

抖动的随机数据处理

如上所述,抖动可以处理其随机数据, 数据看起来“更加随机”。具体而言,处理应该减少(并且 最好去掉)随机数据与均匀分布的偏差, 并减少(理想情况下,移除)随机字节之间的任何互相关关系。

生成已处理样本的主要函数是 jent_gen_entropy、 该方法被 jent_read_entropy 来生成任意大数量的随机字节。 实质上,jent_gen_entropy 会在循环中调用 64 次噪声函数。 jent_lfsr_time 的 64 次调用每次都会混合噪声时间测量结果 转换为抖动随机状态。

经过这 64 次迭代后,随机状态会变为“搅拌”在 jent_stir_pool 通过使用“混合器”进行 XOR 运算值而变化,这取决于 状态。如源代码中所述,此操作无法增加或减少 池中的熵(因为 XOR 是双射的),但有可能改进 随机状态的统计外观。

原则上,调用噪声源函数 64 次应产生 64 次 倍的熵,最多为随机状态可以存储的最大 64 位。 这假设 jent_lfsr_time 中的混合操作是以加密方式进行的 声音。我不是密码分析方面的专家,但 LFSR 本身并不是 具有加密安全性的 RNG,因为 64 个连续位会显示整个状态, 64 位 LFSR 之后,所有过去和未来的值都可以 计算。我不确定抖动方案 — 对时间进行 XOR 运算 衡量结果与 LFSR 的比例进行比较, 安全。未经仔细的加密检查, 我知道可能存在这种现象,但我没有在抖动图中看到它 文档),我倾向于使用未处理的样本, 以已知良好的方式(例如我们现在使用的 SHA-2)导入系统熵池中。

尽管如此,我确实对已处理的数据样本运行了 NIST 测试套件。我的 结果在 “处理原始样本的影响”部分) 。

测试流程

如需了解运行熵源质量测试的过程,请参阅 熵质量测试文档

这些初步结果是基于 Raspberry Pi 上的 Zircon 调试 build 收集的 3,根据现已作废的项目中的 18358de5e90a012cb1e042efae83f5ea264d1502 条款构建而成:https://fuchsia.googlesource.com/zircon/+/a1a80a6a7d “[virtio][entropy] 基本 virtio-rng 驱动程序”。在 我的 local.mk 文件:

ENABLE_ENTROPY_COLLECTOR_TEST=1
ENTROPY_COLLECTOR_TEST_MAXLEN=1048576

我使用 以下内核 cmdline,更改 $ML$LL$RAW 的值:

kernel.entropy-test.src=jitterentropy
kernel.jitterentropy.bs=64
kernel.jitterentropy.bc=1024
kernel.jitterentropy.ml=$ML
kernel.jitterentropy.ll=$LL
kernel.jitterentropy.raw=$RAW

测试结果和分析

伪随机循环计数的影响

原始数据

按照抖动源代码中的逻辑(搜索 MAX_FOLD_LOOP_BITMAX_ACC_LOOP_BIT) 伪随机循环计数会在以下范围内变化:

ml: 1 .. 128 (inclusive)
ll: 1 .. 16 (inclusive)

在本课中,我添加了来自 NIST 套件的整体最小熵估算值, 和两个影响估计值:压缩估计值和 马尔可夫估计值。NIST 最小熵估计值是 10 包括这两个指标。压缩估算通常是指 具有确定性循环计数的抖动原始样本的最小值,以及 对于其他噪声图像, 配置。

ml ll 最小熵(位 / 字节) 预计压缩率 马尔可夫估计值
随机 (1 .. 128) 随机 (1 .. 16) 5.77 6.84 5.77
128 16 1.62 1.62 3.60
1 1 0.20 0.20 0.84

换句话说,改变循环计数会伪随机地增加最小熵 原始样本的估计值(与确定性样本相比) 始终使用伪随机范围中的最大值的版本。

分析

伪随机循环计数值通过额外增加一次时间来确定 每个噪声函数的样本。首先,这些时间样本与 噪声函数时间测量值,因为循环计数时间之间的差距 样本与噪声函数时间测量值可预测地对应。作为 那么就很不容易想到 输出数据的最小熵。第二,认为 循环计数时间样本在某种程度上约为噪声函数的 250%。 因为两者均依赖于相同的噪声来源,只不过 随机抽取, 启动系统以运行测试所需的时长。

因此,我怀疑所发生的情况是伪随机循环 足以“迷惑”一组特定的统计测试和 基于预测器的测试,但使用 关于如何推算抖动伪随机循环计数的具体知识 实际上都可以更准确地预测输出结果。我认为“真正的” 伪随机循环计数测试中的最小熵,以应对 具体针对我们的代码,在两个确定性 也就是大约 0.20 到 1.62 位/字节。

使用伪随机计数方法会迫使我们做出额外的决定: 保守地估计每字节 0.20 位的实际熵内容(好像 伪随机计数函数始终选择 ml = 1ll = 1)?还是我们 选择平均熵内容(可能出现更智能的平均值, 而不是计算 (1.62 + 0.20) / 2 = 0.91 位 / 字节,但 可能会造成伪随机循环计数 是否偶尔导致我们的平均熵值低于该平均水平?如果我们 过于保守,我们将花费更多的时间收集熵;如果 我们过于乐观,认为可能存在安全漏洞。归根结底, 会在安全性(优先考虑保守熵)之间进行权衡 和效率(倾向于乐观熵估算)。

处理原始样本的影响

原始数据

我重复了上面报告的三项测试, 处理功能(使用 kernel.jitterentropy.raw = false,而不是 默认值为 true)。为方便起见,下表同时列出了 前三行中的示例结果(从上面复制),并将处理 结果(新添加)。

ml ll 原始 最小熵(位 / 字节) 预计压缩率 马尔可夫估计值
随机 (1 .. 128) 随机 (1 .. 16) true 5.77 6.84 5.77
128 16 true 1.62 1.62 3.60
1 1 true 0.20 0.20 0.84
ml ll 原始 最小熵(位 / 字节) 预计压缩率 马尔可夫估计值
随机 (1 .. 128) 随机 (1 .. 16) false 5.79 6.59 5.79
128 16 false 5.78 6.97 5.78
1 1 false 5.77 6.71 5.77

分析

后期处理后的最小熵估算值基本上相等(高达 很容易由随机性解释的细微变化),并且也等于 具有伪随机循环计数的原始样本的最小熵估算。

回想一下,抖动的已处理熵由 64 个独立的随机数字组成, 数据样本,在 64 位内部状态缓冲区中混合在一起。每个原始 个样本对应于 raw = true 表中的一个样本。特别是 将 64 个样本与 ml = 1ll = 1 组合起来会是很荒谬的事情, 每处理 8 个字节,就会产生 (5.77 * 8) = 46.2 位熵 因为这意味着 (46.2 / 64) = 0.72 位熵, 与 0.20 位的测量值相反。

此参数适用于 ml = 1ll = 1raw = false 测量值, 但不适用于 ml = 128ll = 16raw = false具体来说, 原则上,将 64 个原始样本与 ml = 128ll = 16 组合使用可以 collection (1.64 * 64 / 8) = 13.1 位的熵每个处理的字节,但 当然,每个字节都有 8 位硬性限制。

有趣的是,最小熵估算器从 传递给马尔可夫估算器。我的理论是 就足以“欺骗”压缩估计值。如果有 抖动处理例程中存在加密漏洞, 可以编写一个类似的估算器, 最小熵。如果我们使用通用测试来确定原始样本数量, 才能获得 256 的最小熵,而对手则使用目标 那么(相对于这种定向攻击)我们的系统的熵可能会降低 的概率高于我们的预期。这是一个安全漏洞。

如果抖动处理例程中有一个非常糟糕的弱点, 减少“true”内部池中的熵。通过 关于 ml = 1ll = 1 的算术参数显示我们不信任 使用 NIST 测试套件准确测量 所以处理过程实际上有可能减少 而我们的工具无法检测到损失。这会加剧 如上一段落中所述)。

总结

最佳情况下,抖动的伪随机循环计数是有问题的, 一旦使用,就会迫使我们在安全性/效率方面做出取舍。除非我们能 有说服力的证据,证明伪随机时间确实 除了使 NIST 测试套件失效,我们还将提高熵估算值, 应使用确定的循环次数,最好是针对 调整预算

抖动的处理也存在问题,因为(据我所知) 尚未经过充分的加密分析和测试 信任。此外,我们无法直接测量 通过 NIST 测试套件对数据进行后处理, 所以我们无法轻易检测到它我认为我们应该 并保留 Zircon CPRNG 中的熵混合码(基于 SHA-2), 已停用抖动处理。

TODO

  1. 针对不同版本的 Zircon 重复上述测试, 以确保熵估算值保持一致。
  2. 在不同的平台和目标上重复这些测试(注意:x86 目标不会 目前可以在前期启动期间访问系统时钟,因此前期启动 熵测试和前期启动 CPRNG 种子尚不支持抖动 x86)。
  3. 自动执行此实验中运行测试和生成报告的过程 文档。具体而言,这两种测试应该比较以下两项:

    • 伪随机循环计数与各种确定性循环计数值
    • 原始样本与处理后的数据