/*
**  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 <sys/param.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/nameser.h>
#include <netdb.h>
#include <resolv.h>
#include <assert.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>

/* libsm includes */
#include <sm/string.h>

/* libar includes */
#if USE_ARLIB
# include <ar.h>
#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:
** 
**  		<selector>._domainkey.<domain> <space> 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;
}


syntax highlighted by Code2HTML, v. 0.9.1