Skip to content

Commit 6ebce14

Browse files
committed
Per-socket ECH config.
1 parent 0167ac1 commit 6ebce14

8 files changed

Lines changed: 506 additions & 84 deletions

File tree

‎Doc/library/ssl.rst‎

Lines changed: 87 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,23 @@ Exceptions
321321

322322
A human readable string of the verification error.
323323

324+
.. exception:: SSLECHRejectedError
325+
326+
A subclass of :exc:`SSLError` raised when the handshake fails because the
327+
server rejected :ref:`Encrypted Client Hello <ssl-encrypted-client-hello>`.
328+
The connection cannot be used. If :attr:`retry_config` is not empty, the
329+
client may retry once with a new connection using that configuration.
330+
331+
This exception is only raised when :data:`HAS_ECH` is ``True``.
332+
333+
.. attribute:: retry_config
334+
335+
The server's ECH retry configuration as a binary-encoded ECHConfigList,
336+
or empty :class:`bytes` if the server did not provide one or the server
337+
could not be authenticated under the outer server name.
338+
339+
.. versionadded:: next
340+
324341
.. exception:: CertificateError
325342

326343
An alias for :exc:`SSLCertVerificationError`.
@@ -1430,7 +1447,10 @@ SSL sockets also have the following additional methods and attributes:
14301447
bytes if no retry configuration is available. When the status returned by
14311448
:meth:`SSLSocket.get_ech_status` after the handshake is
14321449
:attr:`ECHStatus.ECH_STATUS_GREASE_ECH`, the configuration can be used
1433-
for a new connection using ECH.
1450+
for a new connection using ECH. When the server rejects a real ECH
1451+
attempt, the handshake raises :exc:`SSLECHRejectedError`, which carries
1452+
the same configuration in its :attr:`~SSLECHRejectedError.retry_config`
1453+
attribute.
14341454

14351455
Only available when :data:`HAS_ECH` is ``True``.
14361456

@@ -1445,6 +1465,26 @@ SSL sockets also have the following additional methods and attributes:
14451465

14461466
.. versionadded:: next
14471467

1468+
.. method:: SSLSocket.set_ech_config(ech_config)
1469+
1470+
Set the Encrypted Client Hello (ECH) configuration for this connection
1471+
only, replacing any configuration set on the context with
1472+
:meth:`SSLContext.set_ech_config`. The *ech_config* parameter accepts the
1473+
same values. Unlike the context's configuration, which applies to every
1474+
connection made from it, a connection's configuration is only used by that
1475+
connection and can therefore be chosen per destination host, which is how
1476+
ECH configurations are published in DNS.
1477+
1478+
The configuration must be set before the handshake starts, so this is
1479+
mostly useful with ``do_handshake_on_connect=False`` or on a socket that
1480+
is not connected yet. :meth:`SSLContext.wrap_socket` and
1481+
:meth:`SSLContext.wrap_bio` accept the configuration as their *ech_config*
1482+
argument instead.
1483+
1484+
Only available when :data:`HAS_ECH` is ``True``.
1485+
1486+
.. versionadded:: next
1487+
14481488
.. method:: SSLSocket.selected_alpn_protocol()
14491489

14501490
Return the protocol that was selected during the TLS handshake. If
@@ -1932,6 +1972,14 @@ to speed up repeated connections from the same clients.
19321972
an HTTPS resource record in DNS or from :meth:`SSLSocket.get_ech_retry_config`.
19331973
Each call replaces the context's previous configuration.
19341974

1975+
The configuration applies to every connection made from the context, and a
1976+
server that cannot decrypt it rejects the connection with
1977+
:exc:`SSLECHRejectedError`. Because ECH configurations are published per
1978+
host, a context with a configuration should only be used for connections to
1979+
the hosts that publish it. To use a configuration for a single connection,
1980+
pass it as the *ech_config* argument of :meth:`wrap_socket` or
1981+
:meth:`wrap_bio` instead.
1982+
19351983
If the input value provided contains no suitable value (e.g. if it only contains
19361984
ECH configuration versions that are not supported), an :class:`SSLError` will be
19371985
raised.
@@ -2080,7 +2128,7 @@ to speed up repeated connections from the same clients.
20802128

