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