fix(ocm): simpler code

Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
pull/45979/head
Maxence Lange 5 months ago
parent f08d053290
commit 862a411118
  1. 20
      apps/cloud_federation_api/lib/Controller/RequestHandlerController.php
  2. 21
      lib/composer/composer/autoload_classmap.php
  3. 21
      lib/composer/composer/autoload_static.php
  4. 6
      lib/private/Federation/CloudFederationProviderManager.php
  5. 20
      lib/private/OCM/OCMSignatoryManager.php
  6. 179
      lib/private/Security/Signature/Model/IncomingSignedRequest.php
  7. 78
      lib/private/Security/Signature/Model/OutgoingSignedRequest.php
  8. 62
      lib/private/Security/Signature/Model/SignedRequest.php
  9. 530
      lib/private/Security/Signature/SignatureManager.php
  10. 1
      lib/private/Server.php
  11. 2
      lib/unstable/Security/Signature/Exceptions/SignatureElementNotFoundException.php
  12. 6
      lib/unstable/Security/Signature/ISignatoryManager.php
  13. 2
      lib/unstable/Security/Signature/ISignatureManager.php
  14. 49
      lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php
  15. 25
      lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php
  16. 37
      lib/unstable/Security/Signature/Model/ISignedRequest.php
  17. 2
      lib/unstable/Security/Signature/Model/SignatoryStatus.php
  18. 2
      version.php

@ -336,8 +336,11 @@ class RequestHandlerController extends Controller {
*/
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)
@ -346,7 +349,7 @@ class RequestHandlerController extends Controller {
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;
@ -406,10 +409,17 @@ class RequestHandlerController extends Controller {
$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;
}
}
}

@ -12,6 +12,25 @@ return array(
'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',
@ -1393,6 +1412,8 @@ return array(
'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',

@ -53,6 +53,25 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'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',
@ -1434,6 +1453,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'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',

@ -226,6 +226,12 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
*/
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,

@ -6,12 +6,12 @@ declare(strict_types=1);
* 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;
@ -19,6 +19,7 @@ use OC\Security\Signature\Model\Signatory;
use OCP\IAppConfig;
use OCP\IURLGenerator;
use OCP\OCM\Exceptions\OCMProviderException;
use Psr\Log\LoggerInterface;
/**
* @inheritDoc
@ -40,14 +41,15 @@ class OCMSignatoryManager implements ISignatoryManager {
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;
@ -56,8 +58,8 @@ class OCMSignatoryManager implements ISignatoryManager {
/**
* @inheritDoc
*
* @since 31.0.0
* @return array
* @since 31.0.0
*/
public function getOptions(): array {
return [];
@ -121,14 +123,18 @@ class OCMSignatoryManager implements ISignatoryManager {
/**
* @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;
}
}
/**

@ -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,
]
);
}

@ -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,
]
);
}

@ -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,
];
}

@ -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)
]
);
@ -126,6 +130,95 @@ class SignatureManager implements ISignatureManager {
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
*
@ -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,26 +239,43 @@ 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
*
@ -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);
}

@ -8,7 +8,6 @@ namespace OC;
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;

@ -12,5 +12,5 @@ namespace NCU\Security\Signature\Exceptions;
* @since 31.0.0
* @experimental 31.0.0
*/
class IncomingRequestNotFoundException extends SignatureException {
class SignatureElementNotFoundException extends SignatureException {
}

@ -8,7 +8,6 @@ declare(strict_types=1);
*/
namespace NCU\Security\Signature;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
/**
@ -34,6 +33,7 @@ interface ISignatoryManager {
/**
* options that might affect the way the whole process is handled:
* [
* 'bodyMaxSize' => 10000,
* 'ttl' => 300,
* 'ttlSignatory' => 86400*3,
* 'extraSignatureHeaders' => [],
@ -62,10 +62,10 @@ interface ISignatoryManager {
*
* 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;
}

@ -28,7 +28,7 @@ use NCU\Security\Signature\Model\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

@ -8,6 +8,7 @@ declare(strict_types=1);
*/
namespace NCU\Security\Signature\Model;
use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException;
use NCU\Security\Signature\ISignatureManager;
use OCP\IRequest;
@ -20,15 +21,6 @@ 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
*
@ -37,23 +29,6 @@ interface IIncomingSignedRequest extends ISignedRequest {
*/
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.
@ -78,28 +53,8 @@ interface IIncomingSignedRequest extends ISignedRequest {
* 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;
}

@ -9,6 +9,7 @@ declare(strict_types=1);
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
@ -41,12 +42,12 @@ interface IOutgoingSignedRequest extends ISignedRequest {
* 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
@ -57,38 +58,38 @@ interface IOutgoingSignedRequest extends ISignedRequest {
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;
}

@ -9,6 +9,7 @@ declare(strict_types=1);
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.
@ -39,19 +40,47 @@ interface ISignedRequest {
/**
* 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

@ -12,7 +12,7 @@ namespace NCU\Security\Signature\Model;
* 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

@ -9,7 +9,7 @@
// 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';

Loading…
Cancel
Save