20812129
.. method:: SSLContext.wrap_socket(sock, server_side=False, \
20822130
do_handshake_on_connect=True, suppress_ragged_eofs=True, \
2083-
server_hostname=None, session=None)
2131+
server_hostname=None, session=None, ech_config=None)
20842132

20852133
Wrap an existing Python socket *sock* and return an instance of
20862134
:attr:`SSLContext.sslsocket_class` (default :class:`SSLSocket`). The
@@ -2121,6 +2169,10 @@ to speed up repeated connections from the same clients.
21212169

21222170
*session*, see :attr:`~SSLSocket.session`.
21232171

2172+
*ech_config*, see :meth:`SSLSocket.set_ech_config`. Specifying
2173+
*ech_config* will raise a :exc:`ValueError` if *server_side* is true, and
2174+
a :exc:`NotImplementedError` if :data:`HAS_ECH` is ``False``.
2175+
21242176
To wrap an :class:`SSLSocket` in another :class:`SSLSocket`, use
21252177
:meth:`SSLContext.wrap_bio`.
21262178

@@ -2135,6 +2187,9 @@ to speed up repeated connections from the same clients.
21352187
The method returns an instance of :attr:`SSLContext.sslsocket_class`
21362188
instead of hard-coded :class:`SSLSocket`.
21372189

2190+
.. versionchanged:: next
2191+
*ech_config* argument was added.
2192+
21382193
.. attribute:: SSLContext.sslsocket_class
21392194

21402195
The return type of :meth:`SSLContext.wrap_socket`, defaults to
@@ -2145,16 +2200,17 @@ to speed up repeated connections from the same clients.
21452200
.. versionadded:: 3.7
21462201

21472202
.. method:: SSLContext.wrap_bio(incoming, outgoing, server_side=False, \
2148-
server_hostname=None, session=None)
2203+
server_hostname=None, session=None, \
2204+
ech_config=None)
21492205

21502206
Wrap the BIO objects *incoming* and *outgoing* and return an instance of
21512207
:attr:`SSLContext.sslobject_class` (default :class:`SSLObject`). The SSL
21522208
routines will read input data from the incoming BIO and write data to the
21532209
outgoing BIO.
21542210

2155-
The *server_side*, *server_hostname* and *session* parameters have the
2156-
same meaning as in :meth:`SSLContext.wrap_socket`, and are validated in
2157-
the same way: in particular a :exc:`ValueError` is raised when
2211+
The *server_side*, *server_hostname*, *session* and *ech_config*
2212+
parameters have the same meaning as in :meth:`SSLContext.wrap_socket`,
2213+
and are validated in the same way: in particular a :exc:`ValueError` is raised when
21582214
:attr:`~SSLContext.check_hostname` is enabled but no *server_hostname* is
21592215
given, since there would be no name to match the peer's certificate
21602216
against.
@@ -2173,6 +2229,9 @@ to speed up repeated connections from the same clients.
21732229
*server_hostname* was accepted, and verified the certificate chain but
21742230
never the peer's identity.
21752231

2232+
.. versionchanged:: next
2233+
*ech_config* argument was added.
2234+
21762235
.. attribute:: SSLContext.sslobject_class
21772236

21782237
The return type of :meth:`SSLContext.wrap_bio`, defaults to
@@ -2891,6 +2950,7 @@ provided.
28912950
- :meth:`~SSLSocket.verify_client_post_handshake`
28922951
- :meth:`~SSLSocket.unwrap`
28932952
- :meth:`~SSLSocket.get_channel_binding`
2953+
- :meth:`~SSLSocket.set_ech_config`
28942954
- :meth:`~SSLSocket.get_ech_retry_config`
28952955
- :meth:`~SSLSocket.get_ech_status`
28962956
- :meth:`~SSLSocket.version`
@@ -3132,51 +3192,51 @@ ClientHello. ECH requires OpenSSL 4.0 or later with ECH enabled; check
31323192

