CEP-9 make SSLContext creation pluggable

patch by Maulin Vasavada; reviewed by Jon Meredith, Stefan Miklosovic and Berenguer Blasi for CASSANDRA-16666
This commit is contained in:
Maulin Vasavada 2021-05-21 00:43:50 -07:00 committed by Stefan Miklosovic
parent c3c2c7efec
commit 24dcc280c2
33 changed files with 2577 additions and 474 deletions

View File

@ -1,4 +1,5 @@
4.1
* Make SSLContext creation pluggable/extensible (CASSANDRA-16666)
* Add soft/hard limits to local reads to protect against reading too much data in a single query (CASSANDRA-16896)
* Avoid token cache invalidation for removing a non-member node (CASSANDRA-15290)
* Allow configuration of consistency levels on auth operations (CASSANDRA-12988)

View File

@ -88,6 +88,33 @@ As an alternative to the ``optional`` setting, separate ports can also be config
where operational requirements demand it. To do so, set ``optional`` to false and use the ``native_transport_port_ssl``
setting in ``cassandra.yaml`` to specify the port to be used for secure client communication.
.. _customizing-ssl-context:
Customizing SSL Context Creation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
There are situations when the current file-based configurations for keystore/truststore and passwords are not enough and
you want to customize how SSL Context is created to your specific needs. In that case, you can create a custom implementation
of Cassandra's ``org.apache.cassandra.security.ISslContextFactory`` interface and configure ``cassandra.yaml`` to use it.
While the ``ISslContextFactory`` provides full control of building a SSL Context, you can extend
``org.apache.cassandra.security.AbstractSslContextFactory`` or ``org.apache.cassandra.security.FileBasedSslContextFactory``
to keep your custom implementation to the bare minimum. You can find an example of such a customization
in ``examples`` directory for Kubernetes in ``KubernetesSecretsSslContextFactory.java``.
Below is the example to customize internode ssl configuration with ``YourCassandraSslContextFactory``. The same way you
can customize the client-to-node encryption.
::
server_encryption_options:
ssl_context_factory:
class_name: com.your-company.YourCassandraSslContextFactory
parameters:
key1: "value1"
key2: "value2"
key3: "value3"
internode_encryption: none
.. _operation-roles:
Roles

View File

@ -0,0 +1,19 @@
Cassandra Custom SslContextFactory Example:
==========================================
Example-1: Custom SslContextFactory implementation based on Kubernetes secrets
------------------------------------------------------------------------------
For the documentation please refer to the javadocs for the K8SecretsSslContextFactory.java.
Installation:
============
Step 1: Build the Cassandra classes locally
change directory to <cassandra_src_dir>
run "ant build"
Step 2: Run tests for the security examples
change directory to <cassandra_src_dir>/examples/ssl-factory
run "ant test"

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project default="jar" name="ssl-factory-example">
<property name="cassandra.dir" value="../.." />
<property name="cassandra.dir.lib" value="${cassandra.dir}/lib" />
<property name="cassandra.classes" value="${cassandra.dir}/build/classes/main" />
<property name="cassandra.test.lib" value="${cassandra.dir}/build/test/lib" />
<property name="build.src" value="${basedir}/src" />
<property name="build.dir" value="${basedir}/build" />
<property name="conf.dir" value="${basedir}/conf" />
<property name="build.classes" value="${build.dir}/classes" />
<property name="test.src" value="${basedir}/test/unit" />
<property name="test.build.dir" value="${build.dir}/test" />
<property name="test.conf.dir" value="${test.src}/conf" />
<property name="test.build.classes" value="${test.build.dir}/classes" />
<property name="test.build.conf" value="${test.build.dir}/conf" />
<property name="final.name" value="ssl-factory-example" />
<path id="build.classpath">
<fileset dir="${cassandra.dir.lib}">
<include name="**/*.jar" />
</fileset>
<fileset dir="${cassandra.dir}/build/lib/jars">
<include name="**/*.jar" />
</fileset>
<pathelement location="${cassandra.classes}" />
</path>
<path id="test.classpath">
<fileset dir="${cassandra.test.lib}/jars">
<include name="**/*.jar" />
<exclude name="**/ant-*.jar"/>
</fileset>
<path refid="build.classpath"/>
<path location="${build.dir}/${final.name}.jar"/>
<pathelement location="${test.build.classes}"/>
<pathelement location="${test.build.conf}"/>
</path>
<target name="init">
<mkdir dir="${build.classes}" />
<mkdir dir="${test.build.classes}" />
</target>
<target name="build" depends="init">
<javac destdir="${build.classes}" debug="true" includeantruntime="false">
<src path="${build.src}" />
<classpath refid="build.classpath" />
</javac>
</target>
<target name="jar" depends="build">
<jar jarfile="${build.dir}/${final.name}.jar">
<fileset dir="${build.classes}" />
</jar>
</target>
<target name="buildTests" depends="init">
<deltree dir="${test.build.dir}/conf"/>
<copydir src="test/conf" dest="${test.build.dir}/conf"/>
<javac destdir="${test.build.classes}" debug="true" includeantruntime="false">
<src path="${test.src}" />
<src path="${test.build.dir}/conf" />
<classpath refid="test.classpath"/>
</javac>
</target>
<target name="test" depends="jar, buildTests">
<junit printsummary="on" haltonfailure="yes" fork="true">
<classpath refid="test.classpath"/>
<formatter type="brief" usefile="false" />
<batchtest>
<fileset dir="${test.src}" includes="**/*Test.java" />
</batchtest>
</junit>
</target>
<target name="clean">
<delete dir="${build.dir}" />
<delete dir="${test.build.classes}" />
</target>
</project>

View File

@ -0,0 +1,287 @@
/*
* 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.security;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
import java.util.Optional;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.EncryptionOptions;
/**
* Custom {@link ISslContextFactory} implementation based on Kubernetes Secrets. It allows the keystore and
* truststore paths to be configured from the K8 secrets via volumeMount and passwords via K8 secrets environment
* variables. The official Kubernetes Secret Spec can be found <a href="https://kubernetes.io/docs/concepts/configuration/secret/ ">here</a>.
*
* When keystore or truststore is updated, this implementation can detect that based on updated K8 secrets
* at the mounted paths ({@code KEYSTORE_UPDATED_TIMESTAMP_PATH} for the keystore and {@code
* TRUSTSTORE_UPDATED_TIMESTAMP_PATH} for the truststore. The values in those paths are expected to be numeric values.
* The most obvious choice might be to just use the time in nano/milli-seconds precision but any other strategy would work
* as well, as far as the comparison of those values can be done in a consistent/predictable manner. Again, those
* values do not have to necessarily reflect actual file's update timestamps, using the actual file's timestamps is
* just one of the valid options to signal updates.
*
* Defaults:
* <pre>
* keystore path = /etc/my-ssl-store/keystore
* keystore password = cassandra
* keystore updated timestamp path = /etc/my-ssl-store/keystore-last-updatedtime
* truststore path = /etc/my-ssl-store/truststore
* truststore password = cassandra
* truststore updated timestamp path = /etc/my-ssl-store/truststore-last-updatedtime
* </pre>
*
* Customization: In order to customize the K8s secret configuration, override appropriate values in the below Cassandra
* configuration. The similar configuration can be applied to {@code client_encryption_options}.
* <pre>
* server_encryption_options:
* internode_encryption: none
* ssl_context_factory:
* class_name: org.apache.cassandra.security.KubernetesSecretsSslContextFactory
* parameters:
* KEYSTORE_PASSWORD_ENV_VAR: KEYSTORE_PASSWORD
* KEYSTORE_UPDATED_TIMESTAMP_PATH: /etc/my-ssl-store/keystore-last-updatedtime
* TRUSTSTORE_PASSWORD_ENV_VAR: TRUSTSTORE_PASSWORD
* TRUSTSTORE_UPDATED_TIMESTAMP_PATH: /etc/my-ssl-store/truststore-last-updatedtime
* keystore: /etc/my-ssl-store/keystore
* truststore: /etc/my-ssl-store/truststore
* </pre>
*
* Below is the corresponding sample YAML configuration for K8 env.
* <pre>
* apiVersion: v1
* kind: Pod
* metadata:
* name: my-pod
* labels:
* app: my-app
* spec:
* containers:
* - name: my-app
* image: my-app:latest
* imagePullPolicy: Always
* env:
* - name: KEYSTORE_PASSWORD
* valueFrom:
* secretKeyRef:
* name: my-ssl-store
* key: keystore-password
* - name: TRUSTSTORE_PASSWORD
* valueFrom:
* secretKeyRef:
* name: my-ssl-store
* key: truststore-password
* volumeMounts:
* - name: my-ssl-store
* mountPath: "/etc/my-ssl-store"
* readOnly: true
* volumes:
* - name: my-ssl-store
* secret:
* secretName: my-ssl-store
* items:
* - key: cassandra_ssl_keystore
* path: keystore
* - key: keystore-last-updatedtime
* path: keystore-last-updatedtime
* - key: cassandra_ssl_truststore
* path: truststore
* - key: truststore-last-updatedtime
* path: truststore-last-updatedtime
* </pre>
*/
public class KubernetesSecretsSslContextFactory extends FileBasedSslContextFactory
{
private static final Logger logger = LoggerFactory.getLogger(KubernetesSecretsSslContextFactory.class);
/**
* Use below config-keys to configure this factory.
*/
public interface ConfigKeys {
String KEYSTORE_PASSWORD_ENV_VAR = "KEYSTORE_PASSWORD_ENV_VAR";
String TRUSTSTORE_PASSWORD_ENV_VAR = "TRUSTSTORE_PASSWORD_ENV_VAR";
String KEYSTORE_UPDATED_TIMESTAMP_PATH = "KEYSTORE_UPDATED_TIMESTAMP_PATH";
String TRUSTSTORE_UPDATED_TIMESTAMP_PATH = "TRUSTSTORE_UPDATED_TIMESTAMP_PATH";
}
public static final String DEFAULT_KEYSTORE_PASSWORD = "";
public static final String DEFAULT_TRUSTSTORE_PASSWORD = "";
@VisibleForTesting
static final String DEFAULT_KEYSTORE_PASSWORD_ENV_VAR_NAME = "KEYSTORE_PASSWORD";
@VisibleForTesting
static final String DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME = "TRUSTSTORE_PASSWORD";
private static final String KEYSTORE_PATH_VALUE = "/etc/my-ssl-store/keystore";
private static final String TRUSTSTORE_PATH_VALUE = "/etc/my-ssl-store/truststore";
private static final String KEYSTORE_PASSWORD_ENV_VAR_NAME = DEFAULT_KEYSTORE_PASSWORD_ENV_VAR_NAME;
private static final String KEYSTORE_UPDATED_TIMESTAMP_PATH_VALUE = "/etc/my-ssl-store/keystore-last-updatedtime";
private static final String TRUSTSTORE_PASSWORD_ENV_VAR_NAME = DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME;
private static final String TRUSTSTORE_UPDATED_TIMESTAMP_PATH_VALUE = "/etc/my-ssl-store/truststore-last-updatedtime";
private final String keystoreUpdatedTimeSecretKeyPath;
private final String truststoreUpdatedTimeSecretKeyPath;
private long keystoreLastUpdatedTime;
private long truststoreLastUpdatedTime;
public KubernetesSecretsSslContextFactory()
{
keystore = getString(EncryptionOptions.ConfigKey.KEYSTORE.toString(), KEYSTORE_PATH_VALUE);
keystore_password = getValueFromEnv(KEYSTORE_PASSWORD_ENV_VAR_NAME,
DEFAULT_KEYSTORE_PASSWORD);
truststore = getString(EncryptionOptions.ConfigKey.TRUSTSTORE.toString(), TRUSTSTORE_PATH_VALUE);
truststore_password = getValueFromEnv(TRUSTSTORE_PASSWORD_ENV_VAR_NAME,
DEFAULT_TRUSTSTORE_PASSWORD);
keystoreLastUpdatedTime = System.nanoTime();
keystoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.KEYSTORE_UPDATED_TIMESTAMP_PATH,
KEYSTORE_UPDATED_TIMESTAMP_PATH_VALUE);
truststoreLastUpdatedTime = keystoreLastUpdatedTime;
truststoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.TRUSTSTORE_UPDATED_TIMESTAMP_PATH,
TRUSTSTORE_UPDATED_TIMESTAMP_PATH_VALUE);
}
public KubernetesSecretsSslContextFactory(Map<String, Object> parameters)
{
super(parameters);
keystore = getString(EncryptionOptions.ConfigKey.KEYSTORE.toString(), KEYSTORE_PATH_VALUE);
keystore_password = getValueFromEnv(getString(ConfigKeys.KEYSTORE_PASSWORD_ENV_VAR,
KEYSTORE_PASSWORD_ENV_VAR_NAME), DEFAULT_KEYSTORE_PASSWORD);
truststore = getString(EncryptionOptions.ConfigKey.TRUSTSTORE.toString(), TRUSTSTORE_PATH_VALUE);
truststore_password = getValueFromEnv(getString(ConfigKeys.TRUSTSTORE_PASSWORD_ENV_VAR,
TRUSTSTORE_PASSWORD_ENV_VAR_NAME), DEFAULT_TRUSTSTORE_PASSWORD);
keystoreLastUpdatedTime = System.nanoTime();
keystoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.KEYSTORE_UPDATED_TIMESTAMP_PATH,
KEYSTORE_UPDATED_TIMESTAMP_PATH_VALUE);
truststoreLastUpdatedTime = keystoreLastUpdatedTime;
truststoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.TRUSTSTORE_UPDATED_TIMESTAMP_PATH,
TRUSTSTORE_UPDATED_TIMESTAMP_PATH_VALUE);
}
@Override
public synchronized void initHotReloading() {
// No-op
}
/**
* Checks environment variables for {@code K8_SECRET_KEYSTORE_UPDATED_TIMESTAMP_ENV_VAR} and {@code K8_SECRET_TRUSTSTORE_UPDATED_TIMESTAMP_ENV_VAR}
* and compares the values for those variables with the current timestamps. In case the environment variables are
* not valid (either they are not initialized yet, got removed or got corrupted in-flight), this method considers
* that nothing has changed.
* @return {@code true} if either of the timestamps (keystore or truststore) got updated;{@code false} otherwise
*/
@Override
public boolean shouldReload()
{
return hasKeystoreUpdated() || hasTruststoreUpdated();
}
@VisibleForTesting
String getValueFromEnv(String envVarName, String defaultValue) {
String valueFromEnv = StringUtils.isEmpty(envVarName) ? null : System.getenv(envVarName);
return StringUtils.isEmpty(valueFromEnv) ? defaultValue : valueFromEnv;
}
private boolean hasKeystoreUpdated() {
long keystoreUpdatedTime = getKeystoreLastUpdatedTime();
logger.info("Comparing keystore timestamps oldValue {} and newValue {}", keystoreLastUpdatedTime,
keystoreUpdatedTime);
if (keystoreUpdatedTime > keystoreLastUpdatedTime) {
logger.info("Updating the keystoreLastUpdatedTime from oldValue {} to newValue {}",
keystoreLastUpdatedTime, keystoreUpdatedTime);
keystoreLastUpdatedTime = keystoreUpdatedTime;
return true;
} else {
logger.info("Based on the comparision, no keystore update needed");
return false;
}
}
private boolean hasTruststoreUpdated() {
long truststoreUpdatedTime = getTruststoreLastUpdatedTime();
logger.info("Comparing truststore timestamps oldValue {} and newValue {}", truststoreLastUpdatedTime,
truststoreUpdatedTime);
if (truststoreUpdatedTime > truststoreLastUpdatedTime) {
logger.info("Updating the truststoreLastUpdatedTime from oldValue {} to newValue {}",
truststoreLastUpdatedTime, truststoreUpdatedTime);
truststoreLastUpdatedTime = truststoreUpdatedTime;
return true;
} else {
logger.info("Based on the comparision, no truststore update needed");
return false;
}
}
private long getKeystoreLastUpdatedTime() {
Optional<String> keystoreUpdatedTimeSecretKeyValue = readSecretFromMountedVolume(keystoreUpdatedTimeSecretKeyPath);
if (keystoreUpdatedTimeSecretKeyValue.isPresent())
{
return parseLastUpdatedTime(keystoreUpdatedTimeSecretKeyValue.get(), keystoreLastUpdatedTime);
}
else
{
logger.warn("Failed to load {}'s value. Will use existing value {}", keystoreUpdatedTimeSecretKeyPath,
keystoreLastUpdatedTime);
return keystoreLastUpdatedTime;
}
}
private long getTruststoreLastUpdatedTime() {
Optional<String> truststoreUpdatedTimeSecretKeyValue = readSecretFromMountedVolume(truststoreUpdatedTimeSecretKeyPath);
if (truststoreUpdatedTimeSecretKeyValue.isPresent())
{
return parseLastUpdatedTime(truststoreUpdatedTimeSecretKeyValue.get(), truststoreLastUpdatedTime);
}
else
{
logger.warn("Failed to load {}'s value. Will use existing value {}", truststoreUpdatedTimeSecretKeyPath,
truststoreLastUpdatedTime);
return truststoreLastUpdatedTime;
}
}
private Optional<String> readSecretFromMountedVolume(String secretKeyPath) {
try
{
return Optional.of(new String(Files.readAllBytes(Paths.get(secretKeyPath))));
}
catch (IOException e)
{
logger.warn(String.format("Failed to read secretKeyPath %s from the mounted volume: %s", secretKeyPath, e.getMessage()));
return Optional.empty();
}
}
private long parseLastUpdatedTime(String latestUpdatedTime, long currentUpdatedTime) {
try
{
return Long.parseLong(latestUpdatedTime);
} catch(NumberFormatException e) {
logger.warn("Failed to parse the latestUpdatedTime {}. Will use current time {}", latestUpdatedTime,
currentUpdatedTime, e);
return currentUpdatedTime;
}
}
}

