/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  For more information, see <https://www.knot-dns.cz/>
 */

#include <assert.h>
#include <limits.h>
#include <stdio.h>

#include "libdnssec/error.h"
#include "knot/common/log.h"
#include "knot/dnssec/zone-keys.h"
#include "libknot/libknot.h"
#include "contrib/openbsd/strlcat.h"

#define MAX_KEY_INFO 128

typedef struct {
	char msg[MAX_KEY_INFO];
	knot_time_t key_time;
} key_info_t;

knot_dynarray_define(keyptr, zone_key_t *, DYNARRAY_VISIBILITY_NORMAL)

void normalize_generate_flags(kdnssec_generate_flags_t *flags)
{
	if (!(*flags & DNSKEY_GENERATE_KSK) && !(*flags & DNSKEY_GENERATE_ZSK)) {
		*flags |= DNSKEY_GENERATE_ZSK;
	}
	if (!(*flags & DNSKEY_GENERATE_SEP_SPEC)) {
		if ((*flags & DNSKEY_GENERATE_KSK)) {
			*flags |= DNSKEY_GENERATE_SEP_ON;
		} else {
			*flags &= ~DNSKEY_GENERATE_SEP_ON;
		}
	}
}

static int generate_dnssec_key(dnssec_keystore_t *keystore,
                               const knot_dname_t *zone_name,
                               const char *key_label,
                               dnssec_key_algorithm_t alg,
                               unsigned size,
                               kdnssec_generate_flags_t flags,
                               char **id,
                               dnssec_key_t **key)
{
	*key = NULL;
	*id = NULL;

	int ret = dnssec_keystore_generate(keystore, alg, size, key_label, id);
	if (ret != KNOT_EOK) {
		return ret;
	}

	ret = dnssec_key_new(key);
	if (ret != KNOT_EOK) {
		goto fail;
	}

	ret = dnssec_key_set_dname(*key, zone_name);
	if (ret != KNOT_EOK) {
		goto fail;
	}

	dnssec_key_set_flags(*key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON));
	dnssec_key_set_algorithm(*key, alg);

	ret = dnssec_keystore_get_private(keystore, *id, *key);
	if (ret != KNOT_EOK) {
		goto fail;
	}

	return KNOT_EOK;

fail:
	(void)dnssec_keystore_remove(keystore, *id);
	dnssec_key_free(*key);
	*key = NULL;
	free(*id);
	*id = NULL;
	return ret;
}

static bool keytag_in_use(kdnssec_ctx_t *ctx, uint16_t keytag)
{
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		uint16_t used = dnssec_key_get_keytag(ctx->zone->keys[i].key);
		if (used == keytag) {
			return true;
		}
	}
	return false;
}

#define GENERATE_KEYTAG_ATTEMPTS (40)

static int generate_keytag_unconflict(kdnssec_ctx_t *ctx,
                                      knot_kasp_keystore_t *keystore,
                                      kdnssec_generate_flags_t flags,
                                      char **id,
                                      dnssec_key_t **key)
{
	unsigned size = (flags & DNSKEY_GENERATE_KSK) ? ctx->policy->ksk_size :
	                                                ctx->policy->zsk_size;

	const char *label = NULL;

	char label_buf[sizeof(knot_dname_txt_storage_t) + 16];
	if (keystore->key_label &&
	    knot_dname_to_str(label_buf, ctx->zone->dname, sizeof(label_buf)) != NULL) {
		const char *key_type = (flags & DNSKEY_GENERATE_KSK) ? " KSK" : " ZSK" ;
		strlcat(label_buf, key_type, sizeof(label_buf));
		label = label_buf;
	}

	for (size_t i = 0; i < GENERATE_KEYTAG_ATTEMPTS; i++) {
		int ret = generate_dnssec_key(keystore->keystore, ctx->zone->dname, label,
		                              ctx->policy->algorithm, size, flags,
		                              id, key);
		if (ret != KNOT_EOK) {
			return ret;
		}
		uint16_t keytag = dnssec_key_get_keytag(*key);
		if (!keytag_in_use(ctx, keytag) &&
		    keytag % ctx->policy->keytag_modulo == ctx->policy->keytag_remain) {
			return KNOT_EOK;
		}

		(void)dnssec_keystore_remove(keystore->keystore, *id);
		dnssec_key_free(*key);
		free(*id);
	}

	log_zone_notice(ctx->zone->dname, "DNSSEC, generated key with conflicting keytag %hu",
	                dnssec_key_get_keytag(*key));
	return KNOT_EOK;
}

