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