* 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,
) {
}
*
* @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
*/
$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)];
}
}
*/
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;
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;
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);
}
#[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) ||
#[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 ||
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');
+ }
}
"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"
}
}
}
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'],
# 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
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;
class OCMController extends Controller {
public function __construct(
IRequest $request,
- private IConfig $config,
+ private readonly IAppConfig $appConfig,
private LoggerInterface $logger,
) {
parent::__construct('core', $request);
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
)
);
--- /dev/null
+<?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;
+ }
+}
'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',
'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',
'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',
'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',
'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',
'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',
'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',
'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',
*/
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;
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;
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,
) {
}
$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);
$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 {
$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 : [];
$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 {
}
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;
}
}
protected $httpClientService;
/** @var ICertificateManager */
protected $certManager;
+ protected bool $verify = true;
protected LoggerInterface $logger;
protected IEventLogger $eventLogger;
protected IMimeTypeDetector $mimeTypeDetector;
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');
}
}
+ 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']);
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;
private string $endPoint = '';
/** @var IOCMResource[] */
private array $resourceTypes = [];
-
+ private ?ISignatory $signatory = null;
private bool $emittedEvent = false;
public function __construct(
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
*
*/
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 = [];
}
$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');
}
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 = [];
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
];
}
*/
class OCMDiscoveryService implements IOCMDiscoveryService {
private ICache $cache;
- private array $supportedAPIVersion =
- [
- '1.0-proposal1',
- '1.0',
- '1.1'
- ];
public function __construct(
ICacheFactory $cacheFactory,
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
}
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));
- }
}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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];
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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(),
+ ]
+ );
+ }
+}
--- /dev/null
+<?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(),
+ ]
+ );
+ }
+}
--- /dev/null
+<?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()
+ ];
+ }
+}
--- /dev/null
+<?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,
+ ];
+ }
+}
--- /dev/null
+<?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);
+ }
+}
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;
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;
});
$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();
});
$this->registerAlias(IRichTextFormatter::class, \OC\RichObjectStrings\RichTextFormatter::class);
+ $this->registerAlias(IKeyPairManager::class, KeyPairManager::class);
+ $this->registerAlias(ISignatureManager::class, SignatureManager::class);
+
$this->connectDispatcher();
}
namespace OCP\OCM;
use JsonSerializable;
+use NCU\Security\Signature\Model\ISignatory;
use OCP\OCM\Exceptions\OCMArgumentException;
use OCP\OCM\Exceptions\OCMProviderException;
*/
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
*
/**
* @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
*/
--- /dev/null
+<?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 {
+}
--- /dev/null
+<?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 {
+}
--- /dev/null
+<?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 {
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class IdentityNotFoundException extends SignatureException {
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class IncomingRequestException extends SignatureException {
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class IncomingRequestNotFoundException extends SignatureException {
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class InvalidKeyOriginException extends SignatureException {
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class InvalidSignatureException extends SignatureException {
+}
--- /dev/null
+<?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 {
+}
--- /dev/null
+<?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 {
+}
--- /dev/null
+<?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 {
+}
--- /dev/null
+<?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 {
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace NCU\Security\Signature\Exceptions;
+
+/**
+ * @since 31.0.0
+ * @experimental 31.0.0
+ */
+class SignatureNotFoundException extends SignatureException {
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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
+}
--- /dev/null
+<?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';
+}