2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-08-29 13:38:26 +00:00

add "zonemd" option to dnssec-policy

"zonemd <scheme> <digest>;" now causes a ZONEMD record to be
added when inline signing.

"zonemd yes;" is a synonym for "zonemd simple sha384;".
This commit is contained in:
Evan Hunt 2025-06-11 22:33:28 -07:00
parent b05ab7b2a1
commit fcbd4bea02
13 changed files with 407 additions and 12 deletions

View File

@ -40,6 +40,17 @@ dnssec-policy "test" {
signatures-validity P2W;
signatures-validity-dnskey P14D;
zone-propagation-delay PT5M;
zonemd yes;
};
dnssec-policy "nozm" {
zonemd no;
};
dnssec-policy "zm512" {
zonemd simple sha512;
};
dnssec-policy "zm384andzm512" {
zonemd simple sha384;
zonemd simple sha512;
};
key-store "hsm" {
directory ".";

View File

@ -237,6 +237,16 @@ zone "max-zone-ttl.kasp" {
dnssec-policy "ttl";
};
/*
* Zone that uses ZONEMD.
*/
zone "zonemd.kasp" {
type primary;
file "zonemd.kasp.db";
dnssec-policy "zonemd";
};
/*
* Zones in different signing states.
*/

View File

@ -119,3 +119,13 @@ dnssec-policy "keystore" {
zsk key-store "zsk" lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
};
};
dnssec-policy "zonemd" {
dnskey-ttl 305;
zonemd yes;
keys {
ksk key-store "ksk" lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
zsk key-store "zsk" lifetime unlimited algorithm @DEFAULT_ALGORITHM@;
};
};

View File

@ -50,7 +50,7 @@ for zn in default dnssec-keygen some-keys legacy-keys pregenerated \
rumoured rsasha256 rsasha512 ecdsa256 ecdsa384 \
dynamic dynamic-inline-signing inline-signing \
checkds-ksk checkds-doubleksk checkds-csk inherit unlimited \
keystore; do
keystore zonemd; do
setup "${zn}.kasp"
cp template.db.in "$zonefile"
done

View File

@ -1768,3 +1768,10 @@ def test_kasp_manual_mode(ns3):
isctest.kasp.check_keys(zone, keys, expected)
check_all(ns3, zone, policy, ksks, zsks, manual_mode=True)
isctest.kasp.check_dnssec_verify(ns3, zone)
def test_kasp_zonemd(ns3):
msg = isctest.query.create("zonemd.kasp", "zonemd")
res = isctest.query.tcp(msg, ns3.ip)
isctest.check.noerror(res)
isctest.check.rr_count_eq(res.answer, 2)

View File

@ -6605,6 +6605,16 @@ max-zone-ttl
zone is updated to the time when the new version is served by all of
the parent zone's name servers. The default is ``PT1H`` (1 hour).
.. namedconf:statement:: zonemd
:tags: dnssec
:short: Specifies whether a ZONEMD record should be published in an inline-signing zone.
This option controls whether to publish a ZONEMD record in the
signed version of an :any:`inline-signing` zone. If set to ``no``,
no ZONEMD is published. If set to ``simple sha384`` or ``simple sha512``,
a ZONEMD will be published using the SIMPLE scheme and either the
SHA384 and SHA512 digest type. ``yes`` is a synonym for ``simple sha384``.
Automated KSK Rollovers
^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -30,6 +30,7 @@ dnssec-policy <string> {
signatures-validity <duration>;
signatures-validity-dnskey <duration>;
zone-propagation-delay <duration>;
zonemd ( simple | <boolean> ) [ ( sha384 | sha512 ) ]; // may occur multiple times
}; // may occur multiple times
dyndb <string> <quoted_string> { <unspecified-text> }; // may occur multiple times

View File

@ -31,7 +31,9 @@
#include <dns/dnssec.h>
#include <dns/keystore.h>
#include <dns/rdatastruct.h>
#include <dns/types.h>
#include <dns/zonemd.h>
/* For storing a list of digest types */
struct dns_kasp_digest {
@ -110,6 +112,10 @@ struct dns_kasp {
bool inline_signing;
bool manual_mode;
/* ZONEMD settings */
uint8_t zonemd_scheme[DNS_ZONEMD_MAX];
uint8_t zonemd_digest[DNS_ZONEMD_MAX];
/* Parent settings */
dns_ttl_t parent_ds_ttl;
uint32_t parent_propagation_delay;
@ -900,3 +906,30 @@ dns_kasp_adddigest(dns_kasp_t *kasp, dns_dsdigest_t alg);
*
*\li 'kasp' is a valid, thawed kasp.
*/
void
dns_kasp_setzonemd(dns_kasp_t *kasp, uint8_t scheme, uint8_t digest);
/*%<
* Set the scheme and digest type for a ZONEMD record. If 'scheme' and
* 'digest' are nonzero, then if no ZONEMD already exists in the zone,
* one will be added after signing.
*
* Requires:
*
*\li 'kasp' is a valid, thawed kasp.
*
*\li 'scheme' and 'digest' are both 0, or else 'scheme' and 'digest'
* are valid ZONEMD scheme/digest values.
*/
uint8_t *
dns_kasp_zonemd_scheme(dns_kasp_t *kasp);
uint8_t *
dns_kasp_zonemd_digest(dns_kasp_t *kasp);
/*%<
* Get the ZONEMD scheme/digest settings for the zone.
*
* Requires:
*
*\li 'kasp' is a valid, frozen kasp.
*/

View File

@ -681,3 +681,64 @@ dns_kasp_adddigest(dns_kasp_t *kasp, dns_dsdigest_t alg) {
ISC_LINK_INIT(digest, link);
ISC_LIST_APPEND(kasp->digests, digest, link);
}
void
dns_kasp_setzonemd(dns_kasp_t *kasp, uint8_t scheme, uint8_t digest) {
REQUIRE(kasp != NULL);
REQUIRE(!kasp->frozen);
REQUIRE((scheme == 0 && digest == 0) ||
(scheme == DNS_ZONEMD_SCHEME_MAX &&
digest == DNS_ZONEMD_DIGEST_MAX) ||
(scheme != 0 && digest != 0 && scheme < DNS_ZONEMD_SCHEME_MAX &&
digest < DNS_ZONEMD_DIGEST_MAX));
/*
* Delete zonemds.
*/
if (scheme == DNS_ZONEMD_SCHEME_MAX) {
kasp->zonemd_scheme[0] = scheme;
kasp->zonemd_digest[0] = digest;
return;
}
/*
* Maintain zonemds.
*/
if (scheme == 0) {
kasp->zonemd_scheme[0] = scheme;
kasp->zonemd_digest[0] = digest;
return;
}
/*
* Set to this set.
*/
for (size_t i = 0; i < ARRAY_SIZE(kasp->zonemd_scheme) - 1; i++) {
if (kasp->zonemd_scheme[i] == scheme &&
kasp->zonemd_digest[i] == digest)
{
return;
}
if (kasp->zonemd_scheme[i] == 0) {
kasp->zonemd_scheme[i] = scheme;
kasp->zonemd_digest[i] = digest;
return;
}
}
}
uint8_t *
dns_kasp_zonemd_scheme(dns_kasp_t *kasp) {
REQUIRE(kasp != NULL);
REQUIRE(kasp->frozen);
return kasp->zonemd_scheme;
}
uint8_t *
dns_kasp_zonemd_digest(dns_kasp_t *kasp) {
REQUIRE(kasp != NULL);
REQUIRE(kasp->frozen);
return kasp->zonemd_digest;
}

View File

@ -14,11 +14,25 @@
#pragma once
/* Known scheme type(s). */
#define DNS_ZONEMD_SCHEME_SIMPLE (1)
enum {
DNS_ZONEMD_SCHEME_SIMPLE = 1,
DNS_ZONEMD_SCHEME_MAX = 2,
};
/* Known digest type(s). */
#define DNS_ZONEMD_DIGEST_SHA384 (1)
#define DNS_ZONEMD_DIGEST_SHA512 (2)
enum {
DNS_ZONEMD_DIGEST_SHA384 = 1,
DNS_ZONEMD_DIGEST_SHA512 = 2,
DNS_ZONEMD_DIGEST_MAX = 3,
};
/*
* Array size that can hold all possible combinations of schemes and digests
* with a sentinal (0, 0) entry.
*/
#define DNS_ZONEMD_MAX \
(((int)DNS_ZONEMD_SCHEME_MAX - 1) * ((int)DNS_ZONEMD_DIGEST_MAX - 1) + \
1)
/*
* \brief per RFC 8976

View File

@ -10242,6 +10242,132 @@ failure:
return false;
}
static isc_result_t
setup_zonemd(dns_zone_t *zone, dns_db_t *db, dns_dbversion_t *version,
dns_diff_t *diff) {
isc_result_t result;
dns_dbnode_t *node = NULL;
dns_rdata_zonemd_t zonemd;
dns_rdataset_t zmset = DNS_RDATASET_INIT;
unsigned char digest[64] = { 0 };
unsigned char zmbuf[6 + sizeof(digest)];
uint8_t *scheme, *digest_type;
dns_ttl_t ttl = 0;
isc_buffer_t b;
scheme = dns_kasp_zonemd_scheme(zone->kasp);
digest_type = dns_kasp_zonemd_digest(zone->kasp);
/*
* No zonemd processing specified.
*/
if (scheme[0] == 0) {
return ISC_R_SUCCESS;
}
CHECK(dns_db_getoriginnode(db, &node));
/* Is there already a ZONEMD? */
result = dns_db_findrdataset(db, node, version, dns_rdatatype_zonemd, 0,
0, &zmset, NULL);
if (result == ISC_R_SUCCESS) {
ttl = zmset.ttl;
}
/*
* Delete existing ZONEMD records that don't meet policy.
*/
if (result == ISC_R_SUCCESS && scheme[0] != 0) {
DNS_RDATASET_FOREACH(&zmset) {
dns_rdata_t zr = DNS_RDATA_INIT;
bool found = false;
/*
* DNS_ZONEMD_SCHEME_MAX => delete everything.
*/
if (scheme[0] != DNS_ZONEMD_SCHEME_MAX) {
dns_rdataset_current(&zmset, &zr);
(void)dns_rdata_tostruct(&zr, &zonemd, NULL);
for (size_t i = 0; scheme[i] != 0; i++) {
if (zonemd.scheme == scheme[i] &&
zonemd.digest_type == digest[i])
{
found = true;
}
}
}
/*
* Is there a scheme/digest_type mismatch, or ZONEMD is
* being removed?
*/
if (!found) {
CHECK(update_one_rr(db, version, diff,
DNS_DIFFOP_DEL,
&zone->origin, ttl, &zr));
}
}
} else if (result == ISC_R_NOTFOUND) {
result = ISC_R_SUCCESS;
}
if (result != ISC_R_SUCCESS || scheme[0] == DNS_ZONEMD_SCHEME_MAX) {
goto failure;
}
if (!dns_rdataset_isassociated(&zmset)) {
/* Get the SOA TTL */
dns_rdataset_t soaset = DNS_RDATASET_INIT;
CHECK(dns_db_findrdataset(db, node, version, dns_rdatatype_soa,
0, 0, &soaset, NULL));
ttl = soaset.ttl;
dns_rdataset_disassociate(&soaset);
}
/* ZONEMD was either not present or has been deleted: add a dummy */
zonemd.common.rdclass = zone->rdclass;
zonemd.common.rdtype = dns_rdatatype_zonemd;
zonemd.mctx = NULL;
zonemd.serial = 0;
zonemd.digest = digest;
for (size_t i = 0; scheme[i] != 0; i++) {
dns_rdata_t rdata = DNS_RDATA_INIT;
zonemd.scheme = scheme[i];
zonemd.digest_type = digest_type[i];
switch (zonemd.digest_type) {
case DNS_ZONEMD_DIGEST_SHA384:
zonemd.length = ISC_SHA384_DIGESTLENGTH;
break;
case DNS_ZONEMD_DIGEST_SHA512:
zonemd.length = ISC_SHA512_DIGESTLENGTH;
break;
default:
UNREACHABLE();
}
isc_buffer_init(&b, zmbuf, sizeof(zmbuf));
CHECK(dns_rdata_fromstruct(&rdata, zone->rdclass,
dns_rdatatype_zonemd, &zonemd, &b));
CHECK(update_one_rr(db, version, diff, DNS_DIFFOP_ADD,
&zone->origin, ttl, &rdata));
}
failure:
if (dns_rdataset_isassociated(&zmset)) {
dns_rdataset_disassociate(&zmset);
}
if (node != NULL) {
dns_db_detachnode(&node);
}
return result;
}
/*
* Incrementally sign the zone using the keys requested.
* Builds the NSEC chain if required.
@ -10324,6 +10450,23 @@ zone_sign(dns_zone_t *zone) {
goto cleanup;
}
if (zone->kasp != NULL) {
result = setup_zonemd(zone, db, version, &_sig_diff);
if (result != ISC_R_SUCCESS) {
dnssec_log(zone, ISC_LOG_ERROR,
"zone_sign:setup_zonemd -> %s",
isc_result_totext(result));
goto cleanup;
}
}
result = dns_update_zonemd(db, version, &_sig_diff);
if (result != ISC_R_SUCCESS) {
dnssec_log(zone, ISC_LOG_ERROR,
"zone_sign:dns_update_zonemd -> %s",
isc_result_totext(result));
goto cleanup;
}
kasp = zone->kasp;
calculate_rrsig_validity(zone, now, &inception, &soaexpire, NULL,
@ -17321,7 +17464,8 @@ sync_secure_journal(dns_zone_t *zone, dns_zone_t *raw, dns_journal_t *journal,
if (rdata->type == dns_rdatatype_nsec ||
rdata->type == dns_rdatatype_rrsig ||
rdata->type == dns_rdatatype_nsec3 ||
rdata->type == dns_rdatatype_nsec3param)
rdata->type == dns_rdatatype_nsec3param ||
rdata->type == dns_rdatatype_zonemd)
{
continue;
}
@ -17510,7 +17654,8 @@ sync_secure_db(dns_zone_t *seczone, dns_zone_t *raw, dns_db_t *secdb,
if (tuple->rdata.type == dns_rdatatype_nsec ||
tuple->rdata.type == dns_rdatatype_rrsig ||
tuple->rdata.type == dns_rdatatype_nsec3 ||
tuple->rdata.type == dns_rdatatype_nsec3param)
tuple->rdata.type == dns_rdatatype_nsec3param ||
tuple->rdata.type == dns_rdatatype_zonemd)
{
ISC_LIST_UNLINK(diff->tuples, tuple, link);
dns_difftuple_free(&tuple);
@ -17732,6 +17877,14 @@ receive_secure_serial(void *arg) {
dns_db_currentversion(zone->rss_db, &zone->rss_oldver);
CHECK(dns_db_newversion(zone->rss_db, &zone->rss_newver));
/*
* Set up the ZONEMD as specified in the policy.
*/
CHECK(setup_zonemd(zone, zone->rss_db, zone->rss_newver,
&zone->rss_diff));
CHECK(dns_update_zonemd(zone->rss_db, zone->rss_newver,
&zone->rss_diff));
/*
* Try to apply diffs from the raw zone's journal to the secure
* zone. If that fails, we recover by syncing up the databases
@ -18174,7 +18327,8 @@ copy_non_dnssec_records(dns_db_t *db, dns_dbversion_t *version, dns_db_t *rawdb,
if (rdataset.type == dns_rdatatype_nsec ||
rdataset.type == dns_rdatatype_rrsig ||
rdataset.type == dns_rdatatype_nsec3 ||
rdataset.type == dns_rdatatype_nsec3param)
rdataset.type == dns_rdatatype_nsec3param ||
rdataset.type == dns_rdatatype_zonemd)
{
dns_rdataset_disassociate(&rdataset);
continue;
@ -18274,11 +18428,8 @@ receive_secure_db(void *arg) {
}
DNS_DBITERATOR_FOREACH(dbiterator) {
result = copy_non_dnssec_records(db, version, rawdb, dbiterator,
oldserialp);
if (result != ISC_R_SUCCESS) {
goto failure;
}
CHECK(copy_non_dnssec_records(db, version, rawdb, dbiterator,
oldserialp));
}
dns_dbiterator_destroy(&dbiterator);

View File

@ -30,6 +30,7 @@
#include <dns/nsec3.h>
#include <dns/secalg.h>
#include <dns/ttl.h>
#include <dns/zonemd.h>
#include <dst/dst.h>
@ -464,6 +465,7 @@ cfg_kasp_fromconfig(const cfg_obj_t *config, dns_kasp_t *default_kasp,
const cfg_obj_t *inlinesigning = NULL;
const cfg_obj_t *cds = NULL;
const cfg_obj_t *obj = NULL;
const cfg_obj_t *zonemd = NULL;
const char *kaspname = NULL;
dns_kasp_t *kasp = NULL;
size_t i = 0;
@ -600,6 +602,58 @@ cfg_kasp_fromconfig(const cfg_obj_t *config, dns_kasp_t *default_kasp,
DNS_KASP_PARENT_PROPDELAY);
dns_kasp_setparentpropagationdelay(kasp, parentpropdelay);
(void)confget(maps, "zonemd", &zonemd);
if (zonemd != NULL) {
CFG_LIST_FOREACH(zonemd, element) {
obj = cfg_listelt_value(element);
const cfg_obj_t *sobj = cfg_tuple_get(obj, "scheme");
if (cfg_obj_isboolean(sobj)) {
if (!cfg_obj_asboolean(sobj)) {
dns_kasp_setzonemd(
kasp, DNS_ZONEMD_SCHEME_MAX,
DNS_ZONEMD_DIGEST_MAX);
} else {
dns_kasp_setzonemd(
kasp, DNS_ZONEMD_SCHEME_SIMPLE,
DNS_ZONEMD_DIGEST_SHA384);
}
} else if (strcasecmp(cfg_obj_asstring(sobj),
"simple") != 0)
{
cfg_obj_log(
config, ISC_LOG_ERROR,
"dnssec-policy: policy '%s' unknown "
"ZONEMD scheme %s",
kaspname, cfg_obj_asstring(sobj));
result = ISC_R_FAILURE;
} else {
const cfg_obj_t *dobj = cfg_tuple_get(obj,
"digest");
if (strcasecmp(cfg_obj_asstring(dobj),
"sha384"))
{
dns_kasp_setzonemd(
kasp, DNS_ZONEMD_SCHEME_SIMPLE,
DNS_ZONEMD_DIGEST_SHA384);
} else if (strcasecmp(cfg_obj_asstring(dobj),
"sha512"))
{
dns_kasp_setzonemd(
kasp, DNS_ZONEMD_SCHEME_SIMPLE,
DNS_ZONEMD_DIGEST_SHA512);
} else {
cfg_obj_log(config, ISC_LOG_ERROR,
"dnssec-policy: policy "
"'%s' unknown "
"ZONEMD digest %s",
kaspname,
cfg_obj_asstring(dobj));
result = ISC_R_FAILURE;
}
}
}
}
/* Configuration: Keys */
obj = NULL;
(void)confget(maps, "offline-ksk", &obj);

View File

@ -2205,6 +2205,38 @@ static cfg_type_t cfg_type_checkdstype = {
doc_checkds_type, &cfg_rep_string, checkds_enums,
};
/*%
* Zonemd type.
*/
static const char *zonemd_schemes[] = { "simple", NULL };
static const char *zonemd_digests[] = { "sha384", "sha512", NULL };
static isc_result_t
parse_zscheme(cfg_parser_t *pctx, const cfg_type_t *type, cfg_obj_t **ret) {
return cfg_parse_enum_or_other(pctx, type, &cfg_type_boolean, ret);
}
static void
doc_zscheme(cfg_printer_t *pctx, const cfg_type_t *type) {
cfg_doc_enum_or_other(pctx, type, &cfg_type_boolean);
}
static cfg_type_t cfg_type_zscheme = { "zscheme", parse_zscheme,
cfg_print_ustring, doc_zscheme,
&cfg_rep_string, zonemd_schemes };
static cfg_type_t cfg_type_zdigest = { "zdigest", parse_optional_enum,
cfg_print_ustring, doc_optional_enum,
&cfg_rep_string, zonemd_digests };
static cfg_tuplefielddef_t zonemd_fields[] = {
{ "scheme", &cfg_type_zscheme, 0 },
{ "digest", &cfg_type_zdigest, 0 },
{ NULL, NULL, 0 }
};
static cfg_type_t cfg_type_zonemd = { "zonemd", cfg_parse_tuple,
cfg_print_tuple, cfg_doc_tuple,
&cfg_rep_tuple, zonemd_fields };
/*%
* Clauses that can be found in a 'dnssec-policy' statement.
*/
@ -2229,6 +2261,7 @@ static cfg_clausedef_t dnssecpolicy_clauses[] = {
{ "signatures-validity", &cfg_type_duration, 0 },
{ "signatures-validity-dnskey", &cfg_type_duration, 0 },
{ "zone-propagation-delay", &cfg_type_duration, 0 },
{ "zonemd", &cfg_type_zonemd, CFG_CLAUSEFLAG_MULTI },
{ NULL, NULL, 0 }
};