Updated on October 26, 2017
There are lots of articles and other documents written about securing the SOAP messages: XML Signature, Web Services Security, SOAP-DSIG and SSL. SOAP is a standard protocol used to exchange any XML documents. Such XML document exchanging can be applied in many kinds of business starting from public SOAP services for obtaining the latitude/longitude coordinates of an address and ending with the private booking of a hotel room. This leads to a necessity to protect the XML data being transmitted from unauthorized reading and modifying. A detailed explanation of the main purposes of securing SOAP messages can be found in the Web Services Security article, by Bilal Siddiqui.
This article introduces the working sample code in Borland Delphi which implements the digital singing of the SOAP messages using the SHA hash algorithm and the private key cryptography with X509 certificates.
As described in http://www.w3.org/TR/SOAP/, every SOAP message has a SOAP envelope with its body and an unnecessary header inside. The task of digitally signing a SOAP XML message content can be divided into two common tasks: calculating the digest hashes for each XML content part to be secured and the digital signing of composed digest hashes together with the references to the corresponding XML content parts.
As required the SOAP Security specification, before calculating hash values or digital signatures for a specified XML data part, we must apply the XML canonicalization process in order to obtain the logical equivalence between XML documents. The XML canonicalization specification can be read at Canonical XML and also the XML Canonicalization provides detailed descriptions of the XML canonicalization process steps. So, first, let us go ahead and consider the working Delphi class which implements a simple case of the XML document canonicalization process.
A simple case of the canonicalization process without having the references to external XML documents and also without CDATA inclusions can be performed using the following steps:
- encoding the given XML data with the UTF-8 encoding scheme;
- replacing all combinations of line breaks (#xD or a combination of #xA and #xD) with #xA;
- Attribute values normalization (replacing non-ASCII characters with their escape representations, all #x9 characters and also line breaks with #x20);
- double quoting of all attribute values;
- excluding XML and DTD declarations;
- excluding white spaces around all document elements;
- expanding empty elements;
- ordering namespace declarations and attributes.
In order to simplify the description, we used the Microsoft XMLDom document object engine for manipulating with XML data. The listing below demonstrates an algorithm for combining the XML nodes in canonical form:
function TclXmlCanonicalizer.BuildXmlString(ARootNode: IXMLDOMNode): string; var i: Integer; begin if Supports(ARootNode, IXMLDOMText) then begin Result := Result + VarToStr(ARootNode.nodeValue); end else begin Result := '<' + ARootNode.nodeName + BuildAttributes(ARootNode) + '>';
for i := 0 to ARootNode.childNodes.length - 1 do begin Result := Result + BuildXmlString(ARootNode.childNodes.item[i]); end;
Result := Result + '</' + ARootNode.nodeName + '>'; end; end; |
Next, we need to order both the node namespaces and attributes in the ascending lexicographic order. In other words, we can use the Delphi string list object for ordering the namespaces and attributes declared within the current XML node:
function TclXmlCanonicalizer.BuildAttributes(ANode: IXMLDOMNode): string; var i: Integer; attributes, namespaces: TStringList; element: IXMLDOMElement; begin Result := '';
if not Supports(ANode, IXMLDOMElement) then Exit;
attributes := nil; namespaces := nil; try attributes := TStringList.Create(); attributes.Sorted := True;
namespaces := TStringList.Create(); namespaces.Sorted := True;
element := (ANode as IXMLDOMElement); for i := 0 to element.attributes.length - 1 do begin if (system.Pos('xmlns', LowerCase(element.attributes.item[i].nodeName)) = 1) then begin namespaces.Add(element.attributes.item[i].nodeName + '="' + NormalizeAttributeValue(VarToStr(element.attributes.item[i].nodeValue)) + '"'); end else begin attributes.Add(element.attributes.item[i].nodeName + '="' + NormalizeAttributeValue(VarToStr(element.attributes.item[i].nodeValue)) + '"'); end; end;
for i := 0 to namespaces.Count - 1 do begin Result := Result + ' ' + Trim(namespaces[i]); end;
for i := 0 to attributes.Count - 1 do begin Result := Result + ' ' + Trim(attributes[i]); end; finally namespaces.Free(); attributes.Free(); end; end; |
Once the XML document part has been normalized and canonicalized, it is ready for applying the digital signature and digest hash calculation algorithms.
In this step we need to combine the SignedInfo XML node containing references to the XML data being secured together with their digest hash values. The function below accepts the whole SOAP XML document and also the reference URI list, which will be used to locate to the required XML nodes. This CreateSignedInfo function returns a newly created the SignedInfo XML node according to the Canonical XML specification.
function TclSoapRequest.CreateSignedInfo(ADom: IXMLDomDocument; ANameSpace: string; ASignReferences: TStrings): IXMLDomNode; var i: Integer; reference, data, node: IXMLDomNode; canonicalizer: TclXmlCanonicalizer; encoder: TclEncoder; digestValue: string; begin encoder := nil; canonicalizer := nil; try encoder := TclEncoder.Create(nil); canonicalizer := TclXmlCanonicalizer.Create(); Result := ADom.createElement(ANameSpace + ':SignedInfo');
node := ADom.createElement(ANameSpace + ':CanonicalizationMethod'); Result.appendChild(node); (node as IXMLDomElement).setAttribute('Algorithm', 'http://www.w3.org/2001/10/xml-exc-c14n#');
node := ADom.createElement(ANameSpace + ':SignatureMethod'); Result.appendChild(node); (node as IXMLDomElement).setAttribute('Algorithm', 'http://www.w3.org/2000/09/xmldsig#rsa-sha1');
for i := 0 to ASignReferences.Count - 1 do begin reference := ADom.createElement(ANameSpace + ':Reference'); Result.appendChild(reference); (reference as IXMLDomElement).setAttribute('URI', '#' + SignReferences[i]);
data := ADom.selectSingleNode('//*[@id="' + SignReferences[i] + '"]'); Assert(data <> nil);
node := ADom.createElement(ANameSpace + ':DigestMethod'); reference.appendChild(node); (node as IXMLDomElement).setAttribute('Algorithm', 'http://www.w3.org/2000/09/xmldsig#sha1');
node := ADom.createElement(ANameSpace + ':DigestValue'); reference.appendChild(node); encoder.EncodeString(GetDigestValue(canonicalizer.Canonicalize(data)), digestValue, cmMIMEBase64); node.text := digestValue; end; finally canonicalizer.Free(); encoder.Free(); end; end; |
There are different ways to obtain the digest hash value: you can use standard Microsoft shipped CryptoAPI library or use any third party library such as StreamSec tools. In this article we have used the MS CryptoAPI library for calculating SHA1 digest hash values:
function TclSoapRequest.GetDigestValue(const AXml: string): string; var context: HCRYPTPROV; hash: HCRYPTHASH; data: PByte; hashSize, dwordSize: DWORD; begin CryptAcquireContext(@context, nil, nil, PROV_RSA_SCHANNEL, 0); try CryptCreateHash(context, CALG_SHA1, 0, 0, @hash); CryptHashData(hash, Pointer(AXml), Length(AXml), 0); dwordSize := SizeOf(DWORD); CryptGetHashParam(hash, HP_HASHSIZE, @hashSize, @dwordSize, 0); GetMem(data, hashSize); try CryptGetHashParam(hash, HP_HASHVAL, data, @hashSize, 0); SetLength(Result, hashSize); system.Move(data^, Pointer(Result)^, hashSize); finally FreeMem(data); CryptDestroyHash(hash); end; finally CryptReleaseContext(context, 0); end; end; |
In this step we need to digitally sign the SignedInfo XML node we built in the previous chapter using a cryptographic algorithm. In our case we use the private key cryptography with X509 certificates. At first, we need to obtain the required certificate within the certificate store in terms of using the MS CryptoAPI library. When the certificate context is defined, it is time to digitally sign data using the CryptSignMessage CryptoAPI function in detached signature mode:
function TclSoapRequest.GetSignatureValue(certContext: PCCERT_CONTEXT; const AXml: string): string; var xmlData, signature: PByte; data: array[0..0] of PByte; msgCert: array[0..0] of PCCERT_CONTEXT; dwDataSizeArray: array[0..0] of DWORD; sigParams: CRYPT_SIGN_MESSAGE_PARA; cbSignedBlob: DWORD; begin GetMem(xmlData, Length(AXml)); try system.Move(Pointer(AXml)^, xmlData^, Length(AXml)); ZeroMemory(@sigParams, SizeOf(CRYPT_SIGN_MESSAGE_PARA)); sigParams.cbSize := SizeOf(CRYPT_SIGN_MESSAGE_PARA); sigParams.dwMsgEncodingType := (X509_ASN_ENCODING or PKCS_7_ASN_ENCODING); sigParams.pSigningCert := certContext; sigParams.HashAlgorithm.pszObjId := szOID_RSA_MD5; data[0] := xmlData; dwDataSizeArray[0] := Length(AXml); cbSignedBlob := 0;
CryptSignMessage(@sigParams, True, 1, @data[0], @dwDataSizeArray[0], nil, @cbSignedBlob);
GetMem(signature, cbSignedBlob); try CryptSignMessage(@sigParams, True, 1, @data[0], @dwDataSizeArray[0], signature, @cbSignedBlob); SetLength(Result, cbSignedBlob); system.Move(signature^, Pointer(Result)^, cbSignedBlob); finally FreeMem(signature); end; finally FreeMem(xmlData); end; end; |
This sample uses the TclEncoder component from the Clever Internet Suite library for encoding binary values into the Base64 encoding format. But it is possible to use any other encoding library on your convenience. The obtained digital signature value is also encoded using the TclEncoder component and substituted into the Signature XML node.
The new version 9.0 includes a special TclSoapMessage component that implements the message signing and encrypting algorithms. The described in this article algorithm was improved by adding the multiple digital signatures support, a set of canonicalization and cryptographic algorithms, and AES encryption algorithms.
You can use the TclSoapMessage component together with the TclHttpRio component for securely exchanging data with SOAP WSDL services. The TclHttpRio component is inherited from the standard THTTRIO and can be used with any imported WSDL service definitions. If you want to sign and / or encrypt the message, you need to set up both the Signatures and the EncryptedKey properties of the TclHttpRio.SoapRequest property.
The TclHttpRio.SoapRequest property is of TclSoapMessage type and represents the request data to be sent to the WSDL service. In addition, you need to enable the signing and encrypting function at the TclHttpRio component by using of both the Sign and Encrypt properties.
You will need to assing both the OnGetSigningCertificate and OnGetEncryptionCertificate event handlers to provide the TclSoapMessage with the required certificates.
createdOn := Now();
clHttpRio1.SoapRequest.Header.CharSet := 'utf-8'; clHttpRio1.SoapRequest.Timestamp.ID := 'Timestamp-' + GenerateUniqueID(); clHttpRio1.SoapRequest.Timestamp.Created := DateTimeToXMLTime(createdOn); clHttpRio1.SoapRequest.Timestamp.Expires := DateTimeToXMLTime(createdOn + EncodeTime(0, 30, 0, 0)); clHttpRio1.SoapRequest.BodyID := 'Body-' + GenerateUniqueID();
clHttpRio1.SoapRequest.SignReferences.Clear(); clHttpRio1.SoapRequest.SignReferences.Add(Id2UriReference(clSoapRequest.Timestamp.ID)); clHttpRio1.SoapRequest.SignReferences.Add(Id2UriReference(clSoapRequest.BodyID));
clHttpRio1.SoapRequest.EncryptReferences.Clear(); clHttpRio1.SoapRequest.EncryptReferences.Add(Id2UriReference(clSoapRequest.BodyID));
clHttpRio1.URL := 'https://...'; clHttpRio1.Port := 'YourWsdlPort'; clHttpRio1.Service := 'YourWsdlService'; clHttpRio1.Options := clHttpRio1.Options + [soDocument, soLiteralParams];
clHttpRio1.Sign := True; clHttpRio1.Encrypt := True; clHttpRio1.SignBeforeEncrypt := True;
servicePort := (clHttpRio1 as YourServicePort);//imported from WSDL
req := ServiceRequest.Create(); req.FirstOperand := 123; req.SecondOperand := 456;
resp := servicePort.SendRequest(req); //a service method that is imported from WSDL
result := resp.Result; |
Download Clever Internet Suite 9.0
There are the following working demos are available for downloading: SoapDSIG and SoapSecurity.
Download SoapDSIG source code
Download SoapSecurity source code
With Best Regards, Sergey Shirokov Clever Components team.
|