Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Lib/hmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ def __init(self, key, msg, digestmod):
try:
self._init_openssl_hmac(key, msg, digestmod)
return
except _hashopenssl.UnsupportedDigestmodError:
except _hashopenssl.UnsupportedDigestmodError: # pragma: no cover
pass
if _hmac and isinstance(digestmod, str):
try:
self._init_builtin_hmac(key, msg, digestmod)
return
except _hmac.UnknownHashError:
except _hmac.UnknownHashError: # pragma: no cover
pass
self._init_old(key, msg, digestmod)

Expand Down Expand Up @@ -121,12 +121,12 @@ def _init_old(self, key, msg, digestmod):
warnings.warn(f"block_size of {blocksize} seems too small; "
f"using our default of {self.blocksize}.",
RuntimeWarning, 2)
blocksize = self.blocksize
blocksize = self.blocksize # pragma: no cover
else:
warnings.warn("No block_size attribute on given digest object; "
f"Assuming {self.blocksize}.",
RuntimeWarning, 2)
blocksize = self.blocksize
blocksize = self.blocksize # pragma: no cover

if len(key) > blocksize:
key = digest_cons(key).digest()
Expand Down
168 changes: 120 additions & 48 deletions Lib/test/test_hmac.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import test.support.hashlib_helper as hashlib_helper
import types
import unittest
import unittest.mock
import unittest.mock as mock
import warnings
from _operator import _compare_digest as operator_compare_digest
from test.support import check_disallow_instantiation
Expand Down Expand Up @@ -58,10 +58,14 @@ def setUpClass(cls):
cls.hmac = import_fresh_module('_hmac')


# Sentinel object used to detect whether a digestmod is given or not.
DIGESTMOD_SENTINEL = object()


class CreatorMixin:
"""Mixin exposing a method creating a HMAC object."""

def hmac_new(self, key, msg=None, digestmod=None):
def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
"""Create a new HMAC object.

Implementations should accept arbitrary 'digestmod' as this
Expand All @@ -77,7 +81,7 @@ def bind_hmac_new(self, digestmod):
class DigestMixin:
"""Mixin exposing a method computing a HMAC digest."""

def hmac_digest(self, key, msg=None, digestmod=None):
def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
"""Compute a HMAC digest.

Implementations should accept arbitrary 'digestmod' as this
Expand All @@ -90,53 +94,67 @@ def bind_hmac_digest(self, digestmod):
return functools.partial(self.hmac_digest, digestmod=digestmod)


def _call_newobj_func(new_func, key, msg, digestmod):
if digestmod is DIGESTMOD_SENTINEL: # to test when digestmod is missing
return new_func(key, msg) # expected to raise
# functions creating HMAC objects take a 'digestmod' keyword argument
return new_func(key, msg, digestmod=digestmod)


def _call_digest_func(digest_func, key, msg, digestmod):
if digestmod is DIGESTMOD_SENTINEL: # to test when digestmod is missing
return digest_func(key, msg) # expected to raise
# functions directly computing digests take a 'digest' keyword argument
return digest_func(key, msg, digest=digestmod)


class ThroughObjectMixin(ModuleMixin, CreatorMixin, DigestMixin):
"""Mixin delegating to <module>.HMAC() and <module>.HMAC(...).digest().

Both the C implementation and the Python implementation of HMAC should
expose a HMAC class with the same functionalities.
"""

def hmac_new(self, key, msg=None, digestmod=None):
def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
"""Create a HMAC object via a module-level class constructor."""
return self.hmac.HMAC(key, msg, digestmod=digestmod)
return _call_newobj_func(self.hmac.HMAC, key, msg, digestmod)

def hmac_digest(self, key, msg=None, digestmod=None):
def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
"""Call the digest() method on a HMAC object obtained by hmac_new()."""
return self.hmac_new(key, msg, digestmod).digest()
return _call_newobj_func(self.hmac_new, key, msg, digestmod).digest()


class ThroughModuleAPIMixin(ModuleMixin, CreatorMixin, DigestMixin):
"""Mixin delegating to <module>.new() and <module>.digest()."""

def hmac_new(self, key, msg=None, digestmod=None):
def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
"""Create a HMAC object via a module-level function."""
return self.hmac.new(key, msg, digestmod=digestmod)
return _call_newobj_func(self.hmac.new, key, msg, digestmod)

def hmac_digest(self, key, msg=None, digestmod=None):
def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
"""One-shot HMAC digest computation."""
return self.hmac.digest(key, msg, digest=digestmod)
return _call_digest_func(self.hmac.digest, key, msg, digestmod)


@hashlib_helper.requires_hashlib()
class ThroughOpenSSLAPIMixin(CreatorMixin, DigestMixin):
"""Mixin delegating to _hashlib.hmac_new() and _hashlib.hmac_digest()."""

def hmac_new(self, key, msg=None, digestmod=None):
return _hashlib.hmac_new(key, msg, digestmod=digestmod)
def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
return _call_newobj_func(_hashlib.hmac_new, key, msg, digestmod)

