Reputation: 152
I am working to replace a legacy application that is no longer being maintained. I have most things replaced except the digital signature method. I have an implementation in .net core and I'm a bit confused as to why it's failing to verify it's own signed document.
Consider the following xml(code is messy, just running a poc at the moment):
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<tns:Body id="Body" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/">This is a test</tns:Body>
</soap:Envelope>
With the following c#:
var publicCert = new X509Certificate2(File.ReadAllBytes("public.cer"));
var cert = GetSigningCertificate("private.pfx", "super amazing password");
var xml = File.ReadAllText("test.xml");
var doc = new XmlDocument {PreserveWhitespace = false};
doc.LoadXml(xml);
//remove <?xml?> tag
if (doc.FirstChild is XmlDeclaration)
{
doc.RemoveChild(doc.FirstChild);
}
//create the header node: NOTE: This is not on the original document but required by the vendor
var header = new StringBuilder();
header.AppendLine("<SOAP:Header xmlns:SOAP=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-SEC=\"http://schemas.xmlsoap.org/soap/security/2000-12\">");
header.AppendLine("<SOAP-SEC:Signature>");
header.AppendLine("</SOAP-SEC:Signature>");
header.AppendLine("</SOAP:Header>");
var headerXml = new XmlDocument {PreserveWhitespace = false};
headerXml.LoadXml(header.ToString());
//add header document to the main document
if (headerXml.DocumentElement != null) doc.DocumentElement?.InsertBefore(doc.ImportNode(headerXml.DocumentElement, true), doc.DocumentElement?.FirstChild);
var domHeader = (XmlElement) doc.FirstChild.FirstChild;
// Create a SignedXml object.
var referenceUri = "#Body";
var signedXml = new SignedXml(domHeader) { SigningKey = cert.PrivateKey }; #NOTE: signing is done at the /SOAP:Header level per the vendor, not at the ParentDocument level
signedXml.SignedInfo.CanonicalizationMethod = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments";
signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
// Create a reference to be signed.
var reference = new Reference
{
Uri = referenceUri,
DigestMethod = "http://www.w3.org/2000/09/xmldsig#sha1"
};
// Add the reference to the SignedXml object.
signedXml.AddReference(reference);
//add key info
var keyInfo = new KeyInfo();
var certInfo = new KeyInfoX509Data();
if (cert.SerialNumber != null) certInfo.AddIssuerSerial(cert.Issuer, cert.SerialNumber);
keyInfo.AddClause(certInfo);
signedXml.KeyInfo = keyInfo;
// Compute the signature.
signedXml.ComputeSignature();
// Get the XML representation of the signature and save
// it to an XmlElement object.
var xmlDigitalSignature = signedXml.GetXml();
//get the SOAP-SEC header and add our signature to it
var soapSecurityList = doc.GetElementsByTagName("Signature", "http://schemas.xmlsoap.org/soap/security/2000-12");
if(soapSecurityList.Count == 0)
{
throw new Exception("Could not find SOAP-SEC header!");
}
var soapSecurity = soapSecurityList.Item(0);
soapSecurity.AppendChild(xmlDigitalSignature);
This produces the following xml(cert, sig,hash,serial are placeholders for the actual values and have been redacted for posting here):
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP:Header xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-SEC="http://schemas.xmlsoap.org/soap/security/2000-12">
<SOAP-SEC:Signature>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" />
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<Reference URI="#Body">
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>hash</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>sig</SignatureValue>
<KeyInfo>
<X509Data>
<X509IssuerSerial>
<X509IssuerName>cert</X509IssuerName>
<X509SerialNumber>serial</X509SerialNumber>
</X509IssuerSerial>
</X509Data>
</KeyInfo>
</Signature>
</SOAP-SEC:Signature>
</SOAP:Header>
<tns:Body id="Body" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/">This is a test</tns:Body>
</soap:Envelope>
However, trying to validate it fails with the following code:
//verify what we just did
var verifiedXml = new XmlDocument {PreserveWhitespace = false};
verifiedXml.LoadXml(doc.InnerXml); //document object from above
var signature = verifiedXml.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
var validSignature = new SignedXml((XmlElement) signature[0].ParentNode);
return validSignature.CheckSignature(cert, true); //also tried publicCert but no luck
Signature always fails. I have looked at numerous other stack overflow questions and most of them involve whitespace issues(all documents ignore white spaces and all sources are linearized before being read in) or adding "ds" namespace to the signature prefix during the canonicalization method. I implemented that "ds" fix and tried again. It produces the following xml, which still fails to validate.
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP:Header xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-SEC="http://schemas.xmlsoap.org/soap/security/2000-12">
<SOAP-SEC:Signature>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<ds:Reference URI="#Body">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<ds:DigestValue>hash</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>sig</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509IssuerSerial>
<ds:X509IssuerName>cert</ds:X509IssuerName>
<ds:X509SerialNumber>serial</ds:X509SerialNumber>
</ds:X509IssuerSerial>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
</SOAP-SEC:Signature>
</SOAP:Header>
<tns:Body id="Body" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/">This is a test</tns:Body>
</soap:Envelope>
UPDATE Based on comments. Tried the following:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<tns:Body id="Body" xmlns:tns="http://schemas.xmlsoap.org/soap/envelope/">This is a test</tns:Body>
</soap:Envelope>
using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Security.Policy;
namespace XmlDsig
{
public class XmlDsig
{
public bool Sign()
{
try
{
var cert = GetSigningCertificate("cert.pfx", "password");
var xml = File.ReadAllText("test.xml");
// Create a new XML document.
var doc = new XmlDocument {PreserveWhitespace = false};
doc.LoadXml(xml);
//remove <?xml?> tag
if (doc.FirstChild is XmlDeclaration)
{
doc.RemoveChild(doc.FirstChild);
}
//create the header node
var header = new StringBuilder();
header.AppendLine("<SOAP:Header xmlns:SOAP=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:SOAP-SEC=\"http://schemas.xmlsoap.org/soap/security/2000-12\">");
header.AppendLine("<SOAP-SEC:Signature>");
header.AppendLine("</SOAP-SEC:Signature>");
header.AppendLine("</SOAP:Header>");
var headerXml = new XmlDocument {PreserveWhitespace = false};
headerXml.LoadXml(header.ToString());
//add header document to the main document
if (headerXml.DocumentElement != null) doc.DocumentElement?.InsertBefore(doc.ImportNode(headerXml.DocumentElement, true), doc.DocumentElement?.FirstChild);
var domHeader = (XmlElement) doc.FirstChild.FirstChild;
SignXmlWithCertificate(domHeader, cert);
var validatorXml = new XmlDocument();
validatorXml.LoadXml(doc.OuterXml);
var sigContext = validatorXml.GetElementsByTagName("Header", "http://schemas.xmlsoap.org/soap/envelope/");
var validator = new SignedXml((XmlElement) sigContext[0]); return validator.CheckSignature(cert, true);
}
catch (Exception e)
{
return false;
// return e.ToString();
}
}
public static void SignXmlWithCertificate(XmlElement assertion, X509Certificate2 cert)
{
var signedXml = new SignedXml(assertion) {SigningKey = cert.PrivateKey};
var reference = new Reference {Uri = "#Body"};
reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
signedXml.AddReference(reference);
var keyInfo = new KeyInfo();
keyInfo.AddClause(new KeyInfoX509Data(cert));
signedXml.KeyInfo = keyInfo;
signedXml.ComputeSignature();
var xmlsig = signedXml.GetXml();
assertion.FirstChild.AppendChild(xmlsig);
}
}
}
Still fails to validate.
Update 2 Now I am getting the following error when i call CheckSignature:
SignatureDescription could not be created for the signature algorithm supplied.
Seems like this is a common problem on .net 4 with sha256, but I'm using sha1 in my current example and i am using dotnet core. I have tried sha256 too but same issue persists.
Upvotes: 2
Views: 2936
Reputation: 152
Found the answers I was looking for.
var verify_xml = new XmlDocument();
verify_xml.LoadXml(doc.OuterXml); //doc = the signed document loaded from disk
var signature = verify_xml.GetElementsByTagName("Signature", "http://www.w3.org/2000/09/xmldsig#");
var verify = new SignedXml((XmlElement)verify_xml.DocumentElement.FirstChild);
verify.LoadXml((XmlElement) signature[0]);
verify.CheckSignature(cert, true);
The Element passed to the SignedXml() constructor is the signing context(so the SOAP:Header element). LoadXml() is where you actually load the signature itself(not in the constructor). Fixing this resolved all my issues.
Upvotes: 1