static bool key_corresponds(knot_kasp_key_t *key, kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags)
{
	return !key->is_pub_only &&
	       key->is_ksk == (bool)(flags & DNSKEY_GENERATE_KSK) &&
	       key->is_zsk == (bool)(flags & DNSKEY_GENERATE_ZSK) &&
	       dnssec_key_get_algorithm(key->key) == ctx->policy->algorithm &&
	       dnssec_key_get_size(key->key) == (key->is_ksk ? ctx->policy->ksk_size : ctx->policy->zsk_size);
}

int kdnssec_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags,
			 knot_kasp_key_t **key_ptr)
{
	assert(ctx);
	assert(ctx->zone);
	assert(ctx->keystores);
	assert(ctx->policy);

	normalize_generate_flags(&flags);

	for (size_t i = 0; !(flags & DNSKEY_GENERATE_FOR_LATER) && i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *k = &ctx->zone->keys[i];
		if (k->is_for_later && key_corresponds(k, ctx, flags)) {
			*key_ptr = k;
			k->is_for_later = false;
			return KNOT_EOK;
		}
	}

	// generate key in the keystore

	char *id = NULL;
	dnssec_key_t *dnskey = NULL;

	assert(ctx->keystores != NULL && ctx->keystores[0].count > 0 && ctx->keystores[0].keystore != NULL);

	knot_kasp_keystore_t *keystore = knot_store_for_key(ctx->keystores, (flags & DNSKEY_GENERATE_KSK));
	if (keystore == NULL) {
		return KNOT_DNSSEC_ENOKEYSTORE;
	}

	int r = generate_keytag_unconflict(ctx, keystore, flags, &id, &dnskey);
	if (r != KNOT_EOK) {
		return r;
	}

	knot_kasp_key_t *key = calloc(1, sizeof(*key));
	if (!key) {
		dnssec_key_free(dnskey);
		free(id);
		return KNOT_ENOMEM;
	}

	key->id = id;
	key->key = dnskey;
	key->is_ksk = (flags & DNSKEY_GENERATE_KSK);
	key->is_zsk = (flags & DNSKEY_GENERATE_ZSK);
	key->is_for_later = (flags & DNSKEY_GENERATE_FOR_LATER);
	key->timing.created = ctx->now;

	r = kasp_zone_append(ctx->zone, key);
	free(key);
	if (r != KNOT_EOK) {
		(void)dnssec_keystore_remove(keystore->keystore, id);
		dnssec_key_free(dnskey);
		free(id);
		return r;
	}

	if (key_ptr) {
		*key_ptr = &ctx->zone->keys[ctx->zone->num_keys - 1];
	}

	return KNOT_EOK;
}

int kdnssec_share_key(kdnssec_ctx_t *ctx, const knot_dname_t *from_zone, const char *key_id)
{
	knot_dname_t *to_zone = knot_dname_copy(ctx->zone->dname, NULL);
	if (to_zone == NULL) {
		return KNOT_ENOMEM;
	}

	int ret = kdnssec_ctx_commit(ctx);
	if (ret != KNOT_EOK) {
		free(to_zone);
		return ret;
	}

	ret = kasp_db_share_key(ctx->kasp_db, from_zone, ctx->zone->dname, key_id);
	if (ret != KNOT_EOK) {
		free(to_zone);
		return ret;
	}

	kasp_zone_clear(ctx->zone);
	ret = kasp_zone_load(ctx->zone, to_zone, ctx->kasp_db,
	                     &ctx->keytag_conflict);
	free(to_zone);
	return ret;
}

