Merge pull request #492 from public/py-thread-safe

Python implementation of OpenSSL locking callback
diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py
index b5116be..d8d4669 100644
--- a/cryptography/hazmat/backends/openssl/backend.py
+++ b/cryptography/hazmat/backends/openssl/backend.py
@@ -46,6 +46,8 @@
         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 1c17a5b..cde3bdb 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 @@
         "x509v3",
     ]
 
+    _locks = None
+    _lock_cb_handle = None
+    _lock_init_lock = threading.Lock()
+
     ffi = None
     lib = None
 
@@ -95,3 +100,43 @@
     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 40d91bf..81d13b7 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_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_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 @@
 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 373fe47..557f8c4 100644
--- a/docs/hazmat/bindings/openssl.rst
+++ b/docs/hazmat/bindings/openssl.rst
@@ -22,6 +22,26 @@
         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 d1e8505..35eb7e8 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 @@
 
     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()