/* Find public key in DNS * Copyright (C) 2000 D. Hugh Redelmeier. * * 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 the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. See . * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * RCSID $Id: dnskey.c,v 1.6 2000/06/19 20:24:54 dhr Exp $ */ #include #include #include #include #include #include /* ??? for h_errno */ #include #include "constants.h" #include "defs.h" #include "id.h" #include "log.h" #include "connections.h" /* needs id.h */ #include "preshared.h" /* needs connections.h */ #include "dnskey.h" #include "packet.h" /* The interface in RH6.x and BIND distribution 8.2.2 are different, * so we build some of our own :-( */ /* Support deprecated interface -- RH6.1 only supports it * Fake new interface! * See resolver(3) bind distribution (should be in RH6.1, but isn't). */ #ifdef OLD_RESOLVER # ifndef NS_MAXDNAME # define NS_MAXDNAME MAXDNAME # endif # ifndef NS_PACKETSZ # define NS_PACKETSZ PACKETSZ # endif # define res_ninit(statp) res_init() # define res_nquery(statp, dname, class, type, answer, anslen) \ res_query(dname, class, type, answer, anslen) # define res_nclose(statp) res_close() static struct __res_state *statp = &_res; #else /* !OLD_RESOLVER */ static struct __res_stat my_res_state = { 0 }; static res_stat statp = &my_res_state; #endif /* !OLD_RESOLVER */ /* structure of Query Reply (RFC 1035 4.1.1): * * +---------------------+ * | Header | * +---------------------+ * | Question | the question for the name server * +---------------------+ * | Answer | RRs answering the question * +---------------------+ * | Authority | RRs pointing toward an authority * +---------------------+ * | Additional | RRs holding additional information * +---------------------+ */ /* 4.1.1. Header section format: * 1 1 1 1 1 1 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | ID | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * |QR| Opcode |AA|TC|RD|RA| Z | RCODE | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | QDCOUNT | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | ANCOUNT | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | NSCOUNT | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | ARCOUNT | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ */ struct qr_header { u_int16_t id; /* 16-bit identifier to match query */ u_int16_t stuff; /* packed crud: */ #define QRS_QR 0x8000 /* QR: on if this is a response */ #define QRS_OPCODE_SHIFT 11 /* OPCODE field */ #define QRS_OPCODE_MASK 0xF #define QRSO_QUERY 0 /* standard query */ #define QRSO_IQUERY 1 /* inverse query */ #define QRSO_STATUS 2 /* server status request query */ #define QRS_AA 0x0400 /* AA: on if Authoritativ Answer */ #define QRS_TC 0x0200 /* TC: on if truncation happened */ #define QRS_RD 0x0100 /* RD: on if recursion desired */ #define QRS_RA 0x0080 /* RA: on if recursion available */ #define QRS_Z_SHIFT 4 /* Z field (reserved; must be zero) */ #define QRS_Z_MASK 0x7 #define QRS_RCODE_SHIFT 0 /* RCODE field: response code */ #define QRS_RCODE_MASK 0xF #define QRSR_OK 0 u_int16_t qdcount; /* number of entries in question section */ u_int16_t ancount; /* number of resource records in answer section */ u_int16_t nscount; /* number of name server resource records in authority section */ u_int16_t arcount; /* number of resource records in additional records section */ }; static field_desc qr_header_fields[] = { { ft_nat, 16/BITS_PER_BYTE, "ID", NULL }, { ft_nat, 16/BITS_PER_BYTE, "stuff", NULL }, { ft_nat, 16/BITS_PER_BYTE, "QD Count", NULL }, { ft_nat, 16/BITS_PER_BYTE, "Answer Count", NULL }, { ft_nat, 16/BITS_PER_BYTE, "Authority Count", NULL }, { ft_nat, 16/BITS_PER_BYTE, "Additional Count", NULL }, { ft_end, 0, NULL, NULL } }; static struct_desc qr_header_desc = { "Query Response Header", qr_header_fields, sizeof(struct qr_header) }; /* Messages for codes in RCODE (see RFC1035 4.1.1) */ static const complaint_t rcode_text[QRS_RCODE_MASK + 1] = { NULL, /* not an error */ "Format error - The name server was unable to interpret the query", "Server failure - The name server was unable to process this query" " due to a problem with the name server", "Name Error - Meaningful only for responses from an authoritative name" " server, this code signifies that the domain name referenced in" " the query does not exist", "Not Implemented - The name server does not support the requested" " kind of query", "Refused - The name server refuses to perform the specified operation" " for policy reasons", /* the rest are reserved for future use */ }; /* throw away a possibly compressed domain name */ static complaint_t eat_name(pb_stream *pbs) { u_char name_buf[NS_MAXDNAME + 2]; u_char *ip = pbs->cur; unsigned oi = 0; bool primary = TRUE; for (;;) { u_int8_t b; if (ip >= pbs->roof) return "ran out of message while skipping domain name"; b = *ip++; if (primary) pbs->cur = ip; if (b == 0) break; switch (b & 0xC0) { case 0x00: /* we grab the next b characters */ if (oi + b > NS_MAXDNAME) return "domain name too long"; if (pbs->roof - ip <= b) return "domain name falls off end of message"; if (oi != 0) name_buf[oi++] = '.'; memcpy(name_buf + oi, ip, b); oi += b; ip += b; if (primary) pbs->cur = ip; break; case 0xC0: { unsigned index; if (ip >= pbs->roof) return "ran out of message in middle of compressed domain name"; index = ((b & ~0xC0u) << 8) | *ip++; if (primary) pbs->cur = ip; if (index >= pbs_room(pbs)) return "impossible compressed domain name"; primary = FALSE; ip = pbs->start + index; } break; default: return "invalid code in label"; } } name_buf[oi++] = '\0'; DBG(DBG_PARSING, DBG_log("skipping name %s", name_buf)); return NULL; } /* non-variable part of 4.1.2 Question Section entry: * 1 1 1 1 1 1 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | | * / QNAME / * / / * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | QTYPE | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | QCLASS | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ */ struct qs_fixed { u_int16_t qtype; u_int16_t qclass; }; static field_desc qs_fixed_fields[] = { { ft_loose_enum, 16/BITS_PER_BYTE, "QTYPE", &rr_qtype_names }, { ft_loose_enum, 16/BITS_PER_BYTE, "QCLASS", &rr_class_names }, { ft_end, 0, NULL, NULL } }; static struct_desc qs_fixed_desc = { "Question Section entry fixed part", qs_fixed_fields, sizeof(struct qs_fixed) }; /* 4.1.3. Resource record format: * 1 1 1 1 1 1 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | | * / / * / NAME / * | | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | TYPE | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | CLASS | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | TTL | * | | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | RDLENGTH | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| * / RDATA / * / / * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ */ struct rr_fixed { u_int16_t type; u_int16_t class; u_int32_t ttl; /* actually signed */ u_int16_t rdlength; }; static field_desc rr_fixed_fields[] = { { ft_loose_enum, 16/BITS_PER_BYTE, "type", &rr_type_names }, { ft_loose_enum, 16/BITS_PER_BYTE, "class", &rr_class_names }, { ft_nat, 32/BITS_PER_BYTE, "TTL", NULL }, { ft_nat, 16/BITS_PER_BYTE, "RD length", NULL }, { ft_end, 0, NULL, NULL } }; static struct_desc rr_fixed_desc = { "Resource Record fixed part", rr_fixed_fields, /* note: following is tricky: avoids padding problems */ offsetof(struct rr_fixed, rdlength) + sizeof(u_int16_t) }; /* RFC 2355 3.1 KEY RDATA format: * * 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | flags | protocol | algorithm | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | / * / public key / * / / * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-| */ struct key_rdata { u_int16_t flags; u_int8_t protocol; u_int8_t algorithm; }; static field_desc key_rdata_fields[] = { { ft_nat, 16/BITS_PER_BYTE, "flags", NULL }, { ft_nat, 8/BITS_PER_BYTE, "protocol", NULL }, { ft_nat, 8/BITS_PER_BYTE, "algorithm", NULL }, { ft_end, 0, NULL, NULL } }; static struct_desc key_rdata_desc = { "KEY RR RData fixed part", key_rdata_fields, sizeof(struct key_rdata) }; /****************************************************************/ static complaint_t init_dns(void) { static bool inited = FALSE; if (!inited) { int r = res_ninit(statp); if (r != 0) return "undocumented failure of res_ninit"; inited = TRUE; #ifndef OLD_RESOLVER statp->options |= RES_ROTATE; #endif statp->options |= RES_DEBUG; } return NULL; } static complaint_t build_dns_name(u_char name_buf[NS_MAXDNAME + 1], struct id *id) { switch (id->kind) { case ID_IPV4_ADDR: snprintf(name_buf, NS_MAXDNAME + 1, "%d.%d.%d.%d.in-addr.arpa" , (id->ip_addr.s_addr >> 24) & 0xFF , (id->ip_addr.s_addr >> 16) & 0xFF , (id->ip_addr.s_addr >> 8) & 0xFF , (id->ip_addr.s_addr >> 0) & 0xFF); break; case ID_FQDN: if (id->name.len > NS_MAXDNAME) return "FQDN too long for domain name"; memcpy(name_buf, id->name.ptr, id->name.len); name_buf[id->name.len] = '\0'; break; default: return "can only query DNS for key for ID that is a FQDN or IPV4_ADDR"; } DBG(DBG_CONTROL, DBG_log("Querying DNS for KEY for %s", name_buf)); return NULL; } static complaint_t glean_key_from_dns_answer(struct id *id, u_char ans[], int anslen) { int r; /* all-purpose return value holder */ u_int16_t c; /* number of current RR in current answer section */ complaint_t ugh; pb_stream pbs; struct qr_header qr_header; bool key_found = FALSE; chunk_t key; init_pbs(&pbs, ans, anslen, "Query Response Message"); /* decode and check header */ if (!in_struct(&qr_header, &qr_header_desc, &pbs, NULL)) return "malformed header"; /* ID: nothing to do with us */ /* stuff -- lots of things */ if ((qr_header.stuff & QRS_QR) == 0) return "not a response?!?"; if (((qr_header.stuff >> QRS_OPCODE_SHIFT) & QRS_OPCODE_MASK) != QRSO_QUERY) return "unexpected opcode"; /* I don't think we care about AA */ if (qr_header.stuff & QRS_TC) return "response truncated"; /* I don't think we care about RD, RA */ if ((qr_header.stuff >> QRS_Z_SHIFT) & QRS_Z_MASK) return "Z bits are not zero"; r = (qr_header.stuff >> QRS_RCODE_SHIFT) & QRS_RCODE_MASK; if (r != 0) return r < (int)elemsof(rcode_text)? rcode_text[r] : "unknown rcode"; if (qr_header.ancount == 0) return "no KEY RR found by DNS"; /* end of header checking */ /* Question Section processing */ /* 4.1.2. Question section format: * 1 1 1 1 1 1 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | | * / QNAME / * / / * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | QTYPE | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ * | QCLASS | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ */ for (c = 0; c != qr_header.qdcount; c++) { struct qs_fixed qsf; ugh = eat_name(&pbs); if (ugh != NULL) return ugh; if (!in_struct(&qsf, &qs_fixed_desc, &pbs, NULL)) return "failed to get fixed part of Question Section"; if (qsf.qtype != T_KEY) return "unexpected QTYPE in Question Section"; if (qsf.qclass != C_IN) return "unexpected QCLASS in Question Section"; } /* rest of sections are made up of Resource Records */ /* Answer Section processing */ for (c = 0; c != qr_header.ancount; c++) { struct rr_fixed rrf; size_t tail; /* ??? do we need to match the name? */ ugh = eat_name(&pbs); if (ugh != NULL) return ugh; if (!in_struct(&rrf, &rr_fixed_desc, &pbs, NULL)) return "failed to get fixed part of Answer Section Resource Record"; if (rrf.rdlength > pbs_left(&pbs)) return "RD Length extends beyond end of message"; /* ??? should we care about ttl? */ tail = rrf.rdlength; if (rrf.type == T_KEY && rrf.class == C_IN) { struct key_rdata kr; if (tail < sizeof(struct key_rdata)) return "KEY Resource Record's RD Length is too small"; if (!in_struct(&kr, &key_rdata_desc, &pbs, NULL)) return "failed to get fixed part or KEY Resource Record RDATA"; tail -= sizeof(struct key_rdata); if (kr.protocol == 4 /* IPSEC (RFC2535 3.1.3) */ && kr.algorithm == 1 /* RSA/MD5 (RFC2535 3.2) */ && (kr.flags & 0x8000) == 0 /* use for authentication (3.1.2) */ && (kr.flags & 0x2CF0) == 0) /* must be zero */ { /* we have what seems to be a tasty key */ if (key_found) return "too many keys found: only one allowed"; key_found = TRUE; key.len = tail; key.ptr = pbs.cur; } } in_raw(NULL, tail, &pbs, "RR RDATA"); } /* Authority Section processing (just sanity checking) */ for (c = 0; c != qr_header.nscount; c++) { struct rr_fixed rrf; size_t tail; ugh = eat_name(&pbs); if (ugh != NULL) return ugh; if (!in_struct(&rrf, &rr_fixed_desc, &pbs, NULL)) return "failed to get fixed part of Authority Section Resource Record"; if (rrf.rdlength > pbs_left(&pbs)) return "RD Length extends beyond end of message"; /* ??? should we care about ttl? */ tail = rrf.rdlength; in_raw(NULL, tail, &pbs, "RR RDATA"); } /* Additional Section processing (just sanity checking) */ for (c = 0; c != qr_header.arcount; c++) { struct rr_fixed rrf; size_t tail; ugh = eat_name(&pbs); if (ugh != NULL) return ugh; if (!in_struct(&rrf, &rr_fixed_desc, &pbs, NULL)) return "failed to get fixed part of Authority Section Resource Record"; if (rrf.rdlength > pbs_left(&pbs)) return "RD Length extends beyond end of message"; /* ??? should we care about ttl? */ tail = rrf.rdlength; in_raw(NULL, tail, &pbs, "RR RDATA"); } /* done all sections */ /* ??? is padding legal, or can we complain if more left in record? */ if (!key_found) return "no suitable key found in DNS"; add_public_key(id, PUBKEY_ALG_RSA, &key); return NULL; } complaint_t fetch_public_key(struct id *id) { complaint_t ugh; u_char name_buf[NS_MAXDNAME + 1]; u_char ans[NS_PACKETSZ * 10]; /* very probably bigger than necessary */ int anslen; ugh = init_dns(); if (ugh != NULL) return ugh; ugh = build_dns_name(name_buf, id); if (ugh != NULL) return ugh; anslen = res_nquery(statp, name_buf, C_IN, T_KEY, ans, sizeof(ans)); if (anslen == -1) { /* newer resolvers support statp->res_h_errno as well as h_errno. * That might be better, but older resolvers don't. * See resolver(3), if you have it. */ return hstrerror(h_errno); } if (anslen > (int) sizeof(ans)) return "(internal error) answer too long for buffer"; return glean_key_from_dns_answer(id, ans, anslen); }