mirror of
https://github.com/isc-projects/bind9.git
synced 2026-02-03 20:40:08 -05:00
This patch is strictly the result of: $ black $(git ls-files '*.py' '*.py.in') There have been no manual changes.
226 lines
6.5 KiB
Python
226 lines
6.5 KiB
Python
# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
|
|
#
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
#
|
|
# 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 https://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 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):
|
|
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
|
|
|
|
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 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 check(zone, args):
|
|
rrlist = []
|
|
if args.dssetfile:
|
|
fp = open(args.dssetfile).read()
|
|
else:
|
|
cmd = [args.dig, "+noall", "+answer", "-t", "ds", "-q", 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))
|
|
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.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))
|
|
|
|
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(
|
|
"-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(".")
|
|
|
|
return args
|
|
|
|
|
|
############################################################################
|
|
# Main
|
|
############################################################################
|
|
def main():
|
|
args = parse_args()
|
|
match = check(args.zone, args)
|
|
exit(0 if match else 1)
|