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

Reply via email to