Securing some SOAP

Recently, I had to implement a Soap Service. Nothing special, I just hadn’t done it in a while and was amazed at how easy it has become. Using some code generation and Spring Boot (Quarkus support for soap was lacking imho ), most of the work was done with just configuration magic. I was amazed that “old” as it is, it is still fully supported.

Implementing the Service

The first step – and the only way you should ever create a service, be it soap or rest –  is creating the contract and then generating the code. This way you will have full control over the contract and you don’t have to type all the boring code yourself.

<plugin>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-codegen-plugin</artifactId>
    <version>${cxf.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>wsdl2java</goal>
            </goals>
            <configuration>
                <wsdlOptions>
                    <wsdlOption>
                        <wsdl>${basedir}/src/main/resources/wsdl/calculator.wsdl</wsdl>
                        <wsdlLocation>classpath:wsdl/calculator.wsdl</wsdlLocation>
                        <packagenames>
                            <!-- Package Mappings -->
                            <packagename>com.example.producingwebservice.service</packagename>
                        </packagenames>
                    </wsdlOption>
                </wsdlOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

The next thing is creating the service. Which with Spring Boot means you have to implement the service itself. When looking at the generated sources, you will find an interface with a lot of annotations like: “@WebService, @WebMethod” etc. Implement that method (don’t forget your own annotation @Service):

public class Implementation implements CalculatorSoap {
    @Override
    public int subtract(int intA, int intB) {
        return 42;
    }

    @Override
    public int divide(int intA, int intB) {
        return 42;
    }

    @Override
    public int add(int intA, int intB) {
        return 42;
    }

    @Override
    public int multiply(int intA, int intB) {
        return 42;
    }
}

The last part is adding it to the “bus”, apache CXF way of running soap services.

package com.example.producingwebservice.service;

import org.apache.cxf.Bus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.xml.ws.Endpoint;

@Configuration
public class ApplicationConfig {

    @Autowired
    private Bus bus;

    @Bean
    public Endpoint calculateEndpoint(Implementation service) {
        EndpointImpl endpoint = new EndpointImpl(bus, service);
        endpoint.publish("/calculator");
        return endpoint;
    }

}

With the right dependencies, you should be able to start the Spring context and call the soap service at:

http://localhost:8080/services

WS-Security

Alright, we now have ourselves a good old fashioned soap service and a nice contract to adhere to. It is just lacking some basic security. So the next thing we need to do, is to add WS-Security (specifically encryption and signing). Luckily Apache CXF has a nice feature and libraries for this. CXF comes with an interceptor framework, for both incoming and outgoing messages. This allows us to, for example, intercept an incoming message and validate the signature and decrypt the message before it enters the actual implementation class. That way we don’t have to “pollute” our implementation code with any fancy encryption functionality (my editor mumbled something about cross cutting concerns). In all honesty, the next part is based upon a post by someone else and can be found here: http://glenmazza.net/blog/entry/cxf-x509-profile. The problem, however, is that it is a bit outdated. So consider this as a refreshment of Glen’s work.

Keystores & Certificates

First things first, we need some X.509 keys (server + client) for this to actually work:

keytool -genkey -keyalg RSA -sigalg SHA1withRSA -validity 1461 -alias myservicekey -storepass sspass -keystore serviceKeystore.jks -dname "cn=localhost"
keytool -genkey -keyalg RSA -sigalg SHA1withRSA -validity 1461 -alias myclientkey -storepass cspass -keystore clientKeystore.jks -dname "cn=clientuser"

These are the keystores containing some keys, now we need to exchange the public keys (for the sake of trust)

keytool -export -rfc -keystore clientKeystore.jks -storepass cspass -alias myclientkey -file MyClient.cer
keytool -import -trustcacerts -keystore serviceKeystore.jks -storepass sspass -alias myclientkey -file MyClient.cer -noprompt

keytool -export -rfc -keystore serviceKeystore.jks -storepass sspass -alias myservicekey -file MyService.cer
keytool -import -trustcacerts -keystore clientKeystore.jks -storepass cspass -alias myservicekey -file MyService.cer -noprompt

Interceptors

That’s it, you have successfully executed a key exchange! Now the actual work can be done

The next thing to do, is to create the interceptors. Let’s create the OutInterceptor first:

WSS4JOutInterceptor    public WSS4JOutInterceptor generateWSS4JOutputInterceptor() {
        Map<String, Object> outProps = new HashMap<String, Object>();
        outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.SIGNATURE);
        outProps.put(WSHandlerConstants.USER, "myservicekey");
        outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS, KeystoreCallbackHandler.class.getName());
        outProps.put(WSHandlerConstants.SIG_KEY_ID, "DirectReference");
        outProps.put(WSHandlerConstants.SIGNATURE_PARTS, "{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body");
        outProps.put(WSHandlerConstants.SIG_PROP_FILE, "serviceKeystore.properties");

        return new WSS4JOutInterceptor(outProps);
    }

