1313package org .apache .storm .security .auth ;
1414
1515
16+ import java .io .IOException ;
17+ import java .io .InputStream ;
18+ import java .net .InetAddress ;
19+ import java .nio .file .Files ;
20+ import java .nio .file .Paths ;
21+ import java .security .KeyStore ;
22+ import java .security .SecureRandom ;
1623import java .util .HashMap ;
1724import java .util .Map ;
25+ import java .util .concurrent .atomic .AtomicReference ;
26+ import javax .net .ssl .SSLContext ;
27+ import javax .net .ssl .SSLException ;
28+ import javax .net .ssl .SSLHandshakeException ;
29+ import javax .net .ssl .SSLServerSocket ;
30+ import javax .net .ssl .SSLSocket ;
31+ import javax .net .ssl .TrustManagerFactory ;
1832import org .apache .storm .Config ;
1933import org .apache .storm .generated .Nimbus ;
34+ import org .apache .storm .security .auth .tls .ReloadableTsslTransportFactory ;
2035import org .apache .storm .security .auth .tls .TlsTransportPlugin ;
36+ import org .apache .storm .thrift .transport .TServerSocket ;
2137import static org .junit .jupiter .api .Assertions .assertEquals ;
2238import static org .junit .jupiter .api .Assertions .assertThrows ;
39+ import static org .junit .jupiter .api .Assertions .assertTrue ;
2340import static org .junit .jupiter .api .Assertions .fail ;
2441import org .junit .jupiter .api .BeforeEach ;
2542import org .junit .jupiter .api .Test ;
@@ -35,6 +52,7 @@ class TlsTransportPluginTest {
3552 @ BeforeEach
3653 void setUp () {
3754 tlsTransportPlugin = new TlsTransportPlugin ();
55+ conf .clear ();
3856 conf .put (Config .STORM_THRIFT_TRANSPORT_PLUGIN , TlsTransportPlugin .class .getName ());
3957 handler = mock (Nimbus .Iface .class );
4058 }
@@ -65,4 +83,67 @@ void testValidTlsSetup() {
6583 fail ("TLS setup failed: " + e .getMessage ());
6684 }
6785 }
86+
87+ /**
88+ * Regression test for VULN-14. With {@code nimbus.thrift.tls.client.auth.required=true} the
89+ * factory must leave the SSLServerSocket in true "need client auth" mode, so that JSSE fails
90+ * the handshake closed when the client presents no certificate. The {@code CN=ANONYMOUS}
91+ * fallback branch in {@link TlsTransportPlugin} must therefore be unreachable.
92+ */
93+ @ Test
94+ void testClientAuthRequiredRejectsUnauthenticatedClient () throws Exception {
95+ conf .put (Config .NIMBUS_THRIFT_TLS_PORT , 0 );
96+ conf .put (Config .STORM_THRIFT_TLS_SOCKET_TIMEOUT_MS , 5000 );
97+ conf .put (Config .NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PATH , testDataPath + "testKeyStore.jks" );
98+ conf .put (Config .NIMBUS_THRIFT_TLS_SERVER_KEYSTORE_PASSWORD , "testpass" );
99+ conf .put (Config .NIMBUS_THRIFT_TLS_SERVER_TRUSTSTORE_PATH , testDataPath + "testTrustStore.jks" );
100+ conf .put (Config .NIMBUS_THRIFT_TLS_SERVER_TRUSTSTORE_PASSWORD , "testpass" );
101+ conf .put (Config .NIMBUS_THRIFT_TLS_CLIENT_AUTH_REQUIRED , true );
102+
103+ TServerSocket serverTransport = ReloadableTsslTransportFactory .getServerSocket (
104+ 0 , 5000 , InetAddress .getLoopbackAddress (), type , conf );
105+ SSLServerSocket serverSocket = (SSLServerSocket ) serverTransport .getServerSocket ();
106+ try {
107+ assertTrue (serverSocket .getNeedClientAuth (),
108+ "Factory must leave needClientAuth=true so JSSE fails the handshake closed" );
109+
110+ int port = serverSocket .getLocalPort ();
111+ AtomicReference <Throwable > serverError = new AtomicReference <>();
112+ Thread acceptor = new Thread (() -> {
113+ try (SSLSocket accepted = (SSLSocket ) serverSocket .accept ()) {
114+ accepted .setSoTimeout (5000 );
115+ accepted .startHandshake ();
116+ } catch (Throwable t ) {
117+ serverError .set (t );
118+ }
119+ }, "tls-test-acceptor" );
120+ acceptor .setDaemon (true );
121+ acceptor .start ();
122+
123+ // Trust-only client context: no client keystore, so the client cannot present a cert.
124+ KeyStore ts = KeyStore .getInstance ("JKS" );
125+ try (InputStream in = Files .newInputStream (Paths .get (testDataPath + "testTrustStore.jks" ))) {
126+ ts .load (in , "testpass" .toCharArray ());
127+ }
128+ TrustManagerFactory tmf = TrustManagerFactory .getInstance (TrustManagerFactory .getDefaultAlgorithm ());
129+ tmf .init (ts );
130+ SSLContext sslContext = SSLContext .getInstance ("TLSv1.2" );
131+ sslContext .init (null , tmf .getTrustManagers (), new SecureRandom ());
132+
133+ try (SSLSocket client = (SSLSocket ) sslContext .getSocketFactory ()
134+ .createSocket (InetAddress .getLoopbackAddress (), port )) {
135+ client .setSoTimeout (5000 );
136+ // Depending on timing, JSSE surfaces the server-side need-client-auth rejection
137+ // as SSLException or as SocketException ("Connection reset") — both prove the
138+ // handshake failed closed.
139+ assertThrows (IOException .class , client ::startHandshake );
140+ }
141+
142+ acceptor .join (5000 );
143+ assertTrue (serverError .get () instanceof IOException ,
144+ "Server-side handshake must fail; got: " + serverError .get ());
145+ } finally {
146+ serverTransport .close ();
147+ }
148+ }
68149}
0 commit comments