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-2019, 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-2019 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