[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:
1
bin/python/.gitignore
vendored
1
bin/python/.gitignore
vendored
@@ -2,3 +2,4 @@ dnssec-checkds
|
||||
dnssec-checkds.py
|
||||
dnssec-coverage
|
||||
dnssec-coverage.py
|
||||
*.pyc
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
782
bin/python/dnssec-coverage.py.in
Executable file → Normal 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
3
bin/python/isc/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
utils.py
|
||||
parsetab.py
|
||||
parser.out
|
||||
58
bin/python/isc/Makefile.in
Normal file
58
bin/python/isc/Makefile.in
Normal 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
|
||||
24
bin/python/isc/__init__.py
Normal file
24
bin/python/isc/__init__.py
Normal 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
189
bin/python/isc/checkds.py
Normal 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
292
bin/python/isc/coverage.py
Normal 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
504
bin/python/isc/dnskey.py
Normal 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
171
bin/python/isc/eventlist.py
Normal 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
89
bin/python/isc/keydict.py
Normal 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)
|
||||
81
bin/python/isc/keyevent.py
Normal file
81
bin/python/isc/keyevent.py
Normal 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
194
bin/python/isc/keyseries.py
Normal 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
60
bin/python/isc/keyzone.py
Normal 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
|
||||
33
bin/python/isc/tests/Makefile.in
Normal file
33
bin/python/isc/tests/Makefile.in
Normal 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
|
||||
57
bin/python/isc/tests/dnskey_test.py
Normal file
57
bin/python/isc/tests/dnskey_test.py
Normal 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()
|
||||
8
bin/python/isc/tests/testdata/Kexample.com.+007+35529.key
vendored
Normal file
8
bin/python/isc/tests/testdata/Kexample.com.+007+35529.key
vendored
Normal 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=
|
||||
18
bin/python/isc/tests/testdata/Kexample.com.+007+35529.private
vendored
Normal file
18
bin/python/isc/tests/testdata/Kexample.com.+007+35529.private
vendored
Normal 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
|
||||
57
bin/python/isc/utils.py.in
Normal file
57
bin/python/isc/utils.py.in
Normal 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@'
|
||||
Reference in New Issue
Block a user