I have some application that need to run in the same Application server. Each application need to authenticate through the same web service using a certificate specific for that application. Obviously I can put all the certificates inside the same keystore, but how can I specify which one I have to use? For the calls I'm using a Spring WebServiceTemplate and I want to find something that can be easily configure inside the spring xml configuration file.
I'm trying to follow this: How can I have multiple SSL certificates for a Java server
The whole concept is clear but I can't understand how to link it with Spring WebServiceTemplate and how to specify inside the call which certificate I have to use.
I found a solution. It's not perfect, or completely clean. I need more test to be sure thats working, at the moment it is running.
This is the magical FactoryBean "CustomSSLHttpClientFactory.java".
package foo.bar.services;
import java.io.InputStream;
import java.net.Socket;
import java.security.KeyStore;
import java.util.Map;
import javax.net.ssl.SSLContext;
import org.apache.http.client.HttpClient;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.PrivateKeyDetails;
import org.apache.http.conn.ssl.PrivateKeyStrategy;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.core.io.Resource;
/**
* Custom SSL HttpClientFactoy.
* It allow to specify the certificate for a single specific implementation.
* It's needed when you have a single URL to call but different certificate, each one specific for a single page/function/user
*
* @author roberto.gabrieli
*
*/
public class CustomSSLHttpClientFactory implements FactoryBean<HttpClient>
{
protected Resource keyStoreFile;
protected String keyStorePassword;
protected String keyStoreType;
protected Resource trustStoreFile;
protected String trustStorePassword;
protected String[] allowedProtocols;
protected String certAlias;
public CustomSSLHttpClientFactory()
{
}
/**
* Contructor for factory-bean
*
* @param keyStoreFile org.springframework.core.io.Resource to specify the keystore
* @param keyStorePassword
* @param keyStoreType if null default JKS will be used
* @param trustStoreFile
* @param trustStorePassword
* @param allowedProtocols authentication protocols
* @param certAlias the client certificate alias. If null default behavior
*/
public CustomSSLHttpClientFactory(Resource keyStoreFile,
String keyStorePassword,
String keyStoreType,
Resource trustStoreFile,
String trustStorePassword,
String[] allowedProtocols,
String certAlias)
{
super();
this.keyStoreFile = keyStoreFile;
this.keyStorePassword = keyStorePassword;
this.keyStoreType = keyStoreType;
this.trustStoreFile = trustStoreFile;
this.trustStorePassword = trustStorePassword;
this.allowedProtocols = allowedProtocols;
this.certAlias = certAlias;
}
/**
* Little trick to pass over some stupid contentLength error
*
* @author roberto.gabrieli
*/
private class ContentLengthHeaderRemover implements HttpRequestInterceptor
{
@Override
public void process(HttpRequest request,
HttpContext context) throws HttpException, IOException
{
request.removeHeaders(HTTP.CONTENT_LEN);// fighting org.apache.http.protocol.RequestContent's ProtocolException("Content-Length header already present");
}
}
/**
* Private class to hack the certificate alias choice.
*
* @author roberto.gabrieli
*
*/
private class AliasPrivateKeyStrategy implements PrivateKeyStrategy
{
private String alias;
public AliasPrivateKeyStrategy(String alias)
{
this.alias = alias;
}
/**
* This metod return the alias name specified in the constructor.
*/
public String chooseAlias(Map<String, PrivateKeyDetails> aliases,
Socket socket)
{
return alias;
}
}
/**
* Method that return a CloseableHttpClient
*
*/
public CloseableHttpClient getObject() throws Exception
{
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
KeyStore keyStore = KeyStore.getInstance(this.keyStoreType != null ? this.keyStoreType : KeyStore.getDefaultType());
InputStream instreamTrust = trustStoreFile.getInputStream();
InputStream instreamKeys = keyStoreFile.getInputStream();
//Load of KEYSTORE and TRUSTSTORE
try
{
trustStore.load(instreamTrust, trustStorePassword.toCharArray());
keyStore.load(instreamKeys, keyStorePassword.toCharArray());
}
finally
{
instreamKeys.close();
instreamTrust.close();
}
SSLContextBuilder sslCtxBuilder = SSLContexts.custom().loadTrustMaterial(trustStore, new TrustSelfSignedStrategy());
PrivateKeyStrategy apks = null;
// check if the alias is specified null and "" will mean -no alias-
if ( this.certAlias != null && !this.certAlias.trim().equals("") )
{
apks = new AliasPrivateKeyStrategy(this.certAlias);
sslCtxBuilder = sslCtxBuilder.loadKeyMaterial(keyStore, keyStorePassword.toCharArray(), apks);
}
else
{
sslCtxBuilder = sslCtxBuilder.loadKeyMaterial(keyStore, keyStorePassword.toCharArray());
}
SSLContext sslcontext = sslCtxBuilder.build();
//All the stuff for the connection build
HttpClientBuilder builder = HttpClientBuilder.create();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, allowedProtocols, null, SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
builder.setSSLSocketFactory(sslsf);
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory> create().register("https", sslsf).register("http", new PlainConnectionSocketFactory()).build();
HttpClientConnectionManager ccm = new BasicHttpClientConnectionManager(registry);
builder.setConnectionManager(ccm);
CloseableHttpClient httpclient = builder.build();
return httpclient;
}
public Class<?> getObjectType()
{
return HttpClient.class;
}
public boolean isSingleton()
{
return false;
}
}
This is the needed configuration in "spring-config.xml"
<!-- Usual settings for WebServiceTemplate
<bean id="messageFactory" class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory" />
<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="contextPaths">
<list>
<value>foo.bar.model.jaxb</value>
</list>
</property>
</bean>
<!-- The bean that will do the magic! -->
<bean id="CustomSSLHttpClientFactoryFactory" class="foo.bar.services.CustomSSLHttpClientFactoryFactory" />
<!-- Bean that consume the WebService -->
<bean id="myBusinessLogicBean" class="foo.bar.services.MyBusinessLogicBean">
<property name="webServiceTemplate">
<bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
<constructor-arg ref="messageFactory" />
<property name="messageSender">
<bean id="modifiedHttpComponentsMessageSender"
class="org.springframework.ws.transport.http.HttpComponentsMessageSender">
<property name="httpClient">
<bean factory-bean="customSSLHttpClient" class="it.volkswagen.arch.services.security.CustomSSLHttpClientFactory" >
<constructor-arg name="keyStoreFile" value="file://myPath/keystore.jks" />
<constructor-arg name="keyStorePassword" value="myKeyStorePwd" />
<constructor-arg name="trustStoreFile" value="file://myPath/truststore.jks" />
<constructor-arg name="trustStorePassword" value="myTrustStorePwd" />
<constructor-arg name="keyStoreType" value="JKS" />
<constructor-arg name="allowedProtocols">
<array>
<value>TLSv1</value>
</array>
</constructor-arg>
<constructor-arg name="certAlias" value="site_a"/>
</bean>
</property>
</bean>
</property>
<property name="marshaller" ref="marshaller" />
<property name="unmarshaller" ref="marshaller" />
<property name="defaultUri"
value="http://foo.bar/ws-demo/myConsumedWs" />
</bean>
</property>
</bean>
I can't mock the Web Service with all the authentication, so to do some test of my Factory I had to deploy in IIS 8.5 two little sites with SSL Client Certificate authentication and a little java main class Here the source:
package foo.bar.runnable;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import foo.bar.services.CustomSSLHttpClientFactory;
public class RunTestHttpClient
{
private static String urlSitoA = "https://nbk196.addvalue.it";
private static String urlSitoB = "https://nbk196b.addvalue.it";
private static String trustStoreFilePath = "truststore.jks";
private static String trustStorePassword = "P@ssw0rd";
private static String[] allowedProtocols =
{ "TLSv1" };
public static void main(String[] args)
{
System.out.println("########## Test multy call with different cert in same keystore #############");
System.out.println(" ----- ----- CASE OK ----- ----- ");
testLogic("keystore.jks", "keystore.jks", "P@ssw0rd", null, "site_a", "site_b");
System.out.println(" ----- ----- CASE KO ----- ----- ");
System.out.println("########## Test multy call with different keystore #############");
System.out.println(" ----- ----- CASE OK ----- ----- ");
testLogic("site_a.pfx", "site_b.pfx", "P@ssw0rd", "pkcs12", null, null);
System.out.println(" ----- ----- CASE KO ----- ----- ");
testLogic("site_b.pfx", "site_a.pfx", "P@ssw0rd", "pkcs12", null, null);
}
private static void testLogic(String keyStoreFilePathA,
String keyStoreFilePathB,
String keyStorePassword,
String keyStoreType,
String certAliasSitoA,
String certAliasSitoB)
{
Resource keyStoreFileA = new ClassPathResource(keyStoreFilePathA);
Resource keyStoreFileB = new ClassPathResource(keyStoreFilePathB);
Resource trustStoreFile = new ClassPathResource(trustStoreFilePath);
CustomSSLHttpClientFactory clientFactorySitoA = new CustomSSLHttpClientFactory(keyStoreFileA, keyStorePassword, keyStoreType, trustStoreFile, trustStorePassword, allowedProtocols, certAliasSitoA);
CustomSSLHttpClientFactory clientFactorySitoB = new CustomSSLHttpClientFactory(keyStoreFileB, keyStorePassword, keyStoreType, trustStoreFile, trustStorePassword, allowedProtocols, certAliasSitoB);
try
{
CloseableHttpClient httpClientSitoA = clientFactorySitoA.getObject();
HttpGet httpgetSitoA = new HttpGet(urlSitoA);
try (CloseableHttpResponse responseSitoA = httpClientSitoA.execute(httpgetSitoA))
{
HttpEntity entitySitoA = responseSitoA.getEntity();
System.out.println("------------------ SitoA ----------------------");
System.out.println(responseSitoA.getStatusLine());
if ( entitySitoA != null )
{
System.out.println("Response content length: " + entitySitoA.getContentLength());
System.out.printf(EntityUtils.toString(entitySitoA));
}
EntityUtils.consume(entitySitoA);
}
System.out.println();
}
catch ( Exception e )
{
e.printStackTrace(System.out);
}
try
{
CloseableHttpClient httpClientSitoB = clientFactorySitoB.getObject();
HttpGet httpgetSitoB = new HttpGet(urlSitoB);
try (CloseableHttpResponse responseSitoB = httpClientSitoB.execute(httpgetSitoB))
{
HttpEntity entitySitoB = responseSitoB.getEntity();
System.out.println("------------------ SitoB ----------------------");
System.out.println(responseSitoB.getStatusLine());
if ( entitySitoB != null )
{
System.out.println("Response content length: " + entitySitoB.getContentLength());
System.out.printf(EntityUtils.toString(entitySitoB));
}
EntityUtils.consume(entitySitoB);
}
System.out.println();
}
catch ( Exception e )
{
e.printStackTrace(System.out);
}
}
}
This is the console output:
########## Test multy call with different cert in same keystore #############
----- ----- CASE OK ----- -----
------------------ SitoA ----------------------
HTTP/1.1 200 OK
Response content length: -1
<html>
<head></head>
<body>CARICATO SITO A</body>
</html>
------------------ SitoB ----------------------
HTTP/1.1 200 OK
Response content length: -1
<html>
<head></head>
<body>CARICATO SITO B</body>
</html>
----- ----- CASE KO ----- -----
------------------ SitoA ----------------------
HTTP/1.1 401 Unauthorized
Response content length: 6319
java.util.UnknownFormatConversionException: Conversion = ';'
at java.util.Formatter.checkText(Formatter.java:2547)
at java.util.Formatter.parse(Formatter.java:2523)
at java.util.Formatter.format(Formatter.java:2469)
at java.io.PrintStream.format(PrintStream.java:970)
at java.io.PrintStream.printf(PrintStream.java:871)
at foo.bar.runnable.RunTestHttpClient.testLogic(RunTestHttpClient.java:70)
at foo.bar.runnable.RunTestHttpClient.main(RunTestHttpClient.java:32)
------------------ SitoB ----------------------
HTTP/1.1 401 Unauthorized
Response content length: 6320
java.util.UnknownFormatConversionException: Conversion = ';'
at java.util.Formatter.checkText(Formatter.java:2547)
at java.util.Formatter.parse(Formatter.java:2523)
at java.util.Formatter.format(Formatter.java:2469)
at java.io.PrintStream.format(PrintStream.java:970)
at java.io.PrintStream.printf(PrintStream.java:871)
at foo.bar.runnable.RunTestHttpClient.testLogic(RunTestHttpClient.java:97)
at foo.bar.runnable.RunTestHttpClient.main(RunTestHttpClient.java:32)
########## Test multy call with different keystore #############
----- ----- CASE OK ----- -----
------------------ SitoA ----------------------
HTTP/1.1 200 OK
Response content length: -1
<html>
<head></head>
<body>CARICATO SITO A</body>
</html>
------------------ SitoB ----------------------
HTTP/1.1 200 OK
Response content length: -1
<html>
<head></head>
<body>CARICATO SITO B</body>
</html>
----- ----- CASE KO ----- -----
------------------ SitoA ----------------------
HTTP/1.1 401 Unauthorized
Response content length: 6319
java.util.UnknownFormatConversionException: Conversion = ';'
at java.util.Formatter.checkText(Formatter.java:2547)
at java.util.Formatter.parse(Formatter.java:2523)
at java.util.Formatter.format(Formatter.java:2469)
at java.io.PrintStream.format(PrintStream.java:970)
at java.io.PrintStream.printf(PrintStream.java:871)
at foo.bar.runnable.RunTestHttpClient.testLogic(RunTestHttpClient.java:70)
at foo.bar.runnable.RunTestHttpClient.main(RunTestHttpClient.java:37)
------------------ SitoB ----------------------
HTTP/1.1 401 Unauthorized
Response content length: 6320
java.util.UnknownFormatConversionException: Conversion = ';'
at java.util.Formatter.checkText(Formatter.java:2547)
at java.util.Formatter.parse(Formatter.java:2523)
at java.util.Formatter.format(Formatter.java:2469)
at java.io.PrintStream.format(PrintStream.java:970)
at java.io.PrintStream.printf(PrintStream.java:871)
at foo.bar.runnable.RunTestHttpClient.testLogic(RunTestHttpClient.java:97)
at foo.bar.runnable.RunTestHttpClient.main(RunTestHttpClient.java:37)