Adding SignatureProperties to SignedXml

One of the optional portions of the W3C XML digital signature specification allows for a set of SignatureProperties to be assigned to a signature.  SignatureProperties allow the signer to place some metadata into the signature itself, such as the time the signature was created and the name of the person creating the signature.  Since the XML digital signature specification does not lay out specific properties, you are free to create as many domain specific properties as you'd like.  Although the SignedXml class does not support this feature, it's easy enough to add on your own by deriving from the default SignedXml implementation.

Signature properties exist as a SignatureProperties element in the signature itself, embedded as a DataObject.  The signature's references contain a pointer to this element with a Type of  "https://www.w3.org/2000/02/xmldsig#SignatureProperty", as specified in the W3C spec.  The SignatureProperties element will contain as many SignatureProperty elements as needed.  Each SignatureProperty will have a Target pointing to the signature that we're creating and will contain arbitrary XML (which should be in a different namespace to be valid).

In order to implement this, we'll first create a SignaturePropertiesSignedXml class which derives from SignedXml and takes the typical XmlDocument constructor parameter, as well as an ID for the signature (which is needed for the SignatureProperty Target attribute), and the ID of the SignatureProperties element itself (which is needed for the reference to work).  We then use this information to create the SignatureProperties element, and place it into a DataObject.  Finally we create and add the reference to the SignatureProperties element:

/// <summary>
/// XML signature class which enables SignatureProperties
/// </summary>
public sealed class SignaturePropertiesSignedXml : SignedXml
{
    private XmlDocument doc = null;
    private XmlElement signaturePropertiesRoot = null;
    
    /// <summary>
    /// Create a SignedXml class which can have SignatureProperties
    /// </summary>
    /// <param name="doc">XML document the signature belongs in</param>
    /// <param name="signatureId">ID of the signature to create</param>
    /// <param name="propertiesId">ID of the SignatureProperties to create</param>
    public SignaturePropertiesSignedXml(XmlDocument doc, string signatureId, string propertiesId) : base(doc)
    {
        if(String.IsNullOrEmpty(signatureId))
            throw new ArgumentException("signatureId cannot be empty", "signatureId");
        if(String.IsNullOrEmpty(propertiesId))
            throw new ArgumentException("propertiesId cannot be empty", "propertiesId");
        
        this.doc = doc;
        Signature.Id = signatureId;
        
        // create a root element to hold the properties
        signaturePropertiesRoot = doc.CreateElement("SignatureProperties", XmlDsigNamespaceUrl);
        signaturePropertiesRoot.SetAttribute("Id", propertiesId);
        
        // create a data object for the properties to go into
        DataObject signatureProperties = new DataObject();
        signatureProperties.Data = signaturePropertiesRoot.SelectNodes(".");
        AddObject(signatureProperties);

        // and add a reference to the data object
        Reference propertiesRef = new Reference("#" + propertiesId);
        propertiesRef.Type = "https://www.w3.org/2000/02/xmldsig#SignatureProperty";
        AddReference(propertiesRef);
        
        return;
    }

Now that the constructor has done the work of setting up the signature properties, we'll need a simple method to add individual properties.  This is easily accomplished by taking the XML content of the property, creating a SignatureProperty element with a Target of the containing signature, and adding the input XML to the SignatureProperty:

    /// <summary>
    /// Add a signature property to the document
    /// </summary>
    /// <param name="content">XML contents of the property</param>
    public void AddProperty(XmlElement content)
    {
        if(content == null)
            throw new ArgumentNullException("content");
        if(String.Compare(content.NamespaceURI, XmlDsigNamespaceUrl) == 0)
            throw new InvalidOperationException("Signature properties must not be in the XML Digital Signature namespace");

        // wrap the content in a SignatureProperty element
        XmlElement property = doc.CreateElement("SignatureProperty", XmlDsigNamespaceUrl);
        property.SetAttribute("Target", "#" + Signature.Id);
        property.AppendChild(content);

        signaturePropertiesRoot.AppendChild(property);
        return;
    }

Finally, we need to override GetIdElement (see my previous post on doing this for more information), since the default GetIdElement implementation does not search DataObjects and we have a reference that points to our SignatureProperties which is contained within a DataObject.  In the override, we just check to see if we're searching for the signature properties, and if not fall back to default behavior.

    /// <summary>
    /// Get an element refered to by its ID
    /// </summary>
    /// <param name="doc">XML document to search for the element in</param>
    /// <param name="id">element to search for</param>
    /// <returns>Element with the given ID, null if it could not be found</returns>
    public override XmlElement GetIdElement(XmlDocument doc, string id)
    {
        if(id == null)
            return null;
        
        // see if this is the signature properties being referenced, otherwise fall back to default behavior
        if(String.Compare(id, signaturePropertiesRoot.GetAttribute("Id"), StringComparison.OrdinalIgnoreCase) == 0)
            return signaturePropertiesRoot;
        else
            return base.GetIdElement(doc, id);
    }

And that's it!  We can now pretty easily use this class to add metadata to the signature.  For example:

        XmlDocument doc = new XmlDocument();
        doc.Load("order.xml");
        SignaturePropertiesSignedXml signer = new SignaturePropertiesSignedXml(doc, "orderSignature", "signatureProperties");

        RSA key = new RSACryptoServiceProvider();
        signer.SigningKey = key;

        // create a timestamp property
        XmlElement timestamp = doc.CreateElement("TimeStamp", "https://www.example.org/#signatureProperties");
        timestamp.InnerText = DateTime.Now.ToUniversalTime().ToString();
        signer.AddProperty(timestamp);

        // create a signed by property
        XmlElement signedBy = doc.CreateElement("SignedBy", "https://www.example.org/#signatureProperties");
        signedBy.InnerText = new WindowsPrincipal(WindowsIdentity.GetCurrent()).Identity.Name;
        signer.AddProperty(signedBy);

        Reference orderRef = new Reference("");
        orderRef.AddTransform(new XmlDsigEnvelopedSignatureTransform());
        signer.AddReference(orderRef);

        signer.ComputeSignature();
        doc.DocumentElement.AppendChild(signer.GetXml());
        doc.Save("order-signed.xml");

Will produce a signature that contains the time and person who signed the document:

  <Signature Id="orderSignature" xmlns="https://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="https://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod Algorithm="https://www.w3.org/2000/09/xmldsig#rsa-sha1" />
      <Reference URI="#signatureProperties" Type="https://www.w3.org/2000/02/xmldsig#SignatureProperty">
        <DigestMethod Algorithm="https://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>KfpRjNAGP47ZKX/RZ9hFEpKo9u8=</DigestValue>
      </Reference>
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="https://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="https://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>BPoz+CmKZyTATOhskqke3iOXmvA=</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>CmMm ... JKU=</SignatureValue>
    <Object>
      <SignatureProperties Id="signatureProperties">
        <SignatureProperty Target="#orderSignature">
          <TimeStamp xmlns="https://www.example.org/#signatureProperties">11/3/2005 9:35:37 PM</TimeStamp>
        </SignatureProperty>
        <SignatureProperty Target="#orderSignature">
          <SignedBy xmlns="https://www.example.org/#signatureProperties">REDMOND\ShawnFa</SignedBy>
        </SignatureProperty>
      </SignatureProperties>
    </Object>
  </Signature>

To verify this signature you don't even have to use the custom SignaturePropertiesSignedXml class -- since the SignatureProperties element is now a child of the XML document (since we made this an enveloped signature), the standard SignedXml class will be able to find it to verify the signature.