Skip to content

Commit 0033339

Browse files
committed
feat(http-sig): occ commands to manage Ed25519 keys
ocm:keys:list list known keys with their slot and kid ocm:keys:stage generate a pending key, advertise via JWKS ocm:keys:activate promote pending -> active, demote previous active ocm:keys:retire delete the retiring key (kid stops resolving) Plus the autoloader regen covering the new classes from this branch. Signed-off-by: Micke Nordin <kano@sunet.se>
1 parent ce47c96 commit 0033339

7 files changed

Lines changed: 212 additions & 0 deletions

File tree

core/Command/OCM/ActivateKey.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OC\Core\Command\OCM;
10+
11+
use OC\Core\Command\Base;
12+
use OC\OCM\OCMSignatoryManager;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
class ActivateKey extends Base {
17+
public function __construct(
18+
private readonly OCMSignatoryManager $signatoryManager,
19+
) {
20+
parent::__construct();
21+
}
22+
23+
#[\Override]
24+
protected function configure(): void {
25+
$this
26+
->setName('ocm:keys:activate')
27+
->setDescription('promote the staged Ed25519 key to active; the previous active key moves to retiring');
28+
}
29+
30+
#[\Override]
31+
protected function execute(InputInterface $input, OutputInterface $output): int {
32+
try {
33+
$this->signatoryManager->activateStagedEd25519Key();
34+
} catch (\RuntimeException $e) {
35+
$output->writeln('<error>' . $e->getMessage() . '</error>');
36+
return 1;
37+
}
38+
$output->writeln('<info>Staged key promoted to active.</info>');
39+
$output->writeln('Run <info>occ ocm:keys:retire</info> once any in-flight signatures using the previous key have been verified.');
40+
return 0;
41+
}
42+
}

core/Command/OCM/ListKeys.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OC\Core\Command\OCM;
10+
11+
use OC\Core\Command\Base;
12+
use OC\OCM\OCMSignatoryManager;
13+
use Symfony\Component\Console\Helper\Table;
14+
use Symfony\Component\Console\Input\InputInterface;
15+
use Symfony\Component\Console\Output\OutputInterface;
16+
17+
class ListKeys extends Base {
18+
public function __construct(
19+
private readonly OCMSignatoryManager $signatoryManager,
20+
) {
21+
parent::__construct();
22+
}
23+
24+
#[\Override]
25+
protected function configure(): void {
26+
$this
27+
->setName('ocm:keys:list')
28+
->setDescription('list Ed25519 keys used by OCM RFC 9421 HTTP Message Signatures');
29+
parent::configure();
30+
}
31+
32+
#[\Override]
33+
protected function execute(InputInterface $input, OutputInterface $output): int {
34+
$keys = $this->signatoryManager->listEd25519Keys();
35+
$format = $input->getOption('output');
36+
if ($format === self::OUTPUT_FORMAT_JSON || $format === self::OUTPUT_FORMAT_JSON_PRETTY) {
37+
$output->writeln(json_encode($keys, $format === self::OUTPUT_FORMAT_JSON_PRETTY ? JSON_PRETTY_PRINT : 0));
38+
return 0;
39+
}
40+
41+
if ($keys === []) {
42+
$output->writeln('<comment>No Ed25519 keys yet; one will be generated on first OCM request.</comment>');
43+
return 0;
44+
}
45+
46+
$table = new Table($output);
47+
$table->setHeaders(['Pool', 'Slot', 'Key ID']);
48+
foreach ($keys as $key) {
49+
$table->addRow([$key['poolId'], $key['slot'] ?? '-', $key['kid']]);
50+
}
51+
$table->render();
52+
return 0;
53+
}
54+
}

