Enable importing of hash-based .pyc files under FIPS mode

Resolves: RHEL-40769
This commit is contained in:
Charalampos Stratakis 2024-06-11 21:43:32 +02:00
parent 6302d4b3b0
commit af16c99e30
2 changed files with 15 additions and 276 deletions

View File

@ -1,7 +1,7 @@
From 0bce3439227475f9787c13b83e928c02f8f93cd9 Mon Sep 17 00:00:00 2001 From d307f5706434e0cb445fb48291852bd7ec46ddbd Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com> From: Charalampos Stratakis <cstratak@redhat.com>
Date: Thu, 12 Dec 2019 16:58:31 +0100 Date: Thu, 12 Dec 2019 16:58:31 +0100
Subject: [PATCH 1/6] Expose blake2b and blake2s hashes from OpenSSL Subject: [PATCH 1/5] Expose blake2b and blake2s hashes from OpenSSL
These aren't as powerful as Python's own implementation, but they can be These aren't as powerful as Python's own implementation, but they can be
used under FIPS. used under FIPS.
@ -254,10 +254,10 @@ index fb61a44..1e42b87 100644
2.45.0 2.45.0
From cccfda405694f2893057d2b933d41affe80ce464 Mon Sep 17 00:00:00 2001 From c247ff164269fb68236a79a1359cc37c1a8a0004 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pviktori@redhat.com> From: Petr Viktorin <pviktori@redhat.com>
Date: Thu, 25 Jul 2019 17:19:06 +0200 Date: Thu, 25 Jul 2019 17:19:06 +0200
Subject: [PATCH 2/6] Disable Python's hash implementations in FIPS mode, Subject: [PATCH 2/5] Disable Python's hash implementations in FIPS mode,
forcing OpenSSL forcing OpenSSL
--- ---
@ -462,10 +462,10 @@ index 384718d..c4a1198 100644
2.45.0 2.45.0
From 0769f0bc973310a65be84a95b5e01bd7f77e5500 Mon Sep 17 00:00:00 2001 From e58b32f238f1d4503248f3a8b1489f7567bdbd6d Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com> From: Charalampos Stratakis <cstratak@redhat.com>
Date: Fri, 29 Jan 2021 14:16:21 +0100 Date: Fri, 29 Jan 2021 14:16:21 +0100
Subject: [PATCH 3/6] Use python's fall back crypto implementations only if we Subject: [PATCH 3/5] Use python's fall back crypto implementations only if we
are not in FIPS mode are not in FIPS mode
--- ---
@ -555,10 +555,10 @@ index dd61a9a..6031b02 100644
2.45.0 2.45.0
From da1f641e246a1e5ec84ff289f0d12daf787bb2b2 Mon Sep 17 00:00:00 2001 From 2b14d347948dc01af587b9e21cd448833a38c7b5 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com> From: Charalampos Stratakis <cstratak@redhat.com>
Date: Wed, 31 Jul 2019 15:43:43 +0200 Date: Wed, 31 Jul 2019 15:43:43 +0200
Subject: [PATCH 4/6] Test equivalence of hashes for the various digests with Subject: [PATCH 4/5] Test equivalence of hashes for the various digests with
usedforsecurity=True/False usedforsecurity=True/False
--- ---
@ -715,10 +715,10 @@ index 6031b02..5bd5297 100644
2.45.0 2.45.0
From c355137c714856aefaee09662e56e591bfad216e Mon Sep 17 00:00:00 2001 From b98c72b356a529a68cb4216526b838a57937cf6f Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pviktori@redhat.com> From: Petr Viktorin <pviktori@redhat.com>
Date: Mon, 26 Aug 2019 19:39:48 +0200 Date: Mon, 26 Aug 2019 19:39:48 +0200
Subject: [PATCH 5/6] Guard against Python HMAC in FIPS mode Subject: [PATCH 5/5] Guard against Python HMAC in FIPS mode
--- ---
Lib/hmac.py | 12 +++++++++--- Lib/hmac.py | 12 +++++++++---
@ -824,268 +824,3 @@ index a39a2c4..b7b24ab 100644
-- --
2.45.0 2.45.0
From 493b83ae5da48b0eafb90346bd9853a1b745c8e4 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Wed, 25 Aug 2021 16:44:43 +0200
Subject: [PATCH 6/6] Disable hash-based PYCs in FIPS mode
If FIPS mode is on, we can't use siphash-based HMAC
(_Py_KeyedHash), so:
- Unchecked hash PYCs can be imported, but not created
- Checked hash PYCs can not be imported nor created
- The default mode is timestamp-based PYCs, even if
SOURCE_DATE_EPOCH is set.
If FIPS mode is off, there are no changes in behavior.
Resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1835169
---
Lib/py_compile.py | 4 +++-
Lib/test/support/__init__.py | 14 +++++++++++++
Lib/test/test_cmd_line_script.py | 2 ++
Lib/test/test_compileall.py | 11 +++++++++-
.../test_importlib/source/test_file_loader.py | 6 ++++++
Lib/test/test_py_compile.py | 11 ++++++++--
Lib/test/test_zipimport.py | 2 ++
Python/import.c | 20 +++++++++++++++++++
8 files changed, 66 insertions(+), 4 deletions(-)
diff --git a/Lib/py_compile.py b/Lib/py_compile.py
index 388614e..fd9a139 100644
--- a/Lib/py_compile.py
+++ b/Lib/py_compile.py
@@ -70,7 +70,9 @@ class PycInvalidationMode(enum.Enum):
def _get_default_invalidation_mode():
- if os.environ.get('SOURCE_DATE_EPOCH'):
+ import _hashlib
+ if (os.environ.get('SOURCE_DATE_EPOCH') and not
+ _hashlib.get_fips_mode()):
return PycInvalidationMode.CHECKED_HASH
else:
return PycInvalidationMode.TIMESTAMP
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 8c4b4e0..b37aee5 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -2348,6 +2348,20 @@ def sleeping_retry(timeout, err_msg=None, /,
delay = min(delay * 2, max_delay)
+def fails_in_fips_mode(expected_error):
+ import _hashlib
+ if _hashlib.get_fips_mode():
+ def _decorator(func):
+ def _wrapper(self, *args, **kwargs):
+ with self.assertRaises(expected_error):
+ func(self, *args, **kwargs)
+ return _wrapper
+ else:
+ def _decorator(func):
+ return func
+ return _decorator
+
+
@contextlib.contextmanager
def adjust_int_max_str_digits(max_digits):
"""Temporarily change the integer string conversion length limit."""
diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py
index 1b58882..d6caff1 100644
--- a/Lib/test/test_cmd_line_script.py
+++ b/Lib/test/test_cmd_line_script.py
@@ -286,6 +286,7 @@ class CmdLineTest(unittest.TestCase):
self._check_script(zip_name, run_name, zip_name, zip_name, '',
zipimport.zipimporter)
+ @support.fails_in_fips_mode(ImportError)
def test_zipfile_compiled_checked_hash(self):
with os_helper.temp_dir() as script_dir:
script_name = _make_test_script(script_dir, '__main__')
@@ -296,6 +297,7 @@ class CmdLineTest(unittest.TestCase):
self._check_script(zip_name, run_name, zip_name, zip_name, '',
zipimport.zipimporter)
+ @support.fails_in_fips_mode(ImportError)
def test_zipfile_compiled_unchecked_hash(self):
with os_helper.temp_dir() as script_dir:
script_name = _make_test_script(script_dir, '__main__')
diff --git a/Lib/test/test_compileall.py b/Lib/test/test_compileall.py
index 9cd92ad..4ec29a1 100644
--- a/Lib/test/test_compileall.py
+++ b/Lib/test/test_compileall.py
@@ -806,14 +806,23 @@ class CommandLineTestsBase:
out = self.assertRunOK('badfilename')
self.assertRegex(out, b"Can't list 'badfilename'")
- def test_pyc_invalidation_mode(self):
+ @support.fails_in_fips_mode(AssertionError)
+ def test_pyc_invalidation_mode_checked(self):
script_helper.make_script(self.pkgdir, 'f1', '')
pyc = importlib.util.cache_from_source(
os.path.join(self.pkgdir, 'f1.py'))
+
self.assertRunOK('--invalidation-mode=checked-hash', self.pkgdir)
with open(pyc, 'rb') as fp:
data = fp.read()
self.assertEqual(int.from_bytes(data[4:8], 'little'), 0b11)
+
+ @support.fails_in_fips_mode(AssertionError)
+ def test_pyc_invalidation_mode_unchecked(self):
+ script_helper.make_script(self.pkgdir, 'f1', '')
+ pyc = importlib.util.cache_from_source(
+ os.path.join(self.pkgdir, 'f1.py'))
+
self.assertRunOK('--invalidation-mode=unchecked-hash', self.pkgdir)
with open(pyc, 'rb') as fp:
data = fp.read()
diff --git a/Lib/test/test_importlib/source/test_file_loader.py b/Lib/test/test_importlib/source/test_file_loader.py
index f35adec..62087c6 100644
--- a/Lib/test/test_importlib/source/test_file_loader.py
+++ b/Lib/test/test_importlib/source/test_file_loader.py
@@ -16,6 +16,7 @@ import types
import unittest
import warnings
+from test import support
from test.support.import_helper import make_legacy_pyc, unload
from test.test_py_compile import without_source_date_epoch
@@ -237,6 +238,7 @@ class SimpleTest(abc.LoaderTests):
loader.load_module('bad name')
@util.writes_bytecode_files
+ @support.fails_in_fips_mode(ImportError)
def test_checked_hash_based_pyc(self):
with util.create_modules('_temp') as mapping:
source = mapping['_temp']
@@ -268,6 +270,7 @@ class SimpleTest(abc.LoaderTests):
)
@util.writes_bytecode_files
+ @support.fails_in_fips_mode(ImportError)
def test_overridden_checked_hash_based_pyc(self):
with util.create_modules('_temp') as mapping, \
unittest.mock.patch('_imp.check_hash_based_pycs', 'never'):
@@ -293,6 +296,7 @@ class SimpleTest(abc.LoaderTests):
self.assertEqual(mod.state, 'old')
@util.writes_bytecode_files
+ @support.fails_in_fips_mode(ImportError)
def test_unchecked_hash_based_pyc(self):
with util.create_modules('_temp') as mapping:
source = mapping['_temp']
@@ -323,6 +327,7 @@ class SimpleTest(abc.LoaderTests):
)
@util.writes_bytecode_files
+ @support.fails_in_fips_mode(ImportError)
def test_overridden_unchecked_hash_based_pyc(self):
with util.create_modules('_temp') as mapping, \
unittest.mock.patch('_imp.check_hash_based_pycs', 'always'):
@@ -432,6 +437,7 @@ class BadBytecodeTest:
del_source=del_source)
test('_temp', mapping, bc_path)
+ @support.fails_in_fips_mode(ImportError)
def _test_partial_hash(self, test, *, del_source=False):
with util.create_modules('_temp') as mapping:
bc_path = self.manipulate_bytecode(
diff --git a/Lib/test/test_py_compile.py b/Lib/test/test_py_compile.py
index c4e6551..81fd962 100644
--- a/Lib/test/test_py_compile.py
+++ b/Lib/test/test_py_compile.py
@@ -141,13 +141,16 @@ class PyCompileTestsBase:
importlib.util.cache_from_source(bad_coding)))
def test_source_date_epoch(self):
+ import _hashlib
py_compile.compile(self.source_path, self.pyc_path)
self.assertTrue(os.path.exists(self.pyc_path))
self.assertFalse(os.path.exists(self.cache_path))
with open(self.pyc_path, 'rb') as fp:
flags = importlib._bootstrap_external._classify_pyc(
fp.read(), 'test', {})
- if os.environ.get('SOURCE_DATE_EPOCH'):
+ if _hashlib.get_fips_mode():
+ expected_flags = 0b00
+ elif os.environ.get('SOURCE_DATE_EPOCH'):
expected_flags = 0b11
else:
expected_flags = 0b00
@@ -178,7 +181,8 @@ class PyCompileTestsBase:
# Specifying optimized bytecode should lead to a path reflecting that.
self.assertIn('opt-2', py_compile.compile(self.source_path, optimize=2))
- def test_invalidation_mode(self):
+ @support.fails_in_fips_mode(ImportError)
+ def test_invalidation_mode_checked(self):
py_compile.compile(
self.source_path,
invalidation_mode=py_compile.PycInvalidationMode.CHECKED_HASH,
@@ -187,6 +191,9 @@ class PyCompileTestsBase:
flags = importlib._bootstrap_external._classify_pyc(
fp.read(), 'test', {})
self.assertEqual(flags, 0b11)
+
+ @support.fails_in_fips_mode(ImportError)
+ def test_invalidation_mode_unchecked(self):
py_compile.compile(
self.source_path,
invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH,
diff --git a/Lib/test/test_zipimport.py b/Lib/test/test_zipimport.py
index 14c1971..bcd1466 100644
--- a/Lib/test/test_zipimport.py
+++ b/Lib/test/test_zipimport.py
@@ -190,6 +190,7 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
TESTMOD + pyc_ext: (NOW, test_pyc)}
self.doTest(pyc_ext, files, TESTMOD)
+ @support.fails_in_fips_mode(ImportError)
def testUncheckedHashBasedPyc(self):
source = b"state = 'old'"
source_hash = importlib.util.source_hash(source)
@@ -204,6 +205,7 @@ class UncompressedZipImportTestCase(ImportHooksBaseTestCase):
self.assertEqual(mod.state, 'old')
self.doTest(None, files, TESTMOD, call=check)
+ @support.fails_in_fips_mode(ImportError)
@unittest.mock.patch('_imp.check_hash_based_pycs', 'always')
def test_checked_hash_based_change_pyc(self):
source = b"state = 'old'"
diff --git a/Python/import.c b/Python/import.c
index db70909..fb6d01b 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -3829,6 +3829,26 @@ static PyObject *
_imp_source_hash_impl(PyObject *module, long key, Py_buffer *source)
/*[clinic end generated code: output=edb292448cf399ea input=9aaad1e590089789]*/
{
+ PyObject *_hashlib = PyImport_ImportModule("_hashlib");
+ if (_hashlib == NULL) {
+ return NULL;
+ }
+ PyObject *fips_mode_obj = PyObject_CallMethod(_hashlib, "get_fips_mode", NULL);
+ Py_DECREF(_hashlib);
+ if (fips_mode_obj == NULL) {
+ return NULL;
+ }
+ int fips_mode = PyObject_IsTrue(fips_mode_obj);
+ Py_DECREF(fips_mode_obj);
+ if (fips_mode < 0) {
+ return NULL;
+ }
+ if (fips_mode) {
+ PyErr_SetString(
+ PyExc_ImportError,
+ "hash-based PYC validation (siphash24) not available in FIPS mode");
+ return NULL;
+ };
union {
uint64_t x;
char data[sizeof(uint64_t)];
--
2.45.0

View File

@ -17,7 +17,7 @@ URL: https://www.python.org/
#global prerel ... #global prerel ...
%global upstream_version %{general_version}%{?prerel} %global upstream_version %{general_version}%{?prerel}
Version: %{general_version}%{?prerel:~%{prerel}} Version: %{general_version}%{?prerel:~%{prerel}}
Release: 6%{?dist} Release: 7%{?dist}
License: Python-2.0.1 License: Python-2.0.1
@ -1779,6 +1779,10 @@ CheckPython optimized
# ====================================================== # ======================================================
%changelog %changelog
* Tue Jun 11 2024 Charalampos Stratakis <cstratak@redhat.com> - 3.12.2-7
- Enable importing of hash-based .pyc files under FIPS mode
Resolves: RHEL-40769
* Tue May 28 2024 Charalampos Stratakis <cstratak@redhat.com> - 3.12.2-6 * Tue May 28 2024 Charalampos Stratakis <cstratak@redhat.com> - 3.12.2-6
- Support OpenSSL FIPS mode - Support OpenSSL FIPS mode
- Disable the builtin hashlib hashes except blake2 - Disable the builtin hashlib hashes except blake2