[PATCH] mail_{ssl, auth_http}_module: add support for SSL client certificates

Maxim Dounin mdounin at mdounin.ru
Tue Jan 14 12:08:05 UTC 2014


Hello!

On Mon, Jan 13, 2014 at 04:28:25PM +0100, Sven Peter wrote:

> # HG changeset patch
> # User Sven Peter <sven at ha.cki.ng>
> # Date 1389626052 -3600
> #      Mon Jan 13 16:14:12 2014 +0100
> # Node ID a444733105e8eb96212f142533e714532a23cddf
> # Parent  4aa64f6950313311e0d322a2af1788edeb7f036c
> mail_{ssl,auth_http}_module: add support for SSL client certificates

Better summary line would be:

Mail: added support for SSL client certificate.

> 
> This patch adds support for SSL client certificates to the mail proxy
> capabilities of nginx both for STARTTLS and SSL mode.
> Just like the HTTP SSL module a root CA is defined in the mail section
> of the configuration file. Verification can be optional or mandatory.
> Additionally, the result of the verification is exposed to the
> auth http backend via the SSL-Verify, SSL-Subject-DN, SSL-Issuer-DN
> and SSL-Serial HTTP headers.

It would be good idea to add a list of configuration directives 
added.

> 
> diff -r 4aa64f695031 -r a444733105e8 src/mail/ngx_mail_auth_http_module.c
> --- a/src/mail/ngx_mail_auth_http_module.c	Sat Jan 04 03:32:22 2014 +0400
> +++ b/src/mail/ngx_mail_auth_http_module.c	Mon Jan 13 16:14:12 2014 +0100
> @@ -1145,6 +1145,16 @@
>      ngx_str_t                  login, passwd;
>      ngx_mail_core_srv_conf_t  *cscf;
>  
> +#if (NGX_MAIL_SSL)
> +    ngx_str_t ssl_client_verify;
> +    ngx_str_t ssl_client_raw_s_dn;
> +    ngx_str_t ssl_client_raw_i_dn;
> +    ngx_str_t ssl_client_raw_serial;
> +    ngx_str_t ssl_client_s_dn;
> +    ngx_str_t ssl_client_i_dn;
> +    ngx_str_t ssl_client_serial;
> +#endif
> +

This diverges from the style used.  Additionally, variable names 
seems to be too verbose.

>      if (ngx_mail_auth_http_escape(pool, &s->login, &login) != NGX_OK) {
>          return NULL;
>      }
> @@ -1153,6 +1163,51 @@
>          return NULL;
>      }
>  
> +#if (NGX_MAIL_SSL)
> +    if (s->connection->ssl) {
> +        /* ssl_client_verify comes from nginx itself - no need to escape */

This comment looks obvious.

> +        if (ngx_ssl_get_client_verify(s->connection, pool,
> +                                      &ssl_client_verify) != NGX_OK) {
> +            return NULL;
> +        }
> +
> +        if (ngx_ssl_get_subject_dn(s->connection, pool,
> +                                   &ssl_client_raw_s_dn) != NGX_OK) {
> +            return NULL;
> +        }
> +
> +        if (ngx_ssl_get_issuer_dn(s->connection, pool,
> +                                  &ssl_client_raw_i_dn) != NGX_OK) {
> +            return NULL;
> +        }
> +
> +        if (ngx_ssl_get_serial_number(s->connection, pool,
> +                                      &ssl_client_raw_serial) != NGX_OK) {
> +            return NULL;
> +        }
> +
> +        if (ngx_mail_auth_http_escape(pool, &ssl_client_raw_s_dn, 
> +                                      &ssl_client_s_dn) != NGX_OK) {
> +            return NULL;
> +        }
> +
> +        if (ngx_mail_auth_http_escape(pool, &ssl_client_raw_i_dn,
> +                                      &ssl_client_i_dn) != NGX_OK) {
> +            return NULL;
> +        }
> +
> +        if (ngx_mail_auth_http_escape(pool, &ssl_client_raw_serial,
> +                                      &ssl_client_serial) != NGX_OK) {
> +            return NULL;
> +        }

On the other hand, escaping of at least client certificate serial 
number looks unneeded.

> +    } else {
> +        ngx_str_set(&ssl_client_verify, "NONE");
> +        ssl_client_i_dn.len = 0;
> +        ssl_client_s_dn.len = 0;
> +        ssl_client_serial.len = 0;

Using fake values here looks wrong.  In http, nginx marks $ssl_* 
variables as "not found" for non-ssl connections, which is 
essentially equivalent to empty strings, i.e., this contradicts to 
the use of "NONE".

> +    }
> +#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
> @@ -1173,6 +1228,16 @@
>            + sizeof("Auth-SMTP-Helo: ") - 1 + s->smtp_helo.len
>            + sizeof("Auth-SMTP-From: ") - 1 + s->smtp_from.len
>            + sizeof("Auth-SMTP-To: ") - 1 + s->smtp_to.len
> +#if (NGX_MAIL_SSL)
> +          + sizeof("SSL-Verify: ") - 1 + ssl_client_verify.len
> +          + sizeof(CRLF) - 1
> +          + sizeof("SSL-Subject-DN: ") - 1 + ssl_client_s_dn.len
> +          + sizeof(CRLF) - 1
> +          + sizeof("SSL-Issuer-DN: ") - 1 + ssl_client_i_dn.len
> +          + sizeof(CRLF) - 1
> +          + sizeof("SSL-Serial: ") - 1 + ssl_client_serial.len
> +          + sizeof(CRLF) - 1
> +#endif

Using common prefix "Auth-" might be a good idea.

>            + ahcf->header.len
>            + sizeof(CRLF) - 1;
>  
> @@ -1255,6 +1320,34 @@
>  
>      }
>  
> +#if (NGX_MAIL_SSL)
> +    if (ssl_client_verify.len) {
> +        b->last = ngx_cpymem(b->last, "SSL-Verify: ",
> +                             sizeof("SSL-Verify: ") - 1);
> +        b->last = ngx_copy(b->last, ssl_client_verify.data,
> +                           ssl_client_verify.len);
> +        *b->last++ = CR; *b->last++ = LF;
> +
> +        b->last = ngx_cpymem(b->last, "SSL-Subject-DN: ",
> +                             sizeof("SSL-Subject-DN: ") - 1);
> +        b->last = ngx_copy(b->last, ssl_client_s_dn.data,
> +                           ssl_client_s_dn.len);
> +        *b->last++ = CR; *b->last++ = LF;
> +
> +        b->last = ngx_cpymem(b->last, "SSL-Issuer-DN: ",
> +                             sizeof("SSL-Issuer-DN: ") - 1);
> +        b->last = ngx_copy(b->last, ssl_client_i_dn.data,
> +                           ssl_client_i_dn.len);
> +        *b->last++ = CR; *b->last++ = LF;
> +
> +        b->last = ngx_cpymem(b->last, "SSL-Serial: ",
> +                             sizeof("SSL-Serial: ") - 1);
> +        b->last = ngx_copy(b->last, ssl_client_serial.data,
> +                           ssl_client_serial.len);
> +        *b->last++ = CR; *b->last++ = LF;
> +    }
> +#endif
> +

I don't think that these headers should be sent if there is no 
SSL connection.  Any empty headers should be probably ommitted, 
too.

