Link to home
Start Free TrialLog in
Avatar of Roberto Garcia ☁
Roberto Garcia ☁

asked on

How to compute <ds:SignatureValue> base on the node <ds:SignedInfo>, it could be in C#, or any another library like OpenSSL

STORY:


I need to create a SOAP Envelop with C#.NET to consume a Java-based web service

I can use SoapUI for testing and it works properly.

now I need to create a C#.NET console app to consume this web service.

I was able to compute the <ds:DigestValue> but I couldn't' compute the SignatureValue.

note the "ds" prefix which is required for the Java-based web service and I don't have control over it.

I have tried tens of solutions from the internet and none works.

QUESTION:


How to compute <ds:SignatureValue> base on the node <ds:SignedInfo>, it could be in C#, or any another library like OpenSSL


<ds:SignedInfo>
    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
        <ec:InclusiveNamespaces PrefixList="#default"
            xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" />
        </ds:CanonicalizationMethod>
        <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
        <ds:Reference URI="#id-2">
            <ds:Transforms>
                <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                    <ec:InclusiveNamespaces PrefixList=""
                        xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" />
                    </ds:Transform>
                </ds:Transforms>
                <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                <ds:DigestValue>dHC6LFzXQAKVaJt6KZIAqA+bjso=</ds:DigestValue>
            </ds:Reference>
        </ds:SignedInfo>

Open in new window


at the end I need something like this:

<Envelope
    xmlns="http://schemas.xmlsoap.org/soap/envelope/">
    <Header>
        <wsse:Security
            xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
            xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
            xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
            <wsse:BinarySecurityToken EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" wsu:Id="X509-DCF75F4B72AC2DE4DD1561147190445574">MIICzzCCAbegAw....................pM5/2xG</wsse:BinarySecurityToken>
            <wsse:UsernameToken wsu:Id="UsernameToken-DCF75F4B72AC2DE4DD1561147190444573">
                <wsse:Username>MyUserName</wsse:Username>
                <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">MyPassword</wsse:Password>
            </wsse:UsernameToken>
            <ds:Signature Id="SIG-DCF75F4B72AC2DE4DD1561147190447577"
                xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:SignedInfo>
                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                        <ec:InclusiveNamespaces PrefixList="#default"
                            xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                        </ds:CanonicalizationMethod>
                        <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
                        <ds:Reference URI="#id-2">
                            <ds:Transforms>
                                <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                                    <ec:InclusiveNamespaces PrefixList=""
                                        xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                                    </ds:Transform>
                                </ds:Transforms>
                                <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                                <ds:DigestValue>dHC6LFzXQAKVaJt6KZIAqA+bjso=</ds:DigestValue>
                            </ds:Reference>
                        </ds:SignedInfo>
                        <ds:SignatureValue>I6U2GRZYxXoTjf/g-----e7dOw==</ds:SignatureValue>
                        <ds:KeyInfo Id="KI-DCF75F4B72AC2DE4DD1561147190445575">
                            <wsse:SecurityTokenReference wsu:Id="STR-DCF75F4B72AC2DE4DD1561147190445576">
                                <wsse:Reference URI="#X509-DCF75F4B72AC2DE4DD1561147190445574" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
                            </wsse:SecurityTokenReference>
                        </ds:KeyInfo>
                    </ds:Signature>
                </wsse:Security>
            </Header>
            <Body wsu:Id="id-2"
                xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
                <consultarHC
                    xmlns="http://ws.hc.dc.com/v1">
                    <idsus>NumericValue1</idsus>
                    <clasus>StringValue</clasus>
                    <tipoid>Digit</tipoid>
                    <id>NumericValue2</id>
                    <papellido>StringValue</papellido>
                </consultarHC>
            </Body>
        </Envelope>

Open in new window

Avatar of ste5an
ste5an
Flag of Germany image

You just need to import the WSDL as service reference in VS for consuming a SOAP web service in .Net. No need for manual interaction.
I've had this same issue before when I was building a SAML identity provider. The concept here is that you have to first have a public/private key pair. Then you extract the non-signature XML content using a process known as C14N / canonicalization. Once you have that string, you use the private key to generate a digital signature of that string and base64-encode it.

I'm not in front of my computer at the moment but I can get you a C# code sample for this later when I get a moment.
Also a note - it can't be just ANY private key. The web service that consumes your signed request needs the corresponding public key in order to validate the signature.
Okay, so I took my code from my identity provider and modified it to fit your situation.

To try it out:
1. Create a new Windows Forms Application.

2. Add a textbox, make it multi-line, and expand it to be a little larger, and add a button somewhere on the form.

3. Set the textbox contents to be your XML WITHOUT your <ds:Signature> node. That entire signature node will be generated by .NET.
It should look something like this at this point, although appearances don't really matter too much:
User generated image
4. I will assume you're using the very common PEM format for your private key. The .NET framework prefers different formats, so to handle that PEM format, we'll use the awesome and free BouncyCastle library, available for C# here:
http://www.bouncycastle.org/csharp/

Just download the latest release file containing the assembly and then drop that BouncyCastle.Crypto.dll file into your project folder, and then add a reference to it in your project. All we use it for is to load the private key and translate it into the .NET-compatible private key format when the program runs.

5. Also add a project reference to the System.Security assembla (should be a standard .NET assembly).

6. Open up your Form1.cs file and add the following "using" lines to the top:
using System.Xml;
using System.Security.Cryptography.Xml;
using System.IO;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.OpenSsl;

7. Double-click on the button you added in the form designer to generate the initial button event handler code (I called my button "btnSign" so the event handler is "btnSign_Click").

8. Add the following class BENEATH your Form1 class (same namespace, though):
    public class SignedXmlWithNamespacedId : SignedXml
    {
        private string namespacePrefix;
        private string namespaceURI;

        public SignedXmlWithNamespacedId(XmlElement xmlElement, string nsPrefix, string nsURI) : base(xmlElement)
        {
            namespacePrefix = nsPrefix;
            namespaceURI = nsURI;
        }

        public override XmlElement GetIdElement(XmlDocument xmlDoc, string id)
        {
            // If we're here, then the initial call didn't work and we need to specify our "wsu" namespace for the attribute
            XmlNamespaceManager nsm = new XmlNamespaceManager(xmlDoc.NameTable);
            nsm.AddNamespace(namespacePrefix, namespaceURI);
            return xmlDoc.SelectSingleNode("//*[@" + namespacePrefix + ":Id =\"" + id + "\"]", nsm) as XmlElement;
        }
    }

Open in new window


9. Fill the button click handler with this code:
private void btnSign_Click(object sender, EventArgs e)
        {
            // Load the private key
            var privateKey = loadPrivateKey("privatekey.pem");

            // Load the XML
            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(textBox1.Text);


            // Set up the namespace manager so we know what each namespace means
            XmlNamespaceManager nsm = new XmlNamespaceManager(xmlDoc.NameTable);
            nsm.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
            nsm.AddNamespace("wsse", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
            
            // Find XML node with wsu:Id="id-2", which should be the <Body> tag on which we're basing the signature
            string ReferenceID = "id-2";
            var xnList = xmlDoc.SelectNodes("//node()[@wsu:Id='" + ReferenceID + "']", nsm);
            var element = (XmlElement)xnList[0];

            // Set up the signature based on that C14N reference - the custom class is used to tell the default MS class how to find the right element using the namespaced Id attribute
            var signedXmlElement = new SignedXmlWithNamespacedId(element, "wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
            signedXmlElement.SigningKey = privateKey;

            // Set up the C14N canonicalization of the XML
            var reference = new Reference("#" + ReferenceID);
            XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();
            XmlDsigExcC14NTransform c14n = new XmlDsigExcC14NTransform();
            c14n.InclusiveNamespacesPrefixList = "wsu";
            reference.AddTransform(env);
            reference.AddTransform(c14n);

            signedXmlElement.AddReference(reference);
            signedXmlElement.ComputeSignature();

            // Add the KeyInfo as necessary here
            signedXmlElement.KeyInfo = new KeyInfo();

            // Finally, get the XML element for the signature, and add it to the wsse:Security node
            var signatureXmlElement = signedXmlElement.GetXml();
            var securityNodes = xmlDoc.SelectNodes("//wsse:Security", nsm);
            securityNodes[0].AppendChild(signatureXmlElement);

            // Update the text box
            textBox1.Text = xmlDoc.OuterXml;
        }

Open in new window


10. Finally, add the "loadPrivateKey" method into your Form1 class:
private System.Security.Cryptography.RSA loadPrivateKey(string Filename)
        {
            // Sanity check
            if (!File.Exists(Filename))
            {
                throw new Exception("Could not find file!");
            }

            // Load the Private Key
            AsymmetricCipherKeyPair keyPair;
            bool askForPassword = false;

            using (var reader = File.OpenText(Filename))
            {
                try
                {
                    // Try to read without a password
                    keyPair = (AsymmetricCipherKeyPair)new PemReader(reader).ReadObject();
                    return DotNetUtilities.ToRSA((Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters)keyPair.Private);
                }
                catch (PasswordException pex)
                {
                    askForPassword = true;
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message);
                    return null;
                }
            }

            if(askForPassword)
            {
                // Just a placeholder in case you need to prompt the user for a password
            }

            return null;
        }

Open in new window


...and then that should run and generate the signed XML when you click the button.

Also, you'll need to put your private key file into the project and make sure it's copied into your output so the loadPrivateKey() call knows where to find the file.
This question needs an answer!
Become an EE member today
7 DAY FREE TRIAL
Members can start a 7-Day Free trial then enjoy unlimited access to the platform.
View membership options
or
Learn why we charge membership fees
We get it - no one likes a content blocker. Take one extra minute and find out why we block content.