Skip to content

Commit 933dfd7

Browse files
authored
bpo-40645: use C implementation of HMAC (GH-24920)
- [x] fix tests - [ ] add test scenarios for old/new code. Signed-off-by: Christian Heimes <[email protected]>
1 parent 5d6e8c1 commit 933dfd7

File tree

6 files changed

+267
-124
lines changed

6 files changed

+267
-124
lines changed

Lib/hashlib.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def __hash_new(name, data=b'', **kwargs):
173173
algorithms_available = algorithms_available.union(
174174
_hashlib.openssl_md_meth_names)
175175
except ImportError:
176+
_hashlib = None
176177
new = __py_new
177178
__get_hash = __get_builtin_constructor
178179

Lib/hmac.py

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
import _hashlib as _hashopenssl
99
except ImportError:
1010
_hashopenssl = None
11-
_openssl_md_meths = None
11+
_functype = None
1212
from _operator import _compare_digest as compare_digest
1313
else:
14-
_openssl_md_meths = frozenset(_hashopenssl.openssl_md_meth_names)
1514
compare_digest = _hashopenssl.compare_digest
15+
_functype = type(_hashopenssl.openssl_sha256) # builtin type
16+
1617
import hashlib as _hashlib
1718

1819
trans_5C = bytes((x ^ 0x5C) for x in range(256))
@@ -23,7 +24,6 @@
2324
digest_size = None
2425

2526

26-
2727
class HMAC:
2828
"""RFC 2104 HMAC class. Also complies with RFC 4231.
2929
@@ -32,7 +32,7 @@ class HMAC:
3232
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.
3333

3434
__slots__ = (
35-
"_digest_cons", "_inner", "_outer", "block_size", "digest_size"
35+
"_hmac", "_inner", "_outer", "block_size", "digest_size"
3636
)
3737

3838
def __init__(self, key, msg=None, digestmod=''):
@@ -55,15 +55,30 @@ def __init__(self, key, msg=None, digestmod=''):
5555
if not digestmod:
5656
raise TypeError("Missing required parameter 'digestmod'.")
5757

58+
if _hashopenssl and isinstance(digestmod, (str, _functype)):
59+
try:
60+
self._init_hmac(key, msg, digestmod)
61+
except _hashopenssl.UnsupportedDigestmodError:
62+
self._init_old(key, msg, digestmod)
63+
else:
64+
self._init_old(key, msg, digestmod)
65+
66+
def _init_hmac(self, key, msg, digestmod):
67+
self._hmac = _hashopenssl.hmac_new(key, msg, digestmod=digestmod)
68+
self.digest_size = self._hmac.digest_size
69+
self.block_size = self._hmac.block_size
70+
71+
def _init_old(self, key, msg, digestmod):
5872
if callable(digestmod):
59-
self._digest_cons = digestmod
73+
digest_cons = digestmod
6074
elif isinstance(digestmod, str):
61-
self._digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
75+
digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
6276
else:
63-
self._digest_cons = lambda d=b'': digestmod.new(d)
77+
digest_cons = lambda d=b'': digestmod.new(d)
6478

65-
self._outer = self._digest_cons()
66-
self._inner = self._digest_cons()
79+
self._hmac = None
80+
self._outer = digest_cons()
81+
self._inner = digest_cons()
6782
self.digest_size = self._inner.digest_size
6883

6984
if hasattr(self._inner, 'block_size'):
@@ -79,13 +94,13 @@ def __init__(self, key, msg=None, digestmod=''):
7994
RuntimeWarning, 2)
8095
blocksize = self.blocksize
8196

97+
if len(key) > blocksize:
98+
key = digest_cons(key).digest()
99+
82100
# self.blocksize is the default blocksize. self.block_size is
83101
# effective block size as well as the public API attribute.
84102
self.block_size = blocksize
85103

86-
if len(key) > blocksize:
87-
key = self._digest_cons(key).digest()
88-
89104
key = key.ljust(blocksize, b'\0')
90105
self._outer.update(key.translate(trans_5C))
91106
self._inner.update(key.translate(trans_36))
@@ -94,23 +109,15 @@ def __init__(self, key, msg=None, digestmod=''):
94109

95110
@property
96111
def name(self):
97-
return "hmac-" + self._inner.name
98-
99-
@property
100-
def digest_cons(self):
101-
return self._digest_cons
102-
103-
@property
104-
def inner(self):
105-
return self._inner
106-
107-
@property
108-
def outer(self):
109-
return self._outer
112+
if self._hmac:
113+
return self._hmac.name
114+
else:
115+
return f"hmac-{self._inner.name}"
110116

111117
def update(self, msg):
112118
"""Feed data from msg into this hashing object."""
113-
self._inner.update(msg)
119+
inst = self._hmac or self._inner
120+
inst.update(msg)
114121

115122
def copy(self):
116123
"""Return a separate copy of this hashing object.
@@ -119,20 +126,27 @@ def copy(self):
119126
"""
120127
# Call __new__ directly to avoid the expensive __init__.
121128
other = self.__class__.__new__(self.__class__)
122-
other._digest_cons = self._digest_cons
123129
other.digest_size = self.digest_size
124-
other._inner = self._inner.copy()
125-
other._outer = self._outer.copy()
130+
if self._hmac:
131+
other._hmac = self._hmac.copy()
132+
other._inner = other._outer = None
133+
else:
134+
other._hmac = None
135+
other._inner = self._inner.copy()
136+
other._outer = self._outer.copy()
126137
return other
127138

128139
def _current(self):
129140
"""Return a hash object for the current state.
130141
131142
To be used only internally with digest() and hexdigest().
132143
"""
133-
h = self._outer.copy()
134-
h.update(self._inner.digest())
135-
return h
144+
if self._hmac:
145+
return self._hmac
146+
else:
147+
h = self._outer.copy()
148+
h.update(self._inner.digest())
149+
return h
136150

137151
def digest(self):
138152
"""Return the hash value of this hashing object.
@@ -179,9 +193,11 @@ def digest(key, msg, digest):
179193
A hashlib constructor returning a new hash object. *OR*
180194
A module supporting PEP 247.
181195
"""
182-
if (_hashopenssl is not None and
183-
isinstance(digest, str) and digest in _openssl_md_meths):
184-
return _hashopenssl.hmac_digest(key, msg, digest)
196+
if _hashopenssl is not None and isinstance(digest, (str, _functype)):
197+
try:
198+
return _hashopenssl.hmac_digest(key, msg, digest)
199+
except _hashopenssl.UnsupportedDigestmodError:
200+
pass
185201

186202
if callable(digest):
187203
digest_cons = digest

Lib/test/test_hmac.py

Lines changed: 69 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,21 @@
1111
from _operator import _compare_digest as operator_compare_digest
1212

1313
try:
14+
import _hashlib as _hashopenssl
1415
from _hashlib import HMAC as C_HMAC
1516
from _hashlib import hmac_new as c_hmac_new
1617
from _hashlib import compare_digest as openssl_compare_digest
1718
except ImportError:
19+
_hashopenssl = None
1820
C_HMAC = None
1921
c_hmac_new = None
2022
openssl_compare_digest = None
2123

24+
try:
25+
import _sha256 as sha256_module
26+
except ImportError:
27+
sha256_module = None
28+
2229

2330
def ignore_warning(func):
2431
@functools.wraps(func)
@@ -32,22 +39,27 @@ def wrapper(*args, **kwargs):
3239

3340
class TestVectorsTestCase(unittest.TestCase):
3441

35-
def asssert_hmac(
36-
self, key, data, digest, hashfunc, hashname, digest_size, block_size
42+
def assert_hmac_internals(
43+
self, h, digest, hashname, digest_size, block_size
3744
):
38-
h = hmac.HMAC(key, data, digestmod=hashfunc)
3945
self.assertEqual(h.hexdigest().upper(), digest.upper())
4046
self.assertEqual(h.digest(), binascii.unhexlify(digest))
4147
self.assertEqual(h.name, f"hmac-{hashname}")
4248
self.assertEqual(h.digest_size, digest_size)
4349
self.assertEqual(h.block_size, block_size)
4450

51+
def assert_hmac(
52+
self, key, data, digest, hashfunc, hashname, digest_size, block_size
53+
):
54+
h = hmac.HMAC(key, data, digestmod=hashfunc)
55+
self.assert_hmac_internals(
56+
h, digest, hashname, digest_size, block_size
57+
)
58+
4559
h = hmac.HMAC(key, data, digestmod=hashname)
46-
self.assertEqual(h.hexdigest().upper(), digest.upper())
47-
self.assertEqual(h.digest(), binascii.unhexlify(digest))
48-
self.assertEqual(h.name, f"hmac-{hashname}")
49-
self.assertEqual(h.digest_size, digest_size)
50-
self.assertEqual(h.block_size, block_size)
60+
self.assert_hmac_internals(
61+
h, digest, hashname, digest_size, block_size
62+
)
5163

5264
h = hmac.HMAC(key, digestmod=hashname)
5365
h2 = h.copy()
@@ -56,11 +68,9 @@ def asssert_hmac(
5668
self.assertEqual(h.hexdigest().upper(), digest.upper())
5769

5870
h = hmac.new(key, data, digestmod=hashname)
59-
self.assertEqual(h.hexdigest().upper(), digest.upper())
60-
self.assertEqual(h.digest(), binascii.unhexlify(digest))
61-
self.assertEqual(h.name, f"hmac-{hashname}")
62-
self.assertEqual(h.digest_size, digest_size)
63-
self.assertEqual(h.block_size, block_size)
71+
self.assert_hmac_internals(
72+
h, digest, hashname, digest_size, block_size
73+
)
6474

6575
h = hmac.new(key, None, digestmod=hashname)
6676
h.update(data)
@@ -81,36 +91,43 @@ def asssert_hmac(
8191
hmac.digest(key, data, digest=hashfunc),
8292
binascii.unhexlify(digest)
8393
)
84-
with unittest.mock.patch('hmac._openssl_md_meths', {}):
85-
self.assertEqual(
86-
hmac.digest(key, data, digest=hashname),
87-
binascii.unhexlify(digest)
88-
)
89-
self.assertEqual(
90-
hmac.digest(key, data, digest=hashfunc),
91-
binascii.unhexlify(digest)
92-
)
94+
95+
h = hmac.HMAC.__new__(hmac.HMAC)
96+
h._init_old(key, data, digestmod=hashname)
97+
self.assert_hmac_internals(
98+
h, digest, hashname, digest_size, block_size
99+
)
93100

94101
if c_hmac_new is not None:
95102
h = c_hmac_new(key, data, digestmod=hashname)
96-
self.assertEqual(h.hexdigest().upper(), digest.upper())
97-
self.assertEqual(h.digest(), binascii.unhexlify(digest))
98-
self.assertEqual(h.name, f"hmac-{hashname}")
99-
self.assertEqual(h.digest_size, digest_size)
100-
self.assertEqual(h.block_size, block_size)
103+
self.assert_hmac_internals(
104+
h, digest, hashname, digest_size, block_size
105+
)
101106

102107
h = c_hmac_new(key, digestmod=hashname)
103108
h2 = h.copy()
104109
h2.update(b"test update")
105110
h.update(data)
106111
self.assertEqual(h.hexdigest().upper(), digest.upper())
107112

113+
func = getattr(_hashopenssl, f"openssl_{hashname}")
114+
h = c_hmac_new(key, data, digestmod=func)
115+
self.assert_hmac_internals(
116+
h, digest, hashname, digest_size, block_size
117+
)
118+
119+
h = hmac.HMAC.__new__(hmac.HMAC)
120+
h._init_hmac(key, data, digestmod=hashname)
121+
self.assert_hmac_internals(
122+
h, digest, hashname, digest_size, block_size
123+
)
124+
108125
@hashlib_helper.requires_hashdigest('md5', openssl=True)
109126
def test_md5_vectors(self):
110127
# Test the HMAC module against test vectors from the RFC.
111128

112129
def md5test(key, data, digest):
113-
self.asssert_hmac(
130+
self.assert_hmac(
114131
key, data, digest,
115132
hashfunc=hashlib.md5,
116133
hashname="md5",
@@ -150,7 +167,7 @@ def md5test(key, data, digest):
150167
@hashlib_helper.requires_hashdigest('sha1', openssl=True)
151168
def test_sha_vectors(self):
152169
def shatest(key, data, digest):
153-
self.asssert_hmac(
170+
self.assert_hmac(
154171
key, data, digest,
155172
hashfunc=hashlib.sha1,
156173
hashname="sha1",
@@ -191,7 +208,7 @@ def _rfc4231_test_cases(self, hashfunc, hash_name, digest_size, block_size):
191208
def hmactest(key, data, hexdigests):
192209
digest = hexdigests[hashfunc]
193210

194-
self.asssert_hmac(
211+
self.assert_hmac(
195212
key, data, digest,
196213
hashfunc=hashfunc,
197214
hashname=hash_name,
@@ -427,6 +444,15 @@ def test_internal_types(self):
427444
):
428445
C_HMAC()
429446

447+
@unittest.skipUnless(sha256_module is not None, 'need _sha256')
448+
def test_with_sha256_module(self):
449+
h = hmac.HMAC(b"key", b"hash this!", digestmod=sha256_module.sha256)
450+
self.assertEqual(h.hexdigest(), self.expected)
451+
self.assertEqual(h.name, "hmac-sha256")
452+
453+
digest = hmac.digest(b"key", b"hash this!", sha256_module.sha256)
454+
self.assertEqual(digest, binascii.unhexlify(self.expected))
455+
430456

431457
class SanityTestCase(unittest.TestCase):
432458

@@ -447,39 +473,37 @@ def test_exercise_all_methods(self):
447473
class CopyTestCase(unittest.TestCase):
448474

449475
@hashlib_helper.requires_hashdigest('sha256')
450-
def test_attributes(self):
476+
def test_attributes_old(self):
451477
# Testing if attributes are of same type.
452-
h1 = hmac.HMAC(b"key", digestmod="sha256")
478+
h1 = hmac.HMAC.__new__(hmac.HMAC)
479+
h1._init_old(b"key", b"msg", digestmod="sha256")
453480
h2 = h1.copy()
454-
self.assertTrue(h1._digest_cons == h2._digest_cons,
455-
"digest constructors don't match.")
456481
self.assertEqual(type(h1._inner), type(h2._inner),
457482
"Types of inner don't match.")
458483
self.assertEqual(type(h1._outer), type(h2._outer),
459484
"Types of outer don't match.")
460485

461486
@hashlib_helper.requires_hashdigest('sha256')
462-
def test_realcopy(self):
487+
def test_realcopy_old(self):
463488
# Testing if the copy method created a real copy.
464-
h1 = hmac.HMAC(b"key", digestmod="sha256")
489+
h1 = hmac.HMAC.__new__(hmac.HMAC)
490+
h1._init_old(b"key", b"msg", digestmod="sha256")
465491
h2 = h1.copy()
466492
# Using id() in case somebody has overridden __eq__/__ne__.
467493
self.assertTrue(id(h1) != id(h2), "No real copy of the HMAC instance.")
468494
self.assertTrue(id(h1._inner) != id(h2._inner),
469495
"No real copy of the attribute 'inner'.")
470496
self.assertTrue(id(h1._outer) != id(h2._outer),
471497
"No real copy of the attribute 'outer'.")
472-
self.assertEqual(h1._inner, h1.inner)
473-
self.assertEqual(h1._outer, h1.outer)
474-
self.assertEqual(h1._digest_cons, h1.digest_cons)
498+
self.assertIs(h1._hmac, None)
475499

500+
@unittest.skipIf(_hashopenssl is None, "test requires _hashopenssl")
476501
@hashlib_helper.requires_hashdigest('sha256')
477-
def test_properties(self):
478-
# deprecated properties
479-
h1 = hmac.HMAC(b"key", digestmod="sha256")
480-
self.assertEqual(h1._inner, h1.inner)
481-
self.assertEqual(h1._outer, h1.outer)
482-
self.assertEqual(h1._digest_cons, h1.digest_cons)
502+
def test_realcopy_hmac(self):
503+
h1 = hmac.HMAC.__new__(hmac.HMAC)
504+
h1._init_hmac(b"key", b"msg", digestmod="sha256")
505+
h2 = h1.copy()
506+
self.assertTrue(id(h1._hmac) != id(h2._hmac))
483507

484508
@hashlib_helper.requires_hashdigest('sha256')
485509
def test_equality(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The :mod:`hmac` module now uses OpenSSL's HMAC implementation when digestmod
2+
argument is a hash name or builtin hash function.

0 commit comments

Comments
 (0)