cpython/Lib/test/support/hashlib_helper.py
Bénédikt Tran c504f62fe2
gh-136547: fix hashlib_helper for blocking and requesting digests (#136762)
- Fix `hashlib_helper.block_algorithm` where the dummy functions were incorrectly defined.
- Rename `hashlib_helper.HashAPI` to `hashlib_helper.HashInfo` and add more helper methods.
- Simplify `hashlib_helper.requires_*()` functions.
- Rewrite some private helpers in `hashlib_helper`.
- Remove `find_{builtin,openssl}_hashdigest_constructor()` as they are no more needed and were
  not meant to be public in the first place.
- Fix some tests in `test_hashlib` when FIPS mode is on.
2025-07-20 14:32:35 +02:00

796 lines
28 KiB
Python

import contextlib
import enum
import functools
import importlib
import inspect
import unittest
import unittest.mock
from test.support import import_helper
from types import MappingProxyType
def try_import_module(module_name):
"""Try to import a module and return None on failure."""
try:
return importlib.import_module(module_name)
except ImportError:
return None
class HID(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, HID.__members__))
NON_HMAC_DIGEST_NAMES = frozenset((
HID.shake_128, HID.shake_256,
HID.blake2s, HID.blake2b,
))
class HashInfo:
"""Dataclass storing explicit hash constructor names.
- *builtin* is the fully-qualified name for the explicit HACL*
hash constructor function, e.g., "_md5.md5".
- *openssl* is the name of the "_hashlib" module method for the explicit
OpenSSL hash constructor function, e.g., "openssl_md5".
- *hashlib* is the name of the "hashlib" module method for the explicit
hash constructor function, e.g., "md5".
"""
def __init__(self, builtin, openssl=None, hashlib=None):
assert isinstance(builtin, str), builtin
assert len(builtin.split(".")) == 2, builtin
self.builtin = builtin
self.builtin_module_name, self.builtin_method_name = (
self.builtin.split(".", maxsplit=1)
)
assert openssl is None or openssl.startswith("openssl_")
self.openssl = self.openssl_method_name = openssl
self.openssl_module_name = "_hashlib" if openssl else None
assert hashlib is None or isinstance(hashlib, str)
self.hashlib = self.hashlib_method_name = hashlib
self.hashlib_module_name = "hashlib" if hashlib else None
def module_name(self, implementation):
match implementation:
case "builtin":
return self.builtin_module_name
case "openssl":
return self.openssl_module_name
case "hashlib":
return self.hashlib_module_name
raise AssertionError(f"invalid implementation {implementation}")
def method_name(self, implementation):
match implementation:
case "builtin":
return self.builtin_method_name
case "openssl":
return self.openssl_method_name
case "hashlib":
return self.hashlib_method_name
raise AssertionError(f"invalid implementation {implementation}")
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".
"""
module_name = self.module_name(implementation)
method_name = self.method_name(implementation)
if module_name is None or method_name is None:
return None
return f"{module_name}.{method_name}"
# Mapping from a "canonical" name to a pair (HACL*, _hashlib.*, hashlib.*)
# constructors. If the constructor name is None, then this means that the
# algorithm can only be used by the "agile" new() interfaces.
_EXPLICIT_CONSTRUCTORS = MappingProxyType({ # fmt: skip
HID.md5: HashInfo("_md5.md5", "openssl_md5", "md5"),
HID.sha1: HashInfo("_sha1.sha1", "openssl_sha1", "sha1"),
HID.sha224: HashInfo("_sha2.sha224", "openssl_sha224", "sha224"),
HID.sha256: HashInfo("_sha2.sha256", "openssl_sha256", "sha256"),
HID.sha384: HashInfo("_sha2.sha384", "openssl_sha384", "sha384"),
HID.sha512: HashInfo("_sha2.sha512", "openssl_sha512", "sha512"),
HID.sha3_224: HashInfo(
"_sha3.sha3_224", "openssl_sha3_224", "sha3_224"
),
HID.sha3_256: HashInfo(
"_sha3.sha3_256", "openssl_sha3_256", "sha3_256"
),
HID.sha3_384: HashInfo(
"_sha3.sha3_384", "openssl_sha3_384", "sha3_384"
),
HID.sha3_512: HashInfo(
"_sha3.sha3_512", "openssl_sha3_512", "sha3_512"
),
HID.shake_128: HashInfo(
"_sha3.shake_128", "openssl_shake_128", "shake_128"
),
HID.shake_256: HashInfo(
"_sha3.shake_256", "openssl_shake_256", "shake_256"
),
HID.blake2s: HashInfo("_blake2.blake2s", None, "blake2s"),
HID.blake2b: HashInfo("_blake2.blake2b", None, "blake2b"),
})
assert _EXPLICIT_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES
get_hash_info = _EXPLICIT_CONSTRUCTORS.__getitem__
# 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.
_EXPLICIT_HMAC_CONSTRUCTORS = {
HID(name): f"_hmac.compute_{name}"
for name in CANONICAL_DIGEST_NAMES
}
# Neither HACL* nor OpenSSL supports HMAC over XOFs.
_EXPLICIT_HMAC_CONSTRUCTORS[HID.shake_128] = None
_EXPLICIT_HMAC_CONSTRUCTORS[HID.shake_256] = None
# Strictly speaking, HMAC-BLAKE is meaningless as BLAKE2 is already a
# keyed hash function. However, as it's exposed by HACL*, we test it.
_EXPLICIT_HMAC_CONSTRUCTORS[HID.blake2s] = '_hmac.compute_blake2s_32'
_EXPLICIT_HMAC_CONSTRUCTORS[HID.blake2b] = '_hmac.compute_blake2b_32'
_EXPLICIT_HMAC_CONSTRUCTORS = MappingProxyType(_EXPLICIT_HMAC_CONSTRUCTORS)
assert _EXPLICIT_HMAC_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES
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 requires_hashlib():
_hashlib = try_import_module("_hashlib")
return unittest.skipIf(_hashlib is None, "requires _hashlib")
def requires_builtin_hmac():
_hmac = try_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, interface=None):
parts = ["missing", implementation, f"hash algorithm {digestname!r}"]
if interface is not None:
parts.append(f"for {interface}")
super().__init__(" ".join(filter(None, parts)))
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 = try_import_module("_hashlib")
module = _hashlib if openssl and _hashlib is not None else hashlib
try:
module.new(digestname, **kwargs)
except ValueError as exc:
interface = f"{module.__name__}.new"
raise SkipNoHash(digestname, interface=interface) 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 SkipNoHash(digestname, interface="_hashlib.new") 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
fullname = f"_hashlib.openssl_{digestname}"
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, f"openssl_{digestname}", None)
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 _make_requires_hashdigest_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_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_requires_hashdigest_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_requires_hashdigest_decorator(
_openssl_new, digestname, usedforsecurity=usedforsecurity
)
def requires_builtin_hashdigest(
module_name, digestname, *, usedforsecurity=True
):
"""Decorator raising SkipTest if a HACL* hashing algorithm is missing.
- The *module_name* is the C extension module name based on HACL*.
- The *digestname* is one of its member, e.g., 'md5'.
"""
return _make_requires_hashdigest_decorator(
_builtin_hash, module_name, digestname, usedforsecurity=usedforsecurity
)
def requires_builtin_hashes(*ignored, usedforsecurity=True):
"""Decorator raising SkipTest if one HACL* hashing algorithm is missing."""
return _chain_decorators((
requires_builtin_hashdigest(
api.builtin_module_name,
api.builtin_method_name,
usedforsecurity=usedforsecurity,
)
for name, api in _EXPLICIT_CONSTRUCTORS.items()
if name not in ignored
))
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).
"""
DIGEST_NAMES = [
'md5', 'sha1',
'sha224', 'sha256', 'sha384', 'sha512',
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
]
# 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
@classmethod
def setUpClass(cls):
super().setUpClass()
assert CANONICAL_DIGEST_NAMES.issuperset(cls.DIGEST_NAMES)
def is_valid_digest_name(self, digestname):
self.assertIn(digestname, self.DIGEST_NAMES)
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}")
@property
def md5(self):
return self._find_constructor("md5")
@property
def sha1(self):
return self._find_constructor("sha1")
@property
def sha224(self):
return self._find_constructor("sha224")
@property
def sha256(self):
return self._find_constructor("sha256")
@property
def sha384(self):
return self._find_constructor("sha384")
@property
def sha512(self):
return self._find_constructor("sha512")
@property
def sha3_224(self):
return self._find_constructor("sha3_224")
@property
def sha3_256(self):
return self._find_constructor("sha3_256")
@property
def sha3_384(self):
return self._find_constructor("sha3_384")
@property
def sha3_512(self):
return 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 digestname
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 = _EXPLICIT_CONSTRUCTORS[digestname]
return _builtin_hash(
info.builtin_module_name,
info.builtin_method_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 = try_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 := try_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 := try_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 := try_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 HID, 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 = _EXPLICIT_CONSTRUCTORS[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 := try_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 := try_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 = _EXPLICIT_CONSTRUCTORS[name]
module_name = info.module_name(implementation)
method_name = info.method_name(implementation)
if module_name is None or method_name is None:
# function shouldn't exist for this implementation
return contextlib.nullcontext()
try:
module = importlib.import_module(module_name)
except ImportError:
# module is already disabled
return contextlib.nullcontext()
wrapped = getattr(module, method_name)
wrapper = functools.wraps(wrapped)(dummy)
_ensure_wrapper_signature(wrapper, wrapped)
return unittest.mock.patch(info.fullname(implementation), 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."""
fullname = _EXPLICIT_HMAC_CONSTRUCTORS[name]
if fullname is None:
# function shouldn't exist for this implementation
return contextlib.nullcontext()
assert fullname.count('.') == 1, fullname
module_name, method = fullname.split('.', maxsplit=1)
assert module_name == '_hmac', module_name
try:
module = importlib.import_module(module_name)
except ImportError:
# module is already disabled
return contextlib.nullcontext()
@functools.wraps(wrapped := getattr(module, method))
def wrapper(key, obj):
raise ValueError(f"blocked hash name: {name}")
_ensure_wrapper_signature(wrapper, wrapped)
return unittest.mock.patch(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 := try_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 := try_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