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

rzo1 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/storm.git


The following commit(s) were added to refs/heads/master by this push:
     new 1681a12db  Fix TLS client auth socket configuration and modernize 
certificate handling
1681a12db is described below

commit 1681a12dbe581d42dc5d7fcbc7cdfb0f29c0e4f0
Author: Richard Zowalla <[email protected]>
AuthorDate: Thu Apr 16 19:21:31 2026 +0200

     Fix TLS client auth socket configuration and modernize certificate handling
    
     - The SSLServerSocket setup called setWantClientAuth() after 
setNeedClientAuth(),      which per the JSSE specification clears the 
needClientAuth flag. Remove the redundant setWantClientAuth() call so the 
configured client-auth mode is applied correctly.
    
     - Replace deprecated javax.security.cert.X509Certificate / 
getPeerCertificateChain()
        with java.security.cert.X509Certificate / getPeerCertificates()
    
     - Make TTlsWrapProcessor aware of the client-auth configuration so it can
        distinguish encryption-only from mutual-auth mode when handling peer 
certificate lookup failures
    
     - Add regression test for mutual-auth TLS handshake enforcement
---
 .../auth/tls/ReloadableTsslTransportFactory.java   |  1 -
 .../security/auth/tls/TlsTransportPlugin.java      | 29 +++++---
 .../security/auth/TlsTransportPluginTest.java      | 81 ++++++++++++++++++++++
 3 files changed, 101 insertions(+), 10 deletions(-)

diff --git 
a/storm-client/src/jvm/org/apache/storm/security/auth/tls/ReloadableTsslTransportFactory.java
 
b/storm-client/src/jvm/org/apache/storm/security/auth/tls/ReloadableTsslTransportFactory.java
index 0d128a456..34c9534ec 100644
--- 
a/storm-client/src/jvm/org/apache/storm/security/auth/tls/ReloadableTsslTransportFactory.java
+++ 
b/storm-client/src/jvm/org/apache/storm/security/auth/tls/ReloadableTsslTransportFactory.java
@@ -64,7 +64,6 @@ public class ReloadableTsslTransportFactory extends 
TSSLTransportFactory {
 
             serverSocket.setSoTimeout(timeout);
             serverSocket.setNeedClientAuth(clientAuth);
-            serverSocket.setWantClientAuth(clientAuth);
             return new TServerSocket(new 
TServerSocket.ServerSocketTransportArgs().serverSocket(serverSocket).clientTimeout(timeout));
         } catch (Exception e) {
             throw new TTransportException("Could not bind to port " + port, e);
diff --git 
a/storm-client/src/jvm/org/apache/storm/security/auth/tls/TlsTransportPlugin.java
 
b/storm-client/src/jvm/org/apache/storm/security/auth/tls/TlsTransportPlugin.java
index e260c4d0f..211cd7bdc 100644
--- 
a/storm-client/src/jvm/org/apache/storm/security/auth/tls/TlsTransportPlugin.java
+++ 
b/storm-client/src/jvm/org/apache/storm/security/auth/tls/TlsTransportPlugin.java
@@ -16,7 +16,8 @@ import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 import java.net.ServerSocket;
-import java.security.Principal;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
 import java.util.Map;
 import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
@@ -26,7 +27,6 @@ import java.util.concurrent.TimeUnit;
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSocket;
 import javax.security.auth.Subject;
-import javax.security.cert.X509Certificate;
 import org.apache.storm.security.auth.ITransportPlugin;
 import org.apache.storm.security.auth.ReqContext;
 import org.apache.storm.security.auth.SingleUserPrincipal;
@@ -97,7 +97,7 @@ public class TlsTransportPlugin implements ITransportPlugin {
         Integer queueSize = type.getQueueSize(conf);
 
         TThreadPoolServer.Args serverArgs = new 
TThreadPoolServer.Args(serverTransport)
-                .processor(new TTlsWrapProcessor(processor))
+                .processor(new TTlsWrapProcessor(processor, 
type.isClientAuthRequired(conf)))
                 .minWorkerThreads(numWorkerThreads)
                 .maxWorkerThreads(numWorkerThreads)
                 .protocolFactory(new TBinaryProtocol.Factory(false, true));
@@ -130,9 +130,11 @@ public class TlsTransportPlugin implements 
ITransportPlugin {
 
     private static class TTlsWrapProcessor implements TProcessor {
         final TProcessor wrapped;
+        final boolean clientAuthRequired;
 
-        TTlsWrapProcessor(TProcessor wrapped) {
+        TTlsWrapProcessor(TProcessor wrapped, boolean clientAuthRequired) {
             this.wrapped = wrapped;
+            this.clientAuthRequired = clientAuthRequired;
         }
 
         @Override
@@ -144,13 +146,22 @@ public class TlsTransportPlugin implements 
ITransportPlugin {
 
             String principalName = ANONYMOUS_PRINCIPAL_NAME;
             try {
-                for (X509Certificate cert: 
socket.getSession().getPeerCertificateChain()) {
-                    Principal principal = cert.getSubjectDN();
-                    principalName = principal.getName();
-                    break;
+                Certificate[] peers = 
socket.getSession().getPeerCertificates();
+                if (peers.length > 0 && peers[0] instanceof X509Certificate) {
+                    principalName = ((X509Certificate) 
peers[0]).getSubjectX500Principal().getName();
+                } else if (clientAuthRequired) {
+                    throw new TException("TLS peer presented no X.509 
certificate");
                 }
             } catch (SSLPeerUnverifiedException e) {
-                LOG.warn("Client cert is not verified. Set principalName={}.", 
principalName, e);
+                // Encryption-only mode (clientAuthRequired=false): fall 
through with CN=ANONYMOUS.
+                // When client auth IS required this branch is 
belt-and-suspenders against a
+                // misconfigured server socket — fail closed rather than 
assign a default identity.
+                if (clientAuthRequired) {
+                    LOG.warn("Rejecting TLS connection from {}: peer 
certificate not verified",
+                            socket.getInetAddress());
+                    throw new TException("TLS peer not verified", e);
+                }
+                LOG.debug("Client cert not presented; 
clientAuthRequired=false, using {}", principalName);
             }
             LOG.debug("principalName : {} ", principalName);
             ReqContext reqContext = ReqContext.context();
diff --git 
a/storm-client/test/jvm/org/apache/storm/security/auth/TlsTransportPluginTest.java
 
b/storm-client/test/jvm/org/apache/storm/security/auth/TlsTransportPluginTest.java
index 252b7cabf..5e66be3f6 100644
--- 
a/storm-client/test/jvm/org/apache/storm/security/auth/TlsTransportPluginTest.java
+++ 
b/storm-client/test/jvm/org/apache/storm/security/auth/TlsTransportPluginTest.java
@@ -13,13 +13,30 @@
 package org.apache.storm.security.auth;
 
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.security.SecureRandom;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManagerFactory;
 import org.apache.storm.Config;
 import org.apache.storm.generated.Nimbus;
+import org.apache.storm.security.auth.tls.ReloadableTsslTransportFactory;
 import org.apache.storm.security.auth.tls.TlsTransportPlugin;
+import org.apache.storm.thrift.transport.TServerSocket;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -35,6 +52,7 @@ class TlsTransportPluginTest {
     @BeforeEach
     void setUp() {
         tlsTransportPlugin = new TlsTransportPlugin();
+        conf.clear();
         conf.put(Config.STORM_THRIFT_TRANSPORT_PLUGIN, 
TlsTransportPlugin.class.getName());
         handler = mock(Nimbus.Iface.class);
     }
@@ -65,4 +83,67 @@ class TlsTransportPluginTest {
             fail("TLS setup failed: " + e.getMessage());
         }
     }
+
+    /**
+     * Regression test for VULN-14. With {@code 
nimbus.thrift.tls.client.auth.required=true} the
+     * factory must leave the SSLServerSocket in true "need client auth" mode, 
so that JSSE fails
+     * the handshake closed when the client presents no certificate. The 
{@code CN=ANONYMOUS}
+     * fallback branch in {@link TlsTransportPlugin} must therefore be 
unreachable.
+     */
+    @Test
+    void testClientAuthRequiredRejectsUnauthenticatedClient() throws Exception 
{
+        conf.put(Config.NIMBUS_THRIFT_TLS_PORT, 0);
+        conf.put(Config.STORM_THRIFT_TLS_SOCKET_TIMEOUT_MS, 5000);
+        conf.put(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PATH, testDataPath + 
"testKeyStore.jks");
+        conf.put(Config.NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD, 
"testpass");
+        conf.put(Config.NIMBUS_THRIFT_TLS_SERVER_TRUSTSTORE_PATH, testDataPath 
+ "testTrustStore.jks");
+        conf.put(Config.NIMBUS_THRIFT_TLS_SERVER_TRUSTSTORE_PASSWORD, 
"testpass");
+        conf.put(Config.NIMBUS_THRIFT_TLS_CLIENT_AUTH_REQUIRED, true);
+
+        TServerSocket serverTransport = 
ReloadableTsslTransportFactory.getServerSocket(
+                0, 5000, InetAddress.getLoopbackAddress(), type, conf);
+        SSLServerSocket serverSocket = (SSLServerSocket) 
serverTransport.getServerSocket();
+        try {
+            assertTrue(serverSocket.getNeedClientAuth(),
+                    "Factory must leave needClientAuth=true so JSSE fails the 
handshake closed");
+
+            int port = serverSocket.getLocalPort();
+            AtomicReference<Throwable> serverError = new AtomicReference<>();
+            Thread acceptor = new Thread(() -> {
+                try (SSLSocket accepted = (SSLSocket) serverSocket.accept()) {
+                    accepted.setSoTimeout(5000);
+                    accepted.startHandshake();
+                } catch (Throwable t) {
+                    serverError.set(t);
+                }
+            }, "tls-test-acceptor");
+            acceptor.setDaemon(true);
+            acceptor.start();
+
+            // Trust-only client context: no client keystore, so the client 
cannot present a cert.
+            KeyStore ts = KeyStore.getInstance("JKS");
+            try (InputStream in = Files.newInputStream(Paths.get(testDataPath 
+ "testTrustStore.jks"))) {
+                ts.load(in, "testpass".toCharArray());
+            }
+            TrustManagerFactory tmf = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            tmf.init(ts);
+            SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+            sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
+
+            try (SSLSocket client = (SSLSocket) sslContext.getSocketFactory()
+                    .createSocket(InetAddress.getLoopbackAddress(), port)) {
+                client.setSoTimeout(5000);
+                // Depending on timing, JSSE surfaces the server-side 
need-client-auth rejection
+                // as SSLException or as SocketException ("Connection reset") 
— both prove the
+                // handshake failed closed.
+                assertThrows(IOException.class, client::startHandshake);
+            }
+
+            acceptor.join(5000);
+            assertTrue(serverError.get() instanceof IOException,
+                    "Server-side handshake must fail; got: " + 
serverError.get());
+        } finally {
+            serverTransport.close();
+        }
+    }
 }
\ No newline at end of file

Reply via email to