KomodoSoft

mobile apps and more

C# - Sign SOAP message using an X.509 certificate (without using WCF)

Hi, A few days ago I had to develop an application that consume a web service developed in Java with

Hi,

A few days ago I had to develop an application that consume a web service developed in Java with message signature required. At first I tried to develop it using WCF with MultualCertificate authentication method, the request was signed fine, but when I received the response, I got an error with canonicalization method used in response message. By default WCF uses http://www.w3.org/2001/10/xml-exc-c14n#, and the server was using http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments in responses.

Since I have not been able to find a solution using WCF, I decided to do "manually".

Here is the class that compose the SOAP message:

    public class SoapMessage
    {       
        public XmlElement Header { get; set; }

        public XmlElement Body { get; set; }

        public X509Certificate2 Certificate { get; set; }

        public XmlDocument GetXml(bool signed = false)
        {
            XmlDocument doc = new XmlDocument();
            doc.PreserveWhitespace = true;

            XmlElement soapEnvelopeXml = doc.CreateElement("s", "Envelope", "http://schemas.xmlsoap.org/soap/envelope/");
            soapEnvelopeXml.SetAttribute("xmlns:u", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");

            XmlElement soapHeaderXml = doc.CreateElement("s", "Header", "http://schemas.xmlsoap.org/soap/envelope/");
            if (Header != null)
            {
                var imported = doc.ImportNode(Header, true);
                soapHeaderXml.AppendChild(imported);
            }
            soapEnvelopeXml.AppendChild(soapHeaderXml);

            XmlElement soapBodyXml = doc.CreateElement("s", "Body", "http://schemas.xmlsoap.org/soap/envelope/");
            soapBodyXml.SetAttribute("Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "_1");
            if (Body == null)
            {
                throw new Exception("Body is required.");
            }
            else
            {
                XmlNode imported = doc.ImportNode(Body, true);
                soapBodyXml.AppendChild(imported);                
            }

            soapEnvelopeXml.AppendChild(soapBodyXml);
            doc.AppendChild(soapEnvelopeXml);

            if (signed)
            {
                SoapSigner signer = new SoapSigner();

                if (Certificate == null)
                {
                    throw new Exception("A X509 certificate is needed.");
                }

                XmlDocument tempDoc = new XmlDocument();
                tempDoc.PreserveWhitespace = true;
                tempDoc.LoadXml(doc.OuterXml);

                return signer.SignMessage(tempDoc, Certificate, SignAlgorithm.SHA1);
            }

            return doc;
        }

        public void ReadXml(XmlDocument document)
        {
            XmlNamespaceManager ns = new XmlNamespaceManager(document.NameTable);
            ns.AddNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/");

            this.Header = document.DocumentElement.SelectSingleNode("//s:Header", ns) as XmlElement;
            this.Body = document.DocumentElement.SelectSingleNode("//s:Body", ns) as XmlElement;

            if (this.Body == null)
            {
                throw new Exception("No body found.");
            }
        }
    }

Here is the class that sign the SOAP message:

    public class SoapSigner
    {        
               
        public XmlDocument SignMessage(XmlDocument xmlDoc, X509Certificate2 certificate, SignAlgorithm signAlgorithm)
        {
            XmlNamespaceManager ns = new XmlNamespaceManager(xmlDoc.NameTable);
            ns.AddNamespace("s", "http://schemas.xmlsoap.org/soap/envelope/");

            XmlElement soapHeader = xmlDoc.DocumentElement.SelectSingleNode("//s:Header", ns) as XmlElement;
            XmlElement body = xmlDoc.DocumentElement.SelectSingleNode("//s:Body", ns) as XmlElement;

            if (body == null)
                throw new Exception("No body tag found.");

            XmlElement securityNode = xmlDoc.CreateElement(
                "wsse",
                "Security",
                "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");

            XmlElement binarySecurityToken = xmlDoc.CreateElement("wse", "BinarySecurityToken", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
            binarySecurityToken.SetAttribute("EncodingType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary");
            binarySecurityToken.SetAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3");
            binarySecurityToken.SetAttribute("Id", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "BinaryToken1");            

            binarySecurityToken.InnerText = Convert.ToBase64String(certificate.GetRawCertData());

            securityNode.AppendChild(binarySecurityToken);

            soapHeader.AppendChild(securityNode);

            SignedXmlWithId signedXml = new SignedXmlWithId(xmlDoc);

            if (signAlgorithm == SignAlgorithm.SHA1)
            {
                signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
            }
            else
            {
                signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
            }

            RSACryptoServiceProvider rsaKey = null;

            if (signAlgorithm == SignAlgorithm.SHA1)
            {
                rsaKey = (RSACryptoServiceProvider)certificate.PrivateKey;
            }
            else
            {
                var key = (RSACryptoServiceProvider)certificate.PrivateKey;
                var enhCsp = new RSACryptoServiceProvider().CspKeyContainerInfo;
                var cspparams = new CspParameters(enhCsp.ProviderType, enhCsp.ProviderName, key.CspKeyContainerInfo.KeyContainerName);
                rsaKey = new RSACryptoServiceProvider(cspparams);            
            }
            
            signedXml.SigningKey = rsaKey;

            KeyInfo keyInfo = new KeyInfo();
            keyInfo.AddClause(new SecurityTokenReference("BinaryToken1"));

            signedXml.KeyInfo = keyInfo;

            signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;

            Reference reference = new Reference { Uri = "#_1" };
            if (signAlgorithm == SignAlgorithm.SHA1)
            {
                reference.DigestMethod = "http://www.w3.org/2000/09/xmldsig#sha1";
            }
            else
            {
                reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256";
            }
            reference.AddTransform(new XmlDsigExcC14NTransform());
            signedXml.AddReference(reference);

            signedXml.ComputeSignature();

            XmlElement signedElement = signedXml.GetXml();

            securityNode.AppendChild(signedElement);


            if (soapHeader == null)
            {
                soapHeader = xmlDoc.CreateElement("s:Header", "");
                xmlDoc.DocumentElement.InsertBefore(soapHeader, xmlDoc.DocumentElement.ChildNodes[0]);
            }           

            return xmlDoc;
        }
    }

Here is the KeyInfoClause for the binary security token reference:

    public class SecurityTokenReference : KeyInfoClause
    {
        public string BinarySecurityTokenId { get; set; }

        public SecurityTokenReference(string binarySecurityToken)
        {
            this.BinarySecurityTokenId = binarySecurityToken;
        }
        public override XmlElement GetXml()
        {
            XmlDocument doc = new XmlDocument();

            XmlElement strXmlElement = doc.CreateElement("wse", "SecurityTokenReference", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");

            doc.AppendChild(strXmlElement);

            XmlElement reference = doc.CreateElement("wse", "Reference", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
            reference.SetAttribute("URI", "#" + BinarySecurityTokenId);
            reference.SetAttribute("ValueType", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509");

            strXmlElement.AppendChild(reference);

            return strXmlElement;
        }

        public override void LoadXml(XmlElement element)
        {
            throw new NotImplementedException();
        }
    }

Also we need to override the GetIdElement method of SignedXml class to allow find elements by Id attribute defined in http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd

    public class SignedXmlWithId : SignedXml
    {
        public SignedXmlWithId(XmlDocument xml)
            : base(xml)
        {
        }

        public SignedXmlWithId(XmlElement xmlElement)
            : base(xmlElement)
        {
        }

        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("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");

                idElem = doc.SelectSingleNode("//*[@wsu:Id=\"" + id + "\"]", nsManager) as XmlElement;
            }

            return idElem;
        }
    }

The above code will produce an XML document like this:

<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  <s:Header>
    <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
      <wse:BinarySecurityToken xmlns:wse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" u:Id="BinaryToken1" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">MIIDjTCCAva[.......]</wse:BinarySecurityToken>
      <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
          <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
          <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
          <Reference URI="#_1">
            <Transforms>
              <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </Transforms>
            <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
            <DigestValue>DOJ873GFktK2Z23tEeUlZVc0Orc=</DigestValue>
          </Reference>
        </SignedInfo>
        <SignatureValue>GpvLrZFpBV9OHygTA[.......]</SignatureValue>
        <KeyInfo>
          <wse:SecurityTokenReference xmlns:wse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <wse:Reference ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509" URI="#BinaryToken1"/>
          </wse:SecurityTokenReference>
        </KeyInfo>
      </Signature>
    </wsse:Security>
  </s:Header>
  <s:Body u:Id="_1">
    <getVersion xmlns="http://msgsec.wssecfvt.ws.ibm.com"/>
  </s:Body>
</s:Envelope>

You can download the source code at: https://github.com/zinkpad/SignSoapMessage

I hope this will be useful for you :-)

 

Sources:

http://stackoverflow.com/questions/26870832/signing-soap-message-with-xml-signature-and-wcf

http://stackoverflow.com/questions/30779840/change-canonicalization-algorithm-with-wcf

http://stackoverflow.com/questions/31142658/ws-security-verification-signedxml-checksignature-always-returns-false-even-fo