.NET XMLDSIG: What's My Hash?


The following article describes in detail the content hashed during generation of an XML Digital Signature (XMLDSIG) as created by .NET Framework XML 1.0/1.1 classes. It is intended for developers familiar with basic PKCS signatures who wish to understand the XMLDSIG hashing details. While not necessary to understand this level of detail to use XMLDSIG, it may provide useful hints for troubleshooting interoperability issues between different XMLDSIG implementations.

The example below is a very simple "wrapped" signature representing a signed XML fragment. To be very specific, the example used here is based on .NET Framework Security, B. LaMacchia et. al. 2002, Addison Wesley, Listing 32.23 p. 738.

The full C# code used to create the signature results on this page is:


using System; using System.Security.Cryptography; using System.Security.Cryptography.Xml; using System.Xml; public class WrappedSig { const string ContainerName = "Exponent1" ; const int AT_KEYEXCHANGE = 1; static void Main(String[] args) { XmlDocument doc = new XmlDocument(); XmlElement elem = doc.CreateElement("MyElement"); XmlNode child = (XmlNode) doc.CreateTextNode("My Element's Text Value") ; elem.AppendChild(child); CspParameters cp = new CspParameters(); cp.KeyContainerName = ContainerName; cp.KeyNumber = AT_KEYEXCHANGE; RSACryptoServiceProvider rsaCSP = new RSACryptoServiceProvider(cp); XmlElement xmlSignature = CreateWrappedSignature(rsaCSP, elem); Console.WriteLine(xmlSignature.OuterXml); } private static XmlElement CreateWrappedSignature (AsymmetricAlgorithm signingKey, XmlElement contentToSign) { SignedXml signedXml = new SignedXml(); signedXml.SigningKey = signingKey; DataObject dataObject = new DataObject("ID1", null, null, contentToSign); signedXml.AddObject(dataObject); Reference reference = new Reference(); reference.Uri = "#ID1"; signedXml.AddReference(reference); signedXml.ComputeSignature(); return signedXml.GetXml(); } }

In this exercise, we are interested in inspecting two different hash values: The hash value describing the XML fragment and represented in the tag <DigestValue> as b64-encoded, and also the hash value for the <SignedInfo> tag, which is encrypted with an RSA private key to generate the <SignatureValue> data. We wish to manually verify the exact byte-data (octet-stream) which generates these hashes. This byte data will be the C14 canonicalized XML data.

To facilitate actually seeing the hash value for the SignatureValue data, I use a special type of RSA keypair called an "ExponentOfOne" key, which does not encrypt at all, but simply shows the raw RSA signature block, allowing us to actually see the hash value.

Executing the code above, creates the following XML data (pretty printed below for ease of display; this only produces line breaks BETWEEN XML tags):

<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> <SignedInfo> <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" /> <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" /> <Reference URI="#ID1"> <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /> <DigestValue>yE16F3R6FqVErhSWEXlUJekp3s8=</DigestValue> </Reference> </SignedInfo> <SignatureValue>AAH///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AMCEwCQYFKw4DAhoFAAQU//6C4+sBwrj9SgjTVJTo/CdLe4o=</SignatureValue> <Object Id="ID1"> <MyElement xmlns="">My Element's Text Value</MyElement> </Object> </Signature> Notice the abbreviated terminating XML tags. Now, if we transform this XML (without line breaks) using the .NET XmlDsigC14NTransform class methods LoadInput, GetOutput, the resultant pretty-printed output is: <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> <SignedInfo> <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"> </CanonicalizationMethod> <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"> </SignatureMethod> <Reference URI="#ID1"> <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"> </DigestMethod> <DigestValue>yE16F3R6FqVErhSWEXlUJekp3s8=</DigestValue> </Reference> </SignedInfo> <SignatureValue>AAH///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AMCEwCQYFKw4DAhoFAAQU//6C4+sBwrj9SgjTVJTo/CdLe4o=</SignatureValue> <Object Id="ID1"> <MyElement xmlns="">My Element's Text Value</MyElement> </Object> </Signature> Now, the main difference is that all shortened terminating tags have been replaced with full terminating tags, required by the C14 specification. Now, if we calculate the SHA-1 hash for the manually transformed XML fragment Object tag above, which in fact has NOT changed at all after applying XmlDsigC14NTransform: <Object Id="ID1"><MyElement xmlns="">My Element's Text Value</MyElement></Object> we find a hash value that does NOT agree with the value:
  yE16F3R6FqVErhSWEXlUJekp3s8=     (b64)
  C8 4D 7A 17 74 7A 16 A5 44 AE 14 96 11 79 54 25 E9 29 DE CF   (HEX)
Transforming manually with XmlDsigC14NTransform does NOT actually include the namespace propogation. Evidently, oSignedXml.ComputeSignature internally inserts the namespace attribute into the Object tag to generate the actual octet-stream for hashing. The resultant fully C14 transformed data internally passed for hashing is: <Object xmlns="http://www.w3.org/2000/09/xmldsig#" Id="ID1"><MyElement xmlns="">My Element's Text Value</MyElement></Object> which exactly matches the tagged DigestValue hash above.

Consider now the hash value for the entire SignedInfo tag. As mentioned above, an ExponentOfOne keypair allows us to see the actual hash value of the entire data-to-be-signed. The SignedValue data above, after b64 decoding, is just a standard PKCS #1 Type 1 binary signature block of exactly 128 bytes (for a 1024 bit RSA keypair) in big-endian byte order. Displaying it in hex:

  00 01 FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
  FF FF FF FF FF FF FF FF FF FF FF FF 00 30 21 30
  09 06 05 2B 0E 03 02 1A 05 00 04 14 FF FE 82 E3
  EB 01 C2 B8 FD 4A 08 D3 54 94 E8 FC 27 4B 7B 8A
The last 20 bytes represents the SHA-1 hash:
  FF FE 82 E3 EB 01 C2 B8 FD 4A 08 D3 54 94 E8 FC 27 4B 7B 8A
which exactly matches the SHA-1 hash of the fully C14 transformed SignedInfo tag: <SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#"><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></SignatureMethod><Reference URI="#ID1"><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue>yE16F3R6FqVErhSWEXlUJekp3s8=</DigestValue></Reference></SignedInfo> Notice again the "xmlns=..." propogated namespace attribute is added to the SignedInfo tag before hashing.

It is easy to verify the results above directly, or the following web-based hashing Java applet (requires MS JVM and IE 5+) can be used to simply cut/paste the strings above and verify hashes. Note that the above example is an extremely simple example of XMLDSIG wrapped signature over a simple textual XML fragment. Hence, there are no unusual character-conversion issues.


QUESTION for the Student: Can you detect why the signature in LaMacchia et. al. Listing 32.24 p. 739 shows a different DigestValue entry than that shown above, for exactly the same XML fragment??

References


Michel I. Gallant
JavaScience Consulting
neutron@istar.ca