diff --git a/bin/tests/system/isctest/kasp.py b/bin/tests/system/isctest/kasp.py index 1b82baf416..0ea1c774ea 100644 --- a/bin/tests/system/isctest/kasp.py +++ b/bin/tests/system/isctest/kasp.py @@ -24,6 +24,7 @@ import dns import dns.tsig import isctest.log import isctest.query +import isctest.util DEFAULT_TTL = 300 @@ -612,7 +613,7 @@ def check_zone_is_signed(server, zone, tsig=None): assert signed -def verify_keys(zone, keys, expected): +def check_keys(zone, keys, expected): """ Checks keys for a configured zone. This verifies: 1. The expected number of keys exist in 'keys'. @@ -971,16 +972,13 @@ def check_apex(server, zone, ksks, zsks, tsig=None): # test dnskey query dnskeys, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.DNSKEY, tsig=tsig) - assert len(dnskeys) > 0 check_dnskeys(dnskeys, ksks, zsks) - assert len(rrsigs) > 0 check_signatures(rrsigs, dns.rdatatype.DNSKEY, fqdn, ksks, zsks) # test soa query soa, rrsigs = _query_rrset(server, fqdn, dns.rdatatype.SOA, tsig=tsig) assert len(soa) == 1 assert f"{zone}. {DEFAULT_TTL} IN SOA" in soa[0].to_text() - assert len(rrsigs) > 0 check_signatures(rrsigs, dns.rdatatype.SOA, fqdn, ksks, zsks) # test cdnskey query @@ -1016,10 +1014,38 @@ def check_subdomain(server, zone, ksks, zsks, tsig=None): else: assert match in rrset.to_text() - assert len(rrsigs) > 0 check_signatures(rrsigs, qtype, fqdn, ksks, zsks) +def verify_update_is_signed(server, fqdn, qname, qtype, rdata, ksks, zsks, tsig=None): + """ + Test an RRset below the apex and verify it is updated and signed correctly. + """ + response = _query(server, qname, qtype, tsig=tsig) + + if response.rcode() != dns.rcode.NOERROR: + return False + + rrtype = dns.rdatatype.to_text(qtype) + match = f"{qname} {DEFAULT_TTL} IN {rrtype} {rdata}" + rrsigs = [] + for rrset in response.answer: + if rrset.match( + dns.name.from_text(qname), dns.rdataclass.IN, dns.rdatatype.RRSIG, qtype + ): + rrsigs.append(rrset) + elif not match in rrset.to_text(): + return False + + if len(rrsigs) == 0: + return False + + # Zone is updated, ready to verify the signatures. + check_signatures(rrsigs, qtype, fqdn, ksks, zsks) + + return True + + def next_key_event_equals(server, zone, next_event): if next_event is None: # No next key event check. @@ -1101,3 +1127,72 @@ def keydir_to_keylist( def keystr_to_keylist(keystr: str, keydir: Optional[str] = None) -> List[Key]: return [Key(name, keydir) for name in keystr.split()] + + +def policy_to_properties(ttl, keys: List[str]) -> List[KeyProperties]: + """ + Get the policies from a list of specially formatted strings. + The splitted line should result in the following items: + line[0]: Role + line[1]: Lifetime + line[2]: Algorithm + line[3]: Length + Then, optional data for specific tests may follow: + - "goal", "dnskey", "krrsig", "zrrsig", "ds", followed by a value, + sets the given state to the specific value + - "offset", an offset for testing key rollover timings + """ + proplist = [] + count = 0 + for key in keys: + count += 1 + line = key.split() + keyprop = KeyProperties(f"KEY{count}", {}, {}, {}) + keyprop.properties["expect"] = True + keyprop.properties["private"] = True + keyprop.properties["legacy"] = False + keyprop.properties["offset"] = timedelta(0) + keyprop.properties["role"] = line[0] + if line[0] == "zsk": + keyprop.properties["role_full"] = "zone-signing" + keyprop.properties["flags"] = 256 + keyprop.metadata["ZSK"] = "yes" + keyprop.metadata["KSK"] = "no" + else: + keyprop.properties["role_full"] = "key-signing" + keyprop.properties["flags"] = 257 + keyprop.metadata["ZSK"] = "yes" if line[0] == "csk" else "no" + keyprop.metadata["KSK"] = "yes" + + keyprop.properties["dnskey_ttl"] = ttl + keyprop.metadata["Algorithm"] = line[2] + keyprop.metadata["Length"] = line[3] + keyprop.metadata["Lifetime"] = 0 + if line[1] != "unlimited": + keyprop.metadata["Lifetime"] = int(line[1]) + + for i in range(4, len(line)): + if line[i].startswith("goal:"): + keyval = line[i].split(":") + keyprop.metadata["GoalState"] = keyval[1] + elif line[i].startswith("dnskey:"): + keyval = line[i].split(":") + keyprop.metadata["DNSKEYState"] = keyval[1] + elif line[i].startswith("krrsig:"): + keyval = line[i].split(":") + keyprop.metadata["KRRSIGState"] = keyval[1] + elif line[i].startswith("zrrsig:"): + keyval = line[i].split(":") + keyprop.metadata["ZRRSIGState"] = keyval[1] + elif line[i].startswith("ds:"): + keyval = line[i].split(":") + keyprop.metadata["DSState"] = keyval[1] + elif line[i].startswith("offset:"): + keyval = line[i].split(":") + keyprop.properties["offset"] = timedelta(seconds=int(keyval[1])) + else: + assert False, f"undefined optional data {line[i]}" + + proplist.append(keyprop) + + return proplist diff --git a/bin/tests/system/kasp/tests.sh b/bin/tests/system/kasp/tests.sh index cdac5b7a26..ca294bab01 100644 --- a/bin/tests/system/kasp/tests.sh +++ b/bin/tests/system/kasp/tests.sh @@ -54,178 +54,6 @@ next_key_event_threshold=100 # Tests # ############################################################################### -# -# dnssec-keygen -# -set_zone "kasp" -set_policy "kasp" "4" "200" -set_server "keys" "10.53.0.1" - -n=$((n + 1)) -echo_i "check that 'dnssec-keygen -k' (configured policy) creates valid files ($n)" -ret=0 -$KEYGEN -K keys -k "$POLICY" -l kasp.conf "$ZONE" >"keygen.out.$POLICY.test$n" 2>/dev/null || ret=1 -lines=$(wc -l <"keygen.out.$POLICY.test$n") -test "$lines" -eq $NUM_KEYS || log_error "wrong number of keys created for policy kasp: $lines" -# Temporarily don't log errors because we are searching multiple files. -disable_logerror - -# Key properties. -set_keyrole "KEY1" "csk" -set_keylifetime "KEY1" "31536000" -set_keyalgorithm "KEY1" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY1" "yes" -set_zonesigning "KEY1" "yes" - -set_keyrole "KEY2" "ksk" -set_keylifetime "KEY2" "31536000" -set_keyalgorithm "KEY2" "8" "RSASHA256" "2048" -set_keysigning "KEY2" "yes" -set_zonesigning "KEY2" "no" - -set_keyrole "KEY3" "zsk" -set_keylifetime "KEY3" "2592000" -set_keyalgorithm "KEY3" "8" "RSASHA256" "2048" -set_keysigning "KEY3" "no" -set_zonesigning "KEY3" "yes" - -set_keyrole "KEY4" "zsk" -set_keylifetime "KEY4" "16070400" -set_keyalgorithm "KEY4" "8" "RSASHA256" "3072" -set_keysigning "KEY4" "no" -set_zonesigning "KEY4" "yes" - -lines=$(get_keyids "$DIR" "$ZONE" | wc -l) -test "$lines" -eq $NUM_KEYS || log_error "bad number of key ids" -status=$((status + ret)) - -ids=$(get_keyids "$DIR" "$ZONE") -for id in $ids; do - # There are four key files with the same algorithm. - # Check them until a match is found. - ret=0 && check_key "KEY1" "$id" - test "$ret" -eq 0 && continue - - ret=0 && check_key "KEY2" "$id" - test "$ret" -eq 0 && continue - - ret=0 && check_key "KEY3" "$id" - test "$ret" -eq 0 && continue - - ret=0 && check_key "KEY4" "$id" - - # If ret is still non-zero, non of the files matched. - test "$ret" -eq 0 || echo_i "failed" - status=$((status + ret)) -done -# Turn error logs on again. -enable_logerror - -n=$((n + 1)) -echo_i "check that 'dnssec-keygen -k' (default policy) creates valid files ($n)" -ret=0 -set_zone "kasp" -set_policy "default" "1" "3600" -set_server "." "10.53.0.1" -# Key properties. -key_clear "KEY1" -set_keyrole "KEY1" "csk" -set_keylifetime "KEY1" "0" -set_keyalgorithm "KEY1" "13" "ECDSAP256SHA256" "256" -set_keysigning "KEY1" "yes" -set_zonesigning "KEY1" "yes" - -key_clear "KEY2" -key_clear "KEY3" -key_clear "KEY4" - -$KEYGEN -G -k "$POLICY" "$ZONE" >"keygen.out.$POLICY.test$n" 2>/dev/null || ret=1 -lines=$(wc -l <"keygen.out.$POLICY.test$n") -test "$lines" -eq $NUM_KEYS || log_error "wrong number of keys created for policy default: $lines" -# Temporarily adjust max search depth for this test -MAXDEPTH=1 -ids=$(get_keyids "$DIR" "$ZONE") -MAXDEPTH=3 -echo_i "found in dir $DIR for zone $ZONE the following keytags: $ids" -for id in $ids; do - check_key "KEY1" "$id" - test "$ret" -eq 0 && key_save KEY1 - check_keytimes -done -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# -# dnssec-settime -# - -# These test builds upon the latest created key with dnssec-keygen and uses the -# environment variables BASE_FILE, KEY_FILE, PRIVATE_FILE and STATE_FILE. -CMP_FILE="${BASE_FILE}.cmp" -n=$((n + 1)) -echo_i "check that 'dnssec-settime' by default does not edit key state file ($n)" -ret=0 -cp "$STATE_FILE" "$CMP_FILE" -$SETTIME -P +3600 "$BASE_FILE" >/dev/null || log_error "settime failed" -grep "; Publish: " "$KEY_FILE" >/dev/null || log_error "mismatch published in $KEY_FILE" -grep "Publish: " "$PRIVATE_FILE" >/dev/null || log_error "mismatch published in $PRIVATE_FILE" -diff "$CMP_FILE" "$STATE_FILE" || log_error "unexpected file change in $STATE_FILE" -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that 'dnssec-settime -s' also sets publish time metadata and states in key state file ($n)" -ret=0 -cp "$STATE_FILE" "$CMP_FILE" -now=$(date +%Y%m%d%H%M%S) -$SETTIME -s -P "$now" -g "omnipresent" -k "rumoured" "$now" -z "omnipresent" "$now" -r "rumoured" "$now" -d "hidden" "$now" "$BASE_FILE" >/dev/null || log_error "settime failed" -set_keystate "KEY1" "GOAL" "omnipresent" -set_keystate "KEY1" "STATE_DNSKEY" "rumoured" -set_keystate "KEY1" "STATE_KRRSIG" "rumoured" -set_keystate "KEY1" "STATE_ZRRSIG" "omnipresent" -set_keystate "KEY1" "STATE_DS" "hidden" -check_key "KEY1" "$id" -test "$ret" -eq 0 && key_save KEY1 -set_keytime "KEY1" "PUBLISHED" "${now}" -check_keytimes -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that 'dnssec-settime -s' also unsets publish time metadata and states in key state file ($n)" -ret=0 -cp "$STATE_FILE" "$CMP_FILE" -$SETTIME -s -P "none" -g "none" -k "none" "$now" -z "none" "$now" -r "none" "$now" -d "none" "$now" "$BASE_FILE" >/dev/null || log_error "settime failed" -set_keystate "KEY1" "GOAL" "none" -set_keystate "KEY1" "STATE_DNSKEY" "none" -set_keystate "KEY1" "STATE_KRRSIG" "none" -set_keystate "KEY1" "STATE_ZRRSIG" "none" -set_keystate "KEY1" "STATE_DS" "none" -check_key "KEY1" "$id" -test "$ret" -eq 0 && key_save KEY1 -set_keytime "KEY1" "PUBLISHED" "none" -check_keytimes -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "check that 'dnssec-settime -s' also sets active time metadata and states in key state file (uppercase) ($n)" -ret=0 -cp "$STATE_FILE" "$CMP_FILE" -now=$(date +%Y%m%d%H%M%S) -$SETTIME -s -A "$now" -g "HIDDEN" -k "UNRETENTIVE" "$now" -z "UNRETENTIVE" "$now" -r "OMNIPRESENT" "$now" -d "OMNIPRESENT" "$now" "$BASE_FILE" >/dev/null || log_error "settime failed" -set_keystate "KEY1" "GOAL" "hidden" -set_keystate "KEY1" "STATE_DNSKEY" "unretentive" -set_keystate "KEY1" "STATE_KRRSIG" "omnipresent" -set_keystate "KEY1" "STATE_ZRRSIG" "unretentive" -set_keystate "KEY1" "STATE_DS" "omnipresent" -check_key "KEY1" "$id" -test "$ret" -eq 0 && key_save KEY1 -set_keytime "KEY1" "ACTIVE" "${now}" -check_keytimes -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - # # named # @@ -236,6 +64,7 @@ status=$((status + ret)) # infinite loops if there is an error. n=$((n + 1)) echo_i "waiting for kasp signing changes to take effect ($n)" +ret=0 _wait_for_done_apexnsec() { while read -r zone; do @@ -256,18 +85,6 @@ retry_quiet 30 _wait_for_done_apexnsec || ret=1 test "$ret" -eq 0 || echo_i "failed" status=$((status + ret)) -# Test max-zone-ttl rejects zones with too high TTL. -n=$((n + 1)) -echo_i "check that max-zone-ttl rejects zones with too high TTL ($n)" -ret=0 -set_zone "max-zone-ttl.kasp" -grep "loading from master file ${ZONE}.db failed: out of range" "ns3/named.run" >/dev/null || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# -# Zone: default.kasp. -# set_keytimes_csk_policy() { # The first key is immediately published and activated. created=$(key_get KEY1 CREATED) @@ -280,10 +97,6 @@ set_keytimes_csk_policy() { # Key lifetime is unlimited, so not setting RETIRED and REMOVED. } -# Check the zone with default kasp policy has loaded and is signed. -set_zone "default.kasp" -set_policy "default" "1" "3600" -set_server "ns3" "10.53.0.3" # Key properties. set_keyrole "KEY1" "csk" set_keylifetime "KEY1" "0" @@ -297,240 +110,6 @@ set_keystate "KEY1" "STATE_KRRSIG" "rumoured" set_keystate "KEY1" "STATE_ZRRSIG" "rumoured" set_keystate "KEY1" "STATE_DS" "hidden" -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -set_keytimes_csk_policy -check_keytimes -check_apex -check_subdomain -dnssec_verify - -# Trigger a keymgr run. Make sure the key files are not touched if there are -# no modifications to the key metadata. -n=$((n + 1)) -echo_i "make sure key files are untouched if metadata does not change ($n)" -ret=0 -basefile=$(key_get KEY1 BASEFILE) -privkey_stat=$(key_get KEY1 PRIVKEY_STAT) -pubkey_stat=$(key_get KEY1 PUBKEY_STAT) -state_stat=$(key_get KEY1 STATE_STAT) - -nextpart $DIR/named.run >/dev/null -rndccmd 10.53.0.3 loadkeys "$ZONE" >/dev/null || log_error "rndc loadkeys zone ${ZONE} failed" -wait_for_log 3 "keymgr: $ZONE done" $DIR/named.run || ret=1 -privkey_stat2=$(key_stat "${basefile}.private") -pubkey_stat2=$(key_stat "${basefile}.key") -state_stat2=$(key_stat "${basefile}.state") -test "$privkey_stat" = "$privkey_stat2" || log_error "wrong private key file stat (expected $privkey_stat got $privkey_stat2)" -test "$pubkey_stat" = "$pubkey_stat2" || log_error "wrong public key file stat (expected $pubkey_stat got $pubkey_stat2)" -test "$state_stat" = "$state_stat2" || log_error "wrong state file stat (expected $state_stat got $state_stat2)" -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -n=$((n + 1)) -echo_i "again ($n)" -ret=0 - -nextpart $DIR/named.run >/dev/null -rndccmd 10.53.0.3 loadkeys "$ZONE" >/dev/null || log_error "rndc loadkeys zone ${ZONE} failed" -wait_for_log 3 "keymgr: $ZONE done" $DIR/named.run || ret=1 -privkey_stat2=$(key_stat "${basefile}.private") -pubkey_stat2=$(key_stat "${basefile}.key") -state_stat2=$(key_stat "${basefile}.state") -test "$privkey_stat" = "$privkey_stat2" || log_error "wrong private key file stat (expected $privkey_stat got $privkey_stat2)" -test "$pubkey_stat" = "$pubkey_stat2" || log_error "wrong public key file stat (expected $pubkey_stat got $pubkey_stat2)" -test "$state_stat" = "$state_stat2" || log_error "wrong state file stat (expected $state_stat got $state_stat2)" -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Update zone. -n=$((n + 1)) -echo_i "modify unsigned zone file and check that new record is signed for zone ${ZONE} ($n)" -ret=0 -cp "${DIR}/template2.db.in" "${DIR}/${ZONE}.db" -rndccmd 10.53.0.3 reload "$ZONE" >/dev/null || log_error "rndc reload zone ${ZONE} failed" - -update_is_signed() { - ip_a=$1 - ip_d=$2 - - if [ "$ip_a" != "-" ]; then - dig_with_opts "a.${ZONE}" "@${SERVER}" A >"dig.out.$DIR.test$n.a" || return 1 - grep "status: NOERROR" "dig.out.$DIR.test$n.a" >/dev/null || return 1 - grep "a.${ZONE}\..*${DEFAULT_TTL}.*IN.*A.*${ip_a}" "dig.out.$DIR.test$n.a" >/dev/null || return 1 - lines=$(get_keys_which_signed A 0 "dig.out.$DIR.test$n.a" | wc -l) - test "$lines" -eq 1 || return 1 - get_keys_which_signed A 0 "dig.out.$DIR.test$n.a" | grep "^${KEY_ID}$" >/dev/null || return 1 - fi - - if [ "$ip_d" != "-" ]; then - dig_with_opts "d.${ZONE}" "@${SERVER}" A >"dig.out.$DIR.test$n".d || return 1 - grep "status: NOERROR" "dig.out.$DIR.test$n".d >/dev/null || return 1 - grep "d.${ZONE}\..*${DEFAULT_TTL}.*IN.*A.*${ip_d}" "dig.out.$DIR.test$n".d >/dev/null || return 1 - lines=$(get_keys_which_signed A 0 "dig.out.$DIR.test$n".d | wc -l) - test "$lines" -eq 1 || return 1 - get_keys_which_signed A 0 "dig.out.$DIR.test$n".d | grep "^${KEY_ID}$" >/dev/null || return 1 - fi -} - -retry_quiet 10 update_is_signed "10.0.0.11" "10.0.0.44" || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Move the private key file, a rekey event should not introduce replacement -# keys. -ret=0 -echo_i "test that if private key files are inaccessible this doesn't trigger a rollover ($n)" -basefile=$(key_get KEY1 BASEFILE) -mv "${basefile}.private" "${basefile}.offline" -rndccmd 10.53.0.3 loadkeys "$ZONE" >/dev/null || log_error "rndc loadkeys zone ${ZONE} failed" -wait_for_log 3 "zone $ZONE/IN (signed): zone_rekey:zone_verifykeys failed: some key files are missing" $DIR/named.run || ret=1 -mv "${basefile}.offline" "${basefile}.private" -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Nothing has changed. -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -set_keytimes_csk_policy -check_keytimes -check_apex -check_subdomain -dnssec_verify - -# -# A zone with special characters. -# -set_zone "i-am.\":\;?&[]\@!\$*+,|=\.\(\)special.kasp." -set_policy "default" "1" "3600" -set_server "ns3" "10.53.0.3" -# It is non-trivial to adapt the tests to deal with all possible different -# escaping characters, so we will just try to verify the zone. -dnssec_verify - -# -# Zone: dynamic.kasp -# -set_zone "dynamic.kasp" -set_dynamic -set_policy "default" "1" "3600" -set_server "ns3" "10.53.0.3" -# Key properties, timings and states same as above. -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -set_keytimes_csk_policy -check_keytimes -check_apex -check_subdomain -dnssec_verify - -# Update zone with nsupdate. -n=$((n + 1)) -echo_i "nsupdate zone and check that new record is signed for zone ${ZONE} ($n)" -ret=0 -( - echo zone ${ZONE} - echo server 10.53.0.3 "$PORT" - echo update del "a.${ZONE}" 300 A 10.0.0.1 - echo update add "a.${ZONE}" 300 A 10.0.0.101 - echo update add "d.${ZONE}" 300 A 10.0.0.4 - echo send -) | $NSUPDATE - -retry_quiet 10 update_is_signed "10.0.0.101" "10.0.0.4" || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Update zone with nsupdate (reverting the above change). -n=$((n + 1)) -echo_i "nsupdate zone and check that new record is signed for zone ${ZONE} ($n)" -ret=0 -( - echo zone ${ZONE} - echo server 10.53.0.3 "$PORT" - echo update add "a.${ZONE}" 300 A 10.0.0.1 - echo update del "a.${ZONE}" 300 A 10.0.0.101 - echo update del "d.${ZONE}" 300 A 10.0.0.4 - echo send -) | $NSUPDATE - -retry_quiet 10 update_is_signed "10.0.0.1" "-" || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# Update zone with freeze/thaw. -n=$((n + 1)) -echo_i "modify zone file and check that new record is signed for zone ${ZONE} ($n)" -ret=0 -rndccmd 10.53.0.3 freeze "$ZONE" >/dev/null || log_error "rndc freeze zone ${ZONE} failed" -sleep 1 -echo "d.${ZONE}. 300 A 10.0.0.44" >>"${DIR}/${ZONE}.db" -rndccmd 10.53.0.3 thaw "$ZONE" >/dev/null || log_error "rndc thaw zone ${ZONE} failed" - -retry_quiet 10 update_is_signed "10.0.0.1" "10.0.0.44" || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# -# Zone: dynamic-inline-signing.kasp -# -set_zone "dynamic-inline-signing.kasp" -set_dynamic -set_policy "default" "1" "3600" -set_server "ns3" "10.53.0.3" -# Key properties, timings and states same as above. -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -set_keytimes_csk_policy -check_keytimes -check_apex -check_subdomain -dnssec_verify - -# Update zone with freeze/thaw. -n=$((n + 1)) -echo_i "modify unsigned zone file and check that new record is signed for zone ${ZONE} ($n)" -ret=0 -rndccmd 10.53.0.3 freeze "$ZONE" >/dev/null || log_error "rndc freeze zone ${ZONE} failed" -sleep 1 -cp "${DIR}/template2.db.in" "${DIR}/${ZONE}.db" -rndccmd 10.53.0.3 thaw "$ZONE" >/dev/null || log_error "rndc thaw zone ${ZONE} failed" - -retry_quiet 10 update_is_signed || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# -# Zone: dynamic-signed-inline-signing.kasp -# -set_zone "dynamic-signed-inline-signing.kasp" -set_dynamic -set_policy "default" "1" "3600" -set_server "ns3" "10.53.0.3" -dnssec_verify -# Ensure no zone_resigninc for the unsigned version of the zone is triggered. -n=$((n + 1)) -echo_i "check if resigning the raw version of the zone is prevented for zone ${ZONE} ($n)" -ret=0 -grep "zone_resigninc: zone $ZONE/IN (unsigned): enter" $DIR/named.run && ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# -# Zone: inline-signing.kasp -# -set_zone "inline-signing.kasp" -set_policy "default" "1" "3600" -set_server "ns3" "10.53.0.3" -# Key properties, timings and states same as above. -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -set_keytimes_csk_policy -check_keytimes -check_apex -check_subdomain -dnssec_verify - # # Zone: checkds-ksk.kasp. # @@ -876,53 +455,16 @@ if [ $RSASHA1_SUPPORTED = 1 ]; then dnssec_verify fi -# -# Zone: unsigned.kasp. -# -set_zone "unsigned.kasp" -set_policy "none" "0" "0" -set_server "ns3" "10.53.0.3" - -key_clear "KEY1" -key_clear "KEY2" -key_clear "KEY3" -key_clear "KEY4" - -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -check_apex -check_subdomain -# Make sure the zone file is untouched. -n=$((n + 1)) -echo_i "Make sure the zonefile for zone ${ZONE} is not edited ($n)" -ret=0 -diff "${DIR}/${ZONE}.db.infile" "${DIR}/${ZONE}.db" || ret=1 -test "$ret" -eq 0 || echo_i "failed" -status=$((status + ret)) - -# -# Zone: insecure.kasp. -# -set_zone "insecure.kasp" -set_policy "insecure" "0" "0" -set_server "ns3" "10.53.0.3" - -key_clear "KEY1" -key_clear "KEY2" -key_clear "KEY3" -key_clear "KEY4" - -check_keys -check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" -check_apex -check_subdomain - # # Zone: unlimited.kasp. # set_zone "unlimited.kasp" set_policy "unlimited" "1" "1234" set_server "ns3" "10.53.0.3" +key_clear "KEY1" +key_clear "KEY2" +key_clear "KEY3" +key_clear "KEY4" # Key properties. set_keyrole "KEY1" "csk" set_keylifetime "KEY1" "0" diff --git a/bin/tests/system/kasp/tests_kasp.py b/bin/tests/system/kasp/tests_kasp.py new file mode 100644 index 0000000000..7ffa22e740 --- /dev/null +++ b/bin/tests/system/kasp/tests_kasp.py @@ -0,0 +1,576 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +import os +import shutil +import time + +from datetime import timedelta + +import dns +import dns.update +import pytest + +pytest.importorskip("dns", minversion="2.0.0") +import isctest +from isctest.kasp import ( + KeyProperties, + KeyTimingMetadata, +) + +pytestmark = pytest.mark.extra_artifacts( + [ + "K*.private", + "K*.backup", + "K*.cmp", + "K*.key", + "K*.state", + "*.axfr", + "*.created", + "dig.out*", + "keyevent.out.*", + "keygen.out.*", + "keys", + "published.test*", + "python.out.*", + "retired.test*", + "rndc.dnssec.*.out.*", + "rndc.zonestatus.out.*", + "rrsig.out.*", + "created.key-*", + "unused.key-*", + "verify.out.*", + "zone.out.*", + "ns*/K*.key", + "ns*/K*.offline", + "ns*/K*.private", + "ns*/K*.state", + "ns*/*.db", + "ns*/*.db.infile", + "ns*/*.db.signed", + "ns*/*.db.signed.tmp", + "ns*/*.jbk", + "ns*/*.jnl", + "ns*/*.zsk1", + "ns*/*.zsk2", + "ns*/dsset-*", + "ns*/keygen.out.*", + "ns*/keys", + "ns*/ksk", + "ns*/ksk/K*", + "ns*/zsk", + "ns*/zsk", + "ns*/zsk/K*", + "ns*/named-fips.conf", + "ns*/settime.out.*", + "ns*/signer.out.*", + "ns*/zones", + "ns*/policies/*.conf", + "ns3/legacy-keys.*", + "ns3/dynamic-signed-inline-signing.kasp.db.signed.signed", + ] +) + + +def check_all(server, zone, policy, ksks, zsks, tsig=None): + isctest.kasp.check_dnssecstatus(server, zone, ksks + zsks, policy=policy) + isctest.kasp.check_apex(server, zone, ksks, zsks, tsig=tsig) + isctest.kasp.check_subdomain(server, zone, ksks, zsks, tsig=tsig) + isctest.kasp.check_dnssec_verify(server, zone) + + +def set_keytimes_default_policy(kp): + # The first key is immediately published and activated. + kp.timing["Generated"] = kp.key.get_timing("Created") + kp.timing["Published"] = kp.timing["Generated"] + kp.timing["Active"] = kp.timing["Generated"] + # The DS can be published if the DNSKEY and RRSIG records are + # OMNIPRESENT. This happens after max-zone-ttl (1d) plus + # plus zone-propagation-delay (300s). + kp.timing["PublishCDS"] = kp.timing["Published"] + timedelta(days=1, seconds=300) + # Key lifetime is unlimited, so not setting 'Retired' nor 'Removed'. + kp.timing["DNSKEYChange"] = kp.timing["Published"] + kp.timing["DSChange"] = kp.timing["Published"] + kp.timing["KRRSIGChange"] = kp.timing["Active"] + kp.timing["ZRRSIGChange"] = kp.timing["Active"] + + +def test_kasp_default(servers): + server = servers["ns3"] + + # check the zone with default kasp policy has loaded and is signed. + isctest.log.info("check a zone with the default policy is signed") + zone = "default.kasp" + policy = "default" + + # Key properties. + # DNSKEY, RRSIG (ksk), RRSIG (zsk) are published. DS needs to wait. + keyprops = [ + "csk 0 13 256 goal:omnipresent dnskey:rumoured krrsig:rumoured zrrsig:rumoured ds:hidden", + ] + expected = isctest.kasp.policy_to_properties(ttl=3600, keys=keyprops) + keys = isctest.kasp.keydir_to_keylist(zone, "ns3") + isctest.kasp.check_zone_is_signed(server, zone) + isctest.kasp.check_keys(zone, keys, expected) + set_keytimes_default_policy(expected[0]) + isctest.kasp.check_keytimes(keys, expected) + check_all(server, zone, policy, keys, []) + + # Trigger a keymgr run. Make sure the key files are not touched if there + # are no modifications to the key metadata. + isctest.log.info( + "check that key files are untouched if there are no metadata changes" + ) + key = keys[0] + privkey_stat = os.stat(key.privatefile) + pubkey_stat = os.stat(key.keyfile) + state_stat = os.stat(key.statefile) + + with server.watch_log_from_here() as watcher: + server.rndc(f"loadkeys {zone}", log=False) + watcher.wait_for_line(f"keymgr: {zone} done") + + assert privkey_stat.st_mtime == os.stat(key.privatefile).st_mtime + assert pubkey_stat.st_mtime == os.stat(key.keyfile).st_mtime + assert state_stat.st_mtime == os.stat(key.statefile).st_mtime + + # again + with server.watch_log_from_here() as watcher: + server.rndc(f"loadkeys {zone}", log=False) + watcher.wait_for_line(f"keymgr: {zone} done") + + assert privkey_stat.st_mtime == os.stat(key.privatefile).st_mtime + assert pubkey_stat.st_mtime == os.stat(key.keyfile).st_mtime + assert state_stat.st_mtime == os.stat(key.statefile).st_mtime + + # modify unsigned zone file and check that new record is signed. + isctest.log.info("check that an updated zone signs the new record") + shutil.copyfile("ns3/template2.db.in", f"ns3/{zone}.db") + server.rndc(f"reload {zone}", log=False) + + def update_is_signed(): + parts = update.split() + qname = parts[0] + qtype = dns.rdatatype.from_text(parts[1]) + rdata = parts[2] + return isctest.kasp.verify_update_is_signed( + server, zone, qname, qtype, rdata, keys, [] + ) + + expected_updates = [f"a.{zone}. A 10.0.0.11", f"d.{zone}. A 10.0.0.44"] + for update in expected_updates: + isctest.run.retry_with_timeout(update_is_signed, timeout=5) + + # Move the private key file, a rekey event should not introduce + # replacement keys. + isctest.log.info("check that missing private key doesn't trigger rollover") + shutil.move(f"{key.privatefile}", f"{key.path}.offline") + expectmsg = "zone_rekey:zone_verifykeys failed: some key files are missing" + with server.watch_log_from_here() as watcher: + server.rndc(f"loadkeys {zone}", log=False) + watcher.wait_for_line(f"zone {zone}/IN (signed): {expectmsg}") + # Nothing has changed. + expected[0].properties["private"] = False + isctest.kasp.check_keys(zone, keys, expected) + isctest.kasp.check_keytimes(keys, expected) + check_all(server, zone, policy, keys, []) + + # A zone that uses inline-signing. + isctest.log.info("check an inline-signed zone with the default policy is signed") + zone = "inline-signing.kasp" + # Key properties. + key1 = KeyProperties.default() + keys = isctest.kasp.keydir_to_keylist(zone, "ns3") + expected = [key1] + isctest.kasp.check_zone_is_signed(server, zone) + isctest.kasp.check_keys(zone, keys, expected) + set_keytimes_default_policy(key1) + isctest.kasp.check_keytimes(keys, expected) + check_all(server, zone, policy, keys, []) + + +def test_kasp_dynamic(servers): + # Dynamic update test cases. + server = servers["ns3"] + + # Standard dynamic zone. + isctest.log.info("check dynamic zone is updated and signed after update") + zone = "dynamic.kasp" + policy = "default" + # Key properties. + key1 = KeyProperties.default() + expected = [key1] + keys = isctest.kasp.keydir_to_keylist(zone, "ns3") + isctest.kasp.check_zone_is_signed(server, zone) + isctest.kasp.check_keys(zone, keys, expected) + set_keytimes_default_policy(key1) + expected = [key1] + isctest.kasp.check_keytimes(keys, expected) + check_all(server, zone, policy, keys, []) + + # Update zone with nsupdate. + def nsupdate(updates): + message = dns.update.UpdateMessage(zone) + for update in updates: + if update[0] == "del": + message.delete(update[1], update[2], update[3]) + else: + assert update[0] == "add" + message.add(update[1], update[2], update[3], update[4]) + + try: + response = isctest.query.udp( + message, server.ip, server.ports.dns, timeout=3 + ) + assert response.rcode() == dns.rcode.NOERROR + except dns.exception.Timeout: + assert False, f"update timeout for {zone}" + + isctest.log.debug(f"update of zone {zone} to server {server.ip} successful") + + def update_is_signed(): + parts = update.split() + qname = parts[0] + qtype = dns.rdatatype.from_text(parts[1]) + rdata = parts[2] + return isctest.kasp.verify_update_is_signed( + server, zone, qname, qtype, rdata, keys, [] + ) + + updates = [ + ["del", f"a.{zone}.", "A", "10.0.0.1"], + ["add", f"a.{zone}.", 300, "A", "10.0.0.101"], + ["add", f"d.{zone}.", 300, "A", "10.0.0.4"], + ] + nsupdate(updates) + + expected_updates = [f"a.{zone}. A 10.0.0.101", f"d.{zone}. A 10.0.0.4"] + for update in expected_updates: + isctest.run.retry_with_timeout(update_is_signed, timeout=5) + + # Update zone with nsupdate (reverting the above change). + updates = [ + ["add", f"a.{zone}.", 300, "A", "10.0.0.1"], + ["del", f"a.{zone}.", "A", "10.0.0.101"], + ["del", f"d.{zone}.", "A", "10.0.0.4"], + ] + nsupdate(updates) + + update = f"a.{zone}. A 10.0.0.1" + isctest.run.retry_with_timeout(update_is_signed, timeout=5) + + # Update zone with freeze/thaw. + isctest.log.info("check dynamic zone is updated and signed after freeze and thaw") + with server.watch_log_from_here() as watcher: + server.rndc(f"freeze {zone}", log=False) + watcher.wait_for_line(f"freezing zone '{zone}/IN': success") + + time.sleep(1) + with open(f"ns3/{zone}.db", "a", encoding="utf-8") as zonefile: + zonefile.write(f"d.{zone}. 300 A 10.0.0.44\n") + time.sleep(1) + + with server.watch_log_from_here() as watcher: + server.rndc(f"thaw {zone}", log=False) + watcher.wait_for_line(f"thawing zone '{zone}/IN': success") + + expected_updates = [f"a.{zone}. A 10.0.0.1", f"d.{zone}. A 10.0.0.44"] + + for update in expected_updates: + isctest.run.retry_with_timeout(update_is_signed, timeout=5) + + # Dynamic, and inline-signing. + zone = "dynamic-inline-signing.kasp" + # Key properties. + key1 = KeyProperties.default() + expected = [key1] + keys = isctest.kasp.keydir_to_keylist(zone, "ns3") + isctest.kasp.check_zone_is_signed(server, zone) + isctest.kasp.check_keys(zone, keys, expected) + set_keytimes_default_policy(key1) + expected = [key1] + isctest.kasp.check_keytimes(keys, expected) + check_all(server, zone, policy, keys, []) + + # Update zone with freeze/thaw. + isctest.log.info( + "check dynamic inline-signed zone is updated and signed after freeze and thaw" + ) + with server.watch_log_from_here() as watcher: + server.rndc(f"freeze {zone}", log=False) + watcher.wait_for_line(f"freezing zone '{zone}/IN': success") + + time.sleep(1) + shutil.copyfile("ns3/template2.db.in", f"ns3/{zone}.db") + time.sleep(1) + + with server.watch_log_from_here() as watcher: + server.rndc(f"thaw {zone}", log=False) + watcher.wait_for_line(f"thawing zone '{zone}/IN': success") + + expected_updates = [f"a.{zone}. A 10.0.0.11", f"d.{zone}. A 10.0.0.44"] + for update in expected_updates: + isctest.run.retry_with_timeout(update_is_signed, timeout=5) + + # Dynamic, signed, and inline-signing. + isctest.log.info("check dynamic signed, and inline-signed zone") + zone = "dynamic-signed-inline-signing.kasp" + # Key properties. + key1 = KeyProperties.default() + # The ns3/setup.sh script sets all states to omnipresent. + key1.metadata["DNSKEYState"] = "omnipresent" + key1.metadata["KRRSIGState"] = "omnipresent" + key1.metadata["ZRRSIGState"] = "omnipresent" + key1.metadata["DSState"] = "omnipresent" + expected = [key1] + keys = isctest.kasp.keydir_to_keylist(zone, "ns3/keys") + isctest.kasp.check_zone_is_signed(server, zone) + isctest.kasp.check_keys(zone, keys, expected) + check_all(server, zone, policy, keys, []) + # Ensure no zone_resigninc for the unsigned version of the zone is triggered. + assert f"zone_resigninc: zone {zone}/IN (unsigned): enter" not in "ns3/named.run" + + +def test_kasp_special_characters(servers): + server = servers["ns3"] + + # A zone with special characters. + isctest.log.info("check special characters") + + zone = r'i-am.":\;?&[]\@!\$*+,|=\.\(\)special.kasp' + # It is non-trivial to adapt the tests to deal with all possible different + # escaping characters, so we will just try to verify the zone. + isctest.kasp.check_dnssec_verify(server, zone) + + +def test_kasp_insecure(servers): + server = servers["ns3"] + + # Insecure zones. + isctest.log.info("check insecure zones") + + zone = "insecure.kasp" + expected = [] + keys = isctest.kasp.keydir_to_keylist(zone, "ns3") + isctest.kasp.check_keys(zone, keys, expected) + isctest.kasp.check_dnssecstatus(server, zone, keys, policy="insecure") + isctest.kasp.check_apex(server, zone, keys, []) + isctest.kasp.check_subdomain(server, zone, keys, []) + + zone = "unsigned.kasp" + expected = [] + keys = isctest.kasp.keydir_to_keylist(zone, "ns3") + isctest.kasp.check_keys(zone, keys, expected) + isctest.kasp.check_dnssecstatus(server, zone, keys, policy=None) + isctest.kasp.check_apex(server, zone, keys, []) + isctest.kasp.check_subdomain(server, zone, keys, []) + # Make sure the zone file is untouched. + isctest.check.file_contents_equal(f"ns3/{zone}.db.infile", f"ns3/{zone}.db") + + +def test_kasp_bad_maxzonettl(servers): + server = servers["ns3"] + + # check that max-zone-ttl rejects zones with too high TTL. + isctest.log.info("check max-zone-ttl rejects zones with too high TTL") + zone = "max-zone-ttl.kasp" + assert f"loading from master file {zone}.db failed: out of range" in server.log + + +def test_kasp_dnssec_keygen(): + def keygen(zone, policy, keydir=None): + if keydir is None: + keydir = "." + + keygen_command = [ + os.environ.get("KEYGEN"), + "-K", + keydir, + "-k", + policy, + "-l", + "kasp.conf", + zone, + ] + + return isctest.run.cmd(keygen_command, log_stdout=True).stdout.decode("utf-8") + + # check that 'dnssec-keygen -k' (configured policy) creates valid files. + lifetime = { + "P1Y": int(timedelta(days=365).total_seconds()), + "P30D": int(timedelta(days=30).total_seconds()), + "P6M": int(timedelta(days=31 * 6).total_seconds()), + } + keyprops = [ + f"csk {lifetime['P1Y']} 13 256", + f"ksk {lifetime['P1Y']} 8 2048", + f"zsk {lifetime['P30D']} 8 2048", + f"zsk {lifetime['P6M']} 8 3072", + ] + keydir = "keys" + out = keygen("kasp", "kasp", keydir) + keys = isctest.kasp.keystr_to_keylist(out, keydir) + expected = isctest.kasp.policy_to_properties(ttl=200, keys=keyprops) + isctest.kasp.check_keys("kasp", keys, expected) + + # check that 'dnssec-keygen -k' (default policy) creates valid files. + keyprops = ["csk 0 13 256"] + out = keygen("kasp", "default") + keys = isctest.kasp.keystr_to_keylist(out) + expected = isctest.kasp.policy_to_properties(ttl=3600, keys=keyprops) + isctest.kasp.check_keys("kasp", keys, expected) + + # check that 'dnssec-settime' by default does not edit key state file. + key = keys[0] + shutil.copyfile(key.privatefile, f"{key.privatefile}.backup") + shutil.copyfile(key.keyfile, f"{key.keyfile}.backup") + shutil.copyfile(key.statefile, f"{key.statefile}.backup") + + created = key.get_timing("Created") + publish = key.get_timing("Publish") + timedelta(hours=1) + settime = [ + os.environ.get("SETTIME"), + "-P", + str(publish), + key.path, + ] + out = isctest.run.cmd(settime, log_stdout=True).stdout.decode("utf-8") + + isctest.check.file_contents_equal(f"{key.statefile}", f"{key.statefile}.backup") + assert key.get_metadata("Publish", file=key.privatefile) == str(publish) + assert key.get_metadata("Publish", file=key.keyfile, comment=True) == str(publish) + + # check that 'dnssec-settime -s' also sets publish time metadata and + # states in key state file. + now = KeyTimingMetadata.now() + goal = "omnipresent" + dnskey = "rumoured" + krrsig = "rumoured" + zrrsig = "omnipresent" + ds = "hidden" + keyprops = [ + f"csk 0 13 256 goal:{goal} dnskey:{dnskey} krrsig:{krrsig} zrrsig:{zrrsig} ds:{ds}", + ] + expected = isctest.kasp.policy_to_properties(ttl=3600, keys=keyprops) + expected[0].timing = { + "Generated": created, + "Published": now, + "Active": created, + "DNSKEYChange": now, + "KRRSIGChange": now, + "ZRRSIGChange": now, + "DSChange": now, + } + + settime = [ + os.environ.get("SETTIME"), + "-s", + "-P", + str(now), + "-g", + goal, + "-k", + dnskey, + str(now), + "-r", + krrsig, + str(now), + "-z", + zrrsig, + str(now), + "-d", + ds, + str(now), + key.path, + ] + out = isctest.run.cmd(settime, log_stdout=True).stdout.decode("utf-8") + isctest.kasp.check_keys("kasp", keys, expected) + isctest.kasp.check_keytimes(keys, expected) + + # check that 'dnssec-settime -s' also unsets publish time metadata and + # states in key state file. + now = KeyTimingMetadata.now() + keyprops = ["csk 0 13 256"] + expected = isctest.kasp.policy_to_properties(ttl=3600, keys=keyprops) + expected[0].timing = { + "Generated": created, + "Active": created, + } + + settime = [ + os.environ.get("SETTIME"), + "-s", + "-P", + "none", + "-g", + "none", + "-k", + "none", + str(now), + "-z", + "none", + str(now), + "-r", + "none", + str(now), + "-d", + "none", + str(now), + key.path, + ] + out = isctest.run.cmd(settime, log_stdout=True).stdout.decode("utf-8") + isctest.kasp.check_keys("kasp", keys, expected) + isctest.kasp.check_keytimes(keys, expected) + + # check that 'dnssec-settime -s' also sets active time metadata and states in key state file (uppercase) + soon = now + timedelta(hours=2) + goal = "hidden" + dnskey = "unretentive" + krrsig = "omnipresent" + zrrsig = "unretentive" + ds = "omnipresent" + keyprops = [ + f"csk 0 13 256 goal:{goal} dnskey:{dnskey} krrsig:{krrsig} zrrsig:{zrrsig} ds:{ds}", + ] + expected = isctest.kasp.policy_to_properties(ttl=3600, keys=keyprops) + expected[0].timing = { + "Generated": created, + "Active": soon, + "DNSKEYChange": soon, + "KRRSIGChange": soon, + "ZRRSIGChange": soon, + "DSChange": soon, + } + + settime = [ + os.environ.get("SETTIME"), + "-s", + "-A", + str(soon), + "-g", + "HIDDEN", + "-k", + "UNRETENTIVE", + str(soon), + "-z", + "UNRETENTIVE", + str(soon), + "-r", + "OMNIPRESENT", + str(soon), + "-d", + "OMNIPRESENT", + str(soon), + key.path, + ] + out = isctest.run.cmd(settime, log_stdout=True).stdout.decode("utf-8") + isctest.kasp.check_keys("kasp", keys, expected) + isctest.kasp.check_keytimes(keys, expected)