From 01c3a9f31d95aa9e385e2c18ee7dd6dcfbab1526 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 29 Jul 2024 11:19:49 +0200 Subject: [PATCH 1/2] Fix Python 3.12 support from https://github.com/OpenKMIP/PyKMIP/pull/707 --- kmip/services/kmip_client.py | 16 +++++++----- kmip/services/server/server.py | 25 +++++++++++++------ .../tests/unit/services/server/test_server.py | 6 ++--- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/kmip/services/kmip_client.py b/kmip/services/kmip_client.py index 7f72adf7..438391ab 100644 --- a/kmip/services/kmip_client.py +++ b/kmip/services/kmip_client.py @@ -285,13 +285,17 @@ def open(self): six.reraise(*last_error) def _create_socket(self, sock): - self.socket = ssl.wrap_socket( + context = ssl.SSLContext(self.ssl_version) + context.verify_mode = self.cert_reqs + if self.ca_certs: + context.load_verify_locations(self.ca_certs) + if self.keyfile and not self.certfile: + raise ValueError("certfile must be specified") + if self.certfile: + context.load_cert_chain(self.certfile, self.keyfile) + self.socket = context.wrap_socket( sock, - keyfile=self.keyfile, - certfile=self.certfile, - cert_reqs=self.cert_reqs, - ssl_version=self.ssl_version, - ca_certs=self.ca_certs, + server_side=False, do_handshake_on_connect=self.do_handshake_on_connect, suppress_ragged_eofs=self.suppress_ragged_eofs) self.socket.settimeout(self.timeout) diff --git a/kmip/services/server/server.py b/kmip/services/server/server.py index 534ab61d..cfbbb705 100644 --- a/kmip/services/server/server.py +++ b/kmip/services/server/server.py @@ -287,17 +287,26 @@ 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.auth_suite.protocol) + context.verify_mode = ssl.CERT_REQUIRED + if self.auth_suite.ciphers: + context.set_ciphers(self.auth_suite.ciphers) + if cafile: + context.load_verify_locations(cafile) + certfile = self.config.settings.get('certificate_path') + + 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") + + self._socket = context.wrap_socket( self._socket, - keyfile=self.config.settings.get('key_path'), - certfile=self.config.settings.get('certificate_path'), server_side=True, - cert_reqs=ssl.CERT_REQUIRED, - ssl_version=self.auth_suite.protocol, - ca_certs=self.config.settings.get('ca_path'), do_handshake_on_connect=False, - suppress_ragged_eofs=True, - ciphers=self.auth_suite.ciphers + suppress_ragged_eofs=True ) try: diff --git a/kmip/tests/unit/services/server/test_server.py b/kmip/tests/unit/services/server/test_server.py index a9e9f194..0384e8fc 100644 --- a/kmip/tests/unit/services/server/test_server.py +++ b/kmip/tests/unit/services/server/test_server.py @@ -210,9 +210,9 @@ def test_start(self, # 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() @@ -271,7 +271,7 @@ def test_start(self, # Test that a NetworkingError is generated if the socket bind fails. with mock.patch('socket.socket') as socket_mock: - with mock.patch('ssl.wrap_socket') as ssl_mock: + with mock.patch('ssl.SSLContext.wrap_socket') as ssl_mock: socket_mock.return_value = a_mock ssl_mock.return_value = b_mock From 0eddc1167a03b994becdbd322c0238fb7e92bfa1 Mon Sep 17 00:00:00 2001 From: Peter Eisentraut Date: Mon, 29 Jul 2024 11:24:14 +0200 Subject: [PATCH 2/2] Add key_password setting to client This allows passing a password or callback to get the password for the client SSL key. closes #588 --- docs/source/client.rst | 9 ++++++++- kmip/pie/client.py | 4 ++++ kmip/services/kmip_client.py | 16 ++++++++++++---- kmip/tests/unit/services/test_kmip_client.py | 1 + 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/docs/source/client.rst b/docs/source/client.rst index 0788882f..881b7683 100644 --- a/docs/source/client.rst +++ b/docs/source/client.rst @@ -85,6 +85,9 @@ The different configuration options are defined below: * ``password`` A string representing the password to use for KMIP requests. Optional depending on server access policies. Leave blank if not needed. +* ``key_password`` + A string representing the password to unlock the specified SSL + key, if needed. Leave blank if not needed. The client can also be configured manually via Python. The following example shows how to create the ``ProxyKmipClient`` in Python code, directly @@ -122,7 +125,7 @@ Class Documentation ------------------- .. py:module:: kmip.pie.client -.. py:class:: ProxyKmipClient(hostname=None, port=None, cert=None, key=None, ca=None, ssl_version=None, username=None, password=None, config='client', config_file=None, kmip_version=None) +.. py:class:: ProxyKmipClient(hostname=None, port=None, cert=None, key=None, ca=None, ssl_version=None, username=None, password=None, key_password=None, config='client', config_file=None, kmip_version=None) A simplified KMIP client for conducting KMIP operations. @@ -151,6 +154,10 @@ Class Documentation use for operations. Optional, defaults to None. :param string password: The password of the KMIP appliance account to use for operations. Optional, defaults to None. + :param XXX key_password: The password to unlock the specified SSL + key, if needed. This is passed to + `SSLContext.load_cert_chain()` and can use any of the formats + accepted by that method. :param string config: The name of a section in the PyKMIP configuration file. Use to load a specific set of configuration settings from the configuration file, instead of specifying them manually. Optional, diff --git a/kmip/pie/client.py b/kmip/pie/client.py index d27ad4b1..d58285e2 100644 --- a/kmip/pie/client.py +++ b/kmip/pie/client.py @@ -63,6 +63,7 @@ def __init__(self, ssl_version=None, username=None, password=None, + key_password=None, config='client', config_file=None, kmip_version=None): @@ -88,6 +89,8 @@ def __init__(self, use for operations. Optional, defaults to None. password (string): The password of the KMIP appliance account to use for operations. Optional, defaults to None. + key_password (XXX): The password for the 'key', passed to + 'SSLContext.load_cert_chain()'. config (string): The name of a section in the PyKMIP configuration file. Use to load a specific set of configuration settings from the configuration file, instead of specifying them manually. @@ -115,6 +118,7 @@ def __init__(self, ssl_version=ssl_version, username=username, password=password, + key_password=key_password, config=config, config_file=config_file, kmip_version=kmip_version diff --git a/kmip/services/kmip_client.py b/kmip/services/kmip_client.py index 438391ab..4b9d6a0f 100644 --- a/kmip/services/kmip_client.py +++ b/kmip/services/kmip_client.py @@ -80,7 +80,9 @@ def __init__(self, host=None, port=None, keyfile=None, cert_reqs=None, ssl_version=None, ca_certs=None, do_handshake_on_connect=None, suppress_ragged_eofs=None, - username=None, password=None, timeout=30, config='client', + username=None, password=None, + key_password=None, + timeout=30, config='client', config_file=None, kmip_version=None): self.logger = logging.getLogger(__name__) @@ -111,7 +113,8 @@ def __init__(self, host=None, port=None, keyfile=None, self._set_variables(host, port, keyfile, certfile, cert_reqs, ssl_version, ca_certs, do_handshake_on_connect, suppress_ragged_eofs, - username, password, timeout, config_file) + username, password, key_password, + timeout, config_file) self.batch_items = [] self.conformance_clauses = [ @@ -292,7 +295,8 @@ def _create_socket(self, sock): if self.keyfile and not self.certfile: raise ValueError("certfile must be specified") if self.certfile: - context.load_cert_chain(self.certfile, self.keyfile) + context.load_cert_chain(self.certfile, self.keyfile, + password=self.key_password) self.socket = context.wrap_socket( sock, server_side=False, @@ -1742,7 +1746,8 @@ def _send_and_receive_message(self, request): def _set_variables(self, host, port, keyfile, certfile, cert_reqs, ssl_version, ca_certs, do_handshake_on_connect, suppress_ragged_eofs, - username, password, timeout, config_file): + username, password, key_password, + timeout, config_file): conf = ConfigHelper(config_file) # TODO: set this to a host list @@ -1791,6 +1796,9 @@ def _set_variables(self, host, port, keyfile, certfile, self.password = conf.get_valid_value( password, self.config, 'password', conf.DEFAULT_PASSWORD) + self.key_password = conf.get_valid_value( + key_password, self.config, 'key_password', None) + self.timeout = int(conf.get_valid_value( timeout, self.config, 'timeout', conf.DEFAULT_TIMEOUT)) if self.timeout < 0: diff --git a/kmip/tests/unit/services/test_kmip_client.py b/kmip/tests/unit/services/test_kmip_client.py index fa13a204..9137db67 100644 --- a/kmip/tests/unit/services/test_kmip_client.py +++ b/kmip/tests/unit/services/test_kmip_client.py @@ -715,6 +715,7 @@ def test_host_list_import_string(self): suppress_ragged_eofs=None, username=None, password=None, + key_password=None, timeout=None, config_file=None )