Adding endpoint verification option to client_encryption_options

patch by Jyothsna Konisa; reviewed by Jon Meredith, Yifan Cai for CASSANDRA-18034
This commit is contained in:
Jyothsna Konisa 2022-11-14 14:16:07 -08:00 committed by Yifan Cai
parent d5fd0dceee
commit 36e16ee3c9
8 changed files with 113 additions and 4 deletions

View File

@ -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)

View File

@ -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

View File

@ -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());

View File

@ -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);

View File

@ -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));
}
}

Binary file not shown.

View File

@ -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;
}
}