fix(ocm): switching to IdentityProof

Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
pull/45979/head
Maxence Lange 5 months ago
parent 4591430c9c
commit f08d053290
  1. 4
      apps/cloud_federation_api/lib/Capabilities.php
  2. 46
      apps/cloud_federation_api/lib/Controller/RequestHandlerController.php
  3. 3
      apps/federatedfilesharing/lib/FederatedShareProvider.php
  4. 3
      apps/federatedfilesharing/lib/Notifications.php
  5. 2
      build/integration/features/bootstrap/FederationContext.php
  6. 24
      build/integration/federation_features/cleanup-remote-storage.feature
  7. 2
      lib/composer/composer/autoload_classmap.php
  8. 2
      lib/composer/composer/autoload_static.php
  9. 109
      lib/private/Federation/CloudFederationProviderManager.php
  10. 7
      lib/private/Files/Storage/DAV.php
  11. 4
      lib/private/OCM/Model/OCMProvider.php
  12. 13
      lib/private/OCM/OCMDiscoveryService.php
  13. 20
      lib/private/OCM/OCMSignatoryManager.php
  14. 51
      lib/private/Security/IdentityProof/Manager.php
  15. 182
      lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php
  16. 114
      lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php
  17. 7
      lib/private/Security/Signature/SignatureManager.php
  18. 2
      lib/private/Server.php
  19. 4
      lib/public/OCM/IOCMProvider.php
  20. 18
      lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairConflictException.php
  21. 20
      lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairException.php
  22. 16
      lib/unstable/Security/PublicPrivateKeyPairs/Exceptions/KeyPairNotFoundException.php
  23. 80
      lib/unstable/Security/PublicPrivateKeyPairs/IKeyPairManager.php
  24. 85
      lib/unstable/Security/PublicPrivateKeyPairs/Model/IKeyPair.php

@ -8,7 +8,7 @@ declare(strict_types=1);
*/
namespace OCA\CloudFederationAPI;
use NCU\Security\PublicPrivateKeyPairs\Exceptions\KeyPairException;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\Exceptions\SignatoryException;
use OC\OCM\OCMSignatoryManager;
use OCP\Capabilities\ICapability;
@ -79,7 +79,7 @@ class Capabilities implements ICapability {
} else {
$this->logger->debug('ocm public key feature disabled');
}
} catch (SignatoryException|KeyPairException $e) {
} catch (SignatoryException|IdentityNotFoundException $e) {
$this->logger->warning('cannot generate local signatory', ['exception' => $e]);
}

