2

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.

11
  • 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". Commented Jul 2, 2019 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. 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 Commented Jul 2, 2019 at 14:44
  • Verifying is described in How to: Verify the Digital Signatures of XML Documents Commented Jul 2, 2019 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. Commented Jul 2, 2019 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. Commented Jul 2, 2019 at 15:41

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

var reference = new Reference { Uri = "#Body" }; --- signedXml.ComputeSignature(); Now gives: System.Security.Cryptography.CryptographicException: Malformed reference element. .NET CORE 6

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.