Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
21 changes: 21 additions & 0 deletions openssl-classes/src/main/java/io/netty/internal/tcnative/SSL.java
Original file line number Diff line number Diff line change
Expand Up @@ -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()}.
*
* <p>Returns:
* <ul>
* <li>during a handshake that has already selected a credential, the id of that credential;</li>
* <li>otherwise, the id recorded by the most recent completed handshake, if
* {@link SSLContext#setRecordSelectedCredential(long, boolean)} is enabled;</li>
* <li>{@code 0} when no credential was selected, when the credential came from a legacy API, when recording is
* disabled, or when no handshake has completed yet.</li>
* </ul>
*
* <p>This is a BoringSSL-specific feature.</p>
*
* @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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>This only has an effect when using BoringSSL.</p>
*
* @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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()}.
*
* <p>This is a BoringSSL-specific feature.</p>
*
* @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.
*
Expand Down
19 changes: 19 additions & 0 deletions openssl-dynamic/src/main/c/ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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) },
Expand Down Expand Up @@ -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) }
};

Expand Down
3 changes: 3 additions & 0 deletions openssl-dynamic/src/main/c/ssl_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -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(...).
Expand All @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions openssl-dynamic/src/main/c/sslcontext.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include "ssl_private.h"
#include <stdint.h>
#include "sslcontext.h"
#include "sslcredential.h"
#include "cert_compress.h"

#define SSLCONTEXT_CLASSNAME "io/netty/internal/tcnative/SSLContext"
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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 *);
Expand Down Expand Up @@ -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) },
Expand Down
47 changes: 45 additions & 2 deletions openssl-dynamic/src/main/c/sslcredential.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@


#include "tcn.h"
#include "apr_atomic.h"
#include "ssl_private.h"
#include "sslcredential.h"

Expand All @@ -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


Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) },
Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions openssl-dynamic/src/main/c/sslcredential.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down