-
-
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
df8f30e
to
1b02a3c
Compare
Since last review, new commit:
|
c7683aa
to
bcb284a
Compare
Signed-off-by: Maxence Lange <[email protected]>
Signed-off-by: Maxence Lange <[email protected]>
Signed-off-by: Maxence Lange <[email protected]>
bcb284a
to
ab72b03
Compare
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.
There should be a mapper for signatory
There should not be a digest sent to openssl_* which is creating a digest by itself.
clearSignature should be renamed to something more appropriate.
$date = $this->getRequest()->getHeader('date'); | ||
if ($date === '') { | ||
throw new SignatureNotFoundException('missing date in header'); | ||
} | ||
$contentLength = $this->getRequest()->getHeader('content-length'); | ||
if ($contentLength === '') { | ||
throw new SignatureNotFoundException('missing content-length in header'); | ||
} | ||
$digest = $this->getRequest()->getHeader('digest'); | ||
if ($digest === '') { | ||
throw new SignatureNotFoundException('missing digest in header'); | ||
} | ||
if ($this->getRequest()->getHeader('Signature') === '') { | ||
throw new SignatureNotFoundException('missing Signature in header'); | ||
} |
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.
$date = $this->getRequest()->getHeader('date'); | |
if ($date === '') { | |
throw new SignatureNotFoundException('missing date in header'); | |
} | |
$contentLength = $this->getRequest()->getHeader('content-length'); | |
if ($contentLength === '') { | |
throw new SignatureNotFoundException('missing content-length in header'); | |
} | |
$digest = $this->getRequest()->getHeader('digest'); | |
if ($digest === '') { | |
throw new SignatureNotFoundException('missing digest in header'); | |
} | |
if ($this->getRequest()->getHeader('Signature') === '') { | |
throw new SignatureNotFoundException('missing Signature in header'); | |
} | |
$date = $this->request->getHeader('date'); | |
if ($date === '') { | |
throw new SignatureNotFoundException('missing date in header'); | |
} | |
$contentLength = $this->request->getHeader('content-length'); | |
if ($contentLength === '') { | |
throw new SignatureNotFoundException('missing content-length in header'); | |
} | |
$digest = $this->request->getHeader('digest'); | |
if ($digest === '') { | |
throw new SignatureNotFoundException('missing digest in header'); | |
} | |
if ($this->request->getHeader('Signature') === '') { | |
throw new SignatureNotFoundException('missing Signature in header'); | |
} |
// confirm presence of date, content-length, digest and Signature | ||
$date = $this->getRequest()->getHeader('date'); | ||
if ($date === '') { | ||
throw new SignatureNotFoundException('missing date in header'); |
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.
SignatureNotFoundException
does not feel like the right exception here?
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.
All thrown in this method should be IncomingRequestException I think
} | ||
|
||
[$k, $v] = explode('=', $entry, 2); | ||
preg_match('/"([^"]+)"/', $v, $var); |
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.
preg_match('/"([^"]+)"/', $v, $var); | |
preg_match('/^"([^"]+)"$/', $v, $var); |
I suppose, no?
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 know mine is working, what should I expected different from yours ?
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.
https://regex101.com/r/BPNH0j/1
Yours will match quotes even if there is unexpected stuff around.
Mine makes sure that quotes are first and last characters.
* @inheritDoc | ||
* | ||
* @param string $host | ||
* @return IOutgoingSignedRequest |
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.
* @return IOutgoingSignedRequest | |
* @return $this |
You should use this for fluent interfaces in phpdoc.
Also you do not need it in the class when it’s there on the public interface it’s inherited by default.
private readonly string $body, | ||
) { | ||
// digest is created on the fly using $body | ||
$this->digest = 'SHA-256=' . base64_encode(hash('sha256', utf8_encode($body), 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.
utf8_encode
is deprecated.
Do you really have the body in ISO-8859-1
here? Why?
$estimated[] = $key . ': ' . $value; | ||
} | ||
|
||
$signedRequest->setClearSignature(implode("\n", $estimated)); |
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 still dislike that this is stored in the IIncomingSignedRequest while computed outside. Either there is only getClearSignature that computes the value, or the method is only in the signature manager and returns it, no need to set it on the object.
Also, I’m pretty sure this should be named an other way. "clearSignature" is confusing. But I’m not sure what is the common term for this. fingerprint? signatureData? signedData? requestData?
private readonly string $body, | ||
) { | ||
// digest is created on the fly using $body | ||
$this->digest = 'SHA-256=' . base64_encode(hash('sha256', utf8_encode($body), 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.
Actually you should not compute a digest yourself. This is the job of openssl_sign and openssl_verify, and why you give them a sum algorithm to use.
$knownSignatory = $this->getStoredSignatory($signatory->getKeyId()); | ||
switch ($signatory->getType()) { | ||
case SignatoryType::FORGIVABLE: | ||
$this->deleteSignatory($knownSignatory->getKeyId()); | ||
$this->insertSignatory($signatory); | ||
return; | ||
|
||
case SignatoryType::REFRESHABLE: | ||
$this->updateSignatoryPublicKey($signatory); | ||
$this->updateSignatoryMetadata($signatory); | ||
break; | ||
|
||
case SignatoryType::TRUSTED: | ||
// TODO: send notice to admin | ||
throw new SignatoryConflictException(); | ||
|
||
case SignatoryType::STATIC: | ||
// TODO: send warning to admin | ||
throw new SignatoryConflictException(); | ||
} |
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.
A bit overkill to have these 4 types, are they all used and needed? Is it an admin choice?
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 set by the 3rd party ISignatoryManager
that will decide how must be handle a change of the remote public key
Signed-off-by: Maxence Lange <[email protected]>
c00446b
to
618ff23
Compare
Hello, due to the importance of having this issue merged, please let's postpone all approach-related optimization to later. If this PR checks this boxes, please let's approve and merge:
After that we should update the documentation about the config switch. Thanks! 🙏 |
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
can 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 keyISignatureManager
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