I apologize in advance for the rather lengthy block of code, but it's the smallest compilable example I could produce. I already omitted all error checking from the original code. I'm using Visual Studio 2012 and .NET 4.5, although this is nothing new to 4.5, it should work with any version.
I am trying to sign an XML documents' elements to protect them from tampering. I don't want to protect the whole document, but only certain elements. Maybe even different elements with different keys.
However, when I sign three example elements and try to verify them, the first one always verifies, the other two fail. To make it even worse, the first one even succeeds if I modify it after being signed. I have googled a lot, read a lot of tutorials and even asked a theoretical question here, but I don't have any clue what I'm doing wrong. Can anybody spot my mistake?
Note: I'd be more than happy to offer the same bounty that's on friday's question to anybody solving this.
The certificate was created by executing:
"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\makecert" -r -pe -n "CN=XMLDSIG_Test" -b 01/01/2013 -e 01/01/2014 -sky signing -ss my
The Test xml file is:
<?xml version="1.0" encoding="utf-8" ?>
<PackageRoot>
<Package>
<Changes >
<Change/>
</Changes>
</Package>
<Package>
<Changes>
<Change/>
<Change/>
</Changes>
</Package>
<Package>
<Changes>
<Change/>
<Change/>
<Change/>
</Changes>
</Package>
</PackageRoot>
The code to sign and verify:
namespace SOExample
{
using System;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Xml;
public static class Program
{
public static void Sign(this XmlElement element, X509Certificate2 certificate)
{
var identifier = Guid.NewGuid().ToString();
element.SetAttribute("Id", identifier);
var signedXml = new SignedXml(element) { SigningKey = certificate.PrivateKey };
var reference = new Reference("#" + identifier);
reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
signedXml.AddReference(reference);
signedXml.ComputeSignature();
var xmlDigitalSignature = signedXml.GetXml();
element.AppendChild(element.OwnerDocument.ImportNode(xmlDigitalSignature, true));
}
public static bool VerifySignature(this XmlElement element, X509Certificate2 certificate)
{
var signedXml = new SignedXml(element);
XmlNodeList nodeList = element.GetElementsByTagName("Signature");
if (nodeList.Count != 1) return false;
signedXml.LoadXml((XmlElement)nodeList[0]);
return signedXml.CheckSignature(certificate, true);
}
public static void Main()
{
var xmlDoc = new XmlDocument { PreserveWhitespace = true };
xmlDoc.Load("ExamplePackage.xml");
var certificate = GetCertificateBySubject("CN=XMLDSIG_Test");
foreach (XmlElement root in xmlDoc.GetElementsByTagName("PackageRoot"))
{
foreach (XmlElement package in root.GetElementsByTagName("Package"))
{
package.Sign(certificate);
}
}
xmlDoc.Save("test_signed.xml");
Console.WriteLine("XML file signed.");
Console.WriteLine("Press any key to verify");
Console.ReadLine();
var signedDoc = new XmlDocument();
signedDoc.Load("test_signed.xml");
foreach (XmlElement root in xmlDoc.GetElementsByTagName("PackageRoot"))
{
foreach (XmlElement package in root.GetElementsByTagName("Package"))
{
Console.Write("Verifying Package " + package.GetAttribute("Id"));
var success = package.VerifySignature(certificate);
Console.WriteLine(success ? " successful!" : " failed!");
}
}
Console.WriteLine("Done.");
Console.ReadLine();
}
private static X509Certificate2 GetCertificateBySubject(string certificateSubject)
{
var store = new X509Store("My", StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
foreach (X509Certificate2 c in store.Certificates)
{
if (c.Subject == certificateSubject)
{
store.Close();
return c;
}
}
store.Close();
return null;
}
}
}
You have a bug in your test code. The second foreach loops again over xmlDoc
instead of signedDoc
. Fixing this will change the outcome to fail for all nodes.
Why they fail I don't yet know.
I couldn't find out why they fail with your code but I found a way to make it work. The difference: All signatures are direct childs of the root element:
public static void Main()
{
// ...
var signedDoc = new XmlDocument { PreserveWhitespace = true };
signedDoc.Load("test_signed.xml");
foreach (XmlElement root in signedDoc.GetElementsByTagName("PackageRoot"))
{
foreach (XmlElement signature in root.GetElementsByTagName("Signature"))
{
var success = signature.VerifySignature(certificate);
Console.WriteLine(success ? " successful!" : " failed!");
}
}
Console.WriteLine("Done.");
Console.ReadLine();
}
public static void Sign(this XmlElement element, X509Certificate2 certificate)
{
var identifier = Guid.NewGuid().ToString("N");
element.SetAttribute("Id", identifier);
var signedXml = new SignedXml(element) { SigningKey = certificate.PrivateKey };
signedXml.AddReference(new Reference("#" + identifier));
signedXml.ComputeSignature();
var xmlDigitalSignature = signedXml.GetXml();
element.OwnerDocument.DocumentElement.AppendChild(
element.OwnerDocument.ImportNode(xmlDigitalSignature, true));
}
public static bool VerifySignature(this XmlElement element, X509Certificate2 certificate)
{
var signedXml = new SignedXml(element.OwnerDocument);
signedXml.LoadXml(element);
return signedXml.CheckSignature(certificate, true);
}
One important detail to notice: PreserveWhitespace
needs to be set to true
for signedDoc
, too.
来源:https://stackoverflow.com/questions/16517955/xml-signature-for-xmlelement-fails-to-verify