[BACK]Return to dnssec-cds.c CVS log [TXT][DIR] Up to [cvs.NetBSD.org] / src / external / mpl / bind / dist / bin / dnssec

File: [cvs.NetBSD.org] / src / external / mpl / bind / dist / bin / dnssec / dnssec-cds.c (download)

Revision 1.1.1.3 (vendor branch), Sun Feb 24 18:56:43 2019 UTC (5 years, 1 month ago) by christos
Branch: ISC
CVS Tags: bind-9-13-7
Changes since 1.1.1.2: +1 -7 lines

	--- 9.13.7 released ---

5165.	[contrib]	Removed SDB drivers from contrib; they're obsolete.
			[GL #428]

5164.	[bug]		Correct errno to result translation in dlz filesystem
			modules. [GL #884]

5163.	[cleanup]	Out-of-tree builds failed --enable-dnstap. [GL #836]

5162.	[cleanup]	Improve dnssec-keymgr manual. Thanks to Tony Finch.
			[GL !1518]

5161.	[bug]		Do not require the SEP bit to be set for mirror zone
			trust anchors. [GL #873]

5160.	[contrib]	Added DNAME support to the DLZ LDAP schema. Also
			fixed a compilation bug affecting several DLZ
			modules. [GL #872]

5159.	[bug]		dnssec-coverage was incorrectly ignoring
			names specified on the command line without
			trailing dots. [GL !1478]

5158.	[protocol]	Add support for AMTRELAY and ZONEMD. [GL #867]

5157.	[bug]		Nslookup now errors out if there are extra command
			line arguments. [GL #207]

5141.	[security]	Zone transfer controls for writable DLZ zones were
			not effective as the allowzonexfr method was not being
			called for such zones. (CVE-2019-6465) [GL #790]

5118.	[security]	Named could crash if it is managing a key with
			`managed-keys` and the authoritative zone is rolling
			the key to an unsupported algorithm. (CVE-2018-5745)
			[GL #780]

5110.	[security]	Named leaked memory if there were multiple Key Tag
			EDNS options present. (CVE-2018-5744) [GL #772]

	--- 9.13.6 released ---

5156.	[doc]		Extended and refined the section of the ARM describing
			mirror zones. [GL #774]

5155.	[func]		"named -V" now outputs the default paths to
			named.conf, rndc.conf, bind.keys, and other
			files used or created by named and other tools, so
			that the correct paths to these files can quickly be
			determined regardless of the configure settings
			used when BIND was built. [GL #859]

5154.	[bug]		dig: process_opt could be called twice on the same
			message leading to a assertion failure. [GL #860]

5153.	[func]		Zone transfer statistics (size, number of records, and
			number of messages) are now logged for outgoing
			transfers as well as incoming ones. [GL #513]

5152.	[func]		Improved logging of DNSSEC key events:
			- Zone signing and DNSKEY maintenance events are
			  now logged to the "dnssec" category
			- Messages are now logged when DNSSEC keys are
			  pubished, activated, inactivated, deleted,
			  or revoked.
			[GL #714]

5151.	[func]		Options that have been been marked as obsolete in
			named.conf for a very long time are now fatal
			configuration errors. [GL #358]

5150.	[cleanup]	Remove the ability to compile BIND with assertions
			disabled. [GL #735]

5149.	[func]		"rndc dumpdb" now prints a line above a stale RRset
			indicating how long the data will be retained in the
			cache for emergency use. [GL #101]

5148.	[bug]		named did not sign the TKEY response. [GL #821]

5147.	[bug]		dnssec-keymgr: Add a five-minute margin to better
			handle key events close to 'now'. [GL #848]

5146.	[placeholder]

5145.	[func]		Use atomics instead of locked variables for isc_quota
			and isc_counter. [GL !1389]

5144.	[bug]		dig now returns a non-zero exit code when a TCP
			connection is prematurely closed by a peer more than
			once for the same lookup.  [GL #820]

5143.	[bug]		dnssec-keymgr and dnssec-coverage failed to find
			key files for zone names ending in ".". [GL #560]

5142.	[cleanup]	Removed "configure --disable-rpz-nsip" and
			"--disable-rpz-nsdname" options. "nsip-enable"
			and "nsdname-enable" both now default to yes,
			regardless of compile-time settings. [GL #824]

5140.	[bug]		Don't immediately mark existing keys as inactive and
			deleted when running dnssec-keymgr for the first
			time. [GL #117]

5139.	[bug]		If possible, don't use forwarders when priming.
			This ensures we can get root server IP addresses
			from priming query response glue, which may not
			be present if the forwarding server is returning
			minimal responses. [GL #752]

5138.	[bug]		Under some circumstances named could hit an assertion
			failure when doing qname minimization when using
			forwarders. [GL #797]

5137.	[func]		named now logs messages whenever a mirror zone becomes
			usable or unusable for resolution purposes. [GL #818]

5136.	[cleanup]	Check in named-checkconf that allow-update and
			allow-update-forwarding are not set at the
			view/options level; fix documentation. [GL #512]

5135.	[port]		sparc: Use smt_pause() instead of pause. [GL #816]

5134.	[bug]		win32: WSAStartup was not called before getservbyname
			was called. [GL #590]

5133.	[bug]		'rndc managed-keys' didn't handle class and view
			correctly and failed to add new lines between each
			view. [GL !1327]

5132.	[bug]		Fix race condition in cleanup part of dns_dt_create().
			[GL !1323]

5131.	[cleanup]	Address Coverity warnings. [GL #801]

5130.	[cleanup]	Remove support for l10n message catalogs. [GL #709]

5129.	[contrib]	sdlz_helper.c:build_querylist was not properly
			splitting the query string. [GL #798]

5128.	[bug]		Refreshkeytime was not being updated for managed
			keys zones. [GL #784]

5127.	[bug]		rcode.c:maybe_numeric failed to handle NUL in text
			regions. [GL #807]

5126.	[bug]		Named incorrectly accepted empty base64 and hex encoded
			fields when reading master files. [GL #807]

5125.	[bug]		Allow for up to 100 records or 64k of data when caching
			a negative response. [GL #804]

5124.	[bug]		Named could incorrectly return FORMERR rather than
			SERVFAIL. [GL #804]

5123.	[bug]		dig could hang indefinitely after encountering an error
			before creating a TCP socket. [GL #692]

5122.	[bug]		In a "forward first;" configuration, a forwarder
			timeout did not prevent that forwarder from being
			queried again after falling back to full recursive
			resolution. [GL #315]

5121.	[contrib]	dlz_stub_driver.c fails to return ISC_R_NOTFOUND on none
			matching zone names. [GL !1299]

5120.	[placeholder]

5119.	[placeholder]

5117.	[placeholder]

5116.	[bug]		Named/named-checkconf triggered a assertion when
			a mirror zone's name is bad. [GL #778]

5115.	[bug]		Allow unsupported algorithms in zone when not used for
			signing with dnssec-signzone. [GL #783]

5114.	[func]		Include a 'reconfig/reload in progress' status line
			in rndc status, use it in tests.

5113.	[port]		Fixed a Windows build error.

5112.	[bug]		Named/named-checkconf could dump core if there was
			a missing masters clause and a bad notify clause.
			[GL #779]

5111.	[bug]		Occluded DNSKEY records could make it into the
			delegating NSEC/NSEC3 bitmap. [GL #742]

5109.	[cleanup]	Remove support for RSAMD5 algorithm. [GL #628]

/*	$NetBSD: dnssec-cds.c,v 1.1.1.3 2019/02/24 18:56:43 christos Exp $	*/

/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * 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 http://mozilla.org/MPL/2.0/.
 *
 * See the COPYRIGHT file distributed with this work for additional
 * information regarding copyright ownership.
 */

/*
 * Written by Tony Finch <dot@dotat.at> <fanf2@cam.ac.uk>
 * at Cambridge University Information Services
 */

/*! \file */

#include <config.h>

#include <errno.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdlib.h>

#include <isc/buffer.h>
#include <isc/commandline.h>
#include <isc/file.h>
#include <isc/hash.h>
#include <isc/mem.h>
#include <isc/print.h>
#include <isc/serial.h>
#include <isc/string.h>
#include <isc/time.h>
#include <isc/util.h>

#include <dns/callbacks.h>
#include <dns/db.h>
#include <dns/dbiterator.h>
#include <dns/dnssec.h>
#include <dns/ds.h>
#include <dns/fixedname.h>
#include <dns/keyvalues.h>
#include <dns/log.h>
#include <dns/master.h>
#include <dns/name.h>
#include <dns/rdata.h>
#include <dns/rdataclass.h>
#include <dns/rdatalist.h>
#include <dns/rdataset.h>
#include <dns/rdatasetiter.h>
#include <dns/rdatatype.h>
#include <dns/result.h>
#include <dns/time.h>

#include <dst/dst.h>

#if USE_PKCS11
#include <pk11/result.h>
#endif

#include "dnssectool.h"

#ifndef PATH_MAX
#define PATH_MAX 1024   /* WIN32, and others don't define this. */
#endif

const char *program = "dnssec-cds";
int verbose;

/*
 * Infrastructure
 */
static isc_log_t *lctx = NULL;
static isc_mem_t *mctx = NULL;

/*
 * The domain we are working on
 */
static const char *namestr = NULL;
static dns_fixedname_t fixed;
static dns_name_t *name = NULL;
static dns_rdataclass_t rdclass = dns_rdataclass_in;

/*
 * List of digest types used by ds_from_cdnskey(), filled in by add_dtype()
 * from -a arguments. The size of the array is an arbitrary limit.
 */
static uint8_t dtype[8];

static const char *startstr  = NULL;	/* from which we derive notbefore */
static isc_stdtime_t notbefore = 0;	/* restrict sig inception times */
static dns_rdata_rrsig_t oldestsig;	/* for recording inception time */

static int nkey; /* number of child zone DNSKEY records */

/*
 * The validation strategy of this program is top-down.
 *
 * We start with an implicitly trusted authoritative dsset.
 *
 * The child DNSKEY RRset is scanned to find out which keys are
 * authenticated by DS records, and the result is recorded in a key
 * table as described later in this comment.
 *
 * The key table is used up to three times to verify the signatures on
 * the child DNSKEY, CDNSKEY, and CDS RRsets. In this program, only keys
 * that have matching DS records are used for validating signatures.
 *
 * For replay attack protection, signatures are ignored if their inception
 * time is before the previously recorded inception time. We use the earliest
 * signature so that another run of dnssec-cds with the same records will
 * still accept all the signatures.
 *
 * A key table is an array of nkey keyinfo structures, like
 *
 *	keyinfo_t key_tbl[nkey];
 *
 * Each key is decoded into more useful representations, held in
 *	keyinfo->rdata
 *	keyinfo->dst
 *
 * If a key has no matching DS record then keyinfo->dst is NULL.
 *
 * The key algorithm and ID are saved in keyinfo->algo and
 * keyinfo->tag for quicky skipping DS and RRSIG records that can't
 * match.
 */
typedef struct keyinfo {
	dns_rdata_t rdata;
	dst_key_t *dst;
	uint8_t algo;
	dns_keytag_t tag;
} keyinfo_t;

/* A replaceable function that can generate a DS RRset from some input */
typedef isc_result_t
ds_maker_func_t(dns_rdatalist_t *dslist, isc_buffer_t *buf, dns_rdata_t *rdata);

static dns_rdataset_t cdnskey_set, cdnskey_sig;
static dns_rdataset_t cds_set, cds_sig;
static dns_rdataset_t dnskey_set, dnskey_sig;
static dns_rdataset_t old_ds_set, new_ds_set;

static keyinfo_t *old_key_tbl, *new_key_tbl;

isc_buffer_t *new_ds_buf = NULL;	/* backing store for new_ds_set */

static void
verbose_time(int level, const char *msg, isc_stdtime_t time) {
	isc_result_t result;
	isc_buffer_t timebuf;
	char timestr[32];

	if (verbose < level) {
		return;
	}

	isc_buffer_init(&timebuf, timestr, sizeof(timestr));
	result = dns_time64_totext(time, &timebuf);
	check_result(result, "dns_time64_totext()");
	isc_buffer_putuint8(&timebuf, 0);
	if (verbose < 3) {
		vbprintf(level, "%s %s\n", msg, timestr);
	} else {
		vbprintf(level, "%s %s (%" PRIu32 ")\n",
			 msg, timestr, time);
	}
}

static void
initname(char *setname) {
	isc_result_t result;
	isc_buffer_t buf;

	name = dns_fixedname_initname(&fixed);
	namestr = setname;

	isc_buffer_init(&buf, setname, strlen(setname));
	isc_buffer_add(&buf, strlen(setname));
	result = dns_name_fromtext(name, &buf, dns_rootname, 0, NULL);
	if (result != ISC_R_SUCCESS) {
		fatal("could not initialize name %s", setname);
	}
}

static void
findset(dns_db_t *db, dns_dbnode_t *node, dns_rdatatype_t type,
	dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset)
{
	isc_result_t result;

	dns_rdataset_init(rdataset);
	if (sigrdataset != NULL) {
		dns_rdataset_init(sigrdataset);
	}
	result = dns_db_findrdataset(db, node, NULL, type, 0, 0,
				     rdataset, sigrdataset);
	if (result != ISC_R_NOTFOUND) {
		check_result(result, "dns_db_findrdataset()");
	}
}

static void
freeset(dns_rdataset_t *rdataset) {
	if (dns_rdataset_isassociated(rdataset)) {
		dns_rdataset_disassociate(rdataset);
	}
}

static void
freelist(dns_rdataset_t *rdataset) {
	dns_rdatalist_t *rdlist;
	dns_rdata_t *rdata;

	if (!dns_rdataset_isassociated(rdataset)) {
		return;
	}

	dns_rdatalist_fromrdataset(rdataset, &rdlist);

	for (rdata = ISC_LIST_HEAD(rdlist->rdata);
	     rdata != NULL;
	     rdata = ISC_LIST_HEAD(rdlist->rdata))
	{
		ISC_LIST_UNLINK(rdlist->rdata, rdata, link);
		isc_mem_put(mctx, rdata, sizeof(*rdata));
	}
	isc_mem_put(mctx, rdlist, sizeof(*rdlist));
	dns_rdataset_disassociate(rdataset);
}

static void
free_all_sets(void) {
	freeset(&cdnskey_set);
	freeset(&cdnskey_sig);
	freeset(&cds_set);
	freeset(&cds_sig);
	freeset(&dnskey_set);
	freeset(&dnskey_sig);
	freeset(&old_ds_set);
	freelist(&new_ds_set);
	if (new_ds_buf != NULL) {
		isc_buffer_free(&new_ds_buf);
	}
}

static void
load_db(const char *filename, dns_db_t **dbp, dns_dbnode_t **nodep) {
	isc_result_t result;

	result = dns_db_create(mctx, "rbt", name, dns_dbtype_zone,
			       rdclass, 0, NULL, dbp);
	check_result(result, "dns_db_create()");

	result = dns_db_load(*dbp, filename,
			     dns_masterformat_text, DNS_MASTER_HINT);
	if (result != ISC_R_SUCCESS && result != DNS_R_SEENINCLUDE) {
		fatal("can't load %s: %s", filename,
		      isc_result_totext(result));
	}

	result = dns_db_findnode(*dbp, name, false, nodep);
	if (result != ISC_R_SUCCESS) {
		fatal("can't find %s node in %s", namestr, filename);
	}
}

static void
free_db(dns_db_t **dbp, dns_dbnode_t **nodep) {
	dns_db_detachnode(*dbp, nodep);
	dns_db_detach(dbp);
}

static void
load_child_sets(const char *file) {
	dns_db_t *db = NULL;
	dns_dbnode_t *node = NULL;

	load_db(file, &db, &node);
	findset(db, node, dns_rdatatype_dnskey, &dnskey_set, &dnskey_sig);
	findset(db, node, dns_rdatatype_cdnskey, &cdnskey_set, &cdnskey_sig);
	findset(db, node, dns_rdatatype_cds, &cds_set, &cds_sig);
	free_db(&db, &node);
}

static void
get_dsset_name(char *filename, size_t size,
	       const char *path, const char *suffix)
{
	isc_result_t result;
	isc_buffer_t buf;
	size_t len;

	isc_buffer_init(&buf, filename, size);

	len = strlen(path);

	/* allow room for a trailing slash */
	if (isc_buffer_availablelength(&buf) <= len) {
		fatal("%s: pathname too long", path);
	}
	isc_buffer_putstr(&buf, path);

	if (isc_file_isdirectory(path) == ISC_R_SUCCESS) {
		const char *prefix = "dsset-";

		if (path[len - 1] != '/') {
			isc_buffer_putstr(&buf, "/");
		}

		if (isc_buffer_availablelength(&buf) < strlen(prefix)) {
			fatal("%s: pathname too long", path);
		}
		isc_buffer_putstr(&buf, prefix);

		result = dns_name_tofilenametext(name, false, &buf);
		check_result(result, "dns_name_tofilenametext()");
		if (isc_buffer_availablelength(&buf) == 0) {
			fatal("%s: pathname too long", path);
		}
	}
	/* allow room for a trailing nul */
	if (isc_buffer_availablelength(&buf) <= strlen(suffix)) {
		fatal("%s: pathname too long", path);
	}
	isc_buffer_putstr(&buf, suffix);
	isc_buffer_putuint8(&buf, 0);
}

static void
load_parent_set(const char *path) {
	isc_result_t result;
	dns_db_t *db   = NULL;
	dns_dbnode_t *node = NULL;
	isc_time_t modtime;
	char filename[PATH_MAX + 1];

	get_dsset_name(filename, sizeof(filename), path, "");

	result = isc_file_getmodtime(filename, &modtime);
	if (result != ISC_R_SUCCESS) {
		fatal("could not get modification time of %s: %s",
		      filename, isc_result_totext(result));
	}
	notbefore = isc_time_seconds(&modtime);
	if (startstr != NULL) {
		isc_stdtime_t now;
		isc_stdtime_get(&now);
		notbefore = strtotime(startstr, now, notbefore, NULL);
	}
	verbose_time(1, "child records must not be signed before", notbefore);

	load_db(filename, &db, &node);
	findset(db, node, dns_rdatatype_ds, &old_ds_set, NULL);

	if (!dns_rdataset_isassociated(&old_ds_set)) {
		fatal("could not find DS records for %s in %s",
		      namestr, filename);
	}

	free_db(&db, &node);
}

#define MAX_CDS_RDATA_TEXT_SIZE DNS_RDATA_MAXLENGTH * 2

static isc_buffer_t *
formatset(dns_rdataset_t *rdataset) {
	isc_result_t result;
	isc_buffer_t *buf = NULL;
	dns_master_style_t *style = NULL;
	unsigned int styleflags;

	styleflags = (rdataset->ttl == 0) ? DNS_STYLEFLAG_NO_TTL : 0;

	/*
	 * This style is for consistency with the output of dnssec-dsfromkey
	 * which just separates fields with spaces. The huge tab stop width
	 * eliminates any tab characters.
	 */
	result = dns_master_stylecreate(&style, styleflags,
					0, 0, 0, 0, 0, 1000000, 0,
					mctx);
	check_result(result, "dns_master_stylecreate2 failed");

	result = isc_buffer_allocate(mctx, &buf, MAX_CDS_RDATA_TEXT_SIZE);
	check_result(result, "printing DS records");
	result = dns_master_rdatasettotext(name, rdataset, style, buf);

	if ((result == ISC_R_SUCCESS) && isc_buffer_availablelength(buf) < 1) {
		result = ISC_R_NOSPACE;
	}

	check_result(result, "dns_rdataset_totext()");

	isc_buffer_putuint8(buf, 0);

	dns_master_styledestroy(&style, mctx);

	return (buf);
}

static void
write_parent_set(const char *path, const char *inplace,
		 bool nsupdate, dns_rdataset_t *rdataset)
{
	isc_result_t result;
	isc_buffer_t *buf = NULL;
	isc_region_t r;
	isc_time_t filetime;
	char backname[PATH_MAX + 1];
	char filename[PATH_MAX + 1];
	char tmpname[PATH_MAX + 1];
	FILE *fp = NULL;

	if (nsupdate && inplace == NULL) {
		return;
	}

	buf = formatset(rdataset);
	isc_buffer_usedregion(buf, &r);

	/*
	 * Try to ensure a write error doesn't make a zone go insecure!
	 */
	if (inplace == NULL) {
		printf("%s", (char *)r.base);
		isc_buffer_free(&buf);
		if (fflush(stdout) == EOF) {
			fatal("error writing to stdout: %s", strerror(errno));
		}
		return;
	}

	if (inplace[0] != '\0') {
		get_dsset_name(backname, sizeof(backname), path, inplace);
	}
	get_dsset_name(filename, sizeof(filename), path, "");
	get_dsset_name(tmpname, sizeof(tmpname), path, "-XXXXXXXXXX");

	result = isc_file_openunique(tmpname, &fp);
	if (result != ISC_R_SUCCESS) {
		fatal("open %s: %s", tmpname, isc_result_totext(result));
	}
	fprintf(fp, "%s", (char *)r.base);
	isc_buffer_free(&buf);
	if (fclose(fp) == EOF) {
		int err = errno;
		isc_file_remove(tmpname);
		fatal("error writing to %s: %s", tmpname, strerror(err));
	}

	isc_time_set(&filetime, oldestsig.timesigned, 0);
	result = isc_file_settime(tmpname, &filetime);
	if (result != ISC_R_SUCCESS) {
		isc_file_remove(tmpname);
		fatal("can't set modification time of %s: %s",
		      tmpname, isc_result_totext(result));
	}

	if (inplace[0] != '\0') {
		isc_file_rename(filename, backname);
	}
	isc_file_rename(tmpname, filename);
}

typedef enum { LOOSE, TIGHT } strictness_t;

/*
 * Find out if any (C)DS record matches a particular (C)DNSKEY.
 */
static bool
match_key_dsset(keyinfo_t *ki, dns_rdataset_t *dsset, strictness_t strictness)
{
	isc_result_t result;
	unsigned char dsbuf[DNS_DS_BUFFERSIZE];

	for (result = dns_rdataset_first(dsset);
	     result == ISC_R_SUCCESS;
	     result = dns_rdataset_next(dsset))
	{
		dns_rdata_ds_t ds;
		dns_rdata_t dsrdata = DNS_RDATA_INIT;
		dns_rdata_t newdsrdata = DNS_RDATA_INIT;
		bool c;

		dns_rdataset_current(dsset, &dsrdata);
		result = dns_rdata_tostruct(&dsrdata, &ds, NULL);
		check_result(result, "dns_rdata_tostruct(DS)");

		if (ki->tag != ds.key_tag || ki->algo != ds.algorithm) {
			continue;
		}

		result = dns_ds_buildrdata(name, &ki->rdata, ds.digest_type,
					   dsbuf, &newdsrdata);
		if (result != ISC_R_SUCCESS) {
			vbprintf(3, "dns_ds_buildrdata("
				 "keytag=%d, algo=%d, digest=%d): %s\n",
				 ds.key_tag, ds.algorithm, ds.digest_type,
				 dns_result_totext(result));
			continue;
		}
		/* allow for both DS and CDS */
		c = dsrdata.type != dns_rdatatype_ds;
		dsrdata.type = dns_rdatatype_ds;
		if (dns_rdata_compare(&dsrdata, &newdsrdata) == 0) {
			vbprintf(1, "found matching %s %d %d %d\n",
				 c ? "CDS" : "DS",
				 ds.key_tag, ds.algorithm, ds.digest_type);
			return (true);
		} else if (strictness == TIGHT) {
			vbprintf(0, "key does not match %s %d %d %d "
				"when it looks like it should\n",
				 c ? "CDS" : "DS",
				 ds.key_tag, ds.algorithm, ds.digest_type);
			return (false);
		}
	}

	vbprintf(1, "no matching %s for %s %d %d\n",
		 dsset->type == dns_rdatatype_cds
		 ? "CDS" : "DS",
		 ki->rdata.type == dns_rdatatype_cdnskey
		 ? "CDNSKEY" : "DNSKEY",
		 ki->tag, ki->algo);

	return (false);
}

/*
 * Find which (C)DNSKEY records match a (C)DS RRset.
 * This creates a keyinfo_t key_tbl[nkey] array.
 */
static keyinfo_t *
match_keyset_dsset(dns_rdataset_t *keyset, dns_rdataset_t *dsset,
		   strictness_t strictness)
{
	isc_result_t result;
	keyinfo_t *keytable;
	int i;

	nkey = dns_rdataset_count(keyset);

	keytable = isc_mem_get(mctx, sizeof(keyinfo_t) * nkey);
	if (keytable == NULL) {
		fatal("out of memory");
	}

	for (result = dns_rdataset_first(keyset), i = 0;
	     result == ISC_R_SUCCESS;
	     result = dns_rdataset_next(keyset), i++)
	{
		keyinfo_t *ki;
		dns_rdata_dnskey_t dnskey;
		dns_rdata_t *keyrdata;
		isc_region_t r;

		INSIST(i < nkey);
		ki = &keytable[i];
		keyrdata = &ki->rdata;

		dns_rdata_init(keyrdata);
		dns_rdataset_current(keyset, keyrdata);

		result = dns_rdata_tostruct(keyrdata, &dnskey, NULL);
		check_result(result, "dns_rdata_tostruct(DNSKEY)");
		ki->algo = dnskey.algorithm;

		dns_rdata_toregion(keyrdata, &r);
		ki->tag = dst_region_computeid(&r);

		ki->dst = NULL;
		if (!match_key_dsset(ki, dsset, strictness)) {
			continue;
		}

		result = dns_dnssec_keyfromrdata(name, keyrdata,
						 mctx, &ki->dst);
		if (result != ISC_R_SUCCESS) {
			vbprintf(3, "dns_dnssec_keyfromrdata("
				 "keytag=%d, algo=%d): %s\n",
				 ki->tag, ki->algo,
				 dns_result_totext(result));
		}
	}

	return (keytable);
}

static void
free_keytable(keyinfo_t **keytable_p) {
	keyinfo_t *keytable = *keytable_p;
	keyinfo_t *ki;
	int i;

	for (i = 0; i < nkey; i++) {
		ki = &keytable[i];
		if (ki->dst != NULL) {
			dst_key_free(&ki->dst);
		}
	}

	isc_mem_put(mctx, keytable, sizeof(keyinfo_t) * nkey);
	*keytable_p = NULL;
}

/*
 * Find out which keys have signed an RRset. Keys that do not match a
 * DS record are skipped.
 *
 * The return value is an array with nkey elements, one for each key,
 * either zero if the key was skipped or did not sign the RRset, or
 * otherwise the key algorithm. This is used by the signature coverage
 * check functions below.
 */
static uint8_t *
matching_sigs(keyinfo_t *keytbl, dns_rdataset_t *rdataset,
	      dns_rdataset_t *sigset)
{
	isc_result_t result;
	uint8_t *algo;
	int i;

	algo = isc_mem_get(mctx, nkey);
	if (algo == NULL) {
		fatal("allocating RRSIG/DNSKEY match list: %s",
		      isc_result_totext(ISC_R_NOMEMORY));
	}
	memset(algo, 0, nkey);

	for (result = dns_rdataset_first(sigset);
	     result == ISC_R_SUCCESS;
	     result = dns_rdataset_next(sigset))
	{
		dns_rdata_t sigrdata = DNS_RDATA_INIT;
		dns_rdata_rrsig_t sig;

		dns_rdataset_current(sigset, &sigrdata);
		result = dns_rdata_tostruct(&sigrdata, &sig, NULL);
		check_result(result, "dns_rdata_tostruct(RRSIG)");

		/*
		 * Replay attack protection: check against current age limit
		 */
		if (isc_serial_lt(sig.timesigned, notbefore)) {
			vbprintf(1, "skip RRSIG by key %d: too old\n",
				 sig.keyid);
			continue;
		}

		for (i = 0; i < nkey; i++) {
			keyinfo_t *ki = &keytbl[i];
			if (sig.keyid != ki->tag ||
			    sig.algorithm != ki->algo ||
			    !dns_name_equal(&sig.signer, name))
			{
				continue;
			}
			if (ki->dst == NULL) {
				vbprintf(1, "skip RRSIG by key %d:"
					 " no matching (C)DS\n",
					 sig.keyid);
				continue;
			}

			result = dns_dnssec_verify(name, rdataset, ki->dst,
						   false, 0, mctx,
						   &sigrdata, NULL);

			if (result != ISC_R_SUCCESS &&
			    result != DNS_R_FROMWILDCARD) {
				vbprintf(1, "skip RRSIG by key %d:"
					 " verification failed: %s\n",
					 sig.keyid, isc_result_totext(result));
				continue;
			}

			vbprintf(1, "found RRSIG by key %d\n", ki->tag);
			algo[i] = sig.algorithm;

			/*
			 * Replay attack protection: work out next age limit,
			 * only after the signature has been verified
			 */
			if (oldestsig.timesigned == 0 ||
			    isc_serial_lt(sig.timesigned,
					  oldestsig.timesigned))
			{
				verbose_time(2, "this is the oldest so far",
					     sig.timesigned);
				oldestsig = sig;
			}
		}
	}

	return (algo);
}

/*
 * Consume the result of matching_sigs(). When checking records
 * fetched from the child zone, any working signature is enough.
 */
static bool
signed_loose(uint8_t *algo) {
	bool ok = false;
	int i;
	for (i = 0; i < nkey; i++) {
		if (algo[i] != 0) {
			ok = true;
		}
	}
	isc_mem_put(mctx, algo, nkey);
	return (ok);
}

/*
 * Consume the result of matching_sigs(). To ensure that the new DS
 * RRset does not break the chain of trust to the DNSKEY RRset, every
 * key algorithm in the DS RRset must have a signature in the DNSKEY
 * RRset.
 */
static bool
signed_strict(dns_rdataset_t *dsset, uint8_t *algo) {
	isc_result_t result;
	bool all_ok = true;

	for (result = dns_rdataset_first(dsset);
	     result == ISC_R_SUCCESS;
	     result = dns_rdataset_next(dsset))
	{
		dns_rdata_t dsrdata = DNS_RDATA_INIT;
		dns_rdata_ds_t ds;
		bool ds_ok;
		int i;

		dns_rdataset_current(dsset, &dsrdata);
		result = dns_rdata_tostruct(&dsrdata, &ds, NULL);
		check_result(result, "dns_rdata_tostruct(DS)");

		ds_ok = false;
		for (i = 0; i < nkey; i++) {
			if (algo[i] == ds.algorithm) {
				ds_ok = true;
			}
		}
		if (!ds_ok) {
			vbprintf(0, "missing signature for algorithm %d "
				 "(key %d)\n", ds.algorithm, ds.key_tag);
			all_ok = false;
		}
	}

	isc_mem_put(mctx, algo, nkey);
	return (all_ok);
}

static dns_rdata_t *
rdata_get(void) {
	dns_rdata_t *rdata;

	rdata = isc_mem_get(mctx, sizeof(*rdata));
	if (rdata == NULL) {
		fatal("allocating DS rdata: %s",
		      isc_result_totext(ISC_R_NOMEMORY));
	}
	dns_rdata_init(rdata);

	return (rdata);
}

static isc_result_t
rdata_put(isc_result_t result, dns_rdatalist_t *rdlist, dns_rdata_t *rdata) {
	if (result == ISC_R_SUCCESS) {
		ISC_LIST_APPEND(rdlist->rdata, rdata, link);
	} else {
		isc_mem_put(mctx, rdata, sizeof(*rdata));
	}

	return (result);
}

/*
 * This basically copies the rdata into the buffer, but going via the
 * unpacked struct has the side-effect of changing the rdatatype. The
 * dns_rdata_cds_t and dns_rdata_ds_t types are aliases.
 */
static isc_result_t
ds_from_cds(dns_rdatalist_t *dslist, isc_buffer_t *buf, dns_rdata_t *cds) {
	isc_result_t result;
	dns_rdata_ds_t ds;
	dns_rdata_t *rdata;

	rdata = rdata_get();

	result = dns_rdata_tostruct(cds, &ds, NULL);
	check_result(result, "dns_rdata_tostruct(CDS)");
	ds.common.rdtype = dns_rdatatype_ds;

	result = dns_rdata_fromstruct(rdata, rdclass, dns_rdatatype_ds,
				      &ds, buf);

	return (rdata_put(result, dslist, rdata));
}

static isc_result_t
ds_from_cdnskey(dns_rdatalist_t *dslist, isc_buffer_t *buf,
		 dns_rdata_t *cdnskey)
{
	isc_result_t result;
	unsigned i, n;

	n = sizeof(dtype)/sizeof(dtype[0]);
	for (i = 0; i < n; i++) {
		if (dtype[i] != 0) {
			dns_rdata_t *rdata;
			isc_region_t r;

			isc_buffer_availableregion(buf, &r);
			if (r.length < DNS_DS_BUFFERSIZE) {
				return (ISC_R_NOSPACE);
			}

			rdata = rdata_get();
			result = dns_ds_buildrdata(name, cdnskey, dtype[i],
						   r.base, rdata);
			if (result == ISC_R_SUCCESS) {
				isc_buffer_add(buf, DNS_DS_BUFFERSIZE);
			}

			result = rdata_put(result, dslist, rdata);
			if (result != ISC_R_SUCCESS) {
				return (result);
			}
		}
	}

	return (ISC_R_SUCCESS);
}

/*
 * For sorting the digest types so that DS records generated
 * from CDNSKEY records are in canonical order.
 */
static int
cmp_dtype(const void *ap, const void *bp) {
	int a = *(const uint8_t *)ap;
	int b = *(const uint8_t *)bp;
	return (a - b);
}

static void
add_dtype(const char *dn) {
	uint8_t dt;
	unsigned i, n;

	dt = strtodsdigest(dn);
	n = sizeof(dtype)/sizeof(dtype[0]);
	for (i = 0; i < n; i++) {
		if (dtype[i] == 0 || dtype[i] == dt) {
			dtype[i] = dt;
			qsort(dtype, i+1, 1, cmp_dtype);
			return;
		}
	}
	fatal("too many -a digest type arguments");
}

static void
make_new_ds_set(ds_maker_func_t *ds_from_rdata,
		uint32_t ttl, dns_rdataset_t *rdset)
{
	unsigned int size = 16;
	for (;;) {
		isc_result_t result;
		dns_rdatalist_t *dslist;

		dslist = isc_mem_get(mctx, sizeof(*dslist));
		if (dslist == NULL) {
			fatal("allocating new DS list: %s",
			      isc_result_totext(ISC_R_NOMEMORY));
		}

		dns_rdatalist_init(dslist);
		dslist->rdclass = rdclass;
		dslist->type = dns_rdatatype_ds;
		dslist->ttl = ttl;

		dns_rdataset_init(&new_ds_set);
		result = dns_rdatalist_tordataset(dslist, &new_ds_set);
		check_result(result, "dns_rdatalist_tordataset(dslist)");

		result = isc_buffer_allocate(mctx, &new_ds_buf, size);
		check_result(result, "building new DS records");

		for (result = dns_rdataset_first(rdset);
		     result == ISC_R_SUCCESS;
		     result = dns_rdataset_next(rdset))
		{
			isc_result_t tresult;
			dns_rdata_t rdata = DNS_RDATA_INIT;

			dns_rdataset_current(rdset, &rdata);

			tresult = ds_from_rdata(dslist, new_ds_buf, &rdata);
			if (tresult == ISC_R_NOSPACE) {
				vbprintf(20, "DS list buffer size %u\n", size);
				freelist(&new_ds_set);
				isc_buffer_free(&new_ds_buf);
				size *= 2;
				break;
			}

			check_result(tresult, "ds_from_rdata()");
		}

		if (result == ISC_R_NOMORE) {
			break;
		}
	}
}

static inline int
rdata_cmp(const void *rdata1, const void *rdata2) {
	return (dns_rdata_compare((const dns_rdata_t *)rdata1,
				  (const dns_rdata_t *)rdata2));
}

/*
 * Ensure that every key identified by the DS RRset has the same set of
 * digest types.
 */
static bool
consistent_digests(dns_rdataset_t *dsset) {
	isc_result_t result;
	dns_rdata_t *arrdata;
	dns_rdata_ds_t *ds;
	dns_keytag_t key_tag;
	uint8_t algorithm;
	bool match;
	int i, j, n, d;

	/*
	 * First sort the dsset. DS rdata fields are tag, algorithm, digest,
	 * so sorting them brings together all the records for each key.
	 */

	n = dns_rdataset_count(dsset);

	arrdata = isc_mem_get(mctx, n * sizeof(dns_rdata_t));
	if (arrdata == NULL) {
		fatal("allocating DS rdata array: %s",
		      isc_result_totext(ISC_R_NOMEMORY));
	}

	for (result = dns_rdataset_first(dsset), i = 0;
	     result == ISC_R_SUCCESS;
	     result = dns_rdataset_next(dsset), i++)
	{
		dns_rdata_init(&arrdata[i]);
		dns_rdataset_current(dsset, &arrdata[i]);
	}

	qsort(arrdata, n, sizeof(dns_rdata_t), rdata_cmp);

	/*
	 * Convert sorted arrdata to more accessible format
	 */
	ds = isc_mem_get(mctx, n * sizeof(dns_rdata_ds_t));
	if (ds == NULL) {
		fatal("allocating unpacked DS array: %s",
		      isc_result_totext(ISC_R_NOMEMORY));
	}

	for (i = 0; i < n; i++) {
		result = dns_rdata_tostruct(&arrdata[i], &ds[i], NULL);
		check_result(result, "dns_rdata_tostruct(DS)");
	}

	/*
	 * Count number of digest types (d) for first key
	 */
	key_tag = ds[0].key_tag;
	algorithm = ds[0].algorithm;
	for (d = 0, i = 0; i < n; i++, d++) {
		if (ds[i].key_tag != key_tag || ds[i].algorithm != algorithm) {
			break;
		}
	}

	/*
	 * Check subsequent keys match the first one
	 */
	match = true;
	while (i < n) {
		key_tag = ds[i].key_tag;
		algorithm = ds[i].algorithm;
		for (j = 0; j < d && i+j < n; j++) {
			if (ds[i+j].key_tag != key_tag ||
			    ds[i+j].algorithm != algorithm ||
			    ds[i+j].digest_type != ds[j].digest_type)
			{
				match = false;
			}
		}
		i += d;
	}

	/*
	 * Done!
	 */
	isc_mem_put(mctx, ds, n * sizeof(dns_rdata_ds_t));
	isc_mem_put(mctx, arrdata, n * sizeof(dns_rdata_t));

	return (match);
}

static void
print_diff(const char *cmd, dns_rdataset_t *rdataset) {
	isc_buffer_t *buf;
	isc_region_t r;
	unsigned char *nl;
	size_t len;

	buf = formatset(rdataset);
	isc_buffer_usedregion(buf, &r);

	while ((nl = memchr(r.base, '\n', r.length)) != NULL) {
		len = nl - r.base + 1;
		printf("update %s %.*s", cmd, (int)len, (char *)r.base);
		isc_region_consume(&r, len);
	}

	isc_buffer_free(&buf);
}

static void
update_diff(const char *cmd, uint32_t ttl,
	    dns_rdataset_t *addset, dns_rdataset_t *delset)
{
	isc_result_t result;
	dns_db_t *db;
	dns_dbnode_t *node;
	dns_dbversion_t *ver;
	dns_rdataset_t diffset;
	uint32_t save;

	db = NULL;
	result = dns_db_create(mctx, "rbt", name, dns_dbtype_zone,
			       rdclass, 0, NULL, &db);
	check_result(result, "dns_db_create()");

	ver = NULL;
	result = dns_db_newversion(db, &ver);
	check_result(result, "dns_db_newversion()");

	node = NULL;
	result = dns_db_findnode(db, name, true, &node);
	check_result(result, "dns_db_findnode()");

	dns_rdataset_init(&diffset);

	result = dns_db_addrdataset(db, node, ver, 0, addset,
				    DNS_DBADD_MERGE, NULL);
	check_result(result, "dns_db_addrdataset()");

	result = dns_db_subtractrdataset(db, node, ver, delset,
					 0, &diffset);
	if (result == DNS_R_UNCHANGED) {
		save = addset->ttl;
		addset->ttl = ttl;
		print_diff(cmd, addset);
		addset->ttl = save;
	} else if (result != DNS_R_NXRRSET) {
		check_result(result, "dns_db_subtractrdataset()");
		diffset.ttl = ttl;
		print_diff(cmd, &diffset);
		dns_rdataset_disassociate(&diffset);
	}

	dns_db_detachnode(db, &node);
	dns_db_closeversion(db, &ver, false);
	dns_db_detach(&db);
}

static void
nsdiff(uint32_t ttl, dns_rdataset_t *oldset, dns_rdataset_t *newset) {
	if (ttl == 0) {
		vbprintf(1, "warning: no TTL in nsupdate script\n");
	}
	update_diff("add", ttl, newset, oldset);
	update_diff("del", 0, oldset, newset);
	if (verbose > 0) {
		printf("show\nsend\nanswer\n");
	} else {
		printf("send\n");
	}
	if (fflush(stdout) == EOF) {
		fatal("write stdout: %s", strerror(errno));
	}
}

ISC_PLATFORM_NORETURN_PRE static void
usage(void) ISC_PLATFORM_NORETURN_POST;

static void
usage(void) {
	fprintf(stderr, "Usage:\n");
	fprintf(stderr,
		"    %s options [options] -f <file> -d <path> <domain>\n",
		program);
	fprintf(stderr, "Version: %s\n", VERSION);
	fprintf(stderr, "Options:\n"
"    -a <algorithm>     digest algorithm (SHA-1 / SHA-256 / SHA-384)\n"
"    -c <class>         of domain (default IN)\n"
"    -D                 prefer CDNSKEY records instead of CDS\n"
"    -d <file|dir>      where to find parent dsset- file\n"
"    -f <file>          child DNSKEY+CDNSKEY+CDS+RRSIG records\n"
"    -i[extension]      update dsset- file in place\n"
"    -s <start-time>    oldest permitted child signatures\n"
"    -u                 emit nsupdate script\n"
"    -T <ttl>           TTL of DS records\n"
"    -V                 print version\n"
"    -v <verbosity>\n"
	);
	exit(1);
}

int
main(int argc, char *argv[]) {
	const char *child_path = NULL;
	const char *ds_path = NULL;
	const char *inplace = NULL;
	isc_result_t result;
	bool prefer_cdnskey = false;
	bool nsupdate = false;
	uint32_t ttl = 0;
	int ch;
	char *endp;

	result = isc_mem_create(0, 0, &mctx);
	if (result != ISC_R_SUCCESS) {
		fatal("out of memory");
	}

#if USE_PKCS11
	pk11_result_register();
#endif
	dns_result_register();

	isc_commandline_errprint = false;

#define OPTIONS "a:c:Dd:f:i:ms:T:uv:V"
	while ((ch = isc_commandline_parse(argc, argv, OPTIONS)) != -1) {
		switch (ch) {
		case 'a':
			add_dtype(isc_commandline_argument);
			break;
		case 'c':
			rdclass = strtoclass(isc_commandline_argument);
			break;
		case 'D':
			prefer_cdnskey = true;
			break;
		case 'd':
			ds_path = isc_commandline_argument;
			break;
		case 'f':
			child_path = isc_commandline_argument;
			break;
		case 'i':
			/*
			 * This is a bodge to make the argument optional,
			 * so that it works just like sed(1).
			 */
			if (isc_commandline_argument ==
			    argv[isc_commandline_index - 1])
			{
				isc_commandline_index--;
				inplace = "";
			} else {
				inplace = isc_commandline_argument;
			}
			break;
		case 'm':
			isc_mem_debugging = ISC_MEM_DEBUGTRACE |
				ISC_MEM_DEBUGRECORD;
			break;
		case 's':
			startstr = isc_commandline_argument;
			break;
		case 'T':
			ttl = strtottl(isc_commandline_argument);
			break;
		case 'u':
			nsupdate = true;
			break;
		case 'V':
			/* Does not return. */
			version(program);
			break;
		case 'v':
			verbose = strtoul(isc_commandline_argument, &endp, 0);
			if (*endp != '\0') {
				fatal("-v must be followed by a number");
			}
			break;
		default:
			usage();
			break;
		}
	}
	argv += isc_commandline_index;
	argc -= isc_commandline_index;

	if (argc != 1) {
		usage();
	}
	initname(argv[0]);

	/*
	 * Default digest type if none specified.
	 */
	if (dtype[0] == 0) {
		dtype[0] = DNS_DSDIGEST_SHA256;
	}

	setup_logging(mctx, &lctx);

	result = dst_lib_init(mctx, NULL);
	if (result != ISC_R_SUCCESS) {
		fatal("could not initialize dst: %s",
		      isc_result_totext(result));
	}

	if (ds_path == NULL) {
		fatal("missing -d DS pathname");
	}
	load_parent_set(ds_path);

	/*
	 * Preserve the TTL if it wasn't overridden.
	 */
	if (ttl == 0) {
		ttl = old_ds_set.ttl;
	}

	if (child_path == NULL) {
		fatal("path to file containing child data must be specified");
	}

	load_child_sets(child_path);

	/*
	 * Check child records have accompanying RRSIGs and DNSKEYs
	 */

	if (!dns_rdataset_isassociated(&dnskey_set) ||
	    !dns_rdataset_isassociated(&dnskey_sig))
	{
		fatal("could not find signed DNSKEY RRset for %s", namestr);
	}

	if (dns_rdataset_isassociated(&cdnskey_set) &&
	    !dns_rdataset_isassociated(&cdnskey_sig))
	{
		fatal("missing RRSIG CDNSKEY records for %s", namestr);
	}
	if (dns_rdataset_isassociated(&cds_set) &&
	    !dns_rdataset_isassociated(&cds_sig))
	{
		fatal("missing RRSIG CDS records for %s", namestr);
	}

	vbprintf(1, "which child DNSKEY records match parent DS records?\n");
	old_key_tbl = match_keyset_dsset(&dnskey_set, &old_ds_set, LOOSE);

	/*
	 * We have now identified the keys that are allowed to authenticate
	 * the DNSKEY RRset (RFC 4035 section 5.2 bullet 2), and CDNSKEY and
	 * CDS RRsets (RFC 7344 section 4.1 bullet 2).
	 */

	vbprintf(1, "verify DNSKEY signature(s)\n");
	if (!signed_loose(matching_sigs(old_key_tbl, &dnskey_set, &dnskey_sig)))
	{
		fatal("could not validate child DNSKEY RRset for %s", namestr);
	}

	if (dns_rdataset_isassociated(&cdnskey_set)) {
		vbprintf(1, "verify CDNSKEY signature(s)\n");
		if (!signed_loose(matching_sigs(old_key_tbl,
						&cdnskey_set, &cdnskey_sig)))
		{
			fatal("could not validate child CDNSKEY RRset for %s",
			      namestr);
		}
	}
	if (dns_rdataset_isassociated(&cds_set)) {
		vbprintf(1, "verify CDS signature(s)\n");
		if (!signed_loose(matching_sigs(old_key_tbl,
						&cds_set, &cds_sig)))
		{
			fatal("could not validate child CDS RRset for %s",
			      namestr);
		}
	}

	free_keytable(&old_key_tbl);

	/*
	 * Report the result of the replay attack protection checks
	 * used for the output file timestamp
	 */
	if (oldestsig.timesigned != 0 && verbose > 0) {
		char type[32];
		dns_rdatatype_format(oldestsig.covered, type, sizeof(type));
		verbose_time(1, "child signature inception time",
			     oldestsig.timesigned);
		vbprintf(2, "from RRSIG %s by key %d\n",
			 type, oldestsig.keyid);
	}

	/*
	 * Sucessfully do nothing if there's neither CDNSKEY nor CDS
	 * RFC 7344 section 4.1 first paragraph
	 */
	if (!dns_rdataset_isassociated(&cdnskey_set) &&
	    !dns_rdataset_isassociated(&cds_set))
	{
		vbprintf(1, "%s has neither CDS nor CDNSKEY records\n",
			 namestr);
		write_parent_set(ds_path, inplace, nsupdate, &old_ds_set);
		exit(0);
	}

	/*
	 * Make DS records from the CDS or CDNSKEY records
	 * Prefer CDS if present, unless run with -D
	 */
	if (prefer_cdnskey && dns_rdataset_isassociated(&cdnskey_set)) {
		make_new_ds_set(ds_from_cdnskey, ttl, &cdnskey_set);
	} else if (dns_rdataset_isassociated(&cds_set)) {
		make_new_ds_set(ds_from_cds, ttl, &cds_set);
	} else {
		make_new_ds_set(ds_from_cdnskey, ttl, &cdnskey_set);
	}

	/*
	 * Now we have a candidate DS RRset, we need to check it
	 * won't break the delegation.
	 */
	vbprintf(1, "which child DNSKEY records match new DS records?\n");
	new_key_tbl = match_keyset_dsset(&dnskey_set, &new_ds_set, TIGHT);

	if (!consistent_digests(&new_ds_set)) {
		fatal("CDS records at %s do not cover each key "
		      "with the same set of digest types", namestr);
	}

	vbprintf(1, "verify DNSKEY signature(s)\n");
	if (!signed_strict(&new_ds_set,
			   matching_sigs(new_key_tbl,
					 &dnskey_set, &dnskey_sig)))
	{
		fatal("could not validate child DNSKEY RRset "
		      "with new DS records for %s", namestr);
	}

	free_keytable(&new_key_tbl);

	/*
	 * OK, it's all good!
	 */
	if (nsupdate) {
		nsdiff(ttl, &old_ds_set, &new_ds_set);
	}

	write_parent_set(ds_path, inplace, nsupdate, &new_ds_set);

	free_all_sets();
	cleanup_logging(&lctx);
	dst_lib_destroy();
	if (verbose > 10) {
		isc_mem_stats(mctx, stdout);
	}
	isc_mem_destroy(&mctx);

	exit(0);
}