Skip to content

Commit 54f2898

Browse files
tiranencukou
andauthored
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>
1 parent 4654500 commit 54f2898

File tree

4 files changed

+694
-89
lines changed

4 files changed

+694
-89
lines changed

Lib/test/test_hmac.py

Lines changed: 125 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88

99
from test.support import hashlib_helper
1010

11+
try:
12+
from _hashlib import HMAC as C_HMAC
13+
from _hashlib import hmac_new as c_hmac_new
14+
except ImportError:
15+
C_HMAC = None
16+
c_hmac_new = None
17+
1118

1219
def ignore_warning(func):
1320
@functools.wraps(func)
@@ -21,34 +28,91 @@ def wrapper(*args, **kwargs):
2128

2229
class TestVectorsTestCase(unittest.TestCase):
2330

24-
@hashlib_helper.requires_hashdigest('md5', openssl=True)
25-
def test_md5_vectors(self):
26-
# Test the HMAC module against test vectors from the RFC.
31+
def asssert_hmac(
32+
self, key, data, digest, hashfunc, hashname, digest_size, block_size
33+
):
34+
h = hmac.HMAC(key, data, digestmod=hashfunc)
35+
self.assertEqual(h.hexdigest().upper(), digest.upper())
36+
self.assertEqual(h.digest(), binascii.unhexlify(digest))
37+
self.assertEqual(h.name, f"hmac-{hashname}")
38+
self.assertEqual(h.digest_size, digest_size)
39+
self.assertEqual(h.block_size, block_size)
40+
41+
h = hmac.HMAC(key, data, digestmod=hashname)
42+
self.assertEqual(h.hexdigest().upper(), digest.upper())
43+
self.assertEqual(h.digest(), binascii.unhexlify(digest))
44+
self.assertEqual(h.name, f"hmac-{hashname}")
45+
self.assertEqual(h.digest_size, digest_size)
46+
self.assertEqual(h.block_size, block_size)
47+
48+
h = hmac.HMAC(key, digestmod=hashname)
49+
h2 = h.copy()
50+
h2.update(b"test update")
51+
h.update(data)
52+
self.assertEqual(h.hexdigest().upper(), digest.upper())
53+
54+
h = hmac.new(key, data, digestmod=hashname)
55+
self.assertEqual(h.hexdigest().upper(), digest.upper())
56+
self.assertEqual(h.digest(), binascii.unhexlify(digest))
57+
self.assertEqual(h.name, f"hmac-{hashname}")
58+
self.assertEqual(h.digest_size, digest_size)
59+
self.assertEqual(h.block_size, block_size)
60+
61+
h = hmac.new(key, None, digestmod=hashname)
62+
h.update(data)
63+
self.assertEqual(h.hexdigest().upper(), digest.upper())
64+
65+
h = hmac.new(key, digestmod=hashname)
66+
h.update(data)
67+
self.assertEqual(h.hexdigest().upper(), digest.upper())
68+
69+
h = hmac.new(key, data, digestmod=hashfunc)
70+
self.assertEqual(h.hexdigest().upper(), digest.upper())
71+
72+
self.assertEqual(
73+
hmac.digest(key, data, digest=hashname),
74+
binascii.unhexlify(digest)
75+
)
76+
self.assertEqual(
77+
hmac.digest(key, data, digest=hashfunc),
78+
binascii.unhexlify(digest)
79+
)
80+
with unittest.mock.patch('hmac._openssl_md_meths', {}):
81+
self.assertEqual(
82+
hmac.digest(key, data, digest=hashname),
83+
binascii.unhexlify(digest)
84+
)
85+
self.assertEqual(
86+
hmac.digest(key, data, digest=hashfunc),
87+
binascii.unhexlify(digest)
88+
)
2789

28-
def md5test(key, data, digest):
29-
h = hmac.HMAC(key, data, digestmod=hashlib.md5)
90+
if c_hmac_new is not None:
91+
h = c_hmac_new(key, data, digestmod=hashname)
3092
self.assertEqual(h.hexdigest().upper(), digest.upper())
3193
self.assertEqual(h.digest(), binascii.unhexlify(digest))
32-
self.assertEqual(h.name, "hmac-md5")
33-
self.assertEqual(h.digest_size, 16)
34-
self.assertEqual(h.block_size, 64)
94+
self.assertEqual(h.name, f"hmac-{hashname}")
95+
self.assertEqual(h.digest_size, digest_size)
96+
self.assertEqual(h.block_size, block_size)
3597

36-
h = hmac.HMAC(key, data, digestmod='md5')
98+
h = c_hmac_new(key, digestmod=hashname)
99+
h2 = h.copy()
100+
h2.update(b"test update")
101+
h.update(data)
37102
self.assertEqual(h.hexdigest().upper(), digest.upper())
38-
self.assertEqual(h.digest(), binascii.unhexlify(digest))
39-
self.assertEqual(h.name, "hmac-md5")
40-
self.assertEqual(h.digest_size, 16)
41-
self.assertEqual(h.block_size, 64)
42103

