add JSON Web Key (JWK) support

This commit is contained in:
terrafrost 2022-08-20 06:18:54 -05:00
parent 13b241e3e9
commit 0b3c6e27fc
6 changed files with 577 additions and 2 deletions

View File

@ -470,11 +470,11 @@ abstract class Strings
*/
public static function base64url_encode($data)
{
// return self::base64_encode(str_replace(['+', '/'], ['-', '_'], $data));
// return str_replace(['+', '/'], ['-', '_'], self::base64_encode($data));
return function_exists('sodium_bin2base64') ?
sodium_bin2base64($data, SODIUM_BASE64_VARIANT_URLSAFE) :
Base64::encode($data);
Base64UrlSafe::encode($data);
}
/**

View File

@ -0,0 +1,69 @@
<?php
/**
* JSON Web Key (RFC7517) Handler
*
* PHP version 5
*
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2015 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib3\Crypt\Common\Formats\Keys;
use phpseclib3\Common\Functions\Strings;
/**
* JSON Web Key Formatted Key Handler
*
* @author Jim Wigginton <terrafrost@php.net>
*/
abstract class JWK
{
/**
* Break a public or private key down into its constituent components
*
* @param string $key
* @param string $password
* @return array
*/
public static function load($key, $password = '')
{
if (!Strings::is_stringable($key)) {
throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key));
}
$key = preg_replace('#\s#', '', $key); // remove whitespace
if (PHP_VERSION_ID >= 73000) {
$key = json_decode($key, null, 512, JSON_THROW_ON_ERROR);
} else {
$key = json_decode($key);
if (!$key) {
throw new \RuntimeException('Unable to decode JSON');
}
}
if (isset($key->kty)) {
return $key;
}
if (count($key->keys) != 1) {
throw new \RuntimeException('Although the JWK key format supports multiple keys phpseclib does not');
}
return $key->keys[0];
}
/**
* Wrap a key appropriately
*
* @return string
*/
protected static function wrapKey(array $key, array $options)
{
return json_encode(['keys' => [$key + $options]]);
}
}

View File