core/Command/OCM/RetireKey.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OC\Core\Command\OCM;
10+
11+
use OC\Core\Command\Base;
12+
use OC\OCM\OCMSignatoryManager;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
class RetireKey extends Base {
17+
public function __construct(
18+
private readonly OCMSignatoryManager $signatoryManager,
19+
) {
20+
parent::__construct();
21+
}
22+
23+
#[\Override]
24+
protected function configure(): void {
25+
$this
26+
->setName('ocm:keys:retire')
27+
->setDescription('delete the retiring Ed25519 key; signatures that referenced its kid can no longer be verified');
28+
}
29+
30+
#[\Override]
31+
protected function execute(InputInterface $input, OutputInterface $output): int {
32+
try {
33+
$this->signatoryManager->retireEd25519Key();
34+
} catch (\RuntimeException $e) {
35+
$output->writeln('<error>' . $e->getMessage() . '</error>');
36+
return 1;
37+
}
38+
$output->writeln('<info>Retiring key deleted.</info>');
39+
return 0;
40+
}
41+
}

core/Command/OCM/StageKey.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
namespace OC\Core\Command\OCM;
10+
11+
use OC\Core\Command\Base;
12+
use OC\OCM\OCMSignatoryManager;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
16+
class StageKey extends Base {
17+
public function __construct(
18+
private readonly OCMSignatoryManager $signatoryManager,
19+
) {
20+
parent::__construct();
21+
}
22+
23+
#[\Override]
24+
protected function configure(): void {
25+
$this
26+
->setName('ocm:keys:stage')
27+
->setDescription('generate a new Ed25519 key and advertise it via JWKS without using it for signing yet');
28+
}
29+
30+
#[\Override]
31+
protected function execute(InputInterface $input, OutputInterface $output): int {
32+
try {
33+
$signatory = $this->signatoryManager->stageEd25519Key();
34+
} catch (\RuntimeException $e) {
35+
$output->writeln('<error>' . $e->getMessage() . '</error>');
36+
return 1;
37+
}
38+
$output->writeln('Staged new Ed25519 key: <info>' . $signatory->getKeyId() . '</info>');
39+
$output->writeln('Wait for federated peers to refresh their JWKS cache before activating.');
40+
return 0;
41+
}
42+
}

core/register_command.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@
7474
use OC\Core\Command\Memcache\DistributedGet;
7575
use OC\Core\Command\Memcache\DistributedSet;
7676
use OC\Core\Command\Memcache\RedisCommand;
77+
use OC\Core\Command\OCM\ActivateKey as OCMActivateKey;
78+
use OC\Core\Command\OCM\ListKeys as OCMListKeys;
79+
use OC\Core\Command\OCM\RetireKey as OCMRetireKey;
80+
use OC\Core\Command\OCM\StageKey as OCMStageKey;
7781
use OC\Core\Command\Preview\Generate;
7882
use OC\Core\Command\Preview\ResetRenderedTexts;
7983
use OC\Core\Command\Router\ListRoutes;
@@ -251,6 +255,11 @@
251255
$application->add(Server::get(SnowflakeDecodeId::class));
252256
$application->add(Server::get(Get::class));
253257

258+
$application->add(Server::get(OCMListKeys::class));
259+
$application->add(Server::get(OCMStageKey::class));
260+
$application->add(Server::get(OCMActivateKey::class));
261+
$application->add(Server::get(OCMRetireKey::class));
262+
254263
$application->add(Server::get(GetCommand::class));
255264
$application->add(Server::get(EnabledCommand::class));
256265
$application->add(Server::get(Command\TaskProcessing\ListCommand::class));

