All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cassandra.security.PEMBasedSslContextFactory Maven / Gradle / Ivy

Go to download

The Apache Cassandra Project develops a highly scalable second-generation distributed database, bringing together Dynamo's fully distributed design and Bigtable's ColumnFamily-based data model.

There is a newer version: 5.0.2
Show newest version
/*
 * 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 PEM standard encoded PKCS#8 private keys and X509 certificates/public-keys.
 * It parses the key material based on the standard defined in the RFC 7468.
 * It creates PKCS# 8 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 PKCS# 12 store type
 * to be used for keystore and the truststore managers.
 * 

* This factory also supports 'hot reloading' of the key material, the same way as defined by {@link FileBasedSslContextFactory}, * if it is file based. This factory ignores the existing 'store_type' configuration used for other file based store * types like JKS. *

* You can configure this factory with either inline PEM data or with the files having the required PEM data as shown * below, * * Configuration: PEM keys/certs defined inline (mind the spaces in the YAML!) *

 *     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-----
 *              
 *              -----END ENCRYPTED PRIVATE KEY----- OR -----END PRIVATE KEY-----
 *              -----BEGIN CERTIFICATE-----
 *              
 *              -----END CERTIFICATE-----
 *
 *             private_key_password: ""
 *
 *             trusted_certificates: |
 *               -----BEGIN CERTIFICATE-----
 *               
 *               -----END CERTIFICATE-----
 * 
* * Configuration: PEM keys/certs defined in files *
 *     client/server_encryption_options:
 *      ssl_context_factory:
 *         class_name: org.apache.cassandra.security.PEMBasedSslContextFactory
 *      keystore: 
 *      keystore_password: ""
 *      truststore: 
 * 
*/ public final class PEMBasedSslContextFactory extends FileBasedSslContextFactory { public static final String DEFAULT_TARGET_STORETYPE = "PKCS12"; private static final Logger logger = LoggerFactory.getLogger(PEMBasedSslContextFactory.class); private PEMBasedKeyStoreContext pemEncodedTrustCertificates; private PEMBasedKeyStoreContext pemEncodedKeyContext; private PEMBasedKeyStoreContext pemEncodedOutboundKeyContext; public PEMBasedSslContextFactory() { } private void validatePasswords() { boolean shouldThrow = !keystoreContext.passwordMatchesIfPresent(pemEncodedKeyContext.password) || !outboundKeystoreContext.passwordMatchesIfPresent(pemEncodedOutboundKeyContext.password); boolean outboundPasswordMismatch = !outboundKeystoreContext.passwordMatchesIfPresent(pemEncodedOutboundKeyContext.password); String keyName = outboundPasswordMismatch ? "outbound_" : ""; if (shouldThrow) { final String msg = String.format("'%skeystore_password' and '%skey_password' both configurations are given and the values do not match", keyName, keyName); throw new IllegalArgumentException(msg); } } public PEMBasedSslContextFactory(Map parameters) { super(parameters); final String pemEncodedKey = getString(ConfigKey.ENCODED_KEY.getKeyName()); final String pemEncodedKeyPassword = StringUtils.defaultString(getString(ConfigKey.KEY_PASSWORD.getKeyName()), keystoreContext.password); pemEncodedKeyContext = new PEMBasedKeyStoreContext(pemEncodedKey, pemEncodedKeyPassword, StringUtils.isEmpty(pemEncodedKey), keystoreContext); final String pemEncodedOutboundKey = StringUtils.defaultString(getString(ConfigKey.OUTBOUND_ENCODED_KEY.getKeyName()), pemEncodedKey); final String outboundKeyPassword = StringUtils.defaultString(StringUtils.defaultString(getString(ConfigKey.OUTBOUND_ENCODED_KEY_PASSWORD.getKeyName()), outboundKeystoreContext.password), pemEncodedKeyPassword); pemEncodedOutboundKeyContext = new PEMBasedKeyStoreContext(pemEncodedKey, outboundKeyPassword, StringUtils.isEmpty(pemEncodedOutboundKey), outboundKeystoreContext); validatePasswords(); if (!StringUtils.isEmpty(trustStoreContext.password)) { logger.warn("PEM based truststore should not be using password. Ignoring the given value in " + "'truststore_password' configuration."); } final String pemEncodedCerts = getString(ConfigKey.ENCODED_CERTIFICATES.getKeyName()); pemEncodedTrustCertificates = new PEMBasedKeyStoreContext(pemEncodedCerts, null, StringUtils.isEmpty(pemEncodedCerts), trustStoreContext); 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 pemEncodedKeyContext.maybeFilebasedKey ? keystoreContext.hasKeystore() : !StringUtils.isEmpty(pemEncodedKeyContext.key); } /** * Decides if this factory has an outbound keystore defined - key material specified in files or inline to the configuration. * * @return {@code true} if there is an outbound keystore defined; {@code false} otherwise */ @Override public boolean hasOutboundKeystore() { return pemEncodedOutboundKeyContext.maybeFilebasedKey ? outboundKeystoreContext.hasKeystore() : !StringUtils.isEmpty(pemEncodedOutboundKeyContext.key); } /** * 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 pemEncodedTrustCertificates.maybeFilebasedKey ? truststoreFileExists() : !StringUtils.isEmpty(pemEncodedTrustCertificates.key); } /** * Checks if the truststore file exists. * * @return {@code true} if truststore file exists; {@code false} otherwise */ private boolean truststoreFileExists() { return trustStoreContext.filePath != null && new File(trustStoreContext.filePath).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 fileList = new ArrayList<>(); if (pemEncodedKeyContext.maybeFilebasedKey && hasKeystore()) { fileList.add(new HotReloadableFile(keystoreContext.filePath)); } if (pemEncodedOutboundKeyContext.maybeFilebasedKey && hasOutboundKeystore()) { fileList.add(new HotReloadableFile(outboundKeystoreContext.filePath)); } if (pemEncodedTrustCertificates.maybeFilebasedKey && hasTruststore()) { fileList.add(new HotReloadableFile(trustStoreContext.filePath)); } 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 { return buildKeyManagerFactory(pemEncodedKeyContext, keystoreContext); } @Override protected KeyManagerFactory buildOutboundKeyManagerFactory() throws SSLException { return buildKeyManagerFactory(pemEncodedOutboundKeyContext, outboundKeystoreContext); } private KeyManagerFactory buildKeyManagerFactory(PEMBasedKeyStoreContext pemBasedKeyStoreContext, FileBasedStoreContext keyStoreContext) throws SSLException { try { if (pemBasedKeyStoreContext.hasKey()) { if (pemBasedKeyStoreContext.maybeFilebasedKey) { pemBasedKeyStoreContext.key = readPEMFile(keyStoreContext.filePath); // read PEM from the file } KeyManagerFactory kmf = KeyManagerFactory.getInstance( algorithm == null ? KeyManagerFactory.getDefaultAlgorithm() : algorithm); KeyStore ks = buildKeyStore(pemBasedKeyStoreContext.key, pemBasedKeyStoreContext.password); if (!keyStoreContext.checkedExpiry) { checkExpiredCerts(ks); keyStoreContext.checkedExpiry = true; } kmf.init(ks, pemBasedKeyStoreContext.password != null ? pemBasedKeyStoreContext.password.toCharArray() : null); return kmf; } else { throw new SSLException("Must provide outbound_keystore or outbound_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 (pemEncodedTrustCertificates.maybeFilebasedKey) { pemEncodedTrustCertificates.key = readPEMFile(trustStoreContext.filePath); // 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 static KeyStore buildKeyStore(final String pemEncodedKey, final String keyPassword) 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. *

* It uses {@code cassandra-ssl-trusted-cert-} as the alias for the created certificate-entry. */ private KeyStore buildTrustStore() throws GeneralSecurityException, IOException { Certificate[] certChainArray = PEMReader.extractCertificates(pemEncodedTrustCertificates.key); 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 (keystoreContext.hasKeystore() && !StringUtils.isEmpty(pemEncodedKeyContext.key)) { throw new IllegalArgumentException("Configuration must specify value for either keystore or private_key, " + "not both for PEMBasedSSlContextFactory"); } if (outboundKeystoreContext.hasKeystore() && !StringUtils.isEmpty(pemEncodedOutboundKeyContext.key)) { throw new IllegalArgumentException("Configuration must specify value for either outbound_keystore or outbound_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(pemEncodedTrustCertificates.key)) { throw new IllegalArgumentException("Configuration must specify value for either truststore or " + "trusted_certificates, not both for PEMBasedSSlContextFactory"); } } public static class PEMBasedKeyStoreContext { public String key; public final String password; public final boolean maybeFilebasedKey; public final FileBasedStoreContext filebasedKeystoreContext; public PEMBasedKeyStoreContext(final String encodedKey, final String getEncodedKeyPassword, final boolean maybeFilebasedKey, final FileBasedStoreContext filebasedKeystoreContext) { this.key = encodedKey; this.password = getEncodedKeyPassword; this.maybeFilebasedKey = maybeFilebasedKey; this.filebasedKeystoreContext = filebasedKeystoreContext; } public boolean hasKey() { return maybeFilebasedKey ? filebasedKeystoreContext.hasKeystore() : !StringUtils.isEmpty(key); } } public enum ConfigKey { ENCODED_KEY("private_key"), KEY_PASSWORD("private_key_password"), OUTBOUND_ENCODED_KEY("outbound_private_key"), OUTBOUND_ENCODED_KEY_PASSWORD("outbound_private_key_password"), ENCODED_CERTIFICATES("trusted_certificates"); final String keyName; ConfigKey(String keyName) { this.keyName = keyName; } String getKeyName() { return keyName; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy