]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
[master] add dnssec-coverage tool
authorEvan Hunt <each@isc.org>
Wed, 20 Mar 2013 21:31:10 +0000 (14:31 -0700)
committerEvan Hunt <each@isc.org>
Wed, 20 Mar 2013 21:39:13 +0000 (14:39 -0700)
3528. [func] New "dnssec-coverage" command scans the timing
metadata for a set of DNSSEC keys and reports if a
lapse in signing coverage has been scheduled
inadvertently. (Note: This tool depends on python;
it will not be built or installed on systems that
do not have a python interpreter.) [RT #28098]

29 files changed:
CHANGES
bin/python/.gitignore
bin/python/Makefile.in
bin/python/dnssec-coverage.docbook [new file with mode: 0644]
bin/python/dnssec-coverage.py.in [new file with mode: 0755]
bin/tests/system/conf.sh.in
bin/tests/system/coverage/01-ksk-inactive/README [new file with mode: 0644]
bin/tests/system/coverage/01-ksk-inactive/expect [new file with mode: 0644]
bin/tests/system/coverage/02-zsk-inactive/README [new file with mode: 0644]
bin/tests/system/coverage/02-zsk-inactive/expect [new file with mode: 0644]
bin/tests/system/coverage/03-ksk-unpublished/README [new file with mode: 0644]
bin/tests/system/coverage/03-ksk-unpublished/expect [new file with mode: 0644]
bin/tests/system/coverage/04-zsk-unpublished/README [new file with mode: 0644]
bin/tests/system/coverage/04-zsk-unpublished/expect [new file with mode: 0644]
bin/tests/system/coverage/05-ksk-unpub-active/README [new file with mode: 0644]
bin/tests/system/coverage/05-ksk-unpub-active/expect [new file with mode: 0644]
bin/tests/system/coverage/06-zsk-unpub-active/README [new file with mode: 0644]
bin/tests/system/coverage/06-zsk-unpub-active/expect [new file with mode: 0644]
bin/tests/system/coverage/07-ksk-ttl/README [new file with mode: 0644]
bin/tests/system/coverage/07-ksk-ttl/expect [new file with mode: 0644]
bin/tests/system/coverage/08-zsk-ttl/README [new file with mode: 0644]
bin/tests/system/coverage/08-zsk-ttl/expect [new file with mode: 0644]
bin/tests/system/coverage/clean.sh [new file with mode: 0644]
bin/tests/system/coverage/setup.sh [new file with mode: 0644]
bin/tests/system/coverage/tests.sh [new file with mode: 0644]
bin/tests/system/dnssec/tests.sh
configure
configure.in
doc/arm/Bv9ARM-book.xml

diff --git a/CHANGES b/CHANGES
index 4c987efd83177c85f5c2a2cb835a4bbc153ca7d1..431b65bc20b4821a170fd7b040f0dabf73876678 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,10 @@
+3528.  [func]          New "dnssec-coverage" command scans the timing
+                       metadata for a set of DNSSEC keys and reports if a
+                       lapse in signing coverage has been scheduled
+                       inadvertently. (Note: This tool depends on python;
+                       it will not be built or installed on systems that
+                       do not have a python interpreter.) [RT #28098]
+
 3527.  [compat]        Add a URI to allow applications to explicitly
                        request a particular XML schema from the statistics
                        channel, returning 404 if not supported. [RT #32481]
index 7df1706762658492935e5f2299e9f1a339c1b630..f770f2396e7bed693175c6c42d58918b3b711fd2 100644 (file)
@@ -1,2 +1,4 @@
 dnssec-checkds
 dnssec-checkds.py
+dnssec-coverage
+dnssec-coverage.py
index fe647fdde7adf1d12f0390574893ed89c8d6db3b..12695ed5860f1a0cdd0b686c03ba73e8118c804d 100644 (file)
@@ -22,17 +22,19 @@ top_srcdir =        @top_srcdir@
 
 PYTHON =       @PYTHON@
 
-TARGETS =      dnssec-checkds
-SRCS =         dnssec-checkds.py
+TARGETS =      dnssec-checkds dnssec-coverage
+SRCS =         dnssec-checkds.py dnssec-coverage.py
 
-MANPAGES =     dnssec-checkds.8
-HTMLPAGES =    dnssec-checkds.html
+MANPAGES =     dnssec-checkds.8 dnssec-coverage.8
+HTMLPAGES =    dnssec-checkds.html dnssec-coverage.html
 MANOBJS =      ${MANPAGES} ${HTMLPAGES}
 
 @BIND9_MAKE_RULES@
 
 dnssec-checkds: dnssec-checkds.py
 
+dnssec-coverage: dnssec-coverage.py
+
 doc man:: ${MANOBJS}
 
 docclean manclean maintainer-clean::
@@ -44,10 +46,12 @@ installdirs:
 
 install:: ${TARGETS} installdirs
        ${INSTALL_PROGRAM} dnssec-checkds@EXEEXT@ ${DESTDIR}${sbindir}
+       ${INSTALL_PROGRAM} dnssec-coverage@EXEEXT@ ${DESTDIR}${sbindir}
        ${INSTALL_DATA} ${srcdir}/dnssec-checkds.8 ${DESTDIR}${mandir}/man8
+       ${INSTALL_DATA} ${srcdir}/dnssec-coverage.8 ${DESTDIR}${mandir}/man8
 
 clean distclean::
        rm -f ${TARGETS}
 
 distclean::
-       rm -f dnssec-checkds.py
+       rm -f dnssec-checkds.py dnssec-coverage.py
diff --git a/bin/python/dnssec-coverage.docbook b/bin/python/dnssec-coverage.docbook
new file mode 100644 (file)
index 0000000..f78c4cb
--- /dev/null
@@ -0,0 +1,228 @@
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+               "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
+               [<!ENTITY mdash "&#8212;">]>
+<!--
+ - Copyright (C) 2012  Internet Systems Consortium, Inc. ("ISC")
+ -
+ - Permission to use, copy, modify, and/or distribute this software for any
+ - purpose with or without fee is hereby granted, provided that the above
+ - copyright notice and this permission notice appear in all copies.
+ -
+ - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+ - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+ - AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+ - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+ - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+ - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+ - PERFORMANCE OF THIS SOFTWARE.
+-->
+
+<refentry id="man.dnssec-coverage">
+  <refentryinfo>
+    <date>April 16, 2012</date>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle><application>dnssec-coverage</application></refentrytitle>
+    <manvolnum>8</manvolnum>
+    <refmiscinfo>BIND9</refmiscinfo>
+  </refmeta>
+
+  <refnamediv>
+    <refname><application>dnssec-coverage</application></refname>
+    <refpurpose>checks future DNSKEY coverage for a zone</refpurpose>
+  </refnamediv>
+
+  <docinfo>
+    <copyright>
+      <year>2012</year>
+      <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
+    </copyright>
+  </docinfo>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>dnssec-coverage</command>
+      <arg><option>-K <replaceable class="parameter">directory</replaceable></option></arg>
+      <arg><option>-f <replaceable class="parameter">file</replaceable></option></arg>
+      <arg><option>-d <replaceable class="parameter">DNSKEY TTL</replaceable></option></arg>
+      <arg><option>-m <replaceable class="parameter">max TTL</replaceable></option></arg>
+      <arg><option>-r <replaceable class="parameter">interval</replaceable></option></arg>
+      <arg><option>-c <replaceable class="parameter">compilezone path</replaceable></option></arg>
+      <arg choice="opt">zone</arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>DESCRIPTION</title>
+    <para><command>dnssec-coverage</command>
+      verifies that the DNSSEC keys for a given zone or a set of zones
+      have timing metadata set properly to ensure no future lapses in DNSSEC
+      coverage.
+    </para>
+    <para>
+      If <option>zone</option> is specified, then keys found in
+      the key repository matching that zone are scanned, and an ordered
+      list is generated of the events scheduled for that key (i.e.,
+      publication, activation, inactivation, deletion).  The list of
+      events is walked in order of occurrence.  Warnings are generated
+      if any event is scheduled which could cause the zone to enter a
+      state in which validation failures might occur: for example, if
+      the number of published or active keys for a given algorithm drops
+      to zero, or if a key is deleted from the zone too soon after a new
+      key is rolled, and cached data signed by the prior key has not had
+      time to expire from resolver caches.
+    </para>
+    <para>
+      If <option>zone</option> is not specified, then all keys in the
+      key repository will be scanned, and all zones for which there are
+      keys will be analyzed.  (Note: This method of reporting is only
+      accurate if all the zones that have keys in a given repository
+      share the same TTL parameters.)
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>OPTIONS</title>
+
+    <variablelist>
+      <varlistentry>
+        <term>-f <replaceable class="parameter">file</replaceable></term>
+        <listitem>
+          <para>
+            If a <option>file</option> is specified, then the zone is
+            read from that file; the largest TTL and the DNSKEY TTL are
+            determined directly from the zone data, and the
+            <option>-m</option> and <option>-d</option> options do
+            not need to be specified on the command line.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-K <replaceable class="parameter">directory</replaceable></term>
+        <listitem>
+          <para>
+            Sets the directory in which keys can be found.  Defaults to the
+            current working directory.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-m <replaceable class="parameter">maximum TTL</replaceable></term>
+        <listitem>
+          <para>
+            Sets the value to be used as the maximum TTL for the zone or
+            zones being analyzed when determining whether there is a
+            possibility of validation failure.  When a zone-signing key is
+            deactivated, there must be enough time for the record in the
+            zone with the longest TTL to have expired from resolver caches
+            before that key can be purged from the DNSKEY RRset.  If that
+            condition does not apply, a warning will be generated.
+          </para>
+          <para>
+            The length of the TTL can be set in seconds, or in larger units
+            of time by adding a suffix: 'mi' for minutes, 'h' for hours,
+            'd' for days, 'w' for weeks, 'mo' for months, 'y' for years.
+          </para>
+          <para>
+            This option is mandatory unless the <option>-f</option> has
+            been used to specify a zone file.  (If <option>-f</option> has
+            been specified, this option may still be used; it will overrde
+            the value found in the file.)
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-d <replaceable class="parameter">DNSKEY TTL</replaceable></term>
+        <listitem>
+          <para>
+            Sets the value to be used as the DNSKEY TTL for the zone or
+            zones being analyzed when determining whether there is a
+            possibility of validation failure.  When a key is rolled (that
+            is, replaced with a new key), there must be enough time
+            for the old DNSKEY RRset to have expired from resolver caches
+            before the new key is activated and begins generating
+            signatures.  If that condition does not apply, a warning
+            will be generated.
+          </para>
+          <para>
+            The length of the TTL can be set in seconds, or in larger units
+            of time by adding a suffix: 'mi' for minutes, 'h' for hours,
+            'd' for days, 'w' for weeks, 'mo' for months, 'y' for years.
+          </para>
+          <para>
+            This option is mandatory unless the <option>-f</option> has
+            been used to specify a zone file, or a default key TTL was
+            set with the <option>-L</option> to
+            <command>dnssec-keygen</command>.  (If either of those is true,
+            this option may still be used; it will overrde the value found
+            in the zone or key file.)
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-r <replaceable class="parameter">resign interval</replaceable></term>
+        <listitem>
+          <para>
+            Sets the value to be used as the resign interval for the zone
+            or zones being analyzed when determining whether there is a
+            possibility of validation failure.  This value defaults to
+            22.5 days, which is also the default in
+            <command>named</command>.  However, if it has been changed
+            by the <option>sig-validity-interval</option> option in
+            <filename>named.conf</filename>, then it should also be
+            changed here.
+          </para>
+          <para>
+            The length of the interval can be set in seconds, or in larger
+            units of time by adding a suffix: 'mi' for minutes, 'h' for hours,
+            'd' for days, 'w' for weeks, 'mo' for months, 'y' for years.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-c <replaceable class="parameter">compilezone path</replaceable></term>
+        <listitem>
+          <para>
+            Specifies a path to a <command>named-compilezone</command> binary.
+            Used for testing.
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>SEE ALSO</title>
+    <para>
+      <citerefentry>
+        <refentrytitle>dnssec-checkds</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+        <refentrytitle>dnssec-dsfromkey</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+        <refentrytitle>dnssec-keygen</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+        <refentrytitle>dnssec-signzone</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>AUTHOR</title>
+    <para><corpauthor>Internet Systems Consortium</corpauthor>
+    </para>
+  </refsect1>
+
+</refentry><!--
+ - Local variables:
+ - mode: sgml
+ - End:
+-->
diff --git a/bin/python/dnssec-coverage.py.in b/bin/python/dnssec-coverage.py.in
new file mode 100755 (executable)
index 0000000..06b91a3
--- /dev/null
@@ -0,0 +1,737 @@
+#!@PYTHON@
+############################################################################
+# Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+############################################################################
+import argparse
+import os
+import glob
+import sys
+import re
+import time
+import calendar
+from collections import defaultdict
+import pprint
+
+prog='dnssec-coverage'
+
+########################################################################
+# Class Event
+########################################################################
+class Event:
+    """ A discrete key metadata event, e.g., Publish, Activate, Inactive,    
+    Delete. Stores the date of the event, and identifying information about 
+    the key to which the event will occur."""
+
+    def __init__(self, _what, _key):
+        now = time.time()
+        self.what = _what
+        self.when = _key.metadata[_what]
+        self.key = _key
+        self.keyid = _key.keyid
+        self.sep = _key.sep
+        self.zone = _key.zone
+        self.alg = _key.alg
+
+    def __repr__(self):
+        return repr((self.when, self.what, self.keyid, self.sep,
+                     self.zone, self.alg))
+
+    def showtime(self):
+        return time.strftime("%a %b %d %H:%M:%S UTC %Y", self.when)
+
+    def showkey(self):
+        return self.key.showkey()
+
+    def showkeytype(self):
+        return self.key.showkeytype()
+
+########################################################################
+# Class Key
+########################################################################
+class Key:
+    """An individual DNSSEC key.  Identified by path, zone, algorithm, keyid.
+    Contains a dictionary of metadata events."""
+
+    def __init__(self, keyname):
+        directory = os.path.dirname(keyname)
+        key = os.path.basename(keyname)
+        (zone, alg, keyid) = key.split('+')
+        keyid = keyid.split('.')[0]
+        key = [zone, alg, keyid]
+        key_file = directory + os.sep + '+'.join(key) + ".key"
+        private_file = directory + os.sep + '+'.join(key) + ".private"
+
+        self.zone = zone[1:-1]
+        self.alg = int(alg)
+        self.keyid = int(keyid)
+
+        kfp = file(key_file, "r")
+        for line in kfp:
+            if line[0] == ';':
+                continue
+            tokens = line.split()
+            if not tokens:
+                continue
+
+            if tokens[1].lower() in ('in', 'ch', 'hs'):
+                septoken = 3
+                self.ttl = args.keyttl
+                if not self.ttl:
+                    vspace()
+                    print("WARNING: Unable to determine TTL for DNSKEY %s." %
+                           self.showkey())
+                    print("\t Using 1 day (86400 seconds); re-run with the -d "
+                          "option for more\n\t accurate results.")
+                    self.ttl = 86400
+            else:
+                septoken = 4
+                self.ttl = int(tokens[1]) if not args.keyttl else args.keyttl
+
+            if (int(tokens[septoken]) & 0x1) == 1:
+                self.sep = True
+            else:
+                self.sep = False
+        kfp.close()
+
+        pfp = file(private_file, "rU")
+        propDict = dict()
+        for propLine in pfp:
+            propDef = propLine.strip()
+            if len(propDef) == 0:
+                continue
+            if propDef[0] in ('!', '#'):
+                continue
+            punctuation = [propDef.find(c) for c in ':= '] + [len(propDef)]
+            found = min([ pos for pos in punctuation if pos != -1 ])
+            name = propDef[:found].rstrip()
+            value =  propDef[found:].lstrip(":= ").rstrip()
+            propDict[name] = value
+
+        if("Publish" in propDict):
+            propDict["Publish"] = time.strptime(propDict["Publish"],
+                                                "%Y%m%d%H%M%S")
+
+        if("Activate" in propDict):
+            propDict["Activate"] = time.strptime(propDict["Activate"],
+                                                 "%Y%m%d%H%M%S")
+
+        if("Inactive" in propDict):
+            propDict["Inactive"] = time.strptime(propDict["Inactive"],
+                                                 "%Y%m%d%H%M%S")
+
+        if("Delete" in propDict):
+            propDict["Delete"] = time.strptime(propDict["Delete"],
+                                               "%Y%m%d%H%M%S")
+
+        if("Revoke" in propDict):
+            propDict["Revoke"] = time.strptime(propDict["Revoke"],
+                                               "%Y%m%d%H%M%S")
+        pfp.close()
+        self.metadata = propDict
+
+    def showkey(self):
+        return "%s/%03d/%05d" % (self.zone, self.alg, self.keyid);
+
+    def showkeytype(self):
+        return ("KSK" if self.sep else "ZSK")
+
+    # ensure that the gap between Publish and Activate is big enough
+    def check_prepub(self):
+        now = time.time()
+
+        if (not "Activate" in self.metadata):
+            debug_print("No Activate information in key: %s" % self.showkey())
+            return False
+        a = calendar.timegm(self.metadata["Activate"])
+
+        if (not "Publish" in self.metadata):
+            debug_print("No Publish information in key: %s" % self.showkey())
+            if a > now:
+                vspace()
+                print("WARNING: Key %s (%s) is scheduled for activation but \n"
+                  "\t not for publication." %
+                  (self.showkey(), self.showkeytype()))
+            return False
+        p = calendar.timegm(self.metadata["Publish"])
+
+        now = time.time()
+        if p < now and a < now:
+            return True
+
+        if p == a:
+            vspace()
+            print ("WARNING: %s (%s) is scheduled to be published and\n"
+                   "\t activated at the same time. This could result in a\n"
+                   "\t coverage gap if the zone was previously signed." %
+                   (self.showkey(), self.showkeytype()))
+            print("\t Activation should be at least %s after publication."
+                    % duration(self.ttl))
+            return True
+
+        if a < p:
+            vspace()
+            print("WARNING: Key %s (%s) is active before it is published" %
+                  (self.showkey(), self.showkeytype()))
+            return False
+
+        if (a - p < self.ttl):
+            vspace()
+            print("WARNING: Key %s (%s) is activated too soon after\n"
+                  "\t publication; this could result in coverage gaps due to\n"
+                  "\t resolver caches containing old data."
+                  % (self.showkey(), self.showkeytype()))
+            print("\t Activation should be at least %s after publication." %
+                  duration(self.ttl))
+            return False
+
+        return True
+
+    # ensure that the gap between Inactive and Delete is big enough
+    def check_postpub(self, timespan = None):
+        if not timespan:
+            timespan = self.ttl
+
+        now = time.time()
+
+        if (not "Delete" in self.metadata):
+            debug_print("No Delete information in key: %s" % self.showkey())
+            return False
+        d = calendar.timegm(self.metadata["Delete"])
+
+        if (not "Inactive" in self.metadata):
+            debug_print("No Inactive information in key: %s" % self.showkey())
+            if d > now:
+                vspace()
+                print("WARNING: Key %s (%s) is scheduled for deletion but\n"
+                      "\t not for inactivation." %
+                      (self.showkey(), self.showkeytype()))
+            return False
+        i = calendar.timegm(self.metadata["Inactive"])
+
+        if d < now and i < now:
+            return True
+
+        if (d < i):
+            vspace()
+            print("WARNING: Key %s (%s) is scheduled for deletion before\n"
+                  "\t inactivation." % (self.showkey(), self.showkeytype()))
+            return False
+
+        if (d - i < timespan):
+            vspace()
+            print("WARNING: Key %s (%s) scheduled for deletion too soon after\n"
+                  "\t deactivation; this may result in coverage gaps due to\n"
+                  "\t resolver caches containing old data."
+                  % (self.showkey(), self.showkeytype()))
+            print("\t Deletion should be at least %s after inactivation." %
+                  duration(timespan))
+            return False
+
+        return True
+
+########################################################################
+# class Zone
+########################################################################
+class Zone:
+    """Stores data about a specific zone"""
+
+    def __init__(self, _name, _keyttl = None, _maxttl = None):
+        self.name = _name
+        self.keyttl = _keyttl
+        self.maxttl = _maxttl
+
+    def load(self, filename):
+        if not args.compilezone:
+            sys.stderr.write(prog + ': FATAL: "named-compilezone" not found\n')
+            exit(1)
+
+        if not self.name:
+            return
+
+        maxttl = keyttl = None
+
+        fp = os.popen("%s -o - %s %s 2> /dev/null" %
+                      (args.compilezone, self.name, filename))
+        for line in fp:
+            fields = line.split()
+            if not maxttl or int(fields[1]) > maxttl:
+                maxttl = int(fields[1])
+            if fields[3] == "DNSKEY":
+                keyttl = int(fields[1])
+        fp.close()
+
+        self.keyttl = keyttl
+        self.maxttl = maxttl
+
+############################################################################
+# debug_print:
+############################################################################
+def debug_print(debugVar):
+    """pretty print a variable iff debug mode is enabled"""
+    if not args.debug_mode:
+        return
+    if type(debugVar) == str:
+        print("DEBUG: " + debugVar)
+    else:
+        print("DEBUG: " + pprint.pformat(debugVar))
+    return
+
+############################################################################
+# vspace:
+############################################################################
+_firstline = True
+def vspace():
+    """adds vertical space between two sections of output text if and only
+    if this is *not* the first section being printed"""
+    global _firstline
+    if _firstline:
+        _firstline = False
+    else:
+        print
+
+############################################################################
+# vreset:
+############################################################################
+def vreset():
+    """reset vertical spacing"""
+    global _firstline
+    _firstline = True
+
+############################################################################
+# getunit
+############################################################################
+def getunit(secs, size):
+    """given a number of seconds, and a number of seconds in a larger unit of
+    time, calculate how many of the larger unit there are and return both
+    that and a remainder value"""
+    bigunit = secs // size 
+    if bigunit:
+        secs %= size
+    return (bigunit, secs)
+
+############################################################################
+# addtime
+############################################################################
+def addtime(output, unit, t):
+    """add a formatted unit of time to an accumulating string"""
+    if t:
+        output += ("%s%d %s%s" %
+                  ((", " if output else ""),
+                   t, unit, ("s" if t > 1 else "")))
+
+    return output
+
+############################################################################
+# duration:
+############################################################################
+def duration(secs):
+    """given a length of time in seconds, print a formatted human duration
+    in larger units of time
+    """
+    # define units:
+    minute = 60
+    hour = minute * 60
+    day = hour * 24
+    month = day * 30
+    year = day * 365
+
+    # calculate time in units:
+    (years, secs) = getunit(secs, year)
+    (months, secs) = getunit(secs, month)
+    (days, secs) = getunit(secs, day)
+    (hours, secs) = getunit(secs, hour)
+    (minutes, secs) = getunit(secs, minute)
+
+    output = ''
+    output = addtime(output, "year", years)
+    output = addtime(output, "month", months)
+    output = addtime(output, "day", days)
+    output = addtime(output, "hour", hours)
+    output = addtime(output, "minute", minutes)
+    output = addtime(output, "second", secs)
+    return output
+
+############################################################################
+# parse_time
+############################################################################
+def parse_time(s):
+    """convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds"""
+    s = s.strip()
+
+    # if s is an integer, we're done already
+    try:
+        n = int(s)
+        return n
+    except:
+        pass
+
+    # try to parse as a number with a suffix indicating unit of time
+    r = re.compile('([0-9][0-9]*)\s*([A-Za-z]*)')
+    m = r.match(s)
+    if not m:
+        raise Exception("Cannot parse %s" % s)
+    (n, unit) = m.groups()
+    n = int(n)
+    unit = unit.lower()
+    if unit[0] == 'y':
+        return n * 31536000
+    elif unit[0] == 'm' and unit[1] == 'o':
+        return n * 2592000
+    elif unit[0] == 'w':
+        return n * 604800
+    elif unit[0] == 'd':
+        return n * 86400
+    elif unit[0] == 'h':
+        return n * 3600
+    elif unit[0] == 'm' and unit[1] == 'i':
+        return n * 60 
+    elif unit[0] == 's':
+        return n
+    else:
+        raise Exception("Invalid suffix %s" % unit)
+
+############################################################################
+# algname:
+############################################################################
+def algname(alg):
+    """return the mnemonic for a DNSSEC algorithm"""
+    names = (None, 'RSAMD5', 'DH', 'DSA', 'ECC', 'RSASHA1',
+            'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None,
+            'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256', 
+            'ECDSAP384SHA384')
+    name = None
+    if alg in range(len(names)):
+        name = names[alg]
+    return (name if name else str(alg))
+
+############################################################################
+# list_events:
+############################################################################
+def list_events(eventgroup):
+    """print a list of the events in an eventgroup"""
+    if not eventgroup:
+        return
+    print ("  " + eventgroup[0].showtime() + ":")
+    for event in eventgroup:
+        print ("    %s: %s (%s)" %
+               (event.what, event.showkey(), event.showkeytype()))
+
+############################################################################
+# process_events:
+############################################################################
+def process_events(eventgroup, active, published):
+    """go through the events in an event group in time-order, add to active
+     list upon Activate event, add to published list upon Publish event,
+     remove from active list upon Inactive event, and remove from published
+     upon Delete event. Emit warnings when inconsistant states are reached"""
+    for event in eventgroup:
+        if event.what == "Activate":
+            active.add(event.keyid)
+        elif event.what == "Publish":
+            published.add(event.keyid)
+        elif event.what == "Inactive":
+            if event.keyid not in active:
+                vspace()
+                print ("\tWARNING: %s (%s) scheduled to become inactive "
+                       "before it is active" %
+                       (event.showkey(), event.showkeytype()))
+            else:
+                active.remove(event.keyid)
+        elif event.what == "Delete":
+            if event.keyid in published:
+                published.remove(event.keyid)
+            else:
+                vspace()
+                print ("WARNING: key %s (%s) is scheduled for deletion before "
+                       "it is published, at %s" %
+                       (event.showkey(), event.showkeytype()))
+        elif event.what == "Revoke":
+            # We don't need to worry about the logic of this one;
+            # just stop counting this key as either active or published
+            if event.keyid in published:
+                published.remove(event.keyid)
+            if event.keyid in active:
+                active.remove(event.keyid)
+
+    return (active, published)
+
+############################################################################
+# check_events:
+############################################################################
+def check_events(eventsList, ksk):
+    """create lists of events happening at the same time, check for 
+    inconsistancies"""
+    active = set()
+    published = set()
+    eventgroups = list()
+    eventgroup = list()
+    keytype = ("KSK" if ksk else "ZSK")
+
+    # collect up all events that have the same time
+    eventsfound = False
+    for event in eventsList:
+        # if checking ZSKs, skip KSKs, and vice versa
+        if (ksk and not event.sep) or (event.sep and not ksk):
+            continue
+
+        # we found an appropriate (ZSK or KSK event)
+        eventsfound = True
+
+        # add event to current eventgroup
+        if (not eventgroup or eventgroup[0].when == event.when):
+            eventgroup.append(event)
+
+        # if we're at the end of the list, we're done.  if
+        # we've found an event with a later time, start a new
+        # eventgroup
+        if (eventgroup[0].when != event.when):
+            eventgroups.append(eventgroup)
+            eventgroup = list()
+            eventgroup.append(event)
+
+    if eventgroup:
+        eventgroups.append(eventgroup)
+
+    for eventgroup in eventgroups:
+        (active, published) = \
+           process_events(eventgroup, active, published)
+
+        list_events(eventgroup)
+
+        # and then check for inconsistencies:
+        if len(active) == 0:
+            print ("ERROR: No %s's are active after this event" % keytype)
+            return False
+        elif len(published) == 0:
+            sys.stdout.write("ERROR: ")
+            print ("ERROR: No %s's are published after this event" % keytype)
+            return False
+        elif len(published.intersection(active)) == 0:
+            sys.stdout.write("ERROR: ")
+            print (("ERROR: No %s's are both active and published " +
+                    "after this event") % keytype)
+            return False
+
+    if not eventsfound:
+        print ("ERROR: No %s events found in '%s'" %
+               (keytype, args.path))
+        return False
+
+    return True
+
+############################################################################
+# check_zones:
+# ############################################################################
+def check_zones(eventsList):
+    """scan events per zone, algorithm, and key type, in order of occurrance,
+    noting inconsistent states when found"""
+    global foundprob
+
+    foundprob = False
+    zonesfound = False
+    for zone in eventsList:
+        if args.zone and zone != args.zone:
+            continue
+
+        zonesfound = True
+        for alg in eventsList[zone]:
+            vspace()
+            print("Checking scheduled KSK events for zone %s, algorithm %s..." %
+                   (zone, algname(alg)))
+            if not check_events(eventsList[zone][alg], True):
+                foundprob = True
+            else:
+                print ("No errors found")
+
+            vspace()
+            print("Checking scheduled ZSK events for zone %s, algorithm %s..." %
+                  (zone, algname(alg)))
+            if not check_events(eventsList[zone][alg], False):
+                foundprob = True
+            else:
+                print ("No errors found")
+
+    if not zonesfound:
+        print("ERROR: No key events found for %s in '%s'" %
+               (args.zone, args.path))
+        exit(1)
+
+############################################################################
+# fill_eventsList:
+############################################################################
+def fill_eventsList(eventsList):
+    """populate the list of events"""
+    for zone, algorithms in keyDict.items():
+        for alg, keys in  algorithms.items():
+            for keyid, keydata in keys.items():
+                if("Publish" in keydata.metadata):
+                    eventsList[zone][alg].append(Event("Publish", keydata))
+                if("Activate" in keydata.metadata):
+                    eventsList[zone][alg].append(Event("Activate", keydata))
+                if("Inactive" in keydata.metadata):
+                    eventsList[zone][alg].append(Event("Inactive", keydata))
+                if("Delete" in keydata.metadata):
+                    eventsList[zone][alg].append(Event("Delete", keydata))
+
+            eventsList[zone][alg] = sorted(eventsList[zone][alg],
+                                           key=lambda event: event.when)
+
+    foundprob = False
+    if not keyDict:
+        print("ERROR: No key events found in '%s'" % args.path)
+        exit(1)
+
+############################################################################
+# set_path:
+############################################################################
+def set_path(command, default=None):
+    """find the location of a specified command.  if a default is supplied
+    and it works, we use it; otherwise we search PATH for a match.  If
+    not found, error and exit"""
+    fpath = default
+    if not fpath or not os.path.isfile(fpath) or not os.access(fpath, os.X_OK):
+        path = os.environ["PATH"]
+        if not path:
+            path = os.path.defpath
+        for directory in path.split(os.pathsep):
+            fpath = directory + os.sep + command
+            if os.path.isfile(fpath) or os.access(fpath, os.X_OK):
+                break
+            fpath = None
+
+    return fpath
+
+############################################################################
+# parse_args:
+############################################################################
+def parse_args():
+    """Read command line arguments, set global 'args' structure"""
+    global args
+
+    compilezone = set_path('named-compilezone',
+                           '@prefix@/sbin/named-compilezone')
+
+    parser = argparse.ArgumentParser(description=prog + ': checks future ' +
+                                     'DNSKEY coverage for a zone')
+
+    parser.add_argument('zone', type=str, help='zone to check')
+    parser.add_argument('-K', dest='path', default='.', type=str,
+                        help='a directory containing keys to process',
+                        metavar='dir')
+    parser.add_argument('-f', dest='filename', type=str,
+                        help='zone master file', metavar='file')
+    parser.add_argument('-m', dest='maxttl', type=str,
+                        help='the longest TTL in the zone(s)',
+                        metavar='int')
+    parser.add_argument('-d', dest='keyttl', type=str,
+                        help='the DNSKEY TTL', metavar='int')
+    parser.add_argument('-r', dest='resign', default='1944000', 
+                        type=int, help='the RRSIG refresh interval '
+                                       'in seconds [default: 22.5 days]',
+                        metavar='int')
+    parser.add_argument('-c', dest='compilezone',
+                        default=compilezone, type=str,
+                        help='path to \'named-compilezone\'',
+                        metavar='path')
+    parser.add_argument('-D', '--debug', dest='debug_mode',
+                        action='store_true', default=False,
+                        help='Turn on debugging output')
+    parser.add_argument('-v', '--version', action='version', version='9.9.1')
+
+    args = parser.parse_args()
+
+    # convert from time arguments to seconds
+    try:
+        if args.maxttl:
+            m = parse_time(args.maxttl)
+            args.maxttl = m
+    except:
+        pass
+
+    try:
+        if args.keyttl:
+            k = parse_time(args.keyttl)
+            args.keyttl = k
+    except:
+        pass
+
+    try:
+        if args.resign:
+            r = parse_time(args.resign)
+            args.resign = r
+    except:
+        pass
+
+    # if we've got the values we need from the command line, stop now
+    if args.maxttl and args.keyttl:
+        return
+
+    # load keyttl and maxttl data from zonefile
+    if args.zone and args.filename:
+        try:
+            zone = Zone(args.zone)
+            zone.load(args.filename)
+            if not args.maxttl:
+                args.maxttl = zone.maxttl
+            if not args.keyttl:
+                args.keyttl = zone.maxttl
+        except Exception as e:
+            print("Unable to load zone data from %s: " % args.filename, e)
+
+    if not args.maxttl:
+        vspace()
+        print ("WARNING: Maximum TTL value was not specified.  Using 1 week\n"
+               "\t (604800 seconds); re-run with the -m option to get more\n"
+               "\t accurate results.")
+        args.maxttl = 604800
+
+############################################################################
+# Main
+############################################################################
+def main():
+    global keyDict
+
+    parse_args()
+    path=args.path
+
+    print ("PHASE 1--Loading keys to check for internal timing problems")
+    keyDict = defaultdict(lambda : defaultdict(dict))
+    files = glob.glob(os.path.join(path, '*.private'))
+    for infile in files:
+        key = Key(infile)
+        if args.zone and key.zone != args.zone:
+            continue
+        keyDict[key.zone][key.alg][key.keyid] = key
+        key.check_prepub()
+        if key.sep:
+            key.check_postpub()
+        else:
+            key.check_postpub(args.maxttl + args.resign)
+
+    vspace()
+    print ("PHASE 2--Scanning future key events for coverage failures")
+    vreset()
+
+    eventsList = defaultdict(lambda : defaultdict(list))
+    fill_eventsList(eventsList)
+    check_zones(eventsList)
+
+    if foundprob:
+        exit(1)
+    else:
+        exit(0)
+
+if __name__ == "__main__":
+    main()
index 33daafb0f221c59e57744b2723b4e2849cc670a5..9f9f25b1790c8ae3b421dad391d87e6eeb624d5c 100644 (file)
@@ -44,6 +44,7 @@ REVOKE=$TOP/bin/dnssec/dnssec-revoke
 SETTIME=$TOP/bin/dnssec/dnssec-settime
 DSFROMKEY=$TOP/bin/dnssec/dnssec-dsfromkey
 CHECKDS=$TOP/bin/python/dnssec-checkds
+COVERAGE=$TOP/bin/python/dnssec-coverage
 CHECKZONE=$TOP/bin/check/named-checkzone
 CHECKCONF=$TOP/bin/check/named-checkconf
 PK11GEN="$TOP/bin/pkcs11/pkcs11-keygen -s ${SLOT:-0} -p 1234"
@@ -57,8 +58,8 @@ ARPANAME=$TOP/bin/tools/arpaname
 # load on the machine to make it unusable to other users.
 # v6synth
 SUBDIRS="acl additional allow_query addzone autosign builtin
-        cacheclean checkconf @CHECKDS@ checknames checkzone database
-        dlv dlvauto dlz dlzexternal dlzredir dname dns64 dnssec
+        cacheclean checkconf @CHECKDS@ checknames checkzone @COVERAGE@
+         database dlv dlvauto dlz dlzexternal dlzredir dname dns64 dnssec
         dsdigest ecdsa formerr forward glue gost ixfr inline limits
         logfileconfig lwresd masterfile masterformat metadata
         notify nsupdate pending pkcs11 redirect resolver rndc rpz
diff --git a/bin/tests/system/coverage/01-ksk-inactive/README b/bin/tests/system/coverage/01-ksk-inactive/README
new file mode 100644 (file)
index 0000000..8102593
--- /dev/null
@@ -0,0 +1,10 @@
+This set includes one KSK rollover.  The KSK is deactivated prior to
+its replacement being activated.  Tool output should resemble:
+
+Checking KSK events for zone example.com, algorithm 7:
+ERROR: After 2012-31-Jul (20:59:14):
+    Inactive: example.com/007/45435 (KSK)
+No KSK's are active
+
+Checking ZSK events for zone example.com, algorithm 7:
+OK
diff --git a/bin/tests/system/coverage/01-ksk-inactive/expect b/bin/tests/system/coverage/01-ksk-inactive/expect
new file mode 100644 (file)
index 0000000..3d342b1
--- /dev/null
@@ -0,0 +1,6 @@
+args="-d 1h -m 2h"
+warn=0
+error=1
+ok=1
+retcode=1
+match="No KSK's are active"
diff --git a/bin/tests/system/coverage/02-zsk-inactive/README b/bin/tests/system/coverage/02-zsk-inactive/README
new file mode 100644 (file)
index 0000000..5d3fed1
--- /dev/null
@@ -0,0 +1,10 @@
+This set includes one ZSK rollover.  The first ZSK is deactivated
+prior to its replacement being activated.  Tool output should resemble:
+
+Checking KSK events for zone example.com, algorithm 7:
+OK
+
+Checking ZSK events for zone example.com, algorithm 7:
+ERROR: After 2012-05-Dec (20:39:32):
+    Inactive: example.com/005/08376 (ZSK)
+No ZSK's are active
diff --git a/bin/tests/system/coverage/02-zsk-inactive/expect b/bin/tests/system/coverage/02-zsk-inactive/expect
new file mode 100644 (file)
index 0000000..a905b58
--- /dev/null
@@ -0,0 +1,6 @@
+args="-d 1h -m 2h"
+warn=0
+error=1
+ok=1
+retcode=1
+match="No ZSK's are active"
diff --git a/bin/tests/system/coverage/03-ksk-unpublished/README b/bin/tests/system/coverage/03-ksk-unpublished/README
new file mode 100644 (file)
index 0000000..7d8a301
--- /dev/null
@@ -0,0 +1,10 @@
+This set contains one KSK rollover.  The KSK is unpublished before its
+successor is published.  Tool output should resemble:
+
+Checking KSK events for zone example.com, algorithm 7:
+ERROR: After 2012-06-Oct (21:07:57):
+    Delete: example.com/007/23040 (KSK)
+No KSK's are published
+
+Checking ZSK events for zone example.com, algorithm 7:
+OK
diff --git a/bin/tests/system/coverage/03-ksk-unpublished/expect b/bin/tests/system/coverage/03-ksk-unpublished/expect
new file mode 100644 (file)
index 0000000..58c65bd
--- /dev/null
@@ -0,0 +1,7 @@
+args="-d 1h -m 2h"
+warn=1
+error=1
+ok=1
+retcode=1
+match="WARNING: Key .* (KSK) is scheduled for deletion before
+No KSK's are published"
diff --git a/bin/tests/system/coverage/04-zsk-unpublished/README b/bin/tests/system/coverage/04-zsk-unpublished/README
new file mode 100644 (file)
index 0000000..5077abf
--- /dev/null
@@ -0,0 +1,10 @@
+This set contains one ZSK rollover.  The ZSK is unpublished before its
+successor is published.  Tool output should resemble:
+
+Checking KSK events for zone example.com, algorithm 7:
+OK
+
+Checking ZSK events for zone example.com, algorithm 7:
+ERROR: After 2012-06-Oct (21:13:45):
+    Delete: example.com/007/25967 (ZSK)
+No ZSK's are published
diff --git a/bin/tests/system/coverage/04-zsk-unpublished/expect b/bin/tests/system/coverage/04-zsk-unpublished/expect
new file mode 100644 (file)
index 0000000..4ffa0b6
--- /dev/null
@@ -0,0 +1,7 @@
+args="-d 1h -m 2h"
+warn=1
+error=1
+ok=1
+retcode=1
+match="WARNING: Key .* (ZSK) is scheduled for deletion before
+No ZSK's are published"
diff --git a/bin/tests/system/coverage/05-ksk-unpub-active/README b/bin/tests/system/coverage/05-ksk-unpub-active/README
new file mode 100644 (file)
index 0000000..119c1b2
--- /dev/null
@@ -0,0 +1,12 @@
+This set includes one KSK rollover.  The first KSK is deleted
+and its successor published prior to the first KSK being deactivated
+and its successor activated.  Tool output should resemble:
+
+Checking KSK events for zone example.com, algorithm 7:
+ERROR: After 2012-05-Dec (21:22:19):
+    Delete: example.com/007/06219 (KSK)
+    Publish: example.com/007/20559 (KSK)
+No KSK's are both active and published
+
+Checking ZSK events for zone example.com, algorithm 7:
+OK
diff --git a/bin/tests/system/coverage/05-ksk-unpub-active/expect b/bin/tests/system/coverage/05-ksk-unpub-active/expect
new file mode 100644 (file)
index 0000000..b6cf2d2
--- /dev/null
@@ -0,0 +1,7 @@
+args="-d 1h -m 2h"
+warn=1
+error=1
+ok=1
+retcode=1
+match="WARNING: Key .* (KSK) is scheduled for deletion before
+No KSK's are both active and published"
diff --git a/bin/tests/system/coverage/06-zsk-unpub-active/README b/bin/tests/system/coverage/06-zsk-unpub-active/README
new file mode 100644 (file)
index 0000000..84833f8
--- /dev/null
@@ -0,0 +1,12 @@
+This set includes one KSK rollover.  The first KSK is deleted
+and its successor published prior to the first KSK being deactivated
+and its successor activated.  Tool output should resemble:
+
+Checking KSK events for zone example.com, algorithm 7:
+OK
+
+Checking ZSK events for zone example.com, algorithm 7:
+ERROR: After 2012-05-Dec (20:44:18):
+    Delete: example.com/007/26369 (ZSK)
+    Publish: example.com/007/21029 (ZSK)
+No ZSK's are both active and published
diff --git a/bin/tests/system/coverage/06-zsk-unpub-active/expect b/bin/tests/system/coverage/06-zsk-unpub-active/expect
new file mode 100644 (file)
index 0000000..ed2004d
--- /dev/null
@@ -0,0 +1,7 @@
+args="-d 1h -m 2h"
+warn=1
+error=1
+ok=1
+retcode=1
+match="WARNING: Key .* (ZSK) is scheduled for deletion before
+No ZSK's are both active and published"
diff --git a/bin/tests/system/coverage/07-ksk-ttl/README b/bin/tests/system/coverage/07-ksk-ttl/README
new file mode 100644 (file)
index 0000000..2659099
--- /dev/null
@@ -0,0 +1,4 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
+
+Expected tool output TBD.
diff --git a/bin/tests/system/coverage/07-ksk-ttl/expect b/bin/tests/system/coverage/07-ksk-ttl/expect
new file mode 100644 (file)
index 0000000..4cc3af2
--- /dev/null
@@ -0,0 +1,7 @@
+args="-d 1w -m 2w"
+warn=1
+error=0
+ok=2
+retcode=0
+match="WARNING: Key .* (KSK) is activated too soon after
+Activation should be at least 7 days after publication."
diff --git a/bin/tests/system/coverage/08-zsk-ttl/README b/bin/tests/system/coverage/08-zsk-ttl/README
new file mode 100644 (file)
index 0000000..2659099
--- /dev/null
@@ -0,0 +1,4 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
+
+Expected tool output TBD.
diff --git a/bin/tests/system/coverage/08-zsk-ttl/expect b/bin/tests/system/coverage/08-zsk-ttl/expect
new file mode 100644 (file)
index 0000000..0579769
--- /dev/null
@@ -0,0 +1,7 @@
+args="-d 1w -m 2w"
+warn=1
+error=0
+ok=2
+retcode=0
+match="WARNING: Key .* (ZSK) is activated too soon after
+Activation should be at least 7 days after publication."
diff --git a/bin/tests/system/coverage/clean.sh b/bin/tests/system/coverage/clean.sh
new file mode 100644 (file)
index 0000000..ea8ab97
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/sh
+rm -f named-compilezone
+rm -f */K*.key
+rm -f */K*.private
+rm -rf coverage.*
+rm -f random.data
diff --git a/bin/tests/system/coverage/setup.sh b/bin/tests/system/coverage/setup.sh
new file mode 100644 (file)
index 0000000..90ab3b6
--- /dev/null
@@ -0,0 +1,106 @@
+#!/bin/sh
+# Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+SYSTEMTESTTOP=..
+. $SYSTEMTESTTOP/conf.sh
+
+KEYGEN="$KEYGEN -qr random.data"
+
+sh clean.sh
+
+ln -s $CHECKZONE named-compilezone
+../../../tools/genrandom 400 random.data
+
+# Test 1: KSK goes inactive before successor is active
+dir=01-ksk-inactive
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+ksk1=`$KEYGEN -K $dir -3fk example.com`
+$SETTIME -K $dir -I +9mo -D +1y $ksk1 > /dev/null 2>&1
+ksk2=`$KEYGEN -K $dir -S $ksk1`
+$SETTIME -K $dir -I +7mo $ksk1 > /dev/null 2>&1
+zsk1=`$KEYGEN -K $dir -3 example.com`
+
+# Test 2: ZSK goes inactive before successor is active
+dir=02-zsk-inactive
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+zsk1=`$KEYGEN -K $dir -3 example.com`
+$SETTIME -K $dir -I +9mo -D +1y $zsk1 > /dev/null 2>&1
+zsk2=`$KEYGEN -K $dir -S $zsk1`
+$SETTIME -K $dir -I +7mo $zsk1 > /dev/null 2>&1
+ksk1=`$KEYGEN -K $dir -3fk example.com`
+
+# Test 3: KSK is unpublished before its successor is published
+dir=03-ksk-unpublished
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+ksk1=`$KEYGEN -K $dir -3fk example.com`
+$SETTIME -K $dir -I +9mo -D +1y $ksk1 > /dev/null 2>&1
+ksk2=`$KEYGEN -K $dir -S $ksk1`
+$SETTIME -K $dir -D +6mo $ksk1 > /dev/null 2>&1
+zsk1=`$KEYGEN -K $dir -3 example.com`
+
+# Test 4: ZSK is unpublished before its successor is published
+dir=04-zsk-unpublished
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+zsk1=`$KEYGEN -K $dir -3 example.com`
+$SETTIME -K $dir -I +9mo -D +1y $zsk1 > /dev/null 2>&1
+zsk2=`$KEYGEN -K $dir -S $zsk1`
+$SETTIME -K $dir -D +6mo $zsk1 > /dev/null 2>&1
+ksk1=`$KEYGEN -K $dir -3fk example.com`
+
+# Test 5: KSK deleted and successor published before KSK is deactivated
+# and successor activated.
+dir=05-ksk-unpub-active
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+ksk1=`$KEYGEN -K $dir -3fk example.com`
+$SETTIME -K $dir -I +9mo -D +8mo $ksk1 > /dev/null 2>&1
+ksk2=`$KEYGEN -K $dir -S $ksk1`
+zsk1=`$KEYGEN -K $dir -3 example.com`
+
+# Test 6: ZSK deleted and successor published before ZSK is deactivated
+# and successor activated.
+dir=06-zsk-unpub-active
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+zsk1=`$KEYGEN -K $dir -3 example.com`
+$SETTIME -K $dir -I +9mo -D +8mo $zsk1 > /dev/null 2>&1
+zsk2=`$KEYGEN -K $dir -S $zsk1`
+ksk1=`$KEYGEN -K $dir -3fk example.com`
+
+# Test 7: KSK rolled with insufficient delay after prepublication.
+dir=07-ksk-ttl
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+ksk1=`$KEYGEN -K $dir -3fk example.com`
+$SETTIME -K $dir -I +9mo -D +1y $ksk1 > /dev/null 2>&1
+ksk2=`$KEYGEN -K $dir -S $ksk1`
+# allow only 1 day between publication and activation
+$SETTIME -K $dir -P +269d $ksk2 > /dev/null 2>&1
+zsk1=`$KEYGEN -K $dir -3 example.com`
+
+# Test 8: ZSK rolled with insufficient delay after prepublication.
+dir=08-zsk-ttl
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+zsk1=`$KEYGEN -K $dir -3 example.com`
+$SETTIME -K $dir -I +9mo -D +1y $zsk1 > /dev/null 2>&1
+zsk2=`$KEYGEN -K $dir -S $zsk1`
+# allow only 1 day between publication and activation
+$SETTIME -K $dir -P +269d $zsk2 > /dev/null 2>&1
+ksk1=`$KEYGEN -K $dir -3fk example.com`
diff --git a/bin/tests/system/coverage/tests.sh b/bin/tests/system/coverage/tests.sh
new file mode 100644 (file)
index 0000000..d89ff7e
--- /dev/null
@@ -0,0 +1,67 @@
+#!/bin/sh
+# Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
+#
+# Permission to use, copy, modify, and/or distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+SYSTEMTESTTOP=..
+. $SYSTEMTESTTOP/conf.sh
+
+COVERAGE="$COVERAGE -c ./named-compilezone"
+
+status=0
+n=1
+
+matchall () {
+    file=$1
+    echo "$2" | while read matchline; do
+        grep "$matchline" $file > /dev/null 2>&1 || {
+            echo "FAIL"
+            return
+        }
+    done
+}
+
+echo "I:checking for DNSSEC key coverage issues"
+ret=0
+for dir in [0-9][0-9]-*; do
+        ret=0
+        echo "I:$dir"
+        args= warn= error= ok= retcode= match=
+        . $dir/expect
+        $COVERAGE $args -K $dir example.com > coverage.$n 2>&1
+
+        # check that return code matches expectations
+        [ $? -eq $retcode ] || ret=1
+
+        # check for correct number of errors
+        found=`grep ERROR coverage.$n | wc -l`
+        [ $found -eq $error ] || ret=1
+
+        # check for correct number of warnings
+        found=`grep WARNING coverage.$n | wc -l`
+        [ $found -eq $warn ] || ret=1
+
+        # check for correct number of OKs
+        found=`grep "No errors found" coverage.$n | wc -l`
+        [ $found -eq $ok ] || ret=1
+
+        found=`matchall coverage.$n "$match"`
+        [ "$found" = "FAIL" ] && ret=1
+
+        n=`expr $n + 1`
+        if [ $ret != 0 ]; then echo "I:failed"; fi
+        status=`expr $status + $ret`
+done
+
+echo "I:exit status: $status"
+exit $status
index dc0ab1645d351a5294d4b7664b70a1072b8a379a..558810a5e13a622fdb14fe2da11e133283574a35 100644 (file)
@@ -1236,6 +1236,18 @@ israw1 signer/signer.out.7 || ret=1
 if [ $ret != 0 ]; then echo "I:failed"; fi
 status=`expr $status + $ret`
 
+echo "I:checking dnssec-signzone output format ($n)"
+ret=0
+(
+cd signer
+$SIGNER -O full -f - -Sxt -o example example.db > signer.out.3 2>&1
+$SIGNER -O text -f - -Sxt -o example example.db > signer.out.4 2>&1
+) || ret=1
+awk '/IN *SOA/ {if (NF != 11) exit(1)}' signer/signer.out.3 || ret=1
+awk '/IN *SOA/ {if (NF != 7) exit(1)}' signer/signer.out.4 || ret=1
+if [ $ret != 0 ]; then echo "I:failed"; fi
+status=`expr $status + $ret`
+
 echo "I:checking validated data are not cached longer than originalttl ($n)"
 ret=0
 $DIG $DIGOPTS +ttl +noauth a.ttlpatch.example. @10.53.0.3 a > dig.out.ns3.test$n || ret=1
@@ -2011,5 +2023,14 @@ n=`expr $n + 1`
 if [ $ret != 0 ]; then echo "I:failed"; fi
 status=`expr $status + $ret`
 
+echo "I:check dnssec-dsfromkey from stdin($n)"
+ret=0
+$DIG $DIGOPTS dnskey algroll. @10.53.0.2 | \
+        $DSFROMKEY -f - algroll. > dig.out.ns2.test$n || ret=1
+diff -b dig.out.ns2.test$n ns1/dsset-algroll. > /dev/null 2>&1 || ret=1
+n=`expr $n + 1`
+if [ $ret != 0 ]; then echo "I:failed"; fi
+status=`expr $status + $ret`
+
 echo "I:exit status: $status"
 exit $status
index c2a11dd9222344afda1cf219aad952eb2d59ca49..fd1ff12b28ef04c6c713f8ec59a2bc1f9aa96028 100755 (executable)
--- a/configure
+++ b/configure
@@ -1339,6 +1339,7 @@ ISC_PLATFORM_NORETURN_PRE
 ISC_PLATFORM_HAVELONGLONG
 ISC_SOCKADDR_LEN_T
 PYTHON_TOOLS
+COVERAGE
 CHECKDS
 PYTHON
 PERL
@@ -12206,6 +12207,7 @@ $as_echo "found, using $PYTHON" >&6; }
                        unspec)
                                PYTHON=""
 
+
                                { $as_echo "$as_me:${as_lineno-$LINENO}: result: not found, python disabled" >&5
 $as_echo "not found, python disabled" >&6; }
                                ;;
@@ -12221,13 +12223,16 @@ esac
 
 PYTHON_TOOLS=''
 CHECKDS=''
+COVERAGE=''
 if test "X$PYTHON" != "X"; then
         PYTHON_TOOLS=python
        CHECKDS=checkds
+       COVERAGE=coverage
 fi
 
 
 
+
 #
 # Special processing of paths depending on whether --prefix,
 # --sysconfdir or --localstatedir arguments were given.  What's
@@ -20838,7 +20843,7 @@ ac_config_commands="$ac_config_commands chmod"
 # elsewhere if there's a good reason for doing so.
 #
 
-ac_config_files="$ac_config_files make/Makefile make/mkdep Makefile bin/Makefile bin/check/Makefile bin/confgen/Makefile bin/confgen/unix/Makefile bin/dig/Makefile bin/dnssec/Makefile bin/named/Makefile bin/named/unix/Makefile bin/nsupdate/Makefile bin/pkcs11/Makefile bin/python/Makefile bin/python/dnssec-checkds.py bin/rndc/Makefile bin/tests/Makefile bin/tests/atomic/Makefile bin/tests/db/Makefile bin/tests/dst/Makefile bin/tests/dst/Kdh.+002+18602.key bin/tests/dst/Kdh.+002+18602.private bin/tests/dst/Kdh.+002+48957.key bin/tests/dst/Kdh.+002+48957.private bin/tests/dst/Ktest.+001+00002.key bin/tests/dst/Ktest.+001+54622.key bin/tests/dst/Ktest.+001+54622.private bin/tests/dst/Ktest.+003+23616.key bin/tests/dst/Ktest.+003+23616.private bin/tests/dst/Ktest.+003+49667.key bin/tests/dst/dst_2_data bin/tests/dst/t2_data_1 bin/tests/dst/t2_data_2 bin/tests/dst/t2_dsasig bin/tests/dst/t2_rsasig bin/tests/hashes/Makefile bin/tests/headerdep_test.sh bin/tests/master/Makefile bin/tests/mem/Makefile bin/tests/names/Makefile bin/tests/net/Makefile bin/tests/rbt/Makefile bin/tests/resolver/Makefile bin/tests/sockaddr/Makefile bin/tests/system/Makefile bin/tests/system/conf.sh bin/tests/system/dlz/prereq.sh bin/tests/system/dlzexternal/Makefile bin/tests/system/dlzexternal/ns1/named.conf bin/tests/system/dsdigest/prereq.sh bin/tests/system/ecdsa/prereq.sh bin/tests/system/dlzredir/prereq.sh bin/tests/system/filter-aaaa/Makefile bin/tests/system/geoip/Makefile bin/tests/system/gost/prereq.sh bin/tests/system/lwresd/Makefile bin/tests/system/rpz/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/tkey/Makefile bin/tests/system/tsiggss/Makefile bin/tests/tasks/Makefile bin/tests/timers/Makefile bin/tests/virtual-time/Makefile bin/tests/virtual-time/conf.sh bin/tools/Makefile contrib/check-secure-delegation.pl contrib/zone-edit.sh doc/Makefile doc/arm/Makefile doc/doxygen/Doxyfile doc/doxygen/Makefile doc/doxygen/doxygen-input-filter doc/misc/Makefile doc/xsl/Makefile doc/xsl/isc-docbook-chunk.xsl doc/xsl/isc-docbook-html.xsl doc/xsl/isc-docbook-latex.xsl doc/xsl/isc-manpage.xsl isc-config.sh lib/Makefile lib/bind9/Makefile lib/bind9/include/Makefile lib/bind9/include/bind9/Makefile lib/dns/Makefile lib/dns/include/Makefile lib/dns/include/dns/Makefile lib/dns/include/dst/Makefile lib/dns/tests/Makefile lib/export/Makefile lib/export/dns/Makefile lib/export/dns/include/Makefile lib/export/dns/include/dns/Makefile lib/export/dns/include/dst/Makefile lib/export/irs/Makefile lib/export/irs/include/Makefile lib/export/irs/include/irs/Makefile lib/export/isc/$thread_dir/Makefile lib/export/isc/$thread_dir/include/Makefile lib/export/isc/$thread_dir/include/isc/Makefile lib/export/isc/Makefile lib/export/isc/include/Makefile lib/export/isc/include/isc/Makefile lib/export/isc/nls/Makefile lib/export/isc/unix/Makefile lib/export/isc/unix/include/Makefile lib/export/isc/unix/include/isc/Makefile lib/export/isccfg/Makefile lib/export/isccfg/include/Makefile lib/export/isccfg/include/isccfg/Makefile lib/export/samples/Makefile lib/export/samples/Makefile-postinstall lib/irs/Makefile lib/irs/include/Makefile lib/irs/include/irs/Makefile lib/irs/include/irs/netdb.h lib/irs/include/irs/platform.h lib/isc/$arch/Makefile lib/isc/$arch/include/Makefile lib/isc/$arch/include/isc/Makefile lib/isc/$thread_dir/Makefile lib/isc/$thread_dir/include/Makefile lib/isc/$thread_dir/include/isc/Makefile lib/isc/Makefile lib/isc/include/Makefile lib/isc/include/isc/Makefile lib/isc/include/isc/platform.h lib/isc/tests/Makefile lib/isc/nls/Makefile lib/isc/unix/Makefile lib/isc/unix/include/Makefile lib/isc/unix/include/isc/Makefile lib/isccc/Makefile lib/isccc/include/Makefile lib/isccc/include/isccc/Makefile lib/isccfg/Makefile lib/isccfg/include/Makefile lib/isccfg/include/isccfg/Makefile lib/lwres/Makefile lib/lwres/include/Makefile lib/lwres/include/lwres/Makefile lib/lwres/include/lwres/netdb.h lib/lwres/include/lwres/platform.h lib/lwres/man/Makefile lib/lwres/unix/Makefile lib/lwres/unix/include/Makefile lib/lwres/unix/include/lwres/Makefile lib/tests/Makefile lib/tests/include/Makefile lib/tests/include/tests/Makefile unit/Makefile unit/unittest.sh"
+ac_config_files="$ac_config_files make/Makefile make/mkdep Makefile bin/Makefile bin/check/Makefile bin/confgen/Makefile bin/confgen/unix/Makefile bin/dig/Makefile bin/dnssec/Makefile bin/named/Makefile bin/named/unix/Makefile bin/nsupdate/Makefile bin/pkcs11/Makefile bin/python/Makefile bin/python/dnssec-checkds.py bin/python/dnssec-coverage.py bin/rndc/Makefile bin/tests/Makefile bin/tests/atomic/Makefile bin/tests/db/Makefile bin/tests/dst/Makefile bin/tests/dst/Kdh.+002+18602.key bin/tests/dst/Kdh.+002+18602.private bin/tests/dst/Kdh.+002+48957.key bin/tests/dst/Kdh.+002+48957.private bin/tests/dst/Ktest.+001+00002.key bin/tests/dst/Ktest.+001+54622.key bin/tests/dst/Ktest.+001+54622.private bin/tests/dst/Ktest.+003+23616.key bin/tests/dst/Ktest.+003+23616.private bin/tests/dst/Ktest.+003+49667.key bin/tests/dst/dst_2_data bin/tests/dst/t2_data_1 bin/tests/dst/t2_data_2 bin/tests/dst/t2_dsasig bin/tests/dst/t2_rsasig bin/tests/hashes/Makefile bin/tests/headerdep_test.sh bin/tests/master/Makefile bin/tests/mem/Makefile bin/tests/names/Makefile bin/tests/net/Makefile bin/tests/rbt/Makefile bin/tests/resolver/Makefile bin/tests/sockaddr/Makefile bin/tests/system/Makefile bin/tests/system/conf.sh bin/tests/system/dlz/prereq.sh bin/tests/system/dlzexternal/Makefile bin/tests/system/dlzexternal/ns1/named.conf bin/tests/system/dsdigest/prereq.sh bin/tests/system/ecdsa/prereq.sh bin/tests/system/dlzredir/prereq.sh bin/tests/system/filter-aaaa/Makefile bin/tests/system/geoip/Makefile bin/tests/system/gost/prereq.sh bin/tests/system/lwresd/Makefile bin/tests/system/rpz/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/tkey/Makefile bin/tests/system/tsiggss/Makefile bin/tests/tasks/Makefile bin/tests/timers/Makefile bin/tests/virtual-time/Makefile bin/tests/virtual-time/conf.sh bin/tools/Makefile contrib/check-secure-delegation.pl contrib/zone-edit.sh doc/Makefile doc/arm/Makefile doc/doxygen/Doxyfile doc/doxygen/Makefile doc/doxygen/doxygen-input-filter doc/misc/Makefile doc/xsl/Makefile doc/xsl/isc-docbook-chunk.xsl doc/xsl/isc-docbook-html.xsl doc/xsl/isc-docbook-latex.xsl doc/xsl/isc-manpage.xsl isc-config.sh lib/Makefile lib/bind9/Makefile lib/bind9/include/Makefile lib/bind9/include/bind9/Makefile lib/dns/Makefile lib/dns/include/Makefile lib/dns/include/dns/Makefile lib/dns/include/dst/Makefile lib/dns/tests/Makefile lib/export/Makefile lib/export/dns/Makefile lib/export/dns/include/Makefile lib/export/dns/include/dns/Makefile lib/export/dns/include/dst/Makefile lib/export/irs/Makefile lib/export/irs/include/Makefile lib/export/irs/include/irs/Makefile lib/export/isc/$thread_dir/Makefile lib/export/isc/$thread_dir/include/Makefile lib/export/isc/$thread_dir/include/isc/Makefile lib/export/isc/Makefile lib/export/isc/include/Makefile lib/export/isc/include/isc/Makefile lib/export/isc/nls/Makefile lib/export/isc/unix/Makefile lib/export/isc/unix/include/Makefile lib/export/isc/unix/include/isc/Makefile lib/export/isccfg/Makefile lib/export/isccfg/include/Makefile lib/export/isccfg/include/isccfg/Makefile lib/export/samples/Makefile lib/export/samples/Makefile-postinstall lib/irs/Makefile lib/irs/include/Makefile lib/irs/include/irs/Makefile lib/irs/include/irs/netdb.h lib/irs/include/irs/platform.h lib/isc/$arch/Makefile lib/isc/$arch/include/Makefile lib/isc/$arch/include/isc/Makefile lib/isc/$thread_dir/Makefile lib/isc/$thread_dir/include/Makefile lib/isc/$thread_dir/include/isc/Makefile lib/isc/Makefile lib/isc/include/Makefile lib/isc/include/isc/Makefile lib/isc/include/isc/platform.h lib/isc/tests/Makefile lib/isc/nls/Makefile lib/isc/unix/Makefile lib/isc/unix/include/Makefile lib/isc/unix/include/isc/Makefile lib/isccc/Makefile lib/isccc/include/Makefile lib/isccc/include/isccc/Makefile lib/isccfg/Makefile lib/isccfg/include/Makefile lib/isccfg/include/isccfg/Makefile lib/lwres/Makefile lib/lwres/include/Makefile lib/lwres/include/lwres/Makefile lib/lwres/include/lwres/netdb.h lib/lwres/include/lwres/platform.h lib/lwres/man/Makefile lib/lwres/unix/Makefile lib/lwres/unix/include/Makefile lib/lwres/unix/include/lwres/Makefile lib/tests/Makefile lib/tests/include/Makefile lib/tests/include/tests/Makefile unit/Makefile unit/unittest.sh"
 
 
 #
@@ -21846,6 +21851,7 @@ do
     "bin/pkcs11/Makefile") CONFIG_FILES="$CONFIG_FILES bin/pkcs11/Makefile" ;;
     "bin/python/Makefile") CONFIG_FILES="$CONFIG_FILES bin/python/Makefile" ;;
     "bin/python/dnssec-checkds.py") CONFIG_FILES="$CONFIG_FILES bin/python/dnssec-checkds.py" ;;
+    "bin/python/dnssec-coverage.py") CONFIG_FILES="$CONFIG_FILES bin/python/dnssec-coverage.py" ;;
     "bin/rndc/Makefile") CONFIG_FILES="$CONFIG_FILES bin/rndc/Makefile" ;;
     "bin/tests/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/Makefile" ;;
     "bin/tests/atomic/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/atomic/Makefile" ;;
index f29f8cf06a800bef6c730c7dfe0f91be78265950..f61395e8beb03950bb4b6d927d9f1b10557d486f 100644 (file)
@@ -179,6 +179,7 @@ except: exit(1)'
                        unspec)
                                PYTHON=""
                                AC_SUBST(CHECKDS)
+                               AC_SUBST(COVERAGE)
                                AC_MSG_RESULT([not found, python disabled])
                                ;;
                        yes)
@@ -192,11 +193,14 @@ esac
 
 PYTHON_TOOLS=''
 CHECKDS=''
+COVERAGE=''
 if test "X$PYTHON" != "X"; then
         PYTHON_TOOLS=python
        CHECKDS=checkds
+       COVERAGE=coverage
 fi
 AC_SUBST(CHECKDS)
+AC_SUBST(COVERAGE)
 AC_SUBST(PYTHON_TOOLS)
 
 #
@@ -3778,6 +3782,7 @@ AC_CONFIG_FILES([
        bin/pkcs11/Makefile
        bin/python/Makefile
        bin/python/dnssec-checkds.py
+       bin/python/dnssec-coverage.py
        bin/rndc/Makefile
        bin/tests/Makefile
        bin/tests/atomic/Makefile
index d1396487e1bbb608f346acbdcd37337e6e312cf1..3cd31c056ac8790a40fe8b634f1285a11568cef0 100644 (file)
@@ -17504,6 +17504,8 @@ zone "example.com" {
       <title>Manual pages</title>
       <xi:include href="../../bin/dig/dig.docbook"/>
       <xi:include href="../../bin/dig/host.docbook"/>
+      <xi:include href="../../bin/python/dnssec-checkds.docbook"/>
+      <xi:include href="../../bin/python/dnssec-coverage.docbook"/>
       <xi:include href="../../bin/dnssec/dnssec-dsfromkey.docbook"/>
       <xi:include href="../../bin/dnssec/dnssec-keyfromlabel.docbook"/>
       <xi:include href="../../bin/dnssec/dnssec-keygen.docbook"/>