Skip to content

Commit 6e48613

Browse files
committed
fix python fips detection and blocking of blake2
- fixes chainguard-dev/internal-dev#11886 - backports from wolfi-dev#36503 (see python/cpython#127301) Signed-off-by: Dan Ryan <[email protected]>
1 parent 0564d80 commit 6e48613

File tree

6 files changed

+856
-5
lines changed

6 files changed

+856
-5
lines changed

python-3.10.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package:
22
name: python-3.10
33
version: "3.10.17"
4-
epoch: 0
4+
epoch: 1
55
description: "the Python programming language"
66
copyright:
77
- license: PSF-2.0
@@ -57,7 +57,7 @@ pipeline:
5757
5858
- uses: patch
5959
with:
60-
patches: gh-118224.patch
60+
patches: gh-118224.patch gh-127301.patch
6161

6262
- name: Configure
6363
runs: |

python-3.10/gh-127301.patch

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
Upstream: https://github.com/python/cpython/pull/127301
2+
3+
diff --git a/Lib/hashlib.py b/Lib/hashlib.py
4+
index 21a73f3bf6c..9f627441d43 100644
5+
--- a/Lib/hashlib.py
6+
+++ b/Lib/hashlib.py
7+
@@ -79,6 +79,24 @@
8+
'blake2b', 'blake2s',
9+
}
10+
11+
+# Wrapper that only allows usage when usedforsecurity=False
12+
+# (effectively unapproved service indicator)
13+
+def __usedforsecurity_check(md, name, *args, **kwargs):
14+
+ if kwargs.get("usedforsecurity", True):
15+
+ raise ValueError(name + " is blocked when usedforsecurity=True")
16+
+ return md(*args, **kwargs)
17+
+
18+
+# If the _hashlib OpenSSL wrapper is in FIPS mode, wrap other implementations
19+
+# to check the usedforsecurity kwarg. All builtin implementations are treated
20+
+# as only available for useforsecurity=False purposes in the presence of such
21+
+# a configured and linked OpenSSL.
22+
+def __get_wrapped_builtin(md, name):
23+
+ if __openssl_fips_mode != 0:
24+
+ from functools import partial
25+
+ return partial(__usedforsecurity_check, md, name)
26+
+ return md
27+
+
28+
+
29+
def __get_builtin_constructor(name):
30+
cache = __builtin_constructor_cache
31+
constructor = cache.get(name)
32+
@@ -87,32 +105,32 @@ def __get_builtin_constructor(name):
33+
try:
34+
if name in {'SHA1', 'sha1'}:
35+
import _sha1
36+
- cache['SHA1'] = cache['sha1'] = _sha1.sha1
37+
+ cache['SHA1'] = cache['sha1'] = __get_wrapped_builtin(_sha1.sha1, name)
38+
elif name in {'MD5', 'md5'}:
39+
import _md5
40+
- cache['MD5'] = cache['md5'] = _md5.md5
41+
+ cache['MD5'] = cache['md5'] = __get_wrapped_builtin(_md5.md5, name)
42+
elif name in {'SHA256', 'sha256', 'SHA224', 'sha224'}:
43+
import _sha256
44+
- cache['SHA224'] = cache['sha224'] = _sha256.sha224
45+
- cache['SHA256'] = cache['sha256'] = _sha256.sha256
46+
+ cache['SHA224'] = cache['sha224'] = __get_wrapped_builtin(_sha256.sha224, name)
47+
+ cache['SHA256'] = cache['sha256'] = __get_wrapped_builtin(_sha256.sha256, name)
48+
elif name in {'SHA512', 'sha512', 'SHA384', 'sha384'}:
49+
import _sha512
50+
- cache['SHA384'] = cache['sha384'] = _sha512.sha384
51+
- cache['SHA512'] = cache['sha512'] = _sha512.sha512
52+
+ cache['SHA384'] = cache['sha384'] = __get_wrapped_builtin(_sha512.sha384, name)
53+
+ cache['SHA512'] = cache['sha512'] = __get_wrapped_builtin(_sha512.sha512, name)
54+
elif name in {'blake2b', 'blake2s'}:
55+
import _blake2
56+
- cache['blake2b'] = _blake2.blake2b
57+
- cache['blake2s'] = _blake2.blake2s
58+
+ cache['blake2b'] = __get_wrapped_builtin(_blake2.blake2b, name)
59+
+ cache['blake2s'] = __get_wrapped_builtin(_blake2.blake2s, name)
60+
elif name in {'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512'}:
61+
import _sha3
62+
- cache['sha3_224'] = _sha3.sha3_224
63+
- cache['sha3_256'] = _sha3.sha3_256
64+
- cache['sha3_384'] = _sha3.sha3_384
65+
- cache['sha3_512'] = _sha3.sha3_512
66+
+ cache['sha3_224'] = __get_wrapped_builtin(_sha3.sha3_224, name)
67+
+ cache['sha3_256'] = __get_wrapped_builtin(_sha3.sha3_256, name)
68+
+ cache['sha3_384'] = __get_wrapped_builtin(_sha3.sha3_384, name)
69+
+ cache['sha3_512'] = __get_wrapped_builtin(_sha3.sha3_512, name)
70+
elif name in {'shake_128', 'shake_256'}:
71+
import _sha3
72+
- cache['shake_128'] = _sha3.shake_128
73+
- cache['shake_256'] = _sha3.shake_256
74+
+ cache['shake_128'] = __get_wrapped_builtin(_sha3.shake_128, name)
75+
+ cache['shake_256'] = __get_wrapped_builtin(_sha3.shake_256, name)
76+
except ImportError:
77+
pass # no extension module, this hash is unsupported.
78+
79+
@@ -161,9 +179,8 @@ def __hash_new(name, data=b'', **kwargs):
80+
except ValueError:
81+
# If the _hashlib module (OpenSSL) doesn't support the named
82+
# hash, try using our builtin implementations.
83+
- # This allows for SHA224/256 and SHA384/512 support even though
84+
- # the OpenSSL library prior to 0.9.8 doesn't provide them.
85+
- return __get_builtin_constructor(name)(data)
86+
+ # OpenSSL may not have been compiled to support everything.
87+
+ return __get_builtin_constructor(name)(data, **kwargs)
88+
89+
90+
try:
91+
@@ -172,6 +189,10 @@ def __hash_new(name, data=b'', **kwargs):
92+
__get_hash = __get_openssl_constructor
93+
algorithms_available = algorithms_available.union(
94+
_hashlib.openssl_md_meth_names)
95+
+ try:
96+
+ __openssl_fips_mode = _hashlib.get_fips_mode()
97+
+ except ValueError:
98+
+ __openssl_fips_mode = 0
99+
except ImportError:
100+
_hashlib = None
101+
new = __py_new
102+
diff --git a/Lib/test/_test_hashlib_fips.py b/Lib/test/_test_hashlib_fips.py
103+
new file mode 100644
104+
index 00000000000..92537245954
105+
--- /dev/null
106+
+++ b/Lib/test/_test_hashlib_fips.py
107+
@@ -0,0 +1,64 @@
108+
+# Test the hashlib module usedforsecurity wrappers under fips.
109+
+#
110+
+# Copyright (C) 2024 Dimitri John Ledkov ([email protected])
111+
+# Licensed to PSF under a Contributor Agreement.
112+
+#
113+
+
114+
+"""Primarily executed by test_hashlib.py. It can run stand alone by humans."""
115+
+
116+
+import os
117+
+import unittest
118+
+
119+
+OPENSSL_CONF_BACKUP = os.environ.get("OPENSSL_CONF")
120+
+
121+
+
122+
+class HashLibFIPSTestCase(unittest.TestCase):
123+
+ @classmethod
124+
+ def setUpClass(cls):
125+
+ # This openssl.cnf mocks FIPS mode without any digest
126+
+ # loaded. It means all digests must raise ValueError when
127+
+ # usedforsecurity=True via either openssl or builtin
128+
+ # constructors
129+
+ OPENSSL_CONF = os.path.join(os.path.dirname(__file__), "hashlibdata", "openssl.cnf")
130+
+ os.environ["OPENSSL_CONF"] = OPENSSL_CONF
131+
+ # Ensure hashlib is loading a fresh libcrypto with openssl
132+
+ # context affected by the above config file. Check if this can
133+
+ # be folded into test_hashlib.py, specifically if
134+
+ # import_fresh_module() results in a fresh library context
135+
+ import hashlib
136+
+
137+
+ def setUp(self):
138+
+ try:
139+
+ from _hashlib import get_fips_mode
140+
+ except ImportError:
141+
+ self.skipTest('_hashlib not available')
142+
+
143+
+ if get_fips_mode() != 1:
144+
+ self.skipTest('mocking fips mode failed')
145+
+
146+
+ @classmethod
147+
+ def tearDownClass(cls):
148+
+ if OPENSSL_CONF_BACKUP is not None:
149+
+ os.environ["OPENSSL_CONF"] = OPENSSL_CONF_BACKUP
150+
+ else:
151+
+ os.environ.pop("OPENSSL_CONF", None)
152+
+
153+
+ def test_algorithms_available(self):
154+
+ import hashlib
155+
+ self.assertTrue(set(hashlib.algorithms_guaranteed).
156+
+ issubset(hashlib.algorithms_available))
157+
+ # all available algorithms must be loadable, bpo-47101
158+
+ self.assertNotIn("undefined", hashlib.algorithms_available)
159+
+ for name in hashlib.algorithms_available:
160+
+ with self.subTest(name):
161+
+ digest = hashlib.new(name, usedforsecurity=False)
162+
+
163+
+ def test_usedforsecurity_true(self):
164+
+ import hashlib
165+
+ for name in hashlib.algorithms_available:
166+
+ with self.subTest(name):
167+
+ with self.assertRaises(ValueError):
168+
+ digest = hashlib.new(name, usedforsecurity=True)
169+
+
170+
+if __name__ == "__main__":
171+
+ unittest.main()
172+
diff --git a/Lib/test/hashlibdata/openssl.cnf b/Lib/test/hashlibdata/openssl.cnf
173+
new file mode 100644
174+
index 00000000000..9a936ddc5ef
175+
--- /dev/null
176+
+++ b/Lib/test/hashlibdata/openssl.cnf
177+
@@ -0,0 +1,19 @@
178+
+# Activate base provider only, with default properties fips=yes. It
179+
+# means that fips mode is on, and no digest implementations are
180+
+# available. Perfect for mock testing builtin FIPS wrappers.
181+
+
182+
+config_diagnostics = 1
183+
+openssl_conf = openssl_init
184+
+
185+
+[openssl_init]
186+
+providers = provider_sect
187+
+alg_section = algorithm_sect
188+
+
189+
+[provider_sect]
190+
+base = base_sect
191+
+
192+
+[base_sect]
193+
+activate = 1
194+
+
195+
+[algorithm_sect]
196+
+default_properties = fips=yes
197+
diff --git a/Lib/test/support/script_helper.py b/Lib/test/support/script_helper.py
198+
index 6d699c8486c..91f9050f022 100644
199+
--- a/Lib/test/support/script_helper.py
200+
+++ b/Lib/test/support/script_helper.py
201+
@@ -273,7 +273,14 @@ def make_zip_pkg(zip_dir, zip_basename, pkg_name, script_basename,
202+
return zip_name, os.path.join(zip_name, script_name_in_zip)
203+
204+
205+
-def run_test_script(script):
206+
+def run_test_script(script, **kwargs):
207+
+ """Run the file *script* in a child interpreter.
208+
+
209+
+ Keyword arguments are passed on to subprocess.run() within.
210+
+
211+
+ Asserts if the child exits non-zero. Prints child output after
212+
+ execution when run in verbose mode.
213+
+ """
214+
# use -u to try to get the full output if the test hangs or crash
215+
if support.verbose:
216+
def title(text):
217+
@@ -285,7 +292,7 @@ def title(text):
218+
# In verbose mode, the child process inherit stdout and stdout,
219+
# to see output in realtime and reduce the risk of losing output.
220+
args = [sys.executable, "-E", "-X", "faulthandler", "-u", script, "-v"]
221+
- proc = subprocess.run(args)
222+
+ proc = subprocess.run(args, **kwargs)
223+
print(title(f"{name} completed: exit code {proc.returncode}"),
224+
flush=True)
225+
if proc.returncode:
226+
diff --git a/Lib/test/test_hashlib.py b/Lib/test/test_hashlib.py
227+
index 9aa6c1f0a3b..1717954d523 100644
228+
--- a/Lib/test/test_hashlib.py
229+
+++ b/Lib/test/test_hashlib.py
230+
@@ -20,6 +20,7 @@
231+
from test import support
232+
from test.support import _4G, bigmemtest
233+
from test.support.import_helper import import_fresh_module
234+
+from test.support import script_helper
235+
from test.support import threading_helper
236+
from test.support import warnings_helper
237+
from http.client import HTTPException
238+
@@ -1130,6 +1131,18 @@ def test_normalized_name(self):
239+
self.assertNotIn("blake2b512", hashlib.algorithms_available)
240+
self.assertNotIn("sha3-512", hashlib.algorithms_available)
241+
242+
+ def test_builtins_in_openssl_fips_mode(self):
243+
+ try:
244+
+ from _hashlib import get_fips_mode
245+
+ except ImportError:
246+
+ self.skipTest('OpenSSL _hashlib not available')
247+
+ from test import _test_hashlib_fips
248+
+ child_test_path = _test_hashlib_fips.__file__
249+
+ env = os.environ.copy()
250+
+ # A config to mock FIPS mode, see _test_hashlib_fips.py.
251+
+ env["OPENSSL_CONF"] = os.path.join(os.path.dirname(__file__), "hashlibdata", "openssl.cnf")
252+
+ script_helper.run_test_script(child_test_path, env=env)
253+
+
254+
255+
if __name__ == "__main__":
256+
unittest.main()
257+
diff --git a/Makefile.pre.in b/Makefile.pre.in
258+
index fa99dd86c41..3d8b21e52b2 100644
259+
--- a/Makefile.pre.in
260+
+++ b/Makefile.pre.in
261+
@@ -1463,7 +1463,8 @@ TESTSUBDIRS= ctypes/test \
262+
test/capath test/cjkencodings \
263+
test/data test/decimaltestdata \
264+
test/dtracedata test/eintrdata \
265+
- test/encoded_modules test/imghdrdata \
266+
+ test/encoded_modules test/hashlibdata \
267+
+ test/imghdrdata \
268+
test/libregrtest test/sndhdrdata \
269+
test/subprocessdata test/support \
270+
test/test_asyncio \
271+
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
272+
new file mode 100644
273+
index 00000000000..e555661a195
274+
--- /dev/null
275+
+++ b/Misc/NEWS.d/next/Library/2024-11-26-16-31-40.gh-issue-127298.jqYJvn.rst
276+
@@ -0,0 +1,8 @@
277+
+:mod:`hashlib`'s builtin hash implementations now check ``usedforsecurity=False``,
278+
+when the OpenSSL library default provider is in OpenSSL's FIPS mode. This helps
279+
+ensure that only US FIPS approved implementations are in use by default on systems
280+
+configured as such.
281+
+
282+
+This is only active when :mod:`hashlib` has been built with OpenSSL implementation
283+
+support and said OpenSSL library includes the FIPS mode feature. Not all variants
284+
+do, and OpenSSL is not a *required* build time dependency of ``hashlib``.

python-3.11.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pipeline:
5757
5858
- uses: patch
5959
with:
60-
patches: gh-118224.patch
60+
patches: gh-118224.patch gh-127301.patch
6161

6262
- name: Configure
6363
runs: |

0 commit comments

Comments
 (0)