From 2e5281c27e1a688173a7192a0e8f0a2c4e41397f Mon Sep 17 00:00:00 2001 From: Nick Williams Date: Mon, 4 Nov 2019 22:39:45 -0600 Subject: [PATCH 01/25] Adds ES256 to supported algorithms (#239) --- src/JWT.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 22a67e32..388d671f 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -38,9 +38,10 @@ class JWT public static $timestamp = null; public static $supported_algs = array( + 'ES256' => array('openssl', 'SHA256'), 'HS256' => array('hash_hmac', 'SHA256'), - 'HS512' => array('hash_hmac', 'SHA512'), 'HS384' => array('hash_hmac', 'SHA384'), + 'HS512' => array('hash_hmac', 'SHA512'), 'RS256' => array('openssl', 'SHA256'), 'RS384' => array('openssl', 'SHA384'), 'RS512' => array('openssl', 'SHA512'), @@ -53,7 +54,7 @@ class JWT * @param string|array $key The key, or map of keys. * If the algorithm used is asymmetric, this is the public key * @param array $allowed_algs List of supported verification algorithms - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * * @return object The JWT's payload as a PHP object * @@ -144,7 +145,7 @@ public static function decode($jwt, $key, array $allowed_algs = 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 'HS256', 'HS384', 'HS512' and 'RS256' + * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * @param mixed $keyId * @param array $head An array with header elements to attach * @@ -179,7 +180,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he * @param string $msg The message to sign * @param string|resource $key The secret key * @param string $alg The signing algorithm. - * Supported algorithms are 'HS256', 'HS384', 'HS512' and 'RS256' + * Supported algorithms are 'ES256', 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * From 5f68890c13b0bb3415861963143831eb6fd42e6e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 4 Nov 2019 23:08:04 -0800 Subject: [PATCH 02/25] Fixes tests, adds PHP 7.3 (#257) --- .travis.yml | 9 ++++++--- composer.json | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 26f0ff0f..59d474b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,22 @@ language: php php: - - 5.4 - - 5.5 - 5.6 - 7.0 - 7.1 - 7.2 + - 7.3 matrix: include: - php: 5.3 dist: precise + - php: 5.4 + dist: trusty + - php: 5.5 + dist: trusty sudo: false before_script: composer install -script: phpunit +script: vendor/bin/phpunit diff --git a/composer.json b/composer.json index b76ffd19..9f1a42cb 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,6 @@ } }, "require-dev": { - "phpunit/phpunit": " 4.8.35" + "phpunit/phpunit": "^4.8|^5" } } From 264e5c720603bc87bf578b3a5b8e1a06d1c0787b Mon Sep 17 00:00:00 2001 From: Martin Krisell Date: Tue, 12 Nov 2019 20:16:19 +0100 Subject: [PATCH 03/25] Whitespace style fix (#255) --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index 388d671f..a9e4daf6 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -160,7 +160,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he if ($keyId !== null) { $header['kid'] = $keyId; } - if ( isset($head) && is_array($head) ) { + if (isset($head) && is_array($head)) { $header = array_merge($head, $header); } $segments = array(); From 8228431e09bc10d1ffaa2f6f02e51c9539179f92 Mon Sep 17 00:00:00 2001 From: David Mann Date: Tue, 12 Nov 2019 14:16:54 -0500 Subject: [PATCH 04/25] Comment typo fix (#253) --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index a9e4daf6..2fb98625 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -113,7 +113,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) throw new SignatureInvalidException('Signature verification failed'); } - // Check if the nbf if it is defined. This is the time that the + // Check the nbf if it is defined. This is the time that the // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { throw new BeforeValidException( From bc324de7381586cfbbfbe1214b2de10108fbc210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Glatzl?= Date: Tue, 12 Nov 2019 20:18:35 +0100 Subject: [PATCH 05/25] Clearer variable name in README (#229) --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b1a7a3a2..9c8b5455 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Example use \Firebase\JWT\JWT; $key = "example_key"; -$token = array( +$payload = array( "iss" => "http://example.org", "aud" => "http://example.com", "iat" => 1356999524, @@ -36,7 +36,7 @@ $token = array( * https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40 * for a list of spec-compliant algorithms. */ -$jwt = JWT::encode($token, $key); +$jwt = JWT::encode($payload, $key); $decoded = JWT::decode($jwt, $key, array('HS256')); print_r($decoded); @@ -93,14 +93,14 @@ ehde/zUxo6UvS7UrBQIDAQAB -----END PUBLIC KEY----- EOD; -$token = array( +$payload = array( "iss" => "example.org", "aud" => "example.com", "iat" => 1356999524, "nbf" => 1357000000 ); -$jwt = JWT::encode($token, $privateKey, 'RS256'); +$jwt = JWT::encode($payload, $privateKey, 'RS256'); echo "Encode:\n" . print_r($jwt, true) . "\n"; $decoded = JWT::decode($jwt, $publicKey, array('RS256')); From 20f37291b90ddcad3e9a56b6bd1798bd80336055 Mon Sep 17 00:00:00 2001 From: Martin Krisell Date: Tue, 17 Dec 2019 21:19:28 +0100 Subject: [PATCH 06/25] Use json_last_error without unnecessary existence check (#263) --- src/JWT.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 2fb98625..c3c3f667 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -283,7 +283,7 @@ public static function jsonDecode($input) $obj = json_decode($json_without_bigints); } - if (function_exists('json_last_error') && $errno = json_last_error()) { + if ($errno = json_last_error()) { static::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); @@ -303,7 +303,7 @@ public static function jsonDecode($input) public static function jsonEncode($input) { $json = json_encode($input); - if (function_exists('json_last_error') && $errno = json_last_error()) { + if ($errno = json_last_error()) { static::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); From 23192ac99d9b743c197d9bfc6e31e11878039c26 Mon Sep 17 00:00:00 2001 From: sergiy-petrov Date: Wed, 12 Feb 2020 00:35:53 +0200 Subject: [PATCH 07/25] Add php 7.4 to travis (#266) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 59d474b7..fc651ad3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ php: - 7.1 - 7.2 - 7.3 + - 7.4 matrix: include: From ccc74fb918551911caff5e19f3d3c1308a1f7536 Mon Sep 17 00:00:00 2001 From: Sergei Kolesnikov Date: Wed, 19 Feb 2020 21:30:12 +0300 Subject: [PATCH 08/25] docs: add resource type as 2nd param in decode method (#205) --- src/JWT.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index c3c3f667..18756f70 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -50,11 +50,11 @@ class JWT /** * Decodes a JWT string into a PHP object. * - * @param string $jwt The JWT - * @param string|array $key The key, or map of keys. - * 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' + * @param string $jwt The JWT + * @param string|array|resource $key The key, or map of keys. + * 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' * * @return object The JWT's payload as a PHP object * From ecb25af3f5053819431b5b116008d11dce13d60c Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 19 Feb 2020 11:32:56 -0700 Subject: [PATCH 09/25] tests: update PHPUnit test classes (#193) --- tests/JWTTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 804a3769..c164a8f0 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -2,9 +2,9 @@ namespace Firebase\JWT; use ArrayObject; -use PHPUnit_Framework_TestCase; +use PHPUnit\Framework\TestCase; -class JWTTest extends PHPUnit_Framework_TestCase +class JWTTest extends TestCase { public static $opensslVerifyReturnValue; From 78ec50cd5c7d0bbcaed6ece07ace040d8843b9cf Mon Sep 17 00:00:00 2001 From: Ilia Urvachev Date: Wed, 19 Feb 2020 21:34:19 +0300 Subject: [PATCH 10/25] docs: fix missed param name (#207) --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index 18756f70..11f6f108 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -366,7 +366,7 @@ private static function handleJsonError($errno) /** * Get the number of bytes in cryptographic strings. * - * @param string + * @param string $str * * @return int */ From 4566062c68f76f43d44f1643f4970fe89757d4c6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 24 Feb 2020 16:15:03 -0700 Subject: [PATCH 11/25] feat: add support for ES256 algorithm (#256) --- src/JWT.php | 138 +++++++++++++++++++++++++++++++++++++++- tests/JWTTest.php | 16 +++++ tests/ecdsa-private.pem | 18 ++++++ tests/ecdsa-public.pem | 9 +++ 4 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 tests/ecdsa-private.pem create mode 100644 tests/ecdsa-public.pem diff --git a/src/JWT.php b/src/JWT.php index 11f6f108..af206618 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -1,6 +1,7 @@ alg, $allowed_algs)) { throw new UnexpectedValueException('Algorithm not allowed'); } + if ($header->alg === 'ES256') { + // OpenSSL expects an ASN.1 DER sequence for ES256 signatures + $sig = self::signatureToDER($sig); + } + if (is_array($key) || $key instanceof \ArrayAccess) { if (isset($header->kid)) { if (!isset($key[$header->kid])) { @@ -192,7 +201,7 @@ public static function sign($msg, $key, $alg = 'HS256') throw new DomainException('Algorithm not supported'); } list($function, $algorithm) = static::$supported_algs[$alg]; - switch($function) { + switch ($function) { case 'hash_hmac': return hash_hmac($algorithm, $msg, $key, true); case 'openssl': @@ -201,6 +210,9 @@ public static function sign($msg, $key, $alg = 'HS256') if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } else { + if ($alg === 'ES256') { + $signature = self::signatureFromDER($signature, 256); + } return $signature; } } @@ -226,7 +238,7 @@ private static function verify($msg, $signature, $key, $alg) } list($function, $algorithm) = static::$supported_algs[$alg]; - switch($function) { + switch ($function) { case 'openssl': $success = openssl_verify($msg, $signature, $key, $algorithm); if ($success === 1) { @@ -377,4 +389,126 @@ private static function safeStrlen($str) } return strlen($str); } + + /** + * Convert an ECDSA signature to an ASN.1 DER sequence + * + * @param string $sig The ECDSA signature to convert + * @return string The encoded DER object + */ + private static function signatureToDER($sig) + { + // Separate the signature into r-value and s-value + list($r, $s) = str_split($sig, (int) (strlen($sig) / 2)); + + // Trim leading zeros + $r = ltrim($r, "\x00"); + $s = ltrim($s, "\x00"); + + // Convert r-value and s-value from unsigned big-endian integers to + // signed two's complement + if (ord($r[0]) > 0x7f) { + $r = "\x00" . $r; + } + if (ord($s[0]) > 0x7f) { + $s = "\x00" . $s; + } + + return self::encodeDER( + self::ASN1_SEQUENCE, + self::encodeDER(self::ASN1_INTEGER, $r) . + self::encodeDER(self::ASN1_INTEGER, $s) + ); + } + + /** + * Encodes a value into a DER object. + * + * @param int $type DER tag + * @param string $value the value to encode + * @return string the encoded object + */ + private static function encodeDER($type, $value) + { + $tag_header = 0; + if ($type === self::ASN1_SEQUENCE) { + $tag_header |= 0x20; + } + + // Type + $der = chr($tag_header | $type); + + // Length + $der .= chr(strlen($value)); + + return $der . $value; + } + + /** + * Encodes signature from a DER object. + * + * @param string $der binary signature in DER format + * @param int $keySize the nubmer of bits in the key + * @return string the signature + */ + private static function signatureFromDER($der, $keySize) + { + // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE + list($offset, $_) = self::readDER($der); + list($offset, $r) = self::readDER($der, $offset); + list($offset, $s) = self::readDER($der, $offset); + + // Convert r-value and s-value from signed two's compliment to unsigned + // big-endian integers + $r = ltrim($r, "\x00"); + $s = ltrim($s, "\x00"); + + // Pad out r and s so that they are $keySize bits long + $r = str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); + $s = str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); + + return $r . $s; + } + + /** + * Reads binary DER-encoded data and decodes into a single object + * + * @param string $der the binary data in DER format + * @param int $offset the offset of the data stream containing the object + * to decode + * @return array [$offset, $data] the new offset and the decoded object + */ + private static function readDER($der, $offset = 0) + { + $pos = $offset; + $size = strlen($der); + $constructed = (ord($der[$pos]) >> 5) & 0x01; + $type = ord($der[$pos++]) & 0x1f; + + // Length + $len = ord($der[$pos++]); + if ($len & 0x80) { + $n = $len & 0x1f; + $len = 0; + while ($n-- && $pos < $size) { + $len = ($len << 8) | ord($der[$pos++]); + } + } + + // Value + if ($type == self::ASN1_BIT_STRING) { + $pos++; // Skip the first contents octet (padding indicator) + $data = substr($der, $pos, $len - 1); + if (!$ignore_bit_strings) { + $pos += $len - 1; + } + } elseif (!$constructed) { + $data = substr($der, $pos, $len); + $pos += $len; + } else { + $data = null; + } + + return array($pos, $data); + } } diff --git a/tests/JWTTest.php b/tests/JWTTest.php index c164a8f0..0e1c20d1 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -282,6 +282,22 @@ public function testVerifyError() self::$opensslVerifyReturnValue = -1; JWT::decode($msg, $pkey, array('RS256')); } + + /** + * @runInSeparateProcess + */ + public function testEncodeAndDecodeEcdsaToken() + { + $privateKey = file_get_contents(__DIR__ . '/ecdsa-private.pem'); + $payload = array('foo' => 'bar'); + $encoded = JWT::encode($payload, $privateKey, 'ES256'); + + // Verify decoding succeeds + $publicKey = file_get_contents(__DIR__ . '/ecdsa-public.pem'); + $decoded = JWT::decode($encoded, $publicKey, array('ES256')); + + $this->assertEquals('bar', $decoded->foo); + } } /* diff --git a/tests/ecdsa-private.pem b/tests/ecdsa-private.pem new file mode 100644 index 00000000..5c77adaf --- /dev/null +++ b/tests/ecdsa-private.pem @@ -0,0 +1,18 @@ +-----BEGIN EC PARAMETERS----- +MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP////////// +/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6 +k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+ +kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK +fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz +ucrC/GMlUQIBAQ== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIIBaAIBAQQgyP9e7yS1tjpXa0l6o+80dbSxuMcqx3lUg0n2OT9AmiuggfowgfcC +AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA//////////////// +MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr +vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE +axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W +K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8 +YyVRAgEBoUQDQgAE2klp6aX6y5kAir3EWQt0QAeapTW+db/9fD65KAoDzVajtThx +PVLEf1CufcfTxMQAQPM3wkZhu0NjlWFetcMdcQ== +-----END EC PRIVATE KEY----- diff --git a/tests/ecdsa-public.pem b/tests/ecdsa-public.pem new file mode 100644 index 00000000..31fa053d --- /dev/null +++ b/tests/ecdsa-public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA +AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA//// +///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd +NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5 +RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA +//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABNpJaeml+suZAIq9xFkLdEAH +mqU1vnW//Xw+uSgKA81Wo7U4cT1SxH9Qrn3H08TEAEDzN8JGYbtDY5VhXrXDHXE= +-----END PUBLIC KEY----- From 9eb9f98c2fc55625fb57c4ce00e7c8c6d4bc0c58 Mon Sep 17 00:00:00 2001 From: NiMeDia Date: Sat, 29 Feb 2020 01:59:58 +0100 Subject: [PATCH 12/25] fix: added keywords to composer.json (#243) (#271) --- composer.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/composer.json b/composer.json index 9f1a42cb..e72ecd07 100644 --- a/composer.json +++ b/composer.json @@ -2,6 +2,10 @@ "name": "firebase/php-jwt", "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "php", + "jwt" + ], "authors": [ { "name": "Neuman Vong", From 5cd7ae01ae8bfad5b1c25ac8b187265ae1ccf7e6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 5 Mar 2020 13:29:27 -0700 Subject: [PATCH 13/25] remove ignore_bit_strings (#276) --- src/JWT.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index af206618..f8fe7e65 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -499,9 +499,7 @@ private static function readDER($der, $offset = 0) if ($type == self::ASN1_BIT_STRING) { $pos++; // Skip the first contents octet (padding indicator) $data = substr($der, $pos, $len - 1); - if (!$ignore_bit_strings) { - $pos += $len - 1; - } + $pos += $len - 1; } elseif (!$constructed) { $data = substr($der, $pos, $len); $pos += $len; From 51034b43ea55b95d289f0722ba1d84e8bb09ba8e Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Mon, 16 Mar 2020 21:08:03 +0545 Subject: [PATCH 14/25] test: allow for later versions of phpunit (#277) --- composer.json | 2 +- tests/JWTTest.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e72ecd07..25d1cfa9 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^4.8|^5" + "phpunit/phpunit": ">=4.8 <=9" } } diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 0e1c20d1..19520165 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -8,6 +8,17 @@ class JWTTest extends TestCase { public static $opensslVerifyReturnValue; + /* + * For compatibility with PHPUnit 4.8 and PHP < 5.6 + */ + public function setExpectedException($exceptionName, $message = '', $code = NULL) { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } + public function testEncodeDecode() { $msg = JWT::encode('abc', 'my_key'); From b0def5fca80851717920a3816b5c670e6182bc2f Mon Sep 17 00:00:00 2001 From: Eric Tendian Date: Sat, 21 Mar 2020 14:34:46 -0500 Subject: [PATCH 15/25] feat: adds JWK support (#273) --- src/JWK.php | 171 ++++++++++++++++++++++++++++++++++++++++++++++ tests/JWKTest.php | 159 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/JWK.php create mode 100644 tests/JWKTest.php diff --git a/src/JWK.php b/src/JWK.php new file mode 100644 index 00000000..f2777df8 --- /dev/null +++ b/src/JWK.php @@ -0,0 +1,171 @@ + + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + */ +class JWK +{ + /** + * Parse a set of JWK keys + * + * @param array $jwks The JSON Web Key Set as an associative array + * + * @return array An associative array that represents the set of keys + * + * @throws InvalidArgumentException Provided JWK Set is empty + * @throws UnexpectedValueException Provided JWK Set was invalid + * @throws DomainException OpenSSL failure + * + * @uses parseKey + */ + public static function parseKeySet(array $jwks) + { + $keys = array(); + + 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'); + } + + foreach ($jwks['keys'] as $k => $v) { + $kid = isset($v['kid']) ? $v['kid'] : $k; + if ($key = self::parseKey($v)) { + $keys[$kid] = $key; + } + } + + if (0 === count($keys)) { + throw new UnexpectedValueException('No supported algorithms found in JWK Set'); + } + + return $keys; + } + + /** + * Parse a JWK key + * + * @param array $jwk An individual JWK + * + * @return resource|array An associative array that represents the key + * + * @throws InvalidArgumentException Provided JWK is empty + * @throws UnexpectedValueException Provided JWK was invalid + * @throws DomainException OpenSSL failure + * + * @uses createPemFromModulusAndExponent + */ + private static function parseKey(array $jwk) + { + if (empty($jwk)) { + throw new InvalidArgumentException('JWK must not be empty'); + } + if (!isset($jwk['kty'])) { + throw new UnexpectedValueException('JWK must contain a "kty" parameter'); + } + + switch ($jwk['kty']) { + case 'RSA': + if (array_key_exists('d', $jwk)) { + throw new UnexpectedValueException('RSA private keys are not supported'); + } + if (!isset($jwk['n']) || !isset($jwk['e'])) { + throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); + } + + $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); + $publicKey = openssl_pkey_get_public($pem); + if (false === $publicKey) { + throw new DomainException( + 'OpenSSL error: ' . openssl_error_string() + ); + } + return $publicKey; + default: + // Currently only RSA is supported + break; + } + } + + /** + * Create a public key represented in PEM format from RSA modulus and exponent information + * + * @param string $n The RSA modulus encoded in Base64 + * @param string $e The RSA exponent encoded in Base64 + * + * @return string The RSA public key represented in PEM format + * + * @uses encodeLength + */ + private static function createPemFromModulusAndExponent($n, $e) + { + $modulus = JWT::urlsafeB64Decode($n); + $publicExponent = JWT::urlsafeB64Decode($e); + + $components = array( + 'modulus' => pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus), + 'publicExponent' => pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent) + ); + + $rsaPublicKey = pack( + 'Ca*a*a*', + 48, + self::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), + $components['modulus'], + $components['publicExponent'] + ); + + // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. + $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $rsaPublicKey = chr(0) . $rsaPublicKey; + $rsaPublicKey = chr(3) . self::encodeLength(strlen($rsaPublicKey)) . $rsaPublicKey; + + $rsaPublicKey = pack( + 'Ca*a*', + 48, + self::encodeLength(strlen($rsaOID . $rsaPublicKey)), + $rsaOID . $rsaPublicKey + ); + + $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . + chunk_split(base64_encode($rsaPublicKey), 64) . + '-----END PUBLIC KEY-----'; + + return $rsaPublicKey; + } + + /** + * DER-encode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + * + * @param int $length + * @return string + */ + private static function encodeLength($length) + { + if ($length <= 0x7F) { + return chr($length); + } + + $temp = ltrim(pack('N', $length), chr(0)); + + return pack('Ca*', 0x80 | strlen($temp), $temp); + } +} diff --git a/tests/JWKTest.php b/tests/JWKTest.php new file mode 100644 index 00000000..4f8fdf65 --- /dev/null +++ b/tests/JWKTest.php @@ -0,0 +1,159 @@ +expectException($exceptionName); + } else { + parent::setExpectedException($exceptionName, $message, $code); + } + } + + public function testDecodeByJWKKeySetTokenExpired() + { + $jsKey = array( + 'kty' => 'RSA', + 'e' => 'AQAB', + 'use' => 'sig', + 'kid' => 's1', + 'n' => 'kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k', + ); + + $key = JWK::parseKeySet(array('keys' => array($jsKey))); + + $header = array( + 'kid' => 's1', + 'alg' => 'RS256', + ); + $payload = array ( + 'scp' => array ('openid', 'email', 'profile', 'aas'), + 'sub' => 'tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0', + 'clm' => array ('!5v8H'), + 'iss' => 'http://130.211.243.114:8080/c2id', + 'exp' => 1441126539, + 'uip' => array('groups' => array('admin', 'audit')), + 'cid' => 'pk-oidc-01', + ); + $signature = 'PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; + $msg = sprintf('%s.%s.%s', + JWT::urlsafeB64Encode(json_encode($header)), + JWT::urlsafeB64Encode(json_encode($payload)), + $signature + ); + + $this->setExpectedException('Firebase\JWT\ExpiredException'); + + JWT::decode($msg, $key, array('RS256')); + } + + public function testDecodeByJWKKeySet() + { + $jsKey = array( + 'kty' => 'RSA', + 'e' => 'AQAB', + 'use' => 'sig', + 'kid' => 's1', + 'n' => 'kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k', + ); + + $key = JWK::parseKeySet(array('keys' => array($jsKey))); + + $header = array( + 'kid' => 's1', + 'alg' => 'RS256', + ); + $payload = array ( + 'scp' => array ('openid', 'email', 'profile', 'aas'), + 'sub' => 'tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0', + 'clm' => array ('!5v8H'), + 'iss' => 'http://130.211.243.114:8080/c2id', + 'exp' => 1441126539, + 'uip' => array('groups' => array('admin', 'audit')), + 'cid' => 'pk-oidc-01', + ); + $signature = 'PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; + $msg = sprintf('%s.%s.%s', + JWT::urlsafeB64Encode(json_encode($header)), + JWT::urlsafeB64Encode(json_encode($payload)), + $signature + ); + + $this->setExpectedException('Firebase\JWT\ExpiredException'); + + $payload = JWT::decode($msg, $key, array('RS256')); + + $this->assertEquals("tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0", $payload->sub); + $this->assertEquals(1441126539, $payload->exp); + } + + public function testDecodeByMultiJWKKeySet() + { + $jsKey1 = array( + 'kty' => 'RSA', + 'e' => 'AQAB', + 'use' => 'sig', + 'kid' => 'CXup', + 'n' => 'hrwD-lc-IwzwidCANmy4qsiZk11yp9kHykOuP0yOnwi36VomYTQVEzZXgh2sDJpGgAutdQudgwLoV8tVSsTG9SQHgJjH9Pd_9V4Ab6PANyZNG6DSeiq1QfiFlEP6Obt0JbRB3W7X2vkxOVaNoWrYskZodxU2V0ogeVL_LkcCGAyNu2jdx3j0DjJatNVk7ystNxb9RfHhJGgpiIkO5S3QiSIVhbBKaJHcZHPF1vq9g0JMGuUCI-OTSVg6XBkTLEGw1C_R73WD_oVEBfdXbXnLukoLHBS11p3OxU7f4rfxA_f_72_UwmWGJnsqS3iahbms3FkvqoL9x_Vj3GhuJSf97Q', + ); + $jsKey2 = array( + 'kty' => 'EC', + 'use' => 'sig', + 'crv' => 'P-256', + 'kid' => 'yGvt', + 'x' => 'pvgdqM3RCshljmuCF1D2Ez1w5ei5k7-bpimWLPNeEHI', + 'y' => 'JSmUhbUTqiFclVLEdw6dz038F7Whw4URobjXbAReDuM', + ); + $jsKey3 = array( + 'kty' => 'EC', + 'use' => 'sig', + 'crv' => 'P-384', + 'kid' => '9nHY', + 'x' => 'JPKhjhE0Bj579Mgj3Cn3ERGA8fKVYoGOaV9BPKhtnEobphf8w4GSeigMesL-038W', + 'y' => 'UbJa1QRX7fo9LxSlh7FOH5ABT5lEtiQeQUcX9BW0bpJFlEVGqwec80tYLdOIl59M', + ); + $jsKey4 = array( + 'kty' => 'EC', + 'use' => 'sig', + 'crv' => 'P-521', + 'kid' => 'tVzS', + 'x' => 'AZgkRHlIyNQJlPIwTWdHqouw41k9dS3GJO04BDEnJnd_Dd1owlCn9SMXA-JuXINn4slwbG4wcECbctXb2cvdGtmn', + 'y' => 'AdBC6N9lpupzfzcIY3JLIuc8y8MnzV-ItmzHQcC5lYWMTbuM9NU_FlvINeVo8g6i4YZms2xFB-B0VVdaoF9kUswC', + ); + + $key = JWK::parseKeySet(array('keys' => array($jsKey1, $jsKey2, $jsKey3, $jsKey4))); + + $header = array( + 'kid' => 'CXup', + 'alg' => 'RS256', + ); + $payload = array( + 'sub' => 'f8b67cc46030777efd8bce6c1bfe29c6c0f818ec', + 'scp' => array('openid', 'name', 'profile', 'picture', 'email', 'rs-pk-main', 'rs-pk-so', 'rs-pk-issue', 'rs-pk-web'), + 'clm' => array('!5v8H'), + 'iss' => 'https://id.projectkit.net/authenticate', + 'exp' => 1492228336, + 'iat' => 1491364336, + 'cid' => 'cid-pk-web', + ); + $signature = 'KW1K-72bMtiNwvyYBgffG6VaG6I59cELGYQR8M2q7HA8dmzliu6QREJrqyPtwW_rDJZbsD3eylvkRinK9tlsMXCOfEJbxLdAC9b4LKOsnsbuXXwsJHWkFG0a7osdW0ZpXJDoMFlO1aosxRGMkaqhf1wIkvQ5PM_EB08LJv7oz64Antn5bYaoajwgvJRl7ChatRDn9Sx5UIElKD1BK4Uw5WdrZwBlWdWZVNCSFhy4F6SdZvi3OBlXzluDwq61RC-pl2iivilJNljYWVrthHDS1xdtaVz4oteHW13-IS7NNEz6PVnzo5nyoPWMAB4JlRnxcfOFTTUqOA2mX5Csg0UpdQ'; + $msg = sprintf('%s.%s.%s', + JWT::urlsafeB64Encode(json_encode($header)), + JWT::urlsafeB64Encode(json_encode($payload)), + $signature + ); + + $this->setExpectedException('Firebase\JWT\ExpiredException'); + + $payload = JWT::decode($msg, $key, array('RS256')); + + $this->assertEquals("f8b67cc46030777efd8bce6c1bfe29c6c0f818ec", $payload->sub); + $this->assertEquals(1492228336, $payload->exp); + } +} From 27ee05f8a52227d42d09f357d953fd04f6b6deeb Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sat, 21 Mar 2020 12:59:57 -0700 Subject: [PATCH 16/25] test: use keys for testing JWK (#283) --- tests/JWKTest.php | 197 +++++++++++++++-------------------------- tests/rsa-jwkset.json | 17 ++++ tests/rsa1-private.pem | 27 ++++++ tests/rsa2-private.pem | 27 ++++++ 4 files changed, 141 insertions(+), 127 deletions(-) create mode 100644 tests/rsa-jwkset.json create mode 100644 tests/rsa1-private.pem create mode 100644 tests/rsa2-private.pem diff --git a/tests/JWKTest.php b/tests/JWKTest.php index 4f8fdf65..d02534f1 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -5,155 +5,98 @@ class JWKTest extends TestCase { - /* - * For compatibility with PHPUnit 4.8 and PHP < 5.6 - */ - public function setExpectedException($exceptionName, $message = '', $code = NULL) { - if (method_exists($this, 'expectException')) { - $this->expectException($exceptionName); - } else { - parent::setExpectedException($exceptionName, $message, $code); - } - } + private static $keys; + private static $privKey1; + private static $privKey2; - public function testDecodeByJWKKeySetTokenExpired() + public function testMissingKty() { - $jsKey = array( - 'kty' => 'RSA', - 'e' => 'AQAB', - 'use' => 'sig', - 'kid' => 's1', - 'n' => 'kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k', + $this->setExpectedException( + 'UnexpectedValueException', + 'JWK must contain a "kty" parameter' ); - $key = JWK::parseKeySet(array('keys' => array($jsKey))); + $badJwk = array('kid' => 'foo'); + $keys = JWK::parseKeySet(array('keys' => array($badJwk))); + } - $header = array( - 'kid' => 's1', - 'alg' => 'RS256', - ); - $payload = array ( - 'scp' => array ('openid', 'email', 'profile', 'aas'), - 'sub' => 'tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0', - 'clm' => array ('!5v8H'), - 'iss' => 'http://130.211.243.114:8080/c2id', - 'exp' => 1441126539, - 'uip' => array('groups' => array('admin', 'audit')), - 'cid' => 'pk-oidc-01', - ); - $signature = 'PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; - $msg = sprintf('%s.%s.%s', - JWT::urlsafeB64Encode(json_encode($header)), - JWT::urlsafeB64Encode(json_encode($payload)), - $signature + public function testInvalidAlgorithm() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'No supported algorithms found in JWK Set' ); - $this->setExpectedException('Firebase\JWT\ExpiredException'); - - JWT::decode($msg, $key, array('RS256')); + $badJwk = array('kty' => 'BADALG'); + $keys = JWK::parseKeySet(array('keys' => array($badJwk))); } - public function testDecodeByJWKKeySet() + public function testParseJwkKeySet() { - $jsKey = array( - 'kty' => 'RSA', - 'e' => 'AQAB', - 'use' => 'sig', - 'kid' => 's1', - 'n' => 'kWp2zRA23Z3vTL4uoe8kTFptxBVFunIoP4t_8TDYJrOb7D1iZNDXVeEsYKp6ppmrTZDAgd-cNOTKLd4M39WJc5FN0maTAVKJc7NxklDeKc4dMe1BGvTZNG4MpWBo-taKULlYUu0ltYJuLzOjIrTHfarucrGoRWqM0sl3z2-fv9k', + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/rsa-jwkset.json'), + true ); + $keys = JWK::parseKeySet($jwkSet); + $this->assertTrue(is_array($keys)); + $this->assertArrayHasKey('jwk1', $keys); + self::$keys = $keys; + } - $key = JWK::parseKeySet(array('keys' => array($jsKey))); - - $header = array( - 'kid' => 's1', - 'alg' => 'RS256', - ); - $payload = array ( - 'scp' => array ('openid', 'email', 'profile', 'aas'), - 'sub' => 'tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0', - 'clm' => array ('!5v8H'), - 'iss' => 'http://130.211.243.114:8080/c2id', - 'exp' => 1441126539, - 'uip' => array('groups' => array('admin', 'audit')), - 'cid' => 'pk-oidc-01', - ); - $signature = 'PvYrnf3k1Z0wgRwCgq0WXKaoIv1hHtzBFO5cGfCs6bl4suc6ilwCWmJqRxGYkU2fNTGyMOt3OUnnBEwl6v5qN6jv7zbkVAVKVvbQLxhHC2nXe3izvoCiVaMEH6hE7VTWwnPbX_qO72mCwTizHTJTZGLOsyXLYM6ctdOMf7sFPTI'; - $msg = sprintf('%s.%s.%s', - JWT::urlsafeB64Encode(json_encode($header)), - JWT::urlsafeB64Encode(json_encode($payload)), - $signature - ); + /** + * @depends testParseJwkKeySet + */ + public function testDecodeByJwkKeySetTokenExpired() + { + $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $payload = array('exp' => strtotime('-1 hour')); + $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); $this->setExpectedException('Firebase\JWT\ExpiredException'); - $payload = JWT::decode($msg, $key, array('RS256')); - - $this->assertEquals("tUCYtnfIBPWcrSJf4yBfvN1kww4KGcy3LIPk1GVzsE0", $payload->sub); - $this->assertEquals(1441126539, $payload->exp); + JWT::decode($msg, self::$keys, array('RS256')); } - public function testDecodeByMultiJWKKeySet() + /** + * @depends testParseJwkKeySet + */ + public function testDecodeByJwkKeySet() { - $jsKey1 = array( - 'kty' => 'RSA', - 'e' => 'AQAB', - 'use' => 'sig', - 'kid' => 'CXup', - 'n' => 'hrwD-lc-IwzwidCANmy4qsiZk11yp9kHykOuP0yOnwi36VomYTQVEzZXgh2sDJpGgAutdQudgwLoV8tVSsTG9SQHgJjH9Pd_9V4Ab6PANyZNG6DSeiq1QfiFlEP6Obt0JbRB3W7X2vkxOVaNoWrYskZodxU2V0ogeVL_LkcCGAyNu2jdx3j0DjJatNVk7ystNxb9RfHhJGgpiIkO5S3QiSIVhbBKaJHcZHPF1vq9g0JMGuUCI-OTSVg6XBkTLEGw1C_R73WD_oVEBfdXbXnLukoLHBS11p3OxU7f4rfxA_f_72_UwmWGJnsqS3iahbms3FkvqoL9x_Vj3GhuJSf97Q', - ); - $jsKey2 = array( - 'kty' => 'EC', - 'use' => 'sig', - 'crv' => 'P-256', - 'kid' => 'yGvt', - 'x' => 'pvgdqM3RCshljmuCF1D2Ez1w5ei5k7-bpimWLPNeEHI', - 'y' => 'JSmUhbUTqiFclVLEdw6dz038F7Whw4URobjXbAReDuM', - ); - $jsKey3 = array( - 'kty' => 'EC', - 'use' => 'sig', - 'crv' => 'P-384', - 'kid' => '9nHY', - 'x' => 'JPKhjhE0Bj579Mgj3Cn3ERGA8fKVYoGOaV9BPKhtnEobphf8w4GSeigMesL-038W', - 'y' => 'UbJa1QRX7fo9LxSlh7FOH5ABT5lEtiQeQUcX9BW0bpJFlEVGqwec80tYLdOIl59M', - ); - $jsKey4 = array( - 'kty' => 'EC', - 'use' => 'sig', - 'crv' => 'P-521', - 'kid' => 'tVzS', - 'x' => 'AZgkRHlIyNQJlPIwTWdHqouw41k9dS3GJO04BDEnJnd_Dd1owlCn9SMXA-JuXINn4slwbG4wcECbctXb2cvdGtmn', - 'y' => 'AdBC6N9lpupzfzcIY3JLIuc8y8MnzV-ItmzHQcC5lYWMTbuM9NU_FlvINeVo8g6i4YZms2xFB-B0VVdaoF9kUswC', - ); + $privKey1 = file_get_contents(__DIR__ . '/rsa1-private.pem'); + $payload = array('sub' => 'foo', 'exp' => strtotime('+10 seconds')); + $msg = JWT::encode($payload, $privKey1, 'RS256', 'jwk1'); - $key = JWK::parseKeySet(array('keys' => array($jsKey1, $jsKey2, $jsKey3, $jsKey4))); + $result = JWT::decode($msg, self::$keys, array('RS256')); - $header = array( - 'kid' => 'CXup', - 'alg' => 'RS256', - ); - $payload = array( - 'sub' => 'f8b67cc46030777efd8bce6c1bfe29c6c0f818ec', - 'scp' => array('openid', 'name', 'profile', 'picture', 'email', 'rs-pk-main', 'rs-pk-so', 'rs-pk-issue', 'rs-pk-web'), - 'clm' => array('!5v8H'), - 'iss' => 'https://id.projectkit.net/authenticate', - 'exp' => 1492228336, - 'iat' => 1491364336, - 'cid' => 'cid-pk-web', - ); - $signature = 'KW1K-72bMtiNwvyYBgffG6VaG6I59cELGYQR8M2q7HA8dmzliu6QREJrqyPtwW_rDJZbsD3eylvkRinK9tlsMXCOfEJbxLdAC9b4LKOsnsbuXXwsJHWkFG0a7osdW0ZpXJDoMFlO1aosxRGMkaqhf1wIkvQ5PM_EB08LJv7oz64Antn5bYaoajwgvJRl7ChatRDn9Sx5UIElKD1BK4Uw5WdrZwBlWdWZVNCSFhy4F6SdZvi3OBlXzluDwq61RC-pl2iivilJNljYWVrthHDS1xdtaVz4oteHW13-IS7NNEz6PVnzo5nyoPWMAB4JlRnxcfOFTTUqOA2mX5Csg0UpdQ'; - $msg = sprintf('%s.%s.%s', - JWT::urlsafeB64Encode(json_encode($header)), - JWT::urlsafeB64Encode(json_encode($payload)), - $signature - ); + $this->assertEquals("foo", $result->sub); + } - $this->setExpectedException('Firebase\JWT\ExpiredException'); + /** + * @depends testParseJwkKeySet + */ + public function testDecodeByMultiJwkKeySet() + { + $privKey2 = file_get_contents(__DIR__ . '/rsa2-private.pem'); + $payload = array('sub' => 'bar', 'exp' => strtotime('+10 seconds')); + $msg = JWT::encode($payload, $privKey2, 'RS256', 'jwk2'); - $payload = JWT::decode($msg, $key, array('RS256')); + $result = JWT::decode($msg, self::$keys, array('RS256')); - $this->assertEquals("f8b67cc46030777efd8bce6c1bfe29c6c0f818ec", $payload->sub); - $this->assertEquals(1492228336, $payload->exp); + $this->assertEquals("bar", $result->sub); + } + + /* + * For compatibility with PHPUnit 4.8 and PHP < 5.6 + */ + public function setExpectedException($exceptionName, $message = '', $code = NULL) + { + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + if ($message) { + $this->expectExceptionMessage($message); + } + } else { + parent::setExpectedException($exceptionName, $message, $code); + } } } diff --git a/tests/rsa-jwkset.json b/tests/rsa-jwkset.json new file mode 100644 index 00000000..0059f8cc --- /dev/null +++ b/tests/rsa-jwkset.json @@ -0,0 +1,17 @@ +{ + "keys": [ + { + "kty": "RSA", + "e": "AQAB", + "kid": "jwk1", + "n": "0Ttga33B1yX4w77NbpKyNYDNSVCo8j-RlZaZ9tI-KfkV1d-tfsvI9ZPAheP11FoN52ceBaY5ltelHW-IKwCfyT0orLdsxLgowaXki9woF1Azvcg2JVxQLv9aVjjAvy3CZFIG_EeN7J3nsyCXGnu1yMEbnvkWxA88__Q6HQ2K9wqfApkQ0LNlsK0YHz_sfjHNvRKxnbAJk7D5fUhZunPZXOPHXFgA5SvLvMaNIXduMKJh4OMfuoLdJowXJAR9j31Mqz_is4FMhm_9Mq7vZZ-uF09htRvIR8tRY28oJuW1gKWyg7cQQpnjHgFyG3XLXWAeXclWqyh_LfjyHQjrYhyeFw" + + }, + { + "kty": "RSA", + "e": "AQAB", + "kid": "jwk2", + "n": "pXi2o6AnNhwL30MaK_nuDHi2fxZHVen7Xwk0bjLGlHYpq3mSvXm2HBA-zR41vQCbHkYGsDpsyDhIXLBDTbSa7ue7D1ZqYdv5YLIS33zdX9GtUHfFHc6zYgXAU9ziWeyTzVn7icAbjxqcgT2xKNuGK7Zf2ZJ053rr-dxjAE-SjX4SG0WWUhwPjxlr1etF7mEurhHweuSdZYl36g39o9BtTBVfS87io2MwdIRsnL3w8ulgXRVRWjv-vvcuhMS_y6zGbzOC55Yr23sb4h2PSll32bgyglEIsGgHqjOdyjuUzl0t6jh86DHzbu9h-u1iihX8EI8t7CBbizbPPyHQygp-rQ" + } + ] +} \ No newline at end of file diff --git a/tests/rsa1-private.pem b/tests/rsa1-private.pem new file mode 100644 index 00000000..b194b5b4 --- /dev/null +++ b/tests/rsa1-private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA0Ttga33B1yX4w77NbpKyNYDNSVCo8j+RlZaZ9tI+KfkV1d+t +fsvI9ZPAheP11FoN52ceBaY5ltelHW+IKwCfyT0orLdsxLgowaXki9woF1Azvcg2 +JVxQLv9aVjjAvy3CZFIG/EeN7J3nsyCXGnu1yMEbnvkWxA88//Q6HQ2K9wqfApkQ +0LNlsK0YHz/sfjHNvRKxnbAJk7D5fUhZunPZXOPHXFgA5SvLvMaNIXduMKJh4OMf +uoLdJowXJAR9j31Mqz/is4FMhm/9Mq7vZZ+uF09htRvIR8tRY28oJuW1gKWyg7cQ +QpnjHgFyG3XLXWAeXclWqyh/LfjyHQjrYhyeFwIDAQABAoIBAHMqdJsWAGEVNIVB ++792HYNXnydQr32PwemNmLeD59WglgU/9jZJoxaROjI4VLKK0wZg+uRvJ1nA3tCB ++Hh7Anh5Im9XExaAq2ZTkqXtC2AxtBktH6iW1EfaI/Y7jNRuMoaXo+Ku3A62p7cw +JBvepiOXL0Xko0RNguz7mBUvxCLPhYhzn7qCbM8uXLcjsXq/YhWQwQmtMqv0sd3W +Hy+8Jb2c18sqDeZIBne4dWD6qPClPEOsrq9gPTkl0DjbT27oVc2u1p4HMNm5BJIh +u3rMSxnZHUd7Axj1FgyLIOHl63UhaiaA1aPe/fLiVIGOA1jBZrpbnjgqDy9Uxyn6 +eydbiwECgYEA9mtRydz22idyUOlBCDXk+vdGBvFAucNYaNNUAXUJ2wfPmdGgFCA7 +g5eQG8JC6J/FU+2AfIuz6LGr7SxMBYcsWGjFAzGqs/sJib+zzN1dPUSRn4uJNFit +51yQzPgBqHS6S/XBi6YAODeZDl9jiPl3FxxucqLY5NstqZFXbE0SjIECgYEA2V3r +7xnRAK1krY1+zkPof4kcBmjqOXjnl/oRxlXP65lEXmyNJwm/ulOIko9mElWRs8CG +AxSWKaab9Gk6lc8MHjVRbuW52RGLGKq1mp6ENr4d3IBOfrNsTvD3gtNEN1JFLeF1 +jIbSsrbi2txr7VZ06Irac0C/ytro0QDOUoXkvpcCgYA8O0EzmToRWsD7e/g0XJAK +s/Q+8CtE/LWYccc/z+7HxeH9lBqPsM07Pgmwb0xRdfQSrqPQTYl9ICiJAWHXnBG/ +zmQRgstZ0MulCuGU+qq2thLuL3oq/F4NhjeykhA9r8J1nK1hSAMXuqdDtxcqPOfa +E03/4UQotFY181uuEiytgQKBgHQT+gjHqptH/XnJFCymiySAXdz2bg6fCF5aht95 +t/1C7gXWxlJQnHiuX0KVHZcw5wwtBePjPIWlmaceAtE5rmj7ZC9qsqK/AZ78mtql +SEnLoTq9si1rN624dRUCKW25m4Py4MlYvm/9xovGJkSqZOhCLoJZ05JK8QWb/pKH +Oi6lAoGBAOUN6ICpMQvzMGPgIbgS0H/gvRTnpAEs59vdgrkhlCII4tzfgvBQlVae +hRcdM6GTMq5pekBPKu45eanIzwVc88P6coT4qiWYKk2jYoLBa0UV3xEAuqBMymrj +X4nLcSbZtO0tcDGMfMpWF2JGYOEJQNetPozL/ICGVFyIO8yzXm8U +-----END RSA PRIVATE KEY----- diff --git a/tests/rsa2-private.pem b/tests/rsa2-private.pem new file mode 100644 index 00000000..74380869 --- /dev/null +++ b/tests/rsa2-private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEApXi2o6AnNhwL30MaK/nuDHi2fxZHVen7Xwk0bjLGlHYpq3mS +vXm2HBA+zR41vQCbHkYGsDpsyDhIXLBDTbSa7ue7D1ZqYdv5YLIS33zdX9GtUHfF +Hc6zYgXAU9ziWeyTzVn7icAbjxqcgT2xKNuGK7Zf2ZJ053rr+dxjAE+SjX4SG0WW +UhwPjxlr1etF7mEurhHweuSdZYl36g39o9BtTBVfS87io2MwdIRsnL3w8ulgXRVR +Wjv+vvcuhMS/y6zGbzOC55Yr23sb4h2PSll32bgyglEIsGgHqjOdyjuUzl0t6jh8 +6DHzbu9h+u1iihX8EI8t7CBbizbPPyHQygp+rQIDAQABAoIBACF25kj1LLjutx/x +7CsUoqX3C8Fr+gVQCrxPmkDnF+4Sb570OU8EfGX0ix7kiy2sH7LhqpydVD6x00Cb +jSD785F5YAVcDqu31xlNKi/0irjEKO7rKfw7P2AFlb3gIA7bn5CaMBrNtUUdtqUU +mu2OZ/YTLhNMYUQnQe4IOiVn8lWW5D4Kje/RlLRRdGn8voXaD5BnOwZNXAxjdXqM +RxyXRG74tLKyfe3W8xTL8uhlKCNHjsdtUg9IZdnKT7I3DJPobpqgC3fUuC/IbfGf +MPK1aiu067/3DdgonC2ZWqFeKLJqtUa7z0pSQaZeDa1iiUuRivfqKYEBovFre6ni +1qHkp8ECgYEA089VnKc74NRGVbIs0VtQGprNhkl47eBq6jhTlG3hfaFF4VuDiZiu +wT8enlbhlbDb/gM0CDr9tkfDs7R4exNnhSVvn2PT8b1mhonOAeE466y/4YBA0d9x +gj0wF2vjH/bsVNBe6MBrIx12R2tBKTZ7tbCzgJRszSZqkrK7sljTlaUCgYEAx/54 +G3Yd3ULqGIG/JA7w/QEYitgjwAUSJ+eLU+iqlIjo/njAJwJ/kixqaI3Jzcl+kYmp +yNIXNNaJUz8c0M/QsuqvQjLnHkF0FOZUrdyVseU2mSbI6DhAGsPJEtAOep/61vyz +uJSu0z34gQ6bNrKdqfkA7XIQRNJ1r0qQXrVLRmkCgYB2/UYaIDTaREZTBCp7XnHs +0ERfiUz/TZCijgweGXCQ1BXe2TtXBEhAVcZMq4BFSLr9wyzq5sD7Muu1O9BnS+pe ++T3w6/L4Hi/HqwjpM253r2+ILjW78Wvh/5/RuJE6tsvjhb+bv+UwL+/vhUhw76Ol +2WOt+zP4N/ms+e3J7m7G5QKBgQCmasN65nC3WyT8u4pX8O7rOOw5LN2ivRV8ixnO ++r5m1v46MjSCwXtyIO9yjPmt+csOQ+U6LEgPOa4PzWanAyaAmvS3OzBCZui3M2qn +OfR+kWM7UaDAS35cRyqcMvC5bUIHf0P1hhNryBdvHL5fZ4X2mDMDYnTTL+WptXwo +sucucQKBgAGHzi5+ZRwffhpZiYVR/lA6zvqyekAncJZwGe2UVDL0axTumX1NPdin +2mOnVuvKVvJkisyKTIQzFk6ClQEyiArO4+t7zhUbg5Crh8q6nObRo2R2NcP8o0Iq +BRIwPgaG/WlEvZ6zqlHQ0qH7WoL4HnRG5uyLOuzRIkjasYmZdfR8 +-----END RSA PRIVATE KEY----- From 3811d6946955d5cb1f046fe0932a68ef6c413952 Mon Sep 17 00:00:00 2001 From: Phil Davis Date: Tue, 24 Mar 2020 02:02:54 +0545 Subject: [PATCH 17/25] chore: code cleanup (#278) --- src/JWT.php | 2 +- tests/JWTTest.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index f8fe7e65..6f178b4e 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -448,7 +448,7 @@ private static function encodeDER($type, $value) * Encodes signature from a DER object. * * @param string $der binary signature in DER format - * @param int $keySize the nubmer of bits in the key + * @param int $keySize the number of bits in the key * @return string the signature */ private static function signatureFromDER($der, $keySize) diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 19520165..867d87e5 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -159,7 +159,7 @@ public function testInvalidTokenWithNbfLeeway() "nbf" => time() + 65); // not before too far in future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, 'my_key', array('HS256')); JWT::$leeway = 0; } @@ -183,7 +183,7 @@ public function testInvalidTokenWithIatLeeway() "iat" => time() + 65); // issued too far in future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('Firebase\JWT\BeforeValidException'); - $decoded = JWT::decode($encoded, 'my_key', array('HS256')); + JWT::decode($encoded, 'my_key', array('HS256')); JWT::$leeway = 0; } @@ -194,7 +194,7 @@ public function testInvalidToken() "exp" => time() + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('Firebase\JWT\SignatureInvalidException'); - $decoded = JWT::decode($encoded, 'my_key2', array('HS256')); + JWT::decode($encoded, 'my_key2', array('HS256')); } public function testNullKeyFails() @@ -204,7 +204,7 @@ public function testNullKeyFails() "exp" => time() + JWT::$leeway + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('InvalidArgumentException'); - $decoded = JWT::decode($encoded, null, array('HS256')); + JWT::decode($encoded, null, array('HS256')); } public function testEmptyKeyFails() @@ -214,7 +214,7 @@ public function testEmptyKeyFails() "exp" => time() + JWT::$leeway + 20); // time in the future $encoded = JWT::encode($payload, 'my_key'); $this->setExpectedException('InvalidArgumentException'); - $decoded = JWT::decode($encoded, '', array('HS256')); + JWT::decode($encoded, '', array('HS256')); } public function testRSEncodeDecode() From feb0e820b8436873675fd3aca04f3728eb2185cb Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Wed, 25 Mar 2020 11:49:23 -0700 Subject: [PATCH 18/25] chore: fixes cs and adds cs checking to travis (#284) --- .gitattributes | 1 - .travis.yml | 15 ++++- run-tests.sh | 37 ----------- src/BeforeValidException.php | 1 - src/ExpiredException.php | 1 - src/JWK.php | 34 +++++----- src/JWT.php | 106 +++++++++++++++--------------- src/SignatureInvalidException.php | 1 - tests/JWKTest.php | 2 +- tests/JWTTest.php | 25 +------ 10 files changed, 87 insertions(+), 136 deletions(-) delete mode 100755 run-tests.sh diff --git a/.gitattributes b/.gitattributes index c682f861..a791521e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,4 +5,3 @@ /.gitignore export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore -/run-tests.sh export-ignore diff --git a/.travis.yml b/.travis.yml index fc651ad3..90e516cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,8 @@ language: php +branches: + - only: [master] + php: - 5.6 - 7.0 @@ -16,8 +19,18 @@ matrix: dist: trusty - php: 5.5 dist: trusty + - name: "Check Style" + php: "7.4" + env: RUN_CS_FIXER=true sudo: false before_script: composer install -script: vendor/bin/phpunit +script: + - if [ "${RUN_CS_FIXER}" = "true" ]; then + composer require friendsofphp/php-cs-fixer && + vendor/bin/php-cs-fixer fix --diff --dry-run . && + vendor/bin/php-cs-fixer fix --rules=native_function_invocation --allow-risky=yes --diff src; + else + vendor/bin/phpunit; + fi diff --git a/run-tests.sh b/run-tests.sh deleted file mode 100755 index c4bb9348..00000000 --- a/run-tests.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -gpg --fingerprint D8406D0D82947747293778314AA394086372C20A -if [ $? -ne 0 ]; then - echo -e "\033[33mDownloading PGP Public Key...\033[0m" - gpg --recv-keys D8406D0D82947747293778314AA394086372C20A - # Sebastian Bergmann - gpg --fingerprint D8406D0D82947747293778314AA394086372C20A - if [ $? -ne 0 ]; then - echo -e "\033[31mCould not download PGP public key for verification\033[0m" - exit - fi -fi - -# Let's grab the latest release and its signature -if [ ! -f phpunit.phar ]; then - wget https://phar.phpunit.de/phpunit.phar -fi -if [ ! -f phpunit.phar.asc ]; then - wget https://phar.phpunit.de/phpunit.phar.asc -fi - -# Verify before running -gpg --verify phpunit.phar.asc phpunit.phar -if [ $? -eq 0 ]; then - echo - echo -e "\033[33mBegin Unit Testing\033[0m" - # Run the testing suite - php --version - php phpunit.phar --configuration phpunit.xml.dist -else - echo - chmod -x phpunit.phar - mv phpunit.phar /tmp/bad-phpunit.phar - mv phpunit.phar.asc /tmp/bad-phpunit.phar.asc - echo -e "\033[31mSignature did not match! PHPUnit has been moved to /tmp/bad-phpunit.phar\033[0m" - exit 1 -fi diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index a6ee2f7c..fdf82bd9 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -3,5 +3,4 @@ class BeforeValidException extends \UnexpectedValueException { - } diff --git a/src/ExpiredException.php b/src/ExpiredException.php index 3597370a..7f7d0568 100644 --- a/src/ExpiredException.php +++ b/src/ExpiredException.php @@ -3,5 +3,4 @@ class ExpiredException extends \UnexpectedValueException { - } diff --git a/src/JWK.php b/src/JWK.php index f2777df8..1d273917 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -50,7 +50,7 @@ public static function parseKeySet(array $jwks) } } - if (0 === count($keys)) { + if (0 === \count($keys)) { throw new UnexpectedValueException('No supported algorithms found in JWK Set'); } @@ -81,7 +81,7 @@ private static function parseKey(array $jwk) switch ($jwk['kty']) { case 'RSA': - if (array_key_exists('d', $jwk)) { + if (\array_key_exists('d', $jwk)) { throw new UnexpectedValueException('RSA private keys are not supported'); } if (!isset($jwk['n']) || !isset($jwk['e'])) { @@ -89,10 +89,10 @@ private static function parseKey(array $jwk) } $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); - $publicKey = openssl_pkey_get_public($pem); + $publicKey = \openssl_pkey_get_public($pem); if (false === $publicKey) { throw new DomainException( - 'OpenSSL error: ' . openssl_error_string() + 'OpenSSL error: ' . \openssl_error_string() ); } return $publicKey; @@ -118,32 +118,32 @@ private static function createPemFromModulusAndExponent($n, $e) $publicExponent = JWT::urlsafeB64Decode($e); $components = array( - 'modulus' => pack('Ca*a*', 2, self::encodeLength(strlen($modulus)), $modulus), - 'publicExponent' => pack('Ca*a*', 2, self::encodeLength(strlen($publicExponent)), $publicExponent) + 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), + 'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent) ); - $rsaPublicKey = pack( + $rsaPublicKey = \pack( 'Ca*a*a*', 48, - self::encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), + self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])), $components['modulus'], $components['publicExponent'] ); // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. - $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA - $rsaPublicKey = chr(0) . $rsaPublicKey; - $rsaPublicKey = chr(3) . self::encodeLength(strlen($rsaPublicKey)) . $rsaPublicKey; + $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $rsaPublicKey = \chr(0) . $rsaPublicKey; + $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; - $rsaPublicKey = pack( + $rsaPublicKey = \pack( 'Ca*a*', 48, - self::encodeLength(strlen($rsaOID . $rsaPublicKey)), + self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), $rsaOID . $rsaPublicKey ); $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . - chunk_split(base64_encode($rsaPublicKey), 64) . + \chunk_split(\base64_encode($rsaPublicKey), 64) . '-----END PUBLIC KEY-----'; return $rsaPublicKey; @@ -161,11 +161,11 @@ private static function createPemFromModulusAndExponent($n, $e) private static function encodeLength($length) { if ($length <= 0x7F) { - return chr($length); + return \chr($length); } - $temp = ltrim(pack('N', $length), chr(0)); + $temp = \ltrim(\pack('N', $length), \chr(0)); - return pack('Ca*', 0x80 | strlen($temp), $temp); + return \pack('Ca*', 0x80 | \strlen($temp), $temp); } } diff --git a/src/JWT.php b/src/JWT.php index 6f178b4e..4860028b 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -73,13 +73,13 @@ class JWT */ public static function decode($jwt, $key, array $allowed_algs = array()) { - $timestamp = is_null(static::$timestamp) ? time() : static::$timestamp; + $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; if (empty($key)) { throw new InvalidArgumentException('Key may not be empty'); } - $tks = explode('.', $jwt); - if (count($tks) != 3) { + $tks = \explode('.', $jwt); + if (\count($tks) != 3) { throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; @@ -98,7 +98,7 @@ 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)) { + if (!\in_array($header->alg, $allowed_algs)) { throw new UnexpectedValueException('Algorithm not allowed'); } if ($header->alg === 'ES256') { @@ -106,7 +106,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) $sig = self::signatureToDER($sig); } - if (is_array($key) || $key instanceof \ArrayAccess) { + 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'); @@ -126,7 +126,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) // token can actually be used. If it's not yet that time, abort. if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->nbf) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf) ); } @@ -135,7 +135,7 @@ public static function decode($jwt, $key, array $allowed_algs = array()) // correctly used the nbf claim). if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { throw new BeforeValidException( - 'Cannot handle token prior to ' . date(DateTime::ISO8601, $payload->iat) + 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) ); } @@ -169,18 +169,18 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he if ($keyId !== null) { $header['kid'] = $keyId; } - if (isset($head) && is_array($head)) { - $header = array_merge($head, $header); + if (isset($head) && \is_array($head)) { + $header = \array_merge($head, $header); } $segments = array(); $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); - $signing_input = implode('.', $segments); + $signing_input = \implode('.', $segments); $signature = static::sign($signing_input, $key, $alg); $segments[] = static::urlsafeB64Encode($signature); - return implode('.', $segments); + return \implode('.', $segments); } /** @@ -203,10 +203,10 @@ public static function sign($msg, $key, $alg = 'HS256') list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'hash_hmac': - return hash_hmac($algorithm, $msg, $key, true); + return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - $success = openssl_sign($msg, $signature, $key, $algorithm); + $success = \openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } else { @@ -240,7 +240,7 @@ private static function verify($msg, $signature, $key, $alg) list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - $success = openssl_verify($msg, $signature, $key, $algorithm); + $success = \openssl_verify($msg, $signature, $key, $algorithm); if ($success === 1) { return true; } elseif ($success === 0) { @@ -248,19 +248,19 @@ private static function verify($msg, $signature, $key, $alg) } // returns 1 on success, 0 on failure, -1 on error. throw new DomainException( - 'OpenSSL error: ' . openssl_error_string() + 'OpenSSL error: ' . \openssl_error_string() ); case 'hash_hmac': default: - $hash = hash_hmac($algorithm, $msg, $key, true); - if (function_exists('hash_equals')) { - return hash_equals($signature, $hash); + $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)); + $len = \min(static::safeStrlen($signature), static::safeStrlen($hash)); $status = 0; for ($i = 0; $i < $len; $i++) { - $status |= (ord($signature[$i]) ^ ord($hash[$i])); + $status |= (\ord($signature[$i]) ^ \ord($hash[$i])); } $status |= (static::safeStrlen($signature) ^ static::safeStrlen($hash)); @@ -279,23 +279,23 @@ private static function verify($msg, $signature, $key, $alg) */ public static function jsonDecode($input) { - if (version_compare(PHP_VERSION, '5.4.0', '>=') && !(defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { + if (\version_compare(PHP_VERSION, '5.4.0', '>=') && !(\defined('JSON_C_VERSION') && PHP_INT_SIZE > 4)) { /** In PHP >=5.4.0, json_decode() accepts an options parameter, that allows you * to specify that large ints (like Steam Transaction IDs) should be treated as * strings, rather than the PHP default behaviour of converting them to floats. */ - $obj = json_decode($input, false, 512, JSON_BIGINT_AS_STRING); + $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); } else { /** Not all servers will support that, however, so for older versions we must * manually detect large ints in the JSON string and quote them (thus converting *them to strings) before decoding, hence the preg_replace() call. */ - $max_int_length = strlen((string) PHP_INT_MAX) - 1; - $json_without_bigints = preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); - $obj = json_decode($json_without_bigints); + $max_int_length = \strlen((string) PHP_INT_MAX) - 1; + $json_without_bigints = \preg_replace('/:\s*(-?\d{'.$max_int_length.',})/', ': "$1"', $input); + $obj = \json_decode($json_without_bigints); } - if ($errno = json_last_error()) { + if ($errno = \json_last_error()) { static::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); @@ -314,8 +314,8 @@ public static function jsonDecode($input) */ public static function jsonEncode($input) { - $json = json_encode($input); - if ($errno = json_last_error()) { + $json = \json_encode($input); + if ($errno = \json_last_error()) { static::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); @@ -332,12 +332,12 @@ public static function jsonEncode($input) */ public static function urlsafeB64Decode($input) { - $remainder = strlen($input) % 4; + $remainder = \strlen($input) % 4; if ($remainder) { $padlen = 4 - $remainder; - $input .= str_repeat('=', $padlen); + $input .= \str_repeat('=', $padlen); } - return base64_decode(strtr($input, '-_', '+/')); + return \base64_decode(\strtr($input, '-_', '+/')); } /** @@ -349,7 +349,7 @@ public static function urlsafeB64Decode($input) */ public static function urlsafeB64Encode($input) { - return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); + return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); } /** @@ -384,10 +384,10 @@ private static function handleJsonError($errno) */ private static function safeStrlen($str) { - if (function_exists('mb_strlen')) { - return mb_strlen($str, '8bit'); + if (\function_exists('mb_strlen')) { + return \mb_strlen($str, '8bit'); } - return strlen($str); + return \strlen($str); } /** @@ -399,18 +399,18 @@ private static function safeStrlen($str) private static function signatureToDER($sig) { // Separate the signature into r-value and s-value - list($r, $s) = str_split($sig, (int) (strlen($sig) / 2)); + list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2)); // Trim leading zeros - $r = ltrim($r, "\x00"); - $s = ltrim($s, "\x00"); + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); // Convert r-value and s-value from unsigned big-endian integers to // signed two's complement - if (ord($r[0]) > 0x7f) { + if (\ord($r[0]) > 0x7f) { $r = "\x00" . $r; } - if (ord($s[0]) > 0x7f) { + if (\ord($s[0]) > 0x7f) { $s = "\x00" . $s; } @@ -436,10 +436,10 @@ private static function encodeDER($type, $value) } // Type - $der = chr($tag_header | $type); + $der = \chr($tag_header | $type); // Length - $der .= chr(strlen($value)); + $der .= \chr(\strlen($value)); return $der . $value; } @@ -460,12 +460,12 @@ private static function signatureFromDER($der, $keySize) // Convert r-value and s-value from signed two's compliment to unsigned // big-endian integers - $r = ltrim($r, "\x00"); - $s = ltrim($s, "\x00"); + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); // Pad out r and s so that they are $keySize bits long - $r = str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); - $s = str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); + $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); + $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); return $r . $s; } @@ -481,27 +481,27 @@ private static function signatureFromDER($der, $keySize) private static function readDER($der, $offset = 0) { $pos = $offset; - $size = strlen($der); - $constructed = (ord($der[$pos]) >> 5) & 0x01; - $type = ord($der[$pos++]) & 0x1f; + $size = \strlen($der); + $constructed = (\ord($der[$pos]) >> 5) & 0x01; + $type = \ord($der[$pos++]) & 0x1f; // Length - $len = ord($der[$pos++]); + $len = \ord($der[$pos++]); if ($len & 0x80) { $n = $len & 0x1f; $len = 0; while ($n-- && $pos < $size) { - $len = ($len << 8) | ord($der[$pos++]); + $len = ($len << 8) | \ord($der[$pos++]); } } // Value if ($type == self::ASN1_BIT_STRING) { $pos++; // Skip the first contents octet (padding indicator) - $data = substr($der, $pos, $len - 1); + $data = \substr($der, $pos, $len - 1); $pos += $len - 1; } elseif (!$constructed) { - $data = substr($der, $pos, $len); + $data = \substr($der, $pos, $len); $pos += $len; } else { $data = null; diff --git a/src/SignatureInvalidException.php b/src/SignatureInvalidException.php index 27332b21..87cb34df 100644 --- a/src/SignatureInvalidException.php +++ b/src/SignatureInvalidException.php @@ -3,5 +3,4 @@ class SignatureInvalidException extends \UnexpectedValueException { - } diff --git a/tests/JWKTest.php b/tests/JWKTest.php index d02534f1..3d317d55 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -88,7 +88,7 @@ public function testDecodeByMultiJwkKeySet() /* * For compatibility with PHPUnit 4.8 and PHP < 5.6 */ - public function setExpectedException($exceptionName, $message = '', $code = NULL) + public function setExpectedException($exceptionName, $message = '', $code = null) { if (method_exists($this, 'expectException')) { $this->expectException($exceptionName); diff --git a/tests/JWTTest.php b/tests/JWTTest.php index 867d87e5..fc9c3756 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -6,12 +6,11 @@ class JWTTest extends TestCase { - public static $opensslVerifyReturnValue; - /* * For compatibility with PHPUnit 4.8 and PHP < 5.6 */ - public function setExpectedException($exceptionName, $message = '', $code = NULL) { + public function setExpectedException($exceptionName, $message = '', $code = null) + { if (method_exists($this, 'expectException')) { $this->expectException($exceptionName); } else { @@ -285,15 +284,6 @@ public function testInvalidSignatureEncoding() JWT::decode($msg, 'secret', array('HS256')); } - public function testVerifyError() - { - $this->setExpectedException('DomainException'); - $pkey = openssl_pkey_new(); - $msg = JWT::encode('abc', $pkey, 'RS256'); - self::$opensslVerifyReturnValue = -1; - JWT::decode($msg, $pkey, array('RS256')); - } - /** * @runInSeparateProcess */ @@ -310,14 +300,3 @@ public function testEncodeAndDecodeEcdsaToken() $this->assertEquals('bar', $decoded->foo); } } - -/* - * Allows the testing of openssl_verify with an error return value - */ -function openssl_verify($msg, $signature, $key, $algorithm) -{ - if (null !== JWTTest::$opensslVerifyReturnValue) { - return JWTTest::$opensslVerifyReturnValue; - } - return \openssl_verify($msg, $signature, $key, $algorithm); -} From c4de650a0669e116b176b0bde8c31d5abc8c8153 Mon Sep 17 00:00:00 2001 From: Albin Wong Date: Mon, 18 May 2020 11:06:20 +0800 Subject: [PATCH 19/25] Adjust JWK Generate RSA Public Key Format --- src/JWK.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWK.php b/src/JWK.php index 1d273917..4926baeb 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -144,7 +144,7 @@ private static function createPemFromModulusAndExponent($n, $e) $rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . \chunk_split(\base64_encode($rsaPublicKey), 64) . - '-----END PUBLIC KEY-----'; + "\n-----END PUBLIC KEY-----"; return $rsaPublicKey; } From 1fae8c46348ed6da8a56deaff282034cb57673e0 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 26 Oct 2020 10:34:38 -0700 Subject: [PATCH 20/25] docs: add JWK usage to README (#307) --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 9c8b5455..ba139079 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,19 @@ echo "Decode:\n" . print_r($decoded_array, true) . "\n"; ?> ``` +Using JWKs +---------- + +```php +// 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 private +// key. Pass this as the second parameter to JWT::decode. +JWT::decode($payload, JWK::parseKeySet($jwks), $supportedAlgorithm); +``` + Changelog --------- From f42c9110abe98dd6cfe9053c49bc86acc70b2d23 Mon Sep 17 00:00:00 2001 From: Grant Anderson Date: Thu, 11 Feb 2021 17:02:00 -0700 Subject: [PATCH 21/25] fix: add missing use statement in JWK (#303) --- src/JWK.php | 1 + tests/JWKTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/JWK.php b/src/JWK.php index 1d273917..7632f4a4 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -3,6 +3,7 @@ namespace Firebase\JWT; use DomainException; +use InvalidArgumentException; use UnexpectedValueException; /** diff --git a/tests/JWKTest.php b/tests/JWKTest.php index 3d317d55..b8b67540 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -43,6 +43,20 @@ public function testParseJwkKeySet() self::$keys = $keys; } + public function testParseJwkKey_empty() + { + $this->setExpectedException('InvalidArgumentException', 'JWK must not be empty'); + + JWK::parseKeySet(array('keys' => array(array()))); + } + + public function testParseJwkKeySet_empty() + { + $this->setExpectedException('InvalidArgumentException', 'JWK Set did not contain any keys'); + + JWK::parseKeySet(array('keys' => array())); + } + /** * @depends testParseJwkKeySet */ From 7b4f4d2641d5b370f73ed0e6bcf340beddcc0ca3 Mon Sep 17 00:00:00 2001 From: Benoit Borrel <234378+bborrel@users.noreply.github.com> Date: Fri, 5 Mar 2021 14:39:07 -0500 Subject: [PATCH 22/25] chore: add phpdoc @throws in JWT::decode (#320) --- src/JWT.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/JWT.php b/src/JWT.php index 4860028b..76a0551c 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -62,6 +62,7 @@ class JWT * * @return object The JWT's payload as a PHP object * + * @throws InvalidArgumentException Provided JWT was empty * @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' From 474047dbd8442a730ab03810f2835839b107cd29 Mon Sep 17 00:00:00 2001 From: Ashutosh K Tripathi Date: Sat, 6 Mar 2021 03:07:26 +0530 Subject: [PATCH 23/25] chore: remove leading backslashes in imports (#301) Co-authored-by: Brent Shaffer --- src/JWT.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 76a0551c..b167abd7 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -2,10 +2,10 @@ namespace Firebase\JWT; -use \DomainException; -use \InvalidArgumentException; -use \UnexpectedValueException; -use \DateTime; +use DomainException; +use InvalidArgumentException; +use UnexpectedValueException; +use DateTime; /** * JSON Web Token implementation, based on this spec: From 8ddb39535ef82b835e39fe8f5ad3c5bd452a0148 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 17 May 2021 11:37:21 -0500 Subject: [PATCH 24/25] chore: remove travis, add github actions (#331) --- .github/actions/entrypoint.sh | 18 +++++++++ .github/workflows/tests.yml | 66 +++++++++++++++++++++++++++++++ .travis.yml | 36 ----------------- phpunit.xml.dist | 1 - src/BeforeValidException.php | 1 + src/ExpiredException.php | 1 + src/SignatureInvalidException.php | 1 + tests/JWKTest.php | 1 + tests/JWTTest.php | 1 + 9 files changed, 89 insertions(+), 37 deletions(-) create mode 100755 .github/actions/entrypoint.sh create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml diff --git a/.github/actions/entrypoint.sh b/.github/actions/entrypoint.sh new file mode 100755 index 00000000..ce8379cb --- /dev/null +++ b/.github/actions/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh -l + +apt-get update && \ +apt-get install -y --no-install-recommends \ + git \ + zip \ + curl \ + unzip \ + wget + +curl --silent --show-error https://getcomposer.org/installer | php +php composer.phar self-update + +echo "---Installing dependencies ---" +php composer.phar update + +echo "---Running unit tests ---" +vendor/bin/phpunit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..09539931 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,66 @@ +name: Test Suite +on: + push: + branches: + - master + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + php: [ "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0" ] + name: PHP ${{matrix.php }} Unit Test + steps: + - uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - name: Install Dependencies + uses: nick-invision/retry@v1 + with: + timeout_minutes: 10 + max_attempts: 3 + command: composer install + - name: Run Script + run: vendor/bin/phpunit + + # use dockerfiles for old versions of php (setup-php times out for those). + test_php55: + name: "PHP 5.5 Unit Test" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Run Unit Tests + uses: docker://php:5.5-cli + with: + entrypoint: ./.github/actions/entrypoint.sh + + test_php54: + name: "PHP 5.4 Unit Test" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Run Unit Tests + uses: docker://php:5.4-cli + with: + entrypoint: ./.github/actions/entrypoint.sh + + style: + runs-on: ubuntu-latest + name: PHP Style Check + steps: + - uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "7.0" + - name: Run Script + run: | + composer require friendsofphp/php-cs-fixer + vendor/bin/php-cs-fixer fix --diff --dry-run . + vendor/bin/php-cs-fixer fix --rules=native_function_invocation --allow-risky=yes --diff src diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 90e516cd..00000000 --- a/.travis.yml +++ /dev/null @@ -1,36 +0,0 @@ -language: php - -branches: - - only: [master] - -php: - - 5.6 - - 7.0 - - 7.1 - - 7.2 - - 7.3 - - 7.4 - -matrix: - include: - - php: 5.3 - dist: precise - - php: 5.4 - dist: trusty - - php: 5.5 - dist: trusty - - name: "Check Style" - php: "7.4" - env: RUN_CS_FIXER=true - -sudo: false - -before_script: composer install -script: - - if [ "${RUN_CS_FIXER}" = "true" ]; then - composer require friendsofphp/php-cs-fixer && - vendor/bin/php-cs-fixer fix --diff --dry-run . && - vendor/bin/php-cs-fixer fix --rules=native_function_invocation --allow-risky=yes --diff src; - else - vendor/bin/phpunit; - fi diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9f85f5ba..092a662c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" bootstrap="tests/bootstrap.php" > diff --git a/src/BeforeValidException.php b/src/BeforeValidException.php index fdf82bd9..c147852b 100644 --- a/src/BeforeValidException.php +++ b/src/BeforeValidException.php @@ -1,4 +1,5 @@ Date: Mon, 17 May 2021 11:49:46 -0500 Subject: [PATCH 25/25] fix: allow for null d values in RSA JWK (#330) --- src/JWK.php | 2 +- tests/JWKTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/JWK.php b/src/JWK.php index 7632f4a4..29dbbac1 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -82,7 +82,7 @@ private static function parseKey(array $jwk) 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'])) { diff --git a/tests/JWKTest.php b/tests/JWKTest.php index 93572400..0709836d 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -32,6 +32,36 @@ public function testInvalidAlgorithm() $keys = JWK::parseKeySet(array('keys' => array($badJwk))); } + public function testParsePrivateKey() + { + $this->setExpectedException( + 'UnexpectedValueException', + 'RSA private keys are not supported' + ); + + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/rsa-jwkset.json'), + true + ); + $jwkSet['keys'][0]['d'] = 'privatekeyvalue'; + + JWK::parseKeySet($jwkSet); + } + + public function testParseKeyWithEmptyDValue() + { + $jwkSet = json_decode( + file_get_contents(__DIR__ . '/rsa-jwkset.json'), + true + ); + + // empty or null values are ok + $jwkSet['keys'][0]['d'] = null; + + $keys = JWK::parseKeySet($jwkSet); + $this->assertTrue(is_array($keys)); + } + public function testParseJwkKeySet() { $jwkSet = json_decode(