blob: 5f2caaf6951352f53a04afbcd1c236f939e1c436 [file] [log] [blame]
/*
* 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.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
{
try (Cluster cluster = builder().withNodes(1).withConfig(c -> {
c.with(Feature.NATIVE_PROTOCOL);
c.set("client_encryption_options",
ImmutableMap.of("optional", true,
"keystore", "/path/to/bad/keystore/that/should/not/exist",
"truststore", "/path/to/bad/truststore/that/should/not/exist"));
}).createWithoutStarting())
{
assertCannotStartDueToConfigurationException(cluster);
}
}
@Test
public void optionalTlsConnectionDisabledWithoutKeystoreTest() throws Throwable
{
try (Cluster cluster = builder().withNodes(1).withConfig(c -> c.with(Feature.NATIVE_PROTOCOL)).createWithoutStarting())
{
InetAddress address = cluster.get(1).config().broadcastAddress().getAddress();
int port = (int) cluster.get(1).config().get("native_transport_port");
TlsConnection tlsConnection = new TlsConnection(address.getHostAddress(), port);
tlsConnection.assertCannotConnect();
cluster.startup();
Assert.assertEquals("TLS connection should not be possible without keystore",
ConnectResult.FAILED_TO_NEGOTIATE, tlsConnection.connect());
}
}
@Test
public void optionalTlsConnectionAllowedWithKeystoreTest() throws Throwable
{
try (Cluster cluster = builder().withNodes(1).withConfig(c -> {
c.with(Feature.NATIVE_PROTOCOL);
c.set("client_encryption_options", validKeystore);
}).createWithoutStarting())
{
InetAddress address = cluster.get(1).config().broadcastAddress().getAddress();
int port = (int) cluster.get(1).config().get("native_transport_port");
TlsConnection tlsConnection = new TlsConnection(address.getHostAddress(), port);
tlsConnection.assertCannotConnect();
cluster.startup();
Assert.assertEquals("TLS native connection should be possible with keystore by default",
ConnectResult.NEGOTIATED, tlsConnection.connect());
}
}
@Test
public void optionalTlsConnectionAllowedToRegularPortTest() throws Throwable
{
try (Cluster cluster = builder().withNodes(1).withConfig(c -> {
c.with(Feature.NATIVE_PROTOCOL);
c.set("native_transport_port_ssl", 9043);
c.set("client_encryption_options",
ImmutableMap.builder().putAll(validKeystore)
.put("enabled", false)
.put("optional", true)
.build());
}).createWithoutStarting())
{
InetAddress address = cluster.get(1).config().broadcastAddress().getAddress();
int unencrypted_port = (int) cluster.get(1).config().get("native_transport_port");
int ssl_port = (int) cluster.get(1).config().get("native_transport_port_ssl");
// Create the connections and prove they cannot connect before server start
TlsConnection connectionToUnencryptedPort = new TlsConnection(address.getHostAddress(), unencrypted_port);
connectionToUnencryptedPort.assertCannotConnect();
TlsConnection connectionToEncryptedPort = new TlsConnection(address.getHostAddress(), ssl_port);
connectionToEncryptedPort.assertCannotConnect();
cluster.startup();
Assert.assertEquals("TLS native connection should be possible to native_transport_port_ssl",
ConnectResult.NEGOTIATED, connectionToEncryptedPort.connect());
Assert.assertEquals("TLS native connection should not be possible on the regular port if an SSL port is specified",
ConnectResult.FAILED_TO_NEGOTIATE, connectionToUnencryptedPort.connect()); // but did connect
}
}
@Test
public void unencryptedNativeConnectionNotlisteningOnTlsPortTest() throws Throwable
{
try (Cluster cluster = builder().withNodes(1).withConfig(c -> {
c.with(Feature.NATIVE_PROTOCOL);
c.set("native_transport_port_ssl", 9043);
c.set("client_encryption_options",
ImmutableMap.builder().putAll(validKeystore)
.put("enabled", false)
.put("optional", false)
.build());
}).createWithoutStarting())
{
assertCannotStartDueToConfigurationException(cluster);
}
}
@Test
public void negotiatedProtocolMustBeAcceptedProtocolTest() 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("accepted_protocols", Collections.singletonList("TLSv1.1"))
.build());
}).start())
{
InetAddress address = cluster.get(1).config().broadcastAddress().getAddress();
int port = (int) cluster.get(1).config().get("native_transport_port");
TlsConnection tls10Connection = new TlsConnection(address.getHostAddress(), port, Collections.singletonList("TLSv1"));
Assert.assertEquals("Should not be possible to establish a TLSv1 connection",
ConnectResult.FAILED_TO_NEGOTIATE, tls10Connection.connect());
tls10Connection.assertReceivedHandshakeException();
TlsConnection tls11Connection = new TlsConnection(address.getHostAddress(), port, Collections.singletonList("TLSv1.1"));
Assert.assertEquals("Should be possible to establish a TLSv1.1 connection",
ConnectResult.NEGOTIATED, tls11Connection.connect());
Assert.assertEquals("TLSv1.1", tls11Connection.lastProtocol());
TlsConnection tls12Connection = new TlsConnection(address.getHostAddress(), port, Collections.singletonList("TLSv1.2"));
Assert.assertEquals("Should be possible to establish a TLSv1.2 connection",
ConnectResult.FAILED_TO_NEGOTIATE, tls12Connection.connect());
tls12Connection.assertReceivedHandshakeException();
}
}
@Test
public void connectionCannotAgreeOnClientAndServerTest() 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("accepted_protocols", Collections.singletonList("TLSv1.2"))
.put("cipher_suites", Collections.singletonList("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"))
.build());
}).start())
{
InetAddress address = cluster.get(1).config().broadcastAddress().getAddress();
int port = (int) cluster.get(1).config().get("native_transport_port");
TlsConnection connection = new TlsConnection(address.getHostAddress(), port,
Collections.singletonList("TLSv1.2"),
Collections.singletonList("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"));
Assert.assertEquals("Should not be possible to establish a TLSv1.2 connection with different ciphers",
ConnectResult.FAILED_TO_NEGOTIATE, connection.connect());
connection.assertReceivedHandshakeException();
}
}
@Test
public void nodeMustNotStartWithNonExistantProtocolTest() throws Throwable
{
try (Cluster cluster = builder().withNodes(1).withConfig(c -> {
c.with(Feature.NATIVE_PROTOCOL);
c.set("client_encryption_options",
ImmutableMap.<String,Object>builder().putAll(nonExistantProtocol).put("enabled", true).build());
}).createWithoutStarting())
{
assertCannotStartDueToConfigurationException(cluster);
}
}
@Test
public void nodeMustNotStartWithNonExistantCiphersTest() throws Throwable
{
try (Cluster cluster = builder().withNodes(1).withConfig(c -> {
c.with(Feature.NATIVE_PROTOCOL);
c.set("client_encryption_options",
ImmutableMap.<String,Object>builder().putAll(nonExistantCipher).put("enabled", true).build());
}).createWithoutStarting())
{
// Should also log "Dropping unsupported cipher_suite NoCipherIKnow from from native transport configuration"
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;
}
}