diff options
-rw-r--r-- | cryptography/hazmat/backends/openssl/backend.py | 2 | ||||
-rw-r--r-- | cryptography/hazmat/bindings/openssl/binding.py | 45 | ||||
-rw-r--r-- | cryptography/hazmat/bindings/openssl/crypto.py | 7 | ||||
-rw-r--r-- | docs/hazmat/bindings/openssl.rst | 20 | ||||
-rw-r--r-- | tests/hazmat/bindings/test_openssl.py | 73 |
5 files changed, 146 insertions, 1 deletions
diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py index b5116be4..d8d4669a 100644 --- a/cryptography/hazmat/backends/openssl/backend.py +++ b/cryptography/hazmat/backends/openssl/backend.py @@ -46,6 +46,8 @@ class Backend(object): self._ffi = self._binding.ffi self._lib = self._binding.lib + self._binding.init_static_locks() + # adds all ciphers/digests for EVP self._lib.OpenSSL_add_all_algorithms() # registers available SSL/TLS ciphers and digests diff --git a/cryptography/hazmat/bindings/openssl/binding.py b/cryptography/hazmat/bindings/openssl/binding.py index 1c17a5b2..cde3bdbd 100644 --- a/cryptography/hazmat/bindings/openssl/binding.py +++ b/cryptography/hazmat/bindings/openssl/binding.py @@ -14,6 +14,7 @@ from __future__ import absolute_import, division, print_function import sys +import threading from cryptography.hazmat.bindings.utils import build_ffi @@ -70,6 +71,10 @@ class Binding(object): "x509v3", ] + _locks = None + _lock_cb_handle = None + _lock_init_lock = threading.Lock() + ffi = None lib = None @@ -95,3 +100,43 @@ class Binding(object): def is_available(cls): # OpenSSL is the only binding so for now it must always be available return True + + @classmethod + def init_static_locks(cls): + with cls._lock_init_lock: + cls._ensure_ffi_initialized() + + if not cls._lock_cb_handle: + cls._lock_cb_handle = cls.ffi.callback( + "void(int, int, const char *, int)", + cls._lock_cb + ) + + # use Python's implementation if available + + __import__("_ssl") + + if cls.lib.CRYPTO_get_locking_callback() != cls.ffi.NULL: + return + + # otherwise setup our version + + num_locks = cls.lib.CRYPTO_num_locks() + cls._locks = [threading.Lock() for n in range(num_locks)] + + cls.lib.CRYPTO_set_locking_callback(cls._lock_cb_handle) + + @classmethod + def _lock_cb(cls, mode, n, file, line): + lock = cls._locks[n] + + if mode & cls.lib.CRYPTO_LOCK: + lock.acquire() + elif mode & cls.lib.CRYPTO_UNLOCK: + lock.release() + else: + raise RuntimeError( + "Unknown lock mode {0}: lock={1}, file={2}, line={3}".format( + mode, n, file, line + ) + ) diff --git a/cryptography/hazmat/bindings/openssl/crypto.py b/cryptography/hazmat/bindings/openssl/crypto.py index 40d91bf2..81d13b73 100644 --- a/cryptography/hazmat/bindings/openssl/crypto.py +++ b/cryptography/hazmat/bindings/openssl/crypto.py @@ -27,6 +27,11 @@ static const int CRYPTO_MEM_CHECK_ON; static const int CRYPTO_MEM_CHECK_OFF; static const int CRYPTO_MEM_CHECK_ENABLE; static const int CRYPTO_MEM_CHECK_DISABLE; +static const int CRYPTO_LOCK; +static const int CRYPTO_UNLOCK; +static const int CRYPTO_READ; +static const int CRYPTO_WRITE; +static const int CRYPTO_LOCK_SSL; """ FUNCTIONS = """ @@ -43,6 +48,7 @@ void CRYPTO_set_locking_callback(void(*)(int, int, const char *, int)); void CRYPTO_set_id_callback(unsigned long (*)(void)); unsigned long (*CRYPTO_get_id_callback(void))(void); void (*CRYPTO_get_locking_callback(void))(int, int, const char *, int); +void CRYPTO_lock(int, int, const char *, int); void OPENSSL_free(void *); """ @@ -51,7 +57,6 @@ MACROS = """ void CRYPTO_add(int *, int, int); void CRYPTO_malloc_init(void); void CRYPTO_malloc_debug_init(void); - """ CUSTOMIZATIONS = """ diff --git a/docs/hazmat/bindings/openssl.rst b/docs/hazmat/bindings/openssl.rst index 373fe472..557f8c4d 100644 --- a/docs/hazmat/bindings/openssl.rst +++ b/docs/hazmat/bindings/openssl.rst @@ -22,6 +22,26 @@ These are `CFFI`_ bindings to the `OpenSSL`_ C library. This is a ``cffi`` library. It can be used to call OpenSSL functions, and access constants. + .. classmethod:: init_static_locks + + Enables the best available locking callback for OpenSSL. + See :ref:`openssl-threading`. + +.. _openssl-threading: + +Threading +--------- + +``cryptography`` enables OpenSSLs `thread safety facilities`_ in two different +ways depending on the configuration of your system. Normally the locking +callbacks provided by your Python implementation specifically for OpenSSL will +be used. However if you have linked ``cryptography`` to a different version of +OpenSSL than that used by your Python implementation we enable an alternative +locking callback. This version is implemented in Python and so may result in +lower performance in some situations. In particular parallelism is reduced +because it has to acquire the GIL whenever any lock operations occur within +OpenSSL. .. _`CFFI`: https://cffi.readthedocs.org/ .. _`OpenSSL`: https://www.openssl.org/ +.. _`thread safety facilities`: http://www.openssl.org/docs/crypto/threads.html diff --git a/tests/hazmat/bindings/test_openssl.py b/tests/hazmat/bindings/test_openssl.py index d1e85058..35eb7e8d 100644 --- a/tests/hazmat/bindings/test_openssl.py +++ b/tests/hazmat/bindings/test_openssl.py @@ -11,6 +11,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + from cryptography.hazmat.bindings.openssl.binding import Binding @@ -23,3 +25,74 @@ class TestOpenSSL(object): def test_is_available(self): assert Binding.is_available() is True + + def test_crypto_lock_init(self): + b = Binding() + b.init_static_locks() + lock_cb = b.lib.CRYPTO_get_locking_callback() + assert lock_cb != b.ffi.NULL + + def _skip_if_not_fallback_lock(self, b): + # only run this test if we are using our locking cb + original_cb = b.lib.CRYPTO_get_locking_callback() + if original_cb != b._lock_cb_handle: + pytest.skip( + "Not using the fallback Python locking callback " + "implementation. Probably because import _ssl set one" + ) + + def test_fallback_crypto_lock_via_openssl_api(self): + b = Binding() + b.init_static_locks() + + self._skip_if_not_fallback_lock(b) + + # check that the lock state changes appropriately + lock = b._locks[b.lib.CRYPTO_LOCK_SSL] + + # starts out unlocked + assert lock.acquire(False) + lock.release() + + b.lib.CRYPTO_lock( + b.lib.CRYPTO_LOCK | b.lib.CRYPTO_READ, + b.lib.CRYPTO_LOCK_SSL, b.ffi.NULL, 0 + ) + + # becomes locked + assert not lock.acquire(False) + + b.lib.CRYPTO_lock( + b.lib.CRYPTO_UNLOCK | b.lib.CRYPTO_READ, + b.lib.CRYPTO_LOCK_SSL, b.ffi.NULL, 0 + ) + + # then unlocked + assert lock.acquire(False) + lock.release() + + def test_fallback_crypto_lock_via_binding_api(self): + b = Binding() + b.init_static_locks() + + self._skip_if_not_fallback_lock(b) + + lock = b._locks[b.lib.CRYPTO_LOCK_SSL] + + with pytest.raises(RuntimeError): + b._lock_cb(0, b.lib.CRYPTO_LOCK_SSL, "<test>", 1) + + # errors shouldnt cause locking + assert lock.acquire(False) + lock.release() + + b._lock_cb(b.lib.CRYPTO_LOCK | b.lib.CRYPTO_READ, + b.lib.CRYPTO_LOCK_SSL, "<test>", 1) + # locked + assert not lock.acquire(False) + + b._lock_cb(b.lib.CRYPTO_UNLOCK | b.lib.CRYPTO_READ, + b.lib.CRYPTO_LOCK_SSL, "<test>", 1) + # unlocked + assert lock.acquire(False) + lock.release() |