Test installation with (fake) userspace FIPS

Based on userspace FIPS mode by Ondrej Moris.

Userspace FIPS mode fakes a Kernel in FIPS enforcing mode. User space
programs behave like the Kernel was booted in FIPS enforcing mode. Kernel
space code still runs in standard mode.

Fixes: https://pagure.io/freeipa/issue/8118
Signed-off-by: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
This commit is contained in:
Christian Heimes
2019-11-13 16:29:51 +01:00
parent cd887a48b5
commit 8124b1bd4c
12 changed files with 328 additions and 7 deletions

View File

@@ -159,6 +159,18 @@ jobs:
timeout: 3600
topology: *master_1repl_1client
fedora-latest/test_fips:
requires: [fedora-latest/build]
priority: 50
job:
class: RunPytest
args:
build_url: '{fedora-latest/build_url}'
test_suite: test_integration/test_fips.py
template: *ci-master-latest
timeout: 3600
topology: *master_1repl_1client
fedora-latest/test_forced_client_enrolment:
requires: [fedora-latest/build]
priority: 50

View File

@@ -159,6 +159,18 @@ jobs:
timeout: 3600
topology: *master_1repl_1client
fedora-previous/test_fips:
requires: [fedora-previous/build]
priority: 50
job:
class: RunPytest
args:
build_url: '{fedora-previous/build_url}'
test_suite: test_integration/test_fips.py
template: *ci-master-previous
timeout: 3600
topology: *master_1repl_1client
fedora-previous/test_forced_client_enrolment:
requires: [fedora-previous/build]
priority: 50

View File

@@ -159,6 +159,18 @@ jobs:
timeout: 3600
topology: *master_1repl_1client
fedora-rawhide/test_fips:
requires: [fedora-rawhide/build]
priority: 50
job:
class: RunPytest
args:
build_url: '{fedora-rawhide/build_url}'
test_suite: test_integration/test_fips.py
template: *ci-master-frawhide
timeout: 3600
topology: *master_1repl_1client
fedora-rawhide/test_forced_client_enrolment:
requires: [fedora-rawhide/build]
priority: 50

View File

@@ -43,6 +43,7 @@ class Config(pytest_multihost.config.Config):
'dns_forwarder',
'domain_level',
'log_journal_since',
'fips_mode',
}
def __init__(self, **kwargs):
@@ -67,6 +68,7 @@ class Config(pytest_multihost.config.Config):
self.log_journal_since = kwargs.get('log_journal_since') or '-1h'
if self.domain_level is None:
self.domain_level = MAX_DOMAIN_LEVEL
self.fips_mode = kwargs.get('fips_mode', False)
def get_domain_class(self):
return Domain

View File

@@ -63,6 +63,8 @@ _setting_infos = (
_SettingInfo('domain_level', 'DOMAINLVL', MAX_DOMAIN_LEVEL),
_SettingInfo('log_journal_since', 'LOG_JOURNAL_SINCE', '-1h'),
# userspace FIPS mode
_SettingInfo('fips_mode', 'IPA_FIPS_MODE', False),
)

View File

@@ -0,0 +1,67 @@
#
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
"""FIPS testing helpers
Based on userspace FIPS mode by Ondrej Moris.
Userspace FIPS mode fakes a Kernel in FIPS enforcing mode. User space
programs behave like the Kernel was booted in FIPS enforcing mode. Kernel
space code still runs in standard mode.
"""
import os
from ipaplatform.paths import paths
FIPS_OVERLAY_DIR = "/var/tmp/userspace-fips"
FIPS_OVERLAY = os.path.join(FIPS_OVERLAY_DIR, "fips_enabled")
SYSTEM_FIPS = "/etc/system-fips"
def is_fips_enabled(host):
"""Check if host has """
result = host.run_command(
["cat", paths.PROC_FIPS_ENABLED], raiseonerr=False
)
if result.returncode == 1:
# FIPS mode not available
return None
elif result.returncode == 0:
return result.stdout_text.strip() == "1"
else:
raise RuntimeError(result.stderr_text)
def enable_userspace_fips(host):
# create /etc/system-fips
host.put_file_contents(SYSTEM_FIPS, "# userspace fips\n")
# fake Kernel FIPS mode with bind mount
host.run_command(["mkdir", "-p", FIPS_OVERLAY_DIR])
host.put_file_contents(FIPS_OVERLAY, "1\n")
host.run_command(
["mount", "--bind", FIPS_OVERLAY, paths.PROC_FIPS_ENABLED]
)
# set crypto policy to FIPS mode
host.run_command(["update-crypto-policies", "--show"])
host.run_command(["update-crypto-policies", "--set", "FIPS"])
# sanity check
assert is_fips_enabled(host)
result = host.run_command(
["openssl", "md5", "/dev/null"], raiseonerr=False
)
assert result.returncode == 1
assert "EVP_DigestInit_ex:disabled for FIPS" in result.stderr_text
def disable_userspace_fips(host):
host.run_command(["rm", "-f", SYSTEM_FIPS])
host.run_command(["update-crypto-policies", "--set", "DEFAULT"])
result = host.run_command(
["umount", paths.PROC_FIPS_ENABLED], raiseonerr=False
)
host.run_command(["rm", "-rf", FIPS_OVERLAY_DIR])
if result.returncode != 0:
raise RuntimeError(result.stderr_text)
# sanity check
assert not is_fips_enabled(host)
host.run_command(["openssl", "md5", "/dev/null"])

