Skip to content

Commit 4a245a6

Browse files
authored
test FIPS mode on centos8 (pyca#5323)
* test FIPS mode on centos8 * remove branch we don't take * simpler * better comment * rename * revert some things that don't matter * small cleanups
1 parent 2fdb747 commit 4a245a6

18 files changed

Lines changed: 191 additions & 12 deletions

File tree

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ matrix:
6464
- python: 3.6
6565
services: docker
6666
env: TOXENV=py36 DOCKER=pyca/cryptography-runner-centos8
67+
- python: 3.6
68+
services: docker
69+
env: TOXENV=py36 OPENSSL_FORCE_FIPS_MODE=1 DOCKER=pyca/cryptography-runner-centos8-fips
6770
- python: 2.7
6871
services: docker
6972
env: TOXENV=py27 DOCKER=pyca/cryptography-runner-stretch

.travis/run.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ if [ -n "${DOCKER}" ]; then
3232
-v "${TRAVIS_BUILD_DIR}":"${TRAVIS_BUILD_DIR}" \
3333
-v "${HOME}/wycheproof":/wycheproof \
3434
-w "${TRAVIS_BUILD_DIR}" \
35+
-e OPENSSL_FORCE_FIPS_MODE \
3536
-e TOXENV "${DOCKER}" \
3637
/bin/sh -c "tox -- --wycheproof-root='/wycheproof'"
3738
elif [ -n "${TOXENV}" ]; then

.travis/upload_coverage.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ if [ -n "${TOXENV}" ]; then
1414
source ~/.venv/bin/activate
1515
curl -o codecov.sh -f https://codecov.io/bash || curl -o codecov.sh -f https://codecov.io/bash || curl -o codecov.sh -f https://codecov.io/bash
1616

17-
bash codecov.sh -Z -e TRAVIS_OS_NAME,TOXENV,OPENSSL,DOCKER || \
18-
bash codecov.sh -Z -e TRAVIS_OS_NAME,TOXENV,OPENSSL,DOCKER
17+
bash codecov.sh -Z -e TRAVIS_OS_NAME,TOXENV,OPENSSL,DOCKER,OPENSSL_FORCE_FIPS_MODE || \
18+
bash codecov.sh -Z -e TRAVIS_OS_NAME,TOXENV,OPENSSL,DOCKER,OPENSSL_FORCE_FIPS_MODE
1919
;;
2020
esac
2121
fi

src/_cffi_src/openssl/err.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
static const int EVP_R_CAMELLIA_KEY_SETUP_FAILED;
8282
8383
static const int EC_R_UNKNOWN_GROUP;
84+
static const int EC_R_NOT_A_NIST_PRIME;
8485
8586
static const int PEM_R_BAD_BASE64_DECODE;
8687
static const int PEM_R_BAD_DECRYPT;

src/cryptography/hazmat/backends/openssl/backend.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import collections
88
import contextlib
99
import itertools
10+
import warnings
1011
from contextlib import contextmanager
1112

1213
import six
@@ -119,21 +120,59 @@ class Backend(object):
119120
"""
120121
name = "openssl"
121122

123+
# FIPS has opinions about acceptable algorithms and key sizes, but the
124+
# disallowed algorithms are still present in OpenSSL. They just error if
125+
# you try to use them. To avoid that we allowlist the algorithms in
126+
# FIPS 140-3. This isn't ideal, but FIPS 140-3 is trash so here we are.
127+
_fips_aead = {
128+
b'aes-128-ccm', b'aes-192-ccm', b'aes-256-ccm',
129+
b'aes-128-gcm', b'aes-192-gcm', b'aes-256-gcm',
130+
}
131+
_fips_ciphers = (
132+
AES, TripleDES
133+
)
134+
_fips_hashes = (
135+
hashes.SHA1, hashes.SHA224, hashes.SHA256, hashes.SHA384,
136+
hashes.SHA512, hashes.SHA512_224, hashes.SHA512_256, hashes.SHA3_224,
137+
hashes.SHA3_256, hashes.SHA3_384, hashes.SHA3_512, hashes.SHAKE128,
138+
hashes.SHAKE256,
139+
)
140+
_fips_rsa_min_key_size = 2048
141+
_fips_rsa_min_public_exponent = 65537
142+
_fips_dsa_min_modulus = 1 << 2048
143+
_fips_dh_min_key_size = 2048
144+
_fips_dh_min_modulus = 1 << _fips_dh_min_key_size
145+
122146
def __init__(self):
123147
self._binding = binding.Binding()
124148
self._ffi = self._binding.ffi
125149
self._lib = self._binding.lib
150+
self._fips_enabled = self._is_fips_enabled()
126151

127152
self._cipher_registry = {}
128153
self._register_default_ciphers()
129-
self.activate_osrandom_engine()
154+
if self._fips_enabled and self._lib.CRYPTOGRAPHY_NEEDS_OSRANDOM_ENGINE:
155+
warnings.warn(
156+
"OpenSSL FIPS mode is enabled. Can't enable DRBG fork safety.",
157+
UserWarning
158+
)
159+
else:
160+
self.activate_osrandom_engine()
130161
self._dh_types = [self._lib.EVP_PKEY_DH]
131162
if self._lib.Cryptography_HAS_EVP_PKEY_DHX:
132163
self._dh_types.append(self._lib.EVP_PKEY_DHX)
133164

134165
def openssl_assert(self, ok):
135166
return binding._openssl_assert(self._lib, ok)
136167

168+
def _is_fips_enabled(self):
169+
fips_mode = getattr(self._lib, "FIPS_mode", lambda: 0)
170+
mode = fips_mode()
171+
if mode == 0:
172+
# OpenSSL without FIPS pushes an error on the error stack
173+
self._lib.ERR_clear_error()
174+
return bool(mode)
175+
137176
def activate_builtin_random(self):
138177
if self._lib.CRYPTOGRAPHY_NEEDS_OSRANDOM_ENGINE:
139178
# Obtain a new structural reference.
@@ -222,6 +261,9 @@ def _evp_md_non_null_from_algorithm(self, algorithm):
222261
return evp_md
223262

224263
def hash_supported(self, algorithm):
264+
if self._fips_enabled and not isinstance(algorithm, self._fips_hashes):
265+
return False
266+
225267
evp_md = self._evp_md_from_algorithm(algorithm)
226268
return evp_md != self._ffi.NULL
227269

@@ -232,6 +274,8 @@ def create_hash_ctx(self, algorithm):
232274
return _HashContext(self, algorithm)
233275

234276
def cipher_supported(self, cipher, mode):
277+
if self._fips_enabled and not isinstance(cipher, self._fips_ciphers):
278+
return False
235279
try:
236280
adapter = self._cipher_registry[type(cipher), type(mode)]
237281
except KeyError:
@@ -1380,6 +1424,11 @@ def elliptic_curve_supported(self, curve):
13801424
errors[0]._lib_reason_match(
13811425
self._lib.ERR_LIB_EC,
13821426
self._lib.EC_R_UNKNOWN_GROUP
1427+
) or
1428+
# This occurs in FIPS mode for unsupported curves on RHEL
1429+
errors[0]._lib_reason_match(
1430+
self._lib.ERR_LIB_EC,
1431+
self._lib.EC_R_NOT_A_NIST_PRIME
13831432
)
13841433
)
13851434
return False
@@ -1777,6 +1826,16 @@ def _private_key_bytes(self, encoding, format, encryption_algorithm,
17771826

17781827
# TraditionalOpenSSL + PEM/DER
17791828
if format is serialization.PrivateFormat.TraditionalOpenSSL:
1829+
if (
1830+
self._fips_enabled and
1831+
not isinstance(
1832+
encryption_algorithm, serialization.NoEncryption
1833+
)
1834+
):
1835+
raise ValueError(
1836+
"Encrypted traditional OpenSSL format is not "
1837+
"supported in FIPS mode."
1838+
)
17801839
key_type = self._lib.EVP_PKEY_id(evp_pkey)
17811840

17821841
if encoding is serialization.Encoding.PEM:
@@ -2170,6 +2229,8 @@ def x25519_generate_key(self):
21702229
return _X25519PrivateKey(self, evp_pkey)
21712230

21722231
def x25519_supported(self):
2232+
if self._fips_enabled:
2233+
return False
21732234
return self._lib.CRYPTOGRAPHY_OPENSSL_110_OR_GREATER
21742235

21752236
def x448_load_public_bytes(self, data):
@@ -2200,9 +2261,13 @@ def x448_generate_key(self):
22002261
return _X448PrivateKey(self, evp_pkey)
22012262

22022263
def x448_supported(self):
2264+
if self._fips_enabled:
2265+
return False
22032266
return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111
22042267

22052268
def ed25519_supported(self):
2269+
if self._fips_enabled:
2270+
return False
22062271
return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111B
22072272

22082273
def ed25519_load_public_bytes(self, data):
@@ -2238,6 +2303,8 @@ def ed25519_generate_key(self):
22382303
return _Ed25519PrivateKey(self, evp_pkey)
22392304

22402305
def ed448_supported(self):
2306+
if self._fips_enabled:
2307+
return False
22412308
return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111B
22422309

22432310
def ed448_load_public_bytes(self, data):
@@ -2304,6 +2371,8 @@ def derive_scrypt(self, key_material, salt, length, n, r, p):
23042371

23052372
def aead_cipher_supported(self, cipher):
23062373
cipher_name = aead._aead_cipher_name(cipher)
2374+
if self._fips_enabled and cipher_name not in self._fips_aead:
2375+
return False
23072376
return (
23082377
self._lib.EVP_get_cipherbyname(cipher_name) != self._ffi.NULL
23092378
)
@@ -2453,6 +2522,8 @@ def serialize_key_and_certificates_to_pkcs12(self, name, key, cert, cas,
24532522
return self._read_mem_bio(bio)
24542523

24552524
def poly1305_supported(self):
2525+
if self._fips_enabled:
2526+
return False
24562527
return self._lib.Cryptography_HAS_POLY1305 == 1
24572528

24582529
def create_poly1305_ctx(self, key):

tests/conftest.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414

1515

1616
def pytest_report_header(config):
17-
return "OpenSSL: {}".format(openssl_backend.openssl_version_text())
17+
return "\n".join([
18+
"OpenSSL: {}".format(openssl_backend.openssl_version_text()),
19+
"FIPS Enabled: {}".format(openssl_backend._fips_enabled),
20+
])
1821

1922

2023
def pytest_addoption(parser):
@@ -33,6 +36,12 @@ def pytest_generate_tests(metafunc):
3336
metafunc.parametrize("wycheproof", testcases)
3437

3538

39+
def pytest_runtest_setup(item):
40+
if openssl_backend._fips_enabled:
41+
for marker in item.iter_markers(name="skip_fips"):
42+
pytest.skip(marker.kwargs["reason"])
43+
44+
3645
@pytest.fixture()
3746
def backend(request):
3847
required_interfaces = [

tests/hazmat/backends/test_openssl.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def test_bn_to_int(self):
171171
@pytest.mark.skipif(
172172
not backend._lib.CRYPTOGRAPHY_NEEDS_OSRANDOM_ENGINE,
173173
reason="Requires OpenSSL with ENGINE support and OpenSSL < 1.1.1d")
174+
@pytest.mark.skip_fips(reason="osrandom engine disabled for FIPS")
174175
class TestOpenSSLRandomEngine(object):
175176
def setup(self):
176177
# The default RAND engine is global and shared between

tests/hazmat/backends/test_openssl_memleak.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def skip_if_memtesting_not_supported():
160160
)
161161

162162

163+
@pytest.mark.skip_fips(reason="FIPS self-test sets allow_customize = 0")
163164
@skip_if_memtesting_not_supported()
164165
class TestAssertNoMemoryLeaks(object):
165166
def test_no_leak_no_malloc(self):
@@ -205,6 +206,7 @@ def func():
205206
"""))
206207

