Skip to content

Commit bd46651

Browse files
committed
More hashlib robustness in strange "FIPS mode" environments.
1 parent 59d9a85 commit bd46651

File tree

3 files changed

+66
-35
lines changed

3 files changed

+66
-35
lines changed

Doc/library/hashlib.rst

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,14 @@ hash supplied more than 2047 bytes of data at once in its constructor or
5555
.. index:: single: OpenSSL; (use in module hashlib)
5656

5757
Constructors for hash algorithms that are always present in this module are
58-
:func:`sha1`, :func:`sha224`, :func:`sha256`, :func:`sha384`, :func:`sha512`,
58+
:func:`md5`, :func:`sha1`, :func:`sha224`, :func:`sha256`, :func:`sha384`, :func:`sha512`,
5959
:func:`sha3_224`, :func:`sha3_256`, :func:`sha3_384`, :func:`sha3_512`,
6060
:func:`shake_128`, :func:`shake_256`, :func:`blake2b`, and :func:`blake2s`.
61-
:func:`md5` is normally available as well, though it may be missing or blocked
62-
if you are using a rare "FIPS compliant" build of Python.
61+
Some of these may be missing or blocked if you are running in an environment
62+
with OpenSSL's "FIPS mode" configured to exclude some hash algorithms from its
63+
default provider and are using a Python runtime built with that in mind. Such
64+
environments are unusual.
65+
6366
These correspond to :data:`algorithms_guaranteed`.
6467

6568
Additional algorithms may also be available if your Python distribution's
@@ -119,7 +122,7 @@ More condensed:
119122
Constructors
120123
------------
121124

122-
.. function:: new(name[, data], *, usedforsecurity=True)
125+
.. function:: new(name[, data], \*, usedforsecurity=True)
123126

124127
Is a generic constructor that takes the string *name* of the desired
125128
algorithm as its first parameter. It also exists to allow access to the
@@ -134,16 +137,16 @@ Using :func:`new` with an algorithm name:
134137
'031edd7d41651593c5fe5c006fa5752b37fddff7bc4e843aa6af0c950f4b9406'
135138

136139

137-
.. function:: md5([, data], *, usedforsecurity=True)
138-
.. function:: sha1([, data], *, usedforsecurity=True)
139-
.. function:: sha224([, data], *, usedforsecurity=True)
140-
.. function:: sha256([, data], *, usedforsecurity=True)
141-
.. function:: sha384([, data], *, usedforsecurity=True)
142-
.. function:: sha512([, data], *, usedforsecurity=True)
143-
.. function:: sha3_224([, data], *, usedforsecurity=True)
144-
.. function:: sha3_256([, data], *, usedforsecurity=True)
145-
.. function:: sha3_384([, data], *, usedforsecurity=True)
146-
.. function:: sha3_512([, data], *, usedforsecurity=True)
140+
.. function:: md5([, data], \*, usedforsecurity=True)
141+
.. function:: sha1([, data], \*, usedforsecurity=True)
142+
.. function:: sha224([, data], \*, usedforsecurity=True)
143+
.. function:: sha256([, data], \*, usedforsecurity=True)
144+
.. function:: sha384([, data], \*, usedforsecurity=True)
145+
.. function:: sha512([, data], \*, usedforsecurity=True)
146+
.. function:: sha3_224([, data], \*, usedforsecurity=True)
147+
.. function:: sha3_256([, data], \*, usedforsecurity=True)
148+
.. function:: sha3_384([, data], \*, usedforsecurity=True)
149+
.. function:: sha3_512([, data], \*, usedforsecurity=True)
147150

148151
Named constructors such as these are faster than passing an algorithm name to
149152
:func:`new`.
@@ -156,9 +159,10 @@ Hashlib provides the following constant module attributes:
156159
.. data:: algorithms_guaranteed
157160

158161
A set containing the names of the hash algorithms guaranteed to be supported
159-
by this module on all platforms. Note that 'md5' is in this list despite
160-
some upstream vendors offering an odd "FIPS compliant" Python build that
161-
excludes it.
162+
by this module on all platforms. Note that the guarnatees do not hold true
163+
in the face of vendors offering "FIPS compliant" Python builds that exclude
164+
some algorithms entirely. Similarly when OpenSSL is used and its FIPS mode
165+
configuration disables some in the default provider.
162166

163167
.. versionadded:: 3.2
164168

Lib/hashlib.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,18 @@
5555

5656
# This tuple and __get_builtin_constructor() must be modified if a new
5757
# always available algorithm is added.
58-
__always_supported = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
59-
'blake2b', 'blake2s',
60-
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
61-
'shake_128', 'shake_256')
58+
__always_supported = [
59+
'md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
60+
'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
61+
'shake_128', 'shake_256', 'blake2b', 'blake2s'
62+
]
6263

6364

6465
algorithms_guaranteed = set(__always_supported)
6566
algorithms_available = set(__always_supported)
6667

67-
__all__ = __always_supported + ('new', 'algorithms_guaranteed',
68-
'algorithms_available', 'file_digest')
68+
__all__ = __always_supported + [
69+
'new', 'algorithms_guaranteed', 'algorithms_available', 'file_digest']
6970

7071

7172
__builtin_constructor_cache = {}
@@ -243,9 +244,11 @@ def file_digest(fileobj, digest, /, *, _bufsize=2**18):
243244
# version not supporting that algorithm.
244245
try:
245246
globals()[__func_name] = __get_hash(__func_name)
246-
except ValueError:
247-
import logging
248-
logging.exception('code for hash %s was not found.', __func_name)
247+
except ValueError as exc:
248+
# Errors logged here would be seen as noise by most people.
249+
# Code using a missing hash will get an obvious exception.
250+
__all__.remove(__func_name)
251+
algorithms_available.remove(__func_name)
249252

250253

251254
# Cleanup locals()

Lib/test/test_hashlib.py

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,15 @@ def __init__(self, *args, **kwargs):
143143
# For each algorithm, test the direct constructor and the use
144144
# of hashlib.new given the algorithm name.
145145
for algorithm, constructors in self.constructors_to_test.items():
146-
constructors.add(getattr(hashlib, algorithm))
146+
if get_fips_mode():
147+
# Arbitrary algorithms may be missing via openssl.cnf
148+
try:
149+
constructor = getattr(hashlib, algorithm)
150+
except AttributeError:
151+
continue
152+
constructors.add(constructor)
153+
else:
154+
constructors.add(getattr(hashlib, algorithm))
147155
def _test_algorithm_via_hashlib_new(data=None, _alg=algorithm, **kwargs):
148156
if data is None:
149157
return hashlib.new(_alg, **kwargs)
@@ -219,15 +227,23 @@ def test_algorithms_guaranteed(self):
219227
set(_algo for _algo in self.supported_hash_names
220228
if _algo.islower()))
221229

230+
@unittest.skipIf(get_fips_mode(), reason="guaranteed algorithms may not be available in FIPS mode")
222231
def test_algorithms_available(self):
232+
print(f"{get_fips_mode()=}")
223233
self.assertTrue(set(hashlib.algorithms_guaranteed).
224-
issubset(hashlib.algorithms_available))
234+
issubset(hashlib.algorithms_available),
235+
msg=f"\n{sorted(hashlib.algorithms_guaranteed)=}\n{sorted(hashlib.algorithms_available)=}")
225236
# all available algorithms must be loadable, bpo-47101
226237
self.assertNotIn("undefined", hashlib.algorithms_available)
227238
for name in hashlib.algorithms_available:
228-
digest = hashlib.new(name, usedforsecurity=False)
239+
with self.subTest(name=name):
240+
if name in self.blakes and not _blake2:
241+
self.skipTest("requires _blake2")
242+
hashlib.new(name, usedforsecurity=False)
229243

230244
@requires_usedforsecurity
245+
@unittest.skipUnless(hasattr(hashlib, "sha256"), "sha256 unavailable")
246+
@unittest.skipUnless(hasattr(hashlib, "md5"), "md5 unavailable")
231247
def test_usedforsecurity_true(self):
232248
hashlib.new("sha256", usedforsecurity=True)
233249
for cons in self.hash_constructors:
@@ -239,6 +255,8 @@ def test_usedforsecurity_true(self):
239255
self._hashlib.new("md5", usedforsecurity=True)
240256
self._hashlib.openssl_md5(usedforsecurity=True)
241257

258+
@unittest.skipUnless(hasattr(hashlib, "sha256"), "sha256 unavailable")
259+
@unittest.skipUnless(hasattr(hashlib, "md5"), "md5 unavailable")
242260
def test_usedforsecurity_false(self):
243261
hashlib.new("sha256", usedforsecurity=False)
244262
for cons in self.hash_constructors:
@@ -254,6 +272,7 @@ def test_unknown_hash(self):
254272
self.assertRaises(ValueError, hashlib.new, 'spam spam spam spam spam')
255273
self.assertRaises(TypeError, hashlib.new, 1)
256274

275+
@unittest.skipUnless(hasattr(hashlib, "sha256"), "sha256 unavailable")
257276
def test_new_upper_to_lower(self):
258277
self.assertEqual(hashlib.new("SHA256", usedforsecurity=False).name, "sha256")
259278

@@ -387,7 +406,8 @@ def check(self, name, data, hexdigest, shake=False, **kwargs):
387406
hexdigest = hexdigest.lower()
388407
constructors = self.constructors_to_test[name]
389408
# 2 is for hashlib.name(...) and hashlib.new(name, ...)
390-
self.assertGreaterEqual(len(constructors), 2)
409+
if get_fips_mode() == 0:
410+
self.assertGreaterEqual(len(constructors), 2)
391411
for hash_object_constructor in constructors:
392412
m = hash_object_constructor(data, **kwargs)
393413
computed = m.hexdigest() if not shake else m.hexdigest(length)
@@ -407,8 +427,6 @@ def check(self, name, data, hexdigest, shake=False, **kwargs):
407427
# skip shake and blake2 extended parameter tests
408428
self.check_file_digest(name, data, hexdigest)
409429

410-
# defaults True because file_digest doesn't support the parameter.
411-
@requires_usedforsecurity
412430
def check_file_digest(self, name, data, hexdigest):
413431
hexdigest = hexdigest.lower()
414432
try:
@@ -914,6 +932,7 @@ def test_case_shake256_vector(self):
914932
for msg, md in read_vectors('shake_256'):
915933
self.check('shake_256', msg, md, True)
916934

935+
@unittest.skipUnless(hasattr(hashlib, "sha256"), "sha256 unavailable")
917936
def test_gil(self):
918937
# Check things work fine with an input larger than the size required
919938
# for multithreaded operation (which is hardwired to 2048).
@@ -928,7 +947,7 @@ def test_gil(self):
928947
m = cons(b'x' * gil_minsize, usedforsecurity=False)
929948
m.update(b'1')
930949

931-
m = hashlib.sha256()
950+
m = hashlib.sha256(usedforsecurity=False)
932951
m.update(b'1')
933952
m.update(b'#' * gil_minsize)
934953
m.update(b'1')
@@ -937,22 +956,24 @@ def test_gil(self):
937956
'1cfceca95989f51f658e3f3ffe7f1cd43726c9e088c13ee10b46f57cef135b94'
938957
)
939958

940-
m = hashlib.sha256(b'1' + b'#' * gil_minsize + b'1')
959+
m = hashlib.sha256(b'1' + b'#' * gil_minsize + b'1',
960+
usedforsecurity=False)
941961
self.assertEqual(
942962
m.hexdigest(),
943963
'1cfceca95989f51f658e3f3ffe7f1cd43726c9e088c13ee10b46f57cef135b94'
944964
)
945965

946966
@threading_helper.reap_threads
947967
@threading_helper.requires_working_threading()
968+
@unittest.skipUnless(hasattr(hashlib, "sha1"), "sha1 unavailable")
948969
def test_threaded_hashing(self):
949970
# Updating the same hash object from several threads at once
950971
# using data chunk sizes containing the same byte sequences.
951972
#
952973
# If the internal locks are working to prevent multiple
953974
# updates on the same object from running at once, the resulting
954975
# hash will be the same as doing it single threaded upfront.
955-
hasher = hashlib.sha1()
976+
hasher = hashlib.sha1(usedforsecurity=False)
956977
num_threads = 5
957978
smallest_data = b'swineflu'
958979
data = smallest_data * 200000
@@ -1174,6 +1195,9 @@ def test_normalized_name(self):
11741195
self.assertNotIn("blake2b512", hashlib.algorithms_available)
11751196
self.assertNotIn("sha3-512", hashlib.algorithms_available)
11761197

1198+
# defaults True because file_digest doesn't support the parameter.
1199+
@requires_usedforsecurity
1200+
@unittest.skipUnless(hasattr(hashlib, "sha256"), "sha256 unavailable")
11771201
def test_file_digest(self):
11781202
data = b'a' * 65536
11791203
d1 = hashlib.sha256()

0 commit comments

Comments
 (0)