Update NSEC3 iterations handling to conform with RFC 9276.

This commit is contained in:
Simon Kelley
2023-12-31 23:28:11 +00:00
parent be73efc020
commit 51471cafa5
2 changed files with 59 additions and 52 deletions

View File

@@ -757,6 +757,8 @@ struct dyndir {
#define DNSSEC_FAIL_NONSEC 0x0040 /* No NSEC */ #define DNSSEC_FAIL_NONSEC 0x0040 /* No NSEC */
#define DNSSEC_FAIL_NODSSUP 0x0080 /* no supported DS algo. */ #define DNSSEC_FAIL_NODSSUP 0x0080 /* no supported DS algo. */
#define DNSSEC_FAIL_NOKEY 0x0100 /* no DNSKEY */ #define DNSSEC_FAIL_NOKEY 0x0100 /* no DNSKEY */
#define DNSSEC_FAIL_NSEC3_ITERS 0x0200 /* too many iterations in NSEC3 */
#define DNSSEC_FAIL_BADPACKET 0x0400 /* bad packet */
#define STAT_ISEQUAL(a, b) (((a) & 0xffff0000) == (b)) #define STAT_ISEQUAL(a, b) (((a) & 0xffff0000) == (b))

View File

@@ -1179,6 +1179,7 @@ static int hostname_cmp(const char *a, const char *b)
} }
} }
/* returns 0 on success, or DNSSEC_FAIL_* value on failure. */
static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsigned char **nsecs, unsigned char **labels, int nsec_count, static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsigned char **nsecs, unsigned char **labels, int nsec_count,
char *workspace1_in, char *workspace2, char *name, int type, int *nons) char *workspace1_in, char *workspace2, char *name, int type, int *nons)
{ {
@@ -1203,7 +1204,7 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
GETSHORT(rdlen, p); GETSHORT(rdlen, p);
psave = p; psave = p;
if (!extract_name(header, plen, &p, workspace2, 1, 10)) if (!extract_name(header, plen, &p, workspace2, 1, 10))
return 0; return DNSSEC_FAIL_BADPACKET;
/* If NSEC comes from wildcard expansion, use original wildcard /* If NSEC comes from wildcard expansion, use original wildcard
as name for computation. */ as name for computation. */
@@ -1231,7 +1232,7 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
{ {
/* 4035 para 5.4. Last sentence */ /* 4035 para 5.4. Last sentence */
if (type == T_NSEC || type == T_RRSIG) if (type == T_NSEC || type == T_RRSIG)
return 1; return 0;
/* NSEC with the same name as the RR we're testing, check /* NSEC with the same name as the RR we're testing, check
that the type in question doesn't appear in the type map */ that the type in question doesn't appear in the type map */
@@ -1247,25 +1248,25 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
/* A CNAME answer would also be valid, so if there's a CNAME is should /* A CNAME answer would also be valid, so if there's a CNAME is should
have been returned. */ have been returned. */
if ((p[2] & (0x80 >> T_CNAME)) != 0) if ((p[2] & (0x80 >> T_CNAME)) != 0)
return 0; return DNSSEC_FAIL_NONSEC;
/* If the SOA bit is set for a DS record, then we have the /* If the SOA bit is set for a DS record, then we have the
DS from the wrong side of the delegation. For the root DS, DS from the wrong side of the delegation. For the root DS,
this is expected. */ this is expected. */
if (name_labels != 0 && type == T_DS && (p[2] & (0x80 >> T_SOA)) != 0) if (name_labels != 0 && type == T_DS && (p[2] & (0x80 >> T_SOA)) != 0)
return 0; return DNSSEC_FAIL_NONSEC;
} }
while (rdlen >= 2) while (rdlen >= 2)
{ {
if (!CHECK_LEN(header, p, plen, rdlen)) if (!CHECK_LEN(header, p, plen, rdlen))
return 0; return DNSSEC_FAIL_BADPACKET;
if (p[0] == type >> 8) if (p[0] == type >> 8)
{ {
/* Does the NSEC say our type exists? */ /* Does the NSEC say our type exists? */
if (offset < p[1] && (p[offset+2] & mask) != 0) if (offset < p[1] && (p[offset+2] & mask) != 0)
return 0; return DNSSEC_FAIL_NONSEC;
break; /* finished checking */ break; /* finished checking */
} }
@@ -1281,17 +1282,17 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
/* Normal case, name falls between NSEC name and next domain name, /* Normal case, name falls between NSEC name and next domain name,
wrap around case, name falls between NSEC name (rc == -1) and end */ wrap around case, name falls between NSEC name (rc == -1) and end */
if (hostname_cmp(workspace2, name) >= 0 || hostname_cmp(workspace1, workspace2) >= 0) if (hostname_cmp(workspace2, name) >= 0 || hostname_cmp(workspace1, workspace2) >= 0)
return 1; return 0;
} }
else else
{ {
/* wrap around case, name falls between start and next domain name */ /* wrap around case, name falls between start and next domain name */
if (hostname_cmp(workspace1, workspace2) >= 0 && hostname_cmp(workspace2, name) >=0 ) if (hostname_cmp(workspace1, workspace2) >= 0 && hostname_cmp(workspace2, name) >=0 )
return 1; return 0;
} }
} }
return 0; return DNSSEC_FAIL_NONSEC;
} }
/* return digest length, or zero on error */ /* return digest length, or zero on error */
@@ -1464,6 +1465,7 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
return 0; return 0;
} }
/* 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, 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) char *workspace1, char *workspace2, char *name, int type, char *wildname, int *nons)
{ {
@@ -1485,7 +1487,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
for (i = 0; i < nsec_count; i++) for (i = 0; i < nsec_count; i++)
{ {
if (!(p = skip_name(nsecs[i], header, plen, 15))) if (!(p = skip_name(nsecs[i], header, plen, 15)))
return 0; /* bad packet */ return DNSSEC_FAIL_BADPACKET; /* bad packet */
p += 10; /* type, class, TTL, rdlen */ p += 10; /* type, class, TTL, rdlen */
algo = *p++; algo = *p++;
@@ -1496,22 +1498,19 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
/* No usable NSEC3s */ /* No usable NSEC3s */
if (i == nsec_count) if (i == nsec_count)
return 0; return DNSSEC_FAIL_NONSEC;
p++; /* flags */ p++; /* flags */
GETSHORT (iterations, p); GETSHORT (iterations, p);
/* Upper-bound iterations, to avoid DoS. /* Upper-bound iterations, to avoid DoS. RFC 9276 refers. */
Strictly, there are lower bounds for small keys, but if (iterations > 150)
since we don't have key size info here, at least limit return DNSSEC_FAIL_NSEC3_ITERS;
to the largest bound, for 4096-bit keys. RFC 5155 10.3 */
if (iterations > 2500)
return 0;
salt_len = *p++; salt_len = *p++;
salt = p; salt = p;
if (!CHECK_LEN(header, salt, plen, salt_len)) if (!CHECK_LEN(header, salt, plen, salt_len))
return 0; /* bad packet */ return DNSSEC_FAIL_BADPACKET; /* bad packet */
/* Now prune so we only have NSEC3 records with same iterations, salt and algo */ /* Now prune so we only have NSEC3 records with same iterations, salt and algo */
for (i = 0; i < nsec_count; i++) for (i = 0; i < nsec_count; i++)
@@ -1543,7 +1542,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
continue; continue;
if (!CHECK_LEN(header, p, plen, salt_len)) if (!CHECK_LEN(header, p, plen, salt_len))
return 0; /* bad packet */ return DNSSEC_FAIL_BADPACKET; /* bad packet */
if (memcmp(p, salt, salt_len) != 0) if (memcmp(p, salt, salt_len) != 0)
continue; continue;
@@ -1553,10 +1552,10 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
} }
if ((digest_len = hash_name(name, &digest, hash, salt, salt_len, iterations)) == 0) if ((digest_len = hash_name(name, &digest, hash, salt, salt_len, iterations)) == 0)
return 0; return DNSSEC_FAIL_NONSEC;
if (check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, nons, count_labels(name))) if (check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, nons, count_labels(name)))
return 1; return 0;
/* Can't find an NSEC3 which covers the name directly, we need the "closest encloser NSEC3" /* Can't find an NSEC3 which covers the name directly, we need the "closest encloser NSEC3"
or an answer inferred from a wildcard record. */ or an answer inferred from a wildcard record. */
@@ -1572,14 +1571,16 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
break; break;
if ((digest_len = hash_name(closest_encloser, &digest, hash, salt, salt_len, iterations)) == 0) if ((digest_len = hash_name(closest_encloser, &digest, hash, salt, salt_len, iterations)) == 0)
return 0; return DNSSEC_FAIL_NONSEC;
for (i = 0; i < nsec_count; i++) for (i = 0; i < nsec_count; i++)
if ((p = nsecs[i])) if ((p = nsecs[i]))
{ {
if (!extract_name(header, plen, &p, workspace1, 1, 0) || if (!extract_name(header, plen, &p, workspace1, 1, 0))
!(base32_len = base32_decode(workspace1, (unsigned char *)workspace2))) return DNSSEC_FAIL_BADPACKET;
return 0;
if (!(base32_len = base32_decode(workspace1, (unsigned char *)workspace2)))
return DNSSEC_FAIL_NONSEC;
if (digest_len == base32_len && if (digest_len == base32_len &&
memcmp(digest, workspace2, digest_len) == 0) memcmp(digest, workspace2, digest_len) == 0)
@@ -1594,14 +1595,14 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
while ((closest_encloser = strchr(closest_encloser, '.'))); while ((closest_encloser = strchr(closest_encloser, '.')));
if (!closest_encloser || !next_closest) if (!closest_encloser || !next_closest)
return 0; return DNSSEC_FAIL_NONSEC;
/* Look for NSEC3 that proves the non-existence of the next-closest encloser */ /* Look for NSEC3 that proves the non-existence of the next-closest encloser */
if ((digest_len = hash_name(next_closest, &digest, hash, salt, salt_len, iterations)) == 0) if ((digest_len = hash_name(next_closest, &digest, hash, salt, salt_len, iterations)) == 0)
return 0; return DNSSEC_FAIL_NONSEC;
if (!check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, NULL, 1)) if (!check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, NULL, 1))
return 0; 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 (!wildname)
@@ -1613,15 +1614,16 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
*wildcard = '*'; *wildcard = '*';
if ((digest_len = hash_name(wildcard, &digest, hash, salt, salt_len, iterations)) == 0) if ((digest_len = hash_name(wildcard, &digest, hash, salt, salt_len, iterations)) == 0)
return 0; return DNSSEC_FAIL_NONSEC;
if (!check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, NULL, 1)) if (!check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, NULL, 1))
return 0; return DNSSEC_FAIL_NONSEC;
} }
return 1; return 0;
} }
/* 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) 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)
{ {
static unsigned char **nsecset = NULL, **rrsig_labels = NULL; static unsigned char **nsecset = NULL, **rrsig_labels = NULL;
@@ -1634,7 +1636,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
/* Move to NS section */ /* Move to NS section */
if (!p || !(p = skip_section(p, ntohs(header->ancount), header, plen))) if (!p || !(p = skip_section(p, ntohs(header->ancount), header, plen)))
return 0; return DNSSEC_FAIL_BADPACKET;
auth_start = p; auth_start = p;
@@ -1643,7 +1645,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
unsigned char *pstart = p; unsigned char *pstart = p;
if (!extract_name(header, plen, &p, daemon->workspacename, 1, 10)) if (!extract_name(header, plen, &p, daemon->workspacename, 1, 10))
return 0; return DNSSEC_FAIL_BADPACKET;
GETSHORT(type, p); GETSHORT(type, p);
GETSHORT(class, p); GETSHORT(class, p);
@@ -1662,12 +1664,12 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
/* No mixed NSECing 'round here, thankyouverymuch */ /* No mixed NSECing 'round here, thankyouverymuch */
if (type_found != 0 && type_found != type) if (type_found != 0 && type_found != type)
return 0; return DNSSEC_FAIL_NONSEC;
type_found = type; type_found = type;
if (!expand_workspace(&nsecset, &nsecset_sz, nsecs_found)) if (!expand_workspace(&nsecset, &nsecset_sz, nsecs_found))
return 0; return DNSSEC_FAIL_BADPACKET;
if (type == T_NSEC) if (type == T_NSEC)
{ {
@@ -1682,14 +1684,14 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
int res, j, rdlen1, type1, class1; int res, j, rdlen1, type1, class1;
if (!expand_workspace(&rrsig_labels, &rrsig_labels_sz, nsecs_found)) if (!expand_workspace(&rrsig_labels, &rrsig_labels_sz, nsecs_found))
return 0; return DNSSEC_FAIL_BADPACKET;
rrsig_labels[nsecs_found] = NULL; rrsig_labels[nsecs_found] = NULL;
for (j = ntohs(header->nscount); j != 0; j--) for (j = ntohs(header->nscount); j != 0; j--)
{ {
if (!(res = extract_name(header, plen, &p1, daemon->workspacename, 0, 10))) if (!(res = extract_name(header, plen, &p1, daemon->workspacename, 0, 10)))
return 0; return DNSSEC_FAIL_BADPACKET;
GETSHORT(type1, p1); GETSHORT(type1, p1);
GETSHORT(class1, p1); GETSHORT(class1, p1);
@@ -1697,7 +1699,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
GETSHORT(rdlen1, p1); GETSHORT(rdlen1, p1);
if (!CHECK_LEN(header, p1, plen, rdlen1)) if (!CHECK_LEN(header, p1, plen, rdlen1))
return 0; return DNSSEC_FAIL_BADPACKET;
if (res == 1 && class1 == qclass && type1 == T_RRSIG) if (res == 1 && class1 == qclass && type1 == T_RRSIG)
{ {
@@ -1705,7 +1707,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
unsigned char *psav = p1; unsigned char *psav = p1;
if (rdlen1 < 18) if (rdlen1 < 18)
return 0; /* bad packet */ return DNSSEC_FAIL_BADPACKET; /* bad packet */
GETSHORT(type_covered, p1); GETSHORT(type_covered, p1);
@@ -1717,25 +1719,25 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
if (!rrsig_labels[nsecs_found]) if (!rrsig_labels[nsecs_found])
rrsig_labels[nsecs_found] = p1; rrsig_labels[nsecs_found] = p1;
else if (*rrsig_labels[nsecs_found] != *p1) /* algo */ else if (*rrsig_labels[nsecs_found] != *p1) /* algo */
return 0; return DNSSEC_FAIL_NONSEC;
} }
p1 = psav; p1 = psav;
} }
if (!ADD_RDLEN(header, p1, plen, rdlen1)) if (!ADD_RDLEN(header, p1, plen, rdlen1))
return 0; return DNSSEC_FAIL_BADPACKET;
} }
/* Must have found at least one sig. */ /* Must have found at least one sig. */
if (!rrsig_labels[nsecs_found]) if (!rrsig_labels[nsecs_found])
return 0; return DNSSEC_FAIL_NONSEC;
} }
nsecset[nsecs_found++] = pstart; nsecset[nsecs_found++] = pstart;
} }
if (!ADD_RDLEN(header, p, plen, rdlen)) if (!ADD_RDLEN(header, p, plen, rdlen))
return 0; return DNSSEC_FAIL_BADPACKET;
} }
if (type_found == T_NSEC) if (type_found == T_NSEC)
@@ -1743,7 +1745,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
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); return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wildname, nons);
else else
return 0; return DNSSEC_FAIL_NONSEC;
} }
/* Check signing status of name. /* Check signing status of name.
@@ -1857,7 +1859,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
int type1, class1, rdlen1 = 0, type2, class2, rdlen2, qclass, qtype, targetidx; int type1, class1, rdlen1 = 0, type2, class2, rdlen2, qclass, qtype, targetidx;
int i, j, rc = STAT_INSECURE; int i, j, rc = STAT_INSECURE;
int secure = STAT_SECURE; int secure = STAT_SECURE;
int rc_nsec;
/* extend rr_status if necessary */ /* extend rr_status if necessary */
if (daemon->rr_status_sz < ntohs(header->ancount) + ntohs(header->nscount)) if (daemon->rr_status_sz < ntohs(header->ancount) + ntohs(header->nscount))
{ {
@@ -2059,8 +2061,8 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
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 (STAT_ISEQUAL(rc, STAT_SECURE_WILDCARD) &&
!prove_non_existence(header, plen, keyname, name, type1, class1, wildname, NULL, NULL)) ((rc_nsec = prove_non_existence(header, plen, keyname, name, type1, class1, wildname, NULL, NULL))) != 0)
return STAT_BOGUS | DNSSEC_FAIL_NONSEC; return STAT_BOGUS | rc_nsec;
rc = STAT_SECURE; rc = STAT_SECURE;
} }
@@ -2085,20 +2087,21 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
/* For anything other than a DS record, this situation is OK if either /* For anything other than a DS record, this situation is OK if either
the answer is in an unsigned zone, or there's a NSEC records. */ the answer is in an unsigned zone, or there's a NSEC records. */
if (!prove_non_existence(header, plen, keyname, name, qtype, qclass, NULL, nons, nsec_ttl)) if ((rc_nsec = prove_non_existence(header, plen, keyname, name, qtype, qclass, NULL, nons, nsec_ttl)) != 0)
{ {
/* Empty DS without NSECS */ /* Empty DS without NSECS */
if (qtype == T_DS) if (qtype == T_DS)
return STAT_BOGUS | DNSSEC_FAIL_NONSEC; return STAT_BOGUS | rc_nsec;
if (!STAT_ISEQUAL((rc = zone_status(name, qclass, keyname, now)), STAT_SECURE)) if ((rc_nsec & (DNSSEC_FAIL_NONSEC | DNSSEC_FAIL_NSEC3_ITERS)) &&
!STAT_ISEQUAL((rc = zone_status(name, qclass, keyname, now)), STAT_SECURE))
{ {
if (class) if (class)
*class = qclass; /* Class for NEED_DS or NEED_KEY */ *class = qclass; /* Class for NEED_DS or NEED_KEY */
return rc; return rc;
} }
return STAT_BOGUS | DNSSEC_FAIL_NONSEC; /* signed zone, no NSECs */ return STAT_BOGUS | rc_nsec; /* signed zone, no NSECs */
} }
} }
@@ -2180,6 +2183,8 @@ int errflags_to_ede(int status)
return EDE_NO_DNSKEY; return EDE_NO_DNSKEY;
else if (status & DNSSEC_FAIL_NODSSUP) else if (status & DNSSEC_FAIL_NODSSUP)
return EDE_USUPDS; return EDE_USUPDS;
else if (status & DNSSEC_FAIL_NSEC3_ITERS)
return EDE_UNS_NS3_ITER;
else if (status & DNSSEC_FAIL_NONSEC) else if (status & DNSSEC_FAIL_NONSEC)
return EDE_NO_NSEC; return EDE_NO_NSEC;
else if (status & DNSSEC_FAIL_INDET) else if (status & DNSSEC_FAIL_INDET)