XMLSigner No longer works in 4.6.2 - Malformed reference element

2.7k views Asked by At

After Upgrading an application from 3.5 to 4.6.2 The following block of code no longer works. I get "Malformed reference element" Errors, even though it worked just fine as a 3.5 application. The code fails with the above error on what should be a good reference. I've tried everything I can think of can cannot get the ASP.Net version working. I've built a testbed version as a console application which works fine until it gets to the last reference which fails with "Unable to resolve Uri Signature1.jpg." I've read that XMLSigner doesn't accept anything other than id, ID, and Id as elements to look for to match references, however I don't believe this to be the case because it works in the console application.

The core of the question is:

  1. Why am I getting a "Malformed reference element" for signedXMl.AddReference(new Reference("#Head01"));
  2. How do I fix the reference to the customer Signatures "src="Signature1.jpg""

Function in question:

private XmlDocument SignDoc(XmlDocument doc, RSA key, X509Certificate x509cert, ArrayList alSignatures)
    {
        string signatureID = "TamperSealer01";
        Uri uri = new Uri(ConfigurationManager.AppSettings["SomeSetting"]); 
        XmlResolver resolver = new XmlSignatureResolver(uri);
        SignedXml signedXml = new SignedXml(doc);
        signedXml.Signature.Id = signatureID;
        signedXml.Resolver = resolver;

        // Add the key to the SignedXml responseDocument. 
        signedXml.SigningKey = key;

        // Create a new KeyInfo object.
        KeyInfo keyInfo = new KeyInfo();
        keyInfo.AddClause(new RSAKeyValue(key));

        KeyInfoX509Data x509Data = new KeyInfoX509Data(x509cert);

        string subjectName = x509cert.Subject;
        subjectName = subjectName.Replace("S=", "ST=");
        string tmpSubName = subjectName;

        tmpSubName = tmpSubName.Replace("O=A", "O=B");
        tmpSubName = tmpSubName.Replace("CN=A", "CN=B");

        x509Data.AddSubjectName(tmpSubName);
        x509Data.AddIssuerSerial(x509cert.Issuer, x509cert.GetSerialNumberString()); //GetIssuerName
        keyInfo.AddClause(x509Data);
        signedXml.KeyInfo = keyInfo;

        //TIMESTAMP
        XmlElement signaturePropertiesRoot = doc.CreateElement("SignatureProperties", "http://www.w3.org/2000/09/xmldsig#");

        DataObject signatureProperties = new DataObject();
        signatureProperties.Id = "TimeStamp";
        signatureProperties.Data = signaturePropertiesRoot.SelectNodes(".");
        signedXml.AddObject(signatureProperties);

        // and add a reference to the data object
        Reference propertiesRef = new Reference();
        propertiesRef.Uri = "#TimeStamp";
        propertiesRef.Type = "http://www.w3.org/2000/09/xmldsig#SignatureProperties";
        signedXml.AddReference(propertiesRef);

        XmlElement property = doc.CreateElement("SignatureProperty", "http://www.w3.org/2000/09/xmldsig#");
        property.SetAttribute("Id", "TamperSealer01TimeStamp");
        property.SetAttribute("Target", "#" + signedXml.Signature.Id);
        signaturePropertiesRoot.AppendChild(property);

        XmlElement timestamp = doc.CreateElement("DateTimeStamp", "http://www.w3.org/2000/09/xmldsig#");
        timestamp.SetAttribute("DateTime", String.Format("{0:s}Z", DateTime.Now.ToUniversalTime()));
        property.AppendChild(timestamp);
        signedXml.ComputeSignature();

        // References contains three strings "Head01", "Data01", and "View01"
        foreach (string docRef in references) 
        {
            // Create a reference to be signed.
            Reference tempRef = new Reference();
            tempRef.Uri = "#" + docRef;
            Logger.Current.LogInformation("DocRef: #" + docRef + ".");
            // Add the reference to the SignedXml object
            signedXml.AddReference(tempRef);
            signedXml.ComputeSignature(); //Immediately Fails here
        }

        // alSignatures only contains "Signature1.jpg" in this case. Don't yell at me for this crappy code, I didn't write it and plan on fixing it when everything else works.
        int ctr = 0;
        foreach (string str in alSignatures)
        {
            Reference testRef = new Reference();
            Uri relativeUri = new Uri(alSignatures[ctr].ToString(), UriKind.RelativeOrAbsolute);
            Logger.Current.LogInformation("Signature Reference: " + alSignatures[ctr].ToString());
            testRef.Uri = alSignatures[ctr].ToString();
            signedXml.AddReference(testRef);
            ctr += 1;
        }

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

        // Get the XML representation of the signature and save it to an XmlElement object.
        XmlElement xmlDigitalSignature = signedXml.GetXml();
        XmlElement signaturesElement = doc.CreateElement("SIGNATURES", "http://www.mismo.org");

        signaturesElement.AppendChild(signedXml.GetXml());
        doc.DocumentElement.AppendChild(signaturesElement);

        key.Clear();
        key.Dispose();
        return doc;
}

The XML it should be signing is minimally thus:

<?xml version="1.0" encoding="UTF-8"?>
<DOCUMENT MISMOVersionIdentifier="1.02">
    <HEADER _ID="Head01">
        <SIGNATURE_MODEL>
            <SIGNER AreaIDREF="Borrower1SignatureArea" SectionIDREF="BorrowerSignatures" SignatureIDREF="Borrower1SignatureLine" SignatureType="Image" TargetsIDREFS="View01" _RoleType="Borrower" _SignatureOrderNumber="1" />
            <SIGNER SignatureIDREF="TamperSealer01" SignatureType="DigitalSignature" TargetsIDREFS="Head01 Data01 View01" _RoleType="TamperSealer" _SignatureOrderNumber="1" />
        </SIGNATURE_MODEL>
    </HEADER>
    <DATA _ID="Data01">
        <MAIN>
        </MAIN>
    </DATA>
    <VIEW _ID="View01" _MIMETypeDescription="text/html" _TaggedIndicator="True">
        <html xmlns="http://www.w3.org/1999/xhtml">
            <body>
                <span class="dataEntered" id="BORROWER_Signer-Info">
                    <SIGNATURE_SECTION _ID="BorrowerSignatures">
                        <SIGNATURE_AREA _ID="Borrower1SignatureArea">
                            <p class="right">
                                <SIGNATURE_ABOVE_LINE />
                                <SIGNATURE_IMAGE _EncodingTypeDescription="None" _ID="Borrower1SignatureLine" _MIMEType="image/jpeg">
                                    <img align="right" alt="Signature file is missing - Invalid Document" src="Signature1.jpg" />
                                </SIGNATURE_IMAGE>
                            </p>
                            <p>04/12/2011 12:00 PM</p>
                        </SIGNATURE_AREA>
                    </SIGNATURE_SECTION>
                </span>
            </body>
        </html>
    </VIEW>
</DOCUMENT>
2

There are 2 answers

0
Ondrej Svejdar On

The relevant piece of code - CalculateHashValue of System.Security.Cryptography.Xml.Reference:

// for "Head01" this is "#Head01"
if (this.m_uri[0] == '#')
{
   // idFromLocalUri is set to "Head01"
   string idFromLocalUri = Utils.GetIdFromLocalUri(this.m_uri, out flag);
   ...
   // there is no element with Id="Head01" - so xmlElement is null
   var xmlElement = this.SignedXml.GetIdElement(document, idFromLocalUri);
   ...
   if (xmlElement == null)
   {
     // this is the error you're getting
     throw new CryptographicException(SecurityResources.GetResourceString("Cryptography_Xml_InvalidReference"));
   }
}

So you fail on reference validation - there is no element with this id in document - btw. experimentally changing "_ID" to "Id" in your xml fixed the issue.

Good news is the SignedXml class is extensible and you can overload the XmlElement GetIdElement(XmlDocument document, string idValue) method to take "_ID" into an account.

// just a sample
class MyCustomSignedXml : SignedXml {
   ...
   override XmlElement GetIdElement(XmlDocument document, string idValue) {
     var element = document.SelectSingleNode($"//*[@_ID='{idValue}']") as XmlElement;
     if (element != null) {
       return element;
     } 

     return base.GetIdElement(document, idValue);
   }
}
0
sunrunner20 On

With Ondrej Svejdar's hints I was able to get this working. It turns out I needed two classes for this to work. I haven't been able to test in UAT, but so far I needed two classes, and a registry edit. A Custom XmlUrlResolver to allow the DTDs to be in a separate location and point to the same folder as the XML for the external references, and a modified SignedXml class to handle IDs.

Registry edit: https://support.microsoft.com/en-us/help/3148821/after-you-apply-security-update-3141780-net-framework-applications-enc

Modified SignedXml class:

    public class CustomIdSignedXml : SignedXml
{
    private static readonly string[] idAttrs = new string[]
    {
    "_id",
    "_Id",
    "_ID"
    };

    public CustomIdSignedXml(XmlDocument doc) : base(doc)
    {
        return;
    }

    public override XmlElement GetIdElement(XmlDocument doc, string id)
    {
        XmlElement idElem = null;
        // check to see if it's a standard ID reference
        //XmlElement idElem = base.GetIdElement(doc, id);
        //if (idElem != null)
        //  return idElem;

        //I get the feeling this is horridly insecure
        XmlElement elementById1 = doc.GetElementById(id);
        if (elementById1 != null) return elementById1;
        // if not, search for custom ids
        foreach (string idAttr in idAttrs)
        {
            idElem = doc.SelectSingleNode("//*[@" + idAttr + "=\"" + id + "\"]") as XmlElement;
            if (idElem != null)
                break;
        }

        return idElem;
    }
}

Modified XmlResolver:

public class DTDAndSignatureResolver : XmlUrlResolver
{
    private readonly Uri DTDUri;
    private readonly List<string> XmlExtensions = new List<string>() { ".xml" };
    private readonly List<string> DTDExtensions = new List<string>() { ".dtd", ".ent" };
    private ICredentials credentials;

    public DTDAndSignatureResolver(Uri DTDUri)
    {
        this.DTDUri = DTDUri;
    }

    public override ICredentials Credentials
    {
        set { credentials = value; }
    }

    public override object GetEntity(Uri absoluteUri, string role, Type ofObjectToReturn)
    {
        if (DTDExtensions.Any(e => absoluteUri.ToString().ToLower().EndsWith(e)) || XmlExtensions.Any(e => absoluteUri.ToString().ToLower().EndsWith(e)))
        {
            return base.GetEntity(absoluteUri, role, ofObjectToReturn); //For DTD/ENT/XML lookup
        }
        else
        {
            return base.GetEntity(DTDUri, null, ofObjectToReturn); //For signature image lookup
        }
    }

    public override Uri ResolveUri(Uri uri, string relativeUri)
    {
        return base.ResolveUri(DTDUri, relativeUri);
    }
}

With both of these modifications my .Net 4.6.2 code was able to verify the Signed XML documents from .Net 3.5, and vice versa.