| import contextlib | |
| import enum | |
| import functools | |
| import importlib | |
| import inspect | |
| import unittest | |
| import unittest.mock | |
| from test.support import import_helper | |
| def _parse_fullname(fullname, *, strict=False): | |
| """Parse a fully-qualified name ``<module_name>.<member_name>``. | |
| The ``module_name`` component contains one or more dots. | |
| The ``member_name`` component does not contain any dot. | |
| If *strict* is true, *fullname* must be a string. Otherwise, | |
| it can be None, and, ``module_name`` and ``member_name`` will | |
| also be None. | |
| """ | |
| if fullname is None: | |
| assert not strict | |
| return None, None | |
| assert isinstance(fullname, str), fullname | |
| assert fullname.count(".") >= 1, fullname | |
| module_name, member_name = fullname.rsplit(".", maxsplit=1) | |
| return module_name, member_name | |
| def _import_module(module_name, *, strict=False): | |
| """Import a module from its fully-qualified name. | |
| If *strict* is false, import failures are suppressed and None is returned. | |
| """ | |
| if module_name is None: | |
| # To prevent a TypeError in importlib.import_module | |
| if strict: | |
| raise ImportError("no module to import") | |
| return None | |
| try: | |
| return importlib.import_module(module_name) | |
| except ImportError as exc: | |
| if strict: | |
| raise exc | |
| return None | |
| def _import_member(module_name, member_name, *, strict=False): | |
| """Import a member from a module. | |
| If *strict* is false, import failures are suppressed and None is returned. | |
| """ | |
| if member_name is None: | |
| if strict: | |
| raise ImportError(f"no member to import from {module_name}") | |
| return None | |
| module = _import_module(module_name, strict=strict) | |
| if strict: | |
| return getattr(module, member_name) | |
| return getattr(module, member_name, None) | |
| class Implementation(enum.StrEnum): | |
| # Indicate that the hash function is implemented by a built-in module. | |
| builtin = enum.auto() | |
| # Indicate that the hash function is implemented by OpenSSL. | |
| openssl = enum.auto() | |
| # Indicate that the hash function is provided through the public API. | |
| hashlib = enum.auto() | |
| class _HashId(enum.StrEnum): | |
| """Enumeration containing the canonical digest names. | |
| Those names should only be used by hashlib.new() or hmac.new(). | |
| Their support by _hashlib.new() is not necessarily guaranteed. | |
| """ | |
| md5 = enum.auto() | |
| sha1 = enum.auto() | |
| sha224 = enum.auto() | |
| sha256 = enum.auto() | |
| sha384 = enum.auto() | |
| sha512 = enum.auto() | |
| sha3_224 = enum.auto() | |
| sha3_256 = enum.auto() | |
| sha3_384 = enum.auto() | |
| sha3_512 = enum.auto() | |
| shake_128 = enum.auto() | |
| shake_256 = enum.auto() | |
| blake2s = enum.auto() | |
| blake2b = enum.auto() | |
| def __repr__(self): | |
| return str(self) | |
| @property | |
| def is_xof(self): | |
| """Indicate whether the hash is an extendable-output hash function.""" | |
| return self.startswith("shake_") | |
| @property | |
| def is_keyed(self): | |
| """Indicate whether the hash is a keyed hash function.""" | |
| return self.startswith("blake2") | |
| CANONICAL_DIGEST_NAMES = frozenset(map(str, _HashId.__members__)) | |
| NON_HMAC_DIGEST_NAMES = frozenset(( | |
| _HashId.shake_128, _HashId.shake_256, | |
| _HashId.blake2s, _HashId.blake2b, | |
| )) | |
| class _HashInfoItem: | |
| """Interface for interacting with a named object. | |
| The object is entirely described by its fully-qualified *fullname*. | |
| *fullname* must be None or a string "<module_name>.<member_name>". | |
| """ | |
| def __init__(self, fullname=None, *, strict=False): | |
| module_name, member_name = _parse_fullname(fullname, strict=strict) | |
| self.fullname = fullname | |
| self.module_name = module_name | |
| self.member_name = member_name | |
| def import_module(self, *, strict=False): | |
| """Import the described module. | |
| If *strict* is true, an ImportError may be raised if importing fails, | |
| otherwise, None is returned on error. | |
| """ | |
| return _import_module(self.module_name, strict=strict) | |
| def import_member(self, *, strict=False): | |
| """Import the described member. | |
| If *strict* is true, an AttributeError or an ImportError may be | |
| raised if importing fails; otherwise, None is returned on error. | |
| """ | |
| return _import_member( | |
| self.module_name, self.member_name, strict=strict | |
| ) | |
| class _HashInfoBase: | |
| """Base dataclass containing "backend" information. | |
| Subclasses may define an attribute named after one of the known | |
| implementations ("builtin", "openssl" or "hashlib") which stores | |
| an _HashInfoItem object. | |
| Those attributes can be retrieved through __getitem__(), e.g., | |
| ``info["builtin"]`` returns the _HashInfoItem corresponding to | |
| the builtin implementation. | |
| """ | |
| def __init__(self, canonical_name): | |
| assert isinstance(canonical_name, _HashId), canonical_name | |
| self.canonical_name = canonical_name | |
| def __getitem__(self, implementation): | |
| try: | |
| attrname = Implementation(implementation) | |
| except ValueError: | |
| raise self.invalid_implementation_error(implementation) from None | |
| try: | |
| provider = getattr(self, attrname) | |
| except AttributeError: | |
| raise self.invalid_implementation_error(implementation) from None | |
| if not isinstance(provider, _HashInfoItem): | |
| raise KeyError(implementation) | |
| return provider | |
| def invalid_implementation_error(self, implementation): | |
| msg = f"no implementation {implementation} for {self.canonical_name}" | |
| return AssertionError(msg) | |
| class _HashTypeInfo(_HashInfoBase): | |
| """Dataclass containing information for hash functions types. | |
| - *canonical_name* must be a _HashId. | |
| - *builtin* is the fully-qualified name for the builtin HACL* type, | |
| e.g., "_md5.MD5Type". | |
| - *openssl* is the fully-qualified name for the OpenSSL wrapper type, | |
| e.g., "_hashlib.HASH". | |
| """ | |
| def __init__(self, canonical_name, builtin, openssl): | |
| super().__init__(canonical_name) | |
| self.builtin = _HashInfoItem(builtin, strict=True) | |
| self.openssl = _HashInfoItem(openssl, strict=True) | |
| def fullname(self, implementation): | |
| """Get the fully qualified name of a given implementation. | |
| This returns a string of the form "MODULE_NAME.OBJECT_NAME" or None | |
| if the hash function does not have a corresponding implementation. | |
| *implementation* must be "builtin" or "openssl". | |
| """ | |
| return self[implementation].fullname | |
| def module_name(self, implementation): | |
| """Get the name of the module containing the hash object type.""" | |
| return self[implementation].module_name | |
| def object_type_name(self, implementation): | |
| """Get the name of the hash object class name.""" | |
| return self[implementation].member_name | |
| def import_module(self, implementation, *, allow_skip=False): | |
| """Import the module containing the hash object type. | |
| On error, return None if *allow_skip* is false, or raise SkipNoHash. | |
| """ | |
| target = self[implementation] | |
| module = target.import_module() | |
| if allow_skip and module is None: | |
| reason = f"cannot import module {target.module_name}" | |
| raise SkipNoHash(self.canonical_name, implementation, reason) | |
| return module | |
| def import_object_type(self, implementation, *, allow_skip=False): | |
| """Get the runtime hash object type. | |
| On error, return None if *allow_skip* is false, or raise SkipNoHash. | |
| """ | |
| target = self[implementation] | |
| member = target.import_member() | |
| if allow_skip and member is None: | |
| reason = f"cannot import class {target.fullname}" | |
| raise SkipNoHash(self.canonical_name, implementation, reason) | |
| return member | |
| class _HashFuncInfo(_HashInfoBase): | |
| """Dataclass containing information for hash functions constructors. | |
| - *canonical_name* must be a _HashId. | |
| - *builtin* is the fully-qualified name of the HACL* | |
| hash constructor function, e.g., "_md5.md5". | |
| - *openssl* is the fully-qualified name of the "_hashlib" method | |
| for the OpenSSL named constructor, e.g., "_hashlib.openssl_md5". | |
| - *hashlib* is the fully-qualified name of the "hashlib" method | |
| for the explicit named hash constructor, e.g., "hashlib.md5". | |
| """ | |
| def __init__(self, canonical_name, builtin, openssl=None, hashlib=None): | |
| super().__init__(canonical_name) | |
| self.builtin = _HashInfoItem(builtin, strict=True) | |
| self.openssl = _HashInfoItem(openssl, strict=False) | |
| self.hashlib = _HashInfoItem(hashlib, strict=False) | |
| def fullname(self, implementation): | |
| """Get the fully qualified name of a given implementation. | |
| This returns a string of the form "MODULE_NAME.METHOD_NAME" or None | |
| if the hash function does not have a corresponding implementation. | |
| *implementation* must be "builtin", "openssl" or "hashlib". | |
| """ | |
| return self[implementation].fullname | |
| def module_name(self, implementation): | |
| """Get the name of the constructor function module. | |
| The *implementation* must be "builtin", "openssl" or "hashlib". | |
| """ | |
| return self[implementation].module_name | |
| def method_name(self, implementation): | |
| """Get the name of the constructor function module method. | |
| Use fullname() to get the constructor function fully-qualified name. | |
| The *implementation* must be "builtin", "openssl" or "hashlib". | |
| """ | |
| return self[implementation].member_name | |
| class _HashInfo: | |
| """Dataclass containing information for supported hash functions. | |
| Attributes | |
| ---------- | |
| canonical_name : _HashId | |
| The hash function canonical name. | |
| type : _HashTypeInfo | |
| The hash object types information. | |
| func : _HashTypeInfo | |
| The hash object constructors information. | |
| """ | |
| def __init__( | |
| self, | |
| canonical_name, | |
| builtin_object_type_fullname, | |
| openssl_object_type_fullname, | |
| builtin_method_fullname, | |
| openssl_method_fullname=None, | |
| hashlib_method_fullname=None, | |
| ): | |
| """ | |
| - *canonical_name* must be a _HashId. | |
| - *builtin_object_type_fullname* is the fully-qualified name | |
| for the builtin HACL* type, e.g., "_md5.MD5Type". | |
| - *openssl_object_type_fullname* is the fully-qualified name | |
| for the OpenSSL wrapper type, e.g., "_hashlib.HASH". | |
| - *builtin_method_fullname* is the fully-qualified name | |
| of the HACL* hash constructor function, e.g., "_md5.md5". | |
| - *openssl_method_fullname* is the fully-qualified name | |
| of the "_hashlib" module method for the explicit OpenSSL | |
| hash constructor function, e.g., "_hashlib.openssl_md5". | |
| - *hashlib_method_fullname* is the fully-qualified name | |
| of the "hashlib" module method for the explicit hash | |
| constructor function, e.g., "hashlib.md5". | |
| """ | |
| assert isinstance(canonical_name, _HashId), canonical_name | |
| self.canonical_name = canonical_name | |
| self.type = _HashTypeInfo( | |
| canonical_name, | |
| builtin_object_type_fullname, | |
| openssl_object_type_fullname, | |
| ) | |
| self.func = _HashFuncInfo( | |
| canonical_name, | |
| builtin_method_fullname, | |
| openssl_method_fullname, | |
| hashlib_method_fullname, | |
| ) | |
| _HASHINFO_DATABASE = frozendict({ | |
| _HashId.md5: _HashInfo( | |
| _HashId.md5, | |
| "_md5.MD5Type", | |
| "_hashlib.HASH", | |
| "_md5.md5", | |
| "_hashlib.openssl_md5", | |
| "hashlib.md5", | |
| ), | |
| _HashId.sha1: _HashInfo( | |
| _HashId.sha1, | |
| "_sha1.SHA1Type", | |
| "_hashlib.HASH", | |
| "_sha1.sha1", | |
| "_hashlib.openssl_sha1", | |
| "hashlib.sha1", | |
| ), | |
| _HashId.sha224: _HashInfo( | |
| _HashId.sha224, | |
| "_sha2.SHA224Type", | |
| "_hashlib.HASH", | |
| "_sha2.sha224", | |
| "_hashlib.openssl_sha224", | |
| "hashlib.sha224", | |
| ), | |
| _HashId.sha256: _HashInfo( | |
| _HashId.sha256, | |
| "_sha2.SHA256Type", | |
| "_hashlib.HASH", | |
| "_sha2.sha256", | |
| "_hashlib.openssl_sha256", | |
| "hashlib.sha256", | |
| ), | |
| _HashId.sha384: _HashInfo( | |
| _HashId.sha384, | |
| "_sha2.SHA384Type", | |
| "_hashlib.HASH", | |
| "_sha2.sha384", | |
| "_hashlib.openssl_sha384", | |
| "hashlib.sha384", | |
| ), | |
| _HashId.sha512: _HashInfo( | |
| _HashId.sha512, | |
| "_sha2.SHA512Type", | |
| "_hashlib.HASH", | |
| "_sha2.sha512", | |
| "_hashlib.openssl_sha512", | |
| "hashlib.sha512", | |
| ), | |
| _HashId.sha3_224: _HashInfo( | |
| _HashId.sha3_224, | |
| "_sha3.sha3_224", | |
| "_hashlib.HASH", | |
| "_sha3.sha3_224", | |
| "_hashlib.openssl_sha3_224", | |
| "hashlib.sha3_224", | |
| ), | |
| _HashId.sha3_256: _HashInfo( | |
| _HashId.sha3_256, | |
| "_sha3.sha3_256", | |
| "_hashlib.HASH", | |
| "_sha3.sha3_256", | |
| "_hashlib.openssl_sha3_256", | |
| "hashlib.sha3_256", | |
| ), | |
| _HashId.sha3_384: _HashInfo( | |
| _HashId.sha3_384, | |
| "_sha3.sha3_384", | |
| "_hashlib.HASH", | |
| "_sha3.sha3_384", | |
| "_hashlib.openssl_sha3_384", | |
| "hashlib.sha3_384", | |
| ), | |
| _HashId.sha3_512: _HashInfo( | |
| _HashId.sha3_512, | |
| "_sha3.sha3_512", | |
| "_hashlib.HASH", | |
| "_sha3.sha3_512", | |
| "_hashlib.openssl_sha3_512", | |
| "hashlib.sha3_512", | |
| ), | |
| _HashId.shake_128: _HashInfo( | |
| _HashId.shake_128, | |
| "_sha3.shake_128", | |
| "_hashlib.HASHXOF", | |
| "_sha3.shake_128", | |
| "_hashlib.openssl_shake_128", | |
| "hashlib.shake_128", | |
| ), | |
| _HashId.shake_256: _HashInfo( | |
| _HashId.shake_256, | |
| "_sha3.shake_256", | |
| "_hashlib.HASHXOF", | |
| "_sha3.shake_256", | |
| "_hashlib.openssl_shake_256", | |
| "hashlib.shake_256", | |
| ), | |
| _HashId.blake2s: _HashInfo( | |
| _HashId.blake2s, | |
| "_blake2.blake2s", | |
| "_hashlib.HASH", | |
| "_blake2.blake2s", | |
| None, | |
| "hashlib.blake2s", | |
| ), | |
| _HashId.blake2b: _HashInfo( | |
| _HashId.blake2b, | |
| "_blake2.blake2b", | |
| "_hashlib.HASH", | |
| "_blake2.blake2b", | |
| None, | |
| "hashlib.blake2b", | |
| ), | |
| }) | |
| assert _HASHINFO_DATABASE.keys() == CANONICAL_DIGEST_NAMES | |
| def get_hash_type_info(name): | |
| info = _HASHINFO_DATABASE[name] | |
| assert isinstance(info, _HashInfo), info | |
| return info.type | |
| def get_hash_func_info(name): | |
| info = _HASHINFO_DATABASE[name] | |
| assert isinstance(info, _HashInfo), info | |
| return info.func | |
| def _iter_hash_func_info(excluded): | |
| for name, info in _HASHINFO_DATABASE.items(): | |
| if name not in excluded: | |
| yield info.func | |
| # Mapping from canonical hash names to their explicit HACL* HMAC constructor. | |
| # There is currently no OpenSSL one-shot named function and there will likely | |
| # be none in the future. | |
| _HMACINFO_DATABASE = { | |
| _HashId(canonical_name): _HashInfoItem(f"_hmac.compute_{canonical_name}") | |
| for canonical_name in CANONICAL_DIGEST_NAMES | |
| } | |
| # Neither HACL* nor OpenSSL supports HMAC over XOFs. | |
| _HMACINFO_DATABASE[_HashId.shake_128] = _HashInfoItem() | |
| _HMACINFO_DATABASE[_HashId.shake_256] = _HashInfoItem() | |
| # Strictly speaking, HMAC-BLAKE is meaningless as BLAKE2 is already a | |
| # keyed hash function. However, as it's exposed by HACL*, we test it. | |
| _HMACINFO_DATABASE[_HashId.blake2s] = _HashInfoItem('_hmac.compute_blake2s_32') | |
| _HMACINFO_DATABASE[_HashId.blake2b] = _HashInfoItem('_hmac.compute_blake2b_32') | |
| _HMACINFO_DATABASE = frozendict(_HMACINFO_DATABASE) | |
| assert _HMACINFO_DATABASE.keys() == CANONICAL_DIGEST_NAMES | |
| def get_hmac_item_info(name): | |
| info = _HMACINFO_DATABASE[name] | |
| assert isinstance(info, _HashInfoItem), info | |
| return info | |
| def _decorate_func_or_class(decorator_func, func_or_class): | |
| if not isinstance(func_or_class, type): | |
| return decorator_func(func_or_class) | |
| decorated_class = func_or_class | |
| setUpClass = decorated_class.__dict__.get('setUpClass') | |
| if setUpClass is None: | |
| def setUpClass(cls): | |
| super(decorated_class, cls).setUpClass() | |
| setUpClass.__qualname__ = decorated_class.__qualname__ + '.setUpClass' | |
| setUpClass.__module__ = decorated_class.__module__ | |
| else: | |
| setUpClass = setUpClass.__func__ | |
| setUpClass = classmethod(decorator_func(setUpClass)) | |
| decorated_class.setUpClass = setUpClass | |
| return decorated_class | |
| def _chain_decorators(decorators): | |
| """Obtain a decorator by chaining multiple decorators. | |
| The decorators are applied in the order they are given. | |
| """ | |
| def decorator_func(func): | |
| return functools.reduce(lambda w, deco: deco(w), decorators, func) | |
| return functools.partial(_decorate_func_or_class, decorator_func) | |
| def _ensure_wrapper_signature(wrapper, wrapped): | |
| """Ensure that a wrapper has the same signature as the wrapped function. | |
| This is used to guarantee that a TypeError raised due to a bad API call | |
| is raised consistently (using variadic signatures would hide such errors). | |
| """ | |
| try: | |
| wrapped_sig = inspect.signature(wrapped) | |
| except ValueError: # built-in signature cannot be found | |
| return | |
| wrapper_sig = inspect.signature(wrapper) | |
| if wrapped_sig != wrapper_sig: | |
| fullname = f"{wrapped.__module__}.{wrapped.__qualname__}" | |
| raise AssertionError( | |
| f"signature for {fullname}() is incorrect:\n" | |
| f" expect: {wrapped_sig}\n" | |
| f" actual: {wrapper_sig}" | |
| ) | |
| def _make_conditional_decorator(test, /, *test_args, **test_kwargs): | |
| def decorator_func(func): | |
| @functools.wraps(func) | |
| def wrapper(*args, **kwargs): | |
| test(*test_args, **test_kwargs) | |
| return func(*args, **kwargs) | |
| return wrapper | |
| return functools.partial(_decorate_func_or_class, decorator_func) | |
| def requires_openssl_hashlib(): | |
| _hashlib = _import_module("_hashlib") | |
| return unittest.skipIf(_hashlib is None, "requires _hashlib") | |
| def requires_builtin_hmac(): | |
| _hmac = _import_module("_hmac") | |
| return unittest.skipIf(_hmac is None, "requires _hmac") | |
| class SkipNoHash(unittest.SkipTest): | |
| """A SkipTest exception raised when a hash is not available.""" | |
| def __init__(self, digestname, implementation=None, reason=None): | |
| parts = ["missing", implementation, f"hash algorithm {digestname!r}"] | |
| if reason is not None: | |
| parts.insert(0, f"{reason}: ") | |
| super().__init__(" ".join(filter(None, parts))) | |
| class SkipNoHashInCall(SkipNoHash): | |
| def __init__(self, func, digestname, implementation=None): | |
| super().__init__(digestname, implementation, f"cannot use {func}") | |
| def _hashlib_new(digestname, openssl, /, **kwargs): | |
| """Check availability of [hashlib|_hashlib].new(digestname, **kwargs). | |
| If *openssl* is True, module is "_hashlib" (C extension module), | |
| otherwise it is "hashlib" (pure Python interface). | |
| The constructor function is returned (without binding **kwargs), | |
| or SkipTest is raised if none exists. | |
| """ | |
| assert isinstance(digestname, str), digestname | |
| # Re-import 'hashlib' in case it was mocked, but propagate | |
| # exceptions as it should be unconditionally available. | |
| hashlib = importlib.import_module("hashlib") | |
| # re-import '_hashlib' in case it was mocked | |
| _hashlib = _import_module("_hashlib") | |
| module = _hashlib if openssl and _hashlib is not None else hashlib | |
| try: | |
| module.new(digestname, **kwargs) | |
| except ValueError as exc: | |
| raise SkipNoHashInCall(f"{module.__name__}.new", digestname) from exc | |
| return functools.partial(module.new, digestname) | |
| def _builtin_hash(module_name, digestname, /, **kwargs): | |
| """Check availability of <module_name>.<digestname>(**kwargs). | |
| - The *module_name* is the C extension module name based on HACL*. | |
| - The *digestname* is one of its member, e.g., 'md5'. | |
| The constructor function is returned, or SkipTest is raised if none exists. | |
| """ | |
| assert isinstance(module_name, str), module_name | |
| assert isinstance(digestname, str), digestname | |
| fullname = f'{module_name}.{digestname}' | |
| try: | |
| builtin_module = importlib.import_module(module_name) | |
| except ImportError as exc: | |
| raise SkipNoHash(fullname, "builtin") from exc | |
| try: | |
| constructor = getattr(builtin_module, digestname) | |
| except AttributeError as exc: | |
| raise SkipNoHash(fullname, "builtin") from exc | |
| try: | |
| constructor(**kwargs) | |
| except ValueError as exc: | |
| raise SkipNoHash(fullname, "builtin") from exc | |
| return constructor | |
| def _openssl_new(digestname, /, **kwargs): | |
| """Check availability of _hashlib.new(digestname, **kwargs). | |
| The constructor function is returned (without binding **kwargs), | |
| or SkipTest is raised if none exists. | |
| """ | |
| assert isinstance(digestname, str), digestname | |
| try: | |
| # re-import '_hashlib' in case it was mocked | |
| _hashlib = importlib.import_module("_hashlib") | |
| except ImportError as exc: | |
| raise SkipNoHash(digestname, "openssl") from exc | |
| try: | |
| _hashlib.new(digestname, **kwargs) | |
| except ValueError as exc: | |
| raise SkipNoHashInCall("_hashlib.new", digestname) from exc | |
| return functools.partial(_hashlib.new, digestname) | |
| def _openssl_hash(digestname, /, **kwargs): | |
| """Check availability of _hashlib.openssl_<digestname>(**kwargs). | |
| The constructor function is returned (without binding **kwargs), | |
| or SkipTest is raised if none exists. | |
| """ | |
| assert isinstance(digestname, str), digestname | |
| method_name = f"openssl_{digestname}" | |
| fullname = f"_hashlib.{method_name}" | |
| try: | |
| # re-import '_hashlib' in case it was mocked | |
| _hashlib = importlib.import_module("_hashlib") | |
| except ImportError as exc: | |
| raise SkipNoHash(fullname, "openssl") from exc | |
| try: | |
| constructor = getattr(_hashlib, method_name) | |
| except AttributeError as exc: | |
| raise SkipNoHash(fullname, "openssl") from exc | |
| try: | |
| constructor(**kwargs) | |
| except ValueError as exc: | |
| raise SkipNoHash(fullname, "openssl") from exc | |
| return constructor | |
| def requires_hashdigest(digestname, openssl=None, *, usedforsecurity=True): | |
| """Decorator raising SkipTest if a hashing algorithm is not available. | |
| The hashing algorithm may be missing, blocked by a strict crypto policy, | |
| or Python may be configured with `--with-builtin-hashlib-hashes=no`. | |
| If 'openssl' is True, then the decorator checks that OpenSSL provides | |
| the algorithm. Otherwise the check falls back to (optional) built-in | |
| HACL* implementations. | |
| The usedforsecurity flag is passed to the constructor but has no effect | |
| on HACL* implementations. | |
| Examples of exceptions being suppressed: | |
| ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS | |
| ValueError: unsupported hash type md4 | |
| """ | |
| return _make_conditional_decorator( | |
| _hashlib_new, digestname, openssl, usedforsecurity=usedforsecurity | |
| ) | |
| def requires_openssl_hashdigest(digestname, *, usedforsecurity=True): | |
| """Decorator raising SkipTest if an OpenSSL hashing algorithm is missing. | |
| The hashing algorithm may be missing or blocked by a strict crypto policy. | |
| """ | |
| return _make_conditional_decorator( | |
| _openssl_new, digestname, usedforsecurity=usedforsecurity | |
| ) | |
| def _make_requires_builtin_hashdigest_decorator(item, *, usedforsecurity=True): | |
| assert isinstance(item, _HashInfoItem), item | |
| return _make_conditional_decorator( | |
| _builtin_hash, | |
| item.module_name, | |
| item.member_name, | |
| usedforsecurity=usedforsecurity, | |
| ) | |
| def requires_builtin_hashdigest(canonical_name, *, usedforsecurity=True): | |
| """Decorator raising SkipTest if a HACL* hashing algorithm is missing.""" | |
| info = get_hash_func_info(canonical_name) | |
| return _make_requires_builtin_hashdigest_decorator( | |
| info.builtin, usedforsecurity=usedforsecurity | |
| ) | |
| def requires_builtin_hashes(*, exclude=(), usedforsecurity=True): | |
| """Decorator raising SkipTest if one HACL* hashing algorithm is missing.""" | |
| return _chain_decorators(( | |
| _make_requires_builtin_hashdigest_decorator( | |
| info.builtin, usedforsecurity=usedforsecurity | |
| ) for info in _iter_hash_func_info(exclude) | |
| )) | |
| class HashFunctionsTrait: | |
| """Mixin trait class containing hash functions. | |
| This class is assumed to have all unitest.TestCase methods but should | |
| not directly inherit from it to prevent the test suite being run on it. | |
| Subclasses should implement the hash functions by returning an object | |
| that can be recognized as a valid digestmod parameter for both hashlib | |
| and HMAC. In particular, it cannot be a lambda function as it will not | |
| be recognized by hashlib (it will still be accepted by the pure Python | |
| implementation of HMAC). | |
| """ | |
| # Default 'usedforsecurity' to use when checking a hash function. | |
| # When the trait properties are callables (e.g., _md5.md5) and | |
| # not strings, they must be called with the same 'usedforsecurity'. | |
| usedforsecurity = True | |
| def is_valid_digest_name(self, digestname): | |
| self.assertIn(digestname, _HashId) | |
| def _find_constructor(self, digestname): | |
| # By default, a missing algorithm skips the test that uses it. | |
| self.is_valid_digest_name(digestname) | |
| self.skipTest(f"missing hash function: {digestname}") | |
| md5 = property(lambda self: self._find_constructor("md5")) | |
| sha1 = property(lambda self: self._find_constructor("sha1")) | |
| sha224 = property(lambda self: self._find_constructor("sha224")) | |
| sha256 = property(lambda self: self._find_constructor("sha256")) | |
| sha384 = property(lambda self: self._find_constructor("sha384")) | |
| sha512 = property(lambda self: self._find_constructor("sha512")) | |
| sha3_224 = property(lambda self: self._find_constructor("sha3_224")) | |
| sha3_256 = property(lambda self: self._find_constructor("sha3_256")) | |
| sha3_384 = property(lambda self: self._find_constructor("sha3_384")) | |
| sha3_512 = property(lambda self: self._find_constructor("sha3_512")) | |
| class NamedHashFunctionsTrait(HashFunctionsTrait): | |
| """Trait containing named hash functions. | |
| Hash functions are available if and only if they are available in hashlib. | |
| """ | |
| def _find_constructor(self, digestname): | |
| self.is_valid_digest_name(digestname) | |
| return str(digestname) # ensure that we are an exact string | |
| class OpenSSLHashFunctionsTrait(HashFunctionsTrait): | |
| """Trait containing OpenSSL hash functions. | |
| Hash functions are available if and only if they are available in _hashlib. | |
| """ | |
| def _find_constructor(self, digestname): | |
| self.is_valid_digest_name(digestname) | |
| # This returns a function of the form _hashlib.openssl_<name> and | |
| # not a lambda function as it is rejected by _hashlib.hmac_new(). | |
| return _openssl_hash(digestname, usedforsecurity=self.usedforsecurity) | |
| class BuiltinHashFunctionsTrait(HashFunctionsTrait): | |
| """Trait containing HACL* hash functions. | |
| Hash functions are available if and only if they are available in C. | |
| In particular, HACL* HMAC-MD5 may be available even though HACL* md5 | |
| is not since the former is unconditionally built. | |
| """ | |
| def _find_constructor(self, digestname): | |
| self.is_valid_digest_name(digestname) | |
| info = get_hash_func_info(digestname) | |
| return _builtin_hash( | |
| info.builtin.module_name, | |
| info.builtin.member_name, | |
| usedforsecurity=self.usedforsecurity, | |
| ) | |
| def find_gil_minsize(modules_names, default=2048): | |
| """Get the largest GIL_MINSIZE value for the given cryptographic modules. | |
| The valid module names are the following: | |
| - _hashlib | |
| - _md5, _sha1, _sha2, _sha3, _blake2 | |
| - _hmac | |
| """ | |
| sizes = [] | |
| for module_name in modules_names: | |
| module = _import_module(module_name) | |
| if module is not None: | |
| sizes.append(getattr(module, '_GIL_MINSIZE', default)) | |
| return max(sizes, default=default) | |
| def _block_openssl_hash_new(blocked_name): | |
| """Block OpenSSL implementation of _hashlib.new().""" | |
| assert isinstance(blocked_name, str), blocked_name | |
| # re-import '_hashlib' in case it was mocked | |
| if (_hashlib := _import_module("_hashlib")) is None: | |
| return contextlib.nullcontext() | |
| @functools.wraps(wrapped := _hashlib.new) | |
| def _hashlib_new(name, data=b'', *, usedforsecurity=True, string=None): | |
| if name == blocked_name: | |
| raise _hashlib.UnsupportedDigestmodError(blocked_name) | |
| return wrapped(name, data, | |
| usedforsecurity=usedforsecurity, string=string) | |
| _ensure_wrapper_signature(_hashlib_new, wrapped) | |
| return unittest.mock.patch('_hashlib.new', _hashlib_new) | |
| def _block_openssl_hmac_new(blocked_name): | |
| """Block OpenSSL HMAC-HASH implementation.""" | |
| assert isinstance(blocked_name, str), blocked_name | |
| # re-import '_hashlib' in case it was mocked | |
| if (_hashlib := _import_module("_hashlib")) is None: | |
| return contextlib.nullcontext() | |
| @functools.wraps(wrapped := _hashlib.hmac_new) | |
| def wrapper(key, msg=b'', digestmod=None): | |
| if digestmod == blocked_name: | |
| raise _hashlib.UnsupportedDigestmodError(blocked_name) | |
| return wrapped(key, msg, digestmod) | |
| _ensure_wrapper_signature(wrapper, wrapped) | |
| return unittest.mock.patch('_hashlib.hmac_new', wrapper) | |
| def _block_openssl_hmac_digest(blocked_name): | |
| """Block OpenSSL HMAC-HASH one-shot digest implementation.""" | |
| assert isinstance(blocked_name, str), blocked_name | |
| # re-import '_hashlib' in case it was mocked | |
| if (_hashlib := _import_module("_hashlib")) is None: | |
| return contextlib.nullcontext() | |
| @functools.wraps(wrapped := _hashlib.hmac_digest) | |
| def _hashlib_hmac_digest(key, msg, digest): | |
| if digest == blocked_name: | |
| raise _hashlib.UnsupportedDigestmodError(blocked_name) | |
| return wrapped(key, msg, digest) | |
| _ensure_wrapper_signature(_hashlib_hmac_digest, wrapped) | |
| return unittest.mock.patch('_hashlib.hmac_digest', _hashlib_hmac_digest) | |
| def _block_builtin_hash_new(name): | |
| """Block a buitin-in hash name from the hashlib.new() interface.""" | |
| assert isinstance(name, str), name | |
| assert name.lower() == name, f"invalid name: {name}" | |
| assert name in _HashId, f"invalid hash: {name}" | |
| # Re-import 'hashlib' in case it was mocked | |
| hashlib = importlib.import_module('hashlib') | |
| builtin_constructor_cache = getattr(hashlib, '__builtin_constructor_cache') | |
| builtin_constructor_cache_mock = builtin_constructor_cache.copy() | |
| builtin_constructor_cache_mock.pop(name, None) | |
| builtin_constructor_cache_mock.pop(name.upper(), None) | |
| # __get_builtin_constructor() imports the HACL* modules on demand, | |
| # so we need to block the possibility of importing it, but only | |
| # during the call to __get_builtin_constructor(). | |
| get_builtin_constructor = getattr(hashlib, '__get_builtin_constructor') | |
| builtin_module_name = get_hash_func_info(name).builtin.module_name | |
| @functools.wraps(get_builtin_constructor) | |
| def get_builtin_constructor_mock(name): | |
| with import_helper.isolated_modules(): | |
| sys = importlib.import_module("sys") | |
| sys.modules[builtin_module_name] = None # block module's import | |
| return get_builtin_constructor(name) | |
| return unittest.mock.patch.multiple( | |
| hashlib, | |
| __get_builtin_constructor=get_builtin_constructor_mock, | |
| __builtin_constructor_cache=builtin_constructor_cache_mock, | |
| ) | |
| def _block_builtin_hmac_new(blocked_name): | |
| assert isinstance(blocked_name, str), blocked_name | |
| # re-import '_hmac' in case it was mocked | |
| if (_hmac := _import_module("_hmac")) is None: | |
| return contextlib.nullcontext() | |
| @functools.wraps(wrapped := _hmac.new) | |
| def _hmac_new(key, msg=None, digestmod=None): | |
| if digestmod == blocked_name: | |
| raise _hmac.UnknownHashError(blocked_name) | |
| return wrapped(key, msg, digestmod) | |
| _ensure_wrapper_signature(_hmac_new, wrapped) | |
| return unittest.mock.patch('_hmac.new', _hmac_new) | |
| def _block_builtin_hmac_digest(blocked_name): | |
| assert isinstance(blocked_name, str), blocked_name | |
| # re-import '_hmac' in case it was mocked | |
| if (_hmac := _import_module("_hmac")) is None: | |
| return contextlib.nullcontext() | |
| @functools.wraps(wrapped := _hmac.compute_digest) | |
| def _hmac_compute_digest(key, msg, digest): | |
| if digest == blocked_name: | |
| raise _hmac.UnknownHashError(blocked_name) | |
| return wrapped(key, msg, digest) | |
| _ensure_wrapper_signature(_hmac_compute_digest, wrapped) | |
| return unittest.mock.patch('_hmac.compute_digest', _hmac_compute_digest) | |
| def _make_hash_constructor_blocker(name, dummy, implementation): | |
| info = get_hash_func_info(name)[implementation] | |
| if (wrapped := info.import_member()) is None: | |
| # function shouldn't exist for this implementation | |
| return contextlib.nullcontext() | |
| wrapper = functools.wraps(wrapped)(dummy) | |
| _ensure_wrapper_signature(wrapper, wrapped) | |
| return unittest.mock.patch(info.fullname, wrapper) | |
| def _block_hashlib_hash_constructor(name): | |
| """Block explicit public constructors.""" | |
| def dummy(data=b'', *, usedforsecurity=True, string=None): | |
| raise ValueError(f"blocked explicit public hash name: {name}") | |
| return _make_hash_constructor_blocker(name, dummy, 'hashlib') | |
| def _block_openssl_hash_constructor(name): | |
| """Block explicit OpenSSL constructors.""" | |
| def dummy(data=b'', *, usedforsecurity=True, string=None): | |
| raise ValueError(f"blocked explicit OpenSSL hash name: {name}") | |
| return _make_hash_constructor_blocker(name, dummy, 'openssl') | |
| def _block_builtin_hash_constructor(name): | |
| """Block explicit HACL* constructors.""" | |
| def dummy(data=b'', *, usedforsecurity=True, string=b''): | |
| raise ValueError(f"blocked explicit builtin hash name: {name}") | |
| return _make_hash_constructor_blocker(name, dummy, 'builtin') | |
| def _block_builtin_hmac_constructor(name): | |
| """Block explicit HACL* HMAC constructors.""" | |
| info = get_hmac_item_info(name) | |
| assert info.module_name is None or info.module_name == "_hmac", info | |
| if (wrapped := info.import_member()) is None: | |
| # function shouldn't exist for this implementation | |
| return contextlib.nullcontext() | |
| @functools.wraps(wrapped) | |
| def wrapper(key, obj): | |
| raise ValueError(f"blocked hash name: {name}") | |
| _ensure_wrapper_signature(wrapper, wrapped) | |
| return unittest.mock.patch(info.fullname, wrapper) | |
| @contextlib.contextmanager | |
| def block_algorithm(name, *, allow_openssl=False, allow_builtin=False): | |
| """Block a hash algorithm for both hashing and HMAC. | |
| Be careful with this helper as a function may be allowed, but can | |
| still raise a ValueError at runtime if the OpenSSL security policy | |
| disables it, e.g., if allow_openssl=True and FIPS mode is on. | |
| """ | |
| with contextlib.ExitStack() as stack: | |
| if not (allow_openssl or allow_builtin): | |
| # Named constructors have a different behavior in the sense | |
| # that they are either built-ins or OpenSSL ones, but not | |
| # "agile" ones (namely once "hashlib" has been imported, | |
| # they are fixed). | |
| # | |
| # If OpenSSL is not available, hashes fall back to built-in ones, | |
| # in which case we don't need to block the explicit public hashes | |
| # as they will call a mocked one. | |
| # | |
| # If OpenSSL is available, hashes fall back to "openssl_*" ones, | |
| # except for BLAKE2b and BLAKE2s. | |
| stack.enter_context(_block_hashlib_hash_constructor(name)) | |
| elif ( | |
| # In FIPS mode, hashlib.<name>() functions may raise if they use | |
| # the OpenSSL implementation, except with usedforsecurity=False. | |
| # However, blocking such functions also means blocking them | |
| # so we again need to block them if we want to. | |
| (_hashlib := _import_module("_hashlib")) | |
| and _hashlib.get_fips_mode() | |
| and not allow_openssl | |
| ) or ( | |
| # Without OpenSSL, hashlib.<name>() functions are aliases | |
| # to built-in functions, so both of them must be blocked | |
| # as the module may have been imported before the HACL ones. | |
| not (_hashlib := _import_module("_hashlib")) | |
| and not allow_builtin | |
| ): | |
| stack.enter_context(_block_hashlib_hash_constructor(name)) | |
| if not allow_openssl: | |
| # _hashlib.new() | |
| stack.enter_context(_block_openssl_hash_new(name)) | |
| # _hashlib.openssl_*() | |
| stack.enter_context(_block_openssl_hash_constructor(name)) | |
| # _hashlib.hmac_new() | |
| stack.enter_context(_block_openssl_hmac_new(name)) | |
| # _hashlib.hmac_digest() | |
| stack.enter_context(_block_openssl_hmac_digest(name)) | |
| if not allow_builtin: | |
| # __get_builtin_constructor(name) | |
| stack.enter_context(_block_builtin_hash_new(name)) | |
| # <built-in module>.<built-in name>() | |
| stack.enter_context(_block_builtin_hash_constructor(name)) | |
| # _hmac.new(..., name) | |
| stack.enter_context(_block_builtin_hmac_new(name)) | |
| # _hmac.compute_<name>() | |
| stack.enter_context(_block_builtin_hmac_constructor(name)) | |
| # _hmac.compute_digest(..., name) | |
| stack.enter_context(_block_builtin_hmac_digest(name)) | |
| yield | |
| @contextlib.contextmanager | |
| def block_openssl_algorithms(*, exclude=()): | |
| """Block OpenSSL implementations, except those given in *exclude*.""" | |
| with contextlib.ExitStack() as stack: | |
| for name in CANONICAL_DIGEST_NAMES.difference(exclude): | |
| stack.enter_context(block_algorithm(name, allow_builtin=True)) | |
| yield | |
| @contextlib.contextmanager | |
| def block_builtin_algorithms(*, exclude=()): | |
| """Block HACL* implementations, except those given in *exclude*.""" | |
| with contextlib.ExitStack() as stack: | |
| for name in CANONICAL_DIGEST_NAMES.difference(exclude): | |
| stack.enter_context(block_algorithm(name, allow_openssl=True)) | |
| yield |