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

Python 增强提案

PEP 748 – Python 的统一 TLS API

作者:
Joop van de Pol <joop.vandepol at trailofbits.com>,William Woodruff <william at yossarian.net>
发起人:
Alyssa Coghlan <ncoghlan at gmail.com>
讨论至:
Discourse 帖子
状态:
草案
类型:
标准跟踪
创建日期:
2024 年 6 月 27 日
Python 版本:
3.14
发布历史:
2024 年 4 月 17 日
取代:
543

目录

摘要

本 PEP 定义了一个标准 TLS 接口,其形式为协议类的集合。此接口将允许 Python 实现和第三方库提供对 OpenSSL 以外的 TLS 库的绑定。

这些绑定可以被期望使用 Python 标准库提供的接口的工具使用,目标是减少 Python 生态系统对 OpenSSL 的依赖。

基本原理

越来越清楚的是,健壮且用户友好的 TLS 支持是任何流行编程语言生态系统极其重要的一部分。在 Python 生态系统的大部分生命周期中,这一角色主要由 ssl 模块服务,该模块提供了 OpenSSL 库的 Python API。

由于 ssl 模块随 Python 标准库分发,它已成为 Python 中处理 TLS 的最受欢迎的方法。大多数 Python 库,无论是在标准库中还是在 Python Package Index 上,都依赖 ssl 模块进行 TLS 连接。

不幸的是,ssl 模块的卓越地位导致了整个 Python 生态系统与 OpenSSL 紧密捆绑。这迫使 Python 用户即使在 OpenSSL 可能提供比替代 TLS 实现更糟糕的用户体验的情况下也要使用 OpenSSL,这增加了认知负担,并使得提供“平台原生”体验变得困难。

问题

由于 ssl 模块内置于标准库中,这意味着所有标准库 Python 网络库都完全依赖于 Python 实现所链接的 OpenSSL。这导致以下问题:

  • 在不重新编译 Python 以获取新的 OpenSSL 的情况下,很难利用新的、更高安全性的 TLS。虽然有第三方 OpenSSL 绑定(例如 pyOpenSSL),但这些绑定需要被包装成标准库能够理解的格式,这迫使想要使用它们的项目维护大量的兼容层。
  • Python 的 Windows 发行版需要附带一份 OpenSSL 副本。这使得 CPython 开发团队处于 OpenSSL 再分发者的地位,当 OpenSSL 漏洞发布时,可能需要向 Windows Python 发行版提供安全更新。
  • Python 的 macOS 发行版需要附带一份 OpenSSL 副本,或者链接到系统 OpenSSL 库。Apple 已正式弃用链接到系统 OpenSSL 库,即使没有,该库版本在上游已有一年多未受支持(截至撰写本文时)。CPython 开发团队已开始在 python.org 提供的 Python 中附带更新的 OpenSSL,但这与 Windows 的问题相同。
  • 用户可能希望出于其他原因与其他 TLS 库集成,例如维护负担与系统提供的实现相比,或者因为 OpenSSL 对于他们的平台来说太大且笨重(例如对于嵌入式 Python)。这些用户仍然需要使用能够与他们首选 TLS 库交互的第三方网络库,或者将他们首选的库包装到 OpenSSL 特定的 ssl 模块 API 中。

此外,目前实现的 ssl 模块限制了 CPython 本身添加对替代 TLS 实现支持的能力,或在必要或有用时完全移除 OpenSSL 支持的能力。ssl 模块暴露了太多的 OpenSSL 特定的函数调用和特性,以至于难以轻松映射到替代 TLS 实现。

提案

本 PEP 提议在 Python 3.14 中引入几个新的协议类,以提供不那么强烈地绑定到 OpenSSL 的 TLS 功能。它还提议更新标准库模块,尽可能只使用这些协议类暴露的接口。这里有三个目标:

  1. 为核心和第三方开发者提供一个共同的 API 表面,以实现他们的 TLS 实现。这允许 TLS 开发者提供可以被大多数 Python 代码使用的接口,并允许网络开发者拥有一个可以针对并适用于各种 TLS 实现的接口。
  2. 提供一个几乎没有 OpenSSL 特定的概念泄露的 API。当前的 ssl 模块存在一些由于 OpenSSL 概念泄露到 API 而导致的缺陷:新的协议类将移除这些特定的概念。
  3. 为核心开发团队提供一条路径,使 OpenSSL 成为众多可能的 TLS 实现之一,而不是要求它存在于系统上,以便 Python 拥有 TLS 支持。

拟议的接口如下所示。

接口

有几个接口需要标准化。这些接口是:

  1. 配置 TLS,目前由 ssl 模块中的 SSLContext 类实现。
  2. 提供一个内存缓冲区,用于进行内存中的加密或解密,而无需实际 I/O(异步 I/O 模型必需),目前由 ssl 模块中的 SSLObject 类实现。
  3. 包装套接字对象,目前由 ssl 模块中的 SSLSocket 类实现。
  4. 将 TLS 配置应用于 (2) 和 (3) 中的包装对象。目前这也由 ssl 模块中的 SSLContext 类实现。
  5. 指定 TLS 密码套件。标准库中目前没有用于执行此操作的代码:相反,标准库使用 OpenSSL 密码套件字符串。
  6. 指定在 TLS 握手期间可以协商的应用层协议。
  7. 指定 TLS 版本。
  8. 向调用者报告错误,目前由 ssl 模块中的 SSLError 类实现。
  9. 指定要加载的证书,无论是作为客户端还是服务器证书。
  10. 指定应使用哪个信任数据库来验证远程对等方提供的证书。
  11. 在运行时获取这些接口的方法。

为简单起见,本 PEP 提议删除接口 (3) 和 (4),并用一个更简单的接口代替,该接口返回一个套接字,确保通过该套接字的所有通信都受 TLS 保护。换句话说,此接口将套接字初始化、TLS 握手、服务器名称指示 (SNI) 等概念视为创建客户端或服务器连接的原子部分。但是,仍支持内存缓冲区,因为它们对于异步通信很有用。

显然,(5) 不需要协议类:相反,它需要一个更丰富的 API 来配置支持的密码套件,该 API 可以轻松更新以支持不同实现的密码套件。

