Skip to content

Commit a9ea6ad

Browse files
committed
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.
1 parent 6f5a82f commit a9ea6ad

File tree

3 files changed

+136
-42
lines changed

3 files changed

+136
-42
lines changed

Lib/hashlib.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,22 @@ def __get_openssl_constructor(name):
128128
# Prefer our builtin blake2 implementation.
129129
return __get_builtin_constructor(name)
130130
try:
131-
# MD5, SHA1, and SHA2 are in all supported OpenSSL versions
132-
# SHA3/shake are available in OpenSSL 1.1.1+
131+
# Fetch the OpenSSL hash function if it exists,
132+
# independently of the context security policy.
133133
f = getattr(_hashlib, 'openssl_' + name)
134-
# Allow the C module to raise ValueError. The function will be
135-
# defined but the hash not actually available. Don't fall back to
136-
# builtin if the current security policy blocks a digest, bpo#40695.
134+
# Check if the context security policy blocks the digest or not
135+
# by allowing the C module to raise a ValueError. The function
136+
# will be defined but the hash will not be available at runtime.
137+
#
138+
# We use "usedforsecurity=False" to prevent falling back to the
139+
# built-in function in case the security policy does not allow it.
140+
#
141+
# Note that this only affects the explicit named constructors,
142+
# and not the algorithms exposed through hashlib.new() which
143+
# can still be resolved to a built-in function even if the
144+
# current security policy does not allow it.
145+
#
146+
# See https://github.com/python/cpython/issues/84872.
137147
f(usedforsecurity=False)
138148
# Use the C function directly (very fast)
139149
return f

Lib/test/test_hashlib.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -466,13 +466,17 @@ def check(self, name, data, hexdigest, shake=False, **kwargs):
466466

467467
def check_file_digest(self, name, data, hexdigest):
468468
hexdigest = hexdigest.lower()
469-
try:
470-
hashlib.new(name)
471-
except ValueError:
472-
# skip, algorithm is blocked by security policy.
473-
return
474-
digests = [name]
475-
digests.extend(self.constructors_to_test[name])
469+
digests = []
470+
for digest in [name, *self.constructors_to_test[name]]:
471+
try:
472+
if callable(digest):
473+
digest(b"")
474+
else:
475+
hashlib.new(digest)
476+
except ValueError:
477+
# skip, algorithm is blocked by security policy.
478+
continue
479+
digests.append(digest)
476480

477481
with open(os_helper.TESTFN, "wb") as f:
478482
f.write(data)

Lib/test/test_support.py

Lines changed: 110 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import errno
33
import importlib
44
import itertools
5+
import inspect
56
import io
67
import logging
78
import os
@@ -798,6 +799,7 @@ def test_linked_to_musl(self):
798799
# SuppressCrashReport
799800

800801

802+
@hashlib_helper.requires_builtin_hashes()
801803
class TestHashlibSupport(unittest.TestCase):
802804

803805
@classmethod
@@ -806,11 +808,20 @@ def setUpClass(cls):
806808
cls.hashlib = import_helper.import_module("hashlib")
807809
cls.hmac = import_helper.import_module("hmac")
808810

809-
# We required the extension modules to be present since blocking
810-
# HACL* implementations while allowing OpenSSL ones would still
811-
# result in failures.
811+
# All C extension modules must be present since blocking
812+
# the built-in implementation while allowing OpenSSL or vice-versa
813+
# may result in failures depending on the exposed built-in hashes.
812814
cls._hashlib = import_helper.import_module("_hashlib")
813815
cls._hmac = import_helper.import_module("_hmac")
816+
cls._md5 = import_helper.import_module("_md5")
817+
818+
def skip_if_fips_mode(self):
819+
if self._hashlib.get_fips_mode():
820+
self.skipTest("disabled in FIPS mode")
821+
822+
def skip_if_not_fips_mode(self):
823+
if not self._hashlib.get_fips_mode():
824+
self.skipTest("requires FIPS mode")
814825

815826
def check_context(self, disabled=True):
816827
if disabled:
@@ -831,25 +842,19 @@ def try_import_attribute(self, fullname, default=None):
831842
except TypeError:
832843
return default
833844

