Blob Blame History Raw
From 923ea26e87ba7c8b8c818dc6c996604fc709b05a Mon Sep 17 00:00:00 2001
From: Evan Hunt <each@isc.org>
Date: Thu, 28 Apr 2016 00:12:33 -0700
Subject: [PATCH] dnssec-keymgr

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]

Adapt keymgr and coverage tests to v9.9
---
 bin/dnssec/dnssec-settime.c                        |   7 +-
 bin/python/.gitignore                              |   7 +
 bin/python/Makefile.in                             |  20 +-
 bin/python/dnssec-keymgr.docbook                   | 354 ++++++++++
 bin/python/dnssec-keymgr.py.in                     |  27 +
 bin/python/isc/.gitignore                          |   3 +
 bin/python/isc/Makefile.in                         |  67 ++
 bin/python/isc/__init__.py                         |  25 +
 bin/python/isc/checkds.py                          | 189 ++++++
 bin/python/isc/coverage.py                         | 292 +++++++++
 bin/python/isc/dnskey.py                           | 504 +++++++++++++++
 bin/python/isc/eventlist.py                        | 171 +++++
 bin/python/isc/keydict.py                          |  89 +++
 bin/python/isc/keyevent.py                         |  81 +++
 bin/python/isc/keymgr.py                           | 152 +++++
 bin/python/isc/keyseries.py                        | 194 ++++++
 bin/python/isc/keyzone.py                          |  60 ++
 bin/python/isc/policy.py                           | 690 ++++++++++++++++++++
 bin/python/isc/tests/Makefile.in                   |  33 +
 bin/python/isc/tests/dnskey_test.py                |  57 ++
 bin/python/isc/tests/policy_test.py                |  90 +++
 bin/python/isc/tests/test-policies/01-keysize.pol  |  41 ++
 .../isc/tests/test-policies/02-prepublish.pol      |  31 +
 .../isc/tests/test-policies/03-postpublish.pol     |  31 +
 .../tests/test-policies/04-combined-pre-post.pol   |  55 ++
 .../isc/tests/testdata/Kexample.com.+007+35529.key |   8 +
 .../tests/testdata/Kexample.com.+007+35529.private |  18 +
 bin/python/isc/utils.py.in                         |  57 ++
 bin/tests/system/conf.sh.in                        |   9 +-
 bin/tests/system/keymgr/01-ksk-inactive/README     |   2 +
 bin/tests/system/keymgr/01-ksk-inactive/expect     |   9 +
 bin/tests/system/keymgr/02-zsk-inactive/README     |   2 +
 bin/tests/system/keymgr/02-zsk-inactive/expect     |   9 +
 bin/tests/system/keymgr/03-ksk-unpublished/README  |   2 +
 bin/tests/system/keymgr/03-ksk-unpublished/expect  |   9 +
 bin/tests/system/keymgr/04-zsk-unpublished/README  |   2 +
 bin/tests/system/keymgr/04-zsk-unpublished/expect  |   9 +
 bin/tests/system/keymgr/05-ksk-unpub-active/README |   3 +
 bin/tests/system/keymgr/05-ksk-unpub-active/expect |   9 +
 bin/tests/system/keymgr/06-zsk-unpub-active/README |   3 +
 bin/tests/system/keymgr/06-zsk-unpub-active/expect |   9 +
 bin/tests/system/keymgr/07-ksk-ttl/README          |   2 +
 bin/tests/system/keymgr/07-ksk-ttl/expect          |   9 +
 bin/tests/system/keymgr/08-zsk-ttl/README          |   2 +
 bin/tests/system/keymgr/08-zsk-ttl/expect          |   9 +
 bin/tests/system/keymgr/09-no-keys/README          |   1 +
 bin/tests/system/keymgr/09-no-keys/expect          |   9 +
 bin/tests/system/keymgr/10-change-roll/README      |   3 +
 bin/tests/system/keymgr/10-change-roll/expect      |   9 +
 bin/tests/system/keymgr/11-many-simul/README       |   2 +
 bin/tests/system/keymgr/11-many-simul/expect       |   9 +
 bin/tests/system/keymgr/12-many-active/README      |   2 +
 bin/tests/system/keymgr/12-many-active/expect      |   9 +
 bin/tests/system/keymgr/13-noroll/README           |   2 +
 bin/tests/system/keymgr/13-noroll/expect           |   9 +
 bin/tests/system/keymgr/14-wrongalg/README         |   2 +
 bin/tests/system/keymgr/14-wrongalg/expect         |   9 +
 bin/tests/system/keymgr/15-unspec/README           |   2 +
 bin/tests/system/keymgr/15-unspec/expect           |   9 +
 bin/tests/system/keymgr/16-wrongalg-unspec/README  |   2 +
 bin/tests/system/keymgr/16-wrongalg-unspec/expect  |   9 +
 bin/tests/system/keymgr/17-noforce/README          |   2 +
 bin/tests/system/keymgr/17-noforce/expect          |   9 +
 bin/tests/system/keymgr/clean.sh                   |  21 +
 bin/tests/system/keymgr/policy.conf                |  10 +
 bin/tests/system/keymgr/policy.good                | 170 +++++
 bin/tests/system/keymgr/policy.sample              |  40 ++
 bin/tests/system/keymgr/prereq.sh                  |  30 +
 bin/tests/system/keymgr/setup.sh                   | 214 ++++++
 bin/tests/system/keymgr/testpolicy.py              |  29 +
 bin/tests/system/keymgr/tests.sh                   | 106 +++
 configure                                          |  11 +
 configure.in                                       |   9 +
 contrib/kasp/README                                |  11 +
 contrib/kasp/kasp.xml                              | 134 ++++
 contrib/kasp/kasp2policy.py                        | 209 ++++++
 contrib/kasp/policy.good                           |  24 +
 doc/arm/notes.xml                                  | 714 +++++++++++++++++++++
 78 files changed, 5271 insertions(+), 12 deletions(-)
 create mode 100644 bin/python/.gitignore
 create mode 100644 bin/python/dnssec-keymgr.docbook
 create mode 100644 bin/python/dnssec-keymgr.py.in
 create mode 100644 bin/python/isc/.gitignore
 create mode 100644 bin/python/isc/Makefile.in
 create mode 100644 bin/python/isc/__init__.py
 create mode 100644 bin/python/isc/checkds.py
 create mode 100644 bin/python/isc/coverage.py
 create mode 100644 bin/python/isc/dnskey.py
 create mode 100644 bin/python/isc/eventlist.py
 create mode 100644 bin/python/isc/keydict.py
 create mode 100644 bin/python/isc/keyevent.py
 create mode 100644 bin/python/isc/keymgr.py
 create mode 100644 bin/python/isc/keyseries.py
 create mode 100644 bin/python/isc/keyzone.py
 create mode 100644 bin/python/isc/policy.py
 create mode 100644 bin/python/isc/tests/Makefile.in
 create mode 100644 bin/python/isc/tests/dnskey_test.py
 create mode 100644 bin/python/isc/tests/policy_test.py
 create mode 100644 bin/python/isc/tests/test-policies/01-keysize.pol
 create mode 100644 bin/python/isc/tests/test-policies/02-prepublish.pol
 create mode 100644 bin/python/isc/tests/test-policies/03-postpublish.pol
 create mode 100644 bin/python/isc/tests/test-policies/04-combined-pre-post.pol
 create mode 100644 bin/python/isc/tests/testdata/Kexample.com.+007+35529.key
 create mode 100644 bin/python/isc/tests/testdata/Kexample.com.+007+35529.private
 create mode 100644 bin/python/isc/utils.py.in
 create mode 100644 bin/tests/system/keymgr/01-ksk-inactive/README
 create mode 100644 bin/tests/system/keymgr/01-ksk-inactive/expect
 create mode 100644 bin/tests/system/keymgr/02-zsk-inactive/README
 create mode 100644 bin/tests/system/keymgr/02-zsk-inactive/expect
 create mode 100644 bin/tests/system/keymgr/03-ksk-unpublished/README
 create mode 100644 bin/tests/system/keymgr/03-ksk-unpublished/expect
 create mode 100644 bin/tests/system/keymgr/04-zsk-unpublished/README
 create mode 100644 bin/tests/system/keymgr/04-zsk-unpublished/expect
 create mode 100644 bin/tests/system/keymgr/05-ksk-unpub-active/README
 create mode 100644 bin/tests/system/keymgr/05-ksk-unpub-active/expect
 create mode 100644 bin/tests/system/keymgr/06-zsk-unpub-active/README
 create mode 100644 bin/tests/system/keymgr/06-zsk-unpub-active/expect
 create mode 100644 bin/tests/system/keymgr/07-ksk-ttl/README
 create mode 100644 bin/tests/system/keymgr/07-ksk-ttl/expect
 create mode 100644 bin/tests/system/keymgr/08-zsk-ttl/README
 create mode 100644 bin/tests/system/keymgr/08-zsk-ttl/expect
 create mode 100644 bin/tests/system/keymgr/09-no-keys/README
 create mode 100644 bin/tests/system/keymgr/09-no-keys/expect
 create mode 100644 bin/tests/system/keymgr/10-change-roll/README
 create mode 100644 bin/tests/system/keymgr/10-change-roll/expect
 create mode 100644 bin/tests/system/keymgr/11-many-simul/README
 create mode 100644 bin/tests/system/keymgr/11-many-simul/expect
 create mode 100644 bin/tests/system/keymgr/12-many-active/README
 create mode 100644 bin/tests/system/keymgr/12-many-active/expect
 create mode 100644 bin/tests/system/keymgr/13-noroll/README
 create mode 100644 bin/tests/system/keymgr/13-noroll/expect
 create mode 100644 bin/tests/system/keymgr/14-wrongalg/README
 create mode 100644 bin/tests/system/keymgr/14-wrongalg/expect
 create mode 100644 bin/tests/system/keymgr/15-unspec/README
 create mode 100644 bin/tests/system/keymgr/15-unspec/expect
 create mode 100644 bin/tests/system/keymgr/16-wrongalg-unspec/README
 create mode 100644 bin/tests/system/keymgr/16-wrongalg-unspec/expect
 create mode 100644 bin/tests/system/keymgr/17-noforce/README
 create mode 100644 bin/tests/system/keymgr/17-noforce/expect
 create mode 100644 bin/tests/system/keymgr/clean.sh
 create mode 100644 bin/tests/system/keymgr/policy.conf
 create mode 100644 bin/tests/system/keymgr/policy.good
 create mode 100644 bin/tests/system/keymgr/policy.sample
 create mode 100644 bin/tests/system/keymgr/prereq.sh
 create mode 100644 bin/tests/system/keymgr/setup.sh
 create mode 100644 bin/tests/system/keymgr/testpolicy.py
 create mode 100644 bin/tests/system/keymgr/tests.sh
 create mode 100644 contrib/kasp/README
 create mode 100644 contrib/kasp/kasp.xml
 create mode 100644 contrib/kasp/kasp2policy.py
 create mode 100644 contrib/kasp/policy.good
 create mode 100644 doc/arm/notes.xml

