# ------------------------------------------------------------------
|
# Copyright (c) 2020 PyInstaller Development Team.
|
#
|
# This file is distributed under the terms of the GNU General Public
|
# License (version 2.0 or later).
|
#
|
# The full license is available in LICENSE, distributed with
|
# this software.
|
#
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
# ------------------------------------------------------------------
|
"""
|
Hook for cryptography module from the Python Cryptography Authority.
|
"""
|
|
import os
|
import glob
|
import pathlib
|
|
from PyInstaller import compat
|
from PyInstaller import isolated
|
from PyInstaller.utils.hooks import (
|
collect_submodules,
|
copy_metadata,
|
get_module_file_attribute,
|
is_module_satisfies,
|
logger,
|
)
|
|
# get the package data so we can load the backends
|
datas = copy_metadata('cryptography')
|
|
# Add the backends as hidden imports
|
hiddenimports = collect_submodules('cryptography.hazmat.backends')
|
|
# Add the OpenSSL FFI binding modules as hidden imports
|
hiddenimports += collect_submodules('cryptography.hazmat.bindings.openssl') + ['_cffi_backend']
|
|
|
# Include the cffi extensions as binaries in a subfolder named like the package.
|
# The cffi verifier expects to find them inside the package directory for
|
# the main module. We cannot use hiddenimports because that would add the modules
|
# outside the package.
|
# NOTE: this is not true anymore with PyInstaller >= 6.0, but we keep it like this for compatibility with 5.x series.
|
binaries = []
|
cryptography_dir = os.path.dirname(get_module_file_attribute('cryptography'))
|
for ext in compat.EXTENSION_SUFFIXES:
|
ffimods = glob.glob(os.path.join(cryptography_dir, '*_cffi_*%s*' % ext))
|
for f in ffimods:
|
binaries.append((f, 'cryptography'))
|
|
|
# Check if `cryptography` is dynamically linked against OpenSSL >= 3.0.0. In that case, we might need to collect
|
# external OpenSSL modules, if OpenSSL was built with modules support. It seems the best indication of this is the
|
# presence of `ossl-modules` directory next to the OpenSSL shared library.
|
#
|
# NOTE: PyPI wheels ship with extensions statically linked against OpenSSL, so this is mostly catering alternative
|
# installation methods (Anaconda on all OSes, Homebrew on macOS, various linux distributions).
|
try:
|
@isolated.decorate
|
def _check_cryptography_openssl3():
|
# Check if OpenSSL 3 is used.
|
from cryptography.hazmat.backends.openssl.backend import backend
|
openssl_version = backend.openssl_version_number()
|
if openssl_version < 0x30000000:
|
return False, None
|
|
# Obtain path to the bindings module for binary dependency analysis. Under older versions of cryptography,
|
# this was a separate `_openssl` module; in contemporary versions, it is `_rust` module.
|
try:
|
import cryptography.hazmat.bindings._openssl as bindings_module
|
except ImportError:
|
import cryptography.hazmat.bindings._rust as bindings_module
|
|
return True, str(bindings_module.__file__)
|
|
uses_openssl3, bindings_module = _check_cryptography_openssl3()
|
except Exception:
|
logger.warning(
|
"hook-cryptography: failed to determine whether cryptography is using OpenSSL >= 3.0.0", exc_info=True
|
)
|
uses_openssl3, bindings_module = False, None
|
|
if uses_openssl3:
|
# Determine location of OpenSSL shared library, provided that extension module is dynamically linked against it.
|
# This requires the new PyInstaller.bindepend API from PyInstaller >= 6.0.
|
openssl_lib = None
|
if is_module_satisfies("PyInstaller >= 6.0"):
|
from PyInstaller.depend import bindepend
|
|
if compat.is_win:
|
SSL_LIB_NAME = 'libssl-3-x64.dll' if compat.is_64bits else 'libssl-3.dll'
|
elif compat.is_darwin:
|
SSL_LIB_NAME = 'libssl.3.dylib'
|
else:
|
SSL_LIB_NAME = 'libssl.so.3'
|
|
linked_libs = bindepend.get_imports(bindings_module)
|
openssl_lib = [
|
# Compare the basename of lib_name, because lib_fullpath is None if we fail to resolve the library.
|
lib_fullpath for lib_name, lib_fullpath in linked_libs if os.path.basename(lib_name) == SSL_LIB_NAME
|
]
|
openssl_lib = openssl_lib[0] if openssl_lib else None
|
else:
|
logger.warning(
|
"hook-cryptography: full support for cryptography + OpenSSL >= 3.0.0 requires PyInstaller >= 6.0"
|
)
|
|
# Check for presence of ossl-modules directory next to the OpenSSL shared library.
|
if openssl_lib:
|
logger.info("hook-cryptography: cryptography uses dynamically-linked OpenSSL: %r", openssl_lib)
|
|
openssl_lib_dir = pathlib.Path(openssl_lib).parent
|
|
# Collect whole ossl-modules directory, if it exists.
|
ossl_modules_dir = openssl_lib_dir / 'ossl-modules'
|
|
# Msys2/MinGW installations on Windows put the shared library into `bin` directory, but the modules are
|
# located in `lib` directory. Account for that possibility.
|
if not ossl_modules_dir.is_dir() and openssl_lib_dir.name == 'bin':
|
ossl_modules_dir = openssl_lib_dir.parent / 'lib' / 'ossl-modules'
|
|
# On Alpine linux, the true location of shared library is /lib directory, but the modules' directory is located
|
# in /usr/lib instead. Account for that possibility.
|
if not ossl_modules_dir.is_dir() and openssl_lib_dir == pathlib.Path('/lib'):
|
ossl_modules_dir = pathlib.Path('/usr/lib/ossl-modules')
|
|
if ossl_modules_dir.is_dir():
|
logger.debug("hook-cryptography: collecting OpenSSL modules directory: %r", str(ossl_modules_dir))
|
binaries.append((str(ossl_modules_dir), 'ossl-modules'))
|
else:
|
logger.info("hook-cryptography: cryptography does not seem to be using dynamically linked OpenSSL.")
|