blob: e5aa4b10572d00f07940b6f4d9192f6f71813282 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cassandra.security;
import org.apache.cassandra.io.util.File;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 org.apache.cassandra.config.ParameterizedClass;
public class SSLFactoryTest
{
private static final Logger logger = LoggerFactory.getLogger(SSLFactoryTest.class);
static final SelfSignedCertificate ssc;
static
{
DatabaseDescriptor.daemonInitialization();
try
{
ssc = new SelfSignedCertificate();
}
catch (CertificateException e)
{
throw new RuntimeException("failed to create test certs");
}
}
private ServerEncryptionOptions encryptionOptions;
@Before
public void setup()
{
encryptionOptions = new ServerEncryptionOptions()
.withTrustStore("test/conf/cassandra_ssl_test.truststore")
.withTrustStorePassword("cassandra")
.withRequireClientAuth(false)
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA");
}
private ServerEncryptionOptions addKeystoreOptions(ServerEncryptionOptions options)
{
return options.withKeyStore("test/conf/cassandra_ssl_test.keystore")
.withKeyStorePassword("cassandra");
}
private ServerEncryptionOptions addPEMKeystoreOptions(ServerEncryptionOptions options)
{
ParameterizedClass sslContextFactoryClass = new ParameterizedClass("org.apache.cassandra.security.PEMBasedSslContextFactory",
new HashMap<>());
return options.withSslContextFactory(sslContextFactoryClass)
.withKeyStore("test/conf/cassandra_ssl_test.keystore.pem")
.withKeyStorePassword("cassandra")
.withTrustStore("test/conf/cassandra_ssl_test.truststore.pem");
}
@Test
public void testSslContextReload_HappyPath() throws IOException, InterruptedException
{
try
{
ServerEncryptionOptions options = addKeystoreOptions(encryptionOptions)
.withInternodeEncryption(ServerEncryptionOptions.InternodeEncryption.all);
SSLFactory.initHotReloading(options, options, true);
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
File keystoreFile = new File(options.keystore);
SSLFactory.checkCertFilesForHotReloading(options, options);
keystoreFile.trySetLastModified(System.currentTimeMillis() + 15000);
SSLFactory.checkCertFilesForHotReloading(options, options);
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
Assert.assertNotSame(oldCtx, newCtx);
}
catch (Exception e)
{
throw e;
}
finally
{
DatabaseDescriptor.loadConfig();
}
}
@Test
public void testPEMSslContextReload_HappyPath() throws IOException, InterruptedException
{
try
{
ServerEncryptionOptions options = addPEMKeystoreOptions(encryptionOptions)
.withInternodeEncryption(ServerEncryptionOptions.InternodeEncryption.all);
SSLFactory.initHotReloading(options, options, true);
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
File keystoreFile = new File(options.keystore);
SSLFactory.checkCertFilesForHotReloading(options, options);
keystoreFile.trySetLastModified(System.currentTimeMillis() + 15000);
SSLFactory.checkCertFilesForHotReloading(options, options);
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
Assert.assertNotSame(oldCtx, newCtx);
}
catch (Exception e)
{
throw e;
}
finally
{
DatabaseDescriptor.loadConfig();
}
}
@Test(expected = IOException.class)
public void testSslFactorySslInit_BadPassword_ThrowsException() throws IOException
{
ServerEncryptionOptions options = addKeystoreOptions(encryptionOptions)
.withKeyStorePassword("bad password")
.withInternodeEncryption(ServerEncryptionOptions.InternodeEncryption.all);
SSLFactory.validateSslContext("testSslFactorySslInit_BadPassword_ThrowsException", options, false, true);
}
@Test
public void testSslFactoryHotReload_BadPassword_DoesNotClearExistingSslContext() throws IOException
{
try
{
ServerEncryptionOptions options = addKeystoreOptions(encryptionOptions);
SSLFactory.initHotReloading(options, options, true);
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
File keystoreFile = new File(options.keystore);
SSLFactory.checkCertFilesForHotReloading(options, options);
keystoreFile.trySetLastModified(System.currentTimeMillis() + 5000);
ServerEncryptionOptions modOptions = new ServerEncryptionOptions(options)
.withKeyStorePassword("bad password");
SSLFactory.checkCertFilesForHotReloading(modOptions, modOptions);
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
Assert.assertSame(oldCtx, newCtx);
}
finally
{
DatabaseDescriptor.loadConfig();
}
}
@Test
public void testSslFactoryHotReload_CorruptOrNonExistentFile_DoesNotClearExistingSslContext() throws IOException
{
try
{
ServerEncryptionOptions options = addKeystoreOptions(encryptionOptions);
File testKeystoreFile = new File(options.keystore + ".test");
FileUtils.copyFile(new File(options.keystore).toJavaIOFile(), testKeystoreFile.toJavaIOFile());
options = options.withKeyStore(testKeystoreFile.path());
SSLFactory.initHotReloading(options, options, true);
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
SSLFactory.checkCertFilesForHotReloading(options, options);
testKeystoreFile.trySetLastModified(System.currentTimeMillis() + 15000);
FileUtils.forceDelete(testKeystoreFile.toJavaIOFile());
SSLFactory.checkCertFilesForHotReloading(options, options);
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
Assert.assertSame(oldCtx, newCtx);
}
catch (Exception e)
{
throw e;
}
finally
{
DatabaseDescriptor.loadConfig();
FileUtils.deleteQuietly(new File(encryptionOptions.keystore + ".test").toJavaIOFile());
}
}
@Test
public void getSslContext_ParamChanges() throws IOException
{
EncryptionOptions options = addKeystoreOptions(encryptionOptions)
.withEnabled(true)
.withCipherSuites("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
SslContext ctx1 = SSLFactory.getOrCreateSslContext(options, true,
ISslContextFactory.SocketType.SERVER);
Assert.assertTrue(ctx1.isServer());
Assert.assertEquals(ctx1.cipherSuites(), options.cipher_suites);
options = options.withCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
SslContext ctx2 = SSLFactory.getOrCreateSslContext(options, true,
ISslContextFactory.SocketType.CLIENT);
Assert.assertTrue(ctx2.isClient());
Assert.assertEquals(ctx2.cipherSuites(), options.cipher_suites);
}
@Test
public void testCacheKeyEqualityForCustomSslContextFactory() {
Map<String,String> parameters1 = new HashMap<>();
parameters1.put("key1", "value1");
parameters1.put("key2", "value2");
EncryptionOptions encryptionOptions1 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
.withProtocol("TLSv1.1")
.withRequireClientAuth(true)
.withRequireEndpointVerification(false);
SSLFactory.CacheKey cacheKey1 = new SSLFactory.CacheKey(encryptionOptions1, ISslContextFactory.SocketType.SERVER
);
Map<String,String> parameters2 = new HashMap<>();
parameters2.put("key1", "value1");
parameters2.put("key2", "value2");
EncryptionOptions encryptionOptions2 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
.withProtocol("TLSv1.1")
.withRequireClientAuth(true)
.withRequireEndpointVerification(false);
SSLFactory.CacheKey cacheKey2 = new SSLFactory.CacheKey(encryptionOptions2, ISslContextFactory.SocketType.SERVER
);
Assert.assertEquals(cacheKey1, cacheKey2);
}
@Test
public void testCacheKeyInequalityForCustomSslContextFactory() {
Map<String,String> parameters1 = new HashMap<>();
parameters1.put("key1", "value11");
parameters1.put("key2", "value12");
EncryptionOptions encryptionOptions1 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
.withProtocol("TLSv1.1");
SSLFactory.CacheKey cacheKey1 = new SSLFactory.CacheKey(encryptionOptions1, ISslContextFactory.SocketType.SERVER
);
Map<String,String> parameters2 = new HashMap<>();
parameters2.put("key1", "value21");
parameters2.put("key2", "value22");
EncryptionOptions encryptionOptions2 =
new EncryptionOptions()
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
.withProtocol("TLSv1.1");
SSLFactory.CacheKey cacheKey2 = new SSLFactory.CacheKey(encryptionOptions2, ISslContextFactory.SocketType.SERVER
);
Assert.assertNotEquals(cacheKey1, cacheKey2);
}
}