From dc583f0586abd0a857fab7574c32aa7c19c5a5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:05:46 +0200 Subject: [PATCH 01/13] refactor `hashlib_helper` for blocking and requesting digests --- Lib/hashlib.py | 20 +- Lib/test/support/hashlib_helper.py | 443 +++++++++++++++++------------ Lib/test/test_hashlib.py | 18 +- Lib/test/test_support.py | 132 +++++++-- 4 files changed, 394 insertions(+), 219 deletions(-) diff --git a/Lib/hashlib.py b/Lib/hashlib.py index 6c72fba03bf687..e1347fbd0e6bf6 100644 --- a/Lib/hashlib.py +++ b/Lib/hashlib.py @@ -128,12 +128,22 @@ def __get_openssl_constructor(name): # Prefer our builtin blake2 implementation. return __get_builtin_constructor(name) try: - # MD5, SHA1, and SHA2 are in all supported OpenSSL versions - # SHA3/shake are available in OpenSSL 1.1.1+ + # Fetch the OpenSSL hash function if it exists, + # independently of the context security policy. f = getattr(_hashlib, 'openssl_' + name) - # Allow the C module to raise ValueError. The function will be - # defined but the hash not actually available. Don't fall back to - # builtin if the current security policy blocks a digest, bpo#40695. + # Check if the context security policy blocks the digest or not + # by allowing the C module to raise a ValueError. The function + # will be defined but the hash will not be available at runtime. + # + # We use "usedforsecurity=False" to prevent falling back to the + # built-in function in case the security policy does not allow it. + # + # Note that this only affects the explicit named constructors, + # and not the algorithms exposed through hashlib.new() which + # can still be resolved to a built-in function even if the + # current security policy does not allow it. + # + # See https://github.com/python/cpython/issues/84872. f(usedforsecurity=False) # Use the C function directly (very fast) return f diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 337a1e415b0de3..bf9b5bcc7ff2f0 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -6,18 +6,16 @@ import unittest import unittest.mock from collections import namedtuple -from test.support.import_helper import import_module +from functools import partial +from test.support import import_helper from types import MappingProxyType -try: - import _hashlib -except ImportError: - _hashlib = None -try: - import _hmac -except ImportError: - _hmac = None +def try_import_module(name, default=None): + try: + return importlib.import_module(name) + except ImportError: + return None CANONICAL_DIGEST_NAMES = frozenset(( @@ -36,8 +34,16 @@ class HashAPI(namedtuple("HashAPI", "builtin openssl hashlib")): - def fullname(self, typ): - match typ: + @property + def builtin_module_name(self): + return self.builtin.split(".", maxsplit=1)[0] + + @property + def builtin_method_name(self): + return self.builtin.split(".", maxsplit=1)[1] + + def fullname(self, impl): + match impl: case "builtin": return self.builtin case "openssl": @@ -45,7 +51,7 @@ def fullname(self, typ): case "hashlib": return f"hashlib.{self.hashlib}" if self.hashlib else None case _: - raise AssertionError(f"unknown type: {typ}") + raise AssertionError(f"unknown implementation: {impl}") # Mapping from a "canonical" name to a pair (HACL*, _hashlib.*, hashlib.*) @@ -86,6 +92,34 @@ def fullname(self, typ): 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 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. @@ -107,50 +141,126 @@ def _ensure_wrapper_signature(wrapper, wrapped): ) -def requires_hashlib(): - return unittest.skipIf(_hashlib is None, "requires _hashlib") +def _requires_module(name): + def decorator_func(func): + module = try_import_module(name, missing := object()) + return unittest.skipIf(module is missing, f"requires {name}")(func) + return partial(_decorate_func_or_class, decorator_func) + + +requires_hashlib = partial(_requires_module, "_hashlib") +requires_builtin_hmac = partial(_requires_module, "_hmac") + + +class SkipNoHash(unittest.SkipTest): + + def __init__(self, digestname, implementation=None): + parts = ["missing", implementation, f"hash algorithm: {digestname!r}"] + 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, or SkipTest is raised if none exists. + """ + # 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") + mod = _hashlib if openssl and _hashlib is not None else hashlib + constructor = partial(mod.new, digestname, **kwargs) + try: + constructor() + except ValueError: + implementation = f"{mod.__name__}.{new.__name__}" + raise SkipNoHash(digestname, implementation) from exc + return constructor + +def _builtin_hash(module_name, digestname, /, **kwargs): + """Check availability of module_name.digestname(**kwargs). -def requires_builtin_hmac(): - return unittest.skipIf(_hmac is None, "requires _hmac") + - 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 _missing_hash(digestname, implementation=None, *, exc=None): - parts = ["missing", implementation, f"hash algorithm: {digestname!r}"] - msg = " ".join(filter(None, parts)) - raise unittest.SkipTest(msg) from exc +def _openssl_new(digestname, /, **kwargs): + """Check availability of _hashlib.new(digestname, **kwargs). -def _openssl_availabillity(digestname, *, usedforsecurity): + The constructor function is returned, or SkipTest is raised if none exists. + """ assert isinstance(digestname, str), digestname try: - _hashlib.new(digestname, usedforsecurity=usedforsecurity) - except AttributeError: - assert _hashlib is None - _missing_hash(digestname, "OpenSSL") + # re-import '_hashlib' in case it was mocked + _hashlib = importlib.import_module("_hashlib") + except ImportError as exc: + raise SkipNoHash(fullname, "openssl") from exc + constructor = partial(_hashlib.new, digestname, **kwargs) + try: + constructor() except ValueError as exc: - _missing_hash(digestname, "OpenSSL", exc=exc) + raise SkipNoHash(fullname, "_hashlib.new") from exc + return constructor -def _decorate_func_or_class(func_or_class, decorator_func): - if not isinstance(func_or_class, type): - return decorator_func(func_or_class) +def _get_openssl_hash_constructor(digestname, **kwargs): + """Check availability of _hashlib.openssl_(**kwargs). - 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 + The constructor function is returned, 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(check_availability): + def decorator_func(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + check_availability() + return func(*args, **kwargs) + return wrapper + return partial(_decorate_func_or_class, decorator_func) -def requires_hashdigest(digestname, openssl=None, usedforsecurity=True): +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, @@ -167,27 +277,10 @@ def requires_hashdigest(digestname, openssl=None, usedforsecurity=True): ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS ValueError: unsupported hash type md4 """ - assert isinstance(digestname, str), digestname - if openssl and _hashlib is not None: - def test_availability(): - _hashlib.new(digestname, usedforsecurity=usedforsecurity) - else: - def test_availability(): - hashlib.new(digestname, usedforsecurity=usedforsecurity) - - def decorator_func(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - try: - test_availability() - except ValueError as exc: - _missing_hash(digestname, exc=exc) - return func(*args, **kwargs) - return wrapper + def check_availability(): + _hashlib_new(digestname, openssl, usedforsecurity=usedforsecurity) - def decorator(func_or_class): - return _decorate_func_or_class(func_or_class, decorator_func) - return decorator + return _make_requires_hashdigest_decorator(check_availability) def requires_openssl_hashdigest(digestname, *, usedforsecurity=True): @@ -195,27 +288,10 @@ def requires_openssl_hashdigest(digestname, *, usedforsecurity=True): The hashing algorithm may be missing or blocked by a strict crypto policy. """ - assert isinstance(digestname, str), digestname - def decorator_func(func): - @requires_hashlib() # avoid checking at each call - @functools.wraps(func) - def wrapper(*args, **kwargs): - _openssl_availabillity(digestname, usedforsecurity=usedforsecurity) - return func(*args, **kwargs) - return wrapper - - def decorator(func_or_class): - return _decorate_func_or_class(func_or_class, decorator_func) - return decorator + def check_availability(): + _openssl_new(digestname, usedforsecurity=usedforsecurity) - -def find_openssl_hashdigest_constructor(digestname, *, usedforsecurity=True): - """Find the OpenSSL hash function constructor by its name.""" - assert isinstance(digestname, str), digestname - _openssl_availabillity(digestname, usedforsecurity=usedforsecurity) - # This returns a function of the form _hashlib.openssl_ and - # not a lambda function as it is rejected by _hashlib.hmac_new(). - return getattr(_hashlib, f"openssl_{digestname}") + return _make_requires_hashdigest_decorator(check_availability) def requires_builtin_hashdigest( @@ -226,40 +302,23 @@ def requires_builtin_hashdigest( - The *module_name* is the C extension module name based on HACL*. - The *digestname* is one of its member, e.g., 'md5'. """ - assert isinstance(digestname, str), digestname - def decorator_func(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - module = import_module(module_name) - try: - getattr(module, digestname) - except AttributeError: - fullname = f'{module_name}.{digestname}' - _missing_hash(fullname, implementation="HACL") - return func(*args, **kwargs) - return wrapper + def check_availability(): + _builtin_hash(module_name, digestname, usedforsecurity=usedforsecurity) - def decorator(func_or_class): - return _decorate_func_or_class(func_or_class, decorator_func) - return decorator + return _make_requires_hashdigest_decorator(check_availability) -def find_builtin_hashdigest_constructor( - module_name, digestname, *, usedforsecurity=True -): - """Find the HACL* hash function constructor. - - - The *module_name* is the C extension module name based on HACL*. - - The *digestname* is one of its member, e.g., 'md5'. - """ - assert isinstance(digestname, str), digestname - module = import_module(module_name) - try: - constructor = getattr(module, digestname) - constructor(b'', usedforsecurity=usedforsecurity) - except (AttributeError, TypeError, ValueError): - _missing_hash(f'{module_name}.{digestname}', implementation="HACL") - return constructor +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: @@ -357,9 +416,10 @@ class OpenSSLHashFunctionsTrait(HashFunctionsTrait): def _find_constructor(self, digestname): self.is_valid_digest_name(digestname) - return find_openssl_hashdigest_constructor( - digestname, usedforsecurity=self.usedforsecurity - ) + # This returns a function of the form _hashlib.openssl_ and + # not a lambda function as it is rejected by _hashlib.hmac_new(). + return _get_openssl_hash_constructor( + digestname, usedforsecurity=self.usedforsecurity) class BuiltinHashFunctionsTrait(HashFunctionsTrait): @@ -370,49 +430,14 @@ class BuiltinHashFunctionsTrait(HashFunctionsTrait): is not since the former is unconditionally built. """ - def _find_constructor_in(self, module, digestname): + def _find_constructor(self, digestname): self.is_valid_digest_name(digestname) - return find_builtin_hashdigest_constructor(module, digestname) - - @property - def md5(self): - return self._find_constructor_in("_md5", "md5") - - @property - def sha1(self): - return self._find_constructor_in("_sha1", "sha1") - - @property - def sha224(self): - return self._find_constructor_in("_sha2", "sha224") - - @property - def sha256(self): - return self._find_constructor_in("_sha2", "sha256") - - @property - def sha384(self): - return self._find_constructor_in("_sha2", "sha384") - - @property - def sha512(self): - return self._find_constructor_in("_sha2", "sha512") - - @property - def sha3_224(self): - return self._find_constructor_in("_sha3", "sha3_224") - - @property - def sha3_256(self): - return self._find_constructor_in("_sha3","sha3_256") - - @property - def sha3_384(self): - return self._find_constructor_in("_sha3","sha3_384") - - @property - def sha3_512(self): - return self._find_constructor_in("_sha3","sha3_512") + info = _EXPLICIT_CONSTRUCTORS[digestname].builtin + self.assertIsNotNone(info, f"no built-in implementation " + f"for {digestname!r}") + module_name, digestname = info.split('.', maxsplit=1) + return _builtin_hash( + module_name, digestname, usedforsecurity=self.usedforsecurity) def find_gil_minsize(modules_names, default=2048): @@ -426,18 +451,17 @@ def find_gil_minsize(modules_names, default=2048): """ sizes = [] for module_name in modules_names: - try: - module = importlib.import_module(module_name) - except ImportError: - continue - sizes.append(getattr(module, '_GIL_MINSIZE', default)) + 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 - if _hashlib is None: + # 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 wrapper(name, data=b'', *, usedforsecurity=True, string=None): @@ -451,7 +475,8 @@ def wrapper(name, data=b'', *, usedforsecurity=True, string=None): def _block_openssl_hmac_new(blocked_name): """Block OpenSSL HMAC-HASH implementation.""" assert isinstance(blocked_name, str), blocked_name - if _hashlib is None: + # 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): @@ -465,7 +490,8 @@ def wrapper(key, msg=b'', digestmod=None): def _block_openssl_hmac_digest(blocked_name): """Block OpenSSL HMAC-HASH one-shot digest implementation.""" assert isinstance(blocked_name, str), blocked_name - if _hashlib is None: + # 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 wrapper(key, msg, digest): @@ -478,18 +504,39 @@ def wrapper(key, msg, digest): @contextlib.contextmanager 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 _EXPLICIT_CONSTRUCTORS, name + # Re-import 'hashlib' in case it was mocked, but propagate + # exceptions as it should be unconditionally available. + hashlib = importlib.import_module("hashlib") builtin_cache = getattr(hashlib, '__builtin_constructor_cache') if name in builtin_cache: f = builtin_cache.pop(name) F = builtin_cache.pop(name.upper(), None) else: f = F = 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 + + def get_builtin_constructor_wrapper(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) + try: + setattr(hashlib, "__get_builtin_constructor", + get_builtin_constructor_wrapper) yield finally: + setattr(hashlib, "__get_builtin_constructor", get_builtin_constructor) if f is not None: builtin_cache[name] = f if F is not None: @@ -498,7 +545,8 @@ def _block_builtin_hash_new(name): def _block_builtin_hmac_new(blocked_name): assert isinstance(blocked_name, str), blocked_name - if _hmac is None: + # 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 wrapper(key, msg=None, digestmod=None): @@ -511,7 +559,8 @@ def wrapper(key, msg=None, digestmod=None): def _block_builtin_hmac_digest(blocked_name): assert isinstance(blocked_name, str), blocked_name - if _hmac is None: + # 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 wrapper(key, msg, digest): @@ -547,7 +596,7 @@ def _block_hashlib_hash_constructor(name): """Block explicit public constructors.""" assert isinstance(name, str), name def dummy(data=b'', *, usedforsecurity=True, string=None): - raise ValueError(f"unsupported hash name: {name}") + raise ValueError(f"blocked explicit public hash name: {name}") return _make_hash_constructor_blocker(name, dummy, interface='hashlib') @@ -555,7 +604,7 @@ def _block_openssl_hash_constructor(name): """Block explicit OpenSSL constructors.""" assert isinstance(name, str), name def dummy(data=b'', *, usedforsecurity=True, string=None): - raise ValueError(f"unsupported hash name: {name}") + raise ValueError(f"blocked explicit OpenSSL hash name: {name}") return _make_hash_constructor_blocker(name, dummy, interface='openssl') @@ -563,7 +612,7 @@ def _block_builtin_hash_constructor(name): """Block explicit HACL* constructors.""" assert isinstance(name, str), name def dummy(data=b'', *, usedforsecurity=True, string=b''): - raise ValueError(f"unsupported hash name: {name}") + raise ValueError(f"blocked explicit builtin hash name: {name}") return _make_hash_constructor_blocker(name, dummy, interface='builtin') @@ -585,7 +634,7 @@ def _block_builtin_hmac_constructor(name): return contextlib.nullcontext() @functools.wraps(wrapped := getattr(module, method)) def wrapper(key, obj): - raise ValueError(f"unsupported hash name: {name}") + raise ValueError(f"blocked hash name: {name}") _ensure_wrapper_signature(wrapper, wrapped) return unittest.mock.patch(fullname, wrapper) @@ -598,24 +647,56 @@ def block_algorithm(name, *, allow_openssl=False, allow_builtin=False): 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: + with (contextlib.ExitStack() as stack): if not (allow_openssl or allow_builtin): - # If one of the private interface is allowed, then the - # public interface will fallback to it even though the - # comment in hashlib.py says otherwise. + # 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). # - # So we should only block it if the private interfaces - # are blocked as well. + # 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.() 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.() 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)) - stack.enter_context(_block_openssl_hash_constructor(name)) + if not allow_builtin: + # __get_builtin_constructor(name) stack.enter_context(_block_builtin_hash_new(name)) - stack.enter_context(_block_builtin_hmac_new(name)) - stack.enter_context(_block_builtin_hmac_digest(name)) + # .() stack.enter_context(_block_builtin_hash_constructor(name)) + # _hmac.new(..., name) + stack.enter_context(_block_builtin_hmac_new(name)) + # _hmac.compute_() stack.enter_context(_block_builtin_hmac_constructor(name)) + # _hmac.compute_digest(..., name) + stack.enter_context(_block_builtin_hmac_digest(name)) yield diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py index 65e18639f82be5..8e6a07e86b9c17 100644 --- a/Lib/test/test_hashlib.py +++ b/Lib/test/test_hashlib.py @@ -541,13 +541,17 @@ def check(self, name, data, hexdigest, shake=False, **kwargs): def check_file_digest(self, name, data, hexdigest): hexdigest = hexdigest.lower() - try: - hashlib.new(name) - except ValueError: - # skip, algorithm is blocked by security policy. - return - digests = [name] - digests.extend(self.constructors_to_test[name]) + digests = [] + for digest in [name, *self.constructors_to_test[name]]: + try: + if callable(digest): + digest(b"") + else: + hashlib.new(digest) + except ValueError: + # skip, algorithm is blocked by security policy. + continue + digests.append(digest) with tempfile.TemporaryFile() as f: f.write(data) diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index cb31122fee9642..ce195ac4427147 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -2,6 +2,7 @@ import errno import importlib import itertools +import inspect import io import logging import os @@ -820,6 +821,7 @@ def test_linked_to_musl(self): # SuppressCrashReport +@hashlib_helper.requires_builtin_hashes() class TestHashlibSupport(unittest.TestCase): @classmethod @@ -828,11 +830,20 @@ def setUpClass(cls): cls.hashlib = import_helper.import_module("hashlib") cls.hmac = import_helper.import_module("hmac") - # We required the extension modules to be present since blocking - # HACL* implementations while allowing OpenSSL ones would still - # result in failures. + # All C extension modules must be present since blocking + # the built-in implementation while allowing OpenSSL or vice-versa + # may result in failures depending on the exposed built-in hashes. cls._hashlib = import_helper.import_module("_hashlib") cls._hmac = import_helper.import_module("_hmac") + cls._md5 = import_helper.import_module("_md5") + + def skip_if_fips_mode(self): + if self._hashlib.get_fips_mode(): + self.skipTest("disabled in FIPS mode") + + def skip_if_not_fips_mode(self): + if not self._hashlib.get_fips_mode(): + self.skipTest("requires FIPS mode") def check_context(self, disabled=True): if disabled: @@ -853,15 +864,9 @@ def try_import_attribute(self, fullname, default=None): except TypeError: return default - def validate_modules(self): - if hasattr(hashlib_helper, 'hashlib'): - self.assertIs(hashlib_helper.hashlib, self.hashlib) - if hasattr(hashlib_helper, 'hmac'): - self.assertIs(hashlib_helper.hmac, self.hmac) - - def fetch_hash_function(self, name, typ): + def fetch_hash_function(self, name, impl): entry = hashlib_helper._EXPLICIT_CONSTRUCTORS[name] - match typ: + match impl: case "hashlib": assert entry.hashlib is not None, entry return getattr(self.hashlib, entry.hashlib) @@ -870,8 +875,8 @@ def fetch_hash_function(self, name, typ): return getattr(self._hashlib, entry.openssl, None) except TypeError: return None - case "builtin": - return self.try_import_attribute(entry.fullname(typ)) + case _: + return self.try_import_attribute(entry.fullname(impl)) def fetch_hmac_function(self, name): fullname = hashlib_helper._EXPLICIT_HMAC_CONSTRUCTORS[name] @@ -936,16 +941,12 @@ def check_builtin_hmac(self, name, *, disabled=True): ) def test_disable_hash(self, name, allow_openssl, allow_builtin): # In FIPS mode, the function may be available but would still need - # to raise a ValueError. For simplicity, we don't test the helper - # when we're in FIPS mode. - if self._hashlib.get_fips_mode(): - self.skipTest("hash functions may still be blocked in FIPS mode") + # to raise a ValueError, so we will test the helper separately. + self.skip_if_fips_mode() flags = dict(allow_openssl=allow_openssl, allow_builtin=allow_builtin) - is_simple_disabled = not allow_builtin and not allow_openssl + is_fully_disabled = not allow_builtin and not allow_openssl with hashlib_helper.block_algorithm(name, **flags): - self.validate_modules() - # OpenSSL's blake2s and blake2b are unknown names # when only the OpenSSL interface is available. if allow_openssl and not allow_builtin: @@ -954,25 +955,104 @@ def test_disable_hash(self, name, allow_openssl, allow_builtin): else: name_for_hashlib_new = name - with self.check_context(is_simple_disabled): + with self.check_context(is_fully_disabled): _ = self.hashlib.new(name_for_hashlib_new) - with self.check_context(is_simple_disabled): - _ = getattr(self.hashlib, name)(b"") + + # Since _hashlib is present, explicit blake2b/blake2s constructors + # use the built-in implementation, while others (since we are not + # in FIPS mode and since _hashlib exists) use the OpenSSL function. + with self.check_context(is_fully_disabled): + _ = getattr(self.hashlib, name)() self.check_openssl_hash(name, disabled=not allow_openssl) self.check_builtin_hash(name, disabled=not allow_builtin) if name not in hashlib_helper.NON_HMAC_DIGEST_NAMES: - with self.check_context(is_simple_disabled): + with self.check_context(is_fully_disabled): _ = self.hmac.new(b"", b"", name) - with self.check_context(is_simple_disabled): + with self.check_context(is_fully_disabled): _ = self.hmac.HMAC(b"", b"", name) - with self.check_context(is_simple_disabled): + with self.check_context(is_fully_disabled): _ = self.hmac.digest(b"", b"", name) self.check_openssl_hmac(name, disabled=not allow_openssl) self.check_builtin_hmac(name, disabled=not allow_builtin) + @hashlib_helper.block_algorithm("md5") + def test_disable_hash_md5_in_fips_mode(self): + self.skip_if_not_fips_mode() + + self.assertRaises(ValueError, self.hashlib.new, "md5") + self.assertRaises(ValueError, self._hashlib.new, "md5") + self.assertRaises(ValueError, self.hashlib.md5) + self.assertRaises(ValueError, self._hashlib.openssl_md5) + + kwargs = dict(usedforsecurity=True) + self.assertRaises(ValueError, self.hashlib.new, "md5", **kwargs) + self.assertRaises(ValueError, self._hashlib.new, "md5", **kwargs) + self.assertRaises(ValueError, self.hashlib.md5, **kwargs) + self.assertRaises(ValueError, self._hashlib.openssl_md5, **kwargs) + + @hashlib_helper.block_algorithm("md5", allow_openssl=True) + def test_disable_hash_md5_in_fips_mode_allow_openssl(self): + self.skip_if_not_fips_mode() + # Allow the OpenSSL interface to be used but not the HACL* one. + # hashlib.new("md5") is dispatched to hashlib.openssl_md5() + self.assertRaises(ValueError, self.hashlib.new, "md5") + # dispatched to hashlib.openssl_md5() in FIPS mode + h2 = self.hashlib.new("md5", usedforsecurity=False) + self.assertIsInstance(h2, self._hashlib.HASH) + + # block_algorithm() does not mock hashlib.md5 and _hashlib.openssl_md5 + self.assertNotHasAttr(self.hashlib.md5, "__wrapped__") + self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__") + + hashlib_md5 = inspect.unwrap(self.hashlib.md5) + self.assertIs(hashlib_md5, self._hashlib.openssl_md5) + self.assertRaises(ValueError, self.hashlib.md5) + # allow MD5 to be used in FIPS mode if usedforsecurity=False + h3 = self.hashlib.md5(usedforsecurity=False) + self.assertIsInstance(h3, self._hashlib.HASH) + + @hashlib_helper.block_algorithm("md5", allow_builtin=True) + def test_disable_hash_md5_in_fips_mode_allow_builtin(self): + self.skip_if_not_fips_mode() + # Allow the HACL* interface to be used but not the OpenSSL one. + h1 = self.hashlib.new("md5") # dispatched to _md5.md5() + self.assertNotIsInstance(h1, self._hashlib.HASH) + h2 = self.hashlib.new("md5", usedforsecurity=False) + self.assertIsInstance(h2, type(h1)) + + # block_algorithm() mocks hashlib.md5 and _hashlib.openssl_md5 + self.assertHasAttr(self.hashlib.md5, "__wrapped__") + self.assertHasAttr(self._hashlib.openssl_md5, "__wrapped__") + + hashlib_md5 = inspect.unwrap(self.hashlib.md5) + openssl_md5 = inspect.unwrap(self._hashlib.openssl_md5) + self.assertIs(hashlib_md5, openssl_md5) + self.assertRaises(ValueError, self.hashlib.md5) + self.assertRaises(ValueError, self.hashlib.md5, + usedforsecurity=False) + + @hashlib_helper.block_algorithm("md5", + allow_openssl=True, + allow_builtin=True) + def test_disable_hash_md5_in_fips_mode_allow_all(self): + self.skip_if_not_fips_mode() + # hashlib.new() isn't blocked as it falls back to _md5.md5 + self.assertIsInstance(self.hashlib.new("md5"), self._md5.MD5Type) + self.assertRaises(ValueError, self._hashlib.new, "md5") + h = self._hashlib.new("md5", usedforsecurity=False) + self.assertIsInstance(h, self._hashlib.HASH) + + self.assertNotHasAttr(self.hashlib.md5, "__wrapped__") + self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__") + + self.assertIs(self.hashlib.md5, self._hashlib.openssl_md5) + self.assertRaises(ValueError, self.hashlib.md5) + h = self.hashlib.md5(usedforsecurity=False) + self.assertIsInstance(h, self._hashlib.HASH) + if __name__ == '__main__': unittest.main() From 548c2f38be854039b7a2adaceca91a9a836ba7f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:12:30 +0200 Subject: [PATCH 02/13] remove unused import --- Lib/test/support/hashlib_helper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index bf9b5bcc7ff2f0..51c013b8443068 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -1,6 +1,5 @@ import contextlib import functools -import hashlib import importlib import inspect import unittest From 4dfca50da9804dc8ffae59fe40dbd315f838faf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:29:20 +0200 Subject: [PATCH 03/13] fix name error --- Lib/test/support/hashlib_helper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 51c013b8443068..4df0ba23dfb677 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -217,12 +217,12 @@ def _openssl_new(digestname, /, **kwargs): # re-import '_hashlib' in case it was mocked _hashlib = importlib.import_module("_hashlib") except ImportError as exc: - raise SkipNoHash(fullname, "openssl") from exc + raise SkipNoHash(digestname, "openssl") from exc constructor = partial(_hashlib.new, digestname, **kwargs) try: constructor() except ValueError as exc: - raise SkipNoHash(fullname, "_hashlib.new") from exc + raise SkipNoHash(digestname, "_hashlib.new") from exc return constructor From 928ce67275776c8ee5b157f132cdbb532d1d9913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:50:36 +0200 Subject: [PATCH 04/13] fix WASI --- Lib/test/support/hashlib_helper.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 4df0ba23dfb677..401667aa0c7f6b 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -140,15 +140,14 @@ def _ensure_wrapper_signature(wrapper, wrapped): ) -def _requires_module(name): - def decorator_func(func): - module = try_import_module(name, missing := object()) - return unittest.skipIf(module is missing, f"requires {name}")(func) - return partial(_decorate_func_or_class, decorator_func) +def requires_hashlib(): + _hashlib = try_import_module("_hashlib") + return unittest.skipIf(_hashlib is None, "requires _hashlib") -requires_hashlib = partial(_requires_module, "_hashlib") -requires_builtin_hmac = partial(_requires_module, "_hmac") +def requires_builtin_hmac(): + _hmac = try_import_module("_hmac") + return unittest.skipIf(_hmac is None, "requires _hmac") class SkipNoHash(unittest.SkipTest): From 34f8edd6b950cf6e569d9ee60ba2a956f04684f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:41:44 +0200 Subject: [PATCH 05/13] simplifications --- Lib/test/support/hashlib_helper.py | 311 ++++++++++++++++++----------- Lib/test/test_support.py | 16 +- 2 files changed, 203 insertions(+), 124 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 401667aa0c7f6b..741f0a87df045b 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -1,4 +1,5 @@ import contextlib +import enum import functools import importlib import inspect @@ -10,83 +11,173 @@ from types import MappingProxyType -def try_import_module(name, default=None): +def try_import_module(module_name): + """Try to import a module and return None on failure.""" try: - return importlib.import_module(name) + return importlib.import_module(module_name) except ImportError: return None -CANONICAL_DIGEST_NAMES = frozenset(( - 'md5', 'sha1', - 'sha224', 'sha256', 'sha384', 'sha512', - 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', - 'shake_128', 'shake_256', - 'blake2s', 'blake2b', -)) +class HID(enum.StrEnum): + """Enumeration containing the canonical digest names. -NON_HMAC_DIGEST_NAMES = frozenset({ - 'shake_128', 'shake_256', - 'blake2s', 'blake2b', -}) + 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() -class HashAPI(namedtuple("HashAPI", "builtin openssl hashlib")): + 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 builtin_module_name(self): - return self.builtin.split(".", maxsplit=1)[0] + def is_xof(self): + """Indicate whether the hash is an extendable-output hash function.""" + return self.startswith("shake_") @property - def builtin_method_name(self): - return self.builtin.split(".", maxsplit=1)[1] + 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(( # fmt: skip + 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". + + it must be set to "None", which is the case for BLAKE2b and BLAKE2s. + - *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) + ) - def fullname(self, impl): - match impl: + 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 + return self.builtin_module_name case "openssl": - return f"_hashlib.{self.openssl}" if self.openssl else None + return self.openssl_module_name case "hashlib": - return f"hashlib.{self.hashlib}" if self.hashlib else None - case _: - raise AssertionError(f"unknown implementation: {impl}") + 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({ - "md5": HashAPI("_md5.md5", "openssl_md5", "md5"), - "sha1": HashAPI("_sha1.sha1", "openssl_sha1", "sha1"), - "sha224": HashAPI("_sha2.sha224", "openssl_sha224", "sha224"), - "sha256": HashAPI("_sha2.sha256", "openssl_sha256", "sha256"), - "sha384": HashAPI("_sha2.sha384", "openssl_sha384", "sha384"), - "sha512": HashAPI("_sha2.sha512", "openssl_sha512", "sha512"), - "sha3_224": HashAPI("_sha3.sha3_224", "openssl_sha3_224", "sha3_224"), - "sha3_256": HashAPI("_sha3.sha3_256", "openssl_sha3_256", "sha3_256"), - "sha3_384": HashAPI("_sha3.sha3_384", "openssl_sha3_384", "sha3_384"), - "sha3_512": HashAPI("_sha3.sha3_512", "openssl_sha3_512", "sha3_512"), - "shake_128": HashAPI("_sha3.shake_128", "openssl_shake_128", "shake_128"), - "shake_256": HashAPI("_sha3.shake_256", "openssl_shake_256", "shake_256"), - "blake2s": HashAPI("_blake2.blake2s", None, "blake2s"), - "blake2b": HashAPI("_blake2.blake2b", None, "blake2b"), +_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 = { - name: f'_hmac.compute_{name}' for name in ( - 'md5', 'sha1', - 'sha224', 'sha256', 'sha384', 'sha512', - 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', - ) + hid: None if hid.is_xof else f"_hmac.compute_{hid!s}" + # names slightly differ for keyed hash functions + for hid in HID if not hid.is_keyed } -_EXPLICIT_HMAC_CONSTRUCTORS['shake_128'] = None -_EXPLICIT_HMAC_CONSTRUCTORS['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['blake2s'] = '_hmac.compute_blake2s_32' -_EXPLICIT_HMAC_CONSTRUCTORS['blake2b'] = '_hmac.compute_blake2b_32' +_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 @@ -151,9 +242,12 @@ def requires_builtin_hmac(): class SkipNoHash(unittest.SkipTest): + """A SkipTest exception raised when a hash is not available.""" - def __init__(self, digestname, implementation=None): + 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))) @@ -165,6 +259,7 @@ def _hashlib_new(digestname, openssl, /, **kwargs): The constructor function is returned, 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") @@ -175,13 +270,13 @@ def _hashlib_new(digestname, openssl, /, **kwargs): try: constructor() except ValueError: - implementation = f"{mod.__name__}.{new.__name__}" - raise SkipNoHash(digestname, implementation) from exc + interface = f"{mod.__name__}.{new.__name__}" + raise SkipNoHash(digestname, interface=interface) from exc return constructor def _builtin_hash(module_name, digestname, /, **kwargs): - """Check availability of module_name.digestname(**kwargs). + """Check availability of .(**kwargs). - The *module_name* is the C extension module name based on HACL*. - The *digestname* is one of its member, e.g., 'md5'. @@ -221,11 +316,11 @@ def _openssl_new(digestname, /, **kwargs): try: constructor() except ValueError as exc: - raise SkipNoHash(digestname, "_hashlib.new") from exc + raise SkipNoHash(digestname, interface="_hashlib.new") from exc return constructor -def _get_openssl_hash_constructor(digestname, **kwargs): +def _openssl_hash(digestname, /, **kwargs): """Check availability of _hashlib.openssl_(**kwargs). The constructor function is returned, or SkipTest is raised if none exists. @@ -248,11 +343,11 @@ def _get_openssl_hash_constructor(digestname, **kwargs): return constructor -def _make_requires_hashdigest_decorator(check_availability): +def _make_requires_hashdigest_decorator(test, /, *test_args, **test_kwargs): def decorator_func(func): @functools.wraps(func) def wrapper(*args, **kwargs): - check_availability() + test(*test_args, **test_kwargs) return func(*args, **kwargs) return wrapper return partial(_decorate_func_or_class, decorator_func) @@ -275,10 +370,9 @@ def requires_hashdigest(digestname, openssl=None, *, usedforsecurity=True): ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS ValueError: unsupported hash type md4 """ - def check_availability(): - _hashlib_new(digestname, openssl, usedforsecurity=usedforsecurity) - - return _make_requires_hashdigest_decorator(check_availability) + return _make_requires_hashdigest_decorator( + _hashlib_new, digestname, openssl, usedforsecurity=usedforsecurity + ) def requires_openssl_hashdigest(digestname, *, usedforsecurity=True): @@ -286,10 +380,9 @@ def requires_openssl_hashdigest(digestname, *, usedforsecurity=True): The hashing algorithm may be missing or blocked by a strict crypto policy. """ - def check_availability(): - _openssl_new(digestname, usedforsecurity=usedforsecurity) - - return _make_requires_hashdigest_decorator(check_availability) + return _make_requires_hashdigest_decorator( + _openssl_new, digestname, usedforsecurity=usedforsecurity + ) def requires_builtin_hashdigest( @@ -300,10 +393,9 @@ def requires_builtin_hashdigest( - The *module_name* is the C extension module name based on HACL*. - The *digestname* is one of its member, e.g., 'md5'. """ - def check_availability(): - _builtin_hash(module_name, digestname, usedforsecurity=usedforsecurity) - - return _make_requires_hashdigest_decorator(check_availability) + return _make_requires_hashdigest_decorator( + _builtin_hash, module_name, digestname, usedforsecurity=usedforsecurity + ) def requires_builtin_hashes(*ignored, usedforsecurity=True): @@ -312,7 +404,7 @@ def requires_builtin_hashes(*ignored, usedforsecurity=True): requires_builtin_hashdigest( api.builtin_module_name, api.builtin_method_name, - usedforsecurity=usedforsecurity + usedforsecurity=usedforsecurity, ) for name, api in _EXPLICIT_CONSTRUCTORS.items() if name not in ignored @@ -416,8 +508,7 @@ def _find_constructor(self, digestname): self.is_valid_digest_name(digestname) # This returns a function of the form _hashlib.openssl_ and # not a lambda function as it is rejected by _hashlib.hmac_new(). - return _get_openssl_hash_constructor( - digestname, usedforsecurity=self.usedforsecurity) + return _openssl_hash(digestname, usedforsecurity=self.usedforsecurity) class BuiltinHashFunctionsTrait(HashFunctionsTrait): @@ -430,12 +521,12 @@ class BuiltinHashFunctionsTrait(HashFunctionsTrait): def _find_constructor(self, digestname): self.is_valid_digest_name(digestname) - info = _EXPLICIT_CONSTRUCTORS[digestname].builtin - self.assertIsNotNone(info, f"no built-in implementation " - f"for {digestname!r}") - module_name, digestname = info.split('.', maxsplit=1) + info = _EXPLICIT_CONSTRUCTORS[digestname] return _builtin_hash( - module_name, digestname, usedforsecurity=self.usedforsecurity) + info.builtin_module_name, + info.builtin_method_name, + usedforsecurity=self.usedforsecurity, + ) def find_gil_minsize(modules_names, default=2048): @@ -458,6 +549,7 @@ def find_gil_minsize(modules_names, default=2048): 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() @@ -500,22 +592,19 @@ def wrapper(key, msg, digest): return unittest.mock.patch('_hashlib.hmac_digest', wrapper) -@contextlib.contextmanager 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 _EXPLICIT_CONSTRUCTORS, name + assert name in HID, f"invalid hash: {name}" # Re-import 'hashlib' in case it was mocked, but propagate # exceptions as it should be unconditionally available. - hashlib = importlib.import_module("hashlib") - builtin_cache = getattr(hashlib, '__builtin_constructor_cache') - if name in builtin_cache: - f = builtin_cache.pop(name) - F = builtin_cache.pop(name.upper(), None) - else: - f = F = None + 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 @@ -523,22 +612,17 @@ def _block_builtin_hash_new(name): get_builtin_constructor = getattr(hashlib, '__get_builtin_constructor') builtin_module_name = _EXPLICIT_CONSTRUCTORS[name].builtin_module_name - def get_builtin_constructor_wrapper(name): + 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) - try: - setattr(hashlib, "__get_builtin_constructor", - get_builtin_constructor_wrapper) - yield - finally: - setattr(hashlib, "__get_builtin_constructor", get_builtin_constructor) - if f is not None: - builtin_cache[name] = f - if F is not None: - builtin_cache[name.upper()] = F + 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): @@ -569,55 +653,50 @@ def wrapper(key, msg, digest): return unittest.mock.patch('_hmac.compute_digest', wrapper) -def _make_hash_constructor_blocker(name, dummy, *, interface): - assert isinstance(name, str), name - assert interface in ('builtin', 'openssl', 'hashlib') - assert name in _EXPLICIT_CONSTRUCTORS, f"invalid hash: {name}" - fullname = _EXPLICIT_CONSTRUCTORS[name].fullname(interface) - if fullname is None: +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() - assert fullname.count('.') == 1, fullname - module_name, method = fullname.split('.', maxsplit=1) + try: module = importlib.import_module(module_name) except ImportError: # module is already disabled return contextlib.nullcontext() - wrapped = getattr(module, method) + + wrapped = getattr(module, method_name) wrapper = functools.wraps(wrapped)(dummy) _ensure_wrapper_signature(wrapper, wrapped) - return unittest.mock.patch(fullname, wrapper) + return unittest.mock.patch(info.fullname(implementation), wrapper) def _block_hashlib_hash_constructor(name): """Block explicit public constructors.""" - assert isinstance(name, str), name def dummy(data=b'', *, usedforsecurity=True, string=None): raise ValueError(f"blocked explicit public hash name: {name}") - return _make_hash_constructor_blocker(name, dummy, interface='hashlib') + + return _make_hash_constructor_blocker(name, dummy, 'hashlib') def _block_openssl_hash_constructor(name): """Block explicit OpenSSL constructors.""" - assert isinstance(name, str), name def dummy(data=b'', *, usedforsecurity=True, string=None): raise ValueError(f"blocked explicit OpenSSL hash name: {name}") - return _make_hash_constructor_blocker(name, dummy, interface='openssl') + return _make_hash_constructor_blocker(name, dummy, 'openssl') def _block_builtin_hash_constructor(name): """Block explicit HACL* constructors.""" - assert isinstance(name, str), name def dummy(data=b'', *, usedforsecurity=True, string=b''): raise ValueError(f"blocked explicit builtin hash name: {name}") - return _make_hash_constructor_blocker(name, dummy, interface='builtin') + return _make_hash_constructor_blocker(name, dummy, 'builtin') def _block_builtin_hmac_constructor(name): """Block explicit HACL* HMAC constructors.""" - assert isinstance(name, str), name - assert name in _EXPLICIT_HMAC_CONSTRUCTORS, f"invalid hash: {name}" fullname = _EXPLICIT_HMAC_CONSTRUCTORS[name] if fullname is None: # function shouldn't exist for this implementation @@ -645,7 +724,7 @@ def block_algorithm(name, *, allow_openssl=False, allow_builtin=False): 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): + 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 diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index ce195ac4427147..92b3ef26cd979a 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -864,19 +864,19 @@ def try_import_attribute(self, fullname, default=None): except TypeError: return default - def fetch_hash_function(self, name, impl): - entry = hashlib_helper._EXPLICIT_CONSTRUCTORS[name] - match impl: + def fetch_hash_function(self, name, implementation): + info = hashlib_helper.get_hash_info(name) + match implementation: case "hashlib": - assert entry.hashlib is not None, entry - return getattr(self.hashlib, entry.hashlib) + assert info.hashlib is not None, info + return getattr(self.hashlib, info.hashlib) case "openssl": try: - return getattr(self._hashlib, entry.openssl, None) + return getattr(self._hashlib, info.openssl, None) except TypeError: return None - case _: - return self.try_import_attribute(entry.fullname(impl)) + fullname = info.fullname(implementation) + return self.try_import_attribute(fullname) def fetch_hmac_function(self, name): fullname = hashlib_helper._EXPLICIT_HMAC_CONSTRUCTORS[name] From c4ea94ab20f544e5c72be0cc64573a1fa326c78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:45:36 +0200 Subject: [PATCH 06/13] remove unused import --- Lib/test/support/hashlib_helper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 741f0a87df045b..ce748d97c67164 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -5,7 +5,6 @@ import inspect import unittest import unittest.mock -from collections import namedtuple from functools import partial from test.support import import_helper from types import MappingProxyType From 96d6de30615934ae279c702e63e6fbc4a86bfabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:58:13 +0200 Subject: [PATCH 07/13] better comment --- Lib/test/support/hashlib_helper.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index ce748d97c67164..76179e49e6e1f5 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -169,10 +169,12 @@ def fullname(self, implementation): # There is currently no OpenSSL one-shot named function and there will likely # be none in the future. _EXPLICIT_HMAC_CONSTRUCTORS = { - hid: None if hid.is_xof else f"_hmac.compute_{hid!s}" - # names slightly differ for keyed hash functions - for hid in HID if not hid.is_keyed + 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" From f5b3a35547ef0aff6eee3659ecb1e4934636151c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 19 Jul 2025 12:00:09 +0200 Subject: [PATCH 08/13] less diff --- Lib/test/support/hashlib_helper.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 76179e49e6e1f5..ca4d69f63506cd 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -177,8 +177,8 @@ def fullname(self, implementation): _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[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 @@ -550,7 +550,6 @@ def find_gil_minsize(modules_names, default=2048): 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() @@ -599,8 +598,7 @@ def _block_builtin_hash_new(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, but propagate - # exceptions as it should be unconditionally available. + # 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() From 0ad45806ce5cc5389cf76e10219acd250b63e244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 19 Jul 2025 19:47:31 +0200 Subject: [PATCH 09/13] fix comment --- Lib/test/support/hashlib_helper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index ca4d69f63506cd..7200f59b8af61a 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -73,8 +73,6 @@ class HashInfo: - *openssl* is the name of the "_hashlib" module method for the explicit OpenSSL hash constructor function, e.g., "openssl_md5". - - it must be set to "None", which is the case for BLAKE2b and BLAKE2s. - *hashlib* is the name of the "hashlib" module method for the explicit hash constructor function, e.g., "md5". """ From 28a603de66bafdfa95b251b84a2af509d01ad4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 19 Jul 2025 19:52:16 +0200 Subject: [PATCH 10/13] update comments --- Lib/test/support/hashlib_helper.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 7200f59b8af61a..d2dc1a80f51ec4 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -59,7 +59,7 @@ def is_keyed(self): CANONICAL_DIGEST_NAMES = frozenset(map(str, HID.__members__)) -NON_HMAC_DIGEST_NAMES = frozenset(( # fmt: skip +NON_HMAC_DIGEST_NAMES = frozenset(( HID.shake_128, HID.shake_256, HID.blake2s, HID.blake2b, )) @@ -73,6 +73,7 @@ class HashInfo: - *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". """ @@ -244,7 +245,7 @@ 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}"] + parts = ["missing", implementation, f"hash algorithm {digestname!r}"] if interface is not None: parts.append(f"for {interface}") super().__init__(" ".join(filter(None, parts))) From 2419aeeec2c31582b8df78296070fd29b7eb86f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 20 Jul 2025 11:37:36 +0200 Subject: [PATCH 11/13] consistent constructor --- Lib/test/support/hashlib_helper.py | 72 +++++++++++++++++++----------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index d2dc1a80f51ec4..a4e175f6e49c16 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -5,7 +5,6 @@ import inspect import unittest import unittest.mock -from functools import partial from test.support import import_helper from types import MappingProxyType @@ -207,7 +206,7 @@ def _chain_decorators(decorators): """ def decorator_func(func): return functools.reduce(lambda w, deco: deco(w), decorators, func) - return partial(_decorate_func_or_class, decorator_func) + return functools.partial(_decorate_func_or_class, decorator_func) def _ensure_wrapper_signature(wrapper, wrapped): @@ -257,7 +256,8 @@ def _hashlib_new(digestname, openssl, /, **kwargs): If *openssl* is True, module is "_hashlib" (C extension module), otherwise it is "hashlib" (pure Python interface). - The constructor function is returned, or SkipTest is raised if none exists. + 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 @@ -265,14 +265,13 @@ def _hashlib_new(digestname, openssl, /, **kwargs): hashlib = importlib.import_module("hashlib") # re-import '_hashlib' in case it was mocked _hashlib = try_import_module("_hashlib") - mod = _hashlib if openssl and _hashlib is not None else hashlib - constructor = partial(mod.new, digestname, **kwargs) + module = _hashlib if openssl and _hashlib is not None else hashlib try: - constructor() + module.new(digestname, **kwargs) except ValueError: - interface = f"{mod.__name__}.{new.__name__}" + interface = f"{module.__name__}.new" raise SkipNoHash(digestname, interface=interface) from exc - return constructor + return functools.partial(module.new, digestname) def _builtin_hash(module_name, digestname, /, **kwargs): @@ -304,7 +303,8 @@ def _builtin_hash(module_name, digestname, /, **kwargs): def _openssl_new(digestname, /, **kwargs): """Check availability of _hashlib.new(digestname, **kwargs). - The constructor function is returned, or SkipTest is raised if none exists. + The constructor function is returned (without binding **kwargs), + or SkipTest is raised if none exists. """ assert isinstance(digestname, str), digestname try: @@ -312,18 +312,18 @@ def _openssl_new(digestname, /, **kwargs): _hashlib = importlib.import_module("_hashlib") except ImportError as exc: raise SkipNoHash(digestname, "openssl") from exc - constructor = partial(_hashlib.new, digestname, **kwargs) try: - constructor() + _hashlib.new(digestname, **kwargs) except ValueError as exc: raise SkipNoHash(digestname, interface="_hashlib.new") from exc - return constructor + return functools.partial(_hashlib.new, digestname) def _openssl_hash(digestname, /, **kwargs): """Check availability of _hashlib.openssl_(**kwargs). - The constructor function is returned, or SkipTest is raised if none exists. + 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}" @@ -350,7 +350,7 @@ def wrapper(*args, **kwargs): test(*test_args, **test_kwargs) return func(*args, **kwargs) return wrapper - return partial(_decorate_func_or_class, decorator_func) + return functools.partial(_decorate_func_or_class, decorator_func) def requires_hashdigest(digestname, openssl=None, *, usedforsecurity=True): @@ -430,7 +430,9 @@ class HashFunctionsTrait: 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', ] - # Default 'usedforsecurity' to use when looking up a hash function. + # 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 @@ -549,29 +551,35 @@ def find_gil_minsize(modules_names, default=2048): 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 wrapper(name, data=b'', *, usedforsecurity=True, string=None): + def _hashlib_new(name, data=b'', *, usedforsecurity=True, string=None): if name == blocked_name: raise _hashlib.UnsupportedDigestmodError(blocked_name) return wrapped(*args, **kwargs) - _ensure_wrapper_signature(wrapper, wrapped) - return unittest.mock.patch('_hashlib.new', wrapper) + + _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) @@ -579,16 +587,19 @@ def wrapper(key, msg=b'', digestmod=None): 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 wrapper(key, msg, digest): + def _hashlib_hmac_digest(key, msg, digest): if digest == blocked_name: raise _hashlib.UnsupportedDigestmodError(blocked_name) return wrapped(key, msg, digestmod) - _ensure_wrapper_signature(wrapper, wrapped) - return unittest.mock.patch('_hashlib.hmac_digest', wrapper) + + _ensure_wrapper_signature(_hashlib_hmac_digest, wrapped) + return unittest.mock.patch('_hashlib.hmac_digest', _hashlib_hmac_digest) def _block_builtin_hash_new(name): @@ -610,6 +621,7 @@ def _block_builtin_hash_new(name): 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") @@ -625,30 +637,36 @@ def get_builtin_constructor_mock(name): 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 wrapper(key, msg=None, digestmod=None): + 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(wrapper, wrapped) - return unittest.mock.patch('_hmac.new', wrapper) + + _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 wrapper(key, msg, 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(wrapper, wrapped) - return unittest.mock.patch('_hmac.compute_digest', wrapper) + + _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): From 2b8d5f0ddb8f5748857db6b135fe94f44a780fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 20 Jul 2025 11:40:53 +0200 Subject: [PATCH 12/13] fix names --- Lib/test/support/hashlib_helper.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index a4e175f6e49c16..1e7288bfc040ff 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -560,7 +560,8 @@ def _block_openssl_hash_new(blocked_name): def _hashlib_new(name, data=b'', *, usedforsecurity=True, string=None): if name == blocked_name: raise _hashlib.UnsupportedDigestmodError(blocked_name) - return wrapped(*args, **kwargs) + return wrapped(name, data, + usedforsecurity=usedforsecurity, string=string) _ensure_wrapper_signature(_hashlib_new, wrapped) return unittest.mock.patch('_hashlib.new', _hashlib_new) @@ -596,7 +597,7 @@ def _block_openssl_hmac_digest(blocked_name): def _hashlib_hmac_digest(key, msg, digest): if digest == blocked_name: raise _hashlib.UnsupportedDigestmodError(blocked_name) - return wrapped(key, msg, digestmod) + return wrapped(key, msg, digest) _ensure_wrapper_signature(_hashlib_hmac_digest, wrapped) return unittest.mock.patch('_hashlib.hmac_digest', _hashlib_hmac_digest) From f4bf516aba1c3e684e3d57c1c7de1e2fdd2beb4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 20 Jul 2025 13:49:38 +0200 Subject: [PATCH 13/13] fix raise --- Lib/test/support/hashlib_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 1e7288bfc040ff..96be74e4105c18 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -268,7 +268,7 @@ def _hashlib_new(digestname, openssl, /, **kwargs): module = _hashlib if openssl and _hashlib is not None else hashlib try: module.new(digestname, **kwargs) - except ValueError: + except ValueError as exc: interface = f"{module.__name__}.new" raise SkipNoHash(digestname, interface=interface) from exc return functools.partial(module.new, digestname)