/* ** Copyright (c) 2005-2007 Sendmail, Inc. and its suppliers. ** All rights reserved. */ #ifndef lint static char dkim_keys_c_id[] = "@(#)$Id: dkim-keys.c,v 1.29 2007/10/29 23:14:35 msk Exp $"; #endif /* !lint */ /* system includes */ #include #include #include #include #include #include #include #include #include #include #include #include /* libsm includes */ #include /* libar includes */ #if USE_ARLIB # include #endif /* USE_ARLIB */ /* libdkim includes */ #include "dkim-types.h" #include "dkim-keys.h" #include "dkim-cache.h" #include "dkim.h" /* prototypes */ extern void dkim_error __P((DKIM *, const char *, ...)); /* local definitions needed for DNS queries */ #define MAXPACKET 8192 #if defined(__RES) && (__RES >= 19940415) # define RES_UNC_T char * #else /* __RES && __RES >= 19940415 */ # define RES_UNC_T unsigned char * #endif /* __RES && __RES >= 19940415 */ /* ** DKIM_GET_KEY_DNS -- retrieve a DKIM key from DNS ** ** Parameters: ** dkim -- DKIM handle ** sig -- DKIM_SIGINFO handle ** buf -- buffer into which to write the result ** buflen -- bytes available at "buf" ** ** Return value: ** A DKIM_STAT_* constant. */ DKIM_STAT dkim_get_key_dns(DKIM *dkim, DKIM_SIGINFO *sig, u_char *buf, size_t buflen) { #ifdef QUERY_CACHE bool cached = FALSE; int ttl = 0; #endif /* QUERY_CACHE */ int status; int qdcount; int ancount; #if USE_ARLIB int error; #endif /* USE_ARLIB */ int c; int n = 0; int type = -1; int class = -1; size_t anslen; #if USE_ARLIB AR_LIB ar; AR_QUERY q; #endif /* USE_ARLIB */ DKIM_LIB *lib; unsigned char *p; unsigned char *cp; unsigned char *eom; unsigned char *eob; char qname[DKIM_MAXHOSTNAMELEN + 1]; unsigned char ansbuf[MAXPACKET]; #if USE_ARLIB struct timeval timeout; #endif /* USE_ARLIB */ HEADER hdr; assert(dkim != NULL); assert(sig != NULL); assert(sig->sig_selector != NULL); assert(sig->sig_domain != NULL); assert(sig->sig_query == DKIM_QUERY_DNS); lib = dkim->dkim_libhandle; snprintf(qname, sizeof qname - 1, "%s.%s.%s", sig->sig_selector, DKIM_DNSKEYNAME, sig->sig_domain); #ifdef QUERY_CACHE /* see if we have this data already cached */ if (dkim->dkim_libhandle->dkiml_cache != NULL) { int err = 0; size_t blen = buflen; dkim->dkim_cache_queries++; status = dkim_cache_query(dkim->dkim_libhandle->dkiml_cache, qname, 0, buf, &blen, &err); if (status == 0) { dkim->dkim_cache_hits++; return DKIM_STAT_OK; } /* XXX -- do something with errors here */ } #endif /* QUERY_CACHE */ #if USE_ARLIB # ifdef _FFR_DNS_UPGRADE for (c = 0; c < 2; c++) { switch (c) { case 0: ar = dkim->dkim_libhandle->dkiml_arlib; break; case 1: ar = dkim->dkim_libhandle->dkiml_arlibtcp; break; } timeout.tv_sec = dkim->dkim_timeout; timeout.tv_usec = 0; q = ar_addquery(ar, qname, C_IN, T_TXT, MAXCNAMEDEPTH, ansbuf, sizeof ansbuf, &error, dkim->dkim_timeout == 0 ? NULL : &timeout); if (q == NULL) { dkim_error(dkim, "ar_addquery() for `%s' failed", qname); return DKIM_STAT_INTERNAL; } if (lib->dkiml_dns_callback == NULL) { status = ar_waitreply(ar, q, NULL, NULL); } else { for (;;) { timeout.tv_sec = lib->dkiml_callback_int; timeout.tv_usec = 0; status = ar_waitreply(ar, q, NULL, &timeout); if (status != AR_STAT_NOREPLY) break; lib->dkiml_dns_callback(dkim->dkim_user_context); } } (void) ar_cancelquery(ar, q); /* see if the UDP reply was truncated */ if (c == 0 && status == AR_STAT_SUCCESS) { memcpy(&hdr, ansbuf, sizeof hdr); if (hdr.tc) continue; } break; } # else /* _FFR_DNS_UPGRADE */ ar = dkim->dkim_libhandle->dkiml_arlib; timeout.tv_sec = dkim->dkim_timeout; timeout.tv_usec = 0; q = ar_addquery(ar, qname, C_IN, T_TXT, MAXCNAMEDEPTH, ansbuf, sizeof ansbuf, &error, dkim->dkim_timeout == 0 ? NULL : &timeout); if (q == NULL) { dkim_error(dkim, "ar_addquery() for `%s' failed", qname); return DKIM_STAT_INTERNAL; } if (lib->dkiml_dns_callback == NULL) { status = ar_waitreply(ar, q, NULL, NULL); } else { for (;;) { timeout.tv_sec = lib->dkiml_callback_int; timeout.tv_usec = 0; status = ar_waitreply(ar, q, NULL, &timeout); if (status != AR_STAT_NOREPLY) break; lib->dkiml_dns_callback(dkim->dkim_user_context); } } (void) ar_cancelquery(ar, q); # endif /* _FFR_DNS_UPGRADE */ #else /* USE_ARLIB */ status = res_query(qname, C_IN, T_TXT, ansbuf, sizeof ansbuf); #endif /* USE_ARLIB */ #if USE_ARLIB if (status == AR_STAT_ERROR || status == AR_STAT_EXPIRED) { dkim_error(dkim, "ar_waitreply(): `%s' %s", qname, status == AR_STAT_ERROR ? "error" : "expired"); return DKIM_STAT_KEYFAIL; } #else /* USE_ARLIB */ /* ** A -1 return from res_query could mean a bunch of things, ** not just NXDOMAIN. You can use h_errno to determine what ** -1 means. This is poorly documented. */ if (status == -1) { switch (h_errno) { case HOST_NOT_FOUND: case NO_DATA: return DKIM_STAT_NOKEY; case TRY_AGAIN: case NO_RECOVERY: default: dkim_error(dkim, "res_query(): `%s' %s", qname, hstrerror(h_errno)); return DKIM_STAT_KEYFAIL; } } #endif /* USE_ARLIB */ /* set up pointers */ anslen = sizeof ansbuf; memcpy(&hdr, ansbuf, sizeof hdr); cp = (u_char *) &ansbuf + HFIXEDSZ; eom = (u_char *) &ansbuf + anslen; /* skip over the name at the front of the answer */ for (qdcount = ntohs((unsigned short) hdr.qdcount); qdcount > 0; qdcount--) { /* copy it first */ (void) dn_expand((unsigned char *) &ansbuf, eom, cp, qname, sizeof qname); if ((n = dn_skipname(cp, eom)) < 0) { dkim_error(dkim, "`%s' reply corrupt", qname); return DKIM_STAT_INTERNAL; } cp += n; /* extract the type and class */ if (cp + INT16SZ + INT16SZ > eom) { dkim_error(dkim, "`%s' reply corrupt", qname); return DKIM_STAT_INTERNAL; } GETSHORT(type, cp); GETSHORT(class, cp); } if (type != T_TXT || class != C_IN) { dkim_error(dkim, "`%s' unexpected reply type/class", qname); return DKIM_STAT_INTERNAL; } /* if NXDOMAIN, return DKIM_STAT_NOKEY */ if (hdr.rcode == NXDOMAIN) return DKIM_STAT_NOKEY; /* if truncated, we can't do it */ if (hdr.tc) { dkim_error(dkim, "`%s' reply truncated", qname); return DKIM_STAT_INTERNAL; } /* get the answer count */ ancount = ntohs((unsigned short) hdr.ancount); if (ancount == 0) return DKIM_STAT_NOKEY; /* ** Extract the data from the first TXT answer. */ while (--ancount >= 0 && cp < eom) { /* grab the label, even though we know what we asked... */ if ((n = dn_expand((unsigned char *) &ansbuf, eom, cp, (RES_UNC_T) qname, sizeof qname)) < 0) { dkim_error(dkim, "`%s' reply corrupt", qname); return DKIM_STAT_INTERNAL; } /* ...and move past it */ cp += n; /* extract the type and class */ if (cp + INT16SZ + INT16SZ > eom) { dkim_error(dkim, "`%s' reply corrupt", qname); return DKIM_STAT_INTERNAL; } GETSHORT(type, cp); GETSHORT(class, cp); #ifdef QUERY_CACHE /* get the TTL */ GETLONG(ttl, cp); #else /* QUERY_CACHE */ /* skip the TTL */ cp += INT32SZ; #endif /* QUERY_CACHE */ /* skip CNAME if found; assume it was resolved */ if (type == T_CNAME) { char chost[DKIM_MAXHOSTNAMELEN + 1]; n = dn_expand((u_char *) &ansbuf, eom, cp, chost, DKIM_MAXHOSTNAMELEN); cp += n; continue; } else if (type != T_TXT) { dkim_error(dkim, "`%s' reply was unexpected type %d", qname, type); return DKIM_STAT_INTERNAL; } /* found a record we can use; break */ break; } /* if ancount went below 0, there were no good records */ if (ancount < 0) { dkim_error(dkim, "`%s' reply was unresolved CNAME", qname); return DKIM_STAT_INTERNAL; } /* get payload length */ if (cp + INT16SZ > eom) { dkim_error(dkim, "`%s' reply corrupt", qname); return DKIM_STAT_INTERNAL; } GETSHORT(n, cp); /* ** XXX -- maybe deal with a partial reply rather than require ** it all */ if (cp + n > eom) { dkim_error(dkim, "`%s' reply corrupt", qname); return DKIM_STAT_SYNTAX; } /* extract the payload */ memset(buf, '\0', buflen); p = buf; eob = buf + buflen; while (n > 0 && p <= eob) { c = *cp++; n--; while (c > 0 && p <= eob) { *p++ = *cp++; c--; n--; } } #ifdef QUERY_CACHE if (!cached && buf[0] != '\0' && dkim->dkim_libhandle->dkiml_cache != NULL) { int err = 0; status = dkim_cache_insert(dkim->dkim_libhandle->dkiml_cache, qname, buf, ttl, &err); /* XXX -- do something with errors here */ } #endif /* QUERY_CACHE */ return DKIM_STAT_OK; } /* ** DKIM_GET_KEY_FILE -- retrieve a DKIM key from a text file (for testing) ** ** Parameters: ** dkim -- DKIM handle ** sig -- DKIM_SIGINFO handle ** buf -- buffer into which to write the result ** buflen -- bytes available at "buf" ** ** Return value: ** A DKIM_STAT_* constant. ** ** Notes: ** The file opened is defined by the library option DKIM_OPTS_QUERYINFO ** and must be set prior to use of this function. Failing to do ** so will cause this function to return DKIM_STAT_KEYFAIL every time. ** The file should contain lines of the form: ** ** ._domainkey. key-data ** ** Case matching on the left is case-sensitive, but libdkim already ** wraps the domain name to lowercase. */ DKIM_STAT dkim_get_key_file(DKIM *dkim, DKIM_SIGINFO *sig, u_char *buf, size_t buflen) { FILE *f; u_char *p; u_char *p2; char *path; char name[BUFRSZ + 1]; assert(dkim != NULL); assert(sig != NULL); assert(sig->sig_selector != NULL); assert(sig->sig_domain != NULL); assert(sig->sig_query == DKIM_QUERY_FILE); path = dkim->dkim_libhandle->dkiml_queryinfo; if (path[0] == '\0') { dkim_error(dkim, "query file not defined"); return DKIM_STAT_KEYFAIL; } f = fopen(path, "r"); if (f == NULL) { dkim_error(dkim, "%s: fopen(): %s", path, strerror(errno)); return DKIM_STAT_KEYFAIL; } snprintf(name, sizeof name, "%s.%s.%s", sig->sig_selector, DKIM_DNSKEYNAME, sig->sig_domain); memset(buf, '\0', sizeof buf); while (fgets(buf, BUFRSZ, f) != NULL) { p2 = NULL; for (p = buf; *p != '\0'; p++) { if (*p == '\n') { *p = '\0'; break; } else if (isascii(*p) && isspace(*p)) { *p = '\0'; p2 = p + 1; } else if (p2 != NULL) { break; } } if (strcasecmp(name, buf) == 0) { sm_strlcpy(buf, p2, buflen); fclose(f); return DKIM_STAT_OK; } } fclose(f); return DKIM_STAT_NOKEY; }