/*	$NetBSD: zoneverify.c,v 1.13 2025/01/26 16:25:26 christos Exp $	*/

/*
 * 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);
	*element = (struct nsec3_chain_fixed){
		.hash = nsec3->hash,
		.salt_length = nsec3->salt_length,
		.next_length = nsec3->next_length,
		.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 &&
		    (!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) {
			if (rdataset.type == dns_rdatatype_ns) {
				dns_nsec_setbit(types, rdataset.type, 1);
				if (rdataset.type > maxtype) {
					maxtype = rdataset.type;
				}
			}
			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);
			if (rdataset.type > maxtype) {
				maxtype = rdataset.type;
			}
		}
		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) {
	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);

	return result;
}

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;
	}

	sr.base = UNCONST(d1 - first->next_length);
	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);

	sr.base = UNCONST(d1);
	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);

	sr.base = UNCONST(d2);
	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_keynode_detach(&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_cget(vctx->mctx, count, sizeof(*dstkeys));

	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) {
			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);
			dns_db_detachnode(vctx->db, &nextnode);
			switch (result) {
			case ISC_R_SUCCESS:
				break;
			case ISC_R_NOMORE:
				result = dns_dbiterator_next(dbiter);
				continue;
			default:
				dns_db_detachnode(vctx->db, &node);
			}
			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_cput(vctx->mctx, dstkeys, count, sizeof(*dstkeys));
	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;
}
