Skip to content

Commit dcfb282

Browse files
committed
chore(http-sig): Simplify implementation
Signed-off-by: Micke Nordin <kano@sunet.se>
1 parent 30b8eef commit dcfb282

11 files changed

Lines changed: 86 additions & 122 deletions

File tree

‎apps/cloud_federation_api/lib/Controller/OCMRequestController.php‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace OCA\CloudFederationAPI\Controller;
1111

12+
use JsonException;
1213
use OCP\AppFramework\Controller;
1314
use OCP\AppFramework\Http;
1415
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
@@ -17,7 +18,6 @@
1718
use OCP\AppFramework\Http\JSONResponse;
1819
use OCP\AppFramework\Http\Response;
1920
use OCP\EventDispatcher\IEventDispatcher;
20-
use OCP\Federation\ICloudFederationProviderManager;
2121
use OCP\IRequest;
2222
use OCP\OCM\Events\OCMEndpointRequestEvent;
2323
use OCP\OCM\Exceptions\OCMArgumentException;
@@ -31,7 +31,6 @@ public function __construct(
3131
IRequest $request,
3232
private readonly IEventDispatcher $eventDispatcher,
3333
private readonly IOCMDiscoveryService $ocmDiscoveryService,
34-
private readonly ICloudFederationProviderManager $cloudFederationProviderManager,
3534
private readonly LoggerInterface $logger,
3635
) {
3736
parent::__construct($appName, $request);
@@ -57,29 +56,33 @@ public function manageOCMRequests(string $ocmPath): Response {
5756
throw new OCMArgumentException('path is not UTF-8');
5857
}
5958

60-
// Resolve the signer origin from the payload before verification.
61-
$payload = $this->request->getParams();
62-
$origin = null;
63-
if ($payload !== []) {
64-
$identity = $this->cloudFederationProviderManager->resolveSenderIdentity($payload);
65-
if ($identity !== null) {
66-
try {
67-
$origin = $this->ocmDiscoveryService->getHostFromOcmAddress($identity);
68-
} catch (IncomingRequestException) {
69-
// unresolvable origin; verification will fail without one
70-
}
59+
$ocmAddress = null;
60+
$params = $this->request->getParams();
61+
foreach (['owner', 'sender', 'sharedBy'] as $field) {
62+
if (is_string($params[$field] ?? null) && $params[$field] !== '') {
63+
$ocmAddress = $params[$field];
64+
break;
7165
}
7266
}
7367

7468
try {
75-
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest($origin);
69+
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest($ocmAddress);
7670
} catch (IncomingRequestException $e) {
7771
$this->logger->warning('incoming ocm request exception', ['exception' => $e]);
7872
$response = new JSONResponse(['message' => $e->getMessage(), 'validationErrors' => []], Http::STATUS_BAD_REQUEST);
7973
$response->throttle();
8074
return $response;
8175
}
8276

77+
// assuming that ocm request contains a json array
78+
$payload = $signedRequest?->getBody() ?? file_get_contents('php://input');
79+
try {
80+
$payload = ($payload) ? json_decode($payload, true, 512, JSON_THROW_ON_ERROR) : null;
81+
} catch (JsonException $e) {
82+
$this->logger->debug('json decode error', ['exception' => $e]);
83+
$payload = null;
84+
}
85+
8386
$event = new OCMEndpointRequestEvent(
8487
$this->request->getMethod(),
8588
preg_replace('@/+@', '/', $ocmPath),

‎apps/cloud_federation_api/lib/Controller/RequestHandlerController.php‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,7 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
110110
try {
111111
// if request is signed and well signed, no exceptions are thrown
112112
// if request is not signed and host is known for not supporting signed request, no exception are thrown
113-
$origin = $this->ocmDiscoveryService->getHostFromOcmAddress($owner);
114-
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest($origin);
113+
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest($owner);
115114
$this->confirmSignedOrigin($signedRequest, 'owner', $owner);
116115
} catch (IncomingRequestException $e) {
117116
$this->logger->warning('incoming request exception', ['exception' => $e]);
@@ -309,11 +308,7 @@ public function receiveNotification($notificationType, $resourceType, $providerI
309308
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) {
310309
try {
311310
$identity = $this->resolveNotificationIdentity($resourceType, $notification);
312-
$origin = null;
313-
if ($identity !== '') {
314-
$origin = $this->ocmDiscoveryService->getHostFromOcmAddress($identity);
315-
}
316-
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest($origin);
311+
$signedRequest = $this->ocmDiscoveryService->getIncomingSignedRequest($identity !== '' ? $identity : null);
317312
if ($identity !== '') {
318313
$this->ocmDiscoveryService->confirmRequestOrigin($signedRequest?->getOrigin(), $identity);
319314
}

‎apps/cloud_federation_api/lib/Controller/TokenController.php‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,19 @@
2323
use OCP\Authentication\Exceptions\ExpiredTokenException;
2424
use OCP\Authentication\Exceptions\InvalidTokenException;
2525
use OCP\Authentication\Token\IToken;
26+
use OCP\Federation\ICloudIdManager;
2627
use OCP\IAppConfig;
2728
use OCP\IRequest;
28-
use OCP\OCM\IOCMDiscoveryService;
2929
use OCP\Security\ISecureRandom;
30+
use OCP\Security\Signature\Exceptions\IdentityNotFoundException;
3031
use OCP\Security\Signature\Exceptions\IncomingRequestException;
3132
use OCP\Security\Signature\Exceptions\SignatoryNotFoundException;
3233
use OCP\Security\Signature\Exceptions\SignatureException;
3334
use OCP\Security\Signature\Exceptions\SignatureNotFoundException;
3435
use OCP\Security\Signature\IIncomingSignedRequest;
3536
use OCP\Security\Signature\ISignatureManager;
3637
use OCP\Security\Signature\Model\Signatory;
38+
use OCP\Share\Exceptions\ShareNotFound;
3739
use OCP\Share\IManager as IShareManager;
3840
use Psr\Log\LoggerInterface;
3941

@@ -53,7 +55,7 @@ public function __construct(
5355
private readonly IAppConfig $appConfig,
5456
private readonly OcmTokenMapMapper $ocmTokenMapMapper,
5557
private readonly IShareManager $shareManager,
56-
private readonly IOCMDiscoveryService $ocmDiscoveryService,
58+
private readonly ICloudIdManager $cloudIdManager,
5759
) {
5860
parent::__construct('cloud_federation_api', $request);
5961
}
@@ -74,16 +76,17 @@ private function resolveOriginFromRefreshToken(string $code): ?string {
7476
if ($sharedWith === null || $sharedWith === '') {
7577
return null;
7678
}
77-
return $this->ocmDiscoveryService->getHostFromOcmAddress($sharedWith);
78-
} catch (\Throwable) {
79+
$remote = $this->cloudIdManager->resolveCloudId($sharedWith)->getRemote();
80+
return $this->signatureManager->extractIdentityFromUri($remote);
81+
} catch (ShareNotFound|IdentityNotFoundException|\InvalidArgumentException) {
7982
return null;
8083
}
8184
}
8285

8386
/**
8487
* Verify the signature of incoming request if available
8588
*
86-
* @param string|null $origin the origin of the request, or null if unknown
89+
* @param string|null $origin sender origin, or null if unknown
8790
*
8891
* @return IIncomingSignedRequest|null null if remote does not support signed requests
8992
* @throws IncomingRequestException if signature is required but invalid
@@ -148,9 +151,7 @@ private function resolveJwtSigningKey(string $privateKeyPem): array {
148151
public function jwks(): JSONResponse {
149152
$keys = [];
150153
try {
151-
foreach ($this->signatoryManager->getLocalJwks() as $jwk) {
152-
$keys[] = $jwk;
153-
}
154+
$keys = $this->signatoryManager->getLocalJwks();
154155
} catch (\Throwable $e) {
155156
$this->logger->warning('failed to build local JWKs', ['exception' => $e]);
156157
}

‎apps/cloud_federation_api/tests/Controller/TokenControllerTest.php‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@
2121
use OCP\Authentication\Exceptions\ExpiredTokenException;
2222
use OCP\Authentication\Exceptions\InvalidTokenException;
2323
use OCP\Authentication\Token\IToken;
24+
use OCP\Federation\ICloudId;
25+
use OCP\Federation\ICloudIdManager;
2426
use OCP\IAppConfig;
2527
use OCP\IRequest;
26-
use OCP\OCM\IOCMDiscoveryService;
2728
use OCP\Security\ISecureRandom;
2829
use OCP\Security\Signature\Exceptions\SignatoryNotFoundException;
2930
use OCP\Security\Signature\Exceptions\SignatureException;
@@ -48,7 +49,7 @@ class TokenControllerTest extends TestCase {
4849
private IAppConfig&MockObject $appConfig;
4950
private OcmTokenMapMapper&MockObject $ocmTokenMapMapper;
5051
private IShareManager&MockObject $shareManager;
51-
private IOCMDiscoveryService&MockObject $ocmDiscoveryService;
52+
private ICloudIdManager&MockObject $cloudIdManager;
5253

5354
private TokenController $controller;
5455

@@ -65,11 +66,13 @@ protected function setUp(): void {
6566
$this->timeFactory = $this->createMock(ITimeFactory::class);
6667
$this->logger = $this->createMock(LoggerInterface::class);
6768
$this->signatureManager = $this->createMock(ISignatureManager::class);
69+
$this->signatureManager->method('extractIdentityFromUri')
70+
->willReturnCallback(static fn (string $uri): string => (string)parse_url($uri, PHP_URL_HOST));
6871
$this->signatoryManager = $this->createMock(OCMSignatoryManager::class);
6972
$this->appConfig = $this->createMock(IAppConfig::class);
7073
$this->ocmTokenMapMapper = $this->createMock(OcmTokenMapMapper::class);
7174
$this->shareManager = $this->createMock(IShareManager::class);
72-
$this->ocmDiscoveryService = $this->createMock(IOCMDiscoveryService::class);
75+
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
7376

7477
$this->controller = new TokenController(
7578
$this->request,
@@ -82,7 +85,7 @@ protected function setUp(): void {
8285
$this->appConfig,
8386
$this->ocmTokenMapMapper,
8487
$this->shareManager,
85-
$this->ocmDiscoveryService,
88+
$this->cloudIdManager,
8689
);
8790
}
8891

@@ -133,6 +136,11 @@ private function configureHappyPath(
133136
$this->shareManager->method('getShareByToken')
134137
->with($refreshToken)
135138
->willReturn($share);
139+
$cloudId = $this->createMock(ICloudId::class);
140+
$cloudId->method('getRemote')->willReturn('https://remote.example.com');
141+
$this->cloudIdManager->method('resolveCloudId')
142+
->with($sharedWith)
143+
->willReturn($cloudId);
136144

137145
$signatory = new Signatory();
138146
$signatory->setKeyId('https://local.example.com/index.php/ocm#signature');
@@ -153,10 +161,10 @@ public function testAccessTokenSuccess(): void {
153161
$signedRequest = $this->createMock(IIncomingSignedRequest::class);
154162
$signedRequest->method('getOrigin')->willReturn('remote.example.com');
155163
$this->signatureManager->method('getIncomingSignedRequest')
156-
->with($this->signatoryManager)
164+
->with($this->signatoryManager, null, 'remote.example.com')
157165
->willReturn($signedRequest);
158166

159-
$this->configureHappyPath('valid-refresh-token', 123, 'testuser', 'owner', 'sharee@remote.example.com', 'fixedjtivalue00');
167+
$this->configureHappyPath('valid-refresh-token', 123, 'testuser', 'owner', 'sharee@department@remote.example.com', 'fixedjtivalue00');
160168

161169
$this->ocmTokenMapMapper->expects($this->once())
162170
->method('insert')
@@ -181,7 +189,7 @@ public function testAccessTokenSuccess(): void {
181189
$decoded = JWT::decode($data['access_token'], new Key($this->publicKeyPem, 'RS256'));
182190
$this->assertSame('https://local.example.com', $decoded->iss);
183191
$this->assertSame('owner', $decoded->sub);
184-
$this->assertSame('sharee@remote.example.com', $decoded->aud);
192+
$this->assertSame('sharee@department@remote.example.com', $decoded->aud);
185193
$this->assertSame('789', $decoded->client_id);
186194
$this->assertSame('fixedjtivalue00', $decoded->jti);
187195
$this->assertSame(1000000, $decoded->iat);

‎lib/private/AppFramework/Http/Attributes/FederationRateLimit.php‎

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use OC\OCM\OCMDiscoveryService;
1414
use OCA\Federation\TrustedServers;
1515
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
16-
use OCP\Federation\ICloudFederationProviderManager;
1716
use OCP\IRequest;
1817
use OCP\Server;
1918

@@ -25,14 +24,12 @@
2524
*/
2625
#[Attribute(Attribute::TARGET_METHOD)]
2726
class FederationRateLimit extends AnonRateLimit {
28-
private readonly ICloudFederationProviderManager $federationProviderManager;
2927
private readonly OCMDiscoveryService $discoveryService;
3028
private readonly ?TrustedServers $trustedServers;
3129

3230
public function __construct(int $limit, int $period) {
3331
parent::__construct($limit, $period);
3432

35-
$this->federationProviderManager = Server::get(ICloudFederationProviderManager::class);
3633
$this->discoveryService = Server::get(OCMDiscoveryService::class);
3734
$this->trustedServers = Server::get(TrustedServers::class);
3835
}
@@ -44,16 +41,8 @@ public function shouldApply(IRequest $request): bool {
4441
}
4542

4643
try {
47-
// Resolve the signer origin from the payload so trusted servers
48-
// can be exempted.
49-
$parsed = $request->getParams();
50-
$identity = $this->federationProviderManager->resolveSenderIdentity($parsed);
51-
$origin = null;
52-
if ($identity !== null) {
53-
$origin = $this->discoveryService->getHostFromOcmAddress($identity);
54-
}
55-
56-
$signedRequest = $this->discoveryService->getIncomingSignedRequest($origin);
44+
$owner = $request->getParam('owner');
45+
$signedRequest = $this->discoveryService->getIncomingSignedRequest(is_string($owner) ? $owner : null);
5746
if (!$signedRequest) {
5847
return true;
5948
}

‎lib/private/Federation/CloudFederationProviderManager.php‎

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use OCP\Federation\ICloudFederationProviderManager;
1919
use OCP\Federation\ICloudFederationShare;
2020
use OCP\Federation\ICloudIdManager;
21-
use OCP\Federation\ISignedCloudFederationProvider;
2221
use OCP\Http\Client\IClient;
2322
use OCP\Http\Client\IClientService;
2423
use OCP\Http\Client\IResponse;
@@ -106,39 +105,6 @@ public function getCloudFederationProvider($resourceType) {
106105
}
107106
}
108107

109-
/**
110-
* @inheritDoc
111-
*
112-
* Notifications resolve via sharedSecret; shares via owner/sender.
113-
*/
114-
#[\Override]
115-
public function resolveSenderIdentity(array $body): ?string {
116-
$resourceType = $body['resourceType'] ?? '';
117-
if ($resourceType !== '') {
118-
$notification = $body['notification'] ?? null;
119-
$sharedSecret = is_array($notification) ? ($notification['sharedSecret'] ?? '') : '';
120-
if ($sharedSecret !== '') {
121-
try {
122-
$provider = $this->getCloudFederationProvider($resourceType);
123-
if ($provider instanceof ISignedCloudFederationProvider || $provider instanceof \NCU\Federation\ISignedCloudFederationProvider) {
124-
$identity = $provider->getFederationIdFromSharedSecret($sharedSecret, is_array($notification) ? $notification : []);
125-
if ($identity !== '') {
126-
return $identity;
127-
}
128-
}
129-
} catch (\Exception) {
130-
// unresolved; fall through to share-style fields
131-
}
132-
}
133-
}
134-
foreach (['owner', 'sender', 'sharedBy'] as $field) {
135-
if (isset($body[$field]) && is_string($body[$field]) && $body[$field] !== '') {
136-
return $body[$field];
137-
}
138-
}
139-
return null;
140-
}
141-
142108
/**
143109
* @deprecated 29.0.0 - Use {@see sendCloudShare()} instead and handle errors manually
144110
*/

‎lib/private/OCM/OCMDiscoveryService.php‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ public function getLocalOCMProvider(bool $fullDetails = true): IOCMProvider {
259259
* @since 33.0.0
260260
*/
261261
#[\Override]
262-
public function getIncomingSignedRequest(?string $origin = null): ?IIncomingSignedRequest {
262+
public function getIncomingSignedRequest(?string $ocmAddress = null): ?IIncomingSignedRequest {
263+
$origin = $ocmAddress !== null ? $this->getHostFromOcmAddress($ocmAddress) : null;
263264
try {
264265
$signedRequest = $this->signatureManager->getIncomingSignedRequest($this->signatoryManager, null, $origin);
265266
$this->logger->debug('signed request available', ['signedRequest' => $signedRequest]);
@@ -316,8 +317,7 @@ public function confirmRequestOrigin(?string $signedOrigin, string $ocmAddress):
316317
* @return string the host (with port) of the OCM address
317318
* @throws IncomingRequestException on malformed address or unresolvable host
318319
*/
319-
#[\Override]
320-
public function getHostFromOcmAddress(string $entry): string {
320+
private function getHostFromOcmAddress(string $entry): string {
321321
try {
322322
$cloudId = $this->cloudIdManager->resolveCloudId(trim($entry, '@'));
323323
return $this->signatureManager->extractIdentityFromUri($cloudId->getRemote());

‎lib/private/OCM/OCMSignatoryManager.php‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,11 @@ private function buildLocalUrl(string $path): string {
404404
return 'https://' . $identity . $path;
405405
}
406406

407+
try {
408+
return $this->signatureManager->generateKeyIdFromConfig($path);
409+
} catch (IdentityNotFoundException) {
410+
}
411+
407412
return $this->urlGenerator->getAbsoluteURL($path);
408413
}
409414

@@ -522,13 +527,20 @@ private function fetchJwks(string $origin): ?array {
522527
}
523528

524529
/**
525-
* The peer's `jwksUri` from its discovery response. Must be https, or
526-
* http from an http-only peer (the spec's testing fallback): an https
527-
* peer pointing at an http jwksUri would downgrade the key fetch.
530+
* Resolve the peer's `jwksUri`, preferring HTTPS discovery. HTTP is accepted
531+
* only when HTTPS discovery fails and the discovery document is fetched over
532+
* HTTP as well.
528533
*/
529534
private function resolveJwksUri(string $origin): ?string {
530535
try {
531-
$provider = Server::get(IOCMDiscoveryService::class)->discover($origin);
536+
$discoveryService = Server::get(IOCMDiscoveryService::class);
537+
try {
538+
$provider = $discoveryService->discover('https://' . $origin);
539+
$discoveryScheme = 'https';
540+
} catch (OCMProviderException) {
541+
$provider = $discoveryService->discover('http://' . $origin);
542+
$discoveryScheme = 'http';
543+
}
532544
} catch (NotFoundExceptionInterface|ContainerExceptionInterface|OCMProviderException $e) {
533545
$this->logger->warning('cannot discover remote OCM provider for JWKS', ['exception' => $e, 'origin' => $origin]);
534546
return null;
@@ -541,10 +553,14 @@ private function resolveJwksUri(string $origin): ?string {
541553
}
542554
return null;
543555
}
544-
$httpFromHttpPeer = str_starts_with($jwksUri, 'http://')
545-
&& str_starts_with($provider->getEndPoint(), 'http://');
546-
if (!str_starts_with($jwksUri, 'https://') && !$httpFromHttpPeer) {
547-
$this->logger->warning('refusing jwksUri: https is required unless the peer itself is http-only', ['origin' => $origin, 'jwksUri' => $jwksUri]);
556+
$secureJwks = str_starts_with($jwksUri, 'https://');
557+
$httpJwksFromHttpDiscovery = $discoveryScheme === 'http' && str_starts_with($jwksUri, 'http://');
558+
if (!$secureJwks && !$httpJwksFromHttpDiscovery) {
559+
$this->logger->warning('refusing jwksUri for the OCM discovery transport', [
560+
'origin' => $origin,
561+
'jwksUri' => $jwksUri,
562+
'discoveryScheme' => $discoveryScheme,
563+
]);
548564
return null;
549565
}
550566
return $jwksUri;

0 commit comments

Comments
 (0)