diff --git a/Makefile b/Makefile index fe63aee..2f853b9 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,8 @@ version = -DVERSION='\"`$(top)/bld/get-version $(top)`\"' objs = cache.o rfc1035.o util.o option.o forward.o network.o \ dnsmasq.o dhcp.o lease.o rfc2131.o netlink.o dbus.o bpf.o \ helper.o tftp.o log.o conntrack.o dhcp6.o rfc3315.o \ - dhcp-common.o outpacket.o radv.o slaac.o auth.o ipset.o domain.o + dhcp-common.o outpacket.o radv.o slaac.o auth.o ipset.o \ + domain.o dnssec.o hdrs = dnsmasq.h config.h dhcp-protocol.h dhcp6-protocol.h \ dns-protocol.h radv-protocol.h diff --git a/src/dnssec.c b/src/dnssec.c new file mode 100644 index 0000000..27cef65 --- /dev/null +++ b/src/dnssec.c @@ -0,0 +1,278 @@ + +#include "dnsmasq.h" +#include + +#define SERIAL_UNDEF -100 +#define SERIAL_EQ 0 +#define SERIAL_LT -1 +#define SERIAL_GT 1 + +#define countof(x) (long)(sizeof(x) / sizeof(x[0])) +#define MIN(a,b) ((a) < (b) ? (a) : (b)) + +/* + * vtable for a signature verification algorithm. + * + * Each algorithm verifies that a certain signature over a (possibly non-contigous) + * array of data has been made with the specified key. + * + * Sample of usage: + * + * // First, set the signature we need to check. Notice: data is not copied + * // nor consumed, so the pointer must stay valid. + * alg->set_signature(sig, 16); + * + * // Second, push the data in; data is consumed immediately, so the buffer + * // can be freed or modified. + * alg->begin_data(); + * alg->add_data(buf1, 123); + * alg->add_data(buf2, 45); + * alg->add_data(buf3, 678); + * alg->end_data(); + * + * // Third, verify if we got the correct key for this signature. + * alg->verify(key1, 16); + * alg->verify(key2, 16); + */ +typedef struct +{ + int (*set_signature)(unsigned char *data, unsigned len); + void (*begin_data)(void); + void (*add_data)(void *data, unsigned len); + void (*end_data)(void); + int (*verify)(unsigned char *key, unsigned key_len); +} VerifyAlg; + +/* RFC4034, Appendix A.1: only algorithm 3 (DSA/SHA1) and 5 (RSA/SHA1) are + currently valid for zone-signing. */ +static const VerifyAlg valgs[6] = +{ + {0,0,0,0,0}, /* 0: reserved */ + {0,0,0,0,0}, /* 1: RSA/MD5 */ + {0,0,0,0,0}, /* 2: DH */ + {0,0,0,0,0}, /* 3: DSA/SHA1 */ + {0,0,0,0,0}, /* 4: ECC */ + {0,0,0,0,0}, /* 5: RSA/SHA1 */ +}; + +/* Implement RFC1982 wrapped compare for 32-bit numbers */ +static int serial_compare_32(unsigned long s1, unsigned long s2) +{ + if (s1 == s2) + return SERIAL_EQ; + + if ((s1 < s2 && (s2 - s1) < (1UL<<31)) || + (s1 > s2 && (s1 - s2) > (1UL<<31))) + return SERIAL_LT; + if ((s1 < s2 && (s2 - s1) > (1UL<<31)) || + (s1 > s2 && (s1 - s2) < (1UL<<31))) + return SERIAL_GT; + 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 (*buf >= 'A' && *buf <= 'Z') + *buf += 'a' - 'A'; + buf++; + } + *buf++ = '.'; + } + *buf = 0; + if (rr == end) + return 0; + return rr-start; +} + +/* Check whether today/now is between date_start and date_end */ +static int check_date_range(unsigned long date_start, unsigned long date_end) +{ + /* TODO: double-check that time(0) is the correct time we are looking for */ + /* TODO: dnssec requires correct timing; implement SNTP in dnsmasq? */ + unsigned long curtime = time(0); + + /* We must explicitly check against wanted values, because of SERIAL_UNDEF */ + if (serial_compare_32(curtime, date_start) != SERIAL_GT) + return 0; + if (serial_compare_32(curtime, date_end) != SERIAL_LT) + return 0; + return 1; +} + +/* Sort RRs within a RRset in canonical order, according to RFC4034, ยง6.3 + Notice that the RRDATA sections have been already normalized, so a memcpy + is sufficient. + NOTE: r1/r2 point immediately after the owner name. */ +static int rrset_canonical_order(const void *r1, const void *r2) +{ + int r1len, r2len, res; + const unsigned char *pr1=r1, *pr2=r2; + + pr1 += 8; pr2 += 8; + GETSHORT(r1len, pr1); GETSHORT(r2len, pr2); + + /* Lexicographically compare RDATA (thus, if equal, smaller length wins) */ + res = memcmp(pr1, pr2, MIN(r1len, r2len)); + if (res == 0) + { + if (r1len < r2len) + return -1; + else + /* NOTE: RFC2181 says that an RRset is not allowed to contain duplicate + records. If it happens, it is a protocol error and anything goes. */ + return 1; + } + + return res; +} + +static int validate_rrsig(struct dns_header *header, size_t pktlen, + unsigned char *reply, int count, char *owner, + int sigclass, int sigrdlen, unsigned char *sig) +{ + 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 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 (sigalg >= countof(valgs) || !valgs[sigalg].set_signature) + { + printf("RRSIG algorithm not supported: %d\n", sigalg); + return 0; + } + + if (!check_date_range(ntohl(date_start), ntohl(date_end))) + { + printf("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; + assert(rrsetidx < countof(rrset)); + /* TODO: here we should convert to lowercase domain names within + RDATA. We can do it in place. */ + } + p += rdlen; + } + + /* Sort RRset records in canonical order. */ + qsort(rrset, rrsetidx, sizeof(void*), rrset_canonical_order); + + /* Extract the signer name (we need to query DNSKEY of this name) */ + if (!(res = extract_name_no_compression(sig, sigrdlen, signer_name))) + return 0; + sig += res; sigrdlen -= res; + + /* Now initialize the signature verification algorithm and process the whole + RRset */ + const VerifyAlg *alg = &valgs[sigalg]; + if (!alg->set_signature(sig, sigrdlen)) + return 0; + + alg->begin_data(); + alg->add_data(sigrdata, 18); + alg->add_data(signer_name, strlen(signer_name)-1); /* remove trailing dot */ + for (i = 0; i < rrsetidx; ++i) + { + int rdlen; + + alg->add_data(owner, strlen(owner)); + alg->add_data(&sigtype, 2); + alg->add_data(&sigclass, 2); + alg->add_data(&sigttl, 4); + + p = (unsigned char*)(rrset[i]); + p += 8; + GETSHORT(rdlen, p); + alg->add_data(p-2, rdlen+2); + } + alg->end_data(); + + /* TODO: now we need to fetch the DNSKEY of signer_name with the specified + keytag, and check whether it validates with the current algorithm. */ + /* + pseudo-code: + + char *key; int keylen; + if (!fetch_dnskey(signer_name, keytag, &key, &keylen)) + return 0; + return alg->verify(key, keylen); + */ + return 0; +} + + +int dnssec_validate(struct dns_header *header, size_t pktlen) +{ + unsigned char *p, *reply; + char *owner = daemon->namebuff; + int i, qtype, qclass, rdlen; + unsigned long ttl; + + if (header->ancount == 0) + return 0; + if (!(reply = p = skip_questions(header, pktlen))) + return 0; + for (i = 0; i < ntohs(header->ancount); 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\n"); + /* 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? */ + validate_rrsig(header, pktlen, reply, ntohs(header->ancount), owner, qclass, rdlen, p); + } + p += rdlen; + } + return 1; +} diff --git a/src/forward.c b/src/forward.c index 7d106c9..f93cc54 100644 --- a/src/forward.c +++ b/src/forward.c @@ -491,7 +491,12 @@ static size_t process_reply(struct dns_header *header, time_t now, if (!option_bool(OPT_LOG)) server->flags |= SERV_WARNED_RECURSIVE; } - + +#ifdef HAVE_DNSSEC + printf("validate\n"); + dnssec_validate(header, n); +#endif + if (daemon->bogus_addr && RCODE(header) != NXDOMAIN && check_for_bogus_wildcard(header, n, daemon->namebuff, daemon->bogus_addr, now)) {