Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
<command>OCA\Talk\Command\Signaling\Add</command>
<command>OCA\Talk\Command\Signaling\Delete</command>
<command>OCA\Talk\Command\Signaling\ListCommand</command>
<command>OCA\Talk\Command\Signaling\VerifyKeys</command>

<command>OCA\Talk\Command\Stun\Add</command>
<command>OCA\Talk\Command\Stun\Delete</command>
Expand Down
13 changes: 13 additions & 0 deletions docs/occ.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
67 changes: 67 additions & 0 deletions lib/Command/Signaling/VerifyKeys.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk\Command\Signaling;

use OC\Core\Command\Base;
use OCA\Talk\Config;
use OCP\IConfig;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class VerifyKeys extends Base {

public function __construct(
private IConfig $config,
private Config $talkConfig,
) {
parent::__construct();
}

#[\Override]
protected function configure(): void {
parent::configure();

$this
->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('<comment>Stored public key for algorithm ' . strtolower($alg) . ' did not match stored private key.</comment>');
$output->writeln('<info>A new public key was created and stored.</info>');
$this->config->setAppValue('spreed', 'signaling_token_pubkey_' . strtolower($alg), $publicKeyDerived);

return 0;
}

$output->writeln('<error>Stored public key for algorithm ' . strtolower($alg) . ' does not match stored private key</error>');
return 1;
}

$output->writeln('<info>Stored public key for algorithm ' . strtolower($alg) . ' matches stored private key</info>');

return 0;
}
}
32 changes: 32 additions & 0 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions lib/SetupCheck/HighPerformanceBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading