From 923ea26e87ba7c8b8c818dc6c996604fc709b05a Mon Sep 17 00:00:00 2001 From: Evan Hunt 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 @@ + + + + + + 2016-04-03 + + + ISC + Internet Systems Consortium, Inc. + + + + dnssec-keymgr + 8 + BIND9 + + + + dnssec-keymgr + Ensures correct DNSKEY coverage for a zone based on a defined policy + + + + + 2016 + Internet Systems Consortium, Inc. ("ISC") + + + + + + dnssec-keymgr + + + + + + + + zone + + + + DESCRIPTION + + dnssec-keymgr + 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: + dnssec-keygen and + dnssec-settime. + + + DNSSEC policy can be read from a configuration file (default + /etc/dnssec.policy), 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. + + + When dnssec-keymgr 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. + + + A zone policy can specify a duration for which we want to + ensure the key correctness (). It can + also specify a rollover period (). + 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. + + + If zones are specified on the command line, + dnssec-keymgr 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. + + + If zones are not specified on the command + line, then dnssec-keymgr will search the + key directory (either the current working directory or the directory + set by the option), and check the keys for + all the zones represented in the directory. + + + It is expected that this tool will be run automatically and + unattended (for example, by cron). + + + + OPTIONS + + + -K directory + + + Sets the directory in which keys can be found. Defaults to the + current working directory. + + + + + + -c file + + + If is specified, then the DNSSEC + policy is read from . (If not + specified, then the policy is read from + /etc/policy.conf; if that file + doesn't exist, a built-in global default policy is used.) + + + + + + -f + + + 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. + + + + + + -q + + + Quiet: suppress printing of dnssec-keygen + and dnssec-settime. + + + + + + -k + + + Only apply policies to KSK keys. + + + + + + -z + + + Only apply policies to ZSK keys. + + + + + + -g keygen path + + + Specifies a path to a dnssec-keygen binary. + Used for testing. + + + + + + -s settime path + + + Specifies a path to a dnssec-settime binary. + Used for testing. + + + + + + + POLICY CONFIGURATION + + The policy.conf file can specify three kinds + of policies: + + + + Policy classes + () + 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 normal might specify + 1024-bit key sizes, but a class extra might + specify 2048 bits instead; extra would be + used for zones that had unusually high security needs. + + + Algorithm policies: + ( ) + 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 algorithm-policy, and the + new key sizes would then be used for any key of type RSASHA256. + + + Zone policies: + ( ) + set policy for a single zone by name. A zone policy can inherit + a policy class by including a option. + + + + Options that can be specified in policies: + + + + directory + + Specifies the directory in which keys should be stored. + + + + algorithm + + The key algorithm. If no policy is defined, the default is + RSASHA256. + + + + keyttl + + The key TTL. If no policy is defined, the default is one hour. + + + + coverage + + 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. + + + + key-size + + 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. + + + + roll-period + + 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. + + + + pre-publish + + How long before activation a key should be published. Note: If + 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. + + + + post-publish + + How long after inactivation a key should be deleted from the zone. + Note: If 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. + + + + standby + + Not yet implemented. + + + + + + REMAINING WORK + + + Enable scheduling of KSK rollovers using the + and options to + dnssec-keygen and + dnssec-settime. Check the parent zone + (as in dnssec-checkds) to determine when it's + safe for the key to roll. + + + Allow configuration of standby keys and use of the REVOKE bit, + for keys that use RFC 5011 semantics. + + + + + SEE ALSO + + + dnssec-coverage8 + , + + dnssec-keygen8 + , + + dnssec-settime8 + , + + dnssec-checkds8 + + + + + 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 @@ + + + + + + A default policy that will + amaze you and your friends + + PT5M + PT5M + + PT15M + PT15M + + PT2M + PT1M + + + + + + + + + + PT1M + PT0S + PT0S + + + + 5 + PT40M + softHSM + 1 + + + + + 5 + PT25M + softHSM + 1 + + + + + PT0S + + PT0S + PT0S + unixtime + + + + + PT8M + + PT0S + + + PT0S + PT0S + + + + + A default policy that will amaze you and your friends + + PT7M + PT7M + + PT15M + PT16M + + PT2M + PT1M + + + + + P120D + + 1 + 5 + + + + + + + + PT15M + PT0S + PT0S + + + + 7 + PT45M + softHSM + 1 + + + + + 7 + PT25M + softHSM + 1 + + + + + PT0S + + PT0S + PT0S + unixtime + + + + + PT12M + + PT0S + + + PT0S + PT0S + + + + 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 ") + 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 @@ + + + + +]> + + +
+ +
Introduction + + 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. + +
+ +
Download + + The latest versions of BIND 9 software can always be found at + http://www.isc.org/downloads/. + There you will find additional information about each release, + source code, and pre-compiled versions for Microsoft Windows + operating systems. + +
+ +
Security Fixes + + + + None. + + + +
+ +
New Features + + + + 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.) + + + 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. + + + 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. + + + + + 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. + + + + + 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. + + + + + 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.) + + + + + Statistics counters have also been added to track the number + of queries affected by these quotas. + + + + + Added support for dnstap, a fast, + flexible method for capturing and logging DNS traffic, + developed by Robert Edmonds at Farsight Security, Inc., + whose assistance is gratefully acknowledged. + + + To enable dnstap at compile time, + the fstrm and protobuf-c + libraries must be available, and BIND must be configured with + . + + + A new utility dnstap-read has been added + to allow dnstap data to be presented in + a human-readable format. + + + For more information on dnstap, see + http://dnstap.info. + + + + + 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, + http://localhost:8888/xml/v3/traffic + or + http://localhost:8888/json/v1/traffic. + + + + + A new DNSSEC key management utility, + dnssec-keymgr, has been added. This tool + is meant to run unattended (e.g., under cron). + It reads a policy definition file + (default: /etc/dnssec.policy) + 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 dnssec-keymgr man page for full details. + + + Note: dnssec-keymgr depends on Python and on + the Python lex/yacc module, PLY. The other Python-based tools, + dnssec-coverage and + dnssec-checkds, have been + refactored and updated as part of this work. + + + (Many thanks to Sebastián + Castro for his assistance in developing this tool at the IETF + 95 Hackathon in Buenos Aires, April 2016.) + + + + + The serial number of a dynamically updatable zone can + now be set using + rndc signing -serial number zonename. + This is particularly useful with + zones that have been reset. Setting the serial number to a value + larger than that on the slaves will trigger an AXFR-style + transfer. + + + + + 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 , which defaults to 1 second + and has an upper limit of 30. + + + + + The new rndc nta 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 in + named.conf. When added, NTAs are stored in a + file (viewname.nta) + in order to persist across restarts of the named server. + + + + + The EDNS Client Subnet (ECS) option is now supported for + authoritative servers; if a query contains an ECS option then + ACLs containing or + 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. + + + + + 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. + + + + + A new zone option controls + the formatting of text zone files: When set to + full, the zone file will dumped in + single-line-per-record format. + + + + + dig +ednsopt can now be used to set + arbitrary EDNS options in DNS requests. + + + + + dig +ednsflags can now be used to set + yet-to-be-defined EDNS flags in DNS requests. + + + + + dig +[no]ednsnegotiation can now be used enable / + disable EDNS version negotiation. + + + + + dig +header-only can now be used to send + queries without a question section. + + + + + dig +ttlunits causes dig + to print TTL values with time-unit suffixes: w, d, h, m, s for + weeks, days, hours, minutes, and seconds. + + + + + dig +zflag can be used to set the last + unassigned DNS header flag bit. This bit is normally zero. + + + + + dig +dscp=value + can now be used to set the DSCP code point in outgoing query + packets. + + + + + dig +mapped can now be used to determine + if mapped IPv4 addresses can be used. + + + + + can now be set to + date. On update, the serial number will + be set to the current date in YYYYMMDDNN format. + + + + + dnssec-signzone -N date also sets the serial + number to YYYYMMDDNN. + + + + + named -L filename + causes named to send log messages to the + specified file by default instead of to the system log. + + + + + The rate limiter configured by the + option no longer covers + NOTIFY messages; those are now separately controlled by + and + (the latter of which + controls the rate of NOTIFY messages sent when the server + is first started up or reconfigured). + + + + + The default number of tasks and client objects available + for serving lightweight resolver queries have been increased, + and are now configurable via the new + and options in + named.conf. [RT #35857] + + + + + Log output to files can now be buffered by specifying + buffered yes; when creating a channel. + + + + + delv +tcp will exclusively use TCP when + sending queries. + + + + + named 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 or + the -X command line option. The + default lock file is + /var/run/named/named.lock. + Specifying none will disable the lock + file check. + + + + + rndc delzone can now be applied to zones + which were configured in named.conf; + it is no longer restricted to zones which were added by + rndc addzone. (Note, however, that + this does not edit named.conf; the zone + must be removed from the configuration or it will return + when named is restarted or reloaded.) + + + + + rndc modzone can be used to reconfigure + a zone, using similar syntax to rndc addzone. + + + + + rndc showzone displays the current + configuration for a specified zone. + + + + + 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. + + + 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; };". + + + + + The new mdig command is a version of + dig 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] + + + + + To enable better monitoring and troubleshooting of RFC 5011 + trust anchor management, the new rndc managed-keys + 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] + + + + + An --enable-querytrace 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] + + + + + A new tcp-only option can be specified + in server statements to force + named to connect to the specified + server via TCP. [RT #37800] + + + + + The nxdomain-redirect 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 type redirect zone, has + better average performance but is less flexible.) [RT #37989] + + + + + The following types have been implemented: CSYNC, NINFO, RKEY, + SINK, TA, TALINK. + + + + + A new message-compression option can be + used to specify whether or not to use name compression when + answering queries. Setting this to no + results in larger responses, but reduces CPU consumption and + may improve throughput. The default is yes. + + + + + A read-only option is now available in the + controls statement to grant non-destructive + control channel access. In such cases, a restricted set of + rndc commands are allowed, which can + report information from named, but cannot + reconfigure or stop the server. By default, the control channel + access is not restricted to these + read-only operations. [RT #40498] + + + + + When loading a signed zone, named 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. + + + +
+ +
Feature Changes + + + + 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] + + + + + Updated the compiled-in addresses for H.ROOT-SERVERS.NET + and L.ROOT-SERVERS.NET. + + + + + ACLs containing geoip asnum elements were + not correctly matched unless the full organization name was + specified in the ACL (as in + geoip asnum "AS1234 Example, Inc.";). + They can now match against the AS number alone (as in + geoip asnum "AS1234";). + + + + + When using native PKCS#11 cryptography (i.e., + configure --enable-native-pkcs11) HSM PINs + of up to 256 characters can now be used. + + + + + 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.) + + + + + Update forwarding performance has been improved by allowing + a single TCP connection to be shared between multiple updates. + + + + + By default, nsupdate 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 check-names no. + + + + + Added support for OPENPGPKEY type. + + + + + 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, internal.mkeys + or external.nzf). 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. + + + + + "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.) + + + + + Errors reported when running rndc addzone + (e.g., when a zone file cannot be loaded) have been clarified + to make it easier to diagnose problems. + + + + + 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. + + + + + If named 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. + + + + + 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 number;". + [RT #37927] + + + + + 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 named and + dig. + + + The SIT-related named.conf options have been marked as + obsolete, and are otherwise ignored. + + + + + When dig 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] + + + + + 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. + + + + + Retrieving the local port range from net.ipv4.ip_local_port_range + on Linux is now supported. + + + + + Within the option, it is now + possible to configure RPZ rewrite logging on a per-zone basis + using the clause. + + + + + The default preferred glue is now the address type of the + transport the query was received over. + + + + + 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. + + + + + Zone transfers now use smaller message sizes to improve + message compression. This results in reduced network usage. + + + + + Added support for the AVC resource record type (Application + Visibility and Control). + + + +
+ +
Porting Changes + + + + None. + + + +
+ +
Bug Fixes + + + + None. + + + +
+
End of Life + + + 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. + https://www.isc.org/downloads/software-support-policy/ + +
+
Thank You + + + 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 + http://www.isc.org/donate/. + +
+
-- 2.14.3