package Zonemaster::Engine::Test::DNSSEC;

use v5.16.0;
use warnings;

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

use Carp;
use List::Compare;
use List::MoreUtils qw[any uniq];
use List::Util qw[min];
use Locale::TextDomain qw[Zonemaster-Engine];
use Readonly;

use Zonemaster::LDNS::RR;
use Zonemaster::Engine::Profile;
use Zonemaster::Engine::Constants qw[:algo :soa :ip];
use Zonemaster::Engine::Util qw[name should_run_test];
use Zonemaster::Engine::TestMethods;
use Zonemaster::Engine::TestMethodsV2;

=head1 NAME

Zonemaster::Engine::Test::DNSSEC - Module implementing tests focused on DNSSEC

=head1 SYNOPSIS

    my @results = Zonemaster::Engine::Test::DNSSEC->all( $zone );

=cut

### Table fetched from IANA on 2025-07-15
Readonly::Hash our %algo_properties => (
    0 => {
        status      => $ALGO_STATUS_NOT_ZONE_SIGN,
        description => q{Delete DS},
        mnemonic    => q{DELETE},
        sig         => 0,
    },
    1 => {
        status      => $ALGO_STATUS_DEPRECATED,
        description => q{RSA/MD5},
        mnemonic    => q{RSAMD5},
        sig         => 0,
    },
    2 => {
        status      => $ALGO_STATUS_NOT_ZONE_SIGN,
        description => q{Diffie-Hellman},
        mnemonic    => q{DH},
        sig         => 0,
    },
    3 => {
        status      => $ALGO_STATUS_DEPRECATED,
        description => q{DSA/SHA1},
        mnemonic    => q{DSA},
        sig         => 1,
    },
    4 => {
        status      => $ALGO_STATUS_RESERVED,
        description => q{Reserved},
        mnemonic    => q{RESERVED},
    },
    5 => {
        status      => $ALGO_STATUS_DEPRECATED,
        description => q{RSA/SHA1},
        mnemonic    => q{RSASHA1},
        sig         => 1,
    },
    6 => {
        status      => $ALGO_STATUS_DEPRECATED,
        description => q{DSA-NSEC3-SHA1},
        mnemonic    => q{DSA-NSEC3-SHA1},
        sig         => 1,
    },
    7 => {
        status      => $ALGO_STATUS_DEPRECATED,
        description => q{RSASHA1-NSEC3-SHA1},
        mnemonic    => q{RSASHA1-NSEC3-SHA1},
        sig         => 1,
    },
    8 => {
        status      => $ALGO_STATUS_OTHER,
        description => q{RSA/SHA-256},
        mnemonic    => q{RSASHA256},
        sig         => 1,
    },
    9 => {
        status      => $ALGO_STATUS_RESERVED,
        description => q{Reserved},
        mnemonic    => q{RESERVED},
    },
    10 => {
        status      => $ALGO_STATUS_NOT_RECOMMENDED,
        description => q{RSA/SHA-512},
        mnemonic    => q{RSASHA512},
        sig         => 1,
    },
    11 => {
        status      => $ALGO_STATUS_RESERVED,
        description => q{Reserved},
        mnemonic    => q{RESERVED},
    },
    12 => {
        status      => $ALGO_STATUS_DEPRECATED,
        description => q{GOST R 34.10-2001},
        mnemonic    => q{ECC-GOST},
        sig         => 1,
    },
    13 => {
        status      => $ALGO_STATUS_OTHER,
        description => q{ECDSA Curve P-256 with SHA-256},
        mnemonic    => q{ECDSAP256SHA256},
        sig         => 1,
    },
    14 => {
        status      => $ALGO_STATUS_OTHER,
        description => q{ECDSA Curve P-384 with SHA-384},
        mnemonic    => q{ECDSAP384SHA384},
        sig         => 1,
    },
    15 => {
        status      => $ALGO_STATUS_OTHER,
        description => q{Ed25519},
        mnemonic    => q{ED25519},
        sig         => 1,
    },
    16 => {
        status      => $ALGO_STATUS_OTHER,
        description => q{Ed448},
        mnemonic    => q{ED448},
        sig         => 1,
    },
    17 => {
        status      => $ALGO_STATUS_OTHER,
        description => q{SM2 signing algo w SM3 hash algo},
        mnemonic    => q{SM2SM3},
        sig         => 1,
    },
    (
        map { $_ => { status => $ALGO_STATUS_UNASSIGNED, description => q{Unassigned}, mnemonic => q{UNASSIGNED} } } ( 18 .. 22 )
    ),
    23 => {
        status      => $ALGO_STATUS_OTHER,
        description => q{GOST R 34.10-2012},
        mnemonic    => q{ECC-GOST12},
        sig         => 1,
    },
    (
        map { $_ => { status => $ALGO_STATUS_UNASSIGNED, description => q{Unassigned}, mnemonic => q{UNASSIGNED} } } ( 24 .. 122 )
    ),
    (
        map { $_ => { status => $ALGO_STATUS_RESERVED, description => q{Reserved}, mnemonic => q{RESERVED} } } ( 123 .. 251 )
    ),
    252 => {
        status      => $ALGO_STATUS_NOT_ZONE_SIGN,
        description => q{Reserved for Indirect Keys},
        mnemonic    => q{INDIRECT},
        sig         => 0,
    },
    253 => {
        status      => $ALGO_STATUS_PRIVATE,
        description => q{private algorithm},
        mnemonic    => q{PRIVATEDNS},
        sig         => 1,
    },
    254 => {
        status      => $ALGO_STATUS_PRIVATE,
        description => q{private algorithm OID},
        mnemonic    => q{PRIVATEOID},
        sig         => 1,
    },
    255 => {
        status      => $ALGO_STATUS_RESERVED,
        description => q{Reserved},
        mnemonic    => q{RESERVED},
    },
);

Readonly::Hash our %rsa_key_size_details => (
    5 => {
        min_size  => 512,
        max_size  => 4096,
        rec_size  => 2048,
        reference => q{RFC 3110},
    },
    7 => {
        min_size  => 512,
        max_size  => 4096,
        rec_size  => 2048,
        reference => q{RFC 5155},
    },
    8 => {
        min_size  => 512,
        max_size  => 4096,
        rec_size  => 2048,
        reference => q{RFC 5702},
    },
    10 => {
        min_size  => 1024,
        max_size  => 4096,
        rec_size  => 2048,
        reference => q{RFC 5702},
    },
);

Readonly::Hash our %digest_algorithms => (
    0 => q{Reserved},
    1 => q{SHA-1},
    2 => q{SHA-256},
    3 => q{GOST R 34.11-94},
    4 => q{SHA-384},
    5 => q{GOST R 34.11-2012},
    6 => q{SM3},
    (
        map { $_ => q{Unassigned} } ( 7 .. 127 )
    ),
    (
        map { $_ => q{Reserved} } ( 128 .. 252 )
    ),
        (
        map { $_ => q{Reserved for Private Use} } ( 253 .. 254 )
    ),
    255 => q{Unassigned},
);

Readonly::Hash our %LDNS_digest_algorithms_supported => (
    1 => q{sha1},
    2 => q{sha256},
    3 => q{gost},
    4 => q{sha384},
);

Readonly::Hash our %dnssec01_tags_mapping => (
    0 => q{DS01_DS_ALGO_NOT_DS},
    1 => q{DS01_DS_ALGO_DEPRECATED},
    2 => q{DS01_DS_ALGO_OK},
    3 => q{DS01_DS_ALGO_DEPRECATED},
    4 => q{DS01_DS_ALGO_OK},
    5 => q{DS01_DS_ALGO_OK},
    6 => q{DS01_DS_ALGO_OK},
    (
        map { $_ => q{DS01_DS_ALGO_UNASSIGNED} } ( 7 .. 127 )
    ),
    (
        map { $_ => q{DS01_DS_ALGO_RESERVED} } ( 128 .. 252 )
    ),
        (
        map { $_ => q{DS01_DS_ALGO_PRIVATE} } ( 253 .. 254 )
    ),
    255 => q{DS01_DS_ALGO_UNASSIGNED},
);

Readonly::Hash our %dnssec05_tags_mapping => (
    0 => q{DS05_ALGO_NOT_ZONE_SIGN},
    1 => q{DS05_ALGO_DEPRECATED},
    2 => q{DS05_ALGO_NOT_ZONE_SIGN},
    3 => q{DS05_ALGO_DEPRECATED},
    4 => q{DS05_ALGO_RESERVED},
    5 => q{DS05_ALGO_DEPRECATED},
    6 => q{DS05_ALGO_DEPRECATED},
    7 => q{DS05_ALGO_DEPRECATED},
    8 => q{DS05_ALGO_OK},
    9 => q{DS05_ALGO_RESERVED},
    10 => q{DS05_ALGO_NOT_RECOMMENDED},
    11 => q{DS05_ALGO_RESERVED},
    12 => q{DS05_ALGO_DEPRECATED},
    13 => q{DS05_ALGO_OK},
    14 => q{DS05_ALGO_OK},
    15 => q{DS05_ALGO_OK},
    16 => q{DS05_ALGO_OK},
    17 => q{DS05_ALGO_OK},
    (
        map { $_ => q{DS05_ALGO_UNASSIGNED} } ( 18 .. 22 )
    ),
    23 => q{DS05_ALGO_OK},
    (
        map { $_ => q{DS05_ALGO_UNASSIGNED} } ( 24 .. 122 )
    ),
    (
        map { $_ => q{DS05_ALGO_RESERVED} } ( 123 .. 251 )
    ),
    252 => q{DS05_ALGO_NOT_ZONE_SIGN},
    253 => q{DS05_ALGO_PRIVATE},
    254 => q{DS05_ALGO_PRIVATE},
    255 => q{DS05_ALGO_RESERVED},
);

=head1 METHODS

=over

=item all()

    my @logentry_array = all( $zone );

Runs the default set of tests for that module, i.e. between L<one and seventeen tests|/TESTS> depending on the tested zone.
If L<DNSSEC07|/dnssec07()> finds no DNSKEY nor DS RRs, no other test is run. If L<DNSSEC07|/dnssec07()> finds a DNSKEY RR, L<DNSSEC06|/dnssec06()> is run.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub all {
    my ( $class, $zone ) = @_;
    my @results;

    if ( should_run_test( q{dnssec07} ) ) {
        push @results, $class->dnssec07( $zone );
    }

    if ( should_run_test( q{dnssec11} ) ) {
        push @results, $class->dnssec11( $zone );
    }

    if ( any { $_->tag eq 'DS07_NOT_SIGNED' } @results ) {
        return @results;
    }

    if ( should_run_test( q{dnssec01} ) ) {
        push @results, $class->dnssec01( $zone );
    }

    if ( should_run_test( q{dnssec02} ) ) {
        push @results, $class->dnssec02( $zone );
    }

    if ( should_run_test( q{dnssec03} ) ) {
        push @results, $class->dnssec03( $zone );
    }

    if ( should_run_test( q{dnssec04} ) ) {
        push @results, $class->dnssec04( $zone );
    }

    if ( should_run_test( q{dnssec05} ) ) {
        push @results, $class->dnssec05( $zone );
    }

    if ( should_run_test( q{dnssec06} ) ) {
        push @results, $class->dnssec06( $zone );
    }

    if ( should_run_test( q{dnssec08} ) ) {
        push @results, $class->dnssec08( $zone );
    }

    if ( should_run_test( q{dnssec09} ) ) {
        push @results, $class->dnssec09( $zone );
    }

    if ( should_run_test( q{dnssec10} ) ) {
        push @results, $class->dnssec10( $zone );
    }

    if ( should_run_test( q{dnssec13} ) ) {
        push @results, $class->dnssec13( $zone );
    }

    if ( should_run_test( q{dnssec14} ) ) {
        push @results, $class->dnssec14( $zone );
    }

    if ( should_run_test( q{dnssec15} ) ) {
        push @results, $class->dnssec15( $zone );
    }

    if ( should_run_test( q{dnssec16} ) ) {
        push @results, $class->dnssec16( $zone );
    }

    if ( should_run_test( q{dnssec17} ) ) {
        push @results, $class->dnssec17( $zone );
    }

    if ( should_run_test( q{dnssec18} ) ) {
        push @results, $class->dnssec18( $zone );
    }

    return @results;
} ## end sub all

=over

=item metadata()

    my $hash_ref = metadata();

Returns a reference to a hash, the keys of which are the names of all Test Cases in the module, and the corresponding values are references to
an array containing all the message tags that the Test Case can use in L<log entries|Zonemaster::Engine::Logger::Entry>.

=back

=cut

sub metadata {
    my ( $class ) = @_;

    return {
        dnssec01 => [
            qw(
              DS01_DS_ALGO_2_MISSING
              DS01_DS_ALGO_DEPRECATED
              DS01_DS_ALGO_NOT_DS
              DS01_DS_ALGO_OK
              DS01_DS_ALGO_PRIVATE
              DS01_DS_ALGO_RESERVED
              DS01_DS_ALGO_UNASSIGNED
              DS01_NO_RESPONSE
              DS01_PARENT_SERVER_NO_DS
              DS01_PARENT_ZONE_NO_DS
              DS01_ROOT_N_NO_UNDEL_DS
              DS01_UNDEL_N_NO_UNDEL_DS
              )
        ],
        dnssec02 => [
            qw(
              DS02_ALGO_NOT_SUPPORTED_BY_ZM
              DS02_DNSKEY_NOT_FOR_ZONE_SIGNING
              DS02_DNSKEY_NOT_SEP
              DS02_DNSKEY_NOT_SIGNED_BY_ANY_DS
              DS02_NO_DNSKEY_FOR_DS
              DS02_NO_MATCHING_DNSKEY_RRSIG
              DS02_NO_MATCH_DS_DNSKEY
              DS02_NO_VALID_DNSKEY_FOR_ANY_DS
              DS02_RRSIG_NOT_VALID_BY_DNSKEY
              )
        ],
        dnssec03 => [
            qw(
              DS03_ERR_MULT_NSEC3
              DS03_ILLEGAL_HASH_ALGO
              DS03_ILLEGAL_ITERATION_VALUE
              DS03_ILLEGAL_SALT_LENGTH
              DS03_INCONSISTENT_HASH_ALGO
              DS03_INCONSISTENT_ITERATION
              DS03_INCONSISTENT_NSEC3_FLAGS
              DS03_INCONSISTENT_SALT_LENGTH
              DS03_LEGAL_EMPTY_SALT
              DS03_LEGAL_HASH_ALGO
              DS03_LEGAL_ITERATION_VALUE
              DS03_NO_DNSSEC_SUPPORT
              DS03_NO_NSEC3
              DS03_NSEC3_OPT_OUT_DISABLED
              DS03_NSEC3_OPT_OUT_ENABLED_NON_TLD
              DS03_NSEC3_OPT_OUT_ENABLED_TLD
              DS03_SERVER_NO_DNSSEC_SUPPORT
              DS03_SERVER_NO_NSEC3
              DS03_UNASSIGNED_FLAG_USED
              )
        ],
        dnssec04 => [
            qw(
              RRSIG_EXPIRATION
              RRSIG_EXPIRED
              REMAINING_SHORT
              REMAINING_LONG
              DURATION_LONG
              DURATION_OK
              TEST_CASE_END
              TEST_CASE_START
              )
        ],
        dnssec05 => [
            qw(
              DS05_ALGO_DEPRECATED
              DS05_ALGO_NOT_RECOMMENDED
              DS05_ALGO_NOT_ZONE_SIGN
              DS05_ALGO_OK
              DS05_ALGO_PRIVATE
              DS05_ALGO_RESERVED
              DS05_ALGO_UNASSIGNED
              DS05_NO_RESPONSE
              DS05_SERVER_NO_DNSSEC
              DS05_ZONE_NO_DNSSEC
              )
        ],
        dnssec06 => [
            qw(
              EXTRA_PROCESSING_OK
              EXTRA_PROCESSING_BROKEN
              TEST_CASE_END
              TEST_CASE_START
              )
        ],
        dnssec07 => [
            qw(
              DS07_DS_FOR_SIGNED_ZONE
              DS07_DS_ON_PARENT_SERVER
              DS07_INCONSISTENT_DS
              DS07_INCONSISTENT_SIGNED
              DS07_NON_AUTH_RESPONSE_DNSKEY
              DS07_NOT_SIGNED
              DS07_NOT_SIGNED_ON_SERVER
              DS07_NO_DS_ON_PARENT_SERVER
              DS07_NO_DS_FOR_SIGNED_ZONE
              DS07_NO_RESPONSE_DNSKEY
              DS07_SIGNED
              DS07_SIGNED_ON_SERVER
              DS07_UNEXP_RCODE_RESP_DNSKEY
              )
        ],
        dnssec08 => [
            qw(
              DS08_ALGO_NOT_SUPPORTED_BY_ZM
              DS08_DNSKEY_RRSIG_EXPIRED
              DS08_DNSKEY_RRSIG_NOT_YET_VALID
              DS08_MISSING_RRSIG_IN_RESPONSE
              DS08_NO_MATCHING_DNSKEY
              DS08_RRSIG_NOT_VALID_BY_DNSKEY
              )
        ],
        dnssec09 => [
            qw(
              DS09_ALGO_NOT_SUPPORTED_BY_ZM
              DS09_MISSING_RRSIG_IN_RESPONSE
              DS09_NO_MATCHING_DNSKEY
              DS09_RRSIG_NOT_VALID_BY_DNSKEY
              DS09_SOA_RRSIG_EXPIRED
              DS09_SOA_RRSIG_NOT_YET_VALID
              )
        ],
        dnssec10 => [
            qw(
              DS10_ALGO_NOT_SUPPORTED_BY_ZM
              DS10_ERR_MULT_NSEC
              DS10_ERR_MULT_NSEC3
              DS10_EXPECTED_NSEC_NSEC3_MISSING
              DS10_HAS_NSEC
              DS10_HAS_NSEC3
              DS10_INCONSISTENT_NSEC
              DS10_INCONSISTENT_NSEC3
              DS10_INCONSISTENT_NSEC_NSEC3
              DS10_MIXED_NSEC_NSEC3
              DS10_NSEC3PARAM_GIVES_ERR_ANSWER
              DS10_NSEC3PARAM_MISMATCHES_APEX
              DS10_NSEC3PARAM_QUERY_RESPONSE_ERR
              DS10_NSEC3_ERR_TYPE_LIST
              DS10_NSEC3_MISMATCHES_APEX
              DS10_NSEC3_MISSING_SIGNATURE
              DS10_NSEC3_NODATA_MISSING_SOA
              DS10_NSEC3_NODATA_WRONG_SOA
              DS10_NSEC3_NO_VERIFIED_SIGNATURE
              DS10_NSEC3_RRSIG_EXPIRED
              DS10_NSEC3_RRSIG_NOT_YET_VALID
              DS10_NSEC3_RRSIG_NO_DNSKEY
              DS10_NSEC3_RRSIG_VERIFY_ERROR
              DS10_NSEC_ERR_TYPE_LIST
              DS10_NSEC_GIVES_ERR_ANSWER
              DS10_NSEC_MISMATCHES_APEX
              DS10_NSEC_MISSING_SIGNATURE
              DS10_NSEC_NODATA_MISSING_SOA
              DS10_NSEC_NODATA_WRONG_SOA
              DS10_NSEC_NO_VERIFIED_SIGNATURE
              DS10_NSEC_QUERY_RESPONSE_ERR
              DS10_NSEC_RRSIG_EXPIRED
              DS10_NSEC_RRSIG_NOT_YET_VALID
              DS10_NSEC_RRSIG_NO_DNSKEY
              DS10_NSEC_RRSIG_VERIFY_ERROR
              DS10_NONSTANDARD_NSEC_RESPONSE
              DS10_SERVER_NO_DNSSEC
              DS10_ZONE_NO_DNSSEC
              )
        ],
        dnssec11 => [
            qw(
              DS11_INCONSISTENT_DS
              DS11_INCONSISTENT_SIGNED_ZONE
              DS11_UNDETERMINED_DS
              DS11_UNDETERMINED_SIGNED_ZONE
              DS11_PARENT_WITHOUT_DS
              DS11_PARENT_WITH_DS
              DS11_NS_WITH_SIGNED_ZONE
              DS11_NS_WITH_UNSIGNED_ZONE
              DS11_DS_BUT_UNSIGNED_ZONE
              ),
        ],
        dnssec13 => [
            qw(
              DS13_ALGO_NOT_SIGNED_DNSKEY
              DS13_ALGO_NOT_SIGNED_NS
              DS13_ALGO_NOT_SIGNED_SOA
              ),
        ],
        dnssec14 => [
            qw(
              NO_RESPONSE
              NO_RESPONSE_DNSKEY
              DNSKEY_SMALLER_THAN_REC
              DNSKEY_TOO_SMALL_FOR_ALGO
              DNSKEY_TOO_LARGE_FOR_ALGO
              IPV4_DISABLED
              IPV6_DISABLED
              KEY_SIZE_OK
              TEST_CASE_END
              TEST_CASE_START
              ),
        ],
        dnssec15 => [
            qw(
              DS15_HAS_CDNSKEY_NO_CDS
              DS15_HAS_CDS_AND_CDNSKEY
              DS15_HAS_CDS_NO_CDNSKEY
              DS15_INCONSISTENT_CDNSKEY
              DS15_INCONSISTENT_CDS
              DS15_MISMATCH_CDS_CDNSKEY
              DS15_NO_CDS_CDNSKEY
              ),
        ],
        dnssec16 => [
            qw(
              DS16_CDS_INVALID_RRSIG
              DS16_CDS_MATCHES_NON_SEP_DNSKEY
              DS16_CDS_MATCHES_NON_ZONE_DNSKEY
              DS16_CDS_MATCHES_NO_DNSKEY
              DS16_CDS_NOT_SIGNED_BY_CDS
              DS16_CDS_SIGNED_BY_UNKNOWN_DNSKEY
              DS16_CDS_UNSIGNED
              DS16_CDS_WITHOUT_DNSKEY
              DS16_DELETE_CDS
              DS16_DNSKEY_NOT_SIGNED_BY_CDS
              DS16_MIXED_DELETE_CDS
              ),
        ],
        dnssec17 => [
            qw(
              DS17_CDNSKEY_INVALID_RRSIG
              DS17_CDNSKEY_IS_NON_SEP
              DS17_CDNSKEY_IS_NON_ZONE
              DS17_CDNSKEY_MATCHES_NO_DNSKEY
              DS17_CDNSKEY_NOT_SIGNED_BY_CDNSKEY
              DS17_CDNSKEY_SIGNED_BY_UNKNOWN_DNSKEY
              DS17_CDNSKEY_UNSIGNED
              DS17_CDNSKEY_WITHOUT_DNSKEY
              DS17_DELETE_CDNSKEY
              DS17_DNSKEY_NOT_SIGNED_BY_CDNSKEY
              DS17_MIXED_DELETE_CDNSKEY
              ),
        ],
        dnssec18 => [
            qw(
              DS18_NO_MATCH_CDS_RRSIG_DS
              DS18_NO_MATCH_CDNSKEY_RRSIG_DS
              ),
        ],
    };
} ## end sub metadata

