Thursday, December 20, 2012

Interoperable XML Digital Signatures (C#-Java), part 2

The second part of the XMLDSig tutorial. Make sure you don’t miss the previous entries:

In this part of the tutorial we will learn how to create and validate XML digital signatures in C#.

What we demonstrate is three different approaches to:

public interface IDemo
{
    string Name { get; }
 
    string SignXml( XmlDocument Document );
 
    bool VerifyXml( string SignedXmlDocumentString );
    bool VerifyXmlFromStream( Stream SignedXmlDocumentStream );
}

These three approaches will correspond to Enveloped, Enveloping and Detached signatures (refer to the previous part of the tutorial).

We have to start with a helper class to access X509 certificates:

public class CertManager
{
    // note that both *.pfx location and the password are hardcoded!
    // please customize it in a production code
    private X509Certificate2 _certificate;
    public X509Certificate2 Certificate
    {
        get
        {
            if ( _certificate == null )
            {
                using ( FileStream fs =
                   File.Open( "demo.pfx", FileMode.Open ) )
                using ( BinaryReader br = new BinaryReader( fs ) )
                {
                    _certificate = 
                        new X509Certificate2( 
                           br.ReadBytes( (int)br.BaseStream.Length ), "demo" );
                }
            }
 
            return _certificate;
        }
    }
}

Then we will have a base class with a common code to verify XML documents. This is interesting – since we have three types of signatures we surely need three different approaches to signing. However, the same method will be used to validate three different types of signatures, regardless of the type of the signature! The code is rather straightforward and requires no additional comments.

public abstract class BaseXmlDsig : IDemo
{
    public abstract string Name { get; }
 
    public abstract string SignXml( System.Xml.XmlDocument Document );
 
    public bool VerifyXml( string SignedXmlDocumentString )
    {
        byte[] stringData = Encoding.UTF8.GetBytes( SignedXmlDocumentString );
        using ( MemoryStream ms = new MemoryStream( stringData ) )
            return VerifyXmlFromStream( ms );
    }
 
    public bool VerifyXmlFromStream( System.IO.Stream SignedXmlDocumentStream )
    {
        // load the document to be verified
        XmlDocument xd = new XmlDocument();
        xd.PreserveWhitespace = true;
        xd.Load( SignedXmlDocumentStream );
 
        SignedXml signedXml = new SignedXml( xd );
 
        // load the first <signature> node and load the signature  
        XmlNode MessageSignatureNode = 
           xd.GetElementsByTagName( "Signature" )[0];
 
        signedXml.LoadXml( (XmlElement)MessageSignatureNode );
 
        // get the cert from the signature
        X509Certificate2 certificate = null;
        foreach ( KeyInfoClause clause in signedXml.KeyInfo )
        {
            if ( clause is KeyInfoX509Data )
            {
                if ( ( (KeyInfoX509Data)clause ).Certificates.Count > 0 )
                {
                    certificate = 
                    (X509Certificate2)( (KeyInfoX509Data)clause ).Certificates[0];
                }
            }
        }
 
        // check the signature and return the result.
        return signedXml.CheckSignature( certificate, true );
    }
 
    #endregion
}

Next, we need three different providers for three different types of signatures.

Enveloped. Straightforward.

public class XmlDsigEnveloped : BaseXmlDsig, IDemo
{
    private CertManager manager { get; set; }
    private bool c14 { get; set; }
 
    public XmlDsigEnveloped( bool c14, CertManager manager )
    {
        this.manager = manager;
        this.c14 = c14;
    }
 
    #region IDemo Members
 
    public override string Name
    {
        get
        {
            return string.Format( "XmlDsigEnveloped {0} c14", c14 ? "with" : "without" );
        }
    }
 
    public override string SignXml( XmlDocument Document )
    {
        SignedXml signedXml = new SignedXml( Document );
        signedXml.SigningKey = manager.Certificate.PrivateKey;
 
        // Create a reference to be signed.
        Reference reference = new Reference();
        reference.Uri = "";
 
        // Add an enveloped transformation to the reference.            
        XmlDsigEnvelopedSignatureTransform env = 
           new XmlDsigEnvelopedSignatureTransform( true );
        reference.AddTransform( env );
 
        if ( c14 )
        {
            XmlDsigC14NTransform c14t = new XmlDsigC14NTransform();
            reference.AddTransform( c14t );
        }
 
        KeyInfo keyInfo = new KeyInfo();
        KeyInfoX509Data keyInfoData = new KeyInfoX509Data( manager.Certificate );
        keyInfo.AddClause( keyInfoData );
        signedXml.KeyInfo = keyInfo;
 
        // Add the reference to the SignedXml object.
        signedXml.AddReference( reference );
 
        // Compute the signature.
        signedXml.ComputeSignature();
 
        // Get the XML representation of the signature and save 
        // it to an XmlElement object.
        XmlElement xmlDigitalSignature = signedXml.GetXml();
 
        Document.DocumentElement.AppendChild( 
            Document.ImportNode( xmlDigitalSignature, true ) );
 
        return Document.OuterXml;
    }
 
    #endregion
}

Enveloping. This one requires a comment. You see, the XML document embedded in the signature is wrapped in an auxiliary DataObject element. The API requires that the DataObject points to a XmlNodeList and this is unfortunate since we only need a single node, the document we are about to sign. As it turns out, there is no public implementation of a class inheriting from the abstract XmlNodeList class so that I had to provide the CustomXmlNodeList class just to satisfy object contracts. This is a small limitation of the API and a smal inconvenience.

public class XmlDsigEnveloping : BaseXmlDsig, IDemo
{
    private CertManager manager { get; set; }
    private bool c14 { get; set; }
 
    public XmlDsigEnveloping( bool c14, CertManager manager )
    {
        this.manager = manager;
        this.c14 = c14;
    }
 
    #region IDemo Members
 
    public override string Name
    {
        get
        {
            return string.Format( "XmlDsigEnveloping {0} c14", c14 ? "with" : "without" );
        }
    }
 
    public override string SignXml( XmlDocument Document )
    {
        SignedXml signedXml = new SignedXml( Document );
        signedXml.SigningKey = manager.Certificate.PrivateKey;
 
        KeyInfo keyInfo = new KeyInfo();
        KeyInfoX509Data keyInfoData = new KeyInfoX509Data( manager.Certificate );
        keyInfo.AddClause( keyInfoData );
        signedXml.KeyInfo = keyInfo;
 
        // the DataObject has to point to a XmlNodeList
        DataObject dataObject = new DataObject();
        dataObject.Id   = "MyObjectID1";
        dataObject.Data = 
           new CustomXmlNodeList( new[] { Document.DocumentElement } ); 
        signedXml.AddObject( dataObject );
 
        // Add the reference to the SignedXml object.
        Reference reference = new Reference();
        reference.Uri = "#MyObjectID1";
        signedXml.AddReference( reference );
 
        // Create a reference to be signed.
        if ( c14 )
        {
            XmlDsigC14NTransform env = new XmlDsigC14NTransform();
            reference.AddTransform( env );
        }
 
        // Compute the signature.
        signedXml.ComputeSignature();
 
        // Get the XML representation of the signature and save 
        // it to an XmlElement object.
        XmlElement xmlDigitalSignature = signedXml.GetXml();
 
        // create detached envelope 
        XmlDocument envelope = new XmlDocument();
        envelope.AppendChild( envelope.CreateElement( "Envelope" ) );
 
        envelope.DocumentElement.AppendChild( 
           envelope.ImportNode( xmlDigitalSignature, true ) );
 
        return envelope.OuterXml;
    }
 
    #endregion
}
 
public class CustomXmlNodeList : XmlNodeList
{
    XmlNode[] _elements;
 
    public CustomXmlNodeList( XmlNode[] elements )
    {
        if ( elements == null )
            throw new ArgumentException();
 
        this._elements = elements;
    }
 
    public override int Count
    {
        get { return _elements.Count(); }
    }
 
    public override System.Collections.IEnumerator GetEnumerator()
    {
        return _elements.GetEnumerator();
    }
 
    public override XmlNode Item( int index )
    {
        return _elements[index];
    }
}

Detached. Nothing unusual this time.

public class XmlDsigDetached : BaseXmlDsig, IDemo
 {
     private CertManager manager { get; set; }
     private bool c14 { get; set; }
 
     public XmlDsigDetached( bool c14, CertManager manager )
     {
         this.manager = manager;
         this.c14 = c14;
     }
 
     #region IDemo Members
 
     public override string Name
     {
         get
         {
             return string.Format( "XmlDsigDetached {0} c14", c14 ? "with" : "without" );
         }
     }
 
     public override string SignXml( XmlDocument Document )
     {
         // create detached envelope 
         XmlDocument envelope = new XmlDocument();
         envelope.PreserveWhitespace = true;
         envelope.AppendChild( envelope.CreateElement( "Envelope" ) );
 
         XmlElement message = envelope.CreateElement( "Message" );
         message.InnerXml = Document.DocumentElement.OuterXml;
         message.SetAttribute( "Id", "MyObjectID" );
         envelope.DocumentElement.AppendChild( message );
 
         SignedXml signedXml = new SignedXml( envelope );
         signedXml.SigningKey = manager.Certificate.PrivateKey;
 
         // Create a reference to be signed.
         Reference reference = new Reference();
         reference.Uri = "#MyObjectID";
 
         if ( c14 )
         {
             XmlDsigC14NTransform env = new XmlDsigC14NTransform();
             reference.AddTransform( env );
         }
 
         KeyInfo keyInfo = new KeyInfo();
         KeyInfoX509Data keyInfoData = new KeyInfoX509Data( manager.Certificate );
         keyInfo.AddClause( keyInfoData );
         signedXml.KeyInfo = keyInfo;
 
         // Add the reference to the SignedXml object.
         signedXml.AddReference( reference );
 
         // Compute the signature.
         signedXml.ComputeSignature();
 
         // Get the XML representation of the signature and save 
         // it to an XmlElement object.
         XmlElement xmlDigitalSignature = signedXml.GetXml();
 
         envelope.DocumentElement.AppendChild(
            envelope.ImportNode( xmlDigitalSignature, true ) );
 
         return envelope.OuterXml;
     }
 
     #endregion
 }

All these three implementations allow me to sign my XML documents with a signature of my choice. Also, all three should correctly verify signed documents.

In the next part we will discuss Java implementation.

9 comments:

muneeb said...

Hi,
Let me know if there is any difficulty for validating the signature that has namespace prefix like "ds". And also how do i read the xml from Getresponsestream() in order not to have an extra spaces and line breaks in the xml. Appreciate your help.

Wiktor Zychla said...

@mubeeb: the extra spaces and breaks should not be a problem as the document is normalized before it is signed/validated.

MOHAN MAHTHA said...

Thanks a lot for this contribution! It's been very useful for me. Everything is very open and represents very clear explanation of issues. Really blogging is spreading its wings quickly. Your write up is a good example of it. Your website is very useful. Thanks for sharing.
Digital Signature

Luis Falconi said...

Hi, i try to
enveloping Xml Digital Signature (XMLDSIG) with ds prefix with c# but i can't some clue i can do please?
I try to use codeplex xadesnet but can't assign ds prefix

Bhushan Patle said...

Hi,thanks a lot for the excellent blog.I am facing issue while verifying multiple signatures.
Verification fails with reason "Root element is missing."
Would appreciate any help..!!!

WanderingWarrior said...

Is there a download link for the C# code somewhere? I am having trouble getting it to work.

Servicio Santa Ana said...

the add prefix DS into sign ? how to ?
change the signature with add prefix ds?

thanks.

Aprendiz said...

If you change the signature adding the DS you will break the signature. To insert the DS "hand" you need to regenerate the SignatureValue.

Diego Mascheroni said...

Good post!, you look like Machiavelli, hehe... ;)