source: vendor/firebase/php-jwt/src/JWK.php@ f9c482b

Last change on this file since f9c482b was f9c482b, checked in by Vlado 222039 <vlado.popovski@…>, 7 days ago

Upload new project files

  • Property mode set to 100644
File size: 11.6 KB
Line 
1<?php
2
3namespace Firebase\JWT;
4
5use DomainException;
6use InvalidArgumentException;
7use UnexpectedValueException;
8
9/**
10 * JSON Web Key implementation, based on this spec:
11 * https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
12 *
13 * PHP version 5
14 *
15 * @category Authentication
16 * @package Authentication_JWT
17 * @author Bui Sy Nguyen <nguyenbs@gmail.com>
18 * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
19 * @link https://github.com/firebase/php-jwt
20 */
21class JWK
22{
23 private const OID = '1.2.840.10045.2.1';
24 private const ASN1_OBJECT_IDENTIFIER = 0x06;
25 private const ASN1_SEQUENCE = 0x10; // also defined in JWT
26 private const ASN1_BIT_STRING = 0x03;
27 private const EC_CURVES = [
28 'P-256' => '1.2.840.10045.3.1.7', // Len: 64
29 'secp256k1' => '1.3.132.0.10', // Len: 64
30 'P-384' => '1.3.132.0.34', // Len: 96
31 // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
32 ];
33
34 // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype.
35 // This library supports the following subtypes:
36 private const OKP_SUBTYPES = [
37 'Ed25519' => true, // RFC 8037
38 ];
39
40 /**
41 * Parse a set of JWK keys
42 *
43 * @param array<mixed> $jwks The JSON Web Key Set as an associative array
44 * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
45 * JSON Web Key Set
46 *
47 * @return array<string, Key> An associative array of key IDs (kid) to Key objects
48 *
49 * @throws InvalidArgumentException Provided JWK Set is empty
50 * @throws UnexpectedValueException Provided JWK Set was invalid
51 * @throws DomainException OpenSSL failure
52 *
53 * @uses parseKey
54 */
55 public static function parseKeySet(array $jwks, ?string $defaultAlg = null): array
56 {
57 $keys = [];
58
59 if (!isset($jwks['keys'])) {
60 throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
61 }
62
63 if (empty($jwks['keys'])) {
64 throw new InvalidArgumentException('JWK Set did not contain any keys');
65 }
66
67 foreach ($jwks['keys'] as $k => $v) {
68 $kid = isset($v['kid']) ? $v['kid'] : $k;
69 if ($key = self::parseKey($v, $defaultAlg)) {
70 $keys[(string) $kid] = $key;
71 }
72 }
73
74 if (0 === \count($keys)) {
75 throw new UnexpectedValueException('No supported algorithms found in JWK Set');
76 }
77
78 return $keys;
79 }
80
81 /**
82 * Parse a JWK key
83 *
84 * @param array<mixed> $jwk An individual JWK
85 * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
86 * JSON Web Key Set
87 *
88 * @return Key The key object for the JWK
89 *
90 * @throws InvalidArgumentException Provided JWK is empty
91 * @throws UnexpectedValueException Provided JWK was invalid
92 * @throws DomainException OpenSSL failure
93 *
94 * @uses createPemFromModulusAndExponent
95 */
96 public static function parseKey(array $jwk, ?string $defaultAlg = null): ?Key
97 {
98 if (empty($jwk)) {
99 throw new InvalidArgumentException('JWK must not be empty');
100 }
101
102 if (!isset($jwk['kty'])) {
103 throw new UnexpectedValueException('JWK must contain a "kty" parameter');
104 }
105
106 if (!isset($jwk['alg'])) {
107 if (\is_null($defaultAlg)) {
108 // The "alg" parameter is optional in a KTY, but an algorithm is required
109 // for parsing in this library. Use the $defaultAlg parameter when parsing the
110 // key set in order to prevent this error.
111 // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
112 throw new UnexpectedValueException('JWK must contain an "alg" parameter');
113 }
114 $jwk['alg'] = $defaultAlg;
115 }
116
117 switch ($jwk['kty']) {
118 case 'RSA':
119 if (!empty($jwk['d'])) {
120 throw new UnexpectedValueException('RSA private keys are not supported');
121 }
122 if (!isset($jwk['n']) || !isset($jwk['e'])) {
123 throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
124 }
125
126 $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
127 $publicKey = \openssl_pkey_get_public($pem);
128 if (false === $publicKey) {
129 throw new DomainException(
130 'OpenSSL error: ' . \openssl_error_string()
131 );
132 }
133 return new Key($publicKey, $jwk['alg']);
134 case 'EC':
135 if (isset($jwk['d'])) {
136 // The key is actually a private key
137 throw new UnexpectedValueException('Key data must be for a public key');
138 }
139
140 if (empty($jwk['crv'])) {
141 throw new UnexpectedValueException('crv not set');
142 }
143
144 if (!isset(self::EC_CURVES[$jwk['crv']])) {
145 throw new DomainException('Unrecognised or unsupported EC curve');
146 }
147
148 if (empty($jwk['x']) || empty($jwk['y'])) {
149 throw new UnexpectedValueException('x and y not set');
150 }
151
152 $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
153 return new Key($publicKey, $jwk['alg']);
154 case 'OKP':
155 if (isset($jwk['d'])) {
156 // The key is actually a private key
157 throw new UnexpectedValueException('Key data must be for a public key');
158 }
159
160 if (!isset($jwk['crv'])) {
161 throw new UnexpectedValueException('crv not set');
162 }
163
164 if (empty(self::OKP_SUBTYPES[$jwk['crv']])) {
165 throw new DomainException('Unrecognised or unsupported OKP key subtype');
166 }
167
168 if (empty($jwk['x'])) {
169 throw new UnexpectedValueException('x not set');
170 }
171
172 // This library works internally with EdDSA keys (Ed25519) encoded in standard base64.
173 $publicKey = JWT::convertBase64urlToBase64($jwk['x']);
174 return new Key($publicKey, $jwk['alg']);
175 default:
176 break;
177 }
178
179 return null;
180 }
181
182 /**
183 * Converts the EC JWK values to pem format.
184 *
185 * @param string $crv The EC curve (only P-256 & P-384 is supported)
186 * @param string $x The EC x-coordinate
187 * @param string $y The EC y-coordinate
188 *
189 * @return string
190 */
191 private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
192 {
193 $pem =
194 self::encodeDER(
195 self::ASN1_SEQUENCE,
196 self::encodeDER(
197 self::ASN1_SEQUENCE,
198 self::encodeDER(
199 self::ASN1_OBJECT_IDENTIFIER,
200 self::encodeOID(self::OID)
201 )
202 . self::encodeDER(
203 self::ASN1_OBJECT_IDENTIFIER,
204 self::encodeOID(self::EC_CURVES[$crv])
205 )
206 ) .
207 self::encodeDER(
208 self::ASN1_BIT_STRING,
209 \chr(0x00) . \chr(0x04)
210 . JWT::urlsafeB64Decode($x)
211 . JWT::urlsafeB64Decode($y)
212 )
213 );
214
215 return \sprintf(
216 "-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
217 wordwrap(base64_encode($pem), 64, "\n", true)
218 );
219 }
220
221 /**
222 * Create a public key represented in PEM format from RSA modulus and exponent information
223 *
224 * @param string $n The RSA modulus encoded in Base64
225 * @param string $e The RSA exponent encoded in Base64
226 *
227 * @return string The RSA public key represented in PEM format
228 *
229 * @uses encodeLength
230 */
231 private static function createPemFromModulusAndExponent(
232 string $n,
233 string $e
234 ): string {
235 $mod = JWT::urlsafeB64Decode($n);
236 $exp = JWT::urlsafeB64Decode($e);
237
238 $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
239 $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
240
241 $rsaPublicKey = \pack(
242 'Ca*a*a*',
243 48,
244 self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
245 $modulus,
246 $publicExponent
247 );
248
249 // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
250 $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
251 $rsaPublicKey = \chr(0) . $rsaPublicKey;
252 $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
253
254 $rsaPublicKey = \pack(
255 'Ca*a*',
256 48,
257 self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
258 $rsaOID . $rsaPublicKey
259 );
260
261 return "-----BEGIN PUBLIC KEY-----\r\n" .
262 \chunk_split(\base64_encode($rsaPublicKey), 64) .
263 '-----END PUBLIC KEY-----';
264 }
265
266 /**
267 * DER-encode the length
268 *
269 * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
270 * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
271 *
272 * @param int $length
273 * @return string
274 */
275 private static function encodeLength(int $length): string
276 {
277 if ($length <= 0x7F) {
278 return \chr($length);
279 }
280
281 $temp = \ltrim(\pack('N', $length), \chr(0));
282
283 return \pack('Ca*', 0x80 | \strlen($temp), $temp);
284 }
285
286 /**
287 * Encodes a value into a DER object.
288 * Also defined in Firebase\JWT\JWT
289 *
290 * @param int $type DER tag
291 * @param string $value the value to encode
292 * @return string the encoded object
293 */
294 private static function encodeDER(int $type, string $value): string
295 {
296 $tag_header = 0;
297 if ($type === self::ASN1_SEQUENCE) {
298 $tag_header |= 0x20;
299 }
300
301 // Type
302 $der = \chr($tag_header | $type);
303
304 // Length
305 $der .= \chr(\strlen($value));
306
307 return $der . $value;
308 }
309
310 /**
311 * Encodes a string into a DER-encoded OID.
312 *
313 * @param string $oid the OID string
314 * @return string the binary DER-encoded OID
315 */
316 private static function encodeOID(string $oid): string
317 {
318 $octets = explode('.', $oid);
319
320 // Get the first octet
321 $first = (int) array_shift($octets);
322 $second = (int) array_shift($octets);
323 $oid = \chr($first * 40 + $second);
324
325 // Iterate over subsequent octets
326 foreach ($octets as $octet) {
327 if ($octet == 0) {
328 $oid .= \chr(0x00);
329 continue;
330 }
331 $bin = '';
332
333 while ($octet) {
334 $bin .= \chr(0x80 | ($octet & 0x7f));
335 $octet >>= 7;
336 }
337 $bin[0] = $bin[0] & \chr(0x7f);
338
339 // Convert to big endian if necessary
340 if (pack('V', 65534) == pack('L', 65534)) {
341 $oid .= strrev($bin);
342 } else {
343 $oid .= $bin;
344 }
345 }
346
347 return $oid;
348 }
349}
Note: See TracBrowser for help on using the repository browser.