<?php

namespace RobRichards\XMLSecLibs;



use DOMDocument;

use DOMElement;

use DOMNode;

use DOMXPath;

use Exception;



/**

 * xmlseclibs.php

 *

 * Copyright (c) 2007-2016, Robert Richards <rrichards@cdatazone.org>.

 * All rights reserved.

 *

 * Redistribution and use in source and binary forms, with or without

 * modification, are permitted provided that the following conditions

 * are met:

 *

 *   * Redistributions of source code must retain the above copyright

 *     notice, this list of conditions and the following disclaimer.

 *

 *   * Redistributions in binary form must reproduce the above copyright

 *     notice, this list of conditions and the following disclaimer in

 *     the documentation and/or other materials provided with the

 *     distribution.

 *

 *   * Neither the name of Robert Richards nor the names of his

 *     contributors may be used to endorse or promote products derived

 *     from this software without specific prior written permission.

 *

 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS

 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT

 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS

 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE

 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,

 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,

 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER

 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT

 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN

 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE

 * POSSIBILITY OF SUCH DAMAGE.

 *

 * @author    Robert Richards <rrichards@cdatazone.org>

 * @copyright 2007-2016 Robert Richards <rrichards@cdatazone.org>

 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License

 */



class XMLSecurityDSig

{

    const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#';

    const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1';

    const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256';

    const SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384';

    const SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512';

    const RIPEMD160 = 'http://www.w3.org/2001/04/xmlenc#ripemd160';



    const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';

    const C14N_COMMENTS = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments';

    const EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#';

    const EXC_C14N_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments';



    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#" Id="LlamaPeSign">

  <SignedInfo>

    <SignatureMethod />

  </SignedInfo>

</Signature>';



    /** @var DOMElement|null */

    public $sigNode = null;



    /** @var array */

    public $idKeys = array();



    /** @var array */

    public $idNS = array();



    /** @var string|null */

    private $signedInfo = null;



    /** @var DomXPath|null */

    private $xPathCtx = null;



    /** @var string|null */

    private $canonicalMethod = null;



    /** @var string */

    private $prefix = '';



    /** @var string */

    private $searchpfx = 'secdsig';



    /**

     * This variable contains an associative array of validated nodes.

     * @var array|null

     */

    private $validatedNodes = null;



    /**

     * @param string $prefix

     */

    public function __construct($prefix='ds', $id = null)

    {

        $template = self::BASE_TEMPLATE;

        if (! empty($prefix)) {

            $this->prefix = $prefix.':';

            $search = array("<S", "</S", "xmlns=");

            $replace = array("<$prefix:S", "</$prefix:S", "xmlns:$prefix=");

            $template = str_replace($search, $replace, $template);

        }

		

		if(!empty($id)) {

			$template = str_replace('Id=""', 'Id="' . $id . '"', $template);

		}

		

        $sigdoc = new DOMDocument();

        $sigdoc->loadXML($template);

        $this->sigNode = $sigdoc->documentElement;

    }



    /**

     * Reset the XPathObj to null

     */

    private function resetXPathObj()

    {

        $this->xPathCtx = null;

    }



    /**

     * Returns the XPathObj or null if xPathCtx is set and sigNode is empty.

     *

     * @return DOMXPath|null

     */

    private function getXPathObj()

    {

        if (empty($this->xPathCtx) && ! empty($this->sigNode)) {

            $xpath = new DOMXPath($this->sigNode->ownerDocument);

            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);

            $this->xPathCtx = $xpath;

        }