int kdnssec_delete_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key_ptr)
{
	assert(ctx);
	assert(ctx->zone);
	assert(ctx->keystores);
	assert(ctx->keystores[0].count > 0);
	assert(ctx->policy);

	ssize_t key_index = key_ptr - ctx->zone->keys;

	if (key_index < 0 || key_index >= ctx->zone->num_keys) {
		return KNOT_EINVAL;
	}

	bool key_still_used_in_keystore = false;
	int ret = kasp_db_delete_key(ctx->kasp_db, ctx->zone->dname, key_ptr->id, &key_still_used_in_keystore);
	if (ret != KNOT_EOK) {
		return ret;
	}

	if (!key_still_used_in_keystore && !key_ptr->is_pub_only) {
		ret = DNSSEC_ENOENT;
		for (size_t i = 0; i < ctx->keystores[0].count && ret == DNSSEC_ENOENT; i++) {
			ret = dnssec_keystore_remove(ctx->keystores[i].keystore, key_ptr->id);
		}
		if (ret != KNOT_EOK) {
			return ret;
		}
	}

	dnssec_key_free(key_ptr->key);
	free(key_ptr->id);
	memmove(key_ptr, key_ptr + 1, (ctx->zone->num_keys - key_index - 1) * sizeof(*key_ptr));
	ctx->zone->num_keys--;
	return KNOT_EOK;
}

static bool is_published(knot_kasp_key_timing_t *timing, knot_time_t now)
{
	return (knot_time_cmp(timing->publish, now) <= 0 &&
	        knot_time_cmp(timing->post_active, now) > 0 &&
	        knot_time_cmp(timing->remove, now) > 0);
}

static bool is_ready(knot_kasp_key_timing_t *timing, knot_time_t now)
{
	return (knot_time_cmp(timing->ready, now) <= 0 &&
	        knot_time_cmp(timing->active, now) > 0);
}

static bool is_active(knot_kasp_key_timing_t *timing, knot_time_t now)
{
	return (knot_time_cmp(timing->active, now) <= 0 &&
	        knot_time_cmp(timing->retire, now) > 0 &&
	        knot_time_cmp(timing->retire_active, now) > 0 &&
	        knot_time_cmp(timing->remove, now) > 0);
}

static bool alg_has_active_zsk(kdnssec_ctx_t *ctx, uint8_t alg)
{
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *k = &ctx->zone->keys[i];
		if (dnssec_key_get_algorithm(k->key) == alg &&
		    k->is_zsk && is_active(&k->timing, ctx->now)) {
			return true;
		}
	}
	return false;
}

static void fix_revoked_flag(knot_kasp_key_t *key)
{
	uint16_t flags = dnssec_key_get_flags(key->key);
	if ((flags & DNSKEY_FLAGS_REVOKED) != DNSKEY_FLAGS_REVOKED) {
		dnssec_key_set_flags(key->key, flags | DNSKEY_FLAGS_REVOKED); // FYI leading to change of keytag
	}
}

/*!
 * \brief Get key feature flags from key parameters.
 */
