Despite SSL being widely used, Java mutual SSL authentication (also referred to as 2-way SSL authentication or certificate based authentication) is a fairly simple implementation when understanding the key concepts of how mutual SSL authentication works.
Today I noticed that a relatively simple concept as this is widely misunderstood and people seem to really struggle implementing 10 lines of code.
A good primer is to first read the JSSE – Java Secure Socket Extension Reference to understand the capabilities and flexibility within Java. It is also good to understand SSLConnectionSocketFactory as we will be using it. With that in mind, you will then also understand the following concepts required to make mutual authentication work:
The Truststore
The Truststore contains the certificates you use to authenticate / trust other servers. Every Java install comes with it’s own truststore located in $JAVA_HOME/lib/security/cacerts. In our case we will create our own Truststore which will then store trusted CA (Certificate Authority) entries and/or self-signed certificates from third parties we trust.
The Identitystore
The Identitystore is a secure store for keys used in the SSL protocol. We use the Identitystore to store our private keys and their associated certificates used to authenticate ourselves as the client to a server. In mutual SSL authentication we (our Java client) needs to authenticate with the server.
The Java keytool
Java provides the command-line tool “keytool” which we will use in conjunction with openssl to create the above keystores and/or convert certificates.
A note about security: In the example and scripts below we will use hard-coded, simple passwords and key files without passwords. In a production environment you will typically sandbox the key generation and would access passwords from configuration stores and not hardcode in code.
Use Curl to check your certs
When you integrate mutual SSL authentication with a third party, you will typically generate a CSR (Certificate Signing Request) with your private key. The third party will then issue you with a client certificate (and typically will often provide you with CA certificate). Before we dive into the coding part, let’s use good old curl to validate the request
curl -X POST -d '{"someparam1":"somevalue1","someparam2":"somevalue2"}' \ -H "Content-Type: application/json" \ -k https://www.example.com/endpoint \ --cert myclient.cert.pem \ --key myclient.key
The above request uses a JSON HTTP post against a 3rd-party end-point using our client-certificate and client key. If this request succeeds without an error or SSL-handshake issue, we can almost start to code.
Note: Replace the variable $CERT_ALIAS with the actual certificate alias. Use your own password in place of $CERT_PASSWORD.
Generate the TrustStore
If your 3rd party does not provide you with their server certificate, you can easily retrieve it via openssl (otherwise skip the first step):
### 1) If we do not have the server certificate, we use openssl to retrieve it echo -n | openssl s_client -connect SERVERDOMAIN:PORT -servername SERVERDOMAIN \ -key myclient.key -cert myclient.cert.pem \ | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' \ | tee "server.crt" ### 2) Create the Truststore from them server certificate keytool -import -alias $CERT_ALIAS -file server.crt -keystore truststore.jks -deststorepass $CERT_PASSWORD
Generate the IdentityStore
In the IdentityStore will put our private key, our certificate and the CA chain under an alias which our client is going to use to authenticate itself with the server:
### 1) Concatenate all certificates into one PEM file cat intermediate.cert.pem myclient.cert.pem myclient.key > full-chain.keycert.pem ### 2) Generate the PKCS12 keystore with the alias of the server url openssl pkcs12 -export -in full-chain.keycert.pem \ -out full-chain.keycert.p12 \ -password env:$CERT_PASSWORD \ -name $CERT_ALIAS \ -noiter -nomaciter ### 3) Convert .p12 to .jks keytool -importkeystore -srckeystore full-chain.keycert.p12 \ -srcstoretype pkcs12 -srcalias $CERT_ALIAS -srcstorepass $CERT_PASSWORD \ -destkeystore identity.jks -deststoretype jks \ -deststorepass $CERT_PASSWORD -destalias $CERT_ALIAS
Let’s code
The code itself is pretty simple and requires not much explanation. We load the identitystore and truststore into a custom SSL context, create a SSLConnectionSocketFactory and then bind it to a HttpClient.
public class SSLMutualAuthTest { publicSSLMutualAuthTest() { // TODO Auto-generated constructor stub } public static void main (String[] args) { System.out.println("MagicDude4Eva 2-way / mutual SSL-authentication test"); org.apache.log4j.BasicConfigurator.configure(); Logger.getRootLogger().setLevel(Level.INFO); try { String CERT_ALIAS = "myalias", CERT_PASSWORD = "mypassword"; KeyStore identityKeyStore = KeyStore.getInstance("jks"); FileInputStream identityKeyStoreFile = new FileInputStream(new File("identity.jks")); identityKeyStore.load(identityKeyStoreFile, CERT_PASSWORD.toCharArray()); KeyStore trustKeyStore = KeyStore.getInstance("jks"); FileInputStream trustKeyStoreFile = new FileInputStream(new File("truststore.jks")); trustKeyStore.load(trustKeyStoreFile, CERT_PASSWORD.toCharArray()); SSLContext sslContext = SSLContexts.custom() // load identity keystore .loadKeyMaterial(identityKeyStore, CERT_PASSWORD.toCharArray(), new PrivateKeyStrategy() { @Override public String chooseAlias(Map<String, PrivateKeyDetails> aliases, Socket socket) { return CERT_ALIAS; } }) // load trust keystore .loadTrustMaterial(trustKeyStore, null) .build(); SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new String[]{"TLSv1.2", "TLSv1.1"}, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier()); CloseableHttpClient client = HttpClients.custom() .setSSLSocketFactory(sslConnectionSocketFactory) .build(); // Call a SSL-endpoint callEndPoint (client, "https://secure.server.com/endpoint", new JSONObject() .put("param1", "value1") .put("param2", "value2") ); } catch (Exception ex) { System.out.println("Boom, we failed: " + ex); ex.printStackTrace(); } } private static void callEndPoint (CloseableHttpClient aHTTPClient, String aEndPointURL, JSONObject aPostParams) { try { System.out.println("Calling URL: " + aEndPointURL); HttpPost post = new HttpPost(aEndPointURL); post.setHeader("Accept", "application/json"); post.setHeader("Content-type", "application/json"); StringEntity entity = new StringEntity(aPostParams.toString()); post.setEntity(entity); System.out.println("**POST** request Url: " + post.getURI()); System.out.println("Parameters : " + aPostParams); HttpResponse response = aHTTPClient.execute(post); int responseCode = response.getStatusLine().getStatusCode(); System.out.println("Response Code: " + responseCode); System.out.println("Content:-\n"); BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); String line = ""; while ((line = rd.readLine()) != null) { System.out.println(line); } } catch (Exception ex) { System.out.println("Boom, we failed: " + ex); ex.printStackTrace(); } } }
If the above has saved you from trawling through endless Stackoverflow posts for days and you have failed hacking away at code without much success, why don’t you throw some money at the screen:
🍺 Pay it forward: If any of my content helped you in any way, then follow me on Twitter or send me some coins:
(CRO) cro1w2kvwrzp23aq54n3amwav4yy4a9ahq2kz2wtmj (Memo: 644996249) or 0xb83c3Fe378F5224fAdD7a0f8a7dD33a6C96C422C (Cronos)
(USDC) 0xb83c3Fe378F5224fAdD7a0f8a7dD33a6C96C422C
(BTC) 3628nqihXvw2RXsKtTR36dN6WvYzaHyr52
(ETH) 0xb83c3Fe378F5224fAdD7a0f8a7dD33a6C96C422C
(BAT) 0xb83c3Fe378F5224fAdD7a0f8a7dD33a6C96C422C
(LTC) MQxRAfhVU84KDVUqnZ5eV9MGyyaBEcQeDf
(Ripple) rKV8HEL3vLc6q9waTiJcewdRdSFyx67QFb (Tag: 1172047832)
(XLM) GB67TJFJO3GUA432EJ4JTODHFYSBTM44P4XQCDOFTXJNNPV2UKUJYVBF (Memo ID: 1406379394)
🚀 Use my referral link https://crypto.com/app/ref6ayzqvp to sign up for Crypto.com and we both get $25 USD 😸
🧨 During signup use my referral code ref6ayzqvp to claim your reward 🧨
Go to Curve.com to add your Crypto.com card to ApplePay (get 1% cashback)