mirror of
https://github.com/python/cpython.git
synced 2025-10-28 04:04:44 +00:00
gh-136547: fix hashlib_helper for blocking and requesting digests (#136762)
- Fix `hashlib_helper.block_algorithm` where the dummy functions were incorrectly defined.
- Rename `hashlib_helper.HashAPI` to `hashlib_helper.HashInfo` and add more helper methods.
- Simplify `hashlib_helper.requires_*()` functions.
- Rewrite some private helpers in `hashlib_helper`.
- Remove `find_{builtin,openssl}_hashdigest_constructor()` as they are no more needed and were
not meant to be public in the first place.
- Fix some tests in `test_hashlib` when FIPS mode is on.
This commit is contained in:
parent
cc81b4e501
commit
c504f62fe2
4 changed files with 579 additions and 310 deletions
|
|
@ -136,12 +136,22 @@ def __get_openssl_constructor(name):
|
||||||
# Prefer our builtin blake2 implementation.
|
# Prefer our builtin blake2 implementation.
|
||||||
return __get_builtin_constructor(name)
|
return __get_builtin_constructor(name)
|
||||||
try:
|
try:
|
||||||
# MD5, SHA1, and SHA2 are in all supported OpenSSL versions
|
# Fetch the OpenSSL hash function if it exists,
|
||||||
# SHA3/shake are available in OpenSSL 1.1.1+
|
# independently of the context security policy.
|
||||||
f = getattr(_hashlib, 'openssl_' + name)
|
f = getattr(_hashlib, 'openssl_' + name)
|
||||||
# Allow the C module to raise ValueError. The function will be
|
# Check if the context security policy blocks the digest or not
|
||||||
# defined but the hash not actually available. Don't fall back to
|
# by allowing the C module to raise a ValueError. The function
|
||||||
# builtin if the current security policy blocks a digest, bpo#40695.
|
# will be defined but the hash will not be available at runtime.
|
||||||
|
#
|
||||||
|
# We use "usedforsecurity=False" to prevent falling back to the
|
||||||
|
# built-in function in case the security policy does not allow it.
|
||||||
|
#
|
||||||
|
# Note that this only affects the explicit named constructors,
|
||||||
|
# and not the algorithms exposed through hashlib.new() which
|
||||||
|
# can still be resolved to a built-in function even if the
|
||||||
|
# current security policy does not allow it.
|
||||||
|
#
|
||||||
|
# See https://github.com/python/cpython/issues/84872.
|
||||||
f(usedforsecurity=False)
|
f(usedforsecurity=False)
|
||||||
# Use the C function directly (very fast)
|
# Use the C function directly (very fast)
|
||||||
return f
|
return f
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,214 @@
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import enum
|
||||||
import functools
|
import functools
|
||||||
import hashlib
|
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
import unittest
|
import unittest
|
||||||
import unittest.mock
|
import unittest.mock
|
||||||
from collections import namedtuple
|
from test.support import import_helper
|
||||||
from test.support.import_helper import import_module
|
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
|
||||||
try:
|
|
||||||
import _hashlib
|
|
||||||
except ImportError:
|
|
||||||
_hashlib = None
|
|
||||||
|
|
||||||
try:
|
def try_import_module(module_name):
|
||||||
import _hmac
|
"""Try to import a module and return None on failure."""
|
||||||
except ImportError:
|
try:
|
||||||
_hmac = None
|
return importlib.import_module(module_name)
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
CANONICAL_DIGEST_NAMES = frozenset((
|
class HID(enum.StrEnum):
|
||||||
'md5', 'sha1',
|
"""Enumeration containing the canonical digest names.
|
||||||
'sha224', 'sha256', 'sha384', 'sha512',
|
|
||||||
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
|
Those names should only be used by hashlib.new() or hmac.new().
|
||||||
'shake_128', 'shake_256',
|
Their support by _hashlib.new() is not necessarily guaranteed.
|
||||||
'blake2s', 'blake2b',
|
"""
|
||||||
|
|
||||||
|
md5 = enum.auto()
|
||||||
|
sha1 = enum.auto()
|
||||||
|
|
||||||
|
sha224 = enum.auto()
|
||||||
|
sha256 = enum.auto()
|
||||||
|
sha384 = enum.auto()
|
||||||
|
sha512 = enum.auto()
|
||||||
|
|
||||||
|
sha3_224 = enum.auto()
|
||||||
|
sha3_256 = enum.auto()
|
||||||
|
sha3_384 = enum.auto()
|
||||||
|
sha3_512 = enum.auto()
|
||||||
|
|
||||||
|
shake_128 = enum.auto()
|
||||||
|
shake_256 = enum.auto()
|
||||||
|
|
||||||
|
blake2s = enum.auto()
|
||||||
|
blake2b = enum.auto()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_xof(self):
|
||||||
|
"""Indicate whether the hash is an extendable-output hash function."""
|
||||||
|
return self.startswith("shake_")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_keyed(self):
|
||||||
|
"""Indicate whether the hash is a keyed hash function."""
|
||||||
|
return self.startswith("blake2")
|
||||||
|
|
||||||
|
|
||||||
|
CANONICAL_DIGEST_NAMES = frozenset(map(str, HID.__members__))
|
||||||
|
NON_HMAC_DIGEST_NAMES = frozenset((
|
||||||
|
HID.shake_128, HID.shake_256,
|
||||||
|
HID.blake2s, HID.blake2b,
|
||||||
))
|
))
|
||||||
|
|
||||||
NON_HMAC_DIGEST_NAMES = frozenset({
|
|
||||||
'shake_128', 'shake_256',
|
|
||||||
'blake2s', 'blake2b',
|
|
||||||
})
|
|
||||||
|
|
||||||
|
class HashInfo:
|
||||||
|
"""Dataclass storing explicit hash constructor names.
|
||||||
|
|
||||||
class HashAPI(namedtuple("HashAPI", "builtin openssl hashlib")):
|
- *builtin* is the fully-qualified name for the explicit HACL*
|
||||||
|
hash constructor function, e.g., "_md5.md5".
|
||||||
|
|
||||||
def fullname(self, typ):
|
- *openssl* is the name of the "_hashlib" module method for the explicit
|
||||||
match typ:
|
OpenSSL hash constructor function, e.g., "openssl_md5".
|
||||||
|
|
||||||
|
- *hashlib* is the name of the "hashlib" module method for the explicit
|
||||||
|
hash constructor function, e.g., "md5".
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, builtin, openssl=None, hashlib=None):
|
||||||
|
assert isinstance(builtin, str), builtin
|
||||||
|
assert len(builtin.split(".")) == 2, builtin
|
||||||
|
|
||||||
|
self.builtin = builtin
|
||||||
|
self.builtin_module_name, self.builtin_method_name = (
|
||||||
|
self.builtin.split(".", maxsplit=1)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert openssl is None or openssl.startswith("openssl_")
|
||||||
|
self.openssl = self.openssl_method_name = openssl
|
||||||
|
self.openssl_module_name = "_hashlib" if openssl else None
|
||||||
|
|
||||||
|
assert hashlib is None or isinstance(hashlib, str)
|
||||||
|
self.hashlib = self.hashlib_method_name = hashlib
|
||||||
|
self.hashlib_module_name = "hashlib" if hashlib else None
|
||||||
|
|
||||||
|
def module_name(self, implementation):
|
||||||
|
match implementation:
|
||||||
case "builtin":
|
case "builtin":
|
||||||
return self.builtin
|
return self.builtin_module_name
|
||||||
case "openssl":
|
case "openssl":
|
||||||
return f"_hashlib.{self.openssl}" if self.openssl else None
|
return self.openssl_module_name
|
||||||
case "hashlib":
|
case "hashlib":
|
||||||
return f"hashlib.{self.hashlib}" if self.hashlib else None
|
return self.hashlib_module_name
|
||||||
case _:
|
raise AssertionError(f"invalid implementation {implementation}")
|
||||||
raise AssertionError(f"unknown type: {typ}")
|
|
||||||
|
def method_name(self, implementation):
|
||||||
|
match implementation:
|
||||||
|
case "builtin":
|
||||||
|
return self.builtin_method_name
|
||||||
|
case "openssl":
|
||||||
|
return self.openssl_method_name
|
||||||
|
case "hashlib":
|
||||||
|
return self.hashlib_method_name
|
||||||
|
raise AssertionError(f"invalid implementation {implementation}")
|
||||||
|
|
||||||
|
def fullname(self, implementation):
|
||||||
|
"""Get the fully qualified name of a given implementation.
|
||||||
|
|
||||||
|
This returns a string of the form "MODULE_NAME.METHOD_NAME" or None
|
||||||
|
if the hash function does not have a corresponding implementation.
|
||||||
|
|
||||||
|
*implementation* must be "builtin", "openssl" or "hashlib".
|
||||||
|
"""
|
||||||
|
module_name = self.module_name(implementation)
|
||||||
|
method_name = self.method_name(implementation)
|
||||||
|
if module_name is None or method_name is None:
|
||||||
|
return None
|
||||||
|
return f"{module_name}.{method_name}"
|
||||||
|
|
||||||
|
|
||||||
# Mapping from a "canonical" name to a pair (HACL*, _hashlib.*, hashlib.*)
|
# Mapping from a "canonical" name to a pair (HACL*, _hashlib.*, hashlib.*)
|
||||||
# constructors. If the constructor name is None, then this means that the
|
# constructors. If the constructor name is None, then this means that the
|
||||||
# algorithm can only be used by the "agile" new() interfaces.
|
# algorithm can only be used by the "agile" new() interfaces.
|
||||||
_EXPLICIT_CONSTRUCTORS = MappingProxyType({
|
_EXPLICIT_CONSTRUCTORS = MappingProxyType({ # fmt: skip
|
||||||
"md5": HashAPI("_md5.md5", "openssl_md5", "md5"),
|
HID.md5: HashInfo("_md5.md5", "openssl_md5", "md5"),
|
||||||
"sha1": HashAPI("_sha1.sha1", "openssl_sha1", "sha1"),
|
HID.sha1: HashInfo("_sha1.sha1", "openssl_sha1", "sha1"),
|
||||||
"sha224": HashAPI("_sha2.sha224", "openssl_sha224", "sha224"),
|
HID.sha224: HashInfo("_sha2.sha224", "openssl_sha224", "sha224"),
|
||||||
"sha256": HashAPI("_sha2.sha256", "openssl_sha256", "sha256"),
|
HID.sha256: HashInfo("_sha2.sha256", "openssl_sha256", "sha256"),
|
||||||
"sha384": HashAPI("_sha2.sha384", "openssl_sha384", "sha384"),
|
HID.sha384: HashInfo("_sha2.sha384", "openssl_sha384", "sha384"),
|
||||||
"sha512": HashAPI("_sha2.sha512", "openssl_sha512", "sha512"),
|
HID.sha512: HashInfo("_sha2.sha512", "openssl_sha512", "sha512"),
|
||||||
"sha3_224": HashAPI("_sha3.sha3_224", "openssl_sha3_224", "sha3_224"),
|
HID.sha3_224: HashInfo(
|
||||||
"sha3_256": HashAPI("_sha3.sha3_256", "openssl_sha3_256", "sha3_256"),
|
"_sha3.sha3_224", "openssl_sha3_224", "sha3_224"
|
||||||
"sha3_384": HashAPI("_sha3.sha3_384", "openssl_sha3_384", "sha3_384"),
|
),
|
||||||
"sha3_512": HashAPI("_sha3.sha3_512", "openssl_sha3_512", "sha3_512"),
|
HID.sha3_256: HashInfo(
|
||||||
"shake_128": HashAPI("_sha3.shake_128", "openssl_shake_128", "shake_128"),
|
"_sha3.sha3_256", "openssl_sha3_256", "sha3_256"
|
||||||
"shake_256": HashAPI("_sha3.shake_256", "openssl_shake_256", "shake_256"),
|
),
|
||||||
"blake2s": HashAPI("_blake2.blake2s", None, "blake2s"),
|
HID.sha3_384: HashInfo(
|
||||||
"blake2b": HashAPI("_blake2.blake2b", None, "blake2b"),
|
"_sha3.sha3_384", "openssl_sha3_384", "sha3_384"
|
||||||
|
),
|
||||||
|
HID.sha3_512: HashInfo(
|
||||||
|
"_sha3.sha3_512", "openssl_sha3_512", "sha3_512"
|
||||||
|
),
|
||||||
|
HID.shake_128: HashInfo(
|
||||||
|
"_sha3.shake_128", "openssl_shake_128", "shake_128"
|
||||||
|
),
|
||||||
|
HID.shake_256: HashInfo(
|
||||||
|
"_sha3.shake_256", "openssl_shake_256", "shake_256"
|
||||||
|
),
|
||||||
|
HID.blake2s: HashInfo("_blake2.blake2s", None, "blake2s"),
|
||||||
|
HID.blake2b: HashInfo("_blake2.blake2b", None, "blake2b"),
|
||||||
})
|
})
|
||||||
assert _EXPLICIT_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES
|
assert _EXPLICIT_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES
|
||||||
|
get_hash_info = _EXPLICIT_CONSTRUCTORS.__getitem__
|
||||||
|
|
||||||
|
# Mapping from canonical hash names to their explicit HACL* HMAC constructor.
|
||||||
|
# There is currently no OpenSSL one-shot named function and there will likely
|
||||||
|
# be none in the future.
|
||||||
_EXPLICIT_HMAC_CONSTRUCTORS = {
|
_EXPLICIT_HMAC_CONSTRUCTORS = {
|
||||||
name: f'_hmac.compute_{name}' for name in (
|
HID(name): f"_hmac.compute_{name}"
|
||||||
'md5', 'sha1',
|
for name in CANONICAL_DIGEST_NAMES
|
||||||
'sha224', 'sha256', 'sha384', 'sha512',
|
|
||||||
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
_EXPLICIT_HMAC_CONSTRUCTORS['shake_128'] = None
|
# Neither HACL* nor OpenSSL supports HMAC over XOFs.
|
||||||
_EXPLICIT_HMAC_CONSTRUCTORS['shake_256'] = None
|
_EXPLICIT_HMAC_CONSTRUCTORS[HID.shake_128] = None
|
||||||
|
_EXPLICIT_HMAC_CONSTRUCTORS[HID.shake_256] = None
|
||||||
# Strictly speaking, HMAC-BLAKE is meaningless as BLAKE2 is already a
|
# Strictly speaking, HMAC-BLAKE is meaningless as BLAKE2 is already a
|
||||||
# keyed hash function. However, as it's exposed by HACL*, we test it.
|
# keyed hash function. However, as it's exposed by HACL*, we test it.
|
||||||
_EXPLICIT_HMAC_CONSTRUCTORS['blake2s'] = '_hmac.compute_blake2s_32'
|
_EXPLICIT_HMAC_CONSTRUCTORS[HID.blake2s] = '_hmac.compute_blake2s_32'
|
||||||
_EXPLICIT_HMAC_CONSTRUCTORS['blake2b'] = '_hmac.compute_blake2b_32'
|
_EXPLICIT_HMAC_CONSTRUCTORS[HID.blake2b] = '_hmac.compute_blake2b_32'
|
||||||
_EXPLICIT_HMAC_CONSTRUCTORS = MappingProxyType(_EXPLICIT_HMAC_CONSTRUCTORS)
|
_EXPLICIT_HMAC_CONSTRUCTORS = MappingProxyType(_EXPLICIT_HMAC_CONSTRUCTORS)
|
||||||
assert _EXPLICIT_HMAC_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES
|
assert _EXPLICIT_HMAC_CONSTRUCTORS.keys() == CANONICAL_DIGEST_NAMES
|
||||||
|
|
||||||
|
|
||||||
|
def _decorate_func_or_class(decorator_func, func_or_class):
|
||||||
|
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 _chain_decorators(decorators):
|
||||||
|
"""Obtain a decorator by chaining multiple decorators.
|
||||||
|
|
||||||
|
The decorators are applied in the order they are given.
|
||||||
|
"""
|
||||||
|
def decorator_func(func):
|
||||||
|
return functools.reduce(lambda w, deco: deco(w), decorators, func)
|
||||||
|
return functools.partial(_decorate_func_or_class, decorator_func)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_wrapper_signature(wrapper, wrapped):
|
def _ensure_wrapper_signature(wrapper, wrapped):
|
||||||
"""Ensure that a wrapper has the same signature as the wrapped function.
|
"""Ensure that a wrapper has the same signature as the wrapped function.
|
||||||
|
|
||||||
|
|
@ -108,49 +231,129 @@ def _ensure_wrapper_signature(wrapper, wrapped):
|
||||||
|
|
||||||
|
|
||||||
def requires_hashlib():
|
def requires_hashlib():
|
||||||
|
_hashlib = try_import_module("_hashlib")
|
||||||
return unittest.skipIf(_hashlib is None, "requires _hashlib")
|
return unittest.skipIf(_hashlib is None, "requires _hashlib")
|
||||||
|
|
||||||
|
|
||||||
def requires_builtin_hmac():
|
def requires_builtin_hmac():
|
||||||
|
_hmac = try_import_module("_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):
|
class SkipNoHash(unittest.SkipTest):
|
||||||
parts = ["missing", implementation, f"hash algorithm: {digestname!r}"]
|
"""A SkipTest exception raised when a hash is not available."""
|
||||||
msg = " ".join(filter(None, parts))
|
|
||||||
raise unittest.SkipTest(msg) from exc
|
def __init__(self, digestname, implementation=None, interface=None):
|
||||||
|
parts = ["missing", implementation, f"hash algorithm {digestname!r}"]
|
||||||
|
if interface is not None:
|
||||||
|
parts.append(f"for {interface}")
|
||||||
|
super().__init__(" ".join(filter(None, parts)))
|
||||||
|
|
||||||
|
|
||||||
def _openssl_availabillity(digestname, *, usedforsecurity):
|
def _hashlib_new(digestname, openssl, /, **kwargs):
|
||||||
|
"""Check availability of [hashlib|_hashlib].new(digestname, **kwargs).
|
||||||
|
|
||||||
|
If *openssl* is True, module is "_hashlib" (C extension module),
|
||||||
|
otherwise it is "hashlib" (pure Python interface).
|
||||||
|
|
||||||
|
The constructor function is returned (without binding **kwargs),
|
||||||
|
or SkipTest is raised if none exists.
|
||||||
|
"""
|
||||||
|
assert isinstance(digestname, str), digestname
|
||||||
|
# Re-import 'hashlib' in case it was mocked, but propagate
|
||||||
|
# exceptions as it should be unconditionally available.
|
||||||
|
hashlib = importlib.import_module("hashlib")
|
||||||
|
# re-import '_hashlib' in case it was mocked
|
||||||
|
_hashlib = try_import_module("_hashlib")
|
||||||
|
module = _hashlib if openssl and _hashlib is not None else hashlib
|
||||||
|
try:
|
||||||
|
module.new(digestname, **kwargs)
|
||||||
|
except ValueError as exc:
|
||||||
|
interface = f"{module.__name__}.new"
|
||||||
|
raise SkipNoHash(digestname, interface=interface) from exc
|
||||||
|
return functools.partial(module.new, digestname)
|
||||||
|
|
||||||
|
|
||||||
|
def _builtin_hash(module_name, digestname, /, **kwargs):
|
||||||
|
"""Check availability of <module_name>.<digestname>(**kwargs).
|
||||||
|
|
||||||
|
- The *module_name* is the C extension module name based on HACL*.
|
||||||
|
- The *digestname* is one of its member, e.g., 'md5'.
|
||||||
|
|
||||||
|
The constructor function is returned, or SkipTest is raised if none exists.
|
||||||
|
"""
|
||||||
|
assert isinstance(module_name, str), module_name
|
||||||
|
assert isinstance(digestname, str), digestname
|
||||||
|
fullname = f'{module_name}.{digestname}'
|
||||||
|
try:
|
||||||
|
builtin_module = importlib.import_module(module_name)
|
||||||
|
except ImportError as exc:
|
||||||
|
raise SkipNoHash(fullname, "builtin") from exc
|
||||||
|
try:
|
||||||
|
constructor = getattr(builtin_module, digestname)
|
||||||
|
except AttributeError as exc:
|
||||||
|
raise SkipNoHash(fullname, "builtin") from exc
|
||||||
|
try:
|
||||||
|
constructor(**kwargs)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise SkipNoHash(fullname, "builtin") from exc
|
||||||
|
return constructor
|
||||||
|
|
||||||
|
|
||||||
|
def _openssl_new(digestname, /, **kwargs):
|
||||||
|
"""Check availability of _hashlib.new(digestname, **kwargs).
|
||||||
|
|
||||||
|
The constructor function is returned (without binding **kwargs),
|
||||||
|
or SkipTest is raised if none exists.
|
||||||
|
"""
|
||||||
assert isinstance(digestname, str), digestname
|
assert isinstance(digestname, str), digestname
|
||||||
try:
|
try:
|
||||||
_hashlib.new(digestname, usedforsecurity=usedforsecurity)
|
# re-import '_hashlib' in case it was mocked
|
||||||
except AttributeError:
|
_hashlib = importlib.import_module("_hashlib")
|
||||||
assert _hashlib is None
|
except ImportError as exc:
|
||||||
_missing_hash(digestname, "OpenSSL")
|
raise SkipNoHash(digestname, "openssl") from exc
|
||||||
|
try:
|
||||||
|
_hashlib.new(digestname, **kwargs)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
_missing_hash(digestname, "OpenSSL", exc=exc)
|
raise SkipNoHash(digestname, interface="_hashlib.new") from exc
|
||||||
|
return functools.partial(_hashlib.new, digestname)
|
||||||
|
|
||||||
|
|
||||||
def _decorate_func_or_class(func_or_class, decorator_func):
|
def _openssl_hash(digestname, /, **kwargs):
|
||||||
if not isinstance(func_or_class, type):
|
"""Check availability of _hashlib.openssl_<digestname>(**kwargs).
|
||||||
return decorator_func(func_or_class)
|
|
||||||
|
|
||||||
decorated_class = func_or_class
|
The constructor function is returned (without binding **kwargs),
|
||||||
setUpClass = decorated_class.__dict__.get('setUpClass')
|
or SkipTest is raised if none exists.
|
||||||
if setUpClass is None:
|
"""
|
||||||
def setUpClass(cls):
|
assert isinstance(digestname, str), digestname
|
||||||
super(decorated_class, cls).setUpClass()
|
fullname = f"_hashlib.openssl_{digestname}"
|
||||||
setUpClass.__qualname__ = decorated_class.__qualname__ + '.setUpClass'
|
try:
|
||||||
setUpClass.__module__ = decorated_class.__module__
|
# re-import '_hashlib' in case it was mocked
|
||||||
else:
|
_hashlib = importlib.import_module("_hashlib")
|
||||||
setUpClass = setUpClass.__func__
|
except ImportError as exc:
|
||||||
setUpClass = classmethod(decorator_func(setUpClass))
|
raise SkipNoHash(fullname, "openssl") from exc
|
||||||
decorated_class.setUpClass = setUpClass
|
try:
|
||||||
return decorated_class
|
constructor = getattr(_hashlib, f"openssl_{digestname}", None)
|
||||||
|
except AttributeError as exc:
|
||||||
|
raise SkipNoHash(fullname, "openssl") from exc
|
||||||
|
try:
|
||||||
|
constructor(**kwargs)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise SkipNoHash(fullname, "openssl") from exc
|
||||||
|
return constructor
|
||||||
|
|
||||||
|
|
||||||
def requires_hashdigest(digestname, openssl=None, usedforsecurity=True):
|
def _make_requires_hashdigest_decorator(test, /, *test_args, **test_kwargs):
|
||||||
|
def decorator_func(func):
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
test(*test_args, **test_kwargs)
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return functools.partial(_decorate_func_or_class, decorator_func)
|
||||||
|
|
||||||
|
|
||||||
|
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 may be missing, blocked by a strict crypto policy,
|
The hashing algorithm may be missing, blocked by a strict crypto policy,
|
||||||
|
|
@ -167,27 +370,9 @@ def requires_hashdigest(digestname, openssl=None, usedforsecurity=True):
|
||||||
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
|
||||||
"""
|
"""
|
||||||
assert isinstance(digestname, str), digestname
|
return _make_requires_hashdigest_decorator(
|
||||||
if openssl and _hashlib is not None:
|
_hashlib_new, digestname, openssl, usedforsecurity=usedforsecurity
|
||||||
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:
|
|
||||||
test_availability()
|
|
||||||
except ValueError as exc:
|
|
||||||
_missing_hash(digestname, exc=exc)
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
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):
|
def requires_openssl_hashdigest(digestname, *, usedforsecurity=True):
|
||||||
|
|
@ -195,27 +380,9 @@ 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.
|
||||||
"""
|
"""
|
||||||
assert isinstance(digestname, str), digestname
|
return _make_requires_hashdigest_decorator(
|
||||||
def decorator_func(func):
|
_openssl_new, digestname, usedforsecurity=usedforsecurity
|
||||||
@requires_hashlib() # avoid checking at each call
|
)
|
||||||
@functools.wraps(func)
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
_openssl_availabillity(digestname, usedforsecurity=usedforsecurity)
|
|
||||||
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_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(
|
def requires_builtin_hashdigest(
|
||||||
|
|
@ -226,40 +393,22 @@ def requires_builtin_hashdigest(
|
||||||
- The *module_name* is the C extension module name based on HACL*.
|
- The *module_name* is the C extension module name based on HACL*.
|
||||||
- The *digestname* is one of its member, e.g., 'md5'.
|
- The *digestname* is one of its member, e.g., 'md5'.
|
||||||
"""
|
"""
|
||||||
assert isinstance(digestname, str), digestname
|
return _make_requires_hashdigest_decorator(
|
||||||
def decorator_func(func):
|
_builtin_hash, module_name, digestname, usedforsecurity=usedforsecurity
|
||||||
@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(
|
def requires_builtin_hashes(*ignored, usedforsecurity=True):
|
||||||
module_name, digestname, *, usedforsecurity=True
|
"""Decorator raising SkipTest if one HACL* hashing algorithm is missing."""
|
||||||
):
|
return _chain_decorators((
|
||||||
"""Find the HACL* hash function constructor.
|
requires_builtin_hashdigest(
|
||||||
|
api.builtin_module_name,
|
||||||
- The *module_name* is the C extension module name based on HACL*.
|
api.builtin_method_name,
|
||||||
- The *digestname* is one of its member, e.g., 'md5'.
|
usedforsecurity=usedforsecurity,
|
||||||
"""
|
)
|
||||||
assert isinstance(digestname, str), digestname
|
for name, api in _EXPLICIT_CONSTRUCTORS.items()
|
||||||
module = import_module(module_name)
|
if name not in ignored
|
||||||
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:
|
class HashFunctionsTrait:
|
||||||
|
|
@ -281,7 +430,9 @@ class HashFunctionsTrait:
|
||||||
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
|
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Default 'usedforsecurity' to use when looking up a hash function.
|
# Default 'usedforsecurity' to use when checking a hash function.
|
||||||
|
# When the trait properties are callables (e.g., _md5.md5) and
|
||||||
|
# not strings, they must be called with the same 'usedforsecurity'.
|
||||||
usedforsecurity = True
|
usedforsecurity = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -357,9 +508,9 @@ class OpenSSLHashFunctionsTrait(HashFunctionsTrait):
|
||||||
|
|
||||||
def _find_constructor(self, digestname):
|
def _find_constructor(self, digestname):
|
||||||
self.is_valid_digest_name(digestname)
|
self.is_valid_digest_name(digestname)
|
||||||
return find_openssl_hashdigest_constructor(
|
# This returns a function of the form _hashlib.openssl_<name> and
|
||||||
digestname, usedforsecurity=self.usedforsecurity
|
# not a lambda function as it is rejected by _hashlib.hmac_new().
|
||||||
)
|
return _openssl_hash(digestname, usedforsecurity=self.usedforsecurity)
|
||||||
|
|
||||||
|
|
||||||
class BuiltinHashFunctionsTrait(HashFunctionsTrait):
|
class BuiltinHashFunctionsTrait(HashFunctionsTrait):
|
||||||
|
|
@ -370,49 +521,14 @@ class BuiltinHashFunctionsTrait(HashFunctionsTrait):
|
||||||
is not since the former is unconditionally built.
|
is not since the former is unconditionally built.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _find_constructor_in(self, module, digestname):
|
def _find_constructor(self, digestname):
|
||||||
self.is_valid_digest_name(digestname)
|
self.is_valid_digest_name(digestname)
|
||||||
return find_builtin_hashdigest_constructor(module, digestname)
|
info = _EXPLICIT_CONSTRUCTORS[digestname]
|
||||||
|
return _builtin_hash(
|
||||||
@property
|
info.builtin_module_name,
|
||||||
def md5(self):
|
info.builtin_method_name,
|
||||||
return self._find_constructor_in("_md5", "md5")
|
usedforsecurity=self.usedforsecurity,
|
||||||
|
)
|
||||||
@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):
|
||||||
|
|
@ -426,38 +542,45 @@ def find_gil_minsize(modules_names, default=2048):
|
||||||
"""
|
"""
|
||||||
sizes = []
|
sizes = []
|
||||||
for module_name in modules_names:
|
for module_name in modules_names:
|
||||||
try:
|
module = try_import_module(module_name)
|
||||||
module = importlib.import_module(module_name)
|
if module is not None:
|
||||||
except ImportError:
|
sizes.append(getattr(module, '_GIL_MINSIZE', default))
|
||||||
continue
|
|
||||||
sizes.append(getattr(module, '_GIL_MINSIZE', default))
|
|
||||||
return max(sizes, default=default)
|
return max(sizes, default=default)
|
||||||
|
|
||||||
|
|
||||||
def _block_openssl_hash_new(blocked_name):
|
def _block_openssl_hash_new(blocked_name):
|
||||||
"""Block OpenSSL implementation of _hashlib.new()."""
|
"""Block OpenSSL implementation of _hashlib.new()."""
|
||||||
assert isinstance(blocked_name, str), blocked_name
|
assert isinstance(blocked_name, str), blocked_name
|
||||||
if _hashlib is None:
|
|
||||||
|
# re-import '_hashlib' in case it was mocked
|
||||||
|
if (_hashlib := try_import_module("_hashlib")) is None:
|
||||||
return contextlib.nullcontext()
|
return contextlib.nullcontext()
|
||||||
|
|
||||||
@functools.wraps(wrapped := _hashlib.new)
|
@functools.wraps(wrapped := _hashlib.new)
|
||||||
def wrapper(name, data=b'', *, usedforsecurity=True, string=None):
|
def _hashlib_new(name, data=b'', *, usedforsecurity=True, string=None):
|
||||||
if name == blocked_name:
|
if name == blocked_name:
|
||||||
raise _hashlib.UnsupportedDigestmodError(blocked_name)
|
raise _hashlib.UnsupportedDigestmodError(blocked_name)
|
||||||
return wrapped(*args, **kwargs)
|
return wrapped(name, data,
|
||||||
_ensure_wrapper_signature(wrapper, wrapped)
|
usedforsecurity=usedforsecurity, string=string)
|
||||||
return unittest.mock.patch('_hashlib.new', wrapper)
|
|
||||||
|
_ensure_wrapper_signature(_hashlib_new, wrapped)
|
||||||
|
return unittest.mock.patch('_hashlib.new', _hashlib_new)
|
||||||
|
|
||||||
|
|
||||||
def _block_openssl_hmac_new(blocked_name):
|
def _block_openssl_hmac_new(blocked_name):
|
||||||
"""Block OpenSSL HMAC-HASH implementation."""
|
"""Block OpenSSL HMAC-HASH implementation."""
|
||||||
assert isinstance(blocked_name, str), blocked_name
|
assert isinstance(blocked_name, str), blocked_name
|
||||||
if _hashlib is None:
|
|
||||||
|
# re-import '_hashlib' in case it was mocked
|
||||||
|
if (_hashlib := try_import_module("_hashlib")) is None:
|
||||||
return contextlib.nullcontext()
|
return contextlib.nullcontext()
|
||||||
|
|
||||||
@functools.wraps(wrapped := _hashlib.hmac_new)
|
@functools.wraps(wrapped := _hashlib.hmac_new)
|
||||||
def wrapper(key, msg=b'', digestmod=None):
|
def wrapper(key, msg=b'', digestmod=None):
|
||||||
if digestmod == blocked_name:
|
if digestmod == blocked_name:
|
||||||
raise _hashlib.UnsupportedDigestmodError(blocked_name)
|
raise _hashlib.UnsupportedDigestmodError(blocked_name)
|
||||||
return wrapped(key, msg, digestmod)
|
return wrapped(key, msg, digestmod)
|
||||||
|
|
||||||
_ensure_wrapper_signature(wrapper, wrapped)
|
_ensure_wrapper_signature(wrapper, wrapped)
|
||||||
return unittest.mock.patch('_hashlib.hmac_new', wrapper)
|
return unittest.mock.patch('_hashlib.hmac_new', wrapper)
|
||||||
|
|
||||||
|
|
@ -465,112 +588,132 @@ def wrapper(key, msg=b'', digestmod=None):
|
||||||
def _block_openssl_hmac_digest(blocked_name):
|
def _block_openssl_hmac_digest(blocked_name):
|
||||||
"""Block OpenSSL HMAC-HASH one-shot digest implementation."""
|
"""Block OpenSSL HMAC-HASH one-shot digest implementation."""
|
||||||
assert isinstance(blocked_name, str), blocked_name
|
assert isinstance(blocked_name, str), blocked_name
|
||||||
if _hashlib is None:
|
|
||||||
|
# re-import '_hashlib' in case it was mocked
|
||||||
|
if (_hashlib := try_import_module("_hashlib")) is None:
|
||||||
return contextlib.nullcontext()
|
return contextlib.nullcontext()
|
||||||
|
|
||||||
@functools.wraps(wrapped := _hashlib.hmac_digest)
|
@functools.wraps(wrapped := _hashlib.hmac_digest)
|
||||||
def wrapper(key, msg, digest):
|
def _hashlib_hmac_digest(key, msg, digest):
|
||||||
if digest == blocked_name:
|
if digest == blocked_name:
|
||||||
raise _hashlib.UnsupportedDigestmodError(blocked_name)
|
raise _hashlib.UnsupportedDigestmodError(blocked_name)
|
||||||
return wrapped(key, msg, digestmod)
|
return wrapped(key, msg, digest)
|
||||||
_ensure_wrapper_signature(wrapper, wrapped)
|
|
||||||
return unittest.mock.patch('_hashlib.hmac_digest', wrapper)
|
_ensure_wrapper_signature(_hashlib_hmac_digest, wrapped)
|
||||||
|
return unittest.mock.patch('_hashlib.hmac_digest', _hashlib_hmac_digest)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def _block_builtin_hash_new(name):
|
def _block_builtin_hash_new(name):
|
||||||
|
"""Block a buitin-in hash name from the hashlib.new() interface."""
|
||||||
assert isinstance(name, str), name
|
assert isinstance(name, str), name
|
||||||
assert name.lower() == name, f"invalid name: {name}"
|
assert name.lower() == name, f"invalid name: {name}"
|
||||||
|
assert name in HID, f"invalid hash: {name}"
|
||||||
|
|
||||||
builtin_cache = getattr(hashlib, '__builtin_constructor_cache')
|
# Re-import 'hashlib' in case it was mocked
|
||||||
if name in builtin_cache:
|
hashlib = importlib.import_module('hashlib')
|
||||||
f = builtin_cache.pop(name)
|
builtin_constructor_cache = getattr(hashlib, '__builtin_constructor_cache')
|
||||||
F = builtin_cache.pop(name.upper(), None)
|
builtin_constructor_cache_mock = builtin_constructor_cache.copy()
|
||||||
else:
|
builtin_constructor_cache_mock.pop(name, None)
|
||||||
f = F = None
|
builtin_constructor_cache_mock.pop(name.upper(), None)
|
||||||
try:
|
|
||||||
yield
|
# __get_builtin_constructor() imports the HACL* modules on demand,
|
||||||
finally:
|
# so we need to block the possibility of importing it, but only
|
||||||
if f is not None:
|
# during the call to __get_builtin_constructor().
|
||||||
builtin_cache[name] = f
|
get_builtin_constructor = getattr(hashlib, '__get_builtin_constructor')
|
||||||
if F is not None:
|
builtin_module_name = _EXPLICIT_CONSTRUCTORS[name].builtin_module_name
|
||||||
builtin_cache[name.upper()] = F
|
|
||||||
|
@functools.wraps(get_builtin_constructor)
|
||||||
|
def get_builtin_constructor_mock(name):
|
||||||
|
with import_helper.isolated_modules():
|
||||||
|
sys = importlib.import_module("sys")
|
||||||
|
sys.modules[builtin_module_name] = None # block module's import
|
||||||
|
return get_builtin_constructor(name)
|
||||||
|
|
||||||
|
return unittest.mock.patch.multiple(
|
||||||
|
hashlib,
|
||||||
|
__get_builtin_constructor=get_builtin_constructor_mock,
|
||||||
|
__builtin_constructor_cache=builtin_constructor_cache_mock
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _block_builtin_hmac_new(blocked_name):
|
def _block_builtin_hmac_new(blocked_name):
|
||||||
assert isinstance(blocked_name, str), blocked_name
|
assert isinstance(blocked_name, str), blocked_name
|
||||||
if _hmac is None:
|
|
||||||
|
# re-import '_hmac' in case it was mocked
|
||||||
|
if (_hmac := try_import_module("_hmac")) is None:
|
||||||
return contextlib.nullcontext()
|
return contextlib.nullcontext()
|
||||||
|
|
||||||
@functools.wraps(wrapped := _hmac.new)
|
@functools.wraps(wrapped := _hmac.new)
|
||||||
def wrapper(key, msg=None, digestmod=None):
|
def _hmac_new(key, msg=None, digestmod=None):
|
||||||
if digestmod == blocked_name:
|
if digestmod == blocked_name:
|
||||||
raise _hmac.UnknownHashError(blocked_name)
|
raise _hmac.UnknownHashError(blocked_name)
|
||||||
return wrapped(key, msg, digestmod)
|
return wrapped(key, msg, digestmod)
|
||||||
_ensure_wrapper_signature(wrapper, wrapped)
|
|
||||||
return unittest.mock.patch('_hmac.new', wrapper)
|
_ensure_wrapper_signature(_hmac_new, wrapped)
|
||||||
|
return unittest.mock.patch('_hmac.new', _hmac_new)
|
||||||
|
|
||||||
|
|
||||||
def _block_builtin_hmac_digest(blocked_name):
|
def _block_builtin_hmac_digest(blocked_name):
|
||||||
assert isinstance(blocked_name, str), blocked_name
|
assert isinstance(blocked_name, str), blocked_name
|
||||||
if _hmac is None:
|
|
||||||
|
# re-import '_hmac' in case it was mocked
|
||||||
|
if (_hmac := try_import_module("_hmac")) is None:
|
||||||
return contextlib.nullcontext()
|
return contextlib.nullcontext()
|
||||||
|
|
||||||
@functools.wraps(wrapped := _hmac.compute_digest)
|
@functools.wraps(wrapped := _hmac.compute_digest)
|
||||||
def wrapper(key, msg, digest):
|
def _hmac_compute_digest(key, msg, digest):
|
||||||
if digest == blocked_name:
|
if digest == blocked_name:
|
||||||
raise _hmac.UnknownHashError(blocked_name)
|
raise _hmac.UnknownHashError(blocked_name)
|
||||||
return wrapped(key, msg, digest)
|
return wrapped(key, msg, digest)
|
||||||
_ensure_wrapper_signature(wrapper, wrapped)
|
|
||||||
return unittest.mock.patch('_hmac.compute_digest', wrapper)
|
_ensure_wrapper_signature(_hmac_compute_digest, wrapped)
|
||||||
|
return unittest.mock.patch('_hmac.compute_digest', _hmac_compute_digest)
|
||||||
|
|
||||||
|
|
||||||
def _make_hash_constructor_blocker(name, dummy, *, interface):
|
def _make_hash_constructor_blocker(name, dummy, implementation):
|
||||||
assert isinstance(name, str), name
|
info = _EXPLICIT_CONSTRUCTORS[name]
|
||||||
assert interface in ('builtin', 'openssl', 'hashlib')
|
module_name = info.module_name(implementation)
|
||||||
assert name in _EXPLICIT_CONSTRUCTORS, f"invalid hash: {name}"
|
method_name = info.method_name(implementation)
|
||||||
fullname = _EXPLICIT_CONSTRUCTORS[name].fullname(interface)
|
if module_name is None or method_name is None:
|
||||||
if fullname is None:
|
|
||||||
# function shouldn't exist for this implementation
|
# function shouldn't exist for this implementation
|
||||||
return contextlib.nullcontext()
|
return contextlib.nullcontext()
|
||||||
assert fullname.count('.') == 1, fullname
|
|
||||||
module_name, method = fullname.split('.', maxsplit=1)
|
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# module is already disabled
|
# module is already disabled
|
||||||
return contextlib.nullcontext()
|
return contextlib.nullcontext()
|
||||||
wrapped = getattr(module, method)
|
|
||||||
|
wrapped = getattr(module, method_name)
|
||||||
wrapper = functools.wraps(wrapped)(dummy)
|
wrapper = functools.wraps(wrapped)(dummy)
|
||||||
_ensure_wrapper_signature(wrapper, wrapped)
|
_ensure_wrapper_signature(wrapper, wrapped)
|
||||||
return unittest.mock.patch(fullname, wrapper)
|
return unittest.mock.patch(info.fullname(implementation), wrapper)
|
||||||
|
|
||||||
|
|
||||||
def _block_hashlib_hash_constructor(name):
|
def _block_hashlib_hash_constructor(name):
|
||||||
"""Block explicit public constructors."""
|
"""Block explicit public constructors."""
|
||||||
assert isinstance(name, str), name
|
|
||||||
def dummy(data=b'', *, usedforsecurity=True, string=None):
|
def dummy(data=b'', *, usedforsecurity=True, string=None):
|
||||||
raise ValueError(f"unsupported hash name: {name}")
|
raise ValueError(f"blocked explicit public hash name: {name}")
|
||||||
return _make_hash_constructor_blocker(name, dummy, interface='hashlib')
|
|
||||||
|
return _make_hash_constructor_blocker(name, dummy, 'hashlib')
|
||||||
|
|
||||||
|
|
||||||
def _block_openssl_hash_constructor(name):
|
def _block_openssl_hash_constructor(name):
|
||||||
"""Block explicit OpenSSL constructors."""
|
"""Block explicit OpenSSL constructors."""
|
||||||
assert isinstance(name, str), name
|
|
||||||
def dummy(data=b'', *, usedforsecurity=True, string=None):
|
def dummy(data=b'', *, usedforsecurity=True, string=None):
|
||||||
raise ValueError(f"unsupported hash name: {name}")
|
raise ValueError(f"blocked explicit OpenSSL hash name: {name}")
|
||||||
return _make_hash_constructor_blocker(name, dummy, interface='openssl')
|
return _make_hash_constructor_blocker(name, dummy, 'openssl')
|
||||||
|
|
||||||
|
|
||||||
def _block_builtin_hash_constructor(name):
|
def _block_builtin_hash_constructor(name):
|
||||||
"""Block explicit HACL* constructors."""
|
"""Block explicit HACL* constructors."""
|
||||||
assert isinstance(name, str), name
|
|
||||||
def dummy(data=b'', *, usedforsecurity=True, string=b''):
|
def dummy(data=b'', *, usedforsecurity=True, string=b''):
|
||||||
raise ValueError(f"unsupported hash name: {name}")
|
raise ValueError(f"blocked explicit builtin hash name: {name}")
|
||||||
return _make_hash_constructor_blocker(name, dummy, interface='builtin')
|
return _make_hash_constructor_blocker(name, dummy, 'builtin')
|
||||||
|
|
||||||
|
|
||||||
def _block_builtin_hmac_constructor(name):
|
def _block_builtin_hmac_constructor(name):
|
||||||
"""Block explicit HACL* HMAC constructors."""
|
"""Block explicit HACL* HMAC constructors."""
|
||||||
assert isinstance(name, str), name
|
|
||||||
assert name in _EXPLICIT_HMAC_CONSTRUCTORS, f"invalid hash: {name}"
|
|
||||||
fullname = _EXPLICIT_HMAC_CONSTRUCTORS[name]
|
fullname = _EXPLICIT_HMAC_CONSTRUCTORS[name]
|
||||||
if fullname is None:
|
if fullname is None:
|
||||||
# function shouldn't exist for this implementation
|
# function shouldn't exist for this implementation
|
||||||
|
|
@ -585,7 +728,7 @@ def _block_builtin_hmac_constructor(name):
|
||||||
return contextlib.nullcontext()
|
return contextlib.nullcontext()
|
||||||
@functools.wraps(wrapped := getattr(module, method))
|
@functools.wraps(wrapped := getattr(module, method))
|
||||||
def wrapper(key, obj):
|
def wrapper(key, obj):
|
||||||
raise ValueError(f"unsupported hash name: {name}")
|
raise ValueError(f"blocked hash name: {name}")
|
||||||
_ensure_wrapper_signature(wrapper, wrapped)
|
_ensure_wrapper_signature(wrapper, wrapped)
|
||||||
return unittest.mock.patch(fullname, wrapper)
|
return unittest.mock.patch(fullname, wrapper)
|
||||||
|
|
||||||
|
|
@ -600,22 +743,54 @@ def block_algorithm(name, *, allow_openssl=False, allow_builtin=False):
|
||||||
"""
|
"""
|
||||||
with contextlib.ExitStack() as stack:
|
with contextlib.ExitStack() as stack:
|
||||||
if not (allow_openssl or allow_builtin):
|
if not (allow_openssl or allow_builtin):
|
||||||
# If one of the private interface is allowed, then the
|
# Named constructors have a different behavior in the sense
|
||||||
# public interface will fallback to it even though the
|
# that they are either built-ins or OpenSSL ones, but not
|
||||||
# comment in hashlib.py says otherwise.
|
# "agile" ones (namely once "hashlib" has been imported,
|
||||||
|
# they are fixed).
|
||||||
#
|
#
|
||||||
# So we should only block it if the private interfaces
|
# If OpenSSL is not available, hashes fall back to built-in ones,
|
||||||
# are blocked as well.
|
# in which case we don't need to block the explicit public hashes
|
||||||
|
# as they will call a mocked one.
|
||||||
|
#
|
||||||
|
# If OpenSSL is available, hashes fall back to "openssl_*" ones,
|
||||||
|
# except for BLAKE2b and BLAKE2s.
|
||||||
stack.enter_context(_block_hashlib_hash_constructor(name))
|
stack.enter_context(_block_hashlib_hash_constructor(name))
|
||||||
|
elif (
|
||||||
|
# In FIPS mode, hashlib.<name>() functions may raise if they use
|
||||||
|
# the OpenSSL implementation, except with usedforsecurity=False.
|
||||||
|
# However, blocking such functions also means blocking them
|
||||||
|
# so we again need to block them if we want to.
|
||||||
|
(_hashlib := try_import_module("_hashlib"))
|
||||||
|
and _hashlib.get_fips_mode()
|
||||||
|
and not allow_openssl
|
||||||
|
) or (
|
||||||
|
# Without OpenSSL, hashlib.<name>() functions are aliases
|
||||||
|
# to built-in functions, so both of them must be blocked
|
||||||
|
# as the module may have been imported before the HACL ones.
|
||||||
|
not (_hashlib := try_import_module("_hashlib"))
|
||||||
|
and not allow_builtin
|
||||||
|
):
|
||||||
|
stack.enter_context(_block_hashlib_hash_constructor(name))
|
||||||
|
|
||||||
if not allow_openssl:
|
if not allow_openssl:
|
||||||
|
# _hashlib.new()
|
||||||
stack.enter_context(_block_openssl_hash_new(name))
|
stack.enter_context(_block_openssl_hash_new(name))
|
||||||
stack.enter_context(_block_openssl_hmac_new(name))
|
# _hashlib.openssl_*()
|
||||||
stack.enter_context(_block_openssl_hmac_digest(name))
|
|
||||||
stack.enter_context(_block_openssl_hash_constructor(name))
|
stack.enter_context(_block_openssl_hash_constructor(name))
|
||||||
|
# _hashlib.hmac_new()
|
||||||
|
stack.enter_context(_block_openssl_hmac_new(name))
|
||||||
|
# _hashlib.hmac_digest()
|
||||||
|
stack.enter_context(_block_openssl_hmac_digest(name))
|
||||||
|
|
||||||
if not allow_builtin:
|
if not allow_builtin:
|
||||||
|
# __get_builtin_constructor(name)
|
||||||
stack.enter_context(_block_builtin_hash_new(name))
|
stack.enter_context(_block_builtin_hash_new(name))
|
||||||
stack.enter_context(_block_builtin_hmac_new(name))
|
# <built-in module>.<built-in name>()
|
||||||
stack.enter_context(_block_builtin_hmac_digest(name))
|
|
||||||
stack.enter_context(_block_builtin_hash_constructor(name))
|
stack.enter_context(_block_builtin_hash_constructor(name))
|
||||||
|
# _hmac.new(..., name)
|
||||||
|
stack.enter_context(_block_builtin_hmac_new(name))
|
||||||
|
# _hmac.compute_<name>()
|
||||||
stack.enter_context(_block_builtin_hmac_constructor(name))
|
stack.enter_context(_block_builtin_hmac_constructor(name))
|
||||||
|
# _hmac.compute_digest(..., name)
|
||||||
|
stack.enter_context(_block_builtin_hmac_digest(name))
|
||||||
yield
|
yield
|
||||||
|
|
|
||||||
|
|
@ -545,13 +545,17 @@ def check(self, name, data, hexdigest, shake=False, **kwargs):
|
||||||
|
|
||||||
def check_file_digest(self, name, data, hexdigest):
|
def check_file_digest(self, name, data, hexdigest):
|
||||||
hexdigest = hexdigest.lower()
|
hexdigest = hexdigest.lower()
|
||||||
try:
|
digests = []
|
||||||
hashlib.new(name)
|
for digest in [name, *self.constructors_to_test[name]]:
|
||||||
except ValueError:
|
try:
|
||||||
# skip, algorithm is blocked by security policy.
|
if callable(digest):
|
||||||
return
|
digest(b"")
|
||||||
digests = [name]
|
else:
|
||||||
digests.extend(self.constructors_to_test[name])
|
hashlib.new(digest)
|
||||||
|
except ValueError:
|
||||||
|
# skip, algorithm is blocked by security policy.
|
||||||
|
continue
|
||||||
|
digests.append(digest)
|
||||||
|
|
||||||
with tempfile.TemporaryFile() as f:
|
with tempfile.TemporaryFile() as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import errno
|
import errno
|
||||||
import importlib
|
import importlib
|
||||||
import itertools
|
import itertools
|
||||||
|
import inspect
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
@ -820,6 +821,7 @@ def test_linked_to_musl(self):
|
||||||
# SuppressCrashReport
|
# SuppressCrashReport
|
||||||
|
|
||||||
|
|
||||||
|
@hashlib_helper.requires_builtin_hashes()
|
||||||
class TestHashlibSupport(unittest.TestCase):
|
class TestHashlibSupport(unittest.TestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -828,11 +830,20 @@ def setUpClass(cls):
|
||||||
cls.hashlib = import_helper.import_module("hashlib")
|
cls.hashlib = import_helper.import_module("hashlib")
|
||||||
cls.hmac = import_helper.import_module("hmac")
|
cls.hmac = import_helper.import_module("hmac")
|
||||||
|
|
||||||
# We required the extension modules to be present since blocking
|
# All C extension modules must be present since blocking
|
||||||
# HACL* implementations while allowing OpenSSL ones would still
|
# the built-in implementation while allowing OpenSSL or vice-versa
|
||||||
# result in failures.
|
# may result in failures depending on the exposed built-in hashes.
|
||||||
cls._hashlib = import_helper.import_module("_hashlib")
|
cls._hashlib = import_helper.import_module("_hashlib")
|
||||||
cls._hmac = import_helper.import_module("_hmac")
|
cls._hmac = import_helper.import_module("_hmac")
|
||||||
|
cls._md5 = import_helper.import_module("_md5")
|
||||||
|
|
||||||
|
def skip_if_fips_mode(self):
|
||||||
|
if self._hashlib.get_fips_mode():
|
||||||
|
self.skipTest("disabled in FIPS mode")
|
||||||
|
|
||||||
|
def skip_if_not_fips_mode(self):
|
||||||
|
if not self._hashlib.get_fips_mode():
|
||||||
|
self.skipTest("requires FIPS mode")
|
||||||
|
|
||||||
def check_context(self, disabled=True):
|
def check_context(self, disabled=True):
|
||||||
if disabled:
|
if disabled:
|
||||||
|
|
@ -853,25 +864,19 @@ def try_import_attribute(self, fullname, default=None):
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def validate_modules(self):
|
def fetch_hash_function(self, name, implementation):
|
||||||
if hasattr(hashlib_helper, 'hashlib'):
|
info = hashlib_helper.get_hash_info(name)
|
||||||
self.assertIs(hashlib_helper.hashlib, self.hashlib)
|
match implementation:
|
||||||
if hasattr(hashlib_helper, 'hmac'):
|
|
||||||
self.assertIs(hashlib_helper.hmac, self.hmac)
|
|
||||||
|
|
||||||
def fetch_hash_function(self, name, typ):
|
|
||||||
entry = hashlib_helper._EXPLICIT_CONSTRUCTORS[name]
|
|
||||||
match typ:
|
|
||||||
case "hashlib":
|
case "hashlib":
|
||||||
assert entry.hashlib is not None, entry
|
assert info.hashlib is not None, info
|
||||||
return getattr(self.hashlib, entry.hashlib)
|
return getattr(self.hashlib, info.hashlib)
|
||||||
case "openssl":
|
case "openssl":
|
||||||
try:
|
try:
|
||||||
return getattr(self._hashlib, entry.openssl, None)
|
return getattr(self._hashlib, info.openssl, None)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return None
|
return None
|
||||||
case "builtin":
|
fullname = info.fullname(implementation)
|
||||||
return self.try_import_attribute(entry.fullname(typ))
|
return self.try_import_attribute(fullname)
|
||||||
|
|
||||||
def fetch_hmac_function(self, name):
|
def fetch_hmac_function(self, name):
|
||||||
fullname = hashlib_helper._EXPLICIT_HMAC_CONSTRUCTORS[name]
|
fullname = hashlib_helper._EXPLICIT_HMAC_CONSTRUCTORS[name]
|
||||||
|
|
@ -936,16 +941,12 @@ def check_builtin_hmac(self, name, *, disabled=True):
|
||||||
)
|
)
|
||||||
def test_disable_hash(self, name, allow_openssl, allow_builtin):
|
def test_disable_hash(self, name, allow_openssl, allow_builtin):
|
||||||
# In FIPS mode, the function may be available but would still need
|
# In FIPS mode, the function may be available but would still need
|
||||||
# to raise a ValueError. For simplicity, we don't test the helper
|
# to raise a ValueError, so we will test the helper separately.
|
||||||
# when we're in FIPS mode.
|
self.skip_if_fips_mode()
|
||||||
if self._hashlib.get_fips_mode():
|
|
||||||
self.skipTest("hash functions may still be blocked in FIPS mode")
|
|
||||||
flags = dict(allow_openssl=allow_openssl, allow_builtin=allow_builtin)
|
flags = dict(allow_openssl=allow_openssl, allow_builtin=allow_builtin)
|
||||||
is_simple_disabled = not allow_builtin and not allow_openssl
|
is_fully_disabled = not allow_builtin and not allow_openssl
|
||||||
|
|
||||||
with hashlib_helper.block_algorithm(name, **flags):
|
with hashlib_helper.block_algorithm(name, **flags):
|
||||||
self.validate_modules()
|
|
||||||
|
|
||||||
# OpenSSL's blake2s and blake2b are unknown names
|
# OpenSSL's blake2s and blake2b are unknown names
|
||||||
# when only the OpenSSL interface is available.
|
# when only the OpenSSL interface is available.
|
||||||
if allow_openssl and not allow_builtin:
|
if allow_openssl and not allow_builtin:
|
||||||
|
|
@ -954,25 +955,104 @@ def test_disable_hash(self, name, allow_openssl, allow_builtin):
|
||||||
else:
|
else:
|
||||||
name_for_hashlib_new = name
|
name_for_hashlib_new = name
|
||||||
|
|
||||||
with self.check_context(is_simple_disabled):
|
with self.check_context(is_fully_disabled):
|
||||||
_ = self.hashlib.new(name_for_hashlib_new)
|
_ = self.hashlib.new(name_for_hashlib_new)
|
||||||
with self.check_context(is_simple_disabled):
|
|
||||||
_ = getattr(self.hashlib, name)(b"")
|
# Since _hashlib is present, explicit blake2b/blake2s constructors
|
||||||
|
# use the built-in implementation, while others (since we are not
|
||||||
|
# in FIPS mode and since _hashlib exists) use the OpenSSL function.
|
||||||
|
with self.check_context(is_fully_disabled):
|
||||||
|
_ = getattr(self.hashlib, name)()
|
||||||
|
|
||||||
self.check_openssl_hash(name, disabled=not allow_openssl)
|
self.check_openssl_hash(name, disabled=not allow_openssl)
|
||||||
self.check_builtin_hash(name, disabled=not allow_builtin)
|
self.check_builtin_hash(name, disabled=not allow_builtin)
|
||||||
|
|
||||||
if name not in hashlib_helper.NON_HMAC_DIGEST_NAMES:
|
if name not in hashlib_helper.NON_HMAC_DIGEST_NAMES:
|
||||||
with self.check_context(is_simple_disabled):
|
with self.check_context(is_fully_disabled):
|
||||||
_ = self.hmac.new(b"", b"", name)
|
_ = self.hmac.new(b"", b"", name)
|
||||||
with self.check_context(is_simple_disabled):
|
with self.check_context(is_fully_disabled):
|
||||||
_ = self.hmac.HMAC(b"", b"", name)
|
_ = self.hmac.HMAC(b"", b"", name)
|
||||||
with self.check_context(is_simple_disabled):
|
with self.check_context(is_fully_disabled):
|
||||||
_ = self.hmac.digest(b"", b"", name)
|
_ = self.hmac.digest(b"", b"", name)
|
||||||
|
|
||||||
self.check_openssl_hmac(name, disabled=not allow_openssl)
|
self.check_openssl_hmac(name, disabled=not allow_openssl)
|
||||||
self.check_builtin_hmac(name, disabled=not allow_builtin)
|
self.check_builtin_hmac(name, disabled=not allow_builtin)
|
||||||
|
|
||||||
|
@hashlib_helper.block_algorithm("md5")
|
||||||
|
def test_disable_hash_md5_in_fips_mode(self):
|
||||||
|
self.skip_if_not_fips_mode()
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, self.hashlib.new, "md5")
|
||||||
|
self.assertRaises(ValueError, self._hashlib.new, "md5")
|
||||||
|
self.assertRaises(ValueError, self.hashlib.md5)
|
||||||
|
self.assertRaises(ValueError, self._hashlib.openssl_md5)
|
||||||
|
|
||||||
|
kwargs = dict(usedforsecurity=True)
|
||||||
|
self.assertRaises(ValueError, self.hashlib.new, "md5", **kwargs)
|
||||||
|
self.assertRaises(ValueError, self._hashlib.new, "md5", **kwargs)
|
||||||
|
self.assertRaises(ValueError, self.hashlib.md5, **kwargs)
|
||||||
|
self.assertRaises(ValueError, self._hashlib.openssl_md5, **kwargs)
|
||||||
|
|
||||||
|
@hashlib_helper.block_algorithm("md5", allow_openssl=True)
|
||||||
|
def test_disable_hash_md5_in_fips_mode_allow_openssl(self):
|
||||||
|
self.skip_if_not_fips_mode()
|
||||||
|
# Allow the OpenSSL interface to be used but not the HACL* one.
|
||||||
|
# hashlib.new("md5") is dispatched to hashlib.openssl_md5()
|
||||||
|
self.assertRaises(ValueError, self.hashlib.new, "md5")
|
||||||
|
# dispatched to hashlib.openssl_md5() in FIPS mode
|
||||||
|
h2 = self.hashlib.new("md5", usedforsecurity=False)
|
||||||
|
self.assertIsInstance(h2, self._hashlib.HASH)
|
||||||
|
|
||||||
|
# block_algorithm() does not mock hashlib.md5 and _hashlib.openssl_md5
|
||||||
|
self.assertNotHasAttr(self.hashlib.md5, "__wrapped__")
|
||||||
|
self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__")
|
||||||
|
|
||||||
|
hashlib_md5 = inspect.unwrap(self.hashlib.md5)
|
||||||
|
self.assertIs(hashlib_md5, self._hashlib.openssl_md5)
|
||||||
|
self.assertRaises(ValueError, self.hashlib.md5)
|
||||||
|
# allow MD5 to be used in FIPS mode if usedforsecurity=False
|
||||||
|
h3 = self.hashlib.md5(usedforsecurity=False)
|
||||||
|
self.assertIsInstance(h3, self._hashlib.HASH)
|
||||||
|
|
||||||
|
@hashlib_helper.block_algorithm("md5", allow_builtin=True)
|
||||||
|
def test_disable_hash_md5_in_fips_mode_allow_builtin(self):
|
||||||
|
self.skip_if_not_fips_mode()
|
||||||
|
# Allow the HACL* interface to be used but not the OpenSSL one.
|
||||||
|
h1 = self.hashlib.new("md5") # dispatched to _md5.md5()
|
||||||
|
self.assertNotIsInstance(h1, self._hashlib.HASH)
|
||||||
|
h2 = self.hashlib.new("md5", usedforsecurity=False)
|
||||||
|
self.assertIsInstance(h2, type(h1))
|
||||||
|
|
||||||
|
# block_algorithm() mocks hashlib.md5 and _hashlib.openssl_md5
|
||||||
|
self.assertHasAttr(self.hashlib.md5, "__wrapped__")
|
||||||
|
self.assertHasAttr(self._hashlib.openssl_md5, "__wrapped__")
|
||||||
|
|
||||||
|
hashlib_md5 = inspect.unwrap(self.hashlib.md5)
|
||||||
|
openssl_md5 = inspect.unwrap(self._hashlib.openssl_md5)
|
||||||
|
self.assertIs(hashlib_md5, openssl_md5)
|
||||||
|
self.assertRaises(ValueError, self.hashlib.md5)
|
||||||
|
self.assertRaises(ValueError, self.hashlib.md5,
|
||||||
|
usedforsecurity=False)
|
||||||
|
|
||||||
|
@hashlib_helper.block_algorithm("md5",
|
||||||
|
allow_openssl=True,
|
||||||
|
allow_builtin=True)
|
||||||
|
def test_disable_hash_md5_in_fips_mode_allow_all(self):
|
||||||
|
self.skip_if_not_fips_mode()
|
||||||
|
# hashlib.new() isn't blocked as it falls back to _md5.md5
|
||||||
|
self.assertIsInstance(self.hashlib.new("md5"), self._md5.MD5Type)
|
||||||
|
self.assertRaises(ValueError, self._hashlib.new, "md5")
|
||||||
|
h = self._hashlib.new("md5", usedforsecurity=False)
|
||||||
|
self.assertIsInstance(h, self._hashlib.HASH)
|
||||||
|
|
||||||
|
self.assertNotHasAttr(self.hashlib.md5, "__wrapped__")
|
||||||
|
self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__")
|
||||||
|
|
||||||
|
self.assertIs(self.hashlib.md5, self._hashlib.openssl_md5)
|
||||||
|
self.assertRaises(ValueError, self.hashlib.md5)
|
||||||
|
h = self.hashlib.md5(usedforsecurity=False)
|
||||||
|
self.assertIsInstance(h, self._hashlib.HASH)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue