primo commit

This commit is contained in:
2024-12-17 17:34:10 +01:00
commit e650f8df99
16435 changed files with 2451012 additions and 0 deletions

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\ECKey;
use Jose\Component\Core\Util\ECSignature;
use LogicException;
use RuntimeException;
use Throwable;
use function defined;
use function extension_loaded;
use function in_array;
abstract class ECDSA implements SignatureAlgorithm
{
public function __construct()
{
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
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,121 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use ParagonIE\Sodium\Core\Ed25519;
use RuntimeException;
use function assert;
use function extension_loaded;
use function in_array;
use function is_string;
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 = self::getPublicKey($key);
} else {
$x = $key->get('x');
}
if (! is_string($x) || $x === '') {
throw new InvalidArgumentException('Invalid "x" parameter.');
}
/** @var non-empty-string $x */
$x = Base64UrlSafe::decodeNoPadding($x);
/** @var non-empty-string $d */
$d = Base64UrlSafe::decodeNoPadding($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::decodeNoPadding($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 static function getPublicKey(JWK $key): string
{
$d = $key->get('d');
assert(is_string($d), 'Unsupported key type');
switch ($key->get('crv')) {
case 'Ed25519':
return Ed25519::publickey_from_secretkey($d);
case 'X25519':
if (extension_loaded('sodium')) {
return sodium_crypto_scalarmult_base($d);
}
// no break
default:
throw new InvalidArgumentException('Unsupported key type');
}
}
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,49 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use function in_array;
use function is_string;
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::decodeNoPadding($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,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,41 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use function in_array;
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,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,66 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\RSAKey;
use RuntimeException;
use function extension_loaded;
use function in_array;
abstract class RSAPKCS1 implements SignatureAlgorithm
{
public function __construct()
{
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
}
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 InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\RSAKey;
use Jose\Component\Signature\Algorithm\Util\RSA as JoseRSA;
use function in_array;
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,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,190 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Algorithm\Util;
use InvalidArgumentException;
use Jose\Component\Core\Util\BigInteger;
use Jose\Component\Core\Util\Hash;
use Jose\Component\Core\Util\RSAKey;
use RuntimeException;
use function chr;
use function extension_loaded;
use function ord;
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:
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
$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
{
switch ($mode) {
case self::SIGNATURE_PSS:
return self::verifyWithPSS($key, $message, $signature, $hash);
case self::SIGNATURE_PKCS1:
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
return 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,126 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature;
use InvalidArgumentException;
use Jose\Component\Core\JWT;
use function count;
/**
* @see \Jose\Tests\Component\Signature\JWSTest
*/
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,227 @@
<?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\Util\Base64UrlSafe;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Core\Util\KeyChecker;
use Jose\Component\Signature\Algorithm\MacAlgorithm;
use Jose\Component\Signature\Algorithm\SignatureAlgorithm;
use LogicException;
use RuntimeException;
use function array_key_exists;
use function count;
use function in_array;
use function is_array;
use function is_string;
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
{
$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<string, mixed> $protectedHeader
* @param array<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
);
if ($this->isPayloadEncoded === false && $this->isPayloadDetached === false) {
mb_detect_encoding($this->payload, 'UTF-8', true) !== false || throw new InvalidArgumentException(
'The payload must be encoded in UTF-8'
);
}
$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<string, mixed> $protectedHeader
* @param array<string, mixed> $header
* @return MacAlgorithm|SignatureAlgorithm
*/
private function findSignatureAlgorithm(JWK $key, array $protectedHeader, array $header): Algorithm
{
$completeHeader = [...$header, ...$protectedHeader];
$alg = $completeHeader['alg'] ?? null;
if (! is_string($alg)) {
throw new InvalidArgumentException('No "alg" parameter set in the header.');
}
$keyAlg = $key->has('alg') ? $key->get('alg') : null;
if (is_string($keyAlg) && $keyAlg !== $alg) {
throw new InvalidArgumentException(sprintf('The algorithm "%s" is not allowed with this key.', $alg));
}
$algorithm = $this->signatureAlgorithmManager->get($alg);
if (! $algorithm instanceof SignatureAlgorithm && ! $algorithm instanceof MacAlgorithm) {
throw new InvalidArgumentException(sprintf('The algorithm "%s" is not supported.', $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,100 @@
<?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;
/**
* @see \Jose\Tests\Component\Signature\JWSLoaderTest
*/
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\Base64UrlSafe;
use Jose\Component\Core\Util\KeyChecker;
use Jose\Component\Signature\Algorithm\MacAlgorithm;
use Jose\Component\Signature\Algorithm\SignatureAlgorithm;
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,88 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Signature\Serializer;
use InvalidArgumentException;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Signature\JWS;
use LogicException;
use Throwable;
use function count;
use function is_array;
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::decodeNoPadding($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::decodeNoPadding(
$encodedPayload
) : $encodedPayload;
}
$signature = Base64UrlSafe::decodeNoPadding($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 InvalidArgumentException;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Signature\JWS;
use function count;
use function is_array;
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::decodeNoPadding($data['signature']);
if (isset($data['protected'])) {
$encodedProtectedHeader = $data['protected'];
$protectedHeader = JsonConverter::decode(Base64UrlSafe::decodeNoPadding($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::decodeNoPadding(
$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 InvalidArgumentException;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Signature\JWS;
use LogicException;
use function array_key_exists;
use function count;
use function is_array;
use function is_string;
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::decodeNoPadding($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::decodeNoPadding($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::decodeNoPadding($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(iterable $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 InvalidArgumentException;
use function array_key_exists;
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;
}
}