2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-09-01 06:55:30 +00:00

[master] dnssec-keymgr

4349.   [contrib]       kasp2policy: A python script to create a DNSSEC
                        policy file from an OpenDNSSEC KASP XML file.

4348.	[func]		dnssec-keymgr: A new python-based DNSSEC key
			management utility, which reads a policy definition
			file and can create or update DNSSEC keys as needed
			to ensure that a zone's keys match policy, roll over
			correctly on schedule, etc.  Thanks to Sebastian
			Castro for assistance in development. [RT #39211]
This commit is contained in:
Evan Hunt
2016-04-28 00:12:33 -07:00
parent 16591ba9ae
commit f6096b958c
88 changed files with 4686 additions and 1129 deletions

View File

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

View File

@@ -12,8 +12,6 @@
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
# $Id$
srcdir = @srcdir@
VPATH = @srcdir@
top_srcdir = @top_srcdir@
@@ -22,11 +20,13 @@ top_srcdir = @top_srcdir@
PYTHON = @PYTHON@
TARGETS = dnssec-checkds dnssec-coverage
PYSRCS = dnssec-checkds.py dnssec-coverage.py
SUBDIRS = isc
MANPAGES = dnssec-checkds.8 dnssec-coverage.8
HTMLPAGES = dnssec-checkds.html dnssec-coverage.html
TARGETS = dnssec-checkds dnssec-coverage dnssec-keymgr
PYSRCS = dnssec-checkds.py dnssec-coverage.py dnssec-keymgr.py
MANPAGES = dnssec-checkds.8 dnssec-coverage.8 dnssec-keymgr.8
HTMLPAGES = dnssec-checkds.html dnssec-coverage.html dnssec-keymgr.html
MANOBJS = ${MANPAGES} ${HTMLPAGES}
@BIND9_MAKE_RULES@
@@ -39,6 +39,10 @@ dnssec-coverage: dnssec-coverage.py
cp -f dnssec-coverage.py dnssec-coverage
chmod +x dnssec-coverage
dnssec-keymgr: dnssec-keymgr.py
cp -f dnssec-keymgr.py dnssec-keymgr
chmod +x dnssec-keymgr
doc man:: ${MANOBJS}
docclean manclean maintainer-clean::
@@ -49,13 +53,15 @@ installdirs:
$(SHELL) ${top_srcdir}/mkinstalldirs ${DESTDIR}${mandir}/man8
install:: ${TARGETS} installdirs
${INSTALL_SCRIPT} dnssec-checkds@EXEEXT@ ${DESTDIR}${sbindir}
${INSTALL_SCRIPT} dnssec-coverage@EXEEXT@ ${DESTDIR}${sbindir}
${INSTALL_SCRIPT} dnssec-checkds ${DESTDIR}${sbindir}
${INSTALL_SCRIPT} dnssec-coverage ${DESTDIR}${sbindir}
${INSTALL_SCRIPT} dnssec-keymgr ${DESTDIR}${sbindir}
${INSTALL_DATA} ${srcdir}/dnssec-checkds.8 ${DESTDIR}${mandir}/man8
${INSTALL_DATA} ${srcdir}/dnssec-coverage.8 ${DESTDIR}${mandir}/man8
${INSTALL_DATA} ${srcdir}/dnssec-keymgr.8 ${DESTDIR}${mandir}/man8
clean distclean::
rm -f ${TARGETS}
distclean::
rm -f dnssec-checkds.py dnssec-coverage.py
rm -f dnssec-checkds.py dnssec-coverage.py dnssec-keymgr.py

View File

@@ -15,314 +15,13 @@
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import argparse
import pprint
import os
import sys
prog='dnssec-checkds'
sys.path.insert(0, os.path.dirname(sys.argv[0]))
sys.path.insert(1, os.path.join('@prefix@', 'lib'))
# These routines permit platform-independent location of BIND 9 tools
if os.name == 'nt':
import win32con
import win32api
def prefix(bindir = ''):
if os.name != 'nt':
return os.path.join('@prefix@', bindir)
bind_subkey = "Software\\ISC\\BIND"
hKey = None
keyFound = True
try:
hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey)
except:
keyFound = False
if keyFound:
try:
(namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir")
except:
keyFound = False
win32api.RegCloseKey(hKey)
if keyFound:
return os.path.join(namedBase, bindir)
return os.path.join(win32api.GetSystemDirectory(), bindir)
def shellquote(s):
if os.name == 'nt':
return '"' + s.replace('"', '"\\"') + '"'
return "'" + s.replace("'", "'\\''") + "'"
############################################################################
# DSRR class:
# Delegation Signer (DS) resource record
############################################################################
class DSRR:
hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384' }
rrname=''
rrclass='IN'
rrtype='DS'
keyid=None
keyalg=None
hashalg=None
digest=''
ttl=0
def __init__(self, rrtext):
if not rrtext:
return
fields = rrtext.split()
if len(fields) < 7:
return
self.rrname = fields[0].lower()
fields = fields[1:]
if fields[0].upper() in ['IN','CH','HS']:
self.rrclass = fields[0].upper()
fields = fields[1:]
else:
self.ttl = int(fields[0])
self.rrclass = fields[1].upper()
fields = fields[2:]
if fields[0].upper() != 'DS':
raise Exception
self.rrtype = 'DS'
self.keyid = int(fields[1])
self.keyalg = int(fields[2])
self.hashalg = int(fields[3])
self.digest = ''.join(fields[4:]).upper()
def __repr__(self):
return('%s %s %s %d %d %d %s' %
(self.rrname, self.rrclass, self.rrtype, self.keyid,
self.keyalg, self.hashalg, self.digest))
def __eq__(self, other):
return self.__repr__() == other.__repr__()
############################################################################
# DLVRR class:
# DNSSEC Lookaside Validation (DLV) resource record
############################################################################
class DLVRR:
hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384' }
parent=''
dlvname=''
rrname='IN'
rrclass='IN'
rrtype='DLV'
keyid=None
keyalg=None
hashalg=None
digest=''
ttl=0
def __init__(self, rrtext, dlvname):
if not rrtext:
return
fields = rrtext.split()
if len(fields) < 7:
return
self.dlvname = dlvname.lower()
parent = fields[0].lower().strip('.').split('.')
parent.reverse()
dlv = dlvname.split('.')
dlv.reverse()
while len(dlv) != 0 and len(parent) != 0 and parent[0] == dlv[0]:
parent = parent[1:]
dlv = dlv[1:]
if len(dlv) != 0:
raise Exception
parent.reverse()
self.parent = '.'.join(parent)
self.rrname = self.parent + '.' + self.dlvname + '.'
fields = fields[1:]
if fields[0].upper() in ['IN','CH','HS']:
self.rrclass = fields[0].upper()
fields = fields[1:]
else:
self.ttl = int(fields[0])
self.rrclass = fields[1].upper()
fields = fields[2:]
if fields[0].upper() != 'DLV':
raise Exception
self.rrtype = 'DLV'
self.keyid = int(fields[1])
self.keyalg = int(fields[2])
self.hashalg = int(fields[3])
self.digest = ''.join(fields[4:]).upper()
def __repr__(self):
return('%s %s %s %d %d %d %s' %
(self.rrname, self.rrclass, self.rrtype,
self.keyid, self.keyalg, self.hashalg, self.digest))
def __eq__(self, other):
return self.__repr__() == other.__repr__()
############################################################################
# checkds:
# Fetch DS RRset for the given zone from the DNS; fetch DNSKEY
# RRset from the masterfile if specified, or from DNS if not.
# Generate a set of expected DS records from the DNSKEY RRset,
# and report on congruency.
############################################################################
def checkds(zone, masterfile = None):
dslist=[]
fp=os.popen("%s +noall +answer -t ds -q %s" %
(shellquote(args.dig), shellquote(zone)))
for line in fp:
dslist.append(DSRR(line))
dslist = sorted(dslist, key=lambda ds: (ds.keyid, ds.keyalg, ds.hashalg))
fp.close()
dsklist=[]
if masterfile:
fp = os.popen("%s -f %s %s " %
(shellquote(args.dsfromkey), shellquote(masterfile),
shellquote(zone)))
else:
fp = os.popen("%s +noall +answer -t dnskey -q %s | %s -f - %s" %
(shellquote(args.dig), shellquote(zone),
shellquote(args.dsfromkey), shellquote(zone)))
for line in fp:
dsklist.append(DSRR(line))
fp.close()
if (len(dsklist) < 1):
print ("No DNSKEY records found in zone apex")
return False
found = False
for ds in dsklist:
if ds in dslist:
print ("DS for KSK %s/%03d/%05d (%s) found in parent" %
(ds.rrname.strip('.'), ds.keyalg,
ds.keyid, DSRR.hashalgs[ds.hashalg]))
found = True
else:
print ("DS for KSK %s/%03d/%05d (%s) missing from parent" %
(ds.rrname.strip('.'), ds.keyalg,
ds.keyid, DSRR.hashalgs[ds.hashalg]))
if not found:
print ("No DS records were found for any DNSKEY")
return found
############################################################################
# checkdlv:
# Fetch DLV RRset for the given zone from the DNS; fetch DNSKEY
# RRset from the masterfile if specified, or from DNS if not.
# Generate a set of expected DLV records from the DNSKEY RRset,
# and report on congruency.
############################################################################
def checkdlv(zone, lookaside, masterfile = None):
dlvlist=[]
fp=os.popen("%s +noall +answer -t dlv -q %s" %
(shellquote(args.dig), shellquote(zone + '.' + lookaside)))
for line in fp:
dlvlist.append(DLVRR(line, lookaside))
dlvlist = sorted(dlvlist,
key=lambda dlv: (dlv.keyid, dlv.keyalg, dlv.hashalg))
fp.close()
#
# Fetch DNSKEY records from DNS and generate DLV records from them
#
dlvklist=[]
if masterfile:
fp = os.popen("%s -f %s -l %s %s " %
(args.dsfromkey, masterfile, lookaside, zone))
else:
fp = os.popen("%s +noall +answer -t dnskey %s | %s -f - -l %s %s"
% (shellquote(args.dig), shellquote(zone),
shellquote(args.dsfromkey), shellquote(lookaside),
shellquote(zone)))
for line in fp:
dlvklist.append(DLVRR(line, lookaside))
fp.close()
if (len(dlvklist) < 1):
print ("No DNSKEY records found in zone apex")
return False
found = False
for dlv in dlvklist:
if dlv in dlvlist:
print ("DLV for KSK %s/%03d/%05d (%s) found in %s" %
(dlv.parent, dlv.keyalg, dlv.keyid,
DLVRR.hashalgs[dlv.hashalg], dlv.dlvname))
found = True
else:
print ("DLV for KSK %s/%03d/%05d (%s) missing from %s" %
(dlv.parent, dlv.keyalg, dlv.keyid,
DLVRR.hashalgs[dlv.hashalg], dlv.dlvname))
if not found:
print ("No DLV records were found for any DNSKEY")
return found
############################################################################
# parse_args:
# Read command line arguments, set global 'args' structure
############################################################################
def parse_args():
global args
parser = argparse.ArgumentParser(description=prog + ': checks DS coverage')
bindir = 'bin'
if os.name == 'nt':
sbindir = 'bin'
else:
sbindir = 'sbin'
parser.add_argument('zone', type=str, help='zone to check')
parser.add_argument('-f', '--file', dest='masterfile', type=str,
help='zone master file')
parser.add_argument('-l', '--lookaside', dest='lookaside', type=str,
help='DLV lookaside zone')
parser.add_argument('-d', '--dig', dest='dig',
default=os.path.join(prefix(bindir), 'dig'),
type=str, help='path to \'dig\'')
parser.add_argument('-D', '--dsfromkey', dest='dsfromkey',
default=os.path.join(prefix(sbindir),
'dnssec-dsfromkey'),
type=str, help='path to \'dig\'')
parser.add_argument('-v', '--version', action='version',
version='@BIND9_VERSION@')
args = parser.parse_args()
args.zone = args.zone.strip('.')
if args.lookaside:
lookaside = args.lookaside.strip('.')
############################################################################
# Main
############################################################################
def main():
parse_args()
if args.lookaside:
found = checkdlv(args.zone, args.lookaside, args.masterfile)
else:
found = checkds(args.zone, args.masterfile)
exit(0 if found else 1)
import isc.checkds
if __name__ == "__main__":
main()
isc.checkds.main()

View File

@@ -56,7 +56,7 @@
<arg choice="opt" rep="norepeat"><option>-c <replaceable class="parameter">compilezone path</replaceable></option></arg>
<arg choice="opt" rep="norepeat"><option>-k</option></arg>
<arg choice="opt" rep="norepeat"><option>-z</option></arg>
<arg choice="opt" rep="norepeat">zone</arg>
<arg choice="opt" rep="repeat">zone</arg>
</cmdsynopsis>
</refsynopsisdiv>
@@ -151,10 +151,15 @@
'd' for days, 'w' for weeks, 'mo' for months, 'y' for years.
</para>
<para>
This option is mandatory unless the <option>-f</option> has
been used to specify a zone file. (If <option>-f</option> has
This option is not necessary if the <option>-f</option> has
been used to specify a zone file. If <option>-f</option> has
been specified, this option may still be used; it will override
the value found in the file.)
the value found in the file.
</para>
<para>
If this option is not used and the maximum TTL cannot be retrieved
from a zone file, a warning is generated and a default value of
1 week is used.
</para>
</listitem>
</varlistentry>
@@ -166,11 +171,10 @@
Sets the value to be used as the DNSKEY TTL for the zone or
zones being analyzed when determining whether there is a
possibility of validation failure. When a key is rolled (that
is, replaced with a new key), there must be enough time
for the old DNSKEY RRset to have expired from resolver caches
before the new key is activated and begins generating
signatures. If that condition does not apply, a warning
will be generated.
is, replaced with a new key), there must be enough time for the
old DNSKEY RRset to have expired from resolver caches before
the new key is activated and begins generating signatures. If
that condition does not apply, a warning will be generated.
</para>
<para>
The length of the TTL can be set in seconds, or in larger units
@@ -178,12 +182,18 @@
'd' for days, 'w' for weeks, 'mo' for months, 'y' for years.
</para>
<para>
This option is mandatory unless the <option>-f</option> has
been used to specify a zone file, or a default key TTL was
set with the <option>-L</option> to
<command>dnssec-keygen</command>. (If either of those is true,
this option may still be used; it will override the value found
in the zone or key file.)
This option is not necessary if <option>-f</option> has
been used to specify a zone file from which the TTL
of the DNSKEY RRset can be read, or if a default key TTL was
set using ith the <option>-L</option> to
<command>dnssec-keygen</command>. If either of those is true,
this option may still be used; it will override the values
found in the zone file or the key file.
</para>
<para>
If this option is not used and the key TTL cannot be retrieved
from the zone file or the key file, then a warning is generated
and a default value of 1 day is used.
</para>
</listitem>
</varlistentry>

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

