diff --git a/CHANGES b/CHANGES index 51729d2ebf..b2cbd80795 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,6 @@ +5352. [bug] Correctly handle catalog zone entries containing + characters that aren't legal in filenames. [GL #1592] + 5351. [bug] CDS / CDNSKEY consistency checks failed to handle removal records. [GL #1554] diff --git a/bin/tests/system/catz/tests.sh b/bin/tests/system/catz/tests.sh index 0d71b6b1d8..d534b150fd 100644 --- a/bin/tests/system/catz/tests.sh +++ b/bin/tests/system/catz/tests.sh @@ -378,7 +378,7 @@ ret=0 $NSUPDATE -d <> nsupdate.out.test$n 2>&1 || ret=1 server 10.53.0.1 ${PORT} update add masters.catalog1.example. 3600 IN A 10.53.0.3 - update add masters.catalog1.example. 3600 IN AAAA fd92:7065:b8e:ffff::3 + update add masters.catalog1.example. 3600 IN AAAA fd92:7065:b8e:ffff::3 update add 4346f565b4d63ddb99e5d2497ff22d04e878e8f8.zones.catalog1.example. 3600 IN PTR dom6.example. send END @@ -759,96 +759,127 @@ if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status+ret)) ########################################################################## -echo_i "Testing very long domain in catalog" -n=$((n+1)) -echo_i "checking that this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. is not served by master ($n)" -ret=0 -wait_for_no_soa @10.53.0.1 this.is.aery.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. dig.out.test$n || ret=1 -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) +echo_i "Testing catalog entries that can't be represented as filenames" +# note: we need 4 backslashes in the shell to get 2 backslashes in DNS +# presentation format, which is 1 backslash on the wire. +for special in \ + this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example \ + this.zone/domain.has.a.slash.dom10.example \ + this.zone\\\\domain.has.backslash.dom10.example \ + this.zone:domain.has.a.colon.dom.10.example +do + # hashes below are generated by: + # python ${TOP}/contrib/scripts/catzhash.py "${special}" -n=$((n+1)) -echo_i "Adding a domain this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. to master via RNDC ($n)" -ret=0 -echo "@ 3600 IN SOA . . 1 3600 3600 3600 3600" > ns1/dom10.example.db -echo "@ IN NS invalid." >> ns1/dom10.example.db -rndccmd 10.53.0.1 addzone this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. '{type master; file "dom10.example.db";};' || ret=1 -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + case "$special" in + this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example) + hash=825f48b1ce1b4cf5a041d20255a0c8e98d114858 + db=__catz__4d70696f2335687069467f11f5d5378c480383f97782e553fb2d04a7bb2a23ed.db + ;; + this.zone/domain.has.a.slash.dom10.example) + hash=e64cc64c99bf52d0a77fb16dd7ed57cf925a36aa + db=__catz__46ba3e1b28d5955e5313d5fee61bedc78c71d08035aa7ea2f7bf0b8228ab3acc.db + ;; + this.zone\\\\domain.has.backslash.dom10.example) + hash=91e27e02153d38cf656a9b376d7747fbcd19f985 + db=__catz__b667f7ff802c0895e0506699951cff9a1cab68c5ef8546aa0d07425f244ed870.db + ;; + this.zone:domain.has.a.colon.dom.10.example) + hash=8b7238bf4c34045834c573ba4116557ebb24d33c + db=__catz__5c721f7872913a4e7fa8ad42589cce5dd6e551a4c9e6ab3f86e77c0bbc7c2ca6.db + ;; + esac -n=$((n+1)) -echo_i "checking that this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. is now served by master ($n)" -ret=0 -wait_for_soa @10.53.0.1 this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. dig.out.test$n || ret=1 -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + n=$((n+1)) + echo_i "checking that ${special}. is not served by master ($n)" + ret=0 + wait_for_no_soa @10.53.0.1 "${special}" dig.out.test$n || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) -nextpart ns2/named.run >/dev/null + n=$((n+1)) + echo_i "Adding a domain ${special}. to master via RNDC ($n)" + ret=0 + echo "@ 3600 IN SOA . . 1 3600 3600 3600 3600" > ns1/dom10.example.db + echo "@ IN NS invalid." >> ns1/dom10.example.db + rndccmd 10.53.0.1 addzone '"'"${special}"'"' '{type master; file "dom10.example.db";};' || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) -n=$((n+1)) -echo_i "Adding domain this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. to catalog1 zone ($n)" -ret=0 -$NSUPDATE -d <> nsupdate.out.test$n 2>&1 || ret=1 - server 10.53.0.1 ${PORT} - update add 825f48b1ce1b4cf5a041d20255a0c8e98d114858.zones.catalog1.example 3600 IN PTR this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. - send + n=$((n+1)) + echo_i "checking that ${special}. is now served by master ($n)" + ret=0 + wait_for_soa @10.53.0.1 "${special}." dig.out.test$n || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) + + nextpart ns2/named.run >/dev/null + + n=$((n+1)) + echo_i "Adding domain ${special}. to catalog1 zone ($n)" + ret=0 + $NSUPDATE -d <> nsupdate.out.test$n 2>&1 || ret=1 + server 10.53.0.1 ${PORT} + update add ${hash}.zones.catalog1.example 3600 IN PTR ${special}. + send END -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) -n=$((n+1)) -echo_i "waiting for slave to sync up ($n)" -ret=0 -wait_for_message ns2/named.run "catz: adding zone 'this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example' from catalog 'catalog1.example'" && -wait_for_message ns2/named.run "transfer of 'this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example/IN' from 10.53.0.1#${PORT}: Transfer status: success" || ret=1 -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + n=$((n+1)) + echo_i "waiting for slave to sync up ($n)" + ret=0 + wait_for_message ns2/named.run "catz: adding zone '$special' from catalog 'catalog1.example'" && + wait_for_message ns2/named.run "transfer of '$special/IN' from 10.53.0.1#${PORT}: Transfer status: success" || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) -n=$((n+1)) -echo_i "checking that this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. is served by slave ($n)" -ret=0 -wait_for_soa @10.53.0.2 this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. dig.out.test$n || ret=1 -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + n=$((n+1)) + echo_i "checking that ${special}. is served by slave ($n)" + ret=0 + wait_for_soa @10.53.0.2 "${special}." dig.out.test$n || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) -n=$((n+1)) -echo_i "checking that zone-directory is populated with a hashed filename ($n)" -ret=0 -wait_for_zonefile "ns2/zonedir/__catz__4d70696f2335687069467f11f5d5378c480383f97782e553fb2d04a7bb2a23ed.db" || ret=1 -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + n=$((n+1)) + echo_i "checking that zone-directory is populated with a hashed filename ($n)" + ret=0 + wait_for_zonefile "ns2/zonedir/$db" || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) -n=$((n+1)) -echo_i "removing domain this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. from catalog1 zone ($n)" -ret=0 -$NSUPDATE -d <> nsupdate.out.test$n 2>&1 || ret=1 - server 10.53.0.1 ${PORT} - update delete 825f48b1ce1b4cf5a041d20255a0c8e98d114858.zones.catalog1.example - send + n=$((n+1)) + echo_i "removing domain ${special}. from catalog1 zone ($n)" + ret=0 + $NSUPDATE -d <> nsupdate.out.test$n 2>&1 || ret=1 + server 10.53.0.1 ${PORT} + update delete ${hash}.zones.catalog1.example + send END -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) -n=$((n+1)) -echo_i "waiting for slave to sync up ($n)" -ret=0 -wait_for_message ns2/named.run "zone_shutdown: zone this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example/IN: shutting down" || ret=1 -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + n=$((n+1)) + echo_i "waiting for slave to sync up ($n)" + ret=0 + wait_for_message ns2/named.run "zone_shutdown: zone ${special}/IN: shutting down" || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) -n=$((n+1)) -echo_i "checking that this.is.a.very.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. is not served by slave ($n)" -ret=0 -wait_for_no_soa @10.53.0.2 this.is.aery.very.long.long.long.domain.that.will.cause.catalog.zones.to.generate.hash.instead.of.using.regular.filename.dom10.example. dig.out.test$n || ret=1 -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + n=$((n+1)) + echo_i "checking that ${special}. is not served by slave ($n)" + ret=0 + wait_for_no_soa @10.53.0.2 "${special}." dig.out.test$n || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) -n=$((n+1)) -echo_i "checking that zone-directory is emptied ($n)" -ret=0 -wait_for_no_zonefile "ns2/zonedir/__catz__4d70696f2335687069467f11f5d5378c480383f97782e553fb2d04a7bb2a23ed.db" || ret=1 -if [ $ret -ne 0 ]; then echo_i "failed"; fi -status=$((status+ret)) + n=$((n+1)) + echo_i "checking that zone-directory is emptied ($n)" + ret=0 + wait_for_no_zonefile "ns2/zonedir/$db" || ret=1 + if [ $ret -ne 0 ]; then echo_i "failed"; fi + status=$((status+ret)) +done ########################################################################## echo_i "Testing adding a domain and a subdomain of it" @@ -954,8 +985,6 @@ wait_for_soa @10.53.0.2 subdomain.of.dom11.example. dig.out.test$n || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status+ret)) - - n=$((n+1)) echo_i "removing domain dom11.example. from catalog1 zone ($n)" ret=0 @@ -1013,7 +1042,6 @@ wait_for_no_soa @10.53.0.2 subdomain.of.d11.example. dig.out.test$n || ret=1 if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status+ret)) - ########################################################################## echo_i "Testing adding a catalog zone at runtime with rndc reconfig" n=$((n+1)) diff --git a/bin/tests/system/conf.sh.common b/bin/tests/system/conf.sh.common index af4fac1e91..0e559c7f79 100644 --- a/bin/tests/system/conf.sh.common +++ b/bin/tests/system/conf.sh.common @@ -95,6 +95,8 @@ else COLOR_WARN='' fi +SYSTESTDIR="`basename $PWD`" + if type printf > /dev/null 2>&1 then echofail () { @@ -115,6 +117,23 @@ then echoend () { printf "${COLOR_END}%s${COLOR_NONE}\n" "$*" } + echo_i() { + printf '%s\n' "$*" | while read -r __LINE ; do + echoinfo "I:$SYSTESTDIR:$__LINE" + done + } + + echo_ic() { + printf '%s\n' "$*" | while read -r __LINE ; do + echoinfo "I:$SYSTESTDIR: $__LINE" + done + } + + echo_d() { + printf '%s\n' "$*" | while read -r __LINE ; do + echoinfo "D:$SYSTESTDIR:$__LINE" + done + } else echofail () { echo "$*" @@ -134,36 +153,34 @@ else echoend () { echo "$*" } + + echo_i() { + echo "$@" | while read -r __LINE ; do + echoinfo "I:$SYSTESTDIR:$__LINE" + done + } + + echo_ic() { + echo "$@" | while read -r __LINE ; do + echoinfo "I:$SYSTESTDIR: $__LINE" + done + } + + echo_d() { + echo "$@" | while read -r __LINE ; do + echoinfo "D:$SYSTESTDIR:$__LINE" + done + } fi -SYSTESTDIR="`basename $PWD`" - -echo_i() { - echo "$@" | while read __LINE ; do - echoinfo "I:$SYSTESTDIR:$__LINE" - done -} - -echo_ic() { - echo "$@" | while read __LINE ; do - echoinfo "I:$SYSTESTDIR: $__LINE" - done -} - cat_i() { - while read __LINE ; do + while read -r __LINE ; do echoinfo "I:$SYSTESTDIR:$__LINE" done } -echo_d() { - echo "$@" | while read __LINE ; do - echoinfo "D:$SYSTESTDIR:$__LINE" - done -} - cat_d() { - while read __LINE ; do + while read -r __LINE ; do echoinfo "D:$SYSTESTDIR:$__LINE" done } diff --git a/contrib/scripts/catzhash.py b/contrib/scripts/catzhash.py new file mode 100644 index 0000000000..4b59be4834 --- /dev/null +++ b/contrib/scripts/catzhash.py @@ -0,0 +1,30 @@ +#!/usr/bin/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. + +# catzhash.py: generate the SHA-1 hash of a domain name in wire format. +# +# This can be used to determine the label to use in a catalog zone to +# represent the specified zone. For example, the zone +# "domain.example" can be represented in a catalog zone called +# "catalog.example" by adding the following record: +# +# 5960775ba382e7a4e09263fc06e7c00569b6a05c.zones.catalog.example. IN PTR domain.example. +# +# The label "5960775ba382e7a4e09263fc06e7c00569b6a05c" is the output of +# this script when run with the argument "domain.example". + +import sys +import dns.name +import hashlib + +if len(sys.argv) < 2: + print("Usage: %s name" % sys.argv[0]) + +print (hashlib.sha1(dns.name.from_text(sys.argv[1]).to_wire()).hexdigest()) diff --git a/lib/dns/catz.c b/lib/dns/catz.c index 6e24550514..8c3b9fde69 100644 --- a/lib/dns/catz.c +++ b/lib/dns/catz.c @@ -522,8 +522,8 @@ dns_catz_zones_merge(dns_catz_zone_t *target, dns_catz_zone_t *newzone) { dns_name_format(&entry->name, zname, DNS_NAME_FORMATSIZE); result = addzone(entry, target, target->catzs->view, - target->catzs->taskmgr, - target->catzs->zmm->udata); + target->catzs->taskmgr, + target->catzs->zmm->udata); isc_log_write(dns_lctx, DNS_LOGCATEGORY_GENERAL, DNS_LOGMODULE_MASTER, ISC_LOG_INFO, "catz: adding zone '%s' from catalog " @@ -1455,6 +1455,7 @@ dns_catz_generate_masterfilename(dns_catz_zone_t *zone, dns_catz_entry_t *entry, isc_region_t r; isc_result_t result; size_t rlen; + bool special = false; REQUIRE(DNS_CATZ_ZONE_VALID(zone)); REQUIRE(entry != NULL); @@ -1467,24 +1468,39 @@ dns_catz_generate_masterfilename(dns_catz_zone_t *zone, dns_catz_entry_t *entry, isc_buffer_putstr(tbuf, zone->catzs->view->name); isc_buffer_putstr(tbuf, "_"); result = dns_name_totext(&zone->name, true, tbuf); - if (result != ISC_R_SUCCESS) + if (result != ISC_R_SUCCESS) { goto cleanup; + } isc_buffer_putstr(tbuf, "_"); result = dns_name_totext(&entry->name, true, tbuf); - if (result != ISC_R_SUCCESS) + if (result != ISC_R_SUCCESS) { goto cleanup; + } + + /* + * Search for slash and other special characters in the view and + * zone names. Add a null terminator so we can use strpbrk(), then + * remove it. + */ + isc_buffer_putuint8(tbuf, 0); + if (strpbrk(isc_buffer_base(tbuf), "\\/:") != NULL) { + special = true; + } + isc_buffer_subtract(tbuf, 1); /* __catz__.db */ rlen = (isc_md_type_get_size(ISC_MD_SHA256) * 2 + 1) + 12; /* optionally prepend with / */ - if (entry->opts.zonedir != NULL) + if (entry->opts.zonedir != NULL) { rlen += strlen(entry->opts.zonedir) + 1; + } result = isc_buffer_reserve(buffer, (unsigned int)rlen); - if (result != ISC_R_SUCCESS) + if (result != ISC_R_SUCCESS) { goto cleanup; + } if (entry->opts.zonedir != NULL) { isc_buffer_putstr(*buffer, entry->opts.zonedir); @@ -1493,9 +1509,10 @@ dns_catz_generate_masterfilename(dns_catz_zone_t *zone, dns_catz_entry_t *entry, isc_buffer_usedregion(tbuf, &r); isc_buffer_putstr(*buffer, "__catz__"); - if (tbuf->used > ISC_SHA256_DIGESTLENGTH * 2 + 1) { + if (special || tbuf->used > ISC_SHA256_DIGESTLENGTH * 2 + 1) { unsigned char digest[ISC_MAX_MD_SIZE]; unsigned int digestlen; + /* we can do that because digest string < 2 * DNS_NAME */ result = isc_md(ISC_MD_SHA256, r.base, r.length, digest, &digestlen); @@ -1526,7 +1543,7 @@ dns_catz_generate_zonecfg(dns_catz_zone_t *zone, dns_catz_entry_t *entry, { /* * We have to generate a text buffer with regular zone config: - * zone foo.bar { + * zone "foo.bar" { * type slave; * masters [ dscp X ] { ip1 port port1; ip2 port port2; }; * } @@ -1550,9 +1567,9 @@ dns_catz_generate_zonecfg(dns_catz_zone_t *zone, dns_catz_entry_t *entry, isc_buffer_allocate(zone->catzs->mctx, &buffer, ISC_BUFFER_INCR); isc_buffer_setautorealloc(buffer, true); - isc_buffer_putstr(buffer, "zone "); + isc_buffer_putstr(buffer, "zone \""); dns_name_totext(&entry->name, true, buffer); - isc_buffer_putstr(buffer, " { type slave; masters"); + isc_buffer_putstr(buffer, "\" { type slave; masters"); /* * DSCP value has no default, but when it is specified, it is identical @@ -1560,7 +1577,8 @@ dns_catz_generate_zonecfg(dns_catz_zone_t *zone, dns_catz_entry_t *entry, * use the DSCP value set for the first master */ if (entry->opts.masters.count > 0 && - entry->opts.masters.dscps[0] >= 0) { + entry->opts.masters.dscps[0] >= 0) + { isc_buffer_putstr(buffer, " dscp "); snprintf(pbuf, sizeof(pbuf), "%hd", entry->opts.masters.dscps[0]); @@ -1602,8 +1620,9 @@ dns_catz_generate_zonecfg(dns_catz_zone_t *zone, dns_catz_entry_t *entry, isc_buffer_putstr(buffer, " key "); result = dns_name_totext(entry->opts.masters.keys[i], true, buffer); - if (result != ISC_R_SUCCESS) + if (result != ISC_R_SUCCESS) { goto cleanup; + } } isc_buffer_putstr(buffer, "; "); } @@ -1611,8 +1630,9 @@ dns_catz_generate_zonecfg(dns_catz_zone_t *zone, dns_catz_entry_t *entry, if (entry->opts.in_memory == false) { isc_buffer_putstr(buffer, "file \""); result = dns_catz_generate_masterfilename(zone, entry, &buffer); - if (result != ISC_R_SUCCESS) + if (result != ISC_R_SUCCESS) { goto cleanup; + } isc_buffer_putstr(buffer, "\"; "); } @@ -1631,11 +1651,13 @@ dns_catz_generate_zonecfg(dns_catz_zone_t *zone, dns_catz_entry_t *entry, isc_buffer_putstr(buffer, "};"); *buf = buffer; + return (ISC_R_SUCCESS); cleanup: - if (buffer != NULL) + if (buffer != NULL) { isc_buffer_free(&buffer); + } return (result); } diff --git a/lib/dns/include/dns/catz.h b/lib/dns/include/dns/catz.h index cf466d25da..197fcf7d55 100644 --- a/lib/dns/include/dns/catz.h +++ b/lib/dns/include/dns/catz.h @@ -323,13 +323,15 @@ dns_catz_generate_zonecfg(dns_catz_zone_t *zone, dns_catz_entry_t *entry, /* Methods provided by named to dynamically modify the member zones */ /* xxxwpk TODO config! */ typedef isc_result_t (*dns_catz_zoneop_fn_t)(dns_catz_entry_t *entry, - dns_catz_zone_t *origin, dns_view_t *view, - isc_taskmgr_t *taskmgr, void *udata); + dns_catz_zone_t *origin, + dns_view_t *view, + isc_taskmgr_t *taskmgr, + void *udata); struct dns_catz_zonemodmethods { dns_catz_zoneop_fn_t addzone; dns_catz_zoneop_fn_t modzone; dns_catz_zoneop_fn_t delzone; - void * udata; + void *udata; }; diff --git a/util/copyrights b/util/copyrights index 40ab641f55..bc7ffbd33f 100644 --- a/util/copyrights +++ b/util/copyrights @@ -1383,6 +1383,7 @@ ./contrib/kasp/kasp.xml X 2016,2018,2019,2020 ./contrib/kasp/kasp2policy.py X 2016,2018,2019,2020 ./contrib/kasp/policy.good X 2016,2018,2019,2020 +./contrib/scripts/catzhash.py X 2020 ./contrib/scripts/check-secure-delegation.pl.in PERL 2010,2012,2014,2016,2018,2019,2020 ./contrib/scripts/check5011.pl X 2013,2014,2017,2018,2019,2020 ./contrib/scripts/dnssec-keyset.sh X 2015,2018,2019,2020