Skip to content

Commit d2113d9

Browse files
authored
feat: add Ed25519 support to JWT (#343)
1 parent 44d0a5a commit d2113d9

File tree

8 files changed

+110
-11
lines changed

8 files changed

+110
-11
lines changed

.github/actions/entrypoint.sh

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ curl --silent --show-error https://getcomposer.org/installer | php
1212
php composer.phar self-update
1313

1414
echo "---Installing dependencies ---"
15-
php composer.phar update
15+
16+
# Add compatiblity for libsodium with older versions of PHP
17+
php composer.phar require --dev --with-dependencies paragonie/sodium_compat
1618

1719
echo "---Running unit tests ---"
1820
vendor/bin/phpunit

.github/workflows/tests.yml

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ jobs:
2424
timeout_minutes: 10
2525
max_attempts: 3
2626
command: composer install
27+
- if: ${{ matrix.php == '5.6' }}
28+
run: composer require --dev --with-dependencies paragonie/sodium_compat
2729
- name: Run Script
2830
run: vendor/bin/phpunit
2931

README.md

+37
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ Use composer to manage your dependencies and download PHP-JWT:
1616
composer require firebase/php-jwt
1717
```
1818

19+
Optionally, install the `paragonie/sodium_compat` package from composer if your
20+
php is < 7.2 or does not have libsodium installed:
21+
22+
```bash
23+
composer require paragonie/sodium_compat
24+
```
25+
1926
Example
2027
-------
2128
```php
@@ -144,6 +151,36 @@ $decoded = JWT::decode($jwt, $publicKey, array('RS256'));
144151
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
145152
```
146153

154+
Example with EdDSA (libsodium and Ed25519 signature)
155+
----------------------------
156+
```php
157+
use Firebase\JWT\JWT;
158+
159+
// Public and private keys are expected to be Base64 encoded. The last
160+
// non-empty line is used so that keys can be generated with
161+
// sodium_crypto_sign_keypair(). The secret keys generated by other tools may
162+
// need to be adjusted to match the input expected by libsodium.
163+
164+
$keyPair = sodium_crypto_sign_keypair();
165+
166+
$privateKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
167+
168+
$publicKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
169+
170+
$payload = array(
171+
"iss" => "example.org",
172+
"aud" => "example.com",
173+
"iat" => 1356999524,
174+
"nbf" => 1357000000
175+
);
176+
177+
$jwt = JWT::encode($payload, $privateKey, 'EdDSA');
178+
echo "Encode:\n" . print_r($jwt, true) . "\n";
179+
180+
$decoded = JWT::decode($jwt, $publicKey, array('EdDSA'));
181+
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
182+
````
183+
147184
Using JWKs
148185
----------
149186

composer.json

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
"require": {
2323
"php": ">=5.3.0"
2424
},
25+
"suggest": {
26+
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
27+
},
2528
"autoload": {
2629
"psr-4": {
2730
"Firebase\\JWT\\": "src"

src/JWT.php

+34-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Firebase\JWT;
44

55
use DomainException;
6+
use Exception;
67
use InvalidArgumentException;
78
use UnexpectedValueException;
89
use DateTime;
@@ -50,6 +51,7 @@ class JWT
5051
'RS256' => array('openssl', 'SHA256'),
5152
'RS384' => array('openssl', 'SHA384'),
5253
'RS512' => array('openssl', 'SHA512'),
54+
'EdDSA' => array('sodium_crypto', 'EdDSA'),
5355
);
5456

5557
/**
@@ -198,7 +200,7 @@ public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $he
198200
*
199201
* @return string An encrypted message
200202
*
201-
* @throws DomainException Unsupported algorithm was specified
203+
* @throws DomainException Unsupported algorithm or bad key was specified
202204
*/
203205
public static function sign($msg, $key, $alg = 'HS256')
204206
{
@@ -214,14 +216,24 @@ public static function sign($msg, $key, $alg = 'HS256')
214216
$success = \openssl_sign($msg, $signature, $key, $algorithm);
215217
if (!$success) {
216218
throw new DomainException("OpenSSL unable to sign data");
217-
} else {
218-
if ($alg === 'ES256') {
219-
$signature = self::signatureFromDER($signature, 256);
220-
}
221-
if ($alg === 'ES384') {
222-
$signature = self::signatureFromDER($signature, 384);
223-
}
224-
return $signature;
219+
}
220+
if ($alg === 'ES256') {
221+
$signature = self::signatureFromDER($signature, 256);
222+
} elseif ($alg === 'ES384') {
223+
$signature = self::signatureFromDER($signature, 384);
224+
}
225+
return $signature;
226+
case 'sodium_crypto':
227+
if (!function_exists('sodium_crypto_sign_detached')) {
228+
throw new DomainException('libsodium is not available');
229+
}
230+
try {
231+
// The last non-empty line is used as the key.
232+
$lines = array_filter(explode("\n", $key));
233+
$key = base64_decode(end($lines));
234+
return sodium_crypto_sign_detached($msg, $key);
235+
} catch (Exception $e) {
236+
throw new DomainException($e->getMessage(), 0, $e);
225237
}
226238
}
227239
}
@@ -237,7 +249,7 @@ public static function sign($msg, $key, $alg = 'HS256')
237249
*
238250
* @return bool
239251
*
240-
* @throws DomainException Invalid Algorithm or OpenSSL failure
252+
* @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
241253
*/
242254
private static function verify($msg, $signature, $key, $alg)
243255
{
@@ -258,6 +270,18 @@ private static function verify($msg, $signature, $key, $alg)
258270
throw new DomainException(
259271
'OpenSSL error: ' . \openssl_error_string()
260272
);
273+
case 'sodium_crypto':
274+
if (!function_exists('sodium_crypto_sign_verify_detached')) {
275+
throw new DomainException('libsodium is not available');
276+
}
277+
try {
278+
// The last non-empty line is used as the key.
279+
$lines = array_filter(explode("\n", $key));
280+
$key = base64_decode(end($lines));
281+
return sodium_crypto_sign_verify_detached($signature, $msg, $key);
282+
} catch (Exception $e) {
283+
throw new DomainException($e->getMessage(), 0, $e);
284+
}
261285
case 'hash_hmac':
262286
default:
263287
$hash = \hash_hmac($algorithm, $msg, $key, true);

tests/JWTTest.php

+29
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,34 @@ public function testRSEncodeDecode()
285285
$this->assertEquals($decoded, 'abc');
286286
}
287287

288+
public function testEdDsaEncodeDecode()
289+
{
290+
$keyPair = sodium_crypto_sign_keypair();
291+
$privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
292+
293+
$payload = array('foo' => 'bar');
294+
$msg = JWT::encode($payload, $privKey, 'EdDSA');
295+
296+
$pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
297+
$decoded = JWT::decode($msg, $pubKey, array('EdDSA'));
298+
$this->assertEquals('bar', $decoded->foo);
299+
}
300+
301+
public function testInvalidEdDsaEncodeDecode()
302+
{
303+
$keyPair = sodium_crypto_sign_keypair();
304+
$privKey = base64_encode(sodium_crypto_sign_secretkey($keyPair));
305+
306+
$payload = array('foo' => 'bar');
307+
$msg = JWT::encode($payload, $privKey, 'EdDSA');
308+
309+
// Generate a different key.
310+
$keyPair = sodium_crypto_sign_keypair();
311+
$pubKey = base64_encode(sodium_crypto_sign_publickey($keyPair));
312+
$this->setExpectedException('Firebase\JWT\SignatureInvalidException');
313+
JWT::decode($msg, $pubKey, array('EdDSA'));
314+
}
315+
288316
public function testRSEncodeDecodeWithPassphrase()
289317
{
290318
$privateKey = openssl_pkey_get_private(
@@ -322,6 +350,7 @@ public function provideEncodeDecode()
322350
array(__DIR__ . '/ecdsa-private.pem', __DIR__ . '/ecdsa-public.pem', 'ES256'),
323351
array(__DIR__ . '/ecdsa384-private.pem', __DIR__ . '/ecdsa384-public.pem', 'ES384'),
324352
array(__DIR__ . '/rsa1-private.pem', __DIR__ . '/rsa1-public.pub', 'RS512'),
353+
array(__DIR__ . '/ed25519-1.sec', __DIR__ . '/ed25519-1.pub', 'EdDSA'),
325354
);
326355
}
327356
}

tests/ed25519-1.pub

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uOSJMhbKSG4V5xUHS7B9YHmVg/1yVd+G+Io6oBFhSfY=

tests/ed25519-1.sec

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
i4eTKkWNIISKumdk3v90cPDrY/g8WRTJWy7DmGDsdzC45IkyFspIbhXnFQdLsH1geZWD/XJV34b4ijqgEWFJ9g==

0 commit comments

Comments
 (0)