Add CRAM-MD5 authentication support for the DragonFly Mail Agent. This is the
authorMatthias Schmidt <matthias@dragonflybsd.org>
Tue, 2 Sep 2008 15:11:49 +0000 (15:11 +0000)
committerMatthias Schmidt <matthias@dragonflybsd.org>
Tue, 2 Sep 2008 15:11:49 +0000 (15:11 +0000)
first piece of Max's work for the Google Summer of Code 2008.  All other
new features will follow after evaluation/review :)

Besides the CRAM code there is new code within base64.c (BSD licensed and
from the University of Stockholm) and within crypto.c derived from RFC 2104.

Note: This code is tested and works.  If you find a bug, please report back
to the bugs@ list.

Thanks a lot for the good work Max.

Submitted-by: Max Lindner <gisanka@googlemail.com>
Sponsored-by: Google Summer of Code 2008
libexec/dma/base64.c
libexec/dma/crypto.c
libexec/dma/dma.h
libexec/dma/net.c

index 610aa3c..f140027 100644 (file)
@@ -30,7 +30,7 @@
  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  *
- * $DragonFly: src/libexec/dma/base64.c,v 1.1 2008/02/02 18:20:51 matthias Exp $
+ * $DragonFly: src/libexec/dma/base64.c,v 1.2 2008/09/02 15:11:49 matthias Exp $
  */
 
 #include <stdlib.h>
@@ -41,6 +41,17 @@ int base64_encode(const void *data, int size, char **str);
 static char base64_chars[] =
     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
 
+static int
+pos(char c)
+{
+       char *p;
+       for (p = base64_chars; *p; p++)
+               if (*p == c)
+                       return p - base64_chars;
+       return -1;
+}
+
+
 int
 base64_encode(const void *data, int size, char **str)
 {
@@ -79,3 +90,48 @@ base64_encode(const void *data, int size, char **str)
     return strlen(s);
 }
 
+#define DECODE_ERROR 0xffffffff
+
+static unsigned int
+token_decode(const char *token)
+{
+       int i;
+       unsigned int val = 0;
+       int marker = 0;
+       if (strlen(token) < 4)
+               return DECODE_ERROR;
+       for (i = 0; i < 4; i++) {
+               val *= 64;
+               if (token[i] == '=')
+                       marker++;
+               else if (marker > 0)
+                       return DECODE_ERROR;
+               else
+                       val += pos(token[i]);
+       }
+       if (marker > 2)
+               return DECODE_ERROR;
+       return (marker << 24) | val;
+}
+
+int
+base64_decode(const char *str, void *data)
+{
+       const char *p;
+       unsigned char *q;
+
+       q = data;
+       for (p = str; *p && (*p == '=' || strchr(base64_chars, *p)); p += 4) {
+               unsigned int val = token_decode(p);
+               unsigned int marker = (val >> 24) & 0xff;
+               if (val == DECODE_ERROR)
+                       return -1;
+               *q++ = (val >> 16) & 0xff;
+               if (marker < 2)
+                       *q++ = (val >> 8) & 0xff;
+               if (marker < 1)
+                       *q++ = val & 0xff;
+       }
+       return q - (unsigned char *) data;
+}
+
index 57cada8..9182137 100644 (file)
  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  *
- * $DragonFly: src/libexec/dma/crypto.c,v 1.2 2008/03/04 11:36:08 matthias Exp $
+ * $DragonFly: src/libexec/dma/crypto.c,v 1.3 2008/09/02 15:11:49 matthias Exp $
  */
 
 #ifdef HAVE_CRYPTO
 
 #include <openssl/x509.h>
+#include <openssl/md5.h>
 #include <openssl/ssl.h>
 #include <openssl/err.h>
 #include <openssl/pem.h>
@@ -118,9 +119,9 @@ smtp_init_crypto(struct qitem *it, int fd, int feature)
                config->features |= NOSSL;
 
                send_remote_command(fd, "EHLO %s", hostname());
