/* ** Copyright (c) 2007 Sendmail, Inc. and its suppliers. ** All rights reserved. */ #ifndef lint static char dkim_policy_c_id[] = "@(#)$Id: dkim-policy.c,v 1.23 2007/11/02 20:15:24 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.h" #include "dkim-types.h" #include "dkim-policy.h" #ifdef QUERY_CACHE # include "dkim-cache.h" #endif /* QUERY_CACHE */ /* 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_POLICY_FILE -- acquire a domain's policy record using a local file ** ** Parameters: ** dkim -- DKIM handle ** query -- query to execute ** buf -- buffer into which to write policy ** buflen -- number of bytes available at "buf" ** qstatus -- query result code (DNS-style) ** ** Return value: ** 1 -- query completed, answer returned in "buf" ** 0 -- query completed, no answer available ** -1 -- failure */ int dkim_get_policy_file(DKIM *dkim, unsigned char *query, unsigned char *buf, size_t buflen, int *qstatus) { bool found; int n; char *path; unsigned char *p; FILE *f; unsigned char inbuf[BUFRSZ + 1]; assert(dkim != NULL); assert(query != NULL); assert(buf != NULL); assert(qstatus != NULL); path = dkim->dkim_libhandle->dkiml_queryinfo; f = fopen(path, "r"); if (f == NULL) { dkim_error(dkim, "%s: fopen(): %s", path, strerror(errno)); return -1; } n = strlen(query); memset(inbuf, '\0', sizeof inbuf); found = FALSE; while (!found && fgets(inbuf, sizeof inbuf - 1, f) != NULL) { for (p = inbuf; *p != '\0'; p++) { if (*p == '\n' || *p == '#') { *p = '\0'; break; } } /* is this a match? */ if (strncasecmp(inbuf, query, n) == 0 && isascii(inbuf[n]) && isspace(inbuf[n])) { found = TRUE; /* move past spaces */ for (p = &inbuf[n] + 1; isascii(*p) && isspace(*p); p++) continue; sm_strlcpy(buf, p, buflen); *qstatus = NOERROR; fclose(f); return 1; } } if (ferror(f)) { dkim_error(dkim, "%s: fgets(): %s", path, strerror(errno)); fclose(f); return -1; } fclose(f); *qstatus = NXDOMAIN; return 0; } /* ** DKIM_GET_POLICY_DNS -- acquire a domain's policy record using DNS queries ** ** Parameters: ** dkim -- DKIM handle ** query -- query to execute ** usemx -- use an MX record? ** buf -- buffer into which to write policy ** buflen -- number of bytes available at "buf" ** qstatus -- query result code (DNS-style) ** ** Return value: ** 1 -- policy retrieved, stored in buffer ** 0 -- no policy found ** -1 -- failure */ int dkim_get_policy_dns(DKIM *dkim, unsigned char *query, bool usemx, unsigned char *buf, size_t buflen, int *qstatus) { int qdcount; int ancount; int status; int n; int c; int qtype; int type = -1; int class = -1; #ifdef QUERY_CACHE int ttl; #endif /* QUERY_CACHE */ size_t anslen; #if USE_ARLIB AR_LIB ar; AR_QUERY q; int arerror; #endif /* USE_ARLIB */ DKIM_LIB *lib; unsigned char *p; unsigned char *cp; unsigned char *eom; unsigned char ansbuf[MAXPACKET]; unsigned char namebuf[DKIM_MAXHOSTNAMELEN + 1]; unsigned char outbuf[BUFRSZ + 1]; #if USE_ARLIB struct timeval timeout; #endif /* USE_ARLIB */ HEADER hdr; assert(dkim != NULL); assert(query != NULL); assert(buf != NULL); assert(qstatus != NULL); lib = dkim->dkim_libhandle; #ifdef QUERY_CACHE if (lib->dkiml_cache != NULL) { int err = 0; size_t blen = buflen; dkim->dkim_cache_queries++; status = dkim_cache_query(lib->dkiml_cache, query, 0, buf, &blen, &err); if (status == 0) { dkim->dkim_cache_hits++; return (status == DKIM_STAT_OK ? 0 : -1); } /* XXX -- do something with errors here */ } #endif /* QUERY_CACHE */ qtype = (usemx ? T_MX : T_TXT); #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, query, C_IN, qtype, MAXCNAMEDEPTH, ansbuf, sizeof ansbuf, &arerror, dkim->dkim_timeout == 0 ? NULL : &timeout); if (q == NULL) { dkim_error(dkim, "ar_addquery() for `%s' failed", query); return -1; } 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, query, C_IN, qtype, MAXCNAMEDEPTH, ansbuf, sizeof ansbuf, &arerror, dkim->dkim_timeout == 0 ? NULL : &timeout); if (q == NULL) { dkim_error(dkim, "ar_addquery() failed for `%s'", query); return -1; } 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(query, C_IN, qtype, ansbuf, sizeof ansbuf); #endif /* USE_ARLIB */ #if USE_ARLIB if (status == AR_STAT_ERROR || status == AR_STAT_EXPIRED) { dkim_error(dkim, "ar_waitreply(): `%s' query %s", query, status == AR_STAT_ERROR ? "error" : "expired"); if (status == AR_STAT_EXPIRED) { *qstatus = SERVFAIL; return 0; } else { return -1; } } #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: *qstatus = NXDOMAIN; return 0; case NO_DATA: *qstatus = NOERROR; return 0; case TRY_AGAIN: case NO_RECOVERY: default: dkim_error(dkim, "res_query(): `%s' %s", query, hstrerror(h_errno)); *qstatus = SERVFAIL; return 0; } } #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, namebuf, sizeof namebuf); if ((n = dn_skipname(cp, eom)) < 0) { dkim_error(dkim, "`%s' reply corrupt", query); return -1; } cp += n; /* extract the type and class */ if (cp + INT16SZ + INT16SZ > eom) { dkim_error(dkim, "`%s' reply corrupt", query); return -1; } GETSHORT(type, cp); GETSHORT(class, cp); } if (type != qtype || class != C_IN) { dkim_error(dkim, "`%s' unexpected reply class/type", query); return -1; } /* if truncated, we can't do it */ if (hdr.tc) { dkim_error(dkim, "reply for `%s' truncated", query); return -1; } /* if we got something other than NOERROR, just return it */ *qstatus = hdr.rcode; if (hdr.rcode != NOERROR) return 0; /* get the answer count */ ancount = ntohs((unsigned short) hdr.ancount); if (ancount == 0) return 0; /* walk through the answers looking for the right record */ 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) namebuf, sizeof namebuf)) < 0) { dkim_error(dkim, "`%s' reply corrupt", query); return -1; } /* ...and move past it */ cp += n; /* extract the type and class */ if (cp + INT16SZ + INT16SZ > eom) { dkim_error(dkim, "`%s' reply corrupt", query); return -1; } GETSHORT(type, cp); GETSHORT(class, cp); /* handle a CNAME (skip it; 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 != qtype) { /* reject anything not valid (stupid wildcards) */ dkim_error(dkim, "`%s' unexpected reply class/type", query); return -1; } /* process it */ break; } if (ancount < 0) { dkim_error(dkim, "`%s' reply was unresolved CNAME", query); return -1; } /* ** If we're querying something other than a T_TXT here, it was ** used by part of the policy algorithm merely to validate the ** existence of a domain; the payload isn't really required. */ if (usemx) return 1; #ifdef QUERY_CACHE GETLONG(ttl, cp); #else /* QUERY_CACHE */ /* skip the TTL */ cp += INT32SZ; #endif /* QUERY_CACHE */ /* get payload length */ if (cp + INT16SZ > eom) { dkim_error(dkim, "`%s' reply corrupt", query); return -1; } GETSHORT(n, cp); /* XXX -- maybe deal with a partial reply rather than require it all */ if (cp + n > eom || n > BUFRSZ) { dkim_error(dkim, "`%s' reply corrupt", query); return -1; } /* extract the payload */ memset(outbuf, '\0', sizeof outbuf); p = outbuf; while (n > 0) { c = *cp++; n--; while (c > 0) { *p++ = *cp++; c--; n--; } } #ifdef QUERY_CACHE if (dkim->dkim_libhandle->dkiml_cache != NULL) { int err = 0; status = dkim_cache_insert(dkim->dkim_libhandle->dkiml_cache, query, outbuf, ttl, &err); /* XXX -- do something with errors here */ } #endif /* QUERY_CACHE */ sm_strlcpy(buf, outbuf, buflen); return 1; }