Skip to content

Commit 84a0640

Browse files
committed
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.
1 parent e7a3c20 commit 84a0640

File tree

9 files changed

+638
-12
lines changed

9 files changed

+638
-12
lines changed

Doc/library/ssl.rst

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ purposes.
214214
The context now uses :data:`VERIFY_X509_PARTIAL_CHAIN` and
215215
:data:`VERIFY_X509_STRICT` in its default verify flags.
216216

217-
218217
Exceptions
219218
^^^^^^^^^^
220219

@@ -1003,6 +1002,13 @@ Constants
10031002

10041003
.. versionadded:: 3.6
10051004

1005+
.. class:: ECHStatus
1006+
1007+
:class:`enum.IntEnum` collection of Encrypted Client Hello (ECH) statuses
1008+
returned by :meth:`SSLSocket.get_ech_status`.
1009+
1010+
.. versionadded:: TODO XXX
1011+
10061012
.. data:: Purpose.SERVER_AUTH
10071013

10081014
Option for :func:`create_default_context` and
@@ -1307,6 +1313,22 @@ SSL sockets also have the following additional methods and attributes:
13071313

13081314
.. versionadded:: 3.3
13091315

1316+
.. method:: SSLSocket.get_ech_retry_config()
1317+
1318+
When the status returned by :meth:`SSLSocket.get_ech_status` after completion of the
1319+
handshake is :data:`ECHStatus.ECH_STATUS_GREASE_ECH`, this method returns the
1320+
configuration value provided by the server to be used for a new connection using
1321+
ECH.
1322+
1323+
.. versionadded:: TODO XXX
1324+
1325+
.. method:: SSLSocket.get_ech_status()
1326+
1327+
Gets the status of Encrypted Client Hello (ECH) processing. Returns an
1328+
:class:`ECHStatus` instance.
1329+
1330+
.. versionadded:: TODO XXX
1331+
13101332
.. method:: SSLSocket.selected_alpn_protocol()
13111333

13121334
Return the protocol that was selected during the TLS handshake. If
@@ -1379,6 +1401,15 @@ SSL sockets also have the following additional methods and attributes:
13791401

13801402
.. versionadded:: 3.2
13811403

1404+
.. attribute:: SSLSocket.outer_server_hostname
1405+
1406+
Hostname of the server name used in the outer ClientHello when Encrypted Client
1407+
Hello (ECH) is used: :class:`str` type, or ``None`` for server-side socket or
1408+
if the outer server name was not specified in the constructor or the ECH
1409+
configuration.
1410+
1411+
.. versionadded:: TODO XXX
1412+
13821413
.. attribute:: SSLSocket.server_side
13831414

13841415
A boolean which is ``True`` for server-side sockets and ``False`` for
@@ -1680,6 +1711,24 @@ to speed up repeated connections from the same clients.
16801711

16811712
.. versionadded:: 3.5
16821713

1714+
.. method:: SSLContext.set_ech_config(ech_config)
1715+
1716+
Sets an Encrypted Client Hello (ECH) configuration, which may be discovered from
1717+
an HTTPS resource record in DNS or from :meth:`SSLSocket.get_ech_retry_config`.
1718+
Multiple calls to this functions will accumulate the set of values available for
1719+
a connection.
1720+
1721+
If the input value provided contains no suitable value (e.g. if it only contains
1722+
ECH configuration versions that are not supported), an :class:`SSLError` will be
1723+
raised.
1724+
1725+
The ech_config parameter should be a bytes-like object containing the raw ECH
1726+
configuration.
1727+
1728+
This method will raise :exc:`NotImplementedError` if :data:`HAS_ECH` is ``False``.
1729+
1730+
.. versionadded:: TODO XXX
1731+
16831732
.. method:: SSLContext.set_npn_protocols(protocols)
16841733

16851734
Specify which protocols the socket should advertise during the SSL/TLS
@@ -1699,6 +1748,28 @@ to speed up repeated connections from the same clients.
16991748

17001749
NPN has been superseded by ALPN
17011750

