XmlIdSignedXml.cs

using System;
using System.Security.Cryptography.Xml;
using System.Xml;

/// <summary>
/// Provides xml:id support for XML digital signatures
/// </summary>
/// <remarks>
/// This class allows the .NET XML Digital Signature system
/// uniquely identify nodes based upon xml:id's. The xml:id
/// working draft can be found on the W3C's website:
/// https://www.w3.org/TR/2004/WD-xml-id-20040407/
/// </remarks>
public sealed class XmlIdSignedXml : SignedXml
{
    /// <summary>
    /// Namespace URI to map to the xml prefix
    /// </summary>
    public static readonly string XmlIdUrl = "https://www.w3.org/XML/1998/namespace";

    private bool m_strict;        // operate in strict mode?

    /// <summary>
    /// Create a signed XML class that can sign and verify signatures
    /// using xml:id's
    /// </summary>
    /// <see cref='System.Security.Cryptography.Xml.SignedXml'/>
    public XmlIdSignedXml() : base()
    {
        Strict = false;
        return;
    }
    
    /// <summary>
    /// Create a signed XML class that can sign and verify signatures
    /// using xml:id's, using a specific document context
    /// </summary>
    /// <see cref='System.Security.Cryptography.Xml.SignedXml'/>
    public XmlIdSignedXml(XmlDocument document) : base(document)
    {
        Strict = false;
        return;
    }

    /// <summary>
    /// Create a signed XML class that can sign and verify signatures
    /// using xml:id's, initialized with an XML element
    /// </summary>
    /// <see cref='System.Security.Cryptography.Xml.SignedXml'/>
    public XmlIdSignedXml(XmlElement element) : base(element)
    {
        Strict = false;
        return;
    }

    /// <summary>
    /// Flag to indicate if xml:id's should be matched exclusively
    /// or if fallback on default behavior
    /// </summary>
    public bool Strict
    {
        get { return m_strict; }
        set { m_strict = value; }
    }
    
    /// <summary>
    /// Return the XmlElement with the given id from the given document
    /// </summary>
    /// <remarks>
    /// First attempts to match to an xml:id, and if that fails will only
    /// fall back on the default behavior if the Strict flag is unset.
    /// </remarks>
    /// <param name="document">document to search for matching nodes in</param>
    /// <param name="idValue">id of the node to find</param>
    /// <exception cref="System.ArgumentNullException"><paramref name="idValue"/> is null</exception>
    /// <exception cref="System.ArgumentException"><paramref name="idValue"/> contains both single and double quotes</exception>
    /// <exception cref="System.InvalidOperationException"><paramref name="idValue"/> matches multiple nodes</exception>
    /// <returns>
    /// null if no match is found
    /// node with the given xml:id if one is found
    /// node with the given id if no xml:id is found and Strict is false
    /// </returns>
    /// <see cref='Strict'/>
    public override XmlElement GetIdElement(XmlDocument document, string idValue)
    {
        if(idValue == null)
            throw new ArgumentNullException("idValue", "Need an ID value to search for");
        if(document == null)
            return null;        // following the pattern defined in the default
                                // a null document provides no search results, but
                                // also does not throw an exception.
                                
        // setup the namespace mapping for the xml:id namespace
        XmlNamespaceManager nsManager = new XmlNamespaceManager(document.NameTable);
        nsManager.AddNamespace("xml", XmlIdUrl);

        // quote the id to search for
        string searchString = null;
        if(idValue.IndexOf('\'') == -1)
            searchString = "'" + idValue + "'";
        else if(idValue.IndexOf('\"') == -1)
            searchString = "\"" + idValue + "\"";
        else
            throw new ArgumentException("idValue", "Cannot search for an xml:id containing both single and double quotes.");
        
        // get the nodes that have xml:ids which mach the given id
        XmlNodeList xmlIdNodes = document.SelectNodes("//*[@xml:id=" + searchString + "]", nsManager);

        // xml:id's must be unique in the document
        if(xmlIdNodes.Count > 1)
            throw new InvalidOperationException("Search for a non-unique xml:id");

        // we found an xml:id that matches, so return that one
        else if(xmlIdNodes.Count == 1)
            return xmlIdNodes[0] as XmlElement;
        
        // there are no matching xml:id's, if strict matching was requested fail, otherwise
        // default to the SignedXml search
        if(Strict)
            return null;
        else
            return base.GetIdElement(document, idValue);
    }
}