1#!@PYTHON@ 2############################################################################ 3# Copyright (C) 2012-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 pprint 20import os 21 22prog='dnssec-checkds' 23 24# These routines permit platform-independent location of BIND 9 tools 25if os.name == 'nt': 26 import win32con 27 import win32api 28 29def prefix(bindir = ''): 30 if os.name != 'nt': 31 return os.path.join('@prefix@', bindir) 32 33 bind_subkey = "Software\\ISC\\BIND" 34 hKey = None 35 keyFound = True 36 try: 37 hKey = win32api.RegOpenKeyEx(win32con.HKEY_LOCAL_MACHINE, bind_subkey) 38 except: 39 keyFound = False 40 if keyFound: 41 try: 42 (namedBase, _) = win32api.RegQueryValueEx(hKey, "InstallDir") 43 except: 44 keyFound = False 45 win32api.RegCloseKey(hKey) 46 if keyFound: 47 return os.path.join(namedBase, bindir) 48 return os.path.join(win32api.GetSystemDirectory(), bindir) 49 50def shellquote(s): 51 if os.name == 'nt': 52 return '"' + s.replace('"', '"\\"') + '"' 53 return "'" + s.replace("'", "'\\''") + "'" 54 55############################################################################ 56# DSRR class: 57# Delegation Signer (DS) resource record 58############################################################################ 59class DSRR: 60 hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384' } 61 rrname='' 62 rrclass='IN' 63 rrtype='DS' 64 keyid=None 65 keyalg=None 66 hashalg=None 67 digest='' 68 ttl=0 69 70 def __init__(self, rrtext): 71 if not rrtext: 72 return 73 74 fields = rrtext.split() 75 if len(fields) < 7: 76 return 77 78 self.rrname = fields[0].lower() 79 fields = fields[1:] 80 if fields[0].upper() in ['IN','CH','HS']: 81 self.rrclass = fields[0].upper() 82 fields = fields[1:] 83 else: 84 self.ttl = int(fields[0]) 85 self.rrclass = fields[1].upper() 86 fields = fields[2:] 87 88 if fields[0].upper() != 'DS': 89 raise Exception 90 91 self.rrtype = 'DS' 92 self.keyid = int(fields[1]) 93 self.keyalg = int(fields[2]) 94 self.hashalg = int(fields[3]) 95 self.digest = ''.join(fields[4:]).upper() 96 97 def __repr__(self): 98 return('%s %s %s %d %d %d %s' % 99 (self.rrname, self.rrclass, self.rrtype, self.keyid, 100 self.keyalg, self.hashalg, self.digest)) 101 102 def __eq__(self, other): 103 return self.__repr__() == other.__repr__() 104 105############################################################################ 106# DLVRR class: 107# DNSSEC Lookaside Validation (DLV) resource record 108############################################################################ 109class DLVRR: 110 hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384' } 111 parent='' 112 dlvname='' 113 rrname='IN' 114 rrclass='IN' 115 rrtype='DLV' 116 keyid=None 117 keyalg=None 118 hashalg=None 119 digest='' 120 ttl=0 121 122 def __init__(self, rrtext, dlvname): 123 if not rrtext: 124 return 125 126 fields = rrtext.split() 127 if len(fields) < 7: 128 return 129 130 self.dlvname = dlvname.lower() 131 parent = fields[0].lower().strip('.').split('.') 132 parent.reverse() 133 dlv = dlvname.split('.') 134 dlv.reverse() 135 while len(dlv) != 0 and len(parent) != 0 and parent[0] == dlv[0]: 136 parent = parent[1:] 137 dlv = dlv[1:] 138 if len(dlv) != 0: 139 raise Exception 140 parent.reverse() 141 self.parent = '.'.join(parent) 142 self.rrname = self.parent + '.' + self.dlvname + '.' 143 144 fields = fields[1:] 145 if fields[0].upper() in ['IN','CH','HS']: 146 self.rrclass = fields[0].upper() 147 fields = fields[1:] 148 else: 149 self.ttl = int(fields[0]) 150 self.rrclass = fields[1].upper() 151 fields = fields[2:] 152 153 if fields[0].upper() != 'DLV': 154 raise Exception 155 156 self.rrtype = 'DLV' 157 self.keyid = int(fields[1]) 158 self.keyalg = int(fields[2]) 159 self.hashalg = int(fields[3]) 160 self.digest = ''.join(fields[4:]).upper() 161 162 def __repr__(self): 163 return('%s %s %s %d %d %d %s' % 164 (self.rrname, self.rrclass, self.rrtype, 165 self.keyid, self.keyalg, self.hashalg, self.digest)) 166 167 def __eq__(self, other): 168 return self.__repr__() == other.__repr__() 169 170############################################################################ 171# checkds: 172# Fetch DS RRset for the given zone from the DNS; fetch DNSKEY 173# RRset from the masterfile if specified, or from DNS if not. 174# Generate a set of expected DS records from the DNSKEY RRset, 175# and report on congruency. 176############################################################################ 177def checkds(zone, masterfile = None): 178 dslist=[] 179 fp=os.popen("%s +noall +answer -t ds -q %s" % 180 (shellquote(args.dig), shellquote(zone))) 181 for line in fp: 182 dslist.append(DSRR(line)) 183 dslist = sorted(dslist, key=lambda ds: (ds.keyid, ds.keyalg, ds.hashalg)) 184 fp.close() 185 186 dsklist=[] 187 188 if masterfile: 189 fp = os.popen("%s -f %s %s " % 190 (shellquote(args.dsfromkey), shellquote(masterfile), 191 shellquote(zone))) 192 else: 193 fp = os.popen("%s +noall +answer -t dnskey -q %s | %s -f - %s" % 194 (shellquote(args.dig), shellquote(zone), 195 shellquote(args.dsfromkey), shellquote(zone))) 196 197 for line in fp: 198 dsklist.append(DSRR(line)) 199 200 fp.close() 201 202 if (len(dsklist) < 1): 203 print ("No DNSKEY records found in zone apex") 204 return False 205 206 found = False 207 for ds in dsklist: 208 if ds in dslist: 209 print ("DS for KSK %s/%03d/%05d (%s) found in parent" % 210 (ds.rrname.strip('.'), ds.keyalg, 211 ds.keyid, DSRR.hashalgs[ds.hashalg])) 212 found = True 213 else: 214 print ("DS for KSK %s/%03d/%05d (%s) missing from parent" % 215 (ds.rrname.strip('.'), ds.keyalg, 216 ds.keyid, DSRR.hashalgs[ds.hashalg])) 217 218 if not found: 219 print ("No DS records were found for any DNSKEY") 220 221 return found 222 223############################################################################ 224# checkdlv: 225# Fetch DLV RRset for the given zone from the DNS; fetch DNSKEY 226# RRset from the masterfile if specified, or from DNS if not. 227# Generate a set of expected DLV records from the DNSKEY RRset, 228# and report on congruency. 229############################################################################ 230def checkdlv(zone, lookaside, masterfile = None): 231 dlvlist=[] 232 fp=os.popen("%s +noall +answer -t dlv -q %s" % 233 (shellquote(args.dig), shellquote(zone + '.' + lookaside))) 234 for line in fp: 235 dlvlist.append(DLVRR(line, lookaside)) 236 dlvlist = sorted(dlvlist, 237 key=lambda dlv: (dlv.keyid, dlv.keyalg, dlv.hashalg)) 238 fp.close() 239 240 # 241 # Fetch DNSKEY records from DNS and generate DLV records from them 242 # 243 dlvklist=[] 244 if masterfile: 245 fp = os.popen("%s -f %s -l %s %s " % 246 (args.dsfromkey, masterfile, lookaside, zone)) 247 else: 248 fp = os.popen("%s +noall +answer -t dnskey %s | %s -f - -l %s %s" 249 % (shellquote(args.dig), shellquote(zone), 250 shellquote(args.dsfromkey), shellquote(lookaside), 251 shellquote(zone))) 252 253 for line in fp: 254 dlvklist.append(DLVRR(line, lookaside)) 255 256 fp.close() 257 258 if (len(dlvklist) < 1): 259 print ("No DNSKEY records found in zone apex") 260 return False 261 262 found = False 263 for dlv in dlvklist: 264 if dlv in dlvlist: 265 print ("DLV for KSK %s/%03d/%05d (%s) found in %s" % 266 (dlv.parent, dlv.keyalg, dlv.keyid, 267 DLVRR.hashalgs[dlv.hashalg], dlv.dlvname)) 268 found = True 269 else: 270 print ("DLV for KSK %s/%03d/%05d (%s) missing from %s" % 271 (dlv.parent, dlv.keyalg, dlv.keyid, 272 DLVRR.hashalgs[dlv.hashalg], dlv.dlvname)) 273 274 if not found: 275 print ("No DLV records were found for any DNSKEY") 276 277 return found 278 279 280############################################################################ 281# parse_args: 282# Read command line arguments, set global 'args' structure 283############################################################################ 284def parse_args(): 285 global args 286 parser = argparse.ArgumentParser(description=prog + ': checks DS coverage') 287 288 bindir = 'bin' 289 if os.name == 'nt': 290 sbindir = 'bin' 291 else: 292 sbindir = 'sbin' 293 294 parser.add_argument('zone', type=str, help='zone to check') 295 parser.add_argument('-f', '--file', dest='masterfile', type=str, 296 help='zone master file') 297 parser.add_argument('-l', '--lookaside', dest='lookaside', type=str, 298 help='DLV lookaside zone') 299 parser.add_argument('-d', '--dig', dest='dig', 300 default=os.path.join(prefix(bindir), 'dig'), 301 type=str, help='path to \'dig\'') 302 parser.add_argument('-D', '--dsfromkey', dest='dsfromkey', 303 default=os.path.join(prefix(sbindir), 304 'dnssec-dsfromkey'), 305 type=str, help='path to \'dig\'') 306 parser.add_argument('-v', '--version', action='version', version='9.9.1') 307 args = parser.parse_args() 308 309 args.zone = args.zone.strip('.') 310 if args.lookaside: 311 lookaside = args.lookaside.strip('.') 312 313############################################################################ 314# Main 315############################################################################ 316def main(): 317 parse_args() 318 319 if args.lookaside: 320 found = checkdlv(args.zone, args.lookaside, args.masterfile) 321 else: 322 found = checkds(args.zone, args.masterfile) 323 324 exit(0 if found else 1) 325 326if __name__ == "__main__": 327 main() 328