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")
|
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):
|
def _decorate_func_or_class(func_or_class, decorator_func):
|
||||||
if not isinstance(func_or_class, type):
|
if not isinstance(func_or_class, type):
|
||||||
return decorator_func(func_or_class)
|
return decorator_func(func_or_class)
|
||||||
|
@ -71,8 +87,7 @@ def requires_hashdigest(digestname, openssl=None, usedforsecurity=True):
|
||||||
try:
|
try:
|
||||||
test_availability()
|
test_availability()
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
msg = f"missing hash algorithm: {digestname!r}"
|
_missing_hash(digestname, exc=exc)
|
||||||
raise unittest.SkipTest(msg) from exc
|
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return wrapper
|
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.
|
The hashing algorithm may be missing or blocked by a strict crypto policy.
|
||||||
"""
|
"""
|
||||||
def decorator_func(func):
|
def decorator_func(func):
|
||||||
@requires_hashlib()
|
@requires_hashlib() # avoid checking at each call
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
try:
|
_openssl_availabillity(digestname, usedforsecurity=usedforsecurity)
|
||||||
_hashlib.new(digestname, usedforsecurity=usedforsecurity)
|
|
||||||
except ValueError:
|
|
||||||
msg = f"missing OpenSSL hash algorithm: {digestname!r}"
|
|
||||||
raise unittest.SkipTest(msg)
|
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
@ -103,6 +114,202 @@ def requires_openssl_hashdigest(digestname, *, usedforsecurity=True):
|
||||||
return decorator
|
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):
|
def find_gil_minsize(modules_names, default=2048):
|
||||||
"""Get the largest GIL_MINSIZE value for the given cryptographic modules.
|
"""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 binascii
|
||||||
import functools
|
import functools
|
||||||
import hmac
|
import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
import random
|
import random
|
||||||
|
import test.support
|
||||||
import test.support.hashlib_helper as hashlib_helper
|
import test.support.hashlib_helper as hashlib_helper
|
||||||
import types
|
import types
|
||||||
import unittest
|
import unittest
|
||||||
|
@ -10,6 +29,12 @@ import unittest.mock as mock
|
||||||
import warnings
|
import warnings
|
||||||
from _operator import _compare_digest as operator_compare_digest
|
from _operator import _compare_digest as operator_compare_digest
|
||||||
from test.support import check_disallow_instantiation
|
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
|
from test.support.import_helper import import_fresh_module, import_module
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -382,50 +407,7 @@ class BuiltinAssertersMixin(ThroughBuiltinAPIMixin, AssertersMixin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HashFunctionsTrait:
|
class RFCTestCaseMixin(HashFunctionsTrait, AssertersMixin):
|
||||||
"""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):
|
|
||||||
"""Test HMAC implementations against RFC 2202/4231 and NIST test vectors.
|
"""Test HMAC implementations against RFC 2202/4231 and NIST test vectors.
|
||||||
|
|
||||||
- Test vectors for MD5 and SHA-1 are taken from RFC 2202.
|
- Test vectors for MD5 and SHA-1 are taken from RFC 2202.
|
||||||
|
@ -739,26 +721,83 @@ class RFCTestCaseMixin(AssertersMixin, HashFunctionsTrait):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PyRFCTestCase(ThroughObjectMixin, PyAssertersMixin,
|
class PurePythonInitHMAC(PyModuleMixin, HashFunctionsTrait):
|
||||||
WithOpenSSLHashFunctions, RFCTestCaseMixin,
|
|
||||||
unittest.TestCase):
|
@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().
|
"""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,
|
class PyRFCBuiltinTestCase(ThroughObjectMixin,
|
||||||
WithOpenSSLHashFunctions, RFCTestCaseMixin,
|
PyAssertersMixin,
|
||||||
unittest.TestCase):
|
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().
|
"""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,
|
class OpenSSLRFCTestCase(OpenSSLAssertersMixin,
|
||||||
WithOpenSSLHashFunctions, RFCTestCaseMixin,
|
OpenSSLHashFunctionsTrait,
|
||||||
|
RFCTestCaseMixin,
|
||||||
unittest.TestCase):
|
unittest.TestCase):
|
||||||
"""OpenSSL implementation of HMAC.
|
"""OpenSSL implementation of HMAC.
|
||||||
|
|
||||||
|
@ -767,7 +806,8 @@ class OpenSSLRFCTestCase(OpenSSLAssertersMixin,
|
||||||
|
|
||||||
|
|
||||||
class BuiltinRFCTestCase(BuiltinAssertersMixin,
|
class BuiltinRFCTestCase(BuiltinAssertersMixin,
|
||||||
WithNamedHashFunctions, RFCTestCaseMixin,
|
NamedHashFunctionsTrait,
|
||||||
|
RFCTestCaseMixin,
|
||||||
unittest.TestCase):
|
unittest.TestCase):
|
||||||
"""Built-in HACL* implementation of HMAC.
|
"""Built-in HACL* implementation of HMAC.
|
||||||
|
|
||||||
|
@ -784,12 +824,6 @@ class BuiltinRFCTestCase(BuiltinAssertersMixin,
|
||||||
self.check_hmac_hexdigest(key, msg, hexdigest, digest_size, func)
|
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):
|
class DigestModTestCaseMixin(CreatorMixin, DigestMixin):
|
||||||
"""Tests for the 'digestmod' parameter for hmac_new() and hmac_digest()."""
|
"""Tests for the 'digestmod' parameter for hmac_new() and hmac_digest()."""
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue