Skip to content

Commit 0a97427

Browse files
authored
gh-99108: Implement HACL* HMAC (#130157)
A new extension module, `_hmac`, now exposes the HACL* HMAC (formally verified) implementation. The HACL* implementation is used as a fallback implementation when the OpenSSL implementation of HMAC is not available or disabled. For now, only named hash algorithms are recognized and SIMD support provided by HACL* for the BLAKE2 hash functions is not yet used.
1 parent 7099c75 commit 0a97427

29 files changed

+7702
-103
lines changed

Doc/whatsnew/3.14.rst

+16
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,13 @@ Other language changes
455455
The testbed can also be used to run the test suite of projects other than
456456
CPython itself. (Contributed by Russell Keith-Magee in :gh:`127592`.)
457457

458+
* Add a built-in implementation for HMAC (:rfc:`2104`) using formally verified
459+
code from the `HACL* <https://github.com/hacl-star/hacl-star/>`__ project.
460+
This implementation is used as a fallback when the OpenSSL implementation
461+
of HMAC is not available.
462+
(Contributed by Bénédikt Tran in :gh:`99108`.)
463+
464+
458465
.. _whatsnew314-pep765:
459466

460467
PEP 765: Disallow return/break/continue that exit a finally block
@@ -464,6 +471,7 @@ The compiler emits a :exc:`SyntaxWarning` when a :keyword:`return`, :keyword:`br
464471
:keyword:`continue` statements appears where it exits a :keyword:`finally` block.
465472
This change is specified in :pep:`765`.
466473

474+
467475
New modules
468476
===========
469477

@@ -705,6 +713,14 @@ graphlib
705713
(Contributed by Daniel Pope in :gh:`130914`)
706714

707715

716+
hmac
717+
----
718+
719+
* Add a built-in implementation for HMAC (:rfc:`2104`) using formally verified
720+
code from the `HACL* <https://github.com/hacl-star/hacl-star/>`__ project.
721+
(Contributed by Bénédikt Tran in :gh:`99108`.)
722+
723+
708724
http
709725
----
710726

Lib/hmac.py

+66-30
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
Implements the HMAC algorithm as described by RFC 2104.
44
"""
55

6-
import warnings as _warnings
76
try:
87
import _hashlib as _hashopenssl
98
except ImportError:
@@ -14,7 +13,10 @@
1413
compare_digest = _hashopenssl.compare_digest
1514
_functype = type(_hashopenssl.openssl_sha256) # builtin type
1615

17-
import hashlib as _hashlib
16+
try:
17+
import _hmac
18+
except ImportError:
19+
_hmac = None
1820

1921
trans_5C = bytes((x ^ 0x5C) for x in range(256))
2022
trans_36 = bytes((x ^ 0x36) for x in range(256))
@@ -24,11 +26,27 @@
2426
digest_size = None
2527

2628

29+
def _get_digest_constructor(digest_like):
30+
if callable(digest_like):
31+
return digest_like
32+
if isinstance(digest_like, str):
33+
def digest_wrapper(d=b''):
34+
import hashlib
35+
return hashlib.new(digest_like, d)
36+
else:
37+
def digest_wrapper(d=b''):
38+
return digest_like.new(d)
39+
return digest_wrapper
40+
41+
2742
class HMAC:
2843
"""RFC 2104 HMAC class. Also complies with RFC 4231.
2944
3045
This supports the API for Cryptographic Hash Functions (PEP 247).
3146
"""
47+
48+
# Note: self.blocksize is the default blocksize; self.block_size
49+
# is effective block size as well as the public API attribute.
3250
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.
3351

3452
__slots__ = (
@@ -50,32 +68,47 @@ def __init__(self, key, msg=None, digestmod=''):
5068
"""
5169

5270
if not isinstance(key, (bytes, bytearray)):
53-
raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__)
71+
raise TypeError(f"key: expected bytes or bytearray, "
72+
f"but got {type(key).__name__!r}")
5473

5574
if not digestmod:
5675
raise TypeError("Missing required argument 'digestmod'.")
5776

77+
self.__init(key, msg, digestmod)
78+
79+
def __init(self, key, msg, digestmod):
5880
if _hashopenssl and isinstance(digestmod, (str, _functype)):
5981
try:
60-
self._init_hmac(key, msg, digestmod)
82+
self._init_openssl_hmac(key, msg, digestmod)
83+
return
6184
except _hashopenssl.UnsupportedDigestmodError:
62-
self._init_old(key, msg, digestmod)
63-
else:
64-
self._init_old(key, msg, digestmod)
85+
pass
86+
if _hmac and isinstance(digestmod, str):
87+
try:
88+
self._init_builtin_hmac(key, msg, digestmod)
89+
return
90+
except _hmac.UnknownHashError:
91+
pass
92+
self._init_old(key, msg, digestmod)
6593

