From 6d618806574c9abfe7613be3e67399b51abc7336 Mon Sep 17 00:00:00 2001 From: Eduardo Dobay Date: Sun, 3 Apr 2022 21:49:09 -0300 Subject: [PATCH 1/2] feat: add ed25519 support to JWK (public keys) Reference documentation: https://datatracker.ietf.org/doc/html/rfc8037 --- src/JWK.php | 34 ++++++++++++++++++++++++++++++---- src/JWT.php | 4 ++-- tests/JWKTest.php | 1 + tests/data/ed25519-jwkset.json | 10 ++++++++++ 4 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 tests/data/ed25519-jwkset.json diff --git a/src/JWK.php b/src/JWK.php index 67495e61..3bd1906e 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 = \base64_encode(JWT::urlsafeB64Decode($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 cf58fd2a..710acbfb 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -198,7 +198,7 @@ public static function encode( * * @param string $msg The message to sign * @param string|OpenSSLAsymmetricKey|OpenSSLCertificate|array $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 @@ -258,7 +258,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|OpenSSLAsymmetricKey|OpenSSLCertificate|array $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey * @param string $alg The algorithm * * @return bool 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" + } + ] +} From bd3bb474d483d28697e894fa3d3271e17c0cb964 Mon Sep 17 00:00:00 2001 From: Eduardo Dobay Date: Sun, 3 Apr 2022 22:28:28 -0300 Subject: [PATCH 2/2] refactor: Extract JWT::urlsafeToStandardB64 method --- src/JWK.php | 2 +- src/JWT.php | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/JWK.php b/src/JWK.php index 3bd1906e..67c95ae1 100644 --- a/src/JWK.php +++ b/src/JWK.php @@ -161,7 +161,7 @@ public static function parseKey(array $jwk): ?Key throw new UnexpectedValueException('x not set'); } - $publicKey = \base64_encode(JWT::urlsafeB64Decode($jwk['x'])); + $publicKey = JWT::urlsafeToStandardB64($jwk['x']); $alg = self::OKP_CURVES[$jwk['crv']]; return new Key($publicKey, $alg); default: diff --git a/src/JWT.php b/src/JWT.php index 710acbfb..4d7c3ce7 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -367,13 +367,26 @@ public static function jsonEncode(array $input): string|false * @return string A decoded string */ public static function urlsafeB64Decode(string $input): string|false + { + 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, '-_', '+/'); } /**