From 19792cbce22df36bce855b2528fe22a81a32cb19 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, 12 Jul 2025 12:57:25 +0200 Subject: [PATCH 1/4] allow to temporarily disable hash algorithms in tests --- Lib/test/support/hashlib_helper.py | 312 +++++++++++++++++++++++++++-- Lib/test/test_support.py | 167 +++++++++++++++ 2 files changed, 466 insertions(+), 13 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 7032257b06877a..5c4b754df1bb87 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -1,8 +1,13 @@ +import contextlib import functools import hashlib import importlib +import inspect import unittest +import unittest.mock +from collections import namedtuple from test.support.import_helper import import_module +from types import MappingProxyType try: import _hashlib @@ -15,6 +20,91 @@ _hmac = 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', +)) + +NON_HMAC_DIGEST_NAMES = frozenset({ + 'shake_128', 'shake_256', 'blake2s', 'blake2b' +}) + +# 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. + +class HashAPI(namedtuple("HashAPI", "builtin openssl hashlib")): + + def fullname(self, typ): + match typ: + case "builtin": + return self.builtin + case "openssl": + return f"_hashlib.{self.openssl}" if self.openssl else None + case "hashlib": + return f"hashlib.{self.hashlib}" if self.hashlib else None + case _: + raise AssertionError(f"unknown type: {typ}") + + +_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"), +}) + +assert _EXPLICIT_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES + +_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', + ) +} +_EXPLICIT_HMAC_CONSTRUCTORS['shake_128'] = None +_EXPLICIT_HMAC_CONSTRUCTORS['shake_256'] = None +_EXPLICIT_HMAC_CONSTRUCTORS['blake2s'] = '_hmac.compute_blake2s_32' +_EXPLICIT_HMAC_CONSTRUCTORS['blake2b'] = '_hmac.compute_blake2b_32' +_EXPLICIT_HMAC_CONSTRUCTORS = MappingProxyType(_EXPLICIT_HMAC_CONSTRUCTORS) +assert _EXPLICIT_HMAC_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES + + +def _ensure_wrapper_signature(wrapper, wrapped): + """Ensure that a wrapper has the same signature as the wrapped function. + + This is used to guarantee that a TypeError raised due to a bad API call + is raised consistently (using variadic signatures would hide such errors). + """ + try: + wrapped_sig = inspect.signature(wrapped) + except ValueError: # built-in signature cannot be found + return + + wrapper_sig = inspect.signature(wrapper) + if wrapped_sig != wrapper_sig: + fullname = f"{wrapped.__module__}.{wrapped.__qualname__}" + raise AssertionError( + f"signature for {fullname}() is incorrect:\n" + f" expect: {wrapped_sig}\n" + f" actual: {wrapper_sig}" + ) + + def requires_hashlib(): return unittest.skipIf(_hashlib is None, "requires _hashlib") @@ -30,6 +120,7 @@ def _missing_hash(digestname, implementation=None, *, exc=None): def _openssl_availabillity(digestname, *, usedforsecurity): + assert isinstance(digestname, str), digestname try: _hashlib.new(digestname, usedforsecurity=usedforsecurity) except AttributeError: @@ -74,6 +165,7 @@ 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) @@ -101,6 +193,7 @@ 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) @@ -131,6 +224,7 @@ 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): @@ -156,6 +250,7 @@ def find_builtin_hashdigest_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) @@ -178,7 +273,7 @@ class HashFunctionsTrait: implementation of HMAC). """ - ALGORITHMS = [ + DIGEST_NAMES = [ 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', @@ -187,10 +282,18 @@ class HashFunctionsTrait: # Default 'usedforsecurity' to use when looking up a hash function. usedforsecurity = True - def _find_constructor(self, name): + @classmethod + def setUpClass(cls): + super().setUpClass() + assert CANONICAL_DIGEST_NAMES.issuperset(cls.DIGEST_NAMES) + + def is_valid_digest_name(self, digestname): + self.assertIn(digestname, self.DIGEST_NAMES) + + def _find_constructor(self, digestname): # By default, a missing algorithm skips the test that uses it. - self.assertIn(name, self.ALGORITHMS) - self.skipTest(f"missing hash function: {name}") + self.is_valid_digest_name(digestname) + self.skipTest(f"missing hash function: {digestname}") @property def md5(self): @@ -239,9 +342,9 @@ class NamedHashFunctionsTrait(HashFunctionsTrait): Hash functions are available if and only if they are available in hashlib. """ - def _find_constructor(self, name): - self.assertIn(name, self.ALGORITHMS) - return name + def _find_constructor(self, digestname): + self.is_valid_digest_name(digestname) + return digestname class OpenSSLHashFunctionsTrait(HashFunctionsTrait): @@ -250,10 +353,10 @@ class OpenSSLHashFunctionsTrait(HashFunctionsTrait): Hash functions are available if and only if they are available in _hashlib. """ - def _find_constructor(self, name): - self.assertIn(name, self.ALGORITHMS) + def _find_constructor(self, digestname): + self.is_valid_digest_name(digestname) return find_openssl_hashdigest_constructor( - name, usedforsecurity=self.usedforsecurity + digestname, usedforsecurity=self.usedforsecurity ) @@ -265,9 +368,9 @@ class BuiltinHashFunctionsTrait(HashFunctionsTrait): is not since the former is unconditionally built. """ - def _find_constructor_in(self, module, name): - self.assertIn(name, self.ALGORITHMS) - return find_builtin_hashdigest_constructor(module, name) + def _find_constructor_in(self, module, digestname): + self.is_valid_digest_name(digestname) + return find_builtin_hashdigest_constructor(module, digestname) @property def md5(self): @@ -327,3 +430,186 @@ def find_gil_minsize(modules_names, default=2048): continue 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: + return contextlib.nullcontext() + @functools.wraps(wrapped := _hashlib.new) + def wrapper(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) + + +def _block_openssl_hmac_new(blocked_name): + """Block OpenSSL HMAC-HASH implementation.""" + assert isinstance(blocked_name, str), blocked_name + if _hashlib is None: + return contextlib.nullcontext() + @functools.wraps(wrapped := _hashlib.hmac_new) + def wrapper(key, msg=b'', digestmod=None): + if digestmod == blocked_name: + raise _hashlib.UnsupportedDigestmodError(blocked_name) + return wrapped(key, msg, digestmod) + _ensure_wrapper_signature(wrapper, wrapped) + return unittest.mock.patch('_hashlib.hmac_new', wrapper) + + +def _block_openssl_hmac_digest(blocked_name): + """Block OpenSSL HMAC-HASH one-shot digest implementation.""" + assert isinstance(blocked_name, str), blocked_name + if _hashlib is None: + return contextlib.nullcontext() + @functools.wraps(wrapped := _hashlib.hmac_digest) + def wrapper(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) + + +@contextlib.contextmanager +def _block_builtin_hash_new(name): + assert isinstance(name, str), name + assert name.lower() == name, f"invalid name: {name}" + + 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 + try: + yield + finally: + if f is not None: + builtin_cache[name] = f + if F is not None: + builtin_cache[name.upper()] = F + + +def _block_builtin_hmac_new(blocked_name): + assert isinstance(blocked_name, str), blocked_name + if _hmac is None: + return contextlib.nullcontext() + @functools.wraps(wrapped := _hmac.new) + def wrapper(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) + + +def _block_builtin_hmac_digest(blocked_name): + assert isinstance(blocked_name, str), blocked_name + if _hmac is None: + return contextlib.nullcontext() + @functools.wraps(wrapped := _hmac.compute_digest) + def wrapper(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) + + +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: + # 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) + wrapper = functools.wraps(wrapped)(dummy) + _ensure_wrapper_signature(wrapper, wrapped) + return unittest.mock.patch(fullname, 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"unsupported hash name: {name}") + return _make_hash_constructor_blocker(name, dummy, interface='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"unsupported hash name: {name}") + return _make_hash_constructor_blocker(name, dummy, interface='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"unsupported hash name: {name}") + return _make_hash_constructor_blocker(name, dummy, interface='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 + return contextlib.nullcontext() + assert fullname.count('.') == 1, fullname + module_name, method = fullname.split('.', maxsplit=1) + assert module_name == '_hmac', module_name + try: + module = importlib.import_module(module_name) + except ImportError: + # module is already disabled + return contextlib.nullcontext() + @functools.wraps(wrapped := getattr(module, method)) + def wrapper(key, obj): + raise ValueError(f"unsupported hash name: {name}") + _ensure_wrapper_signature(wrapper, wrapped) + return unittest.mock.patch(fullname, wrapper) + + +@contextlib.contextmanager +def block_algorithm(*names, allow_openssl=False, allow_builtin=False): + """Block a hash algorithm for both hashing and HMAC.""" + with contextlib.ExitStack() as stack: + for name in names: + 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. + # + # So we should only block it if the private interfaces + # are blocked as well. + stack.enter_context(_block_hashlib_hash_constructor(name)) + if not allow_openssl: + stack.enter_context(_block_openssl_hash_new(name)) + stack.enter_context(_block_openssl_hmac_new(name)) + stack.enter_context(_block_openssl_hmac_digest(name)) + stack.enter_context(_block_openssl_hash_constructor(name)) + if not allow_builtin: + 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)) + stack.enter_context(_block_builtin_hmac_constructor(name)) + yield diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index e48a2464ee5977..f81dabb07ce186 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -1,6 +1,7 @@ import contextlib import errno import importlib +import itertools import io import logging import os @@ -17,6 +18,7 @@ import warnings from test import support +from test.support import hashlib_helper from test.support import import_helper from test.support import os_helper from test.support import script_helper @@ -818,5 +820,170 @@ def test_linked_to_musl(self): # SuppressCrashReport +class TestHashlibSupport(unittest.TestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.hashlib = import_helper.import_module("hashlib") + cls.hmac = import_helper.import_module("hmac") + + try: + import _hashlib + except ImportError: + cls._hashlib = None + else: + cls._hashlib = _hashlib + try: + import _hmac + except ImportError: + cls._hmac = None + else: + cls._hmac = _hmac + + def check_context(self, disabled=True): + if disabled: + return self.assertRaises(ValueError) + return contextlib.nullcontext() + + def try_import_attribute(self, fullname, default=None): + if fullname is None: + return default + assert fullname.count('.') == 1, fullname + module_name, attribute = fullname.split('.', maxsplit=1) + try: + module = importlib.import_module(module_name) + except ImportError: + return default + try: + return getattr(module, attribute, default) + 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): + entry = hashlib_helper._EXPLICIT_CONSTRUCTORS[name] + match typ: + case "hashlib": + assert entry.hashlib is not None, entry + return getattr(self.hashlib, entry.hashlib) + case "openssl": + try: + return getattr(self._hashlib, entry.openssl, None) + except TypeError: + return None + case "builtin": + return self.try_import_attribute(entry.fullname(typ)) + + def fetch_hmac_function(self, name): + fullname = hashlib_helper._EXPLICIT_HMAC_CONSTRUCTORS[name] + return self.try_import_attribute(fullname) + + def check_openssl_hash(self, name, *, disabled=True): + """Check that OpenSSL HASH interface is enabled/disabled.""" + if self._hashlib is None: + return + + with self.check_context(disabled): + _ = self._hashlib.new(name) + if do_hash := self.fetch_hash_function(name, "openssl"): + self.assertStartsWith(do_hash.__name__, 'openssl_') + with self.check_context(disabled): + _ = do_hash(b"") + + def check_openssl_hmac(self, name, *, disabled=True): + """Check that OpenSSL HMAC interface is enabled/disabled.""" + if self._hashlib is None: + return + + if name in hashlib_helper.NON_HMAC_DIGEST_NAMES: + # HMAC-BLAKE and HMAC-SHAKE raise a ValueError as they are not + # supported at all (they do not make any sense in practice). + with self.assertRaises(ValueError): + self._hashlib.hmac_digest(b"", b"", name) + else: + with self.check_context(disabled): + _ = self._hashlib.hmac_digest(b"", b"", name) + # OpenSSL does not provide one-shot explicit HMAC functions + + def check_builtin_hash(self, name, *, disabled=True): + """Check that HACL* HASH interface is enabled/disabled.""" + if do_hash := self.fetch_hash_function(name, "builtin"): + self.assertEqual(do_hash.__name__, name) + with self.check_context(disabled): + _ = do_hash(b"") + + def check_builtin_hmac(self, name, *, disabled=True): + """Check that HACL* HMAC interface is enabled/disabled.""" + if self._hmac is None: + return + + if name in hashlib_helper.NON_HMAC_DIGEST_NAMES: + # HMAC-BLAKE and HMAC-SHAKE raise a ValueError as they are not + # supported at all (they do not make any sense in practice). + with self.assertRaises(ValueError): + self._hmac.compute_digest(b"", b"", name) + else: + with self.check_context(disabled): + _ = self._hmac.compute_digest(b"", b"", name) + + with self.check_context(disabled): + _ = self._hmac.new(b"", b"", name) + + if do_hmac := self.fetch_hmac_function(name): + self.assertStartsWith(do_hmac.__name__, 'compute_') + with self.check_context(disabled): + _ = do_hmac(b"", b"") + else: + self.assertIn(name, hashlib_helper.NON_HMAC_DIGEST_NAMES) + + @support.subTests( + ('name', 'allow_openssl', 'allow_builtin'), + itertools.product( + hashlib_helper.CANONICAL_DIGEST_NAMES, + [True, False], + [True, False], + ) + ) + def test_disable_hash(self, name, allow_openssl, allow_builtin): + flags = dict(allow_openssl=allow_openssl, allow_builtin=allow_builtin) + is_simple_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: + aliases = {'blake2s': 'blake2s256', 'blake2b': 'blake2b512'} + name_for_hashlib_new = aliases.get(name, name) + else: + name_for_hashlib_new = name + + with self.check_context(is_simple_disabled): + _ = self.hashlib.new(name_for_hashlib_new) + with self.check_context(is_simple_disabled): + _ = getattr(self.hashlib, name)(b"") + + 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): + _ = self.hmac.new(b"", b"", name) + with self.check_context(is_simple_disabled): + _ = self.hmac.HMAC(b"", b"", name) + with self.check_context(is_simple_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) + + if __name__ == '__main__': unittest.main() From 4c36842c6ebebdf23a27b5a7aa6267432a9156b0 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, 12 Jul 2025 14:04:06 +0200 Subject: [PATCH 2/4] fix WASI --- Lib/test/test_support.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index f81dabb07ce186..fc10e48af5c3ba 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -828,18 +828,11 @@ def setUpClass(cls): cls.hashlib = import_helper.import_module("hashlib") cls.hmac = import_helper.import_module("hmac") - try: - import _hashlib - except ImportError: - cls._hashlib = None - else: - cls._hashlib = _hashlib - try: - import _hmac - except ImportError: - cls._hmac = None - else: - cls._hmac = _hmac + # We required the extension modules to be present since blocking + # HACL* implementations while allowing OpenSSL ones would still + # result in failures. + cls._hashlib = import_helper.import_module("_hashlib") + cls._hmac = import_helper.import_module("_hmac") def check_context(self, disabled=True): if disabled: @@ -886,9 +879,6 @@ def fetch_hmac_function(self, name): def check_openssl_hash(self, name, *, disabled=True): """Check that OpenSSL HASH interface is enabled/disabled.""" - if self._hashlib is None: - return - with self.check_context(disabled): _ = self._hashlib.new(name) if do_hash := self.fetch_hash_function(name, "openssl"): @@ -898,9 +888,6 @@ def check_openssl_hash(self, name, *, disabled=True): def check_openssl_hmac(self, name, *, disabled=True): """Check that OpenSSL HMAC interface is enabled/disabled.""" - if self._hashlib is None: - return - if name in hashlib_helper.NON_HMAC_DIGEST_NAMES: # HMAC-BLAKE and HMAC-SHAKE raise a ValueError as they are not # supported at all (they do not make any sense in practice). @@ -920,9 +907,6 @@ def check_builtin_hash(self, name, *, disabled=True): def check_builtin_hmac(self, name, *, disabled=True): """Check that HACL* HMAC interface is enabled/disabled.""" - if self._hmac is None: - return - if name in hashlib_helper.NON_HMAC_DIGEST_NAMES: # HMAC-BLAKE and HMAC-SHAKE raise a ValueError as they are not # supported at all (they do not make any sense in practice). From 20c91031118ebb208ad2d3205a4d308c1718d042 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, 12 Jul 2025 17:36:52 +0200 Subject: [PATCH 3/4] fix tests on FIPS builds --- Lib/test/support/hashlib_helper.py | 48 ++++++++++++++++-------------- Lib/test/test_support.py | 5 ++++ 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 5c4b754df1bb87..a8a42a7f150fac 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -589,27 +589,31 @@ def wrapper(key, obj): @contextlib.contextmanager -def block_algorithm(*names, allow_openssl=False, allow_builtin=False): - """Block a hash algorithm for both hashing and HMAC.""" +def block_algorithm(name, *, allow_openssl=False, allow_builtin=False): + """Block a hash algorithm for both hashing and HMAC. + + Be careful with this helper as a function may be allowed, but can + still raise a ValueError at runtime if the OpenSSL security policy + disables it, e.g., if allow_openssl=True and FIPS mode is on. + """ with contextlib.ExitStack() as stack: - for name in names: - 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. - # - # So we should only block it if the private interfaces - # are blocked as well. - stack.enter_context(_block_hashlib_hash_constructor(name)) - if not allow_openssl: - stack.enter_context(_block_openssl_hash_new(name)) - stack.enter_context(_block_openssl_hmac_new(name)) - stack.enter_context(_block_openssl_hmac_digest(name)) - stack.enter_context(_block_openssl_hash_constructor(name)) - if not allow_builtin: - 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)) - stack.enter_context(_block_builtin_hmac_constructor(name)) + 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. + # + # So we should only block it if the private interfaces + # are blocked as well. + stack.enter_context(_block_hashlib_hash_constructor(name)) + if not allow_openssl: + stack.enter_context(_block_openssl_hash_new(name)) + stack.enter_context(_block_openssl_hmac_new(name)) + stack.enter_context(_block_openssl_hmac_digest(name)) + stack.enter_context(_block_openssl_hash_constructor(name)) + if not allow_builtin: + 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)) + stack.enter_context(_block_builtin_hmac_constructor(name)) yield diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index fc10e48af5c3ba..cb31122fee9642 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -935,6 +935,11 @@ 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") flags = dict(allow_openssl=allow_openssl, allow_builtin=allow_builtin) is_simple_disabled = not allow_builtin and not allow_openssl From 879cc7345c90902cebcfdc6327ce6e442f5efaee 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, 12 Jul 2025 17:55:55 +0200 Subject: [PATCH 4/4] typo --- Lib/test/support/hashlib_helper.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index a8a42a7f150fac..337a1e415b0de3 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -29,12 +29,10 @@ )) NON_HMAC_DIGEST_NAMES = frozenset({ - 'shake_128', 'shake_256', 'blake2s', 'blake2b' + 'shake_128', 'shake_256', + 'blake2s', 'blake2b', }) -# 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. class HashAPI(namedtuple("HashAPI", "builtin openssl hashlib")): @@ -50,6 +48,9 @@ def fullname(self, typ): raise AssertionError(f"unknown type: {typ}") +# 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"), @@ -66,7 +67,6 @@ def fullname(self, typ): "blake2s": HashAPI("_blake2.blake2s", None, "blake2s"), "blake2b": HashAPI("_blake2.blake2b", None, "blake2b"), }) - assert _EXPLICIT_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES _EXPLICIT_HMAC_CONSTRUCTORS = { @@ -78,6 +78,8 @@ def fullname(self, typ): } _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 = MappingProxyType(_EXPLICIT_HMAC_CONSTRUCTORS)