Browse Source

feat(ocm): signing ocm requests

Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
pull/45979/head
Maxence Lange 11 months ago
parent
commit
4591430c9c
  1. 35
      apps/cloud_federation_api/lib/Capabilities.php
  2. 155
      apps/cloud_federation_api/lib/Controller/RequestHandlerController.php
  3. 33
      apps/cloud_federation_api/openapi.json
  4. 1
      apps/files_sharing/lib/External/Storage.php
  5. 3
      build/integration/federation_features/cleanup-remote-storage.feature
  6. 12
      core/Controller/OCMController.php
  7. 135
      core/Migrations/Version31000Date20240101084401.php
  8. 9
      lib/composer/composer/autoload_classmap.php
  9. 9
      lib/composer/composer/autoload_static.php
  10. 73
      lib/private/Federation/CloudFederationProviderManager.php
  11. 7
      lib/private/Files/Storage/DAV.php
  12. 45
      lib/private/OCM/Model/OCMProvider.php
  13. 35
      lib/private/OCM/OCMDiscoveryService.php
  14. 149
      lib/private/OCM/OCMSignatoryManager.php
  15. 182
      lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php
  16. 114
      lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php
  17. 170
      lib/private/Security/Signature/Model/IncomingSignedRequest.php
  18. 131
      lib/private/Security/Signature/Model/OutgoingSignedRequest.php
  19. 147
      lib/private/Security/Signature/Model/Signatory.php
  20. 143
      lib/private/Security/Signature/Model/SignedRequest.php
  21. 828
      lib/private/Security/Signature/SignatureManager.php
  22. 20
      lib/private/Server.php
  23. 25
      lib/public/OCM/IOCMProvider.php
  24. 18
      lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php
  25. 20
      lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairException.php
  26. 16
      lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairNotFoundException.php
  27. 80
      lib/unstable/Security/PublicPrivateKeyPairs/IKeyPairManager.php
  28. 85
      lib/unstable/Security/PublicPrivateKeyPairs/Model/IKeyPair.php
  29. 16
      lib/unstable/Security/Signature/Exceptions/IdentityNotFoundException.php
  30. 16
      lib/unstable/Security/Signature/Exceptions/IncomingRequestException.php
  31. 16
      lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php
  32. 16
      lib/unstable/Security/Signature/Exceptions/InvalidKeyOriginException.php
  33. 16
      lib/unstable/Security/Signature/Exceptions/InvalidSignatureException.php
  34. 16
      lib/unstable/Security/Signature/Exceptions/SignatoryConflictException.php
  35. 16
      lib/unstable/Security/Signature/Exceptions/SignatoryException.php
  36. 16
      lib/unstable/Security/Signature/Exceptions/SignatoryNotFoundException.php
  37. 18
      lib/unstable/Security/Signature/Exceptions/SignatureException.php
  38. 16
      lib/unstable/Security/Signature/Exceptions/SignatureNotFoundException.php
  39. 71
      lib/unstable/Security/Signature/ISignatoryManager.php
  40. 129
      lib/unstable/Security/Signature/ISignatureManager.php
  41. 105
      lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php
  42. 94
      lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php
  43. 160
      lib/unstable/Security/Signature/Model/ISignatory.php
  44. 98
      lib/unstable/Security/Signature/Model/ISignedRequest.php
  45. 25
      lib/unstable/Security/Signature/Model/SignatoryStatus.php
  46. 31
      lib/unstable/Security/Signature/Model/SignatoryType.php
  47. 22
      lib/unstable/Security/Signature/SignatureAlgorithm.php

35
apps/cloud_federation_api/lib/Capabilities.php

@ -6,20 +6,27 @@ declare(strict_types=1);
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\CloudFederationAPI;
use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairException;
use NCU\Security\Signature\Exceptions\SignatoryException;
use OC\OCM\OCMSignatoryManager;
use OCP\Capabilities\ICapability;
use OCP\IAppConfig;
use OCP\IURLGenerator;
use OCP\OCM\Exceptions\OCMArgumentException;
use OCP\OCM\IOCMProvider;
use Psr\Log\LoggerInterface;
class Capabilities implements ICapability {
public const API_VERSION = '1.0-proposal1';
public const API_VERSION = '1.1'; // informative, real version.
public function __construct(
private IURLGenerator $urlGenerator,
private IAppConfig $appConfig,
private IOCMProvider $provider,
private readonly OCMSignatoryManager $ocmSignatoryManager,
private readonly LoggerInterface $logger,
) {
}
@ -28,15 +35,20 @@ class Capabilities implements ICapability {
*
* @return array{
* ocm: array{
* apiVersion: '1.0-proposal1',
* enabled: bool,
* apiVersion: string,
* endPoint: string,
* publicKey: array{
* keyId: string,
* publicKeyPem: string,
* },
* resourceTypes: list<array{
* name: string,
* shareTypes: list<string>,
* protocols: array<string, string>
* }>,
* },
* }>,
* version: string
* }
* }
* @throws OCMArgumentException
*/
@ -60,6 +72,17 @@ class Capabilities implements ICapability {
$this->provider->addResourceType($resource);
return ['ocm' => $this->provider->jsonSerialize()];
// Adding a public key to the ocm discovery
try {
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$this->provider->setSignatory($this->ocmSignatoryManager->getLocalSignatory());
} else {
$this->logger->debug('ocm public key feature disabled');
}
} catch (SignatoryException|KeyPairException $e) {
$this->logger->warning('cannot generate local signatory', ['exception' => $e]);
}
return ['ocm' => json_decode(json_encode($this->provider->jsonSerialize()), true)];
}
}

155
apps/cloud_federation_api/lib/Controller/RequestHandlerController.php

@ -5,6 +5,13 @@
*/
namespace OCA\CloudFederationAPI\Controller;
use NCU\Security\Signature\Exceptions\IncomingRequestException;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
use NCU\Security\Signature\Exceptions\SignatureException;
use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use OC\OCM\OCMSignatoryManager;
use OCA\CloudFederationAPI\Config;
use OCA\CloudFederationAPI\ResponseDefinitions;
use OCP\AppFramework\Controller;
@ -22,11 +29,14 @@ use OCP\Federation\Exceptions\ProviderDoesNotExistsException;
use OCP\Federation\ICloudFederationFactory;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudIdManager;
use OCP\IAppConfig;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IProviderFactory;
use OCP\Share\IShare;
use OCP\Util;
use Psr\Log\LoggerInterface;
@ -50,8 +60,12 @@ class RequestHandlerController extends Controller {
private IURLGenerator $urlGenerator,
private ICloudFederationProviderManager $cloudFederationProviderManager,
private Config $config,
private readonly IAppConfig $appConfig,
private ICloudFederationFactory $factory,
private ICloudIdManager $cloudIdManager,
private readonly ISignatureManager $signatureManager,
private readonly OCMSignatoryManager $signatoryManager,
private readonly IProviderFactory $shareProviderFactory,
) {
parent::__construct($appName, $request);
}
@ -81,11 +95,20 @@ class RequestHandlerController extends Controller {
#[NoCSRFRequired]
#[BruteForceProtection(action: 'receiveFederatedShare')]
public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) {
try {
// if request is signed and well signed, no exception are thrown
// if request is not signed and host is known for not supporting signed request, no exception are thrown
$signedRequest = $this->getSignedRequest();
$this->confirmSignedOrigin($signedRequest, 'owner', $owner);
} catch (IncomingRequestException $e) {
$this->logger->warning('incoming request exception', ['exception' => $e]);
return new JSONResponse(['message' => $e->getMessage(), 'validationErrors' => []], Http::STATUS_BAD_REQUEST);
}
// check if all required parameters are set
if ($shareWith === null ||
$name === null ||
$providerId === null ||
$owner === null ||
$resourceType === null ||
$shareType === null ||
!is_array($protocol) ||
@ -208,6 +231,16 @@ class RequestHandlerController extends Controller {
#[PublicPage]
#[BruteForceProtection(action: 'receiveFederatedShareNotification')]
public function receiveNotification($notificationType, $resourceType, $providerId, ?array $notification) {
try {
// if request is signed and well signed, no exception are thrown
// if request is not signed and host is known for not supporting signed request, no exception are thrown
$signedRequest = $this->getSignedRequest();
$this->confirmShareOrigin($signedRequest, $notification['sharedSecret'] ?? '');
} catch (IncomingRequestException $e) {
$this->logger->warning('incoming request exception', ['exception' => $e]);
return new JSONResponse(['message' => $e->getMessage(), 'validationErrors' => []], Http::STATUS_BAD_REQUEST);
}
// check if all required parameters are set
if ($notificationType === null ||
$resourceType === null ||
@ -286,4 +319,124 @@ class RequestHandlerController extends Controller {
return $uid;
}
/**
* returns signed request if available.
* throw an exception:
* - if request is signed, but wrongly signed
* - if request is not signed but instance is configured to only accept signed ocm request
*
* @return IIncomingSignedRequest|null null if remote does not (and never did) support signed request
* @throws IncomingRequestException
*/
private function getSignedRequest(): ?IIncomingSignedRequest {
try {
return $this->signatureManager->getIncomingSignedRequest($this->signatoryManager);
} catch (SignatureNotFoundException|SignatoryNotFoundException $e) {
// remote does not support signed request.
// currently we still accept unsigned request until lazy appconfig
// core.enforce_signed_ocm_request is set to true (default: false)
if ($this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, lazy: true)) {
$this->logger->notice('ignored unsigned request', ['exception' => $e]);
throw new IncomingRequestException('Unsigned request');
}
} catch (SignatureException $e) {
$this->logger->notice('wrongly signed request', ['exception' => $e]);
throw new IncomingRequestException('Invalid signature');
}
return null;
}
/**
* confirm that the value related to $key entry from the payload is in format userid@hostname
* and compare hostname with the origin of the signed request.
*
* If request is not signed, we still verify that the hostname from the extracted value does,
* actually, not support signed request
*
* @param IIncomingSignedRequest|null $signedRequest
* @param string $key entry from data available in data
* @param string $value value itself used in case request is not signed
*
* @throws IncomingRequestException
*/
private function confirmSignedOrigin(?IIncomingSignedRequest $signedRequest, string $key, string $value): void {
if ($signedRequest === null) {
$instance = $this->getHostFromFederationId($value);
try {
$this->signatureManager->searchSignatory($instance);
throw new IncomingRequestException('instance is supposed to sign its request');
} catch (SignatoryNotFoundException) {
return;
}
}
$body = json_decode($signedRequest->getBody(), true) ?? [];
$entry = trim($body[$key] ?? '', '@');
if ($this->getHostFromFederationId($entry) !== $signedRequest->getOrigin()) {
throw new IncomingRequestException('share initiation from different instance');
}
}
/**
* confirm that the value related to share token is in format userid@hostname
* and compare hostname with the origin of the signed request.
*
* If request is not signed, we still verify that the hostname from the extracted value does,
* actually, not support signed request
*
* @param IIncomingSignedRequest|null $signedRequest
* @param string $token
*
* @return void
* @throws IncomingRequestException
*/
private function confirmShareOrigin(?IIncomingSignedRequest $signedRequest, string $token): void {
if ($token === '') {
throw new BadRequestException(['sharedSecret']);
}
$provider = $this->shareProviderFactory->getProviderForType(IShare::TYPE_REMOTE);
$share = $provider->getShareByToken($token);
$entry = $share->getSharedWith();
$instance = $this->getHostFromFederationId($entry);
if ($signedRequest === null) {
try {
$this->signatureManager->searchSignatory($instance);
throw new IncomingRequestException('instance is supposed to sign its request');
} catch (SignatoryNotFoundException) {
return;
}
} elseif ($instance !== $signedRequest->getOrigin()) {
throw new IncomingRequestException('token sharedWith from different instance');
}
}
/**
* @param string $entry
* @return string
* @throws IncomingRequestException
*/
private function getHostFromFederationId(string $entry): string {
if (!str_contains($entry, '@')) {
throw new IncomingRequestException('entry does not contains @');
}
[, $rightPart] = explode('@', $entry, 2);
$host = parse_url($rightPart, PHP_URL_HOST);
$port = parse_url($rightPart, PHP_URL_PORT);
if ($port !== null && $port !== false) {
$host .= ':' . $port;
}
if (is_string($host) && $host !== '') {
return $host;
}
throw new IncomingRequestException('host is empty');
}
}

33
apps/cloud_federation_api/openapi.json

@ -43,21 +43,41 @@
"ocm": {
"type": "object",
"required": [
"enabled",
"apiVersion",
"enabled",
"endPoint",
"resourceTypes"
"publicKey",
"resourceTypes",
"version"
],
"properties": {
"apiVersion": {
"type": "string",
"enum": [
"1.0-proposal1"
]
},
"enabled": {
"type": "boolean"
},
"apiVersion": {
"type": "string"
},
"endPoint": {
"type": "string"
},
"publicKey": {
"type": "object",
"required": [
"keyId",
"publicKeyPem"
],
"properties": {
"keyId": {
"type": "string"
},
"publicKeyPem": {
"type": "string"
}
}
},
"resourceTypes": {
"type": "array",
"items": {
@ -85,6 +105,9 @@
}
}
}
},
"version": {
"type": "string"
}
}
}

1
apps/files_sharing/lib/External/Storage.php

@ -88,6 +88,7 @@ class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage,
parent::__construct(
[
'secure' => ((parse_url($remote, PHP_URL_SCHEME) ?? 'https') === 'https'),
'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false),
'host' => $host,
'root' => $webDavEndpoint,
'user' => $options['token'],

3
build/integration/federation_features/cleanup-remote-storage.feature

@ -35,6 +35,9 @@ Feature: cleanup-remote-storage
# server may have its own /textfile0.txt" file)
And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
And As an "user1"
And sending "GET" to "/apps/files_sharing/api/v1/shares"
And the list of returned shares has 1 shares
And Using server "LOCAL"
# Accept and download the file to ensure that a storage is created for the
# federated share

12
core/Controller/OCMController.php

@ -17,7 +17,7 @@ use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\Capabilities\ICapability;
use OCP\IConfig;
use OCP\IAppConfig;
use OCP\IRequest;
use OCP\Server;
use Psr\Container\ContainerExceptionInterface;
@ -31,7 +31,7 @@ use Psr\Log\LoggerInterface;
class OCMController extends Controller {
public function __construct(
IRequest $request,
private IConfig $config,
private readonly IAppConfig $appConfig,
private LoggerInterface $logger,
) {
parent::__construct('core', $request);
@ -54,10 +54,10 @@ class OCMController extends Controller {
public function discovery(): DataResponse {
try {
$cap = Server::get(
$this->config->getAppValue(
'core',
'ocm_providers',
'\OCA\CloudFederationAPI\Capabilities'
$this->appConfig->getValueString(
'core', 'ocm_providers',
\OCA\CloudFederationAPI\Capabilities::class,
lazy: true
)
);

135
core/Migrations/Version31000Date20240101084401.php

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\Migrations;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\Attributes\AddIndex;
use OCP\Migration\Attributes\CreateTable;
use OCP\Migration\Attributes\IndexType;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* @since 31.0.0
*/
#[CreateTable(
table: 'sec_signatory',
columns: ['id', 'key_id_sum', 'key_id', 'host', 'provider_id', 'account', 'public_key', 'metadata', 'type', 'status', 'creation', 'last_updated'],
description: 'new table to store remove public/private key pairs'
)]
#[AddIndex(
table: 'sec_signatory',
type: IndexType::PRIMARY
)]
#[AddIndex(
table: 'sec_signatory',
type: IndexType::UNIQUE,
description: 'confirm uniqueness per host, provider and account'
)]
#[AddIndex(
table: 'sec_signatory',
type: IndexType::INDEX,
description: 'to search on key and provider'
)]
class Version31000Date20240101084401 extends SimpleMigrationStep {
public function description(): string {
return "creating new table 'sec_signatory' to store remote signatories";
}
public function name(): string {
return 'create sec_signatory';
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('sec_signatory')) {
$table = $schema->createTable('sec_signatory');
$table->addColumn('id', Types::BIGINT, [
'notnull' => true,
'length' => 64,
'autoincrement' => true,
'unsigned' => true,
]);
// key_id_sum will store a hash version of the key_id, more appropriate for search/index
$table->addColumn('key_id_sum', Types::STRING, [
'notnull' => true,
'length' => 127,
]);
$table->addColumn('key_id', Types::STRING, [
'notnull' => true,
'length' => 512
]);
// host/provider_id/account will help generate a unique entry, not based on key_id
// this way, a spoofed instance cannot publish a new key_id for same host+provider_id
// account will be used only to stored multiple keys for the same provider_id/host
$table->addColumn('host', Types::STRING, [
'notnull' => true,
'length' => 512
]);
$table->addColumn('provider_id', Types::STRING, [
'notnull' => true,
'length' => 31,
]);
$table->addColumn('account', Types::STRING, [
'notnull' => false,
'length' => 127,
'default' => ''
]);
$table->addColumn('public_key', Types::TEXT, [
'notnull' => true,
'default' => ''
]);
$table->addColumn('metadata', Types::TEXT, [
'notnull' => true,
'default' => '[]'
]);
// type+status are informative about the trustability of remote instance and status of the signatory
$table->addColumn('type', Types::SMALLINT, [
'notnull' => true,
'length' => 2,
'default' => 9
]);
$table->addColumn('status', Types::SMALLINT, [
'notnull' => true,
'length' => 2,
'default' => 0,
]);
$table->addColumn('creation', Types::INTEGER, [
'notnull' => false,
'length' => 4,
'default' => 0,
'unsigned' => true,
]);
$table->addColumn('last_updated', Types::INTEGER, [
'notnull' => false,
'length' => 4,
'default' => 0,
'unsigned' => true,
]);
$table->setPrimaryKey(['id'], 'sec_sig_id');
$table->addUniqueIndex(['provider_id', 'host', 'account'], 'sec_sig_unic');
$table->addIndex(['key_id_sum', 'provider_id'], 'sec_sig_key');
return $schema;
}
return null;
}
}

