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

Python 增强提案

PEP 543 – Python 的统一 TLS API

作者:
Cory Benfield <cory at lukasa.co.uk>, Christian Heimes <christian at python.org>
状态:
已撤回
类型:
标准跟踪
创建:
2016 年 10 月 17 日
Python 版本:
3.7
历史记录:
2017 年 1 月 11 日,2017 年 1 月 19 日,2017 年 2 月 2 日,2017 年 2 月 9 日

目录

摘要

该 PEP 将定义一个标准的 TLS 接口,形式为一组抽象基类。该接口将允许 Python 实现和第三方库提供对 OpenSSL 之外的 TLS 库的绑定,这些绑定可以被期望 Python 标准库提供的接口的工具使用,目标是减少 Python 生态系统对 OpenSSL 的依赖。

决议

2020-06-25:鉴于底层操作系统的 API 发生变化,该 PEP 已与一位作者达成当代协议,并与另一位作者达成过去协议,现已撤回。

理由

在 21 世纪,越来越清楚的是,稳健且用户友好的 TLS 支持是任何流行编程语言生态系统中极其重要的组成部分。在大多数时间里,Python 生态系统中的这个角色主要由 ssl 模块 来完成,它为 OpenSSL 库 提供了 Python API。

由于 ssl 模块与 Python 标准库一起分发,因此它已成为处理 Python 中 TLS 的最流行方法。绝大多数 Python 库,包括标准库和 Python 包索引中的库,都依赖于 ssl 模块来实现 TLS 连接。

不幸的是,ssl 模块的领先地位带来了一些意想不到的副作用,这些副作用将整个 Python 生态系统紧密地绑定到 OpenSSL。这迫使 Python 用户即使在替代 TLS 实现可能提供比 OpenSSL 更好的用户体验的情况下也使用 OpenSSL,这会造成认知负担,并难以提供“平台原生”体验。

问题

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

  • 很难利用新的更高安全性 TLS,而无需重新编译 Python 以获得新的 OpenSSL。虽然存在对 OpenSSL 的第三方绑定(例如 pyOpenSSL),但这些需要被缝合到标准库可以理解的格式中,迫使想要使用它们的项目维护大量的兼容性层。
  • 对于 Windows 版本的 Python,需要与 OpenSSL 的副本一起分发。这使 CPython 开发团队成为 OpenSSL 的重新分发者,可能需要在 OpenSSL 发布漏洞时向 Windows Python 分发版发布安全更新。
  • 对于 macOS 版本的 Python,需要与 OpenSSL 的副本一起分发,或者链接到系统 OpenSSL 库。Apple 已正式弃用链接到系统 OpenSSL 库,即使他们没有,该库版本在撰写本文时也已被上游不支持近一年。CPython 开发团队已开始将 Python.org 上提供的 Python 附带更新的 OpenSSL,但这与 Windows 存在相同的问题。
  • 许多系统(包括但不限于 Windows 和 macOS)不会将其系统证书存储提供给 OpenSSL。这迫使用户要么从其他地方获取其信任根(例如 certifi),要么尝试以某种形式导出其系统信任存储。

    依赖于 certifi 不太理想,因为大多数系统管理员不希望从 PyPI 接收安全关键软件更新。此外,将 certifi 信任捆绑包扩展以包含自定义根,或使用 certifi 模型集中管理信任并不容易。

    即使在系统证书存储以某种形式提供给 OpenSSL 的情况下,体验仍然不佳,因为 OpenSSL 执行的验证检查与平台原生 TLS 实现不同。这会导致用户在浏览器或其他平台原生工具中体验到的行为与他们在 Python 中体验到的行为不同,并且几乎没有解决问题的办法。

  • 用户可能希望出于多种其他原因与 OpenSSL 之外的 TLS 库集成,例如 OpenSSL 缺少功能(例如 TLS 1.3 支持),或者因为 OpenSSL 对于平台来说太大太笨重(例如嵌入式 Python)。这些用户需要使用可以与其首选 TLS 库交互的第三方网络库,或者将其首选库缝合到 OpenSSL 特定的 ssl 模块 API 中。

此外,今天实现的 ssl 模块限制了 CPython 本身添加对替代 TLS 后端的支持或完全删除 OpenSSL 支持的能力,无论这两种情况何时变得必要或有用。 ssl 模块公开了太多 OpenSSL 特定的函数调用和功能,因此很难映射到替代 TLS 后端。

建议

该 PEP 建议在 Python 3.7 中引入一些新的抽象基类,以提供与 OpenSSL 关系不那么紧密的 TLS 功能。它还建议更新标准库模块,以便尽可能仅使用这些抽象基类公开的接口。这里有三个目标

  1. 为核心和第三方开发人员提供一个通用 API 表面,以便他们将 TLS 实现定位到该表面。这允许 TLS 开发人员提供可以被大多数 Python 代码使用的接口,并允许网络开发人员拥有可以定位的接口,该接口可以与各种 TLS 实现一起使用。
  2. 提供一个 API,该 API 几乎没有或根本没有 OpenSSL 特定的概念泄露。 ssl 模块在今天有一些由于 OpenSSL 概念泄露到 API 中而造成的弊病:新的 ABC 将消除这些特定概念。
  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 建议对 (2) 和 (3)(即缓冲区和套接字)采用统一的方法。Python 套接字 API 很大,并且实现一个与普通 Python 套接字具有相同行为的包装套接字是一件微妙而棘手的事情。但是,完全可以根据包装缓冲区实现通用包装套接字:也就是说,可以编写一个包装套接字 (3),该套接字适用于提供 (2) 的任何实现。因此,该 PEP 建议提供包装缓冲区的 ABC (2),但提供包装套接字的具体类 (3)。

