Commit 4c1a153807 for openssl.org
commit 4c1a1538078416ecfe9279dbb8ed2a2198d4e535
Author: Matt Caswell <matt@openssl.org>
Date: Wed Sep 9 11:02:16 2026 +0100
Add regression tests for the SSL_set_SSL_CTX() sigalg state
The fix for CVE-2026-72897 made SSL_set_SSL_CTX() refresh the
connection's signature algorithm state so that it describes the context
being installed. Cover it.
That state is sized from the SSL_CTX SSL_new() was called on, and a
provider sigalg's sig_idx is its position in the list of whichever
SSL_CTX a peer codepoint is resolved against, so the two have to
describe the same context. The test provider makes them differ: a
context created before it is loaded lacks the two slots it adds, and one
created afterwards has them.
Three routes an application can switch context from are covered, and
they do not behave alike.
test_sigalg_ctx_switch() switches from the servername and client_hello
callbacks, both of which run before tls1_set_server_sigalgs() allocates
valid_flags. A stale slot count undersized that allocation, and
tls1_process_sigalgs() then read and wrote past its end. The two
callbacks run at different points - the client_hello callback before the
ClientHello extensions are parsed at all, the servername callback from
the final pass over them - so they are checked separately. The extra
provider sigalgs are TLSv1.3 only, so only those cases reached past the
end of the buffer; the TLSv1.2 cases pin the invariant.
test_sigalg_ctx_switch_cert_cb() switches from the certificate callback,
which runs after valid_flags has been sized and filled in. Nothing
recomputes the shared sigalgs later in the same handshake, so nothing
read out of bounds on that route, but the state left behind was
inconsistent. It also checks the built-in slots survive the switch,
which is the only coverage of that part of the change.
test_sigalg_ctx_switch_reneg() switches during a TLSv1.2 renegotiation
with a changed servername. valid_flags is allocated by the initial
handshake and not freed in between, so this is the case where the buffer
has to be replaced rather than merely sized correctly later.
Each test checks the premise it depends on rather than risk becoming
vacuous: that the replacement context really does have more slots, that
the renegotiation really was a full handshake, and that the flags being
compared across the switch were non-zero to begin with.
Assisted-by: Claude Code:claude-opus-5[1m]
Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
Reviewed-by: Neil Horman <nhorman@openssl.org>
Merge-date: Tue Sep 29 11:20:19 2026
diff --git a/test/sslapitest.c b/test/sslapitest.c
index b07bc3b955..911ab4f3bf 100644
--- a/test/sslapitest.c
+++ b/test/sslapitest.c
@@ -13412,6 +13412,415 @@ end:
}
#endif
+#if !defined(OPENSSL_NO_TLS1_2) || !defined(OSSL_NO_USABLE_TLS1_3)
+/*
+ * Regression tests for CVE-2026-72897: SSL_set_SSL_CTX() must refresh the
+ * connection's signature algorithm state to describe the context it installs.
+ *
+ * |ssl_pkey_num| sizes |s3.tmp.valid_flags| and bounds the loops over
+ * |cert->pkeys|, while a provider sigalg's |sig_idx| is its position in the
+ * list of whichever context a peer codepoint is resolved against, so the two
+ * must describe the same one. The test provider is what makes them differ: a
+ * context created before it is loaded lacks the two slots it adds.
+ */
+
+struct sigalg_ctx_switch_st {
+ SSL_CTX *newctx;
+ int switches; /* Number of times the callback switched context */
+ int err; /* Set if SSL_set_SSL_CTX() itself failed */
+ uint32_t preflags; /* valid_flags[SSL_PKEY_RSA] before the switch */
+ uint32_t postflags; /* ...and after it */
+};
+
+static int sigalg_switch_ctx(SSL *s, struct sigalg_ctx_switch_st *data)
+{
+ SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL_ONLY(s);
+
+ if (sc == NULL)
+ return 0;
+
+ if (sc->s3.tmp.valid_flags != NULL)
+ data->preflags = sc->s3.tmp.valid_flags[SSL_PKEY_RSA];
+ if (SSL_set_SSL_CTX(s, data->newctx) == NULL)
+ return 0;
+ if (sc->s3.tmp.valid_flags != NULL)
+ data->postflags = sc->s3.tmp.valid_flags[SSL_PKEY_RSA];
+
+ data->switches++;
+ return 1;
+}
+
+static int sigalg_switch_sni_cb(SSL *s, int *al, void *arg)
+{
+ struct sigalg_ctx_switch_st *data = arg;
+
+ if (SSL_get_SSL_CTX(s) != data->newctx && !sigalg_switch_ctx(s, data)) {
+ data->err = 1;
+ *al = SSL_AD_INTERNAL_ERROR;
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+ }
+ return SSL_TLSEXT_ERR_OK;
+}
+
+static int sigalg_switch_clienthello_cb(SSL *s, int *al, void *arg)
+{
+ struct sigalg_ctx_switch_st *data = arg;
+
+ if (SSL_get_SSL_CTX(s) != data->newctx && !sigalg_switch_ctx(s, data)) {
+ data->err = 1;
+ *al = SSL_AD_INTERNAL_ERROR;
+ return SSL_CLIENT_HELLO_ERROR;
+ }
+ return SSL_CLIENT_HELLO_SUCCESS;
+}
+
+static int sigalg_switch_cert_cb(SSL *s, void *arg)
+{
+ struct sigalg_ctx_switch_st *data = arg;
+
+ if (SSL_get_SSL_CTX(s) != data->newctx && !sigalg_switch_ctx(s, data)) {
+ data->err = 1;
+ return 0;
+ }
+ return 1;
+}
+
+/*
+ * Build the three contexts these tests need. An SSL_CTX takes its snapshot of
+ * the provider sigalgs when it is created, so |*sctx| is created while the test
+ * provider is not loaded and does not have its sigalgs, and |*newsctx| and
+ * |*cctx| are created after it is loaded and do have them. The client needs
+ * them too, so that it actually offers the codepoints which resolve to the
+ * slots the original context does not have.
+ */
+static int sigalg_ctx_switch_setup(OSSL_PROVIDER **tlsprov, int version,
+ SSL_CTX **sctx, SSL_CTX **newsctx, SSL_CTX **cctx)
+{
+ if (!TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(), NULL,
+ version, version, sctx, NULL, cert, privkey)))
+ return 0;
+
+ if (!TEST_ptr(*tlsprov = OSSL_PROVIDER_load(libctx, "tls-provider")))
+ return 0;
+
+ if (!TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(),
+ TLS_client_method(), version, version, newsctx, cctx, cert,
+ privkey)))
+ return 0;
+
+ /*
+ * The premise of the test is that the replacement context describes more
+ * signature algorithm slots than the original. Check that, rather than let
+ * the test quietly become vacuous if that ever stops being true.
+ */
+ if (!TEST_size_t_lt((*sctx)->cert->ssl_pkey_num,
+ (*newsctx)->cert->ssl_pkey_num))
+ return 0;
+
+ return 1;
+}
+
+/*
+ * |ssl_pkey_num| bounds the loops which index |cert->pkeys| and sizes
+ * |valid_flags|, so it must describe the CERT the connection is actually
+ * using, not the one it was created from.
+ */
+static int sigalg_ctx_switch_check(SSL *serverssl)
+{
+ SSL_CONNECTION *sc;
+
+ if (!TEST_ptr(sc = SSL_CONNECTION_FROM_SSL_ONLY(serverssl)))
+ return 0;
+
+ return TEST_size_t_eq(sc->ssl_pkey_num, sc->cert->ssl_pkey_num);
+}
+
+/*
+ * Switch SSL_CTX from a callback which runs *before* tls1_set_server_sigalgs()
+ * allocates |valid_flags|. This is the route CVE-2026-72897 describes: the
+ * stale |ssl_pkey_num| undersizes that allocation, and tls1_process_sigalgs()
+ * reads and writes past its end for each peer codepoint occupying one of the
+ * replacement context's excess slots. The peer chooses how many such accesses
+ * happen, and where, by choosing which codepoints to offer.
+ *
+ * Both callbacks tested here run before that allocation, but not at the same
+ * point: the client_hello callback runs before the ClientHello extensions have
+ * been parsed at all, whereas the servername callback runs from the "final"
+ * pass over them, after the sigalgs extension has been saved and after session
+ * resumption has been decided. They are therefore checked separately.
+ *
+ * The two slots the test provider adds are TLSv1.3 only, so only the TLSv1.3
+ * cases can index past the end of the buffer; the TLSv1.2 cases pin the
+ * invariant for a version which processes signature algorithms identically.
+ *
+ * Test 0: servername callback, TLSv1.3
+ * Test 1: client_hello callback, TLSv1.3
+ * Test 2: servername callback, TLSv1.2
+ * Test 3: client_hello callback, TLSv1.2
+ */
+static int test_sigalg_ctx_switch(int idx)
+{
+ OSSL_PROVIDER *tlsprov = NULL;
+ SSL_CTX *sctx = NULL, *newsctx = NULL, *cctx = NULL;
+ SSL *serverssl = NULL, *clientssl = NULL;
+ struct sigalg_ctx_switch_st cbdata;
+ int tls13 = idx < 2;
+ int ver, testresult = 0;
+
+#ifdef OSSL_NO_USABLE_TLS1_3
+ if (tls13)
+ return TEST_skip("TLSv1.3 is not available");
+#endif
+#ifdef OPENSSL_NO_TLS1_2
+ if (!tls13)
+ return TEST_skip("TLSv1.2 is not available");
+#endif
+ ver = tls13 ? TLS1_3_VERSION : TLS1_2_VERSION;
+
+ memset(&cbdata, 0, sizeof(cbdata));
+
+ if (!sigalg_ctx_switch_setup(&tlsprov, ver, &sctx, &newsctx, &cctx))
+ goto end;
+
+ cbdata.newctx = newsctx;
+
+ if ((idx & 1) == 0) {
+ if (!TEST_true(SSL_CTX_set_tlsext_servername_callback(sctx,
+ sigalg_switch_sni_cb))
+ || !TEST_true(SSL_CTX_set_tlsext_servername_arg(sctx, &cbdata)))
+ goto end;
+ } else {
+ SSL_CTX_set_client_hello_cb(sctx, sigalg_switch_clienthello_cb,
+ &cbdata);
+ }
+
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl,
+ NULL, NULL)))
+ goto end;
+
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")))
+ goto end;
+
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE)))
+ goto end;
+
+ if (!TEST_int_eq(cbdata.err, 0)
+ || !TEST_int_eq(cbdata.switches, 1)
+ || !sigalg_ctx_switch_check(serverssl))
+ goto end;
+
+ testresult = 1;
+
+end:
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(newsctx);
+ SSL_CTX_free(cctx);
+ OSSL_PROVIDER_unload(tlsprov);
+
+ return testresult;
+}
+
+/*
+ * Switch SSL_CTX from the certificate callback. Nothing prevents an
+ * application doing this, so it has to keep working, and unlike the
+ * servername and client_hello callbacks it runs *after*
+ * tls1_set_server_sigalgs() has sized and filled in |valid_flags| for the
+ * original context. Nothing recomputes the shared signature algorithms later
+ * in the same handshake, so what this checks is that the switch leaves both
+ * the slot count and the flags already derived from the peer consistent with
+ * the context now installed.
+ *
+ * Test 0: TLSv1.3
+ * Test 1: TLSv1.2
+ */
+static int test_sigalg_ctx_switch_cert_cb(int idx)
+{
+ OSSL_PROVIDER *tlsprov = NULL;
+ SSL_CTX *sctx = NULL, *newsctx = NULL, *cctx = NULL;
+ SSL *serverssl = NULL, *clientssl = NULL;
+ struct sigalg_ctx_switch_st cbdata;
+ int tls13 = idx == 0;
+ int ver, testresult = 0;
+
+#ifdef OSSL_NO_USABLE_TLS1_3
+ if (tls13)
+ return TEST_skip("TLSv1.3 is not available");
+#endif
+#ifdef OPENSSL_NO_TLS1_2
+ if (!tls13)
+ return TEST_skip("TLSv1.2 is not available");
+#endif
+ ver = tls13 ? TLS1_3_VERSION : TLS1_2_VERSION;
+
+ memset(&cbdata, 0, sizeof(cbdata));
+
+ if (!sigalg_ctx_switch_setup(&tlsprov, ver, &sctx, &newsctx, &cctx))
+ goto end;
+
+ cbdata.newctx = newsctx;
+ SSL_CTX_set_cert_cb(sctx, sigalg_switch_cert_cb, &cbdata);
+
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl,
+ NULL, NULL))
+ || !TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE)))
+ goto end;
+
+ if (!TEST_int_eq(cbdata.err, 0)
+ || !TEST_int_eq(cbdata.switches, 1)
+ || !sigalg_ctx_switch_check(serverssl))
+ goto end;
+
+ /*
+ * The switch happened after |valid_flags| had been populated, so the
+ * built-in slots must have survived it. Nothing recomputes them after a
+ * context switch, and at TLSv1.2 and above ssl_set_masks() needs them to
+ * enable ECDSA, Ed25519 and Ed448.
+ */
+ if (!TEST_uint_ne(cbdata.preflags, 0)
+ || !TEST_uint_eq(cbdata.postflags, cbdata.preflags))
+ goto end;
+
+ testresult = 1;
+
+end:
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(newsctx);
+ SSL_CTX_free(cctx);
+ OSSL_PROVIDER_unload(tlsprov);
+
+ return testresult;
+}
+
+#ifndef OPENSSL_NO_TLS1_2
+#define SIGALG_SWITCH_SNI1 "first.example"
+#define SIGALG_SWITCH_SNI2 "second.example"
+
+static int sigalg_switch_reneg_sni_cb(SSL *s, int *al, void *arg)
+{
+ const char *name = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
+
+ /* Model an application which picks its SSL_CTX from the name offered */
+ if (name == NULL || strcmp(name, SIGALG_SWITCH_SNI2) != 0)
+ return SSL_TLSEXT_ERR_OK;
+
+ return sigalg_switch_sni_cb(s, al, arg);
+}
+
+/*
+ * Switch SSL_CTX from the servername callback during a *renegotiation*
+ * handshake, the client having offered a different name the second time round.
+ * |valid_flags| is allocated during the initial handshake and is not freed in
+ * between, so this is the case where SSL_set_SSL_CTX() has to replace an
+ * existing buffer rather than merely correct the count a later allocation will
+ * use. Renegotiation is TLSv1.2 and below only.
+ *
+ * The slot count is the only signal here: the two slots the test provider adds
+ * are TLSv1.3 only, so nothing indexes past the end of the reused buffer.
+ * Under a sanitiser this also catches replacing that buffer wrongly -
+ * refreshing |ssl_pkey_num| while keeping the old allocation would make the
+ * memset() in tls1_set_server_sigalgs() overrun it on the renegotiation.
+ */
+static int test_sigalg_ctx_switch_reneg(void)
+{
+ OSSL_PROVIDER *tlsprov = NULL;
+ SSL_CTX *sctx = NULL, *newsctx = NULL, *cctx = NULL;
+ SSL *serverssl = NULL, *clientssl = NULL;
+ SSL_CONNECTION *sc;
+ struct sigalg_ctx_switch_st cbdata;
+ size_t readbytes;
+ char buf[80];
+ int i, testresult = 0;
+
+ memset(&cbdata, 0, sizeof(cbdata));
+
+ if (!sigalg_ctx_switch_setup(&tlsprov, TLS1_2_VERSION, &sctx, &newsctx,
+ &cctx))
+ goto end;
+
+ cbdata.newctx = newsctx;
+
+ if (!TEST_true(SSL_CTX_set_tlsext_servername_callback(sctx,
+ sigalg_switch_reneg_sni_cb))
+ || !TEST_true(SSL_CTX_set_tlsext_servername_arg(sctx, &cbdata)))
+ goto end;
+
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl,
+ NULL, NULL)))
+ goto end;
+
+ /*
+ * The first handshake must not switch, so that |valid_flags| is allocated
+ * and populated for the original context.
+ */
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, SIGALG_SWITCH_SNI1))
+ || !TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE))
+ || !TEST_int_eq(cbdata.switches, 0))
+ goto end;
+
+ if (!TEST_ptr(sc = SSL_CONNECTION_FROM_SSL_ONLY(serverssl))
+ || !TEST_ptr(sc->s3.tmp.valid_flags))
+ goto end;
+
+ /* Now renegotiate, offering the name which does select the other context */
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, SIGALG_SWITCH_SNI2))
+ || !TEST_true(SSL_renegotiate(clientssl))
+ || !TEST_true(SSL_renegotiate_pending(clientssl)))
+ goto end;
+
+ for (i = 0; i < 3; i++) {
+ if (SSL_read_ex(clientssl, buf, sizeof(buf), &readbytes) > 0) {
+ if (!TEST_size_t_eq(readbytes, 0))
+ goto end;
+ } else if (!TEST_int_eq(SSL_get_error(clientssl, 0),
+ SSL_ERROR_WANT_READ)) {
+ goto end;
+ }
+ if (SSL_read_ex(serverssl, buf, sizeof(buf), &readbytes) > 0) {
+ if (!TEST_size_t_eq(readbytes, 0))
+ goto end;
+ } else if (!TEST_int_eq(SSL_get_error(serverssl, 0),
+ SSL_ERROR_WANT_READ)) {
+ goto end;
+ }
+ }
+
+ /*
+ * A resumed renegotiation would skip tls1_set_server_sigalgs() altogether,
+ * so check the renegotiation really was a full handshake.
+ */
+ if (!TEST_false(SSL_renegotiate_pending(clientssl))
+ || !TEST_false(SSL_session_reused(serverssl))
+ || !TEST_int_eq(cbdata.err, 0)
+ || !TEST_int_eq(cbdata.switches, 1)
+ || !sigalg_ctx_switch_check(serverssl))
+ goto end;
+
+ /* The built-in slots must survive the buffer being replaced */
+ if (!TEST_uint_ne(cbdata.preflags, 0)
+ || !TEST_uint_eq(cbdata.postflags, cbdata.preflags))
+ goto end;
+
+ testresult = 1;
+
+end:
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(newsctx);
+ SSL_CTX_free(cctx);
+ OSSL_PROVIDER_unload(tlsprov);
+
+ return testresult;
+}
+#endif /* OPENSSL_NO_TLS1_2 */
+#endif
+
#ifndef OPENSSL_NO_TLS1_2
#define CERT_TYPE_C "\x0" /* TLSEXT_cert_type_x509 */
@@ -17669,6 +18078,13 @@ int setup_tests(void)
ADD_ALL_TESTS(test_pluggable_group, 4);
ADD_ALL_TESTS(test_pluggable_signature, 12);
#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_sigalg_ctx_switch_cert_cb, 2);
+#ifndef OPENSSL_NO_TLS1_2
+ ADD_TEST(test_sigalg_ctx_switch_reneg);
+#endif
+#endif
#ifndef OPENSSL_NO_TLS1_2
ADD_TEST(test_ssl_dup);
ADD_ALL_TESTS(test_session_secret_cb, 2);