2 ############################################################################
3 # Copyright (C) 2013, 2014 Internet Systems Consortium, Inc. ("ISC")
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.
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 ############################################################################
25 from collections
import defaultdict
28 prog
='dnssec-coverage'
30 # These routines permit platform-independent location of BIND 9 tools
35 def prefix(bindir
= ''):
37 return os
.path
.join('@prefix@', bindir
)
39 bind_subkey
= "Software\\ISC\\BIND"
43 hKey
= win32api
.RegOpenKeyEx(win32con
.HKEY_LOCAL_MACHINE
, bind_subkey
)
48 (namedBase
, _
) = win32api
.RegQueryValueEx(hKey
, "InstallDir")
51 win32api
.RegCloseKey(hKey
)
53 return os
.path
.join(namedBase
, bindir
)
54 return os
.path
.join(win32api
.GetSystemDirectory(), bindir
)
56 ########################################################################
58 ########################################################################
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."""
64 def __init__(self
, _what
, _key
):
67 self
.when
= _key
.metadata
[_what
]
69 self
.keyid
= _key
.keyid
75 return repr((self
.when
, self
.what
, self
.keyid
, self
.sep
,
79 return time
.strftime("%a %b %d %H:%M:%S UTC %Y", self
.when
)
82 return self
.key
.showkey()
84 def showkeytype(self
):
85 return self
.key
.showkeytype()
87 ########################################################################
89 ########################################################################
91 """An individual DNSSEC key. Identified by path, zone, algorithm, keyid.
92 Contains a dictionary of metadata events."""
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"
103 self
.zone
= zone
[1:-1]
105 self
.keyid
= int(keyid
)
107 kfp
= open(key_file
, "r")
111 tokens
= line
.split()
115 if tokens
[1].lower() in ('in', 'ch', 'hs'):
117 self
.ttl
= args
.keyttl
120 print("WARNING: Unable to determine TTL for DNSKEY %s." %
122 print("\t Using 1 day (86400 seconds); re-run with the -d "
123 "option for more\n\t accurate results.")
127 self
.ttl
= int(tokens
[1]) if not args
.keyttl
else args
.keyttl
129 if (int(tokens
[septoken
]) & 0x1) == 1:
135 pfp
= open(private_file
, "rU")
138 propDef
= propLine
.strip()
139 if len(propDef
) == 0:
141 if propDef
[0] in ('!', '#'):
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
149 if("Publish" in propDict
):
150 propDict
["Publish"] = time
.strptime(propDict
["Publish"],
153 if("Activate" in propDict
):
154 propDict
["Activate"] = time
.strptime(propDict
["Activate"],
157 if("Inactive" in propDict
):
158 propDict
["Inactive"] = time
.strptime(propDict
["Inactive"],
161 if("Delete" in propDict
):
162 propDict
["Delete"] = time
.strptime(propDict
["Delete"],
165 if("Revoke" in propDict
):
166 propDict
["Revoke"] = time
.strptime(propDict
["Revoke"],
169 self
.metadata
= propDict
172 return "%s/%03d/%05d" % (self
.zone
, self
.alg
, self
.keyid
);
174 def showkeytype(self
):
175 return ("KSK" if self
.sep
else "ZSK")
177 # ensure that the gap between Publish and Activate is big enough
178 def check_prepub(self
):
181 if (not "Activate" in self
.metadata
):
182 debug_print("No Activate information in key: %s" % self
.showkey())
184 a
= calendar
.timegm(self
.metadata
["Activate"])
186 if (not "Publish" in self
.metadata
):
187 debug_print("No Publish information in key: %s" % self
.showkey())
190 print("WARNING: Key %s (%s) is scheduled for activation but \n"
191 "\t not for publication." %
192 (self
.showkey(), self
.showkeytype()))
194 p
= calendar
.timegm(self
.metadata
["Publish"])
197 if p
< now
and a
< now
:
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
))
212 print("WARNING: Key %s (%s) is active before it is published" %
213 (self
.showkey(), self
.showkeytype()))
216 if (a
- p
< self
.ttl
):
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." %
228 # ensure that the gap between Inactive and Delete is big enough
229 def check_postpub(self
, timespan
= None):
235 if (not "Delete" in self
.metadata
):
236 debug_print("No Delete information in key: %s" % self
.showkey())
238 d
= calendar
.timegm(self
.metadata
["Delete"])
240 if (not "Inactive" in self
.metadata
):
241 debug_print("No Inactive information in key: %s" % self
.showkey())
244 print("WARNING: Key %s (%s) is scheduled for deletion but\n"
245 "\t not for inactivation." %
246 (self
.showkey(), self
.showkeytype()))
248 i
= calendar
.timegm(self
.metadata
["Inactive"])
250 if d
< now
and i
< now
:
255 print("WARNING: Key %s (%s) is scheduled for deletion before\n"
256 "\t inactivation." % (self
.showkey(), self
.showkeytype()))
259 if (d
- i
< timespan
):
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." %
271 ########################################################################
273 ########################################################################
275 """Stores data about a specific zone"""
277 def __init__(self
, _name
, _keyttl
= None, _maxttl
= None):
279 self
.keyttl
= _keyttl
280 self
.maxttl
= _maxttl
282 def load(self
, filename
):
283 if not args
.compilezone
:
284 sys
.stderr
.write(prog
+ ': FATAL: "named-compilezone" not found\n')
290 maxttl
= keyttl
= None
292 fp
= os
.popen("%s -o - %s %s 2> /dev/null" %
293 (args
.compilezone
, self
.name
, filename
))
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])
305 ############################################################################
307 ############################################################################
308 def debug_print(debugVar
):
309 """pretty print a variable iff debug mode is enabled"""
310 if not args
.debug_mode
:
312 if type(debugVar
) == str:
313 print("DEBUG: " + debugVar
)
315 print("DEBUG: " + pprint
.pformat(debugVar
))
318 ############################################################################
320 ############################################################################
323 """adds vertical space between two sections of output text if and only
324 if this is *not* the first section being printed"""
331 ############################################################################
333 ############################################################################
335 """reset vertical spacing"""
339 ############################################################################
341 ############################################################################
342 def 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
349 return (bigunit
, secs
)
351 ############################################################################
353 ############################################################################
354 def addtime(output
, unit
, t
):
355 """add a formatted unit of time to an accumulating string"""
357 output
+= ("%s%d %s%s" %
358 ((", " if output
else ""),
359 t
, unit
, ("s" if t
> 1 else "")))
363 ############################################################################
365 ############################################################################
367 """given a length of time in seconds, print a formatted human duration
368 in larger units of time
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
)
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
)
393 ############################################################################
395 ############################################################################
397 """convert a formatted time (e.g., 1y, 6mo, 15mi, etc) into seconds"""
400 # if s is an integer, we're done already
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]*)')
411 raise Exception("Cannot parse %s" % s
)
412 (n
, unit
) = m
.groups()
417 elif unit
[0] == 'm' and unit
[1] == 'o':
425 elif unit
[0] == 'm' and unit
[1] == 'i':
430 raise Exception("Invalid suffix %s" % unit
)
432 ############################################################################
434 ############################################################################
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',
442 if alg
in range(len(names
)):
444 return (name
if name
else str(alg
))
446 ############################################################################
448 ############################################################################
449 def list_events(eventgroup
):
450 """print a list of the events in an eventgroup"""
453 print (" " + eventgroup
[0].showtime() + ":")
454 for event
in eventgroup
:
455 print (" %s: %s (%s)" %
456 (event
.what
, event
.showkey(), event
.showkeytype()))
458 ############################################################################
460 ############################################################################
461 def 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
:
474 print ("\tWARNING: %s (%s) scheduled to become inactive "
475 "before it is active" %
476 (event
.showkey(), event
.showkeytype()))
478 active
.remove(event
.keyid
)
479 elif event
.what
== "Delete":
480 if event
.keyid
in published
:
481 published
.remove(event
.keyid
)
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
)
495 return (active
, published
)
497 ############################################################################
499 ############################################################################
500 def check_events(eventsList
, ksk
):
501 """create lists of events happening at the same time, check for
507 keytype
= ("KSK" if ksk
else "ZSK")
509 # collect up all events that have the same time
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
):
516 # we found an appropriate (ZSK or KSK event)
519 # add event to current eventgroup
520 if (not eventgroup
or eventgroup
[0].when
== event
.when
):
521 eventgroup
.append(event
)
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
526 if (eventgroup
[0].when
!= event
.when
):
527 eventgroups
.append(eventgroup
)
529 eventgroup
.append(event
)
532 eventgroups
.append(eventgroup
)
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
)))
542 (active
, published
) = \
543 process_events(eventgroup
, active
, published
)
545 list_events(eventgroup
)
547 # and then check for inconsistencies:
549 print ("ERROR: No %s's are active after this event" % keytype
)
551 elif len(published
) == 0:
552 sys
.stdout
.write("ERROR: ")
553 print ("ERROR: No %s's are published after this event" % keytype
)
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
)
562 print ("ERROR: No %s events found in '%s'" %
563 (keytype
, args
.path
))
568 ############################################################################
570 # ############################################################################
571 def check_zones(eventsList
):
572 """scan events per zone, algorithm, and key type, in order of occurrance,
573 noting inconsistent states when found"""
578 for zone
in eventsList
:
579 if args
.zone
and zone
!= args
.zone
:
583 for alg
in eventsList
[zone
]:
586 print("Checking scheduled KSK events for zone %s, algorithm %s..." %
587 (zone
, algname(alg
)))
588 if not check_events(eventsList
[zone
][alg
], True):
591 print ("No errors found")
595 print("Checking scheduled ZSK events for zone %s, algorithm %s..." %
596 (zone
, algname(alg
)))
597 if not check_events(eventsList
[zone
][alg
], False):
600 print ("No errors found")
603 print("ERROR: No key events found for %s in '%s'" %
604 (args
.zone
, args
.path
))
607 ############################################################################
609 ############################################################################
610 def 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
))
624 eventsList
[zone
][alg
] = sorted(eventsList
[zone
][alg
],
625 key
=lambda event
: event
.when
)
629 print("ERROR: No key events found in '%s'" % args
.path
)
632 ############################################################################
634 ############################################################################
635 def 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"""
640 if not fpath
or not os
.path
.isfile(fpath
) or not os
.access(fpath
, os
.X_OK
):
641 path
= os
.environ
["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
):
652 ############################################################################
654 ############################################################################
656 """Read command line arguments, set global 'args' structure"""
658 compilezone
= set_path('named-compilezone',
659 os
.path
.join(prefix('bin'), 'named-compilezone'))
661 parser
= argparse
.ArgumentParser(description
=prog
+ ': checks future ' +
662 'DNSKEY coverage for a zone')
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',
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)',
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]',
679 parser
.add_argument('-c', dest
='compilezone',
680 default
=compilezone
, type=str,
681 help='path to \'named-compilezone\'',
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)]',
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')
699 args
= parser
.parse_args()
701 if args
.no_zsk
and args
.no_ksk
:
702 print("ERROR: -z and -k cannot be used together.");
705 # convert from time arguments to seconds
708 m
= parse_time(args
.maxttl
)
715 k
= parse_time(args
.keyttl
)
722 r
= parse_time(args
.resign
)
729 lim
= args
.checklimit
730 r
= parse_time(args
.checklimit
)
732 args
.checklimit
= None
734 args
.checklimit
= time
.time() + r
738 # if we've got the values we need from the command line, stop now
739 if args
.maxttl
and args
.keyttl
:
742 # load keyttl and maxttl data from zonefile
743 if args
.zone
and args
.filename
:
745 zone
= Zone(args
.zone
)
746 zone
.load(args
.filename
)
748 args
.maxttl
= zone
.maxttl
750 args
.keyttl
= zone
.maxttl
751 except Exception as e
:
752 print("Unable to load zone data from %s: " % args
.filename
, e
)
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.")
761 ############################################################################
763 ############################################################################
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'))
775 if args
.zone
and key
.zone
!= args
.zone
:
777 keyDict
[key
.zone
][key
.alg
][key
.keyid
] = key
782 key
.check_postpub(args
.maxttl
+ args
.resign
)
785 print ("PHASE 2--Scanning future key events for coverage failures")
788 eventsList
= defaultdict(lambda : defaultdict(list))
789 fill_eventsList(eventsList
)
790 check_zones(eventsList
)
797 if __name__
== "__main__":