1751+
.. method:: SSLContext.set_outer_alpn_protocols(protocols)
1752+
1753+
Specify which protocols the socket should advertise during the TLS
1754+
handshake in the outer ClientHello when ECH is used. The *protocols*
1755+
argument accepts the same values as for
1756+
:meth:`~SSLContext.set_alpn_protocols`.
1757+
1758+
This method will raise :exc:`NotImplementedError` if :data:`HAS_ECH` is
1759+
``False``.
1760+
1761+
.. versionadded:: TODO XXX
1762+
1763+
.. method:: SSLContext.set_outer_server_hostname(server_hostname)
1764+
1765+
Specify which hostname the socket should advertise during the TLS
1766+
handshake in the outer ClientHello when ECH is used.
1767+
1768+
This method will raise :exc:`NotImplementedError` if :data:`HAS_ECH` is
1769+
``False``.
1770+
1771+
.. versionadded:: TODO XXX
1772+
17021773
.. attribute:: SSLContext.sni_callback
17031774

17041775
Register a callback function that will be called after the TLS Client Hello
@@ -2594,6 +2665,8 @@ provided.
25942665
- :meth:`~SSLSocket.verify_client_post_handshake`
25952666
- :meth:`~SSLSocket.unwrap`
25962667
- :meth:`~SSLSocket.get_channel_binding`
2668+
- :meth:`~SSLSocket.get_ech_retry_config`
2669+
- :meth:`~SSLSocket.get_ech_status`
25972670
- :meth:`~SSLSocket.version`
25982671

25992672
When compared to :class:`SSLSocket`, this object lacks the following
@@ -2813,6 +2886,52 @@ of TLS/SSL. Some new TLS 1.3 features are not yet available.
28132886
- TLS 1.3 features like early data, deferred TLS client cert request,
28142887
signature algorithm configuration, and rekeying are not supported yet.
28152888

2889+
Encrypted Client Hello
2890+
^^^^^^^^^^^^^^^^^^^^^^
2891+
2892+
.. versionadded:: TODO XXX
2893+
2894+
Encrypted Client Hello (ECH) allows for encrypting values that have previously only been
2895+
included unencrypted in the ClientHello records when establishing a TLS connection. To use
2896+
ECH it is necessary to provide configuration values that contain a version, algorithm
2897+
parameters, the public key to use for HPKE encryption and the "public_name" that is by
2898+
default used for the unencrypted (outer) SNI when ECH is attempted. These configuration
2899+
values may be discovered through DNS or through the "retry config" mechanism.
2900+
2901+
The following example assumes that you have discovered a set of ECH configuration values
2902+
from DNS, or *ech_configs* may be an empty list to rely on the "retry config" mechanism::
2903+
2904+
import socket
2905+
import ssl
2906+
2907+
2908+
def connect_with_tls_ech(hostname: str, ech_configs: List[str],
2909+
use_retry_config: bool=True) -> ssl.SSLSocket:
2910+
context = ssl.create_default_context()
2911+
for ech_config in ech_configs:
2912+
context.set_ech_config(ech_config)
2913+
with socket.create_connection((hostname, 443)) as sock:
2914+
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
2915+
if (ssock.get_ech_status == ECHStatus.ECH_STATUS_GREASE_ECH
2916+
and use_retry_config):
2917+
return connect_with_ech(hostname, [ssock.get_ech_retry_config()],
2918+
False)
2919+
return ssock
2920+
2921+
hostname = "www.python.org"
2922+
ech_configs = [] # Replace with a call to a function to lookup
2923+
# ECH configurations in DNS
2924+
2925+
ssock = connect_with_tls_ech(hostname, ech_configs)
2926+
2927+
The following classes, methods, and attributes will be useful for using ECH:
2928+
2929+
- :class:`ECHStatus`
2930+
- :meth:`SSLContext.set_ech_config`
2931+
- :meth:`SSLContext.set_outer_alpn_protocols`
2932+
- :meth:`SSLContext.set_outer_server_hostname`
2933+
- :meth:`SSLSocket.get_ech_status`
2934+
- :meth:`SSLSocket.get_ech_retry_config`
28162935

28172936
.. seealso::
28182937

Lib/ssl.py

Lines changed: 51 additions & 10 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 = {value: name for name, value in _SSLMethod.__members__.items()}
155160

@@ -459,7 +464,7 @@ def wrap_socket(self, sock, server_side=False,
459464
suppress_ragged_eofs=suppress_ragged_eofs,
460465
server_hostname=server_hostname,
461466
context=self,
462-
session=session
467+
session=session,
463468
)
464469