lib/composer/composer/autoload_classmap.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,10 @@
14031403
'OC\\Core\\Command\\Memcache\\DistributedGet' => $baseDir . '/core/Command/Memcache/DistributedGet.php',
14041404
'OC\\Core\\Command\\Memcache\\DistributedSet' => $baseDir . '/core/Command/Memcache/DistributedSet.php',
14051405
'OC\\Core\\Command\\Memcache\\RedisCommand' => $baseDir . '/core/Command/Memcache/RedisCommand.php',
1406+
'OC\\Core\\Command\\OCM\\ActivateKey' => $baseDir . '/core/Command/OCM/ActivateKey.php',
1407+
'OC\\Core\\Command\\OCM\\ListKeys' => $baseDir . '/core/Command/OCM/ListKeys.php',
1408+
'OC\\Core\\Command\\OCM\\RetireKey' => $baseDir . '/core/Command/OCM/RetireKey.php',
1409+
'OC\\Core\\Command\\OCM\\StageKey' => $baseDir . '/core/Command/OCM/StageKey.php',
14061410
'OC\\Core\\Command\\Preview\\Cleanup' => $baseDir . '/core/Command/Preview/Cleanup.php',
14071411
'OC\\Core\\Command\\Preview\\Generate' => $baseDir . '/core/Command/Preview/Generate.php',
14081412
'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => $baseDir . '/core/Command/Preview/ResetRenderedTexts.php',
@@ -1948,7 +1952,9 @@
19481952
'OC\\OCM\\Model\\OCMResource' => $baseDir . '/lib/private/OCM/Model/OCMResource.php',
19491953
'OC\\OCM\\OCMDiscoveryHandler' => $baseDir . '/lib/private/OCM/OCMDiscoveryHandler.php',
19501954
'OC\\OCM\\OCMDiscoveryService' => $baseDir . '/lib/private/OCM/OCMDiscoveryService.php',
1955+
'OC\\OCM\\OCMJwksHandler' => $baseDir . '/lib/private/OCM/OCMJwksHandler.php',
19511956
'OC\\OCM\\OCMSignatoryManager' => $baseDir . '/lib/private/OCM/OCMSignatoryManager.php',
1957+
'OC\\OCM\\Rfc9421SignatoryManager' => $baseDir . '/lib/private/OCM/Rfc9421SignatoryManager.php',
19521958
'OC\\OCS\\ApiHelper' => $baseDir . '/lib/private/OCS/ApiHelper.php',
19531959
'OC\\OCS\\CoreCapabilities' => $baseDir . '/lib/private/OCS/CoreCapabilities.php',
19541960
'OC\\OCS\\DiscoveryService' => $baseDir . '/lib/private/OCS/DiscoveryService.php',
@@ -2152,7 +2158,13 @@
21522158
'OC\\Security\\Signature\\Db\\SignatoryMapper' => $baseDir . '/lib/private/Security/Signature/Db/SignatoryMapper.php',
21532159
'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php',
21542160
'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php',
2161+
'OC\\Security\\Signature\\Model\\Rfc9421IncomingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/Rfc9421IncomingSignedRequest.php',
2162+
'OC\\Security\\Signature\\Model\\Rfc9421OutgoingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/Rfc9421OutgoingSignedRequest.php',
21552163
'OC\\Security\\Signature\\Model\\SignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/SignedRequest.php',
2164+
'OC\\Security\\Signature\\Rfc9421\\Algorithm' => $baseDir . '/lib/private/Security/Signature/Rfc9421/Algorithm.php',
2165+
'OC\\Security\\Signature\\Rfc9421\\ContentDigest' => $baseDir . '/lib/private/Security/Signature/Rfc9421/ContentDigest.php',
2166+
'OC\\Security\\Signature\\Rfc9421\\IJwkResolvingSignatoryManager' => $baseDir . '/lib/private/Security/Signature/Rfc9421/IJwkResolvingSignatoryManager.php',
2167+
'OC\\Security\\Signature\\Rfc9421\\SignatureBase' => $baseDir . '/lib/private/Security/Signature/Rfc9421/SignatureBase.php',
21562168
'OC\\Security\\Signature\\SignatureManager' => $baseDir . '/lib/private/Security/Signature/SignatureManager.php',
21572169
'OC\\Security\\TrustedDomainHelper' => $baseDir . '/lib/private/Security/TrustedDomainHelper.php',
21582170
'OC\\Security\\VerificationToken\\CleanUpJob' => $baseDir . '/lib/private/Security/VerificationToken/CleanUpJob.php',

