diff --git a/src/JWK.php b/src/JWK.php index fab13412..d0ae5630 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -31,6 +31,11 @@ class JWK // 'P-521' => '1.3.132.0.35', // Len: 132 (not supported) ]; + // 'crv' identifier => JWT 'alg' + private const OKP_CURVES = [ + 'Ed25519' => 'EdDSA', + ]; + /** * Parse a set of JWK keys * @@ -93,9 +98,10 @@ public static function parseKey(array $jwk): ?Key throw new UnexpectedValueException('JWK must contain a "kty" parameter'); } - if (!isset($jwk['alg'])) { - // The "alg" parameter is optional in a KTY, but is required for parsing in - // this library. Add it manually to your JWK array if it doesn't already exist. + $ktyRequiringAlg = ['RSA', 'EC']; + if (!isset($jwk['alg']) && \in_array($jwk['kty'], $ktyRequiringAlg, true)) { + // The "alg" parameter is optional in a KTY, but is required for parsing certain key + // types in this library. Add it manually to your JWK array if it doesn't already exist. // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 throw new UnexpectedValueException('JWK must contain an "alg" parameter'); } @@ -137,8 +143,28 @@ public static function parseKey(array $jwk): ?Key $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); return new Key($publicKey, $jwk['alg']); + case 'OKP': + if (isset($jwk['d'])) { + // The key is actually a private key + throw new UnexpectedValueException('Key data must be for a public key'); + } + + if (! isset($jwk['crv'])) { + throw new UnexpectedValueException('crv not set'); + } + + if (!isset(self::OKP_CURVES[$jwk['crv']])) { + throw new DomainException('Unrecognised or unsupported OKP curve'); + } + + if (empty($jwk['x'])) { + throw new UnexpectedValueException('x not set'); + } + + $publicKey = JWT::urlsafeToStandardB64($jwk['x']); + $alg = self::OKP_CURVES[$jwk['crv']]; + return new Key($publicKey, $alg); default: - // Currently only RSA is supported break; } diff --git a/src/JWT.php b/src/JWT.php index bf064e34..137e8de1 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -207,7 +207,7 @@ public static function encode( * * @param string $msg The message to sign * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. - * @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', + * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'HS256', 'HS384', * 'HS512', 'RS256', 'RS384', and 'RS512' * * @return string An encrypted message @@ -267,7 +267,7 @@ public static function sign( * * @param string $msg The original message (header and body) * @param string $signature The original signature - * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey * @param string $alg The algorithm * * @return bool @@ -381,13 +381,26 @@ public static function jsonEncode(array $input): string * @throws InvalidArgumentException invalid base64 characters */ public static function urlsafeB64Decode(string $input): string + { + return \base64_decode(self::urlsafeToStandardB64($input)); + } + + /** + * Convert a string from URL-safe Base64 to standard Base64. + * + * @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding) + * + * @return string A Base64 encoded string with standard characters (+/) and padding (=), when + * needed. + */ + public static function urlsafeToStandardB64(string $input): string { $remainder = \strlen($input) % 4; if ($remainder) { $padlen = 4 - $remainder; $input .= \str_repeat('=', $padlen); } - return \base64_decode(\strtr($input, '-_', '+/')); + return \strtr($input, '-_', '+/'); } /** diff --git a/tests/JWKTest.php b/tests/JWKTest.php index 4baefe8a..bbdd7e4c 100644 --- a/tests/JWKTest.php +++ b/tests/JWKTest.php @@ -139,6 +139,7 @@ public function provideDecodeByJwkKeySet() return [ ['rsa1-private.pem', 'rsa-jwkset.json', 'RS256'], ['ecdsa256-private.pem', 'ec-jwkset.json', 'ES256'], + ['ed25519-1.sec', 'ed25519-jwkset.json', 'EdDSA'], ]; } diff --git a/tests/data/ed25519-jwkset.json b/tests/data/ed25519-jwkset.json new file mode 100644 index 00000000..a364af80 --- /dev/null +++ b/tests/data/ed25519-jwkset.json @@ -0,0 +1,10 @@ +{ + "keys": [ + { + "kid": "jwk1", + "kty": "OKP", + "crv": "Ed25519", + "x": "uOSJMhbKSG4V5xUHS7B9YHmVg_1yVd-G-Io6oBFhSfY" + } + ] +}