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:
3
bin/python/isc/.gitignore
vendored
Normal file
3
bin/python/isc/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
utils.py
|
||||
parsetab.py
|
||||
parser.out
|
67
bin/python/isc/Makefile.in
Normal file
67
bin/python/isc/Makefile.in
Normal 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
|
25
bin/python/isc/__init__.py
Normal file
25
bin/python/isc/__init__.py
Normal 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
189
bin/python/isc/checkds.py
Normal file
@@ -0,0 +1,189 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2012-2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from isc.utils import prefix,version
|
||||
|
||||
prog = 'dnssec-checkds'
|
||||
|
||||
|
||||
############################################################################
|
||||
# SECRR class:
|
||||
# Class for DS/DLV resource record
|
||||
############################################################################
|
||||
class SECRR:
|
||||
hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384'}
|
||||
rrname = ''
|
||||
rrclass = 'IN'
|
||||
keyid = None
|
||||
keyalg = None
|
||||
hashalg = None
|
||||
digest = ''
|
||||
ttl = 0
|
||||
|
||||
def __init__(self, rrtext, dlvname = None):
|
||||
if not rrtext:
|
||||
raise Exception
|
||||
|
||||
fields = rrtext.split()
|
||||
if len(fields) < 7:
|
||||
raise Exception
|
||||
|
||||
if dlvname:
|
||||
self.rrtype = "DLV"
|
||||
self.dlvname = dlvname.lower()
|
||||
parent = fields[0].lower().strip('.').split('.')
|
||||
parent.reverse()
|
||||
dlv = dlvname.split('.')
|
||||
dlv.reverse()
|
||||
while len(dlv) != 0 and len(parent) != 0 and parent[0] == dlv[0]:
|
||||
parent = parent[1:]
|
||||
dlv = dlv[1:]
|
||||
if dlv:
|
||||
raise Exception
|
||||
parent.reverse()
|
||||
self.parent = '.'.join(parent)
|
||||
self.rrname = self.parent + '.' + self.dlvname + '.'
|
||||
else:
|
||||
self.rrtype = "DS"
|
||||
self.rrname = fields[0].lower()
|
||||
|
||||
fields = fields[1:]
|
||||
if fields[0].upper() in ['IN', 'CH', 'HS']:
|
||||
self.rrclass = fields[0].upper()
|
||||
fields = fields[1:]
|
||||
else:
|
||||
self.ttl = int(fields[0])
|
||||
self.rrclass = fields[1].upper()
|
||||
fields = fields[2:]
|
||||
|
||||
if fields[0].upper() != self.rrtype:
|
||||
raise Exception
|
||||
|
||||
self.keyid, self.keyalg, self.hashalg = map(int, fields[1:4])
|
||||
self.digest = ''.join(fields[4:]).upper()
|
||||
|
||||
def __repr__(self):
|
||||
return '%s %s %s %d %d %d %s' % \
|
||||
(self.rrname, self.rrclass, self.rrtype,
|
||||
self.keyid, self.keyalg, self.hashalg, self.digest)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__repr__() == other.__repr__()
|
||||
|
||||
|
||||
############################################################################
|
||||
# check:
|
||||
# Fetch DS/DLV RRset for the given zone from the DNS; fetch DNSKEY
|
||||
# RRset from the masterfile if specified, or from DNS if not.
|
||||
# Generate a set of expected DS/DLV records from the DNSKEY RRset,
|
||||
# and report on congruency.
|
||||
############################################################################
|
||||
def check(zone, args, masterfile=None, lookaside=None):
|
||||
rrlist = []
|
||||
cmd = [args.dig, "+noall", "+answer", "-t", "dlv" if lookaside else "ds",
|
||||
"-q", zone + "." + lookaside if lookaside else zone]
|
||||
fp, _ = Popen(cmd, stdout=PIPE).communicate()
|
||||
|
||||
for line in fp.splitlines():
|
||||
rrlist.append(SECRR(line, lookaside))
|
||||
rrlist = sorted(rrlist, key=lambda rr: (rr.keyid, rr.keyalg, rr.hashalg))
|
||||
|
||||
klist = []
|
||||
|
||||
if masterfile:
|
||||
cmd = [args.dsfromkey, "-f", masterfile]
|
||||
if lookaside:
|
||||
cmd += ["-l", lookaside]
|
||||
cmd.append(zone)
|
||||
fp, _ = Popen(cmd, stdout=PIPE).communicate()
|
||||
else:
|
||||
intods, _ = Popen([args.dig, "+noall", "+answer", "-t", "dnskey",
|
||||
"-q", zone], stdout=PIPE).communicate()
|
||||
cmd = [args.dsfromkey, "-f", "-"]
|
||||
if lookaside:
|
||||
cmd += ["-l", lookaside]
|
||||
cmd.append(zone)
|
||||
fp, _ = Popen(cmd, stdin=PIPE, stdout=PIPE).communicate(intods)
|
||||
|
||||
for line in fp.splitlines():
|
||||
klist.append(SECRR(line, lookaside))
|
||||
|
||||
if len(klist) < 1:
|
||||
print ("No DNSKEY records found in zone apex")
|
||||
return False
|
||||
|
||||
found = False
|
||||
for rr in klist:
|
||||
if rr in rrlist:
|
||||
print ("%s for KSK %s/%03d/%05d (%s) found in parent" %
|
||||
(rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
|
||||
rr.keyid, SECRR.hashalgs[rr.hashalg]))
|
||||
found = True
|
||||
else:
|
||||
print ("%s for KSK %s/%03d/%05d (%s) missing from parent" %
|
||||
(rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
|
||||
rr.keyid, SECRR.hashalgs[rr.hashalg]))
|
||||
|
||||
if not found:
|
||||
print ("No %s records were found for any DNSKEY" % ("DLV" if lookaside else "DS"))
|
||||
|
||||
return found
|
||||
|
||||
############################################################################
|
||||
# parse_args:
|
||||
# Read command line arguments, set global 'args' structure
|
||||
############################################################################
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description=prog + ': checks DS coverage')
|
||||
|
||||
bindir = 'bin'
|
||||
sbindir = 'bin' if os.name == 'nt' else 'sbin'
|
||||
|
||||
parser.add_argument('zone', type=str, help='zone to check')
|
||||
parser.add_argument('-f', '--file', dest='masterfile', type=str,
|
||||
help='zone master file')
|
||||
parser.add_argument('-l', '--lookaside', dest='lookaside', type=str,
|
||||
help='DLV lookaside zone')
|
||||
parser.add_argument('-d', '--dig', dest='dig',
|
||||
default=os.path.join(prefix(bindir), 'dig'),
|
||||
type=str, help='path to \'dig\'')
|
||||
parser.add_argument('-D', '--dsfromkey', dest='dsfromkey',
|
||||
default=os.path.join(prefix(sbindir),
|
||||
'dnssec-dsfromkey'),
|
||||
type=str, help='path to \'dig\'')
|
||||
parser.add_argument('-v', '--version', action='version',
|
||||
version=version)
|
||||
args = parser.parse_args()
|
||||
|
||||
args.zone = args.zone.strip('.')
|
||||
if args.lookaside:
|
||||
args.lookaside = args.lookaside.strip('.')
|
||||
|
||||
return args
|
||||
|
||||
|
||||
############################################################################
|
||||
# Main
|
||||
############################################################################
|
||||
def main():
|
||||
args = parse_args()
|
||||
found = check(args.zone, args, args.masterfile, args.lookaside)
|
||||
exit(0 if found else 1)
|
292
bin/python/isc/coverage.py
Normal file
292
bin/python/isc/coverage.py
Normal file
@@ -0,0 +1,292 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
|
||||
from __future__ import print_function
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import glob
|
||||
import re
|
||||
import time
|
||||
import calendar
|
||||
import pprint
|
||||
from collections import defaultdict
|
||||
|
||||
prog = 'dnssec-coverage'
|
||||
|
||||
from isc import *
|
||||
from isc.utils import prefix
|
||||
|
||||
|
||||
############################################################################
|
||||
# print a fatal error and exit
|
||||
############################################################################
|
||||
def fatal(*args, **kwargs):
|
||||
print(*args, **kwargs)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
############################################################################
|
||||
# output:
|
||||
############################################################################
|
||||
_firstline = True
|
||||
def output(*args, **kwargs):
|
||||
"""output text, adding a vertical space this is *not* the first
|
||||
first section being printed since a call to vreset()"""
|
||||
global _firstline
|
||||
if 'skip' in kwargs:
|
||||
skip = kwargs['skip']
|
||||
kwargs.pop('skip', None)
|
||||
else:
|
||||
skip = True
|
||||
if _firstline:
|
||||
_firstline = False
|
||||
elif skip:
|
||||
print('')
|
||||
if args:
|
||||
print(*args, **kwargs)
|
||||
|
||||
|
||||
def vreset():
|
||||
"""reset vertical spacing"""
|
||||
global _firstline
|
||||
_firstline = True
|
||||
|
||||
|
||||
############################################################################
|
||||
# parse_time
|
||||
############################################################################
|
||||
def parse_time(s):
|
||||
""" convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds
|
||||
:param s: String with some text representing a time interval
|
||||
:return: Integer with the number of seconds in the time interval
|
||||
"""
|
||||
s = s.strip()
|
||||
|
||||
# if s is an integer, we're done already
|
||||
try:
|
||||
return int(s)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# try to parse as a number with a suffix indicating unit of time
|
||||
r = re.compile('([0-9][0-9]*)\s*([A-Za-z]*)')
|
||||
m = r.match(s)
|
||||
if not m:
|
||||
raise ValueError("Cannot parse %s" % s)
|
||||
n, unit = m.groups()
|
||||
n = int(n)
|
||||
unit = unit.lower()
|
||||
if unit.startswith('y'):
|
||||
return n * 31536000
|
||||
elif unit.startswith('mo'):
|
||||
return n * 2592000
|
||||
elif unit.startswith('w'):
|
||||
return n * 604800
|
||||
elif unit.startswith('d'):
|
||||
return n * 86400
|
||||
elif unit.startswith('h'):
|
||||
return n * 3600
|
||||
elif unit.startswith('mi'):
|
||||
return n * 60
|
||||
elif unit.startswith('s'):
|
||||
return n
|
||||
else:
|
||||
raise ValueError("Invalid suffix %s" % unit)
|
||||
|
||||
|
||||
############################################################################
|
||||
# set_path:
|
||||
############################################################################
|
||||
def set_path(command, default=None):
|
||||
""" find the location of a specified command. if a default is supplied
|
||||
and it works, we use it; otherwise we search PATH for a match.
|
||||
:param command: string with a command to look for in the path
|
||||
:param default: default location to use
|
||||
:return: detected location for the desired command
|
||||
"""
|
||||
|
||||
fpath = default
|
||||
if not fpath or not os.path.isfile(fpath) or not os.access(fpath, os.X_OK):
|
||||
path = os.environ["PATH"]
|
||||
if not path:
|
||||
path = os.path.defpath
|
||||
for directory in path.split(os.pathsep):
|
||||
fpath = os.path.join(directory, command)
|
||||
if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
|
||||
break
|
||||
fpath = None
|
||||
|
||||
return fpath
|
||||
|
||||
|
||||
############################################################################
|
||||
# parse_args:
|
||||
############################################################################
|
||||
def parse_args():
|
||||
"""Read command line arguments, set global 'args' structure"""
|
||||
compilezone = set_path('named-compilezone',
|
||||
os.path.join(prefix('sbin'), 'named-compilezone'))
|
||||
|
||||
parser = argparse.ArgumentParser(description=prog + ': checks future ' +
|
||||
'DNSKEY coverage for a zone')
|
||||
|
||||
parser.add_argument('zone', type=str, nargs='*', default=None,
|
||||
help='zone(s) to check' +
|
||||
'(default: all zones in the directory)')
|
||||
parser.add_argument('-K', dest='path', default='.', type=str,
|
||||
help='a directory containing keys to process',
|
||||
metavar='dir')
|
||||
parser.add_argument('-f', dest='filename', type=str,
|
||||
help='zone master file', metavar='file')
|
||||
parser.add_argument('-m', dest='maxttl', type=str,
|
||||
help='the longest TTL in the zone(s)',
|
||||
metavar='time')
|
||||
parser.add_argument('-d', dest='keyttl', type=str,
|
||||
help='the DNSKEY TTL', metavar='time')
|
||||
parser.add_argument('-r', dest='resign', default='1944000',
|
||||
type=str, help='the RRSIG refresh interval '
|
||||
'in seconds [default: 22.5 days]',
|
||||
metavar='time')
|
||||
parser.add_argument('-c', dest='compilezone',
|
||||
default=compilezone, type=str,
|
||||
help='path to \'named-compilezone\'',
|
||||
metavar='path')
|
||||
parser.add_argument('-l', dest='checklimit',
|
||||
type=str, default='0',
|
||||
help='Length of time to check for '
|
||||
'DNSSEC coverage [default: 0 (unlimited)]',
|
||||
metavar='time')
|
||||
parser.add_argument('-z', dest='no_ksk',
|
||||
action='store_true', default=False,
|
||||
help='Only check zone-signing keys (ZSKs)')
|
||||
parser.add_argument('-k', dest='no_zsk',
|
||||
action='store_true', default=False,
|
||||
help='Only check key-signing keys (KSKs)')
|
||||
parser.add_argument('-D', '--debug', dest='debug_mode',
|
||||
action='store_true', default=False,
|
||||
help='Turn on debugging output')
|
||||
parser.add_argument('-v', '--version', action='version',
|
||||
version=utils.version)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.no_zsk and args.no_ksk:
|
||||
fatal("ERROR: -z and -k cannot be used together.")
|
||||
elif args.no_zsk or args.no_ksk:
|
||||
args.keytype = "KSK" if args.no_zsk else "ZSK"
|
||||
else:
|
||||
args.keytype = None
|
||||
|
||||
if args.filename and len(args.zone) > 1:
|
||||
fatal("ERROR: -f can only be used with one zone.")
|
||||
|
||||
# convert from time arguments to seconds
|
||||
try:
|
||||
if args.maxttl:
|
||||
m = parse_time(args.maxttl)
|
||||
args.maxttl = m
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if args.keyttl:
|
||||
k = parse_time(args.keyttl)
|
||||
args.keyttl = k
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if args.resign:
|
||||
r = parse_time(args.resign)
|
||||
args.resign = r
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if args.checklimit:
|
||||
lim = args.checklimit
|
||||
r = parse_time(args.checklimit)
|
||||
if r == 0:
|
||||
args.checklimit = None
|
||||
else:
|
||||
args.checklimit = time.time() + r
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# if we've got the values we need from the command line, stop now
|
||||
if args.maxttl and args.keyttl:
|
||||
return args
|
||||
|
||||
# load keyttl and maxttl data from zonefile
|
||||
if args.zone and args.filename:
|
||||
try:
|
||||
zone = keyzone(args.zone[0], args.filename, args.compilezone)
|
||||
args.maxttl = args.maxttl or zone.maxttl
|
||||
args.keyttl = args.maxttl or zone.keyttl
|
||||
except Exception as e:
|
||||
print("Unable to load zone data from %s: " % args.filename, e)
|
||||
|
||||
if not args.maxttl:
|
||||
output("WARNING: Maximum TTL value was not specified. Using 1 week\n"
|
||||
"\t (604800 seconds); re-run with the -m option to get more\n"
|
||||
"\t accurate results.")
|
||||
args.maxttl = 604800
|
||||
|
||||
return args
|
||||
|
||||
############################################################################
|
||||
# Main
|
||||
############################################################################
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
print("PHASE 1--Loading keys to check for internal timing problems")
|
||||
|
||||
try:
|
||||
kd = keydict(path=args.path, zone=args.zone, keyttl=args.keyttl)
|
||||
except Exception as e:
|
||||
fatal('ERROR: Unable to build key dictionary: ' + str(e))
|
||||
|
||||
for key in kd:
|
||||
key.check_prepub(output)
|
||||
if key.sep:
|
||||
key.check_postpub(output)
|
||||
else:
|
||||
key.check_postpub(output, args.maxttl + args.resign)
|
||||
|
||||
output("PHASE 2--Scanning future key events for coverage failures")
|
||||
vreset()
|
||||
|
||||
try:
|
||||
elist = eventlist(kd)
|
||||
except Exception as e:
|
||||
fatal('ERROR: Unable to build event list: ' + str(e))
|
||||
|
||||
errors = False
|
||||
if not args.zone:
|
||||
if not elist.coverage(None, args.keytype, args.checklimit, output):
|
||||
errors = True
|
||||
else:
|
||||
for zone in args.zone:
|
||||
try:
|
||||
if not elist.coverage(zone, args.keytype,
|
||||
args.checklimit, output):
|
||||
errors = True
|
||||
except:
|
||||
output('ERROR: Coverage check failed for zone ' + zone)
|
||||
|
||||
sys.exit(1 if errors else 0)
|
504
bin/python/isc/dnskey.py
Normal file
504
bin/python/isc/dnskey.py
Normal file
@@ -0,0 +1,504 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
|
||||
import os
|
||||
import time
|
||||
import calendar
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
########################################################################
|
||||
# Class dnskey
|
||||
########################################################################
|
||||
class TimePast(Exception):
|
||||
def __init__(self, key, prop, value):
|
||||
super(TimePast, self).__init__('%s time for key %s (%d) is already past'
|
||||
% (prop, key, value))
|
||||
|
||||
class dnskey:
|
||||
"""An individual DNSSEC key. Identified by path, name, algorithm, keyid.
|
||||
Contains a dictionary of metadata events."""
|
||||
|
||||
_PROPS = ('Created', 'Publish', 'Activate', 'Inactive', 'Delete',
|
||||
'Revoke', 'DSPublish', 'SyncPublish', 'SyncDelete')
|
||||
_OPTS = (None, '-P', '-A', '-I', '-D', '-R', None, '-Psync', '-Dsync')
|
||||
|
||||
_ALGNAMES = (None, 'RSAMD5', 'DH', 'DSA', 'ECC', 'RSASHA1',
|
||||
'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None,
|
||||
'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256',
|
||||
'ECDSAP384SHA384')
|
||||
|
||||
def __init__(self, key, directory=None, keyttl=None):
|
||||
# this makes it possible to use algname as a class or instance method
|
||||
if isinstance(key, tuple) and len(key) == 3:
|
||||
self._dir = directory or '.'
|
||||
(name, alg, keyid) = key
|
||||
self.fromtuple(name, alg, keyid, keyttl)
|
||||
|
||||
self._dir = directory or os.path.dirname(key) or '.'
|
||||
key = os.path.basename(key)
|
||||
|
||||
(name, alg, keyid) = key.split('+')
|
||||
name = name[1:-1]
|
||||
alg = int(alg)
|
||||
keyid = int(keyid.split('.')[0])
|
||||
self.fromtuple(name, alg, keyid, keyttl)
|
||||
|
||||
def fromtuple(self, name, alg, keyid, keyttl):
|
||||
if name.endswith('.'):
|
||||
fullname = name
|
||||
name = name.rstrip('.')
|
||||
else:
|
||||
fullname = name + '.'
|
||||
|
||||
keystr = "K%s+%03d+%05d" % (fullname, alg, keyid)
|
||||
key_file = self._dir + (self._dir and os.sep or '') + keystr + ".key"
|
||||
private_file = (self._dir + (self._dir and os.sep or '') +
|
||||
keystr + ".private")
|
||||
|
||||
self.keystr = keystr
|
||||
|
||||
self.name = name
|
||||
self.alg = int(alg)
|
||||
self.keyid = int(keyid)
|
||||
self.fullname = fullname
|
||||
|
||||
kfp = open(key_file, "r")
|
||||
for line in kfp:
|
||||
if line[0] == ';':
|
||||
continue
|
||||
tokens = line.split()
|
||||
if not tokens:
|
||||
continue
|
||||
|
||||
if tokens[1].lower() in ('in', 'ch', 'hs'):
|
||||
septoken = 3
|
||||
self.ttl = keyttl
|
||||
else:
|
||||
septoken = 4
|
||||
self.ttl = int(tokens[1]) if not keyttl else keyttl
|
||||
|
||||
if (int(tokens[septoken]) & 0x1) == 1:
|
||||
self.sep = True
|
||||
else:
|
||||
self.sep = False
|
||||
kfp.close()
|
||||
|
||||
pfp = open(private_file, "rU")
|
||||
|
||||
self.metadata = dict()
|
||||
self._changed = dict()
|
||||
self._delete = dict()
|
||||
self._times = dict()
|
||||
self._fmttime = dict()
|
||||
self._timestamps = dict()
|
||||
self._original = dict()
|
||||
self._origttl = None
|
||||
|
||||
for line in pfp:
|
||||
line = line.strip()
|
||||
if not line or line[0] in ('!#'):
|
||||
continue
|
||||
punctuation = [line.find(c) for c in ':= '] + [len(line)]
|
||||
found = min([pos for pos in punctuation if pos != -1])
|
||||
name = line[:found].rstrip()
|
||||
value = line[found:].lstrip(":= ").rstrip()
|
||||
self.metadata[name] = value
|
||||
|
||||
for prop in dnskey._PROPS:
|
||||
self._changed[prop] = False
|
||||
if prop in self.metadata:
|
||||
t = self.parsetime(self.metadata[prop])
|
||||
self._times[prop] = t
|
||||
self._fmttime[prop] = self.formattime(t)
|
||||
self._timestamps[prop] = self.epochfromtime(t)
|
||||
self._original[prop] = self._timestamps[prop]
|
||||
else:
|
||||
self._times[prop] = None
|
||||
self._fmttime[prop] = None
|
||||
self._timestamps[prop] = None
|
||||
self._original[prop] = None
|
||||
|
||||
pfp.close()
|
||||
|
||||
def commit(self, settime_bin, **kwargs):
|
||||
quiet = kwargs.get('quiet', False)
|
||||
cmd = []
|
||||
first = True
|
||||
|
||||
if self._origttl is not None:
|
||||
cmd += ["-L", str(self.ttl)]
|
||||
|
||||
for prop, opt in zip(dnskey._PROPS, dnskey._OPTS):
|
||||
if not opt or not self._changed[prop]:
|
||||
continue
|
||||
|
||||
delete = False
|
||||
if prop in self._delete and self._delete[prop]:
|
||||
delete = True
|
||||
|
||||
when = 'none' if delete else self._fmttime[prop]
|
||||
cmd += [opt, when]
|
||||
first = False
|
||||
|
||||
if cmd:
|
||||
fullcmd = [settime_bin, "-K", self._dir] + cmd + [self.keystr,]
|
||||
if not quiet:
|
||||
print('# ' + ' '.join(fullcmd))
|
||||
try:
|
||||
p = Popen(fullcmd, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if stderr:
|
||||
raise Exception(str(stderr))
|
||||
except Exception as e:
|
||||
raise Exception('unable to run %s: %s' %
|
||||
(settime_bin, str(e)))
|
||||
self._origttl = None
|
||||
for prop in dnskey._PROPS:
|
||||
self._original[prop] = self._timestamps[prop]
|
||||
self._changed[prop] = False
|
||||
|
||||
@classmethod
|
||||
def generate(cls, keygen_bin, keys_dir, name, alg, keysize, sep,
|
||||
ttl, publish=None, activate=None, **kwargs):
|
||||
quiet = kwargs.get('quiet', False)
|
||||
|
||||
keygen_cmd = [keygen_bin, "-q", "-K", keys_dir, "-L", str(ttl)]
|
||||
|
||||
if sep:
|
||||
keygen_cmd.append("-fk")
|
||||
|
||||
if alg:
|
||||
keygen_cmd += ["-a", alg]
|
||||
|
||||
if keysize:
|
||||
keygen_cmd += ["-b", str(keysize)]
|
||||
|
||||
if publish:
|
||||
t = dnskey.timefromepoch(publish)
|
||||
keygen_cmd += ["-P", dnskey.formattime(t)]
|
||||
|
||||
if activate:
|
||||
t = dnskey.timefromepoch(activate)
|
||||
keygen_cmd += ["-A", dnskey.formattime(activate)]
|
||||
|
||||
keygen_cmd.append(name)
|
||||
|
||||
if not quiet:
|
||||
print('# ' + ' '.join(keygen_cmd))
|
||||
|
||||
p = Popen(keygen_cmd, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if stderr:
|
||||
raise Exception('unable to generate key: ' + str(stderr))
|
||||
|
||||
try:
|
||||
keystr = stdout.splitlines()[0]
|
||||
newkey = dnskey(keystr, keys_dir, ttl)
|
||||
return newkey
|
||||
except Exception as e:
|
||||
raise Exception('unable to generate key: %s' % str(e))
|
||||
|
||||
def generate_successor(self, keygen_bin, **kwargs):
|
||||
quiet = kwargs.get('quiet', False)
|
||||
|
||||
if not self.inactive():
|
||||
raise Exception("predecessor key %s has no inactive date" % self)
|
||||
|
||||
keygen_cmd = [keygen_bin, "-q", "-K", self._dir, "-S", self.keystr]
|
||||
|
||||
if self.ttl:
|
||||
keygen_cmd += ["-L", str(self.ttl)]
|
||||
|
||||
if not quiet:
|
||||
print('# ' + ' '.join(keygen_cmd))
|
||||
|
||||
p = Popen(keygen_cmd, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = p.communicate()
|
||||
if stderr:
|
||||
raise Exception('unable to generate key: ' + stderr)
|
||||
|
||||
try:
|
||||
keystr = stdout.splitlines()[0]
|
||||
newkey = dnskey(keystr, self._dir, self.ttl)
|
||||
return newkey
|
||||
except:
|
||||
raise Exception('unable to generate successor for key %s' % self)
|
||||
|
||||
@staticmethod
|
||||
def algstr(alg):
|
||||
name = None
|
||||
if alg in range(len(dnskey._ALGNAMES)):
|
||||
name = dnskey._ALGNAMES[alg]
|
||||
return name if name else ("%03d" % alg)
|
||||
|
||||
@staticmethod
|
||||
def algnum(alg):
|
||||
if not alg:
|
||||
return None
|
||||
alg = alg.upper()
|
||||
try:
|
||||
return dnskey._ALGNAMES.index(alg)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def algname(self, alg=None):
|
||||
return self.algstr(alg or self.alg)
|
||||
|
||||
@staticmethod
|
||||
def timefromepoch(secs):
|
||||
return time.gmtime(secs)
|
||||
|
||||
@staticmethod
|
||||
def parsetime(string):
|
||||
return time.strptime(string, "%Y%m%d%H%M%S")
|
||||
|
||||
@staticmethod
|
||||
def epochfromtime(t):
|
||||
return calendar.timegm(t)
|
||||
|
||||
@staticmethod
|
||||
def formattime(t):
|
||||
return time.strftime("%Y%m%d%H%M%S", t)
|
||||
|
||||
def setmeta(self, prop, secs, now, **kwargs):
|
||||
force = kwargs.get('force', False)
|
||||
|
||||
if self._timestamps[prop] == secs:
|
||||
return
|
||||
|
||||
if self._original[prop] is not None and \
|
||||
self._original[prop] < now and not force:
|
||||
raise TimePast(self, prop, self._original[prop])
|
||||
|
||||
if secs is None:
|
||||
self._changed[prop] = False \
|
||||
if self._original[prop] is None else True
|
||||
|
||||
self._delete[prop] = True
|
||||
self._timestamps[prop] = None
|
||||
self._times[prop] = None
|
||||
self._fmttime[prop] = None
|
||||
return
|
||||
|
||||
t = self.timefromepoch(secs)
|
||||
self._timestamps[prop] = secs
|
||||
self._times[prop] = t
|
||||
self._fmttime[prop] = self.formattime(t)
|
||||
self._changed[prop] = False if \
|
||||
self._original[prop] == self._timestamps[prop] else True
|
||||
|
||||
def gettime(self, prop):
|
||||
return self._times[prop]
|
||||
|
||||
def getfmttime(self, prop):
|
||||
return self._fmttime[prop]
|
||||
|
||||
def gettimestamp(self, prop):
|
||||
return self._timestamps[prop]
|
||||
|
||||
def created(self):
|
||||
return self._timestamps["Created"]
|
||||
|
||||
def syncpublish(self):
|
||||
return self._timestamps["SyncPublish"]
|
||||
|
||||
def setsyncpublish(self, secs, now=time.time(), **kwargs):
|
||||
self.setmeta("SyncPublish", secs, now, **kwargs)
|
||||
|
||||
def publish(self):
|
||||
return self._timestamps["Publish"]
|
||||
|
||||
def setpublish(self, secs, now=time.time(), **kwargs):
|
||||
self.setmeta("Publish", secs, now, **kwargs)
|
||||
|
||||
def activate(self):
|
||||
return self._timestamps["Activate"]
|
||||
|
||||
def setactivate(self, secs, now=time.time(), **kwargs):
|
||||
self.setmeta("Activate", secs, now, **kwargs)
|
||||
|
||||
def revoke(self):
|
||||
return self._timestamps["Revoke"]
|
||||
|
||||
def setrevoke(self, secs, now=time.time(), **kwargs):
|
||||
self.setmeta("Revoke", secs, now, **kwargs)
|
||||
|
||||
def inactive(self):
|
||||
return self._timestamps["Inactive"]
|
||||
|
||||
def setinactive(self, secs, now=time.time(), **kwargs):
|
||||
self.setmeta("Inactive", secs, now, **kwargs)
|
||||
|
||||
def delete(self):
|
||||
return self._timestamps["Delete"]
|
||||
|
||||
def setdelete(self, secs, now=time.time(), **kwargs):
|
||||
self.setmeta("Delete", secs, now, **kwargs)
|
||||
|
||||
def syncdelete(self):
|
||||
return self._timestamps["SyncDelete"]
|
||||
|
||||
def setsyncdelete(self, secs, now=time.time(), **kwargs):
|
||||
self.setmeta("SyncDelete", secs, now, **kwargs)
|
||||
|
||||
def setttl(self, ttl):
|
||||
if ttl is None or self.ttl == ttl:
|
||||
return
|
||||
elif self._origttl is None:
|
||||
self._origttl = self.ttl
|
||||
self.ttl = ttl
|
||||
elif self._origttl == ttl:
|
||||
self._origttl = None
|
||||
self.ttl = ttl
|
||||
else:
|
||||
self.ttl = ttl
|
||||
|
||||
def keytype(self):
|
||||
return ("KSK" if self.sep else "ZSK")
|
||||
|
||||
def __str__(self):
|
||||
return ("%s/%s/%05d"
|
||||
% (self.name, self.algname(), self.keyid))
|
||||
|
||||
def __repr__(self):
|
||||
return ("%s/%s/%05d (%s)"
|
||||
% (self.name, self.algname(), self.keyid,
|
||||
("KSK" if self.sep else "ZSK")))
|
||||
|
||||
def date(self):
|
||||
return (self.activate() or self.publish() or self.created())
|
||||
|
||||
# keys are sorted first by zone name, then by algorithm. within
|
||||
# the same name/algorithm, they are sorted according to their
|
||||
# 'date' value: the activation date if set, OR the publication
|
||||
# if set, OR the creation date.
|
||||
def __lt__(self, other):
|
||||
if self.name != other.name:
|
||||
return self.name < other.name
|
||||
if self.alg != other.alg:
|
||||
return self.alg < other.alg
|
||||
return self.date() < other.date()
|
||||
|
||||
def check_prepub(self, output=None):
|
||||
def noop(*args, **kwargs): pass
|
||||
if not output:
|
||||
output = noop
|
||||
|
||||
now = int(time.time())
|
||||
a = self.activate()
|
||||
p = self.publish()
|
||||
|
||||
if not a:
|
||||
return False
|
||||
|
||||
if not p:
|
||||
if a > now:
|
||||
output("WARNING: Key %s is scheduled for\n"
|
||||
"\t activation but not for publication."
|
||||
% repr(self))
|
||||
return False
|
||||
|
||||
if p <= now and a <= now:
|
||||
return True
|
||||
|
||||
if p == a:
|
||||
output("WARNING: %s is scheduled to be\n"
|
||||
"\t published and activated at the same time. This\n"
|
||||
"\t could result in a coverage gap if the zone was\n"
|
||||
"\t previously signed. Activation should be at least\n"
|
||||
"\t %s after publication."
|
||||
% (repr(self),
|
||||
dnskey.duration(self.ttl) or 'one DNSKEY TTL'))
|
||||
return True
|
||||
|
||||
if a < p:
|
||||
output("WARNING: Key %s is active before it is published"
|
||||
% repr(self))
|
||||
return False
|
||||
|
||||
if self.ttl is not None and a - p < self.ttl:
|
||||
output("WARNING: Key %s is activated too soon\n"
|
||||
"\t after publication; this could result in coverage \n"
|
||||
"\t gaps due to resolver caches containing old data.\n"
|
||||
"\t Activation should be at least %s after\n"
|
||||
"\t publication."
|
||||
% (repr(self),
|
||||
dnskey.duration(self.ttl) or 'one DNSKEY TTL'))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def check_postpub(self, output = None, timespan = None):
|
||||
def noop(*args, **kwargs): pass
|
||||
if output is None:
|
||||
output = noop
|
||||
|
||||
if timespan is None:
|
||||
timespan = self.ttl
|
||||
|
||||
now = time.time()
|
||||
d = self.delete()
|
||||
i = self.inactive()
|
||||
|
||||
if not d:
|
||||
return False
|
||||
|
||||
if not i:
|
||||
if d > now:
|
||||
output("WARNING: Key %s is scheduled for\n"
|
||||
"\t deletion but not for inactivation." % repr(self))
|
||||
return False
|
||||
|
||||
if d < now and i < now:
|
||||
return True
|
||||
|
||||
if d < i:
|
||||
output("WARNING: Key %s is scheduled for\n"
|
||||
"\t deletion before inactivation."
|
||||
% repr(self))
|
||||
return False
|
||||
|
||||
if d - i < timespan:
|
||||
output("WARNING: Key %s scheduled for\n"
|
||||
"\t deletion too soon after deactivation; this may \n"
|
||||
"\t result in coverage gaps due to resolver caches\n"
|
||||
"\t containing old data. Deletion should be at least\n"
|
||||
"\t %s after inactivation."
|
||||
% (repr(self), dnskey.duration(timespan)))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def duration(secs):
|
||||
if not secs:
|
||||
return None
|
||||
|
||||
units = [("year", 60*60*24*365),
|
||||
("month", 60*60*24*30),
|
||||
("day", 60*60*24),
|
||||
("hour", 60*60),
|
||||
("minute", 60),
|
||||
("second", 1)]
|
||||
|
||||
output = []
|
||||
for unit in units:
|
||||
v, secs = secs // unit[1], secs % unit[1]
|
||||
if v > 0:
|
||||
output.append("%d %s%s" % (v, unit[0], "s" if v > 1 else ""))
|
||||
|
||||
return ", ".join(output)
|
||||
|
171
bin/python/isc/eventlist.py
Normal file
171
bin/python/isc/eventlist.py
Normal file
@@ -0,0 +1,171 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
|
||||
from collections import defaultdict
|
||||
from .dnskey import *
|
||||
from .keydict import *
|
||||
from .keyevent import *
|
||||
|
||||
|
||||
class eventlist:
|
||||
_K = defaultdict(lambda: defaultdict(list))
|
||||
_Z = defaultdict(lambda: defaultdict(list))
|
||||
_zones = set()
|
||||
_kdict = None
|
||||
|
||||
def __init__(self, kdict):
|
||||
properties = ["SyncPublish", "Publish", "SyncDelete",
|
||||
"Activate", "Inactive", "Delete"]
|
||||
self._kdict = kdict
|
||||
for zone in kdict.zones():
|
||||
self._zones.add(zone)
|
||||
for alg, keys in kdict[zone].items():
|
||||
for k in keys.values():
|
||||
for prop in properties:
|
||||
t = k.gettime(prop)
|
||||
if not t:
|
||||
continue
|
||||
e = keyevent(prop, k, t)
|
||||
if k.sep:
|
||||
self._K[zone][alg].append(e)
|
||||
else:
|
||||
self._Z[zone][alg].append(e)
|
||||
|
||||
self._K[zone][alg] = sorted(self._K[zone][alg],
|
||||
key=lambda event: event.when)
|
||||
self._Z[zone][alg] = sorted(self._Z[zone][alg],
|
||||
key=lambda event: event.when)
|
||||
|
||||
# scan events per zone, algorithm, and key type, in order of
|
||||
# occurrance, noting inconsistent states when found
|
||||
def coverage(self, zone, keytype, until, output = None):
|
||||
def noop(*args, **kwargs): pass
|
||||
if not output:
|
||||
output = noop
|
||||
|
||||
no_zsk = True if (keytype and keytype == "KSK") else False
|
||||
no_ksk = True if (keytype and keytype == "ZSK") else False
|
||||
kok = zok = True
|
||||
found = False
|
||||
|
||||
if zone and not zone in self._zones:
|
||||
output("ERROR: No key events found for %s" % zone)
|
||||
return False
|
||||
|
||||
if zone:
|
||||
found = True
|
||||
if not no_ksk:
|
||||
kok = self.checkzone(zone, "KSK", until, output)
|
||||
if not no_zsk:
|
||||
zok = self.checkzone(zone, "ZSK", until, output)
|
||||
else:
|
||||
for z in self._zones:
|
||||
if not no_ksk and z in self._K.keys():
|
||||
found = True
|
||||
kok = self.checkzone(z, "KSK", until, output)
|
||||
if not no_zsk and z in self._Z.keys():
|
||||
found = True
|
||||
kok = self.checkzone(z, "ZSK", until, output)
|
||||
|
||||
if not found:
|
||||
output("ERROR: No key events found")
|
||||
return False
|
||||
|
||||
return (kok and zok)
|
||||
|
||||
def checkzone(self, zone, keytype, until, output):
|
||||
allok = True
|
||||
if keytype == "KSK":
|
||||
kz = self._K[zone]
|
||||
else:
|
||||
kz = self._Z[zone]
|
||||
|
||||
for alg in kz.keys():
|
||||
output("Checking scheduled %s events for zone %s, "
|
||||
"algorithm %s..." %
|
||||
(keytype, zone, dnskey.algstr(alg)))
|
||||
ok = eventlist.checkset(kz[alg], keytype, until, output)
|
||||
if ok:
|
||||
output("No errors found")
|
||||
allok = allok and ok
|
||||
|
||||
return allok
|
||||
|
||||
@staticmethod
|
||||
def showset(eventset, output):
|
||||
if not eventset:
|
||||
return
|
||||
output(" " + eventset[0].showtime() + ":", skip=False)
|
||||
for event in eventset:
|
||||
output(" %s: %s" % (event.what, repr(event.key)), skip=False)
|
||||
|
||||
@staticmethod
|
||||
def checkset(eventset, keytype, until, output):
|
||||
groups = list()
|
||||
group = list()
|
||||
|
||||
# collect up all events that have the same time
|
||||
eventsfound = False
|
||||
for event in eventset:
|
||||
# we found an event
|
||||
eventsfound = True
|
||||
|
||||
# add event to current group
|
||||
if (not group or group[0].when == event.when):
|
||||
group.append(event)
|
||||
|
||||
# if we're at the end of the list, we're done. if
|
||||
# we've found an event with a later time, start a new group
|
||||
if (group[0].when != event.when):
|
||||
groups.append(group)
|
||||
group = list()
|
||||
group.append(event)
|
||||
|
||||
if group:
|
||||
groups.append(group)
|
||||
|
||||
if not eventsfound:
|
||||
output("ERROR: No %s events found" % keytype)
|
||||
return False
|
||||
|
||||
active = published = None
|
||||
for group in groups:
|
||||
if (until and calendar.timegm(group[0].when) > until):
|
||||
output("Ignoring events after %s" %
|
||||
time.strftime("%a %b %d %H:%M:%S UTC %Y",
|
||||
time.gmtime(until)))
|
||||
return True
|
||||
|
||||
for event in group:
|
||||
(active, published) = event.status(active, published)
|
||||
|
||||
eventlist.showset(group, output)
|
||||
|
||||
# and then check for inconsistencies:
|
||||
if not active:
|
||||
output("ERROR: No %s's are active after this event" % keytype)
|
||||
return False
|
||||
elif not published:
|
||||
output("ERROR: No %s's are published after this event"
|
||||
% keytype)
|
||||
return False
|
||||
elif not published.intersection(active):
|
||||
output("ERROR: No %s's are both active and published "
|
||||
"after this event" % keytype)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
89
bin/python/isc/keydict.py
Normal file
89
bin/python/isc/keydict.py
Normal file
@@ -0,0 +1,89 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
|
||||
from collections import defaultdict
|
||||
from . import dnskey
|
||||
import os
|
||||
import glob
|
||||
|
||||
|
||||
########################################################################
|
||||
# Class keydict
|
||||
########################################################################
|
||||
class keydict:
|
||||
""" A dictionary of keys, indexed by name, algorithm, and key id """
|
||||
|
||||
_keydict = defaultdict(lambda: defaultdict(dict))
|
||||
_defttl = None
|
||||
_missing = []
|
||||
|
||||
def __init__(self, dp=None, **kwargs):
|
||||
self._defttl = kwargs.get('keyttl', None)
|
||||
zones = kwargs.get('zones', None)
|
||||
|
||||
if not zones:
|
||||
path = kwargs.get('path',None) or '.'
|
||||
self.readall(path)
|
||||
else:
|
||||
for zone in zones:
|
||||
if 'path' in kwargs and kwargs['path'] is not None:
|
||||
path = kwargs['path']
|
||||
else:
|
||||
path = dp and dp.policy(zone).directory or '.'
|
||||
if not self.readone(path, zone):
|
||||
self._missing.append(zone)
|
||||
|
||||
def readall(self, path):
|
||||
files = glob.glob(os.path.join(path, '*.private'))
|
||||
|
||||
for infile in files:
|
||||
key = dnskey(infile, path, self._defttl)
|
||||
self._keydict[key.name][key.alg][key.keyid] = key
|
||||
|
||||
def readone(self, path, zone):
|
||||
match='K' + zone + '.+*.private'
|
||||
files = glob.glob(os.path.join(path, match))
|
||||
|
||||
found = False
|
||||
for infile in files:
|
||||
key = dnskey(infile, path, self._defttl)
|
||||
if key.name != zone: # shouldn't ever happen
|
||||
continue
|
||||
self._keydict[key.name][key.alg][key.keyid] = key
|
||||
found = True
|
||||
|
||||
return found
|
||||
|
||||
def __iter__(self):
|
||||
for zone, algorithms in self._keydict.items():
|
||||
for alg, keys in algorithms.items():
|
||||
for key in keys.values():
|
||||
yield key
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._keydict[name]
|
||||
|
||||
def zones(self):
|
||||
return (self._keydict.keys())
|
||||
|
||||
def algorithms(self, zone):
|
||||
return (self._keydict[zone].keys())
|
||||
|
||||
def keys(self, zone, alg):
|
||||
return (self._keydict[zone][alg].keys())
|
||||
|
||||
def missing(self):
|
||||
return (self._missing)
|
81
bin/python/isc/keyevent.py
Normal file
81
bin/python/isc/keyevent.py
Normal file
@@ -0,0 +1,81 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
|
||||
import time
|
||||
|
||||
|
||||
########################################################################
|
||||
# Class keyevent
|
||||
########################################################################
|
||||
class keyevent:
|
||||
""" A discrete key event, e.g., Publish, Activate, Inactive, Delete,
|
||||
etc. Stores the date of the event, and identifying information
|
||||
about the key to which the event will occur."""
|
||||
|
||||
def __init__(self, what, key, when=None):
|
||||
self.what = what
|
||||
self.when = when or key.gettime(what)
|
||||
self.key = key
|
||||
self.sep = key.sep
|
||||
self.zone = key.name
|
||||
self.alg = key.alg
|
||||
self.keyid = key.keyid
|
||||
|
||||
def __repr__(self):
|
||||
return repr((self.when, self.what, self.keyid, self.sep,
|
||||
self.zone, self.alg))
|
||||
|
||||
def showtime(self):
|
||||
return time.strftime("%a %b %d %H:%M:%S UTC %Y", self.when)
|
||||
|
||||
# update sets of active and published keys, based on
|
||||
# the contents of this keyevent
|
||||
def status(self, active, published, output = None):
|
||||
def noop(*args, **kwargs): pass
|
||||
if not output:
|
||||
output = noop
|
||||
|
||||
if not active:
|
||||
active = set()
|
||||
if not published:
|
||||
published = set()
|
||||
|
||||
if self.what == "Activate":
|
||||
active.add(self.keyid)
|
||||
elif self.what == "Publish":
|
||||
published.add(self.keyid)
|
||||
elif self.what == "Inactive":
|
||||
if self.keyid not in active:
|
||||
output("\tWARNING: %s scheduled to become inactive "
|
||||
"before it is active"
|
||||
% repr(self.key))
|
||||
else:
|
||||
active.remove(self.keyid)
|
||||
elif self.what == "Delete":
|
||||
if self.keyid in published:
|
||||
published.remove(self.keyid)
|
||||
else:
|
||||
output("WARNING: key %s is scheduled for deletion "
|
||||
"before it is published" % repr(self.key))
|
||||
elif self.what == "Revoke":
|
||||
# We don't need to worry about the logic of this one;
|
||||
# just stop counting this key as either active or published
|
||||
if self.keyid in published:
|
||||
published.remove(self.keyid)
|
||||
if self.keyid in active:
|
||||
active.remove(self.keyid)
|
||||
|
||||
return active, published
|
152
bin/python/isc/keymgr.py
Normal file
152
bin/python/isc/keymgr.py
Normal 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
194
bin/python/isc/keyseries.py
Normal file
@@ -0,0 +1,194 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
|
||||
from collections import defaultdict
|
||||
from .dnskey import *
|
||||
from .keydict import *
|
||||
from .keyevent import *
|
||||
from .policy import *
|
||||
import time
|
||||
|
||||
|
||||
class keyseries:
|
||||
_K = defaultdict(lambda: defaultdict(list))
|
||||
_Z = defaultdict(lambda: defaultdict(list))
|
||||
_zones = set()
|
||||
_kdict = None
|
||||
_context = None
|
||||
|
||||
def __init__(self, kdict, now=time.time(), context=None):
|
||||
self._kdict = kdict
|
||||
self._context = context
|
||||
self._zones = set(kdict.missing())
|
||||
|
||||
for zone in kdict.zones():
|
||||
self._zones.add(zone)
|
||||
for alg, keys in kdict[zone].items():
|
||||
for k in keys.values():
|
||||
if k.sep:
|
||||
self._K[zone][alg].append(k)
|
||||
else:
|
||||
self._Z[zone][alg].append(k)
|
||||
|
||||
for group in [self._K[zone][alg], self._Z[zone][alg]]:
|
||||
group.sort()
|
||||
for k in group:
|
||||
if k.delete() and k.delete() < now:
|
||||
group.remove(k)
|
||||
|
||||
def __iter__(self):
|
||||
for zone in self._zones:
|
||||
for collection in [self._K, self._Z]:
|
||||
if zone not in collection:
|
||||
continue
|
||||
for alg, keys in collection[zone].items():
|
||||
for key in keys:
|
||||
yield key
|
||||
|
||||
def dump(self):
|
||||
for k in self:
|
||||
print("%s" % repr(k))
|
||||
|
||||
def fixseries(self, keys, policy, now, **kwargs):
|
||||
force = kwargs.get('force', False)
|
||||
if not keys:
|
||||
return
|
||||
|
||||
# handle the first key
|
||||
key = keys[0]
|
||||
if key.sep:
|
||||
rp = policy.ksk_rollperiod
|
||||
prepub = policy.ksk_prepublish or (30 * 86400)
|
||||
postpub = policy.ksk_postpublish or (30 * 86400)
|
||||
else:
|
||||
rp = policy.zsk_rollperiod
|
||||
prepub = policy.zsk_prepublish or (30 * 86400)
|
||||
postpub = policy.zsk_postpublish or (30 * 86400)
|
||||
|
||||
# the first key should be published and active
|
||||
p = key.publish()
|
||||
a = key.activate()
|
||||
if not p or p > now:
|
||||
key.setpublish(now)
|
||||
if not a or a > now:
|
||||
key.setactivate(now)
|
||||
|
||||
if not rp:
|
||||
key.setinactive(None, **kwargs)
|
||||
key.setdelete(None, **kwargs)
|
||||
else:
|
||||
key.setinactive(a + rp, **kwargs)
|
||||
key.setdelete(a + rp + postpub, **kwargs)
|
||||
|
||||
if policy.keyttl != key.ttl:
|
||||
key.setttl(policy.keyttl)
|
||||
|
||||
# handle all the subsequent keys
|
||||
prev = key
|
||||
for key in keys[1:]:
|
||||
# if no rollperiod, then all keys after the first in
|
||||
# the series kept inactive.
|
||||
# (XXX: we need to change this to allow standby keys)
|
||||
if not rp:
|
||||
key.setpublish(None, **kwargs)
|
||||
key.setactivate(None, **kwargs)
|
||||
key.setinactive(None, **kwargs)
|
||||
key.setdelete(None, **kwargs)
|
||||
if policy.keyttl != key.ttl:
|
||||
key.setttl(policy.keyttl)
|
||||
continue
|
||||
|
||||
# otherwise, ensure all dates are set correctly based on
|
||||
# the initial key
|
||||
a = prev.inactive()
|
||||
p = a - prepub
|
||||
key.setactivate(a, **kwargs)
|
||||
key.setpublish(p, **kwargs)
|
||||
key.setinactive(a + rp, **kwargs)
|
||||
key.setdelete(a + rp + postpub, **kwargs)
|
||||
prev.setdelete(a + postpub, **kwargs)
|
||||
if policy.keyttl != key.ttl:
|
||||
key.setttl(policy.keyttl)
|
||||
prev = key
|
||||
|
||||
# if we haven't got sufficient coverage, create successor key(s)
|
||||
while rp and prev.inactive() and \
|
||||
prev.inactive() < now + policy.coverage:
|
||||
# commit changes to predecessor: a successor can only be
|
||||
# generated if Inactive has been set in the predecessor key
|
||||
prev.commit(self._context['settime_path'], **kwargs)
|
||||
key = prev.generate_successor(self._context['keygen_path'],
|
||||
**kwargs)
|
||||
|
||||
key.setinactive(key.activate() + rp, **kwargs)
|
||||
key.setdelete(key.inactive() + postpub, **kwargs)
|
||||
keys.append(key)
|
||||
prev = key
|
||||
|
||||
# last key? we already know we have sufficient coverage now, so
|
||||
# disable the inactivation of the final key (if it was set),
|
||||
# ensuring that if dnssec-keymgr isn't run again, the last key
|
||||
# in the series will at least remain usable.
|
||||
prev.setinactive(None, **kwargs)
|
||||
prev.setdelete(None, **kwargs)
|
||||
|
||||
# commit changes
|
||||
for key in keys:
|
||||
key.commit(self._context['settime_path'], **kwargs)
|
||||
|
||||
|
||||
def enforce_policy(self, policies, now=time.time(), **kwargs):
|
||||
# If zones is provided as a parameter, use that list.
|
||||
# If not, use what we have in this object
|
||||
zones = kwargs.get('zones', self._zones)
|
||||
keys_dir = kwargs.get('dir', self._context.get('keys_path', None))
|
||||
force = kwargs.get('force', False)
|
||||
|
||||
for zone in zones:
|
||||
collections = []
|
||||
policy = policies.policy(zone)
|
||||
keys_dir = keys_dir or policy.directory or '.'
|
||||
alg = policy.algorithm
|
||||
algnum = dnskey.algnum(alg)
|
||||
if 'ksk' not in kwargs or not kwargs['ksk']:
|
||||
if len(self._Z[zone][algnum]) == 0:
|
||||
k = dnskey.generate(self._context['keygen_path'],
|
||||
keys_dir, zone, alg,
|
||||
policy.zsk_keysize, False,
|
||||
policy.keyttl or 3600,
|
||||
**kwargs)
|
||||
self._Z[zone][algnum].append(k)
|
||||
collections.append(self._Z[zone])
|
||||
|
||||
if 'zsk' not in kwargs or not kwargs['zsk']:
|
||||
if len(self._K[zone][algnum]) == 0:
|
||||
k = dnskey.generate(self._context['keygen_path'],
|
||||
keys_dir, zone, alg,
|
||||
policy.ksk_keysize, True,
|
||||
policy.keyttl or 3600,
|
||||
**kwargs)
|
||||
self._K[zone][algnum].append(k)
|
||||
collections.append(self._K[zone])
|
||||
|
||||
for collection in collections:
|
||||
for algorithm, keys in collection.items():
|
||||
if algorithm != algnum:
|
||||
continue
|
||||
try:
|
||||
self.fixseries(keys, policy, now, **kwargs)
|
||||
except Exception as e:
|
||||
raise Exception('%s/%s: %s' %
|
||||
(zone, dnskey.algstr(algnum), str(e)))
|
60
bin/python/isc/keyzone.py
Normal file
60
bin/python/isc/keyzone.py
Normal file
@@ -0,0 +1,60 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
########################################################################
|
||||
# Exceptions
|
||||
########################################################################
|
||||
class KeyZoneException(Exception):
|
||||
pass
|
||||
|
||||
########################################################################
|
||||
# class keyzone
|
||||
########################################################################
|
||||
class keyzone:
|
||||
"""reads a zone file to find data relevant to keys"""
|
||||
|
||||
def __init__(self, name, filename, czpath):
|
||||
self.maxttl = None
|
||||
self.keyttl = None
|
||||
|
||||
if not name:
|
||||
return
|
||||
|
||||
if not czpath or not os.path.isfile(czpath) \
|
||||
or not os.access(czpath, os.X_OK):
|
||||
raise KeyZoneException('"named-compilezone" not found')
|
||||
return
|
||||
|
||||
maxttl = keyttl = None
|
||||
|
||||
fp, _ = Popen([czpath, "-o", "-", name, filename],
|
||||
stdout=PIPE, stderr=PIPE).communicate()
|
||||
for line in fp.splitlines():
|
||||
if re.search('^[:space:]*;', line):
|
||||
continue
|
||||
fields = line.split()
|
||||
if not maxttl or int(fields[1]) > maxttl:
|
||||
maxttl = int(fields[1])
|
||||
if fields[3] == "DNSKEY":
|
||||
keyttl = int(fields[1])
|
||||
|
||||
self.keyttl = keyttl
|
||||
self.maxttl = maxttl
|
690
bin/python/isc/policy.py
Normal file
690
bin/python/isc/policy.py
Normal 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])
|
33
bin/python/isc/tests/Makefile.in
Normal file
33
bin/python/isc/tests/Makefile.in
Normal file
@@ -0,0 +1,33 @@
|
||||
# Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
srcdir = @srcdir@
|
||||
VPATH = @srcdir@
|
||||
top_srcdir = @top_srcdir@
|
||||
|
||||
@BIND9_MAKE_INCLUDES@
|
||||
|
||||
PYTHON = @PYTHON@
|
||||
|
||||
PYTESTS = dnskey_test.py policy_test.py
|
||||
|
||||
@BIND9_MAKE_RULES@
|
||||
|
||||
check test:
|
||||
for test in $(PYTESTS); do \
|
||||
$(PYTHON) $$test; \
|
||||
done
|
||||
|
||||
clean distclean::
|
||||
rm -f *.pyc
|
57
bin/python/isc/tests/dnskey_test.py
Normal file
57
bin/python/isc/tests/dnskey_test.py
Normal file
@@ -0,0 +1,57 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
sys.path.append('../..')
|
||||
from isc import *
|
||||
|
||||
kdict = None
|
||||
|
||||
|
||||
def getkey():
|
||||
global kdict
|
||||
if not kdict:
|
||||
kd = keydict(path='testdata')
|
||||
for key in kd:
|
||||
return key
|
||||
|
||||
|
||||
class DnskeyTest(unittest.TestCase):
|
||||
def test_metdata(self):
|
||||
key = getkey()
|
||||
self.assertEqual(key.created(), 1448055647)
|
||||
self.assertEqual(key.publish(), 1445463714)
|
||||
self.assertEqual(key.activate(), 1448055714)
|
||||
self.assertEqual(key.revoke(), 1479591714)
|
||||
self.assertEqual(key.inactive(), 1511127714)
|
||||
self.assertEqual(key.delete(), 1542663714)
|
||||
self.assertEqual(key.syncpublish(), 1442871714)
|
||||
self.assertEqual(key.syncdelete(), 1448919714)
|
||||
|
||||
def test_fmttime(self):
|
||||
key = getkey()
|
||||
self.assertEqual(key.getfmttime('Created'), '20151120214047')
|
||||
self.assertEqual(key.getfmttime('Publish'), '20151021214154')
|
||||
self.assertEqual(key.getfmttime('Activate'), '20151120214154')
|
||||
self.assertEqual(key.getfmttime('Revoke'), '20161119214154')
|
||||
self.assertEqual(key.getfmttime('Inactive'), '20171119214154')
|
||||
self.assertEqual(key.getfmttime('Delete'), '20181119214154')
|
||||
self.assertEqual(key.getfmttime('SyncPublish'), '20150921214154')
|
||||
self.assertEqual(key.getfmttime('SyncDelete'), '20151130214154')
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
90
bin/python/isc/tests/policy_test.py
Normal file
90
bin/python/isc/tests/policy_test.py
Normal 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()
|
41
bin/python/isc/tests/test-policies/01-keysize.pol
Normal file
41
bin/python/isc/tests/test-policies/01-keysize.pol
Normal 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;
|
||||
};
|
31
bin/python/isc/tests/test-policies/02-prepublish.pol
Normal file
31
bin/python/isc/tests/test-policies/02-prepublish.pol
Normal 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;
|
||||
};
|
||||
|
||||
|
31
bin/python/isc/tests/test-policies/03-postpublish.pol
Normal file
31
bin/python/isc/tests/test-policies/03-postpublish.pol
Normal 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;
|
||||
};
|
||||
|
||||
|
55
bin/python/isc/tests/test-policies/04-combined-pre-post.pol
Normal file
55
bin/python/isc/tests/test-policies/04-combined-pre-post.pol
Normal 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;
|
||||
};
|
||||
|
||||
|
8
bin/python/isc/tests/testdata/Kexample.com.+007+35529.key
vendored
Normal file
8
bin/python/isc/tests/testdata/Kexample.com.+007+35529.key
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
; This is a key-signing key, keyid 35529, for example.com.
|
||||
; Created: 20151120214047 (Fri Nov 20 13:40:47 2015)
|
||||
; Publish: 20151021214154 (Wed Oct 21 14:41:54 2015)
|
||||
; Activate: 20151120214154 (Fri Nov 20 13:41:54 2015)
|
||||
; Revoke: 20161119214154 (Sat Nov 19 13:41:54 2016)
|
||||
; Inactive: 20171119214154 (Sun Nov 19 13:41:54 2017)
|
||||
; Delete: 20181119214154 (Mon Nov 19 13:41:54 2018)
|
||||
example.com. IN DNSKEY 257 3 7 AwEAAbbJK96tY8d4sF6RLxh9SVIhho5s2ZhrcijT5j1SNLECen7QLutj VJPEiG8UgBLaJSGkxPDxOygYv4hwh4JXBSj89o9rNabAJtCa9XzIXSpt /cfiCfvqmcOZb9nepmDCXsC7gn/gbae/4Y5ym9XOiCp8lu+tlFWgRiJ+ kxDGN48rRPrGfpq+SfwM9NUtftVa7B0EFVzDkADKedRj0SSGYOqH+WYH CnWjhPFmgJoAw3/m4slTHW1l+mDwFvsCMjXopg4JV0CNnTybnOmyuIwO LWRhB3q8ze24sYBU1fpE9VAMxZ++4Kqh/2MZFeDAs7iPPKSmI3wkRCW5 pkwDLO5lJ9c=
|
18
bin/python/isc/tests/testdata/Kexample.com.+007+35529.private
vendored
Normal file
18
bin/python/isc/tests/testdata/Kexample.com.+007+35529.private
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
Private-key-format: v1.3
|
||||
Algorithm: 7 (NSEC3RSASHA1)
|
||||
Modulus: tskr3q1jx3iwXpEvGH1JUiGGjmzZmGtyKNPmPVI0sQJ6ftAu62NUk8SIbxSAEtolIaTE8PE7KBi/iHCHglcFKPz2j2s1psAm0Jr1fMhdKm39x+IJ++qZw5lv2d6mYMJewLuCf+Btp7/hjnKb1c6IKnyW762UVaBGIn6TEMY3jytE+sZ+mr5J/Az01S1+1VrsHQQVXMOQAMp51GPRJIZg6of5ZgcKdaOE8WaAmgDDf+biyVMdbWX6YPAW+wIyNeimDglXQI2dPJuc6bK4jA4tZGEHerzN7bixgFTV+kT1UAzFn77gqqH/YxkV4MCzuI88pKYjfCREJbmmTAMs7mUn1w==
|
||||
PublicExponent: AQAB
|
||||
PrivateExponent: jfiM6YU1Rd6Y5qrPsK7HP1Ko54DmNbvmzI1hfGmYYZAyQsNCXjQloix5aAW9QGdNhecrzJUhxJAMXFZC+lrKuD5a56R25JDE1Sw21nft3SHXhuQrqw5Z5hIMTWXhRrBR1lMOFnLj2PJxqCmenp+vJYjl1z20RBmbv/keE15SExFRJIJ3G0lI4V0KxprY5rgsT/vID0pS32f7rmXhgEzyWDyuxceTMidBooD5BSeEmSTYa4rvCVZ2vgnzIGSxjYDPJE2rGve2dpvdXQuujRFaf4+/FzjaOgg35rTtUmC9klfB4D6KJIfc1PNUwcH7V0VJ2fFlgZgMYi4W331QORl9sQ==
|
||||
Prime1: 479rW3EeoBwHhUKDy5YeyfnMKjhaosrcYhW4resevLzatFrvS/n2KxJnsHoEzmGr2A13naI61RndgVBBOwNDWI3/tQ+aKvcr+V9m4omROV3xYa8s1FsDbEW0Z6G0UheaqRFir8WK98/Lj6Zht1uBXHSPPf91OW0qj+b5gbX7TK8=
|
||||
Prime2: zXXlxgIq+Ih6kxsUw4Ith0nd/d2P3d42QYPjxYjsg4xYicPAjva9HltnbBQ2lr4JEG9Yyb8KalSnJUSuvXtn7bGfBzLu8W6omCeVWXQVH4NIu9AjpO16NpMKWGRfiHHbbSYJs1daTZKHC2FEmi18MKX/RauHGGOakFQ/3A/GMVk=
|
||||
Exponent1: 0o9UQ1uHNAIWFedUEHJ/jr7LOrGVYnLpZCmu7+S0K0zzatGz8ets44+FnAyDywdUKFDzKSMm/4SFXRwE4vl2VzYZlp2RLG4PEuRYK9OCF6a6F1UsvjxTItQjIbjIDSnTjMINGnMps0lDa1EpgKsyI3eEQ46eI3TBZ//k6D6G0vM=
|
||||
Exponent2: d+CYJgXRyJzo17fvT3s+0TbaHWsOq+chROyNEw4m4UIbzpW2XjO8eF/gYgERMLbEVyCAb4XVr+CgfXArfEbqhpciMHMZUyi7mbtOupiuUmqpH1v70Bj3O6xjVtuJmfTEkFSnSEppV+VsgclI26Q6V7Ai1yWTdzl2T0u4zs8tVlE=
|
||||
Coefficient: E4EYw76gIChdQDn6+Uh44/xH9Uwmvq3OETR8w/kEZ0xQ8AkTdKFKUp84nlR6gN+ljb2mUxERKrVLwnBsU8EbUlo9UccMbBGkkZ/8MyfGCBb9nUyOFtOxdHY2M0MQadesRptXHt/m30XjdohwmT7qfSIENwtgUOHbwFnn7WPMc/k=
|
||||
Created: 20151120214047
|
||||
Publish: 20151021214154
|
||||
Activate: 20151120214154
|
||||
Revoke: 20161119214154
|
||||
Inactive: 20171119214154
|
||||
Delete: 20181119214154
|
||||
SyncPublish: 20150921214154
|
||||
SyncDelete: 20151130214154
|
57
bin/python/isc/utils.py.in
Normal file
57
bin/python/isc/utils.py.in
Normal file
@@ -0,0 +1,57 @@
|
||||
############################################################################
|
||||
# Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for any
|
||||
# purpose with or without fee is hereby granted, provided that the above
|
||||
# copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
|
||||
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
||||
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
############################################################################
|
||||
# utils.py
|
||||
# Grouping shared code in one place
|
||||
############################################################################
|
||||
|
||||
import os
|
||||
|
||||
# These routines permit platform-independent location of BIND 9 tools
|
||||
if os.name == 'nt':
|
||||
import win32con
|
||||
import win32api
|
||||
|
||||
|
||||
def prefix(bindir=''):
|
||||
if os.name != 'nt':
|
||||
return os.path.join('@prefix@', bindir)
|
||||
|
||||
bind_subkey = "Software\\ISC\\BIND"
|
||||
h_key = None
|
||||
key_found = True
|
||||
try:
|
||||
h_key = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey)
|
||||
except:
|
||||
key_found = False
|
||||
if key_found:
|
||||
try:
|
||||
(named_base, _) = win32api.RegQueryValueEx(h_key, "InstallDir")
|
||||
except:
|
||||
key_found = False
|
||||
win32api.RegCloseKey(h_key)
|
||||
if key_found:
|
||||
return os.path.join(named_base, bindir)
|
||||
return os.path.join(win32api.GetSystemDirectory(), bindir)
|
||||
|
||||
|
||||
def shellquote(s):
|
||||
if os.name == 'nt':
|
||||
return '"' + s.replace('"', '"\\"') + '"'
|
||||
return "'" + s.replace("'", "'\\''") + "'"
|
||||
|
||||
|
||||
version = '@BIND9_VERSION@'
|
||||
sysconfdir = '@expanded_sysconfdir@'
|
Reference in New Issue
Block a user