Skip to content

gh-89730: EncryptedClientHello support in ssl module #135435

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
121 changes: 120 additions & 1 deletion Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@
The context now uses :data:`VERIFY_X509_PARTIAL_CHAIN` and
:data:`VERIFY_X509_STRICT` in its default verify flags.


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep this line.

Exceptions
^^^^^^^^^^

Expand Down Expand Up @@ -1003,6 +1002,13 @@

.. versionadded:: 3.6

.. class:: ECHStatus

:class:`enum.IntEnum` collection of Encrypted Client Hello (ECH) statuses
returned by :meth:`SSLSocket.get_ech_status`.

.. versionadded:: TODO XXX
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. versionadded:: TODO XXX
.. versionadded:: next

And ditto for the rest


.. data:: Purpose.SERVER_AUTH

Option for :func:`create_default_context` and
Expand Down Expand Up @@ -1307,6 +1313,22 @@

.. versionadded:: 3.3

.. method:: SSLSocket.get_ech_retry_config()

When the status returned by :meth:`SSLSocket.get_ech_status` after completion of the

Check warning on line 1318 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:data reference target not found: ECHStatus.ECH_STATUS_GREASE_ECH [ref.data]
handshake is :data:`ECHStatus.ECH_STATUS_GREASE_ECH`, this method returns the
configuration value provided by the server to be used for a new connection using
ECH.

.. versionadded:: TODO XXX

.. method:: SSLSocket.get_ech_status()

Gets the status of Encrypted Client Hello (ECH) processing. Returns an
:class:`ECHStatus` instance.

.. versionadded:: TODO XXX

.. method:: SSLSocket.selected_alpn_protocol()

Return the protocol that was selected during the TLS handshake. If
Expand Down Expand Up @@ -1379,6 +1401,15 @@

.. versionadded:: 3.2

.. attribute:: SSLSocket.outer_server_hostname

Hostname of the server name used in the outer ClientHello when Encrypted Client
Hello (ECH) is used: :class:`str` type, or ``None`` for server-side socket or
if the outer server name was not specified in the constructor or the ECH
configuration.

.. versionadded:: TODO XXX

.. attribute:: SSLSocket.server_side

A boolean which is ``True`` for server-side sockets and ``False`` for
Expand Down Expand Up @@ -1680,6 +1711,24 @@

.. versionadded:: 3.5

.. method:: SSLContext.set_ech_config(ech_config)

Sets an Encrypted Client Hello (ECH) configuration, which may be discovered from
an HTTPS resource record in DNS or from :meth:`SSLSocket.get_ech_retry_config`.
Multiple calls to this functions will accumulate the set of values available for
a connection.

If the input value provided contains no suitable value (e.g. if it only contains
ECH configuration versions that are not supported), an :class:`SSLError` will be
raised.

The ech_config parameter should be a bytes-like object containing the raw ECH
configuration.

This method will raise :exc:`NotImplementedError` if :data:`HAS_ECH` is ``False``.

Check warning on line 1728 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:data reference target not found: HAS_ECH [ref.data]

.. versionadded:: TODO XXX

.. method:: SSLContext.set_npn_protocols(protocols)

Specify which protocols the socket should advertise during the SSL/TLS
Expand All @@ -1699,6 +1748,28 @@

NPN has been superseded by ALPN

.. method:: SSLContext.set_outer_alpn_protocols(protocols)

Specify which protocols the socket should advertise during the TLS
handshake in the outer ClientHello when ECH is used. The *protocols*
argument accepts the same values as for
:meth:`~SSLContext.set_alpn_protocols`.

This method will raise :exc:`NotImplementedError` if :data:`HAS_ECH` is

Check warning on line 1758 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:data reference target not found: HAS_ECH [ref.data]
``False``.

.. versionadded:: TODO XXX

.. method:: SSLContext.set_outer_server_hostname(server_hostname)

Specify which hostname the socket should advertise during the TLS
handshake in the outer ClientHello when ECH is used.

This method will raise :exc:`NotImplementedError` if :data:`HAS_ECH` is

Check warning on line 1768 in Doc/library/ssl.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:data reference target not found: HAS_ECH [ref.data]
``False``.

.. versionadded:: TODO XXX

.. attribute:: SSLContext.sni_callback

Register a callback function that will be called after the TLS Client Hello
Expand Down Expand Up @@ -2594,6 +2665,8 @@
- :meth:`~SSLSocket.verify_client_post_handshake`
- :meth:`~SSLSocket.unwrap`
- :meth:`~SSLSocket.get_channel_binding`
- :meth:`~SSLSocket.get_ech_retry_config`
- :meth:`~SSLSocket.get_ech_status`
- :meth:`~SSLSocket.version`

When compared to :class:`SSLSocket`, this object lacks the following
Expand Down Expand Up @@ -2813,6 +2886,52 @@
- TLS 1.3 features like early data, deferred TLS client cert request,
signature algorithm configuration, and rekeying are not supported yet.

Encrypted Client Hello
^^^^^^^^^^^^^^^^^^^^^^

.. versionadded:: TODO XXX

Encrypted Client Hello (ECH) allows for encrypting values that have previously only been
included unencrypted in the ClientHello records when establishing a TLS connection. To use
ECH it is necessary to provide configuration values that contain a version, algorithm
parameters, the public key to use for HPKE encryption and the "public_name" that is by
default used for the unencrypted (outer) SNI when ECH is attempted. These configuration
values may be discovered through DNS or through the "retry config" mechanism.