834-
def validate_modules(self):
835-
if hasattr(hashlib_helper, 'hashlib'):
836-
self.assertIs(hashlib_helper.hashlib, self.hashlib)
837-
if hasattr(hashlib_helper, 'hmac'):
838-
self.assertIs(hashlib_helper.hmac, self.hmac)
839-
840-
def fetch_hash_function(self, name, typ):
841-
entry = hashlib_helper._EXPLICIT_CONSTRUCTORS[name]
842-
match typ:
845+
def fetch_hash_function(self, name, implementation):
846+
info = hashlib_helper.get_hash_info(name)
847+
match implementation:
843848
case "hashlib":
844-
assert entry.hashlib is not None, entry
845-
return getattr(self.hashlib, entry.hashlib)
849+
assert info.hashlib is not None, info
850+
return getattr(self.hashlib, info.hashlib)
846851
case "openssl":
847852
try:
848-
return getattr(self._hashlib, entry.openssl, None)
853+
return getattr(self._hashlib, info.openssl, None)
849854
except TypeError:
850855
return None
851-
case "builtin":
852-
return self.try_import_attribute(entry.fullname(typ))
856+
fullname = info.fullname(implementation)
857+
return self.try_import_attribute(fullname)
853858

854859
def fetch_hmac_function(self, name):
855860
fullname = hashlib_helper._EXPLICIT_HMAC_CONSTRUCTORS[name]
@@ -914,16 +919,12 @@ def check_builtin_hmac(self, name, *, disabled=True):
914919
)
915920
def test_disable_hash(self, name, allow_openssl, allow_builtin):
916921
# In FIPS mode, the function may be available but would still need
917-
# to raise a ValueError. For simplicity, we don't test the helper
918-
# when we're in FIPS mode.
919-
if self._hashlib.get_fips_mode():
920-
self.skipTest("hash functions may still be blocked in FIPS mode")
922+
# to raise a ValueError, so we will test the helper separately.
923+
self.skip_if_fips_mode()
921924
flags = dict(allow_openssl=allow_openssl, allow_builtin=allow_builtin)
922-
is_simple_disabled = not allow_builtin and not allow_openssl
925+
is_fully_disabled = not allow_builtin and not allow_openssl
923926

924927
with hashlib_helper.block_algorithm(name, **flags):
925-
self.validate_modules()
926-
927928
# OpenSSL's blake2s and blake2b are unknown names
928929
# when only the OpenSSL interface is available.
929930
if allow_openssl and not allow_builtin:
@@ -932,25 +933,104 @@ def test_disable_hash(self, name, allow_openssl, allow_builtin):
932933
else:
933934
name_for_hashlib_new = name
934935

935-
with self.check_context(is_simple_disabled):
936+
with self.check_context(is_fully_disabled):
936937
_ = self.hashlib.new(name_for_hashlib_new)
937-
with self.check_context(is_simple_disabled):
938-
_ = getattr(self.hashlib, name)(b"")
938+
939+
# Since _hashlib is present, explicit blake2b/blake2s constructors
940+
# use the built-in implementation, while others (since we are not
941+
# in FIPS mode and since _hashlib exists) use the OpenSSL function.
942+
with self.check_context(is_fully_disabled):
943+
_ = getattr(self.hashlib, name)()
939944

940945
self.check_openssl_hash(name, disabled=not allow_openssl)
941946
self.check_builtin_hash(name, disabled=not allow_builtin)
942947

943948
if name not in hashlib_helper.NON_HMAC_DIGEST_NAMES:
944-
with self.check_context(is_simple_disabled):
949+
with self.check_context(is_fully_disabled):
945950
_ = self.hmac.new(b"", b"", name)
946-
with self.check_context(is_simple_disabled):
951+
with self.check_context(is_fully_disabled):
947952
_ = self.hmac.HMAC(b"", b"", name)
948-
with self.check_context(is_simple_disabled):
953+
with self.check_context(is_fully_disabled):
949954
_ = self.hmac.digest(b"", b"", name)
950955

951956
self.check_openssl_hmac(name, disabled=not allow_openssl)
952957
self.check_builtin_hmac(name, disabled=not allow_builtin)
953958

959+
@hashlib_helper.block_algorithm("md5")
960+
def test_disable_hash_md5_in_fips_mode(self):
961+
self.skip_if_not_fips_mode()
962+
963+
self.assertRaises(ValueError, self.hashlib.new, "md5")
964+
self.assertRaises(ValueError, self._hashlib.new, "md5")
965+
self.assertRaises(ValueError, self.hashlib.md5)
966+
self.assertRaises(ValueError, self._hashlib.openssl_md5)
967+
968+
kwargs = dict(usedforsecurity=True)
969+
self.assertRaises(ValueError, self.hashlib.new, "md5", **kwargs)
970+
self.assertRaises(ValueError, self._hashlib.new, "md5", **kwargs)
971+
self.assertRaises(ValueError, self.hashlib.md5, **kwargs)
972+
self.assertRaises(ValueError, self._hashlib.openssl_md5, **kwargs)
973+
974+
@hashlib_helper.block_algorithm("md5", allow_openssl=True)
975+
def test_disable_hash_md5_in_fips_mode_allow_openssl(self):
976+
self.skip_if_not_fips_mode()
977+
# Allow the OpenSSL interface to be used but not the HACL* one.
978+
# hashlib.new("md5") is dispatched to hashlib.openssl_md5()
979+
self.assertRaises(ValueError, self.hashlib.new, "md5")
980+
# dispatched to hashlib.openssl_md5() in FIPS mode
981+
h2 = self.hashlib.new("md5", usedforsecurity=False)
982+
self.assertIsInstance(h2, self._hashlib.HASH)
983+
984+
# block_algorithm() does not mock hashlib.md5 and _hashlib.openssl_md5
985+
self.assertNotHasAttr(self.hashlib.md5, "__wrapped__")
986+
self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__")
987+
988+
hashlib_md5 = inspect.unwrap(self.hashlib.md5)
989+
self.assertIs(hashlib_md5, self._hashlib.openssl_md5)
990+
self.assertRaises(ValueError, self.hashlib.md5)
991+
# allow MD5 to be used in FIPS mode if usedforsecurity=False
992+
h3 = self.hashlib.md5(usedforsecurity=False)
993+
self.assertIsInstance(h3, self._hashlib.HASH)
994+
995+
@hashlib_helper.block_algorithm("md5", allow_builtin=True)
996+
def test_disable_hash_md5_in_fips_mode_allow_builtin(self):
997+
self.skip_if_not_fips_mode()
998+
# Allow the HACL* interface to be used but not the OpenSSL one.
999+
h1 = self.hashlib.new("md5") # dispatched to _md5.md5()
1000+
self.assertNotIsInstance(h1, self._hashlib.HASH)
1001+
h2 = self.hashlib.new("md5", usedforsecurity=False)
1002+
self.assertIsInstance(h2, type(h1))
1003+
1004+
# block_algorithm() mocks hashlib.md5 and _hashlib.openssl_md5
1005+
self.assertHasAttr(self.hashlib.md5, "__wrapped__")
1006+
self.assertHasAttr(self._hashlib.openssl_md5, "__wrapped__")
1007+
1008+
hashlib_md5 = inspect.unwrap(self.hashlib.md5)
1009+
openssl_md5 = inspect.unwrap(self._hashlib.openssl_md5)
1010+
self.assertIs(hashlib_md5, openssl_md5)
1011+
self.assertRaises(ValueError, self.hashlib.md5)
1012+
self.assertRaises(ValueError, self.hashlib.md5,
1013+
usedforsecurity=False)
1014+
1015+
@hashlib_helper.block_algorithm("md5",
1016+
allow_openssl=True,
1017+
allow_builtin=True)
1018+
def test_disable_hash_md5_in_fips_mode_allow_all(self):
1019+
self.skip_if_not_fips_mode()
1020+
# hashlib.new() isn't blocked as it falls back to _md5.md5
1021+
self.assertIsInstance(self.hashlib.new("md5"), self._md5.MD5Type)
1022+
self.assertRaises(ValueError, self._hashlib.new, "md5")
1023+
h = self._hashlib.new("md5", usedforsecurity=False)
1024+
self.assertIsInstance(h, self._hashlib.HASH)
1025+
1026+
self.assertNotHasAttr(self.hashlib.md5, "__wrapped__")
1027+
self.assertNotHasAttr(self._hashlib.openssl_md5, "__wrapped__")
1028+
1029+
self.assertIs(self.hashlib.md5, self._hashlib.openssl_md5)
1030+
self.assertRaises(ValueError, self.hashlib.md5)
1031+
h = self.hashlib.md5(usedforsecurity=False)
1032+
self.assertIsInstance(h, self._hashlib.HASH)
1033+
9541034

9551035
if __name__ == '__main__':
9561036
unittest.main()

0 commit comments

Comments
 (0)