Skip to content

Commit aa0b447

Browse files
feat: Add support for PS256 (#637)
1 parent 8a6c2a7 commit aa0b447

4 files changed

Lines changed: 171 additions & 4 deletions

File tree

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,38 @@ echo "Decode 1:\n" . print_r((array) $decoded1, true) . "\n";
279279
echo "Decode 2:\n" . print_r((array) $decoded2, true) . "\n";
280280
```
281281

282+
## Example with PS256
283+
284+
### Note
285+
PHP's OpenSSL extension does not support RSASSA-PSS signatures (PS256) by default, so we provide support via a soft dependency on the [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib) library. It is necessary to install this library in your project if you plan to use PS256.
286+
```bash
287+
composer install phpseclib/phpseclib:^3.0
288+
```
289+
290+
```php
291+
use Firebase\JWT\JWT;
292+
use Firebase\JWT\Key;
293+
294+
$privateRsKey = '-----BEGIN RSA PRIVATE KEY----- ...';
295+
$publicKey = '-----BEGIN PUBLIC KEY----- ...';
296+
297+
$payload = [
298+
'iss' => 'example.org',
299+
'aud' => 'example.com',
300+
'iat' => 1356999524,
301+
'nbf' => 1357000000
302+
];
303+
304+
/**
305+
* PS256 support requires phpseclib/phpseclib
306+
*/
307+
$jwt = JWT::encode($payload, $privateRsKey, 'PS256', 'keyid');
308+
echo "Encode:\n" . print_r($jwt, true) . "\n";
309+
310+
$decoded = JWT::decode($jwt, new Key($publicKey, 'PS256'));
311+
echo "Decode:\n" . print_r((array) $decoded, true) . "\n";
312+
```
313+
282314
## Using JWKs
283315

284316
```php

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
},
2525
"suggest": {
2626
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present",
27-
"ext-sodium": "Support EdDSA (Ed25519) signatures"
27+
"ext-sodium": "Support EdDSA (Ed25519) signatures",
28+
"phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures"
2829
},
2930
"autoload": {
3031
"psr-4": {
@@ -38,6 +39,7 @@
3839
"psr/cache": "^2.0||^3.0",
3940
"psr/http-client": "^1.0",
4041
"psr/http-factory": "^1.0",
41-
"phpfastcache/phpfastcache": "^9.2"
42+
"phpfastcache/phpfastcache": "^9.2",
43+
"phpseclib/phpseclib": "~3.0"
4244
}
4345
}

src/JWT.php

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ class JWT
6464
'RS256' => ['openssl', 'SHA256'],
6565
'RS384' => ['openssl', 'SHA384'],
6666
'RS512' => ['openssl', 'SHA512'],
67-
'EdDSA' => ['sodium_crypto', 'EdDSA'],
67+
'PS256' => ['openssl', 'SHA256'],
68+
'EdDSA' => ['sodium_crypto', 'EdDSA']
6869
];
6970

