aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cryptography/hazmat/backends/openssl/backend.py2
-rw-r--r--cryptography/hazmat/bindings/openssl/binding.py45
-rw-r--r--cryptography/hazmat/bindings/openssl/crypto.py7
-rw-r--r--docs/hazmat/bindings/openssl.rst20
-rw-r--r--tests/hazmat/bindings/test_openssl.py73
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()