Readonly my %TAG_DESCRIPTIONS => (
    DNSSEC01 => sub {
        __x    # DNSSEC:DNSSEC01
          "Legal values for the DS hash digest algorithm";
    },
    DNSSEC02 => sub {
        __x    # DNSSEC:DNSSEC02
          "DS must match a valid DNSKEY in the child zone";
    },
    DNSSEC03 => sub {
        __x    # DNSSEC:DNSSEC03
          "Verify NSEC3 parameters";
    },
    DNSSEC04 => sub {
        __x    # DNSSEC:DNSSEC04
          "Check for too short or too long RRSIG lifetimes";
    },
    DNSSEC05 => sub {
        __x    # DNSSEC:DNSSEC05
          "Check for invalid DNSKEY algorithms";
    },
    DNSSEC06 => sub {
        __x    # DNSSEC:DNSSEC06
          "Verify DNSSEC additional processing";
    },
    DNSSEC07 => sub {
        __x    # DNSSEC:DNSSEC07
          "DNSSEC signed zone and DS in parent for signed zone";
    },
    DNSSEC08 => sub {
        __x    # DNSSEC:DNSSEC08
          "Valid RRSIG for DNSKEY";
    },
    DNSSEC09 => sub {
        __x    # DNSSEC:DNSSEC09
          "RRSIG(SOA) must be valid and created by a valid DNSKEY";
    },
    DNSSEC10 => sub {
        __x    # DNSSEC:DNSSEC10
          "Zone contains NSEC or NSEC3 records";
    },
    DNSSEC11 => sub {
        __x    # DNSSEC:DNSSEC11
          "DS in delegation requires signed zone";
    },
    DNSSEC12 => sub {
        __x    # DNSSEC:DNSSEC12
          "Test for DNSSEC Algorithm Completeness";
    },
    DNSSEC13 => sub {
        __x    # DNSSEC:DNSSEC13
          "All DNSKEY algorithms used to sign the zone";
    },
    DNSSEC14 => sub {
        __x    # DNSSEC:DNSSEC14
          "Check for valid RSA DNSKEY key size";
    },
    DNSSEC15 => sub {
        __x    # DNSSEC:DNSSEC15
          "Existence of CDS and CDNSKEY";
    },
    DNSSEC16 => sub {
        __x    # DNSSEC:DNSSEC16
          "Validate CDS";
    },
    DNSSEC17 => sub {
        __x    # DNSSEC:DNSSEC17
          "Validate CDNSKEY";
    },
    DNSSEC18 => sub {
        __x    # DNSSEC:DNSSEC18
          "Validate trust from DS to CDS and CDNSKEY";
    },
    DNSKEY_SMALLER_THAN_REC => sub {
        __x    # DNSSEC:DNSKEY_SMALLER_THAN_REC
          'DNSKEY with tag {keytag} and using algorithm {algo_num} '
          . '({algo_descr}) has a size ({keysize}) smaller than the '
          . 'recommended one ({keysizerec}).',
          @_;
    },
    DNSKEY_TOO_SMALL_FOR_ALGO => sub {
        __x    # DNSSEC:DNSKEY_TOO_SMALL_FOR_ALGO
          'DNSKEY with tag {keytag} and using algorithm {algo_num} '
          . '({algo_descr}) has a size ({keysize}) smaller than the minimum '
          . 'one ({keysizemin}).',
          @_;
    },
    DNSKEY_TOO_LARGE_FOR_ALGO => sub {
        __x    # DNSSEC:DNSKEY_TOO_LARGE_FOR_ALGO
          'DNSKEY with tag {keytag} and using algorithm {algo_num} '
          . '({algo_descr}) has a size ({keysize}) larger than the maximum one '
          . '({keysizemax}).',
          @_;
    },
    DS01_DS_ALGO_2_MISSING => sub {
        __x    # DNSSEC:DS01_DS_ALGO_2_MISSING
           'There is a DS record with keytag {keytag}. A DS record using digest algorithm 2 (SHA-256) '
           . 'is missing. Fetched from parent name servers "{ns_list}".',
        @_;
    },
    DS01_DS_ALGO_DEPRECATED => sub {
        __x    # DNSSEC:DS01_DS_ALGO_DEPRECATED
            'The DS record with keytag {keytag} uses a deprecated digest algorithm {ds_algo_num} '
            . '({ds_algo_descr}). Fetched from parent name servers "{ns_list}".',
          @_;
    },
    DS01_DS_ALGO_NOT_DS => sub {
        __x    # DNSSEC:DS01_DS_ALGO_NOT_DS
            'The DS record with keytag {keytag} uses a digest algorithm {ds_algo_num} ({ds_algo_descr}) '
            . 'not meant for DS records. Fetched from parent name servers "{ns_list}".',
          @_;
    },
    DS01_DS_ALGO_PRIVATE => sub {
        __x    # DNSSEC:DS01_DS_ALGO_PRIVATE
            'The DS record with keytag {keytag} uses a digest algorithm {ds_algo_num} for private use. '
            . 'Fetched from parent name servers "{ns_list}".',
          @_;
    },
    DS01_DS_ALGO_RESERVED => sub {
        __x    # DNSSEC:DS01_DS_ALGO_RESERVED
            'The DS record with keytag {keytag} uses a reserved digest algorithm {ds_algo_num} on name '
            . 'servers "{ns_list}".',
          @_;
    },
    DS01_DS_ALGO_UNASSIGNED => sub {
        __x    # DNSSEC:DS01_DS_ALGO_UNASSIGNED
            'The DS record with keytag {keytag} uses an unassigned digest algorithm {ds_algo_num} on parent '
            . 'name servers "{ns_list}".',
          @_;
    },
    DS01_NO_RESPONSE => sub {
        __x    # DNSSEC:DS01_NO_RESPONSE
            'No response or error in response from all parent name servers on the DS query. Name servers '
            . 'are "{ns_list}".',
          @_;
    },
    DS01_PARENT_SERVER_NO_DS => sub {
        __x    # DNSSEC:DS01_PARENT_SERVER_NO_DS
            'The following name servers do not provide DS record or have not been properly configured. '
            . 'Fetched from parent name servers "{ns_list}".',
          @_;
    },
    DS01_PARENT_ZONE_NO_DS => sub {
        __x    # DNSSEC:DS01_PARENT_ZONE_NO_DS
            'The parent zone provides no DS records for the child zone. Fetched from parent name '
            . 'servers "{ns_list}".',
          @_;
    },
    DS01_ROOT_N_NO_UNDEL_DS => sub {
        __x    # DNSSEC:DS01_ROOT_N_NO_UNDEL_DS
            'Tested zone is the root zone, but no undelegated DS has been provided. DS is not tested.',
          @_;
    },
    DS01_UNDEL_N_NO_UNDEL_DS => sub {
        __x    # DNSSEC:DS01_UNDEL_N_NO_UNDEL_DS
            'Tested zone is undelegated, but no undelegated DS has been provided. DS is not tested.',
          @_;
    },
    DS02_ALGO_NOT_SUPPORTED_BY_ZM => sub {
        __x    # DNSSEC:DS02_ALGO_NOT_SUPPORTED_BY_ZM
          'DNSKEY with tag {keytag} uses unsupported algorithm {algo_num} '
          . '({algo_mnemo}) by this installation of Zonemaster. Fetched from '
          . 'the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS02_DNSKEY_NOT_FOR_ZONE_SIGNING => sub {
        __x    # DNSSEC:DS02_DNSKEY_NOT_FOR_ZONE_SIGNING
          'Flags field of DNSKEY record with tag {keytag} has not ZONE bit set '
          . 'although DS with same tag is present in parent. Fetched from '
          . 'the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS02_DNSKEY_NOT_SEP => sub {
        __x    # DNSSEC:DS02_DNSKEY_NOT_SEP
          'Flags field of DNSKEY record with tag {keytag} has not SEP bit set '
          . 'although DS with same tag is present in parent. Fetched from '
          . 'the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS02_DNSKEY_NOT_SIGNED_BY_ANY_DS => sub {
        __x    # DNSSEC:DS02_DNSKEY_NOT_SIGNED_BY_ANY_DS
          'The DNSKEY RRset has not been signed by any DNSKEY matched by a DS record. '
          . 'Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS02_NO_DNSKEY_FOR_DS => sub {
        __x    # DNSSEC:DS02_NO_DNSKEY_FOR_DS
          'The DNSKEY record with tag {keytag} that the DS refers to does not '
          . 'exist in the DNSKEY RRset. Fetched from the nameservers with IP '
          . '"{ns_ip_list}".',
          @_;
    },
    DS02_NO_MATCHING_DNSKEY_RRSIG => sub {
        __x    # DNSSEC:DS02_NO_MATCHING_DNSKEY_RRSIG
          'The DNSKEY RRset is not signed by the DNSKEY with tag {keytag} that '
          . 'the DS record refers to. Fetched from the nameservers with IP '
          . '"{ns_ip_list}".',
          @_;
    },
    DS02_NO_MATCH_DS_DNSKEY => sub {
        __x    # DNSSEC:DS02_NO_MATCH_DS_DNSKEY
          'The DS record does not match the DNSKEY with tag {keytag} by algorithm '
          . 'or digest. Fetched from the nameservers with IP "{ns_ip_list}".',
          @_;
    },
    DS02_NO_VALID_DNSKEY_FOR_ANY_DS => sub {
        __x    # DNSSEC:DS02_NO_VALID_DNSKEY_FOR_ANY_DS
          'There is no valid DNSKEY matched by any of the DS records. '
          . 'Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS02_RRSIG_NOT_VALID_BY_DNSKEY => sub {
        __x    # DNSSEC:DS02_RRSIG_NOT_VALID_BY_DNSKEY
          'The DNSKEY RRset is signed with an RRSIG with tag {keytag} which cannot '
          . 'be validated by the matching DNSKEY. Fetched from the nameservers with IP '
          . 'addresses "{ns_ip_list}".',
          @_;
    },
    DS03_ERROR_RESPONSE_NSEC_QUERY => sub {
        __x    # DNSSEC:DS03_ERROR_RESPONSE_NSEC_QUERY
          'The following servers give erroneous response to NSEC query. Fetched from name servers "{ns_list}".', @_;
    },
    DS03_ERR_MULT_NSEC3 => sub {
        __x    # DNSSEC:DS03_ERR_MULT_NSEC3
          'Multiple NSEC3 records when one is expected. Fetched from name servers "{ns_list}".', @_;
    },
    DS03_ILLEGAL_HASH_ALGO => sub {
        __x    # DNSSEC:DS03_ILLEGAL_HASH_ALGO
          'The following servers respond with an illegal hash algorithm for NSEC3 ({algo_num}). '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_ILLEGAL_ITERATION_VALUE => sub {
        __x    # DNSSEC:DS03_ILLEGAL_ITERATION_VALUE
          'The following servers respond with the NSEC3 iteration value {int}. '
          . 'The recommended practice is to set this value to 0. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_ILLEGAL_SALT_LENGTH => sub {
        __x    # DNSSEC:DS03_ILLEGAL_SALT_LENGTH
          'The following servers respond with a non-empty salt in NSEC3 ({int} octets). '
          . 'The recommended practice is to use an empty salt. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_INCONSISTENT_HASH_ALGO => sub {
        __x    # DNSSEC:DS03_INCONSISTENT_HASH_ALGO
          'Inconsistent hash algorithm in NSEC3 in responses for the child zone from different name servers.', @_;
    },
    DS03_INCONSISTENT_ITERATION => sub {
        __x    # DNSSEC:DS03_INCONSISTENT_ITERATION
          'Inconsistent NSEC3 iteration value in responses for the child zone from different name servers.', @_;
    },
    DS03_INCONSISTENT_NSEC3_FLAGS => sub {
        __x    # DNSSEC:DS03_INCONSISTENT_NSEC3_FLAGS
          'Inconsistent NSEC3 flag list in responses for the child zone from different name servers.', @_;
    },
    DS03_INCONSISTENT_SALT_LENGTH => sub {
        __x    # DNSSEC:DS03_INCONSISTENT_SALT_LENGTH
          'Inconsistent salt length in NSEC3 in responses for the child zone from different name servers.', @_;
    },
    DS03_LEGAL_EMPTY_SALT => sub {
        __x    # DNSSEC:DS03_LEGAL_EMPTY_SALT
          'The following servers respond with a legal empty salt in NSEC3. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_LEGAL_HASH_ALGO => sub {
        __x    # DNSSEC:DS03_LEGAL_HASH_ALGO
          'The following servers respond with a legal hash algorithm in NSEC3. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_LEGAL_ITERATION_VALUE => sub {
        __x    # DNSSEC:DS03_LEGAL_ITERATION_VALUE
          'The following servers respond with NSEC3 iteration value set to zero (as recommended). '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_NO_DNSSEC_SUPPORT => sub {
        __x    # DNSSEC:DS03_NO_DNSSEC_SUPPORT
          'The zone is not DNSSEC signed or not properly DNSSEC signed. Testing for NSEC3 has been skipped. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_NO_NSEC3 => sub {
        __x    # DNSSEC:DS03_NO_NSEC3
          'The zone does not use NSEC3. Testing for NSEC3 has been skipped. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_NO_RESPONSE_NSEC_QUERY => sub {
        __x    # DNSSEC:DS03_NO_RESPONSE_NSEC_QUERY
        'The following servers do not respond to NSEC query. Fetched from name servers "{ns_list}".', @_;
    },
    DS03_NSEC3_OPT_OUT_DISABLED => sub {
        __x    # DNSSEC:DS03_NSEC3_OPT_OUT_DISABLED
          'The following servers respond with NSEC3 opt-out disabled (as recommended). '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_NSEC3_OPT_OUT_ENABLED_NON_TLD => sub {
        __x    # DNSSEC:DS03_NSEC3_OPT_OUT_ENABLED_NON_TLD
          'The following servers respond with NSEC3 opt-out enabled. '
          . 'The recommended practice is to disable opt-out. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_NSEC3_OPT_OUT_ENABLED_TLD => sub {
        __x    # DNSSEC:DS03_NSEC3_OPT_OUT_ENABLED_TLD
          'The following servers respond with NSEC3 opt-out enabled. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_SERVER_NO_DNSSEC_SUPPORT => sub {
        __x    # DNSSEC:DS03_SERVER_NO_DNSSEC_SUPPORT
          'The following name servers do not support DNSSEC or have not been properly configured. '
          . 'Testing for NSEC3 has been skipped on those servers. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_SERVER_NO_NSEC3 => sub {
        __x    # DNSSEC:DS03_SERVER_NO_NSEC3
          'The following name servers do not use NSEC3, but others do. '
          . 'Testing for NSEC3 has been skipped on the following servers. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS03_UNASSIGNED_FLAG_USED => sub {
        __x    # DNSSEC:DS03_UNASSIGNED_FLAG_USED
          'The following servers respond with an NSEC3 record where an unassigned flag is used (bit {int}). '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS05_ALGO_DEPRECATED => sub {
        __x    # DNSSEC:DS05_ALGO_DEPRECATED
          'The DNSKEY with tag {keytag} uses deprecated algorithm number {algo_num} ("{algo_descr}", {algo_mnemo}). '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS05_ALGO_NOT_RECOMMENDED => sub {
        __x    # DNSSEC:DS05_ALGO_NOT_RECOMMENDED
          'The DNSKEY with tag {keytag} uses unrecommended algorithm number {algo_num} ("{algo_descr}", {algo_mnemo}). '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS05_ALGO_NOT_ZONE_SIGN => sub {
        __x    # DNSSEC:DS05_ALGO_NOT_ZONE_SIGN
          'The DNSKEY with tag {keytag} uses algorithm number {algo_num} ("{algo_descr}", {algo_mnemo}) which is '
          . 'not meant for zone signing. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS05_ALGO_OK => sub {
        __x    # DNSSEC:DS05_ALGO_OK
          'The DNSKEY with tag {keytag} uses algorithm number {algo_num} ("{algo_descr}", {algo_mnemo}). '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS05_ALGO_PRIVATE => sub {
        __x    # DNSSEC:DS05_ALGO_PRIVATE
          'The DNSKEY with tag {keytag} uses algorithm number {algo_num} which is '
          . 'reserved for private use. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS05_ALGO_RESERVED => sub {
        __x    # DNSSEC:DS05_ALGO_RESERVED
          'The DNSKEY with tag {keytag} uses reserved algorithm number {algo_num}. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS05_ALGO_UNASSIGNED => sub {
        __x    # DNSSEC:DS05_ALGO_UNASSIGNED
          'The DNSKEY with tag {keytag} uses unassigned algorithm number {algo_num}. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS05_NO_RESPONSE => sub {
        __x    # DNSSEC:DS05_NO_RESPONSE
          'No response or error in response from all name servers on the DNSKEY query. Failing name servers: "{ns_list}".',
          @_;
    },
    DS05_SERVER_NO_DNSSEC => sub {
        __x    # DNSSEC:DS05_SERVER_NO_DNSSEC
          'Some name servers do not support DNSSEC or have not been properly configured. DNSKEY cannot be tested on '
          . 'those servers. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS05_ZONE_NO_DNSSEC => sub {
        __x    # DNSSEC:DS05_ZONE_NO_DNSSEC
          'The zone is not DNSSEC signed or not properly DNSSEC signed. DNSKEY cannot be tested. Fetched from name '
          . 'servers "{ns_list}".',
          @_;
    },
    DS07_DS_FOR_SIGNED_ZONE => sub {
        __x    # DNSSEC:DS07_DS_FOR_SIGNED_ZONE
          'The parent zone has DS record or records for the signed child zone.';
    },
    DS07_DS_ON_PARENT_SERVER => sub {
        __x    # DNSSEC:DS07_DS_ON_PARENT_SERVER
          'The following parent name servers respond with DS record or records for the child '
          . 'zone. Name servers: "{ns_list}".',
          @_;
    },
    DS07_INCONSISTENT_DS => sub {
        __x    # DNSSEC:DS07_INCONSISTENT_DS
          'Inconsistent responses from parent name servers. Some include DS, others do not.';
    },
    DS07_INCONSISTENT_SIGNED => sub {
        __x    # DNSSEC:DS07_INCONSISTENT_SIGNED
          'Inconsistent responses from name servers. Some include signed responses, others do not.';
    },
    DS07_NON_AUTH_RESPONSE_DNSKEY => sub {
        __x    # DNSSEC:DS07_NON_AUTH_RESPONSE_DNSKEY
          'The following name servers give a non authoritative response on DNSKEY query with DO bit set. '
          . 'Name servers: "{ns_list}".',
          @_;
    },
    DS07_NOT_SIGNED => sub {
        __x    # DNSSEC:DS07_NOT_SIGNED
          'The zone is not signed.';
    },
    DS07_NOT_SIGNED_ON_SERVER => sub {
        __x    # DNSSEC:DS07_NOT_SIGNED_ON_SERVER
          'The following name servers respond with no DNSKEY (unsigned child zone). '
          . 'Name servers: "{ns_list}".',
          @_;
    },
    DS07_NO_DS_ON_PARENT_SERVER => sub {
        __x    # DNSSEC:DS07_NO_DS_ON_PARENT_SERVER
          'The following parent name servers respond without DS record for the child zone. '
          . 'Name servers: "{ns_list}".',
          @_;
    },
    DS07_NO_DS_FOR_SIGNED_ZONE => sub {
        __x    # DNSSEC:DS07_NO_DS_FOR_SIGNED_ZONE
          'The parent zone has no DS record for the signed child zone.';
    },
    DS07_NO_RESPONSE_DNSKEY => sub {
        __x    # DNSSEC:DS07_NO_RESPONSE_DNSKEY
          'The following name servers do not respond on DNSKEY query with DO bit set. '
          . 'Name servers: "{ns_list}".',
          @_;
    },
    DS07_SIGNED => sub {
        __x    # DNSSEC:DS07_SIGNED
          'The zone is signed.';
    },
    DS07_SIGNED_ON_SERVER => sub {
        __x    # DNSSEC:DS07_SIGNED_ON_SERVER
          'The following name servers respond with DNSKEY (signed child zone). '
          . 'Name servers: "{ns_list}".',
          @_;
    },
    DS07_UNEXP_RCODE_RESP_DNSKEY => sub {
        __x    # DNSSEC:DS07_UNEXP_RCODE_RESP_DNSKEY
          'The following name servers respond with RCODE "{rcode}" instead of expected "NOERROR" '
          . 'on DNSKEY query with DO bit set. Name servers: "{ns_list}".',
          @_;
    },
    DS08_ALGO_NOT_SUPPORTED_BY_ZM => sub {
        __x    # DNSSEC:DS08_ALGO_NOT_SUPPORTED_BY_ZM
          'DNSKEY with tag {keytag} uses unsupported algorithm {algo_num} '
          . '({algo_mnemo}) by this installation of Zonemaster. Fetched from '
          . 'the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS08_DNSKEY_RRSIG_EXPIRED => sub {
        __x    # DNSSEC:DS08_DNSKEY_RRSIG_EXPIRED
          'RRSIG with keytag {keytag} and covering type DNSKEY has already expired. '
          . 'Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS08_DNSKEY_RRSIG_NOT_YET_VALID => sub {
        __x    # DNSSEC:DS08_DNSKEY_RRSIG_NOT_YET_VALID
          'RRSIG with keytag {keytag} and covering type DNSKEY has inception date in '
          . 'the future. Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS08_MISSING_RRSIG_IN_RESPONSE => sub {
        __x    # DNSSEC:DS08_MISSING_RRSIG_IN_RESPONSE
          'The DNSKEY RRset is not signed, which is against expectation. Fetched '
          . 'from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS08_NO_MATCHING_DNSKEY => sub {
        __x    # DNSSEC:DS08_NO_MATCHING_DNSKEY
          'The DNSKEY RRset is signed with an RRSIG with tag {keytag} which does '
          . 'not match any DNSKEY record. Fetched from the nameservers with IP '
          . 'addresses "{ns_ip_list}".',
          @_;
    },
    DS08_RRSIG_NOT_VALID_BY_DNSKEY => sub {
        __x    # DNSSEC:DS08_RRSIG_NOT_VALID_BY_DNSKEY
          'The DNSKEY RRset is signed with an RRSIG with tag {keytag} which cannot '
          . 'be validated by the matching DNSKEY. Fetched from the nameservers with IP '
          . 'addresses "{ns_ip_list}".',
          @_;
    },
    DS09_ALGO_NOT_SUPPORTED_BY_ZM => sub {
        __x    # DNSSEC:DS09_ALGO_NOT_SUPPORTED_BY_ZM
          'DNSKEY with tag {keytag} uses unsupported algorithm {algo_num} '
          . '({algo_mnemo}) by this installation of Zonemaster. Fetched from '
          . 'the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS09_MISSING_RRSIG_IN_RESPONSE => sub {
        __x    # DNSSEC:DS09_MISSING_RRSIG_IN_RESPONSE
          'The SOA RRset is not signed, which is against expectation. Fetched '
          . 'from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS09_NO_MATCHING_DNSKEY => sub {
        __x    # DNSSEC:DS09_NO_MATCHING_DNSKEY
          'The SOA RRset is signed with an RRSIG with tag {keytag} which does '
          . 'not match any DNSKEY record. Fetched from the nameservers with IP '
          . 'addresses "{ns_ip_list}".',
          @_;
    },
    DS09_RRSIG_NOT_VALID_BY_DNSKEY => sub {
        __x    # DNSSEC:DS09_RRSIG_NOT_VALID_BY_DNSKEY
          'The SOA RRset is signed with an RRSIG with tag {keytag} which cannot '
          . 'be validated by the matching DNSKEY. Fetched from the nameservers with IP '
          . 'addresses "{ns_ip_list}".',
          @_;
    },
    DS09_SOA_RRSIG_EXPIRED => sub {
        __x    # DNSSEC:DS09_SOA_RRSIG_EXPIRED
          'RRSIG with keytag {keytag} and covering type SOA has already expired. '
          . 'Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS09_SOA_RRSIG_NOT_YET_VALID => sub {
        __x    # DNSSEC:DS09_SOA_RRSIG_NOT_YET_VALID
          'RRSIG with keytag {keytag} and covering type SOA has inception date in '
          . 'the future. Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS10_ALGO_NOT_SUPPORTED_BY_ZM => sub {
        __x    # DNSSEC:DS10_ALGO_NOT_SUPPORTED_BY_ZM
          'DNSKEY with tag {keytag} uses unsupported algorithm {algo_num} '
          . '({algo_mnemo}) by this installation of Zonemaster. Fetched from '
          . 'name servers "{ns_ip_list}".',
          @_;
    },
    DS10_ERR_MULT_NSEC => sub {
        __x    # DNSSEC:DS10_ERR_MULT_NSEC
          'Multiple NSEC records when one is expected. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_ERR_MULT_NSEC3 => sub {
        __x    # DNSSEC:DS10_ERR_MULT_NSEC3
          'Multiple NSEC3 records when one is expected. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_EXPECTED_NSEC_NSEC3_MISSING => sub {
        __x    # DNSSEC:DS10_EXPECTED_NSEC_NSEC3_MISSING
          'The server responded with DNSKEY but not with expected NSEC or NSEC3. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_HAS_NSEC => sub {
        __x    # DNSSEC:DS10_HAS_NSEC
          'The zone has NSEC records. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_HAS_NSEC3 => sub {
        __x    # DNSSEC:DS10_HAS_NSEC3
          'The zone has NSEC3 records. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_INCONSISTENT_NSEC => sub {
        __x    # DNSSEC:DS10_INCONSISTENT_NSEC
          'Inconsistent responses from zone with NSEC. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_INCONSISTENT_NSEC3 => sub {
        __x    # DNSSEC:DS10_INCONSISTENT_NSEC3
          'Inconsistent responses from zone with NSEC3. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_INCONSISTENT_NSEC_NSEC3 => sub {
        __x    # DNSSEC:DS10_INCONSISTENT_NSEC_NSEC3
          'The zone is inconsistent on NSEC and NSEC3. NSEC is fetched from name servers '
          . '"{ns_list_nsec}". NSEC3 is fetched from name servers "{ns_list_nsec3}".',
          @_;
    },
    DS10_MIXED_NSEC_NSEC3 => sub {
        __x    # DNSSEC:DS10_MIXED_NSEC_NSEC3
          'The zone responds with both NSEC and NSEC3, where only one of them is expected. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3PARAM_GIVES_ERR_ANSWER => sub {
        __x    # DNSSEC:DS10_NSEC3PARAM_GIVES_ERR_ANSWER
          'Unexpected DNS record in the answer section on an NSEC3PARAM query. Fetched '
          . 'from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3PARAM_MISMATCHES_APEX => sub {
        __x    # DNSSEC:DS10_NSEC3PARAM_MISMATCHES_APEX
          'The returned NSEC3PARAM record has an unexpected non-apex owner name. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3PARAM_QUERY_RESPONSE_ERR => sub {
        __x    # DNSSEC:DS10_NSEC3PARAM_QUERY_RESPONSE_ERR
          'No response or error in response on query for NSEC3PARAM. Fetched from '
          . 'name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_ERR_TYPE_LIST => sub {
        __x    # DNSSEC:DS10_NSEC3_ERR_TYPE_LIST
          'NSEC3 record for the zone apex with incorrect type list. Fetched from '
          . 'name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_MISMATCHES_APEX => sub {
        __x    # DNSSEC:DS10_NSEC3_MISMATCHES_APEX
          'The returned NSEC3 record unexpectedly does not match the zone name. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_MISSING_SIGNATURE => sub {
        __x    # DNSSEC:DS10_NSEC3_MISSING_SIGNATURE
          'Missing RRSIG (signature) for the NSEC3 record or records. Fetched '
          . 'from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_NODATA_MISSING_SOA => sub {
        __x    # DNSSEC:DS10_NSEC3_NODATA_MISSING_SOA
          'Missing SOA record in NODATA response with NSEC3. Fetched from '
          . 'name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_NODATA_WRONG_SOA => sub {
        __x    # DNSSEC:DS10_NSEC3_NODATA_WRONG_SOA
          'Wrong owner name ("{domain}") on SOA record in NODATA response with NSEC3. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_NO_VERIFIED_SIGNATURE => sub {
        __x    # DNSSEC:DS10_NSEC3_NO_VERIFIED_SIGNATURE
          'The RRSIG (signature) for the NSEC3 record cannot be verified. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_RRSIG_EXPIRED => sub {
        __x    # DNSSEC:DS10_NSEC3_RRSIG_EXPIRED
          'The RRSIG (signature) with tag {keytag} for the NSEC3 record has expired. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_RRSIG_NOT_YET_VALID => sub {
        __x    # DNSSEC:DS10_NSEC3_RRSIG_NOT_YET_VALID
          'The RRSIG (signature) with tag {keytag} for the NSEC3 record it not yet valid. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_RRSIG_NO_DNSKEY => sub {
        __x    # DNSSEC:DS10_NSEC3_RRSIG_NO_DNSKEY
          'There is no DNSKEY record matching the RRSIG (signature) with tag {keytag} for '
          . 'the NSEC3 record. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC3_RRSIG_VERIFY_ERROR => sub {
        __x    # DNSSEC:DS10_NSEC3_RRSIG_VERIFY_ERROR
          'The RRSIG (signature) with tag {keytag} for the NSEC3 record cannot be verified. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_ERR_TYPE_LIST => sub {
        __x    # DNSSEC:DS10_NSEC_ERR_TYPE_LIST
          'NSEC record for the zone apex with incorrect type list. Fetched from name '
          . 'servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_GIVES_ERR_ANSWER => sub {
        __x    # DNSSEC:DS10_NSEC_GIVES_ERR_ANSWER
          'Unexpected DNS record in the answer section on an NSEC query. Fetched from '
          . 'name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_MISMATCHES_APEX => sub {
        __x    # DNSSEC:DS10_NSEC_MISMATCHES_APEX
          'The returned NSEC record has an unexpected non-apex owner name. Fetched from '
          . 'name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_MISSING_SIGNATURE => sub {
        __x    # DNSSEC:DS10_NSEC_MISSING_SIGNATURE
          'Missing RRSIG (signature) for the NSEC record or records. Fetched from '
          . 'name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_NODATA_MISSING_SOA => sub {
        __x    # DNSSEC:DS10_NSEC_NODATA_MISSING_SOA
          'Missing SOA record in NODATA response with NSEC. Fetched from name '
          . 'servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_NODATA_WRONG_SOA => sub {
        __x    # DNSSEC:DS10_NSEC_NODATA_WRONG_SOA
          'Wrong owner name ("{domain}") on SOA record in NODATA response with NSEC. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_NO_VERIFIED_SIGNATURE => sub {
        __x    # DNSSEC:DS10_NSEC_NO_VERIFIED_SIGNATURE
          'There is no RRSIG (signature) for the NSEC record that can be verified. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_QUERY_RESPONSE_ERR => sub {
        __x    # DNSSEC:DS10_NSEC_QUERY_RESPONSE_ERR
          'No response or error in response on query for NSEC. Fetched from name '
          . 'servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_RRSIG_EXPIRED => sub {
        __x    # DNSSEC:DS10_NSEC_RRSIG_EXPIRED
          'The RRSIG (signature) with tag {keytag} for the NSEC record has expired. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_RRSIG_NOT_YET_VALID => sub {
        __x    # DNSSEC:DS10_NSEC_RRSIG_NOT_YET_VALID
          'The RRSIG (signature) with tag {keytag} for the NSEC record it not yet valid. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_RRSIG_NO_DNSKEY => sub {
        __x    # DNSSEC:DS10_NSEC_RRSIG_NO_DNSKEY
          'There is no DNSKEY record matching the RRSIG (signature) with tag {keytag} for '
          . 'the NSEC record. Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NSEC_RRSIG_VERIFY_ERROR => sub {
        __x    # DNSSEC:DS10_NSEC_RRSIG_VERIFY_ERROR
          'The RRSIG (signature) with tag {keytag} for the NSEC record cannot be verified. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_NONSTANDARD_NSEC_RESPONSE => sub {
        __x    # DNSSEC:DS10_NONSTANDARD_NSEC_RESPONSE
          'The following name servers give a non-standard response to the NSEC query '
          . '(NSEC RR in authority section instead of answer section). '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_SERVER_NO_DNSSEC => sub {
        __x    # DNSSEC:DS10_SERVER_NO_DNSSEC
          'The following name servers do not support DNSSEC or have not been properly '
          . 'configured. Testing for NSEC and NSEC3 has been skipped on these servers. '
          . 'Fetched from name servers "{ns_list}".',
          @_;
    },
    DS10_ZONE_NO_DNSSEC => sub {
        __x    # DNSSEC:DS10_ZONE_NO_DNSSEC
          'The zone is not DNSSEC signed or not properly DNSSEC signed. '
          . 'Testing for NSEC and NSEC3 has been skipped. Fetched from '
          . 'name servers "{ns_list}".',
          @_;
    },
    DS11_INCONSISTENT_DS => sub {
        __x    # DNSSEC:DS11_INCONSISTENT_DS
          'Parent name servers are inconsistent on the existence of DS.',
          @_;
    },
    DS11_INCONSISTENT_SIGNED_ZONE => sub {
        __x    # DNSSEC:DS11_INCONSISTENT_SIGNED_ZONE
          'Name servers for the child zone are inconsistent on whether the '
          . 'zone is signed or not.',
          @_;
    },
    DS11_UNDETERMINED_DS => sub {
        __x    # DNSSEC:DS11_UNDETERMINED_DS
          'It cannot be determined if the parent zone has DS for the child '
          . 'zone or not.',
          @_;
    },
    DS11_UNDETERMINED_SIGNED_ZONE => sub {
        __x    # DNSSEC:DS11_UNDETERMINED_SIGNED_ZONE
          'It cannot be determined if the child zone is signed or not.',
          @_;
    },
    DS11_PARENT_WITHOUT_DS => sub {
        __x    # DNSSEC:DS11_PARENT_WITHOUT_DS
          'No DS record for the child zone found on parent nameservers with IP '
          . 'addresses "{ns_ip_list}".',
          @_;
    },
    DS11_PARENT_WITH_DS => sub {
        __x    # DNSSEC:DS11_PARENT_WITH_DS
          'DS record for the child zone found on parent nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS11_NS_WITH_SIGNED_ZONE => sub {
        __x    # DNSSEC:DS11_NS_WITH_SIGNED_ZONE
          'Signed child zone found on nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS11_NS_WITH_UNSIGNED_ZONE => sub {
        __x    # DNSSEC:DS11_NS_WITH_UNSIGNED_ZONE
          'Unsigned child zone found on nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS11_DS_BUT_UNSIGNED_ZONE => sub {
        __x    # DNSSEC:DS11_DS_BUT_UNSIGNED_ZONE
          'The child zone is unsigned, but the parent zone has DS record.',
          @_;
    },
    DS13_ALGO_NOT_SIGNED_DNSKEY => sub {
        __x    # DNSSEC:DS13_ALGO_NOT_SIGNED_DNSKEY
          'The DNSKEY RRset is not signed by algorithm {algo_num} ({algo_mnemo}) '
          . 'present in the DNSKEY RRset. Fetched from the nameservers with IP '
          . 'addresses "{ns_ip_list}".',
          @_;
    },
    DS13_ALGO_NOT_SIGNED_NS => sub {
        __x    # DNSSEC:DS13_ALGO_NOT_SIGNED_NS
          'The NS RRset is not signed by algorithm {algo_num} ({algo_mnemo}) '
          . 'present in the DNSKEY RRset. Fetched from the nameservers with IP '
          . 'addresses "{ns_ip_list}".',
          @_;
    },
    DS13_ALGO_NOT_SIGNED_SOA => sub {
        __x    # DNSSEC:DS13_ALGO_NOT_SIGNED_SOA
          'The SOA RRset is not signed by algorithm {algo_num} ({algo_mnemo}) '
          . 'present in the DNSKEY RRset. Fetched from the nameservers with IP '
          . 'addresses "{ns_ip_list}".',
          @_;
    },
    DS15_HAS_CDNSKEY_NO_CDS => sub {
        __x    # DNSSEC:DS15_HAS_CDNSKEY_NO_CDS
          'CDNSKEY RRset is found on nameservers that resolve to IP addresses '
          . '({ns_ip_list}), but no CDS RRset.',
          @_;
    },
    DS15_HAS_CDS_AND_CDNSKEY => sub {
        __x    # DNSSEC:DS15_HAS_CDS_AND_CDNSKEY
          'CDNSKEY and CDS RRsets are found on nameservers that resolve to IP addresses '
          . '({ns_ip_list}).',
          @_;
    },
    DS15_HAS_CDS_NO_CDNSKEY => sub {
        __x    # DNSSEC:DS15_HAS_CDS_NO_CDNSKEY
          'CDS RRset is found on nameservers that resolve to IP addresses '
          . '({ns_ip_list}), but no CDNSKEY RRset.',
          @_;
    },
    DS15_INCONSISTENT_CDNSKEY => sub {
        __x    # DNSSEC:DS15_INCONSISTENT_CDNSKEY
          'All servers do not have the same CDNSKEY RRset.', @_;
    },
    DS15_INCONSISTENT_CDS => sub {
        __x    # DNSSEC:DS15_INCONSISTENT_CDS
          'All servers do not have the same CDS RRset.', @_;
    },
    DS15_MISMATCH_CDS_CDNSKEY => sub {
        __x    # DNSSEC:DS15_MISMATCH_CDS_CDNSKEY
          'Both CDS and CDNSKEY RRsets are found on nameservers that resolve to IP '
          . 'addresses ({ns_ip_list}) but they do not match.',
          @_;
    },
    DS15_NO_CDS_CDNSKEY => sub {
        __x    # DNSSEC:DS15_NO_CDS_CDNSKEY
          'No CDS or CDNSKEY RRsets are found on any name server.', @_;
    },
    DS16_CDS_INVALID_RRSIG => sub {
        __x    # DNSSEC:DS16_CDS_INVALID_RRSIG
          'The CDS RRset is signed with an RRSIG with tag {keytag}, but the RRSIG does '
          . 'not match the DNSKEY with the same key tag. Fetched from the nameservers '
          . 'with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS16_CDS_MATCHES_NON_SEP_DNSKEY => sub {
        __x    # DNSSEC:DS16_CDS_MATCHES_NON_SEP_DNSKEY
          'The CDS record with tag {keytag} matches a DNSKEY record with SEP bit (bit 15) '
          . 'unset. Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS16_CDS_MATCHES_NON_ZONE_DNSKEY => sub {
        __x    # DNSSEC:DS16_CDS_MATCHES_NON_ZONE_DNSKEY
          'The CDS record with tag {keytag} matches a DNSKEY record with zone bit (bit 7) '
          . 'unset. Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS16_CDS_MATCHES_NO_DNSKEY => sub {
        __x    # DNSSEC:DS16_CDS_MATCHES_NO_DNSKEY
          'The CDS record with tag {keytag} does not match any DNSKEY record. Fetched '
          . 'from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS16_CDS_NOT_SIGNED_BY_CDS => sub {
        __x    # DNSSEC:DS16_CDS_NOT_SIGNED_BY_CDS
          'The CDS RRset is not signed by the DNSKEY that the CDS record with tag '
          . '{keytag} points to. Fetched from the nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS16_CDS_SIGNED_BY_UNKNOWN_DNSKEY => sub {
        __x    # DNSSEC:DS16_CDS_SIGNED_BY_UNKNOWN_DNSKEY
          'The CDS RRset is signed by RRSIG with tag {keytag} but that is not in the '
          . 'DNSKEY RRset. Fetched from the nameservers with P addresses "{ns_ip_list}".',
          @_;
    },
    DS16_CDS_UNSIGNED => sub {
        __x    # DNSSEC:DS16_CDS_UNSIGNED
          'The CDS RRset is not signed. Fetched from the nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS16_CDS_WITHOUT_DNSKEY => sub {
        __x    # DNSSEC:DS16_CDS_WITHOUT_DNSKEY
          'A CDS RRset exists, but no DNSKEY record exists. Fetched from the '
          . 'nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS16_DELETE_CDS => sub {
        __x    # DNSSEC:DS16_DELETE_CDS
          'A single "delete" CDS record is found on the nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS16_DNSKEY_NOT_SIGNED_BY_CDS => sub {
        __x    # DNSSEC:DS16_DNSKEY_NOT_SIGNED_BY_CDS
          'The DNSKEY RRset is not signed by the DNSKEY that the CDS record with tag '
          . '{keytag} points to. Fetched from the nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS16_MIXED_DELETE_CDS => sub {
        __x    # DNSSEC:DS16_MIXED_DELETE_CDS
          'The CDS RRset is a mixture between "delete" record and other records. '
          . 'Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS17_CDNSKEY_INVALID_RRSIG => sub {
        __x    # DNSSEC:DS17_CDNSKEY_INVALID_RRSIG
          'The CDNSKEY RRset is signed with an RRSIG with tag {keytag}, but the RRSIG does '
          . 'not match the DNSKEY with the same key tag. Fetched from the nameservers '
          . 'with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS17_CDNSKEY_IS_NON_SEP => sub {
        __x    # DNSSEC:DS17_CDNSKEY_IS_NON_SEP
          'The CDNSKEY record with tag {keytag} has the SEP bit (bit 15) unset. Fetched '
          . 'from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS17_CDNSKEY_IS_NON_ZONE => sub {
        __x    # DNSSEC:DS17_CDNSKEY_IS_NON_ZONE
          'The CDNSKEY record with tag {keytag} has the zone bit (bit 7) unset. Fetched '
          . 'from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS17_CDNSKEY_MATCHES_NO_DNSKEY => sub {
        __x    # DNSSEC:DS17_CDNSKEY_MATCHES_NO_DNSKEY
          'The CDNSKEY record with tag {keytag} does not match any DNSKEY record. Fetched '
          . 'from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS17_CDNSKEY_NOT_SIGNED_BY_CDNSKEY => sub {
        __x    # DNSSEC:DS17_CDNSKEY_NOT_SIGNED_BY_CDNSKEY
          'The CDNSKEY RRset is not signed by the DNSKEY that the CDNSKEY record with tag '
          . '{keytag} points to. Fetched from the nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS17_CDNSKEY_SIGNED_BY_UNKNOWN_DNSKEY => sub {
        __x    # DNSSEC:DS17_CDNSKEY_SIGNED_BY_UNKNOWN_DNSKEY
          'The CDNSKEY RRset is signed by RRSIG with tag {keytag} but that is not in the '
          . 'DNSKEY RRset. Fetched from the nameservers with P addresses "{ns_ip_list}".',
          @_;
    },
    DS17_CDNSKEY_UNSIGNED => sub {
        __x    # DNSSEC:DS17_CDNSKEY_UNSIGNED
          'The CDNSKEY RRset is not signed. Fetched from the nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS17_CDNSKEY_WITHOUT_DNSKEY => sub {
        __x    # DNSSEC:DS17_CDNSKEY_WITHOUT_DNSKEY
          'A CDNSKEY RRset exists, but no DNSKEY record exists. Fetched from the '
          . 'nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS17_DELETE_CDNSKEY => sub {
        __x    # DNSSEC:DS17_DELETE_CDNSKEY
          'A single "delete" CDNSKEY record is found on the nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS17_DNSKEY_NOT_SIGNED_BY_CDNSKEY => sub {
        __x    # DNSSEC:DS17_DNSKEY_NOT_SIGNED_BY_CDNSKEY
          'The DNSKEY RRset is not signed by the DNSKEY that the CDNSKEY record with tag '
          . '{keytag} points to. Fetched from the nameservers with IP addresses '
          . '"{ns_ip_list}".',
          @_;
    },
    DS17_MIXED_DELETE_CDNSKEY => sub {
        __x    # DNSSEC:DS17_MIXED_DELETE_CDNSKEY
          'The CDNSKEY RRset is a mixture between "delete" record and other records. '
          . 'Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS18_NO_MATCH_CDS_RRSIG_DS => sub {
        __x    # DNSSEC:DS18_NO_MATCH_CDS_RRSIG_DS
          'The CDS RRset is not signed with a DNSKEY record that a DS record points to. '
          . 'Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DS18_NO_MATCH_CDNSKEY_RRSIG_DS => sub {
        __x    # DNSSEC:DS18_NO_MATCH_CDNSKEY_RRSIG_DS
          'The CDNSKEY RRset is not signed with a DNSKEY record that a DS record points to. '
          . 'Fetched from the nameservers with IP addresses "{ns_ip_list}".',
          @_;
    },
    DURATION_LONG => sub {
        __x    # DNSSEC:DURATION_LONG
          'RRSIG with keytag {keytag} and covering type(s) {types} '
          . 'has a duration of {duration} seconds, which is too long.',
          @_;
    },
    DURATION_OK => sub {
        __x    # DNSSEC:DURATION_OK
          'RRSIG with keytag {keytag} and covering type(s) {types} '
          . 'has a duration of {duration} seconds, which is just fine.',
          @_;
    },
    EXTRA_PROCESSING_BROKEN => sub {
        __x    # DNSSEC:EXTRA_PROCESSING_BROKEN
          'Server at {server} sent {keys} DNSKEY records, and {sigs} RRSIG records.', @_;
    },
    EXTRA_PROCESSING_OK => sub {
        __x    # DNSSEC:EXTRA_PROCESSING_OK
          'Server at {server} sent {keys} DNSKEY records and {sigs} RRSIG records.', @_;
    },
    IPV4_DISABLED => sub {
        __x    # DNSSEC:IPV4_DISABLED
          'IPv4 is disabled, not sending "{rrtype}" query to {ns}.', @_;
    },
    IPV6_DISABLED => sub {
        __x    # DNSSEC:IPV6_DISABLED
          'IPv6 is disabled, not sending "{rrtype}" query to {ns}.', @_;
    },
    KEY_SIZE_OK => sub {
        __x    # DNSSEC:KEY_SIZE_OK
          'All keys from the DNSKEY RRset have the correct size.', @_;
    },
    NO_RESPONSE_DNSKEY => sub {
        __x    # DNSSEC:NO_RESPONSE_DNSKEY
          'Nameserver {ns} responded with no DNSKEY record(s).', @_;
    },
    NO_RESPONSE => sub {
        __x    # DNSSEC:NO_RESPONSE
          'Nameserver {ns} did not respond.', @_;
    },
    REMAINING_LONG => sub {
        __x    # DNSSEC:REMAINING_LONG
          'RRSIG with keytag {keytag} and covering type(s) {types} '
          . 'has a remaining validity of {duration} seconds, which is too long.',
          @_;
    },
    REMAINING_SHORT => sub {
        __x    # DNSSEC:REMAINING_SHORT
          'RRSIG with keytag {keytag} and covering type(s) {types} '
          . 'has a remaining validity of {duration} seconds, which is too short.',
          @_;
    },
    RRSIG_EXPIRATION => sub {
        __x    # DNSSEC:RRSIG_EXPIRATION
          'RRSIG with keytag {keytag} and covering type(s) {types} expires at ' . ': {date}.', @_;
    },
    RRSIG_EXPIRED => sub {
        __x    # DNSSEC:RRSIG_EXPIRED
          'RRSIG with keytag {keytag} and covering type(s) {types} has already '
          . 'expired (expiration is: {expiration}).',
          @_;
    },
    TEST_CASE_END => sub {
        __x    # DNSSEC:TEST_CASE_END
          'TEST_CASE_END {testcase}.', @_;
    },
    TEST_CASE_START => sub {
        __x    # DNSSEC:TEST_CASE_START
          'TEST_CASE_START {testcase}.', @_;
    }
);

=over

=item tag_descriptions()

    my $hash_ref = tag_descriptions();

Used by the L<built-in translation system|Zonemaster::Engine::Translator>.

Returns a reference to a hash, the keys of which are the message tags and the corresponding values are strings (message IDs).

=back

=cut

sub tag_descriptions {
    return \%TAG_DESCRIPTIONS;
}

=over

=item version()

    my $version_string = version();

Returns a string containing the version of the current module.

=back

=cut

sub version {
    return "$Zonemaster::Engine::Test::DNSSEC::VERSION";
}

=head1 INTERNAL METHODS

=over

=item _emit_log()

    my $log_entry = _emit_log( $message_tag_string, $hash_ref );

Adds a message to the L<logger|Zonemaster::Engine::Logger> for this module.
See L<Zonemaster::Engine::Logger::Entry/add($tag, $argref, $module, $testcase)> for more details.

Takes a string (message tag) and a reference to a hash (arguments).

Returns a L<Zonemaster::Engine::Logger::Entry> object.

=back

=cut

sub _emit_log { my ( $tag, $argref ) = @_; return Zonemaster::Engine->logger->add( $tag, $argref, 'DNSSEC' ); }

=over

=item _ip_disabled_message()

    my $bool = _ip_disabled_message( $logentry_array_ref, $ns, @query_type_array );

Checks if the IP version of a given name server is allowed to be queried. If not, it adds a logging message and returns true. Else, it returns false.

Takes a reference to an array of L<Zonemaster::Engine::Logger::Entry> objects, a L<Zonemaster::Engine::Nameserver> object and an array of strings (query type).

Returns a boolean.

=back

=cut

sub _ip_disabled_message {
    my ( $results_array, $ns, @rrtypes ) = @_;

    if ( not Zonemaster::Engine::Profile->effective->get(q{net.ipv6}) and $ns->address->version == $IP_VERSION_6 ) {
        push @$results_array, map {
          _emit_log(
            IPV6_DISABLED => {
                ns     => $ns->string,
                rrtype => $_
            }
          )
        } @rrtypes;
        return 1;
    }

    if ( not Zonemaster::Engine::Profile->effective->get(q{net.ipv4}) and $ns->address->version == $IP_VERSION_4 ) {
        push @$results_array, map {
          _emit_log(
            IPV4_DISABLED => {
                ns     => $ns->string,
                rrtype => $_,
            }
          )
        } @rrtypes;
        return 1;
    }
    return 0;
}

=head1 TESTS

=over

=item dnssec01()

    my @logentry_array = dnssec01( $zone );

Runs the L<DNSSEC01 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec01.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec01 {
    my ( $class, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC01';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my @ignored_parent_ns_ip;
    my ( @responds_without_valid_ds, @responds_with_ds );
    my ( %algo_2_ds, %non_algo_2_ds );
    my %sets = (
        'DS01_DS_ALGO_DEPRECATED' => {},
        'DS01_DS_ALGO_RESERVED' => {},
        'DS01_DS_ALGO_UNASSIGNED' => {},
        'DS01_DS_ALGO_PRIVATE' => {},
        'DS01_DS_ALGO_NOT_DS' => {},
        'DS01_DS_ALGO_OK' => {}
    );

    my $parent_nss = Zonemaster::Engine::TestMethodsV2->get_parent_ns_names_and_ips( $zone );

    my @undelegated_ds;
    if ( my $parent = $zone->parent ) {
        foreach my $ns ( @{ $parent->ns } ) {
            if ( $ns->fake_ds->{$zone->name} ) {
                @undelegated_ds = @{ $ns->fake_ds->{$zone->name} };

                if ( @undelegated_ds ) {
                    my $ns_ip =  "-";

                    foreach my $ds_data ( @undelegated_ds ) {
                        my $digest = $ds_data->digtype;
                        my $keytag = $ds_data->keytag;

                        push @{ $sets{$dnssec01_tags_mapping{$digest}}->{$digest}{$keytag} }, $ns_ip;

                        if ( $digest == 2 ) {
                            push @{ $algo_2_ds{$keytag} }, $ns_ip;
                        }
                        else {
                            push @{ $non_algo_2_ds{$keytag} }, $ns_ip;
                        }
                    }

                    push @responds_with_ds, $ns_ip;
                    $parent_nss = undef;
                }
            }
        }
    }

    my @nss = @{ $parent_nss // [] };

    my %ip_already_processed;
    foreach my $ns ( @nss ) {
        my $ns_ip = $ns->address->short;

        next if exists $ip_already_processed{$ns_ip};
        $ip_already_processed{$ns_ip} = [ grep { $_->address->short eq $ns_ip } @nss ];

        if ( _ip_disabled_message( \@results, $ns, q{DS} ) ) {
            next;
        }

        my @matching_nss = @{ $ip_already_processed{$ns_ip} };

        my $p = $ns->query( $zone->name, q{DS}, { dnssec => 1 } );

        if ( not $p or $p->rcode ne q{NOERROR} or not $p->has_edns or not $p->do or not $p->aa ) {
            push @ignored_parent_ns_ip, @matching_nss;
            next;
        }

        my @rrs = $p->get_records( q{DS}, q{answer} );

        my $valid_ds_rr = 0;
        foreach my $ds_rr ( @rrs ) {
            if ( $ds_rr->owner eq $zone->name->fqdn ){
                $valid_ds_rr = 1;
                last;
            }
        }

        if ( not $valid_ds_rr ){
            push @responds_without_valid_ds, @matching_nss;
            next;
        }

        push @responds_with_ds, @matching_nss;

        foreach my $ds_rr ( @rrs ) {
            my $digest = $ds_rr->digtype;
            my $keytag = $ds_rr->keytag;

            push @{ $sets{$dnssec01_tags_mapping{$digest}}->{$digest}{$keytag} }, @matching_nss;

            if ( $digest == 2 ) {
                push @{ $algo_2_ds{$keytag} }, @matching_nss;
            }
            else {
                push @{ $non_algo_2_ds{$keytag} }, @matching_nss;
            }
        }
    }

    while ( my ($tag_name, $set_ref) = each %sets ) {
        my %values = %{ $set_ref };
        if ( %values ) {
            foreach my $digest ( keys %values ) {
                foreach my $keytag ( keys %{ $values{$digest} } ) {
                    push @results,
                        _emit_log(
                            $tag_name => {
                                ns_list    => join( q{;}, uniq sort @{ $values{$digest}{$keytag} } ),
                                keytag        => $keytag,
                                ds_algo_num   => $digest,
                                ds_algo_descr => $digest_algorithms{$digest}
                            }
                        );
                }
            }
        }
    }

    if ( %non_algo_2_ds ) {
        while ( my ($keytag, $ns_ips) = each %non_algo_2_ds ) {
            my $lc = List::Compare->new( $ns_ips, $algo_2_ds{$keytag} );
            my @unique_nss = $lc->get_unique;

            if ( @unique_nss ) {
                push @results,
                    _emit_log(
                        DS01_DS_ALGO_2_MISSING => {
                            ns_list => join( q{;}, uniq sort @unique_nss ),
                            keytag => $keytag
                        }
                    );
            }
        }
    }

    if ( not @responds_without_valid_ds and not @responds_with_ds and @ignored_parent_ns_ip ) {
        push @results,
            _emit_log(
                DS01_NO_RESPONSE => {
                    ns_list => join( q{;}, uniq sort @ignored_parent_ns_ip )
                }
            )
    }

    if ( $zone->name eq '.' and not @undelegated_ds ) {
        push @results,
            _emit_log(
                DS01_ROOT_N_NO_UNDEL_DS => {}
            )
    }

    if ( $zone->name ne '.' and Zonemaster::Engine::Recursor->has_fake_addresses( $zone->name->string ) and not @undelegated_ds ) {
        push @results,
            _emit_log(
                DS01_UNDEL_N_NO_UNDEL_DS => {}
            )
    }

    if ( @responds_without_valid_ds ) {
        if ( not @responds_with_ds ) {
            push @results,
                _emit_log(
                    DS01_PARENT_ZONE_NO_DS => {
                        ns_list => join( q{;}, uniq sort @responds_without_valid_ds )
                    }
                )
        }
        else {
            push @results,
                _emit_log(
                    DS01_PARENT_SERVER_NO_DS => {
                        ns_list => join( q{;}, uniq sort @responds_without_valid_ds )
                    }
                )
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec01

=over

=item dnssec02()

    my @logentry_array = dnssec02( $zone );

Runs the L<DNSSEC02 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec02.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec02 {
    my ( $self, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC02';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my @ds_record;
    my %no_dnskey_for_ds;
    my %no_match_ds_dnskey;
    my %dnskey_not_for_zone_signing;
    my %dnskey_not_sep;
    my %no_matching_dnskey_rrsig;
    my %algo_not_supported_by_zm;
    my %rrsig_not_valid_by_dnskey;
    my %responding_child_ns;
    my %dnskey_matching_ds;
    my %has_dnskey_match_ds;
    my %has_rrsig_match_ds;
    my @ns_dnskey;
    my @ns_rrsig;
    my $continue_with_child_tests = 1;

    my $parent     = Zonemaster::Engine::TestMethods->method1( $zone );
    my @nss_parent = @{ $parent->ns };
    my %nss        = map { $_->name->string . '/' . $_->address->short => $_ } @nss_parent;
    my %ip_already_processed;

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, q{DS} ) ) {
            next;
        }

        my $ds_p = $ns->query( $zone->name, q{DS}, { dnssec => 1 } );
        if ( not $ds_p or $ds_p->rcode ne q{NOERROR} or not $ds_p->has_edns or not $ds_p->do or not $ds_p->aa) {
            next;
        }
        my @tmp_ds_records = $ds_p->get_records_for_name( q{DS}, $zone->name->string, q{answer} );
        if ( not scalar @tmp_ds_records ) {
            next;
        }
        foreach my $tmp_ds_record ( @tmp_ds_records ) {
            if (
                not grep {
                          $tmp_ds_record->keytag == $_->keytag
                      and $tmp_ds_record->digtype == $_->digtype
                      and $tmp_ds_record->algorithm == $_->algorithm
                      and $tmp_ds_record->hexdigest eq $_->hexdigest
                } @ds_record
              )
            {
                push @ds_record, $tmp_ds_record;
            }
        }
    }
    undef %ip_already_processed;

    if ( not scalar @ds_record ) {
        $continue_with_child_tests = 0;
    }

    if ( $continue_with_child_tests ) {

        my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
        my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
        my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;
        my %ip_already_processed;

        for my $nss_key ( sort keys %nss ) {
            my $ns = $nss{$nss_key};

            next if exists $ip_already_processed{$ns->address->short};
            $ip_already_processed{$ns->address->short} = 1;

            if ( _ip_disabled_message( \@results, $ns, q{DNSKEY} ) ) {
                next;
            }

            my $dnskey_p = $ns->query( $zone->name, q{DNSKEY}, { dnssec => 1, usevc => 0 } );
            if ( not $dnskey_p or $dnskey_p->rcode ne q{NOERROR} or not $dnskey_p->has_edns or not $dnskey_p->do or not $dnskey_p->aa ) {
                next;
            }
            my @dnskey_rrs = $dnskey_p->get_records_for_name( q{DNSKEY}, $zone->name->string, q{answer} );
            if ( not scalar @dnskey_rrs ) {
                next;
            }

            $responding_child_ns{$ns->address->short} = 1;

            my @dnskey_rrsig = $dnskey_p->get_records_for_name( q{RRSIG}, $zone->name->string, q{answer} );

            %dnskey_matching_ds = ();

            foreach my $ds ( @ds_record ) {
                my $matching_dnskey = undef;
                my @matching_keytag_dnskeys = grep { $ds->keytag == $_->keytag } @dnskey_rrs;
                my $match_ds_dnskey = 0;

                foreach my $matching_keytag_dnskey ( @matching_keytag_dnskeys ) {
                    if ( exists $LDNS_digest_algorithms_supported{$ds->digtype()} ) {
                        my $tmp_ds = $matching_keytag_dnskey->ds($LDNS_digest_algorithms_supported{$ds->digtype()});

                        if ( not $tmp_ds or $tmp_ds->hexdigest() eq $ds->hexdigest() ) {
                            $matching_dnskey = $matching_keytag_dnskey;
                            $match_ds_dnskey = 1;
                            last;
                        }
                    }
                    else{
                        $matching_dnskey = $matching_keytag_dnskey;
                        $match_ds_dnskey = 1;
                        last;
                    }
                }

                if ( scalar @matching_keytag_dnskeys >= 1 and not $match_ds_dnskey) {
                    $matching_dnskey = shift @matching_keytag_dnskeys;
                }

                if ( not $matching_dnskey ) {
                    push @{ $no_dnskey_for_ds{$ds->keytag} }, $ns->address->short;
                }
                else {
                    if ( not $match_ds_dnskey ) {
                        push @{ $no_match_ds_dnskey{$ds->keytag} }, $ns->address->short;
                    }
                    if ( not $matching_dnskey->flags & 256 ) { # Bit 7 (ZONE)
                        push @{ $dnskey_not_for_zone_signing{$ds->keytag} }, $ns->address->short;
                        next;
                    }
                    if ( not $matching_dnskey->flags & 1 ) { # Bit 15 (SEP)
                        push @{ $dnskey_not_sep{$ds->keytag} }, $ns->address->short;
                    }

                    $dnskey_matching_ds{$matching_dnskey} = $matching_dnskey->keytag;
                    $has_dnskey_match_ds{$ns->address->short} = 1;

                    foreach my $dnskey ( keys %dnskey_matching_ds ) {
                        my @matching_keytag_rrsigs = grep { $dnskey_matching_ds{$dnskey} == $_->keytag } @dnskey_rrsig;
                        my $time = $dnskey_p->timestamp;
                        my $found_match = 0;

                        foreach my $rrsig_record ( @matching_keytag_rrsigs ) {
                            my $msg = q{};
                            # Does not work if we have a list with just a DNSKEY
                            #my @key_list = ( $matching_dnskey );
                            #my $validate = $rrsig_record->verify_time( \@key_list, \@key_list, $time, $msg );
                            my $validate = $rrsig_record->verify_time( \@dnskey_rrs, \@dnskey_rrs, $time, $msg );
                            if ( not $validate and $msg =~ /Unknown cryptographic algorithm/ ) {
                                push @{ $algo_not_supported_by_zm{$rrsig_record->keytag}{$rrsig_record->algorithm} }, $ns->address->short;
                            }
                            elsif ( not $validate ) {
                                push @{ $rrsig_not_valid_by_dnskey{$rrsig_record->keytag} }, $ns->address->short;
                            }
                            else {
                                $found_match++;
                            }
                        }

                        if ( not scalar @matching_keytag_rrsigs or not $found_match ) {
                            push @{ $no_matching_dnskey_rrsig{$dnskey_matching_ds{$dnskey}} }, $ns->address->short;
                        }
                        else {
                            $has_rrsig_match_ds{$ns->address->short} = 1;
                        }
                    }
                }
            }
        }
    }

    if ( scalar keys %no_dnskey_for_ds ) {
        push @results, map {
          _emit_log(
            DS02_NO_DNSKEY_FOR_DS => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $no_dnskey_for_ds{$_} } )
            }
          )
        } keys %no_dnskey_for_ds;
    }
    if ( scalar keys %no_match_ds_dnskey ) {
        push @results, map {
          _emit_log(
            DS02_NO_MATCH_DS_DNSKEY => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $no_match_ds_dnskey{$_} } )
            }
          )
        } keys %no_match_ds_dnskey;
    }
    if ( scalar keys %dnskey_not_for_zone_signing ) {
        push @results, map {
          _emit_log(
            DS02_DNSKEY_NOT_FOR_ZONE_SIGNING => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $dnskey_not_for_zone_signing{$_} } )
            }
          )
        } keys %dnskey_not_for_zone_signing;
    }
    if ( scalar keys %dnskey_not_sep ) {
        push @results, map {
          _emit_log(
            DS02_DNSKEY_NOT_SEP => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $dnskey_not_sep{$_} } )
            }
          )
        } keys %dnskey_not_sep;
    }
    if ( scalar keys %no_matching_dnskey_rrsig ) {
        push @results, map {
          _emit_log(
            DS02_NO_MATCHING_DNSKEY_RRSIG => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $no_matching_dnskey_rrsig{$_} } )
            }
          )
        } keys %no_matching_dnskey_rrsig;
    }
    if ( scalar keys %algo_not_supported_by_zm ) {
        foreach my $keytag ( keys %algo_not_supported_by_zm ) {
            push @results, map {
              _emit_log(
                DS02_ALGO_NOT_SUPPORTED_BY_ZM => {
                    keytag     => $keytag,
                    algo_num   => $_,
                    algo_mnemo => $algo_properties{$_}{mnemonic},
                    ns_ip_list => join( q{;}, uniq sort @{ $algo_not_supported_by_zm{$keytag}{$_} } )
                }
              )
            } keys %{ $algo_not_supported_by_zm{$keytag} };
        }
    }
    if ( scalar keys %rrsig_not_valid_by_dnskey ) {
        push @results, map {
          _emit_log(
            DS02_RRSIG_NOT_VALID_BY_DNSKEY => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $rrsig_not_valid_by_dnskey{$_} } )
            }
          )
        } keys %rrsig_not_valid_by_dnskey;
    }

    foreach my $ns_ip ( keys %responding_child_ns ) {
        push @ns_dnskey, $ns_ip if not exists $has_dnskey_match_ds{$ns_ip};
        push @ns_rrsig, $ns_ip if not exists $has_rrsig_match_ds{$ns_ip};
    }

    if ( scalar @ns_dnskey ) {
        push @results,
            _emit_log(
              DS02_NO_VALID_DNSKEY_FOR_ANY_DS => {
                ns_ip_list => join( q{;}, sort @ns_dnskey )
              }
            )
    }
    else {
        if ( scalar @ns_rrsig ) {
            push @results,
              _emit_log(
                DS02_DNSKEY_NOT_SIGNED_BY_ANY_DS => {
                    ns_ip_list => join( q{;}, sort @ns_rrsig )
                }
              )
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec02

=over

=item dnssec03()

    my @logentry_array = dnssec03( $zone );

Runs the L<DNSSEC03 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec03.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec03 {
    my ( $self, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC03';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my @responds_without_dnskey;
    my @responds_with_dnskey;
    my @responds_without_nsec3;
    my @responds_with_nsec3;
    my @multiple_nsec3;
    my %hash_algorithm;
    my %nsec3_flags;
    my %nsec3_iterations;
    my %nsec3_salt_length;
    my @no_response_nsec_query;
    my @error_response_nsec_query;

    my %ip_already_processed;

    foreach my $ns ( @{ Zonemaster::Engine::TestMethods->method4and5( $zone ) } ){
        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, qw{DNSKEY NSEC} ) ) {
            next;
        }

        my $p1 = $ns->query( $zone->name, q{DNSKEY}, { dnssec => 1 } );

        if ( not $p1 or $p1->rcode ne q{NOERROR} or not $p1->aa ) {
            next;
        }

        if ( not scalar $p1->get_records_for_name( q{DNSKEY}, $zone->name, q{answer} ) ) {
            push @responds_without_dnskey, $ns;
            next;
        }

        push @responds_with_dnskey, $ns;

        my $p2 = $ns->query( $zone->name, q{NSEC}, { dnssec => 1 } );

        if ( not $p2 ) {
            push @no_response_nsec_query, $ns;
            next;
        }

        if ( $p2->rcode ne q{NOERROR} or not $p2->aa ) {
            push @error_response_nsec_query, $ns;
            next;
        }

        my @nsec3_rrs = $p2->get_records( q{NSEC3}, q{authority} );

        if ( not scalar @nsec3_rrs ) {
            push @responds_without_nsec3, $ns;
            next;
        }
        else {
            push @responds_with_nsec3, $ns;

            if ( scalar @nsec3_rrs > 1 ) {
                push @multiple_nsec3, $ns;
            }

            my $rr = ( @nsec3_rrs )[0];

            push @{ $hash_algorithm{$rr->algorithm} }, $ns if defined $rr->algorithm;
            push @{ $nsec3_flags{$rr->flags} }, $ns if defined $rr->flags;
            push @{ $nsec3_iterations{$rr->iterations} }, $ns if defined $rr->iterations;

            if ( defined $rr->salt ) {
                push @{ $nsec3_salt_length{length unpack('H*', $rr->salt)} }, $ns;
            }
            else {
                push @{ $nsec3_salt_length{0} }, $ns;
            }
        }
    }

    if ( not scalar @responds_with_dnskey and scalar @responds_without_dnskey ) {
        push @results,
            _emit_log(
              DS03_NO_DNSSEC_SUPPORT => {
                ns_list => join( q{;}, sort @responds_without_dnskey )
              }
            );
    }

    if ( scalar @responds_with_dnskey and scalar @responds_without_dnskey ) {
        push @results,
            _emit_log(
              DS03_SERVER_NO_DNSSEC_SUPPORT => {
                ns_list => join( q{;}, sort @responds_without_dnskey )
              }
            );
    }

    if ( not scalar @responds_with_nsec3 and scalar @responds_without_nsec3 ) {
        push @results,
            _emit_log(
              DS03_NO_NSEC3 => {
                ns_list => join( q{;}, sort @responds_without_nsec3 )
              }
            );
    }

    if ( scalar @responds_with_nsec3 and scalar @responds_without_nsec3 ) {
        push @results,
            _emit_log(
              DS03_SERVER_NO_NSEC3 => {
                ns_list => join( q{;}, sort @responds_without_nsec3 )
              }
            );
    }

    if ( scalar @multiple_nsec3 ) {
        push @results,
            _emit_log(
              DS03_ERR_MULT_NSEC3 => {
                ns_list => join( q{;}, sort @multiple_nsec3 )
              }
            );
    }

    if ( scalar keys %hash_algorithm ) {
        if ( scalar keys %hash_algorithm > 1 ) {
            push @results,
                _emit_log(
                  DS03_INCONSISTENT_HASH_ALGO => {}
                );
        }

        foreach my $algo ( keys %hash_algorithm ) {
            if ( $algo eq '1' ) {
                push @results,
                    _emit_log(
                      DS03_LEGAL_HASH_ALGO => {
                        ns_list => join( q{;}, sort @{ $hash_algorithm{$algo} } )
                      }
                    );
            }
            else {
                push @results,
                    _emit_log(
                      DS03_ILLEGAL_HASH_ALGO => {
                        ns_list => join( q{;}, sort @{ $hash_algorithm{$algo} } ),
                        algo_num => $algo
                      }
                    );
            }
        }
    }

    if ( scalar keys %nsec3_flags ) {
        if ( scalar keys %nsec3_flags > 1 ) {
            push @results,
                _emit_log(
                  DS03_INCONSISTENT_NSEC3_FLAGS => {}
                );
        }

        foreach my $flag ( keys %nsec3_flags ) {
            # Makes a list of bit positions corresponding to flags that are set, where the most-significant bit is 0.
            my @bit_positions = grep { $flag & (1 << ( 7 - $_ ) ) } (0..7);

            foreach my $bit ( grep { $_ >= 0 and $_ <= 6 } @bit_positions ) {
                push @results,
                    _emit_log(
                      DS03_UNASSIGNED_FLAG_USED => {
                        ns_list => join( q{;}, sort @{ $nsec3_flags{$flag} } ),
                        int => $bit
                      }
                    );
            }

            if ( grep { $_ == 7 } @bit_positions ) {
                # Note below that the Public Suffix List check is not yet implemented.
                if ( $zone->name eq '.' or $zone->name->next_higher eq '.' ) {
                    push @results,
                        _emit_log(
                          DS03_NSEC3_OPT_OUT_ENABLED_TLD => {
                            ns_list => join( q{;}, sort @{ $nsec3_flags{$flag} } )
                          }
                        );
                }
                else {
                    push @results,
                        _emit_log(
                          DS03_NSEC3_OPT_OUT_ENABLED_NON_TLD => {
                            ns_list => join( q{;}, sort @{ $nsec3_flags{$flag} } )
                          }
                        );
                }
            }
            else {
                push @results,
                    _emit_log(
                      DS03_NSEC3_OPT_OUT_DISABLED => {
                        ns_list => join( q{;}, sort @{ $nsec3_flags{$flag} } )
                      }
                );
            }
        }
    }

    if ( scalar keys %nsec3_iterations ) {
        if ( scalar keys %nsec3_iterations > 1 ) {
            push @results,
                _emit_log(
                  DS03_INCONSISTENT_ITERATION => {}
                );
        }

        foreach my $iter ( keys %nsec3_iterations ) {
            if ( $iter eq '0' ) {
                push @results,
                    _emit_log(
                      DS03_LEGAL_ITERATION_VALUE => {
                        ns_list => join( q{;}, sort @{ $nsec3_iterations{$iter} } )
                      }
                    );
            }
            else {
                push @results,
                    _emit_log(
                      DS03_ILLEGAL_ITERATION_VALUE => {
                        ns_list => join( q{;}, sort @{ $nsec3_iterations{$iter} } ),
                        int => $iter
                      }
                    );
            }
        }
    }

    if ( scalar keys %nsec3_salt_length ) {
        if ( scalar keys %nsec3_salt_length > 1 ) {
            push @results,
                _emit_log(
                  DS03_INCONSISTENT_SALT_LENGTH => {}
                );
        }

        foreach my $salt ( keys %nsec3_salt_length ) {
            if ( $salt eq '0' ) {
                push @results,
                    _emit_log(
                      DS03_LEGAL_EMPTY_SALT => {
                        ns_list => join( q{;}, sort @{ $nsec3_salt_length{$salt} } )
                      }
                    );
            }
            else {
                push @results,
                    _emit_log(
                      DS03_ILLEGAL_SALT_LENGTH => {
                        ns_list => join( q{;}, sort @{ $nsec3_salt_length{$salt} } ),
                        int => $salt
                      }
                    );
            }
        }
    }

    if ( scalar @no_response_nsec_query ) {
        push @results,
            _emit_log(
              DS03_NO_RESPONSE_NSEC_QUERY => {
                ns_list => join( q{;}, sort @no_response_nsec_query )
              }
            );
    }

    if ( scalar @error_response_nsec_query ) {
        push @results,
            _emit_log(
              DS03_ERROR_RESPONSE_NSEC_QUERY => {
                ns_list => join( q{;}, sort @error_response_nsec_query )
              }
            );
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec03

=over

=item dnssec04()

    my @logentry_array = dnssec04( $zone );

Runs the L<DNSSEC04 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec04.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec04 {
    my ( $self, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC04';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my $dnskey_p = $zone->query_one( $zone->name, 'DNSKEY', { dnssec => 1 } );
    if ( not $dnskey_p ) {
        return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
    }
    my @keys     = $dnskey_p->get_records( 'DNSKEY', 'answer' );
    my @key_sigs = $dnskey_p->get_records( 'RRSIG',  'answer' );

    my $soa_p = $zone->query_one( $zone->name, 'SOA', { dnssec => 1 } );
    if ( not $soa_p ) {
        return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
    }
    my @soas     = $soa_p->get_records( 'SOA',   'answer' );
    my @soa_sigs = $soa_p->get_records( 'RRSIG', 'answer' );

    foreach my $sig ( @key_sigs, @soa_sigs ) {
        push @results,
          _emit_log(
            RRSIG_EXPIRATION => {
                date   => scalar( gmtime($sig->expiration) ),
                keytag => $sig->keytag,
                types  => $sig->typecovered,
            }
          );

        my $remaining = $sig->expiration - int( $dnskey_p->timestamp );
        my $result_remaining;
        my $remaining_short_limit = Zonemaster::Engine::Profile->effective->get( q{test_cases_vars.dnssec04.REMAINING_SHORT} );
        my $remaining_long_limit  = Zonemaster::Engine::Profile->effective->get( q{test_cases_vars.dnssec04.REMAINING_LONG} );
        my $duration_long_limit   = Zonemaster::Engine::Profile->effective->get( q{test_cases_vars.dnssec04.DURATION_LONG} );

        if ( $remaining < 0 ) {    # already expired
            $result_remaining = _emit_log(
                RRSIG_EXPIRED => {
                    expiration => $sig->expiration,
                    keytag     => $sig->keytag,
                    types      => $sig->typecovered,
                }
            );
        }
        elsif ( $remaining < ( $remaining_short_limit ) ) {
            $result_remaining = _emit_log(
                REMAINING_SHORT => {
                    duration => $remaining,
                    keytag   => $sig->keytag,
                    types    => $sig->typecovered,
                }
            );
        }
        elsif ( $remaining > ( $remaining_long_limit ) ) {
            $result_remaining = _emit_log(
                REMAINING_LONG => {
                    duration => $remaining,
                    keytag   => $sig->keytag,
                    types    => $sig->typecovered,
                }
            );
        }

        my $duration = $sig->expiration - $sig->inception;
        my $result_duration;
        if ( $duration > ( $duration_long_limit ) ) {
            $result_duration = _emit_log(
                DURATION_LONG => {
                    duration => $duration,
                    keytag   => $sig->keytag,
                    types    => $sig->typecovered,
                }
            );
        }

        if ( $result_remaining or $result_duration ) {
            push @results, $result_remaining if $result_remaining;
            push @results, $result_duration  if $result_duration;
        }
        else {
            push @results,
              _emit_log(
                DURATION_OK => {
                    duration => $duration,
                    keytag   => $sig->keytag,
                    types    => $sig->typecovered,
                }
              );
        }
    } ## end foreach my $sig ( @key_sigs...)

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec04

=over

=item dnssec05()

    my @logentry_array = dnssec05( $zone );

Runs the L<DNSSEC05 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec05.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec05 {
    my ( $self, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC05';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my @ignored_ns_ip;
    my @responds_without_dnskey;
    my @responds_with_dnskey;
    my %sets = (
        'DS05_ALGO_DEPRECATED' => {},
        'DS05_ALGO_RESERVED' => {},
        'DS05_ALGO_UNASSIGNED' => {},
        'DS05_ALGO_NOT_RECOMMENDED' => {},
        'DS05_ALGO_PRIVATE' => {},
        'DS05_ALGO_NOT_ZONE_SIGN' => {},
        'DS05_ALGO_OK' => {}
    );

    my @nss = uniq grep { $_->isa('Zonemaster::Engine::Nameserver') } (
                @{ Zonemaster::Engine::TestMethodsV2->get_del_ns_names_and_ips( $zone ) // [] },
                @{ Zonemaster::Engine::TestMethodsV2->get_zone_ns_names_and_ips( $zone ) // [] }
              );

    my %ip_already_processed;
    for my $ns ( @nss ) {
        my $ns_ip = $ns->address->short;

        next if exists $ip_already_processed{$ns_ip};
        $ip_already_processed{$ns_ip} = [ grep { $_->address->short eq $ns_ip } @nss ];

        if ( _ip_disabled_message( \@results, $ns, 'DNSKEY' ) ) {
            next;
        }

        my @matching_nss = @{ $ip_already_processed{$ns_ip} };

        my $dnskey_p = $ns->query( $zone->name, 'DNSKEY', { dnssec => 1 } );

        if ( not $dnskey_p or $dnskey_p->rcode ne 'NOERROR' or not $dnskey_p->aa ) {
            push @ignored_ns_ip, @matching_nss;
            next;
        }

        my @dnskey_rrs = $dnskey_p->get_records_for_name( 'DNSKEY', $zone->name->fqdn, 'answer' );

        if ( not @dnskey_rrs ) {
            push @responds_without_dnskey, @matching_nss;
            next;
        }

        push @responds_with_dnskey, @matching_nss;

        foreach my $rr ( @dnskey_rrs ) {
            my $algo = $rr->algorithm;
            my $keytag = $rr->keytag;

            push @{ $sets{$dnssec05_tags_mapping{$algo}}->{$algo}{$keytag} }, @matching_nss;
        }
    }

    while ( my ($tag_name, $set_ref) = each %sets ) {
        my %values = %{ $set_ref };
        if ( %values ) {
            foreach my $algo ( keys %values ) {
                foreach my $keytag ( keys %{ $values{$algo} } ) {
                    push @results,
                        _emit_log(
                            $tag_name => {
                                ns_list    => join( q{;}, uniq sort @{ $values{$algo}{$keytag} } ),
                                keytag     => $keytag,
                                algo_num   => $algo,
                                algo_descr => $algo_properties{$algo}{description},
                                algo_mnemo => $algo_properties{$algo}{mnemonic}
                            }
                        );
                }
            }
        }
    }

    if ( not @responds_without_dnskey and not @responds_with_dnskey ) {
        push @results,
          _emit_log(
            DS05_NO_RESPONSE => {
                ns_list => join( q{;}, uniq sort @ignored_ns_ip )
            }
        );
    }

    if ( @responds_without_dnskey ) {
        if ( not @responds_with_dnskey ) {
            push @results,
              _emit_log(
                DS05_ZONE_NO_DNSSEC => {
                    ns_list => join( q{;}, uniq sort @responds_without_dnskey )
                }
            );
        }
        else {
            push @results,
              _emit_log(
                DS05_SERVER_NO_DNSSEC => {
                    ns_list => join( q{;}, uniq sort @responds_without_dnskey )
                }
            );
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec05

=over

=item dnssec06()

    my @logentry_array = dnssec06( $zone );

Runs the L<DNSSEC06 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec06.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec06 {
    my ( $self, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC06';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my $dnskey_aref = $zone->query_all( $zone->name, 'DNSKEY', { dnssec => 1 } );
    foreach my $dnskey_p ( @{$dnskey_aref} ) {
        next if not $dnskey_p;

        my @keys = $dnskey_p->get_records( 'DNSKEY', 'answer' );
        my @sigs = $dnskey_p->get_records( 'RRSIG',  'answer' );
        if ( @sigs > 0 and @keys > 0 ) {
            push @results,
              _emit_log(
                EXTRA_PROCESSING_OK => {
                    server => $dnskey_p->answerfrom,
                    keys   => scalar( @keys ),
                    sigs   => scalar( @sigs ),
                }
              );
        }
        elsif ( $dnskey_p->rcode eq q{NOERROR} and ( @sigs == 0 or @keys == 0 ) ) {
            push @results,
              _emit_log(
                EXTRA_PROCESSING_BROKEN => {
                    server => $dnskey_p->answerfrom,
                    keys   => scalar( @keys ),
                    sigs   => scalar( @sigs )
                }
              );
        }
    } ## end foreach my $dnskey_p ( @{$dnskey_aref...})

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec06

=over

=item dnssec07()

    my @logentry_array = dnssec07( $zone );

Runs the L<DNSSEC07 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec07.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec07 {
    my ( $self, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC07';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my $type_soa = q{SOA};
    my $type_dnskey = q{DNSKEY};
    my $type_ds = q{DS};
    my @query_types = ( $type_soa, $type_dnskey, $type_ds );

    my ( @ignored_child_ns, @ignored_parent_ns_ip );
    my ( @no_response_dnskey, @signed_response );
    my ( @no_auth_dnskey, %error_rcode_dnskey );
    my ( @no_dnskey, @no_ds, @ds_in_response );

    my @child_nss = uniq grep { $_->isa('Zonemaster::Engine::Nameserver') } (
            @{ Zonemaster::Engine::TestMethodsV2->get_del_ns_names_and_ips( $zone ) // [] },
            @{ Zonemaster::Engine::TestMethodsV2->get_zone_ns_names_and_ips( $zone ) // [] }
            );

    my %ip_already_processed;
    for my $ns ( @child_nss ) {
        my $ns_ip = $ns->address->short;

        next if exists $ip_already_processed{$ns_ip};
        $ip_already_processed{$ns_ip} = [ grep { $_->address->short eq $ns_ip } @child_nss ];

        if ( _ip_disabled_message( \@results, $ns, @query_types ) ) {
            next;
        }

        my @matching_nss = @{ $ip_already_processed{$ns_ip} };

        my $soa_p = $ns->query( $zone->name, $type_soa );

        if ( not $soa_p or $soa_p->rcode ne 'NOERROR' or not $soa_p->aa or not $soa_p->get_records( $type_soa, 'answer' ) ) {
            push @ignored_child_ns, @matching_nss;
            next;
        }

        my $dnskey_p = $ns->query( $zone->name, $type_dnskey, { dnssec => 1 } );

        if ( not $dnskey_p ) {
            push @no_response_dnskey, @matching_nss;
            next;
        }

        if ( not $dnskey_p->aa ) {
            push @no_auth_dnskey, @matching_nss;
            next;
        }

        if ( $dnskey_p->rcode ne 'NOERROR' ) {
            push @{ $error_rcode_dnskey{$dnskey_p->rcode} }, @matching_nss;
            next;
        }

        my @dnskey_rrs = $dnskey_p->get_records( $type_dnskey, 'answer' );
        my @rrsig_rrs = $dnskey_p->get_records( 'RRSIG', 'answer' );

        my $covered_dnskey = 0;
        foreach my $rr ( @rrsig_rrs ) {
            if ( $rr->typecovered eq $type_dnskey ) {
                $covered_dnskey = 1;
                last;
            }
        }

        if ( $covered_dnskey ) {
            push @signed_response, @matching_nss;
        }
        else {
            push @no_dnskey, @matching_nss;
        }
    }

    my $parent_ref = Zonemaster::Engine::TestMethodsV2->get_parent_ns_names_and_ips( $zone );

    my @undelegated_ds;
    if ( my $parent = $zone->parent ) {
        foreach my $ns ( @{ $parent->ns } ) {
            if ( $ns->fake_ds->{$zone->name} ) {
                @undelegated_ds = @{ $ns->fake_ds->{$zone->name} };

                if ( @undelegated_ds ) {
                    push @ds_in_response, '-';
                    $parent_ref = undef;
                }
            }
        }
    }

    if ( not @signed_response ) {
        $parent_ref = undef;
        @ds_in_response = ();
    }

    my @parent_nss = @{ $parent_ref // [] };

    %ip_already_processed = ();
    foreach my $ns ( @parent_nss ) {
        my $ns_ip = $ns->address->short;

        next if exists $ip_already_processed{$ns_ip};
        $ip_already_processed{$ns_ip} = [ grep { $_->address->short eq $ns_ip } @parent_nss ];

        if ( _ip_disabled_message( \@results, $ns, $type_ds ) ) {
            next;
        }

        my @matching_nss = @{ $ip_already_processed{$ns_ip} };

        my $ds_p = $ns->query( $zone->name, $type_ds, { dnssec => 1 } );

        if ( not $ds_p or $ds_p->rcode ne q{NOERROR} or not $ds_p->has_edns or not $ds_p->do or not $ds_p->aa ) {
            push @ignored_parent_ns_ip, @matching_nss;
            next;
        }

        my @ds_rrs = $ds_p->get_records_for_name( $type_ds, $zone->name, q{answer} );
        my @rrsig_rrs = $ds_p->get_records_for_name( q{RRSIG}, $zone->name, q{answer} );

        my $covered_ds = 0;
        foreach my $rr ( @rrsig_rrs ) {
            if ( $rr->typecovered eq $type_ds ) {
                $covered_ds = 1;
                last;
            }
        }

        if ( $covered_ds ) {
            push @ds_in_response, @matching_nss;
        }
        else {
            push @no_ds, @matching_nss;
        }
    }

    my @combined = uniq ( @ignored_child_ns, @no_response_dnskey, @no_auth_dnskey, map { @{ $error_rcode_dnskey{$_} } } keys %error_rcode_dnskey );
    my $lc = List::Compare->new(\@combined, \@child_nss);

    if ( not $lc->get_symmetric_difference ) {
        push @results,
            _emit_log(
              DS07_NOT_SIGNED => {}
        );
    }

    if ( @no_response_dnskey ) {
        push @results,
            _emit_log(
              DS07_NO_RESPONSE_DNSKEY => {
                  ns_list => join( q{;}, uniq sort @no_response_dnskey )
              }
        );
    }

    if ( @no_auth_dnskey ) {
        push @results,
            _emit_log(
              DS07_NON_AUTH_RESPONSE_DNSKEY => {
                  ns_list => join( q{;}, uniq sort @no_auth_dnskey )
              }
        );
    }

    if ( keys %error_rcode_dnskey ) {
        push @results,
            _emit_log(
              DS07_UNEXP_RCODE_RESP_DNSKEY => {
                  ns_list => join( q{;}, uniq sort @{ $error_rcode_dnskey{$_} } ),
                  rcode => $_
              }
        ) for keys %error_rcode_dnskey;
    }

    if ( @signed_response ) {
        push @results,
            _emit_log(
              DS07_SIGNED_ON_SERVER => {
                  ns_list => join( q{;}, uniq sort @signed_response )
              }
        );
    }

    if ( @no_dnskey ) {
        push @results,
            _emit_log(
              DS07_NOT_SIGNED_ON_SERVER => {
                  ns_list => join( q{;}, uniq sort @no_dnskey )
              }
        );
    }

    if ( @signed_response and @no_dnskey ) {
        push @results,
            _emit_log(
              DS07_INCONSISTENT_SIGNED => {}
        );
    }

    if ( @signed_response and not @no_dnskey ) {
        push @results,
            _emit_log(
              DS07_SIGNED => {}
        );
    }

    if ( not @signed_response and @no_dnskey ) {
        push @results,
            _emit_log(
              DS07_NOT_SIGNED => {}
        );
    }

    if ( @no_ds ) {
        push @results,
            _emit_log(
              DS07_NO_DS_ON_PARENT_SERVER => {
                  ns_list => join( q{;}, uniq sort @no_ds )
              }
        );
    }

    if ( @ds_in_response ) {
        push @results,
            _emit_log(
              DS07_DS_ON_PARENT_SERVER => {
                  ns_list => join( q{;}, uniq sort @ds_in_response )
              }
        );
    }

    if ( @no_ds and @ds_in_response ) {
        push @results,
            _emit_log(
              DS07_INCONSISTENT_DS => {}
        );
    }

    if ( not @no_dnskey and @signed_response ) {
        if ( @no_ds and not @ds_in_response ) {
            push @results,
                _emit_log(
                DS07_NO_DS_FOR_SIGNED_ZONE => {}
            );
        }
        if ( not @no_ds and @ds_in_response ) {
            push @results,
                _emit_log(
                DS07_DS_FOR_SIGNED_ZONE => {}
            );
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec07

=over

=item dnssec08()

    my @logentry_array = dnssec08( $zone );

Runs the L<DNSSEC08 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec08.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec08 {
    my ( $self, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC08';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );
    my @dnskey_without_rrsig;
    my %dnskey_rrsig_not_yet_valid;
    my %dnskey_rrsig_expired;
    my %no_matching_dnskey;
    my %rrsig_not_valid_by_dnskey;
    my %algo_not_supported_by_zm;

    my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
    my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
    my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;
    my %ip_already_processed;

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, q{DNSKEY} ) ) {
            next;
        }

        my $dnskey_p = $ns->query( $zone->name, q{DNSKEY}, { dnssec => 1 } );
        if ( not $dnskey_p ) {
            next;
        }
        if ( $dnskey_p->rcode ne q{NOERROR} ) {
            next;
        }
        if ( not $dnskey_p->aa ) {
            next;
        }

        my @dnskey_records = $dnskey_p->get_records_for_name( q{DNSKEY}, $zone->name->string, q{answer} );
        if ( not scalar @dnskey_records ) {
            next;
        }
        @dnskey_records = $dnskey_p->get_records( q{DNSKEY},  q{answer} );
        my @rrsig_records = $dnskey_p->get_records( q{RRSIG},  q{answer} );

        if ( not scalar @rrsig_records ) {
            push @dnskey_without_rrsig, $ns->address->short;
            next;
        }

        my $time = $dnskey_p->timestamp;
        foreach my $rrsig_record ( @rrsig_records ) {
            if ( $rrsig_record->inception > $time ) {
                push @{ $dnskey_rrsig_not_yet_valid{$rrsig_record->keytag} }, $ns->address->short;
            }
            elsif ( $rrsig_record->expiration < $time ) {
                push @{ $dnskey_rrsig_expired{$rrsig_record->keytag} }, $ns->address->short;
            }
            else {
                my $msg = q{};
                my $validate = $rrsig_record->verify_time( \@dnskey_records, \@dnskey_records, $time, $msg );
                if ( not $validate and $msg =~ /Unknown cryptographic algorithm/ ) {
                    push @{ $algo_not_supported_by_zm{$rrsig_record->keytag}{$rrsig_record->algorithm} }, $ns->address->short;
                }
                elsif ( not scalar grep { $_->keytag == $rrsig_record->keytag } @dnskey_records ) {
                    push @{ $no_matching_dnskey{$rrsig_record->keytag} }, $ns->address->short;
                }
                elsif ( not $validate ) {
                    push @{ $rrsig_not_valid_by_dnskey{$rrsig_record->keytag} }, $ns->address->short;
                }
            }
        }
    }
    if ( scalar @dnskey_without_rrsig ) {
        push @results,
          _emit_log(
            DS08_MISSING_RRSIG_IN_RESPONSE => {
                ns_ip_list => join( q{;}, uniq sort @dnskey_without_rrsig )
            }
          );
    }
    if ( scalar keys %dnskey_rrsig_not_yet_valid ) {
        push @results, map {
          _emit_log(
            DS08_DNSKEY_RRSIG_NOT_YET_VALID => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $dnskey_rrsig_not_yet_valid{$_} } )
            }
          )
        } keys %dnskey_rrsig_not_yet_valid;
    }
    if ( scalar keys %dnskey_rrsig_expired ) {
        push @results, map {
          _emit_log(
            DS08_DNSKEY_RRSIG_EXPIRED => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $dnskey_rrsig_expired{$_} } )
            }
          )
        } keys %dnskey_rrsig_expired;
    }
    if ( scalar keys %no_matching_dnskey ) {
        push @results, map {
          _emit_log(
            DS08_NO_MATCHING_DNSKEY => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $no_matching_dnskey{$_} } )
            }
          )
        } keys %no_matching_dnskey;
    }
    if ( scalar keys %rrsig_not_valid_by_dnskey ) {
        push @results, map {
          _emit_log(
            DS08_RRSIG_NOT_VALID_BY_DNSKEY => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $rrsig_not_valid_by_dnskey{$_} } )
            }
          )
        } keys %rrsig_not_valid_by_dnskey;
    }
    if ( scalar keys %algo_not_supported_by_zm ) {
        foreach my $keytag ( keys %algo_not_supported_by_zm ) {
            push @results, map {
              _emit_log(
                DS08_ALGO_NOT_SUPPORTED_BY_ZM => {
                    keytag     => $keytag,
                    algo_num   => $_,
                    algo_mnemo => $algo_properties{$_}{mnemonic},
                    ns_ip_list => join( q{;}, uniq sort @{ $algo_not_supported_by_zm{$keytag}{$_} } )
                }
              )
            } keys %{ $algo_not_supported_by_zm{$keytag} };
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec08

=over

=item dnssec09()

    my @logentry_array = dnssec09( $zone );

Runs the L<DNSSEC09 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec09.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec09 {
    my ( $self, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC09';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );
    my @soa_without_rrsig;
    my %soa_rrsig_not_yet_valid;
    my %soa_rrsig_expired;
    my %no_matching_dnskey;
    my %rrsig_not_valid_by_dnskey;
    my %algo_not_supported_by_zm;

    my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
    my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
    my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;
    my %ip_already_processed;

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, q{DNSKEY} ) ) {
            next;
        }

        my $dnskey_p = $ns->query( $zone->name, q{DNSKEY}, { dnssec => 1 } );
        if ( not $dnskey_p ) {
            next;
        }
        if ( $dnskey_p->rcode ne q{NOERROR} ) {
            next;
        }
        if ( not $dnskey_p->aa ) {
            next;
        }

        my @dnskey_records = $dnskey_p->get_records_for_name( q{DNSKEY}, $zone->name->string, q{answer} );
        if ( not scalar @dnskey_records ) {
            next;
        }

        my $soa_p = $ns->query( $zone->name, q{SOA}, { dnssec => 1, usevc => 0 } );
        if ( not $soa_p ) {
            next;
        }
        if ( $soa_p->rcode ne q{NOERROR} ) {
            next;
        }
        if ( not $soa_p->aa ) {
            next;
        }

        my @soa_records = $soa_p->get_records_for_name( q{SOA}, $zone->name->string, q{answer} );
        if ( not scalar @soa_records ) {
            next;
        }
        my @rrsig_records = $soa_p->get_records( q{RRSIG}, q{answer} );

        if ( not scalar @rrsig_records ) {
            push @soa_without_rrsig, $ns->address->short;
            next;
        }

        my $time = $dnskey_p->timestamp;
        foreach my $rrsig_record ( @rrsig_records ) {
            if ( $rrsig_record->inception > $time ) {
                push @{ $soa_rrsig_not_yet_valid{$rrsig_record->keytag} }, $ns->address->short;
            }
            elsif ( $rrsig_record->expiration < $time ) {
                push @{ $soa_rrsig_expired{$rrsig_record->keytag} }, $ns->address->short;
            }
            else {
                my $msg = q{};
                my $validate = $rrsig_record->verify_time( \@soa_records, \@dnskey_records, $time, $msg );
                if ( not $validate and $msg =~ /Unknown cryptographic algorithm/ ) {
                    push @{ $algo_not_supported_by_zm{$rrsig_record->keytag}{$rrsig_record->algorithm} }, $ns->address->short;
                }
                elsif ( not scalar grep { $_->keytag == $rrsig_record->keytag } @dnskey_records ) {
                    push @{ $no_matching_dnskey{$rrsig_record->keytag} }, $ns->address->short;
                }
                elsif ( not $validate ) {
                    push @{ $rrsig_not_valid_by_dnskey{$rrsig_record->keytag} }, $ns->address->short;
                }
            }
        }
    }
    if ( scalar @soa_without_rrsig ) {
        push @results,
          _emit_log(
            DS09_MISSING_RRSIG_IN_RESPONSE => {
                ns_ip_list => join( q{;}, uniq sort @soa_without_rrsig )
            }
          );
    }
    if ( scalar keys %soa_rrsig_not_yet_valid ) {
        push @results, map {
          _emit_log(
            DS09_SOA_RRSIG_NOT_YET_VALID => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $soa_rrsig_not_yet_valid{$_} } )
            }
          )
        } keys %soa_rrsig_not_yet_valid;
    }
    if ( scalar keys %soa_rrsig_expired ) {
        push @results, map {
          _emit_log(
            DS09_SOA_RRSIG_EXPIRED => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $soa_rrsig_expired{$_} } )
            }
          )
        } keys %soa_rrsig_expired;
    }
    if ( scalar keys %no_matching_dnskey ) {
        push @results, map {
          _emit_log(
            DS09_NO_MATCHING_DNSKEY => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $no_matching_dnskey{$_} } )
            }
          )
        } keys %no_matching_dnskey;
    }
    if ( scalar keys %rrsig_not_valid_by_dnskey ) {
        push @results, map {
          _emit_log(
            DS09_RRSIG_NOT_VALID_BY_DNSKEY => {
                keytag     => $_,
                ns_ip_list => join( q{;}, uniq sort @{ $rrsig_not_valid_by_dnskey{$_} } )
            }
          )
        } keys %rrsig_not_valid_by_dnskey;
    }
    if ( scalar keys %algo_not_supported_by_zm ) {
        foreach my $keytag ( keys %algo_not_supported_by_zm ) {
            push @results, map {
              _emit_log(
                DS09_ALGO_NOT_SUPPORTED_BY_ZM => {
                    keytag     => $keytag,
                    algo_num   => $_,
                    algo_mnemo => $algo_properties{$_}{mnemonic},
                    ns_ip_list => join( q{;}, uniq sort @{ $algo_not_supported_by_zm{$keytag}{$_} } )
                }
              )
            } keys %{ $algo_not_supported_by_zm{$keytag} }
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec09

=over

=item dnssec10()

    my @logentry_array = dnssec10( $zone );

Runs the L<DNSSEC10 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec10.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec10 {
    my ( $class, $zone ) = @_;
    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC10';

    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my $type_soa = q{SOA};
    my $type_dnskey = q{DNSKEY};
    my $type_nsec = q{NSEC};
    my $type_nsec3 = q{NSEC3};
    my $type_nsec3param = q{NSEC3PARAM};
    my @query_types = ( $type_dnskey, $type_nsec, $type_nsec3param );

    my %algo_not_supported_by_zm;
    my ( @erroneous_multiple_nsec, @erroneous_multiple_nsec3 );
    my ( @nsec_in_response, @nsec_nonstandard_nodata, @nsec3param_in_answer );
    my ( @nsec_incorrect_type_list, @nsec3_incorrect_type_list );
    my ( @nsec_mismatches_apex, @nsec3_mismatches_apex, @nsec3param_mismatches_apex );
    my ( @nsec_missing_signature, @nsec3_missing_signature );
    my ( %nsec_nodata_wrong_soa, %nsec3_nodata_wrong_soa );
    my ( @nsec_nodata_missing_soa, @nsec3_nodata_missing_soa );
    my ( @nsec_erroneous_answer, @nsec3param_erroneous_answer );
    my ( @nsec_nsec3_nodata, @nsec3param_nsec_nodata );
    my ( %nsec_rrsig_verify_error, %nsec3_rrsig_verify_error );
    my ( %nsec_rrsig_expired, %nsec3_rrsig_expired );
    my ( %nsec_rrsig_not_yet_valid, %nsec3_rrsig_not_yet_valid );
    my ( %nsec_rrsig_no_dnskey, %nsec3_rrsig_no_dnskey );
    my ( @nsec_rrsig_verified, @nsec3_rrsig_verified );
    my ( @nsec_response_error, @nsec3param_response_error );
    my ( @with_dnskey, @without_dnskey );

    my @all_ns = uniq grep { $_->isa('Zonemaster::Engine::Nameserver') } (
                @{ Zonemaster::Engine::TestMethodsV2->get_del_ns_names_and_ips( $zone ) // [] },
                @{ Zonemaster::Engine::TestMethodsV2->get_zone_ns_names_and_ips( $zone ) // [] }
              );

    my @ignored_nss;
    my %nss;
    push @{ $nss{$_->address->short} }, $_ for @all_ns;

    my $testing_time = time;

    for my $ns_ip ( keys %nss ) {
        my $ns = $nss{$ns_ip}[0];
        my @all_ns_for_ip = @{ $nss{$ns_ip} };

        if ( _ip_disabled_message( \@results, $ns, @query_types ) ) {
            push @ignored_nss, @all_ns_for_ip;
            next;
        }

        my $dnskey_p = $ns->query( $zone->name, $type_dnskey, { dnssec => 1 } );

        if ( not $dnskey_p or $dnskey_p->rcode ne q{NOERROR} or not $dnskey_p->aa ) {
            push @ignored_nss, @all_ns_for_ip;
            next;
        }

        my @dnskey_records = $dnskey_p->get_records_for_name( $type_dnskey, $zone->name->string, q{answer} );

        if ( not scalar @dnskey_records ) {
            push @without_dnskey, @all_ns_for_ip;
            next;
        }

        push @with_dnskey, @all_ns_for_ip;

        my $nsec_p = $ns->query( $zone->name, $type_nsec, { dnssec => 1 } );

        if ( not $nsec_p or $nsec_p->rcode ne q{NOERROR} or not $nsec_p->aa ) {
            push @nsec_response_error, @all_ns_for_ip;
        }
        elsif ( $nsec_p->answer ) {
            if ( scalar $nsec_p->get_records( $type_nsec, q{answer} ) ) {
                push @nsec_in_response, @all_ns_for_ip;

                if ( scalar $nsec_p->get_records( $type_nsec, q{answer} ) > 1 ) {
                    push @erroneous_multiple_nsec, @all_ns_for_ip;
                }
                elsif ( ($nsec_p->get_records( $type_nsec, q{answer} ))[0]->owner ne $zone->name ) {
                    push @nsec_mismatches_apex, @all_ns_for_ip;
                }
            }
            else {
                push @nsec_erroneous_answer, @all_ns_for_ip;
            }
        }
        elsif ( not $nsec_p->answer and scalar $nsec_p->get_records( $type_nsec, q{authority} ) ) {
            push @nsec_in_response, @all_ns_for_ip;
            push @nsec_nonstandard_nodata, @all_ns_for_ip;

            unless ( scalar $nsec_p->get_records( $type_soa, q{authority} ) ) {
                push @nsec_nodata_missing_soa, @all_ns_for_ip;
            }
            elsif ( ($nsec_p->get_records( $type_soa, q{authority} ))[0]->owner ne $zone->name ) {
                push @{ $nsec_nodata_wrong_soa{$zone->name} }, @all_ns_for_ip;
            }

            my @nsec_rrs = $nsec_p->get_records( $type_nsec, q{authority} );

            if ( scalar @nsec_rrs > 1 ) {
                push @erroneous_multiple_nsec, @all_ns_for_ip;
            }
            else {
                if ( $nsec_rrs[0]->owner ne $zone->name ) {
                    push @nsec_mismatches_apex, @all_ns_for_ip;
                }
                else {
                    my @mandatory_typelist = qw( SOA NS DNSKEY NSEC RRSIG );
                    my @forbidden_typelist = qw( NSEC3PARAM NSEC3 );
                    my %typelist = %{ $nsec_rrs[0]->typehref };

                    foreach my $type ( @mandatory_typelist ) {
                        if ( not exists $typelist{$type} ) {
                            push @nsec_incorrect_type_list, @all_ns_for_ip;
                            last;
                        }
                    }

                    foreach my $type ( @forbidden_typelist ) {
                        if ( exists $typelist{$type} ) {
                            push @nsec_incorrect_type_list, @all_ns_for_ip;
                            last;
                        }
                    }
                }

                my @nsec_rrsig_rrs = grep { $_->typecovered eq q{NSEC} } $nsec_p->get_records_for_name( q{RRSIG}, $nsec_rrs[0]->name );

                unless ( scalar @nsec_rrsig_rrs ) {
                    push @nsec_missing_signature, @all_ns_for_ip;
                }
                else {
                    foreach my $rr ( @nsec_rrsig_rrs ) {
                        my @matching_dnskeys = grep { $rr->keytag == $_->keytag } @dnskey_records;

                        unless ( scalar @matching_dnskeys ) {
                            push @{ $nsec_rrsig_no_dnskey{$rr->keytag} }, @all_ns_for_ip;
                        }
                        elsif ( $rr->expiration < $testing_time ) {
                            push @{ $nsec_rrsig_expired{$rr->keytag} }, @all_ns_for_ip;
                        }
                        elsif ( $rr->inception > $testing_time ) {
                            push @{ $nsec_rrsig_not_yet_valid{$rr->keytag} }, @all_ns_for_ip;
                        }
                        else {
                            my $i = 1;
                            foreach my $dnskey ( @matching_dnskeys ) {
                                my $msg = q{};
                                my $validated = $rr->verify_time( [grep { name( $_->name ) eq name( $rr->name ) } @nsec_rrs], [ $dnskey ], $testing_time, $msg );

                                if ( $validated ) {
                                    push @nsec_rrsig_verified, @all_ns_for_ip;
                                    last;
                                }

                                if ( $i >= scalar @matching_dnskeys ) {
                                    if ( $msg =~ /Unknown cryptographic algorithm/ ) {
                                        push @{ $algo_not_supported_by_zm{$dnskey->keytag}{$dnskey->algorithm} }, @all_ns_for_ip;
                                    }
                                    else {
                                        push @{ $nsec_rrsig_verify_error{$dnskey->keytag} }, @all_ns_for_ip;
                                    }
                                }

                                $i++;
                            }
                        }
                    }
                }
            }
        }
        elsif ( not $nsec_p->answer and scalar $nsec_p->get_records( $type_nsec3, q{authority} ) ) {
            push @nsec_nsec3_nodata, @all_ns_for_ip;

            unless ( scalar $nsec_p->get_records( $type_soa, q{authority} ) ) {
                push @nsec3_nodata_missing_soa, @all_ns_for_ip;
            }
            elsif ( ($nsec_p->get_records( $type_soa, q{authority} ))[0]->owner ne $zone->name ) {
                push @{ $nsec3_nodata_wrong_soa{$zone->name} }, @all_ns_for_ip;
            }

            my @nsec3_rrs = $nsec_p->get_records( $type_nsec3, q{authority} );

            if ( scalar @nsec3_rrs > 1 ) {
                push @erroneous_multiple_nsec3, @all_ns_for_ip;
            }
            else {
                unless ( $nsec3_rrs[0]->hash_name( $zone->name ) eq lc( @{ name($nsec3_rrs[0]->owner)->labels }[0] ) ) {
                    push @nsec3_mismatches_apex, @all_ns_for_ip;
                }
                else {
                    my @mandatory_typelist = qw( SOA NS DNSKEY NSEC3PARAM RRSIG );
                    my @forbidden_typelist = qw( NSEC NSEC3 );
                    my %typelist = %{ $nsec3_rrs[0]->typehref };

                    foreach my $type ( @mandatory_typelist ) {
                        if ( not exists $typelist{$type} ) {
                            push @nsec3_incorrect_type_list, @all_ns_for_ip;
                            last;
                        }
                    }

                    foreach my $type ( @forbidden_typelist ) {
                        if ( exists $typelist{$type} ) {
                            push @nsec3_incorrect_type_list, @all_ns_for_ip;
                            last;
                        }
                    }
                }

                my @nsec3_rrsig_rrs = grep { $_->typecovered eq q{NSEC3} } $nsec_p->get_records_for_name( q{RRSIG}, $nsec3_rrs[0]->name );

                unless ( scalar @nsec3_rrsig_rrs ) {
                    push @nsec3_missing_signature, @all_ns_for_ip;
                }
                else {
                    foreach my $rr ( @nsec3_rrsig_rrs ) {
                        my @matching_dnskeys = grep { $rr->keytag == $_->keytag } @dnskey_records;

                        unless ( scalar @matching_dnskeys ) {
                            push @{ $nsec3_rrsig_no_dnskey{$rr->keytag} }, @all_ns_for_ip;
                        }
                        elsif ( $rr->expiration < $testing_time ) {
                            push @{ $nsec3_rrsig_expired{$rr->keytag} }, @all_ns_for_ip;
                        }
                        elsif ( $rr->inception > $testing_time ) {
                            push @{ $nsec3_rrsig_not_yet_valid{$rr->keytag} }, @all_ns_for_ip;
                        }
                        else {
                            my $i = 1;
                            foreach my $dnskey ( @matching_dnskeys ) {
                                my $msg = q{};
                                my $validated = $rr->verify_time( [grep { name( $_->name ) eq name( $rr->name ) } @nsec3_rrs], [ $dnskey ], $testing_time, $msg );

                                if ( $validated ) {
                                    push @nsec3_rrsig_verified, @all_ns_for_ip;
                                    last;
                                }

                                if ( $i >= scalar @matching_dnskeys ) {
                                    if ( $msg =~ /Unknown cryptographic algorithm/ ) {
                                        push @{ $algo_not_supported_by_zm{$dnskey->keytag}{$dnskey->algorithm} }, @all_ns_for_ip;
                                    }
                                    else {
                                        push @{ $nsec3_rrsig_verify_error{$dnskey->keytag} }, @all_ns_for_ip;
                                    }
                                }

                                $i++;
                            }
                        }
                    }
                }
            }
        }

        my $nsec3param_p = $ns->query( $zone->name, $type_nsec3param, { dnssec => 1 } );

        if ( not $nsec3param_p or $nsec3param_p->rcode ne q{NOERROR} or not $nsec3param_p->aa ) {
            push @nsec3param_response_error, @all_ns_for_ip;
        }
        elsif ( $nsec3param_p->answer ) {
            if ( scalar $nsec3param_p->get_records( $type_nsec3param, q{answer} ) ) {
                push @nsec3param_in_answer, @all_ns_for_ip;

                if ( ($nsec3param_p->get_records( $type_nsec3param, q{answer} ))[0]->owner ne $zone->name ) {
                    push @nsec3param_mismatches_apex, @all_ns_for_ip;
                }
            }
            else {
                push @nsec3param_erroneous_answer, @all_ns_for_ip;
            }
        }
        elsif ( not $nsec3param_p->answer and scalar $nsec3param_p->get_records( $type_nsec, q{authority} ) ) {
            my @nsec_rrs = $nsec3param_p->get_records( $type_nsec, q{authority} );

            push @nsec3param_nsec_nodata, @all_ns_for_ip;

            unless ( scalar $nsec3param_p->get_records( $type_soa, q{authority} ) ) {
                push @nsec_nodata_missing_soa, @all_ns_for_ip;
            }
            elsif ( ($nsec3param_p->get_records( $type_soa, q{authority} ))[0]->owner ne $zone->name ) {
                push @{ $nsec_nodata_wrong_soa{$zone->name} }, @all_ns_for_ip;
            }

            if ( scalar @nsec_rrs > 1 ) {
                push @erroneous_multiple_nsec, @all_ns_for_ip;
            }
            else {
                unless ( $nsec_rrs[0]->owner eq $zone->name ) {
                    push @nsec_mismatches_apex, @all_ns_for_ip;
                }
                else {
                    my @mandatory_typelist = qw( SOA NS DNSKEY NSEC RRSIG );
                    my @forbidden_typelist = qw( NSEC3PARAM NSEC3 );
                    my %typelist = %{ $nsec_rrs[0]->typehref };

                    foreach my $type ( @mandatory_typelist ) {
                        if ( not exists $typelist{$type} ) {
                            push @nsec_incorrect_type_list, @all_ns_for_ip;
                            last;
                        }
                    }

                    foreach my $type ( @forbidden_typelist ) {
                        if ( exists $typelist{$type} ) {
                            push @nsec_incorrect_type_list, @all_ns_for_ip;
                            last;
                        }
                    }
                }

                my @nsec_rrsig_rrs = grep { $_->typecovered eq q{NSEC} } $nsec3param_p->get_records_for_name( q{RRSIG}, $nsec_rrs[0]->name );

                unless ( scalar @nsec_rrsig_rrs ) {
                    push @nsec_missing_signature, @all_ns_for_ip;
                }
                else {
                    foreach my $rr ( @nsec_rrsig_rrs ) {
                        my @matching_dnskeys = grep { $rr->keytag == $_->keytag } @dnskey_records;

                        unless ( scalar @matching_dnskeys ) {
                            push @{ $nsec_rrsig_no_dnskey{$rr->keytag} }, @all_ns_for_ip;
                        }
                        elsif ( $rr->expiration < $testing_time ) {
                            push @{ $nsec_rrsig_expired{$rr->keytag} }, @all_ns_for_ip;
                        }
                        elsif ( $rr->inception > $testing_time ) {
                            push @{ $nsec_rrsig_not_yet_valid{$rr->keytag} }, @all_ns_for_ip;
                        }
                        else {
                            my $i = 1;
                            foreach my $dnskey ( @matching_dnskeys ) {
                                my $msg = q{};
                                my $validated = $rr->verify_time( [grep { name( $_->name ) eq name( $rr->name ) } @nsec_rrs], [ $dnskey ], $testing_time, $msg );

                                if ( $validated ) {
                                    push @nsec_rrsig_verified, @all_ns_for_ip;
                                    last;
                                }

                                if ( $i >= scalar @matching_dnskeys ) {
                                    if ( $msg =~ /Unknown cryptographic algorithm/ ) {
                                        push @{ $algo_not_supported_by_zm{$dnskey->keytag}{$dnskey->algorithm} }, @all_ns_for_ip;
                                    }
                                    else {
                                        push @{ $nsec_rrsig_verify_error{$dnskey->keytag} }, @all_ns_for_ip;
                                    }
                                }

                                $i++;
                            }
                        }
                    }
                }
            }
        }
    }

    if ( scalar @erroneous_multiple_nsec ) {
        push @results,
          _emit_log(
            DS10_ERR_MULT_NSEC => {
                ns_list => join( q{;}, uniq sort @erroneous_multiple_nsec )
            }
          );
    }

    if ( scalar @erroneous_multiple_nsec3 ) {
        push @results,
          _emit_log(
            DS10_ERR_MULT_NSEC3 => {
                ns_list => join( q{;}, uniq sort @erroneous_multiple_nsec3 )
            }
          );
    }

    if ( scalar @nsec_nonstandard_nodata ) {
        push @results,
          _emit_log(
            DS10_NONSTANDARD_NSEC_RESPONSE => {
                ns_list => join( q{;}, uniq sort @nsec_nonstandard_nodata )
            }
          );
    }

    my $lc = List::Compare->new( \@nsec_in_response, \@nsec3param_nsec_nodata );
    my @diff = $lc->get_symmetric_difference;
    my @union = uniq map { $_->string } ( @nsec3param_in_answer, @nsec_nsec3_nodata );
    my $lc2 = List::Compare->new( \@diff, \@union );
    my @final_diff = $lc2->get_symmetric_difference;

    if ( scalar @diff and scalar @final_diff ) {
        push @results,
          _emit_log(
            DS10_INCONSISTENT_NSEC => {
                ns_list => join( q{;}, uniq sort @final_diff )
            }
          );
    }

    $lc = List::Compare->new( \@nsec3param_in_answer, \@nsec_nsec3_nodata );
    @diff = $lc->get_symmetric_difference;
    @union = uniq map { $_->string } ( @nsec_in_response, @nsec3param_nsec_nodata );
    $lc2 = List::Compare->new( \@diff, \@union );
    @final_diff = $lc2->get_symmetric_difference;

    if ( scalar @diff and scalar @final_diff ) {
        push @results,
          _emit_log(
            DS10_INCONSISTENT_NSEC3 => {
                ns_list => join( q{;}, uniq sort @final_diff )
            }
          );
    }

    $lc = List::Compare->new( [ @nsec3param_in_answer, @nsec_nsec3_nodata ], [ @nsec_in_response, @nsec3param_nsec_nodata ] );
    my @intersection = $lc->get_intersection;

    if ( @intersection ) {
        push @results,
          _emit_log(
            DS10_MIXED_NSEC_NSEC3 => {
                ns_list => join( q{;}, uniq sort @intersection )
            }
          );
    }

    if ( ( scalar @nsec_in_response or @nsec3param_nsec_nodata ) and not scalar @nsec3param_in_answer and not scalar @nsec_nsec3_nodata ) {
        push @results,
          _emit_log(
            DS10_HAS_NSEC => {
                ns_list => join( q{;}, uniq sort ( @nsec_in_response, @nsec3param_nsec_nodata ) )
            }
          );
    }

    if ( ( scalar @nsec3param_in_answer or @nsec_nsec3_nodata ) and not scalar @nsec_in_response and not scalar @nsec3param_nsec_nodata ) {
        push @results,
          _emit_log(
            DS10_HAS_NSEC3 => {
                ns_list => join( q{;}, uniq sort ( @nsec3param_in_answer, @nsec_nsec3_nodata ) )
            }
          );
    }

    @union = ( @nsec3param_in_answer, @nsec_nsec3_nodata );
    my @second_union = ( @nsec_in_response, @nsec3param_nsec_nodata );
    $lc = List::Compare->new( \@union, \@second_union );
    my @first = $lc->get_unique;
    my @second = $lc->get_complement;

    if ( scalar @first and scalar @second ) {
        push @results,
          _emit_log(
            DS10_INCONSISTENT_NSEC_NSEC3 => {
                ns_list => join( q{;}, uniq sort ( @union, @second_union ) )
            }
          );
    }

    if ( scalar @nsec_incorrect_type_list ) {
        push @results,
          _emit_log(
            DS10_NSEC_ERR_TYPE_LIST => {
                ns_list => join( q{;}, uniq sort @nsec_incorrect_type_list )
            }
          );
    }

    if ( scalar @nsec_mismatches_apex ) {
        push @results,
          _emit_log(
            DS10_NSEC_MISMATCHES_APEX => {
                ns_list => join( q{;}, uniq sort @nsec_mismatches_apex )
            }
          );
    }

    if ( scalar keys %nsec_nodata_wrong_soa ) {
        push @results, map {
              _emit_log(
                DS10_NSEC_NODATA_WRONG_SOA => {
                    domain => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec_nodata_wrong_soa{$_} } )
                }
              )
            } keys %nsec_nodata_wrong_soa;
    }

    if ( scalar @nsec_nodata_missing_soa ) {
        push @results,
          _emit_log(
            DS10_NSEC_NODATA_MISSING_SOA => {
                ns_list => join( q{;}, uniq sort @nsec_nodata_missing_soa )
            }
          );
    }

    if ( scalar @nsec_erroneous_answer ) {
        push @results,
          _emit_log(
            DS10_NSEC_GIVES_ERR_ANSWER => {
                ns_list => join( q{;}, uniq sort @nsec_erroneous_answer )
            }
          );
    }

    if ( scalar @nsec_response_error ) {
        push @results,
          _emit_log(
            DS10_NSEC_QUERY_RESPONSE_ERR => {
                ns_list => join( q{;}, uniq sort @nsec_response_error )
            }
          );
    }

    if ( scalar @nsec3_incorrect_type_list ) {
        push @results,
          _emit_log(
            DS10_NSEC3_ERR_TYPE_LIST => {
                ns_list => join( q{;}, uniq sort @nsec3_incorrect_type_list )
            }
          );
    }

    if ( scalar @nsec3_mismatches_apex ) {
        push @results,
          _emit_log(
            DS10_NSEC3_MISMATCHES_APEX => {
                ns_list => join( q{;}, uniq sort @nsec3_mismatches_apex )
            }
          );
    }

    if ( scalar keys %nsec3_nodata_wrong_soa ) {
        push @results, map {
              _emit_log(
                DS10_NSEC3_NODATA_WRONG_SOA => {
                    domain => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec3_nodata_wrong_soa{$_} } )
                }
              )
            } keys %nsec3_nodata_wrong_soa;
    }

    if ( scalar @nsec3_nodata_missing_soa ) {
        push @results,
          _emit_log(
            DS10_NSEC3_NODATA_MISSING_SOA => {
                ns_list => join( q{;}, uniq sort @nsec3_nodata_missing_soa )
            }
          );
    }

    if ( scalar @nsec3param_erroneous_answer ) {
        push @results,
          _emit_log(
            DS10_NSEC3PARAM_GIVES_ERR_ANSWER => {
                ns_list => join( q{;}, uniq sort @nsec3param_erroneous_answer )
            }
          );
    }

    if ( scalar @nsec3param_mismatches_apex ) {
        push @results,
          _emit_log(
            DS10_NSEC3PARAM_MISMATCHES_APEX => {
                ns_list => join( q{;}, uniq sort @nsec3param_mismatches_apex )
            }
          );
    }

    if ( scalar @nsec3param_response_error ) {
        push @results,
          _emit_log(
            DS10_NSEC3PARAM_QUERY_RESPONSE_ERR => {
                ns_list => join( q{;}, uniq sort @nsec3param_response_error )
            }
          );
    }

    if ( scalar @nsec_missing_signature ) {
        push @results,
          _emit_log(
            DS10_NSEC_MISSING_SIGNATURE => {
                ns_list => join( q{;}, uniq sort @nsec_missing_signature )
            }
          );
    }

    if ( scalar @nsec3_missing_signature ) {
        push @results,
          _emit_log(
            DS10_NSEC3_MISSING_SIGNATURE => {
                ns_list => join( q{;}, uniq sort @nsec3_missing_signature )
            }
          );
    }

    if ( scalar keys %nsec_rrsig_no_dnskey ) {
        push @results, map {
              _emit_log(
                DS10_NSEC_RRSIG_NO_DNSKEY => {
                    keytag => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec_rrsig_no_dnskey{$_} } )
                }
              )
            } keys %nsec_rrsig_no_dnskey;
    }

    if ( scalar keys %nsec_rrsig_expired ) {
        push @results, map {
              _emit_log(
                DS10_NSEC_RRSIG_EXPIRED => {
                    keytag => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec_rrsig_expired{$_} } )
                }
              )
            } keys %nsec_rrsig_expired;
    }

    if ( scalar keys %nsec_rrsig_not_yet_valid ) {
        push @results, map {
              _emit_log(
                DS10_NSEC_RRSIG_NOT_YET_VALID => {
                    keytag => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec_rrsig_not_yet_valid{$_} } )
                }
              )
            } keys %nsec_rrsig_not_yet_valid;
    }

    if ( scalar keys %nsec_rrsig_verify_error ) {
        push @results, map {
              _emit_log(
                DS10_NSEC_RRSIG_VERIFY_ERROR => {
                    keytag => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec_rrsig_verify_error{$_} } )
                }
              )
            } keys %nsec_rrsig_verify_error;
    }

    if ( values %nsec_rrsig_no_dnskey or values %nsec_rrsig_expired or values %nsec_rrsig_not_yet_valid or values %nsec_rrsig_verify_error ) {
        my @combined_ns = uniq ( values %nsec_rrsig_no_dnskey, values %nsec_rrsig_expired, values %nsec_rrsig_not_yet_valid, values %nsec_rrsig_verify_error );
        my @ns_list;
        
        foreach my $ns_aref ( @combined_ns ) {
            foreach my $ns ( @$ns_aref ) {
                push @ns_list, $ns unless grep { $_ eq $ns } @nsec_rrsig_verified;
            }
        }

        push @results,
          _emit_log(
            DS10_NSEC_NO_VERIFIED_SIGNATURE => {
                ns_list => join( q{;}, uniq sort @ns_list )
            }
          ) if scalar @ns_list;
    }

    if ( scalar keys %nsec3_rrsig_no_dnskey ) {
        push @results, map {
              _emit_log(
                DS10_NSEC3_RRSIG_NO_DNSKEY => {
                    keytag => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec3_rrsig_no_dnskey{$_} } )
                }
              )
            } keys %nsec3_rrsig_no_dnskey;
    }

    if ( scalar keys %nsec3_rrsig_expired ) {
        push @results, map {
              _emit_log(
                DS10_NSEC3_RRSIG_EXPIRED => {
                    keytag => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec3_rrsig_expired{$_} } )
                }
              )
            } keys %nsec3_rrsig_expired;
    }

    if ( scalar keys %nsec3_rrsig_not_yet_valid ) {
        push @results, map {
              _emit_log(
                DS10_NSEC3_RRSIG_NOT_YET_VALID => {
                    keytag => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec3_rrsig_not_yet_valid{$_} } )
                }
              )
            } keys %nsec3_rrsig_not_yet_valid;
    }

    if ( scalar keys %nsec3_rrsig_verify_error ) {
        push @results, map {
              _emit_log(
                DS10_NSEC3_RRSIG_VERIFY_ERROR => {
                    keytag => $_,
                    ns_list => join( q{;}, uniq sort @{ $nsec3_rrsig_verify_error{$_} } )
                }
              )
            } keys %nsec3_rrsig_verify_error;
    }

    if ( values %nsec3_rrsig_no_dnskey or values %nsec3_rrsig_expired or values %nsec3_rrsig_not_yet_valid or values %nsec3_rrsig_verify_error ) {
        my @combined_ns = uniq ( values %nsec3_rrsig_no_dnskey, values %nsec3_rrsig_expired, values %nsec3_rrsig_not_yet_valid, values %nsec3_rrsig_verify_error );
        my @ns_list;
        
        foreach my $ns_aref ( @combined_ns ) {
            foreach my $ns ( @$ns_aref ) {
                push @ns_list, $ns unless grep { $_ eq $ns } @nsec3_rrsig_verified;
            }
        }

        push @results,
          _emit_log(
            DS10_NSEC3_NO_VERIFIED_SIGNATURE => {
                ns_list => join( q{;}, uniq sort @ns_list )
            }
          ) if scalar @ns_list;
    }

    if ( scalar keys %algo_not_supported_by_zm ) {
        foreach my $keytag ( keys %algo_not_supported_by_zm ) {
            push @results, map {
              _emit_log(
                DS10_ALGO_NOT_SUPPORTED_BY_ZM => {
                    keytag     => $keytag,
                    algo_num   => $_,
                    algo_mnemo => $algo_properties{$_}{mnemonic},
                    ns_ip_list => join( q{;}, uniq sort @{ $algo_not_supported_by_zm{$keytag}{$_} } )
                }
              )
            } keys %{ $algo_not_supported_by_zm{$keytag} };
        }
    }

    if ( not scalar @with_dnskey and scalar @without_dnskey ) {
        push @results,
          _emit_log(
            DS10_ZONE_NO_DNSSEC => {
                ns_list => join( q{;}, uniq sort @without_dnskey )
            }
          );
    }

    if ( scalar @with_dnskey and scalar @without_dnskey ) {
        push @results,
          _emit_log(
            DS10_SERVER_NO_DNSSEC => {
                ns_list => join( q{;}, uniq sort @without_dnskey )
            }
          );
    }

    $lc = List::Compare->new( [ @all_ns ], [ @ignored_nss, @without_dnskey, @nsec_in_response, @nsec3param_nsec_nodata, @nsec3param_in_answer, @nsec_nsec3_nodata ] );
    @first = $lc->get_unique;

    if ( @first ) {
        push @results,
          _emit_log(
            DS10_EXPECTED_NSEC_NSEC3_MISSING => {
                ns_list => join( q{;}, uniq sort @first )
            }
          );
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec10

=over

=item dnssec11()

    my @logentry_array = dnssec11( $zone );

Runs the L<DNSSEC11 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec11.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec11 {
    my ( $class, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC11';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );
    my @undetermined_ds;
    my @no_ds_record;
    my @has_ds_record;
    my $continue_with_child_tests = 1;

    my $parent     = Zonemaster::Engine::TestMethods->method1( $zone );
    my @nss_parent = @{ $parent->ns };
    my %nss        = map { $_->name->string . '/' . $_->address->short => $_ } @nss_parent;
    my %ip_already_processed;

    my $is_undelegated = Zonemaster::Engine::Recursor->has_fake_addresses( $zone->name->string );

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        if ( $is_undelegated ){
            if ( not $ns->fake_ds->{$zone->name->string} ){
                return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
            }
            last;
        }

        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, q{DS} ) ) {
            next;
        }

        my $ds_p = $ns->query( $zone->name, q{DS}, { dnssec => 1, usevc => 0 } );
        if ( $ds_p and $ds_p->tc ) {
            $ds_p = $ns->query( $zone->name, q{DS}, { dnssec => 1, usevc => 1 } );
        }

        if ( not $ds_p or $ds_p->rcode ne q{NOERROR} or not $ds_p->aa ) {
            push @undetermined_ds, $ns->address->short;
            next;
        }
        my @ds = $ds_p->get_records_for_name( q{DS}, $zone->name->string, q{answer} );
        if ( not scalar @ds ) {
            push @no_ds_record, $ns->address->short;
        }
        else {
            push @has_ds_record, $ns->address->short;
        }
    }
    undef %ip_already_processed;

    if ( scalar @undetermined_ds and not scalar @no_ds_record and not scalar @has_ds_record ) {
        push @results, _emit_log( DS11_UNDETERMINED_DS => {} );
        $continue_with_child_tests = 0;
    }
    elsif ( scalar @no_ds_record and not scalar @has_ds_record ) {
        $continue_with_child_tests = 0;
    }
    elsif ( scalar @no_ds_record and scalar @has_ds_record ) {
        push @results, _emit_log( DS11_INCONSISTENT_DS => {} );
        push @results,
          _emit_log(
            DS11_PARENT_WITHOUT_DS => {
                ns_ip_list => join( q{;}, sort @no_ds_record )
            }
          );
        push @results,
          _emit_log(
             DS11_PARENT_WITH_DS => {
                ns_ip_list => join( q{;}, sort @has_ds_record )
            }
          );
    }

    if ( $continue_with_child_tests ) {
        my @query_types = qw{SOA DNSKEY};
        my @undetermined_dnskey;
        my @no_dnskey_record;
        my @has_dnskey_record;

        my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
        my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
        my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;
        my %ip_already_processed;

        for my $nss_key ( sort keys %nss ) {
            my $ns = $nss{$nss_key};

            next if exists $ip_already_processed{$ns->address->short};
            $ip_already_processed{$ns->address->short} = 1;

            if ( _ip_disabled_message( \@results, $ns, @query_types ) ) {
                next;
            }

            my $soa_p = $ns->query( $zone->name, q{SOA}, { usevc => 0 } );
            if ( not $soa_p ) {
                next;
            }
            if ( $soa_p->rcode ne q{NOERROR} ) {
                next;
            }
            if ( not $soa_p->aa ) {
                next;
            }
            my @soa = $soa_p->get_records_for_name( q{SOA}, $zone->name->string, q{answer} );
            if ( not scalar @soa ) {
                next;
            }

            my $dnskey_p = $ns->query( $zone->name, q{DNSKEY}, { usevc => 0 } );
            if ( $dnskey_p and $dnskey_p->tc ) {
                $dnskey_p = $ns->query( $zone->name, q{DNSKEY}, { usevc => 1 } );
            }

            if ( not $dnskey_p or $dnskey_p->rcode ne q{NOERROR} or not $dnskey_p->aa ) {
                push @undetermined_dnskey, $ns->address->short;
                next;
            }
            my @dnskey = $dnskey_p->get_records_for_name( q{DNSKEY}, $zone->name->string, q{answer} );
            if ( not scalar @dnskey ) {
                push @no_dnskey_record, $ns->address->short;
            }
            else {
                push @has_dnskey_record, $ns->address->short;
            }
        }
        undef %ip_already_processed;

        if ( scalar @undetermined_dnskey and not scalar @no_dnskey_record and not scalar @has_dnskey_record ) {
            push @results, _emit_log( DS11_UNDETERMINED_SIGNED_ZONE => {} );
        }
        elsif ( scalar @no_dnskey_record and not scalar @has_dnskey_record ) {
            push @results, _emit_log( DS11_DS_BUT_UNSIGNED_ZONE => {} );
        }
        elsif ( scalar @no_dnskey_record and scalar @has_dnskey_record ) {
            push @results, _emit_log( DS11_INCONSISTENT_SIGNED_ZONE => {} );
            push @results,
              _emit_log(
                DS11_NS_WITH_UNSIGNED_ZONE => {
                    ns_ip_list => join( q{;}, sort @no_dnskey_record )
                }
              );
            push @results,
              _emit_log(
                  DS11_NS_WITH_SIGNED_ZONE => {
                    ns_ip_list => join( q{;}, sort @has_dnskey_record )
                }
              );
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec11

=over

=item dnssec13()

    my @logentry_array = dnssec13( $zone );

Runs the L<DNSSEC13 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec13.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec13 {
    my ( $class, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC13';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );
    my @query_types = qw{DNSKEY SOA NS};
    my %algo_not_signed;
    my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
    my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
    my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;
    my %ip_already_processed;

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, @query_types ) ) {
            next;
        }

        my %dnskey_algorithm;
        foreach my $query_type ( @query_types ) {

            my $p = $ns->query( $zone->name, $query_type, { dnssec => 1, usevc => 0 } );
            if ( not $p ) {
                next;
            }
            if ( $p->rcode ne q{NOERROR} ) {
                next;
            }
            if ( not $p->aa ) {
                next;
            }
            my @type_records = $p->get_records( $query_type, q{answer} );
            if ( not scalar @type_records ) {
                next;
            }
            my @rrsig_records = $p->get_records( q{RRSIG} , q{answer} );
            if ( not scalar @rrsig_records ) {
                next;
            }

            if ( $query_type eq q{DNSKEY} ) {
                %dnskey_algorithm = map { $_->algorithm => 1 } @type_records;
            }
            foreach my $algorithm ( keys %dnskey_algorithm ) {
                if ( not scalar grep { $_->algorithm == $algorithm } @rrsig_records ) {
                    push @{ $algo_not_signed{ lc($query_type) }{$algorithm} }, $ns->address->short;
                }
            }
        }
    }

    foreach my $query_type ( @query_types ) {
        if ( exists $algo_not_signed{ lc($query_type) } ) {
            foreach my $algorithm ( keys %{ $algo_not_signed{ lc($query_type) } } ) {
                push @results,
                  _emit_log(
                    "DS13_ALGO_NOT_SIGNED_${query_type}" => {
                        ns_ip_list => join( q{;}, uniq sort @{ $algo_not_signed{ lc($query_type) }{$algorithm} }),
                        algo_num   => $algorithm,
                        algo_mnemo => $algo_properties{$algorithm}{mnemonic}
                    }
                  );
            }
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec13

=over

=item dnssec14()

    my @logentry_array = dnssec14( $zone );

Runs the L<DNSSEC14 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec14.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec14 {
    my ( $class, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC14';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );
    my @dnskey_rrs;

    my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
    my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
    my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        if ( _ip_disabled_message( \@results, $ns, q{DNSKEY} ) ) {
            next;
        }

        my $dnskey_p = $ns->query( $zone->name, 'DNSKEY', { dnssec => 1, usevc => 0 } );
        if ( not $dnskey_p ) {
            push @results, _emit_log( NO_RESPONSE => { ns => $ns->string } );
            next;
        }

        my @keys = $dnskey_p->get_records( 'DNSKEY', 'answer' );
        if ( not @keys ) {
            push @results, _emit_log( NO_RESPONSE_DNSKEY => { ns => $ns->string } );
            next;
        } else {
            push @dnskey_rrs, @keys;
        }
    }

    my %investigated_keys;
    foreach my $key ( @dnskey_rrs ) {
        my $algo = $key->algorithm;

        next if not exists $rsa_key_size_details{$algo};

        # Only test once per keytag, keysize and algorithm
        my $key_ref = join ':', $key->keytag, $key->keysize, $algo;
        next if exists $investigated_keys{$key_ref};

        my $algo_args = {
            algo_num   => $algo,
            algo_descr => $algo_properties{$algo}{description},
            keytag     => $key->keytag,
            keysize    => $key->keysize,
            keysizemin => $rsa_key_size_details{$algo}{min_size},
            keysizemax => $rsa_key_size_details{$algo}{max_size},
            keysizerec => $rsa_key_size_details{$algo}{rec_size},
        };

        if ( $key->keysize < $rsa_key_size_details{$algo}{min_size} ) {
            push @results, _emit_log( DNSKEY_TOO_SMALL_FOR_ALGO => $algo_args );
        }

        if ( $key->keysize < $rsa_key_size_details{$algo}{rec_size} ) {
            push @results, _emit_log( DNSKEY_SMALLER_THAN_REC => $algo_args );
        }

        if ( $key->keysize > $rsa_key_size_details{$algo}{max_size} ) {
            push @results, _emit_log( DNSKEY_TOO_LARGE_FOR_ALGO => $algo_args );
        }

        $investigated_keys{$key_ref} = 1;

    } ## end foreach my $key ( @keys )

    if ( scalar @dnskey_rrs and scalar @results == scalar grep { $_->tag eq 'NO_RESPONSE' } @results) {
        push @results, _emit_log( KEY_SIZE_OK => {} );
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec14

=over

=item dnssec15()

    my @logentry_array = dnssec15( $zone );

Runs the L<DNSSEC15 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec15.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec15 {
    my ( $class, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC15';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );

    my @query_types = qw{CDS CDNSKEY};
    my %cds_rrsets;
    my %cdnskey_rrsets;
    my %mismatch_cds_cdnskey;
    my %has_cds_no_cdnskey;
    my %has_cdnskey_no_cds;
    my %has_cds_and_cdnskey;

    my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
    my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
    my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;
    my %ip_already_processed;

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, @query_types ) ) {
            next;
        }

        my $cds_p = $ns->query( $zone->name, q{CDS}, { dnssec => 1, usevc => 0 } );
        if ( not $cds_p ) {
            next;
        }
        if ( not $cds_p->aa ) {
            next;
        }
        if ( $cds_p->rcode ne q{NOERROR} ) {
            next;
        }
        my @cds_records = $cds_p->get_records( q{CDS}, q{answer} );
        push @{ $cds_rrsets{ $ns->address->short } }, @cds_records;

        my $cdnskey_p = $ns->query( $zone->name, q{CDNSKEY}, { dnssec => 1, usevc => 0 } );
        if ( not $cdnskey_p ) {
            next;
        }
        if ( not $cdnskey_p->aa ) {
            next;
        }
        if ( $cdnskey_p->rcode ne q{NOERROR} ) {
            next;
        }
        my @cdnskey_records = $cdnskey_p->get_records( q{CDNSKEY}, q{answer} );
        push @{ $cdnskey_rrsets{ $ns->address->short } }, @cdnskey_records;
    }
    undef %ip_already_processed;

    my $no_cds_cdnskey = 1;
    for my $ns_ip ( keys %cds_rrsets ) {
        if ( scalar @{ $cds_rrsets{ $ns_ip } } ) {
            $no_cds_cdnskey = 0;
        }
    }
    for my $ns_ip ( keys %cdnskey_rrsets ) {
        if ( scalar @{ $cdnskey_rrsets{ $ns_ip } } ) {
            $no_cds_cdnskey = 0;
        }
    }

    if ( $no_cds_cdnskey ) {
        push @results, _emit_log( DS15_NO_CDS_CDNSKEY => {} );
    }
    else {
        for my $ns_ip ( keys %cds_rrsets ) {

            if ( not exists $cdnskey_rrsets{ $ns_ip } ) {
                next;
            }

            if (
                    scalar @{ $cds_rrsets{ $ns_ip } }
                and not scalar @{ $cdnskey_rrsets{ $ns_ip } }
              )
            {
                $has_cds_no_cdnskey{ $ns_ip } = 1;
            }
            elsif (
                    scalar @{ $cdnskey_rrsets{ $ns_ip } }
                and not scalar @{ $cds_rrsets{ $ns_ip } }
              )
            {
                $has_cdnskey_no_cds{ $ns_ip } = 1;
            }
            elsif (
                    scalar @{ $cds_rrsets{ $ns_ip } }
                and scalar @{ $cdnskey_rrsets{ $ns_ip } }
              )
            {
                $has_cds_and_cdnskey{ $ns_ip } = 1;
            }
        }

        for my $ns_ip ( keys %cds_rrsets ) {
            if (
                    scalar @{ $cds_rrsets{ $ns_ip } }
                and exists $cdnskey_rrsets{ $ns_ip }
                and scalar @{ $cdnskey_rrsets{ $ns_ip } }
              )
            {
                #
                # Quick hack. Proper fix should be available in LDNS 1.8.5: https://github.com/NLnetLabs/ldns/commit/b39813870a5fb0f4e8ff1570b3b09416aaee716c
                #
                my @dnskey;
                foreach my $cdnskey ( @{ $cdnskey_rrsets{ $ns_ip } } ) {
                    my $rr_string = $cdnskey->string;
                    $rr_string =~ s/\s+CDNSKEY\s+/ DNSKEY /;
                    push @dnskey, Zonemaster::LDNS::RR->new( $rr_string );
                }

                foreach my $cds ( @{ $cds_rrsets{ $ns_ip } } ) {
                    my @matching_keys = grep { $cds->keytag == $_->keytag or ($cds->algorithm == 0 and $_->algorithm == 0)} @dnskey;
                    if ( not scalar @matching_keys ) {
                        $mismatch_cds_cdnskey{ $ns_ip } = 1;
                    }
                }

                foreach my $dnskey ( @dnskey ) {
                    my @matching_keys = grep { $dnskey->keytag == $_->keytag or ($dnskey->algorithm == 0 and $_->algorithm == 0)} @{ $cds_rrsets{ $ns_ip } };
                    if ( not scalar @matching_keys ) {
                        $mismatch_cds_cdnskey{ $ns_ip } = 1;
                    }
                }
            }
        }

        if ( scalar keys %has_cds_no_cdnskey ) {
            push @results,
              _emit_log(
                DS15_HAS_CDS_NO_CDNSKEY => {
                    ns_ip_list => join( q{;}, sort keys %has_cds_no_cdnskey )
                }
              );
        }

        if ( scalar keys %has_cdnskey_no_cds ) {
            push @results,
              _emit_log(
                DS15_HAS_CDNSKEY_NO_CDS => {
                    ns_ip_list => join( q{;}, sort keys %has_cdnskey_no_cds )
                }
              );
        }

        if ( scalar keys %has_cds_and_cdnskey ) {
            push @results,
              _emit_log(
                DS15_HAS_CDS_AND_CDNSKEY => {
                    ns_ip_list => join( q{;}, sort keys %has_cds_and_cdnskey )
                }
              );
        }

        my $first = 1;
        my $first_rrlist;
        my $inconsistent_rrset = 0;
        for my $ns_ip ( keys %cds_rrsets ) {
            if ( $first ) {
                $first_rrlist = Zonemaster::LDNS::RRList->new( $cds_rrsets{ $ns_ip } );
                $first = 0;
                next;
            }

            my $rrlist = Zonemaster::LDNS::RRList->new( $cds_rrsets{ $ns_ip } );

            if ( $rrlist ne $first_rrlist ) {
                $inconsistent_rrset = 1;
                last;
            }
        }

        push @results, _emit_log( DS15_INCONSISTENT_CDS => {} ) if $inconsistent_rrset;

        $first = 1;
        $inconsistent_rrset = 0;
        for my $ns_ip ( keys %cdnskey_rrsets ) {
            if ( $first ) {
                $first_rrlist = Zonemaster::LDNS::RRList->new( $cdnskey_rrsets{ $ns_ip } );
                $first = 0;
                next;
            }

            my $rrlist = Zonemaster::LDNS::RRList->new( $cdnskey_rrsets{ $ns_ip } );

            if ( $rrlist ne $first_rrlist ) {
                $inconsistent_rrset = 1;
                last;
            }
        }

        push @results, _emit_log( DS15_INCONSISTENT_CDNSKEY => {} ) if $inconsistent_rrset;

        if ( scalar keys %mismatch_cds_cdnskey ) {
            push @results,
              _emit_log(
                DS15_MISMATCH_CDS_CDNSKEY => {
                    ns_ip_list => join( q{;}, sort keys %mismatch_cds_cdnskey )
                }
              );
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec15

=over

=item dnssec16()

    my @logentry_array = dnssec16( $zone );

Runs the L<DNSSEC16 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec16.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec16 {
    my ( $class, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC16';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );
    my @query_types = qw{CDS DNSKEY};
    my %cds_rrsets;
    my %dnskey_rrsets;
    my %no_dnskey_rrset;
    my %mixed_delete_cds;
    my %delete_cds;
    my %no_match_cds_with_dnskey;
    my %cds_points_to_non_zone_dnskey;
    my %cds_points_to_non_sep_dnskey;
    my %dnskey_not_signed_by_cds;
    my %cds_not_signed_by_cds;
    my %cds_not_signed;
    my %cds_signed_by_unknown_dnskey;
    my %cds_invalid_rrsig;
    my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
    my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
    my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;
    my %ip_already_processed;
    my $testing_time = time;

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, @query_types ) ) {
            next;
        }

        my $cds_p = $ns->query( $zone->name, q{CDS}, { dnssec => 1, usevc => 0 } );
        if ( not $cds_p ) {
            next;
        }
        if ( not $cds_p->aa ) {
            next;
        }
        if ( $cds_p->rcode ne q{NOERROR} ) {
            next;
        }
        my @cds_records = $cds_p->get_records( q{CDS}, q{answer} );
        if ( not scalar @cds_records ) {
            next;
        }
        my @cds_rrsig_records = $cds_p->get_records( q{RRSIG} , q{answer} );
        push @{ $cds_rrsets{ $ns->address->short }{cds} }, @cds_records;
        push @{ $cds_rrsets{ $ns->address->short }{rrsig} }, @cds_rrsig_records;
        foreach my $cds ( @{ $cds_rrsets{ $ns->address->short }{cds} } ) {
            my $rr_string = $cds->string;
            $rr_string =~ s/\s+CDS\s+/ DS /;
            push @{ $cds_rrsets{ $ns->address->short }{ds} }, Zonemaster::LDNS::RR->new( $rr_string );
        }

        my $dnskey_p = $ns->query( $zone->name, q{DNSKEY}, { dnssec => 1, usevc => 0 } );
        if ( not $dnskey_p ) {
            next;
        }
        if ( not $dnskey_p->aa ) {
            next;
        }
        if ( $dnskey_p->rcode ne q{NOERROR} ) {
            next;
        }
        my @dnskey_records = $dnskey_p->get_records( q{DNSKEY}, q{answer} );
        if ( not scalar @dnskey_records ) {
            next;
        }
        my @dnskey_rrsig_records = $dnskey_p->get_records( q{RRSIG} , q{answer} );
        push @{ $dnskey_rrsets{ $ns->address->short }{dnskey} }, @dnskey_records;
        push @{ $dnskey_rrsets{ $ns->address->short }{rrsig} }, @dnskey_rrsig_records;
        $testing_time = $dnskey_p->timestamp;

    }
    undef %ip_already_processed;

    if ( scalar keys %cds_rrsets ) {
        for my $ns_ip ( keys %cds_rrsets ) {
            if ( not scalar @{ $cds_rrsets{ $ns_ip }{cds} } ) {
                next;
            }
            if ( scalar grep { $_->algorithm == 0 } @{ $cds_rrsets{ $ns_ip }{ds} } ) {
                if ( scalar grep { $_->algorithm != 0 } @{ $cds_rrsets{ $ns_ip }{ds} } ) {
                    $mixed_delete_cds{ $ns_ip } = 1;
                }
                else {
                    $delete_cds{ $ns_ip } = 1;
                }
                next;
            }
            if ( not defined $dnskey_rrsets{ $ns_ip }{dnskey} or not scalar @{ $dnskey_rrsets{ $ns_ip }{dnskey} } ) {
                $no_dnskey_rrset{ $ns_ip } = 1;
                next;
            }
            foreach my $ds ( @{ $cds_rrsets{ $ns_ip }{ds} } ) {
                next if $ds->algorithm == 0;
                if ( not scalar grep { $ds->keytag == $_->keytag } @{ $dnskey_rrsets{ $ns_ip }{dnskey} } ) {
                    push @{ $no_match_cds_with_dnskey{ $ds->keytag } }, $ns_ip;
                }
                elsif ( scalar grep { $ds->keytag == $_->keytag and not $_->flags & 256 } @{ $dnskey_rrsets{ $ns_ip }{dnskey} } ) {
                    push @{ $cds_points_to_non_zone_dnskey{ $ds->keytag } }, $ns_ip;
                }
                else {
                    if ( not scalar grep { $ds->keytag == $_->keytag } @{ $dnskey_rrsets{ $ns_ip }{rrsig} } ) {
                        push @{ $dnskey_not_signed_by_cds{ $ds->keytag } }, $ns_ip;
                    }
                    if ( not scalar grep { $ds->keytag == $_->keytag } @{ $cds_rrsets{ $ns_ip }{rrsig} } ) {
                        push @{ $cds_not_signed_by_cds{ $ds->keytag } }, $ns_ip;
                    }
                    if ( scalar grep { $ds->keytag == $_->keytag and not $_->flags & 1 } @{ $dnskey_rrsets{ $ns_ip }{dnskey} } ) {
                        push @{ $cds_points_to_non_sep_dnskey{ $ds->keytag } }, $ns_ip;
                    }
                }
            }
            if ( not scalar @{ $cds_rrsets{ $ns_ip }{rrsig} } ) {
                $cds_not_signed{ $ns_ip } = 1;
            }
            else {
                foreach my $rrsig ( @{ $cds_rrsets{ $ns_ip }{rrsig} } ) {
                    my $msg = q{};
                    my @matching_dnskeys = grep { $rrsig->keytag == $_->keytag } @{ $dnskey_rrsets{ $ns_ip }{dnskey} };
                    if ( not scalar @matching_dnskeys ) {
                        push @{ $cds_signed_by_unknown_dnskey{ $rrsig->keytag } }, $ns_ip;
                    }
                    elsif ( not $rrsig->verify_time( $cds_rrsets{ $ns_ip }{cds} , \@matching_dnskeys, $testing_time, $msg) ) {
                        push @{ $cds_invalid_rrsig{ $rrsig->keytag } }, $ns_ip;
                    }
                }
            }
        }

        if ( scalar keys %no_dnskey_rrset ) {
            push @results,
              _emit_log(
                DS16_CDS_WITHOUT_DNSKEY => {
                    ns_ip_list => join( q{;}, sort keys %no_dnskey_rrset )
                }
              );
        }

        if ( scalar keys %mixed_delete_cds ) {
            push @results,
              _emit_log(
                DS16_MIXED_DELETE_CDS => {
                    ns_ip_list => join( q{;}, sort keys %mixed_delete_cds )
                }
              );
        }

        if ( scalar keys %delete_cds ) {
            push @results,
              _emit_log(
                DS16_DELETE_CDS => {
                    ns_ip_list => join( q{;}, sort keys %delete_cds )
                }
              );
        }

        if ( scalar keys %no_match_cds_with_dnskey ) {
            push @results, map {
              _emit_log(
                DS16_CDS_MATCHES_NO_DNSKEY => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $no_match_cds_with_dnskey{ $_ } } )
                }
              )
            } keys %no_match_cds_with_dnskey;
        }

        if ( scalar keys %cds_points_to_non_zone_dnskey ) {
            push @results, map {
              _emit_log(
                DS16_CDS_MATCHES_NON_ZONE_DNSKEY => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cds_points_to_non_zone_dnskey{ $_ } } )
                }
              )
            } keys %cds_points_to_non_zone_dnskey;
        }

        if ( scalar keys %cds_points_to_non_sep_dnskey ) {
            push @results, map {
              _emit_log(
                DS16_CDS_MATCHES_NON_SEP_DNSKEY => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cds_points_to_non_sep_dnskey{ $_ } } )
                }
              )
            } keys %cds_points_to_non_sep_dnskey;
        }

        if ( scalar keys %dnskey_not_signed_by_cds ) {
            push @results, map {
              _emit_log(
                DS16_DNSKEY_NOT_SIGNED_BY_CDS => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $dnskey_not_signed_by_cds{ $_ } } )
                }
              )
            } keys %dnskey_not_signed_by_cds;
        }

        if ( scalar keys %cds_not_signed_by_cds ) {
            push @results, map {
              _emit_log(
                DS16_CDS_NOT_SIGNED_BY_CDS => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cds_not_signed_by_cds{ $_ } } )
                }
              )
            } keys %cds_not_signed_by_cds;
        }

        if ( scalar keys %cds_invalid_rrsig ) {
            push @results, map {
              _emit_log(
                DS16_CDS_INVALID_RRSIG => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cds_invalid_rrsig{ $_ } } )
                }
              )
            } keys %cds_invalid_rrsig;
        }

        if ( scalar keys %cds_not_signed ) {
            push @results,
              _emit_log(
                DS16_CDS_UNSIGNED => {
                    ns_ip_list => join( q{;}, sort keys %cds_not_signed )
                }
              );
        }

        if ( scalar keys %cds_signed_by_unknown_dnskey ) {
            push @results, map {
              _emit_log(
                DS16_CDS_SIGNED_BY_UNKNOWN_DNSKEY => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cds_signed_by_unknown_dnskey{ $_ } } )
                }
              )
            } keys %cds_signed_by_unknown_dnskey;
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec16

