"Fossies" - the Fresh Open Source Software Archive

Member "bind-9.16.7/bin/python/isc/dnskey.py.in" (4 Sep 2020, 16414 Bytes) of package /linux/misc/dns/bind9/9.16.7/bind-9.16.7.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file.

    1 ############################################################################
    2 # Copyright (C) Internet Systems Consortium, Inc. ("ISC")
    3 #
    4 # This Source Code Form is subject to the terms of the Mozilla Public
    5 # License, v. 2.0. If a copy of the MPL was not distributed with this
    6 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
    7 #
    8 # See the COPYRIGHT file distributed with this work for additional
    9 # information regarding copyright ownership.
   10 ############################################################################
   11 
   12 import os
   13 import time
   14 import calendar
   15 from subprocess import Popen, PIPE
   16 
   17 ########################################################################
   18 # Class dnskey
   19 ########################################################################
   20 class TimePast(Exception):
   21     def __init__(self, key, prop, value):
   22         super(TimePast, self).__init__('%s time for key %s (%d) is already past'
   23                                        % (prop, key, value))
   24 
   25 class dnskey:
   26     """An individual DNSSEC key.  Identified by path, name, algorithm, keyid.
   27     Contains a dictionary of metadata events."""
   28 
   29     _PROPS = ('Created', 'Publish', 'Activate', 'Inactive', 'Delete',
   30               'Revoke', 'DSPublish', 'SyncPublish', 'SyncDelete')
   31     _OPTS = (None, '-P', '-A', '-I', '-D', '-R', None, '-Psync', '-Dsync')
   32 
   33     _ALGNAMES = (None, 'RSAMD5', 'DH', 'DSA', None, 'RSASHA1',
   34                  'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None,
   35                  'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256',
   36                  'ECDSAP384SHA384', 'ED25519', 'ED448')
   37 
   38     def __init__(self, key, directory=None, keyttl=None):
   39         # this makes it possible to use algname as a class or instance method
   40         if isinstance(key, tuple) and len(key) == 3:
   41             self._dir = directory or '.'
   42             (name, alg, keyid) = key
   43             self.fromtuple(name, alg, keyid, keyttl)
   44 
   45         self._dir = directory or os.path.dirname(key) or '.'
   46         key = os.path.basename(key)
   47         (name, alg, keyid) = key.split('+')
   48         name = name[1:-1]
   49         alg = int(alg)
   50         keyid = int(keyid.split('.')[0])
   51         self.fromtuple(name, alg, keyid, keyttl)
   52 
   53     def fromtuple(self, name, alg, keyid, keyttl):
   54         if name.endswith('.'):
   55             fullname = name
   56             name = name.rstrip('.')
   57         else:
   58             fullname = name + '.'
   59 
   60         keystr = "K%s+%03d+%05d" % (fullname, alg, keyid)
   61         key_file = self._dir + (self._dir and os.sep or '') + keystr + ".key"
   62         private_file = (self._dir + (self._dir and os.sep or '') +
   63                         keystr + ".private")
   64 
   65         self.keystr = keystr
   66 
   67         self.name = name
   68         self.alg = int(alg)
   69         self.keyid = int(keyid)
   70         self.fullname = fullname
   71 
   72         kfp = open(key_file, "r")
   73         for line in kfp:
   74             if line[0] == ';':
   75                 continue
   76             tokens = line.split()
   77             if not tokens:
   78                 continue
   79 
   80             if tokens[1].lower() in ('in', 'ch', 'hs'):
   81                 septoken = 3
   82                 self.ttl = keyttl
   83             else:
   84                 septoken = 4
   85                 self.ttl = int(tokens[1]) if not keyttl else keyttl
   86 
   87             if (int(tokens[septoken]) & 0x1) == 1:
   88                 self.sep = True
   89             else:
   90                 self.sep = False
   91         kfp.close()
   92 
   93         pfp = open(private_file, "rU")
   94 
   95         self.metadata = dict()
   96         self._changed = dict()
   97         self._delete = dict()
   98         self._times = dict()
   99         self._fmttime = dict()
  100         self._timestamps = dict()
  101         self._original = dict()
  102         self._origttl = None
  103 
  104         for line in pfp:
  105             line = line.strip()
  106             if not line or line[0] in ('!#'):
  107                 continue
  108             punctuation = [line.find(c) for c in ':= '] + [len(line)]
  109             found = min([pos for pos in punctuation if pos != -1])
  110             name = line[:found].rstrip()
  111             value = line[found:].lstrip(":= ").rstrip()
  112             self.metadata[name] = value
  113 
  114         for prop in dnskey._PROPS:
  115             self._changed[prop] = False
  116             if prop in self.metadata:
  117                 t = self.parsetime(self.metadata[prop])
  118                 self._times[prop] = t
  119                 self._fmttime[prop] = self.formattime(t)
  120                 self._timestamps[prop] = self.epochfromtime(t)
  121                 self._original[prop] = self._timestamps[prop]
  122             else:
  123                 self._times[prop] = None
  124                 self._fmttime[prop] = None
  125                 self._timestamps[prop] = None
  126                 self._original[prop] = None
  127 
  128         pfp.close()
  129 
  130     def commit(self, settime_bin, **kwargs):
  131         quiet = kwargs.get('quiet', False)
  132         cmd = []
  133         first = True
  134 
  135         if self._origttl is not None:
  136             cmd += ["-L", str(self.ttl)]
  137 
  138         for prop, opt in zip(dnskey._PROPS, dnskey._OPTS):
  139             if not opt or not self._changed[prop]:
  140                 continue
  141 
  142             delete = False
  143             if prop in self._delete and self._delete[prop]:
  144                 delete = True
  145 
  146             when = 'none' if delete else self._fmttime[prop]
  147             cmd += [opt, when]
  148             first = False
  149 
  150         if cmd:
  151             fullcmd = [settime_bin, "-K", self._dir] + cmd + [self.keystr,]
  152             if not quiet:
  153                 print('# ' + ' '.join(fullcmd))
  154             try:
  155                 p = Popen(fullcmd, stdout=PIPE, stderr=PIPE)
  156                 stdout, stderr = p.communicate()
  157                 if stderr:
  158                     raise Exception(str(stderr))
  159             except Exception as e:
  160                 raise Exception('unable to run %s: %s' %
  161                                 (settime_bin, str(e)))
  162             self._origttl = None
  163             for prop in dnskey._PROPS:
  164                 self._original[prop] = self._timestamps[prop]
  165                 self._changed[prop] = False
  166 
  167     @classmethod
  168     def generate(cls, keygen_bin, randomdev, keys_dir, name, alg, keysize, sep,
  169                  ttl, publish=None, activate=None, **kwargs):
  170         quiet = kwargs.get('quiet', False)
  171 
  172         keygen_cmd = [keygen_bin, "-q", "-K", keys_dir, "-L", str(ttl)]
  173 
  174         if randomdev:
  175             keygen_cmd += ["-r", randomdev]
  176 
  177         if sep:
  178             keygen_cmd.append("-fk")
  179 
  180         if alg:
  181             keygen_cmd += ["-a", alg]
  182 
  183         if keysize:
  184             keygen_cmd += ["-b", str(keysize)]
  185 
  186         if publish:
  187             t = dnskey.timefromepoch(publish)
  188             keygen_cmd += ["-P", dnskey.formattime(t)]
  189 
  190         if activate:
  191             t = dnskey.timefromepoch(activate)
  192             keygen_cmd += ["-A", dnskey.formattime(activate)]
  193 
  194         keygen_cmd.append(name)
  195 
  196         if not quiet:
  197             print('# ' + ' '.join(keygen_cmd))
  198 
  199         p = Popen(keygen_cmd, stdout=PIPE, stderr=PIPE)
  200         stdout, stderr = p.communicate()
  201         if stderr:
  202             raise Exception('unable to generate key: ' + str(stderr))
  203 
  204         try:
  205             keystr = stdout.splitlines()[0].decode('ascii')
  206             newkey = dnskey(keystr, keys_dir, ttl)
  207             return newkey
  208         except Exception as e:
  209             raise Exception('unable to parse generated key: %s' % str(e))
  210 
  211     def generate_successor(self, keygen_bin, randomdev, prepublish, **kwargs):
  212         quiet = kwargs.get('quiet', False)
  213 
  214         if not self.inactive():
  215             raise Exception("predecessor key %s has no inactive date" % self)
  216 
  217         keygen_cmd = [keygen_bin, "-q", "-K", self._dir, "-S", self.keystr]
  218 
  219         if self.ttl:
  220             keygen_cmd += ["-L", str(self.ttl)]
  221 
  222         if randomdev:
  223             keygen_cmd += ["-r", randomdev]
  224 
  225         if prepublish:
  226             keygen_cmd += ["-i", str(prepublish)]
  227 
  228         if not quiet:
  229             print('# ' + ' '.join(keygen_cmd))
  230 
  231         p = Popen(keygen_cmd, stdout=PIPE, stderr=PIPE)
  232         stdout, stderr = p.communicate()
  233         if stderr:
  234             raise Exception('unable to generate key: ' + stderr)
  235 
  236         try:
  237             keystr = stdout.splitlines()[0].decode('ascii')
  238             newkey = dnskey(keystr, self._dir, self.ttl)
  239             return newkey
  240         except:
  241             raise Exception('unable to generate successor for key %s' % self)
  242 
  243     @staticmethod
  244     def algstr(alg):
  245         name = None
  246         if alg in range(len(dnskey._ALGNAMES)):
  247             name = dnskey._ALGNAMES[alg]
  248         return name if name else ("%03d" % alg)
  249 
  250     @staticmethod
  251     def algnum(alg):
  252         if not alg:
  253             return None
  254         alg = alg.upper()
  255         try:
  256             return dnskey._ALGNAMES.index(alg)
  257         except ValueError:
  258             return None
  259 
  260     def algname(self, alg=None):
  261         return self.algstr(alg or self.alg)
  262 
  263     @staticmethod
  264     def timefromepoch(secs):
  265         return time.gmtime(secs)
  266 
  267     @staticmethod
  268     def parsetime(string):
  269         return time.strptime(string, "%Y%m%d%H%M%S")
  270 
  271     @staticmethod
  272     def epochfromtime(t):
  273         return calendar.timegm(t)
  274 
  275     @staticmethod
  276     def formattime(t):
  277         return time.strftime("%Y%m%d%H%M%S", t)
  278 
  279     def setmeta(self, prop, secs, now, **kwargs):
  280         force = kwargs.get('force', False)
  281 
  282         if self._timestamps[prop] == secs:
  283             return
  284 
  285         if self._original[prop] is not None and \
  286            self._original[prop] < now and not force:
  287             raise TimePast(self, prop, self._original[prop])
  288 
  289         if secs is None:
  290             self._changed[prop] = False \
  291                 if self._original[prop] is None else True
  292 
  293             self._delete[prop] = True
  294             self._timestamps[prop] = None
  295             self._times[prop] = None
  296             self._fmttime[prop] = None
  297             return
  298 
  299         t = self.timefromepoch(secs)
  300         self._timestamps[prop] = secs
  301         self._times[prop] = t
  302         self._fmttime[prop] = self.formattime(t)
  303         self._changed[prop] = False if \
  304             self._original[prop] == self._timestamps[prop] else True
  305 
  306     def gettime(self, prop):
  307         return self._times[prop]
  308 
  309     def getfmttime(self, prop):
  310         return self._fmttime[prop]
  311 
  312     def gettimestamp(self, prop):
  313         return self._timestamps[prop]
  314 
  315     def created(self):
  316         return self._timestamps["Created"]
  317 
  318     def syncpublish(self):
  319         return self._timestamps["SyncPublish"]
  320 
  321     def setsyncpublish(self, secs, now=time.time(), **kwargs):
  322         self.setmeta("SyncPublish", secs, now, **kwargs)
  323 
  324     def publish(self):
  325         return self._timestamps["Publish"]
  326 
  327     def setpublish(self, secs, now=time.time(), **kwargs):
  328         self.setmeta("Publish", secs, now, **kwargs)
  329 
  330     def activate(self):
  331         return self._timestamps["Activate"]
  332 
  333     def setactivate(self, secs, now=time.time(), **kwargs):
  334         self.setmeta("Activate", secs, now, **kwargs)
  335 
  336     def revoke(self):
  337         return self._timestamps["Revoke"]
  338 
  339     def setrevoke(self, secs, now=time.time(), **kwargs):
  340         self.setmeta("Revoke", secs, now, **kwargs)
  341 
  342     def inactive(self):
  343         return self._timestamps["Inactive"]
  344 
  345     def setinactive(self, secs, now=time.time(), **kwargs):
  346         self.setmeta("Inactive", secs, now, **kwargs)
  347 
  348     def delete(self):
  349         return self._timestamps["Delete"]
  350 
  351     def setdelete(self, secs, now=time.time(), **kwargs):
  352         self.setmeta("Delete", secs, now, **kwargs)
  353 
  354     def syncdelete(self):
  355         return self._timestamps["SyncDelete"]
  356 
  357     def setsyncdelete(self, secs, now=time.time(), **kwargs):
  358         self.setmeta("SyncDelete", secs, now, **kwargs)
  359 
  360     def setttl(self, ttl):
  361         if ttl is None or self.ttl == ttl:
  362             return
  363         elif self._origttl is None:
  364             self._origttl = self.ttl
  365             self.ttl = ttl
  366         elif self._origttl == ttl:
  367             self._origttl = None
  368             self.ttl = ttl
  369         else:
  370             self.ttl = ttl
  371 
  372     def keytype(self):
  373         return ("KSK" if self.sep else "ZSK")
  374 
  375     def __str__(self):
  376         return ("%s/%s/%05d"
  377                 % (self.name, self.algname(), self.keyid))
  378 
  379     def __repr__(self):
  380         return ("%s/%s/%05d (%s)"
  381                 % (self.name, self.algname(), self.keyid,
  382                    ("KSK" if self.sep else "ZSK")))
  383 
  384     def date(self):
  385         return (self.activate() or self.publish() or self.created())
  386 
  387     # keys are sorted first by zone name, then by algorithm. within
  388     # the same name/algorithm, they are sorted according to their
  389     # 'date' value: the activation date if set, OR the publication
  390     # if set, OR the creation date.
  391     def __lt__(self, other):
  392         if self.name != other.name:
  393             return self.name < other.name
  394         if self.alg != other.alg:
  395             return self.alg < other.alg
  396         return self.date() < other.date()
  397 
  398     def check_prepub(self, output=None):
  399         def noop(*args, **kwargs): pass
  400         if not output:
  401             output = noop
  402 
  403         now = int(time.time())
  404         a = self.activate()
  405         p = self.publish()
  406 
  407         if not a:
  408             return False
  409 
  410         if not p:
  411             if a > now:
  412                 output("WARNING: Key %s is scheduled for\n"
  413                        "\t activation but not for publication."
  414                        % repr(self))
  415             return False
  416 
  417         if p <= now and a <= now:
  418             return True
  419 
  420         if p == a:
  421             output("WARNING: %s is scheduled to be\n"
  422                    "\t published and activated at the same time. This\n"
  423                    "\t could result in a coverage gap if the zone was\n"
  424                    "\t previously signed. Activation should be at least\n"
  425                    "\t %s after publication."
  426                    % (repr(self),
  427                        dnskey.duration(self.ttl) or 'one DNSKEY TTL'))
  428             return True
  429 
  430         if a < p:
  431             output("WARNING: Key %s is active before it is published"
  432                    % repr(self))
  433             return False
  434 
  435         if self.ttl is not None and a - p < self.ttl:
  436             output("WARNING: Key %s is activated too soon\n"
  437                    "\t after publication; this could result in coverage \n"
  438                    "\t gaps due to resolver caches containing old data.\n"
  439                    "\t Activation should be at least %s after\n"
  440                    "\t publication."
  441                    % (repr(self),
  442                       dnskey.duration(self.ttl) or 'one DNSKEY TTL'))
  443             return False
  444 
  445         return True
  446 
  447     def check_postpub(self, output = None, timespan = None):
  448         def noop(*args, **kwargs): pass
  449         if output is None:
  450             output = noop
  451 
  452         if timespan is None:
  453             timespan = self.ttl
  454 
  455         if timespan is None:
  456             output("WARNING: Key %s using default TTL." % repr(self))
  457             timespan = (60*60*24)
  458 
  459         now = time.time()
  460         d = self.delete()
  461         i = self.inactive()
  462 
  463         if not d:
  464             return False
  465 
  466         if not i:
  467             if d > now:
  468                 output("WARNING: Key %s is scheduled for\n"
  469                        "\t deletion but not for inactivation." % repr(self))
  470             return False
  471 
  472         if d < now and i < now:
  473             return True
  474 
  475         if d < i:
  476             output("WARNING: Key %s is scheduled for\n"
  477                    "\t deletion before inactivation."
  478                    % repr(self))
  479             return False
  480 
  481         if d - i < timespan:
  482             output("WARNING: Key %s scheduled for\n"
  483                    "\t deletion too soon after deactivation; this may \n"
  484                    "\t result in coverage gaps due to resolver caches\n"
  485                    "\t containing old data.  Deletion should be at least\n"
  486                    "\t %s after inactivation."
  487                    % (repr(self), dnskey.duration(timespan)))
  488             return False
  489 
  490         return True
  491 
  492     @staticmethod
  493     def duration(secs):
  494         if not secs:
  495             return None
  496 
  497         units = [("year", 60*60*24*365),
  498                  ("month", 60*60*24*30),
  499                  ("day", 60*60*24),
  500                  ("hour", 60*60),
  501                  ("minute", 60),
  502                  ("second", 1)]
  503 
  504         output = []
  505         for unit in units:
  506             v, secs = secs // unit[1], secs % unit[1]
  507             if v > 0:
  508                 output.append("%d %s%s" % (v, unit[0], "s" if v > 1 else ""))
  509 
  510         return ", ".join(output)
  511