By Katrin Grunert on 28 juni 2021
SOAP is not dead. It is an established, XML-based and mature messaging protocol that comes with built-in security mechanisms, integrity checks, content validation and much more. A lot of enterprises and corporations are using it (sadly) still. Just recently, Vandebron had to implement a SOAP client to communicate with an external party. This blog post will explain with code examples how we at Vandebron are signing and verifying SOAP messages for our latest SOAP client implementation.
For this process, we are using Apache's Web Service Security Library wss4j as it is a proven tool in the WSS context and provides, as a Java library, great interoperability with the programming language Scala.
Here we will take a look at the necessary steps to sign a SOAP message like this one:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Header/> <soapenv:Body> <heading>Hello World</heading> <body>I am just a test</body> </soapenv:Body> </soapenv:Envelope>
To look after signing like this:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Header> <wsse:Security soapenv:mustUnderstand="1" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"> <ds:Signature Id="SIG-ec946953-2470-4689-ad2f-0c579e1e06e3" 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="soapenv" xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/> </ds:CanonicalizationMethod> <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/> <ds:Reference URI="#id-47817454-f6e2-470c-9109-870e7895e3e0"> <ds:Transforms> <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> </ds:Transforms> <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/> <ds:DigestValue>7KfPcTwDYWtLj4ZVWmWmVqX4IGwbBAAmUPigCdXdk4U=</ds:DigestValue> </ds:Reference> </ds:SignedInfo> <ds:SignatureValue> OBnbBWv8S70xDDn5uG++7cTRFa2Uz3D47oxTHuO163Y3/V7H35M1GHXbKaUDOHsgsfx3SdVmVi++ra06cpwJknzqoIQgDV9Qc0ydzfxljCqupPKBnfONDYJtihEE1jtQ0RP7OLzPVNUpgOgHqbLwJu2pRUA05ool+lxIs924OwPVPKyUryoYwWhwY1ttY4P+WY2L3ZqsH3fgoLCyjlvhDEAhsP9PCxsEzPSq3ECC55Nh7nqMoHPj2uNxonuMlPeYbrlMnwyiqEW8s3Sc+WmfiIOgekRE1AdNhpn3ARlO490nObQtXCU/TxeTfbh98TMbQRZWWyT4HuLS3fF6aeyD/Q== </ds:SignatureValue> <ds:KeyInfo Id="KI-e18395de-9a26-4cad-9501-7c6cf6c7c74a"> <wsse:SecurityTokenReference wsu:Id="STR-daa47836-f1f9-4d71-95cc-b7bcc6051c84"> <wsse:KeyIdentifier ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"> ox4ajWTdigy9oApTYs97CuCV/4k= </wsse:KeyIdentifier> </wsse:SecurityTokenReference> </ds:KeyInfo> </ds:Signature> </wsse:Security> </soapenv:Header> <soapenv:Body wsu:Id="id-47817454-f6e2-470c-9109-870e7895e3e0" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"> <heading>Hello World</heading> <body>I am just a test</body> </soapenv:Body> </soapenv:Envelope>
For implementing the steps of the blog post you will need:
Our private and public key pair are available in the PKCS#12-format (.p12 file extension). Check out this to learn more about this format and how to achieve it. The pool of trusted certificates are in the PKCS#7 format (.p7b file extension).
First we have to setup the necessary dependencies:
// in your build.sbt or project/Dependencies.scala // enabling signing and signature verification for SOAP messages lazy val webServiceSecurity = Seq( "org.apache.wss4j" % "wss4j" % "2.3.1" pomOnly (), "org.apache.wss4j" % "wss4j-ws-security-dom" % "2.3.1", "org.apache.wss4j" % "wss4j-ws-security-common" % "2.3.1" ) libraryDependencies ++= webServiceSecurity
Next, we continue with a scala representation of our certificate we are using for signing:
import org.apache.wss4j.dom.WSConstants // algorithm configuration object SigningCertificate { val CanonicalizationMethodURI: String = "http://www.w3.org/2001/10/xml-exc-c14n#" val DigestAlgorithmURI: String = DigestMethod.SHA256 val SignatureAlgorithmURI: String = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" val KeyIdentifierType: Int = WSConstants.SKI_KEY_IDENTIFIER } case class SigningCertificate(keyStore: KeyStore, password: String) { require( keyStore.aliases().asScala.size == 1, s"Certificate of Keystore needs to have one alias but had ${keyStore.aliases().asScala.size}" ) val alias: String = keyStore.aliases().nextElement() override def toString: String = s"SigningCertificate(alias=$alias)" }
In the documentation of the SOAP service that you want to call should stand some information regarding the canonicalization method, signature algorithm, digest algorithm, and the key identifier type. Those are algorithms and information that define the signing process and we explain roughly now.
Before signing a message it has to be canonicalized. "Canonicalization is a method for generating a physical representation, the canonical form, of an XML document that accounts for syntactic changes permitted by the XML specification" (from here). In our case, the Exclusive XML Canonicalization is used.
The digest algorithm is used to ensure the integrity of the message during the verification of a signature. The algorithm is used to calculate a hash of the signed message. It should be documented in the SOAP service documentation. Here we will use SHA256 as a hashing algorithm.
The signature algorithm describes how the message will be signed. It can be defined in the SOAP service documentation but in the worst case you can read this algorithm from the certificate itself by using keytool
:
$ keytool -list -v -keystore signature.p12 Enter keystore password: ... [...] # more information about the certificates Signature algorithm name: SHA256withRSA # thats what we are after! [...] # more information about the certificates
According to the keytool inspection we will use SHA256withRSA (http://www.w3.org/2001/04/xmldsig-more#rsa-sha256) for signing.
Last but not least, in our signature, a <KeyInfo>
element is included. This element contains information about the public key of the sender (us) and is needed for the signature verification once the message is received (read more here). Since we have our public key provided we don't need to do much here. The KeyIdentifierType
describes which form of key identifier is used to present the public key information.
Having all this information about our certificate in place, we build the mechanism to load in our signing certificate. For this, we create the object KeyStoreBuilder
.
import java.io.{File, FileInputStream} object KeyStoreBuilder { def loadSigningCertificate(signingCertificate: File, password: String): SigningCertificate = { val fis = new FileInputStream(signingCertificate) val ks: KeyStore = KeyStore.getInstance("PKCS12") ks.load(fis, password.toCharArray) SigningCertificate(ks, password) } }
Bear in mind, that you probably don't want to version any sensitive information like private keys and passwords hard-coded or in any environment variables, so a safe mechanism for storing/fetching passwords and certificates (like Vault) should be in place.
With the signing certificate in place, we can actually start signing a message. The next code example contains quite some Java boilerplate from wss4j that is required to make the signing mechanism work.
To restrict the usage of Java classes to a small portion of our code we will firstly implement a conversion method .toElem
inside of the companion object SigningService
:
import java.io.StringWriter import javax.xml.transform.{OutputKeys, TransformerFactory} import javax.xml.transform.dom.DOMSource import javax.xml.transform.stream.StreamResult import org.w3c.dom.Document import scala.xml.Elem object SigningService { implicit class RichDocument(document: Document) { private val tf = TransformerFactory.newInstance() def toElem: Elem = val transformer = tf.newTransformer() transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); val stringWriter = new StringWriter() transformer.transform(new DOMSource(document), new StreamResult(stringWriter)) scala.xml.XML.loadString(stringWriter.getBuffer.toString) } }
With that, we can convert any Document
SOAP message representation back to the scala.xml
supported Elem
format.
class SigningService(signingCertificate: SigningCertificate) { // importing our conversion method import SigningService.RichDocument /** * REQUIRED, otherwise it will throw: * * org.apache.wss4j.common.ext.WSSecurityException: * You must initialize the xml-security library correctly before you use it. * Call the static method "org.apache.xml.security.Init.init();" * to do that before you use any functionality from that library */ org.apache.xml.security.Init.init() private val documentBuilderFactory = DocumentBuilderFactory.newInstance() private val crypto: Merlin = getCrypto crypto.setKeyStore(signingCertificate.keyStore) def signElement(elem: Elem): Elem = { documentBuilderFactory.setNamespaceAware(true) // converting Elem to Document (Scala to Java conversion) val doc = documentBuilderFactory.newDocumentBuilder().parse(new InputSource(new StringReader(elem.toString()))) // WSSecHeader wraps around the document we want to sign val header = new WSSecHeader(doc) header.setMustUnderstand(true) header.insertSecurityHeader() // start building Signature, use the (wrapper) header-instance val builder = new WSSecSignature(header) builder.setUserInfo(signingCertificate.alias, signingCertificate.password) // setting algorithms builder.setSignatureAlgorithm(SigningCertificate.SignatureAlgorithmURI) builder.setSigCanonicalization(SigningCertificate.CanonicalizationMethodURI) builder.setDigestAlgo(SigningCertificate.DigestAlgorithmURI) builder.setKeyIdentifierType(SigningCertificate.KeyIdentifierType) builder.setAddInclusivePrefixes(true) // signing the document! val signedDocument = builder.build(crypto) // conversion back to Elem signedDocument.toElem } private def getCrypto: Merlin = { val properties = new Properties() properties.setProperty("org.apache.wss4j.crypto.provider", "class org.apache.ws.security.components.crypto.Merlin") CryptoFactory.getInstance().asInstanceOf[Merlin] } }
Wss4j is a library that maintains an internal state during a signing process, but to avoid confusion it can be summarized as:
WSSecHeader
wraps around the document to be signedheader
will be used as part of the WSSecSignature
-Builderbuilder
gets configured with all necessary information, which algorithms are used for signing, digesting, canonicalization, which key identifier should be included. Those settings an vary from webservice to webservice.The actual signing of the document, which is now nested like a matryoshka doll, is happening with the help of an instance of Crypto
. Crypto
will contain either a keystore or a truststore or even both. It needs to be specified in the crypto.properties
file or a runtime which class of Crypto will be used.
The most common one is Merlin
.
We have decided to specify its configuration during runtime, since it is more visible than a properties file. Nevertheless, the crypto.properties
-file needs to exist in your resources
folder neverthless otherwise you will get a following WSSecurityException
:
org.apache.wss4j.common.ext.WSSecurityException: No message with ID "resourceNotFound" found in resource bundle "org/apache/xml/security/resource/xmlsecurity" [... rest of stacktrace ...] Cause: java.nio.file.NoSuchFileException: crypto.properties
And that's it! The KeyStoreBuilder
helps us to load a SigningCertificate
and the SigningService
uses this loaded certificate to sign SOAP messages.
A receiver of our SOAP message has all the necessary information in our signature to verify that this message has not been tampered with and we are the original sender.
This verification is something we should also do on our side for incoming messages. So let's take a look at how we can verify the signature of received messages.
Verifying the signature of incoming messages is equally important to ensure that the connection is secure. A verification process will tell you if the message is coming from a trusted source and has not been tampered with.
As previously mentioned we need our source of truth, a pool of trusted public keys from all parties which will receive our SOAP messages. These build the basis of the trust store.
We will create a TrustedCertificates
wrapper class in which we will load in the trust store and add this method to the KeyStoreBuilder
.
case class TrustedCertificates(keyStore: KeyStore) object KeyStoreBuilder { def loadTrustedCertificate(certificates: Seq[File]): TrustedCertificates = { val ks = KeyStore.getInstance(KeyStore.getDefaultType) // we just want the keystore to act as a truststore (only containing trusted certificates), so we initialize it empty ks.load(null, null) val cf = CertificateFactory.getInstance("X.509") certificates.foreach { file => CloseableUtil.using(getClass.getResourceAsStream(file.getPath)) { fis => val certPath = cf.generateCertPath(fis, "PKCS7") certPath.getCertificates.asScala.toList.foreach { certificate => ks.setCertificateEntry(file.getName, certificate) } } } TrustedCertificates(ks) } }
This trust store is under the hood also just a KeyStore, without containing a private key that requires a password, that's why we can initialize the KeyStore with null
-parameters.
Now, the SigningService needs to be extended with this trusted certificates and a verifySignatureOf
-method:
import java.io.StringReader import java.util.Properties import javax.xml.parsers.DocumentBuilderFactory import org.apache.wss4j.common.crypto.{ CryptoFactory, Merlin } import org.apache.wss4j.dom.engine.WSSecurityEngine import org.xml.sax.InputSource import scala.util.{Failure, Success, Try} import scala.xml.Elem class SigningService(signingCertificate: SigningCertificate, trustedCertificates: TrustedCertificates) { private val engine = new WSSecurityEngine() private val documentBuilderFactory = DocumentBuilderFactory.newInstance() private val crypto: Merlin = getCrypto crypto.setKeyStore(signingCertificate.keyStore) crypto.setTrustStore(trustedCertificates.keyStore) def verifySignatureOf(elem: Elem): Boolean = { documentBuilderFactory.setNamespaceAware(true) val doc = documentBuilderFactory.newDocumentBuilder().parse(new InputSource(new StringReader(elem.toString()))) Try(engine.processSecurityHeader(doc, null, null, crypto)) match { case Success(_) => true case Failure(exception) => // replace with proper logging println( s"Unsuccessful signature verification, it is most likely that the certificate used for signing is not in our Truststore: ${exception.getMessage}") false } } private def getCrypto: Merlin = { val properties = new Properties() properties.setProperty("org.apache.wss4j.crypto.provider", "class org.apache.ws.security.components.crypto.Merlin") CryptoFactory.getInstance().asInstanceOf[Merlin] } }
And with that, we have completed our roundtrip of signing and verifying SOAP messages!
Here are gists, articles, and documentation that inspired and helped us to figure out the signing and verification process for our SOAP client. Feel free to check them out!
WSSecurityVerifier by Luis Wolff
WSSecuritySigner by Luis Wolff