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]
