The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Zonemaster::Engine::Test::DNSSEC;

use version; our $VERSION = version->declare("v1.0.7");

###
### This test module implements DNSSEC tests.
###

use strict;
use warnings;

use 5.014002;

use Zonemaster::Engine;
use Zonemaster::Engine::Util;
use Zonemaster::Engine::Constants qw[:algo :soa];
use List::Util qw[min];
use List::MoreUtils qw[none];

use Carp;

### Table fetched from IANA on 2017-03-09
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},
    },
    15 => {
        status      => $ALGO_STATUS_VALID,
        description => q{Ed25519},
        mnemonic    => q{Ed25519},
    },
    16 => {
        status      => $ALGO_STATUS_VALID,
        description => q{Ed448},
        mnemonic    => q{Ed448},
    },
    (
        map { $_ => { status => $ALGO_STATUS_UNASSIGNED, description => q{Unassigned}, } } ( 17 .. 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::Engine->config->should_run('dnssec07') ) {
        push @results, $class->dnssec07( $zone );
    }

    if ( Zonemaster::Engine->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::Engine->config->should_run('dnssec01') ) {
            push @results, $class->dnssec01( $zone );
        }

        if ( none { $_->tag eq 'NO_DS' } @results ) {
            if ( Zonemaster::Engine->config->should_run('dnssec02') ) {
                push @results, $class->dnssec02( $zone );
            }
        }

        if ( Zonemaster::Engine->config->should_run('dnssec03') ) {
            push @results, $class->dnssec03( $zone );
        }

        if ( Zonemaster::Engine->config->should_run('dnssec04') ) {
            push @results, $class->dnssec04( $zone );
        }

        if ( Zonemaster::Engine->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::Engine->config->should_run('dnssec06') ) {
                push @results, $class->dnssec06( $zone );
            }
        }
        else {
            push @results,
              info( ADDITIONAL_DNSKEY_SKIPPED => {} );
        }

        if ( Zonemaster::Engine->config->should_run('dnssec08') ) {
            push @results, $class->dnssec08( $zone );
        }

        if ( Zonemaster::Engine->config->should_run('dnssec09') ) {
            push @results, $class->dnssec09( $zone );
        }

        if ( Zonemaster::Engine->config->should_run('dnssec10') ) {
            push @results, $class->dnssec10( $zone );
        }

        if ( Zonemaster::Engine->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
              HAS_NSEC3_OPTOUT )
        ],
        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.",
        "HAS_NSEC3_OPTOUT"        => "The zone has NSEC3 opt-out 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",
        "HAS_NSEC3_OPTOUT"             => "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::Engine::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;
        my $opt_out = 0;
        push @results, info( HAS_NSEC3 => {} );
        foreach my $nsec3 ( @nsec3 ) {
            if ( $nsec3->optout ) {
                $opt_out = 1;
            }
            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,
                }
              );
        }
        if ( $opt_out ) {
            push @results, info( HAS_NSEC3_OPTOUT => {} );
        }
    } ## 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::Engine::Test::DNSSEC - dnssec module showing the expected structure of Zonemaster test modules

=head1 SYNOPSIS

    my @results = Zonemaster::Engine::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