dnssec-checkds.py.in revision 153dce262a0532b45cd08592f367e153ac92ef36
#!@PYTHON@
############################################################################
# Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
############################################################################
import argparse
import pprint
import os
############################################################################
# DSRR class:
# Delegation Signer (DS) resource record
############################################################################
class DSRR:
hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST'}
rrname=''
rrclass='IN'
rrtype='DS'
keyid=None
keyalg=None
hashalg=None
digest=''
ttl=0
def __init__(self, rrtext):
if not rrtext:
return
fields = rrtext.split()
if len(fields) < 7:
return
self.rrname = fields[0].lower()
fields = fields[1:]
if fields[0].upper() in ['IN','CH','HS']:
self.rrclass = fields[0].upper()
fields = fields[1:]
else:
self.ttl = int(fields[0])
self.rrclass = fields[1].upper()
fields = fields[2:]
if fields[0].upper() != 'DS':
raise Exception
self.rrtype = 'DS'
self.keyid = int(fields[1])
self.keyalg = int(fields[2])
self.hashalg = int(fields[3])
self.digest = ''.join(fields[4:]).upper()
def __repr__(self):
return('%s %s %s %d %d %d %s' %
(self.rrname, self.rrclass, self.rrtype, self.keyid,
self.keyalg, self.hashalg, self.digest))
def __eq__(self, other):
return self.__repr__() == other.__repr__()
############################################################################
# DLVRR class:
# DNSSEC Lookaside Validation (DLV) resource record
############################################################################
class DLVRR:
hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST'}
parent=''
dlvname=''
rrname='IN'
rrclass='IN'
rrtype='DLV'
keyid=None
keyalg=None
hashalg=None
digest=''
ttl=0
def __init__(self, rrtext, dlvname):
if not rrtext:
return
fields = rrtext.split()
if len(fields) < 7:
return
self.dlvname = dlvname.lower()
parent = fields[0].lower().strip('.').split('.')
parent.reverse()
dlv = dlvname.split('.')
dlv.reverse()
while len(dlv) != 0 and len(parent) != 0 and parent[0] == dlv[0]:
parent = parent[1:]
dlv = dlv[1:]
if len(dlv) != 0:
raise Exception
parent.reverse()
self.parent = '.'.join(parent)
self.rrname = self.parent + '.' + self.dlvname + '.'
fields = fields[1:]
if fields[0].upper() in ['IN','CH','HS']:
self.rrclass = fields[0].upper()
fields = fields[1:]
else:
self.ttl = int(fields[0])
self.rrclass = fields[1].upper()
fields = fields[2:]
if fields[0].upper() != 'DLV':
raise Exception
self.rrtype = 'DLV'
self.keyid = int(fields[1])
self.keyalg = int(fields[2])
self.hashalg = int(fields[3])
self.digest = ''.join(fields[4:]).upper()
def __repr__(self):
return('%s %s %s %d %d %d %s' %
(self.rrname, self.rrclass, self.rrtype,
self.keyid, self.keyalg, self.hashalg, self.digest))
def __eq__(self, other):
return self.__repr__() == other.__repr__()
############################################################################
# checkds:
# Fetch DS RRset for the given zone from the DNS; fetch DNSKEY
# RRset from the masterfile if specified, or from DNS if not.
# Generate a set of expected DS records from the DNSKEY RRset,
# and report on congruency.
############################################################################
def checkds(zone, masterfile = None):
dslist=[]
fp=os.popen("%s +noall +answer -t ds %s" % (args.dig, zone))
for line in fp:
dslist.append(DSRR(line))
dslist = sorted(dslist, key=lambda ds: (ds.keyid, ds.keyalg, ds.hashalg))
fp.close()
dsklist=[]
if masterfile:
fp = os.popen("%s -f %s %s " %
(args.dsfromkey, masterfile, zone))
else:
fp = os.popen("%s +noall +answer -t dnskey %s | %s -f - %s" %
(args.dig, zone, args.dsfromkey, zone))
for line in fp:
dsklist.append(DSRR(line))
fp.close()
found = False
for ds in dsklist:
if ds in dslist:
print ("DS for KSK %s/%03d/%05d (%s) found in parent" %
(ds.rrname.strip('.'), ds.keyalg,
ds.keyid, DSRR.hashalgs[ds.hashalg]))
found = True
else:
print ("No DS records found for KSK %s/%03d/%05d" %
(ds.rrname, ds.keyalg, ds.keyid))
return found
############################################################################
# checkdlv:
# Fetch DLV RRset for the given zone from the DNS; fetch DNSKEY
# RRset from the masterfile if specified, or from DNS if not.
# Generate a set of expected DLV records from the DNSKEY RRset,
# and report on congruency.
############################################################################
def checkdlv(zone, lookaside, masterfile = None):
dlvlist=[]
fp=os.popen("%s +noall +answer -t dlv %s.%s" %
(args.dig, zone, lookaside))
for line in fp:
dlvlist.append(DLVRR(line, lookaside))
dlvlist = sorted(dlvlist,
key=lambda dlv: (dlv.keyid, dlv.keyalg, dlv.hashalg))
fp.close()
#
# Fetch DNSKEY records from DNS and generate DLV records from them
#
dlvklist=[]
if masterfile:
fp = os.popen("%s -f %s -l %s %s " %
(args.dsfromkey, masterfile, lookaside, zone))
else:
fp = os.popen("%s +noall +answer -t dnskey %s | %s -f - -l %s %s"
% (args.dig, zone, args.dsfromkey, lookaside, zone))
for line in fp:
dlvklist.append(DLVRR(line, lookaside))
fp.close()
found = False
for dlv in dlvklist:
if dlv in dlvlist:
print ("DLV for KSK %s/%03d/%05d (%s) found in %s" %
(dlv.parent, dlv.keyalg, dlv.keyid,
DLVRR.hashalgs[dlv.hashalg], dlv.dlvname))
found = True
else:
print ("No DLV records found for KSK %s/%03d/%05d in %s" %
(dlv.parent, dlv.keyalg, dlv.keyid, dlv.dlvname))
return found
############################################################################
# parse_args:
# Read command line arguments, set global 'args' structure
############################################################################
def parse_args():
global args
parser = argparse.ArgumentParser(description='checkds: checks DS coverage')
parser.add_argument('zone', type=str, help='zone to check')
parser.add_argument('-f', '--file', dest='masterfile', type=str,
help='zone master file')
parser.add_argument('-l', '--lookaside', dest='lookaside', type=str,
help='DLV lookaside zone')
parser.add_argument('-d', '--dig', dest='dig',
default='@prefix@/bin/dig', type=str,
help='path to \'dig\'')
parser.add_argument('-D', '--dsfromkey', dest='dsfromkey',
default='@prefix@/sbin/dnssec-dsfromkey', type=str,
help='path to \'dig\'')
parser.add_argument('-v', '--version', action='version', version='9.9.1')
args = parser.parse_args()
args.zone = args.zone.strip('.')
if args.lookaside:
lookaside = args.lookaside.strip('.')
############################################################################
# Main
############################################################################
def main():
parse_args()
if args.lookaside:
found = checkdlv(args.zone, args.lookaside, args.masterfile)
else:
found = checkds(args.zone, args.masterfile)
exit(0 if found else 1)
if __name__ == "__main__":
main()