From ab72b03144ce08d2429ab4209059762a2e5cbba9 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Thu, 21 Nov 2024 09:25:00 -0100 Subject: [PATCH] fix(ocm): simpler code Signed-off-by: Maxence Lange --- .../Controller/RequestHandlerController.php | 20 +- lib/composer/composer/autoload_classmap.php | 21 + lib/composer/composer/autoload_static.php | 21 + .../CloudFederationProviderManager.php | 6 + lib/private/OCM/OCMSignatoryManager.php | 20 +- .../Signature/Model/IncomingSignedRequest.php | 179 +++--- .../Signature/Model/OutgoingSignedRequest.php | 78 ++- .../Signature/Model/SignedRequest.php | 62 +- .../Security/Signature/SignatureManager.php | 530 ++++++------------ lib/private/Server.php | 1 - ... => SignatureElementNotFoundException.php} | 2 +- .../Security/Signature/ISignatoryManager.php | 6 +- .../Security/Signature/ISignatureManager.php | 2 +- .../Model/IIncomingSignedRequest.php | 49 +- .../Model/IOutgoingSignedRequest.php | 25 +- .../Signature/Model/ISignedRequest.php | 37 +- .../Signature/Model/SignatoryStatus.php | 2 +- version.php | 2 +- 18 files changed, 525 insertions(+), 538 deletions(-) rename lib/unstable/Security/Signature/Exceptions/{IncomingRequestNotFoundException.php => SignatureElementNotFoundException.php} (78%) diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index b1ab1be3f88..e277b9b6389 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.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; + } } } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 744f3b8b8c5..87e617ed4e6 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -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', @@ -1392,6 +1411,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', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 70f9b3b3e63..640924c6579 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.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', @@ -1433,6 +1452,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', diff --git a/lib/private/Federation/CloudFederationProviderManager.php b/lib/private/Federation/CloudFederationProviderManager.php index 74935ead401..e9354294351 100644 --- a/lib/private/Federation/CloudFederationProviderManager.php +++ b/lib/private/Federation/CloudFederationProviderManager.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, diff --git a/lib/private/OCM/OCMSignatoryManager.php b/lib/private/OCM/OCMSignatoryManager.php index a90bb2c1f39..c7eb9ccda5a 100644 --- a/lib/private/OCM/OCMSignatoryManager.php +++ b/lib/private/OCM/OCMSignatoryManager.php @@ -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; + } } /** diff --git a/lib/private/Security/Signature/Model/IncomingSignedRequest.php b/lib/private/Security/Signature/Model/IncomingSignedRequest.php index 8fe83a7b09b..77914d1e3b2 100644 --- a/lib/private/Security/Signature/Model/IncomingSignedRequest.php +++ b/lib/private/Security/Signature/Model/IncomingSignedRequest.php @@ -10,11 +10,14 @@ namespace OC\Security\Signature\Model; use JsonSerializable; use NCU\Security\Signature\Exceptions\IdentityNotFoundException; -use NCU\Security\Signature\Exceptions\IncomingRequestNotFoundException; +use NCU\Security\Signature\Exceptions\IncomingRequestException; use NCU\Security\Signature\Exceptions\SignatoryException; +use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException; +use NCU\Security\Signature\Exceptions\SignatureNotFoundException; use NCU\Security\Signature\ISignatureManager; use NCU\Security\Signature\Model\IIncomingSignedRequest; use NCU\Security\Signature\Model\ISignatory; +use OC\Security\Signature\SignatureManager; use OCP\IRequest; /** @@ -26,77 +29,134 @@ use OCP\IRequest; class IncomingSignedRequest extends SignedRequest implements IIncomingSignedRequest, JsonSerializable { - private ?IRequest $request = null; - private int $time = 0; private string $origin = ''; - private string $estimatedSignature = ''; /** - * @inheritDoc + * @throws IncomingRequestException if incoming request is wrongly signed + * @throws SignatureNotFoundException if signature is not fully implemented + */ + public function __construct( + string $body, + private readonly IRequest $request, + private readonly array $options = [], + ) { + parent::__construct($body); + $this->verifyHeadersFromRequest(); + $this->extractSignatureHeaderFromRequest(); + } + + /** + * confirm that: * - * @param ISignatory $signatory + * - date is available in the header and its value is less than 5 minutes old + * - content-length is available and is the same as the payload size + * - digest is available and fit the checksum of the payload * - * @return $this - * @throws SignatoryException - * @throws IdentityNotFoundException - * @since 31.0.0 + * @throws IncomingRequestException + * @throws SignatureNotFoundException */ - public function setSignatory(ISignatory $signatory): self { - $identity = \OCP\Server::get(ISignatureManager::class)->extractIdentityFromUri($signatory->getKeyId()); - if ($identity !== $this->getOrigin()) { - throw new SignatoryException('keyId from provider is different from the one from signed request'); + private function verifyHeadersFromRequest(): void { + // confirm presence of date, content-length, digest and Signature + $date = $this->getRequest()->getHeader('date'); + if ($date === '') { + throw new SignatureNotFoundException('missing date in header'); + } + $contentLength = $this->getRequest()->getHeader('content-length'); + if ($contentLength === '') { + throw new SignatureNotFoundException('missing content-length in header'); + } + $digest = $this->getRequest()->getHeader('digest'); + if ($digest === '') { + throw new SignatureNotFoundException('missing digest in header'); + } + if ($this->getRequest()->getHeader('Signature') === '') { + throw new SignatureNotFoundException('missing Signature in header'); } - parent::setSignatory($signatory); - return $this; + // confirm date + try { + $dTime = new \DateTime($date); + $requestTime = $dTime->getTimestamp(); + } catch (\Exception) { + throw new IncomingRequestException('datetime exception'); + } + if ($requestTime < (time() - ($this->options['ttl'] ?? SignatureManager::DATE_TTL))) { + throw new IncomingRequestException('object is too old'); + } + + // confirm validity of content-length + if (strlen($this->getBody()) !== (int)$contentLength) { + throw new IncomingRequestException('inexact content-length in header'); + } + + // confirm digest value, based on body + if ($digest !== $this->getDigest()) { + throw new IncomingRequestException('invalid value for digest in header'); + } } /** - * @inheritDoc + * extract data from the header entry 'Signature' and convert its content from string to an array + * also confirm that it contains the minimum mandatory information * - * @param IRequest $request - * @return IIncomingSignedRequest - * @since 31.0.0 + * @throws IncomingRequestException */ - public function setRequest(IRequest $request): IIncomingSignedRequest { - $this->request = $request; - return $this; + private function extractSignatureHeaderFromRequest(): void { + $sign = []; + foreach (explode(',', $this->getRequest()->getHeader('Signature')) as $entry) { + if ($entry === '' || !strpos($entry, '=')) { + continue; + } + + [$k, $v] = explode('=', $entry, 2); + preg_match('/"([^"]+)"/', $v, $var); + if ($var[0] !== '') { + $v = trim($var[0], '"'); + } + $sign[$k] = $v; + } + + $this->setSignatureElements($sign); + + try { + // confirm keys are in the Signature header + $this->getSignatureElement('keyId'); + $this->getSignatureElement('headers'); + $this->setSignedSignature($this->getSignatureElement('signature')); + } catch (SignatureElementNotFoundException $e) { + throw new IncomingRequestException($e->getMessage()); + } } /** * @inheritDoc * * @return IRequest - * @throws IncomingRequestNotFoundException * @since 31.0.0 */ public function getRequest(): IRequest { - if ($this->request === null) { - throw new IncomingRequestNotFoundException(); - } return $this->request; } /** * @inheritDoc * - * @param int $time - * @return IIncomingSignedRequest - * @since 31.0.0 - */ - public function setTime(int $time): IIncomingSignedRequest { - $this->time = $time; - return $this; - } - - /** - * @inheritDoc + * @param ISignatory $signatory * - * @return int + * @return $this + * @throws IdentityNotFoundException + * @throws IncomingRequestException + * @throws SignatoryException * @since 31.0.0 */ - public function getTime(): int { - return $this->time; + public function setSignatory(ISignatory $signatory): self { + $identity = \OCP\Server::get(ISignatureManager::class)->extractIdentityFromUri($signatory->getKeyId()); + if ($identity !== $this->getOrigin()) { + throw new SignatoryException('keyId from provider is different from the one from signed request'); + } + + parent::setSignatory($signatory); + return $this; } /** @@ -115,9 +175,13 @@ class IncomingSignedRequest extends SignedRequest implements * @inheritDoc * * @return string + * @throws IncomingRequestException * @since 31.0.0 */ public function getOrigin(): string { + if ($this->origin === '') { + throw new IncomingRequestException('empty origin'); + } return $this->origin; } @@ -126,44 +190,19 @@ class IncomingSignedRequest extends SignedRequest implements * keyId is a mandatory entry in the headers of a signed request. * * @return string + * @throws SignatureElementNotFoundException * @since 31.0.0 */ public function getKeyId(): string { - return $this->getSignatureHeader()['keyId'] ?? ''; - } - - /** - * @inheritDoc - * - * @param string $signature - * @return IIncomingSignedRequest - * @since 31.0.0 - */ - public function setEstimatedSignature(string $signature): IIncomingSignedRequest { - $this->estimatedSignature = $signature; - return $this; - } - - /** - * @inheritDoc - * - * @return string - * @since 31.0.0 - */ - public function getEstimatedSignature(): string { - return $this->estimatedSignature; + return $this->getSignatureElement('keyId'); } public function jsonSerialize(): array { return array_merge( parent::jsonSerialize(), [ - 'body' => $this->getBody(), - 'time' => $this->getTime(), - 'incomingRequest' => $this->request ?? false, - 'origin' => $this->getOrigin(), - 'keyId' => $this->getKeyId(), - 'estimatedSignature' => $this->getEstimatedSignature(), + 'options' => $this->options, + 'origin' => $this->origin, ] ); } diff --git a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php index 04efcf8bfe1..d2d5b95e7b6 100644 --- a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php +++ b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php @@ -9,8 +9,11 @@ declare(strict_types=1); namespace OC\Security\Signature\Model; use JsonSerializable; +use NCU\Security\Signature\ISignatoryManager; use NCU\Security\Signature\ISignatureManager; use NCU\Security\Signature\Model\IOutgoingSignedRequest; +use NCU\Security\Signature\SignatureAlgorithm; +use OC\Security\Signature\SignatureManager; /** * extends ISignedRequest to add info requested at the generation of the signature @@ -23,8 +26,44 @@ class OutgoingSignedRequest extends SignedRequest implements JsonSerializable { private string $host = ''; private array $headers = []; - private string $clearSignature = ''; - private string $algorithm; + /** @var list $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 $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 * @since 31.0.0 */ - public function getClearSignature(): string { - return $this->clearSignature; + public function getHeaderList(): array { + return $this->headerList; } /** * @inheritDoc * - * @param string $algorithm + * @param SignatureAlgorithm $algorithm * * @return IOutgoingSignedRequest * @since 31.0.0 */ - public function setAlgorithm(string $algorithm): IOutgoingSignedRequest { + public function setAlgorithm(SignatureAlgorithm $algorithm): IOutgoingSignedRequest { $this->algorithm = $algorithm; return $this; } @@ -111,10 +150,10 @@ class OutgoingSignedRequest extends SignedRequest implements /** * @inheritDoc * - * @return string + * @return SignatureAlgorithm * @since 31.0.0 */ - public function getAlgorithm(): string { + public function getAlgorithm(): SignatureAlgorithm { return $this->algorithm; } @@ -122,9 +161,12 @@ class OutgoingSignedRequest extends SignedRequest implements return array_merge( parent::jsonSerialize(), [ + 'host' => $this->host, 'headers' => $this->headers, - 'host' => $this->getHost(), - 'clearSignature' => $this->getClearSignature(), + 'algorithm' => $this->algorithm->value, + 'method' => $this->method, + 'identity' => $this->identity, + 'path' => $this->path, ] ); } diff --git a/lib/private/Security/Signature/Model/SignedRequest.php b/lib/private/Security/Signature/Model/SignedRequest.php index 1587da9d631..56853ebade3 100644 --- a/lib/private/Security/Signature/Model/SignedRequest.php +++ b/lib/private/Security/Signature/Model/SignedRequest.php @@ -10,6 +10,7 @@ namespace OC\Security\Signature\Model; use JsonSerializable; use NCU\Security\Signature\Exceptions\SignatoryNotFoundException; +use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException; use NCU\Security\Signature\Model\ISignatory; use NCU\Security\Signature\Model\ISignedRequest; @@ -20,8 +21,9 @@ use NCU\Security\Signature\Model\ISignedRequest; */ class SignedRequest implements ISignedRequest, JsonSerializable { private string $digest; + private array $signatureElements = []; + private string $clearSignature = ''; private string $signedSignature = ''; - private array $signatureHeader = []; private ?ISignatory $signatory = null; public function __construct( @@ -54,12 +56,13 @@ class SignedRequest implements ISignedRequest, JsonSerializable { /** * @inheritDoc * - * @param array $signatureHeader + * @param array $elements + * * @return ISignedRequest * @since 31.0.0 */ - public function setSignatureHeader(array $signatureHeader): ISignedRequest { - $this->signatureHeader = $signatureHeader; + public function setSignatureElements(array $elements): ISignedRequest { + $this->signatureElements = $elements; return $this; } @@ -69,8 +72,47 @@ class SignedRequest implements ISignedRequest, JsonSerializable { * @return array * @since 31.0.0 */ - public function getSignatureHeader(): array { - return $this->signatureHeader; + public function getSignatureElements(): array { + return $this->signatureElements; + } + + /** + * @param string $key + * + * @return string + * @throws SignatureElementNotFoundException + * @since 31.0.0 + * + */ + public function getSignatureElement(string $key): string { + if (!array_key_exists($key, $this->signatureElements)) { + throw new SignatureElementNotFoundException('missing element ' . $key . ' in Signature header'); + } + + return $this->signatureElements[$key]; + } + + /** + * @inheritDoc + * + * @param string $clearSignature + * + * @return ISignedRequest + * @since 31.0.0 + */ + public function setClearSignature(string $clearSignature): ISignedRequest { + $this->clearSignature = $clearSignature; + return $this; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getClearSignature(): string { + return $this->clearSignature; } /** @@ -134,9 +176,11 @@ class SignedRequest implements ISignedRequest, JsonSerializable { public function jsonSerialize(): array { return [ - 'body' => $this->getBody(), - 'signatureHeader' => $this->getSignatureHeader(), - 'signedSignature' => $this->getSignedSignature(), + 'body' => $this->body, + 'digest' => $this->digest, + 'signatureElements' => $this->signatureElements, + 'clearSignature' => $this->clearSignature, + 'signedSignature' => $this->signedSignature, 'signatory' => $this->signatory ?? false, ]; } diff --git a/lib/private/Security/Signature/SignatureManager.php b/lib/private/Security/Signature/SignatureManager.php index 8717171f4b4..2d895b465ab 100644 --- a/lib/private/Security/Signature/SignatureManager.php +++ b/lib/private/Security/Signature/SignatureManager.php @@ -1,7 +1,6 @@ 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); } diff --git a/lib/private/Server.php b/lib/private/Server.php index 2167bccec89..a20c37732a7 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -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; diff --git a/lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php b/lib/unstable/Security/Signature/Exceptions/SignatureElementNotFoundException.php similarity index 78% rename from lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php rename to lib/unstable/Security/Signature/Exceptions/SignatureElementNotFoundException.php index 1953af39ec5..f40f79410ae 100644 --- a/lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php +++ b/lib/unstable/Security/Signature/Exceptions/SignatureElementNotFoundException.php @@ -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 { } diff --git a/lib/unstable/Security/Signature/ISignatoryManager.php b/lib/unstable/Security/Signature/ISignatoryManager.php index 825ccac1ce9..19ba83a4206 100644 --- a/lib/unstable/Security/Signature/ISignatoryManager.php +++ b/lib/unstable/Security/Signature/ISignatoryManager.php @@ -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; } diff --git a/lib/unstable/Security/Signature/ISignatureManager.php b/lib/unstable/Security/Signature/ISignatureManager.php index cc0297224dc..1969b970aa6 100644 --- a/lib/unstable/Security/Signature/ISignatureManager.php +++ b/lib/unstable/Security/Signature/ISignatureManager.php @@ -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 diff --git a/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php b/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php index a6682eff33c..3e2ebb22a5f 100644 --- a/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php +++ b/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php @@ -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; } diff --git a/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php b/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php index b2ca221e126..3c9445af745 100644 --- a/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php +++ b/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php @@ -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 $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 * @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; } diff --git a/lib/unstable/Security/Signature/Model/ISignedRequest.php b/lib/unstable/Security/Signature/Model/ISignedRequest.php index ebb0e1c5b58..76c033970fe 100644 --- a/lib/unstable/Security/Signature/Model/ISignedRequest.php +++ b/lib/unstable/Security/Signature/Model/ISignedRequest.php @@ -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 diff --git a/lib/unstable/Security/Signature/Model/SignatoryStatus.php b/lib/unstable/Security/Signature/Model/SignatoryStatus.php index 1c28f6580e7..4174102beae 100644 --- a/lib/unstable/Security/Signature/Model/SignatoryStatus.php +++ b/lib/unstable/Security/Signature/Model/SignatoryStatus.php @@ -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 diff --git a/version.php b/version.php index 1b1c154f4a6..e87981f0ec2 100644 --- a/version.php +++ b/version.php @@ -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'; -- 2.39.5