From 555c34b0448082175669f37b0d226ac8115316f1 Mon Sep 17 00:00:00 2001 From: Andy Green Date: Sun, 17 Mar 2019 10:03:22 +0800 Subject: [PATCH] openssl: reuse client SSL_CTX where possible If you have multiple vhosts with client contexts enabled, under OpenSSL each one brings in the system cert bundle. On libwebsockets.org, there are many vhosts and the waste adds up to about 9MB of heap. This patch makes a sha256 from the client context configuration, and if a suitable client context already exists on another vhost, bumps a refcount and reuses the client context. In the case client contexts are configured differently, a new one is created (and is available for reuse as well). --- lib/core-net/vhost.c | 3 +- lib/core-net/wsi.c | 2 +- lib/tls/openssl/openssl-client.c | 126 +++++++++++++++++++++++++++++-- lib/tls/openssl/private.h | 13 ++++ lib/tls/openssl/ssl.c | 31 +++++++- lib/tls/private-network.h | 2 + 6 files changed, 166 insertions(+), 11 deletions(-) diff --git a/lib/core-net/vhost.c b/lib/core-net/vhost.c index 7d485f4f1..37934e75a 100644 --- a/lib/core-net/vhost.c +++ b/lib/core-net/vhost.c @@ -978,7 +978,8 @@ __lws_vhost_destroy2(struct lws_vhost *vh) n = 0; while (n < vh->count_protocols) { wsi.protocol = protocol; - protocol->callback(&wsi, LWS_CALLBACK_PROTOCOL_DESTROY, + if (protocol->callback) + protocol->callback(&wsi, LWS_CALLBACK_PROTOCOL_DESTROY, NULL, NULL, 0); protocol++; n++; diff --git a/lib/core-net/wsi.c b/lib/core-net/wsi.c index 75eb8d951..db97bb664 100644 --- a/lib/core-net/wsi.c +++ b/lib/core-net/wsi.c @@ -109,7 +109,7 @@ lws_vhost_name_to_protocol(struct lws_vhost *vh, const char *name) int n; for (n = 0; n < vh->count_protocols; n++) - if (!strcmp(name, vh->protocols[n].name)) + if (vh->protocols[n].name && !strcmp(name, vh->protocols[n].name)) return &vh->protocols[n]; return NULL; diff --git a/lib/tls/openssl/openssl-client.c b/lib/tls/openssl/openssl-client.c index 299c58ff4..74335e77c 100644 --- a/lib/tls/openssl/openssl-client.c +++ b/lib/tls/openssl/openssl-client.c @@ -21,6 +21,8 @@ #include "core/private.h" +#include "tls/openssl/private.h" + /* * Care: many openssl apis return 1 for success. These are translated to the * lws convention of 0 for success. @@ -364,12 +366,17 @@ lws_tls_client_create_vhost_context(struct lws_vhost *vh, unsigned int cert_mem_len, const char *private_key_filepath) { - SSL_METHOD *method; - unsigned long error; - int n; + struct lws_tls_client_reuse *tcr; const unsigned char **ca_mem_ptr; - X509 *client_CA; X509_STORE *x509_store; + unsigned long error; + SSL_METHOD *method; + EVP_MD_CTX *mdctx; + unsigned int len; + uint8_t hash[32]; + X509 *client_CA; + char c; + int n; /* basic openssl init already happened in context init */ @@ -389,7 +396,95 @@ lws_tls_client_create_vhost_context(struct lws_vhost *vh, (char *)vh->context->pt[0].serv_buf)); return 1; } - /* create context */ + + /* + * OpenSSL client contexts are quite expensive, because they bring in + * the system certificate bundle for each one. So if you have multiple + * vhosts, each with a client context, it can add up to several + * megabytes of heap. In the case the client contexts are configured + * identically, they could perfectly well have shared just the one. + * + * For that reason, use a hash to fingerprint the context configuration + * and prefer to reuse an existing one with the same fingerprint if + * possible. + */ + + mdctx = EVP_MD_CTX_create(); + if (!mdctx) + return 1; + + if (EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL) != 1) { + EVP_MD_CTX_destroy(mdctx); + + return 1; + } + + if (info->ssl_client_options_set) + EVP_DigestUpdate(mdctx, &info->ssl_client_options_set, + sizeof(info->ssl_client_options_set)); + +#if (OPENSSL_VERSION_NUMBER >= 0x009080df) && !defined(USE_WOLFSSL) + if (info->ssl_client_options_clear) + EVP_DigestUpdate(mdctx, &info->ssl_client_options_clear, + sizeof(info->ssl_client_options_clear)); +#endif + + if (cipher_list) + EVP_DigestUpdate(mdctx, cipher_list, strlen(cipher_list)); + +#if defined(LWS_HAVE_SSL_CTX_set_ciphersuites) + if (info->client_tls_1_3_plus_cipher_list) + EVP_DigestUpdate(mdctx, info->client_tls_1_3_plus_cipher_list, + strlen(info->client_tls_1_3_plus_cipher_list)); +#endif + + if (!lws_check_opt(vh->options, LWS_SERVER_OPTION_DISABLE_OS_CA_CERTS)) { + c = 1; + EVP_DigestUpdate(mdctx, &c, 1); + } + + if (ca_filepath) + EVP_DigestUpdate(mdctx, ca_filepath, strlen(ca_filepath)); + + if (cert_filepath) + EVP_DigestUpdate(mdctx, cert_filepath, strlen(cert_filepath)); + + if (private_key_filepath) + EVP_DigestUpdate(mdctx, private_key_filepath, + strlen(private_key_filepath)); + if (ca_mem && ca_mem_len) + EVP_DigestUpdate(mdctx, ca_mem, ca_mem_len); + + if (cert_mem && cert_mem_len) + EVP_DigestUpdate(mdctx, cert_mem, cert_mem_len); + + len = sizeof(hash); + EVP_DigestFinal_ex(mdctx, hash, &len); + EVP_MD_CTX_destroy(mdctx); + + /* look for existing client context with same config already */ + + lws_start_foreach_dll_safe(struct lws_dll *, p, tp, + vh->context->tls.cc_head.next) { + tcr = lws_container_of(p, struct lws_tls_client_reuse, cc_list); + + if (!memcmp(hash, tcr->hash, len)) { + + /* it's a match */ + + tcr->refcount++; + vh->tls.ssl_client_ctx = tcr->ssl_client_ctx; + + lwsl_info("%s: vh %s: reusing client ctx %d: use %d\n", + __func__, vh->name, tcr->index, + tcr->refcount); + + return 0; + } + } lws_end_foreach_dll_safe(p, tp); + + /* no existing one the same... create new client SSL_CTX */ + vh->tls.ssl_client_ctx = SSL_CTX_new(method); if (!vh->tls.ssl_client_ctx) { error = ERR_get_error(); @@ -399,6 +494,27 @@ lws_tls_client_create_vhost_context(struct lws_vhost *vh, return 1; } + tcr = lws_zalloc(sizeof(*tcr), "client ctx tcr"); + if (!tcr) { + SSL_CTX_free(vh->tls.ssl_client_ctx); + return 1; + } + + tcr->ssl_client_ctx = vh->tls.ssl_client_ctx; + tcr->refcount = 1; + memcpy(tcr->hash, hash, len); + tcr->index = vh->context->tls.count_client_contexts++; + lws_dll_add_front(&tcr->cc_list, &vh->context->tls.cc_head); + + lwsl_info("%s: vh %s: created new client ctx %d\n", __func__, + vh->name, tcr->index); + + /* bind the tcr to the client context */ + + SSL_CTX_set_ex_data(vh->tls.ssl_client_ctx, + openssl_SSL_CTX_private_data_index, + (char *)tcr); + #ifdef SSL_OP_NO_COMPRESSION SSL_CTX_set_options(vh->tls.ssl_client_ctx, SSL_OP_NO_COMPRESSION); #endif diff --git a/lib/tls/openssl/private.h b/lib/tls/openssl/private.h index 6a89d51fd..028a4b290 100644 --- a/lib/tls/openssl/private.h +++ b/lib/tls/openssl/private.h @@ -21,6 +21,19 @@ * gencrypto openssl-specific helper declarations */ +/* + * one of these per different client context + * cc_head is in lws_context.lws_context_tls + */ + +struct lws_tls_client_reuse { + lws_tls_ctx *ssl_client_ctx; + uint8_t hash[32]; + struct lws_dll cc_list; + int refcount; + int index; +}; + typedef int (*next_proto_cb)(SSL *, const unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned int inlen, void *arg); diff --git a/lib/tls/openssl/ssl.c b/lib/tls/openssl/ssl.c index 462d9484b..e23e185e9 100644 --- a/lib/tls/openssl/ssl.c +++ b/lib/tls/openssl/ssl.c @@ -90,6 +90,29 @@ lws_ssl_bind_passphrase(SSL_CTX *ssl_ctx, lws_context_init_ssl_pem_passwd_cb); } +static void +lws_ssl_destroy_client_ctx(struct lws_vhost *vhost) +{ + struct lws_tls_client_reuse *tcr; + + if (vhost->tls.user_supplied_ssl_ctx || !vhost->tls.ssl_client_ctx) + return; + + tcr = SSL_CTX_get_ex_data(vhost->tls.ssl_client_ctx, + openssl_SSL_CTX_private_data_index); + + if (--tcr->refcount) + return; + + SSL_CTX_free(vhost->tls.ssl_client_ctx); + vhost->tls.ssl_client_ctx = NULL; + + vhost->context->tls.count_client_contexts--; + + lws_dll_remove(&tcr->cc_list); + lws_free(tcr); +} + LWS_VISIBLE void lws_ssl_destroy(struct lws_vhost *vhost) { @@ -99,8 +122,8 @@ lws_ssl_destroy(struct lws_vhost *vhost) if (vhost->tls.ssl_ctx) SSL_CTX_free(vhost->tls.ssl_ctx); - if (!vhost->tls.user_supplied_ssl_ctx && vhost->tls.ssl_client_ctx) - SSL_CTX_free(vhost->tls.ssl_client_ctx); + + lws_ssl_destroy_client_ctx(vhost); // after 1.1.0 no need #if (OPENSSL_VERSION_NUMBER < 0x10100000) @@ -378,8 +401,8 @@ lws_ssl_SSL_CTX_destroy(struct lws_vhost *vhost) if (vhost->tls.ssl_ctx) SSL_CTX_free(vhost->tls.ssl_ctx); - if (!vhost->tls.user_supplied_ssl_ctx && vhost->tls.ssl_client_ctx) - SSL_CTX_free(vhost->tls.ssl_client_ctx); + lws_ssl_destroy_client_ctx(vhost); + #if defined(LWS_WITH_ACME) lws_tls_acme_sni_cert_destroy(vhost); #endif diff --git a/lib/tls/private-network.h b/lib/tls/private-network.h index 2627d4728..e05f659d3 100644 --- a/lib/tls/private-network.h +++ b/lib/tls/private-network.h @@ -31,6 +31,8 @@ struct lws_context_tls { char alpn_discovered[32]; const char *alpn_default; time_t last_cert_check_s; + struct lws_dll cc_head; + int count_client_contexts; }; struct lws_pt_tls {