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 https://mozilla.org/MPL/2.0/. 7# 8# See the COPYRIGHT file distributed with this work for additional 9# information regarding copyright ownership. 10############################################################################ 11 12import argparse 13import os 14import sys 15from subprocess import Popen, PIPE 16 17from isc.utils import prefix,version 18 19prog = 'dnssec-checkds' 20 21 22############################################################################ 23# SECRR class: 24# Class for DS/DLV resource record 25############################################################################ 26class SECRR: 27 hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384'} 28 rrname = '' 29 rrclass = 'IN' 30 keyid = None 31 keyalg = None 32 hashalg = None 33 digest = '' 34 ttl = 0 35 36 def __init__(self, rrtext, dlvname = None): 37 if not rrtext: 38 raise Exception 39 40 # 'str' does not have decode method in python3 41 if type(rrtext) is not str: 42 fields = rrtext.decode('ascii').split() 43 else: 44 fields = rrtext.split() 45 if len(fields) < 7: 46 raise Exception 47 48 if dlvname: 49 self.rrtype = "DLV" 50 self.dlvname = dlvname.lower() 51 parent = fields[0].lower().strip('.').split('.') 52 parent.reverse() 53 dlv = dlvname.split('.') 54 dlv.reverse() 55 while len(dlv) != 0 and len(parent) != 0 and parent[0] == dlv[0]: 56 parent = parent[1:] 57 dlv = dlv[1:] 58 if dlv: 59 raise Exception 60 parent.reverse() 61 self.parent = '.'.join(parent) 62 self.rrname = self.parent + '.' + self.dlvname + '.' 63 else: 64 self.rrtype = "DS" 65 self.rrname = fields[0].lower() 66 67 fields = fields[1:] 68 if fields[0].upper() in ['IN', 'CH', 'HS']: 69 self.rrclass = fields[0].upper() 70 fields = fields[1:] 71 else: 72 self.ttl = int(fields[0]) 73 self.rrclass = fields[1].upper() 74 fields = fields[2:] 75 76 if fields[0].upper() != self.rrtype: 77 raise Exception('%s does not match %s' % 78 (fields[0].upper(), self.rrtype)) 79 80 self.keyid, self.keyalg, self.hashalg = map(int, fields[1:4]) 81 self.digest = ''.join(fields[4:]).upper() 82 83 def __repr__(self): 84 return '%s %s %s %d %d %d %s' % \ 85 (self.rrname, self.rrclass, self.rrtype, 86 self.keyid, self.keyalg, self.hashalg, self.digest) 87 88 def __eq__(self, other): 89 return self.__repr__() == other.__repr__() 90 91 92############################################################################ 93# check: 94# Fetch DS/DLV RRset for the given zone from the DNS; fetch DNSKEY 95# RRset from the masterfile if specified, or from DNS if not. 96# Generate a set of expected DS/DLV records from the DNSKEY RRset, 97# and report on congruency. 98############################################################################ 99def check(zone, args, masterfile=None, lookaside=None): 100 rrlist = [] 101 cmd = [args.dig, "+noall", "+answer", "-t", "dlv" if lookaside else "ds", 102 "-q", zone + "." + lookaside if lookaside else zone] 103 fp, _ = Popen(cmd, stdout=PIPE).communicate() 104 105 for line in fp.splitlines(): 106 if type(line) is not str: 107 line = line.decode('ascii') 108 rrlist.append(SECRR(line, lookaside)) 109 rrlist = sorted(rrlist, key=lambda rr: (rr.keyid, rr.keyalg, rr.hashalg)) 110 111 klist = [] 112 113 if masterfile: 114 cmd = [args.dsfromkey, "-f", masterfile] 115 if lookaside: 116 cmd += ["-l", lookaside] 117 cmd.append(zone) 118 fp, _ = Popen(cmd, stdout=PIPE).communicate() 119 else: 120 intods, _ = Popen([args.dig, "+noall", "+answer", "-t", "dnskey", 121 "-q", zone], stdout=PIPE).communicate() 122 cmd = [args.dsfromkey, "-f", "-"] 123 if lookaside: 124 cmd += ["-l", lookaside] 125 cmd.append(zone) 126 fp, _ = Popen(cmd, stdin=PIPE, stdout=PIPE).communicate(intods) 127 128 for line in fp.splitlines(): 129 if type(line) is not str: 130 line = line.decode('ascii') 131 klist.append(SECRR(line, lookaside)) 132 133 if len(klist) < 1: 134 print("No DNSKEY records found in zone apex") 135 return False 136 137 found = False 138 for rr in klist: 139 if rr in rrlist: 140 print("%s for KSK %s/%03d/%05d (%s) found in parent" % 141 (rr.rrtype, rr.rrname.strip('.'), rr.keyalg, 142 rr.keyid, SECRR.hashalgs[rr.hashalg])) 143 found = True 144 else: 145 print("%s for KSK %s/%03d/%05d (%s) missing from parent" % 146 (rr.rrtype, rr.rrname.strip('.'), rr.keyalg, 147 rr.keyid, SECRR.hashalgs[rr.hashalg])) 148 149 if not found: 150 print("No %s records were found for any DNSKEY" % ("DLV" if lookaside else "DS")) 151 152 return found 153 154############################################################################ 155# parse_args: 156# Read command line arguments, set global 'args' structure 157############################################################################ 158def parse_args(): 159 parser = argparse.ArgumentParser(description=prog + ': checks DS coverage') 160 161 bindir = 'bin' 162 sbindir = 'bin' if os.name == 'nt' else 'sbin' 163 164 parser.add_argument('zone', type=str, help='zone to check') 165 parser.add_argument('-f', '--file', dest='masterfile', type=str, 166 help='zone master file') 167 parser.add_argument('-l', '--lookaside', dest='lookaside', type=str, 168 help='DLV lookaside zone') 169 parser.add_argument('-d', '--dig', dest='dig', 170 default=os.path.join(prefix(bindir), 'dig'), 171 type=str, help='path to \'dig\'') 172 parser.add_argument('-D', '--dsfromkey', dest='dsfromkey', 173 default=os.path.join(prefix(sbindir), 174 'dnssec-dsfromkey'), 175 type=str, help='path to \'dnssec-dsfromkey\'') 176 parser.add_argument('-v', '--version', action='version', 177 version=version) 178 args = parser.parse_args() 179 180 args.zone = args.zone.strip('.') 181 if args.lookaside: 182 args.lookaside = args.lookaside.strip('.') 183 184 return args 185 186 187############################################################################ 188# Main 189############################################################################ 190def main(): 191 args = parse_args() 192 found = check(args.zone, args, args.masterfile, args.lookaside) 193 exit(0 if found else 1) 194