Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 522 – 允许在安全敏感的 API 中使用 BlockingIOError

作者:
Alyssa Coghlan <ncoghlan at gmail.com>, Nathaniel J. Smith <njs at pobox.com>
状态:
已拒绝
类型:
标准跟踪
需要:
506
创建:
2016年6月16日
Python 版本:
3.6
决议:
安全 SIG 消息

目录

摘要

标准库中许多返回随机值的 API(名义上适用于安全敏感操作)目前存在一种模糊的操作系统相关的故障模式,允许它们返回实际上不适合此类操作的值。

这是由于某些操作系统内核(最著名的是 Linux 内核)允许在系统随机数生成器完全初始化之前从 /dev/urandom 读取数据,而大多数其他操作系统会在此类读取操作上隐式阻塞,直到随机数生成器准备好为止。

对于较低级别的 os.urandomrandom.SystemRandom API,此 PEP 提出将 Python 3.6 中此类故障从当前的静默、难以检测和难以调试的错误更改为易于检测和调试的错误,方法是引发 BlockingIOError 并显示适当的错误消息,从而使开发人员有机会明确指定他们处理这种情况的首选方法。

对于新的高级 secrets API,它建议在该模块生成随机数时根据需要隐式阻塞,并公开一个新的 secrets.wait_for_system_rng() 函数,以允许以其他方式使用低级 API 的代码显式等待系统随机数生成器可用。

此更改将影响提供 getrandom() 系统调用的任何操作系统,无论 /dev/urandom 设备的默认行为是在系统随机数生成器未准备好时返回可能可预测的结果(例如 Linux、NetBSD)还是阻塞(例如 FreeBSD、Solaris、Illumos)。在系统随机数生成器初始化之前阻止执行用户空间代码或不提供 getrandom() 系统调用的操作系统将完全不受此提议更改的影响(例如 Windows、Mac OS X、OpenBSD)。

在以下情况下可能会遇到新的异常或 secrets 模块中的阻塞行为

  • 在 Linux 系统初始化期间调用这些 API 的 Python 代码
  • 在初始化不正确的 Linux 系统上运行的 Python 代码(例如,没有足够的熵源来播种系统随机数生成器的嵌入式硬件,或者未配置为从 VM 主机接收熵的 Linux VM)

与其他 PEP 的关系

此 PEP 依赖于已接受的 PEP 506,该 PEP 添加了 secrets 模块。

此 PEP 与 Victor Stinner 的 PEP 524 竞争,后者建议在系统 RNG 未准备好时使 os.urandom 本身隐式阻塞。

PEP 拒绝

对于参考实现,Guido 拒绝了此 PEP,转而支持 PEP 524 中的无条件隐式阻塞提案(这使 CPython 在 Linux 上的行为与在其他操作系统上的行为保持一致)。

这意味着在 Linux 发行版中关于系统 Python 安装中 os.urandom() 的适当默认行为的任何进一步讨论都应在各自的发行版邮件列表中进行,而不是在上游 CPython 邮件列表中进行。

独立于此 PEP 的更改

CPython 解释器初始化和 random 模块初始化已更新为在系统随机数生成器未准备好时优雅地回退到其他播种选项。

此 PEP 不与 PEP 524 中的提案竞争,该提案是在支持它的平台上添加 os.getrandom() API 以公开 getrandom 系统调用。在 os 模块作为对可能依赖于平台的操作系统功能的薄包装的角色中,添加该 API 的动机足够充分,因此无论这些系统上 os.urandom() 的默认行为如何,都可以添加它。

提案

更改平台上具有 getrandom() 系统调用的 os.urandom()

此 PEP 建议在 Python 3.6 及更高版本中,os.urandom() 更新为在可用时以非阻塞模式调用 getrandom() 系统调用,并在内核报告该调用将阻塞时引发 BlockingIOError: system random number generator is not ready; see secrets.token_bytes()

然后,此行为将传播到现有的 random.SystemRandom,它提供了一个围绕 os.urandom() 的相对较薄的包装器,与 random.Random() API 匹配。

