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

ycai pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 36e16ee3c9 Adding endpoint verification option to 
client_encryption_options
36e16ee3c9 is described below

commit 36e16ee3c911c710129fcf3a69595038c3dbd385
Author: Jyothsna Konisa <[email protected]>
AuthorDate: Mon Nov 14 14:16:07 2022 -0800

    Adding endpoint verification option to client_encryption_options
    
    patch by Jyothsna Konisa; reviewed by Jon Meredith, Yifan Cai for 
CASSANDRA-18034
---
 CHANGES.txt                                        |   1 +
 conf/cassandra.yaml                                |   1 +
 .../org/apache/cassandra/net/SocketFactory.java    |   2 +-
 .../cassandra/transport/PipelineConfigurator.java  |   8 +-
 .../apache/cassandra/transport/SimpleClient.java   |   4 +-
 test/conf/cassandra_ssl_test.truststore            | Bin 3240 -> 5295 bytes
 .../cassandra_ssl_test_endpoint_verify.keystore    | Bin 0 -> 2087 bytes
 .../test/NativeTransportEncryptionOptionsTest.java | 101 +++++++++++++++++++++
 8 files changed, 113 insertions(+), 4 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 0ea3c65c46..0c94126de7 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.2
+ * Adding endpoint verification option to client_encryption_options 
(CASSANDRA-18034)
  * Replace 'wcwidth.py' with pypi module (CASSANDRA-17287)
  * Add nodetool forcecompact to remove tombstoned or ttl'd data ignoring GC 
grace for given table and partition keys (CASSANDRA-17711)
  * Offer IF (NOT) EXISTS in cqlsh completion for CREATE TYPE, DROP TYPE, 
CREATE ROLE and DROP ROLE (CASSANDRA-16640)
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index fbfa468c00..ff074bddd7 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -1394,6 +1394,7 @@ client_encryption_options:
   keystore_password: cassandra
   # Verify client certificates
   require_client_auth: false
+  # require_endpoint_verification: false
   # Set trustore and truststore_password if require_client_auth is true
   # truststore: conf/.truststore
   # truststore_password: cassandra
diff --git a/src/java/org/apache/cassandra/net/SocketFactory.java 
b/src/java/org/apache/cassandra/net/SocketFactory.java
index 33fff6b7a0..b135ed5107 100644
--- a/src/java/org/apache/cassandra/net/SocketFactory.java
+++ b/src/java/org/apache/cassandra/net/SocketFactory.java
@@ -215,7 +215,7 @@ public final class SocketFactory
      * Creates a new {@link SslHandler} from provided SslContext.
      * @param peer enables endpoint verification for remote address when not 
null
      */
-    static SslHandler newSslHandler(Channel channel, SslContext sslContext, 
@Nullable InetSocketAddress peer)
+    public static SslHandler newSslHandler(Channel channel, SslContext 
sslContext, @Nullable InetSocketAddress peer)
     {
         if (peer == null)
             return sslContext.newHandler(channel.alloc());
diff --git a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java 
b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java
index 81ff13605e..bf48eea334 100644
--- a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java
+++ b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java
@@ -48,6 +48,8 @@ import org.apache.cassandra.security.ISslContextFactory;
 import org.apache.cassandra.security.SSLFactory;
 import org.apache.cassandra.transport.messages.StartupMessage;
 
+import static org.apache.cassandra.net.SocketFactory.newSslHandler;
+
 /**
  * Takes care of intializing a Netty Channel and Pipeline for client protocol 
connections.
  * The pipeline is first set up with some common handlers for connection 
limiting, dropping
@@ -181,7 +183,8 @@ public class PipelineConfigurator
                             {
                                 // Connection uses SSL/TLS, replace the 
detection handler with a SslHandler and so use
                                 // encryption.
-                                SslHandler sslHandler = 
sslContext.newHandler(channel.alloc());
+                                InetSocketAddress peer = 
encryptionOptions.require_endpoint_verification ? (InetSocketAddress) 
channel.remoteAddress() : null;
+                                SslHandler sslHandler = newSslHandler(channel, 
sslContext, peer);
                                 
channelHandlerContext.pipeline().replace(SSL_HANDLER, SSL_HANDLER, sslHandler);
                             }
                             else
@@ -199,7 +202,8 @@ public class PipelineConfigurator
                     SslContext sslContext = 
SSLFactory.getOrCreateSslContext(encryptionOptions,
                                                                              
encryptionOptions.require_client_auth,
                                                                              
ISslContextFactory.SocketType.SERVER);
-                    channel.pipeline().addFirst(SSL_HANDLER, 
sslContext.newHandler(channel.alloc()));
+                    InetSocketAddress peer = 
encryptionOptions.require_endpoint_verification ? (InetSocketAddress) 
channel.remoteAddress() : null;
+                    channel.pipeline().addFirst(SSL_HANDLER, 
newSslHandler(channel, sslContext, peer));
                 };
             default:
                 throw new IllegalStateException("Unrecognized TLS encryption 
policy: " + this.tlsEncryptionPolicy);
diff --git a/src/java/org/apache/cassandra/transport/SimpleClient.java 
b/src/java/org/apache/cassandra/transport/SimpleClient.java
index 43bb8addee..2b57d104e4 100644
--- a/src/java/org/apache/cassandra/transport/SimpleClient.java
+++ b/src/java/org/apache/cassandra/transport/SimpleClient.java
@@ -52,6 +52,7 @@ import org.apache.cassandra.security.SSLFactory;
 import org.apache.cassandra.transport.messages.*;
 import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
 
+import static org.apache.cassandra.net.SocketFactory.newSslHandler;
 import static org.apache.cassandra.transport.CQLMessageHandler.envelopeSize;
 import static org.apache.cassandra.transport.Flusher.MAX_FRAMED_PAYLOAD_SIZE;
 import static 
org.apache.cassandra.utils.concurrent.NonBlockingRateLimiter.NO_OP_LIMITER;
@@ -624,7 +625,8 @@ public class SimpleClient implements Closeable
             super.initChannel(channel);
             SslContext sslContext = 
SSLFactory.getOrCreateSslContext(encryptionOptions, 
encryptionOptions.require_client_auth,
                                                                      
ISslContextFactory.SocketType.CLIENT);
-            channel.pipeline().addFirst("ssl", 
sslContext.newHandler(channel.alloc()));
+            InetSocketAddress peer = 
encryptionOptions.require_endpoint_verification ? new InetSocketAddress(host, 
port) : null;
+            channel.pipeline().addFirst("ssl", newSslHandler(channel, 
sslContext, peer));
         }
     }
 
diff --git a/test/conf/cassandra_ssl_test.truststore 
b/test/conf/cassandra_ssl_test.truststore
index 5ba9a9977c..ab01af30cd 100644
Binary files a/test/conf/cassandra_ssl_test.truststore and 
b/test/conf/cassandra_ssl_test.truststore differ
diff --git a/test/conf/cassandra_ssl_test_endpoint_verify.keystore 
b/test/conf/cassandra_ssl_test_endpoint_verify.keystore
new file mode 100644
index 0000000000..951385b263
Binary files /dev/null and 
b/test/conf/cassandra_ssl_test_endpoint_verify.keystore differ
diff --git 
a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java
 
b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java
index c5a810ca25..5f2caaf695 100644
--- 
a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java
+++ 
b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java
@@ -18,18 +18,33 @@
 
 package org.apache.cassandra.distributed.test;
 
+import java.io.FileInputStream;
+import java.io.InputStream;
 import java.net.InetAddress;
+import java.security.KeyStore;
 import java.util.Collections;
 
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.TrustManagerFactory;
+
 import com.google.common.collect.ImmutableMap;
 import org.junit.Assert;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 
+import com.datastax.driver.core.SSLOptions;
+import com.datastax.driver.core.exceptions.NoHostAvailableException;
+import com.datastax.shaded.netty.handler.ssl.SslContext;
+import com.datastax.shaded.netty.handler.ssl.SslContextBuilder;
 import org.apache.cassandra.distributed.Cluster;
 import org.apache.cassandra.distributed.api.Feature;
 
 public class NativeTransportEncryptionOptionsTest extends 
AbstractEncryptionOptionsImpl
 {
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
     @Test
     public void nodeWillNotStartWithBadKeystore() throws Throwable
     {
@@ -219,4 +234,90 @@ public class NativeTransportEncryptionOptionsTest extends 
AbstractEncryptionOpti
             assertCannotStartDueToConfigurationException(cluster);
         }
     }
+
+    @Test
+    public void testEndpointVerificationDisabledIpNotInSAN() throws Throwable
+    {
+        // When required_endpoint_verification is set to false, client 
certificate Ip/hostname should be validated
+        // The certificate in cassandra_ssl_test_outbound.keystore does not 
have IP/hostname embeded, so when
+        // require_endpoint_verification is false, the connection should be 
established
+       testEndpointVerification(false, true);
+    }
+
+    @Test
+    public void testEndpointVerificationEnabledIpNotInSAN() throws Throwable
+    {
+        // When required_endpoint_verification is set to true, client 
certificate Ip/hostname should be validated
+        // The certificate in cassandra_ssl_test_outbound.keystore does not 
have IP/hostname emebeded, so when
+        // require_endpoint_verification is true, the connection should not be 
established
+        testEndpointVerification(true, false);
+    }
+
+    @Test
+    public void testEndpointVerificationEnabledWithIPInSan() throws Throwable
+    {
+        // When required_endpoint_verification is set to true, client 
certificate Ip/hostname should be validated
+        // The certificate in cassandra_ssl_test_outbound.keystore have 
IP/hostname emebeded, so when
+        // require_endpoint_verification is true, the connection should be 
established
+        testEndpointVerification(true, true);
+    }
+
+    private void testEndpointVerification(boolean requireEndpointVerification, 
boolean ipInSAN) throws Throwable
+    {
+        try (Cluster cluster = builder().withNodes(1).withConfig(c -> {
+            c.with(Feature.NATIVE_PROTOCOL);
+            c.set("client_encryption_options",
+                  ImmutableMap.builder().putAll(validKeystore)
+                              .put("enabled", true)
+                              .put("require_client_auth", true)
+                              .put("require_endpoint_verification", 
requireEndpointVerification)
+                              .build());
+        }).start())
+        {
+            InetAddress address = 
cluster.get(1).config().broadcastAddress().getAddress();
+            SslContextBuilder sslContextBuilder = 
SslContextBuilder.forClient();
+            if (ipInSAN)
+                
sslContextBuilder.keyManager(createKeyManagerFactory("test/conf/cassandra_ssl_test_endpoint_verify.keystore",
 "cassandra"));
+            else
+                
sslContextBuilder.keyManager(createKeyManagerFactory("test/conf/cassandra_ssl_test_outbound.keystore",
 "cassandra"));
+
+            SslContext sslContext = 
sslContextBuilder.trustManager(createTrustManagerFactory("test/conf/cassandra_ssl_test.truststore",
 "cassandra"))
+                                                     .build();
+            final SSLOptions sslOptions = socketChannel -> 
sslContext.newHandler(socketChannel.alloc());
+            com.datastax.driver.core.Cluster driverCluster = 
com.datastax.driver.core.Cluster.builder()
+                                                                               
              .addContactPoint(address.getHostAddress())
+                                                                               
              .withSSL(sslOptions)
+                                                                               
              .build();
+
+            if (!ipInSAN)
+            {
+                expectedException.expect(NoHostAvailableException.class);
+            }
+
+            driverCluster.connect();
+        }
+    }
+
+    private KeyManagerFactory createKeyManagerFactory(final String 
keyStorePath,
+                                                     final String 
keyStorePassword) throws Exception
+    {
+        final InputStream stream = new FileInputStream(keyStorePath);
+        final KeyStore ks = KeyStore.getInstance("JKS");
+        ks.load(stream, keyStorePassword.toCharArray());
+        final KeyManagerFactory kmf = 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        kmf.init(ks, keyStorePassword.toCharArray());
+        return kmf;
+    }
+
+    private TrustManagerFactory createTrustManagerFactory(final String 
trustStorePath,
+                                                          final String 
trustStorePassword) throws Exception
+    {
+        final InputStream stream = new FileInputStream(trustStorePath);
+        final KeyStore ts = KeyStore.getInstance("JKS");
+        ts.load(stream, trustStorePassword.toCharArray());
+        final TrustManagerFactory tmf = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        tmf.init(ts);
+        return tmf;
+    }
+
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to