Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use spring-ws client to call the same webservice using different keystore

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.

like image 447
ElPysCampeador Avatar asked Jan 27 '15 11:01

ElPysCampeador


2 Answers

There is a much easier approach rather than using a custom HTTP Client factory bean that manually sets up the SSL context and uses interceptors to remove the content length headers (little Hokie if you ask me).

Spring has a HttpsUrlConnectionMessageSender that will automatically setup the SSLContext correctly and allow you to specify different keystore and truststore via the KeyStoreManager and TrustStoreManager. This approach makes it much cleaner to do mutual SSL authentication from the client side.

public class MyWebServiceClient extends WebServiceGatewaySupport implements MyWebServicePortType {

@Configuration
public static class MyClientConfig {
    @Value("${myws.endpoint.url}")
    private String url;

    @Value("${myws.keystore}")
    private Resource keyStore;
    @Value("${myws.keystore.password}")
    private String keyStorePass;
    @Value("${myws.truststore}")
    private Resource trustStore;
    @Value("${myws.truststore.password}")
    private String trustStorePass;

    @Bean 
    public Jaxb2Marshaller myWebServiceClientMarshaller() {
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setContextPath("com.myws.types");
        return marshaller;
    }

    @Bean
    public MyWebServiceClient myWebServiceClient() throws Exception {
        MyWebServiceClient client = new MyWebServiceClient();
        client.setDefaultUri(this.url);
        client.setMarshaller(myWebServiceClientMarshaller());
        client.setUnmarshaller(myWebServiceClientMarshaller());

        KeyStore ks = KeyStore.getInstance("JKS");
        ks.load(keyStore.getInputStream(), keyStorePass.toCharArray());
        logger.info("Loaded keyStore: "+keyStore.getURI().toString());
        try { keyStore.getInputStream().close(); } catch(IOException e) {}
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(ks, keyStorePass.toCharArray());

        KeyStore ts = KeyStore.getInstance("JKS");
        ts.load(trustStore.getInputStream(), trustStorePass.toCharArray());
        logger.info("Loaded trustStore: "+trustStore.getURI().toString());
        try { trustStore.getInputStream().close(); } catch(IOException e) {}
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(ts);

        HttpsUrlConnectionMessageSender msgSender = new HttpsUrlConnectionMessageSender();
        msgSender.setKeyManagers(keyManagerFactory.getKeyManagers());
        msgSender.setTrustManagers(trustManagerFactory.getTrustManagers());

        client.setMessageSender(msgSender);

        return client;
    }


    // client port method implementations ...
    public MyOperationResponse processMyOperation(MyOperationRequest request) {
        return (MyOperationResponse) getWebServiceTemplate().marshalSendAndReceive(request, new SoapActionCallback("urn:ProcessMyOperation"));
    }

}
like image 138
Aaron Wilson Avatar answered Nov 12 '22 23:11

Aaron Wilson


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)
like image 30
ElPysCampeador Avatar answered Nov 12 '22 23:11

ElPysCampeador