Repository: pyca/cryptography

Issue:
Encode package version as a shared object symbol
One of the more common issues we see with users is an `AttributeError` where one of our `Cryptograpy_HAS_*` can't be found. This is pretty much always an issue where Python has loaded a newer version of cryptography's Python code, but for some reason loads an older shared object (which does not have the symbol the Python code is looking for).

This is a bad error message for a serious environment problem, and it doesn't always happen immediately. We should encode the version of cryptography in the shared object we build and then raise an `ImportError` on import if the shared object version and cryptography's version do not match.

refs https://github.com/certbot/certbot/issues/5651


Discussion:

Resolving patch:
diff --git a/src/_cffi_src/utils.py b/src/_cffi_src/utils.py
--- a/src/_cffi_src/utils.py
+++ b/src/_cffi_src/utils.py
@@ -4,6 +4,7 @@
 
 from __future__ import absolute_import, division, print_function
 
+import os
 import sys
 from distutils.ccompiler import new_compiler
 from distutils.dist import Distribution
@@ -11,6 +12,13 @@
 from cffi import FFI
 
 
+# Load the cryptography __about__ to get the current package version
+base_src = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+about = {}
+with open(os.path.join(base_src, "cryptography", "__about__.py")) as f:
+    exec(f.read(), about)
+
+
 def build_ffi_for_binding(module_name, module_prefix, modules, libraries=[],
                           extra_compile_args=[], extra_link_args=[]):
     """
@@ -55,6 +63,11 @@ def build_ffi_for_binding(module_name, module_prefix, modules, libraries=[],
 def build_ffi(module_name, cdef_source, verify_source, libraries=[],
               extra_compile_args=[], extra_link_args=[]):
     ffi = FFI()
+    # Always add the CRYPTOGRAPHY_PACKAGE_VERSION to the shared object
+    cdef_source += "\nstatic const char *const CRYPTOGRAPHY_PACKAGE_VERSION;"
+    verify_source += '\n#define CRYPTOGRAPHY_PACKAGE_VERSION "{}"'.format(
+        about["__version__"]
+    )
     ffi.cdef(cdef_source)
     ffi.set_source(
         module_name,
diff --git a/src/cryptography/hazmat/bindings/openssl/binding.py b/src/cryptography/hazmat/bindings/openssl/binding.py
--- a/src/cryptography/hazmat/bindings/openssl/binding.py
+++ b/src/cryptography/hazmat/bindings/openssl/binding.py
@@ -9,6 +9,7 @@
 import types
 import warnings
 
+import cryptography
 from cryptography import utils
 from cryptography.exceptions import InternalError
 from cryptography.hazmat.bindings._openssl import ffi, lib
@@ -164,6 +165,29 @@ def _verify_openssl_version(lib):
         )
 
 
+def _verify_package_version(version):
+    # Occasionally we run into situations where the version of the Python
+    # package does not match the version of the shared object that is loaded.
+    # This may occur in environments where multiple versions of cryptography
+    # are installed and available in the python path. To avoid errors cropping
+    # up later this code checks that the currently imported package and the
+    # shared object that were loaded have the same version and raise an
+    # ImportError if they do not
+    so_package_version = ffi.string(lib.CRYPTOGRAPHY_PACKAGE_VERSION)
+    if version.encode("ascii") != so_package_version:
+        raise ImportError(
+            "The version of cryptography does not match the loaded "
+            "shared object. This can happen if you have multiple copies of "
+            "cryptography installed in your Python path. Please try creating "
+            "a new virtual environment to resolve this issue. "
+            "Loaded python version: {}, shared object version: {}".format(
+                version, so_package_version
+            )
+        )
+
+
+_verify_package_version(cryptography.__version__)
+
 # OpenSSL is not thread safe until the locks are initialized. We call this
 # method in module scope so that it executes with the import lock. On
 # Pythons < 3.4 this import lock is a global lock, which can prevent a race


Test patch:
diff --git a/tests/hazmat/bindings/test_openssl.py b/tests/hazmat/bindings/test_openssl.py
--- a/tests/hazmat/bindings/test_openssl.py
+++ b/tests/hazmat/bindings/test_openssl.py
@@ -8,7 +8,7 @@
 
 from cryptography.exceptions import InternalError
 from cryptography.hazmat.bindings.openssl.binding import (
-    Binding, _consume_errors, _openssl_assert
+    Binding, _consume_errors, _openssl_assert, _verify_package_version
 )
 
 
@@ -118,3 +118,7 @@ def test_check_startup_errors_are_allowed(self):
         )
         b._register_osrandom_engine()
         assert _consume_errors(b.lib) == []
+
+    def test_version_mismatch(self):
+        with pytest.raises(ImportError):
+            _verify_package_version("nottherightversion")