View Javadoc
1   /**
2    * This Source Code Form is subject to the terms of the Mozilla Public
3    * License, v. 2.0. If a copy of the MPL was not distributed with this
4    * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5    *
6    * If it is not possible or desirable to put the notice in a particular
7    * file, then You may include the notice in a location (such as a LICENSE
8    * file in a relevant directory) where a recipient would be likely to look
9    * for such a notice.
10  
11   * 
12   */
13   
14  /*  ---------------------------------------------------------------------------
15   *  U.S. Government, Department of the Army
16   *  Army Materiel Command
17   *  Research Development Engineering Command
18   *  Communications Electronics Research Development and Engineering Center
19   *  ---------------------------------------------------------------------------
20   */
21  
22  package org.miloss.fgsms.bueller;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.net.InetAddress;
26  import java.net.Socket;
27  import java.net.URL;
28  import java.net.UnknownHostException;
29  import java.security.GeneralSecurityException;
30  import java.security.KeyStore;
31  import java.security.KeyStoreException;
32  import java.security.NoSuchAlgorithmException;
33  import java.security.UnrecoverableKeyException;
34  import java.security.cert.Certificate;
35  import java.security.cert.CertificateException;
36  import java.security.cert.X509Certificate;
37  import java.util.Enumeration;
38  import org.apache.commons.logging.Log; 
39  import org.apache.commons.logging.LogFactory;
40  
41  import javax.net.ssl.KeyManager;
42  import javax.net.ssl.KeyManagerFactory;
43  import javax.net.ssl.SSLContext;
44  import javax.net.ssl.TrustManager;
45  import javax.net.ssl.TrustManagerFactory;
46  /**
47   * <p>
48   * AuthSSLProtocolSocketFactory can be used to validate the identity of the HTTPS 
49   * server against a list of trusted certificates and to authenticate to the HTTPS 
50   * server using a private key. 
51   * </p>
52   * 
53   * <p>
54   * IF KEYSTORE OR KEYSTOREPASSWORD IS NULL, A CLIENT CERT WILL NOT BE USED
55   * 
56   * AuthSSLProtocolSocketFactory will enable server authentication when supplied with
57   * a {@link KeyStore truststore} file containg one or several trusted certificates. 
58   * The client secure socket will reject the connection during the SSL session handshake 
59   * if the target HTTPS server attempts to authenticate itself with a non-trusted 
60   * certificate.
61   * </p>
62   * 
63   * <p>
64   * Use JDK keytool utility to import a trusted certificate and generate a truststore file:    
65   *    <pre>
66   *     keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
67   *    </pre>
68   * </p>
69   * 
70   * <p>
71   * AuthSSLProtocolSocketFactory will enable client authentication when supplied with
72   * a {@link KeyStore keystore} file containg a private key/public certificate pair. 
73   * The client secure socket will use the private key to authenticate itself to the target 
74   * HTTPS server during the SSL session handshake if requested to do so by the server. 
75   * The target HTTPS server will in its turn verify the certificate presented by the client
76   * in order to establish client's authenticity
77   * </p>
78   * 
79   * <p>
80   * Use the following sequence of actions to generate a keystore file
81   * </p>
82   *   <ul>
83   *     <li>
84   *      <p>
85   *      Use JDK keytool utility to generate a new key
86   *      <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
87   *      For simplicity use the same password for the key as that of the keystore
88   *      </p>
89   *     </li>
90   *     <li>
91   *      <p>
92   *      Issue a certificate signing request (CSR)
93   *      <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
94   *     </p>
95   *     </li>
96   *     <li>
97   *      <p>
98   *      Send the certificate request to the trusted Certificate Authority for signature. 
99   *      One may choose to act as her own CA and sign the certificate request using a PKI 
100  *      tool, such as OpenSSL.
101  *      </p>
102  *     </li>
103  *     <li>
104  *      <p>
105  *       Import the trusted CA root certificate
106  *       <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre> 
107  *      </p>
108  *     </li>
109  *     <li>
110  *      <p>
111  *       Import the PKCS#7 file containg the complete certificate chain
112  *       <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre> 
113  *      </p>
114  *     </li>
115  *     <li>
116  *      <p>
117  *       Verify the content the resultant keystore file
118  *       <pre>keytool -list -v -keystore my.keystore</pre> 
119  *      </p>
120  *     </li>
121  *   </ul>
122  * <p>
123  * Example of using custom protocol socket factory for a specific host:
124  *     <pre>
125  *     Protocol authhttps = new Protocol("https",  
126  *          new AuthSSLProtocolSocketFactory(
127  *              new URL("file:my.keystore"), "mypassword",
128  *              new URL("file:my.truststore"), "mypassword"), 443); 
129  *
130  *     HttpClient client = new HttpClient();
131  *     client.getHostConfiguration().setHost("localhost", 443, authhttps);
132  *     // use relative url only
133  *     GetMethod httpget = new GetMethod("/");
134  *     client.executeMethod(httpget);
135  *     </pre>
136  * </p>
137  * <p>
138  * Example of using custom protocol socket factory per default instead of the standard one:
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  *     Protocol.registerProtocol("https", authhttps);
145  *
146  *     HttpClient client = new HttpClient();
147  *     GetMethod httpget = new GetMethod("https://localhost/");
148  *     client.executeMethod(httpget);
149  *     </pre>
150  * </p>
151  * @author <a href="mailto:oleg -at- ural.ru">Oleg Kalnichevski</a>
152  * 
153  * <p>
154  * DISCLAIMER: HttpClient developers DO NOT actively support this component.
155  * The component is provided as a reference material, which may be inappropriate
156  * for use without additional customization.
157  * </p>
158  */
159 class AuthSSLProtocolSocketFactory{// implements org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory {
160 
161    /** Log object for this class. */
162     private static final Log LOG = LogFactory.getLog(AuthSSLProtocolSocketFactory.class);
163 
164     private URL keystoreUrl = null;
165     private String keystorePassword = null;
166     private URL truststoreUrl = null;
167     private String truststorePassword = null;
168     private SSLContext sslcontext = null;
169 
170     /**
171      * Constructor for AuthSSLProtocolSocketFactory. Either a keystore or truststore file
172      * must be given. Otherwise SSL context initialization error will result.
173      * 
174      * @param keystoreUrl URL of the keystore file. May be <tt>null</tt> if HTTPS client
175      *        authentication is not to be used.
176      * @param keystorePassword Password to unlock the keystore. IMPORTANT: this implementation
177      *        assumes that the same password is used to protect the key and the keystore itself.
178      * @param truststoreUrl URL of the truststore file. May be <tt>null</tt> if HTTPS server
179      *        authentication is not to be used.
180      * @param truststorePassword Password to unlock the truststore.
181      */
182     public AuthSSLProtocolSocketFactory(
183         final URL keystoreUrl, final String keystorePassword, 
184         final URL truststoreUrl, final String truststorePassword)
185     {
186         super();
187         this.keystoreUrl = keystoreUrl;
188         this.keystorePassword = keystorePassword;
189         this.truststoreUrl = truststoreUrl;
190         this.truststorePassword = truststorePassword;
191     }
192 
193     private static KeyStore createKeyStore(final URL url, final String password) 
194         throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException
195     {
196         if (url == null) {
197             throw new IllegalArgumentException("Keystore url may not be null");
198         }
199         LOG.debug("Initializing key store");
200         KeyStore keystore  = KeyStore.getInstance("jks");
201         InputStream is = null;
202         try {
203         	is = url.openStream(); 
204             keystore.load(is, password != null ? password.toCharArray(): null);
205         } finally {
206             try{
207         	if (is != null) is.close();
208             }catch (Exception ex){}
209         }
210         return keystore;
211     }
212     
213     private static KeyManager[] createKeyManagers(final KeyStore keystore, final String password)
214         throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException 
215     {
216         if (keystore == null) {
217             throw new IllegalArgumentException("Keystore may not be null");
218         }
219         LOG.debug("Initializing key manager");
220         KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
221             KeyManagerFactory.getDefaultAlgorithm());
222         kmfactory.init(keystore, password != null ? password.toCharArray(): null);
223         return kmfactory.getKeyManagers(); 
224     }
225 
226     private static TrustManager[] createTrustManagers(final KeyStore keystore)
227         throws KeyStoreException, NoSuchAlgorithmException
228     { 
229         if (keystore == null) {
230             throw new IllegalArgumentException("Keystore may not be null");
231         }
232         LOG.debug("Initializing trust manager");
233           String alg = KeyManagerFactory.getDefaultAlgorithm();
234         TrustManagerFactory fac = TrustManagerFactory.getInstance(alg);
235         fac.init(keystore);
236         return fac.getTrustManagers();
237         /*
238         TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
239             TrustManagerFactory.getDefaultAlgorithm());
240         tmfactory.init(keystore);
241         TrustManager[] trustmanagers = tmfactory.getTrustManagers();
242         for (int i = 0; i < trustmanagers.length; i++) {
243             if (trustmanagers[i] instanceof X509TrustManager) {
244                 trustmanagers[i] = new AuthSSLX509TrustManager(
245                     (X509TrustManager)trustmanagers[i]); 
246             }
247         }
248         return trustmanagers; */
249     }
250 
251     private SSLContext createSSLContext()  {
252         try {
253             KeyManager[] keymanagers = null;
254             TrustManager[] trustmanagers = null;
255             if (this.keystoreUrl != null) {
256                 KeyStore keystore = createKeyStore(this.keystoreUrl, this.keystorePassword);
257                 if (LOG.isDebugEnabled()) {
258                     Enumeration aliases = keystore.aliases();
259                     while (aliases.hasMoreElements()) {
260                         String alias = (String)aliases.nextElement();                        
261                         Certificate[] certs = keystore.getCertificateChain(alias);
262                         if (certs != null) {
263                             LOG.debug("Certificate chain '" + alias + "':");
264                             for (int c = 0; c < certs.length; c++) {
265                                 if (certs[c] instanceof X509Certificate) {
266                                     X509Certificate cert = (X509Certificate)certs[c];
267                                     LOG.debug(" Certificate " + (c + 1) + ":");
268                                     LOG.debug("  Subject DN: " + cert.getSubjectDN());
269                                     LOG.debug("  Signature Algorithm: " + cert.getSigAlgName());
270                                     LOG.debug("  Valid from: " + cert.getNotBefore() );
271                                     LOG.debug("  Valid until: " + cert.getNotAfter());
272                                     LOG.debug("  Issuer: " + cert.getIssuerDN());
273                                 }
274                             }
275                         }
276                     }
277                 }
278                 keymanagers = createKeyManagers(keystore, this.keystorePassword);
279             }
280             if (this.truststoreUrl != null) {
281                 KeyStore keystore = createKeyStore(this.truststoreUrl, this.truststorePassword);
282                 if (LOG.isDebugEnabled()) {
283                     Enumeration aliases = keystore.aliases();
284                     while (aliases.hasMoreElements()) {
285                         String alias = (String)aliases.nextElement();
286                         LOG.debug("Trusted certificate '" + alias + "':");
287                         Certificate trustedcert = keystore.getCertificate(alias);
288                         if (trustedcert != null && trustedcert instanceof X509Certificate) {
289                             X509Certificate cert = (X509Certificate)trustedcert;
290                             LOG.debug("  Subject DN: " + cert.getSubjectDN());
291                             LOG.debug("  Signature Algorithm: " + cert.getSigAlgName());
292                             LOG.debug("  Valid from: " + cert.getNotBefore() );
293                             LOG.debug("  Valid until: " + cert.getNotAfter());
294                             LOG.debug("  Issuer: " + cert.getIssuerDN());
295                         }
296                     }
297                 }
298                 trustmanagers = createTrustManagers(keystore);
299             }
300             SSLContext sslcontext = SSLContext.getInstance("SSL");
301             sslcontext.init(keymanagers, trustmanagers, null);
302             return sslcontext;
303         } catch (NoSuchAlgorithmException e) {
304             LOG.error(e.getMessage(), e);
305            // throw new AuthSSLInitializationError("Unsupported algorithm exception: " + e.getMessage());
306         } catch (KeyStoreException e) {
307             LOG.error(e.getMessage(), e);
308           //  throw new AuthSSLInitializationError("Keystore exception: " + e.getMessage());
309         } catch (GeneralSecurityException e) {
310             LOG.error(e.getMessage(), e);
311            // throw new AuthSSLInitializationError("Key management exception: " + e.getMessage());
312         } catch (IOException e) {
313             LOG.error(e.getMessage(), e);
314          //   throw new AuthSSLInitializationError("I/O error reading keystore/truststore file: " + e.getMessage());
315         }
316         return null;
317     }
318 
319     private SSLContext getSSLContext()  {
320         if (this.sslcontext == null) {
321             this.sslcontext = createSSLContext();
322         }
323         return this.sslcontext;
324     }
325 
326     /**
327      * Attempts to get a new socket connection to the given host within the given time limit.
328      * <p>
329      * To circumvent the limitations of older JREs that do not support connect timeout a 
330      * controller thread is executed. The controller thread attempts to create a new socket 
331      * within the given limit of time. If socket constructor does not return until the 
332      * timeout expires, the controller terminates and throws an {@link ConnectTimeoutException}
333      * </p>
334      *  
335      * @param host the host name/IP
336      * @param port the port on the host
337      * @param clientHost the local host name/IP to bind the socket to
338      * @param clientPort the port on the local machine
339      * @param params {@link HttpConnectionParams Http connection parameters}
340      * 
341      * @return Socket a new socket
342      * 
343      * @throws IOException if an I/O error occurs while creating the socket
344      * @throws UnknownHostException if the IP address of the host cannot be
345      * determined
346      
347     public Socket createSocket(
348         final String host,
349         final int port,
350         final InetAddress localAddress,
351         final int localPort,
352         final HttpConnectionParams params
353     ) throws IOException, UnknownHostException, ConnectTimeoutException {
354         if (params == null) {
355             throw new IllegalArgumentException("Parameters may not be null");
356         }
357         int timeout = params.getConnectionTimeout();
358         SocketFactory socketfactory = getSSLContext().getSocketFactory();
359         if (timeout == 0) {
360             return socketfactory.createSocket(host, port, localAddress, localPort);
361         } else {
362             Socket socket = socketfactory.createSocket();
363             SocketAddress localaddr = new InetSocketAddress(localAddress, localPort);
364             SocketAddress remoteaddr = new InetSocketAddress(host, port);
365             socket.bind(localaddr);
366             socket.connect(remoteaddr, timeout);
367             return socket;
368         }
369     }
370 */
371     /**
372      * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int,java.net.InetAddress,int)
373      */
374     public Socket createSocket(
375         String host,
376         int port,
377         InetAddress clientHost,
378         int clientPort)
379         throws IOException, UnknownHostException
380    {
381        return getSSLContext().getSocketFactory().createSocket(
382             host,
383             port,
384             clientHost,
385             clientPort
386         );
387     }
388 
389     /**
390      * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int)
391      */
392     public Socket createSocket(String host, int port)
393         throws IOException, UnknownHostException
394     {
395         return getSSLContext().getSocketFactory().createSocket(
396             host,
397             port
398         );
399     }
400 
401     /**
402      * @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean)
403      */
404     public Socket createSocket(
405         Socket socket,
406         String host,
407         int port,
408         boolean autoClose)
409         throws IOException, UnknownHostException
410     {
411         return getSSLContext().getSocketFactory().createSocket(
412             socket,
413             host,
414             port,
415             autoClose
416         );
417     }
418     
419 }