|
| 1 | +Upstream: https://github.com/python/cpython/pull/127301 |
| 2 | + |
| 3 | +diff --git a/Lib/hashlib.py b/Lib/hashlib.py |
| 4 | +index 1b2c30cc32f..b71fe5eb90e 100644 |
| 5 | +--- a/Lib/hashlib.py |
| 6 | ++++ b/Lib/hashlib.py |
| 7 | +@@ -79,6 +79,23 @@ |
| 8 | + 'blake2b', 'blake2s', |
| 9 | + } |
| 10 | + |
| 11 | ++# Wrapper that only allows usage when usedforsecurity=False |
| 12 | ++# (effectively unapproved service indicator) |
| 13 | ++def __usedforsecurity_check(md, name, *args, **kwargs): |
| 14 | ++ if kwargs.get("usedforsecurity", True): |
| 15 | ++ raise ValueError(name + " is blocked when usedforsecurity=True") |
| 16 | ++ return md(*args, **kwargs) |
| 17 | ++ |
| 18 | ++# If the _hashlib OpenSSL wrapper is in FIPS mode, wrap other implementations |
| 19 | ++# to check the usedforsecurity kwarg. All builtin implementations are treated |
| 20 | ++# as only available for useforsecurity=False purposes in the presence of such |
| 21 | ++# a configured and linked OpenSSL. |
| 22 | ++def __get_wrapped_builtin(md, name): |
| 23 | ++ if __openssl_fips_mode != 0: |
| 24 | ++ from functools import partial |
| 25 | ++ return partial(__usedforsecurity_check, md, name) |
| 26 | ++ return md |
| 27 | ++ |
| 28 | + def __get_builtin_constructor(name): |
| 29 | + cache = __builtin_constructor_cache |
| 30 | + constructor = cache.get(name) |
| 31 | +@@ -87,32 +104,32 @@ def __get_builtin_constructor(name): |
| 32 | + try: |
| 33 | + if name in {'SHA1', 'sha1'}: |
| 34 | + import _sha1 |
| 35 | +- cache['SHA1'] = cache['sha1'] = _sha1.sha1 |
| 36 | ++ cache['SHA1'] = cache['sha1'] = __get_wrapped_builtin(_sha1.sha1, name) |
| 37 | + elif name in {'MD5', 'md5'}: |
| 38 | + import _md5 |
| 39 | +- cache['MD5'] = cache['md5'] = _md5.md5 |
| 40 | ++ cache['MD5'] = cache['md5'] = __get_wrapped_builtin(_md5.md5, name) |
| 41 | + elif name in {'SHA256', 'sha256', 'SHA224', 'sha224'}: |
| 42 | + import _sha2 |
| 43 | +- cache['SHA224'] = cache['sha224'] = _sha2.sha224 |
| 44 | +- cache['SHA256'] = cache['sha256'] = _sha2.sha256 |
| 45 | ++ cache['SHA224'] = cache['sha224'] = __get_wrapped_builtin(_sha2.sha224, name) |
| 46 | ++ cache['SHA256'] = cache['sha256'] = __get_wrapped_builtin(_sha2.sha256, name) |
| 47 | + elif name in {'SHA512', 'sha512', 'SHA384', 'sha384'}: |
| 48 | + import _sha2 |
| 49 | +- cache['SHA384'] = cache['sha384'] = _sha2.sha384 |
| 50 | +- cache['SHA512'] = cache['sha512'] = _sha2.sha512 |
| 51 | ++ cache['SHA384'] = cache['sha384'] = __get_wrapped_builtin(_sha2.sha384, name) |
| 52 | ++ cache['SHA512'] = cache['sha512'] = __get_wrapped_builtin(_sha2.sha512, name) |
| 53 | + elif name in {'blake2b', 'blake2s'}: |
| 54 | + import _blake2 |
| 55 | +- cache['blake2b'] = _blake2.blake2b |
| 56 | +- cache['blake2s'] = _blake2.blake2s |
| 57 | ++ cache['blake2b'] = __get_wrapped_builtin(_blake2.blake2b, name) |
| 58 | ++ cache['blake2s'] = __get_wrapped_builtin(_blake2.blake2s, name) |
| 59 | + elif name in {'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512'}: |
| 60 | + import _sha3 |
| 61 | +- cache['sha3_224'] = _sha3.sha3_224 |
| 62 | +- cache['sha3_256'] = _sha3.sha3_256 |
| 63 | +- cache['sha3_384'] = _sha3.sha3_384 |
| 64 | +- cache['sha3_512'] = _sha3.sha3_512 |
| 65 | ++ cache['sha3_224'] = __get_wrapped_builtin(_sha3.sha3_224, name) |
| 66 | ++ cache['sha3_256'] = __get_wrapped_builtin(_sha3.sha3_256, name) |
| 67 | ++ cache['sha3_384'] = __get_wrapped_builtin(_sha3.sha3_384, name) |
| 68 | ++ cache['sha3_512'] = __get_wrapped_builtin(_sha3.sha3_512, name) |
| 69 | + elif name in {'shake_128', 'shake_256'}: |
| 70 | + import _sha3 |
| 71 | +- cache['shake_128'] = _sha3.shake_128 |
| 72 | +- cache['shake_256'] = _sha3.shake_256 |
| 73 | ++ cache['shake_128'] = __get_wrapped_builtin(_sha3.shake_128, name) |
| 74 | ++ cache['shake_256'] = __get_wrapped_builtin(_sha3.shake_256, name) |
| 75 | + except ImportError: |
| 76 | + pass # no extension module, this hash is unsupported. |
| 77 | + |
| 78 | +@@ -161,9 +178,8 @@ def __hash_new(name, data=b'', **kwargs): |
| 79 | + except ValueError: |
| 80 | + # If the _hashlib module (OpenSSL) doesn't support the named |
| 81 | + # hash, try using our builtin implementations. |
| 82 | +- # This allows for SHA224/256 and SHA384/512 support even though |
| 83 | +- # the OpenSSL library prior to 0.9.8 doesn't provide them. |
| 84 | +- return __get_builtin_constructor(name)(data) |
| 85 | ++ # OpenSSL may not have been compiled to support everything. |
| 86 | ++ return __get_builtin_constructor(name)(data, **kwargs) |
| 87 | + |
| 88 | + |
| 89 | + try: |
| 90 | +@@ -172,10 +188,15 @@ def __hash_new(name, data=b'', **kwargs): |
| 91 | + __get_hash = __get_openssl_constructor |
| 92 | + algorithms_available = algorithms_available.union( |
| 93 | + _hashlib.openssl_md_meth_names) |
| 94 | ++ try: |
| 95 | ++ __openssl_fips_mode = _hashlib.get_fips_mode() |
| 96 | ++ except ValueError: |
| 97 | ++ __openssl_fips_mode = 0 |
| 98 | + except ImportError: |
| 99 | + _hashlib = None |
| 100 | + new = __py_new |
| 101 | + __get_hash = __get_builtin_constructor |
| 102 | ++ __openssl_fips_mode = 0 |
| 103 | + |
| 104 | + try: |
| 105 | + # OpenSSL's PKCS5_PBKDF2_HMAC requires OpenSSL 1.0+ with HMAC and SHA |
| 106 | +diff --git a/Lib/test/_test_hashlib_fips.py b/Lib/test/_test_hashlib_fips.py |
| 107 | +new file mode 100644 |
| 108 | +index 00000000000..92537245954 |
| 109 | +--- /dev/null |
| 110 | ++++ b/Lib/test/_test_hashlib_fips.py |
| 111 | +@@ -0,0 +1,64 @@ |
| 112 | ++# Test the hashlib module usedforsecurity wrappers under fips. |
| 113 | ++# |
| 114 | ++# Copyright (C) 2024 Dimitri John Ledkov ([email protected]) |
| 115 | ++# Licensed to PSF under a Contributor Agreement. |
| 116 | ++# |
| 117 | ++ |
| 118 | ++"""Primarily executed by test_hashlib.py. It can run stand alone by humans.""" |
| 119 | ++ |
| 120 | ++import os |
| 121 | ++import unittest |
| 122 | ++ |
| 123 | ++OPENSSL_CONF_BACKUP = os.environ.get("OPENSSL_CONF") |
| 124 | ++ |
| 125 | ++ |
| 126 | ++class HashLibFIPSTestCase(unittest.TestCase): |
| 127 | ++ @classmethod |
| 128 | ++ def setUpClass(cls): |
| 129 | ++ # This openssl.cnf mocks FIPS mode without any digest |
| 130 | ++ # loaded. It means all digests must raise ValueError when |
| 131 | ++ # usedforsecurity=True via either openssl or builtin |
| 132 | ++ # constructors |
| 133 | ++ OPENSSL_CONF = os.path.join(os.path.dirname(__file__), "hashlibdata", "openssl.cnf") |
| 134 | ++ os.environ["OPENSSL_CONF"] = OPENSSL_CONF |
| 135 | ++ # Ensure hashlib is loading a fresh libcrypto with openssl |
| 136 | ++ # context affected by the above config file. Check if this can |
| 137 | ++ # be folded into test_hashlib.py, specifically if |
| 138 | ++ # import_fresh_module() results in a fresh library context |
| 139 | ++ import hashlib |
| 140 | ++ |
| 141 | ++ def setUp(self): |
| 142 | ++ try: |
| 143 | ++ from _hashlib import get_fips_mode |
| 144 | ++ except ImportError: |
| 145 | ++ self.skipTest('_hashlib not available') |
| 146 | ++ |
| 147 | ++ if get_fips_mode() != 1: |
| 148 | ++ self.skipTest('mocking fips mode failed') |
| 149 | ++ |
| 150 | ++ @classmethod |
| 151 | ++ def tearDownClass(cls): |
| 152 | ++ if OPENSSL_CONF_BACKUP is not None: |
| 153 | ++ os.environ["OPENSSL_CONF"] = OPENSSL_CONF_BACKUP |
| 154 | ++ else: |
| 155 | ++ os.environ.pop("OPENSSL_CONF", None) |
| 156 | ++ |
| 157 | ++ def test_algorithms_available(self): |
| 158 | ++ import hashlib |
| 159 | ++ self.assertTrue(set(hashlib.algorithms_guaranteed). |
| 160 | ++ issubset(hashlib.algorithms_available)) |
| 161 | ++ # all available algorithms must be loadable, bpo-47101 |
| 162 | ++ self.assertNotIn("undefined", hashlib.algorithms_available) |
| 163 | ++ for name in hashlib.algorithms_available: |
| 164 | ++ with self.subTest(name): |
| 165 | ++ digest = hashlib.new(name, usedforsecurity=False) |
| 166 | ++ |
| 167 | ++ def test_usedforsecurity_true(self): |
| 168 | ++ import hashlib |
| 169 | ++ for name in hashlib.algorithms_available: |
| 170 | ++ with self.subTest(name): |
| 171 | ++ with self.assertRaises(ValueError): |
| 172 | ++ digest = hashlib.new(name, usedforsecurity=True) |
| 173 | ++ |
| 174 | ++if __name__ == "__main__": |
| 175 | ++ unittest.main() |
| 176 | +diff --git a/Lib/test/hashlibdata/openssl.cnf b/Lib/test/hashlibdata/openssl.cnf |
| 177 | +new file mode 100644 |
| 178 | +index 00000000000..9a936ddc5ef |
| 179 | +--- /dev/null |
| 180 | ++++ b/Lib/test/hashlibdata/openssl.cnf |
| 181 | +@@ -0,0 +1,19 @@ |
| 182 | ++# Activate base provider only, with default properties fips=yes. It |
| 183 | ++# means that fips mode is on, and no digest implementations are |
| 184 | ++# available. Perfect for mock testing builtin FIPS wrappers. |
| 185 | ++ |
| 186 | ++config_diagnostics = 1 |
| 187 | ++openssl_conf = openssl_init |
| 188 | ++ |
| 189 | ++[openssl_init] |
| 190 | ++providers = provider_sect |
| 191 | ++alg_section = algorithm_sect |
| 192 | ++ |
| 193 | ++[provider_sect] |
| 194 | ++base = base_sect |
| 195 | ++ |
| 196 | ++[base_sect] |
| 197 | ++activate = 1 |
| 198 | ++ |
| 199 | ++[algorithm_sect] |
| 200 | ++default_properties = fips=yes |
| 201 | +diff --git a/Lib/test/support/script_helper.py b/Lib/test/support/script_helper.py |
| 202 | +index 46ce950433d..7b83c8e0632 100644 |
| 203 | +--- a/Lib/test/support/script_helper.py |
| 204 | ++++ b/Lib/test/support/script_helper.py |
| 205 | +@@ -303,7 +303,14 @@ def make_zip_pkg(zip_dir, zip_basename, pkg_name, script_basename, |
| 206 | + |
| 207 | + |
| 208 | + @support.requires_subprocess() |
| 209 | +-def run_test_script(script): |
| 210 | ++def run_test_script(script, **kwargs): |
| 211 | ++ """Run the file *script* in a child interpreter. |
| 212 | ++ |
| 213 | ++ Keyword arguments are passed on to subprocess.run() within. |
| 214 | ++ |
| 215 | ++ Asserts if the child exits non-zero. Prints child output after |
| 216 | ++ execution when run in verbose mode. |
| 217 | ++ """ |
| 218 | + # use -u to try to get the full output if the test hangs or crash |
| 219 | + if support.verbose: |
| 220 | + def title(text): |
| 221 | +@@ -315,7 +322,7 @@ def title(text): |
| 222 | + # In verbose mode, the child process inherit stdout and stdout, |
| 223 | + # to see output in realtime and reduce the risk of losing output. |
| 224 | + args = [sys.executable, "-E", "-X", "faulthandler", "-u", script, "-v"] |
| 225 | +- proc = subprocess.run(args) |
| 226 | ++ proc = subprocess.run(args, **kwargs) |
| 227 | + print(title(f"{name} completed: exit code {proc.returncode}"), |
| 228 | + flush=True) |
| 229 | + if proc.returncode: |
| 230 | +diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py |
| 231 | +index 575b2cd0da7..f1ff7c33004 100644 |
| 232 | +--- a/Lib/test/test_hashlib.py |
| 233 | ++++ b/Lib/test/test_hashlib.py |
| 234 | +@@ -21,6 +21,7 @@ |
| 235 | + from test.support.import_helper import import_fresh_module |
| 236 | + from test.support import os_helper |
| 237 | + from test.support import requires_resource |
| 238 | ++from test.support import script_helper |
| 239 | + from test.support import threading_helper |
| 240 | + from http.client import HTTPException |
| 241 | + |
| 242 | +@@ -1196,6 +1197,18 @@ def test_file_digest(self): |
| 243 | + with open(os_helper.TESTFN, "wb") as f: |
| 244 | + hashlib.file_digest(f, "sha256") |
| 245 | + |
| 246 | ++ def test_builtins_in_openssl_fips_mode(self): |
| 247 | ++ try: |
| 248 | ++ from _hashlib import get_fips_mode |
| 249 | ++ except ImportError: |
| 250 | ++ self.skipTest('OpenSSL _hashlib not available') |
| 251 | ++ from test import _test_hashlib_fips |
| 252 | ++ child_test_path = _test_hashlib_fips.__file__ |
| 253 | ++ env = os.environ.copy() |
| 254 | ++ # A config to mock FIPS mode, see _test_hashlib_fips.py. |
| 255 | ++ env["OPENSSL_CONF"] = os.path.join(os.path.dirname(__file__), "hashlibdata", "openssl.cnf") |
| 256 | ++ script_helper.run_test_script(child_test_path, env=env) |
| 257 | ++ |
| 258 | + |
| 259 | + if __name__ == "__main__": |
| 260 | + unittest.main() |
| 261 | +diff --git a/Makefile.pre.in b/Makefile.pre.in |
| 262 | +index 8d94ba361fd..908717f1791 100644 |
| 263 | +--- a/Makefile.pre.in |
| 264 | ++++ b/Makefile.pre.in |
| 265 | +@@ -2447,6 +2447,7 @@ TESTSUBDIRS= idlelib/idle_test \ |
| 266 | + test/decimaltestdata \ |
| 267 | + test/dtracedata \ |
| 268 | + test/encoded_modules \ |
| 269 | ++ test/hashlibdata \ |
| 270 | + test/leakers \ |
| 271 | + test/libregrtest \ |
| 272 | + test/mathdata \ |
| 273 | +diff --git a/Misc/NEWS.d/next/Library/2024-11-26-16-31-40.gh-issue-127298.jqYJvn.rst b/Misc/NEWS.d/next/Library/2024-11-26-16-31-40.gh-issue-127298.jqYJvn.rst |
| 274 | +new file mode 100644 |
| 275 | +index 00000000000..e555661a195 |
| 276 | +--- /dev/null |
| 277 | ++++ b/Misc/NEWS.d/next/Library/2024-11-26-16-31-40.gh-issue-127298.jqYJvn.rst |
| 278 | +@@ -0,0 +1,8 @@ |
| 279 | ++:mod:`hashlib`'s builtin hash implementations now check ``usedforsecurity=False``, |
| 280 | ++when the OpenSSL library default provider is in OpenSSL's FIPS mode. This helps |
| 281 | ++ensure that only US FIPS approved implementations are in use by default on systems |
| 282 | ++configured as such. |
| 283 | ++ |
| 284 | ++This is only active when :mod:`hashlib` has been built with OpenSSL implementation |
| 285 | ++support and said OpenSSL library includes the FIPS mode feature. Not all variants |
| 286 | ++do, and OpenSSL is not a *required* build time dependency of ``hashlib``. |
0 commit comments