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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy