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) {