207208

209+
@pytest.mark.skip_fips(reason="FIPS self-test sets allow_customize = 0")
208210
@skip_if_memtesting_not_supported()
209211
class TestOpenSSLMemoryLeaks(object):
210212
@pytest.mark.parametrize("path", [

tests/hazmat/primitives/test_aead.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,15 @@ def test_data_too_large(self):
364364
aesgcm.encrypt(nonce, b"", FakeData())
365365

366366
@pytest.mark.parametrize("vector", _load_gcm_vectors())
367-
def test_vectors(self, vector):
368-
key = binascii.unhexlify(vector["key"])
367+
def test_vectors(self, backend, vector):
369368
nonce = binascii.unhexlify(vector["iv"])
369+
370+
if backend._fips_enabled and len(nonce) != 12:
371+
# Red Hat disables non-96-bit IV support as part of its FIPS
372+
# patches.
373+
pytest.skip("Non-96-bit IVs unsupported in FIPS mode.")
374+
375+
key = binascii.unhexlify(vector["key"])
370376
aad = binascii.unhexlify(vector["aad"])
371377
ct = binascii.unhexlify(vector["ct"])
372378
pt = binascii.unhexlify(vector.get("pt", b""))

tests/hazmat/primitives/test_dh.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def test_dh_parameters_supported_with_q(self, backend, vector):
196196
int(vector["g"], 16),
197197
int(vector["q"], 16))
198198

199+
@pytest.mark.skip_fips(reason="modulus too small for FIPS")
199200
@pytest.mark.parametrize("with_q", [False, True])
200201
def test_convert_to_numbers(self, backend, with_q):
201202
if with_q:
@@ -242,6 +243,7 @@ def test_numbers_unsupported_parameters(self, backend):
242243
with pytest.raises(ValueError):
243244
private.private_key(backend)
244245

246+
@pytest.mark.skip_fips(reason="FIPS requires key size >= 2048")
245247
@pytest.mark.parametrize("with_q", [False, True])
246248
def test_generate_dh(self, backend, with_q):
247249
if with_q:
@@ -309,6 +311,7 @@ def test_exchange_algorithm(self, backend):
309311

310312
assert symkey == symkey_manual
311313

314+
@pytest.mark.skip_fips(reason="key_size too small for FIPS")
312315
def test_symmetric_key_padding(self, backend):
313316
"""
314317
This test has specific parameters that produce a symmetric key
@@ -339,6 +342,11 @@ def test_symmetric_key_padding(self, backend):
339342
os.path.join("asymmetric", "DH", "bad_exchange.txt"),
340343
load_nist_vectors))
341344
def test_bad_exchange(self, backend, vector):
345+
if (
346+
backend._fips_enabled and
347+
int(vector["p1"]) < backend._fips_dh_min_modulus
348+
):
349+
pytest.skip("modulus too small for FIPS mode")
342350
parameters1 = dh.DHParameterNumbers(int(vector["p1"]),
343351
int(vector["g"]))
344352
public1 = dh.DHPublicNumbers(int(vector["y1"]), parameters1)
@@ -370,6 +378,11 @@ def test_bad_exchange(self, backend, vector):
370378
os.path.join("asymmetric", "DH", "vec.txt"),
371379
load_nist_vectors))
372380
def test_dh_vectors(self, backend, vector):
381+
if (
382+
backend._fips_enabled and
383+
int(vector["p"]) < backend._fips_dh_min_modulus
384+
):
385+
pytest.skip("modulus too small for FIPS mode")
373386
parameters = dh.DHParameterNumbers(int(vector["p"]),
374387
int(vector["g"]))
375388
public = dh.DHPublicNumbers(int(vector["y"]), parameters)

0 commit comments

Comments
 (0)