package com.cloudhopper.commons.ssl;
/*
* #%L
* ch-commons-ssl
* %%
* Copyright (C) 2014 Cloudhopper by Twitter
* %%
* Licensed 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.
* #L%
*/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.security.InvalidParameterException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CRL;
import java.security.cert.CertStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.X509CertSelector;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.net.ssl.CertPathTrustManagerParameters;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Factory for SSLContext. This is used to create an SSLContext that is used by
* SMPP clients and servers that are configured to use SSL. This class is modeled
* after the netty SecureChatSslContextFactory example, the jetty SslContextFactory
* utility, and the contribution of bbanko to cloudhopper-smpp.
*
* @author garth, bbanko, Jetty7
*/
public class SslContextFactory {
private static final Logger logger = LoggerFactory.getLogger(SslContextFactory.class);
private SSLContext sslContext;
private InputStream keyStoreInputStream;
private InputStream trustStoreInputStream;
private final SslConfiguration sslConfig;
public SslContextFactory() throws Exception {
this(new SslConfiguration());
}
public SslContextFactory(SslConfiguration sslConfig) throws Exception {
this.sslConfig = sslConfig;
init();
}
/**
* Create the SSLContext
*/
private void init() throws Exception {
if (sslContext == null) {
if (keyStoreInputStream == null && sslConfig.getKeyStorePath() == null &&
trustStoreInputStream == null && sslConfig.getTrustStorePath() == null) {
TrustManager[] trust_managers = null;
if (sslConfig.isTrustAll()) {
logger.debug("No keystore or trust store configured. ACCEPTING UNTRUSTED CERTIFICATES!!!!!");
// Create a trust manager that does not validate certificate chains
TrustManager trustAllCerts = new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
};
trust_managers = new TrustManager[] { trustAllCerts };
}
SecureRandom secureRandom = (sslConfig.getSecureRandomAlgorithm() == null)?null:
SecureRandom.getInstance(sslConfig.getSecureRandomAlgorithm());
sslContext = SSLContext.getInstance(sslConfig.getProtocol());
sslContext.init(null, trust_managers, secureRandom);
} else {
// verify that keystore and truststore
// parameters are set up correctly
checkKeyStore();
KeyStore keyStore = loadKeyStore();
KeyStore trustStore = loadTrustStore();
Collection<? extends CRL> crls = loadCRL(sslConfig.getCrlPath());
if (sslConfig.isValidateCerts() && keyStore != null) {
if (sslConfig.getCertAlias() == null) {
List<String> aliases = Collections.list(keyStore.aliases());
sslConfig.setCertAlias(aliases.size() == 1 ? aliases.get(0) : null);
}
Certificate cert = sslConfig.getCertAlias() == null?null:
keyStore.getCertificate(sslConfig.getCertAlias());
if (cert == null) {
throw new Exception("No certificate found in the keystore" + (sslConfig.getCertAlias() == null ? "":" for alias " + sslConfig.getCertAlias()));
}
CertificateValidator validator = new CertificateValidator(trustStore, crls);
validator.setMaxCertPathLength(sslConfig.getMaxCertPathLength());
validator.setEnableCRLDP(sslConfig.isEnableCRLDP());
validator.setEnableOCSP(sslConfig.isEnableOCSP());
validator.setOcspResponderURL(sslConfig.getOcspResponderURL());
validator.validate(keyStore, cert);
}
KeyManager[] keyManagers = getKeyManagers(keyStore);
TrustManager[] trustManagers = getTrustManagers(trustStore, crls);
SecureRandom secureRandom = (sslConfig.getSecureRandomAlgorithm() == null)?null:
SecureRandom.getInstance(sslConfig.getSecureRandomAlgorithm());
sslContext = (sslConfig.getProvider() == null)?
SSLContext.getInstance(sslConfig.getProtocol()):
SSLContext.getInstance(sslConfig.getProtocol(), sslConfig.getProvider());
sslContext.init(keyManagers, trustManagers, secureRandom);
SSLEngine engine = newSslEngine();
logger.info("Enabled Protocols {} of {}",
Arrays.asList(engine.getEnabledProtocols()),
Arrays.asList(engine.getSupportedProtocols()));
logger.debug("Enabled Ciphers {} of {}",
Arrays.asList(engine.getEnabledCipherSuites()),
Arrays.asList(engine.getSupportedCipherSuites()));
}
}
}
/**
* Get the underlying SSLContext.
*/
public SSLContext getSslContext() {
return sslContext;
}
/**
* Override this method to provide alternate way to load a keystore.
*
* @return the key store instance
* @throws Exception
*/
protected KeyStore loadKeyStore() throws Exception {
return getKeyStore(keyStoreInputStream, sslConfig.getKeyStorePath(),
sslConfig.getKeyStoreType(), sslConfig.getKeyStoreProvider(),
sslConfig.getKeyStorePassword());
}
/**
* Override this method to provide alternate way to load a truststore.
*
* @return the key store instance
* @throws Exception
*/
protected KeyStore loadTrustStore() throws Exception {
return getKeyStore(trustStoreInputStream, sslConfig.getTrustStorePath(),
sslConfig.getTrustStoreType(), sslConfig.getTrustStoreProvider(),
sslConfig.getTrustStorePassword());
}
/**
* Loads certificate revocation list (CRL) from a file.
*
* Required for integrations to be able to override the mechanism used to
* load CRL in order to provide their own implementation.
*
* @param crlPath path of certificate revocation list file
* @return Collection of CRL's
* @throws Exception
*/
protected Collection<? extends CRL> loadCRL(String crlPath) throws Exception {
Collection<? extends CRL> crlList = null;
if (crlPath != null) {
InputStream in = null;
try {
in = new FileInputStream(crlPath); //assume it's a file
crlList = CertificateFactory.getInstance("X.509").generateCRLs(in);
} finally {
if (in != null) {
in.close();
}
}
}
return crlList;
}
/**
* Loads keystore using an input stream or a file path in the same
* order of precedence.
*
* Required for integrations to be able to override the mechanism
* used to load a keystore in order to provide their own implementation.
*
* @param storeStream keystore input stream
* @param storePath path of keystore file
* @param storeType keystore type
* @param storeProvider keystore provider
* @param storePassword keystore password
* @return created keystore
* @throws Exception if the keystore cannot be obtained
*
*/
protected KeyStore getKeyStore(InputStream storeStream, String storePath, String storeType, String storeProvider, String storePassword) throws Exception {
KeyStore keystore = null;
if (storeStream != null || storePath != null) {
InputStream inStream = storeStream;
try {
if (inStream == null) {
inStream = new FileInputStream(storePath); //assume it's a file
}
if (storeProvider != null) {
keystore = KeyStore.getInstance(storeType, storeProvider);
} else {
keystore = KeyStore.getInstance(storeType);
}
keystore.load(inStream, storePassword == null ? null : storePassword.toCharArray());
} finally {
if (inStream != null) {
inStream.close();
}
}
}
return keystore;
}
protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception {
KeyManager[] managers = null;
if (keyStore != null) {
KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(sslConfig.getKeyManagerFactoryAlgorithm());
keyManagerFactory.init(keyStore, sslConfig.getKeyManagerPassword() == null?
(sslConfig.getKeyStorePassword() == null?null:
sslConfig.getKeyStorePassword().toCharArray()):
sslConfig.getKeyManagerPassword().toCharArray());
managers = keyManagerFactory.getKeyManagers();
if (sslConfig.getCertAlias() != null) {
for (int idx = 0; idx < managers.length; idx++) {
if (managers[idx] instanceof X509KeyManager) {
managers[idx] = new AliasedX509ExtendedKeyManager(sslConfig.getCertAlias(),
(X509KeyManager)managers[idx]);
}
}
}
}
return managers;
}
protected TrustManager[] getTrustManagers(KeyStore trustStore, Collection<? extends CRL> crls) throws Exception {
TrustManager[] managers = null;
if (trustStore != null) {
// Revocation checking is only supported for PKIX algorithm
if (sslConfig.isValidatePeerCerts() &&
sslConfig.getTrustManagerFactoryAlgorithm().equalsIgnoreCase("PKIX")) {
PKIXBuilderParameters pbParams = new PKIXBuilderParameters(trustStore, new X509CertSelector());
// Set maximum certification path length
pbParams.setMaxPathLength(sslConfig.getMaxCertPathLength());
// Make sure revocation checking is enabled
pbParams.setRevocationEnabled(true);
if (crls != null && !crls.isEmpty()) {
pbParams.addCertStore(CertStore.getInstance("Collection",
new CollectionCertStoreParameters(crls)));
}
if (sslConfig.isEnableCRLDP()) {
// Enable Certificate Revocation List Distribution Points (CRLDP) support
System.setProperty("com.sun.security.enableCRLDP","true");
}
if (sslConfig.isEnableOCSP()) {
// Enable On-Line Certificate Status Protocol (OCSP) support
Security.setProperty("ocsp.enable","true");
if (sslConfig.getOcspResponderURL() != null) {
// Override location of OCSP Responder
Security.setProperty("ocsp.responderURL", sslConfig.getOcspResponderURL());
}
}
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(sslConfig.getTrustManagerFactoryAlgorithm());
trustManagerFactory.init(new CertPathTrustManagerParameters(pbParams));
managers = trustManagerFactory.getTrustManagers();
} else {
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(sslConfig.getTrustManagerFactoryAlgorithm());
trustManagerFactory.init(trustStore);
managers = trustManagerFactory.getTrustManagers();
}
}
return managers;
}
/**
* Check KeyStore Configuration. Ensures that if keystore has been
* configured but there's no truststore, that keystore is
* used as truststore.
* @throws IllegalStateException if SslContextFactory configuration can't be used.
*/
public void checkKeyStore() {
if (sslContext != null)
return; //nothing to check if using preconfigured context
if (keyStoreInputStream == null &&
sslConfig.getKeyStorePath() == null) {
throw new IllegalStateException("SSL doesn't have a valid keystore");
}
// if the keystore has been configured but there is no
// truststore configured, use the keystore as the truststore
if (trustStoreInputStream == null && sslConfig.getTrustStorePath() == null) {
trustStoreInputStream = keyStoreInputStream;
sslConfig.setTrustStorePath(sslConfig.getKeyStorePath());
sslConfig.setTrustStoreType(sslConfig.getKeyStoreType());
sslConfig.setTrustStoreProvider(sslConfig.getKeyStoreProvider());
sslConfig.setTrustStorePassword(sslConfig.getKeyStorePassword());
sslConfig.setTrustManagerFactoryAlgorithm(sslConfig.getKeyManagerFactoryAlgorithm());
}
// It's the same stream we cannot read it twice, so read it once in memory
if (keyStoreInputStream != null && keyStoreInputStream == trustStoreInputStream) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
streamCopy(keyStoreInputStream, baos, null, false);
keyStoreInputStream.close();
keyStoreInputStream = new ByteArrayInputStream(baos.toByteArray());
trustStoreInputStream = new ByteArrayInputStream(baos.toByteArray());
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
/**
* Copy the contents of is to os.
* @param is
* @param os
* @param buf Can be null
* @param close If true, is is closed after the copy.
* @throws IOException
*/
private static void streamCopy(InputStream is, OutputStream os, byte[] buf, boolean close) throws IOException {
int len;
if (buf == null) {
buf = new byte[4096];
}
while ((len = is.read(buf)) > 0) {
os.write(buf, 0, len);
}
os.flush();
if (close) {
is.close();
}
}
/**
* Does an object array include an object.
* @param arr The array
* @param obj The object
*/
private static boolean contains(Object[] arr, Object obj) {
for (Object o : arr) {
if (o.equals(obj)) return true;
}
return false;
}
/**
* Select cipher suites to be used by the connector
* based on configured inclusion and exclusion lists
* as well as enabled and supported cipher suite lists.
* @param enabledCipherSuites Array of enabled cipher suites
* @param supportedCipherSuites Array of supported cipher suites
* @return Array of cipher suites to enable
*/
public String[] selectProtocols(String[] enabledProtocols, String[] supportedProtocols) {
Set<String> selected_protocols = new HashSet<String>();
// Set the starting protocols - either from the included or enabled list
if (sslConfig.getIncludeProtocols() != null) {
// Use only the supported included protocols
for (String protocol : supportedProtocols)
if (contains(sslConfig.getIncludeProtocols(), protocol))
selected_protocols.add(protocol);
} else {
selected_protocols.addAll(Arrays.asList(enabledProtocols));
}
// Remove any excluded protocols
if (sslConfig.getExcludeProtocols() != null) {
selected_protocols.removeAll(Arrays.asList(sslConfig.getExcludeProtocols()));
}
return selected_protocols.toArray(new String[selected_protocols.size()]);
}
/**
* Select cipher suites to be used by the connector
* based on configured inclusion and exclusion lists
* as well as enabled and supported cipher suite lists.
* @param enabledCipherSuites Array of enabled cipher suites
* @param supportedCipherSuites Array of supported cipher suites
* @return Array of cipher suites to enable
*/
public String[] selectCipherSuites(String[] enabledCipherSuites, String[] supportedCipherSuites) {
Set<String> selected_ciphers = new HashSet<String>();
// Set the starting ciphers - either from the included or enabled list
if (sslConfig.getIncludeCipherSuites() != null) {
// Use only the supported included ciphers
for (String cipherSuite : supportedCipherSuites)
if (contains(sslConfig.getIncludeCipherSuites(), cipherSuite))
selected_ciphers.add(cipherSuite);
} else {
selected_ciphers.addAll(Arrays.asList(enabledCipherSuites));
}
// Remove any excluded ciphers
if (sslConfig.getExcludeCipherSuites() != null) {
selected_ciphers.removeAll(Arrays.asList(sslConfig.getExcludeCipherSuites()));
}
return selected_ciphers.toArray(new String[selected_ciphers.size()]);
}
public SSLServerSocket newSslServerSocket(String host,int port,int backlog) throws IOException {
SSLServerSocketFactory factory = sslContext.getServerSocketFactory();
SSLServerSocket socket =
(SSLServerSocket) (host==null ?
factory.createServerSocket(port, backlog):
factory.createServerSocket(port, backlog, InetAddress.getByName(host)));
if (sslConfig.getWantClientAuth())
socket.setWantClientAuth(sslConfig.getWantClientAuth());
if (sslConfig.getNeedClientAuth())
socket.setNeedClientAuth(sslConfig.getNeedClientAuth());
socket.setEnabledCipherSuites(selectCipherSuites(socket.getEnabledCipherSuites(),
socket.getSupportedCipherSuites()));
socket.setEnabledProtocols(selectProtocols(socket.getEnabledProtocols(),socket.getSupportedProtocols()));
return socket;
}
/**
* Get an SSLSocket from this context.
* {@link SSLContext#getSocketFactory()}
*/
public SSLSocket newSslSocket() throws IOException {
SSLSocketFactory factory = sslContext.getSocketFactory();
SSLSocket socket = (SSLSocket)factory.createSocket();
if (sslConfig.getWantClientAuth())
socket.setWantClientAuth(sslConfig.getWantClientAuth());
if (sslConfig.getNeedClientAuth())
socket.setNeedClientAuth(sslConfig.getNeedClientAuth());
socket.setEnabledCipherSuites(selectCipherSuites(socket.getEnabledCipherSuites(),
socket.getSupportedCipherSuites()));
socket.setEnabledProtocols(selectProtocols(socket.getEnabledProtocols(),socket.getSupportedProtocols()));
return socket;
}
/**
* Get an SSLEngine from this context. Use this method to hint instead of the
* no-op for an internal session reuse strategy. Also, some cipher suites require
* remote hostname information.
* {@link SSLContext#createSSLEngine(String,int)}
* @param host The non-authoritative name of the host
* @param port The non-authoritative port
*/
public SSLEngine newSslEngine(String host,int port) {
SSLEngine sslEngine = sslConfig.isSessionCachingEnabled()
?sslContext.createSSLEngine(host, port)
:sslContext.createSSLEngine();
customize(sslEngine);
return sslEngine;
}
/**
* Get an SSLEngine from this context.
* {@link SSLContext#createSSLEngine()}
*/
public SSLEngine newSslEngine() {
SSLEngine sslEngine = sslContext.createSSLEngine();
customize(sslEngine);
return sslEngine;
}
private void customize(SSLEngine sslEngine) {
if (sslConfig.getWantClientAuth())
sslEngine.setWantClientAuth(sslConfig.getWantClientAuth());
if (sslConfig.getNeedClientAuth())
sslEngine.setNeedClientAuth(sslConfig.getNeedClientAuth());
sslEngine.setEnabledCipherSuites(selectCipherSuites(sslEngine.getEnabledCipherSuites(),
sslEngine.getSupportedCipherSuites()));
sslEngine.setEnabledProtocols(selectProtocols(sslEngine.getEnabledProtocols(),
sslEngine.getSupportedProtocols()));
}
}