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

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