Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-127298: When in FIPS mode ensure builtin hashes check for usedforsecurity=False #127301

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
55 changes: 38 additions & 17 deletions Lib/hashlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 the _hashlib OpenSSL wrapper is in FIPS mode, wrap other implementations
# to check the usedforsecurity kwarg. All builtin implementations are treated
# as only available for useforsecurity=False purposes in the presence of such
# a configured and linked OpenSSL.
def __get_wrapped_builtin(md, name):
if __openssl_fips_mode != 0:
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)
Expand All @@ -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.

Expand Down Expand Up @@ -161,9 +178,8 @@ def __hash_new(name, data=b'', **kwargs):
except ValueError:
# If the _hashlib module (OpenSSL) doesn't support the named
# 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)
# OpenSSL may not have been compiled to support everything.
return __get_builtin_constructor(name)(data, **kwargs)


try:
Expand All @@ -172,10 +188,15 @@ def __hash_new(name, data=b'', **kwargs):
__get_hash = __get_openssl_constructor
algorithms_available = algorithms_available.union(
_hashlib.openssl_md_meth_names)
try:
__openssl_fips_mode = _hashlib.get_fips_mode()
except ValueError:
__openssl_fips_mode = 0
except ImportError:
_hashlib = None
new = __py_new
__get_hash = __get_builtin_constructor
__openssl_fips_mode = 0

try:
# OpenSSL's PKCS5_PBKDF2_HMAC requires OpenSSL 1.0+ with HMAC and SHA
Expand Down
19 changes: 19 additions & 0 deletions Lib/test/hashlibdata/openssl.cnf
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Lib/test/ssltests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
]
Expand Down
63 changes: 63 additions & 0 deletions Lib/test/test_hashlib_fips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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 sys
import unittest

OPENSSL_CONF_BACKUP = os.environ.get("OPENSSL_CONF")


class HashLibFIPSTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
if sys.modules.get("_hashlib") or sys.modules.get("_ssl"):
raise AssertionError("_hashlib or _ssl already imported, too late to change OPENSSL_CONF.")
gpshead marked this conversation as resolved.
Show resolved Hide resolved
# 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 library context
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 is not None:
os.environ["OPENSSL_CONF"] = OPENSSL_CONF_BACKUP
else:
os.environ.pop("OPENSSL_CONF", None)

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()
1 change: 1 addition & 0 deletions Makefile.pre.in
Original file line number Diff line number Diff line change
Expand Up @@ -2447,6 +2447,7 @@ TESTSUBDIRS= idlelib/idle_test \
test/decimaltestdata \
test/dtracedata \
test/encoded_modules \
test/hashlibdata \
test/leakers \
test/libregrtest \
test/mathdata \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
:mod:`hashlib`'s builtin hash implementations now check ``usedforsecurity=False``,
when the OpenSSL library default provider is in OpenSSL's FIPS mode. This helps
ensure that only US FIPS approved implementations are in use by default on systems
configured as such.

This is only active when :mod:`hashlib` has been built with OpenSSL implementation
support and said OpenSSL library includes the FIPS mode feature. Not all variants
do, and OpenSSL is not a *required* build time dependency of ``hashlib``.
Loading