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