8000 EncryptedClientHello support in ssl module · python/cpython@27dcb2f · GitHub
[go: up one dir, main page]

Skip to content

Commit 27dcb2f

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 695ab61 commit 27dcb2f

File tree

9 files changed

+656
-10
lines changed

9 files changed

+656
-10
lines changed

Doc/library/ssl.rst

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

10041004
.. versionadded:: 3.6
10051005

1006+
.. class:: ECHStatus
1007+
1008+
:class:`enum.IntEnum` collection of Encrypted Client Hello (ECH) statuses
1009+
returned by :meth:`SSLSocket.get_ech_status`.
1010+
1011+
.. versionadded:: next
1012+
10061013
.. data:: Purpose.SERVER_AUTH
10071014

10081015
Option for :func:`create_default_context` and
@@ -1307,6 +1314,22 @@ SSL sockets also have the following additional methods and attributes:
13071314

13081315
.. versionadded:: 3.3
13091316

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

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

13801403
.. versionadded:: 3.2
13811404

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

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

16811713
.. versionadded:: 3.5
16821714

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

16851735
Specify which protocols the socket should advertise during the SSL/TLS
@@ -1699,6 +1749,28 @@ to speed up repeated connections from the same clients.
169 23D3 91749

17001750
NPN has been superseded by ALPN
17011751

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

17041776
Register a callback function that will be called after the TLS Client Hello
@@ -2594,6 +2666,8 @@ provided.
25942666
- :meth:`~SSLSocket.verify_client_post_handshake`
25952667
- :meth:`~SSLSocket.unwrap`
25962668
- :meth:`~SSLSocket.get_channel_binding`
2669+
- :meth:`~SSLSocket.get_ech_retry_config`
2670+
- :meth:`~SSLSocket.get_ech_status`
25972671
- :meth:`~SSLSocket.version`
25982672

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

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

28172937
.. seealso::
28182938

Lib/ssl.py

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

@@ -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,12 @@ def context(self):
831833
def context(self, ctx):
832834
self._sslobj.context = ctx
833835

836+
@property
837+
def outer_server_hostname(self):
838+
"""The server name used in the outer ClientHello."""
839+
if self._sslobj:
840+
return self._sslobj.get_ech_status()[2]
841+
834842
@property
835843
def session(self):
836844
"""The SSLSession for client socket."""
@@ -968,6 +976,9 @@ def version(self):
968976
def verify_client_post_handshake(self):
969977
return self._sslobj.verify_client_post_handshake()
970978

979+
def get_ech_status(self):
980+
return ECHStatus(self._sslobj.get_ech_status()[0])
981+
971982

972983
def _sslcopydoc(func):
973984
"""Copy docstring from SSLObject to SSLSocket"""
@@ -990,13 +1001,16 @@ def __init__(self, *args, **kwargs):
9901001
@classmethod
9911002
def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
9921003
suppress_ragged_eofs=True, server_hostname=None,
993-
context=None, session=None):
1004+
context=None, session=None, outer_server_hostname=None):
9941005
if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM:
9951006
raise NotImplementedError("only stream sockets are supported")
9961007
if server_side:
9971008
if server_hostname:
9981009
raise ValueError("server_hostname can only be specified "
9991010
"in client mode")
1011+
if outer_server_hostname:
1012+
raise ValueError("outer_server_hostname can only be specified "
1013+
"in client mode")
10001014
if session is not None:
10011015
raise ValueError("session can only be specified in "
10021016
"client mode")
@@ -1092,6 +1106,12 @@ def context(self, ctx):
10921106
self._context = ctx
10931107
self._sslobj.context = ctx
10941108

1109+
@property
1110+
def outer_server_hostname(self):
1111+
"""The server name used in the outer ClientHello."""
1112+
if self._sslobj:
1113+
return self._sslobj.get_ech_status()[2]
1114+
10951115
@property
10961116
@_sslcopydoc
10971117
def session(self):
@@ -1358,6 +1378,10 @@ def verify_client_post_handshake(self):
13581378
else:
13591379
raise ValueError("No SSL wrapper around " + str(self))
13601380

1381+
def get_ech_status(self):
1382+
if self._sslobj:
1383+
return ECHStatus(self._sslobj.get_ech_status()[0])
1384+
13611385
def _real_close(self):
13621386
self._sslobj = None
13631387
super()._real_close()
@@ -1527,3 +1551,13 @@ def get_server_certificate(addr, ssl_version=PROTOCOL_TLS_CLIENT,
15271551

15281552
def get_protocol_name(protocol_code):
15291553
return _PROTOCOL_NAMES.get(protocol_code, '<unknown>')
1554+
1555+
def encode_alpn_protocol_list(alpn_protocols):
1556+
protos = bytearray()
1557+
for protocol in alpn_protocols:
1558+
b = bytes(protocol, 'ascii')
1559+
if not b or len(b) > 255:
1560+
raise SSLError('ALPN protocols must be 1 to 255 in length')
1561+
protos.append(len(b))
1562+
protos.extend(b)
1563+
return protos

Misc/ACKS

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