55 */
66namespace OCA \CloudFederationAPI \Controller ;
77
8+ use NCU \Security \Signature \Exceptions \IncomingRequestException ;
9+ use NCU \Security \Signature \Exceptions \SignatoryNotFoundException ;
10+ use NCU \Security \Signature \Exceptions \SignatureException ;
11+ use NCU \Security \Signature \Exceptions \SignatureNotFoundException ;
12+ use NCU \Security \Signature \ISignatureManager ;
13+ use NCU \Security \Signature \Model \IIncomingSignedRequest ;
14+ use OC \OCM \OCMSignatoryManager ;
815use OCA \CloudFederationAPI \Config ;
916use OCA \CloudFederationAPI \ResponseDefinitions ;
1017use OCP \AppFramework \Controller ;
2229use OCP \Federation \ICloudFederationFactory ;
2330use OCP \Federation \ICloudFederationProviderManager ;
2431use OCP \Federation \ICloudIdManager ;
32+ use OCP \IAppConfig ;
2533use OCP \IGroupManager ;
2634use OCP \IRequest ;
2735use OCP \IURLGenerator ;
2836use OCP \IUserManager ;
2937use OCP \Share \Exceptions \ShareNotFound ;
38+ use OCP \Share \IProviderFactory ;
39+ use OCP \Share \IShare ;
3040use OCP \Util ;
3141use Psr \Log \LoggerInterface ;
3242
@@ -50,8 +60,12 @@ public function __construct(
5060 private IURLGenerator $ urlGenerator ,
5161 private ICloudFederationProviderManager $ cloudFederationProviderManager ,
5262 private Config $ config ,
63+ private readonly IAppConfig $ appConfig ,
5364 private ICloudFederationFactory $ factory ,
5465 private ICloudIdManager $ cloudIdManager ,
66+ private readonly ISignatureManager $ signatureManager ,
67+ private readonly OCMSignatoryManager $ signatoryManager ,
68+ private readonly IProviderFactory $ shareProviderFactory ,
5569 ) {
5670 parent ::__construct ($ appName , $ request );
5771 }
@@ -81,11 +95,20 @@ public function __construct(
8195 #[NoCSRFRequired]
8296 #[BruteForceProtection(action: 'receiveFederatedShare ' )]
8397 public function addShare ($ shareWith , $ name , $ description , $ providerId , $ owner , $ ownerDisplayName , $ sharedBy , $ sharedByDisplayName , $ protocol , $ shareType , $ resourceType ) {
98+ try {
99+ // if request is signed and well signed, no exception are thrown
100+ // if request is not signed and host is known for not supporting signed request, no exception are thrown
101+ $ signedRequest = $ this ->getSignedRequest ();
102+ $ this ->confirmSignedOrigin ($ signedRequest , 'owner ' , $ owner );
103+ } catch (IncomingRequestException $ e ) {
104+ $ this ->logger ->warning ('incoming request exception ' , ['exception ' => $ e ]);
105+ return new JSONResponse (['message ' => $ e ->getMessage (), 'validationErrors ' => []], Http::STATUS_BAD_REQUEST );
106+ }
107+
84108 // check if all required parameters are set
85109 if ($ shareWith === null ||
86110 $ name === null ||
87111 $ providerId === null ||
88- $ owner === null ||
89112 $ resourceType === null ||
90113 $ shareType === null ||
91114 !is_array ($ protocol ) ||
@@ -208,6 +231,16 @@ public function addShare($shareWith, $name, $description, $providerId, $owner, $
208231 #[PublicPage]
209232 #[BruteForceProtection(action: 'receiveFederatedShareNotification ' )]
210233 public function receiveNotification ($ notificationType , $ resourceType , $ providerId , ?array $ notification ) {
234+ try {
235+ // if request is signed and well signed, no exception are thrown
236+ // if request is not signed and host is known for not supporting signed request, no exception are thrown
237+ $ signedRequest = $ this ->getSignedRequest ();
238+ $ this ->confirmShareOrigin ($ signedRequest , $ notification ['sharedSecret ' ] ?? '' );
239+ } catch (IncomingRequestException $ e ) {
240+ $ this ->logger ->warning ('incoming request exception ' , ['exception ' => $ e ]);
241+ return new JSONResponse (['message ' => $ e ->getMessage (), 'validationErrors ' => []], Http::STATUS_BAD_REQUEST );
242+ }
243+
211244 // check if all required parameters are set
212245 if ($ notificationType === null ||
213246 $ resourceType === null ||
@@ -286,4 +319,124 @@ private function mapUid($uid) {
286319
287320 return $ uid ;
288321 }
322+
323+
324+ /**
325+ * returns signed request if available.
326+ * throw an exception:
327+ * - if request is signed, but wrongly signed
328+ * - if request is not signed but instance is configured to only accept signed ocm request
329+ *
330+ * @return IIncomingSignedRequest|null null if remote does not (and never did) support signed request
331+ * @throws IncomingRequestException
332+ */
333+ private function getSignedRequest (): ?IIncomingSignedRequest {
334+ try {
335+ return $ this ->signatureManager ->getIncomingSignedRequest ($ this ->signatoryManager );
336+ } catch (SignatureNotFoundException |SignatoryNotFoundException $ e ) {
337+ // remote does not support signed request.
338+ // currently we still accept unsigned request until lazy appconfig
339+ // core.enforce_signed_ocm_request is set to true (default: false)
340+ if ($ this ->appConfig ->getValueBool ('core ' , OCMSignatoryManager::APPCONFIG_SIGN_ENFORCED , lazy: true )) {
341+ $ this ->logger ->notice ('ignored unsigned request ' , ['exception ' => $ e ]);
342+ throw new IncomingRequestException ('Unsigned request ' );
343+ }
344+ } catch (SignatureException $ e ) {
345+ $ this ->logger ->notice ('wrongly signed request ' , ['exception ' => $ e ]);
346+ throw new IncomingRequestException ('Invalid signature ' );
347+ }
348+ return null ;
349+ }
350+
351+
352+ /**
353+ * confirm that the value related to $key entry from the payload is in format userid@hostname
354+ * and compare hostname with the origin of the signed request.
355+ *
356+ * If request is not signed, we still verify that the hostname from the extracted value does,
357+ * actually, not support signed request
358+ *
359+ * @param IIncomingSignedRequest|null $signedRequest
360+ * @param string $key entry from data available in data
361+ * @param string $value value itself used in case request is not signed
362+ *
363+ * @throws IncomingRequestException
364+ */
365+ private function confirmSignedOrigin (?IIncomingSignedRequest $ signedRequest , string $ key , string $ value ): void {
366+ if ($ signedRequest === null ) {
367+ $ instance = $ this ->getHostFromFederationId ($ value );
368+ try {
369+ $ this ->signatureManager ->searchSignatory ($ instance );
370+ throw new IncomingRequestException ('instance is supposed to sign its request ' );
371+ } catch (SignatoryNotFoundException ) {
372+ return ;
373+ }
374+ }
375+
376+ $ body = json_decode ($ signedRequest ->getBody (), true ) ?? [];
377+ $ entry = trim ($ body [$ key ] ?? '' , '@ ' );
378+ if ($ this ->getHostFromFederationId ($ entry ) !== $ signedRequest ->getOrigin ()) {
379+ throw new IncomingRequestException ('share initiation from different instance ' );
380+ }
381+ }
382+
383+
384+ /**
385+ * confirm that the value related to share token is in format userid@hostname
386+ * and compare hostname with the origin of the signed request.
387+ *
388+ * If request is not signed, we still verify that the hostname from the extracted value does,
389+ * actually, not support signed request
390+ *
391+ * @param IIncomingSignedRequest|null $signedRequest
392+ * @param string $token
393+ *
394+ * @return void
395+ * @throws IncomingRequestException
396+ */
397+ private function confirmShareOrigin (?IIncomingSignedRequest $ signedRequest , string $ token ): void {
398+ if ($ token === '' ) {
399+ throw new BadRequestException (['sharedSecret ' ]);
400+ }
401+
402+ $ provider = $ this ->shareProviderFactory ->getProviderForType (IShare::TYPE_REMOTE );
403+ $ share = $ provider ->getShareByToken ($ token );
404+ $ entry = $ share ->getSharedWith ();
405+
406+ $ instance = $ this ->getHostFromFederationId ($ entry );
407+ if ($ signedRequest === null ) {
408+ try {
409+ $ this ->signatureManager ->searchSignatory ($ instance );
410+ throw new IncomingRequestException ('instance is supposed to sign its request ' );
411+ } catch (SignatoryNotFoundException ) {
412+ return ;
413+ }
414+ } elseif ($ instance !== $ signedRequest ->getOrigin ()) {
415+ throw new IncomingRequestException ('token sharedWith from different instance ' );
416+ }
417+ }
418+
419+ /**
420+ * @param string $entry
421+ * @return string
422+ * @throws IncomingRequestException
423+ */
424+ private function getHostFromFederationId (string $ entry ): string {
425+ if (!str_contains ($ entry , '@ ' )) {
426+ throw new IncomingRequestException ('entry does not contains @ ' );
427+ }
428+ [, $ rightPart ] = explode ('@ ' , $ entry , 2 );
429+
430+ $ host = parse_url ($ rightPart , PHP_URL_HOST );
431+ $ port = parse_url ($ rightPart , PHP_URL_PORT );
432+ if ($ port !== null && $ port !== false ) {
433+ $ host .= ': ' . $ port ;
434+ }
435+
436+ if (is_string ($ host ) && $ host !== '' ) {
437+ return $ host ;
438+ }
439+
440+ throw new IncomingRequestException ('host is empty ' );
441+ }
289442}
0 commit comments