blob: 309e0dc958357e1136184317f9d900d32fa7caed [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.qpid.proton.engine.impl.ssl;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import org.apache.qpid.proton.engine.SslDomain;
import org.apache.qpid.proton.engine.SslPeerDetails;
import org.apache.qpid.proton.engine.TransportException;
public class SslEngineFacadeFactory
{
private static final Logger _logger = Logger.getLogger(SslEngineFacadeFactory.class.getName());
/**
* The protocol name used to create an {@link SSLContext}, taken from Java's list of
* standard names at http://docs.oracle.com/javase/6/docs/technotes/guides/security/StandardNames.html
*
* TODO allow the protocol name to be overridden somehow
*/
private static final String TLS_PROTOCOL = "TLS";
// BouncyCastle Reflection Helpers
private static final Constructor<?> pemParserCons;
private static final Method pemReadMethod;
private static final Constructor<?> JcaPEMKeyConverterCons;
private static final Class<?> PEMKeyPairClass;
private static final Method getKeyPairMethod;
private static final Method getPrivateKeyMethod;
private static final Class<?> PEMEncryptedKeyPairClass;
private static final Method decryptKeyPairMethod;
private static final Constructor<?> JcePEMDecryptorProviderBuilderCons;
private static final Method builderMethod;
private static final Class<?> PrivateKeyInfoClass;
private static final Exception bouncyCastleSetupException;
static
{
// Setup BouncyCastle Reflection artifacts
Constructor<?> pemParserConsResult = null;
Method pemReadMethodResult = null;
Constructor<?> JcaPEMKeyConverterConsResult = null;
Class<?> PEMKeyPairClassResult = null;
Method getKeyPairMethodResult = null;
Method getPrivateKeyMethodResult = null;
Class<?> PEMEncryptedKeyPairClassResult = null;
Method decryptKeyPairMethodResult = null;
Constructor<?> JcePEMDecryptorProviderBuilderConsResult = null;
Method builderMethodResult = null;
Class<?> PrivateKeyInfoClassResult = null;
Exception bouncyCastleSetupExceptionResult = null;
try
{
final Class<?> pemParserClass = Class.forName("org.bouncycastle.openssl.PEMParser");
pemParserConsResult = pemParserClass.getConstructor(Reader.class);
pemReadMethodResult = pemParserClass.getMethod("readObject");
final Class<?> jcaPEMKeyConverterClass = Class.forName("org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter");
JcaPEMKeyConverterConsResult = jcaPEMKeyConverterClass.getConstructor();
PEMKeyPairClassResult = Class.forName("org.bouncycastle.openssl.PEMKeyPair");
getKeyPairMethodResult = jcaPEMKeyConverterClass.getMethod("getKeyPair", PEMKeyPairClassResult);
final Class<?> PEMDecrypterProvider = Class.forName("org.bouncycastle.openssl.PEMDecryptorProvider");
PEMEncryptedKeyPairClassResult = Class.forName("org.bouncycastle.openssl.PEMEncryptedKeyPair");
decryptKeyPairMethodResult = PEMEncryptedKeyPairClassResult.getMethod("decryptKeyPair", PEMDecrypterProvider);
final Class<?> jcePEMDecryptorProviderBuilderClass = Class.forName(
"org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder");
JcePEMDecryptorProviderBuilderConsResult = jcePEMDecryptorProviderBuilderClass.getConstructor();
builderMethodResult = jcePEMDecryptorProviderBuilderClass.getMethod("build", char[].class);
PrivateKeyInfoClassResult = Class.forName("org.bouncycastle.asn1.pkcs.PrivateKeyInfo");
getPrivateKeyMethodResult = jcaPEMKeyConverterClass.getMethod("getPrivateKey", PrivateKeyInfoClassResult);
registerBouncyCastleProvider();
}
catch (Exception e)
{
bouncyCastleSetupExceptionResult = e;
}
finally {
pemParserCons = pemParserConsResult;
pemReadMethod = pemReadMethodResult;
JcaPEMKeyConverterCons = JcaPEMKeyConverterConsResult;
PEMKeyPairClass = PEMKeyPairClassResult;
getKeyPairMethod = getKeyPairMethodResult;
getPrivateKeyMethod = getPrivateKeyMethodResult;
PEMEncryptedKeyPairClass = PEMEncryptedKeyPairClassResult;
decryptKeyPairMethod = decryptKeyPairMethodResult;
JcePEMDecryptorProviderBuilderCons = JcePEMDecryptorProviderBuilderConsResult;
builderMethod = builderMethodResult;
PrivateKeyInfoClass = PrivateKeyInfoClassResult;
bouncyCastleSetupException = bouncyCastleSetupExceptionResult;
}
}
static void registerBouncyCastleProvider()
throws ClassNotFoundException, InstantiationException, IllegalAccessException,
InvocationTargetException, NoSuchMethodException
{
// Try loading BC as a provider
Class<?> klass = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
Provider bouncyCastleProvider = (Provider) klass.getConstructor().newInstance();
synchronized (Security.class)
{
if(Security.getProvider(bouncyCastleProvider.getName()) == null)
{
Security.addProvider(bouncyCastleProvider);
}
}
}
SslEngineFacadeFactory()
{
}
/**
* This is a list of all anonymous cipher suites supported by Java 6, excluding those that
* use MD5. These are all supported by both Oracle's and IBM's Java 6 implementation.
*/
private static final List<String> ANONYMOUS_CIPHER_SUITES = Arrays.asList(
"TLS_DH_anon_WITH_AES_128_CBC_SHA",
"SSL_DH_anon_WITH_3DES_EDE_CBC_SHA",
"SSL_DH_anon_WITH_DES_CBC_SHA",
"SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA");
/** lazily initialized */
private SSLContext _sslContext;
/**
* Returns a {@link ProtonSslEngine}. May cache the domain's settings so callers should invoke
* {@link #resetCache()} if the domain changes.
*
* @param peerDetails may be used to return an engine that supports SSL resume.
*/
public ProtonSslEngine createProtonSslEngine(SslDomain domain, SslPeerDetails peerDetails)
{
SSLEngine engine = createAndInitialiseSslEngine(domain, peerDetails);
if(_logger.isLoggable(Level.FINE))
{
_logger.fine("Created SSL engine: " + engineToString(engine));
}
return new DefaultSslEngineFacade(engine);
}
/**
* Guarantees that no cached settings are used in subsequent calls to
* {@link #createProtonSslEngine(SslDomain, SslPeerDetails)}.
*/
public void resetCache()
{
_sslContext = null;
}
private SSLEngine createAndInitialiseSslEngine(SslDomain domain, SslPeerDetails peerDetails)
{
SslDomain.Mode mode = domain.getMode();
SSLContext sslContext = getOrCreateSslContext(domain);
SSLEngine sslEngine = createSslEngine(sslContext, peerDetails);
if (domain.getPeerAuthentication() == SslDomain.VerifyMode.ANONYMOUS_PEER)
{
addAnonymousCipherSuites(sslEngine);
}
else
{
if (mode == SslDomain.Mode.SERVER)
{
sslEngine.setNeedClientAuth(true);
}
}
if(_logger.isLoggable(Level.FINE))
{
_logger.log(Level.FINE, mode + " Enabled cipher suites " + Arrays.asList(sslEngine.getEnabledCipherSuites()));
}
boolean useClientMode = mode == SslDomain.Mode.CLIENT ? true : false;
sslEngine.setUseClientMode(useClientMode);
removeSSLv3Support(sslEngine);
return sslEngine;
}
private static final String SSLV3_PROTOCOL = "SSLv3";
private static void removeSSLv3Support(final SSLEngine engine)
{
List<String> enabledProtocols = Arrays.asList(engine.getEnabledProtocols());
if(enabledProtocols.contains(SSLV3_PROTOCOL))
{
List<String> allowedProtocols = new ArrayList<String>(enabledProtocols);
allowedProtocols.remove(SSLV3_PROTOCOL);
engine.setEnabledProtocols(allowedProtocols.toArray(new String[allowedProtocols.size()]));
}
}
/**
* @param sslPeerDetails is allowed to be null. A non-null value is used to hint that SSL resumption
* should be attempted
*/
private SSLEngine createSslEngine(SSLContext sslContext, SslPeerDetails sslPeerDetails)
{
final SSLEngine sslEngine;
if(sslPeerDetails == null)
{
sslEngine = sslContext.createSSLEngine();
}
else
{
sslEngine = sslContext.createSSLEngine(sslPeerDetails.getHostname(), sslPeerDetails.getPort());
}
return sslEngine;
}
private SSLContext getOrCreateSslContext(SslDomain sslDomain)
{
if(_sslContext == null && sslDomain.getSslContext() != null)
{
_sslContext = sslDomain.getSslContext();
}
else if(_sslContext == null)
{
if(_logger.isLoggable(Level.FINE))
{
_logger.fine("lazily creating new SSLContext using domain " + sslDomain);
}
final char[] dummyPassword = "unused-passphrase".toCharArray(); // Dummy password required by KeyStore and KeyManagerFactory, but never referred to again
try
{
SSLContext sslContext = SSLContext.getInstance(TLS_PROTOCOL);
KeyStore ksKeys = createKeyStoreFrom(sslDomain, dummyPassword);
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ksKeys, dummyPassword);
final TrustManager[] trustManagers;
if (sslDomain.getPeerAuthentication() == SslDomain.VerifyMode.ANONYMOUS_PEER)
{
trustManagers = new TrustManager[] { new AlwaysTrustingTrustManager() };
}
else
{
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ksKeys);
trustManagers = tmf.getTrustManagers();
}
sslContext.init(kmf.getKeyManagers(), trustManagers, null);
_sslContext = sslContext;
}
catch (NoSuchAlgorithmException e)
{
throw new TransportException("Unexpected exception creating SSLContext", e);
}
catch (KeyStoreException e)
{
throw new TransportException("Unexpected exception creating SSLContext", e);
}
catch (UnrecoverableKeyException e)
{
throw new TransportException("Unexpected exception creating SSLContext", e);
}
catch (KeyManagementException e)
{
throw new TransportException("Unexpected exception creating SSLContext", e);
}
}
return _sslContext;
}
private KeyStore createKeyStoreFrom(SslDomain sslDomain, char[] dummyPassword)
{
try
{
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(null, null);
if (sslDomain.getTrustedCaDb() != null)
{
String caCertAlias = "cacert";
if(_logger.isLoggable(Level.FINE))
{
_logger.log(Level.FINE, "_sslParams.getTrustedCaDb() : " + sslDomain.getTrustedCaDb());
}
int i = 1;
for(Certificate trustedCaCert : readCertificates(sslDomain.getTrustedCaDb()))
{
keystore.setCertificateEntry(caCertAlias + (i++), trustedCaCert);
}
}
if (sslDomain.getCertificateFile() != null
&& sslDomain.getPrivateKeyFile() != null)
{
String clientPrivateKeyAlias = "clientPrivateKey";
Certificate clientCertificate = (Certificate) readCertificate(sslDomain.getCertificateFile());
PrivateKey clientPrivateKey = readPrivateKey(sslDomain.getPrivateKeyFile(), sslDomain.getPrivateKeyPassword());
keystore.setKeyEntry(clientPrivateKeyAlias, clientPrivateKey,
dummyPassword, new Certificate[] { clientCertificate });
}
return keystore;
}
catch (KeyStoreException e)
{
throw new TransportException("Unexpected exception creating keystore", e);
}
catch (NoSuchAlgorithmException e)
{
throw new TransportException("Unexpected exception creating keystore", e);
}
catch (CertificateException e)
{
throw new TransportException("Unexpected exception creating keystore", e);
}
catch (IOException e)
{
throw new TransportException("Unexpected exception creating keystore", e);
}
}
private void addAnonymousCipherSuites(SSLEngine sslEngine)
{
List<String> supportedSuites = Arrays.asList(sslEngine.getSupportedCipherSuites());
List<String> currentEnabledSuites = Arrays.asList(sslEngine.getEnabledCipherSuites());
List<String> enabledSuites = buildEnabledSuitesIncludingAnonymous(ANONYMOUS_CIPHER_SUITES, supportedSuites, currentEnabledSuites);
sslEngine.setEnabledCipherSuites(enabledSuites.toArray(new String[0]));
}
private List<String> buildEnabledSuitesIncludingAnonymous(
List<String> anonymousCipherSuites, List<String> supportedSuites, List<String> currentEnabled)
{
List<String> newEnabled = new ArrayList<String>(currentEnabled);
int addedAnonymousCipherSuites = 0;
for (String anonymousCipherSuiteName : anonymousCipherSuites)
{
if (supportedSuites.contains(anonymousCipherSuiteName))
{
newEnabled.add(anonymousCipherSuiteName);
addedAnonymousCipherSuites++;
}
}
if (addedAnonymousCipherSuites > 0 && _logger.isLoggable(Level.FINE))
{
_logger.fine("There are now " + newEnabled.size()
+ " cipher suites enabled (previously " + currentEnabled.size()
+ "), including " + addedAnonymousCipherSuites + " out of the "
+ anonymousCipherSuites.size() + " requested anonymous ones." );
}
return newEnabled;
}
private String engineToString(SSLEngine engine)
{
return new StringBuilder("[ " )
.append(engine)
.append(", needClientAuth=").append(engine.getNeedClientAuth())
.append(", useClientMode=").append(engine.getUseClientMode())
.append(", peerHost=").append(engine.getPeerHost())
.append(", peerPort=").append(engine.getPeerPort())
.append(" ]").toString();
}
Certificate readCertificate(String pemFile)
{
InputStream is = null;
try
{
CertificateFactory cFactory = CertificateFactory.getInstance("X.509");
is = new FileInputStream(pemFile);
return cFactory.generateCertificate(is);
}
catch (CertificateException ce)
{
String msg = "Failed to load certificate [" + pemFile + "]";
_logger.log(Level.SEVERE, msg, ce);
throw new TransportException(msg, ce);
}
catch (FileNotFoundException e)
{
String msg = "Certificate file not found [" + pemFile + "]";
_logger.log(Level.SEVERE, msg);
throw new TransportException(msg, e);
}
finally
{
closeSafely(is);
}
}
Collection<? extends Certificate> readCertificates(String pemFile)
{
InputStream is = null;
try
{
CertificateFactory cFactory = CertificateFactory.getInstance("X.509");
is = new FileInputStream(pemFile);
return cFactory.generateCertificates(is);
}
catch (CertificateException ce)
{
String msg = "Failed to load certificates [" + pemFile + "]";
_logger.log(Level.SEVERE, msg, ce);
throw new TransportException(msg, ce);
}
catch (FileNotFoundException e)
{
String msg = "Certificates file not found [" + pemFile + "]";
_logger.log(Level.SEVERE, msg);
throw new TransportException(msg, e);
}
finally
{
closeSafely(is);
}
}
PrivateKey readPrivateKey(String pemFile, String password)
{
if (bouncyCastleSetupException != null)
{
throw new TransportException("BouncyCastle failed to load", bouncyCastleSetupException);
}
final Object pemObject = readPemObject(pemFile);
PrivateKey privateKey = null;
try
{
Object keyConverter = JcaPEMKeyConverterCons.newInstance();
setProvider(keyConverter, "BC");
if (PEMEncryptedKeyPairClass.isInstance(pemObject))
{
Object decryptorBuilder = JcePEMDecryptorProviderBuilderCons.newInstance();
// Build a PEMDecryptProvider
Object decryptProvider = builderMethod.invoke(decryptorBuilder, password.toCharArray());
Object decryptedKeyPair = decryptKeyPairMethod.invoke(pemObject, decryptProvider);
KeyPair keyPair = (KeyPair) getKeyPairMethod.invoke(keyConverter, decryptedKeyPair);
privateKey = keyPair.getPrivate();
}
else if (PEMKeyPairClass.isInstance(pemObject))
{
// It's a KeyPair but not encrypted.
KeyPair keyPair = (KeyPair) getKeyPairMethod.invoke(keyConverter, pemObject);
privateKey = keyPair.getPrivate();
}
else if (PrivateKeyInfoClass.isInstance(pemObject))
{
// It's an unencrypted private key
privateKey = (PrivateKey) getPrivateKeyMethod.invoke(keyConverter, pemObject);
}
else
{
final String msg = "Unable to load PrivateKey, Unpexected Object [" + pemObject.getClass().getName()
+ "]";
_logger.log(Level.SEVERE, msg);
throw new TransportException(msg);
}
}
catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e)
{
final String msg = "Failed to process key file [" + pemFile + "] - " + e.getMessage();
throw new TransportException(msg, e);
}
return privateKey;
}
private Object readPemObject(String pemFile)
{
Reader reader = null;
Object pemParser = null;
Object pemObject = null;
try
{
reader = new FileReader(pemFile);
pemParser = pemParserCons.newInstance(reader); // = new PEMParser(reader);
pemObject = pemReadMethod.invoke(pemParser); // = pemParser.readObject();
}
catch (IOException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException e)
{
_logger.log(Level.SEVERE, "Unable to read PEM object. Perhaps you need the unlimited strength libraries in <java-home>/jre/lib/security/ ?", e);
throw new TransportException("Unable to read PEM object from file " + pemFile, e);
}
finally
{
closeSafely(reader);
}
return pemObject;
}
private void setProvider(Object obj, String provider) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException
{
final Class<?> aClz = obj.getClass();
final Method setProvider = aClz.getMethod("setProvider", String.class);
setProvider.invoke(obj, provider);
}
private void closeSafely(Closeable c)
{
if (c != null)
{
try
{
c.close();
}
catch (IOException e)
{
// Swallow
}
}
}
private static final class AlwaysTrustingTrustManager implements X509TrustManager
{
@Override
public X509Certificate[] getAcceptedIssuers()
{
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException
{
// Do not check certificate
}
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException
{
// Do not check certificate
}
}
}