-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -214,7 +214,6 @@ | |||||
The context now uses :data:`VERIFY_X509_PARTIAL_CHAIN` and | ||||||
:data:`VERIFY_X509_STRICT` in its default verify flags. | ||||||
|
||||||
|
||||||
Exceptions | ||||||
^^^^^^^^^^ | ||||||
|
||||||
|
@@ -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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
And ditto for the rest |
||||||
|
||||||
.. data:: Purpose.SERVER_AUTH | ||||||
|
||||||
Option for :func:`create_default_context` and | ||||||
|
@@ -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 | ||||||
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 | ||||||
|
@@ -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 | ||||||
|
@@ -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``. | ||||||
|
||||||
.. versionadded:: TODO XXX | ||||||
|
||||||
.. method:: SSLContext.set_npn_protocols(protocols) | ||||||
|
||||||
Specify which protocols the socket should advertise during the SSL/TLS | ||||||
|
@@ -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 | ||||||
``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 | ||||||
``False``. | ||||||
|
||||||
.. versionadded:: TODO XXX | ||||||
|
||||||
.. attribute:: SSLContext.sni_callback | ||||||
|
||||||
Register a callback function that will be called after the TLS Client Hello | ||||||
|
@@ -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 | ||||||
|
@@ -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:: | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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()} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
|
@@ -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, | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||||||||||||||||||||||||
|
@@ -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): | ||||||||||||||||||||||||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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.""" | ||||||||||||||||||||||||
|
@@ -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""" | ||||||||||||||||||||||||
|
@@ -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") | ||||||||||||||||||||||||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Other properties actually return None if sslobj is None. |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
@property | ||||||||||||||||||||||||
@_sslcopydoc | ||||||||||||||||||||||||
def session(self): | ||||||||||||||||||||||||
|
@@ -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() | ||||||||||||||||||||||||
|
@@ -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: | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||
raise SSLError('ALPN protocols must be 1 to 255 in length') | ||||||||||||||||||||||||
protos.append(len(b)) | ||||||||||||||||||||||||
protos.extend(b) | ||||||||||||||||||||||||
return protos |
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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This description should also be put in What's New rather here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep this line.