Skip to content

Commit d658b90

Browse files
authored
gh-136912: fix handling of OverflowError in hmac.digest (#136917)
The OpenSSL and HACL* implementations of HMAC single-shot digest computation reject keys whose length exceeds `INT_MAX` and `UINT32_MAX` respectively. The OpenSSL implementation also rejects messages whose length exceed `INT_MAX`. Using such keys in `hmac.digest` previously raised an `OverflowError` which was propagated to the caller. This commit mitigates this case by making `hmac.digest` fall back to HMAC's pure Python implementation which accepts arbitrary large keys or messages. This change only affects the top-level entrypoint `hmac.digest`, leaving `_hashopenssl.hmac_digest` and `_hmac.compute_digest` untouched.
1 parent f7c380e commit d658b90

File tree

3 files changed

+70
-9
lines changed

3 files changed

+70
-9
lines changed

Lib/hmac.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,13 +241,25 @@ def digest(key, msg, digest):
241241
if _hashopenssl and isinstance(digest, (str, _functype)):
242242
try:
243243
return _hashopenssl.hmac_digest(key, msg, digest)
244+
except OverflowError:
245+
# OpenSSL's HMAC limits the size of the key to INT_MAX.
246+
# Instead of falling back to HACL* implementation which
247+
# may still not be supported due to a too large key, we
248+
# directly switch to the pure Python fallback instead
249+
# even if we could have used streaming HMAC for small keys
250+
# but large messages.
251+
return _compute_digest_fallback(key, msg, digest)
244252
except _hashopenssl.UnsupportedDigestmodError:
245253
pass
246254

247255
if _hmac and isinstance(digest, str):
248256
try:
249257
return _hmac.compute_digest(key, msg, digest)
250258
except (OverflowError, _hmac.UnknownHashError):
259+
# HACL* HMAC limits the size of the key to UINT32_MAX
260+
# so we fallback to the pure Python implementation even
261+
# if streaming HMAC may have been used for small keys
262+
# and large messages.
251263
pass
252264

253265
return _compute_digest_fallback(key, msg, digest)

Lib/test/test_hmac.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,21 @@
2121
import hmac
2222
import hashlib
2323
import random
24-
import test.support.hashlib_helper as hashlib_helper
2524
import types
2625
import unittest
27-
import unittest.mock as mock
2826
import warnings
2927
from _operator import _compare_digest as operator_compare_digest
28+
from test.support import _4G, bigmemtest
3029
from test.support import check_disallow_instantiation
30+
from test.support import hashlib_helper, import_helper
3131
from test.support.hashlib_helper import (
3232
BuiltinHashFunctionsTrait,
3333
HashFunctionsTrait,
3434
NamedHashFunctionsTrait,
3535
OpenSSLHashFunctionsTrait,
3636
)
37-
from test.support.import_helper import import_fresh_module, import_module
37+
from test.support.import_helper import import_fresh_module
38+
from unittest.mock import patch
3839

3940
try:
4041
import _hashlib
@@ -727,7 +728,7 @@ def setUpClass(cls):
727728
super().setUpClass()
728729
for meth in ['_init_openssl_hmac', '_init_builtin_hmac']:
729730
fn = getattr(cls.hmac.HMAC, meth)
730-
cm = mock.patch.object(cls.hmac.HMAC, meth, autospec=True, wraps=fn)
731+
cm = patch.object(cls.hmac.HMAC, meth, autospec=True, wraps=fn)
731732
cls.enterClassContext(cm)
732733

733734
@classmethod
@@ -949,7 +950,11 @@ class PyConstructorTestCase(ThroughObjectMixin, PyConstructorBaseMixin,
949950

950951
class PyModuleConstructorTestCase(ThroughModuleAPIMixin, PyConstructorBaseMixin,
951952
unittest.TestCase):
952-
"""Test the hmac.new() and hmac.digest() functions."""
953+
"""Test the hmac.new() and hmac.digest() functions.
954+
955+
Note that "self.hmac" is imported by blocking "_hashlib" and "_hmac".
956+
For testing functions in "hmac", extend PyMiscellaneousTests instead.
957+
"""
953958

954959
def test_hmac_digest_digestmod_parameter(self):
955960
func = self.hmac_digest
@@ -1445,9 +1450,8 @@ def test_hmac_constructor_uses_builtin(self):
14451450
hmac = import_fresh_module("hmac", blocked=["_hashlib"])
14461451

14471452
def watch_method(cls, name):
1448-
return mock.patch.object(
1449-
cls, name, autospec=True, wraps=getattr(cls, name)
1450-
)
1453+
wraps = getattr(cls, name)
1454+
return patch.object(cls, name, autospec=True, wraps=wraps)
14511455

14521456
with (
14531457
watch_method(hmac.HMAC, '_init_openssl_hmac') as f,
@@ -1499,6 +1503,48 @@ def test_with_fallback(self):
14991503
finally:
15001504
cache.pop('foo')
15011505

1506+
@hashlib_helper.requires_openssl_hashdigest("md5")
1507+
@bigmemtest(size=_4G + 5, memuse=2, dry_run=False)
1508+
def test_hmac_digest_overflow_error_openssl_only(self, size):
1509+
hmac = import_fresh_module("hmac", blocked=["_hmac"])
1510+
self.do_test_hmac_digest_overflow_error_switch_to_slow(hmac, size)
1511+
1512+
@hashlib_helper.requires_builtin_hashdigest("_md5", "md5")
1513+
@bigmemtest(size=_4G + 5, memuse=2, dry_run=False)
1514+
def test_hmac_digest_overflow_error_builtin_only(self, size):
1515+
hmac = import_fresh_module("hmac", blocked=["_hashlib"])
1516+
self.do_test_hmac_digest_overflow_error_switch_to_slow(hmac, size)
1517+
1518+
def do_test_hmac_digest_overflow_error_switch_to_slow(self, hmac, size):
1519+
"""Check that hmac.digest() falls back to pure Python.
1520+
1521+
The *hmac* argument implements the HMAC module interface.
1522+
The *size* argument is a large key size or message size that would
1523+
trigger an OverflowError in the C implementation(s) of hmac.digest().
1524+
"""
1525+
1526+
bigkey = b'K' * size
1527+
bigmsg = b'M' * size
1528+
1529+
with patch.object(hmac, "_compute_digest_fallback") as slow:
1530+
hmac.digest(bigkey, b'm', "md5")
1531+
slow.assert_called_once()
1532+
1533+
with patch.object(hmac, "_compute_digest_fallback") as slow:
1534+
hmac.digest(b'k', bigmsg, "md5")
1535+
slow.assert_called_once()
1536+
1537+
@hashlib_helper.requires_hashdigest("md5", openssl=True)
1538+
@bigmemtest(size=_4G + 5, memuse=2, dry_run=False)
1539+
def test_hmac_digest_no_overflow_error_in_fallback(self, size):
1540+
hmac = import_fresh_module("hmac", blocked=["_hashlib", "_hmac"])
1541+
1542+
for key, msg in [(b'K' * size, b'm'), (b'k', b'M' * size)]:
1543+
with self.subTest(keysize=len(key), msgsize=len(msg)):
1544+
with patch.object(hmac, "_compute_digest_fallback") as slow:
1545+
hmac.digest(key, msg, "md5")
1546+
slow.assert_called_once()
1547+
15021548

15031549
class BuiltinMiscellaneousTests(BuiltinModuleMixin, unittest.TestCase):
15041550
"""HMAC-BLAKE2 is not standardized as BLAKE2 is a keyed hash function.
@@ -1511,7 +1557,7 @@ class BuiltinMiscellaneousTests(BuiltinModuleMixin, unittest.TestCase):
15111557
@classmethod
15121558
def setUpClass(cls):
15131559
super().setUpClass()
1514-
cls.blake2 = import_module("_blake2")
1560+
cls.blake2 = import_helper.import_module("_blake2")
15151561
cls.blake2b = cls.blake2.blake2b
15161562
cls.blake2s = cls.blake2.blake2s
15171563

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`hmac.digest` now properly handles large keys and messages
2+
by falling back to the pure Python implementation when necessary.
3+
Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)