>      if (ahcf->header.len) {
>          b->last = ngx_copy(b->last, ahcf->header.data, ahcf->header.len);
>      }
> diff -r 4aa64f695031 -r a444733105e8 src/mail/ngx_mail_handler.c
> --- a/src/mail/ngx_mail_handler.c	Sat Jan 04 03:32:22 2014 +0400
> +++ b/src/mail/ngx_mail_handler.c	Mon Jan 13 16:14:12 2014 +0100
> @@ -236,11 +236,40 @@
>  {
>      ngx_mail_session_t        *s;
>      ngx_mail_core_srv_conf_t  *cscf;
> +    ngx_mail_ssl_conf_t *sslcf;
>  
>      if (c->ssl->handshaked) {
>  
>          s = c->data;
>  
> +        sslcf = ngx_mail_get_module_srv_conf(s, ngx_mail_ssl_module);
> +        if (sslcf->verify != NGX_MAIL_SSL_VERIFY_OFF) {

The use of the != check looks silly.  You may want to preserve the 
same code as used in http, where ->verify is more or less boolean 
with some special true values to differentiate submodes when 
verify is used.

> +            long rc;
> +            rc = SSL_get_verify_result(c->ssl->connection);
> +
> +            if (rc != X509_V_OK &&
> +                (sslcf->verify != NGX_MAIL_SSL_VERIFY_OPTIONAL_NO_CA && 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_mail_close_connection(c);
> +                return;
> +            }

Minor note: a 80+ line here due to use of long names.

Maror problem: you allow "optional_no_ca" here, but this is for 
sure not secure due to no certificate passed to a backend.

> +            
> +            if (sslcf->verify == NGX_MAIL_SSL_VERIFY_ON) {
> +                X509 *cert;
> +                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_mail_close_connection(c); 
> +                    return;          
> +                }
> +                X509_free(cert);
> +            }
> +        }
> +
>          if (s->starttls) {
>              cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module);
>  
> diff -r 4aa64f695031 -r a444733105e8 src/mail/ngx_mail_ssl_module.c
> --- a/src/mail/ngx_mail_ssl_module.c	Sat Jan 04 03:32:22 2014 +0400
> +++ b/src/mail/ngx_mail_ssl_module.c	Mon Jan 13 16:14:12 2014 +0100
> @@ -43,6 +43,13 @@
>      { ngx_null_string, 0 }
>  };
>  
> +static ngx_conf_enum_t ngx_mail_ssl_verify[] = {
> +    { ngx_string("off"), NGX_MAIL_SSL_VERIFY_OFF },
> +    { ngx_string("on"), NGX_MAIL_SSL_VERIFY_ON },
> +    { ngx_string("optional"), NGX_MAIL_SSL_VERIFY_OPTIONAL },
> +    { ngx_string("optional_no_ca"), NGX_MAIL_SSL_VERIFY_OPTIONAL_NO_CA },

As noted above, "optional_no_ca" makes no sense without a way to 
pass a certificate to some backend.

> +    { ngx_null_string, 0 }
> +};
>  
>  static ngx_command_t  ngx_mail_ssl_commands[] = {

You may note that previously there were 2 empty lines between 
blocks.  With your change, there is just 1 empty line.

>  
> @@ -130,7 +137,40 @@
>        offsetof(ngx_mail_ssl_conf_t, session_timeout),
>        NULL },
>  
> -      ngx_null_command
> +    {

The change here indicate you did something wrong with style.

> +      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
> +    },
> +    {

The style here is certainly wrong.

> +      ngx_string("ssl_verify_depth"),
> +      NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_1MORE,

Hm, "1MORE" is wrong here, should be "TAKE1".  Fixed this in http 
ssl module.

> +      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_null_command
>  };
>  
>  
> @@ -184,6 +224,8 @@
>       *     scf->ecdh_curve = { 0, NULL };
>       *     scf->ciphers = { 0, NULL };
>       *     scf->shm_zone = NULL;
> +     *     scf->client_certificate = { 0, NULL };
> +     *     scf->trusted_certificate = { 0, NULL };
>       */
>  
>      scf->enable = NGX_CONF_UNSET;
> @@ -192,6 +234,8 @@
>      scf->builtin_session_cache = NGX_CONF_UNSET;
>      scf->session_timeout = NGX_CONF_UNSET;
>      scf->session_ticket_keys = NGX_CONF_UNSET_PTR;
> +    scf->verify = NGX_CONF_UNSET_UINT;
> +    scf->verify_depth = NGX_CONF_UNSET_UINT;
>  
>      return scf;
>  }
> @@ -230,6 +274,11 @@
>  
>      ngx_conf_merge_str_value(conf->ciphers, prev->ciphers, NGX_DEFAULT_CIPHERS);
>  
> +    ngx_conf_merge_uint_value(conf->verify, prev->verify, NGX_MAIL_SSL_VERIFY_OFF);
> +    ngx_conf_merge_uint_value(conf->verify_depth, prev->verify_depth, 1);
> +
> +    ngx_conf_merge_str_value(conf->client_certificate, prev->client_certificate, "");
> +    ngx_conf_merge_str_value(conf->trusted_certificate, prev->trusted_certificate, "");
>  
>      conf->ssl.log = cf->log;
>  
> @@ -310,6 +359,21 @@
>          return NGX_CONF_ERROR;
>      }
>  
> +    if (conf->verify) {
> +      if (conf->client_certificate.len == 0 && conf->verify != NGX_MAIL_SSL_VERIFY_OPTIONAL_NO_CA) {
> +        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;
> +      }
> +    }
> +

This code looks incomplete (and there are style issues).  E.g., it 
doesn't looks like trusted certificates are loaded at all.

It also lacks ssl_crl support, which is also directly related to 
client certificates.

>      if (conf->prefer_server_ciphers) {
>          SSL_CTX_set_options(conf->ssl.ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
>      }
> diff -r 4aa64f695031 -r a444733105e8 src/mail/ngx_mail_ssl_module.h
> --- a/src/mail/ngx_mail_ssl_module.h	Sat Jan 04 03:32:22 2014 +0400
> +++ b/src/mail/ngx_mail_ssl_module.h	Mon Jan 13 16:14:12 2014 +0100
> @@ -37,8 +37,14 @@
>      ngx_str_t        dhparam;
>      ngx_str_t        ecdh_curve;
>  
> +    ngx_str_t        client_certificate;
> +    ngx_str_t        trusted_certificate;
> +
>      ngx_str_t        ciphers;
>  
> +    ngx_uint_t       verify;
> +    ngx_uint_t       verify_depth;
> +
>      ngx_shm_zone_t  *shm_zone;
>  
>      ngx_array_t     *session_ticket_keys;
> @@ -47,6 +53,13 @@
>      ngx_uint_t       line;
>  } ngx_mail_ssl_conf_t;
>  
> +enum ngx_mail_ssl_verify_enum {
> +  NGX_MAIL_SSL_VERIFY_OFF = 0,
> +  NGX_MAIL_SSL_VERIFY_ON,
> +  NGX_MAIL_SSL_VERIFY_OPTIONAL,
> +  NGX_MAIL_SSL_VERIFY_OPTIONAL_NO_CA,
> +};
> +
>  
>  extern ngx_module_t  ngx_mail_ssl_module;

We usually avoid using enum types for enumerated configuration 
slots.  While questionable, it's currently mostly style.

There are also other style problems here - indentation is wrong, 
as well as number of empty lines.

-- 
Maxim Dounin
http://nginx.org/



More information about the nginx-devel mailing list