        return $this->xPathCtx;

    }



    /**

     * Generate guid

     *

     * @param string $prefix Prefix to use for guid. defaults to pfx

     *

     * @return string The generated guid

     */

    public static function generateGUID($prefix='pfx')

    {

        $uuid = md5(uniqid(mt_rand(), true));

        $guid = $prefix.substr($uuid, 0, 8)."-".

                substr($uuid, 8, 4)."-".

                substr($uuid, 12, 4)."-".

                substr($uuid, 16, 4)."-".

                substr($uuid, 20, 12);

        return $guid;

    }



    /**

     * Generate guid

     *

     * @param string $prefix Prefix to use for guid. defaults to pfx

     *

     * @return string The generated guid

     *

     * @deprecated Method deprecated in Release 1.4.1

     */

    public static function generate_GUID($prefix='pfx')

    {

        return self::generateGUID($prefix);

    }



    /**

     * @param DOMDocument $objDoc

     * @param int $pos

     * @return DOMNode|null

     */

    public function locateSignature($objDoc, $pos=0)

    {

        if ($objDoc instanceof DOMDocument) {

            $doc = $objDoc;

        } else {

            $doc = $objDoc->ownerDocument;

        }

        if ($doc) {

            $xpath = new DOMXPath($doc);

            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);

            $query = ".//secdsig:Signature";

            $nodeset = $xpath->query($query, $objDoc);

            $this->sigNode = $nodeset->item($pos);

            return $this->sigNode;

        }

        return null;

    }



    /**

     * @param string $name

     * @param null|string $value

     * @return DOMElement

     */

    public function createNewSignNode($name, $value=null)

    {

        $doc = $this->sigNode->ownerDocument;

        if (! is_null($value)) {

            $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name, $value);

        } else {

            $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name);

        }

        return $node;

    }



    /**

     * @param string $method

     * @throws Exception

     */

    public function setCanonicalMethod($method)

    {

        switch ($method) {

            case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':

            case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':

            case 'http://www.w3.org/2001/10/xml-exc-c14n#':

            case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':

                $this->canonicalMethod = $method;

                break;

            default:

                throw new Exception('Invalid Canonical Method');

        }

        if ($xpath = $this->getXPathObj()) {

            $query = './'.$this->searchpfx.':SignedInfo';

            $nodeset = $xpath->query($query, $this->sigNode);

            if ($sinfo = $nodeset->item(0)) {

                $query = './'.$this->searchpfx.'CanonicalizationMethod';

                $nodeset = $xpath->query($query, $sinfo);

                if (! ($canonNode = $nodeset->item(0))) {

                    $canonNode = $this->createNewSignNode('CanonicalizationMethod');

                    $sinfo->insertBefore($canonNode, $sinfo->firstChild);

                }

                $canonNode->setAttribute('Algorithm', $this->canonicalMethod);

            }

        }

    }



    /**

     * @param DOMNode $node

     * @param string $canonicalmethod

     * @param null|array $arXPath

     * @param null|array $prefixList

     * @return string

     */

    private function canonicalizeData($node, $canonicalmethod, $arXPath=null, $prefixList=null)

    {

        $exclusive = false;

        $withComments = false;

        switch ($canonicalmethod) {

            case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':

                $exclusive = false;

                $withComments = false;

                break;

            case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':

                $withComments = true;

                break;

            case 'http://www.w3.org/2001/10/xml-exc-c14n#':

                $exclusive = true;

                break;

            case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':

                $exclusive = true;

                $withComments = true;

                break;

        }



        if (is_null($arXPath) && ($node instanceof DOMNode) && ($node->ownerDocument !== null) && $node->isSameNode($node->ownerDocument->documentElement)) {

            /* Check for any PI or comments as they would have been excluded */

            $element = $node;

            while ($refnode = $element->previousSibling) {

                if ($refnode->nodeType == XML_PI_NODE || (($refnode->nodeType == XML_COMMENT_NODE) && $withComments)) {

                    break;

                }

                $element = $refnode;

            }

            if ($refnode == null) {

                $node = $node->ownerDocument;

            }

        }



        return $node->C14N($exclusive, $withComments, $arXPath, $prefixList);

    }



    /**

     * @return null|string

     */

    public function canonicalizeSignedInfo()

    {



        $doc = $this->sigNode->ownerDocument;

        $canonicalmethod = null;

        if ($doc) {

            $xpath = $this->getXPathObj();

            $query = "./secdsig:SignedInfo";

            $nodeset = $xpath->query($query, $this->sigNode);

            if ($signInfoNode = $nodeset->item(0)) {

                $query = "./secdsig:CanonicalizationMethod";

                $nodeset = $xpath->query($query, $signInfoNode);

                if ($canonNode = $nodeset->item(0)) {

                    $canonicalmethod = $canonNode->getAttribute('Algorithm');

                }

                $this->signedInfo = $this->canonicalizeData($signInfoNode, $canonicalmethod);

                return $this->signedInfo;

            }

        }

        return null;

    }



    /**

     * @param string $digestAlgorithm

     * @param string $data

     * @param bool $encode

     * @return string

     * @throws Exception

     */

    public function calculateDigest($digestAlgorithm, $data, $encode = true)

    {

        switch ($digestAlgorithm) {

            case self::SHA1:

                $alg = 'sha1';

                break;

            case self::SHA256:

                $alg = 'sha256';

                break;

            case self::SHA384:

                $alg = 'sha384';

                break;

            case self::SHA512:

                $alg = 'sha512';

                break;

            case self::RIPEMD160:

                $alg = 'ripemd160';

                break;

            default:

                throw new Exception("Cannot validate digest: Unsupported Algorithm <$digestAlgorithm>");

        }



        $digest = hash($alg, $data, true);

        if ($encode) {

            $digest = base64_encode($digest);

        }

        return $digest;



    }



    /**

     * @param $refNode

     * @param string $data

     * @return bool

     */

    public function validateDigest($refNode, $data)

    {

        $xpath = new DOMXPath($refNode->ownerDocument);

        $xpath->registerNamespace('secdsig', self::XMLDSIGNS);

        $query = 'string(./secdsig:DigestMethod/@Algorithm)';

        $digestAlgorithm = $xpath->evaluate($query, $refNode);

        $digValue = $this->calculateDigest($digestAlgorithm, $data, false);

        $query = 'string(./secdsig:DigestValue)';

        $digestValue = $xpath->evaluate($query, $refNode);

        return ($digValue == base64_decode($digestValue));

    }



    /**

     * @param $refNode

     * @param DOMNode $objData

     * @param bool $includeCommentNodes

     * @return string

     */

    public function processTransforms($refNode, $objData, $includeCommentNodes = true)

    {

        $data = $objData;

        $xpath = new DOMXPath($refNode->ownerDocument);

        $xpath->registerNamespace('secdsig', self::XMLDSIGNS);

        $query = './secdsig:Transforms/secdsig:Transform';

        $nodelist = $xpath->query($query, $refNode);

        $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';

        $arXPath = null;

        $prefixList = null;

        foreach ($nodelist AS $transform) {

            $algorithm = $transform->getAttribute("Algorithm");

            switch ($algorithm) {

                case 'http://www.w3.org/2001/10/xml-exc-c14n#':

                case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':



                    if (!$includeCommentNodes) {

                        /* We remove comment nodes by forcing it to use a canonicalization

                         * without comments.

                         */

                        $canonicalMethod = 'http://www.w3.org/2001/10/xml-exc-c14n#';

                    } else {

                        $canonicalMethod = $algorithm;

                    }



                    $node = $transform->firstChild;

                    while ($node) {

                        if ($node->localName == 'InclusiveNamespaces') {

                            if ($pfx = $node->getAttribute('PrefixList')) {

                                $arpfx = array();

                                $pfxlist = explode(" ", $pfx);

                                foreach ($pfxlist AS $pfx) {

                                    $val = trim($pfx);

                                    if (! empty($val)) {

                                        $arpfx[] = $val;

                                    }

                                }

                                if (count($arpfx) > 0) {

                                    $prefixList = $arpfx;

                                }

                            }

                            break;

                        }

                        $node = $node->nextSibling;

                    }

            break;

                case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':

                case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':

                    if (!$includeCommentNodes) {

                        /* We remove comment nodes by forcing it to use a canonicalization

                         * without comments.

                         */

                        $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';

                    } else {

                        $canonicalMethod = $algorithm;

                    }



                    break;

                case 'http://www.w3.org/TR/1999/REC-xpath-19991116':

                    $node = $transform->firstChild;

                    while ($node) {

                        if ($node->localName == 'XPath') {

                            $arXPath = array();

                            $arXPath['query'] = '(.//. | .//@* | .//namespace::*)['.$node->nodeValue.']';

                            $arXpath['namespaces'] = array();

                            $nslist = $xpath->query('./namespace::*', $node);

                            foreach ($nslist AS $nsnode) {

                                if ($nsnode->localName != "xml") {

                                    $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue;

                                }

                            }

                            break;

                        }

                        $node = $node->nextSibling;

                    }

                    break;

            }

        }

        if ($data instanceof DOMNode) {

            $data = $this->canonicalizeData($objData, $canonicalMethod, $arXPath, $prefixList);

        }

        return $data;

    }



    /**

     * @param DOMNode $refNode

     * @return bool

     */

    public function processRefNode($refNode)

    {

        $dataObject = null;



        /*

         * Depending on the URI, we may not want to include comments in the result

         * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel

         */

        $includeCommentNodes = true;



        if ($uri = $refNode->getAttribute("URI")) {

            $arUrl = parse_url($uri);

            if (empty($arUrl['path'])) {

                if ($identifier = $arUrl['fragment']) {



                    /* This reference identifies a node with the given id by using

                     * a URI on the form "#identifier". This should not include comments.

                     */

                    $includeCommentNodes = false;



                    $xPath = new DOMXPath($refNode->ownerDocument);

                    if ($this->idNS && is_array($this->idNS)) {

                        foreach ($this->idNS AS $nspf => $ns) {

                            $xPath->registerNamespace($nspf, $ns);

                        }

                    }

                    $iDlist = '@Id="'.$identifier.'"';

                    if (is_array($this->idKeys)) {

                        foreach ($this->idKeys AS $idKey) {

                            $iDlist .= " or @$idKey='$identifier'";

                        }

                    }

                    $query = '//*['.$iDlist.']';

                    $dataObject = $xPath->query($query)->item(0);

                } else {

                    $dataObject = $refNode->ownerDocument;

                }

            } else {

                $dataObject = file_get_contents($arUrl);

            }

        } else {

            /* This reference identifies the root node with an empty URI. This should

             * not include comments.

             */

            $includeCommentNodes = false;



            $dataObject = $refNode->ownerDocument;

        }

        $data = $this->processTransforms($refNode, $dataObject, $includeCommentNodes);

        if (!$this->validateDigest($refNode, $data)) {

            return false;

        }



        if ($dataObject instanceof DOMNode) {

            /* Add this node to the list of validated nodes. */

            if (! empty($identifier)) {

                $this->validatedNodes[$identifier] = $dataObject;

            } else {

                $this->validatedNodes[] = $dataObject;

            }

        }



        return true;

    }



    /**

     * @param DOMNode $refNode

     * @return null

     */

    public function getRefNodeID($refNode)

    {

        if ($uri = $refNode->getAttribute("URI")) {

            $arUrl = parse_url($uri);

            if (empty($arUrl['path'])) {

                if ($identifier = $arUrl['fragment']) {

                    return $identifier;

                }

            }

        }

        return null;

    }



    /**

     * @return array

     * @throws Exception

     */

    public function getRefIDs()

    {

        $refids = array();



        $xpath = $this->getXPathObj();

        $query = "./secdsig:SignedInfo/secdsig:Reference";

        $nodeset = $xpath->query($query, $this->sigNode);

        if ($nodeset->length == 0) {

            throw new Exception("Reference nodes not found");

        }

        foreach ($nodeset AS $refNode) {

            $refids[] = $this->getRefNodeID($refNode);

        }

        return $refids;

    }



    /**

     * @return bool

     * @throws Exception

     */

    public function validateReference()

    {

        $docElem = $this->sigNode->ownerDocument->documentElement;

        if (! $docElem->isSameNode($this->sigNode)) {

            if ($this->sigNode->parentNode != null) {

                $this->sigNode->parentNode->removeChild($this->sigNode);

            }

        }

        $xpath = $this->getXPathObj();

        $query = "./secdsig:SignedInfo/secdsig:Reference";

        $nodeset = $xpath->query($query, $this->sigNode);

        if ($nodeset->length == 0) {

            throw new Exception("Reference nodes not found");

        }



        /* Initialize/reset the list of validated nodes. */

        $this->validatedNodes = array();



        foreach ($nodeset AS $refNode) {

            if (! $this->processRefNode($refNode)) {

                /* Clear the list of validated nodes. */

                $this->validatedNodes = null;

                throw new Exception("Reference validation failed");

            }

        }

        return true;

    }



    /**

     * @param DOMNode $sinfoNode

     * @param DOMDocument $node

     * @param string $algorithm

     * @param null|array $arTransforms

     * @param null|array $options

     */

    private function addRefInternal($sinfoNode, $node, $algorithm, $arTransforms=null, $options=null)

    {

        $prefix = null;

        $prefix_ns = null;

        $id_name = 'Id';

        $overwrite_id  = true;

        $force_uri = false;



        if (is_array($options)) {

            $prefix = empty($options['prefix']) ? null : $options['prefix'];

            $prefix_ns = empty($options['prefix_ns']) ? null : $options['prefix_ns'];

            $id_name = empty($options['id_name']) ? 'Id' : $options['id_name'];

            $overwrite_id = !isset($options['overwrite']) ? true : (bool) $options['overwrite'];

            $force_uri = !isset($options['force_uri']) ? false : (bool) $options['force_uri'];

        }



        $attname = $id_name;

        if (! empty($prefix)) {

            $attname = $prefix.':'.$attname;

        }



        $refNode = $this->createNewSignNode('Reference');

        $sinfoNode->appendChild($refNode);



        if (! $node instanceof DOMDocument) {

            $uri = null;

            if (! $overwrite_id) {

                $uri = $prefix_ns ? $node->getAttributeNS($prefix_ns, $id_name) : $node->getAttribute($id_name);

            }

            if (empty($uri)) {

                $uri = self::generateGUID();

                $node->setAttributeNS($prefix_ns, $attname, $uri);

            }

            $refNode->setAttribute("URI", '#'.$uri);

        } elseif ($force_uri) {

            $refNode->setAttribute("URI", '');

        }



        $transNodes = $this->createNewSignNode('Transforms');

        $refNode->appendChild($transNodes);



        if (is_array($arTransforms)) {

            foreach ($arTransforms AS $transform) {

                $transNode = $this->createNewSignNode('Transform');

                $transNodes->appendChild($transNode);

                if (is_array($transform) &&

                    (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116'])) &&

                    (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']))) {

                    $transNode->setAttribute('Algorithm', 'http://www.w3.org/TR/1999/REC-xpath-19991116');

                    $XPathNode = $this->createNewSignNode('XPath', $transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']);

                    $transNode->appendChild($XPathNode);

                    if (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'])) {

                        foreach ($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'] AS $prefix => $namespace) {

                            $XPathNode->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:$prefix", $namespace);

                        }

                    }

                } else {

                    $transNode->setAttribute('Algorithm', $transform);

                }

            }

        } elseif (! empty($this->canonicalMethod)) {

            $transNode = $this->createNewSignNode('Transform');

            $transNodes->appendChild($transNode);

            $transNode->setAttribute('Algorithm', $this->canonicalMethod);

        }



        $canonicalData = $this->processTransforms($refNode, $node);

        $digValue = $this->calculateDigest($algorithm, $canonicalData);



        $digestMethod = $this->createNewSignNode('DigestMethod');

        $refNode->appendChild($digestMethod);

        $digestMethod->setAttribute('Algorithm', $algorithm);



        $digestValue = $this->createNewSignNode('DigestValue', $digValue);

        $refNode->appendChild($digestValue);

    }



    /**

     * @param DOMDocument $node

     * @param string $algorithm

     * @param null|array $arTransforms

     * @param null|array $options

     */

    public function addReference($node, $algorithm, $arTransforms=null, $options=null)

    {

        if ($xpath = $this->getXPathObj()) {

            $query = "./secdsig:SignedInfo";

            $nodeset = $xpath->query($query, $this->sigNode);

            if ($sInfo = $nodeset->item(0)) {

                $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options);

            }

        }

    }



    /**

     * @param array $arNodes

     * @param string $algorithm

     * @param null|array $arTransforms

     * @param null|array $options

     */

    public function addReferenceList($arNodes, $algorithm, $arTransforms=null, $options=null)

    {

        if ($xpath = $this->getXPathObj()) {

            $query = "./secdsig:SignedInfo";

            $nodeset = $xpath->query($query, $this->sigNode);

            if ($sInfo = $nodeset->item(0)) {

                foreach ($arNodes AS $node) {

                    $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options);

                }

            }

        }

    }



    /**

     * @param DOMElement|string $data

     * @param null|string $mimetype

     * @param null|string $encoding

     * @return DOMElement

     */

    public function addObject($data, $mimetype=null, $encoding=null)

    {

        $objNode = $this->createNewSignNode('Object');

        $this->sigNode->appendChild($objNode);

        if (! empty($mimetype)) {

            $objNode->setAttribute('MimeType', $mimetype);

        }

        if (! empty($encoding)) {

            $objNode->setAttribute('Encoding', $encoding);

        }



        if ($data instanceof DOMElement) {

            $newData = $this->sigNode->ownerDocument->importNode($data, true);

        } else {

            $newData = $this->sigNode->ownerDocument->createTextNode($data);

        }

        $objNode->appendChild($newData);



        return $objNode;

    }



    /**

     * @param null|DOMNode $node

     * @return null|XMLSecurityKey

     */

    public function locateKey($node=null)

    {

        if (empty($node)) {

            $node = $this->sigNode;

        }

        if (! $node instanceof DOMNode) {

            return null;

        }

        if ($doc = $node->ownerDocument) {

            $xpath = new DOMXPath($doc);

            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);

            $query = "string(./secdsig:SignedInfo/secdsig:SignatureMethod/@Algorithm)";

            $algorithm = $xpath->evaluate($query, $node);

            if ($algorithm) {

                try {

                    $objKey = new XMLSecurityKey($algorithm, array('type' => 'public'));

                } catch (Exception $e) {

                    return null;

                }

                return $objKey;

            }

        }

        return null;

    }



    /**

     * @param XMLSecurityKey $objKey

     * @return bool|int

     * @throws Exception

     */

    public function verify($objKey)

    {

        $doc = $this->sigNode->ownerDocument;

        $xpath = new DOMXPath($doc);

        $xpath->registerNamespace('secdsig', self::XMLDSIGNS);

        $query = "string(./secdsig:SignatureValue)";

        $sigValue = $xpath->evaluate($query, $this->sigNode);

        if (empty($sigValue)) {

            throw new Exception("Unable to locate SignatureValue");

        }

        return $objKey->verifySignature($this->signedInfo, base64_decode($sigValue));

    }



    /**

     * @param XMLSecurityKey $objKey

     * @param string $data

     * @return mixed|string

     */

    public function signData($objKey, $data)

    {

        return $objKey->signData($data);

    }



    /**

     * @param XMLSecurityKey $objKey

     * @param null|DOMNode $appendToNode

     */

    public function sign($objKey, $appendToNode = null)

    {

        // If we have a parent node append it now so C14N properly works

        if ($appendToNode != null) {

            $this->resetXPathObj();

            $this->appendSignature($appendToNode);

            $this->sigNode = $appendToNode->lastChild;

        }

        if ($xpath = $this->getXPathObj()) {

            $query = "./secdsig:SignedInfo";

            $nodeset = $xpath->query($query, $this->sigNode);

            if ($sInfo = $nodeset->item(0)) {

                $query = "./secdsig:SignatureMethod";

                $nodeset = $xpath->query($query, $sInfo);

                $sMethod = $nodeset->item(0);

                $sMethod->setAttribute('Algorithm', $objKey->type);

                $data = $this->canonicalizeData($sInfo, $this->canonicalMethod);

                $sigValue = base64_encode($this->signData($objKey, $data));

                $sigValueNode = $this->createNewSignNode('SignatureValue', $sigValue);

                if ($infoSibling = $sInfo->nextSibling) {

                    $infoSibling->parentNode->insertBefore($sigValueNode, $infoSibling);

                } else {

                    $this->sigNode->appendChild($sigValueNode);

                }

            }

        }

    }



    public function appendCert()

    {



    }



    /**

     * @param XMLSecurityKey $objKey

     * @param null|DOMNode $parent

     */

    public function appendKey($objKey, $parent=null)

    {

        $objKey->serializeKey($parent);

    }





    /**

     * This function inserts the signature element.

     *

     * The signature element will be appended to the element, unless $beforeNode is specified. If $beforeNode

     * is specified, the signature element will be inserted as the last element before $beforeNode.

     *

     * @param DOMNode $node       The node the signature element should be inserted into.

     * @param DOMNode $beforeNode The node the signature element should be located before.

     *

     * @return DOMNode The signature element node

     */

    public function insertSignature($node, $beforeNode = null)

    {



        $document = $node->ownerDocument;

        $signatureElement = $document->importNode($this->sigNode, true);



        if ($beforeNode == null) {

            return $node->insertBefore($signatureElement);

        } else {

            return $node->insertBefore($signatureElement, $beforeNode);

        }

    }



    /**

     * @param DOMNode $parentNode

     * @param bool $insertBefore

     * @return DOMNode

     */

    public function appendSignature($parentNode, $insertBefore = false)

    {

        $beforeNode = $insertBefore ? $parentNode->firstChild : null;

        return $this->insertSignature($parentNode, $beforeNode);

    }



    /**

     * @param string $cert

     * @param bool $isPEMFormat

     * @return string

     */

    public static function get509XCert($cert, $isPEMFormat=true)

    {

        $certs = self::staticGet509XCerts($cert, $isPEMFormat);

        if (! empty($certs)) {

            return $certs[0];

        }

        return '';

    }



    /**

     * @param string $certs

     * @param bool $isPEMFormat

     * @return array

     */

    public static function staticGet509XCerts($certs, $isPEMFormat=true)

    {

        if ($isPEMFormat) {

            $data = '';

            $certlist = array();

            $arCert = explode("\n", $certs);

            $inData = false;

            foreach ($arCert AS $curData) {

                if (! $inData) {

                    if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) {

                        $inData = true;

                    }

                } else {

                    if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) {

                        $inData = false;

                        $certlist[] = $data;

                        $data = '';

                        continue;

                    }

                    $data .= trim($curData);

                }

            }

            return $certlist;

        } else {

            return array($certs);

        }

    }



    /**

     * @param DOMElement $parentRef

     * @param string $cert

     * @param bool $isPEMFormat

     * @param bool $isURL

     * @param null|DOMXPath $xpath

     * @param null|array $options

     * @throws Exception

     */

    public static function staticAdd509Cert($parentRef, $cert, $isPEMFormat=true, $isURL=false, $xpath=null, $options=null)

    {

        if ($isURL) {

            $cert = file_get_contents($cert);

        }

        if (! $parentRef instanceof DOMElement) {

            throw new Exception('Invalid parent Node parameter');

        }

        $baseDoc = $parentRef->ownerDocument;



        if (empty($xpath)) {

            $xpath = new DOMXPath($parentRef->ownerDocument);

            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);

        }



        $query = "./secdsig:KeyInfo";

        $nodeset = $xpath->query($query, $parentRef);

        $keyInfo = $nodeset->item(0);

        $dsig_pfx = '';

        if (! $keyInfo) {

            $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS);

            if (! empty($pfx)) {

                $dsig_pfx = $pfx.":";

            }

            $inserted = false;

            $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo');



            $query = "./secdsig:Object";

            $nodeset = $xpath->query($query, $parentRef);

            if ($sObject = $nodeset->item(0)) {

                $sObject->parentNode->insertBefore($keyInfo, $sObject);

                $inserted = true;

            }



            if (! $inserted) {

                $parentRef->appendChild($keyInfo);

            }

        } else {

            $pfx = $keyInfo->lookupPrefix(self::XMLDSIGNS);

            if (! empty($pfx)) {

                $dsig_pfx = $pfx.":";

            }

        }



        // Add all certs if there are more than one

        $certs = self::staticGet509XCerts($cert, $isPEMFormat);



        // Attach X509 data node

        $x509DataNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Data');

        $keyInfo->appendChild($x509DataNode);



        $issuerSerial = false;

        $subjectName = false;

        if (is_array($options)) {

            if (! empty($options['issuerSerial'])) {

                $issuerSerial = true;

            }

            if (! empty($options['subjectName'])) {

                $subjectName = true;

            }

        }



        // Attach all certificate nodes and any additional data

        foreach ($certs as $X509Cert) {

            if ($issuerSerial || $subjectName) {

                if ($certData = openssl_x509_parse("-----BEGIN CERTIFICATE-----\n".chunk_split($X509Cert, 64, "\n")."-----END CERTIFICATE-----\n")) {

                    if ($subjectName && ! empty($certData['subject'])) {

                        if (is_array($certData['subject'])) {

                            $parts = array();

                            foreach ($certData['subject'] AS $key => $value) {

                                if (is_array($value)) {

                                    foreach ($value as $valueElement) {

                                        array_unshift($parts, "$key=$valueElement");

                                    }

                                } else {

                                    array_unshift($parts, "$key=$value");

                                }

                            }

                            $subjectNameValue = implode(',', $parts);

                        } else {

                            $subjectNameValue = $certData['issuer'];

                        }

                        $x509SubjectNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SubjectName', $subjectNameValue);

                        $x509DataNode->appendChild($x509SubjectNode);

                    }

                    if ($issuerSerial && ! empty($certData['issuer']) && ! empty($certData['serialNumber'])) {

                        if (is_array($certData['issuer'])) {

                            $parts = array();

                            foreach ($certData['issuer'] AS $key => $value) {

                                array_unshift($parts, "$key=$value");

                            }

                            $issuerName = implode(',', $parts);

                        } else {

                            $issuerName = $certData['issuer'];

                        }



                        $x509IssuerNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerSerial');

                        $x509DataNode->appendChild($x509IssuerNode);



                        $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerName', $issuerName);

                        $x509IssuerNode->appendChild($x509Node);

                        $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SerialNumber', $certData['serialNumber']);

                        $x509IssuerNode->appendChild($x509Node);

                    }

                }



            }

            $x509CertNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Certificate', $X509Cert);

            $x509DataNode->appendChild($x509CertNode);

        }

    }



    /**

     * @param string $cert

     * @param bool $isPEMFormat

     * @param bool $isURL

     * @param null|array $options

     */

    public function add509Cert($cert, $isPEMFormat=true, $isURL=false, $options=null)

    {

        if ($xpath = $this->getXPathObj()) {

            self::staticAdd509Cert($this->sigNode, $cert, $isPEMFormat, $isURL, $xpath, $options);

        }

    }



    /**

     * This function appends a node to the KeyInfo.

     *

     * The KeyInfo element will be created if one does not exist in the document.

     *

     * @param DOMNode $node The node to append to the KeyInfo.

     *

     * @return DOMNode The KeyInfo element node

     */

    public function appendToKeyInfo($node)

    {

        $parentRef = $this->sigNode;

        $baseDoc = $parentRef->ownerDocument;



        $xpath = $this->getXPathObj();

        if (empty($xpath)) {

            $xpath = new DOMXPath($parentRef->ownerDocument);

            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);

        }



        $query = "./secdsig:KeyInfo";

        $nodeset = $xpath->query($query, $parentRef);

        $keyInfo = $nodeset->item(0);

        if (! $keyInfo) {

            $dsig_pfx = '';

            $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS);

            if (! empty($pfx)) {

                $dsig_pfx = $pfx.":";

            }

            $inserted = false;

            $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo');



            $query = "./secdsig:Object";

            $nodeset = $xpath->query($query, $parentRef);

            if ($sObject = $nodeset->item(0)) {

                $sObject->parentNode->insertBefore($keyInfo, $sObject);

                $inserted = true;

            }



            if (! $inserted) {

                $parentRef->appendChild($keyInfo);

            }

        }



        $keyInfo->appendChild($node);



        return $keyInfo;

    }



    /**

     * This function retrieves an associative array of the validated nodes.

     *

     * The array will contain the id of the referenced node as the key and the node itself

     * as the value.

     *

     * Returns:

     *  An associative array of validated nodes or null if no nodes have been validated.

     *

     *  @return array Associative array of validated nodes

     */

    public function getValidatedNodes()

    {

        return $this->validatedNodes;

    }

}

