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