/* ** Copyright (c) 2005-2007 Sendmail, Inc. and its suppliers. ** All rights reserved. ** ** $Id: dkim-filter.c,v 1.290 2007/12/18 22:17:31 msk Exp $ */ #ifndef lint static char dkim_filter_c_id[] = "@(#)$Id: dkim-filter.c,v 1.290 2007/12/18 22:17:31 msk Exp $"; #endif /* !lint */ /* system includes */ #include #include #include #include #include #if SOLARIS # if SOLARIS > 20700 # include # else /* SOLARIS > 20700 */ # include # endif /* SOLARIS > 20700 */ #endif /* SOLARIS */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if SOLARIS # define _PATH_DEVNULL "/dev/null" # ifndef _PATH_SENDMAIL # define _PATH_SENDMAIL "/usr/sbin/sendmail" # endif /* ! _PATH_SENDMAIL */ #else /* SOLARIS */ # include #endif /* SOLARIS */ /* sendmail includes */ #include #include /* libmilter includes */ #include "libmilter/mfapi.h" /* libdkim includes */ #include #ifdef _FFR_VBR # include #endif /* _FFR_VBR */ #if VERIFY_DOMAINKEYS /* libdk includes */ #include #endif /* VERIFY_DOMAINKEYS */ #if POPAUTH /* libdb includes */ # include #endif /* POPAUTH */ /* dkim-filter includes */ #include "config.h" #include "dkim-config.h" #include "dkim-filter.h" #include "dkim-ar.h" #include "util.h" #include "test.h" #ifdef _FFR_STATS # include "stats.h" #endif /* _FFR_STATS */ /* ** Header -- a handle referring to a header */ typedef struct Header * Header; struct Header { char * hdr_hdr; char * hdr_val; struct Header * hdr_next; }; /* ** KEYTABLE -- table of keys */ struct keytable { char * key_selector; /* selector */ char * key_domain; /* domain */ regex_t key_re; /* regex for matching */ size_t key_len; /* key length */ unsigned char * key_data; /* private key data */ struct keytable * key_next; /* next record */ }; /* ** MSGCTX -- message context, containing transaction-specific data */ typedef struct msgctx * msgctx; struct msgctx { bool mctx_addheader; /* Authentication-Results: */ bool mctx_signing; /* true iff signing */ bool mctx_headeronly; /* in EOM, only add headers */ #if VERIFY_DOMAINKEYS bool mctx_dksigned; /* DK signature present */ #endif /* VERIFY_DOMAINKEYS */ #if _FFR_CAPTURE_UNKNOWN_ERRORS bool mctx_capture; /* capture message? */ #endif /* _FFR_CAPTURE_UNKNOWN_ERRORS */ int mctx_status; /* status to report back */ dkim_canon_t mctx_hdrcanon; /* header canonicalization */ dkim_canon_t mctx_bodycanon; /* body canonicalization */ dkim_alg_t mctx_signalg; /* signature algorithm */ int mctx_queryalg; /* query algorithm */ int mctx_hdrbytes; /* header space allocated */ struct dkimf_dstring * mctx_tmpstr; /* temporary string */ char * mctx_jobid; /* job ID */ char * mctx_domain; /* domain doing the signing */ DKIM * mctx_dkim; /* DKIM handle */ #if VERIFY_DOMAINKEYS DK * mctx_dk; /* DK handle */ #endif /* VERIFY_DOMAINKEYS */ #ifdef _FFR_VBR VBR * mctx_vbr; /* VBR handle */ #endif /* _FFR_VBR */ struct Header * mctx_hqhead; /* header queue head */ struct Header * mctx_hqtail; /* header queue tail */ struct keytable * mctx_key; /* key information */ char mctx_hlist[MAXHEADERS]; /* header buffer */ }; /* ** CONNCTX -- connection context, containing thread-specific data */ typedef struct connctx * connctx; struct connctx { bool cctx_noleadspc; /* no leading spaces */ char cctx_host[DKIM_MAXHOSTNAMELEN + 1]; /* hostname */ _SOCK_ADDR cctx_ip; /* IP info */ struct msgctx * cctx_msg; /* message context */ }; /* ** CONFIG -- configuration information */ typedef struct Config * Config; struct Config { int cfg_nosig; /* no signature */ int cfg_badsig; /* bad signature */ int cfg_sigmissing; /* missing signature */ int cfg_dnserr; /* DNS error */ int cfg_internal; /* internal error */ int cfg_security; /* security concerns */ }; struct Config defaults = { SMFIS_ACCEPT, /* SMFIS_REJECT, */ SMFIS_ACCEPT, /* SMFIS_REJECT, */ SMFIS_ACCEPT, SMFIS_TEMPFAIL, SMFIS_TEMPFAIL, SMFIS_TEMPFAIL }; /* ** LOOKUP -- lookup table */ struct lookup { char * str; int code; }; #define CFG_DEFAULT 0 #define CFG_NOSIGNATURE 1 #define CFG_BADSIGNATURE 2 #define CFG_SIGMISSING 3 #define CFG_DNSERROR 4 #define CFG_INTERNAL 5 #define CFG_SECURITY 6 #define DKIMF_MODE_SIGNER 0x01 #define DKIMF_MODE_VERIFIER 0x02 #define DKIMF_MODE_DEFAULT (DKIMF_MODE_SIGNER|DKIMF_MODE_VERIFIER) #define DKIMF_STATUS_GOOD 0 #define DKIMF_STATUS_BAD 1 #define DKIMF_STATUS_NOKEY 2 #define DKIMF_STATUS_REVOKED 3 #define DKIMF_STATUS_NOSIGNATURE 4 #define DKIMF_STATUS_BADFORMAT 5 #define DKIMF_STATUS_PARTIAL 6 #define DKIMF_STATUS_VERIFYERR 7 #define DKIMF_STATUS_SUSPICIOUS 8 #define DKIMF_STATUS_UNKNOWN 9 #define SIGMIN_BYTES 0 #define SIGMIN_PERCENT 1 #define SIGMIN_MAXADD 2 #define SSPDENYSMTP "550" #define SSPDENYESC "5.7.1" #define SSPDENYTEXT "rejected due to DKIM SSP evaluation" struct lookup dkimf_params[] = { { "no", CFG_NOSIGNATURE }, { "nosignature", CFG_NOSIGNATURE }, { "bad", CFG_BADSIGNATURE }, { "badsignature", CFG_BADSIGNATURE }, { "miss", CFG_SIGMISSING }, { "signaturemissing", CFG_SIGMISSING }, { "dns", CFG_DNSERROR }, { "dnserror", CFG_DNSERROR }, { "int", CFG_INTERNAL }, { "internal", CFG_INTERNAL }, { "sec", CFG_SECURITY }, { "security", CFG_SECURITY }, { "def", CFG_DEFAULT }, { "default", CFG_DEFAULT }, { NULL, -1 }, }; struct lookup dkimf_values[] = { { "a", SMFIS_ACCEPT }, { "accept", SMFIS_ACCEPT }, { "d", SMFIS_DISCARD }, { "discard", SMFIS_DISCARD }, { "r", SMFIS_REJECT }, { "reject", SMFIS_REJECT }, { "t", SMFIS_TEMPFAIL }, { "tempfail", SMFIS_TEMPFAIL }, { NULL, -1 }, }; struct lookup dkimf_canon[] = { { "relaxed", DKIM_CANON_RELAXED }, { "simple", DKIM_CANON_SIMPLE }, { NULL, -1 }, }; struct lookup dkimf_sign[] = { { "rsa-sha1", DKIM_SIGN_RSASHA1 }, #ifdef SHA256_DIGEST_LENGTH { "rsa-sha256", DKIM_SIGN_RSASHA256 }, #endif /* SHA256_DIGEST_LENGTH */ { NULL, -1 }, }; /* sender headers, in order */ char *senderhdr[] = { "Resent-Sender", "Resent-From", "Sender", "From" }; #define NSENDERHDRS 4 /* default internal list */ char *defilist[] = { "127.0.0.1", NULL }; /* PROTOTYPES */ sfsistat mlfi_abort __P((SMFICTX *)); sfsistat mlfi_body __P((SMFICTX *, u_char *, size_t)); sfsistat mlfi_close __P((SMFICTX *)); sfsistat mlfi_connect __P((SMFICTX *, char *, _SOCK_ADDR *)); sfsistat mlfi_envfrom __P((SMFICTX *, char **)); sfsistat mlfi_envrcpt __P((SMFICTX *, char **)); sfsistat mlfi_eoh __P((SMFICTX *)); sfsistat mlfi_eom __P((SMFICTX *)); sfsistat mlfi_header __P((SMFICTX *, char *, char *)); static void dkimf_cleanup __P((SMFICTX *)); static Header dkimf_findheader __P((msgctx, char *, int)); void *dkimf_getpriv __P((SMFICTX *)); char * dkimf_getsymval __P((SMFICTX *, char *)); sfsistat dkimf_insheader __P((SMFICTX *, int, char *, char *)); static void dkimf_report __P((msgctx, char *, char *)); void dkimf_sendprogress __P((void *)); sfsistat dkimf_setpriv __P((SMFICTX *, void *)); sfsistat dkimf_setreply __P((SMFICTX *, char *, char *, char *)); /* GLOBALS */ bool addxhdr; /* add identifying header? */ bool dolog; /* syslog interesting stuff? */ bool dolog_success; /* syslog successes too? */ bool quarantine; /* quarantine failures? */ bool no_i_whine; /* noted ${i} is undefined */ bool send_reports; /* send failure reports */ bool use_ssp_deny; /* use SSP "deny" directive? */ #if _FFR_REQUIRED_HEADERS bool req_hdrs; /* required header checks */ #endif /* _FFR_REQUIRED_HEADERS */ bool subdomains; /* sign subdomains */ bool die; /* global "die" flag */ bool remarall; /* remove all matching ARs? */ bool remsigs; /* remove current signatures? */ bool testmode; /* test mode */ bool milterv2; /* using milter v2? */ #ifdef QUERY_CACHE bool querycache; /* local query cache */ #endif /* QUERY_CACHE */ unsigned int tmo; /* DNS timeout */ unsigned int mode; /* operating mode */ int maxhdrsz; /* max header bytes */ int diesig; /* signal to distribute */ size_t keylen; /* size of secret key */ size_t sigmin; /* signature minimum */ int sigmintype; /* signature minimum type */ int thread_count; /* thread count */ #ifdef QUERY_CACHE time_t cache_lastlog; /* last cache stats logged */ #endif /* QUERY_CACHE */ dkim_canon_t hdrcanon; /* canon. method for headers */ dkim_canon_t bodycanon; /* canon. method for body */ dkim_alg_t signalg; /* signing algorithm */ off_t signbytes; /* bytes to sign */ char *sock; /* listening socket */ char *progname; /* program name */ dkim_sigkey_t seckey; /* secret key data */ char *selector; /* key selector */ #ifdef _FFR_ZTAGS char *diagdir; /* diagnostics directory */ #endif /* _FFR_ZTAGS */ #ifdef _FFR_SELECTOR_HEADER char *selectorhdr; /* selector header */ #endif /* _FFR_SELECTOR_HEADER */ #ifdef _FFR_VBR char *vbr_defcert; /* default VBR type */ char *vbr_deftype; /* default VBR certifiers */ char **vbr_trusted; /* trusted certifiers */ #endif /* _FFR_VBR */ struct Config conf; /* configuration */ DKIM_LIB *libdkim; /* libdkim handle */ #if VERIFY_DOMAINKEYS DK_LIB *libdk; /* libdk handle */ #endif /* VERIFY_DOMAINKEYS */ struct keytable *keyhead; /* key list */ struct keytable *keytail; /* key list */ #ifdef _FFR_REPLACE_RULES struct replace *replist; /* replacement list */ #endif /* _FFR_REPLACE_RULES */ Peer peerlist; /* queue of "peers" */ Peer internal; /* queue of "internal" hosts */ Peer exignore; /* "external ignore" hosts */ char **domains; /* domains to sign */ regex_t **dompats; /* domain patterns */ char **mtas; /* MTA ports to sign */ char **omithdrs; /* headers to omit */ char **alwayshdrs; /* always include headers */ char **macros; /* macros/values to check */ char **values; /* macros/values to check */ char **remar; /* A-R header removal list */ #if POPAUTH DB *popdb; /* POP auth DB */ #endif /* POPAUTH */ pthread_mutex_t count_lock; /* counter lock */ pthread_mutex_t popen_lock; /* popen() lock */ #ifdef _FFR_STATS char *statspath; /* path for stats DB */ #endif /* _FFR_STATS */ /* Other useful definitions */ #define CRLF "\r\n" /* CRLF */ #ifndef SENDMAIL_OPTIONS # define SENDMAIL_OPTIONS "" /* options for reports */ #endif /* SENDMAIL_OPTIONS */ /* MACROS */ #define DKIM_DEBUG(x) (getenv("DKIMDEBUG") != NULL && \ strchr(getenv("DKIMDEBUG"), (x)) != NULL) #define JOBID(x) ((x) == NULL ? JOBIDUNKNOWN : (x)) #define TRYFREE(x) do { \ if ((x) != NULL) \ { \ free(x); \ (x) = NULL; \ } \ } while (0) #define DKIMF_EOHMACROS "i {daemon_name} {auth_type}" /* ** ================================================================== ** BEGIN private section */ #if NO_SMFI_INSHEADER /* ** SMFI_INSHEADER -- stub for smfi_insheader() which didn't exist before ** sendmail 8.13.0 ** ** Parameters: ** ctx -- milter context ** idx -- insertion index ** hname -- header name ** hvalue -- header value ** ** Return value: ** An sfsistat. */ sfsistat smfi_insheader(SMFICTX *ctx, int idx, char *hname, char *hvalue) { assert(ctx != NULL); assert(hname != NULL); assert(hvalue != NULL); return smfi_addheader(ctx, hname, hvalue); } #endif /* NO_SMFI_INSHEADER */ /* ** DKIMF_GETPRIV -- wrapper for smfi_getpriv() ** ** Parameters: ** ctx -- milter (or test) context ** ** Return value: ** The stored private pointer, or NULL. */ void * dkimf_getpriv(SMFICTX *ctx) { assert(ctx != NULL); if (testmode) return dkimf_test_getpriv((void *) ctx); else return smfi_getpriv(ctx); } /* ** DKIMF_SETPRIV -- wrapper for smfi_setpriv() ** ** Parameters: ** ctx -- milter (or test) context ** ** Return value: ** An sfsistat. */ sfsistat dkimf_setpriv(SMFICTX *ctx, void *ptr) { assert(ctx != NULL); if (testmode) return dkimf_test_setpriv((void *) ctx, ptr); else return smfi_setpriv(ctx, ptr); } /* ** DKIMF_INSHEADER -- wrapper for smfi_insheader() ** ** Parameters: ** ctx -- milter (or test) context ** idx -- index at which to insert ** hname -- header name ** hvalue -- header value ** ** Return value: ** An sfsistat. */ sfsistat dkimf_insheader(SMFICTX *ctx, int idx, char *hname, char *hvalue) { assert(ctx != NULL); assert(hname != NULL); assert(hvalue != NULL); if (testmode) return dkimf_test_insheader(ctx, idx, hname, hvalue); else return smfi_insheader(ctx, idx, hname, hvalue); } /* ** DKIMF_SETREPLY -- wrapper for smfi_setreply() ** ** Parameters: ** ctx -- milter (or test) context ** rcode -- SMTP reply code ** xcode -- SMTP enhanced status code ** replytxt -- reply text ** ** Return value: ** An sfsistat. */ sfsistat dkimf_setreply(SMFICTX *ctx, char *rcode, char *xcode, char *replytxt) { assert(ctx != NULL); if (testmode) return dkimf_test_setreply(ctx, rcode, xcode, replytxt); else return smfi_setreply(ctx, rcode, xcode, replytxt); } /* ** DKIMF_GETSYMVAL -- wrapper for smfi_getsymval() ** ** Parameters: ** ctx -- milter (or test) context ** sym -- symbol to retrieve ** ** Return value: ** Pointer to the value of the requested MTA symbol. */ char * dkimf_getsymval(SMFICTX *ctx, char *sym) { assert(ctx != NULL); assert(sym != NULL); if (testmode) return dkimf_test_getsymval(ctx, sym); else return smfi_getsymval(ctx, sym); } /* ** DKIMF_GETDKIM -- retrieve DKIM handle in use ** ** Parameters: ** vp -- opaque pointer (from test.c) ** ** Return value: ** DKIM handle in use, or NULL. */ DKIM * dkimf_getdkim(void *vp) { struct connctx *cc; assert(vp != NULL); cc = vp; if (cc->cctx_msg != NULL) return cc->cctx_msg->mctx_dkim; else return NULL; } /* ** DKIMF_SIGHANDLER -- signal handler ** ** Parameters: ** sig -- signal received ** ** Return value: ** None. */ static void dkimf_sighandler(int sig) { if (sig == SIGINT || sig == SIGTERM || sig == SIGHUP) { diesig = sig; die = TRUE; } } /* ** DKIMF_KILLCHILD -- kill child process ** ** Parameters: ** pid -- process ID to signal ** sig -- signal to use ** ** Return value: ** None. */ static void dkimf_killchild(pid_t pid, int sig) { if (kill(pid, sig) == -1 && dolog) { syslog(LOG_ERR, "kill(%d, %d): %s", pid, sig, strerror(errno)); } } /* ** DKIMF_ZAPKEY -- clobber the copy of the private key ** ** Parameters: ** None. ** ** Return value: ** None. */ static void dkimf_zapkey(void) { if (seckey != NULL) { memset(seckey, '\0', keylen); free(seckey); } if (keyhead != NULL) { struct keytable *key; struct keytable *next; key = keyhead; while (key != NULL) { next = key->key_next; memset(key->key_data, '\0', key->key_len); free(key->key_data); free(key->key_domain); free(key); key = next; } } } /* ** DKIMF_STDIO -- set up the base descriptors to go nowhere ** ** Parameters: ** None. ** ** Return value: ** None. */ static void dkimf_stdio(void) { int devnull; /* this only fails silently, but that's OK */ devnull = open(_PATH_DEVNULL, O_RDWR, 0); if (devnull != -1) { (void) dup2(devnull, 0); (void) dup2(devnull, 1); (void) dup2(devnull, 2); if (devnull > 2) (void) close(devnull); } (void) setsid(); } /* ** DKIMF_SENDPROGRESS -- tell the MTA "we're working on it!" ** ** Parameters: ** ctx -- context ** ** Return value: ** None (yet). */ void dkimf_sendprogress(void *ctx) { assert(ctx != NULL); (void) smfi_progress((SMFICTX *) ctx); } /* ** DKIMF_INITCONTEXT -- initialize filter context ** ** Parameters: ** None. ** ** Return value: ** A pointer to an allocated and initialized filter context, or NULL ** on failure. ** ** Side effects: ** Crop circles near Birmingham. */ static msgctx dkimf_initcontext(void) { msgctx ctx; ctx = (msgctx) malloc(sizeof(struct msgctx)); if (ctx == NULL) return NULL; (void) memset(ctx, '\0', sizeof(struct msgctx)); ctx->mctx_status = DKIMF_STATUS_UNKNOWN; ctx->mctx_hdrcanon = hdrcanon; ctx->mctx_bodycanon = bodycanon; ctx->mctx_signalg = DKIM_SIGN_DEFAULT; ctx->mctx_queryalg = DKIM_QUERY_DEFAULT; return ctx; } /* ** DKIMF_LOG_SSL_ERRORS -- log any queued SSL library errors ** ** Parameters: ** jobid -- job ID to include in log messages ** ** Return value: ** None. */ static void dkimf_log_ssl_errors(char *jobid) { assert(jobid != NULL); /* log any queued SSL error messages */ if (ERR_peek_error() != 0 && dolog) { int n; int saveerr; u_long e; char errbuf[BUFRSZ + 1]; char tmp[BUFRSZ + 1]; saveerr = errno; memset(errbuf, '\0', sizeof errbuf); for (n = 0; ; n++) { e = ERR_get_error(); if (e == 0) break; memset(tmp, '\0', sizeof tmp); (void) ERR_error_string_n(e, tmp, sizeof tmp); if (n != 0) sm_strlcat(errbuf, "; ", sizeof errbuf); sm_strlcat(errbuf, tmp, sizeof errbuf); } syslog(LOG_INFO, "%s SSL %s", jobid, errbuf); errno = saveerr; } } /* ** DKIMF_CLEANUP -- release local resources related to a message ** ** Parameters: ** ctx -- milter context ** ** Return value: ** None. */ static void dkimf_cleanup(SMFICTX *ctx) { msgctx dfc; connctx cc; assert(ctx != NULL); cc = (connctx) dkimf_getpriv(ctx); if (cc == NULL) return; dfc = cc->cctx_msg; /* release memory */ if (dfc != NULL) { if (dfc->mctx_hqhead != NULL) { Header hdr; Header prev; hdr = dfc->mctx_hqhead; while (hdr != NULL) { TRYFREE(hdr->hdr_hdr); TRYFREE(hdr->hdr_val); prev = hdr; hdr = hdr->hdr_next; TRYFREE(prev); } } if (dfc->mctx_dkim != NULL) dkim_free(dfc->mctx_dkim); #ifdef _FFR_VBR if (dfc->mctx_vbr != NULL) vbr_close(dfc->mctx_vbr); #endif /* _FFR_VBR */ #if VERIFY_DOMAINKEYS if (dfc->mctx_dk != NULL) dk_free(dfc->mctx_dk); #endif /* VERIFY_DOMAINKEYS */ if (dfc->mctx_tmpstr != NULL) dkimf_dstring_free(dfc->mctx_tmpstr); free(dfc); cc->cctx_msg = NULL; } } /* ** DKIMF_CONFIGLOOKUP -- look up the integer code for a config option or value ** ** Parameters: ** opt -- option to look up ** table -- lookup table to use ** ** Return value: ** Integer version of the option, or -1 on error. */ static int dkimf_configlookup(char *opt, struct lookup *table) { int c; for (c = 0; ; c++) { if (table[c].str == NULL || strcasecmp(opt, table[c].str) == 0) return table[c].code; } } /* ** DKIMF_PARSECONFIG2 -- parse a single configuration value ** ** Parameters: ** cfg -- pointer to data loaded from a configuration file ** name -- name of the parameter to query ** code -- equivalent code for the -C option ** str -- string to update ** len -- size of "str" ** ** Return value: ** None. ** ** Notes: ** This is transitional, for use during the time that the -C option ** and the configuration file coexist. The -C option overrides ** the values found in the file. */ static void dkimf_parseconfig2(struct config *cfg, const char *name, const char *code, char *str, size_t len) { char *v; size_t offset; assert(cfg != NULL); assert(name != NULL); assert(code != NULL); assert(str != NULL); assert(len > 0); if (config_get(cfg, name, &v, sizeof(char *)) == 1) { offset = strlen(str); snprintf(str + offset, len - offset, "%s%s=%s", offset == 0 ? "" : ",", code, v); } } /* ** DKIMF_PARSECONFIG -- parse configuration and/or apply defaults ** ** Parameters: ** config -- configuration string, or NULL to apply defaults only ** ** Return value: ** TRUE on success, FALSE on failure. */ static bool dkimf_parseconfig(char *confstr) { int vs; char *p; char *v; char *tmp; /* load defaults */ memcpy(&conf, &defaults, sizeof(conf)); if (confstr == NULL) return TRUE; tmp = strdup(confstr); if (tmp == NULL) { fprintf(stderr, "%s: strdup(): %s\n", progname, strerror(errno)); return FALSE; } /* process configuration */ for (p = strtok(tmp, ","); p != NULL; p = strtok(NULL, ",")) { v = strchr(p, '='); if (v == NULL) { fprintf(stderr, "%s: syntax error in configuration string\n", progname); return FALSE; } *v = '\0'; v++; vs = dkimf_configlookup(v, dkimf_values); if (vs == -1) { fprintf(stderr, "%s: invalid configuration value `%s'\n", progname, v); return FALSE; } /* apply what's been found */ switch (dkimf_configlookup(p, dkimf_params)) { case CFG_NOSIGNATURE: conf.cfg_nosig = vs; break; case CFG_BADSIGNATURE: conf.cfg_badsig = vs; break; case CFG_SIGMISSING: conf.cfg_sigmissing = vs; break; case CFG_DNSERROR: conf.cfg_dnserr = vs; break; case CFG_INTERNAL: conf.cfg_internal = vs; break; case CFG_SECURITY: conf.cfg_security = vs; break; case CFG_DEFAULT: conf.cfg_nosig = vs; conf.cfg_badsig = vs; conf.cfg_sigmissing = vs; conf.cfg_dnserr = vs; conf.cfg_internal = vs; conf.cfg_security = vs; break; default: *v = '='; fprintf(stderr, "%s: invalid configuration parameter `%s'\n", progname, p); return FALSE; } } return TRUE; } /* ** DKIMF_LIBSTATUS -- process a final status returned from libdkim ** ** Parameters: ** ctx -- milter context ** where -- what function reported the error ** status -- status returned by a libdk call (DKIM_STAT_*) ** ** Return value: ** An smfistat value to be returned to libmilter. */ static sfsistat dkimf_libstatus(SMFICTX *ctx, char *where, int status) { int retcode = SMFIS_CONTINUE; msgctx dfc; connctx cc; char *rcode = NULL; char *xcode = NULL; char *replytxt = NULL; assert(ctx != NULL); cc = dkimf_getpriv(ctx); assert(cc != NULL); dfc = cc->cctx_msg; assert(dfc != NULL); switch (status) { case DKIM_STAT_OK: retcode = SMFIS_CONTINUE; break; case DKIM_STAT_INTERNAL: retcode = conf.cfg_internal; #ifdef _FFR_CAPTURE_UNKNOWN_ERRORS dfc->mctx_capture = TRUE; #endif /* _FFR_CAPTURE_UNKNOWN_ERRORS */ if (dolog) { const char *err; err = dkim_geterror(dfc->mctx_dkim); if (err == NULL) err = strerror(errno); syslog(LOG_ERR, "%s: %s%sinternal error from libdkim: %s", JOBID(dfc->mctx_jobid), where == NULL ? "" : where, where == NULL ? "" : ": ", err); } replytxt = "internal DKIM error"; break; case DKIM_STAT_BADSIG: retcode = conf.cfg_badsig; if (dolog) { syslog(LOG_ERR, "%s: bad signature data", JOBID(dfc->mctx_jobid)); } replytxt = "bad DKIM signature data"; break; case DKIM_STAT_NOSIG: retcode = conf.cfg_nosig; if (dolog) { syslog(LOG_ERR, "%s: no signature data", JOBID(dfc->mctx_jobid)); } replytxt = "no DKIM signature data"; break; case DKIM_STAT_NORESOURCE: retcode = conf.cfg_internal; #ifdef _FFR_CAPTURE_UNKNOWN_ERRORS dfc->mctx_capture = TRUE; #endif /* _FFR_CAPTURE_UNKNOWN_ERRORS */ if (dolog) { const char *err; err = dkim_geterror(dfc->mctx_dkim); if (err == NULL) err = strerror(errno); syslog(LOG_ERR, "%s: %s%sresource unavailable: %s", JOBID(dfc->mctx_jobid), where == NULL ? "" : where, where == NULL ? "" : ": ", err); } replytxt = "resource unavailable"; break; case DKIM_STAT_CANTVRFY: retcode = conf.cfg_badsig; if (dolog) { const char *err; err = dkim_geterror(dfc->mctx_dkim); if (err == NULL) err = "unknown cause"; syslog(LOG_ERR, "%s: signature verification failed: %s", JOBID(dfc->mctx_jobid), err); } replytxt = "DKIM signature verification failed"; break; case DKIM_STAT_KEYFAIL: retcode = conf.cfg_dnserr; if (dolog) { syslog(LOG_ERR, "%s: key retrieval failed", JOBID(dfc->mctx_jobid)); } replytxt = "DKIM key retrieval failed"; break; case DKIM_STAT_SYNTAX: retcode = conf.cfg_badsig; if (dolog) { const char *err; err = dkim_geterror(dfc->mctx_dkim); if (err == NULL) err = "unspecified"; syslog(LOG_ERR, "%s: syntax error: %s", JOBID(dfc->mctx_jobid), err); } replytxt = "DKIM signature syntax error"; break; } switch (retcode) { case SMFIS_REJECT: rcode = "550"; xcode = "5.7.0"; break; case SMFIS_DISCARD: rcode = "451"; xcode = "4.7.0"; break; default: break; } if (rcode != NULL && xcode != NULL && replytxt != NULL) (void) dkimf_setreply(ctx, rcode, xcode, replytxt); return retcode; } /* ** DKIMF_FINDHEADER -- find a header ** ** Parameters: ** dfc -- filter context ** hname -- name of the header of interest ** instance -- which instance is wanted (0 = first) ** ** Return value: ** Header handle, or NULL if not found. */ static Header dkimf_findheader(msgctx dfc, char *hname, int instance) { Header hdr; assert(dfc != NULL); assert(hname != NULL); hdr = dfc->mctx_hqhead; while (hdr != NULL) { if (strcasecmp(hdr->hdr_hdr, hname) == 0) { if (instance == 0) return hdr; else instance--; } hdr = hdr->hdr_next; } return NULL; } /* ** DKIMF_LOADKEYS -- load multiple keys ** ** Parameters: ** file -- input file ** ** Return value: ** 0 on success, !0 on failure ** ** Side effects: ** keyhead, keytail are updated. */ static int dkimf_loadkeys(char *file) { bool blank = TRUE; int line = 0; FILE *f; u_char *p; char *re; char *domain; char *path; char buf[BUFRSZ + 1]; char root[MAXPATHLEN + 1]; assert(file != NULL); f = fopen(file, "r"); if (f == NULL) { fprintf(stderr, "%s: %s: fopen(): %s\n", progname, file, strerror(errno)); return -1; } memset(root, '\0', sizeof root); memset(buf, '\0', sizeof buf); while (fgets(buf, sizeof buf, f) != NULL) { /* skip comments */ if (buf[0] == '#') continue; blank = TRUE; /* chomp trailing newline */ for (p = buf; *p != '\0'; p++) { if (*p == '\n') { *p = '\0'; break; } if (isascii(*p) && !isspace(*p)) blank = FALSE; } /* skip blank lines */ if (blank) continue; line++; if (buf[0] == '/') { sm_strlcpy(root, buf, sizeof root); if (root[strlen(root) - 1] != '/') sm_strlcat(root, "/", sizeof root); } re = NULL; domain = NULL; path = NULL; /* ** File format: :: ** ** This means if the sender matches , ** sign with d= and use the private ** key found in , using the filename portion ** of the latter as the name of the selector. ** ** is string matching using "*" as a wildcard ** character. ** ** #-delimited comments are allowed. Blank lines are ignored. ** ** A line starting with "/" is interpreted as a root directory ** for keys, meaning the values after that ** in the file are taken relative to that path. */ re = strtok(buf, ":"); domain = strtok(NULL, ":"); if (domain != NULL) path = strtok(NULL, ":"); if (re == NULL || domain == NULL || path == NULL) { fprintf(stderr, "%s: %s: line %d: malformed\n", progname, file, line); fclose(f); return -1; } else { int status; char *r; char *end; size_t n; struct keytable *new; int keyfd; struct stat s; char retmp[BUFRSZ]; char fn[MAXPATHLEN + 1]; dkimf_mkpath(fn, sizeof fn, root, path); keyfd = open(fn, O_RDONLY, 0); if (keyfd == -1 && errno == ENOENT) { /* try appending ".pem" */ sm_strlcat(fn, ".pem", sizeof fn); keyfd = open(fn, O_RDONLY, 0); } if (keyfd == -1 && errno == ENOENT) { /* try appending ".private" */ dkimf_mkpath(fn, sizeof fn, root, path); sm_strlcat(fn, ".private", sizeof fn); keyfd = open(fn, O_RDONLY, 0); } if (keyfd == -1) { dkimf_mkpath(fn, sizeof fn, root, path); fprintf(stderr, "%s: %s: open(): %s\n", progname, fn, strerror(errno)); fclose(f); return -1; } status = fstat(keyfd, &s); if (status == -1) { fprintf(stderr, "%s: %s: fstat(): %s\n", progname, path, strerror(errno)); close(keyfd); fclose(f); return -1; } *p = '\0'; end = retmp + sizeof retmp - 1; memset(retmp, '\0', sizeof retmp); retmp[0] = '^'; r = &retmp[1]; for (p = re; *p != '\0'; p++) { switch (*p) { case '*': if (r + 3 >= end) { fprintf(stderr, "%s: %s: line %d: \"%s\": expression too large\n", progname, file, line, re); close(keyfd); fclose(f); return -1; } (void) sm_strlcat(retmp, ".*", sizeof retmp); r += 2; break; case '.': case '$': case '[': case ']': case '(': case ')': if (r + 3 >= end) { fprintf(stderr, "%s: %s: line %d: \"%s\": expression too large\n", progname, file, line, re); close(keyfd); fclose(f); return -1; } *r = '\\'; r++; /* FALLTHROUGH */ default: *r = *p; r++; break; } } (void) sm_strlcat(retmp, "$", sizeof retmp); new = (struct keytable *) malloc(sizeof(struct keytable)); if (new == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); close(keyfd); fclose(f); return -1; } new->key_next = NULL; r = strrchr(path, '/'); new->key_selector = strdup(r == NULL ? path : r + 1); if (new->key_selector == NULL) { fprintf(stderr, "%s: strdup(): %s\n", progname, strerror(errno)); close(keyfd); fclose(f); return -1; } if (domain[0] != '*') new->key_domain = strdup(domain); else new->key_domain = NULL; status = regcomp(&new->key_re, retmp, (REG_ICASE|REG_EXTENDED)); if (status != 0) { char err[BUFRSZ]; memset(err, '\0', sizeof err); (void) regerror(status, &new->key_re, err, sizeof err); fprintf(stderr, "%s: %s: line %d: regcomp(): %s\n", progname, file, line, err); close(keyfd); fclose(f); return -1; } new->key_data = malloc(s.st_size); if (new->key_data == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); close(keyfd); fclose(f); return -1; } new->key_len = s.st_size; n = read(keyfd, new->key_data, s.st_size); if (n < s.st_size) { if (n == -1) { fprintf(stderr, "%s: %s: read(): %s\n", progname, path, strerror(errno)); } else { fprintf(stderr, "%s: %s: read() truncated\n", progname, path); } close(keyfd); fclose(f); return -1; } close(keyfd); if (keyhead == NULL) { keyhead = new; keytail = new; } else { keytail->key_next = new; keytail = new; } } } fclose(f); return 0; } /* ** DKIMF_REPORT -- generate a report on failure if possible ** ** Parameters: ** dfc -- message context ** result -- result to report ** reason -- reason to report ** ** Return value: ** None. */ static void dkimf_report(msgctx dfc, char *result, char *reason) { int bfd = -1; int hfd = -1; int status; DKIM_STAT dkstatus; size_t inl; size_t outl; FILE *out; BIO *b64; BIO *bout; DKIM_SIGINFO *sig; struct Header *hdr; char buf[BUFRSZ]; char addr[MAXADDRESS + 1]; char hostname[DKIM_MAXHOSTNAMELEN + 1]; assert(dfc != NULL); if (!send_reports) return; memset(addr, '\0', sizeof addr); memset(hostname, '\0', sizeof hostname); (void) gethostname(hostname, sizeof hostname); sig = dkim_getsignature(dfc->mctx_dkim); dkstatus = dkim_reportinfo(dfc->mctx_dkim, sig, &hfd, &bfd, addr, sizeof addr); if (dkstatus != DKIM_STAT_OK || addr[0] == '\0') return; pthread_mutex_lock(&popen_lock); out = popen(_PATH_SENDMAIL " -t" SENDMAIL_OPTIONS, "w"); pthread_mutex_unlock(&popen_lock); if (out == NULL) { if (dolog) { syslog(LOG_ERR, "%s: popen(): %s", dfc->mctx_jobid, strerror(errno)); } return; } /* we presume sendmail will add From: and Date: ... */ /* To: */ fprintf(out, "To: %s\n", addr); /* Subject: */ fprintf(out, "Subject: DKIM failure report for %s\n", dfc->mctx_jobid); /* MIME stuff */ fprintf(out, "MIME-Version: 1.0\n"); fprintf(out, "Content-Type: multipart/report; boundary=\"dkimreport/%s/%s\"", hostname, dfc->mctx_jobid); /* ok, now then... */ fprintf(out, "\n"); /* first part: a text blob explaining what this is */ fprintf(out, "--dkimreport/%s/%s\n", hostname, dfc->mctx_jobid); fprintf(out, "Content-Type: text/plain\n"); fprintf(out, "\n"); fprintf(out, "DKIM failure report for job %s on %s\n\n", dfc->mctx_jobid, hostname); fprintf(out, "The canonicalized form of the failed message is attached.\n"); fprintf(out, "\n"); /* second part: formatted gunk */ fprintf(out, "--dkimreport/%s/%s\n", hostname, dfc->mctx_jobid); fprintf(out, "Content-Type: message/x-dkim-report\n"); fprintf(out, "\n"); fprintf(out, "MTA: %s\n", hostname); fprintf(out, "Agent: %s %s\n", DKIMF_PRODUCT, DKIMF_VERSION); fprintf(out, "Result: %s\n", result == NULL ? "(none)" : result); fprintf(out, "Reason: %s\n", reason == NULL ? "(none)" : reason); fprintf(out, "\n"); /* third part: a multipart containing the interesting stuff */ fprintf(out, "--dkimreport/%s/%s\n", hostname, dfc->mctx_jobid); fprintf(out, "Content-Type: multipart/mixed; boundary=\"dkimreport/%s/%s-parts\"\n", hostname, dfc->mctx_jobid); fprintf(out, "\n"); /* first subpart: headers */ fprintf(out, "--dkimreport/%s/%s-parts\n", hostname, dfc->mctx_jobid); fprintf(out, "Content-Type: text/rfc822-headers\n"); fprintf(out, "\n"); for (hdr = dfc->mctx_hqhead; hdr != NULL; hdr = hdr->hdr_next) fprintf(out, "%s: %s\n", hdr->hdr_hdr, hdr->hdr_val); fprintf(out, "\n"); /* second subpart: canonicalized form */ fprintf(out, "--dkimreport/%s/%s-parts\n", hostname, dfc->mctx_jobid); fprintf(out, "Content-Type: text/plain\n"); /* ? */ fprintf(out, "Content-Disposition: attachment; filename=\"%s.txt\"\n", dfc->mctx_jobid); if (bfd != -1) fprintf(out, "Content-Description: Canonicalized headers\n"); else fprintf(out, "Content-Description: Canonicalized message\n"); fprintf(out, "Content-Transfer-Encoding: base64\n"); fprintf(out, "\n"); (void) lseek(hfd, SEEK_SET, 0); bout = BIO_new(BIO_s_file()); BIO_set_fp(bout, out, BIO_NOCLOSE); b64 = BIO_new(BIO_f_base64()); bout = BIO_push(b64, bout); for (;;) { inl = read(hfd, buf, sizeof buf); if (inl == 0) break; outl = BIO_write(bout, (char *) buf, inl); if (outl != inl || inl < sizeof buf) break; } BIO_flush(bout); BIO_free(bout); /* third subpart: canonicalized form (body) */ if (bfd != -1) { fprintf(out, "\n--dkimreport/%s/%s-parts\n", hostname, dfc->mctx_jobid); fprintf(out, "Content-Type: text/plain\n"); /* ? */ fprintf(out, "Content-Disposition: attachment; filename=\"%s-body.txt\"\n", dfc->mctx_jobid); fprintf(out, "Content-Description: Canonicalized body\n"); fprintf(out, "Content-Transfer-Encoding: base64\n"); fprintf(out, "\n"); (void) lseek(bfd, SEEK_SET, 0); bout = BIO_new(BIO_s_file()); BIO_set_fp(bout, out, BIO_NOCLOSE); b64 = BIO_new(BIO_f_base64()); bout = BIO_push(b64, bout); for (;;) { inl = read(bfd, buf, sizeof buf); if (inl == 0) break; outl = BIO_write(bout, (char *) buf, inl); if (outl != inl || inl < sizeof buf) break; } BIO_flush(bout); BIO_free(bout); } /* end */ fprintf(out, "\n--dkimreport/%s/%s-parts--\n", hostname, dfc->mctx_jobid); fprintf(out, "\n--dkimreport/%s/%s--\n", hostname, dfc->mctx_jobid); /* send it */ pthread_mutex_lock(&popen_lock); status = pclose(out); pthread_mutex_unlock(&popen_lock); if (status != 0 && dolog) { if (dolog) { syslog(LOG_ERR, "%s: pclose(): %s", dfc->mctx_jobid, strerror(errno)); } } } /* ** END private section ** ================================================================== ** BEGIN milter section */ #if SMFI_VERSION >= 0x01000000 /* ** MLFI_NEGOTIATE -- handler called on new SMTP connection to negotiate ** MTA options ** ** Parameters: ** ctx -- milter context ** f0 -- actions offered by the MTA ** f1 -- protocol steps offered by the MTA ** f2 -- reserved for future extensions ** f3 -- reserved for future extensions ** pf0 -- actions requested by the milter ** pf1 -- protocol steps requested by the milter ** pf2 -- reserved for future extensions ** pf3 -- reserved for future extensions ** ** Return value: ** An SMFIS_* constant. */ static sfsistat mlfi_negotiate(SMFICTX *ctx, unsigned long f0, unsigned long f1, SM_UNUSED(unsigned long f2), SM_UNUSED(unsigned long f3), unsigned long *pf0, unsigned long *pf1, unsigned long *pf2, unsigned long *pf3) { unsigned long reqactions = (SMFIF_ADDHDRS|SMFIF_CHGHDRS); unsigned long wantactions = (SMFIF_SETSYMLIST); unsigned long protosteps = (SMFIP_NOHELO|SMFIP_NORCPT|SMFIP_NOUNKNOWN|SMFIP_NODATA|SMFIP_SKIP); connctx cc; /* verify the actions we need are available */ if (quarantine) reqactions |= SMFIF_QUARANTINE; # ifdef _FFR_CAPTURE_UNKNOWN_ERRORS reqactions |= SMFIF_QUARANTINE; # endif /* _FFR_CAPTURE_UNKNOWN_ERRORS */ if ((f0 & reqactions) != reqactions) { if (dolog) { syslog(LOG_ERR, "mlfi_negotiate(): required milter action(s) not available (got 0x%lx, need 0x%lx)", f0, reqactions); } return SMFIS_REJECT; } /* also try to get some nice features */ wantactions = (wantactions & f0); /* set the actions we want */ *pf0 = (reqactions | wantactions); /* disable as many protocol steps we don't need as are available */ *pf1 = (protosteps & f1); # ifdef SMFIP_HDR_LEADSPC /* request preservation of leading spaces if possible */ if ((f1 & SMFIP_HDR_LEADSPC) != 0) { cc = malloc(sizeof(struct connctx)); if (cc != NULL) { memset(cc, '\0', sizeof(struct connctx)); cc->cctx_noleadspc = TRUE; *pf1 |= SMFIP_HDR_LEADSPC; dkimf_setpriv(ctx, cc); } } # endif /* SMFIP_HDR_LEADSPC */ *pf2 = 0; *pf3 = 0; /* request macros if able */ if (macros != NULL && (wantactions & SMFIF_SETSYMLIST) != 0) { int c; char macrolist[BUFRSZ]; memset(macrolist, '\0', sizeof macrolist); sm_strlcpy(macrolist, DKIMF_EOHMACROS, sizeof macrolist); for (c = 0; macros[c] != NULL; c++) { if (macrolist[0] != '\0') sm_strlcat(macrolist, " ", sizeof macrolist); if (sm_strlcat(macrolist, macros[c], sizeof macrolist) >= sizeof macrolist) { if (dolog) { syslog(LOG_ERR, "mlfi_negotiate(): macro list overflow"); } return SMFIS_REJECT; } if (smfi_setsymlist(ctx, SMFIM_EOH, macrolist) != MI_SUCCESS) { if (dolog) { syslog(LOG_ERR, "smfi_setsymlist() failed"); } return SMFIS_REJECT; } } } /* set "milterv2" flag if SMFIP_SKIP was available */ if ((f1 & SMFIP_SKIP) != 0) milterv2 = TRUE; return SMFIS_CONTINUE; } #endif /* SMFI_VERSION >= 0x01000000 */ /* ** MLFI_CONNECT -- connection handler ** ** Parameters: ** ctx -- milter context ** host -- hostname ** ip -- address, in in_addr form ** ** Return value: ** An SMFIS_* constant. */ sfsistat mlfi_connect(SMFICTX *ctx, char *host, _SOCK_ADDR *ip) { connctx cc; if (DKIM_DEBUG('t')) { pthread_mutex_lock(&count_lock); thread_count++; syslog(LOG_INFO, "thread %p connect (%d)", pthread_self(), thread_count); pthread_mutex_unlock(&count_lock); } /* if the client is on an ignored host, then ignore it */ if (peerlist != NULL) { /* try hostname, if available */ if (host != NULL && host[0] != '\0' && host[0] != '[') { dkimf_lowercase(host); if (dkimf_checkhost(peerlist, host)) return SMFIS_ACCEPT; } /* try IP address, if available */ if (ip != NULL && ip->sa_family == AF_INET) { if (dkimf_checkip(peerlist, ip)) return SMFIS_ACCEPT; } } /* copy hostname and IP information to a connection context */ cc = dkimf_getpriv(ctx); if (cc == NULL) { cc = malloc(sizeof(struct connctx)); if (cc == NULL) { if (dolog) { syslog(LOG_ERR, "%s malloc(): %s", host, strerror(errno)); } return SMFIS_TEMPFAIL; } memset(cc, '\0', sizeof(struct connctx)); } sm_strlcpy(cc->cctx_host, host, sizeof cc->cctx_host); if (ip == NULL) { struct sockaddr_in sin; memset(&sin, '\0', sizeof sin); sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); memcpy(&cc->cctx_ip, &sin, sizeof(cc->cctx_ip)); } else { memcpy(&cc->cctx_ip, ip, sizeof(cc->cctx_ip)); } cc->cctx_msg = NULL; dkimf_setpriv(ctx, cc); return SMFIS_CONTINUE; } /* ** MLFI_ENVFROM -- handler for MAIL FROM command (start of message) ** ** Parameters: ** ctx -- milter context ** envfrom -- envelope from arguments ** ** Return value: ** An SMFIS_* constant. */ sfsistat mlfi_envfrom(SMFICTX *ctx, char **envfrom) { connctx cc; msgctx dfc; assert(ctx != NULL); assert(envfrom != NULL); cc = (connctx) dkimf_getpriv(ctx); assert(cc != NULL); if (DKIM_DEBUG('t')) syslog(LOG_INFO, "thread %p envfrom", pthread_self()); /* ** Initialize a filter context. */ dkimf_cleanup(ctx); dfc = dkimf_initcontext(); if (dfc == NULL) { if (dolog) { syslog(LOG_INFO, "message requeueing (internal error)"); } dkimf_cleanup(ctx); return SMFIS_TEMPFAIL; } /* ** Save it in this thread's private space. */ cc->cctx_msg = dfc; /* ** Continue processing. */ return SMFIS_CONTINUE; } /* ** MLFI_HEADER -- handler for mail headers; stores the header in a vector ** of headers for later perusal, removing RFC822 comment ** substrings ** ** Parameters: ** ctx -- milter context ** headerf -- header ** headerv -- value ** ** Return value: ** An SMFIS_* constant. */ sfsistat mlfi_header(SMFICTX *ctx, char *headerf, char *headerv) { msgctx dfc; connctx cc; Header newhdr; #ifdef _FFR_ANTICIPATE_SENDMAIL_MUNGE char *end; char *p; #endif /* _FFR_ANTICIPATE_SENDMAIL_MUNGE */ assert(ctx != NULL); assert(headerf != NULL); assert(headerv != NULL); if (DKIM_DEBUG('t')) syslog(LOG_INFO, "thread %p header", pthread_self()); cc = (connctx) dkimf_getpriv(ctx); assert(cc != NULL); dfc = cc->cctx_msg; assert(dfc != NULL); /* check for too much header data */ if (maxhdrsz > 0 && dfc->mctx_hdrbytes + strlen(headerf) + strlen(headerv) + 2 > maxhdrsz) { if (dolog) syslog(LOG_NOTICE, "too much header data"); return conf.cfg_security; } newhdr = (Header) malloc(sizeof(struct Header)); if (newhdr == NULL) { if (dolog) syslog(LOG_ERR, "malloc(): %s", strerror(errno)); dkimf_cleanup(ctx); return SMFIS_TEMPFAIL; } (void) memset(newhdr, '\0', sizeof(struct Header)); newhdr->hdr_hdr = strdup(headerf); if (dfc->mctx_tmpstr == NULL) { dfc->mctx_tmpstr = dkimf_dstring_new(BUFRSZ, 0); if (dfc->mctx_tmpstr == NULL) { if (dolog) syslog(LOG_ERR, "dkimf_dstring_new() failed"); return SMFIS_TEMPFAIL; } } else { dkimf_dstring_blank(dfc->mctx_tmpstr); } #ifdef _FFR_ANTICIPATE_SENDMAIL_MUNGE /* ** The sendmail MTA does some minor header rewriting on outgoing ** mail. This makes things slightly prettier for the MUA, but ** these changes are made after this filter has already generated ** and added a signature. As a result, verification of the ** signature will fail because what got signed isn't the same ** as what actually goes out. This chunk of code attempts to ** compensate by arranging to feed to the canonicalization ** algorithms the headers exactly as the MTA will modify them, so ** verification should still work. This is based on experimentation ** and on reading sendmail/headers.c, and may require more tweaking ** before it's precisely right. ** ** There are other munges the sendmail MTA makes which are not ** addressed by this patch. ** ** This should not be used with sendmail 8.14 and later as it ** is not required; that version of sendmail and libmilter ** handles the munging correctly (by suppressing it). */ for (p = headerv; *p != '\0'; p++) { /* skip initial spaces */ if (p == headerv && isascii(*p) && isspace(*p)) continue; dkimf_dstring_cat1(dfc->mctx_tmpstr, *p); } #else /* _FFR_ANTICIPATE_SENDMAIL_MUNGE */ dkimf_dstring_copy(dfc->mctx_tmpstr, headerv); #endif /* _FFR_ANTICIPATE_SENDMAIL_MUNGE */ #ifdef _FFR_REPLACE_RULES if (replist != NULL) /* XXX -- signing mode only? */ { int status; regmatch_t match; char *str; struct dkimf_dstring *tmphdr = NULL; struct replace *rep; tmphdr = dkimf_dstring_new(BUFRSZ, 0); if (tmphdr == NULL) { if (dolog) syslog(LOG_ERR, "dkimf_dstring_new() failed"); return SMFIS_TEMPFAIL; } for (rep = replist; rep != NULL; rep = rep->repl_next) { str = dkimf_dstring_get(dfc->mctx_tmpstr); for (;;) { status = regexec(&rep->repl_re, str, 1, &match, 0); if (status == REG_NOMATCH) { break; } else if (status != 0) { if (dolog) { syslog(LOG_ERR, "regexec() failed"); } return SMFIS_TEMPFAIL; } dkimf_dstring_blank(tmphdr); dkimf_dstring_copy(tmphdr, str); dkimf_dstring_chop(tmphdr, match.rm_so); dkimf_dstring_cat(tmphdr, rep->repl_txt); dkimf_dstring_cat(tmphdr, str + match.rm_eo); dkimf_dstring_blank(dfc->mctx_tmpstr); str = dkimf_dstring_get(tmphdr); dkimf_dstring_cat(dfc->mctx_tmpstr, str); } } dkimf_dstring_free(tmphdr); } #endif /* _FFR_REPLACE_RULES */ newhdr->hdr_val = strdup(dkimf_dstring_get(dfc->mctx_tmpstr)); newhdr->hdr_next = NULL; if (newhdr->hdr_hdr == NULL || newhdr->hdr_val == NULL) { if (dolog) syslog(LOG_ERR, "malloc(): %s", strerror(errno)); TRYFREE(newhdr->hdr_hdr); TRYFREE(newhdr->hdr_val); TRYFREE(newhdr); dkimf_cleanup(ctx); return SMFIS_TEMPFAIL; } dfc->mctx_hdrbytes += strlen(newhdr->hdr_hdr) + 1; dfc->mctx_hdrbytes += strlen(newhdr->hdr_val) + 1; if (dfc->mctx_hqhead == NULL) dfc->mctx_hqhead = newhdr; if (dfc->mctx_hqtail != NULL) dfc->mctx_hqtail->hdr_next = newhdr; dfc->mctx_hqtail = newhdr; #if _FFR_SELECT_CANONICALIZATION if (strcasecmp(headerf, XSELECTCANONHDR) == 0) { int c; char *slash; slash = strchr(headerv, '/'); if (slash != NULL) { *slash = '\0'; c = dkimf_configlookup(headerv, dkimf_canon); if (c != -1) dfc->mctx_hdrcanon = (dkim_canon_t) c; c = dkimf_configlookup(slash + 1, dkimf_canon); if (c != -1) dfc->mctx_bodycanon = (dkim_canon_t) c; *slash = '/'; } else { c = dkimf_configlookup(headerv, dkimf_canon); if (c != -1) dfc->mctx_hdrcanon = (dkim_canon_t) c; } /* XXX -- eat this header? */ } #endif /* _FFR_SELECT_CANONICALIZATION */ #if VERIFY_DOMAINKEYS if (strcasecmp(headerf, DK_SIGNHEADER) == 0) dfc->mctx_dksigned = TRUE; #endif /* VERIFY_DOMAINKEYS */ return SMFIS_CONTINUE; } /* ** MLFI_EOH -- handler called when there are no more headers ** ** Parameters: ** ctx -- milter context ** ** Return value: ** An SMFIS_* constant. */ sfsistat mlfi_eoh(SMFICTX *ctx) { char last; bool domainok; bool originok; int c; DKIM_STAT status; connctx cc; msgctx dfc; char *p; char *user; #ifdef _FFR_VBR char *vbr_cert = NULL; char *vbr_type = NULL; #endif /* _FFR_VBR */ Header from; Header hdr; char addr[MAXADDRESS + 1]; assert(ctx != NULL); if (DKIM_DEBUG('t')) syslog(LOG_INFO, "thread %p eoh", pthread_self()); cc = (connctx) dkimf_getpriv(ctx); assert(cc != NULL); dfc = cc->cctx_msg; assert(dfc != NULL); /* ** Determine the message ID for logging. */ dfc->mctx_jobid = dkimf_getsymval(ctx, "i"); if (dfc->mctx_jobid == NULL) dfc->mctx_jobid = JOBIDUNKNOWN; #if _FFR_REQUIRED_HEADERS /* if requested, verify RFC2822-required headers */ if (req_hdrs) { bool ok = TRUE; /* exactly one From: */ if (dkimf_findheader(dfc, "From", 0) == NULL || dkimf_findheader(dfc, "From", 1) != NULL) ok = FALSE; /* exactly one Date: */ if (dkimf_findheader(dfc, "Date", 0) == NULL || dkimf_findheader(dfc, "Date", 1) != NULL) ok = FALSE; if (!ok) { if (dolog) { syslog(LOG_INFO, "%s RFC2822-required header(s) missing", dfc->mctx_jobid); } dfc->mctx_addheader = TRUE; dfc->mctx_headeronly = TRUE; dfc->mctx_status = DKIMF_STATUS_BADFORMAT; return SMFIS_CONTINUE; } } #endif /* _FFR_REQUIRED_HEADERS */ /* find the Sender: or From: header */ memset(addr, '\0', sizeof addr); for (c = 0; c < NSENDERHDRS; c++) { from = dkimf_findheader(dfc, senderhdr[c], 0); if (from != NULL) break; } if (from == NULL) { if (dolog) { syslog(LOG_INFO, "%s: no sender header found; accepting", dfc->mctx_jobid); } dfc->mctx_addheader = TRUE; dfc->mctx_headeronly = TRUE; dfc->mctx_status = DKIMF_STATUS_BADFORMAT; return SMFIS_CONTINUE; } /* extract the sender's domain */ sm_strlcpy(addr, from->hdr_val, sizeof addr); status = rfc2822_mailbox_split(addr, &user, &dfc->mctx_domain); if (status != 0 || user == NULL || dfc->mctx_domain == NULL || user[0] == '\0' || dfc->mctx_domain[0] == '\0') { if (dolog) { syslog(LOG_INFO, "%s: can't parse From: header", dfc->mctx_jobid); } dfc->mctx_addheader = TRUE; dfc->mctx_headeronly = TRUE; dfc->mctx_status = DKIMF_STATUS_BADFORMAT; return SMFIS_CONTINUE; } /* assume we're not signing */ dfc->mctx_signalg = DKIM_SIGN_UNKNOWN; dfc->mctx_signing = FALSE; domainok = FALSE; originok = FALSE; /* is it a domain we sign for? */ if (domains != NULL && dfc->mctx_domain != NULL) { int n; if (dompats != NULL) { for (n = 0; dompats[n] != NULL; n++) { status = regexec(dompats[n], dfc->mctx_domain, 0, NULL, 0); if (status == 0) { domainok = TRUE; break; } } } else { for (n = 0; domains[n] != NULL; n++) { if (strcasecmp(dfc->mctx_domain, domains[n]) == 0) { dfc->mctx_domain = domains[n]; domainok = TRUE; break; } } } if (subdomains) { for (p = strchr(dfc->mctx_domain, '.'); p != NULL && !domainok; p = strchr(p, '.')) { p++; if (*p == '\0') break; if (dompats != NULL) { for (n = 0; dompats[n] != NULL; n++) { status = regexec(dompats[n], dfc->mctx_domain, 0, NULL, 0); if (status == 0) { domainok = TRUE; break; } } } else { for (n = 0; domains[n] != NULL; n++) { if (strcasecmp(p, domains[n]) == 0) { dfc->mctx_domain = domains[n]; domainok = TRUE; break; } } } } } } #ifdef _FFR_SELECTOR_HEADER /* was there a header naming the selector to use? */ if (domainok && selectorhdr != NULL) { /* find the header */ hdr = dkimf_findheader(dfc, selectorhdr, 0); /* did it match a selector in the keylist? */ if (hdr != NULL) { struct keytable *curkey; for (curkey = keyhead; curkey != NULL; curkey = curkey->key_next) { if (strcasecmp(curkey->key_selector, hdr->hdr_val) == 0) { dfc->mctx_key = curkey; break; } } } } #endif /* _FFR_SELECTOR_HEADER */ /* still no key selected; check the keylist (if any) */ if (dfc->mctx_key == NULL && keyhead != NULL) { struct keytable *curkey; char srchaddr[MAXADDRESS + 1]; snprintf(srchaddr, sizeof srchaddr, "%s@%s", user, dfc->mctx_domain); /* select the key */ for (curkey = keyhead; curkey != NULL; curkey = curkey->key_next) { status = regexec(&curkey->key_re, srchaddr, 0, NULL, 0); if (status == 0) break; if (status != REG_NOMATCH) { if (dolog) { char err[BUFRSZ]; (void) regerror(status, &curkey->key_re, err, sizeof err); syslog(LOG_ERR, "%s regexec(): %s", dfc->mctx_jobid, err); } dkimf_cleanup(ctx); return SMFIS_TEMPFAIL; } } if (curkey != NULL) { dfc->mctx_key = curkey; domainok = TRUE; } } /* see if it came in on an authorized MSA/MTA connection */ if (mtas != NULL) { int n; char *mtaname; mtaname = dkimf_getsymval(ctx, "{daemon_name}"); if (mtaname != NULL) { for (n = 0; mtas[n] != NULL; n++) { if (strcasecmp(mtaname, mtas[n]) == 0) { originok = TRUE; break; } } } } /* see if macro tests passed */ if (macros != NULL) { bool done = FALSE; int n; char *val; char *vals; char *last; char name[BUFRSZ + 1]; if (dfc->mctx_tmpstr == NULL) { dfc->mctx_tmpstr = dkimf_dstring_new(BUFRSZ, 0); if (dfc->mctx_tmpstr == NULL) { if (dolog) { syslog(LOG_ERR, "%s dkimf_dstring_new() failed", dfc->mctx_jobid); } dkimf_cleanup(ctx); return SMFIS_TEMPFAIL; } } for (n = 0; !done && macros[n] != NULL; n++) { /* retrieve the macro */ snprintf(name, sizeof name, "{%s}", macros[n]); val = dkimf_getsymval(ctx, name); /* short-circuit if the macro's not set */ if (val == NULL) continue; /* macro set and we don't care about the value */ if (val != NULL && values[n] == NULL) { originok = TRUE; break; } dkimf_dstring_blank(dfc->mctx_tmpstr); dkimf_dstring_copy(dfc->mctx_tmpstr, values[n]); vals = dkimf_dstring_get(dfc->mctx_tmpstr); for (p = strtok_r(vals, "|", &last); !done && p != NULL; p = strtok_r(NULL, "|", &last)) { if (strcasecmp(val, p) == 0) { originok = TRUE; done = TRUE; } } } } /* see if it came from an internal or authenticated source */ if (!originok) { char *authtype; authtype = dkimf_getsymval(ctx, "{auth_type}"); if ((authtype == NULL || authtype[0] == '\0') && #if POPAUTH !dkimf_checkpopauth(popdb, &cc->cctx_ip) && #endif /* POPAUTH */ !dkimf_checkhost(internal, cc->cctx_host) && !dkimf_checkip(internal, &cc->cctx_ip)) { if (domainok && dolog && !dkimf_checkhost(exignore, cc->cctx_host) && !dkimf_checkip(exignore, &cc->cctx_ip)) { syslog(LOG_NOTICE, "%s external host %s attempted to send as %s", dfc->mctx_jobid, cc->cctx_host, dfc->mctx_domain); } } else { originok = TRUE; } } /* set signing mode if the tests passed */ if (domainok && originok) { dfc->mctx_signalg = signalg; dfc->mctx_signing = TRUE; dfc->mctx_addheader = TRUE; } /* ** If we're not operating in the role matching the required operation, ** just accept the message and be done with it. */ if ((dfc->mctx_signing && (mode & DKIMF_MODE_SIGNER) == 0) || (!dfc->mctx_signing && (mode & DKIMF_MODE_VERIFIER) == 0)) return SMFIS_ACCEPT; /* confirm that we've got a key for signing when a keylist is in use */ if (dfc->mctx_signing && dfc->mctx_key == NULL && keyhead != NULL) { if (dolog) { syslog(LOG_NOTICE, "%s no key selected for signing", dfc->mctx_jobid); } return SMFIS_TEMPFAIL; } /* grab an appropriate handle for message processing */ if (!dfc->mctx_signing) { dfc->mctx_dkim = dkim_verify(libdkim, dfc->mctx_jobid, NULL, &status); } else if (dfc->mctx_key != NULL) { char *sdomain; if (dfc->mctx_key->key_domain != NULL) sdomain = dfc->mctx_key->key_domain; else sdomain = dfc->mctx_domain; dfc->mctx_dkim = dkim_sign(libdkim, dfc->mctx_jobid, NULL, (dkim_sigkey_t) dfc->mctx_key->key_data, dfc->mctx_key->key_selector, sdomain, dfc->mctx_hdrcanon, dfc->mctx_bodycanon, dfc->mctx_signalg, signbytes, &status); } else { dfc->mctx_dkim = dkim_sign(libdkim, dfc->mctx_jobid, NULL, seckey, selector, dfc->mctx_domain, dfc->mctx_hdrcanon, dfc->mctx_bodycanon, dfc->mctx_signalg, signbytes, &status); } if (dfc->mctx_dkim == NULL && status != DKIM_STAT_OK) return dkimf_libstatus(ctx, "dkim_new()", status); #if _FFR_VBR /* establish a VBR handle */ dfc->mctx_vbr = vbr_init(NULL, NULL, NULL); if (dfc->mctx_vbr == NULL) { syslog(LOG_ERR, "%s can't create VBR context", dfc->mctx_jobid); dkimf_cleanup(ctx); return SMFIS_TEMPFAIL; } /* store trusted certifiers */ if (vbr_trusted != NULL) vbr_trustedcerts(dfc->mctx_vbr, vbr_trusted); /* if signing, store the values needed to make a header */ if (dfc->mctx_signing) { /* set the sending domain */ vbr_setdomain(dfc->mctx_vbr, dfc->mctx_domain); /* VBR-Type; get value from headers or use default */ hdr = dkimf_findheader(dfc, XVBRTYPEHEADER, 0); if (hdr != NULL) vbr_type = hdr->hdr_val; else vbr_type = vbr_deftype; /* X-VBR-Certifiers; get value from headers or use default */ hdr = dkimf_findheader(dfc, XVBRCERTHEADER, 0); if (hdr != NULL) vbr_cert = hdr->hdr_val; else vbr_cert = vbr_defcert; /* set the message type and certifiers */ if (vbr_type != NULL && vbr_cert != NULL) { /* set the VBR transaction type */ (void) vbr_settype(dfc->mctx_vbr, vbr_type); /* set the VBR certifier list */ (void) vbr_setcert(dfc->mctx_vbr, vbr_cert); } } #endif /* _FFR_VBR */ #if VERIFY_DOMAINKEYS if (dfc->mctx_dksigned && !dfc->mctx_signing) { dfc->mctx_dk = dk_verify(libdk, dfc->mctx_jobid, NULL, &status); if (dfc->mctx_dk == NULL && status != DKIM_STAT_OK) { if (dolog) { syslog(LOG_ERR, "%s: dk_verify() returned status %d", dfc->mctx_jobid, status); } /* XXX -- temp-fail or continue? */ } } #endif /* VERIFY_DOMAINKEYS */ /* run the headers */ for (hdr = dfc->mctx_hqhead; hdr != NULL; hdr = hdr->hdr_next) { /* colon, space, CR, LF, NULL = 5 */ c = strlen(hdr->hdr_hdr) + strlen(hdr->hdr_val) + 5; if (dfc->mctx_tmpstr == NULL) { dfc->mctx_tmpstr = dkimf_dstring_new(BUFRSZ, 0); if (dfc->mctx_tmpstr == NULL) { if (dolog) { syslog(LOG_ERR, "%s: dkimf_dstring_new() failed", dfc->mctx_jobid); } } } else { dkimf_dstring_blank(dfc->mctx_tmpstr); } dkimf_dstring_copy(dfc->mctx_tmpstr, hdr->hdr_hdr); dkimf_dstring_cat1(dfc->mctx_tmpstr, ':'); if (!cc->cctx_noleadspc) dkimf_dstring_cat1(dfc->mctx_tmpstr, ' '); last = '\0'; /* do milter-ized continuation conversion */ for (p = hdr->hdr_val; *p != '\0'; p++) { if (*p == '\n' && last != '\r') dkimf_dstring_cat1(dfc->mctx_tmpstr, '\r'); dkimf_dstring_cat1(dfc->mctx_tmpstr, *p); last = *p; } status = dkim_header(dfc->mctx_dkim, dkimf_dstring_get(dfc->mctx_tmpstr), dkimf_dstring_len(dfc->mctx_tmpstr)); if (status != DKIM_STAT_OK) return dkimf_libstatus(ctx, "dkim_header()", status); #if VERIFY_DOMAINKEYS if (dfc->mctx_dk != NULL) { dkimf_dstring_cat(dfc->mctx_tmpstr, CRLF); status = dk_header(dfc->mctx_dk, dkimf_dstring_get(dfc->mctx_tmpstr), dkimf_dstring_len(dfc->mctx_tmpstr)); if (status != DK_STAT_OK) { if (dolog) { syslog(LOG_ERR, "%s: dk_header() returned status %d", dfc->mctx_jobid, status); } dk_free(dfc->mctx_dk); dfc->mctx_dk = NULL; } } #endif /* VERIFY_DOMAINKEYS */ } #if VERIFY_DOMAINKEYS /* signal end of headers to libdk */ if (dfc->mctx_dk != NULL) { status = dk_eoh(dfc->mctx_dk); if (status != DK_STAT_OK) { if (dolog) { syslog(LOG_ERR, "%s: dk_eoh() returned status %d", dfc->mctx_jobid, status); } dk_free(dfc->mctx_dk); dfc->mctx_dk = NULL; } } #endif /* VERIFY_DOMAINKEYS */ (void) dkim_set_user_context(dfc->mctx_dkim, ctx); /* signal end of headers to libdkim */ status = dkim_eoh(dfc->mctx_dkim); switch (status) { case DKIM_STAT_REVOKED: dfc->mctx_status = DKIMF_STATUS_REVOKED; dfc->mctx_addheader = TRUE; dfc->mctx_headeronly = TRUE; return SMFIS_CONTINUE; case DKIM_STAT_BADSIG: dfc->mctx_status = DKIMF_STATUS_BAD; dfc->mctx_addheader = TRUE; dfc->mctx_headeronly = TRUE; return SMFIS_CONTINUE; case DKIM_STAT_NOSIG: dfc->mctx_status = DKIMF_STATUS_NOSIGNATURE; dfc->mctx_addheader = TRUE; dfc->mctx_headeronly = TRUE; return SMFIS_CONTINUE; case DKIM_STAT_NOKEY: dfc->mctx_status = DKIMF_STATUS_NOKEY; dfc->mctx_addheader = TRUE; dfc->mctx_headeronly = TRUE; return SMFIS_CONTINUE; /* XXX -- other codes? */ case DKIM_STAT_OK: return SMFIS_CONTINUE; default: return dkimf_libstatus(ctx, "dkim_eoh()", status); } } /* ** MLFI_BODY -- handler for an arbitrary body block ** ** Parameters: ** ctx -- milter context ** bodyp -- body block ** bodylen -- amount of data available at bodyp ** ** Return value: ** An SMFIS_* constant. ** ** Description: ** This function reads the body chunks passed by the MTA and ** stores them for later wrapping, if needed. */ sfsistat mlfi_body(SMFICTX *ctx, u_char *bodyp, size_t bodylen) { int status; msgctx dfc; connctx cc; assert(ctx != NULL); assert(bodyp != NULL); if (DKIM_DEBUG('t')) syslog(LOG_INFO, "thread %p body", pthread_self()); cc = (connctx) dkimf_getpriv(ctx); assert(cc != NULL); dfc = cc->cctx_msg; assert(dfc != NULL); #if VERIFY_DOMAINKEYS if (dfc->mctx_dk != NULL) { status = dk_body(dfc->mctx_dk, bodyp, bodylen); if (status != DK_STAT_OK) { if (dolog) { syslog(LOG_ERR, "%s: dk_body() returned status %d", dfc->mctx_jobid, status); } dk_free(dfc->mctx_dk); dfc->mctx_dk = NULL; } } #endif /* VERIFY_DOMAINKEYS */ /* ** No need to do anything if the body was empty. */ if (bodylen == 0 || dfc->mctx_headeronly) return SMFIS_CONTINUE; status = dkim_body(dfc->mctx_dkim, bodyp, bodylen); if (status != DKIM_STAT_OK) return dkimf_libstatus(ctx, "dkim_body()", status); #ifdef SMFIS_SKIP if (milterv2 && dkim_minbody(dfc->mctx_dkim) == 0) return SMFIS_SKIP; #endif /* SMFIS_SKIP */ return SMFIS_CONTINUE; } /* ** MLFI_EOM -- handler called at the end of the message; we can now decide ** based on the configuration if and how to add the text ** to this message, then release resources ** ** Parameters: ** ctx -- milter context ** ** Return value: ** An SMFIS_* constant. */ sfsistat mlfi_eom(SMFICTX *ctx) { bool testkey = FALSE; bool testpolicy = FALSE; bool susp = FALSE; int status = DKIM_STAT_OK; int c; sfsistat ret; connctx cc; msgctx dfc; char *hostname; Header hdr; unsigned char header[DKIM_MAXHEADER + 1]; assert(ctx != NULL); if (DKIM_DEBUG('t')) syslog(LOG_INFO, "thread %p eom", pthread_self()); cc = (connctx) dkimf_getpriv(ctx); assert(cc != NULL); dfc = cc->cctx_msg; assert(dfc != NULL); /* ** If necessary, try again to get the job ID in case it came down ** later than expected (e.g. postfix). */ if (dfc->mctx_jobid == JOBIDUNKNOWN) { dfc->mctx_jobid = dkimf_getsymval(ctx, "i"); if (dfc->mctx_jobid == NULL) { if (no_i_whine && dolog) { syslog(LOG_WARNING, "WARNING: sendmail symbol 'i' not available"); no_i_whine = FALSE; } dfc->mctx_jobid = JOBIDUNKNOWN; } } /* get hostname; used in the X header and in new MIME boundaries */ hostname = dkimf_getsymval(ctx, "j"); if (hostname == NULL) hostname = HOSTUNKNOWN; /* remove old signatures when signing */ if (remsigs && dfc->mctx_signing) { for (hdr = dfc->mctx_hqhead; hdr != NULL; hdr = hdr->hdr_next) { if (strcasecmp(hdr->hdr_hdr, DKIM_SIGNHEADER) == 0) { if (smfi_chgheader(ctx, hdr->hdr_hdr, 0, NULL) != MI_SUCCESS) { if (dolog) { syslog(LOG_WARNING, "failed to remove %s: header", hdr->hdr_hdr); } } } } } /* ** Remove all Authentication-Results: headers as per configuration ** options when verifying. */ if (!dfc->mctx_signing) { struct authres *ares; ares = (struct authres *) malloc(sizeof(struct authres)); if (ares == NULL) { syslog(LOG_WARNING, "%s malloc(): %s", dfc->mctx_jobid, strerror(errno)); return SMFIS_TEMPFAIL; } c = 0; for (hdr = dfc->mctx_hqhead; hdr != NULL; hdr = hdr->hdr_next) { memset(ares, '\0', sizeof(struct authres)); if (strcasecmp(hdr->hdr_hdr, AUTHRESULTSHDR) == 0) { bool dkimres = FALSE; bool hostmatch = FALSE; int arstat; /* remember index */ c++; /* parse the header */ arstat = ares_parse(hdr->hdr_val, ares); if (arstat == -1) { if (dolog) { syslog(LOG_WARNING, "failed to parse %s: header", hdr->hdr_hdr); } continue; } /* method match? */ if (remarall) { dkimres = TRUE; } else { int d; for (d = 0; d < ares->ares_count; d++) { if (ares->ares_result[d].result_method == ARES_METHOD_DKIM) dkimres = TRUE; } } /* hostname match? */ if (remar != NULL) { if (dkimf_hostlist(ares->ares_host, remar)) hostmatch = TRUE; } else { if (strcasecmp(hostname, ares->ares_host) == 0) hostmatch = TRUE; } /* delete if we found both */ if (dkimres && hostmatch) { if (smfi_chgheader(ctx, hdr->hdr_hdr, c, NULL) != MI_SUCCESS) { if (dolog) { syslog(LOG_WARNING, "failed to remove %s: header", hdr->hdr_hdr); } } } } } free(ares); } if (!dfc->mctx_headeronly && dfc->mctx_dkim != NULL) { (void) dkim_set_user_context(dfc->mctx_dkim, ctx); /* ** Signal end-of-message to DKIM */ status = dkim_eom(dfc->mctx_dkim, &testkey); switch (status) { case DKIM_STAT_OK: if (!dfc->mctx_signing && dkimf_findheader(dfc, DKIM_SIGNHEADER, 0) != NULL) { if (dolog_success) { syslog(LOG_INFO, "%s DKIM verification successful", dfc->mctx_jobid); } dfc->mctx_addheader = TRUE; dfc->mctx_status = DKIMF_STATUS_GOOD; } break; case DKIM_STAT_CANTVRFY: dfc->mctx_addheader = TRUE; dfc->mctx_status = DKIMF_STATUS_VERIFYERR; break; case DKIM_STAT_BADSIG: dfc->mctx_addheader = TRUE; dfc->mctx_status = DKIMF_STATUS_BAD; break; case DKIM_STAT_NOSIG: /* don't know yet... */ break; case DKIM_STAT_NOKEY: dfc->mctx_addheader = TRUE; dfc->mctx_status = DKIMF_STATUS_NOKEY; break; default: dkimf_log_ssl_errors(dfc->mctx_jobid); status = dkimf_libstatus(ctx, "dkim_eom()", status); #ifdef _FFR_CAPTURE_UNKNOWN_ERRORS # ifdef SMFIF_QUARANTINE if (dfc->mctx_capture) { if (smfi_quarantine(ctx, "capture requested") != MI_SUCCESS) { if (dolog) { syslog(LOG_ERR, "%s smfi_quarantine() failed", dfc->mctx_jobid); } } status = SMFIS_ACCEPT; } # endif /* ! SMFIF_QUARANTINE */ #endif /* _FFR_CAPTURE_UNKNOWN_ERRORS */ return status; } #ifdef _FFR_STATS if ((status == DKIM_STAT_OK || status == DKIM_STAT_BADSIG) && !dfc->mctx_signing && statspath != NULL) { bool tmp_lengths = FALSE; dkim_alg_t tmp_signalg = DKIM_SIGN_UNKNOWN; dkim_canon_t tmp_hdrcanon = DKIM_CANON_UNKNOWN; dkim_canon_t tmp_bodycanon = DKIM_CANON_UNKNOWN; off_t signlen; const char *tmp_signdomain = NULL; DKIM_SIGINFO *sig = NULL; sig = dkim_getsignature(dfc->mctx_dkim); (void) dkim_sig_getsignalg(sig, &tmp_signalg); (void) dkim_sig_getcanons(sig, &tmp_hdrcanon, &tmp_bodycanon); (void) dkim_sig_getcanonlen(dfc->mctx_dkim, sig, NULL, NULL, &signlen); tmp_lengths = (signlen != (off_t) -1); tmp_signdomain = dkim_sig_getdomain(sig); dkimf_stats_record(statspath, tmp_signdomain, tmp_hdrcanon, tmp_bodycanon, tmp_signalg, (status == DKIM_STAT_OK), testkey, tmp_lengths); } #endif /* _FFR_STATS */ #ifdef _FFR_ZTAGS if (diagdir != NULL && dfc->mctx_status == DKIMF_STATUS_BAD) { int nhdrs; DKIM_SIGINFO *sig; char *ohdrs[MAXHDRCNT]; nhdrs = MAXHDRCNT; memset(ohdrs, '\0', sizeof ohdrs); sig = dkim_getsignature(dfc->mctx_dkim); status = dkim_ohdrs(dfc->mctx_dkim, sig, ohdrs, &nhdrs); if (status == DKIM_STAT_OK) { FILE *f; char dpath[MAXPATHLEN + 1]; snprintf(dpath, sizeof dpath, "%s/%s", diagdir, dfc->mctx_jobid); f = fopen(dpath, "w"); if (f == NULL) { if (dolog) { syslog(LOG_ERR, "%s %s: fopen(): %s", dfc->mctx_jobid, dpath, strerror(errno)); } } else { int c; # ifdef _FFR_DIFFHEADERS int ndiffs; struct dkim_hdrdiff *diffs; # endif /* _FFR_DIFFHEADERS */ struct Header *hdr; fprintf(f, "z tag headers:\n\n"); for (c = 0; c < nhdrs; c++) fprintf(f, "%s\r\n", ohdrs[c]); fprintf(f, "--------------------\n\n"); fprintf(f, "Received headers:\n\n"); for (hdr = dfc->mctx_hqhead; hdr != NULL; hdr = hdr->hdr_next) { fprintf(f, "%s:%s%s", hdr->hdr_hdr, cc->cctx_noleadspc ? "" : " ", hdr->hdr_val); } # ifdef _FFR_DIFFHEADERS /* XXX -- make the "5" configurable */ status = dkim_diffheaders(dfc->mctx_dkim, 5, ohdrs, nhdrs, &diffs, &ndiffs); if (status == DKIM_STAT_OK && diffs != NULL && ndiffs > 0) { fprintf(f, "--------------------\n\n"); fprintf(f, "Munging detected:\n\n"); for (c = 0; c < ndiffs; c++) { fprintf(f, "-%s\n+%s\n\n", diffs[c].hd_old, diffs[c].hd_new); } } # endif /* _FFR_DIFFHEADERS */ fclose(f); } } } #endif /* _FFR_ZTAGS */ if (dfc->mctx_status == DKIMF_STATUS_GOOD) { if (sigmin > 0) { off_t canonlen; off_t bodylen; DKIM_SIGINFO *sig; sig = dkim_getsignature(dfc->mctx_dkim); (void) dkim_sig_getcanonlen(dfc->mctx_dkim, sig, &bodylen, &canonlen, NULL); if (sigmintype == SIGMIN_PERCENT) { size_t signpct; signpct = (100 * canonlen) / bodylen; if (signpct < sigmin) dfc->mctx_status = DKIMF_STATUS_PARTIAL; } else if (sigmintype == SIGMIN_MAXADD) { if (canonlen + sigmin < bodylen) dfc->mctx_status = DKIMF_STATUS_PARTIAL; } else { size_t required; required = MIN(sigmin, bodylen); if (canonlen < sigmin) dfc->mctx_status = DKIMF_STATUS_PARTIAL; } } } /* compute and insert the signature, if we're signing */ if (dfc->mctx_signing) { size_t len; char *start; memset(header, '\0', sizeof header); len = sizeof header; start = header; if (cc->cctx_noleadspc) { len--; header[0] = ' '; start++; } status = dkim_getsighdr(dfc->mctx_dkim, start, len, DKIM_HDRMARGIN, strlen(DKIM_SIGNHEADER) + 2); if (status != DKIM_STAT_OK) return status; dkimf_stripcr(header); if (dkimf_insheader(ctx, 1, DKIM_SIGNHEADER, header) == MI_FAILURE) { if (dolog) { syslog(LOG_ERR, "%s \"%s\" header add failed", dfc->mctx_jobid, DKIM_SIGNHEADER); } } if (dolog_success) { syslog(LOG_INFO, "%s \"%s\" header added", dfc->mctx_jobid, DKIM_SIGNHEADER); } #ifdef _FFR_VBR /* generate and add a VBR-Info header */ memset(header, '\0', sizeof header); status = vbr_getheader(dfc->mctx_vbr, header, sizeof header); /* XXX -- log errors */ if (status == DKIM_STAT_OK) { if (dkimf_insheader(ctx, 1, VBR_INFOHEADER, header) == MI_FAILURE) { if (dolog) { syslog(LOG_ERR, "%s \"%s\" header add failed", dfc->mctx_jobid, VBR_INFOHEADER); } } } #endif /* _FFR_VBR */ } } if (dfc->mctx_dkim != NULL && !dfc->mctx_signing) { /* ** Evaluate sender signing policy for failed or unsigned ** messages. */ if (status == DKIM_STAT_OK || status == DKIM_STAT_BADSIG || status == DKIM_STAT_CANTVRFY || status == DKIM_STAT_SYNTAX || status == DKIM_STAT_REVOKED || status == DKIM_STAT_KEYFAIL || status == DKIM_STAT_NOSIG) { dkim_handling_t hcode = DKIM_HANDLING_NONE; status = dkim_policy(dfc->mctx_dkim, &testpolicy, &susp, NULL, &hcode, NULL); if (status == DKIM_STAT_OK) { if (susp) { dfc->mctx_addheader = TRUE; dfc->mctx_status = DKIMF_STATUS_SUSPICIOUS; } if (hcode == DKIM_HANDLING_DENY && use_ssp_deny && !testpolicy) { if (dolog) { syslog(LOG_NOTICE, "%s rejected per sender domain policy", dfc->mctx_jobid); } if (smfi_setreply(ctx, SSPDENYSMTP, SSPDENYESC, SSPDENYTEXT) != MI_SUCCESS && dolog) { syslog(LOG_NOTICE, "%s smfi_setreply() failed", dfc->mctx_jobid); } dkimf_cleanup(ctx); return SMFIS_REJECT; } } else if (dolog) { const char *err; err = dkim_geterror(dfc->mctx_dkim); if (err != NULL) { syslog(LOG_ERR, "%s SSP query: %s", dfc->mctx_jobid, err); } } } } /* insert DKIM status */ if (dfc->mctx_addheader && dfc->mctx_status != DKIMF_STATUS_UNKNOWN) { bool test; u_int keybits; char *authresult; char *failstatus; char *method; DKIM_SIGINFO *sig; char comment[BUFRSZ + 1]; memset(comment, '\0', sizeof comment); test = FALSE; failstatus = (testkey ? "neutral" : "hardfail"); switch (dfc->mctx_status) { case DKIMF_STATUS_GOOD: authresult = "pass"; sig = dkim_getsignature(dfc->mctx_dkim); assert(sig != NULL); (void) dkim_sig_getkeysize(sig, &keybits); snprintf(comment, sizeof comment, "%u-bit key", keybits); break; case DKIMF_STATUS_NOSIGNATURE: authresult = failstatus; sm_strlcpy(comment, "no signature", sizeof comment); if (!susp) dfc->mctx_addheader = FALSE; break; case DKIMF_STATUS_BAD: case DKIMF_STATUS_REVOKED: case DKIMF_STATUS_PARTIAL: case DKIMF_STATUS_VERIFYERR: authresult = failstatus; if (dfc->mctx_status == DKIMF_STATUS_REVOKED) { sm_strlcpy(comment, "revoked", sizeof comment); } else if (dfc->mctx_status == DKIMF_STATUS_PARTIAL) { authresult = "permerror"; sm_strlcpy(comment, "partial verification", sizeof comment); } else if (dfc->mctx_status == DKIMF_STATUS_VERIFYERR) { const char *err; authresult = "permerror"; err = dkim_geterror(dfc->mctx_dkim); if (err != NULL) { snprintf(comment, sizeof comment, "verification error: %s", err); } else { sm_strlcpy(comment, "verification error", sizeof comment); } } else { sm_strlcpy(comment, "verification failed", sizeof comment); } break; case DKIMF_STATUS_SUSPICIOUS: authresult = (testpolicy ? "neutral" : "hardfail"); sm_strlcpy(comment, "SSP", sizeof comment); break; case DKIMF_STATUS_BADFORMAT: authresult = "permerror"; sm_strlcpy(comment, "bad format", sizeof comment); break; default: authresult = "neutral"; break; } #ifdef SMFIF_QUARANTINE /* quarantine for "bad" results if requested */ if (quarantine && (dfc->mctx_status == DKIMF_STATUS_BAD || dfc->mctx_status == DKIMF_STATUS_REVOKED || dfc->mctx_status == DKIMF_STATUS_PARTIAL || dfc->mctx_status == DKIMF_STATUS_VERIFYERR || (dfc->mctx_status == DKIMF_STATUS_NOSIGNATURE && dfc->mctx_addheader))) { char qreason[BUFRSZ + 1]; snprintf(qreason, sizeof qreason, "%s: %s: %s", progname, failstatus, comment); if (smfi_quarantine(ctx, qreason) != MI_SUCCESS) { if (dolog) { syslog(LOG_ERR, "%s smfi_quarantine() failed", dfc->mctx_jobid); } } } #endif /* SMFIF_QUARANTINE */ if (test) { if (comment[0] == '\0') sm_strlcpy(comment, "testing", sizeof comment); else sm_strlcat(comment, "/testing", sizeof comment); } memset(header, '\0', sizeof header); method = "dkim"; if (susp && (dfc->mctx_status == DKIMF_STATUS_BAD || dfc->mctx_status == DKIMF_STATUS_GOOD || dfc->mctx_status == DKIMF_STATUS_REVOKED || dfc->mctx_status == DKIMF_STATUS_PARTIAL || dfc->mctx_status == DKIMF_STATUS_VERIFYERR || dfc->mctx_status == DKIMF_STATUS_NOSIGNATURE)) method = "dkim-ssp"; if (dfc->mctx_dkim != NULL) { char val[MAXADDRESS + 1]; memset(val, '\0', sizeof val); sm_strlcpy(val, "unknown", sizeof val); (void) dkim_sig_getidentity(dfc->mctx_dkim, NULL, val, sizeof val); snprintf(header, sizeof header, "%s%s; %s=%s%s%s%s header.i=%s", cc->cctx_noleadspc ? " " : "", hostname, method, authresult, comment[0] == '\0' ? "" : " (", comment[0] == '\0' ? "" : comment, comment[0] == '\0' ? "" : ")", val); if (dfc->mctx_addheader && dkimf_insheader(ctx, 1, AUTHRESULTSHDR, header) == MI_FAILURE) { if (dolog) { syslog(LOG_ERR, "%s \"%s\" header add failed", dfc->mctx_jobid, AUTHRESULTSHDR); } } } if (dfc->mctx_status == DKIMF_STATUS_BAD && dfc->mctx_dkim != NULL) dkimf_report(dfc, authresult, comment); } #ifdef _FFR_VBR if (!dfc->mctx_signing && dkimf_findheader(dfc, VBR_INFOHEADER, 0) != NULL) { bool add_vbr_header = FALSE; VBR_STAT vbr_status; int c; char *vbr_result; char *vbr_domain; char *vbr_certifier; char *vbr_vouchers; char *vbr_type; char *p; char *sctx; char *eq; u_char *param; u_char *value; Header vbr_header; char tmp[DKIM_MAXHEADER + 1]; for (c = 0; ; c++) { vbr_header = dkimf_findheader(dfc, VBR_INFOHEADER, c); if (vbr_header == NULL) break; vbr_result = NULL; vbr_domain = NULL; vbr_certifier = NULL; vbr_vouchers = NULL; vbr_type = NULL; /* break out the VBR-Info header contents */ sm_strlcpy(tmp, vbr_header->hdr_val, sizeof tmp); for (p = strtok_r(tmp, ";", &sctx); p != NULL; p = strtok_r(NULL, ";", &sctx)) { eq = strchr(p, '='); if (eq == NULL) continue; *eq = '\0'; for (param = p; *param != '\0'; param++) { if (!(isascii(*param) && isspace(*param))) break; } dkimf_trimspaces(param); for (value = eq + 1; *value != '\0'; value++) { if (!(isascii(*value) && isspace(*value))) break; } dkimf_trimspaces(value); if (strcasecmp(param, "md") == 0) { vbr_domain = value; } else if (strcasecmp(param, "mc") == 0) { vbr_type = value; } else if (strcasecmp(param, "mv") == 0) { vbr_vouchers = value; } } /* use accessors to set parsed values */ vbr_setcert(dfc->mctx_vbr, vbr_vouchers); vbr_settype(dfc->mctx_vbr, vbr_type); vbr_setdomain(dfc->mctx_vbr, vbr_domain); /* attempt the query */ vbr_status = vbr_query(dfc->mctx_vbr, &vbr_result, &vbr_certifier); switch (vbr_status) { case VBR_STAT_DNSERROR: if (dolog) { const char *err; err = vbr_geterror(dfc->mctx_vbr); syslog(LOG_NOTICE, "%s: can't verify VBR information%s%s", dfc->mctx_jobid, err == NULL ? "" : ": ", err == NULL ? "" : err); } vbr_result = "neutral"; break; case VBR_STAT_INVALID: case VBR_STAT_NORESOURCE: if (dolog) { const char *err; err = vbr_geterror(dfc->mctx_vbr); syslog(LOG_NOTICE, "%s: error handling VBR information%s%s", dfc->mctx_jobid, err == NULL ? "" : ": ", err == NULL ? "" : err); } vbr_result = "neutral"; break; case DKIM_STAT_OK: add_vbr_header = TRUE; break; default: assert(0); } if (add_vbr_header) { char hdr[DKIM_MAXHEADER + 1]; memset(hdr, '\0', sizeof hdr); snprintf(hdr, sizeof hdr, "%s.md", VBR_INFOHEADER); dkimf_lowercase(hdr); snprintf(header, sizeof header, "%s%s %s=%s; vbr=%s%s%s%s", cc->cctx_noleadspc ? " " : "", hostname, hdr, vbr_domain, vbr_result, vbr_certifier == NULL ? "" : " (", vbr_certifier == NULL ? "" : vbr_certifier, vbr_certifier == NULL ? "" : ")"); if (dkimf_insheader(ctx, 1, AUTHRESULTSHDR, header) == MI_FAILURE) { if (dolog) { syslog(LOG_ERR, "%s \"%s\" header add failed", dfc->mctx_jobid, AUTHRESULTSHDR); } } break; } } } #endif /* _FFR_VBR */ #if VERIFY_DOMAINKEYS if (dfc->mctx_dk != NULL) { DK_FLAGS flags; char *authresult = NULL; char *comment = NULL; char hdr[DKIM_MAXHEADER + 1]; char val[MAXADDRESS + 1]; flags = 0; dfc->mctx_addheader = FALSE; status = dk_eom(dfc->mctx_dk, &flags); switch (status) { case DK_STAT_OK: dfc->mctx_addheader = dfc->mctx_dksigned; authresult = "pass"; break; case DK_STAT_BADSIG: dfc->mctx_addheader = TRUE; authresult = "hardfail"; break; case DK_STAT_NOSIG: /* XXX -- extract policy */ dfc->mctx_addheader = TRUE; authresult = "neutral"; comment = "no signature"; break; case DK_STAT_NOKEY: /* XXX -- extract policy */ dfc->mctx_addheader = TRUE; authresult = "neutral"; comment = "no key"; break; default: /* XXX -- do better? */ if (dolog) { #if (DK_LIB_VERSION >= 0x00050000) const char *err; err = dk_geterror(dfc->mctx_dk); if (err == NULL) err = strerror(errno); syslog(LOG_INFO, "%s dk_eom() returned status %d: %s", dfc->mctx_jobid, status, err); #else /* (DK_LIB_VERSION >= 0x00050000) */ syslog(LOG_INFO, "%s dk_eom() returned status %d", dfc->mctx_jobid, status); #endif /* (DK_LIB_VERSION >= 0x00050000) */ } break; } if (dfc->mctx_addheader) { sm_strlcpy(hdr, "unknown", sizeof hdr); sm_strlcpy(val, "unknown", sizeof val); (void) dk_getidentity(dfc->mctx_dk, hdr, sizeof hdr, val, sizeof val); memset(header, '\0', sizeof header); snprintf(header, sizeof header, "%s%s; domainkeys=%s%s%s%s%s header.%s=%s", cc->cctx_noleadspc ? " " : "", hostname, authresult, comment == NULL ? "" : " (", comment == NULL ? "" : comment, comment == NULL ? "" : ")", !(flags & DK_FLAG_TESTING) ? "" : " (testing)", hdr, val); if (dkimf_insheader(ctx, 1, AUTHRESULTSHDR, header) == MI_FAILURE) { if (dolog) { syslog(LOG_ERR, "%s \"%s\" header add failed", dfc->mctx_jobid, AUTHRESULTSHDR); } } } } #endif /* VERIFY_DOMAINKEYS */ /* ** Identify the filter, if requested. */ if (addxhdr) { char xfhdr[DKIM_MAXHEADER + 1]; memset(xfhdr, '\0', sizeof xfhdr); snprintf(xfhdr, DKIM_MAXHEADER, "%s%s v%s %s %s", cc->cctx_noleadspc ? " " : "", DKIMF_PRODUCT, DKIMF_VERSION, hostname, dfc->mctx_jobid != NULL ? dfc->mctx_jobid : JOBIDUNKNOWN); if (dkimf_insheader(ctx, 1, XHEADERNAME, xfhdr) != MI_SUCCESS) { if (dolog) { syslog(LOG_ERR, "%s \"%s\" header add failed", dfc->mctx_jobid, XHEADERNAME); } dkimf_cleanup(ctx); return SMFIS_TEMPFAIL; } } dkimf_log_ssl_errors(dfc->mctx_jobid); /* ** If we got this far, we're ready to complete. */ ret = SMFIS_ACCEPT; /* translate the stored status */ switch (dfc->mctx_status) { case DKIMF_STATUS_GOOD: break; case DKIMF_STATUS_BAD: ret = dkimf_libstatus(ctx, "mlfi_eom()", DKIM_STAT_BADSIG); break; case DKIMF_STATUS_NOKEY: ret = dkimf_libstatus(ctx, "mlfi_eom()", DKIM_STAT_NOKEY); break; case DKIMF_STATUS_REVOKED: ret = SMFIS_TEMPFAIL; break; case DKIMF_STATUS_NOSIGNATURE: /* ** If no header was added, we've already decided that we ** don't care that there was no signature here, so don't ** log anything or react either. */ if (dfc->mctx_addheader) { ret = dkimf_libstatus(ctx, "mlfi_eom()", DKIM_STAT_NOSIG); } break; case DKIMF_STATUS_BADFORMAT: ret = SMFIS_ACCEPT; break; case DKIMF_STATUS_UNKNOWN: break; default: if (status != DKIM_STAT_OK) ret = dkimf_libstatus(ctx, "mlfi_eom()", status); break; } return ret; } /* ** MLFI_ABORT -- handler called if an earlier filter in the filter process ** rejects the message ** ** Parameters: ** ctx -- milter context ** ** Return value: ** An SMFIS_* constant. */ sfsistat mlfi_abort(SMFICTX *ctx) { if (DKIM_DEBUG('t')) syslog(LOG_INFO, "thread %p abort", pthread_self()); dkimf_cleanup(ctx); return SMFIS_CONTINUE; } /* ** MLFI_CLOSE -- handler called on connection shutdown ** ** Parameters: ** ctx -- milter context ** ** Return value: ** An SMFIS_* constant. */ sfsistat mlfi_close(SMFICTX *ctx) { connctx cc; if (DKIM_DEBUG('t')) { pthread_mutex_lock(&count_lock); thread_count--; syslog(LOG_INFO, "thread %p close (%d)", pthread_self(), thread_count); pthread_mutex_unlock(&count_lock); } dkimf_cleanup(ctx); cc = (connctx) dkimf_getpriv(ctx); free(cc); dkimf_setpriv(ctx, NULL); #ifdef QUERY_CACHE if (querycache) { time_t now; (void) time(&now); if (cache_lastlog + CACHESTATSINT < now) { u_int c_hits; u_int c_queries; u_int c_expired; dkim_getcachestats(&c_queries, &c_hits, &c_expired); cache_lastlog = now; syslog(LOG_INFO, "cache: %u quer%s, %u hit%s (%d%%), %u expired", c_queries, c_queries == 1 ? "y" : "ies", c_hits, c_hits == 1 ? "" : "s", (c_hits * 100) / c_queries, c_expired); } } #endif /* QUERY_CACHE */ return SMFIS_CONTINUE; } /* ** smfilter -- the milter module description */ struct smfiDesc smfilter = { DKIMF_PRODUCT, /* filter name */ SMFI_VERSION, /* version code -- do not change */ (SMFIF_ADDHDRS | SMFIF_CHGHDRS | #ifdef SMFIF_QUARANTINE SMFIF_QUARANTINE | #endif /* SMFIF_QUARANTINE */ #ifdef SMFIF_SETSYMLIST SMFIF_SETSYMLIST | #endif /* SMFIF_SETSYMLIST */ 0), /* flags */ mlfi_connect, /* connection info filter */ NULL, /* SMTP HELO command filter */ mlfi_envfrom, /* envelope sender filter */ NULL, /* envelope recipient filter */ mlfi_header, /* header filter */ mlfi_eoh, /* end of header */ mlfi_body, /* body block filter */ mlfi_eom, /* end of message */ mlfi_abort, /* message aborted */ mlfi_close, /* shutdown */ #if SMFI_VERSION > 2 NULL, /* unrecognised command */ #endif #if SMFI_VERSION > 3 NULL, /* DATA */ #endif #if SMFI_VERSION >= 0x01000000 mlfi_negotiate /* negotiation callback */ #endif }; /* ** USAGE -- print a usage message and return the appropriate exit status ** ** Parameters: ** None. ** ** Return value: ** EX_USAGE. */ static int usage(void) { fprintf(stderr, "%s: usage: %s -p socketfile [options]\n" "\t-a peerlist \tfile containing list of hosts to ignore\n" "\t-A \tauto-restart\n" "\t-b modes \tselect operating modes\n" "\t-c canon \tcanonicalization to use when signing\n" "\t-C config \tconfiguration info (see man page)\n" "\t-d domlist \tdomains to sign\n" "\t-D \talso sign subdomains\n" "\t-f \tdon't fork-and-exit\n" "\t-F time \tfixed timestamp to use when signing (test mode only)\n" "\t-h \tappend identifying header\n" "\t-H hdrlist \twhich additional headers to sign\n" "\t-i ilist \tfile containing list of internal (signing) hosts\n" "\t-I elist \tfile containing list of external domain clients\n" "\t-k keyfile \tlocation of secret key file\n" "\t-K \tload a key set instead of a single key\n" "\t-l \tlog activity to system log\n" "\t-L limit \tsignature limit requirements\n" "\t-m mtalist \tMTA daemon names for which to sign\n" "\t-M macrolist\tMTA macros which enable signing\n" "\t-o hdrlist \tlist of headers to omit from signing\n" "\t-P pidfile \tfile to which to write pid\n" #if _FFR_REQUIRE_HEADERS "\t-r \trequire basic RFC2822 header compliance\n" #endif /* _FFR_REQUIRE_HEADERS */ "\t-R \tgenerate verification failure reports\n" "\t-s selector \tselector to use when signing\n" "\t-S signalg \tsignature algorithm to use when signing\n" "\t-t testfile \tevaluate RFC2822 message in \"testfile\"\n" "\t-T timeout \tDNS timeout (seconds)\n" "\t-u userid \tchange to specified userid\n" #if POPAUTH "\t-U dbfile \tuser POP AUTH database\n" #endif /* POPAUTH */ "\t-v \tincrease verbosity during testing\n" "\t-V \tprint version number and terminate\n" "\t-x conffile \tread configuration from conffile\n", progname, progname); return EX_USAGE; } /* ** MAIN -- program mainline ** ** Process command line arguments and call the milter mainline. */ int main(int argc, char **argv) { bool autorestart = FALSE; bool ztags = FALSE; bool blen = FALSE; bool gotp = FALSE; bool dofork = TRUE; bool multikey = FALSE; bool stricttest = FALSE; int c; int status; int n; int verbose = 0; int sigttl = 0; int clockdrift = 0; int maxsign; int filemask = -1; time_t fixedtime = (time_t) -1; unsigned long tmpl; const char *args = CMDLINEOPTS; FILE *f; char *be = NULL; char *become = NULL; char *domlist = NULL; char *mtalist = NULL; char *p; char *pidfile = NULL; char *keyfile = NULL; char *config = NULL; char *peerfile = NULL; #ifdef _FFR_REPLACE_RULES char *repfile = NULL; #endif /* _FFR_REPLACE_RULES */ char *ilist = NULL; char *omitlist = NULL; char *alwayslist = NULL; char *siglimit = NULL; char *remarlist = NULL; #if POPAUTH char *popdbfile = NULL; #endif /* POPAUTH */ char *elist = NULL; char *canonstr = NULL; char *signalgstr = NULL; char *macrolist = NULL; char *testfile = NULL; char *testpubkeys = NULL; char *signhdrs = NULL; unsigned char *s33krit = NULL; struct config *cfg = NULL; char *end; char argstr[MAXARGV]; char confstr[BUFRSZ + 1]; /* initialize */ signbytes = -1L; milterv2 = FALSE; testmode = FALSE; addxhdr = FALSE; dolog = FALSE; dolog_success = FALSE; subdomains = FALSE; remarall = FALSE; remsigs = FALSE; use_ssp_deny = FALSE; #ifdef QUERY_CACHE querycache = FALSE; #endif /* QUERY_CACHE */ sock = NULL; dompats = NULL; omithdrs = NULL; alwayshdrs = NULL; #ifdef _FFR_SELECTOR_HEADER selectorhdr = NULL; #endif /* _FFR_SELECTOR_HEADER */ #if POPAUTH popdb = NULL; #endif /* POPAUTH */ send_reports = FALSE; keyhead = NULL; keytail = NULL; no_i_whine = TRUE; selector = NULL; #ifdef _FFR_ZTAGS diagdir = NULL; #endif /* _FFR_ZTAGS */ seckey = NULL; libdkim = NULL; domains = NULL; mtas = NULL; macros = NULL; values = NULL; peerlist = NULL; remar = NULL; #ifdef _FFR_REPLACE_RULES replist = NULL; #endif /* _FFR_REPLACE_RULES */ internal = NULL; exignore = NULL; #ifdef _FFR_VBR vbr_deftype = NULL; vbr_defcert = NULL; vbr_trusted = NULL; #endif /* _FFR_VBR */ quarantine = FALSE; hdrcanon = DKIM_CANON_DEFAULT; bodycanon = DKIM_CANON_DEFAULT; tmo = DEFTIMEOUT; maxhdrsz = DEFMAXHDRSZ; sigmin = 0; sigmintype = SIGMIN_BYTES; if (DKIM_DEBUG('t')) { thread_count = 0; pthread_mutex_init(&count_lock, NULL); } progname = (p = strrchr(argv[0], '/')) == NULL ? argv[0] : p + 1; /* process command line options */ while ((c = getopt(argc, argv, args)) != -1) { switch (c) { case 'a': if (optarg == NULL || *optarg == '\0') return usage(); peerfile = optarg; break; case 'A': autorestart = TRUE; break; case 'b': if (optarg == NULL || *optarg == '\0') return usage(); be = optarg; break; case 'c': if (optarg == NULL || *optarg == '\0') return usage(); canonstr = optarg; break; case 'C': if (optarg == NULL || *optarg == '\0') return usage(); config = optarg; break; case 'd': if (optarg == NULL || *optarg == '\0') return usage(); domlist = strdup(optarg); if (domlist == NULL) { fprintf(stderr, "%s: strdup(): %s\n", progname, strerror(errno)); return EX_SOFTWARE; } break; case 'D': subdomains = TRUE; break; case 'f': dofork = FALSE; break; case 'F': if (optarg == NULL || *optarg == '\0') return usage(); errno = 0; if (optarg[0] == '-') { errno = ERANGE; fixedtime = ULONG_MAX; } else { fixedtime = strtoul(optarg, &p, 10); } if (fixedtime == ULONG_MAX || errno != 0 || *p != '\0') { fprintf(stderr, "%s: invalid time value\n", progname); return EX_USAGE; } break; case 'h': addxhdr = TRUE; break; case 'i': if (optarg == NULL || *optarg == '\0') return usage(); ilist = optarg; break; case 'I': if (optarg == NULL || *optarg == '\0') return usage(); elist = optarg; break; case 'k': if (optarg == NULL || *optarg == '\0') return usage(); keyfile = optarg; break; case 'K': multikey = TRUE; break; case 'l': dolog = TRUE; break; case 'L': if (optarg == NULL || *optarg == '\0') return usage(); siglimit = optarg; break; case 'm': if (optarg == NULL || *optarg == '\0') return usage(); mtalist = optarg; break; case 'M': if (optarg == NULL || *optarg == '\0') return usage(); macrolist = optarg; break; case 'o': if (optarg == NULL || *optarg == '\0') return usage(); omitlist = optarg; break; case 'p': if (optarg == NULL || *optarg == '\0') return usage(); sock = optarg; (void) smfi_setconn(optarg); gotp = TRUE; break; case 'P': if (optarg == NULL || *optarg == '\0') return usage(); pidfile = optarg; break; case 'q': quarantine = TRUE; break; #if _FFR_REQUIRED_HEADERS case 'r': req_hdrs = TRUE; break; #endif /* _FFR_REQUIRED_HEADERS */ case 'R': send_reports = TRUE; break; case 's': if (optarg == NULL || *optarg == '\0') return usage(); selector = optarg; break; case 'S': if (optarg == NULL || *optarg == '\0') return usage(); signalgstr = optarg; break; case 't': if (optarg == NULL || *optarg == '\0') return usage(); testmode = TRUE; testfile = optarg; break; case 'T': if (optarg == NULL || *optarg == '\0') return usage(); errno = 0; if (optarg[0] == '-') { errno = ERANGE; tmpl = ULONG_MAX; } else { tmpl = strtoul(optarg, &p, 10); } if (tmpl > UINT_MAX || errno != 0 || *p != '\0') { fprintf(stderr, "%s: invalid value for -%c\n", progname, c); return EX_USAGE; } tmo = (unsigned int) tmpl; break; case 'u': if (optarg == NULL || *optarg == '\0') return usage(); become = optarg; break; #if POPAUTH case 'U': if (optarg == NULL || *optarg == '\0') return usage(); popdbfile = optarg; break; #endif /* POPAUTH */ case 'v': verbose++; break; case 'V': printf("%s: %s v%s\n", progname, DKIMF_PRODUCT, DKIMF_VERSION); printf("\tCompiled with %s\n", SSLeay_version(SSLEAY_VERSION)); printf("\tSupported signing algorithms:\n"); for (c = 0; dkimf_sign[c].str != NULL; c++) printf("\t\t%s\n", dkimf_sign[c].str); printf("\tSupported canonicalization algorithms:\n"); for (c = 0; dkimf_canon[c].str != NULL; c++) printf("\t\t%s\n", dkimf_canon[c].str); dkimf_optlist(stdout); return EX_OK; case 'x': if (optarg == NULL || *optarg == '\0') { return usage(); } else { u_int line = 0; char *missing; FILE *cf; cf = fopen(optarg, "r"); if (cf == NULL) { fprintf(stderr, "%s: %s: fopen(): %s\n", progname, optarg, strerror(errno)); return EX_UNAVAILABLE; } cfg = config_load(cf, dkimf_config, &line); fclose(cf); if (cfg == NULL) { fprintf(stderr, "%s: %s: error at line %u\n", progname, optarg, line); return EX_CONFIG; } missing = config_check(cfg, dkimf_config); if (missing != NULL) { fprintf(stderr, "%s: %s: required parameter \"%s\" missing\n", progname, optarg, missing); config_free(cfg); return EX_CONFIG; } } break; default: return usage(); } } if (optind != argc) return usage(); if (dkim_ssl_version() != OPENSSL_VERSION_NUMBER) { fprintf(stderr, "%s: incompatible OpenSSL versions (library = 0x%09lx, filter = %09lx)\n", progname, dkim_ssl_version(), OPENSSL_VERSION_NUMBER); return EX_SOFTWARE; } memset(confstr, '\0', sizeof confstr); /* use what was found in the configuration file, if any */ if (cfg != NULL) { char *tmp = NULL; dkimf_parseconfig2(cfg, "On-Default", "def", confstr, sizeof confstr); dkimf_parseconfig2(cfg, "On-BadSignature", "bad", confstr, sizeof confstr); dkimf_parseconfig2(cfg, "On-DNSError", "dns", confstr, sizeof confstr); dkimf_parseconfig2(cfg, "On-InternalError", "int", confstr, sizeof confstr); dkimf_parseconfig2(cfg, "On-NoSignature", "no", confstr, sizeof confstr); dkimf_parseconfig2(cfg, "On-SignatureMissing", "miss", confstr, sizeof confstr); dkimf_parseconfig2(cfg, "On-Security", "sec", confstr, sizeof confstr); if (peerfile == NULL) { (void) config_get(cfg, "PeerList", &peerfile, sizeof peerfile); } if (!autorestart) { (void) config_get(cfg, "AutoRestart", &autorestart, sizeof autorestart); } (void) config_get(cfg, "BodyLengths", &blen, sizeof blen); (void) config_get(cfg, "Diagnostics", &ztags, sizeof ztags); #ifdef _FFR_ZTAGS (void) config_get(cfg, "DiagnosticDirectory", &diagdir, sizeof diagdir); #endif /* _FFR_ZTAGS */ if (be == NULL) (void) config_get(cfg, "Mode", &be, sizeof be); if (canonstr == NULL) { (void) config_get(cfg, "Canonicalization", &canonstr, sizeof canonstr); } if (domlist == NULL) { (void) config_get(cfg, "Domain", &tmp, sizeof tmp); if (tmp != NULL) { domlist = strdup(tmp); if (domlist == NULL) { fprintf(stderr, "%s: strdup(): %s\n", progname, strerror(errno)); config_free(cfg); return EX_OSERR; } } } if (!subdomains) { (void) config_get(cfg, "SubDomains", &subdomains, sizeof subdomains); } if (dofork) { (void) config_get(cfg, "Background", &dofork, sizeof dofork); } if (!addxhdr) { (void) config_get(cfg, "X-Header", &addxhdr, sizeof addxhdr); } if (ilist == NULL) { (void) config_get(cfg, "InternalHosts", &ilist, sizeof ilist); } if (elist == NULL) { (void) config_get(cfg, "ExternalIgnoreList", &elist, sizeof elist); } if (keyfile == NULL) { (void) config_get(cfg, "KeyList", &keyfile, sizeof keyfile); if (keyfile != NULL) multikey = TRUE; } if (keyfile == NULL) { (void) config_get(cfg, "KeyFile", &keyfile, sizeof keyfile); } if (omitlist == NULL) { (void) config_get(cfg, "OmitHeaders", &omitlist, sizeof omitlist); } (void) config_get(cfg, "AlwaysSignHeaders", &alwayslist, sizeof alwayslist); if (!dolog) (void) config_get(cfg, "Syslog", &dolog, sizeof dolog); (void) config_get(cfg, "SyslogSuccess", &dolog_success, sizeof dolog_success); if (siglimit == NULL) { (void) config_get(cfg, "Minimum", &siglimit, sizeof siglimit); } if (mtalist == NULL) { (void) config_get(cfg, "MTA", &mtalist, sizeof mtalist); } if (macrolist == NULL) { (void) config_get(cfg, "MacroList", ¯olist, sizeof macrolist); } if (!gotp) { (void) config_get(cfg, "Socket", &sock, sizeof sock); if (sock != NULL) { gotp = TRUE; (void) smfi_setconn(sock); } } if (pidfile == NULL) { (void) config_get(cfg, "PidFile", &pidfile, sizeof pidfile); } if (!quarantine) { (void) config_get(cfg, "Quarantine", &quarantine, sizeof quarantine); } #if _FFR_REQUIRED_HEADERS if (!req_hdrs) { (void) config_get(cfg, "RequiredHeaders", &req_hdrs, sizeof req_hdrs); } #endif /* _FFR_REQUIRED_HEADERS */ if (!send_reports) { (void) config_get(cfg, "SendReports", &send_reports, sizeof send_reports); } if (selector == NULL) { (void) config_get(cfg, "Selector", &selector, sizeof selector); } if (signalgstr == NULL) { (void) config_get(cfg, "SignatureAlgorithm", &signalgstr, sizeof signalgstr); } if (tmo == DEFTIMEOUT) (void) config_get(cfg, "DNSTimeout", &tmo, sizeof tmo); maxsign = -1; (void) config_get(cfg, "MaximumSignedBytes", &maxsign, sizeof maxsign); if (maxsign != -1) { signbytes = (long) maxsign; blen = TRUE; } if (become == NULL) { (void) config_get(cfg, "Userid", &become, sizeof become); } #if POPAUTH if (popdbfile == NULL) { (void) config_get(cfg, "POPDBFile", &popdbfile, sizeof popdbfile); } #endif /* POPAUTH */ (void) config_get(cfg, "SignHeaders", &signhdrs, sizeof signhdrs); #if _FFR_VBR (void) config_get(cfg, "VBR-Type", &vbr_deftype, sizeof vbr_deftype); (void) config_get(cfg, "VBR-Certifiers", &vbr_defcert, sizeof vbr_defcert); tmp = NULL; (void) config_get(cfg, "VBR-TrustedCertifiers", &tmp, sizeof tmp); if (tmp != NULL) { for (p = tmp, c = 1; *p != '\0'; p++) { if (*p == ':' || *p == ',') c++; } vbr_trusted = (char **) malloc(sizeof(char *) * (c + 1)); if (vbr_trusted == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_OSERR; } for (p = strtok(tmp, ",:"), c = 0; p != NULL; p = strtok(NULL, ",:"), c++) { vbr_trusted[c] = p; vbr_trusted[c + 1] = NULL; } } #endif /* _FFR_VBR */ (void) config_get(cfg, "TestPublicKeys", &testpubkeys, sizeof testpubkeys); #ifdef _FFR_STATS (void) config_get(cfg, "Statistics", &statspath, sizeof statspath); #endif /* _FFR_STATS */ (void) config_get(cfg, "ClockDrift", &clockdrift, sizeof clockdrift); (void) config_get(cfg, "SignatureTTL", &sigttl, sizeof sigttl); (void) config_get(cfg, "RemoveARFrom", &remarlist, sizeof remarlist); (void) config_get(cfg, "RemoveARAll", &remarall, sizeof remarall); (void) config_get(cfg, "RemoveOldSignatures", &remsigs, sizeof remsigs); (void) config_get(cfg, "UseSSPDeny", &use_ssp_deny, sizeof use_ssp_deny); (void) config_get(cfg, "StrictTestMode", &stricttest, sizeof stricttest); (void) config_get(cfg, "UMask", &filemask, sizeof filemask); #ifdef _FFR_SELECTOR_HEADER (void) config_get(cfg, "SelectorHeader", &selectorhdr, sizeof selectorhdr); #endif /* _FFR_SELECTOR_HEADER */ #ifdef _FFR_REPLACE_RULES (void) config_get(cfg, "ReplaceRules", &repfile, sizeof repfile); #endif /* _FFR_REPLACE_RULES */ #ifdef QUERY_CACHE (void) config_get(cfg, "QueryCache", &querycache, sizeof querycache); #endif /* QUERY_CACHE */ (void) config_get(cfg, "MaximumHeaders", &maxhdrsz, sizeof maxhdrsz); } #ifndef SMFIF_QUARANTINE if (quarantine) { fprintf(stderr, "%s: quarantine service not available\n", progname); return EX_SOFTWARE; } #endif /* ! SMFIF_QUARANTINE */ if (config == NULL && confstr[0] != '\0') config = confstr; if (!dkimf_parseconfig(config)) return EX_USAGE; if (!gotp && !testmode) { fprintf(stderr, "%s: milter socket must be specified\n", progname); if (argc == 1) fprintf(stderr, "\t(use \"-?\" for help)\n"); return EX_CONFIG; } if (siglimit != NULL) { errno = 0; if (siglimit[0] == '-') { tmpl = ULONG_MAX; errno = ERANGE; } tmpl = strtoul(siglimit, &p, 10); if (tmpl > UINT_MAX || errno != 0) { fprintf(stderr, "%s: invalid value for -L: `%s'\n", progname, siglimit); return EX_USAGE; } sigmin = (unsigned int) tmpl; if (*p == '%') { if (sigmin > 100) { fprintf(stderr, "%s: invalid value for -L: `%s'\n", progname, siglimit); return EX_USAGE; } sigmintype = SIGMIN_PERCENT; } else if (*p == '+') { sigmintype = SIGMIN_MAXADD; } else if (*p != '\0') { fprintf(stderr, "%s: invalid value for -L: `%s'\n", progname, siglimit); return EX_USAGE; } } if (be == NULL) { mode = (testmode ? DKIMF_MODE_VERIFIER : DKIMF_MODE_DEFAULT); } else { mode = 0; for (p = be; *p != '\0'; p++) { switch (*p) { case 's': mode |= DKIMF_MODE_SIGNER; break; case 'v': mode |= DKIMF_MODE_VERIFIER; break; default: fprintf(stderr, "%s: unknown role flag '%c'\n", progname, *p); return EX_USAGE; } } } if ((mode & DKIMF_MODE_SIGNER) != 0 && ((multikey && keyfile == NULL) || (!multikey && (keyfile == NULL || selector == NULL)))) { fprintf(stderr, "%s: at least one selector and key required for signing mode\n", progname); return EX_USAGE; } if (canonstr != NULL) { p = strchr(canonstr, '/'); if (p == NULL) { hdrcanon = dkimf_configlookup(canonstr, dkimf_canon); if (hdrcanon == -1) { fprintf(stderr, "%s: unknown canonicalization \"%s\"\n", progname, canonstr); return EX_USAGE; } bodycanon = DKIM_CANON_DEFAULT; } else { *p = '\0'; hdrcanon = dkimf_configlookup(canonstr, dkimf_canon); if (hdrcanon == -1) { fprintf(stderr, "%s: unknown canonicalization \"%s\"\n", progname, canonstr); return EX_USAGE; } bodycanon = dkimf_configlookup(p + 1, dkimf_canon); if (bodycanon == -1) { fprintf(stderr, "%s: unknown canonicalization \"%s\"\n", progname, p + 1); return EX_USAGE; } *p = '/'; } } if (signalgstr != NULL) { signalg = dkimf_configlookup(signalgstr, dkimf_sign); if (signalg == -1) { fprintf(stderr, "%s: unknown signature algorithm \"%s\"\n", progname, signalgstr); return EX_USAGE; } } else { signalg = DKIM_SIGN_DEFAULT; } if (domlist != NULL) { int n; bool makepats = FALSE; if (domlist[0] == '/' && access(domlist, F_OK) == 0) { int nalloc = 0; FILE *f; char line[BUFRSZ + 1]; n = 0; f = fopen(domlist, "r"); if (f == NULL) { fprintf(stderr, "%s: %s: fopen(): %s\n", progname, domlist, strerror(errno)); return EX_UNAVAILABLE; } memset(line, '\0', sizeof line); while (fgets(line, BUFRSZ, f) != NULL) { for (p = line; *p != '\0'; p++) { if (*p == '\n' || *p == '#') { *p = '\0'; break; } } dkimf_trimspaces(line); if (strlen(line) == 0) continue; if (nalloc <= n) { if (nalloc == 0) { domains = (char **) malloc(2 * sizeof(char *)); nalloc = 1; } else { domains = (char **) realloc(domains, (nalloc * 2 + 1) * sizeof(char *)); nalloc = nalloc * 2; } if (domains == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } } domains[n] = strdup(line); if (domains[n] == NULL) { fprintf(stderr, "%s: strdup(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } if (strchr(domains[n], '*') != NULL) makepats = TRUE; n++; } if (domains != NULL) domains[n] = NULL; fclose(f); } else { n = 1; for (p = domlist; *p != '\0'; p++) { if (*p == ',') n++; } domains = (char **) malloc((n + 1) * sizeof(char *)); if (domains == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } n = 0; for (p = strtok(domlist, ","); p != NULL; p = strtok(NULL, ",")) { domains[n] = p; if (strchr(domains[n], '*') != NULL) makepats = TRUE; n++; } domains[n] = NULL; } if (makepats) { char *end; char *q; char patbuf[BUFRSZ + 1]; dompats = (regex_t **) malloc ((n + 1) * sizeof (regex_t *)); if (dompats == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } memset(dompats, '\0', (n + 1) * sizeof (regex_t *)); for (c = 0; c < n; c++) { memset(patbuf, '\0', sizeof patbuf); end = patbuf + sizeof patbuf; patbuf[0] = '^'; for (p = domains[c], q = patbuf + 1; *p != '\0' && q < end; p++) { switch (*p) { case '*': *q = '.'; q++; *q = '*'; q++; break; case '.': *q = '\\'; q++; *q = '.'; q++; break; default: *q = *p; q++; break; } } *q++ = '$'; if (q >= end) { fprintf(stderr, "%s: regular expression for \"%s\" too large\n", progname, domains[c]); return EX_UNAVAILABLE; } dompats[c] = (regex_t *) malloc(sizeof(regex_t)); if (dompats[c] == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } status = regcomp(dompats[c], patbuf, (REG_EXTENDED|REG_ICASE)); if (status != 0) { (void) regerror(status, dompats[c], patbuf, sizeof patbuf); fprintf(stderr, "%s: regcomp(): %s\n", progname, patbuf); return EX_UNAVAILABLE; } } } } if (omitlist != NULL) { int n = 1; for (p = omitlist; *p != '\0'; p++) { if (*p == ',') n++; } omithdrs = (char **) malloc((n + 1) * sizeof(char *)); if (omithdrs == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } n = 0; for (p = strtok(omitlist, ","); p != NULL; p = strtok(NULL, ",")) omithdrs[n++] = p; omithdrs[n] = NULL; } if (alwayslist != NULL) { int n = 1; for (p = alwayslist; *p != '\0'; p++) { if (*p == ',') n++; } alwayshdrs = (char **) malloc((n + 1) * sizeof(char *)); if (alwayshdrs == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } n = 0; for (p = strtok(alwayslist, ","); p != NULL; p = strtok(NULL, ",")) alwayshdrs[n++] = p; alwayshdrs[n] = NULL; } if (macrolist != NULL) { int n = 1; char *macrocopy; for (p = macrolist; *p != '\0'; p++) { if (*p == ',') n++; } macros = (char **) malloc((n + 1) * sizeof(char *)); values = (char **) malloc((n + 1) * sizeof(char *)); macrocopy = strdup(macrolist); if (macros == NULL || values == NULL || macrocopy == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } n = 0; for (p = strtok(macrocopy, ","); p != NULL; p = strtok(NULL, ",")) { macros[n] = p; values[n] = strchr(p, '='); if (values[n] != NULL) { *(values[n]) = '\0'; values[n] += 1; } n++; } macros[n] = NULL; values[n] = NULL; } if (mtalist != NULL) { int n = 1; for (p = mtalist; *p != '\0'; p++) { if (*p == ',') n++; } mtas = (char **) malloc((n + 1) * sizeof(char *)); if (mtas == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } n = 0; for (p = strtok(mtalist, ","); p != NULL; p = strtok(NULL, ",")) mtas[n++] = p; mtas[n] = NULL; } if (remarlist != NULL) { int n = 1; for (p = remarlist; *p != '\0'; p++) { if (*p == ',') n++; } remar = (char **) malloc((n + 1) * sizeof(char *)); if (remar == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } n = 0; for (p = strtok(remarlist, ","); p != NULL; p = strtok(NULL, ",")) remar[n++] = p; remar[n] = NULL; } /* peer list */ if (peerfile != NULL && !testmode) { FILE *f; f = fopen(peerfile, "r"); if (f == NULL) { fprintf(stderr, "%s: %s: fopen(): %s\n", progname, peerfile, strerror(errno)); return EX_UNAVAILABLE; } if (!dkimf_load_list(f, NULL, &peerlist)) { fclose(f); return EX_UNAVAILABLE; } fclose(f); } /* internal list */ if (ilist != NULL && !testmode) { FILE *f; f = fopen(ilist, "r"); if (f == NULL) { fprintf(stderr, "%s: %s: fopen(): %s\n", progname, ilist, strerror(errno)); return EX_UNAVAILABLE; } if (!dkimf_load_list(f, NULL, &internal)) { fclose(f); return EX_UNAVAILABLE; } fclose(f); } else { if (!dkimf_load_list(NULL, defilist, &internal)) return EX_UNAVAILABLE; } /* external ignore list */ if (elist != NULL && !testmode) { FILE *f; f = fopen(elist, "r"); if (f == NULL) { fprintf(stderr, "%s: %s: fopen(): %s\n", progname, elist, strerror(errno)); return EX_UNAVAILABLE; } if (!dkimf_load_list(f, NULL, &exignore)) { fclose(f); return EX_UNAVAILABLE; } fclose(f); } /* suppress a bunch of things if we're in test mode */ if (testmode) { dolog = FALSE; autorestart = FALSE; dofork = FALSE; become = NULL; pidfile = NULL; } #ifdef _FFR_REPLACE_RULES /* replacement list */ if (repfile != NULL) { FILE *f; f = fopen(repfile, "r"); if (f == NULL) { fprintf(stderr, "%s: %s: fopen(): %s\n", progname, repfile, strerror(errno)); return EX_UNAVAILABLE; } if (!dkimf_load_replist(f, &replist)) { fclose(f); return EX_UNAVAILABLE; } fclose(f); } #endif /* _FFR_REPLACE_RULES */ /* activate logging */ if (dolog) { #ifdef LOG_MAIL openlog(progname, LOG_PID, LOG_MAIL); #else /* LOG_MAIL */ openlog(progname, LOG_PID); #endif /* LOG_MAIL */ } dkimf_setmaxfd(); /* change user if appropriate */ if (become != NULL) { gid_t gid; char *colon; struct passwd *pw; struct group *gr = NULL; /* see if there was a group specified; if so, validate */ colon = strchr(become, ':'); if (colon != NULL) { *colon = '\0'; gr = getgrnam(colon + 1); if (gr == NULL) { char *q; gid = (gid_t) strtol(colon + 1, &q, 10); if (*q == '\0') gr = getgrgid(gid); if (gr == NULL) { if (dolog) { syslog(LOG_ERR, "no such group or gid `%s'", colon + 1); } fprintf(stderr, "%s: no such group `%s'\n", progname, colon + 1); return EX_DATAERR; } } } /* validate the user */ pw = getpwnam(become); if (pw == NULL) { char *q; uid_t uid; uid = (uid_t) strtol(become, &q, 10); if (*q == '\0') pw = getpwuid(uid); if (pw == NULL) { if (dolog) { syslog(LOG_ERR, "no such user or uid `%s'", become); } fprintf(stderr, "%s: no such user `%s'\n", progname, become); return EX_DATAERR; } } if (gr == NULL) gid = pw->pw_gid; /* make all the process changes */ if (getuid() != pw->pw_uid) { if (initgroups(pw->pw_name, gid) != 0) { if (dolog) { syslog(LOG_ERR, "initgroups(): %s", strerror(errno)); } fprintf(stderr, "%s: initgroups(): %s\n", progname, strerror(errno)); return EX_NOPERM; } else if (setgid(gid) != 0) { if (dolog) { syslog(LOG_ERR, "setgid(): %s", strerror(errno)); } fprintf(stderr, "%s: setgid(): %s\n", progname, strerror(errno)); return EX_NOPERM; } else if (setuid(pw->pw_uid) != 0) { if (dolog) { syslog(LOG_ERR, "setuid(): %s", strerror(errno)); } fprintf(stderr, "%s: setuid(): %s\n", progname, strerror(errno)); return EX_NOPERM; } } (void) endpwent(); } /* load the secret key, if one was specified */ if (keyfile != NULL && multikey) { status = dkimf_loadkeys(keyfile); if (status != 0) return EX_UNAVAILABLE; } else if (keyfile != NULL) { int fd; size_t rlen; struct stat s; status = stat(keyfile, &s); if (status != 0) { if (dolog) { int saveerrno; saveerrno = errno; syslog(LOG_ERR, "%s: stat(): %s", keyfile, strerror(errno)); errno = saveerrno; } fprintf(stderr, "%s: %s: stat(): %s\n", progname, keyfile, strerror(errno)); return EX_UNAVAILABLE; } s33krit = malloc(s.st_size + 1); if (s33krit == NULL) { if (dolog) { int saveerrno; saveerrno = errno; syslog(LOG_ERR, "malloc(): %s", strerror(errno)); errno = saveerrno; } fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); return EX_UNAVAILABLE; } keylen = s.st_size + 1; fd = open(keyfile, O_RDONLY, 0); if (fd < 0) { if (dolog) { int saveerrno; saveerrno = errno; syslog(LOG_ERR, "%s: open(): %s", keyfile, strerror(errno)); errno = saveerrno; } fprintf(stderr, "%s: %s: open(): %s\n", progname, keyfile, strerror(errno)); free(s33krit); return EX_UNAVAILABLE; } rlen = read(fd, s33krit, s.st_size + 1); if (rlen == -1) { if (dolog) { int saveerrno; saveerrno = errno; syslog(LOG_ERR, "%s: read(): %s", keyfile, strerror(errno)); errno = saveerrno; } fprintf(stderr, "%s: %s: read(): %s\n", progname, keyfile, strerror(errno)); close(fd); free(s33krit); return EX_UNAVAILABLE; } else if (rlen != s.st_size) { if (dolog) { syslog(LOG_ERR, "%s: read() wrong size (%u)", keyfile, rlen); } fprintf(stderr, "%s: %s: read() wrong size (%u)\n", progname, keyfile, rlen); close(fd); free(s33krit); return EX_UNAVAILABLE; } close(fd); s33krit[s.st_size] = '\0'; seckey = s33krit; } die = FALSE; if (autorestart) { bool quitloop = FALSE; int status; pid_t pid; pid_t wpid; struct sigaction sa; if (dofork) { pid = fork(); switch (pid) { case -1: if (dolog) { int saveerrno; saveerrno = errno; syslog(LOG_ERR, "fork(): %s", strerror(errno)); errno = saveerrno; } fprintf(stderr, "%s: fork(): %s\n", progname, strerror(errno)); dkimf_zapkey(); return EX_OSERR; case 0: dkimf_stdio(); break; default: dkimf_zapkey(); return EX_OK; } } if (pidfile != NULL) { f = fopen(pidfile, "w"); if (f != NULL) { fprintf(f, "%ld\n", (long) getpid()); (void) fclose(f); } else { if (dolog) { syslog(LOG_ERR, "can't write pid to %s: %s", pidfile, strerror(errno)); } } } sa.sa_handler = dkimf_sighandler; /* XXX -- HAHAHAH => sa.sa_sigaction = NULL; */ sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGHUP); sigaddset(&sa.sa_mask, SIGINT); sigaddset(&sa.sa_mask, SIGTERM); sa.sa_flags = 0; if (sigaction(SIGHUP, &sa, NULL) != 0 || sigaction(SIGINT, &sa, NULL) != 0 || sigaction(SIGTERM, &sa, NULL) != 0) { if (dolog) { syslog(LOG_ERR, "[parent] sigaction(): %s", strerror(errno)); } } while (!quitloop) { if (dkimf_socket_cleanup(sock) != 0) { syslog(LOG_ERR, "[parent] socket cleanup failed: %s", strerror(errno)); return EX_UNAVAILABLE; } pid = fork(); switch (pid) { case -1: if (dolog) { syslog(LOG_ERR, "fork(): %s", strerror(errno)); } dkimf_zapkey(); return EX_OSERR; case 0: sa.sa_handler = SIG_DFL; if (sigaction(SIGHUP, &sa, NULL) != 0 || sigaction(SIGINT, &sa, NULL) != 0 || sigaction(SIGTERM, &sa, NULL) != 0) { if (dolog) { syslog(LOG_ERR, "[child] sigaction(): %s", strerror(errno)); } } quitloop = TRUE; break; default: for (;;) { wpid = wait(&status); if (wpid == -1 && errno == EINTR) { if (die) { dkimf_killchild(pid, diesig); dkimf_zapkey(); if (pidfile != NULL) (void) unlink(pidfile); exit(EX_OK); } } if (pid != wpid) continue; if (wpid != -1 && dolog) { if (WIFSIGNALED(status)) { syslog(LOG_NOTICE, "terminated with signal %d, restarting", WTERMSIG(status)); } else if (WIFEXITED(status)) { syslog(LOG_NOTICE, "exited with status %d, restarting", WEXITSTATUS(status)); } } break; } break; } } } if (filemask != -1) (void) umask((mode_t) filemask); if (!testmode) { /* register with the milter interface */ if (smfi_register(smfilter) == MI_FAILURE) { if (dolog) syslog(LOG_ERR, "smfi_register() failed"); fprintf(stderr, "%s: smfi_register() failed\n", progname); dkimf_zapkey(); if (!autorestart && pidfile != NULL) (void) unlink(pidfile); return EX_UNAVAILABLE; } /* try to establish the milter socket */ if (smfi_opensocket(FALSE) == MI_FAILURE) { if (dolog) syslog(LOG_ERR, "smfi_opensocket() failed"); fprintf(stderr, "%s: smfi_opensocket() failed\n", progname); dkimf_zapkey(); return EX_UNAVAILABLE; } } if (!autorestart && dofork) { pid_t pid; pid = fork(); switch (pid) { case -1: if (dolog) { int saveerrno; saveerrno = errno; syslog(LOG_ERR, "fork(): %s", strerror(errno)); errno = saveerrno; } fprintf(stderr, "%s: fork(): %s\n", progname, strerror(errno)); dkimf_zapkey(); return EX_OSERR; case 0: dkimf_stdio(); break; default: dkimf_zapkey(); return EX_OK; } } /* write out the pid */ if (!autorestart && pidfile != NULL) { f = fopen(pidfile, "w"); if (f != NULL) { fprintf(f, "%ld\n", (long) getpid()); (void) fclose(f); } else { if (dolog) { syslog(LOG_ERR, "can't write pid to %s: %s", pidfile, strerror(errno)); } } } ERR_load_crypto_strings(); /* initialize the DKIM package */ libdkim = dkim_init(NULL, NULL); if (libdkim == NULL) { if (dolog) syslog(LOG_ERR, "can't initialize DKIM library"); dkimf_zapkey(); if (!autorestart && pidfile != NULL) (void) unlink(pidfile); return EX_UNAVAILABLE; } else { u_int opts; (void) dkim_options(libdkim, DKIM_OP_GETOPT, DKIM_OPTS_FLAGS, &opts, sizeof opts); opts |= DKIM_LIBFLAGS_ACCEPTV05; #ifdef QUERY_CACHE if (querycache) { opts |= DKIM_LIBFLAGS_CACHE; (void) time(&cache_lastlog); } #endif /* QUERY_CACHE */ (void) dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_FLAGS, &opts, sizeof opts); } #ifdef USE_ARLIB /* set the DNS callback */ (void) dkim_set_dns_callback(dfc->mctx_dkim, dkimf_sendprogress, CBINTERVAL); #endif /* USE_ARLIB */ (void) dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_SKIPHDRS, (void *) should_not_signhdrs, sizeof (u_char **)); (void) dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_TIMEOUT, &tmo, sizeof tmo); if (sigttl != 0) { time_t sigtime = (time_t) sigttl; (void) dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_SIGNATURETTL, &sigtime, sizeof sigtime); } if (clockdrift != 0) { time_t drift = (time_t) clockdrift; (void) dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_CLOCKDRIFT, &drift, sizeof drift); } if (send_reports || DKIM_DEBUG('c') || blen || ztags) { u_int opts; (void) dkim_options(libdkim, DKIM_OP_GETOPT, DKIM_OPTS_FLAGS, &opts, sizeof opts); opts |= DKIM_LIBFLAGS_TMPFILES; if (DKIM_DEBUG('c')) opts |= DKIM_LIBFLAGS_KEEPFILES; if (blen) opts |= DKIM_LIBFLAGS_SIGNLEN; if (ztags) opts |= DKIM_LIBFLAGS_ZTAGS; (void) dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_FLAGS, &opts, sizeof opts); } if (omithdrs != NULL) { status = dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_SKIPHDRS, omithdrs, sizeof omithdrs); if (status != DKIM_STAT_OK) { fprintf(stderr, "%s: dkim_options(DKIM_OPTS_SKIPHDRS) returned %d\n", progname, status); if (!autorestart && pidfile != NULL) (void) unlink(pidfile); return EX_SOFTWARE; } } if (alwayshdrs != NULL) { status = dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_ALWAYSHDRS, alwayshdrs, sizeof alwayshdrs); if (status != DKIM_STAT_OK) { fprintf(stderr, "%s: dkim_options(DKIM_OPTS_ALWAYSHDRS) returned %d\n", progname, status); if (!autorestart && pidfile != NULL) (void) unlink(pidfile); return EX_SOFTWARE; } } if (signhdrs != NULL) { size_t len; char **signhdrlist; c = 1; for (p = signhdrs; p != NULL; p = strchr(p + 1, ',')) c++; len = (c + 1) * sizeof(char *); signhdrlist = malloc(len); if (signhdrlist == NULL) { fprintf(stderr, "%s: malloc(): %s\n", progname, strerror(errno)); if (!autorestart && pidfile != NULL) (void) unlink(pidfile); return EX_OSERR; } memset(signhdrlist, '\0', len); for (c = 0, p = strtok(signhdrs, ","); p != NULL; c++, p = strtok(NULL, ",")) signhdrlist[c] = p; status = dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_SIGNHDRS, signhdrlist, sizeof signhdrlist); } if (testpubkeys != NULL) { dkim_query_t qtype = DKIM_QUERY_FILE; (void) dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_QUERYMETHOD, &qtype, sizeof qtype); (void) dkim_options(libdkim, DKIM_OP_SETOPT, DKIM_OPTS_QUERYINFO, testpubkeys, strlen(testpubkeys)); } #if VERIFY_DOMAINKEYS libdk = dk_init(NULL, NULL); if (libdk == NULL) { if (dolog) syslog(LOG_ERR, "can't initialize DK library"); if (!autorestart && pidfile != NULL) (void) unlink(pidfile); return EX_UNAVAILABLE; } #endif /* VERIFY_DOMAINKEYS */ /* perform test mode */ if (testfile != NULL) { status = dkimf_testfile(libdkim, testfile, fixedtime, stricttest, verbose); dkim_close(libdkim); return status; } pthread_mutex_init(&popen_lock, NULL); memset(argstr, '\0', sizeof argstr); end = &argstr[sizeof argstr - 1]; n = sizeof argstr; for (c = 1, p = argstr; c < argc && p < end; c++) { if (strchr(argv[c], ' ') != NULL) { status = snprintf(p, n, "%s \"%s\"", c == 1 ? "args:" : "", argv[c]); } else { status = snprintf(p, n, "%s %s", c == 1 ? "args:" : "", argv[c]); } p += status; n -= status; } #if POPAUTH if (popdbfile != NULL) { status = dkimf_initpopauth(); if (status != 0) { fprintf(stderr, "%s: can't initialize mutex: %s\n", progname, strerror(status)); syslog(LOG_ERR, "can't initialize mutex: %s", popdbfile); } status = 0; # if DB_VERSION_MAJOR > 2 status = db_create(&popdb, NULL, 0); if (status == 0) { # if DB_VERSION_MAJOR > 3 status = popdb->open(popdb, NULL, popdbfile, NULL, DB_UNKNOWN, (DB_RDONLY|DB_THREAD), 0); # else /* DB_VERSION_MAJOR > 3 */ status = popdb->open(popdb, popdbfile, NULL, DB_UNKNOWN, (DB_RDONLY|DB_THREAD), 0); # endif /* DB_VERSION_MAJOR > 3 */ } # elif DB_VERSION_MAJOR == 2 status = db_open(popdbfile, DB_HASH, DB_RDONLY, DB_MODE, NULL, NULL, &popdb); # else /* DB_VERSION_MAJOR < 2 */ popdb = dbopen(popdbfile, O_RDONLY, DB_HASH, NULL); if (popdb == NULL) status = errno; # endif /* DB_VERSION_MAJOR */ if (status != 0) { fprintf(stderr, "%s: can't open database %s: %s\n", progname, popdbfile, db_strerror(errno)); syslog(LOG_ERR, "can't open database %s", popdbfile); dkimf_zapkey(); if (!autorestart && pidfile != NULL) (void) unlink(pidfile); return EX_UNAVAILABLE; } } #endif /* POPAUTH */ #ifdef _FFR_STATS dkimf_stats_init(); #endif /* _FFR_STATS */ if (dolog) { syslog(LOG_INFO, "%s v%s starting (%s)", DKIMF_PRODUCT, DKIMF_VERSION, argstr); } /* call the milter mainline */ errno = 0; status = smfi_main(); if (dolog) { syslog(LOG_INFO, "%s v%s terminating with status %d, errno = %d", DKIMF_PRODUCT, DKIMF_VERSION, status, errno); } #if POPAUTH if (popdb != NULL) { # if DB_VERSION_MAJOR < 2 (void) popdb->close(popdb); # else /* DB_VERSION_MAJOR < 2 */ (void) popdb->close(popdb, 0); # endif /* DB_VERSION_MAJOR */ } #endif /* POPAUTH */ dkim_close(libdkim); dkimf_zapkey(); if (!autorestart && pidfile != NULL) (void) unlink(pidfile); return status; }