在容器和孤立的时代,“始终使用 /dev/urandom”仍然是个好建议吗?

信息安全 密码学 linux 随机的
2021-09-07 04:31:54

简而言之:我没有提出另一个关于何时使用/dev/random而不是 的问题/dev/urandom,而是提出了以下场景,在该场景中,我发现自己处于我正在构建的应用程序中:

  • 虚拟机或容器环境(即全新安装,在应用程序第一次运行时可能只有几秒钟的时间)
  • 需要在安装的剩余生命周期(几个月或更长时间)中使用加密安全的随机字节作为密钥材料
  • 可以接受阻塞(即使是几分钟,如有必要)的用户故事和界面

我想知道:这是阻塞随机源(即,getrandom与阻塞模式标志一起使用)的罕见但正确的用例吗?

更长的形式:

显然/dev/urandomvs/dev/random是一个引发争议性讨论的话题。就我而言,我认为/dev/urandom在几乎所有典型用例中都更可取——事实上,我以前从未使用过阻塞随机源。

在这个流行而精彩的答案中,Thomas Pornin 说明urandom手册页有些误导(同意),并且一旦正确播种,urandom池在任何实际情况下都不会“耗尽”熵 - 这符合我的理解也是。

然而,我认为他urandom说“唯一/dev/urandom可能由于低熵而暗示安全问题的时刻是在新的自动化操作系统安装的最初时刻”。

我的理解是,典型的 Ubuntu 服务器启动的“启动时熵洞”超过一分钟!这是基于J. Alex Halderman 在密歇根大学的研究

Halderman 似乎还说熵池在每次启动时都会填满,而不是像 Pornin 在他的回答中所说的那样,在第一次安装操作系统时。虽然它对我的应用程序来说并不是非常重要,但我想知道:它是什么?

我已经阅读了Thomas Hühn 的“关于 Urandom 的神话”的帖子,但由于几个原因,我觉得它没有说服力,与我的申请最相关的是,该帖子基本上归结为“人们不喜欢以他们的方式被阻止。他们将设计变通办法,编造奇怪的阴谋来让它运行。” 尽管这无疑是正确的(这也是我一直/dev/urandom在其他地方使用的原因,尤其是对于 Web 内容),但有些应用程序用户可以忍受等待,尤其是当他们是第一次安装它时。

我正在构建一个旨在在终端设置中本地运行的应用程序,并且我已经有理由期望初始安装过程会有点涉及。我毫不犹豫地要求用户稍等片刻,如果它可以针对重复的密钥对增加少量的鲁棒性。

事实上,Halderman 说他能够计算出 105,728 台 SSH 主机的私钥——超过他扫描的主机的 1%——因为使用了弱熵池来生成密钥对。在这种情况下,主要是嵌入式设备可能具有糟糕的熵源,因此很难填满它们的池。

但是——这可能是我的问题的核心——在一个应用程序在完全幼稚的容器中交付的时代,意味着在一个闪亮的、新的操作系统安装上运行只有几秒钟的时间,我们不是有理由担心这一点吗?现象?我们不需要一个实用的阻塞接口吗?getrandom是打算成为的吗?

当然,在许多情况下,可以将熵从主机共享给客人。但是为了这个问题的目的,让我们假设作者决定不这样做,要么是因为她对部署的细节没有足够的控制权,要么是因为主机上没有可用的熵池。

再想一想:对于像我上面描述的那样新鲜和幼稚但在初始熵生成前景相当糟糕的设备上运行的环境的最佳实践是什么?我正在考虑带有很少或没有 HID 的小型嵌入式设备,这些设备在初始安装过程中可能是气隙的。

编辑:更新:因此,从 PEP524 开始,Python(编写有问题的应用程序)使用getrandomwhenos.urandom被调用,并在熵池未收集到至少 128 位的情况下阻塞。所以作为一个实际问题,我想我有我的答案 - 只需使用它,它只会在必要时os.urandom表现得像。然而,我对这里的首要问题感兴趣(即,容器化时代是否意味着重新思考“总是只使用 urandom”的正统观念)。/dev/random

2个回答

我写了一个答案,详细描述了getrandom()块如何等待初始熵。

但是,我认为他稍微夸大了 urandom,他说“/dev/urandom 可能由于低熵而暗示安全问题的唯一时刻是在全新的自动化操作系统安装的最初时刻。”

