-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Signed requests #45979
base: master
Are you sure you want to change the base?
Signed requests #45979
Conversation
58d3efd
to
f45a2cb
Compare
// remote does not support signed request. | ||
// currently we still accept unsigned request until lazy appconfig | ||
// core.enforce_signed_ocm_request is set to true (default: false) | ||
if ($this->appConfig->getValueBool('enforce_signed_ocm_request', false, lazy: true)) { |
Check failure
Code scanning / Psalm
InvalidArgument
if ($signatory === null) { | ||
throw new SignatoryNotFoundException('empty result from getRemoteSignatory'); | ||
} | ||
if ($signatory->getKeyId() !== $signedRequest->getKeyId()) { |
Check failure
Code scanning / Psalm
UndefinedInterfaceMethod
f45a2cb
to
a3dda22
Compare
61cc09b
to
9ef90c0
Compare
* @inheritDoc | ||
* | ||
* @param string $publicKey | ||
* @return IKeyPair |
Check failure
Code scanning / Psalm
MismatchingDocblockReturnType
* @inheritDoc | ||
* | ||
* @param string $privateKey | ||
* @return IKeyPair |
Check failure
Code scanning / Psalm
MismatchingDocblockReturnType
* @inheritDoc | ||
* | ||
* @param array $options | ||
* @return IKeyPair |
Check failure
Code scanning / Psalm
MismatchingDocblockReturnType
* @inheritDoc | ||
* | ||
* @param string $host | ||
* @return IOutgoingSignedRequest |
Check failure
Code scanning / Psalm
MismatchingDocblockReturnType
* @param string $key | ||
* @param string|int|float|bool|array $value | ||
* | ||
* @return IOutgoingSignedRequest |
Check failure
Code scanning / Psalm
MismatchingDocblockReturnType
* | ||
* @param string $estimated | ||
* | ||
* @return IOutgoingSignedRequest |
Check failure
Code scanning / Psalm
MismatchingDocblockReturnType
* | ||
* @param string $algorithm | ||
* | ||
* @return IOutgoingSignedRequest |
Check failure
Code scanning / Psalm
MismatchingDocblockReturnType
* @inheritDoc | ||
* | ||
* @param array $signatureHeader | ||
* @return ISignedRequest |
Check failure
Code scanning / Psalm
MismatchingDocblockReturnType
/** | ||
* @inheritDoc | ||
* | ||
* @return ISignatory |
Check failure
Code scanning / Psalm
InvalidNullableReturnType
* @since 30.0.0 | ||
*/ | ||
public function getSignatory(): ISignatory { | ||
return $this->signatory; |
Check failure
Code scanning / Psalm
NullableReturnStatement
// if request is signed and well signed, no exception are thrown | ||
// if request is not signed and host is known for not supporting signed request, no exception are thrown | ||
$signedRequest = $this->getSignedRequest(); | ||
$this->confirmShareOrigin($signedRequest, $notification['sharedSecret'] ?? ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sharedSecret
inside the notification is already used by some OCM messages. So this would break it. Can you take another key? Or maybe you prefix with '$#$'
or something alike?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would not break it as I send the exception for the exact same reason of a missing 'sharedSecret' entry in the notifications request. The only thing is that I do this check earlier than others but I can add a prefix if you want (while I dont feel like necessary myself)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would not break it as I send the exception for the exact same reason of a missing 'sharedSecret' entry in the notifications request.
Nextcloud Talk is sending a data field 'sharedSecret'
and you either overwrite that and it breaks Talk Federation with older servers or you need to use a different key
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As said before, I only use this entry because it exists in the OCM protocol and doing so to compare that the origin of the reshare request, based on the token (and the linked recipient stored in the database), confirm the identity used to sign the request.
If Talk is using this endpoint to initiate anything, signature are to be required
apps/cloud_federation_api/lib/Controller/RequestHandlerController.php
Outdated
Show resolved
Hide resolved
} | ||
} | ||
|
||
private function insertSignatory(ISignatory $signatory): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By now our Entity+QBMapper pattern is widely adapted. Any reason why you didn't go for it and instead went back to doing all this manually again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no real reason other that personal preference
444f9e7
to
f3a1684
Compare
81d2a14
to
c41e150
Compare
6c69420
to
ec4ed86
Compare
* | ||
* @since 31.0.0 | ||
*/ | ||
class KeyPairManager implements IKeyPairManager { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$this->logger->warning('cannot generate local signatory', ['exception' => $e]); | ||
} | ||
|
||
return ['ocm' => json_decode(json_encode($this->provider->jsonSerialize()), true)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the json_decode(json_encode(
really needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it is only requested for psalm as jsonSerialize returns serializable objects
apps/cloud_federation_api/lib/Controller/RequestHandlerController.php
Outdated
Show resolved
Hide resolved
And As an "user1" | ||
And sending "GET" to "/apps/files_sharing/api/v1/shares" | ||
And the list of returned shares has 1 shares |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What change results in this being needed?
// signing the payload using OCMSignatoryManager before initializing the request | ||
$uri = $ocmProvider->getEndPoint() . '/notifications'; | ||
$payload = array_merge($this->getDefaultRequestOptions(), ['body' => json_encode($notification->getMessage())]); | ||
if (!$this->appConfig->getValueBool('core', OCMSignatoryManager::APPCONFIG_SIGN_DISABLED, lazy: true)) { | ||
$signedPayload = $this->signatureManager->signOutgoingRequestIClientPayload( | ||
$this->signatoryManager, | ||
$payload, | ||
'post', $uri | ||
); | ||
} | ||
return $client->post($uri, $signedPayload ?? $payload); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code is duplicated 4 times, please wrap it in a method.
* KeyPairManager store internal public/private key pair using AppConfig, taking advantage of the encryption | ||
* and lazy loading. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should use its own table, it’s a bad habit to put everything into appconfig.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is the main idea of appconfig to avoid creating a new table for only few entries, and the lazy loading helps keeping it light
* @since 31.0.0 | ||
*/ | ||
public function getIncomingSignedRequest( | ||
ISignatoryManager $signatoryManager, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is that not injected? It creates a dependency loop?
* @return array new payload to be sent, including original payload and signature elements in headers | ||
* @since 31.0.0 | ||
*/ | ||
public function signOutgoingRequestIClientPayload( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public function signOutgoingRequestIClientPayload( | |
public function signOutgoingRequestPayload( |
public function searchSignatory(string $host, string $account = ''): ISignatory { | ||
$qb = $this->connection->getQueryBuilder(); | ||
$qb->select( | ||
'id', 'provider_id', 'host', 'account', 'key_id', 'key_id_sum', 'public_key', 'metadata', 'type', | ||
'status', 'creation', 'last_updated' | ||
); | ||
$qb->from(self::TABLE_SIGNATORIES); | ||
$qb->where($qb->expr()->eq('host', $qb->createNamedParameter($host))); | ||
$qb->andWhere($qb->expr()->eq('account', $qb->createNamedParameter($account))); | ||
|
||
$result = $qb->executeQuery(); | ||
$row = $result->fetch(); | ||
$result->closeCursor(); | ||
|
||
if (!$row) { | ||
throw new SignatoryNotFoundException('no signatory found'); | ||
} | ||
|
||
$signature = new Signatory($row['key_id'], $row['public_key']); | ||
|
||
return $signature->importFromDatabase($row); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there no mapper and entities for signatories?
public function extractIdentityFromUri(string $uri): string { | ||
$identity = parse_url($uri, PHP_URL_HOST); | ||
$port = parse_url($uri, PHP_URL_PORT); | ||
if ($identity === null || $identity === false) { | ||
throw new IdentityNotFoundException('cannot extract identity from ' . $uri); | ||
} | ||
|
||
if ($port !== null && $port !== false) { | ||
$identity .= ':' . $port; | ||
} | ||
|
||
return $identity; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have Déjà-vu about this code
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, fixed
9983ea2
to
d7446ed
Compare
Signed-off-by: Maxence Lange <[email protected]>
6eb73da
to
5912b44
Compare
Signed-off-by: Maxence Lange <[email protected]>
5912b44
to
6ee56ab
Compare
Signed Request
Signing request allows to confirm the identity of the sender.
Signing request does not encrypt nor affect its payload.
Signing request only adds metadata to the headers of the request.
Signature
The concept is to add unique metadata and sign them using a private/public key pair.
The location of the public key used to verify the signature will confirm the origin of the request.
Signature does not affect the data of the request, it only adds headers to it:
listed in 'headers' and their value. Some elements (content-length date digest host) are mandatory
to ensure authenticity override protection.
(Those are the minimum required headers, some can be added via options during the process)
ISignatoryManager
Because each protocol have different ways to obtain the public key of a remote instance or entity, some part of the signing/verifying process is managed by a custom provider, one for each protocol.
getProviderId
should returns a unique stringgetOptions
should returns an array that can contains those entries:getLocalSignatory
should return the local signatory, including the full (public+private) key pair.getRemoteSignatory
should returns a remote signatory based on the requested data, must at least contains key id and public keyIKeyPairManager
IKeyPairManager
contains a group of method to create/manage/store internal public/private key pair, stored as sensitive data using a lazy loadedIAppConfig
variable. 2 stringsapp id
andname
are used to identify key pairs.generateKeyPair
generate and store public/private key pair.hasKeyPair
return if key pair is known in database.getKeyPair
return key pair from database based on app id and name.deleteKeyPair
delete key pair from database.ISignatureManager
ISignatureManager
is a service integrated to core that provide tools to set/get authenticity of/from outgoing/incoming requests.getIncomingSignedRequest
extract data from the incoming request and compare headers to confirm authenticity of remote instancegetOutgoingSignedRequest
prep signature to sign an outgoing request.signOutgoingRequestIClientPayload
is the one method to call to fully process of signing and fulfilling the payload for an outgoing request using IClientsearchSignatory
get a remote signatory from the database