@ -5,6 +5,7 @@
*/
namespace OCA\CloudFederationAPI\Controller;
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
use NCU\Security\Signature\Exceptions\IncomingRequestException;
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
use NCU\Security\Signature\Exceptions\SignatureException;
@ -14,6 +15,7 @@ use NCU\Security\Signature\Model\IIncomingSignedRequest;
use OC\OCM\OCMSignatoryManager;
use OCA\CloudFederationAPI\Config;
use OCA\CloudFederationAPI\ResponseDefinitions;
use OCA\FederatedFileSharing\AddressHandler;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
@ -60,6 +62,7 @@ class RequestHandlerController extends Controller {
private IURLGenerator $urlGenerator,
private ICloudFederationProviderManager $cloudFederationProviderManager,
private Config $config,
private readonly AddressHandler $addressHandler,
private readonly IAppConfig $appConfig,
private ICloudFederationFactory $factory,
private ICloudIdManager $cloudIdManager,
@ -289,6 +292,7 @@ class RequestHandlerController extends Controller {
$response->throttle();
return $response;
} catch (\Exception $e) {
$this->logger->warning('incoming notification exception', ['exception' => $e]);
return new JSONResponse(
[
'message' => 'Internal error at ' . $this->urlGenerator->getBaseUrl(),
@ -376,7 +380,7 @@ class RequestHandlerController extends Controller {
$body = json_decode($signedRequest->getBody(), true) ?? [];
$entry = trim($body[$key] ?? '', '@');
if ($this->getHostFromFederationId($entry) !== $signedRequest->getOrigin()) {
throw new IncomingRequestException('share initiation from different instance');
throw new IncomingRequestException('share initiation (' . $signedRequest->getOrigin() . ') from different instance (' . $entry . ') [key=' . $key . ']');
}
}
@ -391,7 +395,6 @@ class RequestHandlerController extends Controller {
* @param IIncomingSignedRequest|null $signedRequest
* @param string $token
*
* @return void
* @throws IncomingRequestException
*/
private function confirmShareOrigin(?IIncomingSignedRequest $signedRequest, string $token): void {
@ -401,8 +404,23 @@ class RequestHandlerController extends Controller {
$provider = $this->shareProviderFactory->getProviderForType(IShare::TYPE_REMOTE);
$share = $provider->getShareByToken($token);
$entry = $share->getSharedWith();
try {
$this->confirmShareEntry($signedRequest, $share->getSharedWith());
} catch (IncomingRequestException) {
// notification might come from the instance that owns the share
$this->logger->debug('could not confirm origin on sharedWith (' . $share->getSharedWIth() . '); going with shareOwner (' . $share->getShareOwner() . ')');
$this->confirmShareEntry($signedRequest, $share->getShareOwner());
}
}
/**
* @param IIncomingSignedRequest|null $signedRequest
* @param string $entry
*
* @return void
* @throws IncomingRequestException
*/
private function confirmShareEntry(?IIncomingSignedRequest $signedRequest, string $entry): void {
$instance = $this->getHostFromFederationId($entry);
if ($signedRequest === null) {
try {
@ -412,7 +430,7 @@ class RequestHandlerController extends Controller {
return;
}
} elseif ($instance !== $signedRequest->getOrigin()) {
throw new IncomingRequestException('token sharedWith from different instance');
throw new IncomingRequestException('token sharedWith (' . $instance . ') not linked to origin (' . $signedRequest->getOrigin() . ')');
}
}
@ -423,20 +441,16 @@ class RequestHandlerController extends Controller {
*/
private function getHostFromFederationId(string $entry): string {
if (!str_contains($entry, '@')) {
throw new IncomingRequestException('entry does not contains @');
throw new IncomingRequestException('entry ' . $entry . ' does not contains @');
}
[, $rightPart] = explode('@', $entry, 2);
$rightPart = substr($entry, strrpos($entry, '@') + 1);
$host = parse_url($rightPart, PHP_URL_HOST);
$port = parse_url($rightPart, PHP_URL_PORT);
if ($port !== null && $port !== false) {
$host .= ':' . $port;
}
if (is_string($host) && $host !== '') {
return $host;
// in case the full scheme is sent; getting rid of it
$rightPart = $this->addressHandler->removeProtocolFromUrl($rightPart);
try {
return $this->signatureManager->extractIdentityFromUri('https://' . $rightPart);
} catch (IdentityNotFoundException) {
throw new IncomingRequestException('invalid host within federation id: ' . $entry);
}
throw new IncomingRequestException('host is empty');
}
}

@ -250,7 +250,8 @@ class FederatedShareProvider implements IShareProvider {
$remote,
$shareWith,
$share->getPermissions(),
$share->getNode()->getName()
$share->getNode()->getName(),
$share->getShareType(),
);
return [$token, $remoteId];

@ -108,12 +108,13 @@ class Notifications {
* @throws HintException
* @throws \OC\ServerNotAvailableException
*/
public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename) {
public function requestReShare($token, $id, $shareId, $remote, $shareWith, $permission, $filename, $shareType) {
$fields = [
'shareWith' => $shareWith,
'token' => $token,
'permission' => $permission,
'remoteId' => $shareId,
'shareType' => $shareType,
];
$ocmFields = $fields;

@ -38,7 +38,7 @@ class FederationContext implements Context, SnippetAcceptingContext {
$port = getenv('PORT_FED');
self::$phpFederatedServerPid = exec('php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!');
self::$phpFederatedServerPid = exec('PHP_CLI_SERVER_WORKERS=2 php -S localhost:' . $port . ' -t ../../ >/dev/null & echo $!');
}
/**

@ -4,6 +4,27 @@ Feature: cleanup-remote-storage
Background:
Given using api version "1"
Scenario: cleanup remote storage with no storage
Given Using server "LOCAL"
And user "user0" exists
Given Using server "REMOTE"
And user "user1" exists
# Rename file so it has a unique name in the target server (as the target
# server may have its own /textfile0.txt" file)
And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
And As an "user1"
And Deleting last share
And the OCS status code should be "100"
And the HTTP status code should be "200"
And Deleting last share
And Using server "LOCAL"
When invoking occ with "sharing:cleanup-remote-storage"
Then the command was successful
And the command output contains the text "0 remote storage(s) need(s) to be checked"
And the command output contains the text "0 remote share(s) exist"
And the command output contains the text "no storages deleted"
Scenario: cleanup remote storage with active storages
Given Using server "LOCAL"
And user "user0" exists
@ -35,9 +56,6 @@ Feature: cleanup-remote-storage
# server may have its own /textfile0.txt" file)
And User "user1" copies file "/textfile0.txt" to "/remote-share.txt"
And User "user1" from server "REMOTE" shares "/remote-share.txt" with user "user0" from server "LOCAL"
And As an "user1"
And sending "GET" to "/apps/files_sharing/api/v1/shares"
And the list of returned shares has 1 shares
And Using server "LOCAL"
# Accept and download the file to ensure that a storage is created for the
# federated share

@ -1902,8 +1902,6 @@ return array(
'OC\\Security\\Ip\\Range' => $baseDir . '/lib/private/Security/Ip/Range.php',
'OC\\Security\\Ip\\RemoteAddress' => $baseDir . '/lib/private/Security/Ip/RemoteAddress.php',
'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php',
'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php',
'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => $baseDir . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php',
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',

@ -1943,8 +1943,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Security\\Ip\\Range' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/Range.php',
'OC\\Security\\Ip\\RemoteAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Ip/RemoteAddress.php',
'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php',
'OC\\Security\\PublicPrivateKeyPairs\\KeyPairManager' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/KeyPairManager.php',
'OC\\Security\\PublicPrivateKeyPairs\\Model\\KeyPair' => __DIR__ . '/../../..' . '/lib/private/Security/PublicPrivateKeyPairs/Model/KeyPair.php',
'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php',
'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php',
'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php',

@ -18,6 +18,7 @@ use OCP\Federation\ICloudFederationProvider;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\Federation\ICloudFederationShare;
use OCP\Federation\ICloudIdManager;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use OCP\IAppConfig;
@ -105,25 +106,11 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
public function sendShare(ICloudFederationShare $share) {
$cloudID = $this->cloudIdManager->resolveCloudId($share->getShareWith());
try {
$ocmProvider = $this->discoveryService->discover($cloudID->getRemote());
} catch (OCMProviderException $e) {
return false;
}
$client = $this->httpClientService->newClient();
try {
// signing the payload using OCMSignatoryManager before initializing the request
$uri = $ocmProvider->getEndPoint() . '/shares';
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]);
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
$payload,
'post', $uri
);
try {
$response = $this->postOcmPayload($cloudID->getRemote(), '/shares', json_encode($share->getShare()));
} catch (OCMProviderException) {
return false;
}
$response = $client->post($uri, $signedPayload ?? $payload);
if ($response->getStatusCode() === Http::STATUS_CREATED) {
$result = json_decode($response->getBody(), true);
return (is_array($result)) ? $result : [];
@ -149,22 +136,9 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
*/
public function sendCloudShare(ICloudFederationShare $share): IResponse {
$cloudID = $this->cloudIdManager->resolveCloudId($share->getShareWith());
$ocmProvider = $this->discoveryService->discover($cloudID->getRemote());
$client = $this->httpClientService->newClient();
try {
// signing the payload using OCMSignatoryManager before initializing the request
$uri = $ocmProvider->getEndPoint() . '/shares';
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($share->getShare())]);
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
$payload,
'post', $uri
);
}
return $client->post($uri, $signedPayload ?? $payload);
return $this->postOcmPayload($cloudID->getRemote(), '/shares', json_encode($share->getShare()), $client);
} catch (\Throwable $e) {
$this->logger->error('Error while sending share to federation server: ' . $e->getMessage(), ['exception' => $e]);
try {
@ -183,26 +157,11 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
*/
public function sendNotification($url, ICloudFederationNotification $notification) {
try {
$ocmProvider = $this->discoveryService->discover($url);
} catch (OCMProviderException $e) {
return false;
}
$client = $this->httpClientService->newClient();
try {
// signing the payload using OCMSignatoryManager before initializing the request
$uri = $ocmProvider->getEndPoint() . '/notifications';
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]);
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
$payload,
'post', $uri
);
try {
$response = $this->postOcmPayload($url, '/notifications', json_encode($notification->getMessage()));
} catch (OCMProviderException) {
return false;
}
$response = $client->post($uri, $signedPayload ?? $payload);
if ($response->getStatusCode() === Http::STATUS_CREATED) {
$result = json_decode($response->getBody(), true);
return (is_array($result)) ? $result : [];
@ -222,21 +181,9 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
* @throws OCMProviderException
*/
public function sendCloudNotification(string $url, ICloudFederationNotification $notification): IResponse {
$ocmProvider = $this->discoveryService->discover($url);
$client = $this->httpClientService->newClient();
try {
// signing the payload using OCMSignatoryManager before initializing the request
$uri = $ocmProvider->getEndPoint() . '/notifications';
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]);
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
$payload,
'post', $uri
);
}
return $client->post($uri, $signedPayload ?? $payload);
return $this->postOcmPayload($url, '/notifications', json_encode($notification->getMessage()), $client);
} catch (\Throwable $e) {
$this->logger->error('Error while sending notification to federation server: ' . $e->getMessage(), ['exception' => $e]);
try {
@ -256,6 +203,40 @@ class CloudFederationProviderManager implements ICloudFederationProviderManager
return $this->appManager->isEnabledForUser('cloud_federation_api');
}
/**
* @param string $cloudId
* @param string $uri
* @param string $payload
*
* @return IResponse
* @throws OCMProviderException
*/
private function postOcmPayload(string $cloudId, string $uri, string $payload, ?IClient $client = null): IResponse {
$ocmProvider = $this->discoveryService->discover($cloudId);
$uri = $ocmProvider->getEndPoint() . '/' . ltrim($uri, '/');
$client = $client ?? $this->httpClientService->newClient();
return $client->post($uri, $this->prepareOcmPayload($uri, $payload));
}
/**
* @param string $uri
* @param string $payload
*
* @return array
*/
private function prepareOcmPayload(string $uri, string $payload): array {
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => $payload]);
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
$this->signatoryManager,
$payload,
'post', $uri
);
}
return $signedPayload ?? $payload;
}
private function getDefaultRequestOptions(): array {
return [
'headers' => ['content-type' => 'application/json'],

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

@ -210,11 +210,11 @@ class OCMProvider implements IOCMProvider {
* apiVersion: '1.0-proposal1',
* endPoint: string,
* publicKey: ISignatory|null,
* resourceTypes: array{
* resourceTypes: list<array{
* name: string,
* shareTypes: list<string>,
* protocols: array<string, string>
* }[],
* }>,
* version: string
* }
*/

@ -46,6 +46,14 @@ class OCMDiscoveryService implements IOCMDiscoveryService {
*/
public function discover(string $remote, bool $skipCache = false): IOCMProvider {
$remote = rtrim($remote, '/');
if (!str_starts_with($remote, 'http://') && !str_starts_with($remote, 'https://')) {
// if scheme not specified, we test both;
try {
return $this->discover('https://' . $remote, $skipCache);
} catch (OCMProviderException) {
return $this->discover('http://' . $remote, $skipCache);
}
}
if (!$skipCache) {
try {
@ -70,10 +78,7 @@ class OCMDiscoveryService implements IOCMDiscoveryService {
if ($this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates') === true) {
$options['verify'] = false;
}
$response = $client->get(
$remote . '/ocm-provider/',
$options,
);
$response = $client->get($remote . '/ocm-provider/', $options);
if ($response->getStatusCode() === Http::STATUS_OK) {
$body = $response->getBody();

@ -8,15 +8,13 @@ declare(strict_types=1);
*/
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\IdentityProof\Manager;
use OC\Security\Signature\Model\Signatory;
use OCP\IAppConfig;
use OCP\IURLGenerator;
@ -40,7 +38,7 @@ class OCMSignatoryManager implements ISignatoryManager {
private readonly IAppConfig $appConfig,
private readonly ISignatureManager $signatureManager,
private readonly IURLGenerator $urlGenerator,
private readonly IKeyPairManager $keyPairManager,
private readonly Manager $identityProofManager,
private readonly OCMDiscoveryService $ocmDiscoveryService,
) {
}
@ -69,7 +67,6 @@ class OCMSignatoryManager implements ISignatoryManager {
* @inheritDoc
*
* @return ISignatory
* @throws KeyPairConflictException
* @throws IdentityNotFoundException
* @since 31.0.0
*/
@ -85,13 +82,16 @@ class OCMSignatoryManager implements ISignatoryManager {
$keyId = $this->generateKeyId();
}
try {
$keyPair = $this->keyPairManager->getKeyPair('core', 'ocm_external');
} catch (KeyPairNotFoundException) {
$keyPair = $this->keyPairManager->generateKeyPair('core', 'ocm_external');
if (!$this->identityProofManager->hasAppKey('core', 'ocm_external')) {
$this->identityProofManager->generateAppKey('core', 'ocm_external', [
'algorithm' => 'rsa',
'private_key_bits' => 2048,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
}
$keyPair = $this->identityProofManager->getAppKey('core', 'ocm_external');
return new Signatory($keyId, $keyPair->getPublicKey(), $keyPair->getPrivateKey(), local: true);
return new Signatory($keyId, $keyPair->getPublic(), $keyPair->getPrivate(), local: true);
}
/**

@ -10,6 +10,7 @@ namespace OC\Security\IdentityProof;
use OC\Files\AppData\Factory;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\IUser;
use OCP\Security\ICrypto;
@ -31,18 +32,20 @@ class Manager {
* Calls the openssl functions to generate a public and private key.
* In a separate function for unit testing purposes.
*
* @param array $options config options to generate key {@see openssl_csr_new}
*
* @return array [$publicKey, $privateKey]
* @throws \RuntimeException
*/
protected function generateKeyPair(): array {
protected function generateKeyPair(array $options = []): array {
$config = [
'digest_alg' => 'sha512',
'private_key_bits' => 2048,
'digest_alg' => $options['algorithm'] ?? 'sha512',
'private_key_bits' => $options['bits'] ?? 2048,
'private_key_type' => $options['type'] ?? OPENSSL_KEYTYPE_RSA,
];
// Generate new key
$res = openssl_pkey_new($config);
if ($res === false) {
$this->logOpensslError();
throw new \RuntimeException('OpenSSL reported a problem');
@ -65,15 +68,17 @@ class Manager {
* Note: If a key already exists it will be overwritten
*
* @param string $id key id
* @param array $options config options to generate key {@see openssl_csr_new}
*
* @throws \RuntimeException
*/
protected function generateKey(string $id): Key {
[$publicKey, $privateKey] = $this->generateKeyPair();
protected function generateKey(string $id, array $options = []): Key {
[$publicKey, $privateKey] = $this->generateKeyPair($options);
// Write the private and public key to the disk
try {
$this->appData->newFolder($id);
} catch (\Exception $e) {
} catch (\Exception) {
}
$folder = $this->appData->getFolder($id);
$folder->newFile('private')
@ -125,6 +130,38 @@ class Manager {
return $this->retrieveKey('system-' . $instanceId);
}
public function hasAppKey(string $app, string $name): bool {
$id = $this->generateAppKeyId($app, $name);
try {
$this->appData->getFolder($id);
return true;
} catch (NotFoundException) {
return false;
}
}
public function getAppKey(string $app, string $name): Key {
return $this->retrieveKey($this->generateAppKeyId($app, $name));
}
public function generateAppKey(string $app, string $name, array $options = []): Key {
return $this->generateKey($this->generateAppKeyId($app, $name), $options);
}
public function deleteAppKey(string $app, string $name): bool {
try {
$folder = $this->appData->getFolder($this->generateAppKeyId($app, $name));
} catch (NotFoundException) {
return false;
}
$folder->delete();
return true;
}
private function generateAppKeyId(string $app, string $name): string {
return 'app-' . $app . '-' . $name;
}
private function logOpensslError(): void {
$errors = [];
while ($error = openssl_error_string()) {

@ -1,182 +0,0 @@
<?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];
}
}

@ -1,114 +0,0 @@
<?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;
}
}

@ -112,9 +112,7 @@ class SignatureManager implements ISignatureManager {
$this->prepIncomingSignatureHeader($signedRequest);
$this->verifyIncomingSignatureHeader($signedRequest);
$this->prepEstimatedSignature($signedRequest, $options['extraSignatureHeaders'] ?? []);
$this->verifyIncomingRequestSignature(
$signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL
);
$this->verifyIncomingRequestSignature($signedRequest, $signatoryManager, $options['ttlSignatory'] ?? self::SIGNATORY_TTL);
} catch (SignatureException $e) {
$this->logger->warning(
'signature could not be verified', [
@ -724,7 +722,6 @@ class SignatureManager implements ISignatureManager {
case SignatoryType::FORGIVABLE:
$this->deleteSignatory($knownSignatory->getKeyId());
$this->insertSignatory($signatory);
return;
case SignatoryType::REFRESHABLE:
@ -735,12 +732,10 @@ class SignatureManager implements ISignatureManager {
case SignatoryType::TRUSTED:
// TODO: send notice to admin
throw new SignatoryConflictException();
break;
case SignatoryType::STATIC:
// TODO: send warning to admin
throw new SignatoryConflictException();
break;
}
}

@ -103,7 +103,6 @@ use OC\Security\CSRF\CsrfTokenManager;
use OC\Security\CSRF\TokenStorage\SessionStorage;
use OC\Security\Hasher;
use OC\Security\Ip\RemoteAddress;
use OC\Security\PublicPrivateKeyPairs\KeyPairManager;
use OC\Security\RateLimiting\Limiter;
use OC\Security\SecureRandom;
use OC\Security\Signature\SignatureManager;
@ -1290,7 +1289,6 @@ class Server extends ServerContainer implements IServerContainer {
$this->registerAlias(IRichTextFormatter::class, \OC\RichObjectStrings\RichTextFormatter::class);
$this->registerAlias(IKeyPairManager::class, KeyPairManager::class);
$this->registerAlias(ISignatureManager::class, SignatureManager::class);
$this->connectDispatcher();

@ -154,11 +154,11 @@ interface IOCMProvider extends JsonSerializable {
* apiVersion: '1.0-proposal1',
* endPoint: string,
* publicKey: ISignatory|null,
* resourceTypes: array{
* resourceTypes: list<array{
* name: string,
* shareTypes: list<string>,
* protocols: array<string, string>
* }[],
* }>,
* version: string
* }
* @since 28.0.0

@ -1,18 +0,0 @@
<?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 {
}

@ -1,20 +0,0 @@
<?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 {
}

@ -1,16 +0,0 @@
<?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 {
}

@ -1,80 +0,0 @@
<?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;
}

@ -1,85 +0,0 @@
<?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;
}
Loading…
Cancel
Save