=over

=item dnssec17()

    my @logentry_array = dnssec17( $zone );

Runs the L<DNSSEC17 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec17.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec17 {
    my ( $class, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC17';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );
    my @query_types = qw{CDNSKEY DNSKEY};
    my %cdnskey_rrsets;
    my %dnskey_rrsets;
    my %no_dnskey_rrset;
    my %mixed_delete_cdnskey;
    my %cdnskey_is_non_zone_key;
    my %cdnskey_is_non_sep_key;
    my %delete_cdnskey;
    my %no_match_cdnskey_with_dnskey;
    my %dnskey_not_signed_by_cdnskey;
    my %cdnskey_not_signed_by_cdnskey;
    my %cdnskey_not_signed;
    my %cdnskey_signed_by_unknown_dnskey;
    my %cdnskey_invalid_rrsig;
    my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
    my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
    my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;
    my %ip_already_processed;
    my $testing_time = time;

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, @query_types ) ) {
            next;
        }

        my $cdnskey_p = $ns->query( $zone->name, q{CDNSKEY}, { dnssec => 1, usevc => 0 } );
        if ( not $cdnskey_p ) {
            next;
        }
        if ( not $cdnskey_p->aa ) {
            next;
        }
        if ( $cdnskey_p->rcode ne q{NOERROR} ) {
            next;
        }
        my @cdnskey_records = $cdnskey_p->get_records( q{CDNSKEY}, q{answer} );
        if ( not scalar @cdnskey_records ) {
            next;
        }
        my @cdnskey_rrsig_records = $cdnskey_p->get_records( q{RRSIG} , q{answer} );
        push @{ $cdnskey_rrsets{ $ns->address->short }{cdnskey} }, @cdnskey_records;
        push @{ $cdnskey_rrsets{ $ns->address->short }{rrsig} }, @cdnskey_rrsig_records;
        foreach my $cdnskey ( @{ $cdnskey_rrsets{ $ns->address->short }{cdnskey} } ) {
            my $rr_string = $cdnskey->string;
            $rr_string =~ s/\s+CDNSKEY\s+/ DNSKEY /;
            push @{ $cdnskey_rrsets{ $ns->address->short }{dnskey} }, Zonemaster::LDNS::RR->new( $rr_string );
        }

        my $dnskey_p = $ns->query( $zone->name, q{DNSKEY}, { dnssec => 1, usevc => 0 } );
        if ( not $dnskey_p ) {
            next;
        }
        if ( not $dnskey_p->aa ) {
            next;
        }
        if ( $dnskey_p->rcode ne q{NOERROR} ) {
            next;
        }
        my @dnskey_records = $dnskey_p->get_records( q{DNSKEY}, q{answer} );
        if ( not scalar @dnskey_records ) {
            next;
        }
        my @dnskey_rrsig_records = $dnskey_p->get_records( q{RRSIG} , q{answer} );
        push @{ $dnskey_rrsets{ $ns->address->short }{dnskey} }, @dnskey_records;
        push @{ $dnskey_rrsets{ $ns->address->short }{rrsig} }, @dnskey_rrsig_records;
        $testing_time = $dnskey_p->timestamp;

    }
    undef %ip_already_processed;

    if ( scalar keys %cdnskey_rrsets ) {
        for my $ns_ip ( keys %cdnskey_rrsets ) {
            if ( not scalar @{ $cdnskey_rrsets{ $ns_ip }{cdnskey} } ) {
                next;
            }
            if ( scalar grep { $_->algorithm == 0 } @{ $cdnskey_rrsets{ $ns_ip }{dnskey} } ) {
                if ( scalar grep { $_->algorithm != 0 } @{ $cdnskey_rrsets{ $ns_ip }{dnskey} } ) {
                    $mixed_delete_cdnskey{ $ns_ip } = 1;
                }
                else {
                    $delete_cdnskey{ $ns_ip } = 1;
                }
                next;
            }
            if ( not defined $dnskey_rrsets{ $ns_ip }{dnskey} or not scalar @{ $dnskey_rrsets{ $ns_ip }{dnskey} } ) {
                $no_dnskey_rrset{ $ns_ip } = 1;
                next;
            }
            foreach my $dnskey ( @{ $cdnskey_rrsets{ $ns_ip }{dnskey} } ) {
                next if $dnskey->algorithm == 0;
                if ( not $dnskey->flags & 256 ) {
                    push @{ $cdnskey_is_non_zone_key{ $dnskey->keytag } }, $ns_ip;
                }
                else {
                    if ( not $dnskey->flags & 1 ) {
                        push @{ $cdnskey_is_non_sep_key{ $dnskey->keytag } }, $ns_ip;
                    }
                    if ( not scalar grep { $dnskey->keytag == $_->keytag } @{ $dnskey_rrsets{ $ns_ip }{dnskey} } ) {
                        push @{ $no_match_cdnskey_with_dnskey{ $dnskey->keytag } }, $ns_ip;
                    }
                    else {
                        if ( not scalar grep { $dnskey->keytag == $_->keytag } @{ $dnskey_rrsets{ $ns_ip }{rrsig} } ) {
                            push @{ $dnskey_not_signed_by_cdnskey{ $dnskey->keytag } }, $ns_ip;
                        }
                        if ( not scalar grep { $dnskey->keytag == $_->keytag } @{ $cdnskey_rrsets{ $ns_ip }{rrsig} } ) {
                            push @{ $cdnskey_not_signed_by_cdnskey{ $dnskey->keytag } }, $ns_ip;
                        }
                    }
                }
            }
            if ( not scalar @{ $cdnskey_rrsets{ $ns_ip }{rrsig} } ) {
                $cdnskey_not_signed{ $ns_ip } = 1;
            }
            else {
                foreach my $rrsig ( @{ $cdnskey_rrsets{ $ns_ip }{rrsig} } ) {
                    my $msg = q{};
                    my @matching_dnskeys = grep { $rrsig->keytag == $_->keytag } @{ $dnskey_rrsets{ $ns_ip }{dnskey} };
                    if ( not scalar @matching_dnskeys ) {
                        push @{ $cdnskey_signed_by_unknown_dnskey{ $rrsig->keytag } }, $ns_ip;
                    }
                    elsif ( not $rrsig->verify_time( $cdnskey_rrsets{ $ns_ip }{cdnskey} , \@matching_dnskeys, $testing_time, $msg) ) {
                        push @{ $cdnskey_invalid_rrsig{ $rrsig->keytag } }, $ns_ip;
                    }
                }
            }
        }

        if ( scalar keys %no_dnskey_rrset ) {
            push @results,
              _emit_log(
                DS17_CDNSKEY_WITHOUT_DNSKEY => {
                    ns_ip_list => join( q{;}, sort keys %no_dnskey_rrset )
                }
              );
        }

        if ( scalar keys %mixed_delete_cdnskey ) {
            push @results,
              _emit_log(
                DS17_MIXED_DELETE_CDNSKEY => {
                    ns_ip_list => join( q{;}, sort keys %mixed_delete_cdnskey )
                }
              );
        }

        if ( scalar keys %delete_cdnskey ) {
            push @results,
              _emit_log(
                DS17_DELETE_CDNSKEY => {
                    ns_ip_list => join( q{;}, sort keys %delete_cdnskey )
                }
              );
        }

        if ( scalar keys %no_match_cdnskey_with_dnskey ) {
            push @results, map {
              _emit_log(
                DS17_CDNSKEY_MATCHES_NO_DNSKEY => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $no_match_cdnskey_with_dnskey{ $_ } } )
                }
              )
            } keys %no_match_cdnskey_with_dnskey;
        }

        if ( scalar keys %cdnskey_is_non_zone_key ) {
            push @results, map {
              _emit_log(
                DS17_CDNSKEY_IS_NON_ZONE => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cdnskey_is_non_zone_key{ $_ } } )
                }
              )
            } keys %cdnskey_is_non_zone_key;
        }


        if ( scalar keys %cdnskey_is_non_sep_key ) {
            push @results, map {
              _emit_log(
                DS17_CDNSKEY_IS_NON_SEP => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cdnskey_is_non_sep_key{ $_ } } )
                }
              )
            } keys %cdnskey_is_non_sep_key;
        }

        if ( scalar keys %dnskey_not_signed_by_cdnskey ) {
            push @results, map {
              _emit_log(
                DS17_DNSKEY_NOT_SIGNED_BY_CDNSKEY => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $dnskey_not_signed_by_cdnskey{ $_ } } )
                }
              )
            } keys %dnskey_not_signed_by_cdnskey;
        }

        if ( scalar keys %cdnskey_not_signed_by_cdnskey ) {
            push @results, map {
              _emit_log(
                DS17_CDNSKEY_NOT_SIGNED_BY_CDNSKEY => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cdnskey_not_signed_by_cdnskey{ $_ } } )
                }
              )
            } keys %cdnskey_not_signed_by_cdnskey;
        }

        if ( scalar keys %cdnskey_invalid_rrsig ) {
            push @results, map {
              _emit_log(
                DS17_CDNSKEY_INVALID_RRSIG => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cdnskey_invalid_rrsig{ $_ } } )
                }
              )
            } keys %cdnskey_invalid_rrsig;
        }

        if ( scalar keys %cdnskey_not_signed ) {
            push @results,
              _emit_log(
                DS17_CDNSKEY_UNSIGNED => {
                    ns_ip_list => join( q{;}, sort keys %cdnskey_not_signed )
                }
              );
        }

        if ( scalar keys %cdnskey_signed_by_unknown_dnskey ) {
            push @results, map {
              _emit_log(
                DS17_CDNSKEY_SIGNED_BY_UNKNOWN_DNSKEY => {
                    keytag     => $_,
                    ns_ip_list => join( q{;}, uniq sort @{ $cdnskey_signed_by_unknown_dnskey{ $_ } } )
                }
              )
            } keys %cdnskey_signed_by_unknown_dnskey;
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec17

=over

=item dnssec18()

    my @logentry_array = dnssec18( $zone );

Runs the L<DNSSEC18 Test Case|https://github.com/zonemaster/zonemaster/blob/master/docs/public/specifications/tests/DNSSEC-TP/dnssec18.md>.

Takes a L<Zonemaster::Engine::Zone> object.

Returns a list of L<Zonemaster::Engine::Logger::Entry> objects.

=back

=cut

sub dnssec18 {
    my ( $class, $zone ) = @_;

    local $Zonemaster::Engine::Logger::TEST_CASE_NAME = 'DNSSEC18';
    push my @results, _emit_log( TEST_CASE_START => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } );
    my %cds_rrsets;
    my %cdnskey_rrsets;
    my %dnskey_rrsets;
    my @ds_records;
    my %ds_no_match_cds_rrsig;
    my %ds_no_match_cdnskey_rrsig;
    my $continue_with_child_tests = 1;

    my $parent     = Zonemaster::Engine::TestMethods->method1( $zone );
    my @nss_parent = @{ $parent->ns };
    my %nss        = map { $_->name->string . '/' . $_->address->short => $_ } @nss_parent;
    my %ip_already_processed;

    for my $nss_key ( sort keys %nss ) {
        my $ns = $nss{$nss_key};

        next if exists $ip_already_processed{$ns->address->short};
        $ip_already_processed{$ns->address->short} = 1;

        if ( _ip_disabled_message( \@results, $ns, q{DS} ) ) {
            next;
        }

        my $ds_p = $ns->query( $zone->name, q{DS}, { dnssec => 1, usevc => 0 } );
        if ( not $ds_p or $ds_p->rcode ne q{NOERROR} or not $ds_p->aa ) {
            next;
        }
        my @tmp_ds_records = $ds_p->get_records_for_name( q{DS}, $zone->name->string, q{answer} );
        if ( not scalar @tmp_ds_records ) {
            next;
        }
        foreach my $tmp_ds_record ( @tmp_ds_records ) {
            if (
                not grep {
                          $tmp_ds_record->keytag == $_->keytag
                      and $tmp_ds_record->digtype == $_->digtype
                      and $tmp_ds_record->algorithm == $_->algorithm
                      and $tmp_ds_record->hexdigest eq $_->hexdigest
                } @ds_records
              )
            {
                push @ds_records, $tmp_ds_record;
            }
        }
    }
    undef %ip_already_processed;

    if ( not scalar @ds_records ) {
        $continue_with_child_tests = 0;
    }

    if ( $continue_with_child_tests ) {

        my @query_types = qw{CDNSKEY CDS DNSKEY};
        my @nss_del   = @{ Zonemaster::Engine::TestMethods->method4( $zone ) };
        my @nss_child = @{ Zonemaster::Engine::TestMethods->method5( $zone ) };
        my %nss       = map { $_->name->string . '/' . $_->address->short => $_ } @nss_del, @nss_child;
        my %ip_already_processed;

        for my $nss_key ( sort keys %nss ) {
            my $ns = $nss{$nss_key};

            next if exists $ip_already_processed{$ns->address->short};
            $ip_already_processed{$ns->address->short} = 1;

            if ( _ip_disabled_message( \@results, $ns, @query_types ) ) {
                next;
            }

            my $cds_p = $ns->query( $zone->name, q{CDS}, { dnssec => 1, usevc => 0 } );
            if ( not $cds_p or not $cds_p->aa or $cds_p->rcode ne q{NOERROR} ) {
                next;
            }
            my @cds_records = $cds_p->get_records( q{CDS}, q{answer} );
            if ( scalar @cds_records ) {
                my @cds_rrsig_records = $cds_p->get_records( q{RRSIG} , q{answer} );
                push @{ $cds_rrsets{ $ns->address->short }{cds} }, @cds_records;
                push @{ $cds_rrsets{ $ns->address->short }{rrsig} }, @cds_rrsig_records;
                foreach my $cds ( @{ $cds_rrsets{ $ns->address->short }{cds} } ) {
                    my $rr_string = $cds->string;
                    $rr_string =~ s/\s+CDS\s+/ DS /;
                    push @{ $cds_rrsets{ $ns->address->short }{ds} }, Zonemaster::LDNS::RR->new( $rr_string );
                }
            }

            my $cdnskey_p = $ns->query( $zone->name, q{CDNSKEY}, { dnssec => 1, usevc => 0 } );
            if ( not $cdnskey_p or not $cdnskey_p->aa or $cdnskey_p->rcode ne q{NOERROR} ) {
                next;
            }
            my @cdnskey_records = $cdnskey_p->get_records( q{CDNSKEY}, q{answer} );
            if ( scalar @cdnskey_records ) {
                my @cdnskey_rrsig_records = $cdnskey_p->get_records( q{RRSIG} , q{answer} );
                push @{ $cdnskey_rrsets{ $ns->address->short }{cdnskey} }, @cdnskey_records;
                push @{ $cdnskey_rrsets{ $ns->address->short }{rrsig} }, @cdnskey_rrsig_records;
                foreach my $cdnskey ( @{ $cdnskey_rrsets{ $ns->address->short }{cdnskey} } ) {
                    my $rr_string = $cdnskey->string;
                    $rr_string =~ s/\s+CDNSKEY\s+/ DNSKEY /;
                    push @{ $cdnskey_rrsets{ $ns->address->short }{dnskey} }, Zonemaster::LDNS::RR->new( $rr_string );
                }
            }

            my $dnskey_p = $ns->query( $zone->name, q{DNSKEY}, { dnssec => 1, usevc => 0 } );
            if ( not $dnskey_p or $dnskey_p->rcode ne q{NOERROR} or not $dnskey_p->aa ) {
                next;
            }
            my @dnskey_records = $dnskey_p->get_records( q{DNSKEY}, q{answer} );
            if ( scalar @dnskey_records ) {
                push @{ $dnskey_rrsets{ $ns->address->short }{dnskey} }, @dnskey_records;
            }
        }
        undef %ip_already_processed;

        if (    not( not scalar keys %cds_rrsets and not scalar keys %cdnskey_rrsets )
            and not( not scalar keys %dnskey_rrsets ) )
        {
            for my $ns_ip ( keys %cds_rrsets ) {
                my (@rrsig, @dnskey);
                push @rrsig, @{ $cds_rrsets{ $ns_ip }{rrsig} };
                push @dnskey, @{ $dnskey_rrsets{ $ns_ip }{dnskey} };
                my $match = 0;
                foreach my $ds ( @ds_records ) {
                    if ( not scalar grep { $ds->keytag == $_->keytag } @dnskey ) {
                        next;
                    }
                    elsif ( scalar grep { $ds->keytag == $_->keytag } @rrsig ) {
                        $match = 1;
                        last;
                    }
                }
                if ( not $match ) {
                    $ds_no_match_cds_rrsig{ $ns_ip } = 1;
                }
            }
            for my $ns_ip ( keys %cdnskey_rrsets ) {
                my (@rrsig, @dnskey);
                push @rrsig, @{ $cdnskey_rrsets{ $ns_ip }{rrsig} };
                push @dnskey, @{ $dnskey_rrsets{ $ns_ip }{dnskey} };
                my $match = 0;
                foreach my $ds ( @ds_records ) {
                    if ( not scalar grep { $ds->keytag == $_->keytag } @dnskey ) {
                        next;
                    }
                    elsif ( scalar grep { $ds->keytag == $_->keytag } @rrsig ) {
                        $match = 1;
                        last;
                    }
                }
                if ( not $match ) {
                    $ds_no_match_cdnskey_rrsig{ $ns_ip } = 1;
                }
            }
            if ( scalar keys %ds_no_match_cds_rrsig ) {
                push @results,
                  _emit_log(
                     DS18_NO_MATCH_CDS_RRSIG_DS => {
                        ns_ip_list => join( q{;}, sort keys %ds_no_match_cds_rrsig )
                    }
                  );
            }
            if ( scalar keys %ds_no_match_cdnskey_rrsig ) {
                push @results,
                  _emit_log(
                     DS18_NO_MATCH_CDNSKEY_RRSIG_DS => {
                        ns_ip_list => join( q{;}, sort keys %ds_no_match_cdnskey_rrsig )
                    }
                  );
            }
        }
    }

    return ( @results, _emit_log( TEST_CASE_END => { testcase => $Zonemaster::Engine::Logger::TEST_CASE_NAME } ) );
} ## end sub dnssec18

1;
