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