-               if (read_remote(fd) == 2) {
+               if (read_remote(fd, 0, NULL) == 2) {
                        send_remote_command(fd, "STARTTLS");
-                       if (read_remote(fd) != 2) {
+                       if (read_remote(fd, 0, NULL) != 2) {
                                syslog(LOG_ERR, "%s: remote delivery failed:"
                                  " STARTTLS not available: %m", it->queueid);
                                config->features &= ~NOSSL;
@@ -169,16 +170,137 @@ smtp_init_crypto(struct qitem *it, int fd, int feature)
        return (0);
 }
 
-#if 0
+/*
+ * hmac_md5() taken out of RFC 2104.  This RFC was written by H. Krawczyk,
+ * M. Bellare and R. Canetti.
+ */ 
+void
+hmac_md5(text, text_len, key, key_len, digest)
+unsigned char*  text;                /* pointer to data stream */
+int             text_len;            /* length of data stream */
+unsigned char*  key;                 /* pointer to authentication key */
+int             key_len;             /* length of authentication key */
+caddr_t         digest;              /* caller digest to be filled in */
+
+{
+        MD5_CTX context;
+        unsigned char k_ipad[65];    /* inner padding -
+                                      * key XORd with ipad
+                                      */
+        unsigned char k_opad[65];    /* outer padding -
+                                      * key XORd with opad
+                                      */
+        unsigned char tk[16];
+        int i;
+        /* if key is longer than 64 bytes reset it to key=MD5(key) */
+        if (key_len > 64) {
+
+                MD5_CTX      tctx;
+
+                MD5_Init(&tctx);
+                MD5_Update(&tctx, key, key_len);
+                MD5_Final(tk, &tctx);
+
+                key = tk;
+                key_len = 16;
+        }
+
+        /*
+         * the HMAC_MD5 transform looks like:
+         *
+         * MD5(K XOR opad, MD5(K XOR ipad, text))
+         *
+         * where K is an n byte key
+         * ipad is the byte 0x36 repeated 64 times
+        *
+         * opad is the byte 0x5c repeated 64 times
+         * and text is the data being protected
+         */
+
+        /* start out by storing key in pads */
+        bzero( k_ipad, sizeof k_ipad);
+        bzero( k_opad, sizeof k_opad);
+        bcopy( key, k_ipad, key_len);
+        bcopy( key, k_opad, key_len);
+
+        /* XOR key with ipad and opad values */
+        for (i=0; i<64; i++) {
+                k_ipad[i] ^= 0x36;
+                k_opad[i] ^= 0x5c;
+        }
+        /*
+         * perform inner MD5
+         */
+        MD5_Init(&context);                   /* init context for 1st
+                                              * pass */
+        MD5_Update(&context, k_ipad, 64);     /* start with inner pad */
+        MD5_Update(&context, text, text_len); /* then text of datagram */
+        MD5_Final(digest, &context);          /* finish up 1st pass */
+        /*
+         * perform outer MD5
+         */
+        MD5_Init(&context);                   /* init context for 2nd
+                                              * pass */
+        MD5_Update(&context, k_opad, 64);     /* start with outer pad */
+        MD5_Update(&context, digest, 16);     /* then results of 1st
+                                              * hash */
+        MD5_Final(digest, &context);          /* finish up 2nd pass */
+}
+
 /*
  * CRAM-MD5 authentication
- *
- * XXX TODO implement me, I don't have a mail server with CRAM-MD5 available
  */
 int
-smtp_auth_md5(int fd, char *login, char *password)
+smtp_auth_md5(struct qitem *it, int fd, char *login, char *password)
 {
+       unsigned char buffer[BUF_SIZE], digest[BUF_SIZE], ascii_digest[33];
+       char *temp;
+       int len, i;
+       static char hextab[] = "0123456789abcdef";
+
+       temp = calloc(BUF_SIZE, 1);
+       memset(buffer, 0, sizeof(buffer));
+       memset(digest, 0, sizeof(digest));
+       memset(ascii_digest, 0, sizeof(ascii_digest));
+
+       /* Send AUTH command according to RFC 2554 */
+       send_remote_command(fd, "AUTH CRAM-MD5");
+       if (read_remote(fd, sizeof(buffer), buffer) != 3) {
+               syslog(LOG_ERR, "%s: smarthost authentification:"
+                      " AUTH cram-md5 not available: %m", it->queueid);
+               /* if cram-md5 is not available */
+               return (-1);
+       }
+
+       /* skip 3 char status + 1 char space */
+       base64_decode(buffer + 4, temp);
+       hmac_md5(temp, strlen(temp), password, strlen(password), digest);
+
+       ascii_digest[32] = 0;
+       for (i = 0; i < 16; i++) {
+               ascii_digest[2*i] = hextab[digest[i] >> 4];
+               ascii_digest[2*i+1] = hextab[digest[i] & 15];
+       }
+
+       /* prepare answer */
+       snprintf(buffer, BUF_SIZE, "%s %s", login, ascii_digest);
+
+       /* temp will be allocated inside base64_encode again */
+       free(temp);
+       /* encode answer */
+       len = base64_encode(buffer, strlen(buffer), &temp);
+       if (len <= 0)
+               return (-1);
+
+       /* send answer */
+       send_remote_command(fd, "%s", temp);
+       if (read_remote(fd, 0, NULL) != 2) {
+               syslog(LOG_ERR, "%s: remote delivery deferred:"
+                               " AUTH cram-md5 failed: %m", it->queueid);
+               return (-2);
+       }
+
+       return (0);
 }
-#endif /* 0 */
 
 #endif /* HAVE_CRYPTO */
index c364f08..b8bc21b 100644 (file)
@@ -32,7 +32,7 @@
  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  *
- * $DragonFly: src/libexec/dma/dma.h,v 1.5 2008/03/04 11:36:09 matthias Exp $
+ * $DragonFly: src/libexec/dma/dma.h,v 1.6 2008/09/02 15:11:49 matthias Exp $
  */
 
 #ifndef DMA_H
@@ -146,12 +146,13 @@ extern int smtp_init_crypto(struct qitem *, int, int);
 #endif /* HAVE_CRYPTO */
 
 /* net.c */
-extern int read_remote(int);
+extern int read_remote(int, int, char *);
 extern ssize_t send_remote_command(int, const char*, ...);
 extern int deliver_remote(struct qitem *, const char **);
 
 /* base64.c */
 extern int base64_encode(const void *, int, char **);
+extern int base64_decode(const char *, void *);
 
 /* dma.c */
 extern char * hostname(void);
index 111f333..fbde18e 100644 (file)
@@ -32,7 +32,7 @@
  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  *
- * $DragonFly: src/libexec/dma/net.c,v 1.6 2008/04/20 13:44:24 swildner Exp $
+ * $DragonFly: src/libexec/dma/net.c,v 1.7 2008/09/02 15:11:49 matthias Exp $
  */
 
 #include <sys/param.h>
@@ -90,12 +90,12 @@ send_remote_command(int fd, const char* fmt, ...)
 }
 
 int
-read_remote(int fd)
+read_remote(int fd, int extbufsize, char *extbuf)
 {
        ssize_t rlen = 0;
        size_t pos, len;
        char buff[BUF_SIZE];
-       int done = 0, status = 0;
+       int done = 0, status = 0, extbufpos = 0;
 
        if (signal(SIGALRM, sig_alarm) == SIG_ERR) {
                syslog(LOG_ERR, "SIGALRM error: %m");
@@ -111,6 +111,7 @@ read_remote(int fd)
         * OpenBSD and released under a BSD style license.
         */
        for (len = pos = 0; !done; ) {
+               rlen = 0;
                if (pos == 0 ||
                    (pos > 0 && memchr(buff + pos, '\n', len - pos) == NULL)) {
                        memmove(buff, buff + pos, len - pos);
@@ -128,6 +129,21 @@ read_remote(int fd)
                        }
                        len += rlen;
                }
+               /*
+                * If there is an external buffer with a size bigger than zero
+                * and as long as there is space in the external buffer and
+                * there are new characters read from the mailserver
+                * copy them to the external buffer
+                */
+               if (extbufpos <= (extbufsize - 1) && rlen && extbufsize > 0 
+                   && extbuf != NULL) {
+                       /* do not write over the bounds of the buffer */
+                       if(extbufpos + rlen > (extbufsize - 1)) {
+                               rlen = extbufsize - extbufpos;
+                       }
+                       memcpy(extbuf + extbufpos, buff + len - rlen, rlen);
+                       extbufpos += rlen;
+               }
                for (; pos < len && buff[pos] >= '0' && buff[pos] <= '9'; pos++)
                        ; /* Do nothing */
 
@@ -152,8 +168,6 @@ read_remote(int fd)
 
 /*
  * Handle SMTP authentication
- *
- * XXX TODO: give me AUTH CRAM-MD5
  */
 static int
 smtp_login(struct qitem *it, int fd, char *login, char* password)
@@ -161,39 +175,58 @@ smtp_login(struct qitem *it, int fd, char *login, char* password)
        char *temp;
        int len, res = 0;
 
-       /* Send AUTH command according to RFC 2554 */
-       send_remote_command(fd, "AUTH LOGIN");
-       if (read_remote(fd) != 3) {
-               syslog(LOG_ERR, "%s: remote delivery deferred:"
-                      " AUTH login not available: %m", it->queueid);
-               return (1);
+#ifdef HAVE_CRYPTO
+       res = smtp_auth_md5(it, fd, login, password);
+       if (res == 0) {
+               return (0);
+       } else if (res == -2) {
+       /*
+        * If the return code is -2, then then the login attempt failed, 
+        * do not try other login mechanisms
+        */
+               return (-1);
        }
+#endif /* HAVE_CRYPTO */
 
-       len = base64_encode(login, strlen(login), &temp);
-       if (len <= 0)
-               return (-1);
+       if ((config->features & INSECURE) != 0) {
+               /* Send AUTH command according to RFC 2554 */
+               send_remote_command(fd, "AUTH LOGIN");
+               if (read_remote(fd, 0, NULL) != 3) {
+                       syslog(LOG_ERR, "%s: remote delivery deferred:"
+                                       " AUTH login not available: %m", it->queueid);
+                       return (1);
+               }
 
-       send_remote_command(fd, "%s", temp);
-       if (read_remote(fd) != 3) {
-               syslog(LOG_ERR, "%s: remote delivery deferred:"
-                      " AUTH login failed: %m", it->queueid);
-               return (-1);
-       }
+               len = base64_encode(login, strlen(login), &temp);
+               if (len <= 0)
+                       return (-1);
 
-       len = base64_encode(password, strlen(password), &temp);
-       if (len <= 0)
-               return (-1);
+               send_remote_command(fd, "%s", temp);
+               if (read_remote(fd, 0, NULL) != 3) {
+                       syslog(LOG_ERR, "%s: remote delivery deferred:"
+                                       " AUTH login failed: %m", it->queueid);
+                       return (-1);
+               }
 
-       send_remote_command(fd, "%s", temp);
-       res = read_remote(fd);
-       if (res == 5) {
-               syslog(LOG_ERR, "%s: remote delivery failed:"
-                      " Authentication failed: %m", it->queueid);
-               return (-1);
-       } else if (res != 2) {
-               syslog(LOG_ERR, "%s: remote delivery failed:"
-                      " AUTH password failed: %m", it->queueid);
-               return (-1);
+               len = base64_encode(password, strlen(password), &temp);
+               if (len <= 0)
+                       return (-1);
+
+               send_remote_command(fd, "%s", temp);
+               res = read_remote(fd, 0, NULL);
+               if (res == 5) {
+                       syslog(LOG_ERR, "%s: remote delivery failed:"
+                                       " Authentication failed: %m", it->queueid);
+                       return (-1);
+               } else if (res != 2) {
+                       syslog(LOG_ERR, "%s: remote delivery failed:"
+                                       " AUTH password failed: %m", it->queueid);
+                       return (-1);
+               }
+       } else {
+               syslog(LOG_ERR, "%s: non-encrypted SMTP login is disabled in config, so skipping it. ",
+                               it->queueid);
+               return (1);
        }
 
        return (0);
@@ -212,6 +245,7 @@ open_connection(struct qitem *it, const char *host)
        else
                port = SMTP_PORT;
 
+       /* FIXME get MX record of host */
        /* Shamelessly taken from getaddrinfo(3) */
        memset(&hints, 0, sizeof(hints));
        hints.ai_family = PF_UNSPEC;
@@ -279,7 +313,7 @@ deliver_remote(struct qitem *it, const char **errmsg)
 
        /* Check first reply from remote host */
        config->features |= NOSSL;
-       res = read_remote(fd);
+       res = read_remote(fd, 0, NULL);
        if (res != 2) {
                syslog(LOG_INFO, "%s: Invalid initial response: %i",
                        it->queueid, res);
@@ -304,7 +338,7 @@ deliver_remote(struct qitem *it, const char **errmsg)
        if (((config->features & STARTTLS) == 0) &&
            ((config->features & SECURETRANS) != 0)) {
                send_remote_command(fd, "EHLO %s", hostname());
-               if (read_remote(fd) != 2) {
+               if (read_remote(fd, 0, NULL) != 2) {
                        syslog(LOG_ERR, "%s: remote delivery deferred: "
                               " EHLO failed: %m", it->queueid);
                        return (-1);
@@ -313,7 +347,7 @@ deliver_remote(struct qitem *it, const char **errmsg)
 #endif /* HAVE_CRYPTO */
        if (((config->features & SECURETRANS) == 0)) {
                send_remote_command(fd, "EHLO %s", hostname());
-               if (read_remote(fd) != 2) {
+               if (read_remote(fd, 0, NULL) != 2) {
                        syslog(LOG_ERR, "%s: remote delivery deferred: "
                               " EHLO failed: %m", it->queueid);
                        return (-1);
@@ -336,44 +370,36 @@ deliver_remote(struct qitem *it, const char **errmsg)
                 * Check if the user wants plain text login without using
                 * encryption.
                 */
-               if ((config->features & INSECURE) != 0) {
-                       syslog(LOG_INFO, "%s: Use SMTP authentication",
+               syslog(LOG_INFO, "%s: Use SMTP authentication",
                                it->queueid);
-                       error = smtp_login(it, fd, a->login, a->password);
-                       if (error < 0) {
-                               syslog(LOG_ERR, "%s: remote delivery failed:"
+               error = smtp_login(it, fd, a->login, a->password);
+               if (error < 0) {
+                       syslog(LOG_ERR, "%s: remote delivery failed:"
                                        " SMTP login failed: %m", it->queueid);
-                               return (-1);
-                       }
-                       /* SMTP login is not available, so try without */
-                       else if (error > 0)
-                               syslog(LOG_ERR, "%s: SMTP login not available."
-                                       " Try without", it->queueid);
-               } else {
-                       syslog(LOG_ERR, "%s: Skip SMTP login. ",
-                               it->queueid);
+                       return (-1);
                }
+               /* SMTP login is not available, so try without */
+               else if (error > 0)
+                       syslog(LOG_ERR, "%s: SMTP login not available."
+                                       " Try without", it->queueid);
        }
 
        send_remote_command(fd, "MAIL FROM:<%s>", it->sender);
-       if (read_remote(fd) != 2) {
+       if (read_remote(fd, 0, NULL) != 2) {
                syslog(LOG_ERR, "%s: remote delivery deferred:"
                       " MAIL FROM failed: %m", it->queueid);
                return (1);
        }
 
-       /* XXX TODO:
-        * Iterate over all recepients and open only one connection
-        */
        send_remote_command(fd, "RCPT TO:<%s>", it->addr);
-       if (read_remote(fd) != 2) {
+       if (read_remote(fd, 0, NULL) != 2) {
                syslog(LOG_ERR, "%s: remote delivery deferred:"
-                      " RCPT TO failed: %m", it->queueid);
+                               " RCPT TO failed: %m", it->queueid);
                return (1);
        }
 
        send_remote_command(fd, "DATA");
-       if (read_remote(fd) != 3) {
+       if (read_remote(fd, 0, NULL) != 3) {
                syslog(LOG_ERR, "%s: remote delivery deferred:"
                       " DATA failed: %m", it->queueid);
                return (1);
@@ -416,14 +442,14 @@ deliver_remote(struct qitem *it, const char **errmsg)
        }
 
        send_remote_command(fd, ".");
-       if (read_remote(fd) != 2) {
+       if (read_remote(fd, 0, NULL) != 2) {
                syslog(LOG_ERR, "%s: remote delivery deferred: %m",
                       it->queueid);
                return (1);
        }
 
        send_remote_command(fd, "QUIT");
-       if (read_remote(fd) != 2) {
+       if (read_remote(fd, 0, NULL) != 2) {
                syslog(LOG_ERR, "%s: remote delivery deferred: "
                       "QUIT failed: %m", it->queueid);
                return (1);