8 * This source file is subject to the new BSD license that is bundled
9 * with this package in the file LICENSE.txt.
10 * It is also available through the world-wide-web at this URL:
11 * http://framework.zend.com/license/new-bsd
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@zend.com so we can send you a copy immediately.
17 * @package Zend_OpenId
18 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
19 * @license http://framework.zend.com/license/new-bsd New BSD License
24 * @see Zend_Controller_Response_Abstract
26 require_once "Zend/Controller/Response/Abstract.php";
29 * Static class that contains common utility functions for
30 * {@link Zend_OpenId_Consumer} and {@link Zend_OpenId_Provider}.
32 * This class implements common utility functions that are used by both
33 * Consumer and Provider. They include functions for Diffie-Hellman keys
34 * generation and exchange, URL normalization, HTTP redirection and some others.
37 * @package Zend_OpenId
38 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
39 * @license http://framework.zend.com/license/new-bsd New BSD License
44 * Default Diffie-Hellman key generator (1024 bit)
46 const DH_P
= 'dcf93a0b883972ec0e19989ac5a2ce310e1d37717e8d9571bb7623731866e61ef75a2e27898b057f9891c2e27a639c3f29b60814581cd3b2ca3986d2683705577d45c2e7e52dc81c7a171876e5cea74b1448bfdfaf18828efd2519f14e45e3826634af1949e5b535cc829a483b8a76223e5d490a257f05bdff16f2fb22c583ab';
49 * Default Diffie-Hellman prime number (should be 2 or 5)
54 * OpenID 2.0 namespace. All OpenID 2.0 messages MUST contain variable
55 * openid.ns with its value.
57 const NS_2_0
= 'http://specs.openid.net/auth/2.0';
60 * Allows enable/disable stoping execution of PHP script after redirect()
62 static public $exitOnRedirect = true;
65 * Alternative request URL that can be used to override the default
68 static public $selfUrl = null;
71 * Sets alternative request URL that can be used to override the default
74 * @param string $selfUrl the URL to be set
75 * @return string the old value of overriding URL
77 static public function setSelfUrl($selfUrl = null)
79 $ret = self
::$selfUrl;
80 self
::$selfUrl = $selfUrl;
85 * Returns a full URL that was requested on current HTTP request.
89 static public function selfUrl()
91 if (self
::$selfUrl !== null) {
92 return self
::$selfUrl;
93 } if (isset($_SERVER['SCRIPT_URI'])) {
94 return $_SERVER['SCRIPT_URI'];
98 if (isset($_SERVER['HTTP_HOST'])) {
99 if (($pos = strpos($_SERVER['HTTP_HOST'], ':')) === false) {
100 if (isset($_SERVER['SERVER_PORT'])) {
101 $port = ':' . $_SERVER['SERVER_PORT'];
103 $url = $_SERVER['HTTP_HOST'];
105 $url = substr($_SERVER['HTTP_HOST'], 0, $pos);
106 $port = substr($_SERVER['HTTP_HOST'], $pos);
108 } else if (isset($_SERVER['SERVER_NAME'])) {
109 $url = $_SERVER['SERVER_NAME'];
110 if (isset($_SERVER['SERVER_PORT'])) {
111 $port = ':' . $_SERVER['SERVER_PORT'];
114 if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
115 $url = 'https://' . $url;
116 if ($port == ':443') {
120 $url = 'http://' . $url;
121 if ($port == ':80') {
127 if (isset($_SERVER['HTTP_X_REWRITE_URL'])) {
128 $url .= $_SERVER['HTTP_X_REWRITE_URL'];
129 } elseif (isset($_SERVER['REQUEST_URI'])) {
130 $query = strpos($_SERVER['REQUEST_URI'], '?');
131 if ($query === false) {
132 $url .= $_SERVER['REQUEST_URI'];
134 $url .= substr($_SERVER['REQUEST_URI'], 0, $query);
136 } else if (isset($_SERVER['SCRIPT_URL'])) {
137 $url .= $_SERVER['SCRIPT_URL'];
138 } else if (isset($_SERVER['REDIRECT_URL'])) {
139 $url .= $_SERVER['REDIRECT_URL'];
140 } else if (isset($_SERVER['PHP_SELF'])) {
141 $url .= $_SERVER['PHP_SELF'];
142 } else if (isset($_SERVER['SCRIPT_NAME'])) {
143 $url .= $_SERVER['SCRIPT_NAME'];
144 if (isset($_SERVER['PATH_INFO'])) {
145 $url .= $_SERVER['PATH_INFO'];
152 * Returns an absolute URL for the given one
154 * @param string $url absilute or relative URL
157 static public function absoluteUrl($url)
160 return Zend_OpenId
::selfUrl();
161 } else if (!preg_match('|^([^:]+)://|', $url)) {
162 if (preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?]*)?((?:[?](?:[^#]*))?(?:#.*)?)$|', Zend_OpenId
::selfUrl(), $reg)) {
169 if ($url[0] == '/') {
174 . (empty($port) ?
'' : (':' . $port))
177 $dir = dirname($path);
182 . (empty($port) ?
'' : (':' . $port))
183 . (strlen($dir) > 1 ?
$dir : '')
193 * Converts variable/value pairs into URL encoded query string
195 * @param array $params variable/value pairs
196 * @return string URL encoded query string
198 static public function paramsToQuery($params)
200 foreach($params as $key => $value) {
202 $query .= '&' . $key . '=' . urlencode($value);
204 $query = $key . '=' . urlencode($value);
207 return isset($query) ?
$query : '';
211 * Normalizes URL according to RFC 3986 to use it in comparison operations.
212 * The function gets URL argument by reference and modifies it.
213 * It returns true on success and false of failure.
215 * @param string &$id url to be normalized
218 static public function normalizeUrl(&$id)
220 // RFC 3986, 6.2.2. Syntax-Based Normalization
222 // RFC 3986, 6.2.2.2 Percent-Encoding Normalization
227 if ($id[$i] == '%') {
232 if ($id[$i] >= '0' && $id[$i] <= '9') {
233 $c = ord($id[$i]) - ord('0');
234 } else if ($id[$i] >= 'A' && $id[$i] <= 'F') {
235 $c = ord($id[$i]) - ord('A') +
10;
236 } else if ($id[$i] >= 'a' && $id[$i] <= 'f') {
237 $c = ord($id[$i]) - ord('a') +
10;
242 if ($id[$i] >= '0' && $id[$i] <= '9') {
243 $c = ($c << 4) |
(ord($id[$i]) - ord('0'));
244 } else if ($id[$i] >= 'A' && $id[$i] <= 'F') {
245 $c = ($c << 4) |
(ord($id[$i]) - ord('A') +
10);
246 } else if ($id[$i] >= 'a' && $id[$i] <= 'f') {
247 $c = ($c << 4) |
(ord($id[$i]) - ord('a') +
10);
253 if (($ch >= 'A' && $ch <= 'Z') ||
254 ($ch >= 'a' && $ch <= 'z') ||
262 if (($c >> 4) < 10) {
263 $res .= chr(($c >> 4) +
ord('0'));
265 $res .= chr(($c >> 4) - 10 +
ord('A'));
269 $res .= chr($c +
ord('0'));
271 $res .= chr($c - 10 +
ord('A'));
279 if (!preg_match('|^([^:]+)://([^:@]*(?:[:][^@]*)?@)?([^/:@?#]*)(?:[:]([^/?#]*))?(/[^?#]*)?((?:[?](?:[^#]*))?)((?:#.*)?)$|', $res, $reg)) {
288 $fragment = $reg[7]; /* strip it */
290 if (empty($scheme) ||
empty($host)) {
294 // RFC 3986, 6.2.2.1. Case Normalization
295 $scheme = strtolower($scheme);
296 $host = strtolower($host);
298 // RFC 3986, 6.2.2.3. Path Segment Normalization
304 if ($path[$i] == '/') {
306 while ($i < $n && $path[$i] == '/') {
309 if ($i < $n && $path[$i] == '.') {
311 if ($i < $n && $path[$i] == '.') {
313 if ($i == $n ||
$path[$i] == '/') {
314 if (($pos = strrpos($res, '/')) !== false) {
315 $res = substr($res, 0, $pos);
320 } else if ($i != $n && $path[$i] != '/') {
333 // RFC 3986,6.2.3. Scheme-Based Normalization
334 if ($scheme == 'http') {
338 } else if ($scheme == 'https') {
351 . (empty($port) ?
'' : (':' . $port))
358 * Normalizes OpenID identifier that can be URL or XRI name.
359 * Returns true on success and false of failure.
361 * Normalization is performed according to the following rules:
362 * 1. If the user's input starts with one of the "xri://", "xri://$ip*",
363 * or "xri://$dns*" prefixes, they MUST be stripped off, so that XRIs
364 * are used in the canonical form, and URI-authority XRIs are further
365 * considered URL identifiers.
366 * 2. If the first character of the resulting string is an XRI Global
367 * Context Symbol ("=", "@", "+", "$", "!"), then the input SHOULD be
369 * 3. Otherwise, the input SHOULD be treated as an http URL; if it does
370 * not include a "http" or "https" scheme, the Identifier MUST be
371 * prefixed with the string "http://".
372 * 4. URL identifiers MUST then be further normalized by both following
373 * redirects when retrieving their content and finally applying the
374 * rules in Section 6 of [RFC3986] to the final destination URL.
375 * @param string &$id identifier to be normalized
378 static public function normalize(&$id)
381 if (strlen($id) === 0) {
386 if (strpos($id, 'xri://$ip*') === 0) {
387 $id = substr($id, strlen('xri://$ip*'));
388 } else if (strpos($id, 'xri://$dns*') === 0) {
389 $id = substr($id, strlen('xri://$dns*'));
390 } else if (strpos($id, 'xri://') === 0) {
391 $id = substr($id, strlen('xri://'));
404 if (strpos($id, "://") === false) {
405 $id = 'http://' . $id;
409 return self
::normalizeURL($id);
413 * Performs a HTTP redirection to specified URL with additional data.
414 * It may generate redirected request using GET or POST HTTP method.
415 * The function never returns.
417 * @param string $url URL to redirect to
418 * @param array $params additional variable/value pairs to send
419 * @param Zend_Controller_Response_Abstract $response
420 * @param string $method redirection method ('GET' or 'POST')
422 static public function redirect($url, $params = null,
423 Zend_Controller_Response_Abstract
$response = null, $method = 'GET')
425 $url = Zend_OpenId
::absoluteUrl($url);
427 if (null === $response) {
428 require_once "Zend/Controller/Response/Http.php";
429 $response = new Zend_Controller_Response_Http();
432 if ($method == 'POST') {
433 $body = "<html><body onLoad=\"document.forms[0].submit();\">\n";
434 $body .= "<form method=\"POST\" action=\"$url\">\n";
435 if (is_array($params) && count($params) > 0) {
436 foreach($params as $key => $value) {
437 $body .= '<input type="hidden" name="' . $key . '" value="' . $value . "\">\n";
440 $body .= "<input type=\"submit\" value=\"Continue OpenID transaction\">\n";
441 $body .= "</form></body></html>\n";
442 } else if (is_array($params) && count($params) > 0) {
443 if (strpos($url, '?') === false) {
444 $url .= '?' . self
::paramsToQuery($params);
446 $url .= '&' . self
::paramsToQuery($params);
450 $response->setBody($body);
451 } else if (!$response->canSendHeaders()) {
452 $response->setBody("<script language=\"JavaScript\"" .
453 " type=\"text/javascript\">window.location='$url';" .
456 $response->setRedirect($url);
458 $response->sendResponse();
459 if (self
::$exitOnRedirect) {
465 * Produces string of random byte of given length.
467 * @param integer $len length of requested string
468 * @return string RAW random binary string
470 static public function randomBytes($len)
473 for($i=0; $i < $len; $i++
) {
474 $key .= chr(mt_rand(0, 255));
480 * Generates a hash value (message digest) according to given algorithm.
481 * It returns RAW binary string.
483 * This is a wrapper function that uses one of available internal function
484 * dependent on given PHP configuration. It may use various functions from
485 * ext/openssl, ext/hash, ext/mhash or ext/standard.
487 * @param string $func digest algorithm
488 * @param string $data data to sign
489 * @return string RAW digital signature
490 * @throws Zend_OpenId_Exception
492 static public function digest($func, $data)
494 if (function_exists('openssl_digest')) {
495 return openssl_digest($data, $func, true);
496 } else if (function_exists('hash')) {
497 return hash($func, $data, true);
498 } else if ($func === 'sha1') {
499 return sha1($data, true);
500 } else if ($func === 'sha256') {
501 if (function_exists('mhash')) {
502 return mhash(MHASH_SHA256
, $data);
505 require_once "Zend/OpenId/Exception.php";
506 throw new Zend_OpenId_Exception(
507 'Unsupported digest algorithm "' . $func . '".',
508 Zend_OpenId_Exception
::UNSUPPORTED_DIGEST
);
512 * Generates a keyed hash value using the HMAC method. It uses ext/hash
513 * if available or user-level PHP implementation, that is not significantly
516 * @param string $macFunc name of selected hashing algorithm (sha1, sha256)
517 * @param string $data data to sign
518 * @param string $secret shared secret key used for generating the HMAC
519 * variant of the message digest
520 * @return string RAW HMAC value
522 static public function hashHmac($macFunc, $data, $secret)
524 // require_once "Zend/Crypt/Hmac.php";
525 // return Zend_Crypt_Hmac::compute($secret, $macFunc, $data, Zend_Crypt_Hmac::BINARY);
526 if (function_exists('hash_hmac')) {
527 return hash_hmac($macFunc, $data, $secret, 1);
529 if (Zend_OpenId
::strlen($secret) > 64) {
530 $secret = self
::digest($macFunc, $secret);
532 $secret = str_pad($secret, 64, chr(0x00));
533 $ipad = str_repeat(chr(0x36), 64);
534 $opad = str_repeat(chr(0x5c), 64);
535 $hash1 = self
::digest($macFunc, ($secret ^
$ipad) . $data);
536 return self
::digest($macFunc, ($secret ^
$opad) . $hash1);
541 * Converts binary representation into ext/gmp or ext/bcmath big integer
544 * @param string $bin binary representation of big number
546 * @throws Zend_OpenId_Exception
548 static protected function binToBigNum($bin)
550 if (extension_loaded('gmp')) {
551 return gmp_init(bin2hex($bin), 16);
552 } else if (extension_loaded('bcmath')) {
554 $len = Zend_OpenId
::strlen($bin);
555 for ($i = 0; $i < $len; $i++
) {
556 $bn = bcmul($bn, 256);
557 $bn = bcadd($bn, ord($bin[$i]));
561 require_once "Zend/OpenId/Exception.php";
562 throw new Zend_OpenId_Exception(
563 'The system doesn\'t have proper big integer extension',
564 Zend_OpenId_Exception
::UNSUPPORTED_LONG_MATH
);
568 * Converts internal ext/gmp or ext/bcmath big integer representation into
571 * @param mixed $bn big number
573 * @throws Zend_OpenId_Exception
575 static protected function bigNumToBin($bn)
577 if (extension_loaded('gmp')) {
578 $s = gmp_strval($bn, 16);
579 if (strlen($s) %
2 != 0) {
581 } else if ($s[0] > '7') {
584 return pack("H*", $s);
585 } else if (extension_loaded('bcmath')) {
586 $cmp = bccomp($bn, 0);
589 } else if ($cmp < 0) {
590 require_once "Zend/OpenId/Exception.php";
591 throw new Zend_OpenId_Exception(
592 'Big integer arithmetic error',
593 Zend_OpenId_Exception
::ERROR_LONG_MATH
);
596 while (bccomp($bn, 0) > 0) {
597 $bin = chr(bcmod($bn, 256)) . $bin;
598 $bn = bcdiv($bn, 256);
600 if (ord($bin[0]) > 127) {
601 $bin = chr(0) . $bin;
605 require_once "Zend/OpenId/Exception.php";
606 throw new Zend_OpenId_Exception(
607 'The system doesn\'t have proper big integer extension',
608 Zend_OpenId_Exception
::UNSUPPORTED_LONG_MATH
);
612 * Performs the first step of a Diffie-Hellman key exchange by generating
613 * private and public DH values based on given prime number $p and
614 * generator $g. Both sides of key exchange MUST have the same prime number
615 * and generator. In this case they will able to create a random shared
616 * secret that is never send from one to the other.
618 * @param string $p prime number in binary representation
619 * @param string $g generator in binary representation
620 * @param string $priv_key private key in binary representation
623 static public function createDhKey($p, $g, $priv_key = null)
625 if (function_exists('openssl_dh_compute_key')) {
630 if ($priv_key !== null) {
631 $dh_details['priv_key'] = $priv_key;
633 return openssl_pkey_new(array('dh'=>$dh_details));
635 $bn_p = self
::binToBigNum($p);
636 $bn_g = self
::binToBigNum($g);
637 if ($priv_key === null) {
638 $priv_key = self
::randomBytes(Zend_OpenId
::strlen($p));
640 $bn_priv_key = self
::binToBigNum($priv_key);
641 if (extension_loaded('gmp')) {
642 $bn_pub_key = gmp_powm($bn_g, $bn_priv_key, $bn_p);
643 } else if (extension_loaded('bcmath')) {
644 $bn_pub_key = bcpowmod($bn_g, $bn_priv_key, $bn_p);
646 $pub_key = self
::bigNumToBin($bn_pub_key);
651 'priv_key' => $bn_priv_key,
652 'pub_key' => $bn_pub_key,
656 'priv_key' => $priv_key,
657 'pub_key' => $pub_key));
662 * Returns an associative array with Diffie-Hellman key components in
663 * binary representation. The array includes original prime number 'p' and
664 * generator 'g', random private key 'priv_key' and corresponding public
667 * @param mixed $dh Diffie-Hellman key
670 static public function getDhKeyDetails($dh)
672 if (function_exists('openssl_dh_compute_key')) {
673 $details = openssl_pkey_get_details($dh);
674 if (isset($details['dh'])) {
675 return $details['dh'];
678 return $dh['details'];
683 * Computes the shared secret from the private DH value $dh and the other
684 * party's public value in $pub_key
686 * @param string $pub_key other party's public value
687 * @param mixed $dh Diffie-Hellman key
689 * @throws Zend_OpenId_Exception
691 static public function computeDhSecret($pub_key, $dh)
693 if (function_exists('openssl_dh_compute_key')) {
694 $ret = openssl_dh_compute_key($pub_key, $dh);
695 if (ord($ret[0]) > 127) {
696 $ret = chr(0) . $ret;
699 } else if (extension_loaded('gmp')) {
700 $bn_pub_key = self
::binToBigNum($pub_key);
701 $bn_secret = gmp_powm($bn_pub_key, $dh['priv_key'], $dh['p']);
702 return self
::bigNumToBin($bn_secret);
703 } else if (extension_loaded('bcmath')) {
704 $bn_pub_key = self
::binToBigNum($pub_key);
705 $bn_secret = bcpowmod($bn_pub_key, $dh['priv_key'], $dh['p']);
706 return self
::bigNumToBin($bn_secret);
708 require_once "Zend/OpenId/Exception.php";
709 throw new Zend_OpenId_Exception(
710 'The system doesn\'t have proper big integer extension',
711 Zend_OpenId_Exception
::UNSUPPORTED_LONG_MATH
);
715 * Takes an arbitrary precision integer and returns its shortest big-endian
716 * two's complement representation.
718 * Arbitrary precision integers MUST be encoded as big-endian signed two's
719 * complement binary strings. Henceforth, "btwoc" is a function that takes
720 * an arbitrary precision integer and returns its shortest big-endian two's
721 * complement representation. All integers that are used with
722 * Diffie-Hellman Key Exchange are positive. This means that the left-most
723 * bit of the two's complement representation MUST be zero. If it is not,
724 * implementations MUST add a zero byte at the front of the string.
726 * @param string $str binary representation of arbitrary precision integer
727 * @return string big-endian signed representation
729 static public function btwoc($str)
731 if (ord($str[0]) > 127) {
732 return chr(0) . $str;
738 * Returns lenght of binary string in bytes
741 * @return int the string lenght
743 static public function strlen($str)
745 if (extension_loaded('mbstring') &&
746 (((int)ini_get('mbstring.func_overload')) & 2)) {
747 return mb_strlen($str, 'latin1');