Skip to content

Commit cfd4ca9

Browse files
authored
Add support for Ed25519, Ed448, X25519, and X448 key types. (#599)
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.
1 parent 3479ed4 commit cfd4ca9

File tree

6 files changed

+166
-49
lines changed

6 files changed

+166
-49
lines changed

Diff for: .github/workflows/integrate.yml

+7-6
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
uses: "shivammathur/setup-php@v2"
3131
with:
3232
php-version: "8.3"
33-
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid"
33+
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium"
3434
tools: castor
3535

3636
- name: "Checkout code"
@@ -70,6 +70,7 @@ jobs:
7070
php-version:
7171
- "8.2"
7272
- "8.3"
73+
- "8.4"
7374
dependencies:
7475
- "lowest"
7576
- "highest"
@@ -79,7 +80,7 @@ jobs:
7980
uses: "shivammathur/setup-php@v2"
8081
with:
8182
php-version: "${{ matrix.php-version }}"
82-
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid"
83+
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium"
8384
tools: castor
8485
coverage: "xdebug"
8586

@@ -106,7 +107,7 @@ jobs:
106107
uses: "shivammathur/setup-php@v2"
107108
with:
108109
php-version: "8.3"
109-
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid"
110+
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium"
110111
tools: castor
111112

112113
- name: "Checkout code"
@@ -132,7 +133,7 @@ jobs:
132133
uses: "shivammathur/setup-php@v2"
133134
with:
134135
php-version: "8.3"
135-
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid"
136+
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium"
136137
tools: castor
137138

138139
- name: "Checkout code"
@@ -161,7 +162,7 @@ jobs:
161162
uses: "shivammathur/setup-php@v2"
162163
with:
163164
php-version: "8.3"
164-
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid"
165+
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium"
165166
tools: castor
166167

167168
- name: "Checkout code"
@@ -187,7 +188,7 @@ jobs:
187188
uses: "shivammathur/setup-php@v2"
188189
with:
189190
php-version: "8.3"
190-
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid"
191+
extensions: "gmp, json, mbstring, openssl, sqlite3, curl, uuid, sodium"
191192
tools: castor
192193
coverage: "xdebug"
193194

Diff for: .gitsplit.yml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ splits:
44
- prefix: "src/Library"
55
target: "https://${GH_TOKEN}@github.com./web-token/jwt-library.git"
66
- prefix: "src/Experimental"
7+
target: "https://${GH_TOKEN}@github.com./web-token/jwt-experimental.git"
78

89
origins:
910
- ^\d+\.\d+\.x$

Diff for: composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"phpstan/phpstan-symfony": "^1.3|^2.0",
8181
"phpunit/phpunit": "^10.5.10|^11.0",
8282
"qossmic/deptrac": "^2.0",
83-
"rector/rector": "^1.0|^2.0.0-rc3",
83+
"rector/rector": "^1.0|^2.0",
8484
"roave/security-advisories": "dev-latest",
8585
"spomky-labs/aes-key-wrap": "^7.0",
8686
"staabm/phpstan-dba": "^0.2.79|^0.3",

Diff for: phpstan-baseline.neon

+36
Original file line numberDiff line numberDiff line change
@@ -5502,6 +5502,42 @@ parameters:
55025502
count: 1
55035503
path: src/Library/KeyManagement/KeyConverter/KeyConverter.php
55045504

5505+
-
5506+
message: '#^Parameter \#1 \$details of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:tryToLoadECKey\(\) expects array\{type\: int, key\: string\}, non\-empty\-array given\.$#'
5507+
identifier: argument.type
5508+
count: 1
5509+
path: src/Library/KeyManagement/KeyConverter/KeyConverter.php
5510+
5511+
-
5512+
message: '#^Parameter \#1 \$details of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:tryToLoadOtherKeyTypes\(\) expects array\{key\: string\}, non\-empty\-array given\.$#'
5513+
identifier: argument.type
5514+
count: 1
5515+
path: src/Library/KeyManagement/KeyConverter/KeyConverter.php
5516+
5517+
-
5518+
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\.$#'
5519+
identifier: argument.type
5520+
count: 1
5521+
path: src/Library/KeyManagement/KeyConverter/KeyConverter.php
5522+
5523+
-
5524+
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\.$#'
5525+
identifier: argument.type
5526+
count: 1
5527+
path: src/Library/KeyManagement/KeyConverter/KeyConverter.php
5528+
5529+
-
5530+
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\.$#'
5531+
identifier: argument.type
5532+
count: 1
5533+
path: src/Library/KeyManagement/KeyConverter/KeyConverter.php
5534+
5535+
-
5536+
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\.$#'
5537+
identifier: argument.type
5538+
count: 1
5539+
path: src/Library/KeyManagement/KeyConverter/KeyConverter.php
5540+
55055541
-
55065542
message: '#^Parameter \#1 \$pem of static method Jose\\Component\\KeyManagement\\KeyConverter\\KeyConverter\:\:loadKeyFromPEM\(\) expects string, mixed given\.$#'
55075543
identifier: argument.type

Diff for: src/Library/KeyManagement/KeyConverter/KeyConverter.php

+117-40
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use OpenSSLCertificate;
1111
use ParagonIE\Sodium\Core\Ed25519;
1212
use RuntimeException;
13+
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
14+
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
1315
use SpomkyLabs\Pki\CryptoEncoding\PEM;
1416
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\AlgorithmIdentifier;
1517
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\PrivateKey;
@@ -228,52 +230,155 @@ private static function loadKeyFromPEM(string $pem, ?string $password = null): a
228230
}
229231

