Skip to content

Commit 4b91de4

Browse files
ZeroIntensityirl
andcommitted
EncryptedClientHello support in ssl module
Exposes options for clients to use Encrypted Client Hello (ECH) when establishing TLS connections. It is up to the user to source the ECH public keys before making use of these options. Co-authored-by: irl <iain@learmonth.me>
1 parent 60e956e commit 4b91de4

9 files changed

Lines changed: 632 additions & 10 deletions

File tree

‎Doc/library/ssl.rst‎

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,13 @@ Constants
10321032

10331033
.. versionadded:: 3.6
10341034

1035+
.. class:: ECHStatus
1036+
1037+
:class:`enum.IntEnum` collection of Encrypted Client Hello (ECH) statuses
1038+
returned by :meth:`SSLSocket.get_ech_status`.
1039+
1040+
.. versionadded:: next
1041+
10351042
.. data:: Purpose.SERVER_AUTH
10361043

10371044
Option for :func:`create_default_context` and
@@ -1368,6 +1375,22 @@ SSL sockets also have the following additional methods and attributes:
13681375

13691376
.. versionadded:: 3.3
13701377

1378+
.. method:: SSLSocket.get_ech_retry_config()
1379+
1380+
When the status returned by :meth:`SSLSocket.get_ech_status` after completion of the
1381+
handshake is :data:`ECHStatus.ECH_STATUS_GREASE_ECH`, this method returns the
1382+
configuration value provided by the server to be used for a new connection using
1383+
ECH.
1384+
1385+
.. versionadded:: next
1386+
1387+
.. method:: SSLSocket.get_ech_status()
1388+
1389+
Gets the status of Encrypted Client Hello (ECH) processing. Returns an
1390+
:class:`ECHStatus` instance.
1391+
1392+
.. versionadded:: next
1393+
13711394
.. method:: SSLSocket.selected_alpn_protocol()
13721395

13731396
Return the protocol that was selected during the TLS handshake. If
@@ -1440,6 +1463,15 @@ SSL sockets also have the following additional methods and attributes:
14401463

14411464
.. versionadded:: 3.2
14421465

1466+
.. attribute:: SSLSocket.outer_server_hostname
1467+
1468+
Hostname of the server name used in the outer ClientHello when Encrypted Client
1469+
Hello (ECH) is used: :class:`str` type, or ``None`` for server-side socket or
1470+
if the outer server name was not specified in the constructor or the ECH
1471+
configuration.
1472+
1473+
.. versionadded:: next
1474+
14431475
.. attribute:: SSLSocket.server_side
14441476

14451477
A boolean which is ``True`` for server-side sockets and ``False`` for
@@ -1816,6 +1848,24 @@ to speed up repeated connections from the same clients.
18161848

18171849
.. versionadded:: 3.5
18181850

1851+
.. method:: SSLContext.set_ech_config(ech_config)
1852+
1853+
Sets an Encrypted Client Hello (ECH) configuration, which may be discovered from
1854+
an HTTPS resource record in DNS or from :meth:`SSLSocket.get_ech_retry_config`.
1855+
Multiple calls to this functions will accumulate the set of values available for
1856+
a connection.
1857+
1858+
If the input value provided contains no suitable value (e.g. if it only contains
1859+
ECH configuration versions that are not supported), an :class:`SSLError` will be
1860+
raised.
1861+
1862+
The ech_config parameter should be a bytes-like object containing the raw ECH
1863+
configuration.
1864+
1865+
This method will raise :exc:`NotImplementedError` if :data:`HAS_ECH` is ``False``.
1866+
1867+
.. versionadded:: next
1868+
18191869
.. method:: SSLContext.set_npn_protocols(npn_protocols)
18201870

18211871
Specify which protocols the socket should advertise during the SSL/TLS
@@ -1835,6 +1885,28 @@ to speed up repeated connections from the same clients.
18351885

18361886
NPN has been superseded by ALPN
18371887

1888+
.. method:: SSLContext.set_outer_alpn_protocols(protocols)
1889+
1890+
Specify which protocols the socket should advertise during the TLS
1891+
handshake in the outer ClientHello when ECH is used. The *protocols*
1892+
argument accepts the same values as for
1893+
:meth:`~SSLContext.set_alpn_protocols`.
1894+
1895+
This method will raise :exc:`NotImplementedError` if :data:`HAS_ECH` is
1896+
``False``.
1897+
1898+
.. versionadded:: next
1899+
1900+
.. method:: SSLContext.set_outer_server_hostname(server_hostname)
1901+
1902+
Specify which hostname the socket should advertise during the TLS
1903+
handshake in the outer ClientHello when ECH is used.
1904+
1905+
This method will raise :exc:`NotImplementedError` if :data:`HAS_ECH` is
1906+
``False``.
1907+
1908+
.. versionadded:: next
1909+
18381910
.. attribute:: SSLContext.sni_callback
18391911

