In my last posting, I promised to write about a more general purpose way of selecting specific XML to sign. Although the technique I presented in the last post will work, it requires a custom class derived from SignedXml, and will not work unless both the signer and the verifier have access to, and use, that class. In order to have signatures validate by users who don't have your implementation of SignedXml (or don't even use the .NET XML digital signature mechanism at all), XPath is a better choice when initially signing a document.
Using XPath to sign specific parts of the XML document involves adding an XPath transform to a reference in your signature. In .NET, the XPath transform is represented by the XmlDsigXPathTransform class. This transform will take an XPath expression, and act as a filter, selecting the nodes you want to sign while removing the others.
The XPath transform works a little differently than you might expect. Instead of specifying an XPath expression that will result in the nodes you wish to sign, you specify a filter. Internally, the transform first takes the document that the reference points to and runs the following XPath expression on it:
This selects all of the nodes in the reference, and gets them into a node list. Each element in this node list is then evaluated against the XPath expression specified in the transform. If the result is true, the node is included in the signature, false excludes it.
When working with the XmlDsigXPathTransform, I use a utility method that creates the transform for me. The reason is that the class does not actually contain a way to programmaticly set the XPath that will be used. Instead, the only way to accomplish this is to create the XML form of the transform and load it into the object. The utility method is shown here.
/// Create an XPath transform
/// <param name='xpath'>XPath expression for the transform</param>
/// <returns>XPath transform that filters according to the given XPath expression</returns>
private static XmlDsigXPathTransform CreateXPathTransform(string xpath)
// create the XML that represents the transform
XmlDocument doc = new XmlDocument();
XmlElement xpathElem = doc.CreateElement("XPath");
xpathElem.InnerText = xpath;
XmlDsigXPathTransform xform = new XmlDsigXPathTransform();
In order to use this transform, first create a reference to the entire document being signed (by passing in an empty string). Then call AddTransform on the reference, passing in the XPath transform. The following sample signs nodes named "signed", and ignores all others. Note the use of the ancestor-or-self:: axis in the query. If this weren't used, than any nodes that were children of the signed nodes wouldn't be signed. Most times this isn't the intention of the signer, and could lead to security holes.
XmlDocument doc = new XmlDocument();
SignedXml signer = new SignedXml(doc);
// create a reference to the whole document
// but sign only tags named signed
Reference r = new Reference("");
signer.SigningKey = new RSACryptoServiceProvider();
In order to sign a document similar to the one from yesterday's post (where I'm signing based on "_Id" attributes, I would use code similar to this:
Applying a signature containing that reference to this XML:
<important _Id='signme'>Signed Data</important>
<vital _Id='signme'>More Signed Data</vital>
<interesting>More Unsigned Data</interesting>
Will result in a signature similar to the following:
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<SignatureValue>meG/yZGO . . . vkjbOIU=</SignatureValue>
The code to verify this signature is exactly the same as the code to verify a standard signature. You'll notice that the output XML contains both the transform itself and the XPath used in the transform. This lets the validating code reconstruct the transform object without any special instructions from you.
One note on using this to sign multiple nodes. If you need to use more than one XPath expression, then you need to use more than one reference. Adding multiple transforms to the same reference will actually just chain them together, making the second one filter the results of the first one. Two XPath transforms on separate references will work as expected, with each transform having access to all of the nodes in the document.