mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
gh-132388: test HACL* and OpenSSL hash functions in pure Python HMAC (#134051)
This commit is contained in:
parent
1566c34dc7
commit
73d71a416f
2 changed files with 309 additions and 68 deletions
|
@ -23,6 +23,22 @@ def requires_builtin_hmac():
|
|||
return unittest.skipIf(_hmac is None, "requires _hmac")
|
||||
|
||||
|
||||
def _missing_hash(digestname, implementation=None, *, exc=None):
|
||||
parts = ["missing", implementation, f"hash algorithm: {digestname!r}"]
|
||||
msg = " ".join(filter(None, parts))
|
||||
raise unittest.SkipTest(msg) from exc
|
||||
|
||||
|
||||
def _openssl_availabillity(digestname, *, usedforsecurity):
|
||||
try:
|
||||
_hashlib.new(digestname, usedforsecurity=usedforsecurity)
|
||||
except AttributeError:
|
||||
assert _hashlib is None
|
||||
_missing_hash(digestname, "OpenSSL")
|
||||
except ValueError as exc:
|
||||
_missing_hash(digestname, "OpenSSL", exc=exc)
|
||||
|
||||
|
||||
def _decorate_func_or_class(func_or_class, decorator_func):
|
||||
if not isinstance(func_or_class, type):
|
||||
return decorator_func(func_or_class)
|
||||
|
@ -71,8 +87,7 @@ def requires_hashdigest(digestname, openssl=None, usedforsecurity=True):
|
|||
try:
|
||||
test_availability()
|
||||
except ValueError as exc:
|
||||
msg = f"missing hash algorithm: {digestname!r}"
|
||||
raise unittest.SkipTest(msg) from exc
|
||||
_missing_hash(digestname, exc=exc)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
@ -87,14 +102,10 @@ def requires_openssl_hashdigest(digestname, *, usedforsecurity=True):
|
|||
The hashing algorithm may be missing or blocked by a strict crypto policy.
|
||||
"""
|
||||
def decorator_func(func):
|
||||
@requires_hashlib()
|
||||
@requires_hashlib() # avoid checking at each call
|
||||
@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)
|
||||
_openssl_availabillity(digestname, usedforsecurity=usedforsecurity)
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
@ -103,6 +114,202 @@ def requires_openssl_hashdigest(digestname, *, usedforsecurity=True):
|
|||
return decorator
|
||||
|
||||
|
||||
def find_openssl_hashdigest_constructor(digestname, *, usedforsecurity=True):
|
||||
"""Find the OpenSSL hash function constructor by its name."""
|
||||
assert isinstance(digestname, str), digestname
|
||||
_openssl_availabillity(digestname, usedforsecurity=usedforsecurity)
|
||||
# This returns a function of the form _hashlib.openssl_<name> and
|
||||
# not a lambda function as it is rejected by _hashlib.hmac_new().
|
||||
return getattr(_hashlib, f"openssl_{digestname}")
|
||||
|
||||
|
||||
def requires_builtin_hashdigest(
|
||||
module_name, digestname, *, usedforsecurity=True
|
||||
):
|
||||
"""Decorator raising SkipTest if a HACL* hashing algorithm is missing.
|
||||
|
||||
- The *module_name* is the C extension module name based on HACL*.
|
||||
- The *digestname* is one of its member, e.g., 'md5'.
|
||||
"""
|
||||
def decorator_func(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
module = import_module(module_name)
|
||||
try:
|
||||
getattr(module, digestname)
|
||||
except AttributeError:
|
||||
fullname = f'{module_name}.{digestname}'
|
||||
_missing_hash(fullname, implementation="HACL")
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
def decorator(func_or_class):
|
||||
return _decorate_func_or_class(func_or_class, decorator_func)
|
||||
return decorator
|
||||
|
||||
|
||||
def find_builtin_hashdigest_constructor(
|
||||
module_name, digestname, *, usedforsecurity=True
|
||||
):
|
||||
"""Find the HACL* hash function constructor.
|
||||
|
||||
- The *module_name* is the C extension module name based on HACL*.
|
||||
- The *digestname* is one of its member, e.g., 'md5'.
|
||||
"""
|
||||
module = import_module(module_name)
|
||||
try:
|
||||
constructor = getattr(module, digestname)
|
||||
constructor(b'', usedforsecurity=usedforsecurity)
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
_missing_hash(f'{module_name}.{digestname}', implementation="HACL")
|
||||
return constructor
|
||||
|
||||
|
||||
class HashFunctionsTrait:
|
||||
"""Mixin trait class containing hash functions.
|
||||
|
||||
This class is assumed to have all unitest.TestCase methods but should
|
||||
not directly inherit from it to prevent the test suite being run on it.
|
||||
|
||||
Subclasses should implement the hash functions by returning an object
|
||||
that can be recognized as a valid digestmod parameter for both hashlib
|
||||
and HMAC. In particular, it cannot be a lambda function as it will not
|
||||
be recognized by hashlib (it will still be accepted by the pure Python
|
||||
implementation of HMAC).
|
||||
"""
|
||||
|
||||
ALGORITHMS = [
|
||||
'md5', 'sha1',
|
||||
'sha224', 'sha256', 'sha384', 'sha512',
|
||||
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
|
||||
]
|
||||
|
||||
# Default 'usedforsecurity' to use when looking up a hash function.
|
||||
usedforsecurity = True
|
||||
|
||||
def _find_constructor(self, name):
|
||||
# By default, a missing algorithm skips the test that uses it.
|
||||
self.assertIn(name, self.ALGORITHMS)
|
||||
self.skipTest(f"missing hash function: {name}")
|
||||
|
||||
@property
|
||||
def md5(self):
|
||||
return self._find_constructor("md5")
|
||||
|
||||
@property
|
||||
def sha1(self):
|
||||
return self._find_constructor("sha1")
|
||||
|
||||
@property
|
||||
def sha224(self):
|
||||
return self._find_constructor("sha224")
|
||||
|
||||
@property
|
||||
def sha256(self):
|
||||
return self._find_constructor("sha256")
|
||||
|
||||
@property
|
||||
def sha384(self):
|
||||
return self._find_constructor("sha384")
|
||||
|
||||
@property
|
||||
def sha512(self):
|
||||
return self._find_constructor("sha512")
|
||||
|
||||
@property
|
||||
def sha3_224(self):
|
||||
return self._find_constructor("sha3_224")
|
||||
|
||||
@property
|
||||
def sha3_256(self):
|
||||
return self._find_constructor("sha3_256")
|
||||
|
||||
@property
|
||||
def sha3_384(self):
|
||||
return self._find_constructor("sha3_384")
|
||||
|
||||
@property
|
||||
def sha3_512(self):
|
||||
return self._find_constructor("sha3_512")
|
||||
|
||||
|
||||
class NamedHashFunctionsTrait(HashFunctionsTrait):
|
||||
"""Trait containing named hash functions.
|
||||
|
||||
Hash functions are available if and only if they are available in hashlib.
|
||||
"""
|
||||
|
||||
def _find_constructor(self, name):
|
||||
self.assertIn(name, self.ALGORITHMS)
|
||||
return name
|
||||
|
||||
|
||||
class OpenSSLHashFunctionsTrait(HashFunctionsTrait):
|
||||
"""Trait containing OpenSSL hash functions.
|
||||
|
||||
Hash functions are available if and only if they are available in _hashlib.
|
||||
"""
|
||||
|
||||
def _find_constructor(self, name):
|
||||
self.assertIn(name, self.ALGORITHMS)
|
||||
return find_openssl_hashdigest_constructor(
|
||||
name, usedforsecurity=self.usedforsecurity
|
||||
)
|
||||
|
||||
|
||||
class BuiltinHashFunctionsTrait(HashFunctionsTrait):
|
||||
"""Trait containing HACL* hash functions.
|
||||
|
||||
Hash functions are available if and only if they are available in C.
|
||||
In particular, HACL* HMAC-MD5 may be available even though HACL* md5
|
||||
is not since the former is unconditionally built.
|
||||
"""
|
||||
|
||||
def _find_constructor_in(self, module, name):
|
||||
self.assertIn(name, self.ALGORITHMS)
|
||||
return find_builtin_hashdigest_constructor(module, name)
|
||||
|
||||
@property
|
||||
def md5(self):
|
||||
return self._find_constructor_in("_md5", "md5")
|
||||
|
||||
@property
|
||||
def sha1(self):
|
||||
return self._find_constructor_in("_sha1", "sha1")
|
||||
|
||||
@property
|
||||
def sha224(self):
|
||||
return self._find_constructor_in("_sha2", "sha224")
|
||||
|
||||
@property
|
||||
def sha256(self):
|
||||
return self._find_constructor_in("_sha2", "sha256")
|
||||
|
||||
@property
|
||||
def sha384(self):
|
||||
return self._find_constructor_in("_sha2", "sha384")
|
||||
|
||||
@property
|
||||
def sha512(self):
|
||||
return self._find_constructor_in("_sha2", "sha512")
|
||||
|
||||
@property
|
||||
def sha3_224(self):
|
||||
return self._find_constructor_in("_sha3", "sha3_224")
|
||||
|
||||
@property
|
||||
def sha3_256(self):
|
||||
return self._find_constructor_in("_sha3","sha3_256")
|
||||
|
||||
@property
|
||||
def sha3_384(self):
|
||||
return self._find_constructor_in("_sha3","sha3_384")
|
||||
|
||||
@property
|
||||
def sha3_512(self):
|
||||
return self._find_constructor_in("_sha3","sha3_512")
|
||||
|
||||
|
||||
def find_gil_minsize(modules_names, default=2048):
|
||||
"""Get the largest GIL_MINSIZE value for the given cryptographic modules.
|
||||
|
||||
|
|
|
@ -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,
|
||||
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,
|
||||
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()."""
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue