Site Map Contact Us Home
E-mail Newsletter
Subscribe to get informed about
Clever Components news.

Your Name:
Your Email:
 
SUBSCRIBE
 
Previous Newsletters
 




Products Articles Downloads Order Support
Customer Portal      

SOAP security: digital signature

Updated on October 26, 2017

Abstract

There are lots of articles and other documents written about securing the SOAP messages: XML SignatureWeb Services SecuritySOAP-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.

 

Some theory

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.

 

Canonicalization

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.

 

Calculating the digest value of data

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;

Signing the SOAP message using the cryptography algorithm

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.

 

Clever Internet Suite 9.0 notes

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;

 

Downloads

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.

    Copyright © 2000-2024