View File

@ -0,0 +1,296 @@
/*
* 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.security;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.config.EncryptionOptions;
import static org.apache.cassandra.security.KubernetesSecretsSslContextFactory.ConfigKeys.KEYSTORE_PASSWORD_ENV_VAR;
import static org.apache.cassandra.security.KubernetesSecretsSslContextFactory.ConfigKeys.KEYSTORE_UPDATED_TIMESTAMP_PATH;
import static org.apache.cassandra.security.KubernetesSecretsSslContextFactory.ConfigKeys.TRUSTSTORE_PASSWORD_ENV_VAR;
import static org.apache.cassandra.security.KubernetesSecretsSslContextFactory.ConfigKeys.TRUSTSTORE_UPDATED_TIMESTAMP_PATH;
public class KubernetesSecretsSslContextFactoryTest
{
private static final Logger logger = LoggerFactory.getLogger(KubernetesSecretsSslContextFactoryTest.class);
private static final String TRUSTSTORE_PATH = EncryptionOptions.ConfigKey.TRUSTSTORE.toString();
private static final String KEYSTORE_PATH = EncryptionOptions.ConfigKey.KEYSTORE.toString();
private Map<String, Object> commonConfig = new HashMap<>();
private final static String truststoreUpdatedTimestampFilepath = "build/test/conf/cassandra_truststore_last_updatedtime";
private final static String keystoreUpdatedTimestampFilepath = "build/test/conf/cassandra_keystore_last_updatedtime";
private static class KubernetesSecretsSslContextFactoryForTestOnly extends KubernetesSecretsSslContextFactory
{
public KubernetesSecretsSslContextFactoryForTestOnly()
{
}
public KubernetesSecretsSslContextFactoryForTestOnly(Map<String, Object> config)
{
super(config);
}
/*
* This is overriden to first give priority to the input map configuration since we should not be setting env
* variables from the unit tests. However, if the input map configuration doesn't have the value for the
* given key then fallback to loading from the real environment variables.
*/
@Override
String getValueFromEnv(String envVarName, String defaultValue)
{
String envVarValue = parameters.get(envVarName) != null ? parameters.get(envVarName).toString() : null;
if (StringUtils.isEmpty(envVarValue))
{
logger.info("Configuration doesn't have env variable {}. Will use parent's implementation", envVarName);
return super.getValueFromEnv(envVarName, defaultValue);
}
else
{
logger.info("Configuration has env variable {} with value {}. Will use that.",
envVarName, envVarValue);
return envVarValue;
}
}
}
@BeforeClass
public static void prepare()
{
deleteFileIfExists(truststoreUpdatedTimestampFilepath);
deleteFileIfExists(keystoreUpdatedTimestampFilepath);
}
private static void deleteFileIfExists(String filePath)
{
try
{
logger.info("Deleting the file {} to prepare for the tests", new File(filePath).getAbsolutePath());
File file = new File(filePath);
if (file.exists())
{
file.delete();
}
}
catch (Exception e)
{
logger.warn("File {} could not be deleted.", filePath, e);
}
}
@Before
public void setup()
{
commonConfig.put(TRUSTSTORE_PATH, "build/test/conf/cassandra_ssl_test.truststore");
commonConfig.put(TRUSTSTORE_PASSWORD_ENV_VAR, "MY_TRUSTSTORE_PASSWORD");
commonConfig.put(TRUSTSTORE_UPDATED_TIMESTAMP_PATH, truststoreUpdatedTimestampFilepath);
/*
* In order to test with real 'env' variables comment out this line and set appropriate env variable. This is
* done to avoid having a dependency on env in the unit test.
*/
commonConfig.put("MY_TRUSTSTORE_PASSWORD", "cassandra");
commonConfig.put("require_client_auth", Boolean.FALSE);
commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA"));
}
private void addKeystoreOptions(Map<String, Object> config)
{
config.put(KEYSTORE_PATH, "build/test/conf/cassandra_ssl_test.keystore");
config.put(KEYSTORE_PASSWORD_ENV_VAR, "MY_KEYSTORE_PASSWORD");
config.put(KEYSTORE_UPDATED_TIMESTAMP_PATH, keystoreUpdatedTimestampFilepath);
/*
* In order to test with real 'env' variables comment out this line and set appropriate env variable. This is
* done to avoid having a dependency on env in the unit test.
*/
config.put("MY_KEYSTORE_PASSWORD", "cassandra");
}
@Test(expected = IOException.class)
public void buildTrustManagerFactoryWithInvalidTruststoreFile() throws IOException
{
Map<String, Object> config = new HashMap<>();
config.putAll(commonConfig);
config.put(TRUSTSTORE_PATH, "/this/is/probably/not/a/file/on/your/test/machine");
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
kubernetesSecretsSslContextFactory.checkedExpiry = false;
kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
}
@Test(expected = IOException.class)
public void buildTrustManagerFactoryWithBadPassword() throws IOException
{
Map<String, Object> config = new HashMap<>();
config.putAll(commonConfig);
config.remove(TRUSTSTORE_PASSWORD_ENV_VAR);
config.put(KubernetesSecretsSslContextFactory.DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME, "HomeOfBadPasswords");
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
kubernetesSecretsSslContextFactory.checkedExpiry = false;
kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
}
@Test
public void buildTrustManagerFactoryWithEmptyPassword() throws IOException
{
Map<String, Object> config = new HashMap<>();
config.putAll(commonConfig);
config.put(TRUSTSTORE_PATH, "build/test/conf/cassandra_ssl_test.truststore-without-password");
config.remove(TRUSTSTORE_PASSWORD_ENV_VAR);
config.put(KubernetesSecretsSslContextFactory.DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME, "");
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
kubernetesSecretsSslContextFactory.checkedExpiry = false;
kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
}
@Test
public void buildTrustManagerFactoryHappyPath() throws IOException
{
Map<String, Object> config = new HashMap<>();
config.putAll(commonConfig);
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
kubernetesSecretsSslContextFactory.checkedExpiry = false;
TrustManagerFactory trustManagerFactory = kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
Assert.assertNotNull(trustManagerFactory);
}
@Test(expected = IOException.class)
public void buildKeyManagerFactoryWithInvalidKeystoreFile() throws IOException
{
Map<String, Object> config = new HashMap<>();
config.putAll(commonConfig);
config.put(KEYSTORE_PATH, "/this/is/probably/not/a/file/on/your/test/machine");
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
kubernetesSecretsSslContextFactory.checkedExpiry = false;
kubernetesSecretsSslContextFactory.buildKeyManagerFactory();
}
@Test(expected = IOException.class)
public void buildKeyManagerFactoryWithBadPassword() throws IOException
{
Map<String, Object> config = new HashMap<>();
config.putAll(commonConfig);
config.put(KEYSTORE_PATH, "build/test/conf/cassandra_ssl_test.keystore");
config.put(KubernetesSecretsSslContextFactory.DEFAULT_KEYSTORE_PASSWORD_ENV_VAR_NAME, "HomeOfBadPasswords");
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
kubernetesSecretsSslContextFactory.buildKeyManagerFactory();
}
@Test
public void buildKeyManagerFactoryHappyPath() throws IOException
{
Map<String, Object> config = new HashMap<>();
config.putAll(commonConfig);
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory1 = new KubernetesSecretsSslContextFactoryForTestOnly(config);
// Make sure the exiry check didn't happen so far for the private key
Assert.assertFalse(kubernetesSecretsSslContextFactory1.checkedExpiry);
addKeystoreOptions(config);
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory2 = new KubernetesSecretsSslContextFactoryForTestOnly(config);
// Trigger the private key loading. That will also check for expired private key
kubernetesSecretsSslContextFactory2.buildKeyManagerFactory();
// Now we should have checked the private key's expiry
Assert.assertTrue(kubernetesSecretsSslContextFactory2.checkedExpiry);
// Make sure that new factory object preforms the fresh private key expiry check
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory3 = new KubernetesSecretsSslContextFactoryForTestOnly(config);
Assert.assertFalse(kubernetesSecretsSslContextFactory3.checkedExpiry);
kubernetesSecretsSslContextFactory3.buildKeyManagerFactory();
Assert.assertTrue(kubernetesSecretsSslContextFactory3.checkedExpiry);
}
@Test
public void checkTruststoreUpdateReloading() throws IOException
{
Map<String, Object> config = new HashMap<>();
config.putAll(commonConfig);
addKeystoreOptions(config);
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
kubernetesSecretsSslContextFactory.checkedExpiry = false;
TrustManagerFactory trustManagerFactory = kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
Assert.assertNotNull(trustManagerFactory);
Assert.assertFalse(kubernetesSecretsSslContextFactory.shouldReload());
updateTimestampFile(config, TRUSTSTORE_UPDATED_TIMESTAMP_PATH);
Assert.assertTrue(kubernetesSecretsSslContextFactory.shouldReload());
config.remove(TRUSTSTORE_UPDATED_TIMESTAMP_PATH);
Assert.assertFalse(kubernetesSecretsSslContextFactory.shouldReload());
}
@Test
public void checkKeystoreUpdateReloading() throws IOException
{
Map<String, Object> config = new HashMap<>();
config.putAll(commonConfig);
addKeystoreOptions(config);
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
kubernetesSecretsSslContextFactory.checkedExpiry = false;
KeyManagerFactory keyManagerFactory = kubernetesSecretsSslContextFactory.buildKeyManagerFactory();
Assert.assertNotNull(keyManagerFactory);
Assert.assertFalse(kubernetesSecretsSslContextFactory.shouldReload());
updateTimestampFile(config, KEYSTORE_UPDATED_TIMESTAMP_PATH);
Assert.assertTrue(kubernetesSecretsSslContextFactory.shouldReload());
config.remove(KEYSTORE_UPDATED_TIMESTAMP_PATH);
Assert.assertFalse(kubernetesSecretsSslContextFactory.shouldReload());
}
private void updateTimestampFile(Map<String, Object> config, String filePathKey)
{
String filePath = config.containsKey(filePathKey) ? config.get(filePathKey).toString() : null;
try (OutputStream os = Files.newOutputStream(Paths.get(filePath)))
{
String timestamp = String.valueOf(System.nanoTime());
os.write(timestamp.getBytes());
logger.info("Successfully wrote to file {}", filePath);
}
catch (IOException e)
{
logger.warn("Failed to write to filePath {} from the mounted volume", filePath, e);
}
}
}

View File

@ -17,27 +17,38 @@
*/
package org.apache.cassandra.config;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.locator.IEndpointSnitch;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.security.SSLFactory;
import org.apache.cassandra.security.ISslContextFactory;
import org.apache.cassandra.utils.FBUtilities;
/**
* This holds various options used for enabling SSL/TLS encryption.
* Examples of such options are: supported cipher-suites, ssl protocol with version, accepted protocols, end-point
* verification, require client-auth/cert etc.
*/
public class EncryptionOptions
{
Logger logger = LoggerFactory.getLogger(EncryptionOptions.class);
public enum TlsEncryptionPolicy
{
UNENCRYPTED("unencrypted"), OPTIONAL("optionally encrypted"), ENCRYPTED("encrypted");
UNENCRYPTED("unencrypted"),
OPTIONAL("optionally encrypted"),
ENCRYPTED("encrypted");
private final String description;
@ -52,6 +63,12 @@ public class EncryptionOptions
}
}
/*
* If the ssl_context_factory is configured, most likely it won't use file based keystores and truststores and
* can choose to completely customize SSL context's creation. Most likely it won't also use keystore_password and
* truststore_passwords configurations as they are in plaintext format.
*/
public final ParameterizedClass ssl_context_factory;
public final String keystore;
public final String keystore_password;
public final String truststore;
@ -73,11 +90,58 @@ public class EncryptionOptions
protected Boolean optional;
// Calculated by calling applyConfig() after populating/parsing
protected Boolean isEnabled = null;
protected Boolean isOptional = null;
protected Boolean isEnabled;
protected Boolean isOptional;
/*
* We will wait to initialize this until applyConfig() call to make sure we do it only when the caller is ready
* to use this option instance.
*/
public ISslContextFactory sslContextFactoryInstance;
public enum ConfigKey
{
KEYSTORE("keystore"),
KEYSTORE_PASSWORD("keystore_password"),
TRUSTSTORE("truststore"),
TRUSTSTORE_PASSWORD("truststore_password"),
CIPHER_SUITES("cipher_suites"),
PROTOCOL("protocol"),
ACCEPTED_PROTOCOLS("accepted_protocols"),
ALGORITHM("algorithm"),
STORE_TYPE("store_type"),
REQUIRE_CLIENT_AUTH("require_client_auth"),
REQUIRE_ENDPOINT_VERIFICATION("require_endpoint_verification"),
ENABLED("enabled"),
OPTIONAL("optional");
final String keyName;
ConfigKey(String keyName)
{
this.keyName=keyName;
}
String getKeyName()
{
return keyName;
}
static Set<String> asSet()
{
Set<String> valueSet = new HashSet<>();
ConfigKey[] values = values();
for(ConfigKey key: values) {
valueSet.add(key.getKeyName().toLowerCase());
}
return valueSet;
}
}
public EncryptionOptions()
{
ssl_context_factory = new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
new HashMap<>());
keystore = "conf/.keystore";
keystore_password = "cassandra";
truststore = "conf/.truststore";
@ -93,8 +157,13 @@ public class EncryptionOptions
optional = null;
}
public EncryptionOptions(String keystore, String keystore_password, String truststore, String truststore_password, List<String> cipher_suites, String protocol, List<String> accepted_protocols, String algorithm, String store_type, boolean require_client_auth, boolean require_endpoint_verification, Boolean enabled, Boolean optional)
public EncryptionOptions(ParameterizedClass ssl_context_factory, String keystore, String keystore_password,
String truststore, String truststore_password, List<String> cipher_suites,
String protocol, List<String> accepted_protocols, String algorithm, String store_type,
boolean require_client_auth, boolean require_endpoint_verification, Boolean enabled,
Boolean optional)
{
this.ssl_context_factory = ssl_context_factory;
this.keystore = keystore;
this.keystore_password = keystore_password;
this.truststore = truststore;
@ -112,6 +181,7 @@ public class EncryptionOptions
public EncryptionOptions(EncryptionOptions options)
{
ssl_context_factory = options.ssl_context_factory;
keystore = options.keystore;
keystore_password = options.keystore_password;
truststore = options.truststore;
@ -130,11 +200,15 @@ public class EncryptionOptions
/* Computes enabled and optional before use. Because the configuration can be loaded
* through pluggable mechanisms this is the only safe way to make sure that
* enabled and optional are set correctly.
*
* It also initializes the ISslContextFactory's instance
*/
public EncryptionOptions applyConfig()
{
ensureConfigNotApplied();
initializeSslContextFactory();
isEnabled = this.enabled != null && enabled;
if (optional != null)
@ -144,7 +218,7 @@ public class EncryptionOptions
// If someone is asking for an _insecure_ connection and not explicitly telling us to refuse
// encrypted connections AND they have a keystore file, we assume they would like to be able
// to transition to encrypted connections in the future.
else if (new File(keystore).exists())
else if (sslContextFactoryInstance.hasKeystore())
{
isOptional = !isEnabled;
}
@ -156,6 +230,63 @@ public class EncryptionOptions
return this;
}
/**
* Prepares the parameterized keys provided in the configuration for {@link ISslContextFactory} to be passed in
* as the constructor for its implementation.
*
* @throws IllegalArgumentException in case any pre-defined key, as per {@link ConfigKey}, for the encryption
* options is duplicated in the parameterized keys.
*/
private void prepareSslContextFactoryParameterizedKeys(Map<String,Object> sslContextFactoryParameters)
{
if (ssl_context_factory.parameters != null)
{
Set<String> configKeys = ConfigKey.asSet();
for (Map.Entry<String, String> entry : ssl_context_factory.parameters.entrySet())
{
if(configKeys.contains(entry.getKey().toLowerCase()))
{
throw new IllegalArgumentException("SslContextFactory "+ssl_context_factory.class_name+" should " +
"configure '"+entry.getKey()+"' as encryption_options instead of" +
" parameterized keys");
}
sslContextFactoryParameters.put(entry.getKey(),entry.getValue());
}
}
}
private void initializeSslContextFactory() {
Map<String,Object> sslContextFactoryParameters = new HashMap<>();
prepareSslContextFactoryParameterizedKeys(sslContextFactoryParameters);
/*
* Copy all configs to the Map to pass it on to the ISslContextFactory's implementation
*/
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.KEYSTORE, this.keystore);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.KEYSTORE_PASSWORD, this.keystore_password);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.TRUSTSTORE, this.truststore);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.TRUSTSTORE_PASSWORD, this.truststore_password);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.CIPHER_SUITES, this.cipher_suites);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.PROTOCOL, this.protocol);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.ACCEPTED_PROTOCOLS, this.accepted_protocols);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.ALGORITHM, this.algorithm);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.STORE_TYPE, this.store_type);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.REQUIRE_CLIENT_AUTH, this.require_client_auth);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.REQUIRE_ENDPOINT_VERIFICATION, this.require_endpoint_verification);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.ENABLED, this.enabled);
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.OPTIONAL, this.optional);
sslContextFactoryInstance = FBUtilities.newSslContextFactory(ssl_context_factory.class_name,
sslContextFactoryParameters);
}
private void putSslContextFactoryParameter(Map<String,Object> existingParameters, ConfigKey configKey,
Object value) {
if (value != null) {
existingParameters.put(configKey.getKeyName(), value);
}
}
private void ensureConfigApplied()
{
if (isEnabled == null || isOptional == null)
@ -236,48 +367,9 @@ public class EncryptionOptions
this.accepted_protocols = accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols);
}
/* This list is substituted in configurations that have explicitly specified the original "TLS" default,
* by extracting it from the default "TLS" SSL Context instance
*/
static private final List<String> TLS_PROTOCOL_SUBSTITUTION = SSLFactory.tlsInstanceProtocolSubstitution();
/**
* Combine the pre-4.0 protocol field with the accepted_protocols list, substituting a list of
* explicit protocols for the previous catchall default of "TLS"
* @return array of protocol names suitable for passing to SslContextBuilder.protocols, or null if the default
*/
public List<String> acceptedProtocols()
{
if (accepted_protocols == null)
{
if (protocol == null)
{
return null;
}
// TLS is accepted by SSLContext.getInstance as a shorthand for give me an engine that
// can speak some of the TLS protocols. It is not supported by SSLEngine.setAcceptedProtocols
// so substitute if the user hasn't provided an accepted protocol configuration
else if (protocol.equalsIgnoreCase("TLS"))
{
return TLS_PROTOCOL_SUBSTITUTION;
}
else // the user was trying to limit to a single specific protocol, so try that
{
return ImmutableList.of(protocol);
}
}
if (protocol != null && !protocol.equalsIgnoreCase("TLS") &&
accepted_protocols.stream().noneMatch(ap -> ap.equalsIgnoreCase(protocol)))
{
// If the user provided a non-generic default protocol, append it to accepted_protocols - they wanted
// it after all.
return ImmutableList.<String>builder().addAll(accepted_protocols).add(protocol).build();
}
else
{
return accepted_protocols;
}
return sslContextFactoryInstance.getAcceptedProtocols();
}
public String[] acceptedProtocolsArray()
@ -286,11 +378,6 @@ public class EncryptionOptions
return ap == null ? new String[0] : ap.toArray(new String[0]);
}
public String[] cipherSuitesArray()
{
return cipher_suites == null ? new String[0] : cipher_suites.toArray(new String[0]);
}
public TlsEncryptionPolicy tlsEncryptionPolicy()
{
if (isOptional())
@ -307,104 +394,126 @@ public class EncryptionOptions
}
}
public EncryptionOptions withSslContextFactory(ParameterizedClass sslContextFactoryClass) {
return new EncryptionOptions(sslContextFactoryClass, keystore, keystore_password, truststore,
truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification,enabled,
optional).applyConfig();
}
public EncryptionOptions withKeyStore(String keystore)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withKeyStorePassword(String keystore_password)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withTrustStore(String truststore)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withTrustStorePassword(String truststore_password)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withCipherSuites(List<String> cipher_suites)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withCipherSuites(String ... cipher_suites)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, ImmutableList.copyOf(cipher_suites),
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, ImmutableList.copyOf(cipher_suites), protocol,
accepted_protocols, algorithm, store_type, require_client_auth,
require_endpoint_verification, enabled, optional).applyConfig();
}
public EncryptionOptions withProtocol(String protocol)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withAcceptedProtocols(List<String> accepted_protocols)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites, protocol,
accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols),
algorithm, store_type, require_client_auth, require_endpoint_verification, enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites,protocol, accepted_protocols == null ? null :
ImmutableList.copyOf(accepted_protocols),
algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
}
public EncryptionOptions withAlgorithm(String algorithm)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withStoreType(String store_type)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withRequireClientAuth(boolean require_client_auth)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withEnabled(boolean enabled)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
public EncryptionOptions withOptional(Boolean optional)
{
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
enabled, optional).applyConfig();
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
store_type, require_client_auth, require_endpoint_verification, enabled,
optional).applyConfig();
}
/**
@ -432,7 +541,8 @@ public class EncryptionOptions
Objects.equals(accepted_protocols, opt.accepted_protocols) &&
Objects.equals(algorithm, opt.algorithm) &&
Objects.equals(store_type, opt.store_type) &&
Objects.equals(cipher_suites, opt.cipher_suites);
Objects.equals(cipher_suites, opt.cipher_suites) &&
Objects.equals(ssl_context_factory, opt.ssl_context_factory);
}
/**
@ -456,6 +566,7 @@ public class EncryptionOptions
result += 31 * (cipher_suites == null ? 0 : cipher_suites.hashCode());
result += 31 * Boolean.hashCode(require_client_auth);
result += 31 * Boolean.hashCode(require_endpoint_verification);
result += 31 * (ssl_context_factory == null ? 0 : ssl_context_factory.hashCode());
return result;
}
@ -475,16 +586,16 @@ public class EncryptionOptions
this.enable_legacy_ssl_storage_port = false;
}
public ServerEncryptionOptions(String keystore, String keystore_password, String truststore,
String truststore_password, List<String> cipher_suites, String protocol,
List<String> accepted_protocols, String algorithm, String store_type,
boolean require_client_auth, boolean require_endpoint_verification,
Boolean optional, InternodeEncryption internode_encryption,
boolean enable_legacy_ssl_storage_port)
public ServerEncryptionOptions(ParameterizedClass sslContextFactoryClass, String keystore,
String keystore_password, String truststore, String truststore_password,
List<String> cipher_suites, String protocol, List<String> accepted_protocols,
String algorithm, String store_type, boolean require_client_auth,
boolean require_endpoint_verification, Boolean optional,
InternodeEncryption internode_encryption, boolean enable_legacy_ssl_storage_port)
{
super(keystore, keystore_password, truststore, truststore_password, cipher_suites, protocol,
accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
null, optional);
super(sslContextFactoryClass, keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
null, optional);
this.internode_encryption = internode_encryption;
this.enable_legacy_ssl_storage_port = enable_legacy_ssl_storage_port;
}
@ -562,110 +673,148 @@ public class EncryptionOptions
return optional != null && optional;
}
public ServerEncryptionOptions withSslContextFactory(ParameterizedClass sslContextFactoryClass)
{
return new ServerEncryptionOptions(sslContextFactoryClass, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withKeyStore(String keystore)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withKeyStorePassword(String keystore_password)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withTrustStore(String truststore)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withTrustStorePassword(String truststore_password)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withCipherSuites(List<String> cipher_suites)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withCipherSuites(String ... cipher_suites)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, ImmutableList.copyOf(cipher_suites),
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, Arrays.asList(cipher_suites), protocol,
accepted_protocols, algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withProtocol(String protocol)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withAcceptedProtocols(List<String> accepted_protocols)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols),
algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withAlgorithm(String algorithm)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withStoreType(String store_type)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withRequireClientAuth(boolean require_client_auth)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withOptional(boolean optional)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withInternodeEncryption(InternodeEncryption internode_encryption)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
public ServerEncryptionOptions withLegacySslStoragePort(boolean enable_legacy_ssl_storage_port)
{
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
truststore_password, cipher_suites, protocol, accepted_protocols,
algorithm, store_type, require_client_auth,
require_endpoint_verification, optional, internode_encryption,
enable_legacy_ssl_storage_port).applyConfigInternal();
}
}

View File

@ -59,6 +59,12 @@ public class ParameterizedClass
return Objects.equal(class_name, that.class_name) && Objects.equal(parameters, that.parameters);
}
@Override
public int hashCode()
{
return Objects.hashCode(class_name, parameters);
}
@Override
public String toString()
{

View File

@ -50,6 +50,7 @@ import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.OutboundConnectionSettings.Framing;
import org.apache.cassandra.security.ISslContextFactory;
import org.apache.cassandra.security.SSLFactory;
import org.apache.cassandra.streaming.async.StreamingInboundHandler;
import org.apache.cassandra.utils.memory.BufferPools;
@ -495,8 +496,9 @@ public class InboundConnectionInitiator
private static SslHandler getSslHandler(String description, Channel channel, EncryptionOptions.ServerEncryptionOptions encryptionOptions) throws IOException
{
final boolean buildTrustStore = true;
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, buildTrustStore, SSLFactory.SocketType.SERVER);
final boolean verifyPeerCertificate = true;
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, verifyPeerCertificate,
ISslContextFactory.SocketType.SERVER);
InetSocketAddress peer = encryptionOptions.require_endpoint_verification ? (InetSocketAddress) channel.remoteAddress() : null;
SslHandler sslHandler = newSslHandler(channel, sslContext, peer);
logger.trace("{} inbound netty SslContext: context={}, engine={}", description, sslContext.getClass().getName(), sslHandler.engine().getClass().getName());

View File

@ -53,6 +53,7 @@ import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.net.HandshakeProtocol.Initiate;
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.MessagingSuccess;
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.StreamingSuccess;
import org.apache.cassandra.security.ISslContextFactory;
import org.apache.cassandra.security.SSLFactory;
import org.apache.cassandra.utils.JVMStabilityInspector;
import org.apache.cassandra.utils.memory.BufferPools;
@ -201,7 +202,8 @@ public class OutboundConnectionInitiator<SuccessType extends OutboundConnectionI
if (settings.withEncryption())
{
// check if we should actually encrypt this connection
SslContext sslContext = SSLFactory.getOrCreateSslContext(settings.encryption, true, SSLFactory.SocketType.CLIENT);
SslContext sslContext = SSLFactory.getOrCreateSslContext(settings.encryption, true,
ISslContextFactory.SocketType.CLIENT);
// for some reason channel.remoteAddress() will return null
InetAddressAndPort address = settings.to;
InetSocketAddress peer = settings.encryption.require_endpoint_verification ? new InetSocketAddress(address.address, address.port) : null;

View File

@ -0,0 +1,266 @@
/*
* 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.security;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import com.google.common.collect.ImmutableList;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import org.apache.cassandra.config.Config;
/**
* Abstract class implementing {@code ISslContextFacotry} to provide most of the functionality that any
* implementation might need. This does not assume any file-based credentials for keys/certs hence provide a good base
* for any implementation that only need to customize the loading of keys/certs in a custom way.
* <p>
* {@code CAUTION:} While this is extremely useful abstraction, please be careful if you need to modify this class
* given possible custom implementations out there!
*
* @see DefaultSslContextFactory
*/
abstract public class AbstractSslContextFactory implements ISslContextFactory
{
/*
This list is substituted in configurations that have explicitly specified the original "TLS" default,
by extracting it from the default "TLS" SSL Context instance
*/
static protected final List<String> TLS_PROTOCOL_SUBSTITUTION = SSLFactory.tlsInstanceProtocolSubstitution();
protected boolean openSslIsAvailable;
protected final Map<String, Object> parameters;
protected final List<String> cipher_suites;
protected final String protocol;
protected final List<String> accepted_protocols;
protected final String algorithm;
protected final String store_type;
protected final boolean require_client_auth;
protected final boolean require_endpoint_verification;
/*
ServerEncryptionOptions does not use the enabled flag at all instead using the existing
internode_encryption option. So we force this protected and expose through isEnabled
so users of ServerEncryptionOptions can't accidentally use this when they should use isEnabled
Long term we need to refactor ClientEncryptionOptions and ServerEncryptionOptions to be separate
classes so we can choose appropriate configuration for each.
See CASSANDRA-15262 and CASSANDRA-15146
*/
protected Boolean enabled;
protected Boolean optional;
/* For test only */
protected AbstractSslContextFactory()
{
parameters = new HashMap<>();
cipher_suites = null;
protocol = null;
accepted_protocols = null;
algorithm = null;
store_type = "JKS";
require_client_auth = false;
require_endpoint_verification = false;
enabled = null;
optional = null;
deriveIfOpenSslAvailable();
}
protected AbstractSslContextFactory(Map<String, Object> parameters)
{
this.parameters = parameters;
cipher_suites = getStringList("cipher_suites");
protocol = getString("protocol");
accepted_protocols = getStringList("accepted_protocols");
algorithm = getString("algorithm");
store_type = getString("store_type", "JKS");
require_client_auth = getBoolean("require_client_auth", false);
require_endpoint_verification = getBoolean("require_endpoint_verification", false);
enabled = getBoolean("enabled");
optional = getBoolean("optional");
deriveIfOpenSslAvailable();
}
/**
* Dervies if {@code OpenSSL} is available. It allows in-jvm dtests to disable tcnative openssl support by
* setting {@code cassandra.disable_tcactive_openssl} system property as {@code true}. Otherwise, it creates a
* circular reference that prevents the instance class loader from being garbage collected.
*/
protected void deriveIfOpenSslAvailable()
{
if (Boolean.getBoolean(Config.PROPERTY_PREFIX + "disable_tcactive_openssl"))
openSslIsAvailable = false;
else
openSslIsAvailable = OpenSsl.isAvailable();
}
protected String getString(String key, String defaultValue)
{
return parameters.get(key) == null ? defaultValue : (String) parameters.get(key);
}
protected String getString(String key)
{
return (String) parameters.get(key);
}
protected List<String> getStringList(String key)
{
return (List<String>) parameters.get(key);
}
protected Boolean getBoolean(String key, boolean defaultValue)
{
return parameters.get(key) == null ? defaultValue : (Boolean) parameters.get(key);
}
protected Boolean getBoolean(String key)
{
return (Boolean) this.parameters.get(key);
}
@Override
public SSLContext createJSSESslContext(boolean verifyPeerCertificate) throws SSLException
{
TrustManager[] trustManagers = null;
if (verifyPeerCertificate)
trustManagers = buildTrustManagerFactory().getTrustManagers();
KeyManagerFactory kmf = buildKeyManagerFactory();
try
{
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), trustManagers, null);
return ctx;
}
catch (Exception e)
{
throw new SSLException("Error creating/initializing the SSL Context", e);
}
}
@Override
public SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType,
CipherSuiteFilter cipherFilter) throws SSLException
{
/*
There is a case where the netty/openssl combo might not support using KeyManagerFactory. Specifically,
I've seen this with the netty-tcnative dynamic openssl implementation. Using the netty-tcnative
static-boringssl works fine with KeyManagerFactory. If we want to support all of the netty-tcnative
options, we would need to fall back to passing in a file reference for both a x509 and PKCS#8 private
key file in PEM format (see {@link SslContextBuilder#forServer(File, File, String)}). However, we are
not supporting that now to keep the config/yaml API simple.
*/
KeyManagerFactory kmf = buildKeyManagerFactory();
SslContextBuilder builder;
if (socketType == SocketType.SERVER)
{
builder = SslContextBuilder.forServer(kmf).clientAuth(this.require_client_auth ? ClientAuth.REQUIRE :
ClientAuth.NONE);
}
else
{
builder = SslContextBuilder.forClient().keyManager(kmf);
}
builder.sslProvider(getSslProvider()).protocols(getAcceptedProtocols());
// only set the cipher suites if the operator has explicity configured values for it; else, use the default
// for each ssl implemention (jdk or openssl)
if (cipher_suites != null && !cipher_suites.isEmpty())
builder.ciphers(cipher_suites, cipherFilter);
if (verifyPeerCertificate)
builder.trustManager(buildTrustManagerFactory());
return builder.build();
}
/**
* Combine the pre-4.0 protocol field with the accepted_protocols list, substituting a list of
* explicit protocols for the previous catchall default of "TLS"
*
* @return array of protocol names suitable for passing to SslContextBuilder.protocols, or null if the default
*/
@Override
public List<String> getAcceptedProtocols()
{
if (accepted_protocols == null)
{
if (protocol == null)
{
return null;
}
// TLS is accepted by SSLContext.getInstance as a shorthand for give me an engine that
// can speak some TLS protocols. It is not supported by SSLEngine.setAcceptedProtocols
// so substitute if the user hasn't provided an accepted protocol configuration
else if (protocol.equalsIgnoreCase("TLS"))
{
return TLS_PROTOCOL_SUBSTITUTION;
}
else // the user was trying to limit to a single specific protocol, so try that
{
return ImmutableList.of(protocol);
}
}
if (protocol != null && !protocol.equalsIgnoreCase("TLS") &&
accepted_protocols.stream().noneMatch(ap -> ap.equalsIgnoreCase(protocol)))
{
// If the user provided a non-generic default protocol, append it to accepted_protocols - they wanted
// it after all.
return ImmutableList.<String>builder().addAll(accepted_protocols).add(protocol).build();
}
else
{
return accepted_protocols;
}
}
@Override
public List<String> getCipherSuites()
{
return cipher_suites;
}
/**
* Returns {@link SslProvider} to be used to build Netty's SslContext.
*
* @return appropriate SslProvider
*/
protected SslProvider getSslProvider()
{
return openSslIsAvailable ? SslProvider.OPENSSL : SslProvider.JDK;
}
abstract protected KeyManagerFactory buildKeyManagerFactory() throws SSLException;
abstract protected TrustManagerFactory buildTrustManagerFactory() throws SSLException;
}

View File

@ -0,0 +1,33 @@
/*
* 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.security;
import java.util.Map;
/**
* Cassandra's default implementation class for the configuration key {@code ssl_context_factory}. It uses
* file based keystores.
*/
public final class DefaultSslContextFactory extends FileBasedSslContextFactory
{
public DefaultSslContextFactory(Map<String, Object> parameters)
{
super(parameters);
}
}

View File

@ -0,0 +1,211 @@
/*
* 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.security;
import java.io.File;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract implementation for {@link ISslContextFactory} using file based, standard keystore format with the ability
* to hot-reload the files upon file changes (detected by the {@code last modified timestamp}).
* <p>
* {@code CAUTION:} While this is a useful abstraction, please be careful if you need to modify this class
* given possible custom implementations out there!
*/
abstract public class FileBasedSslContextFactory extends AbstractSslContextFactory
{
private static final Logger logger = LoggerFactory.getLogger(FileBasedSslContextFactory.class);
@VisibleForTesting
protected volatile boolean checkedExpiry = false;
/**
* List of files that trigger hot reloading of SSL certificates
*/
protected volatile List<HotReloadableFile> hotReloadableFiles = new ArrayList<>();
protected String keystore;
protected String keystore_password;
protected String truststore;
protected String truststore_password;
public FileBasedSslContextFactory()
{
keystore = "conf/.keystore";
keystore_password = "cassandra";
truststore = "conf/.truststore";
truststore_password = "cassandra";
}
public FileBasedSslContextFactory(Map<String, Object> parameters)
{
super(parameters);
keystore = getString("keystore");
keystore_password = getString("keystore_password");
truststore = getString("truststore");
truststore_password = getString("truststore_password");
}
@Override
public boolean shouldReload()
{
return hotReloadableFiles.stream().anyMatch(HotReloadableFile::shouldReload);
}
@Override
public boolean hasKeystore()
{
return keystore != null && new File(keystore).exists();
}
private boolean hasTruststore()
{
return truststore != null && new File(truststore).exists();
}
@Override
public synchronized void initHotReloading()
{
boolean hasKeystore = hasKeystore();
boolean hasTruststore = hasTruststore();
if (hasKeystore || hasTruststore)
{
List<HotReloadableFile> fileList = new ArrayList<>();
if (hasKeystore)
{
fileList.add(new HotReloadableFile(keystore));
}
if (hasTruststore)
{
fileList.add(new HotReloadableFile(truststore));
}
hotReloadableFiles = fileList;
}
}
/**
* Builds required KeyManagerFactory from the file based keystore. It also checks for the PrivateKey's certificate's
* expiry and logs {@code warning} for each expired PrivateKey's certitificate.
*
* @return KeyManagerFactory built from the file based keystore.
* @throws SSLException if any issues encountered during the build process
*/
protected KeyManagerFactory buildKeyManagerFactory() throws SSLException
{
try (InputStream ksf = Files.newInputStream(Paths.get(keystore)))
{
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
algorithm == null ? KeyManagerFactory.getDefaultAlgorithm() : algorithm);
KeyStore ks = KeyStore.getInstance(store_type);
ks.load(ksf, keystore_password.toCharArray());
if (!checkedExpiry)
{
for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements(); )
{
String alias = aliases.nextElement();
if (ks.getCertificate(alias).getType().equals("X.509"))
{
Date expires = ((X509Certificate) ks.getCertificate(alias)).getNotAfter();
if (expires.before(new Date()))
logger.warn("Certificate for {} expired on {}", alias, expires);
}
}
checkedExpiry = true;
}
kmf.init(ks, keystore_password.toCharArray());
return kmf;
}
catch (Exception e)
{
throw new SSLException("failed to build key manager store for secure connections", e);
}
}
/**
* Builds TrustManagerFactory from the file based truststore.
*
* @return TrustManagerFactory from the file based truststore
* @throws SSLException if any issues encountered during the build process
*/
protected TrustManagerFactory buildTrustManagerFactory() throws SSLException
{
try (InputStream tsf = Files.newInputStream(Paths.get(truststore)))
{
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
algorithm == null ? TrustManagerFactory.getDefaultAlgorithm() : algorithm);
KeyStore ts = KeyStore.getInstance(store_type);
ts.load(tsf, truststore_password.toCharArray());
tmf.init(ts);
return tmf;
}
catch (Exception e)
{
throw new SSLException("failed to build trust manager store for secure connections", e);
}
}
/**
* Helper class for hot reloading SSL Contexts
*/
private static class HotReloadableFile
{
private final File file;
private volatile long lastModTime;
HotReloadableFile(String path)
{
file = new File(path);
lastModTime = file.lastModified();
}
boolean shouldReload()
{
long curModTime = file.lastModified();
boolean result = curModTime != lastModTime;
lastModTime = curModTime;
return result;
}
@Override
public String toString()
{
return "HotReloadableFile{" +
"file=" + file +
", lastModTime=" + lastModTime +
'}';
}
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.security;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslContext;
/**
* The purpose of this interface is to provide pluggable mechanism for creating custom JSSE and Netty SSLContext
* objects. Please use the Cassandra configuration key {@code ssl_context_factory} as part of {@code
* client_encryption_options}/{@code server_encryption_options} and provide a custom class-name implementing this
* interface with parameters to be used to plugin a your own way to load the SSLContext.
* <p>
* Implementation of this interface must have a constructor with argument of type {@code Map<String,Object>} to allow
* custom parameters, needed by the implementation, to be passed from the yaml configuration. Common SSL
* configurations like {@code protocol, algorithm, cipher_suites, accepted_protocols, require_client_auth,
* require_endpoint_verification, enabled, optional} will also be passed to that map by Cassanddra.
* <p>
* Since on top of Netty, Cassandra is internally using JSSE SSLContext also for certain use-cases- this interface
* has methods for both.
* <p>
* Below is an example of how to configure a custom implementation with parameters
* <pre>
* ssl_context_factory:
* class_name: org.apache.cassandra.security.YourSslContextFactoryImpl
* parameters:
* key1: "value1"
* key2: "value2"
* key3: "value3"
* </pre>
*/
public interface ISslContextFactory
{
/**
* Creates JSSE SSLContext.
*
* @param verifyPeerCertificate {@code true} if SSL peer's certificate needs to be verified; {@code false} otherwise
* @return JSSE's {@link SSLContext}
* @throws SSLException in case the Ssl Context creation fails for some reason
*/
SSLContext createJSSESslContext(boolean verifyPeerCertificate) throws SSLException;
/**
* Creates Netty's SslContext object.
*
* @param verifyPeerCertificate {@code true} if SSL peer's certificate needs to be verified; {@code false} otherwise
* @param socketType {@link SocketType} for Netty's Inbound or Outbound channels
* @param cipherFilter to allow Netty's cipher suite filtering, e.g.
* {@link io.netty.handler.ssl.SslContextBuilder#ciphers(Iterable, CipherSuiteFilter)}
* @return Netty's {@link SslContext}
* @throws SSLException in case the Ssl Context creation fails for some reason
*/
SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType,
CipherSuiteFilter cipherFilter) throws SSLException;
/**
* Initializes hot reloading of the security keys/certs. The implementation must guarantee this to be thread safe.
*
* @throws SSLException
*/
void initHotReloading() throws SSLException;
/**
* Returns if any changes require the reloading of the SSL context returned by this factory.
* This will be called by Cassandra's periodic polling for any potential changes that will reload the SSL context.
* However only newer connections established after the reload will use the reloaded SSL context.
*
* @return {@code true} if SSL Context needs to be reload; {@code false} otherwise
*/
boolean shouldReload();
/**
* Returns if this factory uses private keystore.
*
* @return {@code true} by default unless the implementation overrides this
*/
default boolean hasKeystore()
{
return true;
}
/**
* Returns the prepared list of accepted protocols.
*
* @return array of protocol names suitable for passing to Netty's SslContextBuilder.protocols, or null if the
* default
*/
List<String> getAcceptedProtocols();
/**
* Returns the list of cipher suites supported by the implementation.
*
* @return List of supported cipher suites
*/
List<String> getCipherSuites();
/**
* Indicates if the process holds the inbound/listening (Server) end of the socket or the outbound side (Client).
*/
enum SocketType
{
SERVER, CLIENT;
}
}

