| 1: | <?php |
| 2: | /* |
| 3: | * SimpleID |
| 4: | * |
| 5: | * Copyright (C) Kelvin Mo 2014-2025 |
| 6: | * |
| 7: | * This program is free software; you can redistribute it and/or |
| 8: | * modify it under the terms of the GNU General Public |
| 9: | * License as published by the Free Software Foundation; either |
| 10: | * version 2 of the License, or (at your option) any later version. |
| 11: | * |
| 12: | * This program is distributed in the hope that it will be useful, |
| 13: | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14: | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 15: | * General Public License for more details. |
| 16: | * |
| 17: | * You should have received a copy of the GNU General Public |
| 18: | * License along with this program; if not, write to the Free |
| 19: | * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
| 20: | * |
| 21: | */ |
| 22: | |
| 23: | namespace SimpleID\Protocols\OpenID; |
| 24: | |
| 25: | use \SimpleID\Crypt\Random; |
| 26: | |
| 27: | |
| 28: | /** |
| 29: | * A class representing an OpenID association. |
| 30: | */ |
| 31: | class Association { |
| 32: | |
| 33: | const ASSOCIATION_PRIVATE = 2; |
| 34: | const ASSOCIATION_SHARED = 1; |
| 35: | |
| 36: | /** @var string the association handle */ |
| 37: | private $assoc_handle; |
| 38: | |
| 39: | /** @var string the association type */ |
| 40: | private $assoc_type; |
| 41: | |
| 42: | /** @var string the MAC key */ |
| 43: | private $mac_key; |
| 44: | |
| 45: | /** @var int the time the association was created */ |
| 46: | private $created; |
| 47: | |
| 48: | /** @var bool whether the association created is private under |
| 49: | * stateless mode */ |
| 50: | private $private = false; |
| 51: | |
| 52: | |
| 53: | /** |
| 54: | * Creates an association for OpenID versions 1 and 2. |
| 55: | * |
| 56: | * This function calls {@link DiffieHellman::associateAsServer()} where required, to |
| 57: | * generate the cryptographic values required for an association response. |
| 58: | * |
| 59: | * @param int $mode either ASSOCIATION_SHARED or ASSOCIATION_PRIVATE |
| 60: | * @param string $assoc_type a valid OpenID association type |
| 61: | * @link http://openid.net/specs/openid-authentication-1_1.html#anchor14, http://openid.net/specs/openid-authentication-2_0.html#anchor20 |
| 62: | */ |
| 63: | function __construct($mode = self::ASSOCIATION_SHARED, $assoc_type = 'HMAC-SHA1') { |
| 64: | $rand = new Random(); |
| 65: | $assoc_types = self::getAssociationTypes(); |
| 66: | |
| 67: | $this->assoc_handle = $rand->id(); |
| 68: | |
| 69: | $this->assoc_type = $assoc_type; |
| 70: | $mac_size = $assoc_types[$assoc_type]['mac_size']; |
| 71: | $this->mac_key = base64_encode($rand->bytes($mac_size)); |
| 72: | |
| 73: | $this->created = time(); |
| 74: | |
| 75: | if ($mode == self::ASSOCIATION_PRIVATE) $this->private = true; |
| 76: | } |
| 77: | |
| 78: | /** |
| 79: | * Returns the association handle. |
| 80: | * |
| 81: | * @return string the association handle |
| 82: | */ |
| 83: | function getHandle() { |
| 84: | return $this->assoc_handle; |
| 85: | } |
| 86: | |
| 87: | /** |
| 88: | * Returns the creation time. |
| 89: | * |
| 90: | * @return int the creation time |
| 91: | */ |
| 92: | function getCreationTime() { |
| 93: | return $this->created; |
| 94: | } |
| 95: | |
| 96: | /** |
| 97: | * Returns whether this is a private association. |
| 98: | * |
| 99: | * @return bool true if this is a private association |
| 100: | */ |
| 101: | function isPrivate() { |
| 102: | return $this->private; |
| 103: | } |
| 104: | |
| 105: | /** |
| 106: | * Creates data an OpenID association response. |
| 107: | * |
| 108: | * This function calls {@link SimpleID\Protocols\OpenID\DiffieHellman::assciateAsServer()} where required, to |
| 109: | * generate the cryptographic values required for an association response. |
| 110: | * |
| 111: | * @param string $session_type a valid OpenID session type |
| 112: | * @param string $dh_consumer_public for Diffie-Hellman key exchange, the public key of the relying party encoded in Base64 |
| 113: | * @param string $dh_modulus for Diffie-Hellman key exchange, the modulus encoded in Base64 |
| 114: | * @param string $dh_gen for Diffie-Hellman key exchange, g encoded in Base64 |
| 115: | * @return array<string, string> data that can be fed into an OpenID association response |
| 116: | * @link http://openid.net/specs/openid-authentication-1_1.html#anchor14, http://openid.net/specs/openid-authentication-2_0.html#anchor20 |
| 117: | */ |
| 118: | function getOpenIDResponse($session_type = 'no-encryption', $dh_consumer_public = NULL, $dh_modulus = NULL, $dh_gen = NULL) { |
| 119: | $assoc_types = self::getAssociationTypes(); |
| 120: | $session_types = self::getSessionTypes(); |
| 121: | |
| 122: | $response = [ |
| 123: | 'assoc_handle' => $this->assoc_handle, |
| 124: | 'assoc_type' => $this->assoc_type, |
| 125: | ]; |
| 126: | |
| 127: | // If $session_type is '', then it must be using OpenID 1.1 (blank parameter |
| 128: | // is not allowed for OpenID 2.0. For OpenID 1.1 blank requests, we don't |
| 129: | // put a session_type in the response. |
| 130: | if ($session_type != '') $response['session_type'] = $session_type; |
| 131: | |
| 132: | if (($session_type == 'no-encryption') || ($session_type == '')) { |
| 133: | $response['mac_key'] = $this->mac_key; |
| 134: | } elseif ($session_type == 'DH-SHA1' || $session_type == 'DH-SHA256') { |
| 135: | $algo = $session_types[$session_type]['algo']; |
| 136: | $dh = new DiffieHellman($dh_modulus, $dh_gen, $algo); |
| 137: | |
| 138: | $result = $dh->associateAsServer(base64_decode($this->mac_key), $dh_consumer_public); |
| 139: | $response['dh_server_public'] = $result['dh_server_public']; |
| 140: | $response['enc_mac_key'] = $result['enc_mac_key']; |
| 141: | } |
| 142: | |
| 143: | return $response; |
| 144: | } |
| 145: | |
| 146: | /** |
| 147: | * Calculates a signature of an OpenID message |
| 148: | * |
| 149: | * @param Message $message the message to sign |
| 150: | * @return string the signature encoded in Base64 |
| 151: | */ |
| 152: | function sign($message) { |
| 153: | $assoc_types = self::getAssociationTypes(); |
| 154: | $signature = ''; |
| 155: | $algo = $assoc_types[$this->assoc_type]['algo']; |
| 156: | $secret = base64_decode($this->mac_key); |
| 157: | $signature = hash_hmac($algo, $message->getSignatureBaseString(), $secret, true); |
| 158: | |
| 159: | return base64_encode($signature); |
| 160: | } |
| 161: | |
| 162: | /** |
| 163: | * Returns a string representation of the association for debugging purposes |
| 164: | * |
| 165: | * @return string a string representation of the association |
| 166: | */ |
| 167: | function toString() { |
| 168: | return sprintf('private: %1$s, assoc_handle: %2$s, assoc_type: %3$s', $this->private, $this->assoc_handle, $this->assoc_type); |
| 169: | } |
| 170: | |
| 171: | /** |
| 172: | * Returns the association types supported by this server. |
| 173: | * |
| 174: | * @return array<string, mixed> an array containing the association types supported by this server as keys |
| 175: | * and an array containing the key size (mac_size) and HMAC algorithm (algo) as |
| 176: | * values |
| 177: | */ |
| 178: | static function getAssociationTypes() { |
| 179: | $association_types = [ 'HMAC-SHA1' => [ 'mac_size' => 20, 'algo' => 'sha1' ] ]; |
| 180: | if (in_array('sha256', hash_algos())) $association_types['HMAC-SHA256'] = [ 'mac_size' => 32, 'algo' => 'sha256' ]; |
| 181: | return $association_types; |
| 182: | } |
| 183: | |
| 184: | /** |
| 185: | * Returns the association types supported by this server and the version of |
| 186: | * OpenID. |
| 187: | * |
| 188: | * OpenID version 1 supports an empty string as the session type. OpenID version 2 |
| 189: | * reqires a session type to be sent. |
| 190: | * |
| 191: | * @param bool $is_https whether the transport layer encryption is used for the current |
| 192: | * connection |
| 193: | * @param float $version the OpenID version, either OPENID_VERSION_1_1 and OPENID_VERSION_2 |
| 194: | * @return array<string, mixed> an array containing the session types supported by this server as keys |
| 195: | * and an array containing the hash function (hash_func) as |
| 196: | * values |
| 197: | */ |
| 198: | static function getSessionTypes($is_https = TRUE, $version = Message::OPENID_VERSION_2) { |
| 199: | $session_types = [ |
| 200: | 'DH-SHA1' => [ 'algo' => 'sha1' ], |
| 201: | ]; |
| 202: | if (in_array('sha256', hash_algos())) $session_types['DH-SHA256'] = [ 'algo' => 'sha256' ]; |
| 203: | if (($version >= Message::OPENID_VERSION_2) && ($is_https == TRUE)) { |
| 204: | // Under OpenID 2.0 no-encryption is only allowed if TLS is used |
| 205: | $session_types['no-encryption'] = []; |
| 206: | } |
| 207: | if ($version == Message::OPENID_VERSION_1_1) $session_types[''] = []; |
| 208: | return $session_types; |
| 209: | } |
| 210: | } |
| 211: | |
| 212: | ?> |