diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index f74a0202..f3343958 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -30,7 +30,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" - extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid" + extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium" tools: castor - name: "Checkout code" @@ -70,6 +70,7 @@ jobs: php-version: - "8.2" - "8.3" + - "8.4" dependencies: - "lowest" - "highest" @@ -79,7 +80,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "${{ matrix.php-version }}" - extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid" + extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium" tools: castor coverage: "xdebug" @@ -106,7 +107,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" - extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid" + extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium" tools: castor - name: "Checkout code" @@ -132,7 +133,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" - extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid" + extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium" tools: castor - name: "Checkout code" @@ -161,7 +162,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" - extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid" + extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium" tools: castor - name: "Checkout code" @@ -187,7 +188,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" - extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid" + extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium" tools: castor coverage: "xdebug" diff --git a/.gitsplit.yml b/.gitsplit.yml index 463f40f2..6086733a 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -4,6 +4,7 @@ splits: - prefix: "src/Library" target: "https://${GH_TOKEN}@github.com/web-token/jwt-library.git" - prefix: "src/Experimental" + target: "https://${GH_TOKEN}@github.com/web-token/jwt-experimental.git" origins: - ^\d+\.\d+\.x$ diff --git a/composer.json b/composer.json index da19d6f8..cb64b3d6 100644 --- a/composer.json +++ b/composer.json @@ -80,7 +80,7 @@ "phpstan/phpstan-symfony": "^1.3|^2.0", "phpunit/phpunit": "^10.5.10|^11.0", "qossmic/deptrac": "^2.0", - "rector/rector": "^1.0|^2.0.0-rc3", + "rector/rector": "^1.0|^2.0", "roave/security-advisories": "dev-latest", "spomky-labs/aes-key-wrap": "^7.0", "staabm/phpstan-dba": "^0.2.79|^0.3", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 696019f3..4e0843a5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5502,6 +5502,42 @@ parameters: count: 1 path: src/Library/KeyManagement/KeyConverter/KeyConverter.php + - + message: '#^Parameter \#1 \$details of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:tryToLoadECKey\(\) expects array\{type\: int, key\: string\}, non\-empty\-array given\.$#' + identifier: argument.type + count: 1 + path: src/Library/KeyManagement/KeyConverter/KeyConverter.php + + - + message: '#^Parameter \#1 \$details of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:tryToLoadOtherKeyTypes\(\) expects array\{key\: string\}, non\-empty\-array given\.$#' + identifier: argument.type + count: 1 + path: src/Library/KeyManagement/KeyConverter/KeyConverter.php + + - + message: '#^Parameter \#1 \$input of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:tryToLoadED25519Key\(\) expects array\{bits\: int, type\: int, key\: string, ed25519\: array\{pub_key\?\: string, priv_key\?\: string\}\}, non\-empty\-array given\.$#' + identifier: argument.type + count: 1 + path: src/Library/KeyManagement/KeyConverter/KeyConverter.php + + - + message: '#^Parameter \#1 \$input of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:tryToLoadED448Key\(\) expects array\{bits\: int, type\: int, key\: string, ed448\: array\{pub_key\?\: string, priv_key\?\: string\}\}, non\-empty\-array given\.$#' + identifier: argument.type + count: 1 + path: src/Library/KeyManagement/KeyConverter/KeyConverter.php + + - + message: '#^Parameter \#1 \$input of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:tryToLoadX25519Key\(\) expects array\{bits\: int, type\: int, key\: string, x25519\: array\{pub_key\?\: string, priv_key\?\: string\}\}, non\-empty\-array given\.$#' + identifier: argument.type + count: 1 + path: src/Library/KeyManagement/KeyConverter/KeyConverter.php + + - + message: '#^Parameter \#1 \$input of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:tryToLoadX448Key\(\) expects array\{bits\: int, type\: int, key\: string, x448\: array\{pub_key\?\: string, priv_key\?\: string\}\}, non\-empty\-array given\.$#' + identifier: argument.type + count: 1 + path: src/Library/KeyManagement/KeyConverter/KeyConverter.php + - message: '#^Parameter \#1 \$pem of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:loadKeyFromPEM\(\) expects string, mixed given\.$#' identifier: argument.type diff --git a/src/Library/KeyManagement/KeyConverter/KeyConverter.php b/src/Library/KeyManagement/KeyConverter/KeyConverter.php index dc0d8020..7f950ad7 100644 --- a/src/Library/KeyManagement/KeyConverter/KeyConverter.php +++ b/src/Library/KeyManagement/KeyConverter/KeyConverter.php @@ -10,6 +10,8 @@ use OpenSSLCertificate; use ParagonIE\Sodium\Core\Ed25519; use RuntimeException; +use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence; +use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType; use SpomkyLabs\Pki\CryptoEncoding\PEM; use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\AlgorithmIdentifier; use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PrivateKey; @@ -228,9 +230,13 @@ private static function loadKeyFromPEM(string $pem, ?string $password = null): a } return match ($details['type']) { - OPENSSL_KEYTYPE_EC => self::tryToLoadECKey($pem), + OPENSSL_KEYTYPE_EC => self::tryToLoadECKey($details, $pem), OPENSSL_KEYTYPE_RSA => RSAKey::createFromPEM($pem)->toArray(), - -1 => self::tryToLoadOtherKeyTypes($pem), + 4 => self::tryToLoadX25519Key($details), // OPENSSL_KEYTYPE_X25519 + 5 => self::tryToLoadED25519Key($details), // OPENSSL_KEYTYPE_ED25519 + 6 => self::tryToLoadX448Key($details), // OPENSSL_KEYTYPE_X448 + 7 => self::tryToLoadED448Key($details), // OPENSSL_KEYTYPE_ED448 + -1 => self::tryToLoadOtherKeyTypes($details, $pem), default => throw new InvalidArgumentException('Unsupported key type'), }; } @@ -238,9 +244,11 @@ private static function loadKeyFromPEM(string $pem, ?string $password = null): a /** * This method tries to load Ed448, X488, Ed25519 and X25519 keys. * + * @param array{type: int, key: string} $details + * * @return array */ - private static function tryToLoadECKey(string $input): array + private static function tryToLoadECKey(array $details, string $input): array { try { return ECKey::createFromPEM($input)->toArray(); @@ -248,32 +256,129 @@ private static function tryToLoadECKey(string $input): array // no break } try { - return self::tryToLoadOtherKeyTypes($input); + return self::tryToLoadOtherKeyTypes($details, $input); } catch (Throwable) { // no break } throw new InvalidArgumentException('Unable to load the key.'); } + /** + * @param array{bits: int, type: int, key: string, x25519: array{pub_key?: string, priv_key?: string}} $input + * + * @return array + */ + private static function tryToLoadX25519Key(array $input): array + { + $values = [ + 'kty' => 'OKP', + 'crv' => 'X25519', + ]; + if (array_key_exists('pub_key', $input['x25519'])) { + $values['x'] = Base64UrlSafe::encodeUnpadded($input['x25519']['pub_key']); + } else { + $values['x'] = self::tryToLoadOtherKeyTypes($input, $input['key'])['x']; + } + if (array_key_exists('priv_key', $input['x25519'])) { + $values['d'] = Base64UrlSafe::encodeUnpadded($input['x25519']['priv_key']); + } + + return $values; + } + + /** + * @param array{bits: int, type: int, key: string, ed25519: array{pub_key?: string, priv_key?: string}} $input + * + * @return array + */ + private static function tryToLoadED25519Key(array $input): array + { + $values = [ + 'kty' => 'OKP', + 'crv' => 'Ed25519', + ]; + if (array_key_exists('pub_key', $input['ed25519'])) { + $values['x'] = Base64UrlSafe::encodeUnpadded($input['ed25519']['pub_key']); + } else { + $values['x'] = self::tryToLoadOtherKeyTypes($input, $input['key'])['x']; + } + if (array_key_exists('priv_key', $input['ed25519'])) { + $values['d'] = Base64UrlSafe::encodeUnpadded($input['ed25519']['priv_key']); + } + + return $values; + } + + /** + * @param array{bits: int, type: int, key: string, x448: array{pub_key?: string, priv_key?: string}} $input + * + * @return array + */ + private static function tryToLoadX448Key(array $input): array + { + $values = [ + 'kty' => 'OKP', + 'crv' => 'X448', + ]; + if (array_key_exists('pub_key', $input['x448'])) { + $values['x'] = Base64UrlSafe::encodeUnpadded($input['x448']['pub_key']); + } else { + $values['x'] = self::tryToLoadOtherKeyTypes($input, $input['key'])['x']; + } + if (array_key_exists('priv_key', $input['x448'])) { + $values['d'] = Base64UrlSafe::encodeUnpadded($input['x448']['priv_key']); + } + + return $values; + } + + /** + * @param array{bits: int, type: int, key: string, ed448: array{pub_key?: string, priv_key?: string}} $input + * + * @return array + */ + private static function tryToLoadED448Key(array $input): array + { + $values = [ + 'kty' => 'OKP', + 'crv' => 'Ed448', + ]; + if (array_key_exists('pub_key', $input['ed448'])) { + $values['x'] = Base64UrlSafe::encodeUnpadded($input['ed448']['pub_key']); + } else { + $values['x'] = self::tryToLoadOtherKeyTypes($input, $input['key'])['x']; + } + if (array_key_exists('priv_key', $input['ed448'])) { + $values['d'] = Base64UrlSafe::encodeUnpadded($input['ed448']['priv_key']); + } + + return $values; + } + /** * This method tries to load Ed448, X488, Ed25519 and X25519 keys. + * Only needed on PHP8.3 and earlier. + * + * @param array{key: string} $details * * @return array */ - private static function tryToLoadOtherKeyTypes(string $input): array + private static function tryToLoadOtherKeyTypes(array $details, string $input): array { $pem = PEM::fromString($input); return match ($pem->type()) { PEM::TYPE_PUBLIC_KEY => self::loadPublicKey($pem), - PEM::TYPE_PRIVATE_KEY => self::loadPrivateKey($pem), + PEM::TYPE_PRIVATE_KEY => self::loadPrivateKey($details, $pem), default => throw new InvalidArgumentException('Unsupported key type'), }; } /** + * @param array{key: string} $details + * * @return array */ - private static function loadPrivateKey(PEM $pem): array + private static function loadPrivateKey(array $details, PEM $pem): array { try { $key = PrivateKey::fromPEM($pem); @@ -296,12 +401,15 @@ private static function loadPrivateKey(PEM $pem): array case AlgorithmIdentifier::OID_X25519: case AlgorithmIdentifier::OID_X448: $curve = self::getCurve($key->algorithmIdentifier()->oid()); - $values = [ + $publicKey = PEM::fromString($details['key']); + /** @var UnspecifiedType $publicKeyBits */ + $publicKeyBits = Sequence::fromDER($publicKey->data())->at(1); + return [ 'kty' => 'OKP', 'crv' => $curve, + 'x' => Base64UrlSafe::encodeUnpadded($publicKeyBits->asBitString()->string()), 'd' => Base64UrlSafe::encodeUnpadded($key->privateKeyData()), ]; - return self::populatePoints($key, $values); default: throw new InvalidArgumentException('Unsupported key type'); } @@ -338,37 +446,6 @@ private static function convertDecimalToBas64Url(string $decimal): string return Base64UrlSafe::encodeUnpadded(BigInteger::fromBase($decimal, 10)->toBytes()); } - /** - * @param array $values - * @return array - */ - private static function populatePoints(PrivateKey $key, array $values): array - { - $crv = $values['crv'] ?? null; - assert(is_string($crv), 'Unsupported key type.'); - $x = self::getPublicKey($key, $crv); - if ($x !== null) { - $values['x'] = Base64UrlSafe::encodeUnpadded($x); - } - - return $values; - } - - private static function getPublicKey(PrivateKey $key, string $crv): ?string - { - switch ($crv) { - case 'Ed25519': - return Ed25519::publickey_from_secretkey($key->privateKeyData()); - case 'X25519': - if (extension_loaded('sodium')) { - return sodium_crypto_scalarmult_base($key->privateKeyData()); - } - // no break - default: - return null; - } - } - private static function checkType(string $curve): void { $curves = ['Ed448ph', 'Ed25519ph', 'Ed448', 'Ed25519', 'X448', 'X25519']; diff --git a/tests/Component/KeyManagement/JWKFactoryTest.php b/tests/Component/KeyManagement/JWKFactoryTest.php index c46c8395..26b9b512 100644 --- a/tests/Component/KeyManagement/JWKFactoryTest.php +++ b/tests/Component/KeyManagement/JWKFactoryTest.php @@ -282,6 +282,7 @@ public static function dataKeys(): iterable 'expectedValues' => [ 'kty' => 'OKP', 'crv' => 'Ed448', + 'x' => 'wwHKDV7s4fBhmFSTzYorlaToGXNcsa7SakZdekT_sexD5ENj5lWP6_KX9_u--w_QSm80rNOodj0A', 'd' => '0GXSbNLOh7NQBlwoF8y2WJmjeP5Puif4_JL4ihFUzRLrb_3r4cH8l_HWJA-2ffY62LEB_ozsehG5', ], ]; @@ -290,6 +291,7 @@ public static function dataKeys(): iterable 'expectedValues' => [ 'kty' => 'OKP', 'crv' => 'X448', + 'x' => 'UoPD73NQACC8A-otDUVun4IrMsk775ShMRf4ThDrq4xY2eAI-pOIVujrvBXXd9g8gUNwBT0fmnc', 'd' => 'OHZK0Fp9MAAmk0yZekiAkB8qxpCVAF4dT2x_xmFNDdCTnyDvixaiZ0NSRpAdR59tA6OJmOFfbck', ], ]; @@ -298,8 +300,8 @@ public static function dataKeys(): iterable 'expectedValues' => [ 'kty' => 'OKP', 'crv' => 'Ed25519', - 'd' => 'Pr9AxZivB-zSq95wLrZfYa7DQ3TUPqZTkP_0w33r3rc', 'x' => 'wrI33AEj15KHHYplueUE5cnJKtbM8oVHFf6wGnw2oOE', + 'd' => 'Pr9AxZivB-zSq95wLrZfYa7DQ3TUPqZTkP_0w33r3rc', ], ]; yield [ @@ -317,8 +319,8 @@ public static function dataKeys(): iterable 'expectedValues' => [ 'kty' => 'OKP', 'crv' => 'X25519', - 'd' => 'mG-fgDwkr58hwIeqCQKZbR8HKeY4yg_AzvU6zyNaVUE', 'x' => '3OJLiffmOCQGtil23QGyn0nk9EBKoZx6P-6o-EnsBB4', + 'd' => 'mG-fgDwkr58hwIeqCQKZbR8HKeY4yg_AzvU6zyNaVUE', ], ]; }