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.
This commit is contained in:
Maxim Dounin 2015-02-25 17:48:05 +03:00
parent 78e1a8ed7f
commit faec547771
8 changed files with 294 additions and 4 deletions

View File

@ -336,6 +336,8 @@ struct ngx_mail_protocol_s {
ngx_mail_auth_state_pt auth_state; ngx_mail_auth_state_pt auth_state;
ngx_str_t internal_server_error; ngx_str_t internal_server_error;
ngx_str_t cert_error;
ngx_str_t no_cert;
}; };

View File

@ -16,6 +16,7 @@ typedef struct {
ngx_addr_t *peer; ngx_addr_t *peer;
ngx_msec_t timeout; ngx_msec_t timeout;
ngx_flag_t pass_client_cert;
ngx_str_t host_header; ngx_str_t host_header;
ngx_str_t uri; ngx_str_t uri;
@ -106,6 +107,13 @@ static ngx_command_t ngx_mail_auth_http_commands[] = {
0, 0,
NULL }, 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 ngx_null_command
}; };
@ -1143,6 +1151,11 @@ ngx_mail_auth_http_create_request(ngx_mail_session_t *s, ngx_pool_t *pool,
size_t len; size_t len;
ngx_buf_t *b; ngx_buf_t *b;
ngx_str_t login, passwd; 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; ngx_mail_core_srv_conf_t *cscf;
if (ngx_mail_auth_http_escape(pool, &s->login, &login) != NGX_OK) { if (ngx_mail_auth_http_escape(pool, &s->login, &login) != NGX_OK) {
@ -1153,6 +1166,61 @@ ngx_mail_auth_http_create_request(ngx_mail_session_t *s, ngx_pool_t *pool,
return NULL; 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); 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 len = sizeof("GET ") - 1 + ahcf->uri.len + sizeof(" HTTP/1.0" CRLF) - 1
@ -1175,6 +1243,13 @@ ngx_mail_auth_http_create_request(ngx_mail_session_t *s, ngx_pool_t *pool,
+ sizeof("Auth-SMTP-To: ") - 1 + s->smtp_to.len + sizeof(CRLF) - 1 + sizeof("Auth-SMTP-To: ") - 1 + s->smtp_to.len + sizeof(CRLF) - 1
#if (NGX_MAIL_SSL) #if (NGX_MAIL_SSL)
+ sizeof("Auth-SSL: on" CRLF) - 1 + 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 #endif
+ ahcf->header.len + ahcf->header.len
+ sizeof(CRLF) - 1; + sizeof(CRLF) - 1;
@ -1260,9 +1335,49 @@ ngx_mail_auth_http_create_request(ngx_mail_session_t *s, ngx_pool_t *pool,
#if (NGX_MAIL_SSL) #if (NGX_MAIL_SSL)
if (s->connection->ssl) { if (c->ssl) {
b->last = ngx_cpymem(b->last, "Auth-SSL: on" CRLF, b->last = ngx_cpymem(b->last, "Auth-SSL: on" CRLF,
sizeof("Auth-SSL: on" CRLF) - 1); 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 #endif
@ -1328,6 +1443,7 @@ ngx_mail_auth_http_create_conf(ngx_conf_t *cf)
} }
ahcf->timeout = NGX_CONF_UNSET_MSEC; ahcf->timeout = NGX_CONF_UNSET_MSEC;
ahcf->pass_client_cert = NGX_CONF_UNSET;
ahcf->file = cf->conf_file->file.name.data; ahcf->file = cf->conf_file->file.name.data;
ahcf->line = cf->conf_file->line; ahcf->line = cf->conf_file->line;
@ -1363,6 +1479,8 @@ ngx_mail_auth_http_merge_conf(ngx_conf_t *cf, void *parent, void *child)
ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000); 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) { if (conf->headers == NULL) {
conf->headers = prev->headers; conf->headers = prev->headers;
conf->header = prev->header; conf->header = prev->header;

View File

@ -16,6 +16,8 @@ static void ngx_mail_init_session(ngx_connection_t *c);
#if (NGX_MAIL_SSL) #if (NGX_MAIL_SSL)
static void ngx_mail_ssl_init_connection(ngx_ssl_t *ssl, ngx_connection_t *c); 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 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 #endif
@ -247,6 +249,10 @@ ngx_mail_ssl_handshake_handler(ngx_connection_t *c)
s = c->data; s = c->data;
if (ngx_mail_verify_cert(s, c) != NGX_OK) {
return;
}
if (s->starttls) { if (s->starttls) {
cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module); cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module);
@ -267,6 +273,71 @@ ngx_mail_ssl_handshake_handler(ngx_connection_t *c)
ngx_mail_close_connection(c); 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 #endif

View File

@ -52,7 +52,9 @@ static ngx_mail_protocol_t ngx_mail_imap_protocol = {
ngx_mail_imap_parse_command, ngx_mail_imap_parse_command,
ngx_mail_imap_auth_state, 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)
}; };

View File

@ -58,7 +58,9 @@ static ngx_mail_protocol_t ngx_mail_pop3_protocol = {
ngx_mail_pop3_parse_command, ngx_mail_pop3_parse_command,
ngx_mail_pop3_auth_state, 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)
}; };

View File

@ -45,7 +45,9 @@ static ngx_mail_protocol_t ngx_mail_smtp_protocol = {
ngx_mail_smtp_parse_command, ngx_mail_smtp_parse_command,
ngx_mail_smtp_auth_state, 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)
}; };

