HTTPClient and Client Side Certificates

October 18, 2011

I had to develop an interface to a leading credit reference agency. The call was based on self signed certificates (no other certificates required), and we had to attach a client side certificate to the request. The payload was a SOAP XML block.

Certificates

I ended up with two keystores, named keystore.jks and truststore.jks. The keystore contained the client side key that I would use to connect to the service. The truststore contained the other certificates as required by the chain. In my situation, I had one client side key in the keystore and two certificates in the truststore.

You can list the contents of a keystore using the keytool command. Here's the list for the truststore:

$ keytool -list -keystore truststore.jks
Enter keystore password:

Keystore type: JKS
Keystore provider: SUN

Your keystore contains 2 entries

certuat, 12-Oct-2011, trustedCertEntry,
Certificate fingerprint (MD5): 3E:1C:23:D8:DB:7C:AF:30:AB:2C:CD:AE:E0:3A:03:0C

credituatroot, 12-Oct-2011, trustedCertEntry,
Certificate fingerprint (MD5): 21:44:F6:7F:1E:F9:4A:81:29:C8:F7:51:C8:77:1A:4D

Here's the list for the keystore:

$ keytool -list -keystore keystore.jks
Enter keystore password:

Keystore type: JKS
Keystore provider: SUN

Your keystore contains 1 entry

clientcert, 12-Oct-2011, PrivateKeyEntry,
Certificate fingerprint (MD5): AB:F9:A1:8D:EB:35:63:99:A7:71:87:4D:1A:3E:12:FC

Required Libraries

This call was implemented using HTTPClient 4.1.2. The required java libraries are:

commons-codec-1.4.jar
commons-logging-1.1.1.jar
httpclient-4.1.2.jar
httpclient-cache-4.1.2.jar
httpcore-4.1.2.jar
httpmime-4.1.2.jar

Constants

The code uses the following constants:

final String KEY_STORE_PATH = "keystore.jks";
final String KEY_STORE_PASSWORD = "changeit";
final String TRUST_STORE_PATH = "truststore.jks";
final String TRUST_STORE_PASSWORD = "changeit";

Load Two Keystores

We first need to load in the two keystores:

KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream keystoreInput = new FileInputStream(KEY_STORE_PATH);
keystore.load(keystoreInput, KEY_STORE_PASSWORD.toCharArray());
System.out.println("Keystore has " + keystore.size() + " keys");

// load the truststore, leave it null to rely on cacerts distributed with the JVM
KeyStore truststore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream truststoreInput = new FileInputStream(TRUST_STORE_PATH);
truststore.load(truststoreInput, TRUST_STORE_PASSWORD.toCharArray());
System.out.println("Truststore has " + truststore.size() + " keys");

Create Scheme Registry

Next, we need to set up the Scheme Registry, passing in both the keystore and the truststore:

SchemeRegistry schemeRegistry = new SchemeRegistry();
SSLSocketFactory lSchemeSocketFactory = new SSLSocketFactory(keystore, KEY_STORE_PASSWORD, truststore);
schemeRegistry.register(new Scheme("https", 443, lSchemeSocketFactory));

Create HTTP Client

We now create a HTTPClient instance:

final HttpParams httpParams = new BasicHttpParams();
DefaultHttpClient lHttpClient = new DefaultHttpClient(new SingleClientConnManager(schemeRegistry), httpParams);

String lUrl = "https://secure.address.soapservice.net/Service.asmx";

### Create XML Create the XML to send.

StringBuffer lXmlBuffer = new StringBuffer();
lXmlBuffer.append("<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\">\n");
lXmlBuffer.append("<SOAP-ENV:Header/>\n");
lXmlBuffer.append("<SOAP-ENV:Body>\n");
lXmlBuffer.append("<ABC xmlns=\"http://www.secure.com/ABC/\">\n");
lXmlBuffer.append("<payload>\n");

lXmlBuffer.append("</payload>\n");
lXmlBuffer.append("</ABC>\n");
lXmlBuffer.append("</SOAP-ENV:Body>\n");
lXmlBuffer.append("</SOAP-ENV:Envelope>\n");

String lXml = lXmlBuffer.toString();

Perform POST

Post to the URL with the XML as a parameter. The contents of the SOAPAction header are defined in the WSDL (look for <soap:operation soapAction="XXX">)

HttpPost lMethod = new HttpPost(lUrl);
HttpEntity lEntity = new StringEntity(lXml, "text/xml", "UTF-8");
lMethod.setEntity(lEntity);
lMethod.setHeader("SOAPAction", "http://www.secure.com/ABC/");

HttpResponse lHttpResponse = lHttpClient.execute(lMethod);

Process Response

We can now get hold of the response XML.

System.out.println("Response status code: " + lHttpResponse.getStatusLine().getStatusCode());
System.out.println("Response body: ");
System.out.println(EntityUtils.toString(lHttpResponse.getEntity()));

Trouble Shooting

We got the following exception:

Exception in thread "main" javax.net.ssl.SSLPeerUnverifiedException: peer not authenticated
at com.sun.net.ssl.internal.ssl.SSLSessionImpl.getPeerCertificates(SSLSessionImpl.java:352)
at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:128)
at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:397)
at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:148)
at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:149)
at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:121)
at org.apache.http.impl.client.DefaultRequestDirector.tryConnect(DefaultRequestDirector.java:573)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:425)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:820)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:754)
at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:732)

This was down to the truststore/keystore key split being wrong. We worked this out using the java option -Djavax.net.debug=all which outputs lots of debug for the SSL Handshake. See http://download.oracle.com/javase/1.5.0/docs/guide/security/jsse/ReadDebug.html for an explanation of the output.

References