diff --git a/src/JWT.php b/src/JWT.php index 3ead151b..c18e4cc0 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -270,6 +270,8 @@ public static function sign( } if (str_starts_with($alg, 'RS')) { self::validateRsaKeyLength($key); + } elseif (str_starts_with($alg, 'ES')) { + self::validateEcKeyLength($key, $alg); } $success = \openssl_sign($msg, $signature, $key, $algorithm); if (!$success) { @@ -330,11 +332,13 @@ private static function verify( list($function, $algorithm) = static::$supported_algs[$alg]; switch ($function) { case 'openssl': - if (str_starts_with($algorithm, 'RS')) { - if (!$key = openssl_pkey_get_private($keyMaterial)) { - throw new DomainException('OpenSSL unable to validate key'); - } + if (!$key = openssl_pkey_get_public($keyMaterial)) { + throw new DomainException('OpenSSL unable to validate key'); + } + if (str_starts_with($alg, 'RS')) { self::validateRsaKeyLength($key); + } elseif (str_starts_with($alg, 'ES')) { + self::validateEcKeyLength($key, $alg); } $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); if ($success === 1) { @@ -721,4 +725,24 @@ private static function validateRsaKeyLength(#[\SensitiveParameter] OpenSSLAsymm throw new DomainException('Provided key is too short'); } } + + /** + * Validate RSA key length + * + * @param OpenSSLAsymmetricKey $key RSA key material + * @param string $algorithm The algorithm + * @throws DomainException Provided key is too short + */ + private static function validateEcKeyLength( + #[\SensitiveParameter] OpenSSLAsymmetricKey $key, + string $algorithm + ): void { + if (!$keyDetails = openssl_pkey_get_details($key)) { + throw new DomainException('Unable to validate key'); + } + $minKeyLength = (int) \str_replace('ES', '', $algorithm); + if ($keyDetails['bits'] < $minKeyLength) { + throw new DomainException('Provided key is too short'); + } + } } diff --git a/tests/JWTTest.php b/tests/JWTTest.php index d920a851..a1dd08a4 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -678,6 +678,65 @@ public function provideHmac() ]; } + /** @dataProvider provideEcKeyInvalidLength */ + public function testEcKeyLengthValidationThrowsExceptionEncode(string $keyFile, string $alg): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Provided key is too short'); + + $tooShortEcKey = file_get_contents(__DIR__ . '/data/' . $keyFile); + $payload = ['message' => 'abc']; + + JWT::encode($payload, $tooShortEcKey, $alg); + } + + public function testEcKeyLengthValidationThrowsExceptionDecode(): void + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Provided key is too short'); + + $payload = ['message' => 'abc']; + + $validEcKeyBytes = file_get_contents(__DIR__ . '/data/ecdsa384-private.pem'); + $encoded = JWT::encode($payload, $validEcKeyBytes, 'ES256'); + + $tooShortEcKey = file_get_contents(__DIR__ . '/data/ecdsa192-public.pem'); + JWT::decode($encoded, new Key($tooShortEcKey, 'ES256')); + } + + /** @dataProvider provideEcKey */ + public function testEcKeyLengthValidationPassesWithCorrectLength( + string $privateKeyFile, + string $publicKeyFile, + string $alg + ): void { + $payload = ['message' => 'test hmac length']; + + // Test with a key that is the required length + $privateKeyBytes = file_get_contents(__DIR__ . '/data/' . $privateKeyFile); + $encoded48 = JWT::encode($payload, $privateKeyBytes, $alg); + + $publicKeyBytes = file_get_contents(__DIR__ . '/data/' . $publicKeyFile); + $decoded48 = JWT::decode($encoded48, new Key($publicKeyBytes, $alg)); + $this->assertEquals($payload['message'], $decoded48->message); + } + + public function provideEcKeyInvalidLength() + { + return [ + ['ecdsa192-private.pem', 'ES256'], + ['ecdsa-private.pem', 'ES384'], + ]; + } + + public function provideEcKey() + { + return [ + ['ecdsa-private.pem', 'ecdsa-public.pem', 'ES256'], + ['ecdsa384-private.pem', 'ecdsa384-public.pem', 'ES384'], + ]; + } + private function generateHmac256(): Key { return new Key(random_bytes(32), 'HS256'); diff --git a/tests/data/ecdsa192-private.pem b/tests/data/ecdsa192-private.pem new file mode 100644 index 00000000..4d9bc0b1 --- /dev/null +++ b/tests/data/ecdsa192-private.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MF8CAQEEGPRkK7lK/9FuZ3BE8ZX+dlHavL22Q9CN2KAKBggqhkjOPQMBAaE0AzIA +BL4pM50YcLq/I9Y8T+C+fwoOtwRW8zdV6yQmG9fD8zWaAs28+UxHeK8VD7THatbp +wg== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/tests/data/ecdsa192-public.pem b/tests/data/ecdsa192-public.pem new file mode 100644 index 00000000..d4aeee1d --- /dev/null +++ b/tests/data/ecdsa192-public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEvikznRhwur8j1jxP4L5/Cg63BFbz +N1XrJCYb18PzNZoCzbz5TEd4rxUPtMdq1unC +-----END PUBLIC KEY-----