changeset 5989:ec01b1d1fff1

Mail: client SSL certificates support. The "ssl_verify_client", "ssl_verify_depth", "ssl_client_certificate", "ssl_trusted_certificate", and "ssl_crl" directives introduced to control SSL client certificate verification in mail proxy module. If there is a certificate, detail of the certificate are passed to the auth_http script configured via Auth-SSL-Verify, Auth-SSL-Subject, Auth-SSL-Issuer, Auth-SSL-Serial, Auth-SSL-Fingerprint headers. If the auth_http_pass_client_cert directive is set, client certificate in PEM format will be passed in the Auth-SSL-Cert header (urlencoded). If there is no required certificate provided during an SSL handshake or certificate verification fails then a protocol-specific error is returned after the SSL handshake and the connection is closed. Based on previous work by Sven Peter, Franck Levionnois and Filipe Da Silva.
author Maxim Dounin <mdounin@mdounin.ru>
date Wed, 25 Feb 2015 17:48:05 +0300
parents 3b3f789655dc
children 6a7c6973d6fc
files src/mail/ngx_mail.h src/mail/ngx_mail_auth_http_module.c src/mail/ngx_mail_handler.c src/mail/ngx_mail_imap_module.c src/mail/ngx_mail_pop3_module.c src/mail/ngx_mail_smtp_module.c src/mail/ngx_mail_ssl_module.c src/mail/ngx_mail_ssl_module.h
diffstat 8 files changed, 294 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/src/mail/ngx_mail.h
+++ b/src/mail/ngx_mail.h
@@ -336,6 +336,8 @@ struct ngx_mail_protocol_s {
     ngx_mail_auth_state_pt      auth_state;
 
     ngx_str_t                   internal_server_error;
+    ngx_str_t                   cert_error;
+    ngx_str_t                   no_cert;
 };
 
 
--- a/src/mail/ngx_mail_auth_http_module.c
+++ b/src/mail/ngx_mail_auth_http_module.c
@@ -16,6 +16,7 @@ typedef struct {
     ngx_addr_t                     *peer;
 
     ngx_msec_t                      timeout;
+    ngx_flag_t                      pass_client_cert;
 
     ngx_str_t                       host_header;
     ngx_str_t                       uri;
@@ -106,6 +107,13 @@ static ngx_command_t  ngx_mail_auth_http
       0,
       NULL },
 
+    { ngx_string("auth_http_pass_client_cert"),
+      NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_MAIL_SRV_CONF_OFFSET,
+      offsetof(ngx_mail_auth_http_conf_t, pass_client_cert),
+      NULL },
+
       ngx_null_command
 };
 
@@ -1143,6 +1151,11 @@ ngx_mail_auth_http_create_request(ngx_ma
     size_t                     len;
     ngx_buf_t                 *b;
     ngx_str_t                  login, passwd;
+#if (NGX_MAIL_SSL)
+    ngx_str_t                  verify, subject, issuer, serial, fingerprint,
+                               raw_cert, cert;
+    ngx_connection_t          *c;
+#endif
     ngx_mail_core_srv_conf_t  *cscf;
 
     if (ngx_mail_auth_http_escape(pool, &s->login, &login) != NGX_OK) {
@@ -1153,6 +1166,61 @@ ngx_mail_auth_http_create_request(ngx_ma
         return NULL;
     }
 
+#if (NGX_MAIL_SSL)
+
+    c = s->connection;
+
+    if (c->ssl) {
+
+        /* certificate details */
+
+        if (ngx_ssl_get_client_verify(c, pool, &verify) != NGX_OK) {
+            return NULL;
+        }
+
+        if (ngx_ssl_get_subject_dn(c, pool, &subject) != NGX_OK) {
+            return NULL;
+        }
+
+        if (ngx_ssl_get_issuer_dn(c, pool, &issuer) != NGX_OK) {
+            return NULL;
+        }
+
+        if (ngx_ssl_get_serial_number(c, pool, &serial) != NGX_OK) {
+            return NULL;
+        }
+
+        if (ngx_ssl_get_fingerprint(c, pool, &fingerprint) != NGX_OK) {
+            return NULL;
+        }
+
+        if (ahcf->pass_client_cert) {
+
+            /* certificate itself, if configured */
+
+            if (ngx_ssl_get_raw_certificate(c, pool, &raw_cert) != NGX_OK) {
+                return NULL;
+            }
+
+            if (ngx_mail_auth_http_escape(pool, &raw_cert, &cert) != NGX_OK) {
+                return NULL;
+            }
+
+        } else {
+            ngx_str_null(&cert);
+        }
+
+    } else {
+        ngx_str_null(&verify);
+        ngx_str_null(&subject);
+        ngx_str_null(&issuer);
+        ngx_str_null(&serial);
+        ngx_str_null(&fingerprint);
+        ngx_str_null(&cert);
+    }
+
+#endif
+
     cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module);
 
     len = sizeof("GET ") - 1 + ahcf->uri.len + sizeof(" HTTP/1.0" CRLF) - 1
@@ -1175,6 +1243,13 @@ ngx_mail_auth_http_create_request(ngx_ma
           + sizeof("Auth-SMTP-To: ") - 1 + s->smtp_to.len + sizeof(CRLF) - 1
 #if (NGX_MAIL_SSL)
           + sizeof("Auth-SSL: on" CRLF) - 1
+          + sizeof("Auth-SSL-Verify: ") - 1 + verify.len + sizeof(CRLF) - 1
+          + sizeof("Auth-SSL-Subject: ") - 1 + subject.len + sizeof(CRLF) - 1
+          + sizeof("Auth-SSL-Issuer: ") - 1 + issuer.len + sizeof(CRLF) - 1
+          + sizeof("Auth-SSL-Serial: ") - 1 + serial.len + sizeof(CRLF) - 1
+          + sizeof("Auth-SSL-Fingerprint: ") - 1 + fingerprint.len
+              + sizeof(CRLF) - 1
+          + sizeof("Auth-SSL-Cert: ") - 1 + cert.len + sizeof(CRLF) - 1
 #endif
           + ahcf->header.len
           + sizeof(CRLF) - 1;
@@ -1260,9 +1335,49 @@ ngx_mail_auth_http_create_request(ngx_ma
 
 #if (NGX_MAIL_SSL)
 
-    if (s->connection->ssl) {
+    if (c->ssl) {
         b->last = ngx_cpymem(b->last, "Auth-SSL: on" CRLF,
                              sizeof("Auth-SSL: on" CRLF) - 1);
+
+        b->last = ngx_cpymem(b->last, "Auth-SSL-Verify: ",
+                             sizeof("Auth-SSL-Verify: ") - 1);
+        b->last = ngx_copy(b->last, verify.data, verify.len);
+        *b->last++ = CR; *b->last++ = LF;
+
+        if (subject.len) {
+            b->last = ngx_cpymem(b->last, "Auth-SSL-Subject: ",
+                                 sizeof("Auth-SSL-Subject: ") - 1);
+            b->last = ngx_copy(b->last, subject.data, subject.len);
+            *b->last++ = CR; *b->last++ = LF;
+        }
+
+        if (issuer.len) {
+            b->last = ngx_cpymem(b->last, "Auth-SSL-Issuer: ",
+                                 sizeof("Auth-SSL-Issuer: ") - 1);
+            b->last = ngx_copy(b->last, issuer.data, issuer.len);
+            *b->last++ = CR; *b->last++ = LF;
+        }
+
+        if (serial.len) {
+            b->last = ngx_cpymem(b->last, "Auth-SSL-Serial: ",
+                                 sizeof("Auth-SSL-Serial: ") - 1);
+            b->last = ngx_copy(b->last, serial.data, serial.len);
+            *b->last++ = CR; *b->last++ = LF;
+        }
+
+        if (fingerprint.len) {
+            b->last = ngx_cpymem(b->last, "Auth-SSL-Fingerprint: ",
+                                 sizeof("Auth-SSL-Fingerprint: ") - 1);
+            b->last = ngx_copy(b->last, fingerprint.data, fingerprint.len);
+            *b->last++ = CR; *b->last++ = LF;
+        }
+
+        if (cert.len) {
+            b->last = ngx_cpymem(b->last, "Auth-SSL-Cert: ",
+                                 sizeof("Auth-SSL-Cert: ") - 1);
+            b->last = ngx_copy(b->last, cert.data, cert.len);
+            *b->last++ = CR; *b->last++ = LF;
+        }
     }
 
 #endif
@@ -1328,6 +1443,7 @@ ngx_mail_auth_http_create_conf(ngx_conf_
     }
 
     ahcf->timeout = NGX_CONF_UNSET_MSEC;
+    ahcf->pass_client_cert = NGX_CONF_UNSET;
 
     ahcf->file = cf->conf_file->file.name.data;
     ahcf->line = cf->conf_file->line;
@@ -1363,6 +1479,8 @@ ngx_mail_auth_http_merge_conf(ngx_conf_t
 
     ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000);
 
+    ngx_conf_merge_value(conf->pass_client_cert, prev->pass_client_cert, 0);
+
     if (conf->headers == NULL) {
         conf->headers = prev->headers;
         conf->header = prev->header;
--- a/src/mail/ngx_mail_handler.c
+++ b/src/mail/ngx_mail_handler.c
@@ -16,6 +16,8 @@ static void ngx_mail_init_session(ngx_co
 #if (NGX_MAIL_SSL)
 static void ngx_mail_ssl_init_connection(ngx_ssl_t *ssl, ngx_connection_t *c);
 static void ngx_mail_ssl_handshake_handler(ngx_connection_t *c);
+static ngx_int_t ngx_mail_verify_cert(ngx_mail_session_t *s,
+    ngx_connection_t *c);
 #endif
 
 
@@ -247,6 +249,10 @@ ngx_mail_ssl_handshake_handler(ngx_conne
 
         s = c->data;
 
+        if (ngx_mail_verify_cert(s, c) != NGX_OK) {
+            return;
+        }
+
         if (s->starttls) {
             cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module);
 
@@ -267,6 +273,71 @@ ngx_mail_ssl_handshake_handler(ngx_conne
     ngx_mail_close_connection(c);
 }
 
+
+static ngx_int_t
+ngx_mail_verify_cert(ngx_mail_session_t *s, ngx_connection_t *c)
+{
+    long                       rc;
+    X509                      *cert;
+    ngx_mail_ssl_conf_t       *sslcf;
+    ngx_mail_core_srv_conf_t  *cscf;
+
+    sslcf = ngx_mail_get_module_srv_conf(s, ngx_mail_ssl_module);
+
+    if (!sslcf->verify) {
+        return NGX_OK;
+    }
+
+    rc = SSL_get_verify_result(c->ssl->connection);
+
+    if (rc != X509_V_OK
+        && (sslcf->verify != 3 || !ngx_ssl_verify_error_optional(rc)))
+    {
+        ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                      "client SSL certificate verify error: (%l:%s)",
+                      rc, X509_verify_cert_error_string(rc));
+
+        ngx_ssl_remove_cached_session(sslcf->ssl.ctx,
+                                      (SSL_get0_session(c->ssl->connection)));
+
+        cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module);
+
+        s->out = cscf->protocol->cert_error;
+        s->quit = 1;
+
+        c->write->handler = ngx_mail_send;
+
+        ngx_mail_send(s->connection->write);
+        return NGX_ERROR;
+    }
+
+    if (sslcf->verify == 1) {
+        cert = SSL_get_peer_certificate(c->ssl->connection);
+
+        if (cert == NULL) {
+            ngx_log_error(NGX_LOG_INFO, c->log, 0,
+                          "client sent no required SSL certificate");
+
+            ngx_ssl_remove_cached_session(sslcf->ssl.ctx,
+                                       (SSL_get0_session(c->ssl->connection)));
+
+            cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module);
+
+            s->out = cscf->protocol->no_cert;
+            s->quit = 1;
+
+            c->write->handler = ngx_mail_send;
+
+            ngx_mail_send(s->connection->write);
+            return NGX_ERROR;
+        }
+
+        X509_free(cert);
+    }
+
+    return NGX_OK;
+}
+
 #endif
 
 
--- a/src/mail/ngx_mail_imap_module.c
+++ b/src/mail/ngx_mail_imap_module.c
@@ -52,7 +52,9 @@ static ngx_mail_protocol_t  ngx_mail_ima
     ngx_mail_imap_parse_command,
     ngx_mail_imap_auth_state,
 
-    ngx_string("* BAD internal server error" CRLF)
+    ngx_string("* BAD internal server error" CRLF),
+    ngx_string("* BYE SSL certificate error" CRLF),
+    ngx_string("* BYE No required SSL certificate" CRLF)
 };
 
 
--- a/src/mail/ngx_mail_pop3_module.c
+++ b/src/mail/ngx_mail_pop3_module.c
@@ -58,7 +58,9 @@ static ngx_mail_protocol_t  ngx_mail_pop
     ngx_mail_pop3_parse_command,
     ngx_mail_pop3_auth_state,
 
-    ngx_string("-ERR internal server error" CRLF)
+    ngx_string("-ERR internal server error" CRLF),
+    ngx_string("-ERR SSL certificate error" CRLF),
+    ngx_string("-ERR No required SSL certificate" CRLF)
 };
 
 
--- a/src/mail/ngx_mail_smtp_module.c
+++ b/src/mail/ngx_mail_smtp_module.c
@@ -45,7 +45,9 @@ static ngx_mail_protocol_t  ngx_mail_smt
     ngx_mail_smtp_parse_command,
     ngx_mail_smtp_auth_state,
 
-    ngx_string("451 4.3.2 Internal server error" CRLF)
+    ngx_string("451 4.3.2 Internal server error" CRLF),
+    ngx_string("421 4.7.1 SSL certificate error" CRLF),
+    ngx_string("421 4.7.1 No required SSL certificate" CRLF)
 };
 
 
--- a/src/mail/ngx_mail_ssl_module.c
+++ b/src/mail/ngx_mail_ssl_module.c
@@ -46,6 +46,15 @@ static ngx_conf_bitmask_t  ngx_mail_ssl_
 };
 
 
+static ngx_conf_enum_t  ngx_mail_ssl_verify[] = {
+    { ngx_string("off"), 0 },
+    { ngx_string("on"), 1 },
+    { ngx_string("optional"), 2 },
+    { ngx_string("optional_no_ca"), 3 },
+    { ngx_null_string, 0 }
+};
+
+
 static ngx_command_t  ngx_mail_ssl_commands[] = {
 
     { ngx_string("ssl"),
@@ -146,6 +155,41 @@ static ngx_command_t  ngx_mail_ssl_comma
       offsetof(ngx_mail_ssl_conf_t, session_timeout),
       NULL },
 
+    { ngx_string("ssl_verify_client"),
+      NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_enum_slot,
+      NGX_MAIL_SRV_CONF_OFFSET,
+      offsetof(ngx_mail_ssl_conf_t, verify),
+      &ngx_mail_ssl_verify },
+
+    { ngx_string("ssl_verify_depth"),
+      NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_num_slot,
+      NGX_MAIL_SRV_CONF_OFFSET,
+      offsetof(ngx_mail_ssl_conf_t, verify_depth),
+      NULL },
+
+    { ngx_string("ssl_client_certificate"),
+      NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_str_slot,
+      NGX_MAIL_SRV_CONF_OFFSET,
+      offsetof(ngx_mail_ssl_conf_t, client_certificate),
+      NULL },
+
+    { ngx_string("ssl_trusted_certificate"),
+      NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_str_slot,
+      NGX_MAIL_SRV_CONF_OFFSET,
+      offsetof(ngx_mail_ssl_conf_t, trusted_certificate),
+      NULL },
+
+    { ngx_string("ssl_crl"),
+      NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_str_slot,
+      NGX_MAIL_SRV_CONF_OFFSET,
+      offsetof(ngx_mail_ssl_conf_t, crl),
+      NULL },
+
       ngx_null_command
 };
 