7071
/**
@@ -250,7 +251,7 @@ public static function encode(
250251
* @param string $msg The message to sign
251252
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
252253
* @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256',
253-
* 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512'
254+
* 'HS384', 'HS512', 'RS256', 'RS384', 'PS256' and 'RS512'
254255
*
255256
* @return string An encrypted message
256257
*
@@ -273,6 +274,9 @@ public static function sign(
273274
self::validateHmacKeyLength($key, $algorithm);
274275
return \hash_hmac($algorithm, $msg, $key, true);
275276
case 'openssl':
277+
if ($alg === 'PS256') {
278+
return self::signPS256($key, $msg);
279+
}
276280
$signature = '';
277281
if (!$key = openssl_pkey_get_private($key)) {
278282
throw new DomainException('OpenSSL unable to validate key');
@@ -329,6 +333,9 @@ private static function verify(
329333
list($function, $algorithm) = static::$supported_algs[$alg];
330334
switch ($function) {
331335
case 'openssl':
336+
if ($alg === 'PS256') {
337+
return self::verifyPS256($keyMaterial, $msg, $signature);
338+
}
332339
if (!$key = openssl_pkey_get_public($keyMaterial)) {
333340
throw new DomainException('OpenSSL unable to validate key');
334341
}
@@ -751,4 +758,76 @@ private static function validateEdDSAKey(#[\SensitiveParameter] $keyMaterial): s
751758
}
752759
return $key;
753760
}
761+
762+
/**
763+
* Signs a message with a PS256 algorithm
764+
*
765+
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key
766+
* @param string $message
767+
* @throws DomainException Provided key is invalid
768+
*/
769+
private static function signPS256(
770+
#[\SensitiveParameter] string|OpenSSLAsymmetricKey|OpenSSLCertificate $key,
771+
string $message
772+
): string {
773+
if (!class_exists('\phpseclib3\Crypt\RSA')) {
774+
throw new DomainException('phpseclib/phpseclib is required for PS256 support');
775+
}
776+
777+
if ($key instanceof OpenSSLCertificate) {
778+
throw new DomainException('Cannot sign with an X.509 certificate. A private key is required.');
779+
}
780+
781+
if ($key instanceof OpenSSLAsymmetricKey) {
782+
if (!openssl_pkey_export($key, $pem)) {
783+
throw new DomainException('OpenSSL unable to export the AsymmetricKey');
784+
}
785+
$key = $pem;
786+
}
787+
788+
/** @var \phpseclib3\Crypt\RSA\PrivateKey $rsa */
789+
$rsa = \phpseclib3\Crypt\PublicKeyLoader::load($key);
790+
791+
return $rsa->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS)
792+
->withHash('sha256')
793+
->sign($message);
794+
}
795+
796+
/**
797+
* Validates a PS256 algorithm signature.
798+
*
799+
* @param string|OpenSSLAsymmetricKey|OpenSSLCertificate $key
800+
* @param string $message
801+
* @param string $signature
802+
* @throws DomainException Provided key is invalid
803+
*/
804+
private static function verifyPS256(
805+
#[\SensitiveParameter] string|OpenSSLAsymmetricKey|OpenSSLCertificate $key,
806+
string $message,
807+
string $signature
808+
): bool {
809+
if (!class_exists('\phpseclib3\Crypt\RSA')) {
810+
throw new DomainException('phpseclib/phpseclib is required for PS256 support');
811+
}
812+
813+
if ($key instanceof OpenSSLAsymmetricKey) {
814+
$details = openssl_pkey_get_details($key);
815+
if (!$details || !isset($details['key'])) {
816+
throw new DomainException('OpenSSL unable to extract public key');
817+
}
818+
$key = $details['key'];
819+
} elseif ($key instanceof OpenSSLCertificate) {
820+
if (!openssl_x509_export($key, $pem)) {
821+
throw new DomainException('OpenSSL unable to export certificate');
822+
}
823+
$key = $pem;
824+
}
825+
826+
/** @var \phpseclib3\Crypt\RSA\PublicKey $rsa */
827+
$rsa = \phpseclib3\Crypt\PublicKeyLoader::load($key);
828+
829+
return $rsa->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS)
830+
->withHash('sha256')
831+
->verify($message, $signature);
832+
}
754833
}

tests/JWTTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,60 @@ public function testRSEncodeDecode()
440440
$this->assertEquals($decoded, $expected);
441441
}
442442

443+
public function testPSEncodeDecode()
444+
{
445+
$privKey = openssl_pkey_new([
446+
'digest_alg' => 'sha256',
447+
'private_key_bits' => 2048,
448+
'private_key_type' => OPENSSL_KEYTYPE_RSA
449+
]);
450+
openssl_pkey_export($privKey, $privKeyPem);
451+
$pubKeyDetails = openssl_pkey_get_details($privKey);
452+
$pubKeyPem = $pubKeyDetails['key'];
453+
454+
$payload = ['message' => 'abc'];
455+
$msg = JWT::encode($payload, $privKeyPem, 'PS256');
456+
$decoded = JWT::decode($msg, new Key($pubKeyPem, 'PS256'));
457+
458+
$this->assertEquals($decoded, (object) $payload);
459+
}
460+
461+
public function testPSEncodeDecodeWithOpenSSLKey()
462+
{
463+
$privKey = openssl_pkey_new([
464+
'digest_alg' => 'sha256',
465+
'private_key_bits' => 2048,
466+
'private_key_type' => OPENSSL_KEYTYPE_RSA
467+
]);
468+
$pubKey = openssl_pkey_get_public(openssl_pkey_get_details($privKey)['key']);
469+
470+
$payload = ['message' => 'abc'];
471+
$msg = JWT::encode($payload, $privKey, 'PS256');
472+
$decoded = JWT::decode($msg, new Key($pubKey, 'PS256'));
473+
474+
$this->assertEquals($decoded, (object) $payload);
475+
}
476+
477+
public function testPSVerifyWithCertificate()
478+
{
479+
$privKey = openssl_pkey_new([
480+
'digest_alg' => 'sha256',
481+
'private_key_bits' => 2048,
482+
'private_key_type' => OPENSSL_KEYTYPE_RSA
483+
]);
484+
485+
$csr = openssl_csr_new(['commonName' => 'example.com'], $privKey);
486+
$cert = openssl_csr_sign($csr, null, $privKey, 1);
487+
488+
$payload = ['message' => 'abc'];
489+
$msg = JWT::encode($payload, $privKey, 'PS256');
490+
491+
// Use certificate for verification
492+
$decoded = JWT::decode($msg, new Key($cert, 'PS256'));
493+
494+
$this->assertEquals($decoded, (object) $payload);
495+
}
496+
443497
public function testEdDsaEncodeDecode()
444498
{
445499
$keyPair = sodium_crypto_sign_keypair();

0 commit comments

Comments
 (0)