Skip to content

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

irl
Copy link

@irl irl commented Jun 12, 2025

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.

To use these options requires a version of openssl that includes these options, which currently are on a feature branch:

https://github.com/openssl/openssl/tree/feature/ech

We do not expect that the API here will have any substaintial changes when it is merged.

For testing, clone the above repository, checkout the feature/ech branch, and build. Then use this openssl installation when building cPython:

git clone https://github.com/openssl/openssl.git
pushd openssl
git checkout feature/ech
./configure --prefix=$HOME/opt/openssl
make -j8
make install_sw
popd
git clone https://github.com/irl/cpython.git
git checkout issue-89730
./configure --with-pydebug --with-openssl=$HOME/opt/openssl
make -j8

Additionally I have added to the documentation an example of the use of these options, which is built at: https://irl.github.io/cpython/library/ssl.html#encrypted-client-hello


📚 Documentation preview 📚: https://cpython-previews--135435.org.readthedocs.build/

@python-cla-bot
Copy link

python-cla-bot bot commented Jun 12, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

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.
Comment on lines +2096 to +2097
PyObject *retval = PyTuple_New(3);
PyTuple_SetItem(retval, 0, PyLong_FromLong(status));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyTuple_New can fail, and use PyTuple_SET_ITEM to avoid error checking there.

PyTuple_SetItem(retval, 0, PyLong_FromLong(status));

if (inner_sni != NULL) {
PyTuple_SetItem(retval, 1, PyUnicode_FromString(inner_sni));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyUnicode_FromString can fail.

@@ -2073,6 +2077,111 @@ cipher_to_dict(const SSL_CIPHER *cipher)
);
}

/*[clinic input]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure you need to decorate these with @critical_section for free-threading.

@@ -70,6 +70,10 @@
#include "openssl/bio.h"
#include "openssl/dh.h"

#if __has_include("openssl/ech.h")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check #ifdef __has_include.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also use the macro OPENSSL_ECH that you defined (which should be renamed)

Copy link
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you mentioned, the ECH feature is not yet available in OpenSSL (https://github.com/openssl/openssl/tree/feature/ech). I would like to defer this until it's released.

In addition:

  • please remove blank lines as much as possile. Too many blank lines hinder the reading flow.
  • check return codes from OpenSSL (SSL_ech_get1_retry_config may fail for instance, so you should check if it succeeded, and if not, correctly handle the return code)
  • we need tests

@@ -214,7 +214,6 @@ purposes.
The context now uses :data:`VERIFY_X509_PARTIAL_CHAIN` and
:data:`VERIFY_X509_STRICT` in its default verify flags.


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep this line.

:class:`enum.IntEnum` collection of Encrypted Client Hello (ECH) statuses
returned by :meth:`SSLSocket.get_ech_status`.

.. versionadded:: TODO XXX
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. versionadded:: TODO XXX
.. versionadded:: next

And ditto for the rest

Comment on lines +1 to +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
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description should also be put in What's New rather here.

@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
libraries like dnspython can be used to query for HTTPS and SVCB records
libraries like ``dnspython`` can be used to query for HTTPS and SVCB records

@@ -70,6 +70,10 @@
#include "openssl/bio.h"
#include "openssl/dh.h"

#if __has_include("openssl/ech.h")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also use the macro OPENSSL_ECH that you defined (which should be renamed)

Comment on lines +4772 to +4776
if ((es_in = BIO_new_mem_buf(ech_config->buf, ech_config->len)) == NULL
|| (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL
|| OSSL_ECHSTORE_read_echconfiglist(es, es_in) != 1
|| SSL_CTX_set1_echstore(self->ctx, es) != 1) {
_setSSLError(get_state_ctx(self), NULL, 0, __FILE__, __LINE__);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Break down this big if into multiple ones and use a goto error with an error label doing the cleanup.

@@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert this change, it's not needed.

Comment on lines +836 to +842
@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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@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))
@property
def outer_server_hostname(self):
"""The server name used in the outer ClientHello."""
return self._sslobj.get_ech_status()[2]

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.

Comment on lines +1112 to +1117
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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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))
def outer_server_hostname(self):
"""The server name used in the outer ClientHello."""
if self._sslobj:
return self._sslobj.get_ech_status()[2]

Other properties actually return None if sslobj is None.

protos = bytearray()
for protocol in alpn_protocols:
b = bytes(protocol, 'ascii')
if len(b) == 0 or len(b) > 255:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(b) == 0 or len(b) > 255:
if not b or len(b) > 255:

@bedevere-app
Copy link

bedevere-app bot commented Jun 13, 2025

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants