This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 1de7fac7384401512f89f3a3af96f17c1ab224fd
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Sep 24 18:18:24 2026 +0100

    First pass at TLS 1.3 support
    
    Co-authored-by: GitHub Copilot (GPT-5.6 Sol) <[email protected]>
---
 .../apache/catalina/tribes/group/GroupChannel.java |  22 ++++-
 .../catalina/tribes/group/GroupChannelMBean.java   |   4 +
 .../catalina/tribes/group/TribesSslContext.java    |  60 ++++++++----
 .../catalina/tribes/transport/nio/TlsChannel.java  |   9 +-
 .../tomcat/util/net/openssl/OpenSSLContext.java    |   4 +-
 .../tomcat/util/net/openssl/OpenSSLEngine.java     |   3 +-
 .../net/openssl/OpenSSLPreSharedKeySelector.java   |  30 +++++-
 .../util/net/openssl/panama/OpenSSLContext.java    | 101 ++++++++++++++++++++-
 .../util/net/openssl/panama/OpenSSLEngine.java     |   3 +-
 .../util/openssl/SSL_psk_use_session_cb_func.java  |  64 +++++++++++++
 java/org/apache/tomcat/util/openssl/openssl_h.java |  38 ++++++++
 .../catalina/tribes/group/TestGroupChannelTls.java |   9 +-
 .../tribes/transport/nio/TestTlsChannel.java       |  58 +++++++++---
 webapps/docs/changelog.xml                         |   2 +-
 webapps/docs/config/cluster-channel.xml            |  16 +++-
 15 files changed, 380 insertions(+), 43 deletions(-)

diff --git a/java/org/apache/catalina/tribes/group/GroupChannel.java 
b/java/org/apache/catalina/tribes/group/GroupChannel.java
index 12dabc315e..6a05efb71f 100644
--- a/java/org/apache/catalina/tribes/group/GroupChannel.java
+++ b/java/org/apache/catalina/tribes/group/GroupChannel.java
@@ -125,8 +125,10 @@ public class GroupChannel extends ChannelInterceptorBase 
implements ManagedChann
     protected boolean optionCheck = false;
 
     private boolean secure;
+    private String pskDigest = "SHA256";
     private String pskIdentity;
     private String pskKey;
+    private String pskProtocol = "TLSv1.3";
     private volatile TribesSslContext sslContext;
 
     /**
@@ -461,7 +463,7 @@ public class GroupChannel extends ChannelInterceptorBase 
implements ManagedChann
         if (sslContext == null) {
             if (pskKey != null && pskIdentity != null) {
                 try {
-                    sslContext = new TribesSslContext(pskIdentity, pskKey);
+                    sslContext = new TribesSslContext(pskIdentity, pskKey, 
pskDigest, pskProtocol);
                 } catch (Exception e) {
                     if (secure) {
                         throw new 
ChannelException(sm.getString("groupChannel.tlsUnavailable"), e);
@@ -665,6 +667,15 @@ public class GroupChannel extends ChannelInterceptorBase 
implements ManagedChann
         this.secure = secure;
     }
 
+    @Override
+    public String getPskDigest() {
+        return pskDigest;
+    }
+
+    public void setPskDigest(String pskDigest) {
+        this.pskDigest = pskDigest;
+    }
+
     @Override
     public String getPskIdentity() {
         return pskIdentity;
@@ -678,6 +689,15 @@ public class GroupChannel extends ChannelInterceptorBase 
implements ManagedChann
         this.pskKey = pskKey;
     }
 
+    @Override
+    public String getPskProtocol() {
+        return pskProtocol;
+    }
+
+    public void setPskProtocol(String pskProtocol) {
+        this.pskProtocol = pskProtocol;
+    }
+
     public TribesSslContext getSslContext() {
         return sslContext;
     }
diff --git a/java/org/apache/catalina/tribes/group/GroupChannelMBean.java 
b/java/org/apache/catalina/tribes/group/GroupChannelMBean.java
index bd63930a9c..45e5491312 100644
--- a/java/org/apache/catalina/tribes/group/GroupChannelMBean.java
+++ b/java/org/apache/catalina/tribes/group/GroupChannelMBean.java
@@ -54,8 +54,12 @@ public interface GroupChannelMBean {
 
     boolean getSecure();
 
+    String getPskDigest();
+
     String getPskIdentity();
 
+    String getPskProtocol();
+
     // Operations
     /**
      * Starts the channel with the given service type.
diff --git a/java/org/apache/catalina/tribes/group/TribesSslContext.java 
b/java/org/apache/catalina/tribes/group/TribesSslContext.java
index 4ab59fbd80..527e6e1453 100644
--- a/java/org/apache/catalina/tribes/group/TribesSslContext.java
+++ b/java/org/apache/catalina/tribes/group/TribesSslContext.java
@@ -18,6 +18,7 @@ package org.apache.catalina.tribes.group;
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
 import java.util.Set;
 
 import javax.net.ssl.SSLEngine;
@@ -34,12 +35,12 @@ public class TribesSslContext implements AutoCloseable {
     private final Object serverContext;
     private final boolean ffm;
 
-    public TribesSslContext(String identity, String key) throws Exception {
-        Object[] contexts = createFfmContexts(identity, key);
+    public TribesSslContext(String identity, String key, String digest, String 
protocol) throws Exception {
+        Object[] contexts = createFfmContexts(identity, key, digest, protocol);
         if (contexts != null) {
             ffm = true;
         } else {
-            contexts = createNativeContexts(identity, key);
+            contexts = createNativeContexts(identity, key, digest, protocol);
             ffm = false;
         }
         if (contexts == null) {
@@ -57,7 +58,8 @@ public class TribesSslContext implements AutoCloseable {
         return createEngine(serverContext, false);
     }
 
-    private static Object[] createFfmContexts(String identity, String key) 
throws Exception {
+    private static Object[] createFfmContexts(String identity, String key, 
String digest, String protocol)
+            throws Exception {
         /*
          * Tribes may be used stand-alone so don't assume there is an 
OpenSSLLifecycleListener that Tomcat is already
          * using but do use the library in a manner that is compatible if 
Tomcat is using such an instance.
@@ -74,28 +76,47 @@ public class TribesSslContext implements AutoCloseable {
             library.getMethod("destroy").invoke(null);
             return null;
         }
-        return createContexts(FFM_CONTEXT, identity, key);
+        return createContexts(FFM_CONTEXT, identity, key, digest, protocol);
     }
 
-    private static Object[] createNativeContexts(String identity, String key) 
throws Exception {
+    private static Object[] createNativeContexts(String identity, String key, 
String digest, String protocol)
+            throws Exception {
         /*
          * Tribes may be used stand-alone so don't assume there is an 
AprLifecycleListener that Tomcat is already using
          * but do use the library in a manner that is compatible if Tomcat is 
using such an instance.
          */
