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 }