Skip to content

Commit 8ad6eda

Browse files
authored
[3.14] gh-136912: fix handling of OverflowError in hmac.digest (GH-136917) (#137116)
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. (cherry picked from commit d658b90)
1 parent c625839 commit 8ad6eda

File tree

3 files changed

+70
-10
lines changed

3 files changed

+70
-10
lines changed

Lib/hmac.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,25 @@ def digest(key, msg, digest):
229229
if _hashopenssl and isinstance(digest, (str, _functype)):
230230
try:
231231
return _hashopenssl.hmac_digest(key, msg, digest)
232+
except OverflowError:
233+
# OpenSSL's HMAC limits the size of the key to INT_MAX.
234+
# Instead of falling back to HACL* implementation which
235+
# may still not be supported due to a too large key, we
236+
# directly switch to the pure Python fallback instead
237+
# even if we could have used streaming HMAC for small keys
238+
# but large messages.
239+
return _compute_digest_fallback(key, msg, digest)
232240
except _hashopenssl.UnsupportedDigestmodError:
233241
pass
234242

235243
if _hmac and isinstance(digest, str):
236244
try:
237245
return _hmac.compute_digest(key, msg, digest)
238246
except (OverflowError, _hmac.UnknownHashError):
247+
# HACL* HMAC limits the size of the key to UINT32_MAX
248+
# so we fallback to the pure Python implementation even
249+
# if streaming HMAC may have been used for small keys
250+
# and large messages.
239251
pass
240252

241253
return _compute_digest_fallback(key, msg, digest)

Lib/test/test_hmac.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@
2121
import hmac
2222
import hashlib
2323
import random
24-
import test.support
25-
import test.support.hashlib_helper as hashlib_helper
2624
import types
2725
import unittest
28-
import unittest.mock as mock
2926
import warnings
3027
from _operator import _compare_digest as operator_compare_digest
28+
from test.support import _4G, bigmemtest
3129
from test.support import check_disallow_instantiation
30+
from test.support import hashlib_helper, import_helper
3231
from test.support.hashlib_helper import (
3332
BuiltinHashFunctionsTrait,
3433
HashFunctionsTrait,
3534
NamedHashFunctionsTrait,
3635
OpenSSLHashFunctionsTrait,
3736
)
38-
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
3939

4040
try:
4141
import _hashlib
@@ -728,7 +728,7 @@ def setUpClass(cls):
728728
super().setUpClass()
729729
for meth in ['_init_openssl_hmac', '_init_builtin_hmac']:
730730
fn = getattr(cls.hmac.HMAC, meth)
731-
cm = mock.patch.object(cls.hmac.HMAC, meth, autospec=True, wraps=fn)
731+
cm = patch.object(cls.hmac.HMAC, meth, autospec=True, wraps=fn)
732732
cls.enterClassContext(cm)
733733

734734
@classmethod
@@ -950,7 +950,11 @@ class PyConstructorTestCase(ThroughObjectMixin, PyConstructorBaseMixin,
950950

951951
class PyModuleConstructorTestCase(ThroughModuleAPIMixin, PyConstructorBaseMixin,
952952
unittest.TestCase):
953-
"""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+
"""
954958

955959
def test_hmac_digest_digestmod_parameter(self):
956960
func = self.hmac_digest
@@ -1446,9 +1450,8 @@ def test_hmac_constructor_uses_builtin(self):
14461450
hmac = import_fresh_module("hmac", blocked=["_hashlib"])
14471451

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

14531456
with (
14541457
watch_method(hmac.HMAC, '_init_openssl_hmac') as f,
@@ -1500,6 +1503,48 @@ def test_with_fallback(self):
15001503
finally:
15011504
cache.pop('foo')
15021505

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+
15031548

15041549
class BuiltinMiscellaneousTests(BuiltinModuleMixin, unittest.TestCase):
15051550
"""HMAC-BLAKE2 is not standardized as BLAKE2 is a keyed hash function.
@@ -1512,7 +1557,7 @@ class BuiltinMiscellaneousTests(BuiltinModuleMixin, unittest.TestCase):
15121557
@classmethod
15131558
def setUpClass(cls):
15141559
super().setUpClass()
1515-
cls.blake2 = import_module("_blake2")
1560+
cls.blake2 = import_helper.import_module("_blake2")
15161561
cls.blake2b = cls.blake2.blake2b
15171562
cls.blake2s = cls.blake2.blake2s
15181563

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)