From: Todd Lyons Date: Mon, 1 Apr 2013 18:33:08 +0000 (-0700) Subject: DMARC support by opendmarc libs X-Git-Tag: exim-4_82_RC1~41 X-Git-Url: https://git.exim.org/exim.git/commitdiff_plain/4840604e4f365545a43f01d0e953ce33afd1c3d5 DMARC support by opendmarc libs --- diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base index 2e2639b85..1500e85ec 100644 --- a/src/OS/Makefile-Base +++ b/src/OS/Makefile-Base @@ -298,7 +298,7 @@ convert4r4: Makefile ../src/convert4r4.src OBJ_WITH_CONTENT_SCAN = malware.o mime.o regex.o spam.o spool_mbox.o OBJ_WITH_OLD_DEMIME = demime.o -OBJ_EXPERIMENTAL = bmi_spam.o spf.o srs.o dcc.o +OBJ_EXPERIMENTAL = bmi_spam.o spf.o srs.o dcc.o dmarc.o # Targets for final binaries; the main one has a build number which is # updated each time. We don't bother with that for the auxiliaries. @@ -605,6 +605,7 @@ bmi_spam.o: $(HDRS) bmi_spam.c spf.o: $(HDRS) spf.h spf.c srs.o: $(HDRS) srs.h srs.c dcc.o: $(HDRS) dcc.h dcc.c +dmarc.o: $(HDRS) dmarc.h dmarc.c # The module containing tables of available lookups, routers, auths, and # transports must be rebuilt if any of them are. However, because the makefiles diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks index 62d248a3c..a9abdab25 100755 --- a/src/scripts/MakeLinks +++ b/src/scripts/MakeLinks @@ -241,6 +241,8 @@ ln -s ../src/verify.c verify.c ln -s ../src/version.c version.c ln -s ../src/dkim.c dkim.c ln -s ../src/dkim.h dkim.h +ln -s ../src/dmarc.c dmarc.c +ln -s ../src/dmarc.h dmarc.h ln -s ../src/valgrind.h valgrind.h ln -s ../src/memcheck.h memcheck.h diff --git a/src/src/EDITME b/src/src/EDITME index 7de915ae9..e29c1eb25 100644 --- a/src/src/EDITME +++ b/src/src/EDITME @@ -460,12 +460,16 @@ EXIM_MONITOR=eximon.bin # EXPERIMENTAL_OCSP=yes -# Uncomment the following line to add Per-Recipient-Data-Response support. +# Uncomment the following line to add DMARC checking capability, implemented +# using libopendmarc libraries. +# EXPERIMENTAL_DMARC=yes +# CFLAGS += -I/usr/local/include +# LDFLAGS += -lopendmarc +# Uncomment the following line to add Per-Recipient-Data-Response support. # EXPERIMENTAL_PRDR=yes - ############################################################################### # THESE ARE THINGS YOU MIGHT WANT TO SPECIFY # ############################################################################### diff --git a/src/src/acl.c b/src/src/acl.c index f61d2dfdf..eb2179610 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -66,6 +66,9 @@ enum { ACLC_ACL, #ifndef DISABLE_DKIM ACLC_DKIM_SIGNER, ACLC_DKIM_STATUS, +#endif +#ifdef EXPERIMENTAL_DMARC + ACLC_DMARC_STATUS, #endif ACLC_DNSLISTS, ACLC_DOMAINS, @@ -129,6 +132,9 @@ static uschar *conditions[] = { #ifndef DISABLE_DKIM US"dkim_signers", US"dkim_status", +#endif +#ifdef EXPERIMENTAL_DMARC + US"dmarc_status", #endif US"dnslists", US"domains", @@ -175,6 +181,10 @@ enum { #ifndef DISABLE_DKIM CONTROL_DKIM_VERIFY, #endif + #ifdef EXPERIMENTAL_DMARC + CONTROL_DMARC_VERIFY, + CONTROL_DMARC_FORENSIC, + #endif CONTROL_DSCP, CONTROL_ERROR, CONTROL_CASEFUL_LOCAL_PART, @@ -211,6 +221,10 @@ static uschar *controls[] = { #ifndef DISABLE_DKIM US"dkim_disable_verify", #endif + #ifdef EXPERIMENTAL_DMARC + US"dmarc_disable_verify", + US"dmarc_enable_forensic", + #endif US"dscp", US"error", US"caseful_local_part", @@ -260,6 +274,9 @@ static uschar cond_expand_at_top[] = { #ifndef DISABLE_DKIM TRUE, /* dkim_signers */ TRUE, /* dkim_status */ +#endif +#ifdef EXPERIMENTAL_DMARC + TRUE, /* dmarc_status */ #endif TRUE, /* dnslists */ FALSE, /* domains */ @@ -321,6 +338,9 @@ static uschar cond_modifiers[] = { #ifndef DISABLE_DKIM FALSE, /* dkim_signers */ FALSE, /* dkim_status */ +#endif +#ifdef EXPERIMENTAL_DMARC + FALSE, /* dmarc_status */ #endif FALSE, /* dnslists */ FALSE, /* domains */ @@ -435,6 +455,11 @@ static unsigned int cond_forbids[] = { ~(1<next) #ifndef DISABLE_DKIM case CONTROL_DKIM_VERIFY: dkim_disable_verify = TRUE; + #ifdef EXPERIMENTAL_DMARC + /* Since DKIM was blocked, skip DMARC too */ + dmarc_disable_verify = TRUE; + dmarc_enable_forensic = FALSE; + #endif + break; + #endif + + #ifdef EXPERIMENTAL_DMARC + case CONTROL_DMARC_VERIFY: + dmarc_disable_verify = TRUE; + break; + + case CONTROL_DMARC_FORENSIC: + dmarc_enable_forensic = TRUE; break; #endif @@ -3275,6 +3326,17 @@ for (; cb != NULL; cb = cb->next) break; #endif + #ifdef EXPERIMENTAL_DMARC + case ACLC_DMARC_STATUS: + if (dmarc_has_been_checked++ == 0) + dmarc_process(); + /* used long way of dmarc_exim_expand_query() in case we need more + * view into the process in the future. */ + rc = match_isinlist(dmarc_exim_expand_query(DMARC_VERIFY_STATUS), + &arg,0,NULL,NULL,MCL_STRING,TRUE,NULL); + break; + #endif + case ACLC_DNSLISTS: rc = verify_check_dnsbl(&arg); break; diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults index b6772c275..1594f6858 100644 --- a/src/src/config.h.defaults +++ b/src/src/config.h.defaults @@ -165,6 +165,7 @@ it's a default value. */ /* EXPERIMENTAL features */ #define EXPERIMENTAL_BRIGHTMAIL #define EXPERIMENTAL_DCC +#define EXPERIMENTAL_DMARC #define EXPERIMENTAL_OCSP #define EXPERIMENTAL_PRDR #define EXPERIMENTAL_SPF diff --git a/src/src/dmarc.c b/src/src/dmarc.c new file mode 100644 index 000000000..85b6ec8fe --- /dev/null +++ b/src/src/dmarc.c @@ -0,0 +1,611 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ +/* Experimental DMARC support. + Copyright (c) Todd Lyons 2012, 2013 + License: GPL */ + +/* Portions Copyright (c) 2012, 2013, The Trusted Domain Project; + All rights reserved, licensed for use per LICENSE.opendmarc. */ + +/* Code for calling dmarc checks via libopendmarc. Called from acl.c. */ + +#include "exim.h" +#ifdef EXPERIMENTAL_DMARC + +#include "functions.h" +#include "dmarc.h" +#include "pdkim/pdkim.h" + +OPENDMARC_LIB_T dmarc_ctx; +DMARC_POLICY_T *dmarc_pctx = NULL; +OPENDMARC_STATUS_T libdm_status, action, dmarc_policy; +OPENDMARC_STATUS_T da, sa, action; +BOOL dmarc_abort = FALSE; +uschar *dmarc_pass_fail = US"skipped"; +extern pdkim_signature *dkim_signatures; +header_line *from_header = NULL; +#ifdef EXPERIMENTAL_SPF +extern SPF_response_t *spf_response; +int dmarc_spf_result = 0; +uschar *spf_sender_domain = NULL; +uschar *spf_human_readable = NULL; +#endif +u_char *header_from_sender = NULL; +int history_file_status = DMARC_HIST_OK; +uschar *history_buffer = NULL; +uschar *dkim_history_buffer= NULL; + +/* Accept an error_block struct, initialize if empty, parse to the + * end, and append the two strings passed to it. Used for adding + * variable amounts of value:pair data to the forensic emails. */ + +static error_block * +add_to_eblock(error_block *eblock, uschar *t1, uschar *t2) +{ + error_block *eb = malloc(sizeof(error_block)); + if (eblock == NULL) + eblock = eb; + else + { + /* Find the end of the eblock struct and point it at eb */ + error_block *tmp = eblock; + while(tmp->next != NULL) + tmp = tmp->next; + tmp->next = eb; + } + eb->text1 = t1; + eb->text2 = t2; + eb->next = NULL; + return eblock; +} + +/* dmarc_init sets up a context that can be re-used for several + messages on the same SMTP connection (that come from the + same host with the same HELO string) */ + +int dmarc_init() { + int *netmask = NULL; /* Ignored */ + int is_ipv6 = 0; + char *tld_file = (dmarc_tld_file == NULL) ? + "/etc/exim/opendmarc.tlds" : + (char *)dmarc_tld_file; + + /* Set some sane defaults. Also clears previous results when + * multiple messages in one connection. */ + dmarc_pctx = NULL; + dmarc_status = US"none"; + dmarc_abort = FALSE; + dmarc_pass_fail = US"skipped"; + dmarc_used_domain = US""; + header_from_sender = NULL; +#ifdef EXPERIMENTAL_SPF + spf_sender_domain = NULL; + spf_human_readable = NULL; +#endif + + /* ACLs have "control=dmarc_disable_verify" */ + if (dmarc_disable_verify == TRUE) + return OK; + + (void) memset(&dmarc_ctx, '\0', sizeof dmarc_ctx); + dmarc_ctx.nscount = 0; + libdm_status = opendmarc_policy_library_init(&dmarc_ctx); + if (libdm_status != DMARC_PARSE_OKAY) + { + log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to init library: %s", + opendmarc_policy_status_to_str(libdm_status)); + dmarc_abort = TRUE; + } + if (opendmarc_tld_read_file(tld_file, NULL, NULL, NULL)) + { + log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to load tld list %s: %d", + tld_file, errno); + dmarc_abort = TRUE; + } + if (sender_host_address == NULL) + dmarc_abort = TRUE; + /* This catches locally originated email and startup errors above. */ + if ( dmarc_abort == FALSE ) + { + is_ipv6 = string_is_ip_address(sender_host_address, netmask); + is_ipv6 = (is_ipv6 == 6) ? TRUE : + (is_ipv6 == 4) ? FALSE : FALSE; + dmarc_pctx = opendmarc_policy_connect_init(sender_host_address, is_ipv6); + if (dmarc_pctx == NULL ) + { + log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure creating policy context: ip=%s", + sender_host_address); + dmarc_abort = TRUE; + } + } + + return OK; +} + + +/* dmarc_store_data stores the header data so that subsequent + * dmarc_process can access the data */ + +int dmarc_store_data(header_line *hdr) { + /* No debug output because would change every test debug output */ + if (dmarc_disable_verify != TRUE) + from_header = hdr; + return OK; +} + + +/* dmarc_process adds the envelope sender address to the existing + context (if any), retrieves the result, sets up expansion + strings and evaluates the condition outcome. */ + +int dmarc_process() { + int sr, origin; /* used in SPF section */ + pdkim_signature *sig = NULL; + BOOL has_dmarc_record = TRUE; + u_char **ruf; /* forensic report addressees, if called for */ + + /* ACLs have "control=dmarc_disable_verify" */ + if (dmarc_disable_verify == TRUE) + { + dmarc_ar_header = dmarc_auth_results_header(from_header, NULL); + return OK; + } + + /* Store the header From: sender domain for this part of DMARC. + * If there is no from_header struct, then it's likely this message + * is locally generated and relying on fixups to add it. Just skip + * the entire DMARC system if we can't find a From: header....or if + * there was a previous error. + */ + if (from_header == NULL || dmarc_abort == TRUE) + dmarc_abort = TRUE; + else + { + /* I strongly encourage anybody who can make this better to contact me directly! + * Is this an insane way to extract the email address from the From: header? + * it's sure a horrid layer-crossing.... + * I'm not denying that :-/ + * there may well be no better though + */ + header_from_sender = expand_string( + string_sprintf("${domain:${extract{1}{:}{${addresses:%s}}}}", + from_header->text) ); + /* The opendmarc library extracts the domain from the email address, but + * only try to store it if it's not empty. Otherwise, skip out of DMARC. */ + if (strcmp( CCS header_from_sender, "") == 0) + dmarc_abort = TRUE; + libdm_status = (dmarc_abort == TRUE) ? + DMARC_PARSE_OKAY : + opendmarc_policy_store_from_domain(dmarc_pctx, header_from_sender); + if (libdm_status != DMARC_PARSE_OKAY) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failure to store header From: in DMARC: %s, header was '%s'", + opendmarc_policy_status_to_str(libdm_status), from_header->text); + dmarc_abort = TRUE; + } + } + + /* Skip DMARC if connection is SMTP Auth. Temporarily, admin should + * instead do this in the ACLs. */ + if (dmarc_abort == FALSE && sender_host_authenticated == NULL) + { +#ifdef EXPERIMENTAL_SPF + /* Use the envelope sender domain for this part of DMARC */ + spf_sender_domain = expand_string(US"$sender_address_domain"); + if ( spf_response == NULL ) + { + /* No spf data means null envelope sender so generate a domain name + * from the sender_host_name || sender_helo_name */ + if (spf_sender_domain == NULL) + { + spf_sender_domain = (sender_host_name == NULL) ? sender_helo_name : sender_host_name; + uschar *subdomain = spf_sender_domain; + int count = 0; + while (subdomain && *subdomain != '.') + { + subdomain++; + count++; + } + /* If parsed characters in temp var "subdomain" and is pointing to + * a period now, get rid of the period and use that. Otherwise + * will use whatever was first set in spf_sender_domain. Goal is to + * generate a sane answer, not necessarily the right/best answer b/c + * at this point with a null sender, it's a bounce message, making + * the spf domain be subjective. */ + if (count > 0 && *subdomain == '.') + { + subdomain++; + spf_sender_domain = subdomain; + } + log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n", + spf_sender_domain); + DEBUG(D_receive) + debug_printf("DMARC using synthesized SPF sender domain = %s\n", spf_sender_domain); + } + dmarc_spf_result = DMARC_POLICY_SPF_OUTCOME_NONE; + origin = DMARC_POLICY_SPF_ORIGIN_HELO; + spf_human_readable = US""; + } + else + { + sr = spf_response->result; + dmarc_spf_result = (sr == SPF_RESULT_NEUTRAL) ? DMARC_POLICY_SPF_OUTCOME_NONE : + (sr == SPF_RESULT_PASS) ? DMARC_POLICY_SPF_OUTCOME_PASS : + (sr == SPF_RESULT_FAIL) ? DMARC_POLICY_SPF_OUTCOME_FAIL : + (sr == SPF_RESULT_SOFTFAIL) ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL : + DMARC_POLICY_SPF_OUTCOME_NONE; + origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM; + spf_human_readable = (uschar *)spf_response->header_comment; + DEBUG(D_receive) + debug_printf("DMARC using SPF sender domain = %s\n", spf_sender_domain); + } + if (strcmp( CCS spf_sender_domain, "") == 0) + dmarc_abort = TRUE; + if (dmarc_abort == FALSE) + { + libdm_status = opendmarc_policy_store_spf(dmarc_pctx, spf_sender_domain, + dmarc_spf_result, origin, spf_human_readable); + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, "failure to store spf for DMARC: %s", + opendmarc_policy_status_to_str(libdm_status)); + } +#endif /* EXPERIMENTAL_SPF */ + + /* Now we cycle through the dkim signature results and put into + * the opendmarc context, further building the DMARC reply. */ + sig = dkim_signatures; + dkim_history_buffer = US""; + while (sig != NULL) + { + int dkim_result, vs; + vs = sig->verify_status; + dkim_result = ( vs == PDKIM_VERIFY_PASS ) ? DMARC_POLICY_DKIM_OUTCOME_PASS : + ( vs == PDKIM_VERIFY_FAIL ) ? DMARC_POLICY_DKIM_OUTCOME_FAIL : + ( vs == PDKIM_VERIFY_INVALID ) ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL : + DMARC_POLICY_DKIM_OUTCOME_NONE; + libdm_status = opendmarc_policy_store_dkim(dmarc_pctx, (uschar *)sig->domain, + dkim_result, US""); + DEBUG(D_receive) + debug_printf("DMARC adding DKIM sender domain = %s\n", sig->domain); + if (libdm_status != DMARC_PARSE_OKAY) + log_write(0, LOG_MAIN|LOG_PANIC, "failure to store dkim (%s) for DMARC: %s", + sig->domain, opendmarc_policy_status_to_str(libdm_status)); + + dkim_history_buffer = string_sprintf("%sdkim %s %d\n", dkim_history_buffer, + sig->domain, dkim_result); + sig = sig->next; + } + libdm_status = opendmarc_policy_query_dmarc(dmarc_pctx, US""); + switch (libdm_status) + { + case DMARC_DNS_ERROR_NXDOMAIN: + case DMARC_DNS_ERROR_NO_RECORD: + DEBUG(D_receive) + debug_printf("DMARC no record found for %s\n", header_from_sender); + has_dmarc_record = FALSE; + break; + case DMARC_PARSE_OKAY: + DEBUG(D_receive) + debug_printf("DMARC record found for %s\n", header_from_sender); + break; + case DMARC_PARSE_ERROR_BAD_VALUE: + DEBUG(D_receive) + debug_printf("DMARC record parse error for %s\n", header_from_sender); + has_dmarc_record = FALSE; + break; + default: + /* everything else, skip dmarc */ + DEBUG(D_receive) + debug_printf("DMARC skipping (%d), unsure what to do with %s", + libdm_status, from_header->text); + has_dmarc_record = FALSE; + break; + } + /* Can't use exim's string manipulation functions so allocate memory + * for libopendmarc using its max hostname length definition. */ + uschar *dmarc_domain = (uschar *)calloc(DMARC_MAXHOSTNAMELEN, sizeof(uschar)); + libdm_status = opendmarc_policy_fetch_utilized_domain(dmarc_pctx, dmarc_domain, + DMARC_MAXHOSTNAMELEN-1); + dmarc_used_domain = string_copy(dmarc_domain); + free(dmarc_domain); + if (libdm_status != DMARC_PARSE_OKAY) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failure to read domainname used for DMARC lookup: %s", + opendmarc_policy_status_to_str(libdm_status)); + } + libdm_status = opendmarc_get_policy_to_enforce(dmarc_pctx); + dmarc_policy = libdm_status; + switch(libdm_status) + { + case DMARC_POLICY_ABSENT: /* No DMARC record found */ + dmarc_status = US"norecord"; + dmarc_pass_fail = US"temperror"; + dmarc_status_text = US"No DMARC record"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_FROM_DOMAIN_ABSENT: /* No From: domain */ + dmarc_status = US"nofrom"; + dmarc_pass_fail = US"temperror"; + dmarc_status_text = US"No From: domain found"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_NONE: /* Accept and report */ + dmarc_status = US"none"; + dmarc_pass_fail = US"none"; + dmarc_status_text = US"None, Accept"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_PASS: /* Explicit accept */ + dmarc_status = US"accept"; + dmarc_pass_fail = US"pass"; + dmarc_status_text = US"Accept"; + action = DMARC_RESULT_ACCEPT; + break; + case DMARC_POLICY_REJECT: /* Explicit reject */ + dmarc_status = US"reject"; + dmarc_pass_fail = US"fail"; + dmarc_status_text = US"Reject"; + action = DMARC_RESULT_REJECT; + break; + case DMARC_POLICY_QUARANTINE: /* Explicit quarantine */ + dmarc_status = US"quarantine"; + dmarc_pass_fail = US"fail"; + dmarc_status_text = US"Quarantine"; + action = DMARC_RESULT_QUARANTINE; + break; + default: + dmarc_status = US"temperror"; + dmarc_pass_fail = US"temperror"; + dmarc_status_text = US"Internal Policy Error"; + action = DMARC_RESULT_TEMPFAIL; + break; + } + + libdm_status = opendmarc_policy_fetch_alignment(dmarc_pctx, &da, &sa); + if (libdm_status != DMARC_PARSE_OKAY) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failure to read DMARC alignment: %s", + opendmarc_policy_status_to_str(libdm_status)); + } + + if (has_dmarc_record == TRUE) + { + log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s " + "spf_align=%s dkim_align=%s enforcement='%s'", + spf_sender_domain, dmarc_used_domain, + (sa==DMARC_POLICY_SPF_ALIGNMENT_PASS) ?"yes":"no", + (da==DMARC_POLICY_DKIM_ALIGNMENT_PASS)?"yes":"no", + dmarc_status_text); + history_file_status = dmarc_write_history_file(); + /* Now get the forensic reporting addresses, if any */ + ruf = opendmarc_policy_fetch_ruf(dmarc_pctx, NULL, 0, 1); + dmarc_send_forensic_report(ruf); + } + } + + /* set some global variables here */ + dmarc_ar_header = dmarc_auth_results_header(from_header, NULL); + + /* shut down libopendmarc */ + if ( dmarc_pctx != NULL ) + (void) opendmarc_policy_connect_shutdown(dmarc_pctx); + if ( dmarc_disable_verify == FALSE ) + (void) opendmarc_policy_library_shutdown(&dmarc_ctx); + + return OK; +} + +int dmarc_write_history_file() +{ + static int history_file_fd; + ssize_t written_len; + int tmp_ans; + u_char **rua; /* aggregate report addressees */ + + if (dmarc_history_file == NULL) + return DMARC_HIST_DISABLED; + history_file_fd = log_create(dmarc_history_file); + + if (history_file_fd < 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failure to create DMARC history file: %s", + dmarc_history_file); + return DMARC_HIST_FILE_ERR; + } + + /* Generate the contents of the history file */ + history_buffer = string_sprintf("job %s\n", message_id); + history_buffer = string_sprintf("%sreporter %s\n", history_buffer, primary_hostname); + history_buffer = string_sprintf("%sreceived %ld\n", history_buffer, time(NULL)); + history_buffer = string_sprintf("%sipaddr %s\n", history_buffer, sender_host_address); + history_buffer = string_sprintf("%sfrom %s\n", history_buffer, header_from_sender); + history_buffer = string_sprintf("%smfrom %s\n", history_buffer, + expand_string(US"$sender_address_domain")); + +#ifdef EXPERIMENTAL_SPF + if (spf_response != NULL) + history_buffer = string_sprintf("%sspf %d\n", history_buffer, dmarc_spf_result); +#else + history_buffer = string_sprintf("%sspf -1\n", history_buffer); +#endif /* EXPERIMENTAL_SPF */ + + history_buffer = string_sprintf("%s%s", history_buffer, dkim_history_buffer); + history_buffer = string_sprintf("%spdomain %s\n", history_buffer, dmarc_used_domain); + history_buffer = string_sprintf("%spolicy %d\n", history_buffer, dmarc_policy); + + rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1); + if (rua != NULL) + { + for (tmp_ans = 0; rua[tmp_ans] != NULL; tmp_ans++) + { + history_buffer = string_sprintf("%srua %s\n", history_buffer, rua[tmp_ans]); + } + } + else + history_buffer = string_sprintf("%srua -\n", history_buffer); + + opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans); + history_buffer = string_sprintf("%spct %d\n", history_buffer, tmp_ans); + + opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans); + history_buffer = string_sprintf("%sadkim %d\n", history_buffer, tmp_ans); + + opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans); + history_buffer = string_sprintf("%saspf %d\n", history_buffer, tmp_ans); + + opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans); + history_buffer = string_sprintf("%sp %d\n", history_buffer, tmp_ans); + + opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans); + history_buffer = string_sprintf("%ssp %d\n", history_buffer, tmp_ans); + + history_buffer = string_sprintf("%salign_dkim %d\n", history_buffer, da); + history_buffer = string_sprintf("%salign_spf %d\n", history_buffer, sa); + history_buffer = string_sprintf("%saction %d\n", history_buffer, action); + + /* Write the contents to the history file */ + DEBUG(D_receive) + debug_printf("DMARC logging history data for opendmarc reporting%s\n", + (host_checking || running_in_test_harness) ? " (not really)" : ""); + if (host_checking || running_in_test_harness) + { + DEBUG(D_receive) + debug_printf("DMARC history data for debugging:\n%s", history_buffer); + } + else + { + written_len = write_to_fd_buf(history_file_fd, + history_buffer, + Ustrlen(history_buffer)); + if (written_len == 0) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s", + dmarc_history_file); + return DMARC_HIST_WRITE_ERR; + } + (void)close(history_file_fd); + } + return DMARC_HIST_OK; +} + +void dmarc_send_forensic_report(u_char **ruf) +{ + int c; + uschar *recipient, *save_sender; + BOOL send_status = FALSE; + error_block *eblock = NULL; + FILE *message_file = NULL; + + /* Earlier ACL does not have *required* control=dmarc_enable_forensic */ + if (dmarc_enable_forensic == FALSE) + return; + + if ((dmarc_policy == DMARC_POLICY_REJECT && action == DMARC_RESULT_REJECT) || + (dmarc_policy == DMARC_POLICY_QUARANTINE && action == DMARC_RESULT_QUARANTINE) ) + { + if (ruf != NULL) + { + eblock = add_to_eblock(eblock, US"Sender Domain", dmarc_used_domain); + eblock = add_to_eblock(eblock, US"Sender IP Address", sender_host_address); + eblock = add_to_eblock(eblock, US"Received Date", tod_stamp(tod_full)); + eblock = add_to_eblock(eblock, US"SPF Alignment", + (sa==DMARC_POLICY_SPF_ALIGNMENT_PASS) ?US"yes":US"no"); + eblock = add_to_eblock(eblock, US"DKIM Alignment", + (da==DMARC_POLICY_DKIM_ALIGNMENT_PASS)?US"yes":US"no"); + eblock = add_to_eblock(eblock, US"DMARC Results", dmarc_status_text); + /* Set a sane default envelope sender */ + dsn_from = dmarc_forensic_sender ? dmarc_forensic_sender : + dsn_from ? dsn_from : + string_sprintf("do-not-reply@%s",primary_hostname); + for (c = 0; ruf[c] != NULL; c++) + { + recipient = string_copylc(ruf[c]); + if (Ustrncmp(recipient, "mailto:",7)) + continue; + /* Move to first character past the colon */ + recipient += 7; + DEBUG(D_receive) + debug_printf("DMARC forensic report to %s%s\n", recipient, + (host_checking || running_in_test_harness) ? " (not really)" : ""); + if (host_checking || running_in_test_harness) + continue; + save_sender = sender_address; + sender_address = recipient; + send_status = moan_to_sender(ERRMESS_DMARC_FORENSIC, eblock, + header_list, message_file, FALSE); + sender_address = save_sender; + if (send_status == FALSE) + log_write(0, LOG_MAIN|LOG_PANIC, "failure to send DMARC forensic report to %s", + recipient); + } + } + } +} + +uschar *dmarc_exim_expand_query(int what) +{ + if (dmarc_disable_verify || !dmarc_pctx) + return dmarc_exim_expand_defaults(what); + + switch(what) { + case DMARC_VERIFY_STATUS: + return(dmarc_status); + default: + return US""; + } +} + +uschar *dmarc_exim_expand_defaults(int what) +{ + switch(what) { + case DMARC_VERIFY_STATUS: + return (dmarc_disable_verify) ? + US"off" : + US"none"; + default: + return US""; + } +} + +uschar *dmarc_auth_results_header(header_line *from_header, uschar *hostname) +{ + uschar *hdr_tmp = US""; + + /* Allow a server hostname to be passed to this function, but is + * currently unused */ + if (hostname == NULL) + hostname = primary_hostname; + hdr_tmp = string_sprintf("%s %s;", DMARC_AR_HEADER, hostname); + +#if 0 + /* I don't think this belongs here, but left it here commented out + * because it was a lot of work to get working right. */ +#ifdef EXPERIMENTAL_SPF + if (spf_response != NULL) { + uschar *dmarc_ar_spf = US""; + int sr = 0; + sr = spf_response->result; + dmarc_ar_spf = (sr == SPF_RESULT_NEUTRAL) ? US"neutral" : + (sr == SPF_RESULT_PASS) ? US"pass" : + (sr == SPF_RESULT_FAIL) ? US"fail" : + (sr == SPF_RESULT_SOFTFAIL) ? US"softfail" : + US"none"; + hdr_tmp = string_sprintf("%s spf=%s (%s) smtp.mail=%s;", + hdr_tmp, dmarc_ar_spf_result, + spf_response->header_comment, + expand_string(US"$sender_address") ); + } +#endif +#endif + hdr_tmp = string_sprintf("%s dmarc=%s", + hdr_tmp, dmarc_pass_fail); + if (header_from_sender) + hdr_tmp = string_sprintf("%s header.from=%s", + hdr_tmp, header_from_sender); + return hdr_tmp; +} + +#endif + +// vim:sw=2 expandtab diff --git a/src/src/dmarc.h b/src/src/dmarc.h new file mode 100644 index 000000000..fa0365e55 --- /dev/null +++ b/src/src/dmarc.h @@ -0,0 +1,48 @@ +/************************************************* +* Exim - an Internet mail transport agent * +*************************************************/ + +/* Experimental DMARC support. + Copyright (c) Todd Lyons 2012, 2013 + License: GPL */ + +/* Portions Copyright (c) 2012, 2013, The Trusted Domain Project; + All rights reserved, licensed for use per LICENSE.opendmarc. */ + +#ifdef EXPERIMENTAL_DMARC + +#include "opendmarc/dmarc.h" +#ifdef EXPERIMENTAL_SPF +#include "spf2/spf.h" +#endif /* EXPERIMENTAL_SPF */ + +/* prototypes */ +int dmarc_init(); +int dmarc_store_data(header_line *); +int dmarc_process(); +uschar *dmarc_exim_expand_query(int); +uschar *dmarc_exim_expand_defaults(int); +uschar *dmarc_auth_results_header(header_line *,uschar *); +int dmarc_write_history_file(); +void dmarc_send_forensic_report(u_char **); + +#define DMARC_AR_HEADER US"Authentication-Results:" +#define DMARC_VERIFY_STATUS 1 + +#define DMARC_HIST_OK 1 +#define DMARC_HIST_DISABLED 2 +#define DMARC_HIST_EMPTY 3 +#define DMARC_HIST_FILE_ERR 4 +#define DMARC_HIST_WRITE_ERR 5 + +/* From opendmarc.c */ +#define DMARC_RESULT_REJECT 0 +#define DMARC_RESULT_DISCARD 1 +#define DMARC_RESULT_ACCEPT 2 +#define DMARC_RESULT_TEMPFAIL 3 +#define DMARC_RESULT_QUARANTINE 4 + + +#endif + +// vim:sw=2 expandtab diff --git a/src/src/exim.c b/src/src/exim.c index e66a9664d..a27e391d1 100644 --- a/src/src/exim.c +++ b/src/src/exim.c @@ -816,6 +816,9 @@ fprintf(f, "Support for:"); #ifdef EXPERIMENTAL_DCC fprintf(f, " Experimental_DCC"); #endif +#ifdef EXPERIMENTAL_DMARC + fprintf(f, " Experimental_DMARC"); +#endif #ifdef EXPERIMENTAL_OCSP fprintf(f, " Experimental_OCSP"); #endif diff --git a/src/src/exim.h b/src/src/exim.h index 066e99d21..ec809d6b7 100644 --- a/src/src/exim.h +++ b/src/src/exim.h @@ -495,6 +495,10 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly. #ifndef DISABLE_DKIM #include "dkim.h" #endif +#ifdef EXPERIMENTAL_DMARC +#include "dmarc.h" +#include +#endif /* The following stuff must follow the inclusion of config.h because it requires various things that are set therein. */ diff --git a/src/src/expand.c b/src/src/expand.c index 464c6f6fc..391b943cf 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -458,6 +458,12 @@ static var_entry var_table[] = { { "dkim_signers", vtype_stringptr, &dkim_signers }, { "dkim_verify_reason", vtype_dkim, (void *)DKIM_VERIFY_REASON }, { "dkim_verify_status", vtype_dkim, (void *)DKIM_VERIFY_STATUS}, +#endif +#ifdef EXPERIMENTAL_DMARC + { "dmarc_ar_header", vtype_stringptr, &dmarc_ar_header }, + { "dmarc_status", vtype_stringptr, &dmarc_status }, + { "dmarc_status_text", vtype_stringptr, &dmarc_status_text }, + { "dmarc_used_domain", vtype_stringptr, &dmarc_used_domain }, #endif { "dnslist_domain", vtype_stringptr, &dnslist_domain }, { "dnslist_matched", vtype_stringptr, &dnslist_matched }, diff --git a/src/src/functions.h b/src/src/functions.h index 20fc9a0b5..e76cd140e 100644 --- a/src/src/functions.h +++ b/src/src/functions.h @@ -177,6 +177,7 @@ extern int ip_recv(int, uschar *, int, int); extern int ip_socket(int, int); extern uschar *local_part_quote(uschar *); +extern int log_create(uschar *); extern int log_create_as_exim(uschar *); extern void log_close_all(void); diff --git a/src/src/globals.c b/src/src/globals.c index a4898fe3f..a491c2746 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -590,6 +590,18 @@ uschar *dkim_verify_signers = US"$dkim_signers"; BOOL dkim_collect_input = FALSE; BOOL dkim_disable_verify = FALSE; #endif +#ifdef EXPERIMENTAL_DMARC +int dmarc_has_been_checked = 0; +uschar *dmarc_ar_header = NULL; +uschar *dmarc_forensic_sender = NULL; +uschar *dmarc_history_file = NULL; +uschar *dmarc_status = NULL; +uschar *dmarc_status_text = NULL; +uschar *dmarc_tld_file = NULL; +uschar *dmarc_used_domain = NULL; +BOOL dmarc_disable_verify = FALSE; +BOOL dmarc_enable_forensic = FALSE; +#endif uschar *dns_again_means_nonexist = NULL; int dns_csa_search_limit = 5; diff --git a/src/src/globals.h b/src/src/globals.h index df6132266..73cfd0ea8 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -346,6 +346,18 @@ extern uschar *dkim_verify_signers; /* Colon-separated list of domains for ea extern BOOL dkim_collect_input; /* Runtime flag that tracks wether SMTP input is fed to DKIM validation */ extern BOOL dkim_disable_verify; /* Set via ACL control statement. When set, DKIM verification is disabled for the current message */ #endif +#ifdef EXPERIMENTAL_DMARC +extern int dmarc_has_been_checked; /* Global variable to check if test has been called yet */ +extern uschar *dmarc_ar_header; /* Expansion variable, suggested header for dmarc auth results */ +extern uschar *dmarc_forensic_sender; /* Set sender address for forensic reports */ +extern uschar *dmarc_history_file; /* Expansion variable, file to store dmarc results */ +extern uschar *dmarc_status; /* Expansion variable, one word value */ +extern uschar *dmarc_status_text; /* Expansion variable, human readable value */ +extern uschar *dmarc_tld_file; /* Mozilla TLDs text file */ +extern uschar *dmarc_used_domain; /* Expansion variable, domain libopendmarc chose for DMARC policy lookup */ +extern BOOL dmarc_disable_verify; /* Set via ACL control statement. When set, DMARC verification is disabled for the current message */ +extern BOOL dmarc_enable_forensic; /* Set via ACL control statement. When set, DMARC forensic reports are enabled for the current message */ +#endif extern uschar *dns_again_means_nonexist; /* Domains that are badly set up */ extern int dns_csa_search_limit; /* How deep to search for CSA SRV records */ diff --git a/src/src/log.c b/src/src/log.c index 7af92818e..1523874d9 100644 --- a/src/src/log.c +++ b/src/src/log.c @@ -179,7 +179,7 @@ overwrite it temporarily if it is necessary to create the directory. Returns: a file descriptor, or < 0 on failure (errno set) */ -static int +int log_create(uschar *name) { int fd = Uopen(name, O_CREAT|O_APPEND|O_WRONLY, LOG_MODE); diff --git a/src/src/macros.h b/src/src/macros.h index b878b415c..a73bb0ba6 100644 --- a/src/src/macros.h +++ b/src/src/macros.h @@ -222,6 +222,9 @@ enum { ERRMESS_TOOMANYRECIP, /* Too many recipients */ ERRMESS_LOCAL_SCAN, /* Rejected by local scan */ ERRMESS_LOCAL_ACL /* Rejected by non-SMTP ACL */ +#ifdef EXPERIMENTAL_DMARC + ,ERRMESS_DMARC_FORENSIC /* DMARC Forensic Report */ +#endif }; /* Error handling styles - set by option, and apply only when receiving diff --git a/src/src/moan.c b/src/src/moan.c index 6c04f7a57..3b670a144 100644 --- a/src/src/moan.c +++ b/src/src/moan.c @@ -202,6 +202,26 @@ switch(ident) fprintf(f, "\n"); break; +#ifdef EXPERIMENTAL_DMARC + case ERRMESS_DMARC_FORENSIC: + bounce_return_message = TRUE; + bounce_return_body = FALSE; + fprintf(f, + "Subject: DMARC Forensic Report for %s from IP %s\n\n", + ((eblock == NULL) ? US"Unknown" : eblock->text2), + sender_host_address); + fprintf(f, + "A message claiming to be from you has failed the published DMARC\n" + "policy for your domain.\n\n"); + while (eblock != NULL) + { + fprintf(f, " %s: %s\n", eblock->text1, eblock->text2); + count++; + eblock = eblock->next; + } + break; +#endif + default: fprintf(f, "Subject: Mail failure\n\n"); fprintf(f, @@ -280,8 +300,16 @@ if (bounce_return_message) } } } +#ifdef EXPERIMENTAL_DMARC + /* Overkill, but use exact test in case future code gets inserted */ + else if (bounce_return_body && message_file == NULL) + { + /* This doesn't print newlines, disable until can parse and fix + * output to be legible. */ + fprintf(f, "%s", expand_string(US"$message_body")); + } +#endif } - /* Close the file, which should send an EOF to the child process that is receiving the message. Wait for it to finish, without a timeout. */ diff --git a/src/src/readconf.c b/src/src/readconf.c index 77836d157..ba69546a3 100644 --- a/src/src/readconf.c +++ b/src/src/readconf.c @@ -211,6 +211,11 @@ static optionlist optionlist_config[] = { { "disable_ipv6", opt_bool, &disable_ipv6 }, #ifndef DISABLE_DKIM { "dkim_verify_signers", opt_stringptr, &dkim_verify_signers }, +#endif +#ifdef EXPERIMENTAL_DMARC + { "dmarc_forensic_sender", opt_stringptr, &dmarc_forensic_sender }, + { "dmarc_history_file", opt_stringptr, &dmarc_history_file }, + { "dmarc_tld_file", opt_stringptr, &dmarc_tld_file }, #endif { "dns_again_means_nonexist", opt_stringptr, &dns_again_means_nonexist }, { "dns_check_names_pattern", opt_stringptr, &check_dns_names_pattern }, diff --git a/src/src/receive.c b/src/src/receive.c index 4cea6d58e..372747360 100644 --- a/src/src/receive.c +++ b/src/src/receive.c @@ -13,6 +13,10 @@ extern int dcc_ok; #endif +#ifdef EXPERIMENTAL_DMARC +#include "dmarc.h" +#endif /* EXPERIMENTAL_DMARC */ + /************************************************* * Local static variables * *************************************************/ @@ -1480,6 +1484,10 @@ header_line *subject_header = NULL; header_line *msgid_header = NULL; header_line *received_header; +#ifdef EXPERIMENTAL_DMARC +int dmarc_up = 0; +#endif /* EXPERIMENTAL_DMARC */ + /* Variables for use when building the Received: header. */ uschar *timestamp; @@ -1536,6 +1544,11 @@ message_linecount = body_linecount = body_zerocount = if (smtp_input && !smtp_batched_input && !dkim_disable_verify) dkim_exim_verify_init(); #endif +#ifdef EXPERIMENTAL_DMARC +/* initialize libopendmarc */ +dmarc_up = dmarc_init(); +#endif + /* Remember the time of reception. Exim uses time+pid for uniqueness of message ids, and fractions of a second are required. See the comments that precede the message id creation below. */ @@ -2706,7 +2719,6 @@ if (from_header != NULL && } } - /* If there are any rewriting rules, apply them to the sender address, unless it has already been rewritten as part of verification for SMTP input. */ @@ -3238,7 +3250,6 @@ else } } } -#endif /* DISABLE_DKIM */ #ifdef WITH_CONTENT_SCAN if (recipients_count > 0 && @@ -3247,6 +3258,10 @@ else goto TIDYUP; #endif /* WITH_CONTENT_SCAN */ +#ifdef EXPERIMENTAL_DMARC + dmarc_up = dmarc_store_data(from_header); +#endif /* EXPERIMENTAL_DMARC */ + #ifdef EXPERIMENTAL_PRDR if (prdr_requested && recipients_count > 1 && acl_smtp_data_prdr != NULL ) { @@ -3411,6 +3426,8 @@ else } } +#endif /* DISABLE_DKIM */ + /* The applicable ACLs have been run */ if (deliver_freeze) frozen_by = US"ACL"; /* for later logging */