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