Skip to content

Commit ad0fab8

Browse files
committed
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.
1 parent 2b0e2b2 commit ad0fab8

File tree

6 files changed

+116
-16
lines changed

6 files changed

+116
-16
lines changed

Lib/hashlib.py

+32-15
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,23 @@
7979
'blake2b', 'blake2s',
8080
}
8181

82+
# Wrapper that only allows usage when usedforsecurity=False
83+
# (effectively unapproved service indicator)
84+
def __usedforsecurity_check(md, name, *args, **kwargs):
85+
if kwargs.get("usedforsecurity", True):
86+
raise ValueError(name + " is blocked when usedforsecurity=True")
87+
return md(*args, **kwargs)
88+
89+
# If _hashlib is in FIPS mode, use the above wrapper to ensure builtin
90+
# implementation checks usedforsecurity kwarg. It means all builtin
91+
# implementations are treated as an unapproved implementation, as they
92+
# are unlikely to have been certified by NIST.
93+
def __get_wrapped_builtin(md, name):
94+
if _hashlib is not None and _hashlib.get_fips_mode() == 1:
95+
from functools import partial
96+
return partial(__usedforsecurity_check, md, name)
97+
return md
98+
8299
def __get_builtin_constructor(name):
83100
cache = __builtin_constructor_cache
84101
constructor = cache.get(name)
@@ -87,32 +104,32 @@ def __get_builtin_constructor(name):
87104
try:
88105
if name in {'SHA1', 'sha1'}:
89106
import _sha1
90-
cache['SHA1'] = cache['sha1'] = _sha1.sha1
107+
cache['SHA1'] = cache['sha1'] = __get_wrapped_builtin(_sha1.sha1, name)
91108
elif name in {'MD5', 'md5'}:
92109
import _md5
93-
cache['MD5'] = cache['md5'] = _md5.md5
110+
cache['MD5'] = cache['md5'] = __get_wrapped_builtin(_md5.md5, name)
94111
elif name in {'SHA256', 'sha256', 'SHA224', 'sha224'}:
95112
import _sha2
96-
cache['SHA224'] = cache['sha224'] = _sha2.sha224
97-
cache['SHA256'] = cache['sha256'] = _sha2.sha256
113+
cache['SHA224'] = cache['sha224'] = __get_wrapped_builtin(_sha2.sha224, name)
114+
cache['SHA256'] = cache['sha256'] = __get_wrapped_builtin(_sha2.sha256, name)
98115
elif name in {'SHA512', 'sha512', 'SHA384', 'sha384'}:
99116
import _sha2
100-
cache['SHA384'] = cache['sha384'] = _sha2.sha384
101-
cache['SHA512'] = cache['sha512'] = _sha2.sha512
117+
cache['SHA384'] = cache['sha384'] = __get_wrapped_builtin(_sha2.sha384, name)
118+
cache['SHA512'] = cache['sha512'] = __get_wrapped_builtin(_sha2.sha512, name)
102119
elif name in {'blake2b', 'blake2s'}:
103120
import _blake2
104-
cache['blake2b'] = _blake2.blake2b
105-
cache['blake2s'] = _blake2.blake2s
121+
cache['blake2b'] = __get_wrapped_builtin(_blake2.blake2b, name)
122+
cache['blake2s'] = __get_wrapped_builtin(_blake2.blake2s, name)
106123
elif name in {'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512'}:
107124
import _sha3
108-
cache['sha3_224'] = _sha3.sha3_224
109-
cache['sha3_256'] = _sha3.sha3_256
110-
cache['sha3_384'] = _sha3.sha3_384
111-
cache['sha3_512'] = _sha3.sha3_512
125+
cache['sha3_224'] = __get_wrapped_builtin(_sha3.sha3_224, name)
126+
cache['sha3_256'] = __get_wrapped_builtin(_sha3.sha3_256, name)
127+
cache['sha3_384'] = __get_wrapped_builtin(_sha3.sha3_384, name)
128+
cache['sha3_512'] = __get_wrapped_builtin(_sha3.sha3_512, name)
112129
elif name in {'shake_128', 'shake_256'}:
113130
import _sha3
114-
cache['shake_128'] = _sha3.shake_128
115-
cache['shake_256'] = _sha3.shake_256
131+
cache['shake_128'] = __get_wrapped_builtin(_sha3.shake_128, name)
132+
cache['shake_256'] = __get_wrapped_builtin(_sha3.shake_256, name)
116133
except ImportError:
117134
pass # no extension module, this hash is unsupported.
118135