但是,PEP 506 引入的新 secrets 模块将更新为捕获新的异常,并在遇到异常时隐式等待系统随机数生成器。

在所有情况下,一旦对这些安全敏感 API 之一的调用成功,则该进程中对这些 API 的所有后续调用都将成功且不会阻塞(一旦系统启动后操作系统随机数生成器准备好,它将保持就绪状态)。

在 Linux 和 NetBSD 上,这将替换先前从 /dev/urandom 读取可能可预测的结果的行为。

在 FreeBSD、Solaris 和 Illumos 上,这将替换先前隐式阻塞直到系统随机数生成器准备就绪的行为。但是,目前尚不清楚这些操作系统是否确实允许用户空间代码(以及 Python)在系统随机数生成器准备好之前运行。

请注意,在所有情况下,如果调用底层 getrandom() API 报告 ENOSYS 而不是返回成功响应或报告 EAGAIN,则 CPython 将继续回退到直接从 /dev/urandom 读取。

添加 secrets.wait_for_system_rng()

不应在没有关于如何在遇到错误时解决该错误的简单建议的情况下添加新的异常(但是,在实践中,遇到新错误的频率预计会很低)。对于确实需要使用系统随机数生成器底层接口(而不是新的 secrets 模块)的安全敏感代码,并且确实收到指示这对该特定应用程序的用户群是一个实际问题而不是理论问题的实时错误报告,此 PEP 的建议将是将以下代码段(直接或间接)添加到 __main__ 模块中

import secrets
secrets.wait_for_system_rng()

或者,如果需要与 Python 3.6 之前的版本兼容

try:
    import secrets
except ImportError:
    pass
else:
    secrets.wait_for_system_rng()

secrets 模块本身中,这将随后在 token_bytes() 中使用,如果遇到新的异常,则隐式阻塞

def token_bytes(nbytes=None):
    if nbytes is None:
        nbytes = DEFAULT_ENTROPY
    try:
        result = os.urandom(nbytes)
    except BlockingIOError:
        wait_for_system_rng()
        result = os.urandom(nbytes)
    return result

然后将模块的其他部分更新为使用 token_bytes() 作为其基本的随机数生成构建块,而不是直接调用 os.urandom()

涵盖几乎肯定需要访问系统随机数生成器的用例(例如 Web 框架)的应用程序框架可以选择将对 secrets.wait_for_system_rng() 的调用隐式合并到启动应用程序的命令中,以便在使用这些框架时,现有对 os.urandom() 的调用将保证永远不会引发新的异常。

对于在无法直接修改的应用程序中遇到错误的情况,可以使用以下命令等待系统随机数生成器初始化,然后再启动该应用程序

python3 -c "import secrets; secrets.wait_for_system_rng()"

例如,此代码段可以添加到 shell 脚本或 systemd ExecStartPre 钩子中(即使后续命令本身不是在 Python 3.6 下运行的应用程序,它也可能被证明对可靠地等待系统随机数生成器准备好很有用)

鉴于以上对 os.urandom() 提出的更改,以及在支持它的系统上包含 os.getrandom() API,此函数的建议实现将是

if hasattr(os, "getrandom"):
    # os.getrandom() always blocks waiting for the system RNG by default
    def wait_for_system_rng():
        """Block waiting for system random number generator to be ready"""
        os.getrandom(1)
        return
else:
   # As far as we know, other platforms will never get BlockingIOError
   # below but the implementation makes pessimistic assumptions
    def wait_for_system_rng():
        """Block waiting for system random number generator to be ready"""
        # If the system RNG is already seeded, don't wait at all
        try:
            os.urandom(1)
            return
        except BlockingIOError:
            pass
        # Avoid the below busy loop if possible
        try:
            block_on_system_rng = open("/dev/random", "rb")
        except FileNotFoundError:
            pass
        else:
            with block_on_system_rng:
                block_on_system_rng.read(1)
        # Busy loop until the system RNG is ready
        while True:
            try:
                os.urandom(1)
                break
            except BlockingIOError:
                # Only check once per millisecond
                time.sleep(0.001)

