phpxmldomdocumentxml-signaturexmlseclibs

XML signature differences between HEREDOC and DOMDocument


An API client I have developed works with XML messages and the messages are signed according to the XML Signature Syntax and Processing specification. After a long struggle, I finally got the signatures working.

At this moment I am building the XML with HEREDOC (simply php strings) and with a cleanup, I'd like to create the XML with DOMDocument directly. However, this causes the message to be invalidated by the server.

This is the current setup (server accepts this message when signed):

$xml = <<<EOT
<?xml version="1.0" encoding="UTF-8"?>
<DirectoryReq xmlns="http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1" version="3.3.1">
  <createDateTimestamp>$timestamp</createDateTimestamp>
  <Merchant>
    <merchantID>$merchantId</merchantID>
    <subID>$subId</subID>
  </Merchant>
</DirectoryReq>
EOT;

$document = new DOMDocument();
$document->loadXML($xml);

This is the OO approach (server rejects this message when signed):

$document = new DOMDocument('1.0', 'UTF-8');
$request  = $document->createElement('DirectoryReq');

$xmlns = $document->createAttribute('xmlns');
$xmlns->value = 'http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1';

$version = $document->createAttribute('version');
$version->value = '3.3.1';

$request->appendChild($xmlns);
$request->appendChild($version);

$merchant = $document->createElement('Merchant');
$merchant->appendChild($document->createElement('merchantID', $merchantId));
$merchant->appendChild($document->createElement('subID', $subId));

$request->appendChild($document->createElement('createDateTimestamp', $timestamp));
$request->appendChild($merchant);

$document->appendChild($request);

What can cause the difference as such the XML signature is invalidated by the server? The code to sign the message is exactly the same. The server is simply reporting "Invalid electronic signature".

If required I can show more code.

EDIT, more output and comparison of XML generated

To give some more information, this is the output of the first (HEREDOC) xml, generated via $document->saveXml():

<?xml version="1.0" encoding="UTF-8"?>
<DirectoryReq xmlns="http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1" version="3.3.1">
  <createDateTimestamp>2013-08-10T19:41:20.000Z</createDateTimestamp>
  <Merchant>
    <merchantID>0020XXXXXX</merchantID>
    <subID>0</subID>
  </Merchant>
</DirectoryReq>

This is the output ($document->saveXML()) for the second (direct DOMDocument generation) method:

<?xml version="1.0" encoding="UTF-8"?>
<DirectoryReq xmlns="http://www.idealdesk.com/ideal/messages/mer-acq/3.3.1" version="3.3.1">
  <createDateTimestamp>2013-08-10T19:41:20.000Z</createDateTimestamp>
  <Merchant>
    <merchantID>0020XXXXXX</merchantID>
    <subID>0</subID>
  </Merchant>
</DirectoryReq>

In php, var_dump() gives the exact same string length. If I compare both strings (=== obviously) they are the same. Comparing both objects, then they are not the same.

Signing example

Signing occurs with the library xmlseclibs with this code (NB. both types are signed the same way!):

public function sign(DOMDocument $document, $fingerprint, $keyfile, $passphrase = null)
{
    $dsig = new XMLSecurityDSig();
    $dsig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
    $dsig->addReference($document, XMLSecurityDSig::SHA256,
        array('http://www.w3.org/2000/09/xmldsig#enveloped-signature'),
        array('force_uri' => true)
    );

    $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'private'));
    if ($passphrase !== null) {
        $key->passphrase = $passphrase;
    }

    $key->loadKey($keyfile, true);
    $dsig->sign($key);

    $dsig->addKeyInfoAndName($fingerprint);
    $dsig->appendSignature($document->documentElement);
}

If I dump the XML after it's signed, the <DigestValue> and <SignatureValue> values are different. So the server is correct the signature is invalid, but I cannot come up with a clue why method A works and B not.


Solution

  • I have now solved it by exporting and importing the XML again. It's quite ugly, but allows me to flexibly handle the DOMNodes.

    protected function repairDOMDocument(DOMDocument $document)
    {
        $xml = $document->saveXML();
    
        $document = new DOMDocument;
        $document->loadXML($xml);
    
        return $document;
    }
    

    If there is a suggestion how to stop doing this, I am pleased to hear so.