diff --git a/lib/php-jwt/readme_moodle.txt b/lib/php-jwt/readme_moodle.txt index 0e617030899..f454f109324 100644 --- a/lib/php-jwt/readme_moodle.txt +++ b/lib/php-jwt/readme_moodle.txt @@ -14,8 +14,3 @@ Instructions 5. Unzip it in lib as php-jwt. 6. Update entry for this library in lib/thirdpartylibs.xml. -2023/01/26 ----------- -- src/JWT.php file has minor changes for PHP 8.2 compatibility. See MDL-76415 for more details. - Since version v6.3.1, the php-jwt already has the fix, so if someone executing the upgrading version and - it has the patch, please ignore this note. diff --git a/lib/php-jwt/src/CachedKeySet.php b/lib/php-jwt/src/CachedKeySet.php index e2215b3091c..baf801f13f0 100644 --- a/lib/php-jwt/src/CachedKeySet.php +++ b/lib/php-jwt/src/CachedKeySet.php @@ -3,6 +3,7 @@ namespace Firebase\JWT; use ArrayAccess; +use InvalidArgumentException; use LogicException; use OutOfBoundsException; use Psr\Cache\CacheItemInterface; @@ -10,6 +11,7 @@ use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use RuntimeException; +use UnexpectedValueException; /** * @implements ArrayAccess @@ -41,7 +43,7 @@ class CachedKeySet implements ArrayAccess */ private $cacheItem; /** - * @var array + * @var array> */ private $keySet; /** @@ -101,7 +103,7 @@ class CachedKeySet implements ArrayAccess if (!$this->keyIdExists($keyId)) { throw new OutOfBoundsException('Key ID not found'); } - return $this->keySet[$keyId]; + return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg); } /** @@ -130,15 +132,43 @@ class CachedKeySet implements ArrayAccess throw new LogicException('Method not implemented'); } + /** + * @return array + */ + private function formatJwksForCache(string $jwks): array + { + $jwks = json_decode($jwks, true); + + if (!isset($jwks['keys'])) { + throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); + } + + if (empty($jwks['keys'])) { + throw new InvalidArgumentException('JWK Set did not contain any keys'); + } + + $keys = []; + foreach ($jwks['keys'] as $k => $v) { + $kid = isset($v['kid']) ? $v['kid'] : $k; + $keys[(string) $kid] = $v; + } + + return $keys; + } + private function keyIdExists(string $keyId): bool { if (null === $this->keySet) { $item = $this->getCacheItem(); // Try to load keys from cache if ($item->isHit()) { - // item found! Return it - $jwks = $item->get(); - $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); + // item found! retrieve it + $this->keySet = $item->get(); + // If the cached item is a string, the JWKS response was cached (previous behavior). + // Parse this into expected format array instead. + if (\is_string($this->keySet)) { + $this->keySet = $this->formatJwksForCache($this->keySet); + } } } @@ -146,17 +176,16 @@ class CachedKeySet implements ArrayAccess if ($this->rateLimitExceeded()) { return false; } - $request = $this->httpFactory->createRequest('get', $this->jwksUri); + $request = $this->httpFactory->createRequest('GET', $this->jwksUri); $jwksResponse = $this->httpClient->sendRequest($request); - $jwks = (string) $jwksResponse->getBody(); - $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); + $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); if (!isset($this->keySet[$keyId])) { return false; } $item = $this->getCacheItem(); - $item->set($jwks); + $item->set($this->keySet); if ($this->expiresAfter) { $item->expiresAfter($this->expiresAfter); } diff --git a/lib/php-jwt/src/JWK.php b/lib/php-jwt/src/JWK.php index 15631ecc246..c7eff8ae483 100644 --- a/lib/php-jwt/src/JWK.php +++ b/lib/php-jwt/src/JWK.php @@ -26,6 +26,7 @@ class JWK private const ASN1_BIT_STRING = 0x03; private const EC_CURVES = [ 'P-256' => '1.2.840.10045.3.1.7', // Len: 64 + 'secp256k1' => '1.3.132.0.10', // Len: 64 // 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported) // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) ]; diff --git a/lib/php-jwt/src/JWT.php b/lib/php-jwt/src/JWT.php index d047f20322c..269e8caf0d3 100644 --- a/lib/php-jwt/src/JWT.php +++ b/lib/php-jwt/src/JWT.php @@ -55,6 +55,7 @@ class JWT public static $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], + 'ES256K' => ['openssl', 'SHA256'], 'HS256' => ['hash_hmac', 'SHA256'], 'HS384' => ['hash_hmac', 'SHA384'], 'HS512' => ['hash_hmac', 'SHA512'], @@ -76,7 +77,7 @@ class JWT * * @return stdClass The JWT's payload as a PHP object * - * @throws InvalidArgumentException Provided key/key-array was empty + * @throws InvalidArgumentException Provided key/key-array was empty or malformed * @throws DomainException Provided JWT is malformed * @throws UnexpectedValueException Provided JWT was invalid * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed @@ -132,11 +133,11 @@ class JWT // See issue #351 throw new UnexpectedValueException('Incorrect key for this algorithm'); } - if ($header->alg === 'ES256' || $header->alg === 'ES384') { - // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures + if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) { + // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures $sig = self::signatureToDER($sig); } - if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { // TODO: Remove this modification in MDL-76415. + if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -166,12 +167,12 @@ class JWT } /** - * Converts and signs a PHP object or array into a JWT string. + * Converts and signs a PHP array into a JWT string. * * @param array $payload PHP array * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. - * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', + * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * @param string $keyId * @param array $head An array with header elements to attach * @@ -210,8 +211,8 @@ class JWT * * @param string $msg The message to sign * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. - * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', + * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * @@ -238,7 +239,7 @@ class JWT if (!$success) { throw new DomainException('OpenSSL unable to sign data'); } - if ($alg === 'ES256') { + if ($alg === 'ES256' || $alg === 'ES256K') { $signature = self::signatureFromDER($signature, 256); } elseif ($alg === 'ES384') { $signature = self::signatureFromDER($signature, 384); @@ -255,6 +256,9 @@ class JWT // The last non-empty line is used as the key. $lines = array_filter(explode("\n", $key)); $key = base64_decode((string) end($lines)); + if (\strlen($key) === 0) { + throw new DomainException('Key cannot be empty string'); + } return sodium_crypto_sign_detached($msg, $key); } catch (Exception $e) { throw new DomainException($e->getMessage(), 0, $e); @@ -312,6 +316,12 @@ class JWT // The last non-empty line is used as the key. $lines = array_filter(explode("\n", $keyMaterial)); $key = base64_decode((string) end($lines)); + if (\strlen($key) === 0) { + throw new DomainException('Key cannot be empty string'); + } + if (\strlen($signature) === 0) { + throw new DomainException('Signature cannot be empty string'); + } return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { throw new DomainException($e->getMessage(), 0, $e); @@ -425,14 +435,15 @@ class JWT return $keyOrKeyArray; } + if (empty($kid)) { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + if ($keyOrKeyArray instanceof CachedKeySet) { // Skip "isset" check, as this will automatically refresh if not set return $keyOrKeyArray[$kid]; } - if (empty($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'); } diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index b0473bd7fe3..c542b3272d6 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -516,7 +516,7 @@ All rights reserved. php-jwt A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to RFC 7519 A simple library to encode and decode JSON Web Tokens (JWT) in PHP, conforming to RFC 7519 - 6.3.0 + 6.4.0 BSD 3-Clause https://github.com/firebase/php-jwt