xref: /minix3/external/bsd/bind/dist/bin/python/dnssec-coverage.py.in (revision 00b67f09dd46474d133c95011a48590a8e8f94c7)
1#!@PYTHON@
2############################################################################
3# Copyright (C) 2013, 2014  Internet Systems Consortium, Inc. ("ISC")
4#
5# Permission to use, copy, modify, and/or distribute this software for any
6# purpose with or without fee is hereby granted, provided that the above
7# copyright notice and this permission notice appear in all copies.
8#
9# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
10# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11# AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
12# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
14# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15# PERFORMANCE OF THIS SOFTWARE.
16############################################################################
17
18import argparse
19import os
20import glob
21import sys
22import re
23import time
24import calendar
25from collections import defaultdict
26import pprint
27
28prog='dnssec-coverage'
29
30# These routines permit platform-independent location of BIND 9 tools
31if os.name == 'nt':
32    import win32con
33    import win32api
34
35def prefix(bindir = ''):
36    if os.name != 'nt':
37        return os.path.join('@prefix@', bindir)
38
39    bind_subkey = "Software\\ISC\\BIND"
40    hKey = None
41    keyFound = True
42    try:
43        hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey)
44    except:
45        keyFound = False
46    if keyFound:
47        try:
48            (namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir")
49        except:
50            keyFound = False
51        win32api.RegCloseKey(hKey)
52    if keyFound:
53        return os.path.join(namedBase, bindir)
54    return os.path.join(win32api.GetSystemDirectory(), bindir)
55
56########################################################################
57# Class Event
58########################################################################
59class Event:
60    """ A discrete key metadata event, e.g., Publish, Activate, Inactive,
61    Delete. Stores the date of the event, and identifying information about
62    the key to which the event will occur."""
63
64    def __init__(self, _what, _key):
65        now = time.time()
66        self.what = _what
67        self.when = _key.metadata[_what]
68        self.key = _key
69        self.keyid = _key.keyid
70        self.sep = _key.sep
71        self.zone = _key.zone
72        self.alg = _key.alg
73
74    def __repr__(self):
75        return repr((self.when, self.what, self.keyid, self.sep,
76                     self.zone, self.alg))
77
78    def showtime(self):
79        return time.strftime("%a %b %d %H:%M:%S UTC %Y", self.when)
80
81    def showkey(self):
82        return self.key.showkey()
83
84    def showkeytype(self):
85        return self.key.showkeytype()
86
87########################################################################
88# Class Key
89########################################################################
90class Key:
91    """An individual DNSSEC key.  Identified by path, zone, algorithm, keyid.
92    Contains a dictionary of metadata events."""
93
94    def __init__(self, keyname):
95        directory = os.path.dirname(keyname)
96        key = os.path.basename(keyname)
97        (zone, alg, keyid) = key.split('+')
98        keyid = keyid.split('.')[0]
99        key = [zone, alg, keyid]
100        key_file = directory + os.sep + '+'.join(key) + ".key"
101        private_file = directory + os.sep + '+'.join(key) + ".private"
102
103        self.zone = zone[1:-1]
104        self.alg = int(alg)
105        self.keyid = int(keyid)
106
107        kfp = open(key_file, "r")
108        for line in kfp:
109            if line[0] == ';':
110                continue
111            tokens = line.split()
112            if not tokens:
113                continue
114
115            if tokens[1].lower() in ('in', 'ch', 'hs'):
116                septoken = 3
117                self.ttl = args.keyttl
118                if not self.ttl:
119                    vspace()
120                    print("WARNING: Unable to determine TTL for DNSKEY %s." %
121                           self.showkey())
122                    print("\t Using 1 day (86400 seconds); re-run with the -d "
123                          "option for more\n\t accurate results.")
124                    self.ttl = 86400
125            else:
126                septoken = 4
127                self.ttl = int(tokens[1]) if not args.keyttl else args.keyttl
128
129            if (int(tokens[septoken]) & 0x1) == 1:
130                self.sep = True
131            else:
132                self.sep = False
133        kfp.close()
134
135        pfp = open(private_file, "rU")
136        propDict = dict()
137        for propLine in pfp:
138            propDef = propLine.strip()
139            if len(propDef) == 0:
140                continue
141            if propDef[0] in ('!', '#'):
142                continue
143            punctuation = [propDef.find(c) for c in ':= '] + [len(propDef)]
144            found = min([ pos for pos in punctuation if pos != -1 ])
145            name = propDef[:found].rstrip()
146            value =  propDef[found:].lstrip(":= ").rstrip()
147            propDict[name] = value
148
149        if("Publish" in propDict):
150            propDict["Publish"] = time.strptime(propDict["Publish"],
151                                                "%Y%m%d%H%M%S")
152
153        if("Activate" in propDict):
154            propDict["Activate"] = time.strptime(propDict["Activate"],
155                                                 "%Y%m%d%H%M%S")
156
157        if("Inactive" in propDict):
158            propDict["Inactive"] = time.strptime(propDict["Inactive"],
159                                                 "%Y%m%d%H%M%S")
160
161        if("Delete" in propDict):
162            propDict["Delete"] = time.strptime(propDict["Delete"],
163                                               "%Y%m%d%H%M%S")
164
165        if("Revoke" in propDict):
166            propDict["Revoke"] = time.strptime(propDict["Revoke"],
167                                               "%Y%m%d%H%M%S")
168        pfp.close()
169        self.metadata = propDict
170
171    def showkey(self):
172        return "%s/%03d/%05d" % (self.zone, self.alg, self.keyid);
173
174    def showkeytype(self):
175        return ("KSK" if self.sep else "ZSK")
176
177    # ensure that the gap between Publish and Activate is big enough
178    def check_prepub(self):
179        now = time.time()
180
181        if (not "Activate" in self.metadata):
182            debug_print("No Activate information in key: %s" % self.showkey())
183            return False
184        a = calendar.timegm(self.metadata["Activate"])
185
186        if (not "Publish" in self.metadata):
187            debug_print("No Publish information in key: %s" % self.showkey())
188            if a > now:
189                vspace()
190                print("WARNING: Key %s (%s) is scheduled for activation but \n"
191                  "\t not for publication." %
192                  (self.showkey(), self.showkeytype()))
193            return False
194        p = calendar.timegm(self.metadata["Publish"])
195
196        now = time.time()
197        if p < now and a < now:
198            return True
199
200        if p == a:
201            vspace()
202            print ("WARNING: %s (%s) is scheduled to be published and\n"
203                   "\t activated at the same time. This could result in a\n"
204                   "\t coverage gap if the zone was previously signed." %
205                   (self.showkey(), self.showkeytype()))
206            print("\t Activation should be at least %s after publication."
207                    % duration(self.ttl))
208            return True
209
210        if a < p:
211            vspace()
212            print("WARNING: Key %s (%s) is active before it is published" %
213                  (self.showkey(), self.showkeytype()))
214            return False
215
216        if (a - p < self.ttl):
217            vspace()
218            print("WARNING: Key %s (%s) is activated too soon after\n"
219                  "\t publication; this could result in coverage gaps due to\n"
220                  "\t resolver caches containing old data."
221                  % (self.showkey(), self.showkeytype()))
222            print("\t Activation should be at least %s after publication." %
223                  duration(self.ttl))
224            return False
225
226        return True
227
228    # ensure that the gap between Inactive and Delete is big enough
229    def check_postpub(self, timespan = None):
230        if not timespan:
231            timespan = self.ttl
232
233        now = time.time()
234
235        if (not "Delete" in self.metadata):
236            debug_print("No Delete information in key: %s" % self.showkey())
237            return False
238        d = calendar.timegm(self.metadata["Delete"])
239
240        if (not "Inactive" in self.metadata):
241            debug_print("No Inactive information in key: %s" % self.showkey())
242            if d > now:
243                vspace()
244                print("WARNING: Key %s (%s) is scheduled for deletion but\n"
245                      "\t not for inactivation." %
246                      (self.showkey(), self.showkeytype()))
247            return False
248        i = calendar.timegm(self.metadata["Inactive"])
249
250        if d < now and i < now:
251            return True
252
253        if (d < i):
254            vspace()
255            print("WARNING: Key %s (%s) is scheduled for deletion before\n"
256                  "\t inactivation." % (self.showkey(), self.showkeytype()))
257            return False
258
259        if (d - i < timespan):
260            vspace()
261            print("WARNING: Key %s (%s) scheduled for deletion too soon after\n"
262                  "\t deactivation; this may result in coverage gaps due to\n"
263                  "\t resolver caches containing old data."
264                  % (self.showkey(), self.showkeytype()))
265            print("\t Deletion should be at least %s after inactivation." %
266                  duration(timespan))
267            return False
268
269        return True
270
271########################################################################
272# class Zone
273########################################################################
274class Zone:
275    """Stores data about a specific zone"""
276
277    def __init__(self, _name, _keyttl = None, _maxttl = None):
278        self.name = _name
279        self.keyttl = _keyttl
280        self.maxttl = _maxttl
281
282    def load(self, filename):
283        if not args.compilezone:
284            sys.stderr.write(prog + ': FATAL: "named-compilezone" not found\n')
285            exit(1)
286
287        if not self.name:
288            return
289
290        maxttl = keyttl = None
291
292        fp = os.popen("%s -o - %s %s 2> /dev/null" %
293                      (args.compilezone, self.name, filename))
294        for line in fp:
295            fields = line.split()
296            if not maxttl or int(fields[1]) > maxttl:
297                maxttl = int(fields[1])
298            if fields[3] == "DNSKEY":
299                keyttl = int(fields[1])
300        fp.close()
301
302        self.keyttl = keyttl
303        self.maxttl = maxttl
304
305############################################################################
306# debug_print:
307############################################################################
308def debug_print(debugVar):
309    """pretty print a variable iff debug mode is enabled"""
310    if not args.debug_mode:
311        return
312    if type(debugVar) == str:
313        print("DEBUG: " + debugVar)
314    else:
315        print("DEBUG: " + pprint.pformat(debugVar))
316    return
317
318############################################################################
319# vspace:
320############################################################################
321_firstline = True
322def vspace():
323    """adds vertical space between two sections of output text if and only
324    if this is *not* the first section being printed"""
325    global _firstline
326    if _firstline:
327        _firstline = False
328    else:
329        print
330
331############################################################################
332# vreset:
333############################################################################
334def vreset():
335    """reset vertical spacing"""
336    global _firstline
337    _firstline = True
338
339############################################################################
340# getunit
341############################################################################
342def getunit(secs, size):
343    """given a number of seconds, and a number of seconds in a larger unit of
344    time, calculate how many of the larger unit there are and return both
345    that and a remainder value"""
346    bigunit = secs // size
347    if bigunit:
348        secs %= size
349    return (bigunit, secs)
350
351############################################################################
352# addtime
353############################################################################
354def addtime(output, unit, t):
355    """add a formatted unit of time to an accumulating string"""
356    if t:
357        output += ("%s%d %s%s" %
358                  ((", " if output else ""),
359                   t, unit, ("s" if t > 1 else "")))
360
361    return output
362
363############################################################################
364# duration:
365############################################################################
366def duration(secs):
367    """given a length of time in seconds, print a formatted human duration
368    in larger units of time
369    """
370    # define units:
371    minute = 60
372    hour = minute * 60
373    day = hour * 24
374    month = day * 30
375    year = day * 365
376
377    # calculate time in units:
378    (years, secs) = getunit(secs, year)
379    (months, secs) = getunit(secs, month)
380    (days, secs) = getunit(secs, day)
381    (hours, secs) = getunit(secs, hour)
382    (minutes, secs) = getunit(secs, minute)
383
384    output = ''
385    output = addtime(output, "year", years)
386    output = addtime(output, "month", months)
387    output = addtime(output, "day", days)
388    output = addtime(output, "hour", hours)
389    output = addtime(output, "minute", minutes)
390    output = addtime(output, "second", secs)
391    return output
392
393############################################################################
394# parse_time
395############################################################################
396def parse_time(s):
397    """convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds"""
398    s = s.strip()
399
400    # if s is an integer, we're done already
401    try:
402        n = int(s)
403        return n
404    except:
405        pass
406
407    # try to parse as a number with a suffix indicating unit of time
408    r = re.compile('([0-9][0-9]*)\s*([A-Za-z]*)')
409    m = r.match(s)
410    if not m:
411        raise Exception("Cannot parse %s" % s)
412    (n, unit) = m.groups()
413    n = int(n)
414    unit = unit.lower()
415    if unit[0] == 'y':
416        return n * 31536000
417    elif unit[0] == 'm' and unit[1] == 'o':
418        return n * 2592000
419    elif unit[0] == 'w':
420        return n * 604800
421    elif unit[0] == 'd':
422        return n * 86400
423    elif unit[0] == 'h':
424        return n * 3600
425    elif unit[0] == 'm' and unit[1] == 'i':
426        return n * 60
427    elif unit[0] == 's':
428        return n
429    else:
430        raise Exception("Invalid suffix %s" % unit)
431
432############################################################################
433# algname:
434############################################################################
435def algname(alg):
436    """return the mnemonic for a DNSSEC algorithm"""
437    names = (None, 'RSAMD5', 'DH', 'DSA', 'ECC', 'RSASHA1',
438            'NSEC3DSA', 'NSEC3RSASHA1', 'RSASHA256', None,
439            'RSASHA512', None, 'ECCGOST', 'ECDSAP256SHA256',
440            'ECDSAP384SHA384')
441    name = None
442    if alg in range(len(names)):
443        name = names[alg]
444    return (name if name else str(alg))
445
446############################################################################
447# list_events:
448############################################################################
449def list_events(eventgroup):
450    """print a list of the events in an eventgroup"""
451    if not eventgroup:
452        return
453    print ("  " + eventgroup[0].showtime() + ":")
454    for event in eventgroup:
455        print ("    %s: %s (%s)" %
456               (event.what, event.showkey(), event.showkeytype()))
457
458############################################################################
459# process_events:
460############################################################################
461def process_events(eventgroup, active, published):
462    """go through the events in an event group in time-order, add to active
463     list upon Activate event, add to published list upon Publish event,
464     remove from active list upon Inactive event, and remove from published
465     upon Delete event. Emit warnings when inconsistant states are reached"""
466    for event in eventgroup:
467        if event.what == "Activate":
468            active.add(event.keyid)
469        elif event.what == "Publish":
470            published.add(event.keyid)
471        elif event.what == "Inactive":
472            if event.keyid not in active:
473                vspace()
474                print ("\tWARNING: %s (%s) scheduled to become inactive "
475                       "before it is active" %
476                       (event.showkey(), event.showkeytype()))
477            else:
478                active.remove(event.keyid)
479        elif event.what == "Delete":
480            if event.keyid in published:
481                published.remove(event.keyid)
482            else:
483                vspace()
484                print ("WARNING: key %s (%s) is scheduled for deletion before "
485                       "it is published, at %s" %
486                       (event.showkey(), event.showkeytype()))
487        elif event.what == "Revoke":
488            # We don't need to worry about the logic of this one;
489            # just stop counting this key as either active or published
490            if event.keyid in published:
491                published.remove(event.keyid)
492            if event.keyid in active:
493                active.remove(event.keyid)
494
495    return (active, published)
496
497############################################################################
498# check_events:
499############################################################################
500def check_events(eventsList, ksk):
501    """create lists of events happening at the same time, check for
502    inconsistancies"""
503    active = set()
504    published = set()
505    eventgroups = list()
506    eventgroup = list()
507    keytype = ("KSK" if ksk else "ZSK")
508
509    # collect up all events that have the same time
510    eventsfound = False
511    for event in eventsList:
512        # if checking ZSKs, skip KSKs, and vice versa
513        if (ksk and not event.sep) or (event.sep and not ksk):
514            continue
515
516        # we found an appropriate (ZSK or KSK event)
517        eventsfound = True
518
519        # add event to current eventgroup
520        if (not eventgroup or eventgroup[0].when == event.when):
521            eventgroup.append(event)
522
523        # if we're at the end of the list, we're done.  if
524        # we've found an event with a later time, start a new
525        # eventgroup
526        if (eventgroup[0].when != event.when):
527            eventgroups.append(eventgroup)
528            eventgroup = list()
529            eventgroup.append(event)
530
531    if eventgroup:
532        eventgroups.append(eventgroup)
533
534    for eventgroup in eventgroups:
535        if (args.checklimit and
536            calendar.timegm(eventgroup[0].when) > args.checklimit):
537            print("Ignoring events after %s" %
538                  time.strftime("%a %b %d %H:%M:%S UTC %Y",
539                      time.gmtime(args.checklimit)))
540            return True
541
542        (active, published) = \
543           process_events(eventgroup, active, published)
544
545        list_events(eventgroup)
546
547        # and then check for inconsistencies:
548        if len(active) == 0:
549            print ("ERROR: No %s's are active after this event" % keytype)
550            return False
551        elif len(published) == 0:
552            sys.stdout.write("ERROR: ")
553            print ("ERROR: No %s's are published after this event" % keytype)
554            return False
555        elif len(published.intersection(active)) == 0:
556            sys.stdout.write("ERROR: ")
557            print (("ERROR: No %s's are both active and published " +
558                    "after this event") % keytype)
559            return False
560
561    if not eventsfound:
562        print ("ERROR: No %s events found in '%s'" %
563               (keytype, args.path))
564        return False
565
566    return True
567
568############################################################################
569# check_zones:
570# ############################################################################
571def check_zones(eventsList):
572    """scan events per zone, algorithm, and key type, in order of occurrance,
573    noting inconsistent states when found"""
574    global foundprob
575
576    foundprob = False
577    zonesfound = False
578    for zone in eventsList:
579        if args.zone and zone != args.zone:
580            continue
581
582        zonesfound = True
583        for alg in eventsList[zone]:
584            if not args.no_ksk:
585                vspace()
586                print("Checking scheduled KSK events for zone %s, algorithm %s..." %
587                       (zone, algname(alg)))
588                if not check_events(eventsList[zone][alg], True):
589                    foundprob = True
590                else:
591                    print ("No errors found")
592
593            if not args.no_zsk:
594                vspace()
595                print("Checking scheduled ZSK events for zone %s, algorithm %s..." %
596                      (zone, algname(alg)))
597                if not check_events(eventsList[zone][alg], False):
598                    foundprob = True
599                else:
600                    print ("No errors found")
601
602    if not zonesfound:
603        print("ERROR: No key events found for %s in '%s'" %
604               (args.zone, args.path))
605        exit(1)
606
607############################################################################
608# fill_eventsList:
609############################################################################
610def fill_eventsList(eventsList):
611    """populate the list of events"""
612    for zone, algorithms in keyDict.items():
613        for alg, keys in  algorithms.items():
614            for keyid, keydata in keys.items():
615                if("Publish" in keydata.metadata):
616                    eventsList[zone][alg].append(Event("Publish", keydata))
617                if("Activate" in keydata.metadata):
618                    eventsList[zone][alg].append(Event("Activate", keydata))
619                if("Inactive" in keydata.metadata):
620                    eventsList[zone][alg].append(Event("Inactive", keydata))
621                if("Delete" in keydata.metadata):
622                    eventsList[zone][alg].append(Event("Delete", keydata))
623
624            eventsList[zone][alg] = sorted(eventsList[zone][alg],
625                                           key=lambda event: event.when)
626
627    foundprob = False
628    if not keyDict:
629        print("ERROR: No key events found in '%s'" % args.path)
630        exit(1)
631
632############################################################################
633# set_path:
634############################################################################
635def set_path(command, default=None):
636    """find the location of a specified command.  if a default is supplied
637    and it works, we use it; otherwise we search PATH for a match.  If
638    not found, error and exit"""
639    fpath = default
640    if not fpath or not os.path.isfile(fpath) or not os.access(fpath, os.X_OK):
641        path = os.environ["PATH"]
642        if not path:
643            path = os.path.defpath
644        for directory in path.split(os.pathsep):
645            fpath = directory + os.sep + command
646            if os.path.isfile(fpath) or os.access(fpath, os.X_OK):
647                break
648            fpath = None
649
650    return fpath
651
652############################################################################
653# parse_args:
654############################################################################
655def parse_args():
656    """Read command line arguments, set global 'args' structure"""
657    global args
658    compilezone = set_path('named-compilezone',
659                           os.path.join(prefix('bin'), 'named-compilezone'))
660
661    parser = argparse.ArgumentParser(description=prog + ': checks future ' +
662                                     'DNSKEY coverage for a zone')
663
664    parser.add_argument('zone', type=str, help='zone to check')
665    parser.add_argument('-K', dest='path', default='.', type=str,
666                        help='a directory containing keys to process',
667                        metavar='dir')
668    parser.add_argument('-f', dest='filename', type=str,
669                        help='zone master file', metavar='file')
670    parser.add_argument('-m', dest='maxttl', type=str,
671                        help='the longest TTL in the zone(s)',
672                        metavar='time')
673    parser.add_argument('-d', dest='keyttl', type=str,
674                        help='the DNSKEY TTL', metavar='time')
675    parser.add_argument('-r', dest='resign', default='1944000',
676                        type=int, help='the RRSIG refresh interval '
677                                       'in seconds [default: 22.5 days]',
678                        metavar='time')
679    parser.add_argument('-c', dest='compilezone',
680                        default=compilezone, type=str,
681                        help='path to \'named-compilezone\'',
682                        metavar='path')
683    parser.add_argument('-l', dest='checklimit',
684                        type=str, default='0',
685                        help='Length of time to check for '
686                             'DNSSEC coverage [default: 0 (unlimited)]',
687                        metavar='time')
688    parser.add_argument('-z', dest='no_ksk',
689                        action='store_true', default=False,
690                        help='Only check zone-signing keys (ZSKs)')
691    parser.add_argument('-k', dest='no_zsk',
692                        action='store_true', default=False,
693                        help='Only check key-signing keys (KSKs)')
694    parser.add_argument('-D', '--debug', dest='debug_mode',
695                        action='store_true', default=False,
696                        help='Turn on debugging output')
697    parser.add_argument('-v', '--version', action='version', version='9.9.1')
698
699    args = parser.parse_args()
700
701    if args.no_zsk and args.no_ksk:
702        print("ERROR: -z and -k cannot be used together.");
703        exit(1)
704
705    # convert from time arguments to seconds
706    try:
707        if args.maxttl:
708            m = parse_time(args.maxttl)
709            args.maxttl = m
710    except:
711        pass
712
713    try:
714        if args.keyttl:
715            k = parse_time(args.keyttl)
716            args.keyttl = k
717    except:
718        pass
719
720    try:
721        if args.resign:
722            r = parse_time(args.resign)
723            args.resign = r
724    except:
725        pass
726
727    try:
728        if args.checklimit:
729            lim = args.checklimit
730            r = parse_time(args.checklimit)
731            if r == 0:
732                args.checklimit = None
733            else:
734                args.checklimit = time.time() + r
735    except:
736        pass
737
738    # if we've got the values we need from the command line, stop now
739    if args.maxttl and args.keyttl:
740        return
741
742    # load keyttl and maxttl data from zonefile
743    if args.zone and args.filename:
744        try:
745            zone = Zone(args.zone)
746            zone.load(args.filename)
747            if not args.maxttl:
748                args.maxttl = zone.maxttl
749            if not args.keyttl:
750                args.keyttl = zone.maxttl
751        except Exception as e:
752            print("Unable to load zone data from %s: " % args.filename, e)
753
754    if not args.maxttl:
755        vspace()
756        print ("WARNING: Maximum TTL value was not specified.  Using 1 week\n"
757               "\t (604800 seconds); re-run with the -m option to get more\n"
758               "\t accurate results.")
759        args.maxttl = 604800
760
761############################################################################
762# Main
763############################################################################
764def main():
765    global keyDict
766
767    parse_args()
768    path=args.path
769
770    print ("PHASE 1--Loading keys to check for internal timing problems")
771    keyDict = defaultdict(lambda : defaultdict(dict))
772    files = glob.glob(os.path.join(path, '*.private'))
773    for infile in files:
774        key = Key(infile)
775        if args.zone and key.zone != args.zone:
776            continue
777        keyDict[key.zone][key.alg][key.keyid] = key
778        key.check_prepub()
779        if key.sep:
780            key.check_postpub()
781        else:
782            key.check_postpub(args.maxttl + args.resign)
783
784    vspace()
785    print ("PHASE 2--Scanning future key events for coverage failures")
786    vreset()
787
788    eventsList = defaultdict(lambda : defaultdict(list))
789    fill_eventsList(eventsList)
790    check_zones(eventsList)
791
792    if foundprob:
793        exit(1)
794    else:
795        exit(0)
796
797if __name__ == "__main__":
798    main()
799