Recieved a question per Mail about my problem today, so I have posted parts of my experimental code here: http://closetnutsnbolts.wordpress.com/2009/03/16/infopathsigning/
Years ago I've been looking for a solution for creating signed XML documents, which could be used and their signatures verified using Microsoft Office Infopath. And of course vice versa, to verify the InfoPath signatures using the .NET XML and cryprography services. Today I've been asked about my findings, so I'm posting my experiments here, perhaps it could save someone some searching and sleepless nights.
Happy experimenting!
Now to the code. First problem is, that SignedXml class did not find the signature in the InfoPath document. We needed to extend the SignedXml, overriding the GetIdElement function. Like this:
public class KdsSignedXml : SignedXml
{
public KdsSignedXml() : base() { }
public KdsSignedXml(XmlDocument document) : base(document) { }
public KdsSignedXml(XmlElement elem) : base(elem) { }
///
/// Searches for element using id in idValue and extended the search into DataObjects added to SignedXml class
///
/// document to search
/// id to search for in document and in data objects
/// XmlElement if found, null if the element with this id is not found
public override XmlElement GetIdElement(XmlDocument document, string idValue)
{
XmlElement retVal = base.GetIdElement(document, idValue);
if (retVal != null)
return retVal;
foreach (DataObject containedDocument in m_signature.ObjectList)
{
retVal = GetIdElement(containedDocument.Data, idValue);
if (retVal != null)
return retVal;
}
return null;
}
private XmlElement GetIdElement(XmlNodeList data, string idValue)
{
if (data == null)
return null;
foreach (XmlNode node in data)
{
XmlElement element1 = GetIdElement(node, idValue);
if (element1 != null)
return element1;
}
return null;
}
private XmlElement GetIdElement(XmlNode node, string idValue)
{
if (node == null)
return null;
XmlElement element1 = node.SelectSingleNode("//*[@Id=\"" + idValue + "\"]") as XmlElement;
if (element1 != null)
{
return element1;
}
element1 = node.SelectSingleNode("//*[@id=\"" + idValue + "\"]") as XmlElement;
if (element1 != null)
{
return element1;
}
return (node.SelectSingleNode("//*[@ID=\"" + idValue + "\"]") as XmlElement);
}
}
After that it should be easy. I have treated InfoPath as a black box, just trying to produce a document exactly in a form InfoPath itself would produce for my specific schema and my specific case. I did not need to look for a generic solution so I did not even try to understand the InfoPath internals to produce a generic solution. So if you are looking for that kind of recipes, there is still a lot of work ahead, digging through the manifest.xsf.
In my experimental signing and verifying code, I have accomplished this like following:
public static void Sign(X509Certificate2 cert, XmlDocument doc, string signatureName, string signatureLocation, XmlElement sigProperties, params string[] references)
{
// preserve whitespace
doc.PreserveWhitespace = true;
// create the signature mgt. object
KdsSignedXml signedXml = new KdsSignedXml(doc);
// add the references
if (references.Length == 0)
{
AddReference(doc, signedXml, signatureLocation, "");
}
else
{
foreach (string s in references)
{
AddReference(doc, signedXml, signatureLocation, s);
}
}
// Add signature properties
AddNamespaces(signatureLocation, doc, sigProperties);
signedXml.AddObject(new DataObject("", "", "", sigProperties));
Reference reference = new Reference("#MyComment_" + signatureName + "_000");
reference.Type = "http://www.w3.org/2000/09/xmldsig#SignatureProperties";
signedXml.AddReference(reference);
// sign the document
KeyInfo keyInfo = new KeyInfo();
keyInfo.AddClause(new KeyInfoX509Data(cert));
signedXml.KeyInfo = keyInfo;
signedXml.SigningKey = cert.PrivateKey;
signedXml.Signature.Id = "MySignature_" + signatureName + "_000";
signedXml.ComputeSignature();
// append the signature
XmlElement xmlDigitalSignature = signedXml.GetXml();
XmlNamespaceManager docNamespaces = new XmlNamespaceManager(doc.NameTable);
GetNamespaces(docNamespaces, doc.DocumentElement);
XmlNode sigNode = doc.SelectSingleNode(signatureLocation, docNamespaces);
XmlNode signatures = doc.SelectSingleNode(signatureLocation, docNamespaces);
if (signatures == null)
signatures = doc.DocumentElement;
signatures.AppendChild(doc.ImportNode(xmlDigitalSignature, true));
}
public static Boolean Verify(XmlDocument doc, string certIssuerName, bool certVerify)
{
doc.PreserveWhitespace = true;
KdsSignedXml signedXml = new KdsSignedXml(doc);
// Find the "Signature" node and create a new
// XmlNodeList object.
XmlNodeList nodeList = doc.GetElementsByTagName("Signature");
bool retVal = true;
foreach (XmlElement e in nodeList)
{
signedXml.LoadXml((e));
XmlNodeList certificates =
e.GetElementsByTagName("X509Certificate");
X509Certificate2 cert =
CreateX509CertificateFromXMLElement(
(XmlElement)certificates[0]);
if (cert.IssuerName.Name != string.Empty && certIssuerName.CompareTo(cert.IssuerName.Name) != 0)
return false;
RSACryptoServiceProvider rsacsp =
(RSACryptoServiceProvider)cert.PublicKey.Key;
retVal = retVal && signedXml.CheckSignature(rsacsp);
if (certVerify)
retVal = retVal && cert.Verify();
}
// Check the signature and return the result.
return retVal;
}
In case you are wondering what some of the utility functions do, here is some experimental and really quick and dirty code used in my experiments.
private static X509Certificate2
CreateX509CertificateFromXMLElement(
System.Xml.XmlElement theCertElement)
{
// The text of the element should be a Base64 representation of
// the certificate, so load it as a string.
string base64CertificateData = theCertElement.InnerText;
// Remove any whitespace that may be cluttering up the data.
base64CertificateData = base64CertificateData.Replace("\n", "");
base64CertificateData = base64CertificateData.Replace("\r", "");
base64CertificateData = base64CertificateData.Replace("\f", "");
base64CertificateData = base64CertificateData.Replace("\t", "");
base64CertificateData = base64CertificateData.Replace(" ", "");
// Convert the data to a byte array
byte[] certificateData = System.Convert.FromBase64String(
base64CertificateData);
// Create a new X509Certificate from the data.
X509Certificate2 cert = new X509Certificate2(certificateData);
// Return the result.
return cert;
}
private static void AddReference(XmlDocument doc, SignedXml sigXml, string sigLoc, string refName)
{
Reference reference = new Reference(refName);
if (refName == String.Empty)
{
// Enveloped Signature Transform
XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();
reference.AddTransform(env);
string dataTransform = "<stylesheet xmlns=\"http://www.w3.org/1999/XSL/Transform\" version=\"1.0\"> <template match=\"/\"><element name=\"SignedData\" namespace=\"http://schemas.microsoft.com/office/infopath/2003/SignatureProperties\"><apply-templates select=\".\" mode=\"skip\"/></element></template><template match=\"@*|node()\" priority=\"100\" mode=\"skip\"><apply-templates select=\"@*|node()\" mode=\"skip\"/></template><template match=\""
+ GetElementXP(GetDocumentRoot(doc))
+ "\" priority=\"200\" mode=\"skip\"><apply-templates select=\".\" mode=\"copy\"/></template><template match=\"@*|node()\" priority=\"100\" mode=\"copy\"><copy><apply-templates select=\"@*|node()\" mode=\"copy\"/></copy></template><template match=\""
+ sigLoc + "/node()\" priority=\"200\" mode=\"copy\" /></stylesheet>";
Transform trnsip = CreateXsltTransform(dataTransform, doc, sigLoc);
reference.AddTransform(trnsip);
Transform trns = new XmlDsigC14NTransform();
reference.AddTransform(trns);
}
sigXml.AddReference(reference);
}
public static void Sign(X509Certificate2 cert, XmlDocument doc, string signatureName, string signatureLocation, string comment, params string[] references)
{
string importProperties = "<SignatureProperties xmlns=\"http://www.w3.org/2000/09/xmldsig#\"> <SignatureProperty Id=\"MyComment_"
+ signatureName
+ "_000\" Target=\"#MySignature_Data_000\"> <Comment xmlns=\"http://schemas.microsoft.com/office/infopath/2003/SignatureProperties\">"
+ comment
+ "</Comment></SignatureProperty></SignatureProperties>";
XmlDocument properties = new XmlDocument();
properties.PreserveWhitespace = true;
properties.Load(new System.IO.StringReader(importProperties));
Sign(cert, doc, signatureName, signatureLocation, properties.DocumentElement, references);
}
P.S. I hope wordpress did not eat any of the important " and \ and < and >