I will show you how you can do both below.
First, some background. Java has a basic API to make a simple HTTP connection to any URL via URL.openConnection(). If your URL uses the "http" protocol, it is very simple to use this to make basic HTTP connections.
Problems creep in when you want a secure connection over SSL (via the "https" protocol). You can still use that API - URL.openConnection() will return a HttpsURLConnection if the URL uses the https protocol - however, you must ensure your JVM can find and access your truststore in order to authenticate the remote server's certificate.
[note: I won't discuss how you get your trusted certificates and how you put them in your truststore - I'll assume you know, or can find out, how to do this.]
You tell your JVM where your truststore is by setting the system property "javax.net.ssl.trustStore" and you tell your JVM how to access your truststore by giving your JVM the password via the system property "javax.net.ssl.trustStorePassword".
The problem is these are global settings (you often see instructions telling you to set these values via the -D command line arguments when starting your Java process) so everything running in your JVM must use that truststore. And you can't alter those system properties during runtime and expect those changes to take effect. Once you ask the JVM to make a secure connection, those system property values appear to be cached in the JVM and are used thereafter for the life of the JVM (I don't know exactly where in the JRE code these values are cached, but my experience shows me that they are). Changing those system properties later on in the lifetime of the JVM has no effect; the original values are forever used.
Another problem that some people run into is having the need for a truststore in the first place. Sometimes you don't have a requirement to authenticate the server endpoint; however, you would still like to send your data encrypted over the wire. You can't do this readily since the connection you obtain from URL.openConnection() will, by default, expect to use your truststore located at the path pointed to by the system property javax.net.ssl.trustStore.
To allow me to use different truststores for different connections, or to allow me to encrypt a connection but not authenticate the endpoint, I wrote a Java utility object that allows you to do just this.
The main constructor is this:
public SecureConnector(String secureSocketProtocol,
File truststoreFile,
String truststorePassword,
String truststoreType,
String truststoreAlgorithm)
You pass it a secure socket protocol (such as "TLS") and your truststore file location. If the truststore file is null, the SecureConnector object will assume you do not want to authenticate the remote server endpoint and you only want to encrypt your over-the-wire traffic. If you do provide a truststore file, you need to provide its password, its type (e.g. "JKS"), and its algorithm (e.g. "SunX509") - if you pass in null for type and/or algorithm, the JVM defaults are used.
Once you create the object, just obtain a secure connection to any URL via a call to SecureConnector.openSecureConnection(URL). This expects your URL to have a protocol of "https". If successful, an HttpsURLConnection object is returned and you can use it like any other connection object. You do not need to set javax.net.ssl.trustStore (or any other javax.net.ssl system property) and, as explained above, you don't even need to provide a truststore at all (assuming you don't need to do any authentication).
The code for this is found inside of RHQ's agent - you can read its javadoc and look through SecureConnector code here.
The core code is found in openSecureConnection and looks like this, I'll break it down:
First, it simply obtains the HTTPS connection object from the URL itself:
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();Then it prepares a custom SSLContext object using the given secure socket protocol:
TrustManager[] trustManagers;If no truststore file was provided, it will build its own "no-op" trust manager and "no-op" hostname verifier. What these "no-op" objects will do is always accept all certificates and hostnames thus they will always allow the SSL communications to flow. This is how the authentication is by-passed:
SSLContext sslContext = SSLContext.getInstance(getSecureSocketProtocol());
if (getTruststoreFile() == null) {If a truststore file was provided, then it will be loaded in memory and stored in a KeyStore instance:
// configured to not care about authenticating server, encrypt but don't worry about certificates
trustManagers = new TrustManager[] { NO_OP_TRUST_MANAGER };
connection.setHostnameVerifier(NO_OP_HOSTNAME_VERIFIER);
} else {The truststore file's content (now stored in a KeyStore object) is used to initialize a trust manager. Unlike the "no-op" trust manager that was created above (if a truststore file was not provided), this trust manager really does perform authentication and it uses the provided truststore's certificates to authorize the server being communicated with. This is why we no longer need to worry about the system properties "javax.net.ssl.trustStore" and "javax.net.ssl.trustStorePassword" - this builds its own trust manager using the data provided by the caller:
// need to configure SSL connection with truststore so we can authenticate the server.
// First, create a KeyStore, but load it with our truststore entries.
KeyStore keyStore = KeyStore.getInstance(getTruststoreType());
keyStore.load(new FileInputStream(getTruststoreFile()), getTruststorePassword().toCharArray());
// create truststore manager and initialize it with KeyStore we created with all truststore entriesFinally, the SSL context is initialized with the trust manager that was created earlier (either the "no-op" trust manager, or the trust manager that was initialized with the truststore's certificates). That SSL context is handed off to the SSL connection so the connection can use the context when it needs to perform authentication:
TrustManagerFactory tmf = TrustManagerFactory.getInstance(getTruststoreAlgorithm());
tmf.init(keyStore);
trustManagers = tmf.getTrustManagers();
}
sslContext.init(null, trustManagers, null);The connection is finally returned to the caller, fully configured and ready to be used.
connection.setSSLSocketFactory(sslContext.getSocketFactory());
return connection;This is helpful for certain use cases. First, it is helpful when you have multiple truststores that you need to choose from when connecting to different servers as well as being able to switch truststores at runtime (remember, the system property values of javax.net.ssl.trustStore, et. al. are fixed for the lifetime of the JVM - this helps bypass that restriction). This is also helpful in local testing, debugging and demo scenarios when you don't really need or care about setting up truststores and certificates but you do want to connect over https.
Where does that NO_OP_TRUST_MANAGER comes from? I always built my own to connect to mail servers (customers don't care about keeping certificates in internal LAN)
ReplyDeleteSolerman - take a look at the source - its a private static class defined inside this SecureConnector class. It's nothing special - just a no-op implementation of javax.net.ssl.X509TrustManager.
ReplyDelete