static void set_key(knot_kasp_key_t *kasp_key, knot_time_t now,
                    zone_key_t *zone_key, bool same_alg_act_zsk)
{
	assert(kasp_key);
	assert(zone_key);

	knot_kasp_key_timing_t *timing = &kasp_key->timing;

	zone_key->id = kasp_key->id;
	zone_key->key = kasp_key->key;

	// next event computation

	knot_time_t next = 0;
	knot_time_t timestamps[] = {
		timing->pre_active,
		timing->publish,
		timing->ready,
		timing->active,
		timing->retire_active,
		timing->retire,
		timing->post_active,
		timing->revoke,
		timing->remove,
	};

	for (int i = 0; i < sizeof(timestamps) / sizeof(knot_time_t); i++) {
		knot_time_t ts = timestamps[i];
		if (knot_time_cmp(now, ts) < 0 && knot_time_cmp(ts, next) < 0) {
			next = ts;
		}
	}

	zone_key->next_event = next;

	zone_key->is_ksk = kasp_key->is_ksk;
	zone_key->is_zsk = kasp_key->is_zsk;

	zone_key->is_public = is_published(timing, now);
	zone_key->is_ready = (zone_key->is_ksk && is_ready(timing, now));
	zone_key->is_active = is_active(timing, now);

	zone_key->is_ksk_active_plus = zone_key->is_public && zone_key->is_ksk && !zone_key->is_active; // KSK is active+ whenever published
	zone_key->is_zsk_active_plus = zone_key->is_ready && !same_alg_act_zsk;
	if (knot_time_cmp(timing->pre_active, now) <= 0 &&
	    knot_time_cmp(timing->ready, now) > 0 &&
	    knot_time_cmp(timing->active, now) > 0 &&
	    knot_time_cmp(timing->remove, now) > 0) {
		zone_key->is_zsk_active_plus = zone_key->is_zsk;
		// zone_key->is_ksk_active_plus = (knot_time_cmp(timing->publish, now) <= 0 && zone_key->is_ksk); // redundant, but helps understand
	}
	if (knot_time_cmp(timing->retire, now) <= 0 &&
	    knot_time_cmp(timing->remove, now) > 0) {
		zone_key->is_ksk_active_plus = false;
		zone_key->is_public = zone_key->is_zsk;
	}
	if (knot_time_cmp(timing->retire_active, now) <= 0 &&
	    knot_time_cmp(timing->retire, now) > 0 &&
	    knot_time_cmp(timing->remove, now) > 0) {
		zone_key->is_ksk_active_plus = zone_key->is_ksk;
		zone_key->is_zsk_active_plus = !same_alg_act_zsk;
	} // not "else" !
	if (knot_time_cmp(timing->post_active, now) <= 0 &&
	    knot_time_cmp(timing->remove, now) > 0) {
		zone_key->is_ksk_active_plus = false;
		zone_key->is_zsk_active_plus = zone_key->is_zsk;
	}
	if (zone_key->is_ksk &&
	    knot_time_cmp(timing->revoke, now) <= 0 &&
	    knot_time_cmp(timing->remove, now) > 0) {
		zone_key->is_ready = false;
		zone_key->is_active = false;
		zone_key->is_ksk_active_plus = true;
		zone_key->is_public = true;
		zone_key->is_revoked = true;
		fix_revoked_flag(kasp_key);
	}
	if (kasp_key->is_pub_only) {
		zone_key->is_active = false;
		zone_key->is_ksk_active_plus = false;
		zone_key->is_zsk_active_plus = false;
		zone_key->is_pub_only = true;
	}
}

/*!
 * \brief Check if algorithm is allowed with NSEC3.
 */
static bool is_nsec3_allowed(uint8_t algorithm)
{
	switch (algorithm) {
	case DNSSEC_KEY_ALGORITHM_RSA_SHA1:
		return false;
	default:
		return true;
	}
}

static int walk_algorithms(kdnssec_ctx_t *ctx, zone_keyset_t *keyset)
{
	if (ctx->policy->unsafe & UNSAFE_KEYSET) {
		return KNOT_EOK;
	}

	uint8_t alg_usage[256] = { 0 };
	bool have_active_alg = false;

	for (size_t i = 0; i < keyset->count; i++) {
		zone_key_t *key = &keyset->keys[i];
		if (key->is_pub_only) {
			continue;
		}
		uint8_t alg = dnssec_key_get_algorithm(key->key);

		if (ctx->policy->nsec3_enabled && !is_nsec3_allowed(alg)) {
			log_zone_warning(ctx->zone->dname, "DNSSEC, key %d "
			                 "cannot be used with NSEC3",
			                 dnssec_key_get_keytag(key->key));
			key->is_public = false;
			key->is_active = false;
			key->is_ready = false;
			key->is_ksk_active_plus = false;
			key->is_zsk_active_plus = false;
			continue;
		}

		if (key->is_ksk && key->is_public) { alg_usage[alg] |= 1; }
		if (key->is_zsk && key->is_public) { alg_usage[alg] |= 2; }
		if (key->is_ksk && (key->is_active || key->is_ksk_active_plus)) { alg_usage[alg] |= 4; }
		if (key->is_zsk && (key->is_active || key->is_zsk_active_plus)) { alg_usage[alg] |= 8; }
	}

	for (size_t i = 0; i < sizeof(alg_usage); i++) {
		if (!(alg_usage[i] & 3)) {
			continue; // no public keys, ignore
		}
		switch (alg_usage[i]) {
		case 15: // all keys ready for signing
			have_active_alg = true;
			break;
		case 5:
		case 10:
			if (ctx->policy->offline_ksk) {
				have_active_alg = true;
				break;
			}
			// else FALLTHROUGH
		default:
			return KNOT_DNSSEC_EMISSINGKEYTYPE;
		}
	}

	if (!have_active_alg) {
		return KNOT_DNSSEC_ENOKEY;
	}

	return KNOT_EOK;
}

int kdnssec_load_private(knot_kasp_keystore_t *keystores, const char *id,
                         dnssec_key_t *key, const char **name, unsigned *backend)
{
	int ret = DNSSEC_ENOENT;
	for (size_t i = 0; i < keystores[0].count && (ret == DNSSEC_ENOENT || ret == DNSSEC_NOT_FOUND); i++) {
		ret = dnssec_keystore_get_private(keystores[i].keystore, id, key);
		if (ret == KNOT_EOK) {
			if (name != NULL) {
				*name = keystores[i].name;
			}
			if (backend != NULL) {
				*backend = keystores[i].backend;
			}
		}
	}
	return ret;
}

knot_kasp_keystore_t *knot_store_for_key(knot_kasp_keystore_t *keystores, bool ksk)
{
	for (size_t i = 0; i < keystores[0].count; i++) {
		if (ksk || !keystores[i].ksk_only) {
			return &keystores[i];
		}
	}
	return NULL;
}

/*!
 * \brief Load private keys for active keys.
 */
static int load_private_keys(kdnssec_ctx_t *ctx, zone_keyset_t *keyset)
{
	assert(ctx);
	assert(keyset);

	for (size_t i = 0; i < keyset->count; i++) {
		zone_key_t *key = &keyset->keys[i];
		if (!key->is_active && !key->is_ksk_active_plus && !key->is_zsk_active_plus) {
			continue;
		}
		int ret = kdnssec_load_private(ctx->keystores, key->id, key->key, NULL, NULL);
		switch (ret) {
		case DNSSEC_EOK:
		case DNSSEC_KEY_ALREADY_PRESENT:
			break;
		default:
			ret = knot_error_from_libdnssec(ret);
			log_zone_error(ctx->zone->dname,
			               "DNSSEC, key %d, failed to load private key (%s)",
			               dnssec_key_get_keytag(key->key), knot_strerror(ret));
			return ret;
		}
	}

	return KNOT_EOK;
}

/*!
 * \brief Log information about zone keys.
 */
static void log_key_info(const zone_key_t *key, char *out, size_t out_len)
{
	assert(key);
	assert(out);

	uint8_t alg_code = dnssec_key_get_algorithm(key->key);
	const knot_lookup_t *alg = knot_lookup_by_id(knot_dnssec_alg_names, alg_code);

	char alg_code_str[8] = "";
	if (alg == NULL) {
		(void)snprintf(alg_code_str, sizeof(alg_code_str), "%d", alg_code);
	}

	(void)snprintf(out, out_len, "DNSSEC, key, tag %5d, algorithm %s%s%s%s%s%s",
	               dnssec_key_get_keytag(key->key),
	               (alg != NULL                ? alg->name  : alg_code_str),
	               (key->is_ksk ? (key->is_zsk ? ", CSK" : ", KSK") : ""),
	               (key->is_public             ? ", public"  : ""),
	               (key->is_ready              ? ", ready"   : ""),
	               (key->is_active             ? ", active"  : ""),
	               (key->is_ksk_active_plus || key->is_zsk_active_plus ? ", active+" : ""));
}

static int log_key_sort(const void *a, const void *b)
{
	const key_info_t *x = a, *y = b;
	return knot_time_cmp(x->key_time, y->key_time);
}

/*!
 * \brief Load zone keys and init cryptographic context.
 */
int load_zone_keys(kdnssec_ctx_t *ctx, zone_keyset_t *keyset_ptr, bool verbose)
{
	if (!ctx || !keyset_ptr) {
		return KNOT_EINVAL;
	}

	zone_keyset_t keyset = { 0 };

	if (ctx->zone->num_keys < 1) {
		log_zone_error(ctx->zone->dname, "DNSSEC, no keys are available");
		return KNOT_DNSSEC_ENOKEY;
	}

	keyset.count = ctx->zone->num_keys;
	keyset.keys = calloc(keyset.count, sizeof(zone_key_t));
	if (!keyset.keys) {
		free_zone_keys(&keyset);
		return KNOT_ENOMEM;
	}

	key_info_t key_info[ctx->zone->num_keys];
	for (size_t i = 0; i < ctx->zone->num_keys; i++) {
		knot_kasp_key_t *kasp_key = &ctx->zone->keys[i];
		uint8_t kk_alg = dnssec_key_get_algorithm(kasp_key->key);
		bool same_alg_zsk = alg_has_active_zsk(ctx, kk_alg);
		set_key(kasp_key, ctx->now, &keyset.keys[i], same_alg_zsk);
		if (verbose) {
			log_key_info(&keyset.keys[i], key_info[i].msg, MAX_KEY_INFO);
			if (knot_time_cmp(kasp_key->timing.pre_active, kasp_key->timing.publish) < 0) {
				key_info[i].key_time = kasp_key->timing.pre_active;
			} else {
				key_info[i].key_time = kasp_key->timing.publish;
			}
		}
	}

	// Sort the keys by publish/pre_active timestamps.
	if (verbose) {
		qsort(key_info, ctx->zone->num_keys, sizeof(key_info[0]), log_key_sort);
		for (size_t i = 0; i < ctx->zone->num_keys; i++) {
			log_zone_info(ctx->zone->dname, "%s", key_info[i].msg);
		}
	}

	int ret = walk_algorithms(ctx, &keyset);
	if (ret != KNOT_EOK) {
		log_zone_error(ctx->zone->dname, "DNSSEC, keys validation failed (%s)",
		               knot_strerror(ret));
		free_zone_keys(&keyset);
		return ret;
	}

	ret = load_private_keys(ctx, &keyset);
	if (ret != KNOT_EOK) {
		free_zone_keys(&keyset);
		return ret;
	}

	*keyset_ptr = keyset;

	return KNOT_EOK;
}

/*!
 * \brief Free structure with zone keys and associated DNSSEC contexts.
 */
void free_zone_keys(zone_keyset_t *keyset)
{
	if (!keyset) {
		return;
	}

	for (size_t i = 0; i < keyset->count; i++) {
		dnssec_binary_free(&keyset->keys[i].precomputed_ds);
	}

	free(keyset->keys);

	memset(keyset, '\0', sizeof(*keyset));
}

/*!
 * \brief Get timestamp of next key event.
 */
knot_time_t knot_get_next_zone_key_event(const zone_keyset_t *keyset)
{
	assert(keyset);

	knot_time_t result = 0;

	for (size_t i = 0; i < keyset->count; i++) {
		zone_key_t *key = &keyset->keys[i];
		if (knot_time_cmp(key->next_event, result) < 0) {
			result = key->next_event;
		}
	}

	return result;
}

/*!
 * \brief Compute DS record rdata from key + cache it.
 */
int zone_key_calculate_ds(zone_key_t *for_key, dnssec_key_digest_t digesttype,
                          dnssec_binary_t *out_donotfree)
{
	assert(for_key);
	assert(out_donotfree);

	int ret = KNOT_EOK;

	if (for_key->precomputed_ds.data == NULL || for_key->precomputed_digesttype != digesttype) {
		dnssec_binary_free(&for_key->precomputed_ds);
		ret = dnssec_key_create_ds(for_key->key, digesttype, &for_key->precomputed_ds);
		ret = knot_error_from_libdnssec(ret);
		for_key->precomputed_digesttype = digesttype;
	}

	*out_donotfree = for_key->precomputed_ds;
	return ret;
}

static int dup_zone_key(const zone_key_t *src, zone_key_t *dst)
{
	assert(src);
	assert(dst);

	*dst = *src;

	dst->key = dnssec_key_dup(src->key);
	if (dst->key == NULL) {
		return KNOT_ENOMEM;
	}

	return KNOT_EOK;
}

