Skip to content

Commit

Permalink
pythongh-127298: When in FIPS mode ensure builtin hashes check for us…
Browse files Browse the repository at this point in the history
…edforsecurity=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.
  • Loading branch information
xnox committed Nov 26, 2024
1 parent 2b0e2b2 commit ad0fab8
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 16 deletions.
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
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
59 changes: 59 additions & 0 deletions Lib/test/test_hashlib_fips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Test the hashlib module usedforsecurity wrappers under fips.
#
# Copyright (C) 2024 Dimitri John Ledkov ([email protected])
# 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 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:
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()
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,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.

0 comments on commit ad0fab8

Please sign in to comment.