该决定会使少数 TLS 库绑定到该 ABC 变得不可能,因为这些 TLS 库无法提供包装缓冲区实现。目前最著名的是 Amazon 的 s2n,它目前没有提供 I/O 抽象层。但是,即使这个库也认为这是一个缺失的功能,并且正在 努力添加它。因此,可以安全地假设根据 (2) 实现 (3) 的具体实现将是一个节省大量工作的方法,并且是一个保证正确性的好工具。因此,该 PEP 建议这样做。

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

(9) 是一个棘手的问题,因为在理想情况下,与这些证书关联的私钥永远不会出现在 Python 进程的内存中(也就是说,TLS 库将与硬件安全模块 (HSM) 协作,以提供私钥,使其无法从进程内存中提取)。因此,我们需要提供一个可扩展的证书提供模型,允许具体实现提供更高级别的安全性,同时还允许那些无法提供更高级别的安全性的实现提供更低的标准。这个较低的标准将与现状相同:也就是说,证书可以从内存缓冲区或磁盘上的文件加载。

(10) 也代表了一个问题,因为不同的 TLS 实现对用户如何选择信任库差异很大。有些实现具有特定于它们的信任库格式(例如由 c_rehash 创建的 OpenSSL CA 目录格式),而其他实现可能不允许您指定不包含其默认信任库的信任库。

因此,我们需要提供一个模型,对信任存储的格式假设很少。下面的“信任存储”部分将更详细地介绍如何实现这一点。

最后,此 API 将拆分当前由 SSLContext 对象承担的职责:具体来说,持有和管理配置的职责以及使用该配置构建包装对象的职责。

这主要是为了支持服务器名称指示 (SNI) 等功能。在 OpenSSL(以及 ssl 模块中),服务器能够修改 TLS 配置,以响应客户端告知服务器它正在尝试到达的主机名。这主要用于更改证书链,以便为给定的主机名呈现正确的 TLS 证书链。完成此操作的具体机制是返回一个具有适当配置的新 SSLContext 对象。

这并非一个与其他 TLS 实现良好映射的模型。相反,我们需要使 SNI 回调能够提供返回值,该返回值可用于指示应进行哪些配置更改。这意味着提供一个可以保存 TLS 配置的对象。此对象需要应用于特定的 TLSWrappedBuffer 和 TLSWrappedSocket 对象。

因此,我们将 SSLContext 的职责拆分为两个独立的对象。 TLSConfiguration 对象是一个充当 TLS 配置容器的对象: ClientContextServerContext 对象是使用 TLSConfiguration 对象实例化的对象。所有三个对象都将是不可变的。

注意

以下 API 声明统一使用类型提示来帮助阅读。其中一些类型提示实际上无法在实践中使用,因为它们是循环引用的。请将它们视为指南,而不是模块中最终代码的反映。

配置

TLSConfiguration 具体类定义一个可以保存和管理 TLS 配置的对象。此类的目标如下

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

此类不是 ABC,主要是因为它预计不会具有特定于实现的行为。将 TLSConfiguration 对象转换为特定 TLS 实现的有用配置集的职责属于下面讨论的 Context 对象。

此类还有另一个值得注意的属性:它是不可变的。这对于几个原因来说是一个理想的特性。最重要的是它允许这些对象用作字典键,这对于某些 TLS 后端及其 SNI 配置来说可能非常有价值。最重要的是,它使实现不再需要担心其配置对象在它们脚下发生更改,这使它们可以避免需要仔细同步其具体数据结构与配置对象之间的更改。

此对象是可扩展的:也就是说,Python 的未来版本可能会在变得有用时向此对象添加配置字段。出于向后兼容的目的,新字段仅附加到此对象。现有字段永远不会被删除、重命名或重新排序。

TLSConfiguration 对象将由以下代码定义

ServerNameCallback = Callable[[TLSBufferObject, Optional[str], TLSConfiguration], Any]


_configuration_fields = [
    'validate_certificates',
    'certificate_chain',
    'ciphers',
    'inner_protocols',
    'lowest_supported_version',
    'highest_supported_version',
    'trust_store',
    'sni_callback',
]


_DEFAULT_VALUE = object()


