Skip to content

Commit

Permalink
Add support for Ed25519, Ed448, X25519, and X448 key types.
Browse files Browse the repository at this point in the history
Implemented methods to handle new elliptic curve key types, enhancing the KeyConverter functionality. Updated tests to verify correct behavior and added necessary PHP extensions and version support in workflows. Minor composer and documentation adjustments were also made.
  • Loading branch information
Spomky committed Jan 3, 2025
1 parent 3479ed4 commit c9b85a1
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 27 deletions.
13 changes: 7 additions & 6 deletions .github/workflows/integrate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -70,6 +70,7 @@ jobs:
php-version:
- "8.2"
- "8.3"
- "8.4"
dependencies:
- "lowest"
- "highest"
Expand All @@ -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"

Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"

Expand Down
1 change: 1 addition & 0 deletions .gitsplit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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$
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 24 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5502,6 +5502,30 @@ parameters:
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
Expand Down
121 changes: 103 additions & 18 deletions src/Library/KeyManagement/KeyConverter/KeyConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ private static function loadKeyFromPEM(string $pem, ?string $password = null): a
return match ($details['type']) {
OPENSSL_KEYTYPE_EC => self::tryToLoadECKey($pem),
OPENSSL_KEYTYPE_RSA => RSAKey::createFromPEM($pem)->toArray(),
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($pem),
default => throw new InvalidArgumentException('Unsupported key type'),
};
Expand All @@ -255,8 +259,101 @@ private static function tryToLoadECKey(string $input): array
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<array-key, mixed>
*/
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['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<array-key, mixed>
*/
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['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<array-key, mixed>
*/
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['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<array-key, mixed>
*/
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['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.
*
* @return array<array-key, mixed>
*/
Expand Down Expand Up @@ -296,12 +393,16 @@ private static function loadPrivateKey(PEM $pem): array
case AlgorithmIdentifier::OID_X25519:
case AlgorithmIdentifier::OID_X448:
$curve = self::getCurve($key->algorithmIdentifier()->oid());
$x = self::getPublicKey($key, $curve);
$values = [
'kty' => 'OKP',
'crv' => $curve,
'd' => Base64UrlSafe::encodeUnpadded($key->privateKeyData()),
];
return self::populatePoints($key, $values);
if ($x !== null) {
$values['x'] = Base64UrlSafe::encodeUnpadded($x);
}
$values['d'] = Base64UrlSafe::encodeUnpadded($key->privateKeyData());
// no break
default:
throw new InvalidArgumentException('Unsupported key type');
}
Expand Down Expand Up @@ -338,22 +439,6 @@ private static function convertDecimalToBas64Url(string $decimal): string
return Base64UrlSafe::encodeUnpadded(BigInteger::fromBase($decimal, 10)->toBytes());
}

/**
* @param array<string, mixed> $values
* @return array<string, mixed>
*/
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) {
Expand Down
6 changes: 4 additions & 2 deletions tests/Component/KeyManagement/JWKFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],
];
Expand All @@ -290,6 +291,7 @@ public static function dataKeys(): iterable
'expectedValues' => [
'kty' => 'OKP',
'crv' => 'X448',
'x' => 'UoPD73NQACC8A-otDUVun4IrMsk775ShMRf4ThDrq4xY2eAI-pOIVujrvBXXd9g8gUNwBT0fmnc',
'd' => 'OHZK0Fp9MAAmk0yZekiAkB8qxpCVAF4dT2x_xmFNDdCTnyDvixaiZ0NSRpAdR59tA6OJmOFfbck',
],
];
Expand All @@ -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 [
Expand All @@ -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',
],
];
}
Expand Down

0 comments on commit c9b85a1

Please sign in to comment.