Commit cb6ca8e08a for openssl.org
commit cb6ca8e08abc1c97e3bba6c0caa843fee4375327
Author: Mar <mardgez13@gmail.com>
Date: Tue Oct 20 13:53:27 2026 +0200
Re-collect ClientHello extensions whenever custom extensions are in use
If the ClientHello callback switches the SSL_CTX with SSL_set_SSL_CTX(),
the ClientHello extensions have already been collected for the previous
SSL_CTX, whose custom extensions can differ from those of the new one.
Collect them again after the callback, so that their number and indices
match the SSL_CTX now in use. The built-in extensions have already
been initialised, and the debug callback has already been given
the extensions, so neither is done again.
Add test_ctx_switch_more_custom_exts to sslapitest, which switches
to an SSL_CTX with more custom extensions in the ClientHello callback,
and check that the new SSL_CTX parses the custom extension the client
sends.
Assisted-by: Claude:claude-opus-5-5
Reviewed-by: Matt Caswell <matt@openssl.foundation>
Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
Merge-date: Fri Oct 30 03:20:43 2026
Merged-from: https://github.com/openssl/openssl/pull/33375
diff --git a/ssl/statem/statem_srvr.c b/ssl/statem/statem_srvr.c
index 288ebde573..52770e12d7 100644
--- a/ssl/statem/statem_srvr.c
+++ b/ssl/statem/statem_srvr.c
@@ -2103,6 +2103,38 @@ static int tls_early_post_process_client_hello(SSL_CONNECTION *s)
}
}
+ /*
+ * The callback may have switched to an SSL_CTX with a different set of
+ * custom extensions, possibly in an earlier call that returned
+ * SSL_CLIENT_HELLO_RETRY. The collected extensions are indexed by the
+ * custom extensions in use when they were collected, so collect them
+ * again unless neither set has any.
+ */
+ if (clienthello->pre_proc_exts_len != TLSEXT_IDX_num_builtins
+ || s->cert->custext.meths_count != 0) {
+ PACKET extensions = clienthello->extensions;
+ void (*debug_cb)(SSL *, int, int, const unsigned char *, int, void *)
+ = s->ext.debug_cb;
+ int ret;
+
+ OPENSSL_free(clienthello->pre_proc_exts);
+ clienthello->pre_proc_exts = NULL;
+ clienthello->pre_proc_exts_len = 0;
+ /*
+ * The first collection has already initialised the built-in
+ * extensions and passed the extensions to the debug callback.
+ */
+ s->ext.debug_cb = NULL;
+ ret = tls_collect_extensions(s, &extensions, SSL_EXT_CLIENT_HELLO,
+ &clienthello->pre_proc_exts,
+ &clienthello->pre_proc_exts_len, 0);
+ s->ext.debug_cb = debug_cb;
+ if (!ret) {
+ /* SSLfatal already been called */
+ goto err;
+ }
+ }
+
/* Set up the client_random */
memcpy(s->s3.client_random, clienthello->random, SSL3_RANDOM_SIZE);
diff --git a/test/sslapitest.c b/test/sslapitest.c
index d2eaa9fb83..46b0fab710 100644
--- a/test/sslapitest.c
+++ b/test/sslapitest.c
@@ -15346,6 +15346,106 @@ end:
return testresult;
}
#endif /* OPENSSL_NO_TLS1_2 */
+
+/*
+ * The ClientHello raw extension array is sized when the ClientHello is
+ * collected. Switching to an SSL_CTX with more custom extensions from the
+ * ClientHello callback must not make the later parse walk past its end, and
+ * the new SSL_CTX must get the custom extension the client sent, which is the
+ * last of its own.
+ */
+struct cust_ext_switch_st {
+ SSL_CTX *newctx;
+ int retry;
+ int parsed; /* Times the new SSL_CTX parsed the client's extension */
+};
+
+static int cust_ext_switch_parse_cb(SSL *s, unsigned int ext_type,
+ unsigned int context, const unsigned char *in, size_t inlen, X509 *x,
+ size_t chainidx, int *al, void *parse_arg)
+{
+ int *parsed = parse_arg;
+
+ if (ext_type == 0xff4f)
+ (*parsed)++;
+ return 1;
+}
+
+static int cust_ext_switch_ch_cb(SSL *s, int *al, void *arg)
+{
+ struct cust_ext_switch_st *data = arg;
+
+ if (SSL_get_SSL_CTX(s) == data->newctx)
+ return SSL_CLIENT_HELLO_SUCCESS;
+ if (SSL_set_SSL_CTX(s, data->newctx) == NULL) {
+ *al = SSL_AD_INTERNAL_ERROR;
+ return SSL_CLIENT_HELLO_ERROR;
+ }
+ return data->retry ? SSL_CLIENT_HELLO_RETRY : SSL_CLIENT_HELLO_SUCCESS;
+}
+
+/*
+ * Test 0: The callback switches the SSL_CTX and returns success
+ * Test 1: The callback switches the SSL_CTX and returns retry, so the
+ * handshake goes on with the new SSL_CTX, which has no callback
+ */
+static int test_ctx_switch_more_custom_exts(int idx)
+{
+ SSL_CTX *sctx = NULL, *newsctx = NULL, *cctx = NULL;
+ SSL *serverssl = NULL, *clientssl = NULL;
+ struct cust_ext_switch_st cbdata;
+ unsigned int i;
+ int testresult = 0;
+
+ memset(&cbdata, 0, sizeof(cbdata));
+
+ if (!TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(),
+ TLS_client_method(), 0, 0, &sctx, &cctx, cert, privkey))
+ || !TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(), NULL,
+ 0, 0, &newsctx, NULL, cert, privkey)))
+ goto end;
+
+ for (i = 0; i < 64; i++)
+ if (!TEST_true(SSL_CTX_add_custom_ext(newsctx, 0xff10 + i,
+ SSL_EXT_CLIENT_HELLO, NULL, NULL, NULL,
+ cust_ext_switch_parse_cb, &cbdata.parsed)))
+ goto end;
+
+ /* The client sends 0xff4f empty */
+ if (!TEST_true(SSL_CTX_add_custom_ext(cctx, 0xff4f, SSL_EXT_CLIENT_HELLO,
+ NULL, NULL, NULL, NULL, NULL)))
+ goto end;
+
+ cbdata.newctx = newsctx;
+ cbdata.retry = idx == 1;
+ SSL_CTX_set_client_hello_cb(sctx, cust_ext_switch_ch_cb, &cbdata);
+
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl,
+ NULL, NULL)))
+ goto end;
+
+ if (cbdata.retry
+ && (!TEST_false(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_WANT_CLIENT_HELLO_CB))
+ || !TEST_int_eq(SSL_get_error(serverssl, -1),
+ SSL_ERROR_WANT_CLIENT_HELLO_CB)))
+ goto end;
+
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE))
+ || !TEST_ptr_eq(SSL_get_SSL_CTX(serverssl), newsctx)
+ || !TEST_int_eq(cbdata.parsed, 1))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(newsctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
#endif
#ifndef OPENSSL_NO_TLS1_2
@@ -19643,6 +19743,7 @@ int setup_tests(void)
#endif
#if !defined(OPENSSL_NO_TLS1_2) || !defined(OSSL_NO_USABLE_TLS1_3)
ADD_ALL_TESTS(test_sigalg_ctx_switch, 4);
+ ADD_ALL_TESTS(test_ctx_switch_more_custom_exts, 2);
ADD_ALL_TESTS(test_sigalg_ctx_switch_cert_cb, 2);
#ifndef OPENSSL_NO_TLS1_2
ADD_TEST(test_sigalg_ctx_switch_reneg);