43-
self.assertEqual(
44-
hmac.digest(key, data, digest='md5'),
45-
binascii.unhexlify(digest)
104+
@hashlib_helper.requires_hashdigest('md5', openssl=True)
105+
def test_md5_vectors(self):
106+
# Test the HMAC module against test vectors from the RFC.
107+
108+
def md5test(key, data, digest):
109+
self.asssert_hmac(
110+
key, data, digest,
111+
hashfunc=hashlib.md5,
112+
hashname="md5",
113+
digest_size=16,
114+
block_size=64
46115
)
47-
with unittest.mock.patch('hmac._openssl_md_meths', {}):
48-
self.assertEqual(
49-
hmac.digest(key, data, digest='md5'),
50-
binascii.unhexlify(digest)
51-
)
52116

53117
md5test(b"\x0b" * 16,
54118
b"Hi There",
@@ -82,26 +146,14 @@ def md5test(key, data, digest):
82146
@hashlib_helper.requires_hashdigest('sha1', openssl=True)
83147
def test_sha_vectors(self):
84148
def shatest(key, data, digest):
85-
h = hmac.HMAC(key, data, digestmod=hashlib.sha1)
86-
self.assertEqual(h.hexdigest().upper(), digest.upper())
87-
self.assertEqual(h.digest(), binascii.unhexlify(digest))
88-
self.assertEqual(h.name, "hmac-sha1")
89-
self.assertEqual(h.digest_size, 20)
90-
self.assertEqual(h.block_size, 64)
91-
92-
h = hmac.HMAC(key, data, digestmod='sha1')
93-
self.assertEqual(h.hexdigest().upper(), digest.upper())
94-
self.assertEqual(h.digest(), binascii.unhexlify(digest))
95-
self.assertEqual(h.name, "hmac-sha1")
96-
self.assertEqual(h.digest_size, 20)
97-
self.assertEqual(h.block_size, 64)
98-
99-
self.assertEqual(
100-
hmac.digest(key, data, digest='sha1'),
101-
binascii.unhexlify(digest)
149+
self.asssert_hmac(
150+
key, data, digest,
151+
hashfunc=hashlib.sha1,
152+
hashname="sha1",
153+
digest_size=20,
154+
block_size=64
102155
)
103156

104-
105157
shatest(b"\x0b" * 20,
106158
b"Hi There",
107159
"b617318655057264e28bc0b6fb378c8ef146be00")
@@ -133,37 +185,15 @@ def shatest(key, data, digest):
133185

134186
def _rfc4231_test_cases(self, hashfunc, hash_name, digest_size, block_size):
135187
def hmactest(key, data, hexdigests):
136-
hmac_name = "hmac-" + hash_name
137-
h = hmac.HMAC(key, data, digestmod=hashfunc)
138-
self.assertEqual(h.hexdigest().lower(), hexdigests[hashfunc])
139-
self.assertEqual(h.name, hmac_name)
140-
self.assertEqual(h.digest_size, digest_size)
141-
self.assertEqual(h.block_size, block_size)
142-
143-
h = hmac.HMAC(key, data, digestmod=hash_name)
144-
self.assertEqual(h.hexdigest().lower(), hexdigests[hashfunc])
145-
self.assertEqual(h.name, hmac_name)
146-
self.assertEqual(h.digest_size, digest_size)
147-
self.assertEqual(h.block_size, block_size)
148-
149-
self.assertEqual(
150-
hmac.digest(key, data, digest=hashfunc),
151-
binascii.unhexlify(hexdigests[hashfunc])
188+
digest = hexdigests[hashfunc]
189+
190+
self.asssert_hmac(
191+
key, data, digest,
192+
hashfunc=hashfunc,
193+
hashname=hash_name,
194+
digest_size=digest_size,
195+
block_size=block_size
152196
)
153-
self.assertEqual(
154-
hmac.digest(key, data, digest=hash_name),
155-
binascii.unhexlify(hexdigests[hashfunc])
156-
)
157-
158-
with unittest.mock.patch('hmac._openssl_md_meths', {}):
159-
self.assertEqual(
160-
hmac.digest(key, data, digest=hashfunc),
161-
binascii.unhexlify(hexdigests[hashfunc])
162-
)
163-
self.assertEqual(
164-
hmac.digest(key, data, digest=hash_name),
165-
binascii.unhexlify(hexdigests[hashfunc])
166-
)
167197

168198
# 4.2. Test Case 1
169199
hmactest(key = b'\x0b'*20,
@@ -385,6 +415,14 @@ def test_withmodule(self):
385415
except Exception:
386416
self.fail("Constructor call with hashlib.sha256 raised exception.")
387417

418+
@unittest.skipUnless(C_HMAC is not None, 'need _hashlib')
419+
def test_internal_types(self):
420+
# internal types like _hashlib.C_HMAC are not constructable
421+
with self.assertRaisesRegex(
422+
TypeError, "cannot create 'HMAC' instance"
423+
):
424+
C_HMAC()
425+
388426

389427
class SanityTestCase(unittest.TestCase):
390428

@@ -395,9 +433,9 @@ def test_exercise_all_methods(self):
395433
try:
396434
h = hmac.HMAC(b"my secret key", digestmod="sha256")
397435
h.update(b"compute the hash of this text!")
398-
dig = h.digest()
399-
dig = h.hexdigest()
400-
h2 = h.copy()
436+
h.digest()
437+
h.hexdigest()
438+
h.copy()
401439
except Exception:
402440
self.fail("Exception raised during normal usage of HMAC class.")
403441

@@ -450,6 +488,21 @@ def test_equality(self):
450488
self.assertEqual(h1.hexdigest(), h2.hexdigest(),
451489
"Hexdigest of copy doesn't match original hexdigest.")
452490

491+
@hashlib_helper.requires_hashdigest('sha256')
492+
def test_equality_new(self):
493+
# Testing if the copy has the same digests with hmac.new().
494+
h1 = hmac.new(b"key", digestmod="sha256")
495+
h1.update(b"some random text")
496+
h2 = h1.copy()
497+
self.assertTrue(
498+
id(h1) != id(h2), "No real copy of the HMAC instance."
499+
)
500+
self.assertEqual(h1.digest(), h2.digest(),
501+
"Digest of copy doesn't match original digest.")
502+
self.assertEqual(h1.hexdigest(), h2.hexdigest(),
503+
"Hexdigest of copy doesn't match original hexdigest.")
504+
505+
453506
class CompareDigestTestCase(unittest.TestCase):
454507

455508
def test_compare_digest(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
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.

0 commit comments

Comments
 (0)