Subversion Repositories oidplus

Rev

Rev 868 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
868 daniel-mar 1
<?php
2
namespace RobRichards\XMLSecLibs;
3
 
4
use DOMDocument;
5
use DOMElement;
6
use DOMNode;
7
use DOMXPath;
8
use Exception;
9
use 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
 
51
class 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'));
1050 daniel-mar 787
                } catch (\Exception $e) {
868 daniel-mar 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
}