lib/composer/composer/autoload_static.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,6 +1444,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
14441444
'OC\\Core\\Command\\Memcache\\DistributedGet' => __DIR__ . '/../../..' . '/core/Command/Memcache/DistributedGet.php',
14451445
'OC\\Core\\Command\\Memcache\\DistributedSet' => __DIR__ . '/../../..' . '/core/Command/Memcache/DistributedSet.php',
14461446
'OC\\Core\\Command\\Memcache\\RedisCommand' => __DIR__ . '/../../..' . '/core/Command/Memcache/RedisCommand.php',
1447+
'OC\\Core\\Command\\OCM\\ActivateKey' => __DIR__ . '/../../..' . '/core/Command/OCM/ActivateKey.php',
1448+
'OC\\Core\\Command\\OCM\\ListKeys' => __DIR__ . '/../../..' . '/core/Command/OCM/ListKeys.php',
1449+
'OC\\Core\\Command\\OCM\\RetireKey' => __DIR__ . '/../../..' . '/core/Command/OCM/RetireKey.php',
1450+
'OC\\Core\\Command\\OCM\\StageKey' => __DIR__ . '/../../..' . '/core/Command/OCM/StageKey.php',
14471451
'OC\\Core\\Command\\Preview\\Cleanup' => __DIR__ . '/../../..' . '/core/Command/Preview/Cleanup.php',
14481452
'OC\\Core\\Command\\Preview\\Generate' => __DIR__ . '/../../..' . '/core/Command/Preview/Generate.php',
14491453
'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => __DIR__ . '/../../..' . '/core/Command/Preview/ResetRenderedTexts.php',
@@ -1989,7 +1993,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
19891993
'OC\\OCM\\Model\\OCMResource' => __DIR__ . '/../../..' . '/lib/private/OCM/Model/OCMResource.php',
19901994
'OC\\OCM\\OCMDiscoveryHandler' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMDiscoveryHandler.php',
19911995
'OC\\OCM\\OCMDiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMDiscoveryService.php',
1996+
'OC\\OCM\\OCMJwksHandler' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMJwksHandler.php',
19921997
'OC\\OCM\\OCMSignatoryManager' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMSignatoryManager.php',
1998+
'OC\\OCM\\Rfc9421SignatoryManager' => __DIR__ . '/../../..' . '/lib/private/OCM/Rfc9421SignatoryManager.php',
19931999
'OC\\OCS\\ApiHelper' => __DIR__ . '/../../..' . '/lib/private/OCS/ApiHelper.php',
19942000
'OC\\OCS\\CoreCapabilities' => __DIR__ . '/../../..' . '/lib/private/OCS/CoreCapabilities.php',
19952001
'OC\\OCS\\DiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCS/DiscoveryService.php',
@@ -2193,7 +2199,13 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
21932199
'OC\\Security\\Signature\\Db\\SignatoryMapper' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Db/SignatoryMapper.php',
21942200
'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php',
21952201
'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php',
2202+
'OC\\Security\\Signature\\Model\\Rfc9421IncomingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/Rfc9421IncomingSignedRequest.php',
2203+
'OC\\Security\\Signature\\Model\\Rfc9421OutgoingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/Rfc9421OutgoingSignedRequest.php',
21962204
'OC\\Security\\Signature\\Model\\SignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/SignedRequest.php',
2205+
'OC\\Security\\Signature\\Rfc9421\\Algorithm' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Rfc9421/Algorithm.php',
2206+
'OC\\Security\\Signature\\Rfc9421\\ContentDigest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Rfc9421/ContentDigest.php',
2207+
'OC\\Security\\Signature\\Rfc9421\\IJwkResolvingSignatoryManager' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Rfc9421/IJwkResolvingSignatoryManager.php',
2208+
'OC\\Security\\Signature\\Rfc9421\\SignatureBase' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Rfc9421/SignatureBase.php',
21972209
'OC\\Security\\Signature\\SignatureManager' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/SignatureManager.php',
21982210
'OC\\Security\\TrustedDomainHelper' => __DIR__ . '/../../..' . '/lib/private/Security/TrustedDomainHelper.php',
21992211
'OC\\Security\\VerificationToken\\CleanUpJob' => __DIR__ . '/../../..' . '/lib/private/Security/VerificationToken/CleanUpJob.php',

0 commit comments

Comments
 (0)