changeset 322:9f8411f3919c

add dkim white/black listing
author Carl Byington <carl@five-ten-sg.com>
date Sat, 17 Dec 2016 17:04:52 -0800
parents e172dc10fe24
children a6de27b0a1e9
files dnsbl.conf src/context.cpp src/context.h src/dnsbl.cpp src/dnsbl.h xml/dnsbl.in
diffstat 6 files changed, 188 insertions(+), 93 deletions(-) [+]
line wrap: on
line diff
--- a/dnsbl.conf	Sat Dec 17 13:47:28 2016 -0800
+++ b/dnsbl.conf	Sat Dec 17 17:04:52 2016 -0800
@@ -53,6 +53,16 @@
     require_rdns    yes;
 
     content on {
+        dkim_signer {
+            sendgrid.me     black;
+            weather.com     white;
+        };
+
+        dkim_from {
+            yahoo.com        require_signed   yahoo.com;
+            gmail.com        signed_white     gmail.com;
+            girlscoutsla.org signed_white     girlscoutsla.ccsend.com;
+        };
         filter    sbl-xbl.spamhaus.org        "Mail containing %s rejected - sbl; see http://www.spamhaus.org/query/bl?ip=%s";
         uribl     multi.surbl.org             "Mail containing %s rejected - surbl; see http://www.surbl.org/surbl-analysis?d=%s";
         #uribl    multi.uribl.com             "Mail containing %s rejected - uribl; see http://l.uribl.com/?d=%s";
@@ -80,17 +90,6 @@
         include "/etc/mail/local-host-names";
     };
 
