From e26118af2e572431541fca58d6c372ac5a3cfdf6 Mon Sep 17 00:00:00 2001 From: Sergey Kandaurov Date: Tue, 15 Jul 2025 15:55:26 +0400 Subject: [PATCH 1/3] SSL: disabled certificate compression by default with OpenSSL. Certificate compression is supported since OpenSSL 3.2, it is enabled automatically as negotiated in a TLSv1.3 handshake. Using certificate compression and decompression in runtime may be suboptimal in terms of CPU and memory consumption in certain typical scenarios, hence it is disabled by default on both server and client sides. It can be enabled with ssl_conf_command and similar directives in upstream as appropriate, for example: ssl_conf_command Options RxCertificateCompression; ssl_conf_command Options TxCertificateCompression; Compressing server certificates requires additional support, this is addressed separately. --- src/event/ngx_event_openssl.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index ff604c562..0c23c3f2f 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -387,6 +387,11 @@ ngx_ssl_create(ngx_ssl_t *ssl, ngx_uint_t protocols, void *data) SSL_CTX_set_options(ssl->ctx, SSL_OP_NO_COMPRESSION); #endif +#ifdef SSL_OP_NO_TX_CERTIFICATE_COMPRESSION + SSL_CTX_set_options(ssl->ctx, SSL_OP_NO_TX_CERTIFICATE_COMPRESSION); + SSL_CTX_set_options(ssl->ctx, SSL_OP_NO_RX_CERTIFICATE_COMPRESSION); +#endif + #ifdef SSL_OP_NO_ANTI_REPLAY SSL_CTX_set_options(ssl->ctx, SSL_OP_NO_ANTI_REPLAY); #endif From 0e29e080052fc0565f09c90365d784b58f5a2755 Mon Sep 17 00:00:00 2001 From: Sergey Kandaurov Date: Wed, 9 Jul 2025 19:02:09 +0400 Subject: [PATCH 2/3] SSL: support for compressed server certificates with OpenSSL. The ssl_certificate_compression directive allows to send compressed server certificates, which are pre-compressed on on nginx startup. To simplify configuration, the SSL_OP_NO_TX_CERTIFICATE_COMPRESSION option is automatically cleared if certificates were pre-compressed. SSL_CTX_compress_certs() may return an error in legitimate cases, e.g., when none of compression algorithms is available or if the resulting compressed size is larger than the original one, thus it is silently ignored. Certificate compression is supported in Chrome with brotli only, in Safari with zlib only, and in Firefox with all listed algorithms. It is supported since Ubuntu 24.10, which has OpenSSL with enabled zlib and zstd support. The actual list of algorithms supported in OpenSSL depends on how the library was configured; it can be brotli, zlib, zstd as listed in RFC 8879. --- src/event/ngx_event_openssl.c | 25 +++++++++++++++++++++++++ src/event/ngx_event_openssl.h | 2 ++ src/http/modules/ngx_http_ssl_module.c | 18 ++++++++++++++++++ src/http/modules/ngx_http_ssl_module.h | 1 + src/mail/ngx_mail_ssl_module.c | 18 ++++++++++++++++++ src/mail/ngx_mail_ssl_module.h | 1 + src/stream/ngx_stream_ssl_module.c | 18 ++++++++++++++++++ src/stream/ngx_stream_ssl_module.h | 1 + 8 files changed, 84 insertions(+) diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index 0c23c3f2f..feeb32dad 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -664,6 +664,31 @@ retry: } +ngx_int_t +ngx_ssl_certificate_compression(ngx_conf_t *cf, ngx_ssl_t *ssl, + ngx_uint_t enable) +{ + if (!enable) { + return NGX_OK; + } + +#ifdef SSL_OP_NO_RX_CERTIFICATE_COMPRESSION + + if (SSL_CTX_compress_certs(ssl->ctx, 0) != 0) { + SSL_CTX_clear_options(ssl->ctx, SSL_OP_NO_TX_CERTIFICATE_COMPRESSION); + } + +#else + + ngx_log_error(NGX_LOG_WARN, ssl->log, 0, + "\"ssl_certificate_compression\" ignored, not supported"); + +#endif + + return NGX_OK; +} + + ngx_int_t ngx_ssl_ciphers(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *ciphers, ngx_uint_t prefer_server_ciphers) diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h index 0c9e9e840..e7ccd51e8 100644 --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -236,6 +236,8 @@ ngx_int_t ngx_ssl_certificate(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_int_t ngx_ssl_connection_certificate(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *cert, ngx_str_t *key, ngx_ssl_cache_t *cache, ngx_array_t *passwords); +ngx_int_t ngx_ssl_certificate_compression(ngx_conf_t *cf, ngx_ssl_t *ssl, + ngx_uint_t enable); ngx_int_t ngx_ssl_ciphers(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *ciphers, ngx_uint_t prefer_server_ciphers); diff --git a/src/http/modules/ngx_http_ssl_module.c b/src/http/modules/ngx_http_ssl_module.c index dbfe5c08b..fbf4ab871 100644 --- a/src/http/modules/ngx_http_ssl_module.c +++ b/src/http/modules/ngx_http_ssl_module.c @@ -124,6 +124,13 @@ static ngx_command_t ngx_http_ssl_commands[] = { 0, NULL }, + { ngx_string("ssl_certificate_compression"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_SRV_CONF_OFFSET, + offsetof(ngx_http_ssl_srv_conf_t, certificate_compression), + NULL }, + { ngx_string("ssl_dhparam"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, @@ -621,6 +628,7 @@ ngx_http_ssl_create_srv_conf(ngx_conf_t *cf) */ sscf->prefer_server_ciphers = NGX_CONF_UNSET; + sscf->certificate_compression = NGX_CONF_UNSET; sscf->early_data = NGX_CONF_UNSET; sscf->reject_handshake = NGX_CONF_UNSET; sscf->buffer_size = NGX_CONF_UNSET_SIZE; @@ -658,6 +666,9 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_value(conf->prefer_server_ciphers, prev->prefer_server_ciphers, 0); + ngx_conf_merge_value(conf->certificate_compression, + prev->certificate_compression, 0); + ngx_conf_merge_value(conf->early_data, prev->early_data, 0); ngx_conf_merge_value(conf->reject_handshake, prev->reject_handshake, 0); @@ -792,6 +803,13 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) { return NGX_CONF_ERROR; } + + if (ngx_ssl_certificate_compression(cf, &conf->ssl, + conf->certificate_compression) + != NGX_OK) + { + return NGX_CONF_ERROR; + } } conf->ssl.buffer_size = conf->buffer_size; diff --git a/src/http/modules/ngx_http_ssl_module.h b/src/http/modules/ngx_http_ssl_module.h index 8650fab93..9b26529fa 100644 --- a/src/http/modules/ngx_http_ssl_module.h +++ b/src/http/modules/ngx_http_ssl_module.h @@ -18,6 +18,7 @@ typedef struct { ngx_ssl_t ssl; ngx_flag_t prefer_server_ciphers; + ngx_flag_t certificate_compression; ngx_flag_t early_data; ngx_flag_t reject_handshake; diff --git a/src/mail/ngx_mail_ssl_module.c b/src/mail/ngx_mail_ssl_module.c index 176e9c624..079d0e773 100644 --- a/src/mail/ngx_mail_ssl_module.c +++ b/src/mail/ngx_mail_ssl_module.c @@ -97,6 +97,13 @@ static ngx_command_t ngx_mail_ssl_commands[] = { 0, NULL }, + { ngx_string("ssl_certificate_compression"), + NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_MAIL_SRV_CONF_OFFSET, + offsetof(ngx_mail_ssl_conf_t, certificate_compression), + NULL }, + { ngx_string("ssl_dhparam"), NGX_MAIL_MAIN_CONF|NGX_MAIL_SRV_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, @@ -314,6 +321,7 @@ ngx_mail_ssl_create_conf(ngx_conf_t *cf) scf->passwords = NGX_CONF_UNSET_PTR; scf->conf_commands = NGX_CONF_UNSET_PTR; scf->prefer_server_ciphers = NGX_CONF_UNSET; + scf->certificate_compression = NGX_CONF_UNSET; scf->verify = NGX_CONF_UNSET_UINT; scf->verify_depth = NGX_CONF_UNSET_UINT; scf->builtin_session_cache = NGX_CONF_UNSET; @@ -343,6 +351,9 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_value(conf->prefer_server_ciphers, prev->prefer_server_ciphers, 0); + ngx_conf_merge_value(conf->certificate_compression, + prev->certificate_compression, 0); + ngx_conf_merge_bitmask_value(conf->protocols, prev->protocols, (NGX_CONF_BITMASK_SET|NGX_SSL_DEFAULT_PROTOCOLS)); @@ -446,6 +457,13 @@ ngx_mail_ssl_merge_conf(ngx_conf_t *cf, void *parent, void *child) return NGX_CONF_ERROR; } + if (ngx_ssl_certificate_compression(cf, &conf->ssl, + conf->certificate_compression) + != NGX_OK) + { + return NGX_CONF_ERROR; + } + if (conf->verify) { if (conf->verify != 3 diff --git a/src/mail/ngx_mail_ssl_module.h b/src/mail/ngx_mail_ssl_module.h index c0eb6a38f..a0e9a173a 100644 --- a/src/mail/ngx_mail_ssl_module.h +++ b/src/mail/ngx_mail_ssl_module.h @@ -21,6 +21,7 @@ typedef struct { ngx_flag_t prefer_server_ciphers; + ngx_flag_t certificate_compression; ngx_ssl_t ssl; diff --git a/src/stream/ngx_stream_ssl_module.c b/src/stream/ngx_stream_ssl_module.c index 2f1b99624..7ce1175f1 100644 --- a/src/stream/ngx_stream_ssl_module.c +++ b/src/stream/ngx_stream_ssl_module.c @@ -133,6 +133,13 @@ static ngx_command_t ngx_stream_ssl_commands[] = { 0, NULL }, + { ngx_string("ssl_certificate_compression"), + NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_STREAM_SRV_CONF_OFFSET, + offsetof(ngx_stream_ssl_srv_conf_t, certificate_compression), + NULL }, + { ngx_string("ssl_dhparam"), NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, @@ -881,6 +888,7 @@ ngx_stream_ssl_create_srv_conf(ngx_conf_t *cf) sscf->passwords = NGX_CONF_UNSET_PTR; sscf->conf_commands = NGX_CONF_UNSET_PTR; sscf->prefer_server_ciphers = NGX_CONF_UNSET; + sscf->certificate_compression = NGX_CONF_UNSET; sscf->reject_handshake = NGX_CONF_UNSET; sscf->verify = NGX_CONF_UNSET_UINT; sscf->verify_depth = NGX_CONF_UNSET_UINT; @@ -914,6 +922,9 @@ ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_value(conf->prefer_server_ciphers, prev->prefer_server_ciphers, 0); + ngx_conf_merge_value(conf->certificate_compression, + prev->certificate_compression, 0); + ngx_conf_merge_value(conf->reject_handshake, prev->reject_handshake, 0); ngx_conf_merge_bitmask_value(conf->protocols, prev->protocols, @@ -1039,6 +1050,13 @@ ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) { return NGX_CONF_ERROR; } + + if (ngx_ssl_certificate_compression(cf, &conf->ssl, + conf->certificate_compression) + != NGX_OK) + { + return NGX_CONF_ERROR; + } } if (conf->verify) { diff --git a/src/stream/ngx_stream_ssl_module.h b/src/stream/ngx_stream_ssl_module.h index ffa03a6f3..31f138cfd 100644 --- a/src/stream/ngx_stream_ssl_module.h +++ b/src/stream/ngx_stream_ssl_module.h @@ -18,6 +18,7 @@ typedef struct { ngx_msec_t handshake_timeout; ngx_flag_t prefer_server_ciphers; + ngx_flag_t certificate_compression; ngx_flag_t reject_handshake; ngx_ssl_t ssl; From 998bb968974e44e69003f45217e7b1e3361b65c0 Mon Sep 17 00:00:00 2001 From: Sergey Kandaurov Date: Tue, 15 Jul 2025 22:22:53 +0400 Subject: [PATCH 3/3] SSL: support for compressed server certificates with BoringSSL. BoringSSL provides two callbacks to register on SSL context for each compression algorithm, which is used to send and receive compressed server certificates, as appropriate. This change adds support for compression with zlib, enabled with the ssl_certificate_compression directive. Compressed certificates are cached in SSL context on the first callback invocation. --- src/event/ngx_event_openssl.c | 167 ++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index feeb32dad..890b72542 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -9,6 +9,10 @@ #include #include +#if (NGX_ZLIB && defined TLSEXT_cert_compression_zlib) +#include +#endif + #define NGX_SSL_PASSWORD_BUFFER_SIZE 4096 @@ -19,6 +23,13 @@ typedef struct { static ngx_inline ngx_int_t ngx_ssl_cert_already_in_hash(void); +#if (NGX_ZLIB && defined TLSEXT_cert_compression_zlib) +static int ngx_ssl_cert_compression_callback(ngx_ssl_conn_t *ssl_conn, + CBB *out, const uint8_t *in, size_t in_len); +static void *ngx_ssl_cert_compression_alloc(void *opaque, u_int items, + u_int size); +static void ngx_ssl_cert_compression_free(void *opaque, void *address); +#endif static int ngx_ssl_verify_callback(int ok, X509_STORE_CTX *x509_store); static void ngx_ssl_info_callback(const ngx_ssl_conn_t *ssl_conn, int where, int ret); @@ -128,6 +139,7 @@ int ngx_ssl_ticket_keys_index; int ngx_ssl_ocsp_index; int ngx_ssl_index; int ngx_ssl_certificate_name_index; +int ngx_ssl_certificate_comp_index; u_char ngx_ssl_session_buffer[NGX_SSL_MAX_SESSION_SIZE]; @@ -270,6 +282,14 @@ ngx_ssl_init(ngx_log_t *log) return NGX_ERROR; } + ngx_ssl_certificate_comp_index = SSL_CTX_get_ex_new_index(0, NULL, NULL, + NULL, NULL); + if (ngx_ssl_certificate_comp_index == -1) { + ngx_ssl_error(NGX_LOG_ALERT, log, 0, + "SSL_CTX_get_ex_new_index() failed"); + return NGX_ERROR; + } + return NGX_OK; } @@ -296,6 +316,14 @@ ngx_ssl_create(ngx_ssl_t *ssl, ngx_uint_t protocols, void *data) return NGX_ERROR; } + if (SSL_CTX_set_ex_data(ssl->ctx, ngx_ssl_certificate_comp_index, NULL) + == 0) + { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "SSL_CTX_set_ex_data() failed"); + return NGX_ERROR; + } + ngx_rbtree_init(&ssl->staple_rbtree, &ssl->staple_sentinel, ngx_rbtree_insert_value); @@ -678,6 +706,15 @@ ngx_ssl_certificate_compression(ngx_conf_t *cf, ngx_ssl_t *ssl, SSL_CTX_clear_options(ssl->ctx, SSL_OP_NO_TX_CERTIFICATE_COMPRESSION); } +#elif (NGX_ZLIB && defined TLSEXT_cert_compression_zlib) + + if (SSL_CTX_add_cert_compression_alg(ssl->ctx, TLSEXT_cert_compression_zlib, + ngx_ssl_cert_compression_callback, + NULL)) + { + return NGX_OK; + } + #else ngx_log_error(NGX_LOG_WARN, ssl->log, 0, @@ -689,6 +726,129 @@ ngx_ssl_certificate_compression(ngx_conf_t *cf, ngx_ssl_t *ssl, } +#if (NGX_ZLIB && defined TLSEXT_cert_compression_zlib) + +static int +ngx_ssl_cert_compression_callback(ngx_ssl_conn_t *ssl_conn, CBB *out, + const uint8_t *in, size_t in_len) +{ + int rc; + u_char *p; + SSL_CTX *ssl_ctx; + z_stream zstream; + ngx_str_t *comp, tmp; + ngx_pool_t *pool; + ngx_connection_t *c; + + ssl_ctx = SSL_get_SSL_CTX(ssl_conn); + comp = SSL_CTX_get_ex_data(ssl_ctx, ngx_ssl_certificate_comp_index); + + if (comp != NULL) { + return CBB_add_bytes(out, comp->data, comp->len); + } + + c = ngx_ssl_get_connection(ssl_conn); + + pool = ngx_create_pool(256, c->log); + if (pool == NULL) { + return 0; + } + + pool->log = c->log; + + ngx_memzero(&zstream, sizeof(z_stream)); + + zstream.zalloc = ngx_ssl_cert_compression_alloc; + zstream.zfree = ngx_ssl_cert_compression_free; + zstream.opaque = pool; + + rc = 0; + + tmp.len = compressBound(in_len); + tmp.data = ngx_palloc(pool, tmp.len); + if (tmp.data == NULL) { + goto done; + } + + zstream.next_in = (u_char *) in; + zstream.avail_in = in_len; + zstream.next_out = tmp.data; + zstream.avail_out = tmp.len; + + rc = deflateInit(&zstream, Z_DEFAULT_COMPRESSION); + + if (rc != Z_OK) { + ngx_log_error(NGX_LOG_ALERT, c->log, 0, "deflateInit() failed: %d", rc); + goto done; + } + + rc = deflate(&zstream, Z_FINISH); + + if (rc != Z_STREAM_END) { + ngx_log_error(NGX_LOG_ALERT, c->log, 0, + "deflate(Z_FINISH) failed: %d", rc); + goto done; + } + + tmp.len -= zstream.avail_out; + + rc = deflateEnd(&zstream); + + if (rc != Z_OK) { + ngx_log_error(NGX_LOG_ALERT, c->log, 0, "deflateEnd() failed: %d", rc); + goto done; + } + + p = ngx_alloc(sizeof(ngx_str_t) + tmp.len, c->log); + if (p == NULL) { + goto done; + } + + comp = (ngx_str_t *) p; + + comp->len = tmp.len; + comp->data = p + sizeof(ngx_str_t); + + ngx_memcpy(comp->data, tmp.data, tmp.len); + + SSL_CTX_set_ex_data(ssl_ctx, ngx_ssl_certificate_comp_index, comp); + + rc = CBB_add_bytes(out, comp->data, comp->len); + +done: + + ngx_destroy_pool(pool); + + return rc; +} + + +static void * +ngx_ssl_cert_compression_alloc(void *opaque, u_int items, u_int size) +{ + ngx_pool_t *pool = opaque; + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pool->log, 0, + "cert compression alloc: n:%ud s:%ud", items, size); + + return ngx_palloc(pool, items * size); +} + + +static void +ngx_ssl_cert_compression_free(void *opaque, void *address) +{ +#if 0 + ngx_pool_t *pool = opaque; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, pool->log, 0, + "cert compression free: %p", address); +#endif +} + +#endif + + ngx_int_t ngx_ssl_ciphers(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *ciphers, ngx_uint_t prefer_server_ciphers) @@ -4787,6 +4947,7 @@ ngx_ssl_cleanup_ctx(void *data) ngx_ssl_t *ssl = data; X509 *cert; + ngx_str_t *comp; ngx_uint_t i; for (i = 0; i < ssl->certs.nelts; i++) { @@ -4794,6 +4955,12 @@ ngx_ssl_cleanup_ctx(void *data) X509_free(cert); } + comp = SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_certificate_comp_index); + + if (comp != NULL) { + ngx_free(comp); + } + SSL_CTX_free(ssl->ctx); }