From 24dcc280c2e442eea27e7129c4c948eb6199ed91 Mon Sep 17 00:00:00 2001 From: Maulin Vasavada Date: Fri, 21 May 2021 00:43:50 -0700 Subject: [PATCH] CEP-9 make SSLContext creation pluggable patch by Maulin Vasavada; reviewed by Jon Meredith, Stefan Miklosovic and Berenguer Blasi for CASSANDRA-16666 --- CHANGES.txt | 1 + doc/source/operating/security.rst | 27 ++ examples/ssl-factory/README.txt | 19 + examples/ssl-factory/build.xml | 100 ++++ .../KubernetesSecretsSslContextFactory.java | 287 +++++++++++ .../test/conf/cassandra_ssl_test.keystore | Bin 0 -> 2281 bytes .../test/conf/cassandra_ssl_test.truststore | Bin 0 -> 992 bytes ...andra_ssl_test.truststore-without-password | Bin 0 -> 992 bytes ...ubernetesSecretsSslContextFactoryTest.java | 296 ++++++++++++ .../cassandra/config/EncryptionOptions.java | 451 ++++++++++++------ .../cassandra/config/ParameterizedClass.java | 6 + .../net/InboundConnectionInitiator.java | 6 +- .../net/OutboundConnectionInitiator.java | 4 +- .../security/AbstractSslContextFactory.java | 266 +++++++++++ .../security/DefaultSslContextFactory.java | 33 ++ .../security/FileBasedSslContextFactory.java | 211 ++++++++ .../security/ISslContextFactory.java | 124 +++++ .../apache/cassandra/security/SSLFactory.java | 281 +++-------- .../transport/PipelineConfigurator.java | 5 +- .../cassandra/transport/SimpleClient.java | 3 +- .../apache/cassandra/utils/FBUtilities.java | 21 +- ...slcontextfactory-invalidconfiguration.yaml | 82 ++++ test/conf/cassandra-sslcontextfactory.yaml | 85 ++++ .../test/AbstractEncryptionOptionsImpl.java | 5 +- .../config/DatabaseDescriptorRefTest.java | 1 + .../config/DatabaseDescriptorTest.java | 15 +- .../config/EncryptionOptionsEqualityTest.java | 142 ++++++ .../config/EncryptionOptionsTest.java | 45 +- .../CustomSslContextFactoryConfigTest.java | 75 +++ ...tomSslContextFactoryInvalidConfigTest.java | 46 ++ .../DefaultSslContextFactoryTest.java | 169 +++++++ .../security/DummySslContextFactoryImpl.java | 82 ++++ .../cassandra/security/SSLFactoryTest.java | 163 +++---- 33 files changed, 2577 insertions(+), 474 deletions(-) create mode 100644 examples/ssl-factory/README.txt create mode 100644 examples/ssl-factory/build.xml create mode 100644 examples/ssl-factory/src/org/apache/cassandra/security/KubernetesSecretsSslContextFactory.java create mode 100644 examples/ssl-factory/test/conf/cassandra_ssl_test.keystore create mode 100644 examples/ssl-factory/test/conf/cassandra_ssl_test.truststore create mode 100644 examples/ssl-factory/test/conf/cassandra_ssl_test.truststore-without-password create mode 100644 examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsSslContextFactoryTest.java create mode 100644 src/java/org/apache/cassandra/security/AbstractSslContextFactory.java create mode 100644 src/java/org/apache/cassandra/security/DefaultSslContextFactory.java create mode 100644 src/java/org/apache/cassandra/security/FileBasedSslContextFactory.java create mode 100644 src/java/org/apache/cassandra/security/ISslContextFactory.java create mode 100644 test/conf/cassandra-sslcontextfactory-invalidconfiguration.yaml create mode 100644 test/conf/cassandra-sslcontextfactory.yaml create mode 100644 test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java create mode 100644 test/unit/org/apache/cassandra/security/CustomSslContextFactoryConfigTest.java create mode 100644 test/unit/org/apache/cassandra/security/CustomSslContextFactoryInvalidConfigTest.java create mode 100644 test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java create mode 100644 test/unit/org/apache/cassandra/security/DummySslContextFactoryImpl.java diff --git a/CHANGES.txt b/CHANGES.txt index 4f37cbe8bf..94ea95a6e0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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) diff --git a/doc/source/operating/security.rst b/doc/source/operating/security.rst index e97baefa2f..d50236a5cd 100644 --- a/doc/source/operating/security.rst +++ b/doc/source/operating/security.rst @@ -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 diff --git a/examples/ssl-factory/README.txt b/examples/ssl-factory/README.txt new file mode 100644 index 0000000000..d0d2b30f24 --- /dev/null +++ b/examples/ssl-factory/README.txt @@ -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 +run "ant build" + +Step 2: Run tests for the security examples + +change directory to /examples/ssl-factory +run "ant test" diff --git a/examples/ssl-factory/build.xml b/examples/ssl-factory/build.xml new file mode 100644 index 0000000000..9f150b03d6 --- /dev/null +++ b/examples/ssl-factory/build.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/ssl-factory/src/org/apache/cassandra/security/KubernetesSecretsSslContextFactory.java b/examples/ssl-factory/src/org/apache/cassandra/security/KubernetesSecretsSslContextFactory.java new file mode 100644 index 0000000000..699efbc29d --- /dev/null +++ b/examples/ssl-factory/src/org/apache/cassandra/security/KubernetesSecretsSslContextFactory.java @@ -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 here. + * + * 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: + *
+ *     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
+ * 
+ * + * 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}. + *
+ *     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
+ * 
+ * + * Below is the corresponding sample YAML configuration for K8 env. + *
+ * 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
+ * 
+ */ +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 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 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 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 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; + } + } +} diff --git a/examples/ssl-factory/test/conf/cassandra_ssl_test.keystore b/examples/ssl-factory/test/conf/cassandra_ssl_test.keystore new file mode 100644 index 0000000000000000000000000000000000000000..8b2b218efab60b26583bc620b281941f10dc0b5c GIT binary patch literal 2281 zcmc(g`8N~{7sqE~3S-|HOEE+-V_y4`X|l$U6&iucz~N-sha>4|spLKitnf=bn4M_nz+3 z2O{m(merQuN|(%qmt$%7epq#|Ah~ zus$ocl=|Mt6S1UF*`_`IbJOTLDqzap$NLEM#Toj=k7pGS_IUjpZ@pU`VQoy#YlnZaH8k`Gd`N4gA$1C?!u4ptFZk8KW=1NHXf{xVCMF%k~pgvstJ z&Mp1EE|AK?&diz-*8W+LBf+wubP-Fg@~q`$&1WRdeq}N%MJ;cL1by$&g8e|-oWe()-&aty!>5fPA&lVs0q4D&r6 zEx{aZ*L!7O;5ksYfBH*;>HBccQweiS$Ne^@54uG{PgjfEMXGgSV<2L*)yhcpNe~}I z^ynvsTpC-AG*;ehAA9$VL!-^X3C8{Cx@BBZfvdkuj&GJ{j6ZkP1uaq6uNDkIMkf9_ zZ3|^L&^zkE;W~}@sTWuQP|KHXv?tikFLtV#YcZm#hS;LFyt>G*n>I3YKkz(bnYbgK z!JH*xdhYZCrykyKhGniNr6tKhJ1yacv-*`nl58GJc2^?ZYN_>(wEA>?ZV}7c;4P{2 z4B~qT>q zI>cGh#t@gK*T+QJwJQDlX*6R#J>Z;Q#n1kUq73btxZDAdLbN^v)G6y(phs|yTuj;; z{KQWWV#zgQbpJsbO#k?~djxVq&`Vh?GbW`tZ9_seYY1IBuef9_BJV znHN$Kaa(qHpM=p#y=G(;)2&oKke;LL%ez$9jhUm(9Q5%MFKI0(e(1Hm2nboyQJhod zZ(XE450$EeXixjOw`){&_=-#FHh@#$)f|P#pz@(o^OVZLN(Y}b(?M7=L3Di~13go9 z=jYuz>QH>WOEch)MH47G?#n_eS{_OU@#&fW*sdwU=QI>1Xf7H(3Xt+y8i0I5MD`8+ zp}%vF0$^>*U9M0r*L5-^^;xYv!}Qt&v6lO}M^D5i>*BpSK=JM+($d=x(GADB=GWTz zJ7Y=JbE!7{W!Mj(wu{t2f6(JO@w4~^rwQGg4co3vNX^XSD;zVsE#Y{eMR9>KR`2uu z^7EKAx-`*b^h^5DdMqTk91F7x7hn_$%5NCU4hBAnV~Iu`<118~ZR+-ZrBN(+N#qnR z_Ci`Al^rPw)0-tvmNfL*h+Vh7VdO8yKN+;4R<{LEYIi2{n#GkmoO4b*-<`5^pVsut zBsrYaG?BUh05}s#1>ZqZK@s_25D)}{*a-YoK*BhWs?f}7-(f%?HwOS2f#N@OOt2$_ zQwZuvCXoZe$iK;vzY*x)2-p9JFjDZ(KHNOqzpdGq?2VE@iv3B!Lj0IOf|n0b4*T1| zkuqX1lok?=($PR^=pZ#N{z|n_TK`S|pQBNM$NnnR;Tr){fqVcY703mm0)c>3i%j>m z;n^|4)rjpPa)K22NT5krSRFwYlR}YIr7O6OnK^8_(HkK}0l;`v$33hT>>BzS$xt+) z%^a&ZWT;sb&6#DRX!_i=6Bp^Rv@74YetOGb{Yl={*8#W&gl3q;gDL!GNghrgHt+kQ zxLS{QLc1!m9975E-4>Fe#g`aKJ0LI+04$P6$|B*1 z+lBK&L?Oag$8Tyss=k*Nk`4`A9D`j()BLX zdeMHR1)0cG-T6+~ zS!EyHx%V!s$vm~p;zia$6ejv>N@5D_sD0-3|j!wqv$W?s6Xq=7g{lv!B7u^=%yBUQl}=5PZ!ab80+ z17kxABSRw#1EVM~*UZoi${k3jH8Cn72NWYK19KB2KZ8LNBNtN>BO^nf=hXO*mmXgi z`cV06abAZsGe?1IdHJ?PdB@&jdF{C>vDe*0zQoPl#j-e`vDNVC6lXKuD&wkbTe18@ z9?q&4ZA}-~u}$+=bzARt%&#Wl)gQ&PpX|Tcd|ExfaDTqvPBqhV$r<;5n50fIeu*&D zkX7Gvar@Em{Wl-<)^6FsyX^Aa-hx`LnXfbavZn=I+9UjHj`>z@^D}`FSJfX$@0yXf zEL+neE~3qCk!k4cm8;}?8g8|ptDao$b|7hsj>sQd*=1#IU(;2;pL`bgyoY^B%H{6g zOw5c7jEfZwKSlF`Qq-@>hgY_r0xwmHq9@}u0{mJeRC*L{0H(8V={T zbXf@Al6gAi(BsQhS37o0=(6AZV`l2wnVO6B^&3U&aw;yWH`QJ|Gq?NG>&dG8e?Gj{ iYRGywzgSfK&JK;;RtxssiVdn*{;X0_$}!}W+5`ZGs(6b4 literal 0 HcmV?d00001 diff --git a/examples/ssl-factory/test/conf/cassandra_ssl_test.truststore-without-password b/examples/ssl-factory/test/conf/cassandra_ssl_test.truststore-without-password new file mode 100644 index 0000000000000000000000000000000000000000..0031b15b8ebb18660a4e48d237c0b37d759889bc GIT binary patch literal 992 zcmezO_TO6u1_mY|W(3nh$%)0qiFqkSiSfn7Iq@Z_#U((g;O<3MDh#Xejv>N@5D_sD0-3|j!wqv$W?s6Xq=7g{lv!B7u^=%yBUQl}=5PZ!ab80+ z17kxABSRw#1EVM~*UZoi${k3jH8Cn72NWYK19KB2KZ8LNBNtN>BO^nf=hXO*mmXgi z`cV06abAZsGe?1IdHJ?PdB@&jdF{C>vDe*0zQoPl#j-e`vDNVC6lXKuD&wkbTe18@ z9?q&4ZA}-~u}$+=bzARt%&#Wl)gQ&PpX|Tcd|ExfaDTqvPBqhV$r<;5n50fIeu*&D zkX7Gvar@Em{Wl-<)^6FsyX^Aa-hx`LnXfbavZn=I+9UjHj`>z@^D}`FSJfX$@0yXf zEL+neE~3qCk!k4cm8;}?8g8|ptDao$b|7hsj>sQd*=1#IU(;2;pL`bgyoY^B%H{6g zOw5c7jEfZwKSlF`Qq-@>hgY_r0xwmHq9@}u0{mJeRC*L{0H(8V={T zbXf@Al6gAi(BsQhS37o0=(6AZV`l2wnVO6B^&3U&aw;yWH`QJ|Gq?NG>&dG8e?Gj{ iYRGywzgSfKjwi!a@wB4>|5G}|!!jqm{Tj8arV9Xiad`j$ literal 0 HcmV?d00001 diff --git a/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsSslContextFactoryTest.java b/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsSslContextFactoryTest.java new file mode 100644 index 0000000000..fec48fd60a --- /dev/null +++ b/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsSslContextFactoryTest.java @@ -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 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 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 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 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 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 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 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 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 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 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 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 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 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); + } + } +} diff --git a/src/java/org/apache/cassandra/config/EncryptionOptions.java b/src/java/org/apache/cassandra/config/EncryptionOptions.java index 93668d9d04..2ac9d56f75 100644 --- a/src/java/org/apache/cassandra/config/EncryptionOptions.java +++ b/src/java/org/apache/cassandra/config/EncryptionOptions.java @@ -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 asSet() + { + Set 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 cipher_suites, String protocol, List 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 cipher_suites, + String protocol, List 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 sslContextFactoryParameters) + { + if (ssl_context_factory.parameters != null) + { + Set configKeys = ConfigKey.asSet(); + for (Map.Entry 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 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 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 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 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.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 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 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 cipher_suites, String protocol, - List 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 cipher_suites, String protocol, List 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 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 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(); } } diff --git a/src/java/org/apache/cassandra/config/ParameterizedClass.java b/src/java/org/apache/cassandra/config/ParameterizedClass.java index d0542f584a..4b8cf5a4a9 100644 --- a/src/java/org/apache/cassandra/config/ParameterizedClass.java +++ b/src/java/org/apache/cassandra/config/ParameterizedClass.java @@ -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() { diff --git a/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java b/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java index d74f802a55..3663029e1e 100644 --- a/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java +++ b/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java @@ -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()); diff --git a/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java b/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java index f1fa6b73cd..15f83c23cb 100644 --- a/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java +++ b/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java @@ -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 + * {@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 TLS_PROTOCOL_SUBSTITUTION = SSLFactory.tlsInstanceProtocolSubstitution(); + + protected boolean openSslIsAvailable; + + protected final Map parameters; + protected final List cipher_suites; + protected final String protocol; + protected final List 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 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 getStringList(String key) + { + return (List) 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 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.builder().addAll(accepted_protocols).add(protocol).build(); + } + else + { + return accepted_protocols; + } + } + + @Override + public List 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; +} diff --git a/src/java/org/apache/cassandra/security/DefaultSslContextFactory.java b/src/java/org/apache/cassandra/security/DefaultSslContextFactory.java new file mode 100644 index 0000000000..92c88c56ac --- /dev/null +++ b/src/java/org/apache/cassandra/security/DefaultSslContextFactory.java @@ -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 parameters) + { + super(parameters); + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/security/FileBasedSslContextFactory.java b/src/java/org/apache/cassandra/security/FileBasedSslContextFactory.java new file mode 100644 index 0000000000..618af51c1d --- /dev/null +++ b/src/java/org/apache/cassandra/security/FileBasedSslContextFactory.java @@ -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}). + *

+ * {@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 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 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 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 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 + + '}'; + } + } +} diff --git a/src/java/org/apache/cassandra/security/ISslContextFactory.java b/src/java/org/apache/cassandra/security/ISslContextFactory.java new file mode 100644 index 0000000000..579c95e43a --- /dev/null +++ b/src/java/org/apache/cassandra/security/ISslContextFactory.java @@ -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. + *

+ * Implementation of this interface must have a constructor with argument of type {@code Map} 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. + *

+ * Since on top of Netty, Cassandra is internally using JSSE SSLContext also for certain use-cases- this interface + * has methods for both. + *

+ * Below is an example of how to configure a custom implementation with parameters + *

+ * ssl_context_factory:
+ *       class_name: org.apache.cassandra.security.YourSslContextFactoryImpl
+ *       parameters:
+ *         key1: "value1"
+ *         key2: "value2"
+ *         key3: "value3"
+ * 
+ */ +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 getAcceptedProtocols(); + + /** + * Returns the list of cipher suites supported by the implementation. + * + * @return List of supported cipher suites + */ + List getCipherSuites(); + + /** + * Indicates if the process holds the inbound/listening (Server) end of the socket or the outbound side (Client). + */ + enum SocketType + { + SERVER, CLIENT; + } +} \ No newline at end of file diff --git a/src/java/org/apache/cassandra/security/SSLFactory.java b/src/java/org/apache/cassandra/security/SSLFactory.java index 22f0a9da72..e06da1f0cb 100644 --- a/src/java/org/apache/cassandra/security/SSLFactory.java +++ b/src/java/org/apache/cassandra/security/SSLFactory.java @@ -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 cachedSslContexts = new ConcurrentHashMap<>(); - /** - * List of files that trigger hot reloading of SSL certificates - */ - private static volatile List 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 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 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; } } diff --git a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java index 0250f1319e..81ff13605e 100644 --- a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java +++ b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java @@ -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: diff --git a/src/java/org/apache/cassandra/transport/SimpleClient.java b/src/java/org/apache/cassandra/transport/SimpleClient.java index 7e8e3e2029..ef075ab3dd 100644 --- a/src/java/org/apache/cassandra/transport/SimpleClient.java +++ b/src/java/org/apache/cassandra/transport/SimpleClient.java @@ -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())); } } diff --git a/src/java/org/apache/cassandra/utils/FBUtilities.java b/src/java/org/apache/cassandra/utils/FBUtilities.java index bb417a5df9..73aa8f026e 100644 --- a/src/java/org/apache/cassandra/utils/FBUtilities.java +++ b/src/java/org/apache/cassandra/utils/FBUtilities.java @@ -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 parameters) throws ConfigurationException { if (!className.contains(".")) @@ -689,6 +690,22 @@ public class FBUtilities } } + public static ISslContextFactory newSslContextFactory(String className, Map 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. diff --git a/test/conf/cassandra-sslcontextfactory-invalidconfiguration.yaml b/test/conf/cassandra-sslcontextfactory-invalidconfiguration.yaml new file mode 100644 index 0000000000..c05bf75e01 --- /dev/null +++ b/test/conf/cassandra-sslcontextfactory-invalidconfiguration.yaml @@ -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 diff --git a/test/conf/cassandra-sslcontextfactory.yaml b/test/conf/cassandra-sslcontextfactory.yaml new file mode 100644 index 0000000000..fd1722645e --- /dev/null +++ b/test/conf/cassandra-sslcontextfactory.yaml @@ -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 diff --git a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java index 3ca69c8ff9..24d0b759d2 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java +++ b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java @@ -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(); diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java index 6e1e98c847..32d5432e82 100644 --- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java +++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java @@ -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", diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java index b15d8cd0d0..8536c01d93 100644 --- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java +++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java @@ -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()); + } } diff --git a/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java b/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java new file mode 100644 index 0000000000..dbb309b39f --- /dev/null +++ b/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java @@ -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 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 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 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 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 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 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()); + } +} diff --git a/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java b/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java index 23a6c005a3..5cd6c8f341 100644 --- a/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java +++ b/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java @@ -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 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 customSslContextFactoryParams = new HashMap<>(); + + for(EncryptionOptions.ConfigKey configKey: EncryptionOptions.ConfigKey.values()) + { + customSslContextFactoryParams.put(configKey.getKeyName(), "my-custom-value"); + } + + EncryptionOptionsTestCase.of(null, absentKeystore, true, customSslContextFactoryParams, ENCRYPTED); + } } diff --git a/test/unit/org/apache/cassandra/security/CustomSslContextFactoryConfigTest.java b/test/unit/org/apache/cassandra/security/CustomSslContextFactoryConfigTest.java new file mode 100644 index 0000000000..c1ab4a4a52 --- /dev/null +++ b/test/unit/org/apache/cassandra/security/CustomSslContextFactoryConfigTest.java @@ -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); + } + } +} diff --git a/test/unit/org/apache/cassandra/security/CustomSslContextFactoryInvalidConfigTest.java b/test/unit/org/apache/cassandra/security/CustomSslContextFactoryInvalidConfigTest.java new file mode 100644 index 0000000000..79e7d52536 --- /dev/null +++ b/test/unit/org/apache/cassandra/security/CustomSslContextFactoryInvalidConfigTest.java @@ -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(); + } +} diff --git a/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java b/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java new file mode 100644 index 0000000000..13d1faca04 --- /dev/null +++ b/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java @@ -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 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 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 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 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 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 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 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 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 config = new HashMap<>(); + config.putAll(commonConfig); + + DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config); + Assert.assertEquals(SslProvider.JDK, defaultSslContextFactoryImpl.getSslProvider()); + System.clearProperty("cassandra.disable_tcactive_openssl"); + } +} diff --git a/test/unit/org/apache/cassandra/security/DummySslContextFactoryImpl.java b/test/unit/org/apache/cassandra/security/DummySslContextFactoryImpl.java new file mode 100644 index 0000000000..3a14ff2937 --- /dev/null +++ b/test/unit/org/apache/cassandra/security/DummySslContextFactoryImpl.java @@ -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 parameters; + public DummySslContextFactoryImpl(Map 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 getAcceptedProtocols() + { + return null; + } + + @Override + public List getCipherSuites() + { + return null; + } + + /* + * For testing only + */ + public String getStringValueFor(String configKey) { + return parameters.containsKey(configKey) ? parameters.get(configKey).toString() : null; + } +} diff --git a/test/unit/org/apache/cassandra/security/SSLFactoryTest.java b/test/unit/org/apache/cassandra/security/SSLFactoryTest.java index 4cbc095929..a2fa1899dc 100644 --- a/test/unit/org/apache/cassandra/security/SSLFactoryTest.java +++ b/test/unit/org/apache/cassandra/security/SSLFactoryTest.java @@ -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 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 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 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 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); + } }