You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@geode.apache.org by GitBox <gi...@apache.org> on 2021/09/08 17:59:43 UTC

[GitHub] [geode] upthewaterspout commented on a change in pull request #6826: GEODE-9542: Enable SSL client authentication for Radish

upthewaterspout commented on a change in pull request #6826:
URL: https://github.com/apache/geode/pull/6826#discussion_r704646242



##########
File path: geode-apis-compatible-with-redis/src/distributedTest/java/org/apache/geode/redis/SSLDUnitTest.java
##########
@@ -0,0 +1,301 @@
+/*
+ * 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.
+ */
+
+package org.apache.geode.redis;
+
+import static org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.FileInputStream;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Properties;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManagerFactory;
+
+import io.netty.handler.ssl.NotSslRecordException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import org.apache.geode.cache.ssl.CertStores;
+import org.apache.geode.cache.ssl.CertificateBuilder;
+import org.apache.geode.cache.ssl.CertificateMaterial;
+import org.apache.geode.internal.net.filewatch.PollingFileWatcher;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class SSLDUnitTest {
+
+  @Rule
+  public RedisClusterStartupRule cluster = new RedisClusterStartupRule();
+
+  private static String commonPassword = "password";
+  private static SANCapturingHostnameVerifier hostnameVerifier = new SANCapturingHostnameVerifier();
+
+  private int redisPort;
+  private CertificateMaterial ca;
+  private String serverKeyStoreFilename;
+  private String serverTrustStoreFilename;
+
+  @Before
+  public void setup() throws Exception {
+    ca = new CertificateBuilder()
+        .commonName("Test CA")
+        .isCA()
+        .generate();
+
+    CertificateMaterial serverCertificate = new CertificateBuilder()
+        .commonName("server")
+        .issuedBy(ca)
+        .generate();
+
+    CertStores serverStore = CertStores.serverStore();
+    serverStore.withCertificate("server", serverCertificate);
+    serverStore.trust("ca", ca);
+    Properties serverProperties = serverStore.propertiesWith("all", true, false);
+
+    MemberVM server = cluster.startRedisVM(0, x -> x.withProperties(serverProperties));
+
+    redisPort = cluster.getRedisPort(server);
+    serverKeyStoreFilename = serverProperties.getProperty("ssl-keystore");
+    serverTrustStoreFilename = serverProperties.getProperty("ssl-truststore");
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientCanConnect() throws Exception {
+    try (Jedis jedis = createClient(true, false)) {
+      assertThat(jedis.ping()).isEqualTo("PONG");
+    }
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientErrorsWithoutKeystore() throws Exception {
+    IgnoredException.addIgnoredException(SSLHandshakeException.class);
+    IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+    Jedis jedis;
+    try {
+      // Create the client without a keystore
+      jedis = createClient(false, false);
+    } catch (JedisConnectionException ignored) {
+      return;
+    }
+
+    // Sometimes the client is created successfully - perhaps this is platform/JDK specific
+    assertThatThrownBy(jedis::ping).satisfiesAnyOf(
+        e -> assertThat(e.getMessage()).contains("SocketException"),
+        e -> assertThat(e.getMessage()).contains("SSLException"),
+        e -> assertThat(e.getMessage()).contains("SSLHandshakeException"));
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientErrorsWithSelfSignedCert() throws Exception {
+    IgnoredException.addIgnoredException(SSLHandshakeException.class);
+    IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+    Jedis jedis;
+    try {
+      // Create the client with a self-signed certificate
+      jedis = createClient(true, true);
+    } catch (JedisConnectionException ignored) {
+      return;
+    }
+
+    // Sometimes the client is created successfully - perhaps this is platform/JDK specific

Review comment:
       You might be able to write these tests a little more cleanly if you just wrap the both `createClient` and `ping` in one single assertThatThrownBy.

##########
File path: geode-apis-compatible-with-redis/src/distributedTest/java/org/apache/geode/redis/SSLDUnitTest.java
##########
@@ -0,0 +1,301 @@
+/*
+ * 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.
+ */
+
+package org.apache.geode.redis;
+
+import static org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.FileInputStream;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Properties;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManagerFactory;
+
+import io.netty.handler.ssl.NotSslRecordException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import org.apache.geode.cache.ssl.CertStores;
+import org.apache.geode.cache.ssl.CertificateBuilder;
+import org.apache.geode.cache.ssl.CertificateMaterial;
+import org.apache.geode.internal.net.filewatch.PollingFileWatcher;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class SSLDUnitTest {
+
+  @Rule
+  public RedisClusterStartupRule cluster = new RedisClusterStartupRule();
+
+  private static String commonPassword = "password";
+  private static SANCapturingHostnameVerifier hostnameVerifier = new SANCapturingHostnameVerifier();
+
+  private int redisPort;
+  private CertificateMaterial ca;
+  private String serverKeyStoreFilename;
+  private String serverTrustStoreFilename;
+
+  @Before
+  public void setup() throws Exception {
+    ca = new CertificateBuilder()
+        .commonName("Test CA")
+        .isCA()
+        .generate();
+
+    CertificateMaterial serverCertificate = new CertificateBuilder()
+        .commonName("server")
+        .issuedBy(ca)
+        .generate();
+
+    CertStores serverStore = CertStores.serverStore();
+    serverStore.withCertificate("server", serverCertificate);
+    serverStore.trust("ca", ca);
+    Properties serverProperties = serverStore.propertiesWith("all", true, false);
+
+    MemberVM server = cluster.startRedisVM(0, x -> x.withProperties(serverProperties));
+
+    redisPort = cluster.getRedisPort(server);
+    serverKeyStoreFilename = serverProperties.getProperty("ssl-keystore");
+    serverTrustStoreFilename = serverProperties.getProperty("ssl-truststore");
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientCanConnect() throws Exception {
+    try (Jedis jedis = createClient(true, false)) {
+      assertThat(jedis.ping()).isEqualTo("PONG");
+    }
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientErrorsWithoutKeystore() throws Exception {
+    IgnoredException.addIgnoredException(SSLHandshakeException.class);
+    IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+    Jedis jedis;
+    try {
+      // Create the client without a keystore
+      jedis = createClient(false, false);
+    } catch (JedisConnectionException ignored) {
+      return;
+    }
+
+    // Sometimes the client is created successfully - perhaps this is platform/JDK specific
+    assertThatThrownBy(jedis::ping).satisfiesAnyOf(
+        e -> assertThat(e.getMessage()).contains("SocketException"),
+        e -> assertThat(e.getMessage()).contains("SSLException"),
+        e -> assertThat(e.getMessage()).contains("SSLHandshakeException"));
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientErrorsWithSelfSignedCert() throws Exception {
+    IgnoredException.addIgnoredException(SSLHandshakeException.class);
+    IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+    Jedis jedis;
+    try {
+      // Create the client with a self-signed certificate
+      jedis = createClient(true, true);
+    } catch (JedisConnectionException ignored) {
+      return;
+    }
+
+    // Sometimes the client is created successfully - perhaps this is platform/JDK specific
+    assertThatThrownBy(jedis::ping).satisfiesAnyOf(
+        e -> assertThat(e.getMessage()).contains("SocketException"),
+        e -> assertThat(e.getMessage()).contains("SSLException"),
+        e -> assertThat(e.getMessage()).contains("SSLHandshakeException"));
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenSslEnabled_clientErrors_whenUsingCleartext() {
+    IgnoredException.addIgnoredException(NotSslRecordException.class);
+
+    try (Jedis jedis = new Jedis(BIND_ADDRESS, redisPort)) {
+      assertThatThrownBy(jedis::ping)
+          .isInstanceOf(JedisConnectionException.class);
+    }
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenServerCertificateIsRotated_clientCanStillConnect() throws Exception {
+    String newServerName = "updated-server";
+
+    try (Jedis jedis = createClient(true, false)) {
+      assertThat(jedis.ping()).isEqualTo("PONG");
+    }
+
+    // create a new certificate for the server
+    CertificateMaterial serverCertificate = new CertificateBuilder()
+        .commonName(newServerName)
+        .issuedBy(ca)
+        .sanDnsName(newServerName)
+        .generate();
+
+    CertStores serverStore = CertStores.serverStore();
+    serverStore.withCertificate("server", serverCertificate);
+    serverStore.trust("ca", ca);
+
+    // Wait for one second since file timestamp granularity may only be seconds depending on the
+    // platform.
+    GeodeAwaitility.await().during(Duration.ofSeconds(1)).until(() -> true);

Review comment:
       Maybe just a Thread.sleep?

##########
File path: geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/NettyRedisServer.java
##########
@@ -180,29 +181,47 @@ public void initChannel(SocketChannel socketChannel) {
 
   private void addSSLIfEnabled(SocketChannel ch, ChannelPipeline p) {
 
-    SSLConfig sslConfigForComponent =
+    SSLConfig sslConfigForServer =
         SSLConfigurationFactory.getSSLConfigForComponent(configSupplier.get(),
             SecurableCommunicationChannel.SERVER);
 
-    if (!sslConfigForComponent.isEnabled()) {
+    if (!sslConfigForServer.isEnabled()) {
       return;
     }
 
+    if (sslConfigForServer.getKeystore() == null) {
+      throw new IllegalStateException(
+          "Cannot start netty as no key manager is configured. Please ensure that the GemFire property 'ssl-keystore' is set.");
+    }
+
     SslContext sslContext;
-    try (FileInputStream fileInputStream =
-        new FileInputStream(sslConfigForComponent.getKeystore())) {
-      KeyStore ks = KeyStore.getInstance("JKS");
-      ks.load(fileInputStream, sslConfigForComponent.getKeystorePassword().toCharArray());
-      // Set up key manager factory to use our key store
-      KeyManagerFactory kmf =
-          KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
-      kmf.init(ks, sslConfigForComponent.getKeystorePassword().toCharArray());
-
-      SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(kmf);
-      sslContext = sslContextBuilder.build();
+    try {
+      KeyManagerFactory keyManagerFactory = new KeyManagerFactoryWrapper(
+          FileWatchingX509ExtendedKeyManager.newFileWatchingKeyManager(sslConfigForServer));
+
+      TrustManagerFactory trustManagerFactory = null;
+      if (sslConfigForServer.getTruststore() != null) {
+        trustManagerFactory = new TrustManagerFactoryWrapper(
+            FileWatchingX509ExtendedTrustManager.newFileWatchingTrustManager(sslConfigForServer));
+      }
+
+      SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(keyManagerFactory);
+      sslContextBuilder.trustManager(trustManagerFactory);
 
-    } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | IOException
-        | CertificateException e) {
+      if (!sslConfigForServer.isAnyCiphers()) {
+        sslContextBuilder.ciphers(Arrays.asList(sslConfigForServer.getCiphersAsStringArray()));
+      }
+
+      if (!sslConfigForServer.isAnyProtocols()) {
+        sslContextBuilder.protocols(
+            Arrays.asList(sslConfigForServer.getProtocolsAsStringArray()));
+      }
+
+      if (sslConfigForServer.isRequireAuth()) {
+        sslContextBuilder.clientAuth(ClientAuth.REQUIRE);
+      }
+      sslContext = sslContextBuilder.build();
+    } catch (IOException e) {
       throw new RuntimeException(e);

Review comment:
       I know this wasn't originally your code - but maybe this method should just throw a IOException. Wrapping in a generic RuntimeException seems a bit suspect.

##########
File path: geode-apis-compatible-with-redis/src/distributedTest/java/org/apache/geode/redis/SSLDUnitTest.java
##########
@@ -0,0 +1,301 @@
+/*
+ * 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.
+ */
+
+package org.apache.geode.redis;
+
+import static org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.FileInputStream;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Properties;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManagerFactory;
+
+import io.netty.handler.ssl.NotSslRecordException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import org.apache.geode.cache.ssl.CertStores;
+import org.apache.geode.cache.ssl.CertificateBuilder;
+import org.apache.geode.cache.ssl.CertificateMaterial;
+import org.apache.geode.internal.net.filewatch.PollingFileWatcher;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class SSLDUnitTest {
+
+  @Rule
+  public RedisClusterStartupRule cluster = new RedisClusterStartupRule();
+
+  private static String commonPassword = "password";
+  private static SANCapturingHostnameVerifier hostnameVerifier = new SANCapturingHostnameVerifier();
+
+  private int redisPort;
+  private CertificateMaterial ca;
+  private String serverKeyStoreFilename;
+  private String serverTrustStoreFilename;
+
+  @Before
+  public void setup() throws Exception {
+    ca = new CertificateBuilder()
+        .commonName("Test CA")
+        .isCA()
+        .generate();
+
+    CertificateMaterial serverCertificate = new CertificateBuilder()
+        .commonName("server")
+        .issuedBy(ca)
+        .generate();
+
+    CertStores serverStore = CertStores.serverStore();
+    serverStore.withCertificate("server", serverCertificate);
+    serverStore.trust("ca", ca);
+    Properties serverProperties = serverStore.propertiesWith("all", true, false);
+
+    MemberVM server = cluster.startRedisVM(0, x -> x.withProperties(serverProperties));
+
+    redisPort = cluster.getRedisPort(server);
+    serverKeyStoreFilename = serverProperties.getProperty("ssl-keystore");
+    serverTrustStoreFilename = serverProperties.getProperty("ssl-truststore");
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientCanConnect() throws Exception {
+    try (Jedis jedis = createClient(true, false)) {
+      assertThat(jedis.ping()).isEqualTo("PONG");
+    }
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientErrorsWithoutKeystore() throws Exception {
+    IgnoredException.addIgnoredException(SSLHandshakeException.class);
+    IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+    Jedis jedis;
+    try {
+      // Create the client without a keystore
+      jedis = createClient(false, false);
+    } catch (JedisConnectionException ignored) {
+      return;
+    }
+
+    // Sometimes the client is created successfully - perhaps this is platform/JDK specific
+    assertThatThrownBy(jedis::ping).satisfiesAnyOf(
+        e -> assertThat(e.getMessage()).contains("SocketException"),
+        e -> assertThat(e.getMessage()).contains("SSLException"),
+        e -> assertThat(e.getMessage()).contains("SSLHandshakeException"));
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientErrorsWithSelfSignedCert() throws Exception {
+    IgnoredException.addIgnoredException(SSLHandshakeException.class);
+    IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+    Jedis jedis;
+    try {
+      // Create the client with a self-signed certificate
+      jedis = createClient(true, true);
+    } catch (JedisConnectionException ignored) {
+      return;
+    }
+
+    // Sometimes the client is created successfully - perhaps this is platform/JDK specific
+    assertThatThrownBy(jedis::ping).satisfiesAnyOf(
+        e -> assertThat(e.getMessage()).contains("SocketException"),
+        e -> assertThat(e.getMessage()).contains("SSLException"),
+        e -> assertThat(e.getMessage()).contains("SSLHandshakeException"));
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenSslEnabled_clientErrors_whenUsingCleartext() {
+    IgnoredException.addIgnoredException(NotSslRecordException.class);
+
+    try (Jedis jedis = new Jedis(BIND_ADDRESS, redisPort)) {
+      assertThatThrownBy(jedis::ping)
+          .isInstanceOf(JedisConnectionException.class);
+    }
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenServerCertificateIsRotated_clientCanStillConnect() throws Exception {
+    String newServerName = "updated-server";
+
+    try (Jedis jedis = createClient(true, false)) {
+      assertThat(jedis.ping()).isEqualTo("PONG");
+    }
+
+    // create a new certificate for the server
+    CertificateMaterial serverCertificate = new CertificateBuilder()
+        .commonName(newServerName)
+        .issuedBy(ca)
+        .sanDnsName(newServerName)
+        .generate();
+
+    CertStores serverStore = CertStores.serverStore();
+    serverStore.withCertificate("server", serverCertificate);
+    serverStore.trust("ca", ca);
+
+    // Wait for one second since file timestamp granularity may only be seconds depending on the
+    // platform.
+    GeodeAwaitility.await().during(Duration.ofSeconds(1)).until(() -> true);
+    serverStore.createKeyStore(serverKeyStoreFilename, commonPassword);
+
+    // Try long enough for the file change to be detected
+    GeodeAwaitility.await().atMost(Duration.ofSeconds(PollingFileWatcher.PERIOD_SECONDS * 2))

Review comment:
       I think maybe we need to up this duration (20 seconds) to make sure this test isn't flaky in CI?

##########
File path: geode-apis-compatible-with-redis/src/main/java/org/apache/geode/redis/internal/netty/NettyRedisServer.java
##########
@@ -180,29 +181,47 @@ public void initChannel(SocketChannel socketChannel) {
 
   private void addSSLIfEnabled(SocketChannel ch, ChannelPipeline p) {
 
-    SSLConfig sslConfigForComponent =
+    SSLConfig sslConfigForServer =
         SSLConfigurationFactory.getSSLConfigForComponent(configSupplier.get(),
             SecurableCommunicationChannel.SERVER);
 
-    if (!sslConfigForComponent.isEnabled()) {
+    if (!sslConfigForServer.isEnabled()) {
       return;
     }
 
+    if (sslConfigForServer.getKeystore() == null) {
+      throw new IllegalStateException(
+          "Cannot start netty as no key manager is configured. Please ensure that the GemFire property 'ssl-keystore' is set.");
+    }
+
     SslContext sslContext;
-    try (FileInputStream fileInputStream =
-        new FileInputStream(sslConfigForComponent.getKeystore())) {
-      KeyStore ks = KeyStore.getInstance("JKS");
-      ks.load(fileInputStream, sslConfigForComponent.getKeystorePassword().toCharArray());
-      // Set up key manager factory to use our key store
-      KeyManagerFactory kmf =
-          KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
-      kmf.init(ks, sslConfigForComponent.getKeystorePassword().toCharArray());
-
-      SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(kmf);
-      sslContext = sslContextBuilder.build();
+    try {
+      KeyManagerFactory keyManagerFactory = new KeyManagerFactoryWrapper(
+          FileWatchingX509ExtendedKeyManager.newFileWatchingKeyManager(sslConfigForServer));
+
+      TrustManagerFactory trustManagerFactory = null;
+      if (sslConfigForServer.getTruststore() != null) {
+        trustManagerFactory = new TrustManagerFactoryWrapper(
+            FileWatchingX509ExtendedTrustManager.newFileWatchingTrustManager(sslConfigForServer));
+      }
+
+      SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(keyManagerFactory);
+      sslContextBuilder.trustManager(trustManagerFactory);
 
-    } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | IOException
-        | CertificateException e) {
+      if (!sslConfigForServer.isAnyCiphers()) {
+        sslContextBuilder.ciphers(Arrays.asList(sslConfigForServer.getCiphersAsStringArray()));
+      }
+
+      if (!sslConfigForServer.isAnyProtocols()) {
+        sslContextBuilder.protocols(
+            Arrays.asList(sslConfigForServer.getProtocolsAsStringArray()));

Review comment:
       I wonder if we should add some tests that our redis server is honoring the ciphers and protocols that the user is specifying. 

##########
File path: geode-apis-compatible-with-redis/src/distributedTest/java/org/apache/geode/redis/SSLDUnitTest.java
##########
@@ -0,0 +1,301 @@
+/*
+ * 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.
+ */
+
+package org.apache.geode.redis;
+
+import static org.apache.geode.test.dunit.rules.RedisClusterStartupRule.BIND_ADDRESS;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.io.FileInputStream;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Properties;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManagerFactory;
+
+import io.netty.handler.ssl.NotSslRecordException;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import org.apache.geode.cache.ssl.CertStores;
+import org.apache.geode.cache.ssl.CertificateBuilder;
+import org.apache.geode.cache.ssl.CertificateMaterial;
+import org.apache.geode.internal.net.filewatch.PollingFileWatcher;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class SSLDUnitTest {
+
+  @Rule
+  public RedisClusterStartupRule cluster = new RedisClusterStartupRule();
+
+  private static String commonPassword = "password";
+  private static SANCapturingHostnameVerifier hostnameVerifier = new SANCapturingHostnameVerifier();
+
+  private int redisPort;
+  private CertificateMaterial ca;
+  private String serverKeyStoreFilename;
+  private String serverTrustStoreFilename;
+
+  @Before
+  public void setup() throws Exception {
+    ca = new CertificateBuilder()
+        .commonName("Test CA")
+        .isCA()
+        .generate();
+
+    CertificateMaterial serverCertificate = new CertificateBuilder()
+        .commonName("server")
+        .issuedBy(ca)
+        .generate();
+
+    CertStores serverStore = CertStores.serverStore();
+    serverStore.withCertificate("server", serverCertificate);
+    serverStore.trust("ca", ca);
+    Properties serverProperties = serverStore.propertiesWith("all", true, false);
+
+    MemberVM server = cluster.startRedisVM(0, x -> x.withProperties(serverProperties));
+
+    redisPort = cluster.getRedisPort(server);
+    serverKeyStoreFilename = serverProperties.getProperty("ssl-keystore");
+    serverTrustStoreFilename = serverProperties.getProperty("ssl-truststore");
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientCanConnect() throws Exception {
+    try (Jedis jedis = createClient(true, false)) {
+      assertThat(jedis.ping()).isEqualTo("PONG");
+    }
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientErrorsWithoutKeystore() throws Exception {
+    IgnoredException.addIgnoredException(SSLHandshakeException.class);
+    IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+    Jedis jedis;
+    try {
+      // Create the client without a keystore
+      jedis = createClient(false, false);
+    } catch (JedisConnectionException ignored) {
+      return;
+    }
+
+    // Sometimes the client is created successfully - perhaps this is platform/JDK specific
+    assertThatThrownBy(jedis::ping).satisfiesAnyOf(
+        e -> assertThat(e.getMessage()).contains("SocketException"),
+        e -> assertThat(e.getMessage()).contains("SSLException"),
+        e -> assertThat(e.getMessage()).contains("SSLHandshakeException"));
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenMutualAuthentication_clientErrorsWithSelfSignedCert() throws Exception {
+    IgnoredException.addIgnoredException(SSLHandshakeException.class);
+    IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+    Jedis jedis;
+    try {
+      // Create the client with a self-signed certificate
+      jedis = createClient(true, true);
+    } catch (JedisConnectionException ignored) {
+      return;
+    }
+
+    // Sometimes the client is created successfully - perhaps this is platform/JDK specific
+    assertThatThrownBy(jedis::ping).satisfiesAnyOf(
+        e -> assertThat(e.getMessage()).contains("SocketException"),
+        e -> assertThat(e.getMessage()).contains("SSLException"),
+        e -> assertThat(e.getMessage()).contains("SSLHandshakeException"));
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenSslEnabled_clientErrors_whenUsingCleartext() {
+    IgnoredException.addIgnoredException(NotSslRecordException.class);
+
+    try (Jedis jedis = new Jedis(BIND_ADDRESS, redisPort)) {
+      assertThatThrownBy(jedis::ping)
+          .isInstanceOf(JedisConnectionException.class);
+    }
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  @Test
+  public void givenServerCertificateIsRotated_clientCanStillConnect() throws Exception {
+    String newServerName = "updated-server";
+
+    try (Jedis jedis = createClient(true, false)) {
+      assertThat(jedis.ping()).isEqualTo("PONG");
+    }
+
+    // create a new certificate for the server
+    CertificateMaterial serverCertificate = new CertificateBuilder()
+        .commonName(newServerName)
+        .issuedBy(ca)
+        .sanDnsName(newServerName)
+        .generate();
+
+    CertStores serverStore = CertStores.serverStore();
+    serverStore.withCertificate("server", serverCertificate);
+    serverStore.trust("ca", ca);
+
+    // Wait for one second since file timestamp granularity may only be seconds depending on the
+    // platform.
+    GeodeAwaitility.await().during(Duration.ofSeconds(1)).until(() -> true);
+    serverStore.createKeyStore(serverKeyStoreFilename, commonPassword);
+
+    // Try long enough for the file change to be detected
+    GeodeAwaitility.await().atMost(Duration.ofSeconds(PollingFileWatcher.PERIOD_SECONDS * 2))
+        .untilAsserted(() -> {
+          try (Jedis jedis = createClient(true, false)) {
+            jedis.ping();
+            assertThat(hostnameVerifier.getSubjectAltNames()).contains(newServerName);
+          }
+        });
+  }
+
+  @Test
+  public void givenServerCAandKeyIsRotated_clientCannotConnect() throws Exception {
+    try (Jedis jedis = createClient(true, false)) {
+      assertThat(jedis.ping()).isEqualTo("PONG");
+    }
+
+    CertificateMaterial newCA = new CertificateBuilder()
+        .commonName("New Test CA")
+        .isCA()
+        .generate();
+
+    CertificateMaterial serverCertificate = new CertificateBuilder()
+        .commonName("server")
+        .issuedBy(newCA)
+        .generate();
+
+    CertStores serverStore = CertStores.serverStore();
+    serverStore.withCertificate("server", serverCertificate);
+    serverStore.trust("ca", newCA);
+
+    // Wait for one second since file timestamp granularity may only be seconds depending on the
+    // platform.
+    GeodeAwaitility.await().during(Duration.ofSeconds(1)).until(() -> true);
+    serverStore.createKeyStore(serverKeyStoreFilename, commonPassword);
+    serverStore.createTrustStore(serverTrustStoreFilename, commonPassword);
+
+    IgnoredException.addIgnoredException(SSLHandshakeException.class);
+    IgnoredException.addIgnoredException("SunCertPathBuilderException");
+
+    // Try long enough for the file change to be detected
+    GeodeAwaitility.await().atMost(Duration.ofSeconds(PollingFileWatcher.PERIOD_SECONDS * 2))
+        .untilAsserted(() -> {
+          assertThatThrownBy(() -> createClient(true, false))
+              .isInstanceOf(JedisConnectionException.class);
+        });
+
+    IgnoredException.removeAllExpectedExceptions();
+  }
+
+  private Jedis createClient(boolean mutualAuthentication, boolean isSelfSigned) throws Exception {

Review comment:
       I didn't see `isSelfSigned` being set to true anywhere in the test.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org