*/
private function getSignedRequest(): ?IIncomingSignedRequest {
try {
- return $this->signatureManager->getIncomingSignedRequest($this->signatoryManager);
+ $signedRequest = $this->signatureManager->getIncomingSignedRequest($this->signatoryManager);
+ $this->logger->debug('signed request available', ['signedRequest' => $signedRequest]);
+ return $signedRequest;
} catch (SignatureNotFoundException|SignatoryNotFoundException $e) {
+ $this->logger->debug('remote does not support signed request', ['exception' => $e]);
// remote does not support signed request.
// currently we still accept unsigned request until lazy appconfig
// core.enforce_signed_ocm_request is set to true (default: false)
throw new IncomingRequestException('Unsigned request');
}
} catch (SignatureException $e) {
- $this->logger->notice('wrongly signed request', ['exception' => $e]);
+ $this->logger->warning('wrongly signed request', ['exception' => $e]);
throw new IncomingRequestException('Invalid signature');
}
return null;
$share = $provider->getShareByToken($token);
try {
$this->confirmShareEntry($signedRequest, $share->getSharedWith());
- } catch (IncomingRequestException) {
+ } catch (IncomingRequestException $e) {
// notification might come from the instance that owns the share
- $this->logger->debug('could not confirm origin on sharedWith (' . $share->getSharedWIth() . '); going with shareOwner (' . $share->getShareOwner() . ')');
- $this->confirmShareEntry($signedRequest, $share->getShareOwner());
+ $this->logger->debug('could not confirm origin on sharedWith (' . $share->getSharedWIth() . '); going with shareOwner (' . $share->getShareOwner() . ')', ['exception' => $e]);
+ try {
+ $this->confirmShareEntry($signedRequest, $share->getShareOwner());
+ } catch (IncomingRequestException $f) {
+ // if both entry are failing, we log first exception as warning and second exception
+ // will be logged as warning by the controller
+ $this->logger->warning('could not confirm origin on sharedWith (' . $share->getSharedWIth() . '); going with shareOwner (' . $share->getShareOwner() . ')', ['exception' => $e]);
+ throw $f;
+ }
}
}
'NCU\\Config\\Exceptions\\UnknownKeyException' => $baseDir . '/lib/unstable/Config/Exceptions/UnknownKeyException.php',
'NCU\\Config\\IUserConfig' => $baseDir . '/lib/unstable/Config/IUserConfig.php',
'NCU\\Config\\ValueType' => $baseDir . '/lib/unstable/Config/ValueType.php',
+ 'NCU\\Security\\Signature\\Exceptions\\IdentityNotFoundException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/IdentityNotFoundException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\IncomingRequestException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/IncomingRequestException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\InvalidKeyOriginException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/InvalidKeyOriginException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\InvalidSignatureException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/InvalidSignatureException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatoryConflictException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/SignatoryConflictException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatoryException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/SignatoryException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatoryNotFoundException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/SignatoryNotFoundException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatureElementNotFoundException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/SignatureElementNotFoundException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatureException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/SignatureException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatureNotFoundException' => $baseDir . '/lib/unstable/Security/Signature/Exceptions/SignatureNotFoundException.php',
+ 'NCU\\Security\\Signature\\ISignatoryManager' => $baseDir . '/lib/unstable/Security/Signature/ISignatoryManager.php',
+ 'NCU\\Security\\Signature\\ISignatureManager' => $baseDir . '/lib/unstable/Security/Signature/ISignatureManager.php',
+ 'NCU\\Security\\Signature\\Model\\IIncomingSignedRequest' => $baseDir . '/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php',
+ 'NCU\\Security\\Signature\\Model\\IOutgoingSignedRequest' => $baseDir . '/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php',
+ 'NCU\\Security\\Signature\\Model\\ISignatory' => $baseDir . '/lib/unstable/Security/Signature/Model/ISignatory.php',
+ 'NCU\\Security\\Signature\\Model\\ISignedRequest' => $baseDir . '/lib/unstable/Security/Signature/Model/ISignedRequest.php',
+ 'NCU\\Security\\Signature\\Model\\SignatoryStatus' => $baseDir . '/lib/unstable/Security/Signature/Model/SignatoryStatus.php',
+ 'NCU\\Security\\Signature\\Model\\SignatoryType' => $baseDir . '/lib/unstable/Security/Signature/Model/SignatoryType.php',
+ 'NCU\\Security\\Signature\\SignatureAlgorithm' => $baseDir . '/lib/unstable/Security/Signature/SignatureAlgorithm.php',
'OCP\\Accounts\\IAccount' => $baseDir . '/lib/public/Accounts/IAccount.php',
'OCP\\Accounts\\IAccountManager' => $baseDir . '/lib/public/Accounts/IAccountManager.php',
'OCP\\Accounts\\IAccountProperty' => $baseDir . '/lib/public/Accounts/IAccountProperty.php',
'OC\\Core\\Migrations\\Version30000Date20240814180800' => $baseDir . '/core/Migrations/Version30000Date20240814180800.php',
'OC\\Core\\Migrations\\Version30000Date20240815080800' => $baseDir . '/core/Migrations/Version30000Date20240815080800.php',
'OC\\Core\\Migrations\\Version30000Date20240906095113' => $baseDir . '/core/Migrations/Version30000Date20240906095113.php',
+ 'OC\\Core\\Migrations\\Version31000Date20240101084401' => $baseDir . '/core/Migrations/Version31000Date20240101084401.php',
+ 'OC\\Core\\Migrations\\Version31000Date20240814184402' => $baseDir . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20241018063111' => $baseDir . '/core/Migrations/Version31000Date20241018063111.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
'NCU\\Config\\Exceptions\\UnknownKeyException' => __DIR__ . '/../../..' . '/lib/unstable/Config/Exceptions/UnknownKeyException.php',
'NCU\\Config\\IUserConfig' => __DIR__ . '/../../..' . '/lib/unstable/Config/IUserConfig.php',
'NCU\\Config\\ValueType' => __DIR__ . '/../../..' . '/lib/unstable/Config/ValueType.php',
+ 'NCU\\Security\\Signature\\Exceptions\\IdentityNotFoundException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/IdentityNotFoundException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\IncomingRequestException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/IncomingRequestException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\InvalidKeyOriginException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/InvalidKeyOriginException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\InvalidSignatureException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/InvalidSignatureException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatoryConflictException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/SignatoryConflictException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatoryException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/SignatoryException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatoryNotFoundException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/SignatoryNotFoundException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatureElementNotFoundException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/SignatureElementNotFoundException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatureException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/SignatureException.php',
+ 'NCU\\Security\\Signature\\Exceptions\\SignatureNotFoundException' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Exceptions/SignatureNotFoundException.php',
+ 'NCU\\Security\\Signature\\ISignatoryManager' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/ISignatoryManager.php',
+ 'NCU\\Security\\Signature\\ISignatureManager' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/ISignatureManager.php',
+ 'NCU\\Security\\Signature\\Model\\IIncomingSignedRequest' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php',
+ 'NCU\\Security\\Signature\\Model\\IOutgoingSignedRequest' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php',
+ 'NCU\\Security\\Signature\\Model\\ISignatory' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Model/ISignatory.php',
+ 'NCU\\Security\\Signature\\Model\\ISignedRequest' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Model/ISignedRequest.php',
+ 'NCU\\Security\\Signature\\Model\\SignatoryStatus' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Model/SignatoryStatus.php',
+ 'NCU\\Security\\Signature\\Model\\SignatoryType' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Model/SignatoryType.php',
+ 'NCU\\Security\\Signature\\SignatureAlgorithm' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/SignatureAlgorithm.php',
'OCP\\Accounts\\IAccount' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccount.php',
'OCP\\Accounts\\IAccountManager' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccountManager.php',
'OCP\\Accounts\\IAccountProperty' => __DIR__ . '/../../..' . '/lib/public/Accounts/IAccountProperty.php',
'OC\\Core\\Migrations\\Version30000Date20240814180800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240814180800.php',
'OC\\Core\\Migrations\\Version30000Date20240815080800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240815080800.php',
'OC\\Core\\Migrations\\Version30000Date20240906095113' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240906095113.php',
+ 'OC\\Core\\Migrations\\Version31000Date20240101084401' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240101084401.php',
+ 'OC\\Core\\Migrations\\Version31000Date20240814184402' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20241018063111' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20241018063111.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
*/
private function prepareOcmPayload(string $uri, string $payload): array {
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => $payload]);
+
+ if ($this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, lazy: true) &&
+ $this->signatoryManager->getRemoteSignatory($this->signatureManager->extractIdentityFromUri($uri)) === null) {
+ return $payload;
+ }
+
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+
namespace OC\OCM;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\ISignatoryManager;
use NCU\Security\Signature\ISignatureManager;
-use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
use NCU\Security\Signature\Model\SignatoryType;
use OC\Security\IdentityProof\Manager;
use OCP\IAppConfig;
use OCP\IURLGenerator;
use OCP\OCM\Exceptions\OCMProviderException;
+use Psr\Log\LoggerInterface;
/**
* @inheritDoc
private readonly IURLGenerator $urlGenerator,
private readonly Manager $identityProofManager,
private readonly OCMDiscoveryService $ocmDiscoveryService,
+ private readonly LoggerInterface $logger,
) {
}
/**
* @inheritDoc
*
- * @since 31.0.0
* @return string
+ * @since 31.0.0
*/
public function getProviderId(): string {
return self::PROVIDER_ID;
/**
* @inheritDoc
*
- * @since 31.0.0
* @return array
+ * @since 31.0.0
*/
public function getOptions(): array {
return [];
/**
* @inheritDoc
*
- * @param IIncomingSignedRequest $signedRequest
+ * @param string $remote
*
* @return ISignatory|null must be NULL if no signatory is found
- * @throws OCMProviderException on fail to discover ocm services
* @since 31.0.0
*/
- public function getRemoteSignatory(IIncomingSignedRequest $signedRequest): ?ISignatory {
- return $this->getRemoteSignatoryFromHost($signedRequest->getOrigin());
+ public function getRemoteSignatory(string $remote): ?ISignatory {
+ try {
+ return $this->getRemoteSignatoryFromHost($remote);
+ } catch (OCMProviderException $e) {
+ $this->logger->warning('fail to get remote signatory', ['exception' => $e, 'remote' => $remote]);
+ return null;
+ }
}
/**
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;
/**
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;
}
/**
* @inheritDoc
*
* @return string
+ * @throws IncomingRequestException
* @since 31.0.0
*/
public function getOrigin(): string {
+ if ($this->origin === '') {
+ throw new IncomingRequestException('empty origin');
+ }
return $this->origin;
}
* 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,
]
);
}
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
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
* @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;
}
}
/**
- * @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;
}
/**
* @inheritDoc
*
- * @return string
+ * @return SignatureAlgorithm
* @since 31.0.0
*/
- public function getAlgorithm(): string {
+ public function getAlgorithm(): SignatureAlgorithm {
return $this->algorithm;
}
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,
]
);
}
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;
*/
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(
/**
* @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;
}
* @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;
}
/**
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,
];
}
<?php
declare(strict_types=1);
-
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
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;
* "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==\""
* }
*
* @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(
?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)
]
);
return $signedRequest;
}
+ /**
+ * 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 string $uri needed in the signature
*
* @return IOutgoingSignedRequest
+ * @throws IdentityNotFoundException
+ * @throws SignatoryException
+ * @throws SignatoryNotFoundException
* @since 31.0.0
*/
public function getOutgoingSignedRequest(
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
*
}
/**
- * 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
*
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]);
}
}
-
- 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);
}
* @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');
}
}
}
}
+ /**
+ * @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())))
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.
}
}
$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);
}
use bantu\IniGetWrapper\IniGetWrapper;
use NCU\Config\IUserConfig;
-use NCU\Security\PublicPrivateKeyPairs\IKeyPairManager;
use NCU\Security\Signature\ISignatureManager;
use OC\Accounts\AccountManager;
use OC\App\AppManager;
+++ /dev/null
-<?php
-
-declare(strict_types=1);
-
-/**
- * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-namespace NCU\Security\Signature\Exceptions;
-
-/**
- * @since 31.0.0
- * @experimental 31.0.0
- */
-class IncomingRequestNotFoundException extends SignatureException {
-}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class SignatureElementNotFoundException extends SignatureException {
+}
*/
namespace NCU\Security\Signature;
-use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
/**
/**
* options that might affect the way the whole process is handled:
* [
+ * 'bodyMaxSize' => 10000,
* 'ttl' => 300,
* 'ttlSignatory' => 86400*3,
* 'extraSignatureHeaders' => [],
*
* Used to confirm authenticity of incoming request.
*
- * @param IIncomingSignedRequest $signedRequest
+ * @param string $remote
*
* @return ISignatory|null must be NULL if no signatory is found
* @since 31.0.0
*/
- public function getRemoteSignatory(IIncomingSignedRequest $signedRequest): ?ISignatory;
+ public function getRemoteSignatory(string $remote): ?ISignatory;
}
* "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 date digest host\",signature=\"DzN12OCS1rsA[...]o0VmxjQooRo6HHabg==\""
+ * "Signature": "keyId=\"https://author.hostname/key\",algorithm=\"sha256\",headers=\"content-length date digest host\",signature=\"DzN12OCS1rsA[...]o0VmxjQooRo6HHabg==\""
* }
*
* 'content-length' is the total length of the data/content
*/
namespace NCU\Security\Signature\Model;
+use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
use NCU\Security\Signature\ISignatureManager;
use OCP\IRequest;
* @since 31.0.0
*/
interface IIncomingSignedRequest extends ISignedRequest {
- /**
- * set the core IRequest that might be signed
- *
- * @param IRequest $request
- * @return IIncomingSignedRequest
- * @since 31.0.0
- */
- public function setRequest(IRequest $request): IIncomingSignedRequest;
-
/**
* returns the base IRequest
*
*/
public function getRequest(): IRequest;
- /**
- * set the time, extracted from the base request headers
- *
- * @param int $time
- * @return IIncomingSignedRequest
- * @since 31.0.0
- */
- public function setTime(int $time): IIncomingSignedRequest;
-
- /**
- * get the time, extracted from the base request headers
- *
- * @return int
- * @since 31.0.0
- */
- public function getTime(): int;
-
/**
* set the hostname at the source of the request,
* based on the keyId defined in the signature header.
* keyId is a mandatory entry in the headers of a signed request.
*
* @return string
+ * @throws SignatureElementNotFoundException
* @since 31.0.0
*/
public function getKeyId(): string;
-
- /**
- * store a clear and estimated version of the signature, based on payload and headers.
- * This clear version will be compared with the real signature using
- * the public key of remote instance at the origin of the request.
- *
- * @param string $signature
- * @return IIncomingSignedRequest
- * @since 31.0.0
- */
- public function setEstimatedSignature(string $signature): IIncomingSignedRequest;
-
- /**
- * returns a clear and estimated version of the signature, based on payload and headers.
- * This clear version will be compared with the real signature using
- * the public key of remote instance at the origin of the request.
- *
- * @return string
- * @since 31.0.0
- */
- public function getEstimatedSignature(): string;
}
namespace NCU\Security\Signature\Model;
use NCU\Security\Signature\ISignatureManager;
+use NCU\Security\Signature\SignatureAlgorithm;
/**
* extends ISignedRequest to add info requested at the generation of the signature
* add a key/value pair to the headers of the request
*
* @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;
/**
* returns list of headers value that will be added to the base request
public function getHeaders(): array;
/**
- * store a clear version of the signature
+ * 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;
+ public function setHeaderList(array $list): IOutgoingSignedRequest;
/**
- * returns the clear version of the signature
+ * returns ordered list of used headers in the Signature
*
- * @return string
+ * @return list<string>
* @since 31.0.0
*/
- public function getClearSignature(): string;
+ public function getHeaderList(): array;
/**
* set algorithm to be used to sign the signature
*
- * @param string $algorithm
+ * @param SignatureAlgorithm $algorithm
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
- public function setAlgorithm(string $algorithm): IOutgoingSignedRequest;
+ public function setAlgorithm(SignatureAlgorithm $algorithm): IOutgoingSignedRequest;
/**
* returns the algorithm set to sign the signature
*
- * @return string
+ * @return SignatureAlgorithm
* @since 31.0.0
*/
- public function getAlgorithm(): string;
+ public function getAlgorithm(): SignatureAlgorithm;
}
namespace NCU\Security\Signature\Model;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
/**
* model that store data related to a possible signature.
/**
* set the list of headers related to the signature of the request
*
- * @param array $signatureHeader
+ * @param array $elements
+ *
* @return ISignedRequest
* @since 31.0.0
*/
- public function setSignatureHeader(array $signatureHeader): ISignedRequest;
+ public function setSignatureElements(array $elements): ISignedRequest;
/**
- * get the list of headers related to the signature of the request
+ * get the list of elements in the Signature header of the request
*
* @return array
* @since 31.0.0
*/
- public function getSignatureHeader(): array;
+ public function getSignatureElements(): array;
+
+ /**
+ * @param string $key
+ *
+ * @return string
+ * @throws SignatureElementNotFoundException
+ * @since 31.0.0
+ */
+ public function getSignatureElement(string $key): string;
+
+ /**
+ * store a clear version of the signature
+ *
+ * @param string $clearSignature
+ *
+ * @return ISignedRequest
+ * @since 31.0.0
+ */
+ public function setClearSignature(string $clearSignature): ISignedRequest;
+
+ /**
+ * returns the clear version of the signature
+ *
+ * @return string
+ * @since 31.0.0
+ */
+ public function getClearSignature(): string;
/**
* set the signed version of the signature
* current status of signatory. is it trustable or not ?
*
* - SYNCED = the remote instance is trustable.
- * - BROKEN = the remote instance does not use the same key pairs
+ * - BROKEN = the remote instance does not use the same key pairs than previously
*
* @experimental 31.0.0
* @since 31.0.0
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level
// when updating major/minor version number.
-$OC_Version = [31, 0, 0, 5];
+$OC_Version = [31, 0, 0, 6];
// The human-readable string
$OC_VersionString = '31.0.0 dev';