From 31a7c1692907a10dfdb2ea88115ac7164fe53d58 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 4 Aug 2021 05:42:09 -0400 Subject: [PATCH 1/7] Proposed Fix for #351 This aims to provide backwards compatibility by guessing the algorithm for a key based on the key's contents, but it will likely fail in corner cases. If this is merged, users **SHOULD** be explicit about the algorithms they're using. i.e. Instead of $keyAsString, pass in (new JWTKey($keyAsString, 'ES384')) --- src/JWT.php | 75 ++++++++++++++++++++++------ src/Keys/JWTKey.php | 113 +++++++++++++++++++++++++++++++++++++++++++ src/Keys/Keyring.php | 81 +++++++++++++++++++++++++++++++ 3 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 src/Keys/JWTKey.php create mode 100644 src/Keys/Keyring.php diff --git a/src/JWT.php b/src/JWT.php index 99d6dcd2..36abd0bd 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -2,8 +2,11 @@ namespace Firebase\JWT; +use ArrayAccess; use DomainException; use Exception; +use Firebase\JWT\Keys\JWTKey; +use Firebase\JWT\Keys\Keyring; use InvalidArgumentException; use UnexpectedValueException; use DateTime; @@ -111,7 +114,9 @@ public static function decode($jwt, $key, array $allowed_algs = array()) $sig = self::signatureToDER($sig); } - if (\is_array($key) || $key instanceof \ArrayAccess) { + /** @var Keyring|JWTKey $key */ + $key = self::getKeyType($key, $allowed_algs); + if ($key instanceof Keyring) { if (isset($header->kid)) { if (!isset($key[$header->kid])) { throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); @@ -121,9 +126,16 @@ public static function decode($jwt, $key, array $allowed_algs = array()) throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); } } + if (!($key instanceof JWTKey)) { + throw new UnexpectedValueException('$key should be an instance of JWTKey'); + } // Check the signature - if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) { + if (!$key->isValidForAlg($header->alg)) { + // See issue #351 + throw new UnexpectedValueException('Incorrect key for this algorithm'); + } + if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -285,18 +297,7 @@ private static function verify($msg, $signature, $key, $alg) 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); } } @@ -384,6 +385,50 @@ public static function urlsafeB64Encode($input) return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } + /** + * @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); + } + + /** + * @param string|array|ArrayAccess $oldType + * @param string[] $algs + * @return KeyInterface + */ + public static function getKeyType($oldType, $algs) + { + if ($oldType instanceof KeyInterface) { + return $oldType; + } + if (is_string($oldType)) { + return new JWTKey($oldType, $algs); + } + if (is_array($oldType) || $oldType instanceof ArrayAccess) { + $keyring = new Keyring(array()); + foreach ($oldType as $kid => $key) { + $keyring[$kid] = new JWTKey($key, $algs); + } + return $keyring; + } + throw new InvalidArgumentException('Invalid type: Must be string or array'); + } + /** * Helper method to create a JSON error. * @@ -414,7 +459,7 @@ private static function handleJsonError($errno) * * @return int */ - private static function safeStrlen($str) + public static function safeStrlen($str) { if (\function_exists('mb_strlen')) { return \mb_strlen($str, '8bit'); diff --git a/src/Keys/JWTKey.php b/src/Keys/JWTKey.php new file mode 100644 index 00000000..6e095ad7 --- /dev/null +++ b/src/Keys/JWTKey.php @@ -0,0 +1,113 @@ +keyMaterial = $keyMaterial; + $this->alg = $alg; + } + + /** + * Is the header algorithm valid for this key? + * + * @param string $headerAlg + * @return bool + */ + public function isValidForAlg($headerAlg) + { + return JWT::constantTimeEquals($this->alg, $headerAlg); + } + + /** + * @return string + */ + public function getKeyMaterial() + { + return $this->keyMaterial; + } + + /** + * This is a best-effort attempt to guess the algorithm for a given key + * based on its contents. + * + * It will probably be wrong in a lot of corner cases. + * + * If it is, construct a JWTKey object and/or Keyring of JWTKey objects + * with the correct algorithms. + * + * @param string $keyMaterial + * @param array $candidates + * @return string + */ + public static function guessAlgFromKeyMaterial($keyMaterial, array $candidates = array()) + { + $length = JWT::safeStrlen($keyMaterial); + if ($length >= 720) { + // RSA keys + if (preg_match('#^-+BEGIN.+(PRIVATE|PUBLIC) KEY-+#', $keyMaterial)) { + if (in_array('RS512', $candidates)) { + return 'RS512'; + } + if (in_array('RS384', $candidates)) { + return 'RS384'; + } + return 'RS256'; + } + } elseif ($length >= 220) { + // ECDSA private keys + if (preg_match('#^-+BEGIN EC PRIVATE KEY-+#', $keyMaterial)) { + if (in_array('ES512', $candidates)) { + return 'ES512'; + } + if (in_array('ES384', $candidates)) { + return 'ES384'; + } + return 'ES256'; + } + } elseif ($length >= 170) { + // ECDSA public keys + if (preg_match('#^-+BEGIN EC PUBLICY-+#', $keyMaterial)) { + if (in_array('ES512', $candidates)) { + return 'ES512'; + } + if (in_array('ES384', $candidates)) { + return 'ES384'; + } + return 'ES256'; + } + } elseif ($length >= 40 && $length <= 88) { + // Likely base64-encoded EdDSA key + if (in_array('EdDSA', $candidates)) { + return 'EdDSA'; + } + } + + // Last resort: HMAC + if (in_array('HS512', $candidates)) { + return 'HS512'; + } + if (in_array('HS384', $candidates)) { + return 'HS384'; + } + return 'HS256'; + } +} diff --git a/src/Keys/Keyring.php b/src/Keys/Keyring.php new file mode 100644 index 00000000..72572a86 --- /dev/null +++ b/src/Keys/Keyring.php @@ -0,0 +1,81 @@ + $mapping */ + private $mapping; + + /** + * @param array $mapping + */ + public function __construct(array $mapping = array()) + { + $this->mapping = $mapping; + } + + /** + * @param string $keyId + * @param JWTKey $key + * @return $this + */ + public function mapKeyId($keyId, JWTKey $key) + { + $this->mapping[$keyId] = $key; + return $this; + } + + /** + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + if (!is_string($offset)) { + throw new RuntimeException('Type error: argument 1 must be a string'); + } + return array_key_exists($offset, $this->mapping); + } + + /** + * @param mixed $offset + * @return JWTKey + */ + public function offsetGet($offset) + { + $value = $this->mapping[$offset]; + if (!($value instanceof JWTKey)) { + throw new RuntimeException('Type error: return value not an instance of JWTKey'); + } + return $value; + } + + /** + * @param string $offset + * @param JWTKey $value + */ + public function offsetSet($offset, $value) + { + if (!is_string($offset)) { + throw new RuntimeException('Type error: argument 1 must be a string'); + } + if (!($value instanceof JWTKey)) { + throw new RuntimeException('Type error: argument 2 must be an instance of JWT'); + } + $this->mapKeyId($offset, $value); + } + + /** + * @param string $offset + */ + public function offsetUnset($offset) + { + if (!is_string($offset)) { + throw new RuntimeException('Type error: argument 1 must be a string'); + } + unset($this->mapping[$offset]); + } +} From e08160a44d4fb7b639c791672419a91d557750e1 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 4 Aug 2021 06:03:00 -0400 Subject: [PATCH 2/7] Add unit tests for current behavior --- tests/Keys/JWTKeyTest.php | 87 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/Keys/JWTKeyTest.php diff --git a/tests/Keys/JWTKeyTest.php b/tests/Keys/JWTKeyTest.php new file mode 100644 index 00000000..6084fa7a --- /dev/null +++ b/tests/Keys/JWTKeyTest.php @@ -0,0 +1,87 @@ +getEcdsaPublicKey(); + $rsa = $this->getRsaPublicKey(); + $misc = 'maybe_use_paseto_instead'; + + $this->assertSame( + 'RS512', + JWTKey::guessAlgFromKeyMaterial($rsa, array('RS512')) + ); + $this->assertSame( + 'RS384', + JWTKey::guessAlgFromKeyMaterial($rsa, array('RS384')) + ); + $this->assertSame( + 'RS256', + JWTKey::guessAlgFromKeyMaterial($rsa, array('RS256')) + ); + $this->assertSame( + 'RS256', + JWTKey::guessAlgFromKeyMaterial($rsa) + ); + $this->assertSame( + 'ES384', + JWTKey::guessAlgFromKeyMaterial($ecc384, array('ES384')) + ); + $this->assertSame( + 'ES256', + JWTKey::guessAlgFromKeyMaterial($ecc384) + ); + $this->assertSame( + 'EdDSA', + JWTKey::guessAlgFromKeyMaterial($eddsa, array('EdDSA')) + ); + $this->assertSame( + 'HS256', + JWTKey::guessAlgFromKeyMaterial($eddsa) + ); + $this->assertSame( + 'HS384', + JWTKey::guessAlgFromKeyMaterial($misc, array('HS512')) + ); + $this->assertSame( + 'HS384', + JWTKey::guessAlgFromKeyMaterial($misc, array('HS384')) + ); + $this->assertSame( + 'HS256', + JWTKey::guessAlgFromKeyMaterial($misc, array('HS256')) + ); + $this->assertSame( + 'HS256', + JWTKey::guessAlgFromKeyMaterial($misc) + ); + } + + public function getRsaPublicKey() + { + $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', + 'private_key_bits' => 1024, + 'private_key_type' => OPENSSL_KEYTYPE_RSA)); + $pubKey = openssl_pkey_get_details($privKey); + return $pubKey['key']; + } + + public function getEcdsaPublicKey() + { + $privKey = openssl_pkey_new( + array( + 'curve_name' => 'secp384r1', + 'digest_alg' => 'sha384', + 'private_key_bits' => 384, + 'private_key_type' => OPENSSL_KEYTYPE_EC + ) + ); + $pubKey = openssl_pkey_get_details($privKey); + return $pubKey['key']; + } +} From 28eb0e3a220aee486986fc1398b7c4f5e9a1c143 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 3 Nov 2021 13:44:10 -0700 Subject: [PATCH 3/7] add key object support in v5 --- .github/actions/entrypoint.sh | 1 + README.md | 18 ++++-- src/JWT.php | 118 +++++++++++++++++++--------------- src/Key.php | 58 +++++++++++++++++ src/Keys/JWTKey.php | 113 -------------------------------- src/Keys/Keyring.php | 81 ----------------------- tests/JWTTest.php | 28 ++++++++ tests/Keys/JWTKeyTest.php | 87 ------------------------- 8 files changed, 166 insertions(+), 338 deletions(-) create mode 100644 src/Key.php delete mode 100644 src/Keys/JWTKey.php delete mode 100644 src/Keys/Keyring.php delete mode 100644 tests/Keys/JWTKeyTest.php diff --git a/.github/actions/entrypoint.sh b/.github/actions/entrypoint.sh index 8b6b9e1b..40402bc8 100755 --- a/.github/actions/entrypoint.sh +++ b/.github/actions/entrypoint.sh @@ -5,6 +5,7 @@ apt-get install -y --no-install-recommends \ git \ zip \ curl \ + ca-certificates \ unzip \ wget diff --git a/README.md b/README.md index a8556aa5..4194b9a6 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Example ------- ```php use Firebase\JWT\JWT; +use Firebase\JWT\Key; $key = "example_key"; $payload = array( @@ -43,7 +44,7 @@ $payload = array( * for a list of spec-compliant algorithms. */ $jwt = JWT::encode($payload, $key); -$decoded = JWT::decode($jwt, $key, array('HS256')); +$decoded = JWT::decode($jwt, new Key($key, 'HS256')); print_r($decoded); @@ -62,12 +63,13 @@ $decoded_array = (array) $decoded; * Source: http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#nbfDef */ JWT::$leeway = 60; // $leeway in seconds -$decoded = JWT::decode($jwt, $key, array('HS256')); +$decoded = JWT::decode($jwt, new Key($key, 'HS256')); ``` Example with RS256 (openssl) ---------------------------- ```php use Firebase\JWT\JWT; +use Firebase\JWT\Key; $privateKey = << []]; // JWK::parseKeySet($jwks) returns an associative array of **kid** to private // key. Pass this as the second parameter to JWT::decode. -JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm); +JWT::decode($payload, JWK::parseKeySet($jwks)); ``` Changelog diff --git a/src/JWT.php b/src/JWT.php index 36abd0bd..b7eded18 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -61,11 +61,13 @@ class JWT * 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 array of Key objects. * If the algorithm used is asymmetric, this is the public key - * @param array $allowed_algs List of supported verification algorithms + * Each Key object contains an algorithm and matching key. * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param array $allowed_algs [DEPRECATED] List of supported verification algorithms. Only + * should be used for BC. * * @return object The JWT's payload as a PHP object * @@ -79,11 +81,11 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $key, array $allowed_algs = array()) + public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array()) { $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); @@ -106,36 +108,32 @@ public static function decode($jwt, $key, array $allowed_algs = array()) 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'); + + list($keyMaterial, $algorithm) = self::getKeyMaterialAndAlgorithm( + $keyOrKeyArray, + empty($header->kid) ? null : $header->kid + ); + + if (empty($algorithm)) { + // Use deprecated "allowed_algs" to determine if the algorithm is supported. + // This opens up the possibility of an attack in some implementations. + // @see https://github.com/firebase/php-jwt/issues/351 + if (!\in_array($header->alg, $allowed_algs)) { + throw new UnexpectedValueException('Algorithm not allowed'); + } + } else { + // Check the algorithm + if (!self::constantTimeEquals($algorithm, $header->alg)) { + // 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 $sig = self::signatureToDER($sig); } - /** @var Keyring|JWTKey $key */ - $key = self::getKeyType($key, $allowed_algs); - if ($key instanceof Keyring) { - 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'); - } - } - if (!($key instanceof JWTKey)) { - throw new UnexpectedValueException('$key should be an instance of JWTKey'); - } - - // Check the signature - if (!$key->isValidForAlg($header->alg)) { - // See issue #351 - throw new UnexpectedValueException('Incorrect key for this algorithm'); - } - if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { + if (!static::verify("$headb64.$bodyb64", $sig, $keyMaterial, $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -385,6 +383,47 @@ public static function urlsafeB64Encode($input) return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } + + /** + * Determine if an algorithm has been provided for each Key + * + * @param string|array $keyOrKeyArray + * + * @return an array containing the keyMaterial and algorithm + */ + private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) + { + if (is_string($keyOrKeyArray)) { + return [$keyOrKeyArray, null]; + } + + if ($keyOrKeyArray instanceof Key) { + return [$keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm()]; + } + + if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { + 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'); + } + + $key = $keyOrKeyArray[$kid]; + + if ($key instanceof Key) { + return [$key->getKeyMaterial(), $key->getAlgorithm()]; + } + + return [$key, null]; + } + + throw new UnexpectedValueException( + '$keyOrKeyArray must be a string key, an array of string keys, ' + . 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys' + ); + } + /** * @param string $left * @param string $right @@ -406,29 +445,6 @@ public static function constantTimeEquals($left, $right) return ($status === 0); } - /** - * @param string|array|ArrayAccess $oldType - * @param string[] $algs - * @return KeyInterface - */ - public static function getKeyType($oldType, $algs) - { - if ($oldType instanceof KeyInterface) { - return $oldType; - } - if (is_string($oldType)) { - return new JWTKey($oldType, $algs); - } - if (is_array($oldType) || $oldType instanceof ArrayAccess) { - $keyring = new Keyring(array()); - foreach ($oldType as $kid => $key) { - $keyring[$kid] = new JWTKey($key, $algs); - } - return $keyring; - } - throw new InvalidArgumentException('Invalid type: Must be string or array'); - } - /** * Helper method to create a JSON error. * diff --git a/src/Key.php b/src/Key.php new file mode 100644 index 00000000..76a4d40f --- /dev/null +++ b/src/Key.php @@ -0,0 +1,58 @@ +keyMaterial = $keyMaterial; + $this->algorithm = $algorithm; + } + + /** + * Return the algorithm valid for this key + * + * @return string + */ + public function getAlgorithm() + { + return $this->algorithm; + } + + /** + * @return string|resource + */ + public function getKeyMaterial() + { + return $this->keyMaterial; + } +} diff --git a/src/Keys/JWTKey.php b/src/Keys/JWTKey.php deleted file mode 100644 index 6e095ad7..00000000 --- a/src/Keys/JWTKey.php +++ /dev/null @@ -1,113 +0,0 @@ -keyMaterial = $keyMaterial; - $this->alg = $alg; - } - - /** - * Is the header algorithm valid for this key? - * - * @param string $headerAlg - * @return bool - */ - public function isValidForAlg($headerAlg) - { - return JWT::constantTimeEquals($this->alg, $headerAlg); - } - - /** - * @return string - */ - public function getKeyMaterial() - { - return $this->keyMaterial; - } - - /** - * This is a best-effort attempt to guess the algorithm for a given key - * based on its contents. - * - * It will probably be wrong in a lot of corner cases. - * - * If it is, construct a JWTKey object and/or Keyring of JWTKey objects - * with the correct algorithms. - * - * @param string $keyMaterial - * @param array $candidates - * @return string - */ - public static function guessAlgFromKeyMaterial($keyMaterial, array $candidates = array()) - { - $length = JWT::safeStrlen($keyMaterial); - if ($length >= 720) { - // RSA keys - if (preg_match('#^-+BEGIN.+(PRIVATE|PUBLIC) KEY-+#', $keyMaterial)) { - if (in_array('RS512', $candidates)) { - return 'RS512'; - } - if (in_array('RS384', $candidates)) { - return 'RS384'; - } - return 'RS256'; - } - } elseif ($length >= 220) { - // ECDSA private keys - if (preg_match('#^-+BEGIN EC PRIVATE KEY-+#', $keyMaterial)) { - if (in_array('ES512', $candidates)) { - return 'ES512'; - } - if (in_array('ES384', $candidates)) { - return 'ES384'; - } - return 'ES256'; - } - } elseif ($length >= 170) { - // ECDSA public keys - if (preg_match('#^-+BEGIN EC PUBLICY-+#', $keyMaterial)) { - if (in_array('ES512', $candidates)) { - return 'ES512'; - } - if (in_array('ES384', $candidates)) { - return 'ES384'; - } - return 'ES256'; - } - } elseif ($length >= 40 && $length <= 88) { - // Likely base64-encoded EdDSA key - if (in_array('EdDSA', $candidates)) { - return 'EdDSA'; - } - } - - // Last resort: HMAC - if (in_array('HS512', $candidates)) { - return 'HS512'; - } - if (in_array('HS384', $candidates)) { - return 'HS384'; - } - return 'HS256'; - } -} diff --git a/src/Keys/Keyring.php b/src/Keys/Keyring.php deleted file mode 100644 index 72572a86..00000000 --- a/src/Keys/Keyring.php +++ /dev/null @@ -1,81 +0,0 @@ - $mapping */ - private $mapping; - - /** - * @param array $mapping - */ - public function __construct(array $mapping = array()) - { - $this->mapping = $mapping; - } - - /** - * @param string $keyId - * @param JWTKey $key - * @return $this - */ - public function mapKeyId($keyId, JWTKey $key) - { - $this->mapping[$keyId] = $key; - return $this; - } - - /** - * @param mixed $offset - * @return bool - */ - public function offsetExists($offset) - { - if (!is_string($offset)) { - throw new RuntimeException('Type error: argument 1 must be a string'); - } - return array_key_exists($offset, $this->mapping); - } - - /** - * @param mixed $offset - * @return JWTKey - */ - public function offsetGet($offset) - { - $value = $this->mapping[$offset]; - if (!($value instanceof JWTKey)) { - throw new RuntimeException('Type error: return value not an instance of JWTKey'); - } - return $value; - } - - /** - * @param string $offset - * @param JWTKey $value - */ - public function offsetSet($offset, $value) - { - if (!is_string($offset)) { - throw new RuntimeException('Type error: argument 1 must be a string'); - } - if (!($value instanceof JWTKey)) { - throw new RuntimeException('Type error: argument 2 must be an instance of JWT'); - } - $this->mapKeyId($offset, $value); - } - - /** - * @param string $offset - */ - public function offsetUnset($offset) - { - if (!is_string($offset)) { - throw new RuntimeException('Type error: argument 1 must be a string'); - } - unset($this->mapping[$offset]); - } -} diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 3dee0450..63386d88 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -344,6 +344,34 @@ public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) $this->assertEquals('bar', $decoded->foo); } + /** + * @runInSeparateProcess + * @dataProvider provideEncodeDecode + */ + public function testEncodeDecodeWithKeyObject($privateKeyFile, $publicKeyFile, $alg) + { + $privateKey = file_get_contents($privateKeyFile); + $payload = array('foo' => 'bar'); + $encoded = JWT::encode($payload, $privateKey, $alg); + + // Verify decoding succeeds + $publicKey = file_get_contents($publicKeyFile); + $decoded = JWT::decode($encoded, new Key($publicKey, $alg)); + + $this->assertEquals('bar', $decoded->foo); + } + + public function testArrayAccessKIDChooserWithKeyObject() + { + $keys = new ArrayObject(array( + '1' => new Key('my_key', 'HS256'), + '2' => new Key('my_key2', 'HS256'), + )); + $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + $decoded = JWT::decode($msg, $keys); + $this->assertEquals($decoded, 'abc'); + } + public function provideEncodeDecode() { return array( diff --git a/tests/Keys/JWTKeyTest.php b/tests/Keys/JWTKeyTest.php deleted file mode 100644 index 6084fa7a..00000000 --- a/tests/Keys/JWTKeyTest.php +++ /dev/null @@ -1,87 +0,0 @@ -getEcdsaPublicKey(); - $rsa = $this->getRsaPublicKey(); - $misc = 'maybe_use_paseto_instead'; - - $this->assertSame( - 'RS512', - JWTKey::guessAlgFromKeyMaterial($rsa, array('RS512')) - ); - $this->assertSame( - 'RS384', - JWTKey::guessAlgFromKeyMaterial($rsa, array('RS384')) - ); - $this->assertSame( - 'RS256', - JWTKey::guessAlgFromKeyMaterial($rsa, array('RS256')) - ); - $this->assertSame( - 'RS256', - JWTKey::guessAlgFromKeyMaterial($rsa) - ); - $this->assertSame( - 'ES384', - JWTKey::guessAlgFromKeyMaterial($ecc384, array('ES384')) - ); - $this->assertSame( - 'ES256', - JWTKey::guessAlgFromKeyMaterial($ecc384) - ); - $this->assertSame( - 'EdDSA', - JWTKey::guessAlgFromKeyMaterial($eddsa, array('EdDSA')) - ); - $this->assertSame( - 'HS256', - JWTKey::guessAlgFromKeyMaterial($eddsa) - ); - $this->assertSame( - 'HS384', - JWTKey::guessAlgFromKeyMaterial($misc, array('HS512')) - ); - $this->assertSame( - 'HS384', - JWTKey::guessAlgFromKeyMaterial($misc, array('HS384')) - ); - $this->assertSame( - 'HS256', - JWTKey::guessAlgFromKeyMaterial($misc, array('HS256')) - ); - $this->assertSame( - 'HS256', - JWTKey::guessAlgFromKeyMaterial($misc) - ); - } - - public function getRsaPublicKey() - { - $privKey = openssl_pkey_new(array('digest_alg' => 'sha256', - 'private_key_bits' => 1024, - 'private_key_type' => OPENSSL_KEYTYPE_RSA)); - $pubKey = openssl_pkey_get_details($privKey); - return $pubKey['key']; - } - - public function getEcdsaPublicKey() - { - $privKey = openssl_pkey_new( - array( - 'curve_name' => 'secp384r1', - 'digest_alg' => 'sha384', - 'private_key_bits' => 384, - 'private_key_type' => OPENSSL_KEYTYPE_EC - ) - ); - $pubKey = openssl_pkey_get_details($privKey); - return $pubKey['key']; - } -} From 65920fde7910205e89d41739fde0a51e511ba22c Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 3 Nov 2021 13:56:45 -0700 Subject: [PATCH 4/7] fix array syntax for PHP 5.3 --- src/JWT.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index b7eded18..740c56c1 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -394,11 +394,11 @@ public static function urlsafeB64Encode($input) private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) { if (is_string($keyOrKeyArray)) { - return [$keyOrKeyArray, null]; + return array($keyOrKeyArray, null); } if ($keyOrKeyArray instanceof Key) { - return [$keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm()]; + return array($keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm()); } if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { @@ -412,10 +412,10 @@ private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) $key = $keyOrKeyArray[$kid]; if ($key instanceof Key) { - return [$key->getKeyMaterial(), $key->getAlgorithm()]; + return array($key->getKeyMaterial(), $key->getAlgorithm()); } - return [$key, null]; + return array($key, null); } throw new UnexpectedValueException( From c6964ebbe166786dccbe6ba65c3bca9da1a37a46 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 3 Nov 2021 09:31:19 -0700 Subject: [PATCH 5/7] feat: require "Key" object when decoding JWTs --- src/JWK.php | 10 +- src/JWT.php | 60 ++++------- src/Key.php | 2 +- tests/JWKTest.php | 38 +++++-- tests/JWTTest.php | 122 +++++++++-------------- tests/autoload.php.dist | 17 ---- tests/bootstrap.php | 13 ++- tests/data/ecdsa-cert.der | Bin 0 -> 913 bytes tests/data/ecdsa-cert.pem | 22 ++++ tests/data/ecdsa-private.der | Bin 0 -> 364 bytes tests/{ => data}/ecdsa-private.pem | 0 tests/{ => data}/ecdsa-public.pem | 0 tests/{ => data}/ecdsa384-private.pem | 0 tests/{ => data}/ecdsa384-public.pem | 0 tests/{ => data}/ed25519-1.pub | 0 tests/{ => data}/ed25519-1.sec | 0 tests/{ => data}/rsa-jwkset.json | 7 +- tests/{ => data}/rsa-with-passphrase.pem | 0 tests/{ => data}/rsa1-private.pem | 0 tests/{ => data}/rsa1-public.pub | 0 tests/{ => data}/rsa2-private.pem | 0 21 files changed, 140 insertions(+), 151 deletions(-) delete mode 100644 tests/autoload.php.dist create mode 100644 tests/data/ecdsa-cert.der create mode 100644 tests/data/ecdsa-cert.pem create mode 100644 tests/data/ecdsa-private.der rename tests/{ => data}/ecdsa-private.pem (100%) rename tests/{ => data}/ecdsa-public.pem (100%) rename tests/{ => data}/ecdsa384-private.pem (100%) rename tests/{ => data}/ecdsa384-public.pem (100%) rename tests/{ => data}/ed25519-1.pub (100%) rename tests/{ => data}/ed25519-1.sec (100%) rename tests/{ => data}/rsa-jwkset.json (85%) rename tests/{ => data}/rsa-with-passphrase.pem (100%) rename tests/{ => data}/rsa1-private.pem (100%) rename tests/{ => data}/rsa1-public.pub (100%) rename tests/{ => data}/rsa2-private.pem (100%) diff --git a/src/JWK.php b/src/JWK.php index 981a9ba7..4106a096 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -47,7 +47,15 @@ public static function parseKeySet(array $jwks) foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; if ($key = self::parseKey($v)) { - $keys[$kid] = $key; + if (isset($v['alg'])) { + $keys[$kid] = new Key($key, $v['alg']); + } else { + // The "alg" parameter is optional in a KTY, but is required + // for parsing in this library. Add it manually to y our JWK + // array if it doesn't already exist. + // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + throw new InvalidArgumentException('JWK key is missing "alg"'); + } } } diff --git a/src/JWT.php b/src/JWT.php index 740c56c1..278df293 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -5,8 +5,6 @@ use ArrayAccess; use DomainException; use Exception; -use Firebase\JWT\Keys\JWTKey; -use Firebase\JWT\Keys\Keyring; use InvalidArgumentException; use UnexpectedValueException; use DateTime; @@ -81,8 +79,9 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array()) + public static function decode($jwt, $keyOrKeyArray) { + // Validate JWT $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; if (empty($keyOrKeyArray)) { @@ -109,31 +108,18 @@ public static function decode($jwt, $keyOrKeyArray, array $allowed_algs = array( throw new UnexpectedValueException('Algorithm not supported'); } - list($keyMaterial, $algorithm) = self::getKeyMaterialAndAlgorithm( - $keyOrKeyArray, - empty($header->kid) ? null : $header->kid - ); + $key = self::getKey($keyOrKeyArray, empty($header->kid) ? null : $header->kid); - if (empty($algorithm)) { - // Use deprecated "allowed_algs" to determine if the algorithm is supported. - // This opens up the possibility of an attack in some implementations. - // @see https://github.com/firebase/php-jwt/issues/351 - if (!\in_array($header->alg, $allowed_algs)) { - throw new UnexpectedValueException('Algorithm not allowed'); - } - } else { - // Check the algorithm - if (!self::constantTimeEquals($algorithm, $header->alg)) { - // See issue #351 - throw new UnexpectedValueException('Incorrect key for this algorithm'); - } + // 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' || $header->alg === 'ES384') { // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } - - if (!static::verify("$headb64.$bodyb64", $sig, $keyMaterial, $header->alg)) { + if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -391,17 +377,21 @@ public static function urlsafeB64Encode($input) * * @return an array containing the keyMaterial and algorithm */ - private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) + private static function getKey($keyOrKeyArray, $kid = null) { - if (is_string($keyOrKeyArray)) { - return array($keyOrKeyArray, null); - } - if ($keyOrKeyArray instanceof Key) { - return array($keyOrKeyArray->getKeyMaterial(), $keyOrKeyArray->getAlgorithm()); + 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'); } @@ -409,18 +399,12 @@ private static function getKeyMaterialAndAlgorithm($keyOrKeyArray, $kid = null) throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); } - $key = $keyOrKeyArray[$kid]; - - if ($key instanceof Key) { - return array($key->getKeyMaterial(), $key->getAlgorithm()); - } - - return array($key, null); + return $keyOrKeyArray[$kid]; } throw new UnexpectedValueException( - '$keyOrKeyArray must be a string key, an array of string keys, ' - . 'an instance of Firebase\JWT\Key key or an array of Firebase\JWT\Key keys' + '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' + . 'array of Firebase\JWT\Key keys' ); } @@ -475,7 +459,7 @@ private static function handleJsonError($errno) * * @return int */ - public static function safeStrlen($str) + private static function safeStrlen($str) { if (\function_exists('mb_strlen')) { return \mb_strlen($str, '8bit'); diff --git a/src/Key.php b/src/Key.php index 76a4d40f..349973c2 100644 --- a/src/Key.php +++ b/src/Key.php @@ -49,7 +49,7 @@ public function getAlgorithm() } /** - * @return string|resource + * @return string|resource|OpenSSLAsymmetricKey */ public function getKeyMaterial() { diff --git a/tests/JWKTest.php b/tests/JWKTest.php index 0709836d..b908ea64 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -38,26 +38,42 @@ public function testParsePrivateKey() 'UnexpectedValueException', 'RSA private keys are not supported' ); - + $jwkSet = json_decode( - file_get_contents(__DIR__ . '/rsa-jwkset.json'), + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true ); $jwkSet['keys'][0]['d'] = 'privatekeyvalue'; - + + JWK::parseKeySet($jwkSet); + } + + public function testParsePrivateKeyWithoutAlg() + { + $this->setExpectedException( + 'InvalidArgumentException', + 'JWK key is missing "alg"' + ); + + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), + true + ); + unset($jwkSet['keys'][0]['alg']); + JWK::parseKeySet($jwkSet); } - + public function testParseKeyWithEmptyDValue() { $jwkSet = json_decode( - file_get_contents(__DIR__ . '/rsa-jwkset.json'), + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true ); - + // empty or null values are ok $jwkSet['keys'][0]['d'] = null; - + $keys = JWK::parseKeySet($jwkSet); $this->assertTrue(is_array($keys)); } @@ -65,7 +81,7 @@ public function testParseKeyWithEmptyDValue() public function testParseJwkKeySet() { $jwkSet = json_decode( - file_get_contents(__DIR__ . '/rsa-jwkset.json'), + file_get_contents(__DIR__ . '/data/rsa-jwkset.json'), true ); $keys = JWK::parseKeySet($jwkSet); @@ -93,7 +109,7 @@ public function testParseJwkKeySet_empty() */ public function testDecodeByJwkKeySetTokenExpired() { - $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); $payload = array('exp' => strtotime('-1 hour')); $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); @@ -107,7 +123,7 @@ public function testDecodeByJwkKeySetTokenExpired() */ public function testDecodeByJwkKeySet() { - $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $privKey1 = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); @@ -121,7 +137,7 @@ public function testDecodeByJwkKeySet() */ public function testDecodeByMultiJwkKeySet() { - $privKey2 = file_get_contents(__DIR__ . '/rsa2-private.pem'); + $privKey2 = file_get_contents(__DIR__ . '/data/rsa2-private.pem'); $payload = array('sub' => 'bar', 'exp' => strtotime('+10 seconds')); $msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2'); diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 63386d88..e1b770df 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -23,7 +23,7 @@ public function testDecodeFromPython() { $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg'; $this->assertEquals( - JWT::decode($msg, 'my_key', array('HS256')), + JWT::decode($msg, new Key('my_key', 'HS256')), '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123' ); } @@ -31,7 +31,7 @@ public function testDecodeFromPython() public function testUrlSafeCharacters() { $encoded = JWT::encode('f?', 'a'); - $this->assertEquals('f?', JWT::decode($encoded, 'a', array('HS256'))); + $this->assertEquals('f?', JWT::decode($encoded, new Key('a', 'HS256'))); } public function testMalformedUtf8StringsFail() @@ -53,7 +53,7 @@ public function testExpiredToken() "message" => "abc", "exp" => time() - 20); // time in the past $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testBeforeValidTokenWithNbf() @@ -63,7 +63,7 @@ public function testBeforeValidTokenWithNbf() "message" => "abc", "nbf" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testBeforeValidTokenWithIat() @@ -73,7 +73,7 @@ public function testBeforeValidTokenWithIat() "message" => "abc", "iat" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, new Key('my_key', 'HS256')); } public function testValidToken() @@ -82,7 +82,7 @@ public function testValidToken() "message" => "abc", "exp" => time() + JWT::$leeway + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); } @@ -93,7 +93,7 @@ public function testValidTokenWithLeeway() "message" => "abc", "exp" => time() - 20); // time in the past $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -106,21 +106,11 @@ public function testExpiredTokenWithLeeway() "exp" => time() - 70); // time far in the past $this->setExpectedException('Firebase\JWT\ExpiredException'); $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } - public function testValidTokenWithList() - { - $payload = array( - "message" => "abc", - "exp" => time() + 20); // time in the future - $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256', 'HS512')); - $this->assertEquals($decoded->message, 'abc'); - } - public function testValidTokenWithNbf() { $payload = array( @@ -129,7 +119,7 @@ public function testValidTokenWithNbf() "exp" => time() + 20, // time in the future "nbf" => time() - 20); $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); } @@ -140,7 +130,7 @@ public function testValidTokenWithNbfLeeway() "message" => "abc", "nbf" => time() + 20); // not before in near (leeway) future $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -153,7 +143,7 @@ public function testInvalidTokenWithNbfLeeway() "nbf" => time() + 65); // not before too far in future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('Firebase\JWT\BeforeValidException'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } @@ -164,7 +154,7 @@ public function testValidTokenWithIatLeeway() "message" => "abc", "iat" => time() + 20); // issued in near (leeway) future $encoded = JWT::encode($payload, 'my_key'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + $decoded = JWT::decode($encoded, new Key('my_key', 'HS256')); $this->assertEquals($decoded->message, 'abc'); JWT::$leeway = 0; } @@ -177,7 +167,7 @@ public function testInvalidTokenWithIatLeeway() "iat" => time() + 65); // issued too far in future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('Firebase\JWT\BeforeValidException'); - JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, new Key('my_key', 'HS256')); JWT::$leeway = 0; } @@ -188,7 +178,7 @@ public function testInvalidToken() "exp" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); - JWT::decode($encoded, 'my_key2', array('HS256')); + JWT::decode($encoded, new Key('my_key2', 'HS256')); } public function testNullKeyFails() @@ -198,7 +188,7 @@ public function testNullKeyFails() "exp" => time() + JWT::$leeway + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('InvalidArgumentException'); - JWT::decode($encoded, null, array('HS256')); + JWT::decode($encoded, new Key(null, 'HS256')); } public function testEmptyKeyFails() @@ -208,22 +198,28 @@ public function testEmptyKeyFails() "exp" => time() + JWT::$leeway + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('InvalidArgumentException'); - JWT::decode($encoded, '', array('HS256')); + JWT::decode($encoded, new Key('', 'HS256')); } public function testKIDChooser() { - $keys = array('1' => 'my_key', '2' => 'my_key2'); - $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); - $decoded = JWT::decode($msg, $keys, array('HS256')); + $keys = array( + '1' => new Key('my_key', 'HS256'), + '2' => new Key('my_key2', 'HS256') + ); + $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + $decoded = JWT::decode($msg, $keys); $this->assertEquals($decoded, 'abc'); } public function testArrayAccessKIDChooser() { - $keys = new ArrayObject(array('1' => 'my_key', '2' => 'my_key2')); - $msg = JWT::encode('abc', $keys['1'], 'HS256', '1'); - $decoded = JWT::decode($msg, $keys, array('HS256')); + $keys = new ArrayObject(array( + '1' => new Key('my_key', 'HS256'), + '2' => new Key('my_key2', 'HS256'), + )); + $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); + $decoded = JWT::decode($msg, $keys); $this->assertEquals($decoded, 'abc'); } @@ -231,46 +227,46 @@ public function testNoneAlgorithm() { $msg = JWT::encode('abc', 'my_key'); $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'my_key', array('none')); + JWT::decode($msg, new Key('my_key', 'none')); } public function testIncorrectAlgorithm() { $msg = JWT::encode('abc', 'my_key'); $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'my_key', array('RS256')); + JWT::decode($msg, new Key('my_key', 'RS256')); } - public function testMissingAlgorithm() + public function testEmptyAlgorithm() { $msg = JWT::encode('abc', 'my_key'); $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'my_key'); + JWT::decode($msg, new Key('my_key', '')); } public function testAdditionalHeaders() { $msg = JWT::encode('abc', 'my_key', 'HS256', null, array('cty' => 'test-eit;v=1')); - $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), 'abc'); } public function testInvalidSegmentCount() { $this->setExpectedException('UnexpectedValueException'); - JWT::decode('brokenheader.brokenbody', 'my_key', array('HS256')); + JWT::decode('brokenheader.brokenbody', new Key('my_key', 'HS256')); } public function testInvalidSignatureEncoding() { $msg = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwibmFtZSI6ImZvbyJ9.Q4Kee9E8o0Xfo4ADXvYA8t7dN_X_bU9K5w6tXuiSjlUxx"; $this->setExpectedException('UnexpectedValueException'); - JWT::decode($msg, 'secret', array('HS256')); + JWT::decode($msg, new Key('secret', 'HS256')); } public function testHSEncodeDecode() { $msg = JWT::encode('abc', 'my_key'); - $this->assertEquals(JWT::decode($msg, 'my_key', array('HS256')), 'abc'); + $this->assertEquals(JWT::decode($msg, new Key('my_key', 'HS256')), 'abc'); } public function testRSEncodeDecode() @@ -281,7 +277,7 @@ public function testRSEncodeDecode() $msg = JWT::encode('abc', $privKey, 'RS256'); $pubKey = openssl_pkey_get_details($privKey); $pubKey = $pubKey['key']; - $decoded = JWT::decode($msg, $pubKey, array('RS256')); + $decoded = JWT::decode($msg, new Key($pubKey, 'RS256')); $this->assertEquals($decoded, 'abc'); } @@ -294,7 +290,7 @@ public function testEdDsaEncodeDecode() $msg = JWT::encode($payload, $privKey, 'EdDSA'); $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); - $decoded = JWT::decode($msg, $pubKey, array('EdDSA')); + $decoded = JWT::decode($msg, new Key($pubKey, 'EdDSA')); $this->assertEquals('bar', $decoded->foo); } @@ -310,20 +306,20 @@ public function testInvalidEdDsaEncodeDecode() $keyPair = sodium_crypto_sign_keypair(); $pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair)); $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); - JWT::decode($msg, $pubKey, array('EdDSA')); + JWT::decode($msg, new Key($pubKey, 'EdDSA')); } public function testRSEncodeDecodeWithPassphrase() { $privateKey = openssl_pkey_get_private( - file_get_contents(__DIR__ . '/rsa-with-passphrase.pem'), + file_get_contents(__DIR__ . '/data/rsa-with-passphrase.pem'), 'passphrase' ); $jwt = JWT::encode('abc', $privateKey, 'RS256'); $keyDetails = openssl_pkey_get_details($privateKey); $pubKey = $keyDetails['key']; - $decoded = JWT::decode($jwt, $pubKey, array('RS256')); + $decoded = JWT::decode($jwt, new Key($pubKey, 'RS256')); $this->assertEquals($decoded, 'abc'); } @@ -337,23 +333,6 @@ public function testEncodeDecode($privateKeyFile, $publicKeyFile, $alg) $payload = array('foo' => 'bar'); $encoded = JWT::encode($payload, $privateKey, $alg); - // Verify decoding succeeds - $publicKey = file_get_contents($publicKeyFile); - $decoded = JWT::decode($encoded, $publicKey, array($alg)); - - $this->assertEquals('bar', $decoded->foo); - } - - /** - * @runInSeparateProcess - * @dataProvider provideEncodeDecode - */ - public function testEncodeDecodeWithKeyObject($privateKeyFile, $publicKeyFile, $alg) - { - $privateKey = file_get_contents($privateKeyFile); - $payload = array('foo' => 'bar'); - $encoded = JWT::encode($payload, $privateKey, $alg); - // Verify decoding succeeds $publicKey = file_get_contents($publicKeyFile); $decoded = JWT::decode($encoded, new Key($publicKey, $alg)); @@ -361,24 +340,13 @@ public function testEncodeDecodeWithKeyObject($privateKeyFile, $publicKeyFile, $ $this->assertEquals('bar', $decoded->foo); } - public function testArrayAccessKIDChooserWithKeyObject() - { - $keys = new ArrayObject(array( - '1' => new Key('my_key', 'HS256'), - '2' => new Key('my_key2', 'HS256'), - )); - $msg = JWT::encode('abc', $keys['1']->getKeyMaterial(), 'HS256', '1'); - $decoded = JWT::decode($msg, $keys); - $this->assertEquals($decoded, 'abc'); - } - public function provideEncodeDecode() { return array( - array(__DIR__ . '/ecdsa-private.pem', __DIR__ . '/ecdsa-public.pem', 'ES256'), - array(__DIR__ . '/ecdsa384-private.pem', __DIR__ . '/ecdsa384-public.pem', 'ES384'), - array(__DIR__ . '/rsa1-private.pem', __DIR__ . '/rsa1-public.pub', 'RS512'), - array(__DIR__ . '/ed25519-1.sec', __DIR__ . '/ed25519-1.pub', 'EdDSA'), + array(__DIR__ . '/data/ecdsa-private.pem', __DIR__ . '/data/ecdsa-public.pem', 'ES256'), + array(__DIR__ . '/data/ecdsa384-private.pem', __DIR__ . '/data/ecdsa384-public.pem', 'ES384'), + array(__DIR__ . '/data/rsa1-private.pem', __DIR__ . '/data/rsa1-public.pub', 'RS512'), + array(__DIR__ . '/data/ed25519-1.sec', __DIR__ . '/data/ed25519-1.pub', 'EdDSA'), ); } } diff --git a/tests/autoload.php.dist b/tests/autoload.php.dist deleted file mode 100644 index 2e4310a0..00000000 --- a/tests/autoload.php.dist +++ /dev/null @@ -1,17 +0,0 @@ -uCmDY#dr`9_MUXn3)V3 z7aDRKaI!Invaks=g$5f68wi3p96Vgki8+~R`9*n|iH2eZA|OF_9^T-@JO#I+#JuFp z;^cfoegj^RI2R9_dwza;PO71(fiOsbn}-J?px~U7Uz!5b&&_nd zafgH-ni#zeni!ec*uf#s#Awj?or#grKnE(o$jGF~@E-^m7#J9V7!E*UFklePq5xt8 zC2^_w!=ey%%=E@8tI5wdzup^KHluA%sw~@vGr=3aS)a49Q@@np%`D1rWUd)U+jEx5 zS!opyh3BemZ=7E@fyI#}Tm0guhdNH~KlVI(n!Lo}Q#o^OqwZ_t1z#3gy55*^Xqx|H zr$_&!>Sy;of9+MnKTk~ioN;)zVcfaX+Z-~?_XmDOcmeDckW=?O`?Yqt_`H_SJ5L?@ zldKvD^fR*)1IsPX%$G}lou0|iwf9IQcZmc0tfi*=O85V*vD>M^#e6nw@m7mM+n^)$ z0qbgyUp{h#!Qr#{A-BZc&dE~~<$3C`af6z literal 0 HcmV?d00001 diff --git a/tests/data/ecdsa-cert.pem b/tests/data/ecdsa-cert.pem new file mode 100644 index 00000000..1a425aaf --- /dev/null +++ b/tests/data/ecdsa-cert.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDjTCCAzOgAwIBAgIUQ2MPWOBcUI2CDXxQUivuaDrDO5YwCgYIKoZIzj0EAwIw +gaExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T +YW4gRnJhbmNpc2NvMQ8wDQYDVQQKDAZHb29nbGUxFTATBgNVBAsMDEdvb2dsZSBD +bG91ZDEWMBQGA1UEAwwNQnJlbnQgU2hhZmZlcjElMCMGCSqGSIb3DQEJARYWYmV0 +dGVyYnJlbnRAZ29vZ2xlLmNvbTAeFw0yMTA4MTgxODE5MzdaFw0yMzA4MTgxODE5 +MzdaMIGhMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UE +BwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGR29vZ2xlMRUwEwYDVQQLDAxHb29n +bGUgQ2xvdWQxFjAUBgNVBAMMDUJyZW50IFNoYWZmZXIxJTAjBgkqhkiG9w0BCQEW +FmJldHRlcmJyZW50QGdvb2dsZS5jb20wggFLMIIBAwYHKoZIzj0CATCB9wIBATAs +BgcqhkjOPQEBAiEA/////wAAAAEAAAAAAAAAAAAAAAD///////////////8wWwQg +/////wAAAAEAAAAAAAAAAAAAAAD///////////////wEIFrGNdiqOpPns+u9VXaY +hrxlHQawzFOw9jvOPD4n0mBLAxUAxJ02CIbnBJNqZnjhE50mt4GffpAEQQRrF9Hy +4SxCR/i85uVjpEDydwN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNX +azFezsu2QGg3v1H1AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVEC +AQEDQgAE2klp6aX6y5kAir3EWQt0QAeapTW+db/9fD65KAoDzVajtThxPVLEf1Cu +fcfTxMQAQPM3wkZhu0NjlWFetcMdcaNTMFEwHQYDVR0OBBYEFLsFk5rxieg+ZZia +ktPbxK1mOIAHMB8GA1UdIwQYMBaAFLsFk5rxieg+ZZiaktPbxK1mOIAHMA8GA1Ud +EwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAM3+1Ay5sNVNa/YzpKBHsENV +3NpBqy2wnQadm592KnwCAiA6N2ewReTYgBF92nrMeH4RAwM+AKjTP3eXdmkdcCVz +Rw== +-----END CERTIFICATE----- diff --git a/tests/data/ecdsa-private.der b/tests/data/ecdsa-private.der new file mode 100644 index 0000000000000000000000000000000000000000..baaed000d8dc2f9743a81dd0d5d90296fdb91bf9 GIT binary patch literal 364 zcmXqLV$5J-WMok|@jvdp%GPaG;n|*5i{G1+ZrQlwxYqH?kY>+smi7*_v==n~GHCn` zRBE8Z#;(=oan6>Jkx7x^KM*i5Ffal!9Du}Nz#y7M0mKGM;!^X6MIq{#>5WxZlb>&X zy*IRMM%$iLS+)&lf;W7#K4)X6eksA5S(M?(Tr-Zg=PZ-6(kdPb&sE#rIKOTJiz7?6 z_{C2Tb)4LP?0NPyd5OcPa^~7b-PgtozAUtKy)om^H2=p=kN!#3&+dEv+N*|to|yJI z Date: Wed, 3 Nov 2021 14:13:58 -0700 Subject: [PATCH 6/7] cleanup --- src/JWK.php | 2 +- tests/data/ecdsa-cert.pem | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 tests/data/ecdsa-cert.pem diff --git a/src/JWK.php b/src/JWK.php index 4106a096..c53251d3 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -51,7 +51,7 @@ public static function parseKeySet(array $jwks) $keys[$kid] = new Key($key, $v['alg']); } else { // The "alg" parameter is optional in a KTY, but is required - // for parsing in this library. Add it manually to y our JWK + // 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 InvalidArgumentException('JWK key is missing "alg"'); diff --git a/tests/data/ecdsa-cert.pem b/tests/data/ecdsa-cert.pem deleted file mode 100644 index 1a425aaf..00000000 --- a/tests/data/ecdsa-cert.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDjTCCAzOgAwIBAgIUQ2MPWOBcUI2CDXxQUivuaDrDO5YwCgYIKoZIzj0EAwIw -gaExCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1T -YW4gRnJhbmNpc2NvMQ8wDQYDVQQKDAZHb29nbGUxFTATBgNVBAsMDEdvb2dsZSBD -bG91ZDEWMBQGA1UEAwwNQnJlbnQgU2hhZmZlcjElMCMGCSqGSIb3DQEJARYWYmV0 -dGVyYnJlbnRAZ29vZ2xlLmNvbTAeFw0yMTA4MTgxODE5MzdaFw0yMzA4MTgxODE5 -MzdaMIGhMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UE -BwwNU2FuIEZyYW5jaXNjbzEPMA0GA1UECgwGR29vZ2xlMRUwEwYDVQQLDAxHb29n -bGUgQ2xvdWQxFjAUBgNVBAMMDUJyZW50IFNoYWZmZXIxJTAjBgkqhkiG9w0BCQEW -FmJldHRlcmJyZW50QGdvb2dsZS5jb20wggFLMIIBAwYHKoZIzj0CATCB9wIBATAs -BgcqhkjOPQEBAiEA/////wAAAAEAAAAAAAAAAAAAAAD///////////////8wWwQg -/////wAAAAEAAAAAAAAAAAAAAAD///////////////wEIFrGNdiqOpPns+u9VXaY -hrxlHQawzFOw9jvOPD4n0mBLAxUAxJ02CIbnBJNqZnjhE50mt4GffpAEQQRrF9Hy -4SxCR/i85uVjpEDydwN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNX -azFezsu2QGg3v1H1AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVEC -AQEDQgAE2klp6aX6y5kAir3EWQt0QAeapTW+db/9fD65KAoDzVajtThxPVLEf1Cu -fcfTxMQAQPM3wkZhu0NjlWFetcMdcaNTMFEwHQYDVR0OBBYEFLsFk5rxieg+ZZia -ktPbxK1mOIAHMB8GA1UdIwQYMBaAFLsFk5rxieg+ZZiaktPbxK1mOIAHMA8GA1Ud -EwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIhAM3+1Ay5sNVNa/YzpKBHsENV -3NpBqy2wnQadm592KnwCAiA6N2ewReTYgBF92nrMeH4RAwM+AKjTP3eXdmkdcCVz -Rw== ------END CERTIFICATE----- From 4062eed0f3e78ae3e5e01436c3e3f01ef3374ea0 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Tue, 9 Nov 2021 12:26:25 -0800 Subject: [PATCH 7/7] fix tests from merge --- tests/JWTTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 47413998..cdb63e44 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -380,15 +380,15 @@ public function provideEncodeDecode() public function testEncodeDecodeWithResource() { - $pem = file_get_contents(__DIR__ . '/rsa1-public.pub'); + $pem = file_get_contents(__DIR__ . '/data/rsa1-public.pub'); $resource = openssl_pkey_get_public($pem); - $privateKey = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $privateKey = file_get_contents(__DIR__ . '/data/rsa1-private.pem'); $payload = array('foo' => 'bar'); $encoded = JWT::encode($payload, $privateKey, 'RS512'); // Verify decoding succeeds - $decoded = JWT::decode($encoded, $resource, array('RS512')); + $decoded = JWT::decode($encoded, new Key($resource, 'RS512')); $this->assertEquals('bar', $decoded->foo); }