A short explanation of the used options:

  • ACTION -> A whitespace separated list of actions, signature, encryption etc.
  • USER -> The alias of the key that is to be used
  • PW_CALLBACK_CLASS -> A CallbackHandler class that returns the correct password for the certificate
  • SIG_KEY_ID -> This indicates that the key needs to be embedded as base64 in the header
  • SIGNATURE_PARTS -> The part of the payload that needs to be included when calculating the signature
  • SIG_PROP_FILE -> A property file containing all the information needed to retrieve the actual certificate.

With this, the interceptor simply generates the signature based upon the body using the Keystore and thus X.509 Key provided in the keystore:

org.apache.ws.security.crypto.merlin.keystore.file=keystore/serviceKeystore.jks
org.apache.ws.security.crypto.merlin.keystore.password=sspass
org.apache.ws.security.crypto.merlin.keystore.type=jks
org.apache.ws.security.crypto.merlin.keystore.alias=myservicekey

Add the interceptor to the Endpoint by adding the WSS4JOutInterceptor to the list of OutInterceptors in the endpoint that we already created.

endpoint.getOutInterceptors().add(generateWSS4JOutputInterceptor());
endpoint.getInInterceptors().add(generateWSS4JInputInterceptor());

Restart the Spring context and call the soap service again. You will notice a nice fat signature.

   <soap:Header>
      <wsse:Security soap:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
         <wsse:BinarySecurityToken EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" wsu:Id="X509-0be7a2c2-1111-41e1-9f6b-426191f9b22a">MIICxzCCAa+gAwIBAgIEJ2C4TjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwHhcNMjEwODI3MTExNjQ1WhcNMjExMTI1MTExNjQ1WjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCIiFMzbBQ026F9HD36WNo8kQFgfC7yRG+QiEFYsw5KDKWUNJl6YNibcjwB+Qt71cosBm1lSODRrdTjVpvITi6Np1X2o6W96Gz3sI6Sfb8SqrDw7k2uirta/UCUAkQMr2qWCK4iI/2EDjuH9AfQvOG5SE7w88lS9hffzQz05fHai2w2h7mRO98SeQk+QKAz/ajz3gE1ON+04YgoRQZw1nfndnFOqx0whk/Ddaou5FJvWwhNyVBKjRYaF8GcNZKUGn06vjDifK1OD245kEfTw7TYZtAlKqg/HBLZ42cRGMothgMrjackByC1f8IBUGDdCiEMWvyf37oAeiUUE3oAe0UnAgMBAAGjITAfMB0GA1UdDgQWBBSGKvLaKtdmR1xLP5EnS93yva+K9DANBgkqhkiG9w0BAQsFAAOCAQEAgxDQIVgTF6Kgm8F34M6TIQMmMG0ibZPdAysPqUq16nHXtU0jM98IJ59hY7z0yTILzFjsml/gfbEg7MuRSopt/IcaVb/DpzAJ4IABslDanLN9Mpp3mp1y0uFX2IHCOT9r6/W2OpPho+pqmvbc78I1OnnYZ6c6GyzIWd9FHG8LRWPgejVTCWkoSZ0cheaZZvhJgWnqnh6J2Z46DhH9aZVBhOBCWMKuMxQ7cOSiaVJcsdjiQirPLq48fORNfDy6tHWUNgWEC+gwyP+lli3u2U/G3T2RyF96JODv67OdI5DQbKPMkm35jUFwXjHIfRHdWIUPknsY2m0QKr375MjR5JS+MA==</wsse:BinarySecurityToken>
         <ds:Signature Id="SIG-b3a01030-be18-4df2-81a5-316b9f4ad8ba" xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:SignedInfo>
               <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                  <ec:InclusiveNamespaces PrefixList="soap" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/>
               </ds:CanonicalizationMethod>
               <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
               <ds:Reference URI="#id-191d9439-76df-445b-9c92-875e2e30cfc2">
                  <ds:Transforms>
                     <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                  </ds:Transforms>
                  <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                  <ds:DigestValue>+IKOAJHKAJnYjfDF9eX2PTSLDgE=</ds:DigestValue>
               </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>enBVhHeat599VSAjlt5rVRfoHYKJOkHSo9jAA9Ve3dKQyGKsPWZQzANNr02rar6eYnGtOBab7kUTc9Nkl6Cy+t0AG8o0i9R1ckF5w5fBwdV8o2Zx6eHOrjkMWs7K8b+8v882W7PWzB3wsVSqjQZNlep2DffaI4HJJQPhAisL/GruN6Jpvgg7PHmDmamAmx08ksgk6DrqhmFp5R5WGpCbtAUQmy/Klljw37RoaReVl/T7dyIexsG9+WcOWZbCpPx4iOGJOIhO87DlrAY5KH5mTF1mGR2UWEvqVn5pzlYiPqZBTWRGQX1X5r8TBDzoeq06M39aQnJo0o5GZOEQg5YjQw==</ds:SignatureValue>
            <ds:KeyInfo Id="KI-b3045af7-6128-4fff-8c9b-46dd4ccea674">
               <wsse:SecurityTokenReference wsu:Id="STR-230a395f-399f-4909-9c30-581ef57e7a11">
                  <wsse:Reference URI="#X509-0be7a2c2-1111-41e1-9f6b-426191f9b22a" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
               </wsse:SecurityTokenReference>
            </ds:KeyInfo>
         </ds:Signature>
      </wsse:Security>
   </soap:Header>

