From fa7311e107b22876dd4a2898e8cef9b5ad0fc5c3 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Thu, 28 Apr 2016 19:45:35 -0700 Subject: [PATCH] [v9_10] refactor python tools 4348. [cleanup] Refactor dnssec-coverage and dnssec-checkds functionality into an "isc" python module. [RT #39211] --- CHANGES | 3 + bin/dnssec/dnssec-settime.c | 7 +- bin/python/.gitignore | 1 + bin/python/Makefile.in | 8 +- bin/python/dnssec-checkds.py.in | 311 +------ bin/python/dnssec-coverage.docbook | 40 +- bin/python/dnssec-coverage.py.in | 782 +----------------- bin/python/isc/.gitignore | 3 + bin/python/isc/Makefile.in | 58 ++ bin/python/isc/__init__.py | 24 + bin/python/isc/checkds.py | 189 +++++ bin/python/isc/coverage.py | 292 +++++++ bin/python/isc/dnskey.py | 504 +++++++++++ bin/python/isc/eventlist.py | 171 ++++ bin/python/isc/keydict.py | 89 ++ bin/python/isc/keyevent.py | 81 ++ bin/python/isc/keyseries.py | 194 +++++ bin/python/isc/keyzone.py | 60 ++ bin/python/isc/tests/Makefile.in | 33 + bin/python/isc/tests/dnskey_test.py | 57 ++ .../testdata/Kexample.com.+007+35529.key | 8 + .../testdata/Kexample.com.+007+35529.private | 18 + bin/python/isc/utils.py.in | 57 ++ configure | 28 +- configure.in | 22 +- 25 files changed, 1923 insertions(+), 1117 deletions(-) mode change 100755 => 100644 bin/python/dnssec-coverage.py.in create mode 100644 bin/python/isc/.gitignore create mode 100644 bin/python/isc/Makefile.in create mode 100644 bin/python/isc/__init__.py create mode 100644 bin/python/isc/checkds.py create mode 100644 bin/python/isc/coverage.py create mode 100644 bin/python/isc/dnskey.py create mode 100644 bin/python/isc/eventlist.py create mode 100644 bin/python/isc/keydict.py create mode 100644 bin/python/isc/keyevent.py create mode 100644 bin/python/isc/keyseries.py create mode 100644 bin/python/isc/keyzone.py create mode 100644 bin/python/isc/tests/Makefile.in create mode 100644 bin/python/isc/tests/dnskey_test.py create mode 100644 bin/python/isc/tests/testdata/Kexample.com.+007+35529.key create mode 100644 bin/python/isc/tests/testdata/Kexample.com.+007+35529.private create mode 100644 bin/python/isc/utils.py.in diff --git a/CHANGES b/CHANGES index 58988d631a..d8cc4fc558 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,9 @@ 4350. [contrib] Declare result in dlz_filesystem_dynamic.c. +4348. [cleanup] Refactor dnssec-coverage and dnssec-checkds + functionality into an "isc" python module. [RT #39211] + --- 9.10.4 released --- --- 9.10.4rc1 released --- diff --git a/bin/dnssec/dnssec-settime.c b/bin/dnssec/dnssec-settime.c index 67b868afad..4259be4bd9 100644 --- a/bin/dnssec/dnssec-settime.c +++ b/bin/dnssec/dnssec-settime.c @@ -472,11 +472,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 f770f2396e..f7bf29a43b 100644 --- a/bin/python/.gitignore +++ b/bin/python/.gitignore @@ -2,3 +2,4 @@ dnssec-checkds dnssec-checkds.py dnssec-coverage dnssec-coverage.py +*.pyc diff --git a/bin/python/Makefile.in b/bin/python/Makefile.in index dfb5d59711..cabadaccbc 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,6 +20,8 @@ top_srcdir = @top_srcdir@ PYTHON = @PYTHON@ +SUBDIRS = isc + TARGETS = dnssec-checkds dnssec-coverage PYSRCS = dnssec-checkds.py dnssec-coverage.py @@ -49,8 +49,8 @@ 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_DATA} ${srcdir}/dnssec-checkds.8 ${DESTDIR}${mandir}/man8 ${INSTALL_DATA} ${srcdir}/dnssec-coverage.8 ${DESTDIR}${mandir}/man8 diff --git a/bin/python/dnssec-checkds.py.in b/bin/python/dnssec-checkds.py.in index 40b730ba07..79db6f1aa1 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 45d5fa86d1..892801ecd0 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 ccd01a9cc1..bbd8629d58 --- 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/isc/.gitignore b/bin/python/isc/.gitignore new file mode 100644 index 0000000000..84554b8a90 --- /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 0000000000..d5a45f546e --- /dev/null +++ b/bin/python/isc/Makefile.in @@ -0,0 +1,58 @@ +# 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 + __init__.pyc dnskey.pyc eventlist.py keydict.py \ + keyevent.pyc keyzone.pyc + +@BIND9_MAKE_RULES@ + +%.pyc: %.py + $(PYTHON) -m compileall . + +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} + +check test: subdirs + +clean distclean:: + rm -f *.pyc + +distclean:: + rm -Rf utils.py diff --git a/bin/python/isc/__init__.py b/bin/python/isc/__init__.py new file mode 100644 index 0000000000..3bef6f36f1 --- /dev/null +++ b/bin/python/isc/__init__.py @@ -0,0 +1,24 @@ +# 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', '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.utils import * diff --git a/bin/python/isc/checkds.py b/bin/python/isc/checkds.py new file mode 100644 index 0000000000..64ca12ebc6 --- /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 0000000000..c9e89596f7 --- /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 0000000000..f1559e7239 --- /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 0000000000..4c91368f12 --- /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 0000000000..cc73dc47ac --- /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 0000000000..9025feec4a --- /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/keyseries.py b/bin/python/isc/keyseries.py new file mode 100644 index 0000000000..ed09f71fda --- /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 0000000000..7dfb31ac55 --- /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/tests/Makefile.in b/bin/python/isc/tests/Makefile.in new file mode 100644 index 0000000000..7c3e4f9fba --- /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 + +@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 0000000000..2a63695ebe --- /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/testdata/Kexample.com.+007+35529.key b/bin/python/isc/tests/testdata/Kexample.com.+007+35529.key new file mode 100644 index 0000000000..c5afbe27ed --- /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 0000000000..af22c6ad52 --- /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 0000000000..48b9685f33 --- /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/configure b/configure index 6779cc31a0..eda6f68375 100755 --- a/configure +++ b/configure @@ -868,6 +868,7 @@ ISC_PLATFORM_NORETURN_POST ISC_PLATFORM_NORETURN_PRE ISC_PLATFORM_HAVELONGLONG ISC_SOCKADDR_LEN_T +expanded_sysconfdir PYTHON_TOOLS COVERAGE CHECKDS @@ -11795,8 +11796,10 @@ 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)' + case "$use_python" in no) { $as_echo "$as_me:${as_lineno-$LINENO}: checking for python support" >&5 @@ -11859,11 +11862,17 @@ 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; } - break + 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}: result: not found" >&5 $as_echo "not found" >&6; } unset ac_cv_path_PYTHON @@ -11939,7 +11948,7 @@ 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 @@ -11997,6 +12006,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 @@ -22207,7 +22218,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/conf.sh bin/tests/system/builtin/Makefile bin/tests/system/dlz/prereq.sh bin/tests/system/dlzexternal/Makefile bin/tests/system/dlzexternal/ns1/named.conf bin/tests/system/dlzredir/prereq.sh bin/tests/system/fetchlimit/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/rpz/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/sit/prereq.sh 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/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/conf.sh bin/tests/system/builtin/Makefile bin/tests/system/dlz/prereq.sh bin/tests/system/dlzexternal/Makefile bin/tests/system/dlzexternal/ns1/named.conf bin/tests/system/dlzredir/prereq.sh bin/tests/system/fetchlimit/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/rpz/Makefile bin/tests/system/rsabigexponent/Makefile bin/tests/system/sit/prereq.sh 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" # @@ -23216,6 +23227,9 @@ 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/rndc/Makefile") CONFIG_FILES="$CONFIG_FILES bin/rndc/Makefile" ;; diff --git a/configure.in b/configure.in index b79aab0a2a..f0dbbdcce3 100644 --- a/configure.in +++ b/configure.in @@ -224,8 +224,10 @@ 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)' + case "$use_python" in no) AC_MSG_CHECKING([for python support]) @@ -241,10 +243,15 @@ case "$use_python" in continue; fi AC_MSG_CHECKING([python module 'argparse']) - if ${PYTHON:-false} -c "$testscript"; then - AC_MSG_RESULT([found, using $PYTHON]) - break + 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_RESULT([not found]) unset ac_cv_path_PYTHON unset PYTHON @@ -272,7 +279,7 @@ 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 @@ -329,6 +336,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 @@ -4714,6 +4723,9 @@ 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/rndc/Makefile