Monday, June 13, 2011

Setting up Apache CXF with SSL for client and server

I recently changed the JAXWS implementation from the reference implementation (RI) to Apache CXF. One of the motivating factors is that we need to be able to communicate via SSL, and to do this with the RI, the JVM system properties need to be changed.  This can, and has, caused problems when the webapp shares Tomcat with other webapps.

In order to this, I needed to set up Apache CXF.  Specifically, our webapp acts as both a client and server.


1. Dependencies
The first thing that needs to be done is to include the dependencies.  We use maven.

(Defined in our main pom's properties section)

    2.4.0

(In the pom for any package that performs client side JAXWS actions)
  
    
        org.apache.cxf
        cxf-rt-frontend-jaxws
        ${cxf-version}
    
    
        org.apache.cxf
        cxf-rt-transports-http
        ${cxf-version}
    

(In the pom for the package that performs the server side JAXWS actions)*

    
        org.apache.cxf
        cxf-rt-transports-http-jetty
        ${cxf-version}  

* I'm not 100% sure if this particular dependency is needed. It doesn't seem to be causing a problem with my build as of now. There is conflicting documentation about this. According to an out-dated how to set up CXF with Maven:


Jetty is needed if you're using the CXFServlet
 

However, according their first jaxws factory bean sample pom:
 Jetty is needed if you're using the CXFServlet

I'll update this post when I find a definitive answer.

2. CXF Server Configuration
The next thing that needs to be done is to register the cxf service.  This is done in the WEB-INF folder in the package that creates the web service .war file.  This folder should contain two files:
  • web.xml
    • Registers the services
  • cxf.xml
    • Configuration file for cxf that tells cxf how to resolve the endpoint
(web.xml)

    webservices-project
    
        CXFServlet
        org.apache.cxf.transport.servlet.CXFServlet
        1
    
    
        CXFServlet
        /*
    
    
        contextConfigLocation
        /WEB-INF/cxf.xml
    
    
        org.springframework.web.context.ContextLoaderListener
    

Two things to note here:
  • On line 6, there is a load-on-startup field. I'll be honest, I don't know what this does. According to the wise stackoverflow:
    load-on-startup can specify an (optional) integer value. If the value is 0 or greater, it indicates an order for servlets to be loaded, servlets with higher numbers get loaded after servlets with lower numbers.
  • On line 14, this is the name and location of the configuration file for cxf. If your cxf config file is named differently, say cxf-mywebapp.xml, indicate it here.

 (cxf.xml)

 
    
    
    
 
    
    

One thing to note here is:
  • Line 11: Specify here the id/name of the web-service, the fully annotated for JaxWS implementation of the web-service and the address.

3. Initialize conduit for SSL communication 
CXF has various methods for controlling settings for the communications. In order to set up the proxy for SSL communications, settings are made to the TlsClientParameters.  I have the following method to set the TlsClientParameters for this (adapted from this post):
public final class Utils
{
    private static final TLSClientParameters tlsParams = new TLSClientParameters();
    private static volatile boolean isTlsSetForSSL = false;
    private static final Lock setTlsParamsLock = new ReentrantLock();
    
    // ----
    // Some other methods and things
    // ---
    static final TLSClientParameters getTlsParams()
    {
        return tlsParams;
    }
    
    static final void initializeConduitForSSL() throws Exception
    {
        // if the tls params have not been set up for SSL
        if(!isTlsSetForSSL)
        {
            // get the lock
            setTlsParamsLock.lock();

            // intentionally checking twice to try to minimize overhead
            // of acquiring lock
            if(!isTlsSetForSSL)
            {
                try
                {
                    isTlsSetForSSL = true;

                    // set up the keystore and password
                    KeyStore keyStore = KeyStore.getInstance("JKS");
                    // -- provide your password
                    String trustpass = "password"; 
                    // -- provide your truststore
                    File truststore = new File("truststore.jks");
                    keyStore.load(new FileInputStream(truststore), trustpass.toCharArray());

                    // should JSEE omit checking if the host name specified in the
                    // URL matches that of the Common Name (CN) on the server's 
                    // certificate.
                    tlsParams.setDisableCNCheck(true);

                    // set the SSL protocol
                    tlsParams.setSecureSocketProtocol("SSL");

                    // set the trust store 
                    // (decides whether credentials presented by a peer should be accepted)
                    TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                    trustFactory.init(keyStore);
                    TrustManager[] tm = trustFactory.getTrustManagers();
                    tlsParams.setTrustManagers(tm);

                    // set our key store 
                    // (used to authenticate the local SSLSocket to its peer)
                    KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
                    keyFactory.init(keyStore, trustpass.toCharArray());
                    KeyManager[] km = keyFactory.getKeyManagers();
                    tlsParams.setKeyManagers(km);

                    // set all the needed include & exclude cipher filters
                    FiltersType filter = new FiltersType();
                    //filter.getInclude().add(".*_WITH_3DES_.*"); // XXX needed?
                    filter.getInclude().add(".*_EXPORT_.*");
                    filter.getInclude().add(".*_EXPORT1024_.*");
                    filter.getInclude().add(".*_WITH_DES_.*");
                    filter.getInclude().add(".*_WITH_NULL_.*");
                    filter.getExclude().add(".*_DH_anon_.*");
                    tlsParams.setCipherSuitesFilter(filter);
                }
                catch(final Exception e)
                {
                    throw new Exception("Failed to initialize conduit!", e);
                }
                finally
                {
                    setTlsParamsLock.unlock();
                }
            }
        }
    }
}

Things to note:
  • According to CXF, proxies can be thread safe, however settings on the conduit are NOT.  The settings on the conduit are per instance.  For this reason, I have done the following:
    • There is a volatile variable isTlsSetForSSL that is checked so the settings are only made once.
    • The settings are done only after acquiring a lock
  • The TlsClientParameters allow for setting two different stores, one for the keystore and one for the truststore.  In this example, both of these happen to be the same file.
  • Line 63 is commented out.  I'm not sure if this is needed or not.  I was having some trouble getting this to work, and thought it was related to the filters that were included.  I came across a post that mentioned:

    2009-10-30 19:37:37:745 INFO [pool-2-thread-4] [PhaseInterceptorChain] - Interceptor has thrown exception, unwinding now org.apache.cxf.interceptor.Fault: Could not send Message. .... Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

    Later I found out that the new server expected to communicate over a 3DES SSL cipher suite because the new Apache configuration was set to use strong encryption (see encryption http://httpd.apache.org/docs/2.2/ssl/ssl_howto.html).


    In my case, it turned out that there was one place in the unit test that did not call this initializing method before making a proxy.
4. Making a proxy to use
Prior to this change, we were using the reference implementation (Sun). In order to have SSL work properly in that implementation, it was necessary to make changes to the JVM wide system properties, which was un-desirable. When making the change to CXF, we were trying to limit the use of implementation specific code. 

CXF has an example that shows how to configure the conduit for a client, and indirectly shows how to get a proxy to work with:
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
...

URL wsdl = getClass().getResource("wsdl/greeting.wsdl");
SOAPService service = new SOAPService(wsdl, serviceName);
Greeter greeter = service.getPort(portName, Greeter.class);

Client client = ClientProxy.getClient(greeter);
HTTPConduit http = (HTTPConduit) client.getConduit();

Since our prior implementation used javax.xml.ws.Service to get the service, I decided to reuse that instead for getting the service:
import javax.xml.ws.Service;
import javax.xml.namespace.QName;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;
...

final QName qServiceName = new QName(myNamespace, serviceName);
final QName qPortName = new QName(myNamespace, portName);
final Service service = Service.create(myUrl, qServiceName);
final Greeter greeter = service.getPort(qPortName, Greeter.class);

Client client = ClientProxy.getClient(greeter);
HTTPConduit http = (HTTPConduit) client.getConduit();
// !! THIS DOES NOT WORK FOR SSL !!
http.setTlsClientParameters(getTlsParams());

After removing all the methods that set the system wide JVM properties and trying this out... it turns out that it DOES NOT WORK. The following is the stacktrace on the server's log of the failure:
Caused by: javax.xml.ws.WebServiceException: org.apache.cxf.service.factory.ServiceConstructionException: Failed to create service.
 at org.apache.cxf.jaxws.ServiceImpl.(ServiceImpl.java:149)
 at org.apache.cxf.jaxws.spi.ProviderImpl.createServiceDelegate(ProviderImpl.java:90)
 at javax.xml.ws.Service.(Service.java:56)
 at javax.xml.ws.Service.create(Service.java:680)
... more
Caused by: org.apache.cxf.service.factory.ServiceConstructionException: Failed to create service.
 at org.apache.cxf.wsdl11.WSDLServiceFactory.(WSDLServiceFactory.java:93)
 at org.apache.cxf.jaxws.ServiceImpl.initializePorts(ServiceImpl.java:203)
 at org.apache.cxf.jaxws.ServiceImpl.(ServiceImpl.java:147)
 ... 15 more
Caused by: javax.wsdl.WSDLException: WSDLException: faultCode=PARSER_ERROR: Problem parsing 'https://localhost:9081/node-server/node?wsdl'.: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at com.ibm.wsdl.xml.WSDLReaderImpl.getDocument(Unknown Source)
 at com.ibm.wsdl.xml.WSDLReaderImpl.readWSDL(Unknown Source)
 at com.ibm.wsdl.xml.WSDLReaderImpl.readWSDL(Unknown Source)
 at org.apache.cxf.wsdl11.WSDLManagerImpl.loadDefinition(WSDLManagerImpl.java:239)
 at org.apache.cxf.wsdl11.WSDLManagerImpl.getDefinition(WSDLManagerImpl.java:186)
 at org.apache.cxf.wsdl11.WSDLServiceFactory.(WSDLServiceFactory.java:91)
 ... 17 more
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:174)
 at com.sun.net.ssl.internal.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1649)
 at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:241)
 at com.sun.net.ssl.internal.ssl.Handshaker.fatalSE(Handshaker.java:235)
 at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1206)
 at com.sun.net.ssl.internal.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:136)
...
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:323)
 at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:217)
 at sun.security.validator.Validator.validate(Validator.java:218)
 at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:126)
 at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:209)
 at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:249)
 at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1185)
 ... 41 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
 at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:174)
 at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:238)
 at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:318)
 ... 47 more

As it turns out, it is failing at line 10 in the above, the javax.xml.ws.Service call does something that tries to connect before getting to the conduit set up. This call relies on the JVM system wide properties, and because I had removed this bit of code, it was failing because it could not longer find the keystore information on the properties.

When I changed the code to use the implementation specific way of setting up a proxy, it worked:
final JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();

factory.setServiceClass(Greeter.class);
factory.setAddress(myUrl.toString());
final Greeter greeter = (Greeter) factory.create();

final Client client = ClientProxy.getClient(port);
HTTPConduit http = (HTTPConduit) client.getConduit();
// make sure to initialize tlsParams prior to this call somewhere
http.setTlsClientParameters(getTlsParams());

One final thing to note. In our unit test, we originally had:
private static final String endpointA = protocol + "://localhost:9080/" + serverContext + "/node";

This resulted in the following error when trying to run the unit test after switching to CXF:
javax.xml.ws.WebServiceException: org.apache.cxf.service.factory.ServiceConstructionException: Failed to create service.
 at org.apache.cxf.jaxws.ServiceImpl.(ServiceImpl.java:149)
 at org.apache.cxf.jaxws.spi.ProviderImpl.createServiceDelegate(ProviderImpl.java:90)
 at javax.xml.ws.Service.(Service.java:56)
 at javax.xml.ws.Service.create(Service.java:680)
... more
Caused by: org.apache.cxf.service.factory.ServiceConstructionException: Failed to create service.
 at org.apache.cxf.wsdl11.WSDLServiceFactory.(WSDLServiceFactory.java:93)
 at org.apache.cxf.jaxws.ServiceImpl.initializePorts(ServiceImpl.java:203)
 at org.apache.cxf.jaxws.ServiceImpl.(ServiceImpl.java:147)
 ... 32 more
Caused by: javax.wsdl.WSDLException: WSDLException: faultCode=PARSER_ERROR: Problem parsing 'https://localhost:9080/server/node'.: java.io.IOException: Server returned HTTP response code: 500 for URL: https://localhost:9080/server/node
 at com.ibm.wsdl.xml.WSDLReaderImpl.getDocument(Unknown Source)
 at com.ibm.wsdl.xml.WSDLReaderImpl.readWSDL(Unknown Source)
 at com.ibm.wsdl.xml.WSDLReaderImpl.readWSDL(Unknown Source)
 at org.apache.cxf.wsdl11.WSDLManagerImpl.loadDefinition(WSDLManagerImpl.java:239)
 at org.apache.cxf.wsdl11.WSDLManagerImpl.getDefinition(WSDLManagerImpl.java:186)
 at org.apache.cxf.wsdl11.WSDLServiceFactory.(WSDLServiceFactory.java:91)
 ... 34 more
Caused by: java.io.IOException: Server returned HTTP response code: 500 for URL: https://localhost:9080/server/node
 at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1436)
 at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:234)
 at org.apache.xerces.impl.XMLEntityManager.setupCurrentEntity(Unknown Source)
 at org.apache.xerces.impl.XMLVersionDetector.determineDocVersion(Unknown Source)
 at org.apache.xerces.parsers.XML11Configuration.parse(Unknown Source)
 at org.apache.xerces.parsers.XML11Configuration.parse(Unknown Source)
 at org.apache.xerces.parsers.XMLParser.parse(Unknown Source)
 at org.apache.xerces.parsers.DOMParser.parse(Unknown Source)
 at org.apache.xerces.jaxp.DocumentBuilderImpl.parse(Unknown Source)
 ... 40 more

Chaniging the endpoint to add a ?wsdl to the end of the string fixed this problem:
private static final String endpointA = protocol + "://localhost:9080/" + serverContext + "/node?wsdl";

And that's it.  Hopefully this helps.