bpo-32433: Optimized HMAC digest (#5023) · pythoncapi/cpython@2f050c7 · GitHub
Skip to content

Commit 2f050c7

Browse files
authored
bpo-32433: Optimized HMAC digest (python#5023)
The hmac module now has hmac.digest(), which provides an optimized HMAC digest for short messages. hmac.digest() is up to three times faster than hmac.HMAC().digest(). Signed-off-by: Christian Heimes <christian@python.org>
1 parent a49ac99 commit 2f050c7

7 files changed

Lines changed: 204 additions & 3 deletions

File tree

Doc/library/hmac.rst

Lines changed: 15 additions & 0 deletions

Doc/whatsnew/3.7.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,13 @@ and the ``--directory`` to the command line of the module :mod:`~http.server`.
492492
With this parameter, the server serves the specified directory, by default it uses the current working directory.
493493
(Contributed by Stéphane Wirtel and Julien Palard in :issue:`28707`.)
494494

495+
hmac
496+
----
497+
498+
The hmac module now has an optimized one-shot :func:`~hmac.digest` function,
499+
which is up to three times faster than :func:`~hmac.HMAC`.
500+
(Contributed by Christian Heimes in :issue:`32433`.)
501+
495502
importlib
496503
---------
497504

Lib/hmac.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55

66
import warnings as _warnings
77
from _operator import _compare_digest as compare_digest
8+
try:
9+
import _hashlib as _hashopenssl
10+
except ImportError:
11+
_hashopenssl = None
12+
_openssl_md_meths = None
13+
else:
14+
_openssl_md_meths = frozenset(_hashopenssl.openssl_md_meth_names)
815
import hashlib as _hashlib
916

1017
trans_5C = bytes((x ^ 0x5C) for x in range(256))
@@ -142,3 +149,38 @@ def new(key, msg = None, digestmod = None):
142149
method.
143150
"""
144151
return HMAC(key, msg, digestmod)
152+
153+
154+
def digest(key, msg, digest):
155+
"""Fast inline implementation of HMAC
156+
157+
key: key for the keyed hash object.
158+
msg: input message
159+
digest: A hash name suitable for hashlib.new() for best performance. *OR*
160+
A hashlib constructor returning a new hash object. *OR*
161+
A module supporting PEP 247.
162+
163+
Note: key and msg must be a bytes or bytearray objects.
164+
"""
165+
if (_hashopenssl is not None and
166+
isinstance(digest, str) and digest in _openssl_md_meths):
167+
return _hashopenssl.hmac_digest(key, msg, digest)
168+
169+
if callable(digest):
170+
digest_cons = digest
171+
elif isinstance(digest, str):
172+
digest_cons = lambda d=b'': _hashlib.new(digest, d)
173+
else:
174+
digest_cons = lambda d=b'': digest.new(d)
175+
176+
inner = digest_cons()
177+
outer = digest_cons()
178+
blocksize = getattr(inner, 'block_size', 64)
179+
if len(key) > blocksize:
180+
key = digest_cons(key).digest()
181+
key = key + b'\x00' * (blocksize - len(key))
182+
inner.update(key.translate(trans_36))
183+
outer.update(key.translate(trans_5C))
184+
inner.update(msg)
185+
outer.update(inner.digest())
186+
return outer.digest()

Lib/test/test_hmac.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import binascii
12
import functools
23
import hmac
34
import hashlib
45
import unittest
6+
import unittest.mock
57
import warnings
68

79

@@ -23,16 +25,27 @@ def test_md5_vectors(self):
2325
def md5test(key, data, digest):
2426
h = hmac.HMAC(key, data, digestmod=hashlib.md5)
2527
self.assertEqual(h.hexdigest().upper(), digest.upper())
28+
self.assertEqual(h.digest(), binascii.unhexlify(digest))
2629
self.assertEqual(h.name, "hmac-md5")
2730
self.assertEqual(h.digest_size, 16)
2831
self.assertEqual(h.block_size, 64)
2932

3033
h = hmac.HMAC(key, data, digestmod='md5')
3134
self.assertEqual(h.hexdigest().upper(), digest.upper())
35+
self.assertEqual(h.digest(), binascii.unhexlify(digest))
3236
self.assertEqual(h.name, "hmac-md5")
3337
self.assertEqual(h.digest_size, 16)
3438
self.assertEqual(h.block_size, 64)
3539

40+
self.assertEqual(
41+
hmac.digest(key, data, digest='md5'),
42+
binascii.unhexlify(digest)
43+
)
44+
with unittest.mock.patch('hmac._openssl_md_meths', {}):
45+
self.assertEqual(
46+
hmac.digest(key, data, digest='md5'),
47+
binascii.unhexlify(digest)
48+
)
3649

3750
md5test(b"\x0b" * 16,
3851
b"Hi There",
@@ -67,16 +80,23 @@ def test_sha_vectors(self):
6780
def shatest(key, data, digest):
6881
h = hmac.HMAC(key, data, digestmod=hashlib.sha1)
6982
self.assertEqual(h.hexdigest().upper(), digest.upper())
83+
self.assertEqual(h.digest(), binascii.unhexlify(digest))
7084
self.assertEqual(h.name, "hmac-sha1")
7185
self.assertEqual(h.digest_size, 20)
7286
self.assertEqual(h.block_size, 64)
7387

7488
h = hmac.HMAC(key, data, digestmod='sha1')
7589
self.assertEqual(h.hexdigest().upper(), digest.upper())
90+
self.assertEqual(h.digest(), binascii.unhexlify(digest))
7691
self.assertEqual(h.name, "hmac-sha1")
7792
self.assertEqual(h.digest_size, 20)
7893
self.assertEqual(h.block_size, 64)
7994

95+
self.assertEqual(
96+
hmac.digest(key, data, digest='sha1'),
97+
binascii.unhexlify(digest)
98+
)
99+
80100

81101
shatest(b"\x0b" * 20,
82102
b"Hi There",
@@ -122,6 +142,24 @@ def hmactest(key, data, hexdigests):
122142
self.assertEqual(h.digest_size, digest_size)
123143
self.assertEqual(h.block_size, block_size)
124144

145+
self.assertEqual(
146+
hmac.digest(key, data, digest=hashfunc),
147+
binascii.unhexlify(hexdigests[hashfunc])
148+
)
149+
self.assertEqual(
150+
hmac.digest(key, data, digest=hash_name),
151+
binascii.unhexlify(hexdigests[hashfunc])
152+
)
153+
154+
with unittest.mock.patch('hmac._openssl_md_meths', {}):
155+
self.assertEqual(
156+
hmac.digest(key, data, digest=hashfunc),
157+
binascii.unhexlify(hexdigests[hashfunc])
158+
)
159+
self.assertEqual(
160+
hmac.digest(key, data, digest=hash_name),
161+
binascii.unhexlify(hexdigests[hashfunc])
162+
)
125163

126164
# 4.2. Test Case 1
127165
hmactest(key = b'\x0b'*20,
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The hmac module now has hmac.digest(), which provides an optimized HMAC
2+
digest.

Modules/_hashopenssl.c

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
/* EVP is the preferred interface to hashing in OpenSSL */
2323
#include <openssl/evp.h>
24+
#include <openssl/hmac.h>
2425
/* We use the object interface to discover what hashes OpenSSL supports. */
2526
#include <openssl/objects.h>
2627
#include "openssl/err.h"
@@ -528,8 +529,6 @@ EVP_new(PyObject *self, PyObject *args, PyObject *kwdict)
528529
return ret_obj;
529530
}
530531

531-
532-
533532
#if (OPENSSL_VERSION_NUMBER >= 0x10000000 && !defined(OPENSSL_NO_HMAC) \
534533
&& !defined(OPENSSL_NO_SHA))
535534

@@ -849,6 +848,61 @@ _hashlib_scrypt_impl(PyObject *module, Py_buffer *password, Py_buffer *salt,
849848
}
850849
#endif
851850

851+
/* Fast HMAC for hmac.digest()
852+
*/
853+
854+
/*[clinic input]
855+
_hashlib.hmac_digest
856+
857+
key: Py_buffer
858+
msg: Py_buffer
859+
digest: str
860+
861+
Single-shot HMAC
862+
[clinic start generated code]*/
863+
864+
static PyObject *
865+
_hashlib_hmac_digest_impl(PyObject *module, Py_buffer *key, Py_buffer *msg,
866+
const char *digest)
867+
/*[clinic end generated code: output=75630e684cdd8762 input=10e964917921e2f2]*/
868+
{
869+
unsigned char md[EVP_MAX_MD_SIZE] = {0};
870+
unsigned int md_len = 0;
871+
unsigned char *result;
872+
const EVP_MD *evp;
873+
874+
evp = EVP_get_digestbyname(digest);
875+
if (evp == NULL) {
876+
PyErr_SetString(PyExc_ValueError, "unsupported hash type");
877+
return NULL;
878+
}
879+
if (key->len > INT_MAX) {
880+
PyErr_SetString(PyExc_OverflowError,
881+
"key is too long.");
882+
return NULL;
883+
}
884+
if (msg->len > INT_MAX) {
885+
PyErr_SetString(PyExc_OverflowError,
886+
"msg is too long.");
887+
return NULL;
888+
}
889+
890+
Py_BEGIN_ALLOW_THREADS
891+
result = HMAC(
892+
evp,
893+
(const void*)key->buf, (int)key->len,
894+
(const unsigned char*)msg->buf, (int)msg->len,
895+
md, &md_len
896+
);
897+
Py_END_ALLOW_THREADS
898+
899+
if (result == NULL) {
900+
_setException(PyExc_ValueError);
901+
return NULL;
902+
}
903+
return PyBytes_FromStringAndSize((const char*)md, md_len);
904+
}
905+
852906
/* State for our callback function so that it can accumulate a result. */
853907
typedef struct _internal_name_mapper_state {
854908
PyObject *set;
@@ -982,6 +1036,7 @@ static struct PyMethodDef EVP_functions[] = {
9821036
pbkdf2_hmac__doc__},
9831037
#endif
9841038
_HASHLIB_SCRYPT_METHODDEF
1039+
_HASHLIB_HMAC_DIGEST_METHODDEF
9851040
CONSTRUCTOR_METH_DEF(md5),
9861041
CONSTRUCTOR_METH_DEF(sha1),
9871042
CONSTRUCTOR_METH_DEF(sha224),

Modules/clinic/_hashopenssl.c.h

Lines changed: 43 additions & 1 deletion

0 commit comments

Comments
 (0)