9
lib/composer/composer/autoload_classmap.php

@ -1393,7 +1393,6 @@ return array(
'OC\\Core\\Migrations\\Version30000Date20240814180800' => $baseDir . '/core/Migrations/Version30000Date20240814180800.php',
'OC\\Core\\Migrations\\Version30000Date20240815080800' => $baseDir . '/core/Migrations/Version30000Date20240815080800.php',
'OC\\Core\\Migrations\\Version30000Date20240906095113' => $baseDir . '/core/Migrations/Version30000Date20240906095113.php',
'OC\\Core\\Migrations\\Version31000Date20240814184402' => $baseDir . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20241018063111' => $baseDir . '/core/Migrations/Version31000Date20241018063111.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
@ -1733,6 +1732,7 @@ return array(
'OC\\OCM\\Model\\OCMProvider' => $baseDir . '/lib/private/OCM/Model/OCMProvider.php',
'OC\\OCM\\Model\\OCMResource' => $baseDir . '/lib/private/OCM/Model/OCMResource.php',
'OC\\OCM\\OCMDiscoveryService' => $baseDir . '/lib/private/OCM/OCMDiscoveryService.php',
'OC\\OCM\\OCMSignatoryManager' => $baseDir . '/lib/private/OCM/OCMSignatoryManager.php',
'OC\\OCS\\ApiHelper' => $baseDir . '/lib/private/OCS/ApiHelper.php',
'OC\\OCS\\CoreCapabilities' => $baseDir . '/lib/private/OCS/CoreCapabilities.php',
'OC\\OCS\\DiscoveryService' => $baseDir . '/lib/private/OCS/DiscoveryService.php',
@ -1902,6 +1902,8 @@ return array(
'OC\\Security\\Ip\\Range' => $baseDir . '/lib/private/Security/Ip/Range.php',
'OC\\Security\\Ip\\RemoteAddress' => $baseDir . '/lib/private/Security/Ip/RemoteAddress.php',
'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php',
'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php',
'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php',
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
@ -1909,6 +1911,11 @@ return array(
'OC\\Security\\RateLimiting\\Limiter' => $baseDir . '/lib/private/Security/RateLimiting/Limiter.php',
'OC\\Security\\RemoteHostValidator' => $baseDir . '/lib/private/Security/RemoteHostValidator.php',
'OC\\Security\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php',
'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php',
'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php',
'OC\\Security\\Signature\\Model\\Signatory' => $baseDir . '/lib/private/Security/Signature/Model/Signatory.php',
'OC\\Security\\Signature\\Model\\SignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/SignedRequest.php',
'OC\\Security\\Signature\\SignatureManager' => $baseDir . '/lib/private/Security/Signature/SignatureManager.php',
'OC\\Security\\TrustedDomainHelper' => $baseDir . '/lib/private/Security/TrustedDomainHelper.php',
'OC\\Security\\VerificationToken\\CleanUpJob' => $baseDir . '/lib/private/Security/VerificationToken/CleanUpJob.php',
'OC\\Security\\VerificationToken\\VerificationToken' => $baseDir . '/lib/private/Security/VerificationToken/VerificationToken.php',

9
lib/composer/composer/autoload_static.php

@ -1434,7 +1434,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version30000Date20240814180800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240814180800.php',
'OC\\Core\\Migrations\\Version30000Date20240815080800' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240815080800.php',
'OC\\Core\\Migrations\\Version30000Date20240906095113' => __DIR__ . '/../../..' . '/core/Migrations/Version30000Date20240906095113.php',
'OC\\Core\\Migrations\\Version31000Date20240814184402' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20241018063111' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20241018063111.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
@ -1774,6 +1773,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\OCM\\Model\\OCMProvider' => __DIR__ . '/../../..' . '/lib/private/OCM/Model/OCMProvider.php',
'OC\\OCM\\Model\\OCMResource' => __DIR__ . '/../../..' . '/lib/private/OCM/Model/OCMResource.php',
'OC\\OCM\\OCMDiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMDiscoveryService.php',
'OC\\OCM\\OCMSignatoryManager' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMSignatoryManager.php',
'OC\\OCS\\ApiHelper' => __DIR__ . '/../../..' . '/lib/private/OCS/ApiHelper.php',
'OC\\OCS\\CoreCapabilities' => __DIR__ . '/../../..' . '/lib/private/OCS/CoreCapabilities.php',
'OC\\OCS\\DiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCS/DiscoveryService.php',
@ -1943,6 +1943,8 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Security\\Ip\\Range' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/Range.php',
'OC\\Security\\Ip\\RemoteAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/RemoteAddress.php',
'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php',
'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php',
'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php',
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',
@ -1950,6 +1952,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Security\\RateLimiting\\Limiter' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Limiter.php',
'OC\\Security\\RemoteHostValidator' => __DIR__ . '/../../..' . '/lib/private/Security/RemoteHostValidator.php',
'OC\\Security\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php',
'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php',
'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php',
'OC\\Security\\Signature\\Model\\Signatory' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/Signatory.php',
'OC\\Security\\Signature\\Model\\SignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/SignedRequest.php',
'OC\\Security\\Signature\\SignatureManager' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/SignatureManager.php',
'OC\\Security\\TrustedDomainHelper' => __DIR__ . '/../../..' . '/lib/private/Security/TrustedDomainHelper.php',
'OC\\Security\\VerificationToken\\CleanUpJob' => __DIR__ . '/../../..' . '/lib/private/Security/VerificationToken/CleanUpJob.php',
'OC\\Security\\VerificationToken\\VerificationToken' => __DIR__ . '/../../..' . '/lib/private/Security/VerificationToken/VerificationToken.php',

73
lib/private/Federation/CloudFederationProviderManager.php

@ -8,7 +8,9 @@ declare(strict_types=1);
*/
namespace OC\Federation;
use NCU\Security\Signature\ISignatureManager;
use OC\AppFramework\Http;
use OC\OCM\OCMSignatoryManager;
use OCP\App\IAppManager;
use OCP\Federation\Exceptions\ProviderDoesNotExistsException;
use OCP\Federation\ICloudFederationNotification;
@ -18,6 +20,7 @@ use OCP\Federation\ICloudFederationShare;
use OCP\Federation\ICloudIdManager;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\OCM\Exceptions\OCMProviderException;
use OCP\OCM\IOCMDiscoveryService;
@ -37,9 +40,12 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
public function __construct(
private IConfig $config,
private IAppManager $appManager,
private IAppConfig $appConfig,
private IClientService $httpClientService,
private ICloudIdManager $cloudIdManager,
private IOCMDiscoveryService $discoveryService,
private readonly ISignatureManager $signatureManager,
private readonly OCMSignatoryManager $signatoryManager,
private LoggerInterface $logger,
) {
}
@ -106,9 +112,17 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
$client = $this->httpClientService->newClient();
try {
$response = $client->post($ocmProvider->getEndPoint() . '/shares', array_merge($this->getDefaultRequestOptions(), [
'body' => json_encode($share->getShare()),
]));
// signing the payload using OCMSignatoryManager before initializing the request
$uri = $ocmProvider->getEndPoint() . '/shares';
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]);
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
$payload,
'post', $uri
);
}
$response = $client->post($uri, $signedPayload ?? $payload);
if ($response->getStatusCode() === Http::STATUS_CREATED) {
$result = json_decode($response->getBody(), true);
@ -139,9 +153,18 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
$client = $this->httpClientService->newClient();
try {
return $client->post($ocmProvider->getEndPoint() . '/shares', array_merge($this->getDefaultRequestOptions(), [
'body' => json_encode($share->getShare()),
]));
// signing the payload using OCMSignatoryManager before initializing the request
$uri = $ocmProvider->getEndPoint() . '/shares';
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]);
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
$payload,
'post', $uri
);
}
return $client->post($uri, $signedPayload ?? $payload);
} catch (\Throwable $e) {
$this->logger->error('Error while sending share to federation server: ' . $e->getMessage(), ['exception' => $e]);
try {
@ -167,9 +190,19 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
$client = $this->httpClientService->newClient();
try {
$response = $client->post($ocmProvider->getEndPoint() . '/notifications', array_merge($this->getDefaultRequestOptions(), [
'body' => json_encode($notification->getMessage()),
]));
// signing the payload using OCMSignatoryManager before initializing the request
$uri = $ocmProvider->getEndPoint() . '/notifications';
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]);
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
$payload,
'post', $uri
);
}
$response = $client->post($uri, $signedPayload ?? $payload);
if ($response->getStatusCode() === Http::STATUS_CREATED) {
$result = json_decode($response->getBody(), true);
return (is_array($result)) ? $result : [];
@ -193,9 +226,17 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
$client = $this->httpClientService->newClient();
try {
return $client->post($ocmProvider->getEndPoint() . '/notifications', array_merge($this->getDefaultRequestOptions(), [
'body' => json_encode($notification->getMessage()),
]));
// signing the payload using OCMSignatoryManager before initializing the request
$uri = $ocmProvider->getEndPoint() . '/notifications';
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]);
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
$payload,
'post', $uri
);
}
return $client->post($uri, $signedPayload ?? $payload);
} catch (\Throwable $e) {
$this->logger->error('Error while sending notification to federation server: ' . $e->getMessage(), ['exception' => $e]);
try {
@ -216,15 +257,11 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
}
private function getDefaultRequestOptions(): array {
$options = [
return [
'headers' => ['content-type' => 'application/json'],
'timeout' => 10,
'connect_timeout' => 10,
'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false),
];
if ($this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates')) {
$options['verify'] = false;
}
return $options;
}
}

7
lib/private/Files/Storage/DAV.php

@ -64,6 +64,7 @@ class DAV extends Common {
protected $httpClientService;
/** @var ICertificateManager */
protected $certManager;
protected bool $verify = true;
protected LoggerInterface $logger;
protected IEventLogger $eventLogger;
protected IMimeTypeDetector $mimeTypeDetector;
@ -103,6 +104,7 @@ class DAV extends Common {
if (isset($parameters['authType'])) {
$this->authType = $parameters['authType'];
}
$this->verify = (($parameters['verify'] ?? true) !== false);
if (isset($parameters['secure'])) {
if (is_string($parameters['secure'])) {
$this->secure = ($parameters['secure'] === 'true');
@ -162,6 +164,11 @@ class DAV extends Common {
}
}
if (!$this->verify) {
$this->client->addCurlSetting(CURLOPT_SSL_VERIFYHOST, 0);
$this->client->addCurlSetting(CURLOPT_SSL_VERIFYPEER, false);
}
$lastRequestStart = 0;
$this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) {
$this->logger->debug('sending dav ' . $request->getMethod() . ' request to external storage: ' . $request->getAbsoluteUrl(), ['app' => 'dav']);

45
lib/private/OCM/Model/OCMProvider.php

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace OC\OCM\Model;
use NCU\Security\Signature\Model\ISignatory;
use OC\Security\Signature\Model\Signatory;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\OCM\Events\ResourceTypeRegisterEvent;
use OCP\OCM\Exceptions\OCMArgumentException;
@ -25,7 +27,7 @@ class OCMProvider implements IOCMProvider {
private string $endPoint = '';
/** @var IOCMResource[] */
private array $resourceTypes = [];
private ?ISignatory $signatory = null;
private bool $emittedEvent = false;
public function __construct(
@ -152,6 +154,14 @@ class OCMProvider implements IOCMProvider {
throw new OCMArgumentException('resource not found');
}
public function setSignatory(ISignatory $signatory): void {
$this->signatory = $signatory;
}
public function getSignatory(): ?ISignatory {
return $this->signatory;
}
/**
* import data from an array
*
@ -163,7 +173,7 @@ class OCMProvider implements IOCMProvider {
*/
public function import(array $data): static {
$this->setEnabled(is_bool($data['enabled'] ?? '') ? $data['enabled'] : false)
->setApiVersion((string)($data['apiVersion'] ?? ''))
->setApiVersion((string)($data['version'] ?? ''))
->setEndPoint($data['endPoint'] ?? '');
$resources = [];
@ -173,6 +183,12 @@ class OCMProvider implements IOCMProvider {
}
$this->setResourceTypes($resources);
// import details about the remote request signing public key, if available
$signatory = new Signatory($data['publicKey']['keyId'] ?? '', $data['publicKey']['publicKeyPem'] ?? '');
if ($signatory->getKeyId() !== '' && $signatory->getPublicKey() !== '') {
$this->setSignatory($signatory);
}
if (!$this->looksValid()) {
throw new OCMProviderException('remote provider does not look valid');
}
@ -188,18 +204,19 @@ class OCMProvider implements IOCMProvider {
return ($this->getApiVersion() !== '' && $this->getEndPoint() !== '');
}
/**
* @return array{
* enabled: bool,
* apiVersion: string,
* endPoint: string,
* resourceTypes: list<array{
* name: string,
* shareTypes: list<string>,
* protocols: array<string, string>
* }>,
* }
* enabled: bool,
* apiVersion: '1.0-proposal1',
* endPoint: string,
* publicKey: ISignatory|null,
* resourceTypes: array{
* name: string,
* shareTypes: list<string>,
* protocols: array<string, string>
* }[],
* version: string
* }
*/
public function jsonSerialize(): array {
$resourceTypes = [];
@ -209,8 +226,10 @@ class OCMProvider implements IOCMProvider {
return [
'enabled' => $this->isEnabled(),
'apiVersion' => $this->getApiVersion(),
'apiVersion' => '1.0-proposal1', // deprecated, but keep it to stay compatible with old version
'version' => $this->getApiVersion(), // informative but real version
'endPoint' => $this->getEndPoint(),
'publicKey' => $this->getSignatory(),
'resourceTypes' => $resourceTypes
];
}

35
lib/private/OCM/OCMDiscoveryService.php

@ -25,12 +25,6 @@ use Psr\Log\LoggerInterface;
*/
class OCMDiscoveryService implements IOCMDiscoveryService {
private ICache $cache;
private array $supportedAPIVersion =
[
'1.0-proposal1',
'1.0',
'1.1'
];
public function __construct(
ICacheFactory $cacheFactory,
@ -61,9 +55,7 @@ class OCMDiscoveryService implements IOCMDiscoveryService {
}
$this->provider->import(json_decode($cached ?? '', 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
}
@ -101,31 +93,6 @@ class OCMDiscoveryService implements IOCMDiscoveryService {
throw new OCMProviderException('error while requesting remote ocm provider');
}
if (!$this->supportedAPIVersion($this->provider->getApiVersion())) {
$this->cache->set($remote, false, 5 * 60);
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));
}
}

149
lib/private/OCM/OCMSignatoryManager.php

@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OCM;
use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairConflictException;
use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairNotFoundException;
use NCU\Security\PublicPrivateKeyPairs\IKeyPairManager;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\ISignatoryManager;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
use NCU\Security\Signature\Model\SignatoryType;
use OC\Security\Signature\Model\Signatory;
use OCP\IAppConfig;
use OCP\IURLGenerator;
use OCP\OCM\Exceptions\OCMProviderException;
/**
* @inheritDoc
*
* returns local signatory using IKeyPairManager
* extract optional signatory (keyId+public key) from ocm discovery service on remote instance
*
* @since 31.0.0
*/
class OCMSignatoryManager implements ISignatoryManager {
public const PROVIDER_ID = 'ocm';
public const APPCONFIG_SIGN_IDENTITY_EXTERNAL = 'ocm_signed_request_identity_external';
public const APPCONFIG_SIGN_DISABLED = 'ocm_signed_request_disabled';
public const APPCONFIG_SIGN_ENFORCED = 'ocm_signed_request_enforced';
public function __construct(
private readonly IAppConfig $appConfig,
private readonly ISignatureManager $signatureManager,
private readonly IURLGenerator $urlGenerator,
private readonly IKeyPairManager $keyPairManager,
private readonly OCMDiscoveryService $ocmDiscoveryService,
) {
}
/**
* @inheritDoc
*
* @since 31.0.0
* @return string
*/
public function getProviderId(): string {
return self::PROVIDER_ID;
}
/**
* @inheritDoc
*
* @since 31.0.0
* @return array
*/
public function getOptions(): array {
return [];
}
/**
* @inheritDoc
*
* @return ISignatory
* @throws KeyPairConflictException
* @throws IdentityNotFoundException
* @since 31.0.0
*/
public function getLocalSignatory(): ISignatory {
/**
* TODO: manage multiple identity (external, internal, ...) to allow a limitation
* based on the requested interface (ie. only accept shares from globalscale)
*/
if ($this->appConfig->hasKey('core', self::APPCONFIG_SIGN_IDENTITY_EXTERNAL, true)) {
$identity = $this->appConfig->getValueString('core', self::APPCONFIG_SIGN_IDENTITY_EXTERNAL, lazy: true);
$keyId = 'https://' . $identity . '/ocm#signature';
} else {
$keyId = $this->generateKeyId();
}
try {
$keyPair = $this->keyPairManager->getKeyPair('core', 'ocm_external');
} catch (KeyPairNotFoundException) {
$keyPair = $this->keyPairManager->generateKeyPair('core', 'ocm_external');
}
return new Signatory($keyId, $keyPair->getPublicKey(), $keyPair->getPrivateKey(), local: true);
}
/**
* - tries to generate a keyId using global configuration (from signature manager) if available
* - generate a keyId using the current route to ocm shares
*
* @return string
* @throws IdentityNotFoundException
*/
private function generateKeyId(): string {
try {
return $this->signatureManager->generateKeyIdFromConfig('/ocm#signature');
} catch (IdentityNotFoundException) {
}
$url = $this->urlGenerator->linkToRouteAbsolute('cloud_federation_api.requesthandlercontroller.addShare');
$identity = $this->signatureManager->extractIdentityFromUri($url);
// catching possible subfolder to create a keyId like 'https://hostname/subfolder/ocm#signature
$path = parse_url($url, PHP_URL_PATH);
$pos = strpos($path, '/ocm/shares');
$sub = ($pos) ? substr($path, 0, $pos) : '';
return 'https://' . $identity . $sub . '/ocm#signature';
}
/**
* @inheritDoc
*
* @param IIncomingSignedRequest $signedRequest
*
* @return ISignatory|null must be NULL if no signatory is found
* @throws OCMProviderException on fail to discover ocm services
* @since 31.0.0
*/
public function getRemoteSignatory(IIncomingSignedRequest $signedRequest): ?ISignatory {
return $this->getRemoteSignatoryFromHost($signedRequest->getOrigin());
}
/**
* As host is enough to generate signatory using OCMDiscoveryService
*
* @param string $host
*
* @return ISignatory|null
* @throws OCMProviderException on fail to discover ocm services
* @since 31.0.0
*/
public function getRemoteSignatoryFromHost(string $host): ?ISignatory {
$ocmProvider = $this->ocmDiscoveryService->discover($host, true);
$signatory = $ocmProvider->getSignatory();
return $signatory?->setType(SignatoryType::TRUSTED);
}
}

182
lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php

@ -0,0 +1,182 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\PublicPrivateKeyPairs;
use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairConflictException;
use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairNotFoundException;
use NCU\Security\PublicPrivateKeyPairs\IKeyPairManager;
use NCU\Security\PublicPrivateKeyPairs\Model\IKeyPair;
use OC\Security\PublicPrivateKeyPairs\Model\KeyPair;
use OCP\IAppConfig;
/**
* @inheritDoc
*
* KeyPairManager store internal public/private key pair using AppConfig, taking advantage of the encryption
* and lazy loading.
*
* @since 31.0.0
*/
class KeyPairManager implements IKeyPairManager {
private const CONFIG_PREFIX = 'security.keypair.';
public function __construct(
private readonly IAppConfig $appConfig,
) {
}
/**
* @inheritDoc
*
* @param string $app appId
* @param string $name key name
* @param array $options algorithms, metadata
*
* @return IKeyPair
* @throws KeyPairConflictException if a key already exist
* @since 31.0.0
*/
public function generateKeyPair(string $app, string $name, array $options = []): IKeyPair {
if ($this->hasKeyPair($app, $name)) {
throw new KeyPairConflictException('key pair already exist');
}
$keyPair = new KeyPair($app, $name);
[$publicKey, $privateKey] = $this->generateKeys($options);
$keyPair->setPublicKey($publicKey)
->setPrivateKey($privateKey)
->setOptions($options);
$this->appConfig->setValueArray(
$app, $this->generateAppConfigKey($name),
[
'public' => $keyPair->getPublicKey(),
'private' => $keyPair->getPrivateKey(),
'options' => $keyPair->getOptions()
],
lazy: true,
sensitive: true
);
return $keyPair;
}
/**
* @inheritDoc
*
* @param string $app appId
* @param string $name key name
*
* @return bool TRUE if key pair exists in database
* @since 31.0.0
*/
public function hasKeyPair(string $app, string $name): bool {
$key = $this->generateAppConfigKey($name);
return $this->appConfig->hasKey($app, $key, lazy: true);
}
/**
* @inheritDoc
*
* @param string $app appId
* @param string $name key name
*
* @return IKeyPair
* @throws KeyPairNotFoundException if key pair is not known
* @since 31.0.0
*/
public function getKeyPair(string $app, string $name): IKeyPair {
if (!$this->hasKeyPair($app, $name)) {
throw new KeyPairNotFoundException('unknown key pair');
}
$key = $this->generateAppConfigKey($name);
$stored = $this->appConfig->getValueArray($app, $key, lazy: true);
if (!array_key_exists('public', $stored) ||
!array_key_exists('private', $stored)) {
throw new KeyPairNotFoundException('corrupted key pair');
}
$keyPair = new KeyPair($app, $name);
return $keyPair->setPublicKey($stored['public'])
->setPrivateKey($stored['private'])
->setOptions($stored['options'] ?? []);
}
/**
* @inheritDoc
*
* @param string $app appid
* @param string $name key name
*
* @since 31.0.0
*/
public function deleteKeyPair(string $app, string $name): void {
$this->appConfig->deleteKey('core', $this->generateAppConfigKey($name));
}
/**
* @inheritDoc
*
* @param IKeyPair $keyPair keypair to test
*
* @return bool
* @since 31.0.0
*/
public function testKeyPair(IKeyPair $keyPair): bool {
$clear = md5((string)time());
// signing with private key
openssl_sign($clear, $signed, $keyPair->getPrivateKey(), OPENSSL_ALGO_SHA256);
$encoded = base64_encode($signed);
// verify with public key
$signed = base64_decode($encoded);
return (openssl_verify($clear, $signed, $keyPair->getPublicKey(), 'sha256') === 1);
}
/**
* return appconfig key based on name of the key pair
*
* @param string $name
*
* @return string
*/
private function generateAppConfigKey(string $name): string {
return self::CONFIG_PREFIX . $name;
}
/**
* generate the key pair, based on $options with the following default values:
* [
* 'algorithm' => 'rsa',
* 'bits' => 2048,
* 'type' => OPENSSL_KEYTYPE_RSA
* ]
*
* @param array $options
*
* @return array
*/
private function generateKeys(array $options = []): array {
$res = openssl_pkey_new(
[
'digest_alg' => $options['algorithm'] ?? 'rsa',
'private_key_bits' => $options['bits'] ?? 2048,
'private_key_type' => $options['type'] ?? OPENSSL_KEYTYPE_RSA,
]
);
openssl_pkey_export($res, $privateKey);
$publicKey = openssl_pkey_get_details($res)['key'];
return [$publicKey, $privateKey];
}
}

114
lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php

@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\PublicPrivateKeyPairs\Model;
use NCU\Security\PublicPrivateKeyPairs\Model\IKeyPair;
/**
* @inheritDoc
*
* @since 31.0.0
*/
class KeyPair implements IKeyPair {
private string $publicKey = '';
private string $privateKey = '';
private array $options = [];
public function __construct(
private readonly string $app,
private readonly string $name,
) {
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getApp(): string {
return $this->app;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getName(): string {
return $this->name;
}
/**
* @inheritDoc
*
* @param string $publicKey
* @return IKeyPair
* @since 31.0.0
*/
public function setPublicKey(string $publicKey): IKeyPair {
$this->publicKey = $publicKey;
return $this;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getPublicKey(): string {
return $this->publicKey;
}
/**
* @inheritDoc
*
* @param string $privateKey
* @return IKeyPair
* @since 31.0.0
*/
public function setPrivateKey(string $privateKey): IKeyPair {
$this->privateKey = $privateKey;
return $this;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getPrivateKey(): string {
return $this->privateKey;
}
/**
* @inheritDoc
*
* @param array $options
* @return IKeyPair
* @since 31.0.0
*/
public function setOptions(array $options): IKeyPair {
$this->options = $options;
return $this;
}
/**
* @inheritDoc
*
* @return array
* @since 31.0.0
*/
public function getOptions(): array {
return $this->options;
}
}

170
lib/private/Security/Signature/Model/IncomingSignedRequest.php

@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Signature\Model;
use JsonSerializable;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\Exceptions\IncomingRequestNotFoundException;
use NCU\Security\Signature\Exceptions\SignatoryException;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
use OCP\IRequest;
/**
* @inheritDoc
*
* @see ISignatureManager for details on signature
* @since 31.0.0
*/
class IncomingSignedRequest extends SignedRequest implements
IIncomingSignedRequest,
JsonSerializable {
private ?IRequest $request = null;
private int $time = 0;
private string $origin = '';
private string $estimatedSignature = '';
/**
* @inheritDoc
*
* @param ISignatory $signatory
*
* @return $this
* @throws SignatoryException
* @throws IdentityNotFoundException
* @since 31.0.0
*/
public function setSignatory(ISignatory $signatory): self {
$identity = \OCP\Server::get(ISignatureManager::class)->extractIdentityFromUri($signatory->getKeyId());
if ($identity !== $this->getOrigin()) {
throw new SignatoryException('keyId from provider is different from the one from signed request');
}
parent::setSignatory($signatory);
return $this;
}
/**
* @inheritDoc
*
* @param IRequest $request
* @return IIncomingSignedRequest
* @since 31.0.0
*/
public function setRequest(IRequest $request): IIncomingSignedRequest {
$this->request = $request;
return $this;
}
/**
* @inheritDoc
*
* @return IRequest
* @throws IncomingRequestNotFoundException
* @since 31.0.0
*/
public function getRequest(): IRequest {
if ($this->request === null) {
throw new IncomingRequestNotFoundException();
}
return $this->request;
}
/**
* @inheritDoc
*
* @param int $time
* @return IIncomingSignedRequest
* @since 31.0.0
*/
public function setTime(int $time): IIncomingSignedRequest {
$this->time = $time;
return $this;
}
/**
* @inheritDoc
*
* @return int
* @since 31.0.0
*/
public function getTime(): int {
return $this->time;
}
/**
* @inheritDoc
*
* @param string $origin
* @return IIncomingSignedRequest
* @since 31.0.0
*/
public function setOrigin(string $origin): IIncomingSignedRequest {
$this->origin = $origin;
return $this;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getOrigin(): string {
return $this->origin;
}
/**
* returns the keyId extracted from the signature headers.
* keyId is a mandatory entry in the headers of a signed request.
*
* @return string
* @since 31.0.0
*/
public function getKeyId(): string {
return $this->getSignatureHeader()['keyId'] ?? '';
}
/**
* @inheritDoc
*
* @param string $signature
* @return IIncomingSignedRequest
* @since 31.0.0
*/
public function setEstimatedSignature(string $signature): IIncomingSignedRequest {
$this->estimatedSignature = $signature;
return $this;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getEstimatedSignature(): string {
return $this->estimatedSignature;
}
public function jsonSerialize(): array {
return array_merge(
parent::jsonSerialize(),
[
'body' => $this->getBody(),
'time' => $this->getTime(),
'incomingRequest' => $this->request ?? false,
'origin' => $this->getOrigin(),
'keyId' => $this->getKeyId(),
'estimatedSignature' => $this->getEstimatedSignature(),
]
);
}
}

131
lib/private/Security/Signature/Model/OutgoingSignedRequest.php

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Signature\Model;
use JsonSerializable;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\IOutgoingSignedRequest;
/**
* extends ISignedRequest to add info requested at the generation of the signature
*
* @see ISignatureManager for details on signature
* @since 31.0.0
*/
class OutgoingSignedRequest extends SignedRequest implements
IOutgoingSignedRequest,
JsonSerializable {
private string $host = '';
private array $headers = [];
private string $clearSignature = '';
private string $algorithm;
/**
* @inheritDoc
*
* @param string $host
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function setHost(string $host): IOutgoingSignedRequest {
$this->host = $host;
return $this;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getHost(): string {
return $this->host;
}
/**
* @inheritDoc
*
* @param string $key
* @param string|int|float|bool|array $value
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function addHeader(string $key, string|int|float|bool|array $value): IOutgoingSignedRequest {
$this->headers[$key] = $value;
return $this;
}
/**
* @inheritDoc
*
* @return array
* @since 31.0.0
*/
public function getHeaders(): array {
return $this->headers;
}
/**
* @inheritDoc
*
* @param string $estimated
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function setClearSignature(string $estimated): IOutgoingSignedRequest {
$this->clearSignature = $estimated;
return $this;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getClearSignature(): string {
return $this->clearSignature;
}
/**
* @inheritDoc
*
* @param string $algorithm
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function setAlgorithm(string $algorithm): IOutgoingSignedRequest {
$this->algorithm = $algorithm;
return $this;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getAlgorithm(): string {
return $this->algorithm;
}
public function jsonSerialize(): array {
return array_merge(
parent::jsonSerialize(),
[
'headers' => $this->headers,
'host' => $this->getHost(),
'clearSignature' => $this->getClearSignature(),
]
);
}
}

147
lib/private/Security/Signature/Model/Signatory.php

@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Signature\Model;
use JsonSerializable;
use NCU\Security\Signature\Model\ISignatory;
use NCU\Security\Signature\Model\SignatoryStatus;
use NCU\Security\Signature\Model\SignatoryType;
class Signatory implements ISignatory, JsonSerializable {
private string $providerId = '';
private string $account = '';
private SignatoryType $type = SignatoryType::STATIC;
private SignatoryStatus $status = SignatoryStatus::SYNCED;
private array $metadata = [];
private int $creation = 0;
private int $lastUpdated = 0;
public function __construct(
private string $keyId,
private readonly string $publicKey,
private readonly string $privateKey = '',
readonly bool $local = false,
) {
// if set as local (for current instance), we apply some filters.
if ($local) {
// to avoid conflict with duplicate key pairs (ie generated url from the occ command), we enforce https as prefix
if (str_starts_with($keyId, 'http://')) {
$keyId = 'https://' . substr($keyId, 7);
}
// removing /index.php from generated url
$path = parse_url($keyId, PHP_URL_PATH);
if (str_starts_with($path, '/index.php/')) {
$pos = strpos($keyId, '/index.php');
if ($pos !== false) {
$keyId = substr_replace($keyId, '', $pos, 10);
}
}
$this->keyId = $keyId;
}
}
public function setProviderId(string $providerId): self {
$this->providerId = $providerId;
return $this;
}
public function getProviderId(): string {
return $this->providerId;
}
public function setAccount(string $account): self {
$this->account = $account;
return $this;
}
public function getAccount(): string {
return $this->account;
}
public function getKeyId(): string {
return $this->keyId;
}
public function getPublicKey(): string {
return $this->publicKey;
}
public function getPrivateKey(): string {
return $this->privateKey;
}
public function setMetadata(array $metadata): self {
$this->metadata = $metadata;
return $this;
}
public function getMetadata(): array {
return $this->metadata;
}
public function setMetaValue(string $key, string|int $value): self {
$this->metadata[$key] = $value;
return $this;
}
public function setType(SignatoryType $type): self {
$this->type = $type;
return $this;
}
public function getType(): SignatoryType {
return $this->type;
}
public function setStatus(SignatoryStatus $status): self {
$this->status = $status;
return $this;
}
public function getStatus(): SignatoryStatus {
return $this->status;
}
public function setCreation(int $creation): self {
$this->creation = $creation;
return $this;
}
public function getCreation(): int {
return $this->creation;
}
public function setLastUpdated(int $lastUpdated): self {
$this->lastUpdated = $lastUpdated;
return $this;
}
public function getLastUpdated(): int {
return $this->lastUpdated;
}
public function importFromDatabase(array $row): self {
$this->setProviderId($row['provider_id'] ?? '')
->setAccount($row['account'] ?? '')
->setMetadata(json_decode($row['metadata'], true) ?? [])
->setType(SignatoryType::from($row['type'] ?? 9))
->setStatus(SignatoryStatus::from($row['status'] ?? 1))
->setCreation($row['creation'] ?? 0)
->setLastUpdated($row['last_updated'] ?? 0);
return $this;
}
public function jsonSerialize(): array {
return [
'keyId' => $this->getKeyId(),
'publicKeyPem' => $this->getPublicKey()
];
}
}

143
lib/private/Security/Signature/Model/SignedRequest.php

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Signature\Model;
use JsonSerializable;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
use NCU\Security\Signature\Model\ISignatory;
use NCU\Security\Signature\Model\ISignedRequest;
/**
* @inheritDoc
*
* @since 31.0.0
*/
class SignedRequest implements ISignedRequest, JsonSerializable {
private string $digest;
private string $signedSignature = '';
private array $signatureHeader = [];
private ?ISignatory $signatory = null;
public function __construct(
private readonly string $body,
) {
// digest is created on the fly using $body
$this->digest = 'SHA-256=' . base64_encode(hash('sha256', utf8_encode($body), true));
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getBody(): string {
return $this->body;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getDigest(): string {
return $this->digest;
}
/**
* @inheritDoc
*
* @param array $signatureHeader
* @return ISignedRequest
* @since 31.0.0
*/
public function setSignatureHeader(array $signatureHeader): ISignedRequest {
$this->signatureHeader = $signatureHeader;
return $this;
}
/**
* @inheritDoc
*
* @return array
* @since 31.0.0
*/
public function getSignatureHeader(): array {
return $this->signatureHeader;
}
/**
* @inheritDoc
*
* @param string $signedSignature
* @return ISignedRequest
* @since 31.0.0
*/
public function setSignedSignature(string $signedSignature): ISignedRequest {
$this->signedSignature = $signedSignature;
return $this;
}
/**
* @inheritDoc
*
* @return string
* @since 31.0.0
*/
public function getSignedSignature(): string {
return $this->signedSignature;
}
/**
* @inheritDoc
*
* @param ISignatory $signatory
* @return ISignedRequest
* @since 31.0.0
*/
public function setSignatory(ISignatory $signatory): ISignedRequest {
$this->signatory = $signatory;
return $this;
}
/**
* @inheritDoc
*
* @return ISignatory
* @throws SignatoryNotFoundException
* @since 31.0.0
*/
public function getSignatory(): ISignatory {
if ($this->signatory === null) {
throw new SignatoryNotFoundException();
}
return $this->signatory;
}
/**
* @inheritDoc
*
* @return bool
* @since 31.0.0
*/
public function hasSignatory(): bool {
return ($this->signatory !== null);
}
public function jsonSerialize(): array {
return [
'body' => $this->getBody(),
'signatureHeader' => $this->getSignatureHeader(),
'signedSignature' => $this->getSignedSignature(),
'signatory' => $this->signatory ?? false,
];
}
}

828
lib/private/Security/Signature/SignatureManager.php

@ -0,0 +1,828 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Security\Signature;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\Exceptions\IncomingRequestException;
use NCU\Security\Signature\Exceptions\InvalidKeyOriginException;
use NCU\Security\Signature\Exceptions\InvalidSignatureException;
use NCU\Security\Signature\Exceptions\SignatoryConflictException;
use NCU\Security\Signature\Exceptions\SignatoryException;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
use NCU\Security\Signature\Exceptions\SignatureException;
use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
use NCU\Security\Signature\ISignatoryManager;
use NCU\Security\Signature\ISignatureManager;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\IOutgoingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
use NCU\Security\Signature\Model\SignatoryType;
use NCU\Security\Signature\SignatureAlgorithm;
use OC\Security\Signature\Model\IncomingSignedRequest;
use OC\Security\Signature\Model\OutgoingSignedRequest;
use OC\Security\Signature\Model\Signatory;
use OCP\DB\Exception as DBException;
use OCP\IAppConfig;
use OCP\IDBConnection;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
/**
* ISignatureManager is a service integrated to core that provide tools
* to set/get authenticity of/from outgoing/incoming request.
*
* Quick description of the signature, added to the headers
* {
* "(request-target)": "post /path",
* "content-length": 385,
* "date": "Mon, 08 Jul 2024 14:16:20 GMT",
* "digest": "SHA-256=U7gNVUQiixe5BRbp4Tg0xCZMTcSWXXUZI2\\/xtHM40S0=",
* "host": "hostname.of.the.recipient",
* "Signature": "keyId=\"https://author.hostname/key\",algorithm=\"ras-sha256\",headers=\"content-length
* date digest host\",signature=\"DzN12OCS1rsA[...]o0VmxjQooRo6HHabg==\""
* }
*
* 'content-length' is the total length of the data/content
* 'date' is the datetime the request have been initiated
* 'digest' is a checksum of the data/content
* 'host' is the hostname of the recipient of the request (remote when signing outgoing request, local on
* incoming request)
* 'Signature' contains the signature generated using the private key, and metadata:
* - 'keyId' is a unique id, formatted as an url. hostname is used to retrieve the public key via custom
* discovery
* - 'algorithm' define the algorithm used to generate signature
* - 'headers' contains a list of element used during the generation of the signature
* - 'signature' is the encrypted string, using local private key, of an array containing elements
* listed in 'headers' and their value. Some elements (content-length date digest host) are mandatory
* to ensure authenticity override protection.
*
* @since 31.0.0
*/
class SignatureManager implements ISignatureManager {
private const DATE_HEADER = 'D, d M Y H:i:s T';
private const DATE_TTL = 300;
private const SIGNATORY_TTL = 86400 * 3;
private const TABLE_SIGNATORIES = 'sec_signatory';
private const BODY_MAXSIZE = 50000; // max size of the payload of the request
public const APPCONFIG_IDENTITY = 'security.signature.identity';
public function __construct(
private readonly IRequest $request,
private readonly IDBConnection $connection,
private readonly IAppConfig $appConfig,
private readonly LoggerInterface $logger,
) {
}
/**
* @inheritDoc
*
* @param ISignatoryManager $signatoryManager used to get details about remote instance
* @param string|null $body if NULL, body will be extracted from php://input
*
* @return IIncomingSignedRequest
* @throws IncomingRequestException if anything looks wrong with the incoming request
* @throws SignatureNotFoundException if incoming request is not signed
* @throws SignatureException if signature could not be confirmed
* @since 31.0.0
*/
public function getIncomingSignedRequest(
ISignatoryManager $signatoryManager,
?string $body = null,
): IIncomingSignedRequest {
$body = $body ?? file_get_contents('php://input');
if (strlen($body) > self::BODY_MAXSIZE) {
throw new IncomingRequestException('content of request is too big');
}
$signedRequest = new IncomingSignedRequest($body);
$signedRequest->setRequest($this->request);
$options = $signatoryManager->getOptions();
try {
$this->verifyIncomingRequestTime($signedRequest, $options['ttl'] ?? self::DATE_TTL);
$this->verifyIncomingRequestContent($signedRequest);
$this->prepIncomingSignatureHeader($signedRequest);
$this->verifyIncomingSignatureHeader($signedRequest);
$this->prepEstimatedSignature($signedRequest, $options['extraSignatureHeaders'] ?? []);
$this->verifyIncomingRequestSignature(
$signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL
);
} catch (SignatureException $e) {
$this->logger->warning(
'signature could not be verified', [
'exception' => $e, 'signedRequest' => $signedRequest,
'signatoryManager' => get_class($signatoryManager)
]
);
throw $e;
}
return $signedRequest;
}
/**
* @inheritDoc
*
* @param ISignatoryManager $signatoryManager
* @param string $content body to be signed
* @param string $method needed in the signature
* @param string $uri needed in the signature
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function getOutgoingSignedRequest(
ISignatoryManager $signatoryManager,
string $content,
string $method,
string $uri,
): IOutgoingSignedRequest {
$signedRequest = new OutgoingSignedRequest($content);
$options = $signatoryManager->getOptions();
$signedRequest->setHost($this->getHostFromUri($uri))
->setAlgorithm($options['algorithm'] ?? 'sha256')
->setSignatory($signatoryManager->getLocalSignatory());
$this->setOutgoingSignatureHeader(
$signedRequest,
strtolower($method),
parse_url($uri, PHP_URL_PATH) ?? '/',
$options['dateHeader'] ?? self::DATE_HEADER
);
$this->setOutgoingClearSignature($signedRequest);
$this->setOutgoingSignedSignature($signedRequest);
$this->signingOutgoingRequest($signedRequest);
return $signedRequest;
}
/**
* @inheritDoc
*
* @param ISignatoryManager $signatoryManager
* @param array $payload original payload, will be used to sign and completed with new headers with
* signature elements
* @param string $method needed in the signature
* @param string $uri needed in the signature
*
* @return array new payload to be sent, including original payload and signature elements in headers
* @since 31.0.0
*/
public function signOutgoingRequestIClientPayload(
ISignatoryManager $signatoryManager,
array $payload,
string $method,
string $uri,
): array {
$signedRequest = $this->getOutgoingSignedRequest($signatoryManager, $payload['body'], $method, $uri);
$payload['headers'] = array_merge($payload['headers'], $signedRequest->getHeaders());
return $payload;
}
/**
* @inheritDoc
*
* @param string $host remote host
* @param string $account linked account, should be used when multiple signature can exist for the same
* host
*
* @return ISignatory
* @throws SignatoryNotFoundException if entry does not exist in local database
* @since 31.0.0
*/
public function searchSignatory(string $host, string $account = ''): ISignatory {
$qb = $this->connection->getQueryBuilder();
$qb->select(
'id', 'provider_id', 'host', 'account', 'key_id', 'key_id_sum', 'public_key', 'metadata', 'type',
'status', 'creation', 'last_updated'
);
$qb->from(self::TABLE_SIGNATORIES);
$qb->where($qb->expr()->eq('host', $qb->createNamedParameter($host)));
$qb->andWhere($qb->expr()->eq('account', $qb->createNamedParameter($account)));
$result = $qb->executeQuery();
$row = $result->fetch();
$result->closeCursor();
if (!$row) {
throw new SignatoryNotFoundException('no signatory found');
}
$signature = new Signatory($row['key_id'], $row['public_key']);
return $signature->importFromDatabase($row);
}
/**
* @inheritDoc
*
* keyId is set using app config 'core/security.signature.identity'
*
* @param string $path
*
* @return string
* @throws IdentityNotFoundException is identity is not set in app config
* @since 31.0.0
*/
public function generateKeyIdFromConfig(string $path): string {
if (!$this->appConfig->hasKey('core', self::APPCONFIG_IDENTITY, true)) {
throw new IdentityNotFoundException(self::APPCONFIG_IDENTITY . ' not set');
}
$identity = trim($this->appConfig->getValueString('core', self::APPCONFIG_IDENTITY, lazy: true), '/');
return 'https://' . $identity . '/' . ltrim($path, '/');
}
/**
* @inheritDoc
*
* @param string $uri
*
* @return string
* @throws IdentityNotFoundException if identity cannot be extracted
* @since 31.0.0
*/
public function extractIdentityFromUri(string $uri): string {
$identity = parse_url($uri, PHP_URL_HOST);
$port = parse_url($uri, PHP_URL_PORT);
if ($identity === null || $identity === false) {
throw new IdentityNotFoundException('cannot extract identity from ' . $uri);
}
if ($port !== null && $port !== false) {
$identity .= ':' . $port;
}
return $identity;
}
/**
* using the requested 'date' entry from header to confirm request is not older than ttl
*
* @param IIncomingSignedRequest $signedRequest
* @param int $ttl
*
* @throws IncomingRequestException
* @throws SignatureNotFoundException
*/
private function verifyIncomingRequestTime(IIncomingSignedRequest $signedRequest, int $ttl): void {
$request = $signedRequest->getRequest();
$date = $request->getHeader('date');
if ($date === '') {
throw new SignatureNotFoundException('missing date in header');
}
try {
$dTime = new \DateTime($date);
$signedRequest->setTime($dTime->getTimestamp());
} catch (\Exception $e) {
$this->logger->warning(
'datetime exception', ['exception' => $e, 'header' => $request->getHeader('date')]
);
throw new IncomingRequestException('datetime exception');
}
if ($signedRequest->getTime() < (time() - $ttl)) {
throw new IncomingRequestException('object is too old');
}
}
/**
* confirm the values of 'content-length' and 'digest' from header
* is related to request content
*
* @param IIncomingSignedRequest $signedRequest
*
* @throws IncomingRequestException
* @throws SignatureNotFoundException
*/
private function verifyIncomingRequestContent(IIncomingSignedRequest $signedRequest): void {
$request = $signedRequest->getRequest();
$contentLength = $request->getHeader('content-length');
if ($contentLength === '') {
throw new SignatureNotFoundException('missing content-length in header');
}
if (strlen($signedRequest->getBody()) !== (int)$request->getHeader('content-length')) {
throw new IncomingRequestException(
'inexact content-length in header: ' . strlen($signedRequest->getBody()) . ' vs '
. (int)$request->getHeader('content-length')
);
}
$digest = $request->getHeader('digest');
if ($digest === '') {
throw new SignatureNotFoundException('missing digest in header');
}
if ($digest !== $signedRequest->getDigest()) {
throw new IncomingRequestException('invalid value for digest in header');
}
}
/**
* preparing a clear version of the signature based on list of metadata from the
* Signature entry in header
*
* @param IIncomingSignedRequest $signedRequest
*
* @throws SignatureNotFoundException
*/
private function prepIncomingSignatureHeader(IIncomingSignedRequest $signedRequest): void {
$sign = [];
$request = $signedRequest->getRequest();
$signature = $request->getHeader('Signature');
if ($signature === '') {
throw new SignatureNotFoundException('missing Signature in header');
}
foreach (explode(',', $signature) as $entry) {
if ($entry === '' || !strpos($entry, '=')) {
continue;
}
[$k, $v] = explode('=', $entry, 2);
preg_match('/"([^"]+)"/', $v, $var);
if ($var[0] !== '') {
$v = trim($var[0], '"');
}
$sign[$k] = $v;
}
$signedRequest->setSignatureHeader($sign);
}
/**
* @param IIncomingSignedRequest $signedRequest
*
* @throws IncomingRequestException
* @throws InvalidKeyOriginException
*/
private function verifyIncomingSignatureHeader(IIncomingSignedRequest $signedRequest): void {
$data = $signedRequest->getSignatureHeader();
if (!array_key_exists('keyId', $data) || !array_key_exists('headers', $data)
|| !array_key_exists('signature', $data)) {
throw new IncomingRequestException('missing keys in signature headers: ' . json_encode($data));
}
try {
$signedRequest->setOrigin($this->getHostFromUri($data['keyId']));
} catch (\Exception) {
throw new InvalidKeyOriginException('cannot retrieve origin from ' . $data['keyId']);
}
$signedRequest->setSignedSignature($data['signature']);
}
/**
* @param IIncomingSignedRequest $signedRequest
* @param array $extraSignatureHeaders
*
* @throws IncomingRequestException
*/
private function prepEstimatedSignature(
IIncomingSignedRequest $signedRequest,
array $extraSignatureHeaders = [],
): void {
$request = $signedRequest->getRequest();
$headers = explode(' ', $signedRequest->getSignatureHeader()['headers'] ?? []);
$enforceHeaders = array_merge(
['date', 'host', 'content-length', 'digest'],
$extraSignatureHeaders
);
$missingHeaders = array_diff($enforceHeaders, $headers);
if ($missingHeaders !== []) {
throw new IncomingRequestException(
'missing elements in headers: ' . json_encode($missingHeaders)
);
}
$target = strtolower($request->getMethod()) . ' ' . $request->getRequestUri();
$estimated = ['(request-target): ' . $target];
foreach ($headers as $key) {
$value = $request->getHeader($key);
if (strtolower($key) === 'host') {
$value = $request->getServerHost();
}
if ($value === '') {
throw new IncomingRequestException('empty elements in header ' . $key);
}
$estimated[] = $key . ': ' . $value;
}
$signedRequest->setEstimatedSignature(implode("\n", $estimated));
}
/**
* @param IIncomingSignedRequest $signedRequest
* @param ISignatoryManager $signatoryManager
*
* @throws SignatoryNotFoundException
* @throws SignatureException
*/
private function verifyIncomingRequestSignature(
IIncomingSignedRequest $signedRequest,
ISignatoryManager $signatoryManager,
int $ttlSignatory,
): void {
$knownSignatory = null;
try {
$knownSignatory = $this->getStoredSignatory($signedRequest->getKeyId());
if ($ttlSignatory > 0 && $knownSignatory->getLastUpdated() < (time() - $ttlSignatory)) {
$signatory = $this->getSafeRemoteSignatory($signatoryManager, $signedRequest);
$this->updateSignatoryMetadata($signatory);
$knownSignatory->setMetadata($signatory->getMetadata());
}
$signedRequest->setSignatory($knownSignatory);
$this->verifySignedRequest($signedRequest);
} catch (InvalidKeyOriginException $e) {
throw $e; // issue while requesting remote instance also means there is no 2nd try
} catch (SignatoryNotFoundException|SignatureException) {
try {
$signatory = $this->getSafeRemoteSignatory($signatoryManager, $signedRequest);
} catch (SignatoryNotFoundException $e) {
$this->manageDeprecatedSignatory($knownSignatory);
throw $e;
}
$signedRequest->setSignatory($signatory);
$this->storeSignatory($signatory);
$this->verifySignedRequest($signedRequest);
}
}
/**
* @param ISignatoryManager $signatoryManager
* @param IIncomingSignedRequest $signedRequest
*
* @return ISignatory
* @throws InvalidKeyOriginException
* @throws SignatoryNotFoundException
*/
private function getSafeRemoteSignatory(
ISignatoryManager $signatoryManager,
IIncomingSignedRequest $signedRequest,
): ISignatory {
$signatory = $signatoryManager->getRemoteSignatory($signedRequest);
if ($signatory === null) {
throw new SignatoryNotFoundException('empty result from getRemoteSignatory');
}
if ($signatory->getKeyId() !== $signedRequest->getKeyId()) {
throw new InvalidKeyOriginException('keyId from signatory not related to the one from request');
}
return $signatory->setProviderId($signatoryManager->getProviderId());
}
private function setOutgoingSignatureHeader(
IOutgoingSignedRequest $signedRequest,
string $method,
string $path,
string $dateHeader,
): void {
$header = [
'(request-target)' => $method . ' ' . $path,
'content-length' => strlen($signedRequest->getBody()),
'date' => gmdate($dateHeader),
'digest' => $signedRequest->getDigest(),
'host' => $signedRequest->getHost()
];
$signedRequest->setSignatureHeader($header);
}
/**
* @param IOutgoingSignedRequest $signedRequest
*/
private function setOutgoingClearSignature(IOutgoingSignedRequest $signedRequest): void {
$signing = [];
$header = $signedRequest->getSignatureHeader();
foreach (array_keys($header) as $element) {
$value = $header[$element];
$signing[] = $element . ': ' . $value;
if ($element !== '(request-target)') {
$signedRequest->addHeader($element, $value);
}
}
$signedRequest->setClearSignature(implode("\n", $signing));
}
private function setOutgoingSignedSignature(IOutgoingSignedRequest $signedRequest): void {
$clear = $signedRequest->getClearSignature();
$signed = $this->signString(
$clear, $signedRequest->getSignatory()->getPrivateKey(), $signedRequest->getAlgorithm()
);
$signedRequest->setSignedSignature($signed);
}
private function signingOutgoingRequest(IOutgoingSignedRequest $signedRequest): void {
$signatureHeader = $signedRequest->getSignatureHeader();
$headers = array_diff(array_keys($signatureHeader), ['(request-target)']);
$signatory = $signedRequest->getSignatory();
$signatureElements = [
'keyId="' . $signatory->getKeyId() . '"',
'algorithm="' . $this->getChosenEncryption($signedRequest->getAlgorithm()) . '"',
'headers="' . implode(' ', $headers) . '"',
'signature="' . $signedRequest->getSignedSignature() . '"'
];
$signedRequest->addHeader('Signature', implode(',', $signatureElements));
}
/**
* @param IIncomingSignedRequest $signedRequest
*
* @return void
* @throws SignatureException
* @throws SignatoryNotFoundException
*/
private function verifySignedRequest(IIncomingSignedRequest $signedRequest): void {
$publicKey = $signedRequest->getSignatory()->getPublicKey();
if ($publicKey === '') {
throw new SignatoryNotFoundException('empty public key');
}
try {
$this->verifyString(
$signedRequest->getEstimatedSignature(),
$signedRequest->getSignedSignature(),
$publicKey,
$this->getUsedEncryption($signedRequest)
);
} catch (InvalidSignatureException $e) {
$this->logger->debug('signature issue', ['signed' => $signedRequest, 'exception' => $e]);
throw $e;
}
}
private function getUsedEncryption(IIncomingSignedRequest $signedRequest): SignatureAlgorithm {
$data = $signedRequest->getSignatureHeader();
return match ($data['algorithm']) {
'rsa-sha512' => SignatureAlgorithm::SHA512,
default => SignatureAlgorithm::SHA256,
};
}
private function getChosenEncryption(string $algorithm): string {
return match ($algorithm) {
'sha512' => 'ras-sha512',
default => 'ras-sha256',
};
}
public function getOpenSSLAlgo(string $algorithm): int {
return match ($algorithm) {
'sha512' => OPENSSL_ALGO_SHA512,
default => OPENSSL_ALGO_SHA256,
};
}
/**
* @param string $clear
* @param string $privateKey
* @param string $algorithm
*
* @return string
* @throws SignatoryException
*/
private function signString(string $clear, string $privateKey, string $algorithm): string {
if ($privateKey === '') {
throw new SignatoryException('empty private key');
}
openssl_sign($clear, $signed, $privateKey, $this->getOpenSSLAlgo($algorithm));
return base64_encode($signed);
}
/**
* @param string $clear
* @param string $encoded
* @param string $publicKey
* @param SignatureAlgorithm $algo
*
* @return void
* @throws InvalidSignatureException
*/
private function verifyString(
string $clear,
string $encoded,
string $publicKey,
SignatureAlgorithm $algo = SignatureAlgorithm::SHA256,
): void {
$signed = base64_decode($encoded);
if (openssl_verify($clear, $signed, $publicKey, $algo->value) !== 1) {
throw new InvalidSignatureException('signature issue');
}
}
/**
* @param string $keyId
*
* @return ISignatory
* @throws SignatoryNotFoundException
*/
private function getStoredSignatory(string $keyId): ISignatory {
$qb = $this->connection->getQueryBuilder();
$qb->select(
'id', 'provider_id', 'host', 'account', 'key_id', 'key_id_sum', 'public_key', 'metadata', 'type',
'status', 'creation', 'last_updated'
);
$qb->from(self::TABLE_SIGNATORIES);
$qb->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId))));
$result = $qb->executeQuery();
$row = $result->fetch();
$result->closeCursor();
if (!$row) {
throw new SignatoryNotFoundException('no signatory found in local');
}
$signature = new Signatory($row['key_id'], $row['public_key']);
$signature->importFromDatabase($row);
return $signature;
}
/**
* @param ISignatory $signatory
*/
private function storeSignatory(ISignatory $signatory): void {
try {
$this->insertSignatory($signatory);
} catch (DBException $e) {
if ($e->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
$this->logger->warning('exception while storing signature', ['exception' => $e]);
throw $e;
}
try {
$this->updateKnownSignatory($signatory);
} catch (SignatoryNotFoundException $e) {
$this->logger->warning('strange behavior, signatory not found ?', ['exception' => $e]);
}
}
}
private function insertSignatory(ISignatory $signatory): void {
$qb = $this->connection->getQueryBuilder();
$qb->insert(self::TABLE_SIGNATORIES)
->setValue('provider_id', $qb->createNamedParameter($signatory->getProviderId()))
->setValue('host', $qb->createNamedParameter($this->getHostFromUri($signatory->getKeyId())))
->setValue('account', $qb->createNamedParameter($signatory->getAccount()))
->setValue('key_id', $qb->createNamedParameter($signatory->getKeyId()))
->setValue('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId())))
->setValue('public_key', $qb->createNamedParameter($signatory->getPublicKey()))
->setValue('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata())))
->setValue('type', $qb->createNamedParameter($signatory->getType()->value))
->setValue('status', $qb->createNamedParameter($signatory->getStatus()->value))
->setValue('creation', $qb->createNamedParameter(time()))
->setValue('last_updated', $qb->createNamedParameter(time()));
$qb->executeStatement();
}
/**
* @param ISignatory $signatory
*
* @throws SignatoryNotFoundException
* @throws SignatoryConflictException
*/
private function updateKnownSignatory(ISignatory $signatory): void {
$knownSignatory = $this->getStoredSignatory($signatory->getKeyId());
switch ($signatory->getType()) {
case SignatoryType::FORGIVABLE:
$this->deleteSignatory($knownSignatory->getKeyId());
$this->insertSignatory($signatory);
return;
case SignatoryType::REFRESHABLE:
$this->updateSignatoryPublicKey($signatory);
$this->updateSignatoryMetadata($signatory);
break;
case SignatoryType::TRUSTED:
// TODO: send notice to admin
throw new SignatoryConflictException();
break;
case SignatoryType::STATIC:
// TODO: send warning to admin
throw new SignatoryConflictException();
break;
}
}
/**
* This is called when a remote signatory does not exist anymore
*
* @param ISignatory|null $knownSignatory NULL is not known
*
* @throws SignatoryConflictException
* @throws SignatoryNotFoundException
*/
private function manageDeprecatedSignatory(?ISignatory $knownSignatory): void {
switch ($knownSignatory?->getType()) {
case null: // unknown in local database
case SignatoryType::FORGIVABLE: // who cares ?
throw new SignatoryNotFoundException(); // meaning we just return the correct exception
case SignatoryType::REFRESHABLE:
// TODO: send notice to admin
throw new SignatoryConflictException();
case SignatoryType::TRUSTED:
case SignatoryType::STATIC:
// TODO: send warning to admin
throw new SignatoryConflictException();
}
}
private function updateSignatoryPublicKey(ISignatory $signatory): void {
$qb = $this->connection->getQueryBuilder();
$qb->update(self::TABLE_SIGNATORIES)
->set('signatory', $qb->createNamedParameter($signatory->getPublicKey()))
->set('last_updated', $qb->createNamedParameter(time()));
$qb->where(
$qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId())))
);
$qb->executeStatement();
}
private function updateSignatoryMetadata(ISignatory $signatory): void {
$qb = $this->connection->getQueryBuilder();
$qb->update(self::TABLE_SIGNATORIES)
->set('metadata', $qb->createNamedParameter(json_encode($signatory->getMetadata())))
->set('last_updated', $qb->createNamedParameter(time()));
$qb->where(
$qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($signatory->getKeyId())))
);
$qb->executeStatement();
}
private function deleteSignatory(string $keyId): void {
$qb = $this->connection->getQueryBuilder();
$qb->delete(self::TABLE_SIGNATORIES)
->where($qb->expr()->eq('key_id_sum', $qb->createNamedParameter($this->hashKeyId($keyId))));
$qb->executeStatement();
}
/**
* @param string $uri
*
* @return string
* @throws InvalidKeyOriginException
*/
private function getHostFromUri(string $uri): string {
$host = parse_url($uri, PHP_URL_HOST);
$port = parse_url($uri, PHP_URL_PORT);
if ($port !== null && $port !== false) {
$host .= ':' . $port;
}
if (is_string($host) && $host !== '') {
return $host;
}
throw new \Exception('invalid/empty uri');
}
private function hashKeyId(string $keyId): string {
return hash('sha256', $keyId);
}
}

20
lib/private/Server.php

@ -8,6 +8,8 @@ namespace OC;
use bantu\IniGetWrapper\IniGetWrapper;
use NCU\Config\IUserConfig;
use NCU\Security\PublicPrivateKeyPairs\IKeyPairManager;
use NCU\Security\Signature\ISignatureManager;
use OC\Accounts\AccountManager;
use OC\App\AppManager;
use OC\App\AppStore\Bundles\BundleFetcher;
@ -101,8 +103,10 @@ use OC\Security\CSRF\CsrfTokenManager;
use OC\Security\CSRF\TokenStorage\SessionStorage;
use OC\Security\Hasher;
use OC\Security\Ip\RemoteAddress;
use OC\Security\PublicPrivateKeyPairs\KeyPairManager;
use OC\Security\RateLimiting\Limiter;
use OC\Security\SecureRandom;
use OC\Security\Signature\SignatureManager;
use OC\Security\TrustedDomainHelper;
use OC\Security\VerificationToken\VerificationToken;
use OC\Session\CryptoWrapper;
@ -1180,18 +1184,7 @@ class Server extends ServerContainer implements IServerContainer {
});
$this->registerAlias(\OCP\GlobalScale\IConfig::class, \OC\GlobalScale\Config::class);
$this->registerService(ICloudFederationProviderManager::class, function (ContainerInterface $c) {
return new CloudFederationProviderManager(
$c->get(\OCP\IConfig::class),
$c->get(IAppManager::class),
$c->get(IClientService::class),
$c->get(ICloudIdManager::class),
$c->get(IOCMDiscoveryService::class),
$c->get(LoggerInterface::class)
);
});
$this->registerAlias(ICloudFederationProviderManager::class, CloudFederationProviderManager::class);
$this->registerService(ICloudFederationFactory::class, function (Server $c) {
return new CloudFederationFactory();
});
@ -1297,6 +1290,9 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(IRichTextFormatter::class, \OC\RichObjectStrings\RichTextFormatter::class);
$this->registerAlias(IKeyPairManager::class, KeyPairManager::class);
$this->registerAlias(ISignatureManager::class, SignatureManager::class);
$this->connectDispatcher();
}

25
lib/public/OCM/IOCMProvider.php

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace OCP\OCM;
use JsonSerializable;
use NCU\Security\Signature\Model\ISignatory;
use OCP\OCM\Exceptions\OCMArgumentException;
use OCP\OCM\Exceptions\OCMProviderException;
@ -120,6 +121,22 @@ interface IOCMProvider extends JsonSerializable {
*/
public function extractProtocolEntry(string $resourceName, string $protocol): string;
/**
* store signatory (public/private key pair) to sign outgoing/incoming request
*
* @param ISignatory $signatory
* @since 31.0.0
*/
public function setSignatory(ISignatory $signatory): void;
/**
* signatory (public/private key pair) used to sign outgoing/incoming request
*
* @return ISignatory|null returns null if no ISignatory available
* @since 31.0.0
*/
public function getSignatory(): ?ISignatory;
/**
* import data from an array
*
@ -134,13 +151,15 @@ interface IOCMProvider extends JsonSerializable {
/**
* @return array{
* enabled: bool,
* apiVersion: string,
* apiVersion: '1.0-proposal1',
* endPoint: string,
* resourceTypes: list<array{
* publicKey: ISignatory|null,
* resourceTypes: array{
* name: string,
* shareTypes: list<string>,
* protocols: array<string, string>
* }>,
* }[],
* version: string
* }
* @since 28.0.0
*/

18
lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\PublicPrivateKeyPairs\Exceptions;
/**
* conflict between public and private key pair
*
* @experimental 31.0.0
* @since 31.0.0
*/
class KeyPairConflictException extends KeyPairException {
}

20
lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairException.php

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\PublicPrivateKeyPairs\Exceptions;
use Exception;
/**
* global exception related to key pairs
*
* @experimental 31.0.0
* @since 31.0.0
*/
class KeyPairException extends Exception {
}

16
lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairNotFoundException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\PublicPrivateKeyPairs\Exceptions;
/**
* @experimental 31.0.0
* @since 31.0.0
*/
class KeyPairNotFoundException extends KeyPairException {
}

80
lib/unstable/Security/PublicPrivateKeyPairs/IKeyPairManager.php

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\PublicPrivateKeyPairs;
use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairConflictException;
use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairNotFoundException;
use NCU\Security\PublicPrivateKeyPairs\Model\IKeyPair;
/**
* IKeyPairManager contains a group of method to create/manage/store internal public/private key pair.
*
* @experimental 31.0.0
* @since 31.0.0
*/
interface IKeyPairManager {
/**
* generate and store public/private key pair.
* throws exception if key pair already exist
*
* @param string $app appId
* @param string $name key name
* @param array $options algorithms, metadata
*
* @return IKeyPair
* @throws KeyPairConflictException if a key already exist
* @since 31.0.0
*/
public function generateKeyPair(string $app, string $name, array $options = []): IKeyPair;
/**
* returns if key pair is known.
*
* @param string $app appId
* @param string $name key name
*
* @return bool TRUE if key pair exists in database
* @since 31.0.0
*/
public function hasKeyPair(string $app, string $name): bool;
/**
* return key pair from database based on $app and $name.
* throws exception if key pair does not exist
*
* @param string $app appId
* @param string $name key name
*
* @return IKeyPair
* @throws KeyPairNotFoundException if key pair is not known
* @since 31.0.0
*/
public function getKeyPair(string $app, string $name): IKeyPair;
/**
* delete key pair from database
*
* @param string $app appid
* @param string $name key name
*
* @since 31.0.0
*/
public function deleteKeyPair(string $app, string $name): void;
/**
* test key pair by encrypting/decrypting a string
*
* @param IKeyPair $keyPair keypair to test
*
* @return bool
* @since 31.0.0
*/
public function testKeyPair(IKeyPair $keyPair): bool;
}

85
lib/unstable/Security/PublicPrivateKeyPairs/Model/IKeyPair.php

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\PublicPrivateKeyPairs\Model;
/**
* simple model that store key pair, its name, its origin (app)
* and the options used during its creation
*
* @experimental 31.0.0
* @since 31.0.0
*/
interface IKeyPair {
/**
* returns id of the app owning the key pair
*
* @return string
* @since 31.0.0
*/
public function getApp(): string;
/**
* returns name of the key pair
*
* @return string
* @since 31.0.0
*/
public function getName(): string;
/**
* set public key
*
* @param string $publicKey
* @return IKeyPair
* @since 31.0.0
*/
public function setPublicKey(string $publicKey): IKeyPair;
/**
* returns public key
*
* @return string
* @since 31.0.0
*/
public function getPublicKey(): string;
/**
* set private key
*
* @param string $privateKey
* @return IKeyPair
* @since 31.0.0
*/
public function setPrivateKey(string $privateKey): IKeyPair;
/**
* returns private key
*
* @return string
* @since 31.0.0
*/
public function getPrivateKey(): string;
/**
* set options
*
* @param array $options
* @return IKeyPair
* @since 31.0.0
*/
public function setOptions(array $options): IKeyPair;
/**
* returns options
*
* @return array
* @since 31.0.0
*/
public function getOptions(): array;
}

16
lib/unstable/Security/Signature/Exceptions/IdentityNotFoundException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class IdentityNotFoundException extends SignatureException {
}

16
lib/unstable/Security/Signature/Exceptions/IncomingRequestException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class IncomingRequestException extends SignatureException {
}

16
lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class IncomingRequestNotFoundException extends SignatureException {
}

16
lib/unstable/Security/Signature/Exceptions/InvalidKeyOriginException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class InvalidKeyOriginException extends SignatureException {
}

16
lib/unstable/Security/Signature/Exceptions/InvalidSignatureException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class InvalidSignatureException extends SignatureException {
}

16
lib/unstable/Security/Signature/Exceptions/SignatoryConflictException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class SignatoryConflictException extends SignatoryException {
}

16
lib/unstable/Security/Signature/Exceptions/SignatoryException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class SignatoryException extends SignatureException {
}

16
lib/unstable/Security/Signature/Exceptions/SignatoryNotFoundException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class SignatoryNotFoundException extends SignatoryException {
}

18
lib/unstable/Security/Signature/Exceptions/SignatureException.php

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
use Exception;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class SignatureException extends Exception {
}

16
lib/unstable/Security/Signature/Exceptions/SignatureNotFoundException.php

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Exceptions;
/**
* @since 31.0.0
* @experimental 31.0.0
*/
class SignatureNotFoundException extends SignatureException {
}

71
lib/unstable/Security/Signature/ISignatoryManager.php

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
/**
* ISignatoryManager contains a group of method that will help
* - signing outgoing request
* - confirm the authenticity of incoming signed request.
*
* @experimental 31.0.0
* @since 31.0.0
*/
interface ISignatoryManager {
/**
* id of the signatory manager.
* This is used to store, confirm uniqueness and avoid conflict of the remote key pairs.
*
* Must be unique.
*
* @return string
* @since 31.0.0
*/
public function getProviderId(): string;
/**
* options that might affect the way the whole process is handled:
* [
* 'ttl' => 300,
* 'ttlSignatory' => 86400*3,
* 'extraSignatureHeaders' => [],
* 'algorithm' => 'sha256',
* 'dateHeader' => "D, d M Y H:i:s T",
* ]
*
* @return array
* @since 31.0.0
*/
public function getOptions(): array;
/**
* generate and returns local signatory including private and public key pair.
*
* Used to sign outgoing request
*
* @return ISignatory
* @since 31.0.0
*/
public function getLocalSignatory(): ISignatory;
/**
* retrieve details and generate signatory from remote instance.
* If signatory cannot be found, returns NULL.
*
* Used to confirm authenticity of incoming request.
*
* @param IIncomingSignedRequest $signedRequest
*
* @return ISignatory|null must be NULL if no signatory is found
* @since 31.0.0
*/
public function getRemoteSignatory(IIncomingSignedRequest $signedRequest): ?ISignatory;
}

129
lib/unstable/Security/Signature/ISignatureManager.php

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\Exceptions\IncomingRequestException;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
use NCU\Security\Signature\Exceptions\SignatureException;
use NCU\Security\Signature\Exceptions\SignatureNotFoundException;
use NCU\Security\Signature\Model\IIncomingSignedRequest;
use NCU\Security\Signature\Model\IOutgoingSignedRequest;
use NCU\Security\Signature\Model\ISignatory;
/**
* ISignatureManager is a service integrated to core that provide tools
* to set/get authenticity of/from outgoing/incoming request.
*
* Quick description of the signature, added to the headers
* {
* "(request-target)": "post /path",
* "content-length": 385,
* "date": "Mon, 08 Jul 2024 14:16:20 GMT",
* "digest": "SHA-256=U7gNVUQiixe5BRbp4Tg0xCZMTcSWXXUZI2\\/xtHM40S0=",
* "host": "hostname.of.the.recipient",
* "Signature": "keyId=\"https://author.hostname/key\",algorithm=\"ras-sha256\",headers=\"content-length date digest host\",signature=\"DzN12OCS1rsA[...]o0VmxjQooRo6HHabg==\""
* }
*
* 'content-length' is the total length of the data/content
* 'date' is the datetime the request have been initiated
* 'digest' is a checksum of the data/content
* 'host' is the hostname of the recipient of the request (remote when signing outgoing request, local on incoming request)
* 'Signature' contains the signature generated using the private key, and metadata:
* - 'keyId' is a unique id, formatted as an url. hostname is used to retrieve the public key via custom discovery
* - 'algorithm' define the algorithm used to generate signature
* - 'headers' contains a list of element used during the generation of the signature
* - 'signature' is the encrypted string, using local private key, of an array containing elements
* listed in 'headers' and their value. Some elements (content-length date digest host) are mandatory
* to ensure authenticity override protection.
*
* @experimental 31.0.0
* @since 31.0.0
*/
interface ISignatureManager {
/**
* Extracting data from headers and body from the incoming request.
* Compare headers and body to confirm authenticity of remote instance.
* Returns details about the signed request or throws exception.
*
* Should be called from Controller.
*
* @param ISignatoryManager $signatoryManager used to get details about remote instance
* @param string|null $body if NULL, body will be extracted from php://input
*
* @return IIncomingSignedRequest
* @throws IncomingRequestException if anything looks wrong with the incoming request
* @throws SignatureNotFoundException if incoming request is not signed
* @throws SignatureException if signature could not be confirmed
* @since 31.0.0
*/
public function getIncomingSignedRequest(ISignatoryManager $signatoryManager, ?string $body = null): IIncomingSignedRequest;
/**
* Preparing signature (and headers) to sign an outgoing request.
* Returns a IOutgoingSignedRequest containing all details to finalise the packaging of the whole payload
*
* @param ISignatoryManager $signatoryManager
* @param string $content body to be signed
* @param string $method needed in the signature
* @param string $uri needed in the signature
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function getOutgoingSignedRequest(ISignatoryManager $signatoryManager, string $content, string $method, string $uri): IOutgoingSignedRequest;
/**
* Complete the full process of signing and filling headers from payload when generating
* an outgoing request with IClient
*
* @param ISignatoryManager $signatoryManager
* @param array $payload original payload, will be used to sign and completed with new headers with signature elements
* @param string $method needed in the signature
* @param string $uri needed in the signature
*
* @return array new payload to be sent, including original payload and signature elements in headers
* @since 31.0.0
*/
public function signOutgoingRequestIClientPayload(ISignatoryManager $signatoryManager, array $payload, string $method, string $uri): array;
/**
* returns remote signatory stored in local database, based on the remote host.
*
* @param string $host remote host
* @param string $account linked account, should be used when multiple signature can exist for the same host
*
* @return ISignatory
* @throws SignatoryNotFoundException if entry does not exist in local database
* @since 31.0.0
*/
public function searchSignatory(string $host, string $account = ''): ISignatory;
/**
* returns a fully formatted keyId, based on a fix hostname and path
*
* @param string $path
*
* @return string
* @throws IdentityNotFoundException if hostname is not set
* @since 31.0.0
*/
public function generateKeyIdFromConfig(string $path): string;
/**
* returns hostname:port extracted from an uri
*
* @param string $uri
*
* @return string
* @throws IdentityNotFoundException if identity cannot be extracted
* @since 31.0.0
*/
public function extractIdentityFromUri(string $uri): string;
}

105
lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Model;
use NCU\Security\Signature\ISignatureManager;
use OCP\IRequest;
/**
* model wrapping an actual incoming request, adding details about the signature and the
* authenticity of the origin of the request.
*
* @see ISignatureManager for details on signature
* @experimental 31.0.0
* @since 31.0.0
*/
interface IIncomingSignedRequest extends ISignedRequest {
/**
* set the core IRequest that might be signed
*
* @param IRequest $request
* @return IIncomingSignedRequest
* @since 31.0.0
*/
public function setRequest(IRequest $request): IIncomingSignedRequest;
/**
* returns the base IRequest
*
* @return IRequest
* @since 31.0.0
*/
public function getRequest(): IRequest;
/**
* set the time, extracted from the base request headers
*
* @param int $time
* @return IIncomingSignedRequest
* @since 31.0.0
*/
public function setTime(int $time): IIncomingSignedRequest;
/**
* get the time, extracted from the base request headers
*
* @return int
* @since 31.0.0
*/
public function getTime(): int;
/**
* set the hostname at the source of the request,
* based on the keyId defined in the signature header.
*
* @param string $origin
* @return IIncomingSignedRequest
* @since 31.0.0
*/
public function setOrigin(string $origin): IIncomingSignedRequest;
/**
* get the hostname at the source of the base request.
* based on the keyId defined in the signature header.
*
* @return string
* @since 31.0.0
*/
public function getOrigin(): string;
/**
* returns the keyId extracted from the signature headers.
* keyId is a mandatory entry in the headers of a signed request.
*
* @return string
* @since 31.0.0
*/
public function getKeyId(): string;
/**
* store a clear and estimated version of the signature, based on payload and headers.
* This clear version will be compared with the real signature using
* the public key of remote instance at the origin of the request.
*
* @param string $signature
* @return IIncomingSignedRequest
* @since 31.0.0
*/
public function setEstimatedSignature(string $signature): IIncomingSignedRequest;
/**
* returns a clear and estimated version of the signature, based on payload and headers.
* This clear version will be compared with the real signature using
* the public key of remote instance at the origin of the request.
*
* @return string
* @since 31.0.0
*/
public function getEstimatedSignature(): string;
}

94
lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Model;
use NCU\Security\Signature\ISignatureManager;
/**
* extends ISignedRequest to add info requested at the generation of the signature
*
* @see ISignatureManager for details on signature
* @experimental 31.0.0
* @since 31.0.0
*/
interface IOutgoingSignedRequest extends ISignedRequest {
/**
* set the host of the recipient of the request.
*
* @param string $host
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function setHost(string $host): IOutgoingSignedRequest;
/**
* get the host of the recipient of the request.
* - on incoming request, this is the local hostname of current instance.
* - on outgoing request, this is the remote instance.
*
* @return string
* @since 31.0.0
*/
public function getHost(): string;
/**
* add a key/value pair to the headers of the request
*
* @param string $key
* @param string|int|float|bool|array $value
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function addHeader(string $key, string|int|float|bool|array $value): IOutgoingSignedRequest;
/**
* returns list of headers value that will be added to the base request
*
* @return array
* @since 31.0.0
*/
public function getHeaders(): array;
/**
* store a clear version of the signature
*
* @param string $estimated
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function setClearSignature(string $estimated): IOutgoingSignedRequest;
/**
* returns the clear version of the signature
*
* @return string
* @since 31.0.0
*/
public function getClearSignature(): string;
/**
* set algorithm to be used to sign the signature
*
* @param string $algorithm
*
* @return IOutgoingSignedRequest
* @since 31.0.0
*/
public function setAlgorithm(string $algorithm): IOutgoingSignedRequest;
/**
* returns the algorithm set to sign the signature
*
* @return string
* @since 31.0.0
*/
public function getAlgorithm(): string;
}

160
lib/unstable/Security/Signature/Model/ISignatory.php

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Model;
use NCU\Security\Signature\ISignatoryManager;
/**
* model that store keys and details related to host and in use protocol
* mandatory details are providerId, host, keyId and public key.
* private key is only used for local signatory, used to sign outgoing request
*
* the pair providerId+host is unique, meaning only one signatory can exist for each host
* and protocol
*
* @since 31.0.0
* @experimental 31.0.0
*/
interface ISignatory {
/**
* unique string, related to the ISignatoryManager
*
* @see ISignatoryManager::getProviderId
* @param string $providerId
*
* @return ISignatory
* @since 31.0.0
*/
public function setProviderId(string $providerId): ISignatory;
/**
* returns the provider id, unique string related to the ISignatoryManager
*
* @return string
* @since 31.0.0
*/
public function getProviderId(): string;
/**
* set account, in case your ISignatoryManager needs to manage multiple keys from same host
*
* @param string $account
*
* @return ISignatory
* @since 31.0.0
*/
public function setAccount(string $account): ISignatory;
/**
* return account name, empty string if not set
*
* @return string
* @since 31.0.0
*/
public function getAccount(): string;
/**
* returns key id
*
* @return string
* @since 31.0.0
*/
public function getKeyId(): string;
/**
* returns public key
*
* @return string
* @since 31.0.0
*/
public function getPublicKey(): string;
/**
* returns private key, if available
*
* @return string
* @since 31.0.0
*/
public function getPrivateKey(): string;
/**
* set metadata
*
* @param array $metadata
*
* @return ISignatory
* @since 31.0.0
*/
public function setMetadata(array $metadata): ISignatory;
/**
* returns metadata
*
* @return array
* @since 31.0.0
*/
public function getMetadata(): array;
/**
* update an entry in metadata
*
* @param string $key
* @param string|int $value
*
* @return ISignatory
* @since 31.0.0
*/
public function setMetaValue(string $key, string|int $value): ISignatory;
/**
* set SignatoryType
*
* @param SignatoryType $type
*
* @return ISignatory
* @since 31.0.0
*/
public function setType(SignatoryType $type): ISignatory;
/**
* returns SignatoryType
*
* @return SignatoryType
* @since 31.0.0
*/
public function getType(): SignatoryType;
/**
* set SignatoryStatus
*
* @param SignatoryStatus $status
*
* @see SignatoryStatus
* @return ISignatory
* @since 31.0.0
*/
public function setStatus(SignatoryStatus $status): ISignatory;
/**
* get SignatoryStatus
*
* @see SignatoryStatus
* @return SignatoryStatus
* @since 31.0.0
*/
public function getStatus(): SignatoryStatus;
/**
* get last timestamp this entry has been updated
*
* @return int
* @since 31.0.0
*/
public function getLastUpdated(): int;
}

98
lib/unstable/Security/Signature/Model/ISignedRequest.php

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Model;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
/**
* model that store data related to a possible signature.
* those details will be used:
* - to confirm authenticity of a signed incoming request
* - to sign an outgoing request
*
* @experimental 31.0.0
* @since 31.0.0
*/
interface ISignedRequest {
/**
* payload of the request
*
* @return string
* @since 31.0.0
*/
public function getBody(): string;
/**
* checksum of the payload of the request
*
* @return string
* @since 31.0.0
*/
public function getDigest(): string;
/**
* set the list of headers related to the signature of the request
*
* @param array $signatureHeader
* @return ISignedRequest
* @since 31.0.0
*/
public function setSignatureHeader(array $signatureHeader): ISignedRequest;
/**
* get the list of headers related to the signature of the request
*
* @return array
* @since 31.0.0
*/
public function getSignatureHeader(): array;
/**
* set the signed version of the signature
*
* @param string $signedSignature
* @return ISignedRequest
* @since 31.0.0
*/
public function setSignedSignature(string $signedSignature): ISignedRequest;
/**
* get the signed version of the signature
*
* @return string
* @since 31.0.0
*/
public function getSignedSignature(): string;
/**
* set the signatory, containing keys and details, related to this request
*
* @param ISignatory $signatory
* @return ISignedRequest
* @since 31.0.0
*/
public function setSignatory(ISignatory $signatory): ISignedRequest;
/**
* get the signatory, containing keys and details, related to this request
*
* @return ISignatory
* @throws SignatoryNotFoundException
* @since 31.0.0
*/
public function getSignatory(): ISignatory;
/**
* returns if a signatory related to this request have been found and defined
*
* @return bool
* @since 31.0.0
*/
public function hasSignatory(): bool;
}

25
lib/unstable/Security/Signature/Model/SignatoryStatus.php

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Model;
/**
* current status of signatory. is it trustable or not ?
*
* - SYNCED = the remote instance is trustable.
* - BROKEN = the remote instance does not use the same key pairs
*
* @experimental 31.0.0
* @since 31.0.0
*/
enum SignatoryStatus: int {
/** @since 31.0.0 */
case SYNCED = 1;
/** @since 31.0.0 */
case BROKEN = 9;
}

31
lib/unstable/Security/Signature/Model/SignatoryType.php

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature\Model;
/**
* type of link between local and remote instance
*
* - FORGIVABLE = the keypair can be deleted and refreshed anytime and silently
* - REFRESHABLE = the keypair can be refreshed but a notice will be generated
* - TRUSTED = any changes of keypair will require human interaction, warning will be issued
* - STATIC = error will be issued on conflict, assume keypair cannot be reset.
*
* @experimental 31.0.0
* @since 31.0.0
*/
enum SignatoryType: int {
/** @since 31.0.0 */
case FORGIVABLE = 1; // no notice on refresh
/** @since 31.0.0 */
case REFRESHABLE = 4; // notice on refresh
/** @since 31.0.0 */
case TRUSTED = 8; // warning on refresh
/** @since 31.0.0 */
case STATIC = 9; // error on refresh
}

22
lib/unstable/Security/Signature/SignatureAlgorithm.php

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace NCU\Security\Signature;
/**
* list of available algorithm when signing payload
*
* @experimental 31.0.0
* @since 31.0.0
*/
enum SignatureAlgorithm: string {
/** @since 31.0.0 */
case SHA256 = 'sha256';
/** @since 31.0.0 */
case SHA512 = 'sha512';
}
Loading…
Cancel
Save