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