content: /*
   * 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.exceptions.ConfigurationException;
  import org.apache.cassandra.io.util.File;
  import org.apache.cassandra.io.util.FileUtils;
  import org.apache.cassandra.utils.Clock;
  
  import static java.lang.String.format;
  
  /**
   * 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", null);
          outboundKeystoreContext = new FileBasedStoreContext("conf/.keystore", "cassandra", null);
          trustStoreContext = new FileBasedStoreContext("conf/.truststore", "cassandra", null);
      }
  
      public FileBasedSslContextFactory(Map<String, Object> parameters)
      {
          super(parameters);
          keystoreContext = new FileBasedStoreContext(getString("keystore"), getString("keystore_password"), getString("keystore_password_file"));
          outboundKeystoreContext = new FileBasedStoreContext(StringUtils.defaultString(getString("outbound_keystore"), keystoreContext.filePath),
                                                              StringUtils.defaultString(getString("outbound_keystore_password"), keystoreContext.password),
                                                              StringUtils.defaultString(getString("outbound_keystore_password_file"), keystoreContext.passwordFilePath));
          trustStoreContext = new FileBasedStoreContext(getString("truststore"), getString("truststore_password"), getString("truststore_password_file"));
      }
  
      @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 null
       */
      protected void validatePassword(boolean isOutboundKeystore, String password)
      {
          if (password == null)
          {
              String keyName = isOutboundKeystore ? "outbound_" : "";
              final String msg = 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);
              final char[] password = context.password.toCharArray();
              ks.load(ksf, password);
  
              if (!context.checkedExpiry)
              {
                  checkExpiredCerts(ks);
                  context.checkedExpiry = true;
              }
              kmf.init(ks, password);
              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 String passwordFilePath;
  
          public FileBasedStoreContext(String keystoreFilePath, String keystorePassword, String keystorePasswordFilePath)
          {
              this.filePath = keystoreFilePath;
              this.passwordFilePath = keystorePasswordFilePath;
              this.password = resolvePassword(keystoreFilePath, keystorePassword, keystorePasswordFilePath);
          }
  
          protected boolean hasKeystore()
          {
              return filePath != null && new File(filePath).exists();
          }
  
          protected boolean passwordMatchesIfPresent(String keyPassword)
          {
              return StringUtils.isEmpty(password) || keyPassword.equals(password);
          }
  
          private static String resolvePassword(String keystoreFilePath, String password, String passwordFilePath)
          {
              if (password != null)
                  return password;
  
              if (StringUtils.isEmpty(passwordFilePath))
                  return null;
  
              File keystorePasswordFile = new File(passwordFilePath);
  
              if (!keystorePasswordFile.exists())
              {
                  final String msg = format("keystore password file %s does not exist", keystorePasswordFile.path());
                  throw new ConfigurationException(msg);
              }
  
              try
              {
                  // we expect a password to be on the first line
                  List<String> lines = FileUtils.readLines(keystorePasswordFile);
                  if (lines.isEmpty())
                      return "";
  
                  return lines.get(0);
              }
              catch (RuntimeException e)
              {
                  throw new ConfigurationException(format("'Failed to read keystore password from the %s for %s",
                                                          keystorePasswordFile, keystoreFilePath),
                                                   e);
              }
          }
      }
  }
  