@@ -1,6 +1,6 @@
#!@PYTHON@
############################################################################
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
# Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
@@ -15,785 +15,13 @@
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import argparse
import os
import glob
import sys
import re
import time
import calendar
from collections import defaultdict
import pprint
prog='dnssec-coverage'
sys.path.insert(0, os.path.dirname(sys.argv[0]))
sys.path.insert(1, os.path.join('@prefix@', 'lib'))
# These routines permit platform-independent location of BIND 9 tools
if os.name == 'nt':
import win32con
import win32api
def prefix(bindir = ''):
if os.name != 'nt':
return os.path.join('@prefix@', bindir)
bind_subkey = "Software\\ISC\\BIND"
hKey = None
keyFound = True
try:
hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey)
except:
keyFound = False
if keyFound:
try:
(namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir")
except:
keyFound = False
win32api.RegCloseKey(hKey)
if keyFound:
return os.path.join(namedBase, bindir)
return os.path.join(win32api.GetSystemDirectory(), bindir)
########################################################################
# Class Event
########################################################################
class Event:
""" A discrete key metadata event, e.g., Publish, Activate, Inactive,
Delete. Stores the date of the event, and identifying information about
the key to which the event will occur."""
def __init__(self, _what, _key):
now = time.time()
self.what = _what
self.when = _key.metadata[_what]
self.key = _key
self.keyid = _key.keyid
self.sep = _key.sep
self.zone = _key.zone
self.alg = _key.alg
def __repr__(self):
return repr((self.when, self.what, self.keyid, self.sep,
self.zone, self.alg))
def showtime(self):
return time.strftime("%a %b %d %H:%M:%S UTC %Y", self.when)
def showkey(self):
return self.key.showkey()
def showkeytype(self):
return self.key.showkeytype()
########################################################################
# Class Key
########################################################################
class Key:
"""An individual DNSSEC key. Identified by path, zone, algorithm, keyid.
Contains a dictionary of metadata events."""
def __init__(self, keyname):
directory = os.path.dirname(keyname)
key = os.path.basename(keyname)
(zone, alg, keyid) = key.split('+')
keyid = keyid.split('.')[0]
key = [zone, alg, keyid]
key_file = directory + os.sep + '+'.join(key) + ".key"
private_file = directory + os.sep + '+'.join(key) + ".private"
self.zone = zone[1:-1]
self.alg = int(alg)
self.keyid = int(keyid)
kfp = open(key_file, "r")
for line in kfp:
if line[0] == ';':
continue
tokens = line.split()
if not tokens:
continue
if tokens[1].lower() in ('in', 'ch', 'hs'):
septoken = 3
self.ttl = args.keyttl
if not self.ttl:
vspace()
print("WARNING: Unable to determine TTL for DNSKEY %s." %
self.showkey())
print("\t Using 1 day (86400 seconds); re-run with the -d "
"option for more\n\t accurate results.")
self.ttl = 86400
else:
septoken = 4
self.ttl = int(tokens[1]) if not args.keyttl else args.keyttl
if (int(tokens[septoken]) & 0x1) == 1:
self.sep = True
else:
self.sep = False
kfp.close()
pfp = open(private_file, "rU")
propDict = dict()
for propLine in pfp:
propDef = propLine.strip()
if len(propDef) == 0:
continue
if propDef[0] in ('!', '#'):
continue
punctuation = [propDef.find(c) for c in ':= '] + [len(propDef)]
found = min([ pos for pos in punctuation if pos != -1 ])
name = propDef[:found].rstrip()
value = propDef[found:].lstrip(":= ").rstrip()
propDict[name] = value
if("Publish" in propDict):
propDict["Publish"] = time.strptime(propDict["Publish"],
"%Y%m%d%H%M%S")
if("Activate" in propDict):
propDict["Activate"] = time.strptime(propDict["Activate"],
"%Y%m%d%H%M%S")
if("Inactive" in propDict):
propDict["Inactive"] = time.strptime(propDict["Inactive"],
"%Y%m%d%H%M%S")
if("Delete" in propDict):
propDict["Delete"] = time.strptime(propDict["Delete"],
"%Y%m%d%H%M%S")
if("Revoke" in propDict):
propDict["Revoke"] = time.strptime(propDict["Revoke"],
"%Y%m%d%H%M%S")
pfp.close()
self.metadata = propDict
def showkey(self):
return "%s/%03d/%05d" % (self.zone, self.alg, self.keyid);
def showkeytype(self):
return ("KSK" if self.sep else "ZSK")
# ensure that the gap between Publish and Activate is big enough
def check_prepub(self):
now = time.time()
if (not "Activate" in self.metadata):
debug_print("No Activate information in key: %s" % self.showkey())
return False
a = calendar.timegm(self.metadata["Activate"])
if (not "Publish" in self.metadata):
debug_print("No Publish information in key: %s" % self.showkey())
if a > now:
vspace()
print("WARNING: Key %s (%s) is scheduled for activation but \n"
"\t not for publication." %
(self.showkey(), self.showkeytype()))
return False
p = calendar.timegm(self.metadata["Publish"])
now = time.time()
if p < now and a < now:
return True
if p == a:
vspace()
print ("WARNING: %s (%s) is scheduled to be published and\n"
"\t activated at the same time. This could result in a\n"
"\t coverage gap if the zone was previously signed." %
(self.showkey(), self.showkeytype()))
print("\t Activation should be at least %s after publication."
% duration(self.ttl))
return True
if a < p:
vspace()
print("WARNING: Key %s (%s) is active before it is published" %
(self.showkey(), self.showkeytype()))
return False
if (a - p < self.ttl):
vspace()
print("WARNING: Key %s (%s) is activated too soon after\n"
"\t publication; this could result in coverage gaps due to\n"
"\t resolver caches containing old data."
% (self.showkey(), self.showkeytype()))
print("\t Activation should be at least %s after publication." %
duration(self.ttl))
return False
return True
# ensure that the gap between Inactive and Delete is big enough
def check_postpub(self, timespan = None):
if not timespan:
timespan = self.ttl
now = time.time()
if (not "Delete" in self.metadata):
debug_print("No Delete information in key: %s" % self.showkey())
return False
d = calendar.timegm(self.metadata["Delete"])
if (not "Inactive" in self.metadata):
debug_print("No Inactive information in key: %s" % self.showkey())
if d > now:
vspace()
print("WARNING: Key %s (%s) is scheduled for deletion but\n"
"\t not for inactivation." %
(self.showkey(), self.showkeytype()))
return False
i = calendar.timegm(self.metadata["Inactive"])
if d < now and i < now:
return True
if (d < i):
vspace()
print("WARNING: Key %s (%s) is scheduled for deletion before\n"
"\t inactivation." % (self.showkey(), self.showkeytype()))
return False
if (d - i < timespan):
vspace()
print("WARNING: Key %s (%s) scheduled for deletion too soon after\n"
"\t deactivation; this may result in coverage gaps due to\n"
"\t resolver caches containing old data."
% (self.showkey(), self.showkeytype()))
print("\t Deletion should be at least %s after inactivation." %
duration(timespan))
return False
return True
########################################################################
# class Zone
########################################################################
class Zone:
"""Stores data about a specific zone"""
def __init__(self, _name, _keyttl = None, _maxttl = None):
self.name = _name
self.keyttl = _keyttl
self.maxttl = _maxttl
def load(self, filename):
if not args.compilezone:
sys.stderr.write(prog + ': FATAL: "named-compilezone" not found\n')
exit(1)
if not self.name:
return
maxttl = keyttl = None
fp = os.popen("%s -o - %s %s 2> /dev/null" %
(args.compilezone, self.name, filename))
for line in fp:
fields = line.split()
if not maxttl or int(fields[1]) > maxttl:
maxttl = int(fields[1])
if fields[3] == "DNSKEY":
keyttl = int(fields[1])
fp.close()
self.keyttl = keyttl
self.maxttl = maxttl
############################################################################
# debug_print:
############################################################################
def debug_print(debugVar):
"""pretty print a variable iff debug mode is enabled"""
if not args.debug_mode:
return
if type(debugVar) == str:
print("DEBUG: " + debugVar)
else:
print("DEBUG: " + pprint.pformat(debugVar))
return
############################################################################
# vspace:
############################################################################
_firstline = True
def vspace():
"""adds vertical space between two sections of output text if and only
if this is *not* the first section being printed"""
global _firstline
if _firstline:
_firstline = False
else:
print('')
############################################################################
# vreset:
############################################################################
def vreset():
"""reset vertical spacing"""
global _firstline
_firstline = True
############################################################################
# getunit
############################################################################
def getunit(secs, size):
"""given a number of seconds, and a number of seconds in a larger unit of
time, calculate how many of the larger unit there are and return both
that and a remainder value"""
bigunit = secs // size
if bigunit:
secs %= size
return (bigunit, secs)
############################################################################
# addtime
############################################################################
def addtime(output, unit, t):
"""add a formatted unit of time to an accumulating string"""
if t:
output += ("%s%d %s%s" %
((", " if output else ""),
t, unit, ("s" if t > 1 else "")))
return output
############################################################################
# duration:
############################################################################
def duration(secs):
"""given a length of time in seconds, print a formatted human duration
in larger units of time
"""
# define units:
minute = 60
hour = minute * 60
day = hour * 24
month = day * 30
year = day * 365
# calculate time in units:
(years, secs) = getunit(secs, year)
(months, secs) = getunit(secs, month)
(days, secs) = getunit(secs, day)
(hours, secs) = getunit(secs, hour)
(minutes, secs) = getunit(secs, minute)
output = ''
output = addtime(output, "year", years)
output = addtime(output, "month", months)
output = addtime(output, "day", days)
output = addtime(output, "hour", hours)
output = addtime(output, "minute", minutes)
output = addtime(output, "second", secs)
return output
############################################################################
# parse_time
############################################################################
def parse_time(s):
"""convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds"""
s = s.strip()
# if s is an integer, we're done already
try:
n = int(s)
return n
except:
pass
# try to parse as a number with a suffix indicating unit of time
r = re.compile('([0-9][0-9]*)\s*([A-Za-z]*)')
m = r.match(s)
if not m:
raise Exception("Cannot parse %s" % s)
(n, unit) = m.groups()
n = int(n)
unit = unit.lower()
if unit[0] == 'y':
return n * 31536000
elif unit[0] == 'm' and unit[1] == 'o':
return n * 2592000
elif unit[0] == 'w':
return n * 604800
elif unit[0] == 'd':
return n * 86400
elif unit[0] == 'h':
return n * 3600
elif unit[0] == 'm' and unit[1] == 'i':
return n * 60
elif unit[0] == 's':
return n
else:
raise Exception("Invalid suffix %s" % unit)
############################################################################
# algname:
############################################################################
def algname(alg):
"""return the mnemonic for a DNSSEC algorithm"""
names = (None, 'RSAMD5', 'DH', 'DSA', 'ECC', 'RSASHA1',
'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None,
'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256',
'ECDSAP384SHA384')
name = None
if alg in range(len(names)):
name = names[alg]
return (name if name else str(alg))
############################################################################
# list_events:
############################################################################
def list_events(eventgroup):
"""print a list of the events in an eventgroup"""
if not eventgroup:
return
print (" " + eventgroup[0].showtime() + ":")
for event in eventgroup:
print (" %s: %s (%s)" %
(event.what, event.showkey(), event.showkeytype()))
############################################################################
# process_events:
############################################################################
def process_events(eventgroup, active, published):
"""go through the events in an event group in time-order, add to active
list upon Activate event, add to published list upon Publish event,
remove from active list upon Inactive event, and remove from published
upon Delete event. Emit warnings when inconsistant states are reached"""
for event in eventgroup:
if event.what == "Activate":
active.add(event.keyid)
elif event.what == "Publish":
published.add(event.keyid)
elif event.what == "Inactive":
if event.keyid not in active:
vspace()
print ("\tWARNING: %s (%s) scheduled to become inactive "
"before it is active" %
(event.showkey(), event.showkeytype()))
else:
active.remove(event.keyid)
elif event.what == "Delete":
if event.keyid in published:
published.remove(event.keyid)
else:
vspace()
print ("WARNING: key %s (%s) is scheduled for deletion before "
"it is published, at %s" %
(event.showkey(), event.showkeytype()))
elif event.what == "Revoke":
# We don't need to worry about the logic of this one;
# just stop counting this key as either active or published
if event.keyid in published:
published.remove(event.keyid)
if event.keyid in active:
active.remove(event.keyid)
return (active, published)
############################################################################
# check_events:
############################################################################
def check_events(eventsList, ksk):
"""create lists of events happening at the same time, check for
inconsistancies"""
active = set()
published = set()
eventgroups = list()
eventgroup = list()
keytype = ("KSK" if ksk else "ZSK")
# collect up all events that have the same time
eventsfound = False
for event in eventsList:
# if checking ZSKs, skip KSKs, and vice versa
if (ksk and not event.sep) or (event.sep and not ksk):
continue
# we found an appropriate (ZSK or KSK event)
eventsfound = True
# add event to current eventgroup
if (not eventgroup or eventgroup[0].when == event.when):
eventgroup.append(event)
# if we're at the end of the list, we're done. if
# we've found an event with a later time, start a new
# eventgroup
if (eventgroup[0].when != event.when):
eventgroups.append(eventgroup)
eventgroup = list()
eventgroup.append(event)
if eventgroup:
eventgroups.append(eventgroup)
for eventgroup in eventgroups:
if (args.checklimit and
calendar.timegm(eventgroup[0].when) > args.checklimit):
print("Ignoring events after %s" %
time.strftime("%a %b %d %H:%M:%S UTC %Y",
time.gmtime(args.checklimit)))
return True
(active, published) = \
process_events(eventgroup, active, published)
list_events(eventgroup)
# and then check for inconsistencies:
if len(active) == 0:
print ("ERROR: No %s's are active after this event" % keytype)
return False
elif len(published) == 0:
sys.stdout.write("ERROR: ")
print ("ERROR: No %s's are published after this event" % keytype)
return False
elif len(published.intersection(active)) == 0:
sys.stdout.write("ERROR: ")
print (("ERROR: No %s's are both active and published " +
"after this event") % keytype)
return False
if not eventsfound:
print ("ERROR: No %s events found in '%s'" %
(keytype, args.path))
return False
return True
############################################################################
# check_zones:
# ############################################################################
def check_zones(eventsList):
"""scan events per zone, algorithm, and key type, in order of occurrance,
noting inconsistent states when found"""
global foundprob
foundprob = False
zonesfound = False
for zone in eventsList:
if args.zone and zone != args.zone:
continue
zonesfound = True
for alg in eventsList[zone]:
if not args.no_ksk:
vspace()
print("Checking scheduled KSK events for zone %s, algorithm %s..." %
(zone, algname(alg)))
if not check_events(eventsList[zone][alg], True):
foundprob = True
else:
print ("No errors found")
if not args.no_zsk:
vspace()
print("Checking scheduled ZSK events for zone %s, algorithm %s..." %
(zone, algname(alg)))
if not check_events(eventsList[zone][alg], False):
foundprob = True
else:
print ("No errors found")
if not zonesfound:
print("ERROR: No key events found for %s in '%s'" %
(args.zone, args.path))
exit(1)
############################################################################
# fill_eventsList:
############################################################################
def fill_eventsList(eventsList):
"""populate the list of events"""
for zone, algorithms in keyDict.items():
for alg, keys in algorithms.items():
for keyid, keydata in keys.items():
if("Publish" in keydata.metadata):
eventsList[zone][alg].append(Event("Publish", keydata))
if("Activate" in keydata.metadata):
eventsList[zone][alg].append(Event("Activate", keydata))
if("Inactive" in keydata.metadata):
eventsList[zone][alg].append(Event("Inactive", keydata))
if("Delete" in keydata.metadata):
eventsList[zone][alg].append(Event("Delete", keydata))
eventsList[zone][alg] = sorted(eventsList[zone][alg],
key=lambda event: event.when)
foundprob = False
if not keyDict:
print("ERROR: No key events found in '%s'" % args.path)
exit(1)
############################################################################
# set_path:
############################################################################
def set_path(command, default=None):
"""find the location of a specified command. if a default is supplied
and it works, we use it; otherwise we search PATH for a match. If
not found, error and exit"""
fpath = default
if not fpath or not os.path.isfile(fpath) or not os.access(fpath, os.X_OK):
path = os.environ["PATH"]
if not path:
path = os.path.defpath
for directory in path.split(os.pathsep):
fpath = directory + os.sep + command
if os.path.isfile(fpath) or os.access(fpath, os.X_OK):
break
fpath = None
return fpath
############################################################################
# parse_args:
############################################################################
def parse_args():
"""Read command line arguments, set global 'args' structure"""
global args
compilezone = set_path('named-compilezone',
os.path.join(prefix('bin'), 'named-compilezone'))
parser = argparse.ArgumentParser(description=prog + ': checks future ' +
'DNSKEY coverage for a zone')
parser.add_argument('zone', type=str, help='zone to check')
parser.add_argument('-K', dest='path', default='.', type=str,
help='a directory containing keys to process',
metavar='dir')
parser.add_argument('-f', dest='filename', type=str,
help='zone master file', metavar='file')
parser.add_argument('-m', dest='maxttl', type=str,
help='the longest TTL in the zone(s)',
metavar='time')
parser.add_argument('-d', dest='keyttl', type=str,
help='the DNSKEY TTL', metavar='time')
parser.add_argument('-r', dest='resign', default='1944000',
type=str, help='the RRSIG refresh interval '
'in seconds [default: 22.5 days]',
metavar='time')
parser.add_argument('-c', dest='compilezone',
default=compilezone, type=str,
help='path to \'named-compilezone\'',
metavar='path')
parser.add_argument('-l', dest='checklimit',
type=str, default='0',
help='Length of time to check for '
'DNSSEC coverage [default: 0 (unlimited)]',
metavar='time')
parser.add_argument('-z', dest='no_ksk',
action='store_true', default=False,
help='Only check zone-signing keys (ZSKs)')
parser.add_argument('-k', dest='no_zsk',
action='store_true', default=False,
help='Only check key-signing keys (KSKs)')
parser.add_argument('-D', '--debug', dest='debug_mode',
action='store_true', default=False,
help='Turn on debugging output')
parser.add_argument('-v', '--version', action='version',
version='@BIND9_VERSION@')
args = parser.parse_args()
if args.no_zsk and args.no_ksk:
print("ERROR: -z and -k cannot be used together.");
exit(1)
# convert from time arguments to seconds
try:
if args.maxttl:
m = parse_time(args.maxttl)
args.maxttl = m
except:
pass
try:
if args.keyttl:
k = parse_time(args.keyttl)
args.keyttl = k
except:
pass
try:
if args.resign:
r = parse_time(args.resign)
args.resign = r
except:
pass
try:
if args.checklimit:
lim = args.checklimit
r = parse_time(args.checklimit)
if r == 0:
args.checklimit = None
else:
args.checklimit = time.time() + r
except:
pass
# if we've got the values we need from the command line, stop now
if args.maxttl and args.keyttl:
return
# load keyttl and maxttl data from zonefile
if args.zone and args.filename:
try:
zone = Zone(args.zone)
zone.load(args.filename)
if not args.maxttl:
args.maxttl = zone.maxttl
if not args.keyttl:
args.keyttl = zone.maxttl
except Exception as e:
print("Unable to load zone data from %s: " % args.filename, e)
if not args.maxttl:
vspace()
print ("WARNING: Maximum TTL value was not specified. Using 1 week\n"
"\t (604800 seconds); re-run with the -m option to get more\n"
"\t accurate results.")
args.maxttl = 604800
############################################################################
# Main
############################################################################
def main():
global keyDict
parse_args()
path=args.path
print ("PHASE 1--Loading keys to check for internal timing problems")
keyDict = defaultdict(lambda : defaultdict(dict))
files = glob.glob(os.path.join(path, '*.private'))
for infile in files:
key = Key(infile)
if args.zone and key.zone != args.zone:
continue
keyDict[key.zone][key.alg][key.keyid] = key
key.check_prepub()
if key.sep:
key.check_postpub()
else:
key.check_postpub(args.maxttl + args.resign)
vspace()
print ("PHASE 2--Scanning future key events for coverage failures")
vreset()
eventsList = defaultdict(lambda : defaultdict(list))
fill_eventsList(eventsList)
check_zones(eventsList)
if foundprob:
exit(1)
else:
exit(0)
import isc.coverage
if __name__ == "__main__":
main()
isc.coverage.main()

View File

@@ -0,0 +1,354 @@
<!--
- 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.
-->
<!-- Converted by db4-upgrade version 1.0 -->
<refentry xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="man.dnssec-keymgr">
<info>
<date>2016-04-03</date>
</info>
<refentryinfo>
<corpname>ISC</corpname>
<corpauthor>Internet Systems Consortium, Inc.</corpauthor>
</refentryinfo>
<refmeta>
<refentrytitle><application>dnssec-keymgr</application></refentrytitle>
<manvolnum>8</manvolnum>
<refmiscinfo>BIND9</refmiscinfo>
</refmeta>
<refnamediv>
<refname><application>dnssec-keymgr</application></refname>
<refpurpose>Ensures correct DNSKEY coverage for a zone based on a defined policy</refpurpose>
</refnamediv>
<docinfo>
<copyright>
<year>2016</year>
<holder>Internet Systems Consortium, Inc. ("ISC")</holder>
</copyright>
</docinfo>
<refsynopsisdiv>
<cmdsynopsis sepchar=" ">
<command>dnssec-keymgr</command>
<arg choice="opt" rep="norepeat"><option>-K <replaceable class="parameter">directory</replaceable></option></arg>
<arg choice="opt" rep="norepeat"><option>-c <replaceable class="parameter">file</replaceable></option></arg>
<arg choice="opt" rep="norepeat"><option>-d <replaceable class="parameter">time</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"><option>-g <replaceable class="parameter">path</replaceable></option></arg>
<arg choice="opt" rep="norepeat"><option>-s <replaceable class="parameter">path</replaceable></option></arg>
<arg choice="opt" rep="repeat">zone</arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsection><info><title>DESCRIPTION</title></info>
<para>
<command>dnssec-keymgr</command>
is a high level Python wrapper to facilitate the key rollover
process for zones handled by BIND. It uses the BIND commands
for manipulating DNSSEC key metadata:
<command>dnssec-keygen</command> and
<command>dnssec-settime</command>.
</para>
<para>
DNSSEC policy can be read from a configuration file (default
<filename>/etc/dnssec.policy</filename>), from which the key
parameters, publication and rollover schedule, and desired
coverage duration for any given zone can be determined. This
file may be used to define individual DNSSEC policies on a
per-zone basis, or to set a default policy used for all zones.
</para>
<para>
When <command>dnssec-keymgr</command> runs, it examines the DNSSEC
keys for one or more zones, comparing their timing metadata against
the policies for those zones. If key settings do not conform to the
DNSSEC policy (for example, because the policy has been changed),
they are automatically corrected.
</para>
</para>
A zone policy can specify a duration for which we want to
ensure the key correctness (<option>coverage</option>). It can
also specify a rollover period (<option>roll-period</option>).
If policy indicates that a key should roll over before the
coverage period ends, then a successor key will automatically be
created and added to the end of the key series.
<para>
<para>
If zones are specified on the command line,
<command>dnssec-keymgr</command> will examine only those zones.
If a specified zone does not already have keys in place, then
keys will be generated for it according to policy.
</para>
<para>
If zones are <emphasis>not</emphasis> specified on the command
line, then <command>dnssec-keymgr</command> will search the
key directory (either the current working directory or the directory
set by the <option>-K</option> option), and check the keys for
all the zones represented in the directory.
</para>
<para>
It is expected that this tool will be run automatically and
unattended (for example, by <command>cron</command>).
</para>
</refsection>
<refsection><info><title>OPTIONS</title></info>
<variablelist>
<varlistentry>
<term>-K <replaceable class="parameter">directory</replaceable></term>
<listitem>
<para>
Sets the directory in which keys can be found. Defaults to the
current working directory.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-c <replaceable class="parameter">file</replaceable></term>
<listitem>
<para>
If <option>-c</option> is specified, then the DNSSEC
policy is read from <option>file</option>. (If not
specified, then the policy is read from
<filename>/etc/policy.conf</filename>; if that file
doesn't exist, a built-in global default policy is used.)
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-f</term>
<listitem>
<para>
Force: allow updating of key events even if they are
already in the past. This is not recommended for use with
zones in which keys have already been published. However,
if a set of keys has been generated all of which have
publication and activation dates in the past, but the
keys have not been published in a zone as yet, then this
option can be used to clean them up and turn them into a
proper series of keys with appropriate rollover intervals.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-q</term>
<listitem>
<para>
Quiet: suppress printing of <command>dnssec-keygen</command>
and <command>dnssec-settime</command>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-k</term>
<listitem>
<para>
Only apply policies to KSK keys.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-z</term>
<listitem>
<para>
Only apply policies to ZSK keys.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-g <replaceable class="parameter">keygen path</replaceable></term>
<listitem>
<para>
Specifies a path to a <command>dnssec-keygen</command> binary.
Used for testing.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-s <replaceable class="parameter">settime path</replaceable></term>
<listitem>
<para>
Specifies a path to a <command>dnssec-settime</command> binary.
Used for testing.
</para>
</listitem>
</varlistentry>
</variablelist>
</refsection>
<refsection><info><title>POLICY CONFIGURATION</title></info>
<para>
The <filename>policy.conf</filename> file can specify three kinds
of policies:
</para>
<itemizedlist>
<listitem>
<emphasis>Policy classes</emphasis>
(<option>policy <replaceable>name</replaceable> { ... };</option>)
can be inherited by zone policies or other policy classes; these
can be used to create sets of different security profiles. For
example, a policy class <userinput>normal</userinput> might specify
1024-bit key sizes, but a class <userinput>extra</userinput> might
specify 2048 bits instead; <userinput>extra</userinput> would be
used for zones that had unusually high security needs.
</listitem>
<listitem>
Algorithm policies:
(<option>algorithm-policy <replaceable>algorithm</replaceable> { ... };</option> )
override default per-algorithm settings. For example, by default,
RSASHA256 keys use 2048-bit key sizes for both KSK and ZSK. This
can be modified using <command>algorithm-policy</command>, and the
new key sizes would then be used for any key of type RSASHA256.
</listitem>
<listitem>
Zone policies:
(<option>zone <replaceable>name</replaceable> { ... };</option> )
set policy for a single zone by name. A zone policy can inherit
a policy class by including a <option>policy</option> option.
</listitem>
</itemizedlist>
<para>
Options that can be specified in policies:
</para>
<variablelist>
<varlistentry>
<term><command>directory</command></term>
<listitem>
Specifies the directory in which keys should be stored.
</listitem>
</varlistentry>
<varlistentry>
<term><command>algorithm</command></term>
<listitem>
The key algorithm. If no policy is defined, the default is
RSASHA256.
</listitem>
</varlistentry>
<varlistentry>
<term><command>keyttl</command></term>
<listitem>
The key TTL. If no policy is defined, the default is one hour.
</listitem>
</varlistentry>
<varlistentry>
<term><command>coverage</command></term>
<listitem>
The length of time to ensure that keys will be correct; no action
will be taken to create new keys to be activated after this time.
This can be represented as a number of seconds, or as a duration using
human-readable units (examples: "1y" or "6 months").
A default value for this option can be set in algorithm policies
as well as in policy classes or zone policies.
If no policy is configured, the default is six months.
</listitem>
</varlistentry>
<varlistentry>
<term><command>key-size</command></term>
<listitem>
Specifies the number of bits to use in creating keys.
Takes two arguments: keytype (eihter "zsk" or "ksk") and size.
A default value for this option can be set in algorithm policies
as well as in policy classes or zone policies. If no policy is
configured, the default is 1024 bits for DSA keys and 2048 for
RSA.
</listitem>
</varlistentry>
<varlistentry>
<term><command>roll-period</command></term>
<listitem>
How frequently keys should be rolled over.
Takes two arguments: keytype (eihter "zsk" or "ksk") and a duration.
A default value for this option can be set in algorithm policies
as well as in policy classes or zone policies. If no policy is
configured, the default is one year for ZSK's. KSK's do not
roll over by default.
</listitem>
</varlistentry>
<varlistentry>
<term><command>pre-publish</command></term>
<listitem>
How long before activation a key should be published. Note: If
<option>roll-period</option> is not set, this value is ignored.
Takes two arguments: keytype (either "zsk" or "ksk") and a duration.
A default value for this option can be set in algorithm policies
as well as in policy classes or zone policies. The default is
one month.
</listitem>
</varlistentry>
<varlistentry>
<term><command>post-publish</command></term>
<listitem>
How long after inactivation a key should be deleted from the zone.
Note: If <option>roll-period</option> is not set, this value is ignored.
Takes two arguments: keytype (eihter "zsk" or "ksk") and a duration.
A default value for this option can be set in algorithm policies
as well as in policy classes or zone policies. The default is one
month.
</listitem>
</varlistentry>
<varlistentry>
<term><command>standby</command></term>
<listitem>
Not yet implemented.
</listitem>
</varlistentry>
</variablelist>
</refsection>
<refsection><info><title>REMAINING WORK</title></info>
<itemizedlist>
<listitem>
Enable scheduling of KSK rollovers using the <option>-P sync</option>
and <option>-D sync</option> options to
<command>dnssec-keygen</command> and
<command>dnssec-settime</command>. Check the parent zone
(as in <command>dnssec-checkds</command>) to determine when it's
safe for the key to roll.
</listitem>
<listitem>
Allow configuration of standby keys and use of the REVOKE bit,
for keys that use RFC 5011 semantics.
</listitem>
</itemizedlist>
</refsection>
<refsection><info><title>SEE ALSO</title></info>
<para>
<citerefentry>
<refentrytitle>dnssec-coverage</refentrytitle><manvolnum>8</manvolnum>
</citerefentry>,
<citerefentry>
<refentrytitle>dnssec-keygen</refentrytitle><manvolnum>8</manvolnum>
</citerefentry>,
<citerefentry>
<refentrytitle>dnssec-settime</refentrytitle><manvolnum>8</manvolnum>
</citerefentry>,
<citerefentry>
<refentrytitle>dnssec-checkds</refentrytitle><manvolnum>8</manvolnum>
</citerefentry>
</para>
</refsection>
</refentry>

View File

@@ -0,0 +1,27 @@
#!@PYTHON@
############################################################################
# Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import os
import sys
sys.path.insert(0, os.path.dirname(sys.argv[0]))
sys.path.insert(1, os.path.join('@prefix@', 'lib'))
import isc.keymgr
if __name__ == "__main__":
isc.keymgr.main()

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

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

View File

@@ -0,0 +1,67 @@
# Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
srcdir = @srcdir@
VPATH = @srcdir@
top_srcdir = @top_srcdir@
@BIND9_MAKE_INCLUDES@
SUBDIRS = tests
PYTHON = @PYTHON@
PYSRCS = __init__.py dnskey.py eventlist.py keydict.py \
keyevent.py keyzone.py policy.py
TARGETS = parsetab.py parsetab.pyc \
__init__.pyc dnskey.pyc eventlist.py keydict.py \
keyevent.pyc keyzone.pyc policy.pyc
@BIND9_MAKE_RULES@
%.pyc: %.py
$(PYTHON) -m compileall .
parsetab.py parsetab.pyc: policy.py
$(PYTHON) policy.py parse /dev/null > /dev/null
$(PYTHON) -m parsetab
installdirs:
$(SHELL) ${top_srcdir}/mkinstalldirs ${DESTDIR}${libdir}/isc
install:: ${PYSRCS} installdirs
${INSTALL_SCRIPT} __init__.py ${DESTDIR}${libdir}
${INSTALL_SCRIPT} __init__.pyc ${DESTDIR}${libdir}
${INSTALL_SCRIPT} dnskey.py ${DESTDIR}${libdir}
${INSTALL_SCRIPT} dnskey.pyc ${DESTDIR}${libdir}
${INSTALL_SCRIPT} eventlist.py ${DESTDIR}${libdir}
${INSTALL_SCRIPT} eventlist.pyc ${DESTDIR}${libdir}
${INSTALL_SCRIPT} keydict.py ${DESTDIR}${libdir}
${INSTALL_SCRIPT} keydict.pyc ${DESTDIR}${libdir}
${INSTALL_SCRIPT} keyevent.py ${DESTDIR}${libdir}
${INSTALL_SCRIPT} keyevent.pyc ${DESTDIR}${libdir}
${INSTALL_SCRIPT} keyzone.py ${DESTDIR}${libdir}
${INSTALL_SCRIPT} keyzone.pyc ${DESTDIR}${libdir}
${INSTALL_SCRIPT} policy.py ${DESTDIR}${libdir}
${INSTALL_SCRIPT} policy.pyc ${DESTDIR}${libdir}
${INSTALL_SCRIPT} parsetab.py ${DESTDIR}${libdir}
${INSTALL_SCRIPT} parsetab.pyc ${DESTDIR}${libdir}
check test: subdirs
clean distclean::
rm -f *.pyc parser.out parsetab.py
distclean::
rm -Rf utils.py

View File

@@ -0,0 +1,25 @@
# Copyright (C) 2015 Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
__all__ = ['dnskey', 'eventlist', 'keydict', 'keyevent', 'keyseries',
'keyzone', 'policy', 'parsetab', 'utils']
from isc.dnskey import *
from isc.eventlist import *
from isc.keydict import *
from isc.keyevent import *
from isc.keyseries import *
from isc.keyzone import *
from isc.policy import *
from isc.utils import *

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

@@ -0,0 +1,189 @@
############################################################################
# Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import argparse
import os
import sys
from subprocess import Popen, PIPE
from isc.utils import prefix,version
prog = 'dnssec-checkds'
############################################################################
# SECRR class:
# Class for DS/DLV resource record
############################################################################
class SECRR:
hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384'}
rrname = ''
rrclass = 'IN'
keyid = None
keyalg = None
hashalg = None
digest = ''
ttl = 0
def __init__(self, rrtext, dlvname = None):
if not rrtext:
raise Exception
fields = rrtext.split()
if len(fields) < 7:
raise Exception
if dlvname:
self.rrtype = "DLV"
self.dlvname = dlvname.lower()
parent = fields[0].lower().strip('.').split('.')
parent.reverse()
dlv = dlvname.split('.')
dlv.reverse()
while len(dlv) != 0 and len(parent) != 0 and parent[0] == dlv[0]:
parent = parent[1:]
dlv = dlv[1:]
if dlv:
raise Exception
parent.reverse()
self.parent = '.'.join(parent)
self.rrname = self.parent + '.' + self.dlvname + '.'
else:
self.rrtype = "DS"
self.rrname = fields[0].lower()
fields = fields[1:]
if fields[0].upper() in ['IN', 'CH', 'HS']:
self.rrclass = fields[0].upper()
fields = fields[1:]
else:
self.ttl = int(fields[0])
self.rrclass = fields[1].upper()
fields = fields[2:]
if fields[0].upper() != self.rrtype:
raise Exception
self.keyid, self.keyalg, self.hashalg = map(int, fields[1:4])
self.digest = ''.join(fields[4:]).upper()
def __repr__(self):
return '%s %s %s %d %d %d %s' % \
(self.rrname, self.rrclass, self.rrtype,
self.keyid, self.keyalg, self.hashalg, self.digest)
def __eq__(self, other):
return self.__repr__() == other.__repr__()
############################################################################
# check:
# Fetch DS/DLV RRset for the given zone from the DNS; fetch DNSKEY
# RRset from the masterfile if specified, or from DNS if not.
# Generate a set of expected DS/DLV records from the DNSKEY RRset,
# and report on congruency.
############################################################################
def check(zone, args, masterfile=None, lookaside=None):
rrlist = []
cmd = [args.dig, "+noall", "+answer", "-t", "dlv" if lookaside else "ds",
"-q", zone + "." + lookaside if lookaside else zone]
fp, _ = Popen(cmd, stdout=PIPE).communicate()
for line in fp.splitlines():
rrlist.append(SECRR(line, lookaside))
rrlist = sorted(rrlist, key=lambda rr: (rr.keyid, rr.keyalg, rr.hashalg))
klist = []
if masterfile:
cmd = [args.dsfromkey, "-f", masterfile]
if lookaside:
cmd += ["-l", lookaside]
cmd.append(zone)
fp, _ = Popen(cmd, stdout=PIPE).communicate()
else:
intods, _ = Popen([args.dig, "+noall", "+answer", "-t", "dnskey",
"-q", zone], stdout=PIPE).communicate()
cmd = [args.dsfromkey, "-f", "-"]
if lookaside:
cmd += ["-l", lookaside]
cmd.append(zone)
fp, _ = Popen(cmd, stdin=PIPE, stdout=PIPE).communicate(intods)
for line in fp.splitlines():
klist.append(SECRR(line, lookaside))
if len(klist) < 1:
print ("No DNSKEY records found in zone apex")
return False
found = False
for rr in klist:
if rr in rrlist:
print ("%s for KSK %s/%03d/%05d (%s) found in parent" %
(rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
rr.keyid, SECRR.hashalgs[rr.hashalg]))
found = True
else:
print ("%s for KSK %s/%03d/%05d (%s) missing from parent" %
(rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
rr.keyid, SECRR.hashalgs[rr.hashalg]))
if not found:
print ("No %s records were found for any DNSKEY" % ("DLV" if lookaside else "DS"))
return found
############################################################################
# parse_args:
# Read command line arguments, set global 'args' structure
############################################################################
def parse_args():
parser = argparse.ArgumentParser(description=prog + ': checks DS coverage')
bindir = 'bin'
sbindir = 'bin' if os.name == 'nt' else 'sbin'
parser.add_argument('zone', type=str, help='zone to check')
parser.add_argument('-f', '--file', dest='masterfile', type=str,
help='zone master file')
parser.add_argument('-l', '--lookaside', dest='lookaside', type=str,
help='DLV lookaside zone')
parser.add_argument('-d', '--dig', dest='dig',
default=os.path.join(prefix(bindir), 'dig'),
type=str, help='path to \'dig\'')
parser.add_argument('-D', '--dsfromkey', dest='dsfromkey',
default=os.path.join(prefix(sbindir),
'dnssec-dsfromkey'),
type=str, help='path to \'dig\'')
parser.add_argument('-v', '--version', action='version',
version=version)
args = parser.parse_args()
args.zone = args.zone.strip('.')
if args.lookaside:
args.lookaside = args.lookaside.strip('.')
return args
############################################################################
# Main
############################################################################
def main():
args = parse_args()
found = check(args.zone, args, args.masterfile, args.lookaside)
exit(0 if found else 1)

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

@@ -0,0 +1,292 @@
############################################################################
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
from __future__ import print_function
import os
import sys
import argparse
import glob
import re
import time
import calendar
import pprint
from collections import defaultdict
prog = 'dnssec-coverage'
from isc import *
from isc.utils import prefix
############################################################################
# print a fatal error and exit
############################################################################
def fatal(*args, **kwargs):
print(*args, **kwargs)
sys.exit(1)
############################################################################
# output:
############################################################################
_firstline = True
def output(*args, **kwargs):
"""output text, adding a vertical space this is *not* the first
first section being printed since a call to vreset()"""
global _firstline
if 'skip' in kwargs:
skip = kwargs['skip']
kwargs.pop('skip', None)
else:
skip = True
if _firstline:
_firstline = False
elif skip:
print('')
if args:
print(*args, **kwargs)
def vreset():
"""reset vertical spacing"""
global _firstline
_firstline = True
############################################################################
# parse_time
############################################################################
def parse_time(s):
""" convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds
:param s: String with some text representing a time interval
:return: Integer with the number of seconds in the time interval
"""
s = s.strip()
# if s is an integer, we're done already
try:
return int(s)
except ValueError:
pass
# try to parse as a number with a suffix indicating unit of time
r = re.compile('([0-9][0-9]*)\s*([A-Za-z]*)')
m = r.match(s)
if not m:
raise ValueError("Cannot parse %s" % s)
n, unit = m.groups()
n = int(n)
unit = unit.lower()
if unit.startswith('y'):
return n * 31536000
elif unit.startswith('mo'):
return n * 2592000
elif unit.startswith('w'):
return n * 604800
elif unit.startswith('d'):
return n * 86400
elif unit.startswith('h'):
return n * 3600
elif unit.startswith('mi'):
return n * 60
elif unit.startswith('s'):
return n
else:
raise ValueError("Invalid suffix %s" % unit)
############################################################################
# set_path:
############################################################################
def set_path(command, default=None):
""" find the location of a specified command. if a default is supplied
and it works, we use it; otherwise we search PATH for a match.
:param command: string with a command to look for in the path
:param default: default location to use
:return: detected location for the desired command
"""
fpath = default
if not fpath or not os.path.isfile(fpath) or not os.access(fpath, os.X_OK):
path = os.environ["PATH"]
if not path:
path = os.path.defpath
for directory in path.split(os.pathsep):
fpath = os.path.join(directory, command)
if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
break
fpath = None
return fpath
############################################################################
# parse_args:
############################################################################
def parse_args():
"""Read command line arguments, set global 'args' structure"""
compilezone = set_path('named-compilezone',
os.path.join(prefix('sbin'), 'named-compilezone'))
parser = argparse.ArgumentParser(description=prog + ': checks future ' +
'DNSKEY coverage for a zone')
parser.add_argument('zone', type=str, nargs='*', default=None,
help='zone(s) to check' +
'(default: all zones in the directory)')
parser.add_argument('-K', dest='path', default='.', type=str,
help='a directory containing keys to process',
metavar='dir')
parser.add_argument('-f', dest='filename', type=str,
help='zone master file', metavar='file')
parser.add_argument('-m', dest='maxttl', type=str,
help='the longest TTL in the zone(s)',
metavar='time')
parser.add_argument('-d', dest='keyttl', type=str,
help='the DNSKEY TTL', metavar='time')
parser.add_argument('-r', dest='resign', default='1944000',
type=str, help='the RRSIG refresh interval '
'in seconds [default: 22.5 days]',
metavar='time')
parser.add_argument('-c', dest='compilezone',
default=compilezone, type=str,
help='path to \'named-compilezone\'',
metavar='path')
parser.add_argument('-l', dest='checklimit',
type=str, default='0',
help='Length of time to check for '
'DNSSEC coverage [default: 0 (unlimited)]',
metavar='time')
parser.add_argument('-z', dest='no_ksk',
action='store_true', default=False,
help='Only check zone-signing keys (ZSKs)')
parser.add_argument('-k', dest='no_zsk',
action='store_true', default=False,
help='Only check key-signing keys (KSKs)')
parser.add_argument('-D', '--debug', dest='debug_mode',
action='store_true', default=False,
help='Turn on debugging output')
parser.add_argument('-v', '--version', action='version',
version=utils.version)
args = parser.parse_args()
if args.no_zsk and args.no_ksk:
fatal("ERROR: -z and -k cannot be used together.")
elif args.no_zsk or args.no_ksk:
args.keytype = "KSK" if args.no_zsk else "ZSK"
else:
args.keytype = None
if args.filename and len(args.zone) > 1:
fatal("ERROR: -f can only be used with one zone.")
# convert from time arguments to seconds
try:
if args.maxttl:
m = parse_time(args.maxttl)
args.maxttl = m
except ValueError:
pass
try:
if args.keyttl:
k = parse_time(args.keyttl)
args.keyttl = k
except ValueError:
pass
try:
if args.resign:
r = parse_time(args.resign)
args.resign = r
except ValueError:
pass
try:
if args.checklimit:
lim = args.checklimit
r = parse_time(args.checklimit)
if r == 0:
args.checklimit = None
else:
args.checklimit = time.time() + r
except ValueError:
pass
# if we've got the values we need from the command line, stop now
if args.maxttl and args.keyttl:
return args
# load keyttl and maxttl data from zonefile
if args.zone and args.filename:
try:
zone = keyzone(args.zone[0], args.filename, args.compilezone)
args.maxttl = args.maxttl or zone.maxttl
args.keyttl = args.maxttl or zone.keyttl
except Exception as e:
print("Unable to load zone data from %s: " % args.filename, e)
if not args.maxttl:
output("WARNING: Maximum TTL value was not specified. Using 1 week\n"
"\t (604800 seconds); re-run with the -m option to get more\n"
"\t accurate results.")
args.maxttl = 604800
return args
############################################################################
# Main
############################################################################
def main():
args = parse_args()
print("PHASE 1--Loading keys to check for internal timing problems")
try:
kd = keydict(path=args.path, zone=args.zone, keyttl=args.keyttl)
except Exception as e:
fatal('ERROR: Unable to build key dictionary: ' + str(e))
for key in kd:
key.check_prepub(output)
if key.sep:
key.check_postpub(output)
else:
key.check_postpub(output, args.maxttl + args.resign)
output("PHASE 2--Scanning future key events for coverage failures")
vreset()
try:
elist = eventlist(kd)
except Exception as e:
fatal('ERROR: Unable to build event list: ' + str(e))
errors = False
if not args.zone:
if not elist.coverage(None, args.keytype, args.checklimit, output):
errors = True
else:
for zone in args.zone:
try:
if not elist.coverage(zone, args.keytype,
args.checklimit, output):
errors = True
except:
output('ERROR: Coverage check failed for zone ' + zone)
sys.exit(1 if errors else 0)

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

@@ -0,0 +1,504 @@
############################################################################
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import os
import time
import calendar
from subprocess import Popen, PIPE
########################################################################
# Class dnskey
########################################################################
class TimePast(Exception):
def __init__(self, key, prop, value):
super(TimePast, self).__init__('%s time for key %s (%d) is already past'
% (prop, key, value))
class dnskey:
"""An individual DNSSEC key. Identified by path, name, algorithm, keyid.
Contains a dictionary of metadata events."""
_PROPS = ('Created', 'Publish', 'Activate', 'Inactive', 'Delete',
'Revoke', 'DSPublish', 'SyncPublish', 'SyncDelete')
_OPTS = (None, '-P', '-A', '-I', '-D', '-R', None, '-Psync', '-Dsync')
_ALGNAMES = (None, 'RSAMD5', 'DH', 'DSA', 'ECC', 'RSASHA1',
'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None,
'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256',
'ECDSAP384SHA384')
def __init__(self, key, directory=None, keyttl=None):
# this makes it possible to use algname as a class or instance method
if isinstance(key, tuple) and len(key) == 3:
self._dir = directory or '.'
(name, alg, keyid) = key
self.fromtuple(name, alg, keyid, keyttl)
self._dir = directory or os.path.dirname(key) or '.'
key = os.path.basename(key)
(name, alg, keyid) = key.split('+')
name = name[1:-1]
alg = int(alg)
keyid = int(keyid.split('.')[0])
self.fromtuple(name, alg, keyid, keyttl)
def fromtuple(self, name, alg, keyid, keyttl):
if name.endswith('.'):
fullname = name
name = name.rstrip('.')
else:
fullname = name + '.'
keystr = "K%s+%03d+%05d" % (fullname, alg, keyid)
key_file = self._dir + (self._dir and os.sep or '') + keystr + ".key"
private_file = (self._dir + (self._dir and os.sep or '') +
keystr + ".private")
self.keystr = keystr
self.name = name
self.alg = int(alg)
self.keyid = int(keyid)
self.fullname = fullname
kfp = open(key_file, "r")
for line in kfp:
if line[0] == ';':
continue
tokens = line.split()
if not tokens:
continue
if tokens[1].lower() in ('in', 'ch', 'hs'):
septoken = 3
self.ttl = keyttl
else:
septoken = 4
self.ttl = int(tokens[1]) if not keyttl else keyttl
if (int(tokens[septoken]) & 0x1) == 1:
self.sep = True
else:
self.sep = False
kfp.close()
pfp = open(private_file, "rU")
self.metadata = dict()
self._changed = dict()
self._delete = dict()
self._times = dict()
self._fmttime = dict()
self._timestamps = dict()
self._original = dict()
self._origttl = None
for line in pfp:
line = line.strip()
if not line or line[0] in ('!#'):
continue
punctuation = [line.find(c) for c in ':= '] + [len(line)]
found = min([pos for pos in punctuation if pos != -1])
name = line[:found].rstrip()
value = line[found:].lstrip(":= ").rstrip()
self.metadata[name] = value
for prop in dnskey._PROPS:
self._changed[prop] = False
if prop in self.metadata:
t = self.parsetime(self.metadata[prop])
self._times[prop] = t
self._fmttime[prop] = self.formattime(t)
self._timestamps[prop] = self.epochfromtime(t)
self._original[prop] = self._timestamps[prop]
else:
self._times[prop] = None
self._fmttime[prop] = None
self._timestamps[prop] = None
self._original[prop] = None
pfp.close()
def commit(self, settime_bin, **kwargs):
quiet = kwargs.get('quiet', False)
cmd = []
first = True
if self._origttl is not None:
cmd += ["-L", str(self.ttl)]
for prop, opt in zip(dnskey._PROPS, dnskey._OPTS):
if not opt or not self._changed[prop]:
continue
delete = False
if prop in self._delete and self._delete[prop]:
delete = True
when = 'none' if delete else self._fmttime[prop]
cmd += [opt, when]
first = False
if cmd:
fullcmd = [settime_bin, "-K", self._dir] + cmd + [self.keystr,]
if not quiet:
print('# ' + ' '.join(fullcmd))
try:
p = Popen(fullcmd, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if stderr:
raise Exception(str(stderr))
except Exception as e:
raise Exception('unable to run %s: %s' %
(settime_bin, str(e)))
self._origttl = None
for prop in dnskey._PROPS:
self._original[prop] = self._timestamps[prop]
self._changed[prop] = False
@classmethod
def generate(cls, keygen_bin, keys_dir, name, alg, keysize, sep,
ttl, publish=None, activate=None, **kwargs):
quiet = kwargs.get('quiet', False)
keygen_cmd = [keygen_bin, "-q", "-K", keys_dir, "-L", str(ttl)]
if sep:
keygen_cmd.append("-fk")
if alg:
keygen_cmd += ["-a", alg]
if keysize:
keygen_cmd += ["-b", str(keysize)]
if publish:
t = dnskey.timefromepoch(publish)
keygen_cmd += ["-P", dnskey.formattime(t)]
if activate:
t = dnskey.timefromepoch(activate)
keygen_cmd += ["-A", dnskey.formattime(activate)]
keygen_cmd.append(name)
if not quiet:
print('# ' + ' '.join(keygen_cmd))
p = Popen(keygen_cmd, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if stderr:
raise Exception('unable to generate key: ' + str(stderr))
try:
keystr = stdout.splitlines()[0]
newkey = dnskey(keystr, keys_dir, ttl)
return newkey
except Exception as e:
raise Exception('unable to generate key: %s' % str(e))
def generate_successor(self, keygen_bin, **kwargs):
quiet = kwargs.get('quiet', False)
if not self.inactive():
raise Exception("predecessor key %s has no inactive date" % self)
keygen_cmd = [keygen_bin, "-q", "-K", self._dir, "-S", self.keystr]
if self.ttl:
keygen_cmd += ["-L", str(self.ttl)]
if not quiet:
print('# ' + ' '.join(keygen_cmd))
p = Popen(keygen_cmd, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if stderr:
raise Exception('unable to generate key: ' + stderr)
try:
keystr = stdout.splitlines()[0]
newkey = dnskey(keystr, self._dir, self.ttl)
return newkey
except:
raise Exception('unable to generate successor for key %s' % self)
@staticmethod
def algstr(alg):
name = None
if alg in range(len(dnskey._ALGNAMES)):
name = dnskey._ALGNAMES[alg]
return name if name else ("%03d" % alg)
@staticmethod
def algnum(alg):
if not alg:
return None
alg = alg.upper()
try:
return dnskey._ALGNAMES.index(alg)
except ValueError:
return None
def algname(self, alg=None):
return self.algstr(alg or self.alg)
@staticmethod
def timefromepoch(secs):
return time.gmtime(secs)
@staticmethod
def parsetime(string):
return time.strptime(string, "%Y%m%d%H%M%S")
@staticmethod
def epochfromtime(t):
return calendar.timegm(t)
@staticmethod
def formattime(t):
return time.strftime("%Y%m%d%H%M%S", t)
def setmeta(self, prop, secs, now, **kwargs):
force = kwargs.get('force', False)
if self._timestamps[prop] == secs:
return
if self._original[prop] is not None and \
self._original[prop] < now and not force:
raise TimePast(self, prop, self._original[prop])
if secs is None:
self._changed[prop] = False \
if self._original[prop] is None else True
self._delete[prop] = True
self._timestamps[prop] = None
self._times[prop] = None
self._fmttime[prop] = None
return
t = self.timefromepoch(secs)
self._timestamps[prop] = secs
self._times[prop] = t
self._fmttime[prop] = self.formattime(t)
self._changed[prop] = False if \
self._original[prop] == self._timestamps[prop] else True
def gettime(self, prop):
return self._times[prop]
def getfmttime(self, prop):
return self._fmttime[prop]
def gettimestamp(self, prop):
return self._timestamps[prop]
def created(self):
return self._timestamps["Created"]
def syncpublish(self):
return self._timestamps["SyncPublish"]
def setsyncpublish(self, secs, now=time.time(), **kwargs):
self.setmeta("SyncPublish", secs, now, **kwargs)
def publish(self):
return self._timestamps["Publish"]
def setpublish(self, secs, now=time.time(), **kwargs):
self.setmeta("Publish", secs, now, **kwargs)
def activate(self):
return self._timestamps["Activate"]
def setactivate(self, secs, now=time.time(), **kwargs):
self.setmeta("Activate", secs, now, **kwargs)
def revoke(self):
return self._timestamps["Revoke"]
def setrevoke(self, secs, now=time.time(), **kwargs):
self.setmeta("Revoke", secs, now, **kwargs)
def inactive(self):
return self._timestamps["Inactive"]
def setinactive(self, secs, now=time.time(), **kwargs):
self.setmeta("Inactive", secs, now, **kwargs)
def delete(self):
return self._timestamps["Delete"]
def setdelete(self, secs, now=time.time(), **kwargs):
self.setmeta("Delete", secs, now, **kwargs)
def syncdelete(self):
return self._timestamps["SyncDelete"]
def setsyncdelete(self, secs, now=time.time(), **kwargs):
self.setmeta("SyncDelete", secs, now, **kwargs)
def setttl(self, ttl):
if ttl is None or self.ttl == ttl:
return
elif self._origttl is None:
self._origttl = self.ttl
self.ttl = ttl
elif self._origttl == ttl:
self._origttl = None
self.ttl = ttl
else:
self.ttl = ttl
def keytype(self):
return ("KSK" if self.sep else "ZSK")
def __str__(self):
return ("%s/%s/%05d"
% (self.name, self.algname(), self.keyid))
def __repr__(self):
return ("%s/%s/%05d (%s)"
% (self.name, self.algname(), self.keyid,
("KSK" if self.sep else "ZSK")))
def date(self):
return (self.activate() or self.publish() or self.created())
# keys are sorted first by zone name, then by algorithm. within
# the same name/algorithm, they are sorted according to their
# 'date' value: the activation date if set, OR the publication
# if set, OR the creation date.
def __lt__(self, other):
if self.name != other.name:
return self.name < other.name
if self.alg != other.alg:
return self.alg < other.alg
return self.date() < other.date()
def check_prepub(self, output=None):
def noop(*args, **kwargs): pass
if not output:
output = noop
now = int(time.time())
a = self.activate()
p = self.publish()
if not a:
return False
if not p:
if a > now:
output("WARNING: Key %s is scheduled for\n"
"\t activation but not for publication."
% repr(self))
return False
if p <= now and a <= now:
return True
if p == a:
output("WARNING: %s is scheduled to be\n"
"\t published and activated at the same time. This\n"
"\t could result in a coverage gap if the zone was\n"
"\t previously signed. Activation should be at least\n"
"\t %s after publication."
% (repr(self),
dnskey.duration(self.ttl) or 'one DNSKEY TTL'))
return True
if a < p:
output("WARNING: Key %s is active before it is published"
% repr(self))
return False
if self.ttl is not None and a - p < self.ttl:
output("WARNING: Key %s is activated too soon\n"
"\t after publication; this could result in coverage \n"
"\t gaps due to resolver caches containing old data.\n"
"\t Activation should be at least %s after\n"
"\t publication."
% (repr(self),
dnskey.duration(self.ttl) or 'one DNSKEY TTL'))
return False
return True
def check_postpub(self, output = None, timespan = None):
def noop(*args, **kwargs): pass
if output is None:
output = noop
if timespan is None:
timespan = self.ttl
now = time.time()
d = self.delete()
i = self.inactive()
if not d:
return False
if not i:
if d > now:
output("WARNING: Key %s is scheduled for\n"
"\t deletion but not for inactivation." % repr(self))
return False
if d < now and i < now:
return True
if d < i:
output("WARNING: Key %s is scheduled for\n"
"\t deletion before inactivation."
% repr(self))
return False
if d - i < timespan:
output("WARNING: Key %s scheduled for\n"
"\t deletion too soon after deactivation; this may \n"
"\t result in coverage gaps due to resolver caches\n"
"\t containing old data. Deletion should be at least\n"
"\t %s after inactivation."
% (repr(self), dnskey.duration(timespan)))
return False
return True
@staticmethod
def duration(secs):
if not secs:
return None
units = [("year", 60*60*24*365),
("month", 60*60*24*30),
("day", 60*60*24),
("hour", 60*60),
("minute", 60),
("second", 1)]
output = []
for unit in units:
v, secs = secs // unit[1], secs % unit[1]
if v > 0:
output.append("%d %s%s" % (v, unit[0], "s" if v > 1 else ""))
return ", ".join(output)

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

@@ -0,0 +1,171 @@
############################################################################
# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
from collections import defaultdict
from .dnskey import *
from .keydict import *
from .keyevent import *
class eventlist:
_K = defaultdict(lambda: defaultdict(list))
_Z = defaultdict(lambda: defaultdict(list))
_zones = set()
_kdict = None
def __init__(self, kdict):
properties = ["SyncPublish", "Publish", "SyncDelete",
"Activate", "Inactive", "Delete"]
self._kdict = kdict
for zone in kdict.zones():
self._zones.add(zone)
for alg, keys in kdict[zone].items():
for k in keys.values():
for prop in properties:
t = k.gettime(prop)
if not t:
continue
e = keyevent(prop, k, t)
if k.sep:
self._K[zone][alg].append(e)
else:
self._Z[zone][alg].append(e)
self._K[zone][alg] = sorted(self._K[zone][alg],
key=lambda event: event.when)
self._Z[zone][alg] = sorted(self._Z[zone][alg],
key=lambda event: event.when)
# scan events per zone, algorithm, and key type, in order of
# occurrance, noting inconsistent states when found
def coverage(self, zone, keytype, until, output = None):
def noop(*args, **kwargs): pass
if not output:
output = noop
no_zsk = True if (keytype and keytype == "KSK") else False
no_ksk = True if (keytype and keytype == "ZSK") else False
kok = zok = True
found = False
if zone and not zone in self._zones:
output("ERROR: No key events found for %s" % zone)
return False
if zone:
found = True
if not no_ksk:
kok = self.checkzone(zone, "KSK", until, output)
if not no_zsk:
zok = self.checkzone(zone, "ZSK", until, output)
else:
for z in self._zones:
if not no_ksk and z in self._K.keys():
found = True
kok = self.checkzone(z, "KSK", until, output)
if not no_zsk and z in self._Z.keys():
found = True
kok = self.checkzone(z, "ZSK", until, output)
if not found:
output("ERROR: No key events found")
return False
return (kok and zok)
def checkzone(self, zone, keytype, until, output):
allok = True
if keytype == "KSK":
kz = self._K[zone]
else:
kz = self._Z[zone]
for alg in kz.keys():
output("Checking scheduled %s events for zone %s, "
"algorithm %s..." %
(keytype, zone, dnskey.algstr(alg)))
ok = eventlist.checkset(kz[alg], keytype, until, output)
if ok:
output("No errors found")
allok = allok and ok
return allok
@staticmethod
def showset(eventset, output):
if not eventset:
return
output(" " + eventset[0].showtime() + ":", skip=False)
for event in eventset:
output(" %s: %s" % (event.what, repr(event.key)), skip=False)
@staticmethod
def checkset(eventset, keytype, until, output):
groups = list()
group = list()
# collect up all events that have the same time
eventsfound = False
for event in eventset:
# we found an event
eventsfound = True
# add event to current group
if (not group or group[0].when == event.when):
group.append(event)
# if we're at the end of the list, we're done. if
# we've found an event with a later time, start a new group
if (group[0].when != event.when):
groups.append(group)
group = list()
group.append(event)
if group:
groups.append(group)
if not eventsfound:
output("ERROR: No %s events found" % keytype)
return False
active = published = None
for group in groups:
if (until and calendar.timegm(group[0].when) > until):
output("Ignoring events after %s" %
time.strftime("%a %b %d %H:%M:%S UTC %Y",
time.gmtime(until)))
return True
for event in group:
(active, published) = event.status(active, published)
eventlist.showset(group, output)
# and then check for inconsistencies:
if not active:
output("ERROR: No %s's are active after this event" % keytype)
return False
elif not published:
output("ERROR: No %s's are published after this event"
% keytype)
return False
elif not published.intersection(active):
output("ERROR: No %s's are both active and published "
"after this event" % keytype)
return False
return True

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

@@ -0,0 +1,89 @@
############################################################################
# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
from collections import defaultdict
from . import dnskey
import os
import glob
########################################################################
# Class keydict
########################################################################
class keydict:
""" A dictionary of keys, indexed by name, algorithm, and key id """
_keydict = defaultdict(lambda: defaultdict(dict))
_defttl = None
_missing = []
def __init__(self, dp=None, **kwargs):
self._defttl = kwargs.get('keyttl', None)
zones = kwargs.get('zones', None)
if not zones:
path = kwargs.get('path',None) or '.'
self.readall(path)
else:
for zone in zones:
if 'path' in kwargs and kwargs['path'] is not None:
path = kwargs['path']
else:
path = dp and dp.policy(zone).directory or '.'
if not self.readone(path, zone):
self._missing.append(zone)
def readall(self, path):
files = glob.glob(os.path.join(path, '*.private'))
for infile in files:
key = dnskey(infile, path, self._defttl)
self._keydict[key.name][key.alg][key.keyid] = key
def readone(self, path, zone):
match='K' + zone + '.+*.private'
files = glob.glob(os.path.join(path, match))
found = False
for infile in files:
key = dnskey(infile, path, self._defttl)
if key.name != zone: # shouldn't ever happen
continue
self._keydict[key.name][key.alg][key.keyid] = key
found = True
return found
def __iter__(self):
for zone, algorithms in self._keydict.items():
for alg, keys in algorithms.items():
for key in keys.values():
yield key
def __getitem__(self, name):
return self._keydict[name]
def zones(self):
return (self._keydict.keys())
def algorithms(self, zone):
return (self._keydict[zone].keys())
def keys(self, zone, alg):
return (self._keydict[zone][alg].keys())
def missing(self):
return (self._missing)

View File

@@ -0,0 +1,81 @@
############################################################################
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import time
########################################################################
# Class keyevent
########################################################################
class keyevent:
""" A discrete key event, e.g., Publish, Activate, Inactive, Delete,
etc. Stores the date of the event, and identifying information
about the key to which the event will occur."""
def __init__(self, what, key, when=None):
self.what = what
self.when = when or key.gettime(what)
self.key = key
self.sep = key.sep
self.zone = key.name
self.alg = key.alg
self.keyid = key.keyid
def __repr__(self):
return repr((self.when, self.what, self.keyid, self.sep,
self.zone, self.alg))
def showtime(self):
return time.strftime("%a %b %d %H:%M:%S UTC %Y", self.when)
# update sets of active and published keys, based on
# the contents of this keyevent
def status(self, active, published, output = None):
def noop(*args, **kwargs): pass
if not output:
output = noop
if not active:
active = set()
if not published:
published = set()
if self.what == "Activate":
active.add(self.keyid)
elif self.what == "Publish":
published.add(self.keyid)
elif self.what == "Inactive":
if self.keyid not in active:
output("\tWARNING: %s scheduled to become inactive "
"before it is active"
% repr(self.key))
else:
active.remove(self.keyid)
elif self.what == "Delete":
if self.keyid in published:
published.remove(self.keyid)
else:
output("WARNING: key %s is scheduled for deletion "
"before it is published" % repr(self.key))
elif self.what == "Revoke":
# We don't need to worry about the logic of this one;
# just stop counting this key as either active or published
if self.keyid in published:
published.remove(self.keyid)
if self.keyid in active:
active.remove(self.keyid)
return active, published

152
bin/python/isc/keymgr.py Normal file
View File

@@ -0,0 +1,152 @@
############################################################################
# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
from __future__ import print_function
import os, sys, argparse, glob, re, time, calendar, pprint
from collections import defaultdict
prog='dnssec-keymgr'
from isc import *
from isc.utils import prefix
############################################################################
# print a fatal error and exit
############################################################################
def fatal(*args, **kwargs):
print(*args, **kwargs)
sys.exit(1)
############################################################################
# find the location of an external command
############################################################################
def set_path(command, default=None):
""" find the location of a specified command. If a default is supplied,
exists and it's an executable, we use it; otherwise we search PATH
for an alternative.
:param command: command to look for
:param default: default value to use
:return: PATH with the location of a suitable binary
"""
fpath = default
if not fpath or not os.path.isfile(fpath) or not os.access(fpath, os.X_OK):
path = os.environ["PATH"]
if not path:
path = os.path.defpath
for directory in path.split(os.pathsep):
fpath = directory + os.sep + command
if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
break
fpath = None
return fpath
############################################################################
# parse arguments
############################################################################
def parse_args():
""" Read command line arguments, returns 'args' object
:return: args object properly prepared
"""
keygen = set_path('dnssec-keygen',
os.path.join(prefix('sbin'), 'dnssec-keygen'))
settime = set_path('dnssec-settime',
os.path.join(prefix('sbin'), 'dnssec-settime'))
parser = argparse.ArgumentParser(description=prog + ': schedule '
'DNSSEC key rollovers according to a '
'pre-defined policy')
parser.add_argument('zone', type=str, nargs='*', default=None,
help='Zone(s) to which the policy should be applied ' +
'(default: all zones in the directory)')
parser.add_argument('-K', dest='path', type=str,
help='Directory containing keys', metavar='dir')
parser.add_argument('-c', dest='policyfile', type=str,
help='Policy definition file', metavar='file')
parser.add_argument('-g', dest='keygen', default=keygen, type=str,
help='Path to \'dnssec-keygen\'',
metavar='path')
parser.add_argument('-s', dest='settime', default=settime, type=str,
help='Path to \'dnssec-settime\'',
metavar='path')
parser.add_argument('-k', dest='no_zsk',
action='store_true', default=False,
help='Only apply policy to key-signing keys (KSKs)')
parser.add_argument('-z', dest='no_ksk',
action='store_true', default=False,
help='Only apply policy to zone-signing keys (ZSKs)')
parser.add_argument('-f', '--force', dest='force', action='store_true',
default=False, help='Force updates to key events '+
'even if they are in the past')
parser.add_argument('-q', '--quiet', dest='quiet', action='store_true',
default=False, help='Update keys silently')
parser.add_argument('-v', '--version', action='version',
version=utils.version)
args = parser.parse_args()
if args.no_zsk and args.no_ksk:
fatal("ERROR: -z and -k cannot be used together.")
if args.keygen is None or args.settime is None:
fatal("ERROR: dnssec-keygen/dnssec-settime not found")
# if a policy file was specified, check that it exists.
# if not, use the default file, unless it doesn't exist
if args.policyfile is not None:
if not os.path.exists(args.policyfile):
fatal('ERROR: Policy file "%s" not found' % args.policyfile)
else:
args.policyfile = os.path.join(utils.sysconfdir, 'policy.conf')
if not os.path.exists(args.policyfile):
args.policyfile = None
return args
############################################################################
# main
############################################################################
def main():
args = parse_args()
# As we may have specific locations for the binaries, we put that info
# into a context object that can be passed around
context = {'keygen_path': args.keygen,
'settime_path': args.settime,
'keys_path': args.path}
try:
dp = policy.dnssec_policy(args.policyfile)
except Exception as e:
fatal('Unable to load DNSSEC policy: ' + str(e))
try:
kd = keydict(dp, path=args.path, zones=args.zone)
except Exception as e:
fatal('Unable to build key dictionary: ' + str(e))
try:
ks = keyseries(kd, context=context)
except Exception as e:
fatal('Unable to build key series: ' + str(e))
try:
ks.enforce_policy(dp, ksk=args.no_zsk, zsk=args.no_ksk,
force=args.force, quiet=args.quiet)
except Exception as e:
fatal('Unable to apply policy: ' + str(e))

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

@@ -0,0 +1,194 @@
############################################################################
# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
from collections import defaultdict
from .dnskey import *
from .keydict import *
from .keyevent import *
from .policy import *
import time
class keyseries:
_K = defaultdict(lambda: defaultdict(list))
_Z = defaultdict(lambda: defaultdict(list))
_zones = set()
_kdict = None
_context = None
def __init__(self, kdict, now=time.time(), context=None):
self._kdict = kdict
self._context = context
self._zones = set(kdict.missing())
for zone in kdict.zones():
self._zones.add(zone)
for alg, keys in kdict[zone].items():
for k in keys.values():
if k.sep:
self._K[zone][alg].append(k)
else:
self._Z[zone][alg].append(k)
for group in [self._K[zone][alg], self._Z[zone][alg]]:
group.sort()
for k in group:
if k.delete() and k.delete() < now:
group.remove(k)
def __iter__(self):
for zone in self._zones:
for collection in [self._K, self._Z]:
if zone not in collection:
continue
for alg, keys in collection[zone].items():
for key in keys:
yield key
def dump(self):
for k in self:
print("%s" % repr(k))
def fixseries(self, keys, policy, now, **kwargs):
force = kwargs.get('force', False)
if not keys:
return
# handle the first key
key = keys[0]
if key.sep:
rp = policy.ksk_rollperiod
prepub = policy.ksk_prepublish or (30 * 86400)
postpub = policy.ksk_postpublish or (30 * 86400)
else:
rp = policy.zsk_rollperiod
prepub = policy.zsk_prepublish or (30 * 86400)
postpub = policy.zsk_postpublish or (30 * 86400)
# the first key should be published and active
p = key.publish()
a = key.activate()
if not p or p > now:
key.setpublish(now)
if not a or a > now:
key.setactivate(now)
if not rp:
key.setinactive(None, **kwargs)
key.setdelete(None, **kwargs)
else:
key.setinactive(a + rp, **kwargs)
key.setdelete(a + rp + postpub, **kwargs)
if policy.keyttl != key.ttl:
key.setttl(policy.keyttl)
# handle all the subsequent keys
prev = key
for key in keys[1:]:
# if no rollperiod, then all keys after the first in
# the series kept inactive.
# (XXX: we need to change this to allow standby keys)
if not rp:
key.setpublish(None, **kwargs)
key.setactivate(None, **kwargs)
key.setinactive(None, **kwargs)
key.setdelete(None, **kwargs)
if policy.keyttl != key.ttl:
key.setttl(policy.keyttl)
continue
# otherwise, ensure all dates are set correctly based on
# the initial key
a = prev.inactive()
p = a - prepub
key.setactivate(a, **kwargs)
key.setpublish(p, **kwargs)
key.setinactive(a + rp, **kwargs)
key.setdelete(a + rp + postpub, **kwargs)
prev.setdelete(a + postpub, **kwargs)
if policy.keyttl != key.ttl:
key.setttl(policy.keyttl)
prev = key
# if we haven't got sufficient coverage, create successor key(s)
while rp and prev.inactive() and \
prev.inactive() < now + policy.coverage:
# commit changes to predecessor: a successor can only be
# generated if Inactive has been set in the predecessor key
prev.commit(self._context['settime_path'], **kwargs)
key = prev.generate_successor(self._context['keygen_path'],
**kwargs)
key.setinactive(key.activate() + rp, **kwargs)
key.setdelete(key.inactive() + postpub, **kwargs)
keys.append(key)
prev = key
# last key? we already know we have sufficient coverage now, so
# disable the inactivation of the final key (if it was set),
# ensuring that if dnssec-keymgr isn't run again, the last key
# in the series will at least remain usable.
prev.setinactive(None, **kwargs)
prev.setdelete(None, **kwargs)
# commit changes
for key in keys:
key.commit(self._context['settime_path'], **kwargs)
def enforce_policy(self, policies, now=time.time(), **kwargs):
# If zones is provided as a parameter, use that list.
# If not, use what we have in this object
zones = kwargs.get('zones', self._zones)
keys_dir = kwargs.get('dir', self._context.get('keys_path', None))
force = kwargs.get('force', False)
for zone in zones:
collections = []
policy = policies.policy(zone)
keys_dir = keys_dir or policy.directory or '.'
alg = policy.algorithm
algnum = dnskey.algnum(alg)
if 'ksk' not in kwargs or not kwargs['ksk']:
if len(self._Z[zone][algnum]) == 0:
k = dnskey.generate(self._context['keygen_path'],
keys_dir, zone, alg,
policy.zsk_keysize, False,
policy.keyttl or 3600,
**kwargs)
self._Z[zone][algnum].append(k)
collections.append(self._Z[zone])
if 'zsk' not in kwargs or not kwargs['zsk']:
if len(self._K[zone][algnum]) == 0:
k = dnskey.generate(self._context['keygen_path'],
keys_dir, zone, alg,
policy.ksk_keysize, True,
policy.keyttl or 3600,
**kwargs)
self._K[zone][algnum].append(k)
collections.append(self._K[zone])
for collection in collections:
for algorithm, keys in collection.items():
if algorithm != algnum:
continue
try:
self.fixseries(keys, policy, now, **kwargs)
except Exception as e:
raise Exception('%s/%s: %s' %
(zone, dnskey.algstr(algnum), str(e)))

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

@@ -0,0 +1,60 @@
############################################################################
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import os
import sys
import re
from subprocess import Popen, PIPE
########################################################################
# Exceptions
########################################################################
class KeyZoneException(Exception):
pass
########################################################################
# class keyzone
########################################################################
class keyzone:
"""reads a zone file to find data relevant to keys"""
def __init__(self, name, filename, czpath):
self.maxttl = None
self.keyttl = None
if not name:
return
if not czpath or not os.path.isfile(czpath) \
or not os.access(czpath, os.X_OK):
raise KeyZoneException('"named-compilezone" not found')
return
maxttl = keyttl = None
fp, _ = Popen([czpath, "-o", "-", name, filename],
stdout=PIPE, stderr=PIPE).communicate()
for line in fp.splitlines():
if re.search('^[:space:]*;', line):
continue
fields = line.split()
if not maxttl or int(fields[1]) > maxttl:
maxttl = int(fields[1])
if fields[3] == "DNSKEY":
keyttl = int(fields[1])
self.keyttl = keyttl
self.maxttl = maxttl

690
bin/python/isc/policy.py Normal file
View File

@@ -0,0 +1,690 @@
############################################################################
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
# policy.py
# This module implements the parser for the dnssec.policy file.
############################################################################
import re
import ply.lex as lex
import ply.yacc as yacc
from string import *
from copy import copy
############################################################################
# PolicyLex: a lexer for the policy file syntax.
############################################################################
class PolicyLex:
reserved = ('POLICY',
'ALGORITHM_POLICY',
'ZONE',
'ALGORITHM',
'DIRECTORY',
'KEYTTL',
'KEY_SIZE',
'ROLL_PERIOD',
'PRE_PUBLISH',
'POST_PUBLISH',
'COVERAGE',
'STANDBY',
'NONE')
tokens = reserved + ('DATESUFFIX',
'KEYTYPE',
'ALGNAME',
'STR',
'QSTRING',
'NUMBER',
'LBRACE',
'RBRACE',
'SEMI')
reserved_map = {}
t_ignore = ' \t'
t_ignore_olcomment = r'(//|\#).*'
t_LBRACE = r'\{'
t_RBRACE = r'\}'
t_SEMI = r';';
def t_newline(self, t):
r'\n+'
t.lexer.lineno += t.value.count("\n")
def t_comment(self, t):
r'/\*(.|\n)*?\*/'
t.lexer.lineno += t.value.count('\n')
def t_DATESUFFIX(self, t):
r'(?i)(?<=[0-9 \t])(y(?:ears|ear|ea|e)?|mo(?:nths|nth|nt|n)?|w(?:eeks|eek|ee|e)?|d(?:ays|ay|a)?|h(?:ours|our|ou|o)?|mi(?:nutes|nute|nut|nu|n)?|s(?:econds|econd|econ|eco|ec|e)?)\b'
t.value = re.match(r'(?i)(y|mo|w|d|h|mi|s)([a-z]*)', t.value).group(1).lower()
return t
def t_KEYTYPE(self, t):
r'(?i)\b(KSK|ZSK)\b'
t.value = t.value.upper()
return t
def t_ALGNAME(self, t):
r'(?i)\b(RSAMD5|DH|DSA|NSEC3DSA|ECC|RSASHA1|NSEC3RSASHA1|RSASHA256|RSASHA512|ECCGOST|ECDSAP256SHA245|ECDSAP384SHA384)\b'
t.value = t.value.upper()
return t
def t_STR(self, t):
r'[A-Za-z._-][\w._-]*'
t.type = self.reserved_map.get(t.value, "STR")
return t
def t_QSTRING(self, t):
r'"([^"\n]|(\\"))*"'
t.type = self.reserved_map.get(t.value, "QSTRING")
t.value = t.value[1:-1]
return t
def t_NUMBER(self, t):
r'\d+'
t.value = int(t.value)
return t
def t_error(self, t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1)
def __init__(self, **kwargs):
for r in self.reserved:
self.reserved_map[r.lower().translate(maketrans('_', '-'))] = r
self.lexer = lex.lex(object=self, **kwargs)
def test(self, text):
self.lexer.input(text)
while True:
t = self.lexer.token()
if not t:
break
print(t)
############################################################################
# Policy: this object holds a set of DNSSEC policy settings.
############################################################################
class Policy:
is_zone = False
is_alg = False
is_constructed = False
ksk_rollperiod = None
zsk_rollperiod = None
ksk_prepublish = None
zsk_prepublish = None
ksk_postpublish = None
zsk_postpublish = None
ksk_keysize = None
zsk_keysize = None
ksk_standby = None
zsk_standby = None
keyttl = None
coverage = None
directory = None
valid_key_sz_per_algo = {'DSA': [512, 1024],
'NSEC3DSA': [512, 1024],
'RSAMD5': [512, 4096],
'RSASHA1': [512, 4096],
'NSEC3RSASHA1': [512, 4096],
'RSASHA256': [512, 4096],
'RSASHA512': [512, 4096],
'ECCGOST': None,
'ECDSAP256SHA245': None,
'ECDSAP384SHA384': None}
def __init__(self, name=None, algorithm=None, parent=None):
self.name = name
self.algorithm = algorithm
self.parent = parent
pass
def __repr__(self):
return ("%spolicy %s:\n"
"\tinherits %s\n"
"\tdirectory %s\n"
"\talgorithm %s\n"
"\tcoverage %s\n"
"\tksk_keysize %s\n"
"\tzsk_keysize %s\n"
"\tksk_rollperiod %s\n"
"\tzsk_rollperiod %s\n"
"\tksk_prepublish %s\n"
"\tksk_postpublish %s\n"
"\tzsk_prepublish %s\n"
"\tzsk_postpublish %s\n"
"\tksk_standby %s\n"
"\tzsk_standby %s\n"
"\tkeyttl %s\n"
%
((self.is_constructed and 'constructed ' or \
self.is_zone and 'zone ' or \
self.is_alg and 'algorithm ' or ''),
self.name or 'UNKNOWN',
self.parent and self.parent.name or 'None',
self.directory and ('"' + str(self.directory) + '"') or 'None',
self.algorithm or 'None',
self.coverage and str(self.coverage) or 'None',
self.ksk_keysize and str(self.ksk_keysize) or 'None',
self.zsk_keysize and str(self.zsk_keysize) or 'None',
self.ksk_rollperiod and str(self.ksk_rollperiod) or 'None',
self.zsk_rollperiod and str(self.zsk_rollperiod) or 'None',
self.ksk_prepublish and str(self.ksk_prepublish) or 'None',
self.ksk_postpublish and str(self.ksk_postpublish) or 'None',
self.zsk_prepublish and str(self.zsk_prepublish) or 'None',
self.zsk_postpublish and str(self.zsk_postpublish) or 'None',
self.ksk_standby and str(self.ksk_standby) or 'None',
self.zsk_standby and str(self.zsk_standby) or 'None',
self.keyttl and str(self.keyttl) or 'None'))
def __verify_size(self, key_size, size_range):
return (size_range[0] <= key_size <= size_range[1])
def get_name(self):
return self.name
def constructed(self):
return self.is_constructed
def validate(self):
""" Check if the values in the policy make sense
:return: True/False if the policy passes validation
"""
if self.ksk_rollperiod and \
self.ksk_prepublish is not None and \
self.ksk_prepublish > self.ksk_rollperiod:
print(self.ksk_rollperiod)
return (False,
('KSK pre-publish period (%d) exceeds rollover period %d'
% (self.ksk_prepublish, self.ksk_rollperiod)))
if self.ksk_rollperiod and \
self.ksk_postpublish is not None and \
self.ksk_postpublish > self.ksk_rollperiod:
return (False,
('KSK post-publish period (%d) exceeds rollover period %d'
% (self.ksk_postpublish, self.ksk_rollperiod)))
if self.zsk_rollperiod and \
self.zsk_prepublish is not None and \
self.zsk_prepublish >= self.zsk_rollperiod:
return (False,
('ZSK pre-publish period (%d) exceeds rollover period %d'
% (self.zsk_prepublish, self.zsk_rollperiod)))
if self.zsk_rollperiod and \
self.zsk_postpublish is not None and \
self.zsk_postpublish >= self.zsk_rollperiod:
return (False,
('ZSK post-publish period (%d) exceeds rollover period %d'
% (self.zsk_postpublish, self.zsk_rollperiod)))
if self.ksk_rollperiod and \
self.ksk_prepublish and self.ksk_postpublish and \
self.ksk_prepublish + self.ksk_postpublish >= self.ksk_rollperiod:
return (False,
(('KSK pre/post-publish periods (%d/%d) ' +
'combined exceed rollover period %d') %
(self.ksk_prepublish,
self.ksk_postpublish,
self.ksk_rollperiod)))
if self.zsk_rollperiod and \
self.zsk_prepublish and self.zsk_postpublish and \
self.zsk_prepublish + self.zsk_postpublish >= self.zsk_rollperiod:
return (False,
(('ZSK pre/post-publish periods (%d/%d) ' +
'combined exceed rollover period %d') %
(self.zsk_prepublish,
self.zsk_postpublish,
self.zsk_rollperiod)))
if self.algorithm is not None:
# Validate the key size
key_sz_range = self.valid_key_sz_per_algo.get(self.algorithm)
if key_sz_range is not None:
# Verify KSK
if not self.__verify_size(self.ksk_keysize, key_sz_range):
return False, 'KSK key size %d outside valid range %s' \
% (self.ksk_keysize, key_sz_range)
# Verify ZSK
if not self.__verify_size(self.zsk_keysize, key_sz_range):
return False, 'ZSK key size %d outside valid range %s' \
% (self.zsk_keysize, key_sz_range)
# Specific check for DSA keys
if self.algorithm in ['DSA', 'NSEC3DSA'] and \
self.ksk_keysize % 64 != 0:
return False, \
('KSK key size %d not divisible by 64 ' +
'as required for DSA') % self.ksk_keysize
if self.algorithm in ['DSA', 'NSEC3DSA'] and \
self.zsk_keysize % 64 != 0:
return False, \
('ZSK key size %d not divisible by 64 ' +
'as required for DSA') % self.zsk_keysize
return True, ''
############################################################################
# dnssec_policy:
# This class reads a dnssec.policy file and creates a dictionary of
# DNSSEC policy rules from which a policy for a specific zone can
# be generated.
############################################################################
class PolicyException(Exception):
pass
class dnssec_policy:
alg_policy = {}
named_policy = {}
zone_policy = {}
current = None
filename = None
initial = True
def __init__(self, filename=None, **kwargs):
self.plex = PolicyLex()
self.tokens = self.plex.tokens
if 'debug' not in kwargs:
kwargs['debug'] = False
if 'write_tables' not in kwargs:
kwargs['write_tables'] = False
self.parser = yacc.yacc(module=self, **kwargs)
# set defaults
self.setup('''policy global { algorithm rsasha256;
key-size ksk 2048;
key-size zsk 2048;
roll-period ksk 0;
roll-period zsk 1y;
pre-publish ksk 1mo;
pre-publish zsk 1mo;
post-publish ksk 1mo;
post-publish zsk 1mo;
standby ksk 0;
standby zsk 0;
keyttl 1h;
coverage 6mo; };
policy default { policy global; };''')
p = Policy()
p.algorithm = None
p.is_alg = True
p.ksk_keysize = 2048;
p.zsk_keysize = 2048;
# set default algorithm policies
# these need a lower default key size:
self.alg_policy['DSA'] = copy(p)
self.alg_policy['DSA'].algorithm = "DSA"
self.alg_policy['DSA'].name = "DSA"
self.alg_policy['DSA'].ksk_keysize = 1024;
self.alg_policy['NSEC3DSA'] = copy(p)
self.alg_policy['NSEC3DSA'].algorithm = "NSEC3DSA"
self.alg_policy['NSEC3DSA'].name = "NSEC3DSA"
self.alg_policy['NSEC3DSA'].ksk_keysize = 1024;
# these can use default settings
self.alg_policy['RSAMD5'] = copy(p)
self.alg_policy['RSAMD5'].algorithm = "RSAMD5"
self.alg_policy['RSAMD5'].name = "RSAMD5"
self.alg_policy['RSASHA1'] = copy(p)
self.alg_policy['RSASHA1'].algorithm = "RSASHA1"
self.alg_policy['RSASHA1'].name = "RSASHA1"
self.alg_policy['NSEC3RSASHA1'] = copy(p)
self.alg_policy['NSEC3RSASHA1'].algorithm = "NSEC3RSASHA1"
self.alg_policy['NSEC3RSASHA1'].name = "NSEC3RSASHA1"
self.alg_policy['RSASHA256'] = copy(p)
self.alg_policy['RSASHA256'].algorithm = "RSASHA256"
self.alg_policy['RSASHA256'].name = "RSASHA256"
self.alg_policy['RSASHA512'] = copy(p)
self.alg_policy['RSASHA512'].algorithm = "RSASHA512"
self.alg_policy['RSASHA512'].name = "RSASHA512"
self.alg_policy['ECCGOST'] = copy(p)
self.alg_policy['ECCGOST'].algorithm = "ECCGOST"
self.alg_policy['ECCGOST'].name = "ECCGOST"
self.alg_policy['ECDSAP256SHA245'] = copy(p)
self.alg_policy['ECDSAP256SHA245'].algorithm = "ECDSAP256SHA256"
self.alg_policy['ECDSAP256SHA245'].name = "ECDSAP256SHA256"
self.alg_policy['ECDSAP384SHA384'] = copy(p)
self.alg_policy['ECDSAP384SHA384'].algorithm = "ECDSAP384SHA384"
self.alg_policy['ECDSAP384SHA384'].name = "ECDSAP384SHA384"
if filename:
self.load(filename)
def load(self, filename):
self.filename = filename
self.initial = True
with open(filename) as f:
text = f.read()
self.plex.lexer.lineno = 0
self.parser.parse(text)
self.filename = None
def setup(self, text):
self.initial = True
self.plex.lexer.lineno = 0
self.parser.parse(text)
def policy(self, zone, **kwargs):
z = zone.lower()
p = None
if z in self.zone_policy:
p = self.zone_policy[z]
if p is None:
p = copy(self.named_policy['default'])
p.name = zone
p.is_constructed = True
if p.algorithm is None:
parent = p.parent or self.named_policy['default']
while parent and not parent.algorithm:
parent = parent.parent
p.algorithm = parent and parent.algorithm or None
if p.algorithm in self.alg_policy:
ap = self.alg_policy[p.algorithm]
else:
raise PolicyException('algorithm not found')
if p.directory is None:
parent = p.parent or self.named_policy['default']
while parent is not None and not parent.directory:
parent = parent.parent
p.directory = parent and parent.directory
if p.coverage is None:
parent = p.parent or self.named_policy['default']
while parent and not parent.coverage:
parent = parent.parent
p.coverage = parent and parent.coverage or ap.coverage
if p.ksk_keysize is None:
parent = p.parent or self.named_policy['default']
while parent.parent and not parent.ksk_keysize:
parent = parent.parent
p.ksk_keysize = parent and parent.ksk_keysize or ap.ksk_keysize
if p.zsk_keysize is None:
parent = p.parent or self.named_policy['default']
while parent.parent and not parent.zsk_keysize:
parent = parent.parent
p.zsk_keysize = parent and parent.zsk_keysize or ap.zsk_keysize
if p.ksk_rollperiod is None:
parent = p.parent or self.named_policy['default']
while parent.parent and not parent.ksk_rollperiod:
parent = parent.parent
p.ksk_rollperiod = parent and \
parent.ksk_rollperiod or ap.ksk_rollperiod
if p.zsk_rollperiod is None:
parent = p.parent or self.named_policy['default']
while parent.parent and not parent.zsk_rollperiod:
parent = parent.parent
p.zsk_rollperiod = parent and \
parent.zsk_rollperiod or ap.zsk_rollperiod
if p.ksk_prepublish is None:
parent = p.parent or self.named_policy['default']
while parent.parent and not parent.ksk_prepublish:
parent = parent.parent
p.ksk_prepublish = parent and \
parent.ksk_prepublish or ap.ksk_prepublish
if p.zsk_prepublish is None:
parent = p.parent or self.named_policy['default']
while parent.parent and not parent.zsk_prepublish:
parent = parent.parent
p.zsk_prepublish = parent and \
parent.zsk_prepublish or ap.zsk_prepublish
if p.ksk_postpublish is None:
parent = p.parent or self.named_policy['default']
while parent.parent and not parent.ksk_postpublish:
parent = parent.parent
p.ksk_postpublish = parent and \
parent.ksk_postpublish or ap.ksk_postpublish
if p.zsk_postpublish is None:
parent = p.parent or self.named_policy['default']
while parent.parent and not parent.zsk_postpublish:
parent = parent.parent
p.zsk_postpublish = parent and \
parent.zsk_postpublish or ap.zsk_postpublish
if 'novalidate' not in kwargs or not kwargs['novalidate']:
(valid, msg) = p.validate()
if not valid:
raise PolicyException(msg)
return None
return p
def p_policylist(self, p):
'''policylist : init policy
| policylist policy'''
pass
def p_init(self, p):
"init :"
self.initial = False
def p_policy(self, p):
'''policy : alg_policy
| zone_policy
| named_policy'''
pass
def p_name(self, p):
'''name : STR
| KEYTYPE
| DATESUFFIX'''
p[0] = p[1]
pass
def p_new_policy(self, p):
"new_policy :"
self.current = Policy()
def p_alg_policy(self, p):
"alg_policy : ALGORITHM_POLICY ALGNAME new_policy alg_option_group SEMI"
self.current.name = p[2]
self.current.is_alg = True
self.alg_policy[p[2]] = self.current
pass
def p_zone_policy(self, p):
"zone_policy : ZONE name new_policy policy_option_group SEMI"
self.current.name = p[2]
self.current.is_zone = True
self.zone_policy[p[2].lower()] = self.current
pass
def p_named_policy(self, p):
"named_policy : POLICY name new_policy policy_option_group SEMI"
self.current.name = p[2]
self.named_policy[p[2].lower()] = self.current
pass
def p_duration_1(self, p):
"duration : NUMBER"
p[0] = p[1]
pass
def p_duration_2(self, p):
"duration : NONE"
p[0] = None
pass
def p_duration_3(self, p):
"duration : NUMBER DATESUFFIX"
if p[2] == "y":
p[0] = p[1] * 31536000 # year
elif p[2] == "mo":
p[0] = p[1] * 2592000 # month
elif p[2] == "w":
p[0] = p[1] * 604800 # week
elif p[2] == "d":
p[0] = p[1] * 86400 # day
elif p[2] == "h":
p[0] = p[1] * 3600 # hour
elif p[2] == "mi":
p[0] = p[1] * 60 # minute
elif p[2] == "s":
p[0] = p[1] # second
else:
raise PolicyException('invalid duration')
def p_policy_option_group(self, p):
"policy_option_group : LBRACE policy_option_list RBRACE"
pass
def p_policy_option_list(self, p):
'''policy_option_list : policy_option SEMI
| policy_option_list policy_option SEMI'''
pass
def p_policy_option(self, p):
'''policy_option : parent_option
| directory_option
| coverage_option
| rollperiod_option
| prepublish_option
| postpublish_option
| keysize_option
| algorithm_option
| keyttl_option
| standby_option'''
pass
def p_alg_option_group(self, p):
"alg_option_group : LBRACE alg_option_list RBRACE"
pass
def p_alg_option_list(self, p):
'''alg_option_list : alg_option SEMI
| alg_option_list alg_option SEMI'''
pass
def p_alg_option(self, p):
'''alg_option : coverage_option
| rollperiod_option
| prepublish_option
| postpublish_option
| keyttl_option
| keysize_option
| standby_option'''
pass
def p_parent_option(self, p):
"parent_option : POLICY name"
self.current.parent = self.named_policy[p[2].lower()]
def p_directory_option(self, p):
"directory_option : DIRECTORY QSTRING"
self.current.directory = p[2]
def p_coverage_option(self, p):
"coverage_option : COVERAGE duration"
self.current.coverage = p[2]
def p_rollperiod_option(self, p):
"rollperiod_option : ROLL_PERIOD KEYTYPE duration"
if p[2] == "KSK":
self.current.ksk_rollperiod = p[3]
else:
self.current.zsk_rollperiod = p[3]
def p_prepublish_option(self, p):
"prepublish_option : PRE_PUBLISH KEYTYPE duration"
if p[2] == "KSK":
self.current.ksk_prepublish = p[3]
else:
self.current.zsk_prepublish = p[3]
def p_postpublish_option(self, p):
"postpublish_option : POST_PUBLISH KEYTYPE duration"
if p[2] == "KSK":
self.current.ksk_postpublish = p[3]
else:
self.current.zsk_postpublish = p[3]
def p_keysize_option(self, p):
"keysize_option : KEY_SIZE KEYTYPE NUMBER"
if p[2] == "KSK":
self.current.ksk_keysize = p[3]
else:
self.current.zsk_keysize = p[3]
def p_standby_option(self, p):
"standby_option : STANDBY KEYTYPE NUMBER"
if p[2] == "KSK":
self.current.ksk_standby = p[3]
else:
self.current.zsk_standby = p[3]
def p_keyttl_option(self, p):
"keyttl_option : KEYTTL duration"
self.current.keyttl = p[2]
def p_algorithm_option(self, p):
"algorithm_option : ALGORITHM ALGNAME"
self.current.algorithm = p[2]
def p_error(self, p):
if p:
print("%s%s%d:syntax error near '%s'" %
(self.filename or "", ":" if self.filename else "",
p.lineno, p.value))
else:
if not self.initial:
raise PolicyException("%s%s%d:unexpected end of input" %
(self.filename or "", ":" if self.filename else "",
p and p.lineno or 0))
if __name__ == "__main__":
import sys
if sys.argv[1] == "lex":
file = open(sys.argv[2])
text = file.read()
file.close()
plex = PolicyLex(debug=1)
plex.test(text)
elif sys.argv[1] == "parse":
try:
pp = dnssec_policy(sys.argv[2], write_tables=True, debug=True)
print(pp.named_policy['default'])
print(pp.policy("nonexistent.zone"))
except Exception as e:
print(e.args[0])

View File

@@ -0,0 +1,33 @@
# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
srcdir = @srcdir@
VPATH = @srcdir@
top_srcdir = @top_srcdir@
@BIND9_MAKE_INCLUDES@
PYTHON = @PYTHON@
PYTESTS = dnskey_test.py policy_test.py
@BIND9_MAKE_RULES@
check test:
for test in $(PYTESTS); do \
$(PYTHON) $$test; \
done
clean distclean::
rm -f *.pyc

View File

@@ -0,0 +1,57 @@
############################################################################
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import sys
import unittest
sys.path.append('../..')
from isc import *
kdict = None
def getkey():
global kdict
if not kdict:
kd = keydict(path='testdata')
for key in kd:
return key
class DnskeyTest(unittest.TestCase):
def test_metdata(self):
key = getkey()
self.assertEqual(key.created(), 1448055647)
self.assertEqual(key.publish(), 1445463714)
self.assertEqual(key.activate(), 1448055714)
self.assertEqual(key.revoke(), 1479591714)
self.assertEqual(key.inactive(), 1511127714)
self.assertEqual(key.delete(), 1542663714)
self.assertEqual(key.syncpublish(), 1442871714)
self.assertEqual(key.syncdelete(), 1448919714)
def test_fmttime(self):
key = getkey()
self.assertEqual(key.getfmttime('Created'), '20151120214047')
self.assertEqual(key.getfmttime('Publish'), '20151021214154')
self.assertEqual(key.getfmttime('Activate'), '20151120214154')
self.assertEqual(key.getfmttime('Revoke'), '20161119214154')
self.assertEqual(key.getfmttime('Inactive'), '20171119214154')
self.assertEqual(key.getfmttime('Delete'), '20181119214154')
self.assertEqual(key.getfmttime('SyncPublish'), '20150921214154')
self.assertEqual(key.getfmttime('SyncDelete'), '20151130214154')
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,90 @@
############################################################################
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import sys
import unittest
sys.path.append('../..')
from isc import *
class PolicyTest(unittest.TestCase):
def test_keysize(self):
pol = policy.dnssec_policy()
pol.load('test-policies/01-keysize.pol')
p = pol.policy('good_rsa.test', novalidate=True)
self.assertEqual(p.get_name(), "good_rsa.test")
self.assertEqual(p.constructed(), False)
self.assertEqual(p.validate(), (True, ""))
p = pol.policy('good_dsa.test', novalidate=True)
self.assertEqual(p.get_name(), "good_dsa.test")
self.assertEqual(p.constructed(), False)
self.assertEqual(p.validate(), (True, ""))
p = pol.policy('bad_dsa.test', novalidate=True)
self.assertEqual(p.validate(),
(False, 'ZSK key size 769 not divisible by 64 as required for DSA'))
def test_prepublish(self):
pol = policy.dnssec_policy()
pol.load('test-policies/02-prepublish.pol')
p = pol.policy('good_prepublish.test', novalidate=True)
self.assertEqual(p.validate(), (True, ""))
p = pol.policy('bad_prepublish.test', novalidate=True)
self.assertEqual(p.validate(),
(False, 'KSK pre/post-publish periods '
'(10368000/5184000) combined exceed '
'rollover period 10368000'))
def test_postpublish(self):
pol = policy.dnssec_policy()
pol.load('test-policies/03-postpublish.pol')
p = pol.policy('good_postpublish.test', novalidate=True)
self.assertEqual(p.validate(), (True, ""))
p = pol.policy('bad_postpublish.test', novalidate=True)
self.assertEqual(p.validate(),
(False, 'KSK pre/post-publish periods '
'(10368000/5184000) combined exceed '
'rollover period 10368000'))
def test_combined_pre_post(self):
pol = policy.dnssec_policy()
pol.load('test-policies/04-combined-pre-post.pol')
p = pol.policy('good_combined_pre_post_ksk.test', novalidate=True)
self.assertEqual(p.validate(), (True, ""))
p = pol.policy('bad_combined_pre_post_ksk.test', novalidate=True)
self.assertEqual(p.validate(),
(False, 'KSK pre/post-publish periods '
'(5184000/5184000) combined exceed '
'rollover period 10368000'))
p = pol.policy('good_combined_pre_post_zsk.test', novalidate=True)
self.assertEqual(p.validate(),
(True, ""))
p = pol.policy('bad_combined_pre_post_zsk.test', novalidate=True)
self.assertEqual(p.validate(),
(False, 'ZSK pre/post-publish periods '
'(5184000/5184000) combined exceed '
'rollover period 7776000'))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,41 @@
policy keysize_rsa {
algorithm rsasha1;
coverage 1y;
roll-period zsk 3mo;
pre-publish zsk 2w;
post-publish zsk 2w;
roll-period ksk 1y;
pre-publish ksk 1mo;
post-publish ksk 2mo;
keyttl 1h;
key-size ksk 2048;
key-size zsk 1024;
};
policy keysize_dsa {
algorithm dsa;
coverage 1y;
key-size ksk 2048;
key-size zsk 1024;
};
zone good_rsa.test {
policy keysize_rsa;
};
zone bad_rsa.test {
policy keysize_rsa;
key-size ksk 511;
};
zone good_dsa.test {
policy keysize_dsa;
key-size ksk 1024;
key-size zsk 768;
};
zone bad_dsa.test {
policy keysize_dsa;
key-size ksk 1024;
key-size zsk 769;
};

View File

@@ -0,0 +1,31 @@
policy prepublish_rsa {
algorithm rsasha1;
coverage 1y;
roll-period zsk 3mo;
pre-publish zsk 2w;
post-publish zsk 2w;
roll-period ksk 1y;
pre-publish ksk 1mo;
post-publish ksk 2mo;
keyttl 1h;
key-size ksk 2048;
key-size zsk 1024;
};
// Policy that defines a pre-publish period lower than the rollover period
zone good_prepublish.test {
policy prepublish_rsa;
coverage 6mo;
roll-period ksk 4mo;
pre-publish ksk 1mo;
};
// Policy that defines a pre-publish period equal to the rollover period
zone bad_prepublish.test {
policy prepublish_rsa;
coverage 6mo;
roll-period ksk 4mo;
pre-publish ksk 4mo;
};

View File

@@ -0,0 +1,31 @@
policy postpublish_rsa {
algorithm rsasha1;
coverage 1y;
roll-period zsk 3mo;
pre-publish zsk 2w;
post-publish zsk 2w;
roll-period ksk 1y;
pre-publish ksk 1mo;
post-publish ksk 2mo;
keyttl 1h;
key-size ksk 2048;
key-size zsk 1024;
};
// Policy that defines a post-publish period lower than the rollover period
zone good_postpublish.test {
policy postpublish_rsa;
coverage 6mo;
roll-period ksk 4mo;
pre-publish ksk 1mo;
};
// Policy that defines a post-publish period equal to the rollover period
zone bad_postpublish.test {
policy postpublish_rsa;
coverage 6mo;
roll-period ksk 4mo;
pre-publish ksk 4mo;
};

View File

@@ -0,0 +1,55 @@
policy combined_pre_post_rsa {
algorithm rsasha1;
coverage 1y;
roll-period zsk 3mo;
pre-publish zsk 2w;
post-publish zsk 2w;
roll-period ksk 1y;
pre-publish ksk 1mo;
post-publish ksk 2mo;
keyttl 1h;
key-size ksk 2048;
key-size zsk 1024;
};
// Policy that defines a combined pre-publish and post-publish period lower
// than the rollover period
zone good_combined_pre_post_ksk.test {
policy combined_pre_post_rsa;
coverage 6mo;
roll-period ksk 4mo;
pre-publish ksk 1mo;
post-publish ksk 1mo;
};
// Policy that defines a combined pre-publish and post-publish period higher
// than the rollover period
zone bad_combined_pre_post_ksk.test {
policy combined_pre_post_rsa;
coverage 6mo;
roll-period ksk 4mo;
pre-publish ksk 2mo;
post-publish ksk 2mo;
};
// Policy that defines a combined pre-publish and post-publish period lower
// than the rollover period
zone good_combined_pre_post_zsk.test {
policy combined_pre_post_rsa;
coverage 1y;
roll-period zsk 3mo;
pre-publish zsk 1mo;
post-publish zsk 1mo;
};
// Policy that defines a combined pre-publish and post-publish period higher
// than the rollover period
zone bad_combined_pre_post_zsk.test {
policy combined_pre_post_rsa;
coverage 1y;
roll-period zsk 3mo;
pre-publish zsk 2mo;
post-publish zsk 2mo;
};

View File

@@ -0,0 +1,8 @@
; This is a key-signing key, keyid 35529, for example.com.
; Created: 20151120214047 (Fri Nov 20 13:40:47 2015)
; Publish: 20151021214154 (Wed Oct 21 14:41:54 2015)
; Activate: 20151120214154 (Fri Nov 20 13:41:54 2015)
; Revoke: 20161119214154 (Sat Nov 19 13:41:54 2016)
; Inactive: 20171119214154 (Sun Nov 19 13:41:54 2017)
; Delete: 20181119214154 (Mon Nov 19 13:41:54 2018)
example.com. IN DNSKEY 257 3 7 AwEAAbbJK96tY8d4sF6RLxh9SVIhho5s2ZhrcijT5j1SNLECen7QLutj VJPEiG8UgBLaJSGkxPDxOygYv4hwh4JXBSj89o9rNabAJtCa9XzIXSpt /cfiCfvqmcOZb9nepmDCXsC7gn/gbae/4Y5ym9XOiCp8lu+tlFWgRiJ+ kxDGN48rRPrGfpq+SfwM9NUtftVa7B0EFVzDkADKedRj0SSGYOqH+WYH CnWjhPFmgJoAw3/m4slTHW1l+mDwFvsCMjXopg4JV0CNnTybnOmyuIwO LWRhB3q8ze24sYBU1fpE9VAMxZ++4Kqh/2MZFeDAs7iPPKSmI3wkRCW5 pkwDLO5lJ9c=

View File

@@ -0,0 +1,18 @@
Private-key-format: v1.3
Algorithm: 7 (NSEC3RSASHA1)
Modulus: tskr3q1jx3iwXpEvGH1JUiGGjmzZmGtyKNPmPVI0sQJ6ftAu62NUk8SIbxSAEtolIaTE8PE7KBi/iHCHglcFKPz2j2s1psAm0Jr1fMhdKm39x+IJ++qZw5lv2d6mYMJewLuCf+Btp7/hjnKb1c6IKnyW762UVaBGIn6TEMY3jytE+sZ+mr5J/Az01S1+1VrsHQQVXMOQAMp51GPRJIZg6of5ZgcKdaOE8WaAmgDDf+biyVMdbWX6YPAW+wIyNeimDglXQI2dPJuc6bK4jA4tZGEHerzN7bixgFTV+kT1UAzFn77gqqH/YxkV4MCzuI88pKYjfCREJbmmTAMs7mUn1w==
PublicExponent: AQAB
PrivateExponent: jfiM6YU1Rd6Y5qrPsK7HP1Ko54DmNbvmzI1hfGmYYZAyQsNCXjQloix5aAW9QGdNhecrzJUhxJAMXFZC+lrKuD5a56R25JDE1Sw21nft3SHXhuQrqw5Z5hIMTWXhRrBR1lMOFnLj2PJxqCmenp+vJYjl1z20RBmbv/keE15SExFRJIJ3G0lI4V0KxprY5rgsT/vID0pS32f7rmXhgEzyWDyuxceTMidBooD5BSeEmSTYa4rvCVZ2vgnzIGSxjYDPJE2rGve2dpvdXQuujRFaf4+/FzjaOgg35rTtUmC9klfB4D6KJIfc1PNUwcH7V0VJ2fFlgZgMYi4W331QORl9sQ==
Prime1: 479rW3EeoBwHhUKDy5YeyfnMKjhaosrcYhW4resevLzatFrvS/n2KxJnsHoEzmGr2A13naI61RndgVBBOwNDWI3/tQ+aKvcr+V9m4omROV3xYa8s1FsDbEW0Z6G0UheaqRFir8WK98/Lj6Zht1uBXHSPPf91OW0qj+b5gbX7TK8=
Prime2: zXXlxgIq+Ih6kxsUw4Ith0nd/d2P3d42QYPjxYjsg4xYicPAjva9HltnbBQ2lr4JEG9Yyb8KalSnJUSuvXtn7bGfBzLu8W6omCeVWXQVH4NIu9AjpO16NpMKWGRfiHHbbSYJs1daTZKHC2FEmi18MKX/RauHGGOakFQ/3A/GMVk=
Exponent1: 0o9UQ1uHNAIWFedUEHJ/jr7LOrGVYnLpZCmu7+S0K0zzatGz8ets44+FnAyDywdUKFDzKSMm/4SFXRwE4vl2VzYZlp2RLG4PEuRYK9OCF6a6F1UsvjxTItQjIbjIDSnTjMINGnMps0lDa1EpgKsyI3eEQ46eI3TBZ//k6D6G0vM=
Exponent2: d+CYJgXRyJzo17fvT3s+0TbaHWsOq+chROyNEw4m4UIbzpW2XjO8eF/gYgERMLbEVyCAb4XVr+CgfXArfEbqhpciMHMZUyi7mbtOupiuUmqpH1v70Bj3O6xjVtuJmfTEkFSnSEppV+VsgclI26Q6V7Ai1yWTdzl2T0u4zs8tVlE=
Coefficient: E4EYw76gIChdQDn6+Uh44/xH9Uwmvq3OETR8w/kEZ0xQ8AkTdKFKUp84nlR6gN+ljb2mUxERKrVLwnBsU8EbUlo9UccMbBGkkZ/8MyfGCBb9nUyOFtOxdHY2M0MQadesRptXHt/m30XjdohwmT7qfSIENwtgUOHbwFnn7WPMc/k=
Created: 20151120214047
Publish: 20151021214154
Activate: 20151120214154
Revoke: 20161119214154
Inactive: 20171119214154
Delete: 20181119214154
SyncPublish: 20150921214154
SyncDelete: 20151130214154

View File

@@ -0,0 +1,57 @@
############################################################################
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
# utils.py
# Grouping shared code in one place
############################################################################
import os
# These routines permit platform-independent location of BIND 9 tools
if os.name == 'nt':
import win32con
import win32api
def prefix(bindir=''):
if os.name != 'nt':
return os.path.join('@prefix@', bindir)
bind_subkey = "Software\\ISC\\BIND"
h_key = None
key_found = True
try:
h_key = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey)
except:
key_found = False
if key_found:
try:
(named_base, _) = win32api.RegQueryValueEx(h_key, "InstallDir")
except:
key_found = False
win32api.RegCloseKey(h_key)
if key_found:
return os.path.join(named_base, bindir)
return os.path.join(win32api.GetSystemDirectory(), bindir)
def shellquote(s):
if os.name == 'nt':
return '"' + s.replace('"', '"\\"') + '"'
return "'" + s.replace("'", "'\\''") + "'"
version = '@BIND9_VERSION@'
sysconfdir = '@expanded_sysconfdir@'