bind9/bin/python/isc/checkds.py.in
Tony Finch 129b731273 Deprecate SHA-1 in dnssec-checkds
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
2019-05-08 18:17:55 -07:00

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)