This is a partial solution, based on mentioned Rick Stahl's article. Let's suppose we have that XML document to sign:
<?xml version="1.0" encoding="UTF-8"?>
<SOAP:Envelope xmlns:r1="http://www.routeone.com/namespace.messaging.diag#"
xmlns:star="http://www.starstandards.org/STAR"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:oa="http://www.openapplications.org/oagis">
<SOAP:Body>
<!-- data to be signed here -->
</SOAP:Body>
</SOAP:Envelope>
With that code:
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Xml;
namespace XmlSigner
{
class Program
{
static void Main(string[] args)
{
// Create a new XML document.
var doc = new XmlDocument();
// Format the document to ignore white spaces.
doc.PreserveWhitespace = false;
// Load the passed XML file using it's name.
doc.Load(new XmlTextReader("C:\\Repos\\request.xml"));
// Initialize the certificate.
var certificate = new X509Certificate2("C:\\Repos\\certificate.pfx", "youdontguessthispassword");
// Sign the document
var signedDoc = SignSoapBody(doc, certificate);
// Write to the output file.
File.WriteAllText("C:\\Repos\\signed.xml", signedDoc.OuterXml);
// Validate the result.
if (ValidateSoapBodySignature(signedDoc, certificate))
Console.WriteLine("Everything looks good, keep going.");
else
Console.WriteLine("You've screwed, back to work.");
}
private const string STR_SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/";
private const string STR_SOAPSEC_NS = "http://schemas.xmlsoap.org/soap/security/2000-12";
/// <summary>
/// Signs the SOAP document and adds a digital signature to it.
///
/// Note a lot of optional settings are applied against
/// key and certificate info to match the required XML document
/// structure the server requests.
/// </summary>
/// <param name="xmlDoc"></param>
/// <param name="certFriendlyName">Friendly Name of Cert installed in the Certificate Store under CurrentUser | Personal</param>
/// <returns></returns>
static public XmlDocument SignSoapBody(XmlDocument xmlDoc, X509Certificate2 cert)
{
// Add search Namespaces references to ensure we can reliably work
// against any SOAP docs regardless of tag naming
XmlNamespaceManager ns = new XmlNamespaceManager(xmlDoc.NameTable);
ns.AddNamespace("SOAP", STR_SOAP_NS);
ns.AddNamespace("SOAP-SEC", STR_SOAPSEC_NS);
// Grab the body element - this is what we create the signature from
XmlElement body = xmlDoc.DocumentElement.SelectSingleNode(@"//SOAP:Body", ns) as XmlElement;
if (body == null)
throw new ApplicationException("No body tag found");
// We'll only encode the <SOAP:Body> - add id: Reference as #Body
var bodyId = $"id-{Guid.NewGuid()}";
body.SetAttribute("id", bodyId);
// Signed XML will create Xml Signature - Xml fragment
SignedXml signedXml = new SignedXml(xmlDoc);
signedXml.Signature.Id = $"sid-{Guid.NewGuid()}";
signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
// Create a KeyInfo structure
KeyInfo keyInfo = new KeyInfo();
keyInfo.Id = $"kid-{Guid.NewGuid()}";
// The actual key for signing - MAKE SURE THIS ISN'T NULL!
signedXml.SigningKey = cert.PrivateKey;
// Specifically use the issuer and serial number for the data rather than the default
KeyInfoX509Data keyInfoData = new KeyInfoX509Data();
keyInfoData.AddIssuerSerial(cert.Issuer, cert.GetSerialNumberString());
keyInfo.AddClause(keyInfoData);
// provide the certficate info that gets embedded - note this is only
// for specific formatting of the message to provide the cert info
signedXml.KeyInfo = keyInfo;
// Again unusual - meant to make the document match template
signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
// Now create reference to sign: Point at the Body element
Reference reference = new Reference();
reference.DigestMethod = "http://www.w3.org/2000/09/xmldsig#sha1";
reference.Uri = $"#{bodyId}"; // reference id=body section in same doc
reference.AddTransform(new XmlDsigExcC14NTransform()); // required to match doc
signedXml.AddReference(reference);
// Finally create the signature
signedXml.ComputeSignature();
// Result is an XML node with the signature detail below it
// Now let's add the sucker into the SOAP-HEADER
XmlElement signedElement = signedXml.GetXml();
// Create SOAP-SEC:Signature element
XmlElement soapSignature = xmlDoc.CreateElement("Security", STR_SOAPSEC_NS);
soapSignature.Prefix = "wsse";
//soapSignature.SetAttribute("MustUnderstand", "", "1");
// And add our signature as content
soapSignature.AppendChild(signedElement);
// Now add the signature header into the master header
XmlElement soapHeader = xmlDoc.DocumentElement.SelectSingleNode("//SOAP:Header", ns) as XmlElement;
if (soapHeader == null)
{
soapHeader = xmlDoc.CreateElement("Header", STR_SOAP_NS);
soapHeader.Prefix = "SOAP";
xmlDoc.DocumentElement.InsertBefore(soapHeader, xmlDoc.DocumentElement.ChildNodes[0]);
}
soapHeader.AppendChild(soapSignature);
return xmlDoc;
}
/// <summary>
/// Validates the Xml Signature in a document.
///
/// This routine is significantly simpler because the key parameters
/// are embedded into the signature itself. All that's needed is a
/// certificate to provide the key - the rest can be read from the
/// Signature itself.
/// </summary>
/// <param name="doc"></param>
/// <param name="publicCertFileName"></param>
/// <returns></returns>
static public bool ValidateSoapBodySignature(XmlDocument doc, X509Certificate2 cert)
{
// Load the doc this time
SignedXml sdoc = new SignedXml(doc);
// Find the signature and load it into SignedXml
XmlNodeList nodeList = doc.GetElementsByTagName("Signature");
sdoc.LoadXml((XmlElement)nodeList[0]);
// Now read the actual signature and validate
bool result = sdoc.CheckSignature(cert, true);
return result;
}
}
}
I have this result:
<?xml version="1.0" encoding="UTF-8"?>
<SOAP:Envelope xmlns:r1="http://www.routeone.com/namespace.messaging.diag#" xmlns:star="http://www.starstandards.org/STAR" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" xmlns:oa="http://www.openapplications.org/oagis">
<SOAP:Header>
<wsse:Security xmlns:wsse="http://schemas.xmlsoap.org/soap/security/2000-12">
<Signature Id="sid-f3d74053-b47a-4740-b105-74ba1a550d38" xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<Reference URI="#id-9718651b-c7a4-47a0-bcd1-f89e1a5f8395">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>SOME_SECRET_VALUES</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>SOME_SECRET_VALUES</SignatureValue>
<KeyInfo Id="kid-626cac63-9f43-46a9-80e3-ed350ef6cea2">
<X509Data>
<X509IssuerSerial>
<X509IssuerName>CN=XX, OU=XX, O=XX, L=XX, C=XX</X509IssuerName>
<X509SerialNumber>SOME_SECRET_VALUES</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</KeyInfo>
</Signature>
</wsse:Security>
</SOAP:Header>
<SOAP:Body id="id-9718651b-c7a4-47a0-bcd1-f89e1a5f8395"> <!-- data to be signed here -->
</SOAP:Body>
</SOAP:Envelope>