Validating XML Digital Signatures with References Using Unrecognized URI Prefixes

During the last few month I have been working on and off on a solution which implements security features for a larger project, part of this work has been to create a wrapper around the XML Digital Signature functionality of .NET Framework 2.0. I thought that I would share a solution to a problem related to validating messages that has reference URIs on the form "cid:<somename>", some of you may recognize this as a reference to a Content-ID (cid) in a MIME message.

I had a signed message that looked something like the sample at the end of this post. Make note of the <Reference> elements and examine the URI attributes.

This XML message was the body of a multipart MIME message and each URI references a message part in the MIME message through the MIME Content-ID. If you load this message in a SignedXml instance and call  CheckSignature you will receive the error: "The URI prefix is not recognized". This error is not unreasonable as SignedXml tries to use the URI to resolve the reference but cannot do that, but I still needed to validate the reference.

I tried various ways to work around this problem. The one that I thought had most merit tried to add Reference instances (associated to the SignedXml instance) which were loaded with the proper data and URI, but neither that nor any other attempt involving pre-populating data in the SignedXml were successful. However, while working with this problem I was examining the call stack of the "The URI prefix is not recognized" error when the solution became obvious to me. Below is a recreation of the call stack:

System.NotSupportedException: The URI prefix is not recognized.
at System.Net.WebRequest.Create(Uri requestUri, Boolean useUriBase)
at System.Net.WebRequest.Create(Uri requestUri)
at System.Security.Cryptography.Xml.Reference.CalculateHashValue(XmlDocument document, CanonicalXmlNodeList refList)
at System.Security.Cryptography.Xml.SignedXml.CheckDigestedReferences()
at System.Security.Cryptography.Xml.SignedXml.CheckSignature()

I realized that it was WebRequest.Create that threw the error, but as this is an extensible part of .NET all I have to do is to registering "cid" as a valid URI with .NET! If you enable "cid" by calling WebRequest.RegisterPrefix then WebRequest.Create will be able to lookup the reference based on the URI. Below is a description of the steps involved in the solution:

  • To register "cid" as an URI in .NET you do this: bool registrationResult = WebRequest.RegisterPrefix("cid:", new CidRequestCreator());
  • Use the classes and techniques described in https://support.microsoft.com/kb/812409/EN-US/ to create the CidRequestCreator, CidWebRequest and CidWebResponse. The KB article describes how to do implement support for the "FTP" URI prefix, so I modified it to work with "cid" the way I wanted it to (read the data stream from a file based on the URI).
  • Before calling SignedXml.CheckSignature you need to save the attachments to the folder from which the CidWebRequest classes read messages.

Below is a high-level example of the logical steps that needs to be performed in my solution:

RegisterCidUriPrefix();
foreach(string uri in _attachments.Keys)
{
SaveAttachmentToDisk(uri, _attachments[uri]);
}

SignedXml signedXml = new SignedXml(xmlDocument);
XmlNodeList nodeList = xmlDocument.GetElementsByTagName("Signature");
signedXml.LoadXml((XmlElement)nodeList[0]);

res = signedXml.CheckSignature();

Please note that RegisterPrefix is only required to be called once per process (or it might be per application domain), this means that if you call it multiple times it will only return true the first time and additional calls will return false. I do not know how expensive the call the RegisterPrefix is, but it might be a good optimization to keep track of if you already have called RegisterPrefix and avoid calling it multiple times.

While customizing the FTP-sample I was able to remove most of the properties of request and response classes, but I also needed to add a Close method to the CidWebResponse class as the stream to the file needs to be closed. The file stream was opened in the constructor of the CidWebRequest class.

Sample of a signed XML message:

<SOAP:Envelope xmlns:SOAP="https://schemas.xmlsoap.org/soap/envelope/">
<SOAP:Header>
    <DroppedStuffNotRelevantToThisDiscussion />
<Signature xmlns:ns0="https://schemas.xmlsoap.org/soap/envelope/" xmlns="https://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="https://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
<SignatureMethod Algorithm="https://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<Reference URI="">
<Transforms>
<Transform Algorithm="https://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<Transform Algorithm="https://www.w3.org/TR/1999/REC-xpath-19991116">
<XPath xmlns:SOAP="https://schemas.xmlsoap.org/soap/envelope/">not(ancestor-or-self::node()[@SOAP:actor="urn:oasis:names:tc:ebxml-msg:actor:nextMSH"] | ancestor-or-self::node()[@SOAP:actor="https://schemas.xmlsoap.org/soap/actor/next"])</XPath>
</Transform>
</Transforms>
<DigestMethod Algorithm="https://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>qBkMHkzxhFGG9HJ1j01u+rrVfGM=</DigestValue>
</Reference>
<Reference URI="cid:msg400123456">
<DigestMethod Algorithm="https://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>WHi7LauQVMt1IfJ3fXqorKEnWFs=</DigestValue>
</Reference>
<Reference URI="cid:msg400123456">
<DigestMethod Algorithm="https://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>+3ZwEOqRmtGtrSfzhicq8lem0w4=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>Ay1DK.../OdTLc=</SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>MIID9...stEw</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
</SOAP:Header>
<SOAP:Body>
</SOAP:Body>
</SOAP:Envelope>