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,46 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use function in_array;
use function is_string;
/**
* This class is a header parameter checker. When the "alg" header parameter is present, it will check if the value is
* within the allowed ones.
*/
final class AlgorithmChecker implements HeaderChecker
{
private const HEADER_NAME = 'alg';
/**
* @param string[] $supportedAlgorithms
*/
public function __construct(
private readonly array $supportedAlgorithms,
private readonly bool $protectedHeader = false
) {
}
public function checkHeader(mixed $value): void
{
if (! is_string($value)) {
throw new InvalidHeaderException('"alg" must be a string.', self::HEADER_NAME, $value);
}
if (! in_array($value, $this->supportedAlgorithms, true)) {
throw new InvalidHeaderException('Unsupported algorithm.', self::HEADER_NAME, $value);
}
}
public function supportedHeader(): string
{
return self::HEADER_NAME;
}
public function protectedHeaderOnly(): bool
{
return $this->protectedHeader;
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use function in_array;
use function is_array;
use function is_string;
/**
* This class is a header parameter and claim checker. When the "aud" header parameter or claim is present, it will
* check if the value is within the allowed ones.
*/
final class AudienceChecker implements ClaimChecker, HeaderChecker
{
private const CLAIM_NAME = 'aud';
public function __construct(
private readonly string $audience,
private readonly bool $protectedHeader = false
) {
}
public function checkClaim(mixed $value): void
{
$this->checkValue($value, InvalidClaimException::class);
}
public function checkHeader(mixed $value): void
{
$this->checkValue($value, InvalidHeaderException::class);
}
public function supportedClaim(): string
{
return self::CLAIM_NAME;
}
public function supportedHeader(): string
{
return self::CLAIM_NAME;
}
public function protectedHeaderOnly(): bool
{
return $this->protectedHeader;
}
private function checkValue(mixed $value, string $class): void
{
if (is_string($value) && $value !== $this->audience) {
throw new $class('Bad audience.', self::CLAIM_NAME, $value);
}
if (is_array($value) && ! in_array($this->audience, $value, true)) {
throw new $class('Bad audience.', self::CLAIM_NAME, $value);
}
if (! is_array($value) && ! is_string($value)) {
throw new $class('Bad audience.', self::CLAIM_NAME, $value);
}
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use InvalidArgumentException;
use function call_user_func;
use function is_callable;
/**
* @see \Jose\Tests\Component\Checker\CallableCheckerTest
*/
final class CallableChecker implements ClaimChecker, HeaderChecker
{
/**
* @param string $key The claim or header parameter name to check.
* @param callable(mixed $value): bool $callable The callable function that will be invoked.
*/
public function __construct(
private readonly string $key,
private $callable,
private readonly bool $protectedHeaderOnly = true
) {
if (! is_callable($this->callable)) { // @phpstan-ignore-line
throw new InvalidArgumentException('The $callable argument must be a callable.');
}
}
public function checkClaim(mixed $value): void
{
if (call_user_func($this->callable, $value) !== true) {
throw new InvalidClaimException(sprintf('The "%s" claim is invalid.', $this->key), $this->key, $value);
}
}
public function supportedClaim(): string
{
return $this->key;
}
public function checkHeader(mixed $value): void
{
if (call_user_func($this->callable, $value) !== true) {
throw new InvalidHeaderException(sprintf('The "%s" header is invalid.', $this->key), $this->key, $value);
}
}
public function supportedHeader(): string
{
return $this->key;
}
public function protectedHeaderOnly(): bool
{
return $this->protectedHeaderOnly;
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
interface ClaimChecker
{
/**
* When the token has the applicable claim, the value is checked. If for some reason the value is not valid, an
* InvalidClaimException must be thrown.
*/
public function checkClaim(mixed $value): void;
/**
* The method returns the claim to be checked.
*/
public function supportedClaim(): string;
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use function array_key_exists;
use function count;
/**
* This manager handles as many claim checkers as needed.
*
* @see \Jose\Tests\Component\Checker\ClaimCheckerManagerTest
*/
class ClaimCheckerManager
{
/**
* @var ClaimChecker[]
*/
private array $checkers = [];
/**
* @param ClaimChecker[] $checkers
*/
public function __construct(iterable $checkers)
{
foreach ($checkers as $checker) {
$this->add($checker);
}
}
/**
* This method returns all checkers handled by this manager.
*
* @return ClaimChecker[]
*/
public function getCheckers(): array
{
return $this->checkers;
}
/**
* This method checks all the claims passed as argument. All claims are checked against the claim checkers. If one
* fails, the InvalidClaimException is thrown.
*
* This method returns an array with all checked claims. It is up to the implementor to decide use the claims that
* have not been checked.
*
* @param string[] $mandatoryClaims
*/
public function check(array $claims, array $mandatoryClaims = []): array
{
$this->checkMandatoryClaims($mandatoryClaims, $claims);
$checkedClaims = [];
foreach ($this->checkers as $claim => $checker) {
if (array_key_exists($claim, $claims)) {
$checker->checkClaim($claims[$claim]);
$checkedClaims[$claim] = $claims[$claim];
}
}
return $checkedClaims;
}
private function add(ClaimChecker $checker): void
{
$claim = $checker->supportedClaim();
$this->checkers[$claim] = $checker;
}
/**
* @param string[] $mandatoryClaims
*/
private function checkMandatoryClaims(array $mandatoryClaims, array $claims): void
{
if (count($mandatoryClaims) === 0) {
return;
}
$diff = array_keys(array_diff_key(array_flip($mandatoryClaims), $claims));
if (count($diff) !== 0) {
throw new MissingMandatoryClaimException(sprintf(
'The following claims are mandatory: %s.',
implode(', ', $diff)
), $diff);
}
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use InvalidArgumentException;
/**
* @see \Jose\Tests\Component\Checker\ClaimCheckerManagerFactoryTest
*/
class ClaimCheckerManagerFactory
{
/**
* @var ClaimChecker[]
*/
private array $checkers = [];
/**
* This method creates a Claim Checker Manager and populate it with the claim checkers found based on the alias. If
* the alias is not supported, an InvalidArgumentException is thrown.
*
* @param string[] $aliases
*/
public function create(array $aliases): ClaimCheckerManager
{
$checkers = [];
foreach ($aliases as $alias) {
if (! isset($this->checkers[$alias])) {
throw new InvalidArgumentException(sprintf(
'The claim checker with the alias "%s" is not supported.',
$alias
));
}
$checkers[] = $this->checkers[$alias];
}
return new ClaimCheckerManager($checkers);
}
/**
* This method adds a claim checker to this factory.
*/
public function add(string $alias, ClaimChecker $checker): void
{
$this->checkers[$alias] = $checker;
}
/**
* Returns all claim checker aliases supported by this factory.
*
* @return string[]
*/
public function aliases(): array
{
return array_keys($this->checkers);
}
/**
* Returns all claim checkers supported by this factory.
*
* @return ClaimChecker[]
*/
public function all(): array
{
return $this->checkers;
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use Throwable;
/**
* Exceptions thrown by this component.
*/
interface ClaimExceptionInterface extends Throwable
{
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use Psr\Clock\ClockInterface;
use function is_float;
use function is_int;
/**
* This class is a claim checker. When the "exp" is present, it will compare the value with the current timestamp.
*/
final class ExpirationTimeChecker implements ClaimChecker, HeaderChecker
{
private const NAME = 'exp';
private readonly ClockInterface $clock;
public function __construct(
private readonly int $allowedTimeDrift = 0,
private readonly bool $protectedHeaderOnly = false,
?ClockInterface $clock = null,
) {
if ($clock === null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$clock" will become mandatory in 4.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}
$this->clock = $clock;
}
public function checkClaim(mixed $value): void
{
if (! is_float($value) && ! is_int($value)) {
throw new InvalidClaimException('"exp" must be an integer.', self::NAME, $value);
}
$now = $this->clock->now()
->getTimestamp();
if ($now > $value + $this->allowedTimeDrift) {
throw new InvalidClaimException('The token expired.', self::NAME, $value);
}
}
public function supportedClaim(): string
{
return self::NAME;
}
public function checkHeader(mixed $value): void
{
if (! is_float($value) && ! is_int($value)) {
throw new InvalidHeaderException('"exp" must be an integer.', self::NAME, $value);
}
$now = $this->clock->now()
->getTimestamp();
if ($now > $value + $this->allowedTimeDrift) {
throw new InvalidHeaderException('The token expired.', self::NAME, $value);
}
}
public function supportedHeader(): string
{
return self::NAME;
}
public function protectedHeaderOnly(): bool
{
return $this->protectedHeaderOnly;
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
interface HeaderChecker
{
/**
* This method is called when the header parameter is present. If for some reason the value is not valid, an
* InvalidHeaderException must be thrown.
*/
public function checkHeader(mixed $value): void;
/**
* The method returns the header parameter to be checked.
*/
public function supportedHeader(): string;
/**
* When true, the header parameter to be checked MUST be set in the protected header of the token.
*/
public function protectedHeaderOnly(): bool;
}

View File

@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use InvalidArgumentException;
use Jose\Component\Core\JWT;
use function array_key_exists;
use function count;
use function is_array;
class HeaderCheckerManager
{
/**
* @var HeaderChecker[]
*/
private array $checkers = [];
/**
* @var TokenTypeSupport[]
*/
private array $tokenTypes = [];
/**
* HeaderCheckerManager constructor.
*
* @param HeaderChecker[] $checkers
* @param TokenTypeSupport[] $tokenTypes
*/
public function __construct(iterable $checkers, iterable $tokenTypes)
{
foreach ($checkers as $checker) {
$this->add($checker);
}
foreach ($tokenTypes as $tokenType) {
$this->addTokenTypeSupport($tokenType);
}
}
/**
* This method returns all checkers handled by this manager.
*
* @return HeaderChecker[]
*/
public function getCheckers(): array
{
return $this->checkers;
}
/**
* This method checks all the header parameters passed as argument. All header parameters are checked against the
* header parameter checkers. If one fails, the InvalidHeaderException is thrown.
*
* @param string[] $mandatoryHeaderParameters
*/
public function check(JWT $jwt, int $index, array $mandatoryHeaderParameters = []): void
{
foreach ($this->tokenTypes as $tokenType) {
if ($tokenType->supports($jwt)) {
$protected = [];
$unprotected = [];
$tokenType->retrieveTokenHeaders($jwt, $index, $protected, $unprotected);
$this->checkDuplicatedHeaderParameters($protected, $unprotected);
$this->checkMandatoryHeaderParameters($mandatoryHeaderParameters, $protected, $unprotected);
$this->checkHeaders($protected, $unprotected);
return;
}
}
throw new InvalidArgumentException('Unsupported token type.');
}
private function addTokenTypeSupport(TokenTypeSupport $tokenType): void
{
$this->tokenTypes[] = $tokenType;
}
private function add(HeaderChecker $checker): void
{
$header = $checker->supportedHeader();
$this->checkers[$header] = $checker;
}
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))
));
}
}
/**
* @param string[] $mandatoryHeaderParameters
*/
private function checkMandatoryHeaderParameters(
array $mandatoryHeaderParameters,
array $protected,
array $unprotected
): void {
if (count($mandatoryHeaderParameters) === 0) {
return;
}
$diff = array_keys(
array_diff_key(array_flip($mandatoryHeaderParameters), array_merge($protected, $unprotected))
);
if (count($diff) !== 0) {
throw new MissingMandatoryHeaderParameterException(sprintf(
'The following header parameters are mandatory: %s.',
implode(', ', $diff)
), $diff);
}
}
private function checkHeaders(array $protected, array $header): void
{
$checkedHeaderParameters = [];
foreach ($this->checkers as $headerParameter => $checker) {
if ($checker->protectedHeaderOnly()) {
if (array_key_exists($headerParameter, $protected)) {
$checker->checkHeader($protected[$headerParameter]);
$checkedHeaderParameters[] = $headerParameter;
} elseif (array_key_exists($headerParameter, $header)) {
throw new InvalidHeaderException(sprintf(
'The header parameter "%s" must be protected.',
$headerParameter
), $headerParameter, $header[$headerParameter]);
}
} else {
if (array_key_exists($headerParameter, $protected)) {
$checker->checkHeader($protected[$headerParameter]);
$checkedHeaderParameters[] = $headerParameter;
} elseif (array_key_exists($headerParameter, $header)) {
$checker->checkHeader($header[$headerParameter]);
$checkedHeaderParameters[] = $headerParameter;
}
}
}
$this->checkCriticalHeader($protected, $header, $checkedHeaderParameters);
}
private function checkCriticalHeader(array $protected, array $header, array $checkedHeaderParameters): void
{
if (array_key_exists('crit', $protected)) {
if (! is_array($protected['crit'])) {
throw new InvalidHeaderException(
'The header "crit" must be a list of header parameters.',
'crit',
$protected['crit']
);
}
$diff = array_diff($protected['crit'], $checkedHeaderParameters);
if (count($diff) !== 0) {
throw new InvalidHeaderException(sprintf(
'One or more header parameters are marked as critical, but they are missing or have not been checked: %s.',
implode(', ', array_values($diff))
), 'crit', $protected['crit']);
}
} elseif (array_key_exists('crit', $header)) {
throw new InvalidHeaderException('The header parameter "crit" must be protected.', 'crit', $header['crit']);
}
}
}

View File

@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use InvalidArgumentException;
/**
* @see \Jose\Tests\Component\Checker\HeaderCheckerManagerFactoryTest
*/
class HeaderCheckerManagerFactory
{
/**
* @var HeaderChecker[]
*/
private array $checkers = [];
/**
* @var TokenTypeSupport[]
*/
private array $tokenTypes = [];
/**
* This method creates a Header Checker Manager and populate it with the header parameter checkers found based on
* the alias. If the alias is not supported, an InvalidArgumentException is thrown.
*
* @param string[] $aliases
*/
public function create(array $aliases): HeaderCheckerManager
{
$checkers = [];
foreach ($aliases as $alias) {
if (! isset($this->checkers[$alias])) {
throw new InvalidArgumentException(sprintf(
'The header checker with the alias "%s" is not supported.',
$alias
));
}
$checkers[] = $this->checkers[$alias];
}
return new HeaderCheckerManager($checkers, $this->tokenTypes);
}
/**
* This method adds a header parameter checker to this factory. The checker is uniquely identified by an alias. This
* allows the same header parameter checker to be added twice (or more) using several configuration options.
*/
public function add(string $alias, HeaderChecker $checker): void
{
$this->checkers[$alias] = $checker;
}
/**
* This method adds a token type support to this factory.
*/
public function addTokenTypeSupport(TokenTypeSupport $tokenType): void
{
$this->tokenTypes[] = $tokenType;
}
/**
* Returns all header parameter checker aliases supported by this factory.
*
* @return string[]
*/
public function aliases(): array
{
return array_keys($this->checkers);
}
/**
* Returns all header parameter checkers supported by this factory.
*
* @return HeaderChecker[]
*/
public function all(): array
{
return $this->checkers;
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use DateTimeImmutable;
use Psr\Clock\ClockInterface;
/**
* @internal
*/
final class InternalClock implements ClockInterface
{
public function now(): DateTimeImmutable
{
return new DateTimeImmutable();
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use Exception;
/**
* This exception is thrown by claim checkers when a claim check failed.
*/
class InvalidClaimException extends Exception implements ClaimExceptionInterface
{
public function __construct(
string $message,
private readonly string $claim,
private readonly mixed $value
) {
parent::__construct($message);
}
/**
* Returns the claim that caused the exception.
*/
public function getClaim(): string
{
return $this->claim;
}
/**
* Returns the claim value that caused the exception.
*/
public function getValue(): mixed
{
return $this->value;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use Exception;
/**
* This exception is thrown by header parameter checkers when a header parameter check failed.
*/
class InvalidHeaderException extends Exception
{
public function __construct(
string $message,
private readonly string $header,
private readonly mixed $value
) {
parent::__construct($message);
}
/**
* Returns the header parameter that caused the exception.
*/
public function getHeader(): string
{
return $this->header;
}
/**
* Returns the header parameter value that caused the exception.
*/
public function getValue(): mixed
{
return $this->value;
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
/**
* @see \Jose\Tests\Component\Checker\IsEqualCheckerTest
*/
final class IsEqualChecker implements ClaimChecker, HeaderChecker
{
/**
* @param string $key The claim or header parameter name to check.
* @param mixed $value The expected value.
* @param bool $protectedHeaderOnly [optional] Whether the header parameter MUST be protected.
* This option has no effect for claim checkers.
*/
public function __construct(
private readonly string $key,
private readonly mixed $value,
private readonly bool $protectedHeaderOnly = true
) {
}
public function checkClaim(mixed $value): void
{
if ($value !== $this->value) {
throw new InvalidClaimException(sprintf('The "%s" claim is invalid.', $this->key), $this->key, $value);
}
}
public function supportedClaim(): string
{
return $this->key;
}
public function checkHeader(mixed $value): void
{
if ($value !== $this->value) {
throw new InvalidHeaderException(sprintf('The "%s" header is invalid.', $this->key), $this->key, $value);
}
}
public function supportedHeader(): string
{
return $this->key;
}
public function protectedHeaderOnly(): bool
{
return $this->protectedHeaderOnly;
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use Psr\Clock\ClockInterface;
use function is_float;
use function is_int;
/**
* This class is a claim checker. When the "iat" is present, it will compare the value with the current timestamp.
*/
final class IssuedAtChecker implements ClaimChecker, HeaderChecker
{
private const NAME = 'iat';
private readonly ClockInterface $clock;
public function __construct(
private readonly int $allowedTimeDrift = 0,
private readonly bool $protectedHeaderOnly = false,
?ClockInterface $clock = null,
) {
if ($clock === null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$clock" will become mandatory in 4.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}
$this->clock = $clock;
}
public function checkClaim(mixed $value): void
{
if (! is_float($value) && ! is_int($value)) {
throw new InvalidClaimException('"iat" must be an integer.', self::NAME, $value);
}
$now = $this->clock->now()
->getTimestamp();
if ($now < $value - $this->allowedTimeDrift) {
throw new InvalidClaimException('The JWT is issued in the future.', self::NAME, $value);
}
}
public function supportedClaim(): string
{
return self::NAME;
}
public function checkHeader(mixed $value): void
{
if (! is_float($value) && ! is_int($value)) {
throw new InvalidHeaderException('The header "iat" must be an integer.', self::NAME, $value);
}
$now = $this->clock->now()
->getTimestamp();
if ($now < $value - $this->allowedTimeDrift) {
throw new InvalidHeaderException('The JWT is issued in the future.', self::NAME, $value);
}
}
public function supportedHeader(): string
{
return self::NAME;
}
public function protectedHeaderOnly(): bool
{
return $this->protectedHeaderOnly;
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use function in_array;
use function is_string;
/**
* This class is a header parameter and claim checker. When the "iss" header parameter or claim is present, it will
* check if the value is within the allowed ones.
*/
final class IssuerChecker implements ClaimChecker, HeaderChecker
{
private const CLAIM_NAME = 'iss';
public function __construct(
private readonly array $issuers,
private readonly bool $protectedHeader = false
) {
}
public function checkClaim(mixed $value): void
{
$this->checkValue($value, InvalidClaimException::class);
}
public function checkHeader(mixed $value): void
{
$this->checkValue($value, InvalidHeaderException::class);
}
public function supportedClaim(): string
{
return self::CLAIM_NAME;
}
public function supportedHeader(): string
{
return self::CLAIM_NAME;
}
public function protectedHeaderOnly(): bool
{
return $this->protectedHeader;
}
private function checkValue(mixed $value, string $class): void
{
if (! is_string($value)) {
throw new $class('Invalid value.', self::CLAIM_NAME, $value);
}
if (! in_array($value, $this->issuers, true)) {
throw new $class('Unknown issuer.', self::CLAIM_NAME, $value);
}
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use Exception;
class MissingMandatoryClaimException extends Exception implements ClaimExceptionInterface
{
/**
* MissingMandatoryClaimException constructor.
*
* @param string[] $claims
*/
public function __construct(
string $message,
private readonly array $claims
) {
parent::__construct($message);
}
/**
* @return string[]
*/
public function getClaims(): array
{
return $this->claims;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use Exception;
class MissingMandatoryHeaderParameterException extends Exception
{
/**
* MissingMandatoryHeaderParameterException constructor.
*
* @param string[] $parameters
*/
public function __construct(
string $message,
private readonly array $parameters
) {
parent::__construct($message);
}
/**
* @return string[]
*/
public function getParameters(): array
{
return $this->parameters;
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use Psr\Clock\ClockInterface;
use function is_float;
use function is_int;
/**
* This class is a claim checker. When the "nbf" is present, it will compare the value with the current timestamp.
*/
final class NotBeforeChecker implements ClaimChecker, HeaderChecker
{
private const NAME = 'nbf';
private readonly ClockInterface $clock;
public function __construct(
private readonly int $allowedTimeDrift = 0,
private readonly bool $protectedHeaderOnly = false,
?ClockInterface $clock = null,
) {
if ($clock === null) {
trigger_deprecation(
'web-token/jwt-library',
'3.3.0',
'The parameter "$clock" will become mandatory in 4.0.0. Please set a valid PSR Clock implementation instead of "null".'
);
$clock = new InternalClock();
}
$this->clock = $clock;
}
public function checkClaim(mixed $value): void
{
if (! is_float($value) && ! is_int($value)) {
throw new InvalidClaimException('"nbf" must be an integer.', self::NAME, $value);
}
$now = $this->clock->now()
->getTimestamp();
if ($now < $value - $this->allowedTimeDrift) {
throw new InvalidClaimException('The JWT can not be used yet.', self::NAME, $value);
}
}
public function supportedClaim(): string
{
return self::NAME;
}
public function checkHeader(mixed $value): void
{
if (! is_float($value) && ! is_int($value)) {
throw new InvalidHeaderException('"nbf" must be an integer.', self::NAME, $value);
}
$now = $this->clock->now()
->getTimestamp();
if ($now < $value - $this->allowedTimeDrift) {
throw new InvalidHeaderException('The JWT can not be used yet.', self::NAME, $value);
}
}
public function supportedHeader(): string
{
return self::NAME;
}
public function protectedHeaderOnly(): bool
{
return $this->protectedHeaderOnly;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use Jose\Component\Core\JWT;
interface TokenTypeSupport
{
/**
* This method will retrieve the protect and unprotected headers of the token for the given index. The index is
* useful when the token is serialized using the Json General Serialization mode. For example the JWE Json General
* Serialization Mode allows several recipients to be set. The unprotected headers correspond to the share
* unprotected header and the selected recipient header.
*
* @param array<string, mixed> $protectedHeader
* @param array<string, mixed> $unprotectedHeader
*/
public function retrieveTokenHeaders(
JWT $jwt,
int $index,
array &$protectedHeader,
array &$unprotectedHeader
): void;
/**
* This method returns true if the token in argument is supported, otherwise false.
*/
public function supports(JWT $jwt): bool;
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Checker;
use function is_bool;
/**
* This class is a header parameter checker. When the "b64" is present, it will check if the value is a boolean or not.
*
* The use of this checker will allow the use of token with unencoded payload.
*/
final class UnencodedPayloadChecker implements HeaderChecker
{
private const HEADER_NAME = 'b64';
public function checkHeader(mixed $value): void
{
if (! is_bool($value)) {
throw new InvalidHeaderException('"b64" must be a boolean.', self::HEADER_NAME, $value);
}
}
public function supportedHeader(): string
{
return self::HEADER_NAME;
}
public function protectedHeaderOnly(): bool
{
return true;
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\JsonConverter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
use function is_string;
#[AsCommand(name: 'keyset:add:key', description: 'Add a key into a key set.',)]
final class AddKeyIntoKeysetCommand extends ObjectOutputCommand
{
protected static $defaultName = 'keyset:add:key';
protected static $defaultDescription = 'Add a key into a key set.';
protected function configure(): void
{
parent::configure();
$this->setHelp('This command adds a key at the end of a key set.')
->addArgument('jwkset', InputArgument::REQUIRED, 'The JWKSet object')
->addArgument('jwk', InputArgument::REQUIRED, 'The new JWK object');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$jwkset = $this->getKeyset($input);
$jwk = $this->getKey($input);
$jwkset = $jwkset->with($jwk);
$this->prepareJsonOutput($input, $output, $jwkset);
return self::SUCCESS;
}
private function getKeyset(InputInterface $input): JWKSet
{
$jwkset = $input->getArgument('jwkset');
if (! is_string($jwkset)) {
throw new InvalidArgumentException('The argument must be a valid JWKSet.');
}
$json = JsonConverter::decode($jwkset);
if (! is_array($json)) {
throw new InvalidArgumentException('The argument must be a valid JWKSet.');
}
return JWKSet::createFromKeyData($json);
}
private function getKey(InputInterface $input): JWK
{
$jwk = $input->getArgument('jwk');
if (! is_string($jwk)) {
throw new InvalidArgumentException('The argument must be a valid JWK.');
}
$json = JsonConverter::decode($jwk);
if (! is_array($json)) {
throw new InvalidArgumentException('The argument must be a valid JWK.');
}
return new JWK($json);
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
#[AsCommand(name: 'key:generate:ec', description: 'Generate an EC key (JWK format)',)]
final class EcKeyGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'key:generate:ec';
protected static $defaultDescription = 'Generate an EC key (JWK format)';
protected function configure(): void
{
parent::configure();
$this->addArgument('curve', InputArgument::REQUIRED, 'Curve of the key.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$curve = $input->getArgument('curve');
if (! is_string($curve)) {
throw new InvalidArgumentException('Invalid curve');
}
$args = $this->getOptions($input);
$jwk = JWKFactory::createECKey($curve, $args);
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWKSet;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
#[AsCommand(name: 'keyset:generate:ec', description: 'Generate an EC key set (JWKSet format)',)]
final class EcKeysetGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'keyset:generate:ec';
protected static $defaultDescription = 'Generate an EC key set (JWKSet format)';
protected function configure(): void
{
parent::configure();
$this->addArgument('quantity', InputArgument::REQUIRED, 'Quantity of keys in the key set.')
->addArgument('curve', InputArgument::REQUIRED, 'Curve of the keys.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$quantity = (int) $input->getArgument('quantity');
if ($quantity < 1) {
throw new InvalidArgumentException('Invalid quantity');
}
$curve = $input->getArgument('curve');
if (! is_string($curve)) {
throw new InvalidArgumentException('Invalid curve');
}
$keyset = new JWKSet([]);
for ($i = 0; $i < $quantity; ++$i) {
$args = $this->getOptions($input);
$keyset = $keyset->with(JWKFactory::createECKey($curve, $args));
}
$this->prepareJsonOutput($input, $output, $keyset);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use function is_bool;
abstract class GeneratorCommand extends ObjectOutputCommand
{
public function isEnabled(): bool
{
return class_exists(JWKFactory::class);
}
protected function configure(): void
{
parent::configure();
$this
->addOption('use', 'u', InputOption::VALUE_OPTIONAL, 'Usage of the key. Must be either "sig" or "enc".')
->addOption('alg', 'a', InputOption::VALUE_OPTIONAL, 'Algorithm for the key.')
->addOption(
'random_id',
null,
InputOption::VALUE_NONE,
'If this option is set, a random key ID (kid) will be generated.'
);
}
protected function getOptions(InputInterface $input): array
{
$args = [];
$useRandomId = $input->getOption('random_id');
if (! is_bool($useRandomId)) {
throw new InvalidArgumentException('Invalid value for option "random_id"');
}
if ($useRandomId) {
$args['kid'] = $this->generateKeyID();
}
foreach (['use', 'alg'] as $key) {
$value = $input->getOption($key);
if ($value !== null) {
$args[$key] = $value;
}
}
return $args;
}
private function generateKeyID(): string
{
return Base64UrlSafe::encode(random_bytes(32));
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\JsonConverter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
use function is_string;
#[AsCommand(name: 'key:thumbprint', description: 'Get the thumbprint of a JWK key.',)]
final class GetThumbprintCommand extends ObjectOutputCommand
{
protected static $defaultName = 'key:thumbprint';
protected static $defaultDescription = 'Get the thumbprint of a JWK key.';
protected function configure(): void
{
parent::configure();
$this->addArgument('jwk', InputArgument::REQUIRED, 'The JWK key.')
->addOption('hash', null, InputOption::VALUE_OPTIONAL, 'The hashing algorithm.', 'sha256');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$jwk = $input->getArgument('jwk');
if (! is_string($jwk)) {
throw new InvalidArgumentException('Invalid JWK');
}
$hash = $input->getOption('hash');
if (! is_string($hash)) {
throw new InvalidArgumentException('Invalid hash algorithm');
}
$json = JsonConverter::decode($jwk);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid input.');
}
$key = new JWK($json);
$output->write($key->thumbprint($hash));
return self::SUCCESS;
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\JKUFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
#[AsCommand(name: 'keyset:load:jku', description: 'Loads a key set from an url.',)]
final class JKULoaderCommand extends ObjectOutputCommand
{
protected static $defaultName = 'keyset:load:jku';
protected static $defaultDescription = 'Loads a key set from an url.';
public function __construct(
private readonly JKUFactory $jkuFactory,
?string $name = null
) {
parent::__construct($name);
}
protected function configure(): void
{
parent::configure();
$this->setHelp('This command will try to get a key set from an URL. The distant key set is a JWKSet.')
->addArgument('url', InputArgument::REQUIRED, 'The URL');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$url = $input->getArgument('url');
if (! is_string($url)) {
throw new InvalidArgumentException('Invalid URL');
}
$result = $this->jkuFactory->loadFromUrl($url);
$this->prepareJsonOutput($input, $output, $result);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\KeyManagement\Analyzer\KeyAnalyzerManager;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
use function is_string;
#[AsCommand(name: 'key:analyze', description: 'JWK quality analyzer.',)]
final class KeyAnalyzerCommand extends Command
{
protected static $defaultName = 'key:analyze';
protected static $defaultDescription = 'JWK quality analyzer.';
public function __construct(
private readonly KeyAnalyzerManager $analyzerManager,
?string $name = null
) {
parent::__construct($name);
}
protected function configure(): void
{
parent::configure();
$this->setHelp('This command will analyze a JWK object and find security issues.')
->addArgument('jwk', InputArgument::REQUIRED, 'The JWK object');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->getFormatter()
->setStyle('success', new OutputFormatterStyle('white', 'green'));
$output->getFormatter()
->setStyle('high', new OutputFormatterStyle('white', 'red', ['bold']));
$output->getFormatter()
->setStyle('medium', new OutputFormatterStyle('yellow'));
$output->getFormatter()
->setStyle('low', new OutputFormatterStyle('blue'));
$jwk = $this->getKey($input);
$result = $this->analyzerManager->analyze($jwk);
if ($result->count() === 0) {
$output->writeln('<success>All good! No issue found.</success>');
} else {
foreach ($result->all() as $message) {
$output->writeln(
'<' . $message->getSeverity() . '>* ' . $message->getMessage() . '</' . $message->getSeverity() . '>'
);
}
}
return self::SUCCESS;
}
private function getKey(InputInterface $input): JWK
{
$jwk = $input->getArgument('jwk');
if (! is_string($jwk)) {
throw new InvalidArgumentException('Invalid JWK');
}
$json = JsonConverter::decode($jwk);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid JWK.');
}
return new JWK($json);
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
#[AsCommand(name: 'key:load:key', description: 'Loads a key from a key file (JWK format)',)]
final class KeyFileLoaderCommand extends GeneratorCommand
{
protected static $defaultName = 'key:load:key';
protected static $defaultDescription = 'Loads a key from a key file (JWK format)';
protected function configure(): void
{
parent::configure();
$this->addArgument('file', InputArgument::REQUIRED, 'Filename of the key.')
->addOption('secret', 's', InputOption::VALUE_OPTIONAL, 'Secret if the key is encrypted.', null);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$file = $input->getArgument('file');
$password = $input->getOption('secret');
if (! is_string($file)) {
throw new InvalidArgumentException('Invalid file');
}
if ($password !== null && ! is_string($password)) {
throw new InvalidArgumentException('Invalid secret');
}
$args = $this->getOptions($input);
$jwk = JWKFactory::createFromKeyFile($file, $password, $args);
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\KeyManagement\Analyzer\KeyAnalyzerManager;
use Jose\Component\KeyManagement\Analyzer\KeysetAnalyzerManager;
use Jose\Component\KeyManagement\Analyzer\MessageBag;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
use function is_string;
#[AsCommand(name: 'keyset:analyze', description: 'JWKSet quality analyzer.',)]
final class KeysetAnalyzerCommand extends Command
{
protected static $defaultName = 'keyset:analyze';
protected static $defaultDescription = 'JWKSet quality analyzer.';
public function __construct(
private readonly KeysetAnalyzerManager $keysetAnalyzerManager,
private readonly KeyAnalyzerManager $keyAnalyzerManager,
?string $name = null
) {
parent::__construct($name);
}
protected function configure(): void
{
parent::configure();
$this->setHelp('This command will analyze a JWKSet object and find security issues.')
->addArgument('jwkset', InputArgument::REQUIRED, 'The JWKSet object');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->getFormatter()
->setStyle('success', new OutputFormatterStyle('white', 'green'));
$output->getFormatter()
->setStyle('high', new OutputFormatterStyle('white', 'red', ['bold']));
$output->getFormatter()
->setStyle('medium', new OutputFormatterStyle('yellow'));
$output->getFormatter()
->setStyle('low', new OutputFormatterStyle('blue'));
$jwkset = $this->getKeyset($input);
$messages = $this->keysetAnalyzerManager->analyze($jwkset);
$this->showMessages($messages, $output);
foreach ($jwkset as $kid => $jwk) {
$output->writeln(sprintf('Analysing key with index/kid "%s"', $kid));
$messages = $this->keyAnalyzerManager->analyze($jwk);
$this->showMessages($messages, $output);
}
return self::SUCCESS;
}
private function showMessages(MessageBag $messages, OutputInterface $output): void
{
if ($messages->count() === 0) {
$output->writeln(' <success>All good! No issue found.</success>');
} else {
foreach ($messages->all() as $message) {
$output->writeln(
' <' . $message->getSeverity() . '>* ' . $message->getMessage() . '</' . $message->getSeverity() . '>'
);
}
}
}
private function getKeyset(InputInterface $input): JWKSet
{
$jwkset = $input->getArgument('jwkset');
if (! is_string($jwkset)) {
throw new InvalidArgumentException('Invalid JWKSet');
}
$json = JsonConverter::decode($jwkset);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid JWKSet');
}
return JWKSet::createFromKeyData($json);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\JsonConverter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
#[AsCommand(name: 'keyset:merge', description: 'Merge several key sets into one.',)]
final class MergeKeysetCommand extends ObjectOutputCommand
{
protected static $defaultName = 'keyset:merge';
protected static $defaultDescription = 'Merge several key sets into one.';
protected function configure(): void
{
parent::configure();
$this->setHelp(
'This command merges several key sets into one. It is very useful when you generate e.g. RSA, EC and OKP keys and you want only one key set to rule them all.'
)
->addArgument('jwksets', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'The JWKSet objects');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string[] $keySets */
$keySets = $input->getArgument('jwksets');
$newJwkset = new JWKSet([]);
foreach ($keySets as $keySet) {
$json = JsonConverter::decode($keySet);
if (! is_array($json)) {
throw new InvalidArgumentException('The argument must be a valid JWKSet.');
}
$jwkset = JWKSet::createFromKeyData($json);
foreach ($jwkset->all() as $jwk) {
$newJwkset = $newJwkset->with($jwk);
}
}
$this->prepareJsonOutput($input, $output, $newJwkset);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(
name: 'key:generate:none',
description: 'Generate a none key (JWK format). This key type is only supposed to be used with the "none" algorithm.',
)]
final class NoneKeyGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'key:generate:none';
protected static $defaultDescription = 'Generate a none key (JWK format). This key type is only supposed to be used with the "none" algorithm.';
protected function configure(): void
{
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$args = $this->getOptions($input);
$jwk = JWKFactory::createNoneKey($args);
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use Jose\Component\Core\Util\JsonConverter;
use JsonSerializable;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
abstract class ObjectOutputCommand extends Command
{
protected function prepareJsonOutput(InputInterface $input, OutputInterface $output, JsonSerializable $json): void
{
$data = JsonConverter::encode($json);
$output->write($data);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'key:generate:oct', description: 'Generate an octet key (JWK format)',)]
final class OctKeyGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'key:generate:oct';
protected static $defaultDescription = 'Generate an octet key (JWK format)';
protected function configure(): void
{
parent::configure();
$this->addArgument('size', InputArgument::REQUIRED, 'Key size.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$size = (int) $input->getArgument('size');
if ($size < 1) {
throw new InvalidArgumentException('Invalid size');
}
$args = $this->getOptions($input);
$jwk = JWKFactory::createOctKey($size, $args);
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWKSet;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'keyset:generate:oct', description: 'Generate a key set with octet keys (JWK format)',)]
final class OctKeysetGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'keyset:generate:oct';
protected static $defaultDescription = 'Generate a key set with octet keys (JWK format)';
protected function configure(): void
{
parent::configure();
$this->addArgument('quantity', InputArgument::REQUIRED, 'Quantity of keys in the key set.')
->addArgument('size', InputArgument::REQUIRED, 'Key size.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$quantity = (int) $input->getArgument('quantity');
$size = (int) $input->getArgument('size');
if ($quantity < 1) {
throw new InvalidArgumentException('Invalid quantity');
}
if ($size < 1) {
throw new InvalidArgumentException('Invalid size');
}
$keyset = new JWKSet([]);
for ($i = 0; $i < $quantity; ++$i) {
$args = $this->getOptions($input);
$keyset = $keyset->with(JWKFactory::createOctKey($size, $args));
}
$this->prepareJsonOutput($input, $output, $keyset);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
#[AsCommand(name: 'key:generate:okp', description: 'Generate an Octet Key Pair key (JWK format)',)]
final class OkpKeyGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'key:generate:okp';
protected static $defaultDescription = 'Generate an Octet Key Pair key (JWK format)';
protected function configure(): void
{
parent::configure();
$this->addArgument('curve', InputArgument::REQUIRED, 'Curve of the key.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$curve = $input->getArgument('curve');
if (! is_string($curve)) {
throw new InvalidArgumentException('Invalid curve');
}
$args = $this->getOptions($input);
$jwk = JWKFactory::createOKPKey($curve, $args);
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWKSet;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
#[AsCommand(
name: 'keyset:generate:okp',
description: 'Generate a key set with Octet Key Pairs keys (JWKSet format)',
)]
final class OkpKeysetGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'keyset:generate:okp';
protected static $defaultDescription = 'Generate a key set with Octet Key Pairs keys (JWKSet format)';
protected function configure(): void
{
parent::configure();
$this->addArgument('quantity', InputArgument::REQUIRED, 'Quantity of keys in the key set.')
->addArgument('curve', InputArgument::REQUIRED, 'Curve of the keys.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$quantity = (int) $input->getArgument('quantity');
$curve = $input->getArgument('curve');
if ($quantity < 1) {
throw new InvalidArgumentException('Invalid quantity');
}
if (! is_string($curve)) {
throw new InvalidArgumentException('Invalid curve');
}
$keyset = new JWKSet([]);
for ($i = 0; $i < $quantity; ++$i) {
$args = $this->getOptions($input);
$keyset = $keyset->with(JWKFactory::createOKPKey($curve, $args));
}
$this->prepareJsonOutput($input, $output, $keyset);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\KeyManagement\KeyConverter\RSAKey;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
use function is_string;
#[AsCommand(name: 'key:optimize', description: 'Optimize a RSA key by calculating additional primes (CRT).',)]
final class OptimizeRsaKeyCommand extends ObjectOutputCommand
{
protected static $defaultName = 'key:optimize';
protected static $defaultDescription = 'Optimize a RSA key by calculating additional primes (CRT).';
protected function configure(): void
{
parent::configure();
$this->addArgument('jwk', InputArgument::REQUIRED, 'The RSA key.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$jwk = $input->getArgument('jwk');
if (! is_string($jwk)) {
throw new InvalidArgumentException('Invalid JWK');
}
$json = JsonConverter::decode($jwk);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid JWK');
}
$key = RSAKey::createFromJWK(new JWK($json));
$key->optimize();
$this->prepareJsonOutput($input, $output, $key->toJwk());
return self::SUCCESS;
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
#[AsCommand(name: 'key:load:p12', description: 'Load a key from a P12 certificate file.',)]
final class P12CertificateLoaderCommand extends GeneratorCommand
{
protected static $defaultName = 'key:load:p12';
protected static $defaultDescription = 'Load a key from a P12 certificate file.';
protected function configure(): void
{
parent::configure();
$this->addArgument('file', InputArgument::REQUIRED, 'Filename of the P12 certificate.')
->addOption('secret', 's', InputOption::VALUE_OPTIONAL, 'Secret if the key is encrypted.', null);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$file = $input->getArgument('file');
$password = $input->getOption('secret');
if (! is_string($file)) {
throw new InvalidArgumentException('Invalid file');
}
if (! is_string($password)) {
throw new InvalidArgumentException('Invalid secret');
}
$args = $this->getOptions($input);
$jwk = JWKFactory::createFromPKCS12CertificateFile($file, $password, $args);
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\ECKey;
use Jose\Component\Core\Util\JsonConverter;
use Jose\Component\Core\Util\RSAKey;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
use function is_string;
#[AsCommand(name: 'key:convert:pkcs1', description: 'Converts a RSA or EC key into PKCS#1 key.',)]
final class PemConverterCommand extends ObjectOutputCommand
{
protected static $defaultName = 'key:convert:pkcs1';
protected static $defaultDescription = 'Converts a RSA or EC key into PKCS#1 key.';
protected function configure(): void
{
parent::configure();
$this->addArgument('jwk', InputArgument::REQUIRED, 'The key');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$jwk = $input->getArgument('jwk');
if (! is_string($jwk)) {
throw new InvalidArgumentException('Invalid JWK');
}
$json = JsonConverter::decode($jwk);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid JWK.');
}
$key = new JWK($json);
$pem = match ($key->get('kty')) {
'RSA' => RSAKey::createFromJWK($key)->toPEM(),
'EC' => ECKey::convertToPEM($key),
default => throw new InvalidArgumentException('Not a RSA or EC key.'),
};
$output->write($pem);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\JsonConverter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
use function is_string;
#[AsCommand(
name: 'key:convert:public',
description: 'Convert a private key into public key. Symmetric keys (shared keys) are not changed.',
)]
final class PublicKeyCommand extends ObjectOutputCommand
{
protected static $defaultName = 'key:convert:public';
protected static $defaultDescription = 'Convert a private key into public key. Symmetric keys (shared keys) are not changed.';
protected function configure(): void
{
parent::configure();
$this->setHelp('This command converts a private key into a public key.')
->addArgument('jwk', InputArgument::REQUIRED, 'The JWK object');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$jwk = $this->getKey($input);
$jwk = $jwk->toPublic();
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
private function getKey(InputInterface $input): JWK
{
$jwk = $input->getArgument('jwk');
if (! is_string($jwk)) {
throw new InvalidArgumentException('Invalid JWK');
}
$json = JsonConverter::decode($jwk);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid JWK');
}
return new JWK($json);
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\JsonConverter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_array;
use function is_string;
#[AsCommand(
name: 'keyset:convert:public',
description: 'Convert private keys in a key set into public keys. Symmetric keys (shared keys) are not changed.',
)]
final class PublicKeysetCommand extends ObjectOutputCommand
{
protected static $defaultName = 'keyset:convert:public';
protected static $defaultDescription = 'Convert private keys in a key set into public keys. Symmetric keys (shared keys) are not changed.';
protected function configure(): void
{
parent::configure();
$this->setHelp('This command converts private keys in a key set into public keys.')
->addArgument('jwkset', InputArgument::REQUIRED, 'The JWKSet object');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$jwkset = $this->getKeyset($input);
$newJwkset = new JWKSet([]);
foreach ($jwkset->all() as $jwk) {
$newJwkset = $newJwkset->with($jwk->toPublic());
}
$this->prepareJsonOutput($input, $output, $newJwkset);
return self::SUCCESS;
}
private function getKeyset(InputInterface $input): JWKSet
{
$jwkset = $input->getArgument('jwkset');
if (! is_string($jwkset)) {
throw new InvalidArgumentException('Invalid JWKSet');
}
$json = JsonConverter::decode($jwkset);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid JWKSet');
}
return JWKSet::createFromKeyData($json);
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Core\Util\JsonConverter;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function count;
use function is_array;
use function is_string;
#[AsCommand(name: 'keyset:rotate', description: 'Rotate a key set.',)]
final class RotateKeysetCommand extends ObjectOutputCommand
{
protected static $defaultName = 'keyset:rotate';
protected static $defaultDescription = 'Rotate a key set.';
protected function configure(): void
{
parent::configure();
$this->setHelp('This command removes the last key in a key set a place a new one at the beginning.')
->addArgument('jwkset', InputArgument::REQUIRED, 'The JWKSet object')
->addArgument('jwk', InputArgument::REQUIRED, 'The new JWK object');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$jwkset = $this->getKeyset($input)
->all();
$jwk = $this->getKey($input);
if (count($jwkset) !== 0) {
array_pop($jwkset);
}
array_unshift($jwkset, $jwk);
$this->prepareJsonOutput($input, $output, new JWKSet($jwkset));
return self::SUCCESS;
}
private function getKeyset(InputInterface $input): JWKSet
{
$jwkset = $input->getArgument('jwkset');
if (! is_string($jwkset)) {
throw new InvalidArgumentException('Invalid JWKSet');
}
$json = JsonConverter::decode($jwkset);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid JWKSet');
}
return JWKSet::createFromKeyData($json);
}
private function getKey(InputInterface $input): JWK
{
$jwk = $input->getArgument('jwk');
if (! is_string($jwk)) {
throw new InvalidArgumentException('Invalid JWK');
}
$json = JsonConverter::decode($jwk);
if (! is_array($json)) {
throw new InvalidArgumentException('Invalid JWK');
}
return new JWK($json);
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'key:generate:rsa', description: 'Generate a RSA key (JWK format)',)]
final class RsaKeyGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'key:generate:rsa';
protected static $defaultDescription = 'Generate a RSA key (JWK format)';
protected function configure(): void
{
parent::configure();
$this->addArgument('size', InputArgument::REQUIRED, 'Key size.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$size = (int) $input->getArgument('size');
$args = $this->getOptions($input);
if ($size < 1) {
throw new InvalidArgumentException('Invalid size');
}
$jwk = JWKFactory::createRSAKey($size, $args);
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\Core\JWKSet;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'keyset:generate:rsa', description: 'Generate a key set with RSA keys (JWK format)',)]
final class RsaKeysetGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'keyset:generate:rsa';
protected static $defaultDescription = 'Generate a key set with RSA keys (JWK format)';
protected function configure(): void
{
parent::configure();
$this->addArgument('quantity', InputArgument::REQUIRED, 'Quantity of keys in the key set.')
->addArgument('size', InputArgument::REQUIRED, 'Key size.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$quantity = (int) $input->getArgument('quantity');
$size = (int) $input->getArgument('size');
if ($quantity < 1) {
throw new InvalidArgumentException('Invalid quantity');
}
if ($size < 1) {
throw new InvalidArgumentException('Invalid size');
}
$keyset = new JWKSet([]);
for ($i = 0; $i < $quantity; ++$i) {
$args = $this->getOptions($input);
$keyset = $keyset->with(JWKFactory::createRSAKey($size, $args));
}
$this->prepareJsonOutput($input, $output, $keyset);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function is_bool;
use function is_string;
#[AsCommand(
name: 'key:generate:from_secret',
description: 'Generate an octet key (JWK format) using an existing secret',
)]
final class SecretKeyGeneratorCommand extends GeneratorCommand
{
protected static $defaultName = 'key:generate:from_secret';
protected static $defaultDescription = 'Generate an octet key (JWK format) using an existing secret';
protected function configure(): void
{
parent::configure();
$this->addArgument('secret', InputArgument::REQUIRED, 'The secret')
->addOption(
'is_b64',
'b',
InputOption::VALUE_NONE,
'Indicates if the secret is Base64 encoded (useful for binary secrets)'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$secret = $input->getArgument('secret');
if (! is_string($secret)) {
throw new InvalidArgumentException('Invalid secret');
}
$isBsae64Encoded = $input->getOption('is_b64');
if (! is_bool($isBsae64Encoded)) {
throw new InvalidArgumentException('Invalid option value for "is_b64"');
}
if ($isBsae64Encoded) {
$secret = base64_decode($secret, true);
}
if (! is_string($secret)) {
throw new InvalidArgumentException('Invalid secret');
}
$args = $this->getOptions($input);
$jwk = JWKFactory::createFromSecret($secret, $args);
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\JWKFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
#[AsCommand(name: 'key:load:x509', description: 'Load a key from a X.509 certificate file.',)]
final class X509CertificateLoaderCommand extends GeneratorCommand
{
protected static $defaultName = 'key:load:x509';
protected static $defaultDescription = 'Load a key from a X.509 certificate file.';
protected function configure(): void
{
parent::configure();
$this->addArgument('file', InputArgument::REQUIRED, 'Filename of the X.509 certificate.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$file = $input->getArgument('file');
if (! is_string($file)) {
throw new InvalidArgumentException('Invalid file');
}
$args = [];
foreach (['use', 'alg'] as $key) {
$value = $input->getOption($key);
if ($value !== null) {
$args[$key] = $value;
}
}
$jwk = JWKFactory::createFromCertificateFile($file, $args);
$this->prepareJsonOutput($input, $output, $jwk);
return self::SUCCESS;
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Console;
use InvalidArgumentException;
use Jose\Component\KeyManagement\X5UFactory;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function is_string;
#[AsCommand(name: 'keyset:load:x5u', description: 'Loads a key set from an url.',)]
final class X5ULoaderCommand extends ObjectOutputCommand
{
protected static $defaultName = 'keyset:load:x5u';
protected static $defaultDescription = 'Loads a key set from an url.';
public function __construct(
private readonly X5UFactory $x5uFactory,
?string $name = null
) {
parent::__construct($name);
}
protected function configure(): void
{
parent::configure();
$this->setHelp(
'This command will try to get a key set from an URL. The distant key set is list of X.509 certificates.'
)
->addArgument('url', InputArgument::REQUIRED, 'The URL');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$url = $input->getArgument('url');
if (! is_string($url)) {
throw new InvalidArgumentException('Invalid URL');
}
$result = $this->x5uFactory->loadFromUrl($url);
$this->prepareJsonOutput($input, $output, $result);
return self::SUCCESS;
}
}

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,77 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core;
use InvalidArgumentException;
use function array_key_exists;
class AlgorithmManager
{
/**
* @var array<string, Algorithm>
*/
private array $algorithms = [];
/**
* @param Algorithm[] $algorithms
*/
public function __construct(iterable $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);
}
/**
* @return array<string, Algorithm>
*/
public function all(): array
{
return $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,82 @@
<?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 = [];
/**
* @param Algorithm[] $algorithms
*/
public function __construct(iterable $algorithms = [])
{
foreach ($algorithms as $algorithm) {
$this->add($algorithm->name(), $algorithm);
}
}
/**
* 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 InvalidArgumentException;
use Jose\Component\Core\Util\Base64UrlSafe;
use JsonSerializable;
use function array_key_exists;
use function in_array;
use function is_array;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
/**
* @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 ArrayIterator;
use Countable;
use InvalidArgumentException;
use IteratorAggregate;
use JsonSerializable;
use Traversable;
use function array_key_exists;
use function count;
use function in_array;
use function is_array;
use const COUNT_NORMAL;
use const JSON_THROW_ON_ERROR;
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,220 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
/**
* Copyright (c) 2016 - 2022 Paragon Initiative Enterprises.
* Copyright (c) 2014 Steve "Sc00bz" Thomas (steve at tobtu dot com)
*
* 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.
*/
use RangeException;
/**
* @readonly
*/
final class Base64UrlSafe
{
public static function encode(string $binString): string
{
return static::doEncode($binString, true);
}
public static function encodeUnpadded(string $src): string
{
return static::doEncode($src, false);
}
public static function decode(string $encodedString, bool $strictPadding = false): string
{
$srcLen = self::safeStrlen($encodedString);
if ($srcLen === 0) {
return '';
}
if ($strictPadding) {
if (($srcLen & 3) === 0) {
if ($encodedString[$srcLen - 1] === '=') {
$srcLen--;
if ($encodedString[$srcLen - 1] === '=') {
$srcLen--;
}
}
}
if (($srcLen & 3) === 1) {
throw new RangeException('Incorrect padding');
}
if ($encodedString[$srcLen - 1] === '=') {
throw new RangeException('Incorrect padding');
}
} else {
$encodedString = rtrim($encodedString, '=');
$srcLen = self::safeStrlen($encodedString);
}
$err = 0;
$dest = '';
for ($i = 0; $i + 4 <= $srcLen; $i += 4) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', self::safeSubstr($encodedString, $i, 4));
$c0 = static::decode6Bits($chunk[1]);
$c1 = static::decode6Bits($chunk[2]);
$c2 = static::decode6Bits($chunk[3]);
$c3 = static::decode6Bits($chunk[4]);
$dest .= pack(
'CCC',
((($c0 << 2) | ($c1 >> 4)) & 0xff),
((($c1 << 4) | ($c2 >> 2)) & 0xff),
((($c2 << 6) | $c3) & 0xff)
);
$err |= ($c0 | $c1 | $c2 | $c3) >> 8;
}
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', self::safeSubstr($encodedString, $i, $srcLen - $i));
$c0 = static::decode6Bits($chunk[1]);
if ($i + 2 < $srcLen) {
$c1 = static::decode6Bits($chunk[2]);
$c2 = static::decode6Bits($chunk[3]);
$dest .= pack('CC', ((($c0 << 2) | ($c1 >> 4)) & 0xff), ((($c1 << 4) | ($c2 >> 2)) & 0xff));
$err |= ($c0 | $c1 | $c2) >> 8;
if ($strictPadding) {
$err |= ($c2 << 6) & 0xff;
}
} elseif ($i + 1 < $srcLen) {
$c1 = static::decode6Bits($chunk[2]);
$dest .= pack('C', ((($c0 << 2) | ($c1 >> 4)) & 0xff));
$err |= ($c0 | $c1) >> 8;
if ($strictPadding) {
$err |= ($c1 << 4) & 0xff;
}
} elseif ($strictPadding) {
$err |= 1;
}
}
$check = ($err === 0);
if (! $check) {
throw new RangeException('Base64::decode() only expects characters in the correct base64 alphabet');
}
return $dest;
}
public static function decodeNoPadding(string $encodedString): string
{
$srcLen = self::safeStrlen($encodedString);
if ($srcLen === 0) {
return '';
}
if (($srcLen & 3) === 0) {
if ($encodedString[$srcLen - 1] === '=') {
throw new InvalidArgumentException("decodeNoPadding() doesn't tolerate padding");
}
if (($srcLen & 3) > 1) {
if ($encodedString[$srcLen - 2] === '=') {
throw new InvalidArgumentException("decodeNoPadding() doesn't tolerate padding");
}
}
}
return static::decode($encodedString, true);
}
private static function doEncode(string $src, bool $pad = true): string
{
$dest = '';
$srcLen = self::safeStrlen($src);
for ($i = 0; $i + 3 <= $srcLen; $i += 3) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', self::safeSubstr($src, $i, 3));
$b0 = $chunk[1];
$b1 = $chunk[2];
$b2 = $chunk[3];
$dest .=
static::encode6Bits($b0 >> 2) .
static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) .
static::encode6Bits($b2 & 63);
}
if ($i < $srcLen) {
/** @var array<int, int> $chunk */
$chunk = unpack('C*', self::safeSubstr($src, $i, $srcLen - $i));
$b0 = $chunk[1];
if ($i + 1 < $srcLen) {
$b1 = $chunk[2];
$dest .=
static::encode6Bits($b0 >> 2) .
static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) .
static::encode6Bits(($b1 << 2) & 63);
if ($pad) {
$dest .= '=';
}
} else {
$dest .=
static::encode6Bits($b0 >> 2) .
static::encode6Bits(($b0 << 4) & 63);
if ($pad) {
$dest .= '==';
}
}
}
return $dest;
}
private static function decode6Bits(int $src): int
{
$ret = -1;
$ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64);
$ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70);
$ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5);
$ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63;
return $ret + ((((0x5e - $src) & ($src - 0x60)) >> 8) & 64);
}
private static function encode6Bits(int $src): string
{
$diff = 0x41;
$diff += ((25 - $src) >> 8) & 6;
$diff -= ((51 - $src) >> 8) & 75;
$diff -= ((61 - $src) >> 8) & 13;
$diff += ((62 - $src) >> 8) & 49;
return pack('C', $src + $diff);
}
private static function safeStrlen(string $str): int
{
return mb_strlen($str, '8bit');
}
private static function safeSubstr(string $str, int $start = 0, $length = null): string
{
if ($length === 0) {
return '';
}
return mb_substr($str, $start, $length, '8bit');
}
}

View File

@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use Brick\Math\BigInteger as BrickBigInteger;
use InvalidArgumentException;
use function chr;
/**
* @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,326 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use RuntimeException;
use function extension_loaded;
use function is_array;
use function is_string;
use const OPENSSL_KEYTYPE_EC;
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-----' . "\n";
$pem .= chunk_split(base64_encode($der), 64, "\n");
return $pem . ('-----END PUBLIC KEY-----' . "\n");
}
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-----' . "\n";
$pem .= chunk_split(base64_encode($der), 64, "\n");
return $pem . ('-----END EC PRIVATE KEY-----' . "\n");
}
/**
* 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::decodeNoPadding($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::decodeNoPadding($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::decodeNoPadding($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::decodeNoPadding($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::decodeNoPadding($x), "\0");
$binY = ltrim(Base64UrlSafe::decodeNoPadding($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,133 @@
<?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,272 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util\Ecc;
use Brick\Math\BigInteger;
use RuntimeException;
use Stringable;
use const STR_PAD_LEFT;
/**
* @internal
*/
final class Curve implements Stringable
{
public function __construct(
private readonly int $size,
private readonly BigInteger $prime,
private readonly BigInteger $a,
private readonly BigInteger $b,
private readonly Point $generator
) {
}
public function __toString(): string
{
return 'curve(' . Math::toString($this->getA()) . ', ' . Math::toString($this->getB()) . ', ' . Math::toString(
$this->getPrime()
) . ')';
}
public function getA(): BigInteger
{
return $this->a;
}
public function getB(): BigInteger
{
return $this->b;
}
public function getPrime(): BigInteger
{
return $this->prime;
}
public function getSize(): int
{
return $this->size;
}
public function getPoint(BigInteger $x, BigInteger $y, ?BigInteger $order = null): Point
{
if (! $this->contains($x, $y)) {
throw new RuntimeException('Curve ' . $this->__toString() . ' does not contain point (' . Math::toString(
$x
) . ', ' . Math::toString($y) . ')');
}
$point = Point::create($x, $y, $order);
if ($order !== null) {
$mul = $this->mul($point, $order);
if (! $mul->isInfinity()) {
throw new RuntimeException('SELF * ORDER MUST EQUAL INFINITY.');
}
}
return $point;
}
public function getPublicKeyFrom(BigInteger $x, BigInteger $y): PublicKey
{
$zero = BigInteger::zero();
if ($x->compareTo($zero) < 0 || $y->compareTo($zero) < 0 || $this->generator->getOrder()->compareTo(
$x
) <= 0 || $this->generator->getOrder()
->compareTo($y) <= 0) {
throw new RuntimeException('Generator point has x and y out of range.');
}
$point = $this->getPoint($x, $y);
return new PublicKey($point);
}
public function contains(BigInteger $x, BigInteger $y): bool
{
return Math::equals(
ModularArithmetic::sub(
$y->power(2),
Math::add(Math::add($x->power(3), $this->getA()->multipliedBy($x)), $this->getB()),
$this->getPrime()
),
BigInteger::zero()
);
}
public function add(Point $one, Point $two): Point
{
if ($two->isInfinity()) {
return clone $one;
}
if ($one->isInfinity()) {
return clone $two;
}
if ($two->getX()->isEqualTo($one->getX())) {
if ($two->getY()->isEqualTo($one->getY())) {
return $this->getDouble($one);
}
return Point::infinity();
}
$slope = ModularArithmetic::div(
$two->getY()
->minus($one->getY()),
$two->getX()
->minus($one->getX()),
$this->getPrime()
);
$xR = ModularArithmetic::sub($slope->power(2)->minus($one->getX()), $two->getX(), $this->getPrime());
$yR = ModularArithmetic::sub(
$slope->multipliedBy($one->getX()->minus($xR)),
$one->getY(),
$this->getPrime()
);
return $this->getPoint($xR, $yR, $one->getOrder());
}
public function mul(Point $one, BigInteger $n): Point
{
if ($one->isInfinity()) {
return Point::infinity();
}
/** @var BigInteger $zero */
$zero = BigInteger::zero();
if ($one->getOrder()->compareTo($zero) > 0) {
$n = $n->mod($one->getOrder());
}
if ($n->isEqualTo($zero)) {
return Point::infinity();
}
/** @var Point[] $r */
$r = [Point::infinity(), clone $one];
$k = $this->getSize();
$n1 = str_pad(Math::baseConvert(Math::toString($n), 10, 2), $k, '0', STR_PAD_LEFT);
for ($i = 0; $i < $k; ++$i) {
$j = $n1[$i];
Point::cswap($r[0], $r[1], $j ^ 1);
$r[0] = $this->add($r[0], $r[1]);
$r[1] = $this->getDouble($r[1]);
Point::cswap($r[0], $r[1], $j ^ 1);
}
$this->validate($r[0]);
return $r[0];
}
public function cmp(self $other): int
{
$equalsA = $this->getA()
->isEqualTo($other->getA());
$equalsB = $this->getB()
->isEqualTo($other->getB());
$equalsPrime = $this->getPrime()
->isEqualTo($other->getPrime());
$equal = $equalsA && $equalsB && $equalsPrime;
return $equal ? 0 : 1;
}
public function equals(self $other): bool
{
return $this->cmp($other) === 0;
}
public function getDouble(Point $point): Point
{
if ($point->isInfinity()) {
return Point::infinity();
}
$a = $this->getA();
$threeX2 = BigInteger::of(3)->multipliedBy($point->getX()->power(2));
$tangent = ModularArithmetic::div(
$threeX2->plus($a),
BigInteger::of(2)->multipliedBy($point->getY()),
$this->getPrime()
);
$x3 = ModularArithmetic::sub(
$tangent->power(2),
BigInteger::of(2)->multipliedBy($point->getX()),
$this->getPrime()
);
$y3 = ModularArithmetic::sub(
$tangent->multipliedBy($point->getX()->minus($x3)),
$point->getY(),
$this->getPrime()
);
return $this->getPoint($x3, $y3, $point->getOrder());
}
public function createPrivateKey(): PrivateKey
{
return PrivateKey::create($this->generate());
}
public function createPublicKey(PrivateKey $privateKey): PublicKey
{
$point = $this->mul($this->generator, $privateKey->getSecret());
return new PublicKey($point);
}
public function getGenerator(): Point
{
return $this->generator;
}
private function validate(Point $point): void
{
if (! $point->isInfinity() && ! $this->contains($point->getX(), $point->getY())) {
throw new RuntimeException('Invalid point');
}
}
private function generate(): BigInteger
{
$max = $this->generator->getOrder();
$numBits = $this->bnNumBits($max);
$numBytes = (int) ceil($numBits / 8);
// Generate an integer of size >= $numBits
$bytes = BigInteger::randomBits($numBytes);
$mask = BigInteger::of(2)->power($numBits)->minus(1);
return $bytes->and($mask);
}
/**
* Returns the number of bits used to store this number. Non-significant upper bits are not counted.
*
* @see https://www.openssl.org/docs/crypto/BN_num_bytes.html
*/
private function bnNumBits(BigInteger $x): int
{
$zero = BigInteger::of(0);
if ($x->isEqualTo($zero)) {
return 0;
}
$log2 = 0;
while (! $x->isEqualTo($zero)) {
$x = $x->shiftedRight(1);
++$log2;
}
return $log2;
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util\Ecc;
use Brick\Math\BigInteger;
/*
* *********************************************************************
* Copyright (C) 2012 Matyas Danter
*
* 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.
* ***********************************************************************
*/
/**
* @internal
*/
final class EcDH
{
public static function computeSharedKey(Curve $curve, PublicKey $publicKey, PrivateKey $privateKey): BigInteger
{
return $curve->mul($publicKey->getPoint(), $privateKey->getSecret())
->getX();
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util\Ecc;
use Brick\Math\BigInteger;
use Jose\Component\Core\Util\BigInteger as CoreBigInteger;
/**
* @internal
*/
final class Math
{
public static function equals(BigInteger $first, BigInteger $other): bool
{
return $first->isEqualTo($other);
}
public static function add(BigInteger $augend, BigInteger $addend): BigInteger
{
return $augend->plus($addend);
}
public static function toString(BigInteger $value): string
{
return $value->toBase(10);
}
public static function inverseMod(BigInteger $a, BigInteger $m): BigInteger
{
return CoreBigInteger::createFromBigInteger($a)->modInverse(CoreBigInteger::createFromBigInteger($m))->get();
}
public static function baseConvert(string $number, int $from, int $to): string
{
return BigInteger::fromBase($number, $from)->toBase($to);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util\Ecc;
use Brick\Math\BigInteger;
/**
* @internal
*/
final class ModularArithmetic
{
public static function sub(BigInteger $minuend, BigInteger $subtrahend, BigInteger $modulus): BigInteger
{
return $minuend->minus($subtrahend)
->mod($modulus);
}
public static function mul(BigInteger $multiplier, BigInteger $muliplicand, BigInteger $modulus): BigInteger
{
return $multiplier->multipliedBy($muliplicand)
->mod($modulus);
}
public static function div(BigInteger $dividend, BigInteger $divisor, BigInteger $modulus): BigInteger
{
return self::mul($dividend, Math::inverseMod($divisor, $modulus), $modulus);
}
}

View File

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util\Ecc;
use Brick\Math\BigInteger;
/**
* Copyright (C) 2012 Matyas Danter.
*
* 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.
*/
/**
* @internal
*/
final class NistCurve
{
/**
* Returns an NIST P-256 curve.
*/
public static function curve256(): Curve
{
$p = BigInteger::fromBase('ffffffff00000001000000000000000000000000ffffffffffffffffffffffff', 16);
$a = BigInteger::fromBase('ffffffff00000001000000000000000000000000fffffffffffffffffffffffc', 16);
$b = BigInteger::fromBase('5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b', 16);
$x = BigInteger::fromBase('6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296', 16);
$y = BigInteger::fromBase('4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5', 16);
$n = BigInteger::fromBase('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551', 16);
$generator = Point::create($x, $y, $n);
return new Curve(256, $p, $a, $b, $generator);
}
/**
* Returns an NIST P-384 curve.
*/
public static function curve384(): Curve
{
$p = BigInteger::fromBase(
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff',
16
);
$a = BigInteger::fromBase(
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc',
16
);
$b = BigInteger::fromBase(
'b3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef',
16
);
$x = BigInteger::fromBase(
'aa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7',
16
);
$y = BigInteger::fromBase(
'3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f',
16
);
$n = BigInteger::fromBase(
'ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973',
16
);
$generator = Point::create($x, $y, $n);
return new Curve(384, $p, $a, $b, $generator);
}
/**
* Returns an NIST P-521 curve.
*/
public static function curve521(): Curve
{
$p = BigInteger::fromBase(
'000001ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
16
);
$a = BigInteger::fromBase(
'000001fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc',
16
);
$b = BigInteger::fromBase(
'00000051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00',
16
);
$x = BigInteger::fromBase(
'000000c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66',
16
);
$y = BigInteger::fromBase(
'0000011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650',
16
);
$n = BigInteger::fromBase(
'000001fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409',
16
);
$generator = Point::create($x, $y, $n);
return new Curve(521, $p, $a, $b, $generator);
}
}

View File

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util\Ecc;
use Brick\Math\BigInteger;
use const STR_PAD_LEFT;
/**
* Copyright (C) 2012 Matyas Danter.
*
* 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.
*/
/**
* @internal
*/
final class Point
{
private function __construct(
private BigInteger $x,
private BigInteger $y,
private BigInteger $order,
private bool $infinity = false
) {
}
public static function create(BigInteger $x, BigInteger $y, ?BigInteger $order = null): self
{
return new self($x, $y, $order ?? BigInteger::zero());
}
public static function infinity(): self
{
$zero = BigInteger::zero();
return new self($zero, $zero, $zero, true);
}
public function isInfinity(): bool
{
return $this->infinity;
}
public function getOrder(): BigInteger
{
return $this->order;
}
public function getX(): BigInteger
{
return $this->x;
}
public function getY(): BigInteger
{
return $this->y;
}
public static function cswap(self $a, self $b, int $cond): void
{
self::cswapBigInteger($a->x, $b->x, $cond);
self::cswapBigInteger($a->y, $b->y, $cond);
self::cswapBigInteger($a->order, $b->order, $cond);
self::cswapBoolean($a->infinity, $b->infinity, $cond);
}
private static function cswapBoolean(bool &$a, bool &$b, int $cond): void
{
$sa = BigInteger::of((int) $a);
$sb = BigInteger::of((int) $b);
self::cswapBigInteger($sa, $sb, $cond);
$a = (bool) $sa->toBase(10);
$b = (bool) $sb->toBase(10);
}
private static function cswapBigInteger(BigInteger &$sa, BigInteger &$sb, int $cond): void
{
$size = max(mb_strlen($sa->toBase(2), '8bit'), mb_strlen($sb->toBase(2), '8bit'));
$mask = (string) (1 - $cond);
$mask = str_pad('', $size, $mask, STR_PAD_LEFT);
$mask = BigInteger::fromBase($mask, 2);
$taA = $sa->and($mask);
$taB = $sb->and($mask);
$sa = $sa->xor($sb)
->xor($taB);
$sb = $sa->xor($sb)
->xor($taA);
$sa = $sa->xor($sb)
->xor($taB);
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util\Ecc;
use Brick\Math\BigInteger;
/**
* Copyright (C) 2012 Matyas Danter.
*
* 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.
*/
/**
* @internal
*/
final class PrivateKey
{
private function __construct(
private readonly BigInteger $secret
) {
}
public static function create(BigInteger $secret): self
{
return new self($secret);
}
public function getSecret(): BigInteger
{
return $this->secret;
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util\Ecc;
/**
* Copyright (C) 2012 Matyas Danter.
*
* 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.
*/
/**
* @internal
*/
final class PublicKey
{
public function __construct(
private readonly Point $point
) {
}
public function getPoint(): Point
{
return $this->point;
}
}

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,37 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use InvalidArgumentException;
use Throwable;
use const JSON_THROW_ON_ERROR;
use const JSON_UNESCAPED_SLASHES;
use const JSON_UNESCAPED_UNICODE;
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 InvalidArgumentException('Invalid content.', $throwable->getCode(), $throwable);
}
}
public static function decode(string $payload): mixed
{
try {
return json_decode(
$payload,
true,
512,
JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
} catch (Throwable $throwable) {
throw new InvalidArgumentException('Unsupported input.', $throwable->getCode(), $throwable);
}
}
}

View File

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use function in_array;
use function is_array;
use function is_string;
/**
* @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,243 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Core\Util;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
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;
use function array_key_exists;
use function count;
use function is_array;
/**
* @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::decodeNoPadding($value));
}
private function fromBase64ToInteger(string $value): string
{
$unpacked = unpack('H*', Base64UrlSafe::decodeNoPadding($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,28 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A128CBCHS256 extends AESCBCHS
{
public function getCEKSize(): int
{
return 256;
}
public function name(): string
{
return 'A128CBC-HS256';
}
protected function getHashAlgorithm(): string
{
return 'sha256';
}
protected function getMode(): string
{
return 'aes-128-cbc';
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A128GCM extends AESGCM
{
public function getCEKSize(): int
{
return 128;
}
public function name(): string
{
return 'A128GCM';
}
protected function getMode(): string
{
return 'aes-128-gcm';
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A192CBCHS384 extends AESCBCHS
{
public function getCEKSize(): int
{
return 384;
}
public function name(): string
{
return 'A192CBC-HS384';
}
protected function getHashAlgorithm(): string
{
return 'sha384';
}
protected function getMode(): string
{
return 'aes-192-cbc';
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A192GCM extends AESGCM
{
public function getCEKSize(): int
{
return 192;
}
public function name(): string
{
return 'A192GCM';
}
protected function getMode(): string
{
return 'aes-192-gcm';
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A256CBCHS512 extends AESCBCHS
{
public function getCEKSize(): int
{
return 512;
}
public function name(): string
{
return 'A256CBC-HS512';
}
protected function getHashAlgorithm(): string
{
return 'sha512';
}
protected function getMode(): string
{
return 'aes-256-cbc';
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
final class A256GCM extends AESGCM
{
public function getCEKSize(): int
{
return 256;
}
public function name(): string
{
return 'A256GCM';
}
protected function getMode(): string
{
return 'aes-256-gcm';
}
}

View File

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithm;
use RuntimeException;
use function extension_loaded;
use const OPENSSL_RAW_DATA;
abstract class AESCBCHS implements ContentEncryptionAlgorithm
{
public function __construct()
{
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
}
public function allowedKeyTypes(): array
{
return []; //Irrelevant
}
public function encryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
?string &$tag = null
): string {
$k = mb_substr($cek, $this->getCEKSize() / 16, null, '8bit');
$result = openssl_encrypt($data, $this->getMode(), $k, OPENSSL_RAW_DATA, $iv);
if ($result === false) {
throw new RuntimeException('Unable to encrypt the content');
}
$tag = $this->calculateAuthenticationTag($result, $cek, $iv, $aad, $encoded_protected_header);
return $result;
}
public function decryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
string $tag
): string {
if (! $this->isTagValid($data, $cek, $iv, $aad, $encoded_protected_header, $tag)) {
throw new RuntimeException('Unable to decrypt or to verify the tag.');
}
$k = mb_substr($cek, $this->getCEKSize() / 16, null, '8bit');
$result = openssl_decrypt($data, $this->getMode(), $k, OPENSSL_RAW_DATA, $iv);
if ($result === false) {
throw new RuntimeException('Unable to decrypt or to verify the tag.');
}
return $result;
}
public function getIVSize(): int
{
return 128;
}
protected function calculateAuthenticationTag(
string $encrypted_data,
string $cek,
string $iv,
?string $aad,
string $encoded_header
): string {
$calculated_aad = $encoded_header;
if ($aad !== null) {
$calculated_aad .= '.' . Base64UrlSafe::encodeUnpadded($aad);
}
$mac_key = mb_substr($cek, 0, $this->getCEKSize() / 16, '8bit');
$auth_data_length = mb_strlen($encoded_header, '8bit');
$secured_input = implode('', [
$calculated_aad,
$iv,
$encrypted_data,
pack('N2', ($auth_data_length / 2_147_483_647) * 8, ($auth_data_length % 2_147_483_647) * 8),
]);
$hash = hash_hmac($this->getHashAlgorithm(), $secured_input, $mac_key, true);
return mb_substr($hash, 0, mb_strlen($hash, '8bit') / 2, '8bit');
}
protected function isTagValid(
string $encrypted_data,
string $cek,
string $iv,
?string $aad,
string $encoded_header,
string $authentication_tag
): bool {
return hash_equals(
$authentication_tag,
$this->calculateAuthenticationTag($encrypted_data, $cek, $iv, $aad, $encoded_header)
);
}
abstract protected function getHashAlgorithm(): string;
abstract protected function getMode(): string;
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\ContentEncryption;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Encryption\Algorithm\ContentEncryptionAlgorithm;
use RuntimeException;
use function extension_loaded;
use const OPENSSL_RAW_DATA;
abstract class AESGCM implements ContentEncryptionAlgorithm
{
public function __construct()
{
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
}
public function allowedKeyTypes(): array
{
return []; //Irrelevant
}
public function encryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
?string &$tag = null
): string {
$calculated_aad = $encoded_protected_header;
if ($aad !== null) {
$calculated_aad .= '.' . Base64UrlSafe::encodeUnpadded($aad);
}
$tag = '';
$result = openssl_encrypt($data, $this->getMode(), $cek, OPENSSL_RAW_DATA, $iv, $tag, $calculated_aad);
if ($result === false) {
throw new RuntimeException('Unable to encrypt the content');
}
return $result;
}
public function decryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
string $tag
): string {
$calculated_aad = $encoded_protected_header;
if ($aad !== null) {
$calculated_aad .= '.' . Base64UrlSafe::encodeUnpadded($aad);
}
$result = openssl_decrypt($data, $this->getMode(), $cek, OPENSSL_RAW_DATA, $iv, $tag, $calculated_aad);
if ($result === false) {
throw new RuntimeException('Unable to decrypt the content');
}
return $result;
}
public function getIVSize(): int
{
return 96;
}
abstract protected function getMode(): string;
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm;
use Jose\Component\Core\Algorithm;
interface ContentEncryptionAlgorithm extends Algorithm
{
/**
* This method encrypts the data using the given CEK, IV, AAD and protected header. The variable $tag is populated
* on success.
*
* @param string $data The data to encrypt
* @param string $cek The content encryption key
* @param string $iv The Initialization Vector
* @param string|null $aad Additional Additional Authenticated Data
* @param string $encoded_protected_header The Protected Header encoded in Base64Url
* @param string $tag Tag
*/
public function encryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
?string &$tag = null
): string;
/**
* This method tries to decrypt the data using the given CEK, IV, AAD, protected header and tag.
*
* @param string $data The data to decrypt
* @param string $cek The content encryption key
* @param string $iv The Initialization Vector
* @param string|null $aad Additional Additional Authenticated Data
* @param string $encoded_protected_header The Protected Header encoded in Base64Url
* @param string $tag Tag
*/
public function decryptContent(
string $data,
string $cek,
string $iv,
?string $aad,
string $encoded_protected_header,
string $tag
): string;
/**
* Returns the size of the IV used by this encryption method.
*/
public function getIVSize(): int;
/**
* Returns the size of the CEK used by this encryption method.
*/
public function getCEKSize(): int;
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
final class A128GCMKW extends AESGCMKW
{
public function name(): string
{
return 'A128GCMKW';
}
protected function getKeySize(): int
{
return 128;
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A128KW as Wrapper;
use AESKW\Wrapper as WrapperInterface;
final class A128KW extends AESKW
{
public function name(): string
{
return 'A128KW';
}
protected function getWrapper(): WrapperInterface
{
return new Wrapper();
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
final class A192GCMKW extends AESGCMKW
{
public function name(): string
{
return 'A192GCMKW';
}
protected function getKeySize(): int
{
return 192;
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A192KW as Wrapper;
use AESKW\Wrapper as WrapperInterface;
final class A192KW extends AESKW
{
public function name(): string
{
return 'A192KW';
}
protected function getWrapper(): WrapperInterface
{
return new Wrapper();
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
final class A256GCMKW extends AESGCMKW
{
public function name(): string
{
return 'A256GCMKW';
}
protected function getKeySize(): int
{
return 256;
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A256KW as Wrapper;
use AESKW\Wrapper as WrapperInterface;
final class A256KW extends AESKW
{
public function name(): string
{
return 'A256KW';
}
protected function getWrapper(): WrapperInterface
{
return new Wrapper();
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\Wrapper as WrapperInterface;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use RuntimeException;
use function extension_loaded;
use function in_array;
use function is_string;
use const OPENSSL_RAW_DATA;
abstract class AESGCMKW implements KeyWrapping
{
public function __construct()
{
if (! extension_loaded('openssl')) {
throw new RuntimeException('Please install the OpenSSL extension');
}
if (! interface_exists(WrapperInterface::class)) {
throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms');
}
}
public function allowedKeyTypes(): array
{
return ['oct'];
}
/**
* @param array<string, mixed> $completeHeader
* @param array<string, mixed> $additionalHeader
*/
public function wrapKey(JWK $key, string $cek, array $completeHeader, array &$additionalHeader): string
{
$kek = $this->getKey($key);
$iv = random_bytes(96 / 8);
$additionalHeader['iv'] = Base64UrlSafe::encodeUnpadded($iv);
$mode = sprintf('aes-%d-gcm', $this->getKeySize());
$tag = '';
$encrypted_cek = openssl_encrypt($cek, $mode, $kek, OPENSSL_RAW_DATA, $iv, $tag, '');
if ($encrypted_cek === false) {
throw new RuntimeException('Unable to encrypt the CEK');
}
$additionalHeader['tag'] = Base64UrlSafe::encodeUnpadded($tag);
return $encrypted_cek;
}
/**
* @param array<string, mixed> $completeHeader
*/
public function unwrapKey(JWK $key, string $encrypted_cek, array $completeHeader): string
{
$kek = $this->getKey($key);
(isset($completeHeader['iv']) && is_string($completeHeader['iv'])) || throw new InvalidArgumentException(
'Parameter "iv" is missing.'
);
(isset($completeHeader['tag']) && is_string($completeHeader['tag'])) || throw new InvalidArgumentException(
'Parameter "tag" is missing.'
);
$tag = Base64UrlSafe::decodeNoPadding($completeHeader['tag']);
$iv = Base64UrlSafe::decodeNoPadding($completeHeader['iv']);
$mode = sprintf('aes-%d-gcm', $this->getKeySize());
$cek = openssl_decrypt($encrypted_cek, $mode, $kek, OPENSSL_RAW_DATA, $iv, $tag, '');
if ($cek === false) {
throw new RuntimeException('Unable to decrypt the CEK');
}
return $cek;
}
public function getKeyManagementMode(): string
{
return self::MODE_WRAP;
}
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 getKeySize(): int;
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\Wrapper as WrapperInterface;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use RuntimeException;
use function in_array;
use function is_string;
abstract class AESKW implements KeyWrapping
{
public function __construct()
{
if (! interface_exists(WrapperInterface::class)) {
throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms');
}
}
public function allowedKeyTypes(): array
{
return ['oct'];
}
/**
* @param array<string, mixed> $completeHeader
* @param array<string, mixed> $additionalHeader
*/
public function wrapKey(JWK $key, string $cek, array $completeHeader, array &$additionalHeader): string
{
$k = $this->getKey($key);
$wrapper = $this->getWrapper();
return $wrapper::wrap($k, $cek);
}
/**
* @param array<string, mixed> $completeHeader
*/
public function unwrapKey(JWK $key, string $encrypted_cek, array $completeHeader): string
{
$k = $this->getKey($key);
$wrapper = $this->getWrapper();
return $wrapper::unwrap($k, $encrypted_cek);
}
public function getKeyManagementMode(): string
{
return self::MODE_WRAP;
}
abstract protected function getWrapper(): WrapperInterface;
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.');
}
return Base64UrlSafe::decodeNoPadding($k);
}
}

View File

@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Brick\Math\BigInteger;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use Jose\Component\Core\Util\Ecc\Curve;
use Jose\Component\Core\Util\Ecc\EcDH;
use Jose\Component\Core\Util\Ecc\NistCurve;
use Jose\Component\Core\Util\Ecc\PrivateKey;
use Jose\Component\Core\Util\ECKey;
use Jose\Component\Encryption\Algorithm\KeyEncryption\Util\ConcatKDF;
use RuntimeException;
use Throwable;
use function array_key_exists;
use function extension_loaded;
use function function_exists;
use function in_array;
use function is_array;
use function is_string;
abstract class AbstractECDH implements KeyAgreement
{
public function allowedKeyTypes(): array
{
return ['EC', 'OKP'];
}
/**
* @param array<string, mixed> $complete_header
* @param array<string, mixed> $additional_header_values
*/
public function getAgreementKey(
int $encryptionKeyLength,
string $algorithm,
JWK $recipientKey,
?JWK $senderKey,
array $complete_header = [],
array &$additional_header_values = []
): string {
if ($recipientKey->has('d')) {
[$public_key, $private_key] = $this->getKeysFromPrivateKeyAndHeader($recipientKey, $complete_header);
} else {
[$public_key, $private_key] = $this->getKeysFromPublicKey(
$recipientKey,
$senderKey,
$additional_header_values
);
}
$agreed_key = $this->calculateAgreementKey($private_key, $public_key);
$apu = array_key_exists('apu', $complete_header) ? $complete_header['apu'] : '';
is_string($apu) || throw new InvalidArgumentException('Invalid APU.');
$apv = array_key_exists('apv', $complete_header) ? $complete_header['apv'] : '';
is_string($apv) || throw new InvalidArgumentException('Invalid APU.');
return ConcatKDF::generate($agreed_key, $algorithm, $encryptionKeyLength, $apu, $apv);
}
public function getKeyManagementMode(): string
{
return self::MODE_AGREEMENT;
}
protected function calculateAgreementKey(JWK $private_key, JWK $public_key): string
{
$crv = $public_key->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Invalid key parameter "crv"');
}
switch ($crv) {
case 'P-256':
case 'P-384':
case 'P-521':
$curve = $this->getCurve($crv);
if (function_exists('openssl_pkey_derive')) {
try {
$publicPem = ECKey::convertPublicKeyToPEM($public_key);
$privatePem = ECKey::convertPrivateKeyToPEM($private_key);
$res = openssl_pkey_derive($publicPem, $privatePem, $curve->getSize());
if ($res === false) {
throw new RuntimeException('Unable to derive the key');
}
return $res;
} catch (Throwable) {
//Does nothing. Will fallback to the pure PHP function
}
}
$x = $public_key->get('x');
if (! is_string($x)) {
throw new InvalidArgumentException('Invalid key parameter "x"');
}
$y = $public_key->get('y');
if (! is_string($y)) {
throw new InvalidArgumentException('Invalid key parameter "y"');
}
$d = $private_key->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Invalid key parameter "d"');
}
$rec_x = $this->convertBase64ToBigInteger($x);
$rec_y = $this->convertBase64ToBigInteger($y);
$sen_d = $this->convertBase64ToBigInteger($d);
$priv_key = PrivateKey::create($sen_d);
$pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y);
return $this->convertDecToBin(EcDH::computeSharedKey($curve, $pub_key, $priv_key));
case 'X25519':
$this->checkSodiumExtensionIsAvailable();
$x = $public_key->get('x');
if (! is_string($x)) {
throw new InvalidArgumentException('Invalid key parameter "x"');
}
$d = $private_key->get('d');
if (! is_string($d)) {
throw new InvalidArgumentException('Invalid key parameter "d"');
}
$sKey = Base64UrlSafe::decodeNoPadding($d);
$recipientPublickey = Base64UrlSafe::decodeNoPadding($x);
return sodium_crypto_scalarmult($sKey, $recipientPublickey);
default:
throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
}
}
/**
* @param array<string, mixed> $additional_header_values
* @return JWK[]
*/
protected function getKeysFromPublicKey(
JWK $recipient_key,
?JWK $senderKey,
array &$additional_header_values
): array {
$this->checkKey($recipient_key, false);
$public_key = $recipient_key;
$crv = $public_key->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Invalid key parameter "crv"');
}
$private_key = match ($crv) {
'P-256', 'P-384', 'P-521' => $senderKey ?? ECKey::createECKey($crv),
'X25519' => $senderKey ?? $this->createOKPKey('X25519'),
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv)),
};
$epk = $private_key->toPublic()
->all();
$additional_header_values['epk'] = $epk;
return [$public_key, $private_key];
}
/**
* @param array<string, mixed> $complete_header
* @return JWK[]
*/
protected function getKeysFromPrivateKeyAndHeader(JWK $recipient_key, array $complete_header): array
{
$this->checkKey($recipient_key, true);
$private_key = $recipient_key;
$public_key = $this->getPublicKey($complete_header);
if ($private_key->get('crv') !== $public_key->get('crv')) {
throw new InvalidArgumentException('Curves are different');
}
return [$public_key, $private_key];
}
/**
* @param array<string, mixed> $complete_header
*/
private function getPublicKey(array $complete_header): JWK
{
if (! isset($complete_header['epk'])) {
throw new InvalidArgumentException('The header parameter "epk" is missing.');
}
if (! is_array($complete_header['epk'])) {
throw new InvalidArgumentException('The header parameter "epk" is not an array of parameters');
}
$public_key = new JWK($complete_header['epk']);
$this->checkKey($public_key, false);
return $public_key;
}
private function checkKey(JWK $key, bool $is_private): 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));
}
}
$crv = $key->get('crv');
if (! is_string($crv)) {
throw new InvalidArgumentException('Invalid key parameter "crv"');
}
switch ($crv) {
case 'P-256':
case 'P-384':
case 'P-521':
if (! $key->has('y')) {
throw new InvalidArgumentException('The key parameter "y" is missing.');
}
break;
case 'X25519':
break;
default:
throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv));
}
if ($is_private === true && ! $key->has('d')) {
throw new InvalidArgumentException('The key parameter "d" is missing.');
}
}
private function getCurve(string $crv): Curve
{
return match ($crv) {
'P-256' => NistCurve::curve256(),
'P-384' => NistCurve::curve384(),
'P-521' => NistCurve::curve521(),
default => throw new InvalidArgumentException(sprintf('The curve "%s" is not supported', $crv)),
};
}
private function convertBase64ToBigInteger(string $value): BigInteger
{
$data = unpack('H*', Base64UrlSafe::decodeNoPadding($value));
if (! is_array($data) || ! isset($data[1]) || ! is_string($data[1])) {
throw new InvalidArgumentException('Unable to convert base64 to integer');
}
return BigInteger::fromBase($data[1], 16);
}
private function convertDecToBin(BigInteger $dec): string
{
if ($dec->compareTo(BigInteger::zero()) < 0) {
throw new InvalidArgumentException('Unable to convert negative integer to string');
}
$hex = $dec->toBase(16);
if (mb_strlen($hex, '8bit') % 2 !== 0) {
$hex = '0' . $hex;
}
$bin = hex2bin($hex);
if ($bin === false) {
throw new InvalidArgumentException('Unable to convert integer to string');
}
return $bin;
}
/**
* @param string $curve The curve
*/
private function createOKPKey(string $curve): JWK
{
$this->checkSodiumExtensionIsAvailable();
switch ($curve) {
case 'X25519':
$keyPair = sodium_crypto_box_keypair();
$d = sodium_crypto_box_secretkey($keyPair);
$x = sodium_crypto_box_publickey($keyPair);
break;
case 'Ed25519':
$keyPair = sodium_crypto_sign_keypair();
$secret = sodium_crypto_sign_secretkey($keyPair);
$secretLength = mb_strlen($secret, '8bit');
$d = mb_substr($secret, 0, -$secretLength / 2, '8bit');
$x = sodium_crypto_sign_publickey($keyPair);
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported "%s" curve', $curve));
}
return new JWK([
'kty' => 'OKP',
'crv' => $curve,
'x' => Base64UrlSafe::encodeUnpadded($x),
'd' => Base64UrlSafe::encodeUnpadded($d),
]);
}
private function checkSodiumExtensionIsAvailable(): void
{
if (! extension_loaded('sodium')) {
throw new RuntimeException('The extension "sodium" is not available. Please install it to use this method');
}
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\Wrapper as WrapperInterface;
use RuntimeException;
abstract class AbstractECDHAESKW implements KeyAgreementWithKeyWrapping
{
public function __construct()
{
if (! interface_exists(WrapperInterface::class)) {
throw new RuntimeException('Please install "spomky-labs/aes-key-wrap" to use AES-KW algorithms');
}
}
public function allowedKeyTypes(): array
{
return ['EC', 'OKP'];
}
public function getKeyManagementMode(): string
{
return self::MODE_WRAP;
}
abstract protected function getWrapper(): WrapperInterface;
abstract protected function getKeyLength(): int;
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use InvalidArgumentException;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\Base64UrlSafe;
use function in_array;
use function is_string;
final class Dir implements DirectEncryption
{
public function getCEK(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);
}
public function name(): string
{
return 'dir';
}
public function allowedKeyTypes(): array
{
return ['oct'];
}
public function getKeyManagementMode(): string
{
return self::MODE_DIRECT;
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\KeyEncryptionAlgorithm;
interface DirectEncryption extends KeyEncryptionAlgorithm
{
/**
* Returns the CEK.
*
* @param JWK $key The key used to get the CEK
*/
public function getCEK(JWK $key): string;
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
final class ECDHES extends AbstractECDH
{
public function name(): string
{
return 'ECDH-ES';
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A128KW as Wrapper;
final class ECDHESA128KW extends ECDHESAESKW
{
public function name(): string
{
return 'ECDH-ES+A128KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 128;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A192KW as Wrapper;
final class ECDHESA192KW extends ECDHESAESKW
{
public function name(): string
{
return 'ECDH-ES+A192KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 192;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A256KW as Wrapper;
final class ECDHESA256KW extends ECDHESAESKW
{
public function name(): string
{
return 'ECDH-ES+A256KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 256;
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
abstract class ECDHESAESKW extends AbstractECDHAESKW
{
/**
* @param array<string, mixed> $complete_header
* @param array<string, mixed> $additional_header_values
*/
public function wrapAgreementKey(
JWK $recipientKey,
?JWK $senderKey,
string $cek,
int $encryption_key_length,
array $complete_header,
array &$additional_header_values
): string {
$ecdh_es = new ECDHES();
$agreement_key = $ecdh_es->getAgreementKey(
$this->getKeyLength(),
$this->name(),
$recipientKey->toPublic(),
$senderKey,
$complete_header,
$additional_header_values
);
$wrapper = $this->getWrapper();
return $wrapper::wrap($agreement_key, $cek);
}
/**
* @param array<string, mixed> $complete_header
*/
public function unwrapAgreementKey(
JWK $recipientKey,
?JWK $senderKey,
string $encrypted_cek,
int $encryption_key_length,
array $complete_header
): string {
$ecdh_es = new ECDHES();
$agreement_key = $ecdh_es->getAgreementKey(
$this->getKeyLength(),
$this->name(),
$recipientKey,
$senderKey,
$complete_header
);
$wrapper = $this->getWrapper();
return $wrapper::unwrap($agreement_key, $encrypted_cek);
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use Jose\Component\Core\JWK;
use LogicException;
final class ECDHSS extends AbstractECDH
{
public function name(): string
{
return 'ECDH-SS';
}
/**
* @param array<string, mixed> $complete_header
* @param array<string, mixed> $additional_header_values
*/
public function getAgreementKey(
int $encryptionKeyLength,
string $algorithm,
JWK $recipientKey,
?JWK $senderKey,
array $complete_header = [],
array &$additional_header_values = []
): string {
if ($senderKey === null) {
throw new LogicException('The sender key shall be set');
}
$agreedKey = parent::getAgreementKey(
$encryptionKeyLength,
$algorithm,
$recipientKey,
$senderKey,
$complete_header,
$additional_header_values
);
unset($additional_header_values['epk']);
return $agreedKey;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A128KW as Wrapper;
final class ECDHSSA128KW extends ECDHSSAESKW
{
public function name(): string
{
return 'ECDH-SS+A128KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 128;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Jose\Component\Encryption\Algorithm\KeyEncryption;
use AESKW\A192KW as Wrapper;
final class ECDHSSA192KW extends ECDHSSAESKW
{
public function name(): string
{
return 'ECDH-SS+A192KW';
}
protected function getWrapper(): Wrapper
{
return new Wrapper();
}
protected function getKeyLength(): int
{
return 192;
}
}

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