Fix DNSSEC fail with CNAME replies to DS queries.

A CNAME reply to a DNSSEC query was confusing the validation logic.
It now accepts a signed CNAME reply to a DS query as proof that no DS
exists at the domain. This fixes the DS/zone break detection logic.
This commit is contained in:
Simon Kelley
2026-01-18 18:57:08 +00:00
parent 8eb36844a0
commit cb321709e9
4 changed files with 52 additions and 36 deletions

View File

@@ -19,6 +19,11 @@ version 2.93
(eg rivcoed.org) will still fail. Thanks to Petr Menšík (eg rivcoed.org) will still fail. Thanks to Petr Menšík
for the bug report. for the bug report.
Fix DNSSEC fail with CNAME replies to DS queries. As CNAME reply
to a DNSSEC query was confusing the validation logic. It now
accepts a signed CNAME reply to a DS query as proof that no DS
exists at the domain. This fixes the DS/zone break detection logic.
version 2.92 version 2.92
Redesign the interaction between DNSSEC validation and per-domain Redesign the interaction between DNSSEC validation and per-domain

View File

@@ -1467,7 +1467,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name,
char *keyname, int class, int *validate_count); char *keyname, int class, int *validate_count);
int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class, int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class,
int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_count); int check_unsigned, int *neganswer, int *prim_ok, int *nons, int *nsec_ttl, int *validate_count);
int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen); int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen);
size_t filter_rrsigs(struct dns_header *header, size_t plen); size_t filter_rrsigs(struct dns_header *header, size_t plen);
int setup_timestamp(void); int setup_timestamp(void);

View File