@ -0,0 +1,190 @@
<?php
/**
* JSON Web Key (RFC7517 / RFC8037) Formatted EC Handler
*
* PHP version 5
*
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2015 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib3\Crypt\EC\Formats\Keys;
use phpseclib3\Crypt\EC\BaseCurves\Base as BaseCurve;
use phpseclib3\Common\Functions\Strings;
use phpseclib3\Crypt\Common\Formats\Keys\JWK as Progenitor;
use phpseclib3\Crypt\EC\BaseCurves\TwistedEdwards as TwistedEdwardsCurve;
use phpseclib3\Crypt\EC\Curves\Ed25519;
use phpseclib3\Crypt\EC\Curves\secp256r1;
use phpseclib3\Crypt\EC\Curves\secp384r1;
use phpseclib3\Crypt\EC\Curves\secp521r1;
use phpseclib3\Crypt\EC\Curves\secp256k1;
use phpseclib3\Exception\UnsupportedCurveException;
use phpseclib3\Math\BigInteger;
/**
* JWK Formatted EC Handler
*
* @author Jim Wigginton <terrafrost@php.net>
*/
abstract class JWK extends Progenitor
{
use Common;
/**
* Break a public or private key down into its constituent components
*
* @param string $key
* @param string $password optional
* @return array
*/
public static function load($key, $password = '')
{
$key = parent::load($key, $password);
switch ($key->kty) {
case 'EC':
switch ($key->crv) {
case 'P-256':
case 'P-384':
case 'P-521':
case 'secp256k1':
break;
default:
throw new UnsupportedCurveException('Only P-256, P-384, P-521 and secp256k1 curves are accepted (' . $key->crv . ' provided)');
}
break;
case 'OKP':
switch ($key->crv) {
case 'Ed25519':
case 'Ed448':
break;
default:
throw new UnsupportedCurveException('Only Ed25519 and Ed448 curves are accepted (' . $key->crv . ' provided)');
}
break;
default:
throw new \Exception('Only EC and OKP JWK keys are supported');
}
$curve = '\phpseclib3\Crypt\EC\Curves\\' . str_replace('P-', 'nistp', $key->crv);
$curve = new $curve;
if ($curve instanceof TwistedEdwardsCurve) {
$QA = self::extractPoint(Strings::base64url_decode($key->x), $curve);
if (!isset($key->d)) {
return compact('curve', 'QA');
}
$dA = $curve->extractSecret(Strings::base64url_decode($key->d));
return compact('curve', 'dA', 'QA');
}
$QA = [
$curve->convertInteger(new BigInteger(Strings::base64url_decode($key->x), 256)),
$curve->convertInteger(new BigInteger(Strings::base64url_decode($key->y), 256))
];
if (!$curve->verifyPoint($QA)) {
throw new \RuntimeException('Unable to verify that point exists on curve');
}
if (!isset($key->d)) {
return compact('curve', 'QA');
}
$dA = new BigInteger(Strings::base64url_decode($key->d), 256);
$curve->rangeCheck($dA);
return compact('curve', 'dA', 'QA');
}
/**
* Returns the alias that corresponds to a curve
*
* @return string
*/
private static function getAlias(BaseCurve $curve)
{
switch (true) {
case $curve instanceof secp256r1:
return 'P-256';
case $curve instanceof secp384r1:
return 'P-384';
case $curve instanceof secp521r1:
return 'P-521';
case $curve instanceof secp256k1:
return 'secp256k1';
}
$reflect = new \ReflectionClass($curve);
$curveName = $reflect->isFinal() ?
$reflect->getParentClass()->getShortName() :
$reflect->getShortName();
throw new UnsupportedCurveException("$curveName is not a supported curve");
}
/**
* Return the array superstructure for an EC public key
*
* @param \phpseclib3\Crypt\EC\BaseCurves\Base $curve
* @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey
* @return array
*/
private static function savePublicKeyHelper(BaseCurve $curve, array $publicKey)
{
if ($curve instanceof TwistedEdwardsCurve) {
return [
'kty' => 'OKP',
'crv' => $curve instanceof Ed25519 ? 'Ed25519' : 'Ed448',
'x' => Strings::base64url_encode($curve->encodePoint($publicKey))
];
}
return [
'kty' => 'EC',
'crv' => self::getAlias($curve),
'x' => Strings::base64url_encode($publicKey[0]->toBytes()),
'y' => Strings::base64url_encode($publicKey[1]->toBytes())
];
}
/**
* Convert an EC public key to the appropriate format
*
* @param \phpseclib3\Crypt\EC\BaseCurves\Base $curve
* @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey
* @param array $options optional
* @return string
*/
public static function savePublicKey(BaseCurve $curve, array $publicKey, array $options = [])
{
$key = self::savePublicKeyHelper($curve, $publicKey);
return self::wrapKey($key, $options);
}
/**
* Convert a private key to the appropriate format.
*
* @param \phpseclib3\Math\BigInteger $privateKey
* @param \phpseclib3\Crypt\EC\Curves\Ed25519 $curve
* @param \phpseclib3\Math\Common\FiniteField\Integer[] $publicKey
* @param string $password optional
* @param array $options optional
* @return string
*/
public static function savePrivateKey(BigInteger $privateKey, BaseCurve $curve, array $publicKey, $password = '', array $options = [])
{
$key = self::savePublicKeyHelper($curve, $publicKey);
$key['d'] = $curve instanceof TwistedEdwardsCurve ?
$privateKey->secret :
$privateKey->toBytes();
$key['d'] = Strings::base64url_encode($key['d']);
return self::wrapKey($key, $options);
}
}

View File

