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