CXF Code First with SSL in ServiceMix

Bart's picture
Mon, 08/05/2013 - 11:00 -- Bart

What I'm going to be doing next is setting up a webservice with SSL and deploy it ServiceMix. Since this is a simple how-to article, the service will only be able to say hi when called upon. I'll also provide a simple client that will call that service.

The POM file

Since I am using Maven I'll start things of with the POM. There is nothing fancy going on. We're just creating a bundle we can deploy in ServiceMix and we would like to be able to run our client with the mvn compile exec:java command

<modelVersion>4.0.0</modelVersion>
<groupId>be.anova</groupId>
<artifactId>cxf</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>bundle</packaging>
<name>CXF :: DEMO</name>
<description>CXF demo</description>

<dependencies>
    <dependency>
        <groupId>org.apache.geronimo.specs</groupId>
        <artifactId>geronimo-ws-metadata_2.0_spec</artifactId>
        <version>1.1.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-api</artifactId>
        <version>2.7.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-rt-transports-http</artifactId>
        <version>2.7.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-rt-frontend-jaxws</artifactId>
        <version>2.7.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.cxf</groupId>
        <artifactId>cxf-bundle-jaxrs</artifactId>
        <version>2.7.3</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.felix</groupId>
            <artifactId>maven-bundle-plugin</artifactId>
            <extensions>true</extensions>
            <configuration>
                <instructions>
                    <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
                    <Bundle-Description>${project.description}</Bundle-Description>
                    <Import-Package>

                    </Import-Package>
                    <Export-Package>
                        be.anova.cxf
                    </Export-Package>
                </instructions>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <configuration>
                <mainClass>be.anova.cxf.Client</mainClass>
                <includePluginDependencies>false</includePluginDependencies>
            </configuration>
        </plugin>
    </plugins>
</build>

The webservice

The Service Endpoint Interface (SEI)

package be.anova.cxf;
 
import javax.jws.WebService;
 
@WebService
public interface WebServiceInterface {
 
    String sayHi(User user);
 
}

The implementation

package be.anova.cxf;
 
import javax.jws.WebService;
 
@WebService(endpointInterface = "be.anova.cxf.WebServiceInterface",
        serviceName = "demo")
public class WebServiceImpl implements WebServiceInterface {
 
 
    public String sayHi(User user) {
        return "Hello user with firstname " + user.getFirstName() + " and lastname " + user.getLastName() +".";
    }
}

Blueprint

In order to deploy our webservice in ServiceMix we'll need a blueprint.

<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jaxws="http://cxf.apache.org/blueprint/jaxws"
    xmlns:cxf="http://cxf.apache.org/blueprint/core"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/blueprint/core http://cxf.apache.org/schemas/blueprint/core.xsd
http://cxf.apache.org/blueprint/jaxws http://cxf.apache.org/schemas/blueprint/jaxws.xsd">
 
    <cxf:bus>
    <cxf:features>
        <cxf:logging/>
    </cxf:features>
    </cxf:bus>
    <jaxws:endpoint id="endpoint"
                    implementor="be.anova.cxf.WebServiceImpl"
                    address="/demo">
    </jaxws:endpoint>
 
</blueprint>

The model

You probably noticed that the sayHi method takes a User object instead of just a String. In order to send a User object to the webservice we'll need an adapter to marshal and unmarshal that object for us.

The User inferface

package be.anova.cxf;
 
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
 
@XmlJavaTypeAdapter(UserAdapter.class)
public interface User {
 
    String getFirstName();
    String getLastName();
}

The User implementation

package be.anova.cxf;
 
import javax.xml.bind.annotation.XmlType;
 
 
@XmlType(name = "User")
public class UserImpl implements User {
    String firstName;
    String lastName;
 
    public UserImpl() {
    }
    public UserImpl(String name, String firstName) {
        this.firstName= firstName;
        this.lastName = lastName;
    }
 
    public String getFirstName() {
        return name;
    }
 
    public void setFirstName(String s) {
        name = s;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public void setLastName(String s) {
        this.lastName = s;
    }
}

The adapter

package be.anova.cxf;
 
 
import javax.xml.bind.annotation.adapters.XmlAdapter;
 
 
public class UserAdapter extends XmlAdapter<UserImpl, User> {
    public UserImpl marshal(User v) throws Exception {
        if (v instanceof UserImpl) {
            return (UserImpl)v;
        }
        return new UserImpl(v.getFirstName(), v.getLastName());
    }
 
