From 58d3efddf74ebe88fed6ee285195bc4e504a0744 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Tue, 25 Jun 2024 12:45:45 -0100 Subject: [PATCH] outgoing request Signed-off-by: Maxence Lange --- .../cloud_federation_api/lib/Capabilities.php | 15 +- .../Controller/RequestHandlerController.php | 14 +- apps/files_sharing/lib/External/Cache.php | 5 +- core/Controller/OCMController.php | 11 +- lib/private/AppFramework/Http/Request.php | 2 +- .../CloudFederationProviderManager.php | 25 ++- lib/private/OCM/Model/OCMProvider.php | 25 ++- lib/private/OCM/OCMDiscoveryService.php | 34 +--- lib/private/OCM/OCMSignatoryManager.php | 55 +++++- .../PublicPrivateKeyPairs/KeyPairManager.php | 22 ++- .../Signature/Model/IncomingSignedRequest.php | 6 +- .../Signature/Model/OutgoingSignedRequest.php | 33 ++-- .../Security/Signature/Model/Signatory.php | 12 +- .../Signature/Model/SignedRequest.php | 4 +- .../Security/Signature/SignatureManager.php | 180 +++++++++++++++--- lib/private/Server.php | 28 ++- lib/public/OCM/IOCMProvider.php | 16 ++ .../Exceptions/SignatoryException.php | 4 +- .../Security/Signature/ISignatoryManager.php | 4 +- .../Security/Signature/ISignatureManager.php | 6 + .../Model/IOutgoingSignedRequest.php | 6 +- .../Security/Signature/Model/ISignatory.php | 1 + 22 files changed, 385 insertions(+), 123 deletions(-) diff --git a/apps/cloud_federation_api/lib/Capabilities.php b/apps/cloud_federation_api/lib/Capabilities.php index 61cc45a24e625..d444e3df90d64 100644 --- a/apps/cloud_federation_api/lib/Capabilities.php +++ b/apps/cloud_federation_api/lib/Capabilities.php @@ -9,17 +9,24 @@ namespace OCA\CloudFederationAPI; +use OC\OCM\OCMSignatoryManager; +use OC\Security\Signature\Model\Signatory; use OCP\Capabilities\ICapability; use OCP\IURLGenerator; use OCP\OCM\Exceptions\OCMArgumentException; use OCP\OCM\IOCMProvider; +use OCP\Security\Signature\Exceptions\SignatoryException; +use OCP\Security\Signature\Model\ISignatory; +use Psr\Log\LoggerInterface; class Capabilities implements ICapability { - public const API_VERSION = '1.0-proposal1'; + public const API_VERSION = '1.1'; // informative, real version. public function __construct( private IURLGenerator $urlGenerator, private IOCMProvider $provider, + private readonly OCMSignatoryManager $ocmSignatoryManager, + private readonly LoggerInterface $logger, ) { } @@ -60,6 +67,12 @@ public function getCapabilities() { $this->provider->addResourceType($resource); + try { + $this->provider->setSignatory($this->ocmSignatoryManager->getLocalSignatory()); + } catch (SignatoryException $e) { + $this->logger->warning('cannot generate local signatory', ['exception' => $e]); + } + return ['ocm' => $this->provider->jsonSerialize()]; } } diff --git a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php index 5623ab930861e..3dd2bb48a9f6e 100644 --- a/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php +++ b/apps/cloud_federation_api/lib/Controller/RequestHandlerController.php @@ -5,6 +5,7 @@ */ namespace OCA\CloudFederationAPI\Controller; +use OC\OCM\OCMSignatoryManager; use OCA\CloudFederationAPI\Config; use OCA\CloudFederationAPI\ResponseDefinitions; use OCP\AppFramework\Controller; @@ -23,6 +24,8 @@ use OCP\IRequest; use OCP\IURLGenerator; use OCP\IUserManager; +use OCP\Security\Signature\Exceptions\SignatureException; +use OCP\Security\Signature\ISignatureManager; use OCP\Share\Exceptions\ShareNotFound; use Psr\Log\LoggerInterface; @@ -47,7 +50,9 @@ public function __construct( private ICloudFederationProviderManager $cloudFederationProviderManager, private Config $config, private ICloudFederationFactory $factory, - private ICloudIdManager $cloudIdManager + private ICloudIdManager $cloudIdManager, + private readonly ISignatureManager $signatureManager, + private readonly OCMSignatoryManager $signatoryManager, ) { parent::__construct($appName, $request); } @@ -77,6 +82,12 @@ public function __construct( * 501: Share type or the resource type is not supported */ public function addShare($shareWith, $name, $description, $providerId, $owner, $ownerDisplayName, $sharedBy, $sharedByDisplayName, $protocol, $shareType, $resourceType) { + try { + $signedRequest = $this->signatureManager->getIncomingSignedRequest($this->signatoryManager); + } catch (SignatureException $e) { + // impossible to confirm signature + } + // check if all required parameters are set if ($shareWith === null || $name === null || @@ -99,6 +110,7 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $ ); } + $supportedShareTypes = $this->config->getSupportedShareTypes($resourceType); if (!in_array($shareType, $supportedShareTypes)) { return new JSONResponse( diff --git a/apps/files_sharing/lib/External/Cache.php b/apps/files_sharing/lib/External/Cache.php index 7fad71b9084cc..cae5dc62c0a60 100644 --- a/apps/files_sharing/lib/External/Cache.php +++ b/apps/files_sharing/lib/External/Cache.php @@ -22,7 +22,10 @@ class Cache extends \OC\Files\Cache\Cache { public function __construct($storage, ICloudId $cloudId) { $this->cloudId = $cloudId; $this->storage = $storage; - [, $remote] = explode('://', $cloudId->getRemote(), 2); + $remote = $cloudId->getRemote(); + if (str_contains($remote, '://')) { + [, $remote] = explode('://', $cloudId->getRemote(), 2); + } $this->remote = $remote; $this->remoteUser = $cloudId->getUser(); parent::__construct($storage); diff --git a/core/Controller/OCMController.php b/core/Controller/OCMController.php index f8110278b2093..41638beb82952 100644 --- a/core/Controller/OCMController.php +++ b/core/Controller/OCMController.php @@ -15,6 +15,7 @@ use OCP\AppFramework\Http\Attribute\FrontpageRoute; use OCP\AppFramework\Http\DataResponse; use OCP\Capabilities\ICapability; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IRequest; use OCP\Server; @@ -29,7 +30,7 @@ class OCMController extends Controller { public function __construct( IRequest $request, - private IConfig $config, + private readonly IAppConfig $appConfig, private LoggerInterface $logger ) { parent::__construct('core', $request); @@ -52,10 +53,10 @@ public function __construct( public function discovery(): DataResponse { try { $cap = Server::get( - $this->config->getAppValue( - 'core', - 'ocm_providers', - '\OCA\CloudFederationAPI\Capabilities' + $this->appConfig->getValueString( + 'core', 'ocm_providers', + \OCA\CloudFederationAPI\Capabilities::class, + lazy: true ) ); diff --git a/lib/private/AppFramework/Http/Request.php b/lib/private/AppFramework/Http/Request.php index 0bd430545d42e..34b429cb0ba48 100644 --- a/lib/private/AppFramework/Http/Request.php +++ b/lib/private/AppFramework/Http/Request.php @@ -352,7 +352,7 @@ public function getCookie(string $key) { * * @throws \LogicException */ - protected function getContent() { + public function getContent() { // If the content can't be parsed into an array then return a stream resource. if ($this->isPutStreamContent()) { if ($this->content === false) { diff --git a/lib/private/Federation/CloudFederationProviderManager.php b/lib/private/Federation/CloudFederationProviderManager.php index 79b37b44c828d..b8ccb90edde05 100644 --- a/lib/private/Federation/CloudFederationProviderManager.php +++ b/lib/private/Federation/CloudFederationProviderManager.php @@ -9,6 +9,7 @@ namespace OC\Federation; use OC\AppFramework\Http; +use OC\OCM\OCMSignatoryManager; use OCP\App\IAppManager; use OCP\Federation\Exceptions\ProviderDoesNotExistsException; use OCP\Federation\ICloudFederationNotification; @@ -21,6 +22,7 @@ use OCP\IConfig; use OCP\OCM\Exceptions\OCMProviderException; use OCP\OCM\IOCMDiscoveryService; +use OCP\Security\Signature\ISignatureManager; use Psr\Log\LoggerInterface; /** @@ -40,7 +42,9 @@ public function __construct( private IClientService $httpClientService, private ICloudIdManager $cloudIdManager, private IOCMDiscoveryService $discoveryService, - private LoggerInterface $logger + private readonly ISignatureManager $signatureManager, + private readonly OCMSignatoryManager $signatoryManager, + private LoggerInterface $logger, ) { } @@ -106,13 +110,18 @@ public function sendShare(ICloudFederationShare $share) { $client = $this->httpClientService->newClient(); try { - $response = $client->post($ocmProvider->getEndPoint() . '/shares', [ - 'body' => json_encode($share->getShare()), - 'headers' => ['content-type' => 'application/json'], - 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false), - 'timeout' => 10, - 'connect_timeout' => 10, - ]); + $uri = $ocmProvider->getEndPoint() . '/shares'; + $signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( + $this->signatoryManager, + [ + 'body' => json_encode($share->getShare()), + 'headers' => ['content-type' => 'application/json'], + 'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates', false), + 'timeout' => 10, + 'connect_timeout' => 10, + ], + 'post', $uri); + $response = $client->post($uri, $signedPayload); if ($response->getStatusCode() === Http::STATUS_CREATED) { $result = json_decode($response->getBody(), true); diff --git a/lib/private/OCM/Model/OCMProvider.php b/lib/private/OCM/Model/OCMProvider.php index 17a356428f7a4..d38569cc334de 100644 --- a/lib/private/OCM/Model/OCMProvider.php +++ b/lib/private/OCM/Model/OCMProvider.php @@ -9,12 +9,14 @@ namespace OC\OCM\Model; +use OC\Security\Signature\Model\Signatory; use OCP\EventDispatcher\IEventDispatcher; use OCP\OCM\Events\ResourceTypeRegisterEvent; use OCP\OCM\Exceptions\OCMArgumentException; use OCP\OCM\Exceptions\OCMProviderException; use OCP\OCM\IOCMProvider; use OCP\OCM\IOCMResource; +use OCP\Security\Signature\Model\ISignatory; /** * @since 28.0.0 @@ -25,7 +27,7 @@ class OCMProvider implements IOCMProvider { private string $endPoint = ''; /** @var IOCMResource[] */ private array $resourceTypes = []; - + private ?ISignatory $signatory = null; private bool $emittedEvent = false; public function __construct( @@ -152,6 +154,14 @@ public function extractProtocolEntry(string $resourceName, string $protocol): st throw new OCMArgumentException('resource not found'); } + public function setSignatory(ISignatory $signatory){ + $this->signatory = $signatory; + } + + public function getSignatory(): ?ISignatory { + return $this->signatory; + } + /** * import data from an array * @@ -163,7 +173,7 @@ public function extractProtocolEntry(string $resourceName, string $protocol): st */ public function import(array $data): static { $this->setEnabled(is_bool($data['enabled'] ?? '') ? $data['enabled'] : false) - ->setApiVersion((string)($data['apiVersion'] ?? '')) + ->setApiVersion((string)($data['version'] ?? '')) ->setEndPoint($data['endPoint'] ?? ''); $resources = []; @@ -173,6 +183,13 @@ public function import(array $data): static { } $this->setResourceTypes($resources); + $signatory = new Signatory($data['publicKey']['keyId'] ?? '', $data['publicKey']['publicKeyPem'] ?? ''); + if ($signatory->getKeyId() !== '' + && $signatory->getPublicKey() !== '') + { + $this->setSignatory($signatory); + } + if (!$this->looksValid()) { throw new OCMProviderException('remote provider does not look valid'); } @@ -209,8 +226,10 @@ public function jsonSerialize(): array { return [ 'enabled' => $this->isEnabled(), - 'apiVersion' => $this->getApiVersion(), + 'apiVersion' => '1.0-proposal1', // keep it like this to stay compatible with old version + 'version' => $this->getApiVersion(), 'endPoint' => $this->getEndPoint(), + 'publicKey' => $this->getSignatory(), 'resourceTypes' => $resourceTypes ]; } diff --git a/lib/private/OCM/OCMDiscoveryService.php b/lib/private/OCM/OCMDiscoveryService.php index 62313a9af80f2..dba1e60ae6695 100644 --- a/lib/private/OCM/OCMDiscoveryService.php +++ b/lib/private/OCM/OCMDiscoveryService.php @@ -25,12 +25,6 @@ */ class OCMDiscoveryService implements IOCMDiscoveryService { private ICache $cache; - private array $supportedAPIVersion = - [ - '1.0-proposal1', - '1.0', - '1.1' - ]; public function __construct( ICacheFactory $cacheFactory, @@ -56,9 +50,7 @@ public function discover(string $remote, bool $skipCache = false): IOCMProvider if (!$skipCache) { try { $this->provider->import(json_decode($this->cache->get($remote) ?? '', true, 8, JSON_THROW_ON_ERROR) ?? []); - if ($this->supportedAPIVersion($this->provider->getApiVersion())) { - return $this->provider; // if cache looks valid, we use it - } + return $this->provider; } catch (JsonException|OCMProviderException $e) { // we ignore cache on issues } @@ -91,30 +83,6 @@ public function discover(string $remote, bool $skipCache = false): IOCMProvider throw new OCMProviderException('error while requesting remote ocm provider'); } - if (!$this->supportedAPIVersion($this->provider->getApiVersion())) { - throw new OCMProviderException('API version not supported'); - } - return $this->provider; } - - /** - * Check the version from remote is supported. - * The minor version of the API will be ignored: - * 1.0.1 is identified as 1.0 - * - * @param string $version - * - * @return bool - */ - private function supportedAPIVersion(string $version): bool { - $dot1 = strpos($version, '.'); - $dot2 = strpos($version, '.', $dot1 + 1); - - if ($dot2 > 0) { - $version = substr($version, 0, $dot2); - } - - return (in_array($version, $this->supportedAPIVersion)); - } } diff --git a/lib/private/OCM/OCMSignatoryManager.php b/lib/private/OCM/OCMSignatoryManager.php index 50b83933be1d4..e7e09d4ea5a8e 100644 --- a/lib/private/OCM/OCMSignatoryManager.php +++ b/lib/private/OCM/OCMSignatoryManager.php @@ -4,11 +4,64 @@ namespace OC\OCM; +use OC\Security\Signature\Model\Signatory; +use OCP\IURLGenerator; +use OCP\Security\PublicPrivateKeyPairs\IKeyPairManager; +use OCP\Security\Signature\Exceptions\SignatoryException; use OCP\Security\Signature\ISignatoryManager; +use OCP\Security\Signature\Model\IIncomingSignedRequest; +use OCP\Security\Signature\Model\ISignatory; class OCMSignatoryManager implements ISignatoryManager { - public function generateSignatory(IIncomingSignedRequest $signedRequest): ISignatory { + public function __construct( + private readonly IURLGenerator $urlGenerator, + private readonly IKeyPairManager $keyPairManager, + private readonly OCMDiscoveryService $ocmDiscoveryService, + ) { + } + + + public function getOptions(): array { + return []; + } + + public function getLocalSignatory(): ISignatory { + $url = $this->urlGenerator->linkToRouteAbsolute('cloud_federation_api.requesthandlercontroller.addShare'); + $pos = strrpos($url, '/'); + if ($pos === false) { + throw new SignatoryException('generated route should contains a slash character'); + } + + // removing /index.php from url + $path = parse_url($url, PHP_URL_PATH); + if (str_starts_with($path, '/index.php/')) { + $pos = strpos($url, '/index.php'); + if ($pos !== false) { + $url = substr_replace($url, '', $pos, 10); + } + } + + $keyId = $url . '#signature'; + $keyPair = $this->keyPairManager->getKeyPair('core', 'ocm'); + + return new Signatory($keyId, $keyPair->getPublicKey(), $keyPair->getPrivateKey()); + } + + + public function getRemoteSignatory(IIncomingSignedRequest $signedRequest, bool $retry): ISignatory { + $data = $signedRequest->getSignatureHeader(); + $origKeyId = $data['keyId']; + + $parsed = parse_url($origKeyId); + $remote = $parsed['scheme'] . '://' . $parsed['host']; + $ocmProvider = $this->ocmDiscoveryService->discover($remote, $retry); + + $signatory = $ocmProvider->getSignatory(); + if ($signatory->getKeyId() !== $origKeyId) { + throw new SignatoryException('keyId from provider is different from the one from signed request'); + } + return $signatory; } } diff --git a/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php b/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php index a98db21aef20f..2e0b8ba235669 100644 --- a/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php +++ b/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php @@ -5,22 +5,25 @@ namespace OC\Security\PublicPrivateKeyPairs; use OC\Security\PublicPrivateKeyPairs\Model\KeyPair; +use OCP\IAppConfig; use OCP\Security\PublicPrivateKeyPairs\IKeyPairManager; use OCP\Security\PublicPrivateKeyPairs\Model\IKeyPair; -use OCP\IAppConfig; class KeyPairManager implements IKeyPairManager { + private const CONFIG_PREFIX = 'security.keypair.'; + public function __construct( private readonly IAppConfig $appConfig, ) { } public function getKeyPair(string $app, string $name, array $options = []): IKeyPair { - if (!$this->appConfig->hasKey($app, $name, lazy: true)) { + $key = $this->generateAppConfigKey($name); + if (!$this->appConfig->hasKey($app, $key, lazy: true)) { return $this->generateKeyPair($app, $name, $options); } - $stored = $this->appConfig->getValueArray($app, $name, lazy: true); + $stored = $this->appConfig->getValueArray($app, $key, lazy: true); if (!array_key_exists('public', $stored) || !array_key_exists('private', $stored)) { return $this->generateKeyPair($app, $name); @@ -34,6 +37,7 @@ public function getKeyPair(string $app, string $name, array $options = []): IKey } public function deleteKeyPair(string $app, string $name): void { + $this->appConfig->deleteKey('core', $this->generateAppConfigKey($name)); } public function testKeyPair(IKeyPair $keyPair): bool { @@ -43,15 +47,19 @@ public function testKeyPair(IKeyPair $keyPair): bool { return false; } + private function generateAppConfigKey(string $name): string { + return self::CONFIG_PREFIX . $name; + } + private function generateKeyPair(string $app, string $name, array $options = []): IKeyPair { $keyPair = new KeyPair($app, $name); [$publicKey, $privateKey] = $this->generateKeys($options); - $keyPair->setPublicKey($publicKey); - $keyPair->setPrivateKey($privateKey); + $keyPair->setPublicKey($publicKey) + ->setPrivateKey($privateKey); $this->appConfig->setValueArray( - $app, 'security.keypair.' . $name, + $app, $this->generateAppConfigKey($name), [ 'public' => $keyPair->getPublicKey(), 'private' => $keyPair->getPrivateKey() @@ -68,7 +76,7 @@ private function generateKeyPair(string $app, string $name, array $options = []) * * @return array */ - private function generateKeys(array $options = []):array { + private function generateKeys(array $options = []): array { $res = openssl_pkey_new( [ 'digest_alg' => $options['algorithm'] ?? 'rsa', diff --git a/lib/private/Security/Signature/Model/IncomingSignedRequest.php b/lib/private/Security/Signature/Model/IncomingSignedRequest.php index 9b95f2df02262..4280a3010d247 100644 --- a/lib/private/Security/Signature/Model/IncomingSignedRequest.php +++ b/lib/private/Security/Signature/Model/IncomingSignedRequest.php @@ -20,9 +20,9 @@ class IncomingSignedRequest extends SignedRequest { private ?IRequest $request = null; private int $time = 0; - private string $origin; - private string $host; - private string $estimatedSignature; + private string $origin = ''; + private string $host = ''; + private string $estimatedSignature = ''; public function setRequest(IRequest $request): IIncomingSignedRequest { $this->request = $request; diff --git a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php index 9975555e761e7..1d8a382839d58 100644 --- a/lib/private/Security/Signature/Model/OutgoingSignedRequest.php +++ b/lib/private/Security/Signature/Model/OutgoingSignedRequest.php @@ -9,7 +9,6 @@ namespace OC\Security\Signature\Model; use JsonSerializable; -use OCP\IRequest; use OCP\Security\Signature\Model\IOutgoingSignedRequest; class OutgoingSignedRequest extends SignedRequest @@ -17,18 +16,10 @@ class OutgoingSignedRequest extends SignedRequest IOutgoingSignedRequest, JsonSerializable { - private IRequest $request; private string $host = ''; + private array $headers = []; private string $clearSignature = ''; - - public function setRequest(IRequest $request): IOutgoingSignedRequest { - $this->request = $request; - return $this; - } - - public function getRequest(): IRequest { - return $this->request; - } + private string $algorithm; /** remote address */ public function setHost(string $host): IOutgoingSignedRequest { @@ -40,6 +31,15 @@ public function getHost(): string { return $this->host; } + public function addHeader(string $key, string|int|float|bool|array $value): IOutgoingSignedRequest { + $this->headers[$key] = $value; + return $this; + } + + public function getHeaders(): array { + return $this->headers; + } + public function setClearSignature(string $estimated): self { $this->clearSignature = $estimated; return $this; @@ -49,11 +49,20 @@ public function getClearSignature(): string { return $this->clearSignature; } + public function setAlgorithm(string $algorithm): IOutgoingSignedRequest { + $this->algorithm = $algorithm; + return $this; + } + + public function getAlgorithm(): string { + return $this->algorithm; + } + public function jsonSerialize(): array { return array_merge( parent::jsonSerialize(), [ - 'outgoingRequest' => $this->request ?? false, + 'headers' => $this->headers, 'host' => $this->getHost(), 'clearSignature' => $this->getClearSignature(), ] diff --git a/lib/private/Security/Signature/Model/Signatory.php b/lib/private/Security/Signature/Model/Signatory.php index 00ad18981e6f4..f7eb5f1aeca30 100644 --- a/lib/private/Security/Signature/Model/Signatory.php +++ b/lib/private/Security/Signature/Model/Signatory.php @@ -13,11 +13,16 @@ class Signatory implements ISignatory, JsonSerializable { public function __construct( + private readonly string $keyId, private readonly string $publicKey, - private readonly string $privateKey = '' + private readonly string $privateKey = '', ) { } + public function getKeyId(): string { + return $this->keyId; + } + public function getPublicKey(): string { return $this->publicKey; } @@ -28,9 +33,8 @@ public function getPrivateKey(): string { public function jsonSerialize(): array { return [ - 'publicKey' => $this->getPublicKey(), - 'publicKeyChecksum' => ($this->getPublicKey() !== '') ? sha1($this->getPublicKey()) : false, - 'privateKeyChecksum' => ($this->getPrivateKey() !== '') ? sha1($this->getPrivateKey()) : false + 'keyId' => $this->getKeyId(), + 'publicKeyPem' => $this->getPublicKey(), ]; } } diff --git a/lib/private/Security/Signature/Model/SignedRequest.php b/lib/private/Security/Signature/Model/SignedRequest.php index 1a201c31560ed..db972bee5f863 100644 --- a/lib/private/Security/Signature/Model/SignedRequest.php +++ b/lib/private/Security/Signature/Model/SignedRequest.php @@ -17,7 +17,7 @@ class SignedRequest implements ISignedRequest, JsonSerializable { private string $digest; private string $signedSignature = ''; private array $signatureHeader = []; - private ISignatory|IKeyPair|null $signatory = null; + private ?ISignatory $signatory = null; public function __construct( private readonly string $body) { @@ -55,7 +55,7 @@ public function setSignatory(ISignatory|IKeyPair $signatory): ISignedRequest { return $this; } - public function getSignatory(): ISignatory|IKeyPair { + public function getSignatory(): ISignatory { return $this->signatory; } diff --git a/lib/private/Security/Signature/SignatureManager.php b/lib/private/Security/Signature/SignatureManager.php index 9903785951e3c..99016c988133b 100644 --- a/lib/private/Security/Signature/SignatureManager.php +++ b/lib/private/Security/Signature/SignatureManager.php @@ -5,6 +5,7 @@ namespace OC\Security\Signature; use OC\Security\Signature\Model\IncomingSignedRequest; +use OC\Security\Signature\Model\OutgoingSignedRequest; use OC\Security\Signature\Model\Signatory; use OCP\Files\AppData\IAppDataFactory; use OCP\Files\IAppData; @@ -16,15 +17,16 @@ use OCP\Security\Signature\Exceptions\IncomingRequestException; use OCP\Security\Signature\Exceptions\InvalidKeyOriginException; use OCP\Security\Signature\Exceptions\InvalidSignatureException; +use OCP\Security\Signature\Exceptions\SignatoryException; use OCP\Security\Signature\Exceptions\SignatoryNotFoundException; use OCP\Security\Signature\Exceptions\SignatureException; use OCP\Security\Signature\ISignatoryManager; use OCP\Security\Signature\ISignatureManager; use OCP\Security\Signature\Model\IIncomingSignedRequest; +use OCP\Security\Signature\Model\IOutgoingSignedRequest; use OCP\Security\Signature\Model\ISignatory; use OCP\Security\Signature\SignatureAlgorithm; use Psr\Log\LoggerInterface; -use Sabre\HTTP\RequestInterface; class SignatureManager implements ISignatureManager { public const DATE_HEADER = 'D, d M Y H:i:s T'; @@ -35,45 +37,85 @@ class SignatureManager implements ISignatureManager { public function __construct( readonly IAppDataFactory $appDataFactory, - private readonly IKeyPairManager $keyPairManager, private readonly IRequest $request, - private readonly RequestInterface $request2, private readonly LoggerInterface $logger, ) { $this->appData = $this->appDataFactory->get('core'); } /** - * must be called from Controller - * - * @param string $body + * @param ISignatoryManager $signatoryManager + * @param array $options * * @return IIncomingSignedRequest + * @throws IncomingRequestException + * @throws SignatoryNotFoundException + * @throws SignatureException */ public function getIncomingSignedRequest( ISignatoryManager $signatoryManager, - array $options = [] + ?string $body = null ): IIncomingSignedRequest { -// if ($body === '') { -// $body = file_get_contents('php://input'); -// } - - $body = $this->request2->getBody(); + $body = $body ?? file_get_contents('php://input'); $this->logger->debug('[<<] incoming signed request', ['body' => $body]); $signedRequest = new IncomingSignedRequest($body); $signedRequest->setRequest($this->request); + $options = $signatoryManager->getOptions(); - $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); + 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); + } catch (SignatureException $e) { + $this->logger->warning('signature could not be verified', ['exception' => $e, 'signedRequest' => $signedRequest, 'signatoryManager' => get_class($signatoryManager)]); + throw $e; + } + + return $signedRequest; + } + + public function getOutgoingSignedRequest( + ISignatoryManager $signatoryManager, + string $content, + string $method, + string $uri + ): IOutgoingSignedRequest { + $signedRequest = new OutgoingSignedRequest($content); + + $parsed = parse_url($uri); + $signedRequest->setHost($parsed['host']) + ->setAlgorithm($options['algorithm'] ?? 'sha256') + ->setSignatory($signatoryManager->getLocalSignatory()); + + $options = $signatoryManager->getOptions(); + $this->setOutgoingSignatureHeader( + $signedRequest, + strtolower($method), + $parsed['path'] ?? '/', + $options['dateHeader'] ?? self::DATE_HEADER + ); + $this->setOutgoingClearSignature($signedRequest); + $this->setOutgoingSignedSignature($signedRequest); + $this->signingOutgoingRequest($signedRequest); return $signedRequest; } + 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; + } + /** * confirm request is not older than ttl * @@ -118,7 +160,7 @@ private function verifyIncomingRequestContent(IIncomingSignedRequest $signedRequ } if (strlen($signedRequest->getBody()) !== (int)$request->getHeader('content-length')) { - throw new IncomingRequestException('invalid content-length in header'); + throw new IncomingRequestException('inegual content-length in header: ' . strlen($signedRequest->getBody()) . ' vs ' . (int)$request->getHeader('content-length')); } $digest = $request->getHeader('digest'); @@ -187,9 +229,7 @@ private function prepEstimatedSignature( $missingHeaders = array_diff($enforceHeaders, $headers); if ($missingHeaders !== []) { - throw new IncomingRequestException( - 'missing elements in headers: ' . json_encode($missingHeaders) - ); + throw new IncomingRequestException('missing elements in headers: ' . json_encode($missingHeaders)); } $target = strtolower($request->getMethod()) . " " . $request->getRequestUri(); @@ -224,14 +264,14 @@ private function verifyIncomingRequestSignature( $data = $signedRequest->getSignatureHeader(); $keyId = $data['keyId']; - $signatoryManager->generateSignatory($signedRequest); +// $signatoryManager->getRemoteSignatory($signedRequest, false); // TODO: check keyId belongs to host ? is it done before already ? try { $signedRequest->setSignatory($this->getStoredSignatory($keyId)); $this->verifySignedRequest($signedRequest); } catch (SignatoryNotFoundException|SignatureException $e) { - $signatory = $signatoryManager->generateSignatory($signedRequest); + $signatory = $signatoryManager->getRemoteSignatory($signedRequest, true); $signedRequest->setSignatory($signatory); $this->verifySignedRequest($signedRequest); $this->storeSignatory($keyId, $signatory); @@ -239,6 +279,64 @@ private function verifyIncomingRequestSignature( } + + 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(), $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 * @@ -265,15 +363,42 @@ private function verifySignedRequest(IIncomingSignedRequest $signedRequest): voi } } + private function getUsedEncryption(IIncomingSignedRequest $signedRequest): SignatureAlgorithm { $data = $signedRequest->getSignatureHeader(); - return match ($data['algorithm']) { 'rsa-sha512' => SignatureAlgorithm::SHA512, default => SignatureAlgorithm::SHA256, }; } + private function getChosenEncryption(string $algorithm): string { + return match ($algorithm) { + 'sha512' => 'ras-sha512', + default => 'ras-sha256', + }; + } + + public function getOpenSSLAlgo(string $algorithm): int { + return match ($algorithm) { + 'sha512' => OPENSSL_ALGO_SHA512, + default => OPENSSL_ALGO_SHA256, + }; + } + + + public function signString(string $clear, ISignatory $signatory, string $algorithm): string { + $privateKey = $signatory->getPrivateKey(); + 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 $signed @@ -294,6 +419,11 @@ private function verifyString( } } + + + + + /** * @param string $keyId * diff --git a/lib/private/Server.php b/lib/private/Server.php index bcdf482f02d0e..519ae29605989 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -100,8 +100,10 @@ use OC\Security\CSRF\CsrfTokenManager; use OC\Security\CSRF\TokenStorage\SessionStorage; use OC\Security\Hasher; +use OC\Security\PublicPrivateKeyPairs\KeyPairManager; use OC\Security\RateLimiting\Limiter; use OC\Security\SecureRandom; +use OC\Security\Signature\SignatureManager; use OC\Security\TrustedDomainHelper; use OC\Security\VerificationToken\VerificationToken; use OC\Session\CryptoWrapper; @@ -215,7 +217,9 @@ use OCP\Security\IHasher; use OCP\Security\ISecureRandom; use OCP\Security\ITrustedDomainHelper; +use OCP\Security\PublicPrivateKeyPairs\IKeyPairManager; use OCP\Security\RateLimiting\ILimiter; +use OCP\Security\Signature\ISignatureManager; use OCP\Security\VerificationToken\IVerificationToken; use OCP\Settings\IDeclarativeManager; use OCP\SetupCheck\ISetupCheckManager; @@ -1287,16 +1291,17 @@ public function __construct($webRoot, \OC\Config $config) { $this->registerAlias(\OCP\GlobalScale\IConfig::class, \OC\GlobalScale\Config::class); - $this->registerService(ICloudFederationProviderManager::class, function (ContainerInterface $c) { - return new CloudFederationProviderManager( - $c->get(\OCP\IConfig::class), - $c->get(IAppManager::class), - $c->get(IClientService::class), - $c->get(ICloudIdManager::class), - $c->get(IOCMDiscoveryService::class), - $c->get(LoggerInterface::class) - ); - }); +// $this->registerService(ICloudFederationProviderManager::class, function (ContainerInterface $c) { +// return new CloudFederationProviderManager( +// $c->get(\OCP\IConfig::class), +// $c->get(IAppManager::class), +// $c->get(IClientService::class), +// $c->get(ICloudIdManager::class), +// $c->get(IOCMDiscoveryService::class), +// $c->get(LoggerInterface::class) +// ); +// }); + $this->registerAlias(ICloudFederationProviderManager::class, CloudFederationProviderManager::class); $this->registerService(ICloudFederationFactory::class, function (Server $c) { return new CloudFederationFactory(); @@ -1401,6 +1406,9 @@ public function __construct($webRoot, \OC\Config $config) { $this->registerAlias(\OCP\TaskProcessing\IManager::class, \OC\TaskProcessing\Manager::class); + $this->registerAlias(IKeyPairManager::class, KeyPairManager::class); + $this->registerAlias(ISignatureManager::class, SignatureManager::class); + $this->connectDispatcher(); } diff --git a/lib/public/OCM/IOCMProvider.php b/lib/public/OCM/IOCMProvider.php index 58b50aca17215..03fc4b4558f84 100644 --- a/lib/public/OCM/IOCMProvider.php +++ b/lib/public/OCM/IOCMProvider.php @@ -12,6 +12,7 @@ use JsonSerializable; use OCP\OCM\Exceptions\OCMArgumentException; use OCP\OCM\Exceptions\OCMProviderException; +use OCP\Security\Signature\Model\ISignatory; /** * Model based on the Open Cloud Mesh Discovery API @@ -120,6 +121,21 @@ public function getResourceTypes(): array; */ public function extractProtocolEntry(string $resourceName, string $protocol): string; + /** + * store signatory (public/private key pair) to sign outgoing/incoming request + * + * @param ISignatory $signatory + * @since 30.0.0 + */ + public function setSignatory(ISignatory $signatory); + + /** + * signatory (public/private key pair) used to sign outgoing/incoming request + * + * @return ISignatory|null returns null if no ISignatory available + */ + public function getSignatory(): ?ISignatory; + /** * import data from an array * diff --git a/lib/public/Security/Signature/Exceptions/SignatoryException.php b/lib/public/Security/Signature/Exceptions/SignatoryException.php index f8449b5986513..e4d8eba53c9b8 100644 --- a/lib/public/Security/Signature/Exceptions/SignatoryException.php +++ b/lib/public/Security/Signature/Exceptions/SignatoryException.php @@ -8,10 +8,8 @@ namespace OCP\Security\Signature\Exceptions; -use Exception; - /** * @since 30.0.0 */ -class SignatoryException extends Exception { +class SignatoryException extends SignatureException { } diff --git a/lib/public/Security/Signature/ISignatoryManager.php b/lib/public/Security/Signature/ISignatoryManager.php index 5477ccc24a144..64059d535c67f 100644 --- a/lib/public/Security/Signature/ISignatoryManager.php +++ b/lib/public/Security/Signature/ISignatoryManager.php @@ -8,5 +8,7 @@ use OCP\Security\Signature\Model\ISignatory; interface ISignatoryManager { - public function generateSignatory(IIncomingSignedRequest $signedRequest): ISignatory; + public function getOptions(): array; + public function getLocalSignatory(): ISignatory; + public function getRemoteSignatory(IIncomingSignedRequest $signedRequest, bool $retry): ISignatory; } diff --git a/lib/public/Security/Signature/ISignatureManager.php b/lib/public/Security/Signature/ISignatureManager.php index 8d4f78b1a97f0..7792de2253282 100644 --- a/lib/public/Security/Signature/ISignatureManager.php +++ b/lib/public/Security/Signature/ISignatureManager.php @@ -4,5 +4,11 @@ namespace OCP\Security\Signature; +use OCP\Security\Signature\Model\IIncomingSignedRequest; +use OCP\Security\Signature\Model\IOutgoingSignedRequest; + interface ISignatureManager { + public function getIncomingSignedRequest(ISignatoryManager $signatoryManager, ?string $body = null): IIncomingSignedRequest; + public function getOutgoingSignedRequest(ISignatoryManager $signatoryManager, string $content, string $method, string $uri): IOutgoingSignedRequest; + public function signOutgoingRequestIClientPayload(ISignatoryManager $signatoryManager, array $payload, string $method, string $uri): array; } diff --git a/lib/public/Security/Signature/Model/IOutgoingSignedRequest.php b/lib/public/Security/Signature/Model/IOutgoingSignedRequest.php index 6ac77f080816c..c9b81afd56fd9 100644 --- a/lib/public/Security/Signature/Model/IOutgoingSignedRequest.php +++ b/lib/public/Security/Signature/Model/IOutgoingSignedRequest.php @@ -7,10 +7,12 @@ use OCP\IRequest; interface IOutgoingSignedRequest extends ISignedRequest { - public function setRequest(IRequest $request): IOutgoingSignedRequest; - public function getRequest(): IRequest; public function setHost(string $host): IOutgoingSignedRequest; public function getHost(): string; + public function addHeader(string $key, string|int|float|bool|array $value): IOutgoingSignedRequest; + public function getHeaders(): array; public function setClearSignature(string $estimated): IOutgoingSignedRequest; public function getClearSignature(): string; + public function setAlgorithm(string $algorithm): IOutgoingSignedRequest; + public function getAlgorithm(): string; } diff --git a/lib/public/Security/Signature/Model/ISignatory.php b/lib/public/Security/Signature/Model/ISignatory.php index f3194f9fb7edc..44d1250c1f3e0 100644 --- a/lib/public/Security/Signature/Model/ISignatory.php +++ b/lib/public/Security/Signature/Model/ISignatory.php @@ -5,6 +5,7 @@ namespace OCP\Security\Signature\Model; interface ISignatory { + public function getKeyId(): string; public function getPublicKey(): string; public function getPrivateKey(): string; }