There are two different strategies to use ssl: simple (when traffic between client and server should only be secured) and custom (when client identify server by server's certificate or server identificate client by client's certificate or both identifies each other). As you can see second strategy is more complicated then first.
In Java logic of ssl communication incapsulated in SSLContext class. Let's construct instance of SSLContext for simple case of ssl. To do this we need implementation of Dummy trust manager:
package org.lehvolk.common.ssl; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import javax.net.ssl.X509TrustManager; /** * Trust manager, makes all hosts trusted */ public class DummyTrustManager implements X509TrustManager { /** * @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[], * java.lang.String) */ public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { } /** * @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[], * java.lang.String) */ public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { } /** * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() */ public X509Certificate[] getAcceptedIssuers() { return null; } }And code belove create dummy SSLConext:
SSLContext sslcontext = SSLContext.getInstance("SSL"); sslcontext.init(null, new TrustManager[] { new DummyTrustManager() }, null);
- enabled - if ssl enabled
- keyStorePath - path to keystore with client certificate.
- keyStorePassword - password to key store.
- trustStorePath - trust store path with trusted server certificate.
- trustStorePassword - password for trust store.
- verifyHost - verify host. it needs to verify that host in certificate and request host equals.
- checkHostTrusted - check that server is trusted.
package org.lehvolk.common.ssl; import javax.xml.bind.JAXB; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement(name = "ssl-configuration") @XmlAccessorType(XmlAccessType.FIELD) public class SSLConfiguration { @XmlElement(name = "keystore-path", required = false) private String keyStorePath; @XmlElement(name = "keystore-password", required = false) private String keyStorePassword; @XmlElement(name = "truststore-path", required = false) private String trustStorePath; @XmlElement(name = "truststore-password", required = false) private String trustStorePassword; @XmlElement(name = "enabled", required = false) private boolean enabled = false; @XmlElement(name = "verify-host", required = false) private boolean verifyHost = false; @XmlElement(name = "check-host-trusted", required = false) private boolean checkHostTrusted = false; // getters and setters }As you can see this configuration can be stored in xml file. So we can write xml file with configuration and use it in out application:
Also we need implemetation of dummy host verifier:/path/client.keystore password /path/server.keystore password false false false
package org.lehvolk.common.ssl; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; public class DummyHostVerifier implements HostnameVerifier { public boolean verify(String name, SSLSession sess) { return true; } }The following util class has method which create SSLContext from SSLConfiguration.
package org.lehvolk.common.ssl; import java.io.FileInputStream; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.CertificateException; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; /** * Utility class for working with SSL */ public class SSLUtils { public static final HostnameVerifier dummyHostVerifier = new DummyHostVerifier(); public static final X509TrustManager dummyTrustManager = new DummyTrustManager(); /** Internal JAXWS property name, allows setup own implementation of HostnameVerifier */ public static final String JAXWS_HOSTNAME_VERIFIER = "com.sun.xml.internal.ws.transport.https.client.hostname.verifier"; /** Internal JAXWS property name, allows setup own implementation of SSLSocketFactory */ public static final String JAXWS_SSL_SOCKET_FACTORY = "com.sun.xml.internal.ws.transport.https.client.SSLSocketFactory"; /** * Creates {@link SSLContext} according with specified parameters * * @param conf - {@link SSLConfiguration} * @return initialized SSLSocketFactory * @throws SSLException if error while building SSLContext occurred */ public static SSLContext createSSLContext(SSLConfiguration conf) throws SSLException { try { KeyManager[] keyManagers = null; if (conf.getKeyStorePath() != null) { KeyStore keyStore = loadKeyStore(conf.getKeyStorePath(), conf.getKeyStorePassword()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, conf.getKeyStorePassword().toCharArray()); keyManagers = keyManagerFactory.getKeyManagers(); } TrustManager[] trustManagers = null; if (conf.getCheckHostTrusted()) { if (conf.getTrustStorePath() != null) { KeyStore trustStore; trustStore = loadKeyStore(conf.getTrustStorePath(), conf.getTrustStorePassword()); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory .getDefaultAlgorithm()); trustManagerFactory.init(trustStore); trustManagers = trustManagerFactory.getTrustManagers(); } } else { trustManagers = new TrustManager[] { dummyTrustManager }; } SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG", "SUN"); SSLContext sslContext = SSLContext.getInstance("SSL"); sslContext.init(keyManagers, trustManagers, secureRandom); return sslContext; } catch (Exception e) { throw new SSLException("Can't create SSLContext", e); } } /** * Creates {@link SSLContext} * * @return initialized SSLSocketFactory * @throws SSLException if error while building SSLContext occurred */ public static SSLContext createDummySSLContext() throws SSLException { try { SSLContext sslcontext = SSLContext.getInstance("SSL"); sslcontext.init(null, new TrustManager[] { dummyTrustManager }, null); return sslcontext; } catch (Exception e) { throw new SSLException("Can't create SSLContext", e); } } /** * @return {@link HostnameVerifier} implementation witch allow all hosts for certificate */ public static HostnameVerifier getAllowAllHostsVerifier() { return dummyHostVerifier; } /** * load key store * * @param keyStorePath path to key store * @param keyStorePass key store pass * @return KeyStore instance * @throws KeyStoreException key store exception * @throws NoSuchAlgorithmException if can't read * @throws CertificateException if can't read * @throws IOException if can't read */ //Initialize KeyStore from specified path and using given password public static KeyStore loadKeyStore(String keyStorePath, String keyStorePass) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { FileInputStream fis = null; try { KeyStore keyStore = KeyStore.getInstance("JKS"); fis = new FileInputStream(keyStorePath); keyStore.load(fis, keyStorePass != null ? keyStorePass.toCharArray() : null); return keyStore; } finally { if (fis != null) { fis.close(); } } } }Now let see how to use this. I want to write simple http client (how to use ssl over soap services I will discribe in another post). I choose apache httpcomponents as a framework for http/https implementation. In apache http components there is implementation of thread safe connection manager which pool and reuse http/https connections. It's realy good solution because establishing https connection has big cost, so re-open https connection on every request may affect the performance of application.
To have ability to configure connection manager and ssl setting lets make HttpConfiguration pojo class.
package org.lehvolk.common.http; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import org.lehvolk.common.ssl.SSLConfiguration; /** * configuration for http client */ @XmlRootElement(name = "http-configuration") @XmlAccessorType(XmlAccessType.FIELD) public class HttpConfiguration { // connection timeout in milliseconds @XmlElement(name = "conn-timeout", required = false) private Integer connectionTimeout; @XmlElement(name = "max-connections") private Integer maxConnections; @XmlElement(name = "max-connections-per-host", required = false) private Integer maxConnectionsPerHost; @XmlElement(name = "ssl-configuration", required = false) private SSLConfiguration sslConfiguration; //getters and setters }Now using this configuration (as you can see you can store it in xml file) we can write thread safe http provider.
package org.lehvolk.common.http; import java.io.IOException; import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.conn.params.ConnManagerParams; import org.apache.http.conn.params.ConnPerRouteBean; import org.apache.http.conn.scheme.PlainSocketFactory; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.HttpParams; import org.lehvolk.common.ssl.DummyTrustManager; import org.lehvolk.common.ssl.SSLConfiguration; import org.lehvolk.common.ssl.SSLUtils; /** * Http provider */ public class HttpProvider { private HttpParams httpParameters; private ThreadSafeClientConnManager cm; private DefaultHttpClient client; /** * @param configuration configuration */ public HttpProvider(HttpConfiguration configuration) throws SSLException { httpParameters = getParams(configuration); SchemeRegistry registry = new SchemeRegistry(); try { SSLConfiguration sslConfig = configuration.getSslConfiguration(); Scheme[] schemes = getSchemes(sslConfig); for (Scheme scheme : schemes) { registry.register(scheme); } // creates thread safe manager for http connections cm = new ThreadSafeClientConnManager(httpParameters, registry); client = new DefaultHttpClient(cm, httpParameters); } catch (Exception e) { throw new SSLException("Error initializing http provider", e); } } private Scheme[] getSchemes(SSLConfiguration sslConfig) throws SSLException { try { if (sslConfig == null || !sslConfig.getEnabled()) { SSLContext sslcontext = SSLContext.getInstance("SSL"); sslcontext.init(null, new TrustManager[] { new DummyTrustManager() }, null); SSLSocketFactory dummyFactory = new SSLSocketFactory(sslcontext); dummyFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); Scheme[] schemes = new Scheme[2]; schemes[0] = new Scheme("http", new PlainSocketFactory(), 80); schemes[1] = new Scheme("https", dummyFactory, 443); return schemes; } SSLContext ctx = SSLUtils.createSSLContext(sslConfig); SSLSocketFactory sf = new SSLSocketFactory(ctx); sf.setHostnameVerifier(sslConfig.getVerifyHost() ? SSLSocketFactory.STRICT_HOSTNAME_VERIFIER : SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); Scheme[] schemes = new Scheme[1]; schemes[0] = new Scheme("https", sf, 443); return schemes; } catch (Exception e) { throw new SSLException(e); } } private HttpParams getParams(HttpConfiguration configuration) { HttpParams params = new BasicHttpParams(); Integer maxConnections = configuration.getMaxConnections(); Integer maxPerHostConnections = configuration.getMaxConnectionsPerHost(); if (maxPerHostConnections == null) { maxPerHostConnections = maxConnections; } // Increase max total connection ConnManagerParams.setMaxTotalConnections(params, maxConnections); // Increase connection per route ConnPerRouteBean connPerRoute = new ConnPerRouteBean(maxPerHostConnections); ConnManagerParams.setMaxConnectionsPerRoute(params, connPerRoute); ConnManagerParams.setTimeout(params, configuration.getConnectionTimeout()); return params; } /** * {@inheritDoc} */ public HttpResponse executeRequest(HttpUriRequest request) throws ClientProtocolException, IOException { return client.execute(request); } /** * {@inheritDoc} */ public void safelyShutdown() { new Thread() { @Override public void run() { if (cm != null) { while (cm.getConnectionsInPool() > 0) { cm.closeExpiredConnections(); cm.closeIdleConnections(10, TimeUnit.MILLISECONDS); try { sleep(2000); } catch (InterruptedException e) { // ignore } } cm.shutdown(); } }; }.start(); } /** * {@inheritDoc} */ public void shutdown() { if (cm != null) { cm.shutdown(); } } }Whats happend in this class. If we don't have the ssl confguration or it's not enabled then HttpProvider makes requests under http or simple https (without customized certificates). If we need to call some https service with specified certificate we should specify enabled property of SSLConfiguration in HttpConfiguration. In this situation provider make calls only under https with specified ssl configuration. Also we use verifyHost property of SSLConfiguration when point which host verifier use in SSLSocketFactory instance.
When provider instance doesn't needed anymore then shutdown or safetyShoutdown method should be called. I should notice that create instance of provider for single call is very expensive. Best practice is to create instance of HttpProvider and cached them into classic signleton or ejb sigleton bean. Also don't forget to clean up entity in HttpResponse.
No comments:
Post a Comment