def hmac_digest(self, key, msg=None, digestmod=None):
return _hashlib.hmac_digest(key, msg, digest=digestmod)
def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
return _call_digest_func(_hashlib.hmac_digest, key, msg, digestmod)


class ThroughBuiltinAPIMixin(BuiltinModuleMixin, CreatorMixin, DigestMixin):
"""Mixin delegating to _hmac.new() and _hmac.compute_digest()."""

def hmac_new(self, key, msg=None, digestmod=None):
return self.hmac.new(key, msg, digestmod=digestmod)
def hmac_new(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
return _call_newobj_func(self.hmac.new, key, msg, digestmod)

def hmac_digest(self, key, msg=None, digestmod=None):
return self.hmac.compute_digest(key, msg, digest=digestmod)
def hmac_digest(self, key, msg=None, digestmod=DIGESTMOD_SENTINEL):
return _call_digest_func(self.hmac.compute_digest, key, msg, digestmod)


class ObjectCheckerMixin:
Expand Down Expand Up @@ -777,7 +795,8 @@ class DigestModTestCaseMixin(CreatorMixin, DigestMixin):

def assert_raises_missing_digestmod(self):
"""A context manager catching errors when a digestmod is missing."""
return self.assertRaisesRegex(TypeError, "Missing required.*digestmod")
return self.assertRaisesRegex(TypeError,
"[M|m]issing.*required.*digestmod")

def assert_raises_unknown_digestmod(self):
"""A context manager catching errors when a digestmod is unknown."""
Expand All @@ -804,19 +823,23 @@ def do_test_constructor_unknown_digestmod(self, catcher):
def cases_missing_digestmod_in_constructor(self):
raise NotImplementedError

def make_missing_digestmod_cases(self, func, choices):
"""Generate cases for missing digestmod tests."""
def make_missing_digestmod_cases(self, func, missing_like=()):
"""Generate cases for missing digestmod tests.

Only the Python implementation should consider "falsey" 'digestmod'
values as being equivalent to a missing one.
"""
key, msg = b'unused key', b'unused msg'
cases = self._invalid_digestmod_cases(func, key, msg, choices)
return [(func, (key,), {}), (func, (key, msg), {})] + cases
choices = [DIGESTMOD_SENTINEL, *missing_like]
return self._invalid_digestmod_cases(func, key, msg, choices)

def cases_unknown_digestmod_in_constructor(self):
raise NotImplementedError

def make_unknown_digestmod_cases(self, func, choices):
def make_unknown_digestmod_cases(self, func, bad_digestmods):
"""Generate cases for unknown digestmod tests."""
key, msg = b'unused key', b'unused msg'
return self._invalid_digestmod_cases(func, key, msg, choices)
return self._invalid_digestmod_cases(func, key, msg, bad_digestmods)

def _invalid_digestmod_cases(self, func, key, msg, choices):
cases = []
Expand Down Expand Up @@ -932,19 +955,12 @@ def test_internal_types(self):
with self.assertRaisesRegex(TypeError, "immutable type"):
self.obj_type.value = None

def assert_digestmod_error(self):
def assert_raises_unknown_digestmod(self):
self.assertIsSubclass(self.exc_type, ValueError)
return self.assertRaises(self.exc_type)

def test_constructor_missing_digestmod(self):
self.do_test_constructor_missing_digestmod(self.assert_digestmod_error)

def test_constructor_unknown_digestmod(self):
self.do_test_constructor_unknown_digestmod(self.assert_digestmod_error)

def cases_missing_digestmod_in_constructor(self):
func, choices = self.hmac_new, ['', None, False]
return self.make_missing_digestmod_cases(func, choices)
return self.make_missing_digestmod_cases(self.hmac_new)

def cases_unknown_digestmod_in_constructor(self):
func, choices = self.hmac_new, ['unknown', 1234]
Expand All @@ -967,7 +983,10 @@ def test_hmac_digest_digestmod_parameter(self):
# TODO(picnixz): remove default arguments in _hashlib.hmac_digest()
# since the return value is not a HMAC object but a bytes object.
for value in [object, 'unknown', 1234, None]:
with self.subTest(value=value), self.assert_digestmod_error():
with (
self.subTest(value=value),
self.assert_raises_unknown_digestmod()
):
self.hmac_digest(b'key', b'msg', value)


Expand All @@ -985,7 +1004,10 @@ def exc_type(self):

def test_hmac_digest_digestmod_parameter(self):
for value in [object, 'unknown', 1234, None]:
with self.subTest(value=value), self.assert_digestmod_error():
with (
self.subTest(value=value),
self.assert_raises_unknown_digestmod(),
):
self.hmac_digest(b'key', b'msg', value)


Expand All @@ -1000,6 +1022,9 @@ class SanityTestCaseMixin(CreatorMixin):
hmac_class: type
# The underlying hash function name (should be accepted by the HMAC class).
digestname: str
# The expected digest and block sizes (must be hardcoded).
digest_size: int
block_size: int

def test_methods(self):
h = self.hmac_new(b"my secret key", digestmod=self.digestname)
Expand All @@ -1009,6 +1034,12 @@ def test_methods(self):
self.assertIsInstance(h.hexdigest(), str)
self.assertIsInstance(h.copy(), self.hmac_class)

def test_properties(self):
h = self.hmac_new(b"my secret key", digestmod=self.digestname)
self.assertEqual(h.name, f"hmac-{self.digestname}")
self.assertEqual(h.digest_size, self.digest_size)
self.assertEqual(h.block_size, self.block_size)

def test_repr(self):
# HMAC object representation may differ across implementations
raise NotImplementedError
Expand All @@ -1023,6 +1054,8 @@ def setUpClass(cls):
super().setUpClass()
cls.hmac_class = cls.hmac.HMAC
cls.digestname = 'sha256'
cls.digest_size = 32
cls.block_size = 64

def test_repr(self):
h = self.hmac_new(b"my secret key", digestmod=self.digestname)
Expand All @@ -1038,6 +1071,8 @@ def setUpClass(cls):
super().setUpClass()
cls.hmac_class = _hashlib.HMAC
cls.digestname = 'sha256'
cls.digest_size = 32
cls.block_size = 64

def test_repr(self):
h = self.hmac_new(b"my secret key", digestmod=self.digestname)
Expand All @@ -1052,6 +1087,8 @@ def setUpClass(cls):
super().setUpClass()
cls.hmac_class = cls.hmac.HMAC
cls.digestname = 'sha256'
cls.digest_size = 32
cls.block_size = 64

def test_repr(self):
h = self.hmac_new(b"my secret key", digestmod=self.digestname)
Expand All @@ -1065,16 +1102,30 @@ def HMAC(self, key, msg=None):
"""Create a HMAC object."""
raise NotImplementedError

def check_update(self, key, chunks):
chunks = list(chunks)
msg = b''.join(chunks)
h1 = self.HMAC(key, msg)

h2 = self.HMAC(key)
for chunk in chunks:
h2.update(chunk)

self.assertEqual(h1.digest(), h2.digest())
self.assertEqual(h1.hexdigest(), h2.hexdigest())

def test_update(self):
key, msg = random.randbytes(16), random.randbytes(16)
with self.subTest(key=key, msg=msg):
h1 = self.HMAC(key, msg)
self.check_update(key, [msg])

h2 = self.HMAC(key)
h2.update(msg)
def test_update_large(self):
HASHLIB_GIL_MINSIZE = 2048

self.assertEqual(h1.digest(), h2.digest())
self.assertEqual(h1.hexdigest(), h2.hexdigest())
key = random.randbytes(16)
top = random.randbytes(HASHLIB_GIL_MINSIZE + 1)
bot = random.randbytes(HASHLIB_GIL_MINSIZE + 1)
self.check_update(key, [top, bot])

def test_update_exceptions(self):
h = self.HMAC(b"key")
Expand All @@ -1084,12 +1135,7 @@ def test_update_exceptions(self):


@hashlib_helper.requires_hashdigest('sha256')
class PyUpdateTestCase(UpdateTestCaseMixin, unittest.TestCase):

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.hmac = import_fresh_module('hmac', blocked=['_hashlib', '_hmac'])
class PyUpdateTestCase(PyModuleMixin, UpdateTestCaseMixin, unittest.TestCase):

def HMAC(self, key, msg=None):
return self.hmac.HMAC(key, msg, digestmod='sha256')
Expand Down Expand Up @@ -1345,6 +1391,32 @@ class OperatorCompareDigestTestCase(CompareDigestMixin, unittest.TestCase):
class PyMiscellaneousTests(unittest.TestCase):
"""Miscellaneous tests for the pure Python HMAC module."""

@hashlib_helper.requires_builtin_hmac()
def test_hmac_constructor_uses_builtin(self):
# Block the OpenSSL implementation and check that
# HMAC() uses the built-in implementation instead.
hmac = import_fresh_module("hmac", blocked=["_hashlib"])

def watch_method(cls, name):
return mock.patch.object(
cls, name, autospec=True, wraps=getattr(cls, name)
)

with (
watch_method(hmac.HMAC, '_init_openssl_hmac') as f,
watch_method(hmac.HMAC, '_init_builtin_hmac') as g,
):
_ = hmac.HMAC(b'key', b'msg', digestmod="sha256")
f.assert_not_called()
g.assert_called_once()

@hashlib_helper.requires_hashdigest('sha256')
def test_hmac_delegated_properties(self):
h = hmac.HMAC(b'key', b'msg', digestmod="sha256")
self.assertEqual(h.name, "hmac-sha256")
self.assertEqual(h.digest_size, 32)
self.assertEqual(h.block_size, 64)

@hashlib_helper.requires_hashdigest('sha256')
def test_legacy_block_size_warnings(self):
class MockCrazyHash(object):
Expand Down
Loading