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 }