(9) 是一个棘手的问题,因为在一个理想的世界中,与这些证书关联的私钥永远不会在 Python 进程的内存中(也就是说,TLS 库将与硬件安全模块 (HSM) 协作,以这样一种方式提供私钥,使其无法从进程内存中提取)。因此,我们需要提供一个可扩展的证书提供模型,允许具体实现提供这种更高级别的安全性,同时对于那些无法提供的实现也允许较低的门槛。这个较低的门槛将与现状相同:也就是说,证书可以从内存缓冲区、磁盘文件加载,或额外通过与系统证书存储对应的任意 ID 引用。

(10) 也提出了一个问题,因为不同的 TLS 实现允许用户选择信任存储的方式差异很大。一些实现有特定的信任存储格式,只有它们可以使用(例如由 c_rehash 创建的 OpenSSL CA 目录格式),而另一些可能不允许您指定不包含其默认信任存储的信任存储。另一方面,大多数实现将支持某种形式的加载自定义 DER 或 PEM 编码证书。

因此,我们需要提供一个对信任存储形式要求很低的模型,同时保持与其他实现的类型兼容性。下面的“证书”、“私钥”和“信任存储”部分将更详细地说明如何实现这一点。

最后,此 API 将分离当前由 SSLContext 对象承担的职责:具体来说,持有和管理配置的职责以及使用该配置构建缓冲区或套接字的职责。

这主要是为了支持服务器名称指示 (SNI) 等功能。在 OpenSSL(以及 ssl 模块)中,服务器能够响应客户端告知服务器正在尝试访问的主机名来修改 TLS 配置。这主要用于更改证书链,以便为给定主机名呈现正确的 TLS 证书链。实现此功能的特定机制是,在用户提供的 SNI 回调函数中,返回一个新的 SSLContext 对象,其中包含适当的配置。

这种模式不能很好地映射到其他 TLS 实现,并给用户带来了编写回调函数的负担。相反,我们建议具体实现者在收到相关证书后,为每个用户透明地处理 SNI。

因此,我们将 SSLContext 的职责拆分为两个独立的 对象,每个对象又分为服务器和客户端版本。TLSServerConfigurationTLSClientConfiguration 对象充当 TLS 配置的容器:ClientContext 和 ServerContext 对象分别使用 TLSClientConfigurationTLSServerConfiguration 对象实例化,并用于创建缓冲区或套接字。所有四个对象都将是不可变的。

注意

以下 API 声明统一使用类型提示来帮助阅读。

配置

TLSServerConfigurationTLSClientConfiguration 具体类定义了可以持有和管理 TLS 配置的对象。这些类的目标如下:

  1. 提供一种指定 TLS 配置的方法,避免输入错误的风险(这排除了使用简单字典)。
  2. 提供一个可以安全地与其他配置对象进行比较以检测 TLS 配置更改的对象,用于 SNI 回调。

这些类不是协议类,主要是因为它们不期望具有特定于实现的行为。将 TLSServerConfigurationTLSClientConfiguration 对象转换为给定 TLS 实现的有用配置集的责任属于下面讨论的上下文对象。

这些类还有一个值得注意的属性:它们是不可变的。出于几个原因,这是一个理想的特性。最重要的是,默认的不可变性是一种良好的工程实践。作为一个附带的好处,它允许这些对象用作字典键,这对于特定的 TLS 实现及其 SNI 配置可能很有用。最重要的是,它使实现者不必担心他们的配置对象在其脚下被更改,这使得他们能够避免需要在其具体数据结构和配置对象之间仔细同步更改。

这些对象是可扩展的:也就是说,Python 的未来版本可能会在这些对象中添加配置字段,因为它们变得有用。出于向后兼容的目的,新字段只追加到这些对象。现有字段永远不会被删除、重命名或重新排序。它们在客户端和服务器之间进行拆分,以最大程度地减少 API 混淆。

TLSClientConfiguration 类将由以下代码定义:

class TLSClientConfiguration:
    __slots__ = (
        "_certificate_chain",
        "_ciphers",
        "_inner_protocols",
        "_lowest_supported_version",
        "_highest_supported_version",
        "_trust_store",
    )

    def __init__(
        self,
        certificate_chain: SigningChain | None = None,
        ciphers: Sequence[CipherSuite] | None = None,
        inner_protocols: Sequence[NextProtocol | bytes] | None = None,
        lowest_supported_version: TLSVersion | None = None,
        highest_supported_version: TLSVersion | None = None,
        trust_store: TrustStore | None = None,
    ) -> None:
        if inner_protocols is None:
            inner_protocols = []

        self._certificate_chain = certificate_chain
        self._ciphers = ciphers
        self._inner_protocols = inner_protocols
        self._lowest_supported_version = lowest_supported_version
        self._highest_supported_version = highest_supported_version
        self._trust_store = trust_store

    @property
    def certificate_chain(self) -> SigningChain | None:
        return self._certificate_chain

    @property
    def ciphers(self) -> Sequence[CipherSuite | int] | None:
        return self._ciphers

    @property
    def inner_protocols(self) -> Sequence[NextProtocol | bytes]:
        return self._inner_protocols

    @property
    def lowest_supported_version(self) -> TLSVersion | None:
        return self._lowest_supported_version

    @property
    def highest_supported_version(self) -> TLSVersion | None:
        return self._highest_supported_version

    @property
    def trust_store(self) -> TrustStore | None:
        return self._trust_store

TLSServerConfiguration 对象与客户端对象类似,不同之处在于它将 Sequence[SigningChain] 作为 certificate_chain 参数。

背景

我们定义了两个上下文协议类。这些协议类定义了允许将 TLS 配置应用于特定连接的对象。它们可以被视为 TLSSocketTLSBuffer 对象的工厂。

与当前的 ssl 模块不同,我们提供了两个上下文类而不是一个。具体来说,我们提供了 ClientContextServerContext 类。这简化了 API(例如,服务器提供 wrap_socket()server_hostname 参数没有意义,但由于只有一个上下文类,该参数仍然可用),并确保实现尽早知道它们将服务 TLS 连接的哪一方。此外,它允许实现选择退出连接的一方或两方。

实现者应尽可能使这些类不可变:也就是说,他们应倾向于不允许用户直接修改其内部状态,而是倾向于从新的 TLSConfiguration 对象创建新的上下文。显然,协议类无法强制执行此约束,因此它们不尝试这样做。

ClientContext 协议类具有以下类定义:

