]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat(ocm): signing ocm requests
authorMaxence Lange <maxence@artificial-owl.com>
Tue, 12 Nov 2024 22:07:16 +0000 (21:07 -0100)
committerMaxence Lange <maxence@artificial-owl.com>
Thu, 21 Nov 2024 10:26:28 +0000 (09:26 -0100)
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
47 files changed:
apps/cloud_federation_api/lib/Capabilities.php
apps/cloud_federation_api/lib/Controller/RequestHandlerController.php
apps/cloud_federation_api/openapi.json
apps/files_sharing/lib/External/Storage.php
build/integration/federation_features/cleanup-remote-storage.feature
core/Controller/OCMController.php
core/Migrations/Version31000Date20240101084401.php [new file with mode: 0644]
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/Federation/CloudFederationProviderManager.php
lib/private/Files/Storage/DAV.php
lib/private/OCM/Model/OCMProvider.php
lib/private/OCM/OCMDiscoveryService.php
lib/private/OCM/OCMSignatoryManager.php [new file with mode: 0644]
lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php [new file with mode: 0644]
lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php [new file with mode: 0644]
lib/private/Security/Signature/Model/IncomingSignedRequest.php [new file with mode: 0644]
lib/private/Security/Signature/Model/OutgoingSignedRequest.php [new file with mode: 0644]
lib/private/Security/Signature/Model/Signatory.php [new file with mode: 0644]
lib/private/Security/Signature/Model/SignedRequest.php [new file with mode: 0644]
lib/private/Security/Signature/SignatureManager.php [new file with mode: 0644]
lib/private/Server.php
lib/public/OCM/IOCMProvider.php
lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php [new file with mode: 0644]
lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairException.php [new file with mode: 0644]
lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairNotFoundException.php [new file with mode: 0644]
lib/unstable/Security/PublicPrivateKeyPairs/IKeyPairManager.php [new file with mode: 0644]
lib/unstable/Security/PublicPrivateKeyPairs/Model/IKeyPair.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/IdentityNotFoundException.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/IncomingRequestException.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/InvalidKeyOriginException.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/InvalidSignatureException.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/SignatoryConflictException.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/SignatoryException.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/SignatoryNotFoundException.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/SignatureException.php [new file with mode: 0644]
lib/unstable/Security/Signature/Exceptions/SignatureNotFoundException.php [new file with mode: 0644]
lib/unstable/Security/Signature/ISignatoryManager.php [new file with mode: 0644]
lib/unstable/Security/Signature/ISignatureManager.php [new file with mode: 0644]
lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php [new file with mode: 0644]
lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php [new file with mode: 0644]
lib/unstable/Security/Signature/Model/ISignatory.php [new file with mode: 0644]
lib/unstable/Security/Signature/Model/ISignedRequest.php [new file with mode: 0644]
lib/unstable/Security/Signature/Model/SignatoryStatus.php [new file with mode: 0644]
lib/unstable/Security/Signature/Model/SignatoryType.php [new file with mode: 0644]
lib/unstable/Security/Signature/SignatureAlgorithm.php [new file with mode: 0644]

index ca4ea928cb823a3380abac2051b901ae06ab7adb..1910a03233791ff12679982b4c75b3e7b4eab741 100644 (file)
@@ -6,20 +6,27 @@ declare(strict_types=1);
  * 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 @@ class Capabilities implements ICapability {
         *
         * @return array{
         *     ocm: array{
+        *         apiVersion: '1.0-proposal1',
         *         enabled: bool,
-        *         apiVersion: string,
         *         endPoint: string,
+        *         publicKey: array{
+        *             keyId: string,
+        *             publicKeyPem: string,
+        *         },
         *         resourceTypes: list<array{
         *             name: string,
         *             shareTypes: list<string>,
         *             protocols: array<string, string>
-        *           }>,
-        *       },
+        *         }>,
+        *         version: string
+        *     }
         * }
         * @throws OCMArgumentException
         */
@@ -60,6 +72,17 @@ class Capabilities implements ICapability {
 
                $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)];
        }
 }
index a7b17f010cee9e40e4fc678c600302248a94e3f0..db7f81d55967520f0a0a38ba2aa6ea2206cded41 100644 (file)
@@ -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\Exceptions\ProviderDoesNotExistsException;
 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 @@ class RequestHandlerController extends Controller {
                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 @@ class RequestHandlerController extends Controller {
        #[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 @@ class RequestHandlerController extends Controller {
        #[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 @@ class RequestHandlerController extends Controller {
 
                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');
+       }
 }
index d15c7cef8130b755d56d629b1bf9f0d3be6ed5d5..1c69ea2d08359f9e45b2c3a8a417dc31787ed4da 100644 (file)
                     "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": {
                                         }
                                     }
                                 }
+                            },
+                            "version": {
+                                "type": "string"
                             }
                         }
                     }
index dbf7d2af6e5ce1dc2c90f47ad65a33e1dae2bf85..bacd2b3f7cf998ffed7151d80d88a7d6c3ece185 100644 (file)
@@ -88,6 +88,7 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage,
                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'],
index 6339edb60b66ec5849b188ef59e8883bfa17bc53..a017b59bcf4cfdde74f25c6500a3881d449f833b 100644 (file)
@@ -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
index 59529b66e121abf88e6a897652eb3f1556105d58..f15a4a56779966578e2eea861139ed94ff690439 100644 (file)
@@ -17,7 +17,7 @@ use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
 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 @@ use Psr\Log\LoggerInterface;
 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 @@ class OCMController extends Controller {
        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 (file)
index 0000000..60792dc
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Core\Migrations;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\Attributes\AddIndex;
+use OCP\Migration\Attributes\CreateTable;
+use OCP\Migration\Attributes\IndexType;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * @since 31.0.0
+ */
+#[CreateTable(
+       table: 'sec_signatory',
+       columns: ['id', 'key_id_sum', 'key_id', 'host', 'provider_id', 'account', 'public_key', 'metadata', 'type', 'status', 'creation', 'last_updated'],
+       description: 'new table to store remove public/private key pairs'
+)]
+#[AddIndex(
+       table: 'sec_signatory',
+       type: IndexType::PRIMARY
+)]
+#[AddIndex(
+       table: 'sec_signatory',
+       type: IndexType::UNIQUE,
+       description: 'confirm uniqueness per host, provider and account'
+)]
+#[AddIndex(
+       table: 'sec_signatory',
+       type: IndexType::INDEX,
+       description: 'to search on key and provider'
+)]
+class Version31000Date20240101084401 extends SimpleMigrationStep {
+       public function description(): string {
+               return "creating new table 'sec_signatory' to store remote signatories";
+       }
+
+       public function name(): string {
+               return 'create sec_signatory';
+       }
+
+       /**
+        * @param IOutput $output
+        * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+        * @param array $options
+        * @return null|ISchemaWrapper
+        */
+       public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+               /** @var ISchemaWrapper $schema */
+               $schema = $schemaClosure();
+
+               if (!$schema->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;
+       }
+}
index cc6b8be326c4964f7ca15e9214a23c25db590e3f..b04f938d2366afa831154e5deac492539ac0b29e 100644 (file)
@@ -1392,7 +1392,6 @@ return array(
     'OC\\Core\\Migrations\\Version30000Date20240814180800' => $baseDir . '/core/Migrations/Version30000Date20240814180800.php',
     'OC\\Core\\Migrations\\Version30000Date20240815080800' => $baseDir . '/core/Migrations/Version30000Date20240815080800.php',
     'OC\\Core\\Migrations\\Version30000Date20240906095113' => $baseDir . '/core/Migrations/Version30000Date20240906095113.php',
-    'OC\\Core\\Migrations\\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 @@ return array(
     '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 @@ return array(
     '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 @@ return array(
     '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',
index 54959bc6b91646e2729f90956ecc9b1c3868eab1..dea35ecdfcd114bfaa4d7196a0c50614c602c9db 100644 (file)
@@ -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',
index bf7648d472b4f136c7268af5a0f4ba9ce3de0203..eeb161c3b2514e6f73d30ce3db7337208b4a0740 100644 (file)
@@ -8,7 +8,9 @@ declare(strict_types=1);
  */
 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\ICloudFederationShare;
 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 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
 
                $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 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
 
                $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 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
 
                $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 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
 
                $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 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
        }
 
        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;
        }
 }
index 10670d6331a0efefe44c7006bb5fa0f097e10702..597b3f474888894c6b9a15730d9bbcdc714282bc 100644 (file)
@@ -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 @@ class DAV extends Common {
                        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 @@ class DAV extends Common {
                        }
                }
 
+               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']);
index 73002ae668de1a23e0e30b43f9c4f19067b9e4ad..cd4e9c49c3b29943628a5b4e6e5ce17649c4bed5 100644 (file)
@@ -9,6 +9,8 @@ declare(strict_types=1);
 
 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 @@ class OCMProvider implements IOCMProvider {
                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 @@ class OCMProvider implements IOCMProvider {
         */
        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 @@ class OCMProvider implements IOCMProvider {
                }
                $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 @@ class OCMProvider implements IOCMProvider {
                return ($this->getApiVersion() !== '' && $this->getEndPoint() !== '');
        }
 
-
        /**
         * @return array{
-        *     enabled: bool,
-        *     apiVersion: string,
-        *     endPoint: string,
-        *     resourceTypes: list<array{
-        *         name: string,
-        *         shareTypes: list<string>,
-        *         protocols: array<string, string>
-        *     }>,
-        * }
+        *      enabled: bool,
+        *      apiVersion: '1.0-proposal1',
+        *      endPoint: string,
+        *      publicKey: ISignatory|null,
+        *      resourceTypes: array{
+        *          name: string,
+        *          shareTypes: list<string>,
+        *          protocols: array<string, string>
+        *      }[],
+        *      version: string
+        *  }
         */
        public function jsonSerialize(): array {
                $resourceTypes = [];
@@ -209,8 +226,10 @@ class OCMProvider implements IOCMProvider {
 
                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
                ];
        }
index 279162c76f283e773c6efd15cea01016d7b9fdb7..f39d0e2382177378f6a2f2899f3c512edf9c2922 100644 (file)
@@ -25,12 +25,6 @@ use Psr\Log\LoggerInterface;
  */
 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 @@ class OCMDiscoveryService implements IOCMDiscoveryService {
                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 @@ class OCMDiscoveryService implements IOCMDiscoveryService {
                        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 (file)
index 0000000..1508c1d
--- /dev/null
@@ -0,0 +1,149 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\OCM;
+
+use NCU\Security\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\Signature\Model\Signatory;
+use OCP\IAppConfig;
+use OCP\IURLGenerator;
+use OCP\OCM\Exceptions\OCMProviderException;
+
+/**
+ * @inheritDoc
+ *
+ * returns local signatory using IKeyPairManager
+ * extract optional signatory (keyId+public key) from ocm discovery service on remote instance
+ *
+ * @since 31.0.0
+ */
+class OCMSignatoryManager implements ISignatoryManager {
+       public const PROVIDER_ID = 'ocm';
+       public const APPCONFIG_SIGN_IDENTITY_EXTERNAL = 'ocm_signed_request_identity_external';
+       public const APPCONFIG_SIGN_DISABLED = 'ocm_signed_request_disabled';
+       public const APPCONFIG_SIGN_ENFORCED = 'ocm_signed_request_enforced';
+
+       public function __construct(
+               private readonly IAppConfig $appConfig,
+               private readonly ISignatureManager $signatureManager,
+               private readonly IURLGenerator $urlGenerator,
+               private readonly IKeyPairManager $keyPairManager,
+               private readonly OCMDiscoveryService $ocmDiscoveryService,
+       ) {
+       }
+
+       /**
+        * @inheritDoc
+        *
+        * @since 31.0.0
+        * @return string
+        */
+       public function getProviderId(): string {
+               return self::PROVIDER_ID;
+       }
+
+       /**
+        * @inheritDoc
+        *
+        * @since 31.0.0
+        * @return array
+        */
+       public function getOptions(): array {
+               return [];
+       }
+
+       /**
+        * @inheritDoc
+        *
+        * @return ISignatory
+        * @throws KeyPairConflictException
+        * @throws IdentityNotFoundException
+        * @since 31.0.0
+        */
+       public function getLocalSignatory(): ISignatory {
+               /**
+                * TODO: manage multiple identity (external, internal, ...) to allow a limitation
+                * based on the requested interface (ie. only accept shares from globalscale)
+                */
+               if ($this->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 (file)
index 0000000..0af960b
--- /dev/null
@@ -0,0 +1,182 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\PublicPrivateKeyPairs;
+
+use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairConflictException;
+use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairNotFoundException;
+use NCU\Security\PublicPrivateKeyPairs\IKeyPairManager;
+use NCU\Security\PublicPrivateKeyPairs\Model\IKeyPair;
+use OC\Security\PublicPrivateKeyPairs\Model\KeyPair;
+use OCP\IAppConfig;
+
+/**
+ * @inheritDoc
+ *
+ * KeyPairManager store internal public/private key pair using AppConfig, taking advantage of the encryption
+ * and lazy loading.
+ *
+ * @since 31.0.0
+ */
+class KeyPairManager implements IKeyPairManager {
+       private const CONFIG_PREFIX = 'security.keypair.';
+
+       public function __construct(
+               private readonly IAppConfig $appConfig,
+       ) {
+       }
+
+       /**
+        * @inheritDoc
+        *
+        * @param string $app appId
+        * @param string $name key name
+        * @param array $options algorithms, metadata
+        *
+        * @return IKeyPair
+        * @throws KeyPairConflictException if a key already exist
+        * @since 31.0.0
+        */
+       public function generateKeyPair(string $app, string $name, array $options = []): IKeyPair {
+               if ($this->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 (file)
index 0000000..523f7c1
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\PublicPrivateKeyPairs\Model;
+
+use NCU\Security\PublicPrivateKeyPairs\Model\IKeyPair;
+
+/**
+ * @inheritDoc
+ *
+ * @since 31.0.0
+ */
+class KeyPair implements IKeyPair {
+       private string $publicKey = '';
+       private string $privateKey = '';
+       private array $options = [];
+
+       public function __construct(
+               private readonly string $app,
+               private readonly string $name,
+       ) {
+       }
+
+       /**
+        * @inheritDoc
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getApp(): string {
+               return $this->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 (file)
index 0000000..8fe83a7
--- /dev/null
@@ -0,0 +1,170 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Signature\Model;
+
+use JsonSerializable;
+use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
+use NCU\Security\Signature\Exceptions\IncomingRequestNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatoryException;
+use NCU\Security\Signature\ISignatureManager;
+use NCU\Security\Signature\Model\IIncomingSignedRequest;
+use NCU\Security\Signature\Model\ISignatory;
+use OCP\IRequest;
+
+/**
+ * @inheritDoc
+ *
+ * @see ISignatureManager for details on signature
+ * @since 31.0.0
+ */
+class IncomingSignedRequest extends SignedRequest implements
+       IIncomingSignedRequest,
+       JsonSerializable {
+       private ?IRequest $request = null;
+       private int $time = 0;
+       private string $origin = '';
+       private string $estimatedSignature = '';
+
+       /**
+        * @inheritDoc
+        *
+        * @param ISignatory $signatory
+        *
+        * @return $this
+        * @throws SignatoryException
+        * @throws IdentityNotFoundException
+        * @since 31.0.0
+        */
+       public function setSignatory(ISignatory $signatory): self {
+               $identity = \OCP\Server::get(ISignatureManager::class)->extractIdentityFromUri($signatory->getKeyId());
+               if ($identity !== $this->getOrigin()) {
+                       throw new SignatoryException('keyId from provider is different from the one from signed request');
+               }
+
+               parent::setSignatory($signatory);
+               return $this;
+       }
+
+       /**
+        * @inheritDoc
+        *
+        * @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 (file)
index 0000000..04efcf8
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Signature\Model;
+
+use JsonSerializable;
+use NCU\Security\Signature\ISignatureManager;
+use NCU\Security\Signature\Model\IOutgoingSignedRequest;
+
+/**
+ * extends ISignedRequest to add info requested at the generation of the signature
+ *
+ * @see ISignatureManager for details on signature
+ * @since 31.0.0
+ */
+class OutgoingSignedRequest extends SignedRequest implements
+       IOutgoingSignedRequest,
+       JsonSerializable {
+       private string $host = '';
+       private array $headers = [];
+       private string $clearSignature = '';
+       private string $algorithm;
+
+       /**
+        * @inheritDoc
+        *
+        * @param string $host
+        * @return IOutgoingSignedRequest
+        * @since 31.0.0
+        */
+       public function setHost(string $host): IOutgoingSignedRequest {
+               $this->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 (file)
index 0000000..b28d2c0
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Signature\Model;
+
+use JsonSerializable;
+use NCU\Security\Signature\Model\ISignatory;
+use NCU\Security\Signature\Model\SignatoryStatus;
+use NCU\Security\Signature\Model\SignatoryType;
+
+class Signatory implements ISignatory, JsonSerializable {
+       private string $providerId = '';
+       private string $account = '';
+       private SignatoryType $type = SignatoryType::STATIC;
+       private SignatoryStatus $status = SignatoryStatus::SYNCED;
+       private array $metadata = [];
+       private int $creation = 0;
+       private int $lastUpdated = 0;
+
+       public function __construct(
+               private string $keyId,
+               private readonly string $publicKey,
+               private readonly string $privateKey = '',
+               readonly bool $local = false,
+       ) {
+               // if set as local (for current instance), we apply some filters.
+               if ($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;
+               }
+       }
+
+       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 (file)
index 0000000..1587da9
--- /dev/null
@@ -0,0 +1,143 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Security\Signature\Model;
+
+use JsonSerializable;
+use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Model\ISignatory;
+use NCU\Security\Signature\Model\ISignedRequest;
+
+/**
+ * @inheritDoc
+ *
+ * @since 31.0.0
+ */
+class SignedRequest implements ISignedRequest, JsonSerializable {
+       private string $digest;
+       private string $signedSignature = '';
+       private array $signatureHeader = [];
+       private ?ISignatory $signatory = null;
+
+       public function __construct(
+               private readonly string $body,
+       ) {
+               // digest is created on the fly using $body
+               $this->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 (file)
index 0000000..d087e8e
--- /dev/null
@@ -0,0 +1,828 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Security\Signature;
+
+use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
+use NCU\Security\Signature\Exceptions\IncomingRequestException;
+use NCU\Security\Signature\Exceptions\InvalidKeyOriginException;
+use NCU\Security\Signature\Exceptions\InvalidSignatureException;
+use NCU\Security\Signature\Exceptions\SignatoryConflictException;
+use NCU\Security\Signature\Exceptions\SignatoryException;
+use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+use NCU\Security\Signature\Exceptions\SignatureException;
+use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
+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 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;
+
+/**
+ * ISignatureManager is a service integrated to core that provide tools
+ * to set/get authenticity of/from outgoing/incoming request.
+ *
+ * Quick description of the signature, added to the headers
+ * {
+ *     "(request-target)": "post /path",
+ *     "content-length": 385,
+ *     "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==\""
+ * }
+ *
+ * 'content-length' is the total length of the data/content
+ * 'date' is the datetime the request have been initiated
+ * 'digest' is a checksum of the data/content
+ * 'host' is the hostname of the recipient of the request (remote when signing outgoing request, local on
+ * incoming request)
+ * 'Signature' contains the signature generated using the private key, and metadata:
+ *    - 'keyId' is a unique id, formatted as an url. hostname is used to retrieve the public key via custom
+ * discovery
+ *    - 'algorithm' define the algorithm used to generate signature
+ *    - 'headers' contains a list of element used during the generation of the signature
+ *    - 'signature' is the encrypted string, using local private key, of an array containing elements
+ *      listed in 'headers' and their value. Some elements (content-length date digest host) are mandatory
+ *      to ensure authenticity override protection.
+ *
+ * @since 31.0.0
+ */
+class SignatureManager implements ISignatureManager {
+       private const DATE_HEADER = 'D, d M Y H:i:s T';
+       private const DATE_TTL = 300;
+       private const SIGNATORY_TTL = 86400 * 3;
+       private const TABLE_SIGNATORIES = 'sec_signatory';
+       private const BODY_MAXSIZE = 50000; // max size of the payload of the request
+       public const APPCONFIG_IDENTITY = 'security.signature.identity';
+
+       public function __construct(
+               private readonly IRequest $request,
+               private readonly IDBConnection $connection,
+               private readonly IAppConfig $appConfig,
+               private readonly LoggerInterface $logger,
+       ) {
+       }
+
+       /**
+        * @inheritDoc
+        *
+        * @param ISignatoryManager $signatoryManager used to get details about remote instance
+        * @param string|null $body if NULL, body will be extracted from php://input
+        *
+        * @return IIncomingSignedRequest
+        * @throws IncomingRequestException if anything looks wrong with the incoming request
+        * @throws SignatureNotFoundException if incoming request is not signed
+        * @throws SignatureException if signature could not be confirmed
+        * @since 31.0.0
+        */
+       public function getIncomingSignedRequest(
+               ISignatoryManager $signatoryManager,
+               ?string $body = null,
+       ): IIncomingSignedRequest {
+               $body = $body ?? file_get_contents('php://input');
+               if (strlen($body) > 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);
+       }
+}
index d57ddf61c0378b1bff3ec2a061936cdbd4e76052..7ff0045b03fa8cbeacd3a909513033363410084c 100644 (file)
@@ -8,6 +8,8 @@ namespace OC;
 
 use bantu\IniGetWrapper\IniGetWrapper;
 use NCU\Config\IUserConfig;
+use NCU\Security\PublicPrivateKeyPairs\IKeyPairManager;
+use NCU\Security\Signature\ISignatureManager;
 use OC\Accounts\AccountManager;
 use OC\App\AppManager;
 use OC\App\AppStore\Bundles\BundleFetcher;
@@ -101,8 +103,10 @@ use OC\Security\CSRF\CsrfTokenManager;
 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 @@ class Server extends ServerContainer implements IServerContainer {
                });
 
                $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 @@ class Server extends ServerContainer implements IServerContainer {
 
                $this->registerAlias(IRichTextFormatter::class, \OC\RichObjectStrings\RichTextFormatter::class);
 
+               $this->registerAlias(IKeyPairManager::class, KeyPairManager::class);
+               $this->registerAlias(ISignatureManager::class, SignatureManager::class);
+
                $this->connectDispatcher();
        }
 
index ba2ab6ce759ba0341945e6b573c4ba79ba7ef470..789462efd7828a589c037dd5fbb5505552b8a4f0 100644 (file)
@@ -10,6 +10,7 @@ declare(strict_types=1);
 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 @@ interface IOCMProvider extends JsonSerializable {
         */
        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 @@ interface IOCMProvider extends JsonSerializable {
        /**
         * @return array{
         *     enabled: bool,
-        *     apiVersion: string,
+        *     apiVersion: '1.0-proposal1',
         *     endPoint: string,
-        *     resourceTypes: list<array{
+        *     publicKey: ISignatory|null,
+        *     resourceTypes: array{
         *         name: string,
         *         shareTypes: list<string>,
         *         protocols: array<string, string>
-        *     }>,
+        *     }[],
+        *     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 (file)
index 0000000..b808342
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\PublicPrivateKeyPairs\Exceptions;
+
+/**
+ * conflict between public and private key pair
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+class KeyPairConflictException extends KeyPairException {
+}
diff --git a/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairException.php b/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairException.php
new file mode 100644 (file)
index 0000000..1cbcf13
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\PublicPrivateKeyPairs\Exceptions;
+
+use Exception;
+
+/**
+ * global exception related to key pairs
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+class KeyPairException extends Exception {
+}
diff --git a/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairNotFoundException.php b/lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairNotFoundException.php
new file mode 100644 (file)
index 0000000..138fd88
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\PublicPrivateKeyPairs\Exceptions;
+
+/**
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+class KeyPairNotFoundException extends KeyPairException {
+}
diff --git a/lib/unstable/Security/PublicPrivateKeyPairs/IKeyPairManager.php b/lib/unstable/Security/PublicPrivateKeyPairs/IKeyPairManager.php
new file mode 100644 (file)
index 0000000..a993cec
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\PublicPrivateKeyPairs;
+
+use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairConflictException;
+use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairNotFoundException;
+use NCU\Security\PublicPrivateKeyPairs\Model\IKeyPair;
+
+/**
+ * IKeyPairManager contains a group of method to create/manage/store internal public/private key pair.
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+interface IKeyPairManager {
+
+       /**
+        * generate and store public/private key pair.
+        * throws exception if key pair already exist
+        *
+        * @param string $app appId
+        * @param string $name key name
+        * @param array $options algorithms, metadata
+        *
+        * @return IKeyPair
+        * @throws KeyPairConflictException if a key already exist
+        * @since 31.0.0
+        */
+       public function generateKeyPair(string $app, string $name, array $options = []): IKeyPair;
+
+       /**
+        * returns if key pair is known.
+        *
+        * @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;
+
+       /**
+        * return key pair from database based on $app and $name.
+        * throws exception if key pair does not exist
+        *
+        * @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;
+
+       /**
+        * delete key pair from database
+        *
+        * @param string $app appid
+        * @param string $name key name
+        *
+        * @since 31.0.0
+        */
+       public function deleteKeyPair(string $app, string $name): void;
+
+       /**
+        * test key pair by encrypting/decrypting a string
+        *
+        * @param IKeyPair $keyPair keypair to test
+        *
+        * @return bool
+        * @since 31.0.0
+        */
+       public function testKeyPair(IKeyPair $keyPair): bool;
+}
diff --git a/lib/unstable/Security/PublicPrivateKeyPairs/Model/IKeyPair.php b/lib/unstable/Security/PublicPrivateKeyPairs/Model/IKeyPair.php
new file mode 100644 (file)
index 0000000..92585b9
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\PublicPrivateKeyPairs\Model;
+
+/**
+ * simple model that store key pair, its name, its origin (app)
+ * and the options used during its creation
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+interface IKeyPair {
+       /**
+        * returns id of the app owning the key pair
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getApp(): string;
+
+       /**
+        * returns name of the key pair
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getName(): string;
+
+       /**
+        * set public key
+        *
+        * @param string $publicKey
+        * @return IKeyPair
+        * @since 31.0.0
+        */
+       public function setPublicKey(string $publicKey): IKeyPair;
+
+       /**
+        * returns public key
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getPublicKey(): string;
+
+       /**
+        * set private key
+        *
+        * @param string $privateKey
+        * @return IKeyPair
+        * @since 31.0.0
+        */
+       public function setPrivateKey(string $privateKey): IKeyPair;
+
+       /**
+        * returns private key
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getPrivateKey(): string;
+
+       /**
+        * set options
+        *
+        * @param array $options
+        * @return IKeyPair
+        * @since 31.0.0
+        */
+       public function setOptions(array $options): IKeyPair;
+
+       /**
+        * returns options
+        *
+        * @return array
+        * @since 31.0.0
+        */
+       public function getOptions(): array;
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/IdentityNotFoundException.php b/lib/unstable/Security/Signature/Exceptions/IdentityNotFoundException.php
new file mode 100644 (file)
index 0000000..30c7f8e
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class IdentityNotFoundException extends SignatureException {
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/IncomingRequestException.php b/lib/unstable/Security/Signature/Exceptions/IncomingRequestException.php
new file mode 100644 (file)
index 0000000..d3b5c93
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class IncomingRequestException extends SignatureException {
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php b/lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php
new file mode 100644 (file)
index 0000000..1953af3
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class IncomingRequestNotFoundException extends SignatureException {
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/InvalidKeyOriginException.php b/lib/unstable/Security/Signature/Exceptions/InvalidKeyOriginException.php
new file mode 100644 (file)
index 0000000..6e17029
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class InvalidKeyOriginException extends SignatureException {
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/InvalidSignatureException.php b/lib/unstable/Security/Signature/Exceptions/InvalidSignatureException.php
new file mode 100644 (file)
index 0000000..dc98d9c
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class InvalidSignatureException extends SignatureException {
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/SignatoryConflictException.php b/lib/unstable/Security/Signature/Exceptions/SignatoryConflictException.php
new file mode 100644 (file)
index 0000000..c2c4d61
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class SignatoryConflictException extends SignatoryException {
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/SignatoryException.php b/lib/unstable/Security/Signature/Exceptions/SignatoryException.php
new file mode 100644 (file)
index 0000000..0645e7b
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class SignatoryException extends SignatureException {
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/SignatoryNotFoundException.php b/lib/unstable/Security/Signature/Exceptions/SignatoryNotFoundException.php
new file mode 100644 (file)
index 0000000..e956264
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class SignatoryNotFoundException extends SignatoryException {
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/SignatureException.php b/lib/unstable/Security/Signature/Exceptions/SignatureException.php
new file mode 100644 (file)
index 0000000..bcd21c9
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Security\Signature\Exceptions;
+
+use Exception;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class SignatureException extends Exception {
+}
diff --git a/lib/unstable/Security/Signature/Exceptions/SignatureNotFoundException.php b/lib/unstable/Security/Signature/Exceptions/SignatureNotFoundException.php
new file mode 100644 (file)
index 0000000..a1bf237
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class SignatureNotFoundException extends SignatureException {
+}
diff --git a/lib/unstable/Security/Signature/ISignatoryManager.php b/lib/unstable/Security/Signature/ISignatoryManager.php
new file mode 100644 (file)
index 0000000..825ccac
--- /dev/null
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature;
+
+use NCU\Security\Signature\Model\IIncomingSignedRequest;
+use NCU\Security\Signature\Model\ISignatory;
+
+/**
+ * ISignatoryManager contains a group of method that will help
+ *   - signing outgoing request
+ *   - confirm the authenticity of incoming signed request.
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+interface ISignatoryManager {
+       /**
+        * id of the signatory manager.
+        * This is used to store, confirm uniqueness and avoid conflict of the remote key pairs.
+        *
+        * Must be unique.
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getProviderId(): string;
+
+       /**
+        * options that might affect the way the whole process is handled:
+        * [
+        *   'ttl' => 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 (file)
index 0000000..cc02972
--- /dev/null
@@ -0,0 +1,129 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature;
+
+use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
+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\Model\IIncomingSignedRequest;
+use NCU\Security\Signature\Model\IOutgoingSignedRequest;
+use NCU\Security\Signature\Model\ISignatory;
+
+/**
+ * ISignatureManager is a service integrated to core that provide tools
+ * to set/get authenticity of/from outgoing/incoming request.
+ *
+ * Quick description of the signature, added to the headers
+ * {
+ *     "(request-target)": "post /path",
+ *     "content-length": 385,
+ *     "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==\""
+ * }
+ *
+ * 'content-length' is the total length of the data/content
+ * 'date' is the datetime the request have been initiated
+ * 'digest' is a checksum of the data/content
+ * 'host' is the hostname of the recipient of the request (remote when signing outgoing request, local on incoming request)
+ * 'Signature' contains the signature generated using the private key, and metadata:
+ *    - 'keyId' is a unique id, formatted as an url. hostname is used to retrieve the public key via custom discovery
+ *    - 'algorithm' define the algorithm used to generate signature
+ *    - 'headers' contains a list of element used during the generation of the signature
+ *    - 'signature' is the encrypted string, using local private key, of an array containing elements
+ *      listed in 'headers' and their value. Some elements (content-length date digest host) are mandatory
+ *      to ensure authenticity override protection.
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+interface ISignatureManager {
+       /**
+        * Extracting data from headers and body from the incoming request.
+        * Compare headers and body to confirm authenticity of remote instance.
+        * Returns details about the signed request or throws exception.
+        *
+        * Should be called from Controller.
+        *
+        * @param ISignatoryManager $signatoryManager used to get details about remote instance
+        * @param string|null $body if NULL, body will be extracted from php://input
+        *
+        * @return IIncomingSignedRequest
+        * @throws IncomingRequestException if anything looks wrong with the incoming request
+        * @throws SignatureNotFoundException if incoming request is not signed
+        * @throws SignatureException if signature could not be confirmed
+        * @since 31.0.0
+        */
+       public function getIncomingSignedRequest(ISignatoryManager $signatoryManager, ?string $body = null): IIncomingSignedRequest;
+
+       /**
+        * Preparing signature (and headers) to sign an outgoing request.
+        * Returns a IOutgoingSignedRequest containing all details to finalise the packaging of the whole payload
+        *
+        * @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;
+
+       /**
+        * Complete the full process of signing and filling headers from payload when generating
+        * an outgoing request with IClient
+        *
+        * @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;
+
+       /**
+        * returns remote signatory stored in local database, based on the remote host.
+        *
+        * @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;
+
+       /**
+        * returns a fully formatted keyId, based on a fix hostname and path
+        *
+        * @param string $path
+        *
+        * @return string
+        * @throws IdentityNotFoundException if hostname is not set
+        * @since 31.0.0
+        */
+       public function generateKeyIdFromConfig(string $path): string;
+
+       /**
+        * returns hostname:port extracted from an uri
+        *
+        * @param string $uri
+        *
+        * @return string
+        * @throws IdentityNotFoundException if identity cannot be extracted
+        * @since 31.0.0
+        */
+       public function extractIdentityFromUri(string $uri): string;
+}
diff --git a/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php b/lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php
new file mode 100644 (file)
index 0000000..a6682ef
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Model;
+
+use NCU\Security\Signature\ISignatureManager;
+use OCP\IRequest;
+
+/**
+ * model wrapping an actual incoming request, adding details about the signature and the
+ * authenticity of the origin of the request.
+ *
+ * @see ISignatureManager for details on signature
+ * @experimental 31.0.0
+ * @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
+        *
+        * @return IRequest
+        * @since 31.0.0
+        */
+       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.
+        *
+        * @param string $origin
+        * @return IIncomingSignedRequest
+        * @since 31.0.0
+        */
+       public function setOrigin(string $origin): IIncomingSignedRequest;
+
+       /**
+        * get the hostname at the source of the base request.
+        * based on the keyId defined in the signature header.
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getOrigin(): string;
+
+       /**
+        * 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;
+
+       /**
+        * 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
new file mode 100644 (file)
index 0000000..b2ca221
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Model;
+
+use NCU\Security\Signature\ISignatureManager;
+
+/**
+ * extends ISignedRequest to add info requested at the generation of the signature
+ *
+ * @see ISignatureManager for details on signature
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+interface IOutgoingSignedRequest extends ISignedRequest {
+       /**
+        * set the host of the recipient of the request.
+        *
+        * @param string $host
+        * @return IOutgoingSignedRequest
+        * @since 31.0.0
+        */
+       public function setHost(string $host): IOutgoingSignedRequest;
+
+       /**
+        * get the host of the recipient of the request.
+        * - on incoming request, this is the local hostname of current instance.
+        * - on outgoing request, this is the remote instance.
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       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
+        *
+        * @return IOutgoingSignedRequest
+        * @since 31.0.0
+        */
+       public function addHeader(string $key, string|int|float|bool|array $value): IOutgoingSignedRequest;
+
+       /**
+        * returns list of headers value that will be added to the base request
+        *
+        * @return array
+        * @since 31.0.0
+        */
+       public function getHeaders(): array;
+
+       /**
+        * store a clear version of the signature
+        *
+        * @param string $estimated
+        *
+        * @return IOutgoingSignedRequest
+        * @since 31.0.0
+        */
+       public function setClearSignature(string $estimated): IOutgoingSignedRequest;
+
+       /**
+        * returns the clear version of the signature
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getClearSignature(): string;
+
+       /**
+        * set algorithm to be used to sign the signature
+        *
+        * @param string $algorithm
+        *
+        * @return IOutgoingSignedRequest
+        * @since 31.0.0
+        */
+       public function setAlgorithm(string $algorithm): IOutgoingSignedRequest;
+
+       /**
+        * returns the algorithm set to sign the signature
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getAlgorithm(): string;
+}
diff --git a/lib/unstable/Security/Signature/Model/ISignatory.php b/lib/unstable/Security/Signature/Model/ISignatory.php
new file mode 100644 (file)
index 0000000..e77b77e
--- /dev/null
@@ -0,0 +1,160 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Model;
+
+use NCU\Security\Signature\ISignatoryManager;
+
+/**
+ * model that store keys and details related to host and in use protocol
+ * mandatory details are providerId, host, keyId and public key.
+ * private key is only used for local signatory, used to sign outgoing request
+ *
+ * the pair providerId+host is unique, meaning only one signatory can exist for each host
+ * and protocol
+ *
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+interface ISignatory {
+       /**
+        * unique string, related to the ISignatoryManager
+        *
+        * @see ISignatoryManager::getProviderId
+        * @param string $providerId
+        *
+        * @return ISignatory
+        * @since 31.0.0
+        */
+       public function setProviderId(string $providerId): ISignatory;
+
+       /**
+        * returns the provider id, unique string related to the ISignatoryManager
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getProviderId(): string;
+
+       /**
+        * set account, in case your ISignatoryManager needs to manage multiple keys from same host
+        *
+        * @param string $account
+        *
+        * @return ISignatory
+        * @since 31.0.0
+        */
+       public function setAccount(string $account): ISignatory;
+
+       /**
+        * return account name, empty string if not set
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getAccount(): string;
+
+       /**
+        * returns key id
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getKeyId(): string;
+
+       /**
+        * returns public key
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getPublicKey(): string;
+
+       /**
+        * returns private key, if available
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getPrivateKey(): string;
+
+       /**
+        * set metadata
+        *
+        * @param array $metadata
+        *
+        * @return ISignatory
+        * @since 31.0.0
+        */
+       public function setMetadata(array $metadata): ISignatory;
+
+       /**
+        * returns metadata
+        *
+        * @return array
+        * @since 31.0.0
+        */
+       public function getMetadata(): array;
+
+       /**
+        * update an entry in metadata
+        *
+        * @param string $key
+        * @param string|int $value
+        *
+        * @return ISignatory
+        * @since 31.0.0
+        */
+       public function setMetaValue(string $key, string|int $value): ISignatory;
+
+       /**
+        * set SignatoryType
+        *
+        * @param SignatoryType $type
+        *
+        * @return ISignatory
+        * @since 31.0.0
+        */
+       public function setType(SignatoryType $type): ISignatory;
+
+       /**
+        * returns SignatoryType
+        *
+        * @return SignatoryType
+        * @since 31.0.0
+        */
+       public function getType(): SignatoryType;
+
+       /**
+        * set SignatoryStatus
+        *
+        * @param SignatoryStatus $status
+        *
+        * @see SignatoryStatus
+        * @return ISignatory
+        * @since 31.0.0
+        */
+       public function setStatus(SignatoryStatus $status): ISignatory;
+
+       /**
+        * get SignatoryStatus
+        *
+        * @see SignatoryStatus
+        * @return SignatoryStatus
+        * @since 31.0.0
+        */
+       public function getStatus(): SignatoryStatus;
+
+       /**
+        * get last timestamp this entry has been updated
+        *
+        * @return int
+        * @since 31.0.0
+        */
+       public function getLastUpdated(): int;
+}
diff --git a/lib/unstable/Security/Signature/Model/ISignedRequest.php b/lib/unstable/Security/Signature/Model/ISignedRequest.php
new file mode 100644 (file)
index 0000000..ebb0e1c
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Model;
+
+use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
+
+/**
+ * model that store data related to a possible signature.
+ * those details will be used:
+ * - to confirm authenticity of a signed incoming request
+ * - to sign an outgoing request
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+interface ISignedRequest {
+       /**
+        * payload of the request
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getBody(): string;
+
+       /**
+        * checksum of the payload of the request
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getDigest(): string;
+
+       /**
+        * set the list of headers related to the signature of the request
+        *
+        * @param array $signatureHeader
+        * @return ISignedRequest
+        * @since 31.0.0
+        */
+       public function setSignatureHeader(array $signatureHeader): ISignedRequest;
+
+       /**
+        * get the list of headers related to the signature of the request
+        *
+        * @return array
+        * @since 31.0.0
+        */
+       public function getSignatureHeader(): array;
+
+       /**
+        * set the signed version of the signature
+        *
+        * @param string $signedSignature
+        * @return ISignedRequest
+        * @since 31.0.0
+        */
+       public function setSignedSignature(string $signedSignature): ISignedRequest;
+
+       /**
+        * get the signed version of the signature
+        *
+        * @return string
+        * @since 31.0.0
+        */
+       public function getSignedSignature(): string;
+
+       /**
+        * set the signatory, containing keys and details, related to this request
+        *
+        * @param ISignatory $signatory
+        * @return ISignedRequest
+        * @since 31.0.0
+        */
+       public function setSignatory(ISignatory $signatory): ISignedRequest;
+
+       /**
+        * get the signatory, containing keys and details, related to this request
+        *
+        * @return ISignatory
+        * @throws SignatoryNotFoundException
+        * @since 31.0.0
+        */
+       public function getSignatory(): ISignatory;
+
+       /**
+        * returns if a signatory related to this request have been found and defined
+        *
+        * @return bool
+        * @since 31.0.0
+        */
+       public function hasSignatory(): bool;
+}
diff --git a/lib/unstable/Security/Signature/Model/SignatoryStatus.php b/lib/unstable/Security/Signature/Model/SignatoryStatus.php
new file mode 100644 (file)
index 0000000..1c28f65
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Model;
+
+/**
+ * current status of signatory. is it trustable or not ?
+ *
+ * - SYNCED = the remote instance is trustable.
+ * - BROKEN = the remote instance does not use the same key pairs
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+enum SignatoryStatus: int {
+       /** @since 31.0.0 */
+       case SYNCED = 1;
+       /** @since 31.0.0 */
+       case BROKEN = 9;
+}
diff --git a/lib/unstable/Security/Signature/Model/SignatoryType.php b/lib/unstable/Security/Signature/Model/SignatoryType.php
new file mode 100644 (file)
index 0000000..652bee2
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Model;
+
+/**
+ * type of link between local and remote instance
+ *
+ * - FORGIVABLE = the keypair can be deleted and refreshed anytime and 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.
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+enum SignatoryType: int {
+       /** @since 31.0.0 */
+       case FORGIVABLE = 1; // no notice on refresh
+       /** @since 31.0.0 */
+       case REFRESHABLE = 4; // notice on refresh
+       /** @since 31.0.0 */
+       case TRUSTED = 8; // warning on refresh
+       /** @since 31.0.0 */
+       case STATIC = 9; // error on refresh
+}
diff --git a/lib/unstable/Security/Signature/SignatureAlgorithm.php b/lib/unstable/Security/Signature/SignatureAlgorithm.php
new file mode 100644 (file)
index 0000000..c0a5a0c
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature;
+
+/**
+ * list of available algorithm when signing payload
+ *
+ * @experimental 31.0.0
+ * @since 31.0.0
+ */
+enum SignatureAlgorithm: string {
+       /** @since 31.0.0 */
+       case SHA256 = 'sha256';
+       /** @since 31.0.0 */
+       case SHA512 = 'sha512';
+}