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 功能。它还提议更新标准库模块,以便尽可能只使用这些抽象基类暴露的接口。这里有三个目标
- 为核心和第三方开发者提供一个共同的 API 表面,以便他们将 TLS 实现目标化。这允许 TLS 开发者提供可被大多数 Python 代码使用的接口,并允许网络开发者拥有一个可以与各种 TLS 实现一起工作的接口。
- 提供一个很少或没有 OpenSSL 特定概念泄露的 API。当前的
ssl模块由于将 OpenSSL 概念泄露到 API 中而存在一些缺陷:新的 ABC 将消除这些特定概念。 - 为核心开发团队提供一条路径,使 OpenSSL 成为众多可能的 TLS 后端之一,而不是要求它必须存在于系统上才能使 Python 拥有 TLS 支持。
提议的接口如下。
接口
有几个接口需要标准化。这些接口是
- 配置 TLS,目前由
ssl模块中的 SSLContext 类实现。 - 提供一个内存缓冲区,用于在内存中进行加密或解密,而不进行实际 I/O(异步 I/O 模型所需),目前由
ssl模块中的 SSLObject 类实现。 - 包装套接字对象,目前由
ssl模块中的 SSLSocket 类实现。 - 将 TLS 配置应用于(2)和(3)中的包装对象。目前这也由
ssl模块中的 SSLContext 类实现。 - 指定 TLS 密码套件。目前标准库中没有用于此的代码:相反,标准库使用 OpenSSL 密码套件字符串。
- 指定可在 TLS 握手期间协商的应用层协议。
- 指定 TLS 版本。
- 向调用者报告错误,目前由
ssl模块中的 SSLError 类实现。 - 指定要加载的证书,作为客户端或服务器证书。
- 指定应使用哪个信任数据库来验证远程对等方提供的证书。
- 在运行时获取这些接口的方法。
为简单起见,本 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 配置容器的对象:ClientContext 和 ServerContext 对象是使用 TLSConfiguration 对象实例化的对象。所有这三个对象都将是不可变的。
注意
以下 API 声明统一使用类型提示来帮助阅读。其中一些类型提示实际上无法在实践中使用,因为它们是循环引用的。将它们视为指导方针,而不是模块中最终代码的反映。
配置
具体的 TLSConfiguration 类定义了一个可以保存和管理 TLS 配置的对象。此类的目标如下
- 提供一种指定 TLS 配置的方法,以避免打字错误的风险(这排除了使用简单字典)。
- 提供一个可以与其他配置对象安全比较的对象,以检测 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 配置应用于特定连接的对象。它们可以被视为 TLSWrappedSocket 和 TLSWrappedBuffer 对象的工厂。
与当前的 ssl 模块不同,我们提供了两个上下文类而不是一个。具体来说,我们提供了 ClientContext 和 ServerContext 类。这简化了 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_3DES 和 CALG_AES_256,它们指的是密码套件中使用的批量加密算法;CALG_DH_EPHEM 和 CALG_RSA_KEYX,它们指的是密码套件中使用的密钥交换算法的一部分;CALG_SHA1 和 CALG_MD5,它们指的是密码套件中使用的消息认证码;以及 CALG_ECDSA 和 CALG_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,例如 0x1301 是 TLS_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