blob: 85078049735f334dcf784a5a86dceea606ad22a6 [file] [log] [blame]
Nico Huberee52fbc2023-06-24 11:52:57 +00001<?php
2/*
3 * SimpleID
4 *
5 * Copyright (C) Kelvin Mo 2007-10
6 *
7 * Includes code Drupal OpenID module (http://drupal.org/project/openid)
8 * Rowan Kerr <rowan@standardinteractive.com>
9 * James Walker <james@bryght.com>
10 *
11 * Copyright (C) Rowan Kerr and James Walker
12 *
13 * This program is free software; you can redistribute it and/or
14 * modify it under the terms of the GNU General Public
15 * License as published by the Free Software Foundation; either
16 * version 2 of the License, or (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 * General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public
24 * License along with this program; if not, write to the Free
25 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
26 *
27 * $Id$
28 */
29
30/**
31 * OpenID related functions.
32 *
33 * @package simpleid
34 * @filesource
35 */
36
37include_once "bignum.inc.php";
38include_once "random.inc.php";
39
40/**
41 * OpenID default modulus for Diffie-Hellman key exchange.
42 *
43 * @link http://openid.net/specs/openid-authentication-1_1.html#pvalue, http://openid.net/specs/openid-authentication-2_0.html#pvalue
44 */
45define('OPENID_DH_DEFAULT_MOD', '155172898181473697471232257763715539915724801'.
46 '966915404479707795314057629378541917580651227423698188993727816152646631'.
47 '438561595825688188889951272158842675419950341258706556549803580104870537'.
48 '681476726513255747040765857479291291572334510643245094715007229621094194'.
49 '349783925984760375594985848253359305585439638443');
50
51/**
52 * OpenID default generator for Diffie-Hellman key exchange.
53 */
54define('OPENID_DH_DEFAULT_GEN', '2');
55
56/** Constant for the global variable {@link $version} */
57define('OPENID_VERSION_2', 2);
58/** Constant for the global variable {@link $version} */
59define('OPENID_VERSION_1_1', 1);
60
61/** Constant for OpenID namespace */
62define('OPENID_NS_2_0', 'http://specs.openid.net/auth/2.0');
63/** Constant for OpenID namespace */
64define('OPENID_NS_1_1', 'http://openid.net/signon/1.1');
65/** Constant for OpenID namespace */
66define('OPENID_NS_1_0', 'http://openid.net/signon/1.0');
67
68/**
69 * Constant for the OP-local identifier which indicates that SimpleID should choose an identifier
70 *
71 * @link http://openid.net/specs/openid-authentication-2_0.html#anchor27
72 */
73define('OPENID_IDENTIFIER_SELECT', 'http://specs.openid.net/auth/2.0/identifier_select');
74/** Constant for the XRDS service type for return_to verification */
75define('OPENID_RETURN_TO', 'http://specs.openid.net/auth/2.0/return_to');
76
77/** Parameter for {@link openid_indirect_response_url()} */
78define('OPENID_RESPONSE_QUERY', 0);
79/** Parameter for {@link openid_indirect_response_url()} */
80define('OPENID_RESPONSE_FRAGMENT', 1);
81
82/**
83 * A mapping of Type URIs of OpenID extnesions to aliases provided in an OpenID
84 * request.
85 *
86 * @global array $openid_ns_to_alias
87 */
88$openid_ns_to_alias = array("http://openid.net/extensions/sreg/1.1" => "sreg"); // For sreg 1.0 compatibility
89
90
91/**
92 * Detects the OpenID version of the current request
93 *
94 * @param mixed $request the OpenID request
95 * @param string $key the key to look for to determine the OpenID
96 * version
97 * @return float either OPENID_VERSION_2 or OPENID_VERSION_1_1
98 * @see $version
99 *
100 */
101function openid_get_version($request, $key = 'openid.ns') {
102 if (!isset($request[$key])) return OPENID_VERSION_1_1;
103 if ($request[$key] != OPENID_NS_2_0) return OPENID_VERSION_1_1;
104 return OPENID_VERSION_2;
105}
106
107/**
108 * Creates a OpenID message for direct response.
109 *
110 * The response will be encoded using Key-Value Form Encoding.
111 *
112 * @param array $data the data in the response
113 * @param float $version the message version
114 * @return string the message in key-value form encoding
115 * @link http://openid.net/specs/openid-authentication-1_1.html#anchor32, http://openid.net/specs/openid-authentication-2_0.html#kvform
116 */
117function openid_direct_message($data, $version = OPENID_VERSION_2) {
118 $message = '';
119 $ns = '';
120
121 // Add namespace for OpenID 2
122 if ($version == OPENID_VERSION_2) $ns = OPENID_NS_2_0;
123 if (($ns != '') && !isset($data['ns'])) $data['ns'] = $ns;
124
125 foreach ($data as $key => $value) {
126 // Filter out invalid characters
127 if (strpos($key, ':') !== false) return null;
128 if (strpos($key, "\n") !== false) return null;
129 if (strpos($value, "\n") !== false) return null;
130
131 $message .= "$key:$value\n";
132 }
133 return $message;
134}
135
136/**
137 * Sends a direct response.
138 *
139 * @param string $message an OpenID message encoded using Key-Value Form
140 * @param string $status the HTTP status to send
141 */
142function openid_direct_response($message, $status = '200 OK') {
143 if (substr(PHP_SAPI, 0, 3) === 'cgi') {
144 header("Status: $status");
145 } else {
146 header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status);
147 }
148
149 header("Content-Type: text/plain");
150 print $message;
151}
152
153/**
154 * Creates a OpenID message for indirect response.
155 *
156 * The response will be encoded using HTTP Encoding.
157 *
158 * @param array $data the data in the response
159 * @param float $version the message version
160 * @return array the message
161 * @link http://openid.net/specs/openid-authentication-2_0.html#indirect_comm
162 */
163function openid_indirect_message($data, $version = OPENID_VERSION_2) {
164 $ns = '';
165
166 // Add namespace for OpenID 2
167 if ($version == OPENID_VERSION_2) $ns = OPENID_NS_2_0;
168 if (($ns != '') && !isset($data['openid.ns'])) $data['openid.ns'] = $ns;
169
170 return $data;
171}
172
173/**
174 * Sends an indirect response to a URL.
175 *
176 * The indirect message is encoded in the URL and returned to the user agent using
177 * a HTTP redirect response. The message can be encoded in either the query component
178 * or the fragment component of the URL.
179 *
180 * @param string $url the URL to which the response is to be sent
181 * @param array|string $message an OpenID message, which can either be an array of keys
182 * and values, or a URL-encoded query string
183 * @param int $component the component of the URL in which the indirect message is
184 * encoded, either OPENID_RESPONSE_QUERY or OPENID_RESPONSE_FRAGMENT
185 */
186function openid_indirect_response($url, $message, $component = OPENID_RESPONSE_QUERY) {
187 if (substr(PHP_SAPI, 0,3) === 'cgi') {
188 header('Status: 303 See Other');
189 } else {
190 header($_SERVER['SERVER_PROTOCOL'] . ' 303 See Other');
191 }
192
193 header('Location: ' . openid_indirect_response_url($url, $message, $component));
194 exit;
195}
196
197/**
198 * Encodes an indirect message into a URL
199 *
200 * @param string $url the URL to which the response is to be sent
201 * @param array|string $message an OpenID message, which can either be an array of keys
202 * and values, or a URL-encoded query string
203 * @param int $component the component of the URL in which the indirect message is
204 * encoded, either OPENID_RESPONSE_QUERY or OPENID_RESPONSE_FRAGMENT
205 * @return string the URL to which the response is to be sent, with the
206 * encoded message
207 */
208function openid_indirect_response_url($url, $message, $component = OPENID_RESPONSE_QUERY) {
209 // 1. Firstly, get the query string
210 $query = '';
211
212 if (is_array($message)) {
213 $query = openid_urlencode_message($message);
214 } else {
215 $query = $message;
216 }
217
218 // 2. If there is no query string, then we just return the URL
219 if (!$query) return $url;
220
221 // 3. The URL may already have a query and a fragment. If this is so, we
222 // need to slot in the new query string properly. We disassemble and
223 // reconstruct the URL.
224 $parts = parse_url($url);
225
226 $url = $parts['scheme'] . '://';
227 if (isset($parts['user'])) {
228 $url .= $parts['user'];
229 if (isset($parts['pass'])) $url .= ':' . $parts['pass'];
230 $url .= '@';
231 }
232 $url .= $parts['host'];
233 if (isset($parts['port'])) $url .= ':' . $parts['port'];
234 if (isset($parts['path'])) $url .= $parts['path'];
235
236 if (($component == OPENID_RESPONSE_QUERY) || (strpos($url, '#') === FALSE)) {
237 $url .= '?' . ((isset($parts['query'])) ? $parts['query'] . '&' : '') . $query;
238 if (isset($parts['fragment'])) $url .= '#' . $parts['fragment'];
239 } elseif ($component == OPENID_RESPONSE_FRAGMENT) {
240 // In theory $parts['fragment'] should be an empty string, but the
241 // current draft specification does not prohibit putting other things
242 // in the fragment.
243
244 if (isset($parts['query'])) {
245 $url .= '?' . $parts['query'] . '#' . $parts['fragment'] . '&' . $query;
246 } else {
247 $url .= '#' . $parts['fragment'] . '?' . $query;
248 }
249 }
250 return $url;
251}
252
253/**
254 * Encodes a message in application/x-www-form-urlencoded format.
255 *
256 * @param array $message the OpenID message to encode
257 * @return string the encoded message
258 * @since 0.8
259 */
260function openid_urlencode_message($message) {
261 $pairs = array();
262
263 foreach ($message as $key => $value) {
264 $pairs[] = $key . '=' . rfc3986_urlencode($value);
265 }
266
267 return implode('&', $pairs);
268}
269
270/**
271 * Sends a direct message indicating an error. This is a convenience function
272 * for {@link openid_direct_response()}.
273 *
274 * @param string $error the error message
275 * @param array $additional any additional data to be sent with the error
276 * message
277 * @param float $version the message version
278 */
279function openid_direct_error($error, $additional = array(), $version = OPENID_VERSION_2) {
280 $message = openid_direct_message(array_merge(array('error' => $error), $additional), $version);
281 openid_direct_response($message, '400 Bad Request');
282}
283
284/**
285 * Sends an indirect message indicating an error. This is a convenience function
286 * for {@link openid_indirect_response()}.
287 *
288 * @param string $url the URL to which the error message is to be sent
289 * @param string $error the error message
290 * @param array $additional any additional data to be sent with the error
291 * message
292 * @param float $version the message version
293 * @param int $component the component of the URL in which the indirect message is
294 * encoded, either OPENID_RESPONSE_QUERY or OPENID_RESPONSE_FRAGMENT
295 */
296function openid_indirect_error($url, $error, $additional = array(), $version = OPENID_VERSION_2, $component = OPENID_RESPONSE_QUERY) {
297 $message = openid_indirect_message(array_merge(array('openid.mode'=> 'error', 'openid.error' => $error), $additional), $version);
298 openid_indirect_response($url, $message, $component);
299}
300
301/**
302 * Gets the realm from the OpenID request. This is specified differently
303 * depending on the OpenID version.
304 *
305 * @param mixed $request the OpenID request
306 * @param float $version the OpenID version for the message
307 * @return string the realm URI
308 */
309function openid_get_realm($request, $version) {
310 if ($version == OPENID_VERSION_1_1) {
311 $realm = $request['openid.trust_root'];
312 }
313
314 if ($version >= OPENID_VERSION_2) {
315 $realm = $request['openid.realm'];
316 }
317
318 if (!$realm) {
319 $realm = $request['openid.return_to'];
320 }
321
322 return $realm;
323}
324
325/**
326 * Parses a direct message.
327 *
328 * @param string $message the direct message to parse
329 * @return array an array containing the parsed key-value pairs
330 *
331 * @since 0.7
332 */
333function openid_parse_direct_message($message) {
334 $data = array();
335
336 $items = explode("\n", $message);
337 foreach ($items as $item) {
338 list ($key, $value) = explode(':', $item, 2);
339 $data[$key] = $value;
340 }
341
342 return $data;
343}
344
345/**
346 * Parses a query string.
347 *
348 * Query strings can be used to receive OpenID indirect messages.
349 *
350 * @param string $query the query string to parse
351 * @return array an array containing the parsed key-value pairs
352 *
353 * @since 0.7
354 */
355function openid_parse_query($query) {
356 $data = array();
357
358 if ($query === NULL) return array();
359 if ($query === '') return array();
360
361 $pairs = explode('&', $query);
362
363 foreach ($pairs as $pair) {
364 list ($key, $value) = explode('=', $pair, 2);
365 $data[$key] = urldecode($value);
366 }
367
368 return $data;
369}
370
371/**
372 * Parses the OpenID request to extract namespace information.
373 *
374 * This function builds a map between namespace aliases and their Type URIs.
375 *
376 * @param array $request the OpenID request
377 */
378function openid_parse_request($request) {
379 global $openid_ns_to_alias;
380
381 foreach ($request as $key => $value) {
382 if (strpos($key, 'openid.ns.') === 0) {
383 $alias = substr($key, 10);
384 $openid_ns_to_alias[$value] = $alias;
385 }
386 }
387}
388
389/**
390 * Determines whether a URL matches a realm.
391 *
392 * A URL matches a realm if:
393 *
394 * 1. The URL scheme and port of the URL are identical to those in the realm.
395 * See RFC 3986, section 3.1 for rules about URI matching.
396 * 2. The URL's path is equal to or a sub-directory of the realm's path.
397 * 3. Either:
398 * (a) The realm's domain contains the wild-card characters "*.", and the
399 * trailing part of the URL's domain is identical to the part of the
400 * realm following the "*." wildcard, or
401 * (b) The URL's domain is identical to the realm's domain
402 *
403 * @param string $url to URL to test
404 * @param string $realm the realm
405 * @return bool true if the URL matches the realm
406 * @since 0.6
407 */
408function openid_url_matches_realm($url, $realm) {
409 $url = parse_url($url);
410 $realm = parse_url($realm);
411
412 foreach(array('user', 'pass', 'fragment') as $key) {
413 if (array_key_exists($key, $url) || array_key_exists($key, $realm))
414 return false;
415 }
416
417 if ($url['scheme'] != $realm['scheme']) return false;
418
419 if (!isset($url['port']))
420 $url['port'] = '';
421 if (!isset($realm['port']))
422 $realm['port'] = '';
423 if (($url['port'] != $realm['port']))
424 return false;
425
426 if (substr($realm['host'], 0, 2) == '*.') {
427 $realm_re = '/^([^.]+\.)?' . preg_quote(substr($realm['host'], 2)) . '$/i';
428 } else {
429 $realm_re = '/^' . preg_quote($realm['host']) . '$/i';
430 }
431
432 if (!preg_match($realm_re, $url['host'])) return false;
433
434 if (!isset($url['path']))
435 $url['path'] = '';
436 if (!isset($realm['path']))
437 $realm['path'] = '';
438 if (substr($realm['path'], -1) == '/') $realm['path'] = substr($realm['path'], 0, -1);
439 if (($url['path'] != $realm['path']) && !preg_match('#^' . preg_quote($realm['path']) . '/.*$#', $url['path'])) return false;
440
441 return true;
442}
443
444/**
445 * Returns the URL of a relying party endpoint for a specified realm. This URL
446 * is used to discover services associated with the realm.
447 *
448 * If the realm's domain contains the wild-card characters "*.", this is substituted
449 * with "www.".
450 *
451 * @param string $realm the realm
452 * @url string the URL
453 *
454 * @since 0.7
455 */
456function openid_realm_discovery_url($realm) {
457 $parts = parse_url($realm);
458 $host = strtr($parts['host'], array('*.' => 'www.'));;
459
460 $url = $parts['scheme'] . '://';
461 if (isset($parts['user'])) {
462 $url .= $parts['user'];
463 if (isset($parts['pass'])) $url .= ':' . $parts['pass'];
464 $url .= '@';
465 }
466 $url .= $host;
467 if (isset($parts['port'])) $url .= ':' . $parts['port'];
468 if (isset($parts['path'])) $url .= $parts['path'];
469 if (isset($parts['query'])) $url .= '?' . $parts['query'];
470 if (isset($parts['fragment'])) $url .= '#' . $parts['fragment'];
471 return $url;
472}
473
474/**
475 * Verifies a return_to URL against the actual URL of the HTTP request.
476 *
477 * The return_to URL matches if:
478 *
479 * - The URL scheme, authority, and path are the same; and
480 * - Any query parameters that are present in the return_to URL are also present
481 * with the same values in the actual request.
482 *
483 * @param string $return_to the URL specified in the openid.return_to parameter
484 * @param string $actual_url the actual URL requested
485 * @return bool true if the URLs match
486 *
487 * @since 0.7
488 */
489function openid_verify_return_to($return_to, $actual_url) {
490 $expected = parse_url($return_to);
491 $actual = parse_url($actual_url);
492
493 // Schemes are case insensitive
494 if (strtoupper($expected['scheme']) != strtoupper($actual['scheme'])) return false;
495
496 // Hosts are case insensitive
497 if (strtoupper($expected['host']) != strtoupper($actual['host'])) return false;
498
499 if (!isset($expected['port']))
500 $expected['port'] = '';
501 if (!isset($actual['port']))
502 $actual['port'] = '';
503 if ($expected['port'] != $actual['port']) return false;
504
505 if (!isset($expected['path']))
506 $expected['path'] = '';
507 if (!isset($actual['path']))
508 $actual['path'] = '';
509 if ($expected['path'] != $actual['path']) return false;
510
511 if ($expected['query']) {
512 $expected_query = openid_parse_query($expected['query']);
513 $actual_query = openid_parse_query($actual['query']);
514
515 foreach ($expected_query as $key => $value) {
516 if (!array_key_exists($key, $actual_query)) return false;
517 if ($value != $actual_query[$key]) return false;
518 }
519 }
520
521 return true;
522}
523
524/**
525 * Filters an OpenID request to find keys specific to an extension, as specified
526 * by the Type URI.
527 *
528 * For exmaple, if the extension has the Type URI http://example.com/ and the
529 * alias example, this function will return an array of all the keys in the
530 * OpenID request which starts with openid.example
531 *
532 * @param string $ns the Type URI of the extension
533 * @param array $request the OpenID request
534 * @return array the filtered request, with the prefix (in the example above,
535 * openid.example.) stripped in the keys.
536 */
537function openid_extension_filter_request($ns, $request) {
538 global $openid_ns_to_alias;
539
540 if (!isset($openid_ns_to_alias[$ns])) return array();
541
542 $alias = $openid_ns_to_alias[$ns];
543 $return = array();
544
545 if (is_array($request)) {
546 foreach ($request as $key => $value) {
547 if ($key == 'openid.' . $alias) {
548 $return['#default'] = $value;
549 }
550 if (strpos($key, 'openid.' . $alias . '.') === 0) {
551 $return[substr($key, strlen('openid.' . $alias . '.'))] = $value;
552 }
553 }
554 }
555
556 return $return;
557}
558
559/**
560 * Determines whether an extension is present in an OpenID request.
561 *
562 * @param string $ns the Type URI of the extension
563 * @param array $request the OpenID request
564 * @return bool true if the extension is present in the request
565 */
566function openid_extension_requested($ns, $request) {
567 global $openid_ns_to_alias;
568
569 if (!isset($openid_ns_to_alias[$ns])) return false;
570 $alias = $openid_ns_to_alias[$ns];
571
572 if (is_array($request)) {
573 foreach ($request as $key => $value) {
574 if ((strpos($key, 'openid.' . $alias . '.') === 0) || (strpos($key, 'openid.' . $alias . '=') === 0)) {
575 return true;
576 }
577 }
578 }
579
580 return false;
581}
582
583/**
584 * Returns the OpenID alias for an extension, given a Type URI, based on the
585 * alias definitions in the current OpenID request.
586 *
587 * @param string $ns the Type URI
588 * @param bool|string $create whether to create an alias if the Type URI does not already
589 * have an alias in the current OpenID request. If this parameter is a string,
590 * then the string specified is the preferred alias to be created, unless a collision
591 * occurs
592 * @return string the alias, or NULL if the Type URI does not already
593 * have an alias in the current OpenID request <i>and</i> $create is false
594 */
595function openid_extension_alias($ns, $create = FALSE) {
596 global $openid_ns_to_alias;
597 static $e = 1;
598
599 if (isset($openid_ns_to_alias[$ns])) return $openid_ns_to_alias[$ns];
600 if ($create !== FALSE) {
601 if ($create === TRUE) {
602 $alias = 'e' . $e;
603 $e++;
604 } elseif (is_string($create)) {
605 $used_aliases = array_values($openid_ns_to_alias);
606
607 $alias = $create;
608 $i = 0;
609
610 while (in_array($alias, $used_aliases)) {
611 $i++;
612 $alias = $create . $i;
613 }
614 }
615 $openid_ns_to_alias[$ns] = $alias;
616 return $alias;
617 }
618 return NULL;
619}
620
621
622/* ------- OpenID nonce functions -------------------------------------------- */
623/**
624 * Generates a nonce for use in OpenID responses
625 *
626 * @return string an OpenID nonce
627 * @link http://openid.net/specs/openid-authentication-2_0.html#positive_assertions
628 */
629function openid_nonce() {
630 return gmstrftime('%Y-%m-%dT%H:%M:%SZ') . bin2hex(random_bytes(4));
631}
632
633/* ------- Diffie-Hellman Key Exchange functions ----------------------------- */
634
635/**
636 * Returns the association types supported by this server.
637 *
638 * @return array an array containing the association types supported by this server as keys
639 * and an array containing the key size (mac_size) and HMAC function (hmac_func) as
640 * values
641 */
642function openid_association_types() {
643 $association_types = array('HMAC-SHA1' => array('mac_size' => 20, 'hmac_func' => '_openid_hmac_sha1'));
644 if (OPENID_SHA256_SUPPORTED) $association_types['HMAC-SHA256'] = array('mac_size' => 32, 'hmac_func' => '_openid_hmac_sha256');
645 return $association_types;
646}
647
648/**
649 * Returns the association types supported by this server and the version of
650 * OpenID.
651 *
652 * OpenID version 1 supports an empty string as the session type. OpenID version 2
653 * reqires a session type to be sent.
654 *
655 * @param bool $is_https whether the transport layer encryption is used for the current
656 * connection
657 * @param float $version the OpenID version, either OPENID_VERSION_1_1 and OPENID_VERSION_2
658 * @return array an array containing the session types supported by this server as keys
659 * and an array containing the hash function (hash_func) as
660 * values
661 */
662function openid_session_types($is_https = FALSE, $version = OPENID_VERSION_2) {
663 $session_types = array(
664 'DH-SHA1' => array('hash_func' => '_openid_sha1'),
665 );
666 if (OPENID_SHA256_SUPPORTED) $session_types['DH-SHA256'] = array('hash_func' => '_openid_sha256');
667 if (($version >= OPENID_VERSION_2) && ($is_https == TRUE)) {
668 // Under OpenID 2.0 no-encryption is only allowed if TLS is used
669 $session_types['no-encryption'] = array();
670 }
671 if ($version == OPENID_VERSION_1_1) $session_types[''] = array();
672 return $session_types;
673}
674
675/**
676 * Generates the cryptographic values required for responding to association
677 * requests
678 *
679 * This involves generating a key pair for the OpenID provider, then calculating
680 * the shared secret. The shared secret is then used to encrypt the MAC key.
681 *
682 * @param string $mac_key the MAC key, in binary representation
683 * @param string $dh_consumer_public the consumer's public key, in Base64 representation
684 * @param string $dh_modulus modulus - a large prime number
685 * @param string $dh_gen generator - a primitive root modulo
686 * @param string $hash_func the hash function
687 * @return array an array containing (a) dh_server_public - the server's public key (in Base64), and (b)
688 * enc_mac_key encrypted MAC key (in Base64), encrypted using the Diffie-Hellman shared secret
689 */
690function openid_dh_server_assoc($mac_key, $dh_consumer_public, $dh_modulus = NULL, $dh_gen = NULL, $hash_func = '_openid_sha1') {
691
692 // Generate a key pair for the server
693 $key_pair = openid_dh_generate_key_pair($dh_modulus, $dh_gen);
694
695 // Generate the shared secret
696 $ZZ = openid_dh_shared_secret($dh_consumer_public, $key_pair['private'], $dh_modulus);
697
698 return array(
699 'dh_server_public' => $key_pair['public'],
700 'enc_mac_key' => openid_encrypt_mac_key($ZZ, $mac_key, $hash_func)
701 );
702}
703
704/**
705 * Complete association by obtaining the session MAC key from the key obtained
706 * from the Diffie-Hellman key exchange
707 *
708 * @param string $enc_mac_key the encrypted session MAC key, in Base64 represnetation
709 * @param string $dh_server_public the server's public key, in Base64 representation
710 * @param string $dh_consumer_private the consumer's private key, in Base64 representation
711 * @param string $dh_modulus modulus, in Base64 representation
712 * @param string $hash_func the hash function
713 * @return string the decrypted session MAC key, in Base64 representation
714 */
715function openid_dh_consumer_assoc($enc_mac_key, $dh_server_public, $dh_consumer_private, $dh_modulus = NULL, $hash_func = '_openid_sha1') {
716 // Retrieve the shared secret
717 $ZZ = openid_dh_shared_secret($dh_server_public, $dh_consumer_private, $dh_modulus);
718
719 // Decode the encrypted MAC key
720 $encrypted_mac_key = base64_decode($enc_mac_key);
721
722 return openid_encrypt_mac_key($ZZ, $encrypted_mac_key, $hash_func);
723}
724
725/**
726 * Calculates the shared secret for Diffie-Hellman key exchange.
727 *
728 * This is the second step in the Diffle-Hellman key exchange process. The other
729 * party (in OpenID 1.0 terms, the consumer) has already generated the public
730 * key ($dh_consumer_public) and sent it to this party (the server). The Diffie-Hellman
731 * modulus ($dh_modulus) and generator ($dh_gen) have either been sent or previously agreed.
732 *
733 * @param string $their_public the other party's public key, in Base64 representation
734 * @param string $my_private this party's private key, in Base64 representation
735 * @param string $dh_modulus modulus, in Base64 representation
736 * @return resource the shared secret (as a bignum)
737 *
738 * @see openid_dh_generate_key_pair()
739 * @link http://www.ietf.org/rfc/rfc2631.txt RFC 2631
740 */
741function openid_dh_shared_secret($their_public, $my_private, $dh_modulus = NULL) {
742 // Decode the keys
743 $y = _openid_base64_to_bignum($their_public);
744 $x = _openid_base64_to_bignum($my_private);
745
746 if ($dh_modulus != NULL) {
747 $p = _openid_base64_to_bignum($dh_modulus);
748 } else {
749 $p = bignum_new(OPENID_DH_DEFAULT_MOD);
750 }
751
752 // Generate the shared secret = their public ^ my private mod p = my public ^ their private mod p
753 $ZZ = bignum_powmod($y, $x, $p);
754
755 return $ZZ;
756}
757
758/**
759 * Generates a key pair for Diffie-Hellman key exchange.
760 *
761 * @param string $dh_modulus modulus, in Base64 representation
762 * @param string $dh_gen generator, in Base64 representation
763 * @return array an array containing: (a) private - the private key, in Base64
764 * and (b) public - the public key, in Base64
765 */
766function openid_dh_generate_key_pair($dh_modulus = NULL, $dh_gen = NULL) {
767 if ($dh_modulus != NULL) {
768 $p = _openid_base64_to_bignum($dh_modulus);
769 } else {
770 $p = bignum_new(OPENID_DH_DEFAULT_MOD);
771 }
772
773 if ($dh_gen != NULL) {
774 $g = _openid_base64_to_bignum($dh_gen);
775 } else {
776 $g = bignum_new(OPENID_DH_DEFAULT_GEN);
777 }
778
779 // Generate the private key - a random number which is less than p
780 $rand = _openid_dh_rand($p);
781 $x = bignum_add($rand, 1);
782
783 // Calculate the public key is g ^ private mod p
784 $y = bignum_powmod($g, $x, $p);
785
786 return array('private' => _openid_bignum_to_base64($x), 'public' => _openid_bignum_to_base64($y));
787}
788
789
790/**
791 * Encrypts/decrypts and encodes the MAC key.
792 *
793 * @param resource $ZZ the Diffie-Hellman key exchange shared secret as a bignum
794 * @param string $mac_key a byte stream containing the MAC key
795 * @param string $hash_func the hash function
796 * @return string the encrypted MAC key in Base64 representation
797 */
798function openid_encrypt_mac_key($ZZ, $mac_key, $hash_func = '_openid_sha1') {
799 // Encrypt/decrypt the MAC key using the shared secret and the hash function
800 $encrypted_mac_key = _openid_xor($ZZ, $mac_key, $hash_func);
801
802 // Encode the encrypted/decrypted MAC key
803 $enc_mac_key = base64_encode($encrypted_mac_key);
804
805 return $enc_mac_key;
806}
807
808/**
809 * Encrypts/decrypts using XOR.
810 *
811 * @param string $key the encryption key as a bignum. This is usually
812 * the shared secret (ZZ) calculated from the Diffie-Hellman key exchange
813 * @param string $plain_cipher the plaintext or ciphertext
814 * @param string $hash_func the hash function
815 * @return string the ciphertext or plaintext
816 */
817function _openid_xor($key, $plain_cipher, $hash_func = '_openid_sha1') {
818 $decoded_key = bignum_val($key, 256);
819 $hashed_key = call_user_func($hash_func, $decoded_key);
820
821 $cipher_plain = "";
822 for ($i = 0; $i < strlen($plain_cipher); $i++) {
823 $cipher_plain .= chr(ord($plain_cipher[$i]) ^ ord($hashed_key[$i]));
824 }
825
826 return $cipher_plain;
827}
828
829/**
830 * Generates a random integer, which will be used to derive a private key
831 * for Diffie-Hellman key exchange. The integer must be less than $stop
832 *
833 * @param resource $stop a prime number as a bignum
834 * @return resource the random integer as a bignum
835 */
836function _openid_dh_rand($stop) {
837 static $duplicate_cache = array();
838
839 // Used as the key for the duplicate cache
840 $rbytes = bignum_val($stop, 256);
841
842 if (array_key_exists($rbytes, $duplicate_cache)) {
843 list($duplicate, $nbytes) = $duplicate_cache[$rbytes];
844 } else {
845 if ($rbytes[0] == "\x00") {
846 $nbytes = strlen($rbytes) - 1;
847 } else {
848 $nbytes = strlen($rbytes);
849 }
850
851 $mxrand = bignum_pow(bignum_new(256), $nbytes);
852
853 // If we get a number less than this, then it is in the
854 // duplicated range.
855 $duplicate = bignum_mod($mxrand, $stop);
856
857 if (count($duplicate_cache) > 10) {
858 $duplicate_cache = array();
859 }
860
861 $duplicate_cache[$rbytes] = array($duplicate, $nbytes);
862 }
863
864 do {
865 $bytes = "\x00" . random_bytes($nbytes);
866 $n = bignum_new($bytes, 256);
867 // Keep looping if this value is in the low duplicated range
868 } while (bignum_cmp($n, $duplicate) < 0);
869
870 return bignum_mod($n, $stop);
871}
872
873/* ------- Arbitary precision arithmetic and conversion functions ------------ */
874/**
875 * Converts an arbitary precision integer, encoded in Base64, to a bignum
876 *
877 * @param string $str arbitary precision integer, encoded in Base64
878 * @return resource the string representation
879 */
880function _openid_base64_to_bignum($str) {
881 return bignum_new(base64_decode($str), 256);
882}
883
884/**
885 * Converts a string representation of an integer to an arbitary precision
886 * integer, then converts it to Base64 encoding.
887 *
888 * @param string $str the string representation
889 * @return string the Base64 encoded arbitary precision integer
890 */
891function _openid_bignum_to_base64($str) {
892 return base64_encode(bignum_val($str, 256));
893}
894
895/**
896 * Encode an integer as big-endian signed two's complement binary string.
897 *
898 * @param string $num the binary integer
899 * @return string the signed two's complement binary string
900 * @link http://openid.net/specs/openid-authentication-2_0.html#btwoc
901 */
902function _openid_btwoc($num) {
903 return pack('H*', $num);
904}
905
906/* ------- Hash and HMAC functions ------------------------------------------- */
907/**
908 * Calculates a signature of an OpenID message
909 *
910 * @param array $data the data in the message
911 * @param array $keys a list of keys in the message to be signed (without the
912 * 'openid.' prefix)
913 * @param string $mac_key the MAC key used to sign the message, in Base64 representation
914 * @param string $hmac_func the HMAC function used in the signing process
915 * @param float $version the OpenID version
916 * @return string the signature encoded in Base64
917 */
918function openid_sign($data, $keys, $mac_key, $hmac_func = '_openid_hmac_sha1', $version = OPENID_VERSION_2) {
919 $signature = '';
920 $sign_data = array();
921
922 foreach ($keys as $key) {
923 if (array_key_exists('openid.' . $key, $data)) {
924 $sign_data[$key] = $data['openid.' . $key];
925 }
926 }
927
928 $signature_base_string = _openid_signature_base_string($sign_data, $version);
929 $secret = base64_decode($mac_key);
930 $signature = call_user_func($hmac_func, $secret, $signature_base_string);
931
932 return base64_encode($signature);
933}
934
935/**
936 * Calculates the base string from which an OpenID signature is generated.
937 *
938 * OpenID versions 1 and 2 specify that messages are to be encoded using Key-Value
939 * Encoding when generating signatures. However, future OpenID version may
940 * specify different ways of encoding the message, such as OAuth.
941 *
942 * @param array $data the data to sign
943 * @param float $version the OpenID version
944 * @return string the signature base string
945 * @link http://openid.net/specs/openid-authentication-2_0.html#anchor11
946 */
947function _openid_signature_base_string($data, $version) {
948 switch ($version) {
949 case OPENID_VERSION_1_1:
950 case OPENID_VERSION_2:
951 // We set OPENID_VERSION_1_1 because we don't want to sign the namespace header
952 $signature_base_string = openid_direct_message($data, OPENID_VERSION_1_1);
953 break;
954 default:
955 // We set OPENID_VERSION_1_1 because we don't want to sign the namespace header
956 $signature_base_string = openid_direct_message($data, OPENID_VERSION_1_1);
957 }
958 return $signature_base_string;
959}
960
961/**
962 * Obtains the SHA1 hash of a string in binary representation.
963 *
964 * @param string $text the text to be hashed
965 * @return string the hash in binary representation
966 */
967function _openid_sha1($text) {
968 return sha1($text, true);
969}
970
971/**
972 * Obtains the keyed hash value using the HMAC method and the SHA1 algorithm
973 *
974 * @param string $key the key in binary representation
975 * @param string $text the text to be hashed
976 * @return string the hash in binary representation
977 */
978function _openid_hmac_sha1($key, $text) {
979 if (function_exists('hash_hmac') && function_exists('hash_algos') && (in_array('sha1', hash_algos()))) {
980 return hash_hmac('sha1', $text, $key, true);
981 } else {
982 if (!defined('OPENID_SHA1_BLOCKSIZE')) define('OPENID_SHA1_BLOCKSIZE', 64);
983
984 if (strlen($key) > OPENID_SHA1_BLOCKSIZE) {
985 $key = _openid_sha1($key);
986 }
987
988 $key = str_pad($key, OPENID_SHA1_BLOCKSIZE, chr(0x00));
989 $ipad = str_repeat(chr(0x36), OPENID_SHA1_BLOCKSIZE);
990 $opad = str_repeat(chr(0x5c), OPENID_SHA1_BLOCKSIZE);
991 $hash1 = _openid_sha1(($key ^ $ipad) . $text);
992 $hmac = _openid_sha1(($key ^ $opad) . $hash1);
993 return $hmac;
994 }
995}
996
997// Check if SHA-256 support is available
998if (function_exists('hash_hmac') && function_exists('hash_algos') && (in_array('sha256', hash_algos()))) {
999
1000 /**
1001 * Whether the current installation of PHP supports SHA256. SHA256 is supported
1002 * if the hash module is properly compiled and loaded into PHP.
1003 */
1004 define('OPENID_SHA256_SUPPORTED', true);
1005
1006 /**
1007 * Obtains the SHA256 hash of a string in binary representation.
1008 *
1009 * @param string $text the text to be hashed
1010 * @return string $hash the hash in binary representation
1011 */
1012 function _openid_sha256($text) {
1013 return hash('sha256', $text, true);
1014 }
1015
1016 /**
1017 * Obtains the keyed hash value using the HMAC method and the SHA256 algorithm
1018 *
1019 * @param string $key the key in binary representation
1020 * @param string $text the text to be hashed
1021 * @return string the hash in binary representation
1022 */
1023 function _openid_hmac_sha256($key, $text) {
1024 return hash_hmac('sha256', $text, $key, true);
1025 }
1026} else {
1027 /** @ignore */
1028 define('OPENID_SHA256_SUPPORTED', false);
1029}
1030
1031if (!function_exists('rfc3986_urlencode')) {
1032 /**
1033 * Encodes a URL using RFC 3986.
1034 *
1035 * PHP's rfc3986_urlencode function encodes a URL using RFC 1738 for PHP versions
1036 * prior to 5.3. RFC 1738 has been
1037 * updated by RFC 3986, which change the list of characters which needs to be
1038 * encoded.
1039 *
1040 * Strictly correct encoding is required for various purposes, such as OAuth
1041 * signature base strings.
1042 *
1043 * @param string $s the URL to encode
1044 * @return string the encoded URL
1045 */
1046 function rfc3986_urlencode($s) {
1047 if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
1048 return rawurlencode($s);
1049 } else {
1050 return str_replace('%7E', '~', rawurlencode($s));
1051 }
1052 }
1053}
1054?>