class TLSConfiguration(namedtuple('TLSConfiguration', _configuration_fields)):
    """
    An immutable TLS Configuration object. This object has the following
    properties:

    :param validate_certificates bool: Whether to validate the TLS
        certificates. This switch operates at a very broad scope: either
        validation is enabled, in which case all forms of validation are
        performed including hostname validation if possible, or validation
        is disabled, in which case no validation is performed.

        Not all backends support having their certificate validation
        disabled. If a backend does not support having their certificate
        validation disabled, attempting to set this property to ``False``
        will throw a ``TLSError`` when this object is passed into a
        context object.

    :param certificate_chain Tuple[Tuple[Certificate],PrivateKey]: The
        certificate, intermediate certificate, and the corresponding
        private key for the leaf certificate. These certificates will be
        offered to the remote peer during the handshake if required.

        The first Certificate in the list must be the leaf certificate. All
        subsequent certificates will be offered as intermediate additional
        certificates.

    :param ciphers Tuple[Union[CipherSuite, int]]:
        The available ciphers for TLS connections created with this
        configuration, in priority order.

    :param inner_protocols Tuple[Union[NextProtocol, bytes]]:
        Protocols that connections created with this configuration should
        advertise as supported during the TLS handshake. These may be
        advertised using either or both of ALPN or NPN. This list of
        protocols should be ordered by preference.

    :param lowest_supported_version TLSVersion:
        The minimum version of TLS that should be allowed on TLS
        connections using this configuration.

    :param highest_supported_version TLSVersion:
        The maximum version of TLS that should be allowed on TLS
        connections using this configuration.

    :param trust_store TrustStore:
        The trust store that connections using this configuration will use
        to validate certificates.

    :param sni_callback Optional[ServerNameCallback]:
        A callback function that will be called after the TLS Client Hello
        handshake message has been received by the TLS server when the TLS
        client specifies a server name indication.

        Only one callback can be set per ``TLSConfiguration``. If the
        ``sni_callback`` is ``None`` then the callback is disabled. If the
        ``TLSConfiguration`` is used for a ``ClientContext`` then this
        setting will be ignored.

        The ``callback`` function will be called with three arguments: the
        first will be the ``TLSBufferObject`` for the connection; the
        second will be a string that represents the server name that the
        client is intending to communicate (or ``None`` if the TLS Client
        Hello does not contain a server name); and the third argument will
        be the original ``TLSConfiguration`` that configured the
        connection. The server name argument will be the IDNA *decoded*
        server name.

        The ``callback`` must return a ``TLSConfiguration`` to allow
        negotiation to continue. Other return values signal errors.
        Attempting to control what error is signaled by the underlying TLS
        implementation is not specified in this API, but is up to the
        concrete implementation to handle.

        The Context will do its best to apply the ``TLSConfiguration``
        changes from its original configuration to the incoming connection.
        This will usually include changing the certificate chain, but may
        also include changes to allowable ciphers or any other
        configuration settings.
    """
    __slots__ = ()

    def __new__(cls, validate_certificates: Optional[bool] = None,
                     certificate_chain: Optional[Tuple[Tuple[Certificate], PrivateKey]] = None,
                     ciphers: Optional[Tuple[Union[CipherSuite, int]]] = None,
                     inner_protocols: Optional[Tuple[Union[NextProtocol, bytes]]] = None,
                     lowest_supported_version: Optional[TLSVersion] = None,
                     highest_supported_version: Optional[TLSVersion] = None,
                     trust_store: Optional[TrustStore] = None,
                     sni_callback: Optional[ServerNameCallback] = None):

        if validate_certificates is None:
            validate_certificates = True

        if ciphers is None:
            ciphers = DEFAULT_CIPHER_LIST

        if inner_protocols is None:
            inner_protocols = []

        if lowest_supported_version is None:
            lowest_supported_version = TLSVersion.TLSv1

        if highest_supported_version is None:
            highest_supported_version = TLSVersion.MAXIMUM_SUPPORTED

        return super().__new__(
            cls, validate_certificates, certificate_chain, ciphers,
            inner_protocols, lowest_supported_version,
            highest_supported_version, trust_store, sni_callback
        )

    def update(self, validate_certificates=_DEFAULT_VALUE,
                     certificate_chain=_DEFAULT_VALUE,
                     ciphers=_DEFAULT_VALUE,
                     inner_protocols=_DEFAULT_VALUE,
                     lowest_supported_version=_DEFAULT_VALUE,
                     highest_supported_version=_DEFAULT_VALUE,
                     trust_store=_DEFAULT_VALUE,
                     sni_callback=_DEFAULT_VALUE):
        """
        Create a new ``TLSConfiguration``, overriding some of the settings
        on the original configuration with the new settings.
        """
        if validate_certificates is _DEFAULT_VALUE:
            validate_certificates = self.validate_certificates

        if certificate_chain is _DEFAULT_VALUE:
            certificate_chain = self.certificate_chain

        if ciphers is _DEFAULT_VALUE:
            ciphers = self.ciphers

        if inner_protocols is _DEFAULT_VALUE:
            inner_protocols = self.inner_protocols

        if lowest_supported_version is _DEFAULT_VALUE:
            lowest_supported_version = self.lowest_supported_version

        if highest_supported_version is _DEFAULT_VALUE:
            highest_supported_version = self.highest_supported_version

        if trust_store is _DEFAULT_VALUE:
            trust_store = self.trust_store

        if sni_callback is _DEFAULT_VALUE:
            sni_callback = self.sni_callback

        return self.__class__(
            validate_certificates, certificate_chain, ciphers,
            inner_protocols, lowest_supported_version,
            highest_supported_version, trust_store, sni_callback
        )

上下文

我们定义了两个 Context 抽象基类。这些 ABC 定义了允许将 TLS 配置应用于特定连接的对象。可以将它们视为 TLSWrappedSocketTLSWrappedBuffer 对象的工厂。

