Hello,

a patch with DNSSEC CI tests attached.

* Two types of installation tested
* Tests check if zones are signed on both replica and master
* The root zone test also checks chain of trust

Can somebody very familiar with pytest do review? I'm not sure If I used pytest friendly constructions.

PS: test may failure occasionally due a bug in DNSSEC code, but CI test itself should be OK

Useful information: http://www.freeipa.org/page/Howto/DNSSEC

--
Martin Basti

From cf9773ce61a8e914c3bcb9441913c74f467ba067 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Thu, 23 Oct 2014 15:06:34 +0200
Subject: [PATCH] DNSSEC CI tests

Tests:
* install master, replica, then instal DNSSEC on master
  * test if zone is signed (added on master)
  * test if zone is signed (added on replica)

* install master with DNSSEC, then install replica
  * test if root zone is signed
  * add zone, verify signatures using our root zone

https://fedorahosted.org/freeipa/ticket/4657
---
 ipaplatform/base/paths.py                |   1 +
 ipatests/test_integration/tasks.py       |  28 ++-
 ipatests/test_integration/test_dnssec.py | 286 +++++++++++++++++++++++++++++++
 3 files changed, 307 insertions(+), 8 deletions(-)
 create mode 100644 ipatests/test_integration/test_dnssec.py

diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 11c7e9212d9f7593da01dc65bd4ca0a81b4fd476..3ad007ce93de66cbe4e23f1624883e8dd1952c2d 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -135,6 +135,7 @@ class BasePathNamespace(object):
     SYSTEMD_IPA_SERVICE = "/etc/systemd/system/multi-user.target.wants/ipa.service"
     SYSTEMD_SSSD_SERVICE = "/etc/systemd/system/multi-user.target.wants/sssd.service"
     SYSTEMD_PKI_TOMCAT_SERVICE = "/etc/systemd/system/pki-tomcatd.target.wants/pki-tomcatd@pki-tomcat.service"
+    DNSSEC_TRUSTED_KEY = "/etc/trusted-key.key"
     HOME_DIR = "/home"
     ROOT_IPA_CACHE = "/root/.ipa_cache"
     ROOT_PKI = "/root/.pki"
diff --git a/ipatests/test_integration/tasks.py b/ipatests/test_integration/tasks.py
index 271d726ca2c3c8814608919b9c956a8ceef53301..c83fc65f18e96c6aa8f3e94fadb6a600143a6bd4 100644
--- a/ipatests/test_integration/tasks.py
+++ b/ipatests/test_integration/tasks.py
@@ -184,7 +184,7 @@ def enable_replication_debugging(host):
                      stdin_text=logging_ldif)
 
 
-def install_master(host):
+def install_master(host, setup_dns=True):
     host.collect_log(paths.IPASERVER_INSTALL_LOG)
     host.collect_log(paths.IPACLIENT_INSTALL_LOG)
     inst = host.domain.realm.replace('.', '-')
@@ -194,20 +194,27 @@ def install_master(host):
     apply_common_fixes(host)
     fix_apache_semaphores(host)
 
-    host.run_command(['ipa-server-install', '-U',
-                      '-r', host.domain.name,
-                      '-p', host.config.dirman_password,
-                      '-a', host.config.admin_password,
-                      '--setup-dns',
-                      '--forwarder', host.config.dns_forwarder])
+    args = [
+        'ipa-server-install', '-U',
+        '-r', host.domain.name,
+        '-p', host.config.dirman_password,
+        '-a', host.config.admin_password
+    ]
 
+    if setup_dns:
+        args.extend([
+            '--setup-dns',
+            '--forwarder', host.config.dns_forwarder
+        ])
+
+    host.run_command(args)
     enable_replication_debugging(host)
     setup_sssd_debugging(host)
 
     kinit_admin(host)
 
 
-def install_replica(master, replica, setup_ca=True):
+def install_replica(master, replica, setup_ca=True, setup_dns=False):
     replica.collect_log(paths.IPAREPLICA_INSTALL_LOG)
     replica.collect_log(paths.IPAREPLICA_CONNCHECK_LOG)
 
@@ -231,6 +238,11 @@ def install_replica(master, replica, setup_ca=True):
             replica_filename]
     if setup_ca:
         args.append('--setup-ca')
+    if setup_dns:
+        args.extend([
+            '--setup-dns',
+            '--forwarder', replica.config.dns_forwarder
+        ])
     replica.run_command(args)
 
     enable_replication_debugging(replica)
diff --git a/ipatests/test_integration/test_dnssec.py b/ipatests/test_integration/test_dnssec.py
new file mode 100644
index 0000000000000000000000000000000000000000..b12434ca31665e629e6320fb1d2b427345ce2528
--- /dev/null
+++ b/ipatests/test_integration/test_dnssec.py
@@ -0,0 +1,286 @@
+#
+# Copyright (C) 2015  FreeIPA Contributors see COPYING for license
+#
+
+import dns.dnssec
+import dns.resolver
+import dns.name
+import time
+import os
+
+from ipapython.dn import DN
+from ipatests.test_integration.base import IntegrationTest
+from ipatests.test_integration import tasks
+from ipaplatform.paths import paths
+
+test_zone = "dnssec.test."
+test_zone_repl = "dnssec-replica.test."
+root_zone = "."
+example_test_zone = "example.test."
+
+
+def resolve_with_dnssec(nameserver, query, log, rtype="SOA"):
+    res = dns.resolver.Resolver()
+    res.nameservers = [nameserver]
+    res.lifetime = 10  # wait max 10 seconds for reply
+    # enable Authenticated Data + Checking Disabled flags
+    res.set_flags(dns.flags.AD | dns.flags.CD)
+
+    # enable EDNS v0 + enable DNSSEC-Ok flag
+    res.use_edns(0, dns.flags.DO, 0)
+
+    ans = res.query(query, rtype)
+    return ans
+
+
+def is_record_signed(nameserver, query, log, rtype="SOA"):
+    try:
+        ans = resolve_with_dnssec(nameserver, query, log, rtype=rtype)
+        ans.response.find_rrset(ans.response.answer, dns.name.from_text(query),
+                                dns.rdataclass.IN, dns.rdatatype.RRSIG,
+                                dns.rdatatype.from_text(rtype))
+    except KeyError:
+        return False
+    except dns.exception.DNSException:
+        return False
+    return True
+
+
+def wait_until_record_is_signed(nameserver, record, log, rtype="SOA",
+                                timeout=100):
+    """
+    Returns True if record is signed, or False on timeout
+    :param nameserver: nameserver to query
+    :param record: query
+    :param log: logger
+    :param rtype: record type
+    :param timeout:
+    :return: True if records is signed, False if timeout
+    """
+    log.info("Waiting for signed %s record of %s from server %s (timeout %s "
+             "sec)", rtype, record, nameserver, timeout)
+    wait_until = time.time() + timeout
+    while time.time() < wait_until:
+        if is_record_signed(nameserver, record, log, rtype=rtype):
+            return True
+        time.sleep(1)
+    return False
+
+
+class TestInstallDNSSECLast(IntegrationTest):
+    """Simple DNSSEC test
+
+    Install a server and a replica with DNS, then reinstall server
+    as DNSSEC master
+    """
+    num_replicas = 1
+    topology = 'star'
+
+    @classmethod
+    def install(cls, mh):
+        tasks.install_master(cls.master, setup_dns=True)
+        tasks.install_replica(cls.master, cls.replicas[0], setup_dns=True)
+
+    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,
+            "-p", self.master.config.dirman_password,
+            "-U",
+        ]
+        self.master.run_command(args)
+
+    def test_if_zone_is_signed_master(self):
+        # add zone with enabled DNSSEC signing on master
+        args = [
+            "ipa",
+            "dnszone-add", test_zone,
+            "--dnssec", "true",
+        ]
+        self.master.run_command(args)
+
+        # test master
+        assert wait_until_record_is_signed(
+            self.master.ip, test_zone, self.log, timeout=100
+        ), "Zone %s is not signed (master)" % test_zone
+
+        # test replica
+        assert wait_until_record_is_signed(
+            self.replicas[0].ip, test_zone, self.log, timeout=200
+        ), "DNS zone %s is not signed (replica)" % test_zone
+
+    def test_if_zone_is_signed_replica(self):
+        # add zone with enabled DNSSEC signing on replica
+        args = [
+            "ipa",
+            "dnszone-add", test_zone_repl,
+            "--dnssec", "true",
+        ]
+        self.replicas[0].run_command(args)
+
+        # test replica
+        assert wait_until_record_is_signed(
+            self.replicas[0].ip, test_zone_repl, self.log, timeout=300
+        ), "Zone %s is not signed (replica)" % test_zone_repl
+
+        # we do not need to wait, on master zones should be singed faster
+        # than on replicas
+
+        assert wait_until_record_is_signed(
+            self.master.ip, test_zone_repl, self.log, timeout=5
+        ), "DNS zone %s is not signed (master)" % test_zone
+
+
+class TestInstallDNSSECFirst(IntegrationTest):
+    """Simple DNSSEC test
+
+    Install the server with DNSSEC and then install the replica with DNS
+    """
+    num_replicas = 1
+    topology = 'star'
+
+    @classmethod
+    def install(cls, mh):
+        tasks.install_master(cls.master, setup_dns=False)
+        args = [
+            "ipa-dns-install",
+            "--dnssec-master",
+            "--forwarder", cls.master.config.dns_forwarder,
+            "-p", cls.master.config.dirman_password,
+            "-U",
+        ]
+        cls.master.run_command(args)
+
+        tasks.install_replica(cls.master, cls.replicas[0], setup_dns=True)
+
+        # backup trusted key
+        tasks.backup_file(cls.master, paths.DNSSEC_TRUSTED_KEY)
+        tasks.backup_file(cls.replicas[0], paths.DNSSEC_TRUSTED_KEY)
+
+    @classmethod
+    def uninstall(cls, mh):
+        # restore trusted key
+        tasks.restore_files(cls.master)
+        tasks.restore_files(cls.replicas[0])
+
+        super(TestInstallDNSSECFirst, cls).uninstall(mh)
+
+    def test_sign_root_zone(self):
+        args = [
+            "ipa", "dnszone-add", root_zone, "--dnssec", "true"
+        ]
+        self.master.run_command(args)
+
+        # make BIND happy, and delegate zone which contains A record of master
+        args = [
+            "ipa", "dnsrecord-add", root_zone, self.master.domain.name,
+            "--ns-rec=" + self.master.hostname
+        ]
+        self.master.run_command(args)
+
+        # test master
+        assert wait_until_record_is_signed(
+            self.master.ip, root_zone, self.log, timeout=100
+        ), "Zone %s is not signed (master)" % root_zone
+
+        # test replica
+        assert wait_until_record_is_signed(
+            self.replicas[0].ip, root_zone, self.log, timeout=300
+        ), "Zone %s is not signed (replica)" % root_zone
+
+    def test_chain_of_trust(self):
+        """
+        Validate signed DNS records, using our own signed root zone
+        :return:
+        """
+
+        # add test zone
+        args = [
+            "ipa", "dnszone-add", example_test_zone, "--dnssec", "true"
+        ]
+
+        self.master.run_command(args)
+
+        # wait until zone is signed
+        assert wait_until_record_is_signed(
+            self.master.ip, example_test_zone, self.log, timeout=100
+        ), "Zone %s is not signed (master)" % example_test_zone
+
+        # GET DNSKEY records from zone
+        ans = resolve_with_dnssec(self.master.ip, example_test_zone, self.log,
+                                  rtype="DNSKEY")
+        dnskey_rrset = ans.response.get_rrset(ans.response.answer,
+                                              dns.name.from_text(example_test_zone),
+                                              dns.rdataclass.IN,
+                                              dns.rdatatype.DNSKEY)
+        assert dnskey_rrset, "No DNSKEY records received"
+
+        self.log.debug("DNSKEY records returned: %s", dnskey_rrset.to_text())
+
+        # generate DS records
+        ds_records = []
+        for key_rdata in dnskey_rrset:
+            if key_rdata.flags != 257:
+                continue  # it is not KSK
+            ds_records.append(dns.dnssec.make_ds(example_test_zone, key_rdata,
+                                                 'sha256'))
+        assert ds_records, "No KSK returned from the %s zone" % example_test_zone
+
+        self.log.debug("DS records for %s created: %r", example_test_zone,
+                       ds_records)
+
+        # add DS records to root zone
+        args = [
+            "ipa", "dnsrecord-add", root_zone, example_test_zone,
+            # DS record requires to coexists with NS
+            "--ns-rec", self.master.hostname,
+        ]
+        for ds in ds_records:
+            args.append("--ds-rec")
+            args.append(ds.to_text())
+
+        self.master.run_command(args)
+
+        # extract DSKEY from root zone
+        ans = resolve_with_dnssec(self.master.ip, root_zone, self.log,
+                                  rtype="DNSKEY")
+        dnskey_rrset = ans.response.get_rrset(ans.response.answer,
+                                              dns.name.from_text(root_zone),
+                                              dns.rdataclass.IN,
+                                              dns.rdatatype.DNSKEY)
+        assert dnskey_rrset, "No DNSKEY records received"
+
+        self.log.debug("DNSKEY records returned: %s", dnskey_rrset.to_text())
+
+        # export trust keys for root zone
+        root_key_rdatas = []
+        for key_rdata in dnskey_rrset:
+            if key_rdata.flags != 257:
+                continue  # it is not KSK
+            root_key_rdatas.append(key_rdata)
+
+        assert root_key_rdatas, "No KSK returned from the root zone"
+
+        root_keys_rrset = dns.rrset.from_rdata_list(dnskey_rrset.name,
+                                                    dnskey_rrset.ttl,
+                                                    root_key_rdatas)
+        self.log.debug("Root zone trusted key: %s", root_keys_rrset.to_text())
+
+        # set trusted key for our root zone
+        self.master.put_file_contents(paths.DNSSEC_TRUSTED_KEY,
+                                      root_keys_rrset.to_text() + '\n')
+        self.replicas[0].put_file_contents(paths.DNSSEC_TRUSTED_KEY,
+                                           root_keys_rrset.to_text() + '\n')
+
+        # verify signatures
+        args = [
+            "drill", "@localhost", "-k",
+            paths.DNSSEC_TRUSTED_KEY, "-S",
+            example_test_zone, "SOA"
+        ]
+
+        # test if signature chains are valid
+        self.master.run_command(args)
+        self.replicas[0].run_command(args)
-- 
2.1.0

-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to