18401912
Register a callback function that will be called after the TLS Client Hello
@@ -2753,6 +2825,8 @@ provided.
27532825
- :meth:`~SSLSocket.verify_client_post_handshake`
27542826
- :meth:`~SSLSocket.unwrap`
27552827
- :meth:`~SSLSocket.get_channel_binding`
2828+
- :meth:`~SSLSocket.get_ech_retry_config`
2829+
- :meth:`~SSLSocket.get_ech_status`
27562830
- :meth:`~SSLSocket.version`
27572831

27582832
When compared to :class:`SSLSocket`, this object lacks the following
@@ -2977,6 +3051,52 @@ of TLS/SSL. Some new TLS 1.3 features are not yet available.
29773051
- TLS 1.3 features like early data, deferred TLS client cert request,
29783052
and rekeying are not supported yet.
29793053

3054+
Encrypted Client Hello
3055+
^^^^^^^^^^^^^^^^^^^^^^
3056+
3057+
.. versionadded:: next
3058+
3059+
Encrypted Client Hello (ECH) allows for encrypting values that have previously only been
3060+
included unencrypted in the ClientHello records when establishing a TLS connection. To use
3061+
ECH it is necessary to provide configuration values that contain a version, algorithm
3062+
parameters, the public key to use for HPKE encryption and the "public_name" that is by
3063+
default used for the unencrypted (outer) SNI when ECH is attempted. These configuration
3064+
values may be discovered through DNS or through the "retry config" mechanism.
3065+
3066+
The following example assumes that you have discovered a set of ECH configuration values
3067+
from DNS, or *ech_configs* may be an empty list to rely on the "retry config" mechanism::
3068+
3069+
import socket
3070+
import ssl
3071+
3072+
3073+
def connect_with_tls_ech(hostname: str, ech_configs: List[str],
3074+
use_retry_config: bool=True) -> ssl.SSLSocket:
3075+
context = ssl.create_default_context()
3076+
for ech_config in ech_configs:
3077+
context.set_ech_config(ech_config)
3078+
with socket.create_connection((hostname, 443)) as sock:
3079+
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
3080+
if (ssock.get_ech_status == ECHStatus.ECH_STATUS_GREASE_ECH
3081+
and use_retry_config):
3082+
return connect_with_ech(hostname, [ssock.get_ech_retry_config()],
3083+
False)
3084+
return ssock
3085+
3086+
hostname = "www.python.org"
3087+
ech_configs = [] # Replace with a call to a function to lookup
3088+
# ECH configurations in DNS
3089+
3090+
ssock = connect_with_tls_ech(hostname, ech_configs)
3091+
3092+
The following classes, methods, and attributes will be useful for using ECH:
3093+
3094+
- :class:`ECHStatus`
3095+
- :meth:`SSLContext.set_ech_config`
3096+
- :meth:`SSLContext.set_outer_alpn_protocols`
3097+
- :meth:`SSLContext.set_outer_server_hostname`
3098+
- :meth:`SSLSocket.get_ech_status`
3099+
- :meth:`SSLSocket.get_ech_retry_config`
29803100

29813101
.. seealso::
29823102

‎Lib/ssl.py‎

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@
150150
lambda name: name.startswith('CERT_'),
151151
source=_ssl)
152152

153+
_IntEnum._convert_(
154+
'ECHStatus', __name__,
155+
lambda name: name.startswith('ECH_STATUS_'),
156+
source=_ssl)
157+
153158
PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_SSLv23 = _SSLMethod.PROTOCOL_TLS
154159
_PROTOCOL_NAMES = frozendict({
155160
value: name for name, value in _SSLMethod.__members__.items()})
@@ -517,16 +522,13 @@ def shim_cb(sslobj, servername, sslctx):
517522
self.sni_callback = shim_cb
518523

519524
def set_alpn_protocols(self, alpn_protocols):
520-
protos = bytearray()
521-
for protocol in alpn_protocols:
522-
b = bytes(protocol, 'ascii')
523-
if len(b) == 0 or len(b) > 255:
524-
raise SSLError('ALPN protocols must be 1 to 255 in length')
525-
protos.append(len(b))
526-
protos.extend(b)
527-
525+
protos = encode_alpn_protocol_list(alpn_protocols)
528526
self._set_alpn_protocols(protos)
529527

