mirror of
https://github.com/isc-projects/bind9.git
synced 2026-02-25 02:42:33 -05:00
When doing a dnssec-policy reconfiguration from a zone with NSEC only keys to a zone that uses NSEC3, figure out to wait with building the NSEC3 chain. Previously, BIND 9 would attempt to sign such a zone, but failed to do so because the NSEC3 chain conflicted with existing DNSKEY records in the zone that were not compatible with NSEC3. There exists logic for detecting such a case in the functions dnskey_sane() (in lib/dns/zone.c) and check_dnssec() (in lib/ns/update.c). Both functions look very similar so refactor them to use the same code and call the new function (called dns_zone_check_dnskey_nsec3()). Also update the dns_nsec_nseconly() function to take an additional parameter 'diff' that, if provided, will be checked whether an offending NSEC only DNSKEY will be deleted from the zone. If so, this key will not be considered when checking the zone for NSEC only DNSKEYs. This is needed to allow a transition from an NSEC zone with NSEC only DNSKEYs to an NSEC3 zone.
531 lines
13 KiB
C
531 lines
13 KiB
C
/*
|
|
* 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 <stdbool.h>
|
|
|
|
#include <isc/log.h>
|
|
#include <isc/result.h>
|
|
#include <isc/string.h>
|
|
#include <isc/util.h>
|
|
|
|
#include <dns/db.h>
|
|
#include <dns/nsec.h>
|
|
#include <dns/rdata.h>
|
|
#include <dns/rdatalist.h>
|
|
#include <dns/rdataset.h>
|
|
#include <dns/rdatasetiter.h>
|
|
#include <dns/rdatastruct.h>
|
|
|
|
#include <dst/dst.h>
|
|
|
|
#define RETERR(x) \
|
|
do { \
|
|
result = (x); \
|
|
if (result != ISC_R_SUCCESS) \
|
|
goto failure; \
|
|
} while (0)
|
|
|
|
void
|
|
dns_nsec_setbit(unsigned char *array, unsigned int type, unsigned int bit) {
|
|
unsigned int shift, mask;
|
|
|
|
shift = 7 - (type % 8);
|
|
mask = 1 << shift;
|
|
|
|
if (bit != 0) {
|
|
array[type / 8] |= mask;
|
|
} else {
|
|
array[type / 8] &= (~mask & 0xFF);
|
|
}
|
|
}
|
|
|
|
bool
|
|
dns_nsec_isset(const unsigned char *array, unsigned int type) {
|
|
unsigned int byte, shift, mask;
|
|
|
|
byte = array[type / 8];
|
|
shift = 7 - (type % 8);
|
|
mask = 1 << shift;
|
|
|
|
return ((byte & mask) != 0);
|
|
}
|
|
|
|
unsigned int
|
|
dns_nsec_compressbitmap(unsigned char *map, const unsigned char *raw,
|
|
unsigned int max_type) {
|
|
unsigned char *start = map;
|
|
unsigned int window;
|
|
int octet;
|
|
|
|
if (raw == NULL) {
|
|
return (0);
|
|
}
|
|
|
|
for (window = 0; window < 256; window++) {
|
|
if (window * 256 > max_type) {
|
|
break;
|
|
}
|
|
for (octet = 31; octet >= 0; octet--) {
|
|
if (*(raw + octet) != 0) {
|
|
break;
|
|
}
|
|
}
|
|
if (octet < 0) {
|
|
raw += 32;
|
|
continue;
|
|
}
|
|
*map++ = window;
|
|
*map++ = octet + 1;
|
|
/*
|
|
* Note: potential overlapping move.
|
|
*/
|
|
memmove(map, raw, octet + 1);
|
|
map += octet + 1;
|
|
raw += 32;
|
|
}
|
|
return ((unsigned int)(map - start));
|
|
}
|
|
|
|
isc_result_t
|
|
dns_nsec_buildrdata(dns_db_t *db, dns_dbversion_t *version, dns_dbnode_t *node,
|
|
const dns_name_t *target, unsigned char *buffer,
|
|
dns_rdata_t *rdata) {
|
|
isc_result_t result;
|
|
dns_rdataset_t rdataset;
|
|
isc_region_t r;
|
|
unsigned int i;
|
|
|
|
unsigned char *nsec_bits, *bm;
|
|
unsigned int max_type;
|
|
dns_rdatasetiter_t *rdsiter;
|
|
|
|
REQUIRE(target != NULL);
|
|
|
|
memset(buffer, 0, DNS_NSEC_BUFFERSIZE);
|
|
dns_name_toregion(target, &r);
|
|
memmove(buffer, r.base, r.length);
|
|
r.base = buffer;
|
|
/*
|
|
* Use the end of the space for a raw bitmap leaving enough
|
|
* space for the window identifiers and length octets.
|
|
*/
|
|
bm = r.base + r.length + 512;
|
|
nsec_bits = r.base + r.length;
|
|
dns_nsec_setbit(bm, dns_rdatatype_rrsig, 1);
|
|
dns_nsec_setbit(bm, dns_rdatatype_nsec, 1);
|
|
max_type = dns_rdatatype_nsec;
|
|
dns_rdataset_init(&rdataset);
|
|
rdsiter = NULL;
|
|
result = dns_db_allrdatasets(db, node, version, 0, &rdsiter);
|
|
if (result != ISC_R_SUCCESS) {
|
|
return (result);
|
|
}
|
|
for (result = dns_rdatasetiter_first(rdsiter); result == ISC_R_SUCCESS;
|
|
result = dns_rdatasetiter_next(rdsiter))
|
|
{
|
|
dns_rdatasetiter_current(rdsiter, &rdataset);
|
|
if (rdataset.type != dns_rdatatype_nsec &&
|
|
rdataset.type != dns_rdatatype_nsec3 &&
|
|
rdataset.type != dns_rdatatype_rrsig)
|
|
{
|
|
if (rdataset.type > max_type) {
|
|
max_type = rdataset.type;
|
|
}
|
|
dns_nsec_setbit(bm, rdataset.type, 1);
|
|
}
|
|
dns_rdataset_disassociate(&rdataset);
|
|
}
|
|
|
|
/*
|
|
* At zone cuts, deny the existence of glue in the parent zone.
|
|
*/
|
|
if (dns_nsec_isset(bm, dns_rdatatype_ns) &&
|
|
!dns_nsec_isset(bm, dns_rdatatype_soa))
|
|
{
|
|
for (i = 0; i <= max_type; i++) {
|
|
if (dns_nsec_isset(bm, i) &&
|
|
!dns_rdatatype_iszonecutauth((dns_rdatatype_t)i)) {
|
|
dns_nsec_setbit(bm, i, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
dns_rdatasetiter_destroy(&rdsiter);
|
|
if (result != ISC_R_NOMORE) {
|
|
return (result);
|
|
}
|
|
|
|
nsec_bits += dns_nsec_compressbitmap(nsec_bits, bm, max_type);
|
|
|
|
r.length = (unsigned int)(nsec_bits - r.base);
|
|
INSIST(r.length <= DNS_NSEC_BUFFERSIZE);
|
|
dns_rdata_fromregion(rdata, dns_db_class(db), dns_rdatatype_nsec, &r);
|
|
|
|
return (ISC_R_SUCCESS);
|
|
}
|
|
|
|
isc_result_t
|
|
dns_nsec_build(dns_db_t *db, dns_dbversion_t *version, dns_dbnode_t *node,
|
|
const dns_name_t *target, dns_ttl_t ttl) {
|
|
isc_result_t result;
|
|
dns_rdata_t rdata = DNS_RDATA_INIT;
|
|
unsigned char data[DNS_NSEC_BUFFERSIZE];
|
|
dns_rdatalist_t rdatalist;
|
|
dns_rdataset_t rdataset;
|
|
|
|
dns_rdataset_init(&rdataset);
|
|
dns_rdata_init(&rdata);
|
|
|
|
RETERR(dns_nsec_buildrdata(db, version, node, target, data, &rdata));
|
|
|
|
dns_rdatalist_init(&rdatalist);
|
|
rdatalist.rdclass = dns_db_class(db);
|
|
rdatalist.type = dns_rdatatype_nsec;
|
|
rdatalist.ttl = ttl;
|
|
ISC_LIST_APPEND(rdatalist.rdata, &rdata, link);
|
|
dns_rdatalist_tordataset(&rdatalist, &rdataset);
|
|
result = dns_db_addrdataset(db, node, version, 0, &rdataset, 0, NULL);
|
|
if (result == DNS_R_UNCHANGED) {
|
|
result = ISC_R_SUCCESS;
|
|
}
|
|
|
|
failure:
|
|
if (dns_rdataset_isassociated(&rdataset)) {
|
|
dns_rdataset_disassociate(&rdataset);
|
|
}
|
|
return (result);
|
|
}
|
|
|
|
bool
|
|
dns_nsec_typepresent(dns_rdata_t *nsec, dns_rdatatype_t type) {
|
|
dns_rdata_nsec_t nsecstruct;
|
|
isc_result_t result;
|
|
bool present;
|
|
unsigned int i, len, window;
|
|
|
|
REQUIRE(nsec != NULL);
|
|
REQUIRE(nsec->type == dns_rdatatype_nsec);
|
|
|
|
/* This should never fail */
|
|
result = dns_rdata_tostruct(nsec, &nsecstruct, NULL);
|
|
INSIST(result == ISC_R_SUCCESS);
|
|
|
|
present = false;
|
|
for (i = 0; i < nsecstruct.len; i += len) {
|
|
INSIST(i + 2 <= nsecstruct.len);
|
|
window = nsecstruct.typebits[i];
|
|
len = nsecstruct.typebits[i + 1];
|
|
INSIST(len > 0 && len <= 32);
|
|
i += 2;
|
|
INSIST(i + len <= nsecstruct.len);
|
|
if (window * 256 > type) {
|
|
break;
|
|
}
|
|
if ((window + 1) * 256 <= type) {
|
|
continue;
|
|
}
|
|
if (type < (window * 256) + len * 8) {
|
|
present = dns_nsec_isset(&nsecstruct.typebits[i],
|
|
type % 256);
|
|
}
|
|
break;
|
|
}
|
|
dns_rdata_freestruct(&nsecstruct);
|
|
return (present);
|
|
}
|
|
|
|
isc_result_t
|
|
dns_nsec_nseconly(dns_db_t *db, dns_dbversion_t *version, dns_diff_t *diff,
|
|
bool *answer) {
|
|
dns_dbnode_t *node = NULL;
|
|
dns_rdataset_t rdataset;
|
|
dns_rdata_dnskey_t dnskey;
|
|
isc_result_t result;
|
|
|
|
REQUIRE(answer != NULL);
|
|
|
|
dns_rdataset_init(&rdataset);
|
|
|
|
result = dns_db_getoriginnode(db, &node);
|
|
if (result != ISC_R_SUCCESS) {
|
|
return (result);
|
|
}
|
|
|
|
result = dns_db_findrdataset(db, node, version, dns_rdatatype_dnskey, 0,
|
|
0, &rdataset, NULL);
|
|
dns_db_detachnode(db, &node);
|
|
|
|
if (result == ISC_R_NOTFOUND) {
|
|
*answer = false;
|
|
}
|
|
if (result != ISC_R_SUCCESS) {
|
|
return (result);
|
|
}
|
|
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, &dnskey, NULL);
|
|
RUNTIME_CHECK(result == ISC_R_SUCCESS);
|
|
|
|
if (dnskey.algorithm == DST_ALG_RSAMD5 ||
|
|
dnskey.algorithm == DST_ALG_DH ||
|
|
dnskey.algorithm == DST_ALG_DSA ||
|
|
dnskey.algorithm == DST_ALG_RSASHA1)
|
|
{
|
|
bool deleted = false;
|
|
if (diff != NULL) {
|
|
for (dns_difftuple_t *tuple =
|
|
ISC_LIST_HEAD(diff->tuples);
|
|
tuple != NULL;
|
|
tuple = ISC_LIST_NEXT(tuple, link))
|
|
{
|
|
if (tuple->rdata.type !=
|
|
dns_rdatatype_dnskey ||
|
|
tuple->op != DNS_DIFFOP_DEL) {
|
|
continue;
|
|
}
|
|
|
|
if (dns_rdata_compare(
|
|
&rdata, &tuple->rdata) == 0)
|
|
{
|
|
deleted = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!deleted) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
dns_rdataset_disassociate(&rdataset);
|
|
if (result == ISC_R_SUCCESS) {
|
|
*answer = true;
|
|
}
|
|
if (result == ISC_R_NOMORE) {
|
|
*answer = false;
|
|
result = ISC_R_SUCCESS;
|
|
}
|
|
return (result);
|
|
}
|
|
|
|
/*%
|
|
* Return ISC_R_SUCCESS if we can determine that the name doesn't exist
|
|
* or we can determine whether there is data or not at the name.
|
|
* If the name does not exist return the wildcard name.
|
|
*
|
|
* Return ISC_R_IGNORE when the NSEC is not the appropriate one.
|
|
*/
|
|
isc_result_t
|
|
dns_nsec_noexistnodata(dns_rdatatype_t type, const dns_name_t *name,
|
|
const dns_name_t *nsecname, dns_rdataset_t *nsecset,
|
|
bool *exists, bool *data, dns_name_t *wild,
|
|
dns_nseclog_t logit, void *arg) {
|
|
int order;
|
|
dns_rdata_t rdata = DNS_RDATA_INIT;
|
|
isc_result_t result;
|
|
dns_namereln_t relation;
|
|
unsigned int olabels, nlabels, labels;
|
|
dns_rdata_nsec_t nsec;
|
|
bool atparent;
|
|
bool ns;
|
|
bool soa;
|
|
|
|
REQUIRE(exists != NULL);
|
|
REQUIRE(data != NULL);
|
|
REQUIRE(nsecset != NULL && nsecset->type == dns_rdatatype_nsec);
|
|
|
|
result = dns_rdataset_first(nsecset);
|
|
if (result != ISC_R_SUCCESS) {
|
|
(*logit)(arg, ISC_LOG_DEBUG(3), "failure processing NSEC set");
|
|
return (result);
|
|
}
|
|
dns_rdataset_current(nsecset, &rdata);
|
|
|
|
#ifdef notyet
|
|
if (!dns_nsec_typepresent(&rdata, dns_rdatatype_rrsig) ||
|
|
!dns_nsec_typepresent(&rdata, dns_rdatatype_nsec))
|
|
{
|
|
(*logit)(arg, ISC_LOG_DEBUG(3),
|
|
"NSEC missing RRSIG and/or NSEC from type map");
|
|
return (ISC_R_IGNORE);
|
|
}
|
|
#endif
|
|
|
|
(*logit)(arg, ISC_LOG_DEBUG(3), "looking for relevant NSEC");
|
|
relation = dns_name_fullcompare(name, nsecname, &order, &olabels);
|
|
|
|
if (order < 0) {
|
|
/*
|
|
* The name is not within the NSEC range.
|
|
*/
|
|
(*logit)(arg, ISC_LOG_DEBUG(3),
|
|
"NSEC does not cover name, before NSEC");
|
|
return (ISC_R_IGNORE);
|
|
}
|
|
|
|
if (order == 0) {
|
|
/*
|
|
* The names are the same. If we are validating "."
|
|
* then atparent should not be set as there is no parent.
|
|
*/
|
|
atparent = (olabels != 1) && dns_rdatatype_atparent(type);
|
|
ns = dns_nsec_typepresent(&rdata, dns_rdatatype_ns);
|
|
soa = dns_nsec_typepresent(&rdata, dns_rdatatype_soa);
|
|
if (ns && !soa) {
|
|
if (!atparent) {
|
|
/*
|
|
* This NSEC record is from somewhere higher in
|
|
* the DNS, and at the parent of a delegation.
|
|
* It can not be legitimately used here.
|
|
*/
|
|
(*logit)(arg, ISC_LOG_DEBUG(3),
|
|
"ignoring parent nsec");
|
|
return (ISC_R_IGNORE);
|
|
}
|
|
} else if (atparent && ns && soa) {
|
|
/*
|
|
* This NSEC record is from the child.
|
|
* It can not be legitimately used here.
|
|
*/
|
|
(*logit)(arg, ISC_LOG_DEBUG(3), "ignoring child nsec");
|
|
return (ISC_R_IGNORE);
|
|
}
|
|
if (type == dns_rdatatype_cname || type == dns_rdatatype_nxt ||
|
|
type == dns_rdatatype_nsec || type == dns_rdatatype_key ||
|
|
!dns_nsec_typepresent(&rdata, dns_rdatatype_cname))
|
|
{
|
|
*exists = true;
|
|
*data = dns_nsec_typepresent(&rdata, type);
|
|
(*logit)(arg, ISC_LOG_DEBUG(3),
|
|
"nsec proves name exists (owner) data=%d",
|
|
*data);
|
|
return (ISC_R_SUCCESS);
|
|
}
|
|
(*logit)(arg, ISC_LOG_DEBUG(3), "NSEC proves CNAME exists");
|
|
return (ISC_R_IGNORE);
|
|
}
|
|
|
|
if (relation == dns_namereln_subdomain &&
|
|
dns_nsec_typepresent(&rdata, dns_rdatatype_ns) &&
|
|
!dns_nsec_typepresent(&rdata, dns_rdatatype_soa))
|
|
{
|
|
/*
|
|
* This NSEC record is from somewhere higher in
|
|
* the DNS, and at the parent of a delegation or
|
|
* at a DNAME.
|
|
* It can not be legitimately used here.
|
|
*/
|
|
(*logit)(arg, ISC_LOG_DEBUG(3), "ignoring parent nsec");
|
|
return (ISC_R_IGNORE);
|
|
}
|
|
|
|
if (relation == dns_namereln_subdomain &&
|
|
dns_nsec_typepresent(&rdata, dns_rdatatype_dname))
|
|
{
|
|
(*logit)(arg, ISC_LOG_DEBUG(3), "nsec proves covered by dname");
|
|
*exists = false;
|
|
return (DNS_R_DNAME);
|
|
}
|
|
|
|
result = dns_rdata_tostruct(&rdata, &nsec, NULL);
|
|
if (result != ISC_R_SUCCESS) {
|
|
return (result);
|
|
}
|
|
relation = dns_name_fullcompare(&nsec.next, name, &order, &nlabels);
|
|
if (order == 0) {
|
|
dns_rdata_freestruct(&nsec);
|
|
(*logit)(arg, ISC_LOG_DEBUG(3),
|
|
"ignoring nsec matches next name");
|
|
return (ISC_R_IGNORE);
|
|
}
|
|
|
|
if (order < 0 && !dns_name_issubdomain(nsecname, &nsec.next)) {
|
|
/*
|
|
* The name is not within the NSEC range.
|
|
*/
|
|
dns_rdata_freestruct(&nsec);
|
|
(*logit)(arg, ISC_LOG_DEBUG(3),
|
|
"ignoring nsec because name is past end of range");
|
|
return (ISC_R_IGNORE);
|
|
}
|
|
|
|
if (order > 0 && relation == dns_namereln_subdomain) {
|
|
(*logit)(arg, ISC_LOG_DEBUG(3),
|
|
"nsec proves name exist (empty)");
|
|
dns_rdata_freestruct(&nsec);
|
|
*exists = true;
|
|
*data = false;
|
|
return (ISC_R_SUCCESS);
|
|
}
|
|
if (wild != NULL) {
|
|
dns_name_t common;
|
|
dns_name_init(&common, NULL);
|
|
if (olabels > nlabels) {
|
|
labels = dns_name_countlabels(nsecname);
|
|
dns_name_getlabelsequence(nsecname, labels - olabels,
|
|
olabels, &common);
|
|
} else {
|
|
labels = dns_name_countlabels(&nsec.next);
|
|
dns_name_getlabelsequence(&nsec.next, labels - nlabels,
|
|
nlabels, &common);
|
|
}
|
|
result = dns_name_concatenate(dns_wildcardname, &common, wild,
|
|
NULL);
|
|
if (result != ISC_R_SUCCESS) {
|
|
dns_rdata_freestruct(&nsec);
|
|
(*logit)(arg, ISC_LOG_DEBUG(3),
|
|
"failure generating wildcard name");
|
|
return (result);
|
|
}
|
|
}
|
|
dns_rdata_freestruct(&nsec);
|
|
(*logit)(arg, ISC_LOG_DEBUG(3), "nsec range ok");
|
|
*exists = false;
|
|
return (ISC_R_SUCCESS);
|
|
}
|
|
|
|
bool
|
|
dns_nsec_requiredtypespresent(dns_rdataset_t *nsecset) {
|
|
dns_rdataset_t rdataset;
|
|
isc_result_t result;
|
|
bool found = false;
|
|
|
|
REQUIRE(DNS_RDATASET_VALID(nsecset));
|
|
REQUIRE(nsecset->type == dns_rdatatype_nsec);
|
|
|
|
dns_rdataset_init(&rdataset);
|
|
dns_rdataset_clone(nsecset, &rdataset);
|
|
|
|
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);
|
|
if (!dns_nsec_typepresent(&rdata, dns_rdatatype_nsec) ||
|
|
!dns_nsec_typepresent(&rdata, dns_rdatatype_rrsig))
|
|
{
|
|
dns_rdataset_disassociate(&rdataset);
|
|
return (false);
|
|
}
|
|
found = true;
|
|
}
|
|
dns_rdataset_disassociate(&rdataset);
|
|
return (found);
|
|
}
|