bpo-40645: Implement HMAC in C (GH-20129)

The internal module ``_hashlib`` wraps and exposes OpenSSL's HMAC API. The
new code will be used in Python 3.10 after the internal implementation
details of the pure Python HMAC module are no longer part of the public API.

The code is based on a patch by Petr Viktorin for RHEL and Python 3.6.

Co-Authored-By: Petr Viktorin <encukou@gmail.com>
diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py
index 1f3ec4c..7a52e39 100644
--- a/Lib/test/test_hmac.py
+++ b/Lib/test/test_hmac.py
@@ -8,6 +8,13 @@
 
 from test.support import hashlib_helper
 
+try:
+    from _hashlib import HMAC as C_HMAC
+    from _hashlib import hmac_new as c_hmac_new
+except ImportError:
+    C_HMAC = None
+    c_hmac_new = None
+
 
 def ignore_warning(func):
     @functools.wraps(func)
@@ -21,34 +28,91 @@
 
 class TestVectorsTestCase(unittest.TestCase):
 
+    def asssert_hmac(
+        self, key, data, digest, hashfunc, hashname, digest_size, block_size
+    ):
+        h = hmac.HMAC(key, data, digestmod=hashfunc)
+        self.assertEqual(h.hexdigest().upper(), digest.upper())
+        self.assertEqual(h.digest(), binascii.unhexlify(digest))
+        self.assertEqual(h.name, f"hmac-{hashname}")
+        self.assertEqual(h.digest_size, digest_size)
+        self.assertEqual(h.block_size, block_size)
+
+        h = hmac.HMAC(key, data, digestmod=hashname)
+        self.assertEqual(h.hexdigest().upper(), digest.upper())
+        self.assertEqual(h.digest(), binascii.unhexlify(digest))
+        self.assertEqual(h.name, f"hmac-{hashname}")
+        self.assertEqual(h.digest_size, digest_size)
+        self.assertEqual(h.block_size, block_size)
+
+        h = hmac.HMAC(key, digestmod=hashname)
+        h2 = h.copy()
+        h2.update(b"test update")
+        h.update(data)
+        self.assertEqual(h.hexdigest().upper(), digest.upper())
+
+        h = hmac.new(key, data, digestmod=hashname)
+        self.assertEqual(h.hexdigest().upper(), digest.upper())
+        self.assertEqual(h.digest(), binascii.unhexlify(digest))
+        self.assertEqual(h.name, f"hmac-{hashname}")
+        self.assertEqual(h.digest_size, digest_size)
+        self.assertEqual(h.block_size, block_size)
+
+        h = hmac.new(key, None, digestmod=hashname)
+        h.update(data)
+        self.assertEqual(h.hexdigest().upper(), digest.upper())
+
+        h = hmac.new(key, digestmod=hashname)
+        h.update(data)
+        self.assertEqual(h.hexdigest().upper(), digest.upper())
+
+        h = hmac.new(key, data, digestmod=hashfunc)
+        self.assertEqual(h.hexdigest().upper(), digest.upper())
+
+        self.assertEqual(
+            hmac.digest(key, data, digest=hashname),
+            binascii.unhexlify(digest)
+        )
+        self.assertEqual(
+            hmac.digest(key, data, digest=hashfunc),
+            binascii.unhexlify(digest)
+        )
+        with unittest.mock.patch('hmac._openssl_md_meths', {}):
+            self.assertEqual(
+                hmac.digest(key, data, digest=hashname),
+                binascii.unhexlify(digest)
+            )
+            self.assertEqual(
+                hmac.digest(key, data, digest=hashfunc),
+                binascii.unhexlify(digest)
+            )
+
+        if c_hmac_new is not None:
+            h = c_hmac_new(key, data, digestmod=hashname)
+            self.assertEqual(h.hexdigest().upper(), digest.upper())
+            self.assertEqual(h.digest(), binascii.unhexlify(digest))
+            self.assertEqual(h.name, f"hmac-{hashname}")
+            self.assertEqual(h.digest_size, digest_size)
+            self.assertEqual(h.block_size, block_size)
+
+            h = c_hmac_new(key, digestmod=hashname)
+            h2 = h.copy()
+            h2.update(b"test update")
+            h.update(data)
+            self.assertEqual(h.hexdigest().upper(), digest.upper())
+
     @hashlib_helper.requires_hashdigest('md5', openssl=True)
     def test_md5_vectors(self):
         # Test the HMAC module against test vectors from the RFC.
 
         def md5test(key, data, digest):
-            h = hmac.HMAC(key, data, digestmod=hashlib.md5)
-            self.assertEqual(h.hexdigest().upper(), digest.upper())
-            self.assertEqual(h.digest(), binascii.unhexlify(digest))
-            self.assertEqual(h.name, "hmac-md5")
-            self.assertEqual(h.digest_size, 16)
-            self.assertEqual(h.block_size, 64)
-
-            h = hmac.HMAC(key, data, digestmod='md5')
-            self.assertEqual(h.hexdigest().upper(), digest.upper())
-            self.assertEqual(h.digest(), binascii.unhexlify(digest))
-            self.assertEqual(h.name, "hmac-md5")
-            self.assertEqual(h.digest_size, 16)
-            self.assertEqual(h.block_size, 64)
-
-            self.assertEqual(
-                hmac.digest(key, data, digest='md5'),
-                binascii.unhexlify(digest)
+            self.asssert_hmac(
+                key, data, digest,
+                hashfunc=hashlib.md5,
+                hashname="md5",
+                digest_size=16,
+                block_size=64
             )
-            with unittest.mock.patch('hmac._openssl_md_meths', {}):
-                self.assertEqual(
-                    hmac.digest(key, data, digest='md5'),
-                    binascii.unhexlify(digest)
-                )
 
         md5test(b"\x0b" * 16,
                 b"Hi There",
@@ -82,26 +146,14 @@
     @hashlib_helper.requires_hashdigest('sha1', openssl=True)
     def test_sha_vectors(self):
         def shatest(key, data, digest):
-            h = hmac.HMAC(key, data, digestmod=hashlib.sha1)
-            self.assertEqual(h.hexdigest().upper(), digest.upper())
-            self.assertEqual(h.digest(), binascii.unhexlify(digest))
-            self.assertEqual(h.name, "hmac-sha1")
-            self.assertEqual(h.digest_size, 20)
-            self.assertEqual(h.block_size, 64)
-
-            h = hmac.HMAC(key, data, digestmod='sha1')
-            self.assertEqual(h.hexdigest().upper(), digest.upper())
-            self.assertEqual(h.digest(), binascii.unhexlify(digest))
-            self.assertEqual(h.name, "hmac-sha1")
-            self.assertEqual(h.digest_size, 20)
-            self.assertEqual(h.block_size, 64)
-
-            self.assertEqual(
-                hmac.digest(key, data, digest='sha1'),
-                binascii.unhexlify(digest)
+            self.asssert_hmac(
+                key, data, digest,
+                hashfunc=hashlib.sha1,
+                hashname="sha1",
+                digest_size=20,
+                block_size=64
             )
 
-
         shatest(b"\x0b" * 20,
                 b"Hi There",
                 "b617318655057264e28bc0b6fb378c8ef146be00")
@@ -133,37 +185,15 @@
 
     def _rfc4231_test_cases(self, hashfunc, hash_name, digest_size, block_size):
         def hmactest(key, data, hexdigests):
-            hmac_name = "hmac-" + hash_name
-            h = hmac.HMAC(key, data, digestmod=hashfunc)
-            self.assertEqual(h.hexdigest().lower(), hexdigests[hashfunc])
-            self.assertEqual(h.name, hmac_name)
-            self.assertEqual(h.digest_size, digest_size)
-            self.assertEqual(h.block_size, block_size)
+            digest = hexdigests[hashfunc]
 
-            h = hmac.HMAC(key, data, digestmod=hash_name)
-            self.assertEqual(h.hexdigest().lower(), hexdigests[hashfunc])
-            self.assertEqual(h.name, hmac_name)
-            self.assertEqual(h.digest_size, digest_size)
-            self.assertEqual(h.block_size, block_size)
-
-            self.assertEqual(
-                hmac.digest(key, data, digest=hashfunc),
-                binascii.unhexlify(hexdigests[hashfunc])
+            self.asssert_hmac(
+                key, data, digest,
+                hashfunc=hashfunc,
+                hashname=hash_name,
+                digest_size=digest_size,
+                block_size=block_size
             )
-            self.assertEqual(
-                hmac.digest(key, data, digest=hash_name),
-                binascii.unhexlify(hexdigests[hashfunc])
-            )
-
-            with unittest.mock.patch('hmac._openssl_md_meths', {}):
-                self.assertEqual(
-                    hmac.digest(key, data, digest=hashfunc),
-                    binascii.unhexlify(hexdigests[hashfunc])
-                )
-                self.assertEqual(
-                    hmac.digest(key, data, digest=hash_name),
-                    binascii.unhexlify(hexdigests[hashfunc])
-                )
 
         # 4.2.  Test Case 1
         hmactest(key = b'\x0b'*20,
@@ -385,6 +415,14 @@
         except Exception:
             self.fail("Constructor call with hashlib.sha256 raised exception.")
 
+    @unittest.skipUnless(C_HMAC is not None, 'need _hashlib')
+    def test_internal_types(self):
+        # internal types like _hashlib.C_HMAC are not constructable
+        with self.assertRaisesRegex(
+            TypeError, "cannot create 'HMAC' instance"
+        ):
+            C_HMAC()
+
 
 class SanityTestCase(unittest.TestCase):
 
@@ -395,9 +433,9 @@
         try:
             h = hmac.HMAC(b"my secret key", digestmod="sha256")
             h.update(b"compute the hash of this text!")
-            dig = h.digest()
-            dig = h.hexdigest()
-            h2 = h.copy()
+            h.digest()
+            h.hexdigest()
+            h.copy()
         except Exception:
             self.fail("Exception raised during normal usage of HMAC class.")
 
@@ -450,6 +488,21 @@
         self.assertEqual(h1.hexdigest(), h2.hexdigest(),
             "Hexdigest of copy doesn't match original hexdigest.")
 
+    @hashlib_helper.requires_hashdigest('sha256')
+    def test_equality_new(self):
+        # Testing if the copy has the same digests with hmac.new().
+        h1 = hmac.new(b"key", digestmod="sha256")
+        h1.update(b"some random text")
+        h2 = h1.copy()
+        self.assertTrue(
+            id(h1) != id(h2), "No real copy of the HMAC instance."
+        )
+        self.assertEqual(h1.digest(), h2.digest(),
+            "Digest of copy doesn't match original digest.")
+        self.assertEqual(h1.hexdigest(), h2.hexdigest(),
+            "Hexdigest of copy doesn't match original hexdigest.")
+
+
 class CompareDigestTestCase(unittest.TestCase):
 
     def test_compare_digest(self):