diff --git a/doc/configuration.rst b/doc/configuration.rst index 60ebbbf4ece8e931b58262327265d4a5eb3c4c13..ebd085e1f362b954735267f2775c894eefb6fe6e 100644 --- a/doc/configuration.rst +++ b/doc/configuration.rst @@ -271,8 +271,6 @@ The signing keys can be generated using ISC ``dnssec-keygen`` tool only and there are some limitations: * Keys for all zones must be placed in one directory. -* Algorithms based on RSA, DSA, and ECDSA are supported, support for - GOST algorithm is not finished yet. * Only key publication, activation, inactivation, and removal time stamps are utilized. Other time stamps are ignored. * It is required, that both ``.private`` and ``.key`` files for each @@ -280,6 +278,7 @@ only and there are some limitations: (even for verification only). * There cannot be more than eight keys per zone. Keys which are not published are not included in this number. +* Single-Type Signing Scheme is not supported. Example how to generate NSEC3 capable zone signing key (ZSK) and key signing key (KSK) for zone ``example.com``:: diff --git a/src/knot/dnssec/zone-keys.c b/src/knot/dnssec/zone-keys.c index 94284334edd9457b39e519ab72abe689c95e1808..6988c495c3d2d14b934446e04d2f1c438ab3e1ac 100644 --- a/src/knot/dnssec/zone-keys.c +++ b/src/knot/dnssec/zone-keys.c @@ -106,6 +106,65 @@ static void set_zone_key_flags(const knot_key_params_t *params, (params->time_delete == 0 || now < params->time_delete); } +/*! + * \brief Check if there is a functional KSK and ZSK for each used algorithm. + */ +static int check_keys_validity(const knot_zone_keys_t *keys) +{ + assert(keys); + + const int MAX_ALGORITHMS = KNOT_DNSSEC_ALG_ECDSAP384SHA384 + 1; + struct { + bool published; + bool ksk_enabled; + bool zsk_enabled; + } algorithms[MAX_ALGORITHMS]; + memset(algorithms, 0, sizeof(algorithms)); + + /* Make a list of used algorithms */ + + const knot_zone_key_t *key = NULL; + WALK_LIST(key, keys->list) { + knot_dnssec_algorithm_t a = key->dnssec_key.algorithm; + assert(a < MAX_ALGORITHMS); + + if (key->is_public) { + // public key creates a requirement for an algorithm + algorithms[a].published = true; + + // need fully enabled ZSK and KSK for each algorithm + if (key->is_active) { + if (key->is_ksk) { + algorithms[a].ksk_enabled = true; + } else { + algorithms[a].zsk_enabled = true; + } + } + } + } + + /* Validate enabled algorithms */ + + int enabled_count = 0; + for (int a = 0; a < MAX_ALGORITHMS; a++) { + if (!algorithms[a].published) { + continue; + } + + if (!algorithms[a].ksk_enabled || !algorithms[a].zsk_enabled) { + return KNOT_DNSSEC_EMISSINGKEYTYPE; + } + + enabled_count += 1; + } + + if (enabled_count == 0) { + return KNOT_DNSSEC_ENOKEY; + } + + return KNOT_EOK; +} + /*! * \brief Load zone keys from a key directory. * @@ -230,8 +289,8 @@ int knot_load_zone_keys(const char *keydir_name, const knot_dname_t *zone_name, closedir(keydir); - if (result == KNOT_EOK && EMPTY_LIST(keys->list)) { - result = KNOT_DNSSEC_ENOKEY; + if (result == KNOT_EOK) { + result = check_keys_validity(keys); } if (result == KNOT_EOK) { diff --git a/src/libknot/errcode.c b/src/libknot/errcode.c index 5d738b472f8ddeda7dda20f1a883766a5b7601f0..cf2d809d3f9b8f4074a69404000172568abcab49 100644 --- a/src/libknot/errcode.c +++ b/src/libknot/errcode.c @@ -112,6 +112,7 @@ const error_table_t error_messages[] = { { KNOT_DNSSEC_ESIGN, "Cannot create the signature" }, { KNOT_DNSSEC_ENOKEY, "No keys for signing" }, { KNOT_DNSSEC_ENOKEYDIR, "Keydir does not exist" }, + { KNOT_DNSSEC_EMISSINGKEYTYPE, "Missing KSK or ZSK for used algorithm" }, /* NSEC3 errors. */ { KNOT_NSEC3_ECOMPUTE_HASH, "Cannot compute NSEC3 hash" }, diff --git a/src/libknot/errcode.h b/src/libknot/errcode.h index 838303940eee458452093a9922feaf1f55d2e046..9b64ea18e684274ebe30dc9cf3f8a77ee92f9232 100644 --- a/src/libknot/errcode.h +++ b/src/libknot/errcode.h @@ -128,6 +128,7 @@ enum knot_error { KNOT_DNSSEC_ESIGN, KNOT_DNSSEC_ENOKEY, KNOT_DNSSEC_ENOKEYDIR, + KNOT_DNSSEC_EMISSINGKEYTYPE, /* NSEC3 errors. */ KNOT_NSEC3_ECOMPUTE_HASH diff --git a/tests-extra/tests/dnssec/dnskey_algorithms/data/generate_keys.sh b/tests-extra/tests/dnssec/dnskey_algorithms/data/generate_keys.sh new file mode 100755 index 0000000000000000000000000000000000000000..cfd52b0ec02b745eeec14c7939865b0b4bd47844 --- /dev/null +++ b/tests-extra/tests/dnssec/dnskey_algorithms/data/generate_keys.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# +# Run this script every 50 years to refresh the keys. :-) +# + +set -xe + +TIME_PAST="-50y" +TIME_FUTURE="+50y" + +keygen() +{ + dnssec-keygen -r/dev/urandom $@ +} + +dir=$(pwd) +keydir=$(mktemp -d) + +pushd "$keydir" + +# +# valid scenarios +# + +keygen -a RSASHA256 -b 2048 -P $TIME_PAST -A $TIME_PAST -f KSK rsa_ok +keygen -a RSASHA256 -b 1024 -P $TIME_PAST -A $TIME_PAST rsa_ok + +keygen -a RSASHA256 -b 2048 -P $TIME_PAST -A $TIME_PAST rsa_ecdsa_ok +keygen -a RSASHA256 -b 1024 -P $TIME_PAST -A $TIME_PAST -f KSK rsa_ecdsa_ok +keygen -a ECDSAP256SHA256 -P $TIME_PAST -A $TIME_PAST rsa_ecdsa_ok +keygen -a ECDSAP256SHA256 -P $TIME_PAST -A $TIME_PAST -f KSK rsa_ecdsa_ok + +keygen -a RSASHA256 -b 2048 -P $TIME_PAST -A $TIME_PAST -f KSK rsa_ecdsa_roll_ok +keygen -a RSASHA256 -b 1024 -P $TIME_PAST -A $TIME_PAST rsa_ecdsa_roll_ok +keygen -a ECDSAP256SHA256 -P $TIME_FUTURE -A $TIME_PAST rsa_ecdsa_roll_ok + +# +# invalid scenarios +# + +keygen -a RSASHA256 -b 2048 -P $TIME_FUTURE -A $TIME_FUTURE -f KSK rsa_future_all +keygen -a RSASHA256 -b 1024 -P $TIME_FUTURE -A $TIME_FUTURE rsa_future_all + +keygen -a RSASHA512 -b 2048 -P $TIME_FUTURE -A $TIME_PAST -f KSK rsa_future_publish +keygen -a RSASHA256 -b 1024 -P $TIME_FUTURE -A $TIME_PAST rsa_future_publish + +keygen -a RSASHA512 -b 2048 -P $TIME_PAST -A $TIME_FUTURE -f KSK rsa_future_active +keygen -a RSASHA256 -b 1024 -P $TIME_PAST -A $TIME_FUTURE rsa_future_active + +keygen -a RSASHA256 -b 2048 -P $TIME_PAST -A $TIME_PAST -f KSK rsa_inactive_zsk +keygen -a RSASHA256 -b 1024 -P $TIME_PAST -A $TIME_FUTURE rsa_inactive_zsk + +keygen -a RSASHA256 -b 2048 -P $TIME_FUTURE -A $TIME_FUTURE -f KSK rsa_no_zsk +keygen -a RSASHA256 -b 1024 -P $TIME_PAST -A $TIME_PAST rsa_no_zsk + +keygen -a RSASHA256 -b 2048 -P $TIME_PAST -A $TIME_PAST -f KSK rsa_twice_ksk +keygen -a RSASHA256 -b 1024 -P $TIME_PAST -A $TIME_PAST -f KSK rsa_twice_ksk + +keygen -a RSASHA256 -b 2048 -P $TIME_PAST -A $TIME_PAST -f KSK rsa_ecdsa_ksk_only +keygen -a RSASHA256 -b 1024 -P $TIME_PAST -A $TIME_PAST rsa_ecdsa_ksk_only +keygen -a ECDSAP256SHA256 -P $TIME_PAST -A $TIME_PAST -f KSK rsa_ecdsa_ksk_only + +keygen -a RSASHA256 -b 2048 -P $TIME_PAST -A $TIME_PAST -f KSK rsa256_rsa512 +keygen -a RSASHA512 -b 2048 -P $TIME_PAST -A $TIME_PAST rsa256_rsa512 + +tar czf "$dir/keys.tgz" K*.{key,private} +popd +rm -rf "$keydir" diff --git a/tests-extra/tests/dnssec/dnskey_algorithms/data/keys.tgz b/tests-extra/tests/dnssec/dnskey_algorithms/data/keys.tgz new file mode 100644 index 0000000000000000000000000000000000000000..25aef643afd6f39ff8f00ea290eb7016b4300bac Binary files /dev/null and b/tests-extra/tests/dnssec/dnskey_algorithms/data/keys.tgz differ diff --git a/tests-extra/tests/dnssec/dnskey_algorithms/test.py b/tests-extra/tests/dnssec/dnskey_algorithms/test.py new file mode 100644 index 0000000000000000000000000000000000000000..20b81d4c4c10535f0cfb2c377971067a3fa8c736 --- /dev/null +++ b/tests-extra/tests/dnssec/dnskey_algorithms/test.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +""" +Validate ZSK and KSK constrains checks. +""" + +import tarfile +import os.path + +import dnstest.zonefile +from dnstest.test import Test + +TEST_CASES = { + # valid cases + "rsa_ok": True, + "rsa_ecdsa_ok": True, + "rsa_ecdsa_roll_ok": True, + # invalid cases + "rsa_future_all": False, + "rsa_future_publish": False, + "rsa_future_active": False, + "rsa_inactive_zsk": False, + "rsa_no_zsk": False, + "rsa_twice_ksk": False, + "rsa_ecdsa_ksk_only": False, + "rsa256_rsa512": False, +} + +t = Test() + +knot = t.server("knot") +knot.dnssec_enable = True + +# setup keys + +keys_archive = os.path.join(t.data_dir, "keys.tgz") +with tarfile.open(keys_archive, "r:*") as tar: + tar.extractall(knot.keydir) + +# setup zones + +zones = [] +for zone_name in TEST_CASES: + zone = dnstest.zonefile.ZoneFile(t.zones_dir) + zone.set_name(zone_name) + zone.gen_file(dnssec=False, nsec3=False, records=5) + zones.append(zone) + +t.link(zones, knot) + +t.start() + +for zone, valid in TEST_CASES.items(): + expected_rcode = "NOERROR" if valid else "SERVFAIL" + knot.dig(zone, "SOA").check(rcode=expected_rcode) + +t.end()