-    dkim_signer {
-        sendgrid.me     black;
-        weather.com     white;
-    };
-
-    dkim_from {
-        yahoo.com        require_signed   yahoo.com;
-        gmail.com        signed_white     gmail.com;
-        girlscoutsla.org signed_white     girlscoutsla.ccsend.com;
-    };
-
     context whitelist {
         content off {};
         env_to {
--- a/src/context.cpp	Sat Dec 17 13:47:28 2016 -0800
+++ b/src/context.cpp	Sat Dec 17 17:04:52 2016 -0800
@@ -74,8 +74,8 @@
 const char *token_signed_white;
 const char *token_signed_black;
 const char *token_require_signed;
+const char *token_myhostname;
 
-const char *token_myhostname;
 #ifndef HOST_NAME_MAX
     #define HOST_NAME_MAX 255
 #endif
@@ -1099,7 +1099,41 @@
 }
 
 
-bool CONTEXT::acceptable_content(recorder &memory, int score, int bulk, string& msg) {
+bool CONTEXT::acceptable_content(recorder &memory, int score, int bulk, const char *signer, const char *from, string& msg) {
+    char buf[maxlen];
+    snprintf(buf, sizeof(buf), "acceptable content from %s signer %s", (signer) ? signer : token_asterisk, (from) ? from : token_asterisk);
+    my_syslog(buf);
+
+    const char *st = find_dkim_signer(signer);
+    if (st == token_white) return true;
+    if (st == token_black) {
+        char buf[maxlen];
+        snprintf(buf, sizeof(buf), "Mail rejected - dkim signed by %s", signer);
+        msg = string(buf);
+        return false;
+    }
+
+    DKIMP dk = find_dkim_from(from);
+    if (dk) {
+        st = dk->action;
+        // signed by a white listed signer
+        if ((st == token_signed_white)   && (strcasecmp(signer,dk->signer) == 0)) return true;
+        // not signed by the required signer
+        if ((st == token_require_signed) && (strcasecmp(signer,dk->signer) != 0)) {
+            char buf[maxlen];
+            snprintf(buf, sizeof(buf), "Mail rejected - not dkim signed by %s", dk->signer);
+            msg = string(buf);
+            return false;
+        }
+        // signed by a black listed signer
+        if ((st == token_signed_black)   && (strcasecmp(signer,dk->signer) == 0)) {
+            char buf[maxlen];
+            snprintf(buf, sizeof(buf), "Mail rejected - dkim signed by %s", dk->signer);
+            msg = string(buf);
+            return false;
+        }
+    }
+
     if (spamassassin_limit && (score > spamassassin_limit)) {
         char buf[maxlen];
         snprintf(buf, sizeof(buf), "Mail rejected - spam assassin score %d", score);
@@ -1168,6 +1202,20 @@
 
     if (content_filtering) {
         printf("%s     content on { \n", indent);
+        printf("%s         dkim_signer { \n", indent);
+        for (string_map::iterator i=dkim_signer_names.begin(); i!=dkim_signer_names.end(); i++) {
+            const char *n = (*i).first;
+            const char *a = (*i).second;
+            printf("%s             %s %s; \n", indent, n, a);
+        }
+        printf("%s         } \n", indent);
+        printf("%s         dkim_from { \n", indent);
+        for (dkimp_map::iterator i=dkim_from_names.begin(); i!=dkim_from_names.end(); i++) {
+            const char *n = (*i).first;
+            DKIM &d = *(*i).second;
+            printf("%s             %s %s %s; \n", indent, n, d.action, d.signer);
+        }
+        printf("%s         } \n", indent);
         if (content_suffix) {
             printf("%s         filter %s \"%s\"; \n", indent, content_suffix, content_message);
         }
@@ -1231,23 +1279,6 @@
         printf("%s     content off {}; \n", indent);
     }
 
-    printf("%s     dkim_signer { \n", indent);
-    for (string_map::iterator i=dkim_signer_names.begin(); i!=dkim_signer_names.end(); i++) {
-        const char *n = (*i).first;
-        const char *a = (*i).second;
-        printf("%s         %s %s; \n", indent, n, a);
-    }
-    printf("%s     } \n", indent);
-
-    printf("%s     dkim_from { \n", indent);
-    for (dkimp_map::iterator i=dkim_from_names.begin(); i!=dkim_from_names.end(); i++) {
-        const char *n = (*i).first;
-        DKIM &d = *(*i).second;
-        printf("%s         %s %s %s; \n", indent, n, d.action, d.signer);
-    }
-
-    printf("%s     } \n", indent);
-
     printf("%s     env_to { \t// %s\n", indent, fullname);
     for (string_set::iterator i=env_to.begin(); i!=env_to.end(); i++) {
         printf("%s         %s; \n", indent, *i);
@@ -1463,6 +1494,62 @@
 
 ////////////////////////////////////////////////
 //
+bool parse_dkim_signer(TOKEN &tok, CONFIG &dc, CONTEXT &me);
+bool parse_dkim_signer(TOKEN &tok, CONFIG &dc, CONTEXT &me) {
+    if (!tsa(tok, token_lbrace)) return false;
+    while (true) {
+        const char *have = tok.next();
+        if (!have) break;
+        if (have == token_rbrace) break;
+        if (have == token_semi) {
+            // optional separators
+        }
+        else {
+            const char *signer = have;
+            const char *action = tok.next();
+            if ((action == token_white) || (action == token_black)) {
+                me.add_dkim_signer(signer, action);
+            }
+            else {
+                tok.token_error("white/black", action);
+            }
+        }
+    }
+    return tsa(tok, token_semi);
+}
+
+
+////////////////////////////////////////////////
+//
+bool parse_dkim_from(TOKEN &tok, CONFIG &dc, CONTEXT &me);
+bool parse_dkim_from(TOKEN &tok, CONFIG &dc, CONTEXT &me) {
+    if (!tsa(tok, token_lbrace)) return false;
+    while (true) {
+        const char *have = tok.next();
+        if (!have) break;
+        if (have == token_rbrace) break;
+        if (have == token_semi) {
+            // optional separators
+        }
+        else {
+            const char *from   = have;
+            const char *action = tok.next();
+            if ((action == token_signed_white) || (action == token_signed_black) || (action == token_require_signed)) {
+                const char *signer = tok.next();
+                if (!signer) break;
+                else         me.add_dkim_from(from, action, signer);
+            }
+            else {
+                tok.token_error("signed_white/signed_black/require_signed", action);
+            }
+        }
+    }
+    return tsa(tok, token_semi);
+}
+
+
+////////////////////////////////////////////////
+//
 bool parse_content(TOKEN &tok, CONFIG &dc, CONTEXT &me);
 bool parse_content(TOKEN &tok, CONFIG &dc, CONTEXT &me) {
     const char *setting = tok.next();
@@ -1628,6 +1715,12 @@
             }
             if (!tsa(tok, token_semi)) return false;
         }
+        else if (have == token_dkim_signer) {
+            if (!parse_dkim_signer(tok, dc, me)) return false;
+        }
+        else if (have == token_dkim_from) {
+            if (!parse_dkim_from(tok, dc, me)) return false;
+        }
         else if (have == token_rbrace) {
             break;  // done
         }
@@ -1859,62 +1952,6 @@
 
 ////////////////////////////////////////////////
 //
-bool parse_dkim_signer(TOKEN &tok, CONFIG &dc, CONTEXT &me);
-bool parse_dkim_signer(TOKEN &tok, CONFIG &dc, CONTEXT &me) {
-    if (!tsa(tok, token_lbrace)) return false;
-    while (true) {
-        const char *have = tok.next();
-        if (!have) break;
-        if (have == token_rbrace) break;
-        if (have == token_semi) {
-            // optional separators
-        }
-        else {
-            const char *signer = have;
-            const char *action = tok.next();
-            if ((action == token_white) || (action == token_black)) {
-                me.add_dkim_signer(signer, action);
-            }
-            else {
-                tok.token_error("white/black", action);
-            }
-        }
-    }
-    return tsa(tok, token_semi);
-}
-
-
-////////////////////////////////////////////////
-//
-bool parse_dkim_from(TOKEN &tok, CONFIG &dc, CONTEXT &me);
-bool parse_dkim_from(TOKEN &tok, CONFIG &dc, CONTEXT &me) {
-    if (!tsa(tok, token_lbrace)) return false;
-    while (true) {
-        const char *have = tok.next();
-        if (!have) break;
-        if (have == token_rbrace) break;
-        if (have == token_semi) {
-            // optional separators
-        }
-        else {
-            const char *from   = have;
-            const char *action = tok.next();
-            if ((action == token_signed_white) || (action == token_signed_black) || (action == token_require_signed)) {
-                const char *signer = tok.next();
-                if (!signer) break;
-                else         me.add_dkim_from(from, action, signer);
-            }
-            else {
-                tok.token_error("signed_white/signed_black/require_signed", action);
-            }
-        }
-    }
-    return tsa(tok, token_semi);
-}
-
-
-////////////////////////////////////////////////
-//
 bool parse_context(TOKEN &tok, CONFIG &dc, CONTEXTP parent);
 bool parse_context(TOKEN &tok, CONFIG &dc, CONTEXTP parent) {
     const char *name = tok.next();
--- a/src/context.h	Sat Dec 17 13:47:28 2016 -0800
+++ b/src/context.h	Sat Dec 17 17:04:52 2016 -0800
@@ -312,7 +312,7 @@
     dnsblp_list&    get_dnsbl_list();
     dnswlp_list&    get_dnswl_list();
 
-    bool        acceptable_content(recorder &memory, int score, int bulk, string& msg);
+    bool        acceptable_content(recorder &memory, int score, int bulk, const char *signer, const char *from, string& msg);
     bool        ignore_host(const char *host);
 
     void        dump(bool isdefault, bool &spamass, int level = 0);
@@ -389,10 +389,12 @@
 extern const char *token_white;
 extern const char *token_white_regex;
 extern const char *token_yes;
-extern const char *token_dkim;
+extern const char *token_dkim_signer;
+extern const char *token_dkim_from;
 extern const char *token_signed_white;
 extern const char *token_signed_black;
 extern const char *token_require_signed;
+extern const char *token_myhostname;
 
 extern pthread_mutex_t verifier_mutex;     // protect the verifier map
 extern pthread_mutex_t whitelister_mutex;  // protect the
--- a/src/dnsbl.cpp	Sat Dec 17 13:47:28 2016 -0800
+++ b/src/dnsbl.cpp	Sat Dec 17 17:04:52 2016 -0800
@@ -98,6 +98,8 @@
 const int maxlen = 1000;    // used for snprintf buffers
 regex_t srs_pattern;        // used to detect srs coding in mail addresses
 regex_t prvs_pattern;       // used to detect prvs coding in mail addresses
+regex_t dkim_pattern;       // used to detect dkim signatures in authentication header generated by the upstream opendkim milter
+regex_t from_pattern;       // used to extract the senders mail domain from the body from: header
 
 pthread_mutex_t  config_mutex;
 pthread_mutex_t  syslog_mutex;
@@ -522,6 +524,8 @@
     mailaddr                = NULL;
     fromaddr                = NULL;
     header_count            = 0;
+    dkim_ok                 = true;
+    dkim_signer             = NULL;
     queueid                 = NULL;
     authenticated           = NULL;
     client_name             = NULL;
@@ -570,6 +574,7 @@
     }
     if (mailaddr)        free((void*)mailaddr);
     if (fromaddr)        free((void*)fromaddr);
+    if (dkim_signer)     free((void*)dkim_signer);
     if (queueid)         free((void*)queueid);
     if (authenticated)   free((void*)authenticated);
     if (client_name)     free((void*)client_name);
@@ -587,6 +592,8 @@
         mailaddr                = NULL;
         fromaddr                = NULL;
         header_count            = 0;
+        dkim_ok                 = true;
+        dkim_signer             = NULL;
         queueid                 = NULL;
         authenticated           = NULL;
         client_name             = NULL;
@@ -1450,8 +1457,8 @@
 {
     mlfiPriv &priv = *MLFIPRIV;
     priv.header_count++;
+    char msg[maxlen];
     if ((priv.header_count < 4) || (strcasecmp(headerf, "from") == 0)) {
-        char msg[maxlen];
         snprintf(msg, sizeof(msg), "header %s: %s", headerf, headerv);
         for (int i=0; i<strlen(msg); i++) {
             if (msg[i] < 0x20) msg[i] = ' ';
@@ -1459,6 +1466,38 @@
         my_syslog(&priv, msg);
     }
 
+    if (priv.dkim_ok) {
+        if ((priv.header_count == 1) && (strcasecmp(headerf, "DKIM-Filter") != 0)) priv.dkim_ok = false;
+        if (priv.header_count == 2) {
+            if (strcasecmp(headerf, "Authentication-Results") != 0) priv.dkim_ok = false;
+            if (strncasecmp(headerv, token_myhostname, strlen(token_myhostname)) != 0) priv.dkim_ok = false;
+            if (priv.dkim_ok) {
+                const int nmatch = 2;
+                regmatch_t match[nmatch];
+                if (0 == regexec(&dkim_pattern, msg, nmatch, match, 0)) {
+                    int s1 = match[1].rm_so;    // domain
+                    int e1 = match[1].rm_eo;
+                    if (s1 != -1) {
+                        msg[e1] = '\0';
+                        priv.dkim_signer = strdup(msg+s1);
+                    }
+                }
+            }
+        }
+        if ((priv.header_count > 2) && (strcasecmp(headerf, "from"))) {
+            const int nmatch = 2;
+            regmatch_t match[nmatch];
+            if (0 == regexec(&from_pattern, msg, nmatch, match, 0)) {
+                int s1 = match[1].rm_so;    // domain
+                int e1 = match[1].rm_eo;
+                if (s1 != -1) {
+                    msg[e1] = '\0';
+                    priv.fromaddr = strdup(msg+s1);
+                }
+            }
+        }
+    }
+
     // headers that avoid autowhitelisting
     if (((strcasecmp(headerf, "precedence") == 0)   && (strcasecmp(headerv, "bulk") == 0)) ||
         ((strcasecmp(headerf, "content-type") == 0) && (strncasecmp(headerv, "multipart/report", 16) == 0))) {
@@ -1546,9 +1585,11 @@
         for (context_map::iterator i=priv.env_to.begin(); i!=priv.env_to.end(); i++) {
             const char *rcpt   = (*i).first;
             CONTEXT &con = *((*i).second);
-            if (!con.acceptable_content(*priv.memory, score, bulk, msg)) {
+            if (!con.acceptable_content(*priv.memory, score, bulk, priv.dkim_signer, priv.fromaddr, msg)) {
                 // bad html tags or excessive hosts or
                 // high spam assassin score or dcc bulk threshold exceedeed
+                // or signed by a dkim signer that we don't like
+                // or header from requires dkim signer that is missing
                 smfi_delrcpt(ctx, (char*)rcpt);
             }
             else {
@@ -1804,6 +1845,18 @@
         exit(3);
     }
 
+    // setup dkim signature detection
+    if (regcomp(&dkim_pattern, " dkim=pass .* header.d=([^ ]+) ", REG_ICASE | REG_EXTENDED)) {
+        printf("cannot compile regex pattern to find dkim signatures\n");
+        exit(3);
+    }
+
+    // setup from domain extraction
+    if (regcomp(&from_pattern, "@([a-zA-Z0-9.-]+)", REG_ICASE | REG_EXTENDED)) {
+        printf("cannot compile regex pattern to find dkim signatures\n");
+        exit(3);
+    }
+
     // Process command line options
     while ((c = getopt(argc, argv, args)) != -1) {
         switch (c) {
--- a/src/dnsbl.h	Sat Dec 17 13:47:28 2016 -0800
+++ b/src/dnsbl.h	Sat Dec 17 17:04:52 2016 -0800
@@ -40,6 +40,8 @@
     const char      *mailaddr;              // envelope from value
     const char      *fromaddr;              // header from value, set by mlfi_header()
     int             header_count;           // count of headers already seen
+    bool            dkim_ok;                // ok to proceed with dkim checking
+    const char      *dkim_signer;           // non null if message was validly signed
     const char      *queueid;               // sendmail queue id
     const char      *authenticated;         // client authenticated? if so, suppress all dnsbl checks, but check rate limits
     const char      *client_name;           // fully qualified host name of the smtp client xxx [ip.ad.dr.es] (may be forged)
--- a/xml/dnsbl.in	Sat Dec 17 13:47:28 2016 -0800
+++ b/xml/dnsbl.in	Sat Dec 17 17:04:52 2016 -0800
@@ -706,7 +706,7 @@
 CONTEXT    = "context" NAME "{" {STATEMENT}+ "}"
 STATEMENT  = ( DNSBL  | DNSBLLIST  | DNSWL   | DNSWLLIST | CONTENT | ENV-TO
              | VERIFY | GENERIC    | W_REGEX | AUTOWHITE | CONTEXT | ENV-FROM
-             | DKIM_SIGNER | DKIM_FROM | RATE-LIMIT | REQUIRERDNS) ";"
+             | RATE-LIMIT | REQUIRERDNS) ";"
 
 DNSBL      = "dnsbl" NAME DNSPREFIX ERROR-MSG1
 DNSBLLIST  = "dnsbl_list" {NAME}*
@@ -719,7 +719,8 @@
 
 CONTENT    = "content" ("on" | "off") "{" {CONTENT-ST}+ "}"
 CONTENT-ST = (FILTER | URIBL | IGNORE | TLD | HTML-TAGS | HTML-LIMIT |
-              HOST-LIMIT | SPAMASS | REQUIRE | DCCGREY | DCCBULK) ";"
+              HOST-LIMIT | SPAMASS | REQUIRE | DCCGREY | DCCBULK   | DKIM_SIGNER |
+              DKIM_FROM) ";"
 FILTER     = "filter" DNSPREFIX ERROR-MSG2
 URIBL      = "uribl"  DNSPREFIX ERROR-MSG3
 IGNORE     = "ignore"     "{" {HOSTNAME [";"]}+ "}"