@ -0,0 +1,142 @@
<?php
/**
* JSON Web Key (RFC7517) Formatted RSA Handler
*
* PHP version 5
*
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2015 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib3\Crypt\RSA\Formats\Keys;
use phpseclib3\Common\Functions\Strings;
use phpseclib3\Crypt\Common\Formats\Keys\JWK as Progenitor;
use phpseclib3\Math\BigInteger;
/**
* JWK Formatted RSA Handler
*
* @author Jim Wigginton <terrafrost@php.net>
*/
abstract class JWK extends Progenitor
{
/**
* Break a public or private key down into its constituent components
*
* @param string $key
* @param string $password optional
* @return array
*/
public static function load($key, $password = '')
{
$key = parent::load($key, $password);
if ($key->kty != 'RSA') {
throw new \RuntimeException('Only RSA JWK keys are supported');
}
$count = $publicCount = 0;
$vars = ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi'];
foreach ($vars as $var) {
if (!isset($key->$var) || !is_string($key->$var)) {
continue;
}
$count++;
$value = new BigInteger(Strings::base64url_decode($key->$var), 256);
switch ($var) {
case 'n':
$publicCount++;
$components['modulus'] = $value;
break;
case 'e':
$publicCount++;
$components['publicExponent'] = $value;
break;
case 'd':
$components['privateExponent'] = $value;
break;
case 'p':
$components['primes'][1] = $value;
break;
case 'q':
$components['primes'][2] = $value;
break;
case 'dp':
$components['exponents'][1] = $value;
break;
case 'dq':
$components['exponents'][2] = $value;
break;
case 'qi':
$components['coefficients'][2] = $value;
}
}
if ($count == count($vars)) {
return $components + ['isPublicKey' => false];
}
if ($count == 2 && $publicCount == 2) {
return $components + ['isPublicKey' => true];
}
throw new \UnexpectedValueException('Key does not have an appropriate number of RSA parameters');
}
/**
* Convert a private key to the appropriate format.
*
* @param \phpseclib3\Math\BigInteger $n
* @param \phpseclib3\Math\BigInteger $e
* @param \phpseclib3\Math\BigInteger $d
* @param array $primes
* @param array $exponents
* @param array $coefficients
* @param string $password optional
* @param array $options optional
* @return string
*/
public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, array $primes, array $exponents, array $coefficients, $password = '', array $options = [])
{
if (count($primes) != 2) {
throw new \InvalidArgumentException('JWK does not support multi-prime RSA keys');
}
$key = [
'kty' => 'RSA',
'n' => Strings::base64url_encode($n->toBytes()),
'e' => Strings::base64url_encode($e->toBytes()),
'd' => Strings::base64url_encode($d->toBytes()),
'p' => Strings::base64url_encode($primes[1]->toBytes()),
'q' => Strings::base64url_encode($primes[2]->toBytes()),
'dp' => Strings::base64url_encode($exponents[1]->toBytes()),
'dq' => Strings::base64url_encode($exponents[2]->toBytes()),
'qi' => Strings::base64url_encode($coefficients[2]->toBytes())
];
return self::wrapKey($key, $options);
}
/**
* Convert a public key to the appropriate format
*
* @param \phpseclib3\Math\BigInteger $n
* @param \phpseclib3\Math\BigInteger $e
* @param array $options optional
* @return string
*/
public static function savePublicKey(BigInteger $n, BigInteger $e, array $options = [])
{
$key = [
'kty' => 'RSA',
'n' => Strings::base64url_encode($n->toBytes()),
'e' => Strings::base64url_encode($e->toBytes())
];
return self::wrapKey($key, $options);
}
}

View File

