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