blob: d62aef5a1ce06dd06c333d60e4c8c8c44f82ca9f [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.IOException;
import java.nio.file.Files;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.ArrayList;
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;
/**
* SslContextFactory for the <a href="">PEM standard</a> encoded PKCS#8 private keys and X509 certificates/public-keys.
* It parses the key material based on the standard defined in the <a href="https://datatracker.ietf.org/doc/html/rfc7468">RFC 7468</a>.
* It creates <a href="https://datatracker.ietf.org/doc/html/rfc5208">PKCS# 8</a> based private key and X509 certificate(s)
* for the public key to build the required keystore and the truststore managers that are used for the SSL context creation.
* Internally it builds Java {@link KeyStore} with <a href="https://datatracker.ietf.org/doc/html/rfc7292">PKCS# 12</a> <a href="https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#keystore-types">store type</a>
* to be used for keystore and the truststore managers.
* <p>
* This factory also supports 'hot reloading' of the key material, the same way as defined by {@link FileBasedSslContextFactory},
* <b>if it is file based</b>. This factory ignores the existing 'store_type' configuration used for other file based store
* types like JKS.
* <p>
* You can configure this factory with either inline PEM data or with the files having the required PEM data as shown
* below,
*
* <b>Configuration: PEM keys/certs defined inline (mind the spaces in the YAML!)</b>
* <pre>
* client/server_encryption_options:
* ssl_context_factory:
* class_name: org.apache.cassandra.security.PEMBasedSslContextFactory
* parameters:
* private_key: |
* -----BEGIN ENCRYPTED PRIVATE KEY----- OR -----BEGIN PRIVATE KEY-----
* <your base64 encoded private key>
* -----END ENCRYPTED PRIVATE KEY----- OR -----END PRIVATE KEY-----
* -----BEGIN CERTIFICATE-----
* <your base64 encoded certificate chain>
* -----END CERTIFICATE-----
*
* private_key_password: "<your password if the private key is encrypted with a password>"
*
* trusted_certificates: |
* -----BEGIN CERTIFICATE-----
* <your base64 encoded certificate>
* -----END CERTIFICATE-----
* </pre>
*
* <b>Configuration: PEM keys/certs defined in files</b>
* <pre>
* client/server_encryption_options:
* ssl_context_factory:
* class_name: org.apache.cassandra.security.PEMBasedSslContextFactory
* keystore: <file path to the keystore file in the PEM format with the private key and the certificate chain>
* keystore_password: "<your password if the private key is encrypted with a password>"
* truststore: <file path to the truststore file in the PEM format>
* </pre>
*/
public final class PEMBasedSslContextFactory extends FileBasedSslContextFactory
{
public static final String DEFAULT_TARGET_STORETYPE = "PKCS12";
private static final Logger logger = LoggerFactory.getLogger(PEMBasedSslContextFactory.class);
private String pemEncodedKey;
private String keyPassword;
private String pemEncodedCertificates;
private boolean maybeFileBasedPrivateKey;
private boolean maybeFileBasedTrustedCertificates;
public PEMBasedSslContextFactory()
{
}
public PEMBasedSslContextFactory(Map<String, Object> parameters)
{
super(parameters);
pemEncodedKey = getString(ConfigKey.ENCODED_KEY.getKeyName());
keyPassword = getString(ConfigKey.KEY_PASSWORD.getKeyName());
if (StringUtils.isEmpty(keyPassword))
{
keyPassword = keystore_password;
}
else if (!StringUtils.isEmpty(keystore_password) && !keyPassword.equals(keystore_password))
{
throw new IllegalArgumentException("'keystore_password' and 'key_password' both configurations are given and the " +
"values do not match");
}
if (!StringUtils.isEmpty(truststore_password))
{
logger.warn("PEM based truststore should not be using password. Ignoring the given value in " +
"'truststore_password' configuration.");
}
pemEncodedCertificates = getString(ConfigKey.ENCODED_CERTIFICATES.getKeyName());
maybeFileBasedPrivateKey = StringUtils.isEmpty(pemEncodedKey);
maybeFileBasedTrustedCertificates = StringUtils.isEmpty(pemEncodedCertificates);
enforceSinglePrivateKeySource();
enforceSingleTurstedCertificatesSource();
}
/**
* Decides if this factory has a keystore defined - key material specified in files or inline to the configuration.
*
* @return {@code true} if there is a keystore defined; {@code false} otherwise
*/
@Override
public boolean hasKeystore()
{
return maybeFileBasedPrivateKey ? keystoreFileExists() :
!StringUtils.isEmpty(pemEncodedKey);
}
/**
* Checks if the keystore file exists.
*
* @return {@code true} if keystore file exists; {@code false} otherwise
*/
private boolean keystoreFileExists()
{
return keystore != null && new File(keystore).exists();
}
/**
* Decides if this factory has a truststore defined - key material specified in files or inline to the
* configuration.
*
* @return {@code true} if there is a truststore defined; {@code false} otherwise
*/
private boolean hasTruststore()
{
return maybeFileBasedTrustedCertificates ? truststoreFileExists() :
!StringUtils.isEmpty(pemEncodedCertificates);
}
/**
* Checks if the truststore file exists.
*
* @return {@code true} if truststore file exists; {@code false} otherwise
*/
private boolean truststoreFileExists()
{
return truststore != null && new File(truststore).exists();
}
/**
* This enables 'hot' reloading of the key/trust stores based on the last updated timestamps if they are file based.
*/
@Override
public synchronized void initHotReloading()
{
List<HotReloadableFile> fileList = new ArrayList<>();
if (maybeFileBasedPrivateKey && hasKeystore())
{
fileList.add(new HotReloadableFile(keystore));
}
if (maybeFileBasedTrustedCertificates && hasTruststore())
{
fileList.add(new HotReloadableFile(truststore));
}
if (!fileList.isEmpty())
{
hotReloadableFiles = fileList;
}
}
/**
* Builds required KeyManagerFactory from the PEM 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 PEM based keystore.
* @throws SSLException if any issues encountered during the build process
*/
@Override
protected KeyManagerFactory buildKeyManagerFactory() throws SSLException
{
try
{
if (hasKeystore())
{
if (maybeFileBasedPrivateKey)
{
pemEncodedKey = readPEMFile(keystore); // read PEM from the file
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
algorithm == null ? KeyManagerFactory.getDefaultAlgorithm() : algorithm);
KeyStore ks = buildKeyStore();
if (!checkedExpiry)
{
checkExpiredCerts(ks);
checkedExpiry = true;
}
kmf.init(ks, keyPassword != null ? keyPassword.toCharArray() : null);
return kmf;
}
else
{
throw new SSLException("Must provide keystore or private_key in configuration for PEMBasedSSlContextFactory");
}
}
catch (Exception e)
{
throw new SSLException("Failed to build key manager store for secure connections", e);
}
}
/**
* Builds TrustManagerFactory from the PEM based truststore.
*
* @return TrustManagerFactory from the PEM based truststore
* @throws SSLException if any issues encountered during the build process
*/
@Override
protected TrustManagerFactory buildTrustManagerFactory() throws SSLException
{
try
{
if (hasTruststore())
{
if (maybeFileBasedTrustedCertificates)
{
pemEncodedCertificates = readPEMFile(truststore); // read PEM from the file
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
algorithm == null ? TrustManagerFactory.getDefaultAlgorithm() : algorithm);
KeyStore ts = buildTrustStore();
tmf.init(ts);
return tmf;
}
else
{
throw new SSLException("Must provide truststore or trusted_certificates in configuration for " +
"PEMBasedSSlContextFactory");
}
}
catch (Exception e)
{
throw new SSLException("Failed to build trust manager store for secure connections", e);
}
}
private String readPEMFile(String file) throws IOException
{
return new String(Files.readAllBytes(File.getPath(file)));
}
/**
* Builds KeyStore object given the {@link #DEFAULT_TARGET_STORETYPE} out of the PEM formatted private key material.
* It uses {@code cassandra-ssl-keystore} as the alias for the created key-entry.
*/
private KeyStore buildKeyStore() throws GeneralSecurityException, IOException
{
char[] keyPasswordArray = keyPassword != null ? keyPassword.toCharArray() : null;
PrivateKey privateKey = PEMReader.extractPrivateKey(pemEncodedKey, keyPassword);
Certificate[] certChainArray = PEMReader.extractCertificates(pemEncodedKey);
if (certChainArray == null || certChainArray.length == 0)
{
throw new SSLException("Could not read any certificates for the certChain for the private key");
}
KeyStore keyStore = KeyStore.getInstance(DEFAULT_TARGET_STORETYPE);
keyStore.load(null, null);
keyStore.setKeyEntry("cassandra-ssl-keystore", privateKey, keyPasswordArray, certChainArray);
return keyStore;
}
/**
* Builds KeyStore object given the {@link #DEFAULT_TARGET_STORETYPE} out of the PEM formatted certificates/public-key
* material.
* <p>
* It uses {@code cassandra-ssl-trusted-cert-<numeric-id>} as the alias for the created certificate-entry.
*/
private KeyStore buildTrustStore() throws GeneralSecurityException, IOException
{
Certificate[] certChainArray = PEMReader.extractCertificates(pemEncodedCertificates);
if (certChainArray == null || certChainArray.length == 0)
{
throw new SSLException("Could not read any certificates from the given PEM");
}
KeyStore keyStore = KeyStore.getInstance(DEFAULT_TARGET_STORETYPE);
keyStore.load(null, null);
for (int i = 0; i < certChainArray.length; i++)
{
keyStore.setCertificateEntry("cassandra-ssl-trusted-cert-" + (i + 1), certChainArray[i]);
}
return keyStore;
}
/**
* Enforces that the configuration specified a sole source of loading private keys - either {@code keystore} (the
* actual file must exist) or {@code private_key}, not both.
*/
private void enforceSinglePrivateKeySource()
{
if (keystoreFileExists() && !StringUtils.isEmpty(pemEncodedKey))
{
throw new IllegalArgumentException("Configuration must specify value for either keystore or private_key, " +
"not both for PEMBasedSSlContextFactory");
}
}
/**
* Enforces that the configuration specified a sole source of loading trusted certificates - either {@code
* truststore} (actual file must exist) or {@code trusted_certificates}, not both.
*/
private void enforceSingleTurstedCertificatesSource()
{
if (truststoreFileExists() && !StringUtils.isEmpty(pemEncodedCertificates))
{
throw new IllegalArgumentException("Configuration must specify value for either truststore or " +
"trusted_certificates, not both for PEMBasedSSlContextFactory");
}
}
public enum ConfigKey
{
ENCODED_KEY("private_key"),
KEY_PASSWORD("private_key_password"),
ENCODED_CERTIFICATES("trusted_certificates");
final String keyName;
ConfigKey(String keyName)
{
this.keyName = keyName;
}
String getKeyName()
{
return keyName;
}
}
}