From f603a4f920e6953b11667d424956fd47373870e9 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Mon, 12 Jan 2026 21:55:41 +0000 Subject: [PATCH] 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. --- CHANGELOG | 10 ++++++++++ src/dnsmasq.h | 7 +++---- src/dnssec.c | 47 +++++++++++++++++++++++------------------------ 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 32981f8..13f0de9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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 Redesign the interaction between DNSSEC validation and per-domain servers, specified as --server=//. This should diff --git a/src/dnsmasq.h b/src/dnsmasq.h index c636563..40f2b5c 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -761,10 +761,9 @@ struct dyndir { #define STAT_NEED_DS 0x40000 #define STAT_NEED_KEY 0x50000 #define STAT_TRUNCATED 0x60000 -#define STAT_SECURE_WILDCARD 0x70000 -#define STAT_OK 0x80000 -#define STAT_ABANDONED 0x90000 -#define STAT_ASYNC 0xa0000 +#define STAT_OK 0x70000 +#define STAT_ABANDONED 0x80000 +#define STAT_ASYNC 0x90000 #define DNSSEC_FAIL_NYV 0x0001 /* key not yet valid */ #define DNSSEC_FAIL_EXP 0x0002 /* key expired */ diff --git a/src/dnssec.c b/src/dnssec.c index 0860daa..ef67eb6 100644 --- a/src/dnssec.c +++ b/src/dnssec.c @@ -438,8 +438,6 @@ int dec_counter(int *counter, char *message) /* Validate a single RRset (class, type, name) in the supplied DNS reply Return code: 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_ABANDONED validation abandoned do to excess resource usage. 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. */ 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) { 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); int time_check = is_check_date(curtime); - if (wildcard_out) - *wildcard_out = NULL; + if (wildcard_offset_out) + *wildcard_offset_out = 0; 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; /* if more labels than in RRsig name, hash *. 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--) { @@ -584,11 +584,11 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in name_start++; } - if (wildcard_out) - *wildcard_out = name_start+1; - + if (wildcard_offset_out) + *wildcard_offset_out = name_start - name + 1; + name_start--; - *name_start = '*'; + *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; 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 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. */ 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; 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 == '.') closest_encloser++; - if (wildname && hostname_isequal(closest_encloser, wildname)) + if (wild_offset != 0 && name - closest_encloser == wild_offset) break; - + if (dec_counter(validate_counter, NULL)) 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; /* Finally, check that there's no seat of wildcard synthesis */ - if (!wildname) + if (wild_offset == 0) { if (!(wildcard = strchr(next_closest, '.')) || wildcard == next_closest) 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. */ 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 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) return prove_non_existence_nsec(header, plen, nsecset, rrsig_labels, nsecs_found, daemon->workspacename, keyname, name, qtype, nons); 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 return DNSSEC_FAIL_NONSEC; } @@ -2183,8 +2183,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch else { /* Not done, validate now */ - int sigcnt, rrcnt; - char *wildname; + int sigcnt, rrcnt, wild_offset; if (!explore_rrset(header, plen, class1, type1, name, keyname, &sigcnt, &rrcnt)) 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; 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)) { @@ -2247,7 +2246,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch return rc; } - /* rc is now STAT_SECURE or STAT_SECURE_WILDCARD */ + /* rc is now STAT_SECURE */ /* Note that RR is validated */ 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. That's not a problem since if the RRsets later fail we'll return BOGUS then. */ - if (STAT_ISEQUAL(rc, STAT_SECURE_WILDCARD) && - ((rc_nsec = prove_non_existence(header, plen, keyname, name, type1, class1, wildname, NULL, NULL, validate_counter))) != 0) + if (wild_offset != 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); 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. 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, 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) return STAT_ABANDONED;