    public User unmarshal(UserImpl v) throws Exception {
        return v;
    }
}

ServiceMix configuration

In order to publish our webservice with an https web adress we need to do some minor configuring for ServiceMix. In $KARAF_HOME/etc/org.ops4j.pax.web.cfg we put the following config:

org.osgi.service.http.secure.enabled=true
org.ops4j.pax.web.ssl.keystore=etc/keystore.jks  //can be generated with keytool
org.ops4j.pax.web.ssl.password=password //same as enterend upon creation of the keystore
org.ops4j.pax.web.ssl.keypassword //likewise
org.osgi.service.http.port=8443
org.ops4j.pax.web.config.file=etc/jetty.xml
org.osgi.service.http.port=8181
javax.servlet.context.tempdir=data/pax-weg-jsp

When we install the bundle in ServiceMix we should now be able to see the wsdl at https://localhost:8443/cxf/demo?wsdl

A test client

What follows is a little test client to call the service we just deployed. In order for the client to be able to connect to our service it needs to trust our keystore. This can be done by running InstallCert.java with parameter https://localhost:8443 and copying the output to $JAVA_HOME/jre/lib/security

package be.anova.cxf;
 
import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.configuration.security.FiltersType;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;
 
import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import javax.xml.ws.soap.SOAPBinding;
import java.net.URL;
 
public class DemolClient {
 
    static {
        //for localhost testing only
        javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(
                new javax.net.ssl.HostnameVerifier(){
 
                    public boolean verify(String hostname,
                                          javax.net.ssl.SSLSession sslSession) {
                        if (hostname.equals("localhost")) {
                            return true;
                        }
                        return false;
                    }
                });
    }
 
    private static final QName SERVICE_NAME
            = new QName("http://demo.anova.be/", "demo");
    private static final QName PORT_NAME
            = new QName("http://demo.anova.be/", "demoPort");
 
    private DemoClient() {
    }
 
    public static void main(String args[]) throws Exception {
        Service service = Service.create(new URL("https://localhost:8443/cxf/demo?wsdl"),SERVICE_NAME);
 
       WebServiceInterface demo = service.getPort(WebServiceInterface.class);
 
        User user = new UserImpl("DEMO", "DEMO");
 
        Client c = ClientProxy.getClient(demo);
        HTTPConduit httpConduit = (HTTPConduit) c.getConduit();
        TLSClientParameters tlsParams = new TLSClientParameters();
        tlsParams.setSecureSocketProtocol("SSL");
        tlsParams.setDisableCNCheck(true);
        FiltersType filters = new FiltersType();
        filters.getInclude().add("SSL_RSA_WITH_RC4_128_MD5");
        filters.getInclude().add("SSL_RSA_WITH_RC4_128_SHA");
        tlsParams.setCipherSuitesFilter(filters);
        httpConduit.setTlsClientParameters(tlsParams);
 
        System.out.println(demo.sayHi(user));
    }
}
/*
 * Copyright 2006 Sun Microsystems, Inc.  All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Sun Microsystems nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
/**
 * http://blogs.sun.com/andreas/resource/InstallCert.java
 * Use:
 * java InstallCert hostname
 * Example:
 *% java InstallCert ecc.fedora.redhat.com
 */
 
import javax.net.ssl.*;
import java.io.*;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
 
/**
 * Class used to add the server's certificate to the KeyStore
 * with your trusted certificates.
 */
public class InstallCert {
 
