Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
382 changes: 377 additions & 5 deletions Doc/library/ssl.rst

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,16 @@ sqlite3
ssl
---

* Added support for :ref:`Encrypted Client Hello <ssl-encrypted-client-hello>`
(ECH) with OpenSSL 4.0 or later. Clients can supply an ECHConfigList
discovered through DNS, for example using a third-party library such as
``dnspython`` to query HTTPS or SVCB records, as the *ech_config* argument
of :meth:`~ssl.SSLContext.wrap_socket` and :meth:`~ssl.SSLContext.wrap_bio`
or with :meth:`~ssl.SSLContext.set_ech_config`.
Servers can use :meth:`~ssl.SSLContext.load_ech_keys` to load ECH private
keys and their configurations from PEM files.
(Contributed by Peter Bierma and Iain R. Learmonth in :gh:`89730`.)

* :const:`ssl.OPENSSL_VERSION_INFO` is now a named tuple.
Added :const:`~ssl.OPENSSL_API_VERSION_INFO` which provides information
about the version of the OpenSSL library that was used for building
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(fold)
STRUCT_FOR_ID(foldspaces)
STRUCT_FOR_ID(follow_symlinks)
STRUCT_FOR_ID(for_retry)
STRUCT_FOR_ID(format)
STRUCT_FOR_ID(format_spec)
STRUCT_FOR_ID(frame_buffer)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

119 changes: 100 additions & 19 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@
from _ssl import _SSLContext, MemoryBIO, SSLSession
from _ssl import (
SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError,
SSLSyscallError, SSLEOFError, SSLCertVerificationError
SSLSyscallError, SSLEOFError, SSLCertVerificationError,
SSLECHRejectedError
)
from _ssl import txt2obj as _txt2obj, nid2obj as _nid2obj
from _ssl import RAND_status, RAND_add, RAND_bytes
Expand All @@ -116,7 +117,8 @@

from _ssl import (
HAS_SNI, HAS_ECDH, HAS_NPN, HAS_ALPN, HAS_SSLv2, HAS_SSLv3, HAS_TLSv1,
HAS_TLSv1_1, HAS_TLSv1_2, HAS_TLSv1_3, HAS_PSK, HAS_PSK_TLS13, HAS_PHA
HAS_TLSv1_1, HAS_TLSv1_2, HAS_TLSv1_3, HAS_PSK, HAS_PSK_TLS13, HAS_PHA,
HAS_ECH
)
from _ssl import _DEFAULT_CIPHERS, _OPENSSL_API_VERSION

Expand Down Expand Up @@ -150,6 +152,12 @@
lambda name: name.startswith('CERT_'),
source=_ssl)

if HAS_ECH:
_IntEnum._convert_(
'ECHStatus', __name__,
lambda name: name.startswith('ECH_STATUS_'),
source=_ssl)

PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_TLS
_PROTOCOL_NAMES = frozendict({
value: name for name, value in _SSLMethod.__members__.items()})
Expand Down Expand Up @@ -374,7 +382,8 @@ def _ipaddress_match(cert_ipaddress, host_ip):
return ip == host_ip


def _check_sslobject_params(server_side, context=None, server_hostname=None, session=None):
def _check_sslobject_params(server_side, context=None, server_hostname=None,
session=None, ech_config=None):
"""Raises a ValueError if SSLObject._create() parameters aren't valid.
"""
if server_side:
Expand All @@ -384,6 +393,11 @@ def _check_sslobject_params(server_side, context=None, server_hostname=None, ses
if session is not None:
raise ValueError("session can only be specified in "
"client mode")
if ech_config is not None:
raise ValueError("ech_config can only be specified in "
"client mode")
if ech_config is not None and not HAS_ECH:
raise NotImplementedError("ECH is not supported by the OpenSSL library")
if context.check_hostname and not server_hostname:
raise ValueError("check_hostname requires server_hostname")

Expand Down Expand Up @@ -464,7 +478,7 @@ def _encode_hostname(self, hostname):
def wrap_socket(self, sock, server_side=False,
do_handshake_on_connect=True,
suppress_ragged_eofs=True,
server_hostname=None, session=None):
server_hostname=None, session=None, ech_config=None):
# SSLSocket class handles server_hostname encoding before it calls
# ctx._wrap_socket()
return self.sslsocket_class._create(
Expand All @@ -474,17 +488,18 @@ def wrap_socket(self, sock, server_side=False,
suppress_ragged_eofs=suppress_ragged_eofs,
server_hostname=server_hostname,
context=self,
session=session
session=session,
ech_config=ech_config,
)

def wrap_bio(self, incoming, outgoing, server_side=False,
server_hostname=None, session=None):
server_hostname=None, session=None, ech_config=None):
# Need to encode server_hostname here because _wrap_bio() can only
# handle ASCII str.
return self.sslobject_class._create(
incoming, outgoing, server_side=server_side,
server_hostname=self._encode_hostname(server_hostname),
session=session, context=self,
session=session, context=self, ech_config=ech_config,
)

def set_npn_protocols(self, npn_protocols):
Expand Down Expand Up @@ -517,16 +532,14 @@ def shim_cb(sslobj, servername, sslctx):
self.sni_callback = shim_cb

def set_alpn_protocols(self, alpn_protocols):
protos = bytearray()
for protocol in alpn_protocols:
b = bytes(protocol, 'ascii')
if len(b) == 0 or len(b) > 255:
raise SSLError('ALPN protocols must be 1 to 255 in length')
protos.append(len(b))
protos.extend(b)

protos = _encode_alpn_protocol_list(alpn_protocols)
self._set_alpn_protocols(protos)

if HAS_ECH:
def set_outer_alpn_protocols(self, alpn_protocols):
protos = _encode_alpn_protocol_list(alpn_protocols)
self._set_outer_alpn_protocols(protos)

def _load_windows_store_certs(self, storename, purpose):
try:
for cert, encoding, trust in enum_certificates(storename):
Expand Down Expand Up @@ -825,15 +838,19 @@ def __init__(self, *args, **kwargs):

@classmethod
def _create(cls, incoming, outgoing, server_side=False,
server_hostname=None, session=None, context=None):
server_hostname=None, session=None, context=None,
ech_config=None):
_check_sslobject_params(server_side=server_side, context=context,
server_hostname=server_hostname, session=session)
server_hostname=server_hostname,
session=session, ech_config=ech_config)
self = cls.__new__(cls)
sslobj = context._wrap_bio(
incoming, outgoing, server_side=server_side,
server_hostname=server_hostname,
owner=self, session=session
)
if ech_config is not None:
sslobj.set_ech_config(ech_config)
self._sslobj = sslobj
return self

Expand All @@ -846,6 +863,12 @@ def context(self):
def context(self, ctx):
self._sslobj.context = ctx

if HAS_ECH:
@property
def outer_server_hostname(self):
"""The server name used in the outer ClientHello."""
return self._sslobj.get_ech_status()[2]

@property
def session(self):
"""The SSLSession for client socket."""
Expand Down Expand Up @@ -995,6 +1018,23 @@ def version(self):
def verify_client_post_handshake(self):
return self._sslobj.verify_client_post_handshake()

if HAS_ECH:
def set_ech_config(self, ech_config):
"""Set the ECH configuration for this connection.
This replaces any configuration inherited from the context
and must be called before the handshake starts.
"""
self._sslobj.set_ech_config(ech_config)

def get_ech_status(self):
"""Return the status of Encrypted Client Hello processing."""
return ECHStatus(self._sslobj.get_ech_status()[0])

def get_ech_retry_config(self):
"""Return the server's ECH retry configuration as bytes."""
return self._sslobj.get_ech_retry_config()


def _sslcopydoc(func):
"""Copy docstring from SSLObject to SSLSocket"""
Expand All @@ -1021,11 +1061,12 @@ def __init__(self, *args, **kwargs):
@classmethod
def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
suppress_ragged_eofs=True, server_hostname=None,
context=None, session=None):
context=None, session=None, ech_config=None):
if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM:
raise NotImplementedError("only stream sockets are supported")
_check_sslobject_params(server_side=server_side, context=context,
server_hostname=server_hostname, session=session)
server_hostname=server_hostname,
session=session, ech_config=ech_config)

sock_timeout = sock.gettimeout()
kwargs = dict(
Expand All @@ -1039,6 +1080,7 @@ def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
try:
self._context = context
self._session = session
self._ech_config = ech_config
self._closed = False
self._sslobj = None
self.server_side = server_side
Expand Down Expand Up @@ -1097,6 +1139,8 @@ def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
self, server_side, self.server_hostname,
owner=self, session=self._session,
)
if self._ech_config is not None:
self._sslobj.set_ech_config(self._ech_config)
if do_handshake_on_connect:
timeout = self.gettimeout()
if timeout == 0.0:
Expand All @@ -1121,6 +1165,13 @@ def context(self, ctx):
self._context = ctx
self._sslobj.context = ctx

if HAS_ECH:
@property
def outer_server_hostname(self):
"""The server name used in the outer ClientHello."""
if self._sslobj is not None:
return self._sslobj.get_ech_status()[2]

@property
@_sslcopydoc
def session(self):
Expand Down Expand Up @@ -1422,6 +1473,23 @@ def verify_client_post_handshake(self):
else:
raise ValueError("No SSL wrapper around " + str(self))

if HAS_ECH:
@_sslcopydoc
def set_ech_config(self, ech_config):
self._ech_config = ech_config
if self._sslobj is not None:
self._sslobj.set_ech_config(ech_config)

@_sslcopydoc
def get_ech_status(self):
if self._sslobj is not None:
return ECHStatus(self._sslobj.get_ech_status()[0])

@_sslcopydoc
def get_ech_retry_config(self):
if self._sslobj is not None:
return self._sslobj.get_ech_retry_config()

def _real_close(self):
self._sslobj = None
super()._real_close()
Expand Down Expand Up @@ -1449,6 +1517,8 @@ def _real_connect(self, addr, connect_ex):
owner=self, session=self._session
)
try:
if self._ech_config is not None:
self._sslobj.set_ech_config(self._ech_config)
if connect_ex:
rc = super().connect_ex(addr)
else:
Expand Down Expand Up @@ -1588,3 +1658,14 @@ def get_server_certificate(addr, ssl_version=PROTOCOL_TLS_CLIENT,

def get_protocol_name(protocol_code):
return _PROTOCOL_NAMES.get(protocol_code, '<unknown>')


def _encode_alpn_protocol_list(alpn_protocols):
protos = bytearray()
for protocol in alpn_protocols:
b = bytes(protocol, 'ascii')
if not b or len(b) > 255:
raise SSLError('ALPN protocols must be 1 to 255 in length')
protos.append(len(b))
protos.extend(b)
return protos
10 changes: 10 additions & 0 deletions Lib/test/certdata/ech-encrypted.pem

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Lib/test/certdata/ech-localhost.pem

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Lib/test/certdata/ech.pem

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading