-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
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
base: main
Are you sure you want to change the base?
Conversation
This is an initial implementation of the feature proposed in issue python#136306.
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.
A first round of comments
Doc/library/ssl.rst
Outdated
>>> ctx.get_groups() | ||
['secp256r1', 'secp384r1', 'secp521r1', 'x25519', 'x448', 'brainpoolP256r1tls13', 'brainpoolP384r1tls13', 'brainpoolP512r1tls13', 'ffdhe2048', 'ffdhe3072', 'ffdhe4096', 'ffdhe6144', 'ffdhe8192', 'MLKEM512', 'MLKEM768', 'MLKEM1024', 'SecP256r1MLKEM768', 'X25519MLKEM768', 'SecP384r1MLKEM1024' | ||
|
||
.. versionadded:: 3.15 |
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.
.. versionadded:: 3.15 | |
.. versionadded:: next |
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.
Done.
Doc/library/ssl.rst
Outdated
values. | ||
|
||
Example:: |
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.
values. | |
Example:: | |
values. For example:: |
And remove the indentation in the example (I don't think it's necessary)
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.
Done.
Doc/library/ssl.rst
Outdated
<https://docs.openssl.org/master/man3/SSL_CTX_set1_groups_list/>`_. | ||
|
||
.. note:: | ||
when connected, the :meth:`SSLSocket.group` method of SSL sockets will |
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.
when connected, the :meth:`SSLSocket.group` method of SSL sockets will | |
When connected, the :meth:`SSLSocket.group` method of SSL sockets will |
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.
Done.
Doc/library/ssl.rst
Outdated
@@ -1789,6 +1817,10 @@ to speed up repeated connections from the same clients. | |||
|
|||
.. versionadded:: 3.3 | |||
|
|||
.. deprecated:: 3.15 |
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.
This should be done in a separate PR as it would be better to raise a warning. Leave it supported for now.
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.
Understood - reverted for now.
Modules/_ssl.c
Outdated
if (self->ssl == NULL) | ||
Py_RETURN_NONE; | ||
group_name = SSL_get0_group_name(self->ssl); | ||
if (group_name == NULL) |
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.
Please follow PEP-7 for new code (add braces).
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.
Done.
Looks like the indentation is required. Got a doc build error without it.
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.
Some other comments (sorry I'm on mobile so it's hard)
Doc/library/ssl.rst
Outdated
the SSLContext's current TLS ``minimum_version`` and ``maximum_version`` | ||
values. For example:: | ||
|
||
>>> ctx = ssl.create_default_context() |
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.
I am a bad reviewer but I just saw that you actually copied the example style of the other example. Sorry to ask you to revert it... I would prefer to update it in a follow-up PR both examplesn(or more) on this page. Again I am very sorry (GH didn't show me enough context)
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.
I actually had to revert this to get the docs to build. In the doc preview, though, it looks like the block isn't actually indented. So, I think we may be good here if I understand correctly what you were looking for.
One thing I didn't like here is that the long list of returned values doesn't wrap. Should I add hard line breaks in there to manually wrap the returned value across multiple lines for readability, even though the real return value wouldn't?
Lib/test/test_ssl.py
Outdated
self.assertIsNone(ctx.set_groups('P-256:X25519')) | ||
|
||
if ssl.OPENSSL_VERSION_INFO >= (3, 5): | ||
self.assertNotIn('P-256', ctx.get_groups()) |
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.
Can we create multiple contexts to check this? Like:
1 context for which we set P-256 and then check get_groups
w and w/o aliases and 1 context for which we set P-256:X25519 and then check get_groups
w and w/o aliases? something like
def test(self):
ctx = ...
ctx.set_groups('P-256')
# checks
ctx = ...
ctx.set_groups('P-256:X25519')
# checks
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.
The values returned by ctx.get_groups() are affected only be ctx.minimum_version and ctx.maximum_version, and not by calls to ctx.set_groups(). There is a separate set of tests added in ThreadedTests.test_groups() which set the client and server contexts to different values and see what the resulting selected value is by querying SSLObject.group() after the handshake completes, including a test where the connection fails due to no overlapping groups. This is following the pattern of the test_ecdh_curve() test just above that.
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.
Ok then maybe only use either P-256 or P-256:X25519 in the set_groups call (namely only one such call) if possible and comment what we are testing (don't use docstrings). The name "test_groups" is not sufficiently clear at first glance.
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.
Ok - I've split out the test methods into separate test_set_groups() and test_get_groups(), and also added some comments within each of those. This also allowed me to do a skipUnless on test_get_groups() on platforms with too old an OpenSSL.
Lib/test/test_ssl.py
Outdated
stats = server_params_test(client_context, server_context, | ||
chatty=True, connectionchatty=True, | ||
sni_name=hostname) | ||
if ssl.OPENSSL_VERSION_INFO >= (3, 2): |
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.
Let's add a constant (in the file) say OPENSSL_CAN_QUERY_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3,2)
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.
In this case, I think two constants would be required. The SSLObject.group() call requires 3.2 or later, but the SSLContext.get_groups() call requires 3.5 or later.
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.
Yes please do so. It'll ease future maintenance especially if we then support other libcrypto implementations
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.
Adding CAN_GET_SELECTED_OPENSSL_GROUP and CAN_GET_AVAILABLE_OPENSSL_GROUPS.
Modules/_ssl.c
Outdated
|
||
for (i = 0; i < num; ++i) { | ||
group = sk_OPENSSL_CSTRING_value(groups, i); | ||
PyList_SET_ITEM(result, i, PyUnicode_DecodeFSDefault(group)); |
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.
This call can fail (decode call) so you need to handle the failure
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.
The values returned here are constant strings coming from OpenSSL, and they should always be plain ASCII. So, the decode should always succeed in this case. I can put in a check here, but it would effectively be dead code unless there was some kind of bug in OpenSSL. The function _ssl__SSLSocket_compression_impl contains another example of this.
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.
We need it if Python runs out of memory unfortunately. It comverts a const char* to a PyObject* so we need such guards.
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.
Ah, gotcha. Change made.
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.
The values returned here are constant strings coming from OpenSSL, and they should always be plain ASCII.
In this case, add an assert with a comment saying that those strings are statically allocated on OpenSSL's side. If they are dynamically fetched, checking for NULL would still be preferred but an assert doesn't hurt
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.
I've added an assert about group elements being non-NULL, as well as a comment about these items being internal constants which shouldn't be modified or freed. I also added a comment about the strings being ASCII, so there should be no decoding errors (though an allocation check on the decoded string is still being handled in the previous commit.
Modules/_ssl.c
Outdated
/*[clinic end generated code: output=6d6209dd1051529b input=3e8ee5deb277dcc5]*/ | ||
{ | ||
#if OPENSSL_VERSION_NUMBER >= 0x30500000L | ||
STACK_OF(OPENSSL_CSTRING) *groups; |
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.
Can we have heap allocation instead here? or is there some magic in OpenSSL?
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.
The word "stack" here is just what OpenSSL calls one of its data types (used for a list of items). The actual allocation is in the call to sk_OPENSSL_CSTRING_new_null just below this (with a check for allocation failure).
Modules/_ssl.c
Outdated
return NULL; | ||
} | ||
|
||
PyList_SET_ITEM(result, i, PyUnicode_DecodeFSDefault(group)); |
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.
PyList_SET_ITEM(result, i, PyUnicode_DecodeFSDefault(group)); | |
PyList_SET_ITEM(result, i, item); |
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.
Thanks for catching this!
Modules/_ssl.c
Outdated
|
||
if (!SSL_CTX_get0_implemented_groups(self->ctx, include_aliases, groups)) { | ||
_setSSLError(get_state_ctx(self), "Can't get groups", 0, __FILE__, __LINE__); | ||
sk_OPENSSL_CSTRING_free(groups); |
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.
Is the free() call a no-op on NULLs? I'd suggest having an error label responsible for XDECREF the result and free the groups (and don't forget to initialize them to NULL in this case).
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.
From what I read, it is safe to call things like sk_OPENSSL_CSTRING_free() on a null pointer. For PyObjects, though, there's Py_DECREF() and Py_XDECREF(). The latter is safe to use on a potential NULL pointer.
I've gone ahead and moved all the freeing to an "error" label as you suggested. Commit will be in shortly.
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.
From what I read, it is safe to call things like sk_OPENSSL_CSTRING_free() on a null pointer. For PyObjects, though, there's Py_DECREF() and Py_XDECREF(). The latter is supposedly safe on NULL pointers, but the former is not.
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? |
Doc/library/ssl.rst
Outdated
>>> ctx.minimum_version=ssl.TLSVersion.TLSv1_3 | ||
>>> ctx.maximum_version=ssl.TLSVersion.TLSv1_3 | ||
>>> ctx.get_groups() | ||
['secp256r1', 'secp384r1', 'secp521r1', 'x25519', 'x448', 'brainpoolP256r1tls13', 'brainpoolP384r1tls13', 'brainpoolP512r1tls13', 'ffdhe2048', 'ffdhe3072', 'ffdhe4096', 'ffdhe6144', 'ffdhe8192', 'MLKEM512', 'MLKEM768', 'MLKEM1024', 'SecP256r1MLKEM768', 'X25519MLKEM768', 'SecP384r1MLKEM1024'] |
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.
To reduce this, just use ... and add a +doctest: SKIP comment
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.
Ok, done.
Doc/library/ssl.rst
Outdated
>>> ctx.minimum_version=ssl.TLSVersion.TLSv1_3 | ||
>>> ctx.maximum_version=ssl.TLSVersion.TLSv1_3 |
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.
>>> ctx.minimum_version=ssl.TLSVersion.TLSv1_3 | |
>>> ctx.maximum_version=ssl.TLSVersion.TLSv1_3 | |
>>> ctx.minimum_version = ssl.TLSVersion.TLSv1_3 | |
>>> ctx.maximum_version = ssl.TLSVersion.TLSv1_3 |
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.
Done.
ctx = ssl.create_default_context() | ||
|
||
# P-256 isn't an IANA name, so it shouldn't be returned by default | ||
self.assertNotIn('P-256', ctx.get_groups()) |
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.
Is there a default group that is known to always be returned?
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.
The set of returned versions varies depending on the OpenSSL version. However, with the default of include_aliases=False
, it should be guaranteed that P-256 will not be included since it is an alias. With include_aliases=True
, I believe it should be included in all versions, going all the way back to OpenSSL 1.1.1, and I did some local testing to confirm that.
Modules/_ssl.c
Outdated
|
||
if (self->ssl == NULL) { | ||
Py_RETURN_NONE; | ||
} | ||
|
||
group_name = SSL_get0_group_name(self->ssl); | ||
if (group_name == NULL) { | ||
Py_RETURN_NONE; | ||
} | ||
|
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.
if (self->ssl == NULL) { | |
Py_RETURN_NONE; | |
} | |
group_name = SSL_get0_group_name(self->ssl); | |
if (group_name == NULL) { | |
Py_RETURN_NONE; | |
} | |
if (self->ssl == NULL) { | |
Py_RETURN_NONE; | |
} | |
group_name = SSL_get0_group_name(self->ssl); | |
if (group_name == NULL) { | |
Py_RETURN_NONE; | |
} |
You can write more compact code in general
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.
What did you have in mind here? With the group_name assignment, it becomes difficult to combine the blocks above and below,e even though they both do a Py_RETURN_NONE. I'm happy to remove the blank lines if you prefer that. The style guide wasn't completely clear on that and the existing code is a mixture of cases containing blank lines after close braces and not.
Modules/_ssl.c
Outdated
} | ||
|
||
/* | ||
* Note: The "groups" stack is dynamically allocated, but the strings |
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.
This comment should be split into parts:
- comment about constness should be above the assert.
- comment about the dynamic memory should be at the declaration of stack_of
- comment about non-failure for decoding should in the if (item == NULL).
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.
Ok - I've made an attempt at this, though I'm not sure it completely matches what you had in mind. In particular, it was difficult to split the comment on the lack of allocation of strings inside the stack from the points later about not needing a NULL check on "group" since there's no allocation. It also meant I had to add a comment about the fact that item could be NULL due to an allocation error, even though there's no issue with decoding errors.
These and other requested changes are now in.
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) |
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.
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. |
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) |
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:
|
This is an initial implementation of the feature proposed in issue #136306.
📚 Documentation preview 📚: https://cpython-previews--136307.org.readthedocs.build/