blob: 9876eb4b8938d2fe16297789ad82dfe6ded87a2b [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 java.io.InputStream;
import java.nio.file.Files;
import java.security.KeyStore;
import java.security.KeyStoreException;
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.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.io.util.File;
import org.apache.cassandra.utils.Clock;
/**
* Abstract implementation for {@link ISslContextFactory} using file based, standard keystore format with the ability
* to hot-reload the files upon file changes (detected by the {@code last modified timestamp}).
* <p>
* {@code CAUTION:} While this is a useful abstraction, please be careful if you need to modify this class
* given possible custom implementations out there!
*/
abstract public class FileBasedSslContextFactory extends AbstractSslContextFactory
{
private static final Logger logger = LoggerFactory.getLogger(FileBasedSslContextFactory.class);
@VisibleForTesting
protected volatile boolean checkedExpiry = false;
/**
* List of files that trigger hot reloading of SSL certificates
*/
protected volatile List<HotReloadableFile> hotReloadableFiles = new ArrayList<>();
protected String keystore;
protected String keystore_password;
protected String truststore;
protected String truststore_password;
public FileBasedSslContextFactory()
{
keystore = "conf/.keystore";
keystore_password = "cassandra";
truststore = "conf/.truststore";
truststore_password = "cassandra";
}
public FileBasedSslContextFactory(Map<String, Object> parameters)
{
super(parameters);
keystore = getString("keystore");
keystore_password = getString("keystore_password");
truststore = getString("truststore");
truststore_password = getString("truststore_password");
}
@Override
public boolean shouldReload()
{
return hotReloadableFiles.stream().anyMatch(HotReloadableFile::shouldReload);
}
@Override
public boolean hasKeystore()
{
return keystore != null && new File(keystore).exists();
}
private boolean hasTruststore()
{
return truststore != null && new File(truststore).exists();
}
@Override
public synchronized void initHotReloading()
{
boolean hasKeystore = hasKeystore();
boolean hasTruststore = hasTruststore();
if (hasKeystore || hasTruststore)
{
List<HotReloadableFile> fileList = new ArrayList<>();
if (hasKeystore)
{
fileList.add(new HotReloadableFile(keystore));
}
if (hasTruststore)
{
fileList.add(new HotReloadableFile(truststore));
}
hotReloadableFiles = fileList;
}
}
/**
* Validates the given keystore password.
*
* @param password value
* @throws IllegalArgumentException if the {@code password} is empty as per the definition of {@link StringUtils#isEmpty(CharSequence)}
*/
protected void validatePassword(String password)
{
boolean keystorePasswordEmpty = StringUtils.isEmpty(password);
if (keystorePasswordEmpty)
{
throw new IllegalArgumentException("'keystore_password' must be specified");
}
}
/**
* 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
*/
@Override
protected KeyManagerFactory buildKeyManagerFactory() throws SSLException
{
/*
* Validation of the password is delayed until this point to allow nullable keystore passwords
* for other use-cases (CASSANDRA-18124).
*/
validatePassword(keystore_password);
try (InputStream ksf = Files.newInputStream(File.getPath(keystore)))
{
final String algorithm = this.algorithm == null ? KeyManagerFactory.getDefaultAlgorithm() : this.algorithm;
KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
KeyStore ks = KeyStore.getInstance(store_type);
ks.load(ksf, keystore_password.toCharArray());
if (!checkedExpiry)
{
checkExpiredCerts(ks);
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
*/
@Override
protected TrustManagerFactory buildTrustManagerFactory() throws SSLException
{
try (InputStream tsf = Files.newInputStream(File.getPath(truststore)))
{
final String algorithm = this.algorithm == null ? TrustManagerFactory.getDefaultAlgorithm() : this.algorithm;
TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm);
KeyStore ts = KeyStore.getInstance(store_type);
final char[] truststorePassword = StringUtils.isEmpty(truststore_password) ? null : truststore_password.toCharArray();
ts.load(tsf, truststorePassword);
tmf.init(ts);
return tmf;
}
catch (Exception e)
{
throw new SSLException("failed to build trust manager store for secure connections", e);
}
}
protected boolean checkExpiredCerts(KeyStore ks) throws KeyStoreException
{
boolean hasExpiredCerts = false;
final Date now = new Date(Clock.Global.currentTimeMillis());
for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements(); )
{
String alias = aliases.nextElement();
if (ks.getCertificate(alias).getType().equals("X.509"))
{
Date expires = ((X509Certificate) ks.getCertificate(alias)).getNotAfter();
if (expires.before(now))
{
hasExpiredCerts = true;
logger.warn("Certificate for {} expired on {}", alias, expires);
}
}
}
return hasExpiredCerts;
}
/**
* Helper class for hot reloading SSL Contexts
*/
protected 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 +
'}';
}
}
}