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日
取代者:
748

目录

摘要

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

决议

2020年6月25日:根据一位作者的当代协议以及另一位作者的过去协议,本 PEP 因底层操作系统的 API 变化而被撤回。

基本原理

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

由于 ssl 模块随 Python 标准库一起分发,它已成为 Python 中处理 TLS 最受欢迎的方法。绝大多数 Python 库,无论是在标准库中还是在 Python 包索引中,都依赖 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 存在相同的问题。
  • 许多系统,包括但不限于 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. 提供一个很少或没有 OpenSSL 特定概念泄露的 API。当前的 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 套接字相同行为的包装套接字是一件微妙而棘手的事情。但是,完全有可能根据包装缓冲区实现一个*通用*包装套接字:也就是说,可以编写一个适用于任何提供 (2) 的实现的包装套接字 (3)。因此,本 PEP 提议为包装缓冲区 (2) 提供一个 ABC,但为包装套接字 (3) 提供一个具体类。

此决定导致无法将少量 TLS 库绑定到此 ABC,因为这些 TLS 库*无法*提供包装缓冲区实现。目前最引人注目的是 Amazon 的 s2n,它目前不提供 I/O 抽象层。然而,即使这个库也认为这是一个缺失的功能,并且正在努力添加它。因此,可以安全地假设 (3) 的具体实现基于 (2) 将是一个节省大量工作量的设备和纠正错误的好工具。因此,本 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 定义,其定义如下

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 密钥交换和 Galois/计数器模式 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 id,例如 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 个套件。

由于这些限制,以及因为枚举不包含所有已定义的密码,并且为了允许面向未来的应用程序,此 API 中接受 CipherSuite 对象的所有部分也将直接接受原始 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,这些枚举成员直接引用密码套件常量的A值。例如,SecureTransport 将密码套件枚举成员 TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 定义为值 0xC02C。这并非巧合,它与上述枚举中的值完全相同。这使得 SecureTransport 和上述枚举之间的映射非常容易。

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

就本 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 加载证书。如果出现通用的接口来完成此操作,则该模块也可能更新以提供此用例的标准构造函数。

具体实现应尽可能使 Certificate 对象可哈希。这将有助于确保与单个具体实现一起使用的 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 类非常相似,该类几乎没有行为,以便为具体实现提供尽可能多的自由来小心处理密钥。

这个类具有 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 实现允许用户选择信任存储库的方式差异很大。因此,我们需要提供一个对信任存储库形式假设很少的模型。

这个问题与 Certificate 和 PrivateKey 类型需要解决的问题相同。因此,我们使用完全相同的模型,通过创建一个不透明类型来封装 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 的应用程序和库启用新 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
  • 亚历克斯·盖诺
  • 安托万·皮特鲁
  • Ashwini Oruganti
  • 唐纳德·斯塔夫特
  • 伊桑·弗曼
  • Glyph
  • Hynek Schlawack
  • Jim J Jewett
  • 纳撒尼尔·J·史密斯
  • 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

最后修改: 2025-02-01 08:59:27 GMT