diff --git a/CHANGES b/CHANGES index 726e201b06..27848a6f95 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,6 @@ +5947. [func] Change dnssec-policy to allow graceful transition from + an NSEC only zone to NSEC3. [GL #3486] + 5946. [bug] Fix statistics channel's handling of multiple HTTP requests in a single connection which have non-empty request bodies. [GL #3463] diff --git a/bin/dnssec/dnssec-signzone.c b/bin/dnssec/dnssec-signzone.c index 889ca79ab5..1780a93ae8 100644 --- a/bin/dnssec/dnssec-signzone.c +++ b/bin/dnssec/dnssec-signzone.c @@ -3901,7 +3901,7 @@ main(int argc, char *argv[]) { hashlist_init(&hashlist, dns_db_nodecount(gdb, dns_dbtree_main) * 2, hash_length); - result = dns_nsec_nseconly(gdb, gversion, &answer); + result = dns_nsec_nseconly(gdb, gversion, NULL, &answer); if (result == ISC_R_NOTFOUND) { fprintf(stderr, "%s: warning: NSEC3 generation " diff --git a/bin/tests/system/checkconf/bad-kasp-nsec3-alg.conf b/bin/tests/system/checkconf/bad-kasp-nsec3-alg.conf new file mode 100644 index 0000000000..ff25ecea38 --- /dev/null +++ b/bin/tests/system/checkconf/bad-kasp-nsec3-alg.conf @@ -0,0 +1,25 @@ +/* + * 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-policy "badnsec3alg" { + keys { + csk lifetime unlimited algorithm rsasha1; + }; + nsec3param iterations 0 optout 0 salt-length 0; +}; + +zone "example.net" { + type primary; + file "example.db"; + dnssec-policy "badnsec3alg"; +}; diff --git a/bin/tests/system/nsec3/clean.sh b/bin/tests/system/nsec3/clean.sh index 6383f29bea..d6b11e749b 100644 --- a/bin/tests/system/nsec3/clean.sh +++ b/bin/tests/system/nsec3/clean.sh @@ -13,9 +13,10 @@ set -e -rm -f dig.out.* rndc.signing.* +rm -f dig.out.* rndc.signing.* verify.out.* rm -f ns*/named.conf ns*/named.memstats ns*/named.run* rm -f ns*/*.jnl ns*/*.jbk ns*/managed-keys.bind rm -f ns*/K*.private ns*/K*.key ns*/K*.state rm -f ns*/dsset-* ns*/*.db ns*/*.db.signed - +rm -f ns*/keygen.out.* ns*/settime.out.* +rm -f created.key-* *.created unused.key-* diff --git a/bin/tests/system/nsec3/ns3/named.conf.in b/bin/tests/system/nsec3/ns3/named.conf.in index 36c217ad3c..c94fa5d679 100644 --- a/bin/tests/system/nsec3/ns3/named.conf.in +++ b/bin/tests/system/nsec3/ns3/named.conf.in @@ -18,6 +18,12 @@ dnssec-policy "nsec" { // NSEC will be used; }; +dnssec-policy "rsasha1" { + keys { + csk lifetime unlimited algorithm rsasha1; + }; +}; + dnssec-policy "nsec3" { nsec3param; }; @@ -59,6 +65,56 @@ zone "nsec-to-nsec3.kasp" { dnssec-policy "nsec"; }; +/* + * This zone starts with NSEC, but will be reconfigured to use NSEC3. + * This should work despite the incompatible RSAHSHA1 algorithm, + * because the DS is still in hidden state. + */ +zone "rsasha1-to-nsec3.kasp" { + type primary; + file "rsasha1-to-nsec3.kasp.db"; + inline-signing yes; + dnssec-policy "rsasha1"; +}; + +/* + * This zone starts with NSEC, but will be reconfigured to use NSEC3. + * This should block because RSASHA1 is not compatible with NSEC3, + * and the DS is published. + */ +zone "rsasha1-to-nsec3-wait.kasp" { + type primary; + file "rsasha1-to-nsec3-wait.kasp.db"; + inline-signing yes; + dnssec-policy "rsasha1"; +}; + +/* + * This zone starts with NSEC3, but will be reconfigured to use NSEC with an + * NSEC only algorithm. This should work despite the incompatible RSAHSHA1 + * algorithm, because the DS is still in hidden state. + */ +zone "nsec3-to-rsasha1.kasp" { + type primary; + file "nsec3-to-rsasha1.kasp.db"; + inline-signing yes; + dnssec-policy "nsec3"; +}; + +/* + * This zone starts with NSEC3, but will be reconfigured to use NSEC with an + * NSEC only algorithm. This should also be fine because we are allowed + * to change to NSEC with any algorithm, then we can also publish the new + * DNSKEY and signatures of the RSASHA1 algorithm. + */ +zone "nsec3-to-rsasha1-ds.kasp" { + type primary; + file "nsec3-to-rsasha1-ds.kasp.db"; + inline-signing yes; + dnssec-policy "nsec3"; +}; + + /* These zones use the default NSEC3 settings. */ zone "nsec3.kasp" { type primary; diff --git a/bin/tests/system/nsec3/ns3/named2.conf.in b/bin/tests/system/nsec3/ns3/named2.conf.in index c81cd70049..d9764abcad 100644 --- a/bin/tests/system/nsec3/ns3/named2.conf.in +++ b/bin/tests/system/nsec3/ns3/named2.conf.in @@ -18,6 +18,12 @@ dnssec-policy "nsec" { // NSEC will be used; }; +dnssec-policy "rsasha1" { + keys { + csk lifetime unlimited algorithm rsasha1; + }; +}; + dnssec-policy "nsec3" { nsec3param; }; @@ -60,6 +66,59 @@ zone "nsec-to-nsec3.kasp" { dnssec-policy "nsec3"; }; +/* + * This zone starts with NSEC, but will be reconfigured to use NSEC3. + * This should work despite the incompatible RSAHSHA1 algorithm, + * because the DS is still in hidden state. + */ +zone "rsasha1-to-nsec3.kasp" { + type primary; + file "rsasha1-to-nsec3.kasp.db"; + inline-signing yes; + //dnssec-policy "rsasha1"; + dnssec-policy "nsec3"; +}; + +/* + * This zone starts with NSEC, but will be reconfigured to use NSEC3. + * This should block because RSASHA1 is not compatible with NSEC3, + * and the DS is published. + */ +zone "rsasha1-to-nsec3-wait.kasp" { + type primary; + file "rsasha1-to-nsec3-wait.kasp.db"; + inline-signing yes; + //dnssec-policy "rsasha1"; + dnssec-policy "nsec3"; +}; + +/* + * This zone starts with NSEC3, but will be reconfigured to use NSEC with an + * NSEC only algorithm. This should work despite the incompatible RSAHSHA1 + * algorithm, because the DS is still in hidden state. + */ +zone "nsec3-to-rsasha1.kasp" { + type primary; + file "nsec3-to-rsasha1.kasp.db"; + inline-signing yes; + //dnssec-policy "nsec3"; + dnssec-policy "rsasha1"; +}; + +/* + * This zone starts with NSEC3, but will be reconfigured to use NSEC with an + * NSEC only algorithm. This should also be fine because we are allowed + * to change to NSEC with any algorithm, then we can also publish the new + * DNSKEY and signatures of the RSASHA1 algorithm. + */ +zone "nsec3-to-rsasha1-ds.kasp" { + type primary; + file "nsec3-to-rsasha1-ds.kasp.db"; + inline-signing yes; + //dnssec-policy "nsec3"; + dnssec-policy "rsasha1"; +}; + /* These zones use the default NSEC3 settings. */ zone "nsec3.kasp" { type primary; diff --git a/bin/tests/system/nsec3/ns3/setup.sh b/bin/tests/system/nsec3/ns3/setup.sh index cbaf84ce8d..e2478ac3df 100644 --- a/bin/tests/system/nsec3/ns3/setup.sh +++ b/bin/tests/system/nsec3/ns3/setup.sh @@ -30,4 +30,29 @@ do setup "${zn}.kasp" done +if (cd ..; $SHELL ../testcrypto.sh -q RSASHA1) +then + for zn in rsasha1-to-nsec3 rsasha1-to-nsec3-wait nsec3-to-rsasha1 \ + nsec3-to-rsasha1-ds + do + setup "${zn}.kasp" + done + + longago="now-1y" + keytimes="-P ${longago} -A ${longago}" + O="omnipresent" + + zone="rsasha1-to-nsec3-wait.kasp" + CSK=$($KEYGEN -k "rsasha1" -l named.conf $keytimes $zone 2> keygen.out.$zone) + echo_i "Created key file $CSK" + $SETTIME -s -g $O -k $O $longago -r $O $longago -z $O $longago -d $O $longago "$CSK" > settime.out.$zone 2>&1 + + zone="nsec3-to-rsasha1-ds.kasp" + CSK=$($KEYGEN -k "default" -l named.conf $keytimes $zone 2> keygen.out.$zone) + echo_i "Created key file $CSK" + $SETTIME -s -g $O -k $O $longago -r $O $longago -z $O $longago -d $O $longago "$CSK" > settime.out.$zone 2>&1 +else + echo_i "skip: skip rsasha1 zones - signing with RSASHA1 not supported" +fi + cp nsec3-fails-to-load.kasp.db.in nsec3-fails-to-load.kasp.db diff --git a/bin/tests/system/nsec3/tests.sh b/bin/tests/system/nsec3/tests.sh index d9c2b83d17..1d9adbc3a2 100644 --- a/bin/tests/system/nsec3/tests.sh +++ b/bin/tests/system/nsec3/tests.sh @@ -36,6 +36,8 @@ rndccmd() { set_zone_policy() { ZONE=$1 POLICY=$2 + NUM_KEYS=$3 + DNSKEY_TTL=$4 } # Set expected NSEC3 parameters: flags ($1), iterations ($2), and # salt length ($3). @@ -47,6 +49,49 @@ set_nsec3param() { SALT="" } +# Set expected default dnssec-policy keys values. +set_key_default_values() { + key_clear $1 + + set_keyrole $1 "csk" + set_keylifetime $1 "0" + set_keyalgorithm $1 "13" "ECDSAP256SHA256" "256" + set_keysigning $1 "yes" + set_zonesigning $1 "yes" + + set_keystate $1 "GOAL" "omnipresent" + set_keystate $1 "STATE_DNSKEY" "rumoured" + set_keystate $1 "STATE_KRRSIG" "rumoured" + set_keystate $1 "STATE_ZRRSIG" "rumoured" + set_keystate $1 "STATE_DS" "hidden" +} + +# Set expected rsasha1 dnssec-policy keys values. +set_key_rsasha1_values() { + key_clear $1 + + set_keyrole $1 "csk" + set_keylifetime $1 "0" + set_keyalgorithm $1 "5" "RSASHA1" "2048" + set_keysigning $1 "yes" + set_zonesigning $1 "yes" + + set_keystate $1 "GOAL" "omnipresent" + set_keystate $1 "STATE_DNSKEY" "rumoured" + set_keystate $1 "STATE_KRRSIG" "rumoured" + set_keystate $1 "STATE_ZRRSIG" "rumoured" + set_keystate $1 "STATE_DS" "hidden" +} + +# Update the key states. +set_key_states() { + set_keystate $1 "GOAL" "$2" + set_keystate $1 "STATE_DNSKEY" "$3" + set_keystate $1 "STATE_KRRSIG" "$4" + set_keystate $1 "STATE_ZRRSIG" "$5" + set_keystate $1 "STATE_DS" "$6" +} + # The apex NSEC3PARAM record indicates that it is signed. _wait_for_nsec3param() { dig_with_opts +noquestion "@${SERVER}" "$ZONE" NSEC3PARAM > "dig.out.test$n.wait" || return 1 @@ -67,7 +112,7 @@ _wait_for_nsec() { wait_for_zone_is_signed() { n=$((n+1)) ret=0 - echo_i "wait for ${ZONE} to be signed ($n)" + echo_i "wait for ${ZONE} to be signed with $1 ($n)" if [ "$1" = "nsec3" ]; then retry_quiet 10 _wait_for_nsec3param || log_error "wait for ${ZONE} to be signed failed" @@ -79,24 +124,45 @@ wait_for_zone_is_signed() { status=$((status+ret)) } +# Test: check DNSSEC verify +_check_dnssec_verify() { + dig_with_opts @$SERVER "${ZONE}" AXFR > "dig.out.test$n.axfr.$ZONE" || return 1 + $VERIFY -z -o "$ZONE" "dig.out.test$n.axfr.$ZONE" > "verify.out.test$n.$ZONE" 2>&1 || return 1 + return 0 +} + # Test: check NSEC in answers -_check_nsec_nsec3param() -{ +_check_nsec_nsec3param() { dig_with_opts +noquestion @$SERVER "${ZONE}" NSEC3PARAM > "dig.out.test$n.nsec3param.$ZONE" || return 1 grep "NSEC3PARAM" "dig.out.test$n.nsec3param.$ZONE" > /dev/null && return 1 return 0 } -_check_nsec_nxdomain() -{ +_check_nsec_nxdomain() { dig_with_opts @$SERVER "nosuchname.${ZONE}" > "dig.out.test$n.nxdomain.$ZONE" || return 1 grep "${ZONE}.*IN.*NSEC.*NS.*SOA.*RRSIG.*NSEC.*DNSKEY" "dig.out.test$n.nxdomain.$ZONE" > /dev/null || return 1 grep "NSEC3" "dig.out.test$n.nxdomain.$ZONE" > /dev/null && return 1 return 0 } -check_nsec() -{ +check_nsec() { + wait_for_zone_is_signed "nsec" + + n=$((n+1)) + echo_i "check DNSKEY rrset is signed correctly for zone ${ZONE} ($n)" + ret=0 + check_keys + retry_quiet 10 _check_apex_dnskey || log_error "bad DNSKEY RRset for zone ${ZONE}" + test "$ret" -eq 0 || echo_i "failed" + status=$((status+ret)) + + n=$((n+1)) + echo_i "verify DNSSEC for zone ${ZONE} ($n)" + ret=0 + retry_quiet 10 _check_dnssec_verify || log_error "DNSSEC verify failed for zone ${ZONE}" + test "$ret" -eq 0 || echo_i "failed" + status=$((status+ret)) + n=$((n+1)) echo_i "check NSEC3PARAM response for zone ${ZONE} ($n)" ret=0 @@ -113,8 +179,7 @@ check_nsec() } # Test: check NSEC3 parameters in answers -_check_nsec3_nsec3param() -{ +_check_nsec3_nsec3param() { dig_with_opts +noquestion @$SERVER "${ZONE}" NSEC3PARAM > "dig.out.test$n.nsec3param.$ZONE" || return 1 grep "${ZONE}.*0.*IN.*NSEC3PARAM.*1.*0.*${ITERATIONS}.*${SALT}" "dig.out.test$n.nsec3param.$ZONE" > /dev/null || return 1 @@ -124,15 +189,15 @@ _check_nsec3_nsec3param() return 0 } -_check_nsec3_nxdomain() -{ +_check_nsec3_nxdomain() { dig_with_opts @$SERVER "nosuchname.${ZONE}" > "dig.out.test$n.nxdomain.$ZONE" || return 1 grep ".*\.${ZONE}.*IN.*NSEC3.*1.${FLAGS}.*${ITERATIONS}.*${SALT}" "dig.out.test$n.nxdomain.$ZONE" > /dev/null || return 1 return 0 } -check_nsec3() -{ +check_nsec3() { + wait_for_zone_is_signed "nsec3" + n=$((n+1)) echo_i "check that NSEC3PARAM 1 0 ${ITERATIONS} is published zone ${ZONE} ($n)" ret=0 @@ -146,74 +211,119 @@ check_nsec3() retry_quiet 10 _check_nsec3_nxdomain || log_error "bad NXDOMAIN response for zone ${ZONE}" test "$ret" -eq 0 || echo_i "failed" status=$((status+ret)) + + n=$((n+1)) + echo_i "verify DNSSEC for zone ${ZONE} ($n)" + ret=0 + retry_quiet 10 _check_dnssec_verify || log_error "DNSSEC verify failed for zone ${ZONE}" + test "$ret" -eq 0 || echo_i "failed" + status=$((status+ret)) } start_time="$(TZ=UTC date +%s)" status=0 n=0 +key_clear "KEY1" +key_clear "KEY2" +key_clear "KEY3" +key_clear "KEY4" + # Zone: nsec-to-nsec3.kasp. -set_zone_policy "nsec-to-nsec3.kasp" "nsec" +set_zone_policy "nsec-to-nsec3.kasp" "nsec" 1 3600 set_server "ns3" "10.53.0.3" +set_key_default_values "KEY1" echo_i "initial check zone ${ZONE}" check_nsec -dnssec_verify + +if ($SHELL ../testcrypto.sh -q RSASHA1) +then + # Zone: rsasha1-to-nsec3.kasp. + set_zone_policy "rsasha1-to-nsec3.kasp" "rsasha1" 1 3600 + set_server "ns3" "10.53.0.3" + set_key_rsasha1_values "KEY1" + echo_i "initial check zone ${ZONE}" + check_nsec + + # Zone: rsasha1-to-nsec3-wait.kasp. + set_zone_policy "rsasha1-to-nsec3-wait.kasp" "rsasha1" 1 3600 + set_server "ns3" "10.53.0.3" + set_key_rsasha1_values "KEY1" + set_key_states "KEY1" "omnipresent" "omnipresent" "omnipresent" "omnipresent" "omnipresent" + echo_i "initial check zone ${ZONE}" + check_nsec + + # Zone: nsec3-to-rsasha1.kasp. + set_zone_policy "nsec3-to-rsasha1.kasp" "nsec3" 1 3600 + set_server "ns3" "10.53.0.3" + set_key_rsasha1_values "KEY1" + echo_i "initial check zone ${ZONE}" + check_nsec3 + + # Zone: nsec3-to-rsasha1-ds.kasp. + set_zone_policy "nsec3-to-rsasha1-ds.kasp" "nsec3" 1 3600 + set_server "ns3" "10.53.0.3" + set_key_rsasha1_values "KEY1" + set_key_states "KEY1" "omnipresent" "omnipresent" "omnipresent" "omnipresent" "omnipresent" + echo_i "initial check zone ${ZONE}" + check_nsec3 +fi # Zone: nsec3.kasp. -set_zone_policy "nsec3.kasp" "nsec3" +set_zone_policy "nsec3.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "initial check zone ${ZONE}" check_nsec3 -dnssec_verify # Zone: nsec3-dynamic.kasp. -set_zone_policy "nsec3-dynamic.kasp" "nsec3" +set_zone_policy "nsec3-dynamic.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "initial check zone ${ZONE}" check_nsec3 -dnssec_verify # Zone: nsec3-change.kasp. -set_zone_policy "nsec3-change.kasp" "nsec3" +set_zone_policy "nsec3-change.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "initial check zone ${ZONE}" check_nsec3 -dnssec_verify # Zone: nsec3-dynamic-change.kasp. -set_zone_policy "nsec3-dynamic-change.kasp" "nsec3" +set_zone_policy "nsec3-dynamic-change.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "initial check zone ${ZONE}" check_nsec3 -dnssec_verify # Zone: nsec3-to-nsec.kasp. -set_zone_policy "nsec3-to-nsec.kasp" "nsec3" +set_zone_policy "nsec3-to-nsec.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "initial check zone ${ZONE}" check_nsec3 -dnssec_verify # Zone: nsec3-to-optout.kasp. -set_zone_policy "nsec3-to-optout.kasp" "nsec3" +set_zone_policy "nsec3-to-optout.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "initial check zone ${ZONE}" check_nsec3 -dnssec_verify # Zone: nsec3-from-optout.kasp. -set_zone_policy "nsec3-from-optout.kasp" "optout" +set_zone_policy "nsec3-from-optout.kasp" "optout" 1 3600 set_nsec3param "1" "0" "0" +set_key_default_values "KEY1" echo_i "initial check zone ${ZONE}" check_nsec3 -dnssec_verify # Zone: nsec3-other.kasp. -set_zone_policy "nsec3-other.kasp" "nsec3-other" +set_zone_policy "nsec3-other.kasp" "nsec3-other" 1 3600 set_nsec3param "1" "11" "8" +set_key_default_values "KEY1" echo_i "initial check zone ${ZONE}" check_nsec3 -dnssec_verify # Reconfig named. echo_i "reconfig dnssec-policy to trigger nsec3 rollovers" @@ -221,88 +331,141 @@ copy_setports ns3/named2.conf.in ns3/named.conf rndc_reconfig ns3 10.53.0.3 # Zone: nsec-to-nsec3.kasp. (reconfigured) -set_zone_policy "nsec-to-nsec3.kasp" "nsec3" +set_zone_policy "nsec-to-nsec3.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "check zone ${ZONE} after reconfig" check_nsec3 -dnssec_verify + +if ($SHELL ../testcrypto.sh -q RSASHA1) +then + # Zone: rsasha1-to-nsec3.kasp. + set_zone_policy "rsasha1-to-nsec3.kasp" "nsec3" 2 3600 + set_server "ns3" "10.53.0.3" + set_key_rsasha1_values "KEY1" + set_key_states "KEY1" "hidden" "unretentive" "unretentive" "unretentive" "hidden" + set_keysigning "KEY1" "no" + set_zonesigning "KEY1" "no" + set_key_default_values "KEY2" + echo_i "check zone ${ZONE} after reconfig" + check_nsec3 + + # Zone: rsasha1-to-nsec3-wait.kasp. + set_zone_policy "rsasha1-to-nsec3-wait.kasp" "nsec3" 2 3600 + set_server "ns3" "10.53.0.3" + set_key_rsasha1_values "KEY1" + set_key_states "KEY1" "hidden" "omnipresent" "omnipresent" "omnipresent" "omnipresent" + set_key_default_values "KEY2" + echo_i "check zone ${ZONE} after reconfig" + + ret=0 + wait_for_log 10 "zone $ZONE/IN (signed): wait building NSEC3 chain until NSEC only DNSKEYs are removed" ns3/named.run || ret=1 + test "$ret" -eq 0 || echo_i "failed" + status=$((status+ret)) + + check_nsec + + # Zone: nsec3-to-rsasha1.kasp. + set_zone_policy "nsec3-to-rsasha1.kasp" "rsasha1" 2 3600 + set_nsec3param "1" "0" "0" + set_server "ns3" "10.53.0.3" + set_key_default_values "KEY1" + set_key_states "KEY1" "hidden" "unretentive" "unretentive" "unretentive" "hidden" + set_keysigning "KEY1" "no" + set_zonesigning "KEY1" "no" + set_key_rsasha1_values "KEY2" + echo_i "check zone ${ZONE} after reconfig" + check_nsec + + # Zone: nsec3-to-rsasha1-ds.kasp. + set_zone_policy "nsec3-to-rsasha1-ds.kasp" "rsasha1" 2 3600 + set_nsec3param "1" "0" "0" + set_server "ns3" "10.53.0.3" + set_key_default_values "KEY1" + set_key_states "KEY1" "hidden" "omnipresent" "omnipresent" "omnipresent" "omnipresent" + set_key_rsasha1_values "KEY2" + echo_i "check zone ${ZONE} after reconfig" + check_nsec + + key_clear "KEY1" + key_clear "KEY2" +fi # Zone: nsec3.kasp. (same) -set_zone_policy "nsec3.kasp" "nsec3" +set_zone_policy "nsec3.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "check zone ${ZONE} after reconfig" check_nsec3 -dnssec_verify # Zone: nsec3-dyamic.kasp. (same) -set_zone_policy "nsec3-dynamic.kasp" "nsec3" +set_zone_policy "nsec3-dynamic.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "check zone ${ZONE} after reconfig" check_nsec3 -dnssec_verify # Zone: nsec3-change.kasp. (reconfigured) -set_zone_policy "nsec3-change.kasp" "nsec3-other" +set_zone_policy "nsec3-change.kasp" "nsec3-other" 1 3600 set_nsec3param "1" "11" "8" +set_key_default_values "KEY1" echo_i "check zone ${ZONE} after reconfig" check_nsec3 -dnssec_verify # Zone: nsec3-dynamic-change.kasp. (reconfigured) -set_zone_policy "nsec3-dynamic-change.kasp" "nsec3-other" +set_zone_policy "nsec3-dynamic-change.kasp" "nsec3-other" 1 3600 set_nsec3param "1" "11" "8" +set_key_default_values "KEY1" echo_i "check zone ${ZONE} after reconfig" check_nsec3 -dnssec_verify # Zone: nsec3-to-nsec.kasp. (reconfigured) -set_zone_policy "nsec3-to-nsec.kasp" "nsec" +set_zone_policy "nsec3-to-nsec.kasp" "nsec" 1 3600 set_nsec3param "1" "11" "8" +set_key_default_values "KEY1" echo_i "check zone ${ZONE} after reconfig" check_nsec -dnssec_verify # Zone: nsec3-to-optout.kasp. (reconfigured) # DISABLED: # There is a bug in the nsec3param building code that thinks when the # optout bit is changed, the chain already exists. [GL #2216] -#set_zone_policy "nsec3-to-optout.kasp" "optout" +#set_zone_policy "nsec3-to-optout.kasp" "optout" 1 3600 #set_nsec3param "1" "0" "0" +#set_key_default_values "KEY1" #echo_i "check zone ${ZONE} after reconfig" #check_nsec3 -#dnssec_verify # Zone: nsec3-from-optout.kasp. (reconfigured) # DISABLED: # There is a bug in the nsec3param building code that thinks when the # optout bit is changed, the chain already exists. [GL #2216] -#set_zone_policy "nsec3-from-optout.kasp" "nsec3" +#set_zone_policy "nsec3-from-optout.kasp" "nsec3" 1 3600 #set_nsec3param "0" "0" "0" +#set_key_default_values "KEY1" #echo_i "check zone ${ZONE} after reconfig" #check_nsec3 -#dnssec_verify # Zone: nsec3-other.kasp. (same) -set_zone_policy "nsec3-other.kasp" "nsec3-other" +set_zone_policy "nsec3-other.kasp" "nsec3-other" 1 3600 set_nsec3param "1" "11" "8" +set_key_default_values "KEY1" echo_i "check zone ${ZONE} after reconfig" check_nsec3 -dnssec_verify # Using rndc signing -nsec3param (should fail) -set_zone_policy "nsec3-change.kasp" "nsec3-other" +set_zone_policy "nsec3-change.kasp" "nsec3-other" 1 3600 echo_i "use rndc signing -nsec3param ${ZONE} to change NSEC3 settings" rndccmd $SERVER signing -nsec3param 1 1 12 ffff $ZONE > rndc.signing.test$n.$ZONE || log_error "failed to call rndc signing -nsec3param $ZONE" grep "zone uses dnssec-policy, use rndc dnssec command instead" rndc.signing.test$n.$ZONE > /dev/null || log_error "rndc signing -nsec3param should fail" check_nsec3 -dnssec_verify # Test NSEC3 and NSEC3PARAM is the same after restart -set_zone_policy "nsec3.kasp" "nsec3" +set_zone_policy "nsec3.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "check zone ${ZONE} before restart" check_nsec3 -dnssec_verify # Restart named, NSEC3 should stay the same. ret=0 @@ -318,22 +481,22 @@ test "$ret" -eq 0 || echo_i "failed" status=$((status+ret)) prevsalt="${SALT}" -set_zone_policy "nsec3.kasp" "nsec3" +set_zone_policy "nsec3.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" SALT="${prevsalt}" echo_i "check zone ${ZONE} after restart has salt ${SALT}" check_nsec3 -dnssec_verify # Zone: nsec3-fails-to-load.kasp. (should be fixed after reload) cp ns3/template.db.in ns3/nsec3-fails-to-load.kasp.db rndc_reload ns3 10.53.0.3 -set_zone_policy "nsec3-fails-to-load.kasp" "nsec3" +set_zone_policy "nsec3-fails-to-load.kasp" "nsec3" 1 3600 set_nsec3param "0" "0" "0" +set_key_default_values "KEY1" echo_i "check zone ${ZONE} after reload" check_nsec3 -dnssec_verify echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/doc/notes/notes-current.rst b/doc/notes/notes-current.rst index ca4a6b79b6..ad45e432bc 100644 --- a/doc/notes/notes-current.rst +++ b/doc/notes/notes-current.rst @@ -38,6 +38,12 @@ Feature Changes - Zones using ``dnssec-policy`` now require dynamic DNS or ``inline-signing`` to be configured explicitly :gl:`#3381`. +- When reconfiguring ``dnssec-policy`` from using NSEC with an NSEC-only DNSKEY + algorithm (e.g. RSASHA1) to a policy that uses NSEC3, BIND will no longer fail + to sign the zone, but keep using NSEC for a little longer until the offending + DNSKEY records have been removed from the zone, then switch to using NSEC3. + :gl:`#3486` + Bug Fixes ~~~~~~~~~ diff --git a/lib/dns/include/dns/nsec.h b/lib/dns/include/dns/nsec.h index 04029e2ff6..e68ea35ebf 100644 --- a/lib/dns/include/dns/nsec.h +++ b/lib/dns/include/dns/nsec.h @@ -19,6 +19,7 @@ #include +#include #include #include @@ -60,11 +61,15 @@ dns_nsec_typepresent(dns_rdata_t *nsec, dns_rdatatype_t type); */ isc_result_t -dns_nsec_nseconly(dns_db_t *db, dns_dbversion_t *version, bool *answer); +dns_nsec_nseconly(dns_db_t *db, dns_dbversion_t *version, dns_diff_t *diff, + bool *answer); /* * Report whether the DNSKEY RRset has a NSEC only algorithm. Unknown * algorithms are assumed to support NSEC3. If DNSKEY is not found, * *answer is set to false, and ISC_R_NOTFOUND is returned. + * If 'diff' is provided, check if the NSEC only DNSKEY will be deleted. + * If so, and there are no other NSEC only DNSKEYs that will stay in 'db', + * consider the DNSKEY RRset to have no NSEC only DNSKEYs. * * Requires: * 'answer' to be non NULL. diff --git a/lib/dns/include/dns/zone.h b/lib/dns/include/dns/zone.h index c9a1487928..0f035cf5a8 100644 --- a/lib/dns/include/dns/zone.h +++ b/lib/dns/include/dns/zone.h @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -2761,3 +2762,20 @@ dns_zone_gettid(dns_zone_t *zone); * * \return thread id associated with the zone */ + +bool +dns_zone_check_dnskey_nsec3(dns_zone_t *zone, dns_db_t *db, + dns_dbversion_t *ver, dns_diff_t *diff, + dst_key_t **keys, unsigned int numkeys); +/**< + * Return whether the zone would enter an inconsistent state where NSEC only + * DNSKEYs are present along NSEC3 chains. + * + * Requires: + * \li 'zone' to be a valid zone. + * \li 'db'is not NULL. + * + * Returns: + * \li 'true' if the check passes, that is the zone remains consistent, + * 'false' if the zone would have NSEC only DNSKEYs and an NSEC3 chain. + */ diff --git a/lib/dns/nsec.c b/lib/dns/nsec.c index afbe9c80f9..84c47d6c21 100644 --- a/lib/dns/nsec.c +++ b/lib/dns/nsec.c @@ -247,7 +247,8 @@ dns_nsec_typepresent(dns_rdata_t *nsec, dns_rdatatype_t type) { } isc_result_t -dns_nsec_nseconly(dns_db_t *db, dns_dbversion_t *version, bool *answer) { +dns_nsec_nseconly(dns_db_t *db, dns_dbversion_t *version, dns_diff_t *diff, + bool *answer) { dns_dbnode_t *node = NULL; dns_rdataset_t rdataset; dns_rdata_dnskey_t dnskey; @@ -282,8 +283,35 @@ dns_nsec_nseconly(dns_db_t *db, dns_dbversion_t *version, bool *answer) { RUNTIME_CHECK(result == ISC_R_SUCCESS); if (dnskey.algorithm == DST_ALG_RSAMD5 || - dnskey.algorithm == DST_ALG_RSASHA1) { - break; + dnskey.algorithm == DST_ALG_DH || + dnskey.algorithm == DST_ALG_DSA || + dnskey.algorithm == DST_ALG_RSASHA1) + { + bool deleted = false; + if (diff != NULL) { + for (dns_difftuple_t *tuple = + ISC_LIST_HEAD(diff->tuples); + tuple != NULL; + tuple = ISC_LIST_NEXT(tuple, link)) + { + if (tuple->rdata.type != + dns_rdatatype_dnskey || + tuple->op != DNS_DIFFOP_DEL) { + continue; + } + + if (dns_rdata_compare( + &rdata, &tuple->rdata) == 0) + { + deleted = true; + break; + } + } + } + + if (!deleted) { + break; + } } } dns_rdataset_disassociate(&rdataset); diff --git a/lib/dns/zone.c b/lib/dns/zone.c index d932b1b841..600fad94e5 100644 --- a/lib/dns/zone.c +++ b/lib/dns/zone.c @@ -3726,7 +3726,7 @@ zone_addnsec3chain(dns_zone_t *zone, dns_rdata_nsec3param_t *nsec3param) { * latter to exist in the first place. */ dns_db_currentversion(db, &version); - result = dns_nsec_nseconly(db, version, &nseconly); + result = dns_nsec_nseconly(db, version, NULL, &nseconly); nsec3ok = (result == ISC_R_SUCCESS && !nseconly); dns_db_closeversion(db, &version, false); if (!nsec3ok && (nsec3param->flags & DNS_NSEC3FLAG_REMOVE) == 0) { @@ -3918,7 +3918,7 @@ resume_addnsec3chain(dns_zone_t *zone) { * In order to create NSEC3 chains we need the DNSKEY RRset at zone * apex to exist and contain no keys using NSEC-only algorithms. */ - result = dns_nsec_nseconly(db, version, &nseconly); + result = dns_nsec_nseconly(db, version, NULL, &nseconly); nsec3ok = (result == ISC_R_SUCCESS && !nseconly); /* @@ -8186,7 +8186,7 @@ try_private: goto add; } - result = dns_nsec_nseconly(db, ver, &nseconly); + result = dns_nsec_nseconly(db, ver, diff, &nseconly); nsec3ok = (result == ISC_R_SUCCESS && !nseconly); /* @@ -9508,6 +9508,105 @@ failure: return (result); } +/* + * Prevent the zone entering a inconsistent state where + * NSEC only DNSKEYs are present with NSEC3 chains. + */ +bool +dns_zone_check_dnskey_nsec3(dns_zone_t *zone, dns_db_t *db, + dns_dbversion_t *ver, dns_diff_t *diff, + dst_key_t **keys, unsigned int numkeys) { + uint8_t alg; + dns_rdatatype_t privatetype; + ; + bool nseconly = false, nsec3 = false; + isc_result_t result; + + REQUIRE(DNS_ZONE_VALID(zone)); + REQUIRE(db != NULL); + + privatetype = dns_zone_getprivatetype(zone); + + /* Scan the tuples for an NSEC-only DNSKEY */ + if (diff != NULL) { + for (dns_difftuple_t *tuple = ISC_LIST_HEAD(diff->tuples); + tuple != NULL; tuple = ISC_LIST_NEXT(tuple, link)) + { + if (nseconly && nsec3) { + break; + } + + if (tuple->op != DNS_DIFFOP_ADD) { + continue; + } + + if (tuple->rdata.type == dns_rdatatype_nsec3param) { + nsec3 = true; + } + + if (tuple->rdata.type != dns_rdatatype_dnskey) { + continue; + } + + alg = tuple->rdata.data[3]; + if (alg == DNS_KEYALG_RSAMD5 || alg == DNS_KEYALG_DH || + alg == DNS_KEYALG_DSA || alg == DNS_KEYALG_RSASHA1) + { + nseconly = true; + } + } + } + /* Scan the zone keys for an NSEC-only DNSKEY */ + if (keys != NULL && !nseconly) { + for (unsigned int i = 0; i < numkeys; i++) { + alg = dst_key_alg(keys[i]); + if (alg == DNS_KEYALG_RSAMD5 || alg == DNS_KEYALG_DH || + alg == DNS_KEYALG_DSA || alg == DNS_KEYALG_RSASHA1) + { + nseconly = true; + break; + } + } + } + + /* Check DB for NSEC-only DNSKEY */ + if (!nseconly) { + result = dns_nsec_nseconly(db, ver, diff, &nseconly); + /* + * Adding an NSEC3PARAM record can proceed without a + * DNSKEY (it will trigger a delayed change), so we can + * ignore ISC_R_NOTFOUND here. + */ + if (result == ISC_R_NOTFOUND) { + result = ISC_R_SUCCESS; + } + CHECK(result); + } + + /* Check existing DB for NSEC3 */ + if (!nsec3) { + CHECK(dns_nsec3_activex(db, ver, false, privatetype, &nsec3)); + } + + /* Check kasp for NSEC3PARAM settings */ + if (!nsec3) { + dns_kasp_t *kasp = dns_zone_getkasp(zone); + if (kasp != NULL) { + nsec3 = dns_kasp_nsec3(kasp); + } + } + + /* Refuse to allow NSEC3 with NSEC-only keys */ + if (nseconly && nsec3) { + goto failure; + } + + return (true); + +failure: + return (false); +} + /* * Incrementally sign the zone using the keys requested. * Builds the NSEC chain if required. @@ -9643,6 +9742,15 @@ zone_sign(dns_zone_t *zone) { /* Determine which type of chain to build */ if (use_kasp) { build_nsec3 = dns_kasp_nsec3(kasp); + if (!dns_zone_check_dnskey_nsec3(zone, db, version, NULL, + (dst_key_t **)&zone_keys, + nkeys)) + { + dnssec_log(zone, ISC_LOG_INFO, + "wait building NSEC3 chain until NSEC only " + "DNSKEYs are removed"); + build_nsec3 = false; + } build_nsec = !build_nsec3; } else { CHECK(dns_private_chains(db, version, zone->privatetype, @@ -20506,63 +20614,6 @@ failure: return (result); } -/* - * Prevent the zone entering a inconsistent state where - * NSEC only DNSKEYs are present with NSEC3 chains. - * See update.c:check_dnssec() - */ -static bool -dnskey_sane(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver, - dns_diff_t *diff) { - isc_result_t result; - dns_difftuple_t *tuple; - bool nseconly = false, nsec3 = false; - dns_rdatatype_t privatetype = dns_zone_getprivatetype(zone); - - /* Scan the tuples for an NSEC-only DNSKEY */ - for (tuple = ISC_LIST_HEAD(diff->tuples); tuple != NULL; - tuple = ISC_LIST_NEXT(tuple, link)) - { - uint8_t alg; - if (tuple->rdata.type != dns_rdatatype_dnskey || - tuple->op != DNS_DIFFOP_ADD) { - continue; - } - - alg = tuple->rdata.data[3]; - if (alg == DST_ALG_RSASHA1) { - nseconly = true; - break; - } - } - - /* Check existing DB for NSEC-only DNSKEY */ - if (!nseconly) { - result = dns_nsec_nseconly(db, ver, &nseconly); - if (result == ISC_R_NOTFOUND) { - result = ISC_R_SUCCESS; - } - CHECK(result); - } - - /* Check existing DB for NSEC3 */ - if (!nsec3) { - CHECK(dns_nsec3_activex(db, ver, false, privatetype, &nsec3)); - } - - /* Refuse to allow NSEC3 with NSEC-only keys */ - if (nseconly && nsec3) { - dnssec_log(zone, ISC_LOG_ERROR, - "NSEC only DNSKEYs and NSEC3 chains not allowed"); - goto failure; - } - - return (true); - -failure: - return (false); -} - static isc_result_t clean_nsec3param(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver, dns_diff_t *diff) { @@ -21558,6 +21609,7 @@ zone_rekey(dns_zone_t *zone) { if (result == ISC_R_SUCCESS) { bool cdsdel = false; bool cdnskeydel = false; + bool sane_diff, sane_dnskey; isc_stdtime_t when; /* @@ -21726,9 +21778,21 @@ zone_rekey(dns_zone_t *zone) { } } - if ((newactive || fullsign || !ISC_LIST_EMPTY(diff.tuples)) && - dnskey_sane(zone, db, ver, &diff)) - { + /* + * A sane diff is one that is not empty, and that does not + * introduce a zone with NSEC only DNSKEYs along with NSEC3 + * chains. + */ + sane_dnskey = dns_zone_check_dnskey_nsec3(zone, db, ver, &diff, + NULL, 0); + sane_diff = !ISC_LIST_EMPTY(diff.tuples) && sane_dnskey; + if (!sane_dnskey) { + dnssec_log(zone, ISC_LOG_ERROR, + "NSEC only DNSKEYs and NSEC3 chains not " + "allowed"); + } + + if (newactive || fullsign || sane_diff) { CHECK(dns_diff_apply(&diff, db, ver)); CHECK(clean_nsec3param(zone, db, ver, &diff)); CHECK(add_signing_records(db, zone->privatetype, ver, @@ -22913,7 +22977,7 @@ rss_post(dns_zone_t *zone, isc_event_t *event) { dns_rdata_init(&rdata); np->data[2] |= DNS_NSEC3FLAG_CREATE; - result = dns_nsec_nseconly(db, newver, &nseconly); + result = dns_nsec_nseconly(db, newver, NULL, &nseconly); if (result == ISC_R_NOTFOUND || nseconly) { np->data[2] |= DNS_NSEC3FLAG_INITIAL; } diff --git a/lib/ns/update.c b/lib/ns/update.c index 28b59a5484..4b32b7508f 100644 --- a/lib/ns/update.c +++ b/lib/ns/update.c @@ -2096,56 +2096,12 @@ failure: static isc_result_t check_dnssec(ns_client_t *client, dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver, dns_diff_t *diff) { - dns_difftuple_t *tuple; - bool nseconly = false, nsec3 = false; isc_result_t result; unsigned int iterations = 0; dns_rdatatype_t privatetype = dns_zone_getprivatetype(zone); - /* Scan the tuples for an NSEC-only DNSKEY or an NSEC3PARAM */ - for (tuple = ISC_LIST_HEAD(diff->tuples); tuple != NULL; - tuple = ISC_LIST_NEXT(tuple, link)) - { - if (tuple->op != DNS_DIFFOP_ADD) { - continue; - } - - if (tuple->rdata.type == dns_rdatatype_dnskey) { - uint8_t alg; - alg = tuple->rdata.data[3]; - if (alg == DST_ALG_RSASHA1) { - nseconly = true; - break; - } - } else if (tuple->rdata.type == dns_rdatatype_nsec3param) { - nsec3 = true; - break; - } - } - - /* Check existing DB for NSEC-only DNSKEY */ - if (!nseconly) { - result = dns_nsec_nseconly(db, ver, &nseconly); - - /* - * An NSEC3PARAM update can proceed without a DNSKEY (it - * will trigger a delayed change), so we can ignore - * ISC_R_NOTFOUND here. - */ - if (result == ISC_R_NOTFOUND) { - result = ISC_R_SUCCESS; - } - - CHECK(result); - } - - /* Check existing DB for NSEC3 */ - if (!nsec3) { - CHECK(dns_nsec3_activex(db, ver, false, privatetype, &nsec3)); - } - /* Refuse to allow NSEC3 with NSEC-only keys */ - if (nseconly && nsec3) { + if (!dns_zone_check_dnskey_nsec3(zone, db, ver, diff, NULL, 0)) { update_log(client, zone, ISC_LOG_ERROR, "NSEC only DNSKEYs and NSEC3 chains not allowed"); result = DNS_R_REFUSED; @@ -2346,8 +2302,11 @@ add_nsec3param_records(ns_client_t *client, dns_zone_t *zone, dns_db_t *db, * supporting an NSEC3 chain, then we set the * INITIAL flag to indicate that these parameters * are to be used later. + * + * Don't provide a 'diff' here because we want to + * know the capability of the current database. */ - result = dns_nsec_nseconly(db, ver, &nseconly); + result = dns_nsec_nseconly(db, ver, NULL, &nseconly); if (result == ISC_R_NOTFOUND || nseconly) { buf[2] |= DNS_NSEC3FLAG_INITIAL; }