230232
return match ($details['type']) {
231-
OPENSSL_KEYTYPE_EC => self::tryToLoadECKey($pem),
233+
OPENSSL_KEYTYPE_EC => self::tryToLoadECKey($details, $pem),
232234
OPENSSL_KEYTYPE_RSA => RSAKey::createFromPEM($pem)->toArray(),
233-
-1 => self::tryToLoadOtherKeyTypes($pem),
235+
4 => self::tryToLoadX25519Key($details), // OPENSSL_KEYTYPE_X25519
236+
5 => self::tryToLoadED25519Key($details), // OPENSSL_KEYTYPE_ED25519
237+
6 => self::tryToLoadX448Key($details), // OPENSSL_KEYTYPE_X448
238+
7 => self::tryToLoadED448Key($details), // OPENSSL_KEYTYPE_ED448
239+
-1 => self::tryToLoadOtherKeyTypes($details, $pem),
234240
default => throw new InvalidArgumentException('Unsupported key type'),
235241
};
236242
}
237243

238244
/**
239245
* This method tries to load Ed448, X488, Ed25519 and X25519 keys.
240246
*
247+
* @param array{type: int, key: string} $details
248+
*
241249
* @return array<array-key, mixed>
242250
*/
243-
private static function tryToLoadECKey(string $input): array
251+
private static function tryToLoadECKey(array $details, string $input): array
244252
{
245253
try {
246254
return ECKey::createFromPEM($input)->toArray();
247255
} catch (Throwable) {
248256
// no break
249257
}
250258
try {
251-
return self::tryToLoadOtherKeyTypes($input);
259+
return self::tryToLoadOtherKeyTypes($details, $input);
252260
} catch (Throwable) {
253261
// no break
254262
}
255263
throw new InvalidArgumentException('Unable to load the key.');
256264
}
257265

