Reputation: 264
I need to add signature using X509 certificate in SOAP request in Apache JMeter. I already have .p12 with me. Please help how can I achieve it in Apache JMeter. I know how to do it in SOAPUI but not finding any way in JMeter.
Upvotes: 4
Views: 3950
Reputation: 2397
don't know wheter you need this answer still but I ran into the same problem and got it fixed after a couple of days of work and I suppose others might find this usefull as well...
I based my answer upon the answer by Dmitri T; the blog post on blazemeter, this bugreport + some custom hacking.
Let's assume you need to sign this request:
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:ser="http://custom.namespace.com/service-v1.0-rc2">
<soap:Header>
<wsa:MessageID xmlns:wsa="http://www.w3.org/2004/12/addressing">a2749a0f-555-9135-367ed901d244</wsa:MessageID>
</soap:Header>
<soap:Body>
<ser:request>
<ser:person>
<ser:id>11552</ser:id>
<ser:number>81067776992</ser:number>
</ser:person>
</ser:request>
</soap:Body>
</soap:Envelope>
You've followed the guide on blazemeter until the part with the custom java code to sign the request.
In stead of using that code do the following:
Copy paste the following code into JMeter(3.0):
import com.example.wss.SOAPSecurity;
import org.apache.jmeter.services.FileServer;
// get SOAP message from parent sampler body
String soapData = sampler.getArguments().getArgument(0).getValue();
String baseDir = FileServer.getFileServer().getBaseDir();
String pathToKeystore = baseDir + File.separator + "keystore_files" + File.separator + "your.jks";
String keystorePassword = "yourPassword";
int timeToLive = 5000;
String signingAlias = "yourAlias";
String encryptAlias = "yourEncryptingAlias";
String secureSoap = "";
try {
secureSoap = SOAPSecurity.secureSoapMessageFromString(soapData, pathToKeystore, keystorePassword, null, null, timeToLive, signingAlias, yourEncryptingAlias);
}
catch (Exception ex){
log.warn("Error in script", ex);
throw ex;
}
// replace parent sampler body with secured SOAP message
sampler.getArguments().getArgument(0).setValue(secureSoap);
vars.put("SoapDataRaw", secureSoap);
Use groovy as interpreter.
This is the java class file adapted for my use case:
package com.example.wss;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSEncryptionPart;
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.components.crypto.Merlin;
import org.apache.ws.security.message.WSSecEncrypt;
import org.apache.ws.security.message.WSSecHeader;
import org.apache.ws.security.message.WSSecSignature;
import org.apache.ws.security.message.WSSecTimestamp;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.soap.*;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.net.ssl.*;
import javax.xml.transform.dom.DOMSource;
import java.io.*;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
public class SOAPSecurity {
private KeyStore keystore;
private KeyManagerFactory keyManagerFactory;
private String keystorePassword;
private TrustManagerFactory trustManagerFactory;
private KeyStore truststore;
private String truststorePassword;
private Crypto crypto;
public SOAPSecurity(String pathToKeystore, String keystorePassword, String pathToTruststore, String truststorePassword) throws IOException, NoSuchAlgorithmException, KeyStoreException, CertificateException, UnrecoverableKeyException, KeyManagementException, WSSecurityException {
keystore = KeyStore.getInstance("JKS");
InputStream fileReader = new FileInputStream(new File(pathToKeystore));
keystore.load(fileReader, keystorePassword.toCharArray());
this.keystorePassword = keystorePassword;
keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keystore, keystorePassword.toCharArray());
Properties properties = new Properties();
properties.setProperty("org.apache.ws.security.crypto.provider", "org.apache.ws.security.components.crypto.Merlin");
crypto = CryptoFactory.getInstance(properties);
((Merlin) crypto).setKeyStore(keystore);
truststore = KeyStore.getInstance("JKS");
fileReader = new FileInputStream(new File(pathToTruststore));
truststore.load(fileReader, truststorePassword.toCharArray());
this.truststorePassword = truststorePassword;
trustManagerFactory = TrustManagerFactory.getInstance("PKIX");
trustManagerFactory.init(truststore);
}
//EDITOR: added constructor without truststore
public SOAPSecurity(String pathToKeystore, String keystorePassword) throws IOException, NoSuchAlgorithmException, KeyStoreException, CertificateException, UnrecoverableKeyException, KeyManagementException, WSSecurityException {
keystore = KeyStore.getInstance("JKS");
InputStream fileReader = new FileInputStream(new File(pathToKeystore));
keystore.load(fileReader, keystorePassword.toCharArray());
this.keystorePassword = keystorePassword;
keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keystore, keystorePassword.toCharArray());
Properties properties = new Properties();
properties.setProperty("org.apache.ws.security.crypto.provider", "org.apache.ws.security.components.crypto.Merlin");
crypto = CryptoFactory.getInstance(properties);
((Merlin) crypto).setKeyStore(keystore);
}
public static String secureSoapMessageFromFile(String messagePath, String pathToKeystore, String keystorePassword,
String pathToTruststore, String trustStorePassword, int timeToLive,
String signingAlias, String encryptAlias) throws
SAXException, ParserConfigurationException, SOAPException, IOException, WSSecurityException,
TransformerException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException,
KeyStoreException, KeyManagementException {
SOAPSecurity soapSecurity = new SOAPSecurity(pathToKeystore, keystorePassword, pathToTruststore, trustStorePassword);
SOAPMessage soapMessage = SOAPSecurity.createSOAPRequestFromFile(messagePath);
return soapSecurity.applyWSSecurity(soapMessage, timeToLive, signingAlias, encryptAlias);
}
public static String secureSoapMessageFromString(String messageString, String pathToKeystore, String keystorePassword,
String pathToTruststore, String trustStorePassword, int timeToLive,
String signingAlias, String encryptAlias) throws
SAXException, ParserConfigurationException, SOAPException, IOException, WSSecurityException,
TransformerException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException,
KeyStoreException, KeyManagementException {
SOAPSecurity soapSecurity = new SOAPSecurity(pathToKeystore, keystorePassword);
SOAPMessage soapMessage = SOAPSecurity.createSOAPRequestFromString(messageString);
return soapSecurity.applyWSSecurity(soapMessage, timeToLive, signingAlias, encryptAlias);
}
//EDITOR: ...and static signing methods as well
public static String secureSoapMessageFromString(String messageString, String pathToKeystore, String keystorePassword,
int timeToLive,
String signingAlias, String encryptAlias) throws
SAXException, ParserConfigurationException, SOAPException, IOException, WSSecurityException,
TransformerException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException,
KeyStoreException, KeyManagementException {
SOAPSecurity soapSecurity = new SOAPSecurity(pathToKeystore, keystorePassword);
SOAPMessage soapMessage = SOAPSecurity.createSOAPRequestFromString(messageString);
return soapSecurity.applyWSSecurity(soapMessage, timeToLive, signingAlias, encryptAlias);
}
public static String secureSoapMessageFromFile(String messagePath, String pathToKeystore, String keystorePassword,
int timeToLive,
String signingAlias, String encryptAlias) throws
SAXException, ParserConfigurationException, SOAPException, IOException, WSSecurityException,
TransformerException, UnrecoverableKeyException, CertificateException, NoSuchAlgorithmException,
KeyStoreException, KeyManagementException {
SOAPSecurity soapSecurity = new SOAPSecurity(pathToKeystore, keystorePassword);
SOAPMessage soapMessage = SOAPSecurity.createSOAPRequestFromFile(messagePath);
return soapSecurity.applyWSSecurity(soapMessage, timeToLive, signingAlias, encryptAlias);
}
public interface SOAPDocWriter {
Document writeDocument(String s, DocumentBuilder documentBuilder) throws IOException, SAXException;
}
public static SOAPMessage createSOAPRequestFromFile(String messagePath) throws SOAPException, IOException, ParserConfigurationException, SAXException {
SOAPDocWriter pathWriter = (s, d) -> {
File messageFile = new File(s);
return d.parse(new InputSource(new FileInputStream(messageFile)));
};
return createSOAPRequestLambda(messagePath, pathWriter);
}
public static SOAPMessage createSOAPRequestFromString(String messageString) throws SOAPException, IOException, ParserConfigurationException, SAXException {
SOAPDocWriter stringWriter = (s, d) -> d.parse(new InputSource(new StringReader(s)));
return createSOAPRequestLambda(messageString, stringWriter);
}
private static SOAPMessage createSOAPRequestLambda(String s, SOAPDocWriter w) throws SOAPException, IOException, ParserConfigurationException, SAXException
{
//MessageFactory messageFactory = MessageFactory.newInstance();
//we use the SOAP 1.2 specification
MessageFactory messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);
SOAPMessage soapMessage = messageFactory.createMessage();
SOAPPart soapPart = soapMessage.getSOAPPart();
SOAPEnvelope soapEnvelope = soapPart.getEnvelope();
SOAPBody soapBody = soapEnvelope.getBody();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
Document document = w.writeDocument(s, documentBuilder);
soapBody.addDocument(document);
soapMessage.saveChanges();
return soapMessage;
}
public static Document toDocument(SOAPMessage soapMsg) throws TransformerConfigurationException, TransformerException, SOAPException, IOException {
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();
}
/**
* Secures a soap message according to the given security actions
*
* @param soapMessage the soap message to be secured
* @param timestampTimeToLive optional: the time to live for the timestamp
* @param signatureKeyAlias optional: the alias for the signature key in the keystore
* @param encryptionKeyAlias optional: the alias for the encryption key in the keystore
* @throws WSSecurityException
* @throws IOException
* @throws SOAPException
* @throws TransformerException
*/
public String applyWSSecurity(SOAPMessage soapMessage,
int timestampTimeToLive,
String signatureKeyAlias,
String encryptionKeyAlias) throws WSSecurityException, IOException, SOAPException, TransformerException {
Document soapMessageDocument = toDocument(soapMessage);
// add security header
WSSecHeader securityHeader = new WSSecHeader();
securityHeader.setMustUnderstand(false);
//we keep the security header element because we need it afterwards
Element secHead = securityHeader.insertSecurityHeader(soapMessageDocument);
/*
for a reason not yet clear to me this method of signing creates a soap request inside a soap request
I don't know why but I know how to work around it:
*/
//append the security header to the soap:Header parent
soapMessageDocument.getElementsByTagName("soap:Header").item(0).appendChild(secHead);
//move the soap:Envelope inner soap message to the root of the document and omit the env:Envelope tree
soapMessageDocument.replaceChild(soapMessageDocument.getElementsByTagName("soap:Envelope").item(0),
soapMessageDocument.getElementsByTagName("env:Envelope").item(0));
//for debugging purposes -> this output shows up in the console output of JMeter.bat
//System.out.println("insert:"+soapMessageDocument.getFirstChild().getNodeName()+",soap:"+soapMessageDocument.getElementsByTagName("soap:Envelope").item(0).getNodeName());
WSSecTimestamp timestamp = null;
// timestamp document
timestamp = new WSSecTimestamp();
timestamp.setTimeToLive(timestampTimeToLive);
timestamp.build(soapMessageDocument, securityHeader);
// sign document
/*
EDITOR: originals are commented out and replaced by own values
values should be adapted from SOAPUI and searched for at:
https://ws.apache.org/wss4j/apidocs/org/apache/wss4j/dom/WSConstants.html
*/
WSSecSignature signatureBuilder = new WSSecSignature();
signatureBuilder.setUserInfo(signatureKeyAlias, keystorePassword);
signatureBuilder.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE);
//signatureBuilder.setSignatureAlgorithm("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
signatureBuilder.setSignatureAlgorithm("http://www.w3.org/2000/09/xmldsig#rsa-sha1");
signatureBuilder.setSigCanonicalization(WSConstants.C14N_EXCL_OMIT_COMMENTS);
/*
also setDigestAlgo can be set
https://ws.apache.org/wss4j/apidocs/org/apache/wss4j/dom/message/WSSecSignature.html#setDigestAlgo-java.lang.String-
but I used the default so I didn't bother
*/
//also custom
signatureBuilder.setUseSingleCertificate(true);
List<WSEncryptionPart> signatureParts = new ArrayList<WSEncryptionPart>();
//WSEncryptionPart timestampPart = new WSEncryptionPart(timestamp.getId());
WSEncryptionPart timestampPart = new WSEncryptionPart("Timestamp","http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd","Content");
signatureParts.add(timestampPart);
//WSEncryptionPart bodyPart = new WSEncryptionPart(WSConstants.ELEM_BODY, WSConstants.URI_SOAP11_ENV, "Element");
WSEncryptionPart bodyPart = new WSEncryptionPart("Body","http://www.w3.org/2003/05/soap-envelope", "Content");
signatureParts.add(bodyPart);
signatureBuilder.setParts(signatureParts);
signatureBuilder.build(soapMessageDocument, crypto, securityHeader);
// encrypt document
/*
we didn't encrypt so no need for this code
(encryption code untested (by me))
WSSecEncrypt encrypt = new WSSecEncrypt();
encrypt.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE);
encrypt.setSymmetricEncAlgorithm(WSConstants.AES_128_GCM);
encrypt.setKeyEncAlgo(WSConstants.KEYTRANSPORT_RSAOEP);
encrypt.setUserInfo(encryptionKeyAlias, keystorePassword);
List<WSEncryptionPart> encryptionParts = new ArrayList<WSEncryptionPart>();
WSEncryptionPart encryptionSignaturePart = new WSEncryptionPart("Signature", WSConstants.SIG_NS, "Element");
WSEncryptionPart encryptionBodyPart = new WSEncryptionPart("Body", WSConstants.URI_SOAP11_ENV, "Content");
encryptionParts.add(encryptionBodyPart);
encryptionParts.add(encryptionSignaturePart);
encrypt.setParts(encryptionParts);
encrypt.build(soapMessageDocument, crypto, securityHeader);
*/
DOMSource domSource = new DOMSource(soapMessageDocument);
soapMessage.getSOAPPart().setContent(domSource);
soapMessage.saveChanges();
ByteArrayOutputStream out = new ByteArrayOutputStream();
soapMessage.writeTo(out);
String strMsg = new String(out.toByteArray());
return strMsg;
}
}
Put the code in a com\example\wss\SOAPSecurity.java file
Put wss4j-1.6.18.jar in the same dir. http://archive.apache.org/dist/ws/wss4j/1.6.18/
I compiled/build/deployed it with this .sh script (you can use cygwin for that):
/cygdrive/c/Program\ Files\ \(x86\)/Java/jdk1.8.0_73/bin/javac.exe -cp wss4j-1.6.18.jar com/example/wss/SOAPSecurity.java
cp SOAPSecurity.jar SOAPSecurity.jar.bak$1
zip -r test.zip com/
mv test.zip SOAPSecurity.jar
cp SOAPSecurity.jar /cygdrive/c/Program\ Files\ \(x86\)/apache-jmeter-3.0/lib/
Run JMeter 3.0 with the .bat file
-> the SOAPSecurity.jar should be in the apache-jmeter-3.0/lib/ folder
-> I have also these configuration in JMeter's system.properties file:
-Djavax.net.ssl.keyStore=C:/path/to/client.jks
-Djavax.net.ssl.keyStorePassword=verySecret
-> and added a keystore configuration element to the soap request in JMeter
So, this is about it, feel free to give it a spin and let me know whether it worked for you, if not I might provide some help, if you'd like to debug yourself an excelent way to do so is by adapting/placing System.out.println's in the custom class, it gives very usefull information about what's going on and can be quite a life saver,
Keep having fun!!!
S.
Upvotes: 3
Reputation: 168042
You need to do some scripting in order to encrypt the message via the JSR223 PreProcessor. The idea is to get current sampler body, encrypt it and replace on-the-fly.
Use the following code as a reference:
import com.sun.org.apache.xml.internal.security.Init;
import com.sun.org.apache.xml.internal.security.c14n.Canonicalizer;
import com.sun.org.apache.xml.internal.security.signature.XMLSignature;
import com.sun.org.apache.xml.internal.security.transforms.Transforms;
import com.sun.org.apache.xml.internal.security.utils.Constants;
import com.sun.org.apache.xml.internal.security.utils.XMLUtils;
import org.apache.jmeter.protocol.http.sampler.SoapSampler;
import org.apache.commons.io.FileUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Date;
//write sampler body into "signature.xml" file
String body = sampler.getXmlData();
FileUtils.writeStringToFile(new File("signature.xml"),body);
//X509 properties
String keystoreType = "JKS";
String keystoreFile = "wso2carbon.jks";
String keystorePass = "wso2carbon";
String privateKeyAlias = "wso2carbon";
String privateKeyPass = "wso2carbon";
String certificateAlias = "wso2carbon";
Element element = null;
String BaseURI = signatureFile.toURI().toURL().toString();
//SOAP envelope to be signed
//get the private key used to sign, from the keystore
KeyStore ks = KeyStore.getInstance(keystoreType);
FileInputStream fis = new FileInputStream(keystoreFile);
ks.load(fis, keystorePass.toCharArray());
PrivateKey privateKey =
(PrivateKey) ks.getKey(privateKeyAlias, privateKeyPass.toCharArray());
//create basic structure of signature
javax.xml.parsers.DocumentBuilderFactory dbf =
javax.xml.parsers.DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
String request = sampler.getXmlData();
ByteArrayInputStream in = new ByteArrayInputStream(request.getBytes());
Document doc = dBuilder.parse(in);
in.close();
Init.init();
XMLSignature sig =
new XMLSignature(doc, BaseURI, XMLSignature.ALGO_ID_SIGNATURE_RSA);
element = doc.getDocumentElement();
element.normalize();
element.getElementsByTagName("soapenv:Header").item(0).appendChild(sig.getElement());
{
Transforms transforms = new Transforms(doc);
transforms.addTransform(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
//Sign the content of SOAP Envelope
sig.addDocument("", transforms, Constants.ALGO_ID_DIGEST_SHA1);
}
//Signing procedure
{
X509Certificate cert =
(X509Certificate) ks.getCertificate(certificateAlias);
sig.addKeyInfo(cert);
sig.addKeyInfo(cert.getPublicKey());
sig.sign(privateKey);
}
//write signature to file
FileOutputStream f = new FileOutputStream(signatureFile);
XMLUtils.outputDOMc14nWithComments(doc, f);
f.close();
//set sampler's XML data from file
String request = FileUtils.readFileToString(signatureFile);
sampler.setXmlData(request);
You will need to replace keystore and encryption related bits as per your configuration and service definition
See Take the Pain out of Load Testing Secure Web Services for comprehensive explanation
Upvotes: 2