https://github.com/python/cpython/commit/9634085af3670b1eb654e3c7820aca66f358f39f
commit: 9634085af3670b1eb654e3c7820aca66f358f39f
branch: main
author: Bénédikt Tran <10796600+picn...@users.noreply.github.com>
committer: picnixz <10796600+picn...@users.noreply.github.com>
date: 2025-04-12T17:43:11Z
summary:

gh-132388: Increase test coverage for HMAC (#132389)

- Correctly test missing `digestmod` and `digest` parameters.
- Test when chunks of length > 2048 are passed to `update()`.
- Test one-shot HMAC-BLAKE2.

files:
M Lib/hmac.py
M Lib/test/test_hmac.py

diff --git a/Lib/hmac.py b/Lib/hmac.py
index 2af11c26947064..3683a4aa653a0a 100644
--- a/Lib/hmac.py
+++ b/Lib/hmac.py
@@ -81,13 +81,13 @@ def __init(self, key, msg, digestmod):
             try:
                 self._init_openssl_hmac(key, msg, digestmod)
                 return
-            except _hashopenssl.UnsupportedDigestmodError:
+            except _hashopenssl.UnsupportedDigestmodError:  # pragma: no cover
                 pass
         if _hmac and isinstance(digestmod, str):
             try:
                 self._init_builtin_hmac(key, msg, digestmod)
                 return
-            except _hmac.UnknownHashError:
+            except _hmac.UnknownHashError:  # pragma: no cover
                 pass
         self._init_old(key, msg, digestmod)
 
@@ -121,12 +121,12 @@ def _init_old(self, key, msg, digestmod):
                 warnings.warn(f"block_size of {blocksize} seems too small; "
                               f"using our default of {self.blocksize}.",
                               RuntimeWarning, 2)
-                blocksize = self.blocksize
+                blocksize = self.blocksize  # pragma: no cover
         else:
             warnings.warn("No block_size attribute on given digest object; "
                           f"Assuming {self.blocksize}.",
                           RuntimeWarning, 2)
-            blocksize = self.blocksize
+            blocksize = self.blocksize  # pragma: no cover
 
         if len(key) > blocksize:
             key = digest_cons(key).digest()
diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py
index 42b8a91ae580d2..3055a036c811b6 100644
--- a/Lib/test/test_hmac.py
+++ b/Lib/test/test_hmac.py
@@ -6,11 +6,11 @@
 import test.support.hashlib_helper as hashlib_helper
 import types
 import unittest
-import unittest.mock
+import unittest.mock as mock
 import warnings
 from _operator import _compare_digest as operator_compare_digest
 from test.support import check_disallow_instantiation
-from test.support.import_helper import import_fresh_module
+from test.support.import_helper import import_fresh_module, import_module
 
 try:
     import _hashlib
@@ -58,10 +58,14 @@ def setUpClass(cls):
         cls.hmac = import_fresh_module('_hmac')
 
 
+# Sentinel object used to detect whether a digestmod is given or not.
+DIGESTMOD_SENTINEL = object()
+
+
 class CreatorMixin:
     """Mixin exposing a method creating a HMAC object."""
 
-    def hmac_new(self, key, msg=None, digestmod=None):
+    def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
         """Create a new HMAC object.
 
         Implementations should accept arbitrary 'digestmod' as this
@@ -77,7 +81,7 @@ def bind_hmac_new(self, digestmod):
 class DigestMixin:
     """Mixin exposing a method computing a HMAC digest."""
 
-    def hmac_digest(self, key, msg=None, digestmod=None):
+    def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
         """Compute a HMAC digest.
 
         Implementations should accept arbitrary 'digestmod' as this
@@ -90,6 +94,20 @@ def bind_hmac_digest(self, digestmod):
         return functools.partial(self.hmac_digest, digestmod=digestmod)
 
 
+def _call_newobj_func(new_func, key, msg, digestmod):
+    if digestmod is DIGESTMOD_SENTINEL:  # to test when digestmod is missing
+        return new_func(key, msg)  # expected to raise
+    # functions creating HMAC objects take a 'digestmod' keyword argument
+    return new_func(key, msg, digestmod=digestmod)
+
+
+def _call_digest_func(digest_func, key, msg, digestmod):
+    if digestmod is DIGESTMOD_SENTINEL:  # to test when digestmod is missing
+        return digest_func(key, msg)  # expected to raise
+    # functions directly computing digests take a 'digest' keyword argument
+    return digest_func(key, msg, digest=digestmod)
+
+
 class ThroughObjectMixin(ModuleMixin, CreatorMixin, DigestMixin):
     """Mixin delegating to <module>.HMAC() and <module>.HMAC(...).digest().
 
@@ -97,46 +115,46 @@ class ThroughObjectMixin(ModuleMixin, CreatorMixin, 
DigestMixin):
     expose a HMAC class with the same functionalities.
     """
 
-    def hmac_new(self, key, msg=None, digestmod=None):
+    def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
         """Create a HMAC object via a module-level class constructor."""
-        return self.hmac.HMAC(key, msg, digestmod=digestmod)
+        return _call_newobj_func(self.hmac.HMAC, key, msg, digestmod)
 
-    def hmac_digest(self, key, msg=None, digestmod=None):
+    def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
         """Call the digest() method on a HMAC object obtained by hmac_new()."""
-        return self.hmac_new(key, msg, digestmod).digest()
+        return _call_newobj_func(self.hmac_new, key, msg, digestmod).digest()
 
 
 class ThroughModuleAPIMixin(ModuleMixin, CreatorMixin, DigestMixin):
     """Mixin delegating to <module>.new() and <module>.digest()."""
 
-    def hmac_new(self, key, msg=None, digestmod=None):
+    def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
         """Create a HMAC object via a module-level function."""
-        return self.hmac.new(key, msg, digestmod=digestmod)
+        return _call_newobj_func(self.hmac.new, key, msg, digestmod)
 
-    def hmac_digest(self, key, msg=None, digestmod=None):
+    def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
         """One-shot HMAC digest computation."""
-        return self.hmac.digest(key, msg, digest=digestmod)
+        return _call_digest_func(self.hmac.digest, key, msg, digestmod)
 
 
 @hashlib_helper.requires_hashlib()
 class ThroughOpenSSLAPIMixin(CreatorMixin, DigestMixin):
     """Mixin delegating to _hashlib.hmac_new() and _hashlib.hmac_digest()."""
 
-    def hmac_new(self, key, msg=None, digestmod=None):
-        return _hashlib.hmac_new(key, msg, digestmod=digestmod)
+    def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
+        return _call_newobj_func(_hashlib.hmac_new, key, msg, digestmod)
 
-    def hmac_digest(self, key, msg=None, digestmod=None):
-        return _hashlib.hmac_digest(key, msg, digest=digestmod)
+    def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
+        return _call_digest_func(_hashlib.hmac_digest, key, msg, digestmod)
 
 
 class ThroughBuiltinAPIMixin(BuiltinModuleMixin, CreatorMixin, DigestMixin):
     """Mixin delegating to _hmac.new() and _hmac.compute_digest()."""
 
-    def hmac_new(self, key, msg=None, digestmod=None):
-        return self.hmac.new(key, msg, digestmod=digestmod)
+    def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
+        return _call_newobj_func(self.hmac.new, key, msg, digestmod)
 
-    def hmac_digest(self, key, msg=None, digestmod=None):
-        return self.hmac.compute_digest(key, msg, digest=digestmod)
+    def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
+        return _call_digest_func(self.hmac.compute_digest, key, msg, digestmod)
 
 
 class ObjectCheckerMixin:
@@ -777,7 +795,8 @@ class DigestModTestCaseMixin(CreatorMixin, DigestMixin):
 
     def assert_raises_missing_digestmod(self):
         """A context manager catching errors when a digestmod is missing."""
-        return self.assertRaisesRegex(TypeError, "Missing required.*digestmod")
+        return self.assertRaisesRegex(TypeError,
+                                      "[M|m]issing.*required.*digestmod")
 
     def assert_raises_unknown_digestmod(self):
         """A context manager catching errors when a digestmod is unknown."""
@@ -804,19 +823,23 @@ def do_test_constructor_unknown_digestmod(self, catcher):
     def cases_missing_digestmod_in_constructor(self):
         raise NotImplementedError
 
-    def make_missing_digestmod_cases(self, func, choices):
-        """Generate cases for missing digestmod tests."""
+    def make_missing_digestmod_cases(self, func, missing_like=()):
+        """Generate cases for missing digestmod tests.
+
+        Only the Python implementation should consider "falsey" 'digestmod'
+        values as being equivalent to a missing one.
+        """
         key, msg = b'unused key', b'unused msg'
-        cases = self._invalid_digestmod_cases(func, key, msg, choices)
-        return [(func, (key,), {}), (func, (key, msg), {})] + cases
+        choices = [DIGESTMOD_SENTINEL, *missing_like]
+        return self._invalid_digestmod_cases(func, key, msg, choices)
 
     def cases_unknown_digestmod_in_constructor(self):
         raise NotImplementedError
 
-    def make_unknown_digestmod_cases(self, func, choices):
+    def make_unknown_digestmod_cases(self, func, bad_digestmods):
         """Generate cases for unknown digestmod tests."""
         key, msg = b'unused key', b'unused msg'
-        return self._invalid_digestmod_cases(func, key, msg, choices)
+        return self._invalid_digestmod_cases(func, key, msg, bad_digestmods)
 
     def _invalid_digestmod_cases(self, func, key, msg, choices):
         cases = []
@@ -932,19 +955,12 @@ def test_internal_types(self):
         with self.assertRaisesRegex(TypeError, "immutable type"):
             self.obj_type.value = None
 
-    def assert_digestmod_error(self):
+    def assert_raises_unknown_digestmod(self):
         self.assertIsSubclass(self.exc_type, ValueError)
         return self.assertRaises(self.exc_type)
 
-    def test_constructor_missing_digestmod(self):
-        self.do_test_constructor_missing_digestmod(self.assert_digestmod_error)
-
-    def test_constructor_unknown_digestmod(self):
-        self.do_test_constructor_unknown_digestmod(self.assert_digestmod_error)
-
     def cases_missing_digestmod_in_constructor(self):
-        func, choices = self.hmac_new, ['', None, False]
-        return self.make_missing_digestmod_cases(func, choices)
+        return self.make_missing_digestmod_cases(self.hmac_new)
 
     def cases_unknown_digestmod_in_constructor(self):
         func, choices = self.hmac_new, ['unknown', 1234]
@@ -967,7 +983,10 @@ def test_hmac_digest_digestmod_parameter(self):
         # TODO(picnixz): remove default arguments in _hashlib.hmac_digest()
         # since the return value is not a HMAC object but a bytes object.
         for value in [object, 'unknown', 1234, None]:
-            with self.subTest(value=value), self.assert_digestmod_error():
+            with (
+                self.subTest(value=value),
+                self.assert_raises_unknown_digestmod(),
+            ):
                 self.hmac_digest(b'key', b'msg', value)
 
 
@@ -985,7 +1004,10 @@ def exc_type(self):
 
     def test_hmac_digest_digestmod_parameter(self):
         for value in [object, 'unknown', 1234, None]:
-            with self.subTest(value=value), self.assert_digestmod_error():
+            with (
+                self.subTest(value=value),
+                self.assert_raises_unknown_digestmod(),
+            ):
                 self.hmac_digest(b'key', b'msg', value)
 
 
@@ -1000,6 +1022,9 @@ class SanityTestCaseMixin(CreatorMixin):
     hmac_class: type
     # The underlying hash function name (should be accepted by the HMAC class).
     digestname: str
+    # The expected digest and block sizes (must be hardcoded).
+    digest_size: int
+    block_size: int
 
     def test_methods(self):
         h = self.hmac_new(b"my secret key", digestmod=self.digestname)
@@ -1009,6 +1034,12 @@ def test_methods(self):
         self.assertIsInstance(h.hexdigest(), str)
         self.assertIsInstance(h.copy(), self.hmac_class)
 
+    def test_properties(self):
+        h = self.hmac_new(b"my secret key", digestmod=self.digestname)
+        self.assertEqual(h.name, f"hmac-{self.digestname}")
+        self.assertEqual(h.digest_size, self.digest_size)
+        self.assertEqual(h.block_size, self.block_size)
+
     def test_repr(self):
         # HMAC object representation may differ across implementations
         raise NotImplementedError
@@ -1023,6 +1054,8 @@ def setUpClass(cls):
         super().setUpClass()
         cls.hmac_class = cls.hmac.HMAC
         cls.digestname = 'sha256'
+        cls.digest_size = 32
+        cls.block_size = 64
 
     def test_repr(self):
         h = self.hmac_new(b"my secret key", digestmod=self.digestname)
@@ -1038,6 +1071,8 @@ def setUpClass(cls):
         super().setUpClass()
         cls.hmac_class = _hashlib.HMAC
         cls.digestname = 'sha256'
+        cls.digest_size = 32
+        cls.block_size = 64
 
     def test_repr(self):
         h = self.hmac_new(b"my secret key", digestmod=self.digestname)
@@ -1052,6 +1087,8 @@ def setUpClass(cls):
         super().setUpClass()
         cls.hmac_class = cls.hmac.HMAC
         cls.digestname = 'sha256'
+        cls.digest_size = 32
+        cls.block_size = 64
 
     def test_repr(self):
         h = self.hmac_new(b"my secret key", digestmod=self.digestname)
@@ -1065,16 +1102,30 @@ def HMAC(self, key, msg=None):
         """Create a HMAC object."""
         raise NotImplementedError
 
+    def check_update(self, key, chunks):
+        chunks = list(chunks)
+        msg = b''.join(chunks)
+        h1 = self.HMAC(key, msg)
+
+        h2 = self.HMAC(key)
+        for chunk in chunks:
+            h2.update(chunk)
+
+        self.assertEqual(h1.digest(), h2.digest())
+        self.assertEqual(h1.hexdigest(), h2.hexdigest())
+
     def test_update(self):
         key, msg = random.randbytes(16), random.randbytes(16)
         with self.subTest(key=key, msg=msg):
-            h1 = self.HMAC(key, msg)
+            self.check_update(key, [msg])
 
-            h2 = self.HMAC(key)
-            h2.update(msg)
+    def test_update_large(self):
+        HASHLIB_GIL_MINSIZE = 2048
 
-            self.assertEqual(h1.digest(), h2.digest())
-            self.assertEqual(h1.hexdigest(), h2.hexdigest())
+        key = random.randbytes(16)
+        top = random.randbytes(HASHLIB_GIL_MINSIZE + 1)
+        bot = random.randbytes(HASHLIB_GIL_MINSIZE + 1)
+        self.check_update(key, [top, bot])
 
     def test_update_exceptions(self):
         h = self.HMAC(b"key")
@@ -1084,12 +1135,7 @@ def test_update_exceptions(self):
 
 
 @hashlib_helper.requires_hashdigest('sha256')
-class PyUpdateTestCase(UpdateTestCaseMixin, unittest.TestCase):
-
-    @classmethod
-    def setUpClass(cls):
-        super().setUpClass()
-        cls.hmac = import_fresh_module('hmac', blocked=['_hashlib', '_hmac'])
+class PyUpdateTestCase(PyModuleMixin, UpdateTestCaseMixin, unittest.TestCase):
 
     def HMAC(self, key, msg=None):
         return self.hmac.HMAC(key, msg, digestmod='sha256')
@@ -1345,6 +1391,32 @@ class OperatorCompareDigestTestCase(CompareDigestMixin, 
unittest.TestCase):
 class PyMiscellaneousTests(unittest.TestCase):
     """Miscellaneous tests for the pure Python HMAC module."""
 
+    @hashlib_helper.requires_builtin_hmac()
+    def test_hmac_constructor_uses_builtin(self):
+        # Block the OpenSSL implementation and check that
+        # HMAC() uses the built-in implementation instead.
+        hmac = import_fresh_module("hmac", blocked=["_hashlib"])
+
+        def watch_method(cls, name):
+            return mock.patch.object(
+                cls, name, autospec=True, wraps=getattr(cls, name)
+            )
+
+        with (
+            watch_method(hmac.HMAC, '_init_openssl_hmac') as f,
+            watch_method(hmac.HMAC, '_init_builtin_hmac') as g,
+        ):
+            _ = hmac.HMAC(b'key', b'msg', digestmod="sha256")
+            f.assert_not_called()
+            g.assert_called_once()
+
+    @hashlib_helper.requires_hashdigest('sha256')
+    def test_hmac_delegated_properties(self):
+        h = hmac.HMAC(b'key', b'msg', digestmod="sha256")
+        self.assertEqual(h.name, "hmac-sha256")
+        self.assertEqual(h.digest_size, 32)
+        self.assertEqual(h.block_size, 64)
+
     @hashlib_helper.requires_hashdigest('sha256')
     def test_legacy_block_size_warnings(self):
         class MockCrazyHash(object):
@@ -1381,5 +1453,36 @@ def test_with_fallback(self):
             cache.pop('foo')
 
 
+class BuiiltinMiscellaneousTests(BuiltinModuleMixin, unittest.TestCase):
+    """HMAC-BLAKE2 is not standardized as BLAKE2 is a keyed hash function.
+
+    In particular, there is no official test vectors for HMAC-BLAKE2.
+    However, we can test that the HACL* interface is correctly used by
+    checking against the pure Python implementation output.
+    """
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.blake2 = blake2 = import_module("_blake2")
+        cls.blake2b = blake2.blake2b
+        cls.blake2s = blake2.blake2s
+
+    def assert_hmac_blake_correctness(self, digest, key, msg, hashfunc):
+        self.assertIsInstance(digest, bytes)
+        expect = hmac._compute_digest_fallback(key, msg, hashfunc)
+        self.assertEqual(digest, expect)
+
+    def test_compute_blake2b_32(self):
+        key, msg = random.randbytes(8), random.randbytes(16)
+        digest = self.hmac.compute_blake2b_32(key, msg)
+        self.assert_hmac_blake_correctness(digest, key, msg, self.blake2b)
+
+    def test_compute_blake2s_32(self):
+        key, msg = random.randbytes(8), random.randbytes(16)
+        digest = self.hmac.compute_blake2s_32(key, msg)
+        self.assert_hmac_blake_correctness(digest, key, msg, self.blake2s)
+
+
 if __name__ == "__main__":
     unittest.main()

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to