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..5ef2ea2d 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" @@ -31,6 +31,6 @@ } }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^7.5||9.5" } } diff --git a/src/JWK.php b/src/JWK.php index d5ad93a8..dbc446e6 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -128,24 +128,18 @@ private static function createPemFromModulusAndExponent( string $n, string $e ): string { - if (false === ($modulus = JWT::urlsafeB64Decode($n))) { - throw new UnexpectedValueException('Invalid JWK encoding'); - } - if (false === ($publicExponent = JWT::urlsafeB64Decode($e))) { - throw new UnexpectedValueException('Invalid header encoding'); - } + $mod = JWT::urlsafeB64Decode($n); + $exp = JWT::urlsafeB64Decode($e); - $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 +170,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 cf58fd2a..f6a4772c 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -36,13 +36,15 @@ class JWT * When checking nbf, iat or expiration times, * we want to provide some extra leeway time to * account for clock skew. + * + * @var int */ - public static int $leeway = 0; + public static $leeway = 0; /** * @var array */ - public static array $supported_algs = [ + public static $supported_algs = [ 'ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], 'HS256' => ['hash_hmac', 'SHA256'], @@ -77,8 +79,10 @@ class JWT * @uses jsonDecode * @uses urlsafeB64Decode */ - public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray): stdClass - { + public static function decode( + string $jwt, + $keyOrKeyArray + ): stdClass { // Validate JWT $timestamp = \time(); @@ -90,24 +94,18 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) 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'); } @@ -159,7 +157,7 @@ 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|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 * @@ -170,7 +168,7 @@ public static function decode(string $jwt, Key|array|ArrayAccess $keyOrKeyArray) */ public static function encode( array $payload, - string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key, + $key, string $alg, string $keyId = null, array $head = null @@ -197,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' * @@ -207,7 +205,7 @@ public static function encode( */ public static function sign( string $msg, - string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $key, + $key, string $alg ): string { if (empty(static::$supported_algs[$alg])) { @@ -222,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"); } @@ -258,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 @@ -268,7 +266,7 @@ public static function sign( private static function verify( string $msg, string $signature, - string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial, + $keyMaterial, string $alg ): bool { if (empty(static::$supported_algs[$alg])) { @@ -278,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) { @@ -322,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); @@ -339,11 +337,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); @@ -356,6 +354,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; } @@ -365,8 +366,10 @@ 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) { @@ -399,8 +402,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( + $keyOrKeyArray, + ?string $kid + ): Key { if ($keyOrKeyArray instanceof Key) { return $keyOrKeyArray; } diff --git a/src/Key.php b/src/Key.php index 2f648dec..b09ad190 100644 --- a/src/Key.php +++ b/src/Key.php @@ -9,14 +9,28 @@ class Key { + /** @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|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial, - private string $algorithm + $keyMaterial, + string $algorithm ) { + 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'); + } + if (empty($keyMaterial)) { throw new InvalidArgumentException('Key material must not be empty'); } @@ -24,6 +38,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; } /** @@ -37,9 +55,9 @@ public function getAlgorithm(): string } /** - * @return string|OpenSSLAsymmetricKey|OpenSSLCertificate|array + * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */ - public function getKeyMaterial(): string|OpenSSLAsymmetricKey|OpenSSLCertificate|array + public function getKeyMaterial() { return $this->keyMaterial; }