diff --git a/bin/dnssec/dnssec-settime.c b/bin/dnssec/dnssec-settime.c
index c71cac7..71c1ac5 100644
--- a/bin/dnssec/dnssec-settime.c
+++ b/bin/dnssec/dnssec-settime.c
@@ -492,11 +492,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",
diff --git a/bin/python/.gitignore b/bin/python/.gitignore
new file mode 100644
index 0000000..2e6963d
--- /dev/null
+++ b/bin/python/.gitignore
@@ -0,0 +1,7 @@
+dnssec-checkds
+dnssec-checkds.py
+dnssec-coverage
+dnssec-coverage.py
+dnssec-keymgr
+dnssec-keymgr.py
+*.pyc
diff --git a/bin/python/Makefile.in b/bin/python/Makefile.in
index 12695ed..1e4af9c 100644
--- a/bin/python/Makefile.in
+++ b/bin/python/Makefile.in
@@ -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
-SRCS =		dnssec-checkds.py dnssec-coverage.py
+TARGETS =	dnssec-checkds dnssec-coverage dnssec-keymgr
+SRCS =		dnssec-checkds.py dnssec-coverage.py dnssec-keymgr.py
+
+SUBDIRS	=	isc
 
-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@
@@ -35,6 +35,10 @@ dnssec-checkds: dnssec-checkds.py
 
 dnssec-coverage: dnssec-coverage.py
 
+dnssec-keymgr: dnssec-keymgr.py
+	cp -f dnssec-keymgr.py dnssec-keymgr
+	chmod +x dnssec-keymgr
+
 doc man:: ${MANOBJS}
 
 docclean manclean maintainer-clean::
@@ -47,11 +51,13 @@ installdirs:
 install:: ${TARGETS} installdirs
 	${INSTALL_PROGRAM} dnssec-checkds@EXEEXT@ ${DESTDIR}${sbindir}
 	${INSTALL_PROGRAM} dnssec-coverage@EXEEXT@ ${DESTDIR}${sbindir}
+	${INSTALL_PROGRAM} dnssec-keymgr@EXEEXT@ ${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
diff --git a/bin/python/dnssec-keymgr.docbook b/bin/python/dnssec-keymgr.docbook
new file mode 100644
index 0000000..2cccb49
--- /dev/null
+++ b/bin/python/dnssec-keymgr.docbook
@@ -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
index 0000000..23d563d
--- /dev/null
+++ b/bin/python/dnssec-keymgr.py.in
@@ -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
index 0000000..84554b8
--- /dev/null
+++ b/bin/python/isc/.gitignore
@@ -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
index 0000000..425d054
--- /dev/null
+++ b/bin/python/isc/Makefile.in
@@ -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
index 0000000..0d79f35
--- /dev/null
+++ b/bin/python/isc/__init__.py
@@ -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
index 0000000..64ca12e
--- /dev/null
+++ b/bin/python/isc/checkds.py
@@ -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
index 0000000..c9e8959
--- /dev/null
+++ b/bin/python/isc/coverage.py
@@ -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
index 0000000..f1559e7
--- /dev/null
+++ b/bin/python/isc/dnskey.py
@@ -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
index 0000000..4c91368
--- /dev/null
+++ b/bin/python/isc/eventlist.py
@@ -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
index 0000000..cc73dc4
--- /dev/null
+++ b/bin/python/isc/keydict.py
@@ -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
index 0000000..9025fee
--- /dev/null
+++ b/bin/python/isc/keyevent.py
@@ -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
index 0000000..a3a9043
--- /dev/null
+++ b/bin/python/isc/keymgr.py
@@ -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
index 0000000..ed09f71
--- /dev/null
+++ b/bin/python/isc/keyseries.py
@@ -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
index 0000000..7dfb31a
--- /dev/null
+++ b/bin/python/isc/keyzone.py
@@ -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
index 0000000..ed106c6
--- /dev/null
+++ b/bin/python/isc/policy.py
@@ -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
index 0000000..506f2cc
--- /dev/null
+++ b/bin/python/isc/tests/Makefile.in
@@ -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
index 0000000..2a63695
--- /dev/null
+++ b/bin/python/isc/tests/dnskey_test.py
@@ -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
index 0000000..c35e023
--- /dev/null
+++ b/bin/python/isc/tests/policy_test.py
@@ -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
index 0000000..b54f1e3
--- /dev/null
+++ b/bin/python/isc/tests/test-policies/01-keysize.pol
@@ -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
index 0000000..e4d11c2
--- /dev/null
+++ b/bin/python/isc/tests/test-policies/02-prepublish.pol
@@ -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
index 0000000..a4c3a99
--- /dev/null
+++ b/bin/python/isc/tests/test-policies/03-postpublish.pol
@@ -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
index 0000000..5695559
--- /dev/null
+++ b/bin/python/isc/tests/test-policies/04-combined-pre-post.pol
@@ -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
index 0000000..c5afbe2
--- /dev/null
+++ b/bin/python/isc/tests/testdata/Kexample.com.+007+35529.key
@@ -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
index 0000000..af22c6a
--- /dev/null
+++ b/bin/python/isc/tests/testdata/Kexample.com.+007+35529.private
@@ -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
index 0000000..48b9685
--- /dev/null
+++ b/bin/python/isc/utils.py.in
@@ -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@'
diff --git a/bin/tests/system/conf.sh.in b/bin/tests/system/conf.sh.in
index 2bd42f9..930928b 100644
--- a/bin/tests/system/conf.sh.in
+++ b/bin/tests/system/conf.sh.in
@@ -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}"
@@ -60,7 +61,7 @@ SAMPLE=$TOP/lib/export/samples/sample
 # load on the machine to make it unusable to other users.
 # v6synth
 SUBDIRS="acl additional allow_query addzone autosign builtin
-	 cacheclean checkconf @CHECKDS@ checknames checkzone @COVERAGE@
+	 cacheclean checkconf @CHECKDS@ checknames checkzone @COVERAGE@ @KEYMGR@
          database digdelv dlv dlvauto dlz dlzexternal dname dns64 dnssec dyndb
          ecdsa formerr forward glue gost ixfr inline limits logfileconfig
          lwresd masterfile masterformat metadata notify nsupdate pending
@@ -70,6 +71,10 @@ SUBDIRS="acl additional allow_query addzone autosign builtin
 
 # PERL will be an empty string if no perl interpreter was found.
 PERL=@PERL@