与当前的 ssl 模块不同,我们提供了两个上下文类而不是一个。具体来说,我们提供了 ClientContextServerContext 类。这简化了 API(例如,服务器没有必要向 ssl.SSLContext.wrap_socket 提供 server_hostname 参数,但由于只有一个上下文类,因此该参数仍然可用),并确保实现尽早知道他们将服务于 TLS 连接的哪一端。此外,它允许实现选择退出连接的一端或两端。例如,macOS 上的 SecureTransport 并不真正适合服务器使用,并且在服务器端使用方面缺少大量功能。这将允许 SecureTransport 实现简单地不定义 ServerContext 的具体子类,以表明它们不支持。

与当前的 ssl 模块的另一个主要区别是,许多标志和选项已被删除。其中大多数是不言而喻的,但值得注意的是, auto_handshake 已从 wrap_socket 中删除。这是因为从根本上来说,它代表了一种奇怪的设计缺陷,它以增加用户和实现者复杂性的代价节省了极少的努力。此 PEP 要求所有用户在连接后显式调用 do_handshake

在尽可能的情况下,实现者应该旨在使这些类不可变:也就是说,他们应该更喜欢不允许用户直接改变其内部状态,而是更喜欢从新的 TLSConfiguration 对象创建新的上下文。显然,ABC 无法强制执行此约束,因此它们不会尝试这样做。

Context 抽象基类具有以下类定义

TLSBufferObject = Union[TLSWrappedSocket, TLSWrappedBuffer]


class _BaseContext(metaclass=ABCMeta):
    @abstractmethod
    def __init__(self, configuration: TLSConfiguration):
        """
        Create a new context object from a given TLS configuration.
        """

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


class ClientContext(_BaseContext):
    def wrap_socket(self,
                    socket: socket.socket,
                    server_hostname: Optional[str]) -> TLSWrappedSocket:
        """
        Wrap an existing Python socket object ``socket`` and return a
        ``TLSWrappedSocket`` object. ``socket`` must be a ``SOCK_STREAM``
        socket: all other socket types are unsupported.

        The returned SSL socket is tied to the context, its settings and
        certificates. The socket object originally passed to this method
        should not be used again: attempting to use it in any way will lead
        to undefined behaviour, especially across different TLS
        implementations. To get the original socket object back once it has
        been wrapped in TLS, see the ``unwrap`` method of the
        TLSWrappedSocket.

        The parameter ``server_hostname`` specifies the hostname of the
        service which we are connecting to. This allows a single server to
        host multiple SSL-based services with distinct certificates, quite
        similarly to HTTP virtual hosts. This is also used to validate the
        TLS certificate for the given hostname. If hostname validation is
        not desired, then pass ``None`` for this parameter. This parameter
        has no default value because opting-out of hostname validation is
        dangerous, and should not be the default behaviour.
        """
        buffer = self.wrap_buffers(server_hostname)
        return TLSWrappedSocket(socket, buffer)

    @abstractmethod
    def wrap_buffers(self, server_hostname: Optional[str]) -> TLSWrappedBuffer:
        """
        Create an in-memory stream for TLS, using memory buffers to store
        incoming and outgoing ciphertext. The TLS routines will read
        received TLS data from one buffer, and write TLS data that needs to
        be emitted to another buffer.

        The implementation details of how this buffering works are up to
        the individual TLS implementation. This allows TLS libraries that
        have their own specialised support to continue to do so, while
        allowing those without to use whatever Python objects they see fit.

        The ``server_hostname`` parameter has the same meaning as in
        ``wrap_socket``.
        """


class ServerContext(_BaseContext):
    def wrap_socket(self, socket: socket.socket) -> TLSWrappedSocket:
        """
        Wrap an existing Python socket object ``socket`` and return a
        ``TLSWrappedSocket`` object. ``socket`` must be a ``SOCK_STREAM``
        socket: all other socket types are unsupported.

        The returned SSL socket is tied to the context, its settings and
        certificates. The socket object originally passed to this method
        should not be used again: attempting to use it in any way will lead
        to undefined behaviour, especially across different TLS
        implementations. To get the original socket object back once it has
        been wrapped in TLS, see the ``unwrap`` method of the
        TLSWrappedSocket.
        """
        buffer = self.wrap_buffers()
        return TLSWrappedSocket(socket, buffer)

    @abstractmethod
    def wrap_buffers(self) -> TLSWrappedBuffer:
        """
        Create an in-memory stream for TLS, using memory buffers to store
        incoming and outgoing ciphertext. The TLS routines will read
        received TLS data from one buffer, and write TLS data that needs to
        be emitted to another buffer.

        The implementation details of how this buffering works are up to
        the individual TLS implementation. This allows TLS libraries that
        have their own specialised support to continue to do so, while
        allowing those without to use whatever Python objects they see fit.
        """

缓冲区

缓冲区包装器 ABC 将由 TLSWrappedBuffer ABC 定义,该 ABC 具有以下定义