@@ -198,6 +242,9 @@ ngx_mail_ssl_create_conf(ngx_conf_t *cf)
      *     scf->certificate_key = { 0, NULL };
      *     scf->dhparam = { 0, NULL };
      *     scf->ecdh_curve = { 0, NULL };
+     *     scf->client_certificate = { 0, NULL };
+     *     scf->trusted_certificate = { 0, NULL };
+     *     scf->crl = { 0, NULL };
      *     scf->ciphers = { 0, NULL };
      *     scf->shm_zone = NULL;
      */
@@ -206,6 +253,8 @@ ngx_mail_ssl_create_conf(ngx_conf_t *cf)
     scf->starttls = NGX_CONF_UNSET_UINT;
     scf->passwords = NGX_CONF_UNSET_PTR;
     scf->prefer_server_ciphers = NGX_CONF_UNSET;
+    scf->verify = NGX_CONF_UNSET_UINT;
+    scf->verify_depth = NGX_CONF_UNSET_UINT;
     scf->builtin_session_cache = NGX_CONF_UNSET;
     scf->session_timeout = NGX_CONF_UNSET;
     scf->session_tickets = NGX_CONF_UNSET;
@@ -238,6 +287,9 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, 
                          (NGX_CONF_BITMASK_SET|NGX_SSL_SSLv3|NGX_SSL_TLSv1
                           |NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2));
 
+    ngx_conf_merge_uint_value(conf->verify, prev->verify, 0);
+    ngx_conf_merge_uint_value(conf->verify_depth, prev->verify_depth, 1);
+
     ngx_conf_merge_str_value(conf->certificate, prev->certificate, "");
     ngx_conf_merge_str_value(conf->certificate_key, prev->certificate_key, "");
 
@@ -248,6 +300,12 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, 
     ngx_conf_merge_str_value(conf->ecdh_curve, prev->ecdh_curve,
                          NGX_DEFAULT_ECDH_CURVE);
 
+    ngx_conf_merge_str_value(conf->client_certificate,
+                         prev->client_certificate, "");
+    ngx_conf_merge_str_value(conf->trusted_certificate,
+                         prev->trusted_certificate, "");
+    ngx_conf_merge_str_value(conf->crl, prev->crl, "");
+
     ngx_conf_merge_str_value(conf->ciphers, prev->ciphers, NGX_DEFAULT_CIPHERS);
 
 
@@ -320,6 +378,35 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, 
         return NGX_CONF_ERROR;
     }
 
+    if (conf->verify) {
+
+        if (conf->client_certificate.len == 0 && conf->verify != 3) {
+            ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
+                          "no ssl_client_certificate for ssl_client_verify");
+            return NGX_CONF_ERROR;
+        }
+
+        if (ngx_ssl_client_certificate(cf, &conf->ssl,
+                                       &conf->client_certificate,
+                                       conf->verify_depth)
+            != NGX_OK)
+        {
+            return NGX_CONF_ERROR;
+        }
+
+        if (ngx_ssl_trusted_certificate(cf, &conf->ssl,
+                                        &conf->trusted_certificate,
+                                        conf->verify_depth)
+            != NGX_OK)
+        {
+            return NGX_CONF_ERROR;
+        }
+
+        if (ngx_ssl_crl(cf, &conf->ssl, &conf->crl) != NGX_OK) {
+            return NGX_CONF_ERROR;
+        }
+    }
+
     if (SSL_CTX_set_cipher_list(conf->ssl.ctx,
                                 (const char *) conf->ciphers.data)
         == 0)
--- a/src/mail/ngx_mail_ssl_module.h
+++ b/src/mail/ngx_mail_ssl_module.h
@@ -28,6 +28,9 @@ typedef struct {
     ngx_uint_t       starttls;
     ngx_uint_t       protocols;
 
+    ngx_uint_t       verify;
+    ngx_uint_t       verify_depth;
+
     ssize_t          builtin_session_cache;
 
     time_t           session_timeout;
@@ -36,6 +39,9 @@ typedef struct {
     ngx_str_t        certificate_key;
     ngx_str_t        dhparam;
     ngx_str_t        ecdh_curve;
+    ngx_str_t        client_certificate;
+    ngx_str_t        trusted_certificate;
+    ngx_str_t        crl;
 
     ngx_str_t        ciphers;