Skip to content

chore: add back compatibility for >= PHP 7.1 #405

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Mar 20, 2022
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -31,6 +31,6 @@
}
},
"require-dev": {
"phpunit/phpunit": "^9.5"
"phpunit/phpunit": "^7.5||9.5"
}
}
22 changes: 8 additions & 14 deletions src/JWK.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
59 changes: 32 additions & 27 deletions src/JWT.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string[]>
*/
public static array $supported_algs = [
public static $supported_algs = [
'ES384' => ['openssl', 'SHA384'],
'ES256' => ['openssl', 'SHA256'],
'HS256' => ['hash_hmac', 'SHA256'],
Expand Down Expand Up @@ -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();

Expand All @@ -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');
}
Expand Down Expand Up @@ -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<mixed> $payload PHP array
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array<mixed> $key The secret key.
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $keyId
* @param array<string, string> $head An array with header elements to attach
*
Expand All @@ -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
Expand All @@ -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<mixed> $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'
*
Expand All @@ -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])) {
Expand All @@ -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");
}
Expand Down Expand Up @@ -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<mixed> $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
Expand All @@ -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])) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);

Expand All @@ -339,11 +337,11 @@ public static function jsonDecode(string $input): mixed
*
* @param array<mixed> $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);
Expand All @@ -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;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down
28 changes: 23 additions & 5 deletions src/Key.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,39 @@

class Key
{
/** @var string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */
private $keyMaterial;
/** @var string */
private $algorithm;

/**
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array<mixed> $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');
}

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;
}

/**
Expand All @@ -37,9 +55,9 @@ public function getAlgorithm(): string
}

/**
* @return string|OpenSSLAsymmetricKey|OpenSSLCertificate|array<mixed>
* @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate
*/
public function getKeyMaterial(): string|OpenSSLAsymmetricKey|OpenSSLCertificate|array
public function getKeyMaterial()
{
return $this->keyMaterial;
}
Expand Down