[v9_10] refactor python tools

4348.	[cleanup]	Refactor dnssec-coverage and dnssec-checkds
			functionality into an "isc" python module. [RT #39211]
This commit is contained in:
Evan Hunt
2016-04-28 19:45:35 -07:00
parent e34864bb31
commit fa7311e107
25 changed files with 1923 additions and 1117 deletions

View File

@@ -2,3 +2,4 @@ dnssec-checkds
dnssec-checkds.py
dnssec-coverage
dnssec-coverage.py
*.pyc

View File

@@ -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

View File

@@ -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()

View File

@@ -56,7 +56,7 @@
<arg choice="opt" rep="norepeat"><option>-c <replaceable class="parameter">compilezone path</replaceable></option></arg>
<arg choice="opt" rep="norepeat"><option>-k</option></arg>
<arg choice="opt" rep="norepeat"><option>-z</option></arg>
<arg choice="opt" rep="norepeat">zone</arg>
<arg choice="opt" rep="repeat">zone</arg>
</cmdsynopsis>
</refsynopsisdiv>
@@ -151,10 +151,15 @@
'd' for days, 'w' for weeks, 'mo' for months, 'y' for years.
</para>
<para>
This option is mandatory unless the <option>-f</option> has
been used to specify a zone file. (If <option>-f</option> has
This option is not necessary if the <option>-f</option> has
been used to specify a zone file. If <option>-f</option> has
been specified, this option may still be used; it will override
the value found in the file.)
the value found in the file.
</para>
<para>
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.
</para>
</listitem>
</varlistentry>
@@ -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.
</para>
<para>
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.
</para>
<para>
This option is mandatory unless the <option>-f</option> has
been used to specify a zone file, or a default key TTL was
set with the <option>-L</option> to
<command>dnssec-keygen</command>. (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 <option>-f</option> 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 <option>-L</option> to
<command>dnssec-keygen</command>. 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.
</para>
<para>
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.
</para>
</listitem>
</varlistentry>

782
bin/python/dnssec-coverage.py.in Executable file → Normal file
View File

@@ -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()

3
bin/python/isc/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
utils.py
parsetab.py
parser.out

View File

@@ -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

View File

@@ -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 *

189
bin/python/isc/checkds.py Normal file
View File

@@ -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)

292
bin/python/isc/coverage.py Normal file
View File

@@ -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)

504
bin/python/isc/dnskey.py Normal file
View File

@@ -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)

171
bin/python/isc/eventlist.py Normal file
View File

@@ -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

89
bin/python/isc/keydict.py Normal file
View File

@@ -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)

View File

@@ -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

194
bin/python/isc/keyseries.py Normal file
View File

@@ -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)))

60
bin/python/isc/keyzone.py Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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=

View File

@@ -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

View File

@@ -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@'