7
7
use Exception ;
8
8
use InvalidArgumentException ;
9
9
use OpenSSLAsymmetricKey ;
10
+ use OpenSSLCertificate ;
10
11
use TypeError ;
11
12
use UnexpectedValueException ;
12
13
use DateTime ;
@@ -38,6 +39,9 @@ class JWT
38
39
*/
39
40
public static int $ leeway = 0 ;
40
41
42
+ /**
43
+ * @var array<string, string[]>
44
+ */
41
45
public static array $ supported_algs = [
42
46
'ES384 ' => ['openssl ' , 'SHA384 ' ],
43
47
'ES256 ' => ['openssl ' , 'SHA256 ' ],
@@ -86,10 +90,16 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
86
90
throw new UnexpectedValueException ('Wrong number of segments ' );
87
91
}
88
92
list ($ headb64 , $ bodyb64 , $ cryptob64 ) = $ tks ;
89
- if (null === ($ header = static ::jsonDecode (static ::urlsafeB64Decode ($ headb64 )))) {
93
+ if (false === ($ headerRaw = static ::urlsafeB64Decode ($ headb64 ))) {
94
+ throw new UnexpectedValueException ('Invalid header encoding ' );
95
+ }
96
+ if (null === ($ header = static ::jsonDecode ($ headerRaw ))) {
90
97
throw new UnexpectedValueException ('Invalid header encoding ' );
91
98
}
92
- if (null === $ payload = static ::jsonDecode (static ::urlsafeB64Decode ($ bodyb64 ))) {
99
+ if (false === ($ payloadRaw = static ::urlsafeB64Decode ($ bodyb64 ))) {
100
+ throw new UnexpectedValueException ('Invalid claims encoding ' );
101
+ }
102
+ if (null === ($ payload = static ::jsonDecode ($ payloadRaw ))) {
93
103
throw new UnexpectedValueException ('Invalid claims encoding ' );
94
104
}
95
105
if (!$ payload instanceof stdClass) {
@@ -116,7 +126,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
116
126
// OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
117
127
$ sig = self ::signatureToDER ($ sig );
118
128
}
119
- if (!static ::verify ("$ headb64. $ bodyb64 " , $ sig , $ key ->getKeyMaterial (), $ header ->alg )) {
129
+ if (!self ::verify ("$ headb64. $ bodyb64 " , $ sig , $ key ->getKeyMaterial (), $ header ->alg )) {
120
130
throw new SignatureInvalidException ('Signature verification failed ' );
121
131
}
122
132
@@ -148,11 +158,10 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
148
158
/**
149
159
* Converts and signs a PHP object or array into a JWT string.
150
160
*
151
- * @param array $payload PHP array
152
- * @param string|OpenSSLAsymmetricKey $key The secret key.
153
- * If the algorithm used is asymmetric, this is the private key
154
- * @param string $keyId
155
- * @param array $head An array with header elements to attach
161
+ * @param array<mixed> $payload PHP array
162
+ * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array<mixed> $key The secret key.
163
+ * @param string $keyId
164
+ * @param array<string, string> $head An array with header elements to attach
156
165
*
157
166
* @return string A signed JWT
158
167
*
@@ -161,7 +170,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
161
170
*/
162
171
public static function encode (
163
172
array $ payload ,
164
- string |OpenSSLAsymmetricKey $ key ,
173
+ string |OpenSSLAsymmetricKey | OpenSSLCertificate | array $ key ,
165
174
string $ alg ,
166
175
string $ keyId = null ,
167
176
array $ head = null
@@ -174,8 +183,8 @@ public static function encode(
174
183
$ header = \array_merge ($ head , $ header );
175
184
}
176
185
$ segments = [];
177
- $ segments [] = static ::urlsafeB64Encode (static ::jsonEncode ($ header ));
178
- $ segments [] = static ::urlsafeB64Encode (static ::jsonEncode ($ payload ));
186
+ $ segments [] = static ::urlsafeB64Encode (( string ) static ::jsonEncode ($ header ));
187
+ $ segments [] = static ::urlsafeB64Encode (( string ) static ::jsonEncode ($ payload ));
179
188
$ signing_input = \implode ('. ' , $ segments );
180
189
181
190
$ signature = static ::sign ($ signing_input , $ key , $ alg );
@@ -187,23 +196,29 @@ public static function encode(
187
196
/**
188
197
* Sign a string with a given key and algorithm.
189
198
*
190
- * @param string $msg The message to sign
191
- * @param string|OpenSSLAsymmetricKey $key The secret key.
192
- * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
193
- * 'HS512', 'RS256', 'RS384', and 'RS512'
199
+ * @param string $msg The message to sign
200
+ * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array<mixed> $key The secret key.
201
+ * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
202
+ * 'HS512', 'RS256', 'RS384', and 'RS512'
194
203
*
195
204
* @return string An encrypted message
196
205
*
197
206
* @throws DomainException Unsupported algorithm or bad key was specified
198
207
*/
199
- public static function sign (string $ msg , string |OpenSSLAsymmetricKey $ key , string $ alg ): string
200
- {
208
+ public static function sign (
209
+ string $ msg ,
210
+ string |OpenSSLAsymmetricKey |OpenSSLCertificate |array $ key ,
211
+ string $ alg
212
+ ): string {
201
213
if (empty (static ::$ supported_algs [$ alg ])) {
202
214
throw new DomainException ('Algorithm not supported ' );
203
215
}
204
216
list ($ function , $ algorithm ) = static ::$ supported_algs [$ alg ];
205
217
switch ($ function ) {
206
218
case 'hash_hmac ' :
219
+ if (!is_string ($ key )) {
220
+ throw new InvalidArgumentException ('key must be a string when using hmac ' );
221
+ }
207
222
return \hash_hmac ($ algorithm , $ msg , $ key , true );
208
223
case 'openssl ' :
209
224
$ signature = '' ;
@@ -221,10 +236,13 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin
221
236
if (!function_exists ('sodium_crypto_sign_detached ' )) {
222
237
throw new DomainException ('libsodium is not available ' );
223
238
}
239
+ if (!is_string ($ key )) {
240
+ throw new InvalidArgumentException ('key must be a string when using EdDSA ' );
241
+ }
224
242
try {
225
243
// The last non-empty line is used as the key.
226
244
$ lines = array_filter (explode ("\n" , $ key ));
227
- $ key = base64_decode (end ($ lines ));
245
+ $ key = base64_decode (( string ) end ($ lines ));
228
246
return sodium_crypto_sign_detached ($ msg , $ key );
229
247
} catch (Exception $ e ) {
230
248
throw new DomainException ($ e ->getMessage (), 0 , $ e );
@@ -238,10 +256,10 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin
238
256
* Verify a signature with the message, key and method. Not all methods
239
257
* are symmetric, so we must have a separate verify and sign method.
240
258
*
241
- * @param string $msg The original message (header and body)
242
- * @param string $signature The original signature
243
- * @param string|OpenSSLAsymmetricKey $key For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
244
- * @param string $alg The algorithm
259
+ * @param string $msg The original message (header and body)
260
+ * @param string $signature The original signature
261
+ * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array<mixed> $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
262
+ * @param string $alg The algorithm
245
263
*
246
264
* @return bool
247
265
*
@@ -250,7 +268,7 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin
250
268
private static function verify (
251
269
string $ msg ,
252
270
string $ signature ,
253
- string |OpenSSLAsymmetricKey $ keyMaterial ,
271
+ string |OpenSSLAsymmetricKey | OpenSSLCertificate | array $ keyMaterial ,
254
272
string $ alg
255
273
): bool {
256
274
if (empty (static ::$ supported_algs [$ alg ])) {
@@ -274,16 +292,22 @@ private static function verify(
274
292
if (!function_exists ('sodium_crypto_sign_verify_detached ' )) {
275
293
throw new DomainException ('libsodium is not available ' );
276
294
}
295
+ if (!is_string ($ keyMaterial )) {
296
+ throw new InvalidArgumentException ('key must be a string when using EdDSA ' );
297
+ }
277
298
try {
278
299
// The last non-empty line is used as the key.
279
300
$ lines = array_filter (explode ("\n" , $ keyMaterial ));
280
- $ key = base64_decode (end ($ lines ));
301
+ $ key = base64_decode (( string ) end ($ lines ));
281
302
return sodium_crypto_sign_verify_detached ($ signature , $ msg , $ key );
282
303
} catch (Exception $ e ) {
283
304
throw new DomainException ($ e ->getMessage (), 0 , $ e );
284
305
}
285
306
case 'hash_hmac ' :
286
307
default :
308
+ if (!is_string ($ keyMaterial )) {
309
+ throw new InvalidArgumentException ('key must be a string when using hmac ' );
310
+ }
287
311
$ hash = \hash_hmac ($ algorithm , $ msg , $ keyMaterial , true );
288
312
return self ::constantTimeEquals ($ hash , $ signature );
289
313
}
@@ -303,7 +327,7 @@ public static function jsonDecode(string $input): mixed
303
327
$ obj = \json_decode ($ input , false , 512 , JSON_BIGINT_AS_STRING );
304
328
305
329
if ($ errno = \json_last_error ()) {
306
- static ::handleJsonError ($ errno );
330
+ self ::handleJsonError ($ errno );
307
331
} elseif ($ obj === null && $ input !== 'null ' ) {
308
332
throw new DomainException ('Null result with non-null input ' );
309
333
}
@@ -313,13 +337,13 @@ public static function jsonDecode(string $input): mixed
313
337
/**
314
338
* Encode a PHP array into a JSON string.
315
339
*
316
- * @param array $input A PHP array
340
+ * @param array<mixed> $input A PHP array
317
341
*
318
- * @return string JSON representation of the PHP array
342
+ * @return string|false JSON representation of the PHP array
319
343
*
320
344
* @throws DomainException Provided object could not be encoded to valid JSON
321
345
*/
322
- public static function jsonEncode (array $ input ): string
346
+ public static function jsonEncode (array $ input ): string | false
323
347
{
324
348
if (PHP_VERSION_ID >= 50400 ) {
325
349
$ json = \json_encode ($ input , \JSON_UNESCAPED_SLASHES );
@@ -328,7 +352,7 @@ public static function jsonEncode(array $input): string
328
352
$ json = \json_encode ($ input );
329
353
}
330
354
if ($ errno = \json_last_error ()) {
331
- static ::handleJsonError ($ errno );
355
+ self ::handleJsonError ($ errno );
332
356
} elseif ($ json === 'null ' && $ input !== null ) {
333
357
throw new DomainException ('Null result with non-null input ' );
334
358
}
@@ -342,7 +366,7 @@ public static function jsonEncode(array $input): string
342
366
*
343
367
* @return string A decoded string
344
368
*/
345
- public static function urlsafeB64Decode (string $ input ): string
369
+ public static function urlsafeB64Decode (string $ input ): string | false
346
370
{
347
371
$ remainder = \strlen ($ input ) % 4 ;
348
372
if ($ remainder ) {
@@ -381,29 +405,22 @@ private static function getKey(Key|array|ArrayAccess $keyOrKeyArray, ?string $ki
381
405
return $ keyOrKeyArray ;
382
406
}
383
407
384
- if (is_array ($ keyOrKeyArray ) || $ keyOrKeyArray instanceof ArrayAccess) {
385
- foreach ($ keyOrKeyArray as $ keyId => $ key ) {
386
- if (!$ key instanceof Key) {
387
- throw new TypeError (
388
- '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
389
- . 'array of Firebase\JWT\Key keys '
390
- );
391
- }
392
- }
393
- if (!isset ($ kid )) {
394
- throw new UnexpectedValueException ('"kid" empty, unable to lookup correct key ' );
395
- }
396
- if (!isset ($ keyOrKeyArray [$ kid ])) {
397
- throw new UnexpectedValueException ('"kid" invalid, unable to lookup correct key ' );
408
+ foreach ($ keyOrKeyArray as $ keyId => $ key ) {
409
+ if (!$ key instanceof Key) {
410
+ throw new TypeError (
411
+ '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
412
+ . 'array of Firebase\JWT\Key keys '
413
+ );
398
414
}
399
-
400
- return $ keyOrKeyArray [$ kid ];
415
+ }
416
+ if (!isset ($ kid )) {
417
+ throw new UnexpectedValueException ('"kid" empty, unable to lookup correct key ' );
418
+ }
419
+ if (!isset ($ keyOrKeyArray [$ kid ])) {
420
+ throw new UnexpectedValueException ('"kid" invalid, unable to lookup correct key ' );
401
421
}
402
422
403
- throw new UnexpectedValueException (
404
- '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an '
405
- . 'array of Firebase\JWT\Key keys '
406
- );
423
+ return $ keyOrKeyArray [$ kid ];
407
424
}
408
425
409
426
/**
@@ -416,13 +433,13 @@ public static function constantTimeEquals(string $left, string $right): bool
416
433
if (\function_exists ('hash_equals ' )) {
417
434
return \hash_equals ($ left , $ right );
418
435
}
419
- $ len = \min (static ::safeStrlen ($ left ), static ::safeStrlen ($ right ));
436
+ $ len = \min (self ::safeStrlen ($ left ), self ::safeStrlen ($ right ));
420
437
421
438
$ status = 0 ;
422
439
for ($ i = 0 ; $ i < $ len ; $ i ++) {
423
440
$ status |= (\ord ($ left [$ i ]) ^ \ord ($ right [$ i ]));
424
441
}
425
- $ status |= (static ::safeStrlen ($ left ) ^ static ::safeStrlen ($ right ));
442
+ $ status |= (self ::safeStrlen ($ left ) ^ self ::safeStrlen ($ right ));
426
443
427
444
return ($ status === 0 );
428
445
}
@@ -476,7 +493,8 @@ private static function safeStrlen(string $str): int
476
493
private static function signatureToDER (string $ sig ): string
477
494
{
478
495
// Separate the signature into r-value and s-value
479
- list ($ r , $ s ) = \str_split ($ sig , (int ) (\strlen ($ sig ) / 2 ));
496
+ $ length = max (1 , (int ) (\strlen ($ sig ) / 2 ));
497
+ list ($ r , $ s ) = \str_split ($ sig , $ length > 0 ? $ length : 1 );
480
498
481
499
// Trim leading zeros
482
500
$ r = \ltrim ($ r , "\x00" );
@@ -556,7 +574,7 @@ private static function signatureFromDER(string $der, int $keySize): string
556
574
* @param int $offset the offset of the data stream containing the object
557
575
* to decode
558
576
*
559
- * @return array [$offset, $data] the new offset and the decoded object
577
+ * @return array{int, string|null} the new offset and the decoded object
560
578
*/
561
579
private static function readDER (string $ der , int $ offset = 0 ): array
562
580
{
0 commit comments