/* ** Copyright (c) 2004-2007 Sendmail, Inc. and its suppliers. ** All rights reserved. ** ** $Id: dk.c,v 1.181 2007/05/31 02:05:39 msk Exp $ */ #ifndef lint static char dk_c_id[] = "@(#)$Id: dk.c,v 1.181 2007/05/31 02:05:39 msk Exp $"; #endif /* !lint */ /* system includes */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* OpenSSL includes */ #include #include #include #include /* libdk includes */ #include #include #include /* libsm includes */ #include #include #include /* libar includes */ #if USE_ARLIB # include "ar.h" #endif /* USE_ARLIB */ /* libdk includes */ #include "rfc2822.h" /* useful stuff */ #ifndef NULL # define NULL 0 #endif /* ! NULL */ #ifndef FALSE # define FALSE 0 #endif /* ! FALSE */ #ifndef TRUE # define TRUE 1 #endif /* ! TRUE */ #ifndef MAXHOSTNAMELEN # define MAXHOSTNAMELEN 256 #endif /* ! MAXHOSTNAMELEN */ #ifndef __P # ifdef __STDC__ # define __P(x) x # else /* __STDC__ */ # define __P(x) () # endif /* __STDC__ */ #endif /* ! __P */ #define CRLF "\r\n" /* CRLF */ /* limits, macros, etc. */ #define BUFRSZ 1024 /* block I/O buffer size */ #define DEFERRLEN 128 /* default error buffer size */ #define DEFTIMEOUT 5 /* default DNS timeout */ #define MAXPARAMETER 256 /* biggest parameter we accept */ /* defaults */ #define DK_DEFAULT_SELECTOR "main" /* default selector (filename only) */ #define DK_DNSNAME "_domainkey" /* common DNS label */ #define DK_TMPDIR "/var/tmp" /* default dir. for temp files */ /* states */ #define DK_STATE_INIT 0 /* initialized */ #define DK_STATE_HEADER 1 /* receiving headers */ #define DK_STATE_EOH 2 /* at or past EOH */ #define DK_STATE_BODY 3 /* receiving body */ #define DK_STATE_EOM 4 /* at EOM */ /* files */ #define DK_PRIVATEKEY "/var/db/domainkeys/%s.key.pem" /* template filename for private keys */ /* prototypes */ static void dk_error(DK *dk, const char *format, ...); /* parameter lookup table */ struct lookup { char *str; int code; }; struct lookup params[] = { { "a", DK_PARAM_SIGNALG }, { "b", DK_PARAM_SIGNATURE }, { "c", DK_PARAM_CANONALG }, { "d", DK_PARAM_DOMAIN }, { "h", DK_PARAM_HDRLIST }, { "q", DK_PARAM_QUERYMETHOD }, { "s", DK_PARAM_SELECTOR }, { NULL, -1 } }; /* 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 */ /* base64 alphabet */ static unsigned char alphabet[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /* base64 decode stuff */ static int decoder[256] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 0, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 0, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; struct dk_lib { int dkl_options; char * dkl_tmpdir; void * (*dkl_malloc) (void *closure, size_t nbytes); void (*dkl_free) (void *closure, void *p); #if USE_ARLIB AR_LIB dkl_arlib; #endif /* USE_ARLIB */ }; /* other stuff */ #ifndef MIN # define MIN(x,y) ((x) < (y) ? (x) : (y)) #endif /* ! MIN */ #define DK_DEBUG(x) (getenv("DKDEBUG") != NULL && \ strchr(getenv("DKDEBUG"), (x)) != NULL) #define DK_DECODE_SIGNATURE 0 #define DK_DECODE_KEY 1 #define DK_CRLF_UNKNOWN (-1) #define DK_CRLF_CRLF 0 #define DK_CRLF_LF 1 #define DK_MODE_UNKNOWN (-1) #define DK_MODE_SIGN 0 #define DK_MODE_VERIFY 1 #define DK_FLAG_FREESIGNATURE 0001 /* ** ===== PRIVATE SECTION */ /* ** DK_MALLOC -- allocate some memory ** ** Parameters: ** libhandle -- DK_LIB handle ** closure -- closure to use ** nbytes -- how many bytes we want ** ** Return value: ** Pointer to new memory, or NULL if it failed. */ static void * dk_malloc(DK_LIB *libhandle, void *closure, size_t nbytes) { assert(libhandle != NULL); if (libhandle->dkl_malloc == NULL) return malloc(nbytes); else return libhandle->dkl_malloc(closure, nbytes); } /* ** DK_MFREE -- free some memory ** ** Parameters: ** libhandle -- DK_LIB handle ** closure -- closure to use ** p -- pointer referring to memory to release ** ** Return value: ** None. */ static void dk_mfree(DK_LIB *libhandle, void *closure, void *p) { assert(libhandle != NULL); if (libhandle->dkl_free == NULL) free(p); else libhandle->dkl_free(closure, p); } /* ** DK_STRDUP -- duplicate a string ** ** Parameters: ** libhandle -- DK_LIB handle ** closure -- closure to use ** str -- string to clone ** ** Return value: ** Pointer to copy of "str", or NULL on failure. */ static char * dk_strdup(DK_LIB *libhandle, void *closure, char *str) { char *new; size_t len; assert(libhandle != NULL); assert(str != NULL); len = strlen(str) + 1; new = dk_malloc(libhandle, closure, len); if (new != NULL) sm_strlcpy(new, str, len); return new; } /* ** DK_ISFWS -- return TRUE iff the specified character is folding whitespace ** ** Parameters: ** c -- character ** ** Return value: ** TRUE iff the specified character is folding whitespace. */ static bool dk_isfws(int c) { return (c == 0x09 || c == 0x0a || c == 0x0d || c == 0x20); } /* ** DK_DECODE -- decode a base64-encoded key or signature ** ** Parameters: ** dk -- DK handle ** what -- what to decode (DK_DECODE_KEY or DK_DECODE_SIGNATURE) ** ** Return value: ** TRUE on success, FALSE otherwise (malloc() failure). */ static bool dk_decode(DK *dk, int what) { int n; int bits = 0; int char_count = 0; size_t keyidx; unsigned char *c; unsigned char *x; assert(dk != NULL); assert(what == DK_DECODE_KEY || what == DK_DECODE_SIGNATURE); if (what == DK_DECODE_KEY) { dk->dk_key = dk_malloc(dk->dk_libhandle, dk->dk_closure, dk->dk_b64len); if (dk->dk_key == NULL) { dk_error(dk, "unable to allocate %d byte(s)", dk->dk_b64len); return FALSE; } memset(dk->dk_key, '\0', dk->dk_b64len); x = dk->dk_key; } else { dk->dk_signature = dk_malloc(dk->dk_libhandle, dk->dk_closure, dk->dk_b64len); if (dk->dk_signature == NULL) { dk_error(dk, "unable to allocate %d byte(s)", dk->dk_b64len); return FALSE; } memset(dk->dk_signature, '\0', dk->dk_b64len); x = dk->dk_signature; dk->dk_flags |= DK_FLAG_FREESIGNATURE; } keyidx = 0; for (c = dk->dk_b64, n = 0; n < dk->dk_b64len; c++, n++) { /* end padding */ if (*c == '=') break; /* skip stuff not part of the base64 alphabet (RFC2045) */ if (!((*c >= 'A' && *c <= 'Z') || (*c >= 'a' && *c <= 'z') || (*c >= '0' && *c <= '9') || (*c == '+') || (*c == '/'))) continue; /* everything else gets decoded */ bits += decoder[(int) *c]; char_count++; if (char_count == 4) { x[keyidx++] = (bits >> 16); x[keyidx++] = ((bits >> 8) & 0xff); x[keyidx++] = (bits & 0xff); bits = 0; char_count = 0; } else { bits <<= 6; } } /* XXX -- don't bother checking for proper termination (for now) */ /* process trailing data, if any */ switch (char_count) { case 0: break; case 1: /* base64 decoding incomplete; at least two bits missing */ break; case 2: x[keyidx++] = (bits >> 10); break; case 3: x[keyidx++] = (bits >> 16); x[keyidx++] = ((bits >> 8) & 0xff); break; } /* store the length of what was decoded */ if (what == DK_DECODE_KEY) { dk->dk_keylen = keyidx; } else { dk->dk_signlen = keyidx; } return TRUE; } /* ** DK_IN_USE -- see if a domain seems to be using DomainKeys ** ** Parameters: ** dk -- DK handle ** ** Return value: ** -1 -- error ** 0 -- domain does not appear to use DomainKeys ** 1 -- domain appears to use DomainKeys */ static int dk_in_use(DK *dk) { int qdcount; int ancount; int status; int n; int c; int type = -1; int class = -1; int state; #if USE_ARLIB int error; #endif /* USE_ARLIB */ size_t plen = 0; size_t anslen; #if USE_ARLIB AR_QUERY q; #endif /* USE_ARLIB */ char *ssel; unsigned char *p; unsigned char *cp; unsigned char *eom; unsigned char ansbuf[MAXPACKET]; unsigned char buf[BUFRSZ + 1]; char qname[MAXHOSTNAMELEN + 1]; char tag[MAXPARAMETER + 1]; char value[MAXPARAMETER + 1]; #if USE_ARLIB struct timeval timeout; #endif /* USE_ARLIB */ HEADER hdr; assert(dk != NULL); ssel = dk_sterilize(dk->dk_domain); if (ssel == NULL) return -1; /* send the NS query */ memset(qname, '\0', sizeof qname); snprintf(qname, sizeof qname - 1, "%s.%s", DK_DNSNAME, dk_sterilize(dk->dk_domain)); #if USE_ARLIB timeout.tv_sec = dk->dk_timeout; timeout.tv_usec = 0; if (DK_DEBUG('r')) { syslog(LOG_INFO, "thread %p ar_addquery(`%s')", pthread_self(), qname); } q = ar_addquery(dk->dk_libhandle->dkl_arlib, qname, C_IN, T_TXT, MAXCNAMEDEPTH, ansbuf, sizeof ansbuf, &error, dk->dk_timeout == 0 ? NULL : &timeout); if (DK_DEBUG('r')) { syslog(LOG_INFO, "thread %p ar_addquery(`%s') done", pthread_self(), qname); } if (q == NULL) return -1; status = ar_waitreply(dk->dk_libhandle->dkl_arlib, q, NULL, NULL); (void) ar_cancelquery(dk->dk_libhandle->dkl_arlib, q); if (DK_DEBUG('r')) { syslog(LOG_INFO, "thread %p ar_cancelquery(`%s')", pthread_self(), qname); } #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) { if (DK_DEBUG('d') && dk->dk_id != NULL) { syslog(LOG_NOTICE, "%s: ar_waitreply(): %s", dk->dk_id, ar_strerror(error)); } 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: case NO_DATA: return 0; case TRY_AGAIN: case NO_RECOVERY: default: return -1; } } #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) return DK_STAT_INTERNAL; cp += n; /* extract the type and class */ if (cp + INT16SZ + INT16SZ > eom) return -1; GETSHORT(type, cp); GETSHORT(class, cp); } if (type != T_TXT || class != C_IN) return -1; /* if NXDOMAIN, return DK_STAT_CANTVRFY */ if (hdr.rcode == NXDOMAIN) return 0; /* get the answer count */ ancount = ntohs((unsigned short) hdr.ancount); if (ancount == 0) return 0; /* if truncated, we can't do it */ if (hdr.tc) return -1; /* 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) return DK_STAT_INTERNAL; /* ...and move past it */ cp += n; /* extract the type and class */ if (cp + INT16SZ + INT16SZ > eom) return -1; GETSHORT(type, cp); GETSHORT(class, cp); /* reject anything that's not valid (stupid wildcards) */ if (type != T_TXT || class != C_IN) return -1; /* skip the TTL */ cp += INT32SZ; /* get payload length */ if (cp + INT16SZ > eom) return -1; GETSHORT(n, cp); /* XXX -- maybe deal with a partial reply rather than require it all */ if (cp + n > eom) return -1; /* extract the payload */ memset(buf, '\0', sizeof buf); p = buf; while (n > 0) { c = *cp++; n--; while (c > 0) { *p++ = *cp++; c--; n--; } } state = 0; for (p = buf; ; p++) { if (*p == '\0') { switch (state) { case 0: case 4: return 1; case 1: case 2: /* malformed */ return -1; case 3: state = 4; break; } } switch (state) { case 0: /* before tag */ if (isascii(*p) && isspace(*p)) continue; memset(tag, '\0', sizeof tag); plen = 0; state = 1; /* FALLTHROUGH */ case 1: /* in tag */ if (*p == '=') { state = 2; continue; } else if (isascii(*p) && isspace(*p)) { continue; } else if (plen < sizeof tag) { tag[plen++] = *p; continue; } /* FALLTHROUGH */ case 2: /* before value */ if (isascii(*p) && isspace(*p)) continue; memset(value, '\0', sizeof value); plen = 0; state = 3; /* FALLTHROUGH */ case 3: /* in value */ if (*p == ';') { state = 4; /* FALLTHROUGH */ } else if (isascii(*p) && isspace(*p)) { continue; } else if (plen < sizeof tag) { value[plen++] = *p; continue; } else { continue; } /* FALLTHROUGH */ case 4: /* after value */ if (strcasecmp(tag, "t") == 0) { if (value[0] == 'y') dk->dk_testing = TRUE; } else if (strcasecmp(tag, "o") == 0) { if (value[0] == '-') dk->dk_signall = TRUE; } else if (strcasecmp(tag, "n") == 0) { /* XXX -- other info? or something */ /* XXX -- yahoo-inc.com uses it, so tolerate */ /* XXX -- (i.e. ignore) it for now */ } else if (strcasecmp(tag, "r") == 0) { sm_strlcpy(dk->dk_reportaddr, value, sizeof dk->dk_reportaddr); } else { return -1; } state = 0; break; } if (*p == '\0') break; } return 1; } /* ** DK_GET_KEY -- acquire the key that will be used to either generate ** or verify a signature ** ** Parameters: ** dk -- DK handle ** ** Return value: ** A DK_STAT. */ static DK_STAT dk_get_key(DK *dk) { bool key = FALSE; int status; int state; int qdcount; int ancount; #if USE_ARLIB int error; #endif /* USE_ARLIB */ int n = 0; int l; int type = -1; int class = -1; int plen = 0; size_t anslen; size_t keyidx; #if USE_ARLIB AR_QUERY q; #endif /* USE_ARLIB */ unsigned char *p; unsigned char *cp; unsigned char *eom; char *ssel; char qname[MAXHOSTNAMELEN + 1]; unsigned char ansbuf[MAXPACKET]; char tag[MAXPARAMETER + 1]; char value[MAXPARAMETER + 1]; #if USE_ARLIB struct timeval timeout; #endif /* USE_ARLIB */ HEADER hdr; assert(dk != NULL); if (dk->dk_mode != DK_MODE_VERIFY) { FILE *f; size_t n; struct stat s; char fn[MAXPATHLEN + 1]; /* already got one? */ if (dk->dk_key != NULL) return DK_STAT_OK; ssel = dk_sterilize(dk->dk_selector); if (ssel == NULL) { dk_error(dk, "unsafe selector data"); return DK_STAT_SYNTAX; } memset(fn, '\0', sizeof fn); snprintf(fn, sizeof fn - 1, DK_PRIVATEKEY, dk->dk_selector == NULL ? DK_DEFAULT_SELECTOR : ssel); f = fopen(fn, "r"); if (f == NULL) { dk_error(dk, "%s: fopen(): %s", fn, strerror(errno)); return DK_STAT_INTERNAL; } if (fstat(fileno(f), &s) != 0) { dk_error(dk, "%s: fstat(): %s", fn, strerror(errno)); return DK_STAT_INTERNAL; } dk->dk_key = dk_malloc(dk->dk_libhandle, dk->dk_closure, (size_t) s.st_size); if (dk->dk_key == NULL) { dk_error(dk, "unable to allocate %d byte(s)", s.st_size); return DK_STAT_NORESOURCE; } dk->dk_keylen = s.st_size; n = fread(dk->dk_key, (unsigned int) s.st_size, 1, f); if (n != 1) { fclose(f); dk_error(dk, "%s: fread(): %s", fn, strerror(errno)); return DK_STAT_INTERNAL; } fclose(f); return DK_STAT_OK; } snprintf(qname, sizeof qname - 1, "%s.%s.%s", dk->dk_selector, DK_DNSNAME, dk->dk_domain); #if USE_ARLIB timeout.tv_sec = dk->dk_timeout; timeout.tv_usec = 0; if (DK_DEBUG('r')) { syslog(LOG_INFO, "thread %p ar_addquery(`%s')", pthread_self(), qname); } q = ar_addquery(dk->dk_libhandle->dkl_arlib, qname, C_IN, T_TXT, MAXCNAMEDEPTH, ansbuf, sizeof ansbuf, &error, dk->dk_timeout == 0 ? NULL : &timeout); if (q == NULL) return DK_STAT_INTERNAL; status = ar_waitreply(dk->dk_libhandle->dkl_arlib, q, NULL, NULL); (void) ar_cancelquery(dk->dk_libhandle->dkl_arlib, q); if (DK_DEBUG('r')) { syslog(LOG_INFO, "thread %p ar_cancelquery(`%s')", pthread_self(), qname); } #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) { if (DK_DEBUG('d') && dk->dk_id != NULL) { syslog(LOG_NOTICE, "%s: ar_waitreply(): %s", dk->dk_id, ar_strerror(error)); } dk_error(dk, "DNS error or timeout for `%s'", qname); return DK_STAT_CANTVRFY; } #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: dk_error(dk, "`%s' not found", qname); return DK_STAT_NOKEY; case TRY_AGAIN: case NO_RECOVERY: default: dk_error(dk, "DNS error or timeout for `%s'", qname); return DK_STAT_CANTVRFY; } } #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) { dk_error(dk, "key record corrupted"); return DK_STAT_INTERNAL; } cp += n; /* extract the type and class */ if (cp + INT16SZ + INT16SZ > eom) return DK_STAT_INTERNAL; GETSHORT(type, cp); GETSHORT(class, cp); } if (type != T_TXT || class != C_IN) { dk_error(dk, "unexpected key reply"); return DK_STAT_INTERNAL; } /* if NXDOMAIN, return DK_STAT_NOKEY */ if (hdr.rcode == NXDOMAIN) { dk_error(dk, "`%s' not found", qname); return DK_STAT_NOKEY; } /* get the answer count */ ancount = ntohs((unsigned short) hdr.ancount); if (ancount == 0) { dk_error(dk, "DNS error or timeout for `%s'", qname); return DK_STAT_CANTVRFY; } /* if truncated, we can't do it */ if (hdr.tc) { dk_error(dk, "DNS reply for `%s' truncated", qname); return DK_STAT_INTERNAL; } /* ** Extract the data from the first answer. */ /* 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) { dk_error(dk, "key record corrupted"); return DK_STAT_INTERNAL; } /* ...and move past it */ cp += n; /* extract the type and class */ if (cp + INT16SZ + INT16SZ > eom) { dk_error(dk, "key record corrupted"); return DK_STAT_INTERNAL; } GETSHORT(type, cp); GETSHORT(class, cp); /* skip the TTL */ cp += INT32SZ; /* get payload length */ if (cp + INT16SZ > eom) { dk_error(dk, "key record corrupted"); return DK_STAT_INTERNAL; } GETSHORT(n, cp); /* XXX -- maybe deal with a partial reply rather than require it all */ if (cp + n > eom) { dk_error(dk, "key record corrupted"); return DK_STAT_INTERNAL; } dk->dk_b64 = dk_malloc(dk->dk_libhandle, dk->dk_closure, n); if (dk->dk_b64 == NULL) { dk_error(dk, "unable to allocate %d byte(s)", n); return DK_STAT_NORESOURCE; } dk->dk_b64len = n; /* decode the tags, allocate memory, and copy */ state = 0; keyidx = 0; for (p = cp, l = 0; p <= eom && n >= 0; p++, n--, l--) { if (p == eom || n == 0) { if (state == 3) { state = 4; } else if (state == 1 || state == 2) { dk_error(dk, "syntax error in key record at `%s'", qname); return DK_STAT_CANTVRFY; } else { break; } } /* if appropriate, consume a length byte */ if (p < eom && n > 0 && l == 0) { l = *p + 1; continue; } switch (state) { case 0: /* before tag */ if (isascii(*p) && isspace(*p)) continue; memset(tag, '\0', sizeof tag); plen = 0; state = 1; /* FALLTHROUGH */ case 1: /* in tag */ if (*p == '=') { state = 2; continue; } else if (isascii(*p) && isspace(*p)) { continue; } else if (plen < sizeof tag) { tag[plen++] = *p; continue; } /* FALLTHROUGH */ case 2: /* before value */ if (isascii(*p) && isspace(*p)) continue; memset(value, '\0', sizeof value); plen = 0; state = 3; key = (strcasecmp(tag, "p") == 0); /* FALLTHROUGH */ case 3: /* in value */ if (*p == ';') { state = 4; /* FALLTHROUGH */ } else if (isascii(*p) && isspace(*p)) { continue; } else if (key && keyidx < dk->dk_b64len) { dk->dk_b64[keyidx] = *p; keyidx++; continue; } else if (!key && plen < sizeof value) { value[plen++] = *p; continue; } else { continue; } /* FALLTHROUGH */ case 4: /* after value */ if (strcasecmp(tag, "g") == 0) { dk->dk_gran = dk_strdup(dk->dk_libhandle, dk->dk_closure, value); } else if (strcasecmp(tag, "k") == 0) { /* key type */ if (strcasecmp(value, "rsa") != 0) { dk_error(dk, "unknown key type `%s' at `%s'", value, qname); return DK_STAT_CANTVRFY; } } else if (strcasecmp(tag, "t") == 0) { if (value[0] == 'y') dk->dk_testing = TRUE; } else if (strcasecmp(tag, "p") == 0) { /* signature; handled earlier */ dk->dk_b64len = keyidx; /* empty == revoked */ if (value[0] == '\0') dk->dk_revoked = TRUE; } else if (strcasecmp(tag, "n") == 0) { /* XXX -- comment */ } else { /* XXX -- unknown; ignore */ } state = 0; break; } } /* XXX -- verify valid and required values here instead of dk_eoh()? */ /* base64-decode the key into dk_key */ if (!dk_decode(dk, DK_DECODE_KEY)) return DK_STAT_NORESOURCE; dk_mfree(dk->dk_libhandle, dk->dk_closure, dk->dk_b64); dk->dk_b64 = NULL; return DK_STAT_OK; } /* ** DK_PARAM_LOOKUP -- lookup the parameter's code ** ** Parameters: ** str -- string to evaluate ** ** Return value: ** Mnemonic code, or -1 on error. */ static int dk_param_lookup(str) char *str; { int c; assert(str != NULL); for (c = 0; params[c].str != NULL; c++) { if (strcasecmp(str, params[c].str) == 0) return params[c].code; } return -1; } /* ** DK_PROCESS_VALUE -- process the value that was found ** ** Parameters: ** dk -- DomainKeys handle ** pcode -- parameter whose value is being provided ** value -- the value ** ** Return value: ** 1 -- valid ** 0 -- invalid ** -1 -- error (malloc() failure in dk_decode()) */ static int dk_process_value(DK *dk, int pcode, char *value) { char *p; char *ctx; assert(dk != NULL); assert(value != NULL); switch (pcode) { case DK_PARAM_SIGNALG: if (value[0] == '\0') { dk->dk_signalg = DK_SIGN_DEFAULT; return 1; } else if (strcasecmp(value, "rsa-sha1") == 0) { dk->dk_signalg = DK_SIGN_RSASHA1; return 1; } return 0; case DK_PARAM_CANONALG: if (value[0] == '\0') { dk->dk_canonalg = DK_CANON_DEFAULT; return 1; } else if (strcasecmp(value, "simple") == 0) { dk->dk_canonalg = DK_CANON_SIMPLE; return 1; } else if (strcasecmp(value, "nofws") == 0) { dk->dk_canonalg = DK_CANON_NOFWS; return 1; } return 0; case DK_PARAM_QUERYMETHOD: if (value[0] == '\0') { /* we'll be generous and allow a default here */ dk->dk_querymethod = DK_QUERY_DEFAULT; return 1; } else if (strcasecmp(value, "dns") == 0) { dk->dk_querymethod = DK_QUERY_DNS; return 1; } return 0; case DK_PARAM_DOMAIN: if (strlen(value) > 0) { dk->dk_domain = dk_strdup(dk->dk_libhandle, dk->dk_closure, value); return 1; } else { /* domain has no default */ return 0; } case DK_PARAM_SELECTOR: if (strlen(value) > 0) { dk->dk_selector = dk_strdup(dk->dk_libhandle, dk->dk_closure, value); return 1; } else { /* selector has no default */ return 0; } case DK_PARAM_SIGNATURE: /* base64-decode the signature into dk_signature */ dk->dk_b64len = strlen(dk->dk_b64); if (!dk_decode(dk, DK_DECODE_SIGNATURE)) return -1; dk_mfree(dk->dk_libhandle, dk->dk_closure, dk->dk_b64); dk->dk_b64 = NULL; return 1; case DK_PARAM_HDRLIST: dk->dk_shdrlist = strdup(value); if (dk->dk_shdrlist == NULL) return -1; for (p = strtok_r(dk->dk_shdrlist, ":", &ctx); p != NULL && dk->dk_hdrlidx < MAXHDRCNT; p = strtok_r(NULL, ":", &ctx)) { while (isascii(*p) && isspace(*p)) p++; dk->dk_hdrlist[dk->dk_hdrlidx] = p; dk->dk_hdrlidx++; } return 1; default: return 0; } } /* ** DK_PROCESS_DKHEADER -- process a DomainKeys: header ** ** Parameters: ** dk -- DomainKeys handle ** hdr -- header contents ** ** Return value: ** -1 -- system error; check errno ** 0 -- success ** 1 -- parse error */ static int dk_process_dkheader(DK *dk, const char *hdr) { bool comment = FALSE; bool escaped = FALSE; bool quoting = FALSE; int state = 0; int status; int plen = 0; int pcode = 0; size_t hlen; const char *p; char param[MAXPARAMETER + 1]; char value[MAXPARAMETER + 1]; assert(dk != NULL); assert(hdr != NULL); hlen = strlen(hdr); for (p = hdr; *p != '\0'; p++) { if (!escaped) { if (*p == '\\') { escaped = TRUE; continue; } if (*p == '(') { comment = TRUE; continue; } else if (*p == ')') { comment = FALSE; continue; } if (*p == '"') quoting = !quoting; } if (comment) continue; if (escaped) escaped = FALSE; switch (state) { case 0: /* before parameter */ if (isascii(*p) && isspace(*p)) continue; state = 1; memset(param, '\0', sizeof param); plen = 0; /* FALLTHROUGH */ case 1: /* in parameter */ if (*p == '=') { state = 3; continue; } else if (!quoting && isascii(*p) && isspace(*p)) { state = 2; } else if (plen < MAXPARAMETER) { param[plen] = *p; plen++; continue; } /* FALLTHROUGH */ case 2: /* after parameter */ if (isascii(*p) && isspace(*p)) continue; else if (*p == '=') state = 3; else return 1; break; case 3: /* before value */ pcode = dk_param_lookup(param); if (pcode == -1) return 1; if (isascii(*p) && isspace(*p)) continue; state = 4; memset(value, '\0', sizeof value); plen = 0; if (pcode == DK_PARAM_SIGNATURE) { dk->dk_b64 = dk_malloc(dk->dk_libhandle, dk->dk_closure, hlen); if (dk->dk_b64 == NULL) return -1; memset(dk->dk_b64, '\0', hlen); } /* FALLTHROUGH */ case 4: /* in value */ if (!quoting && !escaped && *p == ';') { status = dk_process_value(dk, pcode, value); if (status == 0) return 1; else if (status == -1) return -1; if (*p == ';') state = 0; else state = 5; continue; } switch (pcode) { case DK_PARAM_SIGNALG: case DK_PARAM_DOMAIN: case DK_PARAM_CANONALG: case DK_PARAM_QUERYMETHOD: case DK_PARAM_SELECTOR: case DK_PARAM_HDRLIST: if (plen < MAXPARAMETER && (quoting || escaped || !(isascii(*p) && isspace(*p)))) { value[plen] = *p; plen++; } break; case DK_PARAM_SIGNATURE: dk->dk_b64[plen] = *p; plen++; break; default: return 1; } break; case 5: /* after value */ if (isascii(*p) && isspace(*p)) continue; switch (pcode) { case DK_PARAM_SIGNATURE: dk->dk_b64len = plen; break; default: break; } status = dk_process_value(dk, pcode, value); if (status == 0) return 1; else if (status == -1) return -1; if (*p == ';') state = 0; else return 1; break; default: /* shouldn't happen */ assert(0); } } if (state == 4) { status = dk_process_value(dk, pcode, value); if (status == 0) return 1; else if (status == -1) return -1; } /* XXX -- verify valid and required values here instead of dk_eoh()? */ return 0; } /* ** DK_HDRSIGNED -- see if this header is in the list of signed headers ** ** Parameters: ** dk -- DomainKeys handle ** hdr -- header being considered ** mark -- mark this header ("found") ** ** Return value: ** TRUE iff the header under evaluation was included in the signature */ static bool dk_hdrsigned(DK *dk, u_char *hdr, bool mark) { unsigned int c; unsigned char *colon; assert(dk != NULL); assert(hdr != NULL); colon = strchr(hdr, ':'); assert(colon != NULL); /* if verifying and no header list given, all headers were signed */ if (dk->dk_mode == DK_MODE_VERIFY && dk->dk_hdrlidx == 0) return TRUE; for (c = 0; c < dk->dk_hdrlidx; c++) { if (strncasecmp(dk->dk_hdrlist[c], hdr, colon - hdr) == 0 && strlen(dk->dk_hdrlist[c]) == colon - hdr) { if (mark) dk->dk_hdrmark[c] = TRUE; return TRUE; } } return FALSE; } /* ** DK_NEW -- allocate a new DomainKeys handle */ static DK * dk_new(DK_LIB *libhandle, const char *id, void *memclosure, dk_canon_t canon_alg, dk_alg_t sign_alg, DK_STAT *statp) { DK *new; /* allocate the handle */ new = dk_malloc(libhandle, memclosure, sizeof(struct dk)); if (new == NULL) { *statp = DK_STAT_NORESOURCE; return NULL; } /* populate defaults */ memset(new, '\0', sizeof(struct dk)); new->dk_id = id; new->dk_signalg = (sign_alg == -1 ? DK_SIGN_DEFAULT : sign_alg); new->dk_canonalg = (canon_alg == -1 ? DK_CANON_DEFAULT : canon_alg); new->dk_querymethod = DK_QUERY_DEFAULT; new->dk_crlf = DK_CRLF_UNKNOWN; new->dk_mode = DK_MODE_UNKNOWN; new->dk_state = DK_STATE_INIT; new->dk_closure = memclosure; new->dk_libhandle = libhandle; new->dk_tmpdir = libhandle->dkl_tmpdir; new->dk_timeout = DEFTIMEOUT; if ((libhandle->dkl_options & DK_OPTS_HDRLIST) != 0) new->dk_flags |= DK_FLAG_HDRLIST; *statp = DK_STAT_OK; return new; } /* ** DK_CANON -- canonicalize some data ** ** Parameters: ** dk -- DK handle ** buf -- buffer containing data ** buflen -- number of bytes to consume ** ** Return value: ** None. */ static void dk_canon(DK *dk, unsigned char *buf, size_t buflen) { struct dk_sha1 *sha1; assert(dk != NULL); sha1 = (struct dk_sha1 *) dk->dk_signinfo; if (buf == NULL || buflen == 0) return; if (dk->dk_writesep) { if (sha1->sha1_tmpfd != -1) BIO_write(sha1->sha1_tmpbio, CRLF, 2); SHA1_Update(&sha1->sha1_sha1, CRLF, 2); dk->dk_writesep = FALSE; } if (sha1->sha1_tmpfd != -1) BIO_write(sha1->sha1_tmpbio, buf, buflen); SHA1_Update(&sha1->sha1_sha1, buf, buflen); } /* ** DK_VERROR -- log an error into a DK handle (varargs version) ** ** Parameters: ** dk -- DK context in which this is performed ** format -- format to apply ** va -- argument list ** ** Return value: ** None. */ static void dk_verror(DK *dk, const char *format, va_list va) { int flen; int saverr; char *new; assert(dk != NULL); assert(format != NULL); saverr = errno; if (dk->dk_error == NULL) { dk->dk_error = dk_malloc(dk->dk_libhandle, dk->dk_closure, DEFERRLEN); if (dk->dk_error == NULL) { errno = saverr; return; } dk->dk_errlen = DEFERRLEN; } for (;;) { flen = vsnprintf(dk->dk_error, dk->dk_errlen, format, va); /* compensate for broken vsnprintf() implementations */ if (flen == -1) flen = dk->dk_errlen * 2; if (flen >= dk->dk_errlen) { new = dk_malloc(dk->dk_libhandle, dk->dk_closure, flen + 1); if (new == NULL) { errno = saverr; return; } dk_mfree(dk->dk_libhandle, dk->dk_closure, dk->dk_error); dk->dk_error = new; dk->dk_errlen = flen + 1; } else { break; } } errno = saverr; } /* ** DK_ERROR -- log an error into a DK handle ** ** Parameters: ** dk -- DK context in which this is performed ** format -- format to apply ** ... -- arguments ** ** Return value: ** None. */ static void dk_error(DK *dk, const char *format, ...) { va_list va; assert(dk != NULL); assert(format != NULL); va_start(va, format); dk_verror(dk, format, va); va_end(va); } /* ** ===== PUBLIC SECTION */ /* ** DK_INIT -- initialize the DomainKeys system */ DK_LIB * dk_init(void *(*caller_mallocf)(void *closure, size_t nbytes), void (*caller_freef)(void *closure, void *p)) { DK_LIB *libhandle; /* initialize the resolver */ (void) res_init(); /* initialize OpenSSL algorithms */ #if 0 OpenSSL_add_all_algorithms(); #endif /* 0 */ /* copy the parameters */ libhandle = (DK_LIB *) malloc(sizeof(struct dk_lib)); if (libhandle == NULL) return NULL; libhandle->dkl_malloc = caller_mallocf; libhandle->dkl_free = caller_freef; libhandle->dkl_options = DK_OPTS_DEFAULT; libhandle->dkl_tmpdir = getenv("DK_TMPDIR"); if (libhandle->dkl_tmpdir == NULL || libhandle->dkl_tmpdir[0] == '\0') libhandle->dkl_tmpdir = DK_TMPDIR; #if USE_ARLIB libhandle->dkl_arlib = ar_init(NULL, NULL, NULL, 0); if (libhandle->dkl_arlib == NULL) { free(libhandle); return NULL; } #else /* USE_ARLIB */ (void) res_init(); #endif /* USE_ARLIB */ return libhandle; } /* ** DK_SIGN -- allocate a handle for use in a signature operation */ DK * dk_sign(DK_LIB *dklib, const char *id, void *memclosure, const dk_sigkey_t *secretkey, dk_canon_t canonalg, dk_alg_t signalg, DK_STAT *statp) { DK *new; assert(dklib != NULL); assert(canonalg == DK_CANON_SIMPLE || canonalg == DK_CANON_NOFWS); assert(signalg == DK_SIGN_RSASHA1); new = dk_new(dklib, id, memclosure, canonalg, signalg, statp); if (new != NULL) { new->dk_mode = DK_MODE_SIGN; if (secretkey != NULL) { new->dk_keylen = strlen((const char *) secretkey); new->dk_key = (unsigned char *) dk_malloc(dklib, memclosure, new->dk_keylen + 1); if (new->dk_key == NULL) { dk_free(new); return NULL; } memcpy(new->dk_key, secretkey, new->dk_keylen + 1); } } return new; } /* ** DK_VERIFY -- allocate a handle for use in a verification operation */ DK * dk_verify(DK_LIB *dklib, const char *id, void *memclosure, DK_STAT *statp) { DK *new; assert(dklib != NULL); new = dk_new(dklib, id, memclosure, DK_CANON_UNKNOWN, DK_SIGN_UNKNOWN, statp); if (new != NULL) new->dk_mode = DK_MODE_VERIFY; return new; } /* ** DK_GETERROR -- return any stored error string from within the DKIM ** context handle */ const char * dk_geterror(DK *dk) { assert(dk != NULL); return dk->dk_error; } /* ** DK_HEADER -- process a header */ DK_STAT dk_header(DK *dk, u_char *hdr, size_t len) { assert(dk != NULL); assert(hdr != NULL); assert(len > 0); /* verify state */ if (dk->dk_state > DK_STATE_HEADER) return DK_STAT_INTERNAL; dk->dk_state = DK_STATE_HEADER; /* ** If we're signing, or we found the DomainKeys: header before, ** this is a header of interest. Canonicalize and process. */ if (dk->dk_mode != DK_MODE_VERIFY || dk->dk_processing) { bool prevcr; bool skipit = FALSE; unsigned char *p; unsigned char *pend; char *q; char *qend; pend = hdr + len; qend = &dk->dk_hdrbuf[sizeof(dk->dk_hdrbuf)]; prevcr = FALSE; if (dk->dk_mode == DK_MODE_VERIFY) { /* is this header signed? */ if (!dk_hdrsigned(dk, hdr, TRUE)) skipit = TRUE; } else { /* XXX -- omit Received: ? */ if (!dk_hdrsigned(dk, hdr, FALSE) && dk->dk_hdrlidx < MAXHDRCNT) { dk->dk_hdrlist[dk->dk_hdrlidx] = &dk->dk_hdrbuf[dk->dk_hdrlen]; dk->dk_hdrlidx++; } } if (dk->dk_hdrsidx < MAXHDRCNT) { dk->dk_hdrset[dk->dk_hdrsidx] = &dk->dk_hdrbuf[dk->dk_hdrlen]; dk->dk_hdrsidx++; } for (p = hdr, q = &dk->dk_hdrbuf[dk->dk_hdrlen]; !skipit && p < pend && q < qend; p++) { if (*p == '\n' && dk->dk_canonalg != DK_CANON_NOFWS) { if (prevcr) { prevcr = FALSE; *q = '\n'; } else { *q = '\r'; if (q < qend) { q++; *q = '\n'; } } } else if (dk->dk_canonalg == DK_CANON_NOFWS) { if (*p == 0x20 || *p == 0x09 || *p == 0x0a || *p == 0x0D) continue; *q = *p; } else { *q = *p; } if (*p == '\r') prevcr = TRUE; q++; } if (!skipit && dk->dk_canonalg == DK_CANON_NOFWS) { if (q < qend) { *q = '\r'; q++; } if (q < qend) { *q = '\n'; q++; } } *q = '\0'; q++; dk->dk_hdrlen = q - dk->dk_hdrbuf; } /* ** Store the From: or Sender: value regardless of its position ** in case no DomainKey-Signature: header is found. */ if ((strncasecmp(hdr, "from:", 5) == 0 && dk->dk_uhdrn[0] == '\0') || strncasecmp(hdr, "sender:", 7) == 0) { int status; char *user; char *domain; u_char *colon; colon = (u_char *) strchr(hdr, ':'); status = rfc2822_mailbox_split(colon + 1, &user, &domain); if (status != 0 || user == NULL || domain == NULL) { dk_error(dk, "unable to parse sender `%s'", colon + 1); return DK_STAT_SYNTAX; } sm_strlcpy(dk->dk_uhdrn, hdr, MIN(sizeof dk->dk_uhdrn, (u_int) (colon - hdr) + 1)); dk_lowercase(dk->dk_uhdrn); snprintf(dk->dk_uhdrv, sizeof dk->dk_uhdrv, "%s@%s", user, domain); } /* ** If this is a DomainKey-Signature: header, cache it and note that ** the rest is of interest. */ if (strncasecmp(hdr, DK_SIGNHEADER, strlen(DK_SIGNHEADER)) == 0 && hdr[strlen(DK_SIGNHEADER)] == ':') { /* first DomainKey-Signature: header? */ if (!dk->dk_processing) { u_char *hval; hval = hdr + strlen(DK_SIGNHEADER) + 1; if (dk_process_dkheader(dk, hval) != 0) { dk_error(dk, "syntax error in signature"); return DK_STAT_SYNTAX; } dk->dk_processing = TRUE; } /* XXX -- OPTIMIZATION ** For signed messages, at this point we know under which ** domain and selector the message was signed. We could ** start the appropriate TXT query here. */ } return DK_STAT_OK; } /* ** DK_EOH -- note end of headers */ DK_STAT dk_eoh(DK *dk) { int saveerr; unsigned int c; unsigned int d; size_t hlen; char *colon; struct dk_sha1 *sha1; char *sender; char *from; char *hdr; unsigned char *p; assert(dk != NULL); /* verify state */ if (dk->dk_state > DK_STATE_EOH) return DK_STAT_INTERNAL; dk->dk_state = DK_STATE_EOH; sender = NULL; from = NULL; for (c = 0; c < dk->dk_hdrsidx; c++) { if (strncasecmp(dk->dk_hdrset[c], "from:", 5) == 0) { if (from != NULL) { dk_error(dk, "multiple From: headers found"); return DK_STAT_SYNTAX; } from = dk->dk_hdrset[c]; } if (strncasecmp(dk->dk_hdrset[c], "sender:", 7) == 0) { if (sender != NULL) { dk_error(dk, "multiple Sender: headers found"); return DK_STAT_SYNTAX; } sender = dk->dk_hdrset[c]; } } hdr = NULL; if (sender == NULL) hdr = from; else hdr = sender; /* if verifying and no from/sender header was found, short-circuit */ if (hdr == NULL && dk->dk_mode == DK_MODE_VERIFY) { char *at; at = strchr(dk->dk_uhdrv, '@'); if (at == NULL || *(at + 1) == '\0') { dk_error(dk, "missing or empty domain name in sender"); return DK_STAT_SYNTAX; } /* clean up if dk->dk_domain already allocated */ if (dk->dk_domain == NULL) { dk_mfree(dk->dk_libhandle, dk->dk_closure, dk->dk_domain); } dk->dk_domain = dk_malloc(dk->dk_libhandle, dk->dk_closure, strlen(at + 1) + 1); if (dk->dk_domain == NULL) { dk_error(dk, "unable to allocate %d byte(s)", strlen(at + 1) + 1); return DK_STAT_NORESOURCE; } sm_strlcpy(dk->dk_domain, at + 1, strlen(at + 1) + 1); dk->dk_skipbody = TRUE; (void) dk_in_use(dk); if (dk->dk_signall || DK_DEBUG('s')) return DK_STAT_NOSIG; else return DK_STAT_OK; } /* if we found a signature but the sender doesn't match it, say so */ if (hdr != NULL) { int status; size_t len; char *user; char *domain; colon = strchr(hdr, ':'); len = strlen(colon + 1) + 1; dk->dk_sender = dk_malloc(dk->dk_libhandle, dk->dk_closure, len); if (dk->dk_sender == NULL) return DK_STAT_INTERNAL; sm_strlcpy(dk->dk_frombuf, colon + 1, sizeof dk->dk_frombuf); status = rfc2822_mailbox_split(dk->dk_frombuf, &user, &domain); if (status != 0 || user == NULL || domain == NULL) { dk_error(dk, "unable to parse sender `%s'", colon + 1); return DK_STAT_SYNTAX; } snprintf(dk->dk_sender, len, "%s@%s", user, domain); /* verify domain/subdomain match */ if (dk->dk_mode == DK_MODE_VERIFY) { bool ok = FALSE; /* incorrect DomainKey-Signature: header? */ if (dk->dk_domain == NULL) { dk_error(dk, "can't determine signing domain"); return DK_STAT_CANTVRFY; } if (strcasecmp(domain, dk->dk_domain) != 0) { for (p = strchr(domain, '.'); p != NULL; p = strchr(p + 1, '.')) { if (strcasecmp(p + 1, dk->dk_domain) == 0) { ok = TRUE; break; } } } else { ok = TRUE; } if (!ok) { dk_error(dk, "signing domain and sending domain mismatch"); return DK_STAT_CANTVRFY; } } dk->dk_user = dk_strdup(dk->dk_libhandle, dk->dk_closure, user); if (dk->dk_domain == NULL) { dk->dk_domain = dk_strdup(dk->dk_libhandle, dk->dk_closure, domain); } } else { dk_error(dk, "no sender header found"); return DK_STAT_SYNTAX; } /* initialize data hash */ sha1 = dk_malloc(dk->dk_libhandle, dk->dk_closure, sizeof(struct dk_sha1)); if (sha1 == NULL) { dk_error(dk, "unable to allocate %d byte(s)", sizeof(struct dk_sha1)); return DK_STAT_NORESOURCE; } memset(sha1, '\0', sizeof(struct dk_sha1)); sha1->sha1_tmpfd = -1; dk->dk_signinfo = (void *) sha1; /* verify granularity */ if (dk->dk_gran != NULL && dk->dk_gran[0] != '\0') { if (strcmp(dk->dk_user, dk->dk_gran) != 0) { dk_error(dk, "key granularity mismatch"); return DK_STAT_CANTVRFY; } } SHA1_Init(&sha1->sha1_sha1); if (DK_DEBUG('c') || (dk->dk_libhandle->dkl_options & DK_OPTS_TMPFILES) != 0) { /* cache the stored stuff for later processing */ snprintf(sha1->sha1_tmppath, sizeof sha1->sha1_tmppath - 1, "%s/dk.%d.XXXXXX", dk->dk_tmpdir, getpid()); sha1->sha1_tmpfd = mkstemp(sha1->sha1_tmppath); saveerr = errno; if (sha1->sha1_tmpfd == -1) { /* looks like this is a bug on OpenBSD */ (void) unlink(sha1->sha1_tmppath); errno = saveerr; dk_error(dk, "unable to create temporary file at %s", sha1->sha1_tmppath); return DK_STAT_NORESOURCE; } sha1->sha1_tmpbio = BIO_new_fd(sha1->sha1_tmpfd, 1); if (!DK_DEBUG('c')) (void) unlink(sha1->sha1_tmppath); } /* dk_hdrlist -- set of headers to include */ /* dk_hdrset -- set of headers on input */ /* ** I would normally be embarrassed to use an O(N^2) ** algorithm, but this is a small data set. */ if (dk->dk_hdrlidx > 0 && (dk->dk_flags & DK_FLAG_HDRLIST) != 0) { /* reset sender/from buffer to use what was actually signed */ memset(dk->dk_uhdrn, '\0', sizeof dk->dk_uhdrn); memset(dk->dk_uhdrv, '\0', sizeof dk->dk_uhdrv); for (c = 0; c < dk->dk_hdrlidx; c++) { colon = strchr(dk->dk_hdrlist[c], ':'); if (colon == NULL) hlen = strlen(dk->dk_hdrlist[c]); else hlen = colon - dk->dk_hdrlist[c]; for (d = 0; d < dk->dk_hdrsidx; d++) { if (strncasecmp(dk->dk_hdrset[d], dk->dk_hdrlist[c], hlen) == 0 && dk->dk_hdrset[d][hlen] == ':') { dk_canon(dk, dk->dk_hdrset[d], strlen(dk->dk_hdrset[d])); colon = strchr(dk->dk_hdrset[d], ':'); /* extract correct sender/from data */ if ((strncasecmp(dk->dk_hdrlist[c], "from", hlen) == 0 && dk->dk_uhdrn[0] == '\0') || strncasecmp(dk->dk_hdrlist[c], "sender", hlen) == 0) { strncpy(dk->dk_uhdrn, dk->dk_hdrlist[c], hlen); if (colon != NULL) { sm_strlcpy(dk->dk_uhdrv, colon + 1, sizeof dk->dk_uhdrv); } } } } } if (dk->dk_uhdrv[0] != '\0') { int status; char *user; char *domain; u_char *at; char addr[MAXADDRESS + 1]; sm_strlcpy(addr, dk->dk_uhdrv, sizeof addr); status = rfc2822_mailbox_split(addr, &user, &domain); if (status != 0 || user == NULL || domain == NULL) { dk_error(dk, "unable to parse sender `%s'", addr); return DK_STAT_SYNTAX; } snprintf(dk->dk_uhdrv, sizeof dk->dk_uhdrv, "%s@%s", user, domain); at = strchr(dk->dk_uhdrv, '@'); dk_lowercase(at + 1); } } else { unsigned int c; for (c = 0; c < dk->dk_hdrsidx; c++) { dk_canon(dk, dk->dk_hdrset[c], strlen(dk->dk_hdrset[c])); } } /* ** The next call to dk_canon() that actually writes something ** should write an extra CRLF to separate the headers from the ** body. */ dk->dk_writesep = TRUE; /* ** If no DomainKeys: header was found, determine whether or not ** there should be one, and return something appropriate. */ if (!dk->dk_processing) { int status; if (dk->dk_domain == NULL) { char *at; at = strchr(dk->dk_sender, '@'); if (at == NULL || *(at + 1) == '\0') { dk_error(dk, "can't find sender domain in `%s'", dk->dk_sender); return DK_STAT_SYNTAX; } dk->dk_domain = dk_malloc(dk->dk_libhandle, dk->dk_closure, strlen(at + 1) + 1); if (dk->dk_domain == NULL) { dk_error(dk, "unable to allocate %d byte(s)", strlen(at + 1) + 1); return DK_STAT_NORESOURCE; } sm_strlcpy(dk->dk_domain, at + 1, strlen(at + 1) + 1); } if (dk->dk_selector == NULL && dk->dk_mode == DK_MODE_VERIFY) { (void) dk_in_use(dk); if (dk->dk_signall || DK_DEBUG('s')) return DK_STAT_NOSIG; else return DK_STAT_OK; } status = dk_get_key(dk); if (status == DK_STAT_INTERNAL) { return status; } else if (status != DK_STAT_OK) { if (dk->dk_mode == DK_MODE_VERIFY) { if (dk->dk_key != NULL || dk->dk_signall || DK_DEBUG('s')) return DK_STAT_NOSIG; else return DK_STAT_NOKEY; } else { if (dk->dk_key == NULL) return DK_STAT_NOKEY; } } } /* ** If some of the supposedly-signed headers were absent, ** return an error. */ if (dk->dk_processing) { unsigned int c; for (c = 0; c < dk->dk_hdrlidx; c++) { if (!dk->dk_hdrmark[c]) return DK_STAT_BADSIG; } } /* ** Return DK_STAT_REVOKED if the key was revoked. */ if (dk->dk_revoked) return DK_STAT_REVOKED; /* ** Success! */ return DK_STAT_OK; } /* ** DK_BODY -- process a body chunk */ DK_STAT dk_body(DK *dk, u_char *buf, size_t len) { size_t lidx; size_t wlen; u_char *p; u_char *eob; u_char *wrote; unsigned char lbuf[BUFRSZ]; assert(dk != NULL); assert(buf != NULL); assert(len > 0); /* verify state */ if (dk->dk_state > DK_STATE_BODY) return DK_STAT_INTERNAL; dk->dk_state = DK_STATE_BODY; if (dk->dk_skipbody) return DK_STAT_OK; eob = buf + len - 1; wrote = buf; wlen = 0; lidx = 0; /* run it through the canonicalizing stuff */ switch (dk->dk_canonalg) { case DK_CANON_SIMPLE: for (p = buf; p <= eob; p++) { if (*p == '\n') { int c; /* adaptive */ if (dk->dk_crlf == DK_CRLF_UNKNOWN) { if (dk->dk_lastchar == '\r') dk->dk_crlf = DK_CRLF_CRLF; else dk->dk_crlf = DK_CRLF_LF; } if (dk->dk_crlf == DK_CRLF_CRLF && dk->dk_lastchar == '\r') { if (wlen == 1) { dk->dk_blanks++; wrote = p + 1; wlen = 0; } else { for (c = 0; c < dk->dk_blanks; c++) dk_canon(dk, CRLF, 2); dk->dk_blanks = 0; dk_canon(dk, wrote, wlen + 1); wlen = 0; wrote = p + 1; } dk->dk_lastchar = *p; continue; } else if (dk->dk_crlf == DK_CRLF_LF) { if (wlen == 0) { dk->dk_blanks++; wrote = p + 1; } else { for (c = 0; c < dk->dk_blanks; c++) dk_canon(dk, CRLF, 2); dk->dk_blanks = 0; dk_canon(dk, wrote, wlen); dk_canon(dk, CRLF, 2); wlen = 0; wrote = p + 1; } dk->dk_lastchar = *p; continue; } } dk->dk_lastchar = *p; wlen++; } /* write what's left */ if (wlen > 0) dk_canon(dk, wrote, wlen); break; case DK_CANON_NOFWS: for (p = buf; p <= eob; p++) { if (*p == '\n') { int c; /* adaptive */ if (dk->dk_crlf == DK_CRLF_UNKNOWN) { if (dk->dk_lastchar == '\r') dk->dk_crlf = DK_CRLF_CRLF; else dk->dk_crlf = DK_CRLF_LF; } if ((dk->dk_lastchar == '\r' && dk->dk_crlf == DK_CRLF_CRLF) || dk->dk_crlf == DK_CRLF_LF) { if (lidx == 0) { dk->dk_blanks++; } else { for (c = 0; c < dk->dk_blanks; c++) dk_canon(dk, CRLF, 2); dk->dk_blanks = 0; dk_canon(dk, lbuf, lidx); dk_canon(dk, CRLF, 2); lidx = 0; } } } else if (!dk_isfws(*p)) { if (lidx == sizeof lbuf) { dk_canon(dk, lbuf, lidx); lidx = 0; } lbuf[lidx] = *p; lidx++; } dk->dk_lastchar = *p; } if (lidx > 0) dk_canon(dk, lbuf, lidx); break; default: dk_canon(dk, buf, len); break; } return DK_STAT_OK; } /* ** DK_EOM -- note end of message */ DK_STAT dk_eom(DK *dk, DK_FLAGS *dkf) { int ret; BIO *key = NULL; struct dk_sha1 *sha1; unsigned char md[SHA_DIGEST_LENGTH]; assert(dk != NULL); /* verify state */ if (dk->dk_state >= DK_STATE_EOM) return DK_STAT_INTERNAL; dk->dk_state = DK_STATE_EOM; sha1 = (struct dk_sha1 *) dk->dk_signinfo; ret = DK_STAT_OK; /* if we're verifying, do the verification */ if (dk->dk_mode == DK_MODE_VERIFY) { int status; /* no sender header was found below the signature */ if (dk->dk_skipbody && dk->dk_processing) { dk_error(dk, "no sender header found below signature"); return DK_STAT_SYNTAX; } if (dk->dk_key == NULL) { if (dk->dk_selector == NULL) { (void) dk_in_use(dk); if (dk->dk_signall || DK_DEBUG('s')) return DK_STAT_NOSIG; else return DK_STAT_OK; } status = dk_get_key(dk); if (status != DK_STAT_OK) return status; } /* load the public key */ key = BIO_new_mem_buf(dk->dk_key, dk->dk_keylen); if (key == NULL) { dk_error(dk, "BIO_new_mem_buf() failed"); return DK_STAT_NORESOURCE; } sha1->sha1_pkey = d2i_PUBKEY_bio(key, NULL); if (sha1->sha1_pkey == NULL) { dk_error(dk, "d2i_PUBKEY_bio() failed"); BIO_free(key); return DK_STAT_NORESOURCE; } /* set up the RSA object */ sha1->sha1_rsa = EVP_PKEY_get1_RSA(sha1->sha1_pkey); if (sha1->sha1_rsa == NULL) { dk_error(dk, "EVP_PKEY_get1_RSA() failed"); BIO_free(key); return DK_STAT_NORESOURCE; } sha1->sha1_keysize = RSA_size(sha1->sha1_rsa); sha1->sha1_pad = RSA_PKCS1_PADDING; /* compute the SHA1 hash */ SHA1_Final(md, &sha1->sha1_sha1); /* verify the signature */ sha1->sha1_rsain = dk->dk_signature; sha1->sha1_rsainlen = dk->dk_signlen; status = RSA_verify(NID_sha1, md, SHA_DIGEST_LENGTH, sha1->sha1_rsain, sha1->sha1_rsainlen, sha1->sha1_rsa); if (status == 0) ret = DK_STAT_BADSIG; } /* return flags */ (void) dk_in_use(dk); if (dkf != NULL) { if (dk->dk_testing) *dkf |= DK_FLAG_TESTING; if (dk->dk_signall) *dkf |= DK_FLAG_SIGNSALL; } if (key != NULL) BIO_free(key); return ret; } /* ** DK_GETSIG -- compute the signature for a message */ DK_STAT dk_getsig(DK *dk, u_char *buf, size_t len) { int bits; int c; int char_count; int status; u_int l; size_t b64idx; size_t b64len; unsigned char *b64; BIO *key; struct dk_sha1 *sha1; unsigned char md[SHA_DIGEST_LENGTH]; assert(dk != NULL); assert(buf != NULL); assert(len > 0); /* verify state */ if (dk->dk_state != DK_STATE_EOM) { dk_error(dk, "dk_getsig() called out of sequence"); return DK_STAT_INTERNAL; } sha1 = (struct dk_sha1 *) dk->dk_signinfo; /* must be called only with a signing handle */ if (dk->dk_mode == DK_MODE_VERIFY) { dk_error(dk, "dk_getsig() called with verifying handle"); return DK_STAT_INTERNAL; } /* try to get the signature if it wasn't given */ if (dk->dk_key == NULL) { int status; status = dk_get_key(dk); if (status != DK_STAT_OK) return status; } /* load the private key */ key = BIO_new_mem_buf(dk->dk_key, dk->dk_keylen); if (key == NULL) { dk_error(dk, "BIO_new_mem_buf() failed"); return DK_STAT_NORESOURCE; } sha1->sha1_pkey = PEM_read_bio_PrivateKey(key, NULL, NULL, NULL); if (sha1->sha1_pkey == NULL) { dk_error(dk, "PEM_read_bio_PrivateKey() failed"); BIO_free(key); return DK_STAT_NORESOURCE; } /* set up the RSA object */ sha1->sha1_rsa = EVP_PKEY_get1_RSA(sha1->sha1_pkey); if (sha1->sha1_rsa == NULL) { dk_error(dk, "EVP_PKEY_get1_RSA() failed"); BIO_free(key); return DK_STAT_NORESOURCE; } sha1->sha1_keysize = RSA_size(sha1->sha1_rsa); sha1->sha1_pad = RSA_PKCS1_PADDING; sha1->sha1_rsaout = dk_malloc(dk->dk_libhandle, dk->dk_closure, sha1->sha1_keysize); if (sha1->sha1_rsaout == NULL) { dk_error(dk, "unable to allocate %d byte(s)", sha1->sha1_keysize); BIO_free(key); return DK_STAT_NORESOURCE; } BIO_reset(sha1->sha1_tmpbio); /* compute the sha1 hash of the message */ SHA1_Final(md, &sha1->sha1_sha1); /* compute the signature on the hash */ sha1->sha1_rsainlen = SHA_DIGEST_LENGTH; sha1->sha1_rsain = md; status = RSA_sign(NID_sha1, sha1->sha1_rsain, sha1->sha1_rsainlen, sha1->sha1_rsaout, &l, sha1->sha1_rsa); if (status == 0 || l == 0) { BIO_free(key); dk_error(dk, "RSA_sign() returned %d, length %d", status, l); return DK_STAT_INTERNAL; } sha1->sha1_rsaoutlen = l; /* store the signature */ dk->dk_signature = sha1->sha1_rsaout; dk->dk_signlen = sha1->sha1_rsaoutlen; /* base64-encode the signature for validation */ b64len = sha1->sha1_rsaoutlen * 3 + 5; b64len += (b64len / 60); b64 = dk_malloc(dk->dk_libhandle, dk->dk_closure, b64len); if (b64 == NULL) { dk_error(dk, "unable to allocate %d byte(s)", b64len); BIO_free(key); return DK_STAT_NORESOURCE; } memset(b64, '\0', b64len); bits = 0; char_count = 0; b64idx = 0; for (c = 0; c < sha1->sha1_rsaoutlen; c++) { bits += sha1->sha1_rsaout[c]; char_count++; if (char_count == 3) { if (b64idx > b64len - 4) { BIO_free(key); dk_error(dk, "base64 encoding failed"); return DK_STAT_INTERNAL; } b64[b64idx++] = alphabet[bits >> 18]; b64[b64idx++] = alphabet[(bits >> 12) & 0x3f]; b64[b64idx++] = alphabet[(bits >> 6) & 0x3f]; b64[b64idx++] = alphabet[bits & 0x3f]; bits = 0; char_count = 0; } else { bits <<= 8; } } if (char_count != 0) { if (b64idx > b64len - 4) { BIO_free(key); dk_error(dk, "base64 encoding failed"); return DK_STAT_INTERNAL; } bits <<= 16 - (8 * char_count); b64[b64idx++] = alphabet[bits >> 18]; b64[b64idx++] = alphabet[(bits >> 12) & 0x3f]; if (char_count == 1) { b64[b64idx++] = '='; b64[b64idx++] = '='; } else { b64[b64idx++] = alphabet[(bits >> 6) & 0x3f]; b64[b64idx++] = '='; } } dk->dk_b64len = b64idx; dk->dk_b64 = b64; /* extract the signature */ strncpy(buf, dk->dk_b64, MIN(len, dk->dk_b64len)); BIO_free(key); return DK_STAT_OK; } /* ** DK_GETHDRS -- report which headers were included in calculation of ** the signature */ DK_STAT dk_gethdrs(DK *dk, int *hcnt, u_char *buf, size_t buflen) { int cnt = 0; unsigned int c; char *colon; char *end; assert(dk != NULL); assert(buf != NULL); assert(buflen > 0); /* verify state */ if (dk->dk_state != DK_STATE_EOM) return DK_STAT_INTERNAL; end = buf + strlen(buf); for (c = 0; c < dk->dk_hdrlidx; c++) { if (c != 0) { if (sm_strlcat(buf, ":", buflen) >= buflen) return DK_STAT_INTERNAL; end++; } if (sm_strlcat(buf, dk->dk_hdrlist[c], buflen) >= buflen) return DK_STAT_INTERNAL; colon = strchr(end, ':'); if (colon == NULL) return DK_STAT_INTERNAL; cnt++; *colon = '\0'; for (; *end != '\0'; end++) if (isascii(*end) && isupper(*end)) *end = tolower(*end); } *hcnt = cnt; return DK_STAT_OK; } /* ** DK_OPTIONS -- set/get options ** ** Parameters: ** dklib -- DK library handle ** op -- DK_OP_SETOPT to set options, DK_OP_GETOPT to get options ** opts -- pointer to either the new option set, or where to write the ** current option set ** ** Return value: ** A DK_STAT value. */ DK_STAT dk_options(DK_LIB *dklib, int op, int *opts) { assert(dklib != NULL); assert(op == DK_OP_SETOPT || op == DK_OP_GETOPT); assert(opts != NULL); switch (op) { case DK_OP_SETOPT: dklib->dkl_options = *opts; break; case DK_OP_GETOPT: *opts = dklib->dkl_options; break; } return DK_STAT_OK; } /* ** DK_REPORTINFO -- get report information */ DK_STAT dk_reportinfo(DK *dk, int *fd, char *addr, size_t alen) { struct dk_sha1 *sha1; assert(dk != NULL); /* verify state */ if (dk->dk_state != DK_STATE_EOM) return DK_STAT_INTERNAL; sha1 = (struct dk_sha1 *) dk->dk_signinfo; if (fd != NULL) { if (sha1 == NULL) *fd = -1; else *fd = sha1->sha1_tmpfd; } if (addr != NULL) sm_strlcpy(addr, dk->dk_reportaddr, alen); return DK_STAT_OK; } /* ** DK_GETIDENTITY -- retrieve apparent signer's identity */ DK_STAT dk_getidentity(DK *dk, char *hname, size_t hnamelen, char *hval, size_t hvallen) { assert(dk != NULL); if (dk->dk_state < DK_STATE_EOH) return DK_STAT_INTERNAL; if (hname != 0 && hnamelen > 0) sm_strlcpy(hname, dk->dk_uhdrn, hnamelen); if (hval != 0 && hvallen > 0) sm_strlcpy(hval, dk->dk_uhdrv, hvallen); return DK_STAT_OK; } /* ** DK_TIMEOUT -- set/get the current DNS timeout */ DK_STAT dk_timeout(DK *dk, int new, int *old) { assert(dk != NULL); if (old != NULL) *old = dk->dk_timeout; if (new != -1) dk->dk_timeout = new; return DK_STAT_OK; } /* ** DK_FREE -- release resources associated with a DK handle */ DK_STAT dk_free(DK *dk) { struct dk_sha1 *sha1; assert(dk != NULL); sha1 = (struct dk_sha1 *) dk->dk_signinfo; #define CLOBBER(x) if ((x) != NULL) \ { \ dk_mfree(dk->dk_libhandle, dk->dk_closure, (x)); \ (x) = NULL; \ } #define BIO_CLOBBER(x) if ((x) != NULL) \ { \ BIO_free((x)); \ (x) = NULL; \ } #define RSA_CLOBBER(x) if ((x) != NULL) \ { \ RSA_free((x)); \ (x) = NULL; \ } #define EVP_CLOBBER(x) if ((x) != NULL) \ { \ EVP_PKEY_free((x)); \ (x) = NULL; \ } if (sha1 != NULL) { BIO_CLOBBER(sha1->sha1_tmpbio); EVP_CLOBBER(sha1->sha1_pkey); RSA_CLOBBER(sha1->sha1_rsa); CLOBBER(sha1->sha1_rsaout); CLOBBER(dk->dk_signinfo); } CLOBBER(dk->dk_sender); CLOBBER(dk->dk_domain); CLOBBER(dk->dk_user); CLOBBER(dk->dk_selector); if (dk->dk_flags & DK_FLAG_FREESIGNATURE) CLOBBER(dk->dk_signature); CLOBBER(dk->dk_b64); CLOBBER(dk->dk_key); CLOBBER(dk->dk_gran); dk_mfree(dk->dk_libhandle, dk->dk_closure, dk); return DK_STAT_OK; }