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