// ========================================================================
// $Id: SslListener.java,v 1.8 2006/11/22 20:21:30 gregwilkins Exp $
// Copyright 2000-2004 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// 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.
// ========================================================================
//
package net.lightbody.bmp.proxy.jetty.http;
import net.lightbody.bmp.proxy.jetty.jetty.servlet.ServletSSL;
import net.lightbody.bmp.proxy.jetty.log.LogFactory;
import net.lightbody.bmp.proxy.jetty.util.InetAddrPort;
import net.lightbody.bmp.proxy.jetty.util.LogSupport;
import net.lightbody.bmp.proxy.jetty.util.Password;
import net.lightbody.bmp.proxy.jetty.util.Resource;
import org.apache.commons.logging.Log;
import javax.net.ssl.*;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.X509Certificate;
/* ------------------------------------------------------------ */
/**
* JSSE Socket Listener.
*
* This is heavily based on the work from Court Demas, which in turn is based on the work from Forge
* Research.
*
* @version $Id: SslListener.java,v 1.8 2006/11/22 20:21:30 gregwilkins Exp $
* @author Greg Wilkins (gregw@mortbay.com)
* @author Court Demas (court@kiwiconsulting.com)
* @author Forge Research Pty Ltd ACN 003 491 576
* @author Jan Hlavaty
*/
public class SslListener extends SocketListener
{
private static Log log = LogFactory.getLog(SslListener.class);
/** Default value for the cipher Suites. */
private String cipherSuites[] = null;
/** Default value for the keystore location path. */
public static final String DEFAULT_KEYSTORE = System.getProperty("user.home") + File.separator
+ ".keystore";
/** String name of keystore password property. */
public static final String PASSWORD_PROPERTY = "jetty.ssl.password";
/** String name of key password property. */
public static final String KEYPASSWORD_PROPERTY = "jetty.ssl.keypassword";
/**
* The name of the SSLSession attribute that will contain any cached information.
*/
static final String CACHED_INFO_ATTR = CachedInfo.class.getName();
private String _keystore=DEFAULT_KEYSTORE ;
private transient Password _password;
private transient Password _keypassword;
private boolean _needClientAuth = false; // Set to true if we require client certificate authentication.
private boolean _wantClientAuth = false; // Set to true if we would like client certificate authentication.
private String _protocol= "TLS";
private String _algorithm = (Security.getProperty("ssl.KeyManagerFactory.algorithm")==null?"SunX509":Security.getProperty("ssl.KeyManagerFactory.algorithm")); // cert algorithm
private String _keystoreType = "JKS"; // type of the key store
private String _provider = null;
/* ------------------------------------------------------------ */
/**
* Constructor.
*/
public SslListener()
{
super();
setDefaultScheme(HttpMessage.__SSL_SCHEME);
}
/* ------------------------------------------------------------ */
/**
* Constructor.
*
* @param p_address
*/
public SslListener(InetAddrPort p_address)
{
super(p_address);
if (p_address.getPort() == 0)
{
p_address.setPort(443);
setPort(443);
}
setDefaultScheme(HttpMessage.__SSL_SCHEME);
}
/* ------------------------------------------------------------ */
public String[] getCipherSuites() {
return cipherSuites;
}
/* ------------------------------------------------------------ */
/**
* @author Tony Jiang
*/
public void setCipherSuites(String[] cipherSuites) {
this.cipherSuites = cipherSuites;
}
/* ------------------------------------------------------------ */
public void setPassword(String password)
{
_password = Password.getPassword(PASSWORD_PROPERTY,password,null);
}
/* ------------------------------------------------------------ */
public void setKeyPassword(String password)
{
_keypassword = Password.getPassword(KEYPASSWORD_PROPERTY,password,null);
}
/* ------------------------------------------------------------ */
public String getAlgorithm()
{
return (this._algorithm);
}
/* ------------------------------------------------------------ */
public void setAlgorithm(String algorithm)
{
this._algorithm = algorithm;
}
/* ------------------------------------------------------------ */
public String getProtocol()
{
return _protocol;
}
/* ------------------------------------------------------------ */
public void setProtocol(String protocol)
{
_protocol = protocol;
}
/* ------------------------------------------------------------ */
public void setKeystore(String keystore)
{
_keystore = keystore;
}
/* ------------------------------------------------------------ */
public String getKeystore()
{
return _keystore;
}
/* ------------------------------------------------------------ */
public String getKeystoreType()
{
return (_keystoreType);
}
/* ------------------------------------------------------------ */
public void setKeystoreType(String keystoreType)
{
_keystoreType = keystoreType;
}
/* ------------------------------------------------------------ */
/**
* Set the value of the needClientAuth property
*
* @param needClientAuth true iff we require client certificate authentication.
*/
public void setNeedClientAuth(boolean needClientAuth)
{
_needClientAuth = needClientAuth;
}
/* ------------------------------------------------------------ */
public boolean getNeedClientAuth()
{
return _needClientAuth;
}
/* ------------------------------------------------------------ */
/**
* Set the value of the needClientAuth property
*
* @param wantClientAuth true iff we would like client certificate authentication.
*/
public void setWantClientAuth(boolean wantClientAuth)
{
_wantClientAuth = wantClientAuth;
}
/* ------------------------------------------------------------ */
public boolean getWantClientAuth()
{
return _wantClientAuth;
}
/* ------------------------------------------------------------ */
/**
* By default, we're integral, given we speak SSL. But, if we've been told about an integral
* port, and said port is not our port, then we're not. This allows separation of listeners
* providing INTEGRAL versus CONFIDENTIAL constraints, such as one SSL listener configured to
* require client certs providing CONFIDENTIAL, whereas another SSL listener not requiring
* client certs providing mere INTEGRAL constraints.
*/
public boolean isIntegral(HttpConnection connection)
{
final int integralPort = getIntegralPort();
return integralPort == 0 || integralPort == getPort();
}
/* ------------------------------------------------------------ */
/**
* By default, we're confidential, given we speak SSL. But, if we've been told about an
* confidential port, and said port is not our port, then we're not. This allows separation of
* listeners providing INTEGRAL versus CONFIDENTIAL constraints, such as one SSL listener
* configured to require client certs providing CONFIDENTIAL, whereas another SSL listener not
* requiring client certs providing mere INTEGRAL constraints.
*/
public boolean isConfidential(HttpConnection connection)
{
final int confidentialPort = getConfidentialPort();
return confidentialPort == 0 || confidentialPort == getPort();
}
/* ------------------------------------------------------------ */
protected SSLServerSocketFactory createFactory()
throws Exception
{
SSLContext context;
if (_provider == null) {
context = SSLContext.getInstance(_protocol);
} else {
context = SSLContext.getInstance(_protocol, _provider);
}
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(_algorithm);
KeyStore keyStore = KeyStore.getInstance(_keystoreType);
keyStore.load(Resource.newResource(_keystore).getInputStream(), _password.toString().toCharArray());
keyManagerFactory.init(keyStore,_keypassword.toString().toCharArray());
context.init(keyManagerFactory.getKeyManagers(), null, new java.security.SecureRandom());
return context.getServerSocketFactory();
}
/* ------------------------------------------------------------ */
/**
* @param p_address
* @param p_acceptQueueSize
* @return @exception IOException
*/
protected ServerSocket newServerSocket(InetAddrPort p_address, int p_acceptQueueSize)
throws IOException
{
SSLServerSocketFactory factory = null;
SSLServerSocket socket = null;
try
{
factory = createFactory();
if (p_address == null)
{
socket = (SSLServerSocket) factory.createServerSocket(0, p_acceptQueueSize);
}
else
{
socket = (SSLServerSocket) factory.createServerSocket(p_address.getPort(),
p_acceptQueueSize, p_address.getInetAddress());
}
if (_needClientAuth)
socket.setNeedClientAuth(true);
else if (_wantClientAuth)
socket.setWantClientAuth(true);
if(cipherSuites != null && cipherSuites.length >0) {
socket.setEnabledCipherSuites(cipherSuites);
for ( int i=0; i<cipherSuites.length; i++ ) {
log.debug("SslListener enabled ciphersuite: " + cipherSuites[i]);
}
}
}
catch (IOException e)
{
throw e;
}
catch (Exception e)
{
log.warn(LogSupport.EXCEPTION, e);
throw new IOException("Could not create JsseListener: " + e.toString());
}
return socket;
}
/* ------------------------------------------------------------ */
/**
* @param p_serverSocket
* @return @exception IOException
*/
protected Socket accept(ServerSocket p_serverSocket) throws IOException
{
try
{
SSLSocket s = (SSLSocket) p_serverSocket.accept();
if (getMaxIdleTimeMs() > 0) s.setSoTimeout(getMaxIdleTimeMs());
s.startHandshake(); // block until SSL handshaking is done
return s;
}
catch (SSLException e)
{
log.warn(LogSupport.EXCEPTION, e);
throw new IOException(e.getMessage());
}
}
/* ------------------------------------------------------------ */
/**
* Allow the Listener a chance to customise the request. before the server does its stuff. <br>
* This allows the required attributes to be set for SSL requests. <br>
* The requirements of the Servlet specs are:
* <ul>
* <li>an attribute named "javax.servlet.request.cipher_suite" of type String.</li>
* <li>an attribute named "javax.servlet.request.key_size" of type Integer.</li>
* <li>an attribute named "javax.servlet.request.X509Certificate" of type
* java.security.cert.X509Certificate[]. This is an array of objects of type X509Certificate,
* the order of this array is defined as being in ascending order of trust. The first
* certificate in the chain is the one set by the client, the next is the one used to
* authenticate the first, and so on.</li>
* </ul>
*
* @param socket The Socket the request arrived on. This should be a javax.net.ssl.SSLSocket.
* @param request HttpRequest to be customised.
*/
protected void customizeRequest(Socket socket, HttpRequest request)
{
super.customizeRequest(socket, request);
if (!(socket instanceof javax.net.ssl.SSLSocket)) return; // I'm tempted to let it throw an
// exception...
try
{
SSLSocket sslSocket = (SSLSocket) socket;
SSLSession sslSession = sslSocket.getSession();
String cipherSuite = sslSession.getCipherSuite();
Integer keySize;
X509Certificate[] certs;
CachedInfo cachedInfo = (CachedInfo) sslSession.getValue(CACHED_INFO_ATTR);
if (cachedInfo != null)
{
keySize = cachedInfo.getKeySize();
certs = cachedInfo.getCerts();
}
else
{
keySize = new Integer(ServletSSL.deduceKeyLength(cipherSuite));
certs = getCertChain(sslSession);
cachedInfo = new CachedInfo(keySize, certs);
sslSession.putValue(CACHED_INFO_ATTR, cachedInfo);
}
if (certs != null)
request.setAttribute("javax.servlet.request.X509Certificate", certs);
else if (_needClientAuth) // Sanity check
throw new HttpException(HttpResponse.__403_Forbidden);
request.setAttribute("javax.servlet.request.cipher_suite", cipherSuite);
request.setAttribute("javax.servlet.request.key_size", keySize);
}
catch (Exception e)
{
log.warn(LogSupport.EXCEPTION, e);
}
}
/**
* Return the chain of X509 certificates used to negotiate the SSL Session.
* <p>
* Note: in order to do this we must convert a javax.security.cert.X509Certificate[], as used by
* JSSE to a java.security.cert.X509Certificate[],as required by the Servlet specs.
*
* @param sslSession the javax.net.ssl.SSLSession to use as the source of the cert chain.
* @return the chain of java.security.cert.X509Certificates used to negotiate the SSL
* connection. <br>
* Will be null if the chain is missing or empty.
*/
private static X509Certificate[] getCertChain(SSLSession sslSession)
{
try
{
javax.security.cert.X509Certificate javaxCerts[] = sslSession.getPeerCertificateChain();
if (javaxCerts == null || javaxCerts.length == 0) return null;
int length = javaxCerts.length;
X509Certificate[] javaCerts = new X509Certificate[length];
java.security.cert.CertificateFactory cf = java.security.cert.CertificateFactory
.getInstance("X.509");
for (int i = 0; i < length; i++)
{
byte bytes[] = javaxCerts[i].getEncoded();
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
javaCerts[i] = (X509Certificate) cf.generateCertificate(stream);
}
return javaCerts;
}
catch (SSLPeerUnverifiedException pue)
{
return null;
}
catch (Exception e)
{
log.warn(LogSupport.EXCEPTION, e);
return null;
}
}
/**
* Simple bundle of information that is cached in the SSLSession. Stores the effective keySize
* and the client certificate chain.
*/
private class CachedInfo
{
private Integer _keySize;
private X509Certificate[] _certs;
CachedInfo(Integer keySize, X509Certificate[] certs)
{
this._keySize = keySize;
this._certs = certs;
}
Integer getKeySize()
{
return _keySize;
}
X509Certificate[] getCerts()
{
return _certs;
}
}
public String getProvider() {
return _provider;
}
public void setProvider(String _provider) {
this._provider = _provider;
}
}