Skip to content

Commit

Permalink
Add support for Ed25519, Ed448, X25519, and X448 key types. (#599)
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 authored Jan 3, 2025
1 parent 3479ed4 commit cfd4ca9
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 49 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
36 changes: 36 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
157 changes: 117 additions & 40 deletions src/Library/KeyManagement/KeyConverter/KeyConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -228,52 +230,155 @@ 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'),
};
}

/**
* This method tries to load Ed448, X488, Ed25519 and X25519 keys.
*
* @param array{type: int, key: string} $details
*
* @return array<array-key, mixed>
*/
private static function tryToLoadECKey(string $input): array
private static function tryToLoadECKey(array $details, string $input): array
{
try {
return ECKey::createFromPEM($input)->toArray();
} catch (Throwable) {
// 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<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, $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, $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, $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, $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<array-key, mixed>
*/
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<string, mixed>
*/
private static function loadPrivateKey(PEM $pem): array
private static function loadPrivateKey(array $details, PEM $pem): array
{
try {
$key = PrivateKey::fromPEM($pem);
Expand All @@ -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');
}
Expand Down Expand Up @@ -338,37 +446,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) {
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'];
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 cfd4ca9

Please sign in to comment.