Skip to content

gh-136306: Add support for SSL groups #136307

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 14 commits into
base: main
Choose a base branch
from
Open

Conversation

ronf
Copy link
Contributor

@ronf ronf commented Jul 4, 2025

This is an initial implementation of the feature proposed in issue #136306.


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

This is an initial implementation of the feature proposed in issue python#136306.
@picnixz picnixz changed the title gh-136306: Initial cut at SSL groups support gh-136306: Add support for SSL groups Jul 5, 2025
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.

A first round of comments

ronf added 2 commits July 5, 2025 06:39
Looks like the indentation is required. Got a doc build error without it.
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.

Some other comments (sorry I'm on mobile so it's hard)

@ronf
Copy link
Contributor Author

ronf commented Jul 5, 2025

I'm not sure why the latest round of tests failed - The only change in this last commit was a doc file change, so I'm guessing it's a transient CI failure. Is there a way to trigger it to retry?

@picnixz
Copy link
Member

picnixz commented Jul 6, 2025

Is there a way to trigger it to retry?

You can make an empty commit or ask a triager to rerun the CI (but there is none apart from me that is watching the PR I think)

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.

This looks great. I was planning to add some PQ support to Python now that ML-KEM/DEM were standardized but this is a good start as well. I'll have a look at the tests when I'm on my laptop again (Friday) but you can consider this PR to be approved unless I find something by then.

@ronf
Copy link
Contributor Author

ronf commented Jul 7, 2025

This looks great. I was planning to add some PQ support to Python now that ML-KEM/DEM were standardized but this is a good start as well. I'll have a look at the tests when I'm on my laptop again (Friday) but you can consider this PR to be approved unless I find something by then.

Thanks very much! There seems to be less urgency in providing support for ML-DSA and SLH-DSA for authentication than there was for ML-KEM for key agreement mainly because of the "capture now, decrypt later" concerns, but I'd be happy to contribute to an effort around supporting those as well. I'd actually like to learn more about what OpenSSL APIs actually need to change here.

@picnixz
Copy link
Member

picnixz commented Jul 7, 2025