@@ -991,7 +991,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
char *keyname, int class, int *validate_counter) char *keyname, int class, int *validate_counter)
{ {
unsigned char *p = (unsigned char *)(header+1); unsigned char *p = (unsigned char *)(header+1);
int qtype, qclass, rc, i, neganswer = 0, nons = 0, servfail = 0, neg_ttl = 0, found_supported = 0; int qtype, qclass, rc, i, neganswer = 0, prim_ok = 0, nons = 0, servfail = 0, neg_ttl = 0, found_supported = 0;
int aclass, atype, rdlen, flags; int aclass, atype, rdlen, flags;
unsigned long ttl; unsigned long ttl;
union all_addr a; union all_addr a;
@@ -1002,7 +1002,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
if (RCODE(header) == SERVFAIL) if (RCODE(header) == SERVFAIL)
servfail = neganswer = nons = 1; servfail = neganswer = nons = 1;
else else
rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &nons, &neg_ttl, validate_counter); rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &prim_ok, &nons, &neg_ttl, validate_counter);
p = (unsigned char *)(header+1); p = (unsigned char *)(header+1);
if (ntohs(header->qdcount) != 1 || if (ntohs(header->qdcount) != 1 ||
@@ -1019,27 +1019,32 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
{ {
if (STAT_ISEQUAL(rc, STAT_INSECURE)) if (STAT_ISEQUAL(rc, STAT_INSECURE))
{ {
if (option_bool(OPT_BOGUSPRIV) && /* A INSECURE DS answer is OK if it's negative and there's a CNAME answer to the DS answer which is
(flags = in_arpa_name_2_addr(name, &a)) && signed, since that's enough to prove that the DS record doesn't exist. */
((flags == F_IPV6 && private_net6(&a.addr6, 0)) || (flags == F_IPV4 && private_net(a.addr4, 0)))) if (!neganswer || !prim_ok)
{ {
my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming that's OK for a RFC-1918 address."), name); if (option_bool(OPT_BOGUSPRIV) &&
neganswer = 1; (flags = in_arpa_name_2_addr(name, &a)) &&
nons = 0; /* If we're faking a DS, fake one with an NS. */ ((flags == F_IPV6 && private_net6(&a.addr6, 0)) || (flags == F_IPV4 && private_net(a.addr4, 0))))
neg_ttl = DNSSEC_ASSUMED_DS_TTL; {
} my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming that's OK for a RFC-1918 address."), name);
else if (lookup_domain(name, F_DOMAINSRV, NULL, NULL)) neganswer = 1;
{ nons = 0; /* If we're faking a DS, fake one with an NS. */
my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming non-DNSSEC domain-specific server."), name); neg_ttl = DNSSEC_ASSUMED_DS_TTL;
neganswer = 1; }
nons = 0; /* If we're faking a DS, fake one with an NS. */ else if (lookup_domain(name, F_DOMAINSRV, NULL, NULL))
neg_ttl = DNSSEC_ASSUMED_DS_TTL; {
} my_syslog(LOG_INFO, _("Insecure reply received for DS %s, assuming non-DNSSEC domain-specific server."), name);
else neganswer = 1;
{ nons = 0; /* If we're faking a DS, fake one with an NS. */
my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name); neg_ttl = DNSSEC_ASSUMED_DS_TTL;
log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS - not secure", 0); }
return STAT_BOGUS | DNSSEC_FAIL_INDET; else
{
my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name);
log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS - not secure", 0);
return STAT_BOGUS | DNSSEC_FAIL_INDET;
}
} }
} }
else else
@@ -1964,7 +1969,7 @@ static int zone_status(char *name, int class, char *keyname, time_t now)
if the nons argument is non-NULL. if the nons argument is non-NULL.
*/ */
int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname,
int *class, int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_counter) int *class, int check_unsigned, int *neganswer, int *prim_ok, int *nons, int *nsec_ttl, int *validate_counter)
{ {
static unsigned char **targets = NULL; static unsigned char **targets = NULL;
static int target_sz = 0; static int target_sz = 0;
@@ -2273,6 +2278,20 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
secure = STAT_INSECURE; secure = STAT_INSECURE;
} }
/* For a DS record, we are interested also in if the answer to the DS query was
a CNAME RRset which validated. That's proof that the DS doesn't exist,
even if it's a CNAME which is not signed, and therefore we have no proof
of what it actually _is_. This return tells us that the answer to
primary query is secure, even is the whole answer is insecure, because
something down the CNAME list doesn't validate or doesn't exist.
Note that prim_ok is only valid when neganswer is true, ie either
the answer is the requested record or it's a CNAME that ends
in a missing answer or an unsigned zone.
*/
if (prim_ok)
*prim_ok = !targets[0];
/* OK, all the RRsets validate, now see if we have a missing answer or CNAME target. */ /* OK, all the RRsets validate, now see if we have a missing answer or CNAME target. */
for (j = 0; j <targetidx; j++) for (j = 0; j <targetidx; j++)
if ((p2 = targets[j])) if ((p2 = targets[j]))
@@ -2283,20 +2302,12 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
if (!extract_name(header, plen, &p2, name, EXTR_NAME_EXTRACT, 10)) if (!extract_name(header, plen, &p2, name, EXTR_NAME_EXTRACT, 10))
return STAT_BOGUS; /* bad packet */ return STAT_BOGUS; /* bad packet */
/* NXDOMAIN or NODATA reply, unanswered question is (name, qclass, qtype) */ /* NXDOMAIN or NODATA reply, unanswered question is (name, qclass, qtype)
This situation is OK if either the answer is in an unsigned zone, or there's NSEC records. */
/* For anything other than a DS record, this situation is OK if either
the answer is in an unsigned zone, or there's NSEC records.
For a DS record, we return INSECURE, which almost always turns
into BOGUS in the caller. */
if ((rc_nsec = prove_non_existence(header, plen, keyname, name, qtype, qclass, 0, nons, nsec_ttl, validate_counter)) != 0) if ((rc_nsec = prove_non_existence(header, plen, keyname, name, qtype, qclass, 0, nons, nsec_ttl, validate_counter)) != 0)
{ {
if (rc_nsec & DNSSEC_FAIL_WORK) if (rc_nsec & DNSSEC_FAIL_WORK)
return STAT_ABANDONED; return STAT_ABANDONED;
/* Empty DS without NSECS */
if (qtype == T_DS)
return STAT_INSECURE;
if ((rc_nsec & (DNSSEC_FAIL_NONSEC | DNSSEC_FAIL_NSEC3_ITERS)) && if ((rc_nsec & (DNSSEC_FAIL_NONSEC | DNSSEC_FAIL_NSEC3_ITERS)) &&
!STAT_ISEQUAL((rc = zone_status(name, qclass, keyname, now)), STAT_SECURE)) !STAT_ISEQUAL((rc = zone_status(name, qclass, keyname, now)), STAT_SECURE))

View File

@@ -960,7 +960,7 @@ static void dnssec_validate(struct frec *forward, struct dns_header *header,
status = dnssec_validate_ds(now, header, plen, daemon->namebuff, daemon->keyname, forward->class, &orig->validate_counter); status = dnssec_validate_ds(now, header, plen, daemon->namebuff, daemon->keyname, forward->class, &orig->validate_counter);
else else
status = dnssec_validate_reply(now, header, plen, daemon->namebuff, daemon->keyname, &forward->class, status = dnssec_validate_reply(now, header, plen, daemon->namebuff, daemon->keyname, &forward->class,
!option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, &orig->validate_counter); !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, NULL, &orig->validate_counter);
if (STAT_ISEQUAL(status, STAT_ABANDONED)) if (STAT_ISEQUAL(status, STAT_ABANDONED))
log_resource = 1; log_resource = 1;
@@ -2277,7 +2277,7 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si
new_status = dnssec_validate_ds(now, header, n, name, keyname, class, validatecount); new_status = dnssec_validate_ds(now, header, n, name, keyname, class, validatecount);
else else
new_status = dnssec_validate_reply(now, header, n, name, keyname, &class, new_status = dnssec_validate_reply(now, header, n, name, keyname, &class,
!option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, validatecount); !option_bool(OPT_DNSSEC_IGN_NS), NULL, NULL, NULL, NULL, validatecount);
if (!STAT_ISEQUAL(new_status, STAT_NEED_DS) && !STAT_ISEQUAL(new_status, STAT_NEED_KEY) && !STAT_ISEQUAL(new_status, STAT_ABANDONED)) if (!STAT_ISEQUAL(new_status, STAT_NEED_DS) && !STAT_ISEQUAL(new_status, STAT_NEED_KEY) && !STAT_ISEQUAL(new_status, STAT_ABANDONED))
break; break;