Skip to content
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

Add python 3.12 and 3.13 support #707

Open
wants to merge 28 commits into
base: master
Choose a base branch
from

Conversation

blink1073
Copy link

@blink1073 blink1073 commented Oct 4, 2023

Fixes #706

@blink1073
Copy link
Author

The traceback on my fork is:

kmip/tests/integration/conftest.py:49: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
kmip/pie/client.py:173: in open
    self.proxy.open()
kmip/services/kmip_client.py:285: in open
    six.reraise(*last_error)
.tox/integration/lib/python3.9/site-packages/six.py:719: in reraise
    raise value
kmip/services/kmip_client.py:274: in open
    self.socket.connect((self.host, self.port))
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/ssl.py:1376: in connect
    self._real_connect(addr, False)
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/ssl.py:1367: in _real_connect
    self.do_handshake()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <ssl.SSLSocket [closed] fd=-1, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
block = False

    @_sslcopydoc
    def do_handshake(self, block=False):
        self._check_connected()
        timeout = self.gettimeout()
        try:
            if timeout == 0.0 and block:
                self.settimeout(None)
>           self._sslobj.do_handshake()
E           ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1129)

/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/ssl.py:1343: SSLCertVerificationError
_ ERROR at setup of TestProxyKmipClientIntegration.test_x509_certificate_register_get_destroy _

request = <SubRequest 'simple' for <TestCaseFunction test_asymmetric_key_pair_create_get_destroy>>

    @pytest.fixture(scope="class")
    def simple(request):
        config = request.config.getoption("--config")
    
        client = pclient.ProxyKmipClient(config=config)
>       client.open()

kmip/tests/integration/conftest.py:49: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
kmip/pie/client.py:173: in open
    self.proxy.open()
kmip/services/kmip_client.py:285: in open
    six.reraise(*last_error)
.tox/integration/lib/python3.9/site-packages/six.py:719: in reraise
    raise value
kmip/services/kmip_client.py:274: in open
    self.socket.connect((self.host, self.port))
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/ssl.py:1376: in connect
    self._real_connect(addr, False)
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/ssl.py:1367: in _real_connect
    self.do_handshake()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <ssl.SSLSocket [closed] fd=-1, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>
block = False

    @_sslcopydoc
    def do_handshake(self, block=False):
        self._check_connected()
        timeout = self.gettimeout()
        try:
            if timeout == 0.0 and block:
                self.settimeout(None)
>           self._sslobj.do_handshake()
E           ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1129)

/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/ssl.py:1343: SSLCertVerificationError

