From fd700e449558f4136136d8f7dd0de8427facafc7 Mon Sep 17 00:00:00 2001 From: Prashant Singh Chauhan Date: Fri, 30 Oct 2026 05:25:02 +0000 Subject: [PATCH] [3.14] gh-161383: Fix inverted FIPS property query in _hashlib In FIPS mode, usedforsecurity worked the wrong way round: usedforsecurity=False MD5 was refused, usedforsecurity=True MD5 was allowed, and hashlib.md5() fell back to the built-in _md5 without any error. disable_fips_property() returns 1 for usedforsecurity=False, but py_digest_by_name() uses 'fips' as "1 = strict FIPS lookup". main inverts the value with "!"; the 3.14 backport of gh-149816 (GH-160006) lost it. Invert it after the -1 error check. Tests: - Add a FIPS-only test: MD5 must work with usedforsecurity=False and be refused with usedforsecurity=True. - Copy check_file_digest() from main (gh-136547). The 3.14 version checks hashlib.new(name), which works through the built-in MD5, and then also calls _hashlib.openssl_md5, which FIPS refuses. With only the C fix, test_case_md5_* fail in FIPS mode. The main version skips each blocked constructor. Co-Authored-By: Claude Opus 5.5 --- Lib/test/test_hashlib.py | 30 ++++++++++++++----- ...-10-30-09-00-00.gh-issue-161383.fIpS3a.rst | 5 ++++ Modules/_hashopenssl.c | 3 ++ 3 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-30-09-00-00.gh-issue-161383.fIpS3a.rst diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py index 01f2c1ed79511a8..95a859df31deceb 100644 --- a/Lib/test/test_hashlib.py +++ b/Lib/test/test_hashlib.py @@ -277,6 +277,18 @@ def test_usedforsecurity_false(self): self._hashlib.new("md5", usedforsecurity=False) self._hashlib.openssl_md5(usedforsecurity=False) + @unittest.skipUnless(get_fips_mode(), "requires FIPS mode") + def test_md5_usedforsecurity_in_fips_mode(self): + # In FIPS mode, OpenSSL must allow MD5 only with usedforsecurity=False. + if self._hashlib is None: + self.skipTest("OpenSSL hashlib is not available") + self._hashlib.new("md5", usedforsecurity=False) + self._hashlib.openssl_md5(usedforsecurity=False) + with self.assertRaises(ValueError): + self._hashlib.new("md5", usedforsecurity=True) + with self.assertRaises(ValueError): + self._hashlib.openssl_md5(usedforsecurity=True) + @unittest.skipIf(get_fips_mode(), "skip in FIPS mode") def test_clinic_signature(self): for constructor in self.hash_constructors: @@ -512,13 +524,17 @@ def check(self, name, data, hexdigest, shake=False, **kwargs): def check_file_digest(self, name, data, hexdigest): hexdigest = hexdigest.lower() - try: - hashlib.new(name) - except ValueError: - # skip, algorithm is blocked by security policy. - return - digests = [name] - digests.extend(self.constructors_to_test[name]) + digests = [] + for digest in [name, *self.constructors_to_test[name]]: + try: + if callable(digest): + digest(b"") + else: + hashlib.new(digest) + except ValueError: + # skip, algorithm is blocked by security policy. + continue + digests.append(digest) with tempfile.TemporaryFile() as f: f.write(data) diff --git a/Misc/NEWS.d/next/Library/2026-10-30-09-00-00.gh-issue-161383.fIpS3a.rst b/Misc/NEWS.d/next/Library/2026-10-30-09-00-00.gh-issue-161383.fIpS3a.rst new file mode 100644 index 000000000000000..2ed865e0bcd9fdb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-30-09-00-00.gh-issue-161383.fIpS3a.rst @@ -0,0 +1,5 @@ +Fix :mod:`hashlib` in OpenSSL FIPS mode: the :mod:`!_hashlib` digest lookup +introduced by the fix for :gh:`149816` used the FIPS property query the wrong +way round, so ``usedforsecurity=False`` digests such as MD5 failed to load and +``usedforsecurity=True`` ones were no longer refused. Python then silently +fell back to the built-in implementation of the algorithm. diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c index 09aa2af173e7e5b..b564022f80605bb 100644 --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -421,6 +421,9 @@ py_digest_by_name(PyObject *module, const char *name, enum Py_hash_type py_ht) if (fips == -1) { goto finally; } + // 'fips' must be 1 for usedforsecurity=True: the opposite of what + // disable_fips_property() returns. + fips = !fips; if (entry != NULL) { PY_EVP_MD **cached = fips ? &entry->evp : &entry->evp_nosecurity;