diff --git a/lib/php-jwt/LICENSE b/lib/php-jwt/LICENSE index cb0c49b3311..11c0146651c 100644 --- a/lib/php-jwt/LICENSE +++ b/lib/php-jwt/LICENSE @@ -13,7 +13,7 @@ modification, are permitted provided that the following conditions are met: disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Neuman Vong nor the names of other + * Neither the name of the copyright holder nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/lib/php-jwt/README.md b/lib/php-jwt/README.md index 9c8b5455b69..7839af60127 100644 --- a/lib/php-jwt/README.md +++ b/lib/php-jwt/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/firebase/php-jwt.png?branch=master)](https://travis-ci.org/firebase/php-jwt) +![Build Status](https://github.com/firebase/php-jwt/actions/workflows/tests.yml/badge.svg) [![Latest Stable Version](https://poser.pugx.org/firebase/php-jwt/v/stable)](https://packagist.org/packages/firebase/php-jwt) [![Total Downloads](https://poser.pugx.org/firebase/php-jwt/downloads)](https://packagist.org/packages/firebase/php-jwt) [![License](https://poser.pugx.org/firebase/php-jwt/license)](https://packagist.org/packages/firebase/php-jwt) @@ -16,11 +16,18 @@ Use composer to manage your dependencies and download PHP-JWT: composer require firebase/php-jwt ``` +Optionally, install the `paragonie/sodium_compat` package from composer if your +php is < 7.2 or does not have libsodium installed: + +```bash +composer require paragonie/sodium_compat +``` + Example ------- ```php - +$decoded = JWT::decode($jwt, new Key($key, 'HS256')); ``` Example with RS256 (openssl) ---------------------------- ```php - +``` + +Example with a passphrase +------------------------- + +```php +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + +// Your passphrase +$passphrase = '[YOUR_PASSPHRASE]'; + +// Your private key file with passphrase +// Can be generated with "ssh-keygen -t rsa -m pem" +$privateKeyFile = '/path/to/key-with-passphrase.pem'; + +// Create a private key of type "resource" +$privateKey = openssl_pkey_get_private( + file_get_contents($privateKeyFile), + $passphrase +); + +$payload = array( + "iss" => "example.org", + "aud" => "example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +$jwt = JWT::encode($payload, $privateKey, 'RS256'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +// Get public key from the private key, or pull from from a file. +$publicKey = openssl_pkey_get_details($privateKey)['key']; + +$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256')); +echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; +``` + +Example with EdDSA (libsodium and Ed25519 signature) +---------------------------- +```php +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + +// Public and private keys are expected to be Base64 encoded. The last +// non-empty line is used so that keys can be generated with +// sodium_crypto_sign_keypair(). The secret keys generated by other tools may +// need to be adjusted to match the input expected by libsodium. + +$keyPair = sodium_crypto_sign_keypair(); + +$privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair)); + +$publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); + +$payload = array( + "iss" => "example.org", + "aud" => "example.com", + "iat" => 1356999524, + "nbf" => 1357000000 +); + +$jwt = JWT::encode($payload, $privateKey, 'EdDSA'); +echo "Encode:\n" . print_r($jwt, true) . "\n"; + +$decoded = JWT::decode($jwt, new Key($publicKey, 'EdDSA')); +echo "Decode:\n" . print_r((array) $decoded, true) . "\n"; +```` + +Using JWKs +---------- + +```php +use Firebase\JWT\JWK; +use Firebase\JWT\JWT; + +// Set of keys. The "keys" key is required. For example, the JSON response to +// this endpoint: https://www.gstatic.com/iap/verify/public_key-jwk +$jwks = ['keys' => []]; + +// JWK::parseKeySet($jwks) returns an associative array of **kid** to Firebase\JWT\Key +// objects. Pass this as the second parameter to JWT::decode. +JWT::decode($payload, JWK::parseKeySet($jwks)); ``` Changelog --------- +#### 6.0.0 / 2022-01-24 + + - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v5.5.1) for more information. + - New Key object to prevent key/algorithm type confusion (#365) + - Add JWK support (#273) + - Add ES256 support (#256) + - Add ES384 support (#324) + - Add Ed25519 support (#343) + #### 5.0.0 / 2017-06-26 - Support RS384 and RS512. See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! diff --git a/lib/php-jwt/composer.json b/lib/php-jwt/composer.json index 25d1cfa96ad..6146e2dcc12 100644 --- a/lib/php-jwt/composer.json +++ b/lib/php-jwt/composer.json @@ -22,6 +22,9 @@ "require": { "php": ">=5.3.0" }, + "suggest": { + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, "autoload": { "psr-4": { "Firebase\\JWT\\": "src" diff --git a/lib/php-jwt/src/BeforeValidException.php b/lib/php-jwt/src/BeforeValidException.php index fdf82bd9429..c147852b980 100644 --- a/lib/php-jwt/src/BeforeValidException.php +++ b/lib/php-jwt/src/BeforeValidException.php @@ -1,4 +1,5 @@ An associative array of key IDs (kid) to Key objects * * @throws InvalidArgumentException Provided JWK Set is empty * @throws UnexpectedValueException Provided JWK Set was invalid @@ -62,7 +63,7 @@ class JWK * * @param array $jwk An individual JWK * - * @return resource|array An associative array that represents the key + * @return Key The key object for the JWK * * @throws InvalidArgumentException Provided JWK is empty * @throws UnexpectedValueException Provided JWK was invalid @@ -70,7 +71,7 @@ class JWK * * @uses createPemFromModulusAndExponent */ - private static function parseKey(array $jwk) + public static function parseKey(array $jwk) { if (empty($jwk)) { throw new InvalidArgumentException('JWK must not be empty'); @@ -78,10 +79,16 @@ class JWK if (!isset($jwk['kty'])) { throw new UnexpectedValueException('JWK must contain a "kty" parameter'); } + if (!isset($jwk['alg'])) { + // The "alg" parameter is optional in a KTY, but is required for parsing in + // this library. Add it manually to your JWK array if it doesn't already exist. + // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + } switch ($jwk['kty']) { case 'RSA': - if (\array_key_exists('d', $jwk)) { + if (!empty($jwk['d'])) { throw new UnexpectedValueException('RSA private keys are not supported'); } if (!isset($jwk['n']) || !isset($jwk['e'])) { @@ -95,7 +102,7 @@ class JWK 'OpenSSL error: ' . \openssl_error_string() ); } - return $publicKey; + return new Key($publicKey, $jwk['alg']); default: // Currently only RSA is supported break; diff --git a/lib/php-jwt/src/JWT.php b/lib/php-jwt/src/JWT.php index 4ccc1a96c23..725a0832e5e 100644 --- a/lib/php-jwt/src/JWT.php +++ b/lib/php-jwt/src/JWT.php @@ -2,10 +2,13 @@ namespace Firebase\JWT; -use \DomainException; -use \InvalidArgumentException; -use \UnexpectedValueException; -use \DateTime; +use ArrayAccess; +use DomainException; +use Exception; +use InvalidArgumentException; +use OpenSSLAsymmetricKey; +use UnexpectedValueException; +use DateTime; /** * JSON Web Token implementation, based on this spec: @@ -22,16 +25,19 @@ use \DateTime; */ class JWT { - const ASN1_INTEGER = 0x02; - const ASN1_SEQUENCE = 0x10; - const ASN1_BIT_STRING = 0x03; + // const ASN1_INTEGER = 0x02; + // const ASN1_SEQUENCE = 0x10; + // const ASN1_BIT_STRING = 0x03; + private static $asn1Integer = 0x02; + private static $asn1Sequence = 0x10; + private static $asn1BitString = 0x03; /** * When checking nbf, iat or expiration times, * we want to provide some extra leeway time to * account for clock skew. */ - public static $leeway = 180; + public static $leeway = 0; /** * Allow the current timestamp to be specified. @@ -42,6 +48,7 @@ class JWT public static $timestamp = null; public static $supported_algs = array( + 'ES384' => array('openssl', 'SHA384'), 'ES256' => array('openssl', 'SHA256'), 'HS256' => array('hash_hmac', 'SHA256'), 'HS384' => array('hash_hmac', 'SHA384'), @@ -49,19 +56,23 @@ class JWT 'RS256' => array('openssl', 'SHA256'), 'RS384' => array('openssl', 'SHA384'), 'RS512' => array('openssl', 'SHA512'), + 'EdDSA' => array('sodium_crypto', 'EdDSA'), ); /** * Decodes a JWT string into a PHP object. * * @param string $jwt The JWT - * @param string|array|resource $key The key, or map of keys. + * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. * If the algorithm used is asymmetric, this is the public key - * @param array $allowed_algs List of supported verification algorithms - * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * Each Key object contains an algorithm and matching key. + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return object The JWT's payload as a PHP object * + * @throws InvalidArgumentException Provided key/key-array was empty + * @throws DomainException Provided JWT is malformed * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' @@ -71,11 +82,12 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $key, array $allowed_algs = array()) + public static function decode($jwt, $keyOrKeyArray) { + // Validate JWT $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; - if (empty($key)) { + if (empty($keyOrKeyArray)) { throw new InvalidArgumentException('Key may not be empty'); } $tks = \explode('.', $jwt); @@ -98,27 +110,19 @@ class JWT if (empty(static::$supported_algs[$header->alg])) { throw new UnexpectedValueException('Algorithm not supported'); } - if (!\in_array($header->alg, $allowed_algs)) { - throw new UnexpectedValueException('Algorithm not allowed'); + + $key = self::getKey($keyOrKeyArray, empty($header->kid) ? null : $header->kid); + + // Check the algorithm + if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { + // See issue #351 + throw new UnexpectedValueException('Incorrect key for this algorithm'); } - if ($header->alg === 'ES256') { - // OpenSSL expects an ASN.1 DER sequence for ES256 signatures + if ($header->alg === 'ES256' || $header->alg === 'ES384') { + // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } - - if (\is_array($key) || $key instanceof \ArrayAccess) { - if (isset($header->kid)) { - if (!isset($key[$header->kid])) { - throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); - } - $key = $key[$header->kid]; - } else { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); - } - } - - // Check the signature - if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { + if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -150,20 +154,21 @@ class JWT /** * Converts and signs a PHP object or array into a JWT string. * - * @param object|array $payload PHP object or array - * @param string $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $alg The signing algorithm. - * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' - * @param mixed $keyId - * @param array $head An array with header elements to attach + * @param object|array $payload PHP object or array + * @param string|resource $key The secret key. + * If the algorithm used is asymmetric, this is the private key + * @param string $alg The signing algorithm. + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param mixed $keyId + * @param array $head An array with header elements to attach * * @return string A signed JWT * * @uses jsonEncode * @uses urlsafeB64Encode */ - public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null) + public static function encode($payload, $key, $alg, $keyId = null, $head = null) { $header = array('typ' => 'JWT', 'alg' => $alg); if ($keyId !== null) { @@ -189,13 +194,14 @@ class JWT * @param string $msg The message to sign * @param string|resource $key The secret key * @param string $alg The signing algorithm. - * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * - * @throws DomainException Unsupported algorithm was specified + * @throws DomainException Unsupported algorithm or bad key was specified */ - public static function sign($msg, $key, $alg = 'HS256') + public static function sign($msg, $key, $alg) { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); @@ -209,11 +215,24 @@ class JWT $success = \openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { throw new DomainException("OpenSSL unable to sign data"); - } else { - if ($alg === 'ES256') { - $signature = self::signatureFromDER($signature, 256); - } - return $signature; + } + if ($alg === 'ES256') { + $signature = self::signatureFromDER($signature, 256); + } elseif ($alg === 'ES384') { + $signature = self::signatureFromDER($signature, 384); + } + return $signature; + case 'sodium_crypto': + if (!function_exists('sodium_crypto_sign_detached')) { + throw new DomainException('libsodium is not available'); + } + try { + // The last non-empty line is used as the key. + $lines = array_filter(explode("\n", $key)); + $key = base64_decode(end($lines)); + return sodium_crypto_sign_detached($msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); } } } @@ -229,7 +248,7 @@ class JWT * * @return bool * - * @throws DomainException Invalid Algorithm or OpenSSL failure + * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure */ private static function verify($msg, $signature, $key, $alg) { @@ -250,21 +269,22 @@ class JWT throw new DomainException( 'OpenSSL error: ' . \openssl_error_string() ); + case 'sodium_crypto': + if (!function_exists('sodium_crypto_sign_verify_detached')) { + throw new DomainException('libsodium is not available'); + } + try { + // The last non-empty line is used as the key. + $lines = array_filter(explode("\n", $key)); + $key = base64_decode(end($lines)); + return sodium_crypto_sign_verify_detached($signature, $msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); + } case 'hash_hmac': default: $hash = \hash_hmac($algorithm, $msg, $key, true); - if (\function_exists('hash_equals')) { - return \hash_equals($signature, $hash); - } - $len = \min(static::safeStrlen($signature), static::safeStrlen($hash)); - - $status = 0; - for ($i = 0; $i < $len; $i++) { - $status |= (\ord($signature[$i]) ^ \ord($hash[$i])); - } - $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); - - return ($status === 0); + return self::constantTimeEquals($signature, $hash); } } @@ -314,7 +334,12 @@ class JWT */ public static function jsonEncode($input) { - $json = \json_encode($input); + if (PHP_VERSION_ID >= 50400) { + $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); + } else { + // PHP 5.3 only + $json = \json_encode($input); + } if ($errno = \json_last_error()) { static::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { @@ -352,6 +377,69 @@ class JWT return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } + + /** + * Determine if an algorithm has been provided for each Key + * + * @param Key|array $keyOrKeyArray + * @param string|null $kid + * + * @throws UnexpectedValueException + * + * @return array containing the keyMaterial and algorithm + */ + private static function getKey($keyOrKeyArray, $kid = null) + { + if ($keyOrKeyArray instanceof Key) { + return $keyOrKeyArray; + } + + if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { + foreach ($keyOrKeyArray as $keyId => $key) { + if (!$key instanceof Key) { + throw new UnexpectedValueException( + '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' + . 'array of Firebase\JWT\Key keys' + ); + } + } + if (!isset($kid)) { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + if (!isset($keyOrKeyArray[$kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + } + + return $keyOrKeyArray[$kid]; + } + + throw new UnexpectedValueException( + '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' + . 'array of Firebase\JWT\Key keys' + ); + } + + /** + * @param string $left + * @param string $right + * @return bool + */ + public static function constantTimeEquals($left, $right) + { + if (\function_exists('hash_equals')) { + return \hash_equals($left, $right); + } + $len = \min(static::safeStrlen($left), static::safeStrlen($right)); + + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= (\ord($left[$i]) ^ \ord($right[$i])); + } + $status |= (static::safeStrlen($left) ^ static::safeStrlen($right)); + + return ($status === 0); + } + /** * Helper method to create a JSON error. * @@ -415,9 +503,9 @@ class JWT } return self::encodeDER( - self::ASN1_SEQUENCE, - self::encodeDER(self::ASN1_INTEGER, $r) . - self::encodeDER(self::ASN1_INTEGER, $s) + self::$asn1Sequence, + self::encodeDER(self::$asn1Integer, $r) . + self::encodeDER(self::$asn1Integer, $s) ); } @@ -431,7 +519,7 @@ class JWT private static function encodeDER($type, $value) { $tag_header = 0; - if ($type === self::ASN1_SEQUENCE) { + if ($type === self::$asn1Sequence) { $tag_header |= 0x20; } @@ -496,7 +584,7 @@ class JWT } // Value - if ($type == self::ASN1_BIT_STRING) { + if ($type == self::$asn1BitString) { $pos++; // Skip the first contents octet (padding indicator) $data = \substr($der, $pos, $len - 1); $pos += $len - 1; diff --git a/lib/php-jwt/src/Key.php b/lib/php-jwt/src/Key.php new file mode 100644 index 00000000000..f1ede6f2717 --- /dev/null +++ b/lib/php-jwt/src/Key.php @@ -0,0 +1,59 @@ +keyMaterial = $keyMaterial; + $this->algorithm = $algorithm; + } + + /** + * Return the algorithm valid for this key + * + * @return string + */ + public function getAlgorithm() + { + return $this->algorithm; + } + + /** + * @return string|resource|OpenSSLAsymmetricKey + */ + public function getKeyMaterial() + { + return $this->keyMaterial; + } +} diff --git a/lib/php-jwt/src/SignatureInvalidException.php b/lib/php-jwt/src/SignatureInvalidException.php index 87cb34df79b..d35dee9f185 100644 --- a/lib/php-jwt/src/SignatureInvalidException.php +++ b/lib/php-jwt/src/SignatureInvalidException.php @@ -1,4 +1,5 @@