172 lines
5.5 KiB
PHP
172 lines
5.5 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace Firebase\JWT;
|
||
|
|
||
|
use DomainException;
|
||
|
use UnexpectedValueException;
|
||
|
|
||
|
/**
|
||
|
* JSON Web Key implementation, based on this spec:
|
||
|
* https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
|
||
|
*
|
||
|
* PHP version 5
|
||
|
*
|
||
|
* @category Authentication
|
||
|
* @package Authentication_JWT
|
||
|
* @author Bui Sy Nguyen <nguyenbs@gmail.com>
|
||
|
* @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
|
||
|
* @link https://github.com/firebase/php-jwt
|
||
|
*/
|
||
|
class JWK
|
||
|
{
|
||
|
/**
|
||
|
* Parse a set of JWK keys
|
||
|
*
|
||
|
* @param array $jwks The JSON Web Key Set as an associative array
|
||
|
*
|
||
|
* @return array An associative array that represents the set of keys
|
||
|
*
|
||
|
* @throws InvalidArgumentException Provided JWK Set is empty
|
||
|
* @throws UnexpectedValueException Provided JWK Set was invalid
|
||
|
* @throws DomainException OpenSSL failure
|
||
|
*
|
||
|
* @uses parseKey
|
||
|
*/
|
||
|
public static function parseKeySet(array $jwks)
|
||
|
{
|
||
|
$keys = array();
|
||
|
|
||
|
if (!isset($jwks['keys'])) {
|
||
|
throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
|
||
|
}
|
||
|
if (empty($jwks['keys'])) {
|
||
|
throw new InvalidArgumentException('JWK Set did not contain any keys');
|
||
|
}
|
||
|
|
||
|
foreach ($jwks['keys'] as $k => $v) {
|
||
|
$kid = isset($v['kid']) ? $v['kid'] : $k;
|
||
|
if ($key = self::parseKey($v)) {
|
||
|
$keys[$kid] = $key;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (0 === \count($keys)) {
|
||
|
throw new UnexpectedValueException('No supported algorithms found in JWK Set');
|
||
|
}
|
||
|
|
||
|
return $keys;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse a JWK key
|
||
|
*
|
||
|
* @param array $jwk An individual JWK
|
||
|
*
|
||
|
* @return resource|array An associative array that represents the key
|
||
|
*
|
||
|
* @throws InvalidArgumentException Provided JWK is empty
|
||
|
* @throws UnexpectedValueException Provided JWK was invalid
|
||
|
* @throws DomainException OpenSSL failure
|
||
|
*
|
||
|
* @uses createPemFromModulusAndExponent
|
||
|
*/
|
||
|
private static function parseKey(array $jwk)
|
||
|
{
|
||
|
if (empty($jwk)) {
|
||
|
throw new InvalidArgumentException('JWK must not be empty');
|
||
|
}
|
||
|
if (!isset($jwk['kty'])) {
|
||
|
throw new UnexpectedValueException('JWK must contain a "kty" parameter');
|
||
|
}
|
||
|
|
||
|
switch ($jwk['kty']) {
|
||
|
case 'RSA':
|
||
|
if (\array_key_exists('d', $jwk)) {
|
||
|
throw new UnexpectedValueException('RSA private keys are not supported');
|
||
|
}
|
||
|
if (!isset($jwk['n']) || !isset($jwk['e'])) {
|
||
|
throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
|
||
|
}
|
||
|
|
||
|
$pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
|
||
|
$publicKey = \openssl_pkey_get_public($pem);
|
||
|
if (false === $publicKey) {
|
||
|
throw new DomainException(
|
||
|
'OpenSSL error: ' . \openssl_error_string()
|
||
|
);
|
||
|
}
|
||
|
return $publicKey;
|
||
|
default:
|
||
|
// Currently only RSA is supported
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a public key represented in PEM format from RSA modulus and exponent information
|
||
|
*
|
||
|
* @param string $n The RSA modulus encoded in Base64
|
||
|
* @param string $e The RSA exponent encoded in Base64
|
||
|
*
|
||
|
* @return string The RSA public key represented in PEM format
|
||
|
*
|
||
|
* @uses encodeLength
|
||
|
*/
|
||
|
private static function createPemFromModulusAndExponent($n, $e)
|
||
|
{
|
||
|
$modulus = JWT::urlsafeB64Decode($n);
|
||
|
$publicExponent = JWT::urlsafeB64Decode($e);
|
||
|
|
||
|
$components = array(
|
||
|
'modulus' => \pack('Ca*a*', 2, self::encodeLength(\strlen($modulus)), $modulus),
|
||
|
'publicExponent' => \pack('Ca*a*', 2, self::encodeLength(\strlen($publicExponent)), $publicExponent)
|
||
|
);
|
||
|
|
||
|
$rsaPublicKey = \pack(
|
||
|
'Ca*a*a*',
|
||
|
48,
|
||
|
self::encodeLength(\strlen($components['modulus']) + \strlen($components['publicExponent'])),
|
||
|
$components['modulus'],
|
||
|
$components['publicExponent']
|
||
|
);
|
||
|
|
||
|
// sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
|
||
|
$rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
|
||
|
$rsaPublicKey = \chr(0) . $rsaPublicKey;
|
||
|
$rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
|
||
|
|
||
|
$rsaPublicKey = \pack(
|
||
|
'Ca*a*',
|
||
|
48,
|
||
|
self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
|
||
|
$rsaOID . $rsaPublicKey
|
||
|
);
|
||
|
|
||
|
$rsaPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" .
|
||
|
\chunk_split(\base64_encode($rsaPublicKey), 64) .
|
||
|
'-----END PUBLIC KEY-----';
|
||
|
|
||
|
return $rsaPublicKey;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* DER-encode the length
|
||
|
*
|
||
|
* DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
|
||
|
* {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
|
||
|
*
|
||
|
* @param int $length
|
||
|
* @return string
|
||
|
*/
|
||
|
private static function encodeLength($length)
|
||
|
{
|
||
|
if ($length <= 0x7F) {
|
||
|
return \chr($length);
|
||
|
}
|
||
|
|
||
|
$temp = \ltrim(\pack('N', $length), \chr(0));
|
||
|
|
||
|
return \pack('Ca*', 0x80 | \strlen($temp), $temp);
|
||
|
}
|
||
|
}
|