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