class ClientContext(Protocol):
    @abstractmethod
    def __init__(self, configuration: TLSClientConfiguration) -> None:
        """Create a new client context object from a given TLS client configuration."""
        ...

    @property
    @abstractmethod
    def configuration(self) -> TLSClientConfiguration:
        """Returns the TLS client configuration that was used to create the client context."""
        ...

    @abstractmethod
    def connect(self, address: tuple[str | None, int]) -> TLSSocket:
        """Creates a TLSSocket that behaves like a socket.socket, and
        contains information about the TLS exchange
        (cipher, negotiated_protocol, negotiated_tls_version, etc.).
        """
        ...

    @abstractmethod
    def create_buffer(self, server_hostname: str) -> TLSBuffer:
        """Creates a TLSBuffer that acts as an in-memory channel,
        and contains information about the TLS exchange
        (cipher, negotiated_protocol, negotiated_tls_version, etc.)."""
        ...

ServerContext 类似,只是它接受 TLSServerConfiguration

套接字

上下文可用于创建套接字,这些套接字必须遵循 TLSSocket 协议类的规范。具体来说,实现需要实现以下功能:

  • recvsend
  • listenaccept
  • 关闭
  • 获取套接字名称
  • 获取对等方名称

它们还需要实现一些提供有关 TLS 连接信息的接口,例如:

  • 用于创建此套接字的底层上下文对象
  • 协商的密码
  • 协商的“下一个”协议
  • 协商的 TLS 版本

以下代码更详细地描述了这些函数:

class TLSSocket(Protocol):
    """This class implements a socket.socket-like object that creates an OS
    socket, wraps it in an SSL context, and provides read and write methods
    over that channel."""

    @abstractmethod
    def __init__(self, *args: tuple, **kwargs: tuple) -> None:
        """TLSSockets should not be constructed by the user.
        The implementation should implement a method to construct a TLSSocket
        object and call it in ClientContext.connect() and
        ServerContext.connect()."""
        ...

    @abstractmethod
    def recv(self, bufsize: int) -> bytes:
        """Receive data from the socket. The return value is a bytes object
        representing the data received. Should not work before the handshake
        is completed."""
        ...

    @abstractmethod
    def send(self, bytes: bytes) -> int:
        """Send data to the socket. The socket must be connected to a remote socket."""
        ...

    @abstractmethod
    def close(self, force: bool = False) -> None:
        """Shuts down the connection and mark the socket closed.
        If force is True, this method should send the close_notify alert and shut down
        the socket without waiting for the other side.
        If force is False, this method should send the close_notify alert and raise
        the WantReadError exception until a corresponding close_notify alert has been
        received from the other side.
        In either case, this method should return WantWriteError if sending the
        close_notify alert currently fails."""
        ...

    @abstractmethod
    def listen(self, backlog: int) -> None:
        """Enable a server to accept connections. If backlog is specified, it
        specifies the number of unaccepted connections that the system will allow
        before refusing new connections."""
        ...

    @abstractmethod
    def accept(self) -> tuple[TLSSocket, tuple[str | None, int]]:
        """Accept a connection. The socket must be bound to an address and listening
        for connections. The return value is a pair (conn, address) where conn is a
        new TLSSocket object usable to send and receive data on the connection, and
        address is the address bound to the socket on the other end of the connection."""
        ...

    @abstractmethod
    def getsockname(self) -> tuple[str | None, int]:
        """Return the local address to which the socket is connected."""
        ...

    @abstractmethod
    def getpeercert(self) -> bytes | None:
        """
        Return the raw DER bytes of the certificate provided by the peer
        during the handshake, if applicable.
        """
        ...

    @abstractmethod
    def getpeername(self) -> tuple[str | None, int]:
        """Return the remote address to which the socket is connected."""
        ...

    @property
    @abstractmethod
    def context(self) -> ClientContext | ServerContext:
        """The ``Context`` object this socket is tied to."""
        ...

    @abstractmethod
    def cipher(self) -> CipherSuite | int | None:
        """
        Returns the CipherSuite entry for the cipher that has been negotiated on the connection.

        If no connection has been negotiated, returns ``None``. If the cipher negotiated is not
        defined in CipherSuite, returns the 16-bit integer representing that cipher directly.
        """
        ...

    @abstractmethod
    def negotiated_protocol(self) -> NextProtocol | bytes | None:
        """
        Returns the protocol that was selected during the TLS handshake.

        This selection may have been made using ALPN or some future
        negotiation mechanism.

        If the negotiated protocol is one of the protocols defined in the
        ``NextProtocol`` enum, the value from that enum will be returned.
        Otherwise, the raw bytestring of the negotiated protocol will be
        returned.

        If ``Context.set_inner_protocols()`` was not called, if the other
        party does not support protocol negotiation, if this socket does
        not support any of the peer's proposed protocols, or if the
        handshake has not happened yet, ``None`` is returned.
        """
        ...

    @property
    @abstractmethod
    def negotiated_tls_version(self) -> TLSVersion | None:
        """The version of TLS that has been negotiated on this connection."""
        ...

缓冲区

上下文也可用于创建缓冲区,这些缓冲区必须遵循 TLSBuffer 协议类的规范。具体来说,实现需要实现以下功能:

  • readwrite
  • 执行握手
  • 关闭
  • process_incomingprocess_outgoing
  • incoming_bytes_bufferedoutgoing_bytes_buffered
  • 获取对等证书

与套接字情况类似,它们还需要实现一些提供有关 TLS 连接信息的接口,例如:

  • 用于创建此缓冲区的底层上下文对象
  • 协商的密码
  • 协商的“下一个”协议
  • 协商的 TLS 版本

以下代码更详细地描述了这些函数:

class TLSBuffer(Protocol):
    """This class implements an in memory-channel that creates two buffers,
    wraps them in an SSL context, and provides read and write methods over
    that channel."""

    @abstractmethod
    def read(self, amt: int, buffer: Buffer | None) -> bytes | int:
        """
        Read up to ``amt`` bytes of data from the input buffer and return
        the result as a ``bytes`` instance. If an optional buffer is
        provided, the result is written into the buffer and the number of
        bytes is returned instead.

        Once EOF is reached, all further calls to this method return the
        empty byte string ``b''``.

        May read "short": that is, fewer bytes may be returned than were
        requested.

        Raise ``WantReadError`` or ``WantWriteError`` if there is
        insufficient data in either the input or output buffer and the
        operation would have caused data to be written or read.

        May raise ``RaggedEOF`` if the connection has been closed without a
        graceful TLS shutdown. Whether this is an exception that should be
        ignored or not is up to the specific application.

        As at any time a re-negotiation is possible, a call to ``read()``
        can also cause write operations.
        """
        ...

    @abstractmethod
    def write(self, buf: Buffer) -> int:
        """
        Write ``buf`` in encrypted form to the output buffer and return the
        number of bytes written. The ``buf`` argument must be an object
        supporting the buffer interface.

        Raise ``WantReadError`` or ``WantWriteError`` if there is
        insufficient data in either the input or output buffer and the
        operation would have caused data to be written or read. In either
        case, users should endeavour to resolve that situation and then
        re-call this method. When re-calling this method users *should*
        re-use the exact same ``buf`` object, as some implementations require that
        the exact same buffer be used.

        This operation may write "short": that is, fewer bytes may be
        written than were in the buffer.

        As at any time a re-negotiation is possible, a call to ``write()``
        can also cause read operations.
        """
        ...

    @abstractmethod
    def do_handshake(self) -> None:
        """
        Performs the TLS handshake. Also performs certificate validation
        and hostname verification.
        """
        ...

    @abstractmethod
    def cipher(self) -> CipherSuite | int | None:
        """
        Returns the CipherSuite entry for the cipher that has been
        negotiated on the connection. If no connection has been negotiated,
        returns ``None``. If the cipher negotiated is not defined in
        CipherSuite, returns the 16-bit integer representing that cipher
        directly.
        """
        ...

    @abstractmethod
    def negotiated_protocol(self) -> NextProtocol | bytes | None:
        """
        Returns the protocol that was selected during the TLS handshake.
        This selection may have been made using ALPN, NPN, or some future
        negotiation mechanism.

        If the negotiated protocol is one of the protocols defined in the
        ``NextProtocol`` enum, the value from that enum will be returned.
        Otherwise, the raw bytestring of the negotiated protocol will be
        returned.

        If ``Context.set_inner_protocols()`` was not called, if the other
        party does not support protocol negotiation, if this socket does
        not support any of the peer's proposed protocols, or if the
        handshake has not happened yet, ``None`` is returned.
        """
        ...

    @property
    @abstractmethod
    def context(self) -> ClientContext | ServerContext:
        """
        The ``Context`` object this buffer is tied to.
        """
        ...

    @property
    @abstractmethod
    def negotiated_tls_version(self) -> TLSVersion | None:
        """
        The version of TLS that has been negotiated on this connection.
        """
        ...

    @abstractmethod
    def shutdown(self) -> None:
        """
        Performs a clean TLS shut down. This should generally be used
        whenever possible to signal to the remote peer that the content is
        finished.
        """
        ...

    @abstractmethod
    def process_incoming(self, data_from_network: bytes) -> None:
        """
        Receives some TLS data from the network and stores it in an
        internal buffer.

        If the internal buffer is overfull, this method will raise
        ``WantReadError`` and store no data. At this point, the user must
        call ``read`` to remove some data from the internal buffer
        before repeating this call.
        """
        ...

    @abstractmethod
    def incoming_bytes_buffered(self) -> int:
        """
        Returns how many bytes are in the incoming buffer waiting to be processed.
        """
        ...

    @abstractmethod
    def process_outgoing(self, amount_bytes_for_network: int) -> bytes:
        """
        Returns the next ``amt`` bytes of data that should be written to
        the network from the outgoing data buffer, removing it from the
        internal buffer.
        """
        ...

    @abstractmethod
    def outgoing_bytes_buffered(self) -> int:
        """
        Returns how many bytes are in the outgoing buffer waiting to be sent.
        """
        ...

    @abstractmethod
    def getpeercert(self) -> bytes | None:
        """
        Return the raw DER bytes of the certificate provided by the peer
        during the handshake, if applicable.
        """
        ...

密码套件

以真正的库无关方式支持密码套件是一项极其困难的任务。不同的 TLS 实现通常在指定密码套件方面具有截然不同的 API,但更成问题的是,这些 API 在功能和风格上也经常不同。

下面是不同密码套件选择 API 的示例。这些示例并非旨在强制针对每个 API 实现,仅旨在阐明每个 API 所施加的限制。

OpenSSL

OpenSSL 使用一种众所周知的密码字符串格式。这种格式已被大多数使用 OpenSSL 的产品(包括 Python)采纳为配置语言。这种格式相对容易阅读,但有许多缺点:它是一个字符串,这使得提供错误输入很容易;它缺乏详细的验证,这意味着可能以某种方式配置 OpenSSL,使其完全无法协商任何密码;它允许以多种不同方式指定密码套件,这使得解析变得棘手。这种格式最大的问题是它没有正式的规范,这意味着解析给定字符串的唯一方法是让 OpenSSL 解析它。

OpenSSL 的密码字符串可能看起来像这样:

"ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!eNULL:!MD5"

此字符串展示了 OpenSSL 格式的一些复杂性。例如,一个条目可以指定多个密码套件:条目 ECDH+AESGCM 表示“所有包含椭圆曲线 Diffie-Hellman 密钥交换和伽罗瓦/计数器模式下的 AES 的密码套件”。更明确地说,它将扩展为四个密码套件:

"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"

这使得解析完整的 OpenSSL 密码字符串变得极其复杂。再加上还有其他元字符,例如“!”(排除所有符合此条件的密码套件,即使它们原本会被包含:“!MD5”表示不应包含使用 MD5 哈希算法的任何密码套件)、“-”(如果匹配的密码已包含,则排除它们,但如果它们再次被包含,则允许稍后重新添加它们)和“+”(包含匹配的密码,但将它们放在列表的末尾),您会得到一个极其复杂的解析格式。除了这种复杂性之外,还应注意实际结果取决于 OpenSSL 版本,因为 OpenSSL 密码字符串只要包含 OpenSSL 识别的至少一个密码就是有效的。

OpenSSL 对其密码的命名也与相关规范中使用的名称不同。有关更多详细信息,请参阅 ciphers(1) 的手册页。

OpenSSL 内部用于密码字符串的实际 API 很简单:

char *cipher_list = <some cipher list>;
int rc = SSL_CTX_set_cipher_list(context, cipher_list);

这意味着此模块使用的任何格式都必须能够转换为 OpenSSL 密码字符串以供 OpenSSL 使用。

网络框架

网络框架是 macOS (10.15+) 系统 TLS 库。该库在许多方面比 OpenSSL 受到更多限制,因为它具有更受限制的用户类别。其中一个主要的限制是控制支持的密码套件。