你的担心是有根据的。关于那件事及其影响,我有一个悬而未决的问题。问题是持久随机种子需要相当长的时间才能从输入池移动到输出池(阻塞池和 CRNG)。此问题意味着/dev/urandom将在启动后的几分钟内输出潜在的可预测值。正如您所说,解决方案是使用block/dev/random或使用getrandom()set 来阻止。

事实上,在早期启动时在内核日志中看到这样的行并不少见:

random: sn: uninitialized urandom read (4 bytes read, 7 bits of entropy available)
random: sn: uninitialized urandom read (4 bytes read, 15 bits of entropy available)
random: sn: uninitialized urandom read (4 bytes read, 16 bits of entropy available)
random: sn: uninitialized urandom read (4 bytes read, 16 bits of entropy available)
random: sn: uninitialized urandom read (4 bytes read, 20 bits of entropy available)

所有这些都是在收集到足够的熵之前访问非阻塞池的实例。问题是熵的数量太低,以至于此时在密码学上是足够安全的。应该有 2 32 个可能的 4 字节值,但是只有 7 位可用熵,这意味着只有 2 7或 128 种不同的可能性。

Halderman 似乎还说熵池在每次启动时都会填满,而不是像 Pornin 在他的回答中所说的那样,在第一次安装操作系统时。虽然它对我的应用程序来说并不是非常重要,但我想知道:它是什么?

这实际上是语义问题。实际熵(保存在内核中的包含随机值的内存页)在每次启动时都会被持久熵种子和环境噪声填充。但是,熵种子本身是一个在安装时创建的文件,每次系统关闭时都会使用新的随机值进行更新。我想 Pornin 认为随机种子是熵池的一部分(例如,一般熵分配和收集系统的一部分),而 Halderman 认为它是独立的(因为熵池在技术上是内存,仅此而已)。事实是,熵种子在每次启动时都被送入熵池,但实际影响池可能需要几分钟。

三种随机性来源的总结:

  1. /dev/random- 阻塞字符设备在每次读取时都会减少“熵计数”(尽管熵实际上并未耗尽)。但是,它也会阻塞,直到在启动时收集到足够的熵,使其在早期可以安全使用。

  2. /dev/urandom- 每当有人从中读取时,非阻塞字符设备将输出随机数据。一旦收集到足够的熵,它将输出几乎无限的流,与随机数据无法区分。不幸的是,出于兼容性原因,即使在启动的早期,在收集到足够的一次性熵之前,它也是可读的。

  3. getrandom()- 只要熵池已使用所需的最小熵正确初始化,系统调用就会输出随机数据。它默认从非阻塞池中读取。如果给定GRND_NONBLOCK标志,如果熵不足,它将返回错误。如果给定GRND_RANDOM标志,它的行为将与 相同/dev/random,只是阻塞直到有可用的熵。

我建议你使用第三个选项,getrandom()系统调用。这将允许进程以高速读取加密安全的随机数据,并且仅在没有收集到足够的熵时才会在启动早期阻塞。如果os.urandom()如您所说,Python 的函数充当此系统调用的包装器,那么使用它应该没问题。看起来实际上有很多关于是否应该是这种情况的讨论,最终导致它阻塞,直到有足够的熵可用。

再想一想:对于像我上面描述的那样新鲜和幼稚但在初始熵生成前景相当糟糕的设备上运行的环境的最佳实践是什么?

这是一种常见的情况,有几种处理方法:

  • 确保在早期启动时阻止,例如使用/dev/randomor getrandom()

  • 如果可能的话,保留一个持久的随机种子(即,如果您可以在每次启动时写入存储)。

  • 最重要的是,使用硬件 RNG这是#1最有效的措施。

使用硬件随机数生成器非常重要。如果存在任何受支持的 HWRNG 接口,Linux 内核将使用任何支持的 HWRNG 接口初始化其熵池,从而完​​全消除引导熵漏洞。许多嵌入式设备都有自己的随机发生器。

这对于许多嵌入式设备来说尤其重要,因为它们可能没有内核安全地从环境噪声中生成熵所需的高分辨率计时器。例如,某些版本的 MIPS 处理器没有周期计数器。

您如何以及为什么建议使用 urandom 播种(我猜是用户空间?)CSPRNG?这如何击败 getrandom?

非阻塞随机设备不是为高性能而设计的。直到最近,由于使用 SHA-1 来实现随机性,而不是像现在这样使用流密码,因此该设备速度非常慢。使用内核接口实现随机性可能不如本地用户空间 CSPRNG 有效,因为每次调用内核都需要昂贵的上下文切换内核的设计目的是考虑到应用程序想要大量使用它,但是源代码中的注释清楚地表明他们不认为这是正确的做法:

