Search code examples
.netsoapdigital-signaturewebservice-clientsignedxml

.NET SignedXml with a Specific Namespace Prefix ("ds:") and without X509Data


I was struggling while trying to digitally sign soap envelope in Microsoft .NET. Webservice was rejecting my .NET signed request, saying “invalid signature”. In this case webservice was written in Java by third party, so i couldn't make any changes in server side.

Server side was expecting signature element with ds prefix. SignedXml class by default was not producing xml with ds prefix for signature element and child elements. Another problem was related to ds:KeyInfo which should have KeyValue and X509IssuerSerial elements - by default in .NET it is X509Data element. So the structure of message should look like this for the server to accept request:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header>
      <wsse:Security SOAP-ENV:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
         <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:SignedInfo>
               <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
               <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
               <ds:Reference URI="#ea43a55321b243c082dadae4f53f32b5">
                  <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                  <ds:DigestValue>.........</ds:DigestValue>
               </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>.......</ds:SignatureValue>
            <ds:KeyInfo>
               <ds:KeyValue>
                  <ds:RSAKeyValue>
                     <ds:Modulus>.......</ds:Modulus>
                     <ds:Exponent>....</ds:Exponent>
                  </ds:RSAKeyValue>
               </ds:KeyValue>
               <ds:X509IssuerSerial>
                  <ds:X509IssuerName>.......</ds:X509IssuerName>
                  <ds:X509SerialNumber>.......</ds:X509SerialNumber>
               </ds:X509IssuerSerial>
            </ds:KeyInfo>
         </ds:Signature>
      </wsse:Security>
   </SOAP-ENV:Header>
   <SOAP-ENV:Body ds:id="ea43a55321b243c082dadae4f53f32b5" xmlns:ds="http://schemas.xmlsoap.org/soap/security/2000-12">
            .................
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Solution

  • I want to share my solution. Maybe this will be helpful to others, struggling with similar problems.

    I created this with .NET 3.5 and was using Microsoft Web Services Enhancements (WSE) 3.0.

    So i overrided default XmlDocument to have SetPrefix and GetPrefix methods:

     public class XmlDsigDocument : XmlDocument
     {
            // Constants
            public const string XmlDsigNamespacePrefix = "ds";
    
            /// <summary>
            /// Override CreateElement function as it is extensively used by SignedXml
            /// </summary>
            /// <param name="prefix"></param>
            /// <param name="localName"></param>
            /// <param name="namespaceURI"></param>
            /// <returns></returns>
            public override XmlElement CreateElement(string prefix, string localName, string namespaceURI)
            {
                // CAntonio. If this is a Digital signature security element, add the prefix. 
                if (string.IsNullOrEmpty(prefix))
                {
                    // !!! Note: If you comment this line, you'll get a valid signed file! (but without ds prefix)
                    // !!! Note: If you uncomment this line, you'll get an invalid signed file! (with ds prefix within 'Signature' object)
                    //prefix = GetPrefix(namespaceURI);
    
                    // The only way to get a valid signed file is to prevent 'Prefix' on 'SignedInfo' and descendants.
                    List<string> SignedInfoAndDescendants = new List<string>();
                    SignedInfoAndDescendants.Add("SignedInfo");
                    SignedInfoAndDescendants.Add("CanonicalizationMethod");
                    SignedInfoAndDescendants.Add("InclusiveNamespaces");
                    SignedInfoAndDescendants.Add("SignatureMethod");
                    SignedInfoAndDescendants.Add("Reference");
                    SignedInfoAndDescendants.Add("Transforms");
                    SignedInfoAndDescendants.Add("Transform");
                    SignedInfoAndDescendants.Add("InclusiveNamespaces");
                    SignedInfoAndDescendants.Add("DigestMethod");
                    SignedInfoAndDescendants.Add("DigestValue");
                    if (!SignedInfoAndDescendants.Contains(localName))
                    {
                        prefix = GetPrefix(namespaceURI);
                    }
                }
    
                return base.CreateElement(prefix, localName, namespaceURI);
            }
    
            /// <summary>
            /// Select the standar prefix for the namespaceURI provided
            /// </summary>
            /// <param name="namespaceURI"></param>
            /// <returns></returns>
            public static string GetPrefix(string namespaceURI)
            {
                if (namespaceURI == "http://www.w3.org/2001/10/xml-exc-c14n#")
                    return "ec";
                else if (namespaceURI == SignedXml.XmlDsigNamespaceUrl)
                    return "ds";
    
                return string.Empty;
            }
            /// <summary>
            /// Set the prefix to this and all its descendants.
            /// </summary>
            /// <param name="prefix"></param>
            /// <param name="node"></param>
            /// <returns></returns>
            public static XmlNode SetPrefix(string prefix, XmlNode node)
            {
                foreach (XmlNode n in node.ChildNodes)
                {
                    SetPrefix(prefix, n);
                }
                if (node.NamespaceURI == "http://www.w3.org/2001/10/xml-exc-c14n#")
                    node.Prefix = "ec";
                else if ((node.NamespaceURI == SignedXmlWithId.xmlDSignSecurityUrl) || (string.IsNullOrEmpty(node.Prefix)))
                    node.Prefix = prefix;
    
                return node;
            }
    
        }
    

    Then overrided SignedXml class to support id attribute of namespace http://schemas.xmlsoap.org/soap/security/2000-12 in body element

     internal sealed class SignedXmlWithId : SignedXml
     {
            public SignedXmlWithId()
                : base()
            {
            }
    
            public SignedXmlWithId(XmlDocument doc)
                : base(doc)
            {
            }
    
            public SignedXmlWithId(XmlElement elem)
                : base(elem)
            {
            }
    
            public const string xmlSoapEnvelopeUrl = "http://schemas.xmlsoap.org/soap/envelope/";
    
            public const string xmlDSignSecurityUrl = "http://www.w3.org/2000/09/xmldsig#";
    
            public const string xmlBodyIDNamespaceUrl = "http://schemas.xmlsoap.org/soap/security/2000-12";
    
            public const string xmlOasisWSSSecurityExtUrl = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
    
            public override XmlElement GetIdElement(XmlDocument doc, string id)
            {
                // check to see if it's a standard ID reference
                XmlElement idElem = base.GetIdElement(doc, id);
    
                if (idElem == null)
                {
                    XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable);
                    nsManager.AddNamespace("ds", "http://schemas.xmlsoap.org/soap/security/2000-12");
    
                    idElem = doc.SelectSingleNode("//*[@ds:id=\"" + id + "\"]", nsManager) as XmlElement;
                }
    
                return idElem;
            }
    }
    

    And then a bit messy, but working code to sign xml document:

    public class SignatureHelper
    {
            public XmlDsigDocument SignSoapBody(XmlDsigDocument xmlDoc, X509Certificate2 cert)
            {
                XmlNamespaceManager ns = new XmlNamespaceManager(xmlDoc.NameTable);
                ns.AddNamespace("SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/");
    
                XmlElement body = xmlDoc.DocumentElement.SelectSingleNode(@"//SOAP-ENV:Body", ns) as XmlElement;
                if (body == null)
                    throw new Exception("No body tag found");
    
                string bodyId = Guid.NewGuid().ToString().Replace("-", "");
    
                body.SetAttribute("id", "http://schemas.xmlsoap.org/soap/security/2000-12", bodyId);
    
                SignedXmlWithId signedXml = new SignedXmlWithId(xmlDoc);
                signedXml.SigningKey = cert.PrivateKey;
    
                string mySerialNumber = "";
                string[] subjectArray = cert.Subject.Split(',');
    
                for (int i = 0; i < subjectArray.Length; i++)
                {
                    if (subjectArray[i].StartsWith("SERIALNUMBER="))
                    {
                        mySerialNumber = subjectArray[i].Replace("SERIALNUMBER=", "");
                        break;
                    }
                }
    
                RSAKeyValue rsa = new RSAKeyValue((System.Security.Cryptography.RSA)cert.PublicKey.Key);
                XmlElement rsaElem = rsa.GetXml();
    
                KeyInfo keyInfo = new KeyInfo();
                XmlDsigDocument doc = new XmlDsigDocument();
                doc.LoadXml("<x>" + rsaElem.OuterXml + "<X509IssuerSerial><X509IssuerName>" + cert.Issuer + "</X509IssuerName><X509SerialNumber>" + mySerialNumber + "</X509SerialNumber></X509IssuerSerial></x>");
    
                keyInfo = Microsoft.Web.Services3.Security.KeyInfoHelper.LoadXmlKeyInfo(doc.DocumentElement); //Microsoft WSE 3.0
                signedXml.KeyInfo = keyInfo;
    
                signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigC14NWithCommentsTransformUrl;
    
                Reference reference = new Reference();
                reference.Uri = "#" + bodyId;  
    
                signedXml.AddReference(reference);
                signedXml.ComputeSignature();
    
                XmlElement signedElement = signedXml.GetXml();
                signedElement.Prefix = "ds";
    
                for (int i = 0; i < signedElement.ChildNodes.Count; i++)
                {
                    signedElement.ChildNodes[i].Prefix = "ds";
    
                    for (int k = 0; k < signedElement.ChildNodes[i].ChildNodes.Count; k++)
                    {
                        signedElement.ChildNodes[i].ChildNodes[k].Prefix = "ds";
    
                        for (int m = 0; m < signedElement.ChildNodes[i].ChildNodes[k].ChildNodes.Count; m++)
                        {
                            signedElement.ChildNodes[i].ChildNodes[k].ChildNodes[m].Prefix = "ds";
    
                            for (int n = 0; n < signedElement.ChildNodes[i].ChildNodes[k].ChildNodes[m].ChildNodes.Count; n++)
                            {
                                signedElement.ChildNodes[i].ChildNodes[k].ChildNodes[m].ChildNodes[n].Prefix = "ds";
                            }
                        }
                    }
                }
    
                XmlElement soapSignature = xmlDoc.CreateElement("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
                soapSignature.Prefix = "wsse";
                soapSignature.SetAttribute("mustUnderstand", "http://schemas.xmlsoap.org/soap/envelope/", "1");
    
                signedElement.ChildNodes[1].ChildNodes[0].Value = Chunks(signedElement.ChildNodes[1].ChildNodes[0].Value);
                signedElement.ChildNodes[2].ChildNodes[0].ChildNodes[0].ChildNodes[0].ChildNodes[0].Value = Chunks(signedElement.ChildNodes[2].ChildNodes[0].ChildNodes[0].ChildNodes[0].ChildNodes[0].Value);
    
                soapSignature.AppendChild(signedElement);
    
                XmlElement soapHeader = xmlDoc.DocumentElement.SelectSingleNode("//SOAP-ENV:Header", ns) as XmlElement;
                if (soapHeader == null)
                {
                    soapHeader = xmlDoc.CreateElement("Header", "http://schemas.xmlsoap.org/soap/envelope/");
                    soapHeader.Prefix = "SOAP-ENV";
                    xmlDoc.DocumentElement.InsertBefore(soapHeader, xmlDoc.DocumentElement.ChildNodes[0]);
                }
                soapHeader.AppendChild(soapSignature);
    
    
                string xmlContent = xmlDoc.OuterXml;
    
                xmlContent = xmlContent.Replace("X509IssuerSerial", "ds:X509IssuerSerial");
                xmlContent = xmlContent.Replace("X509IssuerName", "ds:X509IssuerName");
                xmlContent = xmlContent.Replace("X509SerialNumber", "ds:X509SerialNumber");
    
                XmlDsigDocument xmlDocResult = new XmlDsigDocument();
                xmlDocResult.LoadXml(xmlContent);
    
                xmlDocResult = GenerateSignatureValue(xmlDocResult, cert);
    
                return xmlDocResult;
            }
    
            private string Chunks(string str)
            {
                byte[] bytes = Convert.FromBase64String(str);
                return Convert.ToBase64String(bytes, Base64FormattingOptions.InsertLineBreaks);
            }
    
            private XmlDsigDocument GenerateSignatureValue(XmlDsigDocument xmlDoc, X509Certificate2 cert)
            {
                RSACryptoServiceProvider privateKey = (RSACryptoServiceProvider)cert.PrivateKey;
    
                XmlNamespaceManager ns = new XmlNamespaceManager(xmlDoc.NameTable);
                ns.AddNamespace("SOAP-ENV", "http://schemas.xmlsoap.org/soap/envelope/");
    
                SignedXmlWithId signedXml = new SignedXmlWithId(xmlDoc);
    
                signedXml.SignedInfo.CanonicalizationMethod = SignedXmlWithId.XmlDsigC14NWithCommentsTransformUrl;
    
                XmlNodeList nodeList = xmlDoc.GetElementsByTagName("ds:Signature");
                signedXml.LoadXml((XmlElement)nodeList[0]);
    
                signedXml.SigningKey = privateKey;
                signedXml.ComputeSignature();
    
                XmlElement signedElement = signedXml.GetXml();
    
                bool ok = signedXml.CheckSignature();
    
                if(!ok)
                {
                    throw new Exception("Invalid signature");
                }
    
                xmlDoc.DocumentElement.ChildNodes[0].ChildNodes[0].RemoveChild(xmlDoc.DocumentElement.ChildNodes[0].ChildNodes[0].ChildNodes[0]);
    
                XmlElement soapHeader = xmlDoc.DocumentElement.SelectSingleNode("//SOAP-ENV:Header", ns) as XmlElement;
                if (soapHeader != null)
                    soapHeader.ChildNodes[0].AppendChild(signedElement);
    
                return xmlDoc;
            }
    }
    

    In my case input XmlDocument passed to method SignSoapBody looks like this:

    <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
       <SOAP-ENV:Header>
       </SOAP-ENV:Header>
       <SOAP-ENV:Body>
          ..............
       </SOAP-ENV:Body>
    </SOAP-ENV:Envelope>
    

    Hope this will be helpful to someone...

    p.s. If i try to sign with .NET 4.0 signature becomes invalid, so for that reason i use older .NET 3.5 (.NET 2.0 is working fine as well). The thing is that in .NET 4.0 System.Security.dll version has changed and i think for that reason it makes invalid signature values which is not acceptable for server side i need to communicate with.