1<?php 2/** 3 * This file is part of php-saml. 4 * 5 * (c) OneLogin Inc 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 * 10 * @package OneLogin 11 * @author OneLogin Inc <saml-info@onelogin.com> 12 * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE 13 * @link https://github.com/onelogin/php-saml 14 */ 15 16namespace OneLogin\Saml2; 17 18use RobRichards\XMLSecLibs\XMLSecurityKey; 19use RobRichards\XMLSecLibs\XMLSecurityDSig; 20use RobRichards\XMLSecLibs\XMLSecEnc; 21 22use DOMDocument; 23use DOMElement; 24use DOMNodeList; 25use DomNode; 26use DOMXPath; 27use Exception; 28 29/** 30 * Utils of OneLogin PHP Toolkit 31 * 32 * Defines several often used methods 33 */ 34class Utils 35{ 36 const RESPONSE_SIGNATURE_XPATH = "/samlp:Response/ds:Signature"; 37 const ASSERTION_SIGNATURE_XPATH = "/samlp:Response/saml:Assertion/ds:Signature"; 38 39 /** 40 * @var bool Control if the `Forwarded-For-*` headers are used 41 */ 42 private static $_proxyVars = false; 43 44 /** 45 * @var string|null 46 */ 47 private static $_host; 48 49 /** 50 * @var string|null 51 */ 52 private static $_protocol; 53 54 /** 55 * @var string 56 */ 57 private static $_protocolRegex = '@^https?://@i'; 58 59 /** 60 * @var int|null 61 */ 62 private static $_port; 63 64 /** 65 * @var string|null 66 */ 67 private static $_baseurlpath; 68 69 /** 70 * This function load an XML string in a save way. 71 * Prevent XEE/XXE Attacks 72 * 73 * @param DOMDocument $dom The document where load the xml. 74 * @param string $xml The XML string to be loaded. 75 * 76 * @return DOMDocument|false $dom The result of load the XML at the DOMDocument 77 * 78 * @throws Exception 79 */ 80 public static function loadXML(DOMDocument $dom, $xml) 81 { 82 assert($dom instanceof DOMDocument); 83 assert(is_string($xml)); 84 85 $oldEntityLoader = libxml_disable_entity_loader(true); 86 87 $res = $dom->loadXML($xml); 88 89 libxml_disable_entity_loader($oldEntityLoader); 90 91 foreach ($dom->childNodes as $child) { 92 if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { 93 throw new Exception( 94 'Detected use of DOCTYPE/ENTITY in XML, disabled to prevent XXE/XEE attacks' 95 ); 96 } 97 } 98 99 if (!$res) { 100 return false; 101 } else { 102 return $dom; 103 } 104 } 105 106 /** 107 * This function attempts to validate an XML string against the specified schema. 108 * 109 * It will parse the string into a DOMDocument and validate this document against the schema. 110 * 111 * @param string|DOMDocument $xml The XML string or document which should be validated. 112 * @param string $schema The schema filename which should be used. 113 * @param bool $debug To disable/enable the debug mode 114 * @param string $schemaPath Change schema path 115 * 116 * @return string|DOMDocument $dom string that explains the problem or the DOMDocument 117 * 118 * @throws Exception 119 */ 120 public static function validateXML($xml, $schema, $debug = false, $schemaPath = null) 121 { 122 assert(is_string($xml) || $xml instanceof DOMDocument); 123 assert(is_string($schema)); 124 125 libxml_clear_errors(); 126 libxml_use_internal_errors(true); 127 128 if ($xml instanceof DOMDocument) { 129 $dom = $xml; 130 } else { 131 $dom = new DOMDocument; 132 $dom = self::loadXML($dom, $xml); 133 if (!$dom) { 134 return 'unloaded_xml'; 135 } 136 } 137 138 if (isset($schemaPath)) { 139 $schemaFile = $schemaPath . $schema; 140 } else { 141 $schemaFile = __DIR__ . '/schemas/' . $schema; 142 } 143 144 $oldEntityLoader = libxml_disable_entity_loader(false); 145 $res = $dom->schemaValidate($schemaFile); 146 libxml_disable_entity_loader($oldEntityLoader); 147 if (!$res) { 148 $xmlErrors = libxml_get_errors(); 149 syslog(LOG_INFO, 'Error validating the metadata: '.var_export($xmlErrors, true)); 150 151 if ($debug) { 152 foreach ($xmlErrors as $error) { 153 echo htmlentities($error->message)."\n"; 154 } 155 } 156 return 'invalid_xml'; 157 } 158 159 return $dom; 160 } 161 162 /** 163 * Import a node tree into a target document 164 * Copy it before a reference node as a sibling 165 * and at the end of the copy remove 166 * the reference node in the target document 167 * As it were 'replacing' it 168 * Leaving nested default namespaces alone 169 * (Standard importNode with deep copy 170 * mangles nested default namespaces) 171 * 172 * The reference node must not be a DomDocument 173 * It CAN be the top element of a document 174 * Returns the copied node in the target document 175 * 176 * @param DomNode $targetNode 177 * @param DomNode $sourceNode 178 * @param bool $recurse 179 * @return DOMNode 180 * @throws Exception 181 */ 182 public static function treeCopyReplace(DomNode $targetNode, DomNode $sourceNode, $recurse = false) 183 { 184 if ($targetNode->parentNode === null) { 185 throw new Exception('Illegal argument targetNode. It has no parentNode.'); 186 } 187 $clonedNode = $targetNode->ownerDocument->importNode($sourceNode, false); 188 if ($recurse) { 189 $resultNode = $targetNode->appendChild($clonedNode); 190 } else { 191 $resultNode = $targetNode->parentNode->insertBefore($clonedNode, $targetNode); 192 } 193 if ($sourceNode->childNodes !== null) { 194 foreach ($sourceNode->childNodes as $child) { 195 self::treeCopyReplace($resultNode, $child, true); 196 } 197 } 198 if (!$recurse) { 199 $targetNode->parentNode->removeChild($targetNode); 200 } 201 return $resultNode; 202 } 203 204 /** 205 * Returns a x509 cert (adding header & footer if required). 206 * 207 * @param string $cert A x509 unformated cert 208 * @param bool $heads True if we want to include head and footer 209 * 210 * @return string $x509 Formatted cert 211 */ 212 public static function formatCert($cert, $heads = true) 213 { 214 $x509cert = str_replace(array("\x0D", "\r", "\n"), "", $cert); 215 if (!empty($x509cert)) { 216 $x509cert = str_replace('-----BEGIN CERTIFICATE-----', "", $x509cert); 217 $x509cert = str_replace('-----END CERTIFICATE-----', "", $x509cert); 218 $x509cert = str_replace(' ', '', $x509cert); 219 220 if ($heads) { 221 $x509cert = "-----BEGIN CERTIFICATE-----\n".chunk_split($x509cert, 64, "\n")."-----END CERTIFICATE-----\n"; 222 } 223 224 } 225 return $x509cert; 226 } 227 228 /** 229 * Returns a private key (adding header & footer if required). 230 * 231 * @param string $key A private key 232 * @param bool $heads True if we want to include head and footer 233 * 234 * @return string $rsaKey Formatted private key 235 */ 236 public static function formatPrivateKey($key, $heads = true) 237 { 238 $key = str_replace(array("\x0D", "\r", "\n"), "", $key); 239 if (!empty($key)) { 240 if (strpos($key, '-----BEGIN PRIVATE KEY-----') !== false) { 241 $key = Utils::getStringBetween($key, '-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----'); 242 $key = str_replace(' ', '', $key); 243 244 if ($heads) { 245 $key = "-----BEGIN PRIVATE KEY-----\n".chunk_split($key, 64, "\n")."-----END PRIVATE KEY-----\n"; 246 } 247 } else if (strpos($key, '-----BEGIN RSA PRIVATE KEY-----') !== false) { 248 $key = Utils::getStringBetween($key, '-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----'); 249 $key = str_replace(' ', '', $key); 250 251 if ($heads) { 252 $key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split($key, 64, "\n")."-----END RSA PRIVATE KEY-----\n"; 253 } 254 } else { 255 $key = str_replace(' ', '', $key); 256 257 if ($heads) { 258 $key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split($key, 64, "\n")."-----END RSA PRIVATE KEY-----\n"; 259 } 260 } 261 } 262 return $key; 263 } 264 265 /** 266 * Extracts a substring between 2 marks 267 * 268 * @param string $str The target string 269 * @param string $start The initial mark 270 * @param string $end The end mark 271 * 272 * @return string A substring or an empty string if is not able to find the marks 273 * or if there is no string between the marks 274 */ 275 public static function getStringBetween($str, $start, $end) 276 { 277 $str = ' ' . $str; 278 $ini = strpos($str, $start); 279 280 if ($ini == 0) { 281 return ''; 282 } 283 284 $ini += strlen($start); 285 $len = strpos($str, $end, $ini) - $ini; 286 return substr($str, $ini, $len); 287 } 288 289 /** 290 * Executes a redirection to the provided url (or return the target url). 291 * 292 * @param string $url The target url 293 * @param array $parameters Extra parameters to be passed as part of the url 294 * @param bool $stay True if we want to stay (returns the url string) False to redirect 295 * 296 * @return string|null $url 297 * 298 * @throws Error 299 */ 300 public static function redirect($url, array $parameters = array(), $stay = false) 301 { 302 assert(is_string($url)); 303 304 if (substr($url, 0, 1) === '/') { 305 $url = self::getSelfURLhost() . $url; 306 } 307 308 /** 309 * Verify that the URL matches the regex for the protocol. 310 * By default this will check for http and https 311 */ 312 $wrongProtocol = !preg_match(self::$_protocolRegex, $url); 313 $url = filter_var($url, FILTER_VALIDATE_URL); 314 if ($wrongProtocol || empty($url)) { 315 throw new Error( 316 'Redirect to invalid URL: ' . $url, 317 Error::REDIRECT_INVALID_URL 318 ); 319 } 320 321 /* Add encoded parameters */ 322 if (strpos($url, '?') === false) { 323 $paramPrefix = '?'; 324 } else { 325 $paramPrefix = '&'; 326 } 327 328 foreach ($parameters as $name => $value) { 329 if ($value === null) { 330 $param = urlencode($name); 331 } else if (is_array($value)) { 332 $param = ""; 333 foreach ($value as $val) { 334 $param .= urlencode($name) . "[]=" . urlencode($val). '&'; 335 } 336 if (!empty($param)) { 337 $param = substr($param, 0, -1); 338 } 339 } else { 340 $param = urlencode($name) . '=' . urlencode($value); 341 } 342 343 if (!empty($param)) { 344 $url .= $paramPrefix . $param; 345 $paramPrefix = '&'; 346 } 347 } 348 349 if ($stay) { 350 return $url; 351 } 352 353 header('Pragma: no-cache'); 354 header('Cache-Control: no-cache, must-revalidate'); 355 header('Location: ' . $url); 356 exit(); 357 } 358 359 /** 360 * @param $protocolRegex string 361 */ 362 public static function setProtocolRegex($protocolRegex) 363 { 364 if (!empty($protocolRegex)) { 365 self::$_protocolRegex = $protocolRegex; 366 } 367 } 368 369 /** 370 * Set the Base URL value. 371 * 372 * @param string $baseurl The base url to be used when constructing URLs 373 */ 374 public static function setBaseURL($baseurl) 375 { 376 if (!empty($baseurl)) { 377 $baseurlpath = '/'; 378 $matches = array(); 379 if (preg_match('#^https?://([^/]*)/?(.*)#i', $baseurl, $matches)) { 380 if (strpos($baseurl, 'https://') === false) { 381 self::setSelfProtocol('http'); 382 $port = '80'; 383 } else { 384 self::setSelfProtocol('https'); 385 $port = '443'; 386 } 387 388 $currentHost = $matches[1]; 389 if (false !== strpos($currentHost, ':')) { 390 list($currentHost, $possiblePort) = explode(':', $matches[1], 2); 391 if (is_numeric($possiblePort)) { 392 $port = $possiblePort; 393 } 394 } 395 396 if (isset($matches[2]) && !empty($matches[2])) { 397 $baseurlpath = $matches[2]; 398 } 399 400 self::setSelfHost($currentHost); 401 self::setSelfPort($port); 402 self::setBaseURLPath($baseurlpath); 403 } 404 } else { 405 self::$_host = null; 406 self::$_protocol = null; 407 self::$_port = null; 408 self::$_baseurlpath = null; 409 } 410 } 411 412 /** 413 * @param bool $proxyVars Whether to use `X-Forwarded-*` headers to determine port/domain/protocol 414 */ 415 public static function setProxyVars($proxyVars) 416 { 417 self::$_proxyVars = (bool)$proxyVars; 418 } 419 420 /** 421 * @return bool 422 */ 423 public static function getProxyVars() 424 { 425 return self::$_proxyVars; 426 } 427 428 /** 429 * Returns the protocol + the current host + the port (if different than 430 * common ports). 431 * 432 * @return string The URL 433 */ 434 public static function getSelfURLhost() 435 { 436 $currenthost = self::getSelfHost(); 437 438 $port = ''; 439 440 if (self::isHTTPS()) { 441 $protocol = 'https'; 442 } else { 443 $protocol = 'http'; 444 } 445 446 $portnumber = self::getSelfPort(); 447 448 if (isset($portnumber) && ($portnumber != '80') && ($portnumber != '443')) { 449 $port = ':' . $portnumber; 450 } 451 452 return $protocol."://" . $currenthost . $port; 453 } 454 455 /** 456 * @param string $host The host to use when constructing URLs 457 */ 458 public static function setSelfHost($host) 459 { 460 self::$_host = $host; 461 } 462 463 /** 464 * @param string $baseurlpath The baseurl path to use when constructing URLs 465 */ 466 public static function setBaseURLPath($baseurlpath) 467 { 468 if (empty($baseurlpath)) { 469 self::$_baseurlpath = null; 470 } else if ($baseurlpath == '/') { 471 self::$_baseurlpath = '/'; 472 } else { 473 self::$_baseurlpath = '/' . trim($baseurlpath, '/') . '/'; 474 } 475 } 476 477 /** 478 * @return string The baseurlpath to be used when constructing URLs 479 */ 480 public static function getBaseURLPath() 481 { 482 return self::$_baseurlpath; 483 } 484 485 /** 486 * @return string The raw host name 487 */ 488 protected static function getRawHost() 489 { 490 if (self::$_host) { 491 $currentHost = self::$_host; 492 } elseif (self::getProxyVars() && array_key_exists('HTTP_X_FORWARDED_HOST', $_SERVER)) { 493 $currentHost = $_SERVER['HTTP_X_FORWARDED_HOST']; 494 } elseif (array_key_exists('HTTP_HOST', $_SERVER)) { 495 $currentHost = $_SERVER['HTTP_HOST']; 496 } elseif (array_key_exists('SERVER_NAME', $_SERVER)) { 497 $currentHost = $_SERVER['SERVER_NAME']; 498 } else { 499 if (function_exists('gethostname')) { 500 $currentHost = gethostname(); 501 } else { 502 $currentHost = php_uname("n"); 503 } 504 } 505 return $currentHost; 506 } 507 508 /** 509 * @param int $port The port number to use when constructing URLs 510 */ 511 public static function setSelfPort($port) 512 { 513 self::$_port = $port; 514 } 515 516 /** 517 * @param string $protocol The protocol to identify as using, usually http or https 518 */ 519 public static function setSelfProtocol($protocol) 520 { 521 self::$_protocol = $protocol; 522 } 523 524 /** 525 * @return string http|https 526 */ 527 public static function getSelfProtocol() 528 { 529 $protocol = 'http'; 530 if (self::$_protocol) { 531 $protocol = self::$_protocol; 532 } elseif (self::getSelfPort() == 443) { 533 $protocol = 'https'; 534 } elseif (self::getProxyVars() && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { 535 $protocol = $_SERVER['HTTP_X_FORWARDED_PROTO']; 536 } elseif (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { 537 $protocol = 'https'; 538 } 539 return $protocol; 540 } 541 542 /** 543 * Returns the current host. 544 * 545 * @return string $currentHost The current host 546 */ 547 public static function getSelfHost() 548 { 549 $currentHost = self::getRawHost(); 550 551 // strip the port 552 if (false !== strpos($currentHost, ':')) { 553 list($currentHost, $port) = explode(':', $currentHost, 2); 554 } 555 556 return $currentHost; 557 } 558 559 /** 560 * @return null|string The port number used for the request 561 */ 562 public static function getSelfPort() 563 { 564 $portnumber = null; 565 if (self::$_port) { 566 $portnumber = self::$_port; 567 } else if (self::getProxyVars() && isset($_SERVER["HTTP_X_FORWARDED_PORT"])) { 568 $portnumber = $_SERVER["HTTP_X_FORWARDED_PORT"]; 569 } else if (isset($_SERVER["SERVER_PORT"])) { 570 $portnumber = $_SERVER["SERVER_PORT"]; 571 } else { 572 $currentHost = self::getRawHost(); 573 574 // strip the port 575 if (false !== strpos($currentHost, ':')) { 576 list($currentHost, $port) = explode(':', $currentHost, 2); 577 if (is_numeric($port)) { 578 $portnumber = $port; 579 } 580 } 581 } 582 return $portnumber; 583 } 584 585 /** 586 * Checks if https or http. 587 * 588 * @return bool $isHttps False if https is not active 589 */ 590 public static function isHTTPS() 591 { 592 return self::getSelfProtocol() == 'https'; 593 } 594 595 /** 596 * Returns the URL of the current host + current view. 597 * 598 * @return string 599 */ 600 public static function getSelfURLNoQuery() 601 { 602 $selfURLNoQuery = self::getSelfURLhost(); 603 604 $infoWithBaseURLPath = self::buildWithBaseURLPath($_SERVER['SCRIPT_NAME']); 605 if (!empty($infoWithBaseURLPath)) { 606 $selfURLNoQuery .= $infoWithBaseURLPath; 607 } else { 608 $selfURLNoQuery .= $_SERVER['SCRIPT_NAME']; 609 } 610 611 if (isset($_SERVER['PATH_INFO'])) { 612 $selfURLNoQuery .= $_SERVER['PATH_INFO']; 613 } 614 615 return $selfURLNoQuery; 616 } 617 618 /** 619 * Returns the routed URL of the current host + current view. 620 * 621 * @return string 622 */ 623 public static function getSelfRoutedURLNoQuery() 624 { 625 $selfURLhost = self::getSelfURLhost(); 626 $route = ''; 627 628 if (!empty($_SERVER['REQUEST_URI'])) { 629 $route = $_SERVER['REQUEST_URI']; 630 if (!empty($_SERVER['QUERY_STRING'])) { 631 $route = self::strLreplace($_SERVER['QUERY_STRING'], '', $route); 632 if (substr($route, -1) == '?') { 633 $route = substr($route, 0, -1); 634 } 635 } 636 } 637 638 $infoWithBaseURLPath = self::buildWithBaseURLPath($route); 639 if (!empty($infoWithBaseURLPath)) { 640 $route = $infoWithBaseURLPath; 641 } 642 643 $selfRoutedURLNoQuery = $selfURLhost . $route; 644 645 $pos = strpos($selfRoutedURLNoQuery, "?"); 646 if ($pos !== false) { 647 $selfRoutedURLNoQuery = substr($selfRoutedURLNoQuery, 0, $pos-1); 648 } 649 650 return $selfRoutedURLNoQuery; 651 } 652 653 public static function strLreplace($search, $replace, $subject) 654 { 655 $pos = strrpos($subject, $search); 656 657 if ($pos !== false) { 658 $subject = substr_replace($subject, $replace, $pos, strlen($search)); 659 } 660 661 return $subject; 662 } 663 664 /** 665 * Returns the URL of the current host + current view + query. 666 * 667 * @return string 668 */ 669 public static function getSelfURL() 670 { 671 $selfURLhost = self::getSelfURLhost(); 672 673 $requestURI = ''; 674 if (!empty($_SERVER['REQUEST_URI'])) { 675 $requestURI = $_SERVER['REQUEST_URI']; 676 $matches = array(); 677 if ($requestURI[0] !== '/' && preg_match('#^https?://[^/]*(/.*)#i', $requestURI, $matches)) { 678 $requestURI = $matches[1]; 679 } 680 } 681 682 $infoWithBaseURLPath = self::buildWithBaseURLPath($requestURI); 683 if (!empty($infoWithBaseURLPath)) { 684 $requestURI = $infoWithBaseURLPath; 685 } 686 687 return $selfURLhost . $requestURI; 688 } 689 690 /** 691 * Returns the part of the URL with the BaseURLPath. 692 * 693 * @param string $info Contains path info 694 * 695 * @return string 696 */ 697 protected static function buildWithBaseURLPath($info) 698 { 699 $result = ''; 700 $baseURLPath = self::getBaseURLPath(); 701 if (!empty($baseURLPath)) { 702 $result = $baseURLPath; 703 if (!empty($info)) { 704 $path = explode('/', $info); 705 $extractedInfo = array_pop($path); 706 if (!empty($extractedInfo)) { 707 $result .= $extractedInfo; 708 } 709 } 710 } 711 return $result; 712 } 713 714 /** 715 * Extract a query param - as it was sent - from $_SERVER[QUERY_STRING] 716 * 717 * @param string $name The param to-be extracted 718 * 719 * @return string 720 */ 721 public static function extractOriginalQueryParam($name) 722 { 723 $index = strpos($_SERVER['QUERY_STRING'], $name.'='); 724 $substring = substr($_SERVER['QUERY_STRING'], $index + strlen($name) + 1); 725 $end = strpos($substring, '&'); 726 return $end ? substr($substring, 0, strpos($substring, '&')) : $substring; 727 } 728 729 /** 730 * Generates an unique string (used for example as ID for assertions). 731 * 732 * @return string A unique string 733 */ 734 public static function generateUniqueID() 735 { 736 return 'ONELOGIN_' . sha1(uniqid((string)mt_rand(), true)); 737 } 738 739 /** 740 * Converts a UNIX timestamp to SAML2 timestamp on the form 741 * yyyy-mm-ddThh:mm:ss(\.s+)?Z. 742 * 743 * @param string|int $time The time we should convert (DateTime). 744 * 745 * @return string $timestamp SAML2 timestamp. 746 */ 747 public static function parseTime2SAML($time) 748 { 749 $date = new \DateTime("@$time", new \DateTimeZone('UTC')); 750 $timestamp = $date->format("Y-m-d\TH:i:s\Z"); 751 return $timestamp; 752 } 753 754 /** 755 * Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z 756 * to a UNIX timestamp. The sub-second part is ignored. 757 * 758 * @param string $time The time we should convert (SAML Timestamp). 759 * 760 * @return int $timestamp Converted to a unix timestamp. 761 * 762 * @throws Exception 763 */ 764 public static function parseSAML2Time($time) 765 { 766 $matches = array(); 767 768 /* We use a very strict regex to parse the timestamp. */ 769 $exp1 = '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)'; 770 $exp2 = 'T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D'; 771 if (preg_match($exp1 . $exp2, $time, $matches) == 0) { 772 throw new Exception( 773 'Invalid SAML2 timestamp passed to' . 774 ' parseSAML2Time: ' . $time 775 ); 776 } 777 778 /* Extract the different components of the time from the 779 * matches in the regex. int cast will ignore leading zeroes 780 * in the string. 781 */ 782 $year = (int) $matches[1]; 783 $month = (int) $matches[2]; 784 $day = (int) $matches[3]; 785 $hour = (int) $matches[4]; 786 $minute = (int) $matches[5]; 787 $second = (int) $matches[6]; 788 789 /* We use gmmktime because the timestamp will always be given 790 * in UTC. 791 */ 792 $ts = gmmktime($hour, $minute, $second, $month, $day, $year); 793 794 return $ts; 795 } 796 797 798 /** 799 * Interprets a ISO8601 duration value relative to a given timestamp. 800 * 801 * @param string $duration The duration, as a string. 802 * @param int|null $timestamp The unix timestamp we should apply the 803 * duration to. Optional, default to the 804 * current time. 805 * 806 * @return int The new timestamp, after the duration is applied. 807 * 808 * @throws Exception 809 */ 810 public static function parseDuration($duration, $timestamp = null) 811 { 812 assert(is_string($duration)); 813 assert(is_null($timestamp) || is_int($timestamp)); 814 815 $matches = array(); 816 817 /* Parse the duration. We use a very strict pattern. */ 818 $durationRegEx = '#^(-?)P(?:(?:(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?)?)|(?:(\\d+)W))$#D'; 819 if (!preg_match($durationRegEx, $duration, $matches)) { 820 throw new Exception('Invalid ISO 8601 duration: ' . $duration); 821 } 822 823 $durYears = (empty($matches[2]) ? 0 : (int)$matches[2]); 824 $durMonths = (empty($matches[3]) ? 0 : (int)$matches[3]); 825 $durDays = (empty($matches[4]) ? 0 : (int)$matches[4]); 826 $durHours = (empty($matches[5]) ? 0 : (int)$matches[5]); 827 $durMinutes = (empty($matches[6]) ? 0 : (int)$matches[6]); 828 $durSeconds = (empty($matches[7]) ? 0 : (int)$matches[7]); 829 $durWeeks = (empty($matches[8]) ? 0 : (int)$matches[8]); 830 831 if (!empty($matches[1])) { 832 /* Negative */ 833 $durYears = -$durYears; 834 $durMonths = -$durMonths; 835 $durDays = -$durDays; 836 $durHours = -$durHours; 837 $durMinutes = -$durMinutes; 838 $durSeconds = -$durSeconds; 839 $durWeeks = -$durWeeks; 840 } 841 842 if ($timestamp === null) { 843 $timestamp = time(); 844 } 845 846 if ($durYears !== 0 || $durMonths !== 0) { 847 /* Special handling of months and years, since they aren't a specific interval, but 848 * instead depend on the current time. 849 */ 850 851 /* We need the year and month from the timestamp. Unfortunately, PHP doesn't have the 852 * gmtime function. Instead we use the gmdate function, and split the result. 853 */ 854 $yearmonth = explode(':', gmdate('Y:n', $timestamp)); 855 $year = (int)$yearmonth[0]; 856 $month = (int)$yearmonth[1]; 857 858 /* Remove the year and month from the timestamp. */ 859 $timestamp -= gmmktime(0, 0, 0, $month, 1, $year); 860 861 /* Add years and months, and normalize the numbers afterwards. */ 862 $year += $durYears; 863 $month += $durMonths; 864 while ($month > 12) { 865 $year += 1; 866 $month -= 12; 867 } 868 while ($month < 1) { 869 $year -= 1; 870 $month += 12; 871 } 872 873 /* Add year and month back into timestamp. */ 874 $timestamp += gmmktime(0, 0, 0, $month, 1, $year); 875 } 876 877 /* Add the other elements. */ 878 $timestamp += $durWeeks * 7 * 24 * 60 * 60; 879 $timestamp += $durDays * 24 * 60 * 60; 880 $timestamp += $durHours * 60 * 60; 881 $timestamp += $durMinutes * 60; 882 $timestamp += $durSeconds; 883 884 return $timestamp; 885 } 886 887 /** 888 * Compares 2 dates and returns the earliest. 889 * 890 * @param string|null $cacheDuration The duration, as a string. 891 * @param string|int|null $validUntil The valid until date, as a string or as a timestamp 892 * 893 * @return int|null $expireTime The expiration time. 894 * 895 * @throws Exception 896 */ 897 public static function getExpireTime($cacheDuration = null, $validUntil = null) 898 { 899 $expireTime = null; 900 901 if ($cacheDuration !== null) { 902 $expireTime = self::parseDuration($cacheDuration, time()); 903 } 904 905 if ($validUntil !== null) { 906 if (is_int($validUntil)) { 907 $validUntilTime = $validUntil; 908 } else { 909 $validUntilTime = self::parseSAML2Time($validUntil); 910 } 911 if ($expireTime === null || $expireTime > $validUntilTime) { 912 $expireTime = $validUntilTime; 913 } 914 } 915 916 return $expireTime; 917 } 918 919 920 /** 921 * Extracts nodes from the DOMDocument. 922 * 923 * @param DOMDocument $dom The DOMDocument 924 * @param string $query \Xpath Expression 925 * @param DOMElement|null $context Context Node (DOMElement) 926 * 927 * @return DOMNodeList The queried nodes 928 */ 929 public static function query(DOMDocument $dom, $query, DOMElement $context = null) 930 { 931 $xpath = new DOMXPath($dom); 932 $xpath->registerNamespace('samlp', Constants::NS_SAMLP); 933 $xpath->registerNamespace('saml', Constants::NS_SAML); 934 $xpath->registerNamespace('ds', Constants::NS_DS); 935 $xpath->registerNamespace('xenc', Constants::NS_XENC); 936 $xpath->registerNamespace('xsi', Constants::NS_XSI); 937 $xpath->registerNamespace('xs', Constants::NS_XS); 938 $xpath->registerNamespace('md', Constants::NS_MD); 939 940 if (isset($context)) { 941 $res = $xpath->query($query, $context); 942 } else { 943 $res = $xpath->query($query); 944 } 945 return $res; 946 } 947 948 /** 949 * Checks if the session is started or not. 950 * 951 * @return bool true if the sessíon is started 952 */ 953 public static function isSessionStarted() 954 { 955 if (PHP_VERSION_ID >= 50400) { 956 return session_status() === PHP_SESSION_ACTIVE ? true : false; 957 } else { 958 return session_id() === '' ? false : true; 959 } 960 } 961 962 /** 963 * Deletes the local session. 964 */ 965 public static function deleteLocalSession() 966 { 967 968 if (Utils::isSessionStarted()) { 969 session_destroy(); 970 } 971 972 unset($_SESSION); 973 } 974 975 /** 976 * Calculates the fingerprint of a x509cert. 977 * 978 * @param string $x509cert x509 cert formatted 979 * @param string $alg Algorithm to be used in order to calculate the fingerprint 980 * 981 * @return null|string Formatted fingerprint 982 */ 983 public static function calculateX509Fingerprint($x509cert, $alg = 'sha1') 984 { 985 assert(is_string($x509cert)); 986 987 $arCert = explode("\n", $x509cert); 988 $data = ''; 989 $inData = false; 990 991 foreach ($arCert as $curData) { 992 if (! $inData) { 993 if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { 994 $inData = true; 995 } elseif ((strncmp($curData, '-----BEGIN PUBLIC KEY', 21) == 0) || (strncmp($curData, '-----BEGIN RSA PRIVATE KEY', 26) == 0)) { 996 /* This isn't an X509 certificate. */ 997 return null; 998 } 999 } else { 1000 if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { 1001 break; 1002 } 1003 $data .= trim($curData); 1004 } 1005 } 1006 1007 if (empty($data)) { 1008 return null; 1009 } 1010 1011 $decodedData = base64_decode($data); 1012 1013 switch ($alg) { 1014 case 'sha512': 1015 case 'sha384': 1016 case 'sha256': 1017 $fingerprint = hash($alg, $decodedData, false); 1018 break; 1019 case 'sha1': 1020 default: 1021 $fingerprint = strtolower(sha1($decodedData)); 1022 break; 1023 } 1024 return $fingerprint; 1025 } 1026 1027 /** 1028 * Formates a fingerprint. 1029 * 1030 * @param string $fingerprint fingerprint 1031 * 1032 * @return string Formatted fingerprint 1033 */ 1034 public static function formatFingerPrint($fingerprint) 1035 { 1036 $formatedFingerprint = str_replace(':', '', $fingerprint); 1037 $formatedFingerprint = strtolower($formatedFingerprint); 1038 return $formatedFingerprint; 1039 } 1040 1041 /** 1042 * Generates a nameID. 1043 * 1044 * @param string $value fingerprint 1045 * @param string $spnq SP Name Qualifier 1046 * @param string|null $format SP Format 1047 * @param string|null $cert IdP Public cert to encrypt the nameID 1048 * @param string|null $nq IdP Name Qualifier 1049 * 1050 * @return string $nameIDElement DOMElement | XMLSec nameID 1051 * 1052 * @throws Exception 1053 */ 1054 public static function generateNameId($value, $spnq, $format = null, $cert = null, $nq = null) 1055 { 1056 1057 $doc = new DOMDocument(); 1058 1059 $nameId = $doc->createElement('saml:NameID'); 1060 if (isset($spnq)) { 1061 $nameId->setAttribute('SPNameQualifier', $spnq); 1062 } 1063 if (isset($nq)) { 1064 $nameId->setAttribute('NameQualifier', $nq); 1065 } 1066 if (isset($format)) { 1067 $nameId->setAttribute('Format', $format); 1068 } 1069 $nameId->appendChild($doc->createTextNode($value)); 1070 1071 $doc->appendChild($nameId); 1072 1073 if (!empty($cert)) { 1074 $seckey = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type'=>'public')); 1075 $seckey->loadKey($cert); 1076 1077 $enc = new XMLSecEnc(); 1078 $enc->setNode($nameId); 1079 $enc->type = XMLSecEnc::Element; 1080 1081 $symmetricKey = new XMLSecurityKey(XMLSecurityKey::AES128_CBC); 1082 $symmetricKey->generateSessionKey(); 1083 $enc->encryptKey($seckey, $symmetricKey); 1084 1085 $encryptedData = $enc->encryptNode($symmetricKey); 1086 1087 $newdoc = new DOMDocument(); 1088 1089 $encryptedID = $newdoc->createElement('saml:EncryptedID'); 1090 1091 $newdoc->appendChild($encryptedID); 1092 1093 $encryptedID->appendChild($encryptedID->ownerDocument->importNode($encryptedData, true)); 1094 1095 return $newdoc->saveXML($encryptedID); 1096 } else { 1097 return $doc->saveXML($nameId); 1098 } 1099 } 1100 1101 1102 /** 1103 * Gets Status from a Response. 1104 * 1105 * @param DOMDocument $dom The Response as XML 1106 * 1107 * @return array $status The Status, an array with the code and a message. 1108 * 1109 * @throws ValidationError 1110 */ 1111 public static function getStatus(DOMDocument $dom) 1112 { 1113 $status = array(); 1114 1115 $statusEntry = self::query($dom, '/samlp:Response/samlp:Status'); 1116 if ($statusEntry->length != 1) { 1117 throw new ValidationError( 1118 "Missing Status on response", 1119 ValidationError::MISSING_STATUS 1120 ); 1121 } 1122 1123 $codeEntry = self::query($dom, '/samlp:Response/samlp:Status/samlp:StatusCode', $statusEntry->item(0)); 1124 if ($codeEntry->length != 1) { 1125 throw new ValidationError( 1126 "Missing Status Code on response", 1127 ValidationError::MISSING_STATUS_CODE 1128 ); 1129 } 1130 $code = $codeEntry->item(0)->getAttribute('Value'); 1131 $status['code'] = $code; 1132 1133 $status['msg'] = ''; 1134 $messageEntry = self::query($dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', $statusEntry->item(0)); 1135 if ($messageEntry->length == 0) { 1136 $subCodeEntry = self::query($dom, '/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode', $statusEntry->item(0)); 1137 if ($subCodeEntry->length == 1) { 1138 $status['msg'] = $subCodeEntry->item(0)->getAttribute('Value'); 1139 } 1140 } else if ($messageEntry->length == 1) { 1141 $msg = $messageEntry->item(0)->textContent; 1142 $status['msg'] = $msg; 1143 } 1144 1145 return $status; 1146 } 1147 1148 /** 1149 * Decrypts an encrypted element. 1150 * 1151 * @param DOMElement $encryptedData The encrypted data. 1152 * @param XMLSecurityKey $inputKey The decryption key. 1153 * @param bool $formatOutput Format or not the output. 1154 * 1155 * @return DOMElement The decrypted element. 1156 * 1157 * @throws ValidationError 1158 */ 1159 public static function decryptElement(DOMElement $encryptedData, XMLSecurityKey $inputKey, $formatOutput = true) 1160 { 1161 1162 $enc = new XMLSecEnc(); 1163 1164 $enc->setNode($encryptedData); 1165 $enc->type = $encryptedData->getAttribute("Type"); 1166 1167 $symmetricKey = $enc->locateKey($encryptedData); 1168 if (!$symmetricKey) { 1169 throw new ValidationError( 1170 'Could not locate key algorithm in encrypted data.', 1171 ValidationError::KEY_ALGORITHM_ERROR 1172 ); 1173 } 1174 1175 $symmetricKeyInfo = $enc->locateKeyInfo($symmetricKey); 1176 if (!$symmetricKeyInfo) { 1177 throw new ValidationError( 1178 "Could not locate <dsig:KeyInfo> for the encrypted key.", 1179 ValidationError::KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA 1180 ); 1181 } 1182 1183 $inputKeyAlgo = $inputKey->getAlgorithm(); 1184 if ($symmetricKeyInfo->isEncrypted) { 1185 $symKeyInfoAlgo = $symmetricKeyInfo->getAlgorithm(); 1186 1187 if ($symKeyInfoAlgo === XMLSecurityKey::RSA_OAEP_MGF1P && $inputKeyAlgo === XMLSecurityKey::RSA_1_5) { 1188 $inputKeyAlgo = XMLSecurityKey::RSA_OAEP_MGF1P; 1189 } 1190 1191 if ($inputKeyAlgo !== $symKeyInfoAlgo) { 1192 throw new ValidationError( 1193 'Algorithm mismatch between input key and key used to encrypt ' . 1194 ' the symmetric key for the message. Key was: ' . 1195 var_export($inputKeyAlgo, true) . '; message was: ' . 1196 var_export($symKeyInfoAlgo, true), 1197 ValidationError::KEY_ALGORITHM_ERROR 1198 ); 1199 } 1200 1201 $encKey = $symmetricKeyInfo->encryptedCtx; 1202 $symmetricKeyInfo->key = $inputKey->key; 1203 $keySize = $symmetricKey->getSymmetricKeySize(); 1204 if ($keySize === null) { 1205 // To protect against "key oracle" attacks 1206 throw new ValidationError( 1207 'Unknown key size for encryption algorithm: ' . var_export($symmetricKey->type, true), 1208 ValidationError::KEY_ALGORITHM_ERROR 1209 ); 1210 } 1211 1212 $key = $encKey->decryptKey($symmetricKeyInfo); 1213 if (strlen($key) != $keySize) { 1214 $encryptedKey = $encKey->getCipherValue(); 1215 $pkey = openssl_pkey_get_details($symmetricKeyInfo->key); 1216 $pkey = sha1(serialize($pkey), true); 1217 $key = sha1($encryptedKey . $pkey, true); 1218 1219 /* Make sure that the key has the correct length. */ 1220 if (strlen($key) > $keySize) { 1221 $key = substr($key, 0, $keySize); 1222 } elseif (strlen($key) < $keySize) { 1223 $key = str_pad($key, $keySize); 1224 } 1225 } 1226 $symmetricKey->loadKey($key); 1227 } else { 1228 $symKeyAlgo = $symmetricKey->getAlgorithm(); 1229 if ($inputKeyAlgo !== $symKeyAlgo) { 1230 throw new ValidationError( 1231 'Algorithm mismatch between input key and key in message. ' . 1232 'Key was: ' . var_export($inputKeyAlgo, true) . '; message was: ' . 1233 var_export($symKeyAlgo, true), 1234 ValidationError::KEY_ALGORITHM_ERROR 1235 ); 1236 } 1237 $symmetricKey = $inputKey; 1238 } 1239 1240 $decrypted = $enc->decryptNode($symmetricKey, false); 1241 1242 $xml = '<root xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'.$decrypted.'</root>'; 1243 $newDoc = new DOMDocument(); 1244 if ($formatOutput) { 1245 $newDoc->preserveWhiteSpace = false; 1246 $newDoc->formatOutput = true; 1247 } 1248 $newDoc = self::loadXML($newDoc, $xml); 1249 if (!$newDoc) { 1250 throw new ValidationError( 1251 'Failed to parse decrypted XML.', 1252 ValidationError::INVALID_XML_FORMAT 1253 ); 1254 } 1255 1256 $decryptedElement = $newDoc->firstChild->firstChild; 1257 if ($decryptedElement === null) { 1258 throw new ValidationError( 1259 'Missing encrypted element.', 1260 ValidationError::MISSING_ENCRYPTED_ELEMENT 1261 ); 1262 } 1263 1264 return $decryptedElement; 1265 } 1266 1267 /** 1268 * Converts a XMLSecurityKey to the correct algorithm. 1269 * 1270 * @param XMLSecurityKey $key The key. 1271 * @param string $algorithm The desired algorithm. 1272 * @param string $type Public or private key, defaults to public. 1273 * 1274 * @return XMLSecurityKey The new key. 1275 * 1276 * @throws Exception 1277 */ 1278 public static function castKey(XMLSecurityKey $key, $algorithm, $type = 'public') 1279 { 1280 assert(is_string($algorithm)); 1281 assert($type === 'public' || $type === 'private'); 1282 1283 // do nothing if algorithm is already the type of the key 1284 if ($key->type === $algorithm) { 1285 return $key; 1286 } 1287 1288 if (!Utils::isSupportedSigningAlgorithm($algorithm)) { 1289 throw new Exception('Unsupported signing algorithm.'); 1290 } 1291 1292 $keyInfo = openssl_pkey_get_details($key->key); 1293 if ($keyInfo === false) { 1294 throw new Exception('Unable to get key details from XMLSecurityKey.'); 1295 } 1296 if (!isset($keyInfo['key'])) { 1297 throw new Exception('Missing key in public key details.'); 1298 } 1299 $newKey = new XMLSecurityKey($algorithm, array('type'=>$type)); 1300 $newKey->loadKey($keyInfo['key']); 1301 return $newKey; 1302 } 1303 1304 /** 1305 * @param $algorithm 1306 * 1307 * @return bool 1308 */ 1309 public static function isSupportedSigningAlgorithm($algorithm) 1310 { 1311 return in_array( 1312 $algorithm, 1313 array( 1314 XMLSecurityKey::RSA_1_5, 1315 XMLSecurityKey::RSA_SHA1, 1316 XMLSecurityKey::RSA_SHA256, 1317 XMLSecurityKey::RSA_SHA384, 1318 XMLSecurityKey::RSA_SHA512 1319 ) 1320 ); 1321 } 1322 1323 /** 1324 * Adds signature key and senders certificate to an element (Message or Assertion). 1325 * 1326 * @param string|DOMDocument $xml The element we should sign 1327 * @param string $key The private key 1328 * @param string $cert The public 1329 * @param string $signAlgorithm Signature algorithm method 1330 * @param string $digestAlgorithm Digest algorithm method 1331 * 1332 * @return string 1333 * 1334 * @throws Exception 1335 */ 1336 public static function addSign($xml, $key, $cert, $signAlgorithm = XMLSecurityKey::RSA_SHA256, $digestAlgorithm = XMLSecurityDSig::SHA256) 1337 { 1338 if ($xml instanceof DOMDocument) { 1339 $dom = $xml; 1340 } else { 1341 $dom = new DOMDocument(); 1342 $dom = self::loadXML($dom, $xml); 1343 if (!$dom) { 1344 throw new Exception('Error parsing xml string'); 1345 } 1346 } 1347 1348 /* Load the private key. */ 1349 $objKey = new XMLSecurityKey($signAlgorithm, array('type' => 'private')); 1350 $objKey->loadKey($key, false); 1351 1352 /* Get the EntityDescriptor node we should sign. */ 1353 $rootNode = $dom->firstChild; 1354 1355 /* Sign the metadata with our private key. */ 1356 $objXMLSecDSig = new XMLSecurityDSig(); 1357 $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); 1358 1359 $objXMLSecDSig->addReferenceList( 1360 array($rootNode), 1361 $digestAlgorithm, 1362 array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N), 1363 array('id_name' => 'ID') 1364 ); 1365 1366 $objXMLSecDSig->sign($objKey); 1367 1368 /* Add the certificate to the signature. */ 1369 $objXMLSecDSig->add509Cert($cert, true); 1370 1371 $insertBefore = $rootNode->firstChild; 1372 $messageTypes = array('AuthnRequest', 'Response', 'LogoutRequest','LogoutResponse'); 1373 if (in_array($rootNode->localName, $messageTypes)) { 1374 $issuerNodes = self::query($dom, '/'.$rootNode->tagName.'/saml:Issuer'); 1375 if ($issuerNodes->length == 1) { 1376 $insertBefore = $issuerNodes->item(0)->nextSibling; 1377 } 1378 } 1379 1380 /* Add the signature. */ 1381 $objXMLSecDSig->insertSignature($rootNode, $insertBefore); 1382 1383 /* Return the DOM tree as a string. */ 1384 $signedxml = $dom->saveXML(); 1385 1386 return $signedxml; 1387 } 1388 1389 /** 1390 * Validates a signature (Message or Assertion). 1391 * 1392 * @param string|\DomNode $xml The element we should validate 1393 * @param string|null $cert The pubic cert 1394 * @param string|null $fingerprint The fingerprint of the public cert 1395 * @param string|null $fingerprintalg The algorithm used to get the fingerprint 1396 * @param string|null $xpath The xpath of the signed element 1397 * @param array|null $multiCerts Multiple public certs 1398 * 1399 * @return bool 1400 * 1401 * @throws Exception 1402 */ 1403 public static function validateSign($xml, $cert = null, $fingerprint = null, $fingerprintalg = 'sha1', $xpath = null, $multiCerts = null) 1404 { 1405 if ($xml instanceof DOMDocument) { 1406 $dom = clone $xml; 1407 } else if ($xml instanceof DOMElement) { 1408 $dom = clone $xml->ownerDocument; 1409 } else { 1410 $dom = new DOMDocument(); 1411 $dom = self::loadXML($dom, $xml); 1412 } 1413 1414 $objXMLSecDSig = new XMLSecurityDSig(); 1415 $objXMLSecDSig->idKeys = array('ID'); 1416 1417 if ($xpath) { 1418 $nodeset = Utils::query($dom, $xpath); 1419 $objDSig = $nodeset->item(0); 1420 $objXMLSecDSig->sigNode = $objDSig; 1421 } else { 1422 $objDSig = $objXMLSecDSig->locateSignature($dom); 1423 } 1424 1425 if (!$objDSig) { 1426 throw new Exception('Cannot locate Signature Node'); 1427 } 1428 1429 $objKey = $objXMLSecDSig->locateKey(); 1430 if (!$objKey) { 1431 throw new Exception('We have no idea about the key'); 1432 } 1433 1434 if (!Utils::isSupportedSigningAlgorithm($objKey->type)) { 1435 throw new Exception('Unsupported signing algorithm.'); 1436 } 1437 1438 $objXMLSecDSig->canonicalizeSignedInfo(); 1439 1440 try { 1441 $retVal = $objXMLSecDSig->validateReference(); 1442 } catch (Exception $e) { 1443 throw $e; 1444 } 1445 1446 XMLSecEnc::staticLocateKeyInfo($objKey, $objDSig); 1447 1448 if (!empty($multiCerts)) { 1449 // If multiple certs are provided, I may ignore $cert and 1450 // $fingerprint provided by the method and just check the 1451 // certs on the array 1452 $fingerprint = null; 1453 } else { 1454 // else I add the cert to the array in order to check 1455 // validate signatures with it and the with it and the 1456 // $fingerprint value 1457 $multiCerts = array($cert); 1458 } 1459 1460 $valid = false; 1461 foreach ($multiCerts as $cert) { 1462 if (!empty($cert)) { 1463 $objKey->loadKey($cert, false, true); 1464 if ($objXMLSecDSig->verify($objKey) === 1) { 1465 $valid = true; 1466 break; 1467 } 1468 } else { 1469 if (!empty($fingerprint)) { 1470 $domCert = $objKey->getX509Certificate(); 1471 $domCertFingerprint = Utils::calculateX509Fingerprint($domCert, $fingerprintalg); 1472 if (Utils::formatFingerPrint($fingerprint) == $domCertFingerprint) { 1473 $objKey->loadKey($domCert, false, true); 1474 if ($objXMLSecDSig->verify($objKey) === 1) { 1475 $valid = true; 1476 break; 1477 } 1478 } 1479 } 1480 } 1481 } 1482 return $valid; 1483 } 1484 1485 /** 1486 * Validates a binary signature 1487 * 1488 * @param string $messageType Type of SAML Message 1489 * @param array $getData HTTP GET array 1490 * @param array $idpData IdP setting data 1491 * @param bool $retrieveParametersFromServer Indicates where to get the values in order to validate the Sign, from getData or from $_SERVER 1492 * 1493 * @return bool 1494 * 1495 * @throws Exception 1496 */ 1497 public static function validateBinarySign($messageType, $getData, $idpData, $retrieveParametersFromServer = false) 1498 { 1499 if (!isset($getData['SigAlg'])) { 1500 $signAlg = XMLSecurityKey::RSA_SHA1; 1501 } else { 1502 $signAlg = $getData['SigAlg']; 1503 } 1504 1505 if ($retrieveParametersFromServer) { 1506 $signedQuery = $messageType.'='.Utils::extractOriginalQueryParam($messageType); 1507 if (isset($getData['RelayState'])) { 1508 $signedQuery .= '&RelayState='.Utils::extractOriginalQueryParam('RelayState'); 1509 } 1510 $signedQuery .= '&SigAlg='.Utils::extractOriginalQueryParam('SigAlg'); 1511 } else { 1512 $signedQuery = $messageType.'='.urlencode($getData[$messageType]); 1513 if (isset($getData['RelayState'])) { 1514 $signedQuery .= '&RelayState='.urlencode($getData['RelayState']); 1515 } 1516 $signedQuery .= '&SigAlg='.urlencode($signAlg); 1517 } 1518 1519 if ($messageType == "SAMLRequest") { 1520 $strMessageType = "Logout Request"; 1521 } else { 1522 $strMessageType = "Logout Response"; 1523 } 1524 $existsMultiX509Sign = isset($idpData['x509certMulti']) && isset($idpData['x509certMulti']['signing']) && !empty($idpData['x509certMulti']['signing']); 1525 if ((!isset($idpData['x509cert']) || empty($idpData['x509cert'])) && !$existsMultiX509Sign) { 1526 throw new Error( 1527 "In order to validate the sign on the ".$strMessageType.", the x509cert of the IdP is required", 1528 Error::CERT_NOT_FOUND 1529 ); 1530 } 1531 1532 if ($existsMultiX509Sign) { 1533 $multiCerts = $idpData['x509certMulti']['signing']; 1534 } else { 1535 $multiCerts = array($idpData['x509cert']); 1536 } 1537 1538 $signatureValid = false; 1539 foreach ($multiCerts as $cert) { 1540 $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'public')); 1541 $objKey->loadKey($cert, false, true); 1542 1543 if ($signAlg != XMLSecurityKey::RSA_SHA1) { 1544 try { 1545 $objKey = Utils::castKey($objKey, $signAlg, 'public'); 1546 } catch (Exception $e) { 1547 $ex = new ValidationError( 1548 "Invalid signAlg in the recieved ".$strMessageType, 1549 ValidationError::INVALID_SIGNATURE 1550 ); 1551 if (count($multiCerts) == 1) { 1552 throw $ex; 1553 } 1554 } 1555 } 1556 1557 if ($objKey->verifySignature($signedQuery, base64_decode($getData['Signature'])) === 1) { 1558 $signatureValid = true; 1559 break; 1560 } 1561 } 1562 return $signatureValid; 1563 } 1564} 1565