网络框架中的密码由 Objective-C uint16_t 枚举表示。此枚举每个密码套件有一个条目,没有聚合条目,这意味着如果没有手工编码每个枚举成员所属的类别,则无法重现 OpenSSL 密码字符串(如 “ECDH+AESGCM”)的含义。

然而,大多数枚举成员的名称与密码套件的正式名称一致:也就是说,OpenSSL 称为 “ECDHE-ECDSA-AES256-GCM-SHA384” 的密码套件在网络框架中称为 “tls_ciphersuite_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384”

网络框架中用于配置密码套件的 API 很简单:

void sec_protocol_options_append_tls_ciphersuite(sec_protocol_options_t options, tls_ciphersuite_t ciphersuite);
SChannel

SChannel 是 Windows 系统 TLS 库。

SChannel 对控制可用 TLS 密码套件的支持极具限制性,此外还采用了第三种方法来表达支持的 TLS 密码套件。

具体来说,SChannel 定义了一组 ALG_ID 常量(C 无符号整数)。这些常量中的每一个都不指整个密码套件,而是指单个算法。例如 CALG_3DESCALG_AES_256,它们指密码套件中使用的批量加密算法;CALG_ECDH_EPHEMCALG_RSA_KEYX,它们指密码套件中使用的密钥交换算法的一部分;CALG_SHA_256CALG_SHA_384,它们指密码套件中使用的消息认证码;以及 CALG_ECDSACALG_RSA_SIGN,它们指密钥交换算法的签名部分。

在 SChannel API 的早期版本中,这些常量用于定义可以使用的算法。然而,最新版本使用这些常量来禁止可以使用的算法。

这可以被认为是 OpenSSL 功能的一半,而 Network Framework 没有:Network Framework 只允许指定精确的密码套件(以及有限数量的预定义密码套件组),而 SChannel 只允许指定密码套件的一部分,而 OpenSSL 则两者都允许。

确定给定连接允许使用哪些密码套件是通过提供指向这些 ALG_ID 常量数组的指针来完成的。这意味着任何合适的 API 都必须允许 Python 代码确定必须提供哪些 ALG_ID 常量。

网络安全服务 (NSS)

NSS 是 Mozilla 的加密和 TLS 库。它用于 Firefox、Thunderbird,并作为 OpenSSL 的替代品用于多个库,例如 curl。

默认情况下,NSS 带有允许密码的安全配置。在某些平台(例如 Fedora)上,已启用密码列表是在系统策略中全局配置的。通常,除非有特殊原因,否则应用程序不应修改密码套件。

NSS 具有用于密码套件的进程全局和每个连接设置。它没有像 OpenSSL 那样的 SSLContext 概念。可以轻松模拟 SSLContext 类似行为。具体来说,可以使用 SSL_CipherPrefSetDefault(PRInt32 cipher, PRBool enabled) 全局启用或禁用密码,并使用 SSL_CipherPrefSet(PRFileDesc *fd, PRInt32 cipher, PRBool enabled) 针对连接进行操作。密码 PRInt32 数字是直接对应于注册的 IANA ID 的有符号 32 位整数,例如 0x1301TLS_AES_128_GCM_SHA256。与 OpenSSL 相反,密码的优先级顺序是固定的,不能在运行时修改。

与网络框架类似,NSS 没有用于聚合条目的 API。一些 NSS 的消费者已经实现了从 OpenSSL 密码名称和规则到 NSS 密码的自定义映射,例如 mod_nss

拟议接口

新模块的拟议接口受上述实现组合限制的影响。具体来说,由于除 OpenSSL 之外的每个实现都要求提供每个单独的密码,因此别无选择,只能提供这种最低公分母方法。

最简单的方法是提供一个枚举类型,其中包含为 TLS 定义的大部分密码套件。枚举成员的值将是其在 TLS 握手中使用的两个八位字节密码标识符,存储为 16 位整数。枚举成员的名称将是其 IANA 注册的密码套件名称。

截至目前,IANA 密码套件注册表包含超过 320 个密码套件。其中大部分密码套件与网络服务的 TLS 连接无关。其他套件指定了已弃用且不安全的算法,这些算法在最近的实现版本中不再提供。该枚举包含为 TLS v1.3 定义的五个固定密码套件。对于 TLS v1.2,它只包含与 TLS v1.3 密码套件对应的密码套件,带有 ECDHE 密钥交换(用于前向保密)和 ECDSA 或 RSA 签名,这另外有十个密码套件。

除了这个枚举,该接口还定义了 TLS v1.2 的默认密码套件列表,该列表仅包含那些基于 AES-GCM 或 ChaCha20-Poly1305 的已定义密码套件。TLS v1.3 的默认密码套件列表将包含规范中定义的五个密码套件。

当前的枚举非常受限制,仅包含提供前向保密的密码套件。由于枚举不包含所有已定义的密码,并且为了允许面向未来的应用程序,此 API 中所有接受 CipherSuite 对象的部分也将直接接受原始 16 位整数。

class CipherSuite(IntEnum):
    """
    Known cipher suites.

    See: <https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml>
    """

    TLS_AES_128_GCM_SHA256 = 0x1301
    TLS_AES_256_GCM_SHA384 = 0x1302
    TLS_CHACHA20_POLY1305_SHA256 = 0x1303
    TLS_AES_128_CCM_SHA256 = 0x1304
    TLS_AES_128_CCM_8_SHA256 = 0x1305
    TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xC02B
    TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xC02C
    TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xC02F
    TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xC030
    TLS_ECDHE_ECDSA_WITH_AES_128_CCM = 0xC0AC
    TLS_ECDHE_ECDSA_WITH_AES_256_CCM = 0xC0AD
    TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 = 0xC0AE
    TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8 = 0xC0AF
    TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA8
    TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 = 0xCCA9

对于网络框架,这些枚举成员直接引用密码套件常量的值。例如,网络框架将密码套件枚举成员 tls_ciphersuite_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 定义为具有值 0xC02C。这并非巧合,它与上述枚举中的值完全相同。这使得网络框架与上述枚举之间的映射变得非常容易。

对于 SChannel,由于 SChannel 配置的是密码而不是密码套件,因此没有简单的直接映射。这代表了 SChannel 的一个持续关注点,即与其他 TLS 实现相比,它很难以特定方式配置。

就本 PEP 而言,任何 SChannel 实现都需要根据枚举成员来确定要选择的密码。这可能比实际的密码套件列表希望允许的更开放,也可能更严格,具体取决于实现的选择。本 PEP 建议更严格,但这当然无法强制执行。

