robrichards / xmlseclibs

A PHP library for XML Security
BSD 3-Clause "New" or "Revised" License
386 stars 180 forks source link

Signatures done with XMLSecurityDSig do not pass C# DOTNET validation #247

Open guycalledseven opened 1 year ago

guycalledseven commented 1 year ago

I have been successfully using xmlseclibs package up until one point. I had to sign xml and one specific c# endpoint had to validate it, and validation was failing. No matter my signed xml document passed internal xmlseclibs validation, even validation in java endpoint and online http://tools.chilkat.io/xmlDsigVerify.cshtml web checker.

I've spent many hours on debugging this issue and finally found out that culprit for failing checks was a whitespace in signature template, namely:

    const template = '<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
  <ds:SignedInfo>
    <ds:SignatureMethod />
  </ds:SignedInfo>
</ds:Signature>';

    const BASE_TEMPLATE = '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
  <SignedInfo>
    <SignatureMethod />
  </SignedInfo>
</Signature>';

...

public function __construct($prefix='ds')
{
...
    $sigdoc = new DOMDocument();
    $sigdoc->loadXML($template);
    $this->sigNode = $sigdoc->documentElement;
...

If xml you are trying to sign xml that has whitespace = false, and this signature with whitespace is added - it freaks out DOTNETs System.Security.Cryptography.Xml.SignedXml; CheckSignature method if DOTNET side does not have whitespace or significantWhitespace properties defined (my working theory, maybe it's also connected with something how DOTNET handles EXC_C14N cannonization ):

using System.Security.Cryptography.X509Certificates;
using static System.Security.Cryptography.Xml.SignedXml;
using System.Security.Cryptography.Xml;
...
            // Load the signature node into a new XML document
            XmlDocument signatureXmlDoc = new XmlDocument();
            signatureXmlDoc.LoadXml(nodeList[0].OuterXml);

            // Create a new instance of the XML signature object
            SignedXml signedXml = new SignedXml(xmlDoc);

            // Load the signature node into the signed XML object
            signedXml.LoadXml(signatureXmlDoc.DocumentElement);

            // Verify the signature
            return signedXml.CheckSignature(rsaKey);
...

So the solution is to strip whitespace out of everything before signing:

  1. xml you are trying to sign
  2. added signature node

Simplified example on how I signed this document with XMLSecurityDSig:

$xml = '<?xml version="1.0" encoding="utf-8"?><ServiceResponse xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><ServiceData></ServiceData><Signatures></Signatures></ServiceResponse>';

$xmlDocument = new DOMDocument('1.0', 'UTF-8');
$xmlDocument->formatOutput = false;
$xmlDocument->preserveWhiteSpace = false;
$xmlDocument->loadXML($xml);

$serviceresponse_node = $xmlDocument->getElementsByTagName("ServiceResponse")->item(0);
$objDSig = new XMLSecurityDSig('');
$objDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); // relaxed about SignedInfo

$objDSig->addReference(
    $serviceresponse_node,
    XMLSecurityDSig::SHA1, // 'http://www.w3.org/2000/09/xmldsig#sha1',
    ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#'],
    ['id_name' => 'Id', 'overwrite' => false],
);

$objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type'=>'private'));
$objKey->loadKey($this->privateCertPath, TRUE);

$objDSig->sign($objKey, $xmlDocument->documentElement);
$objDSig->add509Cert(file_get_contents($this->publicCertPath));

$signatures_node = $xmlDocument->getElementsByTagName("Signatures")->item(0);
$objDSig->appendSignature($signatures_node);
$signedXML = $xmlDocument->saveXML();

edit: php/c# examples