Fix a corner-case in DNSSEC validation with wildcards.

If we have a wildcard record *.example.com and recieve a query for
a.example.com then that's OK, but we have to check that there isn't
an actual a.example.com record. The corner case is when we get a
query for *.example.com in that case the non-existence check
is not required, but was being done.

Thanks to Jan Breig for spotting this.
This commit is contained in:
Simon Kelley committed 2026-01-12 21:55:41 +00:00
1 parent c090f1d17b
commit f603a4f920
3 files changed
+36 -28

No files matched your search

+10
View File
@@ -1,3 +1,13 @@
version 2.93
Fix a corner-case in DNSSEC validation with wildcards. If we have
a wildcard record *.example.com and recieve a query for
a.example.com then that's OK, but we have to check that there isn't
an actual a.example.com record. The corner case is when we get a
query for *.example.com in that case the non-existence check
is not required, was being done. Thanks to Jan Breig for
spotting this.
version 2.92 version 2.92
Redesign the interaction between DNSSEC validation and per-domain Redesign the interaction between DNSSEC validation and per-domain
servers, specified as --server=/<domain>/<ip-address>. This should servers, specified as --server=/<domain>/<ip-address>. This should
+3 -4
View File
@@ -761,10 +761,9 @@ struct dyndir {
#define STAT_NEED_DS 0x40000 #define STAT_NEED_DS 0x40000
#define STAT_NEED_KEY 0x50000 #define STAT_NEED_KEY 0x50000
#define STAT_TRUNCATED 0x60000 #define STAT_TRUNCATED 0x60000
#define STAT_SECURE_WILDCARD 0x70000 #define STAT_OK 0x70000
#define STAT_OK 0x80000 #define STAT_ABANDONED 0x80000
#define STAT_ABANDONED 0x90000 #define STAT_ASYNC 0x90000
#define STAT_ASYNC 0xa0000
#define DNSSEC_FAIL_NYV 0x0001 /* key not yet valid */ #define DNSSEC_FAIL_NYV 0x0001 /* key not yet valid */
#define DNSSEC_FAIL_EXP 0x0002 /* key expired */ #define DNSSEC_FAIL_EXP 0x0002 /* key expired */
+23 -24
View File
@@ -438,8 +438,6 @@ int dec_counter(int *counter, char *message)
/* Validate a single RRset (class, type, name) in the supplied DNS reply /* Validate a single RRset (class, type, name) in the supplied DNS reply
Return code: Return code:
STAT_SECURE if it validates. STAT_SECURE if it validates.
STAT_SECURE_WILDCARD if it validates and is the result of wildcard expansion.
(In this case *wildcard_out points to the "body" of the wildcard within name.)
STAT_BOGUS signature is wrong, bad packet. STAT_BOGUS signature is wrong, bad packet.
STAT_ABANDONED validation abandoned do to excess resource usage. STAT_ABANDONED validation abandoned do to excess resource usage.
STAT_NEED_KEY need DNSKEY to complete validation (name is returned in keyname) STAT_NEED_KEY need DNSKEY to complete validation (name is returned in keyname)
@@ -455,7 +453,7 @@ int dec_counter(int *counter, char *message)
ttl_out is the floor on TTL, based on TTL and orig_ttl and expiration of sig used to validate. ttl_out is the floor on TTL, based on TTL and orig_ttl and expiration of sig used to validate.
*/ */
static int validate_rrset(time_t now, struct dns_header *header, size_t plen, int class, int type, int sigidx, int rrsetidx, static int validate_rrset(time_t now, struct dns_header *header, size_t plen, int class, int type, int sigidx, int rrsetidx,
char *name, char *keyname, char **wildcard_out, struct blockdata *key, int keylen, char *name, char *keyname, int *wildcard_offset_out, struct blockdata *key, int keylen,
int algo_in, int keytag_in, unsigned long *ttl_out, int *validate_counter) int algo_in, int keytag_in, unsigned long *ttl_out, int *validate_counter)
{ {
unsigned char *p; unsigned char *p;
@@ -468,8 +466,8 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
unsigned long curtime = time(0); unsigned long curtime = time(0);
int time_check = is_check_date(curtime); int time_check = is_check_date(curtime);
if (wildcard_out) if (wildcard_offset_out)
*wildcard_out = NULL; *wildcard_offset_out = 0;
name_labels = count_labels(name); /* For 4035 5.3.2 check */ name_labels = count_labels(name); /* For 4035 5.3.2 check */
@@ -574,7 +572,9 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
name_start = name; name_start = name;
/* if more labels than in RRsig name, hash *.<no labels in rrsig labels field> 4035 5.3.2 */ /* if more labels than in RRsig name, hash *.<no labels in rrsig labels field> 4035 5.3.2 */
if (labels < name_labels) /* If the name is already the wildcard, we're not going to change it. */
if (labels < name_labels &&
!(name_labels - labels == 1 && name_start[0] == '*' && name_start[1] == '.'))
{ {
for (j = name_labels - labels; j != 0; j--) for (j = name_labels - labels; j != 0; j--)
{ {
@@ -584,11 +584,11 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
name_start++; name_start++;
} }
if (wildcard_out) if (wildcard_offset_out)
*wildcard_out = name_start+1; *wildcard_offset_out = name_start - name + 1;
name_start--; name_start--;
*name_start = '*'; *name_start = '*';
} }
wire_len = to_wire(name_start); wire_len = to_wire(name_start);
@@ -687,7 +687,7 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
return STAT_ABANDONED; return STAT_ABANDONED;
if (verify(crecp->addr.key.keydata, crecp->addr.key.keylen, sig, sig_len, digest, hash->digest_size, algo)) if (verify(crecp->addr.key.keydata, crecp->addr.key.keylen, sig, sig_len, digest, hash->digest_size, algo))
return (labels < name_labels) ? STAT_SECURE_WILDCARD : STAT_SECURE; return STAT_SECURE;
/* An attacker can waste a lot of our CPU by setting up a giant DNSKEY RRSET full of failing /* An attacker can waste a lot of our CPU by setting up a giant DNSKEY RRSET full of failing
keys, all of which we have to try. Since many failing keys is not likely for keys, all of which we have to try. Since many failing keys is not likely for
@@ -1545,7 +1545,7 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
/* returns 0 on success, or DNSSEC_FAIL_* value on failure. */ /* returns 0 on success, or DNSSEC_FAIL_* value on failure. */
static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, unsigned char **nsecs, int nsec_count, char *workspace1, static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, unsigned char **nsecs, int nsec_count, char *workspace1,
char *workspace2, char *name, int type, char *wildname, int *nons, int *validate_counter) char *workspace2, char *name, int type, int wild_offset, int *nons, int *validate_counter)
{ {
unsigned char *salt, *p, *digest; unsigned char *salt, *p, *digest;
int digest_len, i, iterations, salt_len, base32_len, algo = 0; int digest_len, i, iterations, salt_len, base32_len, algo = 0;
@@ -1643,9 +1643,9 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
if (*closest_encloser == '.') if (*closest_encloser == '.')
closest_encloser++; closest_encloser++;
if (wildname && hostname_isequal(closest_encloser, wildname)) if (wild_offset != 0 && name - closest_encloser == wild_offset)
break; break;
if (dec_counter(validate_counter, NULL)) if (dec_counter(validate_counter, NULL))
return DNSSEC_FAIL_WORK; return DNSSEC_FAIL_WORK;
@@ -1687,7 +1687,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
return DNSSEC_FAIL_NONSEC; return DNSSEC_FAIL_NONSEC;
/* Finally, check that there's no seat of wildcard synthesis */ /* Finally, check that there's no seat of wildcard synthesis */
if (!wildname) if (wild_offset == 0)
{ {
if (!(wildcard = strchr(next_closest, '.')) || wildcard == next_closest) if (!(wildcard = strchr(next_closest, '.')) || wildcard == next_closest)
return DNSSEC_FAIL_NONSEC; return DNSSEC_FAIL_NONSEC;
@@ -1710,7 +1710,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
/* returns 0 on success, or DNSSEC_FAIL_* value on failure. */ /* returns 0 on success, or DNSSEC_FAIL_* value on failure. */
static int prove_non_existence(struct dns_header *header, size_t plen, char *keyname, char *name, int qtype, int qclass, static int prove_non_existence(struct dns_header *header, size_t plen, char *keyname, char *name, int qtype, int qclass,
char *wildname, int *nons, int *nsec_ttl, int *validate_counter) int wild_offset, int *nons, int *nsec_ttl, int *validate_counter)
{ {
static unsigned char **nsecset = NULL, **rrsig_labels = NULL; static unsigned char **nsecset = NULL, **rrsig_labels = NULL;
static int nsecset_sz = 0, rrsig_labels_sz = 0; static int nsecset_sz = 0, rrsig_labels_sz = 0;
@@ -1858,7 +1858,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
if (type_found == T_NSEC) if (type_found == T_NSEC)
return prove_non_existence_nsec(header, plen, nsecset, rrsig_labels, nsecs_found, daemon->workspacename, keyname, name, qtype, nons); return prove_non_existence_nsec(header, plen, nsecset, rrsig_labels, nsecs_found, daemon->workspacename, keyname, name, qtype, nons);
else if (type_found == T_NSEC3) else if (type_found == T_NSEC3)
return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wildname, nons, validate_counter); return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wild_offset, nons, validate_counter);
else else
return DNSSEC_FAIL_NONSEC; return DNSSEC_FAIL_NONSEC;
} }
@@ -2183,8 +2183,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
else else
{ {
/* Not done, validate now */ /* Not done, validate now */
int sigcnt, rrcnt; int sigcnt, rrcnt, wild_offset;
char *wildname;
if (!explore_rrset(header, plen, class1, type1, name, keyname, &sigcnt, &rrcnt)) if (!explore_rrset(header, plen, class1, type1, name, keyname, &sigcnt, &rrcnt))
return STAT_BOGUS; return STAT_BOGUS;
@@ -2238,7 +2237,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
{ {
unsigned long sig_ttl; unsigned long sig_ttl;
rc = validate_rrset(now, header, plen, class1, type1, sigcnt, rc = validate_rrset(now, header, plen, class1, type1, sigcnt,
rrcnt, name, keyname, &wildname, NULL, 0, 0, 0, &sig_ttl, validate_counter); rrcnt, name, keyname, &wild_offset, NULL, 0, 0, 0, &sig_ttl, validate_counter);
if (STAT_ISEQUAL(rc, STAT_BOGUS) || STAT_ISEQUAL(rc, STAT_NEED_KEY) || STAT_ISEQUAL(rc, STAT_NEED_DS) || STAT_ISEQUAL(rc, STAT_ABANDONED)) if (STAT_ISEQUAL(rc, STAT_BOGUS) || STAT_ISEQUAL(rc, STAT_NEED_KEY) || STAT_ISEQUAL(rc, STAT_NEED_DS) || STAT_ISEQUAL(rc, STAT_ABANDONED))
{ {
@@ -2247,7 +2246,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
return rc; return rc;
} }
/* rc is now STAT_SECURE or STAT_SECURE_WILDCARD */ /* rc is now STAT_SECURE */
/* Note that RR is validated */ /* Note that RR is validated */
daemon->rr_status[i] = sig_ttl; daemon->rr_status[i] = sig_ttl;
@@ -2273,8 +2272,8 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
Note that we may not yet have validated the NSEC/NSEC3 RRsets. Note that we may not yet have validated the NSEC/NSEC3 RRsets.
That's not a problem since if the RRsets later fail That's not a problem since if the RRsets later fail
we'll return BOGUS then. */ we'll return BOGUS then. */
if (STAT_ISEQUAL(rc, STAT_SECURE_WILDCARD) && if (wild_offset != 0 &&
((rc_nsec = prove_non_existence(header, plen, keyname, name, type1, class1, wildname, NULL, NULL, validate_counter))) != 0) ((rc_nsec = prove_non_existence(header, plen, keyname, name, type1, class1, wild_offset, NULL, NULL, validate_counter))) != 0)
return (rc_nsec & DNSSEC_FAIL_WORK) ? STAT_ABANDONED : (STAT_BOGUS | rc_nsec); return (rc_nsec & DNSSEC_FAIL_WORK) ? STAT_ABANDONED : (STAT_BOGUS | rc_nsec);
rc = STAT_SECURE; rc = STAT_SECURE;
@@ -2302,7 +2301,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
the answer is in an unsigned zone, or there's NSEC records. the answer is in an unsigned zone, or there's NSEC records.
For a DS record, we return INSECURE, which almost always turns For a DS record, we return INSECURE, which almost always turns
into BOGUS in the caller. */ into BOGUS in the caller. */
if ((rc_nsec = prove_non_existence(header, plen, keyname, name, qtype, qclass, NULL, 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;