]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
[master] dnssec-keymgr
authorEvan Hunt <each@isc.org>
Thu, 28 Apr 2016 07:12:33 +0000 (00:12 -0700)
committerEvan Hunt <each@isc.org>
Thu, 28 Apr 2016 07:16:01 +0000 (00:16 -0700)
4349.   [contrib]       kasp2policy: A python script to create a DNSSEC
                        policy file from an OpenDNSSEC KASP XML file.

4348. [func] dnssec-keymgr: A new python-based DNSSEC key
management utility, which reads a policy definition
file and can create or update DNSSEC keys as needed
to ensure that a zone's keys match policy, roll over
correctly on schedule, etc.  Thanks to Sebastian
Castro for assistance in development. [RT #39211]

88 files changed:
CHANGES
bin/dnssec/dnssec-settime.c
bin/python/.gitignore
bin/python/Makefile.in
bin/python/dnssec-checkds.py.in
bin/python/dnssec-coverage.docbook
bin/python/dnssec-coverage.py.in [changed mode: 0755->0644]
bin/python/dnssec-keymgr.docbook [new file with mode: 0644]
bin/python/dnssec-keymgr.py.in [new file with mode: 0644]
bin/python/isc/.gitignore [new file with mode: 0644]
bin/python/isc/Makefile.in [new file with mode: 0644]
bin/python/isc/__init__.py [new file with mode: 0644]
bin/python/isc/checkds.py [new file with mode: 0644]
bin/python/isc/coverage.py [new file with mode: 0644]
bin/python/isc/dnskey.py [new file with mode: 0644]
bin/python/isc/eventlist.py [new file with mode: 0644]
bin/python/isc/keydict.py [new file with mode: 0644]
bin/python/isc/keyevent.py [new file with mode: 0644]
bin/python/isc/keymgr.py [new file with mode: 0644]
bin/python/isc/keyseries.py [new file with mode: 0644]
bin/python/isc/keyzone.py [new file with mode: 0644]
bin/python/isc/policy.py [new file with mode: 0644]
bin/python/isc/tests/Makefile.in [new file with mode: 0644]
bin/python/isc/tests/dnskey_test.py [new file with mode: 0644]
bin/python/isc/tests/policy_test.py [new file with mode: 0644]
bin/python/isc/tests/test-policies/01-keysize.pol [new file with mode: 0644]
bin/python/isc/tests/test-policies/02-prepublish.pol [new file with mode: 0644]
bin/python/isc/tests/test-policies/03-postpublish.pol [new file with mode: 0644]
bin/python/isc/tests/test-policies/04-combined-pre-post.pol [new file with mode: 0644]
bin/python/isc/tests/testdata/Kexample.com.+007+35529.key [new file with mode: 0644]
bin/python/isc/tests/testdata/Kexample.com.+007+35529.private [new file with mode: 0644]
bin/python/isc/utils.py.in [new file with mode: 0644]
bin/tests/system/conf.sh.in
bin/tests/system/coverage/03-ksk-unpublished/expect
bin/tests/system/coverage/04-zsk-unpublished/expect
bin/tests/system/coverage/05-ksk-unpub-active/expect
bin/tests/system/coverage/06-zsk-unpub-active/expect
bin/tests/system/coverage/07-ksk-ttl/expect
bin/tests/system/coverage/08-zsk-ttl/expect
bin/tests/system/keymgr/01-ksk-inactive/README [new file with mode: 0644]
bin/tests/system/keymgr/01-ksk-inactive/expect [new file with mode: 0644]
bin/tests/system/keymgr/02-zsk-inactive/README [new file with mode: 0644]
bin/tests/system/keymgr/02-zsk-inactive/expect [new file with mode: 0644]
bin/tests/system/keymgr/03-ksk-unpublished/README [new file with mode: 0644]
bin/tests/system/keymgr/03-ksk-unpublished/expect [new file with mode: 0644]
bin/tests/system/keymgr/04-zsk-unpublished/README [new file with mode: 0644]
bin/tests/system/keymgr/04-zsk-unpublished/expect [new file with mode: 0644]
bin/tests/system/keymgr/05-ksk-unpub-active/README [new file with mode: 0644]
bin/tests/system/keymgr/05-ksk-unpub-active/expect [new file with mode: 0644]
bin/tests/system/keymgr/06-zsk-unpub-active/README [new file with mode: 0644]
bin/tests/system/keymgr/06-zsk-unpub-active/expect [new file with mode: 0644]
bin/tests/system/keymgr/07-ksk-ttl/README [new file with mode: 0644]
bin/tests/system/keymgr/07-ksk-ttl/expect [new file with mode: 0644]
bin/tests/system/keymgr/08-zsk-ttl/README [new file with mode: 0644]
bin/tests/system/keymgr/08-zsk-ttl/expect [new file with mode: 0644]
bin/tests/system/keymgr/09-no-keys/README [new file with mode: 0644]
bin/tests/system/keymgr/09-no-keys/expect [new file with mode: 0644]
bin/tests/system/keymgr/10-change-roll/README [new file with mode: 0644]
bin/tests/system/keymgr/10-change-roll/expect [new file with mode: 0644]
bin/tests/system/keymgr/11-many-simul/README [new file with mode: 0644]
bin/tests/system/keymgr/11-many-simul/expect [new file with mode: 0644]
bin/tests/system/keymgr/12-many-active/README [new file with mode: 0644]
bin/tests/system/keymgr/12-many-active/expect [new file with mode: 0644]
bin/tests/system/keymgr/13-noroll/README [new file with mode: 0644]
bin/tests/system/keymgr/13-noroll/expect [new file with mode: 0644]
bin/tests/system/keymgr/14-wrongalg/README [new file with mode: 0644]
bin/tests/system/keymgr/14-wrongalg/expect [new file with mode: 0644]
bin/tests/system/keymgr/15-unspec/README [new file with mode: 0644]
bin/tests/system/keymgr/15-unspec/expect [new file with mode: 0644]
bin/tests/system/keymgr/16-wrongalg-unspec/README [new file with mode: 0644]
bin/tests/system/keymgr/16-wrongalg-unspec/expect [new file with mode: 0644]
bin/tests/system/keymgr/17-noforce/README [new file with mode: 0644]
bin/tests/system/keymgr/17-noforce/expect [new file with mode: 0644]
bin/tests/system/keymgr/clean.sh [new file with mode: 0644]
bin/tests/system/keymgr/policy.conf [new file with mode: 0644]
bin/tests/system/keymgr/policy.good [new file with mode: 0644]
bin/tests/system/keymgr/policy.sample [new file with mode: 0644]
bin/tests/system/keymgr/prereq.sh [new file with mode: 0644]
bin/tests/system/keymgr/setup.sh [new file with mode: 0644]
bin/tests/system/keymgr/testpolicy.py [new file with mode: 0644]
bin/tests/system/keymgr/tests.sh [new file with mode: 0644]
configure
configure.in
contrib/kasp/README [new file with mode: 0644]
contrib/kasp/kasp.xml [new file with mode: 0644]
contrib/kasp/kasp2policy.py [new file with mode: 0644]
contrib/kasp/policy.good [new file with mode: 0644]
doc/arm/notes.xml

diff --git a/CHANGES b/CHANGES
index 9c75d27d38636c376c6be4c542f41ae05a6050d5..31883339ca4d4b44d7d521953b9a8da3d86998eb 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,13 @@
+4349.   [contrib]       kasp2policy: A python script to create a DNSSEC
+                        policy file from an OpenDNSSEC KASP XML file.
+
+4348.  [func]          dnssec-keymgr: A new python-based DNSSEC key
+                       management utility, which reads a policy definition
+                       file and can create or update DNSSEC keys as needed
+                       to ensure that a zone's keys match policy, roll over
+                       correctly on schedule, etc.  Thanks to Sebastian
+                        Castro for assistance in development. [RT #39211]
+
 4347.  [port]          Corrected a build error on x86_64 Solaris. [RT #42150]
 
 4346.  [bug]           Fixed a regression introduced in change #4337 which
index 90d3b67cb4c8932ea533782c938dcb5e66ea845b..8a31cf97455d8b223537a37a5347386c93bdfd2f 100644 (file)
@@ -519,11 +519,12 @@ main(int argc, char **argv) {
        if ((setdel && setinact && del < inact) ||
            (dst_key_gettime(key, DST_TIME_INACTIVE,
                             &previnact) == ISC_R_SUCCESS &&
-            setdel && !setinact && del < previnact) ||
+            setdel && !setinact && !unsetinact && del < previnact) ||
            (dst_key_gettime(key, DST_TIME_DELETE,
                             &prevdel) == ISC_R_SUCCESS &&
-            setinact && !setdel && prevdel < inact) ||
-           (!setdel && !setinact && prevdel < previnact))
+            setinact && !setdel && !unsetdel && prevdel < inact) ||
+           (!setdel && !unsetdel && !setinact && !unsetinact &&
+            prevdel < previnact))
                fprintf(stderr, "%s: warning: Key is scheduled to "
                                "be deleted before it is\n\t"
                                "scheduled to be inactive.\n",
index f770f2396e7bed693175c6c42d58918b3b711fd2..2e6963dfc9d09bc46c961a95bfd41ed4c02094dc 100644 (file)
@@ -2,3 +2,6 @@ dnssec-checkds
 dnssec-checkds.py
 dnssec-coverage
 dnssec-coverage.py
+dnssec-keymgr
+dnssec-keymgr.py
+*.pyc
index dfb5d597114f0eaa6a422fae693d17d7cb33e94d..c731a5d953a2dbfba1427efae2f705419d7df99e 100644 (file)
@@ -12,8 +12,6 @@
 # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 # PERFORMANCE OF THIS SOFTWARE.
 
-# $Id$
-
 srcdir =       @srcdir@
 VPATH =                @srcdir@
 top_srcdir =   @top_srcdir@
@@ -22,11 +20,13 @@ top_srcdir =        @top_srcdir@
 
 PYTHON =       @PYTHON@
 
-TARGETS =      dnssec-checkds dnssec-coverage
-PYSRCS =       dnssec-checkds.py dnssec-coverage.py
+SUBDIRS        =       isc
+
+TARGETS =      dnssec-checkds dnssec-coverage dnssec-keymgr
+PYSRCS =       dnssec-checkds.py dnssec-coverage.py dnssec-keymgr.py
 
-MANPAGES =     dnssec-checkds.8 dnssec-coverage.8
-HTMLPAGES =    dnssec-checkds.html dnssec-coverage.html
+MANPAGES =     dnssec-checkds.8 dnssec-coverage.8 dnssec-keymgr.8
+HTMLPAGES =    dnssec-checkds.html dnssec-coverage.html dnssec-keymgr.html
 MANOBJS =      ${MANPAGES} ${HTMLPAGES}
 
 @BIND9_MAKE_RULES@
@@ -39,6 +39,10 @@ dnssec-coverage: dnssec-coverage.py
        cp -f dnssec-coverage.py dnssec-coverage
        chmod +x dnssec-coverage
 
+dnssec-keymgr: dnssec-keymgr.py
+       cp -f dnssec-keymgr.py dnssec-keymgr
+       chmod +x dnssec-keymgr
+
 doc man:: ${MANOBJS}
 
 docclean manclean maintainer-clean::
@@ -49,13 +53,15 @@ installdirs:
        $(SHELL) ${top_srcdir}/mkinstalldirs ${DESTDIR}${mandir}/man8
 
 install:: ${TARGETS} installdirs
-       ${INSTALL_SCRIPT} dnssec-checkds@EXEEXT@ ${DESTDIR}${sbindir}
-       ${INSTALL_SCRIPT} dnssec-coverage@EXEEXT@ ${DESTDIR}${sbindir}
+       ${INSTALL_SCRIPT} dnssec-checkds ${DESTDIR}${sbindir}
+       ${INSTALL_SCRIPT} dnssec-coverage ${DESTDIR}${sbindir}
+       ${INSTALL_SCRIPT} dnssec-keymgr ${DESTDIR}${sbindir}
        ${INSTALL_DATA} ${srcdir}/dnssec-checkds.8 ${DESTDIR}${mandir}/man8
        ${INSTALL_DATA} ${srcdir}/dnssec-coverage.8 ${DESTDIR}${mandir}/man8
+       ${INSTALL_DATA} ${srcdir}/dnssec-keymgr.8 ${DESTDIR}${mandir}/man8
 
 clean distclean::
        rm -f ${TARGETS}
 
 distclean::
-       rm -f dnssec-checkds.py dnssec-coverage.py
+       rm -f dnssec-checkds.py dnssec-coverage.py dnssec-keymgr.py
index 40b730ba07fc36d827c534afd002f443eb2d204c..79db6f1aa161020e26b3743e495269ce880ffd47 100644 (file)
 # PERFORMANCE OF THIS SOFTWARE.
 ############################################################################
 
-import argparse
-import pprint
 import os
+import sys
 
-prog='dnssec-checkds'
+sys.path.insert(0, os.path.dirname(sys.argv[0]))
+sys.path.insert(1, os.path.join('@prefix@', 'lib'))
 
-# These routines permit platform-independent location of BIND 9 tools
-if os.name == 'nt':
-    import win32con
-    import win32api
-
-def prefix(bindir = ''):
-    if os.name != 'nt':
-        return os.path.join('@prefix@', bindir)
-
-    bind_subkey = "Software\\ISC\\BIND"
-    hKey = None
-    keyFound = True
-    try:
-        hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey)
-    except:
-        keyFound = False
-    if keyFound:
-        try:
-            (namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir")
-        except:
-            keyFound = False
-        win32api.RegCloseKey(hKey)
-    if keyFound:
-        return os.path.join(namedBase, bindir)
-    return os.path.join(win32api.GetSystemDirectory(), bindir)
-
-def shellquote(s):
-    if os.name == 'nt':
-        return '"' + s.replace('"', '"\\"') + '"'
-    return "'" + s.replace("'", "'\\''") + "'"
-
-############################################################################
-# DSRR class:
-# Delegation Signer (DS) resource record
-############################################################################
-class DSRR:
-    hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384' }
-    rrname=''
-    rrclass='IN'
-    rrtype='DS'
-    keyid=None
-    keyalg=None
-    hashalg=None
-    digest=''
-    ttl=0
-
-    def __init__(self, rrtext):
-        if not rrtext:
-            return
-
-        fields = rrtext.split()
-        if len(fields) < 7:
-            return
-
-        self.rrname = fields[0].lower()
-        fields = fields[1:]
-        if fields[0].upper() in ['IN','CH','HS']:
-            self.rrclass = fields[0].upper()
-            fields = fields[1:]
-        else:
-            self.ttl = int(fields[0])
-            self.rrclass = fields[1].upper()
-            fields = fields[2:]
-
-        if fields[0].upper() != 'DS':
-            raise Exception
-
-        self.rrtype = 'DS'
-        self.keyid = int(fields[1])
-        self.keyalg = int(fields[2])
-        self.hashalg = int(fields[3])
-        self.digest = ''.join(fields[4:]).upper()
-
-    def __repr__(self):
-        return('%s %s %s %d %d %d %s' %
-                (self.rrname, self.rrclass, self.rrtype, self.keyid,
-                self.keyalg, self.hashalg, self.digest))
-
-    def __eq__(self, other):
-        return self.__repr__() == other.__repr__()
-
-############################################################################
-# DLVRR class:
-# DNSSEC Lookaside Validation (DLV) resource record
-############################################################################
-class DLVRR:
-    hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384' }
-    parent=''
-    dlvname=''
-    rrname='IN'
-    rrclass='IN'
-    rrtype='DLV'
-    keyid=None
-    keyalg=None
-    hashalg=None
-    digest=''
-    ttl=0
-
-    def __init__(self, rrtext, dlvname):
-        if not rrtext:
-            return
-
-        fields = rrtext.split()
-        if len(fields) < 7:
-            return
-
-        self.dlvname = dlvname.lower()
-        parent = fields[0].lower().strip('.').split('.')
-        parent.reverse()
-        dlv = dlvname.split('.')
-        dlv.reverse()
-        while len(dlv) != 0 and len(parent) != 0 and parent[0] == dlv[0]:
-            parent = parent[1:]
-            dlv = dlv[1:]
-        if len(dlv) != 0:
-            raise Exception
-        parent.reverse()
-        self.parent = '.'.join(parent)
-        self.rrname = self.parent + '.' + self.dlvname + '.'
-        
-        fields = fields[1:]
-        if fields[0].upper() in ['IN','CH','HS']:
-            self.rrclass = fields[0].upper()
-            fields = fields[1:]
-        else:
-            self.ttl = int(fields[0])
-            self.rrclass = fields[1].upper()
-            fields = fields[2:]
-
-        if fields[0].upper() != 'DLV':
-            raise Exception
-
-        self.rrtype = 'DLV'
-        self.keyid = int(fields[1])
-        self.keyalg = int(fields[2])
-        self.hashalg = int(fields[3])
-        self.digest = ''.join(fields[4:]).upper()
-
-    def __repr__(self):
-        return('%s %s %s %d %d %d %s' %
-                (self.rrname, self.rrclass, self.rrtype,
-                self.keyid, self.keyalg, self.hashalg, self.digest))
-
-    def __eq__(self, other):
-        return self.__repr__() == other.__repr__()
-
-############################################################################
-# checkds:
-# Fetch DS RRset for the given zone from the DNS; fetch DNSKEY
-# RRset from the masterfile if specified, or from DNS if not.
-# Generate a set of expected DS records from the DNSKEY RRset,
-# and report on congruency.
-############################################################################
-def checkds(zone, masterfile = None):
-    dslist=[]
-    fp=os.popen("%s +noall +answer -t ds -q %s" %
-                (shellquote(args.dig), shellquote(zone)))
-    for line in fp:
-        dslist.append(DSRR(line))
-    dslist = sorted(dslist, key=lambda ds: (ds.keyid, ds.keyalg, ds.hashalg))
-    fp.close()
-
-    dsklist=[]
-
-    if masterfile:
-        fp = os.popen("%s -f %s %s " %
-                      (shellquote(args.dsfromkey), shellquote(masterfile),
-                       shellquote(zone)))
-    else:
-        fp = os.popen("%s +noall +answer -t dnskey -q %s | %s -f - %s" %
-                      (shellquote(args.dig), shellquote(zone),
-                       shellquote(args.dsfromkey), shellquote(zone)))
-
-    for line in fp:
-        dsklist.append(DSRR(line))
-
-    fp.close()
-
-    if (len(dsklist) < 1):
-        print ("No DNSKEY records found in zone apex")
-        return False
-
-    found = False
-    for ds in dsklist:
-        if ds in dslist:
-            print ("DS for KSK %s/%03d/%05d (%s) found in parent" %
-                   (ds.rrname.strip('.'), ds.keyalg,
-                    ds.keyid, DSRR.hashalgs[ds.hashalg]))
-            found = True
-        else:
-            print ("DS for KSK %s/%03d/%05d (%s) missing from parent" %
-                   (ds.rrname.strip('.'), ds.keyalg,
-                    ds.keyid, DSRR.hashalgs[ds.hashalg]))
-
-    if not found:
-        print ("No DS records were found for any DNSKEY")
-
-    return found
-
-############################################################################
-# checkdlv:
-# Fetch DLV RRset for the given zone from the DNS; fetch DNSKEY
-# RRset from the masterfile if specified, or from DNS if not.
-# Generate a set of expected DLV records from the DNSKEY RRset,
-# and report on congruency.
-############################################################################
-def checkdlv(zone, lookaside, masterfile = None):
-    dlvlist=[]
-    fp=os.popen("%s +noall +answer -t dlv -q %s" %
-                (shellquote(args.dig), shellquote(zone + '.' + lookaside)))
-    for line in fp:
-        dlvlist.append(DLVRR(line, lookaside))
-    dlvlist = sorted(dlvlist,
-                     key=lambda dlv: (dlv.keyid, dlv.keyalg, dlv.hashalg))
-    fp.close()
-
-    #
-    # Fetch DNSKEY records from DNS and generate DLV records from them
-    #
-    dlvklist=[]
-    if masterfile:
-        fp = os.popen("%s -f %s -l %s %s " %
-                      (args.dsfromkey, masterfile, lookaside, zone))
-    else:
-        fp = os.popen("%s +noall +answer -t dnskey %s | %s -f - -l %s %s"
-                      % (shellquote(args.dig), shellquote(zone),
-                         shellquote(args.dsfromkey), shellquote(lookaside),
-                         shellquote(zone)))
-
-    for line in fp:
-        dlvklist.append(DLVRR(line, lookaside))
-
-    fp.close()
-
-    if (len(dlvklist) < 1):
-        print ("No DNSKEY records found in zone apex")
-        return False
-
-    found = False
-    for dlv in dlvklist:
-        if dlv in dlvlist:
-            print ("DLV for KSK %s/%03d/%05d (%s) found in %s" %
-                   (dlv.parent, dlv.keyalg, dlv.keyid,
-                    DLVRR.hashalgs[dlv.hashalg], dlv.dlvname))
-            found = True
-        else:
-            print ("DLV for KSK %s/%03d/%05d (%s) missing from %s" %
-                   (dlv.parent, dlv.keyalg, dlv.keyid, 
-                    DLVRR.hashalgs[dlv.hashalg], dlv.dlvname))
-
-    if not found:
-        print ("No DLV records were found for any DNSKEY")
-
-    return found
-
-
-############################################################################
-# parse_args:
-# Read command line arguments, set global 'args' structure
-############################################################################
-def parse_args():
-    global args
-    parser = argparse.ArgumentParser(description=prog + ': checks DS coverage')
-
-    bindir = 'bin'
-    if os.name == 'nt':
-        sbindir = 'bin'
-    else:
-        sbindir = 'sbin'
-
-    parser.add_argument('zone', type=str, help='zone to check')
-    parser.add_argument('-f', '--file', dest='masterfile', type=str,
-                        help='zone master file')
-    parser.add_argument('-l', '--lookaside', dest='lookaside', type=str,
-                        help='DLV lookaside zone')
-    parser.add_argument('-d', '--dig', dest='dig',
-                        default=os.path.join(prefix(bindir), 'dig'),
-                        type=str, help='path to \'dig\'')
-    parser.add_argument('-D', '--dsfromkey', dest='dsfromkey',
-                        default=os.path.join(prefix(sbindir),
-                                             'dnssec-dsfromkey'),
-                        type=str, help='path to \'dig\'')
-    parser.add_argument('-v', '--version', action='version',
-                        version='@BIND9_VERSION@')
-    args = parser.parse_args()
-
-    args.zone = args.zone.strip('.')
-    if args.lookaside:
-        lookaside = args.lookaside.strip('.')
-
-############################################################################
-# Main
-############################################################################
-def main():
-    parse_args()
-
-    if args.lookaside:
-        found = checkdlv(args.zone, args.lookaside, args.masterfile)
-    else:
-        found = checkds(args.zone, args.masterfile)
-
-    exit(0 if found else 1)
+import isc.checkds
 
 if __name__ == "__main__":
-    main()
+    isc.checkds.main()
index 45d5fa86d1e5987b28e1d1ef94459f4db52061d6..892801ecd08a4a7678c384185147667da384341d 100644 (file)
@@ -56,7 +56,7 @@
       <arg choice="opt" rep="norepeat"><option>-c <replaceable class="parameter">compilezone path</replaceable></option></arg>
       <arg choice="opt" rep="norepeat"><option>-k</option></arg>
       <arg choice="opt" rep="norepeat"><option>-z</option></arg>
-      <arg choice="opt" rep="norepeat">zone</arg>
+      <arg choice="opt" rep="repeat">zone</arg>
     </cmdsynopsis>
   </refsynopsisdiv>
 
             '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
+            This option is not necessary if 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 override
-            the value found in the file.)
+            the value found in the file.
+          </para>
+          <para>
+            If this option is not used and the maximum TTL cannot be retrieved
+            from a zone file, a warning is generated and a default value of
+            1 week is used.
           </para>
         </listitem>
       </varlistentry>
             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.
+            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
             '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 override the value found
-            in the zone or key file.)
+            This option is not necessary if <option>-f</option> has
+            been used to specify a zone file from which the TTL
+            of the DNSKEY RRset can be read, or if a default key TTL was
+            set using ith the <option>-L</option> to
+            <command>dnssec-keygen</command>.  If either of those is true,
+            this option may still be used; it will override the values
+            found in the zone file or the key file.
+          </para>
+          <para>
+            If this option is not used and the key TTL cannot be retrieved
+            from the zone file or the key file, then a warning is generated
+            and a default value of 1 day is used.
           </para>
         </listitem>
       </varlistentry>
old mode 100755 (executable)
new mode 100644 (file)
index ccd01a9..bbd8629
@@ -1,6 +1,6 @@
 #!@PYTHON@
 ############################################################################
-# Copyright (C) 2013-2015  Internet Systems Consortium, Inc. ("ISC")
+# Copyright (C) 2012-2015  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
 # 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'
+sys.path.insert(0, os.path.dirname(sys.argv[0]))
+sys.path.insert(1, os.path.join('@prefix@', 'lib'))
 
-# These routines permit platform-independent location of BIND 9 tools
-if os.name == 'nt':
-    import win32con
-    import win32api
-
-def prefix(bindir = ''):
-    if os.name != 'nt':
-        return os.path.join('@prefix@', bindir)
-
-    bind_subkey = "Software\\ISC\\BIND"
-    hKey = None
-    keyFound = True
-    try:
-        hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey)
-    except:
-        keyFound = False
-    if keyFound:
-        try:
-            (namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir")
-        except:
-            keyFound = False
-        win32api.RegCloseKey(hKey)
-    if keyFound:
-        return os.path.join(namedBase, bindir)
-    return os.path.join(win32api.GetSystemDirectory(), bindir)
-
-########################################################################
-# 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 = open(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 = open(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:
-        if (args.checklimit and
-            calendar.timegm(eventgroup[0].when) > args.checklimit):
-            print("Ignoring events after %s" %
-                  time.strftime("%a %b %d %H:%M:%S UTC %Y", 
-                      time.gmtime(args.checklimit)))
-            return True
-
-        (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]:
-            if not args.no_ksk:
-                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")
-
-            if not args.no_zsk:
-                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',
-                           os.path.join(prefix('bin'), '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='time')
-    parser.add_argument('-d', dest='keyttl', type=str,
-                        help='the DNSKEY TTL', metavar='time')
-    parser.add_argument('-r', dest='resign', default='1944000', 
-                        type=str, help='the RRSIG refresh interval '
-                                       'in seconds [default: 22.5 days]',
-                        metavar='time')
-    parser.add_argument('-c', dest='compilezone',
-                        default=compilezone, type=str,
-                        help='path to \'named-compilezone\'',
-                        metavar='path')
-    parser.add_argument('-l', dest='checklimit',
-                        type=str, default='0',
-                        help='Length of time to check for '
-                             'DNSSEC coverage [default: 0 (unlimited)]',
-                        metavar='time')
-    parser.add_argument('-z', dest='no_ksk',
-                        action='store_true', default=False,
-                        help='Only check zone-signing keys (ZSKs)')
-    parser.add_argument('-k', dest='no_zsk',
-                        action='store_true', default=False,
-                        help='Only check key-signing keys (KSKs)')
-    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='@BIND9_VERSION@')
-
-    args = parser.parse_args()
-
-    if args.no_zsk and args.no_ksk:
-        print("ERROR: -z and -k cannot be used together.");
-        exit(1)
-
-    # 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
-
-    try:
-        if args.checklimit:
-            lim = args.checklimit
-            r = parse_time(args.checklimit)
-            if r == 0:
-                args.checklimit = None
-            else:
-                args.checklimit = time.time() + 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)
+import isc.coverage
 
 if __name__ == "__main__":
-    main()
+    isc.coverage.main()
diff --git a/bin/python/dnssec-keymgr.docbook b/bin/python/dnssec-keymgr.docbook
new file mode 100644 (file)
index 0000000..d0ca1c5
--- /dev/null
@@ -0,0 +1,354 @@
+<!--
+ - Copyright (C) 2015  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.
+-->
+
+<!-- Converted by db4-upgrade version 1.0 -->
+<refentry xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="man.dnssec-keymgr">
+  <info>
+    <date>2016-04-03</date>
+  </info>
+  <refentryinfo>
+    <corpname>ISC</corpname>
+    <corpauthor>Internet Systems Consortium, Inc.</corpauthor>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle><application>dnssec-keymgr</application></refentrytitle>
+    <manvolnum>8</manvolnum>
+    <refmiscinfo>BIND9</refmiscinfo>
+  </refmeta>
+
+  <refnamediv>
+    <refname><application>dnssec-keymgr</application></refname>
+    <refpurpose>Ensures correct DNSKEY coverage for a zone based on a defined policy</refpurpose>
+  </refnamediv>
+
+  <docinfo>
+    <copyright>
+      <year>2016</year>
+      <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
+    </copyright>
+  </docinfo>
+
+  <refsynopsisdiv>
+    <cmdsynopsis sepchar=" ">
+      <command>dnssec-keymgr</command>
+      <arg choice="opt" rep="norepeat"><option>-K <replaceable class="parameter">directory</replaceable></option></arg>
+      <arg choice="opt" rep="norepeat"><option>-c <replaceable class="parameter">file</replaceable></option></arg>
+      <arg choice="opt" rep="norepeat"><option>-d <replaceable class="parameter">time</replaceable></option></arg>
+      <arg choice="opt" rep="norepeat"><option>-k</option></arg>
+      <arg choice="opt" rep="norepeat"><option>-z</option></arg>
+      <arg choice="opt" rep="norepeat"><option>-g <replaceable class="parameter">path</replaceable></option></arg>
+      <arg choice="opt" rep="norepeat"><option>-s <replaceable class="parameter">path</replaceable></option></arg>
+      <arg choice="opt" rep="repeat">zone</arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsection><info><title>DESCRIPTION</title></info>
+    <para>
+      <command>dnssec-keymgr</command>
+      is a high level Python wrapper to facilitate the key rollover 
+      process for zones handled by BIND. It uses the BIND commands
+      for manipulating DNSSEC key metadata:
+      <command>dnssec-keygen</command> and 
+      <command>dnssec-settime</command>.
+    </para>
+    <para>
+      DNSSEC policy can be read from a configuration file (default
+      <filename>/etc/dnssec.policy</filename>), from which the key
+      parameters, publication and rollover schedule, and desired
+      coverage duration for any given zone can be determined.  This
+      file may be used to define individual DNSSEC policies on a
+      per-zone basis, or to set a default policy used for all zones.
+    </para>
+    <para>
+      When <command>dnssec-keymgr</command> runs, it examines the DNSSEC
+      keys for one or more zones, comparing their timing metadata against
+      the policies for those zones.  If key settings do not conform to the
+      DNSSEC policy (for example, because the policy has been changed),
+      they are automatically corrected.
+    </para>
+    </para>
+      A zone policy can specify a duration for which we want to
+      ensure the key correctness (<option>coverage</option>).  It can
+      also specify a rollover period (<option>roll-period</option>).
+      If policy indicates that a key should roll over before the
+      coverage period ends, then a successor key will automatically be
+      created and added to the end of the key series.
+    <para>
+    <para>
+      If zones are specified on the command line,
+      <command>dnssec-keymgr</command> will examine only those zones.
+      If a specified zone does not already have keys in place, then
+      keys will be generated for it according to policy.
+    </para>
+    <para>
+      If zones are <emphasis>not</emphasis> specified on the command
+      line, then <command>dnssec-keymgr</command> will search the
+      key directory (either the current working directory or the directory
+      set by the <option>-K</option> option), and check the keys for
+      all the zones represented in the directory.
+    </para>
+    <para>
+      It is expected that this tool will be run automatically and
+      unattended (for example, by <command>cron</command>).  
+    </para>
+  </refsection>
+
+  <refsection><info><title>OPTIONS</title></info>
+    <variablelist>
+      <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>-c <replaceable class="parameter">file</replaceable></term>
+        <listitem>
+          <para>
+            If <option>-c</option> is specified, then the DNSSEC
+            policy is read from <option>file</option>.  (If not
+            specified, then the policy is read from
+            <filename>/etc/policy.conf</filename>; if that file
+            doesn't exist, a built-in global default policy is used.)
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-f</term>
+        <listitem>
+          <para>
+            Force: allow updating of key events even if they are
+            already in the past. This is not recommended for use with
+            zones in which keys have already been published. However,
+            if a set of keys has been generated all of which have
+            publication and activation dates in the past, but the
+            keys have not been published in a zone as yet, then this
+            option can be used to clean them up and turn them into a
+            proper series of keys with appropriate rollover intervals.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-q</term>
+        <listitem>
+          <para>
+            Quiet: suppress printing of <command>dnssec-keygen</command>
+            and <command>dnssec-settime</command>.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-k</term>
+        <listitem>
+          <para>
+            Only apply policies to KSK keys.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-z</term>
+        <listitem>
+          <para>
+            Only apply policies to ZSK keys.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-g <replaceable class="parameter">keygen path</replaceable></term>
+        <listitem>
+          <para>
+            Specifies a path to a <command>dnssec-keygen</command> binary.
+            Used for testing.
+          </para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term>-s <replaceable class="parameter">settime path</replaceable></term>
+        <listitem>
+          <para>
+            Specifies a path to a <command>dnssec-settime</command> binary.
+            Used for testing.
+          </para>
+        </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsection>
+
+  <refsection><info><title>POLICY CONFIGURATION</title></info>
+    <para>
+      The <filename>policy.conf</filename> file can specify three kinds
+      of policies:
+    </para>
+    <itemizedlist>
+      <listitem>
+       <emphasis>Policy classes</emphasis>
+       (<option>policy <replaceable>name</replaceable> { ... };</option>)
+       can be inherited by zone policies or other policy classes; these
+       can be used to create sets of different security profiles. For
+       example, a policy class <userinput>normal</userinput> might specify
+       1024-bit key sizes, but a class <userinput>extra</userinput> might
+       specify 2048 bits instead; <userinput>extra</userinput> would be
+       used for zones that had unusually high security needs.
+      </listitem>
+      <listitem>
+       Algorithm policies:
+       (<option>algorithm-policy <replaceable>algorithm</replaceable> { ... };</option> )
+       override default per-algorithm settings.  For example, by default,
+       RSASHA256 keys use 2048-bit key sizes for both KSK and ZSK. This
+       can be modified using <command>algorithm-policy</command>, and the
+       new key sizes would then be used for any key of type RSASHA256.
+      </listitem>
+      <listitem>
+       Zone policies:
+       (<option>zone <replaceable>name</replaceable> { ... };</option> )
+       set policy for a single zone by name. A zone policy can inherit
+       a policy class by including a <option>policy</option> option.
+      </listitem>
+    </itemizedlist>
+    <para>
+      Options that can be specified in policies:
+    </para>
+    <variablelist>
+      <varlistentry>
+       <term><command>directory</command></term>
+       <listitem>
+         Specifies the directory in which keys should be stored.
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><command>algorithm</command></term>
+       <listitem>
+         The key algorithm. If no policy is defined, the default is
+      RSASHA256.
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><command>keyttl</command></term>
+       <listitem>
+         The key TTL. If no policy is defined, the default is one hour.
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><command>coverage</command></term>
+       <listitem>
+         The length of time to ensure that keys will be correct; no action
+      will be taken to create new keys to be activated after this time.
+      This can be represented as a number of seconds, or as a duration using
+         human-readable units (examples: "1y" or "6 months").
+         A default value for this option can be set in algorithm policies
+         as well as in policy classes or zone policies.
+      If no policy is configured, the default is six months.
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><command>key-size</command></term>
+       <listitem>
+         Specifies the number of bits to use in creating keys.
+         Takes two arguments: keytype (eihter "zsk" or "ksk") and size. 
+         A default value for this option can be set in algorithm policies
+         as well as in policy classes or zone policies. If no policy is
+      configured, the default is 1024 bits for DSA keys and 2048 for
+      RSA.
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><command>roll-period</command></term>
+       <listitem>
+         How frequently keys should be rolled over.
+         Takes two arguments: keytype (eihter "zsk" or "ksk") and a duration. 
+         A default value for this option can be set in algorithm policies
+         as well as in policy classes or zone policies.  If no policy is
+      configured, the default is one year for ZSK's. KSK's do not
+      roll over by default.
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><command>pre-publish</command></term>
+       <listitem>
+         How long before activation a key should be published.  Note: If
+      <option>roll-period</option> is not set, this value is ignored.
+         Takes two arguments: keytype (either "zsk" or "ksk") and a duration. 
+         A default value for this option can be set in algorithm policies
+         as well as in policy classes or zone policies.  The default is
+      one month.
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><command>post-publish</command></term>
+       <listitem>
+         How long after inactivation a key should be deleted from the zone.
+         Note: If <option>roll-period</option> is not set, this value is ignored.
+         Takes two arguments: keytype (eihter "zsk" or "ksk") and a duration. 
+         A default value for this option can be set in algorithm policies
+         as well as in policy classes or zone policies. The default is one
+      month.
+       </listitem>
+      </varlistentry>
+      <varlistentry>
+       <term><command>standby</command></term>
+       <listitem>
+         Not yet implemented.
+       </listitem>
+      </varlistentry>
+    </variablelist>
+  </refsection>
+
+  <refsection><info><title>REMAINING WORK</title></info>
+  <itemizedlist>
+    <listitem>
+      Enable scheduling of KSK rollovers using the <option>-P sync</option>
+      and <option>-D sync</option> options to
+      <command>dnssec-keygen</command> and
+      <command>dnssec-settime</command>.  Check the parent zone
+      (as in <command>dnssec-checkds</command>) to determine when it's
+      safe for the key to roll.
+    </listitem>
+    <listitem>
+      Allow configuration of standby keys and use of the REVOKE bit,
+      for keys that use RFC 5011 semantics.
+    </listitem>
+  </itemizedlist>
+  </refsection>
+
+  <refsection><info><title>SEE ALSO</title></info>
+    <para>
+      <citerefentry>
+       <refentrytitle>dnssec-coverage</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+       <refentrytitle>dnssec-keygen</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+       <refentrytitle>dnssec-settime</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>,
+      <citerefentry>
+       <refentrytitle>dnssec-checkds</refentrytitle><manvolnum>8</manvolnum>
+      </citerefentry>
+    </para>
+  </refsection>
+
+</refentry>
diff --git a/bin/python/dnssec-keymgr.py.in b/bin/python/dnssec-keymgr.py.in
new file mode 100644 (file)
index 0000000..23d563d
--- /dev/null
@@ -0,0 +1,27 @@
+#!@PYTHON@
+############################################################################
+# Copyright (C) 2012-2015  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 os
+import sys
+
+sys.path.insert(0, os.path.dirname(sys.argv[0]))
+sys.path.insert(1, os.path.join('@prefix@', 'lib'))
+
+import isc.keymgr
+
+if __name__ == "__main__":
+    isc.keymgr.main()
diff --git a/bin/python/isc/.gitignore b/bin/python/isc/.gitignore
new file mode 100644 (file)
index 0000000..84554b8
--- /dev/null
@@ -0,0 +1,3 @@
+utils.py
+parsetab.py
+parser.out
diff --git a/bin/python/isc/Makefile.in b/bin/python/isc/Makefile.in
new file mode 100644 (file)
index 0000000..425d054
--- /dev/null
@@ -0,0 +1,67 @@
+# Copyright (C) 2012-2015  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.
+
+srcdir =       @srcdir@
+VPATH =                @srcdir@
+top_srcdir =   @top_srcdir@
+
+@BIND9_MAKE_INCLUDES@
+
+SUBDIRS        =       tests
+
+PYTHON =       @PYTHON@
+
+PYSRCS =       __init__.py dnskey.py eventlist.py keydict.py \
+               keyevent.py keyzone.py policy.py
+TARGETS =      parsetab.py parsetab.pyc \
+               __init__.pyc dnskey.pyc eventlist.py keydict.py \
+               keyevent.pyc keyzone.pyc policy.pyc
+
+@BIND9_MAKE_RULES@
+
+%.pyc: %.py
+       $(PYTHON) -m compileall .
+
+parsetab.py parsetab.pyc: policy.py
+       $(PYTHON) policy.py parse /dev/null > /dev/null
+       $(PYTHON) -m parsetab
+
+installdirs:
+       $(SHELL) ${top_srcdir}/mkinstalldirs ${DESTDIR}${libdir}/isc
+
+install:: ${PYSRCS} installdirs
+       ${INSTALL_SCRIPT} __init__.py ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} __init__.pyc ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} dnskey.py ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} dnskey.pyc ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} eventlist.py ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} eventlist.pyc ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} keydict.py ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} keydict.pyc ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} keyevent.py ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} keyevent.pyc ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} keyzone.py ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} keyzone.pyc ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} policy.py ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} policy.pyc ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} parsetab.py ${DESTDIR}${libdir}
+       ${INSTALL_SCRIPT} parsetab.pyc ${DESTDIR}${libdir}
+
+check test: subdirs
+
+clean distclean::
+       rm -f *.pyc parser.out parsetab.py
+
+distclean::
+       rm -Rf utils.py
\ No newline at end of file
diff --git a/bin/python/isc/__init__.py b/bin/python/isc/__init__.py
new file mode 100644 (file)
index 0000000..0d79f35
--- /dev/null
@@ -0,0 +1,25 @@
+# Copyright (C) 2015  Internet Systems Consortium.
+#
+# Permission to use, copy, modify, and 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 INTERNET SYSTEMS CONSORTIUM
+# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+# INTERNET SYSTEMS CONSORTIUM 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.
+
+__all__ = ['dnskey', 'eventlist', 'keydict', 'keyevent', 'keyseries',
+           'keyzone', 'policy', 'parsetab', 'utils']
+from isc.dnskey import *
+from isc.eventlist import *
+from isc.keydict import *
+from isc.keyevent import *
+from isc.keyseries import *
+from isc.keyzone import *
+from isc.policy import *
+from isc.utils import *
diff --git a/bin/python/isc/checkds.py b/bin/python/isc/checkds.py
new file mode 100644 (file)
index 0000000..64ca12e
--- /dev/null
@@ -0,0 +1,189 @@
+############################################################################
+# Copyright (C) 2012-2015  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 sys
+from subprocess import Popen, PIPE
+
+from isc.utils import prefix,version
+
+prog = 'dnssec-checkds'
+
+
+############################################################################
+# SECRR class:
+# Class for DS/DLV resource record
+############################################################################
+class SECRR:
+    hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384'}
+    rrname = ''
+    rrclass = 'IN'
+    keyid = None
+    keyalg = None
+    hashalg = None
+    digest = ''
+    ttl = 0
+
+    def __init__(self, rrtext, dlvname = None):
+        if not rrtext:
+            raise Exception
+
+        fields = rrtext.split()
+        if len(fields) < 7:
+            raise Exception
+
+        if dlvname:
+            self.rrtype = "DLV"
+            self.dlvname = dlvname.lower()
+            parent = fields[0].lower().strip('.').split('.')
+            parent.reverse()
+            dlv = dlvname.split('.')
+            dlv.reverse()
+            while len(dlv) != 0 and len(parent) != 0 and parent[0] == dlv[0]:
+                parent = parent[1:]
+                dlv = dlv[1:]
+            if dlv:
+                raise Exception
+            parent.reverse()
+            self.parent = '.'.join(parent)
+            self.rrname = self.parent + '.' + self.dlvname + '.'
+        else:
+            self.rrtype = "DS"
+            self.rrname = fields[0].lower()
+
+        fields = fields[1:]
+        if fields[0].upper() in ['IN', 'CH', 'HS']:
+            self.rrclass = fields[0].upper()
+            fields = fields[1:]
+        else:
+            self.ttl = int(fields[0])
+            self.rrclass = fields[1].upper()
+            fields = fields[2:]
+
+        if fields[0].upper() != self.rrtype:
+            raise Exception
+
+        self.keyid, self.keyalg, self.hashalg = map(int, fields[1:4])
+        self.digest = ''.join(fields[4:]).upper()
+
+    def __repr__(self):
+        return '%s %s %s %d %d %d %s' % \
+               (self.rrname, self.rrclass, self.rrtype,
+                self.keyid, self.keyalg, self.hashalg, self.digest)
+
+    def __eq__(self, other):
+        return self.__repr__() == other.__repr__()
+
+
+############################################################################
+# check:
+# Fetch DS/DLV RRset for the given zone from the DNS; fetch DNSKEY
+# RRset from the masterfile if specified, or from DNS if not.
+# Generate a set of expected DS/DLV records from the DNSKEY RRset,
+# and report on congruency.
+############################################################################
+def check(zone, args, masterfile=None, lookaside=None):
+    rrlist = []
+    cmd = [args.dig, "+noall", "+answer", "-t", "dlv" if lookaside else "ds",
+           "-q", zone + "." + lookaside if lookaside else zone]
+    fp, _ = Popen(cmd, stdout=PIPE).communicate()
+
+    for line in fp.splitlines():
+        rrlist.append(SECRR(line, lookaside))
+    rrlist = sorted(rrlist, key=lambda rr: (rr.keyid, rr.keyalg, rr.hashalg))
+
+    klist = []
+
+    if masterfile:
+        cmd = [args.dsfromkey, "-f", masterfile]
+        if lookaside:
+            cmd += ["-l", lookaside]
+        cmd.append(zone)
+        fp, _ = Popen(cmd, stdout=PIPE).communicate()
+    else:
+        intods, _ = Popen([args.dig, "+noall", "+answer", "-t", "dnskey",
+                           "-q", zone], stdout=PIPE).communicate()
+        cmd = [args.dsfromkey, "-f", "-"]
+        if lookaside:
+            cmd += ["-l", lookaside]
+        cmd.append(zone)
+        fp, _ = Popen(cmd, stdin=PIPE, stdout=PIPE).communicate(intods)
+
+    for line in fp.splitlines():
+        klist.append(SECRR(line, lookaside))
+
+    if len(klist) < 1:
+        print ("No DNSKEY records found in zone apex")
+        return False
+
+    found = False
+    for rr in klist:
+        if rr in rrlist:
+            print ("%s for KSK %s/%03d/%05d (%s) found in parent" %
+                   (rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
+                    rr.keyid, SECRR.hashalgs[rr.hashalg]))
+            found = True
+        else:
+            print ("%s for KSK %s/%03d/%05d (%s) missing from parent" %
+                   (rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
+                    rr.keyid, SECRR.hashalgs[rr.hashalg]))
+
+    if not found:
+        print ("No %s records were found for any DNSKEY" % ("DLV" if lookaside else "DS"))
+
+    return found
+
+############################################################################
+# parse_args:
+# Read command line arguments, set global 'args' structure
+############################################################################
+def parse_args():
+    parser = argparse.ArgumentParser(description=prog + ': checks DS coverage')
+
+    bindir = 'bin'
+    sbindir = 'bin' if os.name == 'nt' else 'sbin'
+
+    parser.add_argument('zone', type=str, help='zone to check')
+    parser.add_argument('-f', '--file', dest='masterfile', type=str,
+                        help='zone master file')
+    parser.add_argument('-l', '--lookaside', dest='lookaside', type=str,
+                        help='DLV lookaside zone')
+    parser.add_argument('-d', '--dig', dest='dig',
+                        default=os.path.join(prefix(bindir), 'dig'),
+                        type=str, help='path to \'dig\'')
+    parser.add_argument('-D', '--dsfromkey', dest='dsfromkey',
+                        default=os.path.join(prefix(sbindir),
+                                             'dnssec-dsfromkey'),
+                        type=str, help='path to \'dig\'')
+    parser.add_argument('-v', '--version', action='version',
+                        version=version)
+    args = parser.parse_args()
+
+    args.zone = args.zone.strip('.')
+    if args.lookaside:
+        args.lookaside = args.lookaside.strip('.')
+
+    return args
+
+
+############################################################################
+# Main
+############################################################################
+def main():
+    args = parse_args()
+    found = check(args.zone, args, args.masterfile, args.lookaside)
+    exit(0 if found else 1)
diff --git a/bin/python/isc/coverage.py b/bin/python/isc/coverage.py
new file mode 100644 (file)
index 0000000..c9e8959
--- /dev/null
@@ -0,0 +1,292 @@
+############################################################################
+# Copyright (C) 2013-2015  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.
+############################################################################
+
+from __future__ import print_function
+import os
+import sys
+import argparse
+import glob
+import re
+import time
+import calendar
+import pprint
+from collections import defaultdict
+
+prog = 'dnssec-coverage'
+
+from isc import *
+from isc.utils import prefix
+
+
+############################################################################
+# print a fatal error and exit
+############################################################################
+def fatal(*args, **kwargs):
+    print(*args, **kwargs)
+    sys.exit(1)
+
+
+############################################################################
+# output:
+############################################################################
+_firstline = True
+def output(*args, **kwargs):
+    """output text, adding a vertical space this is *not* the first
+    first section being printed since a call to vreset()"""
+    global _firstline
+    if 'skip' in kwargs:
+        skip = kwargs['skip']
+        kwargs.pop('skip', None)
+    else:
+        skip = True
+    if _firstline:
+        _firstline = False
+    elif skip:
+        print('')
+    if args:
+        print(*args, **kwargs)
+
+
+def vreset():
+    """reset vertical spacing"""
+    global _firstline
+    _firstline = True
+
+
+############################################################################
+# parse_time
+############################################################################
+def parse_time(s):
+    """ convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds
+    :param s: String with some text representing a time interval
+    :return: Integer with the number of seconds in the time interval
+    """
+    s = s.strip()
+
+    # if s is an integer, we're done already
+    try:
+        return int(s)
+    except ValueError:
+        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 ValueError("Cannot parse %s" % s)
+    n, unit = m.groups()
+    n = int(n)
+    unit = unit.lower()
+    if unit.startswith('y'):
+        return n * 31536000
+    elif unit.startswith('mo'):
+        return n * 2592000
+    elif unit.startswith('w'):
+        return n * 604800
+    elif unit.startswith('d'):
+        return n * 86400
+    elif unit.startswith('h'):
+        return n * 3600
+    elif unit.startswith('mi'):
+        return n * 60
+    elif unit.startswith('s'):
+        return n
+    else:
+        raise ValueError("Invalid suffix %s" % unit)
+
+
+############################################################################
+# 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.
+    :param command: string with a command to look for in the path
+    :param default: default location to use
+    :return: detected location for the desired command
+    """
+
+    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 = os.path.join(directory, command)
+            if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
+                break
+            fpath = None
+
+    return fpath
+
+
+############################################################################
+# parse_args:
+############################################################################
+def parse_args():
+    """Read command line arguments, set global 'args' structure"""
+    compilezone = set_path('named-compilezone',
+                           os.path.join(prefix('sbin'), 'named-compilezone'))
+
+    parser = argparse.ArgumentParser(description=prog + ': checks future ' +
+                                     'DNSKEY coverage for a zone')
+
+    parser.add_argument('zone', type=str, nargs='*', default=None,
+                        help='zone(s) to check' +
+                        '(default: all zones in the directory)')
+    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='time')
+    parser.add_argument('-d', dest='keyttl', type=str,
+                        help='the DNSKEY TTL', metavar='time')
+    parser.add_argument('-r', dest='resign', default='1944000',
+                        type=str, help='the RRSIG refresh interval '
+                                       'in seconds [default: 22.5 days]',
+                        metavar='time')
+    parser.add_argument('-c', dest='compilezone',
+                        default=compilezone, type=str,
+                        help='path to \'named-compilezone\'',
+                        metavar='path')
+    parser.add_argument('-l', dest='checklimit',
+                        type=str, default='0',
+                        help='Length of time to check for '
+                             'DNSSEC coverage [default: 0 (unlimited)]',
+                        metavar='time')
+    parser.add_argument('-z', dest='no_ksk',
+                        action='store_true', default=False,
+                        help='Only check zone-signing keys (ZSKs)')
+    parser.add_argument('-k', dest='no_zsk',
+                        action='store_true', default=False,
+                        help='Only check key-signing keys (KSKs)')
+    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=utils.version)
+
+    args = parser.parse_args()
+
+    if args.no_zsk and args.no_ksk:
+        fatal("ERROR: -z and -k cannot be used together.")
+    elif args.no_zsk or args.no_ksk:
+        args.keytype = "KSK" if args.no_zsk else "ZSK"
+    else:
+        args.keytype = None
+
+    if args.filename and len(args.zone) > 1:
+        fatal("ERROR: -f can only be used with one zone.")
+
+    # convert from time arguments to seconds
+    try:
+        if args.maxttl:
+            m = parse_time(args.maxttl)
+            args.maxttl = m
+    except ValueError:
+        pass
+
+    try:
+        if args.keyttl:
+            k = parse_time(args.keyttl)
+            args.keyttl = k
+    except ValueError:
+        pass
+
+    try:
+        if args.resign:
+            r = parse_time(args.resign)
+            args.resign = r
+    except ValueError:
+        pass
+
+    try:
+        if args.checklimit:
+            lim = args.checklimit
+            r = parse_time(args.checklimit)
+            if r == 0:
+                args.checklimit = None
+            else:
+                args.checklimit = time.time() + r
+    except ValueError:
+        pass
+
+    # if we've got the values we need from the command line, stop now
+    if args.maxttl and args.keyttl:
+        return args
+
+    # load keyttl and maxttl data from zonefile
+    if args.zone and args.filename:
+        try:
+            zone = keyzone(args.zone[0], args.filename, args.compilezone)
+            args.maxttl = args.maxttl or zone.maxttl
+            args.keyttl = args.maxttl or zone.keyttl
+        except Exception as e:
+            print("Unable to load zone data from %s: " % args.filename, e)
+
+    if not args.maxttl:
+        output("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
+
+    return args
+
+############################################################################
+# Main
+############################################################################
+def main():
+    args = parse_args()
+
+    print("PHASE 1--Loading keys to check for internal timing problems")
+
+    try:
+        kd = keydict(path=args.path, zone=args.zone, keyttl=args.keyttl)
+    except Exception as e:
+        fatal('ERROR: Unable to build key dictionary: ' + str(e))
+
+    for key in kd:
+        key.check_prepub(output)
+        if key.sep:
+            key.check_postpub(output)
+        else:
+            key.check_postpub(output, args.maxttl + args.resign)
+
+    output("PHASE 2--Scanning future key events for coverage failures")
+    vreset()
+
+    try:
+        elist = eventlist(kd)
+    except Exception as e:
+        fatal('ERROR: Unable to build event list: ' + str(e))
+
+    errors = False
+    if not args.zone:
+        if not elist.coverage(None, args.keytype, args.checklimit, output):
+            errors = True
+    else:
+        for zone in args.zone:
+            try:
+                if not elist.coverage(zone, args.keytype,
+                                      args.checklimit, output):
+                    errors = True
+            except:
+                output('ERROR: Coverage check failed for zone ' + zone)
+
+    sys.exit(1 if errors else 0)
diff --git a/bin/python/isc/dnskey.py b/bin/python/isc/dnskey.py
new file mode 100644 (file)
index 0000000..f1559e7
--- /dev/null
@@ -0,0 +1,504 @@
+############################################################################
+# Copyright (C) 2013-2015  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 os
+import time
+import calendar
+from subprocess import Popen, PIPE
+
+########################################################################
+# Class dnskey
+########################################################################
+class TimePast(Exception):
+    def __init__(self, key, prop, value):
+        super(TimePast, self).__init__('%s time for key %s (%d) is already past'
+                                       % (prop, key, value))
+
+class dnskey:
+    """An individual DNSSEC key.  Identified by path, name, algorithm, keyid.
+    Contains a dictionary of metadata events."""
+
+    _PROPS = ('Created', 'Publish', 'Activate', 'Inactive', 'Delete',
+              'Revoke', 'DSPublish', 'SyncPublish', 'SyncDelete')
+    _OPTS = (None, '-P', '-A', '-I', '-D', '-R', None, '-Psync', '-Dsync')
+
+    _ALGNAMES = (None, 'RSAMD5', 'DH', 'DSA', 'ECC', 'RSASHA1',
+                 'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None,
+                 'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256',
+                 'ECDSAP384SHA384')
+
+    def __init__(self, key, directory=None, keyttl=None):
+        # this makes it possible to use algname as a class or instance method
+        if isinstance(key, tuple) and len(key) == 3:
+            self._dir = directory or '.'
+            (name, alg, keyid) = key
+            self.fromtuple(name, alg, keyid, keyttl)
+
+        self._dir = directory or os.path.dirname(key) or '.'
+        key = os.path.basename(key)
+
+        (name, alg, keyid) = key.split('+')
+        name = name[1:-1]
+        alg = int(alg)
+        keyid = int(keyid.split('.')[0])
+        self.fromtuple(name, alg, keyid, keyttl)
+
+    def fromtuple(self, name, alg, keyid, keyttl):
+        if name.endswith('.'):
+            fullname = name
+            name = name.rstrip('.')
+        else:
+            fullname = name + '.'
+
+        keystr = "K%s+%03d+%05d" % (fullname, alg, keyid)
+        key_file = self._dir + (self._dir and os.sep or '') + keystr + ".key"
+        private_file = (self._dir + (self._dir and os.sep or '') +
+                        keystr + ".private")
+
+        self.keystr = keystr
+
+        self.name = name
+        self.alg = int(alg)
+        self.keyid = int(keyid)
+        self.fullname = fullname
+
+        kfp = open(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 = keyttl
+            else:
+                septoken = 4
+                self.ttl = int(tokens[1]) if not keyttl else keyttl
+
+            if (int(tokens[septoken]) & 0x1) == 1:
+                self.sep = True
+            else:
+                self.sep = False
+        kfp.close()
+
+        pfp = open(private_file, "rU")
+
+        self.metadata = dict()
+        self._changed = dict()
+        self._delete = dict()
+        self._times = dict()
+        self._fmttime = dict()
+        self._timestamps = dict()
+        self._original = dict()
+        self._origttl = None
+
+        for line in pfp:
+            line = line.strip()
+            if not line or line[0] in ('!#'):
+                continue
+            punctuation = [line.find(c) for c in ':= '] + [len(line)]
+            found = min([pos for pos in punctuation if pos != -1])
+            name = line[:found].rstrip()
+            value = line[found:].lstrip(":= ").rstrip()
+            self.metadata[name] = value
+
+        for prop in dnskey._PROPS:
+            self._changed[prop] = False
+            if prop in self.metadata:
+                t = self.parsetime(self.metadata[prop])
+                self._times[prop] = t
+                self._fmttime[prop] = self.formattime(t)
+                self._timestamps[prop] = self.epochfromtime(t)
+                self._original[prop] = self._timestamps[prop]
+            else:
+                self._times[prop] = None
+                self._fmttime[prop] = None
+                self._timestamps[prop] = None
+                self._original[prop] = None
+
+        pfp.close()
+
+    def commit(self, settime_bin, **kwargs):
+        quiet = kwargs.get('quiet', False)
+        cmd = []
+        first = True
+
+        if self._origttl is not None:
+            cmd += ["-L", str(self.ttl)]
+
+        for prop, opt in zip(dnskey._PROPS, dnskey._OPTS):
+            if not opt or not self._changed[prop]:
+                continue
+
+            delete = False
+            if prop in self._delete and self._delete[prop]:
+                delete = True
+
+            when = 'none' if delete else self._fmttime[prop]
+            cmd += [opt, when]
+            first = False
+
+        if cmd:
+            fullcmd = [settime_bin, "-K", self._dir] + cmd + [self.keystr,]
+            if not quiet:
+                print('# ' + ' '.join(fullcmd))
+            try:
+                p = Popen(fullcmd, stdout=PIPE, stderr=PIPE)
+                stdout, stderr = p.communicate()
+                if stderr:
+                    raise Exception(str(stderr))
+            except Exception as e:
+                raise Exception('unable to run %s: %s' %
+                                (settime_bin, str(e)))
+            self._origttl = None
+            for prop in dnskey._PROPS:
+                self._original[prop] = self._timestamps[prop]
+                self._changed[prop] = False
+
+    @classmethod
+    def generate(cls, keygen_bin, keys_dir, name, alg, keysize, sep,
+                 ttl, publish=None, activate=None, **kwargs):
+        quiet = kwargs.get('quiet', False)
+
+        keygen_cmd = [keygen_bin, "-q", "-K", keys_dir, "-L", str(ttl)]
+
+        if sep:
+            keygen_cmd.append("-fk")
+
+        if alg:
+            keygen_cmd += ["-a", alg]
+
+        if keysize:
+            keygen_cmd += ["-b", str(keysize)]
+
+        if publish:
+            t = dnskey.timefromepoch(publish)
+            keygen_cmd += ["-P", dnskey.formattime(t)]
+
+        if activate:
+            t = dnskey.timefromepoch(activate)
+            keygen_cmd += ["-A", dnskey.formattime(activate)]
+
+        keygen_cmd.append(name)
+
+        if not quiet:
+            print('# ' + ' '.join(keygen_cmd))
+
+        p = Popen(keygen_cmd, stdout=PIPE, stderr=PIPE)
+        stdout, stderr = p.communicate()
+        if stderr:
+            raise Exception('unable to generate key: ' + str(stderr))
+
+        try:
+            keystr = stdout.splitlines()[0]
+            newkey = dnskey(keystr, keys_dir, ttl)
+            return newkey
+        except Exception as e:
+            raise Exception('unable to generate key: %s' % str(e))
+
+    def generate_successor(self, keygen_bin, **kwargs):
+        quiet = kwargs.get('quiet', False)
+
+        if not self.inactive():
+            raise Exception("predecessor key %s has no inactive date" % self)
+
+        keygen_cmd = [keygen_bin, "-q", "-K", self._dir, "-S", self.keystr]
+
+        if self.ttl:
+            keygen_cmd += ["-L", str(self.ttl)]
+
+        if not quiet:
+            print('# ' + ' '.join(keygen_cmd))
+
+        p = Popen(keygen_cmd, stdout=PIPE, stderr=PIPE)
+        stdout, stderr = p.communicate()
+        if stderr:
+            raise Exception('unable to generate key: ' + stderr)
+
+        try:
+            keystr = stdout.splitlines()[0]
+            newkey = dnskey(keystr, self._dir, self.ttl)
+            return newkey
+        except:
+            raise Exception('unable to generate successor for key %s' % self)
+
+    @staticmethod
+    def algstr(alg):
+        name = None
+        if alg in range(len(dnskey._ALGNAMES)):
+            name = dnskey._ALGNAMES[alg]
+        return name if name else ("%03d" % alg)
+
+    @staticmethod
+    def algnum(alg):
+        if not alg:
+            return None
+        alg = alg.upper()
+        try:
+            return dnskey._ALGNAMES.index(alg)
+        except ValueError:
+            return None
+
+    def algname(self, alg=None):
+        return self.algstr(alg or self.alg)
+
+    @staticmethod
+    def timefromepoch(secs):
+        return time.gmtime(secs)
+
+    @staticmethod
+    def parsetime(string):
+        return time.strptime(string, "%Y%m%d%H%M%S")
+
+    @staticmethod
+    def epochfromtime(t):
+        return calendar.timegm(t)
+
+    @staticmethod
+    def formattime(t):
+        return time.strftime("%Y%m%d%H%M%S", t)
+
+    def setmeta(self, prop, secs, now, **kwargs):
+        force = kwargs.get('force', False)
+
+        if self._timestamps[prop] == secs:
+            return
+
+        if self._original[prop] is not None and \
+           self._original[prop] < now and not force:
+            raise TimePast(self, prop, self._original[prop])
+
+        if secs is None:
+            self._changed[prop] = False \
+                if self._original[prop] is None else True
+
+            self._delete[prop] = True
+            self._timestamps[prop] = None
+            self._times[prop] = None
+            self._fmttime[prop] = None
+            return
+
+        t = self.timefromepoch(secs)
+        self._timestamps[prop] = secs
+        self._times[prop] = t
+        self._fmttime[prop] = self.formattime(t)
+        self._changed[prop] = False if \
+            self._original[prop] == self._timestamps[prop] else True
+
+    def gettime(self, prop):
+        return self._times[prop]
+
+    def getfmttime(self, prop):
+        return self._fmttime[prop]
+
+    def gettimestamp(self, prop):
+        return self._timestamps[prop]
+
+    def created(self):
+        return self._timestamps["Created"]
+
+    def syncpublish(self):
+        return self._timestamps["SyncPublish"]
+
+    def setsyncpublish(self, secs, now=time.time(), **kwargs):
+        self.setmeta("SyncPublish", secs, now, **kwargs)
+
+    def publish(self):
+        return self._timestamps["Publish"]
+
+    def setpublish(self, secs, now=time.time(), **kwargs):
+        self.setmeta("Publish", secs, now, **kwargs)
+
+    def activate(self):
+        return self._timestamps["Activate"]
+
+    def setactivate(self, secs, now=time.time(), **kwargs):
+        self.setmeta("Activate", secs, now, **kwargs)
+
+    def revoke(self):
+        return self._timestamps["Revoke"]
+
+    def setrevoke(self, secs, now=time.time(), **kwargs):
+        self.setmeta("Revoke", secs, now, **kwargs)
+
+    def inactive(self):
+        return self._timestamps["Inactive"]
+
+    def setinactive(self, secs, now=time.time(), **kwargs):
+        self.setmeta("Inactive", secs, now, **kwargs)
+
+    def delete(self):
+        return self._timestamps["Delete"]
+
+    def setdelete(self, secs, now=time.time(), **kwargs):
+        self.setmeta("Delete", secs, now, **kwargs)
+
+    def syncdelete(self):
+        return self._timestamps["SyncDelete"]
+
+    def setsyncdelete(self, secs, now=time.time(), **kwargs):
+        self.setmeta("SyncDelete", secs, now, **kwargs)
+
+    def setttl(self, ttl):
+        if ttl is None or self.ttl == ttl:
+            return
+        elif self._origttl is None:
+            self._origttl = self.ttl
+            self.ttl = ttl
+        elif self._origttl == ttl:
+            self._origttl = None
+            self.ttl = ttl
+        else:
+            self.ttl = ttl
+
+    def keytype(self):
+        return ("KSK" if self.sep else "ZSK")
+
+    def __str__(self):
+        return ("%s/%s/%05d"
+                % (self.name, self.algname(), self.keyid))
+
+    def __repr__(self):
+        return ("%s/%s/%05d (%s)"
+                % (self.name, self.algname(), self.keyid,
+                   ("KSK" if self.sep else "ZSK")))
+
+    def date(self):
+        return (self.activate() or self.publish() or self.created())
+
+    # keys are sorted first by zone name, then by algorithm. within
+    # the same name/algorithm, they are sorted according to their
+    # 'date' value: the activation date if set, OR the publication
+    # if set, OR the creation date.
+    def __lt__(self, other):
+        if self.name != other.name:
+            return self.name < other.name
+        if self.alg != other.alg:
+            return self.alg < other.alg
+        return self.date() < other.date()
+
+    def check_prepub(self, output=None):
+        def noop(*args, **kwargs): pass
+        if not output:
+            output = noop
+
+        now = int(time.time())
+        a = self.activate()
+        p = self.publish()
+
+        if not a:
+            return False
+
+        if not p:
+            if a > now:
+                output("WARNING: Key %s is scheduled for\n"
+                       "\t activation but not for publication."
+                       % repr(self))
+            return False
+
+        if p <= now and a <= now:
+            return True
+
+        if p == a:
+            output("WARNING: %s is scheduled to be\n"
+                   "\t published and activated at the same time. This\n"
+                   "\t could result in a coverage gap if the zone was\n"
+                   "\t previously signed. Activation should be at least\n"
+                   "\t %s after publication."
+                   % (repr(self),
+                       dnskey.duration(self.ttl) or 'one DNSKEY TTL'))
+            return True
+
+        if a < p:
+            output("WARNING: Key %s is active before it is published"
+                   % repr(self))
+            return False
+
+        if self.ttl is not None and a - p < self.ttl:
+            output("WARNING: Key %s is activated too soon\n"
+                   "\t after publication; this could result in coverage \n"
+                   "\t gaps due to resolver caches containing old data.\n"
+                   "\t Activation should be at least %s after\n"
+                   "\t publication."
+                   % (repr(self),
+                      dnskey.duration(self.ttl) or 'one DNSKEY TTL'))
+            return False
+
+        return True
+
+    def check_postpub(self, output = None, timespan = None):
+        def noop(*args, **kwargs): pass
+        if output is None:
+            output = noop
+
+        if timespan is None:
+            timespan = self.ttl
+
+        now = time.time()
+        d = self.delete()
+        i = self.inactive()
+
+        if not d:
+            return False
+
+        if not i:
+            if d > now:
+                output("WARNING: Key %s is scheduled for\n"
+                       "\t deletion but not for inactivation." % repr(self))
+            return False
+
+        if d < now and i < now:
+            return True
+
+        if d < i:
+            output("WARNING: Key %s is scheduled for\n"
+                   "\t deletion before inactivation."
+                   % repr(self))
+            return False
+
+        if d - i < timespan:
+            output("WARNING: Key %s scheduled for\n"
+                   "\t deletion too soon after deactivation; this may \n"
+                   "\t result in coverage gaps due to resolver caches\n"
+                   "\t containing old data.  Deletion should be at least\n"
+                   "\t %s after inactivation."
+                   % (repr(self), dnskey.duration(timespan)))
+            return False
+
+        return True
+
+    @staticmethod
+    def duration(secs):
+        if not secs:
+            return None
+
+        units = [("year", 60*60*24*365),
+                 ("month", 60*60*24*30),
+                 ("day", 60*60*24),
+                 ("hour", 60*60),
+                 ("minute", 60),
+                 ("second", 1)]
+
+        output = []
+        for unit in units:
+            v, secs = secs // unit[1], secs % unit[1]
+            if v > 0:
+                output.append("%d %s%s" % (v, unit[0], "s" if v > 1 else ""))
+
+        return ", ".join(output)
+
diff --git a/bin/python/isc/eventlist.py b/bin/python/isc/eventlist.py
new file mode 100644 (file)
index 0000000..4c91368
--- /dev/null
@@ -0,0 +1,171 @@
+############################################################################
+# Copyright (C) 2015  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.
+############################################################################
+
+from collections import defaultdict
+from .dnskey import *
+from .keydict import *
+from .keyevent import *
+
+
+class eventlist:
+    _K = defaultdict(lambda: defaultdict(list))
+    _Z = defaultdict(lambda: defaultdict(list))
+    _zones = set()
+    _kdict = None
+
+    def __init__(self, kdict):
+        properties = ["SyncPublish", "Publish", "SyncDelete",
+                      "Activate", "Inactive", "Delete"]
+        self._kdict = kdict
+        for zone in kdict.zones():
+            self._zones.add(zone)
+            for alg, keys in kdict[zone].items():
+                for k in keys.values():
+                    for prop in properties:
+                        t = k.gettime(prop)
+                        if not t:
+                            continue
+                        e = keyevent(prop, k, t)
+                        if k.sep:
+                            self._K[zone][alg].append(e)
+                        else:
+                            self._Z[zone][alg].append(e)
+
+                self._K[zone][alg] = sorted(self._K[zone][alg],
+                                            key=lambda event: event.when)
+                self._Z[zone][alg] = sorted(self._Z[zone][alg],
+                                            key=lambda event: event.when)
+
+    # scan events per zone, algorithm, and key type, in order of
+    # occurrance, noting inconsistent states when found
+    def coverage(self, zone, keytype, until, output = None):
+        def noop(*args, **kwargs): pass
+        if not output:
+            output = noop
+
+        no_zsk = True if (keytype and keytype == "KSK") else False
+        no_ksk = True if (keytype and keytype == "ZSK") else False
+        kok = zok = True
+        found = False
+
+        if zone and not zone in self._zones:
+            output("ERROR: No key events found for %s" % zone)
+            return False
+
+        if zone:
+            found = True
+            if not no_ksk:
+                kok = self.checkzone(zone, "KSK", until, output)
+            if not no_zsk:
+                zok = self.checkzone(zone, "ZSK", until, output)
+        else:
+            for z in self._zones:
+                if not no_ksk and z in self._K.keys():
+                    found = True
+                    kok = self.checkzone(z, "KSK", until, output)
+                if not no_zsk and z in self._Z.keys():
+                    found = True
+                    kok = self.checkzone(z, "ZSK", until, output)
+
+        if not found:
+            output("ERROR: No key events found")
+            return False
+
+        return (kok and zok)
+
+    def checkzone(self, zone, keytype, until, output):
+        allok = True
+        if keytype == "KSK":
+            kz = self._K[zone]
+        else:
+            kz = self._Z[zone]
+
+        for alg in kz.keys():
+            output("Checking scheduled %s events for zone %s, "
+                   "algorithm %s..." %
+                   (keytype, zone, dnskey.algstr(alg)))
+            ok = eventlist.checkset(kz[alg], keytype, until, output)
+            if ok:
+                output("No errors found")
+            allok = allok and ok
+
+        return allok
+
+    @staticmethod
+    def showset(eventset, output):
+        if not eventset:
+            return
+        output("  " + eventset[0].showtime() + ":", skip=False)
+        for event in eventset:
+            output("    %s: %s" % (event.what, repr(event.key)), skip=False)
+
+    @staticmethod
+    def checkset(eventset, keytype, until, output):
+        groups = list()
+        group = list()
+
+        # collect up all events that have the same time
+        eventsfound = False
+        for event in eventset:
+            # we found an event
+            eventsfound = True
+
+            # add event to current group
+            if (not group or group[0].when == event.when):
+                group.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 group
+            if (group[0].when != event.when):
+                groups.append(group)
+                group = list()
+                group.append(event)
+
+        if group:
+            groups.append(group)
+
+        if not eventsfound:
+            output("ERROR: No %s events found" % keytype)
+            return False
+
+        active = published = None
+        for group in groups:
+            if (until and calendar.timegm(group[0].when) > until):
+                output("Ignoring events after %s" %
+                      time.strftime("%a %b %d %H:%M:%S UTC %Y", 
+                          time.gmtime(until)))
+                return True
+
+            for event in group:
+                (active, published) = event.status(active, published)
+
+            eventlist.showset(group, output)
+
+            # and then check for inconsistencies:
+            if not active:
+                output("ERROR: No %s's are active after this event" % keytype)
+                return False
+            elif not published:
+                output("ERROR: No %s's are published after this event"
+                        % keytype)
+                return False
+            elif not published.intersection(active):
+                output("ERROR: No %s's are both active and published "
+                       "after this event" % keytype)
+                return False
+
+        return True
+
diff --git a/bin/python/isc/keydict.py b/bin/python/isc/keydict.py
new file mode 100644 (file)
index 0000000..cc73dc4
--- /dev/null
@@ -0,0 +1,89 @@
+############################################################################
+# Copyright (C) 2015  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.
+############################################################################
+
+from collections import defaultdict
+from . import dnskey
+import os
+import glob
+
+
+########################################################################
+# Class keydict
+########################################################################
+class keydict:
+    """ A dictionary of keys, indexed by name, algorithm, and key id """
+
+    _keydict = defaultdict(lambda: defaultdict(dict))
+    _defttl = None
+    _missing = []
+
+    def __init__(self, dp=None, **kwargs):
+        self._defttl = kwargs.get('keyttl', None)
+        zones = kwargs.get('zones', None)
+
+        if not zones:
+            path = kwargs.get('path',None) or '.'
+            self.readall(path)
+        else:
+            for zone in zones:
+                if 'path' in kwargs and kwargs['path'] is not None:
+                    path = kwargs['path']
+                else:
+                    path = dp and dp.policy(zone).directory or '.'
+                if not self.readone(path, zone):
+                    self._missing.append(zone)
+
+    def readall(self, path):
+        files = glob.glob(os.path.join(path, '*.private'))
+
+        for infile in files:
+            key = dnskey(infile, path, self._defttl)
+            self._keydict[key.name][key.alg][key.keyid] = key
+
+    def readone(self, path, zone):
+        match='K' + zone + '.+*.private'
+        files = glob.glob(os.path.join(path, match))
+
+        found = False
+        for infile in files:
+            key = dnskey(infile, path, self._defttl)
+            if key.name != zone: # shouldn't ever happen
+                continue
+            self._keydict[key.name][key.alg][key.keyid] = key
+            found = True
+
+        return found
+
+    def __iter__(self):
+        for zone, algorithms in self._keydict.items():
+            for alg, keys in algorithms.items():
+                for key in keys.values():
+                    yield key
+
+    def __getitem__(self, name):
+        return self._keydict[name]
+
+    def zones(self):
+        return (self._keydict.keys())
+
+    def algorithms(self, zone):
+        return (self._keydict[zone].keys())
+
+    def keys(self, zone, alg):
+        return (self._keydict[zone][alg].keys())
+
+    def missing(self):
+        return (self._missing)
diff --git a/bin/python/isc/keyevent.py b/bin/python/isc/keyevent.py
new file mode 100644 (file)
index 0000000..9025fee
--- /dev/null
@@ -0,0 +1,81 @@
+############################################################################
+# Copyright (C) 2013-2015  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 time
+
+
+########################################################################
+# Class keyevent
+########################################################################
+class keyevent:
+    """ A discrete key event, e.g., Publish, Activate, Inactive, Delete,
+    etc. Stores the date of the event, and identifying information
+    about the key to which the event will occur."""
+
+    def __init__(self, what, key, when=None):
+        self.what = what
+        self.when = when or key.gettime(what)
+        self.key = key
+        self.sep = key.sep
+        self.zone = key.name
+        self.alg = key.alg
+        self.keyid = key.keyid
+
+    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)
+
+    # update sets of active and published keys, based on
+    # the contents of this keyevent
+    def status(self, active, published, output = None):
+        def noop(*args, **kwargs): pass
+        if not output:
+            output = noop
+
+        if not active:
+            active = set()
+        if not published:
+            published = set()
+
+        if self.what == "Activate":
+            active.add(self.keyid)
+        elif self.what == "Publish":
+            published.add(self.keyid)
+        elif self.what == "Inactive":
+            if self.keyid not in active:
+                output("\tWARNING: %s scheduled to become inactive "
+                       "before it is active"
+                       % repr(self.key))
+            else:
+                active.remove(self.keyid)
+        elif self.what == "Delete":
+            if self.keyid in published:
+                published.remove(self.keyid)
+            else:
+                output("WARNING: key %s is scheduled for deletion "
+                       "before it is published" % repr(self.key))
+        elif self.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 self.keyid in published:
+                published.remove(self.keyid)
+            if self.keyid in active:
+                active.remove(self.keyid)
+
+        return active, published
diff --git a/bin/python/isc/keymgr.py b/bin/python/isc/keymgr.py
new file mode 100644 (file)
index 0000000..a3a9043
--- /dev/null
@@ -0,0 +1,152 @@
+############################################################################
+# Copyright (C) 2015  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.
+############################################################################
+
+from __future__ import print_function
+import os, sys, argparse, glob, re, time, calendar, pprint
+from collections import defaultdict
+
+prog='dnssec-keymgr'
+
+from isc import *
+from isc.utils import prefix
+
+############################################################################
+# print a fatal error and exit
+############################################################################
+def fatal(*args, **kwargs):
+    print(*args, **kwargs)
+    sys.exit(1)
+
+############################################################################
+# find the location of an external command
+############################################################################
+def set_path(command, default=None):
+    """ find the location of a specified command. If a default is supplied,
+    exists and it's an executable, we use it; otherwise we search PATH
+    for an alternative.
+    :param command: command to look for
+    :param default: default value to use
+    :return: PATH with the location of a suitable binary
+    """
+    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) and os.access(fpath, os.X_OK):
+                break
+            fpath = None
+
+    return fpath
+
+############################################################################
+# parse arguments
+############################################################################
+def parse_args():
+    """ Read command line arguments, returns 'args' object
+    :return: args object properly prepared
+    """
+
+    keygen = set_path('dnssec-keygen',
+                      os.path.join(prefix('sbin'), 'dnssec-keygen'))
+    settime = set_path('dnssec-settime',
+                       os.path.join(prefix('sbin'), 'dnssec-settime'))
+
+    parser = argparse.ArgumentParser(description=prog + ': schedule '
+                                     'DNSSEC key rollovers according to a '
+                                     'pre-defined policy')
+
+    parser.add_argument('zone', type=str, nargs='*', default=None,
+                        help='Zone(s) to which the policy should be applied ' +
+                        '(default: all zones in the directory)')
+    parser.add_argument('-K', dest='path', type=str,
+                        help='Directory containing keys', metavar='dir')
+    parser.add_argument('-c', dest='policyfile', type=str,
+                        help='Policy definition file', metavar='file')
+    parser.add_argument('-g', dest='keygen', default=keygen, type=str,
+                        help='Path to \'dnssec-keygen\'',
+                        metavar='path')
+    parser.add_argument('-s', dest='settime', default=settime, type=str,
+                        help='Path to \'dnssec-settime\'',
+                        metavar='path')
+    parser.add_argument('-k', dest='no_zsk',
+                        action='store_true', default=False,
+                        help='Only apply policy to key-signing keys (KSKs)')
+    parser.add_argument('-z', dest='no_ksk',
+                        action='store_true', default=False,
+                        help='Only apply policy to zone-signing keys (ZSKs)')
+    parser.add_argument('-f', '--force', dest='force', action='store_true',
+                        default=False, help='Force updates to key events '+
+                        'even if they are in the past')
+    parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
+                        default=False, help='Update keys silently')
+    parser.add_argument('-v', '--version', action='version',
+                        version=utils.version)
+
+    args = parser.parse_args()
+
+    if args.no_zsk and args.no_ksk:
+        fatal("ERROR: -z and -k cannot be used together.")
+
+    if args.keygen is None or args.settime is None:
+        fatal("ERROR: dnssec-keygen/dnssec-settime not found")
+
+    # if a policy file was specified, check that it exists.
+    # if not, use the default file, unless it doesn't exist
+    if args.policyfile is not None:
+        if not os.path.exists(args.policyfile):
+            fatal('ERROR: Policy file "%s" not found' % args.policyfile)
+    else:
+        args.policyfile = os.path.join(utils.sysconfdir, 'policy.conf')
+        if not os.path.exists(args.policyfile):
+            args.policyfile = None
+
+    return args
+
+############################################################################
+# main
+############################################################################
+def main():
+    args = parse_args()
+
+    # As we may have specific locations for the binaries, we put that info
+    # into a context object that can be passed around
+    context = {'keygen_path': args.keygen,
+               'settime_path': args.settime,
+               'keys_path': args.path}
+
+    try:
+        dp = policy.dnssec_policy(args.policyfile)
+    except Exception as e:
+        fatal('Unable to load DNSSEC policy: ' + str(e))
+
+    try:
+        kd = keydict(dp, path=args.path, zones=args.zone)
+    except Exception as e:
+        fatal('Unable to build key dictionary: ' + str(e))
+
+    try:
+        ks = keyseries(kd, context=context)
+    except Exception as e:
+        fatal('Unable to build key series: ' + str(e))
+
+    try:
+        ks.enforce_policy(dp, ksk=args.no_zsk, zsk=args.no_ksk,
+                          force=args.force, quiet=args.quiet)
+    except Exception as e:
+        fatal('Unable to apply policy: ' + str(e))
diff --git a/bin/python/isc/keyseries.py b/bin/python/isc/keyseries.py
new file mode 100644 (file)
index 0000000..ed09f71
--- /dev/null
@@ -0,0 +1,194 @@
+############################################################################
+# Copyright (C) 2015  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.
+############################################################################
+
+from collections import defaultdict
+from .dnskey import *
+from .keydict import *
+from .keyevent import *
+from .policy import *
+import time
+
+
+class keyseries:
+    _K = defaultdict(lambda: defaultdict(list))
+    _Z = defaultdict(lambda: defaultdict(list))
+    _zones = set()
+    _kdict = None
+    _context = None
+
+    def __init__(self, kdict, now=time.time(), context=None):
+        self._kdict = kdict
+        self._context = context
+        self._zones = set(kdict.missing())
+
+        for zone in kdict.zones():
+            self._zones.add(zone)
+            for alg, keys in kdict[zone].items():
+                for k in keys.values():
+                    if k.sep:
+                        self._K[zone][alg].append(k)
+                    else:
+                        self._Z[zone][alg].append(k)
+
+                for group in [self._K[zone][alg], self._Z[zone][alg]]:
+                    group.sort()
+                    for k in group:
+                        if k.delete() and k.delete() < now:
+                            group.remove(k)
+
+    def __iter__(self):
+        for zone in self._zones:
+            for collection in [self._K, self._Z]:
+                if zone not in collection:
+                    continue
+                for alg, keys in collection[zone].items():
+                    for key in keys:
+                        yield key
+
+    def dump(self):
+        for k in self:
+            print("%s" % repr(k))
+
+    def fixseries(self, keys, policy, now, **kwargs):
+        force = kwargs.get('force', False)
+        if not keys:
+            return
+
+        # handle the first key
+        key = keys[0]
+        if key.sep:
+            rp = policy.ksk_rollperiod
+            prepub = policy.ksk_prepublish or (30 * 86400)
+            postpub = policy.ksk_postpublish or (30 * 86400)
+        else:
+            rp = policy.zsk_rollperiod
+            prepub = policy.zsk_prepublish or (30 * 86400)
+            postpub = policy.zsk_postpublish or (30 * 86400)
+
+        # the first key should be published and active
+        p = key.publish()
+        a = key.activate()
+        if not p or p > now:
+            key.setpublish(now)
+        if not a or a > now:
+            key.setactivate(now)
+
+        if not rp:
+            key.setinactive(None, **kwargs)
+            key.setdelete(None, **kwargs)
+        else:
+            key.setinactive(a + rp, **kwargs)
+            key.setdelete(a + rp + postpub, **kwargs)
+
+        if policy.keyttl != key.ttl:
+            key.setttl(policy.keyttl)
+
+        # handle all the subsequent keys
+        prev = key
+        for key in keys[1:]:
+            # if no rollperiod, then all keys after the first in
+            # the series kept inactive.
+            # (XXX: we need to change this to allow standby keys)
+            if not rp:
+                key.setpublish(None, **kwargs)
+                key.setactivate(None, **kwargs)
+                key.setinactive(None, **kwargs)
+                key.setdelete(None, **kwargs)
+                if policy.keyttl != key.ttl:
+                    key.setttl(policy.keyttl)
+                continue
+
+            # otherwise, ensure all dates are set correctly based on
+            # the initial key
+            a = prev.inactive()
+            p = a - prepub
+            key.setactivate(a, **kwargs)
+            key.setpublish(p, **kwargs)
+            key.setinactive(a + rp, **kwargs)
+            key.setdelete(a + rp + postpub, **kwargs)
+            prev.setdelete(a + postpub, **kwargs)
+            if policy.keyttl != key.ttl:
+                key.setttl(policy.keyttl)
+            prev = key
+
+        # if we haven't got sufficient coverage, create successor key(s)
+        while rp and prev.inactive() and \
+              prev.inactive() < now + policy.coverage:
+            # commit changes to predecessor: a successor can only be
+            # generated if Inactive has been set in the predecessor key
+            prev.commit(self._context['settime_path'], **kwargs)
+            key = prev.generate_successor(self._context['keygen_path'],
+                                          **kwargs)
+
+            key.setinactive(key.activate() + rp, **kwargs)
+            key.setdelete(key.inactive() + postpub, **kwargs)
+            keys.append(key)
+            prev = key
+
+        # last key? we already know we have sufficient coverage now, so
+        # disable the inactivation of the final key (if it was set),
+        # ensuring that if dnssec-keymgr isn't run again, the last key
+        # in the series will at least remain usable.
+        prev.setinactive(None, **kwargs)
+        prev.setdelete(None, **kwargs)
+
+        # commit changes
+        for key in keys:
+            key.commit(self._context['settime_path'], **kwargs)
+
+
+    def enforce_policy(self, policies, now=time.time(), **kwargs):
+        # If zones is provided as a parameter, use that list.
+        # If not, use what we have in this object
+        zones = kwargs.get('zones', self._zones)
+        keys_dir = kwargs.get('dir', self._context.get('keys_path', None))
+        force = kwargs.get('force', False)
+
+        for zone in zones:
+            collections = []
+            policy = policies.policy(zone)
+            keys_dir = keys_dir or policy.directory or '.'
+            alg = policy.algorithm
+            algnum = dnskey.algnum(alg)
+            if 'ksk' not in kwargs or not kwargs['ksk']:
+                if len(self._Z[zone][algnum]) == 0:
+                    k = dnskey.generate(self._context['keygen_path'],
+                                        keys_dir, zone, alg,
+                                        policy.zsk_keysize, False,
+                                        policy.keyttl or 3600,
+                                        **kwargs)
+                    self._Z[zone][algnum].append(k)
+                collections.append(self._Z[zone])
+
+            if 'zsk' not in kwargs or not kwargs['zsk']:
+                if len(self._K[zone][algnum]) == 0:
+                    k = dnskey.generate(self._context['keygen_path'],
+                                        keys_dir, zone, alg,
+                                        policy.ksk_keysize, True,
+                                        policy.keyttl or 3600,
+                                        **kwargs)
+                    self._K[zone][algnum].append(k)
+                collections.append(self._K[zone])
+
+            for collection in collections:
+                for algorithm, keys in collection.items():
+                    if algorithm != algnum:
+                        continue
+                    try:
+                        self.fixseries(keys, policy, now, **kwargs)
+                    except Exception as e:
+                        raise Exception('%s/%s: %s' %
+                                        (zone, dnskey.algstr(algnum), str(e)))
diff --git a/bin/python/isc/keyzone.py b/bin/python/isc/keyzone.py
new file mode 100644 (file)
index 0000000..7dfb31a
--- /dev/null
@@ -0,0 +1,60 @@
+############################################################################
+# Copyright (C) 2013-2015  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 os
+import sys
+import re
+from subprocess import Popen, PIPE
+
+########################################################################
+# Exceptions
+########################################################################
+class KeyZoneException(Exception):
+    pass
+
+########################################################################
+# class keyzone
+########################################################################
+class keyzone:
+    """reads a zone file to find data relevant to keys"""
+
+    def __init__(self, name, filename, czpath):
+        self.maxttl = None
+        self.keyttl = None
+
+        if not name:
+            return
+
+        if not czpath or not os.path.isfile(czpath) \
+                or not os.access(czpath, os.X_OK):
+            raise KeyZoneException('"named-compilezone" not found')
+            return
+
+        maxttl = keyttl = None
+
+        fp, _ = Popen([czpath, "-o", "-", name, filename],
+                      stdout=PIPE, stderr=PIPE).communicate()
+        for line in fp.splitlines():
+            if re.search('^[:space:]*;', line):
+                continue
+            fields = line.split()
+            if not maxttl or int(fields[1]) > maxttl:
+                maxttl = int(fields[1])
+            if fields[3] == "DNSKEY":
+                keyttl = int(fields[1])
+
+        self.keyttl = keyttl
+        self.maxttl = maxttl
diff --git a/bin/python/isc/policy.py b/bin/python/isc/policy.py
new file mode 100644 (file)
index 0000000..ed106c6
--- /dev/null
@@ -0,0 +1,690 @@
+############################################################################
+# Copyright (C) 2013-2015  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.
+############################################################################
+# policy.py
+# This module implements the parser for the dnssec.policy file.
+############################################################################
+
+import re
+import ply.lex as lex
+import ply.yacc as yacc
+from string import *
+from copy import copy
+
+
+############################################################################
+# PolicyLex: a lexer for the policy file syntax.
+############################################################################
+class PolicyLex:
+    reserved = ('POLICY',
+                'ALGORITHM_POLICY',
+                'ZONE',
+                'ALGORITHM',
+                'DIRECTORY',
+                'KEYTTL',
+                'KEY_SIZE',
+                'ROLL_PERIOD',
+                'PRE_PUBLISH',
+                'POST_PUBLISH',
+                'COVERAGE',
+                'STANDBY',
+                'NONE')
+
+    tokens = reserved + ('DATESUFFIX',
+                         'KEYTYPE',
+                         'ALGNAME',
+                         'STR',
+                         'QSTRING',
+                         'NUMBER',
+                         'LBRACE',
+                         'RBRACE',
+                         'SEMI')
+    reserved_map = {}
+
+    t_ignore           = ' \t'
+    t_ignore_olcomment = r'(//|\#).*'
+
+    t_LBRACE           = r'\{'
+    t_RBRACE           = r'\}'
+    t_SEMI             = r';';
+
+    def t_newline(self, t):
+        r'\n+'
+        t.lexer.lineno += t.value.count("\n")
+
+    def t_comment(self, t):
+        r'/\*(.|\n)*?\*/'
+        t.lexer.lineno += t.value.count('\n')
+
+    def t_DATESUFFIX(self, t):
+        r'(?i)(?<=[0-9 \t])(y(?:ears|ear|ea|e)?|mo(?:nths|nth|nt|n)?|w(?:eeks|eek|ee|e)?|d(?:ays|ay|a)?|h(?:ours|our|ou|o)?|mi(?:nutes|nute|nut|nu|n)?|s(?:econds|econd|econ|eco|ec|e)?)\b'
+        t.value = re.match(r'(?i)(y|mo|w|d|h|mi|s)([a-z]*)', t.value).group(1).lower()
+        return t
+
+    def t_KEYTYPE(self, t):
+        r'(?i)\b(KSK|ZSK)\b'
+        t.value = t.value.upper()
+        return t
+
+    def t_ALGNAME(self, t):
+        r'(?i)\b(RSAMD5|DH|DSA|NSEC3DSA|ECC|RSASHA1|NSEC3RSASHA1|RSASHA256|RSASHA512|ECCGOST|ECDSAP256SHA245|ECDSAP384SHA384)\b'
+        t.value = t.value.upper()
+        return t
+
+    def t_STR(self, t):
+        r'[A-Za-z._-][\w._-]*'
+        t.type = self.reserved_map.get(t.value, "STR")
+        return t
+
+    def t_QSTRING(self, t):
+        r'"([^"\n]|(\\"))*"'
+        t.type = self.reserved_map.get(t.value, "QSTRING")
+        t.value = t.value[1:-1]
+        return t
+
+    def t_NUMBER(self, t):
+        r'\d+'
+        t.value = int(t.value)
+        return t
+
+    def t_error(self, t):
+        print("Illegal character '%s'" % t.value[0])
+        t.lexer.skip(1)
+
+    def __init__(self, **kwargs):
+        for r in self.reserved:
+            self.reserved_map[r.lower().translate(maketrans('_', '-'))] = r
+        self.lexer = lex.lex(object=self, **kwargs)
+
+    def test(self, text):
+        self.lexer.input(text)
+        while True:
+            t = self.lexer.token()
+            if not t:
+                break
+            print(t)
+
+############################################################################
+# Policy: this object holds a set of DNSSEC policy settings.
+############################################################################
+class Policy:
+    is_zone = False
+    is_alg = False
+    is_constructed = False
+    ksk_rollperiod = None
+    zsk_rollperiod = None
+    ksk_prepublish = None
+    zsk_prepublish = None
+    ksk_postpublish = None
+    zsk_postpublish = None
+    ksk_keysize = None
+    zsk_keysize = None
+    ksk_standby = None
+    zsk_standby = None
+    keyttl = None
+    coverage = None
+    directory = None
+    valid_key_sz_per_algo = {'DSA': [512, 1024],
+                             'NSEC3DSA': [512, 1024],
+                             'RSAMD5': [512, 4096],
+                             'RSASHA1': [512, 4096],
+                             'NSEC3RSASHA1': [512, 4096],
+                             'RSASHA256': [512, 4096],
+                             'RSASHA512': [512, 4096],
+                             'ECCGOST': None,
+                             'ECDSAP256SHA245': None,
+                             'ECDSAP384SHA384': None}
+
+    def __init__(self, name=None, algorithm=None, parent=None):
+        self.name = name
+        self.algorithm = algorithm
+        self.parent = parent
+        pass
+
+    def __repr__(self):
+        return ("%spolicy %s:\n"
+                "\tinherits %s\n"
+                "\tdirectory %s\n"
+                "\talgorithm %s\n"
+                "\tcoverage %s\n"
+                "\tksk_keysize %s\n"
+                "\tzsk_keysize %s\n"
+                "\tksk_rollperiod %s\n"
+                "\tzsk_rollperiod %s\n"
+                "\tksk_prepublish %s\n"
+                "\tksk_postpublish %s\n"
+                "\tzsk_prepublish %s\n"
+                "\tzsk_postpublish %s\n"
+                "\tksk_standby %s\n"
+                "\tzsk_standby %s\n"
+                "\tkeyttl %s\n"
+                %
+                ((self.is_constructed and 'constructed ' or \
+                  self.is_zone and 'zone ' or \
+                  self.is_alg and 'algorithm ' or ''),
+                 self.name or 'UNKNOWN',
+                 self.parent and self.parent.name or 'None',
+                 self.directory and ('"' + str(self.directory) + '"') or 'None',
+                 self.algorithm or 'None',
+                 self.coverage and str(self.coverage) or 'None',
+                 self.ksk_keysize and str(self.ksk_keysize) or 'None',
+                 self.zsk_keysize and str(self.zsk_keysize) or 'None',
+                 self.ksk_rollperiod and str(self.ksk_rollperiod) or 'None',
+                 self.zsk_rollperiod and str(self.zsk_rollperiod) or 'None',
+                 self.ksk_prepublish and str(self.ksk_prepublish) or 'None',
+                 self.ksk_postpublish and str(self.ksk_postpublish) or 'None',
+                 self.zsk_prepublish and str(self.zsk_prepublish) or 'None',
+                 self.zsk_postpublish and str(self.zsk_postpublish) or 'None',
+                 self.ksk_standby and str(self.ksk_standby) or 'None',
+                 self.zsk_standby and str(self.zsk_standby) or 'None',
+                 self.keyttl and str(self.keyttl) or 'None'))
+
+    def __verify_size(self, key_size, size_range):
+        return (size_range[0] <= key_size <= size_range[1])
+
+    def get_name(self):
+        return self.name
+
+    def constructed(self):
+        return self.is_constructed
+
+    def validate(self):
+        """ Check if the values in the policy make sense
+        :return: True/False if the policy passes validation
+        """
+        if self.ksk_rollperiod and \
+           self.ksk_prepublish is not None and \
+           self.ksk_prepublish > self.ksk_rollperiod:
+            print(self.ksk_rollperiod)
+            return (False,
+                    ('KSK pre-publish period (%d) exceeds rollover period %d'
+                     % (self.ksk_prepublish, self.ksk_rollperiod)))
+
+        if self.ksk_rollperiod and \
+           self.ksk_postpublish is not None and \
+           self.ksk_postpublish > self.ksk_rollperiod:
+            return (False,
+                    ('KSK post-publish period (%d) exceeds rollover period %d'
+                     % (self.ksk_postpublish, self.ksk_rollperiod)))
+
+        if self.zsk_rollperiod and \
+           self.zsk_prepublish is not None and \
+           self.zsk_prepublish >= self.zsk_rollperiod:
+            return (False,
+                    ('ZSK pre-publish period (%d) exceeds rollover period %d'
+                     % (self.zsk_prepublish, self.zsk_rollperiod)))
+
+        if self.zsk_rollperiod and \
+           self.zsk_postpublish is not None and \
+           self.zsk_postpublish >= self.zsk_rollperiod:
+            return (False,
+                    ('ZSK post-publish period (%d) exceeds rollover period %d'
+                     % (self.zsk_postpublish, self.zsk_rollperiod)))
+
+        if self.ksk_rollperiod and \
+           self.ksk_prepublish and self.ksk_postpublish and \
+           self.ksk_prepublish + self.ksk_postpublish >= self.ksk_rollperiod:
+            return (False,
+                    (('KSK pre/post-publish periods (%d/%d) ' +
+                      'combined exceed rollover period %d') %
+                     (self.ksk_prepublish,
+                      self.ksk_postpublish,
+                      self.ksk_rollperiod)))
+
+        if self.zsk_rollperiod and \
+           self.zsk_prepublish and self.zsk_postpublish and \
+           self.zsk_prepublish + self.zsk_postpublish >= self.zsk_rollperiod:
+            return (False,
+                    (('ZSK pre/post-publish periods (%d/%d) ' +
+                      'combined exceed rollover period %d') %
+                     (self.zsk_prepublish,
+                      self.zsk_postpublish,
+                      self.zsk_rollperiod)))
+
+        if self.algorithm is not None:
+            # Validate the key size
+            key_sz_range = self.valid_key_sz_per_algo.get(self.algorithm)
+            if key_sz_range is not None:
+                # Verify KSK
+                if not self.__verify_size(self.ksk_keysize, key_sz_range):
+                    return False, 'KSK key size %d outside valid range %s' \
+                            % (self.ksk_keysize, key_sz_range)
+
+                # Verify ZSK
+                if not self.__verify_size(self.zsk_keysize, key_sz_range):
+                    return False, 'ZSK key size %d outside valid range %s' \
+                            % (self.zsk_keysize, key_sz_range)
+
+            # Specific check for DSA keys
+            if self.algorithm in ['DSA', 'NSEC3DSA'] and \
+               self.ksk_keysize % 64 != 0:
+                return False, \
+                        ('KSK key size %d not divisible by 64 ' +
+                         'as required for DSA') % self.ksk_keysize
+
+            if self.algorithm in ['DSA', 'NSEC3DSA'] and \
+               self.zsk_keysize % 64 != 0:
+                return False, \
+                        ('ZSK key size %d not divisible by 64 ' +
+                         'as required for DSA') % self.zsk_keysize
+
+        return True, ''
+
+############################################################################
+# dnssec_policy:
+# This class reads a dnssec.policy file and creates a dictionary of
+# DNSSEC policy rules from which a policy for a specific zone can
+# be generated.
+############################################################################
+class PolicyException(Exception):
+    pass
+
+class dnssec_policy:
+    alg_policy = {}
+    named_policy = {}
+    zone_policy = {}
+    current = None
+    filename = None
+    initial = True
+
+    def __init__(self, filename=None, **kwargs):
+        self.plex = PolicyLex()
+        self.tokens = self.plex.tokens
+        if 'debug' not in kwargs:
+            kwargs['debug'] = False
+        if 'write_tables' not in kwargs:
+            kwargs['write_tables'] = False
+        self.parser = yacc.yacc(module=self, **kwargs)
+
+        # set defaults
+        self.setup('''policy global { algorithm rsasha256;
+                                      key-size ksk 2048;
+                                      key-size zsk 2048;
+                                      roll-period ksk 0;
+                                      roll-period zsk 1y;
+                                      pre-publish ksk 1mo;
+                                      pre-publish zsk 1mo;
+                                      post-publish ksk 1mo;
+                                      post-publish zsk 1mo;
+                                      standby ksk 0;
+                                      standby zsk 0;
+                                      keyttl 1h;
+                                      coverage 6mo; };
+                      policy default { policy global; };''')
+
+        p = Policy()
+        p.algorithm = None
+        p.is_alg = True
+        p.ksk_keysize = 2048;
+        p.zsk_keysize = 2048;
+
+        # set default algorithm policies
+        # these need a lower default key size:
+        self.alg_policy['DSA'] = copy(p)
+        self.alg_policy['DSA'].algorithm = "DSA"
+        self.alg_policy['DSA'].name = "DSA"
+        self.alg_policy['DSA'].ksk_keysize = 1024;
+
+        self.alg_policy['NSEC3DSA'] = copy(p)
+        self.alg_policy['NSEC3DSA'].algorithm = "NSEC3DSA"
+        self.alg_policy['NSEC3DSA'].name = "NSEC3DSA"
+        self.alg_policy['NSEC3DSA'].ksk_keysize = 1024;
+
+        # these can use default settings
+        self.alg_policy['RSAMD5'] = copy(p)
+        self.alg_policy['RSAMD5'].algorithm = "RSAMD5"
+        self.alg_policy['RSAMD5'].name = "RSAMD5"
+
+        self.alg_policy['RSASHA1'] = copy(p)
+        self.alg_policy['RSASHA1'].algorithm = "RSASHA1"
+        self.alg_policy['RSASHA1'].name = "RSASHA1"
+
+        self.alg_policy['NSEC3RSASHA1'] = copy(p)
+        self.alg_policy['NSEC3RSASHA1'].algorithm = "NSEC3RSASHA1"
+        self.alg_policy['NSEC3RSASHA1'].name = "NSEC3RSASHA1"
+
+        self.alg_policy['RSASHA256'] = copy(p)
+        self.alg_policy['RSASHA256'].algorithm = "RSASHA256"
+        self.alg_policy['RSASHA256'].name = "RSASHA256"
+
+        self.alg_policy['RSASHA512'] = copy(p)
+        self.alg_policy['RSASHA512'].algorithm = "RSASHA512"
+        self.alg_policy['RSASHA512'].name = "RSASHA512"
+
+        self.alg_policy['ECCGOST'] = copy(p)
+        self.alg_policy['ECCGOST'].algorithm = "ECCGOST"
+        self.alg_policy['ECCGOST'].name = "ECCGOST"
+
+        self.alg_policy['ECDSAP256SHA245'] = copy(p)
+        self.alg_policy['ECDSAP256SHA245'].algorithm = "ECDSAP256SHA256"
+        self.alg_policy['ECDSAP256SHA245'].name = "ECDSAP256SHA256"
+
+        self.alg_policy['ECDSAP384SHA384'] = copy(p)
+        self.alg_policy['ECDSAP384SHA384'].algorithm = "ECDSAP384SHA384"
+        self.alg_policy['ECDSAP384SHA384'].name = "ECDSAP384SHA384"
+
+        if filename:
+            self.load(filename)
+
+    def load(self, filename):
+        self.filename = filename
+        self.initial = True
+        with open(filename) as f:
+            text = f.read()
+            self.plex.lexer.lineno = 0
+            self.parser.parse(text)
+
+        self.filename = None
+
+    def setup(self, text):
+        self.initial = True
+        self.plex.lexer.lineno = 0
+        self.parser.parse(text)
+
+    def policy(self, zone, **kwargs):
+        z = zone.lower()
+        p = None
+
+        if z in self.zone_policy:
+            p = self.zone_policy[z]
+
+        if p is None:
+            p = copy(self.named_policy['default'])
+            p.name = zone
+            p.is_constructed = True
+
+        if p.algorithm is None:
+            parent = p.parent or self.named_policy['default']
+            while parent and not parent.algorithm:
+                parent = parent.parent
+            p.algorithm = parent and parent.algorithm or None
+
+        if p.algorithm in self.alg_policy:
+            ap = self.alg_policy[p.algorithm]
+        else:
+            raise PolicyException('algorithm not found')
+
+        if p.directory is None:
+            parent = p.parent or self.named_policy['default']
+            while parent is not None and not parent.directory:
+                parent = parent.parent
+            p.directory = parent and parent.directory
+
+        if p.coverage is None:
+            parent = p.parent or self.named_policy['default']
+            while parent and not parent.coverage:
+                parent = parent.parent
+            p.coverage = parent and parent.coverage or ap.coverage
+
+        if p.ksk_keysize is None:
+            parent = p.parent or self.named_policy['default']
+            while parent.parent and not parent.ksk_keysize:
+                parent = parent.parent
+            p.ksk_keysize = parent and parent.ksk_keysize or ap.ksk_keysize
+
+        if p.zsk_keysize is None:
+            parent = p.parent or self.named_policy['default']
+            while parent.parent and not parent.zsk_keysize:
+                parent = parent.parent
+            p.zsk_keysize = parent and parent.zsk_keysize or ap.zsk_keysize
+
+        if p.ksk_rollperiod is None:
+            parent = p.parent or self.named_policy['default']
+            while parent.parent and not parent.ksk_rollperiod:
+                parent = parent.parent
+            p.ksk_rollperiod = parent and \
+                parent.ksk_rollperiod or ap.ksk_rollperiod
+
+        if p.zsk_rollperiod is None:
+            parent = p.parent or self.named_policy['default']
+            while parent.parent and not parent.zsk_rollperiod:
+                parent = parent.parent
+            p.zsk_rollperiod = parent and \
+                parent.zsk_rollperiod or ap.zsk_rollperiod
+
+        if p.ksk_prepublish is None:
+            parent = p.parent or self.named_policy['default']
+            while parent.parent and not parent.ksk_prepublish:
+                parent = parent.parent
+            p.ksk_prepublish = parent and \
+                parent.ksk_prepublish or ap.ksk_prepublish
+
+        if p.zsk_prepublish is None:
+            parent = p.parent or self.named_policy['default']
+            while parent.parent and not parent.zsk_prepublish:
+                parent = parent.parent
+            p.zsk_prepublish = parent and \
+                parent.zsk_prepublish or ap.zsk_prepublish
+
+        if p.ksk_postpublish is None:
+            parent = p.parent or self.named_policy['default']
+            while parent.parent and not parent.ksk_postpublish:
+                parent = parent.parent
+            p.ksk_postpublish = parent and \
+                parent.ksk_postpublish or ap.ksk_postpublish
+
+        if p.zsk_postpublish is None:
+            parent = p.parent or self.named_policy['default']
+            while parent.parent and not parent.zsk_postpublish:
+                parent = parent.parent
+            p.zsk_postpublish = parent and \
+                parent.zsk_postpublish or ap.zsk_postpublish
+
+        if 'novalidate' not in kwargs or not kwargs['novalidate']:
+            (valid, msg) = p.validate()
+            if not valid:
+                raise PolicyException(msg)
+                return None
+
+        return p
+
+
+    def p_policylist(self, p):
+        '''policylist : init policy
+                      | policylist policy'''
+        pass
+
+    def p_init(self, p):
+        "init :"
+        self.initial = False
+
+    def p_policy(self, p):
+        '''policy : alg_policy
+                  | zone_policy
+                  | named_policy'''
+        pass
+
+    def p_name(self, p):
+        '''name : STR
+                | KEYTYPE
+                | DATESUFFIX'''
+        p[0] = p[1]
+        pass
+
+    def p_new_policy(self, p):
+        "new_policy :"
+        self.current = Policy()
+
+    def p_alg_policy(self, p):
+        "alg_policy : ALGORITHM_POLICY ALGNAME new_policy alg_option_group SEMI"
+        self.current.name = p[2]
+        self.current.is_alg = True
+        self.alg_policy[p[2]] = self.current
+        pass
+
+    def p_zone_policy(self, p):
+        "zone_policy : ZONE name new_policy policy_option_group SEMI"
+        self.current.name = p[2]
+        self.current.is_zone = True
+        self.zone_policy[p[2].lower()] = self.current
+        pass
+
+    def p_named_policy(self, p):
+        "named_policy : POLICY name new_policy policy_option_group SEMI"
+        self.current.name = p[2]
+        self.named_policy[p[2].lower()] = self.current
+        pass
+
+    def p_duration_1(self, p):
+        "duration : NUMBER"
+        p[0] = p[1]
+        pass
+
+    def p_duration_2(self, p):
+        "duration : NONE"
+        p[0] = None
+        pass
+
+    def p_duration_3(self, p):
+        "duration : NUMBER DATESUFFIX"
+        if p[2] == "y":
+            p[0] = p[1] * 31536000 # year
+        elif p[2] == "mo":
+            p[0] = p[1] * 2592000  # month
+        elif p[2] == "w":
+            p[0] = p[1] * 604800   # week
+        elif p[2] == "d":
+            p[0] = p[1] * 86400    # day
+        elif p[2] == "h":
+            p[0] = p[1] * 3600     # hour
+        elif p[2] == "mi":
+            p[0] = p[1] * 60       # minute
+        elif p[2] == "s":
+            p[0] = p[1]            # second
+        else:
+            raise PolicyException('invalid duration')
+
+    def p_policy_option_group(self, p):
+        "policy_option_group : LBRACE policy_option_list RBRACE"
+        pass
+
+    def p_policy_option_list(self, p):
+        '''policy_option_list : policy_option SEMI
+                              | policy_option_list policy_option SEMI'''
+        pass
+
+    def p_policy_option(self, p):
+        '''policy_option : parent_option
+                         | directory_option
+                         | coverage_option
+                         | rollperiod_option
+                         | prepublish_option
+                         | postpublish_option
+                         | keysize_option
+                         | algorithm_option
+                         | keyttl_option
+                         | standby_option'''
+        pass
+
+    def p_alg_option_group(self, p):
+        "alg_option_group : LBRACE alg_option_list RBRACE"
+        pass
+
+    def p_alg_option_list(self, p):
+        '''alg_option_list : alg_option SEMI
+                           | alg_option_list alg_option SEMI'''
+        pass
+
+    def p_alg_option(self, p):
+        '''alg_option : coverage_option
+                      | rollperiod_option
+                      | prepublish_option
+                      | postpublish_option
+                      | keyttl_option
+                      | keysize_option
+                      | standby_option'''
+        pass
+
+    def p_parent_option(self, p):
+        "parent_option : POLICY name"
+        self.current.parent = self.named_policy[p[2].lower()]
+
+    def p_directory_option(self, p):
+        "directory_option : DIRECTORY QSTRING"
+        self.current.directory = p[2]
+
+    def p_coverage_option(self, p):
+        "coverage_option : COVERAGE duration"
+        self.current.coverage = p[2]
+
+    def p_rollperiod_option(self, p):
+        "rollperiod_option : ROLL_PERIOD KEYTYPE duration"
+        if p[2] == "KSK":
+            self.current.ksk_rollperiod = p[3]
+        else:
+            self.current.zsk_rollperiod = p[3]
+
+    def p_prepublish_option(self, p):
+        "prepublish_option : PRE_PUBLISH KEYTYPE duration"
+        if p[2] == "KSK":
+            self.current.ksk_prepublish = p[3]
+        else:
+            self.current.zsk_prepublish = p[3]
+
+    def p_postpublish_option(self, p):
+        "postpublish_option : POST_PUBLISH KEYTYPE duration"
+        if p[2] == "KSK":
+            self.current.ksk_postpublish = p[3]
+        else:
+            self.current.zsk_postpublish = p[3]
+
+    def p_keysize_option(self, p):
+        "keysize_option : KEY_SIZE KEYTYPE NUMBER"
+        if p[2] == "KSK":
+            self.current.ksk_keysize = p[3]
+        else:
+            self.current.zsk_keysize = p[3]
+
+    def p_standby_option(self, p):
+        "standby_option : STANDBY KEYTYPE NUMBER"
+        if p[2] == "KSK":
+            self.current.ksk_standby = p[3]
+        else:
+            self.current.zsk_standby = p[3]
+
+    def p_keyttl_option(self, p):
+        "keyttl_option : KEYTTL duration"
+        self.current.keyttl = p[2]
+
+    def p_algorithm_option(self, p):
+        "algorithm_option : ALGORITHM ALGNAME"
+        self.current.algorithm = p[2]
+
+    def p_error(self, p):
+        if p:
+            print("%s%s%d:syntax error near '%s'" %
+                    (self.filename or "", ":" if self.filename else "",
+                     p.lineno, p.value))
+        else:
+            if not self.initial:
+                raise PolicyException("%s%s%d:unexpected end of input" %
+                    (self.filename or "", ":" if self.filename else "",
+                     p and p.lineno or 0))
+
+if __name__ == "__main__":
+    import sys
+    if sys.argv[1] == "lex":
+        file = open(sys.argv[2])
+        text = file.read()
+        file.close()
+        plex = PolicyLex(debug=1)
+        plex.test(text)
+    elif sys.argv[1] == "parse":
+        try:
+            pp = dnssec_policy(sys.argv[2], write_tables=True, debug=True)
+            print(pp.named_policy['default'])
+            print(pp.policy("nonexistent.zone"))
+        except Exception as e:
+            print(e.args[0])
diff --git a/bin/python/isc/tests/Makefile.in b/bin/python/isc/tests/Makefile.in
new file mode 100644 (file)
index 0000000..506f2cc
--- /dev/null
@@ -0,0 +1,33 @@
+# Copyright (C) 2015  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.
+
+srcdir =       @srcdir@
+VPATH =                @srcdir@
+top_srcdir =   @top_srcdir@
+
+@BIND9_MAKE_INCLUDES@
+
+PYTHON =       @PYTHON@
+
+PYTESTS =      dnskey_test.py policy_test.py
+
+@BIND9_MAKE_RULES@
+
+check test:
+       for test in $(PYTESTS); do \
+               $(PYTHON) $$test; \
+       done
+
+clean distclean::
+       rm -f *.pyc
diff --git a/bin/python/isc/tests/dnskey_test.py b/bin/python/isc/tests/dnskey_test.py
new file mode 100644 (file)
index 0000000..2a63695
--- /dev/null
@@ -0,0 +1,57 @@
+############################################################################
+# Copyright (C) 2013-2015  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 sys
+import unittest
+sys.path.append('../..')
+from isc import *
+
+kdict = None
+
+
+def getkey():
+    global kdict
+    if not kdict:
+        kd = keydict(path='testdata')
+    for key in kd:
+        return key
+
+
+class DnskeyTest(unittest.TestCase):
+    def test_metdata(self):
+        key = getkey()
+        self.assertEqual(key.created(), 1448055647)
+        self.assertEqual(key.publish(), 1445463714)
+        self.assertEqual(key.activate(), 1448055714)
+        self.assertEqual(key.revoke(), 1479591714)
+        self.assertEqual(key.inactive(), 1511127714)
+        self.assertEqual(key.delete(), 1542663714)
+        self.assertEqual(key.syncpublish(), 1442871714)
+        self.assertEqual(key.syncdelete(), 1448919714)
+
+    def test_fmttime(self):
+        key = getkey()
+        self.assertEqual(key.getfmttime('Created'), '20151120214047')
+        self.assertEqual(key.getfmttime('Publish'), '20151021214154')
+        self.assertEqual(key.getfmttime('Activate'), '20151120214154')
+        self.assertEqual(key.getfmttime('Revoke'), '20161119214154')
+        self.assertEqual(key.getfmttime('Inactive'), '20171119214154')
+        self.assertEqual(key.getfmttime('Delete'), '20181119214154')
+        self.assertEqual(key.getfmttime('SyncPublish'), '20150921214154')
+        self.assertEqual(key.getfmttime('SyncDelete'), '20151130214154')
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/bin/python/isc/tests/policy_test.py b/bin/python/isc/tests/policy_test.py
new file mode 100644 (file)
index 0000000..c35e023
--- /dev/null
@@ -0,0 +1,90 @@
+############################################################################
+# Copyright (C) 2013-2015  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 sys
+import unittest
+sys.path.append('../..')
+from isc import *
+
+
+class PolicyTest(unittest.TestCase):
+    def test_keysize(self):
+        pol = policy.dnssec_policy()
+        pol.load('test-policies/01-keysize.pol')
+
+        p = pol.policy('good_rsa.test', novalidate=True)
+        self.assertEqual(p.get_name(), "good_rsa.test")
+        self.assertEqual(p.constructed(), False)
+        self.assertEqual(p.validate(), (True, ""))
+
+        p = pol.policy('good_dsa.test', novalidate=True)
+        self.assertEqual(p.get_name(), "good_dsa.test")
+        self.assertEqual(p.constructed(), False)
+        self.assertEqual(p.validate(), (True, ""))
+
+        p = pol.policy('bad_dsa.test', novalidate=True)
+        self.assertEqual(p.validate(),
+                         (False, 'ZSK key size 769 not divisible by 64 as required for DSA'))
+
+    def test_prepublish(self):
+        pol = policy.dnssec_policy()
+        pol.load('test-policies/02-prepublish.pol')
+        p = pol.policy('good_prepublish.test', novalidate=True)
+        self.assertEqual(p.validate(), (True, ""))
+
+        p = pol.policy('bad_prepublish.test', novalidate=True)
+        self.assertEqual(p.validate(),
+                         (False, 'KSK pre/post-publish periods '
+                                 '(10368000/5184000) combined exceed '
+                                 'rollover period 10368000'))
+
+    def test_postpublish(self):
+        pol = policy.dnssec_policy()
+        pol.load('test-policies/03-postpublish.pol')
+
+        p = pol.policy('good_postpublish.test', novalidate=True)
+        self.assertEqual(p.validate(), (True, ""))
+
+        p = pol.policy('bad_postpublish.test', novalidate=True)
+        self.assertEqual(p.validate(),
+                         (False, 'KSK pre/post-publish periods '
+                                 '(10368000/5184000) combined exceed '
+                                 'rollover period 10368000'))
+
+    def test_combined_pre_post(self):
+        pol = policy.dnssec_policy()
+        pol.load('test-policies/04-combined-pre-post.pol')
+
+        p = pol.policy('good_combined_pre_post_ksk.test', novalidate=True)
+        self.assertEqual(p.validate(), (True, ""))
+
+        p = pol.policy('bad_combined_pre_post_ksk.test', novalidate=True)
+        self.assertEqual(p.validate(),
+                         (False, 'KSK pre/post-publish periods '
+                                 '(5184000/5184000) combined exceed '
+                                 'rollover period 10368000'))
+
+        p = pol.policy('good_combined_pre_post_zsk.test', novalidate=True)
+        self.assertEqual(p.validate(),
+                         (True, ""))
+        p = pol.policy('bad_combined_pre_post_zsk.test', novalidate=True)
+        self.assertEqual(p.validate(),
+                         (False, 'ZSK pre/post-publish periods '
+                                 '(5184000/5184000) combined exceed '
+                                 'rollover period 7776000'))
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/bin/python/isc/tests/test-policies/01-keysize.pol b/bin/python/isc/tests/test-policies/01-keysize.pol
new file mode 100644 (file)
index 0000000..b54f1e3
--- /dev/null
@@ -0,0 +1,41 @@
+policy keysize_rsa {
+        algorithm rsasha1;
+        coverage 1y;
+        roll-period zsk 3mo;
+        pre-publish zsk 2w;
+        post-publish zsk 2w;
+        roll-period ksk 1y;
+        pre-publish ksk 1mo;
+        post-publish ksk 2mo;
+        keyttl 1h;
+        key-size ksk 2048;
+        key-size zsk 1024;
+};
+
+policy keysize_dsa {
+        algorithm dsa;
+        coverage 1y;
+        key-size ksk 2048;
+        key-size zsk 1024;
+};
+
+zone good_rsa.test {
+        policy keysize_rsa;
+};
+
+zone bad_rsa.test {
+        policy keysize_rsa;
+        key-size ksk 511;
+};
+
+zone good_dsa.test {
+        policy keysize_dsa;
+        key-size ksk 1024;
+        key-size zsk 768;
+};
+
+zone bad_dsa.test {
+        policy keysize_dsa;
+        key-size ksk 1024;
+        key-size zsk 769;
+};
diff --git a/bin/python/isc/tests/test-policies/02-prepublish.pol b/bin/python/isc/tests/test-policies/02-prepublish.pol
new file mode 100644 (file)
index 0000000..e4d11c2
--- /dev/null
@@ -0,0 +1,31 @@
+policy prepublish_rsa {
+        algorithm rsasha1;
+        coverage 1y;
+        roll-period zsk 3mo;
+        pre-publish zsk 2w;
+        post-publish zsk 2w;
+        roll-period ksk 1y;
+        pre-publish ksk 1mo;
+        post-publish ksk 2mo;
+        keyttl 1h;
+        key-size ksk 2048;
+        key-size zsk 1024;
+};
+
+// Policy that defines a pre-publish period lower than the rollover period
+zone good_prepublish.test {
+        policy prepublish_rsa;
+        coverage 6mo;
+        roll-period ksk 4mo;
+        pre-publish ksk 1mo;
+};
+
+// Policy that defines a pre-publish period equal to the rollover period
+zone bad_prepublish.test {
+        policy prepublish_rsa;
+        coverage 6mo;
+        roll-period ksk 4mo;
+        pre-publish ksk 4mo;
+};
+
+
diff --git a/bin/python/isc/tests/test-policies/03-postpublish.pol b/bin/python/isc/tests/test-policies/03-postpublish.pol
new file mode 100644 (file)
index 0000000..a4c3a99
--- /dev/null
@@ -0,0 +1,31 @@
+policy postpublish_rsa {
+        algorithm rsasha1;
+        coverage 1y;
+        roll-period zsk 3mo;
+        pre-publish zsk 2w;
+        post-publish zsk 2w;
+        roll-period ksk 1y;
+        pre-publish ksk 1mo;
+        post-publish ksk 2mo;
+        keyttl 1h;
+        key-size ksk 2048;
+        key-size zsk 1024;
+};
+
+// Policy that defines a post-publish period lower than the rollover period
+zone good_postpublish.test {
+        policy postpublish_rsa;
+        coverage 6mo;
+        roll-period ksk 4mo;
+        pre-publish ksk 1mo;
+};
+
+// Policy that defines a post-publish period equal to the rollover period
+zone bad_postpublish.test {
+        policy postpublish_rsa;
+        coverage 6mo;
+        roll-period ksk 4mo;
+        pre-publish ksk 4mo;
+};
+
+
diff --git a/bin/python/isc/tests/test-policies/04-combined-pre-post.pol b/bin/python/isc/tests/test-policies/04-combined-pre-post.pol
new file mode 100644 (file)
index 0000000..5695559
--- /dev/null
@@ -0,0 +1,55 @@
+policy combined_pre_post_rsa {
+        algorithm rsasha1;
+        coverage 1y;
+        roll-period zsk 3mo;
+        pre-publish zsk 2w;
+        post-publish zsk 2w;
+        roll-period ksk 1y;
+        pre-publish ksk 1mo;
+        post-publish ksk 2mo;
+        keyttl 1h;
+        key-size ksk 2048;
+        key-size zsk 1024;
+};
+
+// Policy that defines a combined pre-publish and post-publish period lower
+// than the rollover period
+zone good_combined_pre_post_ksk.test {
+        policy combined_pre_post_rsa;
+        coverage 6mo;
+        roll-period ksk 4mo;
+        pre-publish ksk 1mo;
+        post-publish ksk 1mo;
+};
+
+// Policy that defines a combined pre-publish and post-publish period higher
+// than the rollover period
+zone bad_combined_pre_post_ksk.test {
+        policy combined_pre_post_rsa;
+        coverage 6mo;
+        roll-period ksk 4mo;
+        pre-publish ksk 2mo;
+        post-publish ksk 2mo;
+};
+
+// Policy that defines a combined pre-publish and post-publish period lower
+// than the rollover period
+zone good_combined_pre_post_zsk.test {
+        policy combined_pre_post_rsa;
+        coverage 1y;
+        roll-period zsk 3mo;
+        pre-publish zsk 1mo;
+        post-publish zsk 1mo;
+};
+
+// Policy that defines a combined pre-publish and post-publish period higher
+// than the rollover period
+zone bad_combined_pre_post_zsk.test {
+        policy combined_pre_post_rsa;
+        coverage 1y;
+        roll-period zsk 3mo;
+        pre-publish zsk 2mo;
+        post-publish zsk 2mo;
+};
+
+
diff --git a/bin/python/isc/tests/testdata/Kexample.com.+007+35529.key b/bin/python/isc/tests/testdata/Kexample.com.+007+35529.key
new file mode 100644 (file)
index 0000000..c5afbe2
--- /dev/null
@@ -0,0 +1,8 @@
+; This is a key-signing key, keyid 35529, for example.com.
+; Created: 20151120214047 (Fri Nov 20 13:40:47 2015)
+; Publish: 20151021214154 (Wed Oct 21 14:41:54 2015)
+; Activate: 20151120214154 (Fri Nov 20 13:41:54 2015)
+; Revoke: 20161119214154 (Sat Nov 19 13:41:54 2016)
+; Inactive: 20171119214154 (Sun Nov 19 13:41:54 2017)
+; Delete: 20181119214154 (Mon Nov 19 13:41:54 2018)
+example.com. IN DNSKEY 257 3 7 AwEAAbbJK96tY8d4sF6RLxh9SVIhho5s2ZhrcijT5j1SNLECen7QLutj VJPEiG8UgBLaJSGkxPDxOygYv4hwh4JXBSj89o9rNabAJtCa9XzIXSpt /cfiCfvqmcOZb9nepmDCXsC7gn/gbae/4Y5ym9XOiCp8lu+tlFWgRiJ+ kxDGN48rRPrGfpq+SfwM9NUtftVa7B0EFVzDkADKedRj0SSGYOqH+WYH CnWjhPFmgJoAw3/m4slTHW1l+mDwFvsCMjXopg4JV0CNnTybnOmyuIwO LWRhB3q8ze24sYBU1fpE9VAMxZ++4Kqh/2MZFeDAs7iPPKSmI3wkRCW5 pkwDLO5lJ9c=
diff --git a/bin/python/isc/tests/testdata/Kexample.com.+007+35529.private b/bin/python/isc/tests/testdata/Kexample.com.+007+35529.private
new file mode 100644 (file)
index 0000000..af22c6a
--- /dev/null
@@ -0,0 +1,18 @@
+Private-key-format: v1.3
+Algorithm: 7 (NSEC3RSASHA1)
+Modulus: tskr3q1jx3iwXpEvGH1JUiGGjmzZmGtyKNPmPVI0sQJ6ftAu62NUk8SIbxSAEtolIaTE8PE7KBi/iHCHglcFKPz2j2s1psAm0Jr1fMhdKm39x+IJ++qZw5lv2d6mYMJewLuCf+Btp7/hjnKb1c6IKnyW762UVaBGIn6TEMY3jytE+sZ+mr5J/Az01S1+1VrsHQQVXMOQAMp51GPRJIZg6of5ZgcKdaOE8WaAmgDDf+biyVMdbWX6YPAW+wIyNeimDglXQI2dPJuc6bK4jA4tZGEHerzN7bixgFTV+kT1UAzFn77gqqH/YxkV4MCzuI88pKYjfCREJbmmTAMs7mUn1w==
+PublicExponent: AQAB
+PrivateExponent: jfiM6YU1Rd6Y5qrPsK7HP1Ko54DmNbvmzI1hfGmYYZAyQsNCXjQloix5aAW9QGdNhecrzJUhxJAMXFZC+lrKuD5a56R25JDE1Sw21nft3SHXhuQrqw5Z5hIMTWXhRrBR1lMOFnLj2PJxqCmenp+vJYjl1z20RBmbv/keE15SExFRJIJ3G0lI4V0KxprY5rgsT/vID0pS32f7rmXhgEzyWDyuxceTMidBooD5BSeEmSTYa4rvCVZ2vgnzIGSxjYDPJE2rGve2dpvdXQuujRFaf4+/FzjaOgg35rTtUmC9klfB4D6KJIfc1PNUwcH7V0VJ2fFlgZgMYi4W331QORl9sQ==
+Prime1: 479rW3EeoBwHhUKDy5YeyfnMKjhaosrcYhW4resevLzatFrvS/n2KxJnsHoEzmGr2A13naI61RndgVBBOwNDWI3/tQ+aKvcr+V9m4omROV3xYa8s1FsDbEW0Z6G0UheaqRFir8WK98/Lj6Zht1uBXHSPPf91OW0qj+b5gbX7TK8=
+Prime2: zXXlxgIq+Ih6kxsUw4Ith0nd/d2P3d42QYPjxYjsg4xYicPAjva9HltnbBQ2lr4JEG9Yyb8KalSnJUSuvXtn7bGfBzLu8W6omCeVWXQVH4NIu9AjpO16NpMKWGRfiHHbbSYJs1daTZKHC2FEmi18MKX/RauHGGOakFQ/3A/GMVk=
+Exponent1: 0o9UQ1uHNAIWFedUEHJ/jr7LOrGVYnLpZCmu7+S0K0zzatGz8ets44+FnAyDywdUKFDzKSMm/4SFXRwE4vl2VzYZlp2RLG4PEuRYK9OCF6a6F1UsvjxTItQjIbjIDSnTjMINGnMps0lDa1EpgKsyI3eEQ46eI3TBZ//k6D6G0vM=
+Exponent2: d+CYJgXRyJzo17fvT3s+0TbaHWsOq+chROyNEw4m4UIbzpW2XjO8eF/gYgERMLbEVyCAb4XVr+CgfXArfEbqhpciMHMZUyi7mbtOupiuUmqpH1v70Bj3O6xjVtuJmfTEkFSnSEppV+VsgclI26Q6V7Ai1yWTdzl2T0u4zs8tVlE=
+Coefficient: E4EYw76gIChdQDn6+Uh44/xH9Uwmvq3OETR8w/kEZ0xQ8AkTdKFKUp84nlR6gN+ljb2mUxERKrVLwnBsU8EbUlo9UccMbBGkkZ/8MyfGCBb9nUyOFtOxdHY2M0MQadesRptXHt/m30XjdohwmT7qfSIENwtgUOHbwFnn7WPMc/k=
+Created: 20151120214047
+Publish: 20151021214154
+Activate: 20151120214154
+Revoke: 20161119214154
+Inactive: 20171119214154
+Delete: 20181119214154
+SyncPublish: 20150921214154
+SyncDelete: 20151130214154
diff --git a/bin/python/isc/utils.py.in b/bin/python/isc/utils.py.in
new file mode 100644 (file)
index 0000000..48b9685
--- /dev/null
@@ -0,0 +1,57 @@
+############################################################################
+# Copyright (C) 2013-2015  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.
+############################################################################
+# utils.py
+# Grouping shared code in one place
+############################################################################
+
+import os
+
+# These routines permit platform-independent location of BIND 9 tools
+if os.name == 'nt':
+    import win32con
+    import win32api
+
+
+def prefix(bindir=''):
+    if os.name != 'nt':
+        return os.path.join('@prefix@', bindir)
+
+    bind_subkey = "Software\\ISC\\BIND"
+    h_key = None
+    key_found = True
+    try:
+        h_key = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey)
+    except:
+        key_found = False
+    if key_found:
+        try:
+            (named_base, _) = win32api.RegQueryValueEx(h_key, "InstallDir")
+        except:
+            key_found = False
+        win32api.RegCloseKey(h_key)
+    if key_found:
+        return os.path.join(named_base, bindir)
+    return os.path.join(win32api.GetSystemDirectory(), bindir)
+
+
+def shellquote(s):
+    if os.name == 'nt':
+        return '"' + s.replace('"', '"\\"') + '"'
+    return "'" + s.replace("'", "'\\''") + "'"
+
+
+version = '@BIND9_VERSION@'
+sysconfdir = '@expanded_sysconfdir@'
index cac8c29ac6e8dcc041bc1214467e3ffe9412bde2..3e924723d1e00d0ee764f95e97f6aa83653e9422 100644 (file)
@@ -46,6 +46,7 @@ DSFROMKEY=$TOP/bin/dnssec/dnssec-dsfromkey
 IMPORTKEY=$TOP/bin/dnssec/dnssec-importkey
 CHECKDS=$TOP/bin/python/dnssec-checkds
 COVERAGE=$TOP/bin/python/dnssec-coverage
+KEYMGR=$TOP/bin/python/dnssec-keymgr
 CHECKZONE=$TOP/bin/check/named-checkzone
 CHECKCONF=$TOP/bin/check/named-checkconf
 PK11GEN="$TOP/bin/pkcs11/pkcs11-keygen -q -s ${SLOT:-0} -p ${HSMPIN:-1234}"
@@ -71,7 +72,7 @@ SUBDIRS="acl additional allow_query addzone autosign builtin
         cookie @COVERAGE@ database digdelv dlv dlvauto dlz dlzexternal
         dname dns64 dnssec dsdigest dscp @DNSTAP@ dyndb ecdsa ednscompliance
         emptyzones fetchlimit filter-aaaa formerr forward geoip glue gost
-        ixfr inline legacy limits logfileconfig lwresd masterfile
+        ixfr inline @KEYMGR@ legacy limits logfileconfig lwresd masterfile
         masterformat metadata mkeys names notify nslookup nsupdate
         pending pipelined @PKCS11_TEST@ reclimit redirect resolver rndc
         rpz rpzrecurse rrl rrchecker rrsetorder rsabigexponent
index 58c65bd1611554c93d5368822c4fa25305f96c05..07bbff11d9a0cd21bc1262ed38bf6ab1d71f7151 100644 (file)
@@ -3,5 +3,6 @@ warn=1
 error=1
 ok=1
 retcode=1
-match="WARNING: Key .* (KSK) is scheduled for deletion before
+match="WARNING: Key .* (KSK) is scheduled for
+deletion before inactivation
 No KSK's are published"
index 4ffa0b663e5e09ceb1e40e8182fd9907bef9b8a5..450ec24bff01ec2ddd6c0d338a4b3e6c692e11cd 100644 (file)
@@ -3,5 +3,6 @@ warn=1
 error=1
 ok=1
 retcode=1
-match="WARNING: Key .* (ZSK) is scheduled for deletion before
+match="WARNING: Key .* (ZSK) is scheduled for
+deletion before inactivation
 No ZSK's are published"
index b6cf2d2c276892e7d100541e8a1c45c2c5149ef8..2edfa0e76ed60cabc67e608662a105eb30e1423d 100644 (file)
@@ -3,5 +3,6 @@ warn=1
 error=1
 ok=1
 retcode=1
-match="WARNING: Key .* (KSK) is scheduled for deletion before
+match="WARNING: Key .* (KSK) is scheduled for
+deletion before inactivation
 No KSK's are both active and published"
index ed2004d2684bc988f2c252a88ad7894d2a92ed2f..0ef5b151e6ca0fdbb706f1510448d2d92862ce2b 100644 (file)
@@ -3,5 +3,6 @@ warn=1
 error=1
 ok=1
 retcode=1
-match="WARNING: Key .* (ZSK) is scheduled for deletion before
+match="WARNING: Key .* (ZSK) is scheduled for
+deletion before inactivation
 No ZSK's are both active and published"
index 4cc3af21d93840b75e0a4aab2bbb166674c3c242..eade21a75fdefe10dbcd4ac7afe395c02d6a8a63 100644 (file)
@@ -3,5 +3,7 @@ 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."
+match="WARNING: Key .* (KSK) is activated too soon
+after publication
+Activation should be at least 7 days after
+publication."
index 0579769ab92f14e07e7210e19082096ec121269f..150c9cdc8fa45ebc9bfcf0a1198eda14131e4beb 100644 (file)
@@ -3,5 +3,7 @@ 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."
+match="WARNING: Key .* (ZSK) is activated too soon
+after publication
+Activation should be at least 7 days after
+publication."
diff --git a/bin/tests/system/keymgr/01-ksk-inactive/README b/bin/tests/system/keymgr/01-ksk-inactive/README
new file mode 100644 (file)
index 0000000..b807029
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes one KSK rollover.  The KSK is deactivated prior to
+its replacement being activated.
diff --git a/bin/tests/system/keymgr/01-ksk-inactive/expect b/bin/tests/system/keymgr/01-ksk-inactive/expect
new file mode 100644 (file)
index 0000000..b076310
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1h -m 2h example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/02-zsk-inactive/README b/bin/tests/system/keymgr/02-zsk-inactive/README
new file mode 100644 (file)
index 0000000..bf56562
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes one ZSK rollover.  The first ZSK is deactivated
+prior to its replacement being activated.
diff --git a/bin/tests/system/keymgr/02-zsk-inactive/expect b/bin/tests/system/keymgr/02-zsk-inactive/expect
new file mode 100644 (file)
index 0000000..b076310
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1h -m 2h example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/03-ksk-unpublished/README b/bin/tests/system/keymgr/03-ksk-unpublished/README
new file mode 100644 (file)
index 0000000..0581c67
--- /dev/null
@@ -0,0 +1,2 @@
+This set contains one KSK rollover.  The KSK is unpublished before its
+successor is published.
diff --git a/bin/tests/system/keymgr/03-ksk-unpublished/expect b/bin/tests/system/keymgr/03-ksk-unpublished/expect
new file mode 100644 (file)
index 0000000..b076310
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1h -m 2h example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/04-zsk-unpublished/README b/bin/tests/system/keymgr/04-zsk-unpublished/README
new file mode 100644 (file)
index 0000000..589490d
--- /dev/null
@@ -0,0 +1,2 @@
+This set contains one ZSK rollover.  The ZSK is unpublished before its
+successor is published.
diff --git a/bin/tests/system/keymgr/04-zsk-unpublished/expect b/bin/tests/system/keymgr/04-zsk-unpublished/expect
new file mode 100644 (file)
index 0000000..b076310
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1h -m 2h example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/05-ksk-unpub-active/README b/bin/tests/system/keymgr/05-ksk-unpub-active/README
new file mode 100644 (file)
index 0000000..026028c
--- /dev/null
@@ -0,0 +1,3 @@
+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.
diff --git a/bin/tests/system/keymgr/05-ksk-unpub-active/expect b/bin/tests/system/keymgr/05-ksk-unpub-active/expect
new file mode 100644 (file)
index 0000000..b076310
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1h -m 2h example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/06-zsk-unpub-active/README b/bin/tests/system/keymgr/06-zsk-unpub-active/README
new file mode 100644 (file)
index 0000000..026028c
--- /dev/null
@@ -0,0 +1,3 @@
+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.
diff --git a/bin/tests/system/keymgr/06-zsk-unpub-active/expect b/bin/tests/system/keymgr/06-zsk-unpub-active/expect
new file mode 100644 (file)
index 0000000..b076310
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1h -m 2h example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/07-ksk-ttl/README b/bin/tests/system/keymgr/07-ksk-ttl/README
new file mode 100644 (file)
index 0000000..8b9dc02
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
diff --git a/bin/tests/system/keymgr/07-ksk-ttl/expect b/bin/tests/system/keymgr/07-ksk-ttl/expect
new file mode 100644 (file)
index 0000000..de792a9
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/08-zsk-ttl/README b/bin/tests/system/keymgr/08-zsk-ttl/README
new file mode 100644 (file)
index 0000000..8b9dc02
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
diff --git a/bin/tests/system/keymgr/08-zsk-ttl/expect b/bin/tests/system/keymgr/08-zsk-ttl/expect
new file mode 100644 (file)
index 0000000..de792a9
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/09-no-keys/README b/bin/tests/system/keymgr/09-no-keys/README
new file mode 100644 (file)
index 0000000..6295fa3
--- /dev/null
@@ -0,0 +1 @@
+This directory has no key set, but one will be initialized by dnssec-keymgr.
diff --git a/bin/tests/system/keymgr/09-no-keys/expect b/bin/tests/system/keymgr/09-no-keys/expect
new file mode 100644 (file)
index 0000000..de792a9
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/10-change-roll/README b/bin/tests/system/keymgr/10-change-roll/README
new file mode 100644 (file)
index 0000000..26073c3
--- /dev/null
@@ -0,0 +1,3 @@
+This directory has a key set which is valid, but has a ZSK rollover period
+of only three months. It will be updated to have a ZSK rollover period of
+one year.
diff --git a/bin/tests/system/keymgr/10-change-roll/expect b/bin/tests/system/keymgr/10-change-roll/expect
new file mode 100644 (file)
index 0000000..de792a9
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/11-many-simul/README b/bin/tests/system/keymgr/11-many-simul/README
new file mode 100644 (file)
index 0000000..8b9dc02
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
diff --git a/bin/tests/system/keymgr/11-many-simul/expect b/bin/tests/system/keymgr/11-many-simul/expect
new file mode 100644 (file)
index 0000000..de792a9
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/12-many-active/README b/bin/tests/system/keymgr/12-many-active/README
new file mode 100644 (file)
index 0000000..8b9dc02
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
diff --git a/bin/tests/system/keymgr/12-many-active/expect b/bin/tests/system/keymgr/12-many-active/expect
new file mode 100644 (file)
index 0000000..f990a7a
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf -f example.com"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/13-noroll/README b/bin/tests/system/keymgr/13-noroll/README
new file mode 100644 (file)
index 0000000..8b9dc02
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
diff --git a/bin/tests/system/keymgr/13-noroll/expect b/bin/tests/system/keymgr/13-noroll/expect
new file mode 100644 (file)
index 0000000..40616e1
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-f -c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/14-wrongalg/README b/bin/tests/system/keymgr/14-wrongalg/README
new file mode 100644 (file)
index 0000000..8b9dc02
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
diff --git a/bin/tests/system/keymgr/14-wrongalg/expect b/bin/tests/system/keymgr/14-wrongalg/expect
new file mode 100644 (file)
index 0000000..436f05f
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=4
diff --git a/bin/tests/system/keymgr/15-unspec/README b/bin/tests/system/keymgr/15-unspec/README
new file mode 100644 (file)
index 0000000..8b9dc02
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
diff --git a/bin/tests/system/keymgr/15-unspec/expect b/bin/tests/system/keymgr/15-unspec/expect
new file mode 100644 (file)
index 0000000..b1ff4fc
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/16-wrongalg-unspec/README b/bin/tests/system/keymgr/16-wrongalg-unspec/README
new file mode 100644 (file)
index 0000000..8b9dc02
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
diff --git a/bin/tests/system/keymgr/16-wrongalg-unspec/expect b/bin/tests/system/keymgr/16-wrongalg-unspec/expect
new file mode 100644 (file)
index 0000000..7a21dec
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf"
+kmatch=""
+kret=0
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=4
diff --git a/bin/tests/system/keymgr/17-noforce/README b/bin/tests/system/keymgr/17-noforce/README
new file mode 100644 (file)
index 0000000..8b9dc02
--- /dev/null
@@ -0,0 +1,2 @@
+This set includes a KSK rollover, with insufficient delay between
+prepublication and rollover.
diff --git a/bin/tests/system/keymgr/17-noforce/expect b/bin/tests/system/keymgr/17-noforce/expect
new file mode 100644 (file)
index 0000000..a5bf1f1
--- /dev/null
@@ -0,0 +1,9 @@
+kargs="-c policy.conf example.com"
+kmatch=""
+kret=1
+cargs="-d 1w -m 2w example.com"
+cmatch=""
+cret=0
+warn=0
+error=0
+ok=2
diff --git a/bin/tests/system/keymgr/clean.sh b/bin/tests/system/keymgr/clean.sh
new file mode 100644 (file)
index 0000000..1f815b5
--- /dev/null
@@ -0,0 +1,20 @@
+#!/bin/sh
+#
+# Copyright (C) 2016  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.
+
+rm -f */K*.key
+rm -f */K*.private
+rm -f coverage.* keymgr.*
+rm -f policy.out
diff --git a/bin/tests/system/keymgr/policy.conf b/bin/tests/system/keymgr/policy.conf
new file mode 100644 (file)
index 0000000..e6b7d98
--- /dev/null
@@ -0,0 +1,10 @@
+policy default {
+        policy global;
+        algorithm nsec3rsasha1;
+        key-size zsk 1024;
+        pre-publish zsk 6w;
+        post-publish zsk 6w;
+        roll-period zsk 6mo;
+        roll-period ksk 0;
+        coverage 364d;
+};
diff --git a/bin/tests/system/keymgr/policy.good b/bin/tests/system/keymgr/policy.good
new file mode 100644 (file)
index 0000000..0038a27
--- /dev/null
@@ -0,0 +1,170 @@
+policy default:
+       inherits global
+       directory None
+       algorithm None
+       coverage None
+       ksk_keysize None
+       zsk_keysize None
+       ksk_rollperiod None
+       zsk_rollperiod None
+       ksk_prepublish None
+       ksk_postpublish None
+       zsk_prepublish None
+       zsk_postpublish None
+       ksk_standby None
+       zsk_standby None
+       keyttl None
+
+policy global:
+       inherits None
+       directory None
+       algorithm RSASHA256
+       coverage 15552000
+       ksk_keysize 2048
+       zsk_keysize 2048
+       ksk_rollperiod None
+       zsk_rollperiod 31536000
+       ksk_prepublish 2592000
+       ksk_postpublish 2592000
+       zsk_prepublish 2592000
+       zsk_postpublish 2592000
+       ksk_standby None
+       zsk_standby None
+       keyttl 3600
+
+constructed policy example.com:
+       inherits global
+       directory None
+       algorithm RSASHA256
+       coverage 15552000
+       ksk_keysize 2048
+       zsk_keysize 2048
+       ksk_rollperiod None
+       zsk_rollperiod 31536000
+       ksk_prepublish 2592000
+       ksk_postpublish 2592000
+       zsk_prepublish 2592000
+       zsk_postpublish 2592000
+       ksk_standby None
+       zsk_standby None
+       keyttl None
+
+policy default:
+       inherits None
+       directory "keydir"
+       algorithm RSASHA1
+       coverage 31536000
+       ksk_keysize None
+       zsk_keysize None
+       ksk_rollperiod None
+       zsk_rollperiod 15552000
+       ksk_prepublish None
+       ksk_postpublish None
+       zsk_prepublish 3628800
+       zsk_postpublish 3628800
+       ksk_standby None
+       zsk_standby None
+       keyttl 3600
+
+zone policy example.com:
+       inherits extra
+       directory "keydir"
+       algorithm NSEC3RSASHA1
+       coverage 12960000
+       ksk_keysize 2048
+       zsk_keysize 2048
+       ksk_rollperiod 31536000
+       zsk_rollperiod 7776000
+       ksk_prepublish 7776000
+       ksk_postpublish None
+       zsk_prepublish 3628800
+       zsk_postpublish 604800
+       ksk_standby None
+       zsk_standby None
+       keyttl None
+
+constructed policy example.org:
+       inherits None
+       directory "keydir"
+       algorithm RSASHA1
+       coverage 31536000
+       ksk_keysize 2048
+       zsk_keysize 1024
+       ksk_rollperiod None
+       zsk_rollperiod 15552000
+       ksk_prepublish None
+       ksk_postpublish None
+       zsk_prepublish 3628800
+       zsk_postpublish 3628800
+       ksk_standby None
+       zsk_standby None
+       keyttl 3600
+
+constructed policy example.net:
+       inherits None
+       directory "keydir"
+       algorithm RSASHA1
+       coverage 31536000
+       ksk_keysize 2048
+       zsk_keysize 1024
+       ksk_rollperiod None
+       zsk_rollperiod 15552000
+       ksk_prepublish None
+       ksk_postpublish None
+       zsk_prepublish 3628800
+       zsk_postpublish 3628800
+       ksk_standby None
+       zsk_standby None
+       keyttl 3600
+
+algorithm policy RSASHA1:
+       inherits None
+       directory None
+       algorithm None
+       coverage None
+       ksk_keysize 2048
+       zsk_keysize 1024
+       ksk_rollperiod None
+       zsk_rollperiod None
+       ksk_prepublish None
+       ksk_postpublish None
+       zsk_prepublish None
+       zsk_postpublish None
+       ksk_standby None
+       zsk_standby None
+       keyttl None
+
+algorithm policy DSA:
+       inherits None
+       directory None
+       algorithm DSA
+       coverage None
+       ksk_keysize 1024
+       zsk_keysize 2048
+       ksk_rollperiod None
+       zsk_rollperiod None
+       ksk_prepublish None
+       ksk_postpublish None
+       zsk_prepublish None
+       zsk_postpublish None
+       ksk_standby None
+       zsk_standby None
+       keyttl None
+
+policy extra:
+       inherits default
+       directory None
+       algorithm None
+       coverage 157680000
+       ksk_keysize None
+       zsk_keysize None
+       ksk_rollperiod 31536000
+       zsk_rollperiod 7776000
+       ksk_prepublish 7776000
+       ksk_postpublish None
+       zsk_prepublish None
+       zsk_postpublish 604800
+       ksk_standby None
+       zsk_standby None
+       keyttl 7200
+
diff --git a/bin/tests/system/keymgr/policy.sample b/bin/tests/system/keymgr/policy.sample
new file mode 100644 (file)
index 0000000..d96a40d
--- /dev/null
@@ -0,0 +1,40 @@
+# a comment which should be skipped
+
+algorithm-policy rsasha1 {
+        key-size ksk 2048;
+        key-size zsk 1024;     // this too
+};
+
+// and this
+
+policy default {
+        directory "keydir";
+        algorithm rsasha1;
+        coverage 1y;            # another comment
+        roll-period zsk 6mo;    // and yet another
+        pre-publish zsk 6w;
+        post-publish zsk 6w;
+        keyttl 1h;
+};
+
+policy extra {
+        policy default;
+        coverage 5y;
+        roll-period KSK 1 year;
+        roll-period zsk 3mo;
+        pre-publish ksk 3mo;
+        post-publish zsk 1w;
+        keyttl 2h;
+};
+
+/*
+ * and this is also a comment,
+ * and it should be ignored like
+ * the others.
+ */
+
+zone example.com {
+        policy extra;
+        coverage 5 mon;
+        algorithm nsec3rsasha1;
+};
diff --git a/bin/tests/system/keymgr/prereq.sh b/bin/tests/system/keymgr/prereq.sh
new file mode 100644 (file)
index 0000000..fcf3a63
--- /dev/null
@@ -0,0 +1,20 @@
+#!/bin/sh
+#
+# Copyright (C) 2016  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
+
+exec $SHELL ../testcrypto.sh
diff --git a/bin/tests/system/keymgr/setup.sh b/bin/tests/system/keymgr/setup.sh
new file mode 100644 (file)
index 0000000..f35c6c1
--- /dev/null
@@ -0,0 +1,212 @@
+#!/bin/sh
+#
+# Copyright (C) 2016  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 $RANDFILE"
+
+$SHELL clean.sh
+
+# Test 1: KSK goes inactive before successor is active
+dir=01-ksk-inactive
+echo I:set up $dir
+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
+echo I:set up $dir
+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
+echo I:set up $dir
+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
+echo I:set up $dir
+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
+echo I:set up $dir
+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
+echo I:set up $dir
+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
+echo I:set up $dir
+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 -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
+echo I:set up $dir
+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`
+
+# Test 9: No special preparation needed
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+
+# Test 10: Valid key set, but rollover period has changed
+dir=10-change-roll
+echo I:set up $dir
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+ksk1=`$KEYGEN -K $dir -3fk example.com`
+zsk1=`$KEYGEN -K $dir -3 example.com`
+$SETTIME -K $dir -I +3mo -D +4mo $zsk1 > /dev/null 2>&1
+zsk2=`$KEYGEN -K $dir -S $zsk1`
+
+# Test 11: Many keys all simultaneously scheduled to be active in the future
+dir=11-many-simul
+echo I:set up $dir
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+k1=`$KEYGEN -K $dir -q3fk -P now+1mo -A now+1mo example.com`
+z1=`$KEYGEN -K $dir -q3 -P now+1mo -A now+1mo example.com`
+z2=`$KEYGEN -K $dir -q3 -P now+1mo -A now+1mo example.com`
+z3=`$KEYGEN -K $dir -q3 -P now+1mo -A now+1mo example.com`
+z4=`$KEYGEN -K $dir -q3 -P now+1mo -A now+1mo example.com`
+
+# Test 12: Many keys all simultaneously scheduled to be active in the past
+dir=12-many-active
+echo I:set up $dir
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+k1=`$KEYGEN -K $dir -q3fk example.com`
+z1=`$KEYGEN -K $dir -q3 example.com`
+z2=`$KEYGEN -K $dir -q3 example.com`
+z3=`$KEYGEN -K $dir -q3 example.com`
+z4=`$KEYGEN -K $dir -q3 example.com`
+
+# Test 13: Multiple simultaneous keys with no configured roll period
+dir=13-noroll
+echo I:set up $dir
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+k1=`$KEYGEN -K $dir -q3fk example.com`
+k2=`$KEYGEN -K $dir -q3fk example.com`
+k3=`$KEYGEN -K $dir -q3fk example.com`
+z1=`$KEYGEN -K $dir -q3 example.com`
+
+# Test 14: Keys exist but have the wrong algorithm
+dir=14-wrongalg
+echo I:set up $dir
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+k1=`$KEYGEN -K $dir -qfk example.com`
+z1=`$KEYGEN -K $dir -q example.com`
+$SETTIME -K $dir -I now+6mo -D now+8mo $z1 > /dev/null
+z2=`$KEYGEN -K $dir -q -S ${z1}.key`
+$SETTIME -K $dir -I now+1y -D now+14mo $z2 > /dev/null
+z3=`$KEYGEN -K $dir -q -S ${z2}.key`
+$SETTIME -K $dir -I now+18mo -D now+20mo $z3 > /dev/null
+z4=`$KEYGEN -K $dir -q -S ${z3}.key`
+
+# Test 15: No zones specified; just search the directory for keys
+dir=15-unspec
+echo I:set up $dir
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+k1=`$KEYGEN -K $dir -q3fk example.com`
+z1=`$KEYGEN -K $dir -q3 example.com`
+$SETTIME -K $dir -I now+6mo -D now+8mo $z1 > /dev/null
+z2=`$KEYGEN -K $dir -q -S ${z1}.key`
+$SETTIME -K $dir -I now+1y -D now+14mo $z2 > /dev/null
+z3=`$KEYGEN -K $dir -q -S ${z2}.key`
+$SETTIME -K $dir -I now+18mo -D now+20mo $z3 > /dev/null
+z4=`$KEYGEN -K $dir -q -S ${z3}.key`
+
+# Test 16: No zones specified; search the directory for keys;
+# keys have the wrong algorithm for their policies
+dir=16-wrongalg-unspec
+echo I:set up $dir
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+k1=`$KEYGEN -K $dir -qfk example.com`
+z1=`$KEYGEN -K $dir -q example.com`
+$SETTIME -K $dir -I now+6mo -D now+8mo $z1 > /dev/null
+z2=`$KEYGEN -K $dir -q -S ${z1}.key`
+$SETTIME -K $dir -I now+1y -D now+14mo $z2 > /dev/null
+z3=`$KEYGEN -K $dir -q -S ${z2}.key`
+$SETTIME -K $dir -I now+18mo -D now+20mo $z3 > /dev/null
+z4=`$KEYGEN -K $dir -q -S ${z3}.key`
+
+# Test 17: Keys are simultaneously active but we run with no force
+# flag (this should fail)
+dir=17-noforce
+echo I:set up $dir
+rm -f $dir/K*.key
+rm -f $dir/K*.private
+k1=`$KEYGEN -K $dir -q3fk example.com`
+z1=`$KEYGEN -K $dir -q3 example.com`
+z2=`$KEYGEN -K $dir -q3 example.com`
+z3=`$KEYGEN -K $dir -q3 example.com`
+z4=`$KEYGEN -K $dir -q3 example.com`
diff --git a/bin/tests/system/keymgr/testpolicy.py b/bin/tests/system/keymgr/testpolicy.py
new file mode 100644 (file)
index 0000000..2dec7ff
--- /dev/null
@@ -0,0 +1,29 @@
+#!/bin/python
+import sys
+sys.path.insert(0, '../../../python')
+from isc import *
+
+pp = policy.dnssec_policy()
+# print the unmodified default and a generated zone policy
+print pp.named_policy['default']
+print pp.named_policy['global']
+print pp.policy('example.com')
+
+if len(sys.argv) > 0:
+    for policy_file in sys.argv[1:]:
+        pp.load(policy_file)
+
+        # now print the modified default and generated zone policies
+        print pp.named_policy['default']
+        print pp.policy('example.com')
+        print pp.policy('example.org')
+        print pp.policy('example.net')
+
+        # print algorithm policies
+        print pp.alg_policy['RSASHA1']
+        print pp.alg_policy['DSA']
+
+        # print another named policy
+        print pp.named_policy['extra']
+else:
+    print("ERROR: Please provide an input file")
diff --git a/bin/tests/system/keymgr/tests.sh b/bin/tests/system/keymgr/tests.sh
new file mode 100644 (file)
index 0000000..f598f0a
--- /dev/null
@@ -0,0 +1,106 @@
+#!/bin/sh
+#
+# Copyright (C) 2016  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
+
+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 ($n)"
+        kargs= cargs= kmatch= cmatch= kret= cret=0 warn= error= ok=
+        . $dir/expect
+
+        # run keymgr to update keys
+        $KEYMGR -K $dir -s $SETTIME $kargs > keymgr.$n 2>&1
+        # check that return code matches expectations
+        found=$?
+        if [ $found -ne $kret ]; then
+            echo "keymgr retcode was $found expected $kret"
+            ret=1
+        fi
+
+        found=`matchall keymgr.$n "$kmatch"`
+        if [ "$found" = "FAIL" ]; then
+            echo "no match on '$kmatch'"
+            ret=1
+        fi
+
+        # now check coverage
+        $COVERAGE -K $dir $cargs > coverage.$n 2>&1
+        # check that return code matches expectations
+        found=$?
+        if [ $found -ne $cret ]; then
+            echo "coverage retcode was $found expected $cret"
+            ret=1
+        fi
+
+        # check for correct number of errors
+        found=`grep ERROR coverage.$n | wc -l`
+        if [ $found -ne $error ]; then
+            echo "error count was $found expected $error"
+            ret=1
+        fi
+
+        # check for correct number of warnings
+        found=`grep WARNING coverage.$n | wc -l`
+        if [ $found -ne $warn ]; then
+            echo "warning count was $found expected $warn"
+            ret=1
+        fi
+
+        # check for correct number of OKs
+        found=`grep "No errors found" coverage.$n | wc -l`
+        if [ $found -ne $ok ]; then
+            echo "good count was $found expected $ok"
+            ret=1
+        fi
+
+        found=`matchall coverage.$n "$cmatch"`
+        if [ "$found" = "FAIL" ]; then
+            echo "no match on '$cmatch'"
+            ret=1
+        fi
+
+        n=`expr $n + 1`
+        if [ $ret != 0 ]; then echo "I:failed"; fi
+        status=`expr $status + $ret`
+done
+
+echo "I:checking policy.conf parser ($n)"
+ret=0
+${PYTHON} testpolicy.py policy.sample > policy.out
+cmp -s policy.good policy.out || ret=1
+if [ $ret != 0 ]; then echo "I:failed"; fi
+status=`expr $status + $ret`
+n=`expr $n + 1`
+
+echo "I:exit status: $status"
+exit $status
index 02fc7a36faf9991137eb60e79fbc0894728bb0f6..36097c774e6d3443a020360e4049f2639eebb537 100755 (executable)
--- a/configure
+++ b/configure
@@ -875,7 +875,9 @@ ISC_PLATFORM_NORETURN_POST
 ISC_PLATFORM_NORETURN_PRE
 ISC_PLATFORM_HAVELONGLONG
 ISC_SOCKADDR_LEN_T
+expanded_sysconfdir
 PYTHON_TOOLS
+KEYMGR
 COVERAGE
 CHECKDS
 PYTHON
 
 
 python="python python3 python3.4 python3.3 python3.2 python3.1 python3.0 python2 python2.7 python2.6 python2.5 python2.4"
-testscript='try: import argparse
+
+testargparse='try: import argparse
+except: exit(1)'
+
+testply='try: from ply import *
 except: exit(1)'
+
 case "$use_python" in
        no)
                { $as_echo "$as_me:${as_lineno-$LINENO}: checking for python support" >&5
@@ -11869,9 +11876,22 @@ done
                                fi
                                { $as_echo "$as_me:${as_lineno-$LINENO}: checking python module 'argparse'" >&5
 $as_echo_n "checking python module 'argparse'... " >&6; }
-                               if ${PYTHON:-false} -c "$testscript"; then
-                                       { $as_echo "$as_me:${as_lineno-$LINENO}: result: found, using $PYTHON" >&5
-$as_echo "found, using $PYTHON" >&6; }
+                               if ${PYTHON:-false} -c "$testargparse"; then
+                                       { $as_echo "$as_me:${as_lineno-$LINENO}: result: found" >&5
+$as_echo "found" >&6; }
+                                else
+                                        { $as_echo "$as_me:${as_lineno-$LINENO}: result: not found" >&5
+$as_echo "not found" >&6; }
+                                        unset ac_cv_path_PYTHON
+                                        unset PYTHON
+                                        continue
+                               fi
+
+                               { $as_echo "$as_me:${as_lineno-$LINENO}: checking python module 'ply'" >&5
+$as_echo_n "checking python module 'ply'... " >&6; }
+                               if ${PYTHON:-false} -c "$testply"; then
+                                       { $as_echo "$as_me:${as_lineno-$LINENO}: result: found" >&5
+$as_echo "found" >&6; }
                                        break
                                fi
                                { $as_echo "$as_me:${as_lineno-$LINENO}: result: not found" >&5
@@ -11949,7 +11969,16 @@ done
                        esac
                        { $as_echo "$as_me:${as_lineno-$LINENO}: checking python module 'argparse'" >&5
 $as_echo_n "checking python module 'argparse'... " >&6; }
-                       if ${PYTHON:-false} -c "$testscript"; then
+                       if ${PYTHON:-false} -c "$testargparse"; then
+                               { $as_echo "$as_me:${as_lineno-$LINENO}: result: found, using $PYTHON" >&5
+$as_echo "found, using $PYTHON" >&6; }
+                               break
+                       else
+                               as_fn_error $? "not found" "$LINENO" 5
+                       fi
+                       { $as_echo "$as_me:${as_lineno-$LINENO}: checking python module 'ply'" >&5
+$as_echo_n "checking python module 'ply'... " >&6; }
+                       if ${PYTHON:-false} -c "$testply"; then
                                { $as_echo "$as_me:${as_lineno-$LINENO}: result: found, using $PYTHON" >&5
 $as_echo "found, using $PYTHON" >&6; }
                                break
@@ -11964,15 +11993,18 @@ esac
 PYTHON_TOOLS=''
 CHECKDS=''
 COVERAGE=''
+KEYMGR=''
 if test "X$PYTHON" != "X"; then
        PYTHON_TOOLS=python
        CHECKDS=checkds
        COVERAGE=coverage
+       KEYMGR=keymgr
 fi
 
 
 
 
+
 #
 # Special processing of paths depending on whether --prefix,
 # --sysconfdir or --localstatedir arguments were given.  What's
@@ -12007,6 +12039,8 @@ case "$prefix" in
                esac
                ;;
 esac
+expanded_sysconfdir=`eval echo $sysconfdir`
+
 
 #
 # Make sure INSTALL uses an absolute path, else it will be wrong in all
@@ -22666,7 +22700,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/delv/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/pkcs11/Makefile bin/tests/pkcs11/benchmarks/Makefile bin/tests/rbt/Makefile bin/tests/resolver/Makefile bin/tests/sockaddr/Makefile bin/tests/system/Makefile bin/tests/system/builtin/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/dyndb/Makefile bin/tests/system/dyndb/driver/Makefile bin/tests/system/filter-aaaa/Makefile bin/tests/system/geoip/Makefile bin/tests/system/inline/checkdsa.sh bin/tests/system/lwresd/Makefile bin/tests/system/pipelined/Makefile bin/tests/system/resolver/Makefile bin/tests/system/rndc/Makefile bin/tests/system/rpz/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/statistics/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/scripts/check-secure-delegation.pl contrib/scripts/zone-edit.sh doc/Makefile doc/arm/Makefile doc/arm/noteversion.xml doc/arm/pkgversion.xml doc/arm/releaseinfo.xml doc/doxygen/Doxyfile doc/doxygen/Makefile doc/doxygen/doxygen-input-filter doc/misc/Makefile doc/tex/Makefile doc/tex/armstyle.sty doc/xsl/Makefile doc/xsl/isc-docbook-chunk.xsl doc/xsl/isc-docbook-html.xsl doc/xsl/isc-manpage.xsl doc/xsl/isc-notes-html.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/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/include/pk11/Makefile lib/isc/include/pkcs11/Makefile lib/isc/tests/Makefile lib/isc/nls/Makefile lib/isc/unix/Makefile lib/isc/unix/include/Makefile lib/isc/unix/include/isc/Makefile lib/isc/unix/include/pkcs11/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/tests/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 lib/samples/Makefile lib/samples/Makefile-postinstall 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/delv/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/isc/Makefile bin/python/isc/utils.py bin/python/isc/tests/Makefile bin/python/dnssec-checkds.py bin/python/dnssec-coverage.py bin/python/dnssec-keymgr.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/pkcs11/Makefile bin/tests/pkcs11/benchmarks/Makefile bin/tests/rbt/Makefile bin/tests/resolver/Makefile bin/tests/sockaddr/Makefile bin/tests/system/Makefile bin/tests/system/builtin/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/dyndb/Makefile bin/tests/system/dyndb/driver/Makefile bin/tests/system/filter-aaaa/Makefile bin/tests/system/geoip/Makefile bin/tests/system/inline/checkdsa.sh bin/tests/system/lwresd/Makefile bin/tests/system/pipelined/Makefile bin/tests/system/resolver/Makefile bin/tests/system/rndc/Makefile bin/tests/system/rpz/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/statistics/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/scripts/check-secure-delegation.pl contrib/scripts/zone-edit.sh doc/Makefile doc/arm/Makefile doc/arm/noteversion.xml doc/arm/pkgversion.xml doc/arm/releaseinfo.xml doc/doxygen/Doxyfile doc/doxygen/Makefile doc/doxygen/doxygen-input-filter doc/misc/Makefile doc/tex/Makefile doc/tex/armstyle.sty doc/xsl/Makefile doc/xsl/isc-docbook-chunk.xsl doc/xsl/isc-docbook-html.xsl doc/xsl/isc-manpage.xsl doc/xsl/isc-notes-html.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/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/include/pk11/Makefile lib/isc/include/pkcs11/Makefile lib/isc/tests/Makefile lib/isc/nls/Makefile lib/isc/unix/Makefile lib/isc/unix/include/Makefile lib/isc/unix/include/isc/Makefile lib/isc/unix/include/pkcs11/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/tests/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 lib/samples/Makefile lib/samples/Makefile-postinstall unit/Makefile unit/unittest.sh"
 
 
 #
     "bin/nsupdate/Makefile") CONFIG_FILES="$CONFIG_FILES bin/nsupdate/Makefile" ;;
     "bin/pkcs11/Makefile") CONFIG_FILES="$CONFIG_FILES bin/pkcs11/Makefile" ;;
     "bin/python/Makefile") CONFIG_FILES="$CONFIG_FILES bin/python/Makefile" ;;
+    "bin/python/isc/Makefile") CONFIG_FILES="$CONFIG_FILES bin/python/isc/Makefile" ;;
+    "bin/python/isc/utils.py") CONFIG_FILES="$CONFIG_FILES bin/python/isc/utils.py" ;;
+    "bin/python/isc/tests/Makefile") CONFIG_FILES="$CONFIG_FILES bin/python/isc/tests/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/python/dnssec-keymgr.py") CONFIG_FILES="$CONFIG_FILES bin/python/dnssec-keymgr.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 4cfce95a0e1443854a5a10bf195781108309f2e6..413fd26d17529a66c950b45bcf8e930b23f0432b 100644 (file)
@@ -223,8 +223,13 @@ AC_ARG_WITH(python,
     use_python="$withval", use_python="unspec")
 
 python="python python3 python3.4 python3.3 python3.2 python3.1 python3.0 python2 python2.7 python2.6 python2.5 python2.4"
-testscript='try: import argparse
+
+testargparse='try: import argparse
+except: exit(1)'
+
+testply='try: from ply import *
 except: exit(1)'
+
 case "$use_python" in
        no)
                AC_MSG_CHECKING([for python support])
@@ -240,8 +245,18 @@ case "$use_python" in
                                        continue;
                                fi
                                AC_MSG_CHECKING([python module 'argparse'])
-                               if ${PYTHON:-false} -c "$testscript"; then
-                                       AC_MSG_RESULT([found, using $PYTHON])
+                               if ${PYTHON:-false} -c "$testargparse"; then
+                                       AC_MSG_RESULT([found])
+                                else
+                                        AC_MSG_RESULT([not found])
+                                        unset ac_cv_path_PYTHON
+                                        unset PYTHON
+                                        continue
+                               fi
+
+                               AC_MSG_CHECKING([python module 'ply'])
+                               if ${PYTHON:-false} -c "$testply"; then
+                                       AC_MSG_RESULT([found])
                                        break
                                fi
                                AC_MSG_RESULT([not found])
@@ -271,7 +286,14 @@ case "$use_python" in
                                ;;
                        esac
                        AC_MSG_CHECKING([python module 'argparse'])
-                       if ${PYTHON:-false} -c "$testscript"; then
+                       if ${PYTHON:-false} -c "$testargparse"; then
+                               AC_MSG_RESULT([found, using $PYTHON])
+                               break
+                       else
+                               AC_MSG_ERROR([not found])
+                       fi
+                       AC_MSG_CHECKING([python module 'ply'])
+                       if ${PYTHON:-false} -c "$testply"; then
                                AC_MSG_RESULT([found, using $PYTHON])
                                break
                        else
@@ -285,13 +307,16 @@ esac
 PYTHON_TOOLS=''
 CHECKDS=''
 COVERAGE=''
+KEYMGR=''
 if test "X$PYTHON" != "X"; then
        PYTHON_TOOLS=python
        CHECKDS=checkds
        COVERAGE=coverage
+       KEYMGR=keymgr
 fi
 AC_SUBST(CHECKDS)
 AC_SUBST(COVERAGE)
+AC_SUBST(KEYMGR)
 AC_SUBST(PYTHON_TOOLS)
 
 #
@@ -328,6 +353,8 @@ case "$prefix" in
                esac
                ;;
 esac
+expanded_sysconfdir=`eval echo $sysconfdir`
+AC_SUBST(expanded_sysconfdir)
 
 #
 # Make sure INSTALL uses an absolute path, else it will be wrong in all
@@ -4843,8 +4870,12 @@ AC_CONFIG_FILES([
        bin/nsupdate/Makefile
        bin/pkcs11/Makefile
        bin/python/Makefile
+       bin/python/isc/Makefile
+       bin/python/isc/utils.py
+       bin/python/isc/tests/Makefile
        bin/python/dnssec-checkds.py
        bin/python/dnssec-coverage.py
+       bin/python/dnssec-keymgr.py
        bin/rndc/Makefile
        bin/tests/Makefile
        bin/tests/atomic/Makefile
diff --git a/contrib/kasp/README b/contrib/kasp/README
new file mode 100644 (file)
index 0000000..fb897f1
--- /dev/null
@@ -0,0 +1,11 @@
+This directory is for tools and scripts related to the OpenDNSSEC KASP
+("key and signature policy") format. Currently it only contains
+"kasp2policy.py", a python script for converting KASP key policy
+to the "dnssec.policy" format that is used by dnssec-keymgr.
+
+This depends on PLY (python lex/yacc) and on the "isc.dnskey" module in
+bin/python/isc.
+
+Basic test:
+$ python kasp2policy.py kasp.xml > policy.out
+$ diff policy.out policy.good 
diff --git a/contrib/kasp/kasp.xml b/contrib/kasp/kasp.xml
new file mode 100644 (file)
index 0000000..d94b084
--- /dev/null
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- Sample KASP file to use for testing kasp2policy.py. -->
+<KASP>
+       <Policy name="Policy1">
+         <Description>A default policy that will
+               amaze you and your friends</Description>
+               <Signatures>
+                       <Resign>PT5M</Resign>
+                       <Refresh>PT5M</Refresh>
+                       <Validity>
+                                       <Default>PT15M</Default>
+                                       <Denial>PT15M</Denial>
+                       </Validity>
+                       <Jitter>PT2M</Jitter>
+                       <InceptionOffset>PT1M</InceptionOffset>
+               </Signatures>
+
+               <Denial>
+                       <NSEC>
+                       </NSEC>
+               </Denial>
+
+               <Keys>
+                       <!-- Parameters for both KSK and ZSK -->
+                       <TTL>PT1M</TTL>
+                       <RetireSafety>PT0S</RetireSafety>
+                       <PublishSafety>PT0S</PublishSafety>
+
+                       <!-- Parameters for KSK only -->
+                       <KSK>
+                               <Algorithm length="2048">5</Algorithm>
+                               <Lifetime>PT40M</Lifetime>
+                               <Repository>softHSM</Repository>
+                               <Standby>1</Standby>
+                       </KSK>
+
+                       <!-- Parameters for ZSK only -->
+                       <ZSK>
+                               <Algorithm length="2048">5</Algorithm>
+                               <Lifetime>PT25M</Lifetime>
+                               <Repository>softHSM</Repository>
+                               <Standby>1</Standby>
+                       </ZSK>
+               </Keys>
+
+               <Zone>
+                       <PropagationDelay>PT0S</PropagationDelay>
+                       <SOA>
+                               <TTL>PT0S</TTL>
+                               <Minimum>PT0S</Minimum>
+                               <Serial>unixtime</Serial>
+                       </SOA>
+               </Zone>
+
+               <Parent>
+                       <PropagationDelay>PT8M</PropagationDelay>
+                       <DS>
+                               <TTL>PT0S</TTL>
+                       </DS>
+                       <SOA>
+                               <TTL>PT0S</TTL>
+                               <Minimum>PT0S</Minimum>
+                       </SOA>
+               </Parent>
+       </Policy>
+       <Policy name="Policy2">
+               <Description>A default policy that will amaze you and your friends</Description>
+               <Signatures>
+                       <Resign>PT7M</Resign>
+                       <Refresh>PT7M</Refresh>
+                       <Validity>
+                                       <Default>PT15M</Default>
+                                       <Denial>PT16M</Denial>
+                       </Validity>
+                       <Jitter>PT2M</Jitter>
+                       <InceptionOffset>PT1M</InceptionOffset>
+               </Signatures>
+
+               <Denial>
+                       <NSEC3>
+                               <Resalt>P120D</Resalt>
+                               <Hash>
+                                       <Algorithm>1</Algorithm>
+                                       <Iterations>5</Iterations>
+                                       <Salt length="8"/>
+                               </Hash>
+                       </NSEC3>
+               </Denial>
+
+               <Keys>
+                       <!-- Parameters for both KSK and ZSK -->
+                       <TTL>PT15M</TTL>
+                       <RetireSafety>PT0S</RetireSafety>
+                       <PublishSafety>PT0S</PublishSafety>
+
+                       <!-- Parameters for KSK only -->
+                       <KSK>
+                               <Algorithm length="2048">7</Algorithm>
+                               <Lifetime>PT45M</Lifetime>
+                               <Repository>softHSM</Repository>
+                               <Standby>1</Standby>
+                       </KSK>
+
+                       <!-- Parameters for ZSK only -->
+                       <ZSK>
+                               <Algorithm length="2048">7</Algorithm>
+                               <Lifetime>PT25M</Lifetime>
+                               <Repository>softHSM</Repository>
+                               <Standby>1</Standby>
+                       </ZSK>
+               </Keys>
+
+               <Zone>
+                       <PropagationDelay>PT0S</PropagationDelay>
+                       <SOA>
+                               <TTL>PT0S</TTL>
+                               <Minimum>PT0S</Minimum>
+                               <Serial>unixtime</Serial>
+                       </SOA>
+               </Zone>
+
+               <Parent>
+                       <PropagationDelay>PT12M</PropagationDelay>
+                       <DS>
+                               <TTL>PT0S</TTL>
+                       </DS>
+                       <SOA>
+                               <TTL>PT0S</TTL>
+                               <Minimum>PT0S</Minimum>
+                       </SOA>
+               </Parent>
+       </Policy>
+</KASP>
diff --git a/contrib/kasp/kasp2policy.py b/contrib/kasp/kasp2policy.py
new file mode 100644 (file)
index 0000000..b78a968
--- /dev/null
@@ -0,0 +1,209 @@
+#!/usr/bin/python
+############################################################################
+# Copyright (C) 2015  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.
+############################################################################
+# kasp2policy.py
+# This translates the Keys section of a KASP XML file into a dnssec.policy
+# file that can be used by dnssec-keymgr.
+############################################################################
+
+from xml.etree import cElementTree as ET
+from collections import defaultdict
+from isc import dnskey
+import ply.yacc as yacc
+import ply.lex as lex
+import re
+
+############################################################################
+# Translate KASP duration values into seconds
+############################################################################
+class kasptime:
+    class ktlex:
+        tokens = ( 'P', 'T', 'Y', 'M', 'D', 'H', 'S', 'NUM' )
+
+        t_P = r'(?i)P'
+        t_T = r'(?i)T'
+        t_Y = r'(?i)Y'
+        t_M = r'(?i)M'
+        t_D = r'(?i)D'
+        t_H = r'(?i)H'
+        t_S = r'(?i)S'
+
+        def t_NUM(self, t):
+            r'\d+'
+            t.value = int(t.value)
+            return t
+
+        def t_error(self, t):
+            print("Illegal character '%s'" % t.value[0])
+            t.lexer.skip(1)
+
+        def __init__(self):
+            self.lexer = lex.lex(object=self)
+
+    def __init__(self):
+        self.lexer = self.ktlex()
+        self.tokens = self.lexer.tokens
+        self.parser = yacc.yacc(debug=False, write_tables=False, module=self)
+
+    def parse(self, text):
+        self.lexer.lexer.lineno = 0
+        return self.parser.parse(text)
+
+    def p_ktime_4(self, p):
+        "ktime : P periods T times"
+        p[0] = p[2] + p[4]
+
+    def p_ktime_3(self, p):
+        "ktime : P T times"
+        p[0] = p[3]
+
+    def p_ktime_2(self, p):
+        "ktime : P periods"
+        p[0] = p[2]
+
+    def p_periods_1(self, p):
+        "periods : period"
+        p[0] = p[1]
+
+    def p_periods_2(self, p):
+        "periods : periods period"
+        p[0] = p[1] + p[2]
+
+    def p_times_1(self, p):
+        "times : time"
+        p[0] = p[1]
+
+    def p_times_2(self, p):
+        "times : times time"
+        p[0] = p[1] + p[2]
+
+    def p_period(self, p):
+        '''period : NUM Y
+                  | NUM M
+                  | NUM D'''
+        if p[2].lower() == 'y':
+            p[0] = int(p[1]) * 31536000
+        elif p[2].lower() == 'm':
+            p[0] = int(p[1]) * 2592000
+        elif p[2].lower() == 'd':
+            p[0] += int(p[1]) * 86400
+
+    def p_time(self, p):
+        '''time : NUM H
+                | NUM M
+                | NUM S'''
+        if p[2].lower() == 'h':
+            p[0] = int(p[1]) * 3600
+        elif p[2].lower() == 'm':
+            p[0] = int(p[1]) * 60
+        elif p[2].lower() == 's':
+            p[0] = int(p[1])
+
+    def p_error(self, p):
+        print("Syntax error")
+
+############################################################################
+# Load the contents of a KASP XML file as a python dictionary
+############################################################################
+class kasp():
+    @staticmethod
+    def _todict(t):
+        d = {t.tag: {} if t.attrib else None}
+        children = list(t)
+        if children:
+            dd = defaultdict(list)
+            for dc in map(kasp._todict, children):
+                for k, v in dc.iteritems():
+                    dd[k].append(v)
+            d = {t.tag:
+                    {k:v[0] if len(v) == 1 else v for k, v in dd.iteritems()}}
+        if t.attrib:
+            d[t.tag].update(('@' + k, v) for k, v in t.attrib.iteritems())
+        if t.text:
+            text = t.text.strip()
+            if children or t.attrib:
+                if text:
+                    d[t.tag]['#text'] = text
+            else:
+                d[t.tag] = text
+        return d
+
+    def __init__(self, filename):
+        self._dict = kasp._todict(ET.parse(filename).getroot())
+
+    def __getitem__(self, key):
+        return self._dict[key]
+
+    def __len__(self):
+        return len(self._dict)
+
+    def __iter__(self):
+        return self._dict.__iter__()
+
+    def __repr__(self):
+        return repr(self._dict)
+
+############################################################################
+# Load the contents of a KASP XML file as a python dictionary
+############################################################################
+if __name__ == "__main__":
+    from pprint import *
+    import sys
+
+    if len(sys.argv) < 2:
+        print("Usage: kasp2policy <filename>")
+        exit(1)
+
+    try:
+        kinfo = kasp(sys.argv[1])
+    except:
+        print("%s: unable to load KASP file '%s'" % (sys.argv[0], sys.argv[1]))
+        exit(1)
+
+    kt = kasptime()
+    first = True
+
+    for p in kinfo['KASP']['Policy']:
+        if not p['@name'] or not p['Keys']: continue
+        if not first:
+            print("")
+        first = False
+        if p['Description']:
+            d = p['Description'].strip()
+            print("# %s" % re.sub(r"\n\s*", "\n# ", d))
+        print("policy %s {" % p['@name'])
+        ksk = p['Keys']['KSK']
+        zsk = p['Keys']['ZSK']
+        kalg = ksk['Algorithm']
+        zalg = zsk['Algorithm']
+        algnum = kalg['#text'] or zalg['#text']
+        if algnum:
+            print("\talgorithm %s;" % dnskey.algstr(int(algnum)))
+        if p['Keys']['TTL']:
+            print("\tkeyttl %d;" % kt.parse(p['Keys']['TTL']))
+        if kalg['@length']:
+            print("\tkey-size ksk %d;" % int(kalg['@length']))
+        if zalg['@length']:
+            print("\tkey-size zsk %d;" % int(zalg['@length']))
+        if ksk['Lifetime']:
+            print("\troll-period ksk %d;" % kt.parse(ksk['Lifetime']))
+        if zsk['Lifetime']:
+            print("\troll-period zsk %d;" % kt.parse(zsk['Lifetime']))
+        if ksk['Standby']:
+            print("\tstandby ksk %d;" % int(ksk['Standby']))
+        if zsk['Standby']:
+            print("\tstandby zsk %d;" % int(zsk['Standby']))
+        print("};")
diff --git a/contrib/kasp/policy.good b/contrib/kasp/policy.good
new file mode 100644 (file)
index 0000000..18c6360
--- /dev/null
@@ -0,0 +1,24 @@
+# A default policy that will
+# amaze you and your friends
+policy Policy1 {
+       algorithm RSASHA1;
+       keyttl 60;
+       key-size ksk 2048;
+       key-size zsk 2048;
+       roll-period ksk 2400;
+       roll-period zsk 1500;
+       standby ksk 1;
+       standby zsk 1;
+};
+
+# A default policy that will amaze you and your friends
+policy Policy2 {
+       algorithm NSEC3RSASHA1;
+       keyttl 900;
+       key-size ksk 2048;
+       key-size zsk 2048;
+       roll-period ksk 2700;
+       roll-period zsk 1500;
+       standby ksk 1;
+       standby zsk 1;
+};
index 001e0e0c4d91d6cc688a7c83424b4fa3f946722f..07776b0fafce6847423f58087af088c7e8e5258e 100644 (file)
@@ -1,6 +1,7 @@
 <!DOCTYPE book [
 <!ENTITY Scaron "&#x160;">
 <!ENTITY ccaron "&#x10D;">
+<!ENTITY aacute "&#x0E1;">
 <!ENTITY mdash "&#8212;">
 <!ENTITY ouml "&#xf6;">]>
 <!--
          <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://localhost:8888/json/v1/traffic">http://localhost:8888/json/v1/traffic</link>.
        </para>
       </listitem>
+      <listitem>
+       <para>
+         A new DNSSEC key management utility,
+         <command>dnssec-keymgr</command>, has been added. This tool
+         is meant to run unattended (e.g., under <command>cron</command>).
+         It reads a policy definition file
+         (default: <filename>/etc/dnssec.policy</command>)
+         and creates or updates DNSSEC keys as necessary to ensure that a
+         zone's keys match the defined policy for that zone.  New keys are
+         created whenever necessary to ensure rollovers occur correctly.
+         Existing keys' timing metadata is adjusted as needed to set the
+         correct rollover period, prepublication interval, etc.  If
+         the configured policy changes, keys are corrected automatically.
+         See the <command>dnssec-keymgr</command> man page for full details.
+       </para>
+       <para>
+         Note: <command>dnssec-keymgr</command> depends on Python and on
+         the Python lex/yacc module, PLY. The other Python-based tools,
+         <command>dnssec-coverage</command> and
+         <command>dnssec-checkds</command>, have been
+         refactored and updated as part of this work.
+       </para>
+       <para>
+         (Many thanks to Sebasti&aacute;n
+         Castro for his assistance in developing this tool at the IETF
+         95 Hackathon in Buenos Aires, April 2016.)
+       </para>
+      </listitem>
       <listitem>
        <para>
          The serial number of a dynamically updatable zone can