From 0135ce4c01ac9592c30e6f4320e0189f5400f85e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 16:59:01 -0600 Subject: [PATCH 01/14] chore: update library to phpstan level 7 --- .github/workflows/tests.yml | 2 +- phpstan.neon.dist | 5 ++ src/JWK.php | 14 +++-- src/JWT.php | 107 ++++++++++++++++++++---------------- src/Key.php | 7 --- 5 files changed, 76 insertions(+), 59 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 80063a00..0a7be1a1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,4 +54,4 @@ jobs: - name: Run Script run: | composer global require phpstan/phpstan - ~/.composer/vendor/bin/phpstan analyse src + ~/.composer/vendor/bin/phpstan analyse \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..56aeebfb --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + level: 7 + paths: + - src + treatPhpDocTypesAsCertain: false diff --git a/src/JWK.php b/src/JWK.php index 5663c948..45038060 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -23,7 +23,7 @@ class JWK /** * Parse a set of JWK keys * - * @param array $jwks The JSON Web Key Set as an associative array + * @param array $jwks The JSON Web Key Set as an associative array * * @return array An associative array of key IDs (kid) to Key objects * @@ -48,7 +48,7 @@ public static function parseKeySet(array $jwks): array foreach ($jwks['keys'] as $k => $v) { $kid = isset($v['kid']) ? $v['kid'] : $k; if ($key = self::parseKey($v)) { - $keys[$kid] = $key; + $keys[(string) $kid] = $key; } } @@ -62,7 +62,7 @@ public static function parseKeySet(array $jwks): array /** * Parse a JWK key * - * @param array $jwk An individual JWK + * @param array $jwk An individual JWK * * @return Key The key object for the JWK * @@ -126,8 +126,12 @@ public static function parseKey(array $jwk): ?Key */ private static function createPemFromModulusAndExponent($n, $e) { - $modulus = JWT::urlsafeB64Decode($n); - $publicExponent = JWT::urlsafeB64Decode($e); + if (false === ($modulus = JWT::urlsafeB64Decode($n))) { + throw new UnexpectedValueException('Invalid JWK encoding'); + } + if (false === ($publicExponent = JWT::urlsafeB64Decode($e))) { + throw new UnexpectedValueException('Invalid header encoding'); + } $components = [ 'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus), diff --git a/src/JWT.php b/src/JWT.php index b725aae4..7bf2628d 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -38,6 +38,9 @@ class JWT */ public static int $leeway = 0; + /** + * @var array> + */ public static array $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], @@ -86,10 +89,16 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; - if (null === ($header = static::jsonDecode(static::urlsafeB64Decode($headb64)))) { + if (false === ($headerRaw = static::urlsafeB64Decode($headb64))) { throw new UnexpectedValueException('Invalid header encoding'); } - if (null === $payload = static::jsonDecode(static::urlsafeB64Decode($bodyb64))) { + if (null === ($header = static::jsonDecode($headerRaw))) { + throw new UnexpectedValueException('Invalid header encoding'); + } + if (false === ($payloadRaw = static::urlsafeB64Decode($bodyb64))) { + throw new UnexpectedValueException('Invalid claims encoding'); + } + if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException('Invalid claims encoding'); } if (!$payload instanceof stdClass) { @@ -116,7 +125,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) // OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures $sig = self::signatureToDER($sig); } - if (!static::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { + if (!self::verify("$headb64.$bodyb64", $sig, $key->getKeyMaterial(), $header->alg)) { throw new SignatureInvalidException('Signature verification failed'); } @@ -148,11 +157,11 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) /** * Converts and signs a PHP object or array into a JWT string. * - * @param array $payload PHP array - * @param string|OpenSSLAsymmetricKey $key The secret key. + * @param array $payload PHP array + * @param string|OpenSSLAsymmetricKey $key The secret key. * If the algorithm used is asymmetric, this is the private key - * @param string $keyId - * @param array $head An array with header elements to attach + * @param string $keyId + * @param array $head An array with header elements to attach * * @return string A signed JWT * @@ -174,8 +183,8 @@ public static function encode( $header = \array_merge($head, $header); } $segments = []; - $segments[] = static::urlsafeB64Encode(static::jsonEncode($header)); - $segments[] = static::urlsafeB64Encode(static::jsonEncode($payload)); + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); $signing_input = \implode('.', $segments); $signature = static::sign($signing_input, $key, $alg); @@ -190,7 +199,7 @@ public static function encode( * @param string $msg The message to sign * @param string|OpenSSLAsymmetricKey $key The secret key. * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * @@ -204,6 +213,9 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'hash_hmac': + if (!is_string($key)) { + throw new InvalidArgumentException('key must be a string when using hmac'); + } return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; @@ -221,10 +233,13 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin if (!function_exists('sodium_crypto_sign_detached')) { throw new DomainException('libsodium is not available'); } + if (!is_string($key)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } try { // The last non-empty line is used as the key. $lines = array_filter(explode("\n", $key)); - $key = base64_decode(end($lines)); + $key = base64_decode((string) end($lines)); return sodium_crypto_sign_detached($msg, $key); } catch (Exception $e) { throw new DomainException($e->getMessage(), 0, $e); @@ -238,10 +253,10 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin * Verify a signature with the message, key and method. Not all methods * are symmetric, so we must have a separate verify and sign method. * - * @param string $msg The original message (header and body) - * @param string $signature The original signature - * @param string|OpenSSLAsymmetricKey $key For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey - * @param string $alg The algorithm + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|OpenSSLAsymmetricKey $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string $alg The algorithm * * @return bool * @@ -274,16 +289,22 @@ private static function verify( if (!function_exists('sodium_crypto_sign_verify_detached')) { throw new DomainException('libsodium is not available'); } + if (!is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } try { // The last non-empty line is used as the key. $lines = array_filter(explode("\n", $keyMaterial)); - $key = base64_decode(end($lines)); + $key = base64_decode((string) end($lines)); return sodium_crypto_sign_verify_detached($signature, $msg, $key); } catch (Exception $e) { throw new DomainException($e->getMessage(), 0, $e); } case 'hash_hmac': default: + if (!is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using hmac'); + } $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); return self::constantTimeEquals($hash, $signature); } @@ -303,7 +324,7 @@ public static function jsonDecode(string $input): mixed $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); if ($errno = \json_last_error()) { - static::handleJsonError($errno); + self::handleJsonError($errno); } elseif ($obj === null && $input !== 'null') { throw new DomainException('Null result with non-null input'); } @@ -313,13 +334,13 @@ public static function jsonDecode(string $input): mixed /** * Encode a PHP array into a JSON string. * - * @param array $input A PHP array + * @param array $input A PHP array * - * @return string JSON representation of the PHP array + * @return string|false JSON representation of the PHP array * * @throws DomainException Provided object could not be encoded to valid JSON */ - public static function jsonEncode(array $input): string + public static function jsonEncode(array $input): string|false { if (PHP_VERSION_ID >= 50400) { $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); @@ -328,7 +349,7 @@ public static function jsonEncode(array $input): string $json = \json_encode($input); } if ($errno = \json_last_error()) { - static::handleJsonError($errno); + self::handleJsonError($errno); } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); } @@ -342,7 +363,7 @@ public static function jsonEncode(array $input): string * * @return string A decoded string */ - public static function urlsafeB64Decode(string $input): string + public static function urlsafeB64Decode(string $input): string|false { $remainder = \strlen($input) % 4; if ($remainder) { @@ -381,29 +402,22 @@ private static function getKey(Key|array|ArrayAccess $keyOrKeyArray, ?string $ki return $keyOrKeyArray; } - if (is_array($keyOrKeyArray) || $keyOrKeyArray instanceof ArrayAccess) { - foreach ($keyOrKeyArray as $keyId => $key) { - if (!$key instanceof Key) { - throw new TypeError( - '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' - . 'array of Firebase\JWT\Key keys' - ); - } - } - if (!isset($kid)) { - throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); - } - if (!isset($keyOrKeyArray[$kid])) { - throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + foreach ($keyOrKeyArray as $keyId => $key) { + if (!$key instanceof Key) { + throw new TypeError( + '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' + . 'array of Firebase\JWT\Key keys' + ); } - - return $keyOrKeyArray[$kid]; + } + 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'); } - throw new UnexpectedValueException( - '$keyOrKeyArray must be an instance of Firebase\JWT\Key key or an ' - . 'array of Firebase\JWT\Key keys' - ); + return $keyOrKeyArray[$kid]; } /** @@ -416,13 +430,13 @@ public static function constantTimeEquals(string $left, string $right): bool if (\function_exists('hash_equals')) { return \hash_equals($left, $right); } - $len = \min(static::safeStrlen($left), static::safeStrlen($right)); + $len = \min(self::safeStrlen($left), self::safeStrlen($right)); $status = 0; for ($i = 0; $i < $len; $i++) { $status |= (\ord($left[$i]) ^ \ord($right[$i])); } - $status |= (static::safeStrlen($left) ^ static::safeStrlen($right)); + $status |= (self::safeStrlen($left) ^ self::safeStrlen($right)); return ($status === 0); } @@ -476,7 +490,8 @@ private static function safeStrlen(string $str): int private static function signatureToDER(string $sig): string { // Separate the signature into r-value and s-value - list($r, $s) = \str_split($sig, (int) (\strlen($sig) / 2)); + $length = max(1, (int) (\strlen($sig) / 2)); + list($r, $s) = \str_split($sig, $length > 0 ? $length : 1); // Trim leading zeros $r = \ltrim($r, "\x00"); @@ -556,7 +571,7 @@ private static function signatureFromDER(string $der, int $keySize): string * @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 + * @return array{int, string|null} the new offset and the decoded object */ private static function readDER(string $der, int $offset = 0): array { diff --git a/src/Key.php b/src/Key.php index 286a3143..61144d06 100644 --- a/src/Key.php +++ b/src/Key.php @@ -16,13 +16,6 @@ public function __construct( private string|OpenSSLAsymmetricKey $keyMaterial, private string $algorithm ) { - if ( - !is_string($keyMaterial) - && !$keyMaterial instanceof OpenSSLAsymmetricKey - ) { - throw new TypeError('Key material must be a string or OpenSSLAsymmetricKey'); - } - if (empty($keyMaterial)) { throw new InvalidArgumentException('Key material must not be empty'); } From 170e080793969df6f6e4424345c34a29bc1f3976 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 16:59:54 -0600 Subject: [PATCH 02/14] eof newline --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0a7be1a1..81630645 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -54,4 +54,4 @@ jobs: - name: Run Script run: | composer global require phpstan/phpstan - ~/.composer/vendor/bin/phpstan analyse \ No newline at end of file + ~/.composer/vendor/bin/phpstan analyse From 75795b7ebf849310287820b158a40d982113742a Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 20:24:23 -0600 Subject: [PATCH 03/14] cleaner array syntax --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index 7bf2628d..b790096d 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -39,7 +39,7 @@ class JWT public static int $leeway = 0; /** - * @var array> + * @var array */ public static array $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], From 964987cb6e9217fef59dd4fdf94f7e17dfd81ded Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 18:26:55 -0800 Subject: [PATCH 04/14] Update JWK.php --- src/JWK.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/JWK.php b/src/JWK.php index 45038060..d5ad93a8 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -124,8 +124,10 @@ public static function parseKey(array $jwk): ?Key * * @uses encodeLength */ - private static function createPemFromModulusAndExponent($n, $e) - { + private static function createPemFromModulusAndExponent( + string $n, + string $e + ): string { if (false === ($modulus = JWT::urlsafeB64Decode($n))) { throw new UnexpectedValueException('Invalid JWK encoding'); } From 1a8793dee13d8493e42ae42d2ce7a47127e1a198 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 08:30:16 -0600 Subject: [PATCH 05/14] add back compatibility for >= PHP 7.1 --- .github/workflows/tests.yml | 2 +- composer.json | 2 +- src/JWK.php | 18 ++++++------ src/JWT.php | 55 +++++++++++++++++++++---------------- src/Key.php | 25 +++++++++++++---- 5 files changed, 61 insertions(+), 41 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 81630645..68d4f10b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ "8.0", "8.1"] + php: [ "7.1", "7.2", "7.3", "7.4", "8.0", "8.1"] name: PHP ${{matrix.php }} Unit Test steps: - uses: actions/checkout@v2 diff --git a/composer.json b/composer.json index 4e190ea3..9351a02a 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ ], "license": "BSD-3-Clause", "require": { - "php": "^8.0" + "php": "^7.1||^8.0" }, "suggest": { "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" diff --git a/src/JWK.php b/src/JWK.php index d5ad93a8..0690fc50 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -128,24 +128,22 @@ private static function createPemFromModulusAndExponent( string $n, string $e ): string { - if (false === ($modulus = JWT::urlsafeB64Decode($n))) { + if (false === ($mod = JWT::urlsafeB64Decode($n))) { throw new UnexpectedValueException('Invalid JWK encoding'); } - if (false === ($publicExponent = JWT::urlsafeB64Decode($e))) { + if (false === ($exp = JWT::urlsafeB64Decode($e))) { throw new UnexpectedValueException('Invalid header encoding'); } - $components = [ - '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($mod)), $mod); + $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); $rsaPublicKey = \pack( 'Ca*a*a*', 48, - self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])), - $components['modulus'], - $components['publicExponent'] + self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), + $modulus, + $publicExponent ); // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. @@ -176,7 +174,7 @@ private static function createPemFromModulusAndExponent( * @param int $length * @return string */ - private static function encodeLength($length) + private static function encodeLength(int $length): string { if ($length <= 0x7F) { return \chr($length); diff --git a/src/JWT.php b/src/JWT.php index b790096d..80b026fa 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -36,12 +36,12 @@ class JWT * we want to provide some extra leeway time to * account for clock skew. */ - public static int $leeway = 0; + public static /* int */ $leeway = 0; /** * @var array */ - public static array $supported_algs = [ + public static /* array */ $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], 'HS256' => ['hash_hmac', 'SHA256'], @@ -76,8 +76,10 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray): stdClass - { + public static function decode( + string $jwt, + /* Key|array|ArrayAccess */ $keyOrKeyArray + ): stdClass { // Validate JWT $timestamp = \time(); @@ -157,11 +159,11 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) /** * Converts and signs a PHP object or array into a JWT string. * - * @param array $payload PHP array - * @param string|OpenSSLAsymmetricKey $key The secret key. - * If the algorithm used is asymmetric, this is the private key - * @param string $keyId - * @param array $head An array with header elements to attach + * @param array $payload PHP array + * @param string|resource|OpenSSLAsymmetricKey $key The secret key. + * If the algorithm used is asymmetric, this is the private key + * @param string $keyId + * @param array $head An array with header elements to attach * * @return string A signed JWT * @@ -170,7 +172,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) */ public static function encode( array $payload, - string|OpenSSLAsymmetricKey $key, + /* string|resource|OpenSSLAsymmetricKey */ $key, string $alg, string $keyId = null, array $head = null @@ -196,17 +198,20 @@ public static function encode( /** * Sign a string with a given key and algorithm. * - * @param string $msg The message to sign - * @param string|OpenSSLAsymmetricKey $key The secret key. - * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', - * 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $msg The message to sign + * @param string|resource|OpenSSLAsymmetricKey $key The secret key. + * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message * * @throws DomainException Unsupported algorithm or bad key was specified */ - public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, string $alg): string - { + public static function sign( + string $msg, + /* string|resource|OpenSSLAsymmetricKey */ $key, + string $alg + ): string { if (empty(static::$supported_algs[$alg])) { throw new DomainException('Algorithm not supported'); } @@ -253,10 +258,10 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin * Verify a signature with the message, key and method. Not all methods * are symmetric, so we must have a separate verify and sign method. * - * @param string $msg The original message (header and body) - * @param string $signature The original signature - * @param string|OpenSSLAsymmetricKey $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey - * @param string $alg The algorithm + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|resource|OpenSSLAsymmetricKey $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string $alg The algorithm * * @return bool * @@ -265,7 +270,7 @@ public static function sign(string $msg, string|OpenSSLAsymmetricKey $key, strin private static function verify( string $msg, string $signature, - string|OpenSSLAsymmetricKey $keyMaterial, + /* string|resource|OpenSSLAsymmetricKey */ $keyMaterial, string $alg ): bool { if (empty(static::$supported_algs[$alg])) { @@ -319,7 +324,7 @@ private static function verify( * * @throws DomainException Provided string was invalid JSON */ - public static function jsonDecode(string $input): mixed + public static function jsonDecode(string $input)/*: mixed */ { $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); @@ -396,8 +401,10 @@ public static function urlsafeB64Encode(string $input): string * * @return Key */ - private static function getKey(Key|array|ArrayAccess $keyOrKeyArray, ?string $kid): Key - { + private static function getKey( + /* Key|array|ArrayAccess */ $keyOrKeyArray, + ?string $kid + ): Key { if ($keyOrKeyArray instanceof Key) { return $keyOrKeyArray; } diff --git a/src/Key.php b/src/Key.php index 61144d06..ca96adfd 100644 --- a/src/Key.php +++ b/src/Key.php @@ -8,14 +8,25 @@ class Key { + private /* string|resource|OpenSSLAsymmetricKey */ $keyMaterial; + private /* string */ $algorithm; + /** - * @param string|OpenSSLAsymmetricKey $keyMaterial + * @param string|resource|OpenSSLAsymmetricKey $keyMaterial * @param string $algorithm */ public function __construct( - private string|OpenSSLAsymmetricKey $keyMaterial, - private string $algorithm + /* private string|resource|OpenSSLAsymmetricKey */ $keyMaterial, + /* private */ string $algorithm ) { + if ( + !is_string($keyMaterial) + && !$keyMaterial instanceof OpenSSLAsymmetricKey + && !is_resource($keyMaterial) + ) { + throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey'); + } + if (empty($keyMaterial)) { throw new InvalidArgumentException('Key material must not be empty'); } @@ -23,6 +34,10 @@ public function __construct( if (empty($algorithm)) { throw new InvalidArgumentException('Algorithm must not be empty'); } + + // TODO: Remove in PHP 8.0 in favor of class constructor property promotion + $this->keyMaterial = $keyMaterial; + $this->algorithm = $algorithm; } /** @@ -36,9 +51,9 @@ public function getAlgorithm(): string } /** - * @return string|OpenSSLAsymmetricKey + * @return string|resource|OpenSSLAsymmetricKey */ - public function getKeyMaterial(): mixed + public function getKeyMaterial()/*: string|resource|OpenSSLAsymmetricKey */ { return $this->keyMaterial; } From 466d0880c3d544644d5aa8ede982513a3234b996 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 08:48:52 -0600 Subject: [PATCH 06/14] add phpunit 7.5 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9351a02a..5ef2ea2d 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^7.5||9.5" } } From 2014a6cd8020307659a0493f45c44d3fd58dd644 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 08:52:56 -0600 Subject: [PATCH 07/14] style fixes --- src/JWT.php | 19 ++++++++++++------- src/Key.php | 10 ++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 80b026fa..fcc5ba6d 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -36,12 +36,12 @@ class JWT * we want to provide some extra leeway time to * account for clock skew. */ - public static /* int */ $leeway = 0; + /* int */ public static $leeway = 0; /** * @var array */ - public static /* array */ $supported_algs = [ + /* array */ public static $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], 'HS256' => ['hash_hmac', 'SHA256'], @@ -78,7 +78,8 @@ class JWT */ public static function decode( string $jwt, - /* Key|array|ArrayAccess */ $keyOrKeyArray + /* Key|array|ArrayAccess */ + $keyOrKeyArray ): stdClass { // Validate JWT $timestamp = \time(); @@ -172,7 +173,8 @@ public static function decode( */ public static function encode( array $payload, - /* string|resource|OpenSSLAsymmetricKey */ $key, + /* string|resource|OpenSSLAsymmetricKey */ + $key, string $alg, string $keyId = null, array $head = null @@ -209,7 +211,8 @@ public static function encode( */ public static function sign( string $msg, - /* string|resource|OpenSSLAsymmetricKey */ $key, + /* string|resource|OpenSSLAsymmetricKey */ + $key, string $alg ): string { if (empty(static::$supported_algs[$alg])) { @@ -270,7 +273,8 @@ public static function sign( private static function verify( string $msg, string $signature, - /* string|resource|OpenSSLAsymmetricKey */ $keyMaterial, + /* string|resource|OpenSSLAsymmetricKey */ + $keyMaterial, string $alg ): bool { if (empty(static::$supported_algs[$alg])) { @@ -402,7 +406,8 @@ public static function urlsafeB64Encode(string $input): string * @return Key */ private static function getKey( - /* Key|array|ArrayAccess */ $keyOrKeyArray, + /* Key|array|ArrayAccess */ + $keyOrKeyArray, ?string $kid ): Key { if ($keyOrKeyArray instanceof Key) { diff --git a/src/Key.php b/src/Key.php index ca96adfd..7d573a8f 100644 --- a/src/Key.php +++ b/src/Key.php @@ -8,16 +8,18 @@ class Key { - private /* string|resource|OpenSSLAsymmetricKey */ $keyMaterial; - private /* string */ $algorithm; + /* string|resource|OpenSSLAsymmetricKey */ private $keyMaterial; + /* string */ private $algorithm; /** * @param string|resource|OpenSSLAsymmetricKey $keyMaterial * @param string $algorithm */ public function __construct( - /* private string|resource|OpenSSLAsymmetricKey */ $keyMaterial, - /* private */ string $algorithm + /* private string|resource|OpenSSLAsymmetricKey */ + $keyMaterial, + /* private */ + string $algorithm ) { if ( !is_string($keyMaterial) From 9b81de9ffaa2ce097e9baaa56b80f102e03a06c1 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 11:27:16 -0600 Subject: [PATCH 08/14] fix static analysis --- src/Key.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Key.php b/src/Key.php index 7d573a8f..c462c41f 100644 --- a/src/Key.php +++ b/src/Key.php @@ -8,8 +8,10 @@ class Key { - /* string|resource|OpenSSLAsymmetricKey */ private $keyMaterial; - /* string */ private $algorithm; + /** @var string|resource|OpenSSLAsymmetricKey */ + private $keyMaterial; + /** @var string */ + private $algorithm; /** * @param string|resource|OpenSSLAsymmetricKey $keyMaterial From 3364bf1325b623ce0bd0f8fb64d0a7e1a8a9eb1b Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 17 Feb 2022 20:47:44 -0600 Subject: [PATCH 09/14] phpstan updates --- src/JWT.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index fcc5ba6d..70a95158 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -35,8 +35,10 @@ class JWT * When checking nbf, iat or expiration times, * we want to provide some extra leeway time to * account for clock skew. + * + * @var int */ - /* int */ public static $leeway = 0; + /* array */ public static $leeway = 0; /** * @var array @@ -227,7 +229,7 @@ public static function sign( return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - $success = \openssl_sign($msg, $signature, $key, $algorithm); + $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } @@ -284,7 +286,7 @@ private static function verify( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); + $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line if ($success === 1) { return true; } elseif ($success === 0) { From e88dfc96e4a009816ef3a28fc6f13db7f3075b35 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sun, 20 Mar 2022 16:38:59 -0600 Subject: [PATCH 10/14] fix errors --- src/JWT.php | 17 +++++++++++++---- src/Key.php | 11 ++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index cc73210e..50599335 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -347,11 +347,11 @@ public static function jsonDecode(string $input)/*: mixed */ * * @param array $input A PHP array * - * @return string|false JSON representation of the PHP array + * @return string JSON representation of the PHP array * * @throws DomainException Provided object could not be encoded to valid JSON */ - public static function jsonEncode(array $input): string|false + public static function jsonEncode(array $input): string { if (PHP_VERSION_ID >= 50400) { $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); @@ -364,6 +364,9 @@ public static function jsonEncode(array $input): string|false } elseif ($json === 'null' && $input !== null) { throw new DomainException('Null result with non-null input'); } + if ($json === false) { + throw new DomainException('Provided object could not be encoded to valid JSON'); + } return $json; } @@ -373,15 +376,21 @@ public static function jsonEncode(array $input): string|false * @param string $input A Base64 encoded string * * @return string A decoded string + * + * @throws InvalidArgumentException invalid base64 characters */ - public static function urlsafeB64Decode(string $input): string|false + public static function urlsafeB64Decode(string $input): string { $remainder = \strlen($input) % 4; if ($remainder) { $padlen = 4 - $remainder; $input .= \str_repeat('=', $padlen); } - return \base64_decode(\strtr($input, '-_', '+/')); + $base64 = \base64_decode(\strtr($input, '-_', '+/')); + if (false === $base64) { + throw new InvalidArgumentException('input contains invalid characters for base64'); + } + return $base64; } /** diff --git a/src/Key.php b/src/Key.php index bb31abba..88dace91 100644 --- a/src/Key.php +++ b/src/Key.php @@ -9,17 +9,17 @@ class Key { - /** @var string|resource|OpenSSLAsymmetricKey */ + /** @var string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ private $keyMaterial; /** @var string */ private $algorithm; /** - * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial * @param string $algorithm */ public function __construct( - /* private string|resource|OpenSSLAsymmetricKey */ + /* private string|OpenSSLAsymmetricKey|OpenSSLCertificate */ $keyMaterial, /* private */ string $algorithm @@ -27,6 +27,7 @@ public function __construct( if ( !is_string($keyMaterial) && !$keyMaterial instanceof OpenSSLAsymmetricKey + && !$keyMaterial instanceof OpenSSLCertificate && !is_resource($keyMaterial) ) { throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey'); @@ -56,9 +57,9 @@ public function getAlgorithm(): string } /** - * @return string|OpenSSLAsymmetricKey|OpenSSLCertificate|array + * @return string|OpenSSLAsymmetricKey|OpenSSLCertificate */ - public function getKeyMaterial()/*: string|resource|OpenSSLAsymmetricKey */ + public function getKeyMaterial()/*: string|OpenSSLAsymmetricKey|OpenSSLCertificate */ { return $this->keyMaterial; } From 6455b0b914a8402bd6596dd6aedbef495123fee0 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sun, 20 Mar 2022 16:47:59 -0600 Subject: [PATCH 11/14] fix lint --- src/JWT.php | 29 +++++++---------------------- src/Key.php | 6 ++---- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index 50599335..394231c3 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -81,7 +81,6 @@ class JWT */ public static function decode( string $jwt, - /* Key|array|ArrayAccess */ $keyOrKeyArray ): stdClass { // Validate JWT @@ -95,24 +94,18 @@ public static function decode( throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; - if (false === ($headerRaw = static::urlsafeB64Decode($headb64))) { - throw new UnexpectedValueException('Invalid header encoding'); - } + $headerRaw = static::urlsafeB64Decode($headb64); if (null === ($header = static::jsonDecode($headerRaw))) { throw new UnexpectedValueException('Invalid header encoding'); } - if (false === ($payloadRaw = static::urlsafeB64Decode($bodyb64))) { - throw new UnexpectedValueException('Invalid claims encoding'); - } + $payloadRaw = static::urlsafeB64Decode($bodyb64); if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException('Invalid claims encoding'); } if (!$payload instanceof stdClass) { throw new UnexpectedValueException('Payload must be a JSON object'); } - if (false === ($sig = static::urlsafeB64Decode($cryptob64))) { - throw new UnexpectedValueException('Invalid signature encoding'); - } + $sig = static::urlsafeB64Decode($cryptob64); if (empty($header->alg)) { throw new UnexpectedValueException('Empty algorithm'); } @@ -175,7 +168,6 @@ public static function decode( */ public static function encode( array $payload, - /* string|resource|OpenSSLAsymmetricKey */ $key, string $alg, string $keyId = null, @@ -203,7 +195,7 @@ public static function encode( * Sign a string with a given key and algorithm. * * @param string $msg The message to sign - * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key The secret key. + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', * 'HS512', 'RS256', 'RS384', and 'RS512' * @@ -213,7 +205,6 @@ public static function encode( */ public static function sign( string $msg, - /* string|resource|OpenSSLAsymmetricKey */ $key, string $alg ): string { @@ -229,7 +220,7 @@ public static function sign( return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line + $success = \openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } @@ -275,7 +266,6 @@ public static function sign( private static function verify( string $msg, string $signature, - /* string|resource|OpenSSLAsymmetricKey */ $keyMaterial, string $alg ): bool { @@ -286,7 +276,7 @@ private static function verify( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line + $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); if ($success === 1) { return true; } elseif ($success === 0) { @@ -386,11 +376,7 @@ public static function urlsafeB64Decode(string $input): string $padlen = 4 - $remainder; $input .= \str_repeat('=', $padlen); } - $base64 = \base64_decode(\strtr($input, '-_', '+/')); - if (false === $base64) { - throw new InvalidArgumentException('input contains invalid characters for base64'); - } - return $base64; + return \base64_decode(\strtr($input, '-_', '+/')); } /** @@ -417,7 +403,6 @@ public static function urlsafeB64Encode(string $input): string * @return Key */ private static function getKey( - /* Key|array|ArrayAccess */ $keyOrKeyArray, ?string $kid ): Key { diff --git a/src/Key.php b/src/Key.php index 88dace91..5a33344b 100644 --- a/src/Key.php +++ b/src/Key.php @@ -19,9 +19,7 @@ class Key * @param string $algorithm */ public function __construct( - /* private string|OpenSSLAsymmetricKey|OpenSSLCertificate */ $keyMaterial, - /* private */ string $algorithm ) { if ( @@ -57,9 +55,9 @@ public function getAlgorithm(): string } /** - * @return string|OpenSSLAsymmetricKey|OpenSSLCertificate + * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ - public function getKeyMaterial()/*: string|OpenSSLAsymmetricKey|OpenSSLCertificate */ + public function getKeyMaterial()/*: string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ { return $this->keyMaterial; } From d10ad4afc2fc25deac47a8d1979e0588982691ea Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sun, 20 Mar 2022 16:53:23 -0600 Subject: [PATCH 12/14] fix lint some more --- src/JWK.php | 8 ++------ src/JWT.php | 6 +++--- src/Key.php | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/JWK.php b/src/JWK.php index 0690fc50..dbc446e6 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -128,12 +128,8 @@ private static function createPemFromModulusAndExponent( string $n, string $e ): string { - if (false === ($mod = JWT::urlsafeB64Decode($n))) { - throw new UnexpectedValueException('Invalid JWK encoding'); - } - if (false === ($exp = JWT::urlsafeB64Decode($e))) { - throw new UnexpectedValueException('Invalid header encoding'); - } + $mod = JWT::urlsafeB64Decode($n); + $exp = JWT::urlsafeB64Decode($e); $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); diff --git a/src/JWT.php b/src/JWT.php index 394231c3..0cc36441 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -157,7 +157,7 @@ public static function decode( * Converts and signs a PHP object or array into a JWT string. * * @param array $payload PHP array - * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key The secret key. + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. * @param string $keyId * @param array $head An array with header elements to attach * @@ -220,7 +220,7 @@ public static function sign( return \hash_hmac($algorithm, $msg, $key, true); case 'openssl': $signature = ''; - $success = \openssl_sign($msg, $signature, $key, $algorithm); + $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line if (!$success) { throw new DomainException("OpenSSL unable to sign data"); } @@ -256,7 +256,7 @@ public static function sign( * * @param string $msg The original message (header and body) * @param string $signature The original signature - * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey * @param string $alg The algorithm * * @return bool diff --git a/src/Key.php b/src/Key.php index 5a33344b..b09ad190 100644 --- a/src/Key.php +++ b/src/Key.php @@ -57,7 +57,7 @@ public function getAlgorithm(): string /** * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ - public function getKeyMaterial()/*: string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ + public function getKeyMaterial() { return $this->keyMaterial; } From c65756dda22692fb23c196e0da1808862bf0d985 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sun, 20 Mar 2022 16:56:07 -0600 Subject: [PATCH 13/14] one more ignoreline --- src/JWT.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index 0cc36441..c8c4f4f8 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -276,7 +276,7 @@ private static function verify( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); + $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line if ($success === 1) { return true; } elseif ($success === 0) { From f4b98ce34f13f7b8d53a9fa8158152457c6665e2 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Sun, 20 Mar 2022 16:58:47 -0600 Subject: [PATCH 14/14] final cleanup --- src/JWT.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index c8c4f4f8..f6a4772c 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -39,12 +39,12 @@ class JWT * * @var int */ - /* array */ public static $leeway = 0; + public static $leeway = 0; /** * @var array */ - /* array */ public static $supported_algs = [ + public static $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], 'HS256' => ['hash_hmac', 'SHA256'], @@ -320,7 +320,7 @@ private static function verify( * * @throws DomainException Provided string was invalid JSON */ - public static function jsonDecode(string $input)/*: mixed */ + public static function jsonDecode(string $input) { $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);