blob: fdf669697c02ce9524712884ccbccb1ce4454d70 [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 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!
*/
public abstract class FileBasedSslContextFactory extends AbstractSslContextFactory
{
private static final Logger logger = LoggerFactory.getLogger(FileBasedSslContextFactory.class);
protected FileBasedStoreContext keystoreContext;
protected FileBasedStoreContext outboundKeystoreContext;
protected FileBasedStoreContext trustStoreContext;
/**
* List of files that trigger hot reloading of SSL certificates
*/
protected volatile List<HotReloadableFile> hotReloadableFiles = new ArrayList<>();
public FileBasedSslContextFactory()
{
keystoreContext = new FileBasedStoreContext("conf/.keystore", "cassandra");
outboundKeystoreContext = new FileBasedStoreContext("conf/.keystore", "cassandra");
trustStoreContext = new FileBasedStoreContext("conf/.truststore", "cassandra");
}
public FileBasedSslContextFactory(Map<String, Object> parameters)
{
super(parameters);
keystoreContext = new FileBasedStoreContext(getString("keystore"), getString("keystore_password"));
outboundKeystoreContext = new FileBasedStoreContext(StringUtils.defaultString(getString("outbound_keystore"), keystoreContext.filePath),
StringUtils.defaultString(getString("outbound_keystore_password"), keystoreContext.password));
trustStoreContext = new FileBasedStoreContext(getString("truststore"), getString("truststore_password"));
}
@Override
public boolean shouldReload()
{
return hotReloadableFiles.stream().anyMatch(HotReloadableFile::shouldReload);
}
@Override
public boolean hasKeystore()
{
return keystoreContext.hasKeystore();
}
@Override
public boolean hasOutboundKeystore()
{
return outboundKeystoreContext.hasKeystore();
}
private boolean hasTruststore()
{
return trustStoreContext.filePath != null && new File(trustStoreContext.filePath).exists();
}
@Override
public synchronized void initHotReloading()
{
boolean hasKeystore = hasKeystore();
boolean hasOutboundKeystore = hasOutboundKeystore();
boolean hasTruststore = hasTruststore();
if (hasKeystore || hasOutboundKeystore || hasTruststore)
{
List<HotReloadableFile> fileList = new ArrayList<>();
if (hasKeystore)
{
fileList.add(new HotReloadableFile(keystoreContext.filePath));
}
if (hasOutboundKeystore)
{
fileList.add(new HotReloadableFile(outboundKeystoreContext.filePath));
}
if (hasTruststore)
{
fileList.add(new HotReloadableFile(trustStoreContext.filePath));
}
hotReloadableFiles = fileList;
}
}
/**
* Validates the given keystore password.
*
* @param isOutboundKeystore {@code true} for the {@code outbound_keystore_password};{@code false} otherwise
* @param password value
* @throws IllegalArgumentException if the {@code password} is empty as per the definition of {@link StringUtils#isEmpty(CharSequence)}
*/
protected void validatePassword(boolean isOutboundKeystore, String password)
{
boolean keystorePasswordEmpty = StringUtils.isEmpty(password);
if (keystorePasswordEmpty)
{
String keyName = isOutboundKeystore ? "outbound_" : "";
final String msg = String.format("'%skeystore_password' must be specified", keyName);
throw new IllegalArgumentException(msg);
}
}
/**
* 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
* @throws IllegalArgumentException if the validation for the {@code keystore_password} fails
* @see #validatePassword(boolean, String)
*/
@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(false, keystoreContext.password);
return getKeyManagerFactory(keystoreContext);
}
@Override
protected KeyManagerFactory buildOutboundKeyManagerFactory() throws SSLException
{
/*
* Validation of the password is delayed until this point to allow nullable keystore passwords
* for other use-cases (CASSANDRA-18124).
*/
validatePassword(true, outboundKeystoreContext.password);
return getKeyManagerFactory(outboundKeystoreContext);
}
/**
* 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(trustStoreContext.filePath)))
{
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(trustStoreContext.password) ? null : trustStoreContext.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);
}
}
private KeyManagerFactory getKeyManagerFactory(final FileBasedStoreContext context) throws SSLException
{
try (InputStream ksf = Files.newInputStream(File.getPath(context.filePath)))
{
final String algorithm = this.algorithm == null ? KeyManagerFactory.getDefaultAlgorithm() : this.algorithm;
KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm);
KeyStore ks = KeyStore.getInstance(store_type);
ks.load(ksf, context.password.toCharArray());
if (!context.checkedExpiry)
{
checkExpiredCerts(ks);
context.checkedExpiry = true;
}
kmf.init(ks, context.password.toCharArray());
return kmf;
}
catch (Exception e)
{
throw new SSLException("failed to build key 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 +
'}';
}
}
protected static class FileBasedStoreContext
{
public volatile boolean checkedExpiry = false;
public String filePath;
public String password;
public FileBasedStoreContext(String keystore, String keystorePassword)
{
this.filePath = keystore;
this.password = keystorePassword;
}
protected boolean hasKeystore()
{
return filePath != null && new File(filePath).exists();
}
protected boolean passwordMatchesIfPresent(String keyPassword)
{
return StringUtils.isEmpty(password) || keyPassword.equals(password);
}
}
}