1

I am working to replace a legacy application that is no longer being maintained. I have most things replaced except the digital signature method. I have an implementation in .net core and I'm a bit confused as to why it's failing to verify it's own signed document.

Consider the following xml(code is messy, just running a poc at the moment):

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <tns:Body id="Body" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/">This is a test</tns:Body>
</soap:Envelope>

With the following c#:

var publicCert = new X509Certificate2(File.ReadAllBytes("public.cer"));
var cert = GetSigningCertificate("private.pfx", "super amazing password");

var xml = File.ReadAllText("test.xml");
var doc = new XmlDocument {PreserveWhitespace = false};
doc.LoadXml(xml);

//remove <?xml?> tag
if (doc.FirstChild is XmlDeclaration)
{
    doc.RemoveChild(doc.FirstChild);
}

//create the header node:  NOTE: This is not on the original document but required by the vendor
var header = new StringBuilder();
header.AppendLine("<SOAP:Header xmlns:SOAP=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-SEC=\"http://schemas.xmlsoap.org/soap/security/2000-12\">");
header.AppendLine("<SOAP-SEC:Signature>");
header.AppendLine("</SOAP-SEC:Signature>");
header.AppendLine("</SOAP:Header>");
var headerXml = new XmlDocument {PreserveWhitespace = false};
headerXml.LoadXml(header.ToString());
//add header document to the main document
if (headerXml.DocumentElement != null) doc.DocumentElement?.InsertBefore(doc.ImportNode(headerXml.DocumentElement, true), doc.DocumentElement?.FirstChild);
var domHeader = (XmlElement) doc.FirstChild.FirstChild;

// Create a SignedXml object.
var referenceUri = "#Body";
var signedXml = new SignedXml(domHeader) { SigningKey = cert.PrivateKey };  #NOTE: signing is done at the /SOAP:Header level per the vendor, not at the ParentDocument level
signedXml.SignedInfo.CanonicalizationMethod = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments";
signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
// Create a reference to be signed.
var reference = new Reference
{
    Uri = referenceUri,
    DigestMethod = "http://www.w3.org/2000/09/xmldsig#sha1"
};

// Add the reference to the SignedXml object.
signedXml.AddReference(reference);

//add key info
var keyInfo = new KeyInfo();
var certInfo = new KeyInfoX509Data();
if (cert.SerialNumber != null) certInfo.AddIssuerSerial(cert.Issuer, cert.SerialNumber);
keyInfo.AddClause(certInfo);
signedXml.KeyInfo = keyInfo;

// Compute the signature.
signedXml.ComputeSignature();

// Get the XML representation of the signature and save
// it to an XmlElement object.
var xmlDigitalSignature = signedXml.GetXml();

//get the SOAP-SEC header and add our signature to it
var soapSecurityList = doc.GetElementsByTagName("Signature", "http://schemas.xmlsoap.org/soap/security/2000-12");
if(soapSecurityList.Count == 0)
{
    throw new Exception("Could not find SOAP-SEC header!");
}
var soapSecurity = soapSecurityList.Item(0);

soapSecurity.AppendChild(xmlDigitalSignature);

This produces the following xml(cert, sig,hash,serial are placeholders for the actual values and have been redacted for posting here):

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <SOAP:Header xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-SEC="http://schemas.xmlsoap.org/soap/security/2000-12">
        <SOAP-SEC:Signature>
            <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
                <SignedInfo>
                    <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" />
                    <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
                    <Reference URI="#Body">
                        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                        <DigestValue>hash</DigestValue>
                    </Reference>
                </SignedInfo>
                <SignatureValue>sig</SignatureValue>
                <KeyInfo>
                    <X509Data>
                        <X509IssuerSerial>
                            <X509IssuerName>cert</X509IssuerName>
                            <X509SerialNumber>serial</X509SerialNumber>
                        </X509IssuerSerial>
                    </X509Data>
                </KeyInfo>
            </Signature>
        </SOAP-SEC:Signature>
    </SOAP:Header>
    <tns:Body id="Body" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/">This is a test</tns:Body>
</soap:Envelope>

However, trying to validate it fails with the following code:

//verify what we just did
var verifiedXml = new XmlDocument {PreserveWhitespace = false};
verifiedXml.LoadXml(doc.InnerXml);  //document object from above
var signature = verifiedXml.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
var validSignature = new SignedXml((XmlElement) signature[0].ParentNode);
return validSignature.CheckSignature(cert, true);  //also tried publicCert but no luck

Signature always fails. I have looked at numerous other stack overflow questions and most of them involve whitespace issues(all documents ignore white spaces and all sources are linearized before being read in) or adding "ds" namespace to the signature prefix during the canonicalization method. I implemented that "ds" fix and tried again. It produces the following xml, which still fails to validate.

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <SOAP:Header xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-SEC="http://schemas.xmlsoap.org/soap/security/2000-12">
        <SOAP-SEC:Signature>
            <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="#Body">
                        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                        <ds:DigestValue>hash</ds:DigestValue>
                    </ds:Reference>
                </ds:SignedInfo>
                <ds:SignatureValue>sig</ds:SignatureValue>
                <ds:KeyInfo>
                    <ds:X509Data>
                        <ds:X509IssuerSerial>
                            <ds:X509IssuerName>cert</ds:X509IssuerName>
                            <ds:X509SerialNumber>serial</ds:X509SerialNumber>
                        </ds:X509IssuerSerial>
                    </ds:X509Data>
                </ds:KeyInfo>
            </ds:Signature>
        </SOAP-SEC:Signature>
    </SOAP:Header>
    <tns:Body id="Body" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/">This is a test</tns:Body>
</soap:Envelope>

UPDATE Based on comments. Tried the following:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
    <tns:Body id="Body" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/">This is a test</tns:Body>
</soap:Envelope>
using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Security.Policy;

namespace XmlDsig
{
    public class XmlDsig
    {
        public bool Sign()
        {
            try
            {
                var cert = GetSigningCertificate("cert.pfx", "password");
                var xml = File.ReadAllText("test.xml");

                // Create a new XML document.
                var doc = new XmlDocument {PreserveWhitespace = false};
                doc.LoadXml(xml);


                //remove <?xml?> tag
                if (doc.FirstChild is XmlDeclaration)
                {
                    doc.RemoveChild(doc.FirstChild);
                }

                //create the header node
                var header = new StringBuilder();
                header.AppendLine("<SOAP:Header xmlns:SOAP=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-SEC=\"http://schemas.xmlsoap.org/soap/security/2000-12\">");
                header.AppendLine("<SOAP-SEC:Signature>");
                header.AppendLine("</SOAP-SEC:Signature>");
                header.AppendLine("</SOAP:Header>");
                var headerXml = new XmlDocument {PreserveWhitespace = false};

                headerXml.LoadXml(header.ToString());


                //add header document to the main document
                if (headerXml.DocumentElement != null) doc.DocumentElement?.InsertBefore(doc.ImportNode(headerXml.DocumentElement, true), doc.DocumentElement?.FirstChild);

                var domHeader = (XmlElement) doc.FirstChild.FirstChild;

                SignXmlWithCertificate(domHeader, cert);


                var validatorXml = new XmlDocument();
                validatorXml.LoadXml(doc.OuterXml);
                var sigContext = validatorXml.GetElementsByTagName("Header", "http://schemas.xmlsoap.org/soap/envelope/");
                var validator = new SignedXml((XmlElement) sigContext[0]);          return validator.CheckSignature(cert, true);


            }
            catch (Exception e)
            {
                return false;
                // return e.ToString();
            }
        }



        public static void SignXmlWithCertificate(XmlElement assertion, X509Certificate2 cert)
        {
            var signedXml = new SignedXml(assertion) {SigningKey = cert.PrivateKey};
            var reference = new Reference {Uri = "#Body"};
            reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
            signedXml.AddReference(reference);

            var keyInfo = new KeyInfo();
            keyInfo.AddClause(new KeyInfoX509Data(cert));

            signedXml.KeyInfo = keyInfo;
            signedXml.ComputeSignature();
            var xmlsig = signedXml.GetXml();

            assertion.FirstChild.AppendChild(xmlsig);
        }
    }
}

Still fails to validate.

Update 2 Now I am getting the following error when i call CheckSignature:

SignatureDescription could not be created for the signature algorithm supplied.

Seems like this is a common problem on .net 4 with sha256, but I'm using sha1 in my current example and i am using dotnet core. I have tried sha256 too but same issue persists.

Morcalavin
  • 142
  • 2
  • 9
  • I would use a sniffer like wireshark or fiddler and compare old working code with new code that is failing. It doesn't look like you have a real hash and the signature only contains the string "sig". – jdweng Jul 02 '19 at 14:43
  • When the code is very messy, any errors are probably caused by the messy code. Check [How to: Sign XML Documents with Digital Signatures](https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-sign-xml-documents-with-digital-signatures). Can you reproduce your problem with *that* code? The docs explain that adding an enveloped transformation is vital for verification and yet there's no such code in the question's code – Panagiotis Kanavos Jul 02 '19 at 14:44
  • Verifying is described in [How to: Verify the Digital Signatures of XML Documents](https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-verify-the-digital-signatures-of-xml-documents) – Panagiotis Kanavos Jul 02 '19 at 14:45
  • @jdweng The "sig" is a placeholder for the actual signature as is the "hash". It is not part of the actual document. – Morcalavin Jul 02 '19 at 15:34
  • Unfortunatley all the documentation provided describes very simplistic methods that do not apply to my use case, specifically not creating the SignedXml object from the root document(doc) but instead from a sub element(domHeader in this case). This changes the context of the signature. Also, the documents being generated (with the ds namespace prefix) 100% match the old system, which is verifying the message just fine, so it's something specific with the implementation. "Messiness" aside, I see no *functional* reason why the code doesn't work as expected. – Morcalavin Jul 02 '19 at 15:41
  • I posted a soap singing a while back that you may want to look at. Did you do a sniffer comparison like I suggested and what differences did you find? Here is the link : https://stackoverflow.com/questions/46722997/saml-assertion-in-a-xml-using-c-sharp/46724392 – jdweng Jul 02 '19 at 15:45
  • @jdweng This is all local dev, so nothing is going across the network. I will take a look at your code and get back to you. As far as transforms go, the current working version of the document sent to the vendor doesn't have any transform nodes at all, and looking at the specification, transform nodes are NOT required and a default is used if there is no transform. However, for completeness I added the following but the code still fails to validate: var env = new XmlDsigC14NWithCommentsTransform(); env.Context = domHeader; reference.AddTransform(env); – Morcalavin Jul 02 '19 at 15:51
  • My code just creates an signed XML and doesn't care if it is being sent across a network. Have you tried an on-line checker to see what part of the xml is failing? The online checker should specify if a part is missing or the CRC/HASH of the signature is failing – jdweng Jul 02 '19 at 15:58
  • @jdweng I was referring to your fiddler/wireshark suggestions for the online thing. – Morcalavin Jul 02 '19 at 16:04
  • The ds is occurring due to this line being different : – jdweng Jul 02 '19 at 16:17
  • @jdwneg If I add the DS namespace during the canonizalization process, I get the same signature between .net and the legacy system(yay). That was working in my original code. At this point, my problem is not "I can't arrive at the same signature as the legacy system". It's "why does the same signature not validate on my code when it does on their side" OR "why does a signature generated in .net code not validate against itself" – Morcalavin Jul 02 '19 at 16:23

1 Answers1

1

Found the answers I was looking for.

var verify_xml = new XmlDocument();
verify_xml.LoadXml(doc.OuterXml);  //doc = the signed document loaded from disk
var signature = verify_xml.GetElementsByTagName("Signature", "http://www.w3.org/2000/09/xmldsig#");
var verify = new SignedXml((XmlElement)verify_xml.DocumentElement.FirstChild);
verify.LoadXml((XmlElement) signature[0]);
verify.CheckSignature(cert, true);

The Element passed to the SignedXml() constructor is the signing context(so the SOAP:Header element). LoadXml() is where you actually load the signature itself(not in the constructor). Fixing this resolved all my issues.

Morcalavin
  • 142
  • 2
  • 9
  • Thank you, this saved me lot of time. – Nikola Gaić Feb 25 '21 at 10:12
  • var reference = new Reference { Uri = "#Body" }; --- signedXml.ComputeSignature(); Now gives: System.Security.Cryptography.CryptographicException: Malformed reference element. .NET CORE 6 – Per G Feb 04 '22 at 09:37