From ca6db2e9df771dcdb2d7af5e53734d63c1a1401e 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, 3 Aug 2025 12:09:54 +0200 Subject: [PATCH 01/22] requires_hashlib -> requires_openssl_hashlib --- Lib/test/support/hashlib_helper.py | 2 +- Lib/test/test_hmac.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 96be74e4105c18..6d38b28f7df892 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -230,7 +230,7 @@ def _ensure_wrapper_signature(wrapper, wrapped): ) -def requires_hashlib(): +def requires_openssl_hashlib(): _hashlib = try_import_module("_hashlib") return unittest.skipIf(_hashlib is None, "requires _hashlib") diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 5c29369d10b143..88f36eb68cca2b 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -161,7 +161,7 @@ def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL): return _call_digest_func(self.hmac.digest, key, msg, digestmod) -@hashlib_helper.requires_hashlib() +@hashlib_helper.requires_openssl_hashlib() class ThroughOpenSSLAPIMixin(CreatorMixin, DigestMixin): """Mixin delegating to _hashlib.hmac_new() and _hashlib.hmac_digest().""" @@ -1431,7 +1431,7 @@ def test_compare_digest_func(self): self.assertIs(self.compare_digest, operator_compare_digest) -@hashlib_helper.requires_hashlib() +@hashlib_helper.requires_openssl_hashlib() class OpenSSLCompareDigestTestCase(CompareDigestMixin, unittest.TestCase): compare_digest = openssl_compare_digest From af5af1ebdd22c91496199efeaab9267bbc9bc2a0 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, 3 Aug 2025 12:10:41 +0200 Subject: [PATCH 02/22] allow to block all algorithms from a given backend --- Lib/test/support/hashlib_helper.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 6d38b28f7df892..59ed858d2fe126 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -794,3 +794,21 @@ def block_algorithm(name, *, allow_openssl=False, allow_builtin=False): # _hmac.compute_digest(..., name) stack.enter_context(_block_builtin_hmac_digest(name)) yield + + +@contextlib.contextmanager +def block_openssl_algorithms(*ignored): + """Block OpenSSL implementations, except those given in *ignored*.""" + with contextlib.ExitStack() as stack: + for name in CANONICAL_DIGEST_NAMES.difference(ignored): + stack.enter_context(block_algorithm(name, allow_builtin=True)) + yield + + +@contextlib.contextmanager +def block_builtin_algorithms(*ignored): + """Block HACL* implementations, except those given in *ignored*.""" + with contextlib.ExitStack() as stack: + for name in CANONICAL_DIGEST_NAMES.difference(ignored): + stack.enter_context(block_algorithm(name, allow_openssl=True)) + yield From 97fc1317fde5fdc3def46a4f7dab78afe343c93b 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, 3 Aug 2025 12:11:55 +0200 Subject: [PATCH 03/22] add known backends --- Lib/test/support/hashlib_helper.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 59ed858d2fe126..b9ce443e4cd84b 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -17,6 +17,15 @@ def try_import_module(module_name): return None +class Implementation(enum.StrEnum): + # Indicate that the hash function is implemented by a built-in module. + builtin = enum.auto() + # Indicate that the hash function is implemented by OpenSSL. + openssl = enum.auto() + # Indicate that the hash function is provided through the public API. + hashlib = enum.auto() + + class HID(enum.StrEnum): """Enumeration containing the canonical digest names. From 1a9bd8cdd1ee908a5a15421edd2e36ded585b0c8 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, 3 Aug 2025 12:18:30 +0200 Subject: [PATCH 04/22] add helpers for importing data --- Lib/test/support/hashlib_helper.py | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index b9ce443e4cd84b..7c40fcaed5fbbc 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -17,6 +17,53 @@ def try_import_module(module_name): return None +def _parse_fullname(fullname, *, strict=False): + """Parse a fully-qualified name .. + + The ``module_name`` component contains one or more dots. + The ``member_name`` component does not contain any dot. + """ + if fullname is None: + assert not strict + return None, None + assert isinstance(fullname, str), fullname + assert fullname.count(".") >= 1, fullname + return fullname.rsplit(".", maxsplit=1) + + +def _import_module(module_name, *, strict=False): + """Import a module from its fully-qualified name. + + If *strict* is false, import failures are suppressed and None is returned. + """ + if module_name is None: + # To prevent a TypeError in importlib.import_module + if strict: + raise ImportError("no module to import") + return None + try: + return importlib.import_module(module_name) + except ImportError as exc: + if strict: + raise exc + return None + + +def _import_member(module_name, member_name, *, strict=False): + """Import a member from a module. + + If *strict* is false, import failures are suppressed and None is returned. + """ + if member_name is None: + if strict: + raise ImportError(f"no member to import from {module_name}") + return None + module = _import_module(module_name, strict=strict) + if strict: + return getattr(module, member_name) + return getattr(module, member_name, None) + + class Implementation(enum.StrEnum): # Indicate that the hash function is implemented by a built-in module. builtin = enum.auto() From 24bd8dd3baf28a112ad740ce7ec759a9537f49a6 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, 3 Aug 2025 12:18:43 +0200 Subject: [PATCH 05/22] `HID` -> `HashId` --- Lib/test/support/hashlib_helper.py | 48 +++++++++++++++--------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 7c40fcaed5fbbc..3c8ff3d24521fb 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -73,7 +73,7 @@ class Implementation(enum.StrEnum): hashlib = enum.auto() -class HID(enum.StrEnum): +class HashId(enum.StrEnum): """Enumeration containing the canonical digest names. Those names should only be used by hashlib.new() or hmac.new(). @@ -113,10 +113,10 @@ def is_keyed(self): return self.startswith("blake2") -CANONICAL_DIGEST_NAMES = frozenset(map(str, HID.__members__)) +CANONICAL_DIGEST_NAMES = frozenset(map(str, HashId.__members__)) NON_HMAC_DIGEST_NAMES = frozenset(( - HID.shake_128, HID.shake_256, - HID.blake2s, HID.blake2b, + HashId.shake_128, HashId.shake_256, + HashId.blake2s, HashId.blake2b, )) @@ -189,32 +189,32 @@ def fullname(self, implementation): # constructors. If the constructor name is None, then this means that the # algorithm can only be used by the "agile" new() interfaces. _EXPLICIT_CONSTRUCTORS = MappingProxyType({ # fmt: skip - HID.md5: HashInfo("_md5.md5", "openssl_md5", "md5"), - HID.sha1: HashInfo("_sha1.sha1", "openssl_sha1", "sha1"), - HID.sha224: HashInfo("_sha2.sha224", "openssl_sha224", "sha224"), - HID.sha256: HashInfo("_sha2.sha256", "openssl_sha256", "sha256"), - HID.sha384: HashInfo("_sha2.sha384", "openssl_sha384", "sha384"), - HID.sha512: HashInfo("_sha2.sha512", "openssl_sha512", "sha512"), - HID.sha3_224: HashInfo( + HashId.md5: HashInfo("_md5.md5", "openssl_md5", "md5"), + HashId.sha1: HashInfo("_sha1.sha1", "openssl_sha1", "sha1"), + HashId.sha224: HashInfo("_sha2.sha224", "openssl_sha224", "sha224"), + HashId.sha256: HashInfo("_sha2.sha256", "openssl_sha256", "sha256"), + HashId.sha384: HashInfo("_sha2.sha384", "openssl_sha384", "sha384"), + HashId.sha512: HashInfo("_sha2.sha512", "openssl_sha512", "sha512"), + HashId.sha3_224: HashInfo( "_sha3.sha3_224", "openssl_sha3_224", "sha3_224" ), - HID.sha3_256: HashInfo( + HashId.sha3_256: HashInfo( "_sha3.sha3_256", "openssl_sha3_256", "sha3_256" ), - HID.sha3_384: HashInfo( + HashId.sha3_384: HashInfo( "_sha3.sha3_384", "openssl_sha3_384", "sha3_384" ), - HID.sha3_512: HashInfo( + HashId.sha3_512: HashInfo( "_sha3.sha3_512", "openssl_sha3_512", "sha3_512" ), - HID.shake_128: HashInfo( + HashId.shake_128: HashInfo( "_sha3.shake_128", "openssl_shake_128", "shake_128" ), - HID.shake_256: HashInfo( + HashId.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"), + HashId.blake2s: HashInfo("_blake2.blake2s", None, "blake2s"), + HashId.blake2b: HashInfo("_blake2.blake2b", None, "blake2b"), }) assert _EXPLICIT_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES get_hash_info = _EXPLICIT_CONSTRUCTORS.__getitem__ @@ -223,16 +223,16 @@ 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(name): f"_hmac.compute_{name}" + HashId(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 +_EXPLICIT_HMAC_CONSTRUCTORS[HashId.shake_128] = None +_EXPLICIT_HMAC_CONSTRUCTORS[HashId.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[HashId.blake2s] = '_hmac.compute_blake2s_32' +_EXPLICIT_HMAC_CONSTRUCTORS[HashId.blake2b] = '_hmac.compute_blake2b_32' _EXPLICIT_HMAC_CONSTRUCTORS = MappingProxyType(_EXPLICIT_HMAC_CONSTRUCTORS) assert _EXPLICIT_HMAC_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES @@ -663,7 +663,7 @@ def _block_builtin_hash_new(name): """Block a buitin-in hash name from the hashlib.new() interface.""" assert isinstance(name, str), name assert name.lower() == name, f"invalid name: {name}" - assert name in HID, f"invalid hash: {name}" + assert name in HashId, f"invalid hash: {name}" # Re-import 'hashlib' in case it was mocked hashlib = importlib.import_module('hashlib') From e027a5e8ad355e9c3b7993e74356351ad01285d3 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, 3 Aug 2025 12:21:30 +0200 Subject: [PATCH 06/22] add base class for holding importable data --- Lib/test/support/hashlib_helper.py | 86 ++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 3c8ff3d24521fb..853ab96bffa9bc 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -120,6 +120,92 @@ def is_keyed(self): )) +class _HashInfoItem: + """Interface for interacting with a named object. + + The object is entirely described by its fully-qualified *fullname*. + + *fullname* must be None or a string ".". + If *strict* is true, *fullname* cannot be None. + """ + + def __init__(self, fullname=None, *, strict=False): + module_name, member_name = _parse_fullname(fullname, strict=strict) + self._fullname = fullname + self._module_name = module_name + self._member_name = member_name + + @property + def fullname(self): + return self._fullname + + @property + def module_name(self): + return self._module_name + + @property + def member_name(self): + return self._member_name + + def import_module(self, *, strict=False): + """Import the described module. + + If *strict* is true, an ImportError may be raised if importing fails, + otherwise, None is returned on error. + """ + return _import_module(self.module_name, strict=strict) + + def import_member(self, *, strict=False): + """Import the described member. + + If *strict* is true, an AttributeError or an ImportError may be + raised if importing fails; otherwise, None is returned on error. + """ + return _import_member( + self.module_name, self.member_name, strict=strict + ) + + +class _HashInfoBase: + """Base dataclass containing "backend" information. + + Subclasses may define an attribute named after one of the known + implementations ("builtin", "openssl" or "hashlib") which stores + an _HashInfoItem object. + + Those attributes can be retrieved through __getitem__(), e.g., + ``info["builtin"]`` returns the _HashInfoItem corresponding to + the builtin implementation. + """ + + def __init__(self, canonical_name): + self._canonical_name = canonical_name + + @property + def canonical_name(self): + """The canonical hash name.""" + return self._canonical_name + + def __getitem__(self, implementation): + try: + attrname = Implementation(implementation) + except ValueError: + raise self.invalid_implementation_error(implementation) from None + + try: + provider = getattr(self, attrname) + except AttributeError: + raise self.invalid_implementation_error(implementation) from None + + if not isinstance(provider, _HashInfoItem): + raise KeyError(implementation) + return provider + + def invalid_implementation_error(self, implementation): + msg = f"no implementation {implementation} for {self.canonical_name}" + return AssertionError(msg) + + class HashInfo: """Dataclass storing explicit hash constructor names. From b4f0d6485c119dbd66d42710bb544c151e3362c1 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, 3 Aug 2025 12:22:01 +0200 Subject: [PATCH 07/22] add class to store information for hash function constructors --- Lib/test/support/hashlib_helper.py | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 853ab96bffa9bc..c2daacf7ed6ca7 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -206,6 +206,52 @@ def invalid_implementation_error(self, implementation): return AssertionError(msg) +class _HashFuncInfo(_HashInfoBase): + """Dataclass containing information for hash functions constructors. + + - *builtin* is the fully-qualified name of the HACL* + hash constructor function, e.g., "_md5.md5". + + - *openssl* is the fully-qualified name of the "_hashlib" method + for the OpenSSL named constructor, e.g., "_hashlib.openssl_md5". + + - *hashlib* is the fully-qualified name of the "hashlib" method + for the explicit named hash constructor, e.g., "hashlib.md5". + """ + + def __init__(self, canonical_name, builtin, openssl=None, hashlib=None): + super().__init__(canonical_name) + self.builtin = _HashInfoItem(builtin, strict=True) + self.openssl = _HashInfoItem(openssl, strict=False) + self.hashlib = _HashInfoItem(hashlib, strict=False) + + 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". + """ + return self[implementation].fullname + + def module_name(self, implementation): + """Get the name of the constructor function module. + + The *implementation* must be "builtin", "openssl" or "hashlib". + """ + return self[implementation].module_name + + def method_name(self, implementation): + """Get the name of the constructor function module method. + + Use fullname() to get the constructor function fully-qualified name. + + The *implementation* must be "builtin", "openssl" or "hashlib". + """ + return self[implementation].member_name + + class HashInfo: """Dataclass storing explicit hash constructor names. From 4ae51c77634311d5b74ed6377272181b66ad1783 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, 3 Aug 2025 12:24:07 +0200 Subject: [PATCH 08/22] restructure the hash functions database --- Lib/test/support/hashlib_helper.py | 144 +++++++++++++++++++++++------ 1 file changed, 118 insertions(+), 26 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index c2daacf7ed6ca7..36ecd7b46920dd 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -317,39 +317,131 @@ def fullname(self, implementation): return f"{module_name}.{method_name}" -# Mapping from a "canonical" name to a pair (HACL*, _hashlib.*, hashlib.*) -# constructors. If the constructor name is None, then this means that the -# algorithm can only be used by the "agile" new() interfaces. -_EXPLICIT_CONSTRUCTORS = MappingProxyType({ # fmt: skip - HashId.md5: HashInfo("_md5.md5", "openssl_md5", "md5"), - HashId.sha1: HashInfo("_sha1.sha1", "openssl_sha1", "sha1"), - HashId.sha224: HashInfo("_sha2.sha224", "openssl_sha224", "sha224"), - HashId.sha256: HashInfo("_sha2.sha256", "openssl_sha256", "sha256"), - HashId.sha384: HashInfo("_sha2.sha384", "openssl_sha384", "sha384"), - HashId.sha512: HashInfo("_sha2.sha512", "openssl_sha512", "sha512"), - HashId.sha3_224: HashInfo( - "_sha3.sha3_224", "openssl_sha3_224", "sha3_224" +class _HashInfo: + """Dataclass containing information for supported hash functions. + + - *builtin_method_fullname* is the fully-qualified name + of the HACL* hash constructor function, e.g., "_md5.md5". + + - *openssl_method_fullname* is the fully-qualified name + of the "_hashlib" module method for the explicit OpenSSL + hash constructor function, e.g., "_hashlib.openssl_md5". + + - *hashlib_method_fullname* is the fully-qualified name + of the "hashlib" module method for the explicit hash + constructor function, e.g., "hashlib.md5". + """ + + def __init__( + self, + name, + builtin_method_fullname, + openssl_method_fullname=None, + hashlib_method_fullname=None, + ): + self.name = name + self.func = _HashFuncInfo( + name, + builtin_method_fullname, + openssl_method_fullname, + hashlib_method_fullname, + ) + + +_HASHINFO_DATABASE = MappingProxyType({ + HashId.md5: _HashInfo( + HashId.md5, + "_md5.md5", + "_hashlib.openssl_md5", + "hashlib.md5", + ), + HashId.sha1: _HashInfo( + HashId.sha1, + "_sha1.sha1", + "_hashlib.openssl_sha1", + "hashlib.sha1", + ), + HashId.sha224: _HashInfo( + HashId.sha224, + "_sha2.sha224", + "_hashlib.openssl_sha224", + "hashlib.sha224", ), - HashId.sha3_256: HashInfo( - "_sha3.sha3_256", "openssl_sha3_256", "sha3_256" + HashId.sha256: _HashInfo( + HashId.sha256, + "_sha2.sha256", + "_hashlib.openssl_sha256", + "hashlib.sha256", ), - HashId.sha3_384: HashInfo( - "_sha3.sha3_384", "openssl_sha3_384", "sha3_384" + HashId.sha384: _HashInfo( + HashId.sha384, + "_sha2.sha384", + "_hashlib.openssl_sha384", + "hashlib.sha384", ), - HashId.sha3_512: HashInfo( - "_sha3.sha3_512", "openssl_sha3_512", "sha3_512" + HashId.sha512: _HashInfo( + HashId.sha512, + "_sha2.sha512", + "_hashlib.openssl_sha512", + "hashlib.sha512", ), - HashId.shake_128: HashInfo( - "_sha3.shake_128", "openssl_shake_128", "shake_128" + HashId.sha3_224: _HashInfo( + HashId.sha3_224, + "_sha3.sha3_224", + "_hashlib.openssl_sha3_224", + "hashlib.sha3_224", ), - HashId.shake_256: HashInfo( - "_sha3.shake_256", "openssl_shake_256", "shake_256" + HashId.sha3_256: _HashInfo( + HashId.sha3_256, + "_sha3.sha3_256", + "_hashlib.openssl_sha3_256", + "hashlib.sha3_256", + ), + HashId.sha3_384: _HashInfo( + HashId.sha3_384, + "_sha3.sha3_384", + "_hashlib.openssl_sha3_384", + "hashlib.sha3_384", + ), + HashId.sha3_512: _HashInfo( + HashId.sha3_512, + "_sha3.sha3_512", + "_hashlib.openssl_sha3_512", + "hashlib.sha3_512", + ), + HashId.shake_128: _HashInfo( + HashId.shake_128, + "_sha3.shake_128", + "_hashlib.openssl_shake_128", + "hashlib.shake_128", + ), + HashId.shake_256: _HashInfo( + HashId.shake_256, + "_sha3.shake_256", + "_hashlib.openssl_shake_256", + "hashlib.shake_256", + ), + HashId.blake2s: _HashInfo( + HashId.blake2s, + "_blake2.blake2s", + None, + "hashlib.blake2s", + ), + HashId.blake2b: _HashInfo( + HashId.blake2b, + "_blake2.blake2b", + None, + "hashlib.blake2b", ), - HashId.blake2s: HashInfo("_blake2.blake2s", None, "blake2s"), - HashId.blake2b: HashInfo("_blake2.blake2b", None, "blake2b"), }) -assert _EXPLICIT_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES -get_hash_info = _EXPLICIT_CONSTRUCTORS.__getitem__ +assert _HASHINFO_DATABASE.keys() == CANONICAL_DIGEST_NAMES + + +def get_hash_func_info(name): + info = _HASHINFO_DATABASE[name] + assert isinstance(info, _HashInfo), info + return info.func + # Mapping from canonical hash names to their explicit HACL* HMAC constructor. # There is currently no OpenSSL one-shot named function and there will likely From 833fc2e03cac8e45f7dd821f4e3468a23d246401 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, 3 Aug 2025 13:05:05 +0200 Subject: [PATCH 09/22] update interface usage --- Lib/test/support/hashlib_helper.py | 159 ++++++++--------------------- Lib/test/test_hmac.py | 2 +- Lib/test/test_support.py | 22 ++-- 3 files changed, 53 insertions(+), 130 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 36ecd7b46920dd..bb56a7e8835748 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -9,14 +9,6 @@ from types import MappingProxyType -def try_import_module(module_name): - """Try to import a module and return None on failure.""" - try: - return importlib.import_module(module_name) - except ImportError: - return None - - def _parse_fullname(fullname, *, strict=False): """Parse a fully-qualified name .. @@ -252,71 +244,6 @@ def method_name(self, implementation): return self[implementation].member_name -class HashInfo: - """Dataclass storing explicit hash constructor names. - - - *builtin* is the fully-qualified name for the explicit HACL* - hash constructor function, e.g., "_md5.md5". - - - *openssl* is the name of the "_hashlib" module method for the explicit - OpenSSL hash constructor function, e.g., "openssl_md5". - - - *hashlib* is the name of the "hashlib" module method for the explicit - hash constructor function, e.g., "md5". - """ - - def __init__(self, builtin, openssl=None, hashlib=None): - assert isinstance(builtin, str), builtin - assert len(builtin.split(".")) == 2, builtin - - self.builtin = builtin - self.builtin_module_name, self.builtin_method_name = ( - self.builtin.split(".", maxsplit=1) - ) - - assert openssl is None or openssl.startswith("openssl_") - self.openssl = self.openssl_method_name = openssl - self.openssl_module_name = "_hashlib" if openssl else None - - assert hashlib is None or isinstance(hashlib, str) - self.hashlib = self.hashlib_method_name = hashlib - self.hashlib_module_name = "hashlib" if hashlib else None - - def module_name(self, implementation): - match implementation: - case "builtin": - return self.builtin_module_name - case "openssl": - return self.openssl_module_name - case "hashlib": - return self.hashlib_module_name - raise AssertionError(f"invalid implementation {implementation}") - - def method_name(self, implementation): - match implementation: - case "builtin": - return self.builtin_method_name - case "openssl": - return self.openssl_method_name - case "hashlib": - return self.hashlib_method_name - raise AssertionError(f"invalid implementation {implementation}") - - def fullname(self, implementation): - """Get the fully qualified name of a given implementation. - - This returns a string of the form "MODULE_NAME.METHOD_NAME" or None - if the hash function does not have a corresponding implementation. - - *implementation* must be "builtin", "openssl" or "hashlib". - """ - module_name = self.module_name(implementation) - method_name = self.method_name(implementation) - if module_name is None or method_name is None: - return None - return f"{module_name}.{method_name}" - - class _HashInfo: """Dataclass containing information for supported hash functions. @@ -443,6 +370,12 @@ def get_hash_func_info(name): return info.func +def _iter_hash_func_info(excluded): + for name, info in _HASHINFO_DATABASE.items(): + if name not in excluded: + yield info.func + + # 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. @@ -511,12 +444,12 @@ def _ensure_wrapper_signature(wrapper, wrapped): def requires_openssl_hashlib(): - _hashlib = try_import_module("_hashlib") + _hashlib = _import_module("_hashlib") return unittest.skipIf(_hashlib is None, "requires _hashlib") def requires_builtin_hmac(): - _hmac = try_import_module("_hmac") + _hmac = _import_module("_hmac") return unittest.skipIf(_hmac is None, "requires _hmac") @@ -544,7 +477,7 @@ def _hashlib_new(digestname, openssl, /, **kwargs): # 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") + _hashlib = _import_module("_hashlib") module = _hashlib if openssl and _hashlib is not None else hashlib try: module.new(digestname, **kwargs) @@ -665,29 +598,30 @@ def requires_openssl_hashdigest(digestname, *, usedforsecurity=True): ) -def requires_builtin_hashdigest( - module_name, digestname, *, usedforsecurity=True -): - """Decorator raising SkipTest if a HACL* hashing algorithm is missing. - - - The *module_name* is the C extension module name based on HACL*. - - The *digestname* is one of its member, e.g., 'md5'. - """ +def _make_requires_builtin_hashdigest_decorator(item, *, usedforsecurity=True): + assert isinstance(item, _HashInfoItem), item return _make_requires_hashdigest_decorator( - _builtin_hash, module_name, digestname, usedforsecurity=usedforsecurity + _builtin_hash, + item.module_name, + item.member_name, + usedforsecurity=usedforsecurity, + ) + + +def requires_builtin_hashdigest(canonical_name, *, usedforsecurity=True): + """Decorator raising SkipTest if a HACL* hashing algorithm is missing.""" + info = get_hash_func_info(canonical_name) + return _make_requires_builtin_hashdigest_decorator( + info.builtin, usedforsecurity=usedforsecurity ) def requires_builtin_hashes(*ignored, usedforsecurity=True): """Decorator raising SkipTest if one HACL* hashing algorithm is missing.""" return _chain_decorators(( - requires_builtin_hashdigest( - api.builtin_module_name, - api.builtin_method_name, - usedforsecurity=usedforsecurity, - ) - for name, api in _EXPLICIT_CONSTRUCTORS.items() - if name not in ignored + _make_requires_builtin_hashdigest_decorator( + info.builtin, usedforsecurity=usedforsecurity + ) for info in _iter_hash_func_info(ignored) )) @@ -803,10 +737,10 @@ class BuiltinHashFunctionsTrait(HashFunctionsTrait): def _find_constructor(self, digestname): self.is_valid_digest_name(digestname) - info = _EXPLICIT_CONSTRUCTORS[digestname] + info = get_hash_func_info(digestname) return _builtin_hash( - info.builtin_module_name, - info.builtin_method_name, + info.builtin.module_name, + info.builtin.member_name, usedforsecurity=self.usedforsecurity, ) @@ -822,7 +756,7 @@ def find_gil_minsize(modules_names, default=2048): """ sizes = [] for module_name in modules_names: - module = try_import_module(module_name) + module = _import_module(module_name) if module is not None: sizes.append(getattr(module, '_GIL_MINSIZE', default)) return max(sizes, default=default) @@ -833,7 +767,7 @@ def _block_openssl_hash_new(blocked_name): assert isinstance(blocked_name, str), blocked_name # re-import '_hashlib' in case it was mocked - if (_hashlib := try_import_module("_hashlib")) is None: + if (_hashlib := _import_module("_hashlib")) is None: return contextlib.nullcontext() @functools.wraps(wrapped := _hashlib.new) @@ -852,7 +786,7 @@ def _block_openssl_hmac_new(blocked_name): assert isinstance(blocked_name, str), blocked_name # re-import '_hashlib' in case it was mocked - if (_hashlib := try_import_module("_hashlib")) is None: + if (_hashlib := _import_module("_hashlib")) is None: return contextlib.nullcontext() @functools.wraps(wrapped := _hashlib.hmac_new) @@ -870,7 +804,7 @@ def _block_openssl_hmac_digest(blocked_name): assert isinstance(blocked_name, str), blocked_name # re-import '_hashlib' in case it was mocked - if (_hashlib := try_import_module("_hashlib")) is None: + if (_hashlib := _import_module("_hashlib")) is None: return contextlib.nullcontext() @functools.wraps(wrapped := _hashlib.hmac_digest) @@ -900,7 +834,7 @@ def _block_builtin_hash_new(name): # 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 + builtin_module_name = get_hash_func_info(name).builtin.module_name @functools.wraps(get_builtin_constructor) def get_builtin_constructor_mock(name): @@ -920,7 +854,7 @@ 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: + if (_hmac := _import_module("_hmac")) is None: return contextlib.nullcontext() @functools.wraps(wrapped := _hmac.new) @@ -937,7 +871,7 @@ 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: + if (_hmac := _import_module("_hmac")) is None: return contextlib.nullcontext() @functools.wraps(wrapped := _hmac.compute_digest) @@ -951,30 +885,19 @@ def _hmac_compute_digest(key, msg, digest): def _make_hash_constructor_blocker(name, dummy, implementation): - info = _EXPLICIT_CONSTRUCTORS[name] - module_name = info.module_name(implementation) - method_name = info.method_name(implementation) - if module_name is None or method_name is None: + info = get_hash_func_info(name)[implementation] + if (wrapped := info.import_member()) is None: # function shouldn't exist for this implementation return contextlib.nullcontext() - - try: - module = importlib.import_module(module_name) - except ImportError: - # module is already disabled - return contextlib.nullcontext() - - wrapped = getattr(module, method_name) wrapper = functools.wraps(wrapped)(dummy) _ensure_wrapper_signature(wrapper, wrapped) - return unittest.mock.patch(info.fullname(implementation), wrapper) + return unittest.mock.patch(info.fullname, wrapper) def _block_hashlib_hash_constructor(name): """Block explicit public constructors.""" def dummy(data=b'', *, usedforsecurity=True, string=None): raise ValueError(f"blocked explicit public hash name: {name}") - return _make_hash_constructor_blocker(name, dummy, 'hashlib') @@ -1040,14 +963,14 @@ def block_algorithm(name, *, allow_openssl=False, allow_builtin=False): # 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")) + (_hashlib := _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")) + not (_hashlib := _import_module("_hashlib")) and not allow_builtin ): stack.enter_context(_block_hashlib_hash_constructor(name)) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 88f36eb68cca2b..7634deeb1d8eb9 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -1509,7 +1509,7 @@ def test_hmac_digest_overflow_error_openssl_only(self, size): hmac = import_fresh_module("hmac", blocked=["_hmac"]) self.do_test_hmac_digest_overflow_error_switch_to_slow(hmac, size) - @hashlib_helper.requires_builtin_hashdigest("_md5", "md5") + @hashlib_helper.requires_builtin_hashdigest("md5") @bigmemtest(size=_4G + 5, memuse=2, dry_run=False) def test_hmac_digest_overflow_error_builtin_only(self, size): hmac = import_fresh_module("hmac", blocked=["_hashlib"]) diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 9ec382afb65fe4..8bb9a63d856688 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -865,17 +865,17 @@ def try_import_attribute(self, fullname, default=None): return default def fetch_hash_function(self, name, implementation): - info = hashlib_helper.get_hash_info(name) - match implementation: - case "hashlib": - assert info.hashlib is not None, info - return getattr(self.hashlib, info.hashlib) - case "openssl": - try: - return getattr(self._hashlib, info.openssl, None) - except TypeError: - return None - fullname = info.fullname(implementation) + info = hashlib_helper.get_hash_func_info(name) + match hashlib_helper.Implementation(implementation): + case hashlib_helper.Implementation.hashlib: + method_name = info.hashlib.member_name + assert isinstance(method_name, str), method_name + return getattr(self.hashlib, method_name) + case hashlib_helper.Implementation.openssl: + method_name = info.openssl.member_name + assert isinstance(method_name, str | None), method_name + return getattr(self._hashlib, method_name or "", None) + fullname = info[implementation].fullname return self.try_import_attribute(fullname) def fetch_hmac_function(self, name): From 356828fdaeeaf8c52a318b362bbed8dc7fa708e1 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, 3 Aug 2025 13:08:46 +0200 Subject: [PATCH 10/22] introduce information for runtime type --- Lib/test/support/hashlib_helper.py | 103 +++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index bb56a7e8835748..203c73594ee084 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -198,6 +198,61 @@ def invalid_implementation_error(self, implementation): return AssertionError(msg) +class _HashTypeInfo(_HashInfoBase): + """Dataclass containing information for hash functions types. + + - *builtin* is the fully-qualified name for the builtin HACL* type, + e.g., "_md5.MD5Type". + + - *openssl* is the fully-qualified name for the OpenSSL wrapper type, + e.g., "_hashlib.HASH". + """ + + def __init__(self, canonical_name, builtin, openssl): + super().__init__(canonical_name) + self.builtin = _HashInfoItem(builtin, strict=True) + self.openssl = _HashInfoItem(openssl, strict=True) + + def fullname(self, implementation): + """Get the fully qualified name of a given implementation. + + This returns a string of the form "MODULE_NAME.OBJECT_NAME" or None + if the hash function does not have a corresponding implementation. + + *implementation* must be "builtin" or "openssl". + """ + return self[implementation].fullname + + def module_name(self, implementation): + """Get the name of the module containing the hash object type.""" + return self[implementation].module_name + + def object_type_name(self, implementation): + """Get the name of the hash object class name.""" + return self[implementation].member_name + + def import_module(self, implementation, *, allow_skip=False): + """Import the module containing the hash object type. + + On error, return None if *allow_skip* is false, or raise SkipNoHash. + """ + module = self[implementation].import_module() + if allow_skip and module is None: + raise SkipNoHash(self.canonical_name, implementation) + return module + + def import_object_type(self, implementation, *, allow_skip=False): + """Get the runtime hash object type. + + On error, return None if *allow_skip* is false, or raise SkipNoHash. + """ + member = self[implementation].import_member() + if allow_skip and member is None: + raise SkipNoHash(self.name, implementation, interface="class") + assert isinstance(member, type | None), member + return member + + class _HashFuncInfo(_HashInfoBase): """Dataclass containing information for hash functions constructors. @@ -247,6 +302,12 @@ def method_name(self, implementation): class _HashInfo: """Dataclass containing information for supported hash functions. + - *builtin_object_type_fullname* is the fully-qualified name + for the builtin HACL* type, e.g., "_md5.MD5Type". + + - *openssl_object_type_fullname* is the fully-qualified name + for the OpenSSL wrapper type, i.e. "_hashlib.HASH" or "_hashlib.HASHXOF". + - *builtin_method_fullname* is the fully-qualified name of the HACL* hash constructor function, e.g., "_md5.md5". @@ -262,11 +323,19 @@ class _HashInfo: def __init__( self, name, + builtin_object_type_fullname, + openssl_object_type_fullname, builtin_method_fullname, openssl_method_fullname=None, hashlib_method_fullname=None, ): self.name = name + + self.type = _HashTypeInfo( + name, + builtin_object_type_fullname, + openssl_object_type_fullname, + ) self.func = _HashFuncInfo( name, builtin_method_fullname, @@ -278,36 +347,48 @@ def __init__( _HASHINFO_DATABASE = MappingProxyType({ HashId.md5: _HashInfo( HashId.md5, + "_md5.MD5Type", + "_hashlib.HASH", "_md5.md5", "_hashlib.openssl_md5", "hashlib.md5", ), HashId.sha1: _HashInfo( HashId.sha1, + "_sha1.SHA1Type", + "_hashlib.HASH", "_sha1.sha1", "_hashlib.openssl_sha1", "hashlib.sha1", ), HashId.sha224: _HashInfo( HashId.sha224, + "_sha2.SHA224Type", + "_hashlib.HASH", "_sha2.sha224", "_hashlib.openssl_sha224", "hashlib.sha224", ), HashId.sha256: _HashInfo( HashId.sha256, + "_sha2.SHA256Type", + "_hashlib.HASH", "_sha2.sha256", "_hashlib.openssl_sha256", "hashlib.sha256", ), HashId.sha384: _HashInfo( HashId.sha384, + "_sha2.SHA384Type", + "_hashlib.HASH", "_sha2.sha384", "_hashlib.openssl_sha384", "hashlib.sha384", ), HashId.sha512: _HashInfo( HashId.sha512, + "_sha2.SHA512Type", + "_hashlib.HASH", "_sha2.sha512", "_hashlib.openssl_sha512", "hashlib.sha512", @@ -315,48 +396,64 @@ def __init__( HashId.sha3_224: _HashInfo( HashId.sha3_224, "_sha3.sha3_224", + "_hashlib.HASH", + "_sha3.sha3_224", "_hashlib.openssl_sha3_224", "hashlib.sha3_224", ), HashId.sha3_256: _HashInfo( HashId.sha3_256, "_sha3.sha3_256", + "_hashlib.HASH", + "_sha3.sha3_256", "_hashlib.openssl_sha3_256", "hashlib.sha3_256", ), HashId.sha3_384: _HashInfo( HashId.sha3_384, "_sha3.sha3_384", + "_hashlib.HASH", + "_sha3.sha3_384", "_hashlib.openssl_sha3_384", "hashlib.sha3_384", ), HashId.sha3_512: _HashInfo( HashId.sha3_512, "_sha3.sha3_512", + "_hashlib.HASH", + "_sha3.sha3_512", "_hashlib.openssl_sha3_512", "hashlib.sha3_512", ), HashId.shake_128: _HashInfo( HashId.shake_128, "_sha3.shake_128", + "_hashlib.HASHXOF", + "_sha3.shake_128", "_hashlib.openssl_shake_128", "hashlib.shake_128", ), HashId.shake_256: _HashInfo( HashId.shake_256, "_sha3.shake_256", + "_hashlib.HASHXOF", + "_sha3.shake_256", "_hashlib.openssl_shake_256", "hashlib.shake_256", ), HashId.blake2s: _HashInfo( HashId.blake2s, "_blake2.blake2s", + "_hashlib.HASH", + "_blake2.blake2s", None, "hashlib.blake2s", ), HashId.blake2b: _HashInfo( HashId.blake2b, "_blake2.blake2b", + "_hashlib.HASH", + "_blake2.blake2b", None, "hashlib.blake2b", ), @@ -364,6 +461,12 @@ def __init__( assert _HASHINFO_DATABASE.keys() == CANONICAL_DIGEST_NAMES +def get_hash_type_info(name): + info = _HASHINFO_DATABASE[name] + assert isinstance(info, _HashInfo), info + return info.type + + def get_hash_func_info(name): info = _HASHINFO_DATABASE[name] assert isinstance(info, _HashInfo), info From 2a3f91a2367f5286d09a9013a54f0a87adbef2e1 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, 3 Aug 2025 13:09:53 +0200 Subject: [PATCH 11/22] introduce information for hash functions family --- Lib/test/support/hashlib_helper.py | 85 +++++++++++++++++++++++------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 203c73594ee084..4421715191b339 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -65,44 +65,89 @@ class Implementation(enum.StrEnum): hashlib = enum.auto() -class HashId(enum.StrEnum): - """Enumeration containing the canonical digest names. - - Those names should only be used by hashlib.new() or hmac.new(). - Their support by _hashlib.new() is not necessarily guaranteed. - """ +class HashFamily(enum.StrEnum): + """Enumerationg containing the algorithmic families known hashes.""" md5 = enum.auto() sha1 = enum.auto() + sha2 = enum.auto() + sha3 = enum.auto() + shake = enum.auto() + blake2 = enum.auto() - sha224 = enum.auto() - sha256 = enum.auto() - sha384 = enum.auto() - sha512 = enum.auto() - sha3_224 = enum.auto() - sha3_256 = enum.auto() - sha3_384 = enum.auto() - sha3_512 = enum.auto() +class HashId(enum.StrEnum): + """Enumeration containing the canonical digest names. - shake_128 = enum.auto() - shake_256 = enum.auto() + Those names should only be used by hashlib.new() or hmac.new(). + Their support by _hashlib.new() is not necessarily guaranteed. + """ - blake2s = enum.auto() - blake2b = enum.auto() + # MD5 family + md5 = (enum.auto(), HashFamily.md5) + + # SHA-1 family + sha1 = (enum.auto(), HashFamily.sha1) + + # SHA-2 family + sha224 = (enum.auto(), HashFamily.sha2) + sha256 = (enum.auto(), HashFamily.sha2) + sha384 = (enum.auto(), HashFamily.sha2) + sha512 = (enum.auto(), HashFamily.sha2) + + # SHA-3 family + sha3_224 = (enum.auto(), HashFamily.sha3) + sha3_256 = (enum.auto(), HashFamily.sha3) + sha3_384 = (enum.auto(), HashFamily.sha3) + sha3_512 = (enum.auto(), HashFamily.sha3) + + # SHA-3-XOF family + shake_128 = (enum.auto(), HashFamily.shake) + shake_256 = (enum.auto(), HashFamily.shake) + + # BLAKE-2 family + blake2s = (enum.auto(), HashFamily.blake2) + blake2b = (enum.auto(), HashFamily.blake2) + + def __new__(cls, *args): + if len(args) == 1: + try: + return getattr(cls, args[0]) + except AttributeError as exc: + raise ValueError(f"unknown hash algorithm: {args[0]}") from exc + elif len(args) != 2: + raise TypeError(f"HashId expects 1 to 2 arguments," + f" got {len(args)}") + value, family = args + assert isinstance(family, HashFamily), family + self = str.__new__(cls, value) + self._value_ = value + self._family = family + return self def __repr__(self): return str(self) + @classmethod + def from_name(cls, canonical_name): + if isinstance(canonical_name, cls): + return canonical_name + return getattr(cls, canonical_name) + + @property + def family(self): + """Get the hash function family.""" + return self._family + @property def is_xof(self): """Indicate whether the hash is an extendable-output hash function.""" - return self.startswith("shake_") + return self.family is HashFamily.shake @property def is_keyed(self): """Indicate whether the hash is a keyed hash function.""" - return self.startswith("blake2") + return self.family is HashFamily.blake2 CANONICAL_DIGEST_NAMES = frozenset(map(str, HashId.__members__)) From f2bbd72fcc9263ed152cd4b613cda83f7a8a249f 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, 3 Aug 2025 13:21:30 +0200 Subject: [PATCH 12/22] simplify `_block_builtin_hmac_constructor` --- Lib/test/support/hashlib_helper.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 4421715191b339..64058fa15611e8 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -1065,23 +1065,16 @@ def dummy(data=b'', *, usedforsecurity=True, string=b''): def _block_builtin_hmac_constructor(name): """Block explicit HACL* HMAC constructors.""" - fullname = _EXPLICIT_HMAC_CONSTRUCTORS[name] - if fullname is None: + info = _HashInfoItem(_EXPLICIT_HMAC_CONSTRUCTORS[name]) + assert info.module_name is None or info.module_name == "_hmac", info + if (wrapped := info.import_member()) 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)) + @functools.wraps(wrapped) def wrapper(key, obj): raise ValueError(f"blocked hash name: {name}") _ensure_wrapper_signature(wrapper, wrapped) - return unittest.mock.patch(fullname, wrapper) + return unittest.mock.patch(info.fullname, wrapper) @contextlib.contextmanager From 20f68a1ce4f06f0f4c6b2f356878fb50dd711f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:40:46 +0200 Subject: [PATCH 13/22] improve skips --- Lib/test/support/hashlib_helper.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 64058fa15611e8..9391e3467eeb05 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -281,9 +281,11 @@ def import_module(self, implementation, *, allow_skip=False): On error, return None if *allow_skip* is false, or raise SkipNoHash. """ - module = self[implementation].import_module() + target = self[implementation] + module = target.import_module() if allow_skip and module is None: - raise SkipNoHash(self.canonical_name, implementation) + reason = f"cannot import module {target.module_name}" + raise SkipNoHash(self.canonical_name, implementation, reason) return module def import_object_type(self, implementation, *, allow_skip=False): @@ -291,10 +293,11 @@ def import_object_type(self, implementation, *, allow_skip=False): On error, return None if *allow_skip* is false, or raise SkipNoHash. """ - member = self[implementation].import_member() + target = self[implementation] + member = target.import_member() if allow_skip and member is None: - raise SkipNoHash(self.name, implementation, interface="class") - assert isinstance(member, type | None), member + reason = f"cannot import class {target.fullname}" + raise SkipNoHash(self.canonical_name, implementation, reason) return member @@ -604,13 +607,19 @@ def requires_builtin_hmac(): class SkipNoHash(unittest.SkipTest): """A SkipTest exception raised when a hash is not available.""" - def __init__(self, digestname, implementation=None, interface=None): + def __init__(self, digestname, implementation=None, reason=None): parts = ["missing", implementation, f"hash algorithm {digestname!r}"] - if interface is not None: - parts.append(f"for {interface}") + if reason is not None: + parts.insert(0, f"{reason}: ") super().__init__(" ".join(filter(None, parts))) +class SkipNoHashInCall(SkipNoHash): + + def __init__(self, func, digestname, implementation=None): + super().__init__(digestname, implementation, f"cannot use {func}") + + def _hashlib_new(digestname, openssl, /, **kwargs): """Check availability of [hashlib|_hashlib].new(digestname, **kwargs). @@ -630,8 +639,7 @@ def _hashlib_new(digestname, openssl, /, **kwargs): try: module.new(digestname, **kwargs) except ValueError as exc: - interface = f"{module.__name__}.new" - raise SkipNoHash(digestname, interface=interface) from exc + raise SkipNoHashInCall(f"{module.__name__}.new", digestname) from exc return functools.partial(module.new, digestname) @@ -676,7 +684,7 @@ def _openssl_new(digestname, /, **kwargs): try: _hashlib.new(digestname, **kwargs) except ValueError as exc: - raise SkipNoHash(digestname, interface="_hashlib.new") from exc + raise SkipNoHashInCall("_hashlib.new", digestname) from exc return functools.partial(_hashlib.new, digestname) From dc97ad039148c0b3d5d1680114165a0a7148e709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:42:32 +0200 Subject: [PATCH 14/22] reduce formatter duplication --- 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 9391e3467eeb05..12dba6f1107b52 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -695,14 +695,15 @@ def _openssl_hash(digestname, /, **kwargs): or SkipTest is raised if none exists. """ assert isinstance(digestname, str), digestname - fullname = f"_hashlib.openssl_{digestname}" + method_name = f"openssl_{digestname}" + fullname = f"_hashlib.{method_name}" 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) + constructor = getattr(_hashlib, method_name, None) except AttributeError as exc: raise SkipNoHash(fullname, "openssl") from exc try: From 9bffba892b3d6963f56cd179bd6b5d91c88c6d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:51:19 +0200 Subject: [PATCH 15/22] Revert "introduce information for hash functions family" This reverts commit 0d858e336ed4cc915bcbcc5e7f5fb136508ead26. --- Lib/test/support/hashlib_helper.py | 85 +++++++----------------------- 1 file changed, 20 insertions(+), 65 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 12dba6f1107b52..3acf82886c069b 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -65,17 +65,6 @@ class Implementation(enum.StrEnum): hashlib = enum.auto() -class HashFamily(enum.StrEnum): - """Enumerationg containing the algorithmic families known hashes.""" - - md5 = enum.auto() - sha1 = enum.auto() - sha2 = enum.auto() - sha3 = enum.auto() - shake = enum.auto() - blake2 = enum.auto() - - class HashId(enum.StrEnum): """Enumeration containing the canonical digest names. @@ -83,71 +72,37 @@ class HashId(enum.StrEnum): Their support by _hashlib.new() is not necessarily guaranteed. """ - # MD5 family - md5 = (enum.auto(), HashFamily.md5) - - # SHA-1 family - sha1 = (enum.auto(), HashFamily.sha1) - - # SHA-2 family - sha224 = (enum.auto(), HashFamily.sha2) - sha256 = (enum.auto(), HashFamily.sha2) - sha384 = (enum.auto(), HashFamily.sha2) - sha512 = (enum.auto(), HashFamily.sha2) - - # SHA-3 family - sha3_224 = (enum.auto(), HashFamily.sha3) - sha3_256 = (enum.auto(), HashFamily.sha3) - sha3_384 = (enum.auto(), HashFamily.sha3) - sha3_512 = (enum.auto(), HashFamily.sha3) - - # SHA-3-XOF family - shake_128 = (enum.auto(), HashFamily.shake) - shake_256 = (enum.auto(), HashFamily.shake) - - # BLAKE-2 family - blake2s = (enum.auto(), HashFamily.blake2) - blake2b = (enum.auto(), HashFamily.blake2) - - def __new__(cls, *args): - if len(args) == 1: - try: - return getattr(cls, args[0]) - except AttributeError as exc: - raise ValueError(f"unknown hash algorithm: {args[0]}") from exc - elif len(args) != 2: - raise TypeError(f"HashId expects 1 to 2 arguments," - f" got {len(args)}") - value, family = args - assert isinstance(family, HashFamily), family - self = str.__new__(cls, value) - self._value_ = value - self._family = family - return self + md5 = enum.auto() + sha1 = enum.auto() - def __repr__(self): - return str(self) + sha224 = enum.auto() + sha256 = enum.auto() + sha384 = enum.auto() + sha512 = enum.auto() - @classmethod - def from_name(cls, canonical_name): - if isinstance(canonical_name, cls): - return canonical_name - return getattr(cls, canonical_name) + sha3_224 = enum.auto() + sha3_256 = enum.auto() + sha3_384 = enum.auto() + sha3_512 = enum.auto() - @property - def family(self): - """Get the hash function family.""" - return self._family + shake_128 = enum.auto() + shake_256 = enum.auto() + + blake2s = enum.auto() + blake2b = enum.auto() + + def __repr__(self): + return str(self) @property def is_xof(self): """Indicate whether the hash is an extendable-output hash function.""" - return self.family is HashFamily.shake + return self.startswith("shake_") @property def is_keyed(self): """Indicate whether the hash is a keyed hash function.""" - return self.family is HashFamily.blake2 + return self.startswith("blake2") CANONICAL_DIGEST_NAMES = frozenset(map(str, HashId.__members__)) From 3cb60662edeecdd2ee991cd6d8aa3b9a5f9da8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:10:23 +0200 Subject: [PATCH 16/22] reduce diff by removing properties --- Lib/test/support/hashlib_helper.py | 34 +++++++----------------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 3acf82886c069b..92ed0a7a8103b8 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -123,21 +123,9 @@ class _HashInfoItem: def __init__(self, fullname=None, *, strict=False): module_name, member_name = _parse_fullname(fullname, strict=strict) - self._fullname = fullname - self._module_name = module_name - self._member_name = member_name - - @property - def fullname(self): - return self._fullname - - @property - def module_name(self): - return self._module_name - - @property - def member_name(self): - return self._member_name + self.fullname = fullname + self.module_name = module_name + self.member_name = member_name def import_module(self, *, strict=False): """Import the described module. @@ -171,12 +159,7 @@ class _HashInfoBase: """ def __init__(self, canonical_name): - self._canonical_name = canonical_name - - @property - def canonical_name(self): - """The canonical hash name.""" - return self._canonical_name + self.canonical_name = canonical_name def __getitem__(self, implementation): try: @@ -325,22 +308,21 @@ class _HashInfo: def __init__( self, - name, + canonical_name, builtin_object_type_fullname, openssl_object_type_fullname, builtin_method_fullname, openssl_method_fullname=None, hashlib_method_fullname=None, ): - self.name = name - + self.canonical_name = canonical_name self.type = _HashTypeInfo( - name, + canonical_name, builtin_object_type_fullname, openssl_object_type_fullname, ) self.func = _HashFuncInfo( - name, + canonical_name, builtin_method_fullname, openssl_method_fullname, hashlib_method_fullname, From 5572288b54d482828c268bba6ab93ef8f956bfa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:10:33 +0200 Subject: [PATCH 17/22] cleanup HMAC info database --- Lib/test/support/hashlib_helper.py | 26 ++++++++++++++++---------- Lib/test/test_support.py | 4 ++-- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 92ed0a7a8103b8..c599587ca6bb24 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -467,19 +467,25 @@ def _iter_hash_func_info(excluded): # 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 = { - HashId(name): f"_hmac.compute_{name}" - for name in CANONICAL_DIGEST_NAMES +_HMACINFO_DATABASE = { + HashId(canonical_name): _HashInfoItem(f"_hmac.compute_{canonical_name}") + for canonical_name in CANONICAL_DIGEST_NAMES } # Neither HACL* nor OpenSSL supports HMAC over XOFs. -_EXPLICIT_HMAC_CONSTRUCTORS[HashId.shake_128] = None -_EXPLICIT_HMAC_CONSTRUCTORS[HashId.shake_256] = None +_HMACINFO_DATABASE[HashId.shake_128] = _HashInfoItem() +_HMACINFO_DATABASE[HashId.shake_256] = _HashInfoItem() # 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[HashId.blake2s] = '_hmac.compute_blake2s_32' -_EXPLICIT_HMAC_CONSTRUCTORS[HashId.blake2b] = '_hmac.compute_blake2b_32' -_EXPLICIT_HMAC_CONSTRUCTORS = MappingProxyType(_EXPLICIT_HMAC_CONSTRUCTORS) -assert _EXPLICIT_HMAC_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES +_HMACINFO_DATABASE[HashId.blake2s] = _HashInfoItem('_hmac.compute_blake2s_32') +_HMACINFO_DATABASE[HashId.blake2b] = _HashInfoItem('_hmac.compute_blake2b_32') +_HMACINFO_DATABASE = MappingProxyType(_HMACINFO_DATABASE) +assert _HMACINFO_DATABASE.keys() == CANONICAL_DIGEST_NAMES + + +def get_hmac_info_item(name): + info = _HMACINFO_DATABASE[name] + assert isinstance(info, _HashInfoItem), info + return info def _decorate_func_or_class(decorator_func, func_or_class): @@ -1011,7 +1017,7 @@ def dummy(data=b'', *, usedforsecurity=True, string=b''): def _block_builtin_hmac_constructor(name): """Block explicit HACL* HMAC constructors.""" - info = _HashInfoItem(_EXPLICIT_HMAC_CONSTRUCTORS[name]) + info = get_hmac_info_item(name) assert info.module_name is None or info.module_name == "_hmac", info if (wrapped := info.import_member()) is None: # function shouldn't exist for this implementation diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 8bb9a63d856688..c6fd05c4c43b4e 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -879,8 +879,8 @@ def fetch_hash_function(self, name, implementation): return self.try_import_attribute(fullname) def fetch_hmac_function(self, name): - fullname = hashlib_helper._EXPLICIT_HMAC_CONSTRUCTORS[name] - return self.try_import_attribute(fullname) + target = hashlib_helper.get_hmac_info_item(name) + return target.import_member() def check_openssl_hash(self, name, *, disabled=True): """Check that OpenSSL HASH interface is enabled/disabled.""" From e2a37473fc13fc5bd36c4759c0d4c4382b3c2566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:58:51 +0200 Subject: [PATCH 18/22] fixups --- Lib/test/support/hashlib_helper.py | 9 +++++---- Lib/test/test_support.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index c599587ca6bb24..2f8e62e648cc1f 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -20,7 +20,8 @@ def _parse_fullname(fullname, *, strict=False): return None, None assert isinstance(fullname, str), fullname assert fullname.count(".") >= 1, fullname - return fullname.rsplit(".", maxsplit=1) + module_name, member_name = fullname.rsplit(".", maxsplit=1) + return module_name, member_name def _import_module(module_name, *, strict=False): @@ -482,7 +483,7 @@ def _iter_hash_func_info(excluded): assert _HMACINFO_DATABASE.keys() == CANONICAL_DIGEST_NAMES -def get_hmac_info_item(name): +def get_hmac_func_info(name): info = _HMACINFO_DATABASE[name] assert isinstance(info, _HashInfoItem), info return info @@ -646,7 +647,7 @@ def _openssl_hash(digestname, /, **kwargs): except ImportError as exc: raise SkipNoHash(fullname, "openssl") from exc try: - constructor = getattr(_hashlib, method_name, None) + constructor = getattr(_hashlib, method_name) except AttributeError as exc: raise SkipNoHash(fullname, "openssl") from exc try: @@ -1017,7 +1018,7 @@ def dummy(data=b'', *, usedforsecurity=True, string=b''): def _block_builtin_hmac_constructor(name): """Block explicit HACL* HMAC constructors.""" - info = get_hmac_info_item(name) + info = get_hmac_func_info(name) assert info.module_name is None or info.module_name == "_hmac", info if (wrapped := info.import_member()) is None: # function shouldn't exist for this implementation diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index c6fd05c4c43b4e..6576169db8dc50 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -879,7 +879,7 @@ def fetch_hash_function(self, name, implementation): return self.try_import_attribute(fullname) def fetch_hmac_function(self, name): - target = hashlib_helper.get_hmac_info_item(name) + target = hashlib_helper.get_hmac_func_info(name) return target.import_member() def check_openssl_hash(self, name, *, disabled=True): From 076bd77456178cf77f811bbdd008800131921056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:20:01 +0200 Subject: [PATCH 19/22] cosmetics --- Lib/test/support/hashlib_helper.py | 121 +++++++++++++---------------- Lib/test/test_support.py | 2 +- 2 files changed, 56 insertions(+), 67 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 2f8e62e648cc1f..80748ea3b3cb3c 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -66,7 +66,7 @@ class Implementation(enum.StrEnum): hashlib = enum.auto() -class HashId(enum.StrEnum): +class _HashId(enum.StrEnum): """Enumeration containing the canonical digest names. Those names should only be used by hashlib.new() or hmac.new(). @@ -106,10 +106,10 @@ def is_keyed(self): return self.startswith("blake2") -CANONICAL_DIGEST_NAMES = frozenset(map(str, HashId.__members__)) +CANONICAL_DIGEST_NAMES = frozenset(map(str, _HashId.__members__)) NON_HMAC_DIGEST_NAMES = frozenset(( - HashId.shake_128, HashId.shake_256, - HashId.blake2s, HashId.blake2b, + _HashId.shake_128, _HashId.shake_256, + _HashId.blake2s, _HashId.blake2b, )) @@ -331,112 +331,112 @@ def __init__( _HASHINFO_DATABASE = MappingProxyType({ - HashId.md5: _HashInfo( - HashId.md5, + _HashId.md5: _HashInfo( + _HashId.md5, "_md5.MD5Type", "_hashlib.HASH", "_md5.md5", "_hashlib.openssl_md5", "hashlib.md5", ), - HashId.sha1: _HashInfo( - HashId.sha1, + _HashId.sha1: _HashInfo( + _HashId.sha1, "_sha1.SHA1Type", "_hashlib.HASH", "_sha1.sha1", "_hashlib.openssl_sha1", "hashlib.sha1", ), - HashId.sha224: _HashInfo( - HashId.sha224, + _HashId.sha224: _HashInfo( + _HashId.sha224, "_sha2.SHA224Type", "_hashlib.HASH", "_sha2.sha224", "_hashlib.openssl_sha224", "hashlib.sha224", ), - HashId.sha256: _HashInfo( - HashId.sha256, + _HashId.sha256: _HashInfo( + _HashId.sha256, "_sha2.SHA256Type", "_hashlib.HASH", "_sha2.sha256", "_hashlib.openssl_sha256", "hashlib.sha256", ), - HashId.sha384: _HashInfo( - HashId.sha384, + _HashId.sha384: _HashInfo( + _HashId.sha384, "_sha2.SHA384Type", "_hashlib.HASH", "_sha2.sha384", "_hashlib.openssl_sha384", "hashlib.sha384", ), - HashId.sha512: _HashInfo( - HashId.sha512, + _HashId.sha512: _HashInfo( + _HashId.sha512, "_sha2.SHA512Type", "_hashlib.HASH", "_sha2.sha512", "_hashlib.openssl_sha512", "hashlib.sha512", ), - HashId.sha3_224: _HashInfo( - HashId.sha3_224, + _HashId.sha3_224: _HashInfo( + _HashId.sha3_224, "_sha3.sha3_224", "_hashlib.HASH", "_sha3.sha3_224", "_hashlib.openssl_sha3_224", "hashlib.sha3_224", ), - HashId.sha3_256: _HashInfo( - HashId.sha3_256, + _HashId.sha3_256: _HashInfo( + _HashId.sha3_256, "_sha3.sha3_256", "_hashlib.HASH", "_sha3.sha3_256", "_hashlib.openssl_sha3_256", "hashlib.sha3_256", ), - HashId.sha3_384: _HashInfo( - HashId.sha3_384, + _HashId.sha3_384: _HashInfo( + _HashId.sha3_384, "_sha3.sha3_384", "_hashlib.HASH", "_sha3.sha3_384", "_hashlib.openssl_sha3_384", "hashlib.sha3_384", ), - HashId.sha3_512: _HashInfo( - HashId.sha3_512, + _HashId.sha3_512: _HashInfo( + _HashId.sha3_512, "_sha3.sha3_512", "_hashlib.HASH", "_sha3.sha3_512", "_hashlib.openssl_sha3_512", "hashlib.sha3_512", ), - HashId.shake_128: _HashInfo( - HashId.shake_128, + _HashId.shake_128: _HashInfo( + _HashId.shake_128, "_sha3.shake_128", "_hashlib.HASHXOF", "_sha3.shake_128", "_hashlib.openssl_shake_128", "hashlib.shake_128", ), - HashId.shake_256: _HashInfo( - HashId.shake_256, + _HashId.shake_256: _HashInfo( + _HashId.shake_256, "_sha3.shake_256", "_hashlib.HASHXOF", "_sha3.shake_256", "_hashlib.openssl_shake_256", "hashlib.shake_256", ), - HashId.blake2s: _HashInfo( - HashId.blake2s, + _HashId.blake2s: _HashInfo( + _HashId.blake2s, "_blake2.blake2s", "_hashlib.HASH", "_blake2.blake2s", None, "hashlib.blake2s", ), - HashId.blake2b: _HashInfo( - HashId.blake2b, + _HashId.blake2b: _HashInfo( + _HashId.blake2b, "_blake2.blake2b", "_hashlib.HASH", "_blake2.blake2b", @@ -469,21 +469,21 @@ def _iter_hash_func_info(excluded): # There is currently no OpenSSL one-shot named function and there will likely # be none in the future. _HMACINFO_DATABASE = { - HashId(canonical_name): _HashInfoItem(f"_hmac.compute_{canonical_name}") + _HashId(canonical_name): _HashInfoItem(f"_hmac.compute_{canonical_name}") for canonical_name in CANONICAL_DIGEST_NAMES } # Neither HACL* nor OpenSSL supports HMAC over XOFs. -_HMACINFO_DATABASE[HashId.shake_128] = _HashInfoItem() -_HMACINFO_DATABASE[HashId.shake_256] = _HashInfoItem() +_HMACINFO_DATABASE[_HashId.shake_128] = _HashInfoItem() +_HMACINFO_DATABASE[_HashId.shake_256] = _HashInfoItem() # Strictly speaking, HMAC-BLAKE is meaningless as BLAKE2 is already a # keyed hash function. However, as it's exposed by HACL*, we test it. -_HMACINFO_DATABASE[HashId.blake2s] = _HashInfoItem('_hmac.compute_blake2s_32') -_HMACINFO_DATABASE[HashId.blake2b] = _HashInfoItem('_hmac.compute_blake2b_32') +_HMACINFO_DATABASE[_HashId.blake2s] = _HashInfoItem('_hmac.compute_blake2s_32') +_HMACINFO_DATABASE[_HashId.blake2b] = _HashInfoItem('_hmac.compute_blake2b_32') _HMACINFO_DATABASE = MappingProxyType(_HMACINFO_DATABASE) assert _HMACINFO_DATABASE.keys() == CANONICAL_DIGEST_NAMES -def get_hmac_func_info(name): +def get_hmac_item_info(name): info = _HMACINFO_DATABASE[name] assert isinstance(info, _HashInfoItem), info return info @@ -538,6 +538,16 @@ def _ensure_wrapper_signature(wrapper, wrapped): ) +def _make_conditional_decorator(test, /, *test_args, **test_kwargs): + def decorator_func(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + test(*test_args, **test_kwargs) + return func(*args, **kwargs) + return wrapper + return functools.partial(_decorate_func_or_class, decorator_func) + + def requires_openssl_hashlib(): _hashlib = _import_module("_hashlib") return unittest.skipIf(_hashlib is None, "requires _hashlib") @@ -657,16 +667,6 @@ def _openssl_hash(digestname, /, **kwargs): return constructor -def _make_requires_hashdigest_decorator(test, /, *test_args, **test_kwargs): - def decorator_func(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - test(*test_args, **test_kwargs) - return func(*args, **kwargs) - return wrapper - return functools.partial(_decorate_func_or_class, decorator_func) - - def requires_hashdigest(digestname, openssl=None, *, usedforsecurity=True): """Decorator raising SkipTest if a hashing algorithm is not available. @@ -684,7 +684,7 @@ def requires_hashdigest(digestname, openssl=None, *, usedforsecurity=True): ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS ValueError: unsupported hash type md4 """ - return _make_requires_hashdigest_decorator( + return _make_conditional_decorator( _hashlib_new, digestname, openssl, usedforsecurity=usedforsecurity ) @@ -694,14 +694,14 @@ def requires_openssl_hashdigest(digestname, *, usedforsecurity=True): The hashing algorithm may be missing or blocked by a strict crypto policy. """ - return _make_requires_hashdigest_decorator( + return _make_conditional_decorator( _openssl_new, digestname, usedforsecurity=usedforsecurity ) def _make_requires_builtin_hashdigest_decorator(item, *, usedforsecurity=True): assert isinstance(item, _HashInfoItem), item - return _make_requires_hashdigest_decorator( + return _make_conditional_decorator( _builtin_hash, item.module_name, item.member_name, @@ -739,24 +739,13 @@ class HashFunctionsTrait: implementation of HMAC). """ - DIGEST_NAMES = [ - 'md5', 'sha1', - 'sha224', 'sha256', 'sha384', 'sha512', - 'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512', - ] - # Default 'usedforsecurity' to use when checking a hash function. # When the trait properties are callables (e.g., _md5.md5) and # not strings, they must be called with the same 'usedforsecurity'. usedforsecurity = True - @classmethod - def setUpClass(cls): - super().setUpClass() - assert CANONICAL_DIGEST_NAMES.issuperset(cls.DIGEST_NAMES) - def is_valid_digest_name(self, digestname): - self.assertIn(digestname, self.DIGEST_NAMES) + self.assertIn(digestname, _HashId) def _find_constructor(self, digestname): # By default, a missing algorithm skips the test that uses it. @@ -922,7 +911,7 @@ 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 HashId, f"invalid hash: {name}" + assert name in _HashId, f"invalid hash: {name}" # Re-import 'hashlib' in case it was mocked hashlib = importlib.import_module('hashlib') @@ -947,7 +936,7 @@ def get_builtin_constructor_mock(name): return unittest.mock.patch.multiple( hashlib, __get_builtin_constructor=get_builtin_constructor_mock, - __builtin_constructor_cache=builtin_constructor_cache_mock + __builtin_constructor_cache=builtin_constructor_cache_mock, ) @@ -1018,7 +1007,7 @@ def dummy(data=b'', *, usedforsecurity=True, string=b''): def _block_builtin_hmac_constructor(name): """Block explicit HACL* HMAC constructors.""" - info = get_hmac_func_info(name) + info = get_hmac_item_info(name) assert info.module_name is None or info.module_name == "_hmac", info if (wrapped := info.import_member()) is None: # function shouldn't exist for this implementation diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 6576169db8dc50..ef72e3e5b58ec3 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -879,7 +879,7 @@ def fetch_hash_function(self, name, implementation): return self.try_import_attribute(fullname) def fetch_hmac_function(self, name): - target = hashlib_helper.get_hmac_func_info(name) + target = hashlib_helper.get_hmac_item_info(name) return target.import_member() def check_openssl_hash(self, name, *, disabled=True): From 4b26903d5cd38eeb8ffecdf886eccab24c4d0478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:20:06 +0200 Subject: [PATCH 20/22] simplify trait interface --- Lib/test/support/hashlib_helper.py | 49 +++++++----------------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 80748ea3b3cb3c..df278df3e42533 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -752,45 +752,18 @@ def _find_constructor(self, digestname): self.is_valid_digest_name(digestname) self.skipTest(f"missing hash function: {digestname}") - @property - def md5(self): - return self._find_constructor("md5") + md5 = property(lambda self: self._find_constructor("md5")) + sha1 = property(lambda self: self._find_constructor("sha1")) - @property - def sha1(self): - return self._find_constructor("sha1") + sha224 = property(lambda self: self._find_constructor("sha224")) + sha256 = property(lambda self: self._find_constructor("sha256")) + sha384 = property(lambda self: self._find_constructor("sha384")) + sha512 = property(lambda self: self._find_constructor("sha512")) - @property - def sha224(self): - return self._find_constructor("sha224") - - @property - def sha256(self): - return self._find_constructor("sha256") - - @property - def sha384(self): - return self._find_constructor("sha384") - - @property - def sha512(self): - return self._find_constructor("sha512") - - @property - def sha3_224(self): - return self._find_constructor("sha3_224") - - @property - def sha3_256(self): - return self._find_constructor("sha3_256") - - @property - def sha3_384(self): - return self._find_constructor("sha3_384") - - @property - def sha3_512(self): - return self._find_constructor("sha3_512") + sha3_224 = property(lambda self: self._find_constructor("sha3_224")) + sha3_256 = property(lambda self: self._find_constructor("sha3_256")) + sha3_384 = property(lambda self: self._find_constructor("sha3_384")) + sha3_512 = property(lambda self: self._find_constructor("sha3_512")) class NamedHashFunctionsTrait(HashFunctionsTrait): @@ -801,7 +774,7 @@ class NamedHashFunctionsTrait(HashFunctionsTrait): def _find_constructor(self, digestname): self.is_valid_digest_name(digestname) - return digestname + return str(digestname) # ensure that we are an exact string class OpenSSLHashFunctionsTrait(HashFunctionsTrait): From a80ad5c7dc051623449151e201a6685e86729f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:21:47 +0200 Subject: [PATCH 21/22] cosmetics --- Lib/test/support/hashlib_helper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index df278df3e42533..f7568fdb89f591 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -985,9 +985,11 @@ def _block_builtin_hmac_constructor(name): if (wrapped := info.import_member()) is None: # function shouldn't exist for this implementation return contextlib.nullcontext() + @functools.wraps(wrapped) def wrapper(key, obj): raise ValueError(f"blocked hash name: {name}") + _ensure_wrapper_signature(wrapper, wrapped) return unittest.mock.patch(info.fullname, wrapper) From 435690642905eef6fb03f33246e01f72b21400eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 5 Aug 2025 11:23:28 +0200 Subject: [PATCH 22/22] explicitly require to pass `exclude=...` to avoid confusions --- Lib/test/support/hashlib_helper.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index f7568fdb89f591..c9962a0ae14862 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -717,12 +717,12 @@ def requires_builtin_hashdigest(canonical_name, *, usedforsecurity=True): ) -def requires_builtin_hashes(*ignored, usedforsecurity=True): +def requires_builtin_hashes(*, exclude=(), usedforsecurity=True): """Decorator raising SkipTest if one HACL* hashing algorithm is missing.""" return _chain_decorators(( _make_requires_builtin_hashdigest_decorator( info.builtin, usedforsecurity=usedforsecurity - ) for info in _iter_hash_func_info(ignored) + ) for info in _iter_hash_func_info(exclude) )) @@ -1058,18 +1058,18 @@ def block_algorithm(name, *, allow_openssl=False, allow_builtin=False): @contextlib.contextmanager -def block_openssl_algorithms(*ignored): - """Block OpenSSL implementations, except those given in *ignored*.""" +def block_openssl_algorithms(*, exclude=()): + """Block OpenSSL implementations, except those given in *exclude*.""" with contextlib.ExitStack() as stack: - for name in CANONICAL_DIGEST_NAMES.difference(ignored): + for name in CANONICAL_DIGEST_NAMES.difference(exclude): stack.enter_context(block_algorithm(name, allow_builtin=True)) yield @contextlib.contextmanager -def block_builtin_algorithms(*ignored): - """Block HACL* implementations, except those given in *ignored*.""" +def block_builtin_algorithms(*, exclude=()): + """Block HACL* implementations, except those given in *exclude*.""" with contextlib.ExitStack() as stack: - for name in CANONICAL_DIGEST_NAMES.difference(ignored): + for name in CANONICAL_DIGEST_NAMES.difference(exclude): stack.enter_context(block_algorithm(name, allow_openssl=True)) yield