+
+# PYTHON will be an empty string if no python interpreter was found.
+PYTHON=@PYTHON@
+
 if test -n "$PERL"
 then
 	if $PERL -e "use IO::Socket::INET6;" 2> /dev/null
@@ -83,5 +88,5 @@ else
 fi
 
 export NAMED LWRESD DIG NSUPDATE KEYGEN KEYFRLAB SIGNER KEYSIGNER KEYSETTOOL \
-       PERL SUBDIRS RNDC CHECKZONE PK11GEN PK11LIST PK11DEL TESTSOCK6 \
+       PERL PYTHON SUBDIRS RNDC CHECKZONE PK11GEN PK11LIST PK11DEL TESTSOCK6 \
        JOURNALPRINT ARPANAME SAMPLE
diff --git a/bin/tests/system/keymgr/01-ksk-inactive/README b/bin/tests/system/keymgr/01-ksk-inactive/README
new file mode 100644
index 0000000..b807029
--- /dev/null
+++ b/bin/tests/system/keymgr/01-ksk-inactive/README
@@ -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
index 0000000..b076310
--- /dev/null
+++ b/bin/tests/system/keymgr/01-ksk-inactive/expect
@@ -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
index 0000000..bf56562
--- /dev/null
+++ b/bin/tests/system/keymgr/02-zsk-inactive/README
@@ -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
index 0000000..b076310
--- /dev/null
+++ b/bin/tests/system/keymgr/02-zsk-inactive/expect
@@ -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
index 0000000..0581c67
--- /dev/null
+++ b/bin/tests/system/keymgr/03-ksk-unpublished/README
@@ -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
index 0000000..b076310
--- /dev/null
+++ b/bin/tests/system/keymgr/03-ksk-unpublished/expect
@@ -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
index 0000000..589490d
--- /dev/null
+++ b/bin/tests/system/keymgr/04-zsk-unpublished/README
@@ -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
index 0000000..b076310
--- /dev/null
+++ b/bin/tests/system/keymgr/04-zsk-unpublished/expect
@@ -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
index 0000000..026028c
--- /dev/null
+++ b/bin/tests/system/keymgr/05-ksk-unpub-active/README
@@ -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
index 0000000..b076310
--- /dev/null
+++ b/bin/tests/system/keymgr/05-ksk-unpub-active/expect
@@ -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
index 0000000..026028c
--- /dev/null
+++ b/bin/tests/system/keymgr/06-zsk-unpub-active/README
@@ -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
index 0000000..b076310
--- /dev/null
+++ b/bin/tests/system/keymgr/06-zsk-unpub-active/expect
@@ -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
index 0000000..8b9dc02
--- /dev/null
+++ b/bin/tests/system/keymgr/07-ksk-ttl/README
@@ -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
index 0000000..de792a9
--- /dev/null
+++ b/bin/tests/system/keymgr/07-ksk-ttl/expect
@@ -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
index 0000000..8b9dc02
--- /dev/null
+++ b/bin/tests/system/keymgr/08-zsk-ttl/README
@@ -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
index 0000000..de792a9
--- /dev/null
+++ b/bin/tests/system/keymgr/08-zsk-ttl/expect
@@ -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
index 0000000..6295fa3
--- /dev/null
+++ b/bin/tests/system/keymgr/09-no-keys/README
@@ -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
index 0000000..de792a9
--- /dev/null
+++ b/bin/tests/system/keymgr/09-no-keys/expect
@@ -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
index 0000000..26073c3
--- /dev/null
+++ b/bin/tests/system/keymgr/10-change-roll/README
@@ -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
index 0000000..de792a9
--- /dev/null
+++ b/bin/tests/system/keymgr/10-change-roll/expect
@@ -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
index 0000000..8b9dc02
--- /dev/null
+++ b/bin/tests/system/keymgr/11-many-simul/README
@@ -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
index 0000000..de792a9
--- /dev/null
+++ b/bin/tests/system/keymgr/11-many-simul/expect
@@ -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
index 0000000..8b9dc02
--- /dev/null
+++ b/bin/tests/system/keymgr/12-many-active/README
@@ -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
index 0000000..f990a7a
--- /dev/null
+++ b/bin/tests/system/keymgr/12-many-active/expect
@@ -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
index 0000000..8b9dc02
--- /dev/null
+++ b/bin/tests/system/keymgr/13-noroll/README
@@ -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
index 0000000..40616e1
--- /dev/null
+++ b/bin/tests/system/keymgr/13-noroll/expect
@@ -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
index 0000000..8b9dc02
--- /dev/null
+++ b/bin/tests/system/keymgr/14-wrongalg/README
@@ -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
index 0000000..436f05f
--- /dev/null
+++ b/bin/tests/system/keymgr/14-wrongalg/expect
@@ -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
index 0000000..8b9dc02
--- /dev/null
+++ b/bin/tests/system/keymgr/15-unspec/README
@@ -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
index 0000000..b1ff4fc
--- /dev/null
+++ b/bin/tests/system/keymgr/15-unspec/expect
@@ -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
index 0000000..8b9dc02
--- /dev/null
+++ b/bin/tests/system/keymgr/16-wrongalg-unspec/README
@@ -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
index 0000000..7a21dec
--- /dev/null
+++ b/bin/tests/system/keymgr/16-wrongalg-unspec/expect
@@ -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
index 0000000..8b9dc02
--- /dev/null
+++ b/bin/tests/system/keymgr/17-noforce/README
@@ -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
index 0000000..a5bf1f1
--- /dev/null
+++ b/bin/tests/system/keymgr/17-noforce/expect
@@ -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
index 0000000..66d3d08
--- /dev/null
+++ b/bin/tests/system/keymgr/clean.sh
@@ -0,0 +1,21 @@
+#!/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
+rm -f random.data
diff --git a/bin/tests/system/keymgr/policy.conf b/bin/tests/system/keymgr/policy.conf
new file mode 100644
index 0000000..e6b7d98
--- /dev/null
+++ b/bin/tests/system/keymgr/policy.conf
@@ -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
index 0000000..0038a27
--- /dev/null
+++ b/bin/tests/system/keymgr/policy.good
@@ -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
index 0000000..d96a40d
--- /dev/null
+++ b/bin/tests/system/keymgr/policy.sample
@@ -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
index 0000000..be2546e
--- /dev/null
+++ b/bin/tests/system/keymgr/prereq.sh
@@ -0,0 +1,30 @@
+#!/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
+
+../../../tools/genrandom 400 random.data
+
+if $KEYGEN -q -a RSAMD5 -b 512 -n zone -r random.data foo > /dev/null 2>&1
+then
+    rm -f Kfoo*
+else
+    echo "I:This test requires cryptography" >&2
+    echo "I:--with-openssl, or --with-pkcs11 and --enable-native-pkcs11" >&2
+    exit 1
+fi
+#exec $SHELL ../testcrypto.sh
diff --git a/bin/tests/system/keymgr/setup.sh b/bin/tests/system/keymgr/setup.sh
new file mode 100644
index 0000000..0483f51
--- /dev/null
+++ b/bin/tests/system/keymgr/setup.sh
@@ -0,0 +1,214 @@
+#!/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
+
+RANDFILE=random.data
+KEYGEN="$KEYGEN -qr $RANDFILE"
+
+$SHELL clean.sh
+../../../tools/genrandom 400 $RANDFILE
+
+# 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
index 0000000..2dec7ff
--- /dev/null
+++ b/bin/tests/system/keymgr/testpolicy.py
@@ -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
index 0000000..f598f0a
--- /dev/null
+++ b/bin/tests/system/keymgr/tests.sh
@@ -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
diff --git a/configure b/configure
index 31c518a..a299aac 100755
--- a/configure
+++ b/configure
@@ -1372,7 +1372,9 @@ ISC_PLATFORM_NORETURN_POST
 ISC_PLATFORM_NORETURN_PRE
 ISC_PLATFORM_HAVELONGLONG
 ISC_SOCKADDR_LEN_T
