Skip to content

Commit

Permalink
test FIPS mode on centos8 (#5323)
Browse files Browse the repository at this point in the history
* test FIPS mode on centos8

* remove branch we don't take

* simpler

* better comment

* rename

* revert some things that don't matter

* small cleanups
  • Loading branch information
reaperhulk authored Jul 20, 2020
1 parent 2fdb747 commit 4a245a6
Show file tree
Hide file tree
Showing 18 changed files with 191 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ matrix:
- python: 3.6
services: docker
env: TOXENV=py36 DOCKER=pyca/cryptography-runner-centos8
- python: 3.6
services: docker
env: TOXENV=py36 OPENSSL_FORCE_FIPS_MODE=1 DOCKER=pyca/cryptography-runner-centos8-fips
- python: 2.7
services: docker
env: TOXENV=py27 DOCKER=pyca/cryptography-runner-stretch
Expand Down
1 change: 1 addition & 0 deletions .travis/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ if [ -n "${DOCKER}" ]; then
-v "${TRAVIS_BUILD_DIR}":"${TRAVIS_BUILD_DIR}" \
-v "${HOME}/wycheproof":/wycheproof \
-w "${TRAVIS_BUILD_DIR}" \
-e OPENSSL_FORCE_FIPS_MODE \
-e TOXENV "${DOCKER}" \
/bin/sh -c "tox -- --wycheproof-root='/wycheproof'"
elif [ -n "${TOXENV}" ]; then
Expand Down
4 changes: 2 additions & 2 deletions .travis/upload_coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ if [ -n "${TOXENV}" ]; then
source ~/.venv/bin/activate
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

bash codecov.sh -Z -e TRAVIS_OS_NAME,TOXENV,OPENSSL,DOCKER || \
bash codecov.sh -Z -e TRAVIS_OS_NAME,TOXENV,OPENSSL,DOCKER
bash codecov.sh -Z -e TRAVIS_OS_NAME,TOXENV,OPENSSL,DOCKER,OPENSSL_FORCE_FIPS_MODE || \
bash codecov.sh -Z -e TRAVIS_OS_NAME,TOXENV,OPENSSL,DOCKER,OPENSSL_FORCE_FIPS_MODE
;;
esac
fi
1 change: 1 addition & 0 deletions src/_cffi_src/openssl/err.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
static const int EVP_R_CAMELLIA_KEY_SETUP_FAILED;
static const int EC_R_UNKNOWN_GROUP;
static const int EC_R_NOT_A_NIST_PRIME;
static const int PEM_R_BAD_BASE64_DECODE;
static const int PEM_R_BAD_DECRYPT;
Expand Down
73 changes: 72 additions & 1 deletion src/cryptography/hazmat/backends/openssl/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import collections
import contextlib
import itertools
import warnings
from contextlib import contextmanager

import six
Expand Down Expand Up @@ -119,21 +120,59 @@ class Backend(object):
"""
name = "openssl"

# FIPS has opinions about acceptable algorithms and key sizes, but the
# disallowed algorithms are still present in OpenSSL. They just error if
# you try to use them. To avoid that we allowlist the algorithms in
# FIPS 140-3. This isn't ideal, but FIPS 140-3 is trash so here we are.
_fips_aead = {
b'aes-128-ccm', b'aes-192-ccm', b'aes-256-ccm',
b'aes-128-gcm', b'aes-192-gcm', b'aes-256-gcm',
}
_fips_ciphers = (
AES, TripleDES
)
_fips_hashes = (
hashes.SHA1, hashes.SHA224, hashes.SHA256, hashes.SHA384,
hashes.SHA512, hashes.SHA512_224, hashes.SHA512_256, hashes.SHA3_224,
hashes.SHA3_256, hashes.SHA3_384, hashes.SHA3_512, hashes.SHAKE128,
hashes.SHAKE256,
)
_fips_rsa_min_key_size = 2048
_fips_rsa_min_public_exponent = 65537
_fips_dsa_min_modulus = 1 << 2048
_fips_dh_min_key_size = 2048
_fips_dh_min_modulus = 1 << _fips_dh_min_key_size

def __init__(self):
self._binding = binding.Binding()
self._ffi = self._binding.ffi
self._lib = self._binding.lib
self._fips_enabled = self._is_fips_enabled()

self._cipher_registry = {}
self._register_default_ciphers()
self.activate_osrandom_engine()
if self._fips_enabled and self._lib.CRYPTOGRAPHY_NEEDS_OSRANDOM_ENGINE:
warnings.warn(
"OpenSSL FIPS mode is enabled. Can't enable DRBG fork safety.",
UserWarning
)
else:
self.activate_osrandom_engine()
self._dh_types = [self._lib.EVP_PKEY_DH]
if self._lib.Cryptography_HAS_EVP_PKEY_DHX:
self._dh_types.append(self._lib.EVP_PKEY_DHX)

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

def _is_fips_enabled(self):
fips_mode = getattr(self._lib, "FIPS_mode", lambda: 0)
mode = fips_mode()
if mode == 0:
# OpenSSL without FIPS pushes an error on the error stack
self._lib.ERR_clear_error()
return bool(mode)

def activate_builtin_random(self):
if self._lib.CRYPTOGRAPHY_NEEDS_OSRANDOM_ENGINE:
# Obtain a new structural reference.
Expand Down Expand Up @@ -222,6 +261,9 @@ def _evp_md_non_null_from_algorithm(self, algorithm):
return evp_md

def hash_supported(self, algorithm):
if self._fips_enabled and not isinstance(algorithm, self._fips_hashes):
return False

evp_md = self._evp_md_from_algorithm(algorithm)
return evp_md != self._ffi.NULL

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

def cipher_supported(self, cipher, mode):
if self._fips_enabled and not isinstance(cipher, self._fips_ciphers):
return False
try:
adapter = self._cipher_registry[type(cipher), type(mode)]
except KeyError:
Expand Down Expand Up @@ -1380,6 +1424,11 @@ def elliptic_curve_supported(self, curve):
errors[0]._lib_reason_match(
self._lib.ERR_LIB_EC,
self._lib.EC_R_UNKNOWN_GROUP
) or
# This occurs in FIPS mode for unsupported curves on RHEL
errors[0]._lib_reason_match(
self._lib.ERR_LIB_EC,
self._lib.EC_R_NOT_A_NIST_PRIME
)
)
return False
Expand Down Expand Up @@ -1777,6 +1826,16 @@ def _private_key_bytes(self, encoding, format, encryption_algorithm,

# TraditionalOpenSSL + PEM/DER
if format is serialization.PrivateFormat.TraditionalOpenSSL:
if (
self._fips_enabled and
not isinstance(
encryption_algorithm, serialization.NoEncryption
)
):
raise ValueError(
"Encrypted traditional OpenSSL format is not "
"supported in FIPS mode."
)
key_type = self._lib.EVP_PKEY_id(evp_pkey)

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

def x25519_supported(self):
if self._fips_enabled:
return False
return self._lib.CRYPTOGRAPHY_OPENSSL_110_OR_GREATER

def x448_load_public_bytes(self, data):
Expand Down Expand Up @@ -2200,9 +2261,13 @@ def x448_generate_key(self):
return _X448PrivateKey(self, evp_pkey)

def x448_supported(self):
if self._fips_enabled:
return False
return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111

def ed25519_supported(self):
if self._fips_enabled:
return False
return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111B

def ed25519_load_public_bytes(self, data):
Expand Down Expand Up @@ -2238,6 +2303,8 @@ def ed25519_generate_key(self):
return _Ed25519PrivateKey(self, evp_pkey)

def ed448_supported(self):
if self._fips_enabled:
return False
return not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111B

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

def aead_cipher_supported(self, cipher):
cipher_name = aead._aead_cipher_name(cipher)
if self._fips_enabled and cipher_name not in self._fips_aead:
return False
return (
self._lib.EVP_get_cipherbyname(cipher_name) != self._ffi.NULL
)
Expand Down Expand Up @@ -2453,6 +2522,8 @@ def serialize_key_and_certificates_to_pkcs12(self, name, key, cert, cas,
return self._read_mem_bio(bio)

def poly1305_supported(self):
if self._fips_enabled:
return False
return self._lib.Cryptography_HAS_POLY1305 == 1

def create_poly1305_ctx(self, key):
Expand Down
11 changes: 10 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@


def pytest_report_header(config):
return "OpenSSL: {}".format(openssl_backend.openssl_version_text())
return "\n".join([
"OpenSSL: {}".format(openssl_backend.openssl_version_text()),
"FIPS Enabled: {}".format(openssl_backend._fips_enabled),
])


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


def pytest_runtest_setup(item):
if openssl_backend._fips_enabled:
for marker in item.iter_markers(name="skip_fips"):
pytest.skip(marker.kwargs["reason"])


@pytest.fixture()
def backend(request):
required_interfaces = [
Expand Down
1 change: 1 addition & 0 deletions tests/hazmat/backends/test_openssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ def test_bn_to_int(self):
@pytest.mark.skipif(
not backend._lib.CRYPTOGRAPHY_NEEDS_OSRANDOM_ENGINE,
reason="Requires OpenSSL with ENGINE support and OpenSSL < 1.1.1d")
@pytest.mark.skip_fips(reason="osrandom engine disabled for FIPS")
class TestOpenSSLRandomEngine(object):
def setup(self):
# The default RAND engine is global and shared between
Expand Down
2 changes: 2 additions & 0 deletions tests/hazmat/backends/test_openssl_memleak.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ def skip_if_memtesting_not_supported():
)


@pytest.mark.skip_fips(reason="FIPS self-test sets allow_customize = 0")
@skip_if_memtesting_not_supported()
class TestAssertNoMemoryLeaks(object):
def test_no_leak_no_malloc(self):
Expand Down Expand Up @@ -205,6 +206,7 @@ def func():
"""))