@ -574,4 +574,96 @@ MIIEDwIBADATBgcqhkjOPQIBBggqhkjOPQMBBwSCA/MwggPvAgEBBIID6P//////
$key = PublicKeyLoader::load($key, 'test');
$this->assertInstanceOf(PrivateKey::class, $key);
}
public function testECasJWK()
{
// keys are from https://datatracker.ietf.org/doc/html/rfc7517#appendix-A
$plaintext = 'zzz';
$key = ' {"keys":
[
{"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE",
"use":"enc",
"kid":"1"}
]
}';
$keyWithoutWS = preg_replace('#\s#', '', $key);
$key = PublicKeyLoader::load($key);
$phpseclibKey = str_replace('=', '', $key->toString('JWK', [
'use' => 'enc',
'kid' => '1'
]));
$this->assertSame($keyWithoutWS, $phpseclibKey);
$sig = $key->sign($plaintext);
$key = '{"keys":
[
{"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use":"enc",
"kid":"1"}
]
}';
$keyWithoutWS = preg_replace('#\s#', '', $key);
$key = PublicKeyLoader::load($key);
$phpseclibKey = str_replace('=', '', $key->toString('JWK', [
'use' => 'enc',
'kid' => '1'
]));
$this->assertSame($keyWithoutWS, $phpseclibKey);
$this->assertTrue($key->verify($plaintext, $sig));
}
public function testEd25519asJWK()
{
// keys are from https://www.rfc-editor.org/rfc/rfc8037.html#appendix-A
$plaintext = 'zzz';
$key = ' {"kty":"OKP","crv":"Ed25519",
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
"d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A"}';
$keyWithoutWS = preg_replace('#\s#', '', $key);
$keyWithoutWS = '{"keys":[' . $keyWithoutWS . ']}';
$key = PublicKeyLoader::load($key);
$phpseclibKey = str_replace('=', '', $key->toString('JWK'));
$this->assertSame($keyWithoutWS, $phpseclibKey);
$sig = $key->sign($plaintext);
$key = ' {"kty":"OKP","crv":"Ed25519",
"x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}';
$keyWithoutWS = preg_replace('#\s#', '', $key);
$keyWithoutWS = '{"keys":[' . $keyWithoutWS . ']}';
$key = PublicKeyLoader::load($key);
$phpseclibKey = str_replace('=', '', $key->toString('JWK'));
$this->assertSame($keyWithoutWS, $phpseclibKey);
$this->assertTrue($key->verify($plaintext, $sig));
}
}

View File

@ -1277,4 +1277,86 @@ LrIZULwMa4nI4Y+RkFftEponSYw=
$key = PublicKeyLoader::load($key, 'test');
$this->assertInstanceOf(PrivateKey::class, $key);
}
public function testJWK()
{
// keys are from https://datatracker.ietf.org/doc/html/rfc7517#appendix-A
$plaintext = 'zzz';
$key = ' {"keys":
[
{"kty":"RSA",
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4
cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMst
n64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2Q
vzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbIS
D08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw
0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9
M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqij
wp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d
_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBz
nbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFz
me1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q",
"p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPV
nwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqV
WlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs",
"q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyum
qjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgx
kIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk",
"dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oim
YwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_Nmtu
YZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0",
"dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUU
vMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9
GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk",
"qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzg
UIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rx
yR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU",
"alg":"RS256",
"kid":"2011-04-29"}
]
}';
$keyWithoutWS = preg_replace('#\s#', '', $key);
$key = PublicKeyLoader::load($key);
$phpseclibKey = str_replace('=', '', $key->toString('JWK', [
'alg' => 'RS256',
'kid' => '2011-04-29'
]));
$this->assertSame($keyWithoutWS, $phpseclibKey);
$sig = $key->sign($plaintext);
$key = ' {"kty":"RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx
4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs
tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2
QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI
SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb
w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"2011-04-29"}';
$keyWithoutWS = preg_replace('#\s#', '', $key);
$keyWithoutWS = '{"keys":[' . $keyWithoutWS . ']}';
$key = PublicKeyLoader::load($key);
$phpseclibKey = str_replace('=', '', $key->toString('JWK', [
'alg' => 'RS256',
'kid' => '2011-04-29'
]));
$this->assertSame($keyWithoutWS, $phpseclibKey);
$this->assertTrue($key->verify($plaintext, $sig));
}
}