266+
/**
267+
* @param array{bits: int, type: int, key: string, x25519: array{pub_key?: string, priv_key?: string}} $input
268+
*
269+
* @return array<array-key, mixed>
270+
*/
271+
private static function tryToLoadX25519Key(array $input): array
272+
{
273+
$values = [
274+
'kty' => 'OKP',
275+
'crv' => 'X25519',
276+
];
277+
if (array_key_exists('pub_key', $input['x25519'])) {
278+
$values['x'] = Base64UrlSafe::encodeUnpadded($input['x25519']['pub_key']);
279+
} else {
280+
$values['x'] = self::tryToLoadOtherKeyTypes($input, $input['key'])['x'];
281+
}
282+
if (array_key_exists('priv_key', $input['x25519'])) {
283+
$values['d'] = Base64UrlSafe::encodeUnpadded($input['x25519']['priv_key']);
284+
}
285+
286+
return $values;
287+
}
288+
289+
/**
290+
* @param array{bits: int, type: int, key: string, ed25519: array{pub_key?: string, priv_key?: string}} $input
291+
*
292+
* @return array<array-key, mixed>
293+
*/
294+
private static function tryToLoadED25519Key(array $input): array
295+
{
296+
$values = [
297+
'kty' => 'OKP',
298+
'crv' => 'Ed25519',
299+
];
300+
if (array_key_exists('pub_key', $input['ed25519'])) {
301+
$values['x'] = Base64UrlSafe::encodeUnpadded($input['ed25519']['pub_key']);
302+
} else {
303+
$values['x'] = self::tryToLoadOtherKeyTypes($input, $input['key'])['x'];
304+
}
305+
if (array_key_exists('priv_key', $input['ed25519'])) {
306+
$values['d'] = Base64UrlSafe::encodeUnpadded($input['ed25519']['priv_key']);
307+
}
308+
309+
return $values;
310+
}
311+
312+
/**
313+
* @param array{bits: int, type: int, key: string, x448: array{pub_key?: string, priv_key?: string}} $input
314+
*
315+
* @return array<array-key, mixed>
316+
*/
317+
private static function tryToLoadX448Key(array $input): array
318+
{
319+
$values = [
320+
'kty' => 'OKP',
321+
'crv' => 'X448',
322+
];
323+
if (array_key_exists('pub_key', $input['x448'])) {
324+
$values['x'] = Base64UrlSafe::encodeUnpadded($input['x448']['pub_key']);
325+
} else {
326+
$values['x'] = self::tryToLoadOtherKeyTypes($input, $input['key'])['x'];
327+
}
328+
if (array_key_exists('priv_key', $input['x448'])) {
329+
$values['d'] = Base64UrlSafe::encodeUnpadded($input['x448']['priv_key']);
330+
}
331+
332+
return $values;
333+
}
334+
335+
/**
336+
* @param array{bits: int, type: int, key: string, ed448: array{pub_key?: string, priv_key?: string}} $input
337+
*
338+
* @return array<array-key, mixed>
339+
*/
340+
private static function tryToLoadED448Key(array $input): array
341+
{
342+
$values = [
343+
'kty' => 'OKP',
344+
'crv' => 'Ed448',
345+
];
346+
if (array_key_exists('pub_key', $input['ed448'])) {
347+
$values['x'] = Base64UrlSafe::encodeUnpadded($input['ed448']['pub_key']);
348+
} else {
349+
$values['x'] = self::tryToLoadOtherKeyTypes($input, $input['key'])['x'];
350+
}
351+
if (array_key_exists('priv_key', $input['ed448'])) {
352+
$values['d'] = Base64UrlSafe::encodeUnpadded($input['ed448']['priv_key']);
353+
}
354+
355+
return $values;
356+
}
357+
258358
/**
259359
* This method tries to load Ed448, X488, Ed25519 and X25519 keys.
360+
* Only needed on PHP8.3 and earlier.
361+
*
362+
* @param array{key: string} $details
260363
*
261364
* @return array<array-key, mixed>
262365
*/
263-
private static function tryToLoadOtherKeyTypes(string $input): array
366+
private static function tryToLoadOtherKeyTypes(array $details, string $input): array
264367
{
265368
$pem = PEM::fromString($input);
266369
return match ($pem->type()) {
267370
PEM::TYPE_PUBLIC_KEY => self::loadPublicKey($pem),
268-
PEM::TYPE_PRIVATE_KEY => self::loadPrivateKey($pem),
371+
PEM::TYPE_PRIVATE_KEY => self::loadPrivateKey($details, $pem),
269372
default => throw new InvalidArgumentException('Unsupported key type'),
270373
};
271374
}
272375

