convert dnssec signing tests to python

the shell tests that queried servers to check correct signing
behavior (using dnssec-signzone, dnssec-policy and nsupdate),
as well as "rndc signing", private-type records, rndc zonestatus,
offline keys, etc, have been moved to tests_signing.py.

the minimal update test in the dnssec_update_test.pl script
was also moved here and the perl script has been removed.
This commit is contained in:
Evan Hunt 2025-07-02 01:06:19 -07:00
parent 950df056b3
commit 4bd0213fe7
7 changed files with 615 additions and 845 deletions

View file

@ -1,99 +0,0 @@
#!/usr/bin/perl
# 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.
#
# DNSSEC Dynamic update test suite.
#
# Usage:
#
# perl update_test.pl [-s server] [-p port] zone
#
# The server defaults to 127.0.0.1.
# The port defaults to 53.
#
# Installation notes:
#
# This program uses the Net::DNS::Resolver module.
# You can install it by saying
#
# perl -MCPAN -e "install Net::DNS"
#
use Getopt::Std;
use Net::DNS;
use Net::DNS::Update;
use Net::DNS::Resolver;
$opt_s = "127.0.0.1";
$opt_p = 53;
getopt('s:p:');
$res = new Net::DNS::Resolver;
$res->nameservers($opt_s);
$res->port($opt_p);
$res->defnames(0); # Do not append default domain.
@ARGV == 1 or die
"usage: perl update_test.pl [-s server] [-p port] zone\n";
$zone = shift @ARGV;
my $failures = 0;
sub assert {
my ($cond, $explanation) = @_;
if (!$cond) {
print "Test Failed: $explanation ***\n";
$failures++
}
}
sub test {
my ($expected, @records) = @_;
my $update = new Net::DNS::Update("$zone");
foreach $rec (@records) {
$update->push(@$rec);
}
$reply = $res->send($update);
# Did it work?
if (defined $reply) {
my $rcode = $reply->header->rcode;
assert($rcode eq $expected, "expected $expected, got $rcode");
} else {
print "Update failed: ", $res->errorstring, "\n";
}
}
sub section {
my ($msg) = @_;
print "$msg\n";
}
section("Add a name");
test("NOERROR", ["update", rr_add("a.$zone 300 A 73.80.65.49")]);
section("Delete the name");
test("NOERROR", ["update", rr_del("a.$zone")]);
if ($failures) {
print "$failures update tests failed.\n";
} else {
print "All update tests successful.\n";
}
exit $failures;

View file

