Skip to content

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

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 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 _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)
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 @@ -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:
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
47 changes: 47 additions & 0 deletions Lib/test/test_hashlib_fips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 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

# 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

class HashLibFIPSTestCase(unittest.TestCase):
def __init__(self, *args, **kwargs):
try:
from _hashlib import get_fips_mode
except ImportError:
self.skipTest('_hashlib not available')

if get_fips_mode() != 1:
self.skipTest('mock fips mode setup failed')

super(HashLibFIPSTestCase, self).__init__(*args, **kwargs)

def test_algorithms_available(self):
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):
for name in hashlib.algorithms_available:
with self.assertRaises(ValueError):
digest = hashlib.new(name, usedforsecurity=True)

if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -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.
Loading