@pytest.mark.skip_fips(reason="FIPS self-test sets allow_customize = 0")
@skip_if_memtesting_not_supported()
class TestOpenSSLMemoryLeaks(object):
@pytest.mark.parametrize("path", [
Expand Down
10 changes: 8 additions & 2 deletions tests/hazmat/primitives/test_aead.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,15 @@ def test_data_too_large(self):
aesgcm.encrypt(nonce, b"", FakeData())

@pytest.mark.parametrize("vector", _load_gcm_vectors())
def test_vectors(self, vector):
key = binascii.unhexlify(vector["key"])
def test_vectors(self, backend, vector):
nonce = binascii.unhexlify(vector["iv"])

if backend._fips_enabled and len(nonce) != 12:
# Red Hat disables non-96-bit IV support as part of its FIPS
# patches.
pytest.skip("Non-96-bit IVs unsupported in FIPS mode.")

key = binascii.unhexlify(vector["key"])
aad = binascii.unhexlify(vector["aad"])
ct = binascii.unhexlify(vector["ct"])
pt = binascii.unhexlify(vector.get("pt", b""))
Expand Down
13 changes: 13 additions & 0 deletions tests/hazmat/primitives/test_dh.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def test_dh_parameters_supported_with_q(self, backend, vector):
int(vector["g"], 16),
int(vector["q"], 16))

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

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

assert symkey == symkey_manual

@pytest.mark.skip_fips(reason="key_size too small for FIPS")
def test_symmetric_key_padding(self, backend):
"""
This test has specific parameters that produce a symmetric key
Expand Down Expand Up @@ -339,6 +342,11 @@ def test_symmetric_key_padding(self, backend):
os.path.join("asymmetric", "DH", "bad_exchange.txt"),
load_nist_vectors))
def test_bad_exchange(self, backend, vector):
if (
backend._fips_enabled and
int(vector["p1"]) < backend._fips_dh_min_modulus
):
pytest.skip("modulus too small for FIPS mode")
parameters1 = dh.DHParameterNumbers(int(vector["p1"]),
int(vector["g"]))
public1 = dh.DHPublicNumbers(int(vector["y1"]), parameters1)
Expand Down Expand Up @@ -370,6 +378,11 @@ def test_bad_exchange(self, backend, vector):
os.path.join("asymmetric", "DH", "vec.txt"),
load_nist_vectors))
def test_dh_vectors(self, backend, vector):
if (
backend._fips_enabled and
int(vector["p"]) < backend._fips_dh_min_modulus
):
pytest.skip("modulus too small for FIPS mode")
parameters = dh.DHParameterNumbers(int(vector["p"]),
int(vector["g"]))
public = dh.DHPublicNumbers(int(vector["y"]), parameters)
Expand Down
14 changes: 12 additions & 2 deletions tests/hazmat/primitives/test_dsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .fixtures_dsa import (
DSA_KEY_1024, DSA_KEY_2048, DSA_KEY_3072
)
from .utils import skip_fips_traditional_openssl
from ...doubles import DummyHashAlgorithm, DummyKeySerializationEncryption
from ...utils import (
load_fips_dsa_key_pair_vectors, load_fips_dsa_sig_vectors,
Expand All @@ -49,7 +50,7 @@ def test_skip_if_dsa_not_supported(backend):
@pytest.mark.requires_backend_interface(interface=DSABackend)
class TestDSA(object):
def test_generate_dsa_parameters(self, backend):
parameters = dsa.generate_parameters(1024, backend)
parameters = dsa.generate_parameters(2048, backend)
assert isinstance(parameters, dsa.DSAParameters)

def test_generate_invalid_dsa_parameters(self, backend):
Expand All @@ -65,6 +66,11 @@ def test_generate_invalid_dsa_parameters(self, backend):
)
)
def test_generate_dsa_keys(self, vector, backend):
if (
backend._fips_enabled and
vector['p'] < backend._fips_dsa_min_modulus
):
pytest.skip("Small modulus blocked in FIPS mode")
parameters = dsa.DSAParameterNumbers(
p=vector['p'],
q=vector['q'],
Expand All @@ -91,7 +97,7 @@ def test_generate_dsa_keys(self, vector, backend):
)

def test_generate_dsa_private_key_and_parameters(self, backend):
skey = dsa.generate_private_key(1024, backend)
skey = dsa.generate_private_key(2048, backend)
assert skey
numbers = skey.private_numbers()
skey_parameters = numbers.public_numbers.parameter_numbers
Expand Down Expand Up @@ -718,6 +724,7 @@ class TestDSASerialization(object):
)
)
def test_private_bytes_encrypted_pem(self, backend, fmt, password):
skip_fips_traditional_openssl(backend, fmt)
key_bytes = load_vectors_from_file(
os.path.join("asymmetric", "PKCS8", "unenc-dsa-pkcs8.pem"),
lambda pemfile: pemfile.read().encode()
Expand Down Expand Up @@ -812,6 +819,9 @@ def test_private_bytes_unencrypted(self, backend, encoding, fmt,
priv_num = key.private_numbers()
assert loaded_priv_num == priv_num

@pytest.mark.skip_fips(
reason="Traditional OpenSSL key format is not supported in FIPS mode."
)
@pytest.mark.parametrize(
("key_path", "encoding", "loader_func"),
[
Expand Down
Loading

0 comments on commit 4a245a6

Please sign in to comment.