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) 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