This is an automated email from the ASF dual-hosted git repository. smiklosovic pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push: new 6ab45971fc Introduce pluggable crypto providers and default to Amazon Corretto Crypto Provider 6ab45971fc is described below commit 6ab45971fc651f78c8748f80e3cd6d4a1b6dbc50 Author: ayushis <ayus...@netflix.com> AuthorDate: Mon Jul 10 15:21:07 2023 -0700 Introduce pluggable crypto providers and default to Amazon Corretto Crypto Provider patch by Ayushi Singh; reviewed by Stefan Miklosovic, Michael Semb Wever and Maxim Muzafarov for CASSANDRA-18624 Co-authored-by: Stefan Miklosovic <smikloso...@apache.org> --- .build/build-resolver.xml | 32 +- .build/parent-pom-template.xml | 41 +++ CHANGES.txt | 1 + NEWS.txt | 7 + bin/cassandra.in.sh | 6 + conf/cassandra.yaml | 15 + debian/cassandra.install | 2 + redhat/cassandra.in.sh | 6 + .../config/CassandraRelevantProperties.java | 3 + src/java/org/apache/cassandra/config/Config.java | 1 + .../cassandra/config/DatabaseDescriptor.java | 53 +++- .../cassandra/security/AbstractCryptoProvider.java | 205 ++++++++++++ .../cassandra/security/DefaultCryptoProvider.java | 67 ++++ .../org/apache/cassandra/security/JREProvider.java | 57 ++++ .../org/apache/cassandra/utils/FBUtilities.java | 21 ++ .../cassandra/distributed/impl/Instance.java | 2 + .../distributed/test/CryptoProviderTest.java | 203 ++++++++++++ .../config/DatabaseDescriptorRefTest.java | 1 + .../cassandra/security/CryptoProviderTest.java | 343 +++++++++++++++++++++ .../cassandra/security/InvalidCryptoProvider.java | 53 ++++ .../apache/cassandra/security/TestJREProvider.java | 42 +++ 21 files changed, 1156 insertions(+), 5 deletions(-) diff --git a/.build/build-resolver.xml b/.build/build-resolver.xml index 468adf76bf..5b95addb0d 100644 --- a/.build/build-resolver.xml +++ b/.build/build-resolver.xml @@ -64,7 +64,7 @@ <macrodef name="resolve"> <!-- - maven-resolver-ant-tasks's resolve logic doesn't have retry logic and does not respect settings.xml, + maven-resolver-ant-tasks's resolve logic doesn't have retry logic and does not respect settings.xml, this causes issues when overriding maven central is required (such as when behind a corporate firewall); it is critical to always provide the 'all' remoterepos to override resolve's default hard coded logic. @@ -74,7 +74,7 @@ <element name="elements" implicit="yes"/> <sequential> <retry retrycount="3"> - <resolver:resolve failonmissingattachments="@{failonmissingattachments}"> + <resolver:resolve failonmissingattachments="@{failonmissingattachments}"> <resolver:remoterepos refid="all"/> <elements/> </resolver:resolve> @@ -88,8 +88,8 @@ <sequential> <retry retrycount="3"> <resolver:pom file="@{file}" id="@{id}"> - <remoterepos refid="all"/> - <elements/> + <remoterepos refid="all"/> + <elements/> </resolver:pom> </retry> </sequential> @@ -206,6 +206,26 @@ </retry> <mkdir dir="${local.repository}/org/apache/cassandra/deps/sigar-bin"/> <mkdir dir="${build.lib}/sigar-bin"/> + <mkdir dir="${build.lib}/x86_64"/> + <mkdir dir="${build.lib}/aarch64"/> <!-- uname -m on arm prints aarch64 instead of aarch_64 --> + + <!-- artifacts needs AmazonCorrettoCryptoProvider for multiple archs --> + <retry retrycount="3" retrydelay="10" > + <resolve> + <dependencies> + <dependency groupId="software.amazon.cryptools" artifactId="AmazonCorrettoCryptoProvider" version="2.2.0" classifier="linux-x86_64" /> + </dependencies> + <files dir="${build.lib}/x86_64" layout="{artifactId}-{version}-{classifier}.{extension}" /> + </resolve> + </retry> + <retry retrycount="3" retrydelay="10" > + <resolve> + <dependencies> + <dependency groupId="software.amazon.cryptools" artifactId="AmazonCorrettoCryptoProvider" version="2.2.0" classifier="linux-aarch_64" /> + </dependencies> + <files dir="${build.lib}/aarch64" layout="{artifactId}-{version}-{classifier}.{extension}" /> + </resolve> + </retry> <retry retrycount="3" retrydelay="10" > <antcall target="_resolver-dist-lib_get_files"/> @@ -240,6 +260,10 @@ <file file="${local.repository}/org/apache/cassandra/deps/sigar-bin/libsigar-x86-linux.so"/> <file file="${local.repository}/org/apache/cassandra/deps/sigar-bin/libsigar-x86-solaris.so"/> </copy> + + <!-- as resolver will copy all dependencies into lib dir, and we are copying jars to lib/{x86_64|aarch64} as well, we would have duplicities --> + <delete file="${build.lib}/AmazonCorrettoCryptoProvider-2.2.0-linux-x86_64.jar" failonerror="false"/> + <delete file="${build.lib}/AmazonCorrettoCryptoProvider-2.2.0-linux-aarch_64.jar" failonerror="false"/> </target> <target name="_resolver-dist-lib_get_files"> diff --git a/.build/parent-pom-template.xml b/.build/parent-pom-template.xml index 50695a348d..4c54e5d94b 100644 --- a/.build/parent-pom-template.xml +++ b/.build/parent-pom-template.xml @@ -233,12 +233,53 @@ <id>zznate</id> <name>Nate McCall</name> </developer> + <developer> + <id>smiklosovic</id> + <name>Stefan Miklosovic</name> + </developer> </developers> <scm> <connection>scm:https://gitbox.apache.org/repos/asf/cassandra.git</connection> <developerConnection>scm:https://gitbox.apache.org/repos/asf/cassandra.git</developerConnection> <url>https://gitbox.apache.org/repos/asf?p=cassandra.git;a=tree</url> </scm> + + <profiles> + <profile> + <id>x86_64</id> + <activation> + <os> + <!-- we need something as a default even if it doesn't successfully load the .so files. --> + <arch>!aarch64</arch> + </os> + </activation> + <dependencies> + <dependency> + <groupId>software.amazon.cryptools</groupId> + <artifactId>AmazonCorrettoCryptoProvider</artifactId> + <classifier>linux-x86_64</classifier> + <version>2.2.0</version> + </dependency> + </dependencies> + </profile> + <profile> + <id>aarch_64</id> + <activation> + <os> + <arch>aarch64</arch> + </os> + </activation> + <dependencies> + <dependency> + <groupId>software.amazon.cryptools</groupId> + <artifactId>AmazonCorrettoCryptoProvider</artifactId> + <classifier>linux-aarch_64</classifier> + <version>2.2.0</version> + </dependency> + </dependencies> + </profile> + </profiles> + <dependencyManagement> <!-- Dependency metadata is specified here (version, scope, exclusions, etc.), then referenced in child POMs by groupId and diff --git a/CHANGES.txt b/CHANGES.txt index 683010434d..de5cd3768a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.0 + * Introduce pluggable crypto providers and default to AmazonCorrettoCryptoProvider (CASSANDRA-18624) * Improved DeletionTime serialization (CASSANDRA-18648) * CEP-7: Storage Attached Indexes (CASSANDRA-16052) * Add equals/hashCode override for ServerEncryptionOptions (CASSANDRA-18428) diff --git a/NEWS.txt b/NEWS.txt index 91fac33c98..1c5be180e8 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -71,6 +71,9 @@ using the provided 'sstableupgrade' tool. New features ------------ + - Pluggable crypto providers were made possible via `crypto_provider` section in cassandra.yaml. The default provider is + Amazon Corretto Crypto Provider and it is installed automatically upon node's start. Only x86_64 and aarch64 architectures are supported now. + Please consult upgrade section to know more details when upgrading from older Cassandra versions. - Added a new secondary index implementation, Storage-Attached Indexes (SAI). Overview documentation and a basic tutorial can be found at src/java/org/apache/cassandra/index/sai/README.md. - *Experimental* support for Java 17 has been added. JVM options that differ between or are @@ -210,6 +213,10 @@ Upgrading Consult cassandra-rackdc.properties for more details. (CASSANDRA-16555) - JMX MBean `org.apache.cassandra.metrics:type=BufferPool` without scope has been removed. Use instead `org.apache.cassandra.metrics:type=BufferPool,scope=chunk-cache`. (CASSANDRA-17668) + - Upon upgrade, when cassandra.yaml does not contain `crypto_provider` configuration section, crypto providers from JRE installation will be used + and no installation of DefaultCryptoProvider installing Amazon Corretto Crypto Provider will be conducted. + You need to explicitly add this section to the old yaml if it does not contain it yet to enable Amazon Corretto Crypto Provider for such node. + New deployments have `crypto_provider` uncommented with DefaultCryptoProvider hence Corretto provider will be installed automatically for corresponding architecture. Deprecation diff --git a/bin/cassandra.in.sh b/bin/cassandra.in.sh index ee7a8e223c..dfa17643fd 100644 --- a/bin/cassandra.in.sh +++ b/bin/cassandra.in.sh @@ -84,6 +84,12 @@ JAVA_AGENT="$JAVA_AGENT -javaagent:$CASSANDRA_HOME/lib/jamm-0.4.0.jar" # Added sigar-bin to the java.library.path CASSANDRA-7838 JAVA_OPTS="$JAVA_OPTS:-Djava.library.path=$CASSANDRA_HOME/lib/sigar-bin" +platform=$(uname -m) +if [ -d "$CASSANDRA_HOME"/lib/"$platform" ]; then + for jar in "$CASSANDRA_HOME"/lib/"$platform"/*.jar ; do + CLASSPATH="$CLASSPATH:${jar}" + done +fi # # Java executable and per-Java version JVM settings diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index a9d0ddadbc..6414fab956 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -1375,6 +1375,21 @@ dynamic_snitch_reset_interval: 600000ms # until the pinned host was 20% worse than the fastest. dynamic_snitch_badness_threshold: 1.0 +# Configures Java crypto provider. By default, it will use DefaultCryptoProvider +# which will install Amazon Correto Crypto Provider. +# +# Amazon Correto Crypto Provider works currently for x86_64 and aarch_64 platforms. +# If this provider fails it will fall back to the default crypto provider in the JRE. +# +# To force failure when the provider was not installed properly, set the property "fail_on_missing_provider" to "true". +# +# To bypass the installation of a crypto provider use class 'org.apache.cassandra.security.JREProvider' +# +crypto_provider: + - class_name: org.apache.cassandra.security.DefaultCryptoProvider + parameters: + - fail_on_missing_provider: "false" + # Configure server-to-server internode encryption # # JVM and netty defaults for supported SSL socket protocols and cipher suites can diff --git a/debian/cassandra.install b/debian/cassandra.install index dced5a29e2..abba6a7843 100644 --- a/debian/cassandra.install +++ b/debian/cassandra.install @@ -29,3 +29,5 @@ tools/bin/sstablepartitions usr/bin lib/*.jar usr/share/cassandra/lib lib/*.zip usr/share/cassandra/lib lib/sigar-bin/* usr/share/cassandra/lib/sigar-bin +lib/x86_64/* usr/share/cassandra/lib/x86_64 +lib/aarch64/* usr/share/cassandra/lib/aarch64 diff --git a/redhat/cassandra.in.sh b/redhat/cassandra.in.sh index fed5d4384e..8ec1905ac0 100644 --- a/redhat/cassandra.in.sh +++ b/redhat/cassandra.in.sh @@ -42,6 +42,12 @@ CLASSPATH="$CLASSPATH:$EXTRA_CLASSPATH" # set JVM javaagent opts to avoid warnings/errors JAVA_AGENT="$JAVA_AGENT -javaagent:$CASSANDRA_HOME/lib/jamm-0.4.0.jar" +platform=$(uname -m) +if [ -d "$CASSANDRA_HOME"/lib/"$platform" ]; then + for jar in "$CASSANDRA_HOME"/lib/"$platform"/*.jar ; do + CLASSPATH="$CLASSPATH:${jar}" + done +fi # # Java executable and per-Java version JVM settings diff --git a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java index 5d5c14270a..b763899d7d 100644 --- a/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java +++ b/src/java/org/apache/cassandra/config/CassandraRelevantProperties.java @@ -170,6 +170,7 @@ public enum CassandraRelevantProperties CONSISTENT_DIRECTORY_LISTINGS("cassandra.consistent_directory_listings"), CONSISTENT_RANGE_MOVEMENT("cassandra.consistent.rangemovement", "true"), CONSISTENT_SIMULTANEOUS_MOVES_ALLOW("cassandra.consistent.simultaneousmoves.allow"), + CRYPTO_PROVIDER_CLASS_NAME("cassandra.crypto_provider_class_name"), CUSTOM_GUARDRAILS_CONFIG_PROVIDER_CLASS("cassandra.custom_guardrails_config_provider_class"), CUSTOM_QUERY_HANDLER_CLASS("cassandra.custom_query_handler_class"), CUSTOM_TRACING_CLASS("cassandra.custom_tracing_class"), @@ -212,6 +213,7 @@ public enum CassandraRelevantProperties EXPIRATION_DATE_OVERFLOW_POLICY("cassandra.expiration_date_overflow_policy"), EXPIRATION_OVERFLOW_WARNING_INTERVAL_MINUTES("cassandra.expiration_overflow_warning_interval_minutes", "5"), FAILURE_LOGGING_INTERVAL_SECONDS("cassandra.request_failure_log_interval_seconds", "60"), + FAIL_ON_MISSING_CRYPTO_PROVIDER("cassandra.fail_on_missing_crypto_provider", "false"), FD_INITIAL_VALUE_MS("cassandra.fd_initial_value_ms"), FD_MAX_INTERVAL_MS("cassandra.fd_max_interval_ms"), FILE_CACHE_ENABLED("cassandra.file_cache_enabled"), @@ -528,6 +530,7 @@ public enum CassandraRelevantProperties TEST_SIMULATOR_PRINT_ASM_CLASSES("cassandra.test.simulator.print_asm_classes", ""), TEST_SIMULATOR_PRINT_ASM_OPTS("cassandra.test.simulator.print_asm_opts", ""), TEST_SIMULATOR_PRINT_ASM_TYPES("cassandra.test.simulator.print_asm_types", ""), + TEST_SKIP_CRYPTO_PROVIDER_INSTALLATION("cassandra.test.security.skip.provider.installation", "false"), TEST_SSTABLE_FORMAT_DEVELOPMENT("cassandra.test.sstableformatdevelopment"), TEST_STRICT_LCS_CHECKS("cassandra.test.strict_lcs_checks"), /** Turns some warnings into exceptions for testing. */ diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index 5b3bc81887..e254e54e05 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -80,6 +80,7 @@ public class Config public String authenticator; public String authorizer; public String role_manager; + public ParameterizedClass crypto_provider; public String network_authorizer; @Replaces(oldName = "permissions_validity_in_ms", converter = Converters.MILLIS_DURATION_INT, deprecated = true) public volatile DurationSpec.IntMillisecondsBound permissions_validity = new DurationSpec.IntMillisecondsBound("2s"); diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index 4e5a26fafc..558f4a349b 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -98,7 +98,9 @@ import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.locator.Replica; import org.apache.cassandra.locator.SeedProvider; +import org.apache.cassandra.security.AbstractCryptoProvider; import org.apache.cassandra.security.EncryptionContext; +import org.apache.cassandra.security.JREProvider; import org.apache.cassandra.security.SSLFactory; import org.apache.cassandra.service.CacheService.CacheType; import org.apache.cassandra.service.paxos.Paxos; @@ -123,10 +125,11 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.SEARCH_CON import static org.apache.cassandra.config.CassandraRelevantProperties.SSL_STORAGE_PORT; import static org.apache.cassandra.config.CassandraRelevantProperties.STORAGE_DIR; import static org.apache.cassandra.config.CassandraRelevantProperties.STORAGE_PORT; -import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_STRICT_RUNTIME_CHECKS; import static org.apache.cassandra.config.CassandraRelevantProperties.SUN_ARCH_DATA_MODEL; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_FAIL_MV_LOCKS_COUNT; import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_JVM_DTEST_DISABLE_SSL; +import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_SKIP_CRYPTO_PROVIDER_INSTALLATION; +import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_STRICT_RUNTIME_CHECKS; import static org.apache.cassandra.config.CassandraRelevantProperties.UNSAFE_SYSTEM; import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.BYTES_PER_SECOND; import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.MEBIBYTES_PER_SECOND; @@ -174,6 +177,7 @@ public class DatabaseDescriptor private static Config.DiskAccessMode indexAccessMode; + private static AbstractCryptoProvider cryptoProvider; private static IAuthenticator authenticator; private static IAuthorizer authorizer; private static INetworkAuthorizer networkAuthorizer; @@ -426,6 +430,8 @@ public class DatabaseDescriptor //InetAddressAndPort cares that applySimpleConfig runs first applySSTableFormats(); + applyCryptoProvider(); + applySimpleConfig(); applyPartitioner(); @@ -1219,6 +1225,42 @@ public class DatabaseDescriptor } } + public static void applyCryptoProvider() + { + if (TEST_SKIP_CRYPTO_PROVIDER_INSTALLATION.getBoolean()) + return; + + if (conf.crypto_provider == null) + conf.crypto_provider = new ParameterizedClass(JREProvider.class.getName(), null); + + // properties beat configuration + String classNameFromSystemProperties = CassandraRelevantProperties.CRYPTO_PROVIDER_CLASS_NAME.getString(); + if (classNameFromSystemProperties != null) + conf.crypto_provider.class_name = classNameFromSystemProperties; + + if (conf.crypto_provider.class_name == null) + throw new ConfigurationException("Failed to initialize crypto provider, class_name cannot be null"); + + if (conf.crypto_provider.parameters == null) + conf.crypto_provider.parameters = new HashMap<>(); + + Map<String, String> cryptoProviderParameters = new HashMap<>(conf.crypto_provider.parameters); + cryptoProviderParameters.putIfAbsent(AbstractCryptoProvider.FAIL_ON_MISSING_PROVIDER_KEY, "false"); + + try + { + cryptoProvider = FBUtilities.newCryptoProvider(conf.crypto_provider.class_name, cryptoProviderParameters); + cryptoProvider.install(); + } + catch (Exception e) + { + if (e instanceof ConfigurationException) + throw (ConfigurationException) e; + else + throw new ConfigurationException(String.format("Failed to initialize crypto provider %s", conf.crypto_provider.class_name), e); + } + } + public static void applySeedProvider() { // load the seeds for node contact points @@ -1502,6 +1544,15 @@ public class DatabaseDescriptor return detector; } + public static AbstractCryptoProvider getCryptoProvider() + { + return cryptoProvider; + } + + public static void setCryptoProvider(AbstractCryptoProvider cryptoProvider) + { + DatabaseDescriptor.cryptoProvider = cryptoProvider; + } public static IAuthenticator getAuthenticator() { return authenticator; diff --git a/src/java/org/apache/cassandra/security/AbstractCryptoProvider.java b/src/java/org/apache/cassandra/security/AbstractCryptoProvider.java new file mode 100644 index 0000000000..1c437e6f2d --- /dev/null +++ b/src/java/org/apache/cassandra/security/AbstractCryptoProvider.java @@ -0,0 +1,205 @@ +/* + * 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.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.FBUtilities; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.Provider; +import java.security.Security; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static java.lang.String.format; +import static org.apache.cassandra.config.CassandraRelevantProperties.FAIL_ON_MISSING_CRYPTO_PROVIDER; + +public abstract class AbstractCryptoProvider +{ + protected static final Logger logger = LoggerFactory.getLogger(AbstractCryptoProvider.class); + public static final String FAIL_ON_MISSING_PROVIDER_KEY = "fail_on_missing_provider"; + + protected final boolean failOnMissingProvider; + private final Map<String, String> properties; + + public AbstractCryptoProvider(Map<String, String> args) + { + this.properties = args == null ? new HashMap<>() : args; + boolean failOnMissingProviderFromProperties = Boolean.parseBoolean(this.properties.getOrDefault(FAIL_ON_MISSING_PROVIDER_KEY, "false")); + failOnMissingProvider = FAIL_ON_MISSING_CRYPTO_PROVIDER.getBoolean(failOnMissingProviderFromProperties); + } + + /** + * Returns unmodifiable properties of this crypto provider + * + * @return crypto provider properties + */ + public Map<String, String> getProperties() + { + return Collections.unmodifiableMap(properties); + } + + /** + * Returns name of the provider, as returned from {@link Provider#getName()} + * + * @return name of the provider + */ + public abstract String getProviderName(); + + /** + * Returns the name of the class which installs specific provider of name {@link #getProviderName()}. + * + * @return name of class of provider + */ + public abstract String getProviderClassAsString(); + + /** + * Returns a runnable which installs this crypto provider. + * + * @return runnable which installs this provider + */ + protected abstract Runnable installator(); + + /** + * Returns boolean telling if this provider was installed properly. + * + * @return {@code true} if provider was installed properly, {@code false} otherwise. + */ + protected abstract boolean isHealthyInstallation() throws Exception; + + /** + * The default installation runs {@link AbstractCryptoProvider#installator()} and after that + * {@link AbstractCryptoProvider#isHealthyInstallation()}. + * <p> + * If any step fails, it will not throw an exception unless the parameter + * {@link AbstractCryptoProvider#FAIL_ON_MISSING_PROVIDER_KEY} is {@code true}. + */ + public void install() throws Exception + { + String failureMessage = null; + Throwable t = null; + try + { + if (JREProvider.class.getName().equals(getProviderClassAsString())) + { + logger.info(format("Installation of a crypto provider was skipped as %s was used.", JREProvider.class.getName())); + return; + } + + FBUtilities.classForName(getProviderClassAsString(), "crypto provider"); + + String providerName = getProviderName(); + int providerPosition = getProviderPosition(providerName); + if (providerPosition > 0) + { + if (providerPosition == 1) + { + logger.info("{} was already installed on position {}.", providerName, providerPosition); + } + else if (failOnMissingProvider) + { + throw new IllegalStateException(String.format("%s was already installed on position %s.", providerName, providerPosition)); + } + else + { + logger.warn("{} was already installed on position {}. Check the configuration of " + + "JRE and either remove the provider from java.security or do not install this provider " + + "by Cassandra.", providerName, providerPosition); + return; + } + } + else + { + Runnable r = installator(); + if (r == null) + throw new IllegalStateException("Installator runnable can not be null!"); + else + r.run(); + } + + if (isHealthyInstallation()) + logger.info("{} health check OK.", getProviderName()); + else + failureMessage = format("%s has not passed the health check. " + + "Check node's architecture (`uname -m`) is supported, see lib/<arch> subdirectories. " + + "The correct architecture-specific library for %s needs to be on the classpath. ", + getProviderName(), + getProviderClassAsString()); + } + catch (ConfigurationException ex) + { + failureMessage = getProviderClassAsString() + " is not on the class path! " + + "Check node's architecture (`uname -m`) is supported, see lib/<arch> subdirectories. " + + "The correct architecture-specific library for needs to be on the classpath."; + } + catch (Throwable ex) + { + failureMessage = format("The installation of %s was not successful, reason: %s", + getProviderClassAsString(), ex.getMessage()); + t = ex; + } + + if (failureMessage != null) + { + // To be sure there is not any leftover, proactively remove this provider in case of any failure. + // This method returns silently if the provider is not installed or if name is null. + try + { + uninstall(); + } + catch (Throwable throwable) + { + logger.warn("Uninstallation of {} failed", getProviderName(), throwable); + } + + if (failOnMissingProvider) + throw new ConfigurationException(failureMessage, t); + else + logger.warn(failureMessage); + } + } + + /** + * Uninstalls this crypto provider of name {@link #getProviderName()} + * + * @see Security#removeProvider(String) + */ + public void uninstall() + { + Security.removeProvider(getProviderName()); + } + + private int getProviderPosition(String providerName) + { + Provider[] providers = Security.getProviders(); + + for (int i = 0; i < providers.length; i++) + { + if (providers[i].getName().equals(providerName)) + { + return i + 1; + } + } + + return -1; + } +} diff --git a/src/java/org/apache/cassandra/security/DefaultCryptoProvider.java b/src/java/org/apache/cassandra/security/DefaultCryptoProvider.java new file mode 100644 index 0000000000..ac7ef2c652 --- /dev/null +++ b/src/java/org/apache/cassandra/security/DefaultCryptoProvider.java @@ -0,0 +1,67 @@ +/* + * 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; +import javax.crypto.Cipher; + +import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider; + +/** + * Default crypto provider tries to install AmazonCorrettoCryptoProvider. + * <p> + * The implementation falls back to in-built crypto provider in JRE if the installation + * is not successful. + */ +public class DefaultCryptoProvider extends AbstractCryptoProvider +{ + public DefaultCryptoProvider(Map<String, String> args) + { + super(args); + } + + @Override + public String getProviderName() + { + return "AmazonCorrettoCryptoProvider"; + } + + @Override + public String getProviderClassAsString() + { + return "com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider"; + } + + @Override + public Runnable installator() + { + return AmazonCorrettoCryptoProvider::install; + } + + @Override + public boolean isHealthyInstallation() throws Exception + { + if (!getProviderName().equals(Cipher.getInstance("AES/GCM/NoPadding").getProvider().getName())) + return false; + + AmazonCorrettoCryptoProvider.INSTANCE.assertHealthy(); + + return true; + } +} diff --git a/src/java/org/apache/cassandra/security/JREProvider.java b/src/java/org/apache/cassandra/security/JREProvider.java new file mode 100644 index 0000000000..0cd61a0d9c --- /dev/null +++ b/src/java/org/apache/cassandra/security/JREProvider.java @@ -0,0 +1,57 @@ +/* + * 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; + +/** + * Crypto provider which does nothing. Handy for situations when a user + * wants to completely bypass crypto provider installation. + */ +public class JREProvider extends AbstractCryptoProvider +{ + public JREProvider(Map<String, String> properties) + { + super(properties); + } + + @Override + public String getProviderName() + { + return JREProvider.class.getSimpleName(); + } + + @Override + public String getProviderClassAsString() + { + return JREProvider.class.getName(); + } + + @Override + protected Runnable installator() + { + return () -> {}; + } + + @Override + protected boolean isHealthyInstallation() throws Exception + { + return true; + } +} diff --git a/src/java/org/apache/cassandra/utils/FBUtilities.java b/src/java/org/apache/cassandra/utils/FBUtilities.java index 6b2ddb0f86..a39b1bb22e 100644 --- a/src/java/org/apache/cassandra/utils/FBUtilities.java +++ b/src/java/org/apache/cassandra/utils/FBUtilities.java @@ -87,6 +87,7 @@ import org.apache.cassandra.io.util.DataOutputBufferFixed; import org.apache.cassandra.io.util.File; import org.apache.cassandra.io.util.FileUtils; import org.apache.cassandra.locator.InetAddressAndPort; +import org.apache.cassandra.security.AbstractCryptoProvider; import org.apache.cassandra.security.ISslContextFactory; import org.apache.cassandra.utils.concurrent.FutureCombiner; import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException; @@ -702,6 +703,26 @@ public class FBUtilities } } + public static AbstractCryptoProvider newCryptoProvider(String className, Map<String, String> parameters) throws ConfigurationException + { + try + { + if (!className.contains(".")) + className = "org.apache.cassandra.security." + className; + + Class<?> cryptoProviderClass = FBUtilities.classForName(className, "crypto provider class"); + return (AbstractCryptoProvider) cryptoProviderClass.getConstructor(Map.class).newInstance(Collections.unmodifiableMap(parameters)); + } + catch (Exception e) + { + // no need to wrap it in another ConfgurationException if FBUtilities.classForName might throw it + if (e instanceof ConfigurationException) + throw (ConfigurationException) e; + else + throw new ConfigurationException(String.format("Unable to create an instance of crypto provider for %s", className), e); + } + } + /** * @return The Class for the given name. * @param classname Fully qualified classname. diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java index 590df6264e..6ae31794f4 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java @@ -923,6 +923,8 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance error = parallelRun(error, executor, this::stopJmx); + error = parallelRun(error, executor, () -> DatabaseDescriptor.getCryptoProvider().uninstall()); + // Make sure any shutdown hooks registered for DeleteOnExit are released to prevent // references to the instance class loaders from being held if (graceful) diff --git a/test/distributed/org/apache/cassandra/distributed/test/CryptoProviderTest.java b/test/distributed/org/apache/cassandra/distributed/test/CryptoProviderTest.java new file mode 100644 index 0000000000..f1e6428f08 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/CryptoProviderTest.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.cassandra.distributed.test; + +import java.security.Provider; +import java.security.Security; +import java.util.HashMap; + +import org.junit.Before; +import org.junit.Test; + +import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.implementation.MethodDelegation; +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IIsolatedExecutor.SerializableCallable; +import org.apache.cassandra.security.DefaultCryptoProvider; +import org.apache.cassandra.security.JREProvider; + +import static java.lang.String.format; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class CryptoProviderTest extends TestBaseImpl +{ + @Before + public void beforeTest() + { + assertNotInstalledDefaultProvider(); + } + + @Test + public void testDefaultCryptoProvider() throws Throwable + { + try (Cluster cluster = init(Cluster.build(1) + .withConfig(config -> config.set("crypto_provider", + new HashMap<>() + {{ + put("class_name", DefaultCryptoProvider.class.getName()); + }})) + .start())) + { + assertTrue("Amazon Corretto Crypto Provider should be installed!", + cluster.get(1).callOnInstance((SerializableCallable<Boolean>) () -> { + Provider provider = Security.getProviders()[0]; + return provider != null && AmazonCorrettoCryptoProvider.PROVIDER_NAME.equals(provider.getName()); + })); + } + finally + { + Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME); + } + } + + @Test + public void testUsingJREProvider() throws Throwable + { + try (Cluster cluster = init(Cluster.build(1) + .withConfig(config -> config.set("crypto_provider", + new HashMap<>() + {{ + put("class_name", JREProvider.class.getName()); + }})) + .start())) + { + assertTrue("Amazon Corretto Crypto Provider should not be installed!", + cluster.get(1).callOnInstance((SerializableCallable<Boolean>) () -> { + Provider provider = Security.getProviders()[0]; + return provider != null && !AmazonCorrettoCryptoProvider.PROVIDER_NAME.equals(provider.getName()); + })); + } + finally + { + Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME); + } + } + + @Test + public void testUsingNoProviderUsesJREProvider() throws Throwable + { + // crypto_provider = null will be in Descriptor if it is not set in yaml (e.g. nodes being upgraded + // with the old cassandra.yaml, or if it is commented out + try (Cluster cluster = init(Cluster.build(1).start())) + { + assertTrue("Amazon Corretto Crypto Provider should not be installed!", + cluster.get(1).callOnInstance((SerializableCallable<Boolean>) () -> { + Provider provider = Security.getProviders()[0]; + return provider != null && !AmazonCorrettoCryptoProvider.PROVIDER_NAME.equals(provider.getName()); + })); + } + finally + { + Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME); + } + } + + @Test + public void testFailedDefaultProviderInstallationShouldResumeStartup() throws Throwable + { + try (Cluster cluster = builder().withNodes(1) + .withInstanceInitializer(BB::install) + .withConfig(config -> config.set("crypto_provider", + new HashMap<>() + {{ + put("class_name", DefaultCryptoProvider.class.getName()); + }})) + .createWithoutStarting()) + { + cluster.get(1).startup(); + + assertTrue("Amazon Corretto Crypto Provider should not be installed!", + cluster.get(1).callOnInstance((SerializableCallable<Boolean>) () -> { + Provider provider = Security.getProviders()[0]; + return provider != null && !AmazonCorrettoCryptoProvider.PROVIDER_NAME.equals(provider.getName()); + })); + } + catch (Exception ex) + { + fail("Startup should not fail! It should fallback to in-built providers and continue to boot"); + } + finally + { + Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME); + } + } + + @Test + public void testFailedDefaultProviderInstallationShouldFailStartupOnFailOnMissingProperty() throws Throwable + { + try (Cluster cluster = builder().withNodes(1) + .withInstanceInitializer(BB::install) + .withConfig(config -> config.set("crypto_provider", + new HashMap<>() + {{ + put("class_name", DefaultCryptoProvider.class.getName()); + put("parameters", new HashMap<>() + {{ + put("fail_on_missing_provider", "true"); + }}); + }})) + .createWithoutStarting()) + { + cluster.get(1).startup(); + + fail("Startup should fail! It should not fallback to in-built providers and continue to boot " + + "as fail_on_missing_provider is set to true"); + } + catch (Exception ex) + { + assertEquals(format("The installation of %s was not successful, reason: exception from test", AmazonCorrettoCryptoProvider.class.getName()), + ex.getMessage()); + + assertNotInstalledDefaultProvider(); + } + finally + { + Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME); + } + } + + public static void assertNotInstalledDefaultProvider() + { + for (Provider provider : Security.getProviders()) + assertNotEquals(AmazonCorrettoCryptoProvider.PROVIDER_NAME, provider.getName()); + } + + public static class BB + { + public static void install(ClassLoader cl, Integer num) + { + new ByteBuddy().redefine(DefaultCryptoProvider.class) + .method(named("installator")) + .intercept(MethodDelegation.to(BB.class)) + .make() + .load(cl, ClassLoadingStrategy.Default.INJECTION); + } + + public static Runnable installator() + { + throw new RuntimeException("exception from test"); + } + } +} diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java index 8d5ff9aca4..4b1890ffff 100644 --- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java +++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java @@ -263,6 +263,7 @@ public class DatabaseDescriptorRefTest "org.apache.cassandra.security.ISslContextFactory", "org.apache.cassandra.security.SSLFactory", "org.apache.cassandra.service.CacheService$CacheType", + "org.apache.cassandra.security.AbstractCryptoProvider", "org.apache.cassandra.transport.ProtocolException", "org.apache.cassandra.utils.Closeable", "org.apache.cassandra.utils.CloseableIterator", diff --git a/test/unit/org/apache/cassandra/security/CryptoProviderTest.java b/test/unit/org/apache/cassandra/security/CryptoProviderTest.java new file mode 100644 index 0000000000..7bb4cbb893 --- /dev/null +++ b/test/unit/org/apache/cassandra/security/CryptoProviderTest.java @@ -0,0 +1,343 @@ +/* + * 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.security.Provider; +import java.security.Security; +import java.util.HashMap; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.ParameterizedClass; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.FBUtilities; +import org.mockito.MockedStatic; + +import static com.google.common.collect.ImmutableMap.of; +import static java.lang.String.format; +import static org.apache.cassandra.config.CassandraRelevantProperties.CRYPTO_PROVIDER_CLASS_NAME; +import static org.apache.cassandra.config.CassandraRelevantProperties.FAIL_ON_MISSING_CRYPTO_PROVIDER; +import static org.apache.cassandra.config.CassandraRelevantProperties.TEST_SKIP_CRYPTO_PROVIDER_INSTALLATION; +import static org.apache.cassandra.security.AbstractCryptoProvider.FAIL_ON_MISSING_PROVIDER_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; + +public class CryptoProviderTest +{ + @BeforeClass + public static void beforeClass() + { + TEST_SKIP_CRYPTO_PROVIDER_INSTALLATION.setBoolean(true); + DatabaseDescriptor.daemonInitialization(); + TEST_SKIP_CRYPTO_PROVIDER_INSTALLATION.setBoolean(false); + } + + @Before + public void beforeTest() + { + // be sure it is uninstalled / reset + DatabaseDescriptor.getRawConfig().crypto_provider = null; + DatabaseDescriptor.setCryptoProvider(null); + Security.removeProvider(AmazonCorrettoCryptoProvider.PROVIDER_NAME); + assertNotInstalledDefaultProvider(); + } + + public static void assertNotInstalledDefaultProvider() + { + for (Provider provider : Security.getProviders()) + assertNotEquals(AmazonCorrettoCryptoProvider.PROVIDER_NAME, provider.getName()); + } + + @Test + public void testCryptoProviderClassSystemProperty() + { + try (WithProperties properties = new WithProperties().set(CRYPTO_PROVIDER_CLASS_NAME, TestJREProvider.class.getName())) + { + DatabaseDescriptor.applyCryptoProvider(); + assertEquals(TestJREProvider.class.getSimpleName(), DatabaseDescriptor.getCryptoProvider().getProviderName()); + } + } + + @Test + public void testNoCryptoProviderInstallationUseJREProvider() + { + DatabaseDescriptor.applyCryptoProvider(); + assertEquals("JREProvider", DatabaseDescriptor.getCryptoProvider().getProviderName()); + } + + @Test + public void testCryptoProviderInstallationWithNullParameters() + { + DatabaseDescriptor.getRawConfig().crypto_provider = new ParameterizedClass(TestJREProvider.class.getName(), null); + DatabaseDescriptor.applyCryptoProvider(); + + AbstractCryptoProvider cryptoProvider = DatabaseDescriptor.getCryptoProvider(); + assertThat(cryptoProvider.getProviderName()).isEqualTo(TestJREProvider.class.getSimpleName()); + assertThat(cryptoProvider.getProperties()).isNotNull() + .isNotEmpty() + .hasSize(1) + .containsKeys("fail_on_missing_provider") + .containsValues("false"); + } + + @Test + public void testCryptoProviderInstallationWithEmptyParameters() + { + DatabaseDescriptor.getRawConfig().crypto_provider = new ParameterizedClass(TestJREProvider.class.getName(), of()); + DatabaseDescriptor.applyCryptoProvider(); + + AbstractCryptoProvider cryptoProvider = DatabaseDescriptor.getCryptoProvider(); + assertThat(cryptoProvider.getProviderName()).isEqualTo(TestJREProvider.class.getSimpleName()); + assertThat(cryptoProvider.getProperties()).isNotNull() + .isNotEmpty() + .hasSize(1) + .containsKeys("fail_on_missing_provider") + .containsValues("false"); + } + + @Test + public void testCryptoProviderInstallationWithNotEmptyParameters() + { + DatabaseDescriptor.getRawConfig().crypto_provider = new ParameterizedClass(TestJREProvider.class.getName(), + of("k1", "v1", "k2", "v2")); + DatabaseDescriptor.applyCryptoProvider(); + + AbstractCryptoProvider cryptoProvider = DatabaseDescriptor.getCryptoProvider(); + assertThat(cryptoProvider.getProviderName()).isEqualTo(TestJREProvider.class.getSimpleName()); + assertThat(cryptoProvider.getProperties()).isNotNull() + .isNotEmpty() + .hasSize(3) + .containsKeys("k1", "k2", "fail_on_missing_provider") + .containsValues("v1", "v2", "false"); + } + + @Test + public void testCryptoProviderInstallationWithSimpleClassName() + { + DatabaseDescriptor.getRawConfig().crypto_provider = new ParameterizedClass(TestJREProvider.class.getSimpleName(), null); + DatabaseDescriptor.applyCryptoProvider(); + + AbstractCryptoProvider cryptoProvider = DatabaseDescriptor.getCryptoProvider(); + assertThat(cryptoProvider.getProviderName()).isEqualTo(TestJREProvider.class.getSimpleName()); + assertThat(cryptoProvider.getProperties()).isNotNull() + .isNotEmpty() + .hasSize(1) + .containsKeys("fail_on_missing_provider") + .containsValues("false"); + } + + @Test + public void testUnableToCreateDefaultCryptoProvider() + { + try (MockedStatic<FBUtilities> fbUtilitiesMock = mockStatic(FBUtilities.class)) + { + DatabaseDescriptor.getRawConfig().crypto_provider = new ParameterizedClass(DefaultCryptoProvider.class.getName(), + of("k1", "v1", "k2", "v2")); + + fbUtilitiesMock.when(() -> FBUtilities.classForName(DefaultCryptoProvider.class.getName(), "crypto provider class")) + .thenThrow(new RuntimeException("exception from test")); + + fbUtilitiesMock.when(() -> FBUtilities.newCryptoProvider(anyString(), anyMap())).thenCallRealMethod(); + + assertThatThrownBy(DatabaseDescriptor::applyCryptoProvider) + .isInstanceOf(ConfigurationException.class) + .hasCauseInstanceOf(RuntimeException.class) + .hasMessage("Unable to create an instance of crypto provider for " + DefaultCryptoProvider.class.getName()) + .hasRootCauseMessage("exception from test"); + + assertNotInstalledDefaultProvider(); + } + } + + @Test + public void testFailOnMissingProviderSystemProperty() + { + try (WithProperties properties = new WithProperties().set(FAIL_ON_MISSING_CRYPTO_PROVIDER, "true") + .set(CRYPTO_PROVIDER_CLASS_NAME, InvalidCryptoProvider.class.getName())) + { + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(DatabaseDescriptor::applyCryptoProvider) + .withMessage("some.package.non.existing.ClassName is not on the class path! Check node's architecture " + + "(`uname -m`) is supported, see lib/<arch> subdirectories. The correct architecture-specific " + + "library for needs to be on the classpath."); + } + } + + @Test + public void testCryptoProviderInstallation() throws Exception + { + AbstractCryptoProvider provider = new DefaultCryptoProvider(new HashMap<>()); + assertFalse(provider.failOnMissingProvider); + + Provider originalProvider = Security.getProviders()[0]; + + provider.install(); + assertTrue(provider.isHealthyInstallation()); + Provider installedProvider = Security.getProviders()[0]; + assertEquals(installedProvider.getName(), provider.getProviderName()); + + provider.uninstall(); + Provider currentProvider = Security.getProviders()[0]; + assertNotEquals(currentProvider.getName(), installedProvider.getName()); + assertEquals(originalProvider.getName(), currentProvider.getName()); + } + + @Test + public void testInvalidProviderInstallator() + { + AbstractCryptoProvider spiedProvider = spy(new DefaultCryptoProvider(of(FAIL_ON_MISSING_PROVIDER_KEY, "true"))); + + Runnable installator = () -> + { + throw new RuntimeException("invalid installator"); + }; + + doReturn(installator).when(spiedProvider).installator(); + + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(spiedProvider::install) + .withRootCauseInstanceOf(RuntimeException.class) + .withMessage("The installation of %s was not successful, reason: invalid installator", spiedProvider.getProviderClassAsString()); + } + + @Test + public void testNullInstallatorThrowsException() + { + AbstractCryptoProvider spiedProvider = spy(new DefaultCryptoProvider(of(FAIL_ON_MISSING_PROVIDER_KEY, "true"))); + + doReturn(null).when(spiedProvider).installator(); + + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(spiedProvider::install) + .withRootCauseInstanceOf(RuntimeException.class) + .withMessage("The installation of %s was not successful, reason: Installator runnable can not be null!", spiedProvider.getProviderClassAsString()); + } + + @Test + public void testProviderHealthcheckerReturningFalse() throws Exception + { + AbstractCryptoProvider spiedProvider = spy(new DefaultCryptoProvider(of(FAIL_ON_MISSING_PROVIDER_KEY, "true"))); + + doReturn(false).when(spiedProvider).isHealthyInstallation(); + + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(spiedProvider::install) + .withCause(null) + .withMessage(format("%s has not passed the health check. " + + "Check node's architecture (`uname -m`) is supported, see lib/<arch> subdirectories. " + + "The correct architecture-specific library for %s needs to be on the classpath. ", + spiedProvider.getProviderName(), + spiedProvider.getProviderClassAsString())); + } + + @Test + public void testHealthcheckerThrowingException() throws Exception + { + AbstractCryptoProvider spiedProvider = spy(new DefaultCryptoProvider(of(FAIL_ON_MISSING_PROVIDER_KEY, "true"))); + + Throwable t = new RuntimeException("error in health checker"); + doThrow(t).when(spiedProvider).isHealthyInstallation(); + + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(spiedProvider::install) + .withCauseInstanceOf(RuntimeException.class) + .withMessage(format("The installation of %s was not successful, reason: %s", + spiedProvider.getProviderClassAsString(), t.getMessage())); + } + + @Test + public void testProviderNotOnClassPathWithPropertyInYaml() throws Exception + { + InvalidCryptoProvider cryptoProvider = new InvalidCryptoProvider(of(FAIL_ON_MISSING_PROVIDER_KEY, "true")); + + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(cryptoProvider::install) + .withMessage("some.package.non.existing.ClassName is not on the class path! Check node's architecture " + + "(`uname -m`) is supported, see lib/<arch> subdirectories. " + + "The correct architecture-specific library for needs to be on the classpath."); + } + + @Test + public void testProviderNotOnClassPathWithSystemProperty() + { + try (WithProperties properties = new WithProperties().set(FAIL_ON_MISSING_CRYPTO_PROVIDER, "true")) + { + InvalidCryptoProvider cryptoProvider = new InvalidCryptoProvider(of()); + + assertThatExceptionOfType(ConfigurationException.class) + .isThrownBy(cryptoProvider::install) + .withMessage("some.package.non.existing.ClassName is not on the class path! Check node's architecture " + + "(`uname -m`) is supported, see lib/<arch> subdirectories. The correct architecture-specific " + + "library for needs to be on the classpath."); + } + } + + @Test + public void testProviderInstallsJustOnce() throws Exception + { + Provider[] originalProviders = Security.getProviders(); + int originalProvidersCount = originalProviders.length; + Provider originalProvider = Security.getProviders()[0]; + + AbstractCryptoProvider provider = new DefaultCryptoProvider(new HashMap<>()); + provider.install(); + + assertEquals(provider.getProviderName(), Security.getProviders()[0].getName()); + assertEquals(originalProvidersCount + 1, Security.getProviders().length); + + // install one more time -> it will do nothing + + provider.install(); + + assertEquals(provider.getProviderName(), Security.getProviders()[0].getName()); + assertEquals(originalProvidersCount + 1, Security.getProviders().length); + + provider.uninstall(); + + assertEquals(originalProvider.getName(), Security.getProviders()[0].getName()); + assertEquals(originalProvidersCount, Security.getProviders().length); + } + + @Test + public void testInstallationOfIJREProvider() throws Exception + { + String originalProvider = Security.getProviders()[0].getName(); + + JREProvider jreProvider = new JREProvider(of()); + jreProvider.install(); + + assertEquals(originalProvider, Security.getProviders()[0].getName()); + } +} diff --git a/test/unit/org/apache/cassandra/security/InvalidCryptoProvider.java b/test/unit/org/apache/cassandra/security/InvalidCryptoProvider.java new file mode 100644 index 0000000000..47aae8ea92 --- /dev/null +++ b/test/unit/org/apache/cassandra/security/InvalidCryptoProvider.java @@ -0,0 +1,53 @@ +/* + * 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; + +public class InvalidCryptoProvider extends AbstractCryptoProvider +{ + public InvalidCryptoProvider(Map<String, String> properties) + { + super(properties); + } + + @Override + public String getProviderName() + { + return null; + } + + @Override + public String getProviderClassAsString() + { + return "some.package.non.existing.ClassName"; + } + + @Override + protected Runnable installator() + { + return () -> {}; + } + + @Override + protected boolean isHealthyInstallation() throws Exception + { + return false; + } +} diff --git a/test/unit/org/apache/cassandra/security/TestJREProvider.java b/test/unit/org/apache/cassandra/security/TestJREProvider.java new file mode 100644 index 0000000000..d6f700b965 --- /dev/null +++ b/test/unit/org/apache/cassandra/security/TestJREProvider.java @@ -0,0 +1,42 @@ +/* + * 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; + +// to be public because it is going to be instantiated by reflection in FBUtilties +public class TestJREProvider extends JREProvider +{ + public TestJREProvider(Map<String, String> properties) + { + super(properties); + } + + @Override + public String getProviderName() + { + return TestJREProvider.class.getSimpleName(); + } + + @Override + public String getProviderClassAsString() + { + return TestJREProvider.class.getName(); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org For additional commands, e-mail: commits-h...@cassandra.apache.org