+expanded_sysconfdir
 PYTHON_TOOLS
+KEYMGR
 COVERAGE
 CHECKDS
 PYTHON
@@ -12270,15 +12272,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
@@ -12313,6 +12318,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
@@ -22273,8 +22280,12 @@ do
     "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/sdb_tools/Makefile") CONFIG_FILES="$CONFIG_FILES bin/sdb_tools/Makefile" ;;
     "bin/tests/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/Makefile" ;;
diff --git a/configure.in b/configure.in
index 529989d..fb2e53e 100644
--- a/configure.in
+++ b/configure.in
@@ -197,13 +197,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)
 
 #
@@ -240,6 +243,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
@@ -4042,8 +4047,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
index 0000000..fb897f1
--- /dev/null
+++ b/contrib/kasp/README
@@ -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
index 0000000..d94b084
--- /dev/null
+++ b/contrib/kasp/kasp.xml
@@ -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
index 0000000..b78a968
--- /dev/null
+++ b/contrib/kasp/kasp2policy.py
@@ -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
index 0000000..18c6360
--- /dev/null
+++ b/contrib/kasp/policy.good
@@ -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;
+};
diff --git a/doc/arm/notes.xml b/doc/arm/notes.xml
new file mode 100644
index 0000000..07776b0
--- /dev/null
+++ b/doc/arm/notes.xml
@@ -0,0 +1,714 @@
+<!DOCTYPE book [
+<!ENTITY Scaron "&#x160;">
+<!ENTITY ccaron "&#x10D;">
+<!ENTITY aacute "&#x0E1;">
+<!ENTITY mdash "&#8212;">
+<!ENTITY ouml "&#xf6;">]>
+<!--
+ - Copyright (C) 2014-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.
+-->
+
+<section xmlns="http://docbook.org/ns/docbook" version="5.0"><info/>
+  <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="noteversion.xml"/>
+  <section xml:id="relnotes_intro"><info><title>Introduction</title></info>
+    <para>
+      BIND 9.11.0 is a new feature release of BIND, still under development.
+      This document summarizes new features and functional changes that
+      have been introduced on this branch.  With each development
+      release leading up to the final BIND 9.11.0 release, this document
+      will be updated with additional features added and bugs fixed.
+    </para>
+  </section>
+
+  <section xml:id="relnotes_download"><info><title>Download</title></info>
+    <para>
+      The latest versions of BIND 9 software can always be found at
+      <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://www.isc.org/downloads/">http://www.isc.org/downloads/</link>.
+      There you will find additional information about each release,
+      source code, and pre-compiled versions for Microsoft Windows
+      operating systems.
+    </para>
+  </section>
+
+  <section xml:id="relnotes_security"><info><title>Security Fixes</title></info>
+    <itemizedlist>
+      <listitem>
+	<para>
+	  None.
+	</para>
+      </listitem>
+    </itemizedlist>
+  </section>
+
+  <section xml:id="relnotes_features"><info><title>New Features</title></info>
+    <itemizedlist>
+      <listitem>
+	<para>
+	  Added support for DynDB, a new interface for loading zone data
+	  from an external database, developed by Red Hat for the FreeIPA
+	  project.  (Thanks in particular to Adam Tkac and Petr
+	  Spacek of Red Hat for the contribution.)
+	</para>
+	<para>
+	  Unlike the existing DLZ and SDB interfaces, which provide a
+	  limited subset of database functionality within BIND &mdash;
+	  translating DNS queries into real-time database lookups with
+	  relatively poor performance and with no ability to handle
+	  DNSSEC-signed data &mdash; DynDB is able to fully implement
+	  and extend the database API used natively by BIND.
+	</para>
+	<para>
+	  A DynDB module could pre-load data from an external data
+	  source, then serve it with the same performance and
+	  functionality as conventional BIND zones, and with the
+	  ability to take advantage of database features not
+	  available in BIND, such as multi-master replication.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  New quotas have been added to limit the queries that are
+	  sent by recursive resolvers to authoritative servers
+	  experiencing denial-of-service attacks. When configured,
+	  these options can both reduce the harm done to authoritative
+	  servers and also avoid the resource exhaustion that can be
+	  experienced by recursives when they are being used as a
+	  vehicle for such an attack.
+	</para>
+	<itemizedlist>
+	  <listitem>
+	    <para>
+	      <option>fetches-per-server</option> limits the number of
+	      simultaneous queries that can be sent to any single
+	      authoritative server.  The configured value is a starting
+	      point; it is automatically adjusted downward if the server is
+	      partially or completely non-responsive. The algorithm used to
+	      adjust the quota can be configured via the
+	      <option>fetch-quota-params</option> option.
+	    </para>
+	  </listitem>
+	  <listitem>
+	    <para>
+	      <option>fetches-per-zone</option> limits the number of
+	      simultaneous queries that can be sent for names within a
+	      single domain.  (Note: Unlike "fetches-per-server", this
+	      value is not self-tuning.)
+	    </para>
+	  </listitem>
+	</itemizedlist>
+	<para>
+	  Statistics counters have also been added to track the number
+	  of queries affected by these quotas.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Added support for <command>dnstap</command>, a fast,
+	  flexible method for capturing and logging DNS traffic,
+	  developed by Robert Edmonds at Farsight Security, Inc.,
+	  whose assistance is gratefully acknowledged.
+	</para>
+	<para>
+	  To enable <command>dnstap</command> at compile time,
+	  the <command>fstrm</command> and <command>protobuf-c</command>
+	  libraries must be available, and BIND must be configured with
+	  <option>--enable-dnstap</option>.
+	</para>
+	<para>
+	  A new utility <command>dnstap-read</command> has been added
+	  to allow <command>dnstap</command> data to be presented in
+	  a human-readable format.
+	</para>
+	<para>
+	  For more information on <command>dnstap</command>, see
+	  <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://dnstap.info">http://dnstap.info</link>.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  New statistics counters have been added to track traffic
+	  sizes, as specified in RSSAC002.  Query and response
+	  message sizes are broken up into ranges of histogram buckets:
+	  TCP and UDP queries of size 0-15, 16-31, ..., 272-288, and 288+,
+	  and TCP and UDP responses of size 0-15, 16-31, ..., 4080-4095,
+	  and 4096+.  These values can be accessed via the XML and JSON
+	  statistics channels at, for example,
+	  <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://localhost:8888/xml/v3/traffic">http://localhost:8888/xml/v3/traffic</link>
+	  or
+	  <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
+	  now be set using
+	  <command>rndc signing -serial <replaceable>number</replaceable> <replaceable>zonename</replaceable></command>.
+	  This is particularly useful with <option>inline-signing</option>
+	  zones that have been reset.  Setting the serial number to a value
+	  larger than that on the slaves will trigger an AXFR-style
+	  transfer.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  When answering recursive queries, SERVFAIL responses can now be
+	  cached by the server for a limited time; subsequent queries for
+	  the same query name and type will return another SERVFAIL until
+	  the cache times out.  This reduces the frequency of retries
+	  when a query is persistently failing, which can be a burden
+	  on recursive serviers.  The SERVFAIL cache timeout is controlled
+	  by <option>servfail-ttl</option>, which defaults to 1 second
+	  and has an upper limit of 30.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The new <command>rndc nta</command> command can now be used to
+	  set a "negative trust anchor" (NTA), disabling DNSSEC validation for
+	  a specific domain; this can be used when responses from a domain
+	  are known to be failing validation due to administrative error
+	  rather than because of a spoofing attack. NTAs are strictly
+	  temporary; by default they expire after one hour, but can be
+	  configured to last up to one week.  The default NTA lifetime
+	  can be changed by setting the <option>nta-lifetime</option> in
+	  <filename>named.conf</filename>. When added, NTAs are stored in a
+	  file (<filename><replaceable>viewname</replaceable>.nta</filename>)
+	  in order to persist across restarts of the <command>named</command> server.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The EDNS Client Subnet (ECS) option is now supported for
+	  authoritative servers; if a query contains an ECS option then
+	  ACLs containing <option>geoip</option> or <option>ecs</option>
+	  elements can match against the address encoded in the option.
+	  This can be used to select a view for a query, so that different
+	  answers can be provided depending on the client network.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The EDNS EXPIRE option has been implemented on the client
+	  side, allowing a slave server to set the expiration timer
+	  correctly when transferring zone data from another slave
+	  server.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  A new <option>masterfile-style</option> zone option controls
+	  the formatting of text zone files:  When set to
+	  <literal>full</literal>, the zone file will dumped in
+	  single-line-per-record format.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>dig +ednsopt</command> can now be used to set
+	  arbitrary EDNS options in DNS requests.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>dig +ednsflags</command> can now be used to set
+	  yet-to-be-defined EDNS flags in DNS requests.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>dig +[no]ednsnegotiation</command> can now be used enable /
+	  disable EDNS version negotiation.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>dig +header-only</command> can now be used to send
+	  queries without a question section.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>dig +ttlunits</command> causes <command>dig</command>
+	  to print TTL values with time-unit suffixes: w, d, h, m, s for
+	  weeks, days, hours, minutes, and seconds.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>dig +zflag</command> can be used to set the last
+	  unassigned DNS header flag bit.  This bit is normally zero.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>dig +dscp=<replaceable>value</replaceable></command>
+	  can now be used to set the DSCP code point in outgoing query
+	  packets.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>dig +mapped</command> can now be used to determine
+	  if mapped IPv4 addresses can be used.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <option>serial-update-method</option> can now be set to
+	  <literal>date</literal>. On update, the serial number will
+	  be set to the current date in YYYYMMDDNN format.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>dnssec-signzone -N date</command> also sets the serial
+	  number to YYYYMMDDNN.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>named -L <replaceable>filename</replaceable></command>
+	  causes <command>named</command> to send log messages to the
+	  specified file by default instead of to the system log.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The rate limiter configured by the
+	  <option>serial-query-rate</option> option no longer covers
+	  NOTIFY messages; those are now separately controlled by
+	  <option>notify-rate</option> and
+	  <option>startup-notify-rate</option> (the latter of which
+	  controls the rate of NOTIFY messages sent when the server
+	  is first started up or reconfigured).
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The default number of tasks and client objects available
+	  for serving lightweight resolver queries have been increased,
+	  and are now configurable via the new <option>lwres-tasks</option>
+	  and <option>lwres-clients</option> options in
+	  <filename>named.conf</filename>. [RT #35857]
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Log output to files can now be buffered by specifying
+	  <command>buffered yes;</command> when creating a channel.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>delv +tcp</command> will exclusively use TCP when
+	  sending queries.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>named</command> will now check to see whether
+	  other name server processes are running before starting up.
+	  This is implemented in two ways: 1) by refusing to start
+	  if the configured network interfaces all return "address
+	  in use", and 2) by attempting to acquire a lock on a file
+	  specified by the <option>lock-file</option> option or
+	  the <command>-X</command> command line option.  The
+	  default lock file is
+	  <filename>/var/run/named/named.lock</filename>.
+	  Specifying <literal>none</literal> will disable the lock
+	  file check.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>rndc delzone</command> can now be applied to zones
+	  which were configured in <filename>named.conf</filename>;
+	  it is no longer restricted to zones which were added by
+	  <command>rndc addzone</command>.  (Note, however, that
+	  this does not edit <filename>named.conf</filename>; the zone
+	  must be removed from the configuration or it will return
+	  when <command>named</command> is restarted or reloaded.)
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>rndc modzone</command> can be used to reconfigure
+	  a zone, using similar syntax to <command>rndc addzone</command>.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  <command>rndc showzone</command> displays the current
+	  configuration for a specified zone.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Added server-side support for pipelined TCP queries.  Clients
+	  may continue sending queries via TCP while previous queries are
+	  processed in parallel.  Responses are sent when they are
+	  ready, not necessarily in the order in which the queries were
+	  received.
+	</para>
+	<para>
+	  To revert to the former behavior for a particular
+	  client address or range of addresses, specify the address prefix
+	  in the "keep-response-order" option.  To revert to the former
+	  behavior for all clients, use "keep-response-order { any; };".
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The new <command>mdig</command> command is a version of
+	  <command>dig</command> that sends multiple pipelined
+	  queries and then waits for responses, instead of sending one
+	  query and waiting the response before sending the next. [RT #38261]
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  To enable better monitoring and troubleshooting of RFC 5011
+	  trust anchor management, the new <command>rndc managed-keys</command>
+	  can be used to check status of trust anchors or to force keys
+	  to be refreshed.  Also, the managed-keys data file now has
+	  easier-to-read comments. [RT #38458]
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  An <command>--enable-querytrace</command> configure switch is
+	  now available to enable very verbose query tracelogging. This
+	  option can only be set at compile time. This option has a
+	  negative performance impact and should be used only for
+	  debugging. [RT #37520]
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  A new <command>tcp-only</command> option can be specified
+	  in <command>server</command> statements to force
+	  <command>named</command> to connect to the specified
+	  server via TCP. [RT #37800]
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The <command>nxdomain-redirect</command> option specifies
+	  a DNS namespace to use for NXDOMAIN redirection. When a
+	  recursive lookup returns NXDOMAIN, a second lookup is
+	  initiated with the specified name appended to the query
+	  name. This allows NXDOMAIN redirection data to be supplied
+	  by multiple zones configured on the server or by recursive
+	  queries to other servers. (The older method, using
+	  a single <command>type redirect</command> zone, has
+	  better average performance but is less flexible.) [RT #37989]
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The following types have been implemented: CSYNC, NINFO, RKEY,
+	  SINK, TA, TALINK.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  A new <command>message-compression</command> option can be
+	  used to specify whether or not to use name compression when
+	  answering queries. Setting this to <userinput>no</userinput>
+	  results in larger responses, but reduces CPU consumption and
+	  may improve throughput.  The default is <userinput>yes</userinput>.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  A <command>read-only</command> option is now available in the
+	  <command>controls</command> statement to grant non-destructive
+	  control channel access. In such cases, a restricted set of
+	  <command>rndc</command> commands are allowed, which can
+	  report information from <command>named</command>, but cannot
+	  reconfigure or stop the server. By default, the control channel
+	  access is <emphasis>not</emphasis> restricted to these
+	  read-only operations. [RT #40498]
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  When loading a signed zone, <command>named</command> will
+	  now check whether an RRSIG's inception time is in the future,
+	  and if so, it will regenerate the RRSIG immediately. This helps
+	  when a system's clock needs to be reset backwards.
+	</para>
+      </listitem>
+    </itemizedlist>
+  </section>
+
+  <section xml:id="relnotes_changes"><info><title>Feature Changes</title></info>
+    <itemizedlist>
+      <listitem>
+        <para>
+	  The timers returned by the statistics channel (indicating current
+	  time, server boot time, and most recent reconfiguration time) are
+	  now reported with millisecond accuracy. [RT #40082]
+	</para>
+      </listitem>
+      <listitem>
+        <para>
+	  Updated the compiled-in addresses for H.ROOT-SERVERS.NET
+	  and L.ROOT-SERVERS.NET.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  ACLs containing <command>geoip asnum</command> elements were
+	  not correctly matched unless the full organization name was
+	  specified in the ACL (as in
+	  <command>geoip asnum "AS1234 Example, Inc.";</command>).
+	  They can now match against the AS number alone (as in
+	  <command>geoip asnum "AS1234";</command>).
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  When using native PKCS#11 cryptography (i.e.,
+	  <command>configure --enable-native-pkcs11</command>) HSM PINs
+	  of up to 256 characters can now be used.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  NXDOMAIN responses to queries of type DS are now cached separately
+	  from those for other types. This helps when using "grafted" zones
+	  of type forward, for which the parent zone does not contain a
+	  delegation, such as local top-level domains.  Previously a query
+	  of type DS for such a zone could cause the zone apex to be cached
+	  as NXDOMAIN, blocking all subsequent queries.  (Note: This
+	  change is only helpful when DNSSEC validation is not enabled.
+	  "Grafted" zones without a delegation in the parent are not a
+	  recommended configuration.)
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Update forwarding performance has been improved by allowing
+	  a single TCP connection to be shared between multiple updates.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  By default, <command>nsupdate</command> will now check
+	  the correctness of hostnames when adding records of type
+	  A, AAAA, MX, SOA, NS, SRV or PTR.  This behavior can be
+	  disabled with <command>check-names no</command>.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Added support for OPENPGPKEY type.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The names of the files used to store managed keys and added
+	  zones for each view are no longer based on the SHA256 hash
+	  of the view name, except when this is necessary because the
+	  view name contains characters that would be incompatible with use
+	  as a file name.  For views whose names do not contain forward
+	  slashes ('/'), backslashes ('\'), or capital letters - which
+	  could potentially cause namespace collision problems on
+	  case-insensitive filesystems - files will now be named
+	  after the view (for example, <filename>internal.mkeys</filename>
+	  or <filename>external.nzf</filename>).  However, to ensure
+	  consistent behavior when upgrading, if a file using the old
+	  name format is found to exist, it will continue to be used.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  "rndc" can now return text output of arbitrary size to
+	  the caller. (Prior to this, certain commands such as
+	  "rndc tsig-list" and "rndc zonestatus" could return
+	  truncated output.)
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Errors reported when running <command>rndc addzone</command>
+	  (e.g., when a zone file cannot be loaded) have been clarified
+	  to make it easier to diagnose problems.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  When encountering an authoritative name server whose name is
+	  an alias pointing to another name, the resolver treats
+	  this as an error and skips to the next server. Previously
+	  this happened silently; now the error will be logged to
+	  the newly-created "cname" log category.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  If <command>named</command> is not configured to validate
+	  answers, then allow fallback to plain DNS on timeout even when
+	  we know the server supports EDNS.  This will allow the server to
+	  potentially resolve signed queries when TCP is being
+	  blocked.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Large inline-signing changes should be less disruptive.
+	  Signature generation is now done incrementally; the number
+	  of signatures to be generated in each quantum is controlled
+	  by "sig-signing-signatures <replaceable>number</replaceable>;".
+	  [RT #37927]
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The experimental SIT option (code point 65001) of BIND
+	  9.10.0 through BIND 9.10.2 has been replaced with the COOKIE
+	  option (code point 10). It is no longer experimental, and
+	  is sent by default, by both <command>named</command> and
+	  <command>dig</command>.
+	</para>
+	<para>
+	  The SIT-related named.conf options have been marked as
+	  obsolete, and are otherwise ignored.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  When <command>dig</command> receives a truncated (TC=1)
+	  response or a BADCOOKIE response code from a server, it
+	  will automatically retry the query using the server COOKIE
+	  that was returned by the server in its initial response.
+	  [RT #39047]
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  A alternative NXDOMAIN redirect method (nxdomain-redirect)
+	  which allows the redirect information to be looked up from
+	  a namespace on the Internet rather than requiring a zone
+	  to be configured on the server is now available.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Retrieving the local port range from net.ipv4.ip_local_port_range
+	  on Linux is now supported.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Within the <option>response-policy</option> option, it is now
+	  possible to configure RPZ rewrite logging on a per-zone basis
+	  using the <option>log</option> clause.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  The default preferred glue is now the address type of the
+	   transport the query was received over.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  On machines with 2 or more processors (CPU), the default value
+	  for the number of UDP listeners has been changed to the number
+	  of detected processors minus one.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Zone transfers now use smaller message sizes to improve
+	  message compression. This results in reduced network usage.
+	</para>
+      </listitem>
+      <listitem>
+	<para>
+	  Added support for the AVC resource record type (Application
+	  Visibility and Control).
+	</para>
+      </listitem>
+    </itemizedlist>
+  </section>
+
+  <section xml:id="relnotes_port"><info><title>Porting Changes</title></info>
+    <itemizedlist>
+      <listitem>
+	<para>
+	  None.
+	</para>
+      </listitem>
+    </itemizedlist>
+  </section>
+
+  <section xml:id="relnotes_bugs"><info><title>Bug Fixes</title></info>
+    <itemizedlist>
+      <listitem>
+	<para>
+	  None.
+	</para>
+      </listitem>
+    </itemizedlist>
+  </section>
+  <section xml:id="end_of_life"><info><title>End of Life</title></info>
+
+    <para>
+      The end of life for BIND 9.11 is yet to be determined but
+      will not be before BIND 9.13.0 has been released for 6 months.
+      <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="https://www.isc.org/downloads/software-support-policy/">https://www.isc.org/downloads/software-support-policy/</link>
+    </para>
+  </section>
+  <section xml:id="relnotes_thanks"><info><title>Thank You</title></info>
+
+    <para>
+      Thank you to everyone who assisted us in making this release possible.
+      If you would like to contribute to ISC to assist us in continuing to
+      make quality open source software, please visit our donations page at
+      <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://www.isc.org/donate/">http://www.isc.org/donate/</link>.
+    </para>
+  </section>
+</section>
-- 
2.14.3