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 "Š">
+<!ENTITY ccaron "č">
+<!ENTITY aacute "á">
+<!ENTITY mdash "—">
+<!ENTITY ouml "ö">]>
+<!--
+ - 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 —
+ translating DNS queries into real-time database lookups with
+ relatively poor performance and with no ability to handle
+ DNSSEC-signed data — 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á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