/*
 * Hack to deal with crazy userspace progams when they are all trying
 * to access /dev/urandom in parallel.  The programs are almost
 * certainly doing something terribly wrong, but we'll work around
 * their brain damage.
 */

OpenSSL 等流行的加密库支持生成随机数据它们可以播种一次或偶尔重新播种,并且能够从并行化中受益更多。此外,它还可以编写不依赖于任何特定操作系统或操作系统版本的可移植代码。

如果你不需要大量的随机性,使用内核的接口是完全可以的。如果您正在开发一个在其整个生命周期中需要大量随机性的加密应用程序,您可能需要使用 OpenSSL 之类的库来为您处理。

系统可以处于三种状态:

  1. 没有收集到足够的熵来安全地初始化 CPRNG。
  2. 已收集到足够的熵以安全地初始化 CPRNG,并且:

    2a。释放的熵比它收集的要多。

    2b。释放的熵少于收集的熵。

从历史上看,人们认为(2a)和(2b)之间的区别很重要。这导致了两个问题。首先,这是错误的——这种区别对于设计合理的 CPRNG 来说毫无意义。其次,对 (2a)-vs-(2b) 区别的强调导致人们忽略了 (1) 和 (2) 之间的区别,这实际上非常重要。人们只是将(1)折叠成(2a)的特例。

你真正想要的是在状态(1)中阻塞的东西,而不是在状态(2a)或(2b)中阻塞的东西。

不幸的是,在过去,(1)和(2a)之间的混淆意味着这不是一个选择。您仅有的两个选项是/dev/random,在情况(1)和(2a)中被阻止,并且/dev/urandom,从未被阻止。但是状态 (1) 几乎永远不会发生——而且在配置良好的系统中根本不会发生,见下文——然后/dev/urandom几乎所有系统都更好,几乎所有时间。这就是所有那些关于“总是使用 urandom”的博客文章的来源——他们试图说服人们停止在 (2a) 和 (2b) 州之间做出无意义和有害的区分。

但是,是的,这些都不是你真正想要的。因此,较新的getrandom系统调用,默认情况下在状态 (1) 中阻塞,而不在状态 (2a) 或 (2b) 中阻塞。所以在现代 Linux 上,正统应该更新为:始终使用getrandom默认设置

额外皱纹:

  • getrandom还支持非默认模式,它的行为类似于/dev/random,可以通过GRND_RANDOM标志请求。AFAIK 这个标志实际上从来没有用过,原因与那些旧博客文章描述的所有相同的原因。不要使用它。

  • getrandom还有一些额外的好处/dev/urandom:无论您的文件系统布局如何,它都可以工作,并且不需要打开文件描述符,这对于希望对将使用的环境做出最小假设的通用库来说都是有问题的。这不会影响加密安全性,但它在操作上很好。

  • 配置良好的系统将始终具有可用的熵,即使在早期启动时也是如此(即,您永远永远不应该进入状态(1))。有很多方法可以解决这个问题:保存上一次启动的一些熵以在下一次启动时使用。安装硬件 RNG。Docker 容器使用主机的内核,因此可以访问其熵池。高质量的虚拟化设置有办法让客户系统通过管理程序接口从主机系统获取熵(例如搜索“virtio rng”)。但当然,并非所有系统都配置良好。如果您有一个配置不佳的系统,您应该看看是否可以将其配置得很好。原则上这应该很便宜,但实际上人们并不优先考虑安全性,所以......它可能需要做一些事情,比如切换云提供商,或切换到不同的嵌入式平台。不幸的是,您可能会发现这比您(或您的老板)愿意支付的要贵,因此您不得不处理配置不当的系统。如果是这样,我表示同情。

  • 正如@forest 所指出的,如果您需要很多 CPRNG 值,那么如果您非常小心,您可以通过在用户空间中运行自己的 CPRNG 来加快速度,同时使用getrandom(重新)播种。不过,这在很大程度上是“仅限专家”的事情,就像您发现自己实现自己的加密原语的任何情况一样。仅当您测量并发现getrandom直接使用对您的需求而言太慢并且您具有重要的加密专业知识时,您才应该这样做。很容易搞砸 CPRNG 实现,从而完全破坏您的安全性,但输出仍然“看起来”是随机的,所以您不会注意到。