最后,我们期望对于大多数用户来说,安全的默认设置就足够了。当未指定密码列表时,实现应使用安全的默认设置(可能源自系统推荐设置)。

协议协商

ALPN 允许作为 HTTP/2 握手的一部分进行协议协商。虽然 ALPN 在根本上是建立在字节串之上的,但基于字符串的 API 经常存在问题,因为它们允许输入错误,而这些错误很难检测。

因此,本模块将定义一种类型,协议协商实现可以传递和被传递。这种类型将包装一个字节串,以允许对知名协议的别名。这使我们能够避免知名协议中的拼写错误所固有的问题,同时允许用户直接传递字节串,从而在需要时充分扩展协议协商层。

class NextProtocol(Enum):
    """The underlying negotiated ("next") protocol."""

    H2 = b"h2"
    H2C = b"h2c"
    HTTP1 = b"http/1.1"
    WEBRTC = b"webrtc"
    C_WEBRTC = b"c-webrtc"
    FTP = b"ftp"
    STUN = b"stun.nat-discovery"
    TURN = b"stun.turn"

TLS 版本

能够限制您愿意支持的 TLS 版本通常很有用。拒绝使用旧版 TLS 有许多安全优势,一些行为不当的服务器会错误处理通告支持更新版本的 TLS 客户端。

以下枚举类型可用于控制 TLS 版本。面向未来的应用程序几乎不应设置最大 TLS 版本,除非绝对必要,因为比使用它的 Python 更新的 TLS 实现可能支持此枚举类型中不存在的 TLS 版本。

此外,此枚举类型还定义了两个额外的标志,它们始终可用于请求实现支持的最低或最高 TLS 版本。至于密码套件,我们期望对于大多数用户来说,安全的默认设置就足够了。当未指定 TLS 版本列表时,实现应使用安全的默认设置(可能源自系统推荐设置)。

class TLSVersion(Enum):
    """
    TLS versions.

    The `MINIMUM_SUPPORTED` and `MAXIMUM_SUPPORTED` variants are "open ended",
    and refer to the "lowest mutually supported" and "highest mutually supported"
    TLS versions, respectively.
    """

    MINIMUM_SUPPORTED = "MINIMUM_SUPPORTED"
    TLSv1_2 = "TLSv1.2"
    TLSv1_3 = "TLSv1.3"
    MAXIMUM_SUPPORTED = "MAXIMUM_SUPPORTED"

错误

本模块将定义四个用于错误处理的基类。与此处定义的许多其他类不同,这些类不是抽象的,因为它们没有行为。它们仅仅是为了表示某些常见行为而存在的。TLS 实现应在其自己的包中子类化这些异常,但不需要为它们定义任何行为。

通常,具体实现应子类化这些异常,而不是直接抛出它们。这使得在调试意外错误时更容易确定正在使用的具体 TLS 实现。但是,这不是强制性的。

错误定义如下:

class TLSError(Exception):
    """
    The base exception for all TLS related errors from any implementation.

    Catching this error should be sufficient to catch *all* TLS errors,
    regardless of what implementation is used.
    """


class WantWriteError(TLSError):
    """
    A special signaling exception used only when non-blocking or buffer-only I/O is used.

    This error signals that the requested
    operation cannot complete until more data is written to the network,
    or until the output buffer is drained.

    This error is should only be raised when it is completely impossible
    to write any data. If a partial write is achievable then this should
    not be raised.
    """


class WantReadError(TLSError):
    """
    A special signaling exception used only when non-blocking or buffer-only I/O is used.

    This error signals that the requested
    operation cannot complete until more data is read from the network, or
    until more data is available in the input buffer.

    This error should only be raised when it is completely impossible to
    write any data. If a partial write is achievable then this should not
    be raised.
    """


class RaggedEOF(TLSError):
    """A special signaling exception used when a TLS connection has been
    closed gracelessly: that is, when a TLS CloseNotify was not received
    from the peer before the underlying TCP socket reached EOF. This is a
    so-called "ragged EOF".

    This exception is not guaranteed to be raised in the face of a ragged
    EOF: some implementations may not be able to detect or report the
    ragged EOF.

    This exception is not always a problem. Ragged EOFs are a concern only
    when protocols are vulnerable to length truncation attacks. Any
    protocol that can detect length truncation attacks at the application
    layer (e.g. HTTP/1.1 and HTTP/2) is not vulnerable to this kind of
    attack and so can ignore this exception.
    """


class ConfigurationError(TLSError):
    """An special exception that implementations can use when the provided
    configuration uses features not supported by that implementation."""

证书

本模块将定义一个具体的证书类。该类几乎没有行为,因为本模块的目标不是提供 X.509 证书可能提供的所有相关密码功能。相反,我们所需要的只是能够向具体实现发出证书来源信号的能力。

因此,此证书类定义了三个属性,对应于三个设想的构造函数:来自文件、来自内存或来自任意标识符的证书。实现可能不支持所有这些构造函数,它们可以通过下面“运行时”部分所述的方式向用户传达这一点。特别是,来自任意标识符的证书预计主要对希望基于 HSM、TPM、SSM 及类似设备构建集成的用户有用。

具体来说,此类别不解析任何提供的输入以验证其是否为正确的证书,也不提供任何形式的对特定证书的自省。TLS 实现也不需要提供此类自省。在握手期间接收的对等证书以原始 DER 字节提供。

class Certificate:
    """Object representing a certificate used in TLS."""

    __slots__ = (
        "_buffer",
        "_path",
        "_id",
    )

    def __init__(
        self, buffer: bytes | None = None, path: os.PathLike[str] | None = None, id: bytes | None = None
    ):
        """
        Creates a Certificate object from a path, buffer, or ID.

        If none of these is given, an exception is raised.
        """

        if buffer is None and path is None and id is None:
            raise ValueError("Certificate cannot be empty.")

        self._buffer = buffer
        self._path = path
        self._id = id

    @classmethod
    def from_buffer(cls, buffer: bytes) -> Certificate:
        """
        Creates a Certificate object from a byte buffer. This byte buffer
        may be either PEM-encoded or DER-encoded. If the buffer is PEM
        encoded it *must* begin with the standard PEM preamble (a series of
        dashes followed by the ASCII bytes "BEGIN CERTIFICATE" and another
        series of dashes). In the absence of that preamble, the
        implementation may assume that the certificate is DER-encoded
        instead.
        """
        return cls(buffer=buffer)

    @classmethod
    def from_file(cls, path: os.PathLike[str]) -> Certificate:
        """
        Creates a Certificate object from a file on disk. The file on disk
        should contain a series of bytes corresponding to a certificate that
        may be either PEM-encoded or DER-encoded. If the bytes are PEM encoded
        it *must* begin with the standard PEM preamble (a series of dashes
        followed by the ASCII bytes "BEGIN CERTIFICATE" and another series of
        dashes). In the absence of that preamble, the implementation may
        assume that the certificate is DER-encoded instead.
        """
        return cls(path=path)

    @classmethod
    def from_id(cls, id: bytes) -> Certificate:
        """
        Creates a Certificate object from an arbitrary identifier. This may
        be useful for implementations that rely on system certificate stores.
        """
        return cls(id=id)

