diff --git a/boringssl-static/src/test/java/io/netty/internal/tcnative/SSLCredentialIdTest.java b/boringssl-static/src/test/java/io/netty/internal/tcnative/SSLCredentialIdTest.java new file mode 100644 index 000000000..f7cf24d37 --- /dev/null +++ b/boringssl-static/src/test/java/io/netty/internal/tcnative/SSLCredentialIdTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2026 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.internal.tcnative; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.File; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SSLCredentialIdTest { + + @BeforeAll + public static void loadNativeLib() throws Exception { + String testClassesRoot = SSLCredentialIdTest.class.getProtectionDomain().getCodeSource().getLocation().getFile(); + File[] directories = new File(testClassesRoot + File.separator + "META-INF" + File.separator + "native") + .listFiles(); + if (directories == null || directories.length != 1) { + throw new IllegalStateException("Could not find platform specific native directory"); + } + String libName = System.mapLibraryName("netty_tcnative") + // Fix the filename (this is needed for macOS). + .replace(".dylib", ".jnilib"); + System.load(directories[0].getAbsoluteFile() + File.separator + libName); + Library.initialize(); + } + + @Test + public void newCredentialsHavePositiveDistinctIds() throws Exception { + long x509 = SSLCredential.newX509(); + long delegated = SSLCredential.newDelegated(); + try { + long x509Id = SSLCredential.getId(x509); + long delegatedId = SSLCredential.getId(delegated); + assertTrue(x509Id > 0); + assertTrue(delegatedId > 0); + assertNotEquals(x509Id, delegatedId); + } finally { + SSLCredential.free(x509); + SSLCredential.free(delegated); + } + } + + @Test + public void freshSslHasNoSelectedCredentialId() throws Exception { + long ctx = SSLContext.make(SSL.SSL_PROTOCOL_TLSV1_2, SSL.SSL_MODE_SERVER); + try { + SSLContext.setRecordSelectedCredential(ctx, true); + long ssl = SSL.newSSL(ctx, true); + try { + assertEquals(0, SSL.getSelectedCredentialId(ssl)); + } finally { + SSL.freeSSL(ssl); + } + } finally { + SSLContext.free(ctx); + } + } +} diff --git a/openssl-classes/src/main/java/io/netty/internal/tcnative/SSL.java b/openssl-classes/src/main/java/io/netty/internal/tcnative/SSL.java index f28df2b61..93a818dc2 100644 --- a/openssl-classes/src/main/java/io/netty/internal/tcnative/SSL.java +++ b/openssl-classes/src/main/java/io/netty/internal/tcnative/SSL.java @@ -1004,6 +1004,27 @@ public static AsyncTask getAsyncTask(long ssl) { */ public static native long getSelectedCredential(long ssl) throws Exception; + /** + * Get the id of the credential selected for an SSL instance, as assigned by {@link SSLCredential#newX509()} or + * {@link SSLCredential#newDelegated()}. + * + *

Returns: + *

+ * + *

This is a BoringSSL-specific feature.

+ * + * @param ssl the SSL instance (SSL *) + * @return the selected credential id, or {@code 0} + * @throws Exception if an error occurred + */ + public static native long getSelectedCredentialId(long ssl) throws Exception; + /** * Get the name of the group used by ssl's most recently completed handshake, or {@code null} if not applicable. * diff --git a/openssl-classes/src/main/java/io/netty/internal/tcnative/SSLContext.java b/openssl-classes/src/main/java/io/netty/internal/tcnative/SSLContext.java index 6d5d2e70e..f4a995dff 100644 --- a/openssl-classes/src/main/java/io/netty/internal/tcnative/SSLContext.java +++ b/openssl-classes/src/main/java/io/netty/internal/tcnative/SSLContext.java @@ -683,6 +683,17 @@ public static void setAlpnProtos(long ctx, String[] alpnProtos, int selectorFail */ public static native void setUseTasks(long ctx, boolean useTasks); + /** + * Enable or disable recording the credential selected by each completed handshake, so that it can be obtained + * via {@link SSL#getSelectedCredentialId(long)} after the handshake. Disabled by default. + * + *

This only has an effect when using BoringSSL.

+ * + * @param ctx context to use + * @param record {@code true} to enable, {@code false} to disable. + */ + public static native void setRecordSelectedCredential(long ctx, boolean record); + /** * Adds a certificate compression algorithm to the given {@link SSLContext} or throws an * exception if certificate compression is not supported or the algorithm not recognized. diff --git a/openssl-classes/src/main/java/io/netty/internal/tcnative/SSLCredential.java b/openssl-classes/src/main/java/io/netty/internal/tcnative/SSLCredential.java index c0581085c..0e3dde6fd 100644 --- a/openssl-classes/src/main/java/io/netty/internal/tcnative/SSLCredential.java +++ b/openssl-classes/src/main/java/io/netty/internal/tcnative/SSLCredential.java @@ -46,6 +46,17 @@ private SSLCredential() { } */ public static native long newX509() throws Exception; + /** + * Get the id that was assigned to an SSL_CREDENTIAL when it was created by {@link #newX509()} or + * {@link #newDelegated()}. + * + *

This is a BoringSSL-specific feature.

+ * + * @param cred the SSL_CREDENTIAL instance (SSL_CREDENTIAL *) + * @return the id, or {@code 0} if none was assigned + */ + public static native long getId(long cred); + /** * Increment the reference count of an SSL_CREDENTIAL. * diff --git a/openssl-dynamic/src/main/c/ssl.c b/openssl-dynamic/src/main/c/ssl.c index 3aec22371..2f2b18b38 100644 --- a/openssl-dynamic/src/main/c/ssl.c +++ b/openssl-dynamic/src/main/c/ssl.c @@ -44,6 +44,7 @@ #include "apr_portable.h" #include "ssl_private.h" #include "ssl.h" +#include "sslcredential.h" #define SSL_CLASSNAME "io/netty/internal/tcnative/SSL" @@ -2773,6 +2774,23 @@ TCN_IMPLEMENT_CALL(jlong, SSL, getSelectedCredential)(TCN_STDARGS, jlong ssl) { #endif } +TCN_IMPLEMENT_CALL(jlong, SSL, getSelectedCredentialId)(TCN_STDARGS, jlong ssl) { +#ifdef OPENSSL_IS_BORINGSSL + SSL *ssl_ = J2P(ssl, SSL *); + TCN_CHECK_NULL(ssl_, ssl, 0); + const SSL_CREDENTIAL* credential = SSL_get0_selected_credential(ssl_); + if (credential != NULL) { + // Handshake in progress: report the live selection. + return tcn_SSL_CREDENTIAL_get_id(credential); + } + tcn_ssl_state_t* state = tcn_SSL_get_app_state(ssl_); + return state == NULL ? 0 : state->selected_credential_id; +#else + tcn_ThrowUnsupportedOperationException(e, "SSL_CREDENTIAL API not available."); + return 0; +#endif +} + // JNI Method Registration Table Begin static const JNINativeMethod method_table[] = { { TCN_METHOD_TABLE_ENTRY(bioLengthByteBuffer, (J)I, SSL) }, @@ -2854,6 +2872,7 @@ static const JNINativeMethod method_table[] = { { TCN_METHOD_TABLE_ENTRY(setRenegotiateMode, (JI)V, SSL) }, { TCN_METHOD_TABLE_ENTRY(addCredential, (JJ)V, SSL) }, { TCN_METHOD_TABLE_ENTRY(getSelectedCredential, (J)J, SSL) }, + { TCN_METHOD_TABLE_ENTRY(getSelectedCredentialId, (J)J, SSL) }, { TCN_METHOD_TABLE_ENTRY(getGroupName, (J)Ljava/lang/String;, SSL) } }; diff --git a/openssl-dynamic/src/main/c/ssl_private.h b/openssl-dynamic/src/main/c/ssl_private.h index da52b9148..47e936564 100644 --- a/openssl-dynamic/src/main/c/ssl_private.h +++ b/openssl-dynamic/src/main/c/ssl_private.h @@ -389,6 +389,7 @@ struct tcn_ssl_ctxt_t { unsigned char context_id[SHA_DIGEST_LENGTH]; int use_tasks; + int record_selected_credential; }; // Store the callback to run and also if it was consumed via SSL.getTask(...). @@ -404,6 +405,8 @@ void tcn_ssl_task_free(JNIEnv*, tcn_ssl_task_t*); typedef struct tcn_ssl_state_t tcn_ssl_state_t; struct tcn_ssl_state_t { int handshakeCount; + // Fills the padding before ctx so recording the selected credential adds no per-connection memory. + apr_uint32_t selected_credential_id; tcn_ssl_ctxt_t *ctx; tcn_ssl_task_t* ssl_task; tcn_ssl_verify_config_t verify_config; diff --git a/openssl-dynamic/src/main/c/sslcontext.c b/openssl-dynamic/src/main/c/sslcontext.c index d66538f1c..c96fbd628 100644 --- a/openssl-dynamic/src/main/c/sslcontext.c +++ b/openssl-dynamic/src/main/c/sslcontext.c @@ -37,6 +37,7 @@ #include "ssl_private.h" #include #include "sslcontext.h" +#include "sslcredential.h" #include "cert_compress.h" #define SSLCONTEXT_CLASSNAME "io/netty/internal/tcnative/SSLContext" @@ -189,6 +190,16 @@ static void ssl_info_callback(const SSL *ssl, int where, int ret) { state->handshakeCount++; } } +#ifdef OPENSSL_IS_BORINGSSL + if (0 != (where & SSL_CB_HANDSHAKE_DONE)) { + // BoringSSL frees the handshake state (and its selected credential) before SSL_do_handshake returns, + // so record the selection here. Assign unconditionally so a later handshake without one clears it. + if ((state = tcn_SSL_get_app_state(ssl)) != NULL && state->ctx->record_selected_credential != 0) { + state->selected_credential_id = + (apr_uint32_t) tcn_SSL_CREDENTIAL_get_id(SSL_get0_selected_credential(ssl)); + } + } +#endif } /* Initialize server context */ @@ -2907,6 +2918,14 @@ TCN_IMPLEMENT_CALL(void, SSLContext, setUseTasks)(TCN_STDARGS, jlong ctx, jboole c->use_tasks = useTasks == JNI_TRUE ? 1 : 0; } +TCN_IMPLEMENT_CALL(void, SSLContext, setRecordSelectedCredential)(TCN_STDARGS, jlong ctx, jboolean record) { + tcn_ssl_ctxt_t *c = J2P(ctx, tcn_ssl_ctxt_t *); + + TCN_CHECK_NULL(c, ctx, /* void */); + + c->record_selected_credential = record == JNI_TRUE ? 1 : 0; +} + TCN_IMPLEMENT_CALL(jboolean, SSLContext, setCurvesList0)(TCN_STDARGS, jlong ctx, jstring curves) { tcn_ssl_ctxt_t *c = J2P(ctx, tcn_ssl_ctxt_t *); @@ -3103,6 +3122,7 @@ static const JNINativeMethod fixed_method_table[] = { { TCN_METHOD_TABLE_ENTRY(disableOcsp, (J)V, SSLContext) }, { TCN_METHOD_TABLE_ENTRY(getSslCtx, (J)J, SSLContext) }, { TCN_METHOD_TABLE_ENTRY(setUseTasks, (JZ)V, SSLContext) }, + { TCN_METHOD_TABLE_ENTRY(setRecordSelectedCredential, (JZ)V, SSLContext) }, { TCN_METHOD_TABLE_ENTRY(setNumTickets, (JI)Z, SSLContext) }, { TCN_METHOD_TABLE_ENTRY(setCurvesList0, (JLjava/lang/String;)Z, SSLContext) }, { TCN_METHOD_TABLE_ENTRY(setMaxCertList, (JI)V, SSLContext) }, diff --git a/openssl-dynamic/src/main/c/sslcredential.c b/openssl-dynamic/src/main/c/sslcredential.c index 18f25709c..a7e6b05b8 100644 --- a/openssl-dynamic/src/main/c/sslcredential.c +++ b/openssl-dynamic/src/main/c/sslcredential.c @@ -24,6 +24,7 @@ #include "tcn.h" +#include "apr_atomic.h" #include "ssl_private.h" #include "sslcredential.h" @@ -37,6 +38,30 @@ static void throw_openssl_error(JNIEnv* env, const char* msg) { ERR_error_string_n(err, err_buf, sizeof(err_buf)); tcn_Throw(env, "%s: %s", msg, err_buf); } + +static int tcn_SSL_CREDENTIAL_id_idx = -1; +static volatile apr_uint32_t tcn_SSL_CREDENTIAL_next_id = 0; + +// The id is a scalar packed into the ex_data slot, so no free callback is needed. +jlong tcn_SSL_CREDENTIAL_get_id(const SSL_CREDENTIAL* cred) { + if (cred == NULL || tcn_SSL_CREDENTIAL_id_idx < 0) { + return 0; + } + return (jlong)(uintptr_t) SSL_CREDENTIAL_get_ex_data(cred, tcn_SSL_CREDENTIAL_id_idx); +} + +static SSL_CREDENTIAL* assign_id(JNIEnv* e, SSL_CREDENTIAL* cred) { + apr_uint32_t id; + do { + id = apr_atomic_inc32(&tcn_SSL_CREDENTIAL_next_id) + 1; + } while (id == 0); + if (!SSL_CREDENTIAL_set_ex_data(cred, tcn_SSL_CREDENTIAL_id_idx, (void*)(uintptr_t) id)) { + SSL_CREDENTIAL_free(cred); + throw_openssl_error(e, "Failed to set SSL_CREDENTIAL id"); + return NULL; + } + return cred; +} #endif @@ -46,7 +71,7 @@ TCN_IMPLEMENT_CALL(jlong, SSLCredential, newX509)(TCN_STDARGS) { #ifdef OPENSSL_IS_BORINGSSL SSL_CREDENTIAL* cred = SSL_CREDENTIAL_new_x509(); TCN_CHECK_NULL(cred, credential, 0); - return (jlong)(intptr_t)cred; + return (jlong)(intptr_t)assign_id(e, cred); #else tcn_ThrowUnsupportedOperationException(e, "SSL_CREDENTIAL API not available."); return 0; @@ -74,6 +99,17 @@ TCN_IMPLEMENT_CALL(void, SSLCredential, free)(TCN_STDARGS, jlong cred) { #endif } +TCN_IMPLEMENT_CALL(jlong, SSLCredential, getId)(TCN_STDARGS, jlong cred) { +#ifdef OPENSSL_IS_BORINGSSL + SSL_CREDENTIAL* c = (SSL_CREDENTIAL*)(intptr_t)cred; + TCN_CHECK_NULL(c, credential, 0); + return tcn_SSL_CREDENTIAL_get_id(c); +#else + tcn_ThrowUnsupportedOperationException(e, "SSL_CREDENTIAL API not available."); + return 0; +#endif +} + // SSL_CREDENTIAL configuration methods TCN_IMPLEMENT_CALL(void, SSLCredential, setPrivateKey)(TCN_STDARGS, jlong cred, jlong key) { #ifdef OPENSSL_IS_BORINGSSL @@ -301,7 +337,7 @@ TCN_IMPLEMENT_CALL(jlong, SSLCredential, newDelegated)(TCN_STDARGS) { throw_openssl_error(e, "Failed to create delegated SSL_CREDENTIAL"); return 0; } - return (jlong)(intptr_t)credential; + return (jlong)(intptr_t)assign_id(e, credential); #else tcn_ThrowUnsupportedOperationException(e, "SSL_CREDENTIAL API not available."); return 0; @@ -346,6 +382,7 @@ static const JNINativeMethod method_table[] = { { TCN_METHOD_TABLE_ENTRY(newX509, ()J, SSLCredential) }, { TCN_METHOD_TABLE_ENTRY(upRef, (J)V, SSLCredential) }, { TCN_METHOD_TABLE_ENTRY(free, (J)V, SSLCredential) }, + { TCN_METHOD_TABLE_ENTRY(getId, (J)J, SSLCredential) }, // Configuration { TCN_METHOD_TABLE_ENTRY(setPrivateKey, (JJ)V, SSLCredential) }, @@ -371,6 +408,12 @@ static const jint method_table_size = sizeof(method_table) / sizeof(method_table // IMPORTANT: If you add any NETTY_JNI_UTIL_LOAD_CLASS or NETTY_JNI_UTIL_FIND_CLASS calls you also need to update // Library to reflect that. jint netty_internal_tcnative_SSLCredential_JNI_OnLoad(JNIEnv* env, const char* packagePrefix) { +#ifdef OPENSSL_IS_BORINGSSL + tcn_SSL_CREDENTIAL_id_idx = SSL_CREDENTIAL_get_ex_new_index(0, NULL, NULL, NULL, NULL); + if (tcn_SSL_CREDENTIAL_id_idx < 0) { + return JNI_ERR; + } +#endif if (netty_jni_util_register_natives(env, packagePrefix, SSLCREDENTIAL_CLASSNAME, diff --git a/openssl-dynamic/src/main/c/sslcredential.h b/openssl-dynamic/src/main/c/sslcredential.h index c38eb8e7b..942ab4b4b 100644 --- a/openssl-dynamic/src/main/c/sslcredential.h +++ b/openssl-dynamic/src/main/c/sslcredential.h @@ -27,6 +27,10 @@ extern "C" { jint netty_internal_tcnative_SSLCredential_JNI_OnLoad(JNIEnv* env, const char* packagePrefix); void netty_internal_tcnative_SSLCredential_JNI_OnUnLoad(JNIEnv* env, const char* packagePrefix); +#ifdef OPENSSL_IS_BORINGSSL +jlong tcn_SSL_CREDENTIAL_get_id(const SSL_CREDENTIAL* cred); +#endif + #ifdef __cplusplus } #endif