    public static void main(String[] args) throws Exception {
        String host;
        int port;
        char[] passphrase;
        if ((args.length == 1) || (args.length == 2)) {
            String[] c = args[0].split(":");
            host = c[0];
            port = (c.length == 1) ? 443 : Integer.parseInt(c[1]);
            String p = (args.length == 1) ? "changeit" : args[1];
            passphrase = p.toCharArray();
        } else {
            System.out.println("Usage: java InstallCert <host>[:port] [passphrase]");
            return;
        }
 
        File file = new File("jssecacerts");
        if (file.isFile() == false) {
            char SEP = File.separatorChar;
            File dir = new File(System.getProperty("java.home") + SEP
                    + "lib" + SEP + "security");
            file = new File(dir, "jssecacerts");
            if (file.isFile() == false) {
                file = new File(dir, "cacerts");
            }
        }
        System.out.println("Loading KeyStore " + file + "...");
        InputStream in = new FileInputStream(file);
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(in, passphrase);
        in.close();
 
        SSLContext context = SSLContext.getInstance("TLS");
        TrustManagerFactory tmf =
                TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        tmf.init(ks);
        X509TrustManager defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
        SavingTrustManager tm = new SavingTrustManager(defaultTrustManager);
        context.init(null, new TrustManager[]{tm}, null);
        SSLSocketFactory factory = context.getSocketFactory();
 
        System.out.println("Opening connection to " + host + ":" + port + "...");
        SSLSocket socket = (SSLSocket) factory.createSocket(host, port);
        socket.setSoTimeout(10000);
        try {
            System.out.println("Starting SSL handshake...");
            socket.startHandshake();
            socket.close();
            System.out.println();
            System.out.println("No errors, certificate is already trusted");
        } catch (SSLException e) {
            System.out.println();
            e.printStackTrace(System.out);
        }
 
        X509Certificate[] chain = tm.chain;
        if (chain == null) {
            System.out.println("Could not obtain server certificate chain");
            return;
        }
 
        BufferedReader reader =
                new BufferedReader(new InputStreamReader(System.in));
 
        System.out.println();
        System.out.println("Server sent " + chain.length + " certificate(s):");
        System.out.println();
        MessageDigest sha1 = MessageDigest.getInstance("SHA1");
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        for (int i = 0; i < chain.length; i++) {
            X509Certificate cert = chain[i];
            System.out.println
                    (" " + (i + 1) + " Subject " + cert.getSubjectDN());
            System.out.println("   Issuer  " + cert.getIssuerDN());
            sha1.update(cert.getEncoded());
            System.out.println("   sha1    " + toHexString(sha1.digest()));
            md5.update(cert.getEncoded());
            System.out.println("   md5     " + toHexString(md5.digest()));
            System.out.println();
        }
 
        System.out.println("Enter certificate to add to trusted keystore or 'q' to quit: [1]");
        String line = reader.readLine().trim();
        int k;
        try {
            k = (line.length() == 0) ? 0 : Integer.parseInt(line) - 1;
        } catch (NumberFormatException e) {
            System.out.println("KeyStore not changed");
            return;
        }
 
        X509Certificate cert = chain[k];
        String alias = host + "-" + (k + 1);
        ks.setCertificateEntry(alias, cert);
 
        OutputStream out = new FileOutputStream("jssecacerts");
        ks.store(out, passphrase);
        out.close();
 
        System.out.println();
        System.out.println(cert);
        System.out.println();
        System.out.println
                ("Added certificate to keystore 'jssecacerts' using alias '"
                        + alias + "'");
    }
 
    private static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();
 
    private static String toHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 3);
        for (int b : bytes) {
            b &= 0xff;
            sb.append(HEXDIGITS[b >> 4]);
            sb.append(HEXDIGITS[b & 15]);
            sb.append(' ');
        }
        return sb.toString();
    }
 
    private static class SavingTrustManager implements X509TrustManager {
 
        private final X509TrustManager tm;
        private X509Certificate[] chain;
 
        SavingTrustManager(X509TrustManager tm) {
            this.tm = tm;
        }
 
        public X509Certificate[] getAcceptedIssuers() {
            throw new UnsupportedOperationException();
        }
 
        public void checkClientTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            throw new UnsupportedOperationException();
        }
 
        public void checkServerTrusted(X509Certificate[] chain, String authType)
                throws CertificateException {
            this.chain = chain;
            tm.checkServerTrusted(chain, authType);
        }
    }
 
}

When all this is done and we run the DemoClient from terminal with mvn comile exec:java we should see the service saying hello to a user with firstname DEMO and a lastname DEMO.

Conclusion

Deploying a secured CXF webservice in ServiceMix really is a walk in the park! Getting a client configured to trust the certificate really is the only thing that takes some effort here.