528+
def set_outer_alpn_protocols(self, alpn_protocols):
529+
protos = encode_alpn_protocol_list(alpn_protocols)
530+
self._set_outer_alpn_protocols(protos)
531+
530532
def _load_windows_store_certs(self, storename, purpose):
531533
try:
532534
for cert, encoding, trust in enum_certificates(storename):
@@ -846,6 +848,11 @@ def context(self):
846848
def context(self, ctx):
847849
self._sslobj.context = ctx
848850

851+
@property
852+
def outer_server_hostname(self):
853+
"""The server name used in the outer ClientHello."""
854+
return self._sslobj.get_ech_status()[2]
855+
849856
@property
850857
def session(self):
851858
"""The SSLSession for client socket."""
@@ -995,6 +1002,9 @@ def version(self):
9951002
def verify_client_post_handshake(self):
9961003
return self._sslobj.verify_client_post_handshake()
9971004

1005+
def get_ech_status(self):
1006+
return ECHStatus(self._sslobj.get_ech_status()[0])
1007+
9981008

9991009
def _sslcopydoc(func):
10001010
"""Copy docstring from SSLObject to SSLSocket"""
@@ -1021,11 +1031,14 @@ def __init__(self, *args, **kwargs):
10211031
@classmethod
10221032
def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
10231033
suppress_ragged_eofs=True, server_hostname=None,
1024-
context=None, session=None):
1034+
context=None, session=None, outer_server_hostname=None):
10251035
if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM:
10261036
raise NotImplementedError("only stream sockets are supported")
10271037
_check_sslobject_params(server_side=server_side, context=context,
10281038
server_hostname=server_hostname, session=session)
1039+
if server_side and outer_server_hostname:
1040+
raise ValueError("outer_server_hostname can only be specified "
1041+
"in client mode")
10291042

10301043
sock_timeout = sock.gettimeout()
10311044
kwargs = dict(
@@ -1121,6 +1134,11 @@ def context(self, ctx):
11211134
self._context = ctx
11221135
self._sslobj.context = ctx
11231136

1137+
@property
1138+
def outer_server_hostname(self):
1139+
"""The server name used in the outer ClientHello."""
1140+
return self._sslobj.get_ech_status()[2]
1141+
11241142
@property
11251143
@_sslcopydoc
11261144
def session(self):
@@ -1422,6 +1440,9 @@ def verify_client_post_handshake(self):
14221440
else:
14231441
raise ValueError("No SSL wrapper around " + str(self))
14241442

1443+
def get_ech_status(self):
1444+
return ECHStatus(self._sslobj.get_ech_status()[0])
1445+
14251446
def _real_close(self):
14261447
self._sslobj = None
14271448
super()._real_close()
@@ -1588,3 +1609,13 @@ def get_server_certificate(addr, ssl_version=PROTOCOL_TLS_CLIENT,
15881609

15891610
def get_protocol_name(protocol_code):
15901611
return _PROTOCOL_NAMES.get(protocol_code, '<unknown>')
1612+
1613+
def encode_alpn_protocol_list(alpn_protocols):
1614+
protos = bytearray()
1615+
for protocol in alpn_protocols:
1616+
b = bytes(protocol, 'ascii')
1617+
if not b or len(b) > 255:
1618+
raise SSLError('ALPN protocols must be 1 to 255 in length')
1619+
protos.append(len(b))
1620+
protos.extend(b)
1621+
return protos

‎Misc/ACKS‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,7 @@ Michael Layzell
11071107
Michael Lazar
11081108
Peter Lazorchak
11091109
Brian Leair
1110+
Iain Learmonth
11101111
Mathieu Leduc-Hamel
11111112
Amandine Lee
11121113
Antony Lee
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Adds support for Encrypted Client Hello (ECH) to the ssl module. Clients may
2+
use the options exposed to establish TLS connections using ECH. Third-party
3+
libraries like ``dnspython`` can be used to query for HTTPS and SVCB records
4+
that contain the public keys required to use ECH with specific servers. If
5+
no public key is available, an option is available to "GREASE" the
6+
connection, and it is possible to retrieve the public key from the retry
7+
configuration sent by servers that support ECH as they terminate the initial
8+
connection.

0 commit comments

Comments
 (0)