From 62b9957afccc9e3dcb6a11ec5ca3217c42547043 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Tue, 12 Nov 2024 21:07:16 -0100 Subject: [PATCH 1/4] feat(ocm): signing ocm requests Signed-off-by: Maxence Lange --- .../cloud_federation_api/lib/Capabilities.php | 35 +- .../Controller/RequestHandlerController.php | 155 +++- apps/cloud_federation_api/openapi.json | 33 +- apps/files_sharing/lib/External/Storage.php | 1 + .../cleanup-remote-storage.feature | 3 + core/Controller/OCMController.php | 12 +- .../Version31000Date20240101084401.php | 135 +++ lib/composer/composer/autoload_classmap.php | 9 +- lib/composer/composer/autoload_static.php | 9 +- .../CloudFederationProviderManager.php | 73 +- lib/private/Files/Storage/DAV.php | 7 + lib/private/OCM/Model/OCMProvider.php | 45 +- lib/private/OCM/OCMDiscoveryService.php | 34 +- lib/private/OCM/OCMSignatoryManager.php | 149 ++++ .../PublicPrivateKeyPairs/KeyPairManager.php | 182 ++++ .../PublicPrivateKeyPairs/Model/KeyPair.php | 114 +++ .../Signature/Model/IncomingSignedRequest.php | 170 ++++ .../Signature/Model/OutgoingSignedRequest.php | 131 +++ .../Security/Signature/Model/Signatory.php | 147 ++++ .../Signature/Model/SignedRequest.php | 143 +++ .../Security/Signature/SignatureManager.php | 828 ++++++++++++++++++ lib/private/Server.php | 20 +- lib/public/OCM/IOCMProvider.php | 25 +- .../Exceptions/KeyPairConflictException.php | 18 + .../Exceptions/KeyPairException.php | 20 + .../Exceptions/KeyPairNotFoundException.php | 16 + .../PublicPrivateKeyPairs/IKeyPairManager.php | 80 ++ .../PublicPrivateKeyPairs/Model/IKeyPair.php | 85 ++ .../Exceptions/IdentityNotFoundException.php | 16 + .../Exceptions/IncomingRequestException.php | 16 + .../IncomingRequestNotFoundException.php | 16 + .../Exceptions/InvalidKeyOriginException.php | 16 + .../Exceptions/InvalidSignatureException.php | 16 + .../Exceptions/SignatoryConflictException.php | 16 + .../Exceptions/SignatoryException.php | 16 + .../Exceptions/SignatoryNotFoundException.php | 16 + .../Exceptions/SignatureException.php | 18 + .../Exceptions/SignatureNotFoundException.php | 16 + .../Security/Signature/ISignatoryManager.php | 71 ++ .../Security/Signature/ISignatureManager.php | 129 +++ .../Model/IIncomingSignedRequest.php | 105 +++ .../Model/IOutgoingSignedRequest.php | 94 ++ .../Security/Signature/Model/ISignatory.php | 160 ++++ .../Signature/Model/ISignedRequest.php | 98 +++ .../Signature/Model/SignatoryStatus.php | 25 + .../Signature/Model/SignatoryType.php | 31 + .../Security/Signature/SignatureAlgorithm.php | 22 + 47 files changed, 3477 insertions(+), 99 deletions(-) create mode 100644 core/Migrations/Version31000Date20240101084401.php create mode 100644 lib/private/OCM/OCMSignatoryManager.php create mode 100644 lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php create mode 100644 lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php create mode 100644 lib/private/Security/Signature/Model/IncomingSignedRequest.php create mode 100644 lib/private/Security/Signature/Model/OutgoingSignedRequest.php create mode 100644 lib/private/Security/Signature/Model/Signatory.php create mode 100644 lib/private/Security/Signature/Model/SignedRequest.php create mode 100644 lib/private/Security/Signature/SignatureManager.php create mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php create mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairException.php create mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairNotFoundException.php create mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/IKeyPairManager.php create mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/Model/IKeyPair.php create mode 100644 lib/unstable/Security/Signature/Exceptions/IdentityNotFoundException.php create mode 100644 lib/unstable/Security/Signature/Exceptions/IncomingRequestException.php create mode 100644 lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php create mode 100644 lib/unstable/Security/Signature/Exceptions/InvalidKeyOriginException.php create mode 100644 lib/unstable/Security/Signature/Exceptions/InvalidSignatureException.php create mode 100644 lib/unstable/Security/Signature/Exceptions/SignatoryConflictException.php create mode 100644 lib/unstable/Security/Signature/Exceptions/SignatoryException.php create mode 100644 lib/unstable/Security/Signature/Exceptions/SignatoryNotFoundException.php create mode 100644 lib/unstable/Security/Signature/Exceptions/SignatureException.php create mode 100644 lib/unstable/Security/Signature/Exceptions/SignatureNotFoundException.php create mode 100644 lib/unstable/Security/Signature/ISignatoryManager.php create mode 100644 lib/unstable/Security/Signature/ISignatureManager.php create mode 100644 lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php create mode 100644 lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php create mode 100644 lib/unstable/Security/Signature/Model/ISignatory.php create mode 100644 lib/unstable/Security/Signature/Model/ISignedRequest.php create mode 100644 lib/unstable/Security/Signature/Model/SignatoryStatus.php create mode 100644 lib/unstable/Security/Signature/Model/SignatoryType.php create mode 100644 lib/unstable/Security/Signature/SignatureAlgorithm.php diff --git a/apps/cloud_federation_api/lib/Capabilities.php b/apps/cloud_federation_api/lib/Capabilities.php index ca4ea928cb823..1910a03233791 100644 --- a/apps/cloud_federation_api/lib/Capabilities.php +++ b/apps/cloud_federation_api/lib/Capabilities.php @@ -6,20 +6,27 @@ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCA\CloudFederationAPI; +use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairException; +use NCU\Security\Signature\Exceptions\SignatoryException; +use OC\OCM\OCMSignatoryManager; use OCP\Capabilities\ICapability; +use OCP\IAppConfig; use OCP\IURLGenerator; use OCP\OCM\Exceptions\OCMArgumentException; use OCP\OCM\IOCMProvider; +use Psr\Log\LoggerInterface; class Capabilities implements ICapability { - public const API_VERSION = '1.0-proposal1'; + public const API_VERSION = '1.1'; // informative, real version. public function __construct( private IURLGenerator $urlGenerator, + private IAppConfig $appConfig, private IOCMProvider $provider, + private readonly OCMSignatoryManager $ocmSignatoryManager, + private readonly LoggerInterface $logger, ) { } @@ -28,15 +35,20 @@ public function __construct( * * @return array{ * ocm: array{ + * apiVersion: '1.0-proposal1', * enabled: bool, - * apiVersion: string, * endPoint: string, + * publicKey: array{ + * keyId: string, + * publicKeyPem: string, + * }, * resourceTypes: list, * protocols: array - * }>, - * }, + * }>, + * version: string + * } * } * @throws OCMArgumentException */ @@ -60,6 +72,17 @@ public function getCapabilities() { $this->provider->addResourceType($resource); - return ['ocm' => $this->provider->jsonSerialize()]; + // Adding a public key to the ocm discovery + try { + if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { + $this->provider->setSignatory($this->ocmSignatoryManager->getLocalSignatory()); + } else { + $this->logger->debug('ocm public key feature disabled'); + } + } catch (SignatoryException|KeyPairException $e) { + $this->logger->warning('cannot generate local signatory', ['exception' => $e]); + } + + return ['ocm' => json_decode(json_encode($this->provider->jsonSerialize()), true)]; } } diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index a7b17f010cee9..db7f81d559675 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -5,6 +5,13 @@ */ namespace OCA\CloudFederationAPI\Controller; +use NCU\Security\Signature\Exceptions\IncomingRequestException; +use NCU\Security\Signature\Exceptions\SignatoryNotFoundException; +use NCU\Security\Signature\Exceptions\SignatureException; +use NCU\Security\Signature\Exceptions\SignatureNotFoundException; +use NCU\Security\Signature\ISignatureManager; +use NCU\Security\Signature\Model\IIncomingSignedRequest; +use OC\OCM\OCMSignatoryManager; use OCA\CloudFederationAPI\Config; use OCA\CloudFederationAPI\ResponseDefinitions; use OCP\AppFramework\Controller; @@ -22,11 +29,14 @@ use OCP\Federation\ICloudFederationFactory; use OCP\Federation\ICloudFederationProviderManager; use OCP\Federation\ICloudIdManager; +use OCP\IAppConfig; use OCP\IGroupManager; use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IProviderFactory; +use OCP\Share\IShare; use OCP\Util; use Psr\Log\LoggerInterface; @@ -50,8 +60,12 @@ public function __construct( private IURLGenerator $urlGenerator, private ICloudFederationProviderManager $cloudFederationProviderManager, private Config $config, + private readonly IAppConfig $appConfig, private ICloudFederationFactory $factory, private ICloudIdManager $cloudIdManager, + private readonly ISignatureManager $signatureManager, + private readonly OCMSignatoryManager $signatoryManager, + private readonly IProviderFactory $shareProviderFactory, ) { parent::__construct($appName, $request); } @@ -81,11 +95,20 @@ public function __construct( #[NoCSRFRequired] #[BruteForceProtection(action: 'receiveFederatedShare')] public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) { + try { + // if request is signed and well signed, no exception are thrown + // if request is not signed and host is known for not supporting signed request, no exception are thrown + $signedRequest = $this->getSignedRequest(); + $this->confirmSignedOrigin($signedRequest, 'owner', $owner); + } catch (IncomingRequestException $e) { + $this->logger->warning('incoming request exception', ['exception' => $e]); + return new JSONResponse(['message' => $e->getMessage(), 'validationErrors' => []], Http::STATUS_BAD_REQUEST); + } + // check if all required parameters are set if ($shareWith === null || $name === null || $providerId === null || - $owner === null || $resourceType === null || $shareType === null || !is_array($protocol) || @@ -208,6 +231,16 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ #[PublicPage] #[BruteForceProtection(action: 'receiveFederatedShareNotification')] public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification) { + try { + // if request is signed and well signed, no exception are thrown + // if request is not signed and host is known for not supporting signed request, no exception are thrown + $signedRequest = $this->getSignedRequest(); + $this->confirmShareOrigin($signedRequest, $notification['sharedSecret'] ?? ''); + } catch (IncomingRequestException $e) { + $this->logger->warning('incoming request exception', ['exception' => $e]); + return new JSONResponse(['message' => $e->getMessage(), 'validationErrors' => []], Http::STATUS_BAD_REQUEST); + } + // check if all required parameters are set if ($notificationType === null || $resourceType === null || @@ -286,4 +319,124 @@ private function mapUid($uid) { return $uid; } + + + /** + * returns signed request if available. + * throw an exception: + * - if request is signed, but wrongly signed + * - if request is not signed but instance is configured to only accept signed ocm request + * + * @return IIncomingSignedRequest|null null if remote does not (and never did) support signed request + * @throws IncomingRequestException + */ + private function getSignedRequest(): ?IIncomingSignedRequest { + try { + return $this->signatureManager->getIncomingSignedRequest($this->signatoryManager); + } catch (SignatureNotFoundException|SignatoryNotFoundException $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) + if ($this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, lazy: true)) { + $this->logger->notice('ignored unsigned request', ['exception' => $e]); + throw new IncomingRequestException('Unsigned request'); + } + } catch (SignatureException $e) { + $this->logger->notice('wrongly signed request', ['exception' => $e]); + throw new IncomingRequestException('Invalid signature'); + } + return null; + } + + + /** + * confirm that the value related to $key entry from the payload is in format userid@hostname + * and compare hostname with the origin of the signed request. + * + * If request is not signed, we still verify that the hostname from the extracted value does, + * actually, not support signed request + * + * @param IIncomingSignedRequest|null $signedRequest + * @param string $key entry from data available in data + * @param string $value value itself used in case request is not signed + * + * @throws IncomingRequestException + */ + private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, string $key, string $value): void { + if ($signedRequest === null) { + $instance = $this->getHostFromFederationId($value); + try { + $this->signatureManager->searchSignatory($instance); + throw new IncomingRequestException('instance is supposed to sign its request'); + } catch (SignatoryNotFoundException) { + return; + } + } + + $body = json_decode($signedRequest->getBody(), true) ?? []; + $entry = trim($body[$key] ?? '', '@'); + if ($this->getHostFromFederationId($entry) !== $signedRequest->getOrigin()) { + throw new IncomingRequestException('share initiation from different instance'); + } + } + + + /** + * confirm that the value related to share token is in format userid@hostname + * and compare hostname with the origin of the signed request. + * + * If request is not signed, we still verify that the hostname from the extracted value does, + * actually, not support signed request + * + * @param IIncomingSignedRequest|null $signedRequest + * @param string $token + * + * @return void + * @throws IncomingRequestException + */ + private function confirmShareOrigin(?IIncomingSignedRequest $signedRequest, string $token): void { + if ($token === '') { + throw new BadRequestException(['sharedSecret']); + } + + $provider = $this->shareProviderFactory->getProviderForType(IShare::TYPE_REMOTE); + $share = $provider->getShareByToken($token); + $entry = $share->getSharedWith(); + + $instance = $this->getHostFromFederationId($entry); + if ($signedRequest === null) { + try { + $this->signatureManager->searchSignatory($instance); + throw new IncomingRequestException('instance is supposed to sign its request'); + } catch (SignatoryNotFoundException) { + return; + } + } elseif ($instance !== $signedRequest->getOrigin()) { + throw new IncomingRequestException('token sharedWith from different instance'); + } + } + + /** + * @param string $entry + * @return string + * @throws IncomingRequestException + */ + private function getHostFromFederationId(string $entry): string { + if (!str_contains($entry, '@')) { + throw new IncomingRequestException('entry does not contains @'); + } + [, $rightPart] = explode('@', $entry, 2); + + $host = parse_url($rightPart, PHP_URL_HOST); + $port = parse_url($rightPart, PHP_URL_PORT); + if ($port !== null && $port !== false) { + $host .= ':' . $port; + } + + if (is_string($host) && $host !== '') { + return $host; + } + + throw new IncomingRequestException('host is empty'); + } } diff --git a/apps/cloud_federation_api/openapi.json b/apps/cloud_federation_api/openapi.json index d15c7cef8130b..1c69ea2d08359 100644 --- a/apps/cloud_federation_api/openapi.json +++ b/apps/cloud_federation_api/openapi.json @@ -43,21 +43,41 @@ "ocm": { "type": "object", "required": [ - "enabled", "apiVersion", + "enabled", "endPoint", - "resourceTypes" + "publicKey", + "resourceTypes", + "version" ], "properties": { + "apiVersion": { + "type": "string", + "enum": [ + "1.0-proposal1" + ] + }, "enabled": { "type": "boolean" }, - "apiVersion": { - "type": "string" - }, "endPoint": { "type": "string" }, + "publicKey": { + "type": "object", + "required": [ + "keyId", + "publicKeyPem" + ], + "properties": { + "keyId": { + "type": "string" + }, + "publicKeyPem": { + "type": "string" + } + } + }, "resourceTypes": { "type": "array", "items": { @@ -85,6 +105,9 @@ } } } + }, + "version": { + "type": "string" } } } diff --git a/apps/files_sharing/lib/External/Storage.php b/apps/files_sharing/lib/External/Storage.php index dbf7d2af6e5ce..bacd2b3f7cf99 100644 --- a/apps/files_sharing/lib/External/Storage.php +++ b/apps/files_sharing/lib/External/Storage.php @@ -88,6 +88,7 @@ public function __construct($options) { parent::__construct( [ 'secure' => ((parse_url($remote, PHP_URL_SCHEME) ?? 'https') === 'https'), + 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false), 'host' => $host, 'root' => $webDavEndpoint, 'user' => $options['token'], diff --git a/build/integration/federation_features/cleanup-remote-storage.feature b/build/integration/federation_features/cleanup-remote-storage.feature index 6339edb60b66e..a017b59bcf4cf 100644 --- a/build/integration/federation_features/cleanup-remote-storage.feature +++ b/build/integration/federation_features/cleanup-remote-storage.feature @@ -35,6 +35,9 @@ Feature: cleanup-remote-storage # server may have its own /textfile0.txt" file) And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And As an "user1" + And sending "GET" to "/apps/files_sharing/api/v1/shares" + And the list of returned shares has 1 shares And Using server "LOCAL" # Accept and download the file to ensure that a storage is created for the # federated share diff --git a/core/Controller/OCMController.php b/core/Controller/OCMController.php index 59529b66e121a..f15a4a5677996 100644 --- a/core/Controller/OCMController.php +++ b/core/Controller/OCMController.php @@ -17,7 +17,7 @@ use OCP\AppFramework\Http\Attribute\PublicPage; use OCP\AppFramework\Http\DataResponse; use OCP\Capabilities\ICapability; -use OCP\IConfig; +use OCP\IAppConfig; use OCP\IRequest; use OCP\Server; use Psr\Container\ContainerExceptionInterface; @@ -31,7 +31,7 @@ class OCMController extends Controller { public function __construct( IRequest $request, - private IConfig $config, + private readonly IAppConfig $appConfig, private LoggerInterface $logger, ) { parent::__construct('core', $request); @@ -54,10 +54,10 @@ public function __construct( public function discovery(): DataResponse { try { $cap = Server::get( - $this->config->getAppValue( - 'core', - 'ocm_providers', - '\OCA\CloudFederationAPI\Capabilities' + $this->appConfig->getValueString( + 'core', 'ocm_providers', + \OCA\CloudFederationAPI\Capabilities::class, + lazy: true ) ); diff --git a/core/Migrations/Version31000Date20240101084401.php b/core/Migrations/Version31000Date20240101084401.php new file mode 100644 index 0000000000000..60792dcac2139 --- /dev/null +++ b/core/Migrations/Version31000Date20240101084401.php @@ -0,0 +1,135 @@ +hasTable('sec_signatory')) { + $table = $schema->createTable('sec_signatory'); + $table->addColumn('id', Types::BIGINT, [ + 'notnull' => true, + 'length' => 64, + 'autoincrement' => true, + 'unsigned' => true, + ]); + // key_id_sum will store a hash version of the key_id, more appropriate for search/index + $table->addColumn('key_id_sum', Types::STRING, [ + 'notnull' => true, + 'length' => 127, + ]); + $table->addColumn('key_id', Types::STRING, [ + 'notnull' => true, + 'length' => 512 + ]); + // host/provider_id/account will help generate a unique entry, not based on key_id + // this way, a spoofed instance cannot publish a new key_id for same host+provider_id + // account will be used only to stored multiple keys for the same provider_id/host + $table->addColumn('host', Types::STRING, [ + 'notnull' => true, + 'length' => 512 + ]); + $table->addColumn('provider_id', Types::STRING, [ + 'notnull' => true, + 'length' => 31, + ]); + $table->addColumn('account', Types::STRING, [ + 'notnull' => false, + 'length' => 127, + 'default' => '' + ]); + $table->addColumn('public_key', Types::TEXT, [ + 'notnull' => true, + 'default' => '' + ]); + $table->addColumn('metadata', Types::TEXT, [ + 'notnull' => true, + 'default' => '[]' + ]); + // type+status are informative about the trustability of remote instance and status of the signatory + $table->addColumn('type', Types::SMALLINT, [ + 'notnull' => true, + 'length' => 2, + 'default' => 9 + ]); + $table->addColumn('status', Types::SMALLINT, [ + 'notnull' => true, + 'length' => 2, + 'default' => 0, + ]); + $table->addColumn('creation', Types::INTEGER, [ + 'notnull' => false, + 'length' => 4, + 'default' => 0, + 'unsigned' => true, + ]); + $table->addColumn('last_updated', Types::INTEGER, [ + 'notnull' => false, + 'length' => 4, + 'default' => 0, + 'unsigned' => true, + ]); + + $table->setPrimaryKey(['id'], 'sec_sig_id'); + $table->addUniqueIndex(['provider_id', 'host', 'account'], 'sec_sig_unic'); + $table->addIndex(['key_id_sum', 'provider_id'], 'sec_sig_key'); + + return $schema; + } + + return null; + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index cc6b8be326c49..b04f938d2366a 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1392,7 +1392,6 @@ '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\\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', @@ -1732,6 +1731,7 @@ 'OC\\OCM\\Model\\OCMProvider' => $baseDir . '/lib/private/OCM/Model/OCMProvider.php', 'OC\\OCM\\Model\\OCMResource' => $baseDir . '/lib/private/OCM/Model/OCMResource.php', 'OC\\OCM\\OCMDiscoveryService' => $baseDir . '/lib/private/OCM/OCMDiscoveryService.php', + 'OC\\OCM\\OCMSignatoryManager' => $baseDir . '/lib/private/OCM/OCMSignatoryManager.php', 'OC\\OCS\\ApiHelper' => $baseDir . '/lib/private/OCS/ApiHelper.php', 'OC\\OCS\\CoreCapabilities' => $baseDir . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\DiscoveryService' => $baseDir . '/lib/private/OCS/DiscoveryService.php', @@ -1901,6 +1901,8 @@ 'OC\\Security\\Ip\\Range' => $baseDir . '/lib/private/Security/Ip/Range.php', 'OC\\Security\\Ip\\RemoteAddress' => $baseDir . '/lib/private/Security/Ip/RemoteAddress.php', 'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php', + 'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php', + 'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php', 'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php', 'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php', 'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php', @@ -1908,6 +1910,11 @@ 'OC\\Security\\RateLimiting\\Limiter' => $baseDir . '/lib/private/Security/RateLimiting/Limiter.php', 'OC\\Security\\RemoteHostValidator' => $baseDir . '/lib/private/Security/RemoteHostValidator.php', 'OC\\Security\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php', + 'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php', + 'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php', + 'OC\\Security\\Signature\\Model\\Signatory' => $baseDir . '/lib/private/Security/Signature/Model/Signatory.php', + 'OC\\Security\\Signature\\Model\\SignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/SignedRequest.php', + 'OC\\Security\\Signature\\SignatureManager' => $baseDir . '/lib/private/Security/Signature/SignatureManager.php', 'OC\\Security\\TrustedDomainHelper' => $baseDir . '/lib/private/Security/TrustedDomainHelper.php', 'OC\\Security\\VerificationToken\\CleanUpJob' => $baseDir . '/lib/private/Security/VerificationToken/CleanUpJob.php', 'OC\\Security\\VerificationToken\\VerificationToken' => $baseDir . '/lib/private/Security/VerificationToken/VerificationToken.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 54959bc6b9164..dea35ecdfcd11 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1433,7 +1433,6 @@ 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\\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', @@ -1773,6 +1772,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\OCM\\Model\\OCMProvider' => __DIR__ . '/../../..' . '/lib/private/OCM/Model/OCMProvider.php', 'OC\\OCM\\Model\\OCMResource' => __DIR__ . '/../../..' . '/lib/private/OCM/Model/OCMResource.php', 'OC\\OCM\\OCMDiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMDiscoveryService.php', + 'OC\\OCM\\OCMSignatoryManager' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMSignatoryManager.php', 'OC\\OCS\\ApiHelper' => __DIR__ . '/../../..' . '/lib/private/OCS/ApiHelper.php', 'OC\\OCS\\CoreCapabilities' => __DIR__ . '/../../..' . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\DiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCS/DiscoveryService.php', @@ -1942,6 +1942,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Security\\Ip\\Range' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/Range.php', 'OC\\Security\\Ip\\RemoteAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/RemoteAddress.php', 'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php', + 'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php', + 'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php', 'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php', 'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php', 'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php', @@ -1949,6 +1951,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Security\\RateLimiting\\Limiter' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Limiter.php', 'OC\\Security\\RemoteHostValidator' => __DIR__ . '/../../..' . '/lib/private/Security/RemoteHostValidator.php', 'OC\\Security\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php', + 'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php', + 'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php', + 'OC\\Security\\Signature\\Model\\Signatory' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/Signatory.php', + 'OC\\Security\\Signature\\Model\\SignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/SignedRequest.php', + 'OC\\Security\\Signature\\SignatureManager' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/SignatureManager.php', 'OC\\Security\\TrustedDomainHelper' => __DIR__ . '/../../..' . '/lib/private/Security/TrustedDomainHelper.php', 'OC\\Security\\VerificationToken\\CleanUpJob' => __DIR__ . '/../../..' . '/lib/private/Security/VerificationToken/CleanUpJob.php', 'OC\\Security\\VerificationToken\\VerificationToken' => __DIR__ . '/../../..' . '/lib/private/Security/VerificationToken/VerificationToken.php', diff --git a/lib/private/Federation/CloudFederationProviderManager.php b/lib/private/Federation/CloudFederationProviderManager.php index bf7648d472b4f..eeb161c3b2514 100644 --- a/lib/private/Federation/CloudFederationProviderManager.php +++ b/lib/private/Federation/CloudFederationProviderManager.php @@ -8,7 +8,9 @@ */ namespace OC\Federation; +use NCU\Security\Signature\ISignatureManager; use OC\AppFramework\Http; +use OC\OCM\OCMSignatoryManager; use OCP\App\IAppManager; use OCP\Federation\Exceptions\ProviderDoesNotExistsException; use OCP\Federation\ICloudFederationNotification; @@ -18,6 +20,7 @@ use OCP\Federation\ICloudIdManager; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; +use OCP\IAppConfig; use OCP\IConfig; use OCP\OCM\Exceptions\OCMProviderException; use OCP\OCM\IOCMDiscoveryService; @@ -37,9 +40,12 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager public function __construct( private IConfig $config, private IAppManager $appManager, + private IAppConfig $appConfig, private IClientService $httpClientService, private ICloudIdManager $cloudIdManager, private IOCMDiscoveryService $discoveryService, + private readonly ISignatureManager $signatureManager, + private readonly OCMSignatoryManager $signatoryManager, private LoggerInterface $logger, ) { } @@ -106,9 +112,17 @@ public function sendShare(ICloudFederationShare $share) { $client = $this->httpClientService->newClient(); try { - $response = $client->post($ocmProvider->getEndPoint() . '/shares', array_merge($this->getDefaultRequestOptions(), [ - 'body' => json_encode($share->getShare()), - ])); + // signing the payload using OCMSignatoryManager before initializing the request + $uri = $ocmProvider->getEndPoint() . '/shares'; + $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]); + if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { + $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( + $this->signatoryManager, + $payload, + 'post', $uri + ); + } + $response = $client->post($uri, $signedPayload ?? $payload); if ($response->getStatusCode() === Http::STATUS_CREATED) { $result = json_decode($response->getBody(), true); @@ -139,9 +153,18 @@ public function sendCloudShare(ICloudFederationShare $share): IResponse { $client = $this->httpClientService->newClient(); try { - return $client->post($ocmProvider->getEndPoint() . '/shares', array_merge($this->getDefaultRequestOptions(), [ - 'body' => json_encode($share->getShare()), - ])); + // signing the payload using OCMSignatoryManager before initializing the request + $uri = $ocmProvider->getEndPoint() . '/shares'; + $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]); + if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { + $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( + $this->signatoryManager, + $payload, + 'post', $uri + ); + } + + return $client->post($uri, $signedPayload ?? $payload); } catch (\Throwable $e) { $this->logger->error('Error while sending share to federation server: ' . $e->getMessage(), ['exception' => $e]); try { @@ -167,9 +190,19 @@ public function sendNotification($url, ICloudFederationNotification $notificatio $client = $this->httpClientService->newClient(); try { - $response = $client->post($ocmProvider->getEndPoint() . '/notifications', array_merge($this->getDefaultRequestOptions(), [ - 'body' => json_encode($notification->getMessage()), - ])); + + // signing the payload using OCMSignatoryManager before initializing the request + $uri = $ocmProvider->getEndPoint() . '/notifications'; + $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]); + if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { + $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( + $this->signatoryManager, + $payload, + 'post', $uri + ); + } + $response = $client->post($uri, $signedPayload ?? $payload); + if ($response->getStatusCode() === Http::STATUS_CREATED) { $result = json_decode($response->getBody(), true); return (is_array($result)) ? $result : []; @@ -193,9 +226,17 @@ public function sendCloudNotification(string $url, ICloudFederationNotification $client = $this->httpClientService->newClient(); try { - return $client->post($ocmProvider->getEndPoint() . '/notifications', array_merge($this->getDefaultRequestOptions(), [ - 'body' => json_encode($notification->getMessage()), - ])); + // signing the payload using OCMSignatoryManager before initializing the request + $uri = $ocmProvider->getEndPoint() . '/notifications'; + $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]); + if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { + $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( + $this->signatoryManager, + $payload, + 'post', $uri + ); + } + return $client->post($uri, $signedPayload ?? $payload); } catch (\Throwable $e) { $this->logger->error('Error while sending notification to federation server: ' . $e->getMessage(), ['exception' => $e]); try { @@ -216,15 +257,11 @@ public function isReady() { } private function getDefaultRequestOptions(): array { - $options = [ + return [ 'headers' => ['content-type' => 'application/json'], 'timeout' => 10, 'connect_timeout' => 10, + 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false), ]; - - if ($this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates')) { - $options['verify'] = false; - } - return $options; } } diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php index 10670d6331a0e..597b3f4748888 100644 --- a/lib/private/Files/Storage/DAV.php +++ b/lib/private/Files/Storage/DAV.php @@ -64,6 +64,7 @@ class DAV extends Common { protected $httpClientService; /** @var ICertificateManager */ protected $certManager; + protected bool $verify = true; protected LoggerInterface $logger; protected IEventLogger $eventLogger; protected IMimeTypeDetector $mimeTypeDetector; @@ -103,6 +104,7 @@ public function __construct(array $parameters) { if (isset($parameters['authType'])) { $this->authType = $parameters['authType']; } + $this->verify = (($parameters['verify'] ?? true) !== false); if (isset($parameters['secure'])) { if (is_string($parameters['secure'])) { $this->secure = ($parameters['secure'] === 'true'); @@ -162,6 +164,11 @@ protected function init(): void { } } + if (!$this->verify) { + $this->client->addCurlSetting(CURLOPT_SSL_VERIFYHOST, 0); + $this->client->addCurlSetting(CURLOPT_SSL_VERIFYPEER, false); + } + $lastRequestStart = 0; $this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) { $this->logger->debug('sending dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl(), ['app' => 'dav']); diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php index 73002ae668de1..cd4e9c49c3b29 100644 --- a/lib/private/OCM/Model/OCMProvider.php +++ b/lib/private/OCM/Model/OCMProvider.php @@ -9,6 +9,8 @@ namespace OC\OCM\Model; +use NCU\Security\Signature\Model\ISignatory; +use OC\Security\Signature\Model\Signatory; use OCP\EventDispatcher\IEventDispatcher; use OCP\OCM\Events\ResourceTypeRegisterEvent; use OCP\OCM\Exceptions\OCMArgumentException; @@ -25,7 +27,7 @@ class OCMProvider implements IOCMProvider { private string $endPoint = ''; /** @var IOCMResource[] */ private array $resourceTypes = []; - + private ?ISignatory $signatory = null; private bool $emittedEvent = false; public function __construct( @@ -152,6 +154,14 @@ public function extractProtocolEntry(string $resourceName, string $protocol): st throw new OCMArgumentException('resource not found'); } + public function setSignatory(ISignatory $signatory): void { + $this->signatory = $signatory; + } + + public function getSignatory(): ?ISignatory { + return $this->signatory; + } + /** * import data from an array * @@ -163,7 +173,7 @@ public function extractProtocolEntry(string $resourceName, string $protocol): st */ public function import(array $data): static { $this->setEnabled(is_bool($data['enabled'] ?? '') ? $data['enabled'] : false) - ->setApiVersion((string)($data['apiVersion'] ?? '')) + ->setApiVersion((string)($data['version'] ?? '')) ->setEndPoint($data['endPoint'] ?? ''); $resources = []; @@ -173,6 +183,12 @@ public function import(array $data): static { } $this->setResourceTypes($resources); + // import details about the remote request signing public key, if available + $signatory = new Signatory($data['publicKey']['keyId'] ?? '', $data['publicKey']['publicKeyPem'] ?? ''); + if ($signatory->getKeyId() !== '' && $signatory->getPublicKey() !== '') { + $this->setSignatory($signatory); + } + if (!$this->looksValid()) { throw new OCMProviderException('remote provider does not look valid'); } @@ -188,18 +204,19 @@ private function looksValid(): bool { return ($this->getApiVersion() !== '' && $this->getEndPoint() !== ''); } - /** * @return array{ - * enabled: bool, - * apiVersion: string, - * endPoint: string, - * resourceTypes: list, - * protocols: array - * }>, - * } + * enabled: bool, + * apiVersion: '1.0-proposal1', + * endPoint: string, + * publicKey: ISignatory|null, + * resourceTypes: array{ + * name: string, + * shareTypes: list, + * protocols: array + * }[], + * version: string + * } */ public function jsonSerialize(): array { $resourceTypes = []; @@ -209,8 +226,10 @@ public function jsonSerialize(): array { return [ 'enabled' => $this->isEnabled(), - 'apiVersion' => $this->getApiVersion(), + 'apiVersion' => '1.0-proposal1', // deprecated, but keep it to stay compatible with old version + 'version' => $this->getApiVersion(), // informative but real version 'endPoint' => $this->getEndPoint(), + 'publicKey' => $this->getSignatory(), 'resourceTypes' => $resourceTypes ]; } diff --git a/lib/private/OCM/OCMDiscoveryService.php b/lib/private/OCM/OCMDiscoveryService.php index 279162c76f283..f39d0e2382177 100644 --- a/lib/private/OCM/OCMDiscoveryService.php +++ b/lib/private/OCM/OCMDiscoveryService.php @@ -25,12 +25,6 @@ */ class OCMDiscoveryService implements IOCMDiscoveryService { private ICache $cache; - private array $supportedAPIVersion = - [ - '1.0-proposal1', - '1.0', - '1.1' - ]; public function __construct( ICacheFactory $cacheFactory, @@ -56,9 +50,7 @@ public function discover(string $remote, bool $skipCache = false): IOCMProvider if (!$skipCache) { try { $this->provider->import(json_decode($this->cache->get($remote) ?? '', true, 8, JSON_THROW_ON_ERROR) ?? []); - if ($this->supportedAPIVersion($this->provider->getApiVersion())) { - return $this->provider; // if cache looks valid, we use it - } + return $this->provider; } catch (JsonException|OCMProviderException $e) { // we ignore cache on issues } @@ -94,30 +86,6 @@ public function discover(string $remote, bool $skipCache = false): IOCMProvider throw new OCMProviderException('error while requesting remote ocm provider'); } - if (!$this->supportedAPIVersion($this->provider->getApiVersion())) { - throw new OCMProviderException('API version not supported'); - } - return $this->provider; } - - /** - * Check the version from remote is supported. - * The minor version of the API will be ignored: - * 1.0.1 is identified as 1.0 - * - * @param string $version - * - * @return bool - */ - private function supportedAPIVersion(string $version): bool { - $dot1 = strpos($version, '.'); - $dot2 = strpos($version, '.', $dot1 + 1); - - if ($dot2 > 0) { - $version = substr($version, 0, $dot2); - } - - return (in_array($version, $this->supportedAPIVersion)); - } } diff --git a/lib/private/OCM/OCMSignatoryManager.php b/lib/private/OCM/OCMSignatoryManager.php new file mode 100644 index 0000000000000..1508c1db1ef1b --- /dev/null +++ b/lib/private/OCM/OCMSignatoryManager.php @@ -0,0 +1,149 @@ +appConfig->hasKey('core', self::APPCONFIG_SIGN_IDENTITY_EXTERNAL, true)) { + $identity = $this->appConfig->getValueString('core', self::APPCONFIG_SIGN_IDENTITY_EXTERNAL, lazy: true); + $keyId = 'https://' . $identity . '/ocm#signature'; + } else { + $keyId = $this->generateKeyId(); + } + + try { + $keyPair = $this->keyPairManager->getKeyPair('core', 'ocm_external'); + } catch (KeyPairNotFoundException) { + $keyPair = $this->keyPairManager->generateKeyPair('core', 'ocm_external'); + } + + return new Signatory($keyId, $keyPair->getPublicKey(), $keyPair->getPrivateKey(), local: true); + } + + /** + * - tries to generate a keyId using global configuration (from signature manager) if available + * - generate a keyId using the current route to ocm shares + * + * @return string + * @throws IdentityNotFoundException + */ + private function generateKeyId(): string { + try { + return $this->signatureManager->generateKeyIdFromConfig('/ocm#signature'); + } catch (IdentityNotFoundException) { + } + + $url = $this->urlGenerator->linkToRouteAbsolute('cloud_federation_api.requesthandlercontroller.addShare'); + $identity = $this->signatureManager->extractIdentityFromUri($url); + + // catching possible subfolder to create a keyId like 'https://hostname/subfolder/ocm#signature + $path = parse_url($url, PHP_URL_PATH); + $pos = strpos($path, '/ocm/shares'); + $sub = ($pos) ? substr($path, 0, $pos) : ''; + + return 'https://' . $identity . $sub . '/ocm#signature'; + } + + /** + * @inheritDoc + * + * @param IIncomingSignedRequest $signedRequest + * + * @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()); + } + + /** + * As host is enough to generate signatory using OCMDiscoveryService + * + * @param string $host + * + * @return ISignatory|null + * @throws OCMProviderException on fail to discover ocm services + * @since 31.0.0 + */ + public function getRemoteSignatoryFromHost(string $host): ?ISignatory { + $ocmProvider = $this->ocmDiscoveryService->discover($host, true); + $signatory = $ocmProvider->getSignatory(); + + return $signatory?->setType(SignatoryType::TRUSTED); + } +} diff --git a/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php b/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php new file mode 100644 index 0000000000000..0af960b3a30a8 --- /dev/null +++ b/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php @@ -0,0 +1,182 @@ +hasKeyPair($app, $name)) { + throw new KeyPairConflictException('key pair already exist'); + } + + $keyPair = new KeyPair($app, $name); + + [$publicKey, $privateKey] = $this->generateKeys($options); + $keyPair->setPublicKey($publicKey) + ->setPrivateKey($privateKey) + ->setOptions($options); + + $this->appConfig->setValueArray( + $app, $this->generateAppConfigKey($name), + [ + 'public' => $keyPair->getPublicKey(), + 'private' => $keyPair->getPrivateKey(), + 'options' => $keyPair->getOptions() + ], + lazy: true, + sensitive: true + ); + + return $keyPair; + } + + /** + * @inheritDoc + * + * @param string $app appId + * @param string $name key name + * + * @return bool TRUE if key pair exists in database + * @since 31.0.0 + */ + public function hasKeyPair(string $app, string $name): bool { + $key = $this->generateAppConfigKey($name); + return $this->appConfig->hasKey($app, $key, lazy: true); + } + + /** + * @inheritDoc + * + * @param string $app appId + * @param string $name key name + * + * @return IKeyPair + * @throws KeyPairNotFoundException if key pair is not known + * @since 31.0.0 + */ + public function getKeyPair(string $app, string $name): IKeyPair { + if (!$this->hasKeyPair($app, $name)) { + throw new KeyPairNotFoundException('unknown key pair'); + } + + $key = $this->generateAppConfigKey($name); + $stored = $this->appConfig->getValueArray($app, $key, lazy: true); + if (!array_key_exists('public', $stored) || + !array_key_exists('private', $stored)) { + throw new KeyPairNotFoundException('corrupted key pair'); + } + + $keyPair = new KeyPair($app, $name); + return $keyPair->setPublicKey($stored['public']) + ->setPrivateKey($stored['private']) + ->setOptions($stored['options'] ?? []); + } + + /** + * @inheritDoc + * + * @param string $app appid + * @param string $name key name + * + * @since 31.0.0 + */ + public function deleteKeyPair(string $app, string $name): void { + $this->appConfig->deleteKey('core', $this->generateAppConfigKey($name)); + } + + /** + * @inheritDoc + * + * @param IKeyPair $keyPair keypair to test + * + * @return bool + * @since 31.0.0 + */ + public function testKeyPair(IKeyPair $keyPair): bool { + $clear = md5((string)time()); + + // signing with private key + openssl_sign($clear, $signed, $keyPair->getPrivateKey(), OPENSSL_ALGO_SHA256); + $encoded = base64_encode($signed); + + // verify with public key + $signed = base64_decode($encoded); + return (openssl_verify($clear, $signed, $keyPair->getPublicKey(), 'sha256') === 1); + } + + /** + * return appconfig key based on name of the key pair + * + * @param string $name + * + * @return string + */ + private function generateAppConfigKey(string $name): string { + return self::CONFIG_PREFIX . $name; + } + + /** + * generate the key pair, based on $options with the following default values: + * [ + * 'algorithm' => 'rsa', + * 'bits' => 2048, + * 'type' => OPENSSL_KEYTYPE_RSA + * ] + * + * @param array $options + * + * @return array + */ + private function generateKeys(array $options = []): array { + $res = openssl_pkey_new( + [ + 'digest_alg' => $options['algorithm'] ?? 'rsa', + 'private_key_bits' => $options['bits'] ?? 2048, + 'private_key_type' => $options['type'] ?? OPENSSL_KEYTYPE_RSA, + ] + ); + + openssl_pkey_export($res, $privateKey); + $publicKey = openssl_pkey_get_details($res)['key']; + + return [$publicKey, $privateKey]; + } +} diff --git a/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php b/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php new file mode 100644 index 0000000000000..523f7c1c38083 --- /dev/null +++ b/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php @@ -0,0 +1,114 @@ +app; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getName(): string { + return $this->name; + } + + /** + * @inheritDoc + * + * @param string $publicKey + * @return IKeyPair + * @since 31.0.0 + */ + public function setPublicKey(string $publicKey): IKeyPair { + $this->publicKey = $publicKey; + return $this; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getPublicKey(): string { + return $this->publicKey; + } + + /** + * @inheritDoc + * + * @param string $privateKey + * @return IKeyPair + * @since 31.0.0 + */ + public function setPrivateKey(string $privateKey): IKeyPair { + $this->privateKey = $privateKey; + return $this; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getPrivateKey(): string { + return $this->privateKey; + } + + /** + * @inheritDoc + * + * @param array $options + * @return IKeyPair + * @since 31.0.0 + */ + public function setOptions(array $options): IKeyPair { + $this->options = $options; + return $this; + } + + /** + * @inheritDoc + * + * @return array + * @since 31.0.0 + */ + public function getOptions(): array { + return $this->options; + } +} diff --git a/lib/private/Security/Signature/Model/IncomingSignedRequest.php b/lib/private/Security/Signature/Model/IncomingSignedRequest.php new file mode 100644 index 0000000000000..8fe83a7b09bd2 --- /dev/null +++ b/lib/private/Security/Signature/Model/IncomingSignedRequest.php @@ -0,0 +1,170 @@ +extractIdentityFromUri($signatory->getKeyId()); + if ($identity !== $this->getOrigin()) { + throw new SignatoryException('keyId from provider is different from the one from signed request'); + } + + parent::setSignatory($signatory); + return $this; + } + + /** + * @inheritDoc + * + * @param IRequest $request + * @return IIncomingSignedRequest + * @since 31.0.0 + */ + public function setRequest(IRequest $request): IIncomingSignedRequest { + $this->request = $request; + return $this; + } + + /** + * @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 + * + * @return int + * @since 31.0.0 + */ + public function getTime(): int { + return $this->time; + } + + /** + * @inheritDoc + * + * @param string $origin + * @return IIncomingSignedRequest + * @since 31.0.0 + */ + public function setOrigin(string $origin): IIncomingSignedRequest { + $this->origin = $origin; + return $this; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getOrigin(): string { + return $this->origin; + } + + /** + * returns the keyId extracted from the signature headers. + * keyId is a mandatory entry in the headers of a signed request. + * + * @return string + * @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; + } + + 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(), + ] + ); + } +} diff --git a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php new file mode 100644 index 0000000000000..04efcf8bfe188 --- /dev/null +++ b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php @@ -0,0 +1,131 @@ +host = $host; + return $this; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getHost(): string { + return $this->host; + } + + /** + * @inheritDoc + * + * @param string $key + * @param string|int|float|bool|array $value + * + * @return IOutgoingSignedRequest + * @since 31.0.0 + */ + public function addHeader(string $key, string|int|float|bool|array $value): IOutgoingSignedRequest { + $this->headers[$key] = $value; + return $this; + } + + /** + * @inheritDoc + * + * @return array + * @since 31.0.0 + */ + public function getHeaders(): array { + return $this->headers; + } + + /** + * @inheritDoc + * + * @param string $estimated + * + * @return IOutgoingSignedRequest + * @since 31.0.0 + */ + public function setClearSignature(string $estimated): IOutgoingSignedRequest { + $this->clearSignature = $estimated; + return $this; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getClearSignature(): string { + return $this->clearSignature; + } + + /** + * @inheritDoc + * + * @param string $algorithm + * + * @return IOutgoingSignedRequest + * @since 31.0.0 + */ + public function setAlgorithm(string $algorithm): IOutgoingSignedRequest { + $this->algorithm = $algorithm; + return $this; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getAlgorithm(): string { + return $this->algorithm; + } + + public function jsonSerialize(): array { + return array_merge( + parent::jsonSerialize(), + [ + 'headers' => $this->headers, + 'host' => $this->getHost(), + 'clearSignature' => $this->getClearSignature(), + ] + ); + } +} diff --git a/lib/private/Security/Signature/Model/Signatory.php b/lib/private/Security/Signature/Model/Signatory.php new file mode 100644 index 0000000000000..b28d2c0415f09 --- /dev/null +++ b/lib/private/Security/Signature/Model/Signatory.php @@ -0,0 +1,147 @@ +keyId = $keyId; + } + } + + public function setProviderId(string $providerId): self { + $this->providerId = $providerId; + return $this; + } + + public function getProviderId(): string { + return $this->providerId; + } + + public function setAccount(string $account): self { + $this->account = $account; + return $this; + } + + public function getAccount(): string { + return $this->account; + } + + public function getKeyId(): string { + return $this->keyId; + } + + public function getPublicKey(): string { + return $this->publicKey; + } + + public function getPrivateKey(): string { + return $this->privateKey; + } + + public function setMetadata(array $metadata): self { + $this->metadata = $metadata; + return $this; + } + + public function getMetadata(): array { + return $this->metadata; + } + + public function setMetaValue(string $key, string|int $value): self { + $this->metadata[$key] = $value; + return $this; + } + + public function setType(SignatoryType $type): self { + $this->type = $type; + return $this; + } + public function getType(): SignatoryType { + return $this->type; + } + + public function setStatus(SignatoryStatus $status): self { + $this->status = $status; + return $this; + } + + public function getStatus(): SignatoryStatus { + return $this->status; + } + + public function setCreation(int $creation): self { + $this->creation = $creation; + return $this; + } + + public function getCreation(): int { + return $this->creation; + } + + public function setLastUpdated(int $lastUpdated): self { + $this->lastUpdated = $lastUpdated; + return $this; + } + + public function getLastUpdated(): int { + return $this->lastUpdated; + } + + public function importFromDatabase(array $row): self { + $this->setProviderId($row['provider_id'] ?? '') + ->setAccount($row['account'] ?? '') + ->setMetadata(json_decode($row['metadata'], true) ?? []) + ->setType(SignatoryType::from($row['type'] ?? 9)) + ->setStatus(SignatoryStatus::from($row['status'] ?? 1)) + ->setCreation($row['creation'] ?? 0) + ->setLastUpdated($row['last_updated'] ?? 0); + return $this; + } + + public function jsonSerialize(): array { + return [ + 'keyId' => $this->getKeyId(), + 'publicKeyPem' => $this->getPublicKey() + ]; + } +} diff --git a/lib/private/Security/Signature/Model/SignedRequest.php b/lib/private/Security/Signature/Model/SignedRequest.php new file mode 100644 index 0000000000000..1587da9d63149 --- /dev/null +++ b/lib/private/Security/Signature/Model/SignedRequest.php @@ -0,0 +1,143 @@ +digest = 'SHA-256=' . base64_encode(hash('sha256', utf8_encode($body), true)); + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getBody(): string { + return $this->body; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getDigest(): string { + return $this->digest; + } + + /** + * @inheritDoc + * + * @param array $signatureHeader + * @return ISignedRequest + * @since 31.0.0 + */ + public function setSignatureHeader(array $signatureHeader): ISignedRequest { + $this->signatureHeader = $signatureHeader; + return $this; + } + + /** + * @inheritDoc + * + * @return array + * @since 31.0.0 + */ + public function getSignatureHeader(): array { + return $this->signatureHeader; + } + + /** + * @inheritDoc + * + * @param string $signedSignature + * @return ISignedRequest + * @since 31.0.0 + */ + public function setSignedSignature(string $signedSignature): ISignedRequest { + $this->signedSignature = $signedSignature; + return $this; + } + + /** + * @inheritDoc + * + * @return string + * @since 31.0.0 + */ + public function getSignedSignature(): string { + return $this->signedSignature; + } + + /** + * @inheritDoc + * + * @param ISignatory $signatory + * @return ISignedRequest + * @since 31.0.0 + */ + public function setSignatory(ISignatory $signatory): ISignedRequest { + $this->signatory = $signatory; + return $this; + } + + /** + * @inheritDoc + * + * @return ISignatory + * @throws SignatoryNotFoundException + * @since 31.0.0 + */ + public function getSignatory(): ISignatory { + if ($this->signatory === null) { + throw new SignatoryNotFoundException(); + } + + return $this->signatory; + } + + /** + * @inheritDoc + * + * @return bool + * @since 31.0.0 + */ + public function hasSignatory(): bool { + return ($this->signatory !== null); + } + + public function jsonSerialize(): array { + return [ + 'body' => $this->getBody(), + 'signatureHeader' => $this->getSignatureHeader(), + 'signedSignature' => $this->getSignedSignature(), + 'signatory' => $this->signatory ?? false, + ]; + } +} diff --git a/lib/private/Security/Signature/SignatureManager.php b/lib/private/Security/Signature/SignatureManager.php new file mode 100644 index 0000000000000..d087e8ebdeb0c --- /dev/null +++ b/lib/private/Security/Signature/SignatureManager.php @@ -0,0 +1,828 @@ + self::BODY_MAXSIZE) { + throw new IncomingRequestException('content of request is too big'); + } + + $signedRequest = new IncomingSignedRequest($body); + $signedRequest->setRequest($this->request); + $options = $signatoryManager->getOptions(); + + 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 + ); + } catch (SignatureException $e) { + $this->logger->warning( + 'signature could not be verified', [ + 'exception' => $e, 'signedRequest' => $signedRequest, + 'signatoryManager' => get_class($signatoryManager) + ] + ); + throw $e; + } + + return $signedRequest; + } + + /** + * @inheritDoc + * + * @param ISignatoryManager $signatoryManager + * @param string $content body to be signed + * @param string $method needed in the signature + * @param string $uri needed in the signature + * + * @return IOutgoingSignedRequest + * @since 31.0.0 + */ + public function getOutgoingSignedRequest( + ISignatoryManager $signatoryManager, + string $content, + 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 + ); + $this->setOutgoingClearSignature($signedRequest); + $this->setOutgoingSignedSignature($signedRequest); + $this->signingOutgoingRequest($signedRequest); + + return $signedRequest; + } + + /** + * @inheritDoc + * + * @param ISignatoryManager $signatoryManager + * @param array $payload original payload, will be used to sign and completed with new headers with + * signature elements + * @param string $method needed in the signature + * @param string $uri needed in the signature + * + * @return array new payload to be sent, including original payload and signature elements in headers + * @since 31.0.0 + */ + public function signOutgoingRequestIClientPayload( + ISignatoryManager $signatoryManager, + array $payload, + string $method, + string $uri, + ): array { + $signedRequest = $this->getOutgoingSignedRequest($signatoryManager, $payload['body'], $method, $uri); + $payload['headers'] = array_merge($payload['headers'], $signedRequest->getHeaders()); + + return $payload; + } + + /** + * @inheritDoc + * + * @param string $host remote host + * @param string $account linked account, should be used when multiple signature can exist for the same + * host + * + * @return ISignatory + * @throws SignatoryNotFoundException if entry does not exist in local database + * @since 31.0.0 + */ + public function searchSignatory(string $host, string $account = ''): ISignatory { + $qb = $this->connection->getQueryBuilder(); + $qb->select( + 'id', 'provider_id', 'host', 'account', 'key_id', 'key_id_sum', 'public_key', 'metadata', 'type', + 'status', 'creation', 'last_updated' + ); + $qb->from(self::TABLE_SIGNATORIES); + $qb->where($qb->expr()->eq('host', $qb->createNamedParameter($host))); + $qb->andWhere($qb->expr()->eq('account', $qb->createNamedParameter($account))); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + if (!$row) { + throw new SignatoryNotFoundException('no signatory found'); + } + + $signature = new Signatory($row['key_id'], $row['public_key']); + + return $signature->importFromDatabase($row); + } + + + /** + * @inheritDoc + * + * keyId is set using app config 'core/security.signature.identity' + * + * @param string $path + * + * @return string + * @throws IdentityNotFoundException is identity is not set in app config + * @since 31.0.0 + */ + public function generateKeyIdFromConfig(string $path): string { + if (!$this->appConfig->hasKey('core', self::APPCONFIG_IDENTITY, true)) { + throw new IdentityNotFoundException(self::APPCONFIG_IDENTITY . ' not set'); + } + + $identity = trim($this->appConfig->getValueString('core', self::APPCONFIG_IDENTITY, lazy: true), '/'); + + return 'https://' . $identity . '/' . ltrim($path, '/'); + } + + /** + * @inheritDoc + * + * @param string $uri + * + * @return string + * @throws IdentityNotFoundException if identity cannot be extracted + * @since 31.0.0 + */ + public function extractIdentityFromUri(string $uri): string { + $identity = parse_url($uri, PHP_URL_HOST); + $port = parse_url($uri, PHP_URL_PORT); + if ($identity === null || $identity === false) { + throw new IdentityNotFoundException('cannot extract identity from ' . $uri); + } + + if ($port !== null && $port !== false) { + $identity .= ':' . $port; + } + + return $identity; + } + + /** + * 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 + * + * @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 + */ + private function getSafeRemoteSignatory( + ISignatoryManager $signatoryManager, + IIncomingSignedRequest $signedRequest, + ): ISignatory { + $signatory = $signatoryManager->getRemoteSignatory($signedRequest); + 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); + } + } + + $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)); + } + + + /** + * @param IIncomingSignedRequest $signedRequest + * + * @return void + * @throws SignatureException + * @throws SignatoryNotFoundException + */ + private function verifySignedRequest(IIncomingSignedRequest $signedRequest): void { + $publicKey = $signedRequest->getSignatory()->getPublicKey(); + if ($publicKey === '') { + throw new SignatoryNotFoundException('empty public key'); + } + + try { + $this->verifyString( + $signedRequest->getEstimatedSignature(), + $signedRequest->getSignedSignature(), + $publicKey, + $this->getUsedEncryption($signedRequest) + ); + } catch (InvalidSignatureException $e) { + $this->logger->debug('signature issue', ['signed' => $signedRequest, 'exception' => $e]); + throw $e; + } + } + + + private function getUsedEncryption(IIncomingSignedRequest $signedRequest): SignatureAlgorithm { + $data = $signedRequest->getSignatureHeader(); + + return match ($data['algorithm']) { + 'rsa-sha512' => SignatureAlgorithm::SHA512, + default => SignatureAlgorithm::SHA256, + }; + } + + private function getChosenEncryption(string $algorithm): string { + return match ($algorithm) { + 'sha512' => 'ras-sha512', + default => 'ras-sha256', + }; + } + + public function getOpenSSLAlgo(string $algorithm): int { + return match ($algorithm) { + 'sha512' => OPENSSL_ALGO_SHA512, + default => OPENSSL_ALGO_SHA256, + }; + } + + + /** + * @param string $clear + * @param string $privateKey + * @param string $algorithm + * + * @return string + * @throws SignatoryException + */ + private function signString(string $clear, string $privateKey, string $algorithm): string { + if ($privateKey === '') { + throw new SignatoryException('empty private key'); + } + + openssl_sign($clear, $signed, $privateKey, $this->getOpenSSLAlgo($algorithm)); + + return base64_encode($signed); + } + + /** + * @param string $clear + * @param string $encoded + * @param string $publicKey + * @param SignatureAlgorithm $algo + * + * @return void + * @throws InvalidSignatureException + */ + private function verifyString( + string $clear, + string $encoded, + string $publicKey, + SignatureAlgorithm $algo = SignatureAlgorithm::SHA256, + ): void { + $signed = base64_decode($encoded); + if (openssl_verify($clear, $signed, $publicKey, $algo->value) !== 1) { + throw new InvalidSignatureException('signature issue'); + } + } + + /** + * @param string $keyId + * + * @return ISignatory + * @throws SignatoryNotFoundException + */ + private function getStoredSignatory(string $keyId): ISignatory { + $qb = $this->connection->getQueryBuilder(); + $qb->select( + 'id', 'provider_id', 'host', 'account', 'key_id', 'key_id_sum', 'public_key', 'metadata', 'type', + 'status', 'creation', 'last_updated' + ); + $qb->from(self::TABLE_SIGNATORIES); + $qb->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId)))); + + $result = $qb->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + if (!$row) { + throw new SignatoryNotFoundException('no signatory found in local'); + } + + $signature = new Signatory($row['key_id'], $row['public_key']); + $signature->importFromDatabase($row); + + return $signature; + } + + /** + * @param ISignatory $signatory + */ + private function storeSignatory(ISignatory $signatory): void { + try { + $this->insertSignatory($signatory); + } catch (DBException $e) { + if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + $this->logger->warning('exception while storing signature', ['exception' => $e]); + throw $e; + } + + try { + $this->updateKnownSignatory($signatory); + } catch (SignatoryNotFoundException $e) { + $this->logger->warning('strange behavior, signatory not found ?', ['exception' => $e]); + } + } + } + + 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('account', $qb->createNamedParameter($signatory->getAccount())) + ->setValue('key_id', $qb->createNamedParameter($signatory->getKeyId())) + ->setValue('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId()))) + ->setValue('public_key', $qb->createNamedParameter($signatory->getPublicKey())) + ->setValue('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata()))) + ->setValue('type', $qb->createNamedParameter($signatory->getType()->value)) + ->setValue('status', $qb->createNamedParameter($signatory->getStatus()->value)) + ->setValue('creation', $qb->createNamedParameter(time())) + ->setValue('last_updated', $qb->createNamedParameter(time())); + + $qb->executeStatement(); + } + + /** + * @param ISignatory $signatory + * + * @throws SignatoryNotFoundException + * @throws SignatoryConflictException + */ + private function updateKnownSignatory(ISignatory $signatory): void { + $knownSignatory = $this->getStoredSignatory($signatory->getKeyId()); + switch ($signatory->getType()) { + case SignatoryType::FORGIVABLE: + $this->deleteSignatory($knownSignatory->getKeyId()); + $this->insertSignatory($signatory); + + return; + + case SignatoryType::REFRESHABLE: + $this->updateSignatoryPublicKey($signatory); + $this->updateSignatoryMetadata($signatory); + break; + + case SignatoryType::TRUSTED: + // TODO: send notice to admin + throw new SignatoryConflictException(); + break; + + case SignatoryType::STATIC: + // TODO: send warning to admin + throw new SignatoryConflictException(); + break; + } + } + + /** + * This is called when a remote signatory does not exist anymore + * + * @param ISignatory|null $knownSignatory NULL is not known + * + * @throws SignatoryConflictException + * @throws SignatoryNotFoundException + */ + private function manageDeprecatedSignatory(?ISignatory $knownSignatory): void { + switch ($knownSignatory?->getType()) { + case null: // unknown in local database + case SignatoryType::FORGIVABLE: // who cares ? + throw new SignatoryNotFoundException(); // meaning we just return the correct exception + + case SignatoryType::REFRESHABLE: + // TODO: send notice to admin + throw new SignatoryConflictException(); + + case SignatoryType::TRUSTED: + case SignatoryType::STATIC: + // TODO: send warning to admin + throw new SignatoryConflictException(); + } + } + + + private function updateSignatoryPublicKey(ISignatory $signatory): void { + $qb = $this->connection->getQueryBuilder(); + $qb->update(self::TABLE_SIGNATORIES) + ->set('signatory', $qb->createNamedParameter($signatory->getPublicKey())) + ->set('last_updated', $qb->createNamedParameter(time())); + + $qb->where( + $qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId()))) + ); + $qb->executeStatement(); + } + + private function updateSignatoryMetadata(ISignatory $signatory): void { + $qb = $this->connection->getQueryBuilder(); + $qb->update(self::TABLE_SIGNATORIES) + ->set('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata()))) + ->set('last_updated', $qb->createNamedParameter(time())); + + $qb->where( + $qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId()))) + ); + $qb->executeStatement(); + } + + private function deleteSignatory(string $keyId): void { + $qb = $this->connection->getQueryBuilder(); + $qb->delete(self::TABLE_SIGNATORIES) + ->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId)))); + $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 d57ddf61c0378..7ff0045b03fa8 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -8,6 +8,8 @@ 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; use OC\App\AppStore\Bundles\BundleFetcher; @@ -101,8 +103,10 @@ use OC\Security\CSRF\TokenStorage\SessionStorage; use OC\Security\Hasher; use OC\Security\Ip\RemoteAddress; +use OC\Security\PublicPrivateKeyPairs\KeyPairManager; use OC\Security\RateLimiting\Limiter; use OC\Security\SecureRandom; +use OC\Security\Signature\SignatureManager; use OC\Security\TrustedDomainHelper; use OC\Security\VerificationToken\VerificationToken; use OC\Session\CryptoWrapper; @@ -1180,18 +1184,7 @@ public function __construct($webRoot, \OC\Config $config) { }); $this->registerAlias(\OCP\GlobalScale\IConfig::class, \OC\GlobalScale\Config::class); - - $this->registerService(ICloudFederationProviderManager::class, function (ContainerInterface $c) { - return new CloudFederationProviderManager( - $c->get(\OCP\IConfig::class), - $c->get(IAppManager::class), - $c->get(IClientService::class), - $c->get(ICloudIdManager::class), - $c->get(IOCMDiscoveryService::class), - $c->get(LoggerInterface::class) - ); - }); - + $this->registerAlias(ICloudFederationProviderManager::class, CloudFederationProviderManager::class); $this->registerService(ICloudFederationFactory::class, function (Server $c) { return new CloudFederationFactory(); }); @@ -1297,6 +1290,9 @@ public function __construct($webRoot, \OC\Config $config) { $this->registerAlias(IRichTextFormatter::class, \OC\RichObjectStrings\RichTextFormatter::class); + $this->registerAlias(IKeyPairManager::class, KeyPairManager::class); + $this->registerAlias(ISignatureManager::class, SignatureManager::class); + $this->connectDispatcher(); } diff --git a/lib/public/OCM/IOCMProvider.php b/lib/public/OCM/IOCMProvider.php index ba2ab6ce759ba..789462efd7828 100644 --- a/lib/public/OCM/IOCMProvider.php +++ b/lib/public/OCM/IOCMProvider.php @@ -10,6 +10,7 @@ namespace OCP\OCM; use JsonSerializable; +use NCU\Security\Signature\Model\ISignatory; use OCP\OCM\Exceptions\OCMArgumentException; use OCP\OCM\Exceptions\OCMProviderException; @@ -120,6 +121,22 @@ public function getResourceTypes(): array; */ public function extractProtocolEntry(string $resourceName, string $protocol): string; + /** + * store signatory (public/private key pair) to sign outgoing/incoming request + * + * @param ISignatory $signatory + * @since 31.0.0 + */ + public function setSignatory(ISignatory $signatory): void; + + /** + * signatory (public/private key pair) used to sign outgoing/incoming request + * + * @return ISignatory|null returns null if no ISignatory available + * @since 31.0.0 + */ + public function getSignatory(): ?ISignatory; + /** * import data from an array * @@ -134,13 +151,15 @@ public function import(array $data): static; /** * @return array{ * enabled: bool, - * apiVersion: string, + * apiVersion: '1.0-proposal1', * endPoint: string, - * resourceTypes: list, * protocols: array - * }>, + * }[], + * version: string * } * @since 28.0.0 */ diff --git a/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php b/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php new file mode 100644 index 0000000000000..b80834264dc16 --- /dev/null +++ b/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php @@ -0,0 +1,18 @@ + 300, + * 'ttlSignatory' => 86400*3, + * 'extraSignatureHeaders' => [], + * 'algorithm' => 'sha256', + * 'dateHeader' => "D, d M Y H:i:s T", + * ] + * + * @return array + * @since 31.0.0 + */ + public function getOptions(): array; + + /** + * generate and returns local signatory including private and public key pair. + * + * Used to sign outgoing request + * + * @return ISignatory + * @since 31.0.0 + */ + public function getLocalSignatory(): ISignatory; + + /** + * retrieve details and generate signatory from remote instance. + * If signatory cannot be found, returns NULL. + * + * Used to confirm authenticity of incoming request. + * + * @param IIncomingSignedRequest $signedRequest + * + * @return ISignatory|null must be NULL if no signatory is found + * @since 31.0.0 + */ + public function getRemoteSignatory(IIncomingSignedRequest $signedRequest): ?ISignatory; +} diff --git a/lib/unstable/Security/Signature/ISignatureManager.php b/lib/unstable/Security/Signature/ISignatureManager.php new file mode 100644 index 0000000000000..cc0297224dc58 --- /dev/null +++ b/lib/unstable/Security/Signature/ISignatureManager.php @@ -0,0 +1,129 @@ + Date: Sun, 17 Nov 2024 23:43:47 -0100 Subject: [PATCH 2/4] fix(ocm): switching to IdentityProof Signed-off-by: Maxence Lange --- .../cloud_federation_api/lib/Capabilities.php | 4 +- .../Controller/RequestHandlerController.php | 46 +++-- .../lib/FederatedShareProvider.php | 3 +- .../lib/Notifications.php | 3 +- .../features/bootstrap/FederationContext.php | 2 +- .../cleanup-remote-storage.feature | 24 ++- lib/composer/composer/autoload_classmap.php | 2 - lib/composer/composer/autoload_static.php | 2 - .../CloudFederationProviderManager.php | 109 +++++------ lib/private/Files/Storage/DAV.php | 7 - lib/private/OCM/Model/OCMProvider.php | 4 +- lib/private/OCM/OCMDiscoveryService.php | 13 +- lib/private/OCM/OCMSignatoryManager.php | 20 +- .../Security/IdentityProof/Manager.php | 51 ++++- .../PublicPrivateKeyPairs/KeyPairManager.php | 182 ------------------ .../PublicPrivateKeyPairs/Model/KeyPair.php | 114 ----------- .../Security/Signature/SignatureManager.php | 7 +- lib/private/Server.php | 2 - lib/public/OCM/IOCMProvider.php | 4 +- .../Exceptions/KeyPairConflictException.php | 18 -- .../Exceptions/KeyPairException.php | 20 -- .../Exceptions/KeyPairNotFoundException.php | 16 -- .../PublicPrivateKeyPairs/IKeyPairManager.php | 80 -------- .../PublicPrivateKeyPairs/Model/IKeyPair.php | 85 -------- 24 files changed, 171 insertions(+), 647 deletions(-) delete mode 100644 lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php delete mode 100644 lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php delete mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php delete mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairException.php delete mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairNotFoundException.php delete mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/IKeyPairManager.php delete mode 100644 lib/unstable/Security/PublicPrivateKeyPairs/Model/IKeyPair.php diff --git a/apps/cloud_federation_api/lib/Capabilities.php b/apps/cloud_federation_api/lib/Capabilities.php index 1910a03233791..08806caa5e3e3 100644 --- a/apps/cloud_federation_api/lib/Capabilities.php +++ b/apps/cloud_federation_api/lib/Capabilities.php @@ -8,7 +8,7 @@ */ namespace OCA\CloudFederationAPI; -use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairException; +use NCU\Security\Signature\Exceptions\IdentityNotFoundException; use NCU\Security\Signature\Exceptions\SignatoryException; use OC\OCM\OCMSignatoryManager; use OCP\Capabilities\ICapability; @@ -79,7 +79,7 @@ public function getCapabilities() { } else { $this->logger->debug('ocm public key feature disabled'); } - } catch (SignatoryException|KeyPairException $e) { + } catch (SignatoryException|IdentityNotFoundException $e) { $this->logger->warning('cannot generate local signatory', ['exception' => $e]); } diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index db7f81d559675..b1ab1be3f8849 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -5,6 +5,7 @@ */ namespace OCA\CloudFederationAPI\Controller; +use NCU\Security\Signature\Exceptions\IdentityNotFoundException; use NCU\Security\Signature\Exceptions\IncomingRequestException; use NCU\Security\Signature\Exceptions\SignatoryNotFoundException; use NCU\Security\Signature\Exceptions\SignatureException; @@ -14,6 +15,7 @@ use OC\OCM\OCMSignatoryManager; use OCA\CloudFederationAPI\Config; use OCA\CloudFederationAPI\ResponseDefinitions; +use OCA\FederatedFileSharing\AddressHandler; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\BruteForceProtection; @@ -60,6 +62,7 @@ public function __construct( private IURLGenerator $urlGenerator, private ICloudFederationProviderManager $cloudFederationProviderManager, private Config $config, + private readonly AddressHandler $addressHandler, private readonly IAppConfig $appConfig, private ICloudFederationFactory $factory, private ICloudIdManager $cloudIdManager, @@ -289,6 +292,7 @@ public function receiveNotification($notificationType, $resourceType, $providerI $response->throttle(); return $response; } catch (\Exception $e) { + $this->logger->warning('incoming notification exception', ['exception' => $e]); return new JSONResponse( [ 'message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl(), @@ -376,7 +380,7 @@ private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, str $body = json_decode($signedRequest->getBody(), true) ?? []; $entry = trim($body[$key] ?? '', '@'); if ($this->getHostFromFederationId($entry) !== $signedRequest->getOrigin()) { - throw new IncomingRequestException('share initiation from different instance'); + throw new IncomingRequestException('share initiation (' . $signedRequest->getOrigin() . ') from different instance (' . $entry . ') [key=' . $key . ']'); } } @@ -391,7 +395,6 @@ private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, str * @param IIncomingSignedRequest|null $signedRequest * @param string $token * - * @return void * @throws IncomingRequestException */ private function confirmShareOrigin(?IIncomingSignedRequest $signedRequest, string $token): void { @@ -401,8 +404,23 @@ private function confirmShareOrigin(?IIncomingSignedRequest $signedRequest, stri $provider = $this->shareProviderFactory->getProviderForType(IShare::TYPE_REMOTE); $share = $provider->getShareByToken($token); - $entry = $share->getSharedWith(); + try { + $this->confirmShareEntry($signedRequest, $share->getSharedWith()); + } catch (IncomingRequestException) { + // 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()); + } + } + /** + * @param IIncomingSignedRequest|null $signedRequest + * @param string $entry + * + * @return void + * @throws IncomingRequestException + */ + private function confirmShareEntry(?IIncomingSignedRequest $signedRequest, string $entry): void { $instance = $this->getHostFromFederationId($entry); if ($signedRequest === null) { try { @@ -412,7 +430,7 @@ private function confirmShareOrigin(?IIncomingSignedRequest $signedRequest, stri return; } } elseif ($instance !== $signedRequest->getOrigin()) { - throw new IncomingRequestException('token sharedWith from different instance'); + throw new IncomingRequestException('token sharedWith (' . $instance . ') not linked to origin (' . $signedRequest->getOrigin() . ')'); } } @@ -423,20 +441,16 @@ private function confirmShareOrigin(?IIncomingSignedRequest $signedRequest, stri */ private function getHostFromFederationId(string $entry): string { if (!str_contains($entry, '@')) { - throw new IncomingRequestException('entry does not contains @'); + throw new IncomingRequestException('entry ' . $entry . ' does not contains @'); } - [, $rightPart] = explode('@', $entry, 2); + $rightPart = substr($entry, strrpos($entry, '@') + 1); - $host = parse_url($rightPart, PHP_URL_HOST); - $port = parse_url($rightPart, PHP_URL_PORT); - if ($port !== null && $port !== false) { - $host .= ':' . $port; - } - - if (is_string($host) && $host !== '') { - return $host; + // in case the full scheme is sent; getting rid of it + $rightPart = $this->addressHandler->removeProtocolFromUrl($rightPart); + try { + return $this->signatureManager->extractIdentityFromUri('https://' . $rightPart); + } catch (IdentityNotFoundException) { + throw new IncomingRequestException('invalid host within federation id: ' . $entry); } - - throw new IncomingRequestException('host is empty'); } } diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php index 11e78f5cb97f6..139c873b0d6e4 100644 --- a/apps/federatedfilesharing/lib/FederatedShareProvider.php +++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php @@ -250,7 +250,8 @@ protected function askOwnerToReShare($shareWith, IShare $share, $shareId) { $remote, $shareWith, $share->getPermissions(), - $share->getNode()->getName() + $share->getNode()->getName(), + $share->getShareType(), ); return [$token, $remoteId]; diff --git a/apps/federatedfilesharing/lib/Notifications.php b/apps/federatedfilesharing/lib/Notifications.php index 3c97177358788..3a111ae0ed0e3 100644 --- a/apps/federatedfilesharing/lib/Notifications.php +++ b/apps/federatedfilesharing/lib/Notifications.php @@ -108,12 +108,13 @@ public function sendRemoteShare($token, $shareWith, $name, $remoteId, $owner, $o * @throws HintException * @throws \OC\ServerNotAvailableException */ - public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename) { + public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename, $shareType) { $fields = [ 'shareWith' => $shareWith, 'token' => $token, 'permission' => $permission, 'remoteId' => $shareId, + 'shareType' => $shareType, ]; $ocmFields = $fields; diff --git a/build/integration/features/bootstrap/FederationContext.php b/build/integration/features/bootstrap/FederationContext.php index e146b46644cda..bbd81396df56a 100644 --- a/build/integration/features/bootstrap/FederationContext.php +++ b/build/integration/features/bootstrap/FederationContext.php @@ -38,7 +38,7 @@ public function startFederatedServer() { $port = getenv('PORT_FED'); - self::$phpFederatedServerPid = exec('php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!'); + self::$phpFederatedServerPid = exec('PHP_CLI_SERVER_WORKERS=2 php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!'); } /** diff --git a/build/integration/federation_features/cleanup-remote-storage.feature b/build/integration/federation_features/cleanup-remote-storage.feature index a017b59bcf4cf..a3585bdee96c2 100644 --- a/build/integration/federation_features/cleanup-remote-storage.feature +++ b/build/integration/federation_features/cleanup-remote-storage.feature @@ -4,6 +4,27 @@ Feature: cleanup-remote-storage Background: Given using api version "1" + Scenario: cleanup remote storage with no storage + Given Using server "LOCAL" + And user "user0" exists + Given Using server "REMOTE" + And user "user1" exists + # Rename file so it has a unique name in the target server (as the target + # server may have its own /textfile0.txt" file) + And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" + And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" + And As an "user1" + And Deleting last share + And the OCS status code should be "100" + And the HTTP status code should be "200" + And Deleting last share + And Using server "LOCAL" + When invoking occ with "sharing:cleanup-remote-storage" + Then the command was successful + And the command output contains the text "0 remote storage(s) need(s) to be checked" + And the command output contains the text "0 remote share(s) exist" + And the command output contains the text "no storages deleted" + Scenario: cleanup remote storage with active storages Given Using server "LOCAL" And user "user0" exists @@ -35,9 +56,6 @@ Feature: cleanup-remote-storage # server may have its own /textfile0.txt" file) And User "user1" copies file "/textfile0.txt" to "/remote-share.txt" And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL" - And As an "user1" - And sending "GET" to "/apps/files_sharing/api/v1/shares" - And the list of returned shares has 1 shares And Using server "LOCAL" # Accept and download the file to ensure that a storage is created for the # federated share diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b04f938d2366a..744f3b8b8c51a 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1901,8 +1901,6 @@ 'OC\\Security\\Ip\\Range' => $baseDir . '/lib/private/Security/Ip/Range.php', 'OC\\Security\\Ip\\RemoteAddress' => $baseDir . '/lib/private/Security/Ip/RemoteAddress.php', 'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php', - 'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php', - 'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php', 'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php', 'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php', 'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index dea35ecdfcd11..70f9b3b3e6328 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1942,8 +1942,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Security\\Ip\\Range' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/Range.php', 'OC\\Security\\Ip\\RemoteAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/RemoteAddress.php', 'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php', - 'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php', - 'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php', 'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php', 'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php', 'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php', diff --git a/lib/private/Federation/CloudFederationProviderManager.php b/lib/private/Federation/CloudFederationProviderManager.php index eeb161c3b2514..74935ead40144 100644 --- a/lib/private/Federation/CloudFederationProviderManager.php +++ b/lib/private/Federation/CloudFederationProviderManager.php @@ -18,6 +18,7 @@ use OCP\Federation\ICloudFederationProviderManager; use OCP\Federation\ICloudFederationShare; use OCP\Federation\ICloudIdManager; +use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; use OCP\IAppConfig; @@ -105,25 +106,11 @@ public function getCloudFederationProvider($resourceType) { public function sendShare(ICloudFederationShare $share) { $cloudID = $this->cloudIdManager->resolveCloudId($share->getShareWith()); try { - $ocmProvider = $this->discoveryService->discover($cloudID->getRemote()); - } catch (OCMProviderException $e) { - return false; - } - - $client = $this->httpClientService->newClient(); - try { - // signing the payload using OCMSignatoryManager before initializing the request - $uri = $ocmProvider->getEndPoint() . '/shares'; - $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]); - if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { - $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( - $this->signatoryManager, - $payload, - 'post', $uri - ); + try { + $response = $this->postOcmPayload($cloudID->getRemote(), '/shares', json_encode($share->getShare())); + } catch (OCMProviderException) { + return false; } - $response = $client->post($uri, $signedPayload ?? $payload); - if ($response->getStatusCode() === Http::STATUS_CREATED) { $result = json_decode($response->getBody(), true); return (is_array($result)) ? $result : []; @@ -149,22 +136,9 @@ public function sendShare(ICloudFederationShare $share) { */ public function sendCloudShare(ICloudFederationShare $share): IResponse { $cloudID = $this->cloudIdManager->resolveCloudId($share->getShareWith()); - $ocmProvider = $this->discoveryService->discover($cloudID->getRemote()); - $client = $this->httpClientService->newClient(); try { - // signing the payload using OCMSignatoryManager before initializing the request - $uri = $ocmProvider->getEndPoint() . '/shares'; - $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]); - if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { - $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( - $this->signatoryManager, - $payload, - 'post', $uri - ); - } - - return $client->post($uri, $signedPayload ?? $payload); + return $this->postOcmPayload($cloudID->getRemote(), '/shares', json_encode($share->getShare()), $client); } catch (\Throwable $e) { $this->logger->error('Error while sending share to federation server: ' . $e->getMessage(), ['exception' => $e]); try { @@ -183,26 +157,11 @@ public function sendCloudShare(ICloudFederationShare $share): IResponse { */ public function sendNotification($url, ICloudFederationNotification $notification) { try { - $ocmProvider = $this->discoveryService->discover($url); - } catch (OCMProviderException $e) { - return false; - } - - $client = $this->httpClientService->newClient(); - try { - - // signing the payload using OCMSignatoryManager before initializing the request - $uri = $ocmProvider->getEndPoint() . '/notifications'; - $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]); - if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { - $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( - $this->signatoryManager, - $payload, - 'post', $uri - ); + try { + $response = $this->postOcmPayload($url, '/notifications', json_encode($notification->getMessage())); + } catch (OCMProviderException) { + return false; } - $response = $client->post($uri, $signedPayload ?? $payload); - if ($response->getStatusCode() === Http::STATUS_CREATED) { $result = json_decode($response->getBody(), true); return (is_array($result)) ? $result : []; @@ -222,21 +181,9 @@ public function sendNotification($url, ICloudFederationNotification $notificatio * @throws OCMProviderException */ public function sendCloudNotification(string $url, ICloudFederationNotification $notification): IResponse { - $ocmProvider = $this->discoveryService->discover($url); - $client = $this->httpClientService->newClient(); try { - // signing the payload using OCMSignatoryManager before initializing the request - $uri = $ocmProvider->getEndPoint() . '/notifications'; - $payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]); - if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { - $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( - $this->signatoryManager, - $payload, - 'post', $uri - ); - } - return $client->post($uri, $signedPayload ?? $payload); + return $this->postOcmPayload($url, '/notifications', json_encode($notification->getMessage()), $client); } catch (\Throwable $e) { $this->logger->error('Error while sending notification to federation server: ' . $e->getMessage(), ['exception' => $e]); try { @@ -256,6 +203,40 @@ public function isReady() { return $this->appManager->isEnabledForUser('cloud_federation_api'); } + /** + * @param string $cloudId + * @param string $uri + * @param string $payload + * + * @return IResponse + * @throws OCMProviderException + */ + private function postOcmPayload(string $cloudId, string $uri, string $payload, ?IClient $client = null): IResponse { + $ocmProvider = $this->discoveryService->discover($cloudId); + $uri = $ocmProvider->getEndPoint() . '/' . ltrim($uri, '/'); + $client = $client ?? $this->httpClientService->newClient(); + return $client->post($uri, $this->prepareOcmPayload($uri, $payload)); + } + + /** + * @param string $uri + * @param string $payload + * + * @return array + */ + private function prepareOcmPayload(string $uri, string $payload): array { + $payload = array_merge($this->getDefaultRequestOptions(), ['body' => $payload]); + if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { + $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( + $this->signatoryManager, + $payload, + 'post', $uri + ); + } + + return $signedPayload ?? $payload; + } + private function getDefaultRequestOptions(): array { return [ 'headers' => ['content-type' => 'application/json'], diff --git a/lib/private/Files/Storage/DAV.php b/lib/private/Files/Storage/DAV.php index 597b3f4748888..10670d6331a0e 100644 --- a/lib/private/Files/Storage/DAV.php +++ b/lib/private/Files/Storage/DAV.php @@ -64,7 +64,6 @@ class DAV extends Common { protected $httpClientService; /** @var ICertificateManager */ protected $certManager; - protected bool $verify = true; protected LoggerInterface $logger; protected IEventLogger $eventLogger; protected IMimeTypeDetector $mimeTypeDetector; @@ -104,7 +103,6 @@ public function __construct(array $parameters) { if (isset($parameters['authType'])) { $this->authType = $parameters['authType']; } - $this->verify = (($parameters['verify'] ?? true) !== false); if (isset($parameters['secure'])) { if (is_string($parameters['secure'])) { $this->secure = ($parameters['secure'] === 'true'); @@ -164,11 +162,6 @@ protected function init(): void { } } - if (!$this->verify) { - $this->client->addCurlSetting(CURLOPT_SSL_VERIFYHOST, 0); - $this->client->addCurlSetting(CURLOPT_SSL_VERIFYPEER, false); - } - $lastRequestStart = 0; $this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) { $this->logger->debug('sending dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl(), ['app' => 'dav']); diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php index cd4e9c49c3b29..95ba83882f22b 100644 --- a/lib/private/OCM/Model/OCMProvider.php +++ b/lib/private/OCM/Model/OCMProvider.php @@ -210,11 +210,11 @@ private function looksValid(): bool { * apiVersion: '1.0-proposal1', * endPoint: string, * publicKey: ISignatory|null, - * resourceTypes: array{ + * resourceTypes: list, * protocols: array - * }[], + * }>, * version: string * } */ diff --git a/lib/private/OCM/OCMDiscoveryService.php b/lib/private/OCM/OCMDiscoveryService.php index f39d0e2382177..30e7909a6aae0 100644 --- a/lib/private/OCM/OCMDiscoveryService.php +++ b/lib/private/OCM/OCMDiscoveryService.php @@ -46,6 +46,14 @@ public function __construct( */ public function discover(string $remote, bool $skipCache = false): IOCMProvider { $remote = rtrim($remote, '/'); + if (!str_starts_with($remote, 'http://') && !str_starts_with($remote, 'https://')) { + // if scheme not specified, we test both; + try { + return $this->discover('https://' . $remote, $skipCache); + } catch (OCMProviderException) { + return $this->discover('http://' . $remote, $skipCache); + } + } if (!$skipCache) { try { @@ -65,10 +73,7 @@ public function discover(string $remote, bool $skipCache = false): IOCMProvider if ($this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates') === true) { $options['verify'] = false; } - $response = $client->get( - $remote . '/ocm-provider/', - $options, - ); + $response = $client->get($remote . '/ocm-provider/', $options); if ($response->getStatusCode() === Http::STATUS_OK) { $body = $response->getBody(); diff --git a/lib/private/OCM/OCMSignatoryManager.php b/lib/private/OCM/OCMSignatoryManager.php index 1508c1db1ef1b..a90bb2c1f395e 100644 --- a/lib/private/OCM/OCMSignatoryManager.php +++ b/lib/private/OCM/OCMSignatoryManager.php @@ -8,15 +8,13 @@ */ namespace OC\OCM; -use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairConflictException; -use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairNotFoundException; -use NCU\Security\PublicPrivateKeyPairs\IKeyPairManager; use NCU\Security\Signature\Exceptions\IdentityNotFoundException; use NCU\Security\Signature\ISignatoryManager; use NCU\Security\Signature\ISignatureManager; use NCU\Security\Signature\Model\IIncomingSignedRequest; use NCU\Security\Signature\Model\ISignatory; use NCU\Security\Signature\Model\SignatoryType; +use OC\Security\IdentityProof\Manager; use OC\Security\Signature\Model\Signatory; use OCP\IAppConfig; use OCP\IURLGenerator; @@ -40,7 +38,7 @@ public function __construct( private readonly IAppConfig $appConfig, private readonly ISignatureManager $signatureManager, private readonly IURLGenerator $urlGenerator, - private readonly IKeyPairManager $keyPairManager, + private readonly Manager $identityProofManager, private readonly OCMDiscoveryService $ocmDiscoveryService, ) { } @@ -69,7 +67,6 @@ public function getOptions(): array { * @inheritDoc * * @return ISignatory - * @throws KeyPairConflictException * @throws IdentityNotFoundException * @since 31.0.0 */ @@ -85,13 +82,16 @@ public function getLocalSignatory(): ISignatory { $keyId = $this->generateKeyId(); } - try { - $keyPair = $this->keyPairManager->getKeyPair('core', 'ocm_external'); - } catch (KeyPairNotFoundException) { - $keyPair = $this->keyPairManager->generateKeyPair('core', 'ocm_external'); + if (!$this->identityProofManager->hasAppKey('core', 'ocm_external')) { + $this->identityProofManager->generateAppKey('core', 'ocm_external', [ + 'algorithm' => 'rsa', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); } + $keyPair = $this->identityProofManager->getAppKey('core', 'ocm_external'); - return new Signatory($keyId, $keyPair->getPublicKey(), $keyPair->getPrivateKey(), local: true); + return new Signatory($keyId, $keyPair->getPublic(), $keyPair->getPrivate(), local: true); } /** diff --git a/lib/private/Security/IdentityProof/Manager.php b/lib/private/Security/IdentityProof/Manager.php index 0ce760ccc63b3..de0b3fe6bd16e 100644 --- a/lib/private/Security/IdentityProof/Manager.php +++ b/lib/private/Security/IdentityProof/Manager.php @@ -10,6 +10,7 @@ use OC\Files\AppData\Factory; use OCP\Files\IAppData; +use OCP\Files\NotFoundException; use OCP\IConfig; use OCP\IUser; use OCP\Security\ICrypto; @@ -31,18 +32,20 @@ public function __construct( * Calls the openssl functions to generate a public and private key. * In a separate function for unit testing purposes. * + * @param array $options config options to generate key {@see openssl_csr_new} + * * @return array [$publicKey, $privateKey] * @throws \RuntimeException */ - protected function generateKeyPair(): array { + protected function generateKeyPair(array $options = []): array { $config = [ - 'digest_alg' => 'sha512', - 'private_key_bits' => 2048, + 'digest_alg' => $options['algorithm'] ?? 'sha512', + 'private_key_bits' => $options['bits'] ?? 2048, + 'private_key_type' => $options['type'] ?? OPENSSL_KEYTYPE_RSA, ]; // Generate new key $res = openssl_pkey_new($config); - if ($res === false) { $this->logOpensslError(); throw new \RuntimeException('OpenSSL reported a problem'); @@ -65,15 +68,17 @@ protected function generateKeyPair(): array { * Note: If a key already exists it will be overwritten * * @param string $id key id + * @param array $options config options to generate key {@see openssl_csr_new} + * * @throws \RuntimeException */ - protected function generateKey(string $id): Key { - [$publicKey, $privateKey] = $this->generateKeyPair(); + protected function generateKey(string $id, array $options = []): Key { + [$publicKey, $privateKey] = $this->generateKeyPair($options); // Write the private and public key to the disk try { $this->appData->newFolder($id); - } catch (\Exception $e) { + } catch (\Exception) { } $folder = $this->appData->getFolder($id); $folder->newFile('private') @@ -125,6 +130,38 @@ public function getSystemKey(): Key { return $this->retrieveKey('system-' . $instanceId); } + public function hasAppKey(string $app, string $name): bool { + $id = $this->generateAppKeyId($app, $name); + try { + $this->appData->getFolder($id); + return true; + } catch (NotFoundException) { + return false; + } + } + + public function getAppKey(string $app, string $name): Key { + return $this->retrieveKey($this->generateAppKeyId($app, $name)); + } + + public function generateAppKey(string $app, string $name, array $options = []): Key { + return $this->generateKey($this->generateAppKeyId($app, $name), $options); + } + + public function deleteAppKey(string $app, string $name): bool { + try { + $folder = $this->appData->getFolder($this->generateAppKeyId($app, $name)); + } catch (NotFoundException) { + return false; + } + $folder->delete(); + return true; + } + + private function generateAppKeyId(string $app, string $name): string { + return 'app-' . $app . '-' . $name; + } + private function logOpensslError(): void { $errors = []; while ($error = openssl_error_string()) { diff --git a/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php b/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php deleted file mode 100644 index 0af960b3a30a8..0000000000000 --- a/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php +++ /dev/null @@ -1,182 +0,0 @@ -hasKeyPair($app, $name)) { - throw new KeyPairConflictException('key pair already exist'); - } - - $keyPair = new KeyPair($app, $name); - - [$publicKey, $privateKey] = $this->generateKeys($options); - $keyPair->setPublicKey($publicKey) - ->setPrivateKey($privateKey) - ->setOptions($options); - - $this->appConfig->setValueArray( - $app, $this->generateAppConfigKey($name), - [ - 'public' => $keyPair->getPublicKey(), - 'private' => $keyPair->getPrivateKey(), - 'options' => $keyPair->getOptions() - ], - lazy: true, - sensitive: true - ); - - return $keyPair; - } - - /** - * @inheritDoc - * - * @param string $app appId - * @param string $name key name - * - * @return bool TRUE if key pair exists in database - * @since 31.0.0 - */ - public function hasKeyPair(string $app, string $name): bool { - $key = $this->generateAppConfigKey($name); - return $this->appConfig->hasKey($app, $key, lazy: true); - } - - /** - * @inheritDoc - * - * @param string $app appId - * @param string $name key name - * - * @return IKeyPair - * @throws KeyPairNotFoundException if key pair is not known - * @since 31.0.0 - */ - public function getKeyPair(string $app, string $name): IKeyPair { - if (!$this->hasKeyPair($app, $name)) { - throw new KeyPairNotFoundException('unknown key pair'); - } - - $key = $this->generateAppConfigKey($name); - $stored = $this->appConfig->getValueArray($app, $key, lazy: true); - if (!array_key_exists('public', $stored) || - !array_key_exists('private', $stored)) { - throw new KeyPairNotFoundException('corrupted key pair'); - } - - $keyPair = new KeyPair($app, $name); - return $keyPair->setPublicKey($stored['public']) - ->setPrivateKey($stored['private']) - ->setOptions($stored['options'] ?? []); - } - - /** - * @inheritDoc - * - * @param string $app appid - * @param string $name key name - * - * @since 31.0.0 - */ - public function deleteKeyPair(string $app, string $name): void { - $this->appConfig->deleteKey('core', $this->generateAppConfigKey($name)); - } - - /** - * @inheritDoc - * - * @param IKeyPair $keyPair keypair to test - * - * @return bool - * @since 31.0.0 - */ - public function testKeyPair(IKeyPair $keyPair): bool { - $clear = md5((string)time()); - - // signing with private key - openssl_sign($clear, $signed, $keyPair->getPrivateKey(), OPENSSL_ALGO_SHA256); - $encoded = base64_encode($signed); - - // verify with public key - $signed = base64_decode($encoded); - return (openssl_verify($clear, $signed, $keyPair->getPublicKey(), 'sha256') === 1); - } - - /** - * return appconfig key based on name of the key pair - * - * @param string $name - * - * @return string - */ - private function generateAppConfigKey(string $name): string { - return self::CONFIG_PREFIX . $name; - } - - /** - * generate the key pair, based on $options with the following default values: - * [ - * 'algorithm' => 'rsa', - * 'bits' => 2048, - * 'type' => OPENSSL_KEYTYPE_RSA - * ] - * - * @param array $options - * - * @return array - */ - private function generateKeys(array $options = []): array { - $res = openssl_pkey_new( - [ - 'digest_alg' => $options['algorithm'] ?? 'rsa', - 'private_key_bits' => $options['bits'] ?? 2048, - 'private_key_type' => $options['type'] ?? OPENSSL_KEYTYPE_RSA, - ] - ); - - openssl_pkey_export($res, $privateKey); - $publicKey = openssl_pkey_get_details($res)['key']; - - return [$publicKey, $privateKey]; - } -} diff --git a/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php b/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php deleted file mode 100644 index 523f7c1c38083..0000000000000 --- a/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php +++ /dev/null @@ -1,114 +0,0 @@ -app; - } - - /** - * @inheritDoc - * - * @return string - * @since 31.0.0 - */ - public function getName(): string { - return $this->name; - } - - /** - * @inheritDoc - * - * @param string $publicKey - * @return IKeyPair - * @since 31.0.0 - */ - public function setPublicKey(string $publicKey): IKeyPair { - $this->publicKey = $publicKey; - return $this; - } - - /** - * @inheritDoc - * - * @return string - * @since 31.0.0 - */ - public function getPublicKey(): string { - return $this->publicKey; - } - - /** - * @inheritDoc - * - * @param string $privateKey - * @return IKeyPair - * @since 31.0.0 - */ - public function setPrivateKey(string $privateKey): IKeyPair { - $this->privateKey = $privateKey; - return $this; - } - - /** - * @inheritDoc - * - * @return string - * @since 31.0.0 - */ - public function getPrivateKey(): string { - return $this->privateKey; - } - - /** - * @inheritDoc - * - * @param array $options - * @return IKeyPair - * @since 31.0.0 - */ - public function setOptions(array $options): IKeyPair { - $this->options = $options; - return $this; - } - - /** - * @inheritDoc - * - * @return array - * @since 31.0.0 - */ - public function getOptions(): array { - return $this->options; - } -} diff --git a/lib/private/Security/Signature/SignatureManager.php b/lib/private/Security/Signature/SignatureManager.php index d087e8ebdeb0c..8717171f4b415 100644 --- a/lib/private/Security/Signature/SignatureManager.php +++ b/lib/private/Security/Signature/SignatureManager.php @@ -112,9 +112,7 @@ public function getIncomingSignedRequest( $this->prepIncomingSignatureHeader($signedRequest); $this->verifyIncomingSignatureHeader($signedRequest); $this->prepEstimatedSignature($signedRequest, $options['extraSignatureHeaders'] ?? []); - $this->verifyIncomingRequestSignature( - $signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL - ); + $this->verifyIncomingRequestSignature($signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL); } catch (SignatureException $e) { $this->logger->warning( 'signature could not be verified', [ @@ -724,7 +722,6 @@ private function updateKnownSignatory(ISignatory $signatory): void { case SignatoryType::FORGIVABLE: $this->deleteSignatory($knownSignatory->getKeyId()); $this->insertSignatory($signatory); - return; case SignatoryType::REFRESHABLE: @@ -735,12 +732,10 @@ private function updateKnownSignatory(ISignatory $signatory): void { case SignatoryType::TRUSTED: // TODO: send notice to admin throw new SignatoryConflictException(); - break; case SignatoryType::STATIC: // TODO: send warning to admin throw new SignatoryConflictException(); - break; } } diff --git a/lib/private/Server.php b/lib/private/Server.php index 7ff0045b03fa8..2167bccec8982 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -103,7 +103,6 @@ use OC\Security\CSRF\TokenStorage\SessionStorage; use OC\Security\Hasher; use OC\Security\Ip\RemoteAddress; -use OC\Security\PublicPrivateKeyPairs\KeyPairManager; use OC\Security\RateLimiting\Limiter; use OC\Security\SecureRandom; use OC\Security\Signature\SignatureManager; @@ -1290,7 +1289,6 @@ public function __construct($webRoot, \OC\Config $config) { $this->registerAlias(IRichTextFormatter::class, \OC\RichObjectStrings\RichTextFormatter::class); - $this->registerAlias(IKeyPairManager::class, KeyPairManager::class); $this->registerAlias(ISignatureManager::class, SignatureManager::class); $this->connectDispatcher(); diff --git a/lib/public/OCM/IOCMProvider.php b/lib/public/OCM/IOCMProvider.php index 789462efd7828..dd36a1c605715 100644 --- a/lib/public/OCM/IOCMProvider.php +++ b/lib/public/OCM/IOCMProvider.php @@ -154,11 +154,11 @@ public function import(array $data): static; * apiVersion: '1.0-proposal1', * endPoint: string, * publicKey: ISignatory|null, - * resourceTypes: array{ + * resourceTypes: list, * protocols: array - * }[], + * }>, * version: string * } * @since 28.0.0 diff --git a/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php b/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php deleted file mode 100644 index b80834264dc16..0000000000000 --- a/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php +++ /dev/null @@ -1,18 +0,0 @@ - Date: Thu, 21 Nov 2024 09:25:00 -0100 Subject: [PATCH 3/4] 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 b1ab1be3f8849..e277b9b638964 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -336,8 +336,11 @@ private function mapUid($uid) { */ 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 @@ private function getSignedRequest(): ?IIncomingSignedRequest { 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 @@ private function confirmShareOrigin(?IIncomingSignedRequest $signedRequest, stri $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 744f3b8b8c51a..87e617ed4e628 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -12,6 +12,25 @@ '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 @@ '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 70f9b3b3e6328..640924c6579ee 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 74935ead40144..e93542943510a 100644 --- a/lib/private/Federation/CloudFederationProviderManager.php +++ b/lib/private/Federation/CloudFederationProviderManager.php @@ -226,6 +226,12 @@ private function postOcmPayload(string $cloudId, string $uri, string $payload, ? */ 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 a90bb2c1f395e..c7eb9ccda5aad 100644 --- a/lib/private/OCM/OCMSignatoryManager.php +++ b/lib/private/OCM/OCMSignatoryManager.php @@ -6,12 +6,12 @@ * 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 OCP\IAppConfig; use OCP\IURLGenerator; use OCP\OCM\Exceptions\OCMProviderException; +use Psr\Log\LoggerInterface; /** * @inheritDoc @@ -40,14 +41,15 @@ public function __construct( 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 @@ public function getProviderId(): string { /** * @inheritDoc * - * @since 31.0.0 * @return array + * @since 31.0.0 */ public function getOptions(): array { return []; @@ -121,14 +123,18 @@ private function generateKeyId(): string { /** * @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 8fe83a7b09bd2..77914d1e3b2dc 100644 --- a/lib/private/Security/Signature/Model/IncomingSignedRequest.php +++ b/lib/private/Security/Signature/Model/IncomingSignedRequest.php @@ -10,11 +10,14 @@ 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 @@ 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 @@ public function setOrigin(string $origin): IIncomingSignedRequest { * @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 @@ public function getOrigin(): string { * 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 04efcf8bfe188..d2d5b95e7b644 100644 --- a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php +++ b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php @@ -9,8 +9,11 @@ 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 @@ public function getHost(): string { * @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 @@ public function getHeaders(): array { } /** - * @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 @@ public function setAlgorithm(string $algorithm): IOutgoingSignedRequest { /** * @inheritDoc * - * @return string + * @return SignatureAlgorithm * @since 31.0.0 */ - public function getAlgorithm(): string { + public function getAlgorithm(): SignatureAlgorithm { return $this->algorithm; } @@ -122,9 +161,12 @@ public function jsonSerialize(): array { 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 1587da9d63149..56853ebade3c5 100644 --- a/lib/private/Security/Signature/Model/SignedRequest.php +++ b/lib/private/Security/Signature/Model/SignedRequest.php @@ -10,6 +10,7 @@ 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 @@ */ 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 @@ public function getDigest(): string { /** * @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 @@ public function setSignatureHeader(array $signatureHeader): ISignedRequest { * @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 @@ public function hasSignatory(): bool { 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 8717171f4b415..2d895b465abeb 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 @@ public function getIncomingSignedRequest( 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 @@ public function getIncomingSignedRequest( * @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 @@ public function getOutgoingSignedRequest( string $method, string $uri, ): IOutgoingSignedRequest { - $signedRequest = new OutgoingSignedRequest($content); - $options = $signatoryManager->getOptions(); - - $signedRequest->setHost($this->getHostFromUri($uri)) - ->setAlgorithm($options['algorithm'] ?? 'sha256') - ->setSignatory($signatoryManager->getLocalSignatory()); - - $this->setOutgoingSignatureHeader( - $signedRequest, - strtolower($method), - parse_url($uri, PHP_URL_PATH) ?? '/', - $options['dateHeader'] ?? self::DATE_HEADER + $signedRequest = new OutgoingSignedRequest( + $content, + $signatoryManager, + $this->extractIdentityFromUri($uri), + $method, + parse_url($uri, PHP_URL_PATH) ?? '/' ); - $this->setOutgoingClearSignature($signedRequest); - $this->setOutgoingSignedSignature($signedRequest); - $this->signingOutgoingRequest($signedRequest); + + $this->signOutgoingRequest($signedRequest); return $signedRequest; } + /** + * signing clear version of the Signature header + * + * @param IOutgoingSignedRequest $signedRequest + * + * @throws SignatoryException + * @throws SignatoryNotFoundException + */ + private function signOutgoingRequest(IOutgoingSignedRequest $signedRequest): void { + $clear = $signedRequest->getClearSignature(); + $signed = $this->signString($clear, $signedRequest->getSignatory()->getPrivateKey(), $signedRequest->getAlgorithm()); + + $signatory = $signedRequest->getSignatory(); + $signatureElements = [ + 'keyId="' . $signatory->getKeyId() . '"', + 'algorithm="' . $signedRequest->getAlgorithm()->value . '"', + 'headers="' . implode(' ', $signedRequest->getHeaderList()) . '"', + 'signature="' . $signed . '"' + ]; + + $signedRequest->setSignedSignature($signed); + $signedRequest->addHeader('Signature', implode(',', $signatureElements)); + } + /** * @inheritDoc * @@ -267,292 +380,36 @@ public function extractIdentityFromUri(string $uri): string { } /** - * 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 @@ private function verifySignedRequest(IIncomingSignedRequest $signedRequest): voi 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 @@ private function verifySignedRequest(IIncomingSignedRequest $signedRequest): voi } } - - 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 @@ private function signString(string $clear, string $privateKey, string $algorithm * @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 @@ private function storeSignatory(ISignatory $signatory): void { } } + /** + * @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 @@ private function manageDeprecatedSignatory(?ISignatory $knownSignatory): void { 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 @@ private function deleteSignatory(string $keyId): void { $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 2167bccec8982..a20c37732a76e 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -8,7 +8,6 @@ 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 1953af39ec583..f40f79410aef4 100644 --- a/lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php +++ b/lib/unstable/Security/Signature/Exceptions/SignatureElementNotFoundException.php @@ -12,5 +12,5 @@ * @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 825ccac1ce92e..19ba83a4206dd 100644 --- a/lib/unstable/Security/Signature/ISignatoryManager.php +++ b/lib/unstable/Security/Signature/ISignatoryManager.php @@ -8,7 +8,6 @@ */ namespace NCU\Security\Signature; -use NCU\Security\Signature\Model\IIncomingSignedRequest; use NCU\Security\Signature\Model\ISignatory; /** @@ -34,6 +33,7 @@ public function getProviderId(): string; /** * options that might affect the way the whole process is handled: * [ + * 'bodyMaxSize' => 10000, * 'ttl' => 300, * 'ttlSignatory' => 86400*3, * 'extraSignatureHeaders' => [], @@ -62,10 +62,10 @@ public function getLocalSignatory(): ISignatory; * * 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 cc0297224dc58..1969b970aa68d 100644 --- a/lib/unstable/Security/Signature/ISignatureManager.php +++ b/lib/unstable/Security/Signature/ISignatureManager.php @@ -28,7 +28,7 @@ * "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 a6682eff33c3c..3e2ebb22a5f6c 100644 --- a/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php +++ b/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php @@ -8,6 +8,7 @@ */ namespace NCU\Security\Signature\Model; +use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException; use NCU\Security\Signature\ISignatureManager; use OCP\IRequest; @@ -20,15 +21,6 @@ * @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 @@ public function setRequest(IRequest $request): IIncomingSignedRequest; */ 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 @@ public function getOrigin(): string; * 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 b2ca221e12679..3c9445af74576 100644 --- a/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php +++ b/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php @@ -9,6 +9,7 @@ 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 @@ public function getHost(): string; * 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 @@ public function addHeader(string $key, string|int|float|bool|array $value): IOut 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 ebb0e1c5b5856..76c033970fe84 100644 --- a/lib/unstable/Security/Signature/Model/ISignedRequest.php +++ b/lib/unstable/Security/Signature/Model/ISignedRequest.php @@ -9,6 +9,7 @@ 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 @@ public function getDigest(): string; /** * 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 1c28f6580e7d0..4174102beaec5 100644 --- a/lib/unstable/Security/Signature/Model/SignatoryStatus.php +++ b/lib/unstable/Security/Signature/Model/SignatoryStatus.php @@ -12,7 +12,7 @@ * 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 1b1c154f4a6fe..e87981f0ec244 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'; From 618ff23cf6d74a41dc71239ace3b86e56794287a Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Fri, 22 Nov 2024 16:20:06 -0100 Subject: [PATCH 4/4] feat(signatory): switch to qbmapper Signed-off-by: Maxence Lange --- .../Controller/RequestHandlerController.php | 6 +- lib/composer/composer/autoload_classmap.php | 16 +- lib/composer/composer/autoload_static.php | 16 +- lib/private/OCM/Model/OCMProvider.php | 11 +- lib/private/OCM/OCMSignatoryManager.php | 17 +- .../Security/IdentityProof/Manager.php | 8 +- .../Security/Signature/Db/SignatoryMapper.php | 114 ++++++++++++ .../Signature/Model/IncomingSignedRequest.php | 8 +- .../Signature/Model/OutgoingSignedRequest.php | 5 +- .../Security/Signature/Model/Signatory.php | 147 ---------------- .../Signature/Model/SignedRequest.php | 14 +- .../Security/Signature/SignatureManager.php | 139 ++++----------- lib/public/OCM/IOCMProvider.php | 13 +- .../{Model => Enum}/SignatoryStatus.php | 2 +- .../{Model => Enum}/SignatoryType.php | 6 +- .../{ => Enum}/SignatureAlgorithm.php | 2 +- .../{Model => }/IIncomingSignedRequest.php | 3 +- .../{Model => }/IOutgoingSignedRequest.php | 5 +- .../Security/Signature/ISignatoryManager.php | 10 +- .../Security/Signature/ISignatureManager.php | 8 +- .../Signature/{Model => }/ISignedRequest.php | 11 +- .../Security/Signature/Model/ISignatory.php | 160 ----------------- .../Security/Signature/Model/Signatory.php | 165 ++++++++++++++++++ 23 files changed, 387 insertions(+), 499 deletions(-) create mode 100644 lib/private/Security/Signature/Db/SignatoryMapper.php delete mode 100644 lib/private/Security/Signature/Model/Signatory.php rename lib/unstable/Security/Signature/{Model => Enum}/SignatoryStatus.php (92%) rename lib/unstable/Security/Signature/{Model => Enum}/SignatoryType.php (87%) rename lib/unstable/Security/Signature/{ => Enum}/SignatureAlgorithm.php (91%) rename lib/unstable/Security/Signature/{Model => }/IIncomingSignedRequest.php (94%) rename lib/unstable/Security/Signature/{Model => }/IOutgoingSignedRequest.php (94%) rename lib/unstable/Security/Signature/{Model => }/ISignedRequest.php (91%) delete mode 100644 lib/unstable/Security/Signature/Model/ISignatory.php create mode 100644 lib/unstable/Security/Signature/Model/Signatory.php diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index e277b9b638964..a243d286c71b2 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -10,8 +10,8 @@ use NCU\Security\Signature\Exceptions\SignatoryNotFoundException; use NCU\Security\Signature\Exceptions\SignatureException; use NCU\Security\Signature\Exceptions\SignatureNotFoundException; +use NCU\Security\Signature\IIncomingSignedRequest; use NCU\Security\Signature\ISignatureManager; -use NCU\Security\Signature\Model\IIncomingSignedRequest; use OC\OCM\OCMSignatoryManager; use OCA\CloudFederationAPI\Config; use OCA\CloudFederationAPI\ResponseDefinitions; @@ -373,7 +373,7 @@ private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, str if ($signedRequest === null) { $instance = $this->getHostFromFederationId($value); try { - $this->signatureManager->searchSignatory($instance); + $this->signatureManager->getSignatory($instance); throw new IncomingRequestException('instance is supposed to sign its request'); } catch (SignatoryNotFoundException) { return; @@ -434,7 +434,7 @@ private function confirmShareEntry(?IIncomingSignedRequest $signedRequest, strin $instance = $this->getHostFromFederationId($entry); if ($signedRequest === null) { try { - $this->signatureManager->searchSignatory($instance); + $this->signatureManager->getSignatory($instance); throw new IncomingRequestException('instance is supposed to sign its request'); } catch (SignatoryNotFoundException) { return; diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 87e617ed4e628..b3d4c92220cac 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -12,6 +12,9 @@ '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\\Enum\\SignatoryStatus' => $baseDir . '/lib/unstable/Security/Signature/Enum/SignatoryStatus.php', + 'NCU\\Security\\Signature\\Enum\\SignatoryType' => $baseDir . '/lib/unstable/Security/Signature/Enum/SignatoryType.php', + 'NCU\\Security\\Signature\\Enum\\SignatureAlgorithm' => $baseDir . '/lib/unstable/Security/Signature/Enum/SignatureAlgorithm.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', @@ -22,15 +25,12 @@ '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\\IIncomingSignedRequest' => $baseDir . '/lib/unstable/Security/Signature/IIncomingSignedRequest.php', + 'NCU\\Security\\Signature\\IOutgoingSignedRequest' => $baseDir . '/lib/unstable/Security/Signature/IOutgoingSignedRequest.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', + 'NCU\\Security\\Signature\\ISignedRequest' => $baseDir . '/lib/unstable/Security/Signature/ISignedRequest.php', + 'NCU\\Security\\Signature\\Model\\Signatory' => $baseDir . '/lib/unstable/Security/Signature/Model/Signatory.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', @@ -1929,9 +1929,9 @@ 'OC\\Security\\RateLimiting\\Limiter' => $baseDir . '/lib/private/Security/RateLimiting/Limiter.php', 'OC\\Security\\RemoteHostValidator' => $baseDir . '/lib/private/Security/RemoteHostValidator.php', 'OC\\Security\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php', + 'OC\\Security\\Signature\\Db\\SignatoryMapper' => $baseDir . '/lib/private/Security/Signature/Db/SignatoryMapper.php', 'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php', 'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php', - 'OC\\Security\\Signature\\Model\\Signatory' => $baseDir . '/lib/private/Security/Signature/Model/Signatory.php', 'OC\\Security\\Signature\\Model\\SignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/SignedRequest.php', 'OC\\Security\\Signature\\SignatureManager' => $baseDir . '/lib/private/Security/Signature/SignatureManager.php', 'OC\\Security\\TrustedDomainHelper' => $baseDir . '/lib/private/Security/TrustedDomainHelper.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 640924c6579ee..31e93679086ec 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -53,6 +53,9 @@ 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\\Enum\\SignatoryStatus' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Enum/SignatoryStatus.php', + 'NCU\\Security\\Signature\\Enum\\SignatoryType' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Enum/SignatoryType.php', + 'NCU\\Security\\Signature\\Enum\\SignatureAlgorithm' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Enum/SignatureAlgorithm.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', @@ -63,15 +66,12 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 '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\\IIncomingSignedRequest' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/IIncomingSignedRequest.php', + 'NCU\\Security\\Signature\\IOutgoingSignedRequest' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/IOutgoingSignedRequest.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', + 'NCU\\Security\\Signature\\ISignedRequest' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/ISignedRequest.php', + 'NCU\\Security\\Signature\\Model\\Signatory' => __DIR__ . '/../../..' . '/lib/unstable/Security/Signature/Model/Signatory.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', @@ -1970,9 +1970,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Security\\RateLimiting\\Limiter' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Limiter.php', 'OC\\Security\\RemoteHostValidator' => __DIR__ . '/../../..' . '/lib/private/Security/RemoteHostValidator.php', 'OC\\Security\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php', + 'OC\\Security\\Signature\\Db\\SignatoryMapper' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Db/SignatoryMapper.php', 'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php', 'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php', - 'OC\\Security\\Signature\\Model\\Signatory' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/Signatory.php', 'OC\\Security\\Signature\\Model\\SignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/SignedRequest.php', 'OC\\Security\\Signature\\SignatureManager' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/SignatureManager.php', 'OC\\Security\\TrustedDomainHelper' => __DIR__ . '/../../..' . '/lib/private/Security/TrustedDomainHelper.php', diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php index 95ba83882f22b..32068efe3eb16 100644 --- a/lib/private/OCM/Model/OCMProvider.php +++ b/lib/private/OCM/Model/OCMProvider.php @@ -9,8 +9,7 @@ namespace OC\OCM\Model; -use NCU\Security\Signature\Model\ISignatory; -use OC\Security\Signature\Model\Signatory; +use NCU\Security\Signature\Model\Signatory; use OCP\EventDispatcher\IEventDispatcher; use OCP\OCM\Events\ResourceTypeRegisterEvent; use OCP\OCM\Exceptions\OCMArgumentException; @@ -27,7 +26,7 @@ class OCMProvider implements IOCMProvider { private string $endPoint = ''; /** @var IOCMResource[] */ private array $resourceTypes = []; - private ?ISignatory $signatory = null; + private ?Signatory $signatory = null; private bool $emittedEvent = false; public function __construct( @@ -154,11 +153,11 @@ public function extractProtocolEntry(string $resourceName, string $protocol): st throw new OCMArgumentException('resource not found'); } - public function setSignatory(ISignatory $signatory): void { + public function setSignatory(Signatory $signatory): void { $this->signatory = $signatory; } - public function getSignatory(): ?ISignatory { + public function getSignatory(): ?Signatory { return $this->signatory; } @@ -209,7 +208,7 @@ private function looksValid(): bool { * enabled: bool, * apiVersion: '1.0-proposal1', * endPoint: string, - * publicKey: ISignatory|null, + * publicKey: Signatory|null, * resourceTypes: list, diff --git a/lib/private/OCM/OCMSignatoryManager.php b/lib/private/OCM/OCMSignatoryManager.php index c7eb9ccda5aad..ca0a295468f57 100644 --- a/lib/private/OCM/OCMSignatoryManager.php +++ b/lib/private/OCM/OCMSignatoryManager.php @@ -9,13 +9,12 @@ namespace OC\OCM; +use NCU\Security\Signature\Enum\SignatoryType; use NCU\Security\Signature\Exceptions\IdentityNotFoundException; use NCU\Security\Signature\ISignatoryManager; use NCU\Security\Signature\ISignatureManager; -use NCU\Security\Signature\Model\ISignatory; -use NCU\Security\Signature\Model\SignatoryType; +use NCU\Security\Signature\Model\Signatory; use OC\Security\IdentityProof\Manager; -use OC\Security\Signature\Model\Signatory; use OCP\IAppConfig; use OCP\IURLGenerator; use OCP\OCM\Exceptions\OCMProviderException; @@ -68,11 +67,11 @@ public function getOptions(): array { /** * @inheritDoc * - * @return ISignatory + * @return Signatory * @throws IdentityNotFoundException * @since 31.0.0 */ - public function getLocalSignatory(): ISignatory { + public function getLocalSignatory(): Signatory { /** * TODO: manage multiple identity (external, internal, ...) to allow a limitation * based on the requested interface (ie. only accept shares from globalscale) @@ -125,10 +124,10 @@ private function generateKeyId(): string { * * @param string $remote * - * @return ISignatory|null must be NULL if no signatory is found + * @return Signatory|null must be NULL if no signatory is found * @since 31.0.0 */ - public function getRemoteSignatory(string $remote): ?ISignatory { + public function getRemoteSignatory(string $remote): ?Signatory { try { return $this->getRemoteSignatoryFromHost($remote); } catch (OCMProviderException $e) { @@ -142,11 +141,11 @@ public function getRemoteSignatory(string $remote): ?ISignatory { * * @param string $host * - * @return ISignatory|null + * @return Signatory|null * @throws OCMProviderException on fail to discover ocm services * @since 31.0.0 */ - public function getRemoteSignatoryFromHost(string $host): ?ISignatory { + public function getRemoteSignatoryFromHost(string $host): ?Signatory { $ocmProvider = $this->ocmDiscoveryService->discover($host, true); $signatory = $ocmProvider->getSignatory(); diff --git a/lib/private/Security/IdentityProof/Manager.php b/lib/private/Security/IdentityProof/Manager.php index de0b3fe6bd16e..935c18bb81dc7 100644 --- a/lib/private/Security/IdentityProof/Manager.php +++ b/lib/private/Security/IdentityProof/Manager.php @@ -133,8 +133,8 @@ public function getSystemKey(): Key { public function hasAppKey(string $app, string $name): bool { $id = $this->generateAppKeyId($app, $name); try { - $this->appData->getFolder($id); - return true; + $folder = $this->appData->getFolder($id); + return ($folder->fileExists('public') && $folder->fileExists('private')); } catch (NotFoundException) { return false; } @@ -151,11 +151,11 @@ public function generateAppKey(string $app, string $name, array $options = []): public function deleteAppKey(string $app, string $name): bool { try { $folder = $this->appData->getFolder($this->generateAppKeyId($app, $name)); + $folder->delete(); + return true; } catch (NotFoundException) { return false; } - $folder->delete(); - return true; } private function generateAppKeyId(string $app, string $name): string { diff --git a/lib/private/Security/Signature/Db/SignatoryMapper.php b/lib/private/Security/Signature/Db/SignatoryMapper.php new file mode 100644 index 0000000000000..47b7932054824 --- /dev/null +++ b/lib/private/Security/Signature/Db/SignatoryMapper.php @@ -0,0 +1,114 @@ + + */ +class SignatoryMapper extends QBMapper { + public const TABLE = 'sec_signatory'; + + public function __construct( + IDBConnection $db, + ) { + parent::__construct($db, self::TABLE, Signatory::class); + } + + /** + * + */ + public function getByHost(string $host, string $account = ''): Signatory { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('host', $qb->createNamedParameter($host))) + ->andWhere($qb->expr()->eq('account', $qb->createNamedParameter($account))); + + try { + return $this->findEntity($qb); + } catch (DoesNotExistException) { + throw new SignatoryNotFoundException('no signatory found'); + } + } + + /** + */ + public function getByKeyId(string $keyId): Signatory { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId)))); + + try { + return $this->findEntity($qb); + } catch (DoesNotExistException) { + throw new SignatoryNotFoundException('no signatory found'); + } + } + + /** + * @param string $keyId + * + * @return int + * @throws Exception + */ + public function deleteByKeyId(string $keyId): int { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId)))); + + return $qb->executeStatement(); + } + + /** + * @param Signatory $signatory + * + * @return int + */ + public function updateMetadata(Signatory $signatory): int { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->getTableName()) + ->set('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata()))) + ->set('last_updated', $qb->createNamedParameter(time())); + $qb->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId())))); + + return $qb->executeStatement(); + } + + /** + * @param Signatory $signator + */ + public function updatePublicKey(Signatory $signatory): int { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->getTableName()) + ->set('signatory', $qb->createNamedParameter($signatory->getPublicKey())) + ->set('last_updated', $qb->createNamedParameter(time())); + $qb->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId())))); + + return $qb->executeStatement(); + } + + /** + * returns a hash version for keyId for better index in the database + * + * @param string $keyId + * + * @return string + */ + private function hashKeyId(string $keyId): string { + return hash('sha256', $keyId); + } +} diff --git a/lib/private/Security/Signature/Model/IncomingSignedRequest.php b/lib/private/Security/Signature/Model/IncomingSignedRequest.php index 77914d1e3b2dc..a2bf9fd0bcce4 100644 --- a/lib/private/Security/Signature/Model/IncomingSignedRequest.php +++ b/lib/private/Security/Signature/Model/IncomingSignedRequest.php @@ -14,9 +14,9 @@ use NCU\Security\Signature\Exceptions\SignatoryException; use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException; use NCU\Security\Signature\Exceptions\SignatureNotFoundException; +use NCU\Security\Signature\IIncomingSignedRequest; use NCU\Security\Signature\ISignatureManager; -use NCU\Security\Signature\Model\IIncomingSignedRequest; -use NCU\Security\Signature\Model\ISignatory; +use NCU\Security\Signature\Model\Signatory; use OC\Security\Signature\SignatureManager; use OCP\IRequest; @@ -141,7 +141,7 @@ public function getRequest(): IRequest { /** * @inheritDoc * - * @param ISignatory $signatory + * @param Signatory $signatory * * @return $this * @throws IdentityNotFoundException @@ -149,7 +149,7 @@ public function getRequest(): IRequest { * @throws SignatoryException * @since 31.0.0 */ - public function setSignatory(ISignatory $signatory): self { + public function setSignatory(Signatory $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'); diff --git a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php index d2d5b95e7b644..611a968d1d23a 100644 --- a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php +++ b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php @@ -9,10 +9,10 @@ namespace OC\Security\Signature\Model; use JsonSerializable; +use NCU\Security\Signature\Enum\SignatureAlgorithm; +use NCU\Security\Signature\IOutgoingSignedRequest; 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; /** @@ -53,7 +53,6 @@ public function __construct( $signing = $headerList = []; foreach ($headers as $element => $value) { - $value = $headers[$element]; $signing[] = $element . ': ' . $value; $headerList[] = $element; if ($element !== '(request-target)') { diff --git a/lib/private/Security/Signature/Model/Signatory.php b/lib/private/Security/Signature/Model/Signatory.php deleted file mode 100644 index b28d2c0415f09..0000000000000 --- a/lib/private/Security/Signature/Model/Signatory.php +++ /dev/null @@ -1,147 +0,0 @@ -keyId = $keyId; - } - } - - public function setProviderId(string $providerId): self { - $this->providerId = $providerId; - return $this; - } - - public function getProviderId(): string { - return $this->providerId; - } - - public function setAccount(string $account): self { - $this->account = $account; - return $this; - } - - public function getAccount(): string { - return $this->account; - } - - public function getKeyId(): string { - return $this->keyId; - } - - public function getPublicKey(): string { - return $this->publicKey; - } - - public function getPrivateKey(): string { - return $this->privateKey; - } - - public function setMetadata(array $metadata): self { - $this->metadata = $metadata; - return $this; - } - - public function getMetadata(): array { - return $this->metadata; - } - - public function setMetaValue(string $key, string|int $value): self { - $this->metadata[$key] = $value; - return $this; - } - - public function setType(SignatoryType $type): self { - $this->type = $type; - return $this; - } - public function getType(): SignatoryType { - return $this->type; - } - - public function setStatus(SignatoryStatus $status): self { - $this->status = $status; - return $this; - } - - public function getStatus(): SignatoryStatus { - return $this->status; - } - - public function setCreation(int $creation): self { - $this->creation = $creation; - return $this; - } - - public function getCreation(): int { - return $this->creation; - } - - public function setLastUpdated(int $lastUpdated): self { - $this->lastUpdated = $lastUpdated; - return $this; - } - - public function getLastUpdated(): int { - return $this->lastUpdated; - } - - public function importFromDatabase(array $row): self { - $this->setProviderId($row['provider_id'] ?? '') - ->setAccount($row['account'] ?? '') - ->setMetadata(json_decode($row['metadata'], true) ?? []) - ->setType(SignatoryType::from($row['type'] ?? 9)) - ->setStatus(SignatoryStatus::from($row['status'] ?? 1)) - ->setCreation($row['creation'] ?? 0) - ->setLastUpdated($row['last_updated'] ?? 0); - return $this; - } - - public function jsonSerialize(): array { - return [ - 'keyId' => $this->getKeyId(), - 'publicKeyPem' => $this->getPublicKey() - ]; - } -} diff --git a/lib/private/Security/Signature/Model/SignedRequest.php b/lib/private/Security/Signature/Model/SignedRequest.php index 56853ebade3c5..e974942a9252d 100644 --- a/lib/private/Security/Signature/Model/SignedRequest.php +++ b/lib/private/Security/Signature/Model/SignedRequest.php @@ -11,8 +11,8 @@ 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; +use NCU\Security\Signature\ISignedRequest; +use NCU\Security\Signature\Model\Signatory; /** * @inheritDoc @@ -24,7 +24,7 @@ class SignedRequest implements ISignedRequest, JsonSerializable { private array $signatureElements = []; private string $clearSignature = ''; private string $signedSignature = ''; - private ?ISignatory $signatory = null; + private ?Signatory $signatory = null; public function __construct( private readonly string $body, @@ -140,11 +140,11 @@ public function getSignedSignature(): string { /** * @inheritDoc * - * @param ISignatory $signatory + * @param Signatory $signatory * @return ISignedRequest * @since 31.0.0 */ - public function setSignatory(ISignatory $signatory): ISignedRequest { + public function setSignatory(Signatory $signatory): ISignedRequest { $this->signatory = $signatory; return $this; } @@ -152,11 +152,11 @@ public function setSignatory(ISignatory $signatory): ISignedRequest { /** * @inheritDoc * - * @return ISignatory + * @return Signatory * @throws SignatoryNotFoundException * @since 31.0.0 */ - public function getSignatory(): ISignatory { + public function getSignatory(): Signatory { if ($this->signatory === null) { throw new SignatoryNotFoundException(); } diff --git a/lib/private/Security/Signature/SignatureManager.php b/lib/private/Security/Signature/SignatureManager.php index 2d895b465abeb..e4f4a2ed1a612 100644 --- a/lib/private/Security/Signature/SignatureManager.php +++ b/lib/private/Security/Signature/SignatureManager.php @@ -8,6 +8,8 @@ namespace OC\Security\Signature; +use NCU\Security\Signature\Enum\SignatoryType; +use NCU\Security\Signature\Enum\SignatureAlgorithm; use NCU\Security\Signature\Exceptions\IdentityNotFoundException; use NCU\Security\Signature\Exceptions\IncomingRequestException; use NCU\Security\Signature\Exceptions\InvalidKeyOriginException; @@ -18,19 +20,16 @@ use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException; use NCU\Security\Signature\Exceptions\SignatureException; use NCU\Security\Signature\Exceptions\SignatureNotFoundException; +use NCU\Security\Signature\IIncomingSignedRequest; +use NCU\Security\Signature\IOutgoingSignedRequest; use NCU\Security\Signature\ISignatoryManager; use NCU\Security\Signature\ISignatureManager; -use NCU\Security\Signature\Model\IIncomingSignedRequest; -use NCU\Security\Signature\Model\IOutgoingSignedRequest; -use NCU\Security\Signature\Model\ISignatory; -use NCU\Security\Signature\Model\SignatoryType; -use NCU\Security\Signature\SignatureAlgorithm; +use NCU\Security\Signature\Model\Signatory; +use OC\Security\Signature\Db\SignatoryMapper; use OC\Security\Signature\Model\IncomingSignedRequest; use OC\Security\Signature\Model\OutgoingSignedRequest; -use OC\Security\Signature\Model\Signatory; use OCP\DB\Exception as DBException; use OCP\IAppConfig; -use OCP\IDBConnection; use OCP\IRequest; use Psr\Log\LoggerInterface; @@ -69,13 +68,12 @@ class SignatureManager implements ISignatureManager { 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( private readonly IRequest $request, - private readonly IDBConnection $connection, + private readonly SignatoryMapper $mapper, private readonly IAppConfig $appConfig, private readonly LoggerInterface $logger, ) { @@ -307,31 +305,12 @@ public function signOutgoingRequestIClientPayload( * @param string $account linked account, should be used when multiple signature can exist for the same * host * - * @return ISignatory + * @return Signatory * @throws SignatoryNotFoundException if entry does not exist in local database * @since 31.0.0 */ - public function searchSignatory(string $host, string $account = ''): ISignatory { - $qb = $this->connection->getQueryBuilder(); - $qb->select( - 'id', 'provider_id', 'host', 'account', 'key_id', 'key_id_sum', 'public_key', 'metadata', 'type', - 'status', 'creation', 'last_updated' - ); - $qb->from(self::TABLE_SIGNATORIES); - $qb->where($qb->expr()->eq('host', $qb->createNamedParameter($host))); - $qb->andWhere($qb->expr()->eq('account', $qb->createNamedParameter($account))); - - $result = $qb->executeQuery(); - $row = $result->fetch(); - $result->closeCursor(); - - if (!$row) { - throw new SignatoryNotFoundException('no signatory found'); - } - - $signature = new Signatory($row['key_id'], $row['public_key']); - - return $signature->importFromDatabase($row); + public function getSignatory(string $host, string $account = ''): Signatory { + return $this->mapper->getByHost($host, $account); } @@ -386,7 +365,7 @@ public function extractIdentityFromUri(string $uri): string { * @param ISignatoryManager $signatoryManager * @param IIncomingSignedRequest $signedRequest * - * @return ISignatory + * @return Signatory * @throws InvalidKeyOriginException * @throws SignatoryNotFoundException * @see ISignatoryManager::getRemoteSignatory @@ -394,7 +373,7 @@ public function extractIdentityFromUri(string $uri): string { private function getSaneRemoteSignatory( ISignatoryManager $signatoryManager, IIncomingSignedRequest $signedRequest, - ): ISignatory { + ): Signatory { $signatory = $signatoryManager->getRemoteSignatory($signedRequest->getOrigin()); if ($signatory === null) { throw new SignatoryNotFoundException('empty result from getRemoteSignatory'); @@ -406,14 +385,14 @@ private function getSaneRemoteSignatory( } catch (SignatureElementNotFoundException) { throw new InvalidKeyOriginException('missing keyId'); } + $signatory->setProviderId($signatoryManager->getProviderId()); - return $signatory->setProviderId($signatoryManager->getProviderId()); + return $signatory; } /** * @param IIncomingSignedRequest $signedRequest * - * @return void * @throws SignatureException * @throws SignatoryNotFoundException */ @@ -477,36 +456,17 @@ private function verifyString( /** * @param string $keyId * - * @return ISignatory + * @return Signatory * @throws SignatoryNotFoundException */ - private function getStoredSignatory(string $keyId): ISignatory { - $qb = $this->connection->getQueryBuilder(); - $qb->select( - 'id', 'provider_id', 'host', 'account', 'key_id', 'key_id_sum', 'public_key', 'metadata', 'type', - 'status', 'creation', 'last_updated' - ); - $qb->from(self::TABLE_SIGNATORIES); - $qb->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId)))); - - $result = $qb->executeQuery(); - $row = $result->fetch(); - $result->closeCursor(); - - if (!$row) { - throw new SignatoryNotFoundException('no signatory found in local'); - } - - $signature = new Signatory($row['key_id'], $row['public_key']); - $signature->importFromDatabase($row); - - return $signature; + private function getStoredSignatory(string $keyId): Signatory { + return $this->mapper->getByKeyId($keyId); } /** - * @param ISignatory $signatory + * @param Signatory $signatory */ - private function storeSignatory(ISignatory $signatory): void { + private function storeSignatory(Signatory $signatory): void { try { $this->insertSignatory($signatory); } catch (DBException $e) { @@ -524,34 +484,20 @@ private function storeSignatory(ISignatory $signatory): void { } /** - * @param ISignatory $signatory + * @param Signatory $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->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()))) - ->setValue('public_key', $qb->createNamedParameter($signatory->getPublicKey())) - ->setValue('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata()))) - ->setValue('type', $qb->createNamedParameter($signatory->getType()->value)) - ->setValue('status', $qb->createNamedParameter($signatory->getStatus()->value)) - ->setValue('creation', $qb->createNamedParameter(time())) - ->setValue('last_updated', $qb->createNamedParameter(time())); - - $qb->executeStatement(); + private function insertSignatory(Signatory $signatory): void { + $this->mapper->insert($signatory); } /** - * @param ISignatory $signatory + * @param Signatory $signatory * * @throws SignatoryNotFoundException * @throws SignatoryConflictException */ - private function updateKnownSignatory(ISignatory $signatory): void { + private function updateKnownSignatory(Signatory $signatory): void { $knownSignatory = $this->getStoredSignatory($signatory->getKeyId()); switch ($signatory->getType()) { case SignatoryType::FORGIVABLE: @@ -577,12 +523,12 @@ private function updateKnownSignatory(ISignatory $signatory): void { /** * This is called when a remote signatory does not exist anymore * - * @param ISignatory|null $knownSignatory NULL is not known + * @param Signatory|null $knownSignatory NULL is not known * * @throws SignatoryConflictException * @throws SignatoryNotFoundException */ - private function manageDeprecatedSignatory(?ISignatory $knownSignatory): void { + private function manageDeprecatedSignatory(?Signatory $knownSignatory): void { switch ($knownSignatory?->getType()) { case null: // unknown in local database case SignatoryType::FORGIVABLE: // who cares ? @@ -600,38 +546,15 @@ private function manageDeprecatedSignatory(?ISignatory $knownSignatory): void { } - private function updateSignatoryPublicKey(ISignatory $signatory): void { - $qb = $this->connection->getQueryBuilder(); - $qb->update(self::TABLE_SIGNATORIES) - ->set('signatory', $qb->createNamedParameter($signatory->getPublicKey())) - ->set('last_updated', $qb->createNamedParameter(time())); - - $qb->where( - $qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId()))) - ); - $qb->executeStatement(); + private function updateSignatoryPublicKey(Signatory $signatory): void { + $this->mapper->updatePublicKey($signatory); } - private function updateSignatoryMetadata(ISignatory $signatory): void { - $qb = $this->connection->getQueryBuilder(); - $qb->update(self::TABLE_SIGNATORIES) - ->set('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata()))) - ->set('last_updated', $qb->createNamedParameter(time())); - - $qb->where( - $qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId()))) - ); - $qb->executeStatement(); + private function updateSignatoryMetadata(Signatory $signatory): void { + $this->mapper->updateMetadata($signatory); } private function deleteSignatory(string $keyId): void { - $qb = $this->connection->getQueryBuilder(); - $qb->delete(self::TABLE_SIGNATORIES) - ->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId)))); - $qb->executeStatement(); - } - - private function hashKeyId(string $keyId): string { - return hash('sha256', $keyId); + $this->mapper->deleteByKeyId($keyId); } } diff --git a/lib/public/OCM/IOCMProvider.php b/lib/public/OCM/IOCMProvider.php index dd36a1c605715..cd2a59ebd5e0a 100644 --- a/lib/public/OCM/IOCMProvider.php +++ b/lib/public/OCM/IOCMProvider.php @@ -6,11 +6,10 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ - namespace OCP\OCM; use JsonSerializable; -use NCU\Security\Signature\Model\ISignatory; +use NCU\Security\Signature\Model\Signatory; use OCP\OCM\Exceptions\OCMArgumentException; use OCP\OCM\Exceptions\OCMProviderException; @@ -124,18 +123,18 @@ public function extractProtocolEntry(string $resourceName, string $protocol): st /** * store signatory (public/private key pair) to sign outgoing/incoming request * - * @param ISignatory $signatory + * @param Signatory $signatory * @since 31.0.0 */ - public function setSignatory(ISignatory $signatory): void; + public function setSignatory(Signatory $signatory): void; /** * signatory (public/private key pair) used to sign outgoing/incoming request * - * @return ISignatory|null returns null if no ISignatory available + * @return Signatory|null returns null if no Signatory available * @since 31.0.0 */ - public function getSignatory(): ?ISignatory; + public function getSignatory(): ?Signatory; /** * import data from an array @@ -153,7 +152,7 @@ public function import(array $data): static; * enabled: bool, * apiVersion: '1.0-proposal1', * endPoint: string, - * publicKey: ISignatory|null, + * publicKey: Signatory|null, * resourceTypes: list, diff --git a/lib/unstable/Security/Signature/Model/SignatoryStatus.php b/lib/unstable/Security/Signature/Enum/SignatoryStatus.php similarity index 92% rename from lib/unstable/Security/Signature/Model/SignatoryStatus.php rename to lib/unstable/Security/Signature/Enum/SignatoryStatus.php index 4174102beaec5..9c77cf9bbc2b1 100644 --- a/lib/unstable/Security/Signature/Model/SignatoryStatus.php +++ b/lib/unstable/Security/Signature/Enum/SignatoryStatus.php @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace NCU\Security\Signature\Model; +namespace NCU\Security\Signature\Enum; /** * current status of signatory. is it trustable or not ? diff --git a/lib/unstable/Security/Signature/Model/SignatoryType.php b/lib/unstable/Security/Signature/Enum/SignatoryType.php similarity index 87% rename from lib/unstable/Security/Signature/Model/SignatoryType.php rename to lib/unstable/Security/Signature/Enum/SignatoryType.php index 652bee2a970fd..86a766d2aa084 100644 --- a/lib/unstable/Security/Signature/Model/SignatoryType.php +++ b/lib/unstable/Security/Signature/Enum/SignatoryType.php @@ -3,15 +3,15 @@ declare(strict_types=1); /** - * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace NCU\Security\Signature\Model; +namespace NCU\Security\Signature\Enum; /** * type of link between local and remote instance * - * - FORGIVABLE = the keypair can be deleted and refreshed anytime and silently + * - FORGIVABLE = the keypair can be deleted and refreshed anytime; silently * - REFRESHABLE = the keypair can be refreshed but a notice will be generated * - TRUSTED = any changes of keypair will require human interaction, warning will be issued * - STATIC = error will be issued on conflict, assume keypair cannot be reset. diff --git a/lib/unstable/Security/Signature/SignatureAlgorithm.php b/lib/unstable/Security/Signature/Enum/SignatureAlgorithm.php similarity index 91% rename from lib/unstable/Security/Signature/SignatureAlgorithm.php rename to lib/unstable/Security/Signature/Enum/SignatureAlgorithm.php index c0a5a0c6c7a96..94996d17bd5ed 100644 --- a/lib/unstable/Security/Signature/SignatureAlgorithm.php +++ b/lib/unstable/Security/Signature/Enum/SignatureAlgorithm.php @@ -6,7 +6,7 @@ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace NCU\Security\Signature; +namespace NCU\Security\Signature\Enum; /** * list of available algorithm when signing payload diff --git a/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php b/lib/unstable/Security/Signature/IIncomingSignedRequest.php similarity index 94% rename from lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php rename to lib/unstable/Security/Signature/IIncomingSignedRequest.php index 3e2ebb22a5f6c..f6ab98dc1b3d1 100644 --- a/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php +++ b/lib/unstable/Security/Signature/IIncomingSignedRequest.php @@ -6,10 +6,9 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace NCU\Security\Signature\Model; +namespace NCU\Security\Signature; use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException; -use NCU\Security\Signature\ISignatureManager; use OCP\IRequest; /** diff --git a/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php b/lib/unstable/Security/Signature/IOutgoingSignedRequest.php similarity index 94% rename from lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php rename to lib/unstable/Security/Signature/IOutgoingSignedRequest.php index 3c9445af74576..7b5df25cbd6cd 100644 --- a/lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php +++ b/lib/unstable/Security/Signature/IOutgoingSignedRequest.php @@ -6,10 +6,9 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace NCU\Security\Signature\Model; +namespace NCU\Security\Signature; -use NCU\Security\Signature\ISignatureManager; -use NCU\Security\Signature\SignatureAlgorithm; +use NCU\Security\Signature\Enum\SignatureAlgorithm; /** * extends ISignedRequest to add info requested at the generation of the signature diff --git a/lib/unstable/Security/Signature/ISignatoryManager.php b/lib/unstable/Security/Signature/ISignatoryManager.php index 19ba83a4206dd..20133de4c9ce3 100644 --- a/lib/unstable/Security/Signature/ISignatoryManager.php +++ b/lib/unstable/Security/Signature/ISignatoryManager.php @@ -8,7 +8,7 @@ */ namespace NCU\Security\Signature; -use NCU\Security\Signature\Model\ISignatory; +use NCU\Security\Signature\Model\Signatory; /** * ISignatoryManager contains a group of method that will help @@ -51,10 +51,10 @@ public function getOptions(): array; * * Used to sign outgoing request * - * @return ISignatory + * @return Signatory * @since 31.0.0 */ - public function getLocalSignatory(): ISignatory; + public function getLocalSignatory(): Signatory; /** * retrieve details and generate signatory from remote instance. @@ -64,8 +64,8 @@ public function getLocalSignatory(): ISignatory; * * @param string $remote * - * @return ISignatory|null must be NULL if no signatory is found + * @return Signatory|null must be NULL if no signatory is found * @since 31.0.0 */ - public function getRemoteSignatory(string $remote): ?ISignatory; + public function getRemoteSignatory(string $remote): ?Signatory; } diff --git a/lib/unstable/Security/Signature/ISignatureManager.php b/lib/unstable/Security/Signature/ISignatureManager.php index 1969b970aa68d..c614a16cd92aa 100644 --- a/lib/unstable/Security/Signature/ISignatureManager.php +++ b/lib/unstable/Security/Signature/ISignatureManager.php @@ -13,9 +13,7 @@ use NCU\Security\Signature\Exceptions\SignatoryNotFoundException; use NCU\Security\Signature\Exceptions\SignatureException; use NCU\Security\Signature\Exceptions\SignatureNotFoundException; -use NCU\Security\Signature\Model\IIncomingSignedRequest; -use NCU\Security\Signature\Model\IOutgoingSignedRequest; -use NCU\Security\Signature\Model\ISignatory; +use NCU\Security\Signature\Model\Signatory; /** * ISignatureManager is a service integrated to core that provide tools @@ -99,11 +97,11 @@ public function signOutgoingRequestIClientPayload(ISignatoryManager $signatoryMa * @param string $host remote host * @param string $account linked account, should be used when multiple signature can exist for the same host * - * @return ISignatory + * @return Signatory * @throws SignatoryNotFoundException if entry does not exist in local database * @since 31.0.0 */ - public function searchSignatory(string $host, string $account = ''): ISignatory; + public function getSignatory(string $host, string $account = ''): Signatory; /** * returns a fully formatted keyId, based on a fix hostname and path diff --git a/lib/unstable/Security/Signature/Model/ISignedRequest.php b/lib/unstable/Security/Signature/ISignedRequest.php similarity index 91% rename from lib/unstable/Security/Signature/Model/ISignedRequest.php rename to lib/unstable/Security/Signature/ISignedRequest.php index 76c033970fe84..be52b06cf6682 100644 --- a/lib/unstable/Security/Signature/Model/ISignedRequest.php +++ b/lib/unstable/Security/Signature/ISignedRequest.php @@ -6,10 +6,11 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -namespace NCU\Security\Signature\Model; +namespace NCU\Security\Signature; use NCU\Security\Signature\Exceptions\SignatoryNotFoundException; use NCU\Security\Signature\Exceptions\SignatureElementNotFoundException; +use NCU\Security\Signature\Model\Signatory; /** * model that store data related to a possible signature. @@ -102,20 +103,20 @@ public function getSignedSignature(): string; /** * set the signatory, containing keys and details, related to this request * - * @param ISignatory $signatory + * @param Signatory $signatory * @return ISignedRequest * @since 31.0.0 */ - public function setSignatory(ISignatory $signatory): ISignedRequest; + public function setSignatory(Signatory $signatory): ISignedRequest; /** * get the signatory, containing keys and details, related to this request * - * @return ISignatory + * @return Signatory * @throws SignatoryNotFoundException * @since 31.0.0 */ - public function getSignatory(): ISignatory; + public function getSignatory(): Signatory; /** * returns if a signatory related to this request have been found and defined diff --git a/lib/unstable/Security/Signature/Model/ISignatory.php b/lib/unstable/Security/Signature/Model/ISignatory.php deleted file mode 100644 index e77b77e66e52a..0000000000000 --- a/lib/unstable/Security/Signature/Model/ISignatory.php +++ /dev/null @@ -1,160 +0,0 @@ -addType('providerId', 'string'); + $this->addType('host', 'string'); + $this->addType('account', 'string'); + $this->addType('keyId', 'string'); + $this->addType('keyIdSum', 'string'); + $this->addType('publicKey', 'string'); + $this->addType('metadata', 'json'); + $this->addType('type', 'integer'); + $this->addType('status', 'integer'); + $this->addType('creation', 'integer'); + $this->addType('lastUpdated', 'integer'); + + $this->setKeyId($keyId); + } + + /** + * @param string $keyId + * + * @since 31.0.0 + */ + public function setKeyId(string $keyId): void { + // if set as local (for current instance), we apply some filters. + if ($this->local) { + // to avoid conflict with duplicate key pairs (ie generated url from the occ command), we enforce https as prefix + if (str_starts_with($keyId, 'http://')) { + $keyId = 'https://' . substr($keyId, 7); + } + + // removing /index.php from generated url + $path = parse_url($keyId, PHP_URL_PATH); + if (str_starts_with($path, '/index.php/')) { + $pos = strpos($keyId, '/index.php'); + if ($pos !== false) { + $keyId = substr_replace($keyId, '', $pos, 10); + } + } + } + $this->keyId = $keyId; + $this->keyIdSum = hash('sha256', $keyId); + } + + /** + * @param SignatoryType $type + * @since 31.0.0 + */ + public function setType(SignatoryType $type): void { + $this->type = $type->value; + } + + /** + * @return SignatoryType + * @since 31.0.0 + */ + public function getType(): SignatoryType { + return SignatoryType::from($this->type); + } + + /** + * @param SignatoryStatus $status + * @since 31.0.0 + */ + public function setStatus(SignatoryStatus $status): void { + $this->status = $status->value; + } + + /** + * @return SignatoryStatus + * @since 31.0.0 + */ + public function getStatus(): SignatoryStatus { + return SignatoryStatus::from($this->status); + } + + /** + * update an entry in metadata + * + * @param string $key + * @param string|int|float|bool|array $value + * @since 31.0.0 + */ + public function setMetaValue(string $key, string|int|float|bool|array $value): void { + $this->metadata[$key] = $value; + } + + /** + * @return array + * @since 31.0.0 + */ + public function jsonSerialize(): array { + return [ + 'keyId' => $this->getKeyId(), + 'publicKeyPem' => $this->getPublicKey() + ]; + } +}