diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 18:37:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 18:37:14 +0000 |
commit | ea648e70a989cca190cd7403fe892fd2dcc290b4 (patch) | |
tree | e2b6b1c647da68b0d4d66082835e256eb30970e8 /bin/python/isc | |
parent | Initial commit. (diff) | |
download | bind9-upstream.tar.xz bind9-upstream.zip |
Adding upstream version 1:9.11.5.P4+dfsg.upstream/1%9.11.5.P4+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
24 files changed, 3176 insertions, 0 deletions
diff --git a/bin/python/isc/Makefile.in b/bin/python/isc/Makefile.in new file mode 100644 index 0000000..ec17b6b --- /dev/null +++ b/bin/python/isc/Makefile.in @@ -0,0 +1,43 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +srcdir = @srcdir@ +VPATH = @srcdir@ +top_srcdir = @top_srcdir@ + +@BIND9_MAKE_INCLUDES@ + +SUBDIRS = tests + +PYTHON = @PYTHON@ + +PYSRCS = __init__.py checkds.py coverage.py dnskey.py eventlist.py \ + keydict.py keyevent.py keymgr.py keyseries.py keyzone.py \ + policy.py rndc.py utils.py + +TARGETS = parsetab.py + +@BIND9_MAKE_RULES@ + +.SUFFIXES: .py .pyc +.py.pyc: + $(PYTHON) -m compileall . + +parsetab.py: policy.py + $(PYTHON) policy.py parse /dev/null > /dev/null + PYTHONPATH=${srcdir} $(PYTHON) -m parsetab + +check test: subdirs + +clean distclean:: + rm -f *.pyc parser.out parsetab.py + rm -rf __pycache__ build + +distclean:: + rm -rf ${PYSRCS} diff --git a/bin/python/isc/__init__.py.in b/bin/python/isc/__init__.py.in new file mode 100644 index 0000000..916333c --- /dev/null +++ b/bin/python/isc/__init__.py.in @@ -0,0 +1,24 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +__all__ = ['checkds', 'coverage', 'keymgr', 'dnskey', 'eventlist', + 'keydict', 'keyevent', 'keyseries', 'keyzone', 'policy', + 'parsetab', 'rndc', '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.rndc import * +from isc.utils import * diff --git a/bin/python/isc/checkds.py.in b/bin/python/isc/checkds.py.in new file mode 100644 index 0000000..42fbfcd --- /dev/null +++ b/bin/python/isc/checkds.py.in @@ -0,0 +1,185 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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.decode('ascii').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('%s does not match %s' % + (fields[0].upper(), self.rrtype)) + + 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) diff --git a/bin/python/isc/coverage.py.in b/bin/python/isc/coverage.py.in new file mode 100644 index 0000000..4e392a7 --- /dev/null +++ b/bin/python/isc/coverage.py.in @@ -0,0 +1,286 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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 dnskey, eventlist, keydict, keyevent, keyzone, utils + +############################################################################ +# 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(r'([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(utils.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) diff --git a/bin/python/isc/dnskey.py.in b/bin/python/isc/dnskey.py.in new file mode 100644 index 0000000..8c5a80f --- /dev/null +++ b/bin/python/isc/dnskey.py.in @@ -0,0 +1,507 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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', 'ED25519', 'ED448') + + 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, randomdev, 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 randomdev: + keygen_cmd += ["-r", randomdev] + + 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].decode('ascii') + newkey = dnskey(keystr, keys_dir, ttl) + return newkey + except Exception as e: + raise Exception('unable to parse generated key: %s' % str(e)) + + def generate_successor(self, keygen_bin, randomdev, prepublish, **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 randomdev: + keygen_cmd += ["-r", randomdev] + + if prepublish: + keygen_cmd += ["-i", str(prepublish)] + + 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].decode('ascii') + 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) + diff --git a/bin/python/isc/eventlist.py.in b/bin/python/isc/eventlist.py.in new file mode 100644 index 0000000..5c1b31a --- /dev/null +++ b/bin/python/isc/eventlist.py.in @@ -0,0 +1,166 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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 + diff --git a/bin/python/isc/keydict.py.in b/bin/python/isc/keydict.py.in new file mode 100644 index 0000000..578a847 --- /dev/null +++ b/bin/python/isc/keydict.py.in @@ -0,0 +1,84 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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) diff --git a/bin/python/isc/keyevent.py.in b/bin/python/isc/keyevent.py.in new file mode 100644 index 0000000..cfc935a --- /dev/null +++ b/bin/python/isc/keyevent.py.in @@ -0,0 +1,76 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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 diff --git a/bin/python/isc/keymgr.py.in b/bin/python/isc/keymgr.py.in new file mode 100644 index 0000000..c193daa --- /dev/null +++ b/bin/python/isc/keymgr.py.in @@ -0,0 +1,154 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +from __future__ import print_function +import os, sys, argparse, glob, re, time, calendar, pprint +from collections import defaultdict + +prog='dnssec-keymgr' + +from isc import dnskey, keydict, keyseries, policy, parsetab, utils + +############################################################################ +# 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(utils.prefix('sbin'), 'dnssec-keygen')) + settime = set_path('dnssec-settime', + os.path.join(utils.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('-r', dest='randomdev', type=str, default=None, + help='Path to a file containing random data to pass 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: + fatal("ERROR: dnssec-keygen not found") + + if args.settime is None: + fatal("ERROR: 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, + 'dnssec-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, + 'randomdev': args.randomdev} + + 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)) diff --git a/bin/python/isc/keyseries.py.in b/bin/python/isc/keyseries.py.in new file mode 100644 index 0000000..e1241f0 --- /dev/null +++ b/bin/python/isc/keyseries.py.in @@ -0,0 +1,191 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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: + if not (k.delete() and k.delete() < now): + self._K[zone][alg].append(k) + else: + if not (k.delete() and k.delete() < now): + self._Z[zone][alg].append(k) + + self._K[zone][alg].sort() + self._Z[zone][alg].sort() + + 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'], + self._context['randomdev'], + prepub, **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'], + self._context['randomdev'], + 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'], + self._context['randomdev'], + 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))) diff --git a/bin/python/isc/keyzone.py.in b/bin/python/isc/keyzone.py.in new file mode 100644 index 0000000..2451d43 --- /dev/null +++ b/bin/python/isc/keyzone.py.in @@ -0,0 +1,55 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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 diff --git a/bin/python/isc/policy.py.in b/bin/python/isc/policy.py.in new file mode 100644 index 0000000..f7829fa --- /dev/null +++ b/bin/python/isc/policy.py.in @@ -0,0 +1,728 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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|ECDSAP256SHA256|ECDSAP384SHA384|ED25519|ED448)\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): + if 'maketrans' in dir(str): + trans = str.maketrans('_', '-') + else: + trans = maketrans('_', '-') + for r in self.reserved: + self.reserved_map[r.lower().translate(trans)] = 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, + 'ECDSAP256SHA256': None, + 'ECDSAP384SHA384': None, + 'ED25519': None, + 'ED448': 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 + + if self.algorithm in ['ECCGOST', \ + 'ECDSAP256SHA256', \ + 'ECDSAP384SHA384', \ + 'ED25519', \ + 'ED448']: + self.ksk_keysize = None + self.zsk_keysize = None + + 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['ECDSAP256SHA256'] = copy(p) + self.alg_policy['ECDSAP256SHA256'].algorithm = "ECDSAP256SHA256" + self.alg_policy['ECDSAP256SHA256'].name = "ECDSAP256SHA256" + self.alg_policy['ECDSAP256SHA256'].ksk_keysize = None; + self.alg_policy['ECDSAP256SHA256'].zsk_keysize = None; + + self.alg_policy['ECDSAP384SHA384'] = copy(p) + self.alg_policy['ECDSAP384SHA384'].algorithm = "ECDSAP384SHA384" + self.alg_policy['ECDSAP384SHA384'].name = "ECDSAP384SHA384" + self.alg_policy['ECDSAP384SHA384'].ksk_keysize = None; + self.alg_policy['ECDSAP384SHA384'].zsk_keysize = None; + + self.alg_policy['ED25519'] = copy(p) + self.alg_policy['ED25519'].algorithm = "ED25519" + self.alg_policy['ED25519'].name = "ED25519" + self.alg_policy['ED25519'].ksk_keysize = None; + self.alg_policy['ED25519'].zsk_keysize = None; + + self.alg_policy['ED448'] = copy(p) + self.alg_policy['ED448'].algorithm = "ED448" + self.alg_policy['ED448'].name = "ED448" + self.alg_policy['ED448'].ksk_keysize = None; + self.alg_policy['ED448'].zsk_keysize = None; + + 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 p.keyttl is None: + parent = p.parent or self.named_policy['default'] + while parent is not None and not parent.keyttl: + parent = parent.parent + p.keyttl = parent and parent.keyttl + + 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_domain(self, p): + '''domain : STR + | QSTRING + | KEYTYPE + | DATESUFFIX''' + p[0] = p[1].strip() + if not re.match(r'^[\w.-][\w.-]*$', p[0]): + raise PolicyException('invalid domain') + 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 domain new_policy policy_option_group SEMI" + self.current.name = p[2].rstrip('.') + self.current.is_zone = True + self.zone_policy[p[2].rstrip('.').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]) diff --git a/bin/python/isc/rndc.py.in b/bin/python/isc/rndc.py.in new file mode 100644 index 0000000..e1a3e77 --- /dev/null +++ b/bin/python/isc/rndc.py.in @@ -0,0 +1,188 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +############################################################################ +# rndc.py +# This module implements the RNDC control protocol. +############################################################################ + +from collections import OrderedDict +import time +import struct +import hashlib +import hmac +import base64 +import random +import socket + + +class rndc(object): + """RNDC protocol client library""" + __algos = {'md5': 157, + 'sha1': 161, + 'sha224': 162, + 'sha256': 163, + 'sha384': 164, + 'sha512': 165} + + def __init__(self, host, algo, secret): + """Creates a persistent connection to RNDC and logs in + host - (ip, port) tuple + algo - HMAC algorithm: one of md5, sha1, sha224, sha256, sha384, sha512 + (with optional prefix 'hmac-') + secret - HMAC secret, base64 encoded""" + self.host = host + algo = algo.lower() + if algo.startswith('hmac-'): + algo = algo[5:] + self.algo = algo + self.hlalgo = getattr(hashlib, algo) + self.secret = base64.b64decode(secret) + self.ser = random.randint(0, 1 << 24) + self.nonce = None + self.__connect_login() + + def call(self, cmd): + """Call a RNDC command, all parsing is done on the server side + cmd - a complete string with a command (eg 'reload zone example.com') + """ + return dict(self.__command(type=cmd)['_data']) + + def __serialize_dict(self, data, ignore_auth=False): + rv = bytearray() + for k, v in data.items(): + if ignore_auth and k == '_auth': + continue + rv += struct.pack('B', len(k)) + k.encode('ascii') + if type(v) == str: + rv += struct.pack('>BI', 1, len(v)) + v.encode('ascii') + elif type(v) == bytes: + rv += struct.pack('>BI', 1, len(v)) + v + elif type(v) == bytearray: + rv += struct.pack('>BI', 1, len(v)) + v + elif type(v) == OrderedDict: + sd = self.__serialize_dict(v) + rv += struct.pack('>BI', 2, len(sd)) + sd + else: + raise NotImplementedError('Cannot serialize element of type %s' + % type(v)) + return rv + + def __prep_message(self, *args, **kwargs): + self.ser += 1 + now = int(time.time()) + data = OrderedDict(*args, **kwargs) + + d = OrderedDict() + d['_auth'] = OrderedDict() + d['_ctrl'] = OrderedDict() + d['_ctrl']['_ser'] = str(self.ser) + d['_ctrl']['_tim'] = str(now) + d['_ctrl']['_exp'] = str(now+60) + if self.nonce is not None: + d['_ctrl']['_nonce'] = self.nonce + d['_data'] = data + + msg = self.__serialize_dict(d, ignore_auth=True) + hash = hmac.new(self.secret, msg, self.hlalgo).digest() + bhash = base64.b64encode(hash) + if self.algo == 'md5': + d['_auth']['hmd5'] = struct.pack('22s', bhash) + else: + d['_auth']['hsha'] = bytearray(struct.pack('B88s', + self.__algos[self.algo], bhash)) + msg = self.__serialize_dict(d) + msg = struct.pack('>II', len(msg) + 4, 1) + msg + return msg + + def __verify_msg(self, msg): + if self.nonce is not None and msg['_ctrl']['_nonce'] != self.nonce: + return False + if self.algo == 'md5': + bhash = msg['_auth']['hmd5'] + else: + bhash = msg['_auth']['hsha'][1:] + if type(bhash) == bytes: + bhash = bhash.decode('ascii') + bhash += '=' * (4 - (len(bhash) % 4)) + remote_hash = base64.b64decode(bhash) + my_msg = self.__serialize_dict(msg, ignore_auth=True) + my_hash = hmac.new(self.secret, my_msg, self.hlalgo).digest() + return (my_hash == remote_hash) + + def __command(self, *args, **kwargs): + msg = self.__prep_message(*args, **kwargs) + sent = self.socket.send(msg) + if sent != len(msg): + raise IOError("Cannot send the message") + + header = self.socket.recv(8) + if len(header) != 8: + # What should we throw here? Bad auth can cause this... + raise IOError("Can't read response header") + + length, version = struct.unpack('>II', header) + if version != 1: + raise NotImplementedError('Wrong message version %d' % version) + + # it includes the header + length -= 4 + data = self.socket.recv(length, socket.MSG_WAITALL) + if len(data) != length: + raise IOError("Can't read response data") + + if type(data) == str: + data = bytearray(data) + msg = self.__parse_message(data) + if not self.__verify_msg(msg): + raise IOError("Authentication failure") + + return msg + + def __connect_login(self): + self.socket = socket.create_connection(self.host) + self.nonce = None + msg = self.__command(type='null') + self.nonce = msg['_ctrl']['_nonce'] + + def __parse_element(self, input): + pos = 0 + labellen = input[pos] + pos += 1 + label = input[pos:pos+labellen].decode('ascii') + pos += labellen + type = input[pos] + pos += 1 + datalen = struct.unpack('>I', input[pos:pos+4])[0] + pos += 4 + data = input[pos:pos+datalen] + pos += datalen + rest = input[pos:] + + if type == 1: # raw binary value + return label, data, rest + elif type == 2: # dictionary + d = OrderedDict() + while len(data) > 0: + ilabel, value, data = self.__parse_element(data) + d[ilabel] = value + return label, d, rest + # TODO type 3 - list + else: + raise NotImplementedError('Unknown element type %d' % type) + + def __parse_message(self, input): + rv = OrderedDict() + hdata = None + while len(input) > 0: + label, value, input = self.__parse_element(input) + rv[label] = value + return rv diff --git a/bin/python/isc/tests/Makefile.in b/bin/python/isc/tests/Makefile.in new file mode 100644 index 0000000..60b9bd7 --- /dev/null +++ b/bin/python/isc/tests/Makefile.in @@ -0,0 +1,31 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +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 \ + PYTHONPATH=${srcdir}/../.. $(PYTHON) ${srcdir}/$$test; \ + done + +clean distclean:: + rm -rf *.pyc __pycache__ + +distclean:: + rm -f ${PYTESTS} diff --git a/bin/python/isc/tests/dnskey_test.py.in b/bin/python/isc/tests/dnskey_test.py.in new file mode 100644 index 0000000..91d5245 --- /dev/null +++ b/bin/python/isc/tests/dnskey_test.py.in @@ -0,0 +1,52 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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() diff --git a/bin/python/isc/tests/policy_test.py.in b/bin/python/isc/tests/policy_test.py.in new file mode 100644 index 0000000..b09c62f --- /dev/null +++ b/bin/python/isc/tests/policy_test.py.in @@ -0,0 +1,92 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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')) + + def test_numeric_zone(self): + pol = policy.dnssec_policy() + pol.load('test-policies/05-numeric-zone.pol') + + p = pol.policy('99example.test', novalidate=True) + self.assertEqual(p.validate(), (True, "")) + +if __name__ == "__main__": + unittest.main() diff --git a/bin/python/isc/tests/test-policies/01-keysize.pol b/bin/python/isc/tests/test-policies/01-keysize.pol new file mode 100644 index 0000000..8b62c5f --- /dev/null +++ b/bin/python/isc/tests/test-policies/01-keysize.pol @@ -0,0 +1,52 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +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; +}; diff --git a/bin/python/isc/tests/test-policies/02-prepublish.pol b/bin/python/isc/tests/test-policies/02-prepublish.pol new file mode 100644 index 0000000..bd35386 --- /dev/null +++ b/bin/python/isc/tests/test-policies/02-prepublish.pol @@ -0,0 +1,42 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +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; +}; + + diff --git a/bin/python/isc/tests/test-policies/03-postpublish.pol b/bin/python/isc/tests/test-policies/03-postpublish.pol new file mode 100644 index 0000000..4d4a8ef --- /dev/null +++ b/bin/python/isc/tests/test-policies/03-postpublish.pol @@ -0,0 +1,42 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +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; +}; + + diff --git a/bin/python/isc/tests/test-policies/04-combined-pre-post.pol b/bin/python/isc/tests/test-policies/04-combined-pre-post.pol new file mode 100644 index 0000000..d612d81 --- /dev/null +++ b/bin/python/isc/tests/test-policies/04-combined-pre-post.pol @@ -0,0 +1,66 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +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; +}; + + diff --git a/bin/python/isc/tests/test-policies/05-numeric-zone.pol b/bin/python/isc/tests/test-policies/05-numeric-zone.pol new file mode 100644 index 0000000..1db8c08 --- /dev/null +++ b/bin/python/isc/tests/test-policies/05-numeric-zone.pol @@ -0,0 +1,15 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +// Zone policy that uses a numeric name +zone "99example.test" { + coverage 6mo; +}; diff --git a/bin/python/isc/tests/testdata/Kexample.com.+007+35529.key b/bin/python/isc/tests/testdata/Kexample.com.+007+35529.key new file mode 100644 index 0000000..c5afbe2 --- /dev/null +++ b/bin/python/isc/tests/testdata/Kexample.com.+007+35529.key @@ -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= diff --git a/bin/python/isc/tests/testdata/Kexample.com.+007+35529.private b/bin/python/isc/tests/testdata/Kexample.com.+007+35529.private new file mode 100644 index 0000000..af22c6a --- /dev/null +++ b/bin/python/isc/tests/testdata/Kexample.com.+007+35529.private @@ -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 diff --git a/bin/python/isc/utils.py.in b/bin/python/isc/utils.py.in new file mode 100644 index 0000000..0241871 --- /dev/null +++ b/bin/python/isc/utils.py.in @@ -0,0 +1,71 @@ +############################################################################ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. +############################################################################ + +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) + + hklm = win32con.HKEY_LOCAL_MACHINE + bind_subkey = "Software\\ISC\\BIND" + sam = win32con.KEY_READ + h_key = None + key_found = True + # can fail if the registry redirected for 32/64 bits + try: + h_key = win32api.RegOpenKeyEx(hklm, bind_subkey, 0, sam) + except: + key_found = False + # retry for 32 bit python with 64 bit bind9 + if not key_found: + key_found = True + sam64 = sam | win32con.KEY_WOW64_64KEY + try: + h_key = win32api.RegOpenKeyEx(hklm, bind_subkey, 0, sam64) + except: + key_found = False + # retry 64 bit python with 32 bit bind9 + if not key_found: + key_found = True + sam32 = sam | win32con.KEY_WOW64_32KEY + try: + h_key = win32api.RegOpenKeyEx(hklm, bind_subkey, 0, sam32) + 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@' +if os.name != 'nt': + sysconfdir = '@expanded_sysconfdir@' +else: + sysconfdir = prefix('etc') |