package Zonemaster::Test::DNSSEC v1.0.5; ### ### This test module implements DNSSEC tests. ### use strict; use warnings; use 5.014002; use Zonemaster; use Zonemaster::Util; use Zonemaster::Constants qw[:algo :soa]; use List::Util qw[min]; use List::MoreUtils qw[none]; use Carp; ### Table fetched from IANA on 2014-09-12 Readonly::Hash our %algo_properties => ( 0 => { status => $ALGO_STATUS_RESERVED, description => q{Reserved}, }, 1 => { status => $ALGO_STATUS_DEPRECATED, description => q{RSA/MD5}, mnemonic => q{RSAMD5}, }, 2 => { status => $ALGO_STATUS_VALID, description => q{Diffie-Hellman}, mnemonic => q{DH}, }, 3 => { status => $ALGO_STATUS_VALID, description => q{DSA/SHA1}, mnemonic => q{DSA}, }, 4 => { status => $ALGO_STATUS_RESERVED, description => q{Reserved}, }, 5 => { status => $ALGO_STATUS_VALID, description => q{RSA/SHA1}, mnemonic => q{RSASHA1}, }, 6 => { status => $ALGO_STATUS_VALID, description => q{DSA-NSEC3-SHA1}, mnemonic => q{DSA-NSEC3-SHA1}, }, 7 => { status => $ALGO_STATUS_VALID, description => q{RSASHA1-NSEC3-SHA1}, mnemonic => q{RSASHA1-NSEC3-SHA1}, }, 8 => { status => $ALGO_STATUS_VALID, description => q{RSA/SHA-256}, mnemonic => q{RSA/SHA256}, }, 9 => { status => $ALGO_STATUS_RESERVED, description => q{Reserved}, }, 10 => { status => $ALGO_STATUS_VALID, description => q{RSA/SHA-512}, mnemonic => q{RSA/SHA512}, }, 11 => { status => $ALGO_STATUS_RESERVED, description => q{Reserved}, }, 12 => { status => $ALGO_STATUS_VALID, description => q{GOST R 34.10-2001}, mnemonic => q{ECC-GOST}, }, 13 => { status => $ALGO_STATUS_VALID, description => q{ECDSA Curve P-256 with SHA-256}, mnemonic => q{ECDSAP256SHA256}, }, 14 => { status => $ALGO_STATUS_VALID, description => q{ECDSA Curve P-384 with SHA-384}, mnemonic => q{ECDSAP384SHA384}, }, ( map { $_ => { status => $ALGO_STATUS_UNASSIGNED, description => q{Unassigned}, } } ( 15 .. 122 ) ), ( map { $_ => { status => $ALGO_STATUS_RESERVED, description => q{Reserved}, } } ( 123 .. 251 ) ), 252 => { status => $ALGO_STATUS_RESERVED, description => q{Reserved for Indirect Keys}, mnemonic => q{INDIRECT}, }, 253 => { status => $ALGO_STATUS_PRIVATE, description => q{private algorithm}, mnemonic => q{PRIVATEDNS}, }, 254 => { status => $ALGO_STATUS_PRIVATE, description => q{private algorithm OID}, mnemonic => q{PRIVATEOID}, }, 255 => { status => $ALGO_STATUS_RESERVED, description => q{Reserved}, }, ); ### ### Entry points ### sub all { my ( $class, $zone ) = @_; my @results; if ( Zonemaster->config->should_run('dnssec07') ) { push @results, $class->dnssec07( $zone ); } if ( Zonemaster->config->should_run('dnssec07') and grep { $_->tag eq 'NEITHER_DNSKEY_NOR_DS' } @results ) { push @results, info( NOT_SIGNED => { zone => q{} . $zone->name } ); } else { if ( Zonemaster->config->should_run('dnssec01') ) { push @results, $class->dnssec01( $zone ); } if ( none { $_->tag eq 'NO_DS' } @results ) { if ( Zonemaster->config->should_run('dnssec02') ) { push @results, $class->dnssec02( $zone ); } } if ( Zonemaster->config->should_run('dnssec03') ) { push @results, $class->dnssec03( $zone ); } if ( Zonemaster->config->should_run('dnssec04') ) { push @results, $class->dnssec04( $zone ); } if ( Zonemaster->config->should_run('dnssec05') ) { push @results, $class->dnssec05( $zone ); } if ( grep { $_->tag eq q{DNSKEY_BUT_NOT_DS} or $_->tag eq q{DNSKEY_AND_DS} } @results ) { if ( Zonemaster->config->should_run('dnssec06') ) { push @results, $class->dnssec06( $zone ); } } else { push @results, info( ADDITIONAL_DNSKEY_SKIPPED => {} ); } if ( Zonemaster->config->should_run('dnssec08') ) { push @results, $class->dnssec08( $zone ); } if ( Zonemaster->config->should_run('dnssec09') ) { push @results, $class->dnssec09( $zone ); } if ( Zonemaster->config->should_run('dnssec10') ) { push @results, $class->dnssec10( $zone ); } if ( Zonemaster->config->should_run('dnssec11') ) { push @results, $class->dnssec11( $zone ); } } return @results; } ## end sub all ### ### Metadata Exposure ### sub metadata { my ( $class ) = @_; return { dnssec01 => [ qw( DS_DIGTYPE_OK DS_DIGTYPE_NOT_OK NO_DS ) ], dnssec02 => [ qw( NO_DS DS_FOUND NO_DNSKEY DS_RFC4509_NOT_VALID COMMON_KEYTAGS DS_MATCHES_DNSKEY DS_DOES_NOT_MATCH_DNSKEY DS_MATCH_FOUND DS_MATCH_NOT_FOUND NO_COMMON_KEYTAGS ) ], dnssec03 => [ qw( NO_NSEC3PARAM NO_DNSKEY MANY_ITERATIONS TOO_MANY_ITERATIONS ITERATIONS_OK ) ], dnssec04 => [ qw( RRSIG_EXPIRATION RRSIG_EXPIRED REMAINING_SHORT REMAINING_LONG DURATION_LONG DURATION_OK ) ], dnssec05 => [ qw( ALGORITHM_DEPRECATED ALGORITHM_RESERVED ALGORITHM_UNASSIGNED ALGORITHM_PRIVATE ALGORITHM_OK ALGORITHM_UNKNOWN KEY_DETAILS ) ], dnssec06 => [ qw( EXTRA_PROCESSING_OK EXTRA_PROCESSING_BROKEN ) ], dnssec07 => [ qw( ADDITIONAL_DNSKEY_SKIPPED DNSKEY_BUT_NOT_DS DNSKEY_AND_DS NEITHER_DNSKEY_NOR_DS DS_BUT_NOT_DNSKEY ) ], dnssec08 => [ qw( DNSKEY_SIGNATURE_OK DNSKEY_SIGNATURE_NOT_OK DNSKEY_SIGNED DNSKEY_NOT_SIGNED NO_KEYS_OR_NO_SIGS ) ], dnssec09 => [ qw( NO_KEYS_OR_NO_SIGS_OR_NO_SOA SOA_SIGNATURE_OK SOA_SIGNATURE_NOT_OK SOA_SIGNED SOA_NOT_SIGNED ) ], dnssec10 => [ qw( INVALID_NAME_RCODE NSEC_COVERS NSEC_COVERS_NOT NSEC_SIG_VERIFY_ERROR NSEC_SIGNED NSEC_NOT_SIGNED HAS_NSEC NSEC3_COVERS NSEC3_COVERS_NOT NSEC3_SIG_VERIFY_ERROR NSEC3_SIGNED NSEC3_NOT_SIGNED HAS_NSEC3 ) ], dnssec11 => [ qw( DELEGATION_NOT_SIGNED DELEGATION_SIGNED ), ], }; } ## end sub metadata sub translation { return { "ADDITIONAL_DNSKEY_SKIPPED" => "No DNSKEYs found. Additional tests skipped.", "ALGORITHM_DEPRECATED" => "The DNSKEY with tag {keytag} uses deprecated algorithm number {algorithm}/({description}).", "ALGORITHM_OK" => "The DNSKEY with tag {keytag} uses algorithm number {algorithm}/({description}), which is OK.", "ALGORITHM_RESERVED" => "The DNSKEY with tag {keytag} uses reserved algorithm number {algorithm}/({description}).", "ALGORITHM_UNASSIGNED" => "The DNSKEY with tag {keytag} uses unassigned algorithm number {algorithm}/({description}).", "ALGORITHM_PRIVATE" => "The DNSKEY with tag {keytag} uses private algorithm number {algorithm}/({description}).", "ALGORITHM_UNKNOWN" => "The DNSKEY with tag {keytag} uses unknown algorithm number {algorithm}.", "COMMON_KEYTAGS" => "There are both DS and DNSKEY records with key tags {keytags}.", "DNSKEY_AND_DS" => "{parent} sent a DS record, and {child} a DNSKEY record.", "DNSKEY_BUT_NOT_DS" => "{child} sent a DNSKEY record, but {parent} did not send a DS record.", "DNSKEY_NOT_SIGNED" => "The apex DNSKEY RRset was not correctly signed.", "DNSKEY_SIGNATURE_NOT_OK" => "Signature for DNSKEY with tag {signature} failed to verify with error '{error}'.", "DNSKEY_SIGNATURE_OK" => "A signature for DNSKEY with tag {signature} was correctly signed.", "DNSKEY_SIGNED" => "The apex DNSKEY RRset was correcly signed.", "DS_BUT_NOT_DNSKEY" => "{parent} sent a DS record, but {child} did not send a DNSKEY record.", "DS_DIGTYPE_NOT_OK" => "DS record with keytag {keytag} uses forbidden digest type {digtype}.", "DS_DIGTYPE_OK" => "DS record with keytag {keytag} uses digest type {digtype}, which is OK.", "DS_DOES_NOT_MATCH_DNSKEY" => "DS record with keytag {keytag} and digest type {digtype} does not match the DNSKEY with the same tag.", "DS_FOUND" => "Found DS records with tags {keytags}.", "DS_MATCHES_DNSKEY" => "DS record with keytag {keytag} and digest type {digtype} matches the DNSKEY with the same tag.", "DS_MATCH_FOUND" => "At least one DS record with a matching DNSKEY record was found.", "DS_MATCH_NOT_FOUND" => "No DS record with a matching DNSKEY record was found.", "DS_RFC4509_NOT_VALID" => "Existing DS with digest type 2, while they do not match DNSKEY records, prevent use of DS with digest type 1 (RFC4509, section 3).", "DURATION_LONG" => "RRSIG with keytag {tag} and covering type(s) {types} has a duration of {duration} seconds, which is too long.", "DURATION_OK" => "RRSIG with keytag {tag} and covering type(s) {types} has a duration of {duration} seconds, which is just fine.", "RRSIG_EXPIRATION" => "RRSIG with keytag {tag} and covering type(s) {types} expires at : {date}.", "RRSIG_EXPIRED" => "RRSIG with keytag {tag} and covering type(s) {types} has already expired (expiration is: {expiration}).", "REMAINING_SHORT" => "RRSIG with keytag {tag} and covering type(s) {types} has a remaining validity of {duration} seconds, which is too short.", "REMAINING_LONG" => "RRSIG with keytag {tag} and covering type(s) {types} has a remaining validity of {duration} seconds, which is too long.", "EXTRA_PROCESSING_BROKEN" => "Server at {server} sent {keys} DNSKEY records, and {sigs} RRSIG records.", "EXTRA_PROCESSING_OK" => "Server at {server} sent {keys} DNSKEY records and {sigs} RRSIG records.", "HAS_NSEC" => "The zone has NSEC records.", "HAS_NSEC3" => "The zone has NSEC3 records.", "INVALID_NAME_RCODE" => "When asked for the name {name}, which must not exist, the response had RCODE {rcode}.", "ITERATIONS_OK" => "The number of NSEC3 iterations is {count}, which is OK.", "KEY_DETAILS" => "Key with keytag {keytag} details : Size = {keysize}, Flags ({sep}, {rfc5011}).", "MANY_ITERATIONS" => "The number of NSEC3 iterations is {count}, which is on the high side.", "NEITHER_DNSKEY_NOR_DS" => "There are neither DS nor DNSKEY records for the zone.", "NOT_SIGNED" => "The zone is not signed with DNSSEC.", "NO_COMMON_KEYTAGS" => "No DS record had a DNSKEY with a matching keytag.", "NO_DNSKEY" => "No DNSKEYs were returned.", "NO_DS" => "{from} returned no DS records for {zone}.", "NO_KEYS_OR_NO_SIGS" => "Cannot test DNSKEY signatures, because we got {keys} DNSKEY records and {sigs} RRSIG records.", "NO_KEYS_OR_NO_SIGS_OR_NO_SOA" => "Cannot test SOA signatures, because we got {keys} DNSKEY records, {sigs} RRSIG records and {soas} SOA records.", "NO_NSEC3PARAM" => "{server} returned no NSEC3PARAM records.", "NSEC3_SIG_VERIFY_ERROR" => "Trying to verify NSEC3 RRset with RRSIG {sig} gave error '{error}'.", "NSEC3_COVERS" => "NSEC3 record covers {name}.", "NSEC3_COVERS_NOT" => "NSEC3 record does not cover {name}.", "NSEC3_NOT_SIGNED" => "No signature correctly signed the NSEC3 RRset.", "NSEC3_SIGNED" => "At least one signature correctly signed the NSEC3 RRset.", "NSEC_COVERS" => "NSEC covers {name}.", "NSEC_COVERS_NOT" => "NSEC does not cover {name}.", "NSEC_NOT_SIGNED" => "No signature correctly signed the NSEC RRset.", "NSEC_SIGNED" => "At least one signature correctly signed the NSEC RRset.", "NSEC_SIG_VERIFY_ERROR" => "Trying to verify NSEC RRset with RRSIG {sig} gave error '{error}'.", "SOA_NOT_SIGNED" => "No RRSIG correctly signed the SOA RRset.", "SOA_SIGNATURE_NOT_OK" => "Trying to verify SOA RRset with signature {signature} gave error '{error}'.", "SOA_SIGNATURE_OK" => "RRSIG {signature} correctly signs SOA RRset.", "SOA_SIGNED" => "At least one RRSIG correctly signs the SOA RRset.", "TOO_MANY_ITERATIONS" => "The number of NSEC3 iterations is {count}, which is too high for key length {keylength}.", "DELEGATION_NOT_SIGNED" => "Delegation from parent to child is not properly signed {reason}.", "DELEGATION_SIGNED" => "Delegation from parent to child is properly signed.", }; } ## end sub translation sub policy { return { "ADDITIONAL_DNSKEY_SKIPPED" => "DEBUG", "ALGORITHM_DEPRECATED" => "WARNING", "ALGORITHM_OK" => "INFO", "ALGORITHM_RESERVED" => "ERROR", "ALGORITHM_UNASSIGNED" => "ERROR", "COMMON_KEYTAGS" => "INFO", "DNSKEY_AND_DS" => "DEBUG", "DNSKEY_BUT_NOT_DS" => "WARNING", "DNSKEY_NOT_SIGNED" => "ERROR", "DNSKEY_SIGNATURE_NOT_OK" => "ERROR", "DNSKEY_SIGNATURE_OK" => "DEBUG", "DNSKEY_SIGNED" => "DEBUG", "DS_BUT_NOT_DNSKEY" => "ERROR", "DS_DIGTYPE_NOT_OK" => "ERROR", "DS_DIGTYPE_OK" => "DEBUG", "DS_DOES_NOT_MATCH_DNSKEY" => "ERROR", "DS_FOUND" => "INFO", "DS_MATCHES_DNSKEY" => "INFO", "DS_MATCH_FOUND" => "INFO", "DS_MATCH_NOT_FOUND" => "ERROR", "DS_RFC4509_NOT_VALID" => "ERROR", "DURATION_LONG" => "WARNING", "DURATION_OK" => "DEBUG", "EXTRA_PROCESSING_BROKEN" => "ERROR", "EXTRA_PROCESSING_OK" => "DEBUG", "HAS_NSEC" => "INFO", "HAS_NSEC3" => "INFO", "INVALID_NAME_RCODE" => "NOTICE", "ITERATIONS_OK" => "DEBUG", "KEY_DETAILS" => "DEBUG", "MANY_ITERATIONS" => "NOTICE", "NEITHER_DNSKEY_NOR_DS" => "NOTICE", "NOT_SIGNED" => "NOTICE", "NO_COMMON_KEYTAGS" => "ERROR", "NO_DNSKEY" => "ERROR", "NO_DS" => "NOTICE", "NO_KEYS_OR_NO_SIGS" => "DEBUG", "NO_KEYS_OR_NO_SIGS_OR_NO_SOA" => "DEBUG", "NO_NSEC3PARAM" => "DEBUG", "NSEC3_SIG_VERIFY_ERROR" => "ERROR", "NSEC3_COVERS" => "DEBUG", "NSEC3_COVERS_NOT" => "WARNING", "NSEC3_NOT_SIGNED" => "ERROR", "NSEC3_SIGNED" => "DEBUG", "NSEC_COVERS" => "DEBUG", "NSEC_COVERS_NOT" => "WARNING", "NSEC_NOT_SIGNED" => "ERROR", "NSEC_SIGNED" => "DEBUG", "NSEC_SIG_VERIFY_ERROR" => "ERROR", "REMAINING_LONG" => "WARNING", "REMAINING_SHORT" => "WARNING", "RRSIG_EXPIRATION" => "INFO", "RRSIG_EXPIRED" => "ERROR", "SOA_NOT_SIGNED" => "ERROR", "SOA_SIGNATURE_NOT_OK" => "ERROR", "SOA_SIGNATURE_OK" => "DEBUG", "SOA_SIGNED" => "DEBUG", "TOO_MANY_ITERATIONS" => "WARNING", "DELEGATION_NOT_SIGNED" => "NOTICE", "DELEGATION_SIGNED" => "INFO", }; } ## end sub policy sub version { return "$Zonemaster::Test::DNSSEC::VERSION"; } ### ### Tests ### sub dnssec01 { my ( $class, $zone ) = @_; my @results; my %type = ( 1 => 'SHA-1', 2 => 'SHA-256', 3 => 'GOST R 34.11-94', 4 => 'SHA-384' ); return if not $zone->parent; my $ds_p = $zone->parent->query_one( $zone->name, 'DS', { dnssec => 1 } ); die "No response from parent nameservers" if not $ds_p; my @ds = $ds_p->get_records( 'DS', 'answer' ); if ( @ds == 0 ) { push @results, info( NO_DS => { zone => q{} . $zone->name, from => $ds_p->answerfrom } ); } else { foreach my $ds ( @ds ) { if ( $type{ $ds->digtype } ) { push @results, info( DS_DIGTYPE_OK => { keytag => $ds->keytag, digtype => $type{ $ds->digtype }, } ); } else { push @results, info( DS_DIGTYPE_NOT_OK => { keytag => $ds->keytag, digtype => $ds->digtype } ); } } ## end foreach my $ds ( @ds ) } ## end else [ if ( @ds == 0 ) ] return @results; } ## end sub dnssec01 sub dnssec02 { my ( $class, $zone ) = @_; my @results; return if not $zone->parent; # 1. Retrieve the DS RR set from the parent zone. If there are no DS RR present, exit the test my $ds_p = $zone->parent->query_one( $zone->name, 'DS', { dnssec => 1 } ); die "No response from parent nameservers" if not $ds_p; my %ds = map { $_->keytag => $_ } $ds_p->get_records( 'DS', 'answer' ); if ( scalar( keys %ds ) == 0 ) { push @results, info( NO_DS => { zone => q{} . $zone->name, from => $ds_p->answerfrom, } ); } else { push @results, info( DS_FOUND => { keytags => join( q{:}, map { $_->keytag } values %ds ), } ); # 2. Retrieve the DNSKEY RR set from the child zone. If there are no DNSKEY RR present, then the test case fail my $dnskey_p = $zone->query_one( $zone->name, 'DNSKEY', { dnssec => 1 } ); my %dnskey; %dnskey = map { $_->keytag => $_ } $dnskey_p->get_records( 'DNSKEY', 'answer' ) if $dnskey_p; if ( scalar( keys %dnskey ) == 0 ) { push @results, info( NO_DNSKEY => {} ); return @results; } # Pick out keys with a tag that a DS has using a hash slice my @common = grep { exists $ds{$_->keytag} } values %dnskey; if ( @common ) { push @results, info( COMMON_KEYTAGS => { keytags => join( q{:}, map { $_->keytag } @common ), } ); my $found = 0; my $rfc4509_compliant = 1; # 4. Match all DS RR with type digest algorithm “2†with DNSKEY RR from the child. If no DS RRs with algorithm 2 matches a # DNSKEY RR from the child, this test case fails. my %ds_digtype2 = map { $_->keytag => $_ } grep { $_->digtype == 2 } $ds_p->get_records( 'DS', 'answer' ); if ( scalar( keys %ds_digtype2 ) >= 1 ) { @common = grep { exists $ds_digtype2{$_->keytag} } values %dnskey; foreach my $key ( @common ) { if ( $ds_digtype2{ $key->keytag }->verify( $key ) ) { push @results, info( DS_MATCHES_DNSKEY => { keytag => $key->keytag, digtype => 2, } ); $found = 1; } else { push @results, info( DS_DOES_NOT_MATCH_DNSKEY => { keytag => $key->keytag, digtype => 2, } ); } } if ( not grep { $_->tag eq q{DS_MATCHES_DNSKEY} } @results ) { $rfc4509_compliant = 0; push @results, info( DS_RFC4509_NOT_VALID => {} ); } } # 5. Match all DS RR with type digest algorithm “1†with DNSKEY RR from the child. If no DS RRs with algorithm 1 matches a # DNSKEY RR from the child, this test case fails. my %ds_digtype1 = map { $_->keytag => $_ } grep { $_->digtype == 1 } $ds_p->get_records( 'DS', 'answer' ); @common = grep { exists $ds_digtype1{$_->keytag} } values %dnskey; foreach my $key ( @common ) { if ( $ds_digtype1{ $key->keytag }->verify( $key ) ) { push @results, info( DS_MATCHES_DNSKEY => { keytag => $key->keytag, digtype => 1, } ); $found = 1; } else { push @results, info( DS_DOES_NOT_MATCH_DNSKEY => { keytag => $key->keytag, digtype => 1, } ); } } if ( $found ) { push @results, info( DS_MATCH_FOUND => {} ); } else { push @results, info( DS_MATCH_NOT_FOUND => {} ); } } ## end if ( @common ) else { # 3. If no Key Tag from the DS RR matches any Key Tag from the DNSKEY RR, this test case fails push @results, info( NO_COMMON_KEYTAGS => { dstags => join( q{:}, keys %ds ), dnskeytags => join( q{:}, keys %dnskey ), } ); } } ## end else [ if ( scalar( keys %ds ...))] return @results; } ## end sub dnssec02 sub dnssec03 { my ( $self, $zone ) = @_; my @results; my $param_p = $zone->query_one( $zone->name, 'NSEC3PARAM', { dnssec => 1 } ); my @nsec3params; @nsec3params = $param_p->get_records( 'NSEC3PARAM', 'answer' ) if $param_p; if ( @nsec3params == 0 ) { push @results, info( NO_NSEC3PARAM => { server => ( $param_p ? $param_p->answerfrom : '<no response>' ), } ); } else { my $dk_p = $zone->query_one( $zone->name, 'DNSKEY', { dnssec => 1 } ); my @dnskey; @dnskey = $dk_p->get_records( 'DNSKEY', 'answer' ) if $dk_p; my $min_len = 0; if ( @dnskey ) { $min_len = min map { $_->keysize } @dnskey; # Do rounding as per RFC5155 section 10.3 if ($min_len > 2048) { $min_len = 4096; } elsif ($min_len > 1024) { $min_len = 2048; } else { $min_len = 1024; } } else { push @results, info( NO_DNSKEY => {} ); } foreach my $n3p ( @nsec3params ) { my $iter = $n3p->iterations; if ( $iter > 100 ) { push @results, info( MANY_ITERATIONS => { count => $iter, } ); if ( ( $min_len >= 4096 and $iter > 2500 ) or ( $min_len < 4096 and $min_len >= 2048 and $iter > 500 ) or ( $min_len < 2048 and $min_len >= 1024 and $iter > 150 ) ) { push @results, info( TOO_MANY_ITERATIONS => { count => $iter, keylength => $min_len, } ); } } ## end if ( $iter > 100 ) elsif ( $min_len > 0 ) { push @results, info( ITERATIONS_OK => { count => $iter, } ); } } ## end foreach my $n3p ( @nsec3params) } ## end else [ if ( @nsec3params == 0)] return @results; } ## end sub dnssec03 sub dnssec04 { my ( $self, $zone ) = @_; my @results; my $key_p = $zone->query_one( $zone->name, 'DNSKEY', { dnssec => 1 } ); if ( not $key_p ) { return; } my @keys = $key_p->get_records( 'DNSKEY', 'answer' ); my @key_sigs = $key_p->get_records( 'RRSIG', 'answer' ); my $soa_p = $zone->query_one( $zone->name, 'SOA', { dnssec => 1 } ); if ( not $soa_p ) { return; } my @soas = $soa_p->get_records( 'SOA', 'answer' ); my @soa_sigs = $soa_p->get_records( 'RRSIG', 'answer' ); foreach my $sig ( @key_sigs, @soa_sigs ) { my $duration = $sig->expiration - $sig->inception; my $remaining = $sig->expiration - int( $key_p->timestamp ); push @results, info( RRSIG_EXPIRATION => { date => scalar( gmtime($sig->expiration) ), tag => $sig->keytag, types => $sig->typecovered, } ); if ( $remaining < 0 ) { # already expired push @results, info( RRSIG_EXPIRED => { expiration => $sig->expiration, tag => $sig->keytag, types => $sig->typecovered, } ); } elsif ( $remaining < ( $DURATION_12_HOURS_IN_SECONDS ) ) { push @results, info( REMAINING_SHORT => { duration => $remaining, tag => $sig->keytag, types => $sig->typecovered, } ); } elsif ( $remaining > ( $DURATION_180_DAYS_IN_SECONDS ) ) { push @results, info( REMAINING_LONG => { duration => $remaining, tag => $sig->keytag, types => $sig->typecovered, } ); } elsif ( $duration > ( $DURATION_180_DAYS_IN_SECONDS ) ) { push @results, info( DURATION_LONG => { duration => $duration, tag => $sig->keytag, types => $sig->typecovered, } ); } else { push @results, info( DURATION_OK => { duration => $duration, tag => $sig->keytag, types => $sig->typecovered, } ); } } ## end foreach my $sig ( @key_sigs...) return @results; } ## end sub dnssec04 sub dnssec05 { my ( $self, $zone ) = @_; my @results; my $key_p = $zone->query_one( $zone->name, 'DNSKEY', { dnssec => 1 } ); if ( not $key_p ) { return; } my @keys = $key_p->get_records( 'DNSKEY', 'answer' ); foreach my $key ( @keys ) { my $algo = $key->algorithm; if ( $algo_properties{$algo}{status} == $ALGO_STATUS_DEPRECATED ) { push @results, info( ALGORITHM_DEPRECATED => { algorithm => $algo, keytag => $key->keytag, description => $algo_properties{$algo}{description}, } ); } elsif ( $algo_properties{$algo}{status} == $ALGO_STATUS_RESERVED ) { push @results, info( ALGORITHM_RESERVED => { algorithm => $algo, keytag => $key->keytag, description => $algo_properties{$algo}{description}, } ); } elsif ( $algo_properties{$algo}{status} == $ALGO_STATUS_UNASSIGNED ) { push @results, info( ALGORITHM_UNASSIGNED => { algorithm => $algo, keytag => $key->keytag, description => $algo_properties{$algo}{description}, } ); } elsif ( $algo_properties{$algo}{status} == $ALGO_STATUS_PRIVATE ) { push @results, info( ALGORITHM_PRIVATE => { algorithm => $algo, keytag => $key->keytag, description => $algo_properties{$algo}{description}, } ); } elsif ( $algo_properties{$algo}{status} == $ALGO_STATUS_VALID ) { push @results, info( ALGORITHM_OK => { algorithm => $algo, keytag => $key->keytag, description => $algo_properties{$algo}{description}, } ); if ( $key->flags & 256 ) { # This is a Key push @results, info( KEY_DETAILS => { keytag => $key->keytag, keysize => $key->keysize, sep => $key->flags & 1 ? q{SEP bit set} : q{SEP bit *not* set}, rfc5011 => $key->flags & 128 ? q{RFC 5011 revocation bit set} : q{RFC 5011 revocation bit *not* set}, } ); } } else { push @results, info( ALGORITHM_UNKNOWN => { algorithm => $algo, keytag => $key->keytag, } ); } } ## end foreach my $key ( @keys ) return @results; } ## end sub dnssec05 sub dnssec06 { my ( $self, $zone ) = @_; my @results; my $key_aref = $zone->query_all( $zone->name, 'DNSKEY', { dnssec => 1 } ); foreach my $key_p ( @{$key_aref} ) { next if not $key_p; my @keys = $key_p->get_records( 'DNSKEY', 'answer' ); my @sigs = $key_p->get_records( 'RRSIG', 'answer' ); if ( @sigs > 0 and @keys > 0 ) { push @results, info( EXTRA_PROCESSING_OK => { server => $key_p->answerfrom, keys => scalar( @keys ), sigs => scalar( @sigs ), } ); } elsif ( $key_p->rcode eq q{NOERROR} and ( @sigs == 0 or @keys == 0 ) ) { push @results, info( EXTRA_PROCESSING_BROKEN => { server => $key_p->answerfrom, keys => scalar( @keys ), sigs => scalar( @sigs ) } ); } } ## end foreach my $key_p ( @{$key_aref...}) return @results; } ## end sub dnssec06 sub dnssec07 { my ( $self, $zone ) = @_; my @results; return if not $zone->parent; my $key_p = $zone->query_one( $zone->name, 'DNSKEY', { dnssec => 1 } ); if ( not $key_p ) { return; } my ( $dnskey ) = $key_p->get_records( 'DNSKEY', 'answer' ); my $ds_p = $zone->parent->query_one( $zone->name, 'DS', { dnssec => 1 } ); if ( not $ds_p ) { return; } my ( $ds ) = $ds_p->get_records( 'DS', 'answer' ); if ( $dnskey and not $ds ) { push @results, info( DNSKEY_BUT_NOT_DS => { child => $key_p->answerfrom, parent => $ds_p->answerfrom, } ); } elsif ( $dnskey and $ds ) { push @results, info( DNSKEY_AND_DS => { child => $key_p->answerfrom, parent => $ds_p->answerfrom, } ); } elsif ( not $dnskey and $ds ) { push @results, info( DS_BUT_NOT_DNSKEY => { child => $key_p->answerfrom, parent => $ds_p->answerfrom, } ); } else { push @results, info( NEITHER_DNSKEY_NOR_DS => { child => $key_p->answerfrom, parent => $ds_p->answerfrom, } ); } return @results; } ## end sub dnssec07 sub dnssec08 { my ( $self, $zone ) = @_; my @results; my $key_p = $zone->query_one( $zone->name, 'DNSKEY', { dnssec => 1 } ); if ( not $key_p ) { return; } my @dnskeys = $key_p->get_records( 'DNSKEY', 'answer' ); my @sigs = $key_p->get_records( 'RRSIG', 'answer' ); if ( @dnskeys == 0 or @sigs == 0 ) { push @results, info( NO_KEYS_OR_NO_SIGS => { keys => scalar( @dnskeys ), sigs => scalar( @sigs ), } ); return @results; } my $ok = 0; foreach my $sig ( @sigs ) { my $msg = q{}; my $time = $key_p->timestamp; if ( $sig->verify_time( \@dnskeys, \@dnskeys, $time, $msg ) ) { push @results, info( DNSKEY_SIGNATURE_OK => { signature => $sig->keytag, } ); $ok = $sig->keytag; } else { if ($sig->algorithm == 12 and $msg =~ /Unknown cryptographic algorithm/) { $msg = 'no GOST support'; } push @results, info( DNSKEY_SIGNATURE_NOT_OK => { signature => $sig->keytag, error => $msg, time => $time, } ); } } ## end foreach my $sig ( @sigs ) if ( $ok ) { push @results, info( DNSKEY_SIGNED => { keytag => $ok, } ); } else { push @results, info( DNSKEY_NOT_SIGNED => {} ); } return @results; } ## end sub dnssec08 sub dnssec09 { my ( $self, $zone ) = @_; my @results; my $key_p = $zone->query_one( $zone->name, 'DNSKEY', { dnssec => 1 } ); if ( not $key_p ) { return; } my @dnskeys = $key_p->get_records( 'DNSKEY', 'answer' ); my $soa_p = $zone->query_one( $zone->name, 'SOA', { dnssec => 1 } ); if ( not $soa_p ) { return; } my @soa = $soa_p->get_records( 'SOA', 'answer' ); my @sigs = $soa_p->get_records( 'RRSIG', 'answer' ); if ( @dnskeys == 0 or @sigs == 0 or @soa == 0 ) { push @results, info( NO_KEYS_OR_NO_SIGS_OR_NO_SOA => { keys => scalar( @dnskeys ), sigs => scalar( @sigs ), soas => scalar( @soa ), } ); return @results; } my $ok = 0; foreach my $sig ( @sigs ) { my $msg = q{}; my $time = $soa_p->timestamp; if ( $sig->verify_time( \@soa, \@dnskeys, $time, $msg ) ) { push @results, info( SOA_SIGNATURE_OK => { signature => $sig->keytag, } ); $ok = $sig->keytag; } else { if ($sig->algorithm == 12 and $msg =~ /Unknown cryptographic algorithm/) { $msg = 'no GOST support'; } push @results, info( SOA_SIGNATURE_NOT_OK => { signature => $sig->keytag, error => $msg, } ); } } ## end foreach my $sig ( @sigs ) if ( $ok ) { push @results, info( SOA_SIGNED => { keytag => $ok, } ); } else { push @results, info( SOA_NOT_SIGNED => {} ); } return @results; } ## end sub dnssec09 sub dnssec10 { my ( $self, $zone ) = @_; my @results; my $key_p = $zone->query_one( $zone->name, 'DNSKEY', { dnssec => 1 } ); if ( not $key_p ) { return; } my @dnskeys = $key_p->get_records( 'DNSKEY', 'answer' ); my $name = $zone->name->prepend( 'xx--example' ); my $test_p = $zone->query_one( $name, 'A', { dnssec => 1 } ); if ( not $test_p ) { return; } if ( $test_p->rcode ne 'NXDOMAIN' and $test_p->rcode ne 'NOERROR' ) { push @results, info( INVALID_NAME_RCODE => { name => $name, rcode => $test_p->rcode, } ); return @results; } my @nsec = $test_p->get_records( 'NSEC', 'authority' ); if ( @nsec ) { push @results, info( HAS_NSEC => {} ); my $covered = 0; foreach my $nsec ( @nsec ) { if ( $nsec->covers( $name ) ) { $covered = 1; my @sigs = grep { $_->typecovered eq 'NSEC' } $test_p->get_records_for_name( 'RRSIG', $nsec->name ); my $ok = 0; foreach my $sig ( @sigs ) { my $msg = q{}; if (@dnskeys == 0) { push @results, info( NSEC_SIG_VERIFY_ERROR => { error => 'DNSKEY missing', sig => $sig->keytag } ); } elsif ( $sig->verify_time( [ grep { name( $_->name ) eq name( $sig->name ) } @nsec ], \@dnskeys, $test_p->timestamp, $msg ) ) { $ok = 1; } else { if ($sig->algorithm == 12 and $msg =~ /Unknown cryptographic algorithm/) { $msg = 'no GOST support'; } push @results, info( NSEC_SIG_VERIFY_ERROR => { error => $msg, sig => $sig->keytag, } ); } if ( $ok ) { push @results, info( NSEC_SIGNED => {} ); } else { push @results, info( NSEC_NOT_SIGNED => {} ); } } ## end foreach my $sig ( @sigs ) } ## end if ( $nsec->covers( $name...)) } ## end foreach my $nsec ( @nsec ) if ( $covered ) { push @results, info( NSEC_COVERS => { name => $name, } ); } else { push @results, info( NSEC_COVERS_NOT => { name => $name, } ); } } ## end if ( @nsec ) my @nsec3 = $test_p->get_records( 'NSEC3', 'authority' ); if ( @nsec3 ) { my $covered = 0; push @results, info( HAS_NSEC3 => {} ); foreach my $nsec3 ( @nsec3 ) { if ( $nsec3->covers( $name ) ) { $covered = 1; my @sigs = grep { $_->typecovered eq 'NSEC3' } $test_p->get_records_for_name( 'RRSIG', $nsec3->name ); my $ok = 0; foreach my $sig ( @sigs ) { my $msg = q{}; if ( $sig->verify_time( [ grep { name( $_->name ) eq name( $sig->name ) } @nsec3 ], \@dnskeys, $test_p->timestamp, $msg ) ) { $ok = 1; } else { if ($sig->algorithm == 12 and $msg =~ /Unknown cryptographic algorithm/) { $msg = 'no GOST support'; } push @results, info( NSEC3_SIG_VERIFY_ERROR => { sig => $sig->keytag, error => $msg, } ); } if ( $ok ) { push @results, info( NSEC3_SIGNED => {} ); } else { push @results, info( NSE3C_NOT_SIGNED => {} ); } } ## end foreach my $sig ( @sigs ) } ## end if ( $nsec3->covers( $name...)) } ## end foreach my $nsec3 ( @nsec3 ) if ( $covered ) { push @results, info( NSEC3_COVERS => { name => $name, } ); } else { push @results, info( NSEC3_COVERS_NOT => { name => $name, } ); } } ## end if ( @nsec3 ) return @results; } ## end sub dnssec10 ### The error reporting in dnssec11 is deliberately simple, since the point of ### the test case is to give a pass/fail test for the delegation step from the ### parent as a whole. sub dnssec11 { my ( $class, $zone ) = @_; my @results; my $ds_p = $zone->parent->query_auth( $zone->name->string, 'DS' ); if ( not $ds_p ) { return info( DELEGATION_NOT_SIGNED => { keytag => 'none', reason => 'no_ds_packet' } ); } my $dnskey_p = $zone->query_auth( $zone->name->string, 'DNSKEY', { dnssec => 1 } ); if ( not $dnskey_p ) { return info( DELEGATION_NOT_SIGNED => { keytag => 'none', reason => 'no_dnskey_packet' } ); } my %ds = map { $_->keytag => $_ } $ds_p->get_records_for_name( 'DS', $zone->name->string ); my %dnskey = map { $_->keytag => $_ } $dnskey_p->get_records_for_name( 'DNSKEY', $zone->name->string ); my %rrsig = map { $_->keytag => $_ } $dnskey_p->get_records_for_name( 'RRSIG', $zone->name->string ); my $pass = 0; my @fail; if ( scalar( keys %ds ) > 0 ) { foreach my $tag ( keys %ds ) { my $ds = $ds{$tag}; my $key = $dnskey{$tag}; my $sig = $rrsig{$tag}; if ( $key ) { if ( $ds->verify( $key ) ) { if ( $sig ) { my $msg = ''; my $ok = $sig->verify_time( [ values %dnskey ], [ values %dnskey ], $dnskey_p->timestamp, $msg ); if ( $ok ) { $pass = $tag; } else { if ($sig->algorithm == 12 and $msg =~ /Unknown cryptographic algorithm/) { $msg = 'no GOST support'; } push @fail, "signature: $msg" ; } } else { push @fail, 'no_signature'; } } else { push @fail, 'dnskey_no_match'; } } ## end if ( $key ) else { push @fail, 'no_dnskey'; } } ## end foreach my $tag ( keys %ds ) } ## end if ( scalar( keys %ds ...)) else { push @fail, 'no_ds'; } if ($pass) { push @results, info( DELEGATION_SIGNED => { keytag => $pass } ) } else { push @results, info( DELEGATION_NOT_SIGNED => { keytag => 'info', reason => join(';', @fail) } ) } return @results; } ## end sub dnssec11 1; =head1 NAME Zonemaster::Test::DNSSEC - dnssec module showing the expected structure of Zonemaster test modules =head1 SYNOPSIS my @results = Zonemaster::Test::DNSSEC->all($zone); =head1 METHODS =over =item all($zone) Runs the default set of tests and returns a list of log entries made by the tests. =item metadata() Returns a reference to a hash, the keys of which are the names of all test methods in the module, and the corresponding values are references to lists with all the tags that the method can use in log entries. =item translation() Returns a reference to a nested hash, where the outermost keys are language codes, the keys below that are message tags and their values are translation strings. =item policy() Returns a reference to a hash with the default policy for the module. The keys are message tags, and the corresponding values are their default log levels. =item version() Returns a version string for the module. =back =head1 TESTS =over =item dnssec01($zone) Verifies that all DS records have digest types registered with IANA. =item dnssec02($zone) Verifies that all DS records have a matching DNSKEY. =item dnssec03($zone) Check iteration counts for NSEC3. =item dnssec04($zone) Checks the durations of the signatures for the DNSKEY and SOA RRsets. =item dnssec05($zone) Check DNSKEY algorithms. =item dnssec06($zone) Check for DNSSEC extra processing at child nameservers. =item dnssec07($zone) Check that both DS and DNSKEY are present. =item dnssec08($zone) Check that the DNSKEY RRset is signed. =item dnssec09($zone) Check that the SOA RRset is signed. =item dnssec10($zone) Check for the presence of either NSEC or NSEC3, with proper coverage and signatures. =item dnssec11($zone) Check that the delegation step from parent is properly signed. =back =cut