View File

@ -46,6 +46,15 @@ static ngx_conf_bitmask_t ngx_mail_ssl_protocols[] = {
}; };
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[] = { static ngx_command_t ngx_mail_ssl_commands[] = {
{ ngx_string("ssl"), { ngx_string("ssl"),
@ -146,6 +155,41 @@ static ngx_command_t ngx_mail_ssl_commands[] = {
offsetof(ngx_mail_ssl_conf_t, session_timeout), offsetof(ngx_mail_ssl_conf_t, session_timeout),
NULL }, 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 ngx_null_command
}; };
@ -198,6 +242,9 @@ ngx_mail_ssl_create_conf(ngx_conf_t *cf)
* scf->certificate_key = { 0, NULL }; * scf->certificate_key = { 0, NULL };
* scf->dhparam = { 0, NULL }; * scf->dhparam = { 0, NULL };
* scf->ecdh_curve = { 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->ciphers = { 0, NULL };
* scf->shm_zone = 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->starttls = NGX_CONF_UNSET_UINT;
scf->passwords = NGX_CONF_UNSET_PTR; scf->passwords = NGX_CONF_UNSET_PTR;
scf->prefer_server_ciphers = NGX_CONF_UNSET; 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->builtin_session_cache = NGX_CONF_UNSET;
scf->session_timeout = NGX_CONF_UNSET; scf->session_timeout = NGX_CONF_UNSET;
scf->session_tickets = NGX_CONF_UNSET; scf->session_tickets = NGX_CONF_UNSET;
@ -238,6 +287,9 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
(NGX_CONF_BITMASK_SET|NGX_SSL_SSLv3|NGX_SSL_TLSv1 (NGX_CONF_BITMASK_SET|NGX_SSL_SSLv3|NGX_SSL_TLSv1
|NGX_SSL_TLSv1_1|NGX_SSL_TLSv1_2)); |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, prev->certificate, "");
ngx_conf_merge_str_value(conf->certificate_key, prev->certificate_key, ""); ngx_conf_merge_str_value(conf->certificate_key, prev->certificate_key, "");
@ -248,6 +300,12 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child)
ngx_conf_merge_str_value(conf->ecdh_curve, prev->ecdh_curve, ngx_conf_merge_str_value(conf->ecdh_curve, prev->ecdh_curve,
NGX_DEFAULT_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); 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, void *parent, void *child)
return NGX_CONF_ERROR; 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, if (SSL_CTX_set_cipher_list(conf->ssl.ctx,
(const char *) conf->ciphers.data) (const char *) conf->ciphers.data)
== 0) == 0)

View File

@ -28,6 +28,9 @@ typedef struct {
ngx_uint_t starttls; ngx_uint_t starttls;
ngx_uint_t protocols; ngx_uint_t protocols;
ngx_uint_t verify;
ngx_uint_t verify_depth;
ssize_t builtin_session_cache; ssize_t builtin_session_cache;
time_t session_timeout; time_t session_timeout;
@ -36,6 +39,9 @@ typedef struct {
ngx_str_t certificate_key; ngx_str_t certificate_key;
ngx_str_t dhparam; ngx_str_t dhparam;
ngx_str_t ecdh_curve; ngx_str_t ecdh_curve;
ngx_str_t client_certificate;
ngx_str_t trusted_certificate;
ngx_str_t crl;
ngx_str_t ciphers; ngx_str_t ciphers;