SSL: added early ClientHello callback. Early ClientHello callback allows configuration of available TLS protocol versions on a per server basis. Works with BoringSSL and OpenSSL (requires OpenSSL 1.1.1). Change-Id: I3cbd7f45eb1d0207adbcb4a0b6442073c5bb8296 Signed-off-by: Piotr Sikora <piotrsikora@google.com> Reviewed-on: https://nginx-review.googlesource.com/c/3420
diff --git a/src/http/modules/ngx_http_ssl_module.c b/src/http/modules/ngx_http_ssl_module.c index 37438bd..8ea3546 100644 --- a/src/http/modules/ngx_http_ssl_module.c +++ b/src/http/modules/ngx_http_ssl_module.c
@@ -682,6 +682,21 @@ #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME + /* + * Install early ClientHello callback to allow configuration of available + * TLS protocol versions on a per server basis, which isn't possible with + * the regular callback. + * + * NOTE: This callback is installed in addition to the regular callback, + * which is going to be called to acknowledge requested server name. + */ + +#if defined(OPENSSL_IS_BORINGSSL) + SSL_CTX_set_select_certificate_cb(conf->ssl.ctx, ngx_http_ssl_client_hello); +#elif defined(SSL_CLIENT_HELLO_CB) + SSL_CTX_set_client_hello_cb(conf->ssl.ctx, ngx_http_ssl_client_hello, NULL); +#endif + if (SSL_CTX_set_tlsext_servername_callback(conf->ssl.ctx, ngx_http_ssl_servername) == 0)
diff --git a/src/http/ngx_http.h b/src/http/ngx_http.h index 29879f0..f6bece5 100644 --- a/src/http/ngx_http.h +++ b/src/http/ngx_http.h
@@ -88,6 +88,12 @@ void ngx_http_close_connection(ngx_connection_t *c); #if (NGX_HTTP_SSL && defined SSL_CTRL_SET_TLSEXT_HOSTNAME) +#if defined(OPENSSL_IS_BORINGSSL) +enum ssl_select_cert_result_t ngx_http_ssl_client_hello( + const SSL_CLIENT_HELLO *client_hello); +#elif defined(SSL_CLIENT_HELLO_CB) +int ngx_http_ssl_client_hello(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg); +#endif int ngx_http_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg); #endif
diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c index 364067f..0218ad2 100644 --- a/src/http/ngx_http_request.c +++ b/src/http/ngx_http_request.c
@@ -835,6 +835,156 @@ #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME +#if defined(OPENSSL_IS_BORINGSSL) + +enum ssl_select_cert_result_t +ngx_http_ssl_client_hello(const SSL_CLIENT_HELLO *client_hello) +{ + CBS extension, list, name; + size_t len; + uint8_t type; + ngx_str_t hostname; + const uint8_t *data; + ngx_connection_t *c; + + /* Based on BoringSSL's ext_sni_parse_clienthello(). */ + + if (SSL_early_callback_ctx_extension_get(client_hello, + TLSEXT_TYPE_server_name, + &data, &len) + == 0) + { + return ssl_select_cert_success; + } + + CBS_init(&extension, data, len); + + if (CBS_get_u16_length_prefixed(&extension, &list) == 0 + || CBS_get_u8(&list, &type) == 0 + || CBS_get_u16_length_prefixed(&list, &name) == 0 + || CBS_len(&list) != 0 + || CBS_len(&extension) != 0) + { + return ssl_select_cert_error; + } + + if (type != TLSEXT_NAMETYPE_host_name + || CBS_len(&name) == 0 + || CBS_len(&name) > TLSEXT_MAXLEN_host_name + || CBS_contains_zero_byte(&name)) + { + return ssl_select_cert_error; + } + + /* + * Store requested server name and call the regular callback, but ignore + * its response, since it returns SSL_TLSEXT_ERRO_NOACK for both: errors + * and when no server matching requested server name was found (in which + * case the default server is used). + */ + + c = ngx_ssl_get_connection(client_hello->ssl); + + hostname.len = CBS_len(&name); + hostname.data = ngx_pnalloc(c->pool, hostname.len + 1); + if (hostname.data == NULL) { + return ssl_select_cert_error; + } + + ngx_memcpy(hostname.data, CBS_data(&name), hostname.len); + hostname.data[hostname.len] = '\0'; + + (void) ngx_http_ssl_servername(client_hello->ssl, 0, hostname.data); + + return ssl_select_cert_success; +} + +#elif defined(SSL_CLIENT_HELLO_CB) + +int +ngx_http_ssl_client_hello(ngx_ssl_conn_t *ssl_conn, int *al, void *arg) +{ + size_t len, remaining; + ngx_str_t hostname; + ngx_connection_t *c; + const unsigned char *p; + + /* Based on OpenSSL's client_hello_select_server_ctx(). */ + + if (SSL_client_hello_get0_ext(ssl_conn, TLSEXT_TYPE_server_name, + &p, &remaining) + == 0) + { + return SSL_CLIENT_HELLO_SUCCESS; + } + + if (remaining <= 2) { + return SSL_CLIENT_HELLO_ERROR; + } + + len = (*(p++) << 8); + len += *(p++); + + if (len + 2 != remaining) { + return SSL_CLIENT_HELLO_ERROR; + } + + remaining -= 2; + + if (remaining == 0) { + return SSL_CLIENT_HELLO_ERROR; + } + + if (*p++ != TLSEXT_NAMETYPE_host_name) { + return SSL_CLIENT_HELLO_ERROR; + } + + remaining--; + + if (remaining <= 2) { + return SSL_CLIENT_HELLO_ERROR; + } + + len = (*(p++) << 8); + len += *(p++); + + if (len + 2 != remaining) { + return SSL_CLIENT_HELLO_ERROR; + } + + if (len == 0 || len > TLSEXT_MAXLEN_host_name) { + return SSL_CLIENT_HELLO_ERROR; + } + + if (memchr(p, 0, len) != NULL) { + return SSL_CLIENT_HELLO_ERROR; + } + + /* + * Store requested server name and call the regular callback, but ignore + * its response, since it returns SSL_TLSEXT_ERRO_NOACK for both: errors + * and when no server matching requested server name was found (in which + * case the default server is used). + */ + + c = ngx_ssl_get_connection(ssl_conn); + + hostname.len = len; + hostname.data = ngx_pnalloc(c->pool, hostname.len + 1); + if (hostname.data == NULL) { + return SSL_CLIENT_HELLO_ERROR; + } + + ngx_memcpy(hostname.data, p, hostname.len); + hostname.data[hostname.len] = '\0'; + + (void) ngx_http_ssl_servername(ssl_conn, 0, hostname.data); + + return SSL_CLIENT_HELLO_SUCCESS; +} + +#endif + int ngx_http_ssl_servername(ngx_ssl_conn_t *ssl_conn, int *ad, void *arg) { @@ -846,6 +996,11 @@ ngx_http_core_loc_conf_t *clcf; ngx_http_core_srv_conf_t *cscf; +#if defined(OPENSSL_IS_BORINGSSL) || defined(SSL_CLIENT_HELLO_CB) + if (arg != NULL) { + servername = (const char *) arg; + } else +#endif servername = SSL_get_servername(ssl_conn, TLSEXT_NAMETYPE_host_name); if (servername == NULL) {