@@ -163,7 +180,7 @@ def __hash_new(name, data=b'', **kwargs):
163180
# hash, try using our builtin implementations.
164181
# This allows for SHA224/256 and SHA384/512 support even though
165182
# the OpenSSL library prior to 0.9.8 doesn't provide them.
166-
return __get_builtin_constructor(name)(data)
183+
return __get_builtin_constructor(name)(data, **kwargs)
167184

168185

169186
try:

Lib/test/hashlibdata/openssl.cnf

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Activate base provider only, with default properties fips=yes. It
2+
# means that fips mode is on, and no digest implementations are
3+
# available. Perfect for mock testing builtin FIPS wrappers.
4+
5+
config_diagnostics = 1
6+
openssl_conf = openssl_init
7+
8+
[openssl_init]
9+
providers = provider_sect
10+
alg_section = algorithm_sect
11+
12+
[provider_sect]
13+
base = base_sect
14+
15+
[base_sect]
16+
activate = 1
17+
18+
[algorithm_sect]
19+
default_properties = fips=yes

Lib/test/ssltests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
TESTS = [
99
'test_asyncio', 'test_ensurepip.py', 'test_ftplib', 'test_hashlib',
10-
'test_hmac', 'test_httplib', 'test_imaplib',
10+
'test_hashlib_fips', 'test_hmac', 'test_httplib', 'test_imaplib',
1111
'test_poplib', 'test_ssl', 'test_smtplib', 'test_smtpnet',
1212
'test_urllib2_localnet', 'test_venv', 'test_xmlrpc'
1313
]

Lib/test/test_hashlib_fips.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Test the hashlib module usedforsecurity wrappers under fips.
2+
#
3+
# Copyright (C) 2024 Dimitri John Ledkov (dimitri.ledkov@surgut.co.uk)
4+
# Licensed to PSF under a Contributor Agreement.
5+
#
6+
7+
import os
8+
import unittest
9+
10+
OPENSSL_CONF_BACKUP = os.environ.get("OPENSSL_CONF")
11+
12+
class HashLibFIPSTestCase(unittest.TestCase):
13+
@classmethod
14+
def setUpClass(cls):
15+
# This openssl.cnf mocks FIPS mode without any digest
16+
# loaded. It means all digests must raise ValueError when
17+
# usedforsecurity=True via either openssl or builtin
18+
# constructors
19+
OPENSSL_CONF = os.path.join(os.path.dirname(__file__), "hashlibdata", "openssl.cnf")
20+
os.environ["OPENSSL_CONF"] = OPENSSL_CONF
21+
# Ensure hashlib is loading a fresh libcrypto with openssl
22+
# context affected by the above config file. Check if this can
23+
# be folded into test_hashlib.py, specifically if
24+
# import_fresh_module() results in a fresh library context
25+
import hashlib
26+
27+
def setUp(self):
28+
try:
29+
from _hashlib import get_fips_mode
30+
except ImportError:
31+
self.skipTest('_hashlib not available')
32+
33+
if get_fips_mode() != 1:
34+
self.skipTest('mocking fips mode failed')
35+
36+
@classmethod
37+
def tearDownClass(cls):
38+
if OPENSSL_CONF_BACKUP:
39+
os.environ["OPENSSL_CONF"] = OPENSSL_CONF_BACKUP
40+
else:
41+
del(os.environ["OPENSSL_CONF"])
42+
43+
def test_algorithms_available(self):
44+
import hashlib
45+
self.assertTrue(set(hashlib.algorithms_guaranteed).
46+
issubset(hashlib.algorithms_available))
47+
# all available algorithms must be loadable, bpo-47101
48+
self.assertNotIn("undefined", hashlib.algorithms_available)
49+
for name in hashlib.algorithms_available:
50+
digest = hashlib.new(name, usedforsecurity=False)
51+
52+
def test_usedforsecurity_true(self):
53+
import hashlib
54+
for name in hashlib.algorithms_available:
55+
with self.assertRaises(ValueError):
56+
digest = hashlib.new(name, usedforsecurity=True)
57+
58+
if __name__ == "__main__":
59+
unittest.main()

Makefile.pre.in

+1
Original file line numberDiff line numberDiff line change
@@ -2447,6 +2447,7 @@ TESTSUBDIRS= idlelib/idle_test \
24472447
test/decimaltestdata \
24482448
test/dtracedata \
24492449
test/encoded_modules \
2450+
test/hashlibdata \
24502451
test/leakers \
24512452
test/libregrtest \
24522453
test/mathdata \
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`hashlib`'s fallback builtin hash implementations now check
2+
usedforsecurity=True, if hashlib is in FIPS mode. This ensures that
3+
approved-only implementations are in use on FIPS systems by default,
4+
and fallback ones are made available for unapproved purposes only.

0 commit comments

Comments
 (0)