From 39715df9b0fd0128a71f7b1fd21dfc186fe40857 Mon Sep 17 00:00:00 2001 From: Joachim Bauch Date: Thu, 7 Jul 2022 14:53:41 +0200 Subject: [PATCH] Add support for algorithm "EdDSA" and add tests. Signed-off-by: Joachim Bauch --- lib/Config.php | 22 ++++-- tests/php/CapabilitiesTest.php | 17 +++++ tests/php/ConfigTest.php | 126 +++++++++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 7 deletions(-) diff --git a/lib/Config.php b/lib/Config.php index c691fdb20a6..b3dda8d8530 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -404,25 +404,33 @@ private function ensureSignalingTokenKeys(string $alg): void { return; } - if (substr($alg, 0, 2) === 'EC') { + if (substr($alg, 0, 2) === 'ES') { $privKey = openssl_pkey_new([ 'curve_name' => 'prime256v1', 'private_key_type' => OPENSSL_KEYTYPE_EC, ]); + $pubKey = openssl_pkey_get_details($privKey); + $public = $pubKey['key']; + if (!openssl_pkey_export($privKey, $secret)) { + throw new \Exception('Could not export private key'); + } } elseif (substr($alg, 0, 2) === 'RS') { $privKey = openssl_pkey_new([ 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA, ]); + $pubKey = openssl_pkey_get_details($privKey); + $public = $pubKey['key']; + if (!openssl_pkey_export($privKey, $secret)) { + throw new \Exception('Could not export private key'); + } + } elseif ($alg === 'EdDSA') { + $privKey = sodium_crypto_sign_keypair(); + $public = base64_encode(sodium_crypto_sign_publickey($privKey)); + $secret = base64_encode(sodium_crypto_sign_secretkey($privKey)); } else { throw new \Exception('Unsupported algorithm ' . $alg); } - $pubKey = openssl_pkey_get_details($privKey); - $public = $pubKey['key']; - - if (!openssl_pkey_export($privKey, $secret)) { - throw new \Exception('Could not export private key'); - } $this->config->setAppValue('spreed', 'signaling_token_privkey_' . strtolower($alg), $secret); $this->config->setAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg), $public); diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php index 65fbe67d27a..9445a48e9a5 100644 --- a/tests/php/CapabilitiesTest.php +++ b/tests/php/CapabilitiesTest.php @@ -315,4 +315,21 @@ public function testGetCapabilitiesUserDisallowed(): void { $this->assertInstanceOf(IPublicCapability::class, $capabilities); $this->assertSame([], $capabilities->getCapabilities()); } + + public function testCapabilitiesHelloV2Key(): void { + $capabilities = new Capabilities( + $this->serverConfig, + $this->talkConfig, + $this->commentsManager, + $this->userSession, + $this->appManager + ); + + $this->talkConfig->expects($this->once()) + ->method('getSignalingTokenPublicKey') + ->willReturn('this-is-the-key'); + + $data = $capabilities->getCapabilities(); + $this->assertEquals('this-is-the-key', $data['spreed']['config']['signaling']['hello-v2-token-key']); + } } diff --git a/tests/php/ConfigTest.php b/tests/php/ConfigTest.php index c98d1c7ae12..569d8917753 100644 --- a/tests/php/ConfigTest.php +++ b/tests/php/ConfigTest.php @@ -20,6 +20,9 @@ */ namespace OCA\Talk\Tests\php; +use Firebase\JWT\JWT; +use Firebase\JWT\Key; + use OCA\Talk\Config; use OCA\Talk\Events\GetTurnServersEvent; use OCA\Talk\Tests\php\Mocks\GetTurnServerListener; @@ -28,6 +31,7 @@ use OCP\IConfig; use OCP\IGroupManager; use OCP\IURLGenerator; +use OCP\IUser; use OCP\IUserManager; use OCP\Security\ISecureRandom; use PHPUnit\Framework\MockObject\MockObject; @@ -330,4 +334,126 @@ public function testGetWebSocketDomainForSignalingServer($url, $expectedWebSocke self::invokePrivate($helper, 'getWebSocketDomainForSignalingServer', [$url]) ); } + + public function dataTicketV2Algorithm() { + return [ + ['ES384'], + ['ES256'], + ['RS256'], + ['RS384'], + ['RS512'], + ['EdDSA'], + ]; + } + + /** + * @dataProvider dataTicketV2Algorithm + * @param string $algo + */ + public function testSignalingTicketV2User(string $algo): void { + /** @var IConfig $config */ + $config = \OC::$server->getConfig(); + /** @var MockObject|ITimeFactory $timeFactory */ + $timeFactory = $this->createMock(ITimeFactory::class); + /** @var MockObject|ISecureRandom $secureRandom */ + $secureRandom = $this->createMock(ISecureRandom::class); + /** @var MockObject|IGroupManager $groupManager */ + $groupManager = $this->createMock(IGroupManager::class); + /** @var MockObject|IUserManager $userManager */ + $userManager = $this->createMock(IUserManager::class); + /** @var MockObject|IURLGenerator $urlGenerator */ + $urlGenerator = $this->createMock(IURLGenerator::class); + /** @var MockObject|IEventDispatcher $dispatcher */ + $dispatcher = $this->createMock(IEventDispatcher::class); + /** @var MockObject|IUser $user */ + $user = $this->createMock(IUser::class); + + $now = time(); + $timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn($now); + $urlGenerator + ->expects($this->once()) + ->method('getAbsoluteURL') + ->with('') + ->willReturn('https://domain.invalid/nextcloud'); + $userManager + ->expects($this->once()) + ->method('get') + ->with('user1') + ->willReturn($user); + $user + ->expects($this->once()) + ->method('getUID') + ->willReturn('user1'); + $user + ->expects($this->once()) + ->method('getDisplayName') + ->willReturn('Jane Doe'); + + $helper = new Config($config, $secureRandom, $groupManager, $userManager, $urlGenerator, $timeFactory, $dispatcher); + + $config->setAppValue('spreed', 'signaling_token_alg', $algo); + // Make sure new keys are generated. + $config->deleteAppValue('spreed', 'signaling_token_privkey_' . strtolower($algo)); + $config->deleteAppValue('spreed', 'signaling_token_pubkey_' . strtolower($algo)); + $ticket = $helper->getSignalingTicket(Config::SIGNALING_TICKET_V2, 'user1'); + $this->assertNotNull($ticket); + + $key = new Key($config->getAppValue('spreed', 'signaling_token_pubkey_' . strtolower($algo)), $algo); + $decoded = JWT::decode($ticket, $key); + + $this->assertEquals($now, $decoded->iat); + $this->assertEquals('https://domain.invalid/nextcloud', $decoded->iss); + $this->assertEquals('user1', $decoded->sub); + $this->assertSame(['displayname' => 'Jane Doe'], (array) $decoded->userdata); + } + + /** + * @dataProvider dataTicketV2Algorithm + * @param string $algo + */ + public function testSignalingTicketV2Anonymous(string $algo): void { + /** @var IConfig $config */ + $config = \OC::$server->getConfig(); + /** @var MockObject|ITimeFactory $timeFactory */ + $timeFactory = $this->createMock(ITimeFactory::class); + /** @var MockObject|ISecureRandom $secureRandom */ + $secureRandom = $this->createMock(ISecureRandom::class); + /** @var MockObject|IGroupManager $groupManager */ + $groupManager = $this->createMock(IGroupManager::class); + /** @var MockObject|IUserManager $userManager */ + $userManager = $this->createMock(IUserManager::class); + /** @var MockObject|IURLGenerator $urlGenerator */ + $urlGenerator = $this->createMock(IURLGenerator::class); + /** @var MockObject|IEventDispatcher $dispatcher */ + $dispatcher = $this->createMock(IEventDispatcher::class); + + $now = time(); + $timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn($now); + $urlGenerator + ->expects($this->once()) + ->method('getAbsoluteURL') + ->with('') + ->willReturn('https://domain.invalid/nextcloud'); + + $helper = new Config($config, $secureRandom, $groupManager, $userManager, $urlGenerator, $timeFactory, $dispatcher); + + $config->setAppValue('spreed', 'signaling_token_alg', $algo); + // Make sure new keys are generated. + $config->deleteAppValue('spreed', 'signaling_token_privkey_' . strtolower($algo)); + $config->deleteAppValue('spreed', 'signaling_token_pubkey_' . strtolower($algo)); + $ticket = $helper->getSignalingTicket(Config::SIGNALING_TICKET_V2, null); + $this->assertNotNull($ticket); + + $key = new Key($config->getAppValue('spreed', 'signaling_token_pubkey_' . strtolower($algo)), $algo); + $decoded = JWT::decode($ticket, $key); + + $this->assertEquals($now, $decoded->iat); + $this->assertEquals('https://domain.invalid/nextcloud', $decoded->iss); + } }