Recently I had the opportunity to implement 2 way authentication between a java server and third party. This post will document the steps necessary to implement two way authentication when your java server is acting as the “client”.
In the following steps when I say “third party” I am talking about the server we are performing the mutual authentication with.
In our particular scenario we generated a certificate signing request (CSR) for the third party using our own private key. The third party then took our CSR and generated a certificate for us. The third party then installed that certificate on their server and returned it to us so that we could install it as well in our application. Our client application was java so we know that we somehow needed to get that certificate into our keystore and then have that keystore send it to the server when necessary.
So first step is to get the cert and private key into your keystore: You will need : openssl, your private key (private.key), the certificate generated from the csr (certificate.cer), keytool, and a keystore
The following command will create a p12 file containing both the private key and the certificate
openssl pkcs12 -export -out output.p12 -inkey private.key -in certificate.cer -name unique-alias-name
Now that you have the file (output.p12) you can import it into your keystore.
keytool -v -importkeystore -srckeystore output.p12 -srcstoretype PKCS12 -destkeystore keystore.jks -deststoretype JKS
Notice the value of destkeystore can be the name of abrand new keystore or an existing keystore
MAKE SURE KEY PASSWORD AND KEYSTORE PASSWORD ARE THE SAME - if they are not, then java will be unable to read the private key when necessary.
Now that you have successfully added the private key and certificate to the keystore you will need to make sure it is loaded appropriately. You can do something like the following
String keystoreFileName = "keystore.jks"
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream is = null;
try
{
is = getClass().getResourceAsStream(keystoreFileName);
trustStore.load(is, "yourkeystorepassword".toCharArray());
}
finally
{
if (is != null)
is.close();
}
char[] pwd = "yourkeystorepassword".toCharArray();
SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(trustStore, pwd).loadTrustMaterial(trustStore).useTLS().build();
SSLConnectionSocketFactory connectionFactory = new SSLConnectionSocketFactory(sslContext, new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }, null,
new AllowAllHostnameVerifier());
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
HttpClient httpClient = HttpClientBuilder.create().setSSLSocketFactory(connectionFactory)
.setDefaultCredentialsProvider(credentialsProvider).build();
The above code snippet will create an httpClient that has been initialzed with an ssl context containing all necessary certs and keys from the keystore provided. Now when you use the httpClient you would expect everything to work as expected (and based on all the articles you read online you will think that it should!). The mutual authentication should work because you have the necessary cert and private key in your keystore. Wrong. If you have multiple certificates in your keystore then java may have a hard time determining which one to send the server during the 2 way auth handshaking. You can use wireshark to see exactly what your client is sending during the request. In our case, this was the wrong certificate and it was causing us lots of grief. The solution for this final problem was to implement a FilteredKeyManager which lets you specify exactly which certificate to send based on the alias.
public class FilteredKeyManager implements X509KeyManager {
private final X509KeyManager originatingKeyManager;
private final X509Certificate[] x509Certificates;
public FilteredKeyManager(X509KeyManager originatingKeyManager, X509Certificate[] x509Certificates) {
this.originatingKeyManager = originatingKeyManager;
this.x509Certificates = x509Certificates;
}
public X509Certificate[] getCertificateChain(String alias) {
return x509Certificates;
}
public String[] getClientAliases(String keyType, Principal[] issuers) {
return new String[] {"DesiredClientCertAlias"};
}
You can tweak the above code to fit your needs. And then when you set up your context:
SSLContext context = SSLContext.getInstance("TLSv1");
context.init(new KeyManager[] { new FilteredKeyManager((X509KeyManager)originalKeyManagers[0], desiredCertsForConnection) },
trustManagerFactory.getTrustManagers(), new SecureRandom());
We created a generalized solution where at any time we can create a new ssl context based on an alias, or simply just grab a default ssl context (when only doing one way auth).
I hope this saves you some time! Please reach out if you have any questions. I know this was a challenging problem for me to work on