Now for the second part, we need to use the WSS4JInInterceptor. This allows us to decrypt and check the signature against a TrustStore. The only downside is that the decryption was optional. So I had to create an OptionalDecryptionWSS4JInInterceptor:

public class OptionalDecryptionWSS4JInInterceptor extends WSS4JInInterceptor {


    public OptionalDecryptionWSS4JInInterceptor(Map<String, Object> properties) {
        super(properties);
    }

    @Override
    public void handleMessage(SoapMessage msg) throws Fault {
        try {
            if (isEncryptedData(msg)){
                getProperties().put(WSHandlerConstants.ACTION, WSHandlerConstants.SIGNATURE + " " + WSHandlerConstants.ENCRYPTION);
            } else{
                getProperties().put(WSHandlerConstants.ACTION, WSHandlerConstants.SIGNATURE);
            }
        } catch (SOAPException e) {
            throw new Fault(e);
        }

        super.handleMessage(msg);
    }

After checking the message for encryption, you can set the required ACTION and then simply call the original InInterceptor to handle the message. Add the interceptor to the message bus and you are good to go:

Note that I have added some extra configuration that specifies some details on the signature that requires checking and I have added some decryption configuration.

    public WSS4JInInterceptor generateWSS4JInputInterceptor() {
        Map<String, Object> outProps = new HashMap<String, Object>();
        outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.SIGNATURE);
        outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS, KeystoreCallbackHandler.class.getName());
        outProps.put(WSHandlerConstants.SIG_PROP_FILE, "serviceKeystore.properties");
        outProps.put(WSHandlerConstants.SIG_ALGO, "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
        outProps.put(WSHandlerConstants.SIG_C14N_ALGO, "http://www.w3.org/2001/10/xml-exc-c14n#");
        outProps.put(WSHandlerConstants.SIG_DIGEST_ALGO, "http://www.w3.org/2000/09/xmldsig#sha1");
        outProps.put(WSHandlerConstants.SIG_KEY_ID, "DirectReference");
        outProps.put(WSHandlerConstants.SIGNATURE_PARTS, "{Element}{http://schemas.xmlsoap.org/soap/envelope/}Body");
        outProps.put(WSHandlerConstants.DEC_PROP_FILE, "serviceKeystore.properties");

        return new OptionalDecryptionWSS4JInInterceptor(outProps);
    }

Testing

The final phase of this problem is the testing. You have to convince SoapUI to encrypt and sign your payload. Follow the instructions here: CLICK. The result should look something like this:

Now the only thing you need to do, is add the WS-Security headers to the request message. This can be done by going to the actual request, right-click in the message outgoing WSS -> select the option you have created and you are good to go!

Conclusion

When I was starting with this SOAP Service, I was a bit hesitant about my chances of an easy implementation. I was quite surprised on how well everything worked. The main problem I had, was finding a blog post that used the latest version of Spring Boot that did not rely on any Spring XML files. Hence, this post.