273376
/**
377+
* @param array{key: string} $details
378+
*
274379
* @return array<string, mixed>
275380
*/
276-
private static function loadPrivateKey(PEM $pem): array
381+
private static function loadPrivateKey(array $details, PEM $pem): array
277382
{
278383
try {
279384
$key = PrivateKey::fromPEM($pem);
@@ -296,12 +401,15 @@ private static function loadPrivateKey(PEM $pem): array
296401
case AlgorithmIdentifier::OID_X25519:
297402
case AlgorithmIdentifier::OID_X448:
298403
$curve = self::getCurve($key->algorithmIdentifier()->oid());
299-
$values = [
404+
$publicKey = PEM::fromString($details['key']);
405+
/** @var UnspecifiedType $publicKeyBits */
406+
$publicKeyBits = Sequence::fromDER($publicKey->data())->at(1);
407+
return [
300408
'kty' => 'OKP',
301409
'crv' => $curve,
410+
'x' => Base64UrlSafe::encodeUnpadded($publicKeyBits->asBitString()->string()),
302411
'd' => Base64UrlSafe::encodeUnpadded($key->privateKeyData()),
303412
];
304-
return self::populatePoints($key, $values);
305413
default:
306414
throw new InvalidArgumentException('Unsupported key type');
307415
}
@@ -338,37 +446,6 @@ private static function convertDecimalToBas64Url(string $decimal): string
338446
return Base64UrlSafe::encodeUnpadded(BigInteger::fromBase($decimal, 10)->toBytes());
339447
}
340448

341-
/**
342-
* @param array<string, mixed> $values
343-
* @return array<string, mixed>
344-
*/
345-
private static function populatePoints(PrivateKey $key, array $values): array
346-
{
347-
$crv = $values['crv'] ?? null;
348-
assert(is_string($crv), 'Unsupported key type.');
349-
$x = self::getPublicKey($key, $crv);
350-
if ($x !== null) {
351-
$values['x'] = Base64UrlSafe::encodeUnpadded($x);
352-
}
353-
354-
return $values;
355-
}
356-
357-
private static function getPublicKey(PrivateKey $key, string $crv): ?string
358-
{
359-
switch ($crv) {
360-
case 'Ed25519':
361-
return Ed25519::publickey_from_secretkey($key->privateKeyData());
362-
case 'X25519':
363-
if (extension_loaded('sodium')) {
364-
return sodium_crypto_scalarmult_base($key->privateKeyData());
365-
}
366-
// no break
367-
default:
368-
return null;
369-
}
370-
}
371-
372449
private static function checkType(string $curve): void
373450
{
374451
$curves = ['Ed448ph', 'Ed25519ph', 'Ed448', 'Ed25519', 'X448', 'X25519'];

Diff for: tests/Component/KeyManagement/JWKFactoryTest.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ public static function dataKeys(): iterable
282282
'expectedValues' => [
283283
'kty' => 'OKP',
284284
'crv' => 'Ed448',
285+
'x' => 'wwHKDV7s4fBhmFSTzYorlaToGXNcsa7SakZdekT_sexD5ENj5lWP6_KX9_u--w_QSm80rNOodj0A',
285286
'd' => '0GXSbNLOh7NQBlwoF8y2WJmjeP5Puif4_JL4ihFUzRLrb_3r4cH8l_HWJA-2ffY62LEB_ozsehG5',
286287
],
287288
];
@@ -290,6 +291,7 @@ public static function dataKeys(): iterable
290291
'expectedValues' => [
291292
'kty' => 'OKP',
292293
'crv' => 'X448',
294+
'x' => 'UoPD73NQACC8A-otDUVun4IrMsk775ShMRf4ThDrq4xY2eAI-pOIVujrvBXXd9g8gUNwBT0fmnc',
293295
'd' => 'OHZK0Fp9MAAmk0yZekiAkB8qxpCVAF4dT2x_xmFNDdCTnyDvixaiZ0NSRpAdR59tA6OJmOFfbck',
294296
],
295297
];
@@ -298,8 +300,8 @@ public static function dataKeys(): iterable
298300
'expectedValues' => [
299301
'kty' => 'OKP',
300302
'crv' => 'Ed25519',
301-
'd' => 'Pr9AxZivB-zSq95wLrZfYa7DQ3TUPqZTkP_0w33r3rc',
302303
'x' => 'wrI33AEj15KHHYplueUE5cnJKtbM8oVHFf6wGnw2oOE',
304+
'd' => 'Pr9AxZivB-zSq95wLrZfYa7DQ3TUPqZTkP_0w33r3rc',
303305
],
304306
];
305307
yield [
@@ -317,8 +319,8 @@ public static function dataKeys(): iterable
317319
'expectedValues' => [
318320
'kty' => 'OKP',
319321
'crv' => 'X25519',
320-
'd' => 'mG-fgDwkr58hwIeqCQKZbR8HKeY4yg_AzvU6zyNaVUE',
321322
'x' => '3OJLiffmOCQGtil23QGyn0nk9EBKoZx6P-6o-EnsBB4',
323+
'd' => 'mG-fgDwkr58hwIeqCQKZbR8HKeY4yg_AzvU6zyNaVUE',
322324
],
323325
];
324326
}

0 commit comments

Comments
 (0)