.NET SignedXml с определенным префиксом пространства имен (ds:) и без X509Data

Я изо всех сил пытался подписать мыльный конверт цифровой подписью в Microsoft .NET. Веб-сервис отклонял мой подписанный запрос .NET, говоря «недопустимая подпись». В этом случае веб-сервис был написан на Java третьей стороной, поэтому я не мог внести какие-либо изменения на стороне сервера.

На стороне сервера ожидался элемент подписи с префиксом ds. Класс SignedXml по умолчанию не создавал XML с префиксом ds для элемента подписи и дочерних элементов. Другая проблема была связана с ds:KeyInfo, который должен иметь элементы KeyValue и X509IssuerSerial — по умолчанию в .NET это элемент X509Data. Таким образом, структура сообщения должна выглядеть так, чтобы сервер мог принять запрос:

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

person Mindaugas    schedule 21.07.2017    source источник


Ответы (1)


Я хочу поделиться своим решением. Возможно, это будет полезно другим, борющимся с подобными проблемами.

Я создал это с помощью .NET 3.5 и использовал Microsoft Web Services Enhancements (WSE) 3.0.

Поэтому я переопределил XmlDocument по умолчанию, чтобы иметь методы SetPrefix и GetPrefix:

 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;
        }

    }

Затем переопределил класс SignedXml для поддержки атрибута id пространства имен http://schemas.xmlsoap.org/soap/security/2000-12 в элементе body

 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;
        }
}

А затем немного запутанный, но рабочий код для подписи xml-документа:

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;
        }
}

В моем случае ввод XmlDocument, переданный методу SignSoapBody, выглядит так:

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

Надеюсь, это будет кому-то полезно...

p.s. Если я попытаюсь подписать с помощью .NET 4.0, подпись станет недействительной, поэтому по этой причине я использую более старую версию .NET 3.5 (.NET 2.0 также работает нормально). Дело в том, что в .NET 4.0 версия System.Security.dll изменилась, и я думаю, что по этой причине она делает недопустимые значения подписи, что неприемлемо для серверной части, с которой мне нужно общаться.

person Mindaugas    schedule 21.07.2017