-        Class<?> listener = 
Class.forName("org.apache.catalina.core.AprLifecycleListener");
-        listener.getConstructor().newInstance();
-        if (!((Boolean) 
listener.getMethod("isAprAvailable").invoke(null)).booleanValue()) {
+        Class<?> listenerClass = 
Class.forName("org.apache.catalina.core.AprLifecycleListener");
+        Object listener = listenerClass.getConstructor().newInstance();
+        if (!((Boolean) 
listenerClass.getMethod("isAprAvailable").invoke(null)).booleanValue()) {
             return null;
         }
-        return createContexts(NATIVE_CONTEXT, identity, key);
+        /*
+         * isAprAvailable() only performs the lightweight native library load 
check. The OpenSSL-specific
+         * initialization (including the native ex_data index set up by 
SSL.initialize(), which the PSK callbacks
+         * rely on to recover their SSL_CTX-specific state) only happens in 
response to a Lifecycle.BEFORE_INIT_EVENT,
+         * which a standalone Tribes channel never fires. Fire it here so 
OpenSSL is fully initialized before any
+         * SSL_CTX/SSL objects are created.
+         */
+        Class<?> lifecycleClass = 
Class.forName("org.apache.catalina.Lifecycle");
+        Object lifecycleProxy = 
Proxy.newProxyInstance(lifecycleClass.getClassLoader(),
+                new Class<?>[] { lifecycleClass }, (proxy, method, args) -> 
null);
+        Class<?> lifecycleEventClass = 
Class.forName("org.apache.catalina.LifecycleEvent");
+        Constructor<?> eventConstructor =
+                lifecycleEventClass.getConstructor(lifecycleClass, 
String.class, Object.class);
+        Object event = eventConstructor.newInstance(lifecycleProxy,
+                lifecycleClass.getField("BEFORE_INIT_EVENT").get(null), null);
+        Class<?> listenerInterface = 
Class.forName("org.apache.catalina.LifecycleListener");
+        listenerInterface.getMethod("lifecycleEvent", 
lifecycleEventClass).invoke(listener, event);
+        return createContexts(NATIVE_CONTEXT, identity, key, digest, protocol);
     }
 
-    private static Object[] createContexts(String className, String identity, 
String key) throws Exception {
+    private static Object[] createContexts(String className, String identity, 
String key, String digest, String protocol)
+            throws Exception {
         Class<?> clazz = Class.forName(className);
         Class<?> certificateClass = 
Class.forName("org.apache.tomcat.util.net.SSLHostConfigCertificate");
         Constructor<?> constructor = clazz.getConstructor(certificateClass, 
java.util.List.class, boolean.class);
-        Object client = constructor.newInstance(createCertificate(identity, 
key), null, Boolean.TRUE);
-        Object server = constructor.newInstance(createCertificate(identity, 
key), null, Boolean.FALSE);
+        Object client = constructor.newInstance(createCertificate(identity, 
key, digest, protocol), null, Boolean.TRUE);
+        Object server = constructor.newInstance(createCertificate(identity, 
key, digest, protocol), null, Boolean.FALSE);
         Method init = clazz.getMethod("init", 
javax.net.ssl.KeyManager[].class, javax.net.ssl.TrustManager[].class,
                 java.security.SecureRandom.class);
         init.invoke(client, null, null, null);
@@ -103,15 +124,22 @@ public class TribesSslContext implements AutoCloseable {
         return new Object[] { client, server };
     }
 
-    private static Object createCertificate(String identity, String key) 
throws Exception {
+    private static Object createCertificate(String identity, String key, 
String digest, String protocol)
+            throws Exception {
         Class<?> configClass = 
Class.forName("org.apache.tomcat.util.net.SSLHostConfig");
         Object config = configClass.getConstructor().newInstance();
-        configClass.getMethod("setProtocols", String.class).invoke(config, 
"TLSv1.2");
+        configClass.getMethod("setProtocols", String.class).invoke(config, 
protocol);
         configClass.getMethod("setEnabledProtocols", 
String[].class).invoke(config,
-                (Object) new String[] { "TLSv1.2" });
+                (Object) new String[] { protocol });
         configClass.getMethod("setCiphers", String.class).invoke(config, 
"PSK-AES128-GCM-SHA256");
+        if ("TLSv1.3".equals(protocol)) {
+            String cipherSuites = 
"SHA384".equalsIgnoreCase(digest.replace("-", "")) ? "TLS_AES_256_GCM_SHA384" :
+                    "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256";
+            configClass.getMethod("setCipherSuites", 
String.class).invoke(config, cipherSuites);
+        }
         Class<?> pskClass = 
Class.forName("org.apache.tomcat.util.net.SSLHostConfigPreSharedKey");
         Object psk = pskClass.getConstructor(configClass).newInstance(config);
+        pskClass.getMethod("setDigest", String.class).invoke(psk, digest);
         pskClass.getMethod("setIdentity", String.class).invoke(psk, identity);
         pskClass.getMethod("setKey", String.class).invoke(psk, key);
         configClass.getMethod("addPreSharedKey", pskClass).invoke(config, psk);
diff --git a/java/org/apache/catalina/tribes/transport/nio/TlsChannel.java 
b/java/org/apache/catalina/tribes/transport/nio/TlsChannel.java
index 6db5243194..f351b72a72 100644
--- a/java/org/apache/catalina/tribes/transport/nio/TlsChannel.java
+++ b/java/org/apache/catalina/tribes/transport/nio/TlsChannel.java
@@ -68,9 +68,16 @@ final class TlsChannel implements ByteChannel {
                 case NEED_WRAP -> {
                     networkOutput.clear();
                     SSLEngineResult result = engine.wrap(EMPTY, networkOutput);
-                    status = result.getHandshakeStatus();
+                    if (result.getStatus() == Status.BUFFER_OVERFLOW) {
+                        networkOutput = expand(networkOutput, 
engine.getSession().getPacketBufferSize());
+                        continue;
+                    }
                     networkOutput.flip();
                     writeFully(networkOutput);
+                    if (result.getStatus() == Status.CLOSED) {
+                        throw new EOFException();
+                    }
+                    status = result.getHandshakeStatus();
                     continue;
                 }
                 case NEED_UNWRAP, NEED_UNWRAP_AGAIN -> {
diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java 
b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
index 2834dc67d7..efc814ffb3 100644
--- a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
+++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
@@ -459,7 +459,9 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
                 for (String protocol : sslHostConfig.getEnabledProtocols()) {
                     if (Constants.SSL_PROTO_TLSv1_2.equals(protocol) && 
clientMode) {
                         SSLContext.setPskClientCallback(state.ctx, selector);
-                    } else if (Constants.SSL_PROTO_TLSv1_2.equals(protocol)) {
+                    } else if (Constants.SSL_PROTO_TLSv1_3.equals(protocol) && 
clientMode) {
+                        SSLContext.setPskUseSessionCallback(state.ctx, 
selector);
+                    } else if (Constants.SSL_PROTO_TLSv1_2.equals(protocol) && 
!clientMode) {
                         SSLContext.setPskServerCallback(state.ctx, selector);
                     } else if (Constants.SSL_PROTO_TLSv1_3.equals(protocol) && 
!clientMode) {
                         SSLContext.setPskFindSessionCallback(state.ctx, 
selector);
diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java 
b/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java
index 1351dc1051..bcb4a6fb88 100644
--- a/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java
+++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java
@@ -1060,7 +1060,8 @@ public final class OpenSSLEngine extends SSLEngine 
implements SSLUtil.ProtocolIn
             // No pending data to be sent to the peer
             // Check to see if we have finished handshaking
             int handshakeCount = SSL.getHandshakeCount(state.ssl);
-            if (handshakeCount != currentHandshake && 
SSL.renegotiatePending(state.ssl) == 0 &&
+            if (handshakeCount != currentHandshake && SSL.isInInit(state.ssl) 
== 0 &&
+                    SSL.renegotiatePending(state.ssl) == 0 &&
                     (SSL.getPostHandshakeAuthInProgress(state.ssl) == 0)) {
                 if (alpn) {
                     selectedProtocol = SSL.getAlpnSelected(state.ssl);
diff --git 
a/java/org/apache/tomcat/util/net/openssl/OpenSSLPreSharedKeySelector.java 
b/java/org/apache/tomcat/util/net/openssl/OpenSSLPreSharedKeySelector.java
index bbc0bd41e6..b7ad9802a3 100644
--- a/java/org/apache/tomcat/util/net/openssl/OpenSSLPreSharedKeySelector.java
+++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLPreSharedKeySelector.java
@@ -76,15 +76,37 @@ public class OpenSSLPreSharedKeySelector implements 
PreSharedKeySelector {
         if (psk == null) {
             return null;
         }
+        if (!setCipherSuite(psk, cipherSuite)) {
+            return null;
+        }
+        // Need to limit keys to 48 bytes for TLS 1.3
+        return truncateToLength(identityString, psk.getKeyInternal(), 48);
+    }
+
+    @Override
+    public byte[] selectClient(long ssl, String[] identity, int[] cipherSuite) 
{
+        if (identityToKeyMap.isEmpty()) {
+            return null;
+        }
+        SSLHostConfigPreSharedKey psk = 
identityToKeyMap.values().iterator().next();
+        if (!setCipherSuite(psk, cipherSuite)) {
+            return null;
+        }
+        identity[0] = psk.getIdentity();
+        // Need to limit keys to 48 bytes for TLS 1.3
+        return truncateToLength(psk.getIdentity(), psk.getKeyInternal(), 48);
+    }
+
+    private static boolean setCipherSuite(SSLHostConfigPreSharedKey psk, int[] 
cipherSuite) {
         switch (psk.getDigestInternal()) {
             case SHA256:
                 // Any TLS ciphersuite that OpenSSL recognises that uses 
SHA256 works here
                 cipherSuite[0] = 0x1301;
-                break;
+                return true;
             case SHA384:
                 // Any TLS ciphersuite that OpenSSL recognises that uses 
SHA384 works here
                 cipherSuite[0] = 0x1302;
-                break;
+                return true;
             case AEAD:
             case GOST89MAC:
             case GOST94:
@@ -92,10 +114,8 @@ public class OpenSSLPreSharedKeySelector implements 
PreSharedKeySelector {
             case SHA1:
             default:
                 // Unsupported digest
-                return null;
+                return false;
         }
-        // Need to limit keys to 48 bytes for TLS 1.3
-        return truncateToLength(identityString, psk.getKeyInternal(), 48);
     }
 
 
diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java 
b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
index 7f0a3fe757..445de98dda 100644
--- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
+++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
@@ -36,7 +36,9 @@ import java.util.Arrays;
 import java.util.Base64;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import javax.net.ssl.KeyManager;
 import javax.net.ssl.SSLEngine;
@@ -76,6 +78,7 @@ import 
org.apache.tomcat.util.openssl.SSL_CTX_set_verify$callback;
 import org.apache.tomcat.util.openssl.SSL_psk_client_cb_func;
 import org.apache.tomcat.util.openssl.SSL_psk_find_session_cb_func;
 import org.apache.tomcat.util.openssl.SSL_psk_server_cb_func;
+import org.apache.tomcat.util.openssl.SSL_psk_use_session_cb_func;
 import org.apache.tomcat.util.openssl.openssl_h;
 import org.apache.tomcat.util.openssl.openssl_h_Compatibility;
 import org.apache.tomcat.util.openssl.pem_password_cb;
@@ -661,7 +664,13 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
                     if (Constants.SSL_PROTO_TLSv1_2.equals(protocol) && 
clientMode) {
                         SSL_CTX_set_psk_client_callback(state.sslCtx,
                                 SSL_psk_client_cb_func.allocate(new 
PskClientCallback(selector), contextArena));
-                    } else if (Constants.SSL_PROTO_TLSv1_2.equals(protocol)) {
+                    } else if (Constants.SSL_PROTO_TLSv1_3.equals(protocol) && 
clientMode) {
+                        if (openssl_h_Compatibility.LIBRESSL) {
+                            throw new 
SSLException(sm.getString("openssl.pskTls13Unsupported"));
+                        }
+                        SSL_CTX_set_psk_use_session_callback(state.sslCtx, 
SSL_psk_use_session_cb_func
+                                .allocate(new PskUseSessionCallback(selector, 
contextArena), contextArena));
+                    } else if (Constants.SSL_PROTO_TLSv1_2.equals(protocol) && 
!clientMode) {
                         SSL_CTX_set_psk_server_callback(state.sslCtx, 
SSL_psk_server_cb_func
                                 .allocate(new PskServerCallback(selector), 
contextArena));
                     } else if (Constants.SSL_PROTO_TLSv1_3.equals(protocol) && 
!clientMode) {
@@ -928,14 +937,94 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
                 try {
                     MemorySegment keySegment = 
localArena.allocateFrom(ValueLayout.JAVA_BYTE, key);
                     try {
+                        MemorySegment sidCtxSegment =
+                                localArena.allocateFrom(ValueLayout.JAVA_BYTE, 
DEFAULT_SESSION_ID_CONTEXT);
+                        if (SSL_SESSION_set1_master_key(session, keySegment, 
key.length) == 0 ||
+                                SSL_SESSION_set_cipher(session, cipher) == 0 ||
+                                SSL_SESSION_set_protocol_version(session, 
TLS1_3_VERSION()) == 0 ||
+                                SSL_SESSION_set1_id_context(session, 
sidCtxSegment,
+                                        DEFAULT_SESSION_ID_CONTEXT.length) == 
0) {
+                            return 0;
+                        }
+                    } finally {
+                        keySegment.fill((byte) 0);
+                    }
+                    sessionPointerSegment.set(ValueLayout.ADDRESS, 0, session);
+                    success = true;
+                    return 1;
+                } finally {
+                    if (!success) {
+                        SSL_SESSION_free(session);
+                    }
+                }
+            }
+        }
+    }
+
+    private static class PskUseSessionCallback implements 
SSL_psk_use_session_cb_func.Function {
+
+        private final OpenSSLPreSharedKeySelector selector;
+        private final Arena contextArena;
+        private final Map<String,ClientIdentity> identities = new 
ConcurrentHashMap<>();
+
+        PskUseSessionCallback(OpenSSLPreSharedKeySelector selector, Arena 
contextArena) {
+            this.selector = selector;
+            this.contextArena = contextArena;
+        }
+
+        @Override
+        public int apply(MemorySegment ssl, MemorySegment md, MemorySegment 
identity, MemorySegment identityLength,
+                MemorySegment sessionPointer) {
+            try (var localArena = Arena.ofConfined()) {
+                MemorySegment sessionPointerSegment =
+                        
sessionPointer.reinterpret(ValueLayout.ADDRESS.byteSize(), localArena, null);
+                sessionPointerSegment.set(ValueLayout.ADDRESS, 0, 
MemorySegment.NULL);
+
+                String[] selectedIdentity = new String[1];
+                int[] cipherSuite = new int[1];
+                byte[] key = selector.selectClient(ssl.address(), 
selectedIdentity, cipherSuite);
+                if (key == null || selectedIdentity[0] == null) {
+                    return 1;
+                }
+                if (key.length == 0 || cipherSuite[0] <= 0 || cipherSuite[0] > 
0xFFFF) {
+                    return 0;
+                }
+
+                byte[] cipherId = new byte[] { (byte) (cipherSuite[0] >> 8), 
(byte) cipherSuite[0] };
+                MemorySegment cipher =
+                        SSL_CIPHER_find(ssl, 
localArena.allocateFrom(ValueLayout.JAVA_BYTE, cipherId));
+                if (MemorySegment.NULL.equals(cipher)
+                        || 
!Constants.SSL_PROTO_TLSv1_3.equals(SSL_CIPHER_get_version(cipher).getString(0)))
 {
+                    return 0;
+                }
+
+                MemorySegment session = SSL_SESSION_new();
+                if (MemorySegment.NULL.equals(session)) {
+                    return 0;
+                }
+                boolean success = false;
+                try {
+                    MemorySegment keySegment = 
localArena.allocateFrom(ValueLayout.JAVA_BYTE, key);
+                    try {
+                        MemorySegment sidCtxSegment =
+                                localArena.allocateFrom(ValueLayout.JAVA_BYTE, 
DEFAULT_SESSION_ID_CONTEXT);
                         if (SSL_SESSION_set1_master_key(session, keySegment, 
key.length) == 0 ||
                                 SSL_SESSION_set_cipher(session, cipher) == 0 ||
-                                SSL_SESSION_set_protocol_version(session, 
TLS1_3_VERSION()) == 0) {
+                                SSL_SESSION_set_protocol_version(session, 
TLS1_3_VERSION()) == 0 ||
+                                SSL_SESSION_set1_id_context(session, 
sidCtxSegment,
+                                        DEFAULT_SESSION_ID_CONTEXT.length) == 
0) {
                             return 0;
                         }
                     } finally {
                         keySegment.fill((byte) 0);
                     }
+
+                    ClientIdentity clientIdentity =
+                            identities.computeIfAbsent(selectedIdentity[0], 
this::createIdentity);
+                    identity.reinterpret(ValueLayout.ADDRESS.byteSize(), 
localArena, null)
+                            .set(ValueLayout.ADDRESS, 0, 
clientIdentity.value());
+                    
identityLength.reinterpret(ValueLayout.JAVA_LONG.byteSize(), localArena, null)
+                            .set(ValueLayout.JAVA_LONG, 0, 
clientIdentity.length());
                     sessionPointerSegment.set(ValueLayout.ADDRESS, 0, session);
                     success = true;
                     return 1;
@@ -946,6 +1035,14 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
                 }
             }
         }
+
+        private ClientIdentity createIdentity(String identity) {
+            byte[] bytes = identity.getBytes(StandardCharsets.UTF_8);
+            return new ClientIdentity(contextArena.allocateFrom(identity), 
bytes.length);
+        }
+    }
+
+    private record ClientIdentity(MemorySegment value, long length) {
     }
 
 
diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java 
b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java
index b7534bebd2..4f40283470 100644
--- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java
+++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLEngine.java
@@ -1031,7 +1031,8 @@ public final class OpenSSLEngine extends SSLEngine 
implements SSLUtil.ProtocolIn
 
             // No pending data to be sent to the peer
             // Check to see if we have finished handshaking
-            if (state.handshakeCount != currentHandshake && 
SSL_renegotiate_pending(state.ssl) == 0 &&
+            if (state.handshakeCount != currentHandshake && 
SSL_in_init(state.ssl) == 0 &&
+                    SSL_renegotiate_pending(state.ssl) == 0 &&
                     (state.phaState != PHAState.START)) {
                 if (alpn) {
                     selectedProtocol = getProtocolNegotiated();
diff --git 
a/java/org/apache/tomcat/util/openssl/SSL_psk_use_session_cb_func.java 
b/java/org/apache/tomcat/util/openssl/SSL_psk_use_session_cb_func.java
new file mode 100644
index 0000000000..78a56e276f
--- /dev/null
+++ b/java/org/apache/tomcat/util/openssl/SSL_psk_use_session_cb_func.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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.
+ */
+
+// Generated by jextract
+
+package org.apache.tomcat.util.openssl;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.FunctionDescriptor;
+import java.lang.foreign.Linker;
+import java.lang.foreign.MemorySegment;
+import java.lang.invoke.MethodHandle;
+
+/**
+ * {@snippet lang = c : * SSL_psk_use_session_cb_func cb
+ * }
+ */
+@SuppressWarnings("javadoc")
+public class SSL_psk_use_session_cb_func {
+
+    public interface Function {
+        int apply(MemorySegment ssl, MemorySegment md, MemorySegment identity, 
MemorySegment identityLength,
+                MemorySegment session);
+    }
+
+    private static final FunctionDescriptor $DESC = 
FunctionDescriptor.of(openssl_h.C_INT, openssl_h.C_POINTER,
+            openssl_h.C_POINTER, openssl_h.C_POINTER, openssl_h.C_POINTER, 
openssl_h.C_POINTER);
+
+    public static FunctionDescriptor descriptor() {
+        return $DESC;
+    }
+
+    private static final MethodHandle UP$MH =
+            openssl_h.upcallHandle(SSL_psk_use_session_cb_func.Function.class, 
"apply", $DESC);
+
+    public static MemorySegment allocate(SSL_psk_use_session_cb_func.Function 
fi, Arena scope) {
+        return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, 
scope);
+    }
+
+    private static final MethodHandle DOWN$MH = 
Linker.nativeLinker().downcallHandle($DESC);
+
+    public static int invoke(MemorySegment funcPtr, MemorySegment ssl, 
MemorySegment md, MemorySegment identity,
+            MemorySegment identityLength, MemorySegment session) {
+        try {
+            return (int) DOWN$MH.invokeExact(funcPtr, ssl, md, identity, 
identityLength, session);
+        } catch (Throwable ex$) {
+            throw new AssertionError("should not reach here", ex$);
+        }
+    }
+}
diff --git a/java/org/apache/tomcat/util/openssl/openssl_h.java 
b/java/org/apache/tomcat/util/openssl/openssl_h.java
index 7b05180d57..36265aeef9 100644
--- a/java/org/apache/tomcat/util/openssl/openssl_h.java
+++ b/java/org/apache/tomcat/util/openssl/openssl_h.java
@@ -10322,6 +10322,25 @@ public class openssl_h {
         }
     }
 
+    private static class SSL_CTX_set_psk_use_session_callback {
+        public static final FunctionDescriptor DESC =
+                FunctionDescriptor.ofVoid(openssl_h.C_POINTER, 
openssl_h.C_POINTER);
+        public static final MemorySegment ADDR = 
openssl_h.findOrThrow("SSL_CTX_set_psk_use_session_callback");
+        public static final MethodHandle HANDLE = 
Linker.nativeLinker().downcallHandle(ADDR, DESC);
+    }
+
+    public static void SSL_CTX_set_psk_use_session_callback(MemorySegment ctx, 
MemorySegment callback) {
+        var mh$ = SSL_CTX_set_psk_use_session_callback.HANDLE;
+        try {
+            if (TRACE_DOWNCALLS) {
+                traceDowncall("SSL_CTX_set_psk_use_session_callback", ctx, 
callback);
+            }
+            mh$.invokeExact(ctx, callback);
+        } catch (Throwable ex$) {
+            throw new AssertionError("should not reach here", ex$);
+        }
+    }
+
     private static class SSL_SESSION_new {
         public static final FunctionDescriptor DESC = 
FunctionDescriptor.of(openssl_h.C_POINTER);
         public static final MemorySegment ADDR = 
openssl_h.findOrThrow("SSL_SESSION_new");
@@ -10377,6 +10396,25 @@ public class openssl_h {
         }
     }
 
+    private static class SSL_SESSION_set1_id_context {
+        public static final FunctionDescriptor DESC = 
FunctionDescriptor.of(openssl_h.C_INT, openssl_h.C_POINTER,
+                openssl_h.C_POINTER, openssl_h.C_INT);
+        public static final MemorySegment ADDR = 
openssl_h.findOrThrow("SSL_SESSION_set1_id_context");
+        public static final MethodHandle HANDLE = 
Linker.nativeLinker().downcallHandle(ADDR, DESC);
+    }
+
+    public static int SSL_SESSION_set1_id_context(MemorySegment session, 
MemorySegment sidCtx, int sidCtxLength) {
+        var mh$ = SSL_SESSION_set1_id_context.HANDLE;
+        try {
+            if (TRACE_DOWNCALLS) {
+                traceDowncall("SSL_SESSION_set1_id_context", session, sidCtx, 
sidCtxLength);
+            }
+            return (int) mh$.invokeExact(session, sidCtx, sidCtxLength);
+        } catch (Throwable ex$) {
+            throw new AssertionError("should not reach here", ex$);
+        }
+    }
+
     private static class SSL_SESSION_set_cipher {
         public static final FunctionDescriptor DESC =
                 FunctionDescriptor.of(openssl_h.C_INT, openssl_h.C_POINTER, 
openssl_h.C_POINTER);
diff --git a/test/org/apache/catalina/tribes/group/TestGroupChannelTls.java 
b/test/org/apache/catalina/tribes/group/TestGroupChannelTls.java
index 01df48af57..8f2f35375e 100644
--- a/test/org/apache/catalina/tribes/group/TestGroupChannelTls.java
+++ b/test/org/apache/catalina/tribes/group/TestGroupChannelTls.java
@@ -32,10 +32,17 @@ import org.apache.catalina.tribes.transport.ReceiverBase;
 
 public class TestGroupChannelTls {
 
+    @Test
+    public void testPskDefaults() {
+        GroupChannel channel = new GroupChannel();
+        Assert.assertEquals("SHA256", channel.getPskDigest());
+        Assert.assertEquals("TLSv1.3", channel.getPskProtocol());
+    }
+
     @Test
     public void testSecureMessage() throws Exception {
         try (@SuppressWarnings("unused") TribesSslContext ignored =
-                new TribesSslContext("tribes-test", 
"000102030405060708090a0b0c0d0e0f")) {
+                new TribesSslContext("tribes-test", 
"000102030405060708090a0b0c0d0e0f", "TLSv1.3", "SHA256")) {
             // Verify that a supported TLS provider is available before 
creating the channels.
         } catch (Exception e) {
             Assume.assumeNoException(e);
diff --git a/test/org/apache/catalina/tribes/transport/nio/TestTlsChannel.java 
b/test/org/apache/catalina/tribes/transport/nio/TestTlsChannel.java
index acf1032d8c..50e0290f5f 100644
--- a/test/org/apache/catalina/tribes/transport/nio/TestTlsChannel.java
+++ b/test/org/apache/catalina/tribes/transport/nio/TestTlsChannel.java
@@ -22,6 +22,9 @@ import java.nio.channels.ServerSocketChannel;
 import java.nio.channels.SocketChannel;
 import java.nio.charset.StandardCharsets;
 import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLEngine;
 
 import org.junit.Assert;
 import org.junit.Assume;
@@ -33,12 +36,27 @@ public class TestTlsChannel {
 
     @Test
     public void testPskRoundTrip() throws Exception {
-        try (TribesSslContext context = createContext();
+        doTestPskRoundTrip(createContext("SHA256", "TLSv1.3"), "TLSv1.3");
+    }
+
+    @Test
+    public void testPskRoundTripTls13Sha384() throws Exception {
+        doTestPskRoundTrip(createContext("SHA384", "TLSv1.3"), "TLSv1.3");
+    }
+
+    @Test
+    public void testPskRoundTripTls12() throws Exception {
+        doTestPskRoundTrip(createContext("SHA256", "TLSv1.2"), "TLSv1.2");
+    }
+
+    private static void doTestPskRoundTrip(TribesSslContext context, String 
protocol) throws Exception {
+        try (context;
                 ServerSocketChannel server = ServerSocketChannel.open()) {
             server.bind(new InetSocketAddress("localhost", 0));
             FutureTask<Void> serverTask = new FutureTask<>(() -> {
-                try (SocketChannel socket = server.accept();
-                        TlsChannel tls = new TlsChannel(socket.socket(), 
context.createServerEngine())) {
+                SSLEngine engine = context.createServerEngine();
+                try (SocketChannel socket = server.accept(); TlsChannel tls = 
new TlsChannel(socket.socket(), engine)) {
+                    
Assert.assertTrue(engine.getSession().getProtocol().startsWith(protocol));
                     Assert.assertEquals("request", read(tls));
                     
tls.write(ByteBuffer.wrap("response".getBytes(StandardCharsets.UTF_8)));
                 }
@@ -46,12 +64,23 @@ public class TestTlsChannel {
             });
             Thread serverThread = new Thread(serverTask);
             serverThread.start();
-            try (SocketChannel socket = 
SocketChannel.open(server.getLocalAddress());
-                    TlsChannel tls = new TlsChannel(socket.socket(), 
context.createClientEngine())) {
-                
tls.write(ByteBuffer.wrap("request".getBytes(StandardCharsets.UTF_8)));
-                Assert.assertEquals("response", read(tls));
+            SSLEngine engine = context.createClientEngine();
+            try {
+                try (SocketChannel socket = 
SocketChannel.open(server.getLocalAddress());
+                        TlsChannel tls = new TlsChannel(socket.socket(), 
engine)) {
+                    
Assert.assertTrue(engine.getSession().getProtocol().startsWith(protocol));
+                    
tls.write(ByteBuffer.wrap("request".getBytes(StandardCharsets.UTF_8)));
+                    Assert.assertEquals("response", read(tls));
+                }
+                serverTask.get();
+            } catch (Exception e) {
+                try {
+                    serverTask.get(5, TimeUnit.SECONDS);
+                } catch (Exception serverException) {
+                    e.addSuppressed(serverException);
+                }
+                throw e;
             }
-            serverTask.get();
         }
     }
 
@@ -87,8 +116,13 @@ public class TestTlsChannel {
     }
 
     private static TribesSslContext createContext() {
+        return createContext("SHA256", "TLSv1.3");
+    }
+
+    private static TribesSslContext createContext(String digest, String 
protocol) {
         try {
-            return new TribesSslContext("tribes-test", 
"000102030405060708090a0b0c0d0e0f");
+            return new TribesSslContext("tribes-test",
+                    
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", digest, 
protocol);
         } catch (Exception e) {
             Assume.assumeNoException(e);
             return null;
@@ -97,7 +131,9 @@ public class TestTlsChannel {
 
     private static String read(TlsChannel channel) throws Exception {
         ByteBuffer buffer = ByteBuffer.allocate(32);
-        channel.read(buffer);
+        while (buffer.position() == 0) {
+            Assert.assertTrue(channel.read(buffer) >= 0);
+        }
         buffer.flip();
         return StandardCharsets.UTF_8.decode(buffer).toString();
     }
@@ -105,7 +141,7 @@ public class TestTlsChannel {
     private static byte[] read(TlsChannel channel, int length) throws 
Exception {
         ByteBuffer buffer = ByteBuffer.allocate(length);
         while (buffer.hasRemaining()) {
-            Assert.assertTrue(channel.read(buffer) > 0);
+            Assert.assertTrue(channel.read(buffer) >= 0);
         }
         return buffer.array();
     }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index e52c43be67..68f3dc9036 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -142,7 +142,7 @@
         (markt)
       </add>
       <add>
-        Add TLS pre-shared key support to Apache Tribes. (markt)
+        Add TLS 1.2 and TLS 1.3 pre-shared key support to Apache Tribes. 
(markt)
       </add>
       <add>
         Add the Jakarta EE 12 XML schemas. (markt)
diff --git a/webapps/docs/config/cluster-channel.xml 
b/webapps/docs/config/cluster-channel.xml
index 146e123e4b..e080a7830d 100644
--- a/webapps/docs/config/cluster-channel.xml
+++ b/webapps/docs/config/cluster-channel.xml
@@ -134,12 +134,24 @@
         <code>pskKey</code> to enable TLS.
       </attribute>
 
+      <attribute name="pskDigest" required="false">
+        The message digest algorithm associated with the pre-shared key for
+        TLS 1.3, either <code>SHA256</code> or <code>SHA384</code>. The default
+        is <code>SHA256</code>. This attribute is ignored for TLS 1.2.
+      </attribute>
+
       <attribute name="pskKey" required="false">
         The TLS pre-shared key encoded as hexadecimal characters. This must be
         configured together with <code>pskIdentity</code>. The key is not
         exposed through JMX. It should be no longer than 512 bytes for TLS 1.2.
-        If the value is truely random then 32 bytes are sufficient for a 
256-bit
-        cipher suite.
+        For TLS 1.3, it must be no longer than 48 bytes. If the value is truely
+        random then 32 bytes are sufficient for a 256-bit cipher suite.
+      </attribute>
+
+      <attribute name="pskProtocol" required="false">
+        The TLS protocol to use with the pre-shared key. The permitted values
+        are <code>TLSv1.2</code> and <code>TLSv1.3</code>. The default is
+        <code>TLSv1.3</code>.
       </attribute>
 
       <attribute name="jmxEnabled" required="false">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to