From: Evan Hunt Date: Thu, 28 Apr 2016 07:12:33 +0000 (-0700) Subject: [master] dnssec-keymgr X-Git-Tag: v9.11.0a2~78 X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=f6096b958c8b58c4709860d7c4dcdde5deeacb7a;p=thirdparty%2Fbind9.git [master] 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] --- diff --git a/CHANGES b/CHANGES index 9c75d27d386..31883339ca4 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,13 @@ +4349. [contrib] kasp2policy: A python script to create a DNSSEC + policy file from an OpenDNSSEC KASP XML file. + +4348. [func] dnssec-keymgr: A new python-based DNSSEC key + management utility, which reads a policy definition + file and can create or update DNSSEC keys as needed + to ensure that a zone's keys match policy, roll over + correctly on schedule, etc. Thanks to Sebastian + Castro for assistance in development. [RT #39211] + 4347. [port] Corrected a build error on x86_64 Solaris. [RT #42150] 4346. [bug] Fixed a regression introduced in change #4337 which diff --git a/bin/dnssec/dnssec-settime.c b/bin/dnssec/dnssec-settime.c index 90d3b67cb4c..8a31cf97455 100644 --- a/bin/dnssec/dnssec-settime.c +++ b/bin/dnssec/dnssec-settime.c @@ -519,11 +519,12 @@ main(int argc, char **argv) { if ((setdel && setinact && del < inact) || (dst_key_gettime(key, DST_TIME_INACTIVE, &previnact) == ISC_R_SUCCESS && - setdel && !setinact && del < previnact) || + setdel && !setinact && !unsetinact && del < previnact) || (dst_key_gettime(key, DST_TIME_DELETE, &prevdel) == ISC_R_SUCCESS && - setinact && !setdel && prevdel < inact) || - (!setdel && !setinact && prevdel < previnact)) + setinact && !setdel && !unsetdel && prevdel < inact) || + (!setdel && !unsetdel && !setinact && !unsetinact && + prevdel < previnact)) fprintf(stderr, "%s: warning: Key is scheduled to " "be deleted before it is\n\t" "scheduled to be inactive.\n", diff --git a/bin/python/.gitignore b/bin/python/.gitignore index f770f2396e7..2e6963dfc9d 100644 --- a/bin/python/.gitignore +++ b/bin/python/.gitignore @@ -2,3 +2,6 @@ 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 dfb5d597114..c731a5d953a 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 -PYSRCS = dnssec-checkds.py dnssec-coverage.py +SUBDIRS = isc + +TARGETS = dnssec-checkds dnssec-coverage dnssec-keymgr +PYSRCS = dnssec-checkds.py dnssec-coverage.py dnssec-keymgr.py -MANPAGES = dnssec-checkds.8 dnssec-coverage.8 -HTMLPAGES = dnssec-checkds.html dnssec-coverage.html +MANPAGES = dnssec-checkds.8 dnssec-coverage.8 dnssec-keymgr.8 +HTMLPAGES = dnssec-checkds.html dnssec-coverage.html dnssec-keymgr.html MANOBJS = ${MANPAGES} ${HTMLPAGES} @BIND9_MAKE_RULES@ @@ -39,6 +39,10 @@ dnssec-coverage: dnssec-coverage.py cp -f dnssec-coverage.py dnssec-coverage chmod +x dnssec-coverage +dnssec-keymgr: dnssec-keymgr.py + cp -f dnssec-keymgr.py dnssec-keymgr + chmod +x dnssec-keymgr + doc man:: ${MANOBJS} docclean manclean maintainer-clean:: @@ -49,13 +53,15 @@ installdirs: $(SHELL) ${top_srcdir}/mkinstalldirs ${DESTDIR}${mandir}/man8 install:: ${TARGETS} installdirs - ${INSTALL_SCRIPT} dnssec-checkds@EXEEXT@ ${DESTDIR}${sbindir} - ${INSTALL_SCRIPT} dnssec-coverage@EXEEXT@ ${DESTDIR}${sbindir} + ${INSTALL_SCRIPT} dnssec-checkds ${DESTDIR}${sbindir} + ${INSTALL_SCRIPT} dnssec-coverage ${DESTDIR}${sbindir} + ${INSTALL_SCRIPT} dnssec-keymgr ${DESTDIR}${sbindir} ${INSTALL_DATA} ${srcdir}/dnssec-checkds.8 ${DESTDIR}${mandir}/man8 ${INSTALL_DATA} ${srcdir}/dnssec-coverage.8 ${DESTDIR}${mandir}/man8 + ${INSTALL_DATA} ${srcdir}/dnssec-keymgr.8 ${DESTDIR}${mandir}/man8 clean distclean:: rm -f ${TARGETS} distclean:: - rm -f dnssec-checkds.py dnssec-coverage.py + rm -f dnssec-checkds.py dnssec-coverage.py dnssec-keymgr.py diff --git a/bin/python/dnssec-checkds.py.in b/bin/python/dnssec-checkds.py.in index 40b730ba07f..79db6f1aa16 100644 --- a/bin/python/dnssec-checkds.py.in +++ b/bin/python/dnssec-checkds.py.in @@ -15,314 +15,13 @@ # PERFORMANCE OF THIS SOFTWARE. ############################################################################ -import argparse -import pprint import os +import sys -prog='dnssec-checkds' +sys.path.insert(0, os.path.dirname(sys.argv[0])) +sys.path.insert(1, os.path.join('@prefix@', 'lib')) -# These routines permit platform-independent location of BIND 9 tools -if os.name == 'nt': - import win32con - import win32api - -def prefix(bindir = ''): - if os.name != 'nt': - return os.path.join('@prefix@', bindir) - - bind_subkey = "Software\\ISC\\BIND" - hKey = None - keyFound = True - try: - hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey) - except: - keyFound = False - if keyFound: - try: - (namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir") - except: - keyFound = False - win32api.RegCloseKey(hKey) - if keyFound: - return os.path.join(namedBase, bindir) - return os.path.join(win32api.GetSystemDirectory(), bindir) - -def shellquote(s): - if os.name == 'nt': - return '"' + s.replace('"', '"\\"') + '"' - return "'" + s.replace("'", "'\\''") + "'" - -############################################################################ -# DSRR class: -# Delegation Signer (DS) resource record -############################################################################ -class DSRR: - hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384' } - rrname='' - rrclass='IN' - rrtype='DS' - keyid=None - keyalg=None - hashalg=None - digest='' - ttl=0 - - def __init__(self, rrtext): - if not rrtext: - return - - fields = rrtext.split() - if len(fields) < 7: - return - - self.rrname = fields[0].lower() - fields = fields[1:] - if fields[0].upper() in ['IN','CH','HS']: - self.rrclass = fields[0].upper() - fields = fields[1:] - else: - self.ttl = int(fields[0]) - self.rrclass = fields[1].upper() - fields = fields[2:] - - if fields[0].upper() != 'DS': - raise Exception - - self.rrtype = 'DS' - self.keyid = int(fields[1]) - self.keyalg = int(fields[2]) - self.hashalg = int(fields[3]) - self.digest = ''.join(fields[4:]).upper() - - def __repr__(self): - return('%s %s %s %d %d %d %s' % - (self.rrname, self.rrclass, self.rrtype, self.keyid, - self.keyalg, self.hashalg, self.digest)) - - def __eq__(self, other): - return self.__repr__() == other.__repr__() - -############################################################################ -# DLVRR class: -# DNSSEC Lookaside Validation (DLV) resource record -############################################################################ -class DLVRR: - hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384' } - parent='' - dlvname='' - rrname='IN' - rrclass='IN' - rrtype='DLV' - keyid=None - keyalg=None - hashalg=None - digest='' - ttl=0 - - def __init__(self, rrtext, dlvname): - if not rrtext: - return - - fields = rrtext.split() - if len(fields) < 7: - return - - self.dlvname = dlvname.lower() - parent = fields[0].lower().strip('.').split('.') - parent.reverse() - dlv = dlvname.split('.') - dlv.reverse() - while len(dlv) != 0 and len(parent) != 0 and parent[0] == dlv[0]: - parent = parent[1:] - dlv = dlv[1:] - if len(dlv) != 0: - raise Exception - parent.reverse() - self.parent = '.'.join(parent) - self.rrname = self.parent + '.' + self.dlvname + '.' - - fields = fields[1:] - if fields[0].upper() in ['IN','CH','HS']: - self.rrclass = fields[0].upper() - fields = fields[1:] - else: - self.ttl = int(fields[0]) - self.rrclass = fields[1].upper() - fields = fields[2:] - - if fields[0].upper() != 'DLV': - raise Exception - - self.rrtype = 'DLV' - self.keyid = int(fields[1]) - self.keyalg = int(fields[2]) - self.hashalg = int(fields[3]) - self.digest = ''.join(fields[4:]).upper() - - def __repr__(self): - return('%s %s %s %d %d %d %s' % - (self.rrname, self.rrclass, self.rrtype, - self.keyid, self.keyalg, self.hashalg, self.digest)) - - def __eq__(self, other): - return self.__repr__() == other.__repr__() - -############################################################################ -# checkds: -# Fetch DS RRset for the given zone from the DNS; fetch DNSKEY -# RRset from the masterfile if specified, or from DNS if not. -# Generate a set of expected DS records from the DNSKEY RRset, -# and report on congruency. -############################################################################ -def checkds(zone, masterfile = None): - dslist=[] - fp=os.popen("%s +noall +answer -t ds -q %s" % - (shellquote(args.dig), shellquote(zone))) - for line in fp: - dslist.append(DSRR(line)) - dslist = sorted(dslist, key=lambda ds: (ds.keyid, ds.keyalg, ds.hashalg)) - fp.close() - - dsklist=[] - - if masterfile: - fp = os.popen("%s -f %s %s " % - (shellquote(args.dsfromkey), shellquote(masterfile), - shellquote(zone))) - else: - fp = os.popen("%s +noall +answer -t dnskey -q %s | %s -f - %s" % - (shellquote(args.dig), shellquote(zone), - shellquote(args.dsfromkey), shellquote(zone))) - - for line in fp: - dsklist.append(DSRR(line)) - - fp.close() - - if (len(dsklist) < 1): - print ("No DNSKEY records found in zone apex") - return False - - found = False - for ds in dsklist: - if ds in dslist: - print ("DS for KSK %s/%03d/%05d (%s) found in parent" % - (ds.rrname.strip('.'), ds.keyalg, - ds.keyid, DSRR.hashalgs[ds.hashalg])) - found = True - else: - print ("DS for KSK %s/%03d/%05d (%s) missing from parent" % - (ds.rrname.strip('.'), ds.keyalg, - ds.keyid, DSRR.hashalgs[ds.hashalg])) - - if not found: - print ("No DS records were found for any DNSKEY") - - return found - -############################################################################ -# checkdlv: -# Fetch DLV RRset for the given zone from the DNS; fetch DNSKEY -# RRset from the masterfile if specified, or from DNS if not. -# Generate a set of expected DLV records from the DNSKEY RRset, -# and report on congruency. -############################################################################ -def checkdlv(zone, lookaside, masterfile = None): - dlvlist=[] - fp=os.popen("%s +noall +answer -t dlv -q %s" % - (shellquote(args.dig), shellquote(zone + '.' + lookaside))) - for line in fp: - dlvlist.append(DLVRR(line, lookaside)) - dlvlist = sorted(dlvlist, - key=lambda dlv: (dlv.keyid, dlv.keyalg, dlv.hashalg)) - fp.close() - - # - # Fetch DNSKEY records from DNS and generate DLV records from them - # - dlvklist=[] - if masterfile: - fp = os.popen("%s -f %s -l %s %s " % - (args.dsfromkey, masterfile, lookaside, zone)) - else: - fp = os.popen("%s +noall +answer -t dnskey %s | %s -f - -l %s %s" - % (shellquote(args.dig), shellquote(zone), - shellquote(args.dsfromkey), shellquote(lookaside), - shellquote(zone))) - - for line in fp: - dlvklist.append(DLVRR(line, lookaside)) - - fp.close() - - if (len(dlvklist) < 1): - print ("No DNSKEY records found in zone apex") - return False - - found = False - for dlv in dlvklist: - if dlv in dlvlist: - print ("DLV for KSK %s/%03d/%05d (%s) found in %s" % - (dlv.parent, dlv.keyalg, dlv.keyid, - DLVRR.hashalgs[dlv.hashalg], dlv.dlvname)) - found = True - else: - print ("DLV for KSK %s/%03d/%05d (%s) missing from %s" % - (dlv.parent, dlv.keyalg, dlv.keyid, - DLVRR.hashalgs[dlv.hashalg], dlv.dlvname)) - - if not found: - print ("No DLV records were found for any DNSKEY") - - return found - - -############################################################################ -# parse_args: -# Read command line arguments, set global 'args' structure -############################################################################ -def parse_args(): - global args - parser = argparse.ArgumentParser(description=prog + ': checks DS coverage') - - bindir = 'bin' - if os.name == 'nt': - sbindir = 'bin' - else: - sbindir = 'sbin' - - parser.add_argument('zone', type=str, help='zone to check') - parser.add_argument('-f', '--file', dest='masterfile', type=str, - help='zone master file') - parser.add_argument('-l', '--lookaside', dest='lookaside', type=str, - help='DLV lookaside zone') - parser.add_argument('-d', '--dig', dest='dig', - default=os.path.join(prefix(bindir), 'dig'), - type=str, help='path to \'dig\'') - parser.add_argument('-D', '--dsfromkey', dest='dsfromkey', - default=os.path.join(prefix(sbindir), - 'dnssec-dsfromkey'), - type=str, help='path to \'dig\'') - parser.add_argument('-v', '--version', action='version', - version='@BIND9_VERSION@') - args = parser.parse_args() - - args.zone = args.zone.strip('.') - if args.lookaside: - lookaside = args.lookaside.strip('.') - -############################################################################ -# Main -############################################################################ -def main(): - parse_args() - - if args.lookaside: - found = checkdlv(args.zone, args.lookaside, args.masterfile) - else: - found = checkds(args.zone, args.masterfile) - - exit(0 if found else 1) +import isc.checkds if __name__ == "__main__": - main() + isc.checkds.main() diff --git a/bin/python/dnssec-coverage.docbook b/bin/python/dnssec-coverage.docbook index 45d5fa86d1e..892801ecd08 100644 --- a/bin/python/dnssec-coverage.docbook +++ b/bin/python/dnssec-coverage.docbook @@ -56,7 +56,7 @@ - zone + zone @@ -151,10 +151,15 @@ 'd' for days, 'w' for weeks, 'mo' for months, 'y' for years. - This option is mandatory unless the has - been used to specify a zone file. (If has + This option is not necessary if the has + been used to specify a zone file. If has been specified, this option may still be used; it will override - the value found in the file.) + the value found in the file. + + + If this option is not used and the maximum TTL cannot be retrieved + from a zone file, a warning is generated and a default value of + 1 week is used. @@ -166,11 +171,10 @@ Sets the value to be used as the DNSKEY TTL for the zone or zones being analyzed when determining whether there is a possibility of validation failure. When a key is rolled (that - is, replaced with a new key), there must be enough time - for the old DNSKEY RRset to have expired from resolver caches - before the new key is activated and begins generating - signatures. If that condition does not apply, a warning - will be generated. + is, replaced with a new key), there must be enough time for the + old DNSKEY RRset to have expired from resolver caches before + the new key is activated and begins generating signatures. If + that condition does not apply, a warning will be generated. The length of the TTL can be set in seconds, or in larger units @@ -178,12 +182,18 @@ 'd' for days, 'w' for weeks, 'mo' for months, 'y' for years. - This option is mandatory unless the has - been used to specify a zone file, or a default key TTL was - set with the to - dnssec-keygen. (If either of those is true, - this option may still be used; it will override the value found - in the zone or key file.) + This option is not necessary if has + been used to specify a zone file from which the TTL + of the DNSKEY RRset can be read, or if a default key TTL was + set using ith the to + dnssec-keygen. If either of those is true, + this option may still be used; it will override the values + found in the zone file or the key file. + + + If this option is not used and the key TTL cannot be retrieved + from the zone file or the key file, then a warning is generated + and a default value of 1 day is used. diff --git a/bin/python/dnssec-coverage.py.in b/bin/python/dnssec-coverage.py.in old mode 100755 new mode 100644 index ccd01a9cc13..bbd8629d58f --- a/bin/python/dnssec-coverage.py.in +++ b/bin/python/dnssec-coverage.py.in @@ -1,6 +1,6 @@ #!@PYTHON@ ############################################################################ -# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC") +# Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC") # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -15,785 +15,13 @@ # PERFORMANCE OF THIS SOFTWARE. ############################################################################ -import argparse import os -import glob import sys -import re -import time -import calendar -from collections import defaultdict -import pprint -prog='dnssec-coverage' +sys.path.insert(0, os.path.dirname(sys.argv[0])) +sys.path.insert(1, os.path.join('@prefix@', 'lib')) -# These routines permit platform-independent location of BIND 9 tools -if os.name == 'nt': - import win32con - import win32api - -def prefix(bindir = ''): - if os.name != 'nt': - return os.path.join('@prefix@', bindir) - - bind_subkey = "Software\\ISC\\BIND" - hKey = None - keyFound = True - try: - hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey) - except: - keyFound = False - if keyFound: - try: - (namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir") - except: - keyFound = False - win32api.RegCloseKey(hKey) - if keyFound: - return os.path.join(namedBase, bindir) - return os.path.join(win32api.GetSystemDirectory(), bindir) - -######################################################################## -# Class Event -######################################################################## -class Event: - """ A discrete key metadata event, e.g., Publish, Activate, Inactive, - Delete. Stores the date of the event, and identifying information about - the key to which the event will occur.""" - - def __init__(self, _what, _key): - now = time.time() - self.what = _what - self.when = _key.metadata[_what] - self.key = _key - self.keyid = _key.keyid - self.sep = _key.sep - self.zone = _key.zone - self.alg = _key.alg - - def __repr__(self): - return repr((self.when, self.what, self.keyid, self.sep, - self.zone, self.alg)) - - def showtime(self): - return time.strftime("%a %b %d %H:%M:%S UTC %Y", self.when) - - def showkey(self): - return self.key.showkey() - - def showkeytype(self): - return self.key.showkeytype() - -######################################################################## -# Class Key -######################################################################## -class Key: - """An individual DNSSEC key. Identified by path, zone, algorithm, keyid. - Contains a dictionary of metadata events.""" - - def __init__(self, keyname): - directory = os.path.dirname(keyname) - key = os.path.basename(keyname) - (zone, alg, keyid) = key.split('+') - keyid = keyid.split('.')[0] - key = [zone, alg, keyid] - key_file = directory + os.sep + '+'.join(key) + ".key" - private_file = directory + os.sep + '+'.join(key) + ".private" - - self.zone = zone[1:-1] - self.alg = int(alg) - self.keyid = int(keyid) - - kfp = open(key_file, "r") - for line in kfp: - if line[0] == ';': - continue - tokens = line.split() - if not tokens: - continue - - if tokens[1].lower() in ('in', 'ch', 'hs'): - septoken = 3 - self.ttl = args.keyttl - if not self.ttl: - vspace() - print("WARNING: Unable to determine TTL for DNSKEY %s." % - self.showkey()) - print("\t Using 1 day (86400 seconds); re-run with the -d " - "option for more\n\t accurate results.") - self.ttl = 86400 - else: - septoken = 4 - self.ttl = int(tokens[1]) if not args.keyttl else args.keyttl - - if (int(tokens[septoken]) & 0x1) == 1: - self.sep = True - else: - self.sep = False - kfp.close() - - pfp = open(private_file, "rU") - propDict = dict() - for propLine in pfp: - propDef = propLine.strip() - if len(propDef) == 0: - continue - if propDef[0] in ('!', '#'): - continue - punctuation = [propDef.find(c) for c in ':= '] + [len(propDef)] - found = min([ pos for pos in punctuation if pos != -1 ]) - name = propDef[:found].rstrip() - value = propDef[found:].lstrip(":= ").rstrip() - propDict[name] = value - - if("Publish" in propDict): - propDict["Publish"] = time.strptime(propDict["Publish"], - "%Y%m%d%H%M%S") - - if("Activate" in propDict): - propDict["Activate"] = time.strptime(propDict["Activate"], - "%Y%m%d%H%M%S") - - if("Inactive" in propDict): - propDict["Inactive"] = time.strptime(propDict["Inactive"], - "%Y%m%d%H%M%S") - - if("Delete" in propDict): - propDict["Delete"] = time.strptime(propDict["Delete"], - "%Y%m%d%H%M%S") - - if("Revoke" in propDict): - propDict["Revoke"] = time.strptime(propDict["Revoke"], - "%Y%m%d%H%M%S") - pfp.close() - self.metadata = propDict - - def showkey(self): - return "%s/%03d/%05d" % (self.zone, self.alg, self.keyid); - - def showkeytype(self): - return ("KSK" if self.sep else "ZSK") - - # ensure that the gap between Publish and Activate is big enough - def check_prepub(self): - now = time.time() - - if (not "Activate" in self.metadata): - debug_print("No Activate information in key: %s" % self.showkey()) - return False - a = calendar.timegm(self.metadata["Activate"]) - - if (not "Publish" in self.metadata): - debug_print("No Publish information in key: %s" % self.showkey()) - if a > now: - vspace() - print("WARNING: Key %s (%s) is scheduled for activation but \n" - "\t not for publication." % - (self.showkey(), self.showkeytype())) - return False - p = calendar.timegm(self.metadata["Publish"]) - - now = time.time() - if p < now and a < now: - return True - - if p == a: - vspace() - print ("WARNING: %s (%s) is scheduled to be published and\n" - "\t activated at the same time. This could result in a\n" - "\t coverage gap if the zone was previously signed." % - (self.showkey(), self.showkeytype())) - print("\t Activation should be at least %s after publication." - % duration(self.ttl)) - return True - - if a < p: - vspace() - print("WARNING: Key %s (%s) is active before it is published" % - (self.showkey(), self.showkeytype())) - return False - - if (a - p < self.ttl): - vspace() - print("WARNING: Key %s (%s) is activated too soon after\n" - "\t publication; this could result in coverage gaps due to\n" - "\t resolver caches containing old data." - % (self.showkey(), self.showkeytype())) - print("\t Activation should be at least %s after publication." % - duration(self.ttl)) - return False - - return True - - # ensure that the gap between Inactive and Delete is big enough - def check_postpub(self, timespan = None): - if not timespan: - timespan = self.ttl - - now = time.time() - - if (not "Delete" in self.metadata): - debug_print("No Delete information in key: %s" % self.showkey()) - return False - d = calendar.timegm(self.metadata["Delete"]) - - if (not "Inactive" in self.metadata): - debug_print("No Inactive information in key: %s" % self.showkey()) - if d > now: - vspace() - print("WARNING: Key %s (%s) is scheduled for deletion but\n" - "\t not for inactivation." % - (self.showkey(), self.showkeytype())) - return False - i = calendar.timegm(self.metadata["Inactive"]) - - if d < now and i < now: - return True - - if (d < i): - vspace() - print("WARNING: Key %s (%s) is scheduled for deletion before\n" - "\t inactivation." % (self.showkey(), self.showkeytype())) - return False - - if (d - i < timespan): - vspace() - print("WARNING: Key %s (%s) scheduled for deletion too soon after\n" - "\t deactivation; this may result in coverage gaps due to\n" - "\t resolver caches containing old data." - % (self.showkey(), self.showkeytype())) - print("\t Deletion should be at least %s after inactivation." % - duration(timespan)) - return False - - return True - -######################################################################## -# class Zone -######################################################################## -class Zone: - """Stores data about a specific zone""" - - def __init__(self, _name, _keyttl = None, _maxttl = None): - self.name = _name - self.keyttl = _keyttl - self.maxttl = _maxttl - - def load(self, filename): - if not args.compilezone: - sys.stderr.write(prog + ': FATAL: "named-compilezone" not found\n') - exit(1) - - if not self.name: - return - - maxttl = keyttl = None - - fp = os.popen("%s -o - %s %s 2> /dev/null" % - (args.compilezone, self.name, filename)) - for line in fp: - fields = line.split() - if not maxttl or int(fields[1]) > maxttl: - maxttl = int(fields[1]) - if fields[3] == "DNSKEY": - keyttl = int(fields[1]) - fp.close() - - self.keyttl = keyttl - self.maxttl = maxttl - -############################################################################ -# debug_print: -############################################################################ -def debug_print(debugVar): - """pretty print a variable iff debug mode is enabled""" - if not args.debug_mode: - return - if type(debugVar) == str: - print("DEBUG: " + debugVar) - else: - print("DEBUG: " + pprint.pformat(debugVar)) - return - -############################################################################ -# vspace: -############################################################################ -_firstline = True -def vspace(): - """adds vertical space between two sections of output text if and only - if this is *not* the first section being printed""" - global _firstline - if _firstline: - _firstline = False - else: - print('') - -############################################################################ -# vreset: -############################################################################ -def vreset(): - """reset vertical spacing""" - global _firstline - _firstline = True - -############################################################################ -# getunit -############################################################################ -def getunit(secs, size): - """given a number of seconds, and a number of seconds in a larger unit of - time, calculate how many of the larger unit there are and return both - that and a remainder value""" - bigunit = secs // size - if bigunit: - secs %= size - return (bigunit, secs) - -############################################################################ -# addtime -############################################################################ -def addtime(output, unit, t): - """add a formatted unit of time to an accumulating string""" - if t: - output += ("%s%d %s%s" % - ((", " if output else ""), - t, unit, ("s" if t > 1 else ""))) - - return output - -############################################################################ -# duration: -############################################################################ -def duration(secs): - """given a length of time in seconds, print a formatted human duration - in larger units of time - """ - # define units: - minute = 60 - hour = minute * 60 - day = hour * 24 - month = day * 30 - year = day * 365 - - # calculate time in units: - (years, secs) = getunit(secs, year) - (months, secs) = getunit(secs, month) - (days, secs) = getunit(secs, day) - (hours, secs) = getunit(secs, hour) - (minutes, secs) = getunit(secs, minute) - - output = '' - output = addtime(output, "year", years) - output = addtime(output, "month", months) - output = addtime(output, "day", days) - output = addtime(output, "hour", hours) - output = addtime(output, "minute", minutes) - output = addtime(output, "second", secs) - return output - -############################################################################ -# parse_time -############################################################################ -def parse_time(s): - """convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds""" - s = s.strip() - - # if s is an integer, we're done already - try: - n = int(s) - return n - except: - pass - - # try to parse as a number with a suffix indicating unit of time - r = re.compile('([0-9][0-9]*)\s*([A-Za-z]*)') - m = r.match(s) - if not m: - raise Exception("Cannot parse %s" % s) - (n, unit) = m.groups() - n = int(n) - unit = unit.lower() - if unit[0] == 'y': - return n * 31536000 - elif unit[0] == 'm' and unit[1] == 'o': - return n * 2592000 - elif unit[0] == 'w': - return n * 604800 - elif unit[0] == 'd': - return n * 86400 - elif unit[0] == 'h': - return n * 3600 - elif unit[0] == 'm' and unit[1] == 'i': - return n * 60 - elif unit[0] == 's': - return n - else: - raise Exception("Invalid suffix %s" % unit) - -############################################################################ -# algname: -############################################################################ -def algname(alg): - """return the mnemonic for a DNSSEC algorithm""" - names = (None, 'RSAMD5', 'DH', 'DSA', 'ECC', 'RSASHA1', - 'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None, - 'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256', - 'ECDSAP384SHA384') - name = None - if alg in range(len(names)): - name = names[alg] - return (name if name else str(alg)) - -############################################################################ -# list_events: -############################################################################ -def list_events(eventgroup): - """print a list of the events in an eventgroup""" - if not eventgroup: - return - print (" " + eventgroup[0].showtime() + ":") - for event in eventgroup: - print (" %s: %s (%s)" % - (event.what, event.showkey(), event.showkeytype())) - -############################################################################ -# process_events: -############################################################################ -def process_events(eventgroup, active, published): - """go through the events in an event group in time-order, add to active - list upon Activate event, add to published list upon Publish event, - remove from active list upon Inactive event, and remove from published - upon Delete event. Emit warnings when inconsistant states are reached""" - for event in eventgroup: - if event.what == "Activate": - active.add(event.keyid) - elif event.what == "Publish": - published.add(event.keyid) - elif event.what == "Inactive": - if event.keyid not in active: - vspace() - print ("\tWARNING: %s (%s) scheduled to become inactive " - "before it is active" % - (event.showkey(), event.showkeytype())) - else: - active.remove(event.keyid) - elif event.what == "Delete": - if event.keyid in published: - published.remove(event.keyid) - else: - vspace() - print ("WARNING: key %s (%s) is scheduled for deletion before " - "it is published, at %s" % - (event.showkey(), event.showkeytype())) - elif event.what == "Revoke": - # We don't need to worry about the logic of this one; - # just stop counting this key as either active or published - if event.keyid in published: - published.remove(event.keyid) - if event.keyid in active: - active.remove(event.keyid) - - return (active, published) - -############################################################################ -# check_events: -############################################################################ -def check_events(eventsList, ksk): - """create lists of events happening at the same time, check for - inconsistancies""" - active = set() - published = set() - eventgroups = list() - eventgroup = list() - keytype = ("KSK" if ksk else "ZSK") - - # collect up all events that have the same time - eventsfound = False - for event in eventsList: - # if checking ZSKs, skip KSKs, and vice versa - if (ksk and not event.sep) or (event.sep and not ksk): - continue - - # we found an appropriate (ZSK or KSK event) - eventsfound = True - - # add event to current eventgroup - if (not eventgroup or eventgroup[0].when == event.when): - eventgroup.append(event) - - # if we're at the end of the list, we're done. if - # we've found an event with a later time, start a new - # eventgroup - if (eventgroup[0].when != event.when): - eventgroups.append(eventgroup) - eventgroup = list() - eventgroup.append(event) - - if eventgroup: - eventgroups.append(eventgroup) - - for eventgroup in eventgroups: - if (args.checklimit and - calendar.timegm(eventgroup[0].when) > args.checklimit): - print("Ignoring events after %s" % - time.strftime("%a %b %d %H:%M:%S UTC %Y", - time.gmtime(args.checklimit))) - return True - - (active, published) = \ - process_events(eventgroup, active, published) - - list_events(eventgroup) - - # and then check for inconsistencies: - if len(active) == 0: - print ("ERROR: No %s's are active after this event" % keytype) - return False - elif len(published) == 0: - sys.stdout.write("ERROR: ") - print ("ERROR: No %s's are published after this event" % keytype) - return False - elif len(published.intersection(active)) == 0: - sys.stdout.write("ERROR: ") - print (("ERROR: No %s's are both active and published " + - "after this event") % keytype) - return False - - if not eventsfound: - print ("ERROR: No %s events found in '%s'" % - (keytype, args.path)) - return False - - return True - -############################################################################ -# check_zones: -# ############################################################################ -def check_zones(eventsList): - """scan events per zone, algorithm, and key type, in order of occurrance, - noting inconsistent states when found""" - global foundprob - - foundprob = False - zonesfound = False - for zone in eventsList: - if args.zone and zone != args.zone: - continue - - zonesfound = True - for alg in eventsList[zone]: - if not args.no_ksk: - vspace() - print("Checking scheduled KSK events for zone %s, algorithm %s..." % - (zone, algname(alg))) - if not check_events(eventsList[zone][alg], True): - foundprob = True - else: - print ("No errors found") - - if not args.no_zsk: - vspace() - print("Checking scheduled ZSK events for zone %s, algorithm %s..." % - (zone, algname(alg))) - if not check_events(eventsList[zone][alg], False): - foundprob = True - else: - print ("No errors found") - - if not zonesfound: - print("ERROR: No key events found for %s in '%s'" % - (args.zone, args.path)) - exit(1) - -############################################################################ -# fill_eventsList: -############################################################################ -def fill_eventsList(eventsList): - """populate the list of events""" - for zone, algorithms in keyDict.items(): - for alg, keys in algorithms.items(): - for keyid, keydata in keys.items(): - if("Publish" in keydata.metadata): - eventsList[zone][alg].append(Event("Publish", keydata)) - if("Activate" in keydata.metadata): - eventsList[zone][alg].append(Event("Activate", keydata)) - if("Inactive" in keydata.metadata): - eventsList[zone][alg].append(Event("Inactive", keydata)) - if("Delete" in keydata.metadata): - eventsList[zone][alg].append(Event("Delete", keydata)) - - eventsList[zone][alg] = sorted(eventsList[zone][alg], - key=lambda event: event.when) - - foundprob = False - if not keyDict: - print("ERROR: No key events found in '%s'" % args.path) - exit(1) - -############################################################################ -# set_path: -############################################################################ -def set_path(command, default=None): - """find the location of a specified command. if a default is supplied - and it works, we use it; otherwise we search PATH for a match. If - not found, error and exit""" - fpath = default - if not fpath or not os.path.isfile(fpath) or not os.access(fpath, os.X_OK): - path = os.environ["PATH"] - if not path: - path = os.path.defpath - for directory in path.split(os.pathsep): - fpath = directory + os.sep + command - if os.path.isfile(fpath) or os.access(fpath, os.X_OK): - break - fpath = None - - return fpath - -############################################################################ -# parse_args: -############################################################################ -def parse_args(): - """Read command line arguments, set global 'args' structure""" - global args - compilezone = set_path('named-compilezone', - os.path.join(prefix('bin'), 'named-compilezone')) - - parser = argparse.ArgumentParser(description=prog + ': checks future ' + - 'DNSKEY coverage for a zone') - - parser.add_argument('zone', type=str, help='zone to check') - parser.add_argument('-K', dest='path', default='.', type=str, - help='a directory containing keys to process', - metavar='dir') - parser.add_argument('-f', dest='filename', type=str, - help='zone master file', metavar='file') - parser.add_argument('-m', dest='maxttl', type=str, - help='the longest TTL in the zone(s)', - metavar='time') - parser.add_argument('-d', dest='keyttl', type=str, - help='the DNSKEY TTL', metavar='time') - parser.add_argument('-r', dest='resign', default='1944000', - type=str, help='the RRSIG refresh interval ' - 'in seconds [default: 22.5 days]', - metavar='time') - parser.add_argument('-c', dest='compilezone', - default=compilezone, type=str, - help='path to \'named-compilezone\'', - metavar='path') - parser.add_argument('-l', dest='checklimit', - type=str, default='0', - help='Length of time to check for ' - 'DNSSEC coverage [default: 0 (unlimited)]', - metavar='time') - parser.add_argument('-z', dest='no_ksk', - action='store_true', default=False, - help='Only check zone-signing keys (ZSKs)') - parser.add_argument('-k', dest='no_zsk', - action='store_true', default=False, - help='Only check key-signing keys (KSKs)') - parser.add_argument('-D', '--debug', dest='debug_mode', - action='store_true', default=False, - help='Turn on debugging output') - parser.add_argument('-v', '--version', action='version', - version='@BIND9_VERSION@') - - args = parser.parse_args() - - if args.no_zsk and args.no_ksk: - print("ERROR: -z and -k cannot be used together."); - exit(1) - - # convert from time arguments to seconds - try: - if args.maxttl: - m = parse_time(args.maxttl) - args.maxttl = m - except: - pass - - try: - if args.keyttl: - k = parse_time(args.keyttl) - args.keyttl = k - except: - pass - - try: - if args.resign: - r = parse_time(args.resign) - args.resign = r - except: - pass - - try: - if args.checklimit: - lim = args.checklimit - r = parse_time(args.checklimit) - if r == 0: - args.checklimit = None - else: - args.checklimit = time.time() + r - except: - pass - - # if we've got the values we need from the command line, stop now - if args.maxttl and args.keyttl: - return - - # load keyttl and maxttl data from zonefile - if args.zone and args.filename: - try: - zone = Zone(args.zone) - zone.load(args.filename) - if not args.maxttl: - args.maxttl = zone.maxttl - if not args.keyttl: - args.keyttl = zone.maxttl - except Exception as e: - print("Unable to load zone data from %s: " % args.filename, e) - - if not args.maxttl: - vspace() - print ("WARNING: Maximum TTL value was not specified. Using 1 week\n" - "\t (604800 seconds); re-run with the -m option to get more\n" - "\t accurate results.") - args.maxttl = 604800 - -############################################################################ -# Main -############################################################################ -def main(): - global keyDict - - parse_args() - path=args.path - - print ("PHASE 1--Loading keys to check for internal timing problems") - keyDict = defaultdict(lambda : defaultdict(dict)) - files = glob.glob(os.path.join(path, '*.private')) - for infile in files: - key = Key(infile) - if args.zone and key.zone != args.zone: - continue - keyDict[key.zone][key.alg][key.keyid] = key - key.check_prepub() - if key.sep: - key.check_postpub() - else: - key.check_postpub(args.maxttl + args.resign) - - vspace() - print ("PHASE 2--Scanning future key events for coverage failures") - vreset() - - eventsList = defaultdict(lambda : defaultdict(list)) - fill_eventsList(eventsList) - check_zones(eventsList) - - if foundprob: - exit(1) - else: - exit(0) +import isc.coverage if __name__ == "__main__": - main() + isc.coverage.main() diff --git a/bin/python/dnssec-keymgr.docbook b/bin/python/dnssec-keymgr.docbook new file mode 100644 index 00000000000..d0ca1c5684f --- /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 00000000000..23d563d71fe --- /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 00000000000..84554b8a90a --- /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 00000000000..425d054cceb --- /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 00000000000..0d79f356fd5 --- /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 00000000000..64ca12ebc6e --- /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 00000000000..c9e89596f77 --- /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 00000000000..f1559e72390 --- /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 00000000000..4c91368f12e --- /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 00000000000..cc73dc47ac6 --- /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 00000000000..9025feec4a3 --- /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 00000000000..a3a9043965a --- /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 00000000000..ed09f71fda3 --- /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 00000000000..7dfb31ac558 --- /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 00000000000..ed106c6c92a --- /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 00000000000..506f2cc992e --- /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 00000000000..2a63695ebe2 --- /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 00000000000..c35e023ad3b --- /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 00000000000..b54f1e33ae4 --- /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 00000000000..e4d11c2e54e --- /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 00000000000..a4c3a99e999 --- /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 00000000000..5695559b67f --- /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 00000000000..c5afbe27edb --- /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 00000000000..af22c6ad523 --- /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 00000000000..48b9685f333 --- /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 cac8c29ac6e..3e924723d1e 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}" @@ -71,7 +72,7 @@ SUBDIRS="acl additional allow_query addzone autosign builtin cookie @COVERAGE@ database digdelv dlv dlvauto dlz dlzexternal dname dns64 dnssec dsdigest dscp @DNSTAP@ dyndb ecdsa ednscompliance emptyzones fetchlimit filter-aaaa formerr forward geoip glue gost - ixfr inline legacy limits logfileconfig lwresd masterfile + ixfr inline @KEYMGR@ legacy limits logfileconfig lwresd masterfile masterformat metadata mkeys names notify nslookup nsupdate pending pipelined @PKCS11_TEST@ reclimit redirect resolver rndc rpz rpzrecurse rrl rrchecker rrsetorder rsabigexponent diff --git a/bin/tests/system/coverage/03-ksk-unpublished/expect b/bin/tests/system/coverage/03-ksk-unpublished/expect index 58c65bd1611..07bbff11d9a 100644 --- a/bin/tests/system/coverage/03-ksk-unpublished/expect +++ b/bin/tests/system/coverage/03-ksk-unpublished/expect @@ -3,5 +3,6 @@ warn=1 error=1 ok=1 retcode=1 -match="WARNING: Key .* (KSK) is scheduled for deletion before +match="WARNING: Key .* (KSK) is scheduled for +deletion before inactivation No KSK's are published" diff --git a/bin/tests/system/coverage/04-zsk-unpublished/expect b/bin/tests/system/coverage/04-zsk-unpublished/expect index 4ffa0b663e5..450ec24bff0 100644 --- a/bin/tests/system/coverage/04-zsk-unpublished/expect +++ b/bin/tests/system/coverage/04-zsk-unpublished/expect @@ -3,5 +3,6 @@ warn=1 error=1 ok=1 retcode=1 -match="WARNING: Key .* (ZSK) is scheduled for deletion before +match="WARNING: Key .* (ZSK) is scheduled for +deletion before inactivation No ZSK's are published" diff --git a/bin/tests/system/coverage/05-ksk-unpub-active/expect b/bin/tests/system/coverage/05-ksk-unpub-active/expect index b6cf2d2c276..2edfa0e76ed 100644 --- a/bin/tests/system/coverage/05-ksk-unpub-active/expect +++ b/bin/tests/system/coverage/05-ksk-unpub-active/expect @@ -3,5 +3,6 @@ warn=1 error=1 ok=1 retcode=1 -match="WARNING: Key .* (KSK) is scheduled for deletion before +match="WARNING: Key .* (KSK) is scheduled for +deletion before inactivation No KSK's are both active and published" diff --git a/bin/tests/system/coverage/06-zsk-unpub-active/expect b/bin/tests/system/coverage/06-zsk-unpub-active/expect index ed2004d2684..0ef5b151e6c 100644 --- a/bin/tests/system/coverage/06-zsk-unpub-active/expect +++ b/bin/tests/system/coverage/06-zsk-unpub-active/expect @@ -3,5 +3,6 @@ warn=1 error=1 ok=1 retcode=1 -match="WARNING: Key .* (ZSK) is scheduled for deletion before +match="WARNING: Key .* (ZSK) is scheduled for +deletion before inactivation No ZSK's are both active and published" diff --git a/bin/tests/system/coverage/07-ksk-ttl/expect b/bin/tests/system/coverage/07-ksk-ttl/expect index 4cc3af21d93..eade21a75fd 100644 --- a/bin/tests/system/coverage/07-ksk-ttl/expect +++ b/bin/tests/system/coverage/07-ksk-ttl/expect @@ -3,5 +3,7 @@ warn=1 error=0 ok=2 retcode=0 -match="WARNING: Key .* (KSK) is activated too soon after -Activation should be at least 7 days after publication." +match="WARNING: Key .* (KSK) is activated too soon +after publication +Activation should be at least 7 days after +publication." diff --git a/bin/tests/system/coverage/08-zsk-ttl/expect b/bin/tests/system/coverage/08-zsk-ttl/expect index 0579769ab92..150c9cdc8fa 100644 --- a/bin/tests/system/coverage/08-zsk-ttl/expect +++ b/bin/tests/system/coverage/08-zsk-ttl/expect @@ -3,5 +3,7 @@ warn=1 error=0 ok=2 retcode=0 -match="WARNING: Key .* (ZSK) is activated too soon after -Activation should be at least 7 days after publication." +match="WARNING: Key .* (ZSK) is activated too soon +after publication +Activation should be at least 7 days after +publication." diff --git a/bin/tests/system/keymgr/01-ksk-inactive/README b/bin/tests/system/keymgr/01-ksk-inactive/README new file mode 100644 index 00000000000..b807029d4a5 --- /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 00000000000..b076310d449 --- /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 00000000000..bf56562b9ab --- /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 00000000000..b076310d449 --- /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 00000000000..0581c6756c7 --- /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 00000000000..b076310d449 --- /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 00000000000..589490d5977 --- /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 00000000000..b076310d449 --- /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 00000000000..026028c5b5c --- /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 00000000000..b076310d449 --- /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 00000000000..026028c5b5c --- /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 00000000000..b076310d449 --- /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 00000000000..8b9dc02503c --- /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 00000000000..de792a9d5ea --- /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 00000000000..8b9dc02503c --- /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 00000000000..de792a9d5ea --- /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 00000000000..6295fa32b06 --- /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 00000000000..de792a9d5ea --- /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 00000000000..26073c37790 --- /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 00000000000..de792a9d5ea --- /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 00000000000..8b9dc02503c --- /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 00000000000..de792a9d5ea --- /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 00000000000..8b9dc02503c --- /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 00000000000..f990a7a0270 --- /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 00000000000..8b9dc02503c --- /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 00000000000..40616e1a937 --- /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 00000000000..8b9dc02503c --- /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 00000000000..436f05fbc9f --- /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 00000000000..8b9dc02503c --- /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 00000000000..b1ff4fc3fe5 --- /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 00000000000..8b9dc02503c --- /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 00000000000..7a21decc0be --- /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 00000000000..8b9dc02503c --- /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 00000000000..a5bf1f1ec5c --- /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 00000000000..1f815b5ca91 --- /dev/null +++ b/bin/tests/system/keymgr/clean.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# +# Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +rm -f */K*.key +rm -f */K*.private +rm -f coverage.* keymgr.* +rm -f policy.out diff --git a/bin/tests/system/keymgr/policy.conf b/bin/tests/system/keymgr/policy.conf new file mode 100644 index 00000000000..e6b7d98c1aa --- /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 00000000000..0038a279398 --- /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 00000000000..d96a40dce90 --- /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 00000000000..fcf3a63a77e --- /dev/null +++ b/bin/tests/system/keymgr/prereq.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# +# Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +SYSTEMTESTTOP=.. +. $SYSTEMTESTTOP/conf.sh + +exec $SHELL ../testcrypto.sh diff --git a/bin/tests/system/keymgr/setup.sh b/bin/tests/system/keymgr/setup.sh new file mode 100644 index 00000000000..f35c6c1967a --- /dev/null +++ b/bin/tests/system/keymgr/setup.sh @@ -0,0 +1,212 @@ +#!/bin/sh +# +# Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +SYSTEMTESTTOP=.. +. $SYSTEMTESTTOP/conf.sh + +KEYGEN="$KEYGEN -qr $RANDFILE" + +$SHELL clean.sh + +# Test 1: KSK goes inactive before successor is active +dir=01-ksk-inactive +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +ksk1=`$KEYGEN -K $dir -3fk example.com` +$SETTIME -K $dir -I +9mo -D +1y $ksk1 > /dev/null 2>&1 +ksk2=`$KEYGEN -K $dir -S $ksk1` +$SETTIME -K $dir -I +7mo $ksk1 > /dev/null 2>&1 +zsk1=`$KEYGEN -K $dir -3 example.com` + +# Test 2: ZSK goes inactive before successor is active +dir=02-zsk-inactive +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +zsk1=`$KEYGEN -K $dir -3 example.com` +$SETTIME -K $dir -I +9mo -D +1y $zsk1 > /dev/null 2>&1 +zsk2=`$KEYGEN -K $dir -S $zsk1` +$SETTIME -K $dir -I +7mo $zsk1 > /dev/null 2>&1 +ksk1=`$KEYGEN -K $dir -3fk example.com` + +# Test 3: KSK is unpublished before its successor is published +dir=03-ksk-unpublished +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +ksk1=`$KEYGEN -K $dir -3fk example.com` +$SETTIME -K $dir -I +9mo -D +1y $ksk1 > /dev/null 2>&1 +ksk2=`$KEYGEN -K $dir -S $ksk1` +$SETTIME -K $dir -D +6mo $ksk1 > /dev/null 2>&1 +zsk1=`$KEYGEN -K $dir -3 example.com` + +# Test 4: ZSK is unpublished before its successor is published +dir=04-zsk-unpublished +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +zsk1=`$KEYGEN -K $dir -3 example.com` +$SETTIME -K $dir -I +9mo -D +1y $zsk1 > /dev/null 2>&1 +zsk2=`$KEYGEN -K $dir -S $zsk1` +$SETTIME -K $dir -D +6mo $zsk1 > /dev/null 2>&1 +ksk1=`$KEYGEN -K $dir -3fk example.com` + +# Test 5: KSK deleted and successor published before KSK is deactivated +# and successor activated. +dir=05-ksk-unpub-active +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +ksk1=`$KEYGEN -K $dir -3fk example.com` +$SETTIME -K $dir -I +9mo -D +8mo $ksk1 > /dev/null 2>&1 +ksk2=`$KEYGEN -K $dir -S $ksk1` +zsk1=`$KEYGEN -K $dir -3 example.com` + +# Test 6: ZSK deleted and successor published before ZSK is deactivated +# and successor activated. +dir=06-zsk-unpub-active +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +zsk1=`$KEYGEN -K $dir -3 example.com` +$SETTIME -K $dir -I +9mo -D +8mo $zsk1 > /dev/null 2>&1 +zsk2=`$KEYGEN -K $dir -S $zsk1` +ksk1=`$KEYGEN -K $dir -3fk example.com` + +# Test 7: KSK rolled with insufficient delay after prepublication. +dir=07-ksk-ttl +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +ksk1=`$KEYGEN -K $dir -3fk example.com` +$SETTIME -K $dir -I +9mo -D +1y $ksk1 > /dev/null 2>&1 +ksk2=`$KEYGEN -K $dir -S $ksk1` +$SETTIME -K $dir -P +269d $ksk2 > /dev/null 2>&1 +zsk1=`$KEYGEN -K $dir -3 example.com` + +# Test 8: ZSK rolled with insufficient delay after prepublication. +dir=08-zsk-ttl +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +zsk1=`$KEYGEN -K $dir -3 example.com` +$SETTIME -K $dir -I +9mo -D +1y $zsk1 > /dev/null 2>&1 +zsk2=`$KEYGEN -K $dir -S $zsk1` +# allow only 1 day between publication and activation +$SETTIME -K $dir -P +269d $zsk2 > /dev/null 2>&1 +ksk1=`$KEYGEN -K $dir -3fk example.com` + +# Test 9: No special preparation needed +rm -f $dir/K*.key +rm -f $dir/K*.private + +# Test 10: Valid key set, but rollover period has changed +dir=10-change-roll +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +ksk1=`$KEYGEN -K $dir -3fk example.com` +zsk1=`$KEYGEN -K $dir -3 example.com` +$SETTIME -K $dir -I +3mo -D +4mo $zsk1 > /dev/null 2>&1 +zsk2=`$KEYGEN -K $dir -S $zsk1` + +# Test 11: Many keys all simultaneously scheduled to be active in the future +dir=11-many-simul +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +k1=`$KEYGEN -K $dir -q3fk -P now+1mo -A now+1mo example.com` +z1=`$KEYGEN -K $dir -q3 -P now+1mo -A now+1mo example.com` +z2=`$KEYGEN -K $dir -q3 -P now+1mo -A now+1mo example.com` +z3=`$KEYGEN -K $dir -q3 -P now+1mo -A now+1mo example.com` +z4=`$KEYGEN -K $dir -q3 -P now+1mo -A now+1mo example.com` + +# Test 12: Many keys all simultaneously scheduled to be active in the past +dir=12-many-active +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +k1=`$KEYGEN -K $dir -q3fk example.com` +z1=`$KEYGEN -K $dir -q3 example.com` +z2=`$KEYGEN -K $dir -q3 example.com` +z3=`$KEYGEN -K $dir -q3 example.com` +z4=`$KEYGEN -K $dir -q3 example.com` + +# Test 13: Multiple simultaneous keys with no configured roll period +dir=13-noroll +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +k1=`$KEYGEN -K $dir -q3fk example.com` +k2=`$KEYGEN -K $dir -q3fk example.com` +k3=`$KEYGEN -K $dir -q3fk example.com` +z1=`$KEYGEN -K $dir -q3 example.com` + +# Test 14: Keys exist but have the wrong algorithm +dir=14-wrongalg +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +k1=`$KEYGEN -K $dir -qfk example.com` +z1=`$KEYGEN -K $dir -q example.com` +$SETTIME -K $dir -I now+6mo -D now+8mo $z1 > /dev/null +z2=`$KEYGEN -K $dir -q -S ${z1}.key` +$SETTIME -K $dir -I now+1y -D now+14mo $z2 > /dev/null +z3=`$KEYGEN -K $dir -q -S ${z2}.key` +$SETTIME -K $dir -I now+18mo -D now+20mo $z3 > /dev/null +z4=`$KEYGEN -K $dir -q -S ${z3}.key` + +# Test 15: No zones specified; just search the directory for keys +dir=15-unspec +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +k1=`$KEYGEN -K $dir -q3fk example.com` +z1=`$KEYGEN -K $dir -q3 example.com` +$SETTIME -K $dir -I now+6mo -D now+8mo $z1 > /dev/null +z2=`$KEYGEN -K $dir -q -S ${z1}.key` +$SETTIME -K $dir -I now+1y -D now+14mo $z2 > /dev/null +z3=`$KEYGEN -K $dir -q -S ${z2}.key` +$SETTIME -K $dir -I now+18mo -D now+20mo $z3 > /dev/null +z4=`$KEYGEN -K $dir -q -S ${z3}.key` + +# Test 16: No zones specified; search the directory for keys; +# keys have the wrong algorithm for their policies +dir=16-wrongalg-unspec +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +k1=`$KEYGEN -K $dir -qfk example.com` +z1=`$KEYGEN -K $dir -q example.com` +$SETTIME -K $dir -I now+6mo -D now+8mo $z1 > /dev/null +z2=`$KEYGEN -K $dir -q -S ${z1}.key` +$SETTIME -K $dir -I now+1y -D now+14mo $z2 > /dev/null +z3=`$KEYGEN -K $dir -q -S ${z2}.key` +$SETTIME -K $dir -I now+18mo -D now+20mo $z3 > /dev/null +z4=`$KEYGEN -K $dir -q -S ${z3}.key` + +# Test 17: Keys are simultaneously active but we run with no force +# flag (this should fail) +dir=17-noforce +echo I:set up $dir +rm -f $dir/K*.key +rm -f $dir/K*.private +k1=`$KEYGEN -K $dir -q3fk example.com` +z1=`$KEYGEN -K $dir -q3 example.com` +z2=`$KEYGEN -K $dir -q3 example.com` +z3=`$KEYGEN -K $dir -q3 example.com` +z4=`$KEYGEN -K $dir -q3 example.com` diff --git a/bin/tests/system/keymgr/testpolicy.py b/bin/tests/system/keymgr/testpolicy.py new file mode 100644 index 00000000000..2dec7ff5f32 --- /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 00000000000..f598f0a2527 --- /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 02fc7a36faf..36097c774e6 100755 --- a/configure +++ b/configure @@ -875,7 +875,9 @@ ISC_PLATFORM_NORETURN_POST ISC_PLATFORM_NORETURN_PRE ISC_PLATFORM_HAVELONGLONG ISC_SOCKADDR_LEN_T +expanded_sysconfdir PYTHON_TOOLS +KEYMGR COVERAGE CHECKDS PYTHON @@ -11805,8 +11807,13 @@ fi python="python python3 python3.4 python3.3 python3.2 python3.1 python3.0 python2 python2.7 python2.6 python2.5 python2.4" -testscript='try: import argparse + +testargparse='try: import argparse +except: exit(1)' + +testply='try: from ply import * except: exit(1)' + case "$use_python" in no) { $as_echo "$as_me:${as_lineno-$LINENO}: checking for python support" >&5 @@ -11869,9 +11876,22 @@ done fi { $as_echo "$as_me:${as_lineno-$LINENO}: checking python module 'argparse'" >&5 $as_echo_n "checking python module 'argparse'... " >&6; } - if ${PYTHON:-false} -c "$testscript"; then - { $as_echo "$as_me:${as_lineno-$LINENO}: result: found, using $PYTHON" >&5 -$as_echo "found, using $PYTHON" >&6; } + if ${PYTHON:-false} -c "$testargparse"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: found" >&5 +$as_echo "found" >&6; } + else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: not found" >&5 +$as_echo "not found" >&6; } + unset ac_cv_path_PYTHON + unset PYTHON + continue + fi + + { $as_echo "$as_me:${as_lineno-$LINENO}: checking python module 'ply'" >&5 +$as_echo_n "checking python module 'ply'... " >&6; } + if ${PYTHON:-false} -c "$testply"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: found" >&5 +$as_echo "found" >&6; } break fi { $as_echo "$as_me:${as_lineno-$LINENO}: result: not found" >&5 @@ -11949,7 +11969,16 @@ done esac { $as_echo "$as_me:${as_lineno-$LINENO}: checking python module 'argparse'" >&5 $as_echo_n "checking python module 'argparse'... " >&6; } - if ${PYTHON:-false} -c "$testscript"; then + if ${PYTHON:-false} -c "$testargparse"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: found, using $PYTHON" >&5 +$as_echo "found, using $PYTHON" >&6; } + break + else + as_fn_error $? "not found" "$LINENO" 5 + fi + { $as_echo "$as_me:${as_lineno-$LINENO}: checking python module 'ply'" >&5 +$as_echo_n "checking python module 'ply'... " >&6; } + if ${PYTHON:-false} -c "$testply"; then { $as_echo "$as_me:${as_lineno-$LINENO}: result: found, using $PYTHON" >&5 $as_echo "found, using $PYTHON" >&6; } break @@ -11964,15 +11993,18 @@ esac PYTHON_TOOLS='' CHECKDS='' COVERAGE='' +KEYMGR='' if test "X$PYTHON" != "X"; then PYTHON_TOOLS=python CHECKDS=checkds COVERAGE=coverage + KEYMGR=keymgr fi + # # Special processing of paths depending on whether --prefix, # --sysconfdir or --localstatedir arguments were given. What's @@ -12007,6 +12039,8 @@ case "$prefix" in esac ;; esac +expanded_sysconfdir=`eval echo $sysconfdir` + # # Make sure INSTALL uses an absolute path, else it will be wrong in all @@ -22666,7 +22700,7 @@ ac_config_commands="$ac_config_commands chmod" # elsewhere if there's a good reason for doing so. # -ac_config_files="$ac_config_files make/Makefile make/mkdep Makefile bin/Makefile bin/check/Makefile bin/confgen/Makefile bin/confgen/unix/Makefile bin/delv/Makefile bin/dig/Makefile bin/dnssec/Makefile bin/named/Makefile bin/named/unix/Makefile bin/nsupdate/Makefile bin/pkcs11/Makefile bin/python/Makefile bin/python/dnssec-checkds.py bin/python/dnssec-coverage.py bin/rndc/Makefile bin/tests/Makefile bin/tests/atomic/Makefile bin/tests/db/Makefile bin/tests/dst/Makefile bin/tests/dst/Kdh.+002+18602.key bin/tests/dst/Kdh.+002+18602.private bin/tests/dst/Kdh.+002+48957.key bin/tests/dst/Kdh.+002+48957.private bin/tests/dst/Ktest.+001+00002.key bin/tests/dst/Ktest.+001+54622.key bin/tests/dst/Ktest.+001+54622.private bin/tests/dst/Ktest.+003+23616.key bin/tests/dst/Ktest.+003+23616.private bin/tests/dst/Ktest.+003+49667.key bin/tests/dst/dst_2_data bin/tests/dst/t2_data_1 bin/tests/dst/t2_data_2 bin/tests/dst/t2_dsasig bin/tests/dst/t2_rsasig bin/tests/hashes/Makefile bin/tests/headerdep_test.sh bin/tests/master/Makefile bin/tests/mem/Makefile bin/tests/names/Makefile bin/tests/net/Makefile bin/tests/pkcs11/Makefile bin/tests/pkcs11/benchmarks/Makefile bin/tests/rbt/Makefile bin/tests/resolver/Makefile bin/tests/sockaddr/Makefile bin/tests/system/Makefile bin/tests/system/builtin/Makefile bin/tests/system/conf.sh bin/tests/system/dlz/prereq.sh bin/tests/system/dlzexternal/Makefile bin/tests/system/dlzexternal/ns1/named.conf bin/tests/system/dyndb/Makefile bin/tests/system/dyndb/driver/Makefile bin/tests/system/filter-aaaa/Makefile bin/tests/system/geoip/Makefile bin/tests/system/inline/checkdsa.sh bin/tests/system/lwresd/Makefile bin/tests/system/pipelined/Makefile bin/tests/system/resolver/Makefile bin/tests/system/rndc/Makefile bin/tests/system/rpz/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/statistics/Makefile bin/tests/system/tkey/Makefile bin/tests/system/tsiggss/Makefile bin/tests/tasks/Makefile bin/tests/timers/Makefile bin/tests/virtual-time/Makefile bin/tests/virtual-time/conf.sh bin/tools/Makefile contrib/scripts/check-secure-delegation.pl contrib/scripts/zone-edit.sh doc/Makefile doc/arm/Makefile doc/arm/noteversion.xml doc/arm/pkgversion.xml doc/arm/releaseinfo.xml doc/doxygen/Doxyfile doc/doxygen/Makefile doc/doxygen/doxygen-input-filter doc/misc/Makefile doc/tex/Makefile doc/tex/armstyle.sty doc/xsl/Makefile doc/xsl/isc-docbook-chunk.xsl doc/xsl/isc-docbook-html.xsl doc/xsl/isc-manpage.xsl doc/xsl/isc-notes-html.xsl isc-config.sh lib/Makefile lib/bind9/Makefile lib/bind9/include/Makefile lib/bind9/include/bind9/Makefile lib/dns/Makefile lib/dns/include/Makefile lib/dns/include/dns/Makefile lib/dns/include/dst/Makefile lib/dns/tests/Makefile lib/irs/Makefile lib/irs/include/Makefile lib/irs/include/irs/Makefile lib/irs/include/irs/netdb.h lib/irs/include/irs/platform.h lib/isc/$arch/Makefile lib/isc/$arch/include/Makefile lib/isc/$arch/include/isc/Makefile lib/isc/$thread_dir/Makefile lib/isc/$thread_dir/include/Makefile lib/isc/$thread_dir/include/isc/Makefile lib/isc/Makefile lib/isc/include/Makefile lib/isc/include/isc/Makefile lib/isc/include/isc/platform.h lib/isc/include/pk11/Makefile lib/isc/include/pkcs11/Makefile lib/isc/tests/Makefile lib/isc/nls/Makefile lib/isc/unix/Makefile lib/isc/unix/include/Makefile lib/isc/unix/include/isc/Makefile lib/isc/unix/include/pkcs11/Makefile lib/isccc/Makefile lib/isccc/include/Makefile lib/isccc/include/isccc/Makefile lib/isccfg/Makefile lib/isccfg/include/Makefile lib/isccfg/include/isccfg/Makefile lib/lwres/Makefile lib/lwres/include/Makefile lib/lwres/include/lwres/Makefile lib/lwres/include/lwres/netdb.h lib/lwres/include/lwres/platform.h lib/lwres/man/Makefile lib/lwres/tests/Makefile lib/lwres/unix/Makefile lib/lwres/unix/include/Makefile lib/lwres/unix/include/lwres/Makefile lib/tests/Makefile lib/tests/include/Makefile lib/tests/include/tests/Makefile lib/samples/Makefile lib/samples/Makefile-postinstall unit/Makefile unit/unittest.sh" +ac_config_files="$ac_config_files make/Makefile make/mkdep Makefile bin/Makefile bin/check/Makefile bin/confgen/Makefile bin/confgen/unix/Makefile bin/delv/Makefile bin/dig/Makefile bin/dnssec/Makefile bin/named/Makefile bin/named/unix/Makefile bin/nsupdate/Makefile bin/pkcs11/Makefile bin/python/Makefile bin/python/isc/Makefile bin/python/isc/utils.py bin/python/isc/tests/Makefile bin/python/dnssec-checkds.py bin/python/dnssec-coverage.py bin/python/dnssec-keymgr.py bin/rndc/Makefile bin/tests/Makefile bin/tests/atomic/Makefile bin/tests/db/Makefile bin/tests/dst/Makefile bin/tests/dst/Kdh.+002+18602.key bin/tests/dst/Kdh.+002+18602.private bin/tests/dst/Kdh.+002+48957.key bin/tests/dst/Kdh.+002+48957.private bin/tests/dst/Ktest.+001+00002.key bin/tests/dst/Ktest.+001+54622.key bin/tests/dst/Ktest.+001+54622.private bin/tests/dst/Ktest.+003+23616.key bin/tests/dst/Ktest.+003+23616.private bin/tests/dst/Ktest.+003+49667.key bin/tests/dst/dst_2_data bin/tests/dst/t2_data_1 bin/tests/dst/t2_data_2 bin/tests/dst/t2_dsasig bin/tests/dst/t2_rsasig bin/tests/hashes/Makefile bin/tests/headerdep_test.sh bin/tests/master/Makefile bin/tests/mem/Makefile bin/tests/names/Makefile bin/tests/net/Makefile bin/tests/pkcs11/Makefile bin/tests/pkcs11/benchmarks/Makefile bin/tests/rbt/Makefile bin/tests/resolver/Makefile bin/tests/sockaddr/Makefile bin/tests/system/Makefile bin/tests/system/builtin/Makefile bin/tests/system/conf.sh bin/tests/system/dlz/prereq.sh bin/tests/system/dlzexternal/Makefile bin/tests/system/dlzexternal/ns1/named.conf bin/tests/system/dyndb/Makefile bin/tests/system/dyndb/driver/Makefile bin/tests/system/filter-aaaa/Makefile bin/tests/system/geoip/Makefile bin/tests/system/inline/checkdsa.sh bin/tests/system/lwresd/Makefile bin/tests/system/pipelined/Makefile bin/tests/system/resolver/Makefile bin/tests/system/rndc/Makefile bin/tests/system/rpz/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/statistics/Makefile bin/tests/system/tkey/Makefile bin/tests/system/tsiggss/Makefile bin/tests/tasks/Makefile bin/tests/timers/Makefile bin/tests/virtual-time/Makefile bin/tests/virtual-time/conf.sh bin/tools/Makefile contrib/scripts/check-secure-delegation.pl contrib/scripts/zone-edit.sh doc/Makefile doc/arm/Makefile doc/arm/noteversion.xml doc/arm/pkgversion.xml doc/arm/releaseinfo.xml doc/doxygen/Doxyfile doc/doxygen/Makefile doc/doxygen/doxygen-input-filter doc/misc/Makefile doc/tex/Makefile doc/tex/armstyle.sty doc/xsl/Makefile doc/xsl/isc-docbook-chunk.xsl doc/xsl/isc-docbook-html.xsl doc/xsl/isc-manpage.xsl doc/xsl/isc-notes-html.xsl isc-config.sh lib/Makefile lib/bind9/Makefile lib/bind9/include/Makefile lib/bind9/include/bind9/Makefile lib/dns/Makefile lib/dns/include/Makefile lib/dns/include/dns/Makefile lib/dns/include/dst/Makefile lib/dns/tests/Makefile lib/irs/Makefile lib/irs/include/Makefile lib/irs/include/irs/Makefile lib/irs/include/irs/netdb.h lib/irs/include/irs/platform.h lib/isc/$arch/Makefile lib/isc/$arch/include/Makefile lib/isc/$arch/include/isc/Makefile lib/isc/$thread_dir/Makefile lib/isc/$thread_dir/include/Makefile lib/isc/$thread_dir/include/isc/Makefile lib/isc/Makefile lib/isc/include/Makefile lib/isc/include/isc/Makefile lib/isc/include/isc/platform.h lib/isc/include/pk11/Makefile lib/isc/include/pkcs11/Makefile lib/isc/tests/Makefile lib/isc/nls/Makefile lib/isc/unix/Makefile lib/isc/unix/include/Makefile lib/isc/unix/include/isc/Makefile lib/isc/unix/include/pkcs11/Makefile lib/isccc/Makefile lib/isccc/include/Makefile lib/isccc/include/isccc/Makefile lib/isccfg/Makefile lib/isccfg/include/Makefile lib/isccfg/include/isccfg/Makefile lib/lwres/Makefile lib/lwres/include/Makefile lib/lwres/include/lwres/Makefile lib/lwres/include/lwres/netdb.h lib/lwres/include/lwres/platform.h lib/lwres/man/Makefile lib/lwres/tests/Makefile lib/lwres/unix/Makefile lib/lwres/unix/include/Makefile lib/lwres/unix/include/lwres/Makefile lib/tests/Makefile lib/tests/include/Makefile lib/tests/include/tests/Makefile lib/samples/Makefile lib/samples/Makefile-postinstall unit/Makefile unit/unittest.sh" # @@ -23675,8 +23709,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/tests/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/Makefile" ;; "bin/tests/atomic/Makefile") CONFIG_FILES="$CONFIG_FILES bin/tests/atomic/Makefile" ;; diff --git a/configure.in b/configure.in index 4cfce95a0e1..413fd26d175 100644 --- a/configure.in +++ b/configure.in @@ -223,8 +223,13 @@ AC_ARG_WITH(python, use_python="$withval", use_python="unspec") python="python python3 python3.4 python3.3 python3.2 python3.1 python3.0 python2 python2.7 python2.6 python2.5 python2.4" -testscript='try: import argparse + +testargparse='try: import argparse +except: exit(1)' + +testply='try: from ply import * except: exit(1)' + case "$use_python" in no) AC_MSG_CHECKING([for python support]) @@ -240,8 +245,18 @@ case "$use_python" in continue; fi AC_MSG_CHECKING([python module 'argparse']) - if ${PYTHON:-false} -c "$testscript"; then - AC_MSG_RESULT([found, using $PYTHON]) + if ${PYTHON:-false} -c "$testargparse"; then + AC_MSG_RESULT([found]) + else + AC_MSG_RESULT([not found]) + unset ac_cv_path_PYTHON + unset PYTHON + continue + fi + + AC_MSG_CHECKING([python module 'ply']) + if ${PYTHON:-false} -c "$testply"; then + AC_MSG_RESULT([found]) break fi AC_MSG_RESULT([not found]) @@ -271,7 +286,14 @@ case "$use_python" in ;; esac AC_MSG_CHECKING([python module 'argparse']) - if ${PYTHON:-false} -c "$testscript"; then + if ${PYTHON:-false} -c "$testargparse"; then + AC_MSG_RESULT([found, using $PYTHON]) + break + else + AC_MSG_ERROR([not found]) + fi + AC_MSG_CHECKING([python module 'ply']) + if ${PYTHON:-false} -c "$testply"; then AC_MSG_RESULT([found, using $PYTHON]) break else @@ -285,13 +307,16 @@ esac PYTHON_TOOLS='' CHECKDS='' COVERAGE='' +KEYMGR='' if test "X$PYTHON" != "X"; then PYTHON_TOOLS=python CHECKDS=checkds COVERAGE=coverage + KEYMGR=keymgr fi AC_SUBST(CHECKDS) AC_SUBST(COVERAGE) +AC_SUBST(KEYMGR) AC_SUBST(PYTHON_TOOLS) # @@ -328,6 +353,8 @@ case "$prefix" in esac ;; esac +expanded_sysconfdir=`eval echo $sysconfdir` +AC_SUBST(expanded_sysconfdir) # # Make sure INSTALL uses an absolute path, else it will be wrong in all @@ -4843,8 +4870,12 @@ AC_CONFIG_FILES([ bin/nsupdate/Makefile bin/pkcs11/Makefile bin/python/Makefile + bin/python/isc/Makefile + bin/python/isc/utils.py + bin/python/isc/tests/Makefile bin/python/dnssec-checkds.py bin/python/dnssec-coverage.py + bin/python/dnssec-keymgr.py bin/rndc/Makefile bin/tests/Makefile bin/tests/atomic/Makefile diff --git a/contrib/kasp/README b/contrib/kasp/README new file mode 100644 index 00000000000..fb897f1073a --- /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 00000000000..d94b0843ec6 --- /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 00000000000..b78a968f43a --- /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 00000000000..18c636083ce --- /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 index 001e0e0c4d9..07776b0fafc 100644 --- a/doc/arm/notes.xml +++ b/doc/arm/notes.xml @@ -1,6 +1,7 @@ + ]>