私钥

本模块将定义一个具体的私钥类。与 Certificate 类非常相似,该类具有三个属性,对应于三个构造函数,并且具有 Certificate 类的所有注意事项。

class PrivateKey:
    """Object representing a private key corresponding to a public key
    for a certificate used in TLS."""

    __slots__ = (
        "_buffer",
        "_path",
        "_id",
    )

    def __init__(
        self, buffer: bytes | None = None, path: os.PathLike | None = None, id: bytes | None = None
    ):
        """
        Creates a PrivateKey object from a path, buffer, or ID.

        If none of these is given, an exception is raised.
        """

        if buffer is None and path is None and id is None:
            raise ValueError("PrivateKey cannot be empty.")

        self._buffer = buffer
        self._path = path
        self._id = id

    @classmethod
    def from_buffer(cls, buffer: bytes) -> PrivateKey:
        """
        Creates a PrivateKey object from a byte buffer. This byte buffer
        may be either PEM-encoded or DER-encoded. If the buffer is PEM
        encoded it *must* begin with the standard PEM preamble (a series of
        dashes followed by the ASCII bytes "BEGIN", the key type, and
        another series of dashes). In the absence of that preamble, the
        implementation may assume that the private key is DER-encoded
        instead.
        """
        return cls(buffer=buffer)

    @classmethod
    def from_file(cls, path: os.PathLike) -> PrivateKey:
        """
        Creates a PrivateKey object from a file on disk. The file on disk
        should contain a series of bytes corresponding to a certificate that
        may be either PEM-encoded or DER-encoded. If the bytes are PEM encoded
        it *must* begin with the standard PEM preamble (a series of dashes
        followed by the ASCII bytes "BEGIN", the key type, and another series
        of dashes). In the absence of that preamble, the implementation may
        assume that the certificate is DER-encoded instead.
        """
        return cls(path=path)

    @classmethod
    def from_id(cls, id: bytes) -> PrivateKey:
        """
        Creates a PrivateKey object from an arbitrary identifier. This may
        be useful for implementations that rely on system private key stores.
        """
        return cls(id=id)

签名链

为了进行身份验证,TLS 参与者需要提供一个叶证书,其链条一直延伸到另一方信任的某个根证书。服务器始终需要向客户端进行身份验证,但客户端也可以在客户端身份验证期间向服务器进行身份验证。此外,叶证书必须附带一个私钥,私钥可以存储在一个单独的对象中,也可以与叶证书本身一起存储。本模块将这些对象的集合定义为 SigningChain,如下所示:

class SigningChain:
    """Object representing a certificate chain used in TLS."""

    leaf: tuple[Certificate, PrivateKey | None]
    chain: list[Certificate]

    def __init__(
        self,
        leaf: tuple[Certificate, PrivateKey | None],
        chain: Sequence[Certificate] | None = None,
    ):
        """Initializes a SigningChain object."""
        self.leaf = leaf
        if chain is None:
            chain = []
        self.chain = list(chain)

如上配置类所示,客户端在客户端认证的情况下可以有一个签名链,否则没有。服务器可以有一个签名链序列,这在它负责多个域时很有用。

信任存储

如上所述,加载信任存储会带来一个问题,因为不同的 TLS 实现允许用户选择信任存储的方式差异很大。因此,我们需要提供一个对信任存储形式要求很低的模型。

这个问题与 CertificatePrivateKey 类型需要解决的问题相同。因此,我们使用完全相同的模型,通过创建一个具体的类来捕获用户如何定义信任存储的各种方式。

给定的 TLS 实现不需要处理所有可能的信任存储。然而,强烈建议给定的 TLS 实现尽可能处理 system 构造函数,因为这是最常用的验证信任存储。TLS 实现可以按照下面“运行时”部分所述的方式传达不受支持的选项。

class TrustStore:
    """
    The trust store that is used to verify certificate validity.
    """

    __slots__ = (
        "_buffer",
        "_path",
        "_id",
    )

    def __init__(
        self, buffer: bytes | None = None, path: os.PathLike | None = None, id: bytes | None = None
    ):
        """
        Creates a TrustStore object from a path, buffer, or ID.

        If none of these is given, the default system trust store is used.
        """

        self._buffer = buffer
        self._path = path
        self._id = id

    @classmethod
    def system(cls) -> TrustStore:
        """
        Returns a TrustStore object that represents the system trust
        database.
        """
        return cls()

    @classmethod
    def from_buffer(cls, buffer: bytes) -> TrustStore:
        """
        Initializes a trust store from a buffer of PEM-encoded certificates.
        """
        return cls(buffer=buffer)

    @classmethod
    def from_file(cls, path: os.PathLike) -> TrustStore:
        """
        Initializes a trust store from a single file containing PEMs.
        """
        return cls(path=path)

    @classmethod
    def from_id(cls, id: bytes) -> TrustStore:
        """
        Initializes a trust store from an arbitrary identifier.
        """
        return cls(id=id)

运行时访问

一个不常见的用例是库用户希望指定要使用的 TLS 实现,同时允许库配置实际 TLS 连接的详细信息。例如,requests 的用户可能希望能够在 Windows 和 macOS 上在 OpenSSL 或平台原生解决方案之间进行选择,或者在某些 Linux 平台上在 OpenSSL 和 NSS 之间进行选择。然而,这些用户可能不关心他们的 TLS 配置究竟是如何完成的。

这带来了两个问题:给定一个任意的具体实现,库如何:

  • 确定该实现是否支持证书或信任存储的特定构造函数(例如,来自任意标识符)?
  • 获取两个上下文类的正确类型?

