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