Search code examples
javaspringsslhttpsspring-ws

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.


Solution

  • 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)