diff --git a/core/Command/OCM/ActivateKey.php b/core/Command/OCM/ActivateKey.php
new file mode 100644
index 00000000000..090538e0024
--- /dev/null
+++ b/core/Command/OCM/ActivateKey.php
@@ -0,0 +1,42 @@
+setName('ocm:keys:activate')
+ ->setDescription('promote the staged Ed25519 key to active; the previous active key moves to retiring');
+ }
+
+ #[\Override]
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ try {
+ $this->signatoryManager->activateStagedEd25519Key();
+ } catch (\RuntimeException $e) {
+ $output->writeln('' . $e->getMessage() . '');
+ return 1;
+ }
+ $output->writeln('Staged key promoted to active.');
+ $output->writeln('Run occ ocm:keys:retire once any in-flight signatures using the previous key have been verified.');
+ return 0;
+ }
+}
diff --git a/core/Command/OCM/ListKeys.php b/core/Command/OCM/ListKeys.php
new file mode 100644
index 00000000000..f73a4763111
--- /dev/null
+++ b/core/Command/OCM/ListKeys.php
@@ -0,0 +1,54 @@
+setName('ocm:keys:list')
+ ->setDescription('list Ed25519 keys used by OCM RFC 9421 HTTP Message Signatures');
+ parent::configure();
+ }
+
+ #[\Override]
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $keys = $this->signatoryManager->listEd25519Keys();
+ $format = $input->getOption('output');
+ if ($format === self::OUTPUT_FORMAT_JSON || $format === self::OUTPUT_FORMAT_JSON_PRETTY) {
+ $output->writeln(json_encode($keys, $format === self::OUTPUT_FORMAT_JSON_PRETTY ? JSON_PRETTY_PRINT : 0));
+ return 0;
+ }
+
+ if ($keys === []) {
+ $output->writeln('No Ed25519 keys yet; one will be generated on first OCM request.');
+ return 0;
+ }
+
+ $table = new Table($output);
+ $table->setHeaders(['Pool', 'Slot', 'Key ID']);
+ foreach ($keys as $key) {
+ $table->addRow([$key['poolId'], $key['slot'] ?? '-', $key['kid']]);
+ }
+ $table->render();
+ return 0;
+ }
+}
diff --git a/core/Command/OCM/RetireKey.php b/core/Command/OCM/RetireKey.php
new file mode 100644
index 00000000000..58db976077c
--- /dev/null
+++ b/core/Command/OCM/RetireKey.php
@@ -0,0 +1,41 @@
+setName('ocm:keys:retire')
+ ->setDescription('delete the retiring Ed25519 key; signatures that referenced its kid can no longer be verified');
+ }
+
+ #[\Override]
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ try {
+ $this->signatoryManager->retireEd25519Key();
+ } catch (\RuntimeException $e) {
+ $output->writeln('' . $e->getMessage() . '');
+ return 1;
+ }
+ $output->writeln('Retiring key deleted.');
+ return 0;
+ }
+}
diff --git a/core/Command/OCM/StageKey.php b/core/Command/OCM/StageKey.php
new file mode 100644
index 00000000000..75437f460bf
--- /dev/null
+++ b/core/Command/OCM/StageKey.php
@@ -0,0 +1,42 @@
+setName('ocm:keys:stage')
+ ->setDescription('generate a new Ed25519 key and advertise it via JWKS without using it for signing yet');
+ }
+
+ #[\Override]
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ try {
+ $signatory = $this->signatoryManager->stageEd25519Key();
+ } catch (\RuntimeException $e) {
+ $output->writeln('' . $e->getMessage() . '');
+ return 1;
+ }
+ $output->writeln('Staged new Ed25519 key: ' . $signatory->getKeyId() . '');
+ $output->writeln('Wait for federated peers to refresh their JWKS cache before activating.');
+ return 0;
+ }
+}
diff --git a/core/register_command.php b/core/register_command.php
index d28c1633c62..856894b5c4c 100644
--- a/core/register_command.php
+++ b/core/register_command.php
@@ -74,6 +74,10 @@ use OC\Core\Command\Memcache\DistributedDelete;
use OC\Core\Command\Memcache\DistributedGet;
use OC\Core\Command\Memcache\DistributedSet;
use OC\Core\Command\Memcache\RedisCommand;
+use OC\Core\Command\OCM\ActivateKey as OCMActivateKey;
+use OC\Core\Command\OCM\ListKeys as OCMListKeys;
+use OC\Core\Command\OCM\RetireKey as OCMRetireKey;
+use OC\Core\Command\OCM\StageKey as OCMStageKey;
use OC\Core\Command\Preview\Generate;
use OC\Core\Command\Preview\ResetRenderedTexts;
use OC\Core\Command\Router\ListRoutes;
@@ -251,6 +255,11 @@ if ($config->getSystemValueBool('installed', false)) {
$application->add(Server::get(SnowflakeDecodeId::class));
$application->add(Server::get(Get::class));
+ $application->add(Server::get(OCMListKeys::class));
+ $application->add(Server::get(OCMStageKey::class));
+ $application->add(Server::get(OCMActivateKey::class));
+ $application->add(Server::get(OCMRetireKey::class));
+
$application->add(Server::get(GetCommand::class));
$application->add(Server::get(EnabledCommand::class));
$application->add(Server::get(Command\TaskProcessing\ListCommand::class));
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 9c7357ea6aa..54ec6a944df 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1408,6 +1408,10 @@ return array(
'OC\\Core\\Command\\Memcache\\DistributedGet' => $baseDir . '/core/Command/Memcache/DistributedGet.php',
'OC\\Core\\Command\\Memcache\\DistributedSet' => $baseDir . '/core/Command/Memcache/DistributedSet.php',
'OC\\Core\\Command\\Memcache\\RedisCommand' => $baseDir . '/core/Command/Memcache/RedisCommand.php',
+ 'OC\\Core\\Command\\OCM\\ActivateKey' => $baseDir . '/core/Command/OCM/ActivateKey.php',
+ 'OC\\Core\\Command\\OCM\\ListKeys' => $baseDir . '/core/Command/OCM/ListKeys.php',
+ 'OC\\Core\\Command\\OCM\\RetireKey' => $baseDir . '/core/Command/OCM/RetireKey.php',
+ 'OC\\Core\\Command\\OCM\\StageKey' => $baseDir . '/core/Command/OCM/StageKey.php',
'OC\\Core\\Command\\Preview\\Cleanup' => $baseDir . '/core/Command/Preview/Cleanup.php',
'OC\\Core\\Command\\Preview\\Generate' => $baseDir . '/core/Command/Preview/Generate.php',
'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => $baseDir . '/core/Command/Preview/ResetRenderedTexts.php',
@@ -1953,7 +1957,9 @@ return array(
'OC\\OCM\\Model\\OCMResource' => $baseDir . '/lib/private/OCM/Model/OCMResource.php',
'OC\\OCM\\OCMDiscoveryHandler' => $baseDir . '/lib/private/OCM/OCMDiscoveryHandler.php',
'OC\\OCM\\OCMDiscoveryService' => $baseDir . '/lib/private/OCM/OCMDiscoveryService.php',
+ 'OC\\OCM\\OCMJwksHandler' => $baseDir . '/lib/private/OCM/OCMJwksHandler.php',
'OC\\OCM\\OCMSignatoryManager' => $baseDir . '/lib/private/OCM/OCMSignatoryManager.php',
+ 'OC\\OCM\\Rfc9421SignatoryManager' => $baseDir . '/lib/private/OCM/Rfc9421SignatoryManager.php',
'OC\\OCS\\ApiHelper' => $baseDir . '/lib/private/OCS/ApiHelper.php',
'OC\\OCS\\CoreCapabilities' => $baseDir . '/lib/private/OCS/CoreCapabilities.php',
'OC\\OCS\\DiscoveryService' => $baseDir . '/lib/private/OCS/DiscoveryService.php',
@@ -2157,7 +2163,13 @@ return array(
'OC\\Security\\Signature\\Db\\SignatoryMapper' => $baseDir . '/lib/private/Security/Signature/Db/SignatoryMapper.php',
'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php',
'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php',
+ 'OC\\Security\\Signature\\Model\\Rfc9421IncomingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/Rfc9421IncomingSignedRequest.php',
+ 'OC\\Security\\Signature\\Model\\Rfc9421OutgoingSignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/Rfc9421OutgoingSignedRequest.php',
'OC\\Security\\Signature\\Model\\SignedRequest' => $baseDir . '/lib/private/Security/Signature/Model/SignedRequest.php',
+ 'OC\\Security\\Signature\\Rfc9421\\Algorithm' => $baseDir . '/lib/private/Security/Signature/Rfc9421/Algorithm.php',
+ 'OC\\Security\\Signature\\Rfc9421\\ContentDigest' => $baseDir . '/lib/private/Security/Signature/Rfc9421/ContentDigest.php',
+ 'OC\\Security\\Signature\\Rfc9421\\IJwkResolvingSignatoryManager' => $baseDir . '/lib/private/Security/Signature/Rfc9421/IJwkResolvingSignatoryManager.php',
+ 'OC\\Security\\Signature\\Rfc9421\\SignatureBase' => $baseDir . '/lib/private/Security/Signature/Rfc9421/SignatureBase.php',
'OC\\Security\\Signature\\SignatureManager' => $baseDir . '/lib/private/Security/Signature/SignatureManager.php',
'OC\\Security\\TrustedDomainHelper' => $baseDir . '/lib/private/Security/TrustedDomainHelper.php',
'OC\\Security\\VerificationToken\\CleanUpJob' => $baseDir . '/lib/private/Security/VerificationToken/CleanUpJob.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index d25c72171fa..2b90f11fa7d 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1449,6 +1449,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Command\\Memcache\\DistributedGet' => __DIR__ . '/../../..' . '/core/Command/Memcache/DistributedGet.php',
'OC\\Core\\Command\\Memcache\\DistributedSet' => __DIR__ . '/../../..' . '/core/Command/Memcache/DistributedSet.php',
'OC\\Core\\Command\\Memcache\\RedisCommand' => __DIR__ . '/../../..' . '/core/Command/Memcache/RedisCommand.php',
+ 'OC\\Core\\Command\\OCM\\ActivateKey' => __DIR__ . '/../../..' . '/core/Command/OCM/ActivateKey.php',
+ 'OC\\Core\\Command\\OCM\\ListKeys' => __DIR__ . '/../../..' . '/core/Command/OCM/ListKeys.php',
+ 'OC\\Core\\Command\\OCM\\RetireKey' => __DIR__ . '/../../..' . '/core/Command/OCM/RetireKey.php',
+ 'OC\\Core\\Command\\OCM\\StageKey' => __DIR__ . '/../../..' . '/core/Command/OCM/StageKey.php',
'OC\\Core\\Command\\Preview\\Cleanup' => __DIR__ . '/../../..' . '/core/Command/Preview/Cleanup.php',
'OC\\Core\\Command\\Preview\\Generate' => __DIR__ . '/../../..' . '/core/Command/Preview/Generate.php',
'OC\\Core\\Command\\Preview\\ResetRenderedTexts' => __DIR__ . '/../../..' . '/core/Command/Preview/ResetRenderedTexts.php',
@@ -1994,7 +1998,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\OCM\\Model\\OCMResource' => __DIR__ . '/../../..' . '/lib/private/OCM/Model/OCMResource.php',
'OC\\OCM\\OCMDiscoveryHandler' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMDiscoveryHandler.php',
'OC\\OCM\\OCMDiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMDiscoveryService.php',
+ 'OC\\OCM\\OCMJwksHandler' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMJwksHandler.php',
'OC\\OCM\\OCMSignatoryManager' => __DIR__ . '/../../..' . '/lib/private/OCM/OCMSignatoryManager.php',
+ 'OC\\OCM\\Rfc9421SignatoryManager' => __DIR__ . '/../../..' . '/lib/private/OCM/Rfc9421SignatoryManager.php',
'OC\\OCS\\ApiHelper' => __DIR__ . '/../../..' . '/lib/private/OCS/ApiHelper.php',
'OC\\OCS\\CoreCapabilities' => __DIR__ . '/../../..' . '/lib/private/OCS/CoreCapabilities.php',
'OC\\OCS\\DiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCS/DiscoveryService.php',
@@ -2198,7 +2204,13 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Security\\Signature\\Db\\SignatoryMapper' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Db/SignatoryMapper.php',
'OC\\Security\\Signature\\Model\\IncomingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/IncomingSignedRequest.php',
'OC\\Security\\Signature\\Model\\OutgoingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/OutgoingSignedRequest.php',
+ 'OC\\Security\\Signature\\Model\\Rfc9421IncomingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/Rfc9421IncomingSignedRequest.php',
+ 'OC\\Security\\Signature\\Model\\Rfc9421OutgoingSignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/Rfc9421OutgoingSignedRequest.php',
'OC\\Security\\Signature\\Model\\SignedRequest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Model/SignedRequest.php',
+ 'OC\\Security\\Signature\\Rfc9421\\Algorithm' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Rfc9421/Algorithm.php',
+ 'OC\\Security\\Signature\\Rfc9421\\ContentDigest' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Rfc9421/ContentDigest.php',
+ 'OC\\Security\\Signature\\Rfc9421\\IJwkResolvingSignatoryManager' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Rfc9421/IJwkResolvingSignatoryManager.php',
+ 'OC\\Security\\Signature\\Rfc9421\\SignatureBase' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/Rfc9421/SignatureBase.php',
'OC\\Security\\Signature\\SignatureManager' => __DIR__ . '/../../..' . '/lib/private/Security/Signature/SignatureManager.php',
'OC\\Security\\TrustedDomainHelper' => __DIR__ . '/../../..' . '/lib/private/Security/TrustedDomainHelper.php',
'OC\\Security\\VerificationToken\\CleanUpJob' => __DIR__ . '/../../..' . '/lib/private/Security/VerificationToken/CleanUpJob.php',