View File

@@ -27,6 +27,10 @@ import pytest_multihost.host
from ipaplatform.paths import paths
from ipapython import ipaldap
from .fips import (
is_fips_enabled, enable_userspace_fips, disable_userspace_fips
)
class LDAPClientWithoutCertCheck(ipaldap.LDAPClient):
"""Adds an option to disable certificate check for TLS connection
@@ -58,6 +62,61 @@ class LDAPClientWithoutCertCheck(ipaldap.LDAPClient):
class Host(pytest_multihost.host.Host):
"""Representation of a remote IPA host"""
def __init__(self, domain, hostname, role, ip=None,
external_hostname=None, username=None, password=None,
test_dir=None, host_type=None):
super().__init__(
domain, hostname, role, ip=ip,
external_hostname=external_hostname, username=username,
password=password, test_dir=test_dir, host_type=host_type
)
self._fips_mode = None
self._userspace_fips = False
@property
def is_fips_mode(self):
"""Check and cache if a system is in FIPS mode
"""
if self._fips_mode is None:
self._fips_mode = is_fips_enabled(self)
return self._fips_mode
@property
def is_userspace_fips(self):
"""Check if host uses fake userspace FIPS
"""
return self._userspace_fips
def enable_userspace_fips(self):
"""Enable fake userspace FIPS mode
The call has no effect if the system is already in FIPS mode.
:return: True if system was modified, else None
"""
if not self.is_fips_mode:
enable_userspace_fips(self)
self._fips_mode = True
self._userspace_fips = True
return True
else:
return False
def disable_userspace_fips(self):
"""Disable fake userspace FIPS mode
The call has no effect if userspace FIPS mode is not enabled.
:return: True if system was modified, else None
"""
if self.is_userspace_fips:
disable_userspace_fips(self)
self._userspace_fips = False
self._fips_mode = False
return True
else:
return False
@staticmethod
def _make_host(domain, hostname, role, ip, external_hostname):
# We need to determine the type of the host, this depends on the domain

View File

@@ -37,6 +37,7 @@ class IntegrationTest:
required_extra_roles = []
topology = None
domain_level = None
fips_mode = None
@classmethod
def setup_class(cls):
@@ -60,12 +61,30 @@ class IntegrationTest:
def get_domains(cls):
return [cls.domain] + cls.ad_domains
@classmethod
def enable_fips_mode(cls):
for host in cls.get_all_hosts():
if not host.is_fips_mode:
host.enable_userspace_fips()
@classmethod
def disable_fips_mode(cls):
for host in cls.get_all_hosts():
if host.is_userspace_fips:
host.disable_userspace_fips()
@classmethod
def install(cls, mh):
if cls.domain_level is not None:
domain_level = cls.domain_level
else:
domain_level = cls.master.config.domain_level
if cls.master.config.fips_mode:
cls.fips_mode = True
if cls.fips_mode:
cls.enable_fips_mode()
if cls.topology is None:
return
else:
@@ -83,3 +102,5 @@ class IntegrationTest:
tasks.uninstall_master(replica)
for client in cls.clients:
tasks.uninstall_client(client)
if cls.fips_mode:
cls.disable_fips_mode()

View File

@@ -98,6 +98,16 @@ def dnszone_add_dnssec(host, test_zone):
return host.run_command(args)
def dnssec_install_master(host):
args = [
"ipa-dns-install",
"--dnssec-master",
"--forwarder", host.config.dns_forwarder,
"-U",
]
return host.run_command(args)
class TestInstallDNSSECLast(IntegrationTest):
"""Simple DNSSEC test
@@ -114,13 +124,7 @@ class TestInstallDNSSECLast(IntegrationTest):
def test_install_dnssec_master(self):
"""Both master and replica have DNS installed"""
args = [
"ipa-dns-install",
"--dnssec-master",
"--forwarder", self.master.config.dns_forwarder,
"-U",
]
self.master.run_command(args)
dnssec_install_master(self.master)
def test_if_zone_is_signed_master(self):
# add zone with enabled DNSSEC signing on master

View File

@@ -0,0 +1,127 @@
#
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
#
"""Smoke tests for FreeIPA installation in (fake) userspace FIPS mode
"""
from ipapython.dn import DN
from ipapython.ipautil import ipa_generate_password, realm_to_suffix
from ipatests.pytest_ipa.integration import tasks
from ipatests.pytest_ipa.integration import fips
from ipatests.test_integration.base import IntegrationTest
from .test_dnssec import (
test_zone,
dnssec_install_master,
dnszone_add_dnssec,
wait_until_record_is_signed,
)
class TestInstallFIPS(IntegrationTest):
num_replicas = 1
num_clients = 1
fips_mode = True
@classmethod
def install(cls, mh):
super(TestInstallFIPS, cls).install(mh)
# sanity check
for host in cls.get_all_hosts():
assert host.is_fips_mode
assert fips.is_fips_enabled(host)
# patch named-pkcs11 crypto policy
# see RHBZ#1772111
for host in [cls.master] + cls.replicas:
host.run_command(
[
"sed",
"-i",
"-E",
"s/RSAMD5;//g",
"/etc/crypto-policies/back-ends/bind.config",
]
)
# master with CA, KRA, DNS+DNSSEC
tasks.install_master(cls.master, setup_dns=True, setup_kra=True)
# replica with CA, KRA, DNS
tasks.install_replica(
cls.master,
cls.replicas[0],
setup_dns=True,
setup_ca=True,
setup_kra=True,
)
tasks.install_clients([cls.master] + cls.replicas, cls.clients)
def test_basic(self):
client = self.clients[0]
tasks.kinit_admin(client)
client.run_command(["ipa", "ping"])
def test_dnssec(self):
dnssec_install_master(self.master)
# DNSSEC zone
dnszone_add_dnssec(self.master, test_zone)
assert wait_until_record_is_signed(
self.master.ip, test_zone, timeout=100
), ("Zone %s is not signed (master)" % test_zone)
# test replica
assert wait_until_record_is_signed(
self.replicas[0].ip, test_zone, timeout=200
), ("DNS zone %s is not signed (replica)" % test_zone)
def test_vault_basic(self):
vault_name = "testvault"
vault_password = ipa_generate_password()
vault_data = "SSBsb3ZlIENJIHRlc3RzCg=="
# create vault
self.master.run_command(
[
"ipa",
"vault-add",
vault_name,
"--password",
vault_password,
"--type",
"symmetric",
]
)
# archive secret
self.master.run_command(
[
"ipa",
"vault-archive",
vault_name,
"--password",
vault_password,
"--data",
vault_data,
]
)
self.master.run_command(
[
"ipa",
"vault-retrieve",
vault_name,
"--password",
vault_password,
]
)
def test_krb_enctypes(self):
realm = self.master.domain.realm
suffix = realm_to_suffix(realm)
dn = DN(("cn", realm), ("cn", "kerberos")) + suffix
args = ["krbSupportedEncSaltTypes", "krbDefaultEncSaltTypes"]
for host in [self.master] + self.replicas:
result = tasks.ldapsearch_dm(host, str(dn), args, scope="base")
assert "camellia" not in result.stdout_text
assert "aes256-cts" in result.stdout_text
assert "aes128-cts" in result.stdout_text
# test that update does not add camellia
self.master.run_command(["ipa-server-upgrade"])
result = tasks.ldapsearch_dm(self.master, str(dn), args, scope="base")
assert "camellia" not in result.stdout_text

View File

@@ -43,6 +43,7 @@ DEFAULT_OUTPUT_DICT = {
"admin_password": "Secret123",
"domain_level": MAX_DOMAIN_LEVEL,
"log_journal_since": "-1h",
"fips_mode": False,
}
DEFAULT_OUTPUT_ENV = {
@@ -62,6 +63,7 @@ DEFAULT_OUTPUT_ENV = {
"IPADEBUG": "",
"DOMAINLVL": str(MAX_DOMAIN_LEVEL),
"LOG_JOURNAL_SINCE": "-1h",
"IPA_FIPS_MODE": "",
}
DEFAULT_INPUT_ENV = {

View File

@@ -194,6 +194,7 @@ ipa_class_members = {
{'ad_admin_name': dir(str)},
{'ad_admin_password': dir(str)},
{'domain_level': dir(str)},
{'fips_mode': dir(bool)},
]},
{'domain': [
{'realm': dir(str)},