From 7ec4ffbdbc562c4a2909d9bf4f3093072e0e3ac1 Mon Sep 17 00:00:00 2001 From: Manuel Mausz Date: Wed, 27 Jun 2018 01:06:16 +0200 Subject: OpenSSL 1.1 compatibility This adds compatibility for OpenSSL 1.1 Since renegotiation is removed from TLS 1.3 we also removed support for authentication via client certificates (control/tlsclients). In general this is still supported by TLS 1.3 however I'm just lazy and we don't need this feature anyway. This also adds optional support for OpenSSL configuration commands for qmail-smtpd and qmail-remote. Commands are loaded from control/opensslconf. For a list of supported commands see https://www.openssl.org/docs/man1.0.2/ssl/SSL_CONF_cmd.html#SUPPORTED-CONFIGURATION-FILE-COMMANDS --- README.starttls | 10 ---- qmail-control.9 | 1 - qmail-remote.c | 49 ++++++++++++++-- qmail-smtpd.8 | 10 ---- qmail-smtpd.c | 177 +++++++++++++++++++++++++------------------------------- ssl_timeoutio.c | 13 ----- ssl_timeoutio.h | 1 - tls.h | 1 + 8 files changed, 125 insertions(+), 137 deletions(-) diff --git a/README.starttls b/README.starttls index 07ee275..6168c6d 100644 --- a/README.starttls +++ b/README.starttls @@ -42,16 +42,6 @@ Optional: - when DEBUG is defined, some extra TLS info will be logged an exhaustive list of hosts TLS is tried on. If /var/qmail/control/notlshosts/host.dom.ain is present, no TLS is tried on this host. - - client authentication: - when relay rules would reject an incoming mail, - qmail-smtpd can allow the mail based on a presented cert. - Certs are verified against a CA list in - /var/qmail/control/clientca.pem (eg. http://www.modssl.org/ - source/cvs/exp/mod_ssl/pkg.mod_ssl/pkg.sslcfg/ca-bundle.crt) - and the cert email-address has to match a line in - /var/qmail/control/tlsclients. This email-address is logged - in the headers. CRLs can be provided through - /var/qmail/control/clientcrl.pem. - cipher selection: qmail-remote: openssl cipher string (`man ciphers`) read from diff --git a/qmail-control.9 b/qmail-control.9 index 9c6edd8..6898643 100644 --- a/qmail-control.9 +++ b/qmail-control.9 @@ -78,7 +78,6 @@ control default used by .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 diff --git a/qmail-remote.c b/qmail-remote.c index d2412aa..94bb69f 100644 --- a/qmail-remote.c +++ b/qmail-remote.c @@ -302,8 +302,8 @@ void smtp_quit() { #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)) + if ((!smtps && !ssl) || (ssl && SSL_is_init_finished(ssl)) + || (!smtps && ssl && SSL_in_before(ssl))) #endif substdio_putsflush(&smtpto,"QUIT\r\n"); /* waiting for remote side is just too ridiculous */ @@ -539,6 +539,41 @@ int tls_init() SSL_set_cipher_list(myssl, ciphers); alloc_free(saciphers.s); +#if OPENSSL_VERSION_NUMBER >= 0x10100005L + stralloc opensslconf = {0}; + if (control_readfile(&opensslconf, "control/opensslconf", 0) == -1) + { SSL_free(myssl); temp_control(); } + if (opensslconf.len) { + SSL_CONF_CTX *cctx = SSL_CONF_CTX_new(); + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_FILE); + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_CLIENT); + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_CERTIFICATE); + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_SHOW_ERRORS); + SSL_CONF_CTX_set_ssl(cctx, myssl); + + int i, j, next = 0; + char *cmd, * arg; + for (i = 0; i < opensslconf.len; i += next) { + cmd = opensslconf.s + i; + next = str_len(cmd) + 1; + + j = str_chr(cmd, ' '); + arg = cmd + j; + while (*arg == ' ') ++arg; + cmd[j] = 0; + + if (SSL_CONF_cmd(cctx, cmd, arg) <= 0) { + SSL_free(myssl); + out("Zopensslconf \""); out(cmd); out(" "); out(arg); + out("\" failed: "); out(ssl_error()); + TLS_QUIT; + } + } + + (void)SSL_CONF_CTX_finish(cctx); + } +#endif + /* set SNI hostname */ if (partner_fqdn) SSL_set_tlsext_host_name(myssl, partner_fqdn); @@ -614,8 +649,12 @@ int tls_init() 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; } + X509_NAME_ENTRY *entry = X509_NAME_get_entry(subj, i); + ASN1_STRING *s = X509_NAME_ENTRY_get_data(entry); +#if OPENSSL_VERSION_NUMBER < 0x10100005L +#define ASN1_STRING_get0_data ASN1_STRING_data +#endif + if (s) { peer.len = ASN1_STRING_length(s); peer.s = (unsigned char *)ASN1_STRING_get0_data(s); } } if (peer.len <= 0) { out("ZTLS unable to verify server "); @@ -668,7 +707,7 @@ int utf8received() if (r == 0) break; if (r == -1) temp_read(); - if (ch == '\n') { + if (ch == '\n' && receivedline.len) { if (!stralloc_append(&header,"\r")) temp_nomem(); /* received.c does not add '\r' */ if (!stralloc_append(&header,"\n")) temp_nomem(); if (case_startb(receivedline.s,5,"Date:")) return 0; /* header to quit asap */ diff --git a/qmail-smtpd.8 b/qmail-smtpd.8 index 05d1239..5920dd9 100644 --- a/qmail-smtpd.8 +++ b/qmail-smtpd.8 @@ -294,16 +294,6 @@ Number of seconds 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 diff --git a/qmail-smtpd.c b/qmail-smtpd.c index 18795bc..f40a4c5 100644 --- a/qmail-smtpd.c +++ b/qmail-smtpd.c @@ -71,7 +71,6 @@ char *relayclient; # define SERVERCERT "control/servercert.pem" void tls_init(); -int tls_verify(); void tls_nogateway(); int ssl_rfd = -1, ssl_wfd = -1; /* SSL_get_Xfd() are broken */ stralloc proto = {0}; @@ -508,9 +507,6 @@ 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; } @@ -1261,19 +1257,44 @@ void smtp_tls(char *arg) * Grab well-defined DH parameters from OpenSSL, see the get_rfc* * functions in for all available primes. */ -static DH *make_dh_params(BIGNUM *(*prime)(BIGNUM *), const char *gen) +#if OPENSSL_VERSION_NUMBER < 0x10100005L +static int DH_set0_pqg(DH *dh, BIGNUM *p, BIGNUM *q, BIGNUM *g) { - DH *dh = DH_new(); - if (!dh) - return NULL; - - dh->p = prime(NULL); - BN_dec2bn(&dh->g, gen); - if (!dh->p || !dh->g) { - DH_free(dh); - return NULL; - } - return dh; + /* q is optional */ + if (p == NULL || g == NULL) + return 0; + BN_free(dh->p); + BN_free(dh->q); + BN_free(dh->g); + dh->p = p; + dh->q = q; + dh->g = g; + + if (q != NULL) + dh->length = BN_num_bits(q); + + return 1; +} +#endif + +static DH *make_dh_params(BIGNUM *(*prime)(BIGNUM *)) +{ + BIGNUM *p, *g; + DH *dh = DH_new(); + if (!dh) + return NULL; + + p = prime(NULL); + g = BN_new(); + if (g != NULL) + BN_set_word(g, 2); + if (!p || !g || !DH_set0_pqg(dh, p, NULL, g)) { + DH_free(dh); + BN_free(p); + BN_free(g); + return NULL; + } + return dh; } /* Storage and initialization for DH parameters. */ @@ -1300,7 +1321,7 @@ static struct dhparam { static DH *tmp_dh_cb(SSL *ssl, int export, int keylen) { EVP_PKEY *pkey = SSL_get_privatekey(ssl); - int type = pkey ? EVP_PKEY_type(pkey->type) : EVP_PKEY_NONE; + int type = pkey ? EVP_PKEY_base_id(pkey) : EVP_PKEY_NONE; unsigned n; /* @@ -1321,7 +1342,7 @@ static DH *tmp_dh_cb(SSL *ssl, int export, int keylen) for (n = 0; n < sizeof(dhparams)/sizeof(dhparams[0]); n++) { if (keylen >= dhparams[n].min) { if (dhparams[n].dh == NULL) - dhparams[n].dh = make_dh_params(dhparams[n].prime, "2"); + dhparams[n].dh = make_dh_params(dhparams[n].prime); return dhparams[n].dh; } } @@ -1338,6 +1359,8 @@ static DH *ssl_dh_GetParamFromFile(const char *file) return NULL; dh = PEM_read_bio_DHparams(bio, NULL, NULL, NULL); BIO_free(bio); + if (!dh) + (void)ERR_get_error(); return dh; } @@ -1351,6 +1374,8 @@ static EC_GROUP *ssl_ec_GetParamFromFile(const char *file) return NULL; group = PEM_read_bio_ECPKParameters(bio, NULL, NULL, NULL); BIO_free(bio); + if (!group) + (void)ERR_get_error(); return group; } @@ -1378,85 +1403,6 @@ void tls_out(const char *s1, const char *s2) } void tls_err(const char *s) { tls_out(s, ssl_error()); if (smtps) die_read(); } -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(); - relayclient = ""; - protocol = proto.s; - } - - 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; @@ -1525,6 +1471,7 @@ void tls_init() SSL_set_cipher_list(myssl, ciphers); alloc_free(saciphers.s); + //TODO: we shouldn't use hardcoded DH: see https://weakdh.org/ SSL_set_tmp_dh_callback(myssl, tmp_dh_cb); /* try to read DH parameters from certificate */ @@ -1550,6 +1497,42 @@ void tls_init() EC_KEY_free(eckey); #endif +#if OPENSSL_VERSION_NUMBER >= 0x10100005L + stralloc opensslconf = {0}; + if (control_readfile(&opensslconf, "control/opensslconf", 0) == -1) + { SSL_free(myssl); die_control(); } + if (opensslconf.len) { + SSL_CONF_CTX *cctx = SSL_CONF_CTX_new(); + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_FILE); + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_SERVER); + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_CERTIFICATE); + SSL_CONF_CTX_set_flags(cctx, SSL_CONF_FLAG_SHOW_ERRORS); + SSL_CONF_CTX_set_ssl(cctx, myssl); + + int i, j, next = 0; + char *cmd, * arg; + for (i = 0; i < opensslconf.len; i += next) { + cmd = opensslconf.s + i; + next = str_len(cmd) + 1; + + j = str_chr(cmd, ' '); + arg = cmd + j; + while (*arg == ' ') ++arg; + cmd[j] = 0; + + if (SSL_CONF_cmd(cctx, cmd, arg) <= 0) { + enew(); eout("opensslconf \""); eout(cmd); eout(" "); eout(arg); + eout("\" failed: "); eout(ssl_error()); eout("\n"); eflush(); + tls_out("OpenSSL", "unable to initialize ssl"); + SSL_free(myssl); + die_read(); + } + } + + (void)SSL_CONF_CTX_finish(cctx); + } +#endif + SSL_set_rfd(myssl, ssl_rfd = substdio_fileno(&ssin)); SSL_set_wfd(myssl, ssl_wfd = substdio_fileno(&ssout)); diff --git a/ssl_timeoutio.c b/ssl_timeoutio.c index 5b2dc9d..30025d5 100644 --- a/ssl_timeoutio.c +++ b/ssl_timeoutio.c @@ -68,19 +68,6 @@ int ssl_timeoutconn(int t, int rfd, int wfd, SSL *ssl) return r; } -int ssl_timeoutrehandshake(int t, int rfd, int wfd, SSL *ssl) -{ - int r; - - SSL_renegotiate(ssl); - r = ssl_timeoutio(SSL_do_handshake, t, rfd, wfd, ssl, NULL, 0); - if (r <= 0 || ssl->type == SSL_ST_CONNECT) return r; - - /* this is for the server only */ - ssl->state = SSL_ST_ACCEPT; - return ssl_timeoutio(SSL_do_handshake, t, rfd, wfd, ssl, NULL, 0); -} - int ssl_timeoutread(int t, int rfd, int wfd, SSL *ssl, char *buf, int len) { if (!buf) return 0; diff --git a/ssl_timeoutio.h b/ssl_timeoutio.h index 073cb67..fff3859 100644 --- a/ssl_timeoutio.h +++ b/ssl_timeoutio.h @@ -10,7 +10,6 @@ int ssl_timeoutconn(int t, int rfd, int wfd, SSL *ssl); int ssl_timeoutaccept(int t, int rfd, int wfd, SSL *ssl); -int ssl_timeoutrehandshake(int t, int rfd, int wfd, SSL *ssl); int ssl_timeoutread(int t, int rfd, int wfd, SSL *ssl, char *buf, int len); int ssl_timeoutwrite(int t, int rfd, int wfd, SSL *ssl, char *buf, int len); diff --git a/tls.h b/tls.h index 808b115..3046c4e 100644 --- a/tls.h +++ b/tls.h @@ -2,6 +2,7 @@ #define TLS_H #include +#include /* ECC: make sure we have at least 1.0.0 */ #if !defined(OPENSSL_NO_EC) && defined(TLSEXT_ECPOINTFORMAT_uncompressed) -- cgit v1.2.3