[Security-sig] Unified TLS API for Python
Cory Benfield
cory at lukasa.co.uk
Wed Jan 11 14:01:14 EST 2017
All,
A recent change in the Fastly policy has led the distutils-sig to discover that there is a very real risk of many macOS installations of Python being entirely unable to access PyPI using pip, due to the fact that they are linked against the ancient system OpenSSL (see [1]). This problem ultimately stems from the fact that Python has never exposed the ability to use TLS backends other than OpenSSL in the standard library. As Christian and I discussed back at PyCon US 2015, the ssl module would more properly be called the openssl module, due to exposing many OpenSSL-specific concepts and behaviours in its API. This has meant that the Python ecosystem is overwhelmingly also an OpenSSL ecosystem, which is problematic on Windows and macOS (and Unices that for whatever reason aren’t interested in shipping an OpenSSL), as it has meant that Python needs to bring its own OpenSSL, and that it is troublesome to interact with the system trust database.
The first step to resolving this would be to provide a new module that exposes TLS concepts in a much more generic manner. There are two possible approaches to this. The first is to have this module be a generic concrete implementation that can be compiled against multiple TLS backends (like curl). This would require that all the relevant bindings be built into the standard library from the get-go, which provides a substantial maintenance burden on a team of people that are already understaffed maintaining the ssl module alone. The second approach is to define a generic high-level TLS interface that provides a minimal usable interface that can be implemented by both first- and third-party modules. This would allow the standard library to continue to ship with only exactly what it needs (for example, OpenSSL, SecureTransport and SChannel), but would give those who want to use more esoteric TLS choices (NSS, GnuTLS, mbedTLS are some examples) an API that they can implement that will guarantee that complying modules can use the appropriate TLS backend.
To that end, I’ve composed a draft PEP that would define this API. The current copy can be found on GitHub[2]. I’d like to see if there is interest from the Python security community in pursuing this approach to generic TLS, and what changes would be necessary to this API before we submitted it to python-dev.
Please let me know what you think.
Cory
[1]: https://mail.python.org/pipermail/distutils-sig/2017-January/029970.html
[2]: https://github.com/Lukasa/peps/pull/1
- - -
Abstract
========
This PEP would define a standard TLS interface in the form of a collection of
abstract base classes. This interface would allow Python implementations and
third-party libraries to provide bindings to TLS libraries other than OpenSSL
that can be used by tools that expect the interface provided by the Python
standard library, with the goal of reducing the dependence of the Python
ecosystem on OpenSSL.
Rationale
=========
In the 21st century it has become increasingly clear that robust and
user-friendly TLS support is an extremely important part of the ecosystem of
any popular programming language. For most of its lifetime, this role in the
Python ecosystem has primarily been served by the `ssl module`_, which provides
a Python API to the `OpenSSL library`_.
Because the ``ssl`` module is distributed with the Python standard library, it
has become the overwhelmingly most-popular method for handling TLS in Python.
An extraordinary majority of Python libraries, both in the standard library and
on the Python Package Index, rely on the ``ssl`` module for their TLS
connectivity.
Unfortunately, the preeminence of the ``ssl`` module has had a number of
unforseen side-effects that have had the effect of tying the entire Python
ecosystem tightly to OpenSSL. This has forced Python users to use OpenSSL even
in situations where it may provide a worse user experience than alternative TLS
implementations, which imposes a cognitive burden and makes it hard to provide
"platform-native" experiences.
Problems
--------
The fact that the ``ssl`` module is build into the standard library has meant
that all standard-library Python networking libraries are entirely reliant on
the OpenSSL that the Python implementation has been linked against. This
leads to the following issues:
* It is difficult to take advantage of new, higher-security TLS without
recompiling Python to get a new OpenSSL. While there are third-party bindings
to OpenSSL (e.g. `pyOpenSSL`_), these need to be shimmed into a format that
the standard library understands, forcing projects that want to use them to
maintain substantial compatibility layers.
* For Windows distributions of Python, they need to be shipped with a copy of
OpenSSL. This puts the CPython development team in the position of being
OpenSSL redistributors, potentially needing to ship security updates to the
Windows Python distributions when OpenSSL vulnerabilities are released.
* For macOS distributions of Python, they need either to be shipped with a copy
of OpenSSL or linked against the system OpenSSL library. Apple has formally
deprecated linking against the system OpenSSL library, and even if they had
not, that library version has been unsupported by upstream for nearly one
year as of the time of writing. The CPython development team has started
shipping newer OpenSSLs with the Python available from python.org, but this
has the same problem as with Windows.
* Many systems, including but not limited to Windows and macOS, do not make
their system certificate stores available to OpenSSL. This forces users to
either obtain their trust roots from elsewhere (e.g. `certifi`_) or to
attempt to export their system trust stores in some form.
Relying on `certifi`_ is less than ideal, as most system administrators do
not expect to receive security-critical software updates from PyPI.
Additionally, it is not easy to extend the `certifi`_ trust bundle to include
custom roots, or to centrally manage trust using the `certifi`_ model.
Even in situations where the system certificate stores are made available to
OpenSSL in some form, the experience is still sub-standard, as OpenSSL will
perform different validation checks than the platform-native TLS
implementation. This can lead to users experiencing different behaviour on
their browsers or other platform-native tools than they experience in Python,
with little or no recourse to resolve the problem.
* Users may wish to integrate with TLS libraries other than OpenSSL for many
other reasons, such as OpenSSL missing features (e.g. TLS 1.3 support), or
because OpenSSL is simply too large and unweildy for the platform (e.g. for
embedded Python). Those users are left with the requirement to use
third-party networking libraries that can interact with their preferred TLS
library or to shim their preferred library into the OpenSSL-specific ``ssl``
module API.
Additionally, the ``ssl`` module as implemented today limits the ability of
CPython itself to add support for alternative TLS backends, or remove OpenSSL
support entirely, should either of these become necessary or useful. The
``ssl`` module exposes too many OpenSSL-specific function calls and features to
easily map to an alternative TLS backend.
Proposal
========
This PEP proposes to introduce a few new Abstract Base Classes in Python 3.7 to
provide TLS functionality that is not so strongly tied to OpenSSL. It also
proposes to update standard library modules to use only the interface exposed
by these abstract base classes wherever possible. There are three goals here:
1. To provide a common API surface for both core and third-party developers to
target their TLS implementations to. This allows TLS developers to provide
interfaces that can be used by most Python code, and allows network
developers to have an interface that they can target that will work with a
wide range of TLS implementations.
2. To provide an API that has few or no OpenSSL-specific concepts leak through.
The ``ssl`` module today has a number of warts caused by leaking OpenSSL
concepts through to the API: the new ABCs would remove those specific
concepts.
3. To provide a path for the core development team to make OpenSSL one of many
possible TLS backends, rather than requiring that it be present on a system
in order for Python to have TLS support.
The proposed interface is laid out below.
Abstract Base Classes
---------------------
There are several interfaces that require standardisation. Those interfaces
are:
1. Configuring TLS, currently implemented by the `SSLContext`_ class in the
``ssl`` module.
2. Wrapping a socket object, currently implemented by the `SSLSocket`_ class
in the ``ssl`` module.
3. Providing an in-memory buffer for doing in-memory encryption or decryption
with no actual I/O (necessary for asynchronous I/O models), currently
implemented by the `SSLObject`_ class in the ``ssl`` module.
4. Specifying TLS cipher suites. There is currently no code for doing this in
the standard library: instead, the standard library uses OpenSSL cipher
suite strings.
5. Specifying application-layer protocols that can be negotiated during the
TLS handshake.
6. Specifying TLS versions.
7. Reporting errors to the caller, currently implemented by the `SSLError`_
class in the ``ssl`` module.
While it is technically possible to define (2) in terms of (3), for the sake of
simplicity it is easier to define these as two separate ABCs. Implementations
are of course free to implement the concrete subclasses however they see fit.
Obviously, (4) doesn't require an abstract base class: instead, it requires a
richer API for configuring supported cipher suites that can be easily updated
with supported cipher suites for different implementations.
Context
~~~~~~~
The ``Context`` abstract base class defines an object that allows configuration
of TLS. It can be thought of as a factory for ``TLSWrappedSocket`` and
``TLSWrappedBuffer`` objects.
The ``Context`` abstract base class has the following class definition::
TLSBufferObject = Union[TLSWrappedSocket, TLSWrappedBuffer]
ServerNameCallback = Callable[[TLSBufferObject, Optional[str], Context], Any]
class _BaseContext(metaclass=ABCMeta):
@property
@abstractmethod
def validate_certificates(self) -> 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``.
"""
@validate_certificates.setter
@abstractmethod
def validate_certificates(self, val: bool) -> None:
pass
@abstractmethod
def register_certificates(self,
certificates: str,
key=None: Optional[str],
password=None: Optional[Callable[[], Union[AnyStr, bytearray]]]) -> None:
"""
Loads a certificate, a number of intermediate certificates, and the
corresponding private key. These certificates will be offered to
the remote peer during the handshake if required.
The ``certificates`` argument must be a bytestring containing the
PEM-encoded certificates. The first PEM-encoded certificate must be
the leaf certificate. All subsequence certificates will be offered
as intermediate additional certificates.
The ``key`` argument, if present, must contain the PEM-encoded
private key associated with the leaf certificate. If not present,
the private key will be extracted from ``certificates``.
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 a string, bytes,
or bytearray. If the return value is a string it will be encoded as
UTF-8 before using it to decrypt the key. Alternatively a string,
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.
"""
@abstractmethod
def set_ciphers(self, ciphers: List[Ciphers]) -> None:
"""
Set the available ciphers for TLS connections created with this
context. ``ciphers`` should be a list of ciphers from the
``Cipher`` registry. If none of the ``ciphers`` provided to this
object are supported or available, a ``TLSError`` will be raised.
"""
@abstractmethod
def set_inner_protocols(self, protocols: List[NextProtocol]) -> None:
"""
Specify which protocols the socket should advertise as supported
during the TLS handshake. This may be advertised using either or
both of ALPN or NPN.
``protocols`` should be a list of acceptable protocols in the form
of ``NextProtocol`` objects, such as ``[H2, HTTP1]``, ordered by
preference. The selection of the protocol will happen during the
handshake, and will use whatever protocol negotiation mechanisms
are available and supported by both peers.
If the TLS implementation doesn't support protocol negotiation,
this method will raise ``NotImplementedError``.
"""
@abstractmethod
def set_sni_callback(self, callback: Optional[ServerNameCallback]) -> None:
"""
Register 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 ``Context``. If ``callback`` is
``None`` then the callback is disabled. Calling this function a
subsequent time will disable the previously registered callback.
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 ``Context``. The server name argument will be the
IDNA *decoded* server name.
The ``callback`` must return ``None`` 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.
"""
@abstractmethod
def set_version_range(self, lower_bound=None: Optional[TLSVersion],
upper_bound=None: Optional[TLSVersion]) -> None:
"""
Set the minumum and maximum versions of TLS that should be allowed
on TLS connections made by this context.
If present, ``lower_bound`` will set the lowest acceptable TLS
version. If present, ``upper_bound`` will set the highest
acceptable TLS version. If either argument is ``None``, this will
leave that bound unchanged.
"""
@abstractmethod
def wrap_socket(self, socket: socket.socket, server_side=False: bool,
auto_handshake=True: bool,
server_hostname=None: 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 parameter ``server_side`` is a boolean which identifies whether
server-side or client-side behavior is desired from this socket.
The parameter ``auto_handshake`` specifies whether to do the SSL
handshake automatically after doing a ``socket.connect()``, or
whether the application program will call it explicitly, by
invoking the ``TLSWrappedSocket.do_handshake()`` method. Calling
``TLSWrappedSocket.do_handshake()`` explicitly gives the program
control over the blocking behavior of the socket I/O involved in
the handshake.
On client connections, the optional 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.
Specifying ``server_hostname`` will raise a ValueError if
``server_side`` is ``True``.
"""
@abstractmethod
def wrap_buffers(self, incoming: Any, outgoing: Any,
server_side=False: bool,
server_hostname=None: Optional[str]) -> TLSWrappedBuffer:
"""
Wrap a pair of buffer objects (``incoming`` and ``outgoing``) to
create an in-memory stream for TLS. The SSL routines will read data
from ``incoming`` and decrypt it, and write encrypted data to
``outgoing``.
The ``server_side`` and ``server_hostname`` parameters have the
same meaning as in ``wrap_socket``.
"""
class ClientContext(metaclass=ABCMeta):
@abstractmethod
def wrap_socket(self, socket: socket.socket,
auto_handshake=True: bool,
server_hostname=None: 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 parameter ``auto_handshake`` specifies whether to do the SSL
handshake automatically after doing a ``socket.connect()``, or
whether the application program will call it explicitly, by
invoking the ``TLSWrappedSocket.do_handshake()`` method. Calling
``TLSWrappedSocket.do_handshake()`` explicitly gives the program
control over the blocking behavior of the socket I/O involved in
the handshake.
The optional 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.
"""
@abstractmethod
def wrap_buffers(self, incoming: Any, outgoing: Any,
server_hostname=None: Optional[str]) -> TLSWrappedBuffer:
"""
Wrap a pair of buffer objects (``incoming`` and ``outgoing``) to
create an in-memory stream for TLS. The SSL routines will read data
from ``incoming`` and decrypt it, and write encrypted data to
``outgoing``.
The ``server_hostname`` parameter has the same meaning as in
``wrap_socket``.
"""
class ServerContext(metaclass=ABCMeta):
@abstractmethod
def wrap_socket(self, socket: socket.socket,
auto_handshake=True: bool) -> 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 parameter ``auto_handshake`` specifies whether to do the SSL
handshake automatically after doing a ``socket.connect()``, or
whether the application program will call it explicitly, by
invoking the ``TLSWrappedSocket.do_handshake()`` method. Calling
``TLSWrappedSocket.do_handshake()`` explicitly gives the program
control over the blocking behavior of the socket I/O involved in
the handshake.
"""
@abstractmethod
def wrap_buffers(self, incoming: Any, outgoing: Any) -> TLSWrappedBuffer:
"""
Wrap a pair of buffer objects (``incoming`` and ``outgoing``) to
create an in-memory stream for TLS. The SSL routines will read data
from ``incoming`` and decrypt it, and write encrypted data to
``outgoing``.
"""
Socket
~~~~~~
The socket-wrapper ABC will be defined by the ``TLSWrappedSocket`` ABC, which
has the following definition::
class TLSWrappedSocket(metaclass=ABCMeta):
# The various socket methods all must be implemented. Their definitions
# have been elided from this class defintion in the PEP because they
# aren't instructive.
@abstractmethod
def do_handshake(self) -> None:
"""
Performs the TLS handshake. Also performs certificate validation
and hostname verification.
"""
@abstractmethod
def cipher(self) -> Optional[Cipher]:
"""
Returns the Cipher entry for the cipher that has been negotiated on
the connection. If no connection has been negotiated, returns
``None``.
"""
@abstractmethod
def negotiated_protocol(self) -> Optional[NextProtocol]:
"""
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 ``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 socket is tied to.
"""
@context.setter
@abstractmethod
def context(self, value: Context) -> None:
"""
Set the value of the ``Context`` object this socket is tied to.
This operation (changing the context) may not always be supported.
"""
@abstractproperty
def negotiated_tls_version(self) -> Optional[TLSVersion]:
"""
The version of TLS that has been negotiated on this connection.
"""
Buffer
~~~~~~
The buffer-wrapper ABC will be defined by the ``TLSWrappedBuffer`` ABC, which
has the following definition::
class TLSWrappedBuffer(metaclass=ABCMeta):
@abstractmethod
def read(self, len=1024: int, buffer=None: Any) -> Union[bytes, int]:
"""
Read up to ``len`` bytes of data from the input buffer and return
the result as a ``bytes`` instance. If ``buffer`` is specified,
then read into the buffer instead, and return the number of bytes
read.
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.
As at any time a re-negotiation is possible, a call to ``read()``
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.
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[Cipher]:
"""
Returns the Cipher entry for the cipher that has been negotiated on
the connection. If no connection has been negotiated, returns
``None``.
"""
@abstractmethod
def negotiated_protocol(self) -> Optional[NextProtocol]:
"""
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 ``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 socket is tied to.
"""
@context.setter
@abstractmethod
def context(self, value: Context) -> None:
"""
Set the value of the ``Context`` object this socket is tied to.
This operation (changing the context) may not always be supported.
"""
@abstractproperty
def negotiated_tls_version(self) -> Optional[TLSVersion]:
"""
The version of TLS that has been negotiated on this connection.
"""
Cipher Suites
~~~~~~~~~~~~~
Todo
Protocol Negotiation
~~~~~~~~~~~~~~~~~~~~
Both NPN and ALPN allow for protocol negotiation as part of the HTTP/2
handshake. While NPN and ALPN are, at their fundamental level, built on top of
bytestrings, string-based APIs are frequently problematic as they allow for
errors in typing that can be hard to detect.
For this reason, this module would define a type that protocol negotiation
implementations can pass and be passed. This type would wrap a bytestring, but
allow for aliases for well-known protocols. This allows us to avoid the
problems inherent in typos for well-known protocols, while allowing the full
extensibility of the protocol negotiation layer if needed.
::
NextProtocol = namedtuple('NextProtocol', ['name'])
H2 = NextProtocol(b'h2')
H2C = NextProtocol(b'h2c')
HTTP1 = NextProtocol(b'http/1.1')
WEBRTC = NextProtocol(b'webrtc')
C_WEBRTC = NextProtocol(b'c-webrtc')
FTP = NextProtocol(b'ftp')
STUN = NextProtocol(b'stun.nat-discovery')
TURN = NextProtocol(b'stun.turn')
TLS Versions
~~~~~~~~~~~~
It is often useful to be able to restrict the versions of TLS you're willing to
support. There are many security advantages in refusing to use old versions of
TLS, and some misbehaving servers will mishandle TLS clients advertising
support for newer versions.
The following enumerated type can be used to gate TLS versions. Forward-looking
applications should almost never set a maximum TLS version unless they
absolutely must, as a TLS backend that is newer than the Python that uses it
may support TLS versions that are not in this enumerated type.
Additionally, this enumerated type defines two additional flags that can always
be used to request either the lowest or highest TLS version supported by an
implementation.
::
class TLSVersion(Enum):
MINIMUM_SUPPORTED
SSLv2
SSLv3
TLSv1
TLSv1_1
TLSv1_2
TLSv1_3
MAXIMUM_SUPPORTED
Errors
~~~~~~
This module would define three base classes for use with error handling. Unlike
the other classes defined here, these classes are not *abstract*, as they have
no behaviour. They exist simply to signal certain common behaviours. Backends
should subclass these exceptions in their own packages, but needn't define any
behaviour for them. These exceptions should *never* be thrown directly, they
should always be subclassed.
The definitions of the errors are below::
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.
"""
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.
"""
Changes to the Standard Library
===============================
The portions of the standard library that interact with TLS should be revised
to use these ABCs. This will allow them to function with other TLS backends.
This includes the following modules:
- asyncio
- ftplib
- http.client
- imaplib
- nntplib
- poplib
- smtplib
Future
======
Major future TLS features may require revisions of these ABCs. These revisions
should be made cautiously: many backends may not be able to move forward
swiftly, and will be invalidated by changes in these ABCs. This is acceptable,
but wherever possible features that are specific to individual implementations
should not be added to the ABCs. The ABCs should restrict themselves to
high-level descriptions of IETF-specified features.
ToDo
====
* Consider adding a new parameter (``valid_subjects``?) to ``wrap_socket`` and
``wrap_buffers`` that specifies in a *typed* manner what kind of entries in
the SAN field are acceptable. This would break the union between SNI and
cert validation, which may be a good thing (you can't SNI an IP address, but
you can validate a cert with one if you want).
* It's annoying that there's no type signature for fileobj. Do I really have to
define one as part of this PEP? Otherwise, how do I define the types of the
arguments to ``wrap_buffers``?
* Do we need ways to control hostname validation?
* Do we need to support getpeercert? Should we always return DER instead of the
weird semi-structured thing?
* How do we load certs from locations on disk? What about HSMs?
* How do we signal to load certs from the OS? What happens if an implementation
doesn't let you *not* load those certs?
References
==========
.. _ssl module: https://docs.python.org/3/library/ssl.html
.. _OpenSSL Library: https://www.openssl.org/
.. _PyOpenSSL: https://pypi.org/project/pyOpenSSL/
.. _certifi: https://pypi.org/project/certifi/
.. _SSLContext: https://docs.python.org/3/library/ssl.html#ssl.SSLContext
.. _SSLSocket: https://docs.python.org/3/library/ssl.html#ssl.SSLSocket
.. _SSLObject: https://docs.python.org/3/library/ssl.html#ssl.SSLObject
.. _SSLError: https://docs.python.org/3/library/ssl.html#ssl.SSLError
Copyright
=========
This document has been placed in the public domain.
More information about the Security-SIG
mailing list