001: /*
002: * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/contrib/org/apache/commons/httpclient/contrib/ssl/AuthSSLProtocolSocketFactory.java,v 1.2 2004/06/10 18:25:24 olegk Exp $
003: * $Revision$
004: * $Date$
005: *
006: * ====================================================================
007: *
008: * Copyright 2002-2004 The Apache Software Foundation
009: *
010: * Licensed under the Apache License, Version 2.0 (the "License");
011: * you may not use this file except in compliance with the License.
012: * You may obtain a copy of the License at
013: *
014: * http://www.apache.org/licenses/LICENSE-2.0
015: *
016: * Unless required by applicable law or agreed to in writing, software
017: * distributed under the License is distributed on an "AS IS" BASIS,
018: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
019: * See the License for the specific language governing permissions and
020: * limitations under the License.
021: * ====================================================================
022: *
023: * This software consists of voluntary contributions made by many
024: * individuals on behalf of the Apache Software Foundation. For more
025: * information on the Apache Software Foundation, please see
026: * <http://www.apache.org/>.
027: *
028: */
029:
030: package org.apache.commons.httpclient.contrib.ssl;
031:
032: import java.io.IOException;
033: import java.net.InetAddress;
034: import java.net.InetSocketAddress;
035: import java.net.Socket;
036: import java.net.SocketAddress;
037: import java.net.URL;
038: import java.net.UnknownHostException;
039: import java.security.GeneralSecurityException;
040: import java.security.KeyStore;
041: import java.security.KeyStoreException;
042: import java.security.NoSuchAlgorithmException;
043: import java.security.UnrecoverableKeyException;
044: import java.security.cert.Certificate;
045: import java.security.cert.CertificateException;
046: import java.security.cert.X509Certificate;
047: import java.util.Enumeration;
048:
049: import org.apache.commons.httpclient.ConnectTimeoutException;
050: import org.apache.commons.httpclient.params.HttpConnectionParams;
051: import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
052: import org.apache.commons.logging.Log;
053: import org.apache.commons.logging.LogFactory;
054:
055: import javax.net.SocketFactory;
056: import javax.net.ssl.KeyManager;
057: import javax.net.ssl.KeyManagerFactory;
058: import javax.net.ssl.SSLContext;
059: import javax.net.ssl.TrustManager;
060: import javax.net.ssl.TrustManagerFactory;
061: import javax.net.ssl.X509TrustManager;
062:
063: /**
064: * <p>
065: * AuthSSLProtocolSocketFactory can be used to validate the identity of the HTTPS
066: * server against a list of trusted certificates and to authenticate to the HTTPS
067: * server using a private key.
068: * </p>
069: *
070: * <p>
071: * AuthSSLProtocolSocketFactory will enable server authentication when supplied with
072: * a {@link KeyStore truststore} file containg one or several trusted certificates.
073: * The client secure socket will reject the connection during the SSL session handshake
074: * if the target HTTPS server attempts to authenticate itself with a non-trusted
075: * certificate.
076: * </p>
077: *
078: * <p>
079: * Use JDK keytool utility to import a trusted certificate and generate a truststore file:
080: * <pre>
081: * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
082: * </pre>
083: * </p>
084: *
085: * <p>
086: * AuthSSLProtocolSocketFactory will enable client authentication when supplied with
087: * a {@link KeyStore keystore} file containg a private key/public certificate pair.
088: * The client secure socket will use the private key to authenticate itself to the target
089: * HTTPS server during the SSL session handshake if requested to do so by the server.
090: * The target HTTPS server will in its turn verify the certificate presented by the client
091: * in order to establish client's authenticity
092: * </p>
093: *
094: * <p>
095: * Use the following sequence of actions to generate a keystore file
096: * </p>
097: * <ul>
098: * <li>
099: * <p>
100: * Use JDK keytool utility to generate a new key
101: * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
102: * For simplicity use the same password for the key as that of the keystore
103: * </p>
104: * </li>
105: * <li>
106: * <p>
107: * Issue a certificate signing request (CSR)
108: * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
109: * </p>
110: * </li>
111: * <li>
112: * <p>
113: * Send the certificate request to the trusted Certificate Authority for signature.
114: * One may choose to act as her own CA and sign the certificate request using a PKI
115: * tool, such as OpenSSL.
116: * </p>
117: * </li>
118: * <li>
119: * <p>
120: * Import the trusted CA root certificate
121: * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
122: * </p>
123: * </li>
124: * <li>
125: * <p>
126: * Import the PKCS#7 file containg the complete certificate chain
127: * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
128: * </p>
129: * </li>
130: * <li>
131: * <p>
132: * Verify the content the resultant keystore file
133: * <pre>keytool -list -v -keystore my.keystore</pre>
134: * </p>
135: * </li>
136: * </ul>
137: * <p>
138: * Example of using custom protocol socket factory for a specific host:
139: * <pre>
140: * Protocol authhttps = new Protocol("https",
141: * new AuthSSLProtocolSocketFactory(
142: * new URL("file:my.keystore"), "mypassword",
143: * new URL("file:my.truststore"), "mypassword"), 443);
144: *
145: * HttpClient client = new HttpClient();
146: * client.getHostConfiguration().setHost("localhost", 443, authhttps);
147: * // use relative url only
148: * GetMethod httpget = new GetMethod("/");
149: * client.executeMethod(httpget);
150: * </pre>
151: * </p>
152: * <p>
153: * Example of using custom protocol socket factory per default instead of the standard one:
154: * <pre>
155: * Protocol authhttps = new Protocol("https",
156: * new AuthSSLProtocolSocketFactory(
157: * new URL("file:my.keystore"), "mypassword",
158: * new URL("file:my.truststore"), "mypassword"), 443);
159: * Protocol.registerProtocol("https", authhttps);
160: *
161: * HttpClient client = new HttpClient();
162: * GetMethod httpget = new GetMethod("https://localhost/");
163: * client.executeMethod(httpget);
164: * </pre>
165: * </p>
166: * @author <a href="mailto:oleg -at- ural.ru">Oleg Kalnichevski</a>
167: *
168: * <p>
169: * DISCLAIMER: HttpClient developers DO NOT actively support this component.
170: * The component is provided as a reference material, which may be inappropriate
171: * for use without additional customization.
172: * </p>
173: */
174:
175: public class AuthSSLProtocolSocketFactory implements
176: SecureProtocolSocketFactory {
177:
178: /** Log object for this class. */
179: private static final Log LOG = LogFactory
180: .getLog(AuthSSLProtocolSocketFactory.class);
181:
182: private URL keystoreUrl = null;
183: private String keystorePassword = null;
184: private URL truststoreUrl = null;
185: private String truststorePassword = null;
186: private SSLContext sslcontext = null;
187:
188: /**
189: * Constructor for AuthSSLProtocolSocketFactory. Either a keystore or truststore file
190: * must be given. Otherwise SSL context initialization error will result.
191: *
192: * @param keystoreUrl URL of the keystore file. May be <tt>null</tt> if HTTPS client
193: * authentication is not to be used.
194: * @param keystorePassword Password to unlock the keystore. IMPORTANT: this implementation
195: * assumes that the same password is used to protect the key and the keystore itself.
196: * @param truststoreUrl URL of the truststore file. May be <tt>null</tt> if HTTPS server
197: * authentication is not to be used.
198: * @param truststorePassword Password to unlock the truststore.
199: */
200: public AuthSSLProtocolSocketFactory(final URL keystoreUrl,
201: final String keystorePassword, final URL truststoreUrl,
202: final String truststorePassword) {
203: super ();
204: this .keystoreUrl = keystoreUrl;
205: this .keystorePassword = keystorePassword;
206: this .truststoreUrl = truststoreUrl;
207: this .truststorePassword = truststorePassword;
208: }
209:
210: private static KeyStore createKeyStore(final URL url,
211: final String password) throws KeyStoreException,
212: NoSuchAlgorithmException, CertificateException, IOException {
213: if (url == null) {
214: throw new IllegalArgumentException(
215: "Keystore url may not be null");
216: }
217: LOG.debug("Initializing key store");
218: KeyStore keystore = KeyStore.getInstance("jks");
219: keystore.load(url.openStream(), password != null ? password
220: .toCharArray() : null);
221: return keystore;
222: }
223:
224: private static KeyManager[] createKeyManagers(
225: final KeyStore keystore, final String password)
226: throws KeyStoreException, NoSuchAlgorithmException,
227: UnrecoverableKeyException {
228: if (keystore == null) {
229: throw new IllegalArgumentException(
230: "Keystore may not be null");
231: }
232: LOG.debug("Initializing key manager");
233: KeyManagerFactory kmfactory = KeyManagerFactory
234: .getInstance(KeyManagerFactory.getDefaultAlgorithm());
235: kmfactory.init(keystore, password != null ? password
236: .toCharArray() : null);
237: return kmfactory.getKeyManagers();
238: }
239:
240: private static TrustManager[] createTrustManagers(
241: final KeyStore keystore) throws KeyStoreException,
242: NoSuchAlgorithmException {
243: if (keystore == null) {
244: throw new IllegalArgumentException(
245: "Keystore may not be null");
246: }
247: LOG.debug("Initializing trust manager");
248: TrustManagerFactory tmfactory = TrustManagerFactory
249: .getInstance(TrustManagerFactory.getDefaultAlgorithm());
250: tmfactory.init(keystore);
251: TrustManager[] trustmanagers = tmfactory.getTrustManagers();
252: for (int i = 0; i < trustmanagers.length; i++) {
253: if (trustmanagers[i] instanceof X509TrustManager) {
254: trustmanagers[i] = new AuthSSLX509TrustManager(
255: (X509TrustManager) trustmanagers[i]);
256: }
257: }
258: return trustmanagers;
259: }
260:
261: private SSLContext createSSLContext() {
262: try {
263: KeyManager[] keymanagers = null;
264: TrustManager[] trustmanagers = null;
265: if (this .keystoreUrl != null) {
266: KeyStore keystore = createKeyStore(this .keystoreUrl,
267: this .keystorePassword);
268: if (LOG.isDebugEnabled()) {
269: Enumeration aliases = keystore.aliases();
270: while (aliases.hasMoreElements()) {
271: String alias = (String) aliases.nextElement();
272: Certificate[] certs = keystore
273: .getCertificateChain(alias);
274: if (certs != null) {
275: LOG.debug("Certificate chain '" + alias
276: + "':");
277: for (int c = 0; c < certs.length; c++) {
278: if (certs[c] instanceof X509Certificate) {
279: X509Certificate cert = (X509Certificate) certs[c];
280: LOG.debug(" Certificate " + (c + 1)
281: + ":");
282: LOG.debug(" Subject DN: "
283: + cert.getSubjectDN());
284: LOG.debug(" Signature Algorithm: "
285: + cert.getSigAlgName());
286: LOG.debug(" Valid from: "
287: + cert.getNotBefore());
288: LOG.debug(" Valid until: "
289: + cert.getNotAfter());
290: LOG.debug(" Issuer: "
291: + cert.getIssuerDN());
292: }
293: }
294: }
295: }
296: }
297: keymanagers = createKeyManagers(keystore,
298: this .keystorePassword);
299: }
300: if (this .truststoreUrl != null) {
301: KeyStore keystore = createKeyStore(this .truststoreUrl,
302: this .truststorePassword);
303: if (LOG.isDebugEnabled()) {
304: Enumeration aliases = keystore.aliases();
305: while (aliases.hasMoreElements()) {
306: String alias = (String) aliases.nextElement();
307: LOG.debug("Trusted certificate '" + alias
308: + "':");
309: Certificate trustedcert = keystore
310: .getCertificate(alias);
311: if (trustedcert != null
312: && trustedcert instanceof X509Certificate) {
313: X509Certificate cert = (X509Certificate) trustedcert;
314: LOG.debug(" Subject DN: "
315: + cert.getSubjectDN());
316: LOG.debug(" Signature Algorithm: "
317: + cert.getSigAlgName());
318: LOG.debug(" Valid from: "
319: + cert.getNotBefore());
320: LOG.debug(" Valid until: "
321: + cert.getNotAfter());
322: LOG
323: .debug(" Issuer: "
324: + cert.getIssuerDN());
325: }
326: }
327: }
328: trustmanagers = createTrustManagers(keystore);
329: }
330: SSLContext sslcontext = SSLContext.getInstance("SSL");
331: sslcontext.init(keymanagers, trustmanagers, null);
332: return sslcontext;
333: } catch (NoSuchAlgorithmException e) {
334: LOG.error(e.getMessage(), e);
335: throw new AuthSSLInitializationError(
336: "Unsupported algorithm exception: "
337: + e.getMessage());
338: } catch (KeyStoreException e) {
339: LOG.error(e.getMessage(), e);
340: throw new AuthSSLInitializationError("Keystore exception: "
341: + e.getMessage());
342: } catch (GeneralSecurityException e) {
343: LOG.error(e.getMessage(), e);
344: throw new AuthSSLInitializationError(
345: "Key management exception: " + e.getMessage());
346: } catch (IOException e) {
347: LOG.error(e.getMessage(), e);
348: throw new AuthSSLInitializationError(
349: "I/O error reading keystore/truststore file: "
350: + e.getMessage());
351: }
352: }
353:
354: private SSLContext getSSLContext() {
355: if (this .sslcontext == null) {
356: this .sslcontext = createSSLContext();
357: }
358: return this .sslcontext;
359: }
360:
361: /**
362: * Attempts to get a new socket connection to the given host within the given time limit.
363: * <p>
364: * To circumvent the limitations of older JREs that do not support connect timeout a
365: * controller thread is executed. The controller thread attempts to create a new socket
366: * within the given limit of time. If socket constructor does not return until the
367: * timeout expires, the controller terminates and throws an {@link ConnectTimeoutException}
368: * </p>
369: *
370: * @param host the host name/IP
371: * @param port the port on the host
372: * @param clientHost the local host name/IP to bind the socket to
373: * @param clientPort the port on the local machine
374: * @param params {@link HttpConnectionParams Http connection parameters}
375: *
376: * @return Socket a new socket
377: *
378: * @throws IOException if an I/O error occurs while creating the socket
379: * @throws UnknownHostException if the IP address of the host cannot be
380: * determined
381: */
382: public Socket createSocket(final String host, final int port,
383: final InetAddress localAddress, final int localPort,
384: final HttpConnectionParams params) throws IOException,
385: UnknownHostException, ConnectTimeoutException {
386: if (params == null) {
387: throw new IllegalArgumentException(
388: "Parameters may not be null");
389: }
390: int timeout = params.getConnectionTimeout();
391: SocketFactory socketfactory = getSSLContext()
392: .getSocketFactory();
393: if (timeout == 0) {
394: return socketfactory.createSocket(host, port, localAddress,
395: localPort);
396: } else {
397: Socket socket = socketfactory.createSocket();
398: SocketAddress localaddr = new InetSocketAddress(
399: localAddress, localPort);
400: SocketAddress remoteaddr = new InetSocketAddress(host, port);
401: socket.bind(localaddr);
402: socket.connect(remoteaddr, timeout);
403: return socket;
404: }
405: }
406:
407: /**
408: * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
409: */
410: public Socket createSocket(String host, int port,
411: InetAddress clientHost, int clientPort) throws IOException,
412: UnknownHostException {
413: return getSSLContext().getSocketFactory().createSocket(host,
414: port, clientHost, clientPort);
415: }
416:
417: /**
418: * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int)
419: */
420: public Socket createSocket(String host, int port)
421: throws IOException, UnknownHostException {
422: return getSSLContext().getSocketFactory().createSocket(host,
423: port);
424: }
425:
426: /**
427: * @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean)
428: */
429: public Socket createSocket(Socket socket, String host, int port,
430: boolean autoClose) throws IOException, UnknownHostException {
431: return getSSLContext().getSocketFactory().createSocket(socket,
432: host, port, autoClose);
433: }
434: }
|