在能够等待系统 RNG 就绪的系统上,如果定义了os.getrandom()os.urandom()本身隐式阻塞,或者/dev/random设备可用,则此函数将无需繁忙循环即可等待。如果系统随机数生成器已就绪,则此调用保证永远不会阻塞,即使系统的/dev/random设备使用允许其在正常系统操作期间间歇性阻塞的设计。

范围限制

对于 Windows 或 Mac OS X 系统,不建议进行任何更改,因为这两个平台都没有提供在操作系统随机数生成器初始化之前运行 Python 代码的机制。Mac OS X 甚至会在无法正确初始化随机数生成器的情况下导致内核恐慌并中止引导过程(尽管 Apple 对支持的硬件平台的限制在实践中使这种情况极不可能发生)。

同样,对于不提供getrandom()系统调用的其他 *nix 系统,也不建议进行任何更改。在这些系统上,os.urandom()将继续阻塞,等待系统随机数生成器初始化。

虽然其他提供非阻塞 API(除了getrandom())来请求适用于安全敏感应用程序的随机数的 *nix 系统可能会收到类似于本 PEP 中针对getrandom()提出的更新,但此类更改不在本提案的范围内。

Python 在受影响平台的旧版本上的行为(这些平台不提供新的getrandom()系统调用)也将保持不变。

基本原理

确保 secrets 模块在需要时隐式阻塞

这样做是为了帮助鼓励人们形成一种观念,即对于生成安全敏感随机数的正确方法,最简单的答案是“在可用时使用 secrets 模块,否则应用程序可能会意外崩溃”,而不是更繁琐的“在可用时始终调用 secrets.wait_for_system_rng(),否则应用程序可能会意外崩溃”。

之所以这样做,还因为 BDFL 对可能会意外阻塞的 API 的容忍度高于对可能会抛出意外异常的 API 的容忍度 [11]

在 Linux 上的 os.urandom() 中引发 BlockingIOError

几年来,安全社区一直建议在 Python 中实现安全敏感操作时使用os.urandom()(或random.SystemRandom()包装器)。

为了帮助改进 API 的可发现性,并更清楚地表明保密和模拟不是同一个问题(即使它们都涉及随机数),PEP 506 将几个基于较低级别os.urandom() API 的单行代码示例收集到一个新的secrets模块中。

但是,此建议也附带了一个长期存在的警告:至少对于 Linux 以及可能的一些其他 *BSD 系统,编写安全敏感软件的开发人员可能需要等到操作系统随机数生成器就绪后才能依赖它进行安全敏感操作。这通常仅在系统初始化过程的早期读取os.urandom()时,或在可用熵源很少的系统(例如,某些类型的虚拟化或嵌入式系统)上才会发生,但不幸的是,触发此情况的确切条件很难预测,并且当它发生时,用户空间没有直接的方法来判断它是否已发生,除非查询特定于操作系统的接口。

在 *BSD 系统(如果特定的 *BSD 变体允许问题发生)以及可能还包括 Solaris 和 Illumos 上,遇到这种情况意味着os.urandom()要么阻塞等待系统随机数生成器就绪(相关的症状是受影响的脚本在第一次调用os.urandom()时意外暂停),要么表现得与在 Linux 上相同。

在 Linux 上,在 Python 3.4 及更早版本以及 Python 3.5.2 之后的 Python 3.5 维护版本中,开发人员没有明确的指示表明他们的软件在 Linux 引导过程的早期运行时或在没有良好熵源来为操作系统随机数生成器播种的硬件上可能无法按预期工作:由于底层/dev/urandom设备的行为,Linux 上的os.urandom()无论如何都会返回结果,并且需要进行大量的统计分析才能证明存在安全漏洞。

相比之下,如果在这些情况下引发了BlockingIOError,那么使用 Python 3.6+ 的开发人员可以轻松选择他们所需的方案

  1. 在应用程序启动时或之前等待系统 RNG(安全敏感)
  2. 切换到使用 random 模块(非安全敏感)

使 secrets.wait_for_system_rng() 公开

此 PEP 的早期版本提出了许多关于包装os.urandom()以使其适用于安全敏感用例的示例。

在 security-sig 邮件列表中讨论该提案时,人们意识到 [9],驱动本 PEP 中 API 设计的核心假设是,在让异常导致应用程序失败、阻塞等待系统 RNG 就绪以及切换到使用random模块而不是os.urandom之间进行选择是一个特定于应用程序和用例的决策,应该考虑到特定于应用程序和用例的细节。

解释器运行时或支持库无法确定特定用例是否安全敏感,虽然应用程序开发人员可以轻松决定如何处理特定 API 抛出的异常,但他们无法轻易解决 API 在他们期望其为非阻塞时阻塞的问题。

因此,PEP 已更新,添加了secrets.wait_for_system_rng()作为应用程序、脚本和框架用来指示它们希望确保系统 RNG 可用后再继续的 API,而库开发人员可以继续调用os.urandom(),而不用担心它可能会意外开始阻塞等待系统 RNG 可用。

向后兼容性影响评估

类似于PEP 476,这是一个将之前静默的安全故障转换为需要应用程序开发人员对他们想要的方案做出明确决策的响亮异常的提案。

由于不建议对不提供getrandom()系统调用的操作系统进行任何更改,因此os.urandom()保留其作为名义上阻塞 API 的现有行为,在实践中是非阻塞的,因为很难安排 Python 代码在操作系统随机数生成器就绪之前运行。我们认为,至少在某些 *BSD 变体上可能会遇到类似于本 PEP 中描述的问题,但没有人明确地证明这一点。在 Mac OS X 和 Windows 上,似乎根本不可能在引导过程的早期运行 Python 解释器。

在 Linux 和其他具有类似/dev/urandom行为的平台上,os.urandom()保留其作为保证非阻塞 API 的状态。但是,在操作系统随机数生成器未准备好用于安全敏感操作的特定情况下,实现该状态的方法发生了变化:历史上它会返回可能可预测的随机数据,在本 PEP 中,它将改为引发BlockingIOError

然后,受影响应用程序的开发人员需要根据他们正在开发的应用程序类型,进行以下更改之一以获得与 Python 3.6 的向前兼容性。

不受影响的应用程序

以下类型的应用程序将完全不受此更改的影响,无论它们是否执行安全敏感操作

  • 不支持 Linux 的应用程序
  • 仅在桌面或传统服务器上运行的应用程序
  • 仅在系统 RNG 就绪后运行的应用程序(包括应用程序框架代表它们调用secrets.wait_for_system_rng()的那些应用程序)

此类别的应用程序根本不会遇到新的异常,因此开发人员等待查看他们是否收到与新的运行时行为相关的 Python 3.6 兼容性错误是合理的,而不是试图预先确定他们是否受影响。

受影响的安全敏感应用程序

安全敏感应用程序需要更改其系统配置,以便应用程序仅在操作系统随机数生成器准备好进行安全敏感操作后才启动,更改应用程序启动代码以调用secrets.wait_for_system_rng(),或者切换到使用新的secrets.token_bytes() API。

例如,对于通过 systemd 单元文件启动的组件,以下代码段将在系统 RNG 就绪之前延迟激活

ExecStartPre=python3 -c “import secrets; secrets.wait_for_system_rng()”

或者,以下代码段将在可用时使用secrets.token_bytes(),否则回退到os.urandom()

try
import secrets.token_bytes as _get_random_bytes
except ImportError
import os.urandom as _get_random_bytes

受影响的非安全敏感应用程序

非安全敏感应用程序应更新为使用random模块而不是os.urandom

def pseudorandom_bytes(num_bytes):
    return random.getrandbits(num_bytes*8).to_bytes(num_bytes, "little")

根据应用程序的详细信息,random 模块可能会提供可以直接使用的其他 API,而无需模拟os.urandom() API 生成的原始字节序列。

其他背景

为什么现在提出这个建议?

主要原因是 Python 3.5.0 版本切换到在可用时使用新的 Linuxgetrandom()系统调用以避免使用文件描述符 [1],这具有使以下操作阻塞等待系统随机数生成器就绪的副作用

  • os.urandom(以及依赖它的 API)
  • 导入random模块
  • 初始化某些内置类型使用的随机化哈希算法

虽然第一个行为可以说是有益的(并且与os.urandom在其他操作系统上的现有行为一致),但后两个行为是不必要且不受欢迎的,最后一个行为现在已知在尝试使用 Python 3.5.0 或 3.5.1 在 Linux 初始化过程中运行 Python 脚本时会导致系统级死锁 [2],而第二个行为在使用未配置健壮熵源的虚拟机时会导致问题 [3]

由于在 CPython 中解耦这些行为将涉及许多更适合功能版本而不是维护版本的实现更改,因此 Python 3.5.2 中应用的相对简单的解决方案是将所有三个行为都恢复到类似于先前 Python 版本的行为:如果新的 Linux 系统调用指示它将阻塞,那么 Python 3.5.2 将隐式地回退到直接读取/dev/urandom [4]

但是,此错误报告导致了一系列建议,建议添加新的 API,如os.getrandom() [5]os.urandom_block() [6]os.pseudorandom()os.cryptorandom() [7],或者向os.urandom() 本身添加新的可选参数 [8],然后尝试教育用户何时应该调用这些 API 而不是仅仅使用简单的os.urandom() 调用。

这些建议可以说是反应过度,因为在 Linux 上可靠地获取适合安全敏感工作的随机数的问题是一个相对模糊的问题,主要引起操作系统开发人员和嵌入式系统程序员的兴趣,这可能不值得用新的 Linux 特定问题扩展 Python 标准库的跨平台 API。特别是考虑到secrets 模块已经作为“使用它,不要担心底层细节”的选项添加到其中,供编写安全敏感软件的开发人员使用,这些软件由于某种原因无法依赖于更高层的特定领域 API(如 Web 框架),并且也不需要担心 Python 3.6 之前的 Python 版本。

也就是说,低成本 ARM 设备也变得越来越普遍,其中许多设备运行 Linux,并且许多人编写在这些设备上运行的 Python 应用程序。这为解决一个模糊的安全问题创造了机会,该问题目前需要大量关于 Linux 启动过程和可证明不可预测的随机数生成的知识来诊断和解决,而是将其转变为一个相对普通且易于在互联网搜索中找到的运行时异常。

os.urandom() 的跨平台行为

在 Linux 和 NetBSD 以外的操作系统上,os.urandom() 可能已经阻塞,等待操作系统的随机数生成器准备好。这最多会在进程的生命周期中发生一次,并且后续保证该调用是非阻塞的。

Linux 和 NetBSD 是例外,即使操作系统的随机数生成器不认为自己已准备好用于安全敏感操作,从/dev/urandom 设备读取也将返回基于其可用的熵的随机值。

这种行为可能存在问题,因此 Linux 3.17 添加了一个新的getrandom() 系统调用,该调用(除其他优点外)允许调用者阻塞等待随机数生成器准备好,或者在随机数生成器未准备好时请求错误返回。值得注意的是,新的 API不支持返回不适合安全敏感用例的数据的旧行为。

直到 Python 3.4(包括 Python 3.4)之前的 Python 版本直接访问 Linux/dev/urandom 设备。

Python 3.5.0 和 3.5.1(在提供新系统调用的系统上构建时)在阻塞模式下调用getrandom() 以避免使用文件描述符访问/dev/urandom。虽然没有报告由于os.urandom() 在用户代码中阻塞而导致的特定问题,但确实存在由于 CPython 在解释器启动时以及导入random 模块时隐式调用阻塞行为而导致的问题。

Python 3.5.2 并没有尝试将 SipHash 初始化与os.urandom() 实现解耦,而是切换到在非阻塞模式下调用getrandom(),如果系统调用指示它将阻塞,则回退到从/dev/urandom 读取。

因此,直到 Python 3.5(包括 Python 3.5)的所有 Python 版本中的os.urandom() 都将底层/dev/urandom 设备的行为传播到 Python 代码中。

Linux 上 /dev/urandom 行为的问题

