gh-130149: cleanup refactorization of test_hmac.py (#131318)

New features:

* refactor `hashlib_helper.requires_hashdigest` in prevision of a future
  `hashlib_helper.requires_builtin_hashdigest` for built-in hashes only
* add `hashlib_helper.requires_openssl_hashdigest` to request OpenSSL
   hashes, assuming that `_hashlib` exists.

Refactoring:

* split hmac.copy() test by implementation
* update how algorithms are discovered for RFC test cases
* simplify how OpenSSL hash digests are requested
* refactor hexdigest tests for RFC test vectors
* typo fix: `assert_hmac_hexdigest_by_new` -> `assert_hmac_hexdigest_by_name`

Improvements:

* strengthen contract on `hmac_new_by_name` and `hmac_digest_by_name`
* rename mixin classes to better match their responsibility
This commit is contained in:
Bénédikt Tran 2025-03-17 11:10:03 +01:00 committed by GitHub
parent 85c04f80fd
commit de8890f5ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 263 additions and 147 deletions

View file

@ -65,6 +65,7 @@ class HMAC:
def _init_hmac(self, key, msg, digestmod): def _init_hmac(self, key, msg, digestmod):
self._hmac = _hashopenssl.hmac_new(key, msg, digestmod=digestmod) self._hmac = _hashopenssl.hmac_new(key, msg, digestmod=digestmod)
self._inner = self._outer = None # because the slots are defined
self.digest_size = self._hmac.digest_size self.digest_size = self._hmac.digest_size
self.block_size = self._hmac.block_size self.block_size = self._hmac.block_size

View file

@ -1,6 +1,7 @@
import functools import functools
import hashlib import hashlib
import unittest import unittest
from test.support.import_helper import import_module
try: try:
import _hashlib import _hashlib
@ -12,44 +13,81 @@ def requires_hashlib():
return unittest.skipIf(_hashlib is None, "requires _hashlib") return unittest.skipIf(_hashlib is None, "requires _hashlib")
def requires_hashdigest(digestname, openssl=None, usedforsecurity=True): def _decorate_func_or_class(func_or_class, decorator_func):
"""Decorator raising SkipTest if a hashing algorithm is not available if not isinstance(func_or_class, type):
return decorator_func(func_or_class)
The hashing algorithm could be missing or blocked by a strict crypto decorated_class = func_or_class
policy. setUpClass = decorated_class.__dict__.get('setUpClass')
if setUpClass is None:
def setUpClass(cls):
super(decorated_class, cls).setUpClass()
setUpClass.__qualname__ = decorated_class.__qualname__ + '.setUpClass'
setUpClass.__module__ = decorated_class.__module__
else:
setUpClass = setUpClass.__func__
setUpClass = classmethod(decorator_func(setUpClass))
decorated_class.setUpClass = setUpClass
return decorated_class
def requires_hashdigest(digestname, openssl=None, usedforsecurity=True):
"""Decorator raising SkipTest if a hashing algorithm is not available.
The hashing algorithm may be missing, blocked by a strict crypto policy,
or Python may be configured with `--with-builtin-hashlib-hashes=no`.
If 'openssl' is True, then the decorator checks that OpenSSL provides If 'openssl' is True, then the decorator checks that OpenSSL provides
the algorithm. Otherwise the check falls back to built-in the algorithm. Otherwise the check falls back to (optional) built-in
implementations. The usedforsecurity flag is passed to the constructor. HACL* implementations.
The usedforsecurity flag is passed to the constructor but has no effect
on HACL* implementations.
Examples of exceptions being suppressed:
ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS
ValueError: unsupported hash type md4 ValueError: unsupported hash type md4
""" """
def decorator(func_or_class):
if isinstance(func_or_class, type):
setUpClass = func_or_class.__dict__.get('setUpClass')
if setUpClass is None:
def setUpClass(cls):
super(func_or_class, cls).setUpClass()
setUpClass.__qualname__ = func_or_class.__qualname__ + '.setUpClass'
setUpClass.__module__ = func_or_class.__module__
else:
setUpClass = setUpClass.__func__
setUpClass = classmethod(decorator(setUpClass))
func_or_class.setUpClass = setUpClass
return func_or_class
@functools.wraps(func_or_class)
def wrapper(*args, **kwargs):
try:
if openssl and _hashlib is not None: if openssl and _hashlib is not None:
def test_availability():
_hashlib.new(digestname, usedforsecurity=usedforsecurity) _hashlib.new(digestname, usedforsecurity=usedforsecurity)
else: else:
def test_availability():
hashlib.new(digestname, usedforsecurity=usedforsecurity) hashlib.new(digestname, usedforsecurity=usedforsecurity)
except ValueError:
raise unittest.SkipTest( def decorator_func(func):
f"hash digest {digestname!r} is not available." @functools.wraps(func)
) def wrapper(*args, **kwargs):
return func_or_class(*args, **kwargs) try:
test_availability()
except ValueError as exc:
msg = f"missing hash algorithm: {digestname!r}"
raise unittest.SkipTest(msg) from exc
return func(*args, **kwargs)
return wrapper return wrapper
def decorator(func_or_class):
return _decorate_func_or_class(func_or_class, decorator_func)
return decorator
def requires_openssl_hashdigest(digestname, *, usedforsecurity=True):
"""Decorator raising SkipTest if an OpenSSL hashing algorithm is missing.
The hashing algorithm may be missing or blocked by a strict crypto policy.
"""
def decorator_func(func):
@requires_hashlib()
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
_hashlib.new(digestname, usedforsecurity=usedforsecurity)
except ValueError:
msg = f"missing OpenSSL hash algorithm: {digestname!r}"
raise unittest.SkipTest(msg)
return func(*args, **kwargs)
return wrapper
def decorator(func_or_class):
return _decorate_func_or_class(func_or_class, decorator_func)
return decorator return decorator

View file

@ -61,7 +61,11 @@ class CreatorMixin:
"""Mixin exposing a method creating a HMAC object.""" """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=None):
"""Create a new HMAC object.""" """Create a new HMAC object.
Implementations should accept arbitrary 'digestmod' as this
method can be used to test which exceptions are being raised.
"""
raise NotImplementedError raise NotImplementedError
def bind_hmac_new(self, digestmod): def bind_hmac_new(self, digestmod):
@ -73,7 +77,11 @@ class DigestMixin:
"""Mixin exposing a method computing a HMAC digest.""" """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=None):
"""Compute a HMAC digest.""" """Compute a HMAC digest.
Implementations should accept arbitrary 'digestmod' as this
method can be used to test which exceptions are being raised.
"""
raise NotImplementedError raise NotImplementedError
def bind_hmac_digest(self, digestmod): def bind_hmac_digest(self, digestmod):
@ -120,7 +128,7 @@ class ThroughOpenSSLAPIMixin(CreatorMixin, DigestMixin):
return _hashlib.hmac_digest(key, msg, digest=digestmod) return _hashlib.hmac_digest(key, msg, digest=digestmod)
class CheckerMixin: class ObjectCheckerMixin:
"""Mixin for checking HMAC objects (pure Python, OpenSSL or built-in).""" """Mixin for checking HMAC objects (pure Python, OpenSSL or built-in)."""
def check_object(self, h, hexdigest, hashname, digest_size, block_size): def check_object(self, h, hexdigest, hashname, digest_size, block_size):
@ -141,10 +149,10 @@ class CheckerMixin:
self.assertEqual(h.hexdigest().upper(), hexdigest.upper()) self.assertEqual(h.hexdigest().upper(), hexdigest.upper())
class TestVectorsMixin(CreatorMixin, DigestMixin, CheckerMixin): class AssertersMixin(CreatorMixin, DigestMixin, ObjectCheckerMixin):
"""Mixin class for all test vectors test cases.""" """Mixin class for common tests."""
def hmac_new_by_name(self, key, msg=None, hashname=None): def hmac_new_by_name(self, key, msg=None, *, hashname):
"""Alternative implementation of hmac_new(). """Alternative implementation of hmac_new().
This is typically useful when one needs to test against an HMAC This is typically useful when one needs to test against an HMAC
@ -152,13 +160,22 @@ class TestVectorsMixin(CreatorMixin, DigestMixin, CheckerMixin):
by their name (all HMAC implementations must at least recognize by their name (all HMAC implementations must at least recognize
hash functions by their names but some may use aliases such as hash functions by their names but some may use aliases such as
`hashlib.sha1` instead of "sha1"). `hashlib.sha1` instead of "sha1").
Unlike hmac_new(), this method may assert the type of 'hashname'
as it should only be used in tests that are expected to create
a HMAC object.
""" """
self.assertIsInstance(hashname, str | None) self.assertIsInstance(hashname, str)
return self.hmac_new(key, msg, digestmod=hashname) return self.hmac_new(key, msg, digestmod=hashname)
def hmac_digest_by_name(self, key, msg=None, hashname=None): def hmac_digest_by_name(self, key, msg=None, *, hashname):
"""Alternative implementation of hmac_digest().""" """Alternative implementation of hmac_digest().
self.assertIsInstance(hashname, str | None)
Unlike hmac_digest(), this method may assert the type of 'hashname'
as it should only be used in tests that are expected to compute a
HMAC digest.
"""
self.assertIsInstance(hashname, str)
return self.hmac_digest(key, msg, digestmod=hashname) return self.hmac_digest(key, msg, digestmod=hashname)
def assert_hmac( def assert_hmac(
@ -196,7 +213,7 @@ class TestVectorsMixin(CreatorMixin, DigestMixin, CheckerMixin):
self.assert_hmac_new_by_name( self.assert_hmac_new_by_name(
key, msg, hexdigest, hashname, digest_size, block_size key, msg, hexdigest, hashname, digest_size, block_size
) )
self.assert_hmac_hexdigest_by_new( self.assert_hmac_hexdigest_by_name(
key, msg, hexdigest, hashname, digest_size key, msg, hexdigest, hashname, digest_size
) )
@ -255,16 +272,28 @@ class TestVectorsMixin(CreatorMixin, DigestMixin, CheckerMixin):
self, key, msg, hexdigest, digestmod, digest_size, self, key, msg, hexdigest, digestmod, digest_size,
): ):
"""Check a HMAC digest computed by hmac_digest().""" """Check a HMAC digest computed by hmac_digest()."""
d = self.hmac_digest(key, msg, digestmod=digestmod) self._check_hmac_hexdigest(
self.assertEqual(len(d), digest_size) key, msg, hexdigest, digest_size,
self.assertEqual(d, binascii.unhexlify(hexdigest)) hmac_digest_func=self.hmac_digest,
hmac_digest_kwds={'digestmod': digestmod},
)
def assert_hmac_hexdigest_by_new( def assert_hmac_hexdigest_by_name(
self, key, msg, hexdigest, hashname, digest_size self, key, msg, hexdigest, hashname, digest_size
): ):
"""Check a HMAC digest computed by hmac_digest_by_name().""" """Check a HMAC digest computed by hmac_digest_by_name()."""
self.assertIsInstance(hashname, str | None) self.assertIsInstance(hashname, str)
d = self.hmac_digest_by_name(key, msg, hashname=hashname) self._check_hmac_hexdigest(
key, msg, hexdigest, digest_size,
hmac_digest_func=self.hmac_digest_by_name,
hmac_digest_kwds={'hashname': hashname},
)
def _check_hmac_hexdigest(
self, key, msg, hexdigest, digest_size,
hmac_digest_func, hmac_digest_kwds,
):
d = hmac_digest_func(key, msg, **hmac_digest_kwds)
self.assertEqual(len(d), digest_size) self.assertEqual(len(d), digest_size)
self.assertEqual(d, binascii.unhexlify(hexdigest)) self.assertEqual(d, binascii.unhexlify(hexdigest))
@ -279,7 +308,7 @@ class TestVectorsMixin(CreatorMixin, DigestMixin, CheckerMixin):
self.check_object(h1, hexdigest, hashname, digest_size, block_size) self.check_object(h1, hexdigest, hashname, digest_size, block_size)
class PyTestVectorsMixin(PyModuleMixin, TestVectorsMixin): class PyAssertersMixin(PyModuleMixin, AssertersMixin):
def assert_hmac_extra_cases( def assert_hmac_extra_cases(
self, key, msg, hexdigest, digestmod, hashname, digest_size, block_size self, key, msg, hexdigest, digestmod, hashname, digest_size, block_size
@ -293,46 +322,62 @@ class PyTestVectorsMixin(PyModuleMixin, TestVectorsMixin):
self.check_object(h, hexdigest, hashname, digest_size, block_size) self.check_object(h, hexdigest, hashname, digest_size, block_size)
class OpenSSLTestVectorsMixin(TestVectorsMixin): class OpenSSLAssertersMixin(ThroughOpenSSLAPIMixin, AssertersMixin):
def hmac_new(self, key, msg=None, digestmod=None): def hmac_new_by_name(self, key, msg=None, *, hashname):
return _hashlib.hmac_new(key, msg, digestmod=digestmod) self.assertIsInstance(hashname, str)
def hmac_digest(self, key, msg=None, digestmod=None):
return _hashlib.hmac_digest(key, msg, digest=digestmod)
def hmac_new_by_name(self, key, msg=None, hashname=None):
# ignore 'digestmod' and use the exact openssl function
openssl_func = getattr(_hashlib, f"openssl_{hashname}") openssl_func = getattr(_hashlib, f"openssl_{hashname}")
return self.hmac_new(key, msg, digestmod=openssl_func) return self.hmac_new(key, msg, digestmod=openssl_func)
def hmac_digest_by_name(self, key, msg=None, hashname=None): def hmac_digest_by_name(self, key, msg=None, *, hashname):
self.assertIsInstance(hashname, str)
openssl_func = getattr(_hashlib, f"openssl_{hashname}") openssl_func = getattr(_hashlib, f"openssl_{hashname}")
return self.hmac_digest(key, msg, digestmod=openssl_func) return self.hmac_digest(key, msg, digestmod=openssl_func)
class RFCTestCasesMixin(TestVectorsMixin): class HashFunctionsTrait:
"""Test HMAC implementations against test vectors from the RFC. """Trait class for 'hashfunc' in hmac_new() and hmac_digest()."""
Subclasses must override the 'md5' and other 'sha*' attributes
to test the implementations. Their value can be a string, a callable,
or a PEP-257 module.
"""
ALGORITHMS = [ ALGORITHMS = [
'md5', 'sha1', 'md5', 'sha1',
'sha224', 'sha256', 'sha384', 'sha512', 'sha224', 'sha256', 'sha384', 'sha512',
] ]
# Those will be automatically set to non-None on subclasses # By default, a missing algorithm skips the test that uses it.
# as they are set by __init_subclass()__. md5 = sha1 = sha224 = sha256 = sha384 = sha512 = property(
md5 = sha1 = sha224 = sha256 = sha384 = sha512 = None lambda self: self.skipTest("missing hash function")
)
class WithOpenSSLHashFunctions(HashFunctionsTrait):
"""Test a HMAC implementation with an OpenSSL-based callable 'hashfunc'."""
@classmethod
def setUpClass(cls):
super().setUpClass()
for name in cls.ALGORITHMS:
@property
@hashlib_helper.requires_openssl_hashdigest(name)
def func(self, *, __name=name): # __name needed to bind 'name'
return getattr(_hashlib, f'openssl_{__name}')
setattr(cls, name, func)
class WithNamedHashFunctions(HashFunctionsTrait):
"""Test a HMAC implementation with a named 'hashfunc'."""
@classmethod
def setUpClass(cls):
super().setUpClass()
def __init_subclass__(cls, *args, **kwargs):
super().__init_subclass__(*args, **kwargs)
for name in cls.ALGORITHMS: for name in cls.ALGORITHMS:
setattr(cls, name, name) setattr(cls, name, name)
class RFCTestCaseMixin(HashFunctionsTrait):
"""Test HMAC implementations against test vectors from the RFC."""
def test_md5(self): def test_md5(self):
def md5test(key, msg, hexdigest): def md5test(key, msg, hexdigest):
self.assert_hmac(key, msg, hexdigest, self.md5, "md5", 16, 64) self.assert_hmac(key, msg, hexdigest, self.md5, "md5", 16, 64)
@ -412,7 +457,6 @@ class RFCTestCasesMixin(TestVectorsMixin):
self._test_sha2_rfc4231(self.sha512, 'sha512', 64, 128) self._test_sha2_rfc4231(self.sha512, 'sha512', 64, 128)
def _test_sha2_rfc4231(self, hashfunc, hashname, digest_size, block_size): def _test_sha2_rfc4231(self, hashfunc, hashname, digest_size, block_size):
def hmactest(key, data, hexdigests): def hmactest(key, data, hexdigests):
hexdigest = hexdigests[hashname] hexdigest = hexdigests[hashname]
@ -531,57 +575,9 @@ class RFCTestCasesMixin(TestVectorsMixin):
'134676fb6de0446065c97440fa8c6a58', '134676fb6de0446065c97440fa8c6a58',
}) })
@hashlib_helper.requires_hashdigest('sha256')
def test_legacy_block_size_warnings(self):
class MockCrazyHash(object):
"""Ain't no block_size attribute here."""
def __init__(self, *args):
self._x = hashlib.sha256(*args)
self.digest_size = self._x.digest_size
def update(self, v):
self._x.update(v)
def digest(self):
return self._x.digest()
with warnings.catch_warnings(): class PyRFCTestCase(ThroughObjectMixin, PyAssertersMixin,
warnings.simplefilter('error', RuntimeWarning) WithOpenSSLHashFunctions, RFCTestCaseMixin,
with self.assertRaises(RuntimeWarning):
hmac.HMAC(b'a', b'b', digestmod=MockCrazyHash)
self.fail('Expected warning about missing block_size')
MockCrazyHash.block_size = 1
with self.assertRaises(RuntimeWarning):
hmac.HMAC(b'a', b'b', digestmod=MockCrazyHash)
self.fail('Expected warning about small block_size')
def test_with_fallback(self):
cache = getattr(hashlib, '__builtin_constructor_cache')
try:
cache['foo'] = hashlib.sha256
hexdigest = hmac.digest(b'key', b'message', 'foo').hex()
expected = ('6e9ef29b75fffc5b7abae527d58fdadb'
'2fe42e7219011976917343065f58ed4a')
self.assertEqual(hexdigest, expected)
finally:
cache.pop('foo')
class RFCWithOpenSSLHashFunctionTestCasesMixin(RFCTestCasesMixin):
def __init_subclass__(cls, *args, **kwargs):
super().__init_subclass__(*args, **kwargs)
for name in cls.ALGORITHMS:
@property
@hashlib_helper.requires_hashlib()
@hashlib_helper.requires_hashdigest(name, openssl=True)
def func(self, *, __name=name): # __name needed to bind 'name'
return getattr(_hashlib, f'openssl_{__name}')
setattr(cls, name, func)
class PyRFCTestCase(PyTestVectorsMixin, ThroughObjectMixin,
RFCWithOpenSSLHashFunctionTestCasesMixin,
unittest.TestCase): unittest.TestCase):
"""Python implementation of HMAC using hmac.HMAC(). """Python implementation of HMAC using hmac.HMAC().
@ -589,8 +585,8 @@ class PyRFCTestCase(PyTestVectorsMixin, ThroughObjectMixin,
""" """
class PyDotNewRFCTestCase(PyTestVectorsMixin, ThroughModuleAPIMixin, class PyDotNewRFCTestCase(ThroughModuleAPIMixin, PyAssertersMixin,
RFCWithOpenSSLHashFunctionTestCasesMixin, WithOpenSSLHashFunctions, RFCTestCaseMixin,
unittest.TestCase): unittest.TestCase):
"""Python implementation of HMAC using hmac.new(). """Python implementation of HMAC using hmac.new().
@ -598,12 +594,13 @@ class PyDotNewRFCTestCase(PyTestVectorsMixin, ThroughModuleAPIMixin,
""" """
class OpenSSLRFCTestCase(OpenSSLTestVectorsMixin, class OpenSSLRFCTestCase(OpenSSLAssertersMixin,
RFCWithOpenSSLHashFunctionTestCasesMixin, WithOpenSSLHashFunctions, RFCTestCaseMixin,
unittest.TestCase): unittest.TestCase):
"""OpenSSL implementation of HMAC. """OpenSSL implementation of HMAC.
The underlying hash functions are also OpenSSL-based.""" The underlying hash functions are also OpenSSL-based.
"""
# TODO(picnixz): once we have a HACL* HMAC, we should also test the Python # TODO(picnixz): once we have a HACL* HMAC, we should also test the Python
@ -668,7 +665,7 @@ class DigestModTestCaseMixin(CreatorMixin, DigestMixin):
return cases return cases
class ConstructorTestCaseMixin(CreatorMixin, DigestMixin, CheckerMixin): class ConstructorTestCaseMixin(CreatorMixin, DigestMixin, ObjectCheckerMixin):
"""HMAC constructor tests based on HMAC-SHA-2/256.""" """HMAC constructor tests based on HMAC-SHA-2/256."""
key = b"key" key = b"key"
@ -847,7 +844,7 @@ class PySanityTestCase(ThroughObjectMixin, PyModuleMixin, SanityTestCaseMixin,
self.assertStartsWith(repr(h), "<hmac.HMAC object at") self.assertStartsWith(repr(h), "<hmac.HMAC object at")
@hashlib_helper.requires_hashdigest('sha256', openssl=True) @hashlib_helper.requires_openssl_hashdigest('sha256')
class OpenSSLSanityTestCase(ThroughOpenSSLAPIMixin, SanityTestCaseMixin, class OpenSSLSanityTestCase(ThroughOpenSSLAPIMixin, SanityTestCaseMixin,
unittest.TestCase): unittest.TestCase):
@ -899,45 +896,50 @@ class PyUpdateTestCase(UpdateTestCaseMixin, unittest.TestCase):
return self.hmac.HMAC(key, msg, digestmod='sha256') return self.hmac.HMAC(key, msg, digestmod='sha256')
@hashlib_helper.requires_hashlib() @hashlib_helper.requires_openssl_hashdigest('sha256')
@hashlib_helper.requires_hashdigest('sha256', openssl=True)
class OpenSSLUpdateTestCase(UpdateTestCaseMixin, unittest.TestCase): class OpenSSLUpdateTestCase(UpdateTestCaseMixin, unittest.TestCase):
def HMAC(self, key, msg=None): def HMAC(self, key, msg=None):
return hmac.new(key, msg, digestmod='sha256') return hmac.new(key, msg, digestmod='sha256')
@hashlib_helper.requires_hashdigest('sha256') class CopyBaseTestCase:
class CopyTestCase(unittest.TestCase):
def test_attributes_old(self): def test_attributes(self):
raise NotImplementedError
def test_realcopy(self):
raise NotImplementedError
@hashlib_helper.requires_hashdigest('sha256')
class PythonCopyTestCase(CopyBaseTestCase, unittest.TestCase):
def test_attributes(self):
# Testing if attributes are of same type. # Testing if attributes are of same type.
h1 = hmac.HMAC.__new__(hmac.HMAC) h1 = hmac.HMAC.__new__(hmac.HMAC)
h1._init_old(b"key", b"msg", digestmod="sha256") h1._init_old(b"key", b"msg", digestmod="sha256")
h2 = h1.copy()
self.assertEqual(type(h1._inner), type(h2._inner))
self.assertEqual(type(h1._outer), type(h2._outer))
def test_realcopy_old(self):
# Testing if the copy method created a real copy.
h1 = hmac.HMAC.__new__(hmac.HMAC)
h1._init_old(b"key", b"msg", digestmod="sha256")
self.assertIsNone(h1._hmac) self.assertIsNone(h1._hmac)
self.assertIsNotNone(h1._inner)
self.assertIsNotNone(h1._outer)
h2 = h1.copy() h2 = h1.copy()
self.assertIsNone(h2._hmac) self.assertIsNone(h2._hmac)
self.assertIsNotNone(h2._inner)
self.assertIsNotNone(h2._outer)
self.assertEqual(type(h1._inner), type(h2._inner))
self.assertEqual(type(h1._outer), type(h2._outer))
def test_realcopy(self):
# Testing if the copy method created a real copy.
h1 = hmac.HMAC.__new__(hmac.HMAC)
h1._init_old(b"key", b"msg", digestmod="sha256")
h2 = h1.copy()
# Using id() in case somebody has overridden __eq__/__ne__. # Using id() in case somebody has overridden __eq__/__ne__.
self.assertNotEqual(id(h1), id(h2)) self.assertNotEqual(id(h1), id(h2))
self.assertNotEqual(id(h1._inner), id(h2._inner)) self.assertNotEqual(id(h1._inner), id(h2._inner))
self.assertNotEqual(id(h1._outer), id(h2._outer)) self.assertNotEqual(id(h1._outer), id(h2._outer))
@hashlib_helper.requires_hashlib()
def test_realcopy_hmac(self):
h1 = hmac.HMAC.__new__(hmac.HMAC)
h1._init_hmac(b"key", b"msg", digestmod="sha256")
h2 = h1.copy()
self.assertNotEqual(id(h1._hmac), id(h2._hmac))
def test_equality(self): def test_equality(self):
# Testing if the copy has the same digests. # Testing if the copy has the same digests.
h1 = hmac.HMAC(b"key", digestmod="sha256") h1 = hmac.HMAC(b"key", digestmod="sha256")
@ -951,11 +953,47 @@ class CopyTestCase(unittest.TestCase):
h1 = hmac.new(b"key", digestmod="sha256") h1 = hmac.new(b"key", digestmod="sha256")
h1.update(b"some random text") h1.update(b"some random text")
h2 = h1.copy() h2 = h1.copy()
# Using id() in case somebody has overridden __eq__/__ne__.
self.assertNotEqual(id(h1), id(h2)) self.assertNotEqual(id(h1), id(h2))
self.assertEqual(h1.digest(), h2.digest()) self.assertEqual(h1.digest(), h2.digest())
self.assertEqual(h1.hexdigest(), h2.hexdigest()) self.assertEqual(h1.hexdigest(), h2.hexdigest())
class ExtensionCopyTestCase(CopyBaseTestCase):
def init(self, h):
"""Call the dedicate init() method to test."""
raise NotImplementedError
def test_attributes(self):
# Testing if attributes are of same type.
h1 = hmac.HMAC.__new__(hmac.HMAC)
self.init(h1)
self.assertIsNotNone(h1._hmac)
self.assertIsNone(h1._inner)
self.assertIsNone(h1._outer)
h2 = h1.copy()
self.assertIsNotNone(h2._hmac)
self.assertIsNone(h2._inner)
self.assertIsNone(h2._outer)
def test_realcopy(self):
h1 = hmac.HMAC.__new__(hmac.HMAC)
self.init(h1)
h2 = h1.copy()
# Using id() in case somebody has overridden __eq__/__ne__.
self.assertNotEqual(id(h1._hmac), id(h2._hmac))
@hashlib_helper.requires_openssl_hashdigest('sha256')
class OpenSSLCopyTestCase(ExtensionCopyTestCase, unittest.TestCase):
def init(self, h):
h._init_hmac(b"key", b"msg", digestmod="sha256")
class CompareDigestMixin: class CompareDigestMixin:
@staticmethod @staticmethod
@ -1087,5 +1125,44 @@ class OperatorCompareDigestTestCase(CompareDigestMixin, unittest.TestCase):
compare_digest = operator_compare_digest compare_digest = operator_compare_digest
class PyMiscellaneousTests(unittest.TestCase):
"""Miscellaneous tests for the pure Python HMAC module."""
@hashlib_helper.requires_hashdigest('sha256')
def test_legacy_block_size_warnings(self):
class MockCrazyHash(object):
"""Ain't no block_size attribute here."""
def __init__(self, *args):
self._x = hashlib.sha256(*args)
self.digest_size = self._x.digest_size
def update(self, v):
self._x.update(v)
def digest(self):
return self._x.digest()
with warnings.catch_warnings():
warnings.simplefilter('error', RuntimeWarning)
with self.assertRaises(RuntimeWarning):
hmac.HMAC(b'a', b'b', digestmod=MockCrazyHash)
self.fail('Expected warning about missing block_size')
MockCrazyHash.block_size = 1
with self.assertRaises(RuntimeWarning):
hmac.HMAC(b'a', b'b', digestmod=MockCrazyHash)
self.fail('Expected warning about small block_size')
@hashlib_helper.requires_hashdigest('sha256')
def test_with_fallback(self):
cache = getattr(hashlib, '__builtin_constructor_cache')
try:
cache['foo'] = hashlib.sha256
hexdigest = hmac.digest(b'key', b'message', 'foo').hex()
expected = ('6e9ef29b75fffc5b7abae527d58fdadb'
'2fe42e7219011976917343065f58ed4a')
self.assertEqual(hexdigest, expected)
finally:
cache.pop('foo')
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()