2016-04-28 00:12:33 -07:00
|
|
|
############################################################################
|
2018-02-23 09:53:12 +01:00
|
|
|
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
2016-04-28 00:12:33 -07:00
|
|
|
#
|
2016-06-27 14:56:38 +10:00
|
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
2018-02-23 09:53:12 +01:00
|
|
|
#
|
|
|
|
# See the COPYRIGHT file distributed with this work for additional
|
|
|
|
# information regarding copyright ownership.
|
2016-04-28 00:12:33 -07:00
|
|
|
############################################################################
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
from subprocess import Popen, PIPE
|
|
|
|
|
|
|
|
from isc.utils import prefix,version
|
|
|
|
|
|
|
|
prog = 'dnssec-checkds'
|
|
|
|
|
|
|
|
|
|
|
|
############################################################################
|
|
|
|
# SECRR class:
|
2019-08-07 12:19:19 -07:00
|
|
|
# Class for DS resource record
|
2016-04-28 00:12:33 -07:00
|
|
|
############################################################################
|
|
|
|
class SECRR:
|
|
|
|
hashalgs = {1: 'SHA-1', 2: 'SHA-256', 3: 'GOST', 4: 'SHA-384'}
|
|
|
|
rrname = ''
|
|
|
|
rrclass = 'IN'
|
|
|
|
keyid = None
|
|
|
|
keyalg = None
|
|
|
|
hashalg = None
|
|
|
|
digest = ''
|
|
|
|
ttl = 0
|
|
|
|
|
2019-08-07 12:19:19 -07:00
|
|
|
def __init__(self, rrtext):
|
2016-04-28 00:12:33 -07:00
|
|
|
if not rrtext:
|
|
|
|
raise Exception
|
|
|
|
|
2017-10-26 21:05:11 -07:00
|
|
|
# 'str' does not have decode method in python3
|
|
|
|
if type(rrtext) is not str:
|
|
|
|
fields = rrtext.decode('ascii').split()
|
|
|
|
else:
|
|
|
|
fields = rrtext.split()
|
2016-04-28 00:12:33 -07:00
|
|
|
if len(fields) < 7:
|
|
|
|
raise Exception
|
|
|
|
|
2019-08-07 12:19:19 -07:00
|
|
|
self.rrtype = "DS"
|
|
|
|
self.rrname = fields[0].lower()
|
2016-04-28 00:12:33 -07:00
|
|
|
|
|
|
|
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() != self.rrtype:
|
2016-04-29 14:17:21 -07:00
|
|
|
raise Exception('%s does not match %s' %
|
|
|
|
(fields[0].upper(), self.rrtype))
|
2016-04-28 00:12:33 -07:00
|
|
|
|
|
|
|
self.keyid, self.keyalg, self.hashalg = map(int, fields[1:4])
|
|
|
|
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__()
|
|
|
|
|
|
|
|
|
|
|
|
############################################################################
|
|
|
|
# check:
|
2019-08-07 12:19:19 -07:00
|
|
|
# Fetch DS RRset for the given zone from the DNS; fetch DNSKEY
|
2016-04-28 00:12:33 -07:00
|
|
|
# RRset from the masterfile if specified, or from DNS if not.
|
2019-08-07 12:19:19 -07:00
|
|
|
# Generate a set of expected DS records from the DNSKEY RRset,
|
2016-04-28 00:12:33 -07:00
|
|
|
# and report on congruency.
|
|
|
|
############################################################################
|
2017-10-26 21:05:11 -07:00
|
|
|
def check(zone, args):
|
2016-04-28 00:12:33 -07:00
|
|
|
rrlist = []
|
2017-10-26 21:05:11 -07:00
|
|
|
if args.dssetfile:
|
|
|
|
fp = open(args.dssetfile).read()
|
|
|
|
else:
|
2019-08-07 12:19:19 -07:00
|
|
|
cmd = [args.dig, "+noall", "+answer", "-t", "ds", "-q", zone]
|
2017-10-26 21:05:11 -07:00
|
|
|
fp, _ = Popen(cmd, stdout=PIPE).communicate()
|
2016-04-28 00:12:33 -07:00
|
|
|
|
|
|
|
for line in fp.splitlines():
|
2019-02-18 16:36:59 +11:00
|
|
|
if type(line) is not str:
|
|
|
|
line = line.decode('ascii')
|
2019-08-07 12:19:19 -07:00
|
|
|
rrlist.append(SECRR(line))
|
2016-04-28 00:12:33 -07:00
|
|
|
rrlist = sorted(rrlist, key=lambda rr: (rr.keyid, rr.keyalg, rr.hashalg))
|
|
|
|
|
|
|
|
klist = []
|
|
|
|
|
2019-02-04 13:46:51 +00:00
|
|
|
cmd = [args.dsfromkey]
|
|
|
|
for algo in args.algo:
|
|
|
|
cmd += ['-a', algo]
|
|
|
|
|
2017-10-26 21:05:11 -07:00
|
|
|
if args.masterfile:
|
2019-02-04 13:46:51 +00:00
|
|
|
cmd += ["-f", args.masterfile, zone]
|
2016-04-28 00:12:33 -07:00
|
|
|
fp, _ = Popen(cmd, stdout=PIPE).communicate()
|
|
|
|
else:
|
|
|
|
intods, _ = Popen([args.dig, "+noall", "+answer", "-t", "dnskey",
|
|
|
|
"-q", zone], stdout=PIPE).communicate()
|
2019-02-04 13:46:51 +00:00
|
|
|
cmd += ["-f", "-", zone]
|
2016-04-28 00:12:33 -07:00
|
|
|
fp, _ = Popen(cmd, stdin=PIPE, stdout=PIPE).communicate(intods)
|
|
|
|
|
|
|
|
for line in fp.splitlines():
|
2019-02-18 16:36:59 +11:00
|
|
|
if type(line) is not str:
|
|
|
|
line = line.decode('ascii')
|
2019-08-07 12:19:19 -07:00
|
|
|
klist.append(SECRR(line))
|
2016-04-28 00:12:33 -07:00
|
|
|
|
|
|
|
if len(klist) < 1:
|
2016-05-25 13:41:48 +10:00
|
|
|
print("No DNSKEY records found in zone apex")
|
2016-04-28 00:12:33 -07:00
|
|
|
return False
|
|
|
|
|
2019-02-04 13:46:51 +00:00
|
|
|
match = True
|
|
|
|
for rr in rrlist:
|
|
|
|
if rr not in klist:
|
|
|
|
print("KSK for %s %s/%03d/%05d (%s) missing from child" %
|
2016-05-25 13:41:48 +10:00
|
|
|
(rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
|
|
|
|
rr.keyid, SECRR.hashalgs[rr.hashalg]))
|
2019-02-04 13:46:51 +00:00
|
|
|
match = False
|
|
|
|
for rr in klist:
|
|
|
|
if rr not in rrlist:
|
2016-05-25 13:41:48 +10:00
|
|
|
print("%s for KSK %s/%03d/%05d (%s) missing from parent" %
|
|
|
|
(rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
|
|
|
|
rr.keyid, SECRR.hashalgs[rr.hashalg]))
|
2019-02-04 13:46:51 +00:00
|
|
|
match = False
|
|
|
|
for rr in klist:
|
|
|
|
if rr in rrlist:
|
|
|
|
print("%s for KSK %s/%03d/%05d (%s) found in parent" %
|
|
|
|
(rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
|
|
|
|
rr.keyid, SECRR.hashalgs[rr.hashalg]))
|
2016-04-28 00:12:33 -07:00
|
|
|
|
2019-02-04 13:46:51 +00:00
|
|
|
return match
|
2016-04-28 00:12:33 -07:00
|
|
|
|
|
|
|
|
|
|
|
############################################################################
|
|
|
|
# parse_args:
|
|
|
|
# Read command line arguments, set global 'args' structure
|
|
|
|
############################################################################
|
|
|
|
def parse_args():
|
|
|
|
parser = argparse.ArgumentParser(description=prog + ': checks DS coverage')
|
|
|
|
|
|
|
|
bindir = 'bin'
|
|
|
|
sbindir = 'bin' if os.name == 'nt' else 'sbin'
|
|
|
|
|
|
|
|
parser.add_argument('zone', type=str, help='zone to check')
|
2019-02-04 13:46:51 +00:00
|
|
|
parser.add_argument('-a', '--algo', dest='algo', action='append',
|
|
|
|
default=[], type=str, help='DS digest algorithm')
|
2016-04-28 00:12:33 -07:00
|
|
|
parser.add_argument('-d', '--dig', dest='dig',
|
|
|
|
default=os.path.join(prefix(bindir), 'dig'),
|
|
|
|
type=str, help='path to \'dig\'')
|
|
|
|
parser.add_argument('-D', '--dsfromkey', dest='dsfromkey',
|
|
|
|
default=os.path.join(prefix(sbindir),
|
|
|
|
'dnssec-dsfromkey'),
|
2019-02-14 15:23:26 +01:00
|
|
|
type=str, help='path to \'dnssec-dsfromkey\'')
|
2017-10-26 21:05:11 -07:00
|
|
|
parser.add_argument('-f', '--file', dest='masterfile', type=str,
|
|
|
|
help='zone master file')
|
|
|
|
parser.add_argument('-s', '--dsset', dest='dssetfile', type=str,
|
|
|
|
help='prepared DSset file')
|
2016-04-28 00:12:33 -07:00
|
|
|
parser.add_argument('-v', '--version', action='version',
|
|
|
|
version=version)
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
args.zone = args.zone.strip('.')
|
|
|
|
|
|
|
|
return args
|
|
|
|
|
|
|
|
|
|
|
|
############################################################################
|
|
|
|
# Main
|
|
|
|
############################################################################
|
|
|
|
def main():
|
|
|
|
args = parse_args()
|
2019-02-04 13:46:51 +00:00
|
|
|
match = check(args.zone, args)
|
|
|
|
exit(0 if match else 1)
|