Using XPath to Sign Specific XML


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:


(//. | //@* | //namespace::*)

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.



/// <summary>
/// Create an XPath transform
/// </summary>
/// <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();
    xform.LoadInnerXml(xpathElem.SelectNodes(“.”));
            
    return xform;
}


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.



// load the XML that will be signed
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);
    
SignedXml signer = new SignedXml(doc);

// create a reference to the whole document
// but sign only tags named signed
Reference r = new Reference(“”);
r.AddTransform(CreateXPathTransform(“ancestor-or-self::signed”));
signer.AddReference(r);

signer.SigningKey = new RSACryptoServiceProvider();
signer.ComputeSignature();


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:



Reference r = new Reference(“”);
r.AddTransform(CreateXPathTransform(“ancestor-or-self::*[@_Id=signme'”));
signer.AddReference(r);


Applying a signature containing that reference to this XML:

<xml>

  <unimportant>Unsigned Data</unimportant>

  <important _Id=’signme’>Signed Data</important>

  <vital _Id=’signme’>More Signed Data</vital>

  <interesting>More Unsigned Data</interesting>

</xml>

Will result in a signature similar to the following:


<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=””>
      <Transforms>
        <Transform Algorithm=”http://www.w3.org/TR/1999/REC-xpath-19991116″>
          <XPath>ancestor-or-self::*[@_Id=’signme’]</XPath>
        </Transform>
      </Transforms>
      <DigestMethod Algorithm=”http://www.w3.org/2000/09/xmldsig#sha1″ />
      <DigestValue>zGdeF6BumSVUfORJxKHfXvNQyG8=</DigestValue>
    </Reference>
  </SignedInfo>
  <SignatureValue>meG/yZGO . . . vkjbOIU=</SignatureValue>
</Signature>


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.

Comments (13)

  1. Elena Neroslavskaya says:

    Hi Shawn,

    I’m trying to use this example with the same WSE class.

    But it always throws the following exception:

    An unhandled exception of type ‘System.ArgumentException’ occurred in microsoft.web.services.dll

    Additional information: nodeList

    Do you know what could cause it ?

    xpath expression I’m trying to sign is "//username".

    Thank you very much,

  2. Shawn says:

    To be clear, I’m using the SignedXml class that’s built into the System.Security namespace, not the one from WSE in these examples. In general, unless you need an extension that is only provided by WSE, it’s recommended that you use the System.Security SignedXml class.

    -Shawn

  3. Elena Neroslavskaya says:

    Hi,

    I thought WSE is using System.Security classes inside ?

    And what is the right way then to implement WS-Security compliant tokens.

    WSE Security filter have to be compiant with XmlDsig as well ?

    Elena

  4. Shawn says:

    So I don’t work with the WSE team at all, I’m on core CLR security. I’m not sure what they use internally (though I’m sure they do use some of our stuff). Since this specific example relies on deriving directly from the System.Security.Cryptography.Xml.SignedXml class, it also depends pretty directly on how that specific instance of SignedXml works. I’d assume there are enough implementation differences between the two that you’d run into problems.

    -Shawn

  5. Hi,

    After some struggling with XmlDsigXPathTransform in WSE implementation I found that in order to make WSE version work, xpath exresssions must be formed to return boolean value instead of nodeset.

    The example above will work with WSE with following XPATH:

    local-name()=’signed’

    Thanks a lot,

    Hope it could help to someone else-;)

  6. Shawn says:

    Ahh … right, that’s how the XPath transform works. From the above posting:

    "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:

    (//. | //@* | //namespace::*)

    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."

    You don’t specify a transform that creates a nodeset, instead you specify a filter that selects which nodes should be included in the signature.

    -Shawn

  7. Hi Shawn,

    Is it the right compliant to XML Signature specification behavior to select first all nodes (//. | //@* | //namespace::*) ?????

    Elena

  8. Sorry for asking, you are right it is compliant

    http://www.w3.org/TR/xmldsig-core/#sec-XPath

    Thanks a lot

  9. Ishmaeel says:

    It appears that security framework does not provide an implementation for the XPath Filter 2.0 (rec: http://www.w3.org/TR/xmldsig-filter2/) unless I am missing something. Are you aware of any plans for implementing this recommendation? It seems to be much simpler and more powerful. Thanks for the great article.

  10. shawnfa says:

    Thanks for the feedback Ishmaeel. That does sound like a reasonable feature request … if you’d like to file it on the MSDN Product Feedback Center or in the "What do you want to see in future versions" thread (http://blogs.msdn.com/shawnfa/archive/2005/12/15/503836.aspx), we’ll take a look at it.

    Thanks!

    -Shawn

  11. Ishmaeel says:

    Thank you for the prompt reply, Shawn. I filed the suggestion in the Feedback Center (http://lab.msdn.microsoft.com/productfeedback/viewfeedback.aspx?feedbackid=dcef9976-39fd-447e-94df-53706c37ad7a). I suppose this would not find its way into the current version with an upgrade, so I think I will attempt to implement the method with a custom transform class that inherits from ..Cryptography.Xml.Transform class. Do you think that’s feasible? Thanks again.

  12. shawnfa says:

    Thanks for filing that bug — you’re right it won’t make the bar for a v2.0 service pack (since we don’t like to add features in an SP), but it’s a possibility for our next release.

    In the mean time, I think creating a custom Transform is exactly the way you’d want to go. If you’re only creating signatures this will be easy — if you need to verify them, then you’ll have to register your transform with CryptoConfig as well so that the signature engine can find your transform.

    -Shawn

  13. Daniel Clarke says:

    This is a really great article.  Many thanks.