class TLSWrappedBuffer(metaclass=ABCMeta):
    @abstractmethod
    def read(self, amt: int) -> bytes:
        """
        Read up to ``amt`` bytes of data from the input buffer and return
        the result as a ``bytes`` instance.

        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 readinto(self, buffer: Any, amt: int) -> int:
        """
        Read up to ``amt`` bytes of data from the input buffer into
        ``buffer``, which must be an object that implements the buffer
        protocol. Returns the number of bytes read.

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

        Raises ``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 read "short": that is, fewer bytes may be read than were
        requested.

        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
        ``readinto()`` can also cause write operations.
        """

    @abstractmethod
    def write(self, buf: Any) -> 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 backends 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) -> Optional[Union[CipherSuite, int]]:
        """
        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) -> Optional[Union[NextProtocol, bytes]]:
        """
        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) -> Context:
        """
        The ``Context`` object this buffer is tied to.
        """

    @abstractproperty
    def negotiated_tls_version(self) -> Optional[TLSVersion]:
        """
        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 receive_from_network(self, data):
        """
        Receives some TLS data from the network and stores it in an
        internal buffer.
        """

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

    @abstractmethod
    def consume_outgoing(self, amt):
        """
        Discard the next ``amt`` bytes from the outgoing data buffer. This
        should be used when ``amt`` bytes have been sent on the network, to
        signal that the data no longer needs to be buffered.
        """

套接字

套接字包装器类将是一个具体类,它在其构造函数中接受两个项目:一个常规套接字对象和一个 TLSWrappedBuffer 对象。此对象太大而无法在此 PEP 中重新创建,但将作为构建模块的工作的一部分提交。

包装的套接字将实现所有套接字 API,尽管它将对仅适用于类型不同于 SOCK_STREAM 的套接字的方法(例如 sendto/recvfrom)具有存根实现。此限制可以随着 DTLS 支持添加到此模块时随时解除。

此外,套接字类还将在常规套接字方法的基础上包含以下额外方法

