LWN:统一Linux 随机数设备!
关注了就能看到更多这么棒的文章哦~
Uniting the Linux random-number devices
By Jake Edge
February 16, 2022
DeepL assisted translation
https://lwn.net/Articles/884875/
内核里随机数生成器(RNG, random-number generator)中有一个阻塞问题,需要进程等待 "足够多的" 熵(entropy)才能生成足够强大的随机数,这一点一直争议不断。多年来,它也导致了各种问题,既有用户空间程序的错误使用而导致的超时和延误,也有启动过程中出现的死锁和其他一些问题。在过去的几年里,这一点经历了不少变动,而且看起来有可能在即将到来的一些内核版本中既可以满足 "好用 " 又可以满足 "具有足够的加密强度的 "随机数的目标。
Random history
kernel RNG 的历史很长,而且有些曲折;内核中有两个随机数设备,/dev/random 和/dev/urandom,可以通过读取它们来获得随机数据。/dev/urandom 一直是几乎所有东西都要使用的那个设备,因为它不会阻塞;它仅仅会把当前内核在读取时所能提供的最好的随机数提供出来。与之相反的,如果没有达到具有足够加密强度的熵的标准时,读取 /dev/random 就会被阻塞住。这些熵都是来自于各种设备(例如磁盘、键盘、网络)的中断触发时间点以及硬件 RNG(如果存在的话)。如果在自己 urandom pool 池被初始化之前调用的话,/dev/urandom 会记录一个 warning 信息(仅警告一次),这个 urandom pool 的初始化操作就是来自已经收集好熵来完成准备的 random pool 的内容。/dev/urandom 会确保把它的伪随机数发生器(PRNG)的内容提供出来,并且永远不会阻塞。
2014 年,针对 Linux 3.17,添加了 getrandom()系统调用以便为用户空间应用程序提供一种可靠的方式来请求随机数,哪怕是在面对文件描述符耗尽或无法访问随机数(比如那些可能运行在容器中的应用程序)。getrandom() 设计上使用了 urandom 池,但需要在它根据 random pool 完全初始化之后。因此,虽然对 /dev/urandom 的读取不会阻塞,但此时对 getrandom() 的调用会阻塞住,直到收集到足够的熵。getrandom() 调用者可以通过一个 flag 来选择使用 random pool,这样就表示这个调用要求来自 random pool 的数据的全部熵值需求。
在 2019 年,ext4 文件系统的一个没有什么关联的 patch 导致了系统无法启动,因为它减少了系统产生的中断数量,所以 urandom pool 就没有被初始化,此时调用 getrandom() 就被阻塞了。由于这些调用是在系统启动过程的早期进行的,系统尚未收集到足够的熵,因为此时启动过程还在等待 getrandom() 的返回,因此就出现了死锁。在 5.3 版本内核中,ext4 的改动被暂时 revert 了,Linus Torvalds 在 5.4 版本中增加了一个更加正规的解决方案,也就是使用 CPU 执行时间的抖动(CPU execution time jitter)来为熵来源,确保 random pool 可以在一秒钟左右完成初始化。这项技术有些争议,甚至连 Torvalds 本人都对它不太确信,但它已经被落实了,而且据大家所知,已经使用了好几年。
在 2020 年,/dev/random 的阻塞特性就被改变为像 getrandom()一样的行为,也就是说会阻塞到被初始化为止,初始化之后就可以提供符合加密强度的随机数。Andy Lutomirski,为这一改变贡献了 patch,他说。"Linux 的 CRNG 产生的输出已经非常好了,甚至足够用于生成密钥。无论从哪个角度来说,这个阻塞 pool 都没有什么好处,而且保留它需要大量不确定是否有价值的基础设施。" 这些 patch 还为 getrandom()增加了一个 GRND_INSECURE 标志,即使 random pool 还没有被初始化,也会 "尽力" 返回一个随机数。
可以看出,随着时间的推移,这两种设备之间的界限已经变得相当模糊了。关于内核 RNG 的历史,甚至可以追溯到更早的时候,都可以在这个 LWN kernel index 索引页面中找到。考虑到这两种设备共同的成长,因此也不奇怪现在看到一个新的建议来事实上消除两者的区别。
No random blocking(for long)
Jason A. Donenfeld,几个月前成为了内核 RNG 子系统的共同维护者,最近很积极地对这部分代码进行了清理和其他一些修改。2月 11 日的时候发布了一个 RFC patch,也许可以算是一个 "request for grumbles",他在其中建议删除 /dev/urandom 在初始化 pool 之前就能返回数据的功能。这意味着内核 RNG 子系统将会一直阻塞住来等待初始化完成,但在初始化完成之后总是能给出具有足够加密强度的随机数(除非使用了 getrandom()的 GRND_INSECURE 标志)。得益于 Torvalds 在 5.4 中进行的改动(Donenfeld 称之为 "Linus Jitter Dance"),这个初始化在最坏情况下也不用等待很久,所以 Donenfeld 提出了这个改动建议:
所以,考虑到内核已经拥有了这种不需要什么东西就能获得随机数种子的机制,而且这个过程也相当快,也许已经没有必要再让/dev/urandom 提供不安全的数据了。过去,我们不希望启动过程出现死锁,这是可以理解的。但是现在在最坏的情况下,只要一秒钟过去了,问题就解决了。看起来,也许我们终于到了可以摆脱臭名昭著的 "urandom read hole" 问题的时候了。
然而,这样做也还有一些潜在障碍。抖动熵(jitter entropy)技术依赖于运行相同代码之时的时间差,这就要求有一个高分辨率的 CPU cycle counter 和一个具有非确定性的 CPU(非确定性来自于 cache、instruction reordering、speculation 等原因)。然而,有一些架构上并不具备这个条件,所以无法通过这种方式收集熵。Donenfeld 指出,除了 Amiga 之外的 m68k 系统、两种 MIPS 型号(R6000 和 R6000A)、以及可能包括 RISC-V 都会受到影响。他不确定是否还有其他类似的架构会受此影响。不过,他认为 RISC-V 的代码并不是真正的问题,目前还没有人出声对此提出异议。同时,可能把其他这些单另处理应该是个正确的做法:
如果我的总体分析是正确的,那么这些古老的平台真的值得继续这样拖下去吗?我半信半疑地期待着收到几个扔来的西红柿、一个愤怒的拳头和一句 "滚出我的草坪!" 之类的批评,如果后续我收到这些信息,那么我就会接受,我们可以忘记我曾经提出过这个建议。如前所述,除非有广泛的共识,否则我不打算合并这个改动。但是,如果人们有不同的看法,也许 Linus Jitter Dance 就能最终解决多年来/dev/urandom 的抱怨。
这个 patch 相当小。它只是删除了/dev/urandom 的 file_operations 结构,并重新使用了/dev/random 的结构,从而使这两个设备的行为相同。这也使 GRND_INSECURE 标志的实现变得更短小了,但他后来说这个改动可能有点分散注意力。他建议的主要意图是要做到下面的变化:
现在我们的用法是:
/dev/random = getrandom(0)
/dev/urandom = getrandom(GRND_INSECURE)
我们的改动是希望变成:
/dev/random = getrandom(0)
/dev/urandom = getrandom(0)
Torvalds 对 RFC 的看法非常积极正面。他说,这个 patch 对于有 cycle counter 的架构是有意义的。jitter entropy 的改动已经完成了两年半了,没有听到什么抱怨,所以 "我认为我们可以称那件事为成功"。可能有一些关于它的抱怨,但是 "老实说,我认为所有的抱怨都是来自那些理论上的装腔作势者,反正他们没有任何实际的建议"。众所周知,Torvalds 对关于密码学的理论问题(或关于其他任何东西的理论问题,实际上也是一样)没有什么耐心。
他确实反对在不能进行 jitter dance 的架构中删除 GRND_INSECURE,因为它是用户空间解决缺乏启动时熵的一种方式,尽管它并不是绝对的安全:
从随机性的角度来看,这些系统应该是无法运行的,毕竟如果没有任何东西产生熵,它还能怎么往下继续运行呢。但不管是否这些平台无法运行了,我都很怀疑它们是否还存在。那些可怕的 MIPS 平台在嵌入式网络中相当普遍(路由器、接入点,都是那些人们 *应该*关心的地方)。
[……]而且几乎没有人测试那些出问题的平台:即使是为那些嵌入式网络环境建立新内核的人,最终也可能在现有的用户空间配置环境中也使用了这些内核,而那些现成环境里面人们也有一些现有的保存的伪熵的来源。因此,他们甚至可能永远不会触发 "首次启动问题",这往往是最糟糕的情况。
但是,他说他愿意合入这个补丁。"在某些时候,'担心坏掉的平台'最终不足以成为不合入这些 patch 的借口"。根据 Joshua Kinard 的说法,这两个 MIPS 型号是 20 世纪 80 年代的,从未在哪些系统中真正使用过,随机代码中对它们的内核测试 "可能是在处理器手册或类似的东西的基础上理解之后添加的"。Maciej W. Rozycki 说,可能有一些系统在使用这些型号,但从来没有为它们制作过 Linux 移植版。这可能意味着,唯一有问题的系统是 "一些 m68k 博物馆中的展品",Donenfeld 说。
不过,正如 Geert Uytterhoeven 所指出的,Linux generic architecture 是所有新增架构的默认起点,其中 cycle-counter 代码被硬性规定为返回 0。"一些架构没有实现 get_cycles(),或者用了一个与通用版本非常相似或相同的变体来实现的。" David Laight 补充了几个例子(老版本的 x86,nios2),这些架构都是这样的情况。
但是我的 NetHack 机器呢?
Lutomirski 有一个更单调的抱怨:
我不喜欢这个 patch,原因与安全无关。在某个地方,有一台 Linux 机器可以在 50 毫秒内直接启动到 Nethack,非常让人满意。如果 Nethack 从/dev/urandom 获得 256 比特的完全的熵值,那么这台机器的主人就得好好操作一下了。如果它时不时地要重启,主人就会感到失望或者觉得很可笑了。如果它得到了一个可以被暴力破解的弱的种子,那么主人就得要冒着被暴力破解的风险。
另一方面,如果它等了 750ms 才有足够的抖动熵,那么它就完全失败了。没有人愿意等待 750ms 来玩 Nethack。
更严重的是,他关注的是像备用相机或灯泡这样需要 "立即" 启动的设备,这些设备中随机数的质量可能并不重要。GRND_INSECURE 这个逃生舱把手的存在正是为了这个原因。同样,Lennart Poettering 也不希望 systemd 等待一秒钟来获取种子生成哈希表,毕竟 systemd 本身已经有了一个机制来重新填充哈希表:
因此,systemd 使用/dev/urandom (此时可能尚未完成初始化)来获取其哈希表的种子。如果随机值最初的熵很低,那也没关系,因为当发生过多的哈希碰撞时,我们会自动重新生成种子,然后使用更新的(希望也是更加好的)种子,这也是通过/dev/urandom 获得的。也就是说,如果种子最初不足以阻止哈希碰撞攻击,那么一旦哈希表真的被攻击了,我们会用更好的种子来替换掉。为此,我们只需要让 random pool 最终能变得更好就可以了。
事实证明,在有 GRND_INSECURE 的系统上,systemd 已经开始在使用它了,所以不改变这个行为,就像最初提议的那样,就可以很好地解决 Poettering 的担忧。Donenfeld 完全同意将禁用 GRND_INSECURE 的改动从他的 patch 中删除。如上所述,这并不是他的提议的重点。
根据 Torvalds 的回应,取消/dev/random 和/dev/urandom 之间的最终区别似乎没有什么巨大的障碍了,当然,除了名字之外。不过,如果有更多的体系架构不能使用 jitter 技术,那么这方面的差异可能会继续存在,因为 Torvalds 也认为保留 "这些愚蠢的东西作为 '不会伤害好的平台,可能会帮助坏的平台' " 可以是有价值的。Donenfeld 说,删除的代码不多,所以它并没有真正简化掉多少代码,它更多的价值是能够消除关于在 Linux 上应该使用哪种随机性来源的无休止的争论。这应该是一个很值得做的事情。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~