Frederik Vermeulen 19991224 http://www.esat.kuleuven.ac.be/~vermeule/qmail/tls.patch This patch implements RFC2487 (starttls) in qmail (qmail-smtpd as server, qmail-remote as client). This means you can get SSL or TLS encrypted SMTP between the MTAs and between MTA and an MUA like Netscape4.5. The code is considered experimental. Usage: - install OpenSSL-0.9.4 http://www.openssl.org/ - apply patch to qmail-1.03 http://www.qmail.org/ Makefile and conf-cc were patched for appropriate linking. Apart from that, the patches to qmail-remote.c and qmail-smtpd.c can be applied separately. - provide a certificate in /var/qmail/control/cert.pem. "make cert" makes a self-signed certificate. "make cert-req" makes a certificate request. - replace qmail-smtpd and/or qmail-remote binary - verify operation (header information should show something like "Received [..] with DES-CBC3-SHA encrypted SMTP;") If you don't have a server to test with, you can test by sending mail to ping@linux.student.kuleuven.ac.be, which will bounce your mail. Optional: - when DEBUG is defined, some extra SSL info will be logged - when a 512 RSA key is provided in /var/qmail/control/rsa512.pem, this key will be used instead of on-the-fly generation by qmail-smtpd. Daily replacement can be done by crontab: 01 01 * * * /usr/local/ssl/bin/openssl genrsa \ -out /var/qmail/control/rsa512.new 512 > /dev/null 2>&1;\ chmod 600 /var/qmail/control/rsa512.new; chown qmaild.qmail \ /var/qmail/control/rsa512.new; /bin/mv -f \ /var/qmail/control/rsa512.new /var/qmail/control/rsa512.pem - qmail-remote requires authentication from servers for which /var/qmail/control/tlshosts/host.dom.ain.pem exists. The .pem file contains the validating CA certificates (or self-signed server certificate with openssl-0.9.5). CommonName has to match. WARNING: this option may cause mail to be delayed, bounced, doublebounced, and lost. Copyright: Same terms as qmail Links with OpenSSL Inspiration from examples in SSLeay (E. Young and T. Hudson ), stunnel (M. Trojnara ), and Postfix/TLS (L. Jaenicke ). Debug code from Jean-Philippe Donnio Openssl usage consulting from Bodo M"oller Interoperability: - Netscape 4.5 and higher - Microsoft Outlook 5 - Postfix/TLS http://www.aet.TU-Cottbus.DE/personen/jaenicke/pfixtls/ - InterChange 3.51.05 if uncommenting SSL_OP_NO_TLSv1 - Microsoft Exchange Internet Mail Server 5.5.2448.0 Patches: mailto: diff -ur qmail-1.03/Makefile qmail-1.03-tls/Makefile --- qmail-1.03/Makefile Mon Jun 15 12:53:16 1998 +++ qmail-1.03-tls/Makefile Fri Dec 24 15:37:03 1999 @@ -1446,7 +1446,8 @@ timeoutwrite.o timeoutconn.o tcpto.o now.o dns.o ip.o \ 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` + str.a fs.a auto_qmail.o `cat dns.lib` `cat socket.lib` \ + -L/usr/local/ssl/lib -lssl -lcrypto qmail-remote.0: \ qmail-remote.8 @@ -1542,7 +1543,7 @@ 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 `cat \ - socket.lib` + socket.lib` -L/usr/local/ssl/lib -lssl -lcrypto qmail-smtpd.0: \ qmail-smtpd.8 @@ -2139,3 +2140,20 @@ wait_pid.o: \ compile wait_pid.c error.h haswaitp.h ./compile wait_pid.c + +cert: + /usr/local/ssl/bin/openssl req -new -x509 -nodes \ + -out /var/qmail/control/cert.pem -days 366 \ + -keyout /var/qmail/control/cert.pem + chmod 640 /var/qmail/control/cert.pem + chown qmaild.qmail /var/qmail/control/cert.pem + +cert-req: + /usr/local/ssl/bin/openssl req -new -nodes \ + -out req.pem \ + -keyout /var/qmail/control/cert.pem + chmod 640 /var/qmail/control/cert.pem + chown qmaild.qmail /var/qmail/control/cert.pem + @echo + @echo "Send req.pem to your CA to obtain signed_req.pem, and do:" + @echo "cat signed_req.pem >> /var/qmail/control/cert.pem" diff -ur qmail-1.03/conf-cc qmail-1.03-tls/conf-cc --- qmail-1.03/conf-cc Mon Jun 15 12:53:16 1998 +++ qmail-1.03-tls/conf-cc Fri Dec 24 16:04:51 1999 @@ -1,3 +1,3 @@ -cc -O2 +cc -O2 -DTLS -I/usr/local/ssl/include This will be used to compile .c files. diff -ur qmail-1.03/dns.c qmail-1.03-tls/dns.c --- qmail-1.03/dns.c Mon Jun 15 12:53:16 1998 +++ qmail-1.03-tls/dns.c Fri Dec 24 15:39:10 1999 @@ -270,6 +270,14 @@ { int r; struct ip_mx ix; +#ifdef TLS + stralloc fqdn = {0}; + + if (!stralloc_copy(&fqdn,sa)) return DNS_MEM; + if (!stralloc_0(&fqdn)) return DNS_MEM; + ix.fqdn = fqdn.s; + alloc_free(fqdn); +#endif if (!stralloc_copy(&glue,sa)) return DNS_MEM; if (!stralloc_0(&glue)) return DNS_MEM; diff -ur qmail-1.03/ipalloc.h qmail-1.03-tls/ipalloc.h --- qmail-1.03/ipalloc.h Mon Jun 15 12:53:16 1998 +++ qmail-1.03-tls/ipalloc.h Fri Dec 24 15:39:42 1999 @@ -3,7 +3,12 @@ #include "ip.h" +#ifdef TLS +#include "stralloc.h" +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 -ur qmail-1.03/qmail-remote.c qmail-1.03-tls/qmail-remote.c --- qmail-1.03/qmail-remote.c Mon Jun 15 12:53:16 1998 +++ qmail-1.03-tls/qmail-remote.c Fri Dec 24 16:06:58 1999 @@ -26,8 +26,16 @@ #include "tcpto.h" #include "readwrite.h" #include "timeoutconn.h" +#ifndef TLS #include "timeoutread.h" #include "timeoutwrite.h" +#endif + +#ifdef TLS +#include +#include +SSL *ssl = NULL; +#endif #define HUGESMTPTEXT 5000 @@ -107,17 +115,57 @@ int smtpfd; int timeout = 1200; +#ifdef TLS +int flagtimedout = 0; +void sigalrm() +{ + flagtimedout = 1; +} +int ssl_timeoutread(timeout,fd,buf,n) int timeout; int fd; char *buf; int n; +{ + int r; int saveerrno; + if (flagtimedout) { errno = error_timeout; return -1; } + alarm(timeout); + if (ssl) r = SSL_read(ssl,buf,n); else r = read(fd,buf,n); + saveerrno = errno; + alarm(0); + if (flagtimedout) { errno = error_timeout; return -1; } + errno = saveerrno; + return r; +} +int ssl_timeoutwrite(timeout,fd,buf,n) int timeout; int fd; char *buf; int n; +{ + int r; int saveerrno; + if (flagtimedout) { errno = error_timeout; return -1; } + alarm(timeout); + if (ssl) r = SSL_write(ssl,buf,n); else r = write(fd,buf,n); + saveerrno = errno; + alarm(0); + if (flagtimedout) { errno = error_timeout; return -1; } + errno = saveerrno; + return r; +} +#endif + int saferead(fd,buf,len) int fd; char *buf; int len; { int r; +#ifdef TLS + r = ssl_timeoutread(timeout,smtpfd,buf,len); +#else r = timeoutread(timeout,smtpfd,buf,len); +#endif if (r <= 0) dropped(); return r; } int safewrite(fd,buf,len) int fd; char *buf; int len; { int r; +#ifdef TLS + r = ssl_timeoutwrite(timeout,smtpfd,buf,len); +#else r = timeoutwrite(timeout,smtpfd,buf,len); +#endif if (r <= 0) dropped(); return r; } @@ -179,6 +227,35 @@ char *prepend; char *append; { +/* TAG */ +#if defined(TLS) && defined(DEBUG) +#define ONELINE_NAME(X) X509_NAME_oneline(X,NULL,0) + + if(ssl){ + X509 *peer; + + out("STARTTLS proto="); out(SSL_get_version(ssl)); + out("; cipher="); out(SSL_CIPHER_get_name(SSL_get_current_cipher(ssl))); + + /* we want certificate details */ + peer=SSL_get_peer_certificate(ssl); + if (peer != NULL) { + char *str; + + str=ONELINE_NAME(X509_get_subject_name(peer)); + out("; subject="); out(str); + Free(str); + str=ONELINE_NAME(X509_get_issuer_name(peer)); + out("; issuer="); out(str); + Free(str); + X509_free(peer); + Free(str); + X509_free(peer); + } + out(";\n"); + } +#endif + substdio_putsflush(&smtpto,"QUIT\r\n"); /* waiting for remote side is just too ridiculous */ out(prepend); @@ -216,20 +293,154 @@ stralloc recip = {0}; +#ifdef TLS +void smtp(fqdn) +char *fqdn; +#else void smtp() +#endif { unsigned long code; int flagbother; int i; - +#ifdef TLS + int needtlsauth = 0; + SSL_CTX *ctx; + int saveerrno, r; +#ifdef DEBUG + char buf[1024]; +#endif + + stralloc servercert = {0}; + struct stat st; + + if(!stralloc_copys(&servercert, "control/tlshosts/")) temp_nomem(); + if(!stralloc_catb(&servercert, fqdn, str_len(fqdn))) temp_nomem(); + if(!stralloc_catb(&servercert, ".pem", 4)) temp_nomem(); + if(!stralloc_0(&servercert)) temp_nomem(); + if (stat(servercert.s,&st) == 0) needtlsauth = 1; +#endif + if (smtpcode() != 220) quit("ZConnected to "," but greeting failed"); +#ifdef TLS + substdio_puts(&smtpto,"EHLO "); +#else substdio_puts(&smtpto,"HELO "); +#endif substdio_put(&smtpto,helohost.s,helohost.len); substdio_puts(&smtpto,"\r\n"); substdio_flush(&smtpto); +#ifdef TLS + if (smtpcode() != 250){ + 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"); + } +#else if (smtpcode() != 250) quit("ZConnected to "," but my name was rejected"); - +#endif + +#ifdef TLS + i = 0; + while((i += str_chr(smtptext.s+i,'\n') + 1) && (i+12 < smtptext.len) && + str_diffn(smtptext.s+i+4,"STARTTLS\n",9)); + if (i+12 < smtptext.len) + { + substdio_puts(&smtpto,"STARTTLS\r\n"); + substdio_flush(&smtpto); + if (smtpcode() == 220) + { +#ifdef DEBUG + SSL_load_error_strings(); +#endif + SSLeay_add_ssl_algorithms(); + if(!(ctx=SSL_CTX_new(SSLv23_client_method()))) +#ifdef DEBUG + {out("ZTLS not available: error initializing ctx"); + out(": "); + out(ERR_error_string(ERR_get_error(), buf)); + out("\n"); +#else + {out("ZTLS not available: error initializing ctx\n"); +#endif + zerodie();} + + SSL_CTX_use_RSAPrivateKey_file(ctx, "control/cert.pem", SSL_FILETYPE_PEM); + SSL_CTX_use_certificate_file(ctx, "control/cert.pem", SSL_FILETYPE_PEM); + /*SSL_CTX_set_options(ctx, SSL_OP_NO_TLSv1);*/ + + if (needtlsauth){ + if (!SSL_CTX_load_verify_locations(ctx, servercert.s, NULL)) + {out("ZTLS unable to load "); out(servercert.s); out("\n"); + zerodie();} + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); + } + + if(!(ssl=SSL_new(ctx))) +#ifdef DEBUG + {out("ZTLS not available: error initializing ssl"); + out(": "); + out(ERR_error_string(ERR_get_error(), buf)); + out("\n"); +#else + {out("ZTLS not available: error initializing ssl\n"); +#endif + zerodie();} + SSL_set_fd(ssl,smtpfd); + + alarm(timeout); + r = SSL_connect(ssl); saveerrno = errno; + alarm(0); + if (flagtimedout) + {out("ZTLS not available: connect timed out\n"); + zerodie();} + errno = saveerrno; + if (r<=0) + {if (needtlsauth && (r=SSL_get_verify_result(ssl)) != X509_V_OK) + {out("ZTLS unable to verify server with "); + out(servercert.s); out(": "); + out(X509_verify_cert_error_string(r)); out("\n");} + else +#ifdef DEBUG + {out("ZTLS not available: connect failed"); + out(": "); + out(ERR_error_string(ERR_get_error(), buf)); + out("\n");} +#else + out("ZTLS not available: connect failed\n"); +#endif + zerodie();} + if (needtlsauth) + {char commonName[256]; + X509_NAME_get_text_by_NID(X509_get_subject_name( + SSL_get_peer_certificate(ssl)), + NID_commonName, commonName, 256); + if (strcasecmp(fqdn,commonName)){ + out("ZTLS connection to "); out(fqdn); + out(" wanted, certificate for "); out(commonName); + out(" received\n"); + zerodie();} + } + + substdio_puts(&smtpto,"EHLO "); + substdio_put(&smtpto,helohost.s,helohost.len); + substdio_puts(&smtpto,"\r\n"); + substdio_flush(&smtpto); + + if (smtpcode() != 250) + { + quit("ZTLS connected to "," but my name was rejected"); + } + } + } + if ((!ssl) && needtlsauth) + {out("ZNo TLS achieved while "); out(servercert.s); out(" exists.\n"); + quit();} +#endif + substdio_puts(&smtpto,"MAIL FROM:<"); substdio_put(&smtpto,sender.s,sender.len); substdio_puts(&smtpto,">\r\n"); @@ -338,7 +549,10 @@ int flagallaliases; int flagalias; char *relayhost; - + +#ifdef TLS + sig_alarmcatch(sigalrm); +#endif sig_pipeignore(); if (argc < 4) perm_usage(); if (chdir(auto_qmail) == -1) temp_chdir(); @@ -417,7 +631,11 @@ 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 + smtp(ip.ix[i].fqdn); /* does not return */ +#else smtp(); /* does not return */ +#endif } tcpto_err(&ip.ix[i].ip,errno == error_timeout); close(smtpfd); diff -ur qmail-1.03/qmail-smtpd.c qmail-1.03-tls/qmail-smtpd.c --- qmail-1.03/qmail-smtpd.c Mon Jun 15 12:53:16 1998 +++ qmail-1.03-tls/qmail-smtpd.c Fri Dec 24 16:02:43 1999 @@ -20,18 +20,60 @@ #include "now.h" #include "exit.h" #include "rcpthosts.h" +#ifndef TLS #include "timeoutread.h" #include "timeoutwrite.h" +#endif #include "commands.h" +#ifdef TLS +#include +SSL *ssl = NULL; +#endif #define MAXHOPS 100 unsigned int databytes = 0; int timeout = 1200; +#ifdef TLS +int flagtimedout = 0; +void sigalrm() +{ + flagtimedout = 1; +} +int ssl_timeoutread(timeout,fd,buf,n) int timeout; int fd; char *buf; int n; +{ + int r; int saveerrno; + if (flagtimedout) { errno = error_timeout; return -1; } + alarm(timeout); + if (ssl) r = SSL_read(ssl,buf,n); else r = read(fd,buf,n); + saveerrno = errno; + alarm(0); + if (flagtimedout) { errno = error_timeout; return -1; } + errno = saveerrno; + return r; +} +int ssl_timeoutwrite(timeout,fd,buf,n) int timeout; int fd; char *buf; int n; +{ + int r; int saveerrno; + if (flagtimedout) { errno = error_timeout; return -1; } + alarm(timeout); + if (ssl) r = SSL_write(ssl,buf,n); else r = write(fd,buf,n); + saveerrno = errno; + alarm(0); + if (flagtimedout) { errno = error_timeout; return -1; } + errno = saveerrno; + return r; +} +#endif + int safewrite(fd,buf,len) int fd; char *buf; int len; { int r; +#ifdef TLS + r = ssl_timeoutwrite(timeout,fd,buf,len); +#else r = timeoutwrite(timeout,fd,buf,len); +#endif if (r <= 0) _exit(1); return r; } @@ -229,7 +271,13 @@ } void smtp_ehlo(arg) char *arg; { - smtp_greet("250-"); out("\r\n250-PIPELINING\r\n250 8BITMIME\r\n"); + smtp_greet("250-"); +#ifdef TLS + if (ssl) out("\r\n250-PIPELINING\r\n250 8BITMIME\r\n"); + else out("\r\n250-PIPELINING\r\n250-STARTTLS\r\n250 8BITMIME\r\n"); +#else + out("\r\n250-PIPELINING\r\n250 8BITMIME\r\n"); +#endif seenmail = 0; dohelo(arg); } void smtp_rset() @@ -269,7 +317,11 @@ { int r; flush(); +#ifdef TLS + r = ssl_timeoutread(timeout,fd,buf,len); +#else r = timeoutread(timeout,fd,buf,len); +#endif if (r == -1) if (errno == error_timeout) die_alarm(); if (r <= 0) die_read(); return r; @@ -369,6 +421,9 @@ int hops; unsigned long qp; char *qqx; +#ifdef TLS + stralloc protocolinfo = {0}; +#endif if (!seenmail) { err_wantmail(); return; } if (!rcptto.len) { err_wantrcpt(); return; } @@ -377,8 +432,17 @@ if (qmail_open(&qqt) == -1) { err_qqt(); return; } qp = qmail_qp(&qqt); out("354 go ahead\r\n"); - +#ifdef TLS + if(ssl){ + if (!stralloc_copys(&protocolinfo, SSL_CIPHER_get_name(SSL_get_current_cipher(ssl)))) die_nomem(); + if (!stralloc_ready(&protocolinfo,protocolinfo.len + 16)) die_nomem(); + byte_copy(protocolinfo.s+protocolinfo.len,16," encrypted SMTP"); + protocolinfo.len += 16; + } else if (!stralloc_copyb(&protocolinfo,"SMTP",5)) die_nomem(); + received(&qqt,protocolinfo.s,local,remoteip,remotehost,remoteinfo,case_diffs(remotehost,helohost.s) ? helohost.s : 0); +#else received(&qqt,"SMTP",local,remoteip,remotehost,remoteinfo,fakehelo); +#endif blast(&hops); hops = (hops >= MAXHOPS); if (hops) qmail_fail(&qqt); @@ -394,6 +458,53 @@ out("\r\n"); } +#ifdef TLS +RSA *tmp_rsa_cb(ssl,export,keylength) SSL *ssl; int export; int keylength; +{ + RSA* rsa; + BIO* in; + + if (!export || keylength == 512) + if (in=BIO_new(BIO_s_file_internal())) + if (BIO_read_filename(in,"control/rsa512.pem") > 0) + if (rsa=PEM_read_bio_RSAPrivateKey(in,NULL,NULL,NULL)) + return rsa; + return (RSA_generate_key(export?keylength:512,RSA_F4,NULL,NULL)); +} + +void smtp_tls(arg) char *arg; +{ + SSL_CTX *ctx; + + if (*arg) + {out("501 Syntax error (no parameters allowed) (#5.5.4)\r\n"); + return;} + + SSLeay_add_ssl_algorithms(); + if(!(ctx=SSL_CTX_new(SSLv23_server_method()))) + {out("454 TLS not available: unable to initialize ctx (#4.3.0)\r\n"); + return;} + if(!SSL_CTX_use_RSAPrivateKey_file(ctx, "control/cert.pem", SSL_FILETYPE_PEM)) + {out("454 TLS not available: missing RSA private key (#4.3.0)\r\n"); + return;} + if(!SSL_CTX_use_certificate_file(ctx, "control/cert.pem", SSL_FILETYPE_PEM)) + {out("454 TLS not available: missing certificate (#4.3.0)\r\n"); + return;} + SSL_CTX_set_tmp_rsa_callback(ctx, tmp_rsa_cb); + + out("220 ready for tls\r\n"); flush(); + + if(!(ssl=SSL_new(ctx))) die_read(); + SSL_set_fd(ssl,0); + if(SSL_accept(ssl)<=0) die_read(); + substdio_fdbuf(&ssout,SSL_write,ssl,ssoutbuf,sizeof(ssoutbuf)); + + remotehost = env_get("TCPREMOTEHOST"); + if (!remotehost) remotehost = "unknown"; + dohelo(remotehost); +} +#endif + struct commands smtpcommands[] = { { "rcpt", smtp_rcpt, 0 } , { "mail", smtp_mail, 0 } @@ -403,6 +514,9 @@ , { "ehlo", smtp_ehlo, flush } , { "rset", smtp_rset, 0 } , { "help", smtp_help, flush } +#ifdef TLS +, { "starttls", smtp_tls, flush } +#endif , { "noop", err_noop, flush } , { "vrfy", err_vrfy, flush } , { 0, err_unimpl, flush } @@ -410,6 +524,7 @@ void main() { + sig_alarmcatch(sigalrm); sig_pipeignore(); if (chdir(auto_qmail) == -1) die_control(); setup();