View File

@ -18,48 +18,33 @@
package org.apache.cassandra.security;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.ClientAuth;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.util.ReferenceCountUtil;
import org.apache.cassandra.concurrent.ScheduledExecutors;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.security.ISslContextFactory.SocketType;
/**
* A Factory for providing and setting up client {@link SSLSocket}s. Also provides
@ -73,18 +58,6 @@ public final class SSLFactory
{
private static final Logger logger = LoggerFactory.getLogger(SSLFactory.class);
/**
* Indicates if the process holds the inbound/listening end of the socket ({@link SocketType#SERVER})), or the
* outbound side ({@link SocketType#CLIENT}).
*/
public enum SocketType
{
SERVER, CLIENT
}
@VisibleForTesting
static volatile boolean checkedExpiry = false;
// Isolate calls to OpenSsl.isAvailable to allow in-jvm dtests to disable tcnative openssl
// support. It creates a circular reference that prevents the instance class loader from being
// garbage collected.
@ -110,11 +83,6 @@ public final class SSLFactory
*/
private static final ConcurrentHashMap<CacheKey, SslContext> cachedSslContexts = new ConcurrentHashMap<>();
/**
* List of files that trigger hot reloading of SSL certificates
*/
private static volatile List<HotReloadableFile> hotReloadableFiles = ImmutableList.of();
/**
* Default initial delay for hot reloading
*/
@ -130,38 +98,6 @@ public final class SSLFactory
*/
private static boolean isHotReloadingInitialized = false;
/**
* Helper class for hot reloading SSL Contexts
*/
private static class HotReloadableFile
{
private final File file;
private volatile long lastModTime;
HotReloadableFile(String path)
{
file = new File(path);
lastModTime = file.lastModified();
}
boolean shouldReload()
{
long curModTime = file.lastModified();
boolean result = curModTime != lastModTime;
lastModTime = curModTime;
return result;
}
@Override
public String toString()
{
return "HotReloadableFile{" +
"file=" + file +
", lastModTime=" + lastModTime +
'}';
}
}
/** 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
@ -185,100 +121,25 @@ public final class SSLFactory
/**
* Create a JSSE {@link SSLContext}.
*/
public static SSLContext createSSLContext(EncryptionOptions options, boolean buildTruststore) throws IOException
public static SSLContext createSSLContext(EncryptionOptions options, boolean verifyPeerCertificate) throws IOException
{
TrustManager[] trustManagers = null;
if (buildTruststore)
trustManagers = buildTrustManagerFactory(options).getTrustManagers();
KeyManagerFactory kmf = buildKeyManagerFactory(options);
try
{
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), trustManagers, null);
return ctx;
}
catch (Exception e)
{
throw new IOException("Error creating/initializing the SSL Context", e);
}
}
static TrustManagerFactory buildTrustManagerFactory(EncryptionOptions options) throws IOException
{
try (InputStream tsf = Files.newInputStream(Paths.get(options.truststore)))
{
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
options.algorithm == null ? TrustManagerFactory.getDefaultAlgorithm() : options.algorithm);
KeyStore ts = KeyStore.getInstance(options.store_type);
ts.load(tsf, options.truststore_password.toCharArray());
tmf.init(ts);
return tmf;
}
catch (Exception e)
{
throw new IOException("failed to build trust manager store for secure connections", e);
}
}
static KeyManagerFactory buildKeyManagerFactory(EncryptionOptions options) throws IOException
{
try (InputStream ksf = Files.newInputStream(Paths.get(options.keystore)))
{
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
options.algorithm == null ? KeyManagerFactory.getDefaultAlgorithm() : options.algorithm);
KeyStore ks = KeyStore.getInstance(options.store_type);
ks.load(ksf, options.keystore_password.toCharArray());
if (!checkedExpiry)
{
for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements(); )
{
String alias = aliases.nextElement();
if (ks.getCertificate(alias).getType().equals("X.509"))
{
Date expires = ((X509Certificate) ks.getCertificate(alias)).getNotAfter();
if (expires.before(new Date()))
logger.warn("Certificate for {} expired on {}", alias, expires);
}
}
checkedExpiry = true;
}
kmf.init(ks, options.keystore_password.toCharArray());
return kmf;
}
catch (Exception e)
{
throw new IOException("failed to build key manager store for secure connections", e);
}
return options.sslContextFactoryInstance.createJSSESslContext(verifyPeerCertificate);
}
/**
* get a netty {@link SslContext} instance
*/
public static SslContext getOrCreateSslContext(EncryptionOptions options, boolean buildTruststore,
public static SslContext getOrCreateSslContext(EncryptionOptions options, boolean verifyPeerCertificate,
SocketType socketType) throws IOException
{
return getOrCreateSslContext(options, buildTruststore, socketType, openSslIsAvailable());
}
/**
* Get a netty {@link SslContext} instance.
*/
@VisibleForTesting
static SslContext getOrCreateSslContext(EncryptionOptions options,
boolean buildTruststore,
SocketType socketType,
boolean useOpenSsl) throws IOException
{
CacheKey key = new CacheKey(options, socketType, useOpenSsl);
CacheKey key = new CacheKey(options, socketType);
SslContext sslContext;
sslContext = cachedSslContexts.get(key);
if (sslContext != null)
return sslContext;
sslContext = createNettySslContext(options, buildTruststore, socketType, useOpenSsl);
sslContext = createNettySslContext(options, verifyPeerCertificate, socketType);
SslContext previous = cachedSslContexts.putIfAbsent(key, sslContext);
if (previous == null)
@ -291,52 +152,21 @@ public final class SSLFactory
/**
* Create a Netty {@link SslContext}
*/
static SslContext createNettySslContext(EncryptionOptions options, boolean buildTruststore,
SocketType socketType, boolean useOpenSsl) throws IOException
static SslContext createNettySslContext(EncryptionOptions options, boolean verifyPeerCertificate,
SocketType socketType) throws IOException
{
return createNettySslContext(options, buildTruststore, socketType, useOpenSsl,
return createNettySslContext(options, verifyPeerCertificate, socketType,
LoggingCipherSuiteFilter.QUIET_FILTER);
}
/**
* Create a Netty {@link SslContext} with a supplied cipherFilter
*/
static SslContext createNettySslContext(EncryptionOptions options, boolean buildTruststore,
SocketType socketType, boolean useOpenSsl, CipherSuiteFilter cipherFilter) throws IOException
static SslContext createNettySslContext(EncryptionOptions options, boolean verifyPeerCertificate,
SocketType socketType, CipherSuiteFilter cipherFilter) throws IOException
{
/*
There is a case where the netty/openssl combo might not support using KeyManagerFactory. specifically,
I've seen this with the netty-tcnative dynamic openssl implementation. using the netty-tcnative static-boringssl
works fine with KeyManagerFactory. If we want to support all of the netty-tcnative options, we would need
to fall back to passing in a file reference for both a x509 and PKCS#8 private key file in PEM format (see
{@link SslContextBuilder#forServer(File, File, String)}). However, we are not supporting that now to keep
the config/yaml API simple.
*/
KeyManagerFactory kmf = buildKeyManagerFactory(options);
SslContextBuilder builder;
if (socketType == SocketType.SERVER)
{
builder = SslContextBuilder.forServer(kmf);
builder.clientAuth(options.require_client_auth ? ClientAuth.REQUIRE : ClientAuth.NONE);
}
else
{
builder = SslContextBuilder.forClient().keyManager(kmf);
}
builder.sslProvider(useOpenSsl ? SslProvider.OPENSSL : SslProvider.JDK);
builder.protocols(options.acceptedProtocols());
// only set the cipher suites if the opertor has explicity configured values for it; else, use the default
// for each ssl implemention (jdk or openssl)
if (options.cipher_suites != null && !options.cipher_suites.isEmpty())
builder.ciphers(options.cipher_suites, cipherFilter);
if (buildTruststore)
builder.trustManager(buildTrustManagerFactory(options));
return builder.build();
return options.sslContextFactoryInstance.createNettySslContext(verifyPeerCertificate, socketType,
cipherFilter);
}
/**
@ -351,21 +181,58 @@ public final class SSLFactory
if (!isHotReloadingInitialized)
throw new IllegalStateException("Hot reloading functionality has not been initialized.");
logger.debug("Checking whether certificates have been updated {}", hotReloadableFiles);
logger.debug("Checking whether certificates have been updated for server {} and client {}",
serverOpts.sslContextFactoryInstance.getClass().getName(), clientOpts.sslContextFactoryInstance.getClass().getName());
if (hotReloadableFiles.stream().anyMatch(HotReloadableFile::shouldReload))
if (serverOpts != null)
{
logger.info("SSL certificates have been updated. Reseting the ssl contexts for new connections.");
try
checkCertFilesForHotReloading(serverOpts, "server_encryption_options", true);
}
if (clientOpts != null)
{
checkCertFilesForHotReloading(clientOpts, "client_encryption_options", clientOpts.require_client_auth);
}
}
private static void checkCertFilesForHotReloading(EncryptionOptions options, String contextDescription,
boolean verifyPeerCertificate)
{
try
{
if (options.sslContextFactoryInstance.shouldReload())
{
validateSslCerts(serverOpts, clientOpts);
cachedSslContexts.clear();
}
catch(Exception e)
{
logger.error("Failed to hot reload the SSL Certificates! Please check the certificate files.", e);
logger.info("SSL certificates have been updated for {}. Resetting the ssl contexts for new " +
"connections.", options.getClass().getName());
validateSslContext(contextDescription, options, verifyPeerCertificate, false);
clearSslContextCache(options);
}
}
catch(Exception e)
{
logger.error("Failed to hot reload the SSL Certificates! Please check the certificate files.", e);
}
}
/**
* This clears the cache of Netty's SslContext objects for Client and Server sockets. This is made publically
* available so that any {@link ISslContextFactory}'s implementation can call this to handle any special scenario
* to invalidate the SslContext cache.
* This should be used with caution since the purpose of this cache is save costly creation of Netty's SslContext
* objects and this essentially results in re-creating it.
*/
public static void clearSslContextCache()
{
cachedSslContexts.clear();
}
private static void clearSslContextCache(EncryptionOptions options)
{
cachedSslContexts.forEachKey(1, cacheKey -> {
if (cacheKey.encryptionOptions.equals(options))
{
cachedSslContexts.remove(cacheKey);
}
});
}
/**
@ -383,22 +250,14 @@ public final class SSLFactory
logger.debug("Initializing hot reloading SSLContext");
List<HotReloadableFile> fileList = new ArrayList<>();
if (serverOpts != null && serverOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED)
{
fileList.add(new HotReloadableFile(serverOpts.keystore));
fileList.add(new HotReloadableFile(serverOpts.truststore));
if ( serverOpts != null && serverOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED) {
serverOpts.sslContextFactoryInstance.initHotReloading();
}
if (clientOpts != null && clientOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED)
{
fileList.add(new HotReloadableFile(clientOpts.keystore));
fileList.add(new HotReloadableFile(clientOpts.truststore));
if ( clientOpts != null && clientOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED) {
clientOpts.sslContextFactoryInstance.initHotReloading();
}
hotReloadableFiles = ImmutableList.copyOf(fileList);
if (!isHotReloadingInitialized)
{
ScheduledExecutors.scheduledTasks
@ -485,7 +344,7 @@ public final class SSLFactory
return !string.equals("SSLv2Hello");
}
public static void validateSslContext(String contextDescription, EncryptionOptions options, boolean buildTrustStore, boolean logProtocolAndCiphers) throws IOException
public static void validateSslContext(String contextDescription, EncryptionOptions options, boolean verifyPeerCertificate, boolean logProtocolAndCiphers) throws IOException
{
if (options != null && options.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED)
{
@ -493,7 +352,7 @@ public final class SSLFactory
{
CipherSuiteFilter loggingCipherSuiteFilter = logProtocolAndCiphers ? new LoggingCipherSuiteFilter(contextDescription)
: LoggingCipherSuiteFilter.QUIET_FILTER;
SslContext serverSslContext = createNettySslContext(options, buildTrustStore, SocketType.SERVER, openSslIsAvailable(), loggingCipherSuiteFilter);
SslContext serverSslContext = createNettySslContext(options, verifyPeerCertificate, SocketType.SERVER, loggingCipherSuiteFilter);
try
{
SSLEngine engine = serverSslContext.newEngine(ByteBufAllocator.DEFAULT);
@ -538,7 +397,7 @@ public final class SSLFactory
}
// Make sure it is possible to build the client context too
SslContext clientSslContext = createNettySslContext(options, buildTrustStore, SocketType.CLIENT, openSslIsAvailable());
SslContext clientSslContext = createNettySslContext(options, verifyPeerCertificate, SocketType.CLIENT);
ReferenceCountUtil.release(clientSslContext);
}
catch (Exception e)
@ -561,13 +420,11 @@ public final class SSLFactory
{
private final EncryptionOptions encryptionOptions;
private final SocketType socketType;
private final boolean useOpenSSL;
public CacheKey(EncryptionOptions encryptionOptions, SocketType socketType, boolean useOpenSSL)
public CacheKey(EncryptionOptions encryptionOptions, SocketType socketType)
{
this.encryptionOptions = encryptionOptions;
this.socketType = socketType;
this.useOpenSSL = useOpenSSL;
}
public boolean equals(Object o)
@ -576,7 +433,6 @@ public final class SSLFactory
if (o == null || getClass() != o.getClass()) return false;
CacheKey cacheKey = (CacheKey) o;
return (socketType == cacheKey.socketType &&
useOpenSSL == cacheKey.useOpenSSL &&
Objects.equals(encryptionOptions, cacheKey.encryptionOptions));
}
@ -585,7 +441,6 @@ public final class SSLFactory
int result = 0;
result += 31 * socketType.hashCode();
result += 31 * encryptionOptions.hashCode();
result += 31 * Boolean.hashCode(useOpenSSL);
return result;
}
}

View File

@ -44,6 +44,7 @@ import io.netty.util.Version;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.net.*;
import org.apache.cassandra.security.ISslContextFactory;
import org.apache.cassandra.security.SSLFactory;
import org.apache.cassandra.transport.messages.StartupMessage;
@ -163,7 +164,7 @@ public class PipelineConfigurator
return channel -> {
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
encryptionOptions.require_client_auth,
SSLFactory.SocketType.SERVER);
ISslContextFactory.SocketType.SERVER);
channel.pipeline().addFirst(SSL_HANDLER, new ByteToMessageDecoder()
{
@ -197,7 +198,7 @@ public class PipelineConfigurator
return channel -> {
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
encryptionOptions.require_client_auth,
SSLFactory.SocketType.SERVER);
ISslContextFactory.SocketType.SERVER);
channel.pipeline().addFirst(SSL_HANDLER, sslContext.newHandler(channel.alloc()));
};
default:

View File

@ -47,6 +47,7 @@ import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.net.*;
import org.apache.cassandra.security.ISslContextFactory;
import org.apache.cassandra.security.SSLFactory;
import org.apache.cassandra.transport.messages.*;
@ -620,7 +621,7 @@ public class SimpleClient implements Closeable
{
super.initChannel(channel);
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, encryptionOptions.require_client_auth,
SSLFactory.SocketType.CLIENT);
ISslContextFactory.SocketType.CLIENT);
channel.pipeline().addFirst("ssl", sslContext.newHandler(channel.alloc()));
}
}

View File

@ -42,8 +42,8 @@ import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
import org.apache.cassandra.audit.IAuditLogger;
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.auth.IAuthorizer;
import org.apache.cassandra.auth.INetworkAuthorizer;
@ -66,6 +66,7 @@ import org.apache.cassandra.io.util.DataOutputBuffer;
import org.apache.cassandra.io.util.DataOutputBufferFixed;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.security.ISslContextFactory;
import static org.apache.cassandra.config.CassandraRelevantProperties.LINE_SEPARATOR;
import static org.apache.cassandra.config.CassandraRelevantProperties.USER_HOME;
@ -672,7 +673,7 @@ public class FBUtilities
}
return FBUtilities.construct(className, "network authorizer");
}
public static IAuditLogger newAuditLogger(String className, Map<String, String> parameters) throws ConfigurationException
{
if (!className.contains("."))
@ -689,6 +690,22 @@ public class FBUtilities
}
}
public static ISslContextFactory newSslContextFactory(String className, Map<String,Object> parameters) throws ConfigurationException
{
if (!className.contains("."))
className = "org.apache.cassandra.security." + className;
try
{
Class<?> sslContextFactoryClass = Class.forName(className);
return (ISslContextFactory) sslContextFactoryClass.getConstructor(Map.class).newInstance(parameters);
}
catch (Exception ex)
{
throw new ConfigurationException("Unable to create instance of ISslContextFactory for " + className, ex);
}
}
/**
* @return The Class for the given name.
* @param classname Fully qualified classname.

View File

@ -0,0 +1,82 @@
#
# 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.
#
#
# Testing for pluggable ssl_context_factory option for client and server encryption options with a valid and a missing
# implementation classes.
#
cluster_name: Test Cluster
# memtable_allocation_type: heap_buffers
memtable_allocation_type: offheap_objects
commitlog_sync: batch
commitlog_sync_batch_window_in_ms: 1.0
commitlog_segment_size_in_mb: 5
commitlog_directory: build/test/cassandra/commitlog
# commitlog_compression:
# - class_name: LZ4Compressor
cdc_raw_directory: build/test/cassandra/cdc_raw
cdc_enabled: false
hints_directory: build/test/cassandra/hints
partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner
listen_address: 127.0.0.1
storage_port: 7012
ssl_storage_port: 17012
start_native_transport: true
native_transport_port: 9042
column_index_size_in_kb: 4
saved_caches_directory: build/test/cassandra/saved_caches
data_file_directories:
- build/test/cassandra/data
disk_access_mode: mmap
seed_provider:
- class_name: org.apache.cassandra.locator.SimpleSeedProvider
parameters:
- seeds: "127.0.0.1:7012"
endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch
dynamic_snitch: true
client_encryption_options:
ssl_context_factory:
class_name: org.apache.cassandra.security.DummySslContextFactoryImpl
parameters:
key1: "value1"
key2: "value2"
key3: "value3"
truststore: conf/.truststore
truststore_password: cassandra
keystore: conf/.keystore
keystore_password: cassandra
server_encryption_options:
internode_encryption: none
keystore: conf/.keystore
keystore_password: cassandra
truststore: conf/.truststore
truststore_password: cassandra
incremental_backups: true
concurrent_compactors: 4
compaction_throughput_mb_per_sec: 0
row_cache_class_name: org.apache.cassandra.cache.OHCProvider
row_cache_size_in_mb: 16
enable_user_defined_functions: true
enable_scripted_user_defined_functions: true
prepared_statements_cache_size_mb: 1
corrupted_tombstone_strategy: exception
stream_entire_sstables: true
stream_throughput_outbound_megabits_per_sec: 200000000
enable_sasi_indexes: true
enable_materialized_views: true
file_cache_enabled: true

View File

@ -0,0 +1,85 @@
#
# 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.
#
#
# Testing for pluggable ssl_context_factory option for client and server encryption options with a valid and a missing
# implementation classes.
#
cluster_name: Test Cluster
# memtable_allocation_type: heap_buffers
memtable_allocation_type: offheap_objects
commitlog_sync: batch
commitlog_sync_batch_window_in_ms: 1.0
commitlog_segment_size_in_mb: 5
commitlog_directory: build/test/cassandra/commitlog
# commitlog_compression:
# - class_name: LZ4Compressor
cdc_raw_directory: build/test/cassandra/cdc_raw
cdc_enabled: false
hints_directory: build/test/cassandra/hints
partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner
listen_address: 127.0.0.1
storage_port: 7012
ssl_storage_port: 17012
start_native_transport: true
native_transport_port: 9042
column_index_size_in_kb: 4
saved_caches_directory: build/test/cassandra/saved_caches
data_file_directories:
- build/test/cassandra/data
disk_access_mode: mmap
seed_provider:
- class_name: org.apache.cassandra.locator.SimpleSeedProvider
parameters:
- seeds: "127.0.0.1:7012"
endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch
dynamic_snitch: true
client_encryption_options:
ssl_context_factory:
class_name: org.apache.cassandra.security.DummySslContextFactoryImpl
parameters:
key1: "value1"
key2: "value2"
key3: "value3"
keystore: dummy-keystore
server_encryption_options:
ssl_context_factory:
class_name: org.apache.cassandra.security.MissingSslContextFactoryImpl
parameters:
key1: "value1"
key2: "value2"
key3: "value3"
internode_encryption: none
keystore: conf/.keystore
keystore_password: cassandra
truststore: conf/.truststore
truststore_password: cassandra
incremental_backups: true
concurrent_compactors: 4
compaction_throughput_mb_per_sec: 0
row_cache_class_name: org.apache.cassandra.cache.OHCProvider
row_cache_size_in_mb: 16
enable_user_defined_functions: true
enable_scripted_user_defined_functions: true
prepared_statements_cache_size_mb: 1
corrupted_tombstone_strategy: exception
stream_entire_sstables: true
stream_throughput_outbound_megabits_per_sec: 200000000
enable_sasi_indexes: true
enable_materialized_views: true
file_cache_enabled: true

View File

@ -50,6 +50,7 @@ import io.netty.util.concurrent.FutureListener;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.distributed.Cluster;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.security.ISslContextFactory;
import org.apache.cassandra.security.SSLFactory;
import org.apache.cassandra.utils.concurrent.SimpleCondition;
@ -194,8 +195,8 @@ public class AbstractEncryptionOptionsImpl extends TestBaseImpl
setProtocolAndCipher(null, null);
SslContext sslContext = SSLFactory.getOrCreateSslContext(
encryptionOptions.withAcceptedProtocols(acceptedProtocols).withCipherSuites(cipherSuites),
true, SSLFactory.SocketType.CLIENT);
encryptionOptions.withAcceptedProtocols(acceptedProtocols).withCipherSuites(cipherSuites),
true, ISslContextFactory.SocketType.CLIENT);
EventLoopGroup workerGroup = new NioEventLoopGroup();
Bootstrap b = new Bootstrap();

View File

@ -149,6 +149,7 @@ public class DatabaseDescriptorRefTest
"org.apache.cassandra.locator.Replica",
"org.apache.cassandra.locator.SimpleSeedProvider",
"org.apache.cassandra.locator.SeedProvider",
"org.apache.cassandra.security.ISslContextFactory",
"org.apache.cassandra.security.SSLFactory",
"org.apache.cassandra.security.EncryptionContext",
"org.apache.cassandra.service.CacheService$CacheType",

View File

@ -265,7 +265,7 @@ public class DatabaseDescriptorTest
}
}
}
@Test
public void testTokensFromString()
{
@ -337,7 +337,7 @@ public class DatabaseDescriptorTest
testConfig.cas_contention_timeout_in_ms = DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT + 1;
testConfig.counter_write_request_timeout_in_ms = DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT + 1;
testConfig.request_timeout_in_ms = DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT + 1;
assertTrue(testConfig.read_request_timeout_in_ms > DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT);
assertTrue(testConfig.range_request_timeout_in_ms > DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT);
assertTrue(testConfig.write_request_timeout_in_ms > DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT);
@ -741,4 +741,15 @@ public class DatabaseDescriptorTest
conf.track_warnings.row_index_size.abort_threshold_kb = 2;
DatabaseDescriptor.applyTrackWarningsValidations(conf);
}
@Test
public void testDefaultSslContextFactoryConfiguration() {
Config config = DatabaseDescriptor.loadConfig();
Assert.assertEquals("org.apache.cassandra.security.DefaultSslContextFactory",
config.client_encryption_options.ssl_context_factory.class_name);
Assert.assertTrue(config.client_encryption_options.ssl_context_factory.parameters.isEmpty());
Assert.assertEquals("org.apache.cassandra.security.DefaultSslContextFactory",
config.server_encryption_options.ssl_context_factory.class_name);
Assert.assertTrue(config.server_encryption_options.ssl_context_factory.parameters.isEmpty());
}
}

View File

@ -0,0 +1,142 @@
/*
* 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.config;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.apache.cassandra.security.DefaultSslContextFactory;
import org.apache.cassandra.security.DummySslContextFactoryImpl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
/**
* This class tests the equals and hashCode method of {@link EncryptionOptions} in order to make sure that the
* caching done in the {@link org.apache.cassandra.security.SSLFactory} doesn't break.
*/
public class EncryptionOptionsEqualityTest
{
@Test
public void testKeystoreOptions() {
EncryptionOptions encryptionOptions1 =
new EncryptionOptions()
.withStoreType("JKS")
.withKeyStore("test/conf/cassandra.keystore")
.withKeyStorePassword("cassandra")
.withTrustStore("test/conf/cassandra_ssl_test.truststore")
.withTrustStorePassword("cassandra")
.withProtocol("TLSv1.1")
.withRequireClientAuth(true)
.withRequireEndpointVerification(false);
EncryptionOptions encryptionOptions2 =
new EncryptionOptions()
.withStoreType("JKS")
.withKeyStore("test/conf/cassandra.keystore")
.withKeyStorePassword("cassandra")
.withTrustStore("test/conf/cassandra_ssl_test.truststore")
.withTrustStorePassword("cassandra")
.withProtocol("TLSv1.1")
.withRequireClientAuth(true)
.withRequireEndpointVerification(false);
assertEquals(encryptionOptions1, encryptionOptions2);
assertEquals(encryptionOptions1.hashCode(), encryptionOptions2.hashCode());
}
@Test
public void testSameCustomSslContextFactoryImplementation() {
Map<String,String> parameters1 = new HashMap<>();
parameters1.put("key1", "value1");
parameters1.put("key2", "value2");
EncryptionOptions encryptionOptions1 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
.withProtocol("TLSv1.1")
.withRequireClientAuth(true)
.withRequireEndpointVerification(false);
Map<String,String> parameters2 = new HashMap<>();
parameters2.put("key1", "value1");
parameters2.put("key2", "value2");
EncryptionOptions encryptionOptions2 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
.withProtocol("TLSv1.1")
.withRequireClientAuth(true)
.withRequireEndpointVerification(false);
assertEquals(encryptionOptions1, encryptionOptions2);
assertEquals(encryptionOptions1.hashCode(), encryptionOptions2.hashCode());
}
@Test
public void testDifferentCustomSslContextFactoryImplementations() {
Map<String,String> parameters1 = new HashMap<>();
parameters1.put("key1", "value1");
parameters1.put("key2", "value2");
EncryptionOptions encryptionOptions1 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
.withProtocol("TLSv1.1")
.withRequireClientAuth(false)
.withRequireEndpointVerification(true);
Map<String,String> parameters2 = new HashMap<>();
parameters2.put("key1", "value1");
parameters2.put("key2", "value2");
EncryptionOptions encryptionOptions2 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DefaultSslContextFactory.class.getName(), parameters2))
.withProtocol("TLSv1.1")
.withRequireClientAuth(false)
.withRequireEndpointVerification(true);
assertNotEquals(encryptionOptions1, encryptionOptions2);
assertNotEquals(encryptionOptions1.hashCode(), encryptionOptions2.hashCode());
}
@Test
public void testDifferentCustomSslContextFactoryParameters() {
Map<String,String> parameters1 = new HashMap<>();
parameters1.put("key1", "value11");
parameters1.put("key2", "value12");
EncryptionOptions encryptionOptions1 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
.withProtocol("TLSv1.1");
Map<String,String> parameters2 = new HashMap<>();
parameters2.put("key1", "value21");
parameters2.put("key2", "value22");
EncryptionOptions encryptionOptions2 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
.withProtocol("TLSv1.1");
assertNotEquals(encryptionOptions1, encryptionOptions2);
assertNotEquals(encryptionOptions1.hashCode(), encryptionOptions2.hashCode());
}
}

View File

@ -20,6 +20,7 @@ package org.apache.cassandra.config;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.google.common.collect.ImmutableMap;
@ -29,14 +30,15 @@ import org.junit.Test;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.assertj.core.api.Assertions;
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED;
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.OPTIONAL;
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.ENCRYPTED;
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.all;
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.dc;
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.none;
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.rack;
import static org.junit.Assert.*;
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.ENCRYPTED;
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.OPTIONAL;
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class EncryptionOptionsTest
{
@ -55,7 +57,24 @@ public class EncryptionOptionsTest
public static EncryptionOptionsTestCase of(Boolean optional, String keystorePath, Boolean enabled, EncryptionOptions.TlsEncryptionPolicy expected)
{
return new EncryptionOptionsTestCase(new EncryptionOptions(keystorePath, "dummypass", "dummytruststore", "dummypass",
return new EncryptionOptionsTestCase(new EncryptionOptions(new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
new HashMap<>()),
keystorePath, "dummypass",
"dummytruststore", "dummypass",
Collections.emptyList(), null, null, null, "JKS", false, false, enabled, optional)
.applyConfig(),
expected,
String.format("optional=%s keystore=%s enabled=%s", optional, keystorePath, enabled));
}
public static EncryptionOptionsTestCase of(Boolean optional, String keystorePath, Boolean enabled,
Map<String,String> customSslContextFactoryParams,
EncryptionOptions.TlsEncryptionPolicy expected)
{
return new EncryptionOptionsTestCase(new EncryptionOptions(new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
customSslContextFactoryParams),
keystorePath, "dummypass",
"dummytruststore", "dummypass",
Collections.emptyList(), null, null, null, "JKS", false, false, enabled, optional)
.applyConfig(),
expected,
@ -105,7 +124,8 @@ public class EncryptionOptionsTest
EncryptionOptions.ServerEncryptionOptions.InternodeEncryption internodeEncryption,
EncryptionOptions.TlsEncryptionPolicy expected)
{
return new ServerEncryptionOptionsTestCase(new EncryptionOptions.ServerEncryptionOptions(keystorePath, "dummypass", "dummytruststore", "dummypass",
return new ServerEncryptionOptionsTestCase(new EncryptionOptions.ServerEncryptionOptions(new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
new HashMap<>()), keystorePath, "dummypass", "dummytruststore", "dummypass",
Collections.emptyList(), null, null, null, "JKS", false, false, optional, internodeEncryption, false)
.applyConfig(),
expected,
@ -175,4 +195,17 @@ public class EncryptionOptionsTest
Assert.assertSame(testCase.description, testCase.expected, testCase.encryptionOptions.tlsEncryptionPolicy());
}
}
@Test(expected = IllegalArgumentException.class)
public void testMisplacedConfigKey()
{
Map<String, String> customSslContextFactoryParams = new HashMap<>();
for(EncryptionOptions.ConfigKey configKey: EncryptionOptions.ConfigKey.values())
{
customSslContextFactoryParams.put(configKey.getKeyName(), "my-custom-value");
}
EncryptionOptionsTestCase.of(null, absentKeystore, true, customSslContextFactoryParams, ENCRYPTED);
}
}

View File

@ -0,0 +1,75 @@
/*
* 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.security;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.exceptions.ConfigurationException;
public class CustomSslContextFactoryConfigTest
{
@BeforeClass
public static void setupDatabaseDescriptor()
{
System.setProperty("cassandra.config", "cassandra-sslcontextfactory.yaml");
}
@AfterClass
public static void tearDownDatabaseDescriptor() {
System.clearProperty("cassandra.config");
}
@Test
public void testValidCustomSslContextFactoryConfiguration() {
Config config = DatabaseDescriptor.loadConfig();
config.client_encryption_options.applyConfig();
Assert.assertEquals("org.apache.cassandra.security.DummySslContextFactoryImpl",
config.client_encryption_options.ssl_context_factory.class_name);
Assert.assertEquals(config.client_encryption_options.ssl_context_factory.class_name,
config.client_encryption_options.sslContextFactoryInstance.getClass().getName());
Assert.assertEquals(3, config.client_encryption_options.ssl_context_factory.parameters.size());
Assert.assertEquals("value1", config.client_encryption_options.ssl_context_factory.parameters.get("key1"));
Assert.assertEquals("value2", config.client_encryption_options.ssl_context_factory.parameters.get("key2"));
Assert.assertEquals("value3", config.client_encryption_options.ssl_context_factory.parameters.get("key3"));
DummySslContextFactoryImpl dummySslContextFactory =
(DummySslContextFactoryImpl)config.client_encryption_options.sslContextFactoryInstance;
Assert.assertEquals("dummy-keystore",dummySslContextFactory.getStringValueFor("keystore"));
}
@Test
public void testInvalidCustomSslContextFactoryConfiguration() {
Config config = DatabaseDescriptor.loadConfig();
try {
config.server_encryption_options.applyConfig();
} catch(ConfigurationException ce) {
Assert.assertEquals("Unable to create instance of ISslContextFactory for org.apache.cassandra.security" +
".MissingSslContextFactoryImpl", ce.getMessage());
Assert.assertNotNull("Unable to find root cause of pluggable ISslContextFactory loading failure",
ce.getCause());
Assert.assertTrue(ce.getCause() instanceof ClassNotFoundException);
}
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.security;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.cassandra.config.Config;
import org.apache.cassandra.config.DatabaseDescriptor;
public class CustomSslContextFactoryInvalidConfigTest
{
@BeforeClass
public static void setupDatabaseDescriptor()
{
System.setProperty("cassandra.config", "cassandra-sslcontextfactory-invalidconfiguration.yaml");
}
@AfterClass
public static void tearDownDatabaseDescriptor() {
System.clearProperty("cassandra.config");
}
@Test(expected = IllegalArgumentException.class)
public void testValidCustomSslContextFactoryConfiguration() {
Config config = DatabaseDescriptor.loadConfig();
config.client_encryption_options.applyConfig();
}
}

View File

@ -0,0 +1,169 @@
/*
* 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.security;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.TrustManagerFactory;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.OpenSslContext;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslProvider;
import org.apache.cassandra.config.EncryptionOptions;
public class DefaultSslContextFactoryTest
{
private Map<String,Object> commonConfig = new HashMap<>();
@Before
public void setup()
{
commonConfig.put("truststore", "test/conf/cassandra_ssl_test.truststore");
commonConfig.put("truststore_password", "cassandra");
commonConfig.put("require_client_auth", Boolean.FALSE);
commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA"));
}
private void addKeystoreOptions(Map<String,Object> config)
{
config.put("keystore", "test/conf/cassandra_ssl_test.keystore");
config.put("keystore_password", "cassandra");
}
@Test
public void getSslContextOpenSSL() throws IOException
{
EncryptionOptions options = new EncryptionOptions().withTrustStore("test/conf/cassandra_ssl_test.truststore")
.withTrustStorePassword("cassandra")
.withKeyStore("test/conf/cassandra_ssl_test.keystore")
.withKeyStorePassword("cassandra")
.withRequireClientAuth(false)
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA");
SslContext sslContext = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
Assert.assertNotNull(sslContext);
if (OpenSsl.isAvailable())
Assert.assertTrue(sslContext instanceof OpenSslContext);
else
Assert.assertTrue(sslContext instanceof SslContext);
}
@Test(expected = IOException.class)
public void buildTrustManagerFactoryWithInvalidTruststoreFile() throws IOException
{
Map<String,Object> config = new HashMap<>();
config.putAll(commonConfig);
config.put("truststore", "/this/is/probably/not/a/file/on/your/test/machine");
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
defaultSslContextFactoryImpl.checkedExpiry = false;
defaultSslContextFactoryImpl.buildTrustManagerFactory();
}
@Test(expected = IOException.class)
public void buildTrustManagerFactoryWithBadPassword() throws IOException
{
Map<String,Object> config = new HashMap<>();
config.putAll(commonConfig);
config.put("truststore_password", "HomeOfBadPasswords");
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
defaultSslContextFactoryImpl.checkedExpiry = false;
defaultSslContextFactoryImpl.buildTrustManagerFactory();
}
@Test
public void buildTrustManagerFactoryHappyPath() throws IOException
{
Map<String,Object> config = new HashMap<>();
config.putAll(commonConfig);
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
defaultSslContextFactoryImpl.checkedExpiry = false;
TrustManagerFactory trustManagerFactory = defaultSslContextFactoryImpl.buildTrustManagerFactory();
Assert.assertNotNull(trustManagerFactory);
}
@Test(expected = IOException.class)
public void buildKeyManagerFactoryWithInvalidKeystoreFile() throws IOException
{
Map<String,Object> config = new HashMap<>();
config.putAll(commonConfig);
config.put("keystore", "/this/is/probably/not/a/file/on/your/test/machine");
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
defaultSslContextFactoryImpl.checkedExpiry = false;
defaultSslContextFactoryImpl.buildKeyManagerFactory();
}
@Test(expected = IOException.class)
public void buildKeyManagerFactoryWithBadPassword() throws IOException
{
Map<String,Object> config = new HashMap<>();
config.putAll(commonConfig);
addKeystoreOptions(config);
config.put("keystore_password", "HomeOfBadPasswords");
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
defaultSslContextFactoryImpl.buildKeyManagerFactory();
}
@Test
public void buildKeyManagerFactoryHappyPath() throws IOException
{
Map<String,Object> config = new HashMap<>();
config.putAll(commonConfig);
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
// Make sure the exiry check didn't happen so far for the private key
Assert.assertFalse(defaultSslContextFactoryImpl.checkedExpiry);
addKeystoreOptions(config);
DefaultSslContextFactory defaultSslContextFactoryImpl2 = new DefaultSslContextFactory(config);
// Trigger the private key loading. That will also check for expired private key
defaultSslContextFactoryImpl2.buildKeyManagerFactory();
// Now we should have checked the private key's expiry
Assert.assertTrue(defaultSslContextFactoryImpl2.checkedExpiry);
// Make sure that new factory object preforms the fresh private key expiry check
DefaultSslContextFactory defaultSslContextFactoryImpl3 = new DefaultSslContextFactory(config);
Assert.assertFalse(defaultSslContextFactoryImpl3.checkedExpiry);
defaultSslContextFactoryImpl3.buildKeyManagerFactory();
Assert.assertTrue(defaultSslContextFactoryImpl3.checkedExpiry);
}
@Test
public void testDisableOpenSslForInJvmDtests() {
// The configuration name below is hard-coded intentionally to make sure we don't break the contract without
// changing the documentation appropriately
System.setProperty("cassandra.disable_tcactive_openssl","true");
Map<String,Object> config = new HashMap<>();
config.putAll(commonConfig);
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
Assert.assertEquals(SslProvider.JDK, defaultSslContextFactoryImpl.getSslProvider());
System.clearProperty("cassandra.disable_tcactive_openssl");
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.security;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslContext;
/**
* TEST ONLY Class. DON'T use it for anything else.
*/
public class DummySslContextFactoryImpl implements ISslContextFactory
{
private Map<String,Object> parameters;
public DummySslContextFactoryImpl(Map<String,Object> parameters) {
this.parameters=parameters;
}
@Override
public SSLContext createJSSESslContext(boolean verifyPeerCertificate) throws SSLException
{
return null;
}
@Override
public SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType,
CipherSuiteFilter cipherFilter) throws SSLException
{
return null;
}
@Override
public void initHotReloading() throws SSLException
{
}
@Override
public boolean shouldReload()
{
return false;
}
@Override
public List<String> getAcceptedProtocols()
{
return null;
}
@Override
public List<String> getCipherSuites()
{
return null;
}
/*
* For testing only
*/
public String getStringValueFor(String configKey) {
return parameters.containsKey(configKey) ? parameters.get(configKey).toString() : null;
}
}

View File

@ -21,7 +21,8 @@ package org.apache.cassandra.security;
import java.io.File;
import java.io.IOException;
import java.security.cert.CertificateException;
import javax.net.ssl.TrustManagerFactory;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.junit.Assert;
@ -30,16 +31,12 @@ import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.handler.ssl.JdkSslContext;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.OpenSslContext;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.EncryptionOptions;
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
import static org.junit.Assert.assertArrayEquals;
import org.apache.cassandra.config.ParameterizedClass;
public class SSLFactoryTest
{
@ -69,34 +66,6 @@ public class SSLFactoryTest
.withTrustStorePassword("cassandra")
.withRequireClientAuth(false)
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA");
SSLFactory.checkedExpiry = false;
}
@Test
public void getSslContext_OpenSSL() throws IOException
{
// only try this test if OpenSsl is available
if (!OpenSsl.isAvailable())
{
logger.warn("OpenSSL not available in this application, so not testing the netty-openssl code paths");
return;
}
EncryptionOptions options = addKeystoreOptions(encryptionOptions);
SslContext sslContext = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, true);
Assert.assertNotNull(sslContext);
Assert.assertTrue(sslContext instanceof OpenSslContext);
}
@Test
public void getSslContext_JdkSsl() throws IOException
{
EncryptionOptions options = addKeystoreOptions(encryptionOptions);
SslContext sslContext = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, false);
Assert.assertNotNull(sslContext);
Assert.assertTrue(sslContext instanceof JdkSslContext);
Assert.assertEquals(encryptionOptions.cipher_suites, sslContext.cipherSuites());
}
private ServerEncryptionOptions addKeystoreOptions(ServerEncryptionOptions options)
@ -105,50 +74,6 @@ public class SSLFactoryTest
.withKeyStorePassword("cassandra");
}
@Test(expected = IOException.class)
public void buildTrustManagerFactory_NoFile() throws IOException
{
SSLFactory.buildTrustManagerFactory(encryptionOptions.withTrustStore("/this/is/probably/not/a/file/on/your/test/machine"));
}
@Test(expected = IOException.class)
public void buildTrustManagerFactory_BadPassword() throws IOException
{
SSLFactory.buildTrustManagerFactory(encryptionOptions.withTrustStorePassword("HomeOfBadPasswords"));
}
@Test
public void buildTrustManagerFactory_HappyPath() throws IOException
{
TrustManagerFactory trustManagerFactory = SSLFactory.buildTrustManagerFactory(encryptionOptions);
Assert.assertNotNull(trustManagerFactory);
}
@Test(expected = IOException.class)
public void buildKeyManagerFactory_NoFile() throws IOException
{
EncryptionOptions options = addKeystoreOptions(encryptionOptions)
.withKeyStore("/this/is/probably/not/a/file/on/your/test/machine");
SSLFactory.buildKeyManagerFactory(options);
}
@Test(expected = IOException.class)
public void buildKeyManagerFactory_BadPassword() throws IOException
{
EncryptionOptions options = addKeystoreOptions(encryptionOptions)
.withKeyStorePassword("HomeOfBadPasswords");
SSLFactory.buildKeyManagerFactory(options);
}
@Test
public void buildKeyManagerFactory_HappyPath() throws IOException
{
Assert.assertFalse(SSLFactory.checkedExpiry);
EncryptionOptions options = addKeystoreOptions(encryptionOptions);
SSLFactory.buildKeyManagerFactory(options);
Assert.assertTrue(SSLFactory.checkedExpiry);
}
@Test
public void testSslContextReload_HappyPath() throws IOException, InterruptedException
{
@ -159,8 +84,7 @@ public class SSLFactoryTest
SSLFactory.initHotReloading(options, options, true);
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
.isAvailable());
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
File keystoreFile = new File(options.keystore);
SSLFactory.checkCertFilesForHotReloading(options, options);
@ -168,8 +92,7 @@ public class SSLFactoryTest
keystoreFile.setLastModified(System.currentTimeMillis() + 15000);
SSLFactory.checkCertFilesForHotReloading(options, options);
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
.isAvailable());
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
Assert.assertNotSame(oldCtx, newCtx);
}
@ -201,8 +124,7 @@ public class SSLFactoryTest
ServerEncryptionOptions options = addKeystoreOptions(encryptionOptions);
SSLFactory.initHotReloading(options, options, true);
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
.isAvailable());
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
File keystoreFile = new File(options.keystore);
SSLFactory.checkCertFilesForHotReloading(options, options);
@ -211,8 +133,7 @@ public class SSLFactoryTest
ServerEncryptionOptions modOptions = new ServerEncryptionOptions(options)
.withKeyStorePassword("bad password");
SSLFactory.checkCertFilesForHotReloading(modOptions, modOptions);
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
.isAvailable());
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
Assert.assertSame(oldCtx, newCtx);
}
@ -235,16 +156,14 @@ public class SSLFactoryTest
SSLFactory.initHotReloading(options, options, true);
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
.isAvailable());
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
SSLFactory.checkCertFilesForHotReloading(options, options);
testKeystoreFile.setLastModified(System.currentTimeMillis() + 15000);
FileUtils.forceDelete(testKeystoreFile);
SSLFactory.checkCertFilesForHotReloading(options, options);
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
.isAvailable());
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
Assert.assertSame(oldCtx, newCtx);
}
@ -267,7 +186,7 @@ public class SSLFactoryTest
.withCipherSuites("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
SslContext ctx1 = SSLFactory.getOrCreateSslContext(options, true,
SSLFactory.SocketType.SERVER, OpenSsl.isAvailable());
ISslContextFactory.SocketType.SERVER);
Assert.assertTrue(ctx1.isServer());
Assert.assertEquals(ctx1.cipherSuites(), options.cipher_suites);
@ -275,9 +194,69 @@ public class SSLFactoryTest
options = options.withCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
SslContext ctx2 = SSLFactory.getOrCreateSslContext(options, true,
SSLFactory.SocketType.CLIENT, OpenSsl.isAvailable());
ISslContextFactory.SocketType.CLIENT);
Assert.assertTrue(ctx2.isClient());
Assert.assertEquals(ctx2.cipherSuites(), options.cipher_suites);
}
@Test
public void testCacheKeyEqualityForCustomSslContextFactory() {
Map<String,String> parameters1 = new HashMap<>();
parameters1.put("key1", "value1");
parameters1.put("key2", "value2");
EncryptionOptions encryptionOptions1 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
.withProtocol("TLSv1.1")
.withRequireClientAuth(true)
.withRequireEndpointVerification(false);
SSLFactory.CacheKey cacheKey1 = new SSLFactory.CacheKey(encryptionOptions1, ISslContextFactory.SocketType.SERVER
);
Map<String,String> parameters2 = new HashMap<>();
parameters2.put("key1", "value1");
parameters2.put("key2", "value2");
EncryptionOptions encryptionOptions2 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
.withProtocol("TLSv1.1")
.withRequireClientAuth(true)
.withRequireEndpointVerification(false);
SSLFactory.CacheKey cacheKey2 = new SSLFactory.CacheKey(encryptionOptions2, ISslContextFactory.SocketType.SERVER
);
Assert.assertEquals(cacheKey1, cacheKey2);
}
@Test
public void testCacheKeyInequalityForCustomSslContextFactory() {
Map<String,String> parameters1 = new HashMap<>();
parameters1.put("key1", "value11");
parameters1.put("key2", "value12");
EncryptionOptions encryptionOptions1 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
.withProtocol("TLSv1.1");
SSLFactory.CacheKey cacheKey1 = new SSLFactory.CacheKey(encryptionOptions1, ISslContextFactory.SocketType.SERVER
);
Map<String,String> parameters2 = new HashMap<>();
parameters2.put("key1", "value21");
parameters2.put("key2", "value22");
EncryptionOptions encryptionOptions2 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
.withProtocol("TLSv1.1");
SSLFactory.CacheKey cacheKey2 = new SSLFactory.CacheKey(encryptionOptions2, ISslContextFactory.SocketType.SERVER
);
Assert.assertNotEquals(cacheKey1, cacheKey2);
}
}