diff --git a/appinfo/info.xml b/appinfo/info.xml index ac3c7f90e1d..fc223e0a283 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -119,6 +119,7 @@ OCA\Talk\Command\Signaling\Add OCA\Talk\Command\Signaling\Delete OCA\Talk\Command\Signaling\ListCommand + OCA\Talk\Command\Signaling\VerifyKeys OCA\Talk\Command\Stun\Add OCA\Talk\Command\Stun\Delete diff --git a/docs/occ.md b/docs/occ.md index ffd634f9e81..d71ce3c7816 100644 --- a/docs/occ.md +++ b/docs/occ.md @@ -386,6 +386,19 @@ List external signaling servers. |---|---|---|---|---|---| | `--output` | Output format (plain, json or json_pretty, default is plain) | yes | no | no | `'plain'` | +## talk:signaling:verify-keys + +Verify if the stored public key matches the stored private key for the signaling server + +### Usage + +* `talk:signaling:verify-keys [--output [OUTPUT]] [--update]` + +| Options | Description | Accept value | Is value required | Is multiple | Default | +|---|---|---|---|---|---| +| `--output` | Output format (plain, json or json_pretty, default is plain) | yes | no | no | `'plain'` | +| `--update` | Updates the stored public key to match the private key if there is a mis-match | no | no | no | `false` | + ## talk:stun:add Add a new STUN server. diff --git a/lib/Command/Signaling/VerifyKeys.php b/lib/Command/Signaling/VerifyKeys.php new file mode 100644 index 00000000000..a2088872576 --- /dev/null +++ b/lib/Command/Signaling/VerifyKeys.php @@ -0,0 +1,67 @@ +setName('talk:signaling:verify-keys') + ->setDescription('Verify if the stored public key matches the stored private key for the signaling server') + ->addOption('update', null, InputOption::VALUE_NONE, 'Updates the stored public key to match the private key if there is a mis-match'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $update = $input->getOption('update'); + + $alg = $this->talkConfig->getSignalingTokenAlgorithm(); + $privateKey = $this->talkConfig->getSignalingTokenPrivateKey(); + $publicKey = $this->talkConfig->getSignalingTokenPublicKey(); + $publicKeyDerived = $this->talkConfig->deriveSignalingTokenPublicKey($privateKey, $alg); + + $output->writeln('Stored public key:'); + $output->writeln($publicKey); + $output->writeln('Derived public key:'); + $output->writeln($publicKeyDerived); + + if ($publicKey != $publicKeyDerived) { + if ($update) { + $output->writeln('Stored public key for algorithm ' . strtolower($alg) . ' did not match stored private key.'); + $output->writeln('A new public key was created and stored.'); + $this->config->setAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg), $publicKeyDerived); + + return 0; + } + + $output->writeln('Stored public key for algorithm ' . strtolower($alg) . ' does not match stored private key'); + return 1; + } + + $output->writeln('Stored public key for algorithm ' . strtolower($alg) . ' matches stored private key'); + + return 0; + } +} diff --git a/lib/Config.php b/lib/Config.php index 25027bf4e18..75a9f1493e5 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -578,6 +578,38 @@ public function getSignalingTokenPublicKey(?string $alg = null): string { return $this->config->getAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg)); } + public function deriveSignalingTokenPublicKey(string $privateKey, string $alg): string { + if (str_starts_with($alg, 'ES') || str_starts_with($alg, 'RS')) { + $opensslPrivateKey = openssl_pkey_get_private($privateKey); + $this->throwOnOpensslError(); + + $pubKey = openssl_pkey_get_details($opensslPrivateKey); + $this->throwOnOpensslError(); + + $public = $pubKey['key']; + if (!openssl_pkey_export($privateKey, $secret)) { + throw new \Exception('Could not export private key'); + } + } elseif ($alg === 'EdDSA') { + $public = base64_encode(sodium_crypto_sign_publickey_from_secretkey($privateKey)); + } else { + throw new \Exception('Unsupported algorithm ' . $alg); + } + + return $public; + } + + private function throwOnOpensslError() { + $errors = []; + while ($error = openssl_error_string()) { + $errors[] = $error; + } + + if (!empty($errors)) { + throw new \Exception("OpenSSL error:\n" . implode("\n", $errors)); + } + } + /** * @param IUser $user * @return array diff --git a/lib/SetupCheck/HighPerformanceBackend.php b/lib/SetupCheck/HighPerformanceBackend.php index 3a85f0c75a8..ac8c3eb242b 100644 --- a/lib/SetupCheck/HighPerformanceBackend.php +++ b/lib/SetupCheck/HighPerformanceBackend.php @@ -71,6 +71,20 @@ public function run(): SetupResult { ); } + // Verify stored signaling key pair + try { + $alg = $this->talkConfig->getSignalingTokenAlgorithm(); + $privateKey = $this->talkConfig->getSignalingTokenPrivateKey(); + $publicKey = $this->talkConfig->getSignalingTokenPublicKey(); + $publicKeyDerived = $this->talkConfig->deriveSignalingTokenPublicKey($privateKey, $alg); + + if ($publicKey != $publicKeyDerived) { + return SetupResult::error($this->l->t('The stored public key for used algorithm %1$s does not match the stored private key. Run %2$s to fix the issue.', [$alg, '`occ talk:signaling:verify-keys --update`'])); + } + } catch (\Exception) { + return SetupResult::error($this->l->t('High-performance backend not configured correctly. Run %s for details.', ['`occ talk:signaling:verify-keys`'])); + } + try { $testResult = $this->signalManager->checkServerCompatibility(0); } catch (\OutOfBoundsException) {