/*
* Sun Public License
*
* The contents of this file are subject to the Sun Public License Version
* 1.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is available at http://www.sun.com/
*
* The Original Code is the SLAMD Distributed Load Generation Engine.
* The Initial Developer of the Original Code is Neil A. Wilson.
* Portions created by Neil A. Wilson are Copyright (C) 2004-2010.
* Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc.
* All Rights Reserved.
*
* Contributor(s): Neil A. Wilson
*/
package com.slamd.jobs;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Random;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import netscape.ldap.LDAPException;
import netscape.ldap.LDAPSocketFactory;
/**
* This class provides an implementation of an SSL socket factory that will use
* JSSE to create an SSL client socket. The first time the server requests a
* client certificate, one will be chosen at random from the appropriate set of
* keys in the JSSE JKS-format key store. Subsequent requests will continue to
* use the same client certificate until the <CODE>chooseNewClientCert</CODE>
* method is called. In addition, this class implements a trust manager so that
* any SSL certificate presented by the server will be trusted.
*
*
* @author Neil A. Wilson
*/
public class JSSERandomClientCertSocketFactory
extends SSLSocketFactory
implements LDAPSocketFactory, X509KeyManager, X509TrustManager
{
// Indicates whether the client certificate to use should always be chosen at
// random, or if it should only be chosen at random the first time a
// certificate is needed and any time the chooseNewClientCert method is
// called.
private boolean alwaysRandom;
// Indicates whether debug mode will be enabled (will print a message to
// standard error whenever any method is called).
private boolean debugMode;
// Indicates whether to disable session caching for connections created with
// this socket factory.
private boolean disableSessionCaching;
// The random number generator that will be used to select which client
// certificate to present.
private Random random;
// The SSL context that will be used to manage all things SSL.
private SSLContext sslContext;
// The SSL socket factory that will actually be used to create the sockets.
private SSLSocketFactory sslSocketFactory;
// The alias of the certificate that should be presented to the server
// whenever one is requested.
private String currentAlias;
// The set of aliases of the client certificates that are available for use.
private String[] aliases;
// The parent key manager that we will use for operations other than choosing
// the alias of the client certificate to present.
private X509KeyManager parentKeyManager;
/**
* Creates a new instance of this SSL socket factory.
*
* @param keyStoreFile The path to the JKS-format JSSE keystore
* containing the client certificates to use in the
* authentication process.
* @param keyStorePassword The password needed to access the information in
* the keystore, formatted as a character array.
*
* @throws LDAPException If a problem occurs while initializing this socket
* factory.
*/
public JSSERandomClientCertSocketFactory(String keyStoreFile,
char[] keyStorePassword)
throws LDAPException
{
this(keyStoreFile, keyStorePassword, false);
}
/**
* Creates a new instance of this SSL socket factory.
*
* @param keyStoreFile The path to the JKS-format JSSE keystore
* containing the client certificates to use in the
* authentication process.
* @param keyStorePassword The password needed to access the information in
* the keystore, formatted as a character array.
* @param debugMode Indicates whether this socket factory will
* operate in debug mode.
*
* @throws LDAPException If a problem occurs while initializing this socket
* factory.
*/
public JSSERandomClientCertSocketFactory(String keyStoreFile,
char[] keyStorePassword,
boolean debugMode)
throws LDAPException
{
this.debugMode = debugMode;
alwaysRandom = false;
disableSessionCaching = false;
// Read in the contents of the JSSE keystore.
KeyStore keyStore;
try
{
FileInputStream inputStream = new FileInputStream(keyStoreFile);
keyStore = KeyStore.getInstance("JKS");
keyStore.load(inputStream, keyStorePassword);
inputStream.close();
}
catch (Exception e)
{
String message = "Unable to read key store file \"" + keyStoreFile +
"\" -- " + e;
if (debugMode)
{
System.err.println(message);
}
throw new LDAPException(message);
}
try
{
// Iterate through the key store to find all the aliases of the client
// certificates.
ArrayList<String> aliasList = new ArrayList<String>();
Enumeration<String> keyStoreAliases = keyStore.aliases();
while (keyStoreAliases.hasMoreElements())
{
String alias = keyStoreAliases.nextElement();
if (keyStore.isKeyEntry(alias))
{
aliasList.add(keyStoreAliases.nextElement());
}
}
aliases = new String[aliasList.size()];
aliasList.toArray(aliases);
}
catch (KeyStoreException kse)
{
String message = "Unable to retrieve aliases of client certificates " +
"from the key store -- " + kse;
if (debugMode)
{
System.err.println(message);
}
throw new LDAPException(message);
}
// Make sure that there is at least one client certificate available.
if ((aliases == null) || (aliases.length == 0))
{
String message = "No client certificates found in key store \"" +
keyStoreFile + '"';
if (debugMode)
{
System.err.println(message);
}
throw new LDAPException(message);
}
// Get the default X.509 key manager.
try
{
KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, keyStorePassword);
KeyManager[] managers = keyManagerFactory.getKeyManagers();
if ((managers == null) || (managers.length == 0))
{
throw new NoSuchAlgorithmException("No X.509 key managers are " +
"available.");
}
parentKeyManager = (X509KeyManager) managers[0];
}
catch (Exception e)
{
String message = "Unable to obtain a handle to the default X.509 key " +
"manager -- " + e;
if (debugMode)
{
System.err.println(message);
}
throw new LDAPException(message);
}
// Indicate that we will be using JSSE for the SSL-based connections.
Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
System.setProperty("java.protocol.handler.pkgs",
"com.sun.net.ssl.internal.www.protocol");
// Get the default SSL context.
try
{
sslContext = SSLContext.getInstance("SSLv3");
}
catch (NoSuchAlgorithmException nsae)
{
String message = "Unable to initialize the SSL context -- " + nsae;
if (debugMode)
{
System.err.println(message);
}
throw new LDAPException(message);
}
// Initialize the SSL context with our own trust manager (this class) to
// use when determining whether to trust a client certificate.
try
{
sslContext.init(new KeyManager[] { this }, new TrustManager[] { this },
null);
}
catch (KeyManagementException kme)
{
String message = "Unable to register a key and trust managers with the " +
"SSL context: " + kme;
if (debugMode)
{
System.err.println(message);
}
throw new LDAPException(message);
}
// Get the socket factory to use when creating the certificates.
sslSocketFactory = sslContext.getSocketFactory();
// Initialize the random number generator that we will use for selecting
// client certificates.
random = new Random();
// If we are in debug mode, indicate that the socket factory has been
// created.
if (debugMode)
{
System.err.println("New JSSERandomClientCertSocketFactory created");
}
}
/**
* Retrieves the alias of the client certificate that will be used the next
* time the client must present a certificate to an SSL server.
*
* @return The alias of the client certificate that will be used the next
* time the client must present a certificate to an SSL server, or
* <CODE>null</CODE> if the next certificate will be chosen at
* random.
*/
public String getCurrentAlias()
{
return currentAlias;
}
/**
* Specifies the alias of the client certificate that should be used the next
* time the client must present a certificate to an SSL server. This
* selection will remain in effect until the <CODE>chooseNewClientCert</CODE>
* method is called (in which case the next certificate will be chosen at
* random) or the <CODE>setCurrentAlias</CODE> method is called again to
* choose a different alias. Note that no error checking is performed, so if
* the specified alias does not exist in the keystore, then attempts to use
* that certificate will fail. Also note that this will override the setting
* of the <CODE>alwaysRandom</CODE> flag, so this certificate will always be
* used until the <CODE>chooseNewClientCert</CODE> method is called, at which
* point the <CODE>alwaysRandom</CODE> flag will again be honored.
*
* @param alias The alias of the client certificate that should be used the
* next time the client must present a certificate to an SSL
* server. A value of <CODE>null</CODE> indicates that the
* next selection should be random.
*/
public void setCurrentAlias(String alias)
{
this.currentAlias = alias;
}
/**
* Retrieves the aliases of the client certificates that are available for use
* in the key store.
*
* @return The aliases of the client certificates that are available for use
* in the key store.
*/
public String[] getAliases()
{
return aliases;
}
/**
* Indicates that this socket factory should choose a new client certificate
* at random the next time it must present a certificate to an SSL server.
*/
public void chooseNewClientCert()
{
currentAlias = null;
}
/**
* Indicates whether the client certificate selection will be always taken at
* random, or if the selection should only be random the first time a
* certificate is needed or after the <CODE>chooseNewClientCert</CODE> method
* is called.
*
* @return <CODE>true</CODE> if the client certificate selection will always
* be random, or <CODE>false</CODE> if not.
*/
public boolean alwaysRandom()
{
return alwaysRandom;
}
/**
* Specifies whether the client certificate selection should always be random,
* or if the selection should only be random the first time a certificate is
* needed or after the <CODE>chooseNewClientCert</CODE> method is called.
*
* @param alwaysRandom Specifies whether the client certificate selection
* should always be random.
*/
public void setAlwaysRandom(boolean alwaysRandom)
{
this.alwaysRandom = alwaysRandom;
}
/**
* Indicates whether session caching has been disabled for SSL sockets created
* using this socket factory.
*
* @return <CODE>true</CODE> if session caching has been disabled, or
* <CODE>false</CODE> if not.
*/
public boolean disableSessionCaching()
{
return disableSessionCaching;
}
/**
* Specifies whether session caching should be disabled for SSL sockets
* created using this socket factory.
*
* @param disableSessionCaching Indicates whether session caching should be
* disabled for SSL sockets created using this
* socket factory.
*/
public void setDisableSessionCaching(boolean disableSessionCaching)
{
this.disableSessionCaching = disableSessionCaching;
}
/**
* Chooses the alias of the client certificate that should be presented to the
* server.
*
* @param keyTypes The key type algorithm name(s) to use in making the
* selection.
* @param issuers The set of accepted issuers to use in making the
* selection.
* @param socket The socket to use in making the selection.
*
* @return The alias of the client certificate that should be presented to
* the server.
*/
public String chooseClientAlias(String[] keyTypes, Principal[] issuers,
Socket socket)
{
if (currentAlias != null)
{
return currentAlias;
}
String alias = aliases[(random.nextInt() & 0x7FFFFFFF) % aliases.length];
if (! alwaysRandom)
{
currentAlias = alias;
}
return alias;
}
/**
* Retrieves the aliases of the certificates available for use by clients, in
* accordance with the provided criteria.
*
* @param keyType The key type algorithm name of certificates to include in
* the set of aliases returned.
* @param issuers The set of accepted issuers of certificates to include in
* the set of aliases returned.
*
* @return The aliases of the certificates available for use by clients, in
* accordance with the provided criteria.
*/
public String[] getClientAliases(String keyType, Principal[] issuers)
{
return parentKeyManager.getClientAliases(keyType, issuers);
}
/**
* Chooses the alias of the server certificate that should be presented to
* clients.
*
* @param keyType The key type algorithm name to use in making the
* selection.
* @param issuers The set of accepted issuers to use in making the
* selection.
* @param socket The socket to use in making the selection.
*
* @return The alias of the server certificate that should be presented to
* clients.
*/
public String chooseServerAlias(String keyType, Principal[] issuers,
Socket socket)
{
return parentKeyManager.chooseServerAlias(keyType, issuers, socket);
}
/**
* Retrieves the aliases of the certificates available for use by an SSL
* server, in accordance with the provided criteria.
*
* @param keyType The key type algorithm name of certificates to include in
* the set of aliases returned.
* @param issuers The set of accepted issuers of certificates to include in
* the set of aliases returned.
*
* @return The aliases of the certificates available for use by an SSL
* server, in accordance with the provided criteria.
*/
public String[] getServerAliases(String keyType, Principal[] issuers)
{
return parentKeyManager.getServerAliases(keyType, issuers);
}
/**
* Retrieves the private key for the certificate with the specified alias.
*
* @param alias The alias of the certificate for which to retrieve the
* private key.
*
* @return The private key of the requested certificate, or <CODE>null</CODE>
* if the specified certificate cannot be found.
*/
public PrivateKey getPrivateKey(String alias)
{
return parentKeyManager.getPrivateKey(alias);
}
/**
* Retrieves the certificate chain for the certificate with the given alias.
* The chain will be returned in order, with the specified certificate first
* and the root issuer last.
*
* @param alias The alias of the certificate for which to retrieve the
* certificate chain.
*
* @return The certificate chain for the certificate with the given alias, or
* <CODE>null</CODE> if the specified certificate cannot be found.
*/
public X509Certificate[] getCertificateChain(String alias)
{
return parentKeyManager.getCertificateChain(alias);
}
/**
* Determines whether the provided client certificate should be trusted. In
* this case, the certificate will always be trusted.
*
* @param chain The peer certificate chain.
* @param authType The authentication type based on the client certificate.
*/
public void checkClientTrusted(X509Certificate[] chain, String authType)
{
// No implementation required. If we don't throw an exception, then there
// is no problem with the cert.
if (debugMode)
{
System.err.println("checkClientTrusted() invoked");
}
}
/**
* Determines whether the provided server certificate should be trusted. In
* this case, the certificate will always be trusted.
*
* @param chain The peer certificate chain.
* @param authType The authentication type based on the server certificate.
*/
public void checkServerTrusted(X509Certificate[] chain, String authType)
{
// No implementation required. If we don't throw an exception, then there
// is no problem with the cert.
if (debugMode)
{
System.err.println("checkServerTrusted() invoked");
}
}
/**
* Retrieves an array of CA certificates that are trusted for authenticating
* peers.
*
* @return An empty array, because we don't care about any list of CAs.
*/
public X509Certificate[] getAcceptedIssuers()
{
if (debugMode)
{
System.err.println("getAcceptedIssuers() invoked");
}
return null;
}
/**
* Establishes an SSL socket to the provided host and port that can be used by
* the LDAP SDK for Java for communicating with an LDAP directory server.
*
* @param host The address of the server to which the connection is to be
* established.
* @param port The port number of the server to which the connection is to
* be established.
*
* @return The SSL socket that may be used for communicating with the
* directory server.
*
* @throws LDAPException If a problem occurs while trying to establish the
* connection.
*/
public Socket makeSocket(String host, int port)
throws LDAPException
{
if (debugMode)
{
System.err.println("makeSocket(" + host + ',' + port + ") invoked");
}
try
{
SSLSocket sslSocket =
(SSLSocket) sslSocketFactory.createSocket(host, port);
if (disableSessionCaching)
{
sslSocket.getSession().invalidate();
}
return sslSocket;
}
catch (Exception e)
{
throw new LDAPException("Unable to establish the SSL connection: " + e);
}
}
/**
* Creates a new SSL socket connected to the specified host and port.
*
* @param host The address of the system to which the SSL socket should be
* connected.
* @param port The port on the target system to which the SSL socket should
* be connected.
*
* @return The created SSL socket.
*
* @throws IOException If a problem occurs while creating the SSL socket.
*/
@Override()
public Socket createSocket(String host, int port)
throws IOException
{
if (debugMode)
{
System.err.println("createSocket(" + host + ',' + port + ") invoked");
}
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port);
if (disableSessionCaching)
{
sslSocket.getSession().invalidate();
}
return sslSocket;
}
/**
* Creates a new SSL socket connected to the specified host and port.
*
* @param host The address of the system to which the SSL socket should
* be connected.
* @param port The port on the target system to which the SSL socket
* should be connected.
* @param localHost The address on the local system from which the socket
* should originate.
* @param localPort The port on the local system from which the socket
* should originate.
*
* @return The created SSL socket.
*
* @throws IOException If a problem occurs while creating the SSL socket.
*/
@Override()
public Socket createSocket(String host, int port, InetAddress localHost,
int localPort)
throws IOException
{
if (debugMode)
{
System.err.println("createSocket(" + host + ',' + port + ", " +
localHost.getHostAddress() + ", " + localPort +
") invoked");
}
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port,
localHost,
localPort);
if (disableSessionCaching)
{
sslSocket.getSession().invalidate();
}
return sslSocket;
}
/**
* Creates a new SSL socket connected to the specified host and port.
*
* @param host The address of the system to which the SSL socket should be
* connected.
* @param port The port on the target system to which the SSL socket should
* be connected.
*
* @return The created SSL socket.
*
* @throws IOException If a problem occurs while creating the SSL socket.
*/
@Override()
public Socket createSocket(InetAddress host, int port)
throws IOException
{
if (debugMode)
{
System.err.println("createSocket(" + host.getHostAddress() + ", " + port +
") invoked");
}
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(host, port);
if (disableSessionCaching)
{
sslSocket.getSession().invalidate();
}
return sslSocket;
}
/**
* Creates a new SSL socket connected to the specified host and port.
*
* @param host The address of the system to which the SSL socket
* should be connected.
* @param port The port on the target system to which the SSL socket
* should be connected.
* @param localAddress The address on the local system from which the socket
* should originate.
* @param localPort The port on the local system from which the socket
* should originate.
*
* @return The created SSL socket.
*
* @throws IOException If a problem occurs while creating the SSL socket.
*/
@Override()
public Socket createSocket(InetAddress host, int port,
InetAddress localAddress, int localPort)
throws IOException
{
if (debugMode)
{
System.err.println("createSocket(" + host.getHostAddress() + ',' + port +
", " + localAddress.getHostAddress() + ", " +
localPort + ") invoked");
}
SSLSocket sslSocket =
(SSLSocket) sslSocketFactory.createSocket(host, port, localAddress,
localPort);
if (disableSessionCaching)
{
sslSocket.getSession().invalidate();
}
return sslSocket;
}
/**
* Converts the provided socket to an SSL socket using this socket factory.
*
* @param socket The socket to convert to an SSL socket.
* @param host The host to which the socket is connected.
* @param port The port to which the socket is connected.
* @param autoClose Indicates whether the underlying socket should be closed
* when the returned SSL socket is closed.
*
* @return The created SSL socket.
*
* @throws IOException If a problem occurs while creating the SSL socket.
*/
@Override()
public Socket createSocket(Socket socket, String host, int port,
boolean autoClose)
throws IOException
{
if (debugMode)
{
System.err.println("createSocket(Socket, " + host + ", " + port + ", " +
autoClose + ") invoked");
}
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket,
host, port,
autoClose);
if (disableSessionCaching)
{
sslSocket.getSession().invalidate();
}
return sslSocket;
}
/**
* Retrieves the set of cipher suites that are enabled by default.
*
* @return The set of cipher suites that are enabled by default.
*/
@Override()
public String[] getDefaultCipherSuites()
{
if (debugMode)
{
System.err.println("getDefaultCipherSuites() invoked");
}
return sslSocketFactory.getDefaultCipherSuites();
}
/**
* Retrieves the set of cipher suites that can be used to create SSL sockets.
*
* @return The set of cipher suites that can be used to create SSL sockets.
*/
@Override()
public String[] getSupportedCipherSuites()
{
if (debugMode)
{
System.err.println("getSupportedCipherSuites() invoked");
}
return sslSocketFactory.getSupportedCipherSuites();
}
}