2424import static org .junit .jupiter .api .Assertions .fail ;
2525import java .io .BufferedReader ;
2626import java .io .File ;
27+ import java .io .FileInputStream ;
2728import java .io .IOException ;
2829import java .io .InputStreamReader ;
2930import java .net .HttpURLConnection ;
3031import java .net .SocketException ;
3132import java .net .URL ;
3233import java .nio .file .Path ;
3334import java .security .GeneralSecurityException ;
35+ import java .security .KeyStore ;
36+ import java .security .KeyStoreException ;
37+ import java .security .NoSuchAlgorithmException ;
3438import java .security .Security ;
39+ import java .security .UnrecoverableKeyException ;
40+ import java .security .cert .CertificateException ;
3541import java .security .cert .X509Certificate ;
3642import javax .net .ssl .HostnameVerifier ;
3743import javax .net .ssl .HttpsURLConnection ;
44+ import javax .net .ssl .KeyManager ;
45+ import javax .net .ssl .KeyManagerFactory ;
3846import javax .net .ssl .SSLContext ;
47+ import javax .net .ssl .SSLHandshakeException ;
3948import javax .net .ssl .SSLSession ;
49+ import javax .net .ssl .SSLSocket ;
50+ import javax .net .ssl .SSLSocketFactory ;
4051import javax .net .ssl .TrustManager ;
52+ import javax .net .ssl .TrustManagerFactory ;
4153import javax .net .ssl .X509TrustManager ;
4254import org .apache .zookeeper .PortAssignment ;
4355import org .apache .zookeeper .ZKTestCase ;
@@ -65,6 +77,9 @@ public class JettyAdminServerTest extends ZKTestCase {
6577 static final String URL_FORMAT = "http://localhost:%d/commands" ;
6678 static final String HTTPS_URL_FORMAT = "https://localhost:%d/commands" ;
6779 private final int jettyAdminPort = PortAssignment .unique ();
80+ private static final String KEYSTORE_TYPE_JKS = "JKS" ;
81+ private String keyStorePath ;
82+ private String trustStorePath ;
6883
6984 @ BeforeEach
7085 public void enableServer () {
@@ -85,6 +100,8 @@ public void setupEncryption(@TempDir File tempDir) {
85100 .setTrustStorePassword ("" )
86101 .setTrustStoreKeyType (X509KeyType .EC )
87102 .build ();
103+ keyStorePath = x509TestContext .getKeyStoreFile (KeyStoreFileType .JKS ).getAbsolutePath ();
104+ trustStorePath = x509TestContext .getTrustStoreFile (KeyStoreFileType .JKS ).getAbsolutePath ();
88105 System .setProperty (
89106 "zookeeper.ssl.quorum.keyStore.location" ,
90107 x509TestContext .getKeyStoreFile (KeyStoreFileType .PEM ).getAbsolutePath ());
@@ -148,6 +165,8 @@ public void cleanUp() {
148165 System .clearProperty ("zookeeper.ssl.quorum.trustStore.password" );
149166 System .clearProperty ("zookeeper.ssl.quorum.trustStore.passwordPath" );
150167 System .clearProperty ("zookeeper.ssl.quorum.trustStore.type" );
168+ System .clearProperty ("zookeeper.ssl.quorum.ciphersuites" );
169+ System .clearProperty ("zookeeper.ssl.quorum.enabledProtocols" );
151170 System .clearProperty ("zookeeper.admin.portUnification" );
152171 System .clearProperty ("zookeeper.admin.forceHttps" );
153172 }
@@ -306,6 +325,116 @@ private void queryAdminServer(String urlStr, boolean encrypted) throws IOExcepti
306325 assertTrue (line .length () > 0 );
307326 }
308327
328+ @ Test
329+ public void testHandshakeWithSupportedProtocol () throws Exception {
330+ System .setProperty ("zookeeper.admin.forceHttps" , "true" );
331+ System .setProperty ("zookeeper.ssl.quorum.enabledProtocols" , "TLSv1.3" );
332+
333+ JettyAdminServer server = new JettyAdminServer ();
334+ try {
335+ server .start ();
336+
337+ // Use a raw SSLSocket to verify the handshake
338+ SSLContext sslContext = createSSLContext (keyStorePath , "" .toCharArray (), trustStorePath , "TLSv1.3" );
339+ SSLSocketFactory factory = sslContext .getSocketFactory ();
340+
341+ try (SSLSocket socket = (SSLSocket ) factory .createSocket ("localhost" , jettyAdminPort )) {
342+ socket .startHandshake ();
343+ String negotiatedProtocol = socket .getSession ().getProtocol ();
344+
345+ // Verify that we actually landed on the protocol we expected
346+ assertEquals ("TLSv1.3" , negotiatedProtocol ,
347+ "The negotiated protocol should be TLSv1.3." );
348+ }
349+ } finally {
350+ server .shutdown ();
351+ }
352+ }
353+
354+ @ Test
355+ public void testHandshakeWithUnsupportedProtocolFails () throws Exception {
356+ System .setProperty ("zookeeper.admin.forceHttps" , "true" );
357+ System .setProperty ("zookeeper.ssl.quorum.enabledProtocols" , "TLSv1.3" );
358+
359+ JettyAdminServer server = new JettyAdminServer ();
360+ try {
361+ server .start ();
362+
363+ SSLContext sslContext = createSSLContext (keyStorePath , "" .toCharArray (), trustStorePath , "TLSv1.1" );
364+ SSLSocketFactory factory = sslContext .getSocketFactory ();
365+
366+ try (SSLSocket socket = (SSLSocket ) factory .createSocket ("localhost" , jettyAdminPort )) {
367+ SSLHandshakeException exception = assertThrows (SSLHandshakeException .class , socket ::startHandshake );
368+ assertEquals (
369+ "No appropriate protocol (protocol is disabled or cipher suites are inappropriate)" ,
370+ exception .getMessage (),
371+ "The handshake should have failed due to a protocol mismatch." );
372+ }
373+ } finally {
374+ server .shutdown ();
375+ }
376+ }
377+
378+ @ Test
379+ public void testCipherMismatchFails () throws Exception {
380+ System .setProperty ("zookeeper.admin.forceHttps" , "true" );
381+ System .setProperty ("zookeeper.ssl.quorum.ciphersuites" , "TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384" );
382+
383+ JettyAdminServer server = new JettyAdminServer ();
384+ try {
385+ server .start ();
386+
387+ SSLContext sslContext = createSSLContext (keyStorePath , "" .toCharArray (), trustStorePath , "TLSv1.2" );
388+ SSLSocketFactory factory = sslContext .getSocketFactory ();
389+
390+ try (SSLSocket socket = (SSLSocket ) factory .createSocket ("localhost" , jettyAdminPort )) {
391+ // Force the client to use a cipher NOT enabled for the AdminServer
392+ String [] unsupportedCiphers = new String []{"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" };
393+ socket .setEnabledCipherSuites (unsupportedCiphers );
394+
395+ assertThrows (SSLHandshakeException .class , socket ::startHandshake ,
396+ "The handshake should have failed due to a cipher mismatch." );
397+ }
398+ } finally {
399+ server .shutdown ();
400+ }
401+ }
402+
403+ private SSLContext createSSLContext (String keystorePath , char [] password , String trustStorePath , String protocol )
404+ throws Exception {
405+ KeyManager [] keyManagers = getKeyManagers (keystorePath , password );
406+ TrustManager [] trustManagers = getTrustManagers (trustStorePath , password );
407+
408+ SSLContext sslContext = SSLContext .getInstance (protocol );
409+ sslContext .init (keyManagers , trustManagers , null );
410+
411+ return sslContext ;
412+ }
413+
414+ private static KeyManager [] getKeyManagers (String keystorePath , char [] password ) throws KeyStoreException ,
415+ IOException , NoSuchAlgorithmException , CertificateException , UnrecoverableKeyException {
416+ KeyStore keyStore = KeyStore .getInstance (KEYSTORE_TYPE_JKS );
417+ try (FileInputStream fis = new FileInputStream (keystorePath )) {
418+ keyStore .load (fis , password );
419+ }
420+
421+ KeyManagerFactory kmf = KeyManagerFactory .getInstance (KeyManagerFactory .getDefaultAlgorithm ());
422+ kmf .init (keyStore , password );
423+ return kmf .getKeyManagers ();
424+ }
425+
426+ public TrustManager [] getTrustManagers (String trustStorePath , char [] password ) throws Exception {
427+ KeyStore trustStore = KeyStore .getInstance (KEYSTORE_TYPE_JKS );
428+ try (FileInputStream fis = new FileInputStream (trustStorePath )) {
429+ trustStore .load (fis , password );
430+ }
431+
432+ TrustManagerFactory tmf = TrustManagerFactory .getInstance (TrustManagerFactory .getDefaultAlgorithm ());
433+ tmf .init (trustStore );
434+
435+ return tmf .getTrustManagers ();
436+ }
437+
309438 /**
310439 * Using TRACE method to visit admin server
311440 */
0 commit comments