@ -185,7 +185,7 @@ zone "cdnskey-auto.secure" {
zone "updatecheck-kskonly.secure" {
type primary;
file "updatecheck-kskonly.secure.db.signed";
file "updatecheck-kskonly.secure.db";
dnssec-policy kskonly;
allow-update { any; };
};

View file

@ -297,7 +297,6 @@ $SETTIME -s -g OMNIPRESENT -k OMNIPRESENT now -r OMNIPRESENT now -d RUMOURED now
$SETTIME -s -g OMNIPRESENT -k OMNIPRESENT now -z OMNIPRESENT now $key2 >settime.out.$zone.zsk 2>&1
# Don't sign, let dnssec-policy maintain do it.
cat "$infile" "$key1.key" "$key2.key" >"$zonefile"
mv $zonefile "$zonefile.signed"
zone=hours-vs-days
infile=hours-vs-days.db.in

View file

@ -19,62 +19,10 @@ set -e
status=0
n=1
rm -f dig.out.*
dig_with_opts() {
"$DIG" +tcp +noadd +nosea +nostat +nocmd +dnssec -p "$PORT" "$@"
}
dig_with_answeropts() {
"$DIG" +noall +answer +dnssec -p "$PORT" "$@"
}
delv_with_opts() {
"$DELV" -a ns1/trusted.conf -p "$PORT" "$@"
}
rndccmd() {
"$RNDC" -c ../_common/rndc.conf -p "$CONTROLPORT" -s "$@"
}
# TODO: Move loadkeys_on to conf.sh.common
dnssec_loadkeys_on() {
nsidx=$1
zone=$2
nextpart ns${nsidx}/named.run >/dev/null
rndccmd 10.53.0.${nsidx} loadkeys ${zone} | sed "s/^/ns${nsidx} /" | cat_i
wait_for_log 20 "next key event" ns${nsidx}/named.run || return 1
}
# convert private-type records to readable form
showprivate() {
echo "-- $* --"
dig_with_opts +nodnssec +short "@$2" -t type65534 "$1" >dig.out.$1.test$n
cut -f3 -d' ' <dig.out.$1.$n | while read -r record; do
# shellcheck disable=SC2016
$PERL -e 'my $rdata = pack("H*", @ARGV[0]);
die "invalid record" unless length($rdata) == 5 || length($rdata) == 7;
my ($dns, $key, $remove, $complete, $alg) = unpack("CnCCn", $rdata);
die "invalid record" unless $dns != 0;
my $action = "signing";
$action = "removing" if $remove;
my $state = " (incomplete)";
$state = " (complete)" if $complete;
$alg = $dns if ! defined($alg);
print ("$action: alg: $alg, key: $key$state\n");' "$record"
done
}
# check that signing records are marked as complete
checkprivate() {
for i in 1 2 3 4 5 6 7 8 9 10; do
showprivate "$@" | grep -q incomplete || return 0
sleep 1
done
echo_d "$1 signing incomplete"
return 1
}
if [ -x "${DELV}" ]; then
ret=0
echo_i "checking positive validation NSEC using dns_client ($n)"
@ -286,691 +234,5 @@ if [ -x "${DELV}" ]; then
status=$((status + ret))
fi
# Run a minimal update test if possible. This is really just
# a regression test for RT #2399; more tests should be added.
if $PERL -e 'use Net::DNS;' 2>/dev/null; then
echo_i "running DNSSEC update test"
ret=0
{
output=$($PERL dnssec_update_test.pl -s 10.53.0.3 -p "$PORT" dynamic.example.)
rc=$?
} || true
test "$rc" -eq 0 || ret=1
echo "$output" | cat_i
[ $ret -eq 1 ] && status=1
else
echo_i "The DNSSEC update test requires the Net::DNS library." >&2
fi
echo_i "checking that the NSEC3 record for the apex is properly signed when a DNSKEY is added via UPDATE ($n)"
ret=0
(
kskname=$($KEYGEN -q -3 -a $DEFAULT_ALGORITHM -fk update-nsec3.example)
(
echo zone update-nsec3.example
echo server 10.53.0.3 "$PORT"
grep DNSKEY "${kskname}.key" | sed -e 's/^/update add /' -e 's/IN/300 IN/'
echo send
) | $NSUPDATE
)
dig_with_opts +dnssec a update-nsec3.example. @10.53.0.4 >dig.out.ns4.test$n || ret=1
grep "NOERROR" dig.out.ns4.test$n >/dev/null || ret=1
grep "flags:.* ad[ ;]" dig.out.ns4.test$n >/dev/null || ret=1
grep "NSEC3 1 0 0 - .*" dig.out.ns4.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "checking that signing records have been marked as complete ($n)"
ret=0
checkprivate dynamic.example 10.53.0.3 || ret=1
checkprivate auto-nsec3.example 10.53.0.3 || ret=1
checkprivate expiring.example 10.53.0.3 || ret=1
checkprivate auto-nsec.example 10.53.0.3 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that 'rndc signing' without arguments is handled ($n)"
ret=0
rndccmd 10.53.0.3 signing >/dev/null 2>&1 && ret=1
rndccmd 10.53.0.3 status >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that 'rndc signing -list' without zone is handled ($n)"
ret=0
rndccmd 10.53.0.3 signing -list >/dev/null 2>&1 && ret=1
rndccmd 10.53.0.3 status >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that 'rndc signing -clear' without additional arguments is handled ($n)"
ret=0
rndccmd 10.53.0.3 signing -clear >/dev/null 2>&1 && ret=1
rndccmd 10.53.0.3 status >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that 'rndc signing -clear all' without zone is handled ($n)"
ret=0
rndccmd 10.53.0.3 signing -clear all >/dev/null 2>&1 && ret=1
rndccmd 10.53.0.3 status >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check rndc signing -list output ($n)"
ret=0
{ rndccmd 10.53.0.3 signing -list dynamic.example >signing.out.dynamic.example; } 2>&1
grep -q "No signing records found" signing.out.dynamic.example || {
ret=1
sed 's/^/ns3 /' signing.out.dynamic.example | cat_i
}
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that a split dnssec dnssec-signzone work ($n)"
ret=0
dig_with_opts soa split-dnssec.example. @10.53.0.4 >dig.out.ns4.test$n || ret=1
grep "NOERROR" dig.out.ns4.test$n >/dev/null || ret=1
grep "ANSWER: 2," dig.out.ns4.test$n >/dev/null || ret=1
grep "flags:.* ad[ ;]" dig.out.ns4.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that a smart split dnssec dnssec-signzone work ($n)"
ret=0
dig_with_opts soa split-smart.example. @10.53.0.4 >dig.out.ns4.test$n || ret=1
grep "NOERROR" dig.out.ns4.test$n >/dev/null || ret=1
grep "ANSWER: 2," dig.out.ns4.test$n >/dev/null || ret=1
grep "flags:.* ad[ ;]" dig.out.ns4.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "testing soon-to-expire RRSIGs without a replacement private key ($n)"
ret=0
dig_with_answeropts +nottlid expiring.example ns @10.53.0.3 | grep RRSIG >dig.out.ns3.test$n 2>&1
# there must be a signature here
[ -s dig.out.ns3.test$n ] || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that named doesn't loop when all private keys are not available ($n)"
ret=0
lines=$(grep -c "reading private key file expiring.example" ns3/named.run || true)
test "${lines:-1000}" -lt 15 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check the correct resigning time is reported in zonestatus ($n)"
ret=0
rndccmd 10.53.0.3 \
zonestatus secure.example >rndc.out.ns3.test$n
# next resign node: secure.example/DNSKEY
qname=$(awk '/next resign node:/ { print $4 }' rndc.out.ns3.test$n | sed 's,/.*,,')
qtype=$(awk '/next resign node:/ { print $4 }' rndc.out.ns3.test$n | sed 's,.*/,,')
# next resign time: Thu, 24 Apr 2014 10:38:16 GMT
time=$(awk 'BEGIN { m["Jan"] = "01"; m["Feb"] = "02"; m["Mar"] = "03";
m["Apr"] = "04"; m["May"] = "05"; m["Jun"] = "06";
m["Jul"] = "07"; m["Aug"] = "08"; m["Sep"] = "09";
m["Oct"] = "10"; m["Nov"] = "11"; m["Dec"] = "12";}
/next resign time:/ { printf "%d%s%02d%s\n", $7, m[$6], $5, $8 }' rndc.out.ns3.test$n | sed 's/://g')
dig_with_opts +noall +answer "$qname" "$qtype" @10.53.0.3 >dig.out.test$n
expire=$(awk '$4 == "RRSIG" { print $9 }' dig.out.test$n)
inception=$(awk '$4 == "RRSIG" { print $10 }' dig.out.test$n)
$PERL -e 'exit(0) if ("'"$time"'" lt "'"$expire"'" && "'"$time"'" gt "'"$inception"'"); exit(1);' || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that CDS records are signed using KSK by dnssec-signzone ($n)"
ret=0
dig_with_opts +noall +answer @10.53.0.2 cds cds.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDS" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 2 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that CDS records are not signed using ZSK by dnssec-signzone -x ($n)"
ret=0
dig_with_opts +noall +answer @10.53.0.2 cds cds-x.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDS" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 2 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that CDS records are signed using KSK by with dnssec-policy ($n)"
ret=0
dig_with_opts +noall +answer @10.53.0.2 cds cds-auto.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDS" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that a CDS deletion record is accepted ($n)"
ret=0
(
echo zone cds-update.secure
echo server 10.53.0.2 "$PORT"
echo update delete cds-update.secure CDS
echo update add cds-update.secure 0 CDS 0 0 0 00
echo send
) | $NSUPDATE >nsupdate.out.test$n 2>&1
dig_with_opts +noall +answer @10.53.0.2 cds cds-update.secure >dig.out.test$n
lines=$(awk '$4 == "CDS" {print}' dig.out.test$n | wc -l)
test "${lines:-10}" -eq 1 || ret=1
lines=$(awk '$4 == "CDS" && $5 == "0" && $6 == "0" && $7 == "0" && $8 == "00" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that CDS records are signed only using KSK when added by nsupdate ($n)"
ret=0
keyid=$(cat ns2/cds-update.secure.id)
(
echo zone cds-update.secure
echo server 10.53.0.2 "$PORT"
echo update delete cds-update.secure CDS
echo send
dig_with_opts +noall +answer @10.53.0.2 dnskey cds-update.secure \
| grep "DNSKEY.257" \
| $DSFROMKEY -12 -C -f - -T 1 cds-update.secure \
| sed "s/^/update add /"
echo send
) | $NSUPDATE
dig_with_opts +noall +answer @10.53.0.2 cds cds-update.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDS" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
lines=$(awk -v id="${keyid}" '$4 == "RRSIG" && $5 == "CDS" && $11 == id {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
lines=$(awk '$4 == "CDS" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 2 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that CDS deletion records are signed only using KSK when added by nsupdate ($n)"
ret=0
keyid=$(cat ns2/cds-update.secure.id)
(
echo zone cds-update.secure
echo server 10.53.0.2 "$PORT"
echo update delete cds-update.secure CDS
echo update add cds-update.secure 0 CDS 0 0 0 00
echo send
) | $NSUPDATE
dig_with_opts +noall +answer @10.53.0.2 cds cds-update.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDS" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
lines=$(awk -v id="${keyid}" '$4 == "RRSIG" && $5 == "CDS" && $11 == id {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
lines=$(awk '$4 == "CDS" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
lines=$(awk '$4 == "CDS" && $5 == "0" && $6 == "0" && $7 == "0" && $8 == "00" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that a non matching CDS record is accepted with a matching CDS record ($n)"
ret=0
(
echo zone cds-update.secure
echo server 10.53.0.2 "$PORT"
echo update delete cds-update.secure CDS
echo send
dig_with_opts +noall +answer @10.53.0.2 dnskey cds-update.secure \
| grep "DNSKEY.257" \
| $DSFROMKEY -12 -C -f - -T 1 cds-update.secure \
| sed "s/^/update add /"
dig_with_opts +noall +answer @10.53.0.2 dnskey cds-update.secure \
| grep "DNSKEY.257" | sed 's/DNSKEY.257/DNSKEY 258/' \
| $DSFROMKEY -12 -C -A -f - -T 1 cds-update.secure \
| sed "s/^/update add /"
echo send
) | $NSUPDATE
dig_with_opts +noall +answer @10.53.0.2 cds cds-update.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDS" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
lines=$(awk '$4 == "CDS" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 4 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that CDNSKEY records are signed using KSK by dnssec-signzone ($n)"
ret=0
dig_with_opts +noall +answer @10.53.0.2 cdnskey cdnskey.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDNSKEY" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 2 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that CDNSKEY records are not signed using ZSK by dnssec-signzone -x ($n)"
ret=0
dig_with_opts +noall +answer @10.53.0.2 cdnskey cdnskey-x.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDNSKEY" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 2 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that CDNSKEY records are signed using KSK by with dnssec-auto ($n)"
ret=0
dig_with_opts +noall +answer @10.53.0.2 cdnskey cdnskey-auto.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDNSKEY" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# TODO: test case for GL #1689.
# If we allow the dnssec tools to use deprecated algorithms (such as RSAMD5)
# we could write a test that signs a zone with supported and unsupported
# algorithm, apply a fixed rrset order such that the unsupported algorithm
# precedes the supported one in the DNSKEY RRset, and verify the result still
# validates succesfully.
echo_i "check that a CDNSKEY deletion record is accepted ($n)"
ret=0
(
echo zone cdnskey-update.secure
echo server 10.53.0.2 "$PORT"
echo update delete cdnskey-update.secure CDNSKEY
echo update add cdnskey-update.secure 0 CDNSKEY 0 3 0 AA==
echo send
) | $NSUPDATE >nsupdate.out.test$n 2>&1
dig_with_opts +noall +answer @10.53.0.2 cdnskey cdnskey-update.secure >dig.out.test$n
lines=$(awk '$4 == "CDNSKEY" {print}' dig.out.test$n | wc -l)
test "${lines:-10}" -eq 1 || ret=1
lines=$(awk '$4 == "CDNSKEY" && $5 == "0" && $6 == "3" && $7 == "0" && $8 == "AA==" {print}' dig.out.test$n | wc -l)
test "${lines:-10}" -eq 1 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that CDNSKEY records are signed using KSK only when added by nsupdate ($n)"
ret=0
keyid=$(cat ns2/cdnskey-update.secure.id)
(
echo zone cdnskey-update.secure
echo server 10.53.0.2 "$PORT"
echo update delete cdnskey-update.secure CDNSKEY
dig_with_opts +noall +answer @10.53.0.2 dnskey cdnskey-update.secure \
| sed -n -e "s/^/update add /" -e 's/DNSKEY.257/CDNSKEY 257/p'
echo send
) | $NSUPDATE
dig_with_opts +noall +answer @10.53.0.2 cdnskey cdnskey-update.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDNSKEY" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
lines=$(awk -v id="${keyid}" '$4 == "RRSIG" && $5 == "CDNSKEY" && $11 == id {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
lines=$(awk '$4 == "CDNSKEY" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that a non matching CDNSKEY record is accepted with a matching CDNSKEY record ($n)"
ret=0
(
echo zone cdnskey-update.secure
echo server 10.53.0.2 "$PORT"
echo update delete cdnskey-update.secure CDNSKEY
dig_with_opts +noall +answer @10.53.0.2 dnskey cdnskey-update.secure \
| sed -n -e "s/^/update add /" -e 's/DNSKEY.257/CDNSKEY 257/p'
dig_with_opts +noall +answer @10.53.0.2 dnskey cdnskey-update.secure \
| sed -n -e "s/^/update add /" -e 's/DNSKEY.257/CDNSKEY 258/p'
echo send
) | $NSUPDATE
dig_with_opts +noall +answer @10.53.0.2 cdnskey cdnskey-update.secure >dig.out.test$n
lines=$(awk '$4 == "RRSIG" && $5 == "CDNSKEY" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
lines=$(awk '$4 == "CDNSKEY" {print}' dig.out.test$n | wc -l)
test "$lines" -eq 2 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that DNAME at apex with NSEC3 is correctly signed (dnssec-signzone) ($n)"
ret=0
dig_with_opts txt dname-at-apex-nsec3.example @10.53.0.3 >dig.out.ns3.test$n || ret=1
grep "RRSIG.NSEC3 $DEFAULT_ALGORITHM_NUMBER 3 600" dig.out.ns3.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "check that DNSKEY and other occluded data are excluded from the delegating bitmap ($n)"
ret=0
dig_with_opts axfr occluded.example @10.53.0.3 >dig.out.ns3.test$n || ret=1
grep "^delegation.occluded.example..*NSEC.*NS KEY DS RRSIG NSEC$" dig.out.ns3.test$n >/dev/null || ret=1
grep "^delegation.occluded.example..*DNSKEY.*" dig.out.ns3.test$n >/dev/null || ret=1
grep "^delegation.occluded.example..*AAAA.*" dig.out.ns3.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "checking DNSSEC records are occluded from ANY in an insecure zone ($n)"
ret=0
dig_with_opts any x.insecure.example. @10.53.0.3 >dig.out.ns3.1.test$n || ret=1
grep "status: NOERROR" dig.out.ns3.1.test$n >/dev/null || ret=1
grep "ANSWER: 0," dig.out.ns3.1.test$n >/dev/null || ret=1
dig_with_opts any z.secure.example. @10.53.0.3 >dig.out.ns3.2.test$n || ret=1
grep "status: NOERROR" dig.out.ns3.2.test$n >/dev/null || ret=1
# A+RRSIG, NSEC+RRSIG
grep "ANSWER: 4," dig.out.ns3.2.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
###
### Additional checks for when the KSK is offline.
###
# Save some useful information
zone="updatecheck-kskonly.secure"
KSK=$(cat ns2/${zone}.ksk.key)
ZSK=$(cat ns2/${zone}.zsk.key)
KSK_ID=$(cat ns2/${zone}.ksk.id)
ZSK_ID=$(cat ns2/${zone}.zsk.id)
SECTIONS="+answer +noauthority +noadditional"
echo_i "testing zone $zone KSK=$KSK_ID ZSK=$ZSK_ID"
# Set key state for KSK. The ZSK rollovers below assume that there is a chain
# of trust established, so we tell named that the DS is in omnipresent state.
$SETTIME -s -d OMNIPRESENT now -K ns2 $KSK >/dev/null
# Print IDs of keys used for generating RRSIG records for RRsets of type $1
# found in dig output file $2.
get_keys_which_signed() {
qtype=$1
output=$2
# The key ID is the 11th column of the RRSIG record line.
awk -v qt="$qtype" '$4 == "RRSIG" && $5 == qt {print $11}' <"$output"
}
# Basic checks to make sure everything is fine before the KSK is made offline.
for qtype in "DNSKEY" "CDNSKEY" "CDS"; do
echo_i "checking $qtype RRset is signed with KSK only ($n)"
ret=0
dig_with_opts $SECTIONS @10.53.0.2 $qtype $zone >dig.out.test$n
lines=$(get_keys_which_signed $qtype dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$KSK_ID$" >/dev/null || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID$" >/dev/null && ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
done
echo_i "checking SOA RRset is signed with ZSK only ($n)"
ret=0
dig_with_opts $SECTIONS @10.53.0.2 soa $zone >dig.out.test$n
lines=$(get_keys_which_signed "SOA" dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
get_keys_which_signed "SOA" dig.out.test$n | grep "^$KSK_ID$" >/dev/null && ret=1
get_keys_which_signed "SOA" dig.out.test$n | grep "^$ZSK_ID$" >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Roll the ZSK.
zsk2=$("$KEYGEN" -q -P none -A none -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -K ns2 "$zone")
keyfile_to_key_id "$zsk2" >ns2/$zone.zsk.id2
ZSK_ID2=$(cat ns2/$zone.zsk.id2)
ret=0
echo_i "prepublish new ZSK $ZSK_ID2 for $zone ($n)"
rndccmd 10.53.0.2 dnssec -rollover -key $ZSK_ID $zone 2>&1 | sed 's/^/ns2 /' | cat_i
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
zsk_count_equals() {
expectedzsks=$1
dig_with_opts @10.53.0.2 DNSKEY $zone >dig.out.test$n
lines=$(cat dig.out.test$n | grep "DNSKEY.*256 3 13" | wc -l)
test "$lines" -eq $expectedzsks || return 1
}
echo_i "check DNSKEY RRset has successor ZSK $ZSK_ID2 ($n)"
ret=0
# The expected number of ZSKs is 2.
retry_quiet 5 zsk_count_equals 2 || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Make new ZSK active.
echo_i "make ZSK $ZSK_ID inactive and make new ZSK $ZSK_ID2 active for zone $zone ($n)"
ret=0
$SETTIME -s -I now -K ns2 $ZSK >/dev/null
$SETTIME -s -k OMNIPRESENT now -A now -K ns2 $zsk2 >/dev/null
dnssec_loadkeys_on 2 $zone || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Wait for newest ZSK to become active.
echo_i "wait until new ZSK $ZSK_ID2 active and ZSK $ZSK_ID inactive"
for i in 1 2 3 4 5 6 7 8 9 10; do
ret=0
grep "DNSKEY $zone/$DEFAULT_ALGORITHM/$ZSK_ID2 (ZSK) is now active" ns2/named.run >/dev/null || ret=1
grep "DNSKEY $zone/$DEFAULT_ALGORITHM/$ZSK_ID (ZSK) is now inactive" ns2/named.run >/dev/null || ret=1
[ "$ret" -eq 0 ] && break
sleep 1
done
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Remove the KSK from disk.
echo_i "remove the KSK $KSK_ID for zone $zone from disk"
mv ns2/$KSK.key ns2/$KSK.key.bak
mv ns2/$KSK.private ns2/$KSK.private.bak
# Update the zone that requires a resign of the SOA RRset.
echo_i "update the zone with $zone IN TXT nsupdate added me"
(
echo zone $zone
echo server 10.53.0.2 "$PORT"
echo update add $zone. 300 in txt "nsupdate added me"
echo send
) | $NSUPDATE
# Redo the tests now that the zone is updated and the KSK is offline.
for qtype in "DNSKEY" "CDNSKEY" "CDS"; do
echo_i "checking $qtype RRset is signed with KSK only, KSK offline ($n)"
ret=0
dig_with_opts $SECTIONS @10.53.0.2 $qtype $zone >dig.out.test$n
lines=$(get_keys_which_signed $qtype dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$KSK_ID$" >/dev/null || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID2$" >/dev/null && ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
done
for qtype in "SOA" "TXT"; do
echo_i "checking $qtype RRset is signed with new ZSK $ZSK_ID2 only, KSK offline ($n)"
ret=0
dig_with_opts $SECTIONS @10.53.0.2 $qtype $zone >dig.out.test$n
lines=$(get_keys_which_signed $qtype dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$KSK_ID$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID2$" >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
done
# Put back the KSK.
echo_i "put back the KSK $KSK_ID for zone $zone from disk"
mv ns2/$KSK.key.bak ns2/$KSK.key
mv ns2/$KSK.private.bak ns2/$KSK.private
# Roll the ZSK again.
zsk3=$("$KEYGEN" -q -P none -A none -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -K ns2 "$zone")
ret=0
keyfile_to_key_id "$zsk3" >ns2/$zone.zsk.id3
ZSK_ID3=$(cat ns2/$zone.zsk.id3)
echo_i "delete old ZSK $ZSK_ID, schedule ZSK $ZSK_ID2 inactive, and pre-publish ZSK $ZSK_ID3 for zone $zone ($n)"
$SETTIME -s -k HIDDEN now -z HIDDEN now -D now -K ns2 $ZSK >/dev/null
$SETTIME -s -k OMNIPRESENT now -z OMNIPRESENT now -K ns2 $zsk2 >/dev/null
dnssec_loadkeys_on 2 $zone || ret=1
rndccmd 10.53.0.2 dnssec -rollover -key $ZSK_ID2 $zone 2>&1 | sed 's/^/ns2 /' | cat_i
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Wait for newest ZSK to become published.
echo_i "wait until new ZSK $ZSK_ID3 published"
for i in 1 2 3 4 5 6 7 8 9 10; do
ret=0
grep "DNSKEY $zone/$DEFAULT_ALGORITHM/$ZSK_ID3 (ZSK) is now published" ns2/named.run >/dev/null || ret=1
[ "$ret" -eq 0 ] && break
sleep 1
done
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Remove the KSK from disk.
echo_i "remove the KSK $KSK_ID for zone $zone from disk"
mv ns2/$KSK.key ns2/$KSK.key.bak
mv ns2/$KSK.private ns2/$KSK.private.bak
# Update the zone that requires a resign of the SOA RRset.
echo_i "update the zone with $zone IN TXT nsupdate added me again"
(
echo zone $zone
echo server 10.53.0.2 "$PORT"
echo update add $zone. 300 in txt "nsupdate added me again"
echo send
) | $NSUPDATE
# Redo the tests now that the ZSK roll has deleted the old key.
for qtype in "DNSKEY" "CDNSKEY" "CDS"; do
echo_i "checking $qtype RRset is signed with KSK only, old ZSK deleted ($n)"
ret=0
dig_with_opts $SECTIONS @10.53.0.2 $qtype $zone >dig.out.test$n
lines=$(get_keys_which_signed $qtype dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$KSK_ID$" >/dev/null || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID2$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID3$" >/dev/null && ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
done
for qtype in "SOA" "TXT"; do
echo_i "checking $qtype RRset is signed with ZSK $ZSK_ID2 only, old ZSK deleted ($n)"
ret=0
dig_with_opts $SECTIONS @10.53.0.2 $qtype $zone >dig.out.test$n
lines=$(get_keys_which_signed $qtype dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$KSK_ID$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID2$" >/dev/null || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID3$" >/dev/null && ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
done
# Put back the KSK.
echo_i "put back the KSK $KSK_ID for zone $zone from disk"
mv ns2/$KSK.key.bak ns2/$KSK.key
mv ns2/$KSK.private.bak ns2/$KSK.private
# Make the new ZSK (ZSK3) active.
echo_i "make new ZSK $ZSK_ID3 active for zone $zone ($n)"
ret=0
$SETTIME -s -I now -K ns2 $zsk2 >/dev/null
$SETTIME -s -k OMNIPRESENT now -A now -K ns2 $zsk3 >/dev/null
dnssec_loadkeys_on 2 $zone || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Wait for newest ZSK to become active.
echo_i "wait until new ZSK $ZSK_ID3 active and ZSK $ZSK_ID2 inactive"
for i in 1 2 3 4 5 6 7 8 9 10; do
ret=0
grep "DNSKEY $zone/$DEFAULT_ALGORITHM/$ZSK_ID3 (ZSK) is now active" ns2/named.run >/dev/null || ret=1
grep "DNSKEY $zone/$DEFAULT_ALGORITHM/$ZSK_ID2 (ZSK) is now inactive" ns2/named.run >/dev/null || ret=1
[ "$ret" -eq 0 ] && break
sleep 1
done
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Remove the KSK from disk.
echo_i "remove the KSK $KSK_ID for zone $zone from disk"
mv ns2/$KSK.key ns2/$KSK.key.bak
mv ns2/$KSK.private ns2/$KSK.private.bak
# Update the zone that requires a resign of the SOA RRset.
echo_i "update the zone with $zone IN TXT nsupdate added me one more time"
(
echo zone $zone
echo server 10.53.0.2 "$PORT"
echo update add $zone. 300 in txt "nsupdate added me one more time"
echo send
) | $NSUPDATE
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
# Redo the tests one more time.
for qtype in "DNSKEY" "CDNSKEY" "CDS"; do
echo_i "checking $qtype RRset is signed with KSK only, new ZSK active ($n)"
ret=0
dig_with_opts $SECTIONS @10.53.0.2 $qtype $zone >dig.out.test$n
lines=$(get_keys_which_signed $qtype dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$KSK_ID$" >/dev/null || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID2$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID3$" >/dev/null && ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
done
for qtype in "SOA" "TXT"; do
echo_i "checking $qtype RRset is signed with new ZSK $ZSK_ID3 only, new ZSK active ($n)"
ret=0
dig_with_opts $SECTIONS @10.53.0.2 $qtype $zone >dig.out.test$n
lines=$(get_keys_which_signed $qtype dig.out.test$n | wc -l)
test "$lines" -eq 1 || ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$KSK_ID$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID2$" >/dev/null && ret=1
get_keys_which_signed $qtype dig.out.test$n | grep "^$ZSK_ID3$" >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
done
echo_i "exit status: $status"
[ $status -eq 0 ] || exit 1

View file

@ -72,6 +72,7 @@ pytestmark = pytest.mark.extra_artifacts(
"ns2/too-many-iterations.db",
"ns2/inconsistent.db",
"ns2/trusted.db",
"ns2/updatecheck-kskonly.secure.db",
"ns2/updatecheck-kskonly.secure.ksk.id",
"ns2/updatecheck-kskonly.secure.ksk.key",
"ns2/updatecheck-kskonly.secure.zsk.id",

View file

@ -0,0 +1,613 @@
# 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.
from collections import namedtuple
import os
import re
import struct
import time
from dns import dnssec, name, rdataclass, rdatatype, update
import pytest
pytest.importorskip("dns", minversion="2.0.0")
import isctest
# helper functions
def grep_c(regex, filename):
with open(filename, "r", encoding="utf-8") as f:
blob = f.read().splitlines()
results = [x for x in blob if re.search(regex, x)]
return len(results)
# run dnssec-keygen
def keygen(*args):
keygen_cmd = [os.environ.get("KEYGEN")]
keygen_cmd.extend(args)
return isctest.run.cmd(keygen_cmd, log_stdout=True).stdout.decode("utf-8").strip()
# run dnssec-settime
def settime(*args):
settime_cmd = [os.environ.get("SETTIME")]
settime_cmd.extend(args)
return isctest.run.cmd(settime_cmd, log_stdout=True).stdout.decode("utf-8").strip()
@pytest.mark.parametrize(
"domain",
[
"auto-nsec.example",
"auto-nsec3.example",
],
)
def test_signing_complete(domain):
PrivateType = namedtuple("PrivateType", ["alg", "key", "rem", "complete"])
def convert_private(rdata) -> PrivateType:
length = len(rdata.to_wire())
assert length in (5, 7)
if length == 7:
_, key, rem, complete, alg = struct.unpack(">BHBBH", rdata.to_wire())
else:
alg, key, rem, complete = struct.unpack(">BHBB", rdata.to_wire())
return PrivateType(alg, key, rem, complete)
# query for a private type record, make sure it shows "complete"
def check_complete():
msg = isctest.query.create(domain, 65534)
res = isctest.query.tcp(msg, "10.53.0.3")
assert res.answer
for rdata in res.answer[0]:
record = convert_private(rdata)
assert record.complete
return True
isctest.run.retry_with_timeout(check_complete, 10)
def test_split_dnssec():
# check that split-dnssec signing worked (dnssec-signzone -D)
msg = isctest.query.create("split-dnssec.example.", "SOA")
res1 = isctest.query.tcp(msg, "10.53.0.3")
res2 = isctest.query.tcp(msg, "10.53.0.4")
isctest.check.same_answer(res1, res2)
isctest.check.noerror(res2)
isctest.check.rr_count_eq(res2.answer, 2)
isctest.check.adflag(res2)
# check that smart split-dnssec signing worked (dnssec-signzone -DS)
msg = isctest.query.create("split-smart.example.", "SOA")
res1 = isctest.query.tcp(msg, "10.53.0.3")
res2 = isctest.query.tcp(msg, "10.53.0.4")
isctest.check.same_answer(res1, res2)
isctest.check.noerror(res2)
isctest.check.rr_count_eq(res2.answer, 2)
isctest.check.adflag(res2)
def test_expiring_rrsig():
# check soon-to-expire RRSIGs without a replacement private
# key aren't deleted. this response has to have an RRSIG:
msg = isctest.query.create("expiring.example.", "NS")
res = isctest.query.tcp(msg, "10.53.0.3")
_, sigs = res.answer
assert sigs
# check that named doesn't loop when private keys are not available
n = grep_c("reading private key file expiring.example", "ns3/named.run")
assert n < 15
# check expired signatures stay place when updates are disabled
msg = isctest.query.create("expired.example", "SOA")
res = isctest.query.tcp(msg, "10.53.0.3")
_, sigs = res.answer
assert sigs
def test_apex_signing():
# check that DNAME at apex with NSEC3 is correctly signed
msg = isctest.query.create("dname-at-apex-nsec3.example.", "TXT")
res = isctest.query.tcp(msg, "10.53.0.3")
sigs = [str(a) for a in res.authority if a.rdtype == rdatatype.RRSIG]
alg = os.environ.get("DEFAULT_ALGORITHM_NUMBER")
assert any(f"NSEC3 {alg} 3 600" in a for a in sigs)
def test_occluded_data():
# check that DNSKEY and other occluded data are excluded from
# a delegating bitmap
msg = isctest.query.create("occluded.example.", "AXFR")
res = isctest.query.tcp(msg, "10.53.0.3")
n = "delegation.occluded.example."
delegation = [r for r in res.answer if str(r.name) == n]
assert [r for r in delegation if r.rdtype == rdatatype.DNSKEY], str(delegation)
assert [r for r in delegation if r.rdtype == rdatatype.AAAA], str(delegation)
nsec = [r for r in delegation if r.rdtype == rdatatype.NSEC]
assert nsec, str(delegation)
assert "DNSKEY" not in str(nsec[0]), str(res)
assert "AAAA" not in str(nsec[0]), str(res)
# check that DNSSEC records are occluded from ANY in an insecure zone
msg = isctest.query.create("x.extrakey.example.", "ANY")
res = isctest.query.tcp(msg, "10.53.0.3")
isctest.check.noerror(res)
isctest.check.empty_answer(res)
msg = isctest.query.create("z.secure.example.", "ANY")
res = isctest.query.tcp(msg, "10.53.0.3")
isctest.check.noerror(res)
isctest.check.rr_count_eq(res.answer, 4) # A+RRSIG, NSEC+RRSIG
def test_update_signing():
# minimal update test: add and delete a single record
up = update.UpdateMessage("dynamic.example.")
up.add("a.dynamic.example.", 300, "A", "73.80.65.49")
res = isctest.query.tcp(up, "10.53.0.3")
isctest.check.noerror(res)
up = update.UpdateMessage("dynamic.example.")
up.delete("a.dynamic.example.")
res = isctest.query.tcp(up, "10.53.0.3")
isctest.check.noerror(res)
msg = isctest.query.create("a.dynamic.example", "A")
res = isctest.query.tcp(msg, "10.53.0.4")
isctest.check.nxdomain(res)
isctest.check.adflag(res)
# check that the NSEC3 record for the apex is properly signed
# when a DNSKEY is added via UPDATE
key = keygen(
"-q3fk", "-a", os.environ["DEFAULT_ALGORITHM"], "update-nsec3.example."
)
with open(f"{key}.key", "r", encoding="utf-8") as f:
dnskey = f.read().splitlines()[-1]
dnskey = " ".join(dnskey.split()[3:])
up = update.UpdateMessage("update-nsec3.example.")
up.add("update-nsec3.example.", 300, "DNSKEY", dnskey)
res = isctest.query.tcp(up, "10.53.0.3")
isctest.check.noerror(res)
msg = isctest.query.create("update-nsec3.example", "A")
res = isctest.query.tcp(msg, "10.53.0.4")
isctest.check.noerror(res)
isctest.check.adflag(res)
nsec3 = [str(a) for a in res.authority if a.rdtype == rdatatype.NSEC3]
assert any("1 0 0 -" in a for a in nsec3)
def test_cds_signing():
# check that CDS records are signed using KSK+ZSK by dnssec-signzone
msg = isctest.query.create("cds.secure.", "CDS")
res = isctest.query.tcp(msg, "10.53.0.2")
cds, sigs = res.answer
assert len(sigs) == 2
# check that CDS records are not signed using ZSK by dnssec-signzone -x
msg = isctest.query.create("cds-x.secure.", "CDS")
res = isctest.query.tcp(msg, "10.53.0.2")
cds, sigs = res.answer
assert len(sigs) == 2 # there are two KSKs here
# check that CDS records are signed using KSK by dnssec-policy
msg = isctest.query.create("cds-auto.secure.", "CDS")
res = isctest.query.tcp(msg, "10.53.0.2")
cds, sigs = res.answer
assert len(sigs) == 1
# check that CDS records are signed only using KSK when added by nsupdate
with open("ns2/cds-update.secure.id", encoding="utf-8") as f:
keyid = int(f.read().splitlines()[0])
up = update.UpdateMessage("cds-update.secure.")
up.delete("cds-update.secure.", "CDS")
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
msg = isctest.query.create("cds-update.secure.", "DNSKEY")
res = isctest.query.tcp(msg, "10.53.0.2")
isctest.check.noerror(res)
dnskeys, sigs = res.answer
ksk = [a for a in dnskeys if a.flags == 257][0]
ds = dnssec.make_ds("cds-update.secure.", ksk, 2)
up.add("cds-update.secure.", 1, "CDS", str(ds))
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
msg = isctest.query.create("cds-update.secure.", "CDS")
res = isctest.query.tcp(msg, "10.53.0.2")
cds, sig = res.answer
assert len(cds) == 1
assert len(sig) == 1
assert sig[0].key_tag == keyid
# check that CDS deletion records are signed only using KSK when
# added by nsupdate
up = update.UpdateMessage("cds-update.secure.")
up.delete("cds-update.secure.", "CDS")
up.add("cds-update.secure.", 0, "CDS", "0 0 0 00")
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
msg = isctest.query.create("cds-update.secure.", "CDS")
res = isctest.query.tcp(msg, "10.53.0.2")
cds, sig = res.answer
assert len(cds) == 1
assert "0 0 0 00" in str(cds[0])
assert len(sig) == 1
assert sig[0].key_tag == keyid
# check that a non-matching CDS record is accepted with a
# matching CDS record. first, generate a DNSKEY with different flags:
badksk = type(ksk)(
ksk.rdclass, ksk.rdtype, ksk.flags + 1, ksk.protocol, ksk.algorithm, ksk.key
)
up = update.UpdateMessage("cds-update.secure.")
badds = dnssec.make_ds("cds-update.secure.", badksk, 2)
up.delete("cds-update.secure.", "CDS")
up.add("cds-update.secure.", 1, "CDS", str(ds))
up.add("cds-update.secure.", 1, "CDS", str(badds))
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
msg = isctest.query.create("cds-update.secure.", "CDS")
res = isctest.query.tcp(msg, "10.53.0.2")
cds, sig = res.answer
assert len(cds) == 2
assert len(sig) == 1
def test_cdnskey_signing():
# check that CDNSKEY records are signed using KSK+ZSK by dnssec-signzone
msg = isctest.query.create("cdnskey.secure.", "CDNSKEY")
res = isctest.query.tcp(msg, "10.53.0.2")
cdnskey, sigs = res.answer
assert len(sigs) == 2
# check that CDNSKEY records are not signed using ZSK by dnssec-signzone -x
msg = isctest.query.create("cdnskey-x.secure.", "CDNSKEY")
res = isctest.query.tcp(msg, "10.53.0.2")
cdnskey, sigs = res.answer
assert len(sigs) == 2 # two KSKs here
# check that CDNSKEY records are signed using KSK by dnssec-policy
msg = isctest.query.create("cdnskey-auto.secure.", "CDNSKEY")
res = isctest.query.tcp(msg, "10.53.0.2")
cdnskey, sigs = res.answer
assert len(sigs) == 1
# check that CDNSKEY records are signed only using KSK
# when added by nsupdate
with open("ns2/cdnskey-update.secure.id", encoding="utf-8") as f:
keyid = int(f.read().splitlines()[0])
up = update.UpdateMessage("cdnskey-update.secure.")
up.delete("cdnskey-update.secure.", "CDNSKEY")
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
msg = isctest.query.create("cdnskey-update.secure.", "DNSKEY")
res = isctest.query.tcp(msg, "10.53.0.2")
isctest.check.noerror(res)
dnskeys, sigs = res.answer
ksk = [a for a in dnskeys if a.flags == 257][0]
up.add("cdnskey-update.secure.", 1, "CDNSKEY", str(ksk))
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
msg = isctest.query.create("cdnskey-update.secure.", "CDNSKEY")
res = isctest.query.tcp(msg, "10.53.0.2")
cdnskey, sig = res.answer
assert len(cdnskey) == 1
assert len(sig) == 1
assert sig[0].key_tag == keyid
# check that CDNSKEY deletion records are signed only using KSK when
# added by nsupdate
up = update.UpdateMessage("cdnskey-update.secure.")
up.delete("cdnskey-update.secure.", "CDNSKEY")
up.add("cdnskey-update.secure.", 0, "CDNSKEY", "0 3 0 AA==")
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
msg = isctest.query.create("cdnskey-update.secure.", "CDNSKEY")
res = isctest.query.tcp(msg, "10.53.0.2")
cdnskey, sig = res.answer
assert len(cdnskey) == 1
assert "0 3 0 AA==" in str(cdnskey[0])
assert len(sig) == 1
assert sig[0].key_tag == keyid
# check that a non-matching CDNSKEY record is accepted with a
# matching CDNSKEY record. first, generate a DNSKEY with different flags:
badksk = type(ksk)(
ksk.rdclass, ksk.rdtype, ksk.flags + 1, ksk.protocol, ksk.algorithm, ksk.key
)
up = update.UpdateMessage("cdnskey-update.secure.")
up.delete("cdnskey-update.secure.", "CDNSKEY")
up.add("cdnskey-update.secure.", 1, "CDNSKEY", str(ksk))
up.add("cdnskey-update.secure.", 1, "CDNSKEY", str(badksk))
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
msg = isctest.query.create("cdnskey-update.secure.", "CDNSKEY")
res = isctest.query.tcp(msg, "10.53.0.2")
cdnskey, sig = res.answer
assert len(cdnskey) == 2
assert len(sig) == 1
@pytest.mark.parametrize(
"cmd",
[
"signing", # without arguments
"signing -list", # without zone
"signing -clear", # without zone
"signing -clear all", # without zone
],
)
def test_rndc_signing_except(cmd, servers):
ns3 = servers["ns3"]
# check that 'rndc signing' errors are handled
with pytest.raises(isctest.rndc.RNDCException):
ns3.rndc(cmd, log=False)
ns3.rndc("status", log=False)
def test_rndc_signing_output(servers):
ns3 = servers["ns3"]
response = ns3.rndc("signing -list dynamic.example", log=False)
assert "No signing records found" in response
def test_zonestatus_signing(servers):
ns3 = servers["ns3"]
# check that the correct resigning time is reported in zonestatus.
# zonestatus reports a name/type and expecting resigning time;
# we convert the time to seconds since epoch, look up the RRSIG
# for the name and type, and check that the resigning time is
# after the inception and before the expiration.
response = ns3.rndc("zonestatus secure.example", log=False)
# next resign node: secure.example/DNSKEY
nrn = [r for r in response.splitlines() if "next resign node" in r][0]
rdname, rdtype = nrn.split()[3].split("/")
# next resign time: Thu, 24 Apr 2014 10:38:16 GMT
nrt = [r for r in response.splitlines() if "next resign time" in r][0]
rtime = " ".join(nrt.split()[3:])
rt = time.strptime(rtime, "%a, %d %b %Y %H:%M:%S %Z")
when = int(time.strftime("%s", rt))
msg = isctest.query.create(rdname, rdtype)
res = isctest.query.tcp(msg, "10.53.0.3")
_, sigs = res.answer
assert sigs[0].inception < when
assert when < sigs[0].expiration
def test_offline_ksk_signing(servers):
def getfrom(file):
with open(file, encoding="utf-8") as f:
return f.read().strip()
def getkeyid(key: str):
m = re.match(r"K.*\+\d*\+(\d*)", key)
return int(m.group(1))
def check_signing_keys(types: list[str], expect: list[str], prohibit: list[str]):
for qtype in types:
isctest.log.debug(f"checking signing keys for {qtype}")
msg = isctest.query.create(zone, qtype)
res = isctest.query.tcp(msg, "10.53.0.2")
assert res.answer, str(res)
rrset = res.get_rrset(
res.answer,
name.from_text(f"{zone}."),
rdataclass.IN,
rdatatype.RRSIG,
rdatatype.RdataType.make(qtype),
)
assert rrset, f"expected RRSIG({qtype}) missing from ANSWER" + str(res)
keys = {rr.key_tag for rr in rrset}
assert len(keys) == 1, str(res)
for exp in expect:
assert exp in keys
for proh in prohibit:
assert proh not in keys
return True
def check_zskcount():
msg = isctest.query.create(zone, "DNSKEY")
res = isctest.query.tcp(msg, "10.53.0.2")
dnskeys, _ = res.answer
zskcount = len([rr for rr in dnskeys if rr.flags == 256])
assert zskcount == 2, str(res)
return True
def ksk_remove():
isctest.log.info("remove the KSK from disk")
os.rename(f"ns2/{KSK}.key", f"ns2/{KSK}.key.bak")
os.rename(f"ns2/{KSK}.private", f"ns2/{KSK}.private.bak")
def ksk_recover():
isctest.log.info("put back the KSK")
os.rename(f"ns2/{KSK}.key.bak", f"ns2/{KSK}.key")
os.rename(f"ns2/{KSK}.private.bak", f"ns2/{KSK}.private")
def loadkeys():
pattern = re.compile(f"{zone}/IN.*next key event")
with ns2.watch_log_from_here() as watcher:
ns2.rndc(f"loadkeys {zone}", log=False)
watcher.wait_for_line(pattern)
ksk_only_types = ["DNSKEY", "CDNSKEY", "CDS"]
ns2 = servers["ns2"]
zone = "updatecheck-kskonly.secure"
KSK = getfrom(f"ns2/{zone}.ksk.key")
ZSK = getfrom(f"ns2/{zone}.zsk.key")
KSKID = int(getfrom(f"ns2/{zone}.ksk.id"))
ZSKID = int(getfrom(f"ns2/{zone}.zsk.id"))
# set key state for KSK. the ZSK rollovers below assume that there is a
# chain of trust established, so we tell named that the DS is in
# omnipresent state.
settime("-s", "-d", "OMNIPRESENT", "now", "-Kns2", KSK)
isctest.log.info("check state before KSK is made offline")
isctest.log.info("make sure certain types are signed with KSK only")
check_signing_keys(ksk_only_types, expect=[KSKID], prohibit=[ZSKID])
isctest.log.info("check SOA is signed with ZSK only")
check_signing_keys(["SOA"], expect=[ZSKID], prohibit=[KSKID])
isctest.log.info("roll the ZSK")
ZSK2 = keygen(
"-qKns2",
"-Pnone",
"-Anone",
"-a",
os.environ["DEFAULT_ALGORITHM"],
"-b",
os.environ["DEFAULT_BITS"],
zone,
)
ZSKID2 = getkeyid(ZSK2)
isctest.log.info("prepublish new ZSK")
ns2.rndc(f"dnssec -rollover -key {ZSKID} {zone}", log=False)
isctest.run.retry_with_timeout(check_zskcount, 5)
isctest.log.info("make the new ZSK active")
settime("-sKns2", "-Inow", ZSK)
settime("-sKns2", "-Anow", "-k", "OMNIPRESENT", "now", ZSK2)
loadkeys()
with ns2.watch_log_from_start() as watcher:
watcher.wait_for_line(
[f"{ZSKID2} (ZSK) is now active", f"{ZSKID} (ZSK) is now inactive"]
)
ksk_remove()
isctest.log.info("update the zone, requiring a resign of the SOA RRset")
up = update.UpdateMessage(f"{zone}.")
up.add(f"{zone}.", 300, "TXT", "added by UPDATE")
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
isctest.log.info(
"redo the tests now that the zone is updated and the KSK is offline"
)
isctest.log.info("make sure certain types are signed with KSK only")
check_signing_keys(ksk_only_types, expect=[KSKID], prohibit=[ZSKID, ZSKID2])
isctest.log.info("check TXT, SOA are signed with ZSK2 only")
def check_txt_soa_zsk2():
return check_signing_keys(
["TXT", "SOA"], expect=[ZSKID2], prohibit=[KSKID, ZSKID]
)
isctest.run.retry_with_timeout(check_txt_soa_zsk2, 5)
ksk_recover()
isctest.log.info("roll the ZSK again")
ZSK3 = keygen(
"-qKns2",
"-Pnone",
"-Anone",
"-a",
os.environ["DEFAULT_ALGORITHM"],
"-b",
os.environ["DEFAULT_BITS"],
zone,
)
ZSKID3 = getkeyid(ZSK3)
isctest.log.info("delete old ZSK, schedule ZSK2 inactive, pre-publish ZSK3")
settime("-sKns2", "-k", "HIDDEN", "now", "-z", "HIDDEN", "now", "-Dnow", ZSK)
settime("-sKns2", "-k", "OMNIPRESENT", "now", "-z", "OMNIPRESENT", "now", ZSK2)
loadkeys()
ns2.rndc(f"dnssec -rollover -key {ZSKID2} {zone}", log=False)
with ns2.watch_log_from_start() as watcher:
watcher.wait_for_line(f"{ZSKID3} (ZSK) is now published")
ksk_remove()
isctest.log.info("update the zone again, requiring a resign of the SOA RRset")
up = update.UpdateMessage(f"{zone}.")
up.add(f"{zone}.", 300, "TXT", "added by UPDATE again")
up.add(f"{zone}.", 300, "A", "1.2.3.4")
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
isctest.log.info("redo the tests now that the ZSK roll has deleted the old key")
isctest.log.info("make sure certain types are signed with KSK only")
check_signing_keys(ksk_only_types, expect=[KSKID], prohibit=[ZSKID, ZSKID2, ZSKID3])
isctest.log.info("check A, TXT, SOA are signed with ZSK2 only")
def check_a_txt_soa_zsk2():
return check_signing_keys(
["A", "TXT", "SOA"], expect=[ZSKID2], prohibit=[KSKID, ZSKID, ZSKID3]
)
isctest.run.retry_with_timeout(check_a_txt_soa_zsk2, 5)
ksk_recover()
isctest.log.info("make ZSK3 active")
settime("-sKns2", "-Inow", ZSK2)
settime("-sKns2", "-k", "OMNIPRESENT", "now", "-Anow", ZSK3)
loadkeys()
with ns2.watch_log_from_start() as watcher:
watcher.wait_for_line(
[f"{ZSKID3} (ZSK) is now active", f"{ZSKID2} (ZSK) is now inactive"]
)
ksk_remove()
isctest.log.info("update the zone again, requiring a resign of the SOA RRset")
up = update.UpdateMessage(f"{zone}.")
up.add(f"{zone}.", 300, "TXT", "added by UPDATE one more time")
up.add(f"{zone}.", 300, "A", "4.3.2.1")
up.add(f"{zone}.", 300, "AAAA", "dead::beef")
res = isctest.query.tcp(up, "10.53.0.2")
isctest.check.noerror(res)
isctest.log.info("redo the tests one last time")
isctest.log.info("make sure certain types are signed with KSK only")
check_signing_keys(ksk_only_types, expect=[KSKID], prohibit=[ZSKID, ZSKID2, ZSKID3])
isctest.log.info("check A, TXT, SOA are signed with ZSK2 only")
def check_aaaa_a_txt_soa_zsk3():
return check_signing_keys(
["AAAA", "A", "TXT", "SOA"],
expect=[ZSKID3],
prohibit=[KSKID, ZSKID, ZSKID2],
)
isctest.run.retry_with_timeout(check_aaaa_a_txt_soa_zsk3, 5)

View file

@ -1084,12 +1084,6 @@ def test_validating_forwarder(servers):
def test_expired_signatures(servers):
# check expired signatures are still in place when updates are disabled
msg = isctest.query.create("expired.example", "SOA")
res = isctest.query.tcp(msg, "10.53.0.3")
soa, sigs = res.answer
assert sigs
# check expired signatures do not validate
msg = isctest.query.create("expired.example", "SOA")
res = isctest.query.tcp(msg, "10.53.0.3")