diff options
Diffstat (limited to '')
-rw-r--r-- | lib/dns/zoneverify.c | 2037 |
1 files changed, 2037 insertions, 0 deletions
diff --git a/lib/dns/zoneverify.c b/lib/dns/zoneverify.c new file mode 100644 index 0000000..d5c1537 --- /dev/null +++ b/lib/dns/zoneverify.c @@ -0,0 +1,2037 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +/*! \file */ + +#include <inttypes.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> + +#include <isc/base32.h> +#include <isc/buffer.h> +#include <isc/heap.h> +#include <isc/iterated_hash.h> +#include <isc/log.h> +#include <isc/mem.h> +#include <isc/region.h> +#include <isc/result.h> +#include <isc/types.h> +#include <isc/util.h> + +#include <dns/db.h> +#include <dns/dbiterator.h> +#include <dns/dnssec.h> +#include <dns/fixedname.h> +#include <dns/keytable.h> +#include <dns/keyvalues.h> +#include <dns/log.h> +#include <dns/name.h> +#include <dns/nsec.h> +#include <dns/nsec3.h> +#include <dns/rdata.h> +#include <dns/rdataset.h> +#include <dns/rdatasetiter.h> +#include <dns/rdatastruct.h> +#include <dns/rdatatype.h> +#include <dns/secalg.h> +#include <dns/types.h> +#include <dns/zone.h> +#include <dns/zoneverify.h> + +#include <dst/dst.h> + +typedef struct vctx { + isc_mem_t *mctx; + dns_zone_t *zone; + dns_db_t *db; + dns_dbversion_t *ver; + dns_name_t *origin; + dns_keytable_t *secroots; + bool goodksk; + bool goodzsk; + dns_rdataset_t keyset; + dns_rdataset_t keysigs; + dns_rdataset_t soaset; + dns_rdataset_t soasigs; + dns_rdataset_t nsecset; + dns_rdataset_t nsecsigs; + dns_rdataset_t nsec3paramset; + dns_rdataset_t nsec3paramsigs; + unsigned char revoked_ksk[256]; + unsigned char revoked_zsk[256]; + unsigned char standby_ksk[256]; + unsigned char standby_zsk[256]; + unsigned char ksk_algorithms[256]; + unsigned char zsk_algorithms[256]; + unsigned char bad_algorithms[256]; + unsigned char act_algorithms[256]; + isc_heap_t *expected_chains; + isc_heap_t *found_chains; +} vctx_t; + +struct nsec3_chain_fixed { + uint8_t hash; + uint8_t salt_length; + uint8_t next_length; + uint16_t iterations; + /* + * The following non-fixed-length data is stored in memory after the + * fields declared above for each NSEC3 chain element: + * + * unsigned char salt[salt_length]; + * unsigned char owner[next_length]; + * unsigned char next[next_length]; + */ +}; + +/* + * Helper function used to calculate length of variable-length + * data section in object pointed to by 'chain'. + */ +static size_t +chain_length(struct nsec3_chain_fixed *chain) { + return (chain->salt_length + 2 * chain->next_length); +} + +/*% + * Log a zone verification error described by 'fmt' and the variable arguments + * following it. Either use dns_zone_logv() or print to stderr, depending on + * whether the function was invoked from within named or by a standalone tool, + * respectively. + */ +static void +zoneverify_log_error(const vctx_t *vctx, const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + if (vctx->zone != NULL) { + dns_zone_logv(vctx->zone, DNS_LOGCATEGORY_GENERAL, + ISC_LOG_ERROR, NULL, fmt, ap); + } else { + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + } + va_end(ap); +} + +static bool +is_delegation(const vctx_t *vctx, const dns_name_t *name, dns_dbnode_t *node, + uint32_t *ttlp) { + dns_rdataset_t nsset; + isc_result_t result; + + if (dns_name_equal(name, vctx->origin)) { + return (false); + } + + dns_rdataset_init(&nsset); + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_ns, 0, 0, &nsset, NULL); + if (dns_rdataset_isassociated(&nsset)) { + if (ttlp != NULL) { + *ttlp = nsset.ttl; + } + dns_rdataset_disassociate(&nsset); + } + + return ((result == ISC_R_SUCCESS)); +} + +/*% + * Return true if version 'ver' of database 'db' contains a DNAME RRset at + * 'node'; return false otherwise. + */ +static bool +has_dname(const vctx_t *vctx, dns_dbnode_t *node) { + dns_rdataset_t dnameset; + isc_result_t result; + + dns_rdataset_init(&dnameset); + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_dname, 0, 0, &dnameset, + NULL); + if (dns_rdataset_isassociated(&dnameset)) { + dns_rdataset_disassociate(&dnameset); + } + + return ((result == ISC_R_SUCCESS)); +} + +static bool +goodsig(const vctx_t *vctx, dns_rdata_t *sigrdata, const dns_name_t *name, + dst_key_t **dstkeys, size_t nkeys, dns_rdataset_t *rdataset) { + dns_rdata_rrsig_t sig; + isc_result_t result; + + result = dns_rdata_tostruct(sigrdata, &sig, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + for (size_t key = 0; key < nkeys; key++) { + if (sig.algorithm != dst_key_alg(dstkeys[key]) || + sig.keyid != dst_key_id(dstkeys[key]) || + !dns_name_equal(&sig.signer, vctx->origin)) + { + continue; + } + result = dns_dnssec_verify(name, rdataset, dstkeys[key], false, + 0, vctx->mctx, sigrdata, NULL); + if (result == ISC_R_SUCCESS || result == DNS_R_FROMWILDCARD) { + return (true); + } + } + return (false); +} + +static bool +nsec_bitmap_equal(dns_rdata_nsec_t *nsec, dns_rdata_t *rdata) { + isc_result_t result; + dns_rdata_nsec_t tmpnsec; + + result = dns_rdata_tostruct(rdata, &tmpnsec, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + if (nsec->len != tmpnsec.len || + memcmp(nsec->typebits, tmpnsec.typebits, nsec->len) != 0) + { + return (false); + } + return (true); +} + +static isc_result_t +verifynsec(const vctx_t *vctx, const dns_name_t *name, dns_dbnode_t *node, + const dns_name_t *nextname, isc_result_t *vresult) { + unsigned char buffer[DNS_NSEC_BUFFERSIZE]; + char namebuf[DNS_NAME_FORMATSIZE]; + char nextbuf[DNS_NAME_FORMATSIZE]; + char found[DNS_NAME_FORMATSIZE]; + dns_rdataset_t rdataset; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_t tmprdata = DNS_RDATA_INIT; + dns_rdata_nsec_t nsec; + isc_result_t result; + + dns_rdataset_init(&rdataset); + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_nsec, 0, 0, &rdataset, NULL); + if (result != ISC_R_SUCCESS) { + dns_name_format(name, namebuf, sizeof(namebuf)); + zoneverify_log_error(vctx, "Missing NSEC record for %s", + namebuf); + *vresult = ISC_R_FAILURE; + result = ISC_R_SUCCESS; + goto done; + } + + result = dns_rdataset_first(&rdataset); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_rdataset_first(): %s", + isc_result_totext(result)); + goto done; + } + + dns_rdataset_current(&rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &nsec, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + /* Check next name is consistent */ + if (!dns_name_equal(&nsec.next, nextname)) { + dns_name_format(name, namebuf, sizeof(namebuf)); + dns_name_format(nextname, nextbuf, sizeof(nextbuf)); + dns_name_format(&nsec.next, found, sizeof(found)); + zoneverify_log_error(vctx, + "Bad NSEC record for %s, next name " + "mismatch (expected:%s, found:%s)", + namebuf, nextbuf, found); + *vresult = ISC_R_FAILURE; + goto done; + } + + /* Check bit map is consistent */ + result = dns_nsec_buildrdata(vctx->db, vctx->ver, node, nextname, + buffer, &tmprdata); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_nsec_buildrdata(): %s", + isc_result_totext(result)); + goto done; + } + if (!nsec_bitmap_equal(&nsec, &tmprdata)) { + dns_name_format(name, namebuf, sizeof(namebuf)); + zoneverify_log_error(vctx, + "Bad NSEC record for %s, bit map " + "mismatch", + namebuf); + *vresult = ISC_R_FAILURE; + goto done; + } + + result = dns_rdataset_next(&rdataset); + if (result != ISC_R_NOMORE) { + dns_name_format(name, namebuf, sizeof(namebuf)); + zoneverify_log_error(vctx, "Multiple NSEC records for %s", + namebuf); + *vresult = ISC_R_FAILURE; + goto done; + } + + *vresult = ISC_R_SUCCESS; + result = ISC_R_SUCCESS; + +done: + if (dns_rdataset_isassociated(&rdataset)) { + dns_rdataset_disassociate(&rdataset); + } + + return (result); +} + +static isc_result_t +check_no_rrsig(const vctx_t *vctx, const dns_rdataset_t *rdataset, + const dns_name_t *name, dns_dbnode_t *node) { + char namebuf[DNS_NAME_FORMATSIZE]; + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + dns_rdataset_t sigrdataset; + dns_rdatasetiter_t *rdsiter = NULL; + isc_result_t result; + + dns_rdataset_init(&sigrdataset); + result = dns_db_allrdatasets(vctx->db, node, vctx->ver, 0, 0, &rdsiter); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_db_allrdatasets(): %s", + isc_result_totext(result)); + return (result); + } + for (result = dns_rdatasetiter_first(rdsiter); result == ISC_R_SUCCESS; + result = dns_rdatasetiter_next(rdsiter)) + { + dns_rdatasetiter_current(rdsiter, &sigrdataset); + if (sigrdataset.type == dns_rdatatype_rrsig && + sigrdataset.covers == rdataset->type) + { + dns_name_format(name, namebuf, sizeof(namebuf)); + dns_rdatatype_format(rdataset->type, typebuf, + sizeof(typebuf)); + zoneverify_log_error( + vctx, + "Warning: Found unexpected signatures " + "for %s/%s", + namebuf, typebuf); + break; + } + dns_rdataset_disassociate(&sigrdataset); + } + if (dns_rdataset_isassociated(&sigrdataset)) { + dns_rdataset_disassociate(&sigrdataset); + } + dns_rdatasetiter_destroy(&rdsiter); + + return (ISC_R_SUCCESS); +} + +static bool +chain_compare(void *arg1, void *arg2) { + struct nsec3_chain_fixed *e1 = arg1, *e2 = arg2; + /* + * Do each element in turn to get a stable sort. + */ + if (e1->hash < e2->hash) { + return (true); + } + if (e1->hash > e2->hash) { + return (false); + } + if (e1->iterations < e2->iterations) { + return (true); + } + if (e1->iterations > e2->iterations) { + return (false); + } + if (e1->salt_length < e2->salt_length) { + return (true); + } + if (e1->salt_length > e2->salt_length) { + return (false); + } + if (e1->next_length < e2->next_length) { + return (true); + } + if (e1->next_length > e2->next_length) { + return (false); + } + if (memcmp(e1 + 1, e2 + 1, chain_length(e1)) < 0) { + return (true); + } + return (false); +} + +static bool +chain_equal(const struct nsec3_chain_fixed *e1, + const struct nsec3_chain_fixed *e2, size_t data_length) { + if (e1->hash != e2->hash) { + return (false); + } + if (e1->iterations != e2->iterations) { + return (false); + } + if (e1->salt_length != e2->salt_length) { + return (false); + } + if (e1->next_length != e2->next_length) { + return (false); + } + + return (memcmp(e1 + 1, e2 + 1, data_length) == 0); +} + +static void +record_nsec3(const vctx_t *vctx, const unsigned char *rawhash, + const dns_rdata_nsec3_t *nsec3, isc_heap_t *chains) { + struct nsec3_chain_fixed *element = NULL; + unsigned char *cp = NULL; + size_t len; + + len = sizeof(*element) + nsec3->next_length * 2 + nsec3->salt_length; + + element = isc_mem_get(vctx->mctx, len); + memset(element, 0, len); + element->hash = nsec3->hash; + element->salt_length = nsec3->salt_length; + element->next_length = nsec3->next_length; + element->iterations = nsec3->iterations; + cp = (unsigned char *)(element + 1); + memmove(cp, nsec3->salt, nsec3->salt_length); + cp += nsec3->salt_length; + memmove(cp, rawhash, nsec3->next_length); + cp += nsec3->next_length; + memmove(cp, nsec3->next, nsec3->next_length); + isc_heap_insert(chains, element); +} + +/* + * Check whether any NSEC3 within 'rdataset' matches the parameters in + * 'nsec3param'. + */ +static isc_result_t +find_nsec3_match(const dns_rdata_nsec3param_t *nsec3param, + dns_rdataset_t *rdataset, size_t rhsize, + dns_rdata_nsec3_t *nsec3_match) { + isc_result_t result; + + /* + * Find matching NSEC3 record. + */ + for (result = dns_rdataset_first(rdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(rdataset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdataset_current(rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, nsec3_match, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (nsec3_match->hash == nsec3param->hash && + nsec3_match->next_length == rhsize && + nsec3_match->iterations == nsec3param->iterations && + nsec3_match->salt_length == nsec3param->salt_length && + memcmp(nsec3_match->salt, nsec3param->salt, + nsec3param->salt_length) == 0) + { + return (ISC_R_SUCCESS); + } + } + + return (result); +} + +static isc_result_t +match_nsec3(const vctx_t *vctx, const dns_name_t *name, + const dns_rdata_nsec3param_t *nsec3param, dns_rdataset_t *rdataset, + const unsigned char types[8192], unsigned int maxtype, + const unsigned char *rawhash, size_t rhsize, + isc_result_t *vresult) { + unsigned char cbm[8244]; + char namebuf[DNS_NAME_FORMATSIZE]; + dns_rdata_nsec3_t nsec3; + isc_result_t result; + unsigned int len; + + result = find_nsec3_match(nsec3param, rdataset, rhsize, &nsec3); + if (result != ISC_R_SUCCESS) { + dns_name_format(name, namebuf, sizeof(namebuf)); + zoneverify_log_error(vctx, "Missing NSEC3 record for %s", + namebuf); + *vresult = result; + return (ISC_R_SUCCESS); + } + + /* + * Check the type list. + */ + len = dns_nsec_compressbitmap(cbm, types, maxtype); + if (nsec3.len != len || memcmp(cbm, nsec3.typebits, len) != 0) { + dns_name_format(name, namebuf, sizeof(namebuf)); + zoneverify_log_error(vctx, + "Bad NSEC3 record for %s, bit map " + "mismatch", + namebuf); + *vresult = ISC_R_FAILURE; + return (ISC_R_SUCCESS); + } + + /* + * Record chain. + */ + record_nsec3(vctx, rawhash, &nsec3, vctx->expected_chains); + + /* + * Make sure there is only one NSEC3 record with this set of + * parameters. + */ + for (result = dns_rdataset_next(rdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(rdataset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdataset_current(rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &nsec3, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (nsec3.hash == nsec3param->hash && + nsec3.iterations == nsec3param->iterations && + nsec3.salt_length == nsec3param->salt_length && + memcmp(nsec3.salt, nsec3param->salt, nsec3.salt_length) == + 0) + { + dns_name_format(name, namebuf, sizeof(namebuf)); + zoneverify_log_error(vctx, + "Multiple NSEC3 records with the " + "same parameter set for %s", + namebuf); + *vresult = DNS_R_DUPLICATE; + return (ISC_R_SUCCESS); + } + } + if (result != ISC_R_NOMORE) { + return (result); + } + + *vresult = ISC_R_SUCCESS; + + return (ISC_R_SUCCESS); +} + +static bool +innsec3params(const dns_rdata_nsec3_t *nsec3, dns_rdataset_t *nsec3paramset) { + dns_rdata_nsec3param_t nsec3param; + isc_result_t result; + + for (result = dns_rdataset_first(nsec3paramset); + result == ISC_R_SUCCESS; result = dns_rdataset_next(nsec3paramset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + + dns_rdataset_current(nsec3paramset, &rdata); + result = dns_rdata_tostruct(&rdata, &nsec3param, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (nsec3param.flags == 0 && nsec3param.hash == nsec3->hash && + nsec3param.iterations == nsec3->iterations && + nsec3param.salt_length == nsec3->salt_length && + memcmp(nsec3param.salt, nsec3->salt, nsec3->salt_length) == + 0) + { + return (true); + } + } + return (false); +} + +static isc_result_t +record_found(const vctx_t *vctx, const dns_name_t *name, dns_dbnode_t *node, + dns_rdataset_t *nsec3paramset) { + unsigned char owner[NSEC3_MAX_HASH_LENGTH]; + dns_rdata_nsec3_t nsec3; + dns_rdataset_t rdataset; + dns_label_t hashlabel; + isc_buffer_t b; + isc_result_t result; + + if (nsec3paramset == NULL || !dns_rdataset_isassociated(nsec3paramset)) + { + return (ISC_R_SUCCESS); + } + + dns_rdataset_init(&rdataset); + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_nsec3, 0, 0, &rdataset, + NULL); + if (result != ISC_R_SUCCESS) { + return (ISC_R_SUCCESS); + } + + dns_name_getlabel(name, 0, &hashlabel); + isc_region_consume(&hashlabel, 1); + isc_buffer_init(&b, owner, sizeof(owner)); + result = isc_base32hex_decoderegion(&hashlabel, &b); + if (result != ISC_R_SUCCESS) { + result = ISC_R_SUCCESS; + goto cleanup; + } + + for (result = dns_rdataset_first(&rdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(&rdataset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdataset_current(&rdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &nsec3, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (nsec3.next_length != isc_buffer_usedlength(&b)) { + continue; + } + + /* + * We only care about NSEC3 records that match a NSEC3PARAM + * record. + */ + if (!innsec3params(&nsec3, nsec3paramset)) { + continue; + } + + /* + * Record chain. + */ + record_nsec3(vctx, owner, &nsec3, vctx->found_chains); + } + result = ISC_R_SUCCESS; + +cleanup: + dns_rdataset_disassociate(&rdataset); + return (result); +} + +static isc_result_t +isoptout(const vctx_t *vctx, const dns_rdata_nsec3param_t *nsec3param, + bool *optout) { + dns_rdataset_t rdataset; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_nsec3_t nsec3; + dns_fixedname_t fixed; + dns_name_t *hashname; + isc_result_t result; + dns_dbnode_t *node = NULL; + unsigned char rawhash[NSEC3_MAX_HASH_LENGTH]; + size_t rhsize = sizeof(rawhash); + + dns_fixedname_init(&fixed); + result = dns_nsec3_hashname(&fixed, rawhash, &rhsize, vctx->origin, + vctx->origin, nsec3param->hash, + nsec3param->iterations, nsec3param->salt, + nsec3param->salt_length); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_nsec3_hashname(): %s", + isc_result_totext(result)); + return (result); + } + + dns_rdataset_init(&rdataset); + hashname = dns_fixedname_name(&fixed); + result = dns_db_findnsec3node(vctx->db, hashname, false, &node); + if (result == ISC_R_SUCCESS) { + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_nsec3, 0, 0, + &rdataset, NULL); + } + if (result != ISC_R_SUCCESS) { + *optout = false; + result = ISC_R_SUCCESS; + goto done; + } + + result = dns_rdataset_first(&rdataset); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_rdataset_first(): %s", + isc_result_totext(result)); + goto done; + } + + dns_rdataset_current(&rdataset, &rdata); + + result = dns_rdata_tostruct(&rdata, &nsec3, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + *optout = ((nsec3.flags & DNS_NSEC3FLAG_OPTOUT) != 0); + +done: + if (dns_rdataset_isassociated(&rdataset)) { + dns_rdataset_disassociate(&rdataset); + } + if (node != NULL) { + dns_db_detachnode(vctx->db, &node); + } + + return (result); +} + +static isc_result_t +verifynsec3(const vctx_t *vctx, const dns_name_t *name, + const dns_rdata_t *rdata, bool delegation, bool empty, + const unsigned char types[8192], unsigned int maxtype, + isc_result_t *vresult) { + char namebuf[DNS_NAME_FORMATSIZE]; + char hashbuf[DNS_NAME_FORMATSIZE]; + dns_rdataset_t rdataset; + dns_rdata_nsec3param_t nsec3param; + dns_fixedname_t fixed; + dns_name_t *hashname; + isc_result_t result, tvresult = ISC_R_UNSET; + dns_dbnode_t *node = NULL; + unsigned char rawhash[NSEC3_MAX_HASH_LENGTH]; + size_t rhsize = sizeof(rawhash); + bool optout = false; + + result = dns_rdata_tostruct(rdata, &nsec3param, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + if (nsec3param.flags != 0) { + return (ISC_R_SUCCESS); + } + + if (!dns_nsec3_supportedhash(nsec3param.hash)) { + return (ISC_R_SUCCESS); + } + + if (nsec3param.iterations > DNS_NSEC3_MAXITERATIONS) { + result = DNS_R_NSEC3ITERRANGE; + zoneverify_log_error(vctx, "verifynsec3: %s", + isc_result_totext(result)); + return (result); + } + + result = isoptout(vctx, &nsec3param, &optout); + if (result != ISC_R_SUCCESS) { + return (result); + } + + dns_fixedname_init(&fixed); + result = dns_nsec3_hashname( + &fixed, rawhash, &rhsize, name, vctx->origin, nsec3param.hash, + nsec3param.iterations, nsec3param.salt, nsec3param.salt_length); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_nsec3_hashname(): %s", + isc_result_totext(result)); + return (result); + } + + /* + * We don't use dns_db_find() here as it works with the chosen + * nsec3 chain and we may also be called with uncommitted data + * from dnssec-signzone so the secure status of the zone may not + * be up to date. + */ + dns_rdataset_init(&rdataset); + hashname = dns_fixedname_name(&fixed); + result = dns_db_findnsec3node(vctx->db, hashname, false, &node); + if (result == ISC_R_SUCCESS) { + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_nsec3, 0, 0, + &rdataset, NULL); + } + if (result != ISC_R_SUCCESS && + (!delegation || (empty && !optout) || + (!empty && dns_nsec_isset(types, dns_rdatatype_ds)))) + { + dns_name_format(name, namebuf, sizeof(namebuf)); + dns_name_format(hashname, hashbuf, sizeof(hashbuf)); + zoneverify_log_error(vctx, "Missing NSEC3 record for %s (%s)", + namebuf, hashbuf); + } else if (result == ISC_R_NOTFOUND && delegation && (!empty || optout)) + { + result = ISC_R_SUCCESS; + } else if (result == ISC_R_SUCCESS) { + result = match_nsec3(vctx, name, &nsec3param, &rdataset, types, + maxtype, rawhash, rhsize, &tvresult); + if (result != ISC_R_SUCCESS) { + goto done; + } + result = tvresult; + } + + *vresult = result; + result = ISC_R_SUCCESS; + +done: + if (dns_rdataset_isassociated(&rdataset)) { + dns_rdataset_disassociate(&rdataset); + } + if (node != NULL) { + dns_db_detachnode(vctx->db, &node); + } + + return (result); +} + +static isc_result_t +verifynsec3s(const vctx_t *vctx, const dns_name_t *name, + dns_rdataset_t *nsec3paramset, bool delegation, bool empty, + const unsigned char types[8192], unsigned int maxtype, + isc_result_t *vresult) { + isc_result_t result; + + for (result = dns_rdataset_first(nsec3paramset); + result == ISC_R_SUCCESS; result = dns_rdataset_next(nsec3paramset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + + dns_rdataset_current(nsec3paramset, &rdata); + result = verifynsec3(vctx, name, &rdata, delegation, empty, + types, maxtype, vresult); + if (result != ISC_R_SUCCESS) { + return (result); + } + if (*vresult != ISC_R_SUCCESS) { + break; + } + } + if (result == ISC_R_NOMORE) { + result = ISC_R_SUCCESS; + } + return (result); +} + +static isc_result_t +verifyset(vctx_t *vctx, dns_rdataset_t *rdataset, const dns_name_t *name, + dns_dbnode_t *node, dst_key_t **dstkeys, size_t nkeys) { + unsigned char set_algorithms[256] = { 0 }; + char namebuf[DNS_NAME_FORMATSIZE]; + char algbuf[DNS_SECALG_FORMATSIZE]; + char typebuf[DNS_RDATATYPE_FORMATSIZE]; + dns_rdataset_t sigrdataset; + dns_rdatasetiter_t *rdsiter = NULL; + isc_result_t result; + + dns_rdataset_init(&sigrdataset); + result = dns_db_allrdatasets(vctx->db, node, vctx->ver, 0, 0, &rdsiter); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_db_allrdatasets(): %s", + isc_result_totext(result)); + return (result); + } + for (result = dns_rdatasetiter_first(rdsiter); result == ISC_R_SUCCESS; + result = dns_rdatasetiter_next(rdsiter)) + { + dns_rdatasetiter_current(rdsiter, &sigrdataset); + if (sigrdataset.type == dns_rdatatype_rrsig && + sigrdataset.covers == rdataset->type) + { + break; + } + dns_rdataset_disassociate(&sigrdataset); + } + if (result != ISC_R_SUCCESS) { + dns_name_format(name, namebuf, sizeof(namebuf)); + dns_rdatatype_format(rdataset->type, typebuf, sizeof(typebuf)); + zoneverify_log_error(vctx, "No signatures for %s/%s", namebuf, + typebuf); + for (size_t i = 0; i < ARRAY_SIZE(set_algorithms); i++) { + if (vctx->act_algorithms[i] != 0) { + vctx->bad_algorithms[i] = 1; + } + } + result = ISC_R_SUCCESS; + goto done; + } + + for (result = dns_rdataset_first(&sigrdataset); result == ISC_R_SUCCESS; + result = dns_rdataset_next(&sigrdataset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_rrsig_t sig; + + dns_rdataset_current(&sigrdataset, &rdata); + result = dns_rdata_tostruct(&rdata, &sig, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (rdataset->ttl != sig.originalttl) { + dns_name_format(name, namebuf, sizeof(namebuf)); + dns_rdatatype_format(rdataset->type, typebuf, + sizeof(typebuf)); + zoneverify_log_error(vctx, + "TTL mismatch for " + "%s %s keytag %u", + namebuf, typebuf, sig.keyid); + continue; + } + if ((set_algorithms[sig.algorithm] != 0) || + (vctx->act_algorithms[sig.algorithm] == 0)) + { + continue; + } + if (goodsig(vctx, &rdata, name, dstkeys, nkeys, rdataset)) { + dns_rdataset_settrust(rdataset, dns_trust_secure); + dns_rdataset_settrust(&sigrdataset, dns_trust_secure); + set_algorithms[sig.algorithm] = 1; + } + } + result = ISC_R_SUCCESS; + + if (memcmp(set_algorithms, vctx->act_algorithms, + sizeof(set_algorithms)) != 0) + { + dns_name_format(name, namebuf, sizeof(namebuf)); + dns_rdatatype_format(rdataset->type, typebuf, sizeof(typebuf)); + for (size_t i = 0; i < ARRAY_SIZE(set_algorithms); i++) { + if ((vctx->act_algorithms[i] != 0) && + (set_algorithms[i] == 0)) + { + dns_secalg_format(i, algbuf, sizeof(algbuf)); + zoneverify_log_error(vctx, + "No correct %s signature " + "for %s %s", + algbuf, namebuf, typebuf); + vctx->bad_algorithms[i] = 1; + } + } + } + +done: + if (dns_rdataset_isassociated(&sigrdataset)) { + dns_rdataset_disassociate(&sigrdataset); + } + dns_rdatasetiter_destroy(&rdsiter); + + return (result); +} + +static isc_result_t +verifynode(vctx_t *vctx, const dns_name_t *name, dns_dbnode_t *node, + bool delegation, dst_key_t **dstkeys, size_t nkeys, + dns_rdataset_t *nsecset, dns_rdataset_t *nsec3paramset, + const dns_name_t *nextname, isc_result_t *vresult) { + unsigned char types[8192] = { 0 }; + unsigned int maxtype = 0; + dns_rdataset_t rdataset; + dns_rdatasetiter_t *rdsiter = NULL; + isc_result_t result, tvresult = ISC_R_UNSET; + + REQUIRE(vresult != NULL || (nsecset == NULL && nsec3paramset == NULL)); + + result = dns_db_allrdatasets(vctx->db, node, vctx->ver, 0, 0, &rdsiter); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_db_allrdatasets(): %s", + isc_result_totext(result)); + return (result); + } + + result = dns_rdatasetiter_first(rdsiter); + dns_rdataset_init(&rdataset); + while (result == ISC_R_SUCCESS) { + dns_rdatasetiter_current(rdsiter, &rdataset); + /* + * If we are not at a delegation then everything should be + * signed. If we are at a delegation then only the DS set + * is signed. The NS set is not signed at a delegation but + * its existence is recorded in the bit map. Anything else + * other than NSEC and DS is not signed at a delegation. + */ + if (rdataset.type != dns_rdatatype_rrsig && + rdataset.type != dns_rdatatype_dnskey && + (!delegation || rdataset.type == dns_rdatatype_ds || + rdataset.type == dns_rdatatype_nsec)) + { + result = verifyset(vctx, &rdataset, name, node, dstkeys, + nkeys); + if (result != ISC_R_SUCCESS) { + dns_rdataset_disassociate(&rdataset); + dns_rdatasetiter_destroy(&rdsiter); + return (result); + } + dns_nsec_setbit(types, rdataset.type, 1); + if (rdataset.type > maxtype) { + maxtype = rdataset.type; + } + } else if (rdataset.type != dns_rdatatype_rrsig && + rdataset.type != dns_rdatatype_dnskey) + { + if (rdataset.type == dns_rdatatype_ns) { + dns_nsec_setbit(types, rdataset.type, 1); + } + result = check_no_rrsig(vctx, &rdataset, name, node); + if (result != ISC_R_SUCCESS) { + dns_rdataset_disassociate(&rdataset); + dns_rdatasetiter_destroy(&rdsiter); + return (result); + } + } else { + dns_nsec_setbit(types, rdataset.type, 1); + } + dns_rdataset_disassociate(&rdataset); + result = dns_rdatasetiter_next(rdsiter); + } + dns_rdatasetiter_destroy(&rdsiter); + if (result != ISC_R_NOMORE) { + zoneverify_log_error(vctx, "rdataset iteration failed: %s", + isc_result_totext(result)); + return (result); + } + + if (vresult == NULL) { + return (ISC_R_SUCCESS); + } + + *vresult = ISC_R_SUCCESS; + + if (nsecset != NULL && dns_rdataset_isassociated(nsecset)) { + result = verifynsec(vctx, name, node, nextname, &tvresult); + if (result != ISC_R_SUCCESS) { + return (result); + } + *vresult = tvresult; + } + + if (nsec3paramset != NULL && dns_rdataset_isassociated(nsec3paramset)) { + result = verifynsec3s(vctx, name, nsec3paramset, delegation, + false, types, maxtype, &tvresult); + if (result != ISC_R_SUCCESS) { + return (result); + } + if (*vresult == ISC_R_SUCCESS) { + *vresult = tvresult; + } + } + + return (ISC_R_SUCCESS); +} + +static isc_result_t +is_empty(const vctx_t *vctx, dns_dbnode_t *node, bool *empty) { + dns_rdatasetiter_t *rdsiter = NULL; + isc_result_t result; + + result = dns_db_allrdatasets(vctx->db, node, vctx->ver, 0, 0, &rdsiter); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_db_allrdatasets(): %s", + isc_result_totext(result)); + return (result); + } + result = dns_rdatasetiter_first(rdsiter); + dns_rdatasetiter_destroy(&rdsiter); + + *empty = (result == ISC_R_NOMORE); + + return (ISC_R_SUCCESS); +} + +static isc_result_t +check_no_nsec(const vctx_t *vctx, const dns_name_t *name, dns_dbnode_t *node) { + bool nsec_exists = false; + dns_rdataset_t rdataset; + isc_result_t result; + + dns_rdataset_init(&rdataset); + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_nsec, 0, 0, &rdataset, NULL); + if (result != ISC_R_NOTFOUND) { + char namebuf[DNS_NAME_FORMATSIZE]; + dns_name_format(name, namebuf, sizeof(namebuf)); + zoneverify_log_error(vctx, "unexpected NSEC RRset at %s", + namebuf); + nsec_exists = true; + } + + if (dns_rdataset_isassociated(&rdataset)) { + dns_rdataset_disassociate(&rdataset); + } + + return (nsec_exists ? ISC_R_FAILURE : ISC_R_SUCCESS); +} + +static void +free_element(isc_mem_t *mctx, struct nsec3_chain_fixed *e) { + size_t len; + + len = sizeof(*e) + e->salt_length + 2 * e->next_length; + isc_mem_put(mctx, e, len); +} + +static void +free_element_heap(void *element, void *uap) { + struct nsec3_chain_fixed *e = (struct nsec3_chain_fixed *)element; + isc_mem_t *mctx = (isc_mem_t *)uap; + + free_element(mctx, e); +} + +static bool +_checknext(const vctx_t *vctx, const struct nsec3_chain_fixed *first, + const struct nsec3_chain_fixed *e) { + char buf[512]; + const unsigned char *d1 = (const unsigned char *)(first + 1); + const unsigned char *d2 = (const unsigned char *)(e + 1); + isc_buffer_t b; + isc_region_t sr; + + d1 += first->salt_length + first->next_length; + d2 += e->salt_length; + + if (memcmp(d1, d2, first->next_length) == 0) { + return (true); + } + + DE_CONST(d1 - first->next_length, sr.base); + sr.length = first->next_length; + isc_buffer_init(&b, buf, sizeof(buf)); + isc_base32hex_totext(&sr, 1, "", &b); + zoneverify_log_error(vctx, "Break in NSEC3 chain at: %.*s", + (int)isc_buffer_usedlength(&b), buf); + + DE_CONST(d1, sr.base); + sr.length = first->next_length; + isc_buffer_init(&b, buf, sizeof(buf)); + isc_base32hex_totext(&sr, 1, "", &b); + zoneverify_log_error(vctx, "Expected: %.*s", + (int)isc_buffer_usedlength(&b), buf); + + DE_CONST(d2, sr.base); + sr.length = first->next_length; + isc_buffer_init(&b, buf, sizeof(buf)); + isc_base32hex_totext(&sr, 1, "", &b); + zoneverify_log_error(vctx, "Found: %.*s", + (int)isc_buffer_usedlength(&b), buf); + + return (false); +} + +static bool +checknext(isc_mem_t *mctx, const vctx_t *vctx, + const struct nsec3_chain_fixed *first, struct nsec3_chain_fixed *prev, + const struct nsec3_chain_fixed *cur) { + bool result = _checknext(vctx, prev, cur); + + if (prev != first) { + free_element(mctx, prev); + } + + return (result); +} + +static bool +checklast(isc_mem_t *mctx, const vctx_t *vctx, struct nsec3_chain_fixed *first, + struct nsec3_chain_fixed *prev) { + bool result = _checknext(vctx, prev, first); + if (prev != first) { + free_element(mctx, prev); + } + free_element(mctx, first); + + return (result); +} + +static isc_result_t +verify_nsec3_chains(const vctx_t *vctx, isc_mem_t *mctx) { + isc_result_t result = ISC_R_SUCCESS; + struct nsec3_chain_fixed *e, *f = NULL; + struct nsec3_chain_fixed *first = NULL, *prev = NULL; + + while ((e = isc_heap_element(vctx->expected_chains, 1)) != NULL) { + isc_heap_delete(vctx->expected_chains, 1); + if (f == NULL) { + f = isc_heap_element(vctx->found_chains, 1); + } + if (f != NULL) { + isc_heap_delete(vctx->found_chains, 1); + + /* + * Check that they match. + */ + if (chain_equal(e, f, chain_length(e))) { + free_element(mctx, f); + f = NULL; + } else { + if (result == ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "Expected " + "and found " + "NSEC3 " + "chains not " + "equal"); + } + result = ISC_R_FAILURE; + /* + * Attempt to resync found_chain. + */ + while (f != NULL && !chain_compare(e, f)) { + free_element(mctx, f); + f = isc_heap_element(vctx->found_chains, + 1); + if (f != NULL) { + isc_heap_delete( + vctx->found_chains, 1); + } + if (f != NULL && + chain_equal(e, f, chain_length(e))) + { + free_element(mctx, f); + f = NULL; + break; + } + } + } + } else if (result == ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "Expected and found NSEC3 " + "chains " + "not equal"); + result = ISC_R_FAILURE; + } + + if (first == NULL) { + prev = first = e; + } else if (!chain_equal(first, e, first->salt_length)) { + if (!checklast(mctx, vctx, first, prev)) { + result = ISC_R_FAILURE; + } + + prev = first = e; + } else { + if (!checknext(mctx, vctx, first, prev, e)) { + result = ISC_R_FAILURE; + } + + prev = e; + } + } + if (prev != NULL) { + if (!checklast(mctx, vctx, first, prev)) { + result = ISC_R_FAILURE; + } + } + do { + if (f != NULL) { + if (result == ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "Expected and found " + "NSEC3 chains not " + "equal"); + result = ISC_R_FAILURE; + } + free_element(mctx, f); + } + f = isc_heap_element(vctx->found_chains, 1); + if (f != NULL) { + isc_heap_delete(vctx->found_chains, 1); + } + } while (f != NULL); + + return (result); +} + +static isc_result_t +verifyemptynodes(const vctx_t *vctx, const dns_name_t *name, + const dns_name_t *prevname, bool isdelegation, + dns_rdataset_t *nsec3paramset, isc_result_t *vresult) { + dns_namereln_t reln; + int order; + unsigned int labels, nlabels, i; + dns_name_t suffix; + isc_result_t result, tvresult = ISC_R_UNSET; + + *vresult = ISC_R_SUCCESS; + + reln = dns_name_fullcompare(prevname, name, &order, &labels); + if (order >= 0) { + return (ISC_R_SUCCESS); + } + + nlabels = dns_name_countlabels(name); + + if (reln == dns_namereln_commonancestor || + reln == dns_namereln_contains) + { + dns_name_init(&suffix, NULL); + for (i = labels + 1; i < nlabels; i++) { + dns_name_getlabelsequence(name, nlabels - i, i, + &suffix); + if (nsec3paramset != NULL && + dns_rdataset_isassociated(nsec3paramset)) + { + result = verifynsec3s( + vctx, &suffix, nsec3paramset, + isdelegation, true, NULL, 0, &tvresult); + if (result != ISC_R_SUCCESS) { + return (result); + } + if (*vresult == ISC_R_SUCCESS) { + *vresult = tvresult; + } + } + } + } + + return (ISC_R_SUCCESS); +} + +static void +vctx_init(vctx_t *vctx, isc_mem_t *mctx, dns_zone_t *zone, dns_db_t *db, + dns_dbversion_t *ver, dns_name_t *origin, dns_keytable_t *secroots) { + memset(vctx, 0, sizeof(*vctx)); + + vctx->mctx = mctx; + vctx->zone = zone; + vctx->db = db; + vctx->ver = ver; + vctx->origin = origin; + vctx->secroots = secroots; + vctx->goodksk = false; + vctx->goodzsk = false; + + dns_rdataset_init(&vctx->keyset); + dns_rdataset_init(&vctx->keysigs); + dns_rdataset_init(&vctx->soaset); + dns_rdataset_init(&vctx->soasigs); + dns_rdataset_init(&vctx->nsecset); + dns_rdataset_init(&vctx->nsecsigs); + dns_rdataset_init(&vctx->nsec3paramset); + dns_rdataset_init(&vctx->nsec3paramsigs); + + vctx->expected_chains = NULL; + isc_heap_create(mctx, chain_compare, NULL, 1024, + &vctx->expected_chains); + + vctx->found_chains = NULL; + isc_heap_create(mctx, chain_compare, NULL, 1024, &vctx->found_chains); +} + +static void +vctx_destroy(vctx_t *vctx) { + if (dns_rdataset_isassociated(&vctx->keyset)) { + dns_rdataset_disassociate(&vctx->keyset); + } + if (dns_rdataset_isassociated(&vctx->keysigs)) { + dns_rdataset_disassociate(&vctx->keysigs); + } + if (dns_rdataset_isassociated(&vctx->soaset)) { + dns_rdataset_disassociate(&vctx->soaset); + } + if (dns_rdataset_isassociated(&vctx->soasigs)) { + dns_rdataset_disassociate(&vctx->soasigs); + } + if (dns_rdataset_isassociated(&vctx->nsecset)) { + dns_rdataset_disassociate(&vctx->nsecset); + } + if (dns_rdataset_isassociated(&vctx->nsecsigs)) { + dns_rdataset_disassociate(&vctx->nsecsigs); + } + if (dns_rdataset_isassociated(&vctx->nsec3paramset)) { + dns_rdataset_disassociate(&vctx->nsec3paramset); + } + if (dns_rdataset_isassociated(&vctx->nsec3paramsigs)) { + dns_rdataset_disassociate(&vctx->nsec3paramsigs); + } + isc_heap_foreach(vctx->expected_chains, free_element_heap, vctx->mctx); + isc_heap_destroy(&vctx->expected_chains); + isc_heap_foreach(vctx->found_chains, free_element_heap, vctx->mctx); + isc_heap_destroy(&vctx->found_chains); +} + +static isc_result_t +check_apex_rrsets(vctx_t *vctx) { + dns_dbnode_t *node = NULL; + isc_result_t result; + + result = dns_db_findnode(vctx->db, vctx->origin, false, &node); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, + "failed to find the zone's origin: %s", + isc_result_totext(result)); + return (result); + } + + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_dnskey, 0, 0, &vctx->keyset, + &vctx->keysigs); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "Zone contains no DNSSEC keys"); + goto done; + } + + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_soa, 0, 0, &vctx->soaset, + &vctx->soasigs); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "Zone contains no SOA record"); + goto done; + } + + result = dns_db_findrdataset(vctx->db, node, vctx->ver, + dns_rdatatype_nsec, 0, 0, &vctx->nsecset, + &vctx->nsecsigs); + if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) { + zoneverify_log_error(vctx, "NSEC lookup failed"); + goto done; + } + + result = dns_db_findrdataset( + vctx->db, node, vctx->ver, dns_rdatatype_nsec3param, 0, 0, + &vctx->nsec3paramset, &vctx->nsec3paramsigs); + if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) { + zoneverify_log_error(vctx, "NSEC3PARAM lookup failed"); + goto done; + } + + if (!dns_rdataset_isassociated(&vctx->keysigs)) { + zoneverify_log_error(vctx, "DNSKEY is not signed " + "(keys offline or inactive?)"); + result = ISC_R_FAILURE; + goto done; + } + + if (!dns_rdataset_isassociated(&vctx->soasigs)) { + zoneverify_log_error(vctx, "SOA is not signed " + "(keys offline or inactive?)"); + result = ISC_R_FAILURE; + goto done; + } + + if (dns_rdataset_isassociated(&vctx->nsecset) && + !dns_rdataset_isassociated(&vctx->nsecsigs)) + { + zoneverify_log_error(vctx, "NSEC is not signed " + "(keys offline or inactive?)"); + result = ISC_R_FAILURE; + goto done; + } + + if (dns_rdataset_isassociated(&vctx->nsec3paramset) && + !dns_rdataset_isassociated(&vctx->nsec3paramsigs)) + { + zoneverify_log_error(vctx, "NSEC3PARAM is not signed " + "(keys offline or inactive?)"); + result = ISC_R_FAILURE; + goto done; + } + + if (!dns_rdataset_isassociated(&vctx->nsecset) && + !dns_rdataset_isassociated(&vctx->nsec3paramset)) + { + zoneverify_log_error(vctx, "No valid NSEC/NSEC3 chain for " + "testing"); + result = ISC_R_FAILURE; + goto done; + } + + result = ISC_R_SUCCESS; + +done: + dns_db_detachnode(vctx->db, &node); + + return (result); +} + +/*% + * Update 'vctx' tables tracking active and standby key algorithms used in the + * verified zone based on the signatures made using 'dnskey' (prepared from + * 'rdata') found at zone apex. Set 'vctx->goodksk' or 'vctx->goodzsk' to true + * if 'dnskey' correctly signs the DNSKEY RRset at zone apex and either + * 'vctx->secroots' is NULL or 'dnskey' is present in 'vctx->secroots'. + * + * The variables to update are chosen based on 'is_ksk', which is true when + * 'dnskey' is a KSK and false otherwise. + */ +static void +check_dnskey_sigs(vctx_t *vctx, const dns_rdata_dnskey_t *dnskey, + dns_rdata_t *keyrdata, bool is_ksk) { + unsigned char *active_keys = NULL, *standby_keys = NULL; + dns_keynode_t *keynode = NULL; + bool *goodkey = NULL; + dst_key_t *key = NULL; + isc_result_t result; + dns_rdataset_t dsset; + + active_keys = (is_ksk ? vctx->ksk_algorithms : vctx->zsk_algorithms); + standby_keys = (is_ksk ? vctx->standby_ksk : vctx->standby_zsk); + goodkey = (is_ksk ? &vctx->goodksk : &vctx->goodzsk); + + /* + * First, does this key sign the DNSKEY rrset? + */ + if (!dns_dnssec_selfsigns(keyrdata, vctx->origin, &vctx->keyset, + &vctx->keysigs, false, vctx->mctx)) + { + if (!is_ksk && + dns_dnssec_signs(keyrdata, vctx->origin, &vctx->soaset, + &vctx->soasigs, false, vctx->mctx)) + { + if (active_keys[dnskey->algorithm] != DNS_KEYALG_MAX) { + active_keys[dnskey->algorithm]++; + } + } else { + if (standby_keys[dnskey->algorithm] != DNS_KEYALG_MAX) { + standby_keys[dnskey->algorithm]++; + } + } + return; + } + + if (active_keys[dnskey->algorithm] != DNS_KEYALG_MAX) { + active_keys[dnskey->algorithm]++; + } + + /* + * If a trust anchor table was not supplied, a correctly self-signed + * DNSKEY RRset is good enough. + */ + if (vctx->secroots == NULL) { + *goodkey = true; + return; + } + + /* + * Convert the supplied key rdata to dst_key_t. (If this + * fails we can't go further.) + */ + result = dns_dnssec_keyfromrdata(vctx->origin, keyrdata, vctx->mctx, + &key); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + /* + * Look up the supplied key in the trust anchor table. + * If we don't find an exact match, or if the keynode data + * is NULL, then we have neither a DNSKEY nor a DS format + * trust anchor, and can give up. + */ + result = dns_keytable_find(vctx->secroots, vctx->origin, &keynode); + if (result != ISC_R_SUCCESS) { + /* No such trust anchor */ + goto cleanup; + } + + /* + * If the keynode has any DS format trust anchors, that means + * it doesn't have any DNSKEY ones. So, we can check for a DS + * match and then stop. + */ + dns_rdataset_init(&dsset); + if (dns_keynode_dsset(keynode, &dsset)) { + for (result = dns_rdataset_first(&dsset); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(&dsset)) + { + dns_rdata_t dsrdata = DNS_RDATA_INIT; + dns_rdata_t newdsrdata = DNS_RDATA_INIT; + unsigned char buf[DNS_DS_BUFFERSIZE]; + dns_rdata_ds_t ds; + + dns_rdata_reset(&dsrdata); + dns_rdataset_current(&dsset, &dsrdata); + result = dns_rdata_tostruct(&dsrdata, &ds, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + if (ds.key_tag != dst_key_id(key) || + ds.algorithm != dst_key_alg(key)) + { + continue; + } + + result = dns_ds_buildrdata(vctx->origin, keyrdata, + ds.digest_type, buf, + &newdsrdata); + if (result != ISC_R_SUCCESS) { + continue; + } + + if (dns_rdata_compare(&dsrdata, &newdsrdata) == 0) { + dns_rdataset_settrust(&vctx->keyset, + dns_trust_secure); + dns_rdataset_settrust(&vctx->keysigs, + dns_trust_secure); + *goodkey = true; + break; + } + } + dns_rdataset_disassociate(&dsset); + + goto cleanup; + } + +cleanup: + if (keynode != NULL) { + dns_keytable_detachkeynode(vctx->secroots, &keynode); + } + if (key != NULL) { + dst_key_free(&key); + } +} + +/*% + * Check that the DNSKEY RR has at least one self signing KSK and one ZSK per + * algorithm in it (or, if -x was used, one self-signing KSK). + */ +static isc_result_t +check_dnskey(vctx_t *vctx) { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdata_dnskey_t dnskey; + isc_result_t result; + bool is_ksk; + + for (result = dns_rdataset_first(&vctx->keyset); + result == ISC_R_SUCCESS; result = dns_rdataset_next(&vctx->keyset)) + { + dns_rdataset_current(&vctx->keyset, &rdata); + result = dns_rdata_tostruct(&rdata, &dnskey, NULL); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + is_ksk = ((dnskey.flags & DNS_KEYFLAG_KSK) != 0); + + if ((dnskey.flags & DNS_KEYOWNER_ZONE) != 0 && + (dnskey.flags & DNS_KEYFLAG_REVOKE) != 0) + { + if ((dnskey.flags & DNS_KEYFLAG_KSK) != 0 && + !dns_dnssec_selfsigns(&rdata, vctx->origin, + &vctx->keyset, &vctx->keysigs, + false, vctx->mctx)) + { + char namebuf[DNS_NAME_FORMATSIZE]; + char buffer[1024]; + isc_buffer_t buf; + + dns_name_format(vctx->origin, namebuf, + sizeof(namebuf)); + isc_buffer_init(&buf, buffer, sizeof(buffer)); + result = dns_rdata_totext(&rdata, NULL, &buf); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error( + vctx, "dns_rdata_totext: %s", + isc_result_totext(result)); + return (ISC_R_FAILURE); + } + zoneverify_log_error( + vctx, + "revoked KSK is not self signed:\n" + "%s DNSKEY %.*s", + namebuf, + (int)isc_buffer_usedlength(&buf), + buffer); + return (ISC_R_FAILURE); + } + if ((dnskey.flags & DNS_KEYFLAG_KSK) != 0 && + vctx->revoked_ksk[dnskey.algorithm] != + DNS_KEYALG_MAX) + { + vctx->revoked_ksk[dnskey.algorithm]++; + } else if ((dnskey.flags & DNS_KEYFLAG_KSK) == 0 && + vctx->revoked_zsk[dnskey.algorithm] != + DNS_KEYALG_MAX) + { + vctx->revoked_zsk[dnskey.algorithm]++; + } + } else { + check_dnskey_sigs(vctx, &dnskey, &rdata, is_ksk); + } + dns_rdata_freestruct(&dnskey); + dns_rdata_reset(&rdata); + } + + return (ISC_R_SUCCESS); +} + +static void +determine_active_algorithms(vctx_t *vctx, bool ignore_kskflag, + bool keyset_kskonly, + void (*report)(const char *, ...)) { + char algbuf[DNS_SECALG_FORMATSIZE]; + + report("Verifying the zone using the following algorithms:"); + + for (size_t i = 0; i < ARRAY_SIZE(vctx->act_algorithms); i++) { + if (ignore_kskflag) { + vctx->act_algorithms[i] = (vctx->ksk_algorithms[i] != + 0 || + vctx->zsk_algorithms[i] != 0) + ? 1 + : 0; + } else { + vctx->act_algorithms[i] = vctx->ksk_algorithms[i] != 0 + ? 1 + : 0; + } + if (vctx->act_algorithms[i] != 0) { + dns_secalg_format(i, algbuf, sizeof(algbuf)); + report("- %s", algbuf); + } + } + + if (ignore_kskflag || keyset_kskonly) { + return; + } + + for (size_t i = 0; i < ARRAY_SIZE(vctx->ksk_algorithms); i++) { + /* + * The counts should both be zero or both be non-zero. Mark + * the algorithm as bad if this is not met. + */ + if ((vctx->ksk_algorithms[i] != 0) == + (vctx->zsk_algorithms[i] != 0)) + { + continue; + } + dns_secalg_format(i, algbuf, sizeof(algbuf)); + zoneverify_log_error(vctx, "Missing %s for algorithm %s", + (vctx->ksk_algorithms[i] != 0) ? "ZSK" + : "self-" + "signed " + "KSK", + algbuf); + vctx->bad_algorithms[i] = 1; + } +} + +/*% + * Check that all the records not yet verified were signed by keys that are + * present in the DNSKEY RRset. + */ +static isc_result_t +verify_nodes(vctx_t *vctx, isc_result_t *vresult) { + dns_fixedname_t fname, fnextname, fprevname, fzonecut; + dns_name_t *name, *nextname, *prevname, *zonecut; + dns_dbnode_t *node = NULL, *nextnode; + dns_dbiterator_t *dbiter = NULL; + dst_key_t **dstkeys; + size_t count, nkeys = 0; + bool done = false; + isc_result_t tvresult = ISC_R_UNSET; + isc_result_t result; + + name = dns_fixedname_initname(&fname); + nextname = dns_fixedname_initname(&fnextname); + dns_fixedname_init(&fprevname); + prevname = NULL; + dns_fixedname_init(&fzonecut); + zonecut = NULL; + + count = dns_rdataset_count(&vctx->keyset); + dstkeys = isc_mem_get(vctx->mctx, sizeof(*dstkeys) * count); + + for (result = dns_rdataset_first(&vctx->keyset); + result == ISC_R_SUCCESS; result = dns_rdataset_next(&vctx->keyset)) + { + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdataset_current(&vctx->keyset, &rdata); + dstkeys[nkeys] = NULL; + result = dns_dnssec_keyfromrdata(vctx->origin, &rdata, + vctx->mctx, &dstkeys[nkeys]); + if (result == ISC_R_SUCCESS) { + nkeys++; + } + } + + result = dns_db_createiterator(vctx->db, DNS_DB_NONSEC3, &dbiter); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_db_createiterator(): %s", + isc_result_totext(result)); + goto done; + } + + result = dns_dbiterator_first(dbiter); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_dbiterator_first(): %s", + isc_result_totext(result)); + goto done; + } + + while (!done) { + bool isdelegation = false; + + result = dns_dbiterator_current(dbiter, &node, name); + if (result != ISC_R_SUCCESS && result != DNS_R_NEWORIGIN) { + zoneverify_log_error(vctx, + "dns_dbiterator_current(): %s", + isc_result_totext(result)); + goto done; + } + if (!dns_name_issubdomain(name, vctx->origin)) { + result = check_no_nsec(vctx, name, node); + if (result != ISC_R_SUCCESS) { + dns_db_detachnode(vctx->db, &node); + goto done; + } + dns_db_detachnode(vctx->db, &node); + result = dns_dbiterator_next(dbiter); + if (result == ISC_R_NOMORE) { + done = true; + } else if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, + "dns_dbiterator_next(): " + "%s", + isc_result_totext(result)); + goto done; + } + continue; + } + if (is_delegation(vctx, name, node, NULL)) { + zonecut = dns_fixedname_name(&fzonecut); + dns_name_copy(name, zonecut); + isdelegation = true; + } else if (has_dname(vctx, node)) { + zonecut = dns_fixedname_name(&fzonecut); + dns_name_copy(name, zonecut); + } + nextnode = NULL; + result = dns_dbiterator_next(dbiter); + while (result == ISC_R_SUCCESS) { + bool empty; + result = dns_dbiterator_current(dbiter, &nextnode, + nextname); + if (result != ISC_R_SUCCESS && + result != DNS_R_NEWORIGIN) + { + zoneverify_log_error(vctx, + "dns_dbiterator_current():" + " %s", + isc_result_totext(result)); + dns_db_detachnode(vctx->db, &node); + goto done; + } + if (!dns_name_issubdomain(nextname, vctx->origin) || + (zonecut != NULL && + dns_name_issubdomain(nextname, zonecut))) + { + result = check_no_nsec(vctx, nextname, + nextnode); + if (result != ISC_R_SUCCESS) { + dns_db_detachnode(vctx->db, &node); + dns_db_detachnode(vctx->db, &nextnode); + goto done; + } + dns_db_detachnode(vctx->db, &nextnode); + result = dns_dbiterator_next(dbiter); + continue; + } + result = is_empty(vctx, nextnode, &empty); + dns_db_detachnode(vctx->db, &nextnode); + if (result != ISC_R_SUCCESS) { + dns_db_detachnode(vctx->db, &node); + goto done; + } + if (empty) { + result = dns_dbiterator_next(dbiter); + continue; + } + break; + } + if (result == ISC_R_NOMORE) { + done = true; + nextname = vctx->origin; + } else if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, + "iterating through the database " + "failed: %s", + isc_result_totext(result)); + dns_db_detachnode(vctx->db, &node); + goto done; + } + result = verifynode(vctx, name, node, isdelegation, dstkeys, + nkeys, &vctx->nsecset, &vctx->nsec3paramset, + nextname, &tvresult); + if (result != ISC_R_SUCCESS) { + dns_db_detachnode(vctx->db, &node); + goto done; + } + if (*vresult == ISC_R_UNSET) { + *vresult = ISC_R_SUCCESS; + } + if (*vresult == ISC_R_SUCCESS) { + *vresult = tvresult; + } + if (prevname != NULL) { + result = verifyemptynodes( + vctx, name, prevname, isdelegation, + &vctx->nsec3paramset, &tvresult); + if (result != ISC_R_SUCCESS) { + dns_db_detachnode(vctx->db, &node); + goto done; + } + } else { + prevname = dns_fixedname_name(&fprevname); + } + dns_name_copy(name, prevname); + if (*vresult == ISC_R_SUCCESS) { + *vresult = tvresult; + } + dns_db_detachnode(vctx->db, &node); + } + + dns_dbiterator_destroy(&dbiter); + + result = dns_db_createiterator(vctx->db, DNS_DB_NSEC3ONLY, &dbiter); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "dns_db_createiterator(): %s", + isc_result_totext(result)); + return (result); + } + + for (result = dns_dbiterator_first(dbiter); result == ISC_R_SUCCESS; + result = dns_dbiterator_next(dbiter)) + { + result = dns_dbiterator_current(dbiter, &node, name); + if (result != ISC_R_SUCCESS && result != DNS_R_NEWORIGIN) { + zoneverify_log_error(vctx, + "dns_dbiterator_current(): %s", + isc_result_totext(result)); + goto done; + } + result = verifynode(vctx, name, node, false, dstkeys, nkeys, + NULL, NULL, NULL, NULL); + if (result != ISC_R_SUCCESS) { + zoneverify_log_error(vctx, "verifynode: %s", + isc_result_totext(result)); + dns_db_detachnode(vctx->db, &node); + goto done; + } + result = record_found(vctx, name, node, &vctx->nsec3paramset); + dns_db_detachnode(vctx->db, &node); + if (result != ISC_R_SUCCESS) { + goto done; + } + } + + result = ISC_R_SUCCESS; + +done: + while (nkeys-- > 0U) { + dst_key_free(&dstkeys[nkeys]); + } + isc_mem_put(vctx->mctx, dstkeys, sizeof(*dstkeys) * count); + if (dbiter != NULL) { + dns_dbiterator_destroy(&dbiter); + } + + return (result); +} + +static isc_result_t +check_bad_algorithms(const vctx_t *vctx, void (*report)(const char *, ...)) { + char algbuf[DNS_SECALG_FORMATSIZE]; + bool first = true; + + for (size_t i = 0; i < ARRAY_SIZE(vctx->bad_algorithms); i++) { + if (vctx->bad_algorithms[i] == 0) { + continue; + } + if (first) { + report("The zone is not fully signed " + "for the following algorithms:"); + } + dns_secalg_format(i, algbuf, sizeof(algbuf)); + report(" %s", algbuf); + first = false; + } + + if (!first) { + report("."); + } + + return (first ? ISC_R_SUCCESS : ISC_R_FAILURE); +} + +static void +print_summary(const vctx_t *vctx, bool keyset_kskonly, + void (*report)(const char *, ...)) { + char algbuf[DNS_SECALG_FORMATSIZE]; + + report("Zone fully signed:"); + for (size_t i = 0; i < ARRAY_SIZE(vctx->ksk_algorithms); i++) { + if ((vctx->ksk_algorithms[i] == 0) && + (vctx->standby_ksk[i] == 0) && + (vctx->revoked_ksk[i] == 0) && + (vctx->zsk_algorithms[i] == 0) && + (vctx->standby_zsk[i] == 0) && (vctx->revoked_zsk[i] == 0)) + { + continue; + } + dns_secalg_format(i, algbuf, sizeof(algbuf)); + report("Algorithm: %s: KSKs: " + "%u active, %u stand-by, %u revoked", + algbuf, vctx->ksk_algorithms[i], vctx->standby_ksk[i], + vctx->revoked_ksk[i]); + report("%*sZSKs: " + "%u active, %u %s, %u revoked", + (int)strlen(algbuf) + 13, "", vctx->zsk_algorithms[i], + vctx->standby_zsk[i], + keyset_kskonly ? "present" : "stand-by", + vctx->revoked_zsk[i]); + } +} + +isc_result_t +dns_zoneverify_dnssec(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *ver, + dns_name_t *origin, dns_keytable_t *secroots, + isc_mem_t *mctx, bool ignore_kskflag, bool keyset_kskonly, + void (*report)(const char *, ...)) { + const char *keydesc = (secroots == NULL ? "self-signed" : "trusted"); + isc_result_t result, vresult = ISC_R_UNSET; + vctx_t vctx; + + vctx_init(&vctx, mctx, zone, db, ver, origin, secroots); + + result = check_apex_rrsets(&vctx); + if (result != ISC_R_SUCCESS) { + goto done; + } + + result = check_dnskey(&vctx); + if (result != ISC_R_SUCCESS) { + goto done; + } + + if (ignore_kskflag) { + if (!vctx.goodksk && !vctx.goodzsk) { + zoneverify_log_error(&vctx, "No %s DNSKEY found", + keydesc); + result = ISC_R_FAILURE; + goto done; + } + } else if (!vctx.goodksk) { + zoneverify_log_error(&vctx, "No %s KSK DNSKEY found", keydesc); + result = ISC_R_FAILURE; + goto done; + } + + determine_active_algorithms(&vctx, ignore_kskflag, keyset_kskonly, + report); + + result = verify_nodes(&vctx, &vresult); + if (result != ISC_R_SUCCESS) { + goto done; + } + + result = verify_nsec3_chains(&vctx, mctx); + if (vresult == ISC_R_UNSET) { + vresult = ISC_R_SUCCESS; + } + if (result != ISC_R_SUCCESS && vresult == ISC_R_SUCCESS) { + vresult = result; + } + + result = check_bad_algorithms(&vctx, report); + if (result != ISC_R_SUCCESS) { + report("DNSSEC completeness test failed."); + goto done; + } + + result = vresult; + if (result != ISC_R_SUCCESS) { + report("DNSSEC completeness test failed (%s).", + isc_result_totext(result)); + goto done; + } + + if (vctx.goodksk || ignore_kskflag) { + print_summary(&vctx, keyset_kskonly, report); + } + +done: + vctx_destroy(&vctx); + + return (result); +} |