context.check_hostname = False
if self.certfile:
context.load_cert_chain(self.certfile, self.keyfile)
self.socket = context.wrap_socket(

Check failure

Code scanning / CodeQL

Use of insecure SSL/TLS version

Insecure SSL/TLS protocol version TLSv1 allowed by [call to ssl.create_default_context](1). Insecure SSL/TLS protocol version TLSv1_1 allowed by [call to ssl.create_default_context](1).
keyfile = self.config.settings.get('key_path')
context.load_cert_chain(certfile, keyfile=keyfile)

self._socket = context.wrap_socket(

Check failure

Code scanning / CodeQL

Use of insecure SSL/TLS version

Insecure SSL/TLS protocol version TLSv1 allowed by [call to ssl.create_default_context](1). Insecure SSL/TLS protocol version TLSv1_1 allowed by [call to ssl.create_default_context](1).
@s-t-e-v-e-n-k
Copy link

The problem with your mocking is that you can't mock ssl.SSLContext.wrap_socket because the code constructs an SSLContext object and then calls wrap_socket on that, so you're not mocking the right thing.

I've edited the test changes for your patch (and not your code changes, which looks good) thusly:

@@ -210,9 +210,9 @@ class TestKmipServer(testtools.TestCase)
         # Test that in ideal cases no errors are generated and the right
         # log messages are.
         with mock.patch('socket.socket') as socket_mock:
-            with mock.patch('ssl.wrap_socket') as ssl_mock:
+            with mock.patch('ssl.SSLContext') as ssl_mock:
                 socket_mock.return_value = a_mock
-                ssl_mock.return_value = b_mock
+                ssl_mock.return_value.wrap_socket.return_value = b_mock

                 manager_mock.assert_not_called()
                 monitor_mock.assert_not_called()

And I've confirmed the tests pass with Python 3.9-3.12.

@blink1073
Copy link
Author

Thanks @s-t-e-v-e-n-k, new build in progress on my fork.

@blink1073
Copy link
Author

Still no joy: ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate (_ssl.c:1133)

@avikivity
Copy link

@blink1073 were you able to make this work?

@blink1073
Copy link
Author

No, I haven't been able to figure out the testing portion.

@@ -285,13 +285,14 @@ def open(self):
six.reraise(*last_error)

def _create_socket(self, sock):
self.socket = ssl.wrap_socket(
purpose = ssl.Purpose.SERVER_AUTH
context = ssl.create_default_context(purpose=purpose, capath=self.ca_certs)
Copy link

Choose a reason for hiding this comment

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

This should be cafile=... instead of capath=..., because the underlying configuration parameter points to a file.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

self._socket = ssl.wrap_socket(
purpose = ssl.Purpose.CLIENT_AUTH
capath = self.config.settings.get('ca_path')
context = ssl.create_default_context(purpose=purpose, capath=capath)
Copy link

Choose a reason for hiding this comment

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

This should be cafile=capath instead of capath=capath, because the underlying configuration parameter points to a file. (The naming is unfortunately inconsistent between the Python ssl module and pykmip.)

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

Copy link

Choose a reason for hiding this comment

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

This no longer pays attention to the configuration settings self.cert_reqs and self.ssl_versions. That's probably okay in practice, since the defaults are good, but these settings are documented, so they should work.

One option might be to not use ssl.create_default_context() and instead use ssl.SSLContext directly and apply these settings. Like

        context = ssl.SSLContext(self.ssl_version)
        context.verify_mode = self.cert_reqs
        if self.ca_certs:
            context.load_verify_locations(self.ca_certs)

This is just about as much code as before but it reproduces the previous behavior more precisely.

Copy link
Author

Choose a reason for hiding this comment

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

Done

Copy link

Choose a reason for hiding this comment

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

Similar to the client, we should make sure that this change does not lose any previously working configuration settings. I see for example that ssl_version=self.auth_suite.protocol and ciphers=self.auth_suite.ciphers don't get used anymore.

Copy link
Author

Choose a reason for hiding this comment

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

Updated

self._socket = ssl.wrap_socket(
purpose = ssl.Purpose.CLIENT_AUTH
capath = self.config.settings.get('ca_path')
context = ssl.create_default_context(purpose=purpose, capath=capath)
Copy link

Choose a reason for hiding this comment

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

You must set

context.verify_mode=ssl.CERT_REQUIRED

after creating context. This matches what the previous code did. Without this, the server won't ask the client to send a certificate, and without a client certificate this will later fail at higher levels (such as

certificate = auth.get_certificate_from_connection(
).

Copy link
Author

Choose a reason for hiding this comment

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

Done

@blink1073
Copy link
Author

Thanks @petere! I believe I've addressed your feedback.

@blink1073 blink1073 requested review from petere July 25, 2024 00:07
context.verify_mode = self.cert_reqs
if self.ca_certs:
context.load_verify_locations(self.ca_certs)
context.check_hostname = False
Copy link

Choose a reason for hiding this comment

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

This line doesn't appear to be needed. Probably left over from a previous version that used create_default_context().

Copy link
Author

Choose a reason for hiding this comment

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

Removed

if self.ca_certs:
context.load_verify_locations(self.ca_certs)
context.check_hostname = False
if self.certfile:
Copy link

Choose a reason for hiding this comment

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

I'm testing combinations of "forgetting" to specify certfile or keyfile in the configuration file. With this code, if you specify certfile but not keyfile, you'll get an exception from load_cert_chain. If you specify keyfile but not certfile, this code is skipped and you'll get whatever the SSL stack ends up deciding. The old module-level wrap_socket had an additional check

    if keyfile and not certfile:
        raise ValueError("certfile must be specified")

to prevent this. Maybe this would be good to have here as well.

Copy link
Author

Choose a reason for hiding this comment

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

Done

@@ -287,17 +287,24 @@ def interrupt_handler(trigger, frame):
for cipher in auth_suite_ciphers:
self._logger.debug(cipher)

self._socket = ssl.wrap_socket(
cafile = self.config.settings.get('ca_path')
context = ssl.SSLContext(self.ssl_version)
Copy link

Choose a reason for hiding this comment

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

This should be

context = ssl.SSLContext(self.auth_suite.protocol)

(wrongly copy-pasted from client code?)

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

cafile = self.config.settings.get('ca_path')
context = ssl.SSLContext(self.ssl_version)
context.verify_mode = ssl.CERT_REQUIRED
if auth_suite_ciphers:
Copy link

Choose a reason for hiding this comment

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

This should be self.auth_suite.ciphers. I'm not sure exactly what the relationship between that and auth_suite_ciphers is, but this one crashes.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

context.load_verify_locations(cafile)
certfile = self.config.settings.get('certificate_path')

if certfile:
Copy link

Choose a reason for hiding this comment

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

As with the client, it's worth checking here about how different combinations of not specifying certificate or key behave. For example, the old module-level wrap_socket() had a check

    if server_side and not certfile:
        raise ValueError("certfile must be specified for server-side "
                         "operations")

so omitting the certificate would have been a hard error.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

Copy link

Choose a reason for hiding this comment

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

I think what I had in mind was like

         if certfile:
             keyfile = self.config.settings.get('key_path')
             context.load_cert_chain(certfile, keyfile=keyfile)
+        else:
+            raise ValueError("certfile must be specified for server-side operations")

Copy link
Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Author

Choose a reason for hiding this comment

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

I instead let the context raise an appropriate error. This has the benefit of letting us mock the context behavior in the tests.

keyfile = self.config.settings.get('key_path')
context.load_cert_chain(certfile, keyfile=keyfile)
else:
raise ValueError("certfile must be specified for server-side operations")
Copy link

Choose a reason for hiding this comment

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

What we don't have here is the equivalent of the client-side check

        if self.keyfile and not self.certfile:
            raise ValueError("certfile must be specified")

but it also turns out that the higher levels already check that both certificate and key are specified:

kmip.core.exceptions.ConfigurationError: Setting 'key_path' is missing from the configuration file.
kmip.core.exceptions.ConfigurationError: Setting 'certificate_path' is missing from the configuration file.

So it's probably not necessary.

Copy link

Choose a reason for hiding this comment

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

Also, it looks like this causes the test suite to fail in TestKmipServer.test_start:

ValueError: certfile must be specified for server-side operations

So either this code or the test setup would need adjustments.

Copy link
Author

Choose a reason for hiding this comment

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

Removed from the server

petere added a commit to petere/PyKMIP that referenced this pull request Jul 29, 2024
@blink1073 blink1073 requested a review from petere July 29, 2024 18:37
@blink1073
Copy link
Author

@petere is there anything else you'd like me to try?

@petere
Copy link

petere commented Nov 27, 2024

Other than the single comment I just added (which I just got my looking at my local copy, but it's been a while since I played with this), I am content with this version.

How we get from that to getting this merged and released, I don't know.

@blink1073 blink1073 changed the title Add python 3.12 support Add python 3.12 and 3.13 support Dec 2, 2024
@blink1073 blink1073 marked this pull request as ready for review December 2, 2024 12:21
@blink1073
Copy link
Author

This is now ready for review. cc @arp102.

@blink1073
Copy link
Author

The workflows are all passing on my fork: https://github.com/blink1073/PyKMIP/actions/runs/12119149288?pr=1

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

Successfully merging this pull request may close these issues.

Use of removed ssl.wrap_socket
4 participants