1<?php
2namespace RobRichards\XMLSecLibs;
3
4use DOMDocument;
5use DOMElement;
6use DOMNode;
7use DOMXPath;
8use Exception;
9use RobRichards\XMLSecLibs\Utils\XPath as XPath;
10
11/**
12 * xmlseclibs.php
13 *
14 * Copyright (c) 2007-2020, Robert Richards <rrichards@cdatazone.org>.
15 * All rights reserved.
16 *
17 * Redistribution and use in source and binary forms, with or without
18 * modification, are permitted provided that the following conditions
19 * are met:
20 *
21 *   * Redistributions of source code must retain the above copyright
22 *     notice, this list of conditions and the following disclaimer.
23 *
24 *   * Redistributions in binary form must reproduce the above copyright
25 *     notice, this list of conditions and the following disclaimer in
26 *     the documentation and/or other materials provided with the
27 *     distribution.
28 *
29 *   * Neither the name of Robert Richards nor the names of his
30 *     contributors may be used to endorse or promote products derived
31 *     from this software without specific prior written permission.
32 *
33 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
34 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
35 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
36 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
37 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
38 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
39 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
40 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
41 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
42 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
43 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
44 * POSSIBILITY OF SUCH DAMAGE.
45 *
46 * @author    Robert Richards <rrichards@cdatazone.org>
47 * @copyright 2007-2020 Robert Richards <rrichards@cdatazone.org>
48 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD License
49 */
50
51class XMLSecurityDSig
52{
53    const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#';
54    const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1';
55    const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256';
56    const SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384';
57    const SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512';
58    const RIPEMD160 = 'http://www.w3.org/2001/04/xmlenc#ripemd160';
59
60    const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
61    const C14N_COMMENTS = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments';
62    const EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#';
63    const EXC_C14N_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments';
64
65    const template = '<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
66  <ds:SignedInfo>
67    <ds:SignatureMethod />
68  </ds:SignedInfo>
69</ds:Signature>';
70
71    const BASE_TEMPLATE = '<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
72  <SignedInfo>
73    <SignatureMethod />
74  </SignedInfo>
75</Signature>';
76
77    /** @var DOMElement|null */
78    public $sigNode = null;
79
80    /** @var array */
81    public $idKeys = array();
82
83    /** @var array */
84    public $idNS = array();
85
86    /** @var string|null */
87    private $signedInfo = null;
88
89    /** @var DomXPath|null */
90    private $xPathCtx = null;
91
92    /** @var string|null */
93    private $canonicalMethod = null;
94
95    /** @var string */
96    private $prefix = '';
97
98    /** @var string */
99    private $searchpfx = 'secdsig';
100
101    /**
102     * This variable contains an associative array of validated nodes.
103     * @var array|null
104     */
105    private $validatedNodes = null;
106
107    /**
108     * @param string $prefix
109     */
110    public function __construct($prefix='ds')
111    {
112        $template = self::BASE_TEMPLATE;
113        if (! empty($prefix)) {
114            $this->prefix = $prefix.':';
115            $search = array("<S", "</S", "xmlns=");
116            $replace = array("<$prefix:S", "</$prefix:S", "xmlns:$prefix=");
117            $template = str_replace($search, $replace, $template);
118        }
119        $sigdoc = new DOMDocument();
120        $sigdoc->loadXML($template);
121        $this->sigNode = $sigdoc->documentElement;
122    }
123
124    /**
125     * Reset the XPathObj to null
126     */
127    private function resetXPathObj()
128    {
129        $this->xPathCtx = null;
130    }
131
132    /**
133     * Returns the XPathObj or null if xPathCtx is set and sigNode is empty.
134     *
135     * @return DOMXPath|null
136     */
137    private function getXPathObj()
138    {
139        if (empty($this->xPathCtx) && ! empty($this->sigNode)) {
140            $xpath = new DOMXPath($this->sigNode->ownerDocument);
141            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
142            $this->xPathCtx = $xpath;
143        }
144        return $this->xPathCtx;
145    }
146
147    /**
148     * Generate guid
149     *
150     * @param string $prefix Prefix to use for guid. defaults to pfx
151     *
152     * @return string The generated guid
153     */
154    public static function generateGUID($prefix='pfx')
155    {
156        $uuid = md5(uniqid(mt_rand(), true));
157        $guid = $prefix.substr($uuid, 0, 8)."-".
158                substr($uuid, 8, 4)."-".
159                substr($uuid, 12, 4)."-".
160                substr($uuid, 16, 4)."-".
161                substr($uuid, 20, 12);
162        return $guid;
163    }
164
165    /**
166     * Generate guid
167     *
168     * @param string $prefix Prefix to use for guid. defaults to pfx
169     *
170     * @return string The generated guid
171     *
172     * @deprecated Method deprecated in Release 1.4.1
173     */
174    public static function generate_GUID($prefix='pfx')
175    {
176        return self::generateGUID($prefix);
177    }
178
179    /**
180     * @param DOMDocument $objDoc
181     * @param int $pos
182     * @return DOMNode|null
183     */
184    public function locateSignature($objDoc, $pos=0)
185    {
186        if ($objDoc instanceof DOMDocument) {
187            $doc = $objDoc;
188        } else {
189            $doc = $objDoc->ownerDocument;
190        }
191        if ($doc) {
192            $xpath = new DOMXPath($doc);
193            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
194            $query = ".//secdsig:Signature";
195            $nodeset = $xpath->query($query, $objDoc);
196            $this->sigNode = $nodeset->item($pos);
197            $query = "./secdsig:SignedInfo";
198            $nodeset = $xpath->query($query, $this->sigNode);
199            if ($nodeset->length > 1) {
200                throw new Exception("Invalid structure - Too many SignedInfo elements found");
201            }
202            return $this->sigNode;
203        }
204        return null;
205    }
206
207    /**
208     * @param string $name
209     * @param null|string $value
210     * @return DOMElement
211     */
212    public function createNewSignNode($name, $value=null)
213    {
214        $doc = $this->sigNode->ownerDocument;
215        if (! is_null($value)) {
216            $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name, $value);
217        } else {
218            $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name);
219        }
220        return $node;
221    }
222
223    /**
224     * @param string $method
225     * @throws Exception
226     */
227    public function setCanonicalMethod($method)
228    {
229        switch ($method) {
230            case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':
231            case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':
232            case 'http://www.w3.org/2001/10/xml-exc-c14n#':
233            case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':
234                $this->canonicalMethod = $method;
235                break;
236            default:
237                throw new Exception('Invalid Canonical Method');
238        }
239        if ($xpath = $this->getXPathObj()) {
240            $query = './'.$this->searchpfx.':SignedInfo';
241            $nodeset = $xpath->query($query, $this->sigNode);
242            if ($sinfo = $nodeset->item(0)) {
243                $query = './'.$this->searchpfx.'CanonicalizationMethod';
244                $nodeset = $xpath->query($query, $sinfo);
245                if (! ($canonNode = $nodeset->item(0))) {
246                    $canonNode = $this->createNewSignNode('CanonicalizationMethod');
247                    $sinfo->insertBefore($canonNode, $sinfo->firstChild);
248                }
249                $canonNode->setAttribute('Algorithm', $this->canonicalMethod);
250            }
251        }
252    }
253
254    /**
255     * @param DOMNode $node
256     * @param string $canonicalmethod
257     * @param null|array $arXPath
258     * @param null|array $prefixList
259     * @return string
260     */
261    private function canonicalizeData($node, $canonicalmethod, $arXPath=null, $prefixList=null)
262    {
263        $exclusive = false;
264        $withComments = false;
265        switch ($canonicalmethod) {
266            case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':
267                $exclusive = false;
268                $withComments = false;
269                break;
270            case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':
271                $withComments = true;
272                break;
273            case 'http://www.w3.org/2001/10/xml-exc-c14n#':
274                $exclusive = true;
275                break;
276            case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':
277                $exclusive = true;
278                $withComments = true;
279                break;
280        }
281
282        if (is_null($arXPath) && ($node instanceof DOMNode) && ($node->ownerDocument !== null) && $node->isSameNode($node->ownerDocument->documentElement)) {
283            /* Check for any PI or comments as they would have been excluded */
284            $element = $node;
285            while ($refnode = $element->previousSibling) {
286                if ($refnode->nodeType == XML_PI_NODE || (($refnode->nodeType == XML_COMMENT_NODE) && $withComments)) {
287                    break;
288                }
289                $element = $refnode;
290            }
291            if ($refnode == null) {
292                $node = $node->ownerDocument;
293            }
294        }
295
296        return $node->C14N($exclusive, $withComments, $arXPath, $prefixList);
297    }
298
299    /**
300     * @return null|string
301     */
302    public function canonicalizeSignedInfo()
303    {
304
305        $doc = $this->sigNode->ownerDocument;
306        $canonicalmethod = null;
307        if ($doc) {
308            $xpath = $this->getXPathObj();
309            $query = "./secdsig:SignedInfo";
310            $nodeset = $xpath->query($query, $this->sigNode);
311            if ($nodeset->length > 1) {
312                throw new Exception("Invalid structure - Too many SignedInfo elements found");
313            }
314            if ($signInfoNode = $nodeset->item(0)) {
315                $query = "./secdsig:CanonicalizationMethod";
316                $nodeset = $xpath->query($query, $signInfoNode);
317                $prefixList = null;
318                if ($canonNode = $nodeset->item(0)) {
319                    $canonicalmethod = $canonNode->getAttribute('Algorithm');
320                    foreach ($canonNode->childNodes as $node)
321                    {
322                        if ($node->localName == 'InclusiveNamespaces') {
323                            if ($pfx = $node->getAttribute('PrefixList')) {
324                                $arpfx = array_filter(explode(' ', $pfx));
325                                if (count($arpfx) > 0) {
326                                    $prefixList = array_merge($prefixList ? $prefixList : array(), $arpfx);
327                                }
328                            }
329                        }
330                    }
331                }
332                $this->signedInfo = $this->canonicalizeData($signInfoNode, $canonicalmethod, null, $prefixList);
333                return $this->signedInfo;
334            }
335        }
336        return null;
337    }
338
339    /**
340     * @param string $digestAlgorithm
341     * @param string $data
342     * @param bool $encode
343     * @return string
344     * @throws Exception
345     */
346    public function calculateDigest($digestAlgorithm, $data, $encode = true)
347    {
348        switch ($digestAlgorithm) {
349            case self::SHA1:
350                $alg = 'sha1';
351                break;
352            case self::SHA256:
353                $alg = 'sha256';
354                break;
355            case self::SHA384:
356                $alg = 'sha384';
357                break;
358            case self::SHA512:
359                $alg = 'sha512';
360                break;
361            case self::RIPEMD160:
362                $alg = 'ripemd160';
363                break;
364            default:
365                throw new Exception("Cannot validate digest: Unsupported Algorithm <$digestAlgorithm>");
366        }
367
368        $digest = hash($alg, $data, true);
369        if ($encode) {
370            $digest = base64_encode($digest);
371        }
372        return $digest;
373
374    }
375
376    /**
377     * @param $refNode
378     * @param string $data
379     * @return bool
380     */
381    public function validateDigest($refNode, $data)
382    {
383        $xpath = new DOMXPath($refNode->ownerDocument);
384        $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
385        $query = 'string(./secdsig:DigestMethod/@Algorithm)';
386        $digestAlgorithm = $xpath->evaluate($query, $refNode);
387        $digValue = $this->calculateDigest($digestAlgorithm, $data, false);
388        $query = 'string(./secdsig:DigestValue)';
389        $digestValue = $xpath->evaluate($query, $refNode);
390        return ($digValue === base64_decode($digestValue));
391    }
392
393    /**
394     * @param $refNode
395     * @param DOMNode $objData
396     * @param bool $includeCommentNodes
397     * @return string
398     */
399    public function processTransforms($refNode, $objData, $includeCommentNodes = true)
400    {
401        $data = $objData;
402        $xpath = new DOMXPath($refNode->ownerDocument);
403        $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
404        $query = './secdsig:Transforms/secdsig:Transform';
405        $nodelist = $xpath->query($query, $refNode);
406        $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
407        $arXPath = null;
408        $prefixList = null;
409        foreach ($nodelist AS $transform) {
410            $algorithm = $transform->getAttribute("Algorithm");
411            switch ($algorithm) {
412                case 'http://www.w3.org/2001/10/xml-exc-c14n#':
413                case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments':
414
415                    if (!$includeCommentNodes) {
416                        /* We remove comment nodes by forcing it to use a canonicalization
417                         * without comments.
418                         */
419                        $canonicalMethod = 'http://www.w3.org/2001/10/xml-exc-c14n#';
420                    } else {
421                        $canonicalMethod = $algorithm;
422                    }
423
424                    $node = $transform->firstChild;
425                    while ($node) {
426                        if ($node->localName == 'InclusiveNamespaces') {
427                            if ($pfx = $node->getAttribute('PrefixList')) {
428                                $arpfx = array();
429                                $pfxlist = explode(" ", $pfx);
430                                foreach ($pfxlist AS $pfx) {
431                                    $val = trim($pfx);
432                                    if (! empty($val)) {
433                                        $arpfx[] = $val;
434                                    }
435                                }
436                                if (count($arpfx) > 0) {
437                                    $prefixList = $arpfx;
438                                }
439                            }
440                            break;
441                        }
442                        $node = $node->nextSibling;
443                    }
444            break;
445                case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315':
446                case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments':
447                    if (!$includeCommentNodes) {
448                        /* We remove comment nodes by forcing it to use a canonicalization
449                         * without comments.
450                         */
451                        $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
452                    } else {
453                        $canonicalMethod = $algorithm;
454                    }
455
456                    break;
457                case 'http://www.w3.org/TR/1999/REC-xpath-19991116':
458                    $node = $transform->firstChild;
459                    while ($node) {
460                        if ($node->localName == 'XPath') {
461                            $arXPath = array();
462                            $arXPath['query'] = '(.//. | .//@* | .//namespace::*)['.$node->nodeValue.']';
463                            $arXPath['namespaces'] = array();
464                            $nslist = $xpath->query('./namespace::*', $node);
465                            foreach ($nslist AS $nsnode) {
466                                if ($nsnode->localName != "xml") {
467                                    $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue;
468                                }
469                            }
470                            break;
471                        }
472                        $node = $node->nextSibling;
473                    }
474                    break;
475            }
476        }
477        if ($data instanceof DOMNode) {
478            $data = $this->canonicalizeData($objData, $canonicalMethod, $arXPath, $prefixList);
479        }
480        return $data;
481    }
482
483    /**
484     * @param DOMNode $refNode
485     * @return bool
486     */
487    public function processRefNode($refNode)
488    {
489        $dataObject = null;
490
491        /*
492         * Depending on the URI, we may not want to include comments in the result
493         * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel
494         */
495        $includeCommentNodes = true;
496
497        if ($uri = $refNode->getAttribute("URI")) {
498            $arUrl = parse_url($uri);
499            if (empty($arUrl['path'])) {
500                if ($identifier = $arUrl['fragment']) {
501
502                    /* This reference identifies a node with the given id by using
503                     * a URI on the form "#identifier". This should not include comments.
504                     */
505                    $includeCommentNodes = false;
506
507                    $xPath = new DOMXPath($refNode->ownerDocument);
508                    if ($this->idNS && is_array($this->idNS)) {
509                        foreach ($this->idNS as $nspf => $ns) {
510                            $xPath->registerNamespace($nspf, $ns);
511                        }
512                    }
513                    $iDlist = '@Id="'.XPath::filterAttrValue($identifier, XPath::DOUBLE_QUOTE).'"';
514                    if (is_array($this->idKeys)) {
515                        foreach ($this->idKeys as $idKey) {
516                            $iDlist .= " or @".XPath::filterAttrName($idKey).'="'.
517                                XPath::filterAttrValue($identifier, XPath::DOUBLE_QUOTE).'"';
518                        }
519                    }
520                    $query = '//*['.$iDlist.']';
521                    $dataObject = $xPath->query($query)->item(0);
522                } else {
523                    $dataObject = $refNode->ownerDocument;
524                }
525            }
526        } else {
527            /* This reference identifies the root node with an empty URI. This should
528             * not include comments.
529             */
530            $includeCommentNodes = false;
531
532            $dataObject = $refNode->ownerDocument;
533        }
534        $data = $this->processTransforms($refNode, $dataObject, $includeCommentNodes);
535        if (!$this->validateDigest($refNode, $data)) {
536            return false;
537        }
538
539        if ($dataObject instanceof DOMNode) {
540            /* Add this node to the list of validated nodes. */
541            if (! empty($identifier)) {
542                $this->validatedNodes[$identifier] = $dataObject;
543            } else {
544                $this->validatedNodes[] = $dataObject;
545            }
546        }
547
548        return true;
549    }
550
551    /**
552     * @param DOMNode $refNode
553     * @return null
554     */
555    public function getRefNodeID($refNode)
556    {
557        if ($uri = $refNode->getAttribute("URI")) {
558            $arUrl = parse_url($uri);
559            if (empty($arUrl['path'])) {
560                if ($identifier = $arUrl['fragment']) {
561                    return $identifier;
562                }
563            }
564        }
565        return null;
566    }
567
568    /**
569     * @return array
570     * @throws Exception
571     */
572    public function getRefIDs()
573    {
574        $refids = array();
575
576        $xpath = $this->getXPathObj();
577        $query = "./secdsig:SignedInfo[1]/secdsig:Reference";
578        $nodeset = $xpath->query($query, $this->sigNode);
579        if ($nodeset->length == 0) {
580            throw new Exception("Reference nodes not found");
581        }
582        foreach ($nodeset AS $refNode) {
583            $refids[] = $this->getRefNodeID($refNode);
584        }
585        return $refids;
586    }
587
588    /**
589     * @return bool
590     * @throws Exception
591     */
592    public function validateReference()
593    {
594        $docElem = $this->sigNode->ownerDocument->documentElement;
595        if (! $docElem->isSameNode($this->sigNode)) {
596            if ($this->sigNode->parentNode != null) {
597                $this->sigNode->parentNode->removeChild($this->sigNode);
598            }
599        }
600        $xpath = $this->getXPathObj();
601        $query = "./secdsig:SignedInfo[1]/secdsig:Reference";
602        $nodeset = $xpath->query($query, $this->sigNode);
603        if ($nodeset->length == 0) {
604            throw new Exception("Reference nodes not found");
605        }
606
607        /* Initialize/reset the list of validated nodes. */
608        $this->validatedNodes = array();
609
610        foreach ($nodeset AS $refNode) {
611            if (! $this->processRefNode($refNode)) {
612                /* Clear the list of validated nodes. */
613                $this->validatedNodes = null;
614                throw new Exception("Reference validation failed");
615            }
616        }
617        return true;
618    }
619
620    /**
621     * @param DOMNode $sinfoNode
622     * @param DOMDocument $node
623     * @param string $algorithm
624     * @param null|array $arTransforms
625     * @param null|array $options
626     */
627    private function addRefInternal($sinfoNode, $node, $algorithm, $arTransforms=null, $options=null)
628    {
629        $prefix = null;
630        $prefix_ns = null;
631        $id_name = 'Id';
632        $overwrite_id  = true;
633        $force_uri = false;
634
635        if (is_array($options)) {
636            $prefix = empty($options['prefix']) ? null : $options['prefix'];
637            $prefix_ns = empty($options['prefix_ns']) ? null : $options['prefix_ns'];
638            $id_name = empty($options['id_name']) ? 'Id' : $options['id_name'];
639            $overwrite_id = !isset($options['overwrite']) ? true : (bool) $options['overwrite'];
640            $force_uri = !isset($options['force_uri']) ? false : (bool) $options['force_uri'];
641        }
642
643        $attname = $id_name;
644        if (! empty($prefix)) {
645            $attname = $prefix.':'.$attname;
646        }
647
648        $refNode = $this->createNewSignNode('Reference');
649        $sinfoNode->appendChild($refNode);
650
651        if (! $node instanceof DOMDocument) {
652            $uri = null;
653            if (! $overwrite_id) {
654                $uri = $prefix_ns ? $node->getAttributeNS($prefix_ns, $id_name) : $node->getAttribute($id_name);
655            }
656            if (empty($uri)) {
657                $uri = self::generateGUID();
658                $node->setAttributeNS($prefix_ns, $attname, $uri);
659            }
660            $refNode->setAttribute("URI", '#'.$uri);
661        } elseif ($force_uri) {
662            $refNode->setAttribute("URI", '');
663        }
664
665        $transNodes = $this->createNewSignNode('Transforms');
666        $refNode->appendChild($transNodes);
667
668        if (is_array($arTransforms)) {
669            foreach ($arTransforms AS $transform) {
670                $transNode = $this->createNewSignNode('Transform');
671                $transNodes->appendChild($transNode);
672                if (is_array($transform) &&
673                    (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116'])) &&
674                    (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']))) {
675                    $transNode->setAttribute('Algorithm', 'http://www.w3.org/TR/1999/REC-xpath-19991116');
676                    $XPathNode = $this->createNewSignNode('XPath', $transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']);
677                    $transNode->appendChild($XPathNode);
678                    if (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'])) {
679                        foreach ($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'] AS $prefix => $namespace) {
680                            $XPathNode->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:$prefix", $namespace);
681                        }
682                    }
683                } else {
684                    $transNode->setAttribute('Algorithm', $transform);
685                }
686            }
687        } elseif (! empty($this->canonicalMethod)) {
688            $transNode = $this->createNewSignNode('Transform');
689            $transNodes->appendChild($transNode);
690            $transNode->setAttribute('Algorithm', $this->canonicalMethod);
691        }
692
693        $canonicalData = $this->processTransforms($refNode, $node);
694        $digValue = $this->calculateDigest($algorithm, $canonicalData);
695
696        $digestMethod = $this->createNewSignNode('DigestMethod');
697        $refNode->appendChild($digestMethod);
698        $digestMethod->setAttribute('Algorithm', $algorithm);
699
700        $digestValue = $this->createNewSignNode('DigestValue', $digValue);
701        $refNode->appendChild($digestValue);
702    }
703
704    /**
705     * @param DOMDocument $node
706     * @param string $algorithm
707     * @param null|array $arTransforms
708     * @param null|array $options
709     */
710    public function addReference($node, $algorithm, $arTransforms=null, $options=null)
711    {
712        if ($xpath = $this->getXPathObj()) {
713            $query = "./secdsig:SignedInfo";
714            $nodeset = $xpath->query($query, $this->sigNode);
715            if ($sInfo = $nodeset->item(0)) {
716                $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options);
717            }
718        }
719    }
720
721    /**
722     * @param array $arNodes
723     * @param string $algorithm
724     * @param null|array $arTransforms
725     * @param null|array $options
726     */
727    public function addReferenceList($arNodes, $algorithm, $arTransforms=null, $options=null)
728    {
729        if ($xpath = $this->getXPathObj()) {
730            $query = "./secdsig:SignedInfo";
731            $nodeset = $xpath->query($query, $this->sigNode);
732            if ($sInfo = $nodeset->item(0)) {
733                foreach ($arNodes AS $node) {
734                    $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options);
735                }
736            }
737        }
738    }
739
740    /**
741     * @param DOMElement|string $data
742     * @param null|string $mimetype
743     * @param null|string $encoding
744     * @return DOMElement
745     */
746    public function addObject($data, $mimetype=null, $encoding=null)
747    {
748        $objNode = $this->createNewSignNode('Object');
749        $this->sigNode->appendChild($objNode);
750        if (! empty($mimetype)) {
751            $objNode->setAttribute('MimeType', $mimetype);
752        }
753        if (! empty($encoding)) {
754            $objNode->setAttribute('Encoding', $encoding);
755        }
756
757        if ($data instanceof DOMElement) {
758            $newData = $this->sigNode->ownerDocument->importNode($data, true);
759        } else {
760            $newData = $this->sigNode->ownerDocument->createTextNode($data);
761        }
762        $objNode->appendChild($newData);
763
764        return $objNode;
765    }
766
767    /**
768     * @param null|DOMNode $node
769     * @return null|XMLSecurityKey
770     */
771    public function locateKey($node=null)
772    {
773        if (empty($node)) {
774            $node = $this->sigNode;
775        }
776        if (! $node instanceof DOMNode) {
777            return null;
778        }
779        if ($doc = $node->ownerDocument) {
780            $xpath = new DOMXPath($doc);
781            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
782            $query = "string(./secdsig:SignedInfo/secdsig:SignatureMethod/@Algorithm)";
783            $algorithm = $xpath->evaluate($query, $node);
784            if ($algorithm) {
785                try {
786                    $objKey = new XMLSecurityKey($algorithm, array('type' => 'public'));
787                } catch (Exception $e) {
788                    return null;
789                }
790                return $objKey;
791            }
792        }
793        return null;
794    }
795
796    /**
797     * Returns:
798     *  Bool when verifying HMAC_SHA1;
799     *  Int otherwise, with following meanings:
800     *    1 on succesful signature verification,
801     *    0 when signature verification failed,
802     *   -1 if an error occurred during processing.
803     *
804     * NOTE: be very careful when checking the int return value, because in
805     * PHP, -1 will be cast to True when in boolean context. Always check the
806     * return value in a strictly typed way, e.g. "$obj->verify(...) === 1".
807     *
808     * @param XMLSecurityKey $objKey
809     * @return bool|int
810     * @throws Exception
811     */
812    public function verify($objKey)
813    {
814        $doc = $this->sigNode->ownerDocument;
815        $xpath = new DOMXPath($doc);
816        $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
817        $query = "string(./secdsig:SignatureValue)";
818        $sigValue = $xpath->evaluate($query, $this->sigNode);
819        if (empty($sigValue)) {
820            throw new Exception("Unable to locate SignatureValue");
821        }
822        return $objKey->verifySignature($this->signedInfo, base64_decode($sigValue));
823    }
824
825    /**
826     * @param XMLSecurityKey $objKey
827     * @param string $data
828     * @return mixed|string
829     */
830    public function signData($objKey, $data)
831    {
832        return $objKey->signData($data);
833    }
834
835    /**
836     * @param XMLSecurityKey $objKey
837     * @param null|DOMNode $appendToNode
838     */
839    public function sign($objKey, $appendToNode = null)
840    {
841        // If we have a parent node append it now so C14N properly works
842        if ($appendToNode != null) {
843            $this->resetXPathObj();
844            $this->appendSignature($appendToNode);
845            $this->sigNode = $appendToNode->lastChild;
846        }
847        if ($xpath = $this->getXPathObj()) {
848            $query = "./secdsig:SignedInfo";
849            $nodeset = $xpath->query($query, $this->sigNode);
850            if ($sInfo = $nodeset->item(0)) {
851                $query = "./secdsig:SignatureMethod";
852                $nodeset = $xpath->query($query, $sInfo);
853                $sMethod = $nodeset->item(0);
854                $sMethod->setAttribute('Algorithm', $objKey->type);
855                $data = $this->canonicalizeData($sInfo, $this->canonicalMethod);
856                $sigValue = base64_encode($this->signData($objKey, $data));
857                $sigValueNode = $this->createNewSignNode('SignatureValue', $sigValue);
858                if ($infoSibling = $sInfo->nextSibling) {
859                    $infoSibling->parentNode->insertBefore($sigValueNode, $infoSibling);
860                } else {
861                    $this->sigNode->appendChild($sigValueNode);
862                }
863            }
864        }
865    }
866
867    public function appendCert()
868    {
869
870    }
871
872    /**
873     * @param XMLSecurityKey $objKey
874     * @param null|DOMNode $parent
875     */
876    public function appendKey($objKey, $parent=null)
877    {
878        $objKey->serializeKey($parent);
879    }
880
881
882    /**
883     * This function inserts the signature element.
884     *
885     * The signature element will be appended to the element, unless $beforeNode is specified. If $beforeNode
886     * is specified, the signature element will be inserted as the last element before $beforeNode.
887     *
888     * @param DOMNode $node       The node the signature element should be inserted into.
889     * @param DOMNode $beforeNode The node the signature element should be located before.
890     *
891     * @return DOMNode The signature element node
892     */
893    public function insertSignature($node, $beforeNode = null)
894    {
895
896        $document = $node->ownerDocument;
897        $signatureElement = $document->importNode($this->sigNode, true);
898
899        if ($beforeNode == null) {
900            return $node->insertBefore($signatureElement);
901        } else {
902            return $node->insertBefore($signatureElement, $beforeNode);
903        }
904    }
905
906    /**
907     * @param DOMNode $parentNode
908     * @param bool $insertBefore
909     * @return DOMNode
910     */
911    public function appendSignature($parentNode, $insertBefore = false)
912    {
913        $beforeNode = $insertBefore ? $parentNode->firstChild : null;
914        return $this->insertSignature($parentNode, $beforeNode);
915    }
916
917    /**
918     * @param string $cert
919     * @param bool $isPEMFormat
920     * @return string
921     */
922    public static function get509XCert($cert, $isPEMFormat=true)
923    {
924        $certs = self::staticGet509XCerts($cert, $isPEMFormat);
925        if (! empty($certs)) {
926            return $certs[0];
927        }
928        return '';
929    }
930
931    /**
932     * @param string $certs
933     * @param bool $isPEMFormat
934     * @return array
935     */
936    public static function staticGet509XCerts($certs, $isPEMFormat=true)
937    {
938        if ($isPEMFormat) {
939            $data = '';
940            $certlist = array();
941            $arCert = explode("\n", $certs);
942            $inData = false;
943            foreach ($arCert AS $curData) {
944                if (! $inData) {
945                    if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) {
946                        $inData = true;
947                    }
948                } else {
949                    if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) {
950                        $inData = false;
951                        $certlist[] = $data;
952                        $data = '';
953                        continue;
954                    }
955                    $data .= trim($curData);
956                }
957            }
958            return $certlist;
959        } else {
960            return array($certs);
961        }
962    }
963
964    /**
965     * @param DOMElement $parentRef
966     * @param string $cert
967     * @param bool $isPEMFormat
968     * @param bool $isURL
969     * @param null|DOMXPath $xpath
970     * @param null|array $options
971     * @throws Exception
972     */
973    public static function staticAdd509Cert($parentRef, $cert, $isPEMFormat=true, $isURL=false, $xpath=null, $options=null)
974    {
975        if ($isURL) {
976            $cert = file_get_contents($cert);
977        }
978        if (! $parentRef instanceof DOMElement) {
979            throw new Exception('Invalid parent Node parameter');
980        }
981        $baseDoc = $parentRef->ownerDocument;
982
983        if (empty($xpath)) {
984            $xpath = new DOMXPath($parentRef->ownerDocument);
985            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
986        }
987
988        $query = "./secdsig:KeyInfo";
989        $nodeset = $xpath->query($query, $parentRef);
990        $keyInfo = $nodeset->item(0);
991        $dsig_pfx = '';
992        if (! $keyInfo) {
993            $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS);
994            if (! empty($pfx)) {
995                $dsig_pfx = $pfx.":";
996            }
997            $inserted = false;
998            $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo');
999
1000            $query = "./secdsig:Object";
1001            $nodeset = $xpath->query($query, $parentRef);
1002            if ($sObject = $nodeset->item(0)) {
1003                $sObject->parentNode->insertBefore($keyInfo, $sObject);
1004                $inserted = true;
1005            }
1006
1007            if (! $inserted) {
1008                $parentRef->appendChild($keyInfo);
1009            }
1010        } else {
1011            $pfx = $keyInfo->lookupPrefix(self::XMLDSIGNS);
1012            if (! empty($pfx)) {
1013                $dsig_pfx = $pfx.":";
1014            }
1015        }
1016
1017        // Add all certs if there are more than one
1018        $certs = self::staticGet509XCerts($cert, $isPEMFormat);
1019
1020        // Attach X509 data node
1021        $x509DataNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Data');
1022        $keyInfo->appendChild($x509DataNode);
1023
1024        $issuerSerial = false;
1025        $subjectName = false;
1026        if (is_array($options)) {
1027            if (! empty($options['issuerSerial'])) {
1028                $issuerSerial = true;
1029            }
1030            if (! empty($options['subjectName'])) {
1031                $subjectName = true;
1032            }
1033        }
1034
1035        // Attach all certificate nodes and any additional data
1036        foreach ($certs as $X509Cert) {
1037            if ($issuerSerial || $subjectName) {
1038                if ($certData = openssl_x509_parse("-----BEGIN CERTIFICATE-----\n".chunk_split($X509Cert, 64, "\n")."-----END CERTIFICATE-----\n")) {
1039                    if ($subjectName && ! empty($certData['subject'])) {
1040                        if (is_array($certData['subject'])) {
1041                            $parts = array();
1042                            foreach ($certData['subject'] AS $key => $value) {
1043                                if (is_array($value)) {
1044                                    foreach ($value as $valueElement) {
1045                                        array_unshift($parts, "$key=$valueElement");
1046                                    }
1047                                } else {
1048                                    array_unshift($parts, "$key=$value");
1049                                }
1050                            }
1051                            $subjectNameValue = implode(',', $parts);
1052                        } else {
1053                            $subjectNameValue = $certData['issuer'];
1054                        }
1055                        $x509SubjectNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SubjectName', $subjectNameValue);
1056                        $x509DataNode->appendChild($x509SubjectNode);
1057                    }
1058                    if ($issuerSerial && ! empty($certData['issuer']) && ! empty($certData['serialNumber'])) {
1059                        if (is_array($certData['issuer'])) {
1060                            $parts = array();
1061                            foreach ($certData['issuer'] AS $key => $value) {
1062                                array_unshift($parts, "$key=$value");
1063                            }
1064                            $issuerName = implode(',', $parts);
1065                        } else {
1066                            $issuerName = $certData['issuer'];
1067                        }
1068
1069                        $x509IssuerNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerSerial');
1070                        $x509DataNode->appendChild($x509IssuerNode);
1071
1072                        $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerName', $issuerName);
1073                        $x509IssuerNode->appendChild($x509Node);
1074                        $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SerialNumber', $certData['serialNumber']);
1075                        $x509IssuerNode->appendChild($x509Node);
1076                    }
1077                }
1078
1079            }
1080            $x509CertNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Certificate', $X509Cert);
1081            $x509DataNode->appendChild($x509CertNode);
1082        }
1083    }
1084
1085    /**
1086     * @param string $cert
1087     * @param bool $isPEMFormat
1088     * @param bool $isURL
1089     * @param null|array $options
1090     */
1091    public function add509Cert($cert, $isPEMFormat=true, $isURL=false, $options=null)
1092    {
1093        if ($xpath = $this->getXPathObj()) {
1094            self::staticAdd509Cert($this->sigNode, $cert, $isPEMFormat, $isURL, $xpath, $options);
1095        }
1096    }
1097
1098    /**
1099     * This function appends a node to the KeyInfo.
1100     *
1101     * The KeyInfo element will be created if one does not exist in the document.
1102     *
1103     * @param DOMNode $node The node to append to the KeyInfo.
1104     *
1105     * @return DOMNode The KeyInfo element node
1106     */
1107    public function appendToKeyInfo($node)
1108    {
1109        $parentRef = $this->sigNode;
1110        $baseDoc = $parentRef->ownerDocument;
1111
1112        $xpath = $this->getXPathObj();
1113        if (empty($xpath)) {
1114            $xpath = new DOMXPath($parentRef->ownerDocument);
1115            $xpath->registerNamespace('secdsig', self::XMLDSIGNS);
1116        }
1117
1118        $query = "./secdsig:KeyInfo";
1119        $nodeset = $xpath->query($query, $parentRef);
1120        $keyInfo = $nodeset->item(0);
1121        if (! $keyInfo) {
1122            $dsig_pfx = '';
1123            $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS);
1124            if (! empty($pfx)) {
1125                $dsig_pfx = $pfx.":";
1126            }
1127            $inserted = false;
1128            $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo');
1129
1130            $query = "./secdsig:Object";
1131            $nodeset = $xpath->query($query, $parentRef);
1132            if ($sObject = $nodeset->item(0)) {
1133                $sObject->parentNode->insertBefore($keyInfo, $sObject);
1134                $inserted = true;
1135            }
1136
1137            if (! $inserted) {
1138                $parentRef->appendChild($keyInfo);
1139            }
1140        }
1141
1142        $keyInfo->appendChild($node);
1143
1144        return $keyInfo;
1145    }
1146
1147    /**
1148     * This function retrieves an associative array of the validated nodes.
1149     *
1150     * The array will contain the id of the referenced node as the key and the node itself
1151     * as the value.
1152     *
1153     * Returns:
1154     *  An associative array of validated nodes or null if no nodes have been validated.
1155     *
1156     *  @return array Associative array of validated nodes
1157     */
1158    public function getValidatedNodes()
1159    {
1160        return $this->validatedNodes;
1161    }
1162}
1163