From 1fa7a882dd22d5f619b3645c6597a419034e9b4e Mon Sep 17 00:00:00 2001
From: Michael Tremer <michael.tremer@ipfire.org>
Date: Mon, 9 Nov 2015 21:52:08 +0000
Subject: [PATCH] Implement better authentication

DMA tries to authenticate by simply trying various authentication
mechanisms. This is obviously not conforming to RFC and some mail
providers detect this is spam and reject all emails.

This patch parses the EHLO response and reads various keywords
from it that can then later in the program be used to jump into
certain code paths.

Currently this is used to only authenticate with CRAM-MD5 and/or
LOGIN if the server supports one or both of these. The
implementation can be easily be extended though.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
---
 crypto.c |   6 +-
 dma.h    |  13 +++-
 net.c    | 219 +++++++++++++++++++++++++++++++++++++++++++++++----------------
 3 files changed, 181 insertions(+), 57 deletions(-)

diff --git a/crypto.c b/crypto.c
index 897b55b..8048f20 100644
--- a/crypto.c
+++ b/crypto.c
@@ -77,7 +77,7 @@ init_cert_file(SSL_CTX *ctx, const char *path)
 }
 
 int
-smtp_init_crypto(int fd, int feature)
+smtp_init_crypto(int fd, int feature, struct smtp_features* features)
 {
 	SSL_CTX *ctx = NULL;
 #if (OPENSSL_VERSION_NUMBER >= 0x00909000L)
@@ -118,8 +118,7 @@ smtp_init_crypto(int fd, int feature)
 		/* TLS init phase, disable SSL_write */
 		config.features |= NOSSL;
 
-		send_remote_command(fd, "EHLO %s", hostname());
-		if (read_remote(fd, 0, NULL) == 2) {
+		if (perform_server_greeting(fd, features) == 0) {
 			send_remote_command(fd, "STARTTLS");
 			if (read_remote(fd, 0, NULL) != 2) {
 				if ((feature & TLS_OPP) == 0) {
@@ -131,6 +130,7 @@ smtp_init_crypto(int fd, int feature)
 				}
 			}
 		}
+
 		/* End of TLS init phase, enable SSL_write/read */
 		config.features &= ~NOSSL;
 	}
diff --git a/dma.h b/dma.h
index acf5e44..ee749d8 100644
--- a/dma.h
+++ b/dma.h
@@ -51,6 +51,7 @@
 #define BUF_SIZE	2048
 #define ERRMSG_SIZE	200
 #define USERNAME_SIZE	50
+#define EHLO_RESPONSE_SIZE BUF_SIZE
 #define MIN_RETRY	300		/* 5 minutes */
 #define MAX_RETRY	(3*60*60)	/* retry at least every 3 hours */
 #define MAX_TIMEOUT	(5*24*60*60)	/* give up after 5 days */
@@ -160,6 +161,15 @@ struct mx_hostentry {
 	struct sockaddr_storage	sa;
 };
 
+struct smtp_auth_mechanisms {
+	int cram_md5;
+	int login;
+};
+
+struct smtp_features {
+	struct smtp_auth_mechanisms auth;
+	int starttls;
+};
 
 /* global variables */
 extern struct aliases aliases;
@@ -187,7 +197,7 @@ void parse_authfile(const char *);
 /* crypto.c */
 void hmac_md5(unsigned char *, int, unsigned char *, int, unsigned char *);
 int smtp_auth_md5(int, char *, char *);
-int smtp_init_crypto(int, int);
+int smtp_init_crypto(int, int, struct smtp_features*);
 
 /* dns.c */
 int dns_get_mx_list(const char *, int, struct mx_hostentry **, int);
@@ -196,6 +206,7 @@ int dns_get_mx_list(const char *, int, struct mx_hostentry **, int);
 char *ssl_errstr(void);
 int read_remote(int, int, char *);
 ssize_t send_remote_command(int, const char*, ...)  __attribute__((__nonnull__(2), __format__ (__printf__, 2, 3)));
+int perform_server_greeting(int, struct smtp_features*);
 int deliver_remote(struct qitem *);
 
 /* base64.c */
diff --git a/net.c b/net.c
index 26935a8..33ff8f5 100644
--- a/net.c
+++ b/net.c
@@ -247,64 +247,70 @@ read_remote(int fd, int extbufsize, char *extbuf)
  * Handle SMTP authentication
  */
 static int
-smtp_login(int fd, char *login, char* password)
+smtp_login(int fd, char *login, char* password, const struct smtp_features* features)
 {
 	char *temp;
 	int len, res = 0;
 
-	res = smtp_auth_md5(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);
-	}
-
-	if ((config.features & INSECURE) != 0 ||
-	    (config.features & SECURETRANS) != 0) {
-		/* Send AUTH command according to RFC 2554 */
-		send_remote_command(fd, "AUTH LOGIN");
-		if (read_remote(fd, 0, NULL) != 3) {
-			syslog(LOG_NOTICE, "remote delivery deferred:"
-					" AUTH login not available: %s",
-					neterr);
+	// CRAM-MD5
+	if (features->auth.cram_md5) {
+		res = smtp_auth_md5(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);
 		}
+	}
 
-		len = base64_encode(login, strlen(login), &temp);
-		if (len < 0) {
+	// LOGIN
+	if (features->auth.login) {
+		if ((config.features & INSECURE) != 0 ||
+		    (config.features & SECURETRANS) != 0) {
+			/* Send AUTH command according to RFC 2554 */
+			send_remote_command(fd, "AUTH LOGIN");
+			if (read_remote(fd, 0, NULL) != 3) {
+				syslog(LOG_NOTICE, "remote delivery deferred:"
+						" AUTH login not available: %s",
+						neterr);
+				return (1);
+			}
+
+			len = base64_encode(login, strlen(login), &temp);
+			if (len < 0) {
 encerr:
-			syslog(LOG_ERR, "can not encode auth reply: %m");
-			return (1);
-		}
+				syslog(LOG_ERR, "can not encode auth reply: %m");
+				return (1);
+			}
 
-		send_remote_command(fd, "%s", temp);
-		free(temp);
-		res = read_remote(fd, 0, NULL);
-		if (res != 3) {
-			syslog(LOG_NOTICE, "remote delivery %s: AUTH login failed: %s",
-			       res == 5 ? "failed" : "deferred", neterr);
-			return (res == 5 ? -1 : 1);
-		}
+			send_remote_command(fd, "%s", temp);
+			free(temp);
+			res = read_remote(fd, 0, NULL);
+			if (res != 3) {
+				syslog(LOG_NOTICE, "remote delivery %s: AUTH login failed: %s",
+				       res == 5 ? "failed" : "deferred", neterr);
+				return (res == 5 ? -1 : 1);
+			}
 
-		len = base64_encode(password, strlen(password), &temp);
-		if (len < 0)
-			goto encerr;
-
-		send_remote_command(fd, "%s", temp);
-		free(temp);
-		res = read_remote(fd, 0, NULL);
-		if (res != 2) {
-			syslog(LOG_NOTICE, "remote delivery %s: Authentication failed: %s",
-					res == 5 ? "failed" : "deferred", neterr);
-			return (res == 5 ? -1 : 1);
+			len = base64_encode(password, strlen(password), &temp);
+			if (len < 0)
+				goto encerr;
+
+			send_remote_command(fd, "%s", temp);
+			free(temp);
+			res = read_remote(fd, 0, NULL);
+			if (res != 2) {
+				syslog(LOG_NOTICE, "remote delivery %s: Authentication failed: %s",
+						res == 5 ? "failed" : "deferred", neterr);
+				return (res == 5 ? -1 : 1);
+			}
+		} else {
+			syslog(LOG_WARNING, "non-encrypted SMTP login is disabled in config, so skipping it. ");
+			return (1);
 		}
-	} else {
-		syslog(LOG_WARNING, "non-encrypted SMTP login is disabled in config, so skipping it. ");
-		return (1);
 	}
 
 	return (0);
@@ -348,10 +354,115 @@ close_connection(int fd)
 	close(fd);
 }
 
+static void parse_auth_line(char* line, struct smtp_auth_mechanisms* auth) {
+	// Skip the auth prefix
+	line += strlen("AUTH ");
+
+	char* method = strtok(line, " ");
+	while (method) {
+		if (strcmp(method, "CRAM-MD5") == 0)
+			auth->cram_md5 = 1;
+
+		else if (strcmp(method, "LOGIN") == 0)
+			auth->login = 1;
+
+		method = strtok(NULL, " ");
+	}
+}
+
+int perform_server_greeting(int fd, struct smtp_features* features) {
+	/*
+		Send EHLO
+		XXX allow HELO fallback
+	*/
+	send_remote_command(fd, "EHLO %s", hostname());
+
+	char buffer[EHLO_RESPONSE_SIZE];
+	memset(buffer, 0, sizeof(buffer));
+
+	int res = read_remote(fd, sizeof(buffer) - 1, buffer);
+
+	// Got an unexpected response
+	if (res != 2)
+		return -1;
+
+	// Reset all features
+	memset(features, 0, sizeof(*features));
+
+	// Run through the buffer line by line
+	char linebuffer[EHLO_RESPONSE_SIZE];
+	char* p = buffer;
+
+	while (*p) {
+		char* line = linebuffer;
+		while (*p && *p != '\n') {
+			*line++ = *p++;
+		}
+
+		// p should never point to NULL after the loop
+		// above unless we reached the end of the buffer.
+		// In that case we will raise an error.
+		if (!*p) {
+			return -1;
+		}
+
+		// Otherwise p points to the newline character which
+		// we will skip.
+		p++;
+
+		// Terminte the string (and remove the carriage-return character)
+		*--line = '\0';
+		line = linebuffer;
+
+		// End main loop for empty lines
+		if (*line == '\0')
+			break;
+
+		// Process the line
+		// - Must start with 250, followed by dash or space
+		// - We won't check for the correct usage of space and dash because
+		//    that is already done in read_remote().
+		if ((strncmp(line, "250-", 4) != 0) && (strncmp(line, "250 ", 4) != 0)) {
+			syslog(LOG_ERR, "Invalid line: %s\n", line);
+			return -1;
+		}
+
+		// Skip the prefix
+		line += 4;
+
+		// Check for STARTTLS
+		if (strcmp(line, "STARTTLS") == 0)
+			features->starttls = 1;
+
+		// Parse authentication mechanisms
+		else if (strncmp(line, "AUTH ", 5) == 0)
+			parse_auth_line(line, &features->auth);
+	}
+
+	syslog(LOG_DEBUG, "Server greeting successfully completed");
+
+	// STARTTLS
+	if (features->starttls)
+		syslog(LOG_DEBUG, "  Server supports STARTTLS");
+	else
+		syslog(LOG_DEBUG, "  Server does not support STARTTLS");
+
+	// Authentication
+	if (features->auth.cram_md5) {
+		syslog(LOG_DEBUG, "  Server supports CRAM-MD5 authentication");
+	}
+	if (features->auth.login) {
+		syslog(LOG_DEBUG, "  Server supports LOGIN authentication");
+	}
+
+	return 0;
+}
+
 static int
 deliver_to_host(struct qitem *it, struct mx_hostentry *host)
 {
 	struct authuser *a;
+	struct smtp_features features;
 	char line[1000];
 	size_t linelen;
 	int fd, error = 0, do_auth = 0, res = 0;
@@ -389,7 +500,7 @@ deliver_to_host(struct qitem *it, struct mx_hostentry *host)
 	}
 
 	if ((config.features & SECURETRANS) != 0) {
-		error = smtp_init_crypto(fd, config.features);
+		error = smtp_init_crypto(fd, config.features, &features);
 		if (error == 0)
 			syslog(LOG_DEBUG, "SSL initialization successful");
 		else
@@ -399,10 +510,12 @@ deliver_to_host(struct qitem *it, struct mx_hostentry *host)
 			READ_REMOTE_CHECK("connect", 2);
 	}
 
-	/* XXX allow HELO fallback */
-	/* XXX record ESMTP keywords */
-	send_remote_command(fd, "EHLO %s", hostname());
-	READ_REMOTE_CHECK("EHLO", 2);
+	// Say EHLO
+	if (perform_server_greeting(fd, &features) != 0) {
+		syslog(LOG_ERR, "Could not perform server greeting at %s [%s]: %s",
+			host->host, host->addr, neterr);
+		return -1;
+	}
 
 	/*
 	 * Use SMTP authentication if the user defined an entry for the remote
@@ -421,7 +534,7 @@ deliver_to_host(struct qitem *it, struct mx_hostentry *host)
 		 * encryption.
 		 */
 		syslog(LOG_INFO, "using SMTP authentication for user %s", a->login);
-		error = smtp_login(fd, a->login, a->password);
+		error = smtp_login(fd, a->login, a->password, &features);
 		if (error < 0) {
 			syslog(LOG_ERR, "remote delivery failed:"
 					" SMTP login failed: %m");