Pythonos 模块在很大程度上与 Linux API 共同发展,因此当在 Linux 上运行时,使os 模块函数紧密遵循其 Linux 操作系统级对应函数的行为通常被认为是一个理想的功能。

但是,/dev/urandom 代表了一种当前行为被认为存在问题的情况,但已证明在内核级别单方面修复它会阻止某些 Linux 发行版启动(至少部分原因是像 Python 这样的组件目前在系统初始化过程的早期将其用于非安全敏感目的)。

作为类比,考虑以下两个函数

def generate_example_password():
    """Generates passwords solely for use in code examples"""
    return generate_unpredictable_password()

def generate_actual_password():
    """Generates actual passwords for use in real applications"""
    return generate_unpredictable_password()

如果您将操作系统的随机数生成器视为生成不可预测的秘密密码的方法,那么您可以将 Linux 的/dev/urandom 视为像这样实现

# Oversimplified artist's conception of the kernel code
# implementing /dev/urandom
def generate_unpredictable_password():
    if system_rng_is_ready:
        return use_system_rng_to_generate_password()
    else:
        # we can't make an unpredictable password; silently return a
        # potentially predictable one instead:
        return "p4ssw0rd"

在这种情况下,generate_example_password 的作者没问题——即使"p4ssw0rd" 出现的频率比他们预期的要高一些,它也只用于示例。但是,generate_actual_password 的作者遇到了问题——他们如何证明他们对generate_unpredictable_password 的调用从未遵循返回可预测答案的路径?

在现实生活中,情况比这稍微复杂一些,因为可能存在一定程度的系统熵可用——因此回退可能更像是return random.choice(["p4ssword", "passw0rd", "p4ssw0rd"]) 或其他更易变的东西,因此只有在比generate_actual_password 的作者预期的更好的几率下才能进行统计预测。不过,这并没有真正使事情变得更安全;它主要意味着,如果您尝试以明显的方式捕获问题——if returned_password == "p4ssw0rd": raise UhOh——那么它不起作用,因为returned_password 可能改为p4ssword 甚至pa55word,或者只是从少于 2**64 个可能性中选择的任意 64 位序列。因此,即使它对 Linux 内核团队在不诉诸破坏向后兼容性的情况下缓解此问题的实际后果所做的努力极不公平,但此粗略草图确实给出了“比预期更可预测”的回退行为的后果的正确总体思路。

这种设计普遍被认为是一个坏主意。据我们所知,没有任何用例实际上需要这种行为。它导致在实际系统上使用了不安全的ssh 密钥,并且许多类 Unix 系统(包括至少 Mac OS X、OpenBSD 和 FreeBSD)已修改了其/dev/urandom 实现,以便它们永远不会返回可预测的输出,要么在这种情况下使读取阻塞,要么干脆拒绝运行任何用户空间程序,直到系统 RNG 初始化完成。不幸的是,Linux 到目前为止一直无法效仿,因为根据经验确定,启用阻塞行为会导致某些现有发行版无法启动。

相反,引入了新的getrandom() 系统调用,使用户空间应用程序能够安全地访问系统随机数生成器成为可能,而不会在现有 Linux 发行版的系统初始化过程中引入难以调试的死锁问题。

getrandom() 可用性对 Python 的影响

在引入getrandom() 系统调用之前,根本无法以可证明安全的方式访问 Linux 系统随机数生成器,因此我们被迫选择从/dev/urandom 读取作为最佳可用选项。但是,由于getrandom() 坚持引发错误或阻塞而不是返回可预测的数据,以及具有其他优势,因此它现在是访问 Linux 上内核 RNG 的推荐方法,直接读取/dev/urandom 降级为“遗留”状态。这使 Linux 与 Windows 等其他操作系统处于同一类别:实现os.urandom() 的最佳可用选项不再仅仅是从/dev/urandom 设备读取字节。

这意味着曾经是其他人(Linux 内核开发团队)的问题现在变成了 Python 的问题——在有办法检测到系统 RNG 未初始化的情况下,我们必须在每次尝试使用系统 RNG 时选择如何处理这种情况。

