Title: SOAP security: digital signature
Question: This article introduces the working sample code in Borland Delphi which implements digitally singing of SOAP messages
Answer:
Abstract
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 currency exchange rates (provided by XMethods.net) 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 := '';
for i := 0 to ARootNode.childNodes.length - 1 do
begin
Result := Result + BuildXmlString(ARootNode.childNodes.item[i]);
end;
Result := Result + '';
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;
The full sample source code and also the NormalizeAttributeValue function can be downloaded at soapsecurity.zip.
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;
This sample uses the TclEncoder component of 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.
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;
The obtained digital signature value is also encoded using the TclEncoder component and substituted into the Signature XML node.
Source Code and working sample
The sample code introduced in the article listings is simplified and does not provide the error handling code when signing the XML data. Also the sample does not implement any XML transformations which can be applied to the XML document when canonicalizing it before digitally signing and encoding.
In the next article about SOAP security extensions we will expand the sample code with XML transformations feature and also with including digital certificates into the SOAP XML data.
A full source code of all classes described in this article can be downloaded at soapsecurity.zip
This code is constantly being refined and improved and your comments and suggestions are always welcome.
Please write us at info@clevercomponents.com
With best regards,
Sergey Shirokov
Clever Components team.
www.clevercomponents.com