Browse Source
feat(ocm): signing ocm requests
feat(ocm): signing ocm requests
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>pull/45979/head
47 changed files with 3477 additions and 100 deletions
-
35apps/cloud_federation_api/lib/Capabilities.php
-
155apps/cloud_federation_api/lib/Controller/RequestHandlerController.php
-
33apps/cloud_federation_api/openapi.json
-
1apps/files_sharing/lib/External/Storage.php
-
3build/integration/federation_features/cleanup-remote-storage.feature
-
12core/Controller/OCMController.php
-
135core/Migrations/Version31000Date20240101084401.php
-
9lib/composer/composer/autoload_classmap.php
-
9lib/composer/composer/autoload_static.php
-
73lib/private/Federation/CloudFederationProviderManager.php
-
7lib/private/Files/Storage/DAV.php
-
45lib/private/OCM/Model/OCMProvider.php
-
35lib/private/OCM/OCMDiscoveryService.php
-
149lib/private/OCM/OCMSignatoryManager.php
-
182lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php
-
114lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php
-
170lib/private/Security/Signature/Model/IncomingSignedRequest.php
-
131lib/private/Security/Signature/Model/OutgoingSignedRequest.php
-
147lib/private/Security/Signature/Model/Signatory.php
-
143lib/private/Security/Signature/Model/SignedRequest.php
-
828lib/private/Security/Signature/SignatureManager.php
-
20lib/private/Server.php
-
25lib/public/OCM/IOCMProvider.php
-
18lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php
-
20lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairException.php
-
16lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairNotFoundException.php
-
80lib/unstable/Security/PublicPrivateKeyPairs/IKeyPairManager.php
-
85lib/unstable/Security/PublicPrivateKeyPairs/Model/IKeyPair.php
-
16lib/unstable/Security/Signature/Exceptions/IdentityNotFoundException.php
-
16lib/unstable/Security/Signature/Exceptions/IncomingRequestException.php
-
16lib/unstable/Security/Signature/Exceptions/IncomingRequestNotFoundException.php
-
16lib/unstable/Security/Signature/Exceptions/InvalidKeyOriginException.php
-
16lib/unstable/Security/Signature/Exceptions/InvalidSignatureException.php
-
16lib/unstable/Security/Signature/Exceptions/SignatoryConflictException.php
-
16lib/unstable/Security/Signature/Exceptions/SignatoryException.php
-
16lib/unstable/Security/Signature/Exceptions/SignatoryNotFoundException.php
-
18lib/unstable/Security/Signature/Exceptions/SignatureException.php
-
16lib/unstable/Security/Signature/Exceptions/SignatureNotFoundException.php
-
71lib/unstable/Security/Signature/ISignatoryManager.php
-
129lib/unstable/Security/Signature/ISignatureManager.php
-
105lib/unstable/Security/Signature/Model/IIncomingSignedRequest.php
-
94lib/unstable/Security/Signature/Model/IOutgoingSignedRequest.php
-
160lib/unstable/Security/Signature/Model/ISignatory.php
-
98lib/unstable/Security/Signature/Model/ISignedRequest.php
-
25lib/unstable/Security/Signature/Model/SignatoryStatus.php
-
31lib/unstable/Security/Signature/Model/SignatoryType.php
-
22lib/unstable/Security/Signature/SignatureAlgorithm.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; |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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]; |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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(), |
|||
] |
|||
); |
|||
} |
|||
} |
@ -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(), |
|||
] |
|||
); |
|||
} |
|||
} |
@ -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() |
|||
]; |
|||
} |
|||
} |
@ -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, |
|||
]; |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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
|
|||
} |
@ -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'; |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue