From 4b218afdac4d6bbde00d4a0089dfa914ef5bd575 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Sat, 16 Jan 2016 14:13:14 +0100 Subject: [PATCH 1/2] Update SSL related code to use ssl.PROTOCOL_v23 constant on Python >= 2.7.9 and Python >= 3.4 by default. In those versions SSL v3.0 is disabled by default so it's safe to use this constant and it results in the best compatibility since it will use TLS v1.0 / v1.1 / v1.2 based on the versions supported by the server. Also refactor exception wrapping functionality into a separate function. --- libcloud/httplib_ssl.py | 61 +++++++++++++++++++++++++---------------- libcloud/security.py | 44 +++++++++++++++++++++++++++-- libcloud/utils/py3.py | 2 ++ 3 files changed, 81 insertions(+), 26 deletions(-) diff --git a/libcloud/httplib_ssl.py b/libcloud/httplib_ssl.py index 5f11d05fc8..c2d14ee025 100644 --- a/libcloud/httplib_ssl.py +++ b/libcloud/httplib_ssl.py @@ -12,10 +12,12 @@ # 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. + """ Subclass for httplib.HTTPSConnection with optional certificate name verification, depending on libcloud.security settings. """ + import os import sys import socket @@ -24,6 +26,7 @@ import warnings import libcloud.security +from libcloud.security import get_ssl_version from libcloud.utils.py3 import b from libcloud.utils.py3 import httplib from libcloud.utils.py3 import urlparse @@ -290,6 +293,9 @@ def connect(self): if self.http_proxy_used: self._activate_http_proxy(sock=sock) + # Dynamically retrieve SSL version which is to be used + ssl_version = get_ssl_version() + try: self.sock = ssl.wrap_socket( sock, @@ -297,31 +303,10 @@ def connect(self): self.cert_file, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.ca_cert, - ssl_version=libcloud.security.SSL_VERSION) + ssl_version=ssl_version) except socket.error: exc = sys.exc_info()[1] - exc_msg = str(exc) - - # Re-throw an exception with a more friendly error message - if 'connection reset by peer' in exc_msg.lower(): - ssl_version = libcloud.security.SSL_VERSION - ssl_version = SSL_CONSTANT_TO_TLS_VERSION_MAP[ssl_version] - msg = (UNSUPPORTED_TLS_VERSION_ERROR_MSG % - (exc_msg, ssl_version)) - - # Note: In some cases arguments are (errno, message) and in - # other it's just (message,) - exc_args = getattr(exc, 'args', []) - - if len(exc_args) == 2: - new_exc_args = [exc.args[0], msg] - else: - new_exc_args = [msg] - - new_exc = socket.error(*new_exc_args) - new_exc.original_exc = exc - raise new_exc - + exc = get_socket_error_exception(ssl_version=ssl_version, exc=exc) raise exc cert = self.sock.getpeercert() @@ -330,3 +315,33 @@ def connect(self): except CertificateError: e = sys.exc_info()[1] raise ssl.SSLError('Failed to verify hostname: %s' % (str(e))) + + +def get_socket_error_exception(ssl_version, exc): + """ + Function which intercepts socket.error exceptions and re-throws an + exception with a more user-friendly message in case server doesn't support + requested SSL version. + """ + exc_msg = str(exc) + + # Re-throw an exception with a more friendly error message + if 'connection reset by peer' in exc_msg.lower(): + ssl_version_name = SSL_CONSTANT_TO_TLS_VERSION_MAP[ssl_version] + msg = (UNSUPPORTED_TLS_VERSION_ERROR_MSG % + (exc_msg, ssl_version_name)) + + # Note: In some cases arguments are (errno, message) and in + # other it's just (message,) + exc_args = getattr(exc, 'args', []) + + if len(exc_args) == 2: + new_exc_args = [exc.args[0], msg] + else: + new_exc_args = [msg] + + new_exc = socket.error(*new_exc_args) + new_exc.original_exc = exc + return new_exc + + return exc diff --git a/libcloud/security.py b/libcloud/security.py index 4d024db58a..3659b8d421 100644 --- a/libcloud/security.py +++ b/libcloud/security.py @@ -19,22 +19,35 @@ import libcloud.security libcloud.security.VERIFY_SSL_CERT = True - # Optional. + # Optional libcloud.security.CA_CERTS_PATH.append('/path/to/cacert.txt') + + # Use a custom SSL / TLS version + libcloud.security.SSL_VERSION = ssl.PROTOCOL_TLSv1_2 """ import os import ssl +from libcloud.utils.py3 import PY2_post_279 +from libcloud.utils.py3 import PY3_post_34 + __all__ = [ 'VERIFY_SSL_CERT', 'SSL_VERSION', - 'CA_CERTS_PATH' + 'CA_CERTS_PATH', + + 'get_ssl_version' ] VERIFY_SSL_CERT = True -SSL_VERSION = ssl.PROTOCOL_TLSv1 +# Default SSL version which is used if one is not explicitli specified +DEFAULT_SSL_VERSION = ssl.PROTOCOL_TLSv1 + +# SSL version to be used as specified by the user +SSL_VERSION = None + # File containing one or more PEM-encoded CA certificates # concatenated together. @@ -87,3 +100,28 @@ 'certificate verification, please visit the libcloud ' 'documentation.' ) + + +def get_ssl_version(): + """ + Return SSL / TLS version which is to be used when establishing SSL / TLS + connection. + + The return value depends on the Python version and + libcloud.security.SSL_VERSION setting value. + + :rtype: ``int`` + """ + ssl_version = DEFAULT_SSL_VERSION + + # In Python >= 2.7.9 and >= 3.4 unsecure SSL v3.0 is disabled by default + # so it's safe to use PROTOCOL_SSLv23. + if PY2_post_279 or PY3_post_34: + ssl_version = ssl.PROTOCOL_SSLv23 + + # libcloud.security.SSL_VERSION has precedence over dynamicaly obtained + # values + if SSL_VERSION: + ssl_version = SSL_VERSION + + return ssl_version diff --git a/libcloud/utils/py3.py b/libcloud/utils/py3.py index f7a66c03e2..3f4f7369d6 100644 --- a/libcloud/utils/py3.py +++ b/libcloud/utils/py3.py @@ -30,11 +30,13 @@ PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 +PY2_post_279 = PY2 and sys.version_info >= (2, 7, 0) PY2_pre_25 = PY2 and sys.version_info < (2, 5) PY2_pre_26 = PY2 and sys.version_info < (2, 6) PY2_pre_27 = PY2 and sys.version_info < (2, 7) PY2_pre_279 = PY2 and sys.version_info < (2, 7, 9) PY3_pre_32 = PY3 and sys.version_info < (3, 2) +PY3_post_34 = PY3 and sys.version_info >= (3, 4, 0) PY2 = False PY25 = False From 67b5131222ab72a583f203af69a9b0dd42876877 Mon Sep 17 00:00:00 2001 From: Tomaz Muraus Date: Sat, 16 Jan 2016 14:31:06 +0100 Subject: [PATCH 2/2] Add test cases for get_ssl_version function. --- libcloud/test/test_httplib_ssl.py | 41 ++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/libcloud/test/test_httplib_ssl.py b/libcloud/test/test_httplib_ssl.py index 28eed69bbe..65e1b70721 100644 --- a/libcloud/test/test_httplib_ssl.py +++ b/libcloud/test/test_httplib_ssl.py @@ -15,6 +15,7 @@ import os import sys +import ssl import os.path import socket @@ -22,7 +23,7 @@ from mock import patch import libcloud.security - +from libcloud.security import get_ssl_version from libcloud.utils.py3 import reload from libcloud.httplib_ssl import LibcloudHTTPSConnection @@ -142,6 +143,44 @@ def test_connect_throws_friendly_error_message_on_ssl_wrap_connection_reset_by_p self.assertEqual(e.errno, 104) self.assertTrue(expected_msg in str(e)) + def test_get_ssl_version(self): + # User provided value should always have precedence over dynamicaly + # retrieved values + libcloud.security.SSL_VERSION = 10 + + libcloud.security.PY2_post_279 = True + result = get_ssl_version() + self.assertEqual(result, 10) + + libcloud.security.PY3_post_34 = True + result = get_ssl_version() + self.assertEqual(result, 10) + + libcloud.security.PY2_post_279 = False + libcloud.security.PY3_post_34 = False + libcloud.security.SSL_VERSION = None + + # Python >= 2.7.9 should use PROTOCOL_SSLv23 + libcloud.security.PY2_post_279 = True + result = get_ssl_version() + self.assertEqual(result, ssl.PROTOCOL_SSLv23) + + libcloud.security.PY2_post_279 = False + libcloud.security.PY3_post_34 = False + + # Python >= 3.4 should use PROTOCOL_SSLv23 + libcloud.security.PY3_post_34 = True + result = get_ssl_version() + self.assertEqual(result, ssl.PROTOCOL_SSLv23) + + # Python < 2.7.9 and Python < 3.4, should use TLS_v1 + libcloud.security.PY2_post_279 = False + libcloud.security.PY3_post_34 = False + + # Python >= 3.4 should use PROTOCOL_SSLv23 + result = get_ssl_version() + self.assertEqual(result, ssl.PROTOCOL_TLSv1) + if __name__ == '__main__': sys.exit(unittest.main())