summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Denker <jsd@av8n.com>2016-01-01 11:15:35 -0700
committerJohn Denker <jsd@av8n.com>2016-01-01 16:33:29 -0800
commita16bea1ca0aa3ef44919fbe045b9040874fd8628 (patch)
tree99ac443b96f8b89f8a480bb378b619d18e8cfc31
parent4dabcdf185f53439af8fdf71bd2da7317336bcf0 (diff)
the big starttls patch
-rw-r--r--Makefile43
-rw-r--r--TARGETS4
-rw-r--r--conf-cc2
-rw-r--r--dns.c15
-rw-r--r--hier.c3
-rw-r--r--ipalloc.h8
-rw-r--r--qmail-control.99
-rw-r--r--qmail-remote.843
-rw-r--r--qmail-remote.c349
-rw-r--r--qmail-smtpd.865
-rw-r--r--qmail-smtpd.c292
11 files changed, 824 insertions, 9 deletions
diff --git a/Makefile b/Makefile
index 65e9e9c..9db8e63 100644
--- a/Makefile
+++ b/Makefile
@@ -812,7 +812,7 @@ dnsptr dnsip dnsmxip dnsfq hostname ipmeprint qreceipt qsmhook qbiff \
forward preline condredirect bouncesaying except maildirmake \
maildir2mbox maildirwatch qail elq pinq idedit install-big install \
instcheck home home+df proc proc+df binm1 binm1+df binm2 binm2+df \
-binm3 binm3+df
+binm3 binm3+df update_tmprsadh
load: \
make-load warn-auto.sh systype
@@ -1448,6 +1448,7 @@ ndelay.a case.a sig.a open.a lock.a seek.a getln.a stralloc.a alloc.a \
substdio.a error.a str.a fs.a auto_qmail.o dns.lib socket.lib
./load qmail-remote control.o constmap.o timeoutread.o \
timeoutwrite.o timeoutconn.o tcpto.o now.o dns.o ip.o \
+ tls.o ssl_timeoutio.o -L/usr/local/ssl/lib -lssl -lcrypto \
ipalloc.o ipme.o quote.o ndelay.a case.a sig.a open.a \
lock.a seek.a getln.a stralloc.a alloc.a substdio.a error.a \
str.a fs.a auto_qmail.o `cat dns.lib` `cat socket.lib`
@@ -1543,6 +1544,7 @@ open.a sig.a case.a env.a stralloc.a alloc.a substdio.a error.a str.a \
fs.a auto_qmail.o base64.o socket.lib
./load qmail-smtpd rcpthosts.o commands.o timeoutread.o \
timeoutwrite.o ip.o ipme.o ipalloc.o control.o constmap.o \
+ tls.o ssl_timeoutio.o ndelay.a -L/usr/local/ssl/lib -lssl -lcrypto \
received.o date822fmt.o now.o qmail.o cdb.a fd.a wait.a \
datetime.a getln.a open.a sig.a case.a env.a stralloc.a \
alloc.a substdio.a error.a str.a fs.a auto_qmail.o base64.o `cat \
@@ -1832,7 +1834,8 @@ date822fmt.h date822fmt.c dns.h dns.c trylsock.c tryrsolv.c ip.h ip.c \
ipalloc.h ipalloc.c select.h1 select.h2 trysysel.c ndelay.h ndelay.c \
ndelay_off.c direntry.3 direntry.h1 direntry.h2 trydrent.c prot.h \
prot.c chkshsgr.c warn-shsgr tryshsgr.c ipme.h ipme.c trysalen.c \
-maildir.5 maildir.h maildir.c tcp-environ.5 constmap.h constmap.c
+maildir.5 maildir.h maildir.c tcp-environ.5 constmap.h constmap.c \
+update_tmprsadh
shar -m `cat FILES` > shar
chmod 400 shar
@@ -2113,6 +2116,19 @@ timeoutwrite.o: \
compile timeoutwrite.c timeoutwrite.h select.h error.h readwrite.h
./compile timeoutwrite.c
+qmail-smtpd: tls.o ssl_timeoutio.o ndelay.a
+qmail-remote: tls.o ssl_timeoutio.o
+qmail-smtpd.o: tls.h ssl_timeoutio.h
+qmail-remote.o: tls.h ssl_timeoutio.h
+
+tls.o: \
+compile tls.c exit.h error.h
+ ./compile tls.c
+
+ssl_timeoutio.o: \
+compile ssl_timeoutio.c ssl_timeoutio.h select.h error.h ndelay.h
+ ./compile ssl_timeoutio.c
+
token822.o: \
compile token822.c stralloc.h gen_alloc.h alloc.h str.h token822.h \
gen_alloc.h gen_allocdefs.h
@@ -2144,3 +2160,26 @@ compile wait_nohang.c haswaitp.h
wait_pid.o: \
compile wait_pid.c error.h haswaitp.h
./compile wait_pid.c
+
+cert cert-req: \
+Makefile-cert
+ @$(MAKE) -sf $< $@
+
+Makefile-cert: \
+conf-qmail conf-users conf-groups Makefile-cert.mk
+ @cat Makefile-cert.mk \
+ | sed s}QMAIL}"`head -1 conf-qmail`"}g \
+ > $@
+
+update_tmprsadh: \
+conf-qmail conf-users conf-groups update_tmprsadh.sh
+ @cat update_tmprsadh.sh\
+ | sed s}UGQMAILD}"`head -2 conf-users|tail -1`:`head -1 conf-groups`"}g \
+ | sed s}QMAIL}"`head -1 conf-qmail`"}g \
+ > $@
+ chmod 755 update_tmprsadh
+
+tmprsadh: \
+update_tmprsadh
+ echo "Creating new temporary RSA and DH parameters"
+ ./update_tmprsadh
diff --git a/TARGETS b/TARGETS
index 75ec741..8925746 100644
--- a/TARGETS
+++ b/TARGETS
@@ -168,6 +168,8 @@ control.o
constmap.o
timeoutread.o
timeoutwrite.o
+tls.o
+ssl_timeoutio.o
timeoutconn.o
tcpto.o
dns.o
@@ -321,6 +323,7 @@ binm2
binm2+df
binm3
binm3+df
+Makefile-cert
it
qmail-local.0
qmail-lspawn.0
@@ -386,3 +389,4 @@ forgeries.0
man
setup
check
+update_tmprsadh
diff --git a/conf-cc b/conf-cc
index e58fb9b..8feb117 100644
--- a/conf-cc
+++ b/conf-cc
@@ -1,3 +1,3 @@
-cc -O2
+cc -O2 -DTLS=20151215 -I/usr/local/ssl/include
This will be used to compile .c files.
diff --git a/dns.c b/dns.c
index c2f7d86..dc3bd84 100644
--- a/dns.c
+++ b/dns.c
@@ -284,12 +284,11 @@ stralloc *sa;
int pref;
{
int r;
- struct ip_mx ix;
+ struct ip_mx ix = {0};
if (!stralloc_copy(&glue,sa)) return DNS_MEM;
if (!stralloc_0(&glue)) return DNS_MEM;
if (glue.s[0]) {
- ix.pref = 0;
if (!glue.s[ip_scan(glue.s,&ix.ip)] || !glue.s[ip_scanbracket(glue.s,&ix.ip)])
{
if (!ipalloc_append(ia,&ix)) return DNS_MEM;
@@ -308,9 +307,16 @@ int pref;
ix.ip = ip;
ix.pref = pref;
if (r == DNS_SOFT) return DNS_SOFT;
- if (r == 1)
+ if (r == 1) {
+#ifdef IX_FQDN
+ ix.fqdn = glue.s;
+#endif
if (!ipalloc_append(ia,&ix)) return DNS_MEM;
}
+ }
+#ifdef IX_FQDN
+ glue.s = 0;
+#endif
return 0;
}
@@ -330,7 +336,7 @@ unsigned long random;
{
int r;
struct mx { stralloc sa; unsigned short p; } *mx;
- struct ip_mx ix;
+ struct ip_mx ix = {0};
int nummx;
int i;
int j;
@@ -342,7 +348,6 @@ unsigned long random;
if (!stralloc_copy(&glue,sa)) return DNS_MEM;
if (!stralloc_0(&glue)) return DNS_MEM;
if (glue.s[0]) {
- ix.pref = 0;
if (!glue.s[ip_scan(glue.s,&ix.ip)] || !glue.s[ip_scanbracket(glue.s,&ix.ip)])
{
if (!ipalloc_append(ia,&ix)) return DNS_MEM;
diff --git a/hier.c b/hier.c
index 28e568d..4a304ce 100644
--- a/hier.c
+++ b/hier.c
@@ -143,6 +143,9 @@ void hier()
c(auto_qmail,"bin","qail",auto_uido,auto_gidq,0755);
c(auto_qmail,"bin","elq",auto_uido,auto_gidq,0755);
c(auto_qmail,"bin","pinq",auto_uido,auto_gidq,0755);
+#ifdef TLS
+ c(auto_qmail,"bin","update_tmprsadh",auto_uido,auto_gidq,0755);
+#endif
c(auto_qmail,"man/man5","addresses.5",auto_uido,auto_gidq,0644);
c(auto_qmail,"man/cat5","addresses.0",auto_uido,auto_gidq,0644);
diff --git a/ipalloc.h b/ipalloc.h
index ad61475..bf9d060 100644
--- a/ipalloc.h
+++ b/ipalloc.h
@@ -3,7 +3,15 @@
#include "ip.h"
+#ifdef TLS
+# define IX_FQDN 1
+#endif
+
+#ifdef IX_FQDN
+struct ip_mx { struct ip_address ip; int pref; char *fqdn; } ;
+#else
struct ip_mx { struct ip_address ip; int pref; } ;
+#endif
#include "gen_alloc.h"
diff --git a/qmail-control.9 b/qmail-control.9
index 503ce93..945f15f 100644
--- a/qmail-control.9
+++ b/qmail-control.9
@@ -43,11 +43,14 @@ control default used by
.I badmailfrom \fR(none) \fRqmail-smtpd
.I bouncefrom \fRMAILER-DAEMON \fRqmail-send
.I bouncehost \fIme \fRqmail-send
+.I clientca.pem \fR(none) \fRqmail-smtpd
+.I clientcert.pem \fR(none) \fRqmail-remote
.I concurrencylocal \fR10 \fRqmail-send
.I concurrencyremote \fR20 \fRqmail-send
.I defaultdomain \fIme \fRqmail-inject
.I defaulthost \fIme \fRqmail-inject
.I databytes \fR0 \fRqmail-smtpd
+.I dh2048.pem \fR(none) \fRqmail-smtpd
.I doublebouncehost \fIme \fRqmail-send
.I doublebounceto \fRpostmaster \fRqmail-send
.I envnoathost \fIme \fRqmail-send
@@ -61,11 +64,17 @@ control default used by
.I qmqpservers \fR(none) \fRqmail-qmqpc
.I queuelifetime \fR604800 \fRqmail-send
.I rcpthosts \fR(none) \fRqmail-smtpd
+.I rsa2048.pem \fR(none) \fRqmail-smtpd
+.I servercert.pem \fR(none) \fRqmail-smtpd
.I smtpgreeting \fIme \fRqmail-smtpd
.I smtproutes \fR(none) \fRqmail-remote
.I timeoutconnect \fR60 \fRqmail-remote
.I timeoutremote \fR1200 \fRqmail-remote
.I timeoutsmtpd \fR1200 \fRqmail-smtpd
+.I tlsclients \fR(none) \fRqmail-smtpd
+.I tlsclientciphers \fR(none) \fRqmail-remote
+.I tlshosts/FQDN.pem \fR(none) \fRqmail-remote
+.I tlsserverciphers \fR(none) \fRqmail-smtpd
.I virtualdomains \fR(none) \fRqmail-send
.fi
.RE
diff --git a/qmail-remote.8 b/qmail-remote.8
index 08bae85..5fac0f2 100644
--- a/qmail-remote.8
+++ b/qmail-remote.8
@@ -114,6 +114,10 @@ arguments.
always exits zero.
.SH "CONTROL FILES"
.TP 5
+.I clientcert.pem
+SSL certificate that is used to authenticate with the remote server
+during a TLS session.
+.TP 5
.I helohost
Current host name,
for use solely in saying hello to the remote SMTP server.
@@ -123,6 +127,16 @@ if that is supplied;
otherwise
.B qmail-remote
refuses to run.
+
+.TP 5
+.I notlshosts/<FQDN>
+.B qmail-remote
+will not try TLS on servers for which this file exists
+.RB ( <FQDN>
+is the fully-qualified domain name of the server).
+.IR (tlshosts/<FQDN>.pem
+takes precedence over this file however).
+
.TP 5
.I smtproutes
Artificial SMTP routes.
@@ -156,6 +170,8 @@ may be empty;
this tells
.B qmail-remote
to look up MX records as usual.
+.I port
+value of 465 (deprecated smtps port) causes TLS session to be started.
.I smtproutes
may include wildcards:
@@ -195,6 +211,33 @@ Number of seconds
.B qmail-remote
will wait for each response from the remote SMTP server.
Default: 1200.
+
+.TP 5
+.I tlsclientciphers
+A set of OpenSSL client cipher strings. Multiple ciphers
+contained in a string should be separated by a colon.
+
+.TP 5
+.I tlshosts/<FQDN>.pem
+.B qmail-remote
+requires TLS authentication from servers for which this file exists
+.RB ( <FQDN>
+is the fully-qualified domain name of the server). One of the
+.I dNSName
+or the
+.I CommonName
+attributes have to match. The file contains the trusted CA certificates.
+
+.B WARNING:
+this option may cause mail to be delayed, bounced, doublebounced, or lost.
+
+.TP 5
+.I tlshosts/exhaustivelist
+if this file exists
+no TLS will be tried on hosts other than those for which a file
+.B tlshosts/<FQDN>.pem
+exists.
+
.SH "SEE ALSO"
addresses(5),
envelopes(5),
diff --git a/qmail-remote.c b/qmail-remote.c
index 7d65473..24ee375 100644
--- a/qmail-remote.c
+++ b/qmail-remote.c
@@ -48,6 +48,17 @@ saa reciplist = {0};
struct ip_address partner;
+#ifdef TLS
+# include <sys/stat.h>
+# include "tls.h"
+# include "ssl_timeoutio.h"
+# include <openssl/x509v3.h>
+# define EHLO 1
+
+int tls_init();
+const char *ssl_err_str = 0;
+#endif
+
void out(s) char *s; { if (substdio_puts(subfdoutsmall,s) == -1) _exit(0); }
void zero() { if (substdio_put(subfdoutsmall,"\0",1) == -1) _exit(0); }
void zerodie() { zero(); substdio_flush(subfdoutsmall); _exit(0); }
@@ -99,6 +110,9 @@ void dropped() {
outhost();
out(" but connection died. ");
if (flagcritical) out("Possible duplicate! ");
+#ifdef TLS
+ if (ssl_err_str) { out(ssl_err_str); out(" "); }
+#endif
out("(#4.4.2)\n");
zerodie();
}
@@ -110,6 +124,12 @@ int timeout = 1200;
int saferead(fd,buf,len) int fd; char *buf; int len;
{
int r;
+#ifdef TLS
+ if (ssl) {
+ r = ssl_timeoutread(timeout, smtpfd, smtpfd, ssl, buf, len);
+ if (r < 0) ssl_err_str = ssl_error_str();
+ } else
+#endif
r = timeoutread(timeout,smtpfd,buf,len);
if (r <= 0) dropped();
return r;
@@ -117,6 +137,12 @@ int saferead(fd,buf,len) int fd; char *buf; int len;
int safewrite(fd,buf,len) int fd; char *buf; int len;
{
int r;
+#ifdef TLS
+ if (ssl) {
+ r = ssl_timeoutwrite(timeout, smtpfd, smtpfd, ssl, buf, len);
+ if (r < 0) ssl_err_str = ssl_error_str();
+ } else
+#endif
r = timeoutwrite(timeout,smtpfd,buf,len);
if (r <= 0) dropped();
return r;
@@ -163,6 +189,65 @@ unsigned long smtpcode()
return code;
}
+#ifdef EHLO
+saa ehlokw = {0}; /* list of EHLO keywords and parameters */
+int maxehlokwlen = 0;
+
+unsigned long ehlo()
+{
+ stralloc *sa;
+ char *s, *e, *p;
+ unsigned long code;
+
+ if (ehlokw.len > maxehlokwlen) maxehlokwlen = ehlokw.len;
+ ehlokw.len = 0;
+
+# ifdef MXPS
+ if (type == 's') return 0;
+# endif
+
+ substdio_puts(&smtpto, "EHLO ");
+ substdio_put(&smtpto, helohost.s, helohost.len);
+ substdio_puts(&smtpto, "\r\n");
+ substdio_flush(&smtpto);
+
+ code = smtpcode();
+ if (code != 250) return code;
+
+ s = smtptext.s;
+ while (*s++ != '\n') ; /* skip the first line: contains the domain */
+
+ e = smtptext.s + smtptext.len - 6; /* 250-?\n */
+ while (s <= e)
+ {
+ int wasspace = 0;
+
+ if (!saa_readyplus(&ehlokw, 1)) temp_nomem();
+ sa = ehlokw.sa + ehlokw.len++;
+ if (ehlokw.len > maxehlokwlen) *sa = sauninit; else sa->len = 0;
+
+ /* smtptext is known to end in a '\n' */
+ for (p = (s += 4); ; ++p)
+ if (*p == '\n' || *p == ' ' || *p == '\t') {
+ if (!wasspace)
+ if (!stralloc_catb(sa, s, p - s) || !stralloc_0(sa)) temp_nomem();
+ if (*p == '\n') break;
+ wasspace = 1;
+ } else if (wasspace == 1) {
+ wasspace = 0;
+ s = p;
+ }
+ s = ++p;
+
+ /* keyword should consist of alpha-num and '-'
+ * broken AUTH might use '=' instead of space */
+ for (p = sa->s; *p; ++p) if (*p == '=') { *p = 0; break; }
+ }
+
+ return 250;
+}
+#endif
+
void outsmtptext()
{
int i;
@@ -179,6 +264,11 @@ void quit(prepend,append)
char *prepend;
char *append;
{
+#ifdef TLS
+ /* shouldn't talk to the client unless in an appropriate state */
+ int state = ssl ? ssl->state : SSL_ST_BEFORE;
+ if (state & SSL_ST_OK || (!smtps && state & SSL_ST_BEFORE))
+#endif
substdio_putsflush(&smtpto,"QUIT\r\n");
/* waiting for remote side is just too ridiculous */
out(prepend);
@@ -186,6 +276,30 @@ char *append;
out(append);
out(".\n");
outsmtptext();
+
+#if defined(TLS) && defined(DEBUG)
+ if (ssl) {
+ X509 *peercert;
+
+ out("STARTTLS proto="); out(SSL_get_version(ssl));
+ out("; cipher="); out(SSL_get_cipher(ssl));
+
+ /* we want certificate details */
+ if (peercert = SSL_get_peer_certificate(ssl)) {
+ char *str;
+
+ str = X509_NAME_oneline(X509_get_subject_name(peercert), NULL, 0);
+ out("; subject="); out(str); OPENSSL_free(str);
+
+ str = X509_NAME_oneline(X509_get_issuer_name(peercert), NULL, 0);
+ out("; issuer="); out(str); OPENSSL_free(str);
+
+ X509_free(peercert);
+ }
+ out(";\n");
+ }
+#endif
+
zerodie();
}
@@ -214,6 +328,199 @@ void blast()
substdio_flush(&smtpto);
}
+#ifdef TLS
+char *partner_fqdn = 0;
+
+# define TLS_QUIT quit(ssl ? "; connected to " : "; connecting to ", "")
+void tls_quit(const char *s1, const char *s2)
+{
+ out(s1); if (s2) { out(": "); out(s2); } TLS_QUIT;
+}
+# define tls_quit_error(s) tls_quit(s, ssl_error())
+
+int match_partner(const char *s, int len)
+{
+ if (!case_diffb(partner_fqdn, len, s) && !partner_fqdn[len]) return 1;
+ /* we also match if the name is *.domainname */
+ if (*s == '*') {
+ const char *domain = partner_fqdn + str_chr(partner_fqdn, '.');
+ if (!case_diffb(domain, --len, ++s) && !domain[len]) return 1;
+ }
+ return 0;
+}
+
+/* don't want to fail handshake if certificate can't be verified */
+int verify_cb(int preverify_ok, X509_STORE_CTX *ctx) { return 1; }
+
+int tls_init()
+{
+ int i;
+ SSL *myssl;
+ SSL_CTX *ctx;
+ stralloc saciphers = {0};
+ const char *ciphers, *servercert = 0;
+
+ if (partner_fqdn) {
+ struct stat st;
+ stralloc tmp = {0};
+ if (!stralloc_copys(&tmp, "control/tlshosts/")
+ || !stralloc_catb(&tmp, partner_fqdn, str_len(partner_fqdn))
+ || !stralloc_catb(&tmp, ".pem", 5)) temp_nomem();
+ if (stat(tmp.s, &st) == 0)
+ servercert = tmp.s;
+ else {
+ if (!stralloc_copys(&tmp, "control/notlshosts/")
+ || !stralloc_catb(&tmp, partner_fqdn, str_len(partner_fqdn)+1))
+ temp_nomem();
+ if ((stat("control/tlshosts/exhaustivelist", &st) == 0) ||
+ (stat(tmp.s, &st) == 0)) {
+ alloc_free(tmp.s);
+ return 0;
+ }
+ alloc_free(tmp.s);
+ }
+ }
+
+ if (!smtps) {
+ stralloc *sa = ehlokw.sa;
+ unsigned int len = ehlokw.len;
+ /* look for STARTTLS among EHLO keywords */
+ for ( ; len && case_diffs(sa->s, "STARTTLS"); ++sa, --len) ;
+ if (!len) {
+ if (!servercert) return 0;
+ out("ZNo TLS achieved while "); out(servercert);
+ out(" exists"); smtptext.len = 0; TLS_QUIT;
+ }
+ }
+
+ SSL_library_init();
+ ctx = SSL_CTX_new(SSLv23_client_method());
+ if (!ctx) {
+ if (!smtps && !servercert) return 0;
+ smtptext.len = 0;
+ tls_quit_error("ZTLS error initializing ctx");
+ }
+
+ /* POODLE vulnerability */
+ SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
+
+ if (servercert) {
+ if (!SSL_CTX_load_verify_locations(ctx, servercert, NULL)) {
+ SSL_CTX_free(ctx);
+ smtptext.len = 0;
+ out("ZTLS unable to load "); tls_quit_error(servercert);
+ }
+ /* set the callback here; SSL_set_verify didn't work before 0.9.6c */
+ SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_cb);
+ }
+
+ /* let the other side complain if it needs a cert and we don't have one */
+# define CLIENTCERT "control/clientcert.pem"
+ if (SSL_CTX_use_certificate_chain_file(ctx, CLIENTCERT))
+ SSL_CTX_use_RSAPrivateKey_file(ctx, CLIENTCERT, SSL_FILETYPE_PEM);
+# undef CLIENTCERT
+
+ myssl = SSL_new(ctx);
+ SSL_CTX_free(ctx);
+ if (!myssl) {
+ if (!smtps && !servercert) return 0;
+ smtptext.len = 0;
+ tls_quit_error("ZTLS error initializing ssl");
+ }
+
+ if (!smtps) substdio_putsflush(&smtpto, "STARTTLS\r\n");
+
+ /* while the server is preparing a response, do something else */
+ if (control_readfile(&saciphers, "control/tlsclientciphers", 0) == -1)
+ { SSL_free(myssl); temp_control(); }
+ if (saciphers.len) {
+ for (i = 0; i < saciphers.len - 1; ++i)
+ if (!saciphers.s[i]) saciphers.s[i] = ':';
+ ciphers = saciphers.s;
+ }
+ else ciphers = "DEFAULT";
+ SSL_set_cipher_list(myssl, ciphers);
+ alloc_free(saciphers.s);
+
+ SSL_set_fd(myssl, smtpfd);
+
+ /* read the response to STARTTLS */
+ if (!smtps) {
+ if (smtpcode() != 220) {
+ SSL_free(myssl);
+ if (!servercert) return 0;
+ out("ZSTARTTLS rejected while ");
+ out(servercert); out(" exists"); TLS_QUIT;
+ }
+ smtptext.len = 0;
+ }
+
+ ssl = myssl;
+ if (ssl_timeoutconn(timeout, smtpfd, smtpfd, ssl) <= 0)
+ tls_quit("ZTLS connect failed", ssl_error_str());
+
+ if (servercert) {
+ X509 *peercert;
+ STACK_OF(GENERAL_NAME) *gens;
+ int found_gen_dns = 0;
+
+ int r = SSL_get_verify_result(ssl);
+ if (r != X509_V_OK) {
+ out("ZTLS unable to verify server with ");
+ tls_quit(servercert, X509_verify_cert_error_string(r));
+ }
+ alloc_free(servercert);
+
+ peercert = SSL_get_peer_certificate(ssl);
+ if (!peercert) {
+ out("ZTLS unable to verify server ");
+ tls_quit(partner_fqdn, "no certificate provided");
+ }
+
+ /* RFC 2595 section 2.4: find a matching name
+ * first find a match among alternative names */
+ gens = X509_get_ext_d2i(peercert, NID_subject_alt_name, 0, 0);
+ if (gens) {
+ for (i = 0, r = sk_GENERAL_NAME_num(gens); i < r; ++i)
+ {
+ const GENERAL_NAME *gn = sk_GENERAL_NAME_value(gens, i);
+ if (gn->type == GEN_DNS){
+ found_gen_dns = 1;
+ if (match_partner(gn->d.ia5->data, gn->d.ia5->length)) break;
+ }
+ }
+ sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free);
+ }
+
+ /* no SubjectAltName of type DNS found, look up commonName */
+ if (!found_gen_dns) {
+ stralloc peer = {0};
+ X509_NAME *subj = X509_get_subject_name(peercert);
+ i = X509_NAME_get_index_by_NID(subj, NID_commonName, -1);
+ if (i >= 0) {
+ const ASN1_STRING *s = X509_NAME_get_entry(subj, i)->value;
+ if (s) { peer.len = s->length; peer.s = s->data; }
+ }
+ if (peer.len <= 0) {
+ out("ZTLS unable to verify server ");
+ tls_quit(partner_fqdn, "certificate contains no valid commonName");
+ }
+ if (!match_partner(peer.s, peer.len)) {
+ out("ZTLS unable to verify server "); out(partner_fqdn);
+ out(": received certificate for "); outsafe(&peer); TLS_QUIT;
+ }
+ }
+
+ X509_free(peercert);
+ }
+
+ if (smtps) if (smtpcode() != 220)
+ quit("ZTLS Connected to "," but greeting failed");
+
+ return 1;
+}
+#endif
+
stralloc recip = {0};
void smtp()
@@ -221,15 +528,54 @@ void smtp()
unsigned long code;
int flagbother;
int i;
+
+#ifndef PORT_SMTP
+ /* the qmtpc patch uses smtp_port and undefines PORT_SMTP */
+# define port smtp_port
+#endif
+
+#ifdef TLS
+# ifdef MXPS
+ if (type == 'S') smtps = 1;
+ else if (type != 's')
+# endif
+ if (port == 465) smtps = 1;
+ if (!smtps)
+#endif
if (smtpcode() != 220) quit("ZConnected to "," but greeting failed");
+#ifdef EHLO
+# ifdef TLS
+ if (!smtps)
+# endif
+ code = ehlo();
+
+# ifdef TLS
+ if (tls_init())
+ /* RFC2487 says we should issue EHLO (even if we might not need
+ * extensions); at the same time, it does not prohibit a server
+ * to reject the EHLO and make us fallback to HELO */
+ code = ehlo();
+# endif
+
+ if (code == 250) {
+ /* add EHLO response checks here */
+
+ /* and if EHLO failed, use HELO */
+ } else {
+#endif
+
substdio_puts(&smtpto,"HELO ");
substdio_put(&smtpto,helohost.s,helohost.len);
substdio_puts(&smtpto,"\r\n");
substdio_flush(&smtpto);
if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected");
+#ifdef EHLO
+ }
+#endif
+
substdio_puts(&smtpto,"MAIL FROM:<");
substdio_put(&smtpto,sender.s,sender.len);
substdio_puts(&smtpto,">\r\n");
@@ -417,6 +763,9 @@ char **argv;
if (timeoutconn(smtpfd,&ip.ix[i].ip,(unsigned int) port,timeoutconnect) == 0) {
tcpto_err(&ip.ix[i].ip,0);
partner = ip.ix[i].ip;
+#ifdef TLS
+ partner_fqdn = ip.ix[i].fqdn;
+#endif
smtp(); /* does not return */
}
tcpto_err(&ip.ix[i].ip,errno == error_timeout);
diff --git a/qmail-smtpd.8 b/qmail-smtpd.8
index 3e6cce2..4e83fe1 100644
--- a/qmail-smtpd.8
+++ b/qmail-smtpd.8
@@ -19,6 +19,15 @@ must be supplied several environment variables;
see
.BR tcp-environ(5) .
+If the environment variable
+.B SMTPS
+is non-empty,
+.B qmail-smtpd
+starts a TLS session (to support the deprecated SMTPS protocol,
+normally on port 465). Otherwise,
+.B qmail-smtpd
+offers the STARTTLS extension to ESMTP.
+
.B qmail-smtpd
is responsible for counting hops.
It rejects any message with 100 or more
@@ -76,6 +85,19 @@ may be of the form
.BR @\fIhost ,
meaning every address at
.IR host .
+
+.TP 5
+.I clientca.pem
+A list of Certifying Authority (CA) certificates that are used to verify
+the client-presented certificates during a TLS-encrypted session.
+
+.TP 5
+.I clientcrl.pem
+A list of Certificate Revocation Lists (CRLs). If present it
+should contain the CRLs of the CAs in
+.I clientca.pem
+and client certs will be checked for revocation.
+
.TP 5
.I databytes
Maximum number of bytes allowed in a message,
@@ -103,6 +125,18 @@ If the environment variable
.B DATABYTES
is set, it overrides
.IR databytes .
+
+.TP 5
+.I dh2048.pem
+If these 2048 bit DH parameters are provided,
+.B qmail-smtpd
+will use them for TLS sessions instead of generating one on-the-fly
+(which is very timeconsuming).
+.TP 5
+.I dh2048.pem
+2048 bit counterpart for
+.B dh2048.pem.
+
.TP 5
.I localiphost
Replacement host name for local IP addresses.
@@ -178,6 +212,19 @@ may include wildcards:
Envelope recipient addresses without @ signs are
always allowed through.
+
+.TP 5
+.I rsa512.pem
+If this 512 bit RSA key is provided,
+.B qmail-smtpd
+will use it for TLS sessions instead of generating one on-the-fly.
+
+.TP 5
+.I servercert.pem
+SSL certificate to be presented to clients in TLS-encrypted sessions.
+Should contain both the certificate and the private key. Certifying Authority
+(CA) and intermediate certificates can be added at the end of the file.
+
.TP 5
.I smtpgreeting
SMTP greeting message.
@@ -196,6 +243,24 @@ Number of seconds
.B qmail-smtpd
will wait for each new buffer of data from the remote SMTP client.
Default: 1200.
+
+.TP 5
+.I tlsclients
+A list of email addresses. When relay rules would reject an incoming message,
+.B qmail-smtpd
+can allow it if the client presents a certificate that can be verified against
+the CA list in
+.I clientca.pem
+and the certificate email address is in
+.IR tlsclients .
+
+.TP 5
+.I tlsserverciphers
+A set of OpenSSL cipher strings. Multiple ciphers contained in a
+string should be separated by a colon. If the environment variable
+.B TLSCIPHERS
+is set to such a string, it takes precedence.
+
.SH "SEE ALSO"
tcp-env(1),
tcp-environ(5),
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 }