证书和信任存储对象的构建应在实现之外进行。因此,实现需要提供一种方法,让用户验证实现是否与用户构建的证书和信任存储兼容。因此,每个实现都应实现一个 validate_config 方法,该方法接受 TLSClientConfigurationTLSServerConfiguration 对象,并在使用了不受支持的构造函数时引发异常。

对于类型,有两种选择:所有具体实现都被要求符合特定的命名方案,或者我们可以提供一个 API,使得获取这些对象成为可能。

本 PEP 建议我们使用第二种方法。这给予具体实现最大的自由,可以根据自己的需要组织代码,只要求它们提供一个具有适当属性的单一对象。然后,用户可以将此实现对象传递给支持它的库,这些库将负责配置和使用具体实现。

所有具体实现都必须提供获取 TLSImplementation 对象的方法。TLSImplementation 对象可以是全局单例,如果这样做有优势,也可以由可调用对象创建。

TLSImplementation 对象的定义如下:

class TLSImplementation(Generic[_ClientContext, _ServerContext]):
    __slots__ = (
        "_client_context",
        "_server_context",
        "_validate_config",
    )

    def __init__(
        self,
        client_context: type[_ClientContext],
        server_context: type[_ServerContext],
        validate_config: Callable[[TLSClientConfiguration | TLSServerConfiguration], None],
    ) -> None:
        self._client_context = client_context
        self._server_context = server_context
        self._validate_config = validate_config

前两个属性必须提供相关协议类的具体实现。例如,对于客户端上下文:

@property
def client_context(self) -> type[_ClientContext]:
    """The concrete implementation of the PEP 543 Client Context object,
    if this TLS implementation supports being the client on a TLS connection.
    """
    return self._client_context

这确保了这样的代码适用于任何实现:

client_config = TLSClientConfiguration()
client_context = implementation.client_context(client_config)

第三个属性必须提供一个函数,用于验证给定的 TLS 配置是否包含与实现兼容的证书、私钥和信任存储:

@property
def validate_config(self) -> Callable[[TLSClientConfiguration | TLSServerConfiguration], None]:
    """A function that reveals whether this TLS implementation supports a
    particular TLS configuration.
    """
    return self._validate_config

请注意,此函数只需验证证书、私钥和信任存储是否使用了受支持的构造函数。它无需解析或检索对象以进行进一步验证。

不安全用法

以上所有假设用户希望以安全的方式使用模块。有时,用户希望做一些不明智的事情,例如出于测试目的禁用证书验证。为此,我们提出了一个单独的 insecure 模块,允许用户这样做。该模块包含配置、上下文和实现对象的不安全变体,允许禁用证书验证以及服务器主机名检查。

此功能放置在一个单独的模块中,以尽量防止合法用户意外使用不安全功能。此外,它还定义了一个名为 SecurityWarning 的新警告,并在尝试创建不安全连接的每个步骤中发出响亮的警告。

此模块仅用于测试目的。在真实世界中,当用户想要连接到只具有自签名证书的物联网设备时,强烈建议将此证书添加到自定义信任存储中,而不是使用不安全模块禁用证书验证。

标准库的变更

标准库中与 TLS 交互的部分应修改为使用这些协议类。这将允许它们与其他 TLS 实现协同工作。这包括以下模块:

ssl 模块的迁移

当然,我们需要扩展 ssl 模块本身以符合这些协议类。此扩展将以新类的形式出现,可能在一个全新的模块中。这将允许利用当前 ssl 模块的应用程序继续这样做,同时为希望使用新 API 的应用程序和库启用新 API。

一般来说,从 ssl 模块迁移到新的协议类预计不会是线性的。这通常是可以接受的:大多数使用 ssl 模块的工具都会向用户隐藏它,因此重构以使用新模块应该是不显眼的。

然而,一个具体问题来自那些泄露 ssl 模块异常的库或应用程序,无论是作为其定义 API 的一部分还是偶然(这很容易发生)。这些工具的用户可能已经编写了能够容忍和处理 ssl 模块引发的异常的代码:迁移到此处提出的协议类可能会导致上面定义的异常被抛出,而现有的 except 块将无法捕获它们。

因此,ssl 模块迁移的一部分将要求 ssl 模块中的异常别名上面定义的那些。也就是说,它们将要求以下所有语句都成功:

assert ssl.SSLError is tls.TLSError
assert ssl.SSLWantReadError is tls.WantReadError
assert ssl.SSLWantWriteError is tls.WantWriteError

具体的实现机制超出了本 PEP 的范围,因为当前的 SSL 异常在 C 代码中定义,使其变得更加复杂,但更多细节可以在 Christian Heimes 发送给 Security-SIG 的一封电子邮件中找到。

未来

未来的主要 TLS 功能可能需要修订这些协议类。这些修订应谨慎进行:许多实现可能无法迅速推进,并且会被这些协议类的更改所失效。这是可以接受的,但应尽可能不将特定于个别实现的功能添加到协议类中。协议类应限制自己对 IETF 指定功能进行高级描述。

然而,对于此 API 的合理扩展绝对应该进行。此 API 的重点是为 Python 社区提供一个统一的最低公分母配置选项。TLS 并非静态目标,随着 TLS 的发展,此 API 也必须发展。

致谢

本 PEP 大部分内容改编自 PEP 543,该 PEP 已于 2020 年撤回。PEP 543 由 Cory Benfield 和 Christian Heimes 撰写,并获得了社区中许多个人进行的大量审查,他们为塑造该 PEP 做出了巨大贡献。对 PEP 543 和本 PEP 都提供了详细审查的有:

  • 亚历克斯·陈
  • 亚历克斯·盖诺
  • 安托万·皮特鲁
  • 阿什维尼·奥鲁甘蒂
  • 唐纳德·斯塔夫特
  • 伊桑·弗曼
  • 格利夫
  • 海内克·施劳瓦克
  • 吉姆·J·朱厄特
  • 纳撒尼尔·J·史密斯
  • Alyssa Coghlan
  • 保罗·克雷尔
  • Steve Dower
  • 史蒂文·法克勒
  • 韦斯·特纳
  • 威尔·邦德
  • 科里·本菲尔德
  • 马克-安德烈·伦堡
  • 塞思·M·拉森
  • Victor Stinner
  • 罗纳德·奥索伦

PEP 543 的进一步审查由 Security-SIG 和 python-ideas 邮件列表提供。


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

最后修改:2025-04-01 14:40:02 GMT