| /* |
| * 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); |
| } |
| } |
| } |