Last week I posted a detailed entry in regards to digitally signing an XML document and then also validating the document, using certficiates both internally generated and using a public CA signed key certificate. I've been working with a customer on a project where sending signed documents back and forth as part of a custom messaging interface is necessary and getting the signature creation and validation was the first and presumably most important step.
Unfortunately as soon as we started exchanging messages with the server we found out that even though my local test code checked messages for validity before sending that the remote machine was not able to validate the signature. Conversely sample documents sent from the service vendor for us to validate with their public certificate also failed for validate for us - the signatures failed to validate.
This has been a frustrating experience, since our end of the code validates just fine and round trips from signing through validation, why would the end consumer fail with that very same data? We had a bunch of back and forth with the vendor that was more or less grasping at straws (both ends <g>). One of the comments in the last post from Oleg pointed me in the right direction by chance.
It turns out the problem is related to XmlDocument parser settings that are differing between our implementation and the service provider's. Specifically the problem was the PreserveWhitespace property of XmlDocument which determines how XML treats white space in the XML document. Usually this setting is not really critical because XML internally doesn't consider white space as part of the payload content, but when it comes to the signature, this property setting makes all the the difference in the world, because it determines whether the signature includes white space in the document.
Essentially, we had to ensure that all XML documents that are involved with signatures are loaded and saved with PreserveWhiteSpace=true explicitly to force the white space treatment correctly to the same format the vendor was using. FWIW, this has bitten me before in other situations and it's quite common when dealing with non-.NET XML solutions for PreserveWhitespace to be the default.
Here's what the signature and validation routines look like now:
[TestMethod()]
public void GetSignatureTest()
{
RouteOneProcessor sign = new RouteOneProcessor();
string certName = App.Configuration.CertificateName;
// *** Retrieve a certificate by Subject - Tidewater Finance Company
X509Certificate2 cert = sign.GetCertificateBySubject(App.Configuration.CertificateName,
StoreName.My, StoreLocation.LocalMachine);
// *** XmlDoc is input - this would be our input from POST data - here from file
XmlDocument doc = new XmlDocument();
doc.PreserveWhitespace = true;
doc.Load(STR_PROJECT_PATH + STR_InputDocument);
// *** Sign the XmlDocument and return a new XmlDocument
XmlDocument xmlDoc = sign.SignSoapBody(doc, cert);
Assert.IsNotNull(xmlDoc, "XmlDocument null when returned.");
// *** Check if the signature got added
XmlElement element = xmlDoc.GetElementsByTagName("Signature")[0] as XmlElement;
// *** Write out the document for testing so we can see structure
xmlDoc.Save(STR_PROJECT_PATH + STR_OutputDocument);
Assert.IsNotNull(element, "Signature element missing in result.");
}
/// <summary>
///A test for ValidateXmlSignature and verifying with a certificate store certificate
///</summary>
[TestMethod()]
public void ValidateXmlSignatureTest()
{
RouteOneProcessor sign = new RouteOneProcessor();
// *** Load signed output document from file
XmlDocument doc = new XmlDocument();
doc.PreserveWhitespace = true;
doc.Load(STR_PROJECT_PATH + STR_OutputDocument);
// *** Get cert by subject again
X509Certificate2 cert = sign.GetCertificateBySubject(App.Configuration.CertificateName,
StoreName.My, StoreLocation.LocalMachine);
// *** And validate the signature
Assert.IsTrue(sign.ValidateSoapBodySignature(doc, cert), "Xml Signature failed.");
}
It's important to note that the flag has to be set everywhere in the process - before loading any XML using Load or LoadXml and before saving. If you skip the step somewhere along the way and write out the document without the flag, then read it with the flag again the signature will fail to validate. IOW, the PreserveWhitespace setting must match when writing the signature and validating it.
This is one of those simple, but subtle things that are not easily discoverable and that can trip you up for a day or two. Hopefully this helps someone out and saves you that time...
Other Posts you might also like