Skip to content
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

Merged
merged 7 commits into from
Dec 4, 2024
Merged

Signed requests #45979

merged 7 commits into from
Dec 4, 2024

Conversation

ArtificialOwl
Copy link
Member

@ArtificialOwl ArtificialOwl commented Jun 19, 2024

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:

 {
     "(request-target)": "post /path",
     "content-length": 380,
     "date": "Mon, 08 Jul 2024 14:16:20 GMT",
     "digest": "SHA-256=U7gNVUQiixe5BRbp4Tg0xCZMTcSWXXUZI2\\/xtHM40S0=",
     "host": "hostname.of.the.recipient",
     "Signature": "keyId=\"https://author.hostname/ocm#signature\",algorithm=\"sha256\",headers=\"request-target) content-length date digest host\",signature=\"DzN12OCS1rsA[...]o0VmxjQooRo6HHabg==\""
 }
  • 'content-length' is the total length of the data/content
  • 'date' is the datetime the request have been initiated
  • 'digest' is a checksum of the data/content
  • 'host' is the hostname of the recipient of the request (remote when signing outgoing request, local on incoming request)
  • 'Signature' contains the signature generated using the private key, and metadata:
    • 'keyId' is a unique id, formatted as an url. hostname is used to retrieve the public key via custom discovery
    • 'algorithm' define the algorithm used to generate signature
    • 'headers' contains a list of element used during the generation of the signature
    • 'signature' is the encrypted string, using local private key, of an array containing elements
      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 string

  • getOptions can returns an array that can contains those entries:

    • 'ttl' (300) is the lifetime (in secondes) of the signature
    • 'ttlSignatory' (86400*3) the cache lifetime on a remote signatory
    • 'extraSignatureHeaders' ([]) (list of extra headers to include to the signature)
    • 'algorithm' ('sha256') is the algorithm used to generate the signature
    • 'dateHeader' ("D, d M Y H:i:s T") the format of the 'date' header
  • 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 key

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 instance
  • getOutgoingSignedRequest 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 IClient
  • searchSignatory get a remote signatory from the database

lib/private/OCM/OCMSignatoryManager.php Fixed Show fixed Hide fixed
lib/private/OCM/OCMSignatoryManager.php Fixed Show fixed Hide fixed
lib/private/OCM/OCMSignatoryManager.php Fixed Show fixed Hide fixed
lib/private/OCM/OCMSignatoryManager.php Fixed Show fixed Hide fixed
lib/private/OCM/OCMSignatoryManager.php Fixed Show fixed Hide fixed