The following example assumes that you have discovered a set of ECH configuration values
from DNS, or *ech_configs* may be an empty list to rely on the "retry config" mechanism::

import socket
import ssl


def connect_with_tls_ech(hostname: str, ech_configs: List[str],
use_retry_config: bool=True) -> ssl.SSLSocket:
context = ssl.create_default_context()
for ech_config in ech_configs:
context.set_ech_config(ech_config)
with socket.create_connection((hostname, 443)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
if (ssock.get_ech_status == ECHStatus.ECH_STATUS_GREASE_ECH
and use_retry_config):
return connect_with_ech(hostname, [ssock.get_ech_retry_config()],
False)
return ssock

hostname = "www.python.org"
ech_configs = [] # Replace with a call to a function to lookup
# ECH configurations in DNS

ssock = connect_with_tls_ech(hostname, ech_configs)

The following classes, methods, and attributes will be useful for using ECH:

- :class:`ECHStatus`
- :meth:`SSLContext.set_ech_config`
- :meth:`SSLContext.set_outer_alpn_protocols`
- :meth:`SSLContext.set_outer_server_hostname`
- :meth:`SSLSocket.get_ech_status`
- :meth:`SSLSocket.get_ech_retry_config`

.. seealso::

Expand Down
61 changes: 51 additions & 10 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@
lambda name: name.startswith('CERT_'),
source=_ssl)

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

PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_TLS
_PROTOCOL_NAMES = {value: name for name, value in _SSLMethod.__members__.items()}

Expand Down Expand Up @@ -459,7 +464,7 @@ def wrap_socket(self, sock, server_side=False,
suppress_ragged_eofs=suppress_ragged_eofs,
server_hostname=server_hostname,
context=self,
session=session
session=session,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert this change, it's not needed.

)

def wrap_bio(self, incoming, outgoing, server_side=False,
Expand Down Expand Up @@ -502,16 +507,13 @@ 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)

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 @@ -831,6 +833,14 @@ def context(self):
def context(self, ctx):
self._sslobj.context = ctx

@property
def outer_server_hostname(self) -> str:
"""The server name used in the outer ClientHello."""
if self._sslobj:
return self._sslobj.get_ech_status()[2]
else:
raise ValueError("No SSL wrapper around " + str(self))
Comment on lines +836 to +842
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@property
def outer_server_hostname(self) -> str:
"""The server name used in the outer ClientHello."""
if self._sslobj:
return self._sslobj.get_ech_status()[2]
else:
raise ValueError("No SSL wrapper around " + str(self))
@property
def outer_server_hostname(self):
"""The server name used in the outer ClientHello."""
return self._sslobj.get_ech_status()[2]

We don't use type annotations in the standard library, it's left to typeshed. And let's just propagate an AttributeError as for the other properties.


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

def get_ech_status(self):
return ECHStatus(self._sslobj.get_ech_status()[0])


def _sslcopydoc(func):
"""Copy docstring from SSLObject to SSLSocket"""
Expand All @@ -990,13 +1003,16 @@ 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, outer_server_hostname=None):
if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM:
raise NotImplementedError("only stream sockets are supported")
if server_side:
if server_hostname:
raise ValueError("server_hostname can only be specified "
"in client mode")
if outer_server_hostname:
raise ValueError("outer_server_hostname can only be specified "
"in client mode")
if session is not None:
raise ValueError("session can only be specified in "
"client mode")
Expand Down Expand Up @@ -1092,6 +1108,14 @@ def context(self, ctx):
self._context = ctx
self._sslobj.context = ctx

@property
def outer_server_hostname(self) -> str:
"""The server name used in the outer ClientHello."""
if self._sslobj:
return self._sslobj.get_ech_status()[2]
else:
raise ValueError("No SSL wrapper around " + str(self))
Comment on lines +1112 to +1117
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def outer_server_hostname(self) -> str:
"""The server name used in the outer ClientHello."""
if self._sslobj:
return self._sslobj.get_ech_status()[2]
else:
raise ValueError("No SSL wrapper around " + str(self))
def outer_server_hostname(self):
"""The server name used in the outer ClientHello."""
if self._sslobj:
return self._sslobj.get_ech_status()[2]

Other properties actually return None if sslobj is None.


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


def get_ech_status(self):
if self._sslobj:
return ECHStatus(self._sslobj.get_ech_status()[0])
else:
raise ValueError("No SSL wrapper around " + str(self))

def _real_close(self):
self._sslobj = None
super()._real_close()
Expand Down Expand Up @@ -1527,3 +1558,13 @@ 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 len(b) == 0 or len(b) > 255:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(b) == 0 or len(b) > 255:
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
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1081,6 +1081,7 @@ Michael Layzell
Michael Lazar
Peter Lazorchak
Brian Leair
Iain Learmonth
Mathieu Leduc-Hamel
Amandine Lee
Antony Lee
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Adds support for Encrypted Client Hello (ECH) to the ssl module. Clients may
use the options exposed to establish TLS connections using ECH. Third-party
libraries like dnspython can be used to query for HTTPS and SVCB records
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
libraries like dnspython can be used to query for HTTPS and SVCB records
libraries like ``dnspython`` can be used to query for HTTPS and SVCB records

that contain the public keys required to use ECH with specific servers. If
no public key is available, an option is available to "GREASE" the
connection, and it is possible to retrieve the public key from the retry
configuration sent by servers that support ECH as they terminate the initial
connection.
Comment on lines +1 to +8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description should also be put in What's New rather here.

Loading
Loading