31333193
Servers can load a PEM file containing their ECH private key and configuration
31343194
with :meth:`SSLContext.load_ech_keys`. Clients use the corresponding public
3135-
ECHConfigList with :meth:`SSLContext.set_ech_config`.
3195+
ECHConfigList, published in DNS for each host, as the *ech_config* argument of
3196+
:meth:`SSLContext.wrap_socket` or :meth:`SSLContext.wrap_bio`, or for every
3197+
connection from a context with :meth:`SSLContext.set_ech_config`.
31363198

3137-
Configuration values may be discovered through DNS or through the server's
3138-
retry configuration. The following example accepts a binary-encoded
3139-
ECHConfigList discovered through DNS. If none is supplied, it enables ECH
3140-
GREASE to request a retry configuration from the server::
3199+
A server that has rotated its ECH keys rejects the connection and provides a
3200+
retry configuration, which the handshake raises as
3201+
:exc:`SSLECHRejectedError`. The following example accepts a binary-encoded
3202+
ECHConfigList discovered through DNS and retries once with the server's retry
3203+
configuration. If no configuration is supplied, it enables ECH GREASE, which a
3204+
server may answer with a retry configuration for a later connection::
31413205

31423206
import socket
31433207
import ssl
31443208

31453209

3146-
def connect_with_tls_ech(hostname, ech_config=None, use_retry_config=True):
3210+
def connect_with_tls_ech(hostname, ech_config=None, retry=True):
31473211
context = ssl.create_default_context()
3148-
if ech_config is not None:
3149-
context.set_ech_config(ech_config)
3150-
else:
3212+
if ech_config is None:
31513213
context.options |= ssl.OP_ECH_GREASE
31523214
sock = socket.create_connection((hostname, 443))
31533215
try:
3154-
ssock = context.wrap_socket(sock, server_hostname=hostname)
3155-
except BaseException:
3156-
sock.close()
3216+
return context.wrap_socket(sock, server_hostname=hostname,
3217+
ech_config=ech_config)
3218+
except ssl.SSLECHRejectedError as exc:
3219+
if retry and exc.retry_config:
3220+
return connect_with_tls_ech(hostname, exc.retry_config,
3221+
retry=False)
31573222
raise
3158-
if (use_retry_config
3159-
and ssock.get_ech_status()
3160-
== ssl.ECHStatus.ECH_STATUS_GREASE_ECH):
3161-
try:
3162-
retry_config = ssock.get_ech_retry_config()
3163-
finally:
3164-
ssock.close()
3165-
return connect_with_tls_ech(hostname, retry_config, False)
3166-
return ssock
31673223

31683224
hostname = "www.python.org"
31693225
ech_config = None # Replace with an ECHConfigList discovered through DNS.
31703226

31713227
with connect_with_tls_ech(hostname, ech_config) as ssock:
31723228
print(ssock.get_ech_status())
3229+
if ssock.get_ech_status() == ssl.ECHStatus.ECH_STATUS_GREASE_ECH:
3230+
print("retry configuration:", ssock.get_ech_retry_config())
31733231

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

31763234
- :class:`ECHStatus`
3235+
- :exc:`SSLECHRejectedError`
31773236
- :meth:`SSLContext.load_ech_keys`
31783237
- :meth:`SSLContext.set_ech_config`
31793238
- :meth:`SSLContext.set_outer_alpn_protocols`
3239+
- :meth:`SSLSocket.set_ech_config`
31803240
- :meth:`SSLSocket.get_ech_status`
31813241
- :meth:`SSLSocket.get_ech_retry_config`
31823242

‎Doc/whatsnew/3.16.rst‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -689,12 +689,15 @@ ssl
689689
---
690690

