first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
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,66 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core;
use function array_key_exists;
use InvalidArgumentException;
class AlgorithmManager
{
private array $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
*/
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,72 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core;
use InvalidArgumentException;
use function is_string;
/**
* @see \Jose\Tests\Component\Core\AlgorithmManagerFactoryTest
*/
class AlgorithmManagerFactory
{
private array $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
*/
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,127 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core;
use function array_key_exists;
use function in_array;
use InvalidArgumentException;
use function is_array;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
use JsonSerializable;
use ParagonIE\ConstantTime\Base64UrlSafe;
/**
* @see \Jose\Tests\Component\Core\JWKTest
*/
class JWK implements JsonSerializable
{
private array $values = [];
/**
* Creates a JWK object using the given values. The member "kty" is mandatory. Other members are NOT checked.
*/
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.
*/
public static function createFromJson(string $json): self
{
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
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
*
* @return mixed|null
*/
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
*/
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 ($input === false) {
throw new InvalidArgumentException('Unable to compute the key thumbprint');
}
return Base64UrlSafe::encodeUnpadded(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".
*/
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,296 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core;
use function array_key_exists;
use ArrayIterator;
use function count;
use const COUNT_NORMAL;
use Countable;
use function in_array;
use InvalidArgumentException;
use function is_array;
use IteratorAggregate;
use const JSON_THROW_ON_ERROR;
use JsonSerializable;
use Traversable;
class JWKSet implements Countable, IteratorAggregate, JsonSerializable
{
private array $keys = [];
/**
* @param JWK[] $keys
*/
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.
*/
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.
*/
public static function createFromJson(string $json): self
{
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
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.
*/
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
*/
public function without(int|string $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.
*/
public function has(int|string $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.
*/
public function get(int|string $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 Algorithm|null $algorithm Specifies the algorithm to be used
* @param array<string, mixed> $restrictions More restrictions such as 'kid' or 'kty'
*/
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 ($can_use === false) {
continue;
}
$ind += $can_use;
$alg = $this->canKeyBeUsedWithAlgorithm($algorithm, $key);
if ($alg === false) {
continue;
}
$ind += $alg;
if ($this->doesKeySatisfyRestrictions($restrictions, $key) === false) {
continue;
}
$result[] = [
'key' => $key,
'ind' => $ind,
];
}
if (count($result) === 0) {
return null;
}
usort($result, [$this, 'sortKeys']);
return $result[0]['key'];
}
/**
* Internal method only. Should not be used.
*
* @internal
*/
public static function sortKeys(array $a, array $b): int
{
return $b['ind'] <=> $a['ind'];
}
/**
* Internal method only. Should not be used.
*
* @internal
*/
public function getIterator(): Traversable
{
return new ArrayIterator($this->keys);
}
private function canKeyBeUsedFor(string $type, JWK $key): bool|int
{
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;
}
private function canKeyBeUsedWithAlgorithm(?Algorithm $algorithm, JWK $key): bool|int
{
if ($algorithm === null) {
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;
}
private static function convertKeyOpsToKeyUse(array $key_ops): string
{
return match (true) {
in_array('verify', $key_ops, true), in_array('sign', $key_ops, true) => 'sig',
in_array('encrypt', $key_ops, true), in_array('decrypt', $key_ops, true), in_array(
'wrapKey',
$key_ops,
true
), in_array(
'unwrapKey',
$key_ops,
true
), in_array('deriveKey', $key_ops, true), in_array('deriveBits', $key_ops, true) => 'enc',
default => throw new InvalidArgumentException(sprintf(
'Unsupported key operation value "%s"',
implode(', ', $key_ops)
)),
};
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
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,163 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use Brick\Math\BigInteger as BrickBigInteger;
use function chr;
use InvalidArgumentException;
/**
* @internal
*/
final class BigInteger
{
private function __construct(
private readonly BrickBigInteger $value
) {
}
public static function createFromBinaryString(string $value): self
{
$res = unpack('H*', $value);
if ($res === false) {
throw new InvalidArgumentException('Unable to convert the value');
}
$data = current($res);
return new self(BrickBigInteger::fromBase($data, 16));
}
public static function createFromDecimal(int $value): self
{
return new self(BrickBigInteger::of($value));
}
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 ($temp === false) {
throw new InvalidArgumentException('Unable to convert the value into bytes');
}
return ltrim($temp, chr(0));
}
/**
* Adds two BigIntegers.
*/
public function add(self $y): self
{
$value = $this->value->plus($y->value);
return new self($value);
}
/**
* Subtracts two BigIntegers.
*/
public function subtract(self $y): self
{
$value = $this->value->minus($y->value);
return new self($value);
}
/**
* Multiplies two BigIntegers.
*/
public function multiply(self $x): self
{
$value = $this->value->multipliedBy($x->value);
return new self($value);
}
/**
* Divides two BigIntegers.
*/
public function divide(self $x): self
{
$value = $this->value->dividedBy($x->value);
return new self($value);
}
/**
* Performs modular exponentiation.
*/
public function modPow(self $e, self $n): self
{
$value = $this->value->modPow($e->value, $n->value);
return new self($value);
}
/**
* Performs modular exponentiation.
*/
public function mod(self $d): self
{
$value = $this->value->mod($d->value);
return new self($value);
}
public function modInverse(self $m): self
{
return new self($this->value->modInverse($m->value));
}
/**
* Compares two numbers.
*/
public function compare(self $y): int
{
return $this->value->compareTo($y->value);
}
public function equals(self $y): bool
{
return $this->value->isEqualTo($y->value);
}
public static function random(self $y): self
{
return new self(BrickBigInteger::randomRange(0, $y->value));
}
public function gcd(self $y): self
{
return new self($this->value->gcd($y->value));
}
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,328 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use function extension_loaded;
use InvalidArgumentException;
use function is_array;
use function is_string;
use Jose\Component\Core\JWK;
use const OPENSSL_KEYTYPE_EC;
use ParagonIE\ConstantTime\Base64UrlSafe;
use const PHP_EOL;
use RuntimeException;
use const STR_PAD_LEFT;
/**
* @internal
*/
final class ECKey
{
public static function convertToPEM(JWK $jwk): string
{
if ($jwk->has('d')) {
return self::convertPrivateKeyToPEM($jwk);
}
return self::convertPublicKeyToPEM($jwk);
}
public static function convertPublicKeyToPEM(JWK $jwk): string
{
$der = match ($jwk->get('crv')) {
'P-256' => self::p256PublicKey(),
'secp256k1' => self::p256KPublicKey(),
'P-384' => self::p384PublicKey(),
'P-521' => self::p521PublicKey(),
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);
return $pem . ('-----END PUBLIC KEY-----' . PHP_EOL);
}
public static function convertPrivateKeyToPEM(JWK $jwk): string
{
$der = match ($jwk->get('crv')) {
'P-256' => self::p256PrivateKey($jwk),
'secp256k1' => self::p256KPrivateKey($jwk),
'P-384' => self::p384PrivateKey($jwk),
'P-521' => self::p521PrivateKey($jwk),
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);
return $pem . ('-----END EC PRIVATE KEY-----' . PHP_EOL);
}
/**
* 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);
}
private static function getNistCurveSize(string $curve): int
{
return match ($curve) {
'P-256', 'secp256k1' => 256,
'P-384' => 384,
'P-521' => 521,
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported.', $curve)),
};
}
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 ($key === false) {
throw new RuntimeException('Unable to create the key');
}
$result = openssl_pkey_export($key, $out);
if ($result === false) {
throw new RuntimeException('Unable to create the key');
}
$res = openssl_pkey_get_private($out);
if ($res === false) {
throw new RuntimeException('Unable to create the key');
}
$details = openssl_pkey_get_details($res);
if ($details === false) {
throw new InvalidArgumentException('Unable to get the key details');
}
$nistCurveSize = self::getNistCurveSize($curve);
return [
'kty' => 'EC',
'crv' => $curve,
'd' => Base64UrlSafe::encodeUnpadded(
str_pad((string) $details['ec']['d'], (int) ceil($nistCurveSize / 8), "\0", STR_PAD_LEFT)
),
'x' => Base64UrlSafe::encodeUnpadded(
str_pad((string) $details['ec']['x'], (int) ceil($nistCurveSize / 8), "\0", STR_PAD_LEFT)
),
'y' => Base64UrlSafe::encodeUnpadded(
str_pad((string) $details['ec']['y'], (int) ceil($nistCurveSize / 8), "\0", STR_PAD_LEFT)
),
];
}
private static function getOpensslCurveName(string $curve): string
{
return match ($curve) {
'P-256' => 'prime256v1',
'secp256k1' => 'secp256k1',
'P-384' => 'secp384r1',
'P-521' => '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 = $jwk->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Unable to get the private key');
}
$d = unpack('H*', str_pad(Base64UrlSafe::decode($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 = $jwk->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Unable to get the private key');
}
$d = unpack('H*', str_pad(Base64UrlSafe::decode($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 = $jwk->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Unable to get the private key');
}
$d = unpack('H*', str_pad(Base64UrlSafe::decode($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 = $jwk->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Unable to get the private key');
}
$d = unpack('H*', str_pad(Base64UrlSafe::decode($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
{
$crv = $jwk->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Unable to get the curve');
}
$nistCurveSize = self::getNistCurveSize($crv);
$length = (int) ceil($nistCurveSize / 8);
$x = $jwk->get('x');
if (! is_string($x)) {
throw new InvalidArgumentException('Unable to get the public key');
}
$y = $jwk->get('y');
if (! is_string($y)) {
throw new InvalidArgumentException('Unable to get the public key');
}
$binX = ltrim(Base64UrlSafe::decode($x), "\0");
$binY = ltrim(Base64UrlSafe::decode($y), "\0");
return "\04"
. str_pad($binX, $length, "\0", STR_PAD_LEFT)
. str_pad($binY, $length, "\0", STR_PAD_LEFT)
;
}
}

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
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;
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;
}
public static function fromAsn1(string $signature, int $length): string
{
$message = bin2hex($signature);
$position = 0;
if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_SEQUENCE) {
throw new InvalidArgumentException('Invalid data. Should start with a sequence.');
}
if (self::readAsn1Content($message, $position, self::BYTE_SIZE) === self::ASN1_LENGTH_2BYTES) {
$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 (mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit') === 0
&& 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;
}
private static function readAsn1Integer(string $message, int &$position): string
{
if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_INTEGER) {
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 (mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit') === 0
&& 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,61 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
/**
* @internal
*/
final class Hash
{
private function __construct(
private readonly string $hash,
private readonly int $length,
private readonly string $t
) {
}
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");
}
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");
}
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");
}
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,28 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
use RuntimeException;
use Throwable;
final class JsonConverter
{
public static function encode(mixed $payload): string
{
try {
return json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (Throwable $throwable) {
throw new RuntimeException('Invalid content.', $throwable->getCode(), $throwable);
}
}
public static function decode(string $payload): mixed
{
return json_decode($payload, true, 512, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
}

View File

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use function in_array;
use InvalidArgumentException;
use function is_array;
use function is_string;
use Jose\Component\Core\JWK;
/**
* @internal
*/
final 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);
}
}
public static function checkKeyAlgorithm(JWK $key, string $algorithm): void
{
if (! $key->has('alg')) {
return;
}
$alg = $key->get('alg');
if (! is_string($alg)) {
throw new InvalidArgumentException('Invalid algorithm.');
}
if ($alg !== $algorithm) {
throw new InvalidArgumentException(sprintf('Key is only allowed for algorithm "%s".', $alg));
}
}
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.');
}
}
private static function checkUsage(JWK $key, string $usage): void
{
$use = $key->get('use');
switch ($usage) {
case 'verification':
case 'signature':
if ($use !== 'sig') {
throw new InvalidArgumentException('Key cannot be used to sign or verify a signature.');
}
break;
case 'encryption':
case 'decryption':
if ($use !== 'enc') {
throw new InvalidArgumentException('Key cannot be used to encrypt or decrypt.');
}
break;
default:
throw new InvalidArgumentException('Unsupported key usage.');
}
}
}

View File

@ -0,0 +1,244 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use function array_key_exists;
use function count;
use InvalidArgumentException;
use function is_array;
use Jose\Component\Core\JWK;
use ParagonIE\ConstantTime\Base64UrlSafe;
use RuntimeException;
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
use SpomkyLabs\Pki\ASN1\Type\Primitive\BitString;
use SpomkyLabs\Pki\ASN1\Type\Primitive\Integer;
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
use SpomkyLabs\Pki\CryptoEncoding\PEM;
use SpomkyLabs\Pki\CryptoTypes\AlgorithmIdentifier\Asymmetric\RSAEncryptionAlgorithmIdentifier;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RSA\RSAPrivateKey;
use SpomkyLabs\Pki\CryptoTypes\Asymmetric\RSA\RSAPublicKey;
/**
* @internal
*/
final class RSAKey
{
private null|Sequence $sequence = null;
private readonly array $values;
private BigInteger $modulus;
private int $modulus_length;
private BigInteger $public_exponent;
private ?BigInteger $private_exponent = null;
/**
* @var BigInteger[]
*/
private array $primes = [];
/**
* @var BigInteger[]
*/
private array $exponents = [];
private ?BigInteger $coefficient = null;
private function __construct(JWK $data)
{
$this->values = $data->all();
$this->populateBigIntegers();
}
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 ($d !== null) {
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);
}
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 (array_key_exists('d', $this->values)) {
$this->sequence = Sequence::create(
Integer::create(0),
RSAEncryptionAlgorithmIdentifier::create()->toASN1(),
OctetString::create(
RSAPrivateKey::create(
$this->fromBase64ToInteger($this->values['n']),
$this->fromBase64ToInteger($this->values['e']),
$this->fromBase64ToInteger($this->values['d']),
isset($this->values['p']) ? $this->fromBase64ToInteger($this->values['p']) : '0',
isset($this->values['q']) ? $this->fromBase64ToInteger($this->values['q']) : '0',
isset($this->values['dp']) ? $this->fromBase64ToInteger($this->values['dp']) : '0',
isset($this->values['dq']) ? $this->fromBase64ToInteger($this->values['dq']) : '0',
isset($this->values['qi']) ? $this->fromBase64ToInteger($this->values['qi']) : '0',
)->toDER()
)
);
return PEM::create(PEM::TYPE_PRIVATE_KEY, $this->sequence->toDER())
->string();
}
$this->sequence = Sequence::create(
RSAEncryptionAlgorithmIdentifier::create()->toASN1(),
BitString::create(
RSAPublicKey::create(
$this->fromBase64ToInteger($this->values['n']),
$this->fromBase64ToInteger($this->values['e'])
)->toDER()
)
);
return PEM::create(PEM::TYPE_PUBLIC_KEY, $this->sequence->toDER())
->string();
}
/**
* Exponentiate with or without Chinese Remainder Theorem. Operation with primes 'p' and 'q' is appox. 2x faster.
*/
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() || $key->getCoefficient() === null || count($key->getPrimes()) === 0 || count(
$key->getExponents()
) === 0) {
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(Base64UrlSafe::decode($value));
}
private function fromBase64ToInteger(string $value): string
{
$unpacked = unpack('H*', Base64UrlSafe::decode($value));
if (! is_array($unpacked) || count($unpacked) === 0) {
throw new InvalidArgumentException('Unable to get the private key');
}
return \Brick\Math\BigInteger::fromBase(current($unpacked), 16)->toBase(10);
}
}

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
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 openssl_verify($input, $der, $pem, $this->getHashAlgorithm()) === 1;
} catch (Throwable) {
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,23 @@
<?php
declare(strict_types=1);
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,23 @@
<?php
declare(strict_types=1);
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,23 @@
<?php
declare(strict_types=1);
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,101 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use function extension_loaded;
use function in_array;
use InvalidArgumentException;
use function is_string;
use Jose\Component\Core\JWK;
use ParagonIE\ConstantTime\Base64UrlSafe;
use RuntimeException;
final class EdDSA implements SignatureAlgorithm
{
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'];
}
/**
* @return non-empty-string
*/
public function sign(JWK $key, string $input): string
{
$this->checkKey($key);
if (! $key->has('d')) {
throw new InvalidArgumentException('The EC key is not private');
}
$d = $key->get('d');
if (! is_string($d) || $d === '') {
throw new InvalidArgumentException('Invalid "d" parameter.');
}
if (! $key->has('x')) {
$x = sodium_crypto_sign_publickey_from_secretkey($d);
} else {
$x = $key->get('x');
}
if (! is_string($x) || $x === '') {
throw new InvalidArgumentException('Invalid "x" parameter.');
}
/** @var non-empty-string $x */
$x = Base64UrlSafe::decode($x);
/** @var non-empty-string $d */
$d = Base64UrlSafe::decode($d);
$secret = $d . $x;
return match ($key->get('crv')) {
'Ed25519' => sodium_crypto_sign_detached($input, $secret),
default => throw new InvalidArgumentException('Unsupported curve'),
};
}
/**
* @param non-empty-string $signature
*/
public function verify(JWK $key, string $input, string $signature): bool
{
$this->checkKey($key);
$x = $key->get('x');
if (! is_string($x)) {
throw new InvalidArgumentException('Invalid "x" parameter.');
}
/** @var non-empty-string $public */
$public = Base64UrlSafe::decode($x);
return match ($key->get('crv')) {
'Ed25519' => sodium_crypto_sign_verify_detached($signature, $input, $public),
default => throw new InvalidArgumentException('Unsupported curve'),
};
}
public function name(): string
{
return 'EdDSA';
}
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 ($key->get('crv') !== 'Ed25519') {
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,61 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use function in_array;
use InvalidArgumentException;
use function is_string;
use Jose\Component\Core\JWK;
use ParagonIE\ConstantTime\Base64UrlSafe;
/**
* @see \Jose\Tests\Component\Signature\Algorithm\Blake2bTest
*/
final class Blake2b implements MacAlgorithm
{
private const MINIMUM_KEY_LENGTH = 32;
public function allowedKeyTypes(): array
{
return ['oct'];
}
public function name(): string
{
return 'BLAKE2B';
}
public function verify(JWK $key, string $input, string $signature): bool
{
return hash_equals($this->hash($key, $input), $signature);
}
public function hash(JWK $key, string $input): string
{
$k = $this->getKey($key);
return sodium_crypto_generichash($input, $k);
}
private function getKey(JWK $key): string
{
if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
if (! $key->has('k')) {
throw new InvalidArgumentException('The key parameter "k" is missing.');
}
$k = $key->get('k');
if (! is_string($k)) {
throw new InvalidArgumentException('The key parameter "k" is invalid.');
}
$key = Base64UrlSafe::decode($k);
if (mb_strlen($key, '8bit') < self::MINIMUM_KEY_LENGTH) {
throw new InvalidArgumentException('Key provided is shorter than 256 bits.');
}
return $key;
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
final class ES256K extends ECDSA
{
public function name(): string
{
return 'ES256K';
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
protected function getSignaturePartLength(): int
{
return 64;
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
final class HS1 extends HMAC
{
public function name(): string
{
return 'HS1';
}
protected function getHashAlgorithm(): string
{
return 'sha1';
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use Jose\Component\Core\JWK;
final class HS256_64 extends HMAC
{
public function hash(JWK $key, string $input): string
{
$signature = parent::hash($key, $input);
return mb_substr($signature, 0, 8, '8bit');
}
public function name(): string
{
return 'HS256/64';
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
}

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,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
final class RS1 extends RSAPKCS1
{
public function name(): string
{
return 'RS1';
}
protected function getAlgorithm(): string
{
return 'sha1';
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use function in_array;
use InvalidArgumentException;
use function is_string;
use Jose\Component\Core\JWK;
use ParagonIE\ConstantTime\Base64UrlSafe;
abstract class HMAC implements MacAlgorithm
{
public function allowedKeyTypes(): array
{
return ['oct'];
}
public function verify(JWK $key, string $input, string $signature): bool
{
return hash_equals($this->hash($key, $input), $signature);
}
public function hash(JWK $key, string $input): string
{
$k = $this->getKey($key);
return hash_hmac($this->getHashAlgorithm(), $input, $k, true);
}
protected function getKey(JWK $key): string
{
if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
if (! $key->has('k')) {
throw new InvalidArgumentException('The key parameter "k" is missing.');
}
$k = $key->get('k');
if (! is_string($k)) {
throw new InvalidArgumentException('The key parameter "k" is invalid.');
}
return Base64UrlSafe::decode($k);
}
abstract protected function getHashAlgorithm(): string;
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
final class HS256 extends HMAC
{
public function name(): string
{
return 'HS256';
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
protected function getKey(JWK $key): string
{
$k = parent::getKey($key);
if (mb_strlen($k, '8bit') < 32) {
throw new InvalidArgumentException('Invalid key length.');
}
return $k;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
final class HS384 extends HMAC
{
public function name(): string
{
return 'HS384';
}
protected function getHashAlgorithm(): string
{
return 'sha384';
}
protected function getKey(JWK $key): string
{
$k = parent::getKey($key);
if (mb_strlen($k, '8bit') < 48) {
throw new InvalidArgumentException('Invalid key length.');
}
return $k;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
final class HS512 extends HMAC
{
public function name(): string
{
return 'HS512';
}
protected function getHashAlgorithm(): string
{
return 'sha512';
}
protected function getKey(JWK $key): string
{
$k = parent::getKey($key);
if (mb_strlen($k, '8bit') < 64) {
throw new InvalidArgumentException('Invalid key length.');
}
return $k;
}
}

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,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,41 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use function in_array;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
final class None implements SignatureAlgorithm
{
public function allowedKeyTypes(): array
{
return ['none'];
}
public function sign(JWK $key, string $input): string
{
$this->checkKey($key);
return '';
}
public function verify(JWK $key, string $input, string $signature): bool
{
return $signature === '';
}
public function name(): string
{
return 'none';
}
private function checkKey(JWK $key): void
{
if (! in_array($key->get('kty'), $this->allowedKeyTypes(), true)) {
throw new InvalidArgumentException('Wrong key type.');
}
}
}

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,18 @@
<?php
declare(strict_types=1);
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,18 @@
<?php
declare(strict_types=1);
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,18 @@
<?php
declare(strict_types=1);
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,18 @@
<?php
declare(strict_types=1);
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,18 @@
<?php
declare(strict_types=1);
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,18 @@
<?php
declare(strict_types=1);
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,58 @@
<?php
declare(strict_types=1);
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 openssl_verify($input, $signature, $pub->toPEM(), $this->getAlgorithm()) === 1;
}
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 ($result !== true) {
throw new RuntimeException('Unable to sign');
}
return $signature;
}
abstract protected function getAlgorithm(): string;
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,56 @@
<?php
declare(strict_types=1);
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);
}
/**
* @return non-empty-string
*/
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;
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,180 @@
<?php
declare(strict_types=1);
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;
use const STR_PAD_LEFT;
/**
* @internal
*/
final class RSA
{
/**
* Probabilistic Signature Scheme.
*/
public const SIGNATURE_PSS = 1;
/**
* Use the PKCS#1.
*/
public const SIGNATURE_PKCS1 = 2;
/**
* @return non-empty-string
*/
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 ($result !== true) {
throw new RuntimeException('Unable to sign the data');
}
return $signature;
default:
throw new InvalidArgumentException('Unsupported mode.');
}
}
/**
* Create a signature.
*
* @return non-empty-string
*/
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);
$result = self::convertIntegerToOctetString($signature, $key->getModulusLength());
if ($result === '') {
throw new InvalidArgumentException('Invalid signature.');
}
return $result;
}
public static function verify(RSAKey $key, string $message, string $signature, string $hash, int $mode): bool
{
return match ($mode) {
self::SIGNATURE_PSS => self::verifyWithPSS($key, $message, $signature, $hash),
self::SIGNATURE_PKCS1 => openssl_verify($message, $signature, $key->toPEM(), $hash) === 1,
default => throw new InvalidArgumentException('Unsupported mode.'),
};
}
/**
* Verifies a signature.
*/
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());
}
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.
*/
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];
return $maskedDB . $h . chr(0xBC);
}
/**
* EMSA-PSS-VERIFY.
*/
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 (ord($db[$temp]) !== 1) {
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);
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
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,28 @@
<?php
declare(strict_types=1);
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 non-empty-string $input The input
* @param non-empty-string $signature The signature to verify
*/
public function verify(JWK $key, string $input, string $signature): bool;
}

View File

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature;
use function count;
use InvalidArgumentException;
use Jose\Component\Core\JWT;
class JWS implements JWT
{
/**
* @var Signature[]
*/
private array $signatures = [];
public function __construct(
private readonly ?string $payload,
private readonly ?string $encodedPayload = null,
private readonly bool $isPayloadDetached = false
) {
}
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 ($this->isPayloadDetached() === true) {
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.
*/
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
*
* @param array{alg?: string, string?: mixed} $protectedHeader
* @param array{alg?: string, string?: mixed} $header
*/
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,223 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature;
use function array_key_exists;
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 ParagonIE\ConstantTime\Base64UrlSafe;
use RuntimeException;
class JWSBuilder
{
protected ?string $payload = null;
protected bool $isPayloadDetached = false;
/**
* @var array<array{
* header: array<string, mixed>,
* protected_header: array<string, mixed>,
* signature_key: JWK,
* signature_algorithm: Algorithm
* }>
*/
protected array $signatures = [];
protected ?bool $isPayloadEncoded = null;
public function __construct(
private readonly AlgorithmManager $signatureAlgorithmManager
) {
}
/**
* Returns the algorithm manager associated to the builder.
*/
public function getSignatureAlgorithmManager(): AlgorithmManager
{
return $this->signatureAlgorithmManager;
}
/**
* Reset the current data.
*/
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.
*/
public function withPayload(string $payload, bool $isPayloadDetached = false): self
{
if (mb_detect_encoding($payload, 'UTF-8', true) === false) {
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.
*
* @param array{alg?: string, string?: mixed} $protectedHeader
* @param array{alg?: string, string?: mixed} $header
*/
public function addSignature(JWK $signatureKey, array $protectedHeader, array $header = []): self
{
$this->checkB64AndCriticalHeader($protectedHeader);
$isPayloadEncoded = $this->checkIfPayloadIsEncoded($protectedHeader);
if ($this->isPayloadEncoded === null) {
$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.
*/
public function build(): JWS
{
if ($this->payload === null) {
throw new RuntimeException('The payload is not set.');
}
if (count($this->signatures) === 0) {
throw new RuntimeException('At least one signature must be set.');
}
$encodedPayload = $this->isPayloadEncoded === false ? $this->payload : Base64UrlSafe::encodeUnpadded(
$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<string, mixed> $protectedHeader */
$protectedHeader = $signature['protected_header'];
/** @var array<string, mixed> $header */
$header = $signature['header'];
$encodedProtectedHeader = count($protectedHeader) === 0 ? null : Base64UrlSafe::encodeUnpadded(
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;
}
/**
* @param array<string, mixed> $protectedHeader
*/
private function checkIfPayloadIsEncoded(array $protectedHeader): bool
{
return ! array_key_exists('b64', $protectedHeader) || $protectedHeader['b64'] === true;
}
/**
* @param array<string, mixed> $protectedHeader
*/
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.'
);
}
}
/**
* @param array{alg?: string, string?: mixed} $protectedHeader
* @param array{alg?: string, string?: mixed} $header
* @return MacAlgorithm|SignatureAlgorithm
*/
private function findSignatureAlgorithm(JWK $key, array $protectedHeader, array $header): Algorithm
{
$completeHeader = [...$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;
}
/**
* @param array<string, mixed> $header1
* @param array<string, mixed> $header2
*/
private function checkDuplicatedHeaderParameters(array $header1, array $header2): void
{
$inter = array_intersect_key($header1, $header2);
if (count($inter) !== 0) {
throw new InvalidArgumentException(sprintf(
'The header contains duplicated entries: %s.',
implode(', ', array_keys($inter))
));
}
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature;
use Jose\Component\Core\AlgorithmManagerFactory;
class JWSBuilderFactory
{
public function __construct(
private readonly AlgorithmManagerFactory $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,97 @@
<?php
declare(strict_types=1);
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
{
public function __construct(
private readonly JWSSerializerManager $serializerManager,
private readonly JWSVerifier $jwsVerifier,
private readonly ?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.
*/
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.
*/
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) {
// 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 ($this->headerCheckerManager !== null) {
$this->headerCheckerManager->check($jws, $signature);
}
return $this->jwsVerifier->verifyWithKeySet($jws, $keyset, $signature, $payload);
} catch (Throwable) {
return false;
}
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature;
use Jose\Component\Checker\HeaderCheckerManagerFactory;
use Jose\Component\Signature\Serializer\JWSSerializerManagerFactory;
class JWSLoaderFactory
{
public function __construct(
private readonly JWSSerializerManagerFactory $jwsSerializerManagerFactory,
private readonly JWSVerifierFactory $jwsVerifierFactory,
private readonly ?HeaderCheckerManagerFactory $headerCheckerManagerFactory
) {
}
/**
* Creates a JWSLoader using the given serializer aliases, signature algorithm aliases and (optionally) the header
* checker aliases.
*/
/**
* @param array<string> $serializers
* @param array<string> $algorithms
* @param array<string> $headerCheckers
*/
public function create(array $serializers, array $algorithms, array $headerCheckers = []): JWSLoader
{
$serializerManager = $this->jwsSerializerManagerFactory->create($serializers);
$jwsVerifier = $this->jwsVerifierFactory->create($algorithms);
if ($this->headerCheckerManagerFactory !== null) {
$headerCheckerManager = $this->headerCheckerManagerFactory->create($headerCheckers);
} else {
$headerCheckerManager = null;
}
return new JWSLoader($serializerManager, $jwsVerifier, $headerCheckerManager);
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
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;
}
/**
* @param array<string, mixed> $protectedHeader
* @param array<string, mixed> $unprotectedHeader
*/
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,163 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature;
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 ParagonIE\ConstantTime\Base64UrlSafe;
use Throwable;
class JWSVerifier
{
public function __construct(
private readonly AlgorithmManager $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 string|null $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.
*
* @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 ($jwkset->count() === 0) {
throw new InvalidArgumentException('There is no key in the key set.');
}
if ($jws->countSignatures() === 0) {
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 ($algorithm->verify($jwk, $input, $signature->getSignature()) === true) {
$successJwk = $jwk;
return true;
}
} catch (Throwable) {
//We do nothing, we continue with other keys
continue;
}
}
return false;
}
private function getInputToVerify(JWS $jws, Signature $signature, ?string $detachedPayload): string
{
$payload = $jws->getPayload();
$isPayloadEmpty = $payload === null || $payload === '';
$encodedProtectedHeader = $signature->getEncodedProtectedHeader() ?? '';
$isPayloadBase64Encoded = ! $signature->hasProtectedHeaderParameter(
'b64'
) || $signature->getProtectedHeaderParameter('b64') === true;
$encodedPayload = $jws->getEncodedPayload();
if ($isPayloadBase64Encoded && $encodedPayload !== null) {
return sprintf('%s.%s', $encodedProtectedHeader, $encodedPayload);
}
$callable = $isPayloadBase64Encoded === true ? static fn (?string $p): string => Base64UrlSafe::encodeUnpadded(
$p ?? ''
)
: static fn (?string $p): string => $p ?? '';
$payloadToUse = $callable($isPayloadEmpty ? $detachedPayload : $payload);
return sprintf('%s.%s', $encodedProtectedHeader, $payloadToUse);
}
private function checkPayload(JWS $jws, ?string $detachedPayload = null): void
{
$isPayloadEmpty = $this->isPayloadEmpty($jws->getPayload());
if ($detachedPayload !== null && ! $isPayloadEmpty) {
throw new InvalidArgumentException('A detached payload is set, but the JWS already has a payload.');
}
if ($isPayloadEmpty && $detachedPayload === null) {
throw new InvalidArgumentException('The JWS has a detached payload, but no payload is provided.');
}
}
/**
* @return MacAlgorithm|SignatureAlgorithm
*/
private function getAlgorithm(Signature $signature): Algorithm
{
$completeHeader = [...$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 $payload === null || $payload === '';
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature;
use Jose\Component\Core\AlgorithmManagerFactory;
class JWSVerifierFactory
{
public function __construct(
private readonly 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.

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Serializer;
use function count;
use InvalidArgumentException;
use function is_array;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Signature\JWS;
use LogicException;
use ParagonIE\ConstantTime\Base64UrlSafe;
use Throwable;
final class CompactSerializer extends Serializer
{
public const NAME = 'jws_compact';
public function displayName(): string
{
return 'JWS Compact';
}
public function name(): string
{
return self::NAME;
}
public function serialize(JWS $jws, ?int $signatureIndex = null): string
{
if ($signatureIndex === null) {
$signatureIndex = 0;
}
$signature = $jws->getSignature($signatureIndex);
if (count($signature->getHeader()) !== 0) {
throw new LogicException(
'The signature contains unprotected header parameters and cannot be converted into compact JSON.'
);
}
$isEmptyPayload = $jws->getEncodedPayload() === null || $jws->getEncodedPayload() === '';
if (! $isEmptyPayload && ! $this->isPayloadEncoded($signature->getProtectedHeader())) {
if (preg_match('/^[\x{20}-\x{2d}|\x{2f}-\x{7e}]*$/u', $jws->getPayload() ?? '') !== 1) {
throw new LogicException('Unable to convert the JWS with non-encoded payload.');
}
}
return sprintf(
'%s.%s.%s',
$signature->getEncodedProtectedHeader(),
$jws->getEncodedPayload(),
Base64UrlSafe::encodeUnpadded($signature->getSignature())
);
}
public function unserialize(string $input): JWS
{
$parts = explode('.', $input);
if (count($parts) !== 3) {
throw new InvalidArgumentException('Unsupported input');
}
try {
$encodedProtectedHeader = $parts[0];
$protectedHeader = JsonConverter::decode(Base64UrlSafe::decode($parts[0]));
if (! is_array($protectedHeader)) {
throw new InvalidArgumentException('Bad protected header.');
}
$hasPayload = $parts[1] !== '';
if (! $hasPayload) {
$payload = null;
$encodedPayload = null;
} else {
$encodedPayload = $parts[1];
$payload = $this->isPayloadEncoded($protectedHeader) ? Base64UrlSafe::decode(
$encodedPayload
) : $encodedPayload;
}
$signature = Base64UrlSafe::decode($parts[2]);
$jws = new JWS($payload, $encodedPayload, ! $hasPayload);
return $jws->addSignature($signature, $protectedHeader, $encodedProtectedHeader);
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Unsupported input', $throwable->getCode(), $throwable);
}
}
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Serializer;
use function count;
use InvalidArgumentException;
use function is_array;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Signature\JWS;
use ParagonIE\ConstantTime\Base64UrlSafe;
final class JSONFlattenedSerializer extends Serializer
{
public const NAME = 'jws_json_flattened';
public function displayName(): string
{
return 'JWS JSON Flattened';
}
public function name(): string
{
return self::NAME;
}
public function serialize(JWS $jws, ?int $signatureIndex = null): string
{
if ($signatureIndex === null) {
$signatureIndex = 0;
}
$signature = $jws->getSignature($signatureIndex);
$data = [];
$encodedPayload = $jws->getEncodedPayload();
if ($encodedPayload !== null && $encodedPayload !== '') {
$data['payload'] = $encodedPayload;
}
$encodedProtectedHeader = $signature->getEncodedProtectedHeader();
if ($encodedProtectedHeader !== null && $encodedProtectedHeader !== '') {
$data['protected'] = $encodedProtectedHeader;
}
$header = $signature->getHeader();
if (count($header) !== 0) {
$data['header'] = $header;
}
$data['signature'] = Base64UrlSafe::encodeUnpadded($signature->getSignature());
return JsonConverter::encode($data);
}
public function unserialize(string $input): JWS
{
$data = JsonConverter::decode($input);
if (! is_array($data)) {
throw new InvalidArgumentException('Unsupported input.');
}
if (! isset($data['signature'])) {
throw new InvalidArgumentException('Unsupported input.');
}
$signature = Base64UrlSafe::decode($data['signature']);
if (isset($data['protected'])) {
$encodedProtectedHeader = $data['protected'];
$protectedHeader = JsonConverter::decode(Base64UrlSafe::decode($data['protected']));
if (! is_array($protectedHeader)) {
throw new InvalidArgumentException('Bad protected header.');
}
} else {
$encodedProtectedHeader = null;
$protectedHeader = [];
}
if (isset($data['header'])) {
if (! is_array($data['header'])) {
throw new InvalidArgumentException('Bad header.');
}
$header = $data['header'];
} else {
$header = [];
}
if (isset($data['payload'])) {
$encodedPayload = $data['payload'];
$payload = $this->isPayloadEncoded($protectedHeader) ? Base64UrlSafe::decode(
$encodedPayload
) : $encodedPayload;
} else {
$payload = null;
$encodedPayload = null;
}
$jws = new JWS($payload, $encodedPayload, $encodedPayload === null);
return $jws->addSignature($signature, $protectedHeader, $encodedProtectedHeader, $header);
}
}

View File

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Serializer;
use function array_key_exists;
use function count;
use InvalidArgumentException;
use function is_array;
use function is_string;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Signature\JWS;
use LogicException;
use ParagonIE\ConstantTime\Base64UrlSafe;
final class JSONGeneralSerializer extends Serializer
{
public const NAME = 'jws_json_general';
public function displayName(): string
{
return 'JWS JSON General';
}
public function name(): string
{
return self::NAME;
}
public function serialize(JWS $jws, ?int $signatureIndex = null): string
{
if ($jws->countSignatures() === 0) {
throw new LogicException('No signature.');
}
$data = [];
$this->checkPayloadEncoding($jws);
if ($jws->isPayloadDetached() === false) {
$data['payload'] = $jws->getEncodedPayload();
}
$data['signatures'] = [];
foreach ($jws->getSignatures() as $signature) {
$tmp = [
'signature' => Base64UrlSafe::encodeUnpadded($signature->getSignature()),
];
$values = [
'protected' => $signature->getEncodedProtectedHeader(),
'header' => $signature->getHeader(),
];
foreach ($values as $key => $value) {
if ((is_string($value) && $value !== '') || (is_array($value) && count($value) !== 0)) {
$tmp[$key] = $value;
}
}
$data['signatures'][] = $tmp;
}
return JsonConverter::encode($data);
}
public function unserialize(string $input): JWS
{
$data = JsonConverter::decode($input);
if (! is_array($data)) {
throw new InvalidArgumentException('Unsupported input.');
}
if (! isset($data['signatures'])) {
throw new InvalidArgumentException('Unsupported input.');
}
$isPayloadEncoded = null;
$rawPayload = $data['payload'] ?? null;
$signatures = [];
foreach ($data['signatures'] as $signature) {
if (! isset($signature['signature'])) {
throw new InvalidArgumentException('Unsupported input.');
}
[$encodedProtectedHeader, $protectedHeader, $header] = $this->processHeaders($signature);
$signatures[] = [
'signature' => Base64UrlSafe::decode($signature['signature']),
'protected' => $protectedHeader,
'encoded_protected' => $encodedProtectedHeader,
'header' => $header,
];
$isPayloadEncoded = $this->processIsPayloadEncoded($isPayloadEncoded, $protectedHeader);
}
$payload = $this->processPayload($rawPayload, $isPayloadEncoded);
$jws = new JWS($payload, $rawPayload);
foreach ($signatures as $signature) {
$jws = $jws->addSignature(
$signature['signature'],
$signature['protected'],
$signature['encoded_protected'],
$signature['header']
);
}
return $jws;
}
/**
* @param array<string, mixed> $protectedHeader
*/
private function processIsPayloadEncoded(?bool $isPayloadEncoded, array $protectedHeader): bool
{
if ($isPayloadEncoded === null) {
return $this->isPayloadEncoded($protectedHeader);
}
if ($this->isPayloadEncoded($protectedHeader) !== $isPayloadEncoded) {
throw new InvalidArgumentException('Foreign payload encoding detected.');
}
return $isPayloadEncoded;
}
/**
* @param array{protected?: string, header?: array<string, mixed>} $signature
* @return array<mixed>
*/
private function processHeaders(array $signature): array
{
$encodedProtectedHeader = $signature['protected'] ?? null;
$protectedHeader = $encodedProtectedHeader === null ? [] : JsonConverter::decode(
Base64UrlSafe::decode($encodedProtectedHeader)
);
$header = array_key_exists('header', $signature) ? $signature['header'] : [];
return [$encodedProtectedHeader, $protectedHeader, $header];
}
private function processPayload(?string $rawPayload, ?bool $isPayloadEncoded): ?string
{
if ($rawPayload === null) {
return null;
}
return $isPayloadEncoded === false ? $rawPayload : Base64UrlSafe::decode($rawPayload);
}
private function checkPayloadEncoding(JWS $jws): void
{
if ($jws->isPayloadDetached()) {
return;
}
$is_encoded = null;
foreach ($jws->getSignatures() as $signature) {
if ($is_encoded === null) {
$is_encoded = $this->isPayloadEncoded($signature->getProtectedHeader());
}
if ($is_encoded !== $this->isPayloadEncoded($signature->getProtectedHeader())) {
throw new LogicException('Foreign payload encoding detected.');
}
}
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Serializer;
use Jose\Component\Signature\JWS;
interface JWSSerializer
{
/**
* The name of the serialization.
*/
public function name(): string;
public function displayName(): string;
/**
* Converts a JWS into a string.
*/
public function serialize(JWS $jws, ?int $signatureIndex = null): string;
/**
* Loads data and return a JWS object.
*
* @param string $input A string that represents a JWS
*/
public function unserialize(string $input): JWS;
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Serializer;
use InvalidArgumentException;
use Jose\Component\Signature\JWS;
class JWSSerializerManager
{
/**
* @var JWSSerializer[]
*/
private array $serializers = [];
/**
* @param JWSSerializer[] $serializers
*/
public function __construct(array $serializers)
{
foreach ($serializers as $serializer) {
$this->add($serializer);
}
}
/**
* @return string[]
*/
public function list(): array
{
return array_keys($this->serializers);
}
/**
* Converts a JWS into a string.
*/
public function serialize(string $name, JWS $jws, ?int $signatureIndex = null): string
{
if (! isset($this->serializers[$name])) {
throw new InvalidArgumentException(sprintf('Unsupported serializer "%s".', $name));
}
return $this->serializers[$name]->serialize($jws, $signatureIndex);
}
/**
* Loads data and return a JWS object.
*
* @param string $input A string that represents a JWS
* @param string|null $name the name of the serializer if the input is unserialized
*/
public function unserialize(string $input, ?string &$name = null): JWS
{
foreach ($this->serializers as $serializer) {
try {
$jws = $serializer->unserialize($input);
$name = $serializer->name();
return $jws;
} catch (InvalidArgumentException) {
continue;
}
}
throw new InvalidArgumentException('Unsupported input.');
}
private function add(JWSSerializer $serializer): void
{
$this->serializers[$serializer->name()] = $serializer;
}
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Serializer;
use InvalidArgumentException;
class JWSSerializerManagerFactory
{
/**
* @var JWSSerializer[]
*/
private array $serializers = [];
/**
* @param string[] $names
*/
public function create(array $names): JWSSerializerManager
{
$serializers = [];
foreach ($names as $name) {
if (! isset($this->serializers[$name])) {
throw new InvalidArgumentException(sprintf('Unsupported serializer "%s".', $name));
}
$serializers[] = $this->serializers[$name];
}
return new JWSSerializerManager($serializers);
}
/**
* @return string[]
*/
public function names(): array
{
return array_keys($this->serializers);
}
/**
* @return JWSSerializer[]
*/
public function all(): array
{
return $this->serializers;
}
public function add(JWSSerializer $serializer): void
{
$this->serializers[$serializer->name()] = $serializer;
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Serializer;
use function array_key_exists;
abstract class Serializer implements JWSSerializer
{
/**
* @param array<string, mixed> $protectedHeader
*/
protected function isPayloadEncoded(array $protectedHeader): bool
{
return ! array_key_exists('b64', $protectedHeader) || $protectedHeader['b64'] === true;
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature;
use function array_key_exists;
use InvalidArgumentException;
class Signature
{
private readonly ?string $encodedProtectedHeader;
/**
* @var array<string, mixed>
*/
private readonly array $protectedHeader;
/**
* @param array{alg?: string, string?: mixed} $protectedHeader
* @param array{alg?: string, string?: mixed} $header
*/
public function __construct(
private readonly string $signature,
array $protectedHeader,
?string $encodedProtectedHeader,
private readonly array $header
) {
$this->protectedHeader = $encodedProtectedHeader === null ? [] : $protectedHeader;
$this->encodedProtectedHeader = $encodedProtectedHeader;
}
/**
* The protected header associated with the signature.
*
* @return array<string, mixed>
*/
public function getProtectedHeader(): array
{
return $this->protectedHeader;
}
/**
* The unprotected header associated with the signature.
*
* @return array<string, mixed>
*/
public function getHeader(): array
{
return $this->header;
}
/**
* The protected header associated with the signature.
*/
public function getEncodedProtectedHeader(): ?string
{
return $this->encodedProtectedHeader;
}
/**
* Returns the value of the protected header of the specified key.
*
* @param string $key The key
*
* @return mixed|null Header value
*/
public function getProtectedHeaderParameter(string $key)
{
if ($this->hasProtectedHeaderParameter($key)) {
return $this->getProtectedHeader()[$key];
}
throw new InvalidArgumentException(sprintf('The protected header "%s" does not exist', $key));
}
/**
* Returns true if the protected header has the given parameter.
*
* @param string $key The key
*/
public function hasProtectedHeaderParameter(string $key): bool
{
return array_key_exists($key, $this->getProtectedHeader());
}
/**
* Returns the value of the unprotected header of the specified key.
*
* @param string $key The key
*
* @return mixed|null Header value
*/
public function getHeaderParameter(string $key)
{
if (array_key_exists($key, $this->header)) {
return $this->header[$key];
}
throw new InvalidArgumentException(sprintf('The header "%s" does not exist', $key));
}
/**
* Returns true if the unprotected header has the given parameter.
*
* @param string $key The key
*/
public function hasHeaderParameter(string $key): bool
{
return array_key_exists($key, $this->header);
}
/**
* Returns the value of the signature.
*/
public function getSignature(): string
{
return $this->signature;
}
}