URL: https://github.com/freeipa/freeipa/pull/3902 Author: tiran Title: #3902: [Backport][ipa-4-8] Test installation with (fake) userspace FIPS Action: opened
PR body: """ Manual backport of PR #3897 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 <chei...@redhat.com> """ To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/3902/head:pr3902 git checkout pr3902
From 000113c3d65fb08b33ed5af60999f9a22078e411 Mon Sep 17 00:00:00 2001 From: Christian Heimes <chei...@redhat.com> Date: Wed, 13 Nov 2019 16:29:51 +0100 Subject: [PATCH] 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 <chei...@redhat.com> --- .../prci_definitions/nightly_ipa-4-8.yaml | 12 ++ ipatests/pytest_ipa/integration/config.py | 2 + ipatests/pytest_ipa/integration/env_config.py | 2 + ipatests/pytest_ipa/integration/fips.py | 67 +++++++++ ipatests/pytest_ipa/integration/host.py | 59 ++++++++ ipatests/test_integration/base.py | 21 +++ ipatests/test_integration/test_dnssec.py | 18 ++- ipatests/test_integration/test_fips.py | 127 ++++++++++++++++++ ipatests/test_integration/test_testconfig.py | 2 + pylint_plugins.py | 1 + 10 files changed, 304 insertions(+), 7 deletions(-) create mode 100644 ipatests/pytest_ipa/integration/fips.py create mode 100644 ipatests/test_integration/test_fips.py diff --git a/ipatests/prci_definitions/nightly_ipa-4-8.yaml b/ipatests/prci_definitions/nightly_ipa-4-8.yaml index cce5779a96..b9b647f278 100644 --- a/ipatests/prci_definitions/nightly_ipa-4-8.yaml +++ b/ipatests/prci_definitions/nightly_ipa-4-8.yaml @@ -159,6 +159,18 @@ jobs: timeout: 3600 topology: *master_1repl_1client + fedora-30/test_fips: + requires: [fedora-30/build] + priority: 50 + job: + class: RunPytest + args: + build_url: '{fedora-30/build_url}' + test_suite: test_integration/test_fips.py + template: *ci-master-f30 + timeout: 3600 + topology: *master_1repl_1client + fedora-30/test_forced_client_enrolment: requires: [fedora-30/build] priority: 50 diff --git a/ipatests/pytest_ipa/integration/config.py b/ipatests/pytest_ipa/integration/config.py index b6ef8a0446..8a69eb04de 100644 --- a/ipatests/pytest_ipa/integration/config.py +++ b/ipatests/pytest_ipa/integration/config.py @@ -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 @@ def __init__(self, **kwargs): 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 diff --git a/ipatests/pytest_ipa/integration/env_config.py b/ipatests/pytest_ipa/integration/env_config.py index 19f0a2af9c..efbfbf0549 100644 --- a/ipatests/pytest_ipa/integration/env_config.py +++ b/ipatests/pytest_ipa/integration/env_config.py @@ -63,6 +63,8 @@ _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), ) diff --git a/ipatests/pytest_ipa/integration/fips.py b/ipatests/pytest_ipa/integration/fips.py new file mode 100644 index 0000000000..eef2973492 --- /dev/null +++ b/ipatests/pytest_ipa/integration/fips.py @@ -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"]) diff --git a/ipatests/pytest_ipa/integration/host.py b/ipatests/pytest_ipa/integration/host.py index 17ff6cb3ad..33f7c732ac 100644 --- a/ipatests/pytest_ipa/integration/host.py +++ b/ipatests/pytest_ipa/integration/host.py @@ -27,6 +27,10 @@ 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 @@ def _connect(self): 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 diff --git a/ipatests/test_integration/base.py b/ipatests/test_integration/base.py index 57bca6cac2..07f7c15d05 100644 --- a/ipatests/test_integration/base.py +++ b/ipatests/test_integration/base.py @@ -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 @@ def get_all_hosts(cls): 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 @@ def uninstall(cls, mh): tasks.uninstall_master(replica) for client in cls.clients: tasks.uninstall_client(client) + if cls.fips_mode: + cls.disable_fips_mode() diff --git a/ipatests/test_integration/test_dnssec.py b/ipatests/test_integration/test_dnssec.py index 874eb06eba..0689fc7efc 100644 --- a/ipatests/test_integration/test_dnssec.py +++ b/ipatests/test_integration/test_dnssec.py @@ -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 @@ def install(cls, mh): 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 diff --git a/ipatests/test_integration/test_fips.py b/ipatests/test_integration/test_fips.py new file mode 100644 index 0000000000..680a44926d --- /dev/null +++ b/ipatests/test_integration/test_fips.py @@ -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 diff --git a/ipatests/test_integration/test_testconfig.py b/ipatests/test_integration/test_testconfig.py index b4a72937ae..8749d7144f 100644 --- a/ipatests/test_integration/test_testconfig.py +++ b/ipatests/test_integration/test_testconfig.py @@ -43,6 +43,7 @@ "admin_password": "Secret123", "domain_level": MAX_DOMAIN_LEVEL, "log_journal_since": "-1h", + "fips_mode": False, } DEFAULT_OUTPUT_ENV = { @@ -62,6 +63,7 @@ "IPADEBUG": "", "DOMAINLVL": str(MAX_DOMAIN_LEVEL), "LOG_JOURNAL_SINCE": "-1h", + "IPA_FIPS_MODE": "", } DEFAULT_INPUT_ENV = { diff --git a/pylint_plugins.py b/pylint_plugins.py index 0cd854f797..70bd1d5b63 100644 --- a/pylint_plugins.py +++ b/pylint_plugins.py @@ -194,6 +194,7 @@ def fake_class(name_or_class_obj, members=()): {'ad_admin_name': dir(str)}, {'ad_admin_password': dir(str)}, {'domain_level': dir(str)}, + {'fips_mode': dir(bool)}, ]}, {'domain': [ {'realm': dir(str)},
_______________________________________________ FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org Fedora Code of Conduct: https://docs.fedoraproject.org/en-US/project/code-of-conduct/ List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines List Archives: https://lists.fedorahosted.org/archives/list/freeipa-devel@lists.fedorahosted.org