summaryrefslogtreecommitdiff
path: root/qmail-smtpd.c
diff options
context:
space:
mode:
authorJohn Denker <jsd@av8n.com>2016-01-01 18:15:35 (GMT)
committerJohn Denker <jsd@av8n.com>2016-01-02 00:33:29 (GMT)
commita16bea1ca0aa3ef44919fbe045b9040874fd8628 (patch)
tree99ac443b96f8b89f8a480bb378b619d18e8cfc31 /qmail-smtpd.c
parent4dabcdf185f53439af8fdf71bd2da7317336bcf0 (diff)
the big starttls patch
Diffstat (limited to 'qmail-smtpd.c')
-rw-r--r--qmail-smtpd.c292
1 files changed, 291 insertions, 1 deletions
diff --git a/qmail-smtpd.c b/qmail-smtpd.c
index 582a695..b545760 100644
--- a/qmail-smtpd.c
+++ b/qmail-smtpd.c
@@ -37,9 +37,27 @@
unsigned int databytes = 0;
int timeout = 1200;
+const char *protocol = "SMTP";
+
+#ifdef TLS
+#include <sys/stat.h>
+#include "tls.h"
+#include "ssl_timeoutio.h"
+
+void tls_init();
+int tls_verify();
+void tls_nogateway();
+int ssl_rfd = -1, ssl_wfd = -1; /* SSL_get_Xfd() are broken */
+#endif
+
int safewrite(fd,buf,len) int fd; char *buf; int len;
{
int r;
+#ifdef TLS
+ if (ssl && fd == ssl_wfd)
+ r = ssl_timeoutwrite(timeout, ssl_rfd, ssl_wfd, ssl, buf, len);
+ else
+#endif
r = timeoutwrite(timeout,fd,buf,len);
if (r <= 0) _exit(1);
return r;
@@ -60,7 +78,16 @@ void straynewline() { out("451 See http://pobox.com/~djb/docs/smtplf.html.\r\n")
void err_badbounce() { out("550 sorry, bounce messages should have a single envelope recipient (#5.7.1)\r\n"); }
void err_bmf() { out("553 sorry, your envelope sender is in my badmailfrom list (#5.7.1)\r\n"); }
+#ifndef TLS
void err_nogateway() { out("553 sorry, that domain isn't in my list of allowed rcpthosts (#5.7.1)\r\n"); }
+#else
+void err_nogateway()
+{
+ out("553 sorry, that domain isn't in my list of allowed rcpthosts");
+ tls_nogateway();
+ out(" (#5.7.1)\r\n");
+}
+#endif
void err_unimpl(arg) char *arg; { out("502 unimplemented (#5.5.1)\r\n"); }
void err_syntax() { out("555 syntax error (#5.5.4)\r\n"); }
void err_relay() { out("553 we don't relay (#5.7.1)\r\n"); }
@@ -151,6 +178,11 @@ void setup()
if (!remotehost) remotehost = "unknown";
remoteinfo = env_get("TCPREMOTEINFO");
relayclient = env_get("RELAYCLIENT");
+
+#ifdef TLS
+ if (env_get("SMTPS")) { smtps = 1; tls_init(); }
+ else
+#endif
dohelo(remotehost);
}
@@ -236,6 +268,9 @@ int addrallowed()
int r;
r = rcpthosts(addr.s,str_len(addr.s));
if (r == -1) die_control();
+#ifdef TLS
+ if (r == 0) if (tls_verify()) r = -2;
+#endif
return r;
}
@@ -268,9 +303,17 @@ void smtp_helo(arg) char *arg;
smtp_greet("250 "); out("\r\n");
seenmail = 0; dohelo(arg);
}
+/* ESMTP extensions are published here */
void smtp_ehlo(arg) char *arg;
{
+#ifdef TLS
+ struct stat st;
+#endif
smtp_greet("250-");
+#ifdef TLS
+ if (!ssl && (stat("control/servercert.pem",&st) == 0))
+ out("\r\n250-STARTTLS");
+#endif
out("\r\n250-PIPELINING\r\n250 8BITMIME\r\n");
seenmail = 0; dohelo(arg);
}
@@ -313,6 +356,11 @@ int saferead(fd,buf,len) int fd; char *buf; int len;
{
int r;
flush();
+#ifdef TLS
+ if (ssl && fd == ssl_rfd)
+ r = ssl_timeoutread(timeout, ssl_rfd, ssl_wfd, ssl, buf, len);
+ else
+#endif
r = timeoutread(timeout,fd,buf,len);
if (r == -1) if (errno == error_timeout) die_alarm();
if (r <= 0) die_read();
@@ -321,6 +369,9 @@ int saferead(fd,buf,len) int fd; char *buf; int len;
char ssinbuf[1024];
substdio ssin = SUBSTDIO_FDBUF(saferead,0,ssinbuf,sizeof ssinbuf);
+#ifdef TLS
+void flush_io() { ssin.p = 0; flush(); }
+#endif
struct qmail qqt;
unsigned int bytestooverflow = 0;
@@ -423,7 +474,7 @@ void smtp_data(arg) char *arg; {
qp = qmail_qp(&qqt);
out("354 go ahead\r\n");
- received(&qqt,"SMTP",local,remoteip,remotehost,remoteinfo,fakehelo);
+ received(&qqt,protocol,local,remoteip,remotehost,remoteinfo,fakehelo);
blast(&hops);
hops = (hops >= MAXHOPS);
if (hops) qmail_fail(&qqt);
@@ -659,6 +710,242 @@ char *arg;
}
}
+#ifdef TLS
+stralloc proto = {0};
+int ssl_verified = 0;
+const char *ssl_verify_err = 0;
+
+void smtp_tls(char *arg)
+{
+ if (ssl) err_unimpl();
+ else if (*arg) out("501 Syntax error (no parameters allowed) (#5.5.4)\r\n");
+ else tls_init();
+}
+
+RSA *tmp_rsa_cb(SSL *ssl, int export, int keylen)
+{
+ if (!export) keylen = 2048;
+ if (keylen == 2048) {
+ FILE *in = fopen("control/rsa2048.pem", "r");
+ if (in) {
+ RSA *rsa = PEM_read_RSAPrivateKey(in, NULL, NULL, NULL);
+ fclose(in);
+ if (rsa) return rsa;
+ }
+ }
+ return RSA_generate_key(keylen, RSA_F4, NULL, NULL);
+}
+
+DH *tmp_dh_cb(SSL *ssl, int export, int keylen)
+{
+ if (!export) keylen = 2048;
+ if (keylen == 2048) {
+ FILE *in = fopen("control/dh2048.pem", "r");
+ if (in) {
+ DH *dh = PEM_read_DHparams(in, NULL, NULL, NULL);
+ fclose(in);
+ if (dh) return dh;
+ }
+ }
+ return DH_generate_parameters(keylen, DH_GENERATOR_2, NULL, NULL);
+}
+
+/* don't want to fail handshake if cert isn't verifiable */
+int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { return 1; }
+
+void tls_nogateway()
+{
+ /* there may be cases when relayclient is set */
+ if (!ssl || relayclient) return;
+ out("; no valid cert for gatewaying");
+ if (ssl_verify_err) { out(": "); out(ssl_verify_err); }
+}
+void tls_out(const char *s1, const char *s2)
+{
+ out("454 TLS "); out(s1);
+ if (s2) { out(": "); out(s2); }
+ out(" (#4.3.0)\r\n"); flush();
+}
+void tls_err(const char *s) { tls_out(s, ssl_error()); if (smtps) die_read(); }
+
+# define CLIENTCA "control/clientca.pem"
+# define CLIENTCRL "control/clientcrl.pem"
+# define SERVERCERT "control/servercert.pem"
+
+int tls_verify()
+{
+ stralloc clients = {0};
+ struct constmap mapclients;
+
+ if (!ssl || relayclient || ssl_verified) return 0;
+ ssl_verified = 1; /* don't do this twice */
+
+ /* request client cert to see if it can be verified by one of our CAs
+ * and the associated email address matches an entry in tlsclients */
+ switch (control_readfile(&clients, "control/tlsclients", 0))
+ {
+ case 1:
+ if (constmap_init(&mapclients, clients.s, clients.len, 0)) {
+ /* if CLIENTCA contains all the standard root certificates, a
+ * 0.9.6b client might fail with SSL_R_EXCESSIVE_MESSAGE_SIZE;
+ * it is probably due to 0.9.6b supporting only 8k key exchange
+ * data while the 0.9.6c release increases that limit to 100k */
+ STACK_OF(X509_NAME) *sk = SSL_load_client_CA_file(CLIENTCA);
+ if (sk) {
+ SSL_set_client_CA_list(ssl, sk);
+ SSL_set_verify(ssl, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, NULL);
+ break;
+ }
+ constmap_free(&mapclients);
+ }
+ case 0: alloc_free(clients.s); return 0;
+ case -1: die_control();
+ }
+
+ if (ssl_timeoutrehandshake(timeout, ssl_rfd, ssl_wfd, ssl) <= 0) {
+ const char *err = ssl_error_str();
+ tls_out("rehandshake failed", err); die_read();
+ }
+
+ do { /* one iteration */
+ X509 *peercert;
+ X509_NAME *subj;
+ stralloc email = {0};
+
+ int n = SSL_get_verify_result(ssl);
+ if (n != X509_V_OK)
+ { ssl_verify_err = X509_verify_cert_error_string(n); break; }
+ peercert = SSL_get_peer_certificate(ssl);
+ if (!peercert) break;
+
+ subj = X509_get_subject_name(peercert);
+ n = X509_NAME_get_index_by_NID(subj, NID_pkcs9_emailAddress, -1);
+ if (n >= 0) {
+ const ASN1_STRING *s = X509_NAME_get_entry(subj, n)->value;
+ if (s) { email.len = s->length; email.s = s->data; }
+ }
+
+ if (email.len <= 0)
+ ssl_verify_err = "contains no email address";
+ else if (!constmap(&mapclients, email.s, email.len))
+ ssl_verify_err = "email address not in my list of tlsclients";
+ else {
+ /* add the cert email to the proto if it helped allow relaying */
+ --proto.len;
+ if (!stralloc_cats(&proto, "\n (cert ") /* continuation line */
+ || !stralloc_catb(&proto, email.s, email.len)
+ || !stralloc_cats(&proto, ")")
+ || !stralloc_0(&proto)) die_nomem();
+ protocol = proto.s;
+ relayclient = "";
+ /* also inform qmail-queue */
+ if (!env_put("RELAYCLIENT=")) die_nomem();
+ }
+
+ X509_free(peercert);
+ } while (0);
+ constmap_free(&mapclients); alloc_free(clients.s);
+
+ /* we are not going to need this anymore: free the memory */
+ SSL_set_client_CA_list(ssl, NULL);
+ SSL_set_verify(ssl, SSL_VERIFY_NONE, NULL);
+
+ return relayclient ? 1 : 0;
+}
+
+void tls_init()
+{
+ SSL *myssl;
+ SSL_CTX *ctx;
+ const char *ciphers;
+ stralloc saciphers = {0};
+ X509_STORE *store;
+ X509_LOOKUP *lookup;
+
+ SSL_library_init();
+
+ /* a new SSL context with the bare minimum of options */
+ ctx = SSL_CTX_new(SSLv23_server_method());
+ if (!ctx) { tls_err("unable to initialize ctx"); return; }
+
+ /* POODLE vulnerability */
+ SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
+
+ if (!SSL_CTX_use_certificate_chain_file(ctx, SERVERCERT))
+ { SSL_CTX_free(ctx); tls_err("missing certificate"); return; }
+ SSL_CTX_load_verify_locations(ctx, CLIENTCA, NULL);
+
+#if OPENSSL_VERSION_NUMBER >= 0x00907000L
+ /* crl checking */
+ store = SSL_CTX_get_cert_store(ctx);
+ if ((lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file())) &&
+ (X509_load_crl_file(lookup, CLIENTCRL, X509_FILETYPE_PEM) == 1))
+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK |
+ X509_V_FLAG_CRL_CHECK_ALL);
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= 0x10002000L
+ /* support ECDH */
+ SSL_CTX_set_ecdh_auto(ctx,1);
+#endif
+
+ /* set the callback here; SSL_set_verify didn't work before 0.9.6c */
+ SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, verify_cb);
+
+ /* a new SSL object, with the rest added to it directly to avoid copying */
+ myssl = SSL_new(ctx);
+ SSL_CTX_free(ctx);
+ if (!myssl) { tls_err("unable to initialize ssl"); return; }
+
+ /* this will also check whether public and private keys match */
+ if (!SSL_use_RSAPrivateKey_file(myssl, SERVERCERT, SSL_FILETYPE_PEM))
+ { SSL_free(myssl); tls_err("no valid RSA private key"); return; }
+
+ ciphers = env_get("TLSCIPHERS");
+ if (!ciphers) {
+ if (control_readfile(&saciphers, "control/tlsserverciphers", 0) == -1)
+ { SSL_free(myssl); die_control(); }
+ if (saciphers.len) { /* convert all '\0's except the last one to ':' */
+ int i;
+ for (i = 0; i < saciphers.len - 1; ++i)
+ if (!saciphers.s[i]) saciphers.s[i] = ':';
+ ciphers = saciphers.s;
+ }
+ }
+ if (!ciphers || !*ciphers) ciphers = "DEFAULT";
+ SSL_set_cipher_list(myssl, ciphers);
+ alloc_free(saciphers.s);
+
+ SSL_set_tmp_rsa_callback(myssl, tmp_rsa_cb);
+ SSL_set_tmp_dh_callback(myssl, tmp_dh_cb);
+ SSL_set_rfd(myssl, ssl_rfd = substdio_fileno(&ssin));
+ SSL_set_wfd(myssl, ssl_wfd = substdio_fileno(&ssout));
+
+ if (!smtps) { out("220 ready for tls\r\n"); flush(); }
+
+ if (ssl_timeoutaccept(timeout, ssl_rfd, ssl_wfd, myssl) <= 0) {
+ /* neither cleartext nor any other response here is part of a standard */
+ const char *err = ssl_error_str();
+ ssl_free(myssl); tls_out("connection failed", err); die_read();
+ }
+ ssl = myssl;
+
+ /* populate the protocol string, used in Received */
+ if (!stralloc_copys(&proto, "ESMTPS (")
+ || !stralloc_cats(&proto, SSL_get_cipher(ssl))
+ || !stralloc_cats(&proto, " encrypted)")) die_nomem();
+ if (!stralloc_0(&proto)) die_nomem();
+ protocol = proto.s;
+
+ /* have to discard the pre-STARTTLS HELO/EHLO argument, if any */
+ dohelo(remotehost);
+}
+
+# undef SERVERCERT
+# undef CLIENTCA
+
+#endif
+
struct commands smtpcommands[] = {
{ "rcpt", smtp_rcpt, 0 }
, { "mail", smtp_mail, 0 }
@@ -669,6 +956,9 @@ struct commands smtpcommands[] = {
, { "ehlo", smtp_ehlo, flush }
, { "rset", smtp_rset, 0 }
, { "help", smtp_help, flush }
+#ifdef TLS
+, { "starttls", smtp_tls, flush_io }
+#endif
, { "noop", err_noop, flush }
, { "vrfy", err_vrfy, flush }
, { 0, err_unimpl, flush }