From: Simon Kelley Date: Wed, 8 Jan 2014 10:26:58 +0000 (+0000) Subject: First functional DNSSEC - highly alpha. X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=0fc2f31368017b8a5d1ac80b8ae3efb4dacc24ad;p=people%2Fms%2Fdnsmasq.git First functional DNSSEC - highly alpha. --- diff --git a/src/cache.c b/src/cache.c index cfbeae3..ee27e4e 100644 --- a/src/cache.c +++ b/src/cache.c @@ -56,6 +56,8 @@ static const struct { { 38, "A6" }, { 39, "DNAME" }, { 41, "OPT" }, + { 43, "DS" }, + { 46, "RRSIG" }, { 48, "DNSKEY" }, { 249, "TKEY" }, { 250, "TSIG" }, @@ -916,12 +918,19 @@ void cache_reload(void) struct name_list *nl; struct cname *a; struct interface_name *intr; +#ifdef HAVE_DNSSEC + struct dnskey *key; +#endif cache_inserted = cache_live_freed = 0; for (i=0; iflags & (F_DNSKEY | F_DS)) + blockdata_free(cache->addr.key.keydata); +#endif tmp = cache->hash_next; if (cache->flags & (F_HOSTS | F_CONFIG)) { @@ -948,13 +957,27 @@ void cache_reload(void) if (hostname_isequal(a->target, intr->name) && ((cache = whine_malloc(sizeof(struct crec))))) { - cache->flags = F_FORWARD | F_NAMEP | F_CNAME | F_IMMORTAL | F_CONFIG; + cache->flags = F_FORWARD | F_NAMEP | F_CNAME | F_IMMORTAL | F_CONFIG | F_DNSSECOK; cache->name.namep = a->alias; cache->addr.cname.target.int_name = intr; cache->addr.cname.uid = -1; cache_hash(cache); add_hosts_cname(cache); /* handle chains */ } + +#ifdef HAVE_DNSSEC + for (key = daemon->dnskeys; key; key = key->next) + if ((cache = whine_malloc(sizeof(struct crec))) && + (cache->addr.key.keydata = blockdata_alloc(key->key, key->keylen))) + { + cache->flags = F_FORWARD | F_IMMORTAL | F_DNSKEY | F_CONFIG | F_NAMEP; + cache->name.namep = key->name; + cache->uid = key->keylen; + cache->addr.key.algo = key->algo; + cache->addr.key.keytag = dnskey_keytag(key->algo, key->flags, (unsigned char *)key->key, key->keylen); + cache_hash(cache); + } +#endif /* borrow the packet buffer for a temporary by-address hash */ memset(daemon->packet, 0, daemon->packet_buff_sz); @@ -1197,16 +1220,13 @@ void dump_cache(time_t now) for (i=0; ihash_next) { - char *a, *p = daemon->namebuff; - p += sprintf(p, "%-40.40s ", cache_get_name(cache)); - if ((cache->flags & F_NEG) && (cache->flags & F_FORWARD)) - a = ""; - else if (cache->flags & F_CNAME) - { - a = ""; - if (!is_outdated_cname_pointer(cache)) - a = cache_get_cname_target(cache); - } + char *a = daemon->addrbuff, *p = daemon->namebuff, *n = cache_get_name(cache); + *a = 0; + if (strlen(n) == 0) + n = ""; + p += sprintf(p, "%-40.40s ", n); + if ((cache->flags & F_CNAME) && !is_outdated_cname_pointer(cache)) + a = cache_get_cname_target(cache); #ifdef HAVE_DNSSEC else if (cache->flags & F_DNSKEY) { @@ -1216,11 +1236,11 @@ void dump_cache(time_t now) else if (cache->flags & F_DS) { a = daemon->addrbuff; - sprintf(a, "%5u %3u %3u %u", cache->addr.key.keytag, - cache->addr.key.algo, cache->addr.key.digest, cache->uid); + sprintf(a, "%5u %3u %3u", cache->addr.key.keytag, + cache->addr.key.algo, cache->addr.key.digest); } #endif - else + else if (!(cache->flags & F_NEG) || !(cache->flags & F_FORWARD)) { a = daemon->addrbuff; if (cache->flags & F_IPV4) @@ -1291,13 +1311,20 @@ void log_query(unsigned int flags, char *name, struct all_addr *addr, char *arg) if (addr) { + if (flags & F_KEYTAG) + sprintf(daemon->addrbuff, arg, addr->addr.keytag); + else + { #ifdef HAVE_IPV6 - inet_ntop(flags & F_IPV4 ? AF_INET : AF_INET6, - addr, daemon->addrbuff, ADDRSTRLEN); + inet_ntop(flags & F_IPV4 ? AF_INET : AF_INET6, + addr, daemon->addrbuff, ADDRSTRLEN); #else - strncpy(daemon->addrbuff, inet_ntoa(addr->addr.addr4), ADDRSTRLEN); + strncpy(daemon->addrbuff, inet_ntoa(addr->addr.addr4), ADDRSTRLEN); #endif + } } + else + dest = arg; if (flags & F_REVERSE) { @@ -1339,6 +1366,8 @@ void log_query(unsigned int flags, char *name, struct all_addr *addr, char *arg) source = arg; else if (flags & F_UPSTREAM) source = "reply"; + else if (flags & F_SECSTAT) + source = "validation"; else if (flags & F_AUTH) source = "auth"; else if (flags & F_SERVER) @@ -1351,6 +1380,11 @@ void log_query(unsigned int flags, char *name, struct all_addr *addr, char *arg) source = arg; verb = "from"; } + else if (flags & F_DNSSEC) + { + source = arg; + verb = "to"; + } else source = "cached"; @@ -1422,6 +1456,21 @@ void blockdata_free(struct blockdata *blocks) keyblock_free = blocks; } } + +void blockdata_retrieve(struct blockdata *block, size_t len, void *data) +{ + size_t blen; + struct blockdata *b; + + for (b = block; len > 0 && b; b = b->next) + { + blen = len > KEYBLOCK_LEN ? KEYBLOCK_LEN : len; + memcpy(data, b->key, blen); + data += blen; + len -= blen; + } +} + #endif diff --git a/src/config.h b/src/config.h index 079c3d9..969c2b5 100644 --- a/src/config.h +++ b/src/config.h @@ -139,8 +139,8 @@ RESOLVFILE /* #define HAVE_DBUS */ /* #define HAVE_IDN */ /* #define HAVE_CONNTRACK */ -#define HAVE_DNSSEC -#define HAVE_OPENSSL +#define HAVE_DNSSEC +#define HAVE_OPENSSL /* Default locations for important system files. */ @@ -385,7 +385,12 @@ static char *compile_opts = #ifndef HAVE_AUTH "no-" #endif - "auth"; +"auth " +#ifndef HAVE_DNSSEC +"no-" +#endif +"DNSSEC"; + #endif diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 8a3541a..044c865 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -242,6 +242,7 @@ struct all_addr { #ifdef HAVE_IPV6 struct in6_addr addr6; #endif + unsigned int keytag; } addr; }; @@ -286,6 +287,12 @@ struct cname { struct cname *next; }; +struct dnskey { + char *name, *key; + int keylen, algo, flags; + struct dnskey *next; +}; + #define ADDRLIST_LITERAL 1 #define ADDRLIST_IPV6 2 @@ -360,7 +367,7 @@ struct crec { } key; } addr; time_t ttd; /* time to die */ - /* used as keylen if F_DS or F_DNSKEY, index to source for F_HOSTS */ + /* used as keylen ifF_DNSKEY, index to source for F_HOSTS */ int uid; unsigned short flags; union { @@ -395,6 +402,9 @@ struct crec { #define F_QUERY (1u<<19) #define F_NOERR (1u<<20) #define F_AUTH (1u<<21) +#define F_DNSSEC (1u<<22) +#define F_KEYTAG (1u<<23) +#define F_SECSTAT (1u<<24) /* composites */ #define F_TYPE (F_IPV4 | F_IPV6 | F_DNSKEY | F_DS) /* Only one may be set */ @@ -896,6 +906,9 @@ extern struct daemon { #ifdef OPTION6_PREFIX_CLASS struct prefix_class *prefix_classes; #endif +#ifdef HAVE_DNSSEC + struct dnskey *dnskeys; +#endif /* globally used stuff for DNS */ char *packet; /* packet buffer */ @@ -977,6 +990,7 @@ struct crec *cache_enumerate(int init); #ifdef HAVE_DNSSEC struct blockdata *blockdata_alloc(char *data, size_t len); size_t blockdata_walk(struct blockdata **key, unsigned char **p, size_t cnt); +void blockdata_retrieve(struct blockdata *block, size_t len, void *data); void blockdata_free(struct blockdata *blocks); #endif @@ -1000,7 +1014,7 @@ size_t setup_reply(struct dns_header *header, size_t qlen, unsigned long local_ttl); int extract_addresses(struct dns_header *header, size_t qlen, char *namebuff, time_t now, char **ipsets, int is_sign, int checkrebind, - int checking_disabled); + int no_cache, int secure); size_t answer_request(struct dns_header *header, char *limit, size_t qlen, struct in_addr local_addr, struct in_addr local_netmask, time_t now); int check_for_bogus_wildcard(struct dns_header *header, size_t qlen, char *name, @@ -1034,11 +1048,13 @@ int in_zone(struct auth_zone *zone, char *name, char **cut); #endif /* dnssec.c */ -size_t dnssec_generate_query(struct dns_header *header, char *name, int class, int type); +size_t dnssec_generate_query(struct dns_header *header, char *name, int class, int type, union mysockaddr *addr); int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t n, char *name, char *keyname, int class); int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class); -int validate_rrset(time_t now, struct dns_header *header, size_t plen, int class, int type, char *name, char *keyname); -int dnssec_validate_reply(struct dns_header *header, size_t plen, char *name, char *keyname, int *class); +int validate_rrset(time_t now, struct dns_header *header, size_t plen, int class, + int type, char *name, char *keyname, struct blockdata *key, int keylen, int algo, int keytag); +int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class); +int dnskey_keytag(int alg, int flags, unsigned char *rdata, int rdlen); /* util.c */ void rand_init(void); @@ -1065,6 +1081,9 @@ void prettyprint_time(char *buf, unsigned int t); int prettyprint_addr(union mysockaddr *addr, char *buf); int parse_hex(char *in, unsigned char *out, int maxlen, unsigned int *wildcard_mask, int *mac_type); +#ifdef HAVE_DNSSEC +int parse_base64(char *in, char *out); +#endif int memcmp_masked(unsigned char *a, unsigned char *b, int len, unsigned int mask); int expand_buf(struct iovec *iov, size_t size); diff --git a/src/dnssec-crypto.h b/src/dnssec-crypto.h index 1717db6..77c5bc5 100644 --- a/src/dnssec-crypto.h +++ b/src/dnssec-crypto.h @@ -17,7 +17,7 @@ #ifndef DNSSEC_CRYPTO_H #define DNSSEC_CRYPTO_H -struct keydata; +struct blockdata; /* * vtable for a signature verification algorithm. @@ -49,7 +49,7 @@ typedef struct VerifyAlgCtx VerifyAlgCtx; typedef struct { int digest_algo; - int (*verify)(VerifyAlgCtx *ctx, struct keydata *key, unsigned key_len); + int (*verify)(VerifyAlgCtx *ctx, struct blockdata *key, unsigned key_len); } VerifyAlg; struct VerifyAlgCtx @@ -74,9 +74,9 @@ int verifyalg_algonum(VerifyAlgCtx *a); #define DIGESTALG_SHA512 257 int digestalg_supported(int algo); -int digestalg_begin(int algo); +void digestalg_begin(int algo); void digestalg_add_data(void *data, unsigned len); -void digestalg_add_keydata(struct keydata *key, size_t len); +void digestalg_add_keydata(struct blockdata *key, size_t len); unsigned char *digestalg_final(void); int digestalg_len(void); diff --git a/src/dnssec-openssl.c b/src/dnssec-openssl.c index 4bf7e73..2e25f82 100644 --- a/src/dnssec-openssl.c +++ b/src/dnssec-openssl.c @@ -14,13 +14,16 @@ along with this program. If not, see . */ -#include #include "dnsmasq.h" + +#ifdef HAVE_DNSSEC + #include "dnssec-crypto.h" #include #include #include #include +#include #define POOL_SIZE 1 static union _Pool @@ -39,20 +42,20 @@ static void print_hex(unsigned char *data, unsigned len) printf("\n"); } -static int keydata_to_bn(BIGNUM *ret, struct keydata **key_data, unsigned char **p, unsigned len) +static int keydata_to_bn(BIGNUM *ret, struct blockdata **key_data, unsigned char **p, unsigned len) { size_t cnt; BIGNUM temp; BN_init(ret); - cnt = keydata_walk(key_data, p, len); + cnt = blockdata_walk(key_data, p, len); BN_bin2bn(*p, cnt, ret); len -= cnt; *p += cnt; while (len > 0) { - if (!(cnt = keydata_walk(key_data, p, len))) + if (!(cnt = blockdata_walk(key_data, p, len))) return 0; BN_lshift(ret, ret, cnt*8); BN_init(&temp); @@ -64,7 +67,7 @@ static int keydata_to_bn(BIGNUM *ret, struct keydata **key_data, unsigned char * return 1; } -static int rsasha1_parse_key(BIGNUM *exp, BIGNUM *mod, struct keydata *key_data, unsigned key_len) +static int rsasha1_parse_key(BIGNUM *exp, BIGNUM *mod, struct blockdata *key_data, unsigned key_len) { unsigned char *p = key_data->key; size_t exp_len, mod_len; @@ -80,7 +83,7 @@ static int rsasha1_parse_key(BIGNUM *exp, BIGNUM *mod, struct keydata *key_data, keydata_to_bn(mod, &key_data, &p, mod_len); } -static int dsasha1_parse_key(BIGNUM *Q, BIGNUM *P, BIGNUM *G, BIGNUM *Y, struct keydata *key_data, unsigned key_len) +static int dsasha1_parse_key(BIGNUM *Q, BIGNUM *P, BIGNUM *G, BIGNUM *Y, struct blockdata *key_data, unsigned key_len) { unsigned char *p = key_data->key; int T; @@ -93,7 +96,7 @@ static int dsasha1_parse_key(BIGNUM *Q, BIGNUM *P, BIGNUM *G, BIGNUM *Y, struct keydata_to_bn(Y, &key_data, &p, 64+T*8); } -static int rsa_verify(VerifyAlgCtx *ctx, struct keydata *key_data, unsigned key_len, int nid, int dlen) +static int rsa_verify(VerifyAlgCtx *ctx, struct blockdata *key_data, unsigned key_len, int nid, int dlen) { int validated = 0; @@ -108,27 +111,27 @@ static int rsa_verify(VerifyAlgCtx *ctx, struct keydata *key_data, unsigned key_ return validated; } -static int rsamd5_verify(VerifyAlgCtx *ctx, struct keydata *key_data, unsigned key_len) +static int rsamd5_verify(VerifyAlgCtx *ctx, struct blockdata *key_data, unsigned key_len) { return rsa_verify(ctx, key_data, key_len, NID_md5, 16); } -static int rsasha1_verify(VerifyAlgCtx *ctx, struct keydata *key_data, unsigned key_len) +static int rsasha1_verify(VerifyAlgCtx *ctx, struct blockdata *key_data, unsigned key_len) { return rsa_verify(ctx, key_data, key_len, NID_sha1, 20); } -static int rsasha256_verify(VerifyAlgCtx *ctx, struct keydata *key_data, unsigned key_len) +static int rsasha256_verify(VerifyAlgCtx *ctx, struct blockdata *key_data, unsigned key_len) { return rsa_verify(ctx, key_data, key_len, NID_sha256, 32); } -static int rsasha512_verify(VerifyAlgCtx *ctx, struct keydata *key_data, unsigned key_len) +static int rsasha512_verify(VerifyAlgCtx *ctx, struct blockdata *key_data, unsigned key_len) { return rsa_verify(ctx, key_data, key_len, NID_sha512, 64); } -static int dsasha1_verify(VerifyAlgCtx *ctx, struct keydata *key_data, unsigned key_len) +static int dsasha1_verify(VerifyAlgCtx *ctx, struct blockdata *key_data, unsigned key_len) { static unsigned char asn1_signature[] = { @@ -222,9 +225,6 @@ VerifyAlgCtx* verifyalg_alloc(int algo) int i; VerifyAlgCtx *ret = 0; - if (!verifyalg_supported(algo)) - return 0; - if (pool_used == (1< + and Copyright (c) 2012-2014 Simon Kelley This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -15,11 +16,14 @@ */ #include "dnsmasq.h" + +#ifdef HAVE_DNSSEC + #include "dnssec-crypto.h" #include /* Maximum length in octects of a domain name, in wire format */ -#define MAXCDNAME 256 +#define MAXCDNAME 256 #define MAXRRSET 16 @@ -28,8 +32,6 @@ #define SERIAL_LT -1 #define SERIAL_GT 1 -static int dnskey_keytag(int alg, unsigned char *rdata, int rdlen); - /* Implement RFC1982 wrapped compare for 32-bit numbers */ static int serial_compare_32(unsigned long s1, unsigned long s2) { @@ -45,36 +47,6 @@ static int serial_compare_32(unsigned long s1, unsigned long s2) return SERIAL_UNDEF; } -/* Extract a DNS name from wire format, without handling compression. This is - faster than extract_name() and does not require access to the full dns - packet. */ -static int extract_name_no_compression(unsigned char *rr, int maxlen, char *buf) -{ - unsigned char *start=rr, *end = rr+maxlen; - int count; - - while (rr < end && *rr != 0) - { - count = *rr++; - while (count-- > 0 && rr < end) - { - *buf = *rr++; - if (!isascii(*buf) || iscntrl(*buf) || *buf == '.') - return 0; - if (*buf >= 'A' && *buf <= 'Z') - *buf += 'a' - 'A'; - buf++; - } - *buf++ = '.'; - } - /* Remove trailing dot (if any) */ - if (rr != start) - *(--buf) = 0; - if (rr == end) - return 0; - /* Trailing \0 in source data must be consumed */ - return rr-start+1; -} /* process_domain_name() - do operations with domain names in canonicalized wire format. * @@ -419,46 +391,55 @@ typedef struct PendingRRSIGValidation int keytag; } PendingRRSIGValidation; -/* strchrnul - like strchr, but when character is not found, returns a pointer to the terminating \0. - This is an existing C GNU extension, but it's easier to reimplement it, - rather than tweaking with configure. */ -static char *my_strchrnul(char *str, char ch) +/* Convert from presentation format to wire format, in place. + Also map UC -> LC. + Note that using extract_name to get presentation format + then calling to_wire() removes compression and maps case, + thus generating names in canonical form. + Calling to_wire followed by from_wire is almost an identity, + except that the UC remains mapped to LC. +*/ +static int to_wire(char *name) { - while (*str && *str != ch) - str++; - return str; + unsigned char *l, *p, term; + int len; + + for (l = (unsigned char*)name; *l != 0; l = p) + { + for (p = l; *p != '.' && *p != 0; p++) + if (*p >= 'A' && *p <= 'Z') + *p = *p - 'A' + 'a'; + + term = *p; + + if ((len = p - l) != 0) + memmove(l+1, l, len); + *l = len; + + p++; + + if (term == 0) + *p = 0; + } + + return l + 1 - (unsigned char *)name; } -/* Convert a domain name to wire format */ -static int convert_domain_to_wire(char *name, unsigned char* out) +/* Note: no compression allowed in input. */ +static void from_wire(char *name) { - unsigned char len; - unsigned char *start = out; - char *p; + unsigned char *l; + int len; - do + for (l = (unsigned char *)name; *l != 0; l += len+1) { - p = my_strchrnul(name, '.'); - if ((len = p-name)) - { - *out++ = len; - while (len--) - { - char ch = *name++; - /* TODO: this will not be required anymore once we - remove all usages of extract_name() from DNSSEC code */ - if (ch >= 'A' && ch <= 'Z') - ch = ch - 'A' + 'a'; - *out++ = ch; - } - } - name = p+1; + len = *l; + memmove(l, l+1, len); + l[len] = '.'; } - while (*p); - *out++ = '\0'; - return out-start; + *(l-1) = 0; } @@ -498,43 +479,56 @@ static int digestalg_add_rdata(int sigtype, struct dns_header *header, size_t pk return 1; } -size_t dnssec_generate_query(struct dns_header *header, char *name, int class, int type) +size_t dnssec_generate_query(struct dns_header *header, char *name, int class, int type, union mysockaddr *addr) { unsigned char *p; + char types[20]; + + querystr("dnssec", types, type); + if (addr->sa.sa_family == AF_INET) + log_query(F_DNSSEC | F_IPV4, name, (struct all_addr *)&addr->in.sin_addr, types); +#ifdef HAVE_IPV6 + else + log_query(F_DNSSEC | F_IPV6, name, (struct all_addr *)&addr->in6.sin6_addr, types); +#endif + header->qdcount = htons(1); header->ancount = htons(0); header->nscount = htons(0); header->arcount = htons(0); - header->hb3 = HB3_RD; + header->hb3 = HB3_RD; SET_OPCODE(header, QUERY); - header->hb4 = 0; + header->hb4 = HB4_CD; /* ID filled in later */ p = (unsigned char *)(header+1); p = do_rfc1035_name(p, name); + *p++ = 0; PUTSHORT(type, p); PUTSHORT(class, p); return add_do_bit(header, p - (unsigned char *)header, ((char *) header) + PACKETSZ); } -/* The DNS packet is expected to contain the answer to a DNSKEY query +/* The DNS packet is expected to contain the answer to a DNSKEY query. + Leave name of qury in name. Put all DNSKEYs in the answer which are valid into the cache. return codes: STAT_INSECURE bad packet, no DNSKEYs in reply. STAT_SECURE At least one valid DNSKEY found and in cache. - STAT_BOGUS At least one DNSKEY found, which fails validation. - STAT_NEED_DS DS records to validate a key not found, name in namebuff + STAT_BOGUS No DNSKEYs found, which can be validated with DS, + or self-sign for DNSKEY RRset is not valid. + STAT_NEED_DS DS records to validate a key not found, name in keyname */ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class) { - unsigned char *p; + unsigned char *psave, *p = (unsigned char *)(header+1); struct crec *crecp, *recp1; - int j, qtype, qclass, ttl, rdlen, flags, protocol, algo, gotone; + int rc, j, qtype, qclass, ttl, rdlen, flags, algo, valid, keytag; struct blockdata *key; if (ntohs(header->qdcount) != 1) @@ -546,15 +540,23 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch GETSHORT(qtype, p); GETSHORT(qclass, p); - if (qtype != T_DNSKEY || qclass != class) + if (qtype != T_DNSKEY || qclass != class || ntohs(header->ancount) == 0) return STAT_INSECURE; + /* See if we have cached a DS record which validates this key */ + if (!(crecp = cache_find_by_name(NULL, name, now, F_DS))) + { + strcpy(keyname, name); + return STAT_NEED_DS; + } + cache_start_insert(); - for (gotone = 0, j = ntohs(header->ancount); j != 0; j--) + /* NOTE, we need to find ONE DNSKEY which matches the DS */ + for (valid = 0, j = ntohs(header->ancount); j != 0; j--) { /* Ensure we have type, class TTL and length */ - if (!extract_name(header, plen, &p, name, 1, 10)) + if (!(rc = extract_name(header, plen, &p, name, 0, 10))) return STAT_INSECURE; /* bad packet */ GETSHORT(qtype, p); @@ -562,64 +564,89 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch GETLONG(ttl, p); GETSHORT(rdlen, p); - if (qclass != class || qtype != T_DNSKEY || rdlen < 4) + if (qclass != class || qtype != T_DNSKEY || rc == 2) { - /* skip all records other than DNSKEY */ - p += rdlen; - continue; + if (ADD_RDLEN(header, p, plen, rdlen)) + continue; + + return STAT_INSECURE; /* bad packet */ } - crecp = cache_find_by_name(NULL, name, now, F_DS); + if (!CHECK_LEN(header, p, plen, rdlen) || rdlen < 4) + return STAT_INSECURE; /* bad packet */ + + psave = p; /* length at least covers flags, protocol and algo now. */ GETSHORT(flags, p); - protocol = *p++; + if (*p++ != 3) + return STAT_INSECURE; algo = *p++; - - /* See if we have cached a DS record which validates this key */ - for (recp1 = crecp; recp1; recp1 = cache_find_by_name(recp1, name, now, F_DS)) - if (recp1->addr.key.algo == algo && is_supported_digest(recp1->addr.key.digest)) - break; + keytag = dnskey_keytag(algo, flags, p, rdlen - 4); - /* DS record needed to validate key is missing, return name of DS in namebuff */ - if (!recp1) - return STAT_NEED_DS; - else + /* Put the key into the cache. Note that if the validation fails, we won't + call cache_end_insert() and this will never be committed. */ + if ((key = blockdata_alloc((char*)p, rdlen - 4)) && + (recp1 = cache_insert(name, NULL, now, ttl, F_FORWARD | F_DNSKEY))) { - int valid = 1; - /* calculate digest of canonicalised DNSKEY data using digest in (recp1->addr.key.digest) - and see if it equals digest stored in recp1 - */ - - if (!valid) - return STAT_BOGUS; + recp1->uid = rdlen - 4; + recp1->addr.key.keydata = key; + recp1->addr.key.algo = algo; + recp1->addr.key.keytag = keytag; } - if ((key = blockdata_alloc((char*)p, rdlen))) - { - - /* We've proved that the KEY is OK, store it in the cache */ - if ((crecp = cache_insert(name, NULL, now, ttl, F_FORWARD | F_DNSKEY))) - { - crecp->uid = rdlen; - crecp->addr.key.keydata = key; - crecp->addr.key.algo = algo; - crecp->addr.key.keytag = dnskey_keytag(algo, (char*)p, rdlen); - gotone = 1; - } - } + p = psave; + if (!ADD_RDLEN(header, p, plen, rdlen)) + return STAT_INSECURE; /* bad packet */ + /* Already determined that message is OK. Just loop stuffing cache */ + if (valid || !key) + continue; + + for (recp1 = crecp; recp1; recp1 = cache_find_by_name(recp1, name, now, F_DS)) + if (recp1->addr.key.algo == algo && + recp1->addr.key.keytag == keytag && + (flags & 0x100) && /* zone key flag */ + digestalg_supported(recp1->addr.key.digest)) + { + int wire_len = to_wire(name); + + digestalg_begin(recp1->addr.key.digest); + digestalg_add_data(name, wire_len); + digestalg_add_data((char *)psave, rdlen); + + from_wire(name); + + /* TODO fragented digest */ + if (memcmp(digestalg_final(), recp1->addr.key.keydata->key, digestalg_len()) == 0 && + validate_rrset(now, header, plen, class, T_DNSKEY, name, keyname, key, rdlen - 4, algo, keytag)) + { + struct all_addr a; + valid = 1; + a.addr.keytag = keytag; + log_query(F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %u"); + break; + } + } } - - cache_end_insert(); - - return gotone ? STAT_SECURE : STAT_INSECURE; + if (valid) + { + /* commit cache insert. */ + cache_end_insert(); + return STAT_SECURE; + } + + log_query(F_UPSTREAM, name, NULL, "BOGUS DNSKEY"); + return STAT_BOGUS; } + + /* The DNS packet is expected to contain the answer to a DS query + Leave name of DS query in name. Put all DSs in the answer which are valid into the cache. return codes: - STAT_INSECURE bad packet, no DNSKEYs in reply. + STAT_INSECURE bad packet, no DS in reply. STAT_SECURE At least one valid DS found and in cache. STAT_BOGUS At least one DS found, which fails validation. STAT_NEED_DNSKEY DNSKEY records to validate a DS not found, name in keyname @@ -627,8 +654,8 @@ 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, char *keyname, int class) { - unsigned char *p = (unsigned char *)(header+1); - struct crec *crecp, *recp1; + unsigned char *psave, *p = (unsigned char *)(header+1); + struct crec *crecp; int qtype, qclass, val, j, gotone; struct blockdata *key; @@ -641,10 +668,13 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char GETSHORT(qtype, p); GETSHORT(qclass, p); - if (qtype != T_DS || qclass != class) + if (qtype != T_DS || qclass != class || ntohs(header->ancount) == 0) return STAT_INSECURE; - - val = validate_rrset(header, plen, class, T_DS, name, keyname); + + val = validate_rrset(now, header, plen, class, T_DS, name, keyname, NULL, 0, 0, 0); + + if (val == STAT_BOGUS) + log_query(F_UPSTREAM, name, NULL, "BOGUS DS"); /* failed to validate or missing key. */ if (val != STAT_SECURE) @@ -654,7 +684,7 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char for (gotone = 0, j = ntohs(header->ancount); j != 0; j--) { - int ttl, rdlen, rc, algo; + int ttl, rdlen, rc, algo, digest, keytag; /* Ensure we have type, class TTL and length */ if (!(rc = extract_name(header, plen, &p, name, 0, 10))) @@ -666,32 +696,42 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char GETSHORT(rdlen, p); /* check type, class and name, skip if not in DS rrset */ - if (qclass != class || qtype != T_DS || rc == 2) - { - p += rdlen; - continue; - } - - if ((key = blockdata_alloc((char*)p, rdlen))) + if (qclass == class && qtype == T_DS && rc == 1) { + if (!CHECK_LEN(header, p, plen, rdlen) || rdlen < 4) + return STAT_INSECURE; /* bad packet */ + + psave = p; + GETSHORT(keytag, p); + algo = *p++; + digest = *p++; /* We've proved that the DS is OK, store it in the cache */ - if ((crecp = cache_insert(name, NULL, now, ttl, F_FORWARD | F_DS))) + if ((key = blockdata_alloc((char*)p, rdlen - 4)) && + (crecp = cache_insert(name, NULL, now, ttl, F_FORWARD | F_DS))) { - crecp->uid = rdlen; + struct all_addr a; + a.addr.keytag = keytag; + log_query(F_KEYTAG | F_UPSTREAM, name, &a, "DS keytag %u"); + crecp->addr.key.digest = digest; crecp->addr.key.keydata = key; crecp->addr.key.algo = algo; - crecp->addr.key.keytag = dnskey_keytag(algo, (char*)p, rdlen); - gotone = 1; + crecp->addr.key.keytag = keytag; } + else + return STAT_INSECURE; /* cache problem */ + + p = psave; } - + + if (!ADD_RDLEN(header, p, plen, rdlen)) + return STAT_INSECURE; /* bad packet */ + } cache_end_insert(); - - return gotone ? STAT_SECURE : STAT_INSECURE; + return STAT_SECURE; } @@ -702,528 +742,249 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char STAT_INSECURE can't validate (no RRSIG, bad packet). STAT_BOGUS signature is wrong. STAT_NEED_KEY need DNSKEY to complete validation (name is returned in keyname) + + if key is non-NULL, use that key, which has the algo and tag given in the params of those names, + otherwise find the key in the cache. */ -int validate_rrset(time_t now, struct dns_header *header, size_t plen, int class, int type, char *name, char *keyname) +int validate_rrset(time_t now, struct dns_header *header, size_t plen, int class, + int type, char *name, char *keyname, struct blockdata *key, int keylen, int algo_in, int keytag_in) { - unsigned char *p, *psav, *sig; - int rrsetidx, res, sigttl, sig_data_len, j; - struct crec *crecp; - void *rrset[MAXRRSET]; /* TODO: max RRset size? */ + unsigned char *p; + int rrsetidx, sigidx, res, rdlen, j; + struct crec *crecp = NULL; + void *rrset[MAXRRSET], *sigs[MAXRRSET]; /* TODO: max RRset size? */ int type_covered, algo, labels, orig_ttl, sig_expiration, sig_inception, key_tag; if (!(p = skip_questions(header, plen))) return STAT_INSECURE; /* look for an RRSIG record for this RRset and get pointers to each record */ - for (rrsetidx = 0, sig = NULL, j = ntohs(header->ancount) + ntohs(header->nscount); + for (rrsetidx = 0, sigidx = 0, j = ntohs(header->ancount) + ntohs(header->nscount); j != 0; j--) { - unsigned char *pstart = p; - int stype, sclass, sttl, rdlen; + unsigned char *pstart; + int stype, sclass, sttl; if (!(res = extract_name(header, plen, &p, name, 0, 10))) return STAT_INSECURE; /* bad packet */ + pstart = p; + GETSHORT(stype, p); GETSHORT(sclass, p); GETLONG(sttl, p); GETSHORT(rdlen, p); + + (void)sttl; if (!CHECK_LEN(header, p, plen, rdlen)) return STAT_INSECURE; /* bad packet */ - if (res == 2 || htons(stype) != T_RRSIG || htons(sclass) != class) - continue; - - if (htons(stype) == type) - { - rrset[rrsetidx++] = pstart; - if (rrsetidx == MAXRRSET) - return STAT_INSECURE; /* RRSET too big TODO */ - } - - if (htons(stype) == T_RRSIG) + if (res == 1 && sclass == class) { - /* name matches, RRSIG for correct class */ - /* enough data? */ - if (rdlen < 18) - return STAT_INSECURE; - - GETSHORT(type_covered, p); - algo = *p++; - labels = *p++; - GETLONG(orig_ttl, p); - GETLONG(sig_expiration, p); - GETLONG(sig_inception, p); - GETSHORT(key_tag, p); - - if (type_covered != type || - !check_date_range(sig_inception, sig_expiration)) + if (stype == type) { - /* covers wrong type or out of date - skip */ - p = psav; - if (!ADD_RDLEN(header, p, plen, rdlen)) - return STAT_INSECURE; - continue; + rrset[rrsetidx++] = pstart; + if (rrsetidx == MAXRRSET) + return STAT_INSECURE; /* RRSET too big TODO */ } - if (!extract_name(header, plen, &p, keyname, 1, 0)) - return STAT_INSECURE; - - /* OK, we have the signature record, see if the - relevant DNSKEY is in the cache. */ - for (crecp = cache_find_by_name(NULL, keyname, now, F_DNSKEY); - crecp; - crecp = cache_find_by_name(crecp, keyname, now, F_DNSKEY)) - if (crecp->addr.key.algo == algo && crecp->addr.key.keytag == key_tag) - break; - - /* No, abort for now whilst we get it */ - if (!crecp) - return STAT_NEED_KEY; - - /* Save point to signature data */ - sig = p; - sig_data_len = rdlen - (p - psav); - sigttl = sttl; - - /* next record */ - p = psav; - if (!ADD_RDLEN(header, p, plen, rdlen)) - return STAT_INSECURE; - } + if (stype == T_RRSIG) + { + sigs[sigidx++] = pstart; + if (sigidx == MAXRRSET) + return STAT_INSECURE; /* RRSET too big TODO */ + } + } + + if (!ADD_RDLEN(header, p, plen, rdlen)) + return STAT_INSECURE; } - /* Didn't find RRSIG or RRset is empty */ - if (!sig || rrsetidx == 0) + /* RRset empty, no RRSIGs */ + if (rrsetidx == 0 || sigidx == 0) return STAT_INSECURE; - - /* OK, we have an RRSIG and an RRset and we have a the DNSKEY that validates them. */ - - /* Sort RRset records in canonical order. */ - rrset_canonical_order_ctx.header = header; - rrset_canonical_order_ctx.pktlen = plen; - qsort(rrset, rrsetidx, sizeof(void*), rrset_canonical_order); - - /* Now initialize the signature verification algorithm and process the whole - RRset */ - VerifyAlgCtx *alg = verifyalg_alloc(algo); - if (!alg) - return STAT_INSECURE; - - alg->sig = sig; - alg->siglen = sig_data_len; - - u16 ntype = htons(type); - u16 nclass = htons(class); - u32 nsigttl = htonl(sigttl); - - /* TODO: we shouldn't need to convert this to wire here. Best solution would be: - - Use process_name() instead of extract_name() everywhere in dnssec code - - Convert from wire format to representation format only for querying/storing cache - */ - unsigned char owner_wire[MAXCDNAME]; - int owner_wire_len = convert_domain_to_wire(name, owner_wire); - - digestalg_begin(alg->vtbl->digest_algo); - digestalg_add_data(sigrdata, 18+signer_name_rdlen); - for (i = 0; i < rrsetidx; ++i) + + /* Now try all the sigs to try and find one which validates */ + for (j = 0; j digest, digestalg_final(), digest_len); - - if (alg->vtbl->verify(alg, crecp->addr.key.keydata, crecp_uid)) - return STAT_SECURE; - - return STAT_INSECURE; -} - - -#if 0 -static int begin_rrsig_validation(struct dns_header *header, size_t pktlen, - unsigned char *reply, int count, char *owner, - int sigclass, int sigrdlen, unsigned char *sig, - PendingRRSIGValidation *out) -{ - int i, res; - int sigtype, sigalg, siglbl; - unsigned char *sigrdata = sig; - unsigned long sigttl, date_end, date_start; - unsigned char* p = reply; - char* signer_name = daemon->namebuff; - int signer_name_rdlen; - int keytag; - void *rrset[16]; /* TODO: max RRset size? */ - int rrsetidx = 0; - - if (sigrdlen < 18) - return 0; - GETSHORT(sigtype, sig); - sigalg = *sig++; - siglbl = *sig++; - GETLONG(sigttl, sig); - GETLONG(date_end, sig); - GETLONG(date_start, sig); - GETSHORT(keytag, sig); - sigrdlen -= 18; - - if (!verifyalg_supported(sigalg)) - { - printf("ERROR: RRSIG algorithm not supported: %d\n", sigalg); - return 0; - } - - if (!check_date_range(date_start, date_end)) - { - printf("ERROR: RRSIG outside date range\n"); - return 0; - } - - /* Iterate within the answer and find the RRsets matching the current RRsig */ - for (i = 0; i < count; ++i) - { - int qtype, qclass, rdlen; - if (!(res = extract_name(header, pktlen, &p, owner, 0, 10))) - return 0; - rrset[rrsetidx] = p; - GETSHORT(qtype, p); - GETSHORT(qclass, p); - p += 4; /* skip ttl */ GETSHORT(rdlen, p); - if (res == 1 && qtype == sigtype && qclass == sigclass) - { - ++rrsetidx; - if (rrsetidx == countof(rrset)) - { - /* Internal buffer too small */ - printf("internal buffer too small for this RRset\n"); - return 0; - } - } - p += rdlen; - } - - /* Sort RRset records in canonical order. */ - rrset_canonical_order_ctx.header = header; - rrset_canonical_order_ctx.pktlen = pktlen; - qsort(rrset, rrsetidx, sizeof(void*), rrset_canonical_order); - - /* Skip through the signer name; we don't extract it right now because - we don't want to overwrite the single daemon->namebuff which contains - the owner name. We'll get to this later. */ - if (!(p = skip_name(sig, header, pktlen, 0))) - return 0; - signer_name_rdlen = p - sig; - sig = p; sigrdlen -= signer_name_rdlen; - - /* Now initialize the signature verification algorithm and process the whole - RRset */ - VerifyAlgCtx *alg = verifyalg_alloc(sigalg); - if (!alg) - return 0; - alg->sig = sig; - alg->siglen = sigrdlen; - - sigtype = htons(sigtype); - sigclass = htons(sigclass); - sigttl = htonl(sigttl); - - /* TODO: we shouldn't need to convert this to wire here. Best solution would be: - - Use process_name() instead of extract_name() everywhere in dnssec code - - Convert from wire format to representation format only for querying/storing cache - */ - unsigned char owner_wire[MAXCDNAME]; - int owner_wire_len = convert_domain_to_wire(owner, owner_wire); - - digestalg_begin(alg->vtbl->digest_algo); - digestalg_add_data(sigrdata, 18+signer_name_rdlen); - for (i = 0; i < rrsetidx; ++i) - { - p = (unsigned char*)(rrset[i]); - - digestalg_add_data(owner_wire, owner_wire_len); - digestalg_add_data(&sigtype, 2); - digestalg_add_data(&sigclass, 2); - digestalg_add_data(&sigttl, 4); + + if (rdlen < 18) + return STAT_INSECURE; /* bad packet */ + + psav = p; + + GETSHORT(type_covered, p); + algo = *p++; + labels = *p++; + GETLONG(orig_ttl, p); + GETLONG(sig_expiration, p); + GETLONG(sig_inception, p); + GETSHORT(key_tag, p); + + if (type_covered != type || + !check_date_range(sig_inception, sig_expiration) || + !verifyalg_supported(algo)) + { + /* covers wrong type or out of date - skip */ + p = psav; + if (!ADD_RDLEN(header, p, plen, rdlen)) + return STAT_INSECURE; + continue; + } + + if (!extract_name(header, plen, &p, keyname, 1, 0)) + return STAT_INSECURE; + + /* OK, we have the signature record, see if the relevant DNSKEY is in the cache. */ + if (!key && !(crecp = cache_find_by_name(NULL, keyname, now, F_DNSKEY))) + return STAT_NEED_KEY; + + /* Sort RRset records in canonical order. */ + rrset_canonical_order_ctx.header = header; + rrset_canonical_order_ctx.pktlen = plen; + qsort(rrset, rrsetidx, sizeof(void*), rrset_canonical_order); + + alg = verifyalg_alloc(algo); + alg->sig = p; + alg->siglen = rdlen - (p - psav); + + ntype = htons(type); + nclass = htons(class); + nsigttl = htonl(orig_ttl); + + digestalg_begin(alg->vtbl->digest_algo); + digestalg_add_data(psav, 18); + wire_len = to_wire(keyname); + digestalg_add_data(keyname, wire_len); + from_wire(keyname); + + /* TODO wildcard rules : 4035 5.3.2 */ + for (i = 0; i < rrsetidx; ++i) + { + p = (unsigned char*)(rrset[i]); + + wire_len = to_wire(name); + digestalg_add_data(name, wire_len); + from_wire(name); + digestalg_add_data(&ntype, 2); + digestalg_add_data(&nclass, 2); + digestalg_add_data(&nsigttl, 4); + + p += 8; + if (!digestalg_add_rdata(type, header, plen, p)) + return STAT_INSECURE; + } - p += 8; - if (!digestalg_add_rdata(ntohs(sigtype), header, pktlen, p)) - return 0; - } - int digest_len = digestalg_len(); - memcpy(alg->digest, digestalg_final(), digest_len); - - /* We don't need the owner name anymore; now extract the signer name */ - if (!extract_name_no_compression(sigrdata+18, signer_name_rdlen, signer_name)) - return 0; + memcpy(alg->digest, digestalg_final(), digestalg_len()); - out->alg = alg; - out->keytag = keytag; - out->signer_name = signer_name; - return 1; -} + if (key) + { + if (algo_in == algo && keytag_in == key_tag && + alg->vtbl->verify(alg, key, keylen)) + return STAT_SECURE; + } + else + { + /* iterate through all possible keys 4035 5.3.1 */ + for (; crecp; crecp = cache_find_by_name(crecp, keyname, now, F_DNSKEY)) + if (crecp->addr.key.algo == algo && crecp->addr.key.keytag == key_tag && + alg->vtbl->verify(alg, crecp->addr.key.keydata, crecp->uid)) + return STAT_SECURE; + } + } -static int end_rrsig_validation(PendingRRSIGValidation *val, struct crec *crec_dnskey) -{ - /* FIXME: keydata is non-contiguous */ - return val->alg->vtbl->verify(val->alg, crec_dnskey->addr.key.keydata, crec_dnskey->uid); + return STAT_BOGUS; } + - -static void dnssec_parserrsig(struct dns_header *header, size_t pktlen, - unsigned char *reply, int count, char *owner, - int sigclass, int sigrdlen, unsigned char *sig) +/* Validate all the RRsets in the answer and authority sections of the reply (4035:3.2.3) */ +int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class) { - PendingRRSIGValidation val; - - /* Initiate the RRSIG validation process. The pending state is returned into val. */ - if (!begin_rrsig_validation(header, pktlen, reply, count, owner, sigclass, sigrdlen, sig, &val)) - return; + unsigned char *ans_start, *p1, *p2; + int type1, class1, rdlen1, type2, class2, rdlen2; + int i, j, rc; - printf("RRSIG: querying cache for DNSKEY %s (keytag: %d)\n", val.signer_name, val.keytag); - - /* Look in the cache for *all* the DNSKEYs with matching signer_name and keytag */ - char onekey = 0; - struct crec *crecp = NULL; - while ((crecp = cache_find_by_name(crecp, val.signer_name, time(0), F_DNSKEY))) /* TODO: time(0) */ + if (!(ans_start = skip_questions(header, plen))) + return STAT_INSECURE; + + for (p1 = ans_start, i = 0; i < ntohs(header->ancount) + ntohs(header->nscount); i++) { - onekey = 1; - - if (crecp->addr.key.keytag == val.keytag - && crecp->addr.key.algo == verifyalg_algonum(val.alg)) - { - printf("RRSIG: found DNSKEY %d in cache, attempting validation\n", val.keytag); + if (!extract_name(header, plen, &p1, name, 1, 10)) + return STAT_INSECURE; /* bad packet */ + + GETSHORT(type1, p1); + GETSHORT(class1, p1); + p1 += 4; /* TTL */ + GETSHORT(rdlen1, p1); + + /* Don't try and validate RRSIGs! */ + if (type1 != T_RRSIG) + { + /* Check if we've done this RRset already */ + for (p2 = ans_start, j = 0; j < i; j++) + { + if (!(rc = extract_name(header, plen, &p2, name, 0, 10))) + return STAT_INSECURE; /* bad packet */ + + GETSHORT(type2, p2); + GETSHORT(class2, p2); + p2 += 4; /* TTL */ + GETSHORT(rdlen2, p2); + + if (type2 == type1 && class2 == class1 && rc == 1) + break; /* Done it before: name, type, class all match. */ + + if (!ADD_RDLEN(header, p2, plen, rdlen2)) + return STAT_INSECURE; + } + + /* Not done, validate now */ + if (j == i && (rc = validate_rrset(now, header, plen, class1, type1, name, keyname, NULL, 0, 0, 0)) != STAT_SECURE) + { + *class = class1; /* Class for DS or DNSKEY */ + return rc; + } + } - if (end_rrsig_validation(&val, crecp)) - printf("Validation OK\n"); - else - printf("ERROR: Validation FAILED (%s, keytag:%d, algo:%d)\n", owner, val.keytag, verifyalg_algonum(val.alg)); - } + if (!ADD_RDLEN(header, p1, plen, rdlen1)) + return STAT_INSECURE; } - if (!onekey) - { - printf("DNSKEY not found, need to fetch it\n"); - /* TODO: store PendingRRSIGValidation in routing table, - fetch key (and make it go through dnssec_parskey), then complete validation. */ - } + return STAT_SECURE; } -#endif /* comment out */ /* Compute keytag (checksum to quickly index a key). See RFC4034 */ -static int dnskey_keytag(int alg, unsigned char *rdata, int rdlen) +int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen) { if (alg == 1) { /* Algorithm 1 (RSAMD5) has a different (older) keytag calculation algorithm. See RFC4034, Appendix B.1 */ - return rdata[rdlen-3] * 256 + rdata[rdlen-2]; + return key[keylen-4] * 256 + key[keylen-3]; } else { unsigned long ac; int i; - ac = 0; - for (i = 0; i < rdlen; ++i) - ac += (i & 1) ? rdata[i] : rdata[i] << 8; - ac += (ac >> 16) & 0xFFFF; - return ac & 0xFFFF; - } -} - -/* Check if the DS record (from cache) points to the DNSKEY record (from cache) */ -static int dnskey_ds_match(struct crec *dnskey, struct crec *ds) -{ - if (dnskey->addr.key.keytag != ds->addr.key.keytag) - return 0; - if (dnskey->addr.key.algo != ds->addr.key.algo) - return 0; - - unsigned char owner[MAXCDNAME]; /* TODO: user part of daemon->namebuff */ - int owner_len = convert_domain_to_wire(cache_get_name(ds), owner); - size_t keylen = dnskey->uid; - int dig = ds->uid; - int digsize; - - if (!digestalg_begin(dig)) - return 0; - digsize = digestalg_len(); - digestalg_add_data(owner, owner_len); - digestalg_add_data("\x01\x01\x03", 3); - digestalg_add_data(&ds->addr.key.algo, 1); - digestalg_add_keydata(dnskey->addr.key.keydata, keylen); - return (memcmp(digestalg_final(), ds->addr.key.keydata->key, digsize) == 0); -} - -int dnssec_parsekey(struct dns_header *header, size_t pktlen, char *owner, unsigned long ttl, - int rdlen, unsigned char *rdata) -{ - int flags, proto, alg; - struct blockdata *key; struct crec *crecp; - unsigned char *ordata = rdata; int ordlen = rdlen; - - CHECKED_GETSHORT(flags, rdata, rdlen); - CHECKED_GETCHAR(proto, rdata, rdlen); - CHECKED_GETCHAR(alg, rdata, rdlen); - - if (proto != 3) - return 0; - /* Skip non-signing keys (as specified in RFC4034 */ - if (!(flags & 0x100)) - return 0; - - key = blockdata_alloc((char*)rdata, rdlen); - - /* TODO: time(0) is correct here? */ - crecp = cache_insert(owner, NULL, time(0), ttl, F_FORWARD | F_DNSKEY); - if (crecp) - { - /* TODO: improve union not to name "uid" this field */ - crecp->uid = rdlen; - crecp->addr.key.keydata = key; - crecp->addr.key.algo = alg; - crecp->addr.key.keytag = dnskey_keytag(alg, ordata, ordlen); - printf("DNSKEY: storing key for %s (keytag: %d)\n", owner, crecp->addr.key.keytag); - } - else - { - blockdata_free(key); - /* TODO: if insertion really might fail, verify we don't depend on cache - insertion success for validation workflow correctness */ - printf("DNSKEY: cache insertion failure\n"); - return 0; - } - return 1; -} - -int dnssec_parseds(struct dns_header *header, size_t pktlen, char *owner, unsigned long ttl, - int rdlen, unsigned char *rdata) -{ - int keytag, algo, dig; - struct blockdata *key; struct crec *crec_ds, *crec_key; - - CHECKED_GETSHORT(keytag, rdata, rdlen); - CHECKED_GETCHAR(algo, rdata, rdlen); - CHECKED_GETCHAR(dig, rdata, rdlen); - - if (!digestalg_supported(dig)) - return 0; - - key = blockdata_alloc((char*)rdata, rdlen); - - /* TODO: time(0) is correct here? */ - crec_ds = cache_insert(owner, NULL, time(0), ttl, F_FORWARD | F_DS); - if (!crec_ds) - { - blockdata_free(key); - /* TODO: if insertion really might fail, verify we don't depend on cache - insertion success for validation workflow correctness */ - printf("DS: cache insertion failure\n"); - return 0; + ac = ((htons(flags) >> 8) | ((htons(flags) << 8) & 0xff00)) + 0x300 + alg; + for (i = 0; i < keylen; ++i) + ac += (i & 1) ? key[i] : key[i] << 8; + ac += (ac >> 16) & 0xffff; + return ac & 0xffff; } - - /* TODO: improve union not to name "uid" this field */ - crec_ds->uid = dig; - crec_ds->addr.key.keydata = key; - crec_ds->addr.key.algo = algo; - crec_ds->addr.key.keytag = keytag; - printf("DS: storing key for %s (digest: %d)\n", owner, dig); - - /* Now try to find a DNSKEY which matches this DS digest. */ - printf("Looking for a DNSKEY matching DS %d...\n", keytag); - crec_key = NULL; - while ((crec_key = cache_find_by_name(crec_key, owner, time(0), F_DNSKEY))) /* TODO: time(0) */ - { - if (dnskey_ds_match(crec_key, crec_ds)) - { - /* TODO: create a link within the cache: ds => dnskey */ - printf("MATCH FOUND for keytag %d\n", keytag); - return 1; - } - } - - printf("ERROR: match not found for DS %d (owner: %s)\n", keytag, owner); - return 0; } -int dnssec1_validate(struct dns_header *header, size_t pktlen) -{ - unsigned char *p, *reply; - char *owner = daemon->namebuff; - int i, s, qtype, qclass, rdlen; - unsigned long ttl; - int slen[3] = { ntohs(header->ancount), ntohs(header->nscount), ntohs(header->arcount) }; - if (slen[0] + slen[1] + slen[2] == 0) - return 0; - if (!(reply = p = skip_questions(header, pktlen))) - return 0; - - /* First, process DNSKEY/DS records and add them to the cache. */ - cache_start_insert(); - for (i = 0; i < slen[0]; i++) - { - if (!extract_name(header, pktlen, &p, owner, 1, 10)) - return 0; - GETSHORT(qtype, p); - GETSHORT(qclass, p); - GETLONG(ttl, p); - GETSHORT(rdlen, p); - if (qtype == T_DS) - { - printf("DS found\n"); - dnssec_parseds(header, pktlen, owner, ttl, rdlen, p); - } - else if (qtype == T_DNSKEY) - { - printf("DNSKEY found\n"); - dnssec_parsekey(header, pktlen, owner, ttl, rdlen, p); - } - p += rdlen; - } - cache_end_insert(); - - /* After we have cached DNSKEY/DS records, start looking for RRSIGs. - We want to do this in a separate step because we want the cache - to be already populated with DNSKEYs before parsing signatures. */ - p = reply; - for (s = 0; s < 3; ++s) - { - reply = p; - for (i = 0; i < slen[s]; i++) - { - if (!extract_name(header, pktlen, &p, owner, 1, 10)) - return 0; - GETSHORT(qtype, p); - GETSHORT(qclass, p); - GETLONG(ttl, p); - GETSHORT(rdlen, p); - if (qtype == T_RRSIG) - { - printf("RRSIG found (owner: %s)\n", owner); - /* TODO: missing logic. We should only validate RRSIGs for which we - have a valid DNSKEY that is referenced by a DS record upstream. - There is a memory vs CPU conflict here; should we validate everything - to save memory and thus waste CPU, or better first acquire all information - (wasting memory) and then doing the minimum CPU computations required? */ - dnssec_parserrsig(header, pktlen, reply, slen[s], owner, qclass, rdlen, p); - } - p += rdlen; - } - } - - return 1; -} +#endif /* HAVE_DNSSEC */ diff --git a/src/forward.c b/src/forward.c index 7e422e3..64f9bac 100644 --- a/src/forward.c +++ b/src/forward.c @@ -344,7 +344,10 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, #ifdef HAVE_DNSSEC if (option_bool(OPT_DNSSEC_VALID)) - plen = add_do_bit(header, plen, ((char *) header) + PACKETSZ); + { + plen = add_do_bit(header, plen, ((char *) header) + PACKETSZ); + header->hb4 |= HB4_CD; + } #endif while (1) @@ -550,7 +553,7 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server SET_RCODE(header, NOERROR); } - if (extract_addresses(header, n, daemon->namebuff, now, sets, is_sign, check_rebind, no_cache)) + if (extract_addresses(header, n, daemon->namebuff, now, sets, is_sign, check_rebind, no_cache, cache_secure)) { my_syslog(LOG_WARNING, _("possible DNS-rebind attack detected: %s"), daemon->namebuff); munged = 1; @@ -678,41 +681,51 @@ void reply_query(int fd, int family, time_t now) if (option_bool(OPT_DNSSEC_VALID) && !(forward->flags & FREC_CHECKING_DISABLED)) { int status; - int class; + + /* We've had a reply already, which we're validating. Ignore this duplicate */ + if (forward->stash) + return; if (forward->flags & FREC_DNSKEY_QUERY) - status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class); + status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class); else if (forward->flags & FREC_DS_QUERY) status = dnssec_validate_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class); else - status = dnssec_validate_reply(header, n, daemon->namebuff, daemon->keyname, &forward->class); + status = dnssec_validate_reply(now, header, n, daemon->namebuff, daemon->keyname, &forward->class); /* Can't validate, as we're missing key data. Put this answer aside, whilst we get that. */ if (status == STAT_NEED_DS || status == STAT_NEED_KEY) { struct frec *new; - if ((forward->stash = blockdata_alloc((char *)header, n))) + + if ((new = get_new_frec(now, NULL, 1))) { - forward->stash_len = n; + struct frec *next = new->next; + *new = *forward; /* copy everything, then overwrite */ + new->next = next; + new->stash = NULL; + new->blocking_query = NULL; + new->flags &= ~(FREC_DNSKEY_QUERY | FREC_DS_QUERY); - if ((new = get_new_frec(now, NULL, 1))) + if ((forward->stash = blockdata_alloc((char *)header, n))) { int fd; - new = forward; /* copy everything, then overwrite */ + forward->stash_len = n; + new->dependent = forward; /* to find query awaiting new one. */ forward->blocking_query = new; /* for garbage cleaning */ - /* validate routines leave name of required record in daemon->namebuff */ + /* validate routines leave name of required record in daemon->keyname */ if (status == STAT_NEED_KEY) { new->flags |= FREC_DNSKEY_QUERY; - nn = dnssec_generate_query(header, daemon->namebuff, class, T_DNSKEY); + nn = dnssec_generate_query(header, daemon->keyname, forward->class, T_DNSKEY, &server->addr); } else if (status == STAT_NEED_DS) { new->flags |= FREC_DS_QUERY; - nn = dnssec_generate_query(header, daemon->namebuff, class, T_DS); + nn = dnssec_generate_query(header, daemon->keyname, forward->class, T_DS, &server->addr); } new->crc = questions_crc(header, nn, daemon->namebuff); new->new_id = get_id(new->crc); @@ -739,9 +752,11 @@ void reply_query(int fd, int family, time_t now) } /* Send DNSSEC query to same server as original query */ - while (sendto(fd, (char *)header, nn, 0, &server->addr.sa, sa_len(&server->addr)) == -1 && retry_send()); + while (sendto(fd, (char *)header, nn, 0, &server->addr.sa, sa_len(&server->addr)) == -1 && retry_send()); + server->queries++; } } + return; } @@ -750,35 +765,56 @@ void reply_query(int fd, int family, time_t now) and validate them with the new data. Failure to find needed data here is an internal error. Once we get to the original answer (FREC_DNSSEC_QUERY not set) and it validates, return it to the original requestor. */ - while (forward->dependent) + if (forward->flags & (FREC_DNSKEY_QUERY | FREC_DS_QUERY)) { - struct frec *prev = forward->dependent; - free_frec(forward); - forward = prev; - blockdata_retrieve_and_free(forward->stash, forward->stash_len, (void *)header); - n = forward->stash_len; - if (status == STAT_SECURE) + while (forward->dependent) { - if (forward->flags & FREC_DNSKEY_QUERY) - status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class); - else if (forward->flags & FREC_DS_QUERY) - status = dnssec_validate_dnskey(now, header, n, daemon->namebuff, daemon->keyname, forward->class); + struct frec *prev; - if (status == STAT_NEED_DS || status == STAT_NEED_KEY) - my_syslog(LOG_ERR, _("Unexpected missing data for DNSSEC validation")); + if (status == STAT_SECURE) + { + if (forward->flags & FREC_DNSKEY_QUERY) + status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class); + else if (forward->flags & FREC_DS_QUERY) + status = dnssec_validate_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class); + } + + prev = forward->dependent; + free_frec(forward); + forward = prev; + forward->blocking_query = NULL; /* already gone */ + blockdata_retrieve(forward->stash, forward->stash_len, (void *)header); + n = forward->stash_len; + } + + /* All DNSKEY and DS records done and in cache, now finally validate original + answer, provided last DNSKEY is OK. */ + if (status == STAT_SECURE) + status = dnssec_validate_reply(now, header, n, daemon->namebuff, daemon->keyname, &forward->class); + + if (status == STAT_NEED_DS || status == STAT_NEED_KEY) + { + my_syslog(LOG_ERR, _("Unexpected missing data for DNSSEC validation")); + status = STAT_INSECURE; } } - - /* All DNSKEY and DS records done and in cache, now finally validate original - answer, provided last DNSKEY is OK. */ - if (status == STAT_SECURE) - status = dnssec_validate_reply(header, n, daemon->namebuff, daemon->keyname, &forward->class); + + log_query(F_KEYTAG | F_SECSTAT, "result", NULL, + status == STAT_SECURE ? "SECURE" : (status == STAT_INSECURE ? "INSECURE" : "BOGUS")); + + no_cache_dnssec = 0; if (status == STAT_SECURE) cache_secure = 1; /* TODO return SERVFAIL here */ else if (status == STAT_BOGUS) no_cache_dnssec = 1; + + /* restore CD bit to the value in the query */ + if (forward->flags & FREC_CHECKING_DISABLED) + header->hb4 |= HB4_CD; + else + header->hb4 &= ~HB4_CD; } #endif @@ -1342,7 +1378,6 @@ static struct randfd *allocate_rfd(int family) return NULL; /* doom */ } - static void free_frec(struct frec *f) { if (f->rfd4 && --(f->rfd4->refcount) == 0) @@ -1361,7 +1396,10 @@ static void free_frec(struct frec *f) #ifdef HAVE_DNSSEC if (f->stash) - blockdata_free(f->stash); + { + blockdata_free(f->stash); + f->stash = NULL; + } /* Anything we're waiting on is pointless now, too */ if (f->blocking_query) diff --git a/src/option.c b/src/option.c index cadabd5..2933e9f 100644 --- a/src/option.c +++ b/src/option.c @@ -139,6 +139,7 @@ struct myoption { #define LOPT_QUIET_DHCP6 327 #define LOPT_QUIET_RA 328 #define LOPT_SEC_VALID 329 +#define LOPT_DNSKEY 330 #ifdef HAVE_GETOPT_LONG @@ -276,6 +277,7 @@ static const struct myoption opts[] = { "ipset", 1, 0, LOPT_IPSET }, { "synth-domain", 1, 0, LOPT_SYNTH }, { "dnssec", 0, 0, LOPT_SEC_VALID }, + { "dnskey", 1, 0, LOPT_DNSKEY }, #ifdef OPTION6_PREFIX_CLASS { "dhcp-prefix-class", 1, 0, LOPT_PREF_CLSS }, #endif @@ -428,6 +430,7 @@ static struct { { LOPT_SYNTH, ARG_DUP, ",,[]", gettext_noop("Specify a domain and address range for synthesised names"), NULL }, #ifdef HAVE_DNSSEC { LOPT_SEC_VALID, OPT_DNSSEC_VALID, NULL, gettext_noop("Activate DNSSEC validation"), NULL }, + { LOPT_DNSKEY, ARG_DUP, ",,", gettext_noop("Specify trust anchor DNSKEY"), NULL }, #endif #ifdef OPTION6_PREFIX_CLASS { LOPT_PREF_CLSS, ARG_DUP, "set:tag,", gettext_noop("Specify DHCPv6 prefix class"), NULL }, @@ -3670,9 +3673,34 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma daemon->host_records_tail = new; break; } - + +#ifdef HAVE_DNSSEC + case LOPT_DNSKEY: + { + struct dnskey *new = opt_malloc(sizeof(struct dnskey)); + char *key64, *algo; + + if (!(comma = split(arg)) || !(algo = split(comma)) || !(key64 = split(algo)) || + !atoi_check16(comma, &new->flags) || !atoi_check16(algo, &new->algo) || + !(new->name = canonicalise_opt(arg))) + ret_err(_("bad DNSKEY")); + + + /* Upper bound on length */ + new->key = opt_malloc((3*strlen(key64)/4)); + unhide_metas(key64); + if ((new->keylen = parse_base64(key64, new->key)) == -1) + ret_err(_("bad base64 in DNSKEY")); + + new->next = daemon->dnskeys; + daemon->dnskeys = new; + + break; + } +#endif + default: - ret_err(_("unsupported option (check that dnsmasq was compiled with DHCP/TFTP/DBus support)")); + ret_err(_("unsupported option (check that dnsmasq was compiled with DHCP/TFTP/DNSSEC/DBus support)")); } diff --git a/src/rfc1035.c b/src/rfc1035.c index e547782..a05060a 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -577,10 +577,13 @@ static size_t add_pseudoheader(struct dns_header *header, size_t plen, unsigned return plen; /* Too big */ } - PUTSHORT(optno, p); - PUTSHORT(optlen, p); - memcpy(p, opt, optlen); - p += optlen; + if (optno != 0) + { + PUTSHORT(optno, p); + PUTSHORT(optlen, p); + memcpy(p, opt, optlen); + p += optlen; + } PUTSHORT(p - datap, lenp); return p - (unsigned char *)header; @@ -889,7 +892,7 @@ static int find_soa(struct dns_header *header, size_t qlen, char *name) expired and cleaned out that way. Return 1 if we reject an address because it look like part of dns-rebinding attack. */ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t now, - char **ipsets, int is_sign, int check_rebind, int no_cache_dnssec) + char **ipsets, int is_sign, int check_rebind, int no_cache_dnssec, int secure) { unsigned char *p, *p1, *endrr, *namep; int i, j, qtype, qclass, aqtype, aqclass, ardlen, res, searched_soa = 0; @@ -919,6 +922,12 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t struct crec *cpp = NULL; int flags = RCODE(header) == NXDOMAIN ? F_NXDOMAIN : 0; unsigned long cttl = ULONG_MAX, attl; + + if (RCODE(header) == NXDOMAIN) + flags |= F_NXDOMAIN; + + if (secure) + flags |= F_DNSSECOK; namep = p; if (!extract_name(header, qlen, &p, name, 1, 4)) @@ -1446,7 +1455,7 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, int dryrun = 0, sec_reqd = 0; int is_sign; struct crec *crecp; - int nxdomain = 0, auth = 1, trunc = 0; + int nxdomain = 0, auth = 1, trunc = 0, sec_data = 1; struct mx_srv_record *rec; /* If there is an RFC2671 pseudoheader then it will be overwritten by @@ -1621,6 +1630,9 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, if (qtype == T_ANY && !(crecp->flags & (F_HOSTS | F_DHCP))) continue; + if (!(crecp->flags & F_DNSSECOK)) + sec_data = 0; + if (crecp->flags & F_NEG) { ans = 1; @@ -1794,6 +1806,9 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, if (qtype == T_ANY && !(crecp->flags & (F_HOSTS | F_DHCP | F_CONFIG))) break; + if (!(crecp->flags & F_DNSSECOK)) + sec_data = 0; + if (crecp->flags & F_CNAME) { char *cname_target = cache_get_cname_target(crecp); @@ -1868,6 +1883,9 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, if ((crecp = cache_find_by_name(NULL, name, now, F_CNAME)) && (qtype == T_CNAME || (crecp->flags & (F_HOSTS | F_DHCP | F_CONFIG)))) { + if (!(crecp->flags & F_DNSSECOK)) + sec_data = 0; + ans = 1; if (!dryrun) { @@ -2046,7 +2064,13 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, /* truncation */ if (trunc) header->hb3 |= HB3_TC; - + + header->hb4 &= ~HB4_AD; + + if (option_bool(OPT_DNSSEC_VALID) || option_bool(OPT_DNSSEC_PROXY)) + if (sec_data) + header->hb4 |= HB4_AD; + if (nxdomain) SET_RCODE(header, NXDOMAIN); else diff --git a/src/util.c b/src/util.c index 096297d..c210add 100644 --- a/src/util.c +++ b/src/util.c @@ -109,10 +109,10 @@ static int check_name(char *in) if (in[l-1] == '.') { - if (l == 1) return 0; in[l-1] = 0; + nowhite = 1; } - + for (; (c = *in); in++) { if (c == '.') @@ -482,6 +482,66 @@ int parse_hex(char *in, unsigned char *out, int maxlen, return i; } +#ifdef HAVE_DNSSEC +static int charval(char c) +{ + if (c >= 'A' && c <= 'Z') + return c - 'A'; + + if (c >= 'a' && c <= 'z') + return c - 'a' + 26; + + if (c >= '0' && c <= '9') + return c - '0' + 52; + + if (c == '+') + return 62; + + if (c == '/') + return 63; + + if (c == '=') + return -1; + + return -2; +} + +int parse_base64(char *in, char *out) +{ + char *p = out; + int i, val[4]; + + while (*in) + { + for (i = 0; i < 4; i++) + { + while (*in == ' ') + in++; + if (*in == 0) + return -1; + if ((val[i] = charval(*in++)) == -2) + return -1; + } + + while (*in == ' ') + in++; + + if (val[1] == -1) + return -1; /* too much padding */ + + *p++ = (val[0] << 2) | (val[1] >> 4); + + if (val[2] != -1) + *p++ = (val[1] << 4) | ( val[2] >> 2); + + if (val[3] != -1) + *p++ = (val[2] << 6) | val[3]; + } + + return p - out; +} +#endif + /* return 0 for no match, or (no matched octets) + 1 */ int memcmp_masked(unsigned char *a, unsigned char *b, int len, unsigned int mask) {