691691
* Added support for :ref:`Encrypted Client Hello <ssl-encrypted-client-hello>`
692-
(ECH) with OpenSSL 4.0 or later. Clients can use
693-
:meth:`~ssl.SSLContext.set_ech_config` to supply an ECHConfigList discovered
694-
through DNS, for example using a third-party library such as ``dnspython``
695-
to query HTTPS or SVCB records. Clients can also enable
696-
:data:`~ssl.OP_ECH_GREASE` and retrieve a server's retry configuration
697-
with :meth:`~ssl.SSLSocket.get_ech_retry_config` for a subsequent connection.
692+
(ECH) with OpenSSL 4.0 or later. Clients can supply an ECHConfigList
693+
discovered through DNS, for example using a third-party library such as
694+
``dnspython`` to query HTTPS or SVCB records, as the *ech_config* argument
695+
of :meth:`~ssl.SSLContext.wrap_socket` and :meth:`~ssl.SSLContext.wrap_bio`
696+
or with :meth:`~ssl.SSLContext.set_ech_config`. A rejected ECH attempt
697+
raises :exc:`~ssl.SSLECHRejectedError`, which carries the server's retry
698+
configuration. Clients can also enable :data:`~ssl.OP_ECH_GREASE` and
699+
retrieve a server's retry configuration with
700+
:meth:`~ssl.SSLSocket.get_ech_retry_config` for a subsequent connection.
698701
Servers can use :meth:`~ssl.SSLContext.load_ech_keys` to load ECH private
699702
keys and their configurations from PEM files.
700703
:data:`~ssl.HAS_ECH` indicates whether ECH is available, and

‎Lib/ssl.py‎

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@
107107
from _ssl import _SSLContext, MemoryBIO, SSLSession
108108
from _ssl import (
109109
SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError,
110-
SSLSyscallError, SSLEOFError, SSLCertVerificationError
110+
SSLSyscallError, SSLEOFError, SSLCertVerificationError,
111+
SSLECHRejectedError
111112
)
112113
from _ssl import txt2obj as _txt2obj, nid2obj as _nid2obj
113114
from _ssl import RAND_status, RAND_add, RAND_bytes
@@ -381,7 +382,8 @@ def _ipaddress_match(cert_ipaddress, host_ip):
381382
return ip == host_ip
382383

383384

384-
def _check_sslobject_params(server_side, context=None, server_hostname=None, session=None):
385+
def _check_sslobject_params(server_side, context=None, server_hostname=None,
386+
session=None, ech_config=None):
385387
"""Raises a ValueError if SSLObject._create() parameters aren't valid.
386388
"""
387389
if server_side:
@@ -391,6 +393,11 @@ def _check_sslobject_params(server_side, context=None, server_hostname=None, ses
391393
if session is not None:
392394
raise ValueError("session can only be specified in "
393395
"client mode")
396+
if ech_config is not None:
397+
raise ValueError("ech_config can only be specified in "
398+
"client mode")
399+
if ech_config is not None and not HAS_ECH:
400+
raise NotImplementedError("ECH is not supported by the OpenSSL library")
394401
if context.check_hostname and not server_hostname:
395402
raise ValueError("check_hostname requires server_hostname")
396403

@@ -471,7 +478,7 @@ def _encode_hostname(self, hostname):
471478
def wrap_socket(self, sock, server_side=False,
472479
do_handshake_on_connect=True,
473480
suppress_ragged_eofs=True,
474-
server_hostname=None, session=None):
481+
server_hostname=None, session=None, ech_config=None):
475482
# SSLSocket class handles server_hostname encoding before it calls
476483
# ctx._wrap_socket()
477484
return self.sslsocket_class._create(
@@ -481,17 +488,18 @@ def wrap_socket(self, sock, server_side=False,
481488
suppress_ragged_eofs=suppress_ragged_eofs,
482489
server_hostname=server_hostname,
483490
context=self,
484-
session=session
491+
session=session,
492+
ech_config=ech_config,
485493
)
486494

487495
def wrap_bio(self, incoming, outgoing, server_side=False,
488-
server_hostname=None, session=None):
496+
server_hostname=None, session=None, ech_config=None):
489497
# Need to encode server_hostname here because _wrap_bio() can only
490498
# handle ASCII str.
491499
return self.sslobject_class._create(
492500
incoming, outgoing, server_side=server_side,
493501
server_hostname=self._encode_hostname(server_hostname),
494-
session=session, context=self,
502+
session=session, context=self, ech_config=ech_config,
495503
)
496504

