From 4a8ad0a77f7f8baa0fda4d30760b6fbd2e617845 Mon Sep 17 00:00:00 2001 From: Matthijs Mekking Date: Mon, 12 Apr 2021 15:26:34 +0200 Subject: [PATCH] Add kasp tests for offline keys Add a test for default.kasp that if we remove the private key file, no successor key is created for it. We need to update the kasp script to deal with a missing private key. If this is the case, skip checks for private key files. Add a test with a zone for which the private key of the ZSK is missing. Add a test with a zone for which the private key of the KSK is missing. --- bin/tests/system/kasp.sh | 69 +++++++++++++++++++------ bin/tests/system/kasp/ns3/named.conf.in | 9 ++++ bin/tests/system/kasp/ns3/setup.sh | 25 ++++++++- bin/tests/system/kasp/tests.sh | 60 ++++++++++++++++++++- 4 files changed, 143 insertions(+), 20 deletions(-) diff --git a/bin/tests/system/kasp.sh b/bin/tests/system/kasp.sh index 94e395fb4d..d21839a08b 100644 --- a/bin/tests/system/kasp.sh +++ b/bin/tests/system/kasp.sh @@ -59,6 +59,7 @@ VIEW2="4xILSZQnuO1UKubXHkYUsvBRPu8=" # EXPECT_ZRRSIG # EXPECT_KRRSIG # LEGACY +# PRIVATE key_key() { echo "${1}__${2}" @@ -112,6 +113,7 @@ key_clear() { key_set "$1" "EXPECT_ZRRSIG" 'no' key_set "$1" "EXPECT_KRRSIG" 'no' key_set "$1" "LEGACY" 'no' + key_set "$1" "PRIVATE" 'yes' } # Start clear. @@ -303,6 +305,7 @@ check_key() { _dnskey_ttl="$DNSKEY_TTL" _lifetime=$(key_get "$1" LIFETIME) _legacy=$(key_get "$1" LEGACY) + _private=$(key_get "$1" PRIVATE) _published=$(key_get "$1" PUBLISHED) _active=$(key_get "$1" ACTIVE) @@ -341,7 +344,9 @@ check_key() { # Check file existence. [ -s "$KEY_FILE" ] || ret=1 - [ -s "$PRIVATE_FILE" ] || ret=1 + if [ "$_private" = "yes" ]; then + [ -s "$PRIVATE_FILE" ] || ret=1 + fi if [ "$_legacy" = "no" ]; then [ -s "$STATE_FILE" ] || ret=1 fi @@ -352,7 +357,9 @@ check_key() { grep "; Created:" "$KEY_FILE" > "${ZONE}.${KEY_ID}.${_alg_num}.created" || _log_error "mismatch created comment in $KEY_FILE" KEY_CREATED=$(awk '{print $3}' < "${ZONE}.${KEY_ID}.${_alg_num}.created") - grep "Created: ${KEY_CREATED}" "$PRIVATE_FILE" > /dev/null || _log_error "mismatch created in $PRIVATE_FILE" + if [ "$_private" = "yes" ]; then + grep "Created: ${KEY_CREATED}" "$PRIVATE_FILE" > /dev/null || _log_error "mismatch created in $PRIVATE_FILE" + fi if [ "$_legacy" = "no" ]; then grep "Generated: ${KEY_CREATED}" "$STATE_FILE" > /dev/null || _log_error "mismatch generated in $STATE_FILE" fi @@ -363,8 +370,10 @@ check_key() { grep "This is a ${_role2} key, keyid ${_key_id}, for ${_zone}." "$KEY_FILE" > /dev/null || _log_error "mismatch top comment in $KEY_FILE" grep "${_zone}\. ${_dnskey_ttl} IN DNSKEY ${_flags} 3 ${_alg_num}" "$KEY_FILE" > /dev/null || _log_error "mismatch DNSKEY record in $KEY_FILE" # Now check the private key file. - grep "Private-key-format: v1.3" "$PRIVATE_FILE" > /dev/null || _log_error "mismatch private key format in $PRIVATE_FILE" - grep "Algorithm: ${_alg_num} (${_alg_string})" "$PRIVATE_FILE" > /dev/null || _log_error "mismatch algorithm in $PRIVATE_FILE" + if [ "$_private" = "yes" ]; then + grep "Private-key-format: v1.3" "$PRIVATE_FILE" > /dev/null || _log_error "mismatch private key format in $PRIVATE_FILE" + grep "Algorithm: ${_alg_num} (${_alg_string})" "$PRIVATE_FILE" > /dev/null || _log_error "mismatch algorithm in $PRIVATE_FILE" + fi # Now check the key state file. if [ "$_legacy" = "no" ]; then grep "This is the state of key ${_key_id}, for ${_zone}." "$STATE_FILE" > /dev/null || _log_error "mismatch top comment in $STATE_FILE" @@ -444,6 +453,8 @@ check_timingmetadata() { _key_file="${_base_file}.key" _private_file="${_base_file}.private" _state_file="${_base_file}.state" + _legacy=$(key_get "$1" LEGACY) + _private=$(key_get "$1" PRIVATE) _published=$(key_get "$1" PUBLISHED) _syncpublish=$(key_get "$1" SYNCPUBLISH) @@ -459,13 +470,17 @@ check_timingmetadata() { if [ "$_published" = "none" ]; then grep "; Publish:" "${_key_file}" > /dev/null && _log_error "unexpected publish comment in ${_key_file}" - grep "Publish:" "${_private_file}" > /dev/null && _log_error "unexpected publish in ${_private_file}" + if [ "$_private" = "yes" ]; then + grep "Publish:" "${_private_file}" > /dev/null && _log_error "unexpected publish in ${_private_file}" + fi if [ "$_legacy" = "no" ]; then grep "Published: " "${_state_file}" > /dev/null && _log_error "unexpected publish in ${_state_file}" fi else grep "; Publish: $_published" "${_key_file}" > /dev/null || _log_error "mismatch publish comment in ${_key_file} (expected ${_published})" - grep "Publish: $_published" "${_private_file}" > /dev/null || _log_error "mismatch publish in ${_private_file} (expected ${_published})" + if [ "$_private" = "yes" ]; then + grep "Publish: $_published" "${_private_file}" > /dev/null || _log_error "mismatch publish in ${_private_file} (expected ${_published})" + fi if [ "$_legacy" = "no" ]; then grep "Published: $_published" "${_state_file}" > /dev/null || _log_error "mismatch publish in ${_state_file} (expected ${_published})" fi @@ -473,13 +488,17 @@ check_timingmetadata() { if [ "$_syncpublish" = "none" ]; then grep "; SyncPublish:" "${_key_file}" > /dev/null && _log_error "unexpected syncpublish comment in ${_key_file}" - grep "SyncPublish:" "${_private_file}" > /dev/null && _log_error "unexpected syncpublish in ${_private_file}" + if [ "$_private" = "yes" ]; then + grep "SyncPublish:" "${_private_file}" > /dev/null && _log_error "unexpected syncpublish in ${_private_file}" + fi if [ "$_legacy" = "no" ]; then grep "PublishCDS: " "${_state_file}" > /dev/null && _log_error "unexpected syncpublish in ${_state_file}" fi else grep "; SyncPublish: $_syncpublish" "${_key_file}" > /dev/null || _log_error "mismatch syncpublish comment in ${_key_file} (expected ${_syncpublish})" - grep "SyncPublish: $_syncpublish" "${_private_file}" > /dev/null || _log_error "mismatch syncpublish in ${_private_file} (expected ${_syncpublish})" + if [ "$_private" = "yes" ]; then + grep "SyncPublish: $_syncpublish" "${_private_file}" > /dev/null || _log_error "mismatch syncpublish in ${_private_file} (expected ${_syncpublish})" + fi if [ "$_legacy" = "no" ]; then grep "PublishCDS: $_syncpublish" "${_state_file}" > /dev/null || _log_error "mismatch syncpublish in ${_state_file} (expected ${_syncpublish})" fi @@ -487,13 +506,17 @@ check_timingmetadata() { if [ "$_active" = "none" ]; then grep "; Activate:" "${_key_file}" > /dev/null && _log_error "unexpected active comment in ${_key_file}" - grep "Activate:" "${_private_file}" > /dev/null && _log_error "unexpected active in ${_private_file}" + if [ "$_private" = "yes" ]; then + grep "Activate:" "${_private_file}" > /dev/null && _log_error "unexpected active in ${_private_file}" + fi if [ "$_legacy" = "no" ]; then grep "Active: " "${_state_file}" > /dev/null && _log_error "unexpected active in ${_state_file}" fi else grep "; Activate: $_active" "${_key_file}" > /dev/null || _log_error "mismatch active comment in ${_key_file} (expected ${_active})" - grep "Activate: $_active" "${_private_file}" > /dev/null || _log_error "mismatch active in ${_private_file} (expected ${_active})" + if [ "$_private" = "yes" ]; then + grep "Activate: $_active" "${_private_file}" > /dev/null || _log_error "mismatch active in ${_private_file} (expected ${_active})" + fi if [ "$_legacy" = "no" ]; then grep "Active: $_active" "${_state_file}" > /dev/null || _log_error "mismatch active in ${_state_file} (expected ${_active})" fi @@ -501,13 +524,17 @@ check_timingmetadata() { if [ "$_retired" = "none" ]; then grep "; Inactive:" "${_key_file}" > /dev/null && _log_error "unexpected retired comment in ${_key_file}" - grep "Inactive:" "${_private_file}" > /dev/null && _log_error "unexpected retired in ${_private_file}" + if [ "$_private" = "yes" ]; then + grep "Inactive:" "${_private_file}" > /dev/null && _log_error "unexpected retired in ${_private_file}" + fi if [ "$_legacy" = "no" ]; then grep "Retired: " "${_state_file}" > /dev/null && _log_error "unexpected retired in ${_state_file}" fi else grep "; Inactive: $_retired" "${_key_file}" > /dev/null || _log_error "mismatch retired comment in ${_key_file} (expected ${_retired})" - grep "Inactive: $_retired" "${_private_file}" > /dev/null || _log_error "mismatch retired in ${_private_file} (expected ${_retired})" + if [ "$_private" = "yes" ]; then + grep "Inactive: $_retired" "${_private_file}" > /dev/null || _log_error "mismatch retired in ${_private_file} (expected ${_retired})" + fi if [ "$_legacy" = "no" ]; then grep "Retired: $_retired" "${_state_file}" > /dev/null || _log_error "mismatch retired in ${_state_file} (expected ${_retired})" fi @@ -515,13 +542,17 @@ check_timingmetadata() { if [ "$_revoked" = "none" ]; then grep "; Revoke:" "${_key_file}" > /dev/null && _log_error "unexpected revoked comment in ${_key_file}" - grep "Revoke:" "${_private_file}" > /dev/null && _log_error "unexpected revoked in ${_private_file}" + if [ "$_private" = "yes" ]; then + grep "Revoke:" "${_private_file}" > /dev/null && _log_error "unexpected revoked in ${_private_file}" + fi if [ "$_legacy" = "no" ]; then grep "Revoked: " "${_state_file}" > /dev/null && _log_error "unexpected revoked in ${_state_file}" fi else grep "; Revoke: $_revoked" "${_key_file}" > /dev/null || _log_error "mismatch revoked comment in ${_key_file} (expected ${_revoked})" - grep "Revoke: $_revoked" "${_private_file}" > /dev/null || _log_error "mismatch revoked in ${_private_file} (expected ${_revoked})" + if [ "$_private" = "yes" ]; then + grep "Revoke: $_revoked" "${_private_file}" > /dev/null || _log_error "mismatch revoked in ${_private_file} (expected ${_revoked})" + fi if [ "$_legacy" = "no" ]; then grep "Revoked: $_revoked" "${_state_file}" > /dev/null || _log_error "mismatch revoked in ${_state_file} (expected ${_revoked})" fi @@ -529,13 +560,17 @@ check_timingmetadata() { if [ "$_removed" = "none" ]; then grep "; Delete:" "${_key_file}" > /dev/null && _log_error "unexpected removed comment in ${_key_file}" - grep "Delete:" "${_private_file}" > /dev/null && _log_error "unexpected removed in ${_private_file}" + if [ "$_private" = "yes" ]; then + grep "Delete:" "${_private_file}" > /dev/null && _log_error "unexpected removed in ${_private_file}" + fi if [ "$_legacy" = "no" ]; then grep "Removed: " "${_state_file}" > /dev/null && _log_error "unexpected removed in ${_state_file}" fi else grep "; Delete: $_removed" "${_key_file}" > /dev/null || _log_error "mismatch removed comment in ${_key_file} (expected ${_removed})" - grep "Delete: $_removed" "${_private_file}" > /dev/null || _log_error "mismatch removed in ${_private_file} (expected ${_removed})" + if [ "$_private" = "yes" ]; then + grep "Delete: $_removed" "${_private_file}" > /dev/null || _log_error "mismatch removed in ${_private_file} (expected ${_removed})" + fi if [ "$_legacy" = "no" ]; then grep "Removed: $_removed" "${_state_file}" > /dev/null || _log_error "mismatch removed in ${_state_file} (expected ${_removed})" fi @@ -672,7 +707,7 @@ _check_keys() { # Check key files. _ids=$(get_keyids "$DIR" "$ZONE") for _id in $_ids; do - # There are three key files with the same algorithm. + # There are multiple key files with the same algorithm. # Check them until a match is found. ret=0 echo_i "check key id $_id" diff --git a/bin/tests/system/kasp/ns3/named.conf.in b/bin/tests/system/kasp/ns3/named.conf.in index 6e6f7bfa06..a2731099e9 100644 --- a/bin/tests/system/kasp/ns3/named.conf.in +++ b/bin/tests/system/kasp/ns3/named.conf.in @@ -252,6 +252,15 @@ zone "unfresh-sigs.autosign" { dnssec-policy "autosign"; }; +/* + * Zone that has missing private KSK. + */ +zone "ksk-missing.autosign" { + type primary; + file "ksk-missing.autosign.db"; + dnssec-policy "autosign"; +}; + /* * Zone that has missing private ZSK. */ diff --git a/bin/tests/system/kasp/ns3/setup.sh b/bin/tests/system/kasp/ns3/setup.sh index 55e862856c..7e45193438 100644 --- a/bin/tests/system/kasp/ns3/setup.sh +++ b/bin/tests/system/kasp/ns3/setup.sh @@ -194,7 +194,25 @@ private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$KSK" >> "$infile" private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$ZSK" >> "$infile" $SIGNER -S -x -s now-1w -e now+1w -o $zone -O full -f $zonefile $infile > signer.out.$zone.1 2>&1 -# These signatures are already expired, and the private ZSK is missing. +# These signatures are still good, but the private KSK is missing. +setup ksk-missing.autosign +T="now-6mo" +ksktimes="-P $T -A $T -P sync $T" +zsktimes="-P $T -A $T" +KSK=$($KEYGEN -a $DEFAULT_ALGORITHM -L 300 -f KSK $ksktimes $zone 2> keygen.out.$zone.1) +ZSK=$($KEYGEN -a $DEFAULT_ALGORITHM -L 300 $zsktimes $zone 2> keygen.out.$zone.2) +$SETTIME -s -g $O -d $O $T -k $O $T -r $O $T "$KSK" > settime.out.$zone.1 2>&1 +$SETTIME -s -g $O -k $O $T -z $O $T "$ZSK" > settime.out.$zone.2 2>&1 +cat template.db.in "${KSK}.key" "${ZSK}.key" > "$infile" +private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$KSK" >> "$infile" +private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$ZSK" >> "$infile" +$SIGNER -S -x -s now-1w -e now+1w -o $zone -O full -f $zonefile $infile > signer.out.$zone.1 2>&1 +echo "KSK: yes" >> "${KSK}".state +echo "ZSK: no" >> "${KSK}".state +echo "Lifetime: 63072000" >> "${KSK}".state # PT2Y +rm -f "${KSK}".private + +# These signatures are still good, but the private ZSK is missing. setup zsk-missing.autosign T="now-6mo" ksktimes="-P $T -A $T -P sync $T" @@ -206,7 +224,10 @@ $SETTIME -s -g $O -k $O $T -z $O $T "$ZSK" > settime.out.$zone.2 2>&1 cat template.db.in "${KSK}.key" "${ZSK}.key" > "$infile" private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$KSK" >> "$infile" private_type_record $zone $DEFAULT_ALGORITHM_NUMBER "$ZSK" >> "$infile" -$SIGNER -PS -x -s now-2w -e now-1mi -o $zone -O full -f $zonefile $infile > signer.out.$zone.1 2>&1 +$SIGNER -S -x -s now-1w -e now+1w -o $zone -O full -f $zonefile $infile > signer.out.$zone.1 2>&1 +echo "KSK: no" >> "${ZSK}".state +echo "ZSK: yes" >> "${ZSK}".state +echo "Lifetime: 31536000" >> "${ZSK}".state # PT1Y rm -f "${ZSK}".private # These signatures are already expired, and the private ZSK is retired. diff --git a/bin/tests/system/kasp/tests.sh b/bin/tests/system/kasp/tests.sh index be49ac7574..d8a96c3d9f 100644 --- a/bin/tests/system/kasp/tests.sh +++ b/bin/tests/system/kasp/tests.sh @@ -325,6 +325,27 @@ 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 "offline, policy default" $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 + # # Zone: dynamic.kasp # @@ -1339,6 +1360,25 @@ check_subdomain dnssec_verify check_rrsig_refresh +# +# Zone: ksk-missing.autosign. +# +set_zone "ksk-missing.autosign" +set_policy "autosign" "2" "300" +set_server "ns3" "10.53.0.3" +# Key properties, timings and states same as above. +# Skip checking the private file, because it is missing. +key_set "KEY1" "PRIVATE" "no" + +check_keys +check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" +check_apex +check_subdomain +dnssec_verify + +# Restore the PRIVATE variable. +key_set "KEY1" "PRIVATE" "yes" + # # Zone: zsk-missing.autosign. # @@ -1346,7 +1386,25 @@ set_zone "zsk-missing.autosign" set_policy "autosign" "2" "300" set_server "ns3" "10.53.0.3" # Key properties, timings and states same as above. -# TODO. +# Skip checking the private file, because it is missing. +key_set "KEY2" "PRIVATE" "no" + +check_keys +check_dnssecstatus "$SERVER" "$POLICY" "$ZONE" +# For the apex, we expect the SOA to be signed with the KSK because the ZSK is +# offline. Temporary treat KEY1 as a zone signing key too. +set_keyrole "KEY1" "csk" +set_zonesigning "KEY1" "yes" +set_zonesigning "KEY2" "no" +check_apex +set_keyrole "KEY1" "ksk" +set_zonesigning "KEY1" "no" +set_zonesigning "KEY2" "yes" +check_subdomain +dnssec_verify + +# Restore the PRIVATE variable. +key_set "KEY2" "PRIVATE" "yes" # # Zone: zsk-retired.autosign.