66-
def _init_hmac(self, key, msg, digestmod):
94+
def _init_openssl_hmac(self, key, msg, digestmod):
6795
self._hmac = _hashopenssl.hmac_new(key, msg, digestmod=digestmod)
6896
self._inner = self._outer = None # because the slots are defined
6997
self.digest_size = self._hmac.digest_size
7098
self.block_size = self._hmac.block_size
7199

100+
_init_hmac = _init_openssl_hmac # for backward compatibility (if any)
101+
102+
def _init_builtin_hmac(self, key, msg, digestmod):
103+
self._hmac = _hmac.new(key, msg, digestmod=digestmod)
104+
self._inner = self._outer = None # because the slots are defined
105+
self.digest_size = self._hmac.digest_size
106+
self.block_size = self._hmac.block_size
107+
72108
def _init_old(self, key, msg, digestmod):
73-
if callable(digestmod):
74-
digest_cons = digestmod
75-
elif isinstance(digestmod, str):
76-
digest_cons = lambda d=b'': _hashlib.new(digestmod, d)
77-
else:
78-
digest_cons = lambda d=b'': digestmod.new(d)
109+
import warnings
110+
111+
digest_cons = _get_digest_constructor(digestmod)
79112

80113
self._hmac = None
81114
self._outer = digest_cons()
@@ -85,21 +118,19 @@ def _init_old(self, key, msg, digestmod):
85118
if hasattr(self._inner, 'block_size'):
86119
blocksize = self._inner.block_size
87120
if blocksize < 16:
88-
_warnings.warn('block_size of %d seems too small; using our '
89-
'default of %d.' % (blocksize, self.blocksize),
90-
RuntimeWarning, 2)
121+
warnings.warn(f"block_size of {blocksize} seems too small; "
122+
f"using our default of {self.blocksize}.",
123+
RuntimeWarning, 2)
91124
blocksize = self.blocksize
92125
else:
93-
_warnings.warn('No block_size attribute on given digest object; '
94-
'Assuming %d.' % (self.blocksize),
95-
RuntimeWarning, 2)
126+
warnings.warn("No block_size attribute on given digest object; "
127+
f"Assuming {self.blocksize}.",
128+
RuntimeWarning, 2)
96129
blocksize = self.blocksize
97130

98131
if len(key) > blocksize:
99132
key = digest_cons(key).digest()
100133

101-
# self.blocksize is the default blocksize. self.block_size is
102-
# effective block size as well as the public API attribute.
103134
self.block_size = blocksize
104135

105136
key = key.ljust(blocksize, b'\0')
@@ -165,6 +196,7 @@ def hexdigest(self):
165196
h = self._current()
166197
return h.hexdigest()
167198

199+
168200
def new(key, msg=None, digestmod=''):
169201
"""Create a new hashing object and return it.
170202
@@ -194,25 +226,29 @@ def digest(key, msg, digest):
194226
A hashlib constructor returning a new hash object. *OR*
195227
A module supporting PEP 247.
196228
"""
197-
if _hashopenssl is not None and isinstance(digest, (str, _functype)):
229+
if _hashopenssl and isinstance(digest, (str, _functype)):
198230
try:
199231
return _hashopenssl.hmac_digest(key, msg, digest)
200232
except _hashopenssl.UnsupportedDigestmodError:
201233
pass
202234

203-
if callable(digest):
204-
digest_cons = digest
205-
elif isinstance(digest, str):
206-
digest_cons = lambda d=b'': _hashlib.new(digest, d)
207-
else:
208-
digest_cons = lambda d=b'': digest.new(d)
235+
if _hmac and isinstance(digest, str):
236+
try:
237+
return _hmac.compute_digest(key, msg, digest)
238+
except (OverflowError, _hmac.UnknownHashError):
239+
pass
240+
241+
return _compute_digest_fallback(key, msg, digest)
242+
209243

244+
def _compute_digest_fallback(key, msg, digest):
245+
digest_cons = _get_digest_constructor(digest)
210246
inner = digest_cons()
211247
outer = digest_cons()
212248
blocksize = getattr(inner, 'block_size', 64)
213249
if len(key) > blocksize:
214250
key = digest_cons(key).digest()
215-
key = key + b'\x00' * (blocksize - len(key))
251+
key = key.ljust(blocksize, b'\0')
216252
inner.update(key.translate(trans_36))
217253
outer.update(key.translate(trans_5C))
218254
inner.update(msg)

Lib/test/support/hashlib_helper.py

+9
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,20 @@
88
except ImportError:
99
_hashlib = None
1010

11+
try:
12+
import _hmac
13+
except ImportError:
14+
_hmac = None
15+
1116

1217
def requires_hashlib():
1318
return unittest.skipIf(_hashlib is None, "requires _hashlib")
1419

1520

21+
def requires_builtin_hmac():
22+
return unittest.skipIf(_hmac is None, "requires _hmac")
23+
24+
1625
def _decorate_func_or_class(func_or_class, decorator_func):
1726
if not isinstance(func_or_class, type):
1827
return decorator_func(func_or_class)

0 commit comments

Comments
 (0)