它可以简单地阻塞,就像在 3.5.0 中无意中实现的那样,并且如 Victor Stinner 在他竞争的 PEP 中所建议的那样

# artist's impression of the CPython 3.5.0-3.5.1 behavior
def generate_unpredictable_bytes_or_block(num_bytes):
    while not system_rng_is_ready:
        wait
    return unpredictable_bytes(num_bytes)

或者它可以引发错误,就像本 PEP(在某些情况下)所建议的那样

# artist's impression of the behavior proposed in this PEP
def generate_unpredictable_bytes_or_raise(num_bytes):
    if system_rng_is_ready:
        return unpredictable_bytes(num_bytes)
    else:
        raise BlockingIOError

或者它可以显式地模拟/dev/urandom 回退行为,就像在 3.5.2rc1 中实现的那样,并且预计将在 3.5.x 周期的剩余时间内保持这种状态

# artist's impression of the CPython 3.5.2rc1+ behavior
def generate_unpredictable_bytes_or_maybe_not(num_bytes):
    if system_rng_is_ready:
        return unpredictable_bytes(num_bytes)
    else:
        return (b"p4ssw0rd" * (num_bytes // 8 + 1))[:num_bytes]

(并且与上面/dev/urandomgenerate_unpredictable_password 草图一样,相同的警告也适用于此草图。)

CPython 和标准库尝试使用操作系统的随机数生成器的地方有五个,因此必须做出此决定的五个地方

  • 初始化用于保护str.__hash__ 及其朋友免受 DoS 攻击的 SipHash(在启动时无条件调用)
  • 初始化random 模块(在导入random 时调用)
  • 为用户对os.urandom 公共 API 的调用提供服务
  • 更高级别的random.SystemRandom 公共 API

  • PEP 506添加的新的secrets模块公共API

以前,这五个地方都使用相同的底层代码,因此以相同的方式做出此决定。

整个问题最初被注意到是因为3.5.0将底层代码切换到generate_unpredictable_bytes_or_block行为,事实证明,在某些罕见情况下,Linux启动脚本尝试在系统初始化期间运行Python程序,Python启动序列在尝试初始化SipHash时阻塞,然后这触发了一个死锁,因为系统停止执行任何操作 - 包括收集新的熵 - 直到Python脚本被外部计时器强制终止。这尤其不幸,因为相关脚本从未处理不受信任的输入,因此首先不需要用可证明不可预测的随机数据初始化SipHash。这促使在3.5.2rc1中进行更改,以在所有情况下模拟旧的/dev/urandom行为(通过以非阻塞模式调用getrandom(),然后如果系统调用指示/dev/urandom池尚未完全初始化,则回退到读取/dev/urandom)。

我们不知道此类问题是否也可能存在于Fedora/RHEL/CentOS生态系统中,因为这些发行版的构建系统在运行较旧操作系统内核的服务器上使用chroot,该内核不提供getrandom()系统调用,这意味着CPython的当前构建配置编译出该系统调用的运行时检查[10]

由于random模块作为导入的副作用调用os.urandom以播种默认的全局random.Random()实例,发现了类似的问题。

我们没有收到任何关于直接调用os.urandom()random.SystemRandom()在3.5.0或3.5.1中阻塞的具体投诉 - 只有由于解释器启动时的隐式阻塞以及导入random模块的副作用而导致的问题报告。

独立于此PEP,前两种情况已经更新为永远不会阻塞,而不管os.urandom()的行为如何。

PEP 524建议使后三种情况中的所有三种都隐式阻塞的情况下,此PEP建议仅对最后一种情况(secrets)模块采用这种方法,其中os.urandom()random.SystemRandom()在检测到底层操作系统调用将阻塞时引发异常。

参考文献

有关此PEP和Victor的竞争PEP中未包含的其他背景详细信息,还可以参阅Victor之前收集的相关信息和链接,网址为https://haypo-notes.readthedocs.io/summary_python_random_issue.html


来源:https://github.com/python/peps/blob/main/peps/pep-0522.rst

上次修改时间:2023-10-11 12:05:51 GMT