From 4f52cd3f5dc503e21d0e9dbb60dbd0c94754d675 Mon Sep 17 00:00:00 2001 From: Bernie Hackett Date: Mon, 30 Jan 2017 16:26:09 -0800 Subject: [PATCH] PYTHON-1158 - Use SSLContext and PROTOCOL_TLS_CLIENT This change works around deprecations in CPython 3.6 and expected deprecations in later Python releases. --- pymongo/pool.py | 53 ++++++++++++++-------- pymongo/ssl_context.py | 99 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 pymongo/ssl_context.py diff --git a/pymongo/pool.py b/pymongo/pool.py index 6e6becca2..75cdc4706 100644 --- a/pymongo/pool.py +++ b/pymongo/pool.py @@ -24,9 +24,14 @@ from pymongo.common import HAS_SSL from pymongo.errors import ConnectionFailure, ConfigurationError try: - from ssl import match_hostname + from ssl import match_hostname, CertificateError except ImportError: - from pymongo.ssl_match_hostname import match_hostname + from pymongo.ssl_match_hostname import match_hostname, CertificateError + +try: + from ssl import SSLContext as _SSLContext +except ImportError: + from pymongo.ssl_context import SSLContext as _SSLContext if HAS_SSL: import ssl @@ -182,15 +187,30 @@ class Pool: self.wait_queue_timeout = wait_queue_timeout self.wait_queue_multiple = wait_queue_multiple self.socket_keepalive = socket_keepalive - self.use_ssl = use_ssl - self.ssl_keyfile = ssl_keyfile - self.ssl_certfile = ssl_certfile - self.ssl_cert_reqs = ssl_cert_reqs - self.ssl_ca_certs = ssl_ca_certs + self.ssl_ctx = None self.ssl_match_hostname = ssl_match_hostname - if HAS_SSL and use_ssl and not ssl_cert_reqs: - self.ssl_cert_reqs = ssl.CERT_NONE + if HAS_SSL and use_ssl: + # PROTOCOL_TLS_CLIENT added and PROTOCOL_SSLv23 + # deprecated in CPython 3.6 + self.ssl_ctx = _SSLContext( + getattr(ssl, 'PROTOCOL_TLS_CLIENT', ssl.PROTOCOL_SSLv23)) + # SSLContext.check_hostname was added in 2.7.9 and 3.4. Using it + # forces the use of SNI, which PyMongo 2.x doesn't support. + # PROTOCOL_TLS_CLIENT enables this by default. Since we call + # match_hostname directly disable this explicitly. + if hasattr(self.ssl_ctx, "check_hostname"): + self.ssl_ctx.check_hostname = False + if ssl_certfile is not None: + self.ssl_ctx.load_cert_chain(ssl_certfile, ssl_keyfile) + if ssl_ca_certs is not None: + self.ssl_ctx.load_verify_locations(ssl_ca_certs) + # PROTOCOL_TLS_CLIENT sets verify_mode to CERT_REQUIRED so + # we always have to set this explicitly. + if ssl_cert_reqs is not None: + self.ssl_ctx.verify_mode = ssl_cert_reqs + else: + self.ssl_ctx.verify_mode = ssl.CERT_NONE # Map self._ident.get() -> request socket self._tid_to_sock = {} @@ -295,16 +315,15 @@ class Pool: sock = self.create_connection() hostname = self.pair[0] - if self.use_ssl: + if self.ssl_ctx is not None: try: - sock = ssl.wrap_socket(sock, - certfile=self.ssl_certfile, - keyfile=self.ssl_keyfile, - ca_certs=self.ssl_ca_certs, - cert_reqs=self.ssl_cert_reqs) - if self.ssl_cert_reqs and self.ssl_match_hostname: + sock = self.ssl_ctx.wrap_socket(sock) + if self.ssl_ctx.verify_mode and self.ssl_match_hostname: match_hostname(sock.getpeercert(), hostname) - + # CertificateError doesn't inherit from SSLError. + except CertificateError: + sock.close() + raise except ssl.SSLError: sock.close() raise ConnectionFailure("SSL handshake failed. MongoDB may " diff --git a/pymongo/ssl_context.py b/pymongo/ssl_context.py new file mode 100644 index 000000000..5939e4fa3 --- /dev/null +++ b/pymongo/ssl_context.py @@ -0,0 +1,99 @@ +# Copyright 2014-2015 MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you +# may not use this file except in compliance with the License. You +# may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. See the License for the specific language governing +# permissions and limitations under the License. + +"""A fake SSLContext implementation.""" + +try: + import ssl + _CERT_NONE = ssl.CERT_NONE +except ImportError: + _CERT_NONE = None + + +class SSLContext(object): + """A fake SSLContext. + + This implements an API similar to ssl.SSLContext from python 3.2 + but does not implement methods or properties that would be + incompatible with ssl.wrap_socket from python 2.6. + + You must pass protocol which must be one of the PROTOCOL_* constants + defined in the ssl module. ssl.PROTOCOL_SSLv23 is recommended for maximum + interoperability. + """ + + __slots__ = ('_cafile', '_certfile', + '_keyfile', '_protocol', '_verify_mode') + + def __init__(self, protocol): + self._cafile = None + self._certfile = None + self._keyfile = None + self._protocol = protocol + self._verify_mode = _CERT_NONE + + @property + def protocol(self): + """The protocol version chosen when constructing the context. + This attribute is read-only. + """ + return self._protocol + + def __get_verify_mode(self): + """Whether to try to verify other peers' certificates and how to + behave if verification fails. This attribute must be one of + ssl.CERT_NONE, ssl.CERT_OPTIONAL or ssl.CERT_REQUIRED. + """ + return self._verify_mode + + def __set_verify_mode(self, value): + """Setter for verify_mode.""" + self._verify_mode = value + + verify_mode = property(__get_verify_mode, __set_verify_mode) + + def load_cert_chain(self, certfile, keyfile=None): + """Load a private key and the corresponding certificate. The certfile + string must be the path to a single file in PEM format containing the + certificate as well as any number of CA certificates needed to + establish the certificate's authenticity. The keyfile string, if + present, must point to a file containing the private key. Otherwise + the private key will be taken from certfile as well. + """ + self._certfile = certfile + self._keyfile = keyfile + + def load_verify_locations(self, cafile=None, dummy=None): + """Load a set of "certification authority"(CA) certificates used to + validate other peers' certificates when `~verify_mode` is other than + ssl.CERT_NONE. + """ + self._cafile = cafile + + # suppress_ragged_eofs (dummy0) is not supported in the ssl module from pypi + # ciphers (dummy1) is not supported in CPython < 2.7.9 / 3.2 + def wrap_socket(self, sock, server_side=False, + do_handshake_on_connect=True, + dummy0=None, dummy1=None): + """Wrap an existing Python socket sock and return an ssl.SSLSocket + object. + """ + return ssl.wrap_socket(sock, keyfile=self._keyfile, + certfile=self._certfile, + server_side=server_side, + cert_reqs=self._verify_mode, + ssl_version=self._protocol, + ca_certs=self._cafile, + do_handshake_on_connect=do_handshake_on_connect) +