updated plugin WP-WebAuthn version 1.3.1

This commit is contained in:
2023-10-22 22:21:36 +00:00
committed by Gitium
parent 959829cf69
commit c7746517a0
931 changed files with 5408 additions and 1937 deletions

View File

@ -0,0 +1,4 @@
# Contributing
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
Please do not submit any Pull Requests here. It will be automatically closed.

View File

@ -0,0 +1 @@
patreon: FlorentMorselli

View File

@ -0,0 +1,3 @@
Please do not submit any Pull Requests here. It will be automatically closed.
You should submit it here: https://github.com/web-token/jwt-framework/pulls

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core;
interface Algorithm
{
/**
* Returns the name of the algorithm.
*/
public function name(): string;
/**
* Returns the key types suitable for this algorithm (e.g. "oct", "RSA"...).
*
* @return string[]
*/
public function allowedKeyTypes(): array;
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core;
use function array_key_exists;
use InvalidArgumentException;
class AlgorithmManager
{
/**
* @var array
*/
private $algorithms = [];
/**
* @param Algorithm[] $algorithms
*/
public function __construct(array $algorithms)
{
foreach ($algorithms as $algorithm) {
$this->add($algorithm);
}
}
/**
* Returns true if the algorithm is supported.
*
* @param string $algorithm The algorithm
*/
public function has(string $algorithm): bool
{
return array_key_exists($algorithm, $this->algorithms);
}
/**
* Returns the list of names of supported algorithms.
*
* @return string[]
*/
public function list(): array
{
return array_keys($this->algorithms);
}
/**
* Returns the algorithm if supported, otherwise throw an exception.
*
* @param string $algorithm The algorithm
*
* @throws InvalidArgumentException if the algorithm is not supported
*/
public function get(string $algorithm): Algorithm
{
if (!$this->has($algorithm)) {
throw new InvalidArgumentException(sprintf('The algorithm "%s" is not supported.', $algorithm));
}
return $this->algorithms[$algorithm];
}
/**
* Adds an algorithm to the manager.
*/
public function add(Algorithm $algorithm): void
{
$name = $algorithm->name();
$this->algorithms[$name] = $algorithm;
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core;
use InvalidArgumentException;
use function is_string;
class AlgorithmManagerFactory
{
/**
* @var array
*/
private $algorithms = [];
/**
* Adds an algorithm.
*
* Each algorithm is identified by an alias hence it is allowed to have the same algorithm twice (or more).
* This can be helpful when an algorithm have several configuration options.
*/
public function add(string $alias, Algorithm $algorithm): void
{
$this->algorithms[$alias] = $algorithm;
}
/**
* Returns the list of aliases.
*
* @return string[]
*/
public function aliases(): array
{
return array_keys($this->algorithms);
}
/**
* Returns all algorithms supported by this factory.
* This is an associative array. Keys are the aliases of the algorithms.
*
* @return Algorithm[]
*/
public function all(): array
{
return $this->algorithms;
}
/**
* Create an algorithm manager using the given aliases.
*
* @param string[] $aliases
*
* @throws InvalidArgumentException if the alias is invalid or is not supported
*/
public function create(array $aliases): AlgorithmManager
{
$algorithms = [];
foreach ($aliases as $alias) {
if (!is_string($alias)) {
throw new InvalidArgumentException('Invalid alias');
}
if (!isset($this->algorithms[$alias])) {
throw new InvalidArgumentException(sprintf('The algorithm with the alias "%s" is not supported.', $alias));
}
$algorithms[] = $this->algorithms[$alias];
}
return new AlgorithmManager($algorithms);
}
}

View File

@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core;
use function array_key_exists;
use Base64Url\Base64Url;
use function in_array;
use InvalidArgumentException;
use function is_array;
use JsonSerializable;
class JWK implements JsonSerializable
{
/**
* @var array
*/
private $values = [];
/**
* Creates a JWK object using the given values.
* The member "kty" is mandatory. Other members are NOT checked.
*
* @throws InvalidArgumentException if the key parameter "kty" is missing
*/
public function __construct(array $values)
{
if (!isset($values['kty'])) {
throw new InvalidArgumentException('The parameter "kty" is mandatory.');
}
$this->values = $values;
}
/**
* Creates a JWK object using the given Json string.
*
* @throws InvalidArgumentException if the data is not valid
*
* @return JWK
*/
public static function createFromJson(string $json): self
{
$data = json_decode($json, true);
if (!is_array($data)) {
throw new InvalidArgumentException('Invalid argument.');
}
return new self($data);
}
/**
* Returns the values to be serialized.
*/
public function jsonSerialize(): array
{
return $this->values;
}
/**
* Get the value with a specific key.
*
* @param string $key The key
*
* @throws InvalidArgumentException if the key does not exist
*
* @return null|mixed
*/
public function get(string $key)
{
if (!$this->has($key)) {
throw new InvalidArgumentException(sprintf('The value identified by "%s" does not exist.', $key));
}
return $this->values[$key];
}
/**
* Returns true if the JWK has the value identified by.
*
* @param string $key The key
*/
public function has(string $key): bool
{
return array_key_exists($key, $this->values);
}
/**
* Get all values stored in the JWK object.
*
* @return array Values of the JWK object
*/
public function all(): array
{
return $this->values;
}
/**
* Returns the thumbprint of the key.
*
* @see https://tools.ietf.org/html/rfc7638
*
* @throws InvalidArgumentException if the hashing function is not supported
*/
public function thumbprint(string $hash_algorithm): string
{
if (!in_array($hash_algorithm, hash_algos(), true)) {
throw new InvalidArgumentException(sprintf('The hash algorithm "%s" is not supported.', $hash_algorithm));
}
$values = array_intersect_key($this->values, array_flip(['kty', 'n', 'e', 'crv', 'x', 'y', 'k']));
ksort($values);
$input = json_encode($values, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if (false === $input) {
throw new InvalidArgumentException('Unable to compute the key thumbprint');
}
return Base64Url::encode(hash($hash_algorithm, $input, true));
}
/**
* Returns the associated public key.
* This method has no effect for:
* - public keys
* - shared keys
* - unknown keys.
*
* Known keys are "oct", "RSA", "EC" and "OKP".
*
* @return JWK
*/
public function toPublic(): self
{
$values = array_diff_key($this->values, array_flip(['p', 'd', 'q', 'dp', 'dq', 'qi']));
return new self($values);
}
}

View File

@ -0,0 +1,340 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core;
use function array_key_exists;
use ArrayIterator;
use function count;
use Countable;
use function in_array;
use InvalidArgumentException;
use function is_array;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
class JWKSet implements Countable, IteratorAggregate, JsonSerializable
{
/**
* @var array
*/
private $keys = [];
/**
* @param JWK[] $keys
*
* @throws InvalidArgumentException if the list is invalid
*/
public function __construct(array $keys)
{
foreach ($keys as $k => $key) {
if (!$key instanceof JWK) {
throw new InvalidArgumentException('Invalid list. Should only contains JWK objects');
}
if ($key->has('kid')) {
unset($keys[$k]);
$this->keys[$key->get('kid')] = $key;
} else {
$this->keys[] = $key;
}
}
}
/**
* Creates a JWKSet object using the given values.
*
* @throws InvalidArgumentException if the keyset is not valid
*
* @return JWKSet
*/
public static function createFromKeyData(array $data): self
{
if (!isset($data['keys'])) {
throw new InvalidArgumentException('Invalid data.');
}
if (!is_array($data['keys'])) {
throw new InvalidArgumentException('Invalid data.');
}
$jwkset = new self([]);
foreach ($data['keys'] as $key) {
$jwk = new JWK($key);
if ($jwk->has('kid')) {
$jwkset->keys[$jwk->get('kid')] = $jwk;
} else {
$jwkset->keys[] = $jwk;
}
}
return $jwkset;
}
/**
* Creates a JWKSet object using the given Json string.
*
* @throws InvalidArgumentException if the data is not valid
*
* @return JWKSet
*/
public static function createFromJson(string $json): self
{
$data = json_decode($json, true);
if (!is_array($data)) {
throw new InvalidArgumentException('Invalid argument.');
}
return self::createFromKeyData($data);
}
/**
* Returns an array of keys stored in the key set.
*
* @return JWK[]
*/
public function all(): array
{
return $this->keys;
}
/**
* Add key to store in the key set.
* This method is immutable and will return a new object.
*
* @return JWKSet
*/
public function with(JWK $jwk): self
{
$clone = clone $this;
if ($jwk->has('kid')) {
$clone->keys[$jwk->get('kid')] = $jwk;
} else {
$clone->keys[] = $jwk;
}
return $clone;
}
/**
* Remove key from the key set.
* This method is immutable and will return a new object.
*
* @param int|string $key Key to remove from the key set
*
* @return JWKSet
*/
public function without($key): self
{
if (!$this->has($key)) {
return $this;
}
$clone = clone $this;
unset($clone->keys[$key]);
return $clone;
}
/**
* Returns true if the key set contains a key with the given index.
*
* @param int|string $index
*/
public function has($index): bool
{
return array_key_exists($index, $this->keys);
}
/**
* Returns the key with the given index. Throws an exception if the index is not present in the key store.
*
* @param int|string $index
*
* @throws InvalidArgumentException if the index is not defined
*/
public function get($index): JWK
{
if (!$this->has($index)) {
throw new InvalidArgumentException('Undefined index.');
}
return $this->keys[$index];
}
/**
* Returns the values to be serialized.
*/
public function jsonSerialize(): array
{
return ['keys' => array_values($this->keys)];
}
/**
* Returns the number of keys in the key set.
*
* @param int $mode
*/
public function count($mode = COUNT_NORMAL): int
{
return count($this->keys, $mode);
}
/**
* Try to find a key that fits on the selected requirements.
* Returns null if not found.
*
* @param string $type Must be 'sig' (signature) or 'enc' (encryption)
* @param null|Algorithm $algorithm Specifies the algorithm to be used
* @param array $restrictions More restrictions such as 'kid' or 'kty'
*
* @throws InvalidArgumentException if the key type is not valid (must be "sig" or "enc")
*/
public function selectKey(string $type, ?Algorithm $algorithm = null, array $restrictions = []): ?JWK
{
if (!in_array($type, ['enc', 'sig'], true)) {
throw new InvalidArgumentException('Allowed key types are "sig" or "enc".');
}
$result = [];
foreach ($this->keys as $key) {
$ind = 0;
$can_use = $this->canKeyBeUsedFor($type, $key);
if (false === $can_use) {
continue;
}
$ind += $can_use;
$alg = $this->canKeyBeUsedWithAlgorithm($algorithm, $key);
if (false === $alg) {
continue;
}
$ind += $alg;
if (false === $this->doesKeySatisfyRestrictions($restrictions, $key)) {
continue;
}
$result[] = ['key' => $key, 'ind' => $ind];
}
if (0 === count($result)) {
return null;
}
usort($result, [$this, 'sortKeys']);
return $result[0]['key'];
}
/**
* Internal method only. Should not be used.
*
* @internal
* @internal
*/
public static function sortKeys(array $a, array $b): int
{
if ($a['ind'] === $b['ind']) {
return 0;
}
return ($a['ind'] > $b['ind']) ? -1 : 1;
}
/**
* Internal method only. Should not be used.
*
* @internal
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->keys);
}
/**
* @throws InvalidArgumentException if the key does not fulfill with the "key_ops" constraint
*
* @return bool|int
*/
private function canKeyBeUsedFor(string $type, JWK $key)
{
if ($key->has('use')) {
return $type === $key->get('use') ? 1 : false;
}
if ($key->has('key_ops')) {
$key_ops = $key->get('key_ops');
if (!is_array($key_ops)) {
throw new InvalidArgumentException('Invalid key parameter "key_ops". Should be a list of key operations');
}
return $type === self::convertKeyOpsToKeyUse($key_ops) ? 1 : false;
}
return 0;
}
/**
* @return bool|int
*/
private function canKeyBeUsedWithAlgorithm(?Algorithm $algorithm, JWK $key)
{
if (null === $algorithm) {
return 0;
}
if (!in_array($key->get('kty'), $algorithm->allowedKeyTypes(), true)) {
return false;
}
if ($key->has('alg')) {
return $algorithm->name() === $key->get('alg') ? 2 : false;
}
return 1;
}
private function doesKeySatisfyRestrictions(array $restrictions, JWK $key): bool
{
foreach ($restrictions as $k => $v) {
if (!$key->has($k) || $v !== $key->get($k)) {
return false;
}
}
return true;
}
/**
* @throws InvalidArgumentException if the key operation is not supported
*/
private static function convertKeyOpsToKeyUse(array $key_ops): string
{
switch (true) {
case in_array('verify', $key_ops, true):
case in_array('sign', $key_ops, true):
return 'sig';
case in_array('encrypt', $key_ops, true):
case in_array('decrypt', $key_ops, true):
case in_array('wrapKey', $key_ops, true):
case in_array('unwrapKey', $key_ops, true):
case in_array('deriveKey', $key_ops, true):
case in_array('deriveBits', $key_ops, true):
return 'enc';
default:
throw new InvalidArgumentException(sprintf('Unsupported key operation value "%s"', implode(', ', $key_ops)));
}
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core;
interface JWT
{
/**
* Returns the payload of the JWT.
* null is a valid payload (e.g. JWS with detached payload).
*/
public function getPayload(): ?string;
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2019 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,15 @@
PHP JWT Core Component
======================
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
**Please do not submit any Pull Request here.**
You should go to [the main repository](https://github.com/web-token/jwt-framework) instead.
# Documentation
The official documentation is available as https://web-token.spomky-labs.com/
# Licence
This software is release under [MIT licence](LICENSE).

View File

@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core\Util;
use Brick\Math\BigInteger as BrickBigInteger;
use function chr;
use InvalidArgumentException;
/**
* @internal
*/
class BigInteger
{
/**
* Holds the BigInteger's value.
*
* @var BrickBigInteger
*/
private $value;
private function __construct(BrickBigInteger $value)
{
$this->value = $value;
}
/**
* @return BigInteger
*/
public static function createFromBinaryString(string $value): self
{
$res = unpack('H*', $value);
if (false === $res) {
throw new InvalidArgumentException('Unable to convert the value');
}
$data = current($res);
return new self(BrickBigInteger::fromBase($data, 16));
}
/**
* @return BigInteger
*/
public static function createFromDecimal(int $value): self
{
return new self(BrickBigInteger::of($value));
}
/**
* @return BigInteger
*/
public static function createFromBigInteger(BrickBigInteger $value): self
{
return new self($value);
}
/**
* Converts a BigInteger to a binary string.
*/
public function toBytes(): string
{
if ($this->value->isEqualTo(BrickBigInteger::zero())) {
return '';
}
$temp = $this->value->toBase(16);
$temp = 0 !== (mb_strlen($temp, '8bit') & 1) ? '0'.$temp : $temp;
$temp = hex2bin($temp);
if (false === $temp) {
throw new InvalidArgumentException('Unable to convert the value into bytes');
}
return ltrim($temp, chr(0));
}
/**
* Adds two BigIntegers.
*
* @param BigInteger $y
*
* @return BigInteger
*/
public function add(self $y): self
{
$value = $this->value->plus($y->value);
return new self($value);
}
/**
* Subtracts two BigIntegers.
*
* @param BigInteger $y
*
* @return BigInteger
*/
public function subtract(self $y): self
{
$value = $this->value->minus($y->value);
return new self($value);
}
/**
* Multiplies two BigIntegers.
*
* @param BigInteger $x
*
* @return BigInteger
*/
public function multiply(self $x): self
{
$value = $this->value->multipliedBy($x->value);
return new self($value);
}
/**
* Divides two BigIntegers.
*
* @param BigInteger $x
*
* @return BigInteger
*/
public function divide(self $x): self
{
$value = $this->value->dividedBy($x->value);
return new self($value);
}
/**
* Performs modular exponentiation.
*
* @param BigInteger $e
* @param BigInteger $n
*
* @return BigInteger
*/
public function modPow(self $e, self $n): self
{
$value = $this->value->modPow($e->value, $n->value);
return new self($value);
}
/**
* Performs modular exponentiation.
*
* @param BigInteger $d
*
* @return BigInteger
*/
public function mod(self $d): self
{
$value = $this->value->mod($d->value);
return new self($value);
}
public function modInverse(BigInteger $m): BigInteger
{
return new self($this->value->modInverse($m->value));
}
/**
* Compares two numbers.
*
* @param BigInteger $y
*/
public function compare(self $y): int
{
return $this->value->compareTo($y->value);
}
/**
* @param BigInteger $y
*/
public function equals(self $y): bool
{
return $this->value->isEqualTo($y->value);
}
/**
* @param BigInteger $y
*
* @return BigInteger
*/
public static function random(self $y): self
{
return new self(BrickBigInteger::randomRange(0, $y->value));
}
/**
* @param BigInteger $y
*
* @return BigInteger
*/
public function gcd(self $y): self
{
return new self($this->value->gcd($y->value));
}
/**
* @param BigInteger $y
*/
public function lowerThan(self $y): bool
{
return $this->value->isLessThan($y->value);
}
public function isEven(): bool
{
return $this->value->isEven();
}
public function get(): BrickBigInteger
{
return $this->value;
}
}

View File

@ -0,0 +1,366 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core\Util;
use Base64Url\Base64Url;
use function extension_loaded;
use InvalidArgumentException;
use function is_array;
use Jose\Component\Core\JWK;
use RuntimeException;
/**
* @internal
*/
class ECKey
{
public static function convertToPEM(JWK $jwk): string
{
if ($jwk->has('d')) {
return self::convertPrivateKeyToPEM($jwk);
}
return self::convertPublicKeyToPEM($jwk);
}
/**
* @throws InvalidArgumentException if the curve is not supported
*/
public static function convertPublicKeyToPEM(JWK $jwk): string
{
switch ($jwk->get('crv')) {
case 'P-256':
$der = self::p256PublicKey();
break;
case 'secp256k1':
$der = self::p256KPublicKey();
break;
case 'P-384':
$der = self::p384PublicKey();
break;
case 'P-521':
$der = self::p521PublicKey();
break;
default:
throw new InvalidArgumentException('Unsupported curve.');
}
$der .= self::getKey($jwk);
$pem = '-----BEGIN PUBLIC KEY-----'.PHP_EOL;
$pem .= chunk_split(base64_encode($der), 64, PHP_EOL);
$pem .= '-----END PUBLIC KEY-----'.PHP_EOL;
return $pem;
}
/**
* @throws InvalidArgumentException if the curve is not supported
*/
public static function convertPrivateKeyToPEM(JWK $jwk): string
{
switch ($jwk->get('crv')) {
case 'P-256':
$der = self::p256PrivateKey($jwk);
break;
case 'secp256k1':
$der = self::p256KPrivateKey($jwk);
break;
case 'P-384':
$der = self::p384PrivateKey($jwk);
break;
case 'P-521':
$der = self::p521PrivateKey($jwk);
break;
default:
throw new InvalidArgumentException('Unsupported curve.');
}
$der .= self::getKey($jwk);
$pem = '-----BEGIN EC PRIVATE KEY-----'.PHP_EOL;
$pem .= chunk_split(base64_encode($der), 64, PHP_EOL);
$pem .= '-----END EC PRIVATE KEY-----'.PHP_EOL;
return $pem;
}
/**
* Creates a EC key with the given curve and additional values.
*
* @param string $curve The curve
* @param array $values values to configure the key
*/
public static function createECKey(string $curve, array $values = []): JWK
{
$jwk = self::createECKeyUsingOpenSSL($curve);
$values = array_merge($values, $jwk);
return new JWK($values);
}
/**
* @throws InvalidArgumentException if the curve is not supported
*/
private static function getNistCurveSize(string $curve): int
{
switch ($curve) {
case 'P-256':
case 'secp256k1':
return 256;
case 'P-384':
return 384;
case 'P-521':
return 521;
default:
throw new InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve));
}
}
/**
* @throws RuntimeException if the extension OpenSSL is not available
* @throws RuntimeException if the key cannot be created
*/
private static function createECKeyUsingOpenSSL(string $curve): array
{
if (!extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
$key = openssl_pkey_new([
'curve_name' => self::getOpensslCurveName($curve),
'private_key_type' => OPENSSL_KEYTYPE_EC,
]);
if (false === $key) {
throw new RuntimeException('Unable to create the key');
}
$result = openssl_pkey_export($key, $out);
if (false === $result) {
throw new RuntimeException('Unable to create the key');
}
$res = openssl_pkey_get_private($out);
if (false === $res) {
throw new RuntimeException('Unable to create the key');
}
$details = openssl_pkey_get_details($res);
if (false === $details) {
throw new InvalidArgumentException('Unable to get the key details');
}
$nistCurveSize = self::getNistCurveSize($curve);
return [
'kty' => 'EC',
'crv' => $curve,
'd' => Base64Url::encode(str_pad($details['ec']['d'], (int) ceil($nistCurveSize / 8), "\0", STR_PAD_LEFT)),
'x' => Base64Url::encode(str_pad($details['ec']['x'], (int) ceil($nistCurveSize / 8), "\0", STR_PAD_LEFT)),
'y' => Base64Url::encode(str_pad($details['ec']['y'], (int) ceil($nistCurveSize / 8), "\0", STR_PAD_LEFT)),
];
}
/**
* @throws InvalidArgumentException if the curve is not supported
*/
private static function getOpensslCurveName(string $curve): string
{
switch ($curve) {
case 'P-256':
return 'prime256v1';
case 'secp256k1':
return 'secp256k1';
case 'P-384':
return 'secp384r1';
case 'P-521':
return 'secp521r1';
default:
throw new InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve));
}
}
private static function p256PublicKey(): string
{
return pack(
'H*',
'3059' // SEQUENCE, length 89
.'3013' // SEQUENCE, length 19
.'0607' // OID, length 7
.'2a8648ce3d0201' // 1.2.840.10045.2.1 = EC Public Key
.'0608' // OID, length 8
.'2a8648ce3d030107' // 1.2.840.10045.3.1.7 = P-256 Curve
.'0342' // BIT STRING, length 66
.'00' // prepend with NUL - pubkey will follow
);
}
private static function p256KPublicKey(): string
{
return pack(
'H*',
'3056' // SEQUENCE, length 86
.'3010' // SEQUENCE, length 16
.'0607' // OID, length 7
.'2a8648ce3d0201' // 1.2.840.10045.2.1 = EC Public Key
.'0605' // OID, length 8
.'2B8104000A' // 1.3.132.0.10 secp256k1
.'0342' // BIT STRING, length 66
.'00' // prepend with NUL - pubkey will follow
);
}
private static function p384PublicKey(): string
{
return pack(
'H*',
'3076' // SEQUENCE, length 118
.'3010' // SEQUENCE, length 16
.'0607' // OID, length 7
.'2a8648ce3d0201' // 1.2.840.10045.2.1 = EC Public Key
.'0605' // OID, length 5
.'2b81040022' // 1.3.132.0.34 = P-384 Curve
.'0362' // BIT STRING, length 98
.'00' // prepend with NUL - pubkey will follow
);
}
private static function p521PublicKey(): string
{
return pack(
'H*',
'30819b' // SEQUENCE, length 154
.'3010' // SEQUENCE, length 16
.'0607' // OID, length 7
.'2a8648ce3d0201' // 1.2.840.10045.2.1 = EC Public Key
.'0605' // OID, length 5
.'2b81040023' // 1.3.132.0.35 = P-521 Curve
.'038186' // BIT STRING, length 134
.'00' // prepend with NUL - pubkey will follow
);
}
private static function p256PrivateKey(JWK $jwk): string
{
$d = unpack('H*', str_pad(Base64Url::decode($jwk->get('d')), 32, "\0", STR_PAD_LEFT));
if (!is_array($d) || !isset($d[1])) {
throw new InvalidArgumentException('Unable to get the private key');
}
return pack(
'H*',
'3077' // SEQUENCE, length 87+length($d)=32
.'020101' // INTEGER, 1
.'0420' // OCTET STRING, length($d) = 32
.$d[1]
.'a00a' // TAGGED OBJECT #0, length 10
.'0608' // OID, length 8
.'2a8648ce3d030107' // 1.3.132.0.34 = P-256 Curve
.'a144' // TAGGED OBJECT #1, length 68
.'0342' // BIT STRING, length 66
.'00' // prepend with NUL - pubkey will follow
);
}
private static function p256KPrivateKey(JWK $jwk): string
{
$d = unpack('H*', str_pad(Base64Url::decode($jwk->get('d')), 32, "\0", STR_PAD_LEFT));
if (!is_array($d) || !isset($d[1])) {
throw new InvalidArgumentException('Unable to get the private key');
}
return pack(
'H*',
'3074' // SEQUENCE, length 84+length($d)=32
.'020101' // INTEGER, 1
.'0420' // OCTET STRING, length($d) = 32
.$d[1]
.'a007' // TAGGED OBJECT #0, length 7
.'0605' // OID, length 5
.'2b8104000a' // 1.3.132.0.10 secp256k1
.'a144' // TAGGED OBJECT #1, length 68
.'0342' // BIT STRING, length 66
.'00' // prepend with NUL - pubkey will follow
);
}
private static function p384PrivateKey(JWK $jwk): string
{
$d = unpack('H*', str_pad(Base64Url::decode($jwk->get('d')), 48, "\0", STR_PAD_LEFT));
if (!is_array($d) || !isset($d[1])) {
throw new InvalidArgumentException('Unable to get the private key');
}
return pack(
'H*',
'3081a4' // SEQUENCE, length 116 + length($d)=48
.'020101' // INTEGER, 1
.'0430' // OCTET STRING, length($d) = 30
.$d[1]
.'a007' // TAGGED OBJECT #0, length 7
.'0605' // OID, length 5
.'2b81040022' // 1.3.132.0.34 = P-384 Curve
.'a164' // TAGGED OBJECT #1, length 100
.'0362' // BIT STRING, length 98
.'00' // prepend with NUL - pubkey will follow
);
}
private static function p521PrivateKey(JWK $jwk): string
{
$d = unpack('H*', str_pad(Base64Url::decode($jwk->get('d')), 66, "\0", STR_PAD_LEFT));
if (!is_array($d) || !isset($d[1])) {
throw new InvalidArgumentException('Unable to get the private key');
}
return pack(
'H*',
'3081dc' // SEQUENCE, length 154 + length($d)=66
.'020101' // INTEGER, 1
.'0442' // OCTET STRING, length(d) = 66
.$d[1]
.'a007' // TAGGED OBJECT #0, length 7
.'0605' // OID, length 5
.'2b81040023' // 1.3.132.0.35 = P-521 Curve
.'a18189' // TAGGED OBJECT #1, length 137
.'038186' // BIT STRING, length 134
.'00' // prepend with NUL - pubkey will follow
);
}
private static function getKey(JWK $jwk): string
{
$nistCurveSize = self::getNistCurveSize($jwk->get('crv'));
$length = (int) ceil($nistCurveSize / 8);
return
"\04"
.str_pad(Base64Url::decode($jwk->get('x')), $length, "\0", STR_PAD_LEFT)
.str_pad(Base64Url::decode($jwk->get('y')), $length, "\0", STR_PAD_LEFT);
}
}

View File

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core\Util;
use InvalidArgumentException;
use function is_string;
use const STR_PAD_LEFT;
/**
* @internal
*/
final class ECSignature
{
private const ASN1_SEQUENCE = '30';
private const ASN1_INTEGER = '02';
private const ASN1_MAX_SINGLE_BYTE = 128;
private const ASN1_LENGTH_2BYTES = '81';
private const ASN1_BIG_INTEGER_LIMIT = '7f';
private const ASN1_NEGATIVE_INTEGER = '00';
private const BYTE_SIZE = 2;
/**
* @throws InvalidArgumentException if the length of the signature is invalid
*/
public static function toAsn1(string $signature, int $length): string
{
$signature = bin2hex($signature);
if (self::octetLength($signature) !== $length) {
throw new InvalidArgumentException('Invalid signature length.');
}
$pointR = self::preparePositiveInteger(mb_substr($signature, 0, $length, '8bit'));
$pointS = self::preparePositiveInteger(mb_substr($signature, $length, null, '8bit'));
$lengthR = self::octetLength($pointR);
$lengthS = self::octetLength($pointS);
$totalLength = $lengthR + $lengthS + self::BYTE_SIZE + self::BYTE_SIZE;
$lengthPrefix = $totalLength > self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : '';
$bin = hex2bin(
self::ASN1_SEQUENCE
.$lengthPrefix.dechex($totalLength)
.self::ASN1_INTEGER.dechex($lengthR).$pointR
.self::ASN1_INTEGER.dechex($lengthS).$pointS
);
if (!is_string($bin)) {
throw new InvalidArgumentException('Unable to parse the data');
}
return $bin;
}
/**
* @throws InvalidArgumentException if the signature is not an ASN.1 sequence
*/
public static function fromAsn1(string $signature, int $length): string
{
$message = bin2hex($signature);
$position = 0;
if (self::ASN1_SEQUENCE !== self::readAsn1Content($message, $position, self::BYTE_SIZE)) {
throw new InvalidArgumentException('Invalid data. Should start with a sequence.');
}
if (self::ASN1_LENGTH_2BYTES === self::readAsn1Content($message, $position, self::BYTE_SIZE)) {
$position += self::BYTE_SIZE;
}
$pointR = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));
$pointS = self::retrievePositiveInteger(self::readAsn1Integer($message, $position));
$bin = hex2bin(str_pad($pointR, $length, '0', STR_PAD_LEFT).str_pad($pointS, $length, '0', STR_PAD_LEFT));
if (!is_string($bin)) {
throw new InvalidArgumentException('Unable to parse the data');
}
return $bin;
}
private static function octetLength(string $data): int
{
return (int) (mb_strlen($data, '8bit') / self::BYTE_SIZE);
}
private static function preparePositiveInteger(string $data): string
{
if (mb_substr($data, 0, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) {
return self::ASN1_NEGATIVE_INTEGER.$data;
}
while (0 === mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit')
&& mb_substr($data, 2, self::BYTE_SIZE, '8bit') <= self::ASN1_BIG_INTEGER_LIMIT) {
$data = mb_substr($data, 2, null, '8bit');
}
return $data;
}
private static function readAsn1Content(string $message, int &$position, int $length): string
{
$content = mb_substr($message, $position, $length, '8bit');
$position += $length;
return $content;
}
/**
* @throws InvalidArgumentException if the data is not an integer
*/
private static function readAsn1Integer(string $message, int &$position): string
{
if (self::ASN1_INTEGER !== self::readAsn1Content($message, $position, self::BYTE_SIZE)) {
throw new InvalidArgumentException('Invalid data. Should contain an integer.');
}
$length = (int) hexdec(self::readAsn1Content($message, $position, self::BYTE_SIZE));
return self::readAsn1Content($message, $position, $length * self::BYTE_SIZE);
}
private static function retrievePositiveInteger(string $data): string
{
while (0 === mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit')
&& mb_substr($data, 2, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) {
$data = mb_substr($data, 2, null, '8bit');
}
return $data;
}
}

View File

@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core\Util;
/**
* @internal
*/
class Hash
{
/**
* Hash Parameter.
*
* @var string
*/
private $hash;
/**
* DER encoding T.
*
* @var string
*/
private $t;
/**
* Hash Length.
*
* @var int
*/
private $length;
private function __construct(string $hash, int $length, string $t)
{
$this->hash = $hash;
$this->length = $length;
$this->t = $t;
}
/**
* @return Hash
*/
public static function sha1(): self
{
return new self('sha1', 20, "\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14");
}
/**
* @return Hash
*/
public static function sha256(): self
{
return new self('sha256', 32, "\x30\x31\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x01\x05\x00\x04\x20");
}
/**
* @return Hash
*/
public static function sha384(): self
{
return new self('sha384', 48, "\x30\x41\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x02\x05\x00\x04\x30");
}
/**
* @return Hash
*/
public static function sha512(): self
{
return new self('sha512', 64, "\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40");
}
public function getLength(): int
{
return $this->length;
}
/**
* Compute the HMAC.
*/
public function hash(string $text): string
{
return hash($this->hash, $text, true);
}
public function name(): string
{
return $this->hash;
}
public function t(): string
{
return $this->t;
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core\Util;
use InvalidArgumentException;
use function is_string;
use RuntimeException;
use Throwable;
final class JsonConverter
{
/**
* @param mixed $payload
*
* @throws RuntimeException if the payload cannot be encoded
*/
public static function encode($payload): string
{
try {
$data = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
if (!is_string($data)) {
throw new InvalidArgumentException('Unable to encode the data');
}
return $data;
} catch (Throwable $throwable) {
throw new RuntimeException('Invalid content.', $throwable->getCode(), $throwable);
}
}
/**
* @throws RuntimeException if the payload cannot be decoded
*
* @return mixed
*/
public static function decode(string $payload)
{
try {
return json_decode($payload, true, 512, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Throwable $throwable) {
throw new RuntimeException('Invalid content.', $throwable->getCode(), $throwable);
}
}
}

View File

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core\Util;
use function in_array;
use InvalidArgumentException;
use function is_array;
use Jose\Component\Core\JWK;
/**
* @internal
*/
class KeyChecker
{
public static function checkKeyUsage(JWK $key, string $usage): void
{
if ($key->has('use')) {
self::checkUsage($key, $usage);
}
if ($key->has('key_ops')) {
self::checkOperation($key, $usage);
}
}
/**
* @throws InvalidArgumentException if the key is not suitable for the selected algorithm
*/
public static function checkKeyAlgorithm(JWK $key, string $algorithm): void
{
if (!$key->has('alg')) {
return;
}
if ($key->get('alg') !== $algorithm) {
throw new InvalidArgumentException(sprintf('Key is only allowed for algorithm "%s".', $key->get('alg')));
}
}
/**
* @throws InvalidArgumentException if the key is not suitable for the selected operation
*/
private static function checkOperation(JWK $key, string $usage): void
{
$ops = $key->get('key_ops');
if (!is_array($ops)) {
throw new InvalidArgumentException('Invalid key parameter "key_ops". Should be a list of key operations');
}
switch ($usage) {
case 'verification':
if (!in_array('verify', $ops, true)) {
throw new InvalidArgumentException('Key cannot be used to verify a signature');
}
break;
case 'signature':
if (!in_array('sign', $ops, true)) {
throw new InvalidArgumentException('Key cannot be used to sign');
}
break;
case 'encryption':
if (!in_array('encrypt', $ops, true) && !in_array('wrapKey', $ops, true) && !in_array('deriveKey', $ops, true)) {
throw new InvalidArgumentException('Key cannot be used to encrypt');
}
break;
case 'decryption':
if (!in_array('decrypt', $ops, true) && !in_array('unwrapKey', $ops, true) && !in_array('deriveBits', $ops, true)) {
throw new InvalidArgumentException('Key cannot be used to decrypt');
}
break;
default:
throw new InvalidArgumentException('Unsupported key usage.');
}
}
/**
* @throws InvalidArgumentException if the key is not suitable for the selected operation
*/
private static function checkUsage(JWK $key, string $usage): void
{
$use = $key->get('use');
switch ($usage) {
case 'verification':
case 'signature':
if ('sig' !== $use) {
throw new InvalidArgumentException('Key cannot be used to sign or verify a signature.');
}
break;
case 'encryption':
case 'decryption':
if ('enc' !== $use) {
throw new InvalidArgumentException('Key cannot be used to encrypt or decrypt.');
}
break;
default:
throw new InvalidArgumentException('Unsupported key usage.');
}
}
}

View File

@ -0,0 +1,315 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Core\Util;
use function array_key_exists;
use Base64Url\Base64Url;
use function count;
use FG\ASN1\Universal\BitString;
use FG\ASN1\Universal\Integer;
use FG\ASN1\Universal\NullObject;
use FG\ASN1\Universal\ObjectIdentifier;
use FG\ASN1\Universal\OctetString;
use FG\ASN1\Universal\Sequence;
use InvalidArgumentException;
use function is_array;
use Jose\Component\Core\JWK;
use RuntimeException;
/**
* @internal
*/
class RSAKey
{
/**
* @var Sequence
*/
private $sequence;
/**
* @var bool
*/
private $private;
/**
* @var array
*/
private $values;
/**
* @var BigInteger
*/
private $modulus;
/**
* @var int
*/
private $modulus_length;
/**
* @var BigInteger
*/
private $public_exponent;
/**
* @var null|BigInteger
*/
private $private_exponent;
/**
* @var BigInteger[]
*/
private $primes = [];
/**
* @var BigInteger[]
*/
private $exponents = [];
/**
* @var null|BigInteger
*/
private $coefficient;
private function __construct(JWK $data)
{
$this->values = $data->all();
$this->populateBigIntegers();
$this->private = array_key_exists('d', $this->values);
}
/**
* @return RSAKey
*/
public static function createFromJWK(JWK $jwk): self
{
return new self($jwk);
}
public function getModulus(): BigInteger
{
return $this->modulus;
}
public function getModulusLength(): int
{
return $this->modulus_length;
}
public function getExponent(): BigInteger
{
$d = $this->getPrivateExponent();
if (null !== $d) {
return $d;
}
return $this->getPublicExponent();
}
public function getPublicExponent(): BigInteger
{
return $this->public_exponent;
}
public function getPrivateExponent(): ?BigInteger
{
return $this->private_exponent;
}
/**
* @return BigInteger[]
*/
public function getPrimes(): array
{
return $this->primes;
}
/**
* @return BigInteger[]
*/
public function getExponents(): array
{
return $this->exponents;
}
public function getCoefficient(): ?BigInteger
{
return $this->coefficient;
}
public function isPublic(): bool
{
return !array_key_exists('d', $this->values);
}
/**
* @param RSAKey $private
*
* @return RSAKey
*/
public static function toPublic(self $private): self
{
$data = $private->toArray();
$keys = ['p', 'd', 'q', 'dp', 'dq', 'qi'];
foreach ($keys as $key) {
if (array_key_exists($key, $data)) {
unset($data[$key]);
}
}
return new self(new JWK($data));
}
public function toArray(): array
{
return $this->values;
}
public function toPEM(): string
{
if (null === $this->sequence) {
$this->sequence = new Sequence();
if (array_key_exists('d', $this->values)) {
$this->initPrivateKey();
} else {
$this->initPublicKey();
}
}
$result = '-----BEGIN '.($this->private ? 'RSA PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
$result .= chunk_split(base64_encode($this->sequence->getBinary()), 64, PHP_EOL);
$result .= '-----END '.($this->private ? 'RSA PRIVATE' : 'PUBLIC').' KEY-----'.PHP_EOL;
return $result;
}
/**
* Exponentiate with or without Chinese Remainder Theorem.
* Operation with primes 'p' and 'q' is appox. 2x faster.
*
* @param RSAKey $key
*
* @throws RuntimeException if the exponentiation cannot be achieved
*/
public static function exponentiate(self $key, BigInteger $c): BigInteger
{
if ($c->compare(BigInteger::createFromDecimal(0)) < 0 || $c->compare($key->getModulus()) > 0) {
throw new RuntimeException();
}
if ($key->isPublic() || null === $key->getCoefficient() || 0 === count($key->getPrimes()) || 0 === count($key->getExponents())) {
return $c->modPow($key->getExponent(), $key->getModulus());
}
$p = $key->getPrimes()[0];
$q = $key->getPrimes()[1];
$dP = $key->getExponents()[0];
$dQ = $key->getExponents()[1];
$qInv = $key->getCoefficient();
$m1 = $c->modPow($dP, $p);
$m2 = $c->modPow($dQ, $q);
$h = $qInv->multiply($m1->subtract($m2)->add($p))->mod($p);
return $m2->add($h->multiply($q));
}
private function populateBigIntegers(): void
{
$this->modulus = $this->convertBase64StringToBigInteger($this->values['n']);
$this->modulus_length = mb_strlen($this->getModulus()->toBytes(), '8bit');
$this->public_exponent = $this->convertBase64StringToBigInteger($this->values['e']);
if (!$this->isPublic()) {
$this->private_exponent = $this->convertBase64StringToBigInteger($this->values['d']);
if (array_key_exists('p', $this->values) && array_key_exists('q', $this->values)) {
$this->primes = [
$this->convertBase64StringToBigInteger($this->values['p']),
$this->convertBase64StringToBigInteger($this->values['q']),
];
if (array_key_exists('dp', $this->values) && array_key_exists('dq', $this->values) && array_key_exists('qi', $this->values)) {
$this->exponents = [
$this->convertBase64StringToBigInteger($this->values['dp']),
$this->convertBase64StringToBigInteger($this->values['dq']),
];
$this->coefficient = $this->convertBase64StringToBigInteger($this->values['qi']);
}
}
}
}
private function convertBase64StringToBigInteger(string $value): BigInteger
{
return BigInteger::createFromBinaryString(Base64Url::decode($value));
}
private function initPublicKey(): void
{
$oid_sequence = new Sequence();
$oid_sequence->addChild(new ObjectIdentifier('1.2.840.113549.1.1.1'));
$oid_sequence->addChild(new NullObject());
$this->sequence->addChild($oid_sequence);
$n = new Integer($this->fromBase64ToInteger($this->values['n']));
$e = new Integer($this->fromBase64ToInteger($this->values['e']));
$key_sequence = new Sequence();
$key_sequence->addChild($n);
$key_sequence->addChild($e);
$key_bit_string = new BitString(bin2hex($key_sequence->getBinary()));
$this->sequence->addChild($key_bit_string);
}
private function initPrivateKey(): void
{
$this->sequence->addChild(new Integer(0));
$oid_sequence = new Sequence();
$oid_sequence->addChild(new ObjectIdentifier('1.2.840.113549.1.1.1'));
$oid_sequence->addChild(new NullObject());
$this->sequence->addChild($oid_sequence);
$v = new Integer(0);
$n = new Integer($this->fromBase64ToInteger($this->values['n']));
$e = new Integer($this->fromBase64ToInteger($this->values['e']));
$d = new Integer($this->fromBase64ToInteger($this->values['d']));
$p = new Integer($this->fromBase64ToInteger($this->values['p']));
$q = new Integer($this->fromBase64ToInteger($this->values['q']));
$dp = array_key_exists('dp', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['dp'])) : new Integer(0);
$dq = array_key_exists('dq', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['dq'])) : new Integer(0);
$qi = array_key_exists('qi', $this->values) ? new Integer($this->fromBase64ToInteger($this->values['qi'])) : new Integer(0);
$key_sequence = new Sequence();
$key_sequence->addChild($v);
$key_sequence->addChild($n);
$key_sequence->addChild($e);
$key_sequence->addChild($d);
$key_sequence->addChild($p);
$key_sequence->addChild($q);
$key_sequence->addChild($dp);
$key_sequence->addChild($dq);
$key_sequence->addChild($qi);
$key_octet_string = new OctetString(bin2hex($key_sequence->getBinary()));
$this->sequence->addChild($key_octet_string);
}
/**
* @param string $value
*
* @return string
*/
private function fromBase64ToInteger($value)
{
$unpacked = unpack('H*', Base64Url::decode($value));
if (!is_array($unpacked) || 0 === count($unpacked)) {
throw new InvalidArgumentException('Unable to get the private key');
}
return \Brick\Math\BigInteger::fromBase(current($unpacked), 16)->toBase(10);
}
}

View File

@ -0,0 +1,33 @@
{
"name": "web-token/jwt-core",
"description": "Core component of the JWT Framework.",
"type": "library",
"license": "MIT",
"keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"],
"homepage": "https://github.com/web-token",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},{
"name": "All contributors",
"homepage": "https://github.com/web-token/jwt-framework/contributors"
}
],
"autoload": {
"psr-4": {
"Jose\\Component\\Core\\": ""
}
},
"require": {
"php": ">=7.2",
"ext-json": "*",
"ext-mbstring": "*",
"brick/math": "^0.8.17|^0.9",
"fgrosse/phpasn1": "^2.0",
"spomky-labs/base64url": "^1.0|^2.0"
},
"conflict": {
"spomky-labs/jose": "*"
}
}

View File

@ -0,0 +1,4 @@
# Contributing
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
Please do not submit any Pull Requests here. It will be automatically closed.

View File

@ -0,0 +1 @@
patreon: FlorentMorselli

View File

@ -0,0 +1,3 @@
Please do not submit any Pull Requests here. It will be automatically closed.
You should submit it here: https://github.com/web-token/jwt-framework/pulls

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Jose\Component\Core\JWK;
final class AlgorithmAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if (!$jwk->has('alg')) {
$bag->add(Message::medium('The parameter "alg" should be added.'));
}
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Base64Url\Base64Url;
use Brick\Math\BigInteger;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Ecc\NistCurve;
use RuntimeException;
final class ES256KeyAnalyzer implements KeyAnalyzer
{
/**
* @throws RuntimeException if the component "web-token/jwt-util-ecc" is missing
*/
public function __construct()
{
if (!class_exists(NistCurve::class)) {
throw new RuntimeException('Please install web-token/jwt-util-ecc to use this key analyzer');
}
}
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('EC' !== $jwk->get('kty')) {
return;
}
if (!$jwk->has('crv')) {
$bag->add(Message::high('Invalid key. The components "crv" is missing.'));
return;
}
if ('P-256' !== $jwk->get('crv')) {
return;
}
$x = Base64Url::decode($jwk->get('x'));
$xLength = 8 * mb_strlen($x, '8bit');
$y = Base64Url::decode($jwk->get('y'));
$yLength = 8 * mb_strlen($y, '8bit');
if ($yLength !== $xLength || 256 !== $yLength) {
$bag->add(Message::high('Invalid key. The components "x" and "y" size shall be 256 bits.'));
}
$xBI = BigInteger::fromBase(bin2hex($x), 16);
$yBI = BigInteger::fromBase(bin2hex($y), 16);
$curve = NistCurve::curve256();
if (!$curve->contains($xBI, $yBI)) {
$bag->add(Message::high('Invalid key. The point is not on the curve.'));
}
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Base64Url\Base64Url;
use Brick\Math\BigInteger;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Ecc\NistCurve;
use RuntimeException;
final class ES384KeyAnalyzer implements KeyAnalyzer
{
/**
* @throws RuntimeException if the component "web-token/jwt-util-ecc" is missing
*/
public function __construct()
{
if (!class_exists(NistCurve::class)) {
throw new RuntimeException('Please install web-token/jwt-util-ecc to use this key analyzer');
}
}
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('EC' !== $jwk->get('kty')) {
return;
}
if (!$jwk->has('crv')) {
$bag->add(Message::high('Invalid key. The components "crv" is missing.'));
return;
}
if ('P-384' !== $jwk->get('crv')) {
return;
}
$x = Base64Url::decode($jwk->get('x'));
$xLength = 8 * mb_strlen($x, '8bit');
$y = Base64Url::decode($jwk->get('y'));
$yLength = 8 * mb_strlen($y, '8bit');
if ($yLength !== $xLength || 384 !== $yLength) {
$bag->add(Message::high('Invalid key. The components "x" and "y" size shall be 384 bits.'));
}
$xBI = BigInteger::fromBase(bin2hex($x), 16);
$yBI = BigInteger::fromBase(bin2hex($y), 16);
$curve = NistCurve::curve384();
if (!$curve->contains($xBI, $yBI)) {
$bag->add(Message::high('Invalid key. The point is not on the curve.'));
}
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Base64Url\Base64Url;
use Brick\Math\BigInteger;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Ecc\NistCurve;
use RuntimeException;
final class ES512KeyAnalyzer implements KeyAnalyzer
{
/**
* @throws RuntimeException if the component "web-token/jwt-util-ecc" is missing
*/
public function __construct()
{
if (!class_exists(NistCurve::class)) {
throw new RuntimeException('Please install web-token/jwt-util-ecc to use this key analyzer');
}
}
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('EC' !== $jwk->get('kty')) {
return;
}
if (!$jwk->has('crv')) {
$bag->add(Message::high('Invalid key. The components "crv" is missing.'));
return;
}
if ('P-521' !== $jwk->get('crv')) {
return;
}
$x = Base64Url::decode($jwk->get('x'));
$xLength = 8 * mb_strlen($x, '8bit');
$y = Base64Url::decode($jwk->get('y'));
$yLength = 8 * mb_strlen($y, '8bit');
if ($yLength !== $xLength || 528 !== $yLength) {
$bag->add(Message::high('Invalid key. The components "x" and "y" size shall be 528 bits.'));
}
$xBI = BigInteger::fromBase(bin2hex($x), 16);
$yBI = BigInteger::fromBase(bin2hex($y), 16);
$curve = NistCurve::curve521();
if (!$curve->contains($xBI, $yBI)) {
$bag->add(Message::high('Invalid key. The point is not on the curve.'));
}
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Base64Url\Base64Url;
use Jose\Component\Core\JWK;
final class HS256KeyAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('oct' !== $jwk->get('kty')) {
return;
}
if (!$jwk->has('alg') || 'HS256' !== $jwk->get('alg')) {
return;
}
$k = Base64Url::decode($jwk->get('k'));
$kLength = 8 * mb_strlen($k, '8bit');
if ($kLength < 256) {
$bag->add(Message::high('HS256 algorithm requires at least 256 bits key length.'));
}
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Base64Url\Base64Url;
use Jose\Component\Core\JWK;
final class HS384KeyAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('oct' !== $jwk->get('kty')) {
return;
}
if (!$jwk->has('alg') || 'HS384' !== $jwk->get('alg')) {
return;
}
$k = Base64Url::decode($jwk->get('k'));
$kLength = 8 * mb_strlen($k, '8bit');
if ($kLength < 384) {
$bag->add(Message::high('HS384 algorithm requires at least 384 bits key length.'));
}
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Base64Url\Base64Url;
use Jose\Component\Core\JWK;
final class HS512KeyAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('oct' !== $jwk->get('kty')) {
return;
}
if (!$jwk->has('alg') || 'HS512' !== $jwk->get('alg')) {
return;
}
$k = Base64Url::decode($jwk->get('k'));
$kLength = 8 * mb_strlen($k, '8bit');
if ($kLength < 512) {
$bag->add(Message::high('HS512 algorithm requires at least 512 bits key length.'));
}
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Jose\Component\Core\JWK;
interface KeyAnalyzer
{
/**
* This method will analyse the key and add messages to the message bag if needed.
*/
public function analyze(JWK $jwk, MessageBag $bag): void;
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Jose\Component\Core\JWK;
class KeyAnalyzerManager
{
/**
* @var KeyAnalyzer[]
*/
private $analyzers = [];
/**
* Adds a Key Analyzer to the manager.
*/
public function add(KeyAnalyzer $analyzer): void
{
$this->analyzers[] = $analyzer;
}
/**
* This method will analyze the JWK object using all analyzers.
* It returns a message bag that may contains messages.
*/
public function analyze(JWK $jwk): MessageBag
{
$bag = new MessageBag();
foreach ($this->analyzers as $analyzer) {
$analyzer->analyze($jwk, $bag);
}
return $bag;
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Jose\Component\Core\JWK;
final class KeyIdentifierAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if (!$jwk->has('kid')) {
$bag->add(Message::medium('The parameter "kid" should be added.'));
}
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Jose\Component\Core\JWKSet;
interface KeysetAnalyzer
{
/**
* This method will analyse the key set and add messages to the message bag if needed.
*/
public function analyze(JWKSet $JWKSet, MessageBag $bag): void;
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Jose\Component\Core\JWKSet;
class KeysetAnalyzerManager
{
/**
* @var KeysetAnalyzer[]
*/
private $analyzers = [];
/**
* Adds a Keyset Analyzer to the manager.
*/
public function add(KeysetAnalyzer $analyzer): void
{
$this->analyzers[] = $analyzer;
}
/**
* This method will analyze the JWKSet object using all analyzers.
* It returns a message bag that may contains messages.
*/
public function analyze(JWKSet $jwkset): MessageBag
{
$bag = new MessageBag();
foreach ($this->analyzers as $analyzer) {
$analyzer->analyze($jwkset, $bag);
}
return $bag;
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use JsonSerializable;
class Message implements JsonSerializable
{
public const SEVERITY_LOW = 'low';
public const SEVERITY_MEDIUM = 'medium';
public const SEVERITY_HIGH = 'high';
/**
* @var string
*/
private $message;
/**
* @var string
*/
private $severity;
/**
* Message constructor.
*/
private function __construct(string $message, string $severity)
{
$this->message = $message;
$this->severity = $severity;
}
/**
* Creates a message with severity=low.
*
* @return Message
*/
public static function low(string $message): self
{
return new self($message, self::SEVERITY_LOW);
}
/**
* Creates a message with severity=medium.
*
* @return Message
*/
public static function medium(string $message): self
{
return new self($message, self::SEVERITY_MEDIUM);
}
/**
* Creates a message with severity=high.
*
* @return Message
*/
public static function high(string $message): self
{
return new self($message, self::SEVERITY_HIGH);
}
/**
* Returns the message.
*/
public function getMessage(): string
{
return $this->message;
}
/**
* Returns the severity of the message.
*/
public function getSeverity(): string
{
return $this->severity;
}
public function jsonSerialize(): array
{
return [
'message' => $this->message,
'severity' => $this->severity,
];
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use ArrayIterator;
use function count;
use Countable;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
class MessageBag implements JsonSerializable, IteratorAggregate, Countable
{
/**
* @var Message[]
*/
private $messages = [];
/**
* Adds a message to the message bag.
*/
public function add(Message $message): void
{
$this->messages[] = $message;
}
/**
* Returns all messages.
*
* @return Message[]
*/
public function all(): array
{
return $this->messages;
}
/**
* {@inheritdoc}
*/
public function jsonSerialize(): array
{
return array_values($this->messages);
}
/**
* {@inheritdoc}
*/
public function count(): int
{
return count($this->messages);
}
/**
* {@inheritdoc}
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->messages);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Jose\Component\Core\JWKSet;
final class MixedKeyTypes implements KeysetAnalyzer
{
public function analyze(JWKSet $jwkset, MessageBag $bag): void
{
if (0 === $jwkset->count()) {
return;
}
$hasSymmetricKeys = false;
$hasAsymmetricKeys = false;
foreach ($jwkset as $jwk) {
switch ($jwk->get('kty')) {
case 'oct':
$hasSymmetricKeys = true;
break;
case 'OKP':
case 'RSA':
case 'EC':
$hasAsymmetricKeys = true;
break;
}
}
if ($hasAsymmetricKeys && $hasSymmetricKeys) {
$bag->add(Message::medium('This key set mixes symmetric and assymetric keys.'));
}
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Jose\Component\Core\JWKSet;
final class MixedPublicAndPrivateKeys implements KeysetAnalyzer
{
public function analyze(JWKSet $jwkset, MessageBag $bag): void
{
if (0 === $jwkset->count()) {
return;
}
$hasPublicKeys = false;
$hasPrivateKeys = false;
foreach ($jwkset as $jwk) {
switch ($jwk->get('kty')) {
case 'OKP':
case 'RSA':
case 'EC':
if ($jwk->has('d')) {
$hasPrivateKeys = true;
} else {
$hasPublicKeys = true;
}
break;
}
}
if ($hasPrivateKeys && $hasPublicKeys) {
$bag->add(Message::high('This key set mixes public and private keys.'));
}
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Jose\Component\Core\JWK;
final class NoneAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('none' !== $jwk->get('kty')) {
return;
}
$bag->add(Message::high('This key is a meant to be used with the algorithm "none". This algorithm is not secured and should be used with care.'));
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Base64Url\Base64Url;
use Jose\Component\Core\JWK;
final class OctAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('oct' !== $jwk->get('kty')) {
return;
}
$k = Base64Url::decode($jwk->get('k'));
$kLength = 8 * mb_strlen($k, '8bit');
if ($kLength < 128) {
$bag->add(Message::high('The key length is less than 128 bits.'));
}
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Base64Url\Base64Url;
use InvalidArgumentException;
use function is_array;
use Jose\Component\Core\JWK;
final class RsaAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('RSA' !== $jwk->get('kty')) {
return;
}
$this->checkExponent($jwk, $bag);
$this->checkModulus($jwk, $bag);
}
private function checkExponent(JWK $jwk, MessageBag $bag): void
{
$exponent = unpack('l', str_pad(Base64Url::decode($jwk->get('e')), 4, "\0"));
if (!is_array($exponent) || !isset($exponent[1])) {
throw new InvalidArgumentException('Unable to get the private key');
}
if ($exponent[1] < 65537) {
$bag->add(Message::high('The exponent is too low. It should be at least 65537.'));
}
}
private function checkModulus(JWK $jwk, MessageBag $bag): void
{
$n = 8 * mb_strlen(Base64Url::decode($jwk->get('n')), '8bit');
if ($n < 2048) {
$bag->add(Message::high('The key length is less than 2048 bits.'));
}
if ($jwk->has('d') && (!$jwk->has('p') || !$jwk->has('q') || !$jwk->has('dp') || !$jwk->has('dq') || !$jwk->has('p') || !$jwk->has('qi'))) {
$bag->add(Message::medium('The key is a private RSA key, but Chinese Remainder Theorem primes are missing. These primes are not mandatory, but signatures and decryption processes are faster when available.'));
}
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use function in_array;
use Jose\Component\Core\JWK;
final class UsageAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if (!$jwk->has('use')) {
$bag->add(Message::medium('The parameter "use" should be added.'));
} elseif (!in_array($jwk->get('use'), ['sig', 'enc'], true)) {
$bag->add(Message::high(sprintf('The parameter "use" has an unsupported value "%s". Please use "sig" (signature) or "enc" (encryption).', $jwk->get('use'))));
}
if ($jwk->has('key_ops') && !in_array($jwk->get('key_ops'), ['sign', 'verify', 'encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], true)) {
$bag->add(Message::high(sprintf('The parameter "key_ops" has an unsupported value "%s". Please use one of the following values: %s.', $jwk->get('use'), implode(', ', ['verify', 'sign', 'encryp', 'decrypt', 'wrapKey', 'unwrapKey']))));
}
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\Analyzer;
use Base64Url\Base64Url;
use Jose\Component\Core\JWK;
use ZxcvbnPhp\Zxcvbn;
final class ZxcvbnKeyAnalyzer implements KeyAnalyzer
{
public function analyze(JWK $jwk, MessageBag $bag): void
{
if ('oct' !== $jwk->get('kty')) {
return;
}
$k = Base64Url::decode($jwk->get('k'));
if (class_exists(Zxcvbn::class)) {
$zxcvbn = new Zxcvbn();
$strength = $zxcvbn->passwordStrength($k);
switch (true) {
case $strength['score'] < 3:
$bag->add(Message::high('The octet string is weak and easily guessable. Please change your key as soon as possible.'));
break;
case 3 === $strength['score']:
$bag->add(Message::medium('The octet string is safe, but a longer key is preferable.'));
break;
default:
break;
}
}
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement;
use function is_array;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\JsonConverter;
use RuntimeException;
class JKUFactory extends UrlKeySetFactory
{
/**
* This method will try to fetch the url a retrieve the key set.
* Throws an exception in case of failure.
*
* @throws RuntimeException if the key cannot be reached
*/
public function loadFromUrl(string $url, array $header = []): JWKSet
{
$content = $this->getContent($url, $header);
$data = JsonConverter::decode($content);
if (!is_array($data)) {
throw new RuntimeException('Invalid content.');
}
return JWKSet::createFromKeyData($data);
}
}

View File

@ -0,0 +1,328 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement;
use function array_key_exists;
use Base64Url\Base64Url;
use function extension_loaded;
use InvalidArgumentException;
use function is_array;
use function is_string;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\ECKey;
use Jose\Component\KeyManagement\KeyConverter\KeyConverter;
use Jose\Component\KeyManagement\KeyConverter\RSAKey;
use RuntimeException;
use Throwable;
class JWKFactory
{
/**
* Creates a RSA key with the given key size and additional values.
*
* @param int $size The key size in bits
* @param array $values values to configure the key
*
* @throws InvalidArgumentException if the key has an invalid size
* @throws InvalidArgumentException if it is not possible to create the key
*/
public static function createRSAKey(int $size, array $values = []): JWK
{
if (0 !== $size % 8) {
throw new InvalidArgumentException('Invalid key size.');
}
if (512 > $size) {
throw new InvalidArgumentException('Key length is too short. It needs to be at least 512 bits.');
}
$key = openssl_pkey_new([
'private_key_bits' => $size,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
if (false === $key) {
throw new InvalidArgumentException('Unable to create the key');
}
$details = openssl_pkey_get_details($key);
if (!is_array($details)) {
throw new InvalidArgumentException('Unable to create the key');
}
$rsa = RSAKey::createFromKeyDetails($details['rsa']);
$values = array_merge(
$values,
$rsa->toArray()
);
return new JWK($values);
}
/**
* Creates a EC key with the given curve and additional values.
*
* @param string $curve The curve
* @param array $values values to configure the key
*/
public static function createECKey(string $curve, array $values = []): JWK
{
return ECKey::createECKey($curve, $values);
}
/**
* Creates a octet key with the given key size and additional values.
*
* @param int $size The key size in bits
* @param array $values values to configure the key
*
* @throws InvalidArgumentException if the key has an invalid size
*/
public static function createOctKey(int $size, array $values = []): JWK
{
if (0 !== $size % 8) {
throw new InvalidArgumentException('Invalid key size.');
}
$values = array_merge(
$values,
[
'kty' => 'oct',
'k' => Base64Url::encode(random_bytes($size / 8)),
]
);
return new JWK($values);
}
/**
* Creates a OKP key with the given curve and additional values.
*
* @param string $curve The curve
* @param array $values values to configure the key
*
* @throws InvalidArgumentException if the extension "sobium" is not available
* @throws InvalidArgumentException if the curve is not supported
*/
public static function createOKPKey(string $curve, array $values = []): JWK
{
if (!extension_loaded('sodium')) {
throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method');
}
switch ($curve) {
case 'X25519':
$keyPair = sodium_crypto_box_keypair();
$secret = sodium_crypto_box_secretkey($keyPair);
$x = sodium_crypto_box_publickey($keyPair);
break;
case 'Ed25519':
$keyPair = sodium_crypto_sign_keypair();
$secret = sodium_crypto_sign_secretkey($keyPair);
$x = sodium_crypto_sign_publickey($keyPair);
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve));
}
$secretLength = mb_strlen($secret, '8bit');
$d = mb_substr($secret, 0, -$secretLength / 2, '8bit');
$values = array_merge(
$values,
[
'kty' => 'OKP',
'crv' => $curve,
'd' => Base64Url::encode($d),
'x' => Base64Url::encode($x),
]
);
return new JWK($values);
}
/**
* Creates a none key with the given additional values.
* Please note that this key type is not pat of any specification.
* It is used to prevent the use of the "none" algorithm with other key types.
*
* @param array $values values to configure the key
*/
public static function createNoneKey(array $values = []): JWK
{
$values = array_merge(
$values,
[
'kty' => 'none',
'alg' => 'none',
'use' => 'sig',
]
);
return new JWK($values);
}
/**
* Creates a key from a Json string.
*
* @throws InvalidArgumentException if the key or keyset is not valid
*
* @return JWK|JWKSet
*/
public static function createFromJsonObject(string $value)
{
$json = json_decode($value, true);
if (!is_array($json)) {
throw new InvalidArgumentException('Invalid key or key set.');
}
return self::createFromValues($json);
}
/**
* Creates a key or key set from the given input.
*
* @return JWK|JWKSet
*/
public static function createFromValues(array $values)
{
if (array_key_exists('keys', $values) && is_array($values['keys'])) {
return JWKSet::createFromKeyData($values);
}
return new JWK($values);
}
/**
* This method create a JWK object using a shared secret.
*/
public static function createFromSecret(string $secret, array $additional_values = []): JWK
{
$values = array_merge(
$additional_values,
[
'kty' => 'oct',
'k' => Base64Url::encode($secret),
]
);
return new JWK($values);
}
/**
* This method will try to load a X.509 certificate and convert it into a public key.
*/
public static function createFromCertificateFile(string $file, array $additional_values = []): JWK
{
$values = KeyConverter::loadKeyFromCertificateFile($file);
$values = array_merge($values, $additional_values);
return new JWK($values);
}
/**
* Extract a keyfrom a key set identified by the given index .
*
* @param int|string $index
*/
public static function createFromKeySet(JWKSet $jwkset, $index): JWK
{
return $jwkset->get($index);
}
/**
* This method will try to load a PKCS#12 file and convert it into a public key.
*
* @throws InvalidArgumentException if the certificate cannot be loaded
*/
public static function createFromPKCS12CertificateFile(string $file, ?string $secret = '', array $additional_values = []): JWK
{
try {
$content = file_get_contents($file);
if (!is_string($content)) {
throw new RuntimeException('Unable to read the file.');
}
openssl_pkcs12_read($content, $certs, $secret);
} catch (Throwable $throwable) {
throw new RuntimeException('Unable to load the certificates.', $throwable->getCode(), $throwable);
}
if (!is_array($certs) || !array_key_exists('pkey', $certs)) {
throw new RuntimeException('Unable to load the certificates.');
}
return self::createFromKey($certs['pkey'], null, $additional_values);
}
/**
* This method will try to convert a X.509 certificate into a public key.
*/
public static function createFromCertificate(string $certificate, array $additional_values = []): JWK
{
$values = KeyConverter::loadKeyFromCertificate($certificate);
$values = array_merge($values, $additional_values);
return new JWK($values);
}
/**
* This method will try to convert a X.509 certificate resource into a public key.
*
* @param resource $res
*/
public static function createFromX509Resource($res, array $additional_values = []): JWK
{
$values = KeyConverter::loadKeyFromX509Resource($res);
$values = array_merge($values, $additional_values);
return new JWK($values);
}
/**
* This method will try to load and convert a key file into a JWK object.
* If the key is encrypted, the password must be set.
*/
public static function createFromKeyFile(string $file, ?string $password = null, array $additional_values = []): JWK
{
$values = KeyConverter::loadFromKeyFile($file, $password);
$values = array_merge($values, $additional_values);
return new JWK($values);
}
/**
* This method will try to load and convert a key into a JWK object.
* If the key is encrypted, the password must be set.
*/
public static function createFromKey(string $key, ?string $password = null, array $additional_values = []): JWK
{
$values = KeyConverter::loadFromKey($key, $password);
$values = array_merge($values, $additional_values);
return new JWK($values);
}
/**
* This method will try to load and convert a X.509 certificate chain into a public key.
*
* Be careful! The certificate chain is loaded, but it is NOT VERIFIED by any mean!
* It is mandatory to verify the root CA or intermediate CA are trusted.
* If not done, it may lead to potential security issues.
*/
public static function createFromX5C(array $x5c, array $additional_values = []): JWK
{
$values = KeyConverter::loadFromX5C($x5c);
$values = array_merge($values, $additional_values);
return new JWK($values);
}
}

View File

@ -0,0 +1,307 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\KeyConverter;
use function array_key_exists;
use Base64Url\Base64Url;
use function count;
use FG\ASN1\ASNObject;
use FG\ASN1\Exception\ParserException;
use FG\ASN1\ExplicitlyTaggedObject;
use FG\ASN1\Universal\BitString;
use FG\ASN1\Universal\Integer;
use FG\ASN1\Universal\ObjectIdentifier;
use FG\ASN1\Universal\OctetString;
use FG\ASN1\Universal\Sequence;
use InvalidArgumentException;
use function is_array;
use function is_string;
/**
* @internal
*/
class ECKey
{
/**
* @var array
*/
private $values = [];
private function __construct(array $data)
{
$this->loadJWK($data);
}
public static function createFromPEM(string $pem): self
{
$data = self::loadPEM($pem);
return new self($data);
}
/**
* @param ECKey $private
*
* @return ECKey
*/
public static function toPublic(self $private): self
{
$data = $private->toArray();
if (array_key_exists('d', $data)) {
unset($data['d']);
}
return new self($data);
}
/**
* @return array
*/
public function toArray()
{
return $this->values;
}
/**
* @throws InvalidArgumentException if the key cannot be loaded
* @throws ParserException if the key cannot be loaded
*/
private static function loadPEM(string $data): array
{
$data = base64_decode(preg_replace('#-.*-|\r|\n#', '', $data), true);
$asnObject = ASNObject::fromBinary($data);
if (!$asnObject instanceof Sequence) {
throw new InvalidArgumentException('Unable to load the key.');
}
$children = $asnObject->getChildren();
if (self::isPKCS8($children)) {
$children = self::loadPKCS8($children);
}
if (4 === count($children)) {
return self::loadPrivatePEM($children);
}
if (2 === count($children)) {
return self::loadPublicPEM($children);
}
throw new InvalidArgumentException('Unable to load the key.');
}
/**
* @param ASNObject[] $children
*
* @throws InvalidArgumentException if the key cannot be loaded
* @throws ParserException if the key cannot be loaded
*/
private static function loadPKCS8(array $children): array
{
$binary = hex2bin($children[2]->getContent());
$asnObject = ASNObject::fromBinary($binary);
if (!$asnObject instanceof Sequence) {
throw new InvalidArgumentException('Unable to load the key.');
}
return $asnObject->getChildren();
}
/**
* @throws InvalidArgumentException if the key cannot be loaded
*/
private static function loadPublicPEM(array $children): array
{
if (!$children[0] instanceof Sequence) {
throw new InvalidArgumentException('Unsupported key type.');
}
$sub = $children[0]->getChildren();
if (!$sub[0] instanceof ObjectIdentifier) {
throw new InvalidArgumentException('Unsupported key type.');
}
if ('1.2.840.10045.2.1' !== $sub[0]->getContent()) {
throw new InvalidArgumentException('Unsupported key type.');
}
if (!$sub[1] instanceof ObjectIdentifier) {
throw new InvalidArgumentException('Unsupported key type.');
}
if (!$children[1] instanceof BitString) {
throw new InvalidArgumentException('Unable to load the key.');
}
$bits = $children[1]->getContent();
$bits_length = mb_strlen($bits, '8bit');
if (0 !== mb_strpos($bits, '04', 0, '8bit')) {
throw new InvalidArgumentException('Unsupported key type');
}
$values = ['kty' => 'EC'];
$values['crv'] = self::getCurve($sub[1]->getContent());
$xBin = hex2bin(mb_substr($bits, 2, ($bits_length - 2) / 2, '8bit'));
$yBin = hex2bin(mb_substr($bits, (int) (($bits_length - 2) / 2 + 2), ($bits_length - 2) / 2, '8bit'));
if (!is_string($xBin) || !is_string($yBin)) {
throw new InvalidArgumentException('Unable to load the key.');
}
$values['x'] = Base64Url::encode($xBin);
$values['y'] = Base64Url::encode($yBin);
return $values;
}
/**
* @throws InvalidArgumentException if the OID is not supported
*/
private static function getCurve(string $oid): string
{
$curves = self::getSupportedCurves();
$curve = array_search($oid, $curves, true);
if (!is_string($curve)) {
throw new InvalidArgumentException('Unsupported OID.');
}
return $curve;
}
private static function getSupportedCurves(): array
{
return [
'P-256' => '1.2.840.10045.3.1.7',
'P-384' => '1.3.132.0.34',
'P-521' => '1.3.132.0.35',
];
}
/**
* @throws InvalidArgumentException if the key cannot be loaded
*/
private static function verifyVersion(ASNObject $children): void
{
if (!$children instanceof Integer || '1' !== $children->getContent()) {
throw new InvalidArgumentException('Unable to load the key.');
}
}
/**
* @throws InvalidArgumentException if the key cannot be loaded
*/
private static function getXAndY(ASNObject $children, string &$x, string &$y): void
{
if (!$children instanceof ExplicitlyTaggedObject || !is_array($children->getContent())) {
throw new InvalidArgumentException('Unable to load the key.');
}
if (!$children->getContent()[0] instanceof BitString) {
throw new InvalidArgumentException('Unable to load the key.');
}
$bits = $children->getContent()[0]->getContent();
$bits_length = mb_strlen($bits, '8bit');
if (0 !== mb_strpos($bits, '04', 0, '8bit')) {
throw new InvalidArgumentException('Unsupported key type');
}
$x = mb_substr($bits, 2, (int) (($bits_length - 2) / 2), '8bit');
$y = mb_substr($bits, (int) (($bits_length - 2) / 2 + 2), (int) (($bits_length - 2) / 2), '8bit');
}
/**
* @throws InvalidArgumentException if the key cannot be loaded
*/
private static function getD(ASNObject $children): string
{
if (!$children instanceof OctetString) {
throw new InvalidArgumentException('Unable to load the key.');
}
return $children->getContent();
}
/**
* @throws InvalidArgumentException if the key cannot be loaded
*/
private static function loadPrivatePEM(array $children): array
{
self::verifyVersion($children[0]);
$x = '';
$y = '';
$d = self::getD($children[1]);
self::getXAndY($children[3], $x, $y);
if (!$children[2] instanceof ExplicitlyTaggedObject || !is_array($children[2]->getContent())) {
throw new InvalidArgumentException('Unable to load the key.');
}
if (!$children[2]->getContent()[0] instanceof ObjectIdentifier) {
throw new InvalidArgumentException('Unable to load the key.');
}
$curve = $children[2]->getContent()[0]->getContent();
$dBin = hex2bin($d);
$xBin = hex2bin($x);
$yBin = hex2bin($y);
if (!is_string($dBin) || !is_string($xBin) || !is_string($yBin)) {
throw new InvalidArgumentException('Unable to load the key.');
}
$values = ['kty' => 'EC'];
$values['crv'] = self::getCurve($curve);
$values['d'] = Base64Url::encode($dBin);
$values['x'] = Base64Url::encode($xBin);
$values['y'] = Base64Url::encode($yBin);
return $values;
}
/**
* @param ASNObject[] $children
*/
private static function isPKCS8(array $children): bool
{
if (3 !== count($children)) {
return false;
}
$classes = [0 => Integer::class, 1 => Sequence::class, 2 => OctetString::class];
foreach ($classes as $k => $class) {
if (!$children[$k] instanceof $class) {
return false;
}
}
return true;
}
/**
* @throws InvalidArgumentException if the key is invalid
*/
private function loadJWK(array $jwk): void
{
$keys = [
'kty' => 'The key parameter "kty" is missing.',
'crv' => 'Curve parameter is missing',
'x' => 'Point parameters are missing.',
'y' => 'Point parameters are missing.',
];
foreach ($keys as $k => $v) {
if (!array_key_exists($k, $jwk)) {
throw new InvalidArgumentException($v);
}
}
if ('EC' !== $jwk['kty']) {
throw new InvalidArgumentException('JWK is not an Elliptic Curve key.');
}
$this->values = $jwk;
}
}

View File

@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\KeyConverter;
use function array_key_exists;
use Base64Url\Base64Url;
use function count;
use function extension_loaded;
use InvalidArgumentException;
use function is_array;
use function is_string;
use RuntimeException;
use Throwable;
/**
* @internal
*/
class KeyConverter
{
/**
* @throws InvalidArgumentException if the certificate file cannot be read
*/
public static function loadKeyFromCertificateFile(string $file): array
{
if (!file_exists($file)) {
throw new InvalidArgumentException(sprintf('File "%s" does not exist.', $file));
}
$content = file_get_contents($file);
if (!is_string($content)) {
throw new InvalidArgumentException(sprintf('File "%s" cannot be read.', $file));
}
return self::loadKeyFromCertificate($content);
}
/**
* @throws InvalidArgumentException if the OpenSSL extension is not available
* @throws InvalidArgumentException if the certificate is invalid or cannot be loaded
*/
public static function loadKeyFromCertificate(string $certificate): array
{
if (!extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
try {
$res = openssl_x509_read($certificate);
if (false === $res) {
throw new InvalidArgumentException('Unable to load the certificate.');
}
} catch (Throwable $e) {
$certificate = self::convertDerToPem($certificate);
$res = openssl_x509_read($certificate);
}
if (false === $res) {
throw new InvalidArgumentException('Unable to load the certificate.');
}
return self::loadKeyFromX509Resource($res);
}
/**
* @param resource $res
*
* @throws InvalidArgumentException if the OpenSSL extension is not available
* @throws InvalidArgumentException if the certificate is invalid or cannot be loaded
*/
public static function loadKeyFromX509Resource($res): array
{
if (!extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
$key = openssl_get_publickey($res);
if (false === $key) {
throw new InvalidArgumentException('Unable to load the certificate.');
}
$details = openssl_pkey_get_details($key);
if (!is_array($details)) {
throw new InvalidArgumentException('Unable to load the certificate');
}
if (isset($details['key'])) {
$values = self::loadKeyFromPEM($details['key']);
openssl_x509_export($res, $out);
$x5c = preg_replace('#-.*-#', '', $out);
$x5c = preg_replace('~\R~', PHP_EOL, $x5c);
if (!is_string($x5c)) {
throw new InvalidArgumentException('Unable to load the certificate');
}
$x5c = trim($x5c);
$x5tsha1 = openssl_x509_fingerprint($res, 'sha1', true);
$x5tsha256 = openssl_x509_fingerprint($res, 'sha256', true);
if (!is_string($x5tsha1) || !is_string($x5tsha256)) {
throw new InvalidArgumentException('Unable to compute the certificate fingerprint');
}
$values['x5c'] = [$x5c];
$values['x5t'] = Base64Url::encode($x5tsha1);
$values['x5t#256'] = Base64Url::encode($x5tsha256);
return $values;
}
throw new InvalidArgumentException('Unable to load the certificate');
}
public static function loadFromKeyFile(string $file, ?string $password = null): array
{
$content = file_get_contents($file);
if (!is_string($content)) {
throw new InvalidArgumentException('Unable to load the key from the file.');
}
return self::loadFromKey($content, $password);
}
public static function loadFromKey(string $key, ?string $password = null): array
{
try {
return self::loadKeyFromDER($key, $password);
} catch (Throwable $e) {
return self::loadKeyFromPEM($key, $password);
}
}
/**
* Be careful! The certificate chain is loaded, but it is NOT VERIFIED by any mean!
* It is mandatory to verify the root CA or intermediate CA are trusted.
* If not done, it may lead to potential security issues.
*
* @throws InvalidArgumentException if the certificate chain is empty
* @throws InvalidArgumentException if the OpenSSL extension is not available
*/
public static function loadFromX5C(array $x5c): array
{
if (0 === count($x5c)) {
throw new InvalidArgumentException('The certificate chain is empty');
}
foreach ($x5c as $id => $cert) {
$x5c[$id] = '-----BEGIN CERTIFICATE-----'.PHP_EOL.chunk_split($cert, 64, PHP_EOL).'-----END CERTIFICATE-----';
$x509 = openssl_x509_read($x5c[$id]);
if (false === $x509) {
throw new InvalidArgumentException('Unable to load the certificate chain');
}
$parsed = openssl_x509_parse($x509);
if (false === $parsed) {
throw new InvalidArgumentException('Unable to load the certificate chain');
}
}
return self::loadKeyFromCertificate(reset($x5c));
}
private static function loadKeyFromDER(string $der, ?string $password = null): array
{
$pem = self::convertDerToPem($der);
return self::loadKeyFromPEM($pem, $password);
}
/**
* @throws InvalidArgumentException if the OpenSSL extension is not available
* @throws InvalidArgumentException if the key cannot be loaded
*/
private static function loadKeyFromPEM(string $pem, ?string $password = null): array
{
if (1 === preg_match('#DEK-Info: (.+),(.+)#', $pem, $matches)) {
$pem = self::decodePem($pem, $matches, $password);
}
if (!extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
self::sanitizePEM($pem);
$res = openssl_pkey_get_private($pem);
if (false === $res) {
$res = openssl_pkey_get_public($pem);
}
if (false === $res) {
throw new InvalidArgumentException('Unable to load the key.');
}
$details = openssl_pkey_get_details($res);
if (!is_array($details) || !array_key_exists('type', $details)) {
throw new InvalidArgumentException('Unable to get details of the key');
}
switch ($details['type']) {
case OPENSSL_KEYTYPE_EC:
$ec_key = ECKey::createFromPEM($pem);
return $ec_key->toArray();
case OPENSSL_KEYTYPE_RSA:
$rsa_key = RSAKey::createFromPEM($pem);
return $rsa_key->toArray();
default:
throw new InvalidArgumentException('Unsupported key type');
}
}
/**
* This method modifies the PEM to get 64 char lines and fix bug with old OpenSSL versions.
*/
private static function sanitizePEM(string &$pem): void
{
preg_match_all('#(-.*-)#', $pem, $matches, PREG_PATTERN_ORDER);
$ciphertext = preg_replace('#-.*-|\r|\n| #', '', $pem);
$pem = $matches[0][0].PHP_EOL;
$pem .= chunk_split($ciphertext, 64, PHP_EOL);
$pem .= $matches[0][1].PHP_EOL;
}
/**
* @param string[] $matches
*
* @throws InvalidArgumentException if the password to decrypt the key is not provided
* @throws InvalidArgumentException if the key cannot be loaded
*/
private static function decodePem(string $pem, array $matches, ?string $password = null): string
{
if (null === $password) {
throw new InvalidArgumentException('Password required for encrypted keys.');
}
$iv = pack('H*', trim($matches[2]));
$iv_sub = mb_substr($iv, 0, 8, '8bit');
$symkey = pack('H*', md5($password.$iv_sub));
$symkey .= pack('H*', md5($symkey.$password.$iv_sub));
$key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $pem);
$ciphertext = base64_decode(preg_replace('#-.*-|\r|\n#', '', $key), true);
if (!is_string($ciphertext)) {
throw new InvalidArgumentException('Unable to encode the data.');
}
$decoded = openssl_decrypt($ciphertext, mb_strtolower($matches[1]), $symkey, OPENSSL_RAW_DATA, $iv);
if (false === $decoded) {
throw new RuntimeException('Unable to decrypt the key');
}
$number = preg_match_all('#-{5}.*-{5}#', $pem, $result);
if (2 !== $number) {
throw new InvalidArgumentException('Unable to load the key');
}
$pem = $result[0][0].PHP_EOL;
$pem .= chunk_split(base64_encode($decoded), 64);
$pem .= $result[0][1].PHP_EOL;
return $pem;
}
private static function convertDerToPem(string $der_data): string
{
$pem = chunk_split(base64_encode($der_data), 64, PHP_EOL);
return '-----BEGIN CERTIFICATE-----'.PHP_EOL.$pem.'-----END CERTIFICATE-----'.PHP_EOL;
}
}

View File

@ -0,0 +1,263 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement\KeyConverter;
use function array_key_exists;
use Base64Url\Base64Url;
use function extension_loaded;
use function in_array;
use InvalidArgumentException;
use function is_array;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\BigInteger;
use RuntimeException;
/**
* @internal
*/
class RSAKey
{
/**
* @var array
*/
private $values = [];
/**
* RSAKey constructor.
*/
private function __construct(array $data)
{
$this->loadJWK($data);
}
/**
* @return RSAKey
*/
public static function createFromKeyDetails(array $details): self
{
$values = ['kty' => 'RSA'];
$keys = [
'n' => 'n',
'e' => 'e',
'd' => 'd',
'p' => 'p',
'q' => 'q',
'dp' => 'dmp1',
'dq' => 'dmq1',
'qi' => 'iqmp',
];
foreach ($details as $key => $value) {
if (in_array($key, $keys, true)) {
$value = Base64Url::encode($value);
$values[array_search($key, $keys, true)] = $value;
}
}
return new self($values);
}
/**
* @throws RuntimeException if the extension OpenSSL is not available
* @throws InvalidArgumentException if the key cannot be loaded
*
* @return RSAKey
*/
public static function createFromPEM(string $pem): self
{
if (!extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
$res = openssl_pkey_get_private($pem);
if (false === $res) {
$res = openssl_pkey_get_public($pem);
}
if (false === $res) {
throw new InvalidArgumentException('Unable to load the key.');
}
$details = openssl_pkey_get_details($res);
if (!is_array($details) || !isset($details['rsa'])) {
throw new InvalidArgumentException('Unable to load the key.');
}
return self::createFromKeyDetails($details['rsa']);
}
/**
* @return RSAKey
*/
public static function createFromJWK(JWK $jwk): self
{
return new self($jwk->all());
}
public function isPublic(): bool
{
return !array_key_exists('d', $this->values);
}
/**
* @param RSAKey $private
*
* @return RSAKey
*/
public static function toPublic(self $private): self
{
$data = $private->toArray();
$keys = ['p', 'd', 'q', 'dp', 'dq', 'qi'];
foreach ($keys as $key) {
if (array_key_exists($key, $data)) {
unset($data[$key]);
}
}
return new self($data);
}
public function toArray(): array
{
return $this->values;
}
public function toJwk(): JWK
{
return new JWK($this->values);
}
/**
* This method will try to add Chinese Remainder Theorem (CRT) parameters.
* With those primes, the decryption process is really fast.
*/
public function optimize(): void
{
if (array_key_exists('d', $this->values)) {
$this->populateCRT();
}
}
/**
* @throws InvalidArgumentException if the key is invalid or not an RSA key
*/
private function loadJWK(array $jwk): void
{
if (!array_key_exists('kty', $jwk)) {
throw new InvalidArgumentException('The key parameter "kty" is missing.');
}
if ('RSA' !== $jwk['kty']) {
throw new InvalidArgumentException('The JWK is not a RSA key.');
}
$this->values = $jwk;
}
/**
* This method adds Chinese Remainder Theorem (CRT) parameters if primes 'p' and 'q' are available.
* If 'p' and 'q' are missing, they are computed and added to the key data.
*/
private function populateCRT(): void
{
if (!array_key_exists('p', $this->values) && !array_key_exists('q', $this->values)) {
$d = BigInteger::createFromBinaryString(Base64Url::decode($this->values['d']));
$e = BigInteger::createFromBinaryString(Base64Url::decode($this->values['e']));
$n = BigInteger::createFromBinaryString(Base64Url::decode($this->values['n']));
[$p, $q] = $this->findPrimeFactors($d, $e, $n);
$this->values['p'] = Base64Url::encode($p->toBytes());
$this->values['q'] = Base64Url::encode($q->toBytes());
}
if (array_key_exists('dp', $this->values) && array_key_exists('dq', $this->values) && array_key_exists('qi', $this->values)) {
return;
}
$one = BigInteger::createFromDecimal(1);
$d = BigInteger::createFromBinaryString(Base64Url::decode($this->values['d']));
$p = BigInteger::createFromBinaryString(Base64Url::decode($this->values['p']));
$q = BigInteger::createFromBinaryString(Base64Url::decode($this->values['q']));
$this->values['dp'] = Base64Url::encode($d->mod($p->subtract($one))->toBytes());
$this->values['dq'] = Base64Url::encode($d->mod($q->subtract($one))->toBytes());
$this->values['qi'] = Base64Url::encode($q->modInverse($p)->toBytes());
}
/**
* @throws RuntimeException if the prime factors cannot be found
*
* @return BigInteger[]
*/
private function findPrimeFactors(BigInteger $d, BigInteger $e, BigInteger $n): array
{
$zero = BigInteger::createFromDecimal(0);
$one = BigInteger::createFromDecimal(1);
$two = BigInteger::createFromDecimal(2);
$k = $d->multiply($e)->subtract($one);
if ($k->isEven()) {
$r = $k;
$t = $zero;
do {
$r = $r->divide($two);
$t = $t->add($one);
} while ($r->isEven());
$found = false;
$y = null;
for ($i = 1; $i <= 100; ++$i) {
$g = BigInteger::random($n->subtract($one));
$y = $g->modPow($r, $n);
if ($y->equals($one) || $y->equals($n->subtract($one))) {
continue;
}
for ($j = $one; $j->lowerThan($t->subtract($one)); $j = $j->add($one)) {
$x = $y->modPow($two, $n);
if ($x->equals($one)) {
$found = true;
break;
}
if ($x->equals($n->subtract($one))) {
continue;
}
$y = $x;
}
$x = $y->modPow($two, $n);
if ($x->equals($one)) {
$found = true;
break;
}
}
if (null === $y) {
throw new InvalidArgumentException('Unable to find prime factors.');
}
if (true === $found) {
$p = $y->subtract($one)->gcd($n);
$q = $n->divide($p);
return [$p, $q];
}
}
throw new InvalidArgumentException('Unable to find prime factors.');
}
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2019 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,15 @@
PHP JWT Key Management Component
================================
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
**Please do not submit any Pull Request here.**
You should go to [the main repository](https://github.com/web-token/jwt-framework) instead.
# Documentation
The official documentation is available as https://web-token.spomky-labs.com/
# Licence
This software is release under [MIT licence](LICENSE).

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use RuntimeException;
abstract class UrlKeySetFactory
{
/**
* @var ClientInterface
*/
private $client;
/**
* @var RequestFactoryInterface
*/
private $requestFactory;
/**
* UrlKeySetFactory constructor.
*/
public function __construct(ClientInterface $client, RequestFactoryInterface $requestFactory)
{
$this->client = $client;
$this->requestFactory = $requestFactory;
}
/**
* @throws RuntimeException if the response content is invalid
*/
protected function getContent(string $url, array $header = []): string
{
$request = $this->requestFactory->createRequest('GET', $url);
foreach ($header as $k => $v) {
$request = $request->withHeader($k, $v);
}
$response = $this->client->sendRequest($request);
if ($response->getStatusCode() >= 400) {
throw new RuntimeException('Unable to get the key set.', $response->getStatusCode());
}
return $response->getBody()->getContents();
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\KeyManagement;
use function is_array;
use function is_string;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\KeyManagement\KeyConverter\KeyConverter;
use RuntimeException;
class X5UFactory extends UrlKeySetFactory
{
/**
* This method will try to fetch the url a retrieve the key set.
* Throws an exception in case of failure.
*
* @throws RuntimeException if the response content is invalid
*/
public function loadFromUrl(string $url, array $header = []): JWKSet
{
$content = $this->getContent($url, $header);
$data = JsonConverter::decode($content);
if (!is_array($data)) {
throw new RuntimeException('Invalid content.');
}
$keys = [];
foreach ($data as $kid => $cert) {
if (false === mb_strpos($cert, '-----BEGIN CERTIFICATE-----')) {
$cert = '-----BEGIN CERTIFICATE-----'.PHP_EOL.$cert.PHP_EOL.'-----END CERTIFICATE-----';
}
$jwk = KeyConverter::loadKeyFromCertificate($cert);
if (is_string($kid)) {
$jwk['kid'] = $kid;
$keys[$kid] = new JWK($jwk);
} else {
$keys[] = new JWK($jwk);
}
}
return new JWKSet($keys);
}
}

View File

@ -0,0 +1,34 @@
{
"name": "web-token/jwt-key-mgmt",
"description": "Key Management component of the JWT Framework.",
"type": "library",
"license": "MIT",
"keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"],
"homepage": "https://github.com/web-token",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},{
"name": "All contributors",
"homepage": "https://github.com/web-token/jwt-key-mgmt/contributors"
}
],
"autoload": {
"psr-4": {
"Jose\\Component\\KeyManagement\\": ""
}
},
"require": {
"ext-openssl": "*",
"psr/http-factory": "^1.0",
"psr/http-client": "^1.0",
"web-token/jwt-core": "^2.0"
},
"suggest": {
"ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys",
"web-token/jwt-util-ecc": "To use EC key analyzers.",
"php-http/message-factory": "To enable JKU/X5U support.",
"php-http/httplug": "To enable JKU/X5U support."
}
}

View File

@ -0,0 +1,4 @@
# Contributing
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
Please do not submit any Pull Requests here. It will be automatically closed.

View File

@ -0,0 +1 @@
patreon: FlorentMorselli

View File

@ -0,0 +1,3 @@
Please do not submit any Pull Requests here. It will be automatically closed.
You should submit it here: https://github.com/web-token/jwt-framework/pulls

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
use function defined;
use function in_array;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\ECKey;
use Jose\Component\Core\Util\ECSignature;
use LogicException;
use Throwable;
abstract class ECDSA implements SignatureAlgorithm
{
public function __construct()
{
if (!defined('OPENSSL_KEYTYPE_EC')) {
throw new LogicException('Elliptic Curve key type not supported by your environment.');
}
}
public function allowedKeyTypes(): array
{
return ['EC'];
}
public function sign(JWK $key, string $input): string
{
$this->checkKey($key);
if (!$key->has('d')) {
throw new InvalidArgumentException('The EC key is not private');
}
$pem = ECKey::convertPrivateKeyToPEM($key);
openssl_sign($input, $signature, $pem, $this->getHashAlgorithm());
return ECSignature::fromAsn1($signature, $this->getSignaturePartLength());
}
public function verify(JWK $key, string $input, string $signature): bool
{
$this->checkKey($key);
try {
$der = ECSignature::toAsn1($signature, $this->getSignaturePartLength());
$pem = ECKey::convertPublicKeyToPEM($key);
return 1 === openssl_verify($input, $der, $pem, $this->getHashAlgorithm());
} catch (Throwable $e) {
return false;
}
}
abstract protected function getHashAlgorithm(): string;
abstract protected function getSignaturePartLength(): int;
private function checkKey(JWK $key): void
{
if (!in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
foreach (['x', 'y', 'crv'] as $k) {
if (!$key->has($k)) {
throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
}
}
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
final class ES256 extends ECDSA
{
public function name(): string
{
return 'ES256';
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
protected function getSignaturePartLength(): int
{
return 64;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
final class ES384 extends ECDSA
{
public function name(): string
{
return 'ES384';
}
protected function getHashAlgorithm(): string
{
return 'sha384';
}
protected function getSignaturePartLength(): int
{
return 96;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
final class ES512 extends ECDSA
{
public function name(): string
{
return 'ES512';
}
protected function getHashAlgorithm(): string
{
return 'sha512';
}
protected function getSignaturePartLength(): int
{
return 132;
}
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2019 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,15 @@
ECDSA Based Signature Algorithms For JWT-Framework
==================================================
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
**Please do not submit any Pull Request here.**
You should go to [the main repository](https://github.com/web-token/jwt-framework) instead.
# Documentation
The official documentation is available as https://web-token.spomky-labs.com/
# Licence
This software is release under [MIT licence](LICENSE).

View File

@ -0,0 +1,26 @@
{
"name": "web-token/jwt-signature-algorithm-ecdsa",
"description": "ECDSA Based Signature Algorithms the JWT Framework.",
"type": "library",
"license": "MIT",
"keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"],
"homepage": "https://github.com/web-token",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},{
"name": "All contributors",
"homepage": "https://github.com/web-token/jwt-framework/contributors"
}
],
"autoload": {
"psr-4": {
"Jose\\Component\\Signature\\Algorithm\\": ""
}
},
"require": {
"ext-openssl": "*",
"web-token/jwt-signature": "^2.0"
}
}

View File

@ -0,0 +1,4 @@
# Contributing
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
Please do not submit any Pull Requests here. It will be automatically closed.

View File

@ -0,0 +1 @@
patreon: FlorentMorselli

View File

@ -0,0 +1,3 @@
Please do not submit any Pull Requests here. It will be automatically closed.
You should submit it here: https://github.com/web-token/jwt-framework/pulls

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
use Base64Url\Base64Url;
use function extension_loaded;
use function in_array;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use RuntimeException;
final class EdDSA implements SignatureAlgorithm
{
/**
* EdDSA constructor.
*
* @throws RuntimeException if the extension "sodium" is not available
*/
public function __construct()
{
if (!extension_loaded('sodium')) {
throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method');
}
}
public function allowedKeyTypes(): array
{
return ['OKP'];
}
/**
* @throws InvalidArgumentException if the key is not private
* @throws InvalidArgumentException if the curve is not supported
*/
public function sign(JWK $key, string $input): string
{
$this->checkKey($key);
if (!$key->has('d')) {
throw new InvalidArgumentException('The EC key is not private');
}
$x = Base64Url::decode($key->get('x'));
$d = Base64Url::decode($key->get('d'));
$secret = $d.$x;
switch ($key->get('crv')) {
case 'Ed25519':
return sodium_crypto_sign_detached($input, $secret);
default:
throw new InvalidArgumentException('Unsupported curve');
}
}
/**
* @throws InvalidArgumentException if the curve is not supported
*/
public function verify(JWK $key, string $input, string $signature): bool
{
$this->checkKey($key);
$public = Base64Url::decode($key->get('x'));
switch ($key->get('crv')) {
case 'Ed25519':
return sodium_crypto_sign_verify_detached($signature, $input, $public);
default:
throw new InvalidArgumentException('Unsupported curve');
}
}
public function name(): string
{
return 'EdDSA';
}
/**
* @throws InvalidArgumentException if the key type is not valid
* @throws InvalidArgumentException if a mandatory key parameter is missing
* @throws InvalidArgumentException if the curve is not suuported
*/
private function checkKey(JWK $key): void
{
if (!in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
foreach (['x', 'crv'] as $k) {
if (!$key->has($k)) {
throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
}
}
if ('Ed25519' !== $key->get('crv')) {
throw new InvalidArgumentException('Unsupported curve.');
}
}
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2019 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,15 @@
EdDSA Based Signature Algorithms For JWT-Framework
==================================================
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
**Please do not submit any Pull Request here.**
You should go to [the main repository](https://github.com/web-token/jwt-framework) instead.
# Documentation
The official documentation is available as https://web-token.spomky-labs.com/
# Licence
This software is release under [MIT licence](LICENSE).

View File

@ -0,0 +1,26 @@
{
"name": "web-token/jwt-signature-algorithm-eddsa",
"description": "EdDSA Signature Algorithm the JWT Framework.",
"type": "library",
"license": "MIT",
"keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"],
"homepage": "https://github.com/web-token",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},{
"name": "All contributors",
"homepage": "https://github.com/web-token/jwt-framework/contributors"
}
],
"autoload": {
"psr-4": {
"Jose\\Component\\Signature\\Algorithm\\": ""
}
},
"require": {
"ext-sodium": "*",
"web-token/jwt-signature": "^2.1"
}
}

View File

@ -0,0 +1,4 @@
# Contributing
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
Please do not submit any Pull Requests here. It will be automatically closed.

View File

@ -0,0 +1 @@
patreon: FlorentMorselli

View File

@ -0,0 +1,3 @@
Please do not submit any Pull Requests here. It will be automatically closed.
You should submit it here: https://github.com/web-token/jwt-framework/pulls

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2019 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
final class PS256 extends RSAPSS
{
public function name(): string
{
return 'PS256';
}
protected function getAlgorithm(): string
{
return 'sha256';
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
final class PS384 extends RSAPSS
{
public function name(): string
{
return 'PS384';
}
protected function getAlgorithm(): string
{
return 'sha384';
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
final class PS512 extends RSAPSS
{
public function name(): string
{
return 'PS512';
}
protected function getAlgorithm(): string
{
return 'sha512';
}
}

View File

@ -0,0 +1,15 @@
RSA Based Signature Algorithms For JWT-Framework
================================================
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
**Please do not submit any Pull Request here.**
You should go to [the main repository](https://github.com/web-token/jwt-framework) instead.
# Documentation
The official documentation is available as https://web-token.spomky-labs.com/
# Licence
This software is release under [MIT licence](LICENSE).

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
final class RS256 extends RSAPKCS1
{
public function name(): string
{
return 'RS256';
}
protected function getAlgorithm(): string
{
return 'sha256';
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
final class RS384 extends RSAPKCS1
{
public function name(): string
{
return 'RS384';
}
protected function getAlgorithm(): string
{
return 'sha384';
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
final class RS512 extends RSAPKCS1
{
public function name(): string
{
return 'RS512';
}
protected function getAlgorithm(): string
{
return 'sha512';
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
use function in_array;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\RSAKey;
use Jose\Component\Signature\Algorithm\Util\RSA as JoseRSA;
/**
* @deprecated Please use either RSAPSS or RSAPKCS1 depending on the padding mode
*/
abstract class RSA implements SignatureAlgorithm
{
public function allowedKeyTypes(): array
{
return ['RSA'];
}
public function verify(JWK $key, string $input, string $signature): bool
{
$this->checkKey($key);
$pub = RSAKey::createFromJWK($key->toPublic());
return JoseRSA::verify($pub, $input, $signature, $this->getAlgorithm(), $this->getSignatureMethod());
}
/**
* @throws InvalidArgumentException if the key is not private
*/
public function sign(JWK $key, string $input): string
{
$this->checkKey($key);
if (!$key->has('d')) {
throw new InvalidArgumentException('The key is not a private key.');
}
$priv = RSAKey::createFromJWK($key);
return JoseRSA::sign($priv, $input, $this->getAlgorithm(), $this->getSignatureMethod());
}
abstract protected function getAlgorithm(): string;
abstract protected function getSignatureMethod(): int;
/**
* @throws InvalidArgumentException if the key type is not allowed
* @throws InvalidArgumentException if the key is invalid
*/
private function checkKey(JWK $key): void
{
if (!in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
foreach (['n', 'e'] as $k) {
if (!$key->has($k)) {
throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
}
}
}
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
use function in_array;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\RSAKey;
use RuntimeException;
abstract class RSAPKCS1 implements SignatureAlgorithm
{
public function allowedKeyTypes(): array
{
return ['RSA'];
}
public function verify(JWK $key, string $input, string $signature): bool
{
$this->checkKey($key);
$pub = RSAKey::createFromJWK($key->toPublic());
return 1 === openssl_verify($input, $signature, $pub->toPEM(), $this->getAlgorithm());
}
/**
* @throws InvalidArgumentException if the key is not private
* @throws InvalidArgumentException if the data cannot be signed
*/
public function sign(JWK $key, string $input): string
{
$this->checkKey($key);
if (!$key->has('d')) {
throw new InvalidArgumentException('The key is not a private key.');
}
$priv = RSAKey::createFromJWK($key);
$result = openssl_sign($input, $signature, $priv->toPEM(), $this->getAlgorithm());
if (true !== $result) {
throw new RuntimeException('Unable to sign');
}
return $signature;
}
abstract protected function getAlgorithm(): string;
/**
* @throws InvalidArgumentException if the key type is not allowed
* @throws InvalidArgumentException if the key is not valid
*/
private function checkKey(JWK $key): void
{
if (!in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
foreach (['n', 'e'] as $k) {
if (!$key->has($k)) {
throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
}
}
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
use function in_array;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\RSAKey;
use Jose\Component\Signature\Algorithm\Util\RSA as JoseRSA;
abstract class RSAPSS implements SignatureAlgorithm
{
public function allowedKeyTypes(): array
{
return ['RSA'];
}
public function verify(JWK $key, string $input, string $signature): bool
{
$this->checkKey($key);
$pub = RSAKey::createFromJWK($key->toPublic());
return JoseRSA::verify($pub, $input, $signature, $this->getAlgorithm(), JoseRSA::SIGNATURE_PSS);
}
/**
* @throws InvalidArgumentException if the key is not private
*/
public function sign(JWK $key, string $input): string
{
$this->checkKey($key);
if (!$key->has('d')) {
throw new InvalidArgumentException('The key is not a private key.');
}
$priv = RSAKey::createFromJWK($key);
return JoseRSA::sign($priv, $input, $this->getAlgorithm(), JoseRSA::SIGNATURE_PSS);
}
abstract protected function getAlgorithm(): string;
/**
* @throws InvalidArgumentException if the key type is not allowed
* @throws InvalidArgumentException if the key is not valid
*/
private function checkKey(JWK $key): void
{
if (!in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
foreach (['n', 'e'] as $k) {
if (!$key->has($k)) {
throw new InvalidArgumentException(sprintf('The key parameter "%s" is missing.', $k));
}
}
}
}

View File

@ -0,0 +1,251 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm\Util;
use function chr;
use InvalidArgumentException;
use Jose\Component\Core\Util\BigInteger;
use Jose\Component\Core\Util\Hash;
use Jose\Component\Core\Util\RSAKey;
use function ord;
use RuntimeException;
/**
* @internal
*/
class RSA
{
/**
* Probabilistic Signature Scheme.
*/
public const SIGNATURE_PSS = 1;
/**
* Use the PKCS#1.
*/
public const SIGNATURE_PKCS1 = 2;
/**
* @throws RuntimeException if the data cannot be signed
* @throws InvalidArgumentException if the signature mode is not supported
*/
public static function sign(RSAKey $key, string $message, string $hash, int $mode): string
{
switch ($mode) {
case self::SIGNATURE_PSS:
return self::signWithPSS($key, $message, $hash);
case self::SIGNATURE_PKCS1:
$result = openssl_sign($message, $signature, $key->toPEM(), $hash);
if (true !== $result) {
throw new RuntimeException('Unable to sign the data');
}
return $signature;
default:
throw new InvalidArgumentException('Unsupported mode.');
}
}
/**
* Create a signature.
*/
public static function signWithPSS(RSAKey $key, string $message, string $hash): string
{
$em = self::encodeEMSAPSS($message, 8 * $key->getModulusLength() - 1, Hash::$hash());
$message = BigInteger::createFromBinaryString($em);
$signature = RSAKey::exponentiate($key, $message);
return self::convertIntegerToOctetString($signature, $key->getModulusLength());
}
/**
* Create a signature.
*
* @deprecated Please use openssl_sign
*/
public static function signWithPKCS15(RSAKey $key, string $message, string $hash): string
{
$em = self::encodeEMSA15($message, $key->getModulusLength(), Hash::$hash());
$message = BigInteger::createFromBinaryString($em);
$signature = RSAKey::exponentiate($key, $message);
return self::convertIntegerToOctetString($signature, $key->getModulusLength());
}
/**
* @throws InvalidArgumentException if the signature mode is not supported
*/
public static function verify(RSAKey $key, string $message, string $signature, string $hash, int $mode): bool
{
switch ($mode) {
case self::SIGNATURE_PSS:
return self::verifyWithPSS($key, $message, $signature, $hash);
case self::SIGNATURE_PKCS1:
return 1 === openssl_verify($message, $signature, $key->toPEM(), $hash);
default:
throw new InvalidArgumentException('Unsupported mode.');
}
}
/**
* Verifies a signature.
*
* @throws RuntimeException if the signature cannot be verified
*/
public static function verifyWithPSS(RSAKey $key, string $message, string $signature, string $hash): bool
{
if (mb_strlen($signature, '8bit') !== $key->getModulusLength()) {
throw new RuntimeException();
}
$s2 = BigInteger::createFromBinaryString($signature);
$m2 = RSAKey::exponentiate($key, $s2);
$em = self::convertIntegerToOctetString($m2, $key->getModulusLength());
$modBits = 8 * $key->getModulusLength();
return self::verifyEMSAPSS($message, $em, $modBits - 1, Hash::$hash());
}
/**
* Verifies a signature.
*
* @deprecated Please use openssl_sign
*
* @throws RuntimeException if the signature cannot be verified
*/
public static function verifyWithPKCS15(RSAKey $key, string $message, string $signature, string $hash): bool
{
if (mb_strlen($signature, '8bit') !== $key->getModulusLength()) {
throw new RuntimeException();
}
$signature = BigInteger::createFromBinaryString($signature);
$m2 = RSAKey::exponentiate($key, $signature);
$em = self::convertIntegerToOctetString($m2, $key->getModulusLength());
return hash_equals($em, self::encodeEMSA15($message, $key->getModulusLength(), Hash::$hash()));
}
/**
* @throws RuntimeException if the value cannot be converted
*/
private static function convertIntegerToOctetString(BigInteger $x, int $xLen): string
{
$x = $x->toBytes();
if (mb_strlen($x, '8bit') > $xLen) {
throw new RuntimeException();
}
return str_pad($x, $xLen, chr(0), STR_PAD_LEFT);
}
/**
* MGF1.
*/
private static function getMGF1(string $mgfSeed, int $maskLen, Hash $mgfHash): string
{
$t = '';
$count = ceil($maskLen / $mgfHash->getLength());
for ($i = 0; $i < $count; ++$i) {
$c = pack('N', $i);
$t .= $mgfHash->hash($mgfSeed.$c);
}
return mb_substr($t, 0, $maskLen, '8bit');
}
/**
* EMSA-PSS-ENCODE.
*
* @throws RuntimeException if the message length is invalid
*/
private static function encodeEMSAPSS(string $message, int $modulusLength, Hash $hash): string
{
$emLen = ($modulusLength + 1) >> 3;
$sLen = $hash->getLength();
$mHash = $hash->hash($message);
if ($emLen <= $hash->getLength() + $sLen + 2) {
throw new RuntimeException();
}
$salt = random_bytes($sLen);
$m2 = "\0\0\0\0\0\0\0\0".$mHash.$salt;
$h = $hash->hash($m2);
$ps = str_repeat(chr(0), $emLen - $sLen - $hash->getLength() - 2);
$db = $ps.chr(1).$salt;
$dbMask = self::getMGF1($h, $emLen - $hash->getLength() - 1, $hash);
$maskedDB = $db ^ $dbMask;
$maskedDB[0] = ~chr(0xFF << ($modulusLength & 7)) & $maskedDB[0];
$em = $maskedDB.$h.chr(0xBC);
return $em;
}
/**
* EMSA-PSS-VERIFY.
*
* @throws InvalidArgumentException if the signature cannot be verified
*/
private static function verifyEMSAPSS(string $m, string $em, int $emBits, Hash $hash): bool
{
$emLen = ($emBits + 1) >> 3;
$sLen = $hash->getLength();
$mHash = $hash->hash($m);
if ($emLen < $hash->getLength() + $sLen + 2) {
throw new InvalidArgumentException();
}
if ($em[mb_strlen($em, '8bit') - 1] !== chr(0xBC)) {
throw new InvalidArgumentException();
}
$maskedDB = mb_substr($em, 0, -$hash->getLength() - 1, '8bit');
$h = mb_substr($em, -$hash->getLength() - 1, $hash->getLength(), '8bit');
$temp = chr(0xFF << ($emBits & 7));
if ((~$maskedDB[0] & $temp) !== $temp) {
throw new InvalidArgumentException();
}
$dbMask = self::getMGF1($h, $emLen - $hash->getLength() - 1, $hash/*MGF*/);
$db = $maskedDB ^ $dbMask;
$db[0] = ~chr(0xFF << ($emBits & 7)) & $db[0];
$temp = $emLen - $hash->getLength() - $sLen - 2;
if (mb_substr($db, 0, $temp, '8bit') !== str_repeat(chr(0), $temp)) {
throw new InvalidArgumentException();
}
if (1 !== ord($db[$temp])) {
throw new InvalidArgumentException();
}
$salt = mb_substr($db, $temp + 1, null, '8bit'); // should be $sLen long
$m2 = "\0\0\0\0\0\0\0\0".$mHash.$salt;
$h2 = $hash->hash($m2);
return hash_equals($h, $h2);
}
/**
* @throws RuntimeException if the value cannot be encoded
*/
private static function encodeEMSA15(string $m, int $emBits, Hash $hash): string
{
$h = $hash->hash($m);
$t = $hash->t();
$t .= $h;
$tLen = mb_strlen($t, '8bit');
if ($emBits < $tLen + 11) {
throw new RuntimeException();
}
$ps = str_repeat(chr(0xFF), $emBits - $tLen - 3);
return "\0\1{$ps}\0{$t}";
}
}

View File

@ -0,0 +1,31 @@
{
"name": "web-token/jwt-signature-algorithm-rsa",
"description": "RSA Based Signature Algorithms the JWT Framework.",
"type": "library",
"license": "MIT",
"keywords": ["JWS", "JWT", "JWE", "JWA", "JWK", "JWKSet", "Jot", "Jose", "RFC7515", "RFC7516", "RFC7517", "RFC7518", "RFC7519", "RFC7520", "Bundle", "Symfony"],
"homepage": "https://github.com/web-token",
"authors": [
{
"name": "Florent Morselli",
"homepage": "https://github.com/Spomky"
},{
"name": "All contributors",
"homepage": "https://github.com/web-token/jwt-framework/contributors"
}
],
"autoload": {
"psr-4": {
"Jose\\Component\\Signature\\Algorithm\\": ""
}
},
"require": {
"brick/math": "^0.8.17|^0.9",
"ext-openssl": "*",
"web-token/jwt-signature": "^2.1"
},
"suggest": {
"ext-gmp": "GMP or BCMath is highly recommended to improve the library performance",
"ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance"
}
}

View File

@ -0,0 +1,4 @@
# Contributing
This repository is a sub repository of [the JWT Framework](https://github.com/web-token/jwt-framework) project and is READ ONLY.
Please do not submit any Pull Requests here. It will be automatically closed.

View File

@ -0,0 +1 @@
patreon: FlorentMorselli

View File

@ -0,0 +1,3 @@
Please do not submit any Pull Requests here. It will be automatically closed.
You should submit it here: https://github.com/web-token/jwt-framework/pulls

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
use Jose\Component\Core\Algorithm;
use Jose\Component\Core\JWK;
interface MacAlgorithm extends Algorithm
{
/**
* Sign the input.
*
* @param JWK $key The private key used to hash the data
* @param string $input The input
*/
public function hash(JWK $key, string $input): string;
/**
* Verify the signature of data.
*
* @param JWK $key The private key used to hash the data
* @param string $input The input
* @param string $signature The signature to verify
*/
public function verify(JWK $key, string $input, string $signature): bool;
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature\Algorithm;
use Jose\Component\Core\Algorithm;
use Jose\Component\Core\JWK;
interface SignatureAlgorithm extends Algorithm
{
/**
* Sign the input.
*
* @param JWK $key The private key used to sign the data
* @param string $input The input
*/
public function sign(JWK $key, string $input): string;
/**
* Verify the signature of data.
*
* @param JWK $key The private key used to sign the data
* @param string $input The input
* @param string $signature The signature to verify
*/
public function verify(JWK $key, string $input, string $signature): bool;
}

View File

@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature;
use function count;
use InvalidArgumentException;
use Jose\Component\Core\JWT;
class JWS implements JWT
{
/**
* @var bool
*/
private $isPayloadDetached = false;
/**
* @var null|string
*/
private $encodedPayload;
/**
* @var Signature[]
*/
private $signatures = [];
/**
* @var null|string
*/
private $payload;
public function __construct(?string $payload, ?string $encodedPayload = null, bool $isPayloadDetached = false)
{
$this->payload = $payload;
$this->encodedPayload = $encodedPayload;
$this->isPayloadDetached = $isPayloadDetached;
}
public function getPayload(): ?string
{
return $this->payload;
}
/**
* Returns true if the payload is detached.
*/
public function isPayloadDetached(): bool
{
return $this->isPayloadDetached;
}
/**
* Returns the Base64Url encoded payload.
* If the payload is detached, this method returns null.
*/
public function getEncodedPayload(): ?string
{
if (true === $this->isPayloadDetached()) {
return null;
}
return $this->encodedPayload;
}
/**
* Returns the signatures associated with the JWS.
*
* @return Signature[]
*/
public function getSignatures(): array
{
return $this->signatures;
}
/**
* Returns the signature at the given index.
*
* @throws InvalidArgumentException if the signature index does not exist
*/
public function getSignature(int $id): Signature
{
if (isset($this->signatures[$id])) {
return $this->signatures[$id];
}
throw new InvalidArgumentException('The signature does not exist.');
}
/**
* This method adds a signature to the JWS object.
* Its returns a new JWS object.
*
* @internal
*
* @return JWS
*/
public function addSignature(string $signature, array $protectedHeader, ?string $encodedProtectedHeader, array $header = []): self
{
$jws = clone $this;
$jws->signatures[] = new Signature($signature, $protectedHeader, $encodedProtectedHeader, $header);
return $jws;
}
/**
* Returns the number of signature associated with the JWS.
*/
public function countSignatures(): int
{
return count($this->signatures);
}
/**
* This method splits the JWS into a list of JWSs.
* It is only useful when the JWS contains more than one signature (JSON General Serialization).
*
* @return JWS[]
*/
public function split(): array
{
$result = [];
foreach ($this->signatures as $signature) {
$jws = new self(
$this->payload,
$this->encodedPayload,
$this->isPayloadDetached
);
$jws = $jws->addSignature(
$signature->getSignature(),
$signature->getProtectedHeader(),
$signature->getEncodedProtectedHeader(),
$signature->getHeader()
);
$result[] = $jws;
}
return $result;
}
}

View File

@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature;
use function array_key_exists;
use Base64Url\Base64Url;
use function count;
use function in_array;
use InvalidArgumentException;
use function is_array;
use Jose\Component\Core\Algorithm;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Core\Util\KeyChecker;
use Jose\Component\Signature\Algorithm\MacAlgorithm;
use Jose\Component\Signature\Algorithm\SignatureAlgorithm;
use LogicException;
use RuntimeException;
class JWSBuilder
{
/**
* @var null|string
*/
protected $payload;
/**
* @var bool
*/
protected $isPayloadDetached;
/**
* @var array
*/
protected $signatures = [];
/**
* @var null|bool
*/
protected $isPayloadEncoded;
/**
* @var AlgorithmManager
*/
private $signatureAlgorithmManager;
public function __construct(AlgorithmManager $signatureAlgorithmManager)
{
$this->signatureAlgorithmManager = $signatureAlgorithmManager;
}
/**
* Returns the algorithm manager associated to the builder.
*/
public function getSignatureAlgorithmManager(): AlgorithmManager
{
return $this->signatureAlgorithmManager;
}
/**
* Reset the current data.
*
* @return JWSBuilder
*/
public function create(): self
{
$this->payload = null;
$this->isPayloadDetached = false;
$this->signatures = [];
$this->isPayloadEncoded = null;
return $this;
}
/**
* Set the payload.
* This method will return a new JWSBuilder object.
*
* @throws InvalidArgumentException if the payload is not UTF-8 encoded
*
* @return JWSBuilder
*/
public function withPayload(string $payload, bool $isPayloadDetached = false): self
{
if (false === mb_detect_encoding($payload, 'UTF-8', true)) {
throw new InvalidArgumentException('The payload must be encoded in UTF-8');
}
$clone = clone $this;
$clone->payload = $payload;
$clone->isPayloadDetached = $isPayloadDetached;
return $clone;
}
/**
* Adds the information needed to compute the signature.
* This method will return a new JWSBuilder object.
*
* @throws InvalidArgumentException if the payload encoding is inconsistent
*
* @return JWSBuilder
*/
public function addSignature(JWK $signatureKey, array $protectedHeader, array $header = []): self
{
$this->checkB64AndCriticalHeader($protectedHeader);
$isPayloadEncoded = $this->checkIfPayloadIsEncoded($protectedHeader);
if (null === $this->isPayloadEncoded) {
$this->isPayloadEncoded = $isPayloadEncoded;
} elseif ($this->isPayloadEncoded !== $isPayloadEncoded) {
throw new InvalidArgumentException('Foreign payload encoding detected.');
}
$this->checkDuplicatedHeaderParameters($protectedHeader, $header);
KeyChecker::checkKeyUsage($signatureKey, 'signature');
$algorithm = $this->findSignatureAlgorithm($signatureKey, $protectedHeader, $header);
KeyChecker::checkKeyAlgorithm($signatureKey, $algorithm->name());
$clone = clone $this;
$clone->signatures[] = [
'signature_algorithm' => $algorithm,
'signature_key' => $signatureKey,
'protected_header' => $protectedHeader,
'header' => $header,
];
return $clone;
}
/**
* Computes all signatures and return the expected JWS object.
*
* @throws RuntimeException if the payload is not set
* @throws RuntimeException if no signature is defined
*/
public function build(): JWS
{
if (null === $this->payload) {
throw new RuntimeException('The payload is not set.');
}
if (0 === count($this->signatures)) {
throw new RuntimeException('At least one signature must be set.');
}
$encodedPayload = false === $this->isPayloadEncoded ? $this->payload : Base64Url::encode($this->payload);
$jws = new JWS($this->payload, $encodedPayload, $this->isPayloadDetached);
foreach ($this->signatures as $signature) {
/** @var MacAlgorithm|SignatureAlgorithm $algorithm */
$algorithm = $signature['signature_algorithm'];
/** @var JWK $signatureKey */
$signatureKey = $signature['signature_key'];
/** @var array $protectedHeader */
$protectedHeader = $signature['protected_header'];
/** @var array $header */
$header = $signature['header'];
$encodedProtectedHeader = 0 === count($protectedHeader) ? null : Base64Url::encode(JsonConverter::encode($protectedHeader));
$input = sprintf('%s.%s', $encodedProtectedHeader, $encodedPayload);
if ($algorithm instanceof SignatureAlgorithm) {
$s = $algorithm->sign($signatureKey, $input);
} else {
$s = $algorithm->hash($signatureKey, $input);
}
$jws = $jws->addSignature($s, $protectedHeader, $encodedProtectedHeader, $header);
}
return $jws;
}
private function checkIfPayloadIsEncoded(array $protectedHeader): bool
{
return !array_key_exists('b64', $protectedHeader) || true === $protectedHeader['b64'];
}
/**
* @throws LogicException if the header parameter "crit" is missing, invalid or does not contain "b64" when "b64" is set
*/
private function checkB64AndCriticalHeader(array $protectedHeader): void
{
if (!array_key_exists('b64', $protectedHeader)) {
return;
}
if (!array_key_exists('crit', $protectedHeader)) {
throw new LogicException('The protected header parameter "crit" is mandatory when protected header parameter "b64" is set.');
}
if (!is_array($protectedHeader['crit'])) {
throw new LogicException('The protected header parameter "crit" must be an array.');
}
if (!in_array('b64', $protectedHeader['crit'], true)) {
throw new LogicException('The protected header parameter "crit" must contain "b64" when protected header parameter "b64" is set.');
}
}
/**
* @throws InvalidArgumentException if the header parameter "alg" is missing or the algorithm is not allowed/not supported
*
* @return MacAlgorithm|SignatureAlgorithm
*/
private function findSignatureAlgorithm(JWK $key, array $protectedHeader, array $header): Algorithm
{
$completeHeader = array_merge($header, $protectedHeader);
if (!array_key_exists('alg', $completeHeader)) {
throw new InvalidArgumentException('No "alg" parameter set in the header.');
}
if ($key->has('alg') && $key->get('alg') !== $completeHeader['alg']) {
throw new InvalidArgumentException(sprintf('The algorithm "%s" is not allowed with this key.', $completeHeader['alg']));
}
$algorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']);
if (!$algorithm instanceof SignatureAlgorithm && !$algorithm instanceof MacAlgorithm) {
throw new InvalidArgumentException(sprintf('The algorithm "%s" is not supported.', $completeHeader['alg']));
}
return $algorithm;
}
/**
* @throws InvalidArgumentException if the header contains duplicated entries
*/
private function checkDuplicatedHeaderParameters(array $header1, array $header2): void
{
$inter = array_intersect_key($header1, $header2);
if (0 !== count($inter)) {
throw new InvalidArgumentException(sprintf('The header contains duplicated entries: %s.', implode(', ', array_keys($inter))));
}
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature;
use Jose\Component\Core\AlgorithmManagerFactory;
class JWSBuilderFactory
{
/**
* @var AlgorithmManagerFactory
*/
private $signatureAlgorithmManagerFactory;
public function __construct(AlgorithmManagerFactory $signatureAlgorithmManagerFactory)
{
$this->signatureAlgorithmManagerFactory = $signatureAlgorithmManagerFactory;
}
/**
* This method creates a JWSBuilder using the given algorithm aliases.
*
* @param string[] $algorithms
*/
public function create(array $algorithms): JWSBuilder
{
$algorithmManager = $this->signatureAlgorithmManagerFactory->create($algorithms);
return new JWSBuilder($algorithmManager);
}
}

View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature;
use Exception;
use Jose\Component\Checker\HeaderCheckerManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Signature\Serializer\JWSSerializerManager;
use Throwable;
class JWSLoader
{
/**
* @var JWSVerifier
*/
private $jwsVerifier;
/**
* @var null|HeaderCheckerManager
*/
private $headerCheckerManager;
/**
* @var JWSSerializerManager
*/
private $serializerManager;
/**
* JWSLoader constructor.
*/
public function __construct(JWSSerializerManager $serializerManager, JWSVerifier $jwsVerifier, ?HeaderCheckerManager $headerCheckerManager)
{
$this->serializerManager = $serializerManager;
$this->jwsVerifier = $jwsVerifier;
$this->headerCheckerManager = $headerCheckerManager;
}
/**
* Returns the JWSVerifier associated to the JWSLoader.
*/
public function getJwsVerifier(): JWSVerifier
{
return $this->jwsVerifier;
}
/**
* Returns the Header Checker Manager associated to the JWSLoader.
*/
public function getHeaderCheckerManager(): ?HeaderCheckerManager
{
return $this->headerCheckerManager;
}
/**
* Returns the JWSSerializer associated to the JWSLoader.
*/
public function getSerializerManager(): JWSSerializerManager
{
return $this->serializerManager;
}
/**
* This method will try to load and verify the token using the given key.
* It returns a JWS and will populate the $signature variable in case of success, otherwise an exception is thrown.
*
* @throws Exception if the token cannot be loaded or verified
*/
public function loadAndVerifyWithKey(string $token, JWK $key, ?int &$signature, ?string $payload = null): JWS
{
$keyset = new JWKSet([$key]);
return $this->loadAndVerifyWithKeySet($token, $keyset, $signature, $payload);
}
/**
* This method will try to load and verify the token using the given key set.
* It returns a JWS and will populate the $signature variable in case of success, otherwise an exception is thrown.
*
* @throws Exception if the token cannot be loaded or verified
*/
public function loadAndVerifyWithKeySet(string $token, JWKSet $keyset, ?int &$signature, ?string $payload = null): JWS
{
try {
$jws = $this->serializerManager->unserialize($token);
$nbSignatures = $jws->countSignatures();
for ($i = 0; $i < $nbSignatures; ++$i) {
if ($this->processSignature($jws, $keyset, $i, $payload)) {
$signature = $i;
return $jws;
}
}
} catch (Throwable $e) {
// Nothing to do. Exception thrown just after
}
throw new Exception('Unable to load and verify the token.');
}
private function processSignature(JWS $jws, JWKSet $keyset, int $signature, ?string $payload): bool
{
try {
if (null !== $this->headerCheckerManager) {
$this->headerCheckerManager->check($jws, $signature);
}
return $this->jwsVerifier->verifyWithKeySet($jws, $keyset, $signature, $payload);
} catch (Throwable $e) {
return false;
}
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature;
use Jose\Component\Checker\HeaderCheckerManagerFactory;
use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory;
class JWSLoaderFactory
{
/**
* @var JWSVerifierFactory
*/
private $jwsVerifierFactory;
/**
* @var JWSSerializerManagerFactory
*/
private $jwsSerializerManagerFactory;
/**
* @var null|HeaderCheckerManagerFactory
*/
private $headerCheckerManagerFactory;
public function __construct(JWSSerializerManagerFactory $jwsSerializerManagerFactory, JWSVerifierFactory $jwsVerifierFactory, ?HeaderCheckerManagerFactory $headerCheckerManagerFactory)
{
$this->jwsSerializerManagerFactory = $jwsSerializerManagerFactory;
$this->jwsVerifierFactory = $jwsVerifierFactory;
$this->headerCheckerManagerFactory = $headerCheckerManagerFactory;
}
/**
* Creates a JWSLoader using the given serializer aliases, signature algorithm aliases and (optionally)
* the header checker aliases.
*/
public function create(array $serializers, array $algorithms, array $headerCheckers = []): JWSLoader
{
$serializerManager = $this->jwsSerializerManagerFactory->create($serializers);
$jwsVerifier = $this->jwsVerifierFactory->create($algorithms);
if (null !== $this->headerCheckerManagerFactory) {
$headerCheckerManager = $this->headerCheckerManagerFactory->create($headerCheckers);
} else {
$headerCheckerManager = null;
}
return new JWSLoader($serializerManager, $jwsVerifier, $headerCheckerManager);
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature;
use InvalidArgumentException;
use Jose\Component\Checker\TokenTypeSupport;
use Jose\Component\Core\JWT;
final class JWSTokenSupport implements TokenTypeSupport
{
public function supports(JWT $jwt): bool
{
return $jwt instanceof JWS;
}
/**
* @throws InvalidArgumentException if the signature index does not exist
*/
public function retrieveTokenHeaders(JWT $jwt, int $index, array &$protectedHeader, array &$unprotectedHeader): void
{
if (!$jwt instanceof JWS) {
return;
}
if ($index > $jwt->countSignatures()) {
throw new InvalidArgumentException('Unknown signature index.');
}
$protectedHeader = $jwt->getSignature($index)->getProtectedHeader();
$unprotectedHeader = $jwt->getSignature($index)->getHeader();
}
}

View File

@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature;
use Base64Url\Base64Url;
use InvalidArgumentException;
use Jose\Component\Core\Algorithm;
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\KeyChecker;
use Jose\Component\Signature\Algorithm\MacAlgorithm;
use Jose\Component\Signature\Algorithm\SignatureAlgorithm;
use Throwable;
class JWSVerifier
{
/**
* @var AlgorithmManager
*/
private $signatureAlgorithmManager;
/**
* JWSVerifier constructor.
*/
public function __construct(AlgorithmManager $signatureAlgorithmManager)
{
$this->signatureAlgorithmManager = $signatureAlgorithmManager;
}
/**
* Returns the algorithm manager associated to the JWSVerifier.
*/
public function getSignatureAlgorithmManager(): AlgorithmManager
{
return $this->signatureAlgorithmManager;
}
/**
* This method will try to verify the JWS object using the given key and for the given signature.
* It returns true if the signature is verified, otherwise false.
*
* @return bool true if the verification of the signature succeeded, else false
*/
public function verifyWithKey(JWS $jws, JWK $jwk, int $signature, ?string $detachedPayload = null): bool
{
$jwkset = new JWKSet([$jwk]);
return $this->verifyWithKeySet($jws, $jwkset, $signature, $detachedPayload);
}
/**
* This method will try to verify the JWS object using the given key set and for the given signature.
* It returns true if the signature is verified, otherwise false.
*
* @param JWS $jws A JWS object
* @param JWKSet $jwkset The signature will be verified using keys in the key set
* @param JWK $jwk The key used to verify the signature in case of success
* @param null|string $detachedPayload If not null, the value must be the detached payload encoded in Base64 URL safe. If the input contains a payload, throws an exception.
*
* @throws InvalidArgumentException if there is no key in the keyset
* @throws InvalidArgumentException if the token does not contain any signature
*
* @return bool true if the verification of the signature succeeded, else false
*/
public function verifyWithKeySet(JWS $jws, JWKSet $jwkset, int $signatureIndex, ?string $detachedPayload = null, JWK &$jwk = null): bool
{
if (0 === $jwkset->count()) {
throw new InvalidArgumentException('There is no key in the key set.');
}
if (0 === $jws->countSignatures()) {
throw new InvalidArgumentException('The JWS does not contain any signature.');
}
$this->checkPayload($jws, $detachedPayload);
$signature = $jws->getSignature($signatureIndex);
return $this->verifySignature($jws, $jwkset, $signature, $detachedPayload, $jwk);
}
private function verifySignature(JWS $jws, JWKSet $jwkset, Signature $signature, ?string $detachedPayload = null, JWK &$successJwk = null): bool
{
$input = $this->getInputToVerify($jws, $signature, $detachedPayload);
$algorithm = $this->getAlgorithm($signature);
foreach ($jwkset->all() as $jwk) {
try {
KeyChecker::checkKeyUsage($jwk, 'verification');
KeyChecker::checkKeyAlgorithm($jwk, $algorithm->name());
if (true === $algorithm->verify($jwk, $input, $signature->getSignature())) {
$successJwk = $jwk;
return true;
}
} catch (Throwable $e) {
//We do nothing, we continue with other keys
continue;
}
}
return false;
}
private function getInputToVerify(JWS $jws, Signature $signature, ?string $detachedPayload): string
{
$isPayloadEmpty = $this->isPayloadEmpty($jws->getPayload());
$encodedProtectedHeader = $signature->getEncodedProtectedHeader();
if (!$signature->hasProtectedHeaderParameter('b64') || true === $signature->getProtectedHeaderParameter('b64')) {
if (null !== $jws->getEncodedPayload()) {
return sprintf('%s.%s', $encodedProtectedHeader, $jws->getEncodedPayload());
}
$payload = $isPayloadEmpty ? $detachedPayload : $jws->getPayload();
return sprintf('%s.%s', $encodedProtectedHeader, Base64Url::encode($payload));
}
$payload = $isPayloadEmpty ? $detachedPayload : $jws->getPayload();
return sprintf('%s.%s', $encodedProtectedHeader, $payload);
}
/**
* @throws InvalidArgumentException if the payload is set when a detached payload is provided or no payload is defined
*/
private function checkPayload(JWS $jws, ?string $detachedPayload = null): void
{
$isPayloadEmpty = $this->isPayloadEmpty($jws->getPayload());
if (null !== $detachedPayload && !$isPayloadEmpty) {
throw new InvalidArgumentException('A detached payload is set, but the JWS already has a payload.');
}
if ($isPayloadEmpty && null === $detachedPayload) {
throw new InvalidArgumentException('The JWS has a detached payload, but no payload is provided.');
}
}
/**
* @throws InvalidArgumentException if the header parameter "alg" is missing or invalid
*
* @return MacAlgorithm|SignatureAlgorithm
*/
private function getAlgorithm(Signature $signature): Algorithm
{
$completeHeader = array_merge($signature->getProtectedHeader(), $signature->getHeader());
if (!isset($completeHeader['alg'])) {
throw new InvalidArgumentException('No "alg" parameter set in the header.');
}
$algorithm = $this->signatureAlgorithmManager->get($completeHeader['alg']);
if (!$algorithm instanceof SignatureAlgorithm && !$algorithm instanceof MacAlgorithm) {
throw new InvalidArgumentException(sprintf('The algorithm "%s" is not supported or is not a signature or MAC algorithm.', $completeHeader['alg']));
}
return $algorithm;
}
private function isPayloadEmpty(?string $payload): bool
{
return null === $payload || '' === $payload;
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2020 Spomky-Labs
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*/
namespace Jose\Component\Signature;
use Jose\Component\Core\AlgorithmManagerFactory;
class JWSVerifierFactory
{
/**
* @var AlgorithmManagerFactory
*/
private $algorithmManagerFactory;
public function __construct(AlgorithmManagerFactory $algorithmManagerFactory)
{
$this->algorithmManagerFactory = $algorithmManagerFactory;
}
/**
* Creates a JWSVerifier using the given signature algorithm aliases.
*
* @param string[] $algorithms
*/
public function create(array $algorithms): JWSVerifier
{
$algorithmManager = $this->algorithmManagerFactory->create($algorithms);
return new JWSVerifier($algorithmManager);
}
}

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2019 Spomky-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Some files were not shown because too many files have changed in this diff Show More