I am currently modernizing the use of OpenSSL HMAC in hashlib and I actually wondered whether we used APIs that were deprecated in 3.0. If you want, you can help me here, at least for the SSL module (I'd prefer that we work on separate modules to avoid merge conflicts), namely look at the calls we make to OpenSSL and check if there are deprecated calls in 3.x. If there are, open an issue and a PR that modernize such calls (for HMAC, we moved from using the old HMAC_* interface to the more generic EVP_MAC interface)

@ronf
Copy link
Contributor Author

ronf commented Jul 7, 2025

If you want, you can help me here, at least for the SSL module (I'd prefer that we work on separate modules to avoid merge conflicts), namely look at the calls we make to OpenSSL and check if there are deprecated calls in 3.x.

I don't know if I'll have the cycles to actually take on fixing all the issues that we find, but I gave this a quick look to get a sense for the scope of the problem. There aren't as many changes as I was expecting to find, but it's still fairly complicated if we want to continue to support all the way back to OpenSSL 1.1.1 while avoiding functions deprecated in OpenSSL 3.x.

In many cases, the old API calls still exist in 3.x but the new API will only work on 3.x. I'm thinking leaving the old calls in place may be our best bet for now, to avoid conditional compilation. An example is replacing all references to BIO_new() with BIO_new_ex() where we pass in an explicit NULL argument for the library context. It doesn't seem worth a #if everywhere that appears as long as 3.x continues to provide a wrapper for the old BIO_new() call. A similar issue shows up around one use of BIO_new_mem_buf().

Here are some of the items I've found:

  • It looks like OpenSSL has split the cipher setting function into two separate functions for TLS 1.2 and below vs. TLS 1.3. The code currently calls SSL_CTX_set_cipher_list() which is TLS 1.2-only and there's another function SSL_CTX_set_ciphersuites() for TLS 1. Since we don't call the latter function right now, I think that means there's no way currently to set a non-default TLS 1.3 cipher suite list. I'm thinking this is a good candidate for treating as a bug.
  • There are some calls (d2i_X509_bio and i2d_X509) for reading certs that should be changed to use the OSSL_DECODER and OSSL_ENCODER APIs.
  • There's code reading DH params with PEM_read_DHparams() which returns a low-level DH object. I think should change to use PEM_read_bio_Parameters(), which returns an EVP_PKEY. There's also a call to DH_free that would change to EVP_PKEY_free().
  • Another place is setting DH params with SSL_CTX_set_tmp_dh(), and that should change to SSL_CTX_set0_tmp_dh_pkey(), which operates on EVP_PKEYs.
  • There's code using SSL_CTX_set_psk_client_callback() and SSL_CTX_set_psk_server_callback() which could be changed to use the OpenSSL 3.x SSL_CTX_set_psk_use_session_callback() and SSL_CTX_set_psk_find_session_callback() APIs. However, I think those new APIs only work on TLS 1.3, so this may be another case where we need to keep both around, or just continue to use the deprecated API for now, since I think it is possible to use that for both TLS 1.2 and TLS 1.3 connections if you're careful. There were some notes on this in the OpenSSL migration guide.

@ronf
Copy link
Contributor Author

ronf commented Jul 8, 2025

Following up on my last comment: In the SSL module docs, there's a "TLS 1.3" section which actually mentions that set_ciphers() can't be used to control which TLS 1.3 ciphers are enabled, even though both TLS 1.3 and older ciphers are returned by the get_ciphers() call. So, I guess this isn't a bug. There are a number of other documented limitations here, though, so I wonder if correcting some of those issues might be more useful than any deprecation issues in the OpenSSL APIs.

@picnixz
Copy link
Member

picnixz commented Jul 8, 2025

Helping fixing those issues or modernizing API calls is appreciated so choose what you want!

@ronf
Copy link
Contributor Author

ronf commented Jul 9, 2025

Thanks!

I've actually got another change ready which adds support for setting TLS 1.3 ciphers using a new set_ciphersuites() function on SSLContext. This mirrors the OpenSSL API which has SSL_CTX_set_cipher_list() and SSL_CTX_set_ciphersuites() functions. I thought about trying to do it all from within the existing set_ciphers() call, but that actually turns out to be really messy, especially given that the SSL_CTX_set_cipher_list() supports punctuation and partial matching on the various parts of the cipher suite name, whereas SSL_CTX_set_ciphersuites() just takes a simple colon-delimited list of exact TLS 1.3 cipher names. Keeping them separate is also important to let each of the two have its own independent default value. So, if you only call one of these two functions, the other one remains set to its default. It also does a better job reporting errors when the calls are separate, as you can report separately whether it failed to match any of the supported ciphers of the associated version in each of the two calls.

It'll probably be easier if I wait until this PR is merged before I create a new issue and PR to avoid conflicts in the "what's new" entry. Alternately, if you want me to expand the scope of this issue/PR to cover both setting TLS 1.3 cipher and TLS 1.3 groups and check in the additional changes hree, I could do that. While they're independent, both of them are about providing more complete TLS 1.3 support, so I'm good with either option.

@picnixz
Copy link
Member

picnixz commented Jul 11, 2025

I would prefer having separate issues and PRs even though they are related. It makes reverting stuff easier. I will be available tomorrow (maybe this evening, Paris time)

@ronf
Copy link
Contributor Author

ronf commented Jul 11, 2025

No problem - I'll hold onto the cipher suite change for now. Also, I have a third change planned to add support for getting and setting sigalgs. It should be able to follow the same pattern as ciphers and groups.

Once those changes are in, I'll revisit use of deprecated APIs, but for now I'd recommend only rewriting such code if the preferred replacement is supported in 1.1.1 or earlier. That will avoid needing to keep two versions of the code around depending on the OpenSSL version. As an example, I think replacing code which uses DH APIs with EVP APIs should be doable without any compile-time conditionals, but for now I'd avoid replacing calls to BIO_new, as the replacement function BIO_new_ex() doesn't appear until OpenSSL 3.0.

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.

Some final nits

Comment on lines 965 to 972
def test_set_groups(self):
ctx = ssl.create_default_context()

# Test valid group list
self.assertIsNone(ctx.set_groups('P-256:X25519'))

# Test invalid group list
self.assertRaises(ssl.SSLError, ctx.set_groups, 'P-256: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
def test_set_groups(self):
ctx = ssl.create_default_context()
# Test valid group list
self.assertIsNone(ctx.set_groups('P-256:X25519'))
# Test invalid group list
self.assertRaises(ssl.SSLError, ctx.set_groups, 'P-256:xxx')
def test_set_groups(self):
ctx = ssl.create_default_context()
self.assertIsNone(ctx.set_groups('P-256:X25519'))
self.assertRaises(ssl.SSLError, ctx.set_groups, 'P-256:xxx')

We can be less verbose.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Comment on lines +2161 to +2163
if (group_name == NULL) {
Py_RETURN_NONE;
}
Copy link
Member

Choose a reason for hiding this comment

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

According to the docs, group_name may return NULL in case of an error. Maybe check that there isn't an error?

Copy link
Member

Choose a reason for hiding this comment

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

Note 2: the docs only mentions the case when the TLS session wasn't established yet, but to be on the safe side, I'd prefer checking if there's an error on OpenSSL's side.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you suggesting adding a unit test here which verifies that OpenSSL returning NULL will hit this code and return None, or are you thinking this should return an error instead?

Right now, this API matches the behavior of SSLSocket.cipher(), returning None when a TLS session is not established (either because of an error or because the application queries this information too early, before the handshake is performed). If there is an error during the handshake, I would expect that to have already raised an error to the calling code. This check is just to handle if the calling code decides to try and query the group despite the incomplete handshake.

Copy link
Member

@picnixz picnixz Jul 12, 2025

Choose a reason for hiding this comment

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

Ah no, I'm just telling to check that we don't have an SSL error set before returning None. If there is an SSL error, we can raise an exception. Otherwise, you can return None as you did.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looking at the documentation, I can see where you'd think SSL_get0_group_name() might set an SSL error before returning NULL. Looking at the implementation, though, the functions called by SSL_get0_group_name() return NULL without actually ever setting an error themselves. They just check if other state is NULL or not, and return the name if one is available. I think the error referred to in the docs would have been returned in a prior call, such as when initiating the handshake.

I could put in an explicit call to ERR_get_error here, but I can't find any other code in this module which looks at that to decide whether to return an error. There's always a check for a special error return value in the function before looking at any SSL error information.

In this case, since NULL is returned without setting an SSL error, I would recommend we treat all NULL values as indicating the handshake didn't complete or wasn't attempted yet, and always return None for both cipher and group (and eventually sigalgs when that support is added).

Copy link
Member

Choose a reason for hiding this comment

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

In this case, since NULL is returned without setting an SSL error

Ah, so it would be hard to detect it. Ok, we can return None, and anyway, if there's an issue, subsequent calls to the interface will fail I think.

I would recommend we treat all NULL values as indicating the handshake didn't complete or wasn't attempted yet,

That could be more useful for the user. Can you make some test for that (namely, you set a group, and try to get its name even though the session doesn't exist and see that it returns None). We should also document that it returns None if no session exists yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The doc about returning None is already in place:

.. method:: SSLSocket.group()

   Return the group used for doing key agreement on this connection. If no
   connection has been established, returns ``None``.

   .. versionadded:: next

This is very similar to the docs for SSLSocket.cipher().

I should be able to come up with a unit test which creates an SSLSocket without connecting it and then tries to query the group - working on that now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Trying to get an SSLSocket created without an underlying connection is actually somewhat messy. I think a more realistic case would actually be to try and query the cipher & group after a handshake failure, like no matching ciphers between the client and server. There's already a test for that, and it's straightforward to add in checks that both cipher() and group() return None in that case. Checking that in momentarily.

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.

2 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy