From 21b378c5e5fee253e532f64250a1b919f5955fd3 Mon Sep 17 00:00:00 2001 From: Alessio Caprari Date: Mon, 26 Jan 2015 16:26:24 +0100 Subject: [PATCH 1/2] Use ssl module from the standard library also on Python >= 2.7.9 --- riak/security.py | 86 +++++++++-------- riak/tests/test_security.py | 11 +-- riak/transports/http/__init__.py | 16 ++-- riak/transports/pbc/connection.py | 6 +- riak/transports/security.py | 152 +++++++++++++++--------------- 5 files changed, 140 insertions(+), 131 deletions(-) diff --git a/riak/security.py b/riak/security.py index 7da79ea7..542ff225 100644 --- a/riak/security.py +++ b/riak/security.py @@ -16,34 +16,25 @@ under the License. """ +import ssl import warnings -from six import PY2 from riak import RiakError from riak.util import str_to_long -OPENSSL_VERSION_101G = 268439679 -if PY2: +if hasattr(ssl, 'SSLContext'): + # For Python >= 2.7.9 and Python 3.x + USE_STDLIB_SSL = True +else: + # For Python 2.6 and <= 2.7.8 + USE_STDLIB_SSL = False + +if not USE_STDLIB_SSL: import OpenSSL.SSL from OpenSSL import crypto - sslver = OpenSSL.SSL.OPENSSL_VERSION_NUMBER - # Be sure to use at least OpenSSL 1.0.1g - if (sslver < OPENSSL_VERSION_101G) or \ - not hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'): - verstring = OpenSSL.SSL.SSLeay_version(OpenSSL.SSL.SSLEAY_VERSION) - msg = "Found {0} version, but expected at least OpenSSL 1.0.1g. " \ - "Security may not support TLS 1.2.".format(verstring) - warnings.warn(msg, UserWarning) - if hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'): - DEFAULT_TLS_VERSION = OpenSSL.SSL.TLSv1_2_METHOD - elif hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'): - DEFAULT_TLS_VERSION = OpenSSL.SSL.TLSv1_1_METHOD - elif hasattr(OpenSSL.SSL, 'TLSv1_METHOD'): - DEFAULT_TLS_VERSION = OpenSSL.SSL.TLSv1_METHOD - else: - DEFAULT_TLS_VERSION = OpenSSL.SSL.SSLv23_METHOD -else: - import ssl +OPENSSL_VERSION_101G = 268439679 +if hasattr(ssl, 'OPENSSL_VERSION_NUMBER'): + # For Python 2.7 and Python 3.x sslver = ssl.OPENSSL_VERSION_NUMBER # Be sure to use at least OpenSSL 1.0.1g if sslver < OPENSSL_VERSION_101G or \ @@ -61,6 +52,25 @@ else: DEFAULT_TLS_VERSION = ssl.PROTOCOL_SSLv23 +else: + # For Python 2.6 + sslver = OpenSSL.SSL.OPENSSL_VERSION_NUMBER + # Be sure to use at least OpenSSL 1.0.1g + if (sslver < OPENSSL_VERSION_101G) or \ + not hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'): + verstring = OpenSSL.SSL.SSLeay_version(OpenSSL.SSL.SSLEAY_VERSION) + msg = "Found {0} version, but expected at least OpenSSL 1.0.1g. " \ + "Security may not support TLS 1.2.".format(verstring) + warnings.warn(msg, UserWarning) + if hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'): + DEFAULT_TLS_VERSION = OpenSSL.SSL.TLSv1_2_METHOD + elif hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'): + DEFAULT_TLS_VERSION = OpenSSL.SSL.TLSv1_1_METHOD + elif hasattr(OpenSSL.SSL, 'TLSv1_METHOD'): + DEFAULT_TLS_VERSION = OpenSSL.SSL.TLSv1_METHOD + else: + DEFAULT_TLS_VERSION = OpenSSL.SSL.SSLv23_METHOD + class SecurityError(RiakError): """ @@ -197,7 +207,7 @@ def ssl_version(self): """ return self._ssl_version - if PY2: + if not USE_STDLIB_SSL: @property def pkey(self): """ @@ -266,20 +276,20 @@ def _has_credential(self, key): return (getattr(self, internal_key) is not None) or \ (getattr(self, internal_key + "_file") is not None) - def _check_revoked_cert(self, ssl_socket): - """ - Checks whether the server certificate on the passed socket has been - revoked by checking the CRL. + def _check_revoked_cert(self, ssl_socket): + """ + Checks whether the server certificate on the passed socket has been + revoked by checking the CRL. - :param ssl_socket: the SSL/TLS socket - :rtype: bool - :raises SecurityError: when the certificate has been revoked - """ - if not self._has_credential('crl'): - return True - - servcert = ssl_socket.get_peer_certificate() - servserial = servcert.get_serial_number() - for rev in self.crl.get_revoked(): - if servserial == str_to_long(rev.get_serial(), 16): - raise SecurityError("Server certificate has been revoked") + :param ssl_socket: the SSL/TLS socket + :rtype: bool + :raises SecurityError: when the certificate has been revoked + """ + if not self._has_credential('crl'): + return True + + servcert = ssl_socket.get_peer_certificate() + servserial = servcert.get_serial_number() + for rev in self.crl.get_revoked(): + if servserial == str_to_long(rev.get_serial(), 16): + raise SecurityError("Server certificate has been revoked") diff --git a/riak/tests/test_security.py b/riak/tests/test_security.py index b036a94b..ffcada84 100644 --- a/riak/tests/test_security.py +++ b/riak/tests/test_security.py @@ -17,8 +17,8 @@ under the License. """ -import platform -if platform.python_version() < '2.7': +import sys +if sys.version_info < (2, 7): unittest = __import__('unittest2') else: import unittest @@ -26,7 +26,6 @@ SECURITY_CACERT, SECURITY_KEY, SECURITY_CERT, SECURITY_REVOKED, \ SECURITY_CERT_USER, SECURITY_CERT_PASSWD, SECURITY_BAD_CERT from riak.security import SecurityCreds -from six import PY3 class SecurityTests(object): @@ -110,9 +109,9 @@ def test_security_revoked_cert(self): creds = SecurityCreds(username=SECURITY_USER, password=SECURITY_PASSWD, cacert_file=SECURITY_CACERT, crl_file=SECURITY_REVOKED) - # Curenly Python 3.x native CRL doesn't seem to work - # as advertised - if PY3: + # Currently Python >= 2.7.9 and Python 3.x native CRL doesn't seem to + # work as advertised + if sys.version_info >= (2, 7, 9): return client = self.create_client(credentials=creds) with self.assertRaises(Exception): diff --git a/riak/transports/http/__init__.py b/riak/transports/http/__init__.py index 1d073604..deb334a2 100644 --- a/riak/transports/http/__init__.py +++ b/riak/transports/http/__init__.py @@ -19,16 +19,21 @@ import socket import select from six import PY2 -if PY2: +from riak.security import SecurityError, USE_STDLIB_SSL +if USE_STDLIB_SSL: + import ssl + from riak.transports.security import configure_ssl_context +else: import OpenSSL.SSL + from riak.transports.security import RiakWrappedSocket,\ + configure_pyopenssl_context +if PY2: from httplib import HTTPConnection, \ NotConnected, \ IncompleteRead, \ ImproperConnectionState, \ BadStatusLine, \ HTTPSConnection - from riak.transports.security import RiakWrappedSocket,\ - configure_pyopenssl_context else: from http.client import HTTPConnection, \ HTTPSConnection, \ @@ -36,10 +41,7 @@ IncompleteRead, \ ImproperConnectionState, \ BadStatusLine - import ssl - from riak.transports.security import configure_ssl_context -from riak.security import SecurityError from riak.transports.pool import Pool from riak.transports.http.transport import RiakHttpTransport @@ -106,7 +108,7 @@ def connect(self): Connect to a host on a given (SSL) port using PyOpenSSL. """ sock = socket.create_connection((self.host, self.port), self.timeout) - if PY2: + if not USE_STDLIB_SSL: ssl_ctx = configure_pyopenssl_context(self.credentials) # attempt to upgrade the socket to TLS diff --git a/riak/transports/pbc/connection.py b/riak/transports/pbc/connection.py index 6f4ee95a..293d05c3 100644 --- a/riak/transports/pbc/connection.py +++ b/riak/transports/pbc/connection.py @@ -19,7 +19,7 @@ import socket import struct import riak_pb -from riak.security import SecurityError +from riak.security import SecurityError, USE_STDLIB_SSL from riak import RiakError from riak_pb.messages import ( MESSAGE_CLASSES, @@ -30,7 +30,7 @@ ) from riak.util import bytes_to_str, str_to_bytes from six import PY2 -if PY2: +if not USE_STDLIB_SSL: from OpenSSL.SSL import Connection from riak.transports.security import configure_pyopenssl_context else: @@ -113,7 +113,7 @@ def _auth(self): else: return False - if PY2: + if not USE_STDLIB_SSL: def _ssl_handshake(self): """ Perform an SSL handshake w/ the server. diff --git a/riak/transports/security.py b/riak/transports/security.py index 8a098449..b108e427 100644 --- a/riak/transports/security.py +++ b/riak/transports/security.py @@ -17,16 +17,15 @@ """ import socket -from six import PY2 -if PY2: +from riak.security import SecurityError, USE_STDLIB_SSL +if USE_STDLIB_SSL: + import ssl +else: import OpenSSL.SSL try: from cStringIO import StringIO except ImportError: from StringIO import StringIO -else: - import ssl -from riak.security import SecurityError def verify_cb(conn, cert, errnum, depth, ok): @@ -39,42 +38,10 @@ def verify_cb(conn, cert, errnum, depth, ok): return ok -if PY2: - def configure_pyopenssl_context(credentials): - """ - Set various options on the SSL context for Python 2.x. - - :param credentials: Riak Security Credentials - :type credentials: :class:`~riak.security.SecurityCreds` - :rtype ssl_ctx: :class:`~OpenSSL.SSL.Context` - """ - - ssl_ctx = OpenSSL.SSL.Context(credentials.ssl_version) - if credentials._has_credential('pkey'): - ssl_ctx.use_privatekey(credentials.pkey) - if credentials._has_credential('cert'): - ssl_ctx.use_certificate(credentials.cert) - if credentials._has_credential('cacert'): - store = ssl_ctx.get_cert_store() - cacerts = credentials.cacert - if not isinstance(cacerts, list): - cacerts = [cacerts] - for cacert in cacerts: - store.add_cert(cacert) - else: - raise SecurityError("cacert_file is required in SecurityCreds") - ciphers = credentials.ciphers - if ciphers is not None: - ssl_ctx.set_cipher_list(ciphers) - # Demand a certificate - ssl_ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | - OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, - verify_cb) - return ssl_ctx -else: +if USE_STDLIB_SSL: def configure_ssl_context(credentials): """ - Set various options on the SSL context for Python 3.x. + Set various options on the SSL context for Python >= 2.7.9 and 3.x. N.B. versions earlier than 3.4 may not support all security measures, e.g., hostname check. @@ -121,49 +88,80 @@ def configure_ssl_context(credentials): return ssl_ctx - -# Inspired by -# https://github.com/shazow/urllib3/blob/master/urllib3/contrib/pyopenssl.py -class RiakWrappedSocket(socket.socket): - def __init__(self, connection, socket): +else: + def configure_pyopenssl_context(credentials): """ - API-compatibility wrapper for Python OpenSSL's Connection-class. + Set various options on the SSL context for Python <= 2.7.8. - :param connection: OpenSSL connection - :type connection: OpenSSL.SSL.Connection - :param socket: Underlying already connected socket - :type socket: socket + :param credentials: Riak Security Credentials + :type credentials: :class:`~riak.security.SecurityCreds` + :rtype ssl_ctx: :class:`~OpenSSL.SSL.Context` """ - self.connection = connection - self.socket = socket - - def fileno(self): - return self.socket.fileno() - - def makefile(self, mode, bufsize=-1): - return fileobject(self.connection, mode, bufsize) - - def settimeout(self, timeout): - return self.socket.settimeout(timeout) - - def sendall(self, data): - # SSL seems to need bytes, so force the data to byte encoding - return self.connection.sendall(bytes(data)) - - def close(self): - try: - return self.connection.shutdown() - except OpenSSL.SSL.Error as err: - if err.args == ([],): - return False - else: - raise err + ssl_ctx = OpenSSL.SSL.Context(credentials.ssl_version) + if credentials._has_credential('pkey'): + ssl_ctx.use_privatekey(credentials.pkey) + if credentials._has_credential('cert'): + ssl_ctx.use_certificate(credentials.cert) + if credentials._has_credential('cacert'): + store = ssl_ctx.get_cert_store() + cacerts = credentials.cacert + if not isinstance(cacerts, list): + cacerts = [cacerts] + for cacert in cacerts: + store.add_cert(cacert) + else: + raise SecurityError("cacert_file is required in SecurityCreds") + ciphers = credentials.ciphers + if ciphers is not None: + ssl_ctx.set_cipher_list(ciphers) + # Demand a certificate + ssl_ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, + verify_cb) + return ssl_ctx -# Blatantly Stolen from -# https://github.com/shazow/urllib3/blob/master/urllib3/contrib/pyopenssl.py -# which is basically a port of the `socket._fileobject` class -if PY2: + # Inspired by + # https://github.com/shazow/urllib3/blob/master/urllib3/contrib/pyopenssl.py + class RiakWrappedSocket(socket.socket): + def __init__(self, connection, socket): + """ + API-compatibility wrapper for Python OpenSSL's Connection-class. + + :param connection: OpenSSL connection + :type connection: OpenSSL.SSL.Connection + :param socket: Underlying already connected socket + :type socket: socket + """ + self.connection = connection + self.socket = socket + + def fileno(self): + return self.socket.fileno() + + def makefile(self, mode, bufsize=-1): + return fileobject(self.connection, mode, bufsize) + + def settimeout(self, timeout): + return self.socket.settimeout(timeout) + + def sendall(self, data): + # SSL seems to need bytes, so force the data to byte encoding + return self.connection.sendall(bytes(data)) + + def close(self): + try: + return self.connection.shutdown() + except OpenSSL.SSL.Error as err: + if err.args == ([],): + return False + else: + raise err + + + # Blatantly Stolen from + # https://github.com/shazow/urllib3/blob/master/urllib3/contrib/pyopenssl.py + # which is basically a port of the `socket._fileobject` class class fileobject(socket._fileobject): """ Extension of the socket module's fileobject to use PyOpenSSL. From 8a127a6b28ca54864e4d5a0d3c4957ad811bc83a Mon Sep 17 00:00:00 2001 From: Alessio Caprari Date: Mon, 26 Jan 2015 16:57:06 +0100 Subject: [PATCH 2/2] Include dependency on pyOpenSSL only on Python < 2.7.9 --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 09093ea8..549f2799 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -import platform +import sys from setuptools import setup, find_packages from version import get_version from commands import preconfigure, configure, create_bucket_types, \ @@ -7,16 +7,17 @@ install_requires = ['six >= 1.8.0'] requires = ['six(>=1.8.0)'] -if platform.python_version() < '3.0': +if sys.version_info < (2, 7, 9): install_requires.append("pyOpenSSL >= 0.14") requires.append("pyOpenSSL(>=0.14)") +if sys.version_info < (3, ): install_requires.append("riak_pb >=2.0.0") requires.append("riak_pb(>=2.0.0)") else: install_requires.append("python3_riak_pb >=2.0.0") requires.append("python3_riak_pb(>=2.0.0)") tests_require = [] -if platform.python_version() < '2.7': +if sys.version_info < (2, 7): tests_require.append("unittest2") setup(