[$k, $v] = explode('=', $entry, 2);
preg_match('/"([^"]+)"/', $v, $varr);
if ($varr[0] !== null) {

Check failure

Code scanning / Psalm

RedundantCondition

string can never contain null
lib/private/OCM/OCMSignatoryManager.php Fixed Show fixed Hide fixed
lib/private/OCM/OCMSignatoryManager.php Fixed Show fixed Hide fixed

$parsed = parse_url($uri);
$signedRequest->setHost($parsed['host'])
->setAlgorithm($options['algorithm'] ?? 'sha256')

Check failure

Code scanning / Psalm

UndefinedVariable

Cannot find referenced variable $options
apps/files_sharing/lib/External/Cache.php Fixed Show fixed Hide fixed
@ArtificialOwl ArtificialOwl force-pushed the enh/noid/signed-request branch from 58d3efd to f45a2cb Compare July 2, 2024 04:46
// 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

Argument 2 of OCP\IAppConfig::getValueBool cannot be false, string value expected
if ($signatory === null) {
throw new SignatoryNotFoundException('empty result from getRemoteSignatory');
}
if ($signatory->getKeyId() !== $signedRequest->getKeyId()) {

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod

Method OCP\Security\Signature\Model\IIncomingSignedRequest::getKeyId does not exist
@ArtificialOwl ArtificialOwl force-pushed the enh/noid/signed-request branch from f45a2cb to a3dda22 Compare July 4, 2024 16:03
if (!str_contains($entry, '@')) {
throw new IncomingRequestException('entry does not contains @');
}
[, $instance] = explode('@', $entry, 2);

Check notice

Code scanning / Psalm

PossiblyUndefinedArrayOffset

Possibly undefined array key
@ArtificialOwl ArtificialOwl force-pushed the enh/noid/signed-request branch from 61cc09b to 9ef90c0 Compare July 10, 2024 12:01
* @inheritDoc
*
* @param string $publicKey
* @return IKeyPair

Check failure

Code scanning / Psalm

MismatchingDocblockReturnType

Docblock has incorrect return type 'OCP\Security\PublicPrivateKeyPairs\Model\IKeyPair', should be 'OC\Security\PublicPrivateKeyPairs\Model\KeyPair'
* @inheritDoc
*
* @param string $privateKey
* @return IKeyPair

Check failure

Code scanning / Psalm

MismatchingDocblockReturnType

Docblock has incorrect return type 'OCP\Security\PublicPrivateKeyPairs\Model\IKeyPair', should be 'OC\Security\PublicPrivateKeyPairs\Model\KeyPair'
* @inheritDoc
*
* @param array $options
* @return IKeyPair

Check failure

Code scanning / Psalm

MismatchingDocblockReturnType

Docblock has incorrect return type 'OCP\Security\PublicPrivateKeyPairs\Model\IKeyPair', should be 'OC\Security\PublicPrivateKeyPairs\Model\KeyPair'
* @inheritDoc
*
* @param string $host
* @return IOutgoingSignedRequest

Check failure

Code scanning / Psalm

MismatchingDocblockReturnType

Docblock has incorrect return type 'OCP\Security\Signature\Model\IOutgoingSignedRequest', should be 'OC\Security\Signature\Model\OutgoingSignedRequest'
* @param string $key
* @param string|int|float|bool|array $value
*
* @return IOutgoingSignedRequest

Check failure

Code scanning / Psalm

MismatchingDocblockReturnType

Docblock has incorrect return type 'OCP\Security\Signature\Model\IOutgoingSignedRequest', should be 'OC\Security\Signature\Model\OutgoingSignedRequest'
*
* @param string $estimated
*
* @return IOutgoingSignedRequest

Check failure

Code scanning / Psalm

MismatchingDocblockReturnType

Docblock has incorrect return type 'OCP\Security\Signature\Model\IOutgoingSignedRequest', should be 'OC\Security\Signature\Model\OutgoingSignedRequest'
*
* @param string $algorithm
*
* @return IOutgoingSignedRequest

Check failure

Code scanning / Psalm

MismatchingDocblockReturnType

Docblock has incorrect return type 'OCP\Security\Signature\Model\IOutgoingSignedRequest', should be 'OC\Security\Signature\Model\OutgoingSignedRequest'
* @inheritDoc
*
* @param array $signatureHeader
* @return ISignedRequest

Check failure

Code scanning / Psalm

MismatchingDocblockReturnType

Docblock has incorrect return type 'OCP\Security\Signature\Model\ISignedRequest', should be 'OC\Security\Signature\Model\SignedRequest'
/**
* @inheritDoc
*
* @return ISignatory

Check failure

Code scanning / Psalm

InvalidNullableReturnType

The declared return type 'OCP\Security\Signature\Model\ISignatory' for OC\Security\Signature\Model\SignedRequest::getSignatory is not nullable, but 'OCP\Security\Signature\Model\ISignatory|null' contains null
* @since 30.0.0
*/
public function getSignatory(): ISignatory {
return $this->signatory;

Check failure

Code scanning / Psalm

NullableReturnStatement

The declared return type 'OCP\Security\Signature\Model\ISignatory' for OC\Security\Signature\Model\SignedRequest::getSignatory is not nullable, but the function returns 'OCP\Security\Signature\Model\ISignatory|null'
@ArtificialOwl ArtificialOwl added the 3. to review Waiting for reviews label Jul 10, 2024
@ArtificialOwl ArtificialOwl added this to the Nextcloud 30 milestone Jul 10, 2024
@ArtificialOwl ArtificialOwl marked this pull request as ready for review July 10, 2024 16:24
to.json Outdated Show resolved Hide resolved
// 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'] ?? '');
Copy link
Member

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?

Copy link
Member Author

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)

Copy link
Member

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

Copy link
Member Author

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

lib/private/Federation/CloudFederationProviderManager.php Outdated Show resolved Hide resolved
lib/public/Security/Signature/SignatureAlgorithm.php Outdated Show resolved Hide resolved
lib/public/Security/Signature/Model/SignatoryType.php Outdated Show resolved Hide resolved
lib/private/Security/Signature/Model/SignedRequest.php Outdated Show resolved Hide resolved
lib/private/Security/Signature/SignatureManager.php Outdated Show resolved Hide resolved
core/Migrations/Version30000Date20240101084401.php Outdated Show resolved Hide resolved
lib/private/Security/Signature/SignatureManager.php Outdated Show resolved Hide resolved
lib/private/Security/Signature/SignatureManager.php Outdated Show resolved Hide resolved
This was referenced Jul 30, 2024
This was referenced Aug 5, 2024
@skjnldsv skjnldsv mentioned this pull request Aug 13, 2024
@skjnldsv skjnldsv modified the milestones: Nextcloud 30, Nextcloud 31 Aug 14, 2024
@skjnldsv skjnldsv added 2. developing Work in progress and removed 3. to review Waiting for reviews labels Oct 29, 2024
@ArtificialOwl ArtificialOwl force-pushed the enh/noid/signed-request branch 4 times, most recently from 444f9e7 to f3a1684 Compare October 31, 2024 12:21
@ArtificialOwl ArtificialOwl force-pushed the enh/noid/signed-request branch 5 times, most recently from 8807d38 to 5ac1e9a Compare December 2, 2024 12:31
lib/public/OCM/IOCMProvider.php Outdated Show resolved Hide resolved
lib/unstable/Security/Signature/IIncomingSignedRequest.php Outdated Show resolved Hide resolved
lib/unstable/Security/Signature/ISignedRequest.php Outdated Show resolved Hide resolved
lib/unstable/Security/Signature/ISignedRequest.php Outdated Show resolved Hide resolved
lib/unstable/Security/Signature/ISignedRequest.php Outdated Show resolved Hide resolved
lib/unstable/Security/Signature/Model/Signatory.php Outdated Show resolved Hide resolved
@ArtificialOwl ArtificialOwl force-pushed the enh/noid/signed-request branch 4 times, most recently from 952b97b to 58e8195 Compare December 3, 2024 17:01
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
@ArtificialOwl ArtificialOwl force-pushed the enh/noid/signed-request branch from 55a2663 to 15b7228 Compare December 4, 2024 10:31
@ArtificialOwl ArtificialOwl merged commit c54784c into master Dec 4, 2024
190 checks passed
@ArtificialOwl ArtificialOwl deleted the enh/noid/signed-request branch December 4, 2024 10:48
]);
$table->addColumn('metadata', Types::TEXT, [
'notnull' => true,
'default' => '[]'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mysql> DESCRIBE oc_sec_signatory;
+--------------+-----------------+------+-----+---------+----------------+
| Field        | Type            | Null | Key | Default | Extra          |
+--------------+-----------------+------+-----+---------+----------------+
| id           | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| key_id_sum   | varchar(127)    | NO   | MUL | NULL    |                |
| key_id       | varchar(512)    | NO   |     | NULL    |                |
| host         | varchar(512)    | NO   |     | NULL    |                |
| provider_id  | varchar(31)     | NO   | MUL | NULL    |                |
| account      | varchar(127)    | YES  |     |         |                |
| public_key   | longtext        | NO   |     | NULL    |                |
| metadata     | longtext        | NO   |     | NULL    |                |
| type         | smallint        | NO   |     | 9       |                |
| status       | smallint        | NO   |     | 0       |                |
| creation     | int unsigned    | YES  |     | 0       |                |
| last_updated | int unsigned    | YES  |     | 0       |                |
+--------------+-----------------+------+-----+---------+----------------+
12 rows in set (0,00 sec)

Seems this does not work. TEXT fields can not have a default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks Talk integration tests on:
MySQL

{"Exception":"OC\\DB\\Exceptions\\DbalException","Message":"An exception occurred while executing a query: SQLSTATE[HY000]: General error: 1364 Field 'metadata' doesn't have a default value"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is another failure with oracle catched by richdocuments integration tests:

{"Exception":"Exception","Message":"Cannot assign null to property NCU\\Security\\Signature\\Model\\Signatory::$account of type string in file '\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/public\/AppFramework\/Db\/Entity.php' line 149","Code":0,"Trace":[{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/AppFramework\/App.php","line":161,"function":"dispatch","class":"OC\\AppFramework\\Http\\Dispatcher","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/Route\/Router.php","line":306,"function":"main","class":"OC\\AppFramework\\App","type":"::"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/base.php","line":1019,"function":"match","class":"OC\\Route\\Router","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/index.php","line":24,"function":"handleRequest","class":"OC","type":"::"}],"File":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/AppFramework\/Http\/Dispatcher.php","Line":146,"Previous":{"Exception":"TypeError","Message":"Cannot assign null to property NCU\\Security\\Signature\\Model\\Signatory::$account of type string","Code":0,"Trace":[{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/public\/AppFramework\/Db\/Entity.php","line":59,"function":"setter","class":"OCP\\AppFramework\\Db\\Entity","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/public\/AppFramework\/Db\/QBMapper.php","line":315,"function":"fromRow","class":"OCP\\AppFramework\\Db\\Entity","type":"::"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/public\/AppFramework\/Db\/QBMapper.php","line":375,"function":"mapRowToEntity","class":"OCP\\AppFramework\\Db\\QBMapper","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/Security\/Signature\/Db\/SignatoryMapper.php","line":56,"function":"findEntity","class":"OCP\\AppFramework\\Db\\QBMapper","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/Security\/Signature\/SignatureManager.php","line":326,"function":"getByKeyId","class":"OC\\Security\\Signature\\Db\\SignatoryMapper","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/Security\/Signature\/SignatureManager.php","line":140,"function":"getStoredSignatory","class":"OC\\Security\\Signature\\SignatureManager","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/Security\/Signature\/SignatureManager.php","line":108,"function":"confirmIncomingRequestSignature","class":"OC\\Security\\Signature\\SignatureManager","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/apps\/cloud_federation_api\/lib\/Controller\/RequestHandlerController.php","line":339,"function":"getIncomingSignedRequest","class":"OC\\Security\\Signature\\SignatureManager","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/apps\/cloud_federation_api\/lib\/Controller\/RequestHandlerController.php","line":104,"function":"getSignedRequest","class":"OCA\\CloudFederationAPI\\Controller\\RequestHandlerController","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/AppFramework\/Http\/Dispatcher.php","line":200,"function":"addShare","class":"OCA\\CloudFederationAPI\\Controller\\RequestHandlerController","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/AppFramework\/Http\/Dispatcher.php","line":114,"function":"executeController","class":"OC\\AppFramework\\Http\\Dispatcher","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/AppFramework\/App.php","line":161,"function":"dispatch","class":"OC\\AppFramework\\Http\\Dispatcher","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/private\/Route\/Router.php","line":306,"function":"main","class":"OC\\AppFramework\\App","type":"::"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/base.php","line":1019,"function":"match","class":"OC\\Route\\Router","type":"->"},{"file":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/index.php","line":24,"function":"handleRequest","class":"OC","type":"::"}],"File":"\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/public\/AppFramework\/Db\/Entity.php","Line":149},"message":"Cannot assign null to property NCU\\Security\\Signature\\Model\\Signatory::$account of type string in file '\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/public\/AppFramework\/Db\/Entity.php' line 149","exception":{},"CustomMessage":"Cannot assign null to property NCU\\Security\\Signature\\Model\\Signatory::$account of type string in file '\/home\/runner\/actions-runner\/_work\/richdocuments\/richdocuments\/lib\/public\/AppFramework\/Db\/Entity.php' line 149"}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: ☑️ Done
Development

Successfully merging this pull request may close these issues.

7 participants