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: ?>