497505
def set_npn_protocols(self, npn_protocols):
@@ -830,15 +838,19 @@ def __init__(self, *args, **kwargs):
830838

831839
@classmethod
832840
def _create(cls, incoming, outgoing, server_side=False,
833-
server_hostname=None, session=None, context=None):
841+
server_hostname=None, session=None, context=None,
842+
ech_config=None):
834843
_check_sslobject_params(server_side=server_side, context=context,
835-
server_hostname=server_hostname, session=session)
844+
server_hostname=server_hostname,
845+
session=session, ech_config=ech_config)
836846
self = cls.__new__(cls)
837847
sslobj = context._wrap_bio(
838848
incoming, outgoing, server_side=server_side,
839849
server_hostname=server_hostname,
840850
owner=self, session=session
841851
)
852+
if ech_config is not None:
853+
sslobj.set_ech_config(ech_config)
842854
self._sslobj = sslobj
843855
return self
844856

@@ -1007,6 +1019,14 @@ def verify_client_post_handshake(self):
10071019
return self._sslobj.verify_client_post_handshake()
10081020

10091021
if HAS_ECH:
1022+
def set_ech_config(self, ech_config):
1023+
"""Set the ECH configuration for this connection.
1024+
1025+
This replaces any configuration inherited from the context
1026+
and must be called before the handshake starts.
1027+
"""
1028+
self._sslobj.set_ech_config(ech_config)
1029+
10101030
def get_ech_status(self):
10111031
"""Return the status of Encrypted Client Hello processing."""
10121032
return ECHStatus(self._sslobj.get_ech_status()[0])
@@ -1041,11 +1061,12 @@ def __init__(self, *args, **kwargs):
10411061
@classmethod
10421062
def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
10431063
suppress_ragged_eofs=True, server_hostname=None,
1044-
context=None, session=None):
1064+
context=None, session=None, ech_config=None):
10451065
if sock.getsockopt(SOL_SOCKET, SO_TYPE) != SOCK_STREAM:
10461066
raise NotImplementedError("only stream sockets are supported")
10471067
_check_sslobject_params(server_side=server_side, context=context,
1048-
server_hostname=server_hostname, session=session)
1068+
server_hostname=server_hostname,
1069+
session=session, ech_config=ech_config)
10491070

10501071
sock_timeout = sock.gettimeout()
10511072
kwargs = dict(
@@ -1059,6 +1080,7 @@ def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
10591080
try:
10601081
self._context = context
10611082
self._session = session
1083+
self._ech_config = ech_config
10621084
self._closed = False
10631085
self._sslobj = None
10641086
self.server_side = server_side
@@ -1117,6 +1139,8 @@ def _create(cls, sock, server_side=False, do_handshake_on_connect=True,
11171139
self, server_side, self.server_hostname,
11181140
owner=self, session=self._session,
11191141
)
1142+
if self._ech_config is not None:
1143+
self._sslobj.set_ech_config(self._ech_config)
11201144
if do_handshake_on_connect:
11211145
timeout = self.gettimeout()
11221146
if timeout == 0.0:
@@ -1450,6 +1474,12 @@ def verify_client_post_handshake(self):
14501474
raise ValueError("No SSL wrapper around " + str(self))
14511475

14521476
if HAS_ECH:
1477+
@_sslcopydoc
1478+
def set_ech_config(self, ech_config):
1479+
self._ech_config = ech_config
1480+
if self._sslobj is not None:
1481+
self._sslobj.set_ech_config(ech_config)
1482+
14531483
@_sslcopydoc
14541484
def get_ech_status(self):
14551485
if self._sslobj is not None:
@@ -1487,6 +1517,8 @@ def _real_connect(self, addr, connect_ex):
14871517
owner=self, session=self._session
14881518
)
14891519
try:
1520+
if self._ech_config is not None:
1521+
self._sslobj.set_ech_config(self._ech_config)
14901522
if connect_ex:
14911523
rc = super().connect_ex(addr)
14921524
else:

0 commit comments

Comments
 (0)