gh-132388: test HACL* and OpenSSL hash functions in pure Python HMAC (#134051)

This commit is contained in:
Bénédikt Tran 2025-05-16 14:00:01 +02:00 committed by GitHub
parent 1566c34dc7
commit 73d71a416f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 309 additions and 68 deletions

View file

@ -1,8 +1,27 @@
"""Test suite for HMAC.
Python provides three different implementations of HMAC:
- OpenSSL HMAC using OpenSSL hash functions.
- HACL* HMAC using HACL* hash functions.
- Generic Python HMAC using user-defined hash functions.
The generic Python HMAC implementation is able to use OpenSSL
callables or names, HACL* named hash functions or arbitrary
objects implementing PEP 247 interface.
In the two first cases, Python HMAC wraps a C HMAC object (either OpenSSL
or HACL*-based). As a last resort, HMAC is re-implemented in pure Python.
It is however interesting to test the pure Python implementation against
the OpenSSL and HACL* hash functions.
"""
import binascii
import functools
import hmac
import hashlib
import random
import test.support
import test.support.hashlib_helper as hashlib_helper
import types
import unittest
@ -10,6 +29,12 @@ import unittest.mock as mock
import warnings
from _operator import _compare_digest as operator_compare_digest
from test.support import check_disallow_instantiation
from test.support.hashlib_helper import (
BuiltinHashFunctionsTrait,
HashFunctionsTrait,
NamedHashFunctionsTrait,
OpenSSLHashFunctionsTrait,
)
from test.support.import_helper import import_fresh_module, import_module
try:
@ -382,50 +407,7 @@ class BuiltinAssertersMixin(ThroughBuiltinAPIMixin, AssertersMixin):
pass
class HashFunctionsTrait:
"""Trait class for 'hashfunc' in hmac_new() and hmac_digest()."""
ALGORITHMS = [
'md5', 'sha1',
'sha224', 'sha256', 'sha384', 'sha512',
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
]
# By default, a missing algorithm skips the test that uses it.
_ = property(lambda self: self.skipTest("missing hash function"))
md5 = sha1 = _
sha224 = sha256 = sha384 = sha512 = _
sha3_224 = sha3_256 = sha3_384 = sha3_512 = _
del _
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()
for name in cls.ALGORITHMS:
setattr(cls, name, name)
class RFCTestCaseMixin(AssertersMixin, HashFunctionsTrait):
class RFCTestCaseMixin(HashFunctionsTrait, AssertersMixin):
"""Test HMAC implementations against RFC 2202/4231 and NIST test vectors.
- Test vectors for MD5 and SHA-1 are taken from RFC 2202.
@ -739,26 +721,83 @@ class RFCTestCaseMixin(AssertersMixin, HashFunctionsTrait):
)
class PyRFCTestCase(ThroughObjectMixin, PyAssertersMixin,
WithOpenSSLHashFunctions, RFCTestCaseMixin,
unittest.TestCase):
class PurePythonInitHMAC(PyModuleMixin, HashFunctionsTrait):
@classmethod
def setUpClass(cls):
super().setUpClass()
for meth in ['_init_openssl_hmac', '_init_builtin_hmac']:
fn = getattr(cls.hmac.HMAC, meth)
cm = mock.patch.object(cls.hmac.HMAC, meth, autospec=True, wraps=fn)
cls.enterClassContext(cm)
@classmethod
def tearDownClass(cls):
cls.hmac.HMAC._init_openssl_hmac.assert_not_called()
cls.hmac.HMAC._init_builtin_hmac.assert_not_called()
# Do not assert that HMAC._init_old() has been called as it's tricky
# to determine whether a test for a specific hash function has been
# executed or not. On regular builds, it will be called but if a
# hash function is not available, it's hard to detect for which
# test we should checj HMAC._init_old() or not.
super().tearDownClass()
class PyRFCOpenSSLTestCase(ThroughObjectMixin,
PyAssertersMixin,
OpenSSLHashFunctionsTrait,
RFCTestCaseMixin,
PurePythonInitHMAC,
unittest.TestCase):
"""Python implementation of HMAC using hmac.HMAC().
The underlying hash functions are OpenSSL-based.
The underlying hash functions are OpenSSL-based but
_init_old() is used instead of _init_openssl_hmac().
"""
class PyDotNewRFCTestCase(ThroughModuleAPIMixin, PyAssertersMixin,
WithOpenSSLHashFunctions, RFCTestCaseMixin,
unittest.TestCase):
class PyRFCBuiltinTestCase(ThroughObjectMixin,
PyAssertersMixin,
BuiltinHashFunctionsTrait,
RFCTestCaseMixin,
PurePythonInitHMAC,
unittest.TestCase):
"""Python implementation of HMAC using hmac.HMAC().
The underlying hash functions are HACL*-based but
_init_old() is used instead of _init_builtin_hmac().
"""
class PyDotNewOpenSSLRFCTestCase(ThroughModuleAPIMixin,
PyAssertersMixin,
OpenSSLHashFunctionsTrait,
RFCTestCaseMixin,
PurePythonInitHMAC,
unittest.TestCase):
"""Python implementation of HMAC using hmac.new().
The underlying hash functions are OpenSSL-based.
The underlying hash functions are OpenSSL-based but
_init_old() is used instead of _init_openssl_hmac().
"""
class PyDotNewBuiltinRFCTestCase(ThroughModuleAPIMixin,
PyAssertersMixin,
BuiltinHashFunctionsTrait,
RFCTestCaseMixin,
PurePythonInitHMAC,
unittest.TestCase):
"""Python implementation of HMAC using hmac.new().
The underlying hash functions are HACL-based but
_init_old() is used instead of _init_openssl_hmac().
"""
class OpenSSLRFCTestCase(OpenSSLAssertersMixin,
WithOpenSSLHashFunctions, RFCTestCaseMixin,
OpenSSLHashFunctionsTrait,
RFCTestCaseMixin,
unittest.TestCase):
"""OpenSSL implementation of HMAC.
@ -767,7 +806,8 @@ class OpenSSLRFCTestCase(OpenSSLAssertersMixin,
class BuiltinRFCTestCase(BuiltinAssertersMixin,
WithNamedHashFunctions, RFCTestCaseMixin,
NamedHashFunctionsTrait,
RFCTestCaseMixin,
unittest.TestCase):
"""Built-in HACL* implementation of HMAC.
@ -784,12 +824,6 @@ class BuiltinRFCTestCase(BuiltinAssertersMixin,
self.check_hmac_hexdigest(key, msg, hexdigest, digest_size, func)
# 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
# also test the RFC test vectors against all possible implementations.
class DigestModTestCaseMixin(CreatorMixin, DigestMixin):
"""Tests for the 'digestmod' parameter for hmac_new() and hmac_digest()."""