Using client side SSL/TLS certificates in Java with Apache HTTP Client



Using of TLS/SSL client certificates allows such features as: securely sign-in without passwords, uniquely identify to websites, simple and effective 2-factor authentication, and others. This technology is not new and actually known since the very first days of SSL in the Internet.

In this post we implement client-side certificates support on the client side. The server side is usually supported by a web server, such as NGINX, Apache, IIS, etc. The post is written assuming that the application needs to reach different servers, so it should be smart enough to use corresponding client certificate for appropriate server. I also assume that Apache HttpClient is used for accessing web sites by the application.

First of all, we need to store information regarding which certificate should be used for which website/server. Since we’re writing a Java application, let’s keep the information in an XML file (client-certs.xml):

<?xml version="1.0"?>
<CERTS>
	<CERT PASSWORD="hellothere" FILENAME="client.p12" HOSTNAME="server1.com"/>
	<CERT PASSWORD="cacti" FILENAME="myCert.pfx" HOSTNAME="www.server2.com"/>
</CERTS>

And here we have two client certificates descriptions: one will be used when we’re accessing server1.com and another one is for www.server2.com; also, each certificate has its own password.

As you see, we’re using not a plain certificates (like .crt) here, but instead we work with PKCS12 keystores (.pfx and .p12).

Now, we need to: - create our own keystore in runtime - parse the XML file and extract the certificates from the keystores - put the extracted certificates into our new generated keystore, so we could use it in further

The following method implements all the needs and returns an array of KeyManager objects. The key managers will be used then for detecting which certificate should be used for which website.

When we add a certificate to our keystore, we assign an alias for it, so then we could find it in the keystore by the alias. You can use whatever string you want as the alias, and here I use IP address of the corresponding hostname. Then, when request to a webserver will being made, the application will find appropriate certificate using the webserver’s IP address.

	private Set<String> clientCerts = new HashSet<>();
	
	public KeyManager[] getDefaultKeyManagers(String clientCertsConfig) {
	 KeyManager[] keyManagers = null;
	 try {
	   File f = new File(clientCertsConfig);
	   if(f.exists() && !f.isDirectory()) {
	     KeyStore keyStore = KeyStore.getInstance("PKCS12");
	     char[] password = "myKeyStorePassword".toCharArray();
	     keyStore.load(null, password);
	
	     XMLConfiguration xmlConfig = getConfigFromXMLFile(clientCertsConfig);
	     List<HierarchicalConfiguration<ImmutableNode>> certs =
	                               xmlConfig.configurationsAt("CERT");
	     for (HierarchicalConfiguration cert : certs) {
	       String certPassword = cert.getString("[@PASSWORD]");
	       String certFilename = cert.getString("[@FILENAME]");
	       String certHostname = cert.getString("[@HOSTNAME]");
	
	       InputStream certIs = new FileInputStream(certFilename);
	       KeyStore ks = KeyStore.getInstance("PKCS12");
	       ks.load(certIs, certPassword.toCharArray());
	       String alias = ks.aliases().nextElement();
	
	       String ip = InetAddress.getByName(certHostname).getHostAddress();
	
	       keyStore.setKeyEntry(ip, ks.getKey(alias,
	                certPassword.toCharArray()), password,
	                         ks.getCertificateChain(alias));
	
	       clientCerts.add(ip);
	     }
	     KeyManagerFactory keyManagerFactory =
	           KeyManagerFactory.getInstance("SunX509");
	     keyManagerFactory.init(keyStore, password);
	     keyManagers = keyManagerFactory.getKeyManagers();
	   } else {
	     log.warn("No client certificates configuration was found");
	   }
	 } catch (Exception e) {
	   log.error("Exception happened when creating key manager: {}", e);
	 }
	 return keyManagers;
	}
	
	public Set<String> getClientCertsAliases() {
	   return clientCerts;
	}
	
	private XMLConfiguration getConfigFromXMLFile(String filename)
	        throws ConfigurationException {
	 Parameters params = new Parameters();
	 FileBasedConfigurationBuilder<XMLConfiguration> builder =
	        new FileBasedConfigurationBuilder<>(XMLConfiguration.class)
	        .configure(params.xml()
	           .setFileName(filename)
	           .setLogger(ConfigurationLogger.newDummyLogger())
	           .setThrowExceptionOnMissing(true)
	           .setValidating(false));
	 return builder.getConfiguration();
	}

So, at this point we created our new keystore and loaded it up with client certificates we have. Now it’s the time to implement a custom key manager - it is a class that implements the main idea: it detects which certificate should be used for which web server.

The custom key manager implements X509KeyManager and takes the array of key managers we generated in the previous method given above. The “main” method here is chooseClientAlias - it detects if we have a client certificate for the certain web server and returns the alias if so; the alias will be used then for getting corresponding certificate from the keystore we generated previously (see the code above).

	class CustomKeyManager implements X509KeyManager {
	  private X509KeyManager defaultKeyManager;
	  private Set<String> knownAliases;
	
	 CustomKeyManager(X509KeyManager defaultKeyManager) {
	    this.defaultKeyManager = defaultKeyManager;
	    this.knownAliases = knownAliases;
	  }
	
	  @Override
	  public String[] getClientAliases(String s, Principal[] principals) {
	    return defaultKeyManager.getClientAliases(s, principals);
	  }
	
	  @Override
	  public String chooseClientAlias(String[] keyType,
	                       Principal[] principals, Socket socket) {
	    String ip = socket.getInetAddress().getHostAddress();
	    if (ip != null && knownAliases.contains(ip)) {
	      return ip;
	    } else {
	      return defaultKeyManager.chooseClientAlias(keyType, principals, socket);
	    }
	  }
	
	  @Override
	  public String[] getServerAliases(String s, Principal[] principals) {
	    return defaultKeyManager.getServerAliases(s, principals);
	  }
	
	  @Override
	  public String chooseServerAlias(String s,
	                    Principal[] principals, Socket socket) {
	    return defaultKeyManager.chooseServerAlias(s, principals, socket);
	  }
	
	  @Override
	  public X509Certificate[] getCertificateChain(String s) {
	    return defaultKeyManager.getCertificateChain(s);
	  }
	
	  @Override
	  public PrivateKey getPrivateKey(String s) {
	    return defaultKeyManager.getPrivateKey(s);
	  }
	}

As we agreed, we use IP address of the web servers as an alias for storing and getting certificates. So, the main purpose of this class is the following: - when another request to a web server is being made, the chooseClientAlias method of the class is called - the method takes the web server’s IP address and checks if we have a certificate for the web server (using the clientCerts hashset we filled up previously at the beginning) - if we have the certificate, the method returns its alias (the IP address), which will be used then to take the certificate from the key store

Now, here is how we create and initialize the custom key manager:

	private KeyManager[] getKeyManagers() {
	  KeyManager[] keyManagers = null;
	  KeyManager[] defaultKeyManagers = getDefaultKeyManagers("client-certs.xml");
	  if (defaultKeyManagers != null) {
	    X509KeyManager x509KeyManager = null;
	    for (KeyManager defaultKeyManager : defaultKeyManagers) {
	      if (defaultKeyManager instanceof X509KeyManager) {
	        x509KeyManager = (X509KeyManager) defaultKeyManager;
	        break;
	      }
	     }
	     if (x509KeyManager == null) {
	       log.error("The default algorithm did not produce a X509 Key manager");
	     } else {
	       keyManagers = new X509KeyManager[] {
	         new CustomKeyManager(x509KeyManager, getClientCertsAliases())
	       };
	     }
	   }
	 return keyManagers;
	}

This method above utilizes all our previous code for creating array of key managers.

Now, here is the code for creating an httpclient that utilizes all the stuff described above:

	SSLContext sc = SSLContext.getInstance("TLS");
	sc.init(getKeyManagers(), null, new SecureRandom());
	
	SSLConnectionSocketFactory sslSocketFactory =
	    new SSLConnectionSocketFactory(sc, new NoopHostnameVerifier());
	
	ConnectionSocketFactory socketFactory = new PlainConnectionSocketFactory();
	
	Registry<ConnectionSocketFactory> socketFactoryRegistry =
	    RegistryBuilder.<ConnectionSocketFactory>create()
	    .register("https", sslSocketFactory)
	    .register("http", socketFactory).build();
	
	PoolingHttpClientConnectionManager cm =
	    new PoolingHttpClientConnectionManager(socketFactoryRegistry);
	
	CloseableHttpClient httpClient =
	    HttpClients.custom()
	    .setSSLSocketFactory(sslSocketFactory)
	    .setConnectionManager(cm).build();

Hence, at this point we have the http client which can be used for making HTTP requests as usual. Although, when connecting to a web site we have a client certificate for, the corresponding certificate will be used.