From mdounin at mdounin.ru Sat Mar 8 01:49:40 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sat, 08 Mar 2025 04:49:40 +0300 Subject: [PATCH 1 of 2] Tests: fixed LibreSSL TODOs in h3_ssl_early_data.t Message-ID: # HG changeset patch # User Maxim Dounin # Date 1741398260 -10800 # Sat Mar 08 04:44:20 2025 +0300 # Node ID a84cf984d25e61f759ebabe5f7fabb79d3653ac2 # Parent 570d0c2667b8bce8d76e9f58ddc2515b36d6b0de Tests: fixed LibreSSL TODOs in h3_ssl_early_data.t. Previously, the last test wasn't properly excluded from the TODO scope and was reported as an unexpected success. diff --git a/h3_ssl_early_data.t b/h3_ssl_early_data.t --- a/h3_ssl_early_data.t +++ b/h3_ssl_early_data.t @@ -80,13 +80,13 @@ my $frames = $s->read(all => [{ sid => $ my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-session'}, '.', 'new session'); -local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL'); - my $psk_list = $s->{psk_list}; $s = Test::Nginx::HTTP3->new(8980, psk_list => $psk_list, early_data => {}); TODO: { +local $TODO = 'no TLSv1.3 sessions in LibreSSL' + if $t->has_module('LibreSSL'); local $TODO = 'no 0-RTT in OpenSSL compat layer' unless $t->has_module('OpenSSL [.0-9]+\+quic') or $t->has_module('BoringSSL') @@ -99,9 +99,16 @@ is($frame->{headers}->{'x-early'}, '1', } +TODO: { +local $TODO = 'no TLSv1.3 sessions in LibreSSL' + if $t->has_module('LibreSSL'); + $frames = $s->read(all => [{ sid => $s->new_stream(), fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-session'}, 'r', 'reused session 1rtt'); + +} + is($frame->{headers}->{'x-early'}, undef, 'reused session not early'); ############################################################################### From mdounin at mdounin.ru Sat Mar 8 01:49:41 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sat, 08 Mar 2025 04:49:41 +0300 Subject: [PATCH 2 of 2] Tests: adjusted TODOs for LibreSSL 4.0.0 In-Reply-To: References: Message-ID: <00307a7f3cadcc3a1eb4.1741398581@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1741398262 -10800 # Sat Mar 08 04:44:22 2025 +0300 # Node ID 00307a7f3cadcc3a1eb4f9446e779e8a093657c4 # Parent a84cf984d25e61f759ebabe5f7fabb79d3653ac2 Tests: adjusted TODOs for LibreSSL 4.0.0. Issue with signature algorithms in TLSv1.3 is fixed in LibreSSL 4.0.0 (https://github.com/libressl/portable/issues/1058), ssl_certificates.t and ssl_stapling.t tests adjusted accordingly. Note thought that LibreSSL also fails to provide correct certificate information when OCSP stapling is used with TLSv1.3 and multiple certificates (https://github.com/libressl/portable/issues/1059), so some tests in ssl_stapling.t are still failing even with the fix. Additionally, sending alerts in QUIC is also fixed in LibreSSL 4.0.0, as seen in the h3_ssl_reject_handshake.t test. diff --git a/h3_ssl_reject_handshake.t b/h3_ssl_reject_handshake.t --- a/h3_ssl_reject_handshake.t +++ b/h3_ssl_reject_handshake.t @@ -114,7 +114,9 @@ skip "OpenSSL too old", 3 if $got && $go # default virtual server rejected TODO: { -local $TODO = 'broken send_alert in LibreSSL' if $t->has_module('LibreSSL'); +local $TODO = 'broken send_alert in LibreSSL' + if $t->has_module('LibreSSL') + and not $t->has_feature('libressl:4.0.0'); is(bad('default', 8980), $alert, 'default rejected'); is(bad(undef, 8980), $alert, 'absent sni rejected'); @@ -132,7 +134,9 @@ like(get(undef, 8982), qr/200/, 'absent like(get('virtual1', 8982), qr/virtual1/, 'virtual 1 accepted'); TODO: { -local $TODO = 'broken send_alert in LibreSSL' if $t->has_module('LibreSSL'); +local $TODO = 'broken send_alert in LibreSSL' + if $t->has_module('LibreSSL') + and not $t->has_feature('libressl:4.0.0'); is(bad('virtual2', 8982), $alert, 'virtual 2 rejected'); diff --git a/ssl_certificates.t b/ssl_certificates.t --- a/ssl_certificates.t +++ b/ssl_certificates.t @@ -96,7 +96,9 @@ foreach my $name ('ec', 'rsa') { TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' - if $t->has_module('LibreSSL') && test_tls13(); + if $t->has_module('LibreSSL') + && !$t->has_feature('libressl:4.0.0') + && test_tls13(); like(cert('RSA'), qr/CN=rsa/, 'ssl cert RSA'); diff --git a/ssl_stapling.t b/ssl_stapling.t --- a/ssl_stapling.t +++ b/ssl_stapling.t @@ -298,6 +298,7 @@ ok(!staple(8449, 'ECDSA'), 'ocsp error') TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' if $t->has_module('LibreSSL') + && !$t->has_feature('libressl:4.0.0') && !Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); From mdounin at mdounin.ru Sun Mar 9 00:27:39 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 09 Mar 2025 03:27:39 +0300 Subject: [PATCH 0 of 3] SSL certificate verification context checks Message-ID: Hello! The following patch series somewhat improves checking of the SSL context where the client certificate was verified. Notably: - Session ID context now includes trusted certificates list, so a session cannot be restored in a server block with different trusted certificates list (even if it uses the same server certificate and the same client CA certificates list). - Reshaped HTTP level server name checks to better match existing certificate verification checking code. As a side effect, now it is now allowed to use different names at HTTP level as long as verification context stays the same (that is, names within the same server block). - Added a workaround to prevent incorrect session reuse in OpenSSL 1.1.1e and above when using TLSv1.3, which allows session reuse with different server names without any checks. -- Maxim Dounin From mdounin at mdounin.ru Sun Mar 9 00:27:40 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 09 Mar 2025 03:27:40 +0300 Subject: [PATCH 1 of 3] SSL: added trusted certificates into session id context In-Reply-To: References: Message-ID: <48cff1a93a0e8ed50699.1741480060@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1741442050 -10800 # Sat Mar 08 16:54:10 2025 +0300 # Node ID 48cff1a93a0e8ed50699e9201a805d5e14aab84d # Parent 1996ea0bc55d2bc950297f9d7990a5b07948f5e2 SSL: added trusted certificates into session id context. This ensures that sessions cannot be incorrectly restored between servers with identical certificates and client certificates, but with different trusted certificates. Note that with BoringSSL this applies to SNI-based virtual servers, since with BoringSSL session resumption happens after the TLS servername callback, and session id context of the selected virtual server is used to decide whether the session can be resumed or not. diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -131,6 +131,7 @@ int ngx_ssl_server_conf_index; int ngx_ssl_session_cache_index; int ngx_ssl_ticket_keys_index; int ngx_ssl_ocsp_index; +int ngx_ssl_trusted_list_index; int ngx_ssl_certificate_index; int ngx_ssl_next_certificate_index; int ngx_ssl_certificate_name_index; @@ -258,6 +259,14 @@ ngx_ssl_init(ngx_log_t *log) return NGX_ERROR; } + ngx_ssl_trusted_list_index = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, + NULL); + if (ngx_ssl_trusted_list_index == -1) { + ngx_ssl_error(NGX_LOG_ALERT, log, 0, + "SSL_CTX_get_ex_new_index() failed"); + return NGX_ERROR; + } + ngx_ssl_certificate_index = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); if (ngx_ssl_certificate_index == -1) { @@ -951,6 +960,8 @@ ngx_int_t ngx_ssl_trusted_certificate(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *cert, ngx_int_t depth) { + STACK_OF(X509_NAME) *list; + SSL_CTX_set_verify(ssl->ctx, SSL_CTX_get_verify_mode(ssl->ctx), ngx_ssl_verify_callback); @@ -980,6 +991,24 @@ ngx_ssl_trusted_certificate(ngx_conf_t * ERR_clear_error(); + if (SSL_CTX_get_verify_mode(ssl->ctx) != SSL_VERIFY_PEER) { + return NGX_OK; + } + + list = SSL_load_client_CA_file((char *) cert->data); + + if (list == NULL) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "SSL_load_client_CA_file(\"%s\") failed", cert->data); + return NGX_ERROR; + } + + if (SSL_CTX_set_ex_data(ssl->ctx, ngx_ssl_trusted_list_index, list) == 0) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "SSL_CTX_set_ex_data() failed"); + return NGX_ERROR; + } + return NGX_OK; } @@ -3857,6 +3886,28 @@ ngx_ssl_session_id_context(ngx_ssl_t *ss } } + list = SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_trusted_list_index); + + if (list != NULL) { + n = sk_X509_NAME_num(list); + + for (i = 0; i < n; i++) { + name = sk_X509_NAME_value(list, i); + + if (X509_NAME_digest(name, EVP_sha1(), buf, &len) == 0) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "X509_NAME_digest() failed"); + goto failed; + } + + if (EVP_DigestUpdate(md, buf, len) == 0) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "EVP_DigestUpdate() failed"); + goto failed; + } + } + } + if (EVP_DigestFinal_ex(md, buf, &len) == 0) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "EVP_DigestFinal_ex() failed"); @@ -4826,7 +4877,8 @@ ngx_ssl_cleanup_ctx(void *data) { ngx_ssl_t *ssl = data; - X509 *cert, *next; + X509 *cert, *next; + STACK_OF(X509_NAME) *list; cert = SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_certificate_index); @@ -4836,6 +4888,12 @@ ngx_ssl_cleanup_ctx(void *data) cert = next; } + list = SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_trusted_list_index); + + if (list) { + sk_X509_NAME_pop_free(list, X509_NAME_free); + } + SSL_CTX_free(ssl->ctx); } diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -332,6 +332,7 @@ extern int ngx_ssl_server_conf_index; extern int ngx_ssl_session_cache_index; extern int ngx_ssl_ticket_keys_index; extern int ngx_ssl_ocsp_index; +extern int ngx_ssl_trusted_list_index; extern int ngx_ssl_certificate_index; extern int ngx_ssl_next_certificate_index; extern int ngx_ssl_certificate_name_index; From mdounin at mdounin.ru Sun Mar 9 00:27:41 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 09 Mar 2025 03:27:41 +0300 Subject: [PATCH 2 of 3] SSL: reworked restriction on hosts other than negotiated In-Reply-To: References: Message-ID: <4dc9fb4dd95248df980a.1741480061@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1741455881 -10800 # Sat Mar 08 20:44:41 2025 +0300 # Node ID 4dc9fb4dd95248df980aefbf946b4f299dcae00f # Parent 48cff1a93a0e8ed50699e9201a805d5e14aab84d SSL: reworked restriction on hosts other than negotiated. Following 5095:4fbef397c753, attempts to request hosts other than negotiated are rejected with 421 (Misdirected Request) HTTP error as long as certificate verification is enabled in the requested server block. With this change, such requests are now rejected in ngx_http_process_request(), along with other certificate verification checks. diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -5068,6 +5068,47 @@ ngx_ssl_check_name(ngx_str_t *name, ASN1 ngx_int_t +ngx_ssl_check_verify_context(ngx_connection_t *c, ngx_ssl_t *ssl) +{ +#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME + + SSL_CTX *ctx; + const char *name; + + /* + * Check current SSL context to match the one where the certificate + * verification happened. + * + * If certificate verification was in the default server block without + * a server name, all contexts are allowed, as non-SNI clients are + * allowed to request other virtual servers. + * + * If there is no context in the current server block, verification + * is expected to happen in the default server. + */ + + ctx = SSL_get_SSL_CTX(c->ssl->connection); + name = SSL_get_servername(c->ssl->connection, TLSEXT_NAMETYPE_host_name); + + if (ctx == ssl->ctx) { + return NGX_OK; + } + + if (ctx == c->ssl->session_ctx && (ssl->ctx == NULL || name == NULL)) { + return NGX_OK; + } + + return NGX_ERROR; + +#else + + return NGX_OK; + +#endif +} + + +ngx_int_t ngx_ssl_get_protocol(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { s->data = (u_char *) SSL_get_version(c->ssl->connection); diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -259,6 +259,7 @@ ngx_ssl_session_t *ngx_ssl_get0_session( || n == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE) ngx_int_t ngx_ssl_check_host(ngx_connection_t *c, ngx_str_t *name); +ngx_int_t ngx_ssl_check_verify_context(ngx_connection_t *c, ngx_ssl_t *ssl); ngx_int_t ngx_ssl_get_protocol(ngx_connection_t *c, ngx_pool_t *pool, diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c --- a/src/http/ngx_http_request.c +++ b/src/http/ngx_http_request.c @@ -2073,6 +2073,16 @@ ngx_http_process_request(ngx_http_reques sscf = ngx_http_get_module_srv_conf(r, ngx_http_ssl_module); if (sscf->verify) { + + if (ngx_ssl_check_verify_context(c, &sscf->ssl) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "client SSL certificate was negotiated " + "with a different server name"); + + ngx_http_finalize_request(r, NGX_HTTP_MISDIRECTED_REQUEST); + return; + } + rc = SSL_get_verify_result(c->ssl->connection); if (rc != X509_V_OK @@ -2280,22 +2290,10 @@ ngx_http_set_virtual_server(ngx_http_req #if (NGX_HTTP_SSL && defined SSL_CTRL_SET_TLSEXT_HOSTNAME) if (hc->ssl_servername) { - ngx_http_ssl_srv_conf_t *sscf; - if (rc == NGX_DECLINED) { cscf = hc->addr_conf->default_server; rc = NGX_OK; } - - sscf = ngx_http_get_module_srv_conf(cscf->ctx, ngx_http_ssl_module); - - if (sscf->verify) { - ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, - "client attempted to request the server name " - "different from the one that was negotiated"); - ngx_http_finalize_request(r, NGX_HTTP_MISDIRECTED_REQUEST); - return NGX_ERROR; - } } #endif From mdounin at mdounin.ru Sun Mar 9 00:27:42 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 09 Mar 2025 03:27:42 +0300 Subject: [PATCH 3 of 3] SSL: added additional verify context check for OpenSSL In-Reply-To: References: Message-ID: <094e0ea330f5416750aa.1741480062@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1741457467 -10800 # Sat Mar 08 21:11:07 2025 +0300 # Node ID 094e0ea330f5416750aa663647f60462a0c4b0cf # Parent 4dc9fb4dd95248df980aefbf946b4f299dcae00f SSL: added additional verify context check for OpenSSL. When using TLSv1.3, OpenSSL 1.1.1e+ allows session resumption with names other than initially negotiated, and provides no documented way to prevent it or even detect. This makes it possible to resume the session with a certificate verified in the context of a different server block, similarly to 5095:4fbef397c753. There is no such problem in LibreSSL since it doesn't support session resumption with TLSv1.3 (and does not allow session resumption with different names with TLSv1.2 and below). Similarly, there is no such problem with BoringSSL. While BoringSSL allows sessions to be resumed with different names, it does so only after checking session id context set by the servername callback. With this change, SSL_get_servername() (which, for TLSv1.3, returns name as sent by the client in the current handshake) is checked against SSL_SESSION_get0_hostname(), and thus prevents incorrect use of certificates verified in different contexts. Note that SSL_SESSION_get0_hostname() is documented to return NULL with TLSv1.3, but in practice it returns the originally negotiated name at least up to OpenSSL 3.4.1, and the documentation is expected to be fixed. diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -5091,20 +5091,65 @@ ngx_ssl_check_verify_context(ngx_connect name = SSL_get_servername(c->ssl->connection, TLSEXT_NAMETYPE_host_name); if (ctx == ssl->ctx) { - return NGX_OK; + goto next; } if (ctx == c->ssl->session_ctx && (ssl->ctx == NULL || name == NULL)) { - return NGX_OK; + goto next; } return NGX_ERROR; -#else +next: + +#if (defined TLS1_3_VERSION \ + && !defined LIBRESSL_VERSION_NUMBER \ + && !defined OPENSSL_IS_BORINGSSL) + { + const char *orig; + SSL_SESSION *sess; + + /* + * When using TLSv1.3, OpenSSL 1.1.1e+ allows session resumption + * with names other than initially negotiated. We use + * SSL_SESSION_get0_hostname() to prevent use of certificates + * verified in different contexts. + */ + + if (SSL_version(c->ssl->connection) != TLS1_3_VERSION) { + return NGX_OK; + } + + if (!SSL_session_reused(c->ssl->connection)) { + return NGX_OK; + } + + sess = SSL_get0_session(c->ssl->connection); + + if (sess == NULL) { + return NGX_OK; + } + + /* + * If a server name was originally negotiated, it shouldn't be changed + * or dropped. If there was no server name, it is not checked, since + * non-SNI clients are allowed to request other virtual servers. + */ + + orig = SSL_SESSION_get0_hostname(sess); + + if (orig == NULL) { + return NGX_OK; + } + + if (name == NULL || ngx_strcmp(name, orig) != 0) { + return NGX_ERROR; + } + } +#endif +#endif return NGX_OK; - -#endif } From mdounin at mdounin.ru Sun Mar 9 00:48:53 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 09 Mar 2025 03:48:53 +0300 Subject: [PATCH] Tests: tests for SSL verification context checks In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1741458004 -10800 # Sat Mar 08 21:20:04 2025 +0300 # Node ID a23ab99972ae28e2bd2ce9badfaa2e52c6a03e24 # Parent 00307a7f3cadcc3a1eb4f9446e779e8a093657c4 Tests: tests for SSL verification context checks. diff --git a/ssl_verify_context.t b/ssl_verify_context.t new file mode 100644 --- /dev/null +++ b/ssl_verify_context.t @@ -0,0 +1,290 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for http ssl module, ssl_verify_client and verification context checks. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_ssl sni rewrite socket_ssl_sni/) + ->has_daemon('openssl') + ->plan(15) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + add_header X-SSL-Protocol $ssl_protocol always; + add_header X-SSL-Verify $ssl_client_verify always; + add_header X-SSL-Client $ssl_client_s_dn always; + add_header X-SSL-Reused $ssl_session_reused always; + add_header X-SSL-Name $ssl_server_name always; + + server { + listen 127.0.0.1:8443 ssl; + server_name localhost; + + ssl_certificate localhost.crt; + ssl_certificate_key localhost.key; + + ssl_verify_client optional; + ssl_client_certificate localhost.crt; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } + + server { + listen 127.0.0.1:8443; + server_name nocontext; + + ssl_verify_client on; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } + + server { + listen 127.0.0.1:8443; + server_name one; + + ssl_certificate one.crt; + ssl_certificate_key one.key; + + ssl_verify_client on; + ssl_client_certificate one.crt; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } + + server { + listen 127.0.0.1:8443; + server_name two; + + ssl_certificate two.crt; + ssl_certificate_key two.key; + + ssl_verify_client on; + ssl_client_certificate two.crt; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } + + server { + listen 127.0.0.1:8443; + server_name twobis; + + ssl_certificate two.crt; + ssl_certificate_key two.key; + + ssl_verify_client on; + ssl_client_certificate two.crt; + ssl_trusted_certificate one.crt; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('localhost', 'one', 'two') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->run(); + +############################################################################### + +# Tests for various HTTP-level requests of servers which do not match +# negotiated server name. + +# Following 5095:4fbef397c753, these are somewhat restricted to ensure +# that client certificates verified in a virtual server cannot be used +# in other virtual servers with client certificate verification configured, +# as these can use different CA certificates configured. Still, requests +# from non-SNI clients are allowed in all virtual servers. + +# for non-SNI clients, requests to all servers are allowed + +my $ctx = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_cert_file => "$d/localhost.crt", + SSL_key_file => "$d/localhost.key" + ); + +like(get('', '', $ctx), qr/^localhost::/m, + 'http, no server name, default server'); +like(get('', 'one', $ctx), qr/^one::/m, + 'http, no server name, virtual server'); +like(get('', 'nocontext', $ctx), qr/^nocontext::/m, + 'http, no server name, no context'); + + +# with SNI, corresponding virtual server is allowed, +# but other virtual servers return 421 (Misdirected Request) error +# (if there is client certificate verification configured) + +my $ctx1 = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_cert_file => "$d/one.crt", + SSL_key_file => "$d/one.key" + ); + +like(get('one', 'one', $ctx1), qr/^one:one:/m, + 'http, server name'); +like(get('one', 'two', $ctx1), qr/421 Misdirected/, + 'http, server name, other server rejected'); +like(get('one', '', $ctx1), qr/421 Misdirected/, + 'http, server name, default server rejected'); +like(get('localhost', 'one', $ctx1), qr/421 Misdirected/, + 'http, server name, default to virtual rejected'); + +like(get('nocontext', 'nocontext', $ctx), qr/^nocontext:nocontext:/m, + 'http, server name, no context'); +like(get('one', 'nocontext', $ctx1), qr/421 Misdirected/, + 'http, server name, virtual to no context rejected'); + +# Tests for session reuse with different names. + +# OpenSSL 1.1.1e+ with TLSv1.3 allows session resumption +# with names other than initially negotiated + +# BoringSSL allows session resumption with names other than +# initially negotiated, but checks session id context of the +# SNI-selected server + +# LibreSSL does not support session resumption with TLSv1.3, +# and with older protocols rejects hanshakes trying to resume +# a session with a different name + +$ctx = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_session_cache_size => 100, + SSL_cert_file => "$d/one.crt", + SSL_key_file => "$d/one.key" + ); + +like(get('one', 'one', $ctx), qr/^one:one:\.$/m, + 'ssl server name'); + +TODO: { +local $TODO = 'no TLSv1.3 sessions, old Net::SSLeay' + if $Net::SSLeay::VERSION < 1.88 && test_tls13(); +local $TODO = 'no TLSv1.3 sessions, old IO::Socket::SSL' + if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in LibreSSL' + if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); + +like(get('one', 'one', $ctx), qr/^one:one:r$/m, + 'ssl server name, reused'); + +} + +TODO: { +local $TODO = 'not yet' + if !$t->has_version('1.27.5') + && ($t->has_feature('openssl:1.1.1e') && test_tls13()); + +like(get('two', 'two', $ctx), qr/(421 Misdirected|400 Bad|^$)/, + 'ssl server name, reuse in other server rejected'); + +like(get('', '', $ctx), qr/(421 Misdirected|400 Bad|^$)/, + 'ssl server name, reuse in default server rejected'); + +} + +# for mostly identical servers (same certificate, same client CA list) +# make sure different trusted certificates is enough to prevent reuse + +$ctx = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_session_cache_size => 100, + SSL_cert_file => "$d/one.crt", + SSL_key_file => "$d/one.key" + ); + +like(get('twobis', 'twobis', $ctx), qr/^twobis:twobis:\.$/m, + 'ssl server name, trusted'); + +TODO: { +local $TODO = 'not yet' + if !$t->has_version('1.27.5') + && (($t->has_feature('openssl:1.1.1e') && test_tls13()) + || $t->has_module('BoringSSL')); + +like(get('two', 'two', $ctx), qr/(421 Misdirected|400 Bad|^$)/, + 'ssl server name, different trusted rejected'); + +} + +############################################################################### + +sub test_tls13 { + get() =~ /TLSv1.3/; +} + +sub get { + my ($sni, $host, $ctx) = @_; + return http( + "GET / HTTP/1.0" . CRLF . + ($host ? "Host: $host" . CRLF : "") . CRLF, + SSL => 1, + SSL_hostname => $sni, + SSL_reuse_ctx => $ctx + ); +} + +############################################################################### From mdounin at mdounin.ru Sat Mar 15 19:28:53 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Sat, 15 Mar 2025 22:28:53 +0300 Subject: [PATCH 2 of 4] HTTP: added MPTCP support In-Reply-To: References: Message-ID: Hello! On Tue, Aug 13, 2024 at 11:37:04AM +0200, Anthony Doeraene wrote: > # HG changeset patch > # User Anthony Doeraene > # Date 1723532143 -7200 > # Tue Aug 13 08:55:43 2024 +0200 > # Node ID d5b3c722c6796f5b163821b9a8402457420ade4a > # Parent b72362042b52f378e18ff6d01ec533e447331214 > HTTP: added MPTCP support. > > Multipath TCP (MPTCP), standardized in RFC8684 [1], is a TCP extension > that enables a TCP connection to use different paths. > > Multipath TCP has been used for several use cases. On smartphones, MPTCP > enables seamless handovers between cellular and Wi-Fi networks while > preserving Established connections. This use-case is what pushed Apple > to use MPTCP since 2013 in multiple applications [2]. On dual-stack > hosts, Multipath TCP enables the TCP connection to automatically use the > best performing path, either IPv4 or IPv6. If one path fails, MPTCP > automatically uses the other path. > > The benefit from MPTCP, both the client and the server have to support > it. Multipath TCP is a backward-compatible TCP extension that is enabled > by default on recent Linux distributions (Debian, Ubuntu, Redhat, ...). > Multipath TCP is included in the Linux kernel since version 5.6 [3]. > To use it on Linux, an application must explicitly enable it when > creating the socket. No need to change anything else in the application. > > Even if MPTCP is supported by different OS, only Linux supports the > `IPPROTO_MPTCP` protocol, which is why this feature is currently > limited to Linux only. > > This patch adds a new parameter 'multipath' to the 'listen' directive > in the HTTP module. This new parameter is only compatible with TCP if > IPPROTO_MPTCP is defined, not with QUIC so far. > > Co-developed-by: Maxime Dourov > > Link: https://www.rfc-editor.org/rfc/rfc8684.html [1] > Link: https://www.tessares.net/apples-mptcp-story-so-far/ [2] > Link: https://www.mptcp.dev [3] Sorry for the long delay with review, I wasn't very active for personal reasons. Responding to the HTTP patch, since it is most relevant (and I don't actually think that at least HTTP part should be in a separate patch from core support). [...] > diff -r b72362042b52 -r d5b3c722c679 src/http/ngx_http_core_module.c > --- a/src/http/ngx_http_core_module.c Tue Aug 13 08:50:15 2024 +0200 > +++ b/src/http/ngx_http_core_module.c Tue Aug 13 08:55:43 2024 +0200 > @@ -4062,6 +4062,13 @@ > } > #endif > > +#ifdef IPPROTO_MPTCP > + if (ngx_strcmp(value[n].data, "multipath") == 0) { > + lsopt.protocol = IPPROTO_MPTCP; > + continue; > + } > +#endif > + > if (ngx_strncmp(value[n].data, "backlog=", 8) == 0) { > lsopt.backlog = ngx_atoi(value[n].data + 8, value[n].len - 8); > lsopt.set = 1; First of all, note that neither lsopt.set nor lsopt.bind is set here. As a result, in the following configuration the "multipath" option will be silently ignored due to no lsopt.set: server { listen 80; server_name one; } server { listen 80 multipath; server_name two; } Similarly, in the following configuration there will be no socket where the "multipath" option is expected to be used: server { listen 80; } server { listen 127.0.0.1:80 multipath; } This can be easily solved by using lsopt.set and lsopt.bind, much like it is done for other socket options. Another issue is that it is basically impossible to change a normal listening socket into a Multipath TCP one. As such, when someone changes listen 80; into listen 80 multipath; the "multipath" option will be silently ignored by configuration reloads and even binary upgrades unless [free]nginx is restarted. I tend to think that this should at least give a warning to the user that the change was ignored. Another possible approach would be to reopen the socket if the multipath option changes. The patch below tries to do this, forcing SO_REUSEPORT to make it possible to reopen the socket. Also, it addresses the bind/set issues mentioned above, fixes udp check mismerge in the stream module, changes lsopt.protocol to lsopt.multipath flag as suggested in the initial review, and introduces configure check for NGX_HAVE_MULTIPATH (which is in line with other socket options and makes it possible to explicitly disable it during compilation). Please take a look: # HG changeset patch # User Maxim Dounin # Date 1741921604 -10800 # Fri Mar 14 06:06:44 2025 +0300 # Node ID 4de34f55afb5625b039d331f525e27b0a3ee90c5 # Parent 094e0ea330f5416750aa663647f60462a0c4b0cf Support for Multipath TCP on Linux. With this change, Multipath TCP on Linux can be activated with "listen ... multipath". The "listen ... multipath" option is supported in http, stream, and mail modules. Note that on Linux to activate Multipath TCP one should create a socket with the IPPROTO_MPTCP protocol explicitly specified, and it is not possible to change an existing socket. To make transition possible with minimal impact on client connections, if the multipath option is changed in the configuration, SO_REUSEPORT is set on the old socket, and the new socket is opened with IPPROTO_MPTCP. Note that this creates a race window, and connection requests which are assigned to the old socket will be lost. In particular, this might affect binary upgrade when the WINCH signal is used to preserve the old master process. Requires Linux kernel 5.6 or newer. Note though that some of the socket options might not be supported with Multipath TCP or only supported in new kernels. Most notably, TCP_NODELAY is only supported with kernel version 5.17 or newer. Based on patches by Maxime Dourov and Anthony Doeraene. diff --git a/auto/unix b/auto/unix --- a/auto/unix +++ b/auto/unix @@ -532,6 +532,17 @@ ngx_feature_test="socklen_t optlen = siz . auto/feature +ngx_feature="IPPROTO_MPTCP" +ngx_feature_name="NGX_HAVE_MULTIPATH" +ngx_feature_run=no +ngx_feature_incs="#include + #include " +ngx_feature_path= +ngx_feature_libs= +ngx_feature_test="socket(0, 0, IPPROTO_MPTCP)" +. auto/feature + + ngx_feature="accept4()" ngx_feature_name="NGX_HAVE_ACCEPT4" ngx_feature_run=no diff --git a/src/core/ngx_connection.c b/src/core/ngx_connection.c --- a/src/core/ngx_connection.c +++ b/src/core/ngx_connection.c @@ -150,6 +150,9 @@ ngx_set_inherited_sockets(ngx_cycle_t *c #if (NGX_HAVE_REUSEPORT) int reuseport; #endif +#if (NGX_HAVE_MULTIPATH) + int protocol; +#endif ls = cycle->listening.elts; for (i = 0; i < cycle->listening.nelts; i++) { @@ -338,6 +341,25 @@ ngx_set_inherited_sockets(ngx_cycle_t *c #endif +#if (NGX_HAVE_MULTIPATH) + + protocol = 0; + olen = sizeof(int); + + if (getsockopt(ls[i].fd, SOL_SOCKET, SO_PROTOCOL, + (void *) &protocol, &olen) + == -1) + { + ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_socket_errno, + "getsockopt(SO_PROTOCOL) %V failed, ignored", + &ls[i].addr_text); + + } else { + ls[i].multipath = (protocol == IPPROTO_MPTCP) ? 1 : 0; + } + +#endif + #if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER) ngx_memzero(&af, sizeof(struct accept_filter_arg)); @@ -406,7 +428,7 @@ ngx_set_inherited_sockets(ngx_cycle_t *c ngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle) { - int reuseaddr; + int reuseaddr, proto; ngx_uint_t i, tries, failed; ngx_err_t err; ngx_log_t *log; @@ -436,7 +458,7 @@ ngx_open_listening_sockets(ngx_cycle_t * #if (NGX_HAVE_REUSEPORT) - if (ls[i].add_reuseport) { + if (ls[i].add_reuseport || ls[i].reopen) { /* * to allow transition from a socket without SO_REUSEPORT @@ -472,6 +494,18 @@ ngx_open_listening_sockets(ngx_cycle_t * ls[i].add_reuseport = 0; } + + if (ls[i].reopen) { + + /* + * to allow transition to Multipath TCP we set SO_REUSEPORT + * on the old socket, and then open a new one + */ + + ls[i].fd = (ngx_socket_t) -1; + ls[i].inherited = 0; + ls[i].previous->remain = 0; + } #endif if (ls[i].fd != (ngx_socket_t) -1) { @@ -487,7 +521,15 @@ ngx_open_listening_sockets(ngx_cycle_t * continue; } - s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); + proto = 0; + +#if (NGX_HAVE_MULTIPATH) + if (ls[i].multipath) { + proto = IPPROTO_MPTCP; + } +#endif + + s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, proto); if (s == (ngx_socket_t) -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno, @@ -517,7 +559,7 @@ ngx_open_listening_sockets(ngx_cycle_t * #if (NGX_HAVE_REUSEPORT) - if (ls[i].reuseport && !ngx_test_config) { + if ((ls[i].reuseport || ls[i].reopen) && !ngx_test_config) { int reuseport; reuseport = 1; diff --git a/src/core/ngx_connection.h b/src/core/ngx_connection.h --- a/src/core/ngx_connection.h +++ b/src/core/ngx_connection.h @@ -57,6 +57,7 @@ struct ngx_listening_s { unsigned open:1; unsigned remain:1; unsigned ignore:1; + unsigned reopen:1; unsigned bound:1; /* already bound */ unsigned inherited:1; /* inherited from previous process */ @@ -72,6 +73,7 @@ struct ngx_listening_s { #endif unsigned reuseport:1; unsigned add_reuseport:1; + unsigned multipath:1; unsigned keepalive:2; unsigned quic:1; diff --git a/src/core/ngx_cycle.c b/src/core/ngx_cycle.c --- a/src/core/ngx_cycle.c +++ b/src/core/ngx_cycle.c @@ -581,6 +581,12 @@ ngx_init_cycle(ngx_cycle_t *old_cycle) } #endif +#if (NGX_HAVE_MULTIPATH) + if (ls[i].multipath != nls[n].multipath) { + nls[n].reopen = 1; + } +#endif + break; } } diff --git a/src/http/ngx_http.c b/src/http/ngx_http.c --- a/src/http/ngx_http.c +++ b/src/http/ngx_http.c @@ -1880,6 +1880,10 @@ ngx_http_add_listening(ngx_conf_t *cf, n ls->reuseport = addr->opt.reuseport; #endif +#if (NGX_HAVE_MULTIPATH) + ls->multipath = addr->opt.multipath; +#endif + ls->wildcard = addr->opt.wildcard; #if (NGX_HTTP_V3) diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c --- a/src/http/ngx_http_core_module.c +++ b/src/http/ngx_http_core_module.c @@ -4179,6 +4179,19 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx continue; } + if (ngx_strcmp(value[n].data, "multipath") == 0) { +#if (NGX_HAVE_MULTIPATH) + lsopt.multipath = 1; + lsopt.set = 1; + lsopt.bind = 1; +#else + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "multipath is not supported " + "on this platform, ignored"); +#endif + continue; + } + if (ngx_strcmp(value[n].data, "ssl") == 0) { #if (NGX_HTTP_SSL) lsopt.ssl = 1; @@ -4345,6 +4358,12 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx } #endif +#if (NGX_HAVE_MULTIPATH) + if (lsopt.multipath) { + return "\"multipath\" parameter is incompatible with \"quic\""; + } +#endif + #if (NGX_HTTP_SSL) if (lsopt.ssl) { return "\"ssl\" parameter is incompatible with \"quic\""; diff --git a/src/http/ngx_http_core_module.h b/src/http/ngx_http_core_module.h --- a/src/http/ngx_http_core_module.h +++ b/src/http/ngx_http_core_module.h @@ -81,6 +81,7 @@ typedef struct { #endif unsigned deferred_accept:1; unsigned reuseport:1; + unsigned multipath:1; unsigned so_keepalive:2; unsigned proxy_protocol:1; diff --git a/src/mail/ngx_mail.c b/src/mail/ngx_mail.c --- a/src/mail/ngx_mail.c +++ b/src/mail/ngx_mail.c @@ -347,6 +347,10 @@ ngx_mail_optimize_servers(ngx_conf_t *cf ls->ipv6only = addr[i].opt.ipv6only; #endif +#if (NGX_HAVE_MULTIPATH) + ls->multipath = addr[i].opt.multipath; +#endif + mport = ngx_palloc(cf->pool, sizeof(ngx_mail_port_t)); if (mport == NULL) { return NGX_CONF_ERROR; diff --git a/src/mail/ngx_mail.h b/src/mail/ngx_mail.h --- a/src/mail/ngx_mail.h +++ b/src/mail/ngx_mail.h @@ -40,6 +40,7 @@ typedef struct { #if (NGX_HAVE_INET6) unsigned ipv6only:1; #endif + unsigned multipath:1; unsigned so_keepalive:2; unsigned proxy_protocol:1; #if (NGX_HAVE_KEEPALIVE_TUNABLE) diff --git a/src/mail/ngx_mail_core_module.c b/src/mail/ngx_mail_core_module.c --- a/src/mail/ngx_mail_core_module.c +++ b/src/mail/ngx_mail_core_module.c @@ -456,6 +456,18 @@ ngx_mail_core_listen(ngx_conf_t *cf, ngx #endif } + if (ngx_strcmp(value[i].data, "multipath") == 0) { +#if (NGX_HAVE_MULTIPATH) + ls->multipath = 1; + ls->bind = 1; +#else + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "multipath is not supported " + "on this platform, ignored"); +#endif + continue; + } + if (ngx_strcmp(value[i].data, "ssl") == 0) { #if (NGX_MAIL_SSL) ngx_mail_ssl_conf_t *sslcf; diff --git a/src/stream/ngx_stream.c b/src/stream/ngx_stream.c --- a/src/stream/ngx_stream.c +++ b/src/stream/ngx_stream.c @@ -518,6 +518,10 @@ ngx_stream_optimize_servers(ngx_conf_t * ls->reuseport = addr[i].opt.reuseport; #endif +#if (NGX_HAVE_MULTIPATH) + ls->multipath = addr[i].opt.multipath; +#endif + stport = ngx_palloc(cf->pool, sizeof(ngx_stream_port_t)); if (stport == NULL) { return NGX_CONF_ERROR; diff --git a/src/stream/ngx_stream.h b/src/stream/ngx_stream.h --- a/src/stream/ngx_stream.h +++ b/src/stream/ngx_stream.h @@ -55,6 +55,7 @@ typedef struct { unsigned ipv6only:1; #endif unsigned reuseport:1; + unsigned multipath:1; unsigned so_keepalive:2; unsigned proxy_protocol:1; #if (NGX_HAVE_KEEPALIVE_TUNABLE) diff --git a/src/stream/ngx_stream_core_module.c b/src/stream/ngx_stream_core_module.c --- a/src/stream/ngx_stream_core_module.c +++ b/src/stream/ngx_stream_core_module.c @@ -738,6 +738,18 @@ ngx_stream_core_listen(ngx_conf_t *cf, n continue; } + if (ngx_strcmp(value[i].data, "multipath") == 0) { +#if (NGX_HAVE_MULTIPATH) + ls->multipath = 1; + ls->bind = 1; +#else + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "multipath is not supported " + "on this platform, ignored"); +#endif + continue; + } + if (ngx_strcmp(value[i].data, "ssl") == 0) { #if (NGX_STREAM_SSL) ngx_stream_ssl_conf_t *sslcf; @@ -884,6 +896,12 @@ ngx_stream_core_listen(ngx_conf_t *cf, n return "\"fastopen\" parameter is incompatible with \"udp\""; } #endif + +#if (NGX_HAVE_MULTIPATH) + if (ls->multipath) { + return "\"multipath\" parameter is incompatible with \"udp\""; + } +#endif } for (n = 0; n < u.naddrs; n++) { -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Mon Mar 24 00:51:06 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 24 Mar 2025 03:51:06 +0300 Subject: [nginx] SSL: added trusted certificates into session id context. Message-ID: details: http://freenginx.org/hg/nginx/rev/48cff1a93a0e branches: changeset: 9334:48cff1a93a0e user: Maxim Dounin date: Sat Mar 08 16:54:10 2025 +0300 description: SSL: added trusted certificates into session id context. This ensures that sessions cannot be incorrectly restored between servers with identical certificates and client certificates, but with different trusted certificates. Note that with BoringSSL this applies to SNI-based virtual servers, since with BoringSSL session resumption happens after the TLS servername callback, and session id context of the selected virtual server is used to decide whether the session can be resumed or not. diffstat: src/event/ngx_event_openssl.c | 60 ++++++++++++++++++++++++++++++++++++++++++- src/event/ngx_event_openssl.h | 1 + 2 files changed, 60 insertions(+), 1 deletions(-) diffs (123 lines): diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -131,6 +131,7 @@ int ngx_ssl_server_conf_index; int ngx_ssl_session_cache_index; int ngx_ssl_ticket_keys_index; int ngx_ssl_ocsp_index; +int ngx_ssl_trusted_list_index; int ngx_ssl_certificate_index; int ngx_ssl_next_certificate_index; int ngx_ssl_certificate_name_index; @@ -258,6 +259,14 @@ ngx_ssl_init(ngx_log_t *log) return NGX_ERROR; } + ngx_ssl_trusted_list_index = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, + NULL); + if (ngx_ssl_trusted_list_index == -1) { + ngx_ssl_error(NGX_LOG_ALERT, log, 0, + "SSL_CTX_get_ex_new_index() failed"); + return NGX_ERROR; + } + ngx_ssl_certificate_index = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); if (ngx_ssl_certificate_index == -1) { @@ -951,6 +960,8 @@ ngx_int_t ngx_ssl_trusted_certificate(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *cert, ngx_int_t depth) { + STACK_OF(X509_NAME) *list; + SSL_CTX_set_verify(ssl->ctx, SSL_CTX_get_verify_mode(ssl->ctx), ngx_ssl_verify_callback); @@ -980,6 +991,24 @@ ngx_ssl_trusted_certificate(ngx_conf_t * ERR_clear_error(); + if (SSL_CTX_get_verify_mode(ssl->ctx) != SSL_VERIFY_PEER) { + return NGX_OK; + } + + list = SSL_load_client_CA_file((char *) cert->data); + + if (list == NULL) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "SSL_load_client_CA_file(\"%s\") failed", cert->data); + return NGX_ERROR; + } + + if (SSL_CTX_set_ex_data(ssl->ctx, ngx_ssl_trusted_list_index, list) == 0) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "SSL_CTX_set_ex_data() failed"); + return NGX_ERROR; + } + return NGX_OK; } @@ -3857,6 +3886,28 @@ ngx_ssl_session_id_context(ngx_ssl_t *ss } } + list = SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_trusted_list_index); + + if (list != NULL) { + n = sk_X509_NAME_num(list); + + for (i = 0; i < n; i++) { + name = sk_X509_NAME_value(list, i); + + if (X509_NAME_digest(name, EVP_sha1(), buf, &len) == 0) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "X509_NAME_digest() failed"); + goto failed; + } + + if (EVP_DigestUpdate(md, buf, len) == 0) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "EVP_DigestUpdate() failed"); + goto failed; + } + } + } + if (EVP_DigestFinal_ex(md, buf, &len) == 0) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "EVP_DigestFinal_ex() failed"); @@ -4826,7 +4877,8 @@ ngx_ssl_cleanup_ctx(void *data) { ngx_ssl_t *ssl = data; - X509 *cert, *next; + X509 *cert, *next; + STACK_OF(X509_NAME) *list; cert = SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_certificate_index); @@ -4836,6 +4888,12 @@ ngx_ssl_cleanup_ctx(void *data) cert = next; } + list = SSL_CTX_get_ex_data(ssl->ctx, ngx_ssl_trusted_list_index); + + if (list) { + sk_X509_NAME_pop_free(list, X509_NAME_free); + } + SSL_CTX_free(ssl->ctx); } diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -332,6 +332,7 @@ extern int ngx_ssl_server_conf_index; extern int ngx_ssl_session_cache_index; extern int ngx_ssl_ticket_keys_index; extern int ngx_ssl_ocsp_index; +extern int ngx_ssl_trusted_list_index; extern int ngx_ssl_certificate_index; extern int ngx_ssl_next_certificate_index; extern int ngx_ssl_certificate_name_index; From mdounin at mdounin.ru Mon Mar 24 00:51:07 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 24 Mar 2025 03:51:07 +0300 Subject: [nginx] SSL: reworked restriction on hosts other than negotiated. Message-ID: details: http://freenginx.org/hg/nginx/rev/4dc9fb4dd952 branches: changeset: 9335:4dc9fb4dd952 user: Maxim Dounin date: Sat Mar 08 20:44:41 2025 +0300 description: SSL: reworked restriction on hosts other than negotiated. Following 5095:4fbef397c753, attempts to request hosts other than negotiated are rejected with 421 (Misdirected Request) HTTP error as long as certificate verification is enabled in the requested server block. With this change, such requests are now rejected in ngx_http_process_request(), along with other certificate verification checks. diffstat: src/event/ngx_event_openssl.c | 41 +++++++++++++++++++++++++++++++++++++++++ src/event/ngx_event_openssl.h | 1 + src/http/ngx_http_request.c | 22 ++++++++++------------ 3 files changed, 52 insertions(+), 12 deletions(-) diffs (105 lines): diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -5068,6 +5068,47 @@ ngx_ssl_check_name(ngx_str_t *name, ASN1 ngx_int_t +ngx_ssl_check_verify_context(ngx_connection_t *c, ngx_ssl_t *ssl) +{ +#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME + + SSL_CTX *ctx; + const char *name; + + /* + * Check current SSL context to match the one where the certificate + * verification happened. + * + * If certificate verification was in the default server block without + * a server name, all contexts are allowed, as non-SNI clients are + * allowed to request other virtual servers. + * + * If there is no context in the current server block, verification + * is expected to happen in the default server. + */ + + ctx = SSL_get_SSL_CTX(c->ssl->connection); + name = SSL_get_servername(c->ssl->connection, TLSEXT_NAMETYPE_host_name); + + if (ctx == ssl->ctx) { + return NGX_OK; + } + + if (ctx == c->ssl->session_ctx && (ssl->ctx == NULL || name == NULL)) { + return NGX_OK; + } + + return NGX_ERROR; + +#else + + return NGX_OK; + +#endif +} + + +ngx_int_t ngx_ssl_get_protocol(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { s->data = (u_char *) SSL_get_version(c->ssl->connection); diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -259,6 +259,7 @@ ngx_ssl_session_t *ngx_ssl_get0_session( || n == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE) ngx_int_t ngx_ssl_check_host(ngx_connection_t *c, ngx_str_t *name); +ngx_int_t ngx_ssl_check_verify_context(ngx_connection_t *c, ngx_ssl_t *ssl); ngx_int_t ngx_ssl_get_protocol(ngx_connection_t *c, ngx_pool_t *pool, diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c --- a/src/http/ngx_http_request.c +++ b/src/http/ngx_http_request.c @@ -2073,6 +2073,16 @@ ngx_http_process_request(ngx_http_reques sscf = ngx_http_get_module_srv_conf(r, ngx_http_ssl_module); if (sscf->verify) { + + if (ngx_ssl_check_verify_context(c, &sscf->ssl) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "client SSL certificate was negotiated " + "with a different server name"); + + ngx_http_finalize_request(r, NGX_HTTP_MISDIRECTED_REQUEST); + return; + } + rc = SSL_get_verify_result(c->ssl->connection); if (rc != X509_V_OK @@ -2280,22 +2290,10 @@ ngx_http_set_virtual_server(ngx_http_req #if (NGX_HTTP_SSL && defined SSL_CTRL_SET_TLSEXT_HOSTNAME) if (hc->ssl_servername) { - ngx_http_ssl_srv_conf_t *sscf; - if (rc == NGX_DECLINED) { cscf = hc->addr_conf->default_server; rc = NGX_OK; } - - sscf = ngx_http_get_module_srv_conf(cscf->ctx, ngx_http_ssl_module); - - if (sscf->verify) { - ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, - "client attempted to request the server name " - "different from the one that was negotiated"); - ngx_http_finalize_request(r, NGX_HTTP_MISDIRECTED_REQUEST); - return NGX_ERROR; - } } #endif From mdounin at mdounin.ru Mon Mar 24 00:51:07 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 24 Mar 2025 03:51:07 +0300 Subject: [nginx] SSL: added additional verify context check for OpenSSL. Message-ID: details: http://freenginx.org/hg/nginx/rev/094e0ea330f5 branches: changeset: 9336:094e0ea330f5 user: Maxim Dounin date: Sat Mar 08 21:11:07 2025 +0300 description: SSL: added additional verify context check for OpenSSL. When using TLSv1.3, OpenSSL 1.1.1e+ allows session resumption with names other than initially negotiated, and provides no documented way to prevent it or even detect. This makes it possible to resume the session with a certificate verified in the context of a different server block, similarly to 5095:4fbef397c753. There is no such problem in LibreSSL since it doesn't support session resumption with TLSv1.3 (and does not allow session resumption with different names with TLSv1.2 and below). Similarly, there is no such problem with BoringSSL. While BoringSSL allows sessions to be resumed with different names, it does so only after checking session id context set by the servername callback. With this change, SSL_get_servername() (which, for TLSv1.3, returns name as sent by the client in the current handshake) is checked against SSL_SESSION_get0_hostname(), and thus prevents incorrect use of certificates verified in different contexts. Note that SSL_SESSION_get0_hostname() is documented to return NULL with TLSv1.3, but in practice it returns the originally negotiated name at least up to OpenSSL 3.4.1, and the documentation is expected to be fixed. diffstat: src/event/ngx_event_openssl.c | 55 +++++++++++++++++++++++++++++++++++++++--- 1 files changed, 50 insertions(+), 5 deletions(-) diffs (74 lines): diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -5091,20 +5091,65 @@ ngx_ssl_check_verify_context(ngx_connect name = SSL_get_servername(c->ssl->connection, TLSEXT_NAMETYPE_host_name); if (ctx == ssl->ctx) { - return NGX_OK; + goto next; } if (ctx == c->ssl->session_ctx && (ssl->ctx == NULL || name == NULL)) { - return NGX_OK; + goto next; } return NGX_ERROR; -#else +next: + +#if (defined TLS1_3_VERSION \ + && !defined LIBRESSL_VERSION_NUMBER \ + && !defined OPENSSL_IS_BORINGSSL) + { + const char *orig; + SSL_SESSION *sess; + + /* + * When using TLSv1.3, OpenSSL 1.1.1e+ allows session resumption + * with names other than initially negotiated. We use + * SSL_SESSION_get0_hostname() to prevent use of certificates + * verified in different contexts. + */ + + if (SSL_version(c->ssl->connection) != TLS1_3_VERSION) { + return NGX_OK; + } + + if (!SSL_session_reused(c->ssl->connection)) { + return NGX_OK; + } + + sess = SSL_get0_session(c->ssl->connection); + + if (sess == NULL) { + return NGX_OK; + } + + /* + * If a server name was originally negotiated, it shouldn't be changed + * or dropped. If there was no server name, it is not checked, since + * non-SNI clients are allowed to request other virtual servers. + */ + + orig = SSL_SESSION_get0_hostname(sess); + + if (orig == NULL) { + return NGX_OK; + } + + if (name == NULL || ngx_strcmp(name, orig) != 0) { + return NGX_ERROR; + } + } +#endif +#endif return NGX_OK; - -#endif } From mdounin at mdounin.ru Mon Mar 24 00:52:22 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 24 Mar 2025 03:52:22 +0300 Subject: [nginx-tests] Tests: fixed LibreSSL TODOs in h3_ssl_early_data.t. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/a84cf984d25e branches: changeset: 2004:a84cf984d25e user: Maxim Dounin date: Sat Mar 08 04:44:20 2025 +0300 description: Tests: fixed LibreSSL TODOs in h3_ssl_early_data.t. Previously, the last test wasn't properly excluded from the TODO scope and was reported as an unexpected success. diffstat: h3_ssl_early_data.t | 11 +++++++++-- 1 files changed, 9 insertions(+), 2 deletions(-) diffs (36 lines): diff --git a/h3_ssl_early_data.t b/h3_ssl_early_data.t --- a/h3_ssl_early_data.t +++ b/h3_ssl_early_data.t @@ -80,13 +80,13 @@ my $frames = $s->read(all => [{ sid => $ my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-session'}, '.', 'new session'); -local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL'); - my $psk_list = $s->{psk_list}; $s = Test::Nginx::HTTP3->new(8980, psk_list => $psk_list, early_data => {}); TODO: { +local $TODO = 'no TLSv1.3 sessions in LibreSSL' + if $t->has_module('LibreSSL'); local $TODO = 'no 0-RTT in OpenSSL compat layer' unless $t->has_module('OpenSSL [.0-9]+\+quic') or $t->has_module('BoringSSL') @@ -99,9 +99,16 @@ is($frame->{headers}->{'x-early'}, '1', } +TODO: { +local $TODO = 'no TLSv1.3 sessions in LibreSSL' + if $t->has_module('LibreSSL'); + $frames = $s->read(all => [{ sid => $s->new_stream(), fin => 1 }]); ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; is($frame->{headers}->{'x-session'}, 'r', 'reused session 1rtt'); + +} + is($frame->{headers}->{'x-early'}, undef, 'reused session not early'); ############################################################################### From mdounin at mdounin.ru Mon Mar 24 00:52:22 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 24 Mar 2025 03:52:22 +0300 Subject: [nginx-tests] Tests: adjusted TODOs for LibreSSL 4.0.0. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/00307a7f3cad branches: changeset: 2005:00307a7f3cad user: Maxim Dounin date: Sat Mar 08 04:44:22 2025 +0300 description: Tests: adjusted TODOs for LibreSSL 4.0.0. Issue with signature algorithms in TLSv1.3 is fixed in LibreSSL 4.0.0 (https://github.com/libressl/portable/issues/1058), ssl_certificates.t and ssl_stapling.t tests adjusted accordingly. Note thought that LibreSSL also fails to provide correct certificate information when OCSP stapling is used with TLSv1.3 and multiple certificates (https://github.com/libressl/portable/issues/1059), so some tests in ssl_stapling.t are still failing even with the fix. Additionally, sending alerts in QUIC is also fixed in LibreSSL 4.0.0, as seen in the h3_ssl_reject_handshake.t test. diffstat: h3_ssl_reject_handshake.t | 8 ++++++-- ssl_certificates.t | 4 +++- ssl_stapling.t | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diffs (50 lines): diff --git a/h3_ssl_reject_handshake.t b/h3_ssl_reject_handshake.t --- a/h3_ssl_reject_handshake.t +++ b/h3_ssl_reject_handshake.t @@ -114,7 +114,9 @@ skip "OpenSSL too old", 3 if $got && $go # default virtual server rejected TODO: { -local $TODO = 'broken send_alert in LibreSSL' if $t->has_module('LibreSSL'); +local $TODO = 'broken send_alert in LibreSSL' + if $t->has_module('LibreSSL') + and not $t->has_feature('libressl:4.0.0'); is(bad('default', 8980), $alert, 'default rejected'); is(bad(undef, 8980), $alert, 'absent sni rejected'); @@ -132,7 +134,9 @@ like(get(undef, 8982), qr/200/, 'absent like(get('virtual1', 8982), qr/virtual1/, 'virtual 1 accepted'); TODO: { -local $TODO = 'broken send_alert in LibreSSL' if $t->has_module('LibreSSL'); +local $TODO = 'broken send_alert in LibreSSL' + if $t->has_module('LibreSSL') + and not $t->has_feature('libressl:4.0.0'); is(bad('virtual2', 8982), $alert, 'virtual 2 rejected'); diff --git a/ssl_certificates.t b/ssl_certificates.t --- a/ssl_certificates.t +++ b/ssl_certificates.t @@ -96,7 +96,9 @@ foreach my $name ('ec', 'rsa') { TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' - if $t->has_module('LibreSSL') && test_tls13(); + if $t->has_module('LibreSSL') + && !$t->has_feature('libressl:4.0.0') + && test_tls13(); like(cert('RSA'), qr/CN=rsa/, 'ssl cert RSA'); diff --git a/ssl_stapling.t b/ssl_stapling.t --- a/ssl_stapling.t +++ b/ssl_stapling.t @@ -298,6 +298,7 @@ ok(!staple(8449, 'ECDSA'), 'ocsp error') TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' if $t->has_module('LibreSSL') + && !$t->has_feature('libressl:4.0.0') && !Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); From mdounin at mdounin.ru Mon Mar 24 00:52:23 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 24 Mar 2025 03:52:23 +0300 Subject: [nginx-tests] Tests: tests for SSL verification context checks. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/a23ab99972ae branches: changeset: 2006:a23ab99972ae user: Maxim Dounin date: Sat Mar 08 21:20:04 2025 +0300 description: Tests: tests for SSL verification context checks. diffstat: ssl_verify_context.t | 290 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 290 insertions(+), 0 deletions(-) diffs (295 lines): diff --git a/ssl_verify_context.t b/ssl_verify_context.t new file mode 100644 --- /dev/null +++ b/ssl_verify_context.t @@ -0,0 +1,290 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for http ssl module, ssl_verify_client and verification context checks. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_ssl sni rewrite socket_ssl_sni/) + ->has_daemon('openssl') + ->plan(15) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + add_header X-SSL-Protocol $ssl_protocol always; + add_header X-SSL-Verify $ssl_client_verify always; + add_header X-SSL-Client $ssl_client_s_dn always; + add_header X-SSL-Reused $ssl_session_reused always; + add_header X-SSL-Name $ssl_server_name always; + + server { + listen 127.0.0.1:8443 ssl; + server_name localhost; + + ssl_certificate localhost.crt; + ssl_certificate_key localhost.key; + + ssl_verify_client optional; + ssl_client_certificate localhost.crt; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } + + server { + listen 127.0.0.1:8443; + server_name nocontext; + + ssl_verify_client on; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } + + server { + listen 127.0.0.1:8443; + server_name one; + + ssl_certificate one.crt; + ssl_certificate_key one.key; + + ssl_verify_client on; + ssl_client_certificate one.crt; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } + + server { + listen 127.0.0.1:8443; + server_name two; + + ssl_certificate two.crt; + ssl_certificate_key two.key; + + ssl_verify_client on; + ssl_client_certificate two.crt; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } + + server { + listen 127.0.0.1:8443; + server_name twobis; + + ssl_certificate two.crt; + ssl_certificate_key two.key; + + ssl_verify_client on; + ssl_client_certificate two.crt; + ssl_trusted_certificate one.crt; + + location / { + return 200 $server_name:$ssl_server_name:$ssl_session_reused; + } + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('localhost', 'one', 'two') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->run(); + +############################################################################### + +# Tests for various HTTP-level requests of servers which do not match +# negotiated server name. + +# Following 5095:4fbef397c753, these are somewhat restricted to ensure +# that client certificates verified in a virtual server cannot be used +# in other virtual servers with client certificate verification configured, +# as these can use different CA certificates configured. Still, requests +# from non-SNI clients are allowed in all virtual servers. + +# for non-SNI clients, requests to all servers are allowed + +my $ctx = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_cert_file => "$d/localhost.crt", + SSL_key_file => "$d/localhost.key" + ); + +like(get('', '', $ctx), qr/^localhost::/m, + 'http, no server name, default server'); +like(get('', 'one', $ctx), qr/^one::/m, + 'http, no server name, virtual server'); +like(get('', 'nocontext', $ctx), qr/^nocontext::/m, + 'http, no server name, no context'); + + +# with SNI, corresponding virtual server is allowed, +# but other virtual servers return 421 (Misdirected Request) error +# (if there is client certificate verification configured) + +my $ctx1 = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_cert_file => "$d/one.crt", + SSL_key_file => "$d/one.key" + ); + +like(get('one', 'one', $ctx1), qr/^one:one:/m, + 'http, server name'); +like(get('one', 'two', $ctx1), qr/421 Misdirected/, + 'http, server name, other server rejected'); +like(get('one', '', $ctx1), qr/421 Misdirected/, + 'http, server name, default server rejected'); +like(get('localhost', 'one', $ctx1), qr/421 Misdirected/, + 'http, server name, default to virtual rejected'); + +like(get('nocontext', 'nocontext', $ctx), qr/^nocontext:nocontext:/m, + 'http, server name, no context'); +like(get('one', 'nocontext', $ctx1), qr/421 Misdirected/, + 'http, server name, virtual to no context rejected'); + +# Tests for session reuse with different names. + +# OpenSSL 1.1.1e+ with TLSv1.3 allows session resumption +# with names other than initially negotiated + +# BoringSSL allows session resumption with names other than +# initially negotiated, but checks session id context of the +# SNI-selected server + +# LibreSSL does not support session resumption with TLSv1.3, +# and with older protocols rejects hanshakes trying to resume +# a session with a different name + +$ctx = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_session_cache_size => 100, + SSL_cert_file => "$d/one.crt", + SSL_key_file => "$d/one.key" + ); + +like(get('one', 'one', $ctx), qr/^one:one:\.$/m, + 'ssl server name'); + +TODO: { +local $TODO = 'no TLSv1.3 sessions, old Net::SSLeay' + if $Net::SSLeay::VERSION < 1.88 && test_tls13(); +local $TODO = 'no TLSv1.3 sessions, old IO::Socket::SSL' + if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in LibreSSL' + if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); + +like(get('one', 'one', $ctx), qr/^one:one:r$/m, + 'ssl server name, reused'); + +} + +TODO: { +local $TODO = 'not yet' + if !$t->has_version('1.27.5') + && ($t->has_feature('openssl:1.1.1e') && test_tls13()); + +like(get('two', 'two', $ctx), qr/(421 Misdirected|400 Bad|^$)/, + 'ssl server name, reuse in other server rejected'); + +like(get('', '', $ctx), qr/(421 Misdirected|400 Bad|^$)/, + 'ssl server name, reuse in default server rejected'); + +} + +# for mostly identical servers (same certificate, same client CA list) +# make sure different trusted certificates is enough to prevent reuse + +$ctx = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_session_cache_size => 100, + SSL_cert_file => "$d/one.crt", + SSL_key_file => "$d/one.key" + ); + +like(get('twobis', 'twobis', $ctx), qr/^twobis:twobis:\.$/m, + 'ssl server name, trusted'); + +TODO: { +local $TODO = 'not yet' + if !$t->has_version('1.27.5') + && (($t->has_feature('openssl:1.1.1e') && test_tls13()) + || $t->has_module('BoringSSL')); + +like(get('two', 'two', $ctx), qr/(421 Misdirected|400 Bad|^$)/, + 'ssl server name, different trusted rejected'); + +} + +############################################################################### + +sub test_tls13 { + get() =~ /TLSv1.3/; +} + +sub get { + my ($sni, $host, $ctx) = @_; + return http( + "GET / HTTP/1.0" . CRLF . + ($host ? "Host: $host" . CRLF : "") . CRLF, + SSL => 1, + SSL_hostname => $sni, + SSL_reuse_ctx => $ctx + ); +} + +###############################################################################