diff --git a/CHANGES.txt b/CHANGES.txt index b593a1b44a..10e7a433bf 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,7 @@ 4.0-beta5 * Restore sasi dependencies jflex, snowball-stemmer, and concurrent-trees, in the cassandra-all pom (CASSANDRA-16303) * Fix DecimalDeserializer#toString OOM (CASSANDRA-14925) + * SSLFactory should initialize SSLContext before setting protocols (CASSANDRA-16362) 4.0-beta4 * DROP COMPACT STORAGE should invalidate prepared statements still using CompactTableMetadata (CASSANDRA-16361) diff --git a/src/java/org/apache/cassandra/config/EncryptionOptions.java b/src/java/org/apache/cassandra/config/EncryptionOptions.java index fad6190667..a534e36fad 100644 --- a/src/java/org/apache/cassandra/config/EncryptionOptions.java +++ b/src/java/org/apache/cassandra/config/EncryptionOptions.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.security.SSLFactory; public class EncryptionOptions { @@ -236,10 +237,9 @@ public class EncryptionOptions } /* This list is substituted in configurations that have explicitly specified the original "TLS" default, - * it is not a 'default' list or 'support protocol versions' list. It is just an attempt to preserve the - * original intent for the user configuration + * by extracting it from the default "TLS" SSL Context instance */ - private final List TLS_PROTOCOL_SUBSTITUTION = ImmutableList.of("TLSv1.3", "TLSv1.2", "TLSv1.1", "TLSv1"); + static private final List TLS_PROTOCOL_SUBSTITUTION = SSLFactory.tlsInstanceProtocolSubstitution(); /** * Combine the pre-4.0 protocol field with the accepted_protocols list, substituting a list of diff --git a/src/java/org/apache/cassandra/security/SSLFactory.java b/src/java/org/apache/cassandra/security/SSLFactory.java index 7dc025612e..2f7ea75873 100644 --- a/src/java/org/apache/cassandra/security/SSLFactory.java +++ b/src/java/org/apache/cassandra/security/SSLFactory.java @@ -161,6 +161,26 @@ public final class SSLFactory } } + /** Provides the list of protocols that would have been supported if "TLS" was selected as the + * protocol before the change for CASSANDRA-13325 that expects explicit protocol versions. + * @return list of enabled protocol names + */ + public static List tlsInstanceProtocolSubstitution() + { + try + { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, null, null); + SSLParameters params = ctx.getSupportedSSLParameters(); + String[] protocols = params.getProtocols(); + return Arrays.asList(protocols); + } + catch (Exception e) + { + throw new RuntimeException("Error finding supported TLS Protocols", e); + } + } + /** * Create a JSSE {@link SSLContext}. */ @@ -175,7 +195,6 @@ public final class SSLFactory try { SSLContext ctx = SSLContext.getInstance("TLS"); - ctx.getDefaultSSLParameters().setProtocols(options.acceptedProtocolsArray()); ctx.init(kmf.getKeyManagers(), trustManagers, null); return ctx; } diff --git a/src/java/org/apache/cassandra/tools/BulkLoader.java b/src/java/org/apache/cassandra/tools/BulkLoader.java index 5fd3885465..c1de1ffa8a 100644 --- a/src/java/org/apache/cassandra/tools/BulkLoader.java +++ b/src/java/org/apache/cassandra/tools/BulkLoader.java @@ -21,15 +21,17 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.util.Set; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; import com.datastax.driver.core.AuthProvider; -import com.datastax.driver.core.JdkSSLOptions; +import com.datastax.driver.core.RemoteEndpointAwareJdkSSLOptions; import com.datastax.driver.core.SSLOptions; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; +import com.datastax.shaded.netty.channel.socket.SocketChannel; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.config.EncryptionOptions; import org.apache.cassandra.io.sstable.SSTableLoader; @@ -261,10 +263,22 @@ public class BulkLoader throw new RuntimeException("Could not create SSL Context.", e); } - return JdkSSLOptions.builder() - .withSSLContext(sslContext) - .withCipherSuites(clientEncryptionOptions.cipherSuitesArray()) - .build(); + // Temporarily override newSSLEngine to set accepted protocols until it is added to + // RemoteEndpointAwareJdkSSLOptions. See CASSANDRA-13325 and CASSANDRA-16362. + RemoteEndpointAwareJdkSSLOptions sslOptions = new RemoteEndpointAwareJdkSSLOptions(sslContext, null) + { + protected SSLEngine newSSLEngine(SocketChannel channel, InetSocketAddress remoteEndpoint) + { + SSLEngine engine = super.newSSLEngine(channel, remoteEndpoint); + + String[] acceptedProtocols = clientEncryptionOptions.acceptedProtocolsArray(); + if (acceptedProtocols != null && acceptedProtocols.length > 0) + engine.setEnabledProtocols(acceptedProtocols); + + return engine; + } + }; + return sslOptions; } static class ExternalClient extends NativeSSTableLoaderClient diff --git a/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java b/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java new file mode 100644 index 0000000000..1ddc1fb4d7 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java @@ -0,0 +1,106 @@ +/* + * 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.IOException; +import java.util.Collections; + +import com.google.common.collect.ImmutableMap; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.tools.BulkLoader; +import org.apache.cassandra.tools.ToolRunner; + +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + + +public class SSTableLoaderEncryptionOptionsTest extends AbstractEncryptionOptionsImpl +{ + static Cluster CLUSTER; + static String NODES; + static int NATIVE_PORT; + static int STORAGE_PORT; + + @BeforeClass + public static void setupCluster() throws IOException + { + CLUSTER = Cluster.build().withNodes(1).withConfig(c -> { + c.with(Feature.NATIVE_PROTOCOL, Feature.NETWORK, Feature.GOSSIP); // need gossip to get hostid for java driver + c.set("server_encryption_options", + ImmutableMap.builder().putAll(validKeystore) + .put("internode_encryption", "all") + .put("optional", false) + .build()); + c.set("client_encryption_options", + ImmutableMap.builder().putAll(validKeystore) + .put("enabled", true) + .put("optional", false) + .put("accepted_protocols", Collections.singletonList("TLSv1.2")) + .build()); + }).start(); + NODES = CLUSTER.get(1).config().broadcastAddress().getHostString(); + NATIVE_PORT = CLUSTER.get(1).callOnInstance(DatabaseDescriptor::getNativeTransportPort); + STORAGE_PORT = CLUSTER.get(1).callOnInstance(DatabaseDescriptor::getStoragePort); + } + + @AfterClass + public static void tearDownCluster() + { + if (CLUSTER != null) + CLUSTER.close(); + } + @Test + public void bulkLoaderSuccessfullyConnectsOverSsl() throws Throwable + { + ToolRunner.ToolResult tool = ToolRunner.invokeClass(BulkLoader.class, + "--nodes", NODES, + "--port", Integer.toString(NATIVE_PORT), + "--storage-port", Integer.toString(STORAGE_PORT), + "--keystore", validKeyStorePath, + "--keystore-password", validKeyStorePassword, + "--truststore", validTrustStorePath, + "--truststore-password", validTrustStorePassword, + "test/data/legacy-sstables/na/legacy_tables/legacy_na_clust"); + tool.assertOnCleanExit(); + assertTrue(tool.getStdout().contains("Summary statistics")); + } + + @Test + public void bulkLoaderCannotAgreeOnClientTLSProtocol() + { + ToolRunner.ToolResult tool = ToolRunner.invokeClass(BulkLoader.class, + "--ssl-protocol", "TLSv1", + "--nodes", NODES, + "--port", Integer.toString(NATIVE_PORT), + "--storage-port", Integer.toString(STORAGE_PORT), + "--keystore", validKeyStorePath, + "--keystore-password", validKeyStorePassword, + "--truststore", validTrustStorePath, + "--truststore-password", validTrustStorePassword, + "test/data/legacy-sstables/na/legacy_tables/legacy_na_clust"); + assertNotEquals(0, tool.getExitCode()); + assertTrue(tool.getStdout().contains("SSLHandshakeException")); + } +} diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java index 7dc84f6dbb..521071cb84 100644 --- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java +++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java @@ -141,6 +141,7 @@ public class DatabaseDescriptorRefTest "org.apache.cassandra.locator.Replica", "org.apache.cassandra.locator.SimpleSeedProvider", "org.apache.cassandra.locator.SeedProvider", + "org.apache.cassandra.security.SSLFactory", "org.apache.cassandra.security.EncryptionContext", "org.apache.cassandra.service.CacheService$CacheType", "org.apache.cassandra.utils.binlog.BinLogOptions", diff --git a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java index c81da51b13..a2fda49e2b 100644 --- a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java @@ -36,6 +36,7 @@ import org.apache.cassandra.config.Config; import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption; import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.cql3.CQLTester; +import org.apache.cassandra.security.SSLFactory; public class SettingsTableTest extends CQLTester { @@ -154,7 +155,7 @@ public class SettingsTableTest extends CQLTester check(pre + "protocol", "[TLSv5]"); config.server_encryption_options = config.server_encryption_options.withProtocol("TLS"); - check(pre + "protocol", "[TLSv1.3, TLSv1.2, TLSv1.1, TLSv1]"); + check(pre + "protocol", SSLFactory.tlsInstanceProtocolSubstitution().toString()); config.server_encryption_options = config.server_encryption_options.withProtocol("TLS"); config.server_encryption_options = config.server_encryption_options.withAcceptedProtocols(ImmutableList.of("TLSv1.2","TLSv1.1")); diff --git a/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java b/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java index b93476901f..49cb0a8639 100644 --- a/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java +++ b/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java @@ -17,16 +17,19 @@ */ package org.apache.cassandra.stress.util; +import java.net.InetSocketAddress; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; import com.datastax.driver.core.*; import com.datastax.driver.core.policies.DCAwareRoundRobinPolicy; import com.datastax.driver.core.policies.LoadBalancingPolicy; import com.datastax.driver.core.policies.TokenAwarePolicy; import com.datastax.driver.core.policies.WhiteListPolicy; +import com.datastax.shaded.netty.channel.socket.SocketChannel; import io.netty.util.internal.logging.InternalLoggerFactory; import io.netty.util.internal.logging.Slf4JLoggerFactory; import org.apache.cassandra.config.EncryptionOptions; @@ -142,9 +145,22 @@ public class JavaDriverClient { SSLContext sslContext; sslContext = SSLFactory.createSSLContext(encryptionOptions, true); - SSLOptions sslOptions = JdkSSLOptions.builder() - .withSSLContext(sslContext) - .withCipherSuites(encryptionOptions.cipherSuitesArray()).build(); + + // Temporarily override newSSLEngine to set accepted protocols until it is added to + // RemoteEndpointAwareJdkSSLOptions. See CASSANDRA-13325 and CASSANDRA-16362. + RemoteEndpointAwareJdkSSLOptions sslOptions = new RemoteEndpointAwareJdkSSLOptions(sslContext, null) + { + protected SSLEngine newSSLEngine(SocketChannel channel, InetSocketAddress remoteEndpoint) + { + SSLEngine engine = super.newSSLEngine(channel, remoteEndpoint); + + String[] acceptedProtocols = encryptionOptions.acceptedProtocolsArray(); + if (acceptedProtocols != null && acceptedProtocols.length > 0) + engine.setEnabledProtocols(acceptedProtocols); + + return engine; + } + }; clusterBuilder.withSSL(sslOptions); }