Sunday, January 24, 2016

Set up Service and Client for Sign and Encrypt.


During one of my assignment to implement message level security for a SOAP based web service, I came across many difficulties and hurdles. After completing the assignment, I thought to share the experience so that, someone else can easily get solution.

 To explain the entire solution, I'll be using Netbeans IDE and it's sample web services.

To Create a Web Service:

Step 1: Create a Sample Calculator Web Service


Click on create new project and select Samples->WebServices->Calculator
 


Step 2:
Click next and then finish.

Step 3:
Select Calculator application and start it. It will open next screen




Create Client:
 Step 1:



Create a Java application and then create a new web service client as in below screen



Step 2:

 Select WSDL URL in above screen and then enter Calculator service WSDL url. Enter a package name then click finish. It will automatically generate Web Services Reference
Step 3:
Drag Newly created web service reference by expanding Web Services References tree named add into a newly created java class TestServices. It will automatically create service call.






  The code of newly create client code is as below.

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.test;

/**
 *
 * @author shashi.ranjan
 */
public class TestService {

    public static void main(String[] args) {
        int result = add(4, 5);
        System.out.println(" Adding two numbers : " + result);
    }

    private static int add(int i, int j) {
        com.test.CalculatorWSService service = new com.test.CalculatorWSService();
        com.test.CalculatorWS port = service.getCalculatorWSPort();
        return port.add(i, j);
    }

}

Step 4:

Create a SecurityHandler class into you client application. Below is the code.

package com.test;

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.components.crypto.Crypto;
import org.apache.ws.security.components.crypto.CryptoFactory;
import org.apache.ws.security.message.WSSecEncrypt;
import org.apache.ws.security.message.WSSecEncryptedKey;
import org.apache.ws.security.message.WSSecHeader;
import org.apache.ws.security.message.WSSecSignature;
import org.w3c.dom.Document;

/**
*
* @author shashi.ranjan
*/
public class SecurityHandler implements SOAPHandler<SOAPMessageContext> {

    @Override
    public boolean handleMessage(SOAPMessageContext messageContext) {
        try {

            /**
             * Getting document from soap context
             */
            SOAPMessage message = messageContext.getMessage();
            SOAPEnvelope envelop = message.getSOAPPart().getEnvelope();
            Document doc = toDocument(message);
            /**
             * Creating crypto Object.
             */
            Crypto crypto = CryptoFactory.getInstance("crypto.properties");
            /**
             * Using WSS4J API to create encryptor key
             */
            WSSecEncryptedKey encrKey = new WSSecEncryptedKey();
            encrKey.setKeyIdentifierType(WSConstants.SKI_KEY_IDENTIFIER);
            encrKey.setUserInfo("selfsigned", "demo");
            encrKey.prepare(doc, crypto);
            /**
             * Creating encrypter out of encrypt key
             */
            WSSecEncrypt builder = new WSSecEncrypt();
             builder.setUserInfo("selfsigned", "demo");
            builder.setEncKeyId(encrKey.getId());
            builder.setEphemeralKey(encrKey.getEphemeralKey());
            builder.setEncryptSymmKey(false);
            builder.setEncryptedKeyElement(encrKey.getEncryptedKeyElement());
            builder.setKeyIdentifierType(WSConstants.SKI_KEY_IDENTIFIER);
            builder.setUseKeyIdentifier(true);
            /**
             * Preparing to sign the document
             */
            WSSecSignature sign = new WSSecSignature();
            sign.setCustomTokenId(encrKey.getId());
            sign.setSignatureAlgorithm(SignatureMethod.RSA_SHA1);
            sign.setKeyIdentifierType(WSConstants.SKI_KEY_IDENTIFIER);
            sign.setUserInfo("selfsigned", "demo");
            /**
             * Creating Security Header to insert into document
             */
            WSSecHeader secHeader = new WSSecHeader();
            secHeader.insertSecurityHeader(doc);
            /**
             * Encrypting document with crypto and security header
             */
            Document signedDoc = sign.build(doc, crypto, secHeader);
            builder.build(signedDoc, crypto, secHeader);
            /**
             * Getting encrypted document
             */
            Document encryptedDoc = doc;
            /**
             * Updating message with encrypted document
             */
            DOMSource domSource = new DOMSource(encryptedDoc);
            message.getSOAPPart().setContent(domSource);
            String outputString = org.apache.ws.security.util.XMLUtils.PrettyDocumentToString(encryptedDoc);
            System.out.println("Encrypted Document : " + outputString);
            /**
             * Logging encrypted soap into file
             */
            Boolean outboundProperty = (Boolean) messageContext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
            if (outboundProperty) {
                logToSystemOut(message);
            }
            /**
             * Updating message before sending to service.
             */
            message.saveChanges();

        } catch (SOAPException | WSSecurityException | TransformerException ex) {
            Logger.getLogger(SecurityHandler.class.getName()).log(Level.SEVERE, null, ex);
        } catch (Exception ex) {
            Logger.getLogger(SecurityHandler.class.getName()).log(Level.SEVERE, null, ex);
        }
        return true;
    }

    @Override
    public Set<QName> getHeaders() {
        return Collections.EMPTY_SET;
    }

    @Override
    public boolean handleFault(SOAPMessageContext messageContext) {
        return true;
    }

    @Override
    public void close(MessageContext context) {
    }

    public static org.w3c.dom.Document toDocument(SOAPMessage soapMsg)
            throws SOAPException, TransformerException {
        Source src = soapMsg.getSOAPPart().getContent();
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        DOMResult result = new DOMResult();
        transformer.transform(src, result);
        return (Document) result.getNode();
    }

    private void logToSystemOut(SOAPMessage message) throws SOAPException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyymmddhhmm");
        Date date = new Date();
        String dateString = dateFormat.format(date);
        System.out.println("\nOutbound message:");
        File file = null;
        PrintStream out = null;
        try {
            String fileName = "request_invoicedetails_" + dateString + ".xml";
            file = new File("D:\\keystore\\" + fileName);
            if (!file.exists()) {
                file.createNewFile();
            }
            out = new PrintStream(file);

            message.writeTo(out);
            System.out.println(out.toString());
        } catch (IOException | SOAPException e) {
            System.out.println("Exception in handler: " + e);
        } finally {
            out.close();
        }

    }
}

The above code is responsible for signing and encrypting the SOAP message. It also dumps SOAP request into a file. Please change path of the file as per your convenience.

Step 5:
Add this SecurityHandler to the webservice by right click on service referece and select add handler.





Run TestService will give you SOAPFaultException because add service is not secure. The same security handler has worked for me in real world scenario where it was a secure SAP service.

Libraries Used:

Its important to note that, whenever I changed versions of jars, I was getting error and exceptions. 





















Setting up crypto properties:

 In order to run the above program, the key is hidden into setting up crypto.properties. I'll explain the process of creating and setting up crypto.properties.

Step 1:
Create a file crypto.properties using notepad and add below entries into it.

org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin
org.apache.ws.security.crypto.merlin.keystore.type=pkcs12
org.apache.ws.security.crypto.merlin.keystore.password=demo
org.apache.ws.security.crypto.merlin.keystore.alias=selfsigned
org.apache.ws.security.crypto.merlin.alias.password=demo
org.apache.ws.security.crypto.merlin.file=d:\\keystore\\mykeystore.pkcs12

The above propeties values are self explanatory. Replace values of your keystore, type and password as per your keystore values.

Step 2:
 Add this crypto.properties into wss4j.1.5.9.jar by executing below command and update library.Once you add this property file to your jar file, you'll be able to create crypto object easily by calling

Crypto crypto = CryptoFactory.getInstance("crypto.properties");

jar -uvf wss4j.1.5.9.jar crypto.properties

Steps to create Sample keystore
Keystores can be created using either keytool or openssl tool. I'll explain both ways.

Using Keytool:



keytool -genkey -keyalg RSA -alias selfsigned -keystore d:\keystore\mycert.jks -storepass shashi

You can change values as per actual you want to use.

To give public certificate to someone else you can create .cer from this keystore and share.


keytool -export -keystore d:\keystore\mycert.jks -alias selfsigned -file mycert.cer

 

Using openssl:

Step1: Create Key and Certificate using openssl command

openssl req -new -x509 -extensions v3_ca -keyout private/MyKey.pem -out MyKeycert.pem -days 3650 -config ./openssl.conf

Content of openssl.conf:
[ req ]
default_bits = 1024 # Size of keys
default_keyfile = key.pem # name of generated keys
default_md = md5 # message digest algorithm
string_mask = nombstr # permitted characters
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
# Variable name   Prompt string
#----------------------   ----------------------------------
0.organizationName = Organization Name (company)
organizationalUnitName = Organizational Unit Name (department, division)
emailAddress = Email Address
emailAddress_max = 40
localityName = Locality Name (city, district)
stateOrProvinceName = State or Province Name (full name)
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64

# Default values for the above, for consistency and less typing.
default_keyfile = key.pem # name of generated keys
default_md = md5 # message digest algorithm
string_mask = nombstr # permitted characters
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
# Variable name   Prompt string
#----------------------   ----------------------------------
0.organizationName = Organization Name (company)
organizationalUnitName = Organizational Unit Name (department, division)
emailAddress = Email Address
emailAddress_max = 40
localityName = Locality Name (city, district)
stateOrProvinceName = State or Province Name (full name)
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64

# Default values for the above, for consistency and less typing.
# Variable name   Value
#------------------------------   ------------------------------
0.organizationName_default = The Sample Company
localityName_default = Metropolis
stateOrProvinceName_default = New York
countryName_default = US

[ v3_ca ]
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always

Step 2:
 Export certificate in .pkcs12 format
cat private/MyKey.pem MyKeycert.pem > MyKeyCertificate.pem

openssl pkcs12 -export -in MyKeyCertificate.pem -out MyKeyCertificate.pkcs12 -name selfsigned -noiter -nomaciter

choose password as per actual.

If you have to give public certificate after extracting from MyKeyCertificate.pkcs12, then use below command to do so.

keytool -keystore MyKeyCertificate.pkcs12 -storetype pkcs12 -exportcert -alias selfsigned  -file MyKeyCertificate.cer