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) {