zone_sign_ctx_t *zone_sign_ctx(const zone_keyset_t *keyset, const kdnssec_ctx_t *dnssec_ctx)
{
	zone_sign_ctx_t *ctx = calloc(1, sizeof(*ctx) + keyset->count * sizeof(*ctx->sign_ctxs));
	if (ctx == NULL) {
		return NULL;
	}

	ctx->sign_ctxs = (dnssec_sign_ctx_t **)(ctx + 1);

	ctx->keys = calloc(keyset->count, sizeof(*ctx->keys));
	if (ctx->keys == NULL) {
		zone_sign_ctx_free(ctx);
		return NULL;
	}
	ctx->count = keyset->count;

	ctx->dnssec_ctx = dnssec_ctx;
	for (size_t i = 0; i < ctx->count; i++) {
		// Clone the key to avoid thread contention on the key mutex.
		int ret = dup_zone_key(&keyset->keys[i], &ctx->keys[i]);
		if (ret != KNOT_EOK) {
			zone_sign_ctx_free(ctx);
			return NULL;
		}

		ret = dnssec_sign_new(&ctx->sign_ctxs[i], ctx->keys[i].key);
		if (ret != DNSSEC_EOK) {
			zone_sign_ctx_free(ctx);
			return NULL;
		}
	}

	return ctx;
}

zone_sign_ctx_t *zone_validation_ctx(const kdnssec_ctx_t *dnssec_ctx)
{
	size_t count = dnssec_ctx->zone->num_keys;
	zone_sign_ctx_t *ctx = calloc(1, sizeof(*ctx) + count * sizeof(*ctx->sign_ctxs));
	if (ctx == NULL) {
		return NULL;
	}

	ctx->sign_ctxs = (dnssec_sign_ctx_t **)(ctx + 1);
	ctx->count = count;
	ctx->keys = NULL;
	ctx->dnssec_ctx = dnssec_ctx;
	for (size_t i = 0; i < ctx->count; i++) {
		int ret = dnssec_sign_new(&ctx->sign_ctxs[i], dnssec_ctx->zone->keys[i].key);
		if (ret != DNSSEC_EOK) {
			zone_sign_ctx_free(ctx);
			return NULL;
		}
	}

	return ctx;
}

void zone_sign_ctx_free(zone_sign_ctx_t *ctx)
{
	if (ctx != NULL) {
		for (size_t i = 0; i < ctx->count; i++) {
			if (ctx->keys != NULL) {
				dnssec_key_free(ctx->keys[i].key);
			}
			dnssec_sign_free(ctx->sign_ctxs[i]);
		}
		free(ctx->keys);
		free(ctx);
	}
}

int dnssec_key_from_rdata(dnssec_key_t **key, const knot_dname_t *owner,
                          const uint8_t *rdata, size_t rdlen)
{
	if (key == NULL || rdata == NULL || rdlen == 0) {
		return KNOT_EINVAL;
	}

	const dnssec_binary_t binary_key = {
		.size = rdlen,
		.data = (uint8_t *)rdata
	};

	dnssec_key_t *new_key = NULL;
	int ret = dnssec_key_new(&new_key);
	if (ret != DNSSEC_EOK) {
		return knot_error_from_libdnssec(ret);
	}
	ret = dnssec_key_set_rdata(new_key, &binary_key);
	if (ret != DNSSEC_EOK) {
		dnssec_key_free(new_key);
		return knot_error_from_libdnssec(ret);
	}
	if (owner != NULL) {
		ret = dnssec_key_set_dname(new_key, owner);
		if (ret != DNSSEC_EOK) {
			dnssec_key_free(new_key);
			return knot_error_from_libdnssec(ret);
		}
	}

	*key = new_key;
	return KNOT_EOK;
}

static bool soa_signed_by_key(const zone_key_t *key, const knot_rdataset_t *apex_rrsig)
{
	assert(key != NULL);
	if (apex_rrsig == NULL) {
		return false;
	}
	uint16_t keytag = dnssec_key_get_keytag(key->key);

	knot_rdata_t *rr = apex_rrsig->rdata;
	for (int i = 0; i < apex_rrsig->count; i++) {
		if (knot_rrsig_type_covered(rr) == KNOT_RRTYPE_SOA &&
		    knot_rrsig_key_tag(rr) == keytag) {
			return true;
		}
		rr = knot_rdataset_next(rr);
	}

	return false;
}

int is_soa_signed_by_all_zsks(const zone_keyset_t *keyset,
                              const knot_rdataset_t *apex_rrsig)
{
	if (keyset == NULL || keyset->count == 0) {
		return false;
	}

	for (size_t i = 0; i < keyset->count; i++) {
		const zone_key_t *key = &keyset->keys[i];
		if (key->is_zsk && key->is_active &&
		    !soa_signed_by_key(key, apex_rrsig)) {
			return false;
		}
	}

	return true;
}
