Skip to content

Commit acc1e0d

Browse files
committed
feat(http-sig): OCM Ed25519 keys, JWKS endpoint, http-sig capability
OCM dual-stack integration of RFC 9421 alongside the existing cavage publicKey path: - OCMSignatoryManager: Ed25519 active/pending/retiring slot rotation backed by numbered pool appkeys, getRemoteKey for inbound JWK lookup with per-origin cache + cache-miss refetch, and getLocalEd25519Jwks for the JWKS endpoint. - Rfc9421SignatoryManager: per-call wrapper that swaps in the Ed25519 signatory and toggles `rfc9421.format`. - OCMJwksHandler: serves /.well-known/jwks.json (RFC 7517) when signing is enabled. - OCMDiscoveryService: advertises `http-sig` in capabilities when signing is enabled, and picks the signature scheme on outbound based on the remote's advertised capabilities. - Application.php: register the JWKS well-known handler. Signed-off-by: Micke Nordin <kano@sunet.se>
1 parent 29394c2 commit acc1e0d

10 files changed

Lines changed: 1185 additions & 30 deletions

core/AppInfo/Application.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use OC\Core\Listener\PasswordUpdatedListener;
2424
use OC\Core\Notification\CoreNotifier;
2525
use OC\OCM\OCMDiscoveryHandler;
26+
use OC\OCM\OCMJwksHandler;
2627
use OC\TagManager;
2728
use OCP\AppFramework\App;
2829
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -88,6 +89,7 @@ public function register(IRegistrationContext $context): void {
8889
$context->registerConfigLexicon(ConfigLexicon::class);
8990

9091
$context->registerWellKnownHandler(OCMDiscoveryHandler::class);
92+
$context->registerWellKnownHandler(OCMJwksHandler::class);
9193
$context->registerCapability(Capabilities::class);
9294
}
9395

lib/private/OCM/OCMDiscoveryService.php

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,15 @@ public function getLocalOCMProvider(bool $fullDetails = true): IOCMProvider {
199199
return $provider;
200200
}
201201

202+
$signingEnabled = !$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true);
203+
202204
$provider->setEnabled(true);
203205
$provider->setApiVersion(self::API_VERSION);
204206
$provider->setEndPoint(substr($url, 0, $pos));
205207
$provider->setCapabilities(['invite-accepted', 'notifications', 'shares']);
208+
if ($signingEnabled) {
209+
$provider->setCapabilities(['http-sig']);
210+
}
206211

207212
// The inviteAcceptDialog is available from the contacts app, if this config value is set
208213
$inviteAcceptDialog = $this->appConfig->getValueString('core', ConfigLexicon::OCM_INVITE_ACCEPT_DIALOG);
@@ -217,9 +222,8 @@ public function getLocalOCMProvider(bool $fullDetails = true): IOCMProvider {
217222
$provider->addResourceType($resource);
218223

219224
if ($fullDetails) {
220-
// Adding a public key to the ocm discovery
221225
try {
222-
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
226+
if ($signingEnabled) {
223227
/**
224228
* @experimental 31.0.0
225229
* @psalm-suppress UndefinedInterfaceMethod
@@ -342,31 +346,43 @@ public function requestRemoteOcmEndpoint(
342346
}
343347

344348
/**
345-
* add entries to the payload to auth the whole request
349+
* Sign the outgoing payload using the scheme the remote advertises
350+
* (RFC 9421 if `http-sig`, else cavage if a `publicKey` is present).
351+
* APPCONFIG_SIGN_ENFORCED / APPCONFIG_SIGN_DISABLED still apply.
346352
*
347353
* @throws OCMProviderException
348-
* @return array
349354
*/
350355
private function prepareOcmPayload(string $uri, string $method, array $options, string $payload, bool $signed): array {
351356
$payload = array_merge($this->generateRequestOptions($options), ['body' => $payload]);
352357
if (!$signed) {
353358
return $payload;
354359
}
355360

356-
if ($this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, lazy: true)
357-
&& $this->signatoryManager->getRemoteSignatory($this->signatureManager->extractIdentityFromUri($uri)) === null) {
361+
$origin = $this->signatureManager->extractIdentityFromUri($uri);
362+
$ocmProvider = $this->discover($origin);
363+
364+
$useRfc9421 = $ocmProvider->hasCapability('http-sig');
365+
$hasPublicKey = $this->signatoryManager->getRemoteSignatory($origin) !== null;
366+
367+
if (!$useRfc9421 && !$hasPublicKey
368+
&& $this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED, lazy: true)) {
358369
throw new OCMProviderException('remote endpoint does not support signed request');
359370
}
360371

361-
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
362-
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload(
363-
$this->signatoryManager,
364-
$payload,
365-
$method, $uri
366-
);
372+
if ($this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
373+
return $payload;
367374
}
368375

369-
return $signedPayload ?? $payload;
376+
$signatoryManager = $useRfc9421
377+
? new Rfc9421SignatoryManager($this->signatoryManager)
378+
: $this->signatoryManager;
379+
380+
return $this->signatureManager->signOutgoingRequestIClientPayload(
381+
$signatoryManager,
382+
$payload,
383+
$method,
384+
$uri,
385+
);
370386
}
371387

372388
private function generateRequestOptions(array $options): array {

lib/private/OCM/OCMJwksHandler.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
10+
namespace OC\OCM;
11+
12+
use OCP\AppFramework\Http\JSONResponse;
13+
use OCP\Http\WellKnown\GenericResponse;
14+
use OCP\Http\WellKnown\IHandler;
15+
use OCP\Http\WellKnown\IRequestContext;
16+
use OCP\Http\WellKnown\IResponse;
17+
use OCP\IAppConfig;
18+
use Psr\Log\LoggerInterface;
19+
use Throwable;
20+
21+
/** Serves `/.well-known/jwks.json` (RFC 7517) for the RFC 9421 keys. */
22+
class OCMJwksHandler implements IHandler {
23+
public function __construct(
24+
private readonly IAppConfig $appConfig,
25+
private readonly OCMSignatoryManager $signatoryManager,
26+
private readonly LoggerInterface $logger,
27+
) {
28+
}
29+
30+
#[\Override]
31+
public function handle(string $service, IRequestContext $context, ?IResponse $previousResponse): ?IResponse {
32+
if ($service !== 'jwks.json') {
33+
return $previousResponse;
34+
}
35+
36+
$keys = [];
37+
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
38+
try {
39+
foreach ($this->signatoryManager->getLocalEd25519Jwks() as $jwk) {
40+
$keys[] = $jwk;
41+
}
42+
} catch (Throwable $e) {
43+
$this->logger->warning('failed to build local Ed25519 JWKs', ['exception' => $e]);
44+
}
45+
}
46+
47+
return new GenericResponse(new JSONResponse(['keys' => $keys]));
48+
}
49+
}

0 commit comments

Comments
 (0)