465470
def wrap_bio(self, incoming, outgoing, server_side=False,
@@ -502,16 +507,13 @@ def shim_cb(sslobj, servername, sslctx):
502507
self.sni_callback = shim_cb
503508

504509
def set_alpn_protocols(self, alpn_protocols):
505-
protos = bytearray()
506-
for protocol in alpn_protocols:
507-
b = bytes(protocol, 'ascii')
508-
if len(b) == 0 or len(b) > 255:
509-
raise SSLError('ALPN protocols must be 1 to 255 in length')
510-
protos.append(len(b))
511-
protos.extend(b)
512-
510+
protos = encode_alpn_protocol_list(alpn_protocols)
513511
self._set_alpn_protocols(protos)
514512

513+
def set_outer_alpn_protocols(self, alpn_protocols):
514+
protos = encode_alpn_protocol_list(alpn_protocols)
515+
self._set_outer_alpn_protocols(protos)
516+
515517
def _load_windows_store_certs(self, storename, purpose):
516518
try:
517519
for cert, encoding, trust in enum_certificates(storename):
@@ -831,6 +833,14 @@ def context(self):
831833
def context(self, ctx):
832834
self._sslobj.context = ctx
833835

836+
@property
837+
def outer_server_hostname(self) -> str:
838+
"""The server name used in the outer ClientHello."""
839+
if self._sslobj:
840+
return self._sslobj.get_ech_status()[2]
841+
else:
842+
raise ValueError("No SSL wrapper around " + str(self))
843+
834844
@property
835845
def session(self):
836846
"""The SSLSession for client socket."""
@@ -968,6 +978,9 @@ def version(self):
968978
def verify_client_post_handshake(self):
969979
return self._sslobj.verify_client_post_handshake()
970980

981+
def get_ech_status(self):
982+
return ECHStatus(self._sslobj.get_ech_status()[0])
983+
971984

972985
def _sslcopydoc(func):
973986
"""Copy docstring from SSLObject to SSLSocket"""
@@ -990,13 +1003,16 @@ def __init__(self, *args, **kwargs):
9901003
@classmethod
9911004
def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
9921005
suppress_ragged_eofs=True, server_hostname=None,
993-
context=None, session=None):
1006+
context=None, session=None, outer_server_hostname=None):
9941007
if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM:
9951008
raise NotImplementedError("only stream sockets are supported")
9961009
if server_side:
9971010
if server_hostname:
9981011
raise ValueError("server_hostname can only be specified "
9991012
"in client mode")
1013+
if outer_server_hostname:
1014+
raise ValueError("outer_server_hostname can only be specified "
1015+
"in client mode")
10001016
if session is not None:
10011017
raise ValueError("session can only be specified in "
10021018
"client mode")
@@ -1092,6 +1108,14 @@ def context(self, ctx):
10921108
self._context = ctx
10931109
self._sslobj.context = ctx
10941110

1111+
@property
1112+
def outer_server_hostname(self) -> str:
1113+
"""The server name used in the outer ClientHello."""
1114+
if self._sslobj:
1115+
return self._sslobj.get_ech_status()[2]
1116+
else:
1117+
raise ValueError("No SSL wrapper around " + str(self))
1118+
10951119
@property
10961120
@_sslcopydoc
10971121
def session(self):
@@ -1358,6 +1382,13 @@ def verify_client_post_handshake(self):
13581382
else:
13591383
raise ValueError("No SSL wrapper around " + str(self))
13601384

1385+
1386+
def get_ech_status(self):
1387+
if self._sslobj:
1388+
return ECHStatus(self._sslobj.get_ech_status()[0])
1389+
else:
1390+
raise ValueError("No SSL wrapper around " + str(self))
1391+
13611392
def _real_close(self):
13621393
self._sslobj = None
13631394
super()._real_close()
@@ -1527,3 +1558,13 @@ def get_server_certificate(addr, ssl_version=PROTOCOL_TLS_CLIENT,
15271558

15281559
def get_protocol_name(protocol_code):
15291560
return _PROTOCOL_NAMES.get(protocol_code, '<unknown>')
1561+
1562+
def encode_alpn_protocol_list(alpn_protocols):
1563+
protos = bytearray()
1564+
for protocol in alpn_protocols:
1565+
b = bytes(protocol, 'ascii')
1566+
if len(b) == 0 or len(b) > 255:
1567+
raise SSLError('ALPN protocols must be 1 to 255 in length')
1568+
protos.append(len(b))
1569+
protos.extend(b)
1570+
return protos

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,7 @@ Michael Layzell
10811081
Michael Lazar
10821082
Peter Lazorchak
10831083
Brian Leair
1084+
Iain Learmonth
10841085
Mathieu Leduc-Hamel
10851086
Amandine Lee
10861087
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)