aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Security
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Security')
-rw-r--r--lib/private/Security/Signature/Model/IncomingSignedRequest.php179
-rw-r--r--lib/private/Security/Signature/Model/OutgoingSignedRequest.php78
-rw-r--r--lib/private/Security/Signature/Model/SignedRequest.php62
-rw-r--r--lib/private/Security/Signature/SignatureManager.php530
4 files changed, 394 insertions, 455 deletions
diff --git a/lib/private/Security/Signature/Model/IncomingSignedRequest.php b/lib/private/Security/Signature/Model/IncomingSignedRequest.php
index 8fe83a7b09b..77914d1e3b2 100644
--- a/lib/private/Security/Signature/Model/IncomingSignedRequest.php
+++ b/lib/private/Security/Signature/Model/IncomingSignedRequest.php
@@ -10,11 +10,14 @@ namespace OC\Security\Signature\Model;
use JsonSerializable;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
-use NCU\Security\Signature\Exceptions\IncomingRequestNotFoundException;
+use NCU\Security\Signature\Exceptions\IncomingRequestException;
use NCU\Security\Signature\Exceptions\SignatoryException;
+use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
+use OC\Security\Signature\SignatureManager;
use OCP\IRequest;
/**
@@ -26,77 +29,134 @@ use OCP\IRequest;
class IncomingSignedRequest extends SignedRequest implements
IIncomingSignedRequest,
JsonSerializable {
- private ?IRequest $request = null;
- private int $time = 0;
private string $origin = '';
- private string $estimatedSignature = '';
/**
- * @inheritDoc
+ * @throws IncomingRequestException if incoming request is wrongly signed
+ * @throws SignatureNotFoundException if signature is not fully implemented
+ */
+ public function __construct(
+ string $body,
+ private readonly IRequest $request,
+ private readonly array $options = [],
+ ) {
+ parent::__construct($body);
+ $this->verifyHeadersFromRequest();
+ $this->extractSignatureHeaderFromRequest();
+ }
+
+ /**
+ * confirm that:
*
- * @param ISignatory $signatory
+ * - date is available in the header and its value is less than 5 minutes old
+ * - content-length is available and is the same as the payload size
+ * - digest is available and fit the checksum of the payload
*
- * @return $this
- * @throws SignatoryException
- * @throws IdentityNotFoundException
- * @since 31.0.0
+ * @throws IncomingRequestException
+ * @throws SignatureNotFoundException
*/
- public function setSignatory(ISignatory $signatory): self {
- $identity = \OCP\Server::get(ISignatureManager::class)->extractIdentityFromUri($signatory->getKeyId());
- if ($identity !== $this->getOrigin()) {
- throw new SignatoryException('keyId from provider is different from the one from signed request');
+ private function verifyHeadersFromRequest(): void {
+ // confirm presence of date, content-length, digest and Signature
+ $date = $this->getRequest()->getHeader('date');
+ if ($date === '') {
+ throw new SignatureNotFoundException('missing date in header');
+ }
+ $contentLength = $this->getRequest()->getHeader('content-length');
+ if ($contentLength === '') {
+ throw new SignatureNotFoundException('missing content-length in header');
+ }
+ $digest = $this->getRequest()->getHeader('digest');
+ if ($digest === '') {
+ throw new SignatureNotFoundException('missing digest in header');
+ }
+ if ($this->getRequest()->getHeader('Signature') === '') {
+ throw new SignatureNotFoundException('missing Signature in header');
}
- parent::setSignatory($signatory);
- return $this;
+ // confirm date
+ try {
+ $dTime = new \DateTime($date);
+ $requestTime = $dTime->getTimestamp();
+ } catch (\Exception) {
+ throw new IncomingRequestException('datetime exception');
+ }
+ if ($requestTime < (time() - ($this->options['ttl'] ?? SignatureManager::DATE_TTL))) {
+ throw new IncomingRequestException('object is too old');
+ }
+
+ // confirm validity of content-length
+ if (strlen($this->getBody()) !== (int)$contentLength) {
+ throw new IncomingRequestException('inexact content-length in header');
+ }
+
+ // confirm digest value, based on body
+ if ($digest !== $this->getDigest()) {
+ throw new IncomingRequestException('invalid value for digest in header');
+ }
}
/**
- * @inheritDoc
+ * extract data from the header entry 'Signature' and convert its content from string to an array
+ * also confirm that it contains the minimum mandatory information
*
- * @param IRequest $request
- * @return IIncomingSignedRequest
- * @since 31.0.0
+ * @throws IncomingRequestException
*/
- public function setRequest(IRequest $request): IIncomingSignedRequest {
- $this->request = $request;
- return $this;
+ private function extractSignatureHeaderFromRequest(): void {
+ $sign = [];
+ foreach (explode(',', $this->getRequest()->getHeader('Signature')) as $entry) {
+ if ($entry === '' || !strpos($entry, '=')) {
+ continue;
+ }
+
+ [$k, $v] = explode('=', $entry, 2);
+ preg_match('/"([^"]+)"/', $v, $var);
+ if ($var[0] !== '') {
+ $v = trim($var[0], '"');
+ }
+ $sign[$k] = $v;
+ }
+
+ $this->setSignatureElements($sign);
+
+ try {
+ // confirm keys are in the Signature header
+ $this->getSignatureElement('keyId');
+ $this->getSignatureElement('headers');
+ $this->setSignedSignature($this->getSignatureElement('signature'));
+ } catch (SignatureElementNotFoundException $e) {
+ throw new IncomingRequestException($e->getMessage());
+ }
}
/**
* @inheritDoc
*
* @return IRequest
- * @throws IncomingRequestNotFoundException
* @since 31.0.0
*/
public function getRequest(): IRequest {
- if ($this->request === null) {
- throw new IncomingRequestNotFoundException();
- }
return $this->request;
}
/**
* @inheritDoc
*
- * @param int $time
- * @return IIncomingSignedRequest
- * @since 31.0.0
- */
- public function setTime(int $time): IIncomingSignedRequest {
- $this->time = $time;
- return $this;
- }
-
- /**
- * @inheritDoc
+ * @param ISignatory $signatory
*
- * @return int
+ * @return $this
+ * @throws IdentityNotFoundException
+ * @throws IncomingRequestException
+ * @throws SignatoryException
* @since 31.0.0
*/
- public function getTime(): int {
- return $this->time;
+ public function setSignatory(ISignatory $signatory): self {
+ $identity = \OCP\Server::get(ISignatureManager::class)->extractIdentityFromUri($signatory->getKeyId());
+ if ($identity !== $this->getOrigin()) {
+ throw new SignatoryException('keyId from provider is different from the one from signed request');
+ }
+
+ parent::setSignatory($signatory);
+ return $this;
}
/**
@@ -115,9 +175,13 @@ class IncomingSignedRequest extends SignedRequest implements
* @inheritDoc
*
* @return string
+ * @throws IncomingRequestException
* @since 31.0.0
*/
public function getOrigin(): string {
+ if ($this->origin === '') {
+ throw new IncomingRequestException('empty origin');
+ }
return $this->origin;
}
@@ -126,44 +190,19 @@ class IncomingSignedRequest extends SignedRequest implements
* keyId is a mandatory entry in the headers of a signed request.
*
* @return string
+ * @throws SignatureElementNotFoundException
* @since 31.0.0
*/
public function getKeyId(): string {
- return $this->getSignatureHeader()['keyId'] ?? '';
- }
-
- /**
- * @inheritDoc
- *
- * @param string $signature
- * @return IIncomingSignedRequest
- * @since 31.0.0
- */
- public function setEstimatedSignature(string $signature): IIncomingSignedRequest {
- $this->estimatedSignature = $signature;
- return $this;
- }
-
- /**
- * @inheritDoc
- *
- * @return string
- * @since 31.0.0
- */
- public function getEstimatedSignature(): string {
- return $this->estimatedSignature;
+ return $this->getSignatureElement('keyId');
}
public function jsonSerialize(): array {
return array_merge(
parent::jsonSerialize(),
[
- 'body' => $this->getBody(),
- 'time' => $this->getTime(),
- 'incomingRequest' => $this->request ?? false,
- 'origin' => $this->getOrigin(),
- 'keyId' => $this->getKeyId(),
- 'estimatedSignature' => $this->getEstimatedSignature(),
+ 'options' => $this->options,
+ 'origin' => $this->origin,
]
);
}
diff --git a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php
index 04efcf8bfe1..d2d5b95e7b6 100644
--- a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php
+++ b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php
@@ -9,8 +9,11 @@ declare(strict_types=1);
namespace OC\Security\Signature\Model;
use JsonSerializable;
+use NCU\Security\Signature\ISignatoryManager;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\IOutgoingSignedRequest;
+use NCU\Security\Signature\SignatureAlgorithm;
+use OC\Security\Signature\SignatureManager;
/**
* extends ISignedRequest to add info requested at the generation of the signature
@@ -23,8 +26,44 @@ class OutgoingSignedRequest extends SignedRequest implements
JsonSerializable {
private string $host = '';
private array $headers = [];
- private string $clearSignature = '';
- private string $algorithm;
+ /** @var list<string> $headerList */
+ private array $headerList = [];
+ private SignatureAlgorithm $algorithm;
+ public function __construct(
+ string $body,
+ ISignatoryManager $signatoryManager,
+ private readonly string $identity,
+ private readonly string $method,
+ private readonly string $path,
+ ) {
+ parent::__construct($body);
+
+ $options = $signatoryManager->getOptions();
+ $this->setHost($identity)
+ ->setAlgorithm(SignatureAlgorithm::from($options['algorithm'] ?? 'sha256'))
+ ->setSignatory($signatoryManager->getLocalSignatory());
+
+ $headers = array_merge([
+ '(request-target)' => strtolower($method) . ' ' . $path,
+ 'content-length' => strlen($this->getBody()),
+ 'date' => gmdate($options['dateHeader'] ?? SignatureManager::DATE_HEADER),
+ 'digest' => $this->getDigest(),
+ 'host' => $this->getHost()
+ ], $options['extraSignatureHeaders'] ?? []);
+
+ $signing = $headerList = [];
+ foreach ($headers as $element => $value) {
+ $value = $headers[$element];
+ $signing[] = $element . ': ' . $value;
+ $headerList[] = $element;
+ if ($element !== '(request-target)') {
+ $this->addHeader($element, $value);
+ }
+ }
+
+ $this->setHeaderList($headerList)
+ ->setClearSignature(implode("\n", $signing));
+ }
/**
* @inheritDoc
@@ -52,12 +91,12 @@ class OutgoingSignedRequest extends SignedRequest implements
* @inheritDoc
*
* @param string $key
- * @param string|int|float|bool|array $value
+ * @param string|int|float $value
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
- public function addHeader(string $key, string|int|float|bool|array $value): IOutgoingSignedRequest {
+ public function addHeader(string $key, string|int|float $value): IOutgoingSignedRequest {
$this->headers[$key] = $value;
return $this;
}
@@ -73,37 +112,37 @@ class OutgoingSignedRequest extends SignedRequest implements
}
/**
- * @inheritDoc
+ * set the ordered list of used headers in the Signature
*
- * @param string $estimated
+ * @param list<string> $list
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
- public function setClearSignature(string $estimated): IOutgoingSignedRequest {
- $this->clearSignature = $estimated;
+ public function setHeaderList(array $list): IOutgoingSignedRequest {
+ $this->headerList = $list;
return $this;
}
/**
- * @inheritDoc
+ * returns ordered list of used headers in the Signature
*
- * @return string
+ * @return list<string>
* @since 31.0.0
*/
- public function getClearSignature(): string {
- return $this->clearSignature;
+ public function getHeaderList(): array {
+ return $this->headerList;
}
/**
* @inheritDoc
*
- * @param string $algorithm
+ * @param SignatureAlgorithm $algorithm
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
- public function setAlgorithm(string $algorithm): IOutgoingSignedRequest {
+ public function setAlgorithm(SignatureAlgorithm $algorithm): IOutgoingSignedRequest {
$this->algorithm = $algorithm;
return $this;
}
@@ -111,10 +150,10 @@ class OutgoingSignedRequest extends SignedRequest implements
/**
* @inheritDoc
*
- * @return string
+ * @return SignatureAlgorithm
* @since 31.0.0
*/
- public function getAlgorithm(): string {
+ public function getAlgorithm(): SignatureAlgorithm {
return $this->algorithm;
}
@@ -122,9 +161,12 @@ class OutgoingSignedRequest extends SignedRequest implements
return array_merge(
parent::jsonSerialize(),
[
+ 'host' => $this->host,
'headers' => $this->headers,
- 'host' => $this->getHost(),
- 'clearSignature' => $this->getClearSignature(),
+ 'algorithm' => $this->algorithm->value,
+ 'method' => $this->method,
+ 'identity' => $this->identity,
+ 'path' => $this->path,
]
);
}
diff --git a/lib/private/Security/Signature/Model/SignedRequest.php b/lib/private/Security/Signature/Model/SignedRequest.php
index 1587da9d631..56853ebade3 100644
--- a/lib/private/Security/Signature/Model/SignedRequest.php
+++ b/lib/private/Security/Signature/Model/SignedRequest.php
@@ -10,6 +10,7 @@ namespace OC\Security\Signature\Model;
use JsonSerializable;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
use NCU\Security\Signature\Model\ISignatory;
use NCU\Security\Signature\Model\ISignedRequest;
@@ -20,8 +21,9 @@ use NCU\Security\Signature\Model\ISignedRequest;
*/
class SignedRequest implements ISignedRequest, JsonSerializable {
private string $digest;
+ private array $signatureElements = [];
+ private string $clearSignature = '';
private string $signedSignature = '';
- private array $signatureHeader = [];
private ?ISignatory $signatory = null;
public function __construct(
@@ -54,12 +56,13 @@ class SignedRequest implements ISignedRequest, JsonSerializable {
/**
* @inheritDoc
*
- * @param array $signatureHeader
+ * @param array $elements
+ *
* @return ISignedRequest
* @since 31.0.0
*/
- public function setSignatureHeader(array $signatureHeader): ISignedRequest {
- $this->signatureHeader = $signatureHeader;
+ public function setSignatureElements(array $elements): ISignedRequest {
+ $this->signatureElements = $elements;
return $this;
}
@@ -69,8 +72,47 @@ class SignedRequest implements ISignedRequest, JsonSerializable {
* @return array
* @since 31.0.0
*/
- public function getSignatureHeader(): array {
- return $this->signatureHeader;
+ public function getSignatureElements(): array {
+ return $this->signatureElements;
+ }
+
+ /**
+ * @param string $key
+ *
+ * @return string
+ * @throws SignatureElementNotFoundException
+ * @since 31.0.0
+ *
+ */
+ public function getSignatureElement(string $key): string {
+ if (!array_key_exists($key, $this->signatureElements)) {
+ throw new SignatureElementNotFoundException('missing element ' . $key . ' in Signature header');
+ }
+
+ return $this->signatureElements[$key];
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @param string $clearSignature
+ *
+ * @return ISignedRequest
+ * @since 31.0.0
+ */
+ public function setClearSignature(string $clearSignature): ISignedRequest {
+ $this->clearSignature = $clearSignature;
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getClearSignature(): string {
+ return $this->clearSignature;
}
/**
@@ -134,9 +176,11 @@ class SignedRequest implements ISignedRequest, JsonSerializable {
public function jsonSerialize(): array {
return [
- 'body' => $this->getBody(),
- 'signatureHeader' => $this->getSignatureHeader(),
- 'signedSignature' => $this->getSignedSignature(),
+ 'body' => $this->body,
+ 'digest' => $this->digest,
+ 'signatureElements' => $this->signatureElements,
+ 'clearSignature' => $this->clearSignature,
+ 'signedSignature' => $this->signedSignature,
'signatory' => $this->signatory ?? false,
];
}
diff --git a/lib/private/Security/Signature/SignatureManager.php b/lib/private/Security/Signature/SignatureManager.php
index 8717171f4b4..2d895b465ab 100644
--- a/lib/private/Security/Signature/SignatureManager.php
+++ b/lib/private/Security/Signature/SignatureManager.php
@@ -1,7 +1,6 @@
<?php
declare(strict_types=1);
-
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
@@ -16,6 +15,7 @@ use NCU\Security\Signature\Exceptions\InvalidSignatureException;
use NCU\Security\Signature\Exceptions\SignatoryConflictException;
use NCU\Security\Signature\Exceptions\SignatoryException;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
use NCU\Security\Signature\Exceptions\SignatureException;
use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
use NCU\Security\Signature\ISignatoryManager;
@@ -45,7 +45,7 @@ use Psr\Log\LoggerInterface;
* "date": "Mon, 08 Jul 2024 14:16:20 GMT",
* "digest": "SHA-256=U7gNVUQiixe5BRbp4Tg0xCZMTcSWXXUZI2\\/xtHM40S0=",
* "host": "hostname.of.the.recipient",
- * "Signature": "keyId=\"https://author.hostname/key\",algorithm=\"ras-sha256\",headers=\"content-length
+ * "Signature": "keyId=\"https://author.hostname/key\",algorithm=\"sha256\",headers=\"content-length
* date digest host\",signature=\"DzN12OCS1rsA[...]o0VmxjQooRo6HHabg==\""
* }
*
@@ -66,11 +66,11 @@ use Psr\Log\LoggerInterface;
* @since 31.0.0
*/
class SignatureManager implements ISignatureManager {
- private const DATE_HEADER = 'D, d M Y H:i:s T';
- private const DATE_TTL = 300;
- private const SIGNATORY_TTL = 86400 * 3;
- private const TABLE_SIGNATORIES = 'sec_signatory';
- private const BODY_MAXSIZE = 50000; // max size of the payload of the request
+ public const DATE_HEADER = 'D, d M Y H:i:s T';
+ public const DATE_TTL = 300;
+ public const SIGNATORY_TTL = 86400 * 3;
+ public const TABLE_SIGNATORIES = 'sec_signatory';
+ public const BODY_MAXSIZE = 50000; // max size of the payload of the request
public const APPCONFIG_IDENTITY = 'security.signature.identity';
public function __construct(
@@ -98,25 +98,29 @@ class SignatureManager implements ISignatureManager {
?string $body = null,
): IIncomingSignedRequest {
$body = $body ?? file_get_contents('php://input');
- if (strlen($body) > self::BODY_MAXSIZE) {
+ $options = $signatoryManager->getOptions();
+ if (strlen($body) > ($options['bodyMaxSize'] ?? self::BODY_MAXSIZE)) {
throw new IncomingRequestException('content of request is too big');
}
- $signedRequest = new IncomingSignedRequest($body);
- $signedRequest->setRequest($this->request);
- $options = $signatoryManager->getOptions();
+ // generate IncomingSignedRequest based on body and request
+ $signedRequest = new IncomingSignedRequest($body, $this->request, $options);
+ try {
+ // we set origin based on the keyId defined in the Signature header of the request
+ $signedRequest->setOrigin($this->extractIdentityFromUri($signedRequest->getSignatureElement('keyId')));
+ } catch (IdentityNotFoundException $e) {
+ throw new IncomingRequestException($e->getMessage());
+ }
try {
- $this->verifyIncomingRequestTime($signedRequest, $options['ttl'] ?? self::DATE_TTL);
- $this->verifyIncomingRequestContent($signedRequest);
- $this->prepIncomingSignatureHeader($signedRequest);
- $this->verifyIncomingSignatureHeader($signedRequest);
- $this->prepEstimatedSignature($signedRequest, $options['extraSignatureHeaders'] ?? []);
- $this->verifyIncomingRequestSignature($signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL);
+ // confirm the validity of content and identity of the incoming request
+ $this->generateExpectedClearSignatureFromRequest($signedRequest, $options['extraSignatureHeaders'] ?? []);
+ $this->confirmIncomingRequestSignature($signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL);
} catch (SignatureException $e) {
$this->logger->warning(
'signature could not be verified', [
- 'exception' => $e, 'signedRequest' => $signedRequest,
+ 'exception' => $e,
+ 'signedRequest' => $signedRequest,
'signatoryManager' => get_class($signatoryManager)
]
);
@@ -127,6 +131,95 @@ class SignatureManager implements ISignatureManager {
}
/**
+ * generating the expected signature (clear version) sent by the remote instance
+ * based on the data available in the Signature header.
+ *
+ * @param IIncomingSignedRequest $signedRequest
+ * @param array $extraSignatureHeaders
+ *
+ * @throws SignatureException
+ */
+ private function generateExpectedClearSignatureFromRequest(
+ IIncomingSignedRequest $signedRequest,
+ array $extraSignatureHeaders = [],
+ ): void {
+ $request = $signedRequest->getRequest();
+ $usedHeaders = explode(' ', $signedRequest->getSignatureElement('headers'));
+ $neededHeaders = array_merge(['date', 'host', 'content-length', 'digest'], array_keys($extraSignatureHeaders));
+
+ $missingHeaders = array_diff($neededHeaders, $usedHeaders);
+ if ($missingHeaders !== []) {
+ throw new SignatureException('missing entries in Signature.headers: ' . json_encode($missingHeaders));
+ }
+
+ $estimated = ['(request-target): ' . strtolower($request->getMethod()) . ' ' . $request->getRequestUri()];
+ foreach ($usedHeaders as $key) {
+ if ($key === '(request-target)') {
+ continue;
+ }
+ $value = (strtolower($key) === 'host') ? $request->getServerHost() : $request->getHeader($key);
+ if ($value === '') {
+ throw new SignatureException('missing header ' . $key . ' in request');
+ }
+
+ $estimated[] = $key . ': ' . $value;
+ }
+
+ $signedRequest->setClearSignature(implode("\n", $estimated));
+ }
+
+ /**
+ * confirm that the Signature is signed using the correct private key, using
+ * clear version of the Signature and the public key linked to the keyId
+ *
+ * @param IIncomingSignedRequest $signedRequest
+ * @param ISignatoryManager $signatoryManager
+ *
+ * @throws SignatoryNotFoundException
+ * @throws SignatureException
+ */
+ private function confirmIncomingRequestSignature(
+ IIncomingSignedRequest $signedRequest,
+ ISignatoryManager $signatoryManager,
+ int $ttlSignatory,
+ ): void {
+ $knownSignatory = null;
+ try {
+ $knownSignatory = $this->getStoredSignatory($signedRequest->getKeyId());
+ // refreshing ttl and compare with previous public key
+ if ($ttlSignatory > 0 && $knownSignatory->getLastUpdated() < (time() - $ttlSignatory)) {
+ $signatory = $this->getSaneRemoteSignatory($signatoryManager, $signedRequest);
+ $this->updateSignatoryMetadata($signatory);
+ $knownSignatory->setMetadata($signatory->getMetadata());
+ }
+
+ $signedRequest->setSignatory($knownSignatory);
+ $this->verifySignedRequest($signedRequest);
+ } catch (InvalidKeyOriginException $e) {
+ throw $e; // issue while requesting remote instance also means there is no 2nd try
+ } catch (SignatoryNotFoundException) {
+ // if no signatory in cache, we retrieve the one from the remote instance (using
+ // $signatoryManager), check its validity with current signature and store it
+ $signatory = $this->getSaneRemoteSignatory($signatoryManager, $signedRequest);
+ $signedRequest->setSignatory($signatory);
+ $this->verifySignedRequest($signedRequest);
+ $this->storeSignatory($signatory);
+ } catch (SignatureException) {
+ // if public key (from cache) is not valid, we try to refresh it (based on SignatoryType)
+ try {
+ $signatory = $this->getSaneRemoteSignatory($signatoryManager, $signedRequest);
+ } catch (SignatoryNotFoundException $e) {
+ $this->manageDeprecatedSignatory($knownSignatory);
+ throw $e;
+ }
+
+ $signedRequest->setSignatory($signatory);
+ $this->verifySignedRequest($signedRequest);
+ $this->storeSignatory($signatory);
+ }
+ }
+
+ /**
* @inheritDoc
*
* @param ISignatoryManager $signatoryManager
@@ -135,6 +228,9 @@ class SignatureManager implements ISignatureManager {
* @param string $uri needed in the signature
*
* @return IOutgoingSignedRequest
+ * @throws IdentityNotFoundException
+ * @throws SignatoryException
+ * @throws SignatoryNotFoundException
* @since 31.0.0
*/
public function getOutgoingSignedRequest(
@@ -143,27 +239,44 @@ class SignatureManager implements ISignatureManager {
string $method,
string $uri,
): IOutgoingSignedRequest {
- $signedRequest = new OutgoingSignedRequest($content);
- $options = $signatoryManager->getOptions();
-
- $signedRequest->setHost($this->getHostFromUri($uri))
- ->setAlgorithm($options['algorithm'] ?? 'sha256')
- ->setSignatory($signatoryManager->getLocalSignatory());
-
- $this->setOutgoingSignatureHeader(
- $signedRequest,
- strtolower($method),
- parse_url($uri, PHP_URL_PATH) ?? '/',
- $options['dateHeader'] ?? self::DATE_HEADER
+ $signedRequest = new OutgoingSignedRequest(
+ $content,
+ $signatoryManager,
+ $this->extractIdentityFromUri($uri),
+ $method,
+ parse_url($uri, PHP_URL_PATH) ?? '/'
);
- $this->setOutgoingClearSignature($signedRequest);
- $this->setOutgoingSignedSignature($signedRequest);
- $this->signingOutgoingRequest($signedRequest);
+
+ $this->signOutgoingRequest($signedRequest);
return $signedRequest;
}
/**
+ * signing clear version of the Signature header
+ *
+ * @param IOutgoingSignedRequest $signedRequest
+ *
+ * @throws SignatoryException
+ * @throws SignatoryNotFoundException
+ */
+ private function signOutgoingRequest(IOutgoingSignedRequest $signedRequest): void {
+ $clear = $signedRequest->getClearSignature();
+ $signed = $this->signString($clear, $signedRequest->getSignatory()->getPrivateKey(), $signedRequest->getAlgorithm());
+
+ $signatory = $signedRequest->getSignatory();
+ $signatureElements = [
+ 'keyId="' . $signatory->getKeyId() . '"',
+ 'algorithm="' . $signedRequest->getAlgorithm()->value . '"',
+ 'headers="' . implode(' ', $signedRequest->getHeaderList()) . '"',
+ 'signature="' . $signed . '"'
+ ];
+
+ $signedRequest->setSignedSignature($signed);
+ $signedRequest->addHeader('Signature', implode(',', $signatureElements));
+ }
+
+ /**
* @inheritDoc
*
* @param ISignatoryManager $signatoryManager
@@ -267,292 +380,36 @@ class SignatureManager implements ISignatureManager {
}
/**
- * using the requested 'date' entry from header to confirm request is not older than ttl
- *
- * @param IIncomingSignedRequest $signedRequest
- * @param int $ttl
- *
- * @throws IncomingRequestException
- * @throws SignatureNotFoundException
- */
- private function verifyIncomingRequestTime(IIncomingSignedRequest $signedRequest, int $ttl): void {
- $request = $signedRequest->getRequest();
- $date = $request->getHeader('date');
- if ($date === '') {
- throw new SignatureNotFoundException('missing date in header');
- }
-
- try {
- $dTime = new \DateTime($date);
- $signedRequest->setTime($dTime->getTimestamp());
- } catch (\Exception $e) {
- $this->logger->warning(
- 'datetime exception', ['exception' => $e, 'header' => $request->getHeader('date')]
- );
- throw new IncomingRequestException('datetime exception');
- }
-
- if ($signedRequest->getTime() < (time() - $ttl)) {
- throw new IncomingRequestException('object is too old');
- }
- }
-
-
- /**
- * confirm the values of 'content-length' and 'digest' from header
- * is related to request content
- *
- * @param IIncomingSignedRequest $signedRequest
- *
- * @throws IncomingRequestException
- * @throws SignatureNotFoundException
- */
- private function verifyIncomingRequestContent(IIncomingSignedRequest $signedRequest): void {
- $request = $signedRequest->getRequest();
- $contentLength = $request->getHeader('content-length');
- if ($contentLength === '') {
- throw new SignatureNotFoundException('missing content-length in header');
- }
-
- if (strlen($signedRequest->getBody()) !== (int)$request->getHeader('content-length')) {
- throw new IncomingRequestException(
- 'inexact content-length in header: ' . strlen($signedRequest->getBody()) . ' vs '
- . (int)$request->getHeader('content-length')
- );
- }
-
- $digest = $request->getHeader('digest');
- if ($digest === '') {
- throw new SignatureNotFoundException('missing digest in header');
- }
-
- if ($digest !== $signedRequest->getDigest()) {
- throw new IncomingRequestException('invalid value for digest in header');
- }
- }
-
- /**
- * preparing a clear version of the signature based on list of metadata from the
- * Signature entry in header
- *
- * @param IIncomingSignedRequest $signedRequest
- *
- * @throws SignatureNotFoundException
- */
- private function prepIncomingSignatureHeader(IIncomingSignedRequest $signedRequest): void {
- $sign = [];
- $request = $signedRequest->getRequest();
- $signature = $request->getHeader('Signature');
- if ($signature === '') {
- throw new SignatureNotFoundException('missing Signature in header');
- }
-
- foreach (explode(',', $signature) as $entry) {
- if ($entry === '' || !strpos($entry, '=')) {
- continue;
- }
-
- [$k, $v] = explode('=', $entry, 2);
- preg_match('/"([^"]+)"/', $v, $var);
- if ($var[0] !== '') {
- $v = trim($var[0], '"');
- }
- $sign[$k] = $v;
- }
-
- $signedRequest->setSignatureHeader($sign);
- }
-
-
- /**
- * @param IIncomingSignedRequest $signedRequest
- *
- * @throws IncomingRequestException
- * @throws InvalidKeyOriginException
- */
- private function verifyIncomingSignatureHeader(IIncomingSignedRequest $signedRequest): void {
- $data = $signedRequest->getSignatureHeader();
- if (!array_key_exists('keyId', $data) || !array_key_exists('headers', $data)
- || !array_key_exists('signature', $data)) {
- throw new IncomingRequestException('missing keys in signature headers: ' . json_encode($data));
- }
-
- try {
- $signedRequest->setOrigin($this->getHostFromUri($data['keyId']));
- } catch (\Exception) {
- throw new InvalidKeyOriginException('cannot retrieve origin from ' . $data['keyId']);
- }
-
- $signedRequest->setSignedSignature($data['signature']);
- }
-
-
- /**
- * @param IIncomingSignedRequest $signedRequest
- * @param array $extraSignatureHeaders
- *
- * @throws IncomingRequestException
- */
- private function prepEstimatedSignature(
- IIncomingSignedRequest $signedRequest,
- array $extraSignatureHeaders = [],
- ): void {
- $request = $signedRequest->getRequest();
- $headers = explode(' ', $signedRequest->getSignatureHeader()['headers'] ?? []);
-
- $enforceHeaders = array_merge(
- ['date', 'host', 'content-length', 'digest'],
- $extraSignatureHeaders
- );
-
- $missingHeaders = array_diff($enforceHeaders, $headers);
- if ($missingHeaders !== []) {
- throw new IncomingRequestException(
- 'missing elements in headers: ' . json_encode($missingHeaders)
- );
- }
-
- $target = strtolower($request->getMethod()) . ' ' . $request->getRequestUri();
- $estimated = ['(request-target): ' . $target];
-
- foreach ($headers as $key) {
- $value = $request->getHeader($key);
- if (strtolower($key) === 'host') {
- $value = $request->getServerHost();
- }
- if ($value === '') {
- throw new IncomingRequestException('empty elements in header ' . $key);
- }
-
- $estimated[] = $key . ': ' . $value;
- }
-
- $signedRequest->setEstimatedSignature(implode("\n", $estimated));
- }
-
-
- /**
- * @param IIncomingSignedRequest $signedRequest
- * @param ISignatoryManager $signatoryManager
+ * get remote signatory using the ISignatoryManager
+ * and confirm the validity of the keyId
*
- * @throws SignatoryNotFoundException
- * @throws SignatureException
- */
- private function verifyIncomingRequestSignature(
- IIncomingSignedRequest $signedRequest,
- ISignatoryManager $signatoryManager,
- int $ttlSignatory,
- ): void {
- $knownSignatory = null;
- try {
- $knownSignatory = $this->getStoredSignatory($signedRequest->getKeyId());
- if ($ttlSignatory > 0 && $knownSignatory->getLastUpdated() < (time() - $ttlSignatory)) {
- $signatory = $this->getSafeRemoteSignatory($signatoryManager, $signedRequest);
- $this->updateSignatoryMetadata($signatory);
- $knownSignatory->setMetadata($signatory->getMetadata());
- }
-
- $signedRequest->setSignatory($knownSignatory);
- $this->verifySignedRequest($signedRequest);
- } catch (InvalidKeyOriginException $e) {
- throw $e; // issue while requesting remote instance also means there is no 2nd try
- } catch (SignatoryNotFoundException|SignatureException) {
- try {
- $signatory = $this->getSafeRemoteSignatory($signatoryManager, $signedRequest);
- } catch (SignatoryNotFoundException $e) {
- $this->manageDeprecatedSignatory($knownSignatory);
- throw $e;
- }
-
- $signedRequest->setSignatory($signatory);
- $this->storeSignatory($signatory);
- $this->verifySignedRequest($signedRequest);
- }
- }
-
-
- /**
* @param ISignatoryManager $signatoryManager
* @param IIncomingSignedRequest $signedRequest
*
* @return ISignatory
* @throws InvalidKeyOriginException
* @throws SignatoryNotFoundException
+ * @see ISignatoryManager::getRemoteSignatory
*/
- private function getSafeRemoteSignatory(
+ private function getSaneRemoteSignatory(
ISignatoryManager $signatoryManager,
IIncomingSignedRequest $signedRequest,
): ISignatory {
- $signatory = $signatoryManager->getRemoteSignatory($signedRequest);
+ $signatory = $signatoryManager->getRemoteSignatory($signedRequest->getOrigin());
if ($signatory === null) {
throw new SignatoryNotFoundException('empty result from getRemoteSignatory');
}
- if ($signatory->getKeyId() !== $signedRequest->getKeyId()) {
- throw new InvalidKeyOriginException('keyId from signatory not related to the one from request');
- }
-
- return $signatory->setProviderId($signatoryManager->getProviderId());
- }
-
- private function setOutgoingSignatureHeader(
- IOutgoingSignedRequest $signedRequest,
- string $method,
- string $path,
- string $dateHeader,
- ): void {
- $header = [
- '(request-target)' => $method . ' ' . $path,
- 'content-length' => strlen($signedRequest->getBody()),
- 'date' => gmdate($dateHeader),
- 'digest' => $signedRequest->getDigest(),
- 'host' => $signedRequest->getHost()
- ];
-
- $signedRequest->setSignatureHeader($header);
- }
-
-
- /**
- * @param IOutgoingSignedRequest $signedRequest
- */
- private function setOutgoingClearSignature(IOutgoingSignedRequest $signedRequest): void {
- $signing = [];
- $header = $signedRequest->getSignatureHeader();
- foreach (array_keys($header) as $element) {
- $value = $header[$element];
- $signing[] = $element . ': ' . $value;
- if ($element !== '(request-target)') {
- $signedRequest->addHeader($element, $value);
+ try {
+ if ($signatory->getKeyId() !== $signedRequest->getKeyId()) {
+ throw new InvalidKeyOriginException('keyId from signatory not related to the one from request');
}
+ } catch (SignatureElementNotFoundException) {
+ throw new InvalidKeyOriginException('missing keyId');
}
- $signedRequest->setClearSignature(implode("\n", $signing));
- }
-
-
- private function setOutgoingSignedSignature(IOutgoingSignedRequest $signedRequest): void {
- $clear = $signedRequest->getClearSignature();
- $signed = $this->signString(
- $clear, $signedRequest->getSignatory()->getPrivateKey(), $signedRequest->getAlgorithm()
- );
- $signedRequest->setSignedSignature($signed);
- }
-
- private function signingOutgoingRequest(IOutgoingSignedRequest $signedRequest): void {
- $signatureHeader = $signedRequest->getSignatureHeader();
- $headers = array_diff(array_keys($signatureHeader), ['(request-target)']);
- $signatory = $signedRequest->getSignatory();
- $signatureElements = [
- 'keyId="' . $signatory->getKeyId() . '"',
- 'algorithm="' . $this->getChosenEncryption($signedRequest->getAlgorithm()) . '"',
- 'headers="' . implode(' ', $headers) . '"',
- 'signature="' . $signedRequest->getSignedSignature() . '"'
- ];
-
- $signedRequest->addHeader('Signature', implode(',', $signatureElements));
+ return $signatory->setProviderId($signatoryManager->getProviderId());
}
-
/**
* @param IIncomingSignedRequest $signedRequest
*
@@ -568,10 +425,10 @@ class SignatureManager implements ISignatureManager {
try {
$this->verifyString(
- $signedRequest->getEstimatedSignature(),
+ $signedRequest->getClearSignature(),
$signedRequest->getSignedSignature(),
$publicKey,
- $this->getUsedEncryption($signedRequest)
+ SignatureAlgorithm::tryFrom($signedRequest->getSignatureElement('algorithm')) ?? SignatureAlgorithm::SHA256
);
} catch (InvalidSignatureException $e) {
$this->logger->debug('signature issue', ['signed' => $signedRequest, 'exception' => $e]);
@@ -579,45 +436,20 @@ class SignatureManager implements ISignatureManager {
}
}
-
- private function getUsedEncryption(IIncomingSignedRequest $signedRequest): SignatureAlgorithm {
- $data = $signedRequest->getSignatureHeader();
-
- return match ($data['algorithm']) {
- 'rsa-sha512' => SignatureAlgorithm::SHA512,
- default => SignatureAlgorithm::SHA256,
- };
- }
-
- private function getChosenEncryption(string $algorithm): string {
- return match ($algorithm) {
- 'sha512' => 'ras-sha512',
- default => 'ras-sha256',
- };
- }
-
- public function getOpenSSLAlgo(string $algorithm): int {
- return match ($algorithm) {
- 'sha512' => OPENSSL_ALGO_SHA512,
- default => OPENSSL_ALGO_SHA256,
- };
- }
-
-
/**
* @param string $clear
* @param string $privateKey
- * @param string $algorithm
+ * @param SignatureAlgorithm $algorithm
*
* @return string
* @throws SignatoryException
*/
- private function signString(string $clear, string $privateKey, string $algorithm): string {
+ private function signString(string $clear, string $privateKey, SignatureAlgorithm $algorithm): string {
if ($privateKey === '') {
throw new SignatoryException('empty private key');
}
- openssl_sign($clear, $signed, $privateKey, $this->getOpenSSLAlgo($algorithm));
+ openssl_sign($clear, $signed, $privateKey, $algorithm->value);
return base64_encode($signed);
}
@@ -626,19 +458,18 @@ class SignatureManager implements ISignatureManager {
* @param string $clear
* @param string $encoded
* @param string $publicKey
- * @param SignatureAlgorithm $algo
+ * @param SignatureAlgorithm $algorithm
*
- * @return void
* @throws InvalidSignatureException
*/
private function verifyString(
string $clear,
string $encoded,
string $publicKey,
- SignatureAlgorithm $algo = SignatureAlgorithm::SHA256,
+ SignatureAlgorithm $algorithm = SignatureAlgorithm::SHA256,
): void {
$signed = base64_decode($encoded);
- if (openssl_verify($clear, $signed, $publicKey, $algo->value) !== 1) {
+ if (openssl_verify($clear, $signed, $publicKey, $algorithm->value) !== 1) {
throw new InvalidSignatureException('signature issue');
}
}
@@ -692,11 +523,15 @@ class SignatureManager implements ISignatureManager {
}
}
+ /**
+ * @param ISignatory $signatory
+ * @throws DBException
+ */
private function insertSignatory(ISignatory $signatory): void {
$qb = $this->connection->getQueryBuilder();
$qb->insert(self::TABLE_SIGNATORIES)
->setValue('provider_id', $qb->createNamedParameter($signatory->getProviderId()))
- ->setValue('host', $qb->createNamedParameter($this->getHostFromUri($signatory->getKeyId())))
+ ->setValue('host', $qb->createNamedParameter($this->extractIdentityFromUri($signatory->getKeyId())))
->setValue('account', $qb->createNamedParameter($signatory->getAccount()))
->setValue('key_id', $qb->createNamedParameter($signatory->getKeyId()))
->setValue('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId())))
@@ -755,12 +590,12 @@ class SignatureManager implements ISignatureManager {
case SignatoryType::REFRESHABLE:
// TODO: send notice to admin
- throw new SignatoryConflictException();
+ throw new SignatoryConflictException(); // while it can be refreshed, it must exist
case SignatoryType::TRUSTED:
case SignatoryType::STATIC:
// TODO: send warning to admin
- throw new SignatoryConflictException();
+ throw new SignatoryConflictException(); // no way.
}
}
@@ -796,27 +631,6 @@ class SignatureManager implements ISignatureManager {
$qb->executeStatement();
}
-
- /**
- * @param string $uri
- *
- * @return string
- * @throws InvalidKeyOriginException
- */
- private function getHostFromUri(string $uri): string {
- $host = parse_url($uri, PHP_URL_HOST);
- $port = parse_url($uri, PHP_URL_PORT);
- if ($port !== null && $port !== false) {
- $host .= ':' . $port;
- }
-
- if (is_string($host) && $host !== '') {
- return $host;
- }
-
- throw new \Exception('invalid/empty uri');
- }
-
private function hashKeyId(string $keyId): string {
return hash('sha256', $keyId);
}