class TLSWrappedSocket:
    def do_handshake(self) -> None:
        """
        Performs the TLS handshake. Also performs certificate validation
        and hostname verification. This must be called after the socket has
        connected (either via ``connect`` or ``accept``), before any other
        operation is performed on the socket.
        """

    def cipher(self) -> Optional[Union[CipherSuite, int]]:
        """
        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.
        """

    def negotiated_protocol(self) -> Optional[Union[NextProtocol, bytes]]:
        """
        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
    def context(self) -> Context:
        """
        The ``Context`` object this socket is tied to.
        """

    def negotiated_tls_version(self) -> Optional[TLSVersion]:
        """
        The version of TLS that has been negotiated on this connection.
        """

    def unwrap(self) -> socket.socket:
        """
        Cleanly terminate the TLS connection on this wrapped socket. Once
        called, this ``TLSWrappedSocket`` can no longer be used to transmit
        data. Returns the socket that was wrapped with TLS.
        """

密码套件

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

OpenSSL

OpenSSL 使用一种众所周知的密码字符串格式。这种格式已被大多数使用 OpenSSL 的产品(包括 Python)用作配置语言。这种格式相对易于阅读,但有一些缺点:它是一个字符串,这使得提供错误的输入变得非常容易;它缺乏许多详细的验证,这意味着可以以一种不允许它协商任何密码的方式配置 OpenSSL;并且它允许以多种不同的方式指定密码套件,这使得解析变得很困难。此格式最大的问题是它没有正式规范,这意味着解析给定字符串的方式与 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 一起使用。

SecureTransport

SecureTransport 是 macOS 系统 TLS 库。与 OpenSSL 相比,该库在许多方面受到的限制更大,因为它拥有一类用户群,而且这些用户群的限制要严格得多。其中一个重大的限制是控制支持的密码套件。

SecureTransport 中的密码由 C enum 表示。此枚举每个密码套件只有一个条目,没有聚合条目,这意味着如果没有手工编码每个枚举成员所属的类别,就不可能重现 OpenSSL 密码字符串(如“ECDH+AESGCM”)的含义。

但是,大多数枚举成员的名称与密码套件的正式名称一致:也就是说,OpenSSL 称为“ECDHE-ECDSA-AES256-GCM-SHA384” 的密码套件在 SecureTransport 中称为“TLS_ECDHE_ECDHSA_WITH_AES_256_GCM_SHA384”。

SecureTransport 内部配置密码套件的 API 很简单

SSLCipherSuite ciphers[] = {TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, ...};
OSStatus status = SSLSetEnabledCiphers(context, ciphers, sizeof(ciphers));
SChannel

SChannel 是 Windows 系统 TLS 库。

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

具体来说,SChannel 定义了一组名为 ALG_ID 的常量(C 无符号整数)。每个常量不代表整个密码套件,而是代表一个单独的算法。例如 CALG_3DESCALG_AES_256 指的是密码套件中使用的块加密算法,CALG_DH_EPHEMCALG_RSA_KEYX 指的是密码套件中使用的密钥交换算法的一部分,CALG_SHA1CALG_MD5 指的是密码套件中使用的消息认证码,CALG_ECDSACALG_RSA_SIGN 指的是密钥交换算法中的签名部分。

可以将它理解为 OpenSSL 功能中 SecureTransport 不具备的那一半:SecureTransport 仅允许指定确切的密码套件,而 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 数字是一个带符号的 32 位整数,它直接对应于注册的 IANA 标识符,例如 0x1301TLS_AES_128_GCM_SHA256。与 OpenSSL 相反,密码的优先级顺序是固定的,无法在运行时修改。

与 SecureTransport 一样,NSS 也没有针对聚合条目的 API。一些 NSS 的消费者已经实现了从 OpenSSL 密码名称和规则到 NSS 密码的自定义映射,例如 mod_nss

拟议接口

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

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

截至目前,IANA 密码套件注册表 中包含超过 320 种密码套件。其中很大一部分密码套件与网络服务 TLS 连接无关。其他套件指定了已弃用和不安全的算法,这些算法不再由最新版本的实现提供。枚举不包含使用以下内容的密码

  • 密钥交换:NULL、Kerberos (KRB5)、预共享密钥 (PSK)、安全远程传输 (TLS-SRP)
  • 身份验证:NULL、匿名、出口等级、Kerberos (KRB5)、预共享密钥 (PSK)、安全远程传输 (TLS-SRP)、DSA 证书 (DSS)
  • 加密:NULL、ARIA、DES、RC2、出口等级 40 位
  • PRF:MD5
  • SCSV 密码套件

3DES、RC4、SEED 和 IDEA 包含在内,用于遗留应用程序。此外,还包含了 TLS 1.3 草案 (draft-ietf-tls-tls13-18) 中的另外五个密码套件。TLS 1.3 不与 TLS 1.2 及更早版本共享任何密码套件。由此产生的枚举将包含大约 110 种套件。

由于这些限制,以及由于枚举不包含每个定义的密码,以及为了允许面向未来的应用程序,所有接受 CipherSuite 对象的此 API 部分也将直接接受原始 16 位整数。

我们没有手动填充此枚举,而是使用了一个 TLS 枚举脚本,该脚本从 Christian Heimes 的 tlsdb JSON 文件(警告:大文件)和 IANA 密码套件注册表 中构建它。TLSDB 还为 API 提供了扩展可能性,例如确定哪些 TLS 版本支持哪些密码,如果发现此功能有用或必要。

如果用户发现这种方法过于繁琐,那么此 API 的未来扩展可以提供可以重新引入 OpenSSL 的聚合功能的帮助程序。

class CipherSuite(IntEnum):
    TLS_RSA_WITH_RC4_128_SHA = 0x0005
    TLS_RSA_WITH_IDEA_CBC_SHA = 0x0007
    TLS_RSA_WITH_3DES_EDE_CBC_SHA = 0x000a
    TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA = 0x0010
    TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA = 0x0016
    TLS_RSA_WITH_AES_128_CBC_SHA = 0x002f
    TLS_DH_RSA_WITH_AES_128_CBC_SHA = 0x0031
    TLS_DHE_RSA_WITH_AES_128_CBC_SHA = 0x0033
    TLS_RSA_WITH_AES_256_CBC_SHA = 0x0035
    TLS_DH_RSA_WITH_AES_256_CBC_SHA = 0x0037
    TLS_DHE_RSA_WITH_AES_256_CBC_SHA = 0x0039
    TLS_RSA_WITH_AES_128_CBC_SHA256 = 0x003c
    TLS_RSA_WITH_AES_256_CBC_SHA256 = 0x003d
    TLS_DH_RSA_WITH_AES_128_CBC_SHA256 = 0x003f
    TLS_RSA_WITH_CAMELLIA_128_CBC_SHA = 0x0041
    TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA = 0x0043
    TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA = 0x0045
    TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 = 0x0067
    TLS_DH_RSA_WITH_AES_256_CBC_SHA256 = 0x0069
    TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 = 0x006b
    TLS_RSA_WITH_CAMELLIA_256_CBC_SHA = 0x0084
    TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA = 0x0086
    TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA = 0x0088
    TLS_RSA_WITH_SEED_CBC_SHA = 0x0096
    TLS_DH_RSA_WITH_SEED_CBC_SHA = 0x0098
    TLS_DHE_RSA_WITH_SEED_CBC_SHA = 0x009a
    TLS_RSA_WITH_AES_128_GCM_SHA256 = 0x009c
    TLS_RSA_WITH_AES_256_GCM_SHA384 = 0x009d
    TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 = 0x009e
    TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = 0x009f
    TLS_DH_RSA_WITH_AES_128_GCM_SHA256 = 0x00a0
    TLS_DH_RSA_WITH_AES_256_GCM_SHA384 = 0x00a1
    TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0x00ba
    TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0x00bc
    TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0x00be
    TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256 = 0x00c0
    TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256 = 0x00c2
    TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256 = 0x00c4
    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_ECDH_ECDSA_WITH_RC4_128_SHA = 0xc002
    TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA = 0xc003
    TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA = 0xc004
    TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA = 0xc005
    TLS_ECDHE_ECDSA_WITH_RC4_128_SHA = 0xc007
    TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA = 0xc008
    TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA = 0xc009
    TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA = 0xc00a
    TLS_ECDH_RSA_WITH_RC4_128_SHA = 0xc00c
    TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA = 0xc00d
    TLS_ECDH_RSA_WITH_AES_128_CBC_SHA = 0xc00e
    TLS_ECDH_RSA_WITH_AES_256_CBC_SHA = 0xc00f
    TLS_ECDHE_RSA_WITH_RC4_128_SHA = 0xc011
    TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA = 0xc012
    TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA = 0xc013
    TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA = 0xc014
    TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 = 0xc023
    TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 = 0xc024
    TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 = 0xc025
    TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384 = 0xc026
    TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 = 0xc027
    TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 = 0xc028
    TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 = 0xc029
    TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384 = 0xc02a
    TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 = 0xc02b
    TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 = 0xc02c
    TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256 = 0xc02d
    TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384 = 0xc02e
    TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 = 0xc02f
    TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 = 0xc030
    TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256 = 0xc031
    TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384 = 0xc032
    TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 = 0xc072
    TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 = 0xc073
    TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256 = 0xc074
    TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384 = 0xc075
    TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0xc076
    TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384 = 0xc077
    TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256 = 0xc078
    TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384 = 0xc079
    TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc07a
    TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc07b
    TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc07c
    TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc07d
    TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc07e
    TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc07f
    TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc086
    TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc087
    TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc088
    TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc089
    TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc08a
    TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc08b
    TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256 = 0xc08c
    TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384 = 0xc08d
    TLS_RSA_WITH_AES_128_CCM = 0xc09c
    TLS_RSA_WITH_AES_256_CCM = 0xc09d
    TLS_DHE_RSA_WITH_AES_128_CCM = 0xc09e
    TLS_DHE_RSA_WITH_AES_256_CCM = 0xc09f
    TLS_RSA_WITH_AES_128_CCM_8 = 0xc0a0
    TLS_RSA_WITH_AES_256_CCM_8 = 0xc0a1
    TLS_DHE_RSA_WITH_AES_128_CCM_8 = 0xc0a2
    TLS_DHE_RSA_WITH_AES_256_CCM_8 = 0xc0a3
    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_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = 0xccaa

枚举成员可以映射到 OpenSSL 密码名称

>>> import ssl
>>> ctx = ssl.SSLContext(ssl.PROTOCOL_TLS)
>>> ctx.set_ciphers('ALL:COMPLEMENTOFALL')
>>> ciphers = {c['id'] & 0xffff: c['name'] for c in ctx.get_ciphers()}
>>> ciphers[CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]
'ECDHE-RSA-AES128-GCM-SHA256'

对于 SecureTransport,这些枚举成员直接引用密码套件常量的值。例如,SecureTransport 定义了密码套件枚举成员 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,其值为 0xC02C。毫不奇怪,这与它在上述枚举中的值相同。这使得 SecureTransport 和上述枚举之间的映射非常容易。

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

出于本 PEP 的目的,任何 SChannel 实现都需要根据枚举成员确定要选择哪些密码。这可能比实际的密码套件列表实际允许的范围更广,也可能更严格,具体取决于实现的选择。本 PEP 建议它更严格,但当然无法强制执行。

协议协商

NPN 和 ALPN 都允许作为 HTTP/2 握手的一部分进行协议协商。虽然 NPN 和 ALPN 从根本上构建在字节串之上,但基于字符串的 API 通常会出现问题,因为它们允许出现难以检测的类型错误。

为此,本模块将定义一个类型,协议协商实现可以传递并被传递。此类型将包装一个字节串以允许为众所周知的协议提供别名。这使我们能够避免在众所周知的协议中由于拼写错误而导致的问题,同时允许用户直接传递字节串,从而在协议协商层需要时提供完全的扩展性。

class NextProtocol(Enum):
    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 版本。

class TLSVersion(Enum):
    MINIMUM_SUPPORTED = auto()
    SSLv2 = auto()
    SSLv3 = auto()
    TLSv1 = auto()
    TLSv1_1 = auto()
    TLSv1_2 = auto()
    TLSv1_3 = auto()
    MAXIMUM_SUPPORTED = auto()

错误

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

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

以下是错误的定义

class TLSError(Exception):
    """
    The base exception for all TLS related errors from any backend.
    Catching this error should be sufficient to catch *all* TLS errors,
    regardless of what backend 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.
    """

证书

本模块将定义一个抽象的 X509 证书类。此类几乎没有任何行为,因为本模块的目标不是提供 X509 证书可以提供的全部相关加密功能。相反,我们只需要能够向具体实现发出证书来源的信号。

因此,此证书实现只定义了构造函数。本质上,本模块中的证书对象可以像一个句柄一样抽象,该句柄可以用来定位一个特定的证书。

具体实现可以选择提供替代构造函数,例如,从 HSM 加载证书。如果为此目的出现了通用接口,则可以更新本模块以为此用例提供标准构造函数。

具体实现应该尽可能使证书对象可哈希。这将有助于确保与单个具体实现一起使用的 TLSConfiguration 对象也是可哈希的。

class Certificate(metaclass=ABCMeta):
    @abstractclassmethod
    def from_buffer(cls, buffer: bytes):
        """
        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.
        """

    @abstractclassmethod
    def from_file(cls, path: Union[pathlib.Path, AnyStr]):
        """
        Creates a Certificate object from a file on disk. This method may
        be a convenience method that wraps ``open`` and ``from_buffer``,
        but some TLS implementations may be able to provide more-secure or
        faster methods of loading certificates that do not involve Python
        code.
        """

私钥

本模块将定义一个抽象的私钥类。与证书类非常相似,此类几乎没有行为,以便为具体实现提供尽可能多的自由来谨慎地处理密钥。

此类具有 Certificate 类的所有注意事项。

class PrivateKey(metaclass=ABCMeta):
    @abstractclassmethod
    def from_buffer(cls,
                    buffer: bytes,
                    password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None):
        """
        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 certificate is DER-encoded
        instead.

        The key may additionally be encrypted. If it is, the ``password``
        argument can be used to decrypt the key. The ``password`` argument
        may be a function to call to get the password for decrypting the
        private key. It will only be called if the private key is encrypted
        and a password is necessary. It will be called with no arguments,
        and it should return either bytes or bytearray containing the
        password. Alternatively a bytes, or bytearray value may be supplied
        directly as the password argument. It will be ignored if the
        private key is not encrypted and no password is needed.
        """

    @abstractclassmethod
    def from_file(cls,
                  path: Union[pathlib.Path, bytes, str],
                  password: Optional[Union[Callable[[], Union[bytes, bytearray]], bytes, bytearray]] = None):
        """
        Creates a PrivateKey object from a file on disk. This method may
        be a convenience method that wraps ``open`` and ``from_buffer``,
        but some TLS implementations may be able to provide more-secure or
        faster methods of loading certificates that do not involve Python
        code.

        The ``password`` parameter behaves exactly as the equivalent
        parameter on ``from_buffer``.
        """

信任库

如上所述,加载信任存储是一个问题,因为不同的 TLS 实现对用户选择信任存储的方式有很大差异。因此,我们需要提供一个假设信任存储形式非常少的模型。

这个问题与证书和私钥类型需要解决的问题相同。因此,我们使用完全相同的模型,通过创建一个不透明类型来封装 TLS 后端可能打开信任存储的各种方式。

给定的 TLS 实现不需要实现所有构造函数。但是,强烈建议给定的 TLS 实现尽可能提供 system 构造函数,因为这是最常用的验证信任存储。具体实现也可以添加自己的构造函数。

具体实现应尽可能地使 TrustStore 对象可哈希。这将有助于确保与单个具体实现一起使用的 TLSConfiguration 对象也可哈希。

class TrustStore(metaclass=ABCMeta):
    @abstractclassmethod
    def system(cls) -> TrustStore:
        """
        Returns a TrustStore object that represents the system trust
        database.
        """

    @abstractclassmethod
    def from_pem_file(cls, path: Union[pathlib.Path, bytes, str]) -> TrustStore:
        """
        Initializes a trust store from a single file full of PEMs.
        """

运行时访问

库用户的一个常见用例是希望库控制 TLS 配置,但希望选择使用的后端。例如,Requests 用户可能希望能够在 Windows 和 macOS 上选择 OpenSSL 或平台原生解决方案,或者在某些 Linux 平台上选择 OpenSSL 和 NSS。但是,这些用户可能并不关心其 TLS 配置的具体方式。

这带来了一个问题:给定一个任意具体实现,库如何确定如何将证书加载到信任存储中?有两种选择:要么所有具体实现都必须符合特定的命名方案,要么我们可以提供一个 API,使获取这些对象成为可能。

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

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

Backend 对象具有以下定义

Backend = namedtuple(
    'Backend',
    ['client_context', 'server_context',
     'certificate', 'private_key', 'trust_store']
)

每个属性都必须提供相关 ABC 的具体实现。这确保了以下代码对任何后端都有效

trust_store = backend.trust_store.system()

标准库的更改

与 TLS 交互的标准库部分应修改为使用这些 ABC。这将使它们能够与其他 TLS 后端一起使用。这包括以下模块

  • asyncio
  • ftplib
  • http
  • imaplib
  • nntplib
  • poplib
  • smtplib
  • urllib

ssl 模块的迁移

自然地,我们需要扩展 ssl 模块本身以符合这些 ABC。此扩展将采用新类的形式,可能在全新的模块中。这将允许利用当前 ssl 模块的应用程序继续这样做,同时为想要使用它们的应用程序和库启用新的 API。

通常,从 ssl 模块迁移到新的 ABC 不是一对一的。这通常是可以接受的:大多数使用 ssl 模块的工具会将它隐藏在用户面前,因此重构以使用新模块应该是不可见的。

但是,从泄漏 ssl 模块异常的库或应用程序会产生一个具体问题,无论是作为其定义的 API 的一部分,还是偶然发生的(这很容易做到)。这些工具的用户可能编写了容忍和处理 ssl 模块抛出异常的代码:迁移到此处介绍的 ABC 可能导致上面定义的异常被抛出,而现有的 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 功能可能需要修改这些 ABC。这些修改应谨慎进行:许多后端可能无法快速向前推进,并且将被这些 ABC 的更改所无效。这是可以接受的,但只要有可能,特定于单个实现的功能就不应添加到 ABC 中。ABC 应将自己限制为对 IETF 指定功能的高级描述。

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

致谢

本文档已由社区中的许多人进行了广泛审查,他们在很大程度上帮助塑造了它。详细审查由以下人员提供

  • Alex Chan
  • Alex Gaynor
  • Antoine Pitrou
  • Ashwini Oruganti
  • Donald Stufft
  • Ethan Furman
  • Glyph
  • Hynek Schlawack
  • Jim J Jewett
  • Nathaniel J. Smith
  • Alyssa Coghlan
  • Paul Kehrer
  • Steve Dower
  • Steven Fackler
  • Wes Turner
  • Will Bond

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


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

最后修改时间: 2023-10-11 12:05:51 GMT