From 756cd9388bb679745cc3fda5bdd770c67f052eea Mon Sep 17 00:00:00 2001 From: Dimitri John Ledkov Date: Tue, 26 Nov 2024 15:14:31 +0000 Subject: [PATCH] gh-127298: When in FIPS mode ensure builtin hashes check for usedforsecurity=False When _hashlib/OpenSSL is available, and OpenSSL is in FIPS mode, ensure that builtin (fallback) hash implementations are wrapped with a check for usedforsecurity=False. It is likely that buitin implementations are FIPS unapproved (either algorithm disallowed; or the implementation not certified by NIST). This enables strict approved-only compliance when usedforsecurity=True on FIPS systems only. And yet it also enables fallback access with usedforsecurity=False for any missing (historical, disallowed or missing certified implementation) algorithms (i.e. blake2, md5, shake/sha3) depending on the runtime configuration of OpenSSL. --- Lib/hashlib.py | 47 ++++++++++----- Lib/test/hashlibdata/openssl.cnf | 19 ++++++ Lib/test/ssltests.py | 2 +- Lib/test/test_hashlib_fips.py | 59 +++++++++++++++++++ ...-11-26-16-31-40.gh-issue-127298.jqYJvn.rst | 4 ++ 5 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 Lib/test/hashlibdata/openssl.cnf create mode 100644 Lib/test/test_hashlib_fips.py create mode 100644 Misc/NEWS.d/next/Library/2024-11-26-16-31-40.gh-issue-127298.jqYJvn.rst diff --git a/Lib/hashlib.py b/Lib/hashlib.py index 1b2c30cc32f5647..879693f8ba64b92 100644 --- a/Lib/hashlib.py +++ b/Lib/hashlib.py @@ -79,6 +79,23 @@ 'blake2b', 'blake2s', } +# Wrapper that only allows usage when usedforsecurity=False +# (effectively unapproved service indicator) +def __usedforsecurity_check(md, name, *args, **kwargs): + if kwargs.get("usedforsecurity", True): + raise ValueError(name + " is blocked when usedforsecurity=True") + return md(*args, **kwargs) + +# If _hashlib is in FIPS mode, use the above wrapper to ensure builtin +# implementation checks usedforsecurity kwarg. It means all builtin +# implementations are treated as an unapproved implementation, as they +# are unlikely to have been certified by NIST. +def __get_wrapped_builtin(md, name): + if _hashlib is not None and _hashlib.get_fips_mode() == 1: + from functools import partial + return partial(__usedforsecurity_check, md, name) + return md + def __get_builtin_constructor(name): cache = __builtin_constructor_cache constructor = cache.get(name) @@ -87,32 +104,32 @@ def __get_builtin_constructor(name): try: if name in {'SHA1', 'sha1'}: import _sha1 - cache['SHA1'] = cache['sha1'] = _sha1.sha1 + cache['SHA1'] = cache['sha1'] = __get_wrapped_builtin(_sha1.sha1, name) elif name in {'MD5', 'md5'}: import _md5 - cache['MD5'] = cache['md5'] = _md5.md5 + cache['MD5'] = cache['md5'] = __get_wrapped_builtin(_md5.md5, name) elif name in {'SHA256', 'sha256', 'SHA224', 'sha224'}: import _sha2 - cache['SHA224'] = cache['sha224'] = _sha2.sha224 - cache['SHA256'] = cache['sha256'] = _sha2.sha256 + cache['SHA224'] = cache['sha224'] = __get_wrapped_builtin(_sha2.sha224, name) + cache['SHA256'] = cache['sha256'] = __get_wrapped_builtin(_sha2.sha256, name) elif name in {'SHA512', 'sha512', 'SHA384', 'sha384'}: import _sha2 - cache['SHA384'] = cache['sha384'] = _sha2.sha384 - cache['SHA512'] = cache['sha512'] = _sha2.sha512 + cache['SHA384'] = cache['sha384'] = __get_wrapped_builtin(_sha2.sha384, name) + cache['SHA512'] = cache['sha512'] = __get_wrapped_builtin(_sha2.sha512, name) elif name in {'blake2b', 'blake2s'}: import _blake2 - cache['blake2b'] = _blake2.blake2b - cache['blake2s'] = _blake2.blake2s + cache['blake2b'] = __get_wrapped_builtin(_blake2.blake2b, name) + cache['blake2s'] = __get_wrapped_builtin(_blake2.blake2s, name) elif name in {'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512'}: import _sha3 - cache['sha3_224'] = _sha3.sha3_224 - cache['sha3_256'] = _sha3.sha3_256 - cache['sha3_384'] = _sha3.sha3_384 - cache['sha3_512'] = _sha3.sha3_512 + cache['sha3_224'] = __get_wrapped_builtin(_sha3.sha3_224, name) + cache['sha3_256'] = __get_wrapped_builtin(_sha3.sha3_256, name) + cache['sha3_384'] = __get_wrapped_builtin(_sha3.sha3_384, name) + cache['sha3_512'] = __get_wrapped_builtin(_sha3.sha3_512, name) elif name in {'shake_128', 'shake_256'}: import _sha3 - cache['shake_128'] = _sha3.shake_128 - cache['shake_256'] = _sha3.shake_256 + cache['shake_128'] = __get_wrapped_builtin(_sha3.shake_128, name) + cache['shake_256'] = __get_wrapped_builtin(_sha3.shake_256, name) except ImportError: pass # no extension module, this hash is unsupported. @@ -163,7 +180,7 @@ def __hash_new(name, data=b'', **kwargs): # hash, try using our builtin implementations. # This allows for SHA224/256 and SHA384/512 support even though # the OpenSSL library prior to 0.9.8 doesn't provide them. - return __get_builtin_constructor(name)(data) + return __get_builtin_constructor(name)(data, **kwargs) try: diff --git a/Lib/test/hashlibdata/openssl.cnf b/Lib/test/hashlibdata/openssl.cnf new file mode 100644 index 000000000000000..9a936ddc5effb8e --- /dev/null +++ b/Lib/test/hashlibdata/openssl.cnf @@ -0,0 +1,19 @@ +# Activate base provider only, with default properties fips=yes. It +# means that fips mode is on, and no digest implementations are +# available. Perfect for mock testing builtin FIPS wrappers. + +config_diagnostics = 1 +openssl_conf = openssl_init + +[openssl_init] +providers = provider_sect +alg_section = algorithm_sect + +[provider_sect] +base = base_sect + +[base_sect] +activate = 1 + +[algorithm_sect] +default_properties = fips=yes diff --git a/Lib/test/ssltests.py b/Lib/test/ssltests.py index ee03aed5cca532f..0ecd82658601cb4 100644 --- a/Lib/test/ssltests.py +++ b/Lib/test/ssltests.py @@ -7,7 +7,7 @@ TESTS = [ 'test_asyncio', 'test_ensurepip.py', 'test_ftplib', 'test_hashlib', - 'test_hmac', 'test_httplib', 'test_imaplib', + 'test_hashlib_fips', 'test_hmac', 'test_httplib', 'test_imaplib', 'test_poplib', 'test_ssl', 'test_smtplib', 'test_smtpnet', 'test_urllib2_localnet', 'test_venv', 'test_xmlrpc' ] diff --git a/Lib/test/test_hashlib_fips.py b/Lib/test/test_hashlib_fips.py new file mode 100644 index 000000000000000..066bbc7b1cdbcdb --- /dev/null +++ b/Lib/test/test_hashlib_fips.py @@ -0,0 +1,59 @@ +# Test the hashlib module usedforsecurity wrappers under fips. +# +# Copyright (C) 2024 Dimitri John Ledkov (dimitri.ledkov@surgut.co.uk) +# Licensed to PSF under a Contributor Agreement. +# + +import os +import unittest + +OPENSSL_CONF_BACKUP = os.environ.get("OPENSSL_CONF") + +class HashLibFIPSTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + # This openssl.cnf mocks FIPS mode without any digest + # loaded. It means all digests must raise ValueError when + # usedforsecurity=True via either openssl or builtin + # constructors + OPENSSL_CONF = os.path.join(os.path.dirname(__file__), "hashlibdata", "openssl.cnf") + os.environ["OPENSSL_CONF"] = OPENSSL_CONF + # Ensure hashlib is loading a fresh libcrypto with openssl context + # affected by the above config file. Check if this can be + # folded into test_hashlib.py, specifically if + # import_fresh_module() results in a fresh Open + import hashlib + + def setUp(self): + try: + from _hashlib import get_fips_mode + except ImportError: + self.skipTest('_hashlib not available') + + if get_fips_mode() != 1: + self.skipTest('mocking fips mode failed') + + @classmethod + def tearDownClass(cls): + if OPENSSL_CONF_BACKUP: + os.environ["OPENSSL_CONF"] = OPENSSL_CONF_BACKUP + else: + del(os.environ["OPENSSL_CONF"]) + + def test_algorithms_available(self): + import hashlib + self.assertTrue(set(hashlib.algorithms_guaranteed). + issubset(hashlib.algorithms_available)) + # all available algorithms must be loadable, bpo-47101 + self.assertNotIn("undefined", hashlib.algorithms_available) + for name in hashlib.algorithms_available: + digest = hashlib.new(name, usedforsecurity=False) + + def test_usedforsecurity_true(self): + import hashlib + for name in hashlib.algorithms_available: + with self.assertRaises(ValueError): + digest = hashlib.new(name, usedforsecurity=True) + +if __name__ == "__main__": + unittest.main() 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 new file mode 100644 index 000000000000000..99ef85f7f5b1f9a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-11-26-16-31-40.gh-issue-127298.jqYJvn.rst @@ -0,0 +1,4 @@ +:mod:`hashlib`'s fallback builtin hash implementations now check +usedforsecurity=True, if hashlib is in FIPS mode. This ensures that +approved-only implementations are in use on FIPS systems by default, +and fallback ones are made available for unapproved purposes only.