77use Exception ;
88use InvalidArgumentException ;
99use OpenSSLAsymmetricKey ;
10+ use OpenSSLCertificate ;
1011use TypeError ;
1112use UnexpectedValueException ;
1213use DateTime ;
@@ -38,6 +39,9 @@ class JWT
3839 */
3940 public static int $ leeway = 0 ;
4041
42+ /**
43+ * @var array<string, string[]>
44+ */
4145 public static array $ supported_algs = [
4246 'ES384 ' => ['openssl ' , 'SHA384 ' ],
4347 'ES256 ' => ['openssl ' , 'SHA256 ' ],
@@ -86,10 +90,16 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
8690 throw new UnexpectedValueException ('Wrong number of segments ' );
8791 }
8892 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 ))) {
9097 throw new UnexpectedValueException ('Invalid header encoding ' );
9198 }
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 ))) {
93103 throw new UnexpectedValueException ('Invalid claims encoding ' );
94104 }
95105 if (!$ payload instanceof stdClass) {
@@ -116,7 +126,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
116126 // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
117127 $ sig = self ::signatureToDER ($ sig );
118128 }
119- if (!static ::verify ("$ headb64. $ bodyb64 " , $ sig , $ key ->getKeyMaterial (), $ header ->alg )) {
129+ if (!self ::verify ("$ headb64. $ bodyb64 " , $ sig , $ key ->getKeyMaterial (), $ header ->alg )) {
120130 throw new SignatureInvalidException ('Signature verification failed ' );
121131 }
122132
@@ -148,11 +158,10 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
148158 /**
149159 * Converts and signs a PHP object or array into a JWT string.
150160 *
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
156165 *
157166 * @return string A signed JWT
158167 *
@@ -161,7 +170,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray)
161170 */
162171 public static function encode (
163172 array $ payload ,
164- string |OpenSSLAsymmetricKey $ key ,
173+ string |OpenSSLAsymmetricKey | OpenSSLCertificate | array $ key ,
165174 string $ alg ,
166175 string $ keyId = null ,
167176 array $ head = null
@@ -174,8 +183,8 @@ public static function encode(
174183 $ header = \array_merge ($ head , $ header );
175184 }
176185 $ 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 ));
179188 $ signing_input = \implode ('. ' , $ segments );
180189
181190 $ signature = static ::sign ($ signing_input , $ key , $ alg );
@@ -187,23 +196,29 @@ public static function encode(
187196 /**
188197 * Sign a string with a given key and algorithm.
189198 *
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'
194203 *
195204 * @return string An encrypted message
196205 *
197206 * @throws DomainException Unsupported algorithm or bad key was specified
198207 */
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 {
201213 if (empty (static ::$ supported_algs [$ alg ])) {
202214 throw new DomainException ('Algorithm not supported ' );
203215 }
204216 list ($ function , $ algorithm ) = static ::$ supported_algs [$ alg ];
205217 switch ($ function ) {
206218 case 'hash_hmac ' :
219+ if (!is_string ($ key )) {
220+ throw new InvalidArgumentException ('key must be a string when using hmac ' );
221+ }
207222 return \hash_hmac ($ algorithm , $ msg , $ key , true );
208223 case 'openssl ' :
209224 $ signature = '' ;
@@ -221,10 +236,13 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin
221236 if (!function_exists ('sodium_crypto_sign_detached ' )) {
222237 throw new DomainException ('libsodium is not available ' );
223238 }
239+ if (!is_string ($ key )) {
240+ throw new InvalidArgumentException ('key must be a string when using EdDSA ' );
241+ }
224242 try {
225243 // The last non-empty line is used as the key.
226244 $ lines = array_filter (explode ("\n" , $ key ));
227- $ key = base64_decode (end ($ lines ));
245+ $ key = base64_decode (( string ) end ($ lines ));
228246 return sodium_crypto_sign_detached ($ msg , $ key );
229247 } catch (Exception $ e ) {
230248 throw new DomainException ($ e ->getMessage (), 0 , $ e );
@@ -238,10 +256,10 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin
238256 * Verify a signature with the message, key and method. Not all methods
239257 * are symmetric, so we must have a separate verify and sign method.
240258 *
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
245263 *
246264 * @return bool
247265 *
@@ -250,7 +268,7 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin
250268 private static function verify (
251269 string $ msg ,
252270 string $ signature ,
253- string |OpenSSLAsymmetricKey $ keyMaterial ,
271+ string |OpenSSLAsymmetricKey | OpenSSLCertificate | array $ keyMaterial ,
254272 string $ alg
255273 ): bool {
256274 if (empty (static ::$ supported_algs [$ alg ])) {
@@ -274,16 +292,22 @@ private static function verify(
274292 if (!function_exists ('sodium_crypto_sign_verify_detached ' )) {
275293 throw new DomainException ('libsodium is not available ' );
276294 }
295+ if (!is_string ($ keyMaterial )) {
296+ throw new InvalidArgumentException ('key must be a string when using EdDSA ' );
297+ }
277298 try {
278299 // The last non-empty line is used as the key.
279300 $ lines = array_filter (explode ("\n" , $ keyMaterial ));
280- $ key = base64_decode (end ($ lines ));
301+ $ key = base64_decode (( string ) end ($ lines ));
281302 return sodium_crypto_sign_verify_detached ($ signature , $ msg , $ key );
282303 } catch (Exception $ e ) {
283304 throw new DomainException ($ e ->getMessage (), 0 , $ e );
284305 }
285306 case 'hash_hmac ' :
286307 default :
308+ if (!is_string ($ keyMaterial )) {
309+ throw new InvalidArgumentException ('key must be a string when using hmac ' );
310+ }
287311 $ hash = \hash_hmac ($ algorithm , $ msg , $ keyMaterial , true );
288312 return self ::constantTimeEquals ($ hash , $ signature );
289313 }
@@ -303,7 +327,7 @@ public static function jsonDecode(string $input): mixed
303327 $ obj = \json_decode ($ input , false , 512 , JSON_BIGINT_AS_STRING );
304328
305329 if ($ errno = \json_last_error ()) {
306- static ::handleJsonError ($ errno );
330+ self ::handleJsonError ($ errno );
307331 } elseif ($ obj === null && $ input !== 'null ' ) {
308332 throw new DomainException ('Null result with non-null input ' );
309333 }
@@ -313,13 +337,13 @@ public static function jsonDecode(string $input): mixed
313337 /**
314338 * Encode a PHP array into a JSON string.
315339 *
316- * @param array $input A PHP array
340+ * @param array<mixed> $input A PHP array
317341 *
318- * @return string JSON representation of the PHP array
342+ * @return string|false JSON representation of the PHP array
319343 *
320344 * @throws DomainException Provided object could not be encoded to valid JSON
321345 */
322- public static function jsonEncode (array $ input ): string
346+ public static function jsonEncode (array $ input ): string | false
323347 {
324348 if (PHP_VERSION_ID >= 50400 ) {
325349 $ json = \json_encode ($ input , \JSON_UNESCAPED_SLASHES );
@@ -328,7 +352,7 @@ public static function jsonEncode(array $input): string
328352 $ json = \json_encode ($ input );
329353 }
330354 if ($ errno = \json_last_error ()) {
331- static ::handleJsonError ($ errno );
355+ self ::handleJsonError ($ errno );
332356 } elseif ($ json === 'null ' && $ input !== null ) {
333357 throw new DomainException ('Null result with non-null input ' );
334358 }
@@ -342,7 +366,7 @@ public static function jsonEncode(array $input): string
342366 *
343367 * @return string A decoded string
344368 */
345- public static function urlsafeB64Decode (string $ input ): string
369+ public static function urlsafeB64Decode (string $ input ): string | false
346370 {
347371 $ remainder = \strlen ($ input ) % 4 ;
348372 if ($ remainder ) {
@@ -381,29 +405,22 @@ private static function getKey(Key|array|ArrayAccess $keyOrKeyArray, ?string $ki
381405 return $ keyOrKeyArray ;
382406 }
383407
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+ );
398414 }
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 ' );
401421 }
402422
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 ];
407424 }
408425
409426 /**
@@ -416,13 +433,13 @@ public static function constantTimeEquals(string $left, string $right): bool
416433 if (\function_exists ('hash_equals ' )) {
417434 return \hash_equals ($ left , $ right );
418435 }
419- $ len = \min (static ::safeStrlen ($ left ), static ::safeStrlen ($ right ));
436+ $ len = \min (self ::safeStrlen ($ left ), self ::safeStrlen ($ right ));
420437
421438 $ status = 0 ;
422439 for ($ i = 0 ; $ i < $ len ; $ i ++) {
423440 $ status |= (\ord ($ left [$ i ]) ^ \ord ($ right [$ i ]));
424441 }
425- $ status |= (static ::safeStrlen ($ left ) ^ static ::safeStrlen ($ right ));
442+ $ status |= (self ::safeStrlen ($ left ) ^ self ::safeStrlen ($ right ));
426443
427444 return ($ status === 0 );
428445 }
@@ -476,7 +493,8 @@ private static function safeStrlen(string $str): int
476493 private static function signatureToDER (string $ sig ): string
477494 {
478495 // 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 );
480498
481499 // Trim leading zeros
482500 $ r = \ltrim ($ r , "\x00" );
@@ -556,7 +574,7 @@ private static function signatureFromDER(string $der, int $keySize): string
556574 * @param int $offset the offset of the data stream containing the object
557575 * to decode
558576 *
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
560578 */
561579 private static function readDER (string $ der , int $ offset = 0 ): array
562580 {
0 commit comments