From 67fce6d90404f3544ef1ae4ddde7ef6c15253303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 11 Mar 2025 13:14:57 +0100 Subject: [PATCH 01/12] add more `hashlib` test helpers - add `requires_builtin_hmac` to check if built-in HMAC is available - refactor `requires_hashdigest` in prevision of a future `requires_builtin_hashdigest` for built-in hashes only --- Lib/test/support/hashlib_helper.py | 73 ++++++++++++++++++------------ 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 477e0f110eabba..261e0d94dbbea4 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -12,44 +12,59 @@ def requires_hashlib(): return unittest.skipIf(_hashlib is None, "requires _hashlib") +def _decorate_func_or_class(func_or_class, decorator_func): + if not isinstance(func_or_class, type): + return decorator_func(func_or_class) + + decorated_class = func_or_class + 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 + """Decorator raising SkipTest if a hashing algorithm is not available. - The hashing algorithm could be missing or blocked by a strict crypto - policy. + 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 - the algorithm. Otherwise the check falls back to built-in - implementations. The usedforsecurity flag is passed to the constructor. + the algorithm. Otherwise the check falls back to (optional) built-in + HACL* implementations. + + The usedforsecurity flag is passed to the constructor but has no effect + for HACL* implementations. + Examples of exceptions being suppressed: ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS 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) + if openssl and _hashlib is not None: + def test_availability(): + _hashlib.new(digestname, usedforsecurity=usedforsecurity) + else: + def test_availability(): + hashlib.new(digestname, usedforsecurity=usedforsecurity) + + def decorator_func(func): + @functools.wraps(func) def wrapper(*args, **kwargs): try: - if openssl and _hashlib is not None: - _hashlib.new(digestname, usedforsecurity=usedforsecurity) - else: - hashlib.new(digestname, usedforsecurity=usedforsecurity) - except ValueError: - raise unittest.SkipTest( - f"hash digest {digestname!r} is not available." - ) - return func_or_class(*args, **kwargs) + 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 + + def decorator(func_or_class): + return _decorate_func_or_class(func_or_class, decorator_func) return decorator From 4feec61a2aaa8af01a7f103258e5727210b8e6ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 11 Mar 2025 13:16:37 +0100 Subject: [PATCH 02/12] move some tests --- Lib/test/test_hmac.py | 73 +++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index c8c806dc1baf8b..52fcac762f4e3d 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -531,40 +531,6 @@ def hmactest(key, data, hexdigests): '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(): - 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') - - 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): @@ -1087,5 +1053,44 @@ class OperatorCompareDigestTestCase(CompareDigestMixin, unittest.TestCase): 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__": unittest.main() From c125411a90b4b7dba4554a592094be6a9fa107aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 11 Mar 2025 13:20:48 +0100 Subject: [PATCH 03/12] update comments - update comments for `CreatorMixin.hmac_new()` - update comments for `DigestMixin.hmac_digest()` - update comments for `TestVectorsMixin` - update comments for `TestVectorsMixin.hmac_new_by_name()` - update comments for `TestVectorsMixin.hmac_digest_by_name()` --- Lib/test/test_hmac.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 52fcac762f4e3d..0ab0a12f150051 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -61,7 +61,11 @@ class CreatorMixin: """Mixin exposing a method creating a HMAC object.""" 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 def bind_hmac_new(self, digestmod): @@ -73,7 +77,11 @@ class DigestMixin: """Mixin exposing a method computing a HMAC digest.""" 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 def bind_hmac_digest(self, digestmod): @@ -142,7 +150,7 @@ def check_hexdigest(self, h, hexdigest, digest_size): class TestVectorsMixin(CreatorMixin, DigestMixin, CheckerMixin): - """Mixin class for all test vectors test cases.""" + """Mixin class for common tests.""" def hmac_new_by_name(self, key, msg=None, hashname=None): """Alternative implementation of hmac_new(). @@ -152,6 +160,10 @@ def hmac_new_by_name(self, key, msg=None, hashname=None): by their name (all HMAC implementations must at least recognize hash functions by their names but some may use aliases such as `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) return self.hmac_new(key, msg, digestmod=hashname) @@ -159,6 +171,12 @@ def hmac_new_by_name(self, key, msg=None, hashname=None): def hmac_digest_by_name(self, key, msg=None, hashname=None): """Alternative implementation of hmac_digest().""" self.assertIsInstance(hashname, str | None) + """Alternative implementation of hmac_digest(). + + 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. + """ return self.hmac_digest(key, msg, digestmod=hashname) def assert_hmac( From 56475d7a182c5c6c2a1d72e50a1f6fa766902193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 11 Mar 2025 13:33:07 +0100 Subject: [PATCH 04/12] update how algorithms are discovered for RFC test cases --- Lib/test/test_hmac.py | 74 ++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 0ab0a12f150051..c3289adf3fe6ba 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -329,28 +329,50 @@ def hmac_digest_by_name(self, key, msg=None, hashname=None): return self.hmac_digest(key, msg, digestmod=openssl_func) -class RFCTestCasesMixin(TestVectorsMixin): - """Test HMAC implementations against test vectors from the RFC. - - 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. - """ +class HashFunctionsTrait: + """Trait class for 'hashfunc' in hmac_new() and hmac_digest().""" ALGORITHMS = [ 'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512', ] - # Those will be automatically set to non-None on subclasses - # as they are set by __init_subclass()__. - md5 = sha1 = sha224 = sha256 = sha384 = sha512 = None + # By default, a missing algorithm skips the test that uses it. + md5 = sha1 = sha224 = sha256 = sha384 = sha512 = property( + 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_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 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: setattr(cls, name, name) + +class RFCTestCasesMixin(HashFunctionsTrait): + """Test HMAC implementations against test vectors from the RFC.""" + def test_md5(self): def md5test(key, msg, hexdigest): self.assert_hmac(key, msg, hexdigest, self.md5, "md5", 16, 64) @@ -550,22 +572,8 @@ def hmactest(key, data, hexdigests): }) -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, +class PyRFCTestCase(ThroughObjectMixin, PyTestVectorsMixin, + WithOpenSSLHashFunctions, RFCTestCasesMixin, unittest.TestCase): """Python implementation of HMAC using hmac.HMAC(). @@ -573,8 +581,8 @@ class PyRFCTestCase(PyTestVectorsMixin, ThroughObjectMixin, """ -class PyDotNewRFCTestCase(PyTestVectorsMixin, ThroughModuleAPIMixin, - RFCWithOpenSSLHashFunctionTestCasesMixin, +class PyDotNewRFCTestCase(ThroughModuleAPIMixin, PyTestVectorsMixin, + WithOpenSSLHashFunctions, RFCTestCasesMixin, unittest.TestCase): """Python implementation of HMAC using hmac.new(). @@ -583,11 +591,13 @@ class PyDotNewRFCTestCase(PyTestVectorsMixin, ThroughModuleAPIMixin, class OpenSSLRFCTestCase(OpenSSLTestVectorsMixin, - RFCWithOpenSSLHashFunctionTestCasesMixin, + WithOpenSSLHashFunctions, RFCTestCasesMixin, unittest.TestCase): """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 From 236828deeb5343f3e0ba665b8da4012ae7f5c8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 11 Mar 2025 13:38:29 +0100 Subject: [PATCH 05/12] strengthen contract on `hmac_new_by_name` and `hmac_digest_by_name` --- Lib/test/test_hmac.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index c3289adf3fe6ba..60b9b6353721af 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -152,7 +152,7 @@ def check_hexdigest(self, h, hexdigest, digest_size): class TestVectorsMixin(CreatorMixin, DigestMixin, CheckerMixin): """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(). This is typically useful when one needs to test against an HMAC @@ -165,18 +165,17 @@ def hmac_new_by_name(self, key, msg=None, hashname=None): 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) - def hmac_digest_by_name(self, key, msg=None, hashname=None): - """Alternative implementation of hmac_digest().""" - self.assertIsInstance(hashname, str | None) + def hmac_digest_by_name(self, key, msg=None, *, hashname): """Alternative implementation of hmac_digest(). 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) def assert_hmac( @@ -311,20 +310,15 @@ def assert_hmac_extra_cases( self.check_object(h, hexdigest, hashname, digest_size, block_size) -class OpenSSLTestVectorsMixin(TestVectorsMixin): - - def hmac_new(self, key, msg=None, digestmod=None): - return _hashlib.hmac_new(key, msg, digestmod=digestmod) - - def hmac_digest(self, key, msg=None, digestmod=None): - return _hashlib.hmac_digest(key, msg, digest=digestmod) +class OpenSSLTestVectorsMixin(ThroughOpenSSLAPIMixin, TestVectorsMixin): - def hmac_new_by_name(self, key, msg=None, hashname=None): - # ignore 'digestmod' and use the exact openssl function + def hmac_new_by_name(self, key, msg=None, *, hashname): + self.assertIsInstance(hashname, str) openssl_func = getattr(_hashlib, f"openssl_{hashname}") 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}") return self.hmac_digest(key, msg, digestmod=openssl_func) From 7f61b1bfb60f17fde71b9f62c3289c8afa01c9a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 11 Mar 2025 13:41:11 +0100 Subject: [PATCH 06/12] refactor hexdigest tests for RFC test vectors * `assert_hmac_hexdigest_by_new` -> `assert_hmac_hexdigest_by_name` * use a single function for checking the hexdigest. --- Lib/test/test_hmac.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 60b9b6353721af..8c5e033a5ffdc4 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -213,7 +213,7 @@ def assert_hmac( self.assert_hmac_new_by_name( 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 ) @@ -272,16 +272,28 @@ def assert_hmac_hexdigest( self, key, msg, hexdigest, digestmod, digest_size, ): """Check a HMAC digest computed by hmac_digest().""" - d = self.hmac_digest(key, msg, digestmod=digestmod) - self.assertEqual(len(d), digest_size) - self.assertEqual(d, binascii.unhexlify(hexdigest)) + self._check_hmac_hexdigest( + key, msg, hexdigest, digest_size, + 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 ): """Check a HMAC digest computed by hmac_digest_by_name().""" - self.assertIsInstance(hashname, str | None) - d = self.hmac_digest_by_name(key, msg, hashname=hashname) + self.assertIsInstance(hashname, str) + 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(d, binascii.unhexlify(hexdigest)) From bc48eac0612cd8ff75f4aca4c9778691ba913e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 11 Mar 2025 13:17:49 +0100 Subject: [PATCH 07/12] split hmac.copy() test by implementation --- Lib/test/test_hmac.py | 68 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 8c5e033a5ffdc4..ac16d0ba596a6d 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -907,37 +907,42 @@ def HMAC(self, key, msg=None): return hmac.new(key, msg, digestmod='sha256') +class CopyBaseTestCase: + + def test_attributes(self): + raise NotImplementedError + + def test_realcopy(self): + raise NotImplementedError + + @hashlib_helper.requires_hashdigest('sha256') -class CopyTestCase(unittest.TestCase): +class PythonCopyTestCase(CopyBaseTestCase, unittest.TestCase): - def test_attributes_old(self): + def test_attributes(self): # Testing if attributes are of same type. h1 = hmac.HMAC.__new__(hmac.HMAC) h1._init_old(b"key", b"msg", digestmod="sha256") + self.assertIsNone(h1._hmac) + self.assertIsNotNone(h1._inner) + self.assertIsNotNone(h1._outer) + h2 = h1.copy() + 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_old(self): + 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") - self.assertIsNone(h1._hmac) - h2 = h1.copy() - self.assertIsNone(h2._hmac) # Using id() in case somebody has overridden __eq__/__ne__. self.assertNotEqual(id(h1), id(h2)) self.assertNotEqual(id(h1._inner), id(h2._inner)) 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): # Testing if the copy has the same digests. h1 = hmac.HMAC(b"key", digestmod="sha256") @@ -951,11 +956,48 @@ def test_equality_new(self): h1 = hmac.new(b"key", digestmod="sha256") h1.update(b"some random text") h2 = h1.copy() + # Using id() in case somebody has overridden __eq__/__ne__. self.assertNotEqual(id(h1), id(h2)) self.assertEqual(h1.digest(), h2.digest()) 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.assertNotHasAttr(h1, '_inner') + self.assertNotHasAttr(h1, '_outer') + + h2 = h1.copy() + self.assertIsNotNone(h2._hmac) + self.assertNotHasAttr(h2, '_inner') + self.assertNotHasAttr(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_hashlib() +@hashlib_helper.requires_hashdigest('sha256', openssl=True) +class OpenSSLCopyTestCase(ExtensionCopyTestCase, unittest.TestCase): + + def init(self, h): + h._init_openssl_hmac(b"key", b"msg", digestmod="sha256") + + class CompareDigestMixin: @staticmethod From 8c883605967b9a531624442fc624efe4ed30d2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 11 Mar 2025 13:53:02 +0100 Subject: [PATCH 08/12] rename mixin classes to better match their responsibility --- Lib/test/test_hmac.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index ac16d0ba596a6d..3167d01519701f 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -128,7 +128,7 @@ def hmac_digest(self, key, msg=None, digestmod=None): return _hashlib.hmac_digest(key, msg, digest=digestmod) -class CheckerMixin: +class ObjectCheckerMixin: """Mixin for checking HMAC objects (pure Python, OpenSSL or built-in).""" def check_object(self, h, hexdigest, hashname, digest_size, block_size): @@ -149,7 +149,7 @@ def check_hexdigest(self, h, hexdigest, digest_size): self.assertEqual(h.hexdigest().upper(), hexdigest.upper()) -class TestVectorsMixin(CreatorMixin, DigestMixin, CheckerMixin): +class AssertersMixin(CreatorMixin, DigestMixin, ObjectCheckerMixin): """Mixin class for common tests.""" def hmac_new_by_name(self, key, msg=None, *, hashname): @@ -308,7 +308,7 @@ def assert_hmac_extra_cases( self.check_object(h1, hexdigest, hashname, digest_size, block_size) -class PyTestVectorsMixin(PyModuleMixin, TestVectorsMixin): +class PyAssertersMixin(PyModuleMixin, AssertersMixin): def assert_hmac_extra_cases( self, key, msg, hexdigest, digestmod, hashname, digest_size, block_size @@ -322,7 +322,7 @@ def assert_hmac_extra_cases( self.check_object(h, hexdigest, hashname, digest_size, block_size) -class OpenSSLTestVectorsMixin(ThroughOpenSSLAPIMixin, TestVectorsMixin): +class OpenSSLAssertersMixin(ThroughOpenSSLAPIMixin, AssertersMixin): def hmac_new_by_name(self, key, msg=None, *, hashname): self.assertIsInstance(hashname, str) @@ -376,7 +376,7 @@ def setUpClass(cls): setattr(cls, name, name) -class RFCTestCasesMixin(HashFunctionsTrait): +class RFCTestCaseMixin(HashFunctionsTrait): """Test HMAC implementations against test vectors from the RFC.""" def test_md5(self): @@ -458,7 +458,6 @@ def test_sha2_512_rfc4231(self): self._test_sha2_rfc4231(self.sha512, 'sha512', 64, 128) def _test_sha2_rfc4231(self, hashfunc, hashname, digest_size, block_size): - def hmactest(key, data, hexdigests): hexdigest = hexdigests[hashname] @@ -578,8 +577,8 @@ def hmactest(key, data, hexdigests): }) -class PyRFCTestCase(ThroughObjectMixin, PyTestVectorsMixin, - WithOpenSSLHashFunctions, RFCTestCasesMixin, +class PyRFCTestCase(ThroughObjectMixin, PyAssertersMixin, + WithOpenSSLHashFunctions, RFCTestCaseMixin, unittest.TestCase): """Python implementation of HMAC using hmac.HMAC(). @@ -587,8 +586,8 @@ class PyRFCTestCase(ThroughObjectMixin, PyTestVectorsMixin, """ -class PyDotNewRFCTestCase(ThroughModuleAPIMixin, PyTestVectorsMixin, - WithOpenSSLHashFunctions, RFCTestCasesMixin, +class PyDotNewRFCTestCase(ThroughModuleAPIMixin, PyAssertersMixin, + WithOpenSSLHashFunctions, RFCTestCaseMixin, unittest.TestCase): """Python implementation of HMAC using hmac.new(). @@ -596,8 +595,8 @@ class PyDotNewRFCTestCase(ThroughModuleAPIMixin, PyTestVectorsMixin, """ -class OpenSSLRFCTestCase(OpenSSLTestVectorsMixin, - WithOpenSSLHashFunctions, RFCTestCasesMixin, +class OpenSSLRFCTestCase(OpenSSLAssertersMixin, + WithOpenSSLHashFunctions, RFCTestCaseMixin, unittest.TestCase): """OpenSSL implementation of HMAC. @@ -605,7 +604,6 @@ class OpenSSLRFCTestCase(OpenSSLTestVectorsMixin, """ - # TODO(picnixz): once we have a HACL* HMAC, we should also test the Python # implementation of HMAC with a HACL*-based hash function. For now, we only # test it partially via the '_sha2' module, but for completeness we could @@ -668,7 +666,7 @@ def _invalid_digestmod_cases(self, func, key, msg, choices): return cases -class ConstructorTestCaseMixin(CreatorMixin, DigestMixin, CheckerMixin): +class ConstructorTestCaseMixin(CreatorMixin, DigestMixin, ObjectCheckerMixin): """HMAC constructor tests based on HMAC-SHA-2/256.""" key = b"key" From a608ff837ff4c526f03a3c66cda010cf2e3bb666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:54:18 +0100 Subject: [PATCH 09/12] simplify how OpenSSL hash digests are requested --- Lib/test/support/hashlib_helper.py | 23 +++++++++++++++++++++++ Lib/test/test_hmac.py | 11 ++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 261e0d94dbbea4..15fb2cd0029375 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -1,6 +1,7 @@ import functools import hashlib import unittest +from test.support.import_helper import import_module try: import _hashlib @@ -68,3 +69,25 @@ def wrapper(*args, **kwargs): 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 diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 3167d01519701f..886f0c2ec48d7f 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -358,8 +358,7 @@ def setUpClass(cls): for name in cls.ALGORITHMS: @property - @hashlib_helper.requires_hashlib() - @hashlib_helper.requires_hashdigest(name, openssl=True) + @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) @@ -845,7 +844,7 @@ def test_repr(self): self.assertStartsWith(repr(h), " Date: Sun, 16 Mar 2025 12:05:51 +0100 Subject: [PATCH 10/12] fix tests --- Lib/hmac.py | 1 + Lib/test/test_hmac.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/hmac.py b/Lib/hmac.py index 8b4eb2fe741e60..30b6b478734dfc 100644 --- a/Lib/hmac.py +++ b/Lib/hmac.py @@ -65,6 +65,7 @@ def __init__(self, key, msg=None, digestmod=''): def _init_hmac(self, key, msg, 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.block_size = self._hmac.block_size diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 886f0c2ec48d7f..0d3888bf1ba76a 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -970,13 +970,13 @@ def test_attributes(self): self.init(h1) self.assertIsNotNone(h1._hmac) - self.assertNotHasAttr(h1, '_inner') - self.assertNotHasAttr(h1, '_outer') + self.assertIsNone(h1._inner) + self.assertIsNone(h1._outer) h2 = h1.copy() self.assertIsNotNone(h2._hmac) - self.assertNotHasAttr(h2, '_inner') - self.assertNotHasAttr(h2, '_outer') + self.assertIsNone(h2._inner) + self.assertIsNone(h2._outer) def test_realcopy(self): h1 = hmac.HMAC.__new__(hmac.HMAC) @@ -990,7 +990,7 @@ def test_realcopy(self): class OpenSSLCopyTestCase(ExtensionCopyTestCase, unittest.TestCase): def init(self, h): - h._init_openssl_hmac(b"key", b"msg", digestmod="sha256") + h._init_hmac(b"key", b"msg", digestmod="sha256") class CompareDigestMixin: From d07eef69a3f1048d2e7fd90c7dc7ba806ccfaf9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:58:18 +0100 Subject: [PATCH 11/12] increase test coverage --- Lib/test/test_hmac.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_hmac.py b/Lib/test/test_hmac.py index 0d3888bf1ba76a..ed96af2b2d874b 100644 --- a/Lib/test/test_hmac.py +++ b/Lib/test/test_hmac.py @@ -924,6 +924,7 @@ def test_attributes(self): self.assertIsNotNone(h1._outer) h2 = h1.copy() + self.assertIsNone(h2._hmac) self.assertIsNotNone(h2._inner) self.assertIsNotNone(h2._outer) self.assertEqual(type(h1._inner), type(h2._inner)) From acf7aba976cd6c8ca64d9053ce81ba37482fc924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 16 Mar 2025 18:26:08 +0100 Subject: [PATCH 12/12] fix typo Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> --- Lib/test/support/hashlib_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/support/hashlib_helper.py b/Lib/test/support/hashlib_helper.py index 15fb2cd0029375..bed3d696cb384d 100644 --- a/Lib/test/support/hashlib_helper.py +++ b/Lib/test/support/hashlib_helper.py @@ -42,7 +42,7 @@ def requires_hashdigest(digestname, openssl=None, usedforsecurity=True): HACL* implementations. The usedforsecurity flag is passed to the constructor but has no effect - for HACL* implementations. + on HACL* implementations. Examples of exceptions being suppressed: ValueError: [digital envelope routines: EVP_DigestInit_ex] disabled for FIPS