mirror of
https://github.com/isc-projects/bind9.git
synced 2026-02-22 09:20:51 -05:00
This changes the behaviour so that it explicitly lists DS records that are present in the parent but do not have keys in the child. Any inconsistency is reported as an error, which is somewhat stricter than before. This is for conformance with the DS/CDS algorithm requirements in https://tools.ietf.org/html/draft-ietf-dnsop-algorithm-update
206 lines
7.3 KiB
Python
206 lines
7.3 KiB
Python
############################################################################
|
|
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
|
#
|
|
# 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/.
|
|
#
|
|
# See the COPYRIGHT file distributed with this work for additional
|
|
# information regarding copyright ownership.
|
|
############################################################################
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from subprocess import Popen, PIPE
|
|
|
|
from isc.utils import prefix,version
|
|
|
|
prog = 'dnssec-checkds'
|
|
|
|
|
|
############################################################################
|
|
# SECRR class:
|
|
# Class for DS/DLV resource record
|
|
############################################################################
|
|
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
|
|
|
|
def __init__(self, rrtext, dlvname = None):
|
|
if not rrtext:
|
|
raise Exception
|
|
|
|
# 'str' does not have decode method in python3
|
|
if type(rrtext) is not str:
|
|
fields = rrtext.decode('ascii').split()
|
|
else:
|
|
fields = rrtext.split()
|
|
if len(fields) < 7:
|
|
raise Exception
|
|
|
|
if dlvname:
|
|
self.rrtype = "DLV"
|
|
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 dlv:
|
|
raise Exception
|
|
parent.reverse()
|
|
self.parent = '.'.join(parent)
|
|
self.rrname = self.parent + '.' + self.dlvname + '.'
|
|
else:
|
|
self.rrtype = "DS"
|
|
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() != self.rrtype:
|
|
raise Exception('%s does not match %s' %
|
|
(fields[0].upper(), self.rrtype))
|
|
|
|
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:
|
|
# Fetch DS/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 DS/DLV records from the DNSKEY RRset,
|
|
# and report on congruency.
|
|
############################################################################
|
|
def check(zone, args):
|
|
rrlist = []
|
|
if args.dssetfile:
|
|
fp = open(args.dssetfile).read()
|
|
else:
|
|
cmd = [args.dig, "+noall", "+answer", "-t",
|
|
"dlv" if args.lookaside else "ds", "-q",
|
|
zone + "." + args.lookaside if args.lookaside else zone]
|
|
fp, _ = Popen(cmd, stdout=PIPE).communicate()
|
|
|
|
for line in fp.splitlines():
|
|
if type(line) is not str:
|
|
line = line.decode('ascii')
|
|
rrlist.append(SECRR(line, args.lookaside))
|
|
rrlist = sorted(rrlist, key=lambda rr: (rr.keyid, rr.keyalg, rr.hashalg))
|
|
|
|
klist = []
|
|
|
|
cmd = [args.dsfromkey]
|
|
for algo in args.algo:
|
|
cmd += ['-a', algo]
|
|
if args.lookaside:
|
|
cmd += ["-l", args.lookaside]
|
|
|
|
if args.masterfile:
|
|
cmd += ["-f", args.masterfile, zone]
|
|
fp, _ = Popen(cmd, stdout=PIPE).communicate()
|
|
else:
|
|
intods, _ = Popen([args.dig, "+noall", "+answer", "-t", "dnskey",
|
|
"-q", zone], stdout=PIPE).communicate()
|
|
cmd += ["-f", "-", zone]
|
|
fp, _ = Popen(cmd, stdin=PIPE, stdout=PIPE).communicate(intods)
|
|
|
|
for line in fp.splitlines():
|
|
if type(line) is not str:
|
|
line = line.decode('ascii')
|
|
klist.append(SECRR(line, args.lookaside))
|
|
|
|
if len(klist) < 1:
|
|
print("No DNSKEY records found in zone apex")
|
|
return False
|
|
|
|
match = True
|
|
for rr in rrlist:
|
|
if rr not in klist:
|
|
print("KSK for %s %s/%03d/%05d (%s) missing from child" %
|
|
(rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
|
|
rr.keyid, SECRR.hashalgs[rr.hashalg]))
|
|
match = False
|
|
for rr in klist:
|
|
if rr not in rrlist:
|
|
print("%s for KSK %s/%03d/%05d (%s) missing from parent" %
|
|
(rr.rrtype, rr.rrname.strip('.'), rr.keyalg,
|
|
rr.keyid, SECRR.hashalgs[rr.hashalg]))
|
|
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]))
|
|
|
|
return match
|
|
|
|
|
|
############################################################################
|
|
# 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')
|
|
parser.add_argument('-a', '--algo', dest='algo', action='append',
|
|
default=[], type=str, help='DS digest algorithm')
|
|
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'),
|
|
type=str, help='path to \'dnssec-dsfromkey\'')
|
|
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('-s', '--dsset', dest='dssetfile', type=str,
|
|
help='prepared DSset file')
|
|
parser.add_argument('-v', '--version', action='version',
|
|
version=version)
|
|
args = parser.parse_args()
|
|
|
|
args.zone = args.zone.strip('.')
|
|
if args.lookaside:
|
|
args.lookaside = args.lookaside.strip('.')
|
|
|
|
return args
|
|
|
|
|
|
############################################################################
|
|
# Main
|
|
############################################################################
|
|
def main():
|
|
args = parse_args()
|
|
match = check(args.zone, args)
|
|
exit(0 if match else 1)
|