From b99c3d4b67396f285da6202a196fbc1cfa4baa52 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Tue, 11 Dec 2018 10:42:33 -0100 Subject: [PATCH] VERIFY linked data signature --- composer.json | 3 +- composer.lock | 56 +++- lib/Controller/ActivityPubController.php | 18 +- .../LinkedDataSignatureMissingException.php | 8 + lib/Model/LinkedDataSignature.php | 293 ++++++++++++++++++ lib/Service/ActivityService.php | 40 ++- lib/Service/ImportService.php | 9 +- 7 files changed, 411 insertions(+), 16 deletions(-) create mode 100644 lib/Exceptions/LinkedDataSignatureMissingException.php create mode 100644 lib/Model/LinkedDataSignature.php diff --git a/composer.json b/composer.json index d83a222f..e0b5708d 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ } ], "require": { - "daita/my-small-php-tools": "dev-master" + "daita/my-small-php-tools": "dev-master", + "digitalbazaar/json-ld": "0.4.7" }, "require-dev": { "jakub-onderka/php-parallel-lint": "^1.0" diff --git a/composer.lock b/composer.lock index 83f63fdb..81e358ae 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "17d7e7fc4c9cdd0ddc5d6166607fce8f", + "content-hash": "2c21b8c338f3d2f929f349df19edbc0e", "packages": [ { "name": "daita/my-small-php-tools", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/daita/my-small-php-tools.git", - "reference": "405a5e6afadfd7c0630cf33b9e48a39f6938f605" + "reference": "29754f18951856a22c0fd5fc388b6162ea98fe8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/daita/my-small-php-tools/zipball/405a5e6afadfd7c0630cf33b9e48a39f6938f605", - "reference": "405a5e6afadfd7c0630cf33b9e48a39f6938f605", + "url": "https://api.github.com/repos/daita/my-small-php-tools/zipball/29754f18951856a22c0fd5fc388b6162ea98fe8a", + "reference": "29754f18951856a22c0fd5fc388b6162ea98fe8a", "shasum": "" }, "require": { @@ -40,7 +40,53 @@ } ], "description": "My small PHP Tools", - "time": "2018-12-08T15:17:26+00:00" + "time": "2018-12-18T00:38:01+00:00" + }, + { + "name": "digitalbazaar/json-ld", + "version": "0.4.7", + "source": { + "type": "git", + "url": "https://github.com/digitalbazaar/php-json-ld.git", + "reference": "dc1bd23f0ee2efd27ccf636d32d2738dabcee182" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/digitalbazaar/php-json-ld/zipball/dc1bd23f0ee2efd27ccf636d32d2738dabcee182", + "reference": "dc1bd23f0ee2efd27ccf636d32d2738dabcee182", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "files": [ + "jsonld.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Digital Bazaar, Inc.", + "email": "support@digitalbazaar.com" + } + ], + "description": "A JSON-LD Processor and API implementation in PHP.", + "homepage": "https://github.com/digitalbazaar/php-json-ld", + "keywords": [ + "JSON-LD", + "Linked Data", + "RDF", + "Semantic Web", + "json", + "jsonld" + ], + "time": "2016-04-25T04:17:52+00:00" } ], "packages-dev": [ diff --git a/lib/Controller/ActivityPubController.php b/lib/Controller/ActivityPubController.php index b686bf65..8c63bc61 100644 --- a/lib/Controller/ActivityPubController.php +++ b/lib/Controller/ActivityPubController.php @@ -35,8 +35,16 @@ use Exception; use OC\AppFramework\Http; use OCA\Social\AppInfo\Application; use OCA\Social\Db\NotesRequest; +use OCA\Social\Exceptions\ActivityPubFormatException; +use OCA\Social\Exceptions\InvalidResourceEntryException; +use OCA\Social\Exceptions\InvalidResourceException; +use OCA\Social\Exceptions\Request410Exception; +use OCA\Social\Exceptions\RequestException; use OCA\Social\Exceptions\SignatureIsGoneException; +use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Exceptions\UnknownItemException; +use OCA\Social\Exceptions\UrlCloudException; +use OCA\Social\Model\ActivityPub\ACore; use OCA\Social\Service\ActivityPub\FollowService; use OCA\Social\Service\ActivityPub\PersonService; use OCA\Social\Service\ActivityService; @@ -181,7 +189,10 @@ class ActivityPubController extends Controller { $origin = $this->activityService->checkRequest($this->request); $activity = $this->importService->importFromJson($body); - $activity->setOrigin($origin); + if (!$this->activityService->checkObject($activity)) { + $activity->setOrigin($origin); + } + try { $this->importService->parseIncomingRequest($activity); } catch (UnknownItemException $e) { @@ -219,7 +230,10 @@ class ActivityPubController extends Controller { // $actor = $this->actorService->getActor($username); $activity = $this->importService->importFromJson($body); - $activity->setOrigin($origin); + if (!$this->activityService->checkObject($activity)) { + $activity->setOrigin($origin); + } + try { $this->importService->parseIncomingRequest($activity); } catch (UnknownItemException $e) { diff --git a/lib/Exceptions/LinkedDataSignatureMissingException.php b/lib/Exceptions/LinkedDataSignatureMissingException.php new file mode 100644 index 00000000..f49db7b6 --- /dev/null +++ b/lib/Exceptions/LinkedDataSignatureMissingException.php @@ -0,0 +1,8 @@ + + * @copyright 2018, Maxence Lange + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Social\Model; + + +use daita\MySmallPhpTools\Traits\TArrayTools; +use JsonSerializable; +use OCA\Social\Exceptions\LinkedDataSignatureMissingException; +use OCA\Social\Model\ActivityPub\ACore; + + +/** + * Class InstancePath + * + * @package OCA\Social\Model + */ +class LinkedDataSignature implements JsonSerializable { + + + use TArrayTools; + + /** @var string */ + private $type = ''; + + /** @var string */ + private $creator = ''; + + /** @var string */ + private $created = ''; + + /** @var string */ + private $signatureValue = ''; + + /** @var string */ + private $privateKey = ''; + + /** @var string */ + private $publicKey = ''; + + /** @var array */ + private $object = []; + + + /** + * LinkedDataSignature constructor. + */ + public function __construct() { + } + + /** + * @return string + */ + public function getType(): string { + return $this->type; + } + + /** + * @param string $type + * + * @return LinkedDataSignature + */ + public function setType(string $type): LinkedDataSignature { + $this->type = $type; + + return $this; + } + + /** + * @return string + */ + public function getCreator(): string { + return $this->creator; + } + + /** + * @param string $creator + * + * @return LinkedDataSignature + */ + public function setCreator(string $creator): LinkedDataSignature { + $this->creator = $creator; + + return $this; + } + + /** + * @return string + */ + public function getCreated(): string { + return $this->created; + } + + /** + * @param string $created + * + * @return LinkedDataSignature + */ + public function setCreated(string $created): LinkedDataSignature { + $this->created = $created; + + return $this; + } + + /** + * @return string + */ + public function getSignatureValue(): string { + return $this->signatureValue; + } + + /** + * @param string $signatureValue + * + * @return LinkedDataSignature + */ + public function setSignatureValue(string $signatureValue): LinkedDataSignature { + $this->signatureValue = $signatureValue; + + return $this; + } + + + /** + * @return array + */ + public function getObject(): array { + return $this->object; + } + + /** + * @param array $object + * + * @return LinkedDataSignature + */ + public function setObject(array $object): LinkedDataSignature { + $this->object = $object; + + return $this; + } + + + /** + * @return string + */ + public function getPrivateKey(): string { + return $this->privateKey; + } + + /** + * @param string $privateKey + * + * @return LinkedDataSignature + */ + public function setPrivateKey(string $privateKey): LinkedDataSignature { + $this->privateKey = $privateKey; + + return $this; + } + + + /** + * @return string + */ + public function getPublicKey(): string { + return $this->publicKey; + } + + /** + * @param string $publicKey + * + * @return LinkedDataSignature + */ + public function setPublicKey(string $publicKey): LinkedDataSignature { + $this->publicKey = $publicKey; + + return $this; + } + + + public function sign() { + } + + + /** + * @return bool + */ + public function verify(): bool { + + $header = [ + '@context' => 'https://w3id.org/identity/v1', + 'creator' => $this->getCreator(), + 'created' => $this->getCreated() + ]; + + $hash = $this->hashedCanonicalize($header) . $this->hashedCanonicalize($this->getObject()); + $signed = base64_decode($this->getSignatureValue()); + + $algo = OPENSSL_ALGO_SHA256; + if ($this->getType() === 'RsaSignature2017') { + $algo = OPENSSL_ALGO_SHA256; + } + + if (openssl_verify($hash, $signed, $this->getPublicKey(), $algo) === 1) { + return true; + } + + return false; + } + + + private function hashedCanonicalize(array $data): string { + $object = json_decode(json_encode($data), false); + $res = jsonld_normalize( + $object, + [ + 'algorithm' => 'URDNA2015', + 'format' => 'application/nquads' + ] + ); + + return hash('sha256', $res); + } + + + /** + * @param array $data + * + * @throws LinkedDataSignatureMissingException + */ + public function import(array $data) { + + if (!in_array(ACore::CONTEXT_SECURITY, $this->getArray('@context', $data, []))) { + throw new LinkedDataSignatureMissingException(); + } + + $signature = $this->getArray('signature', $data, []); + if ($signature === []) { + throw new LinkedDataSignatureMissingException(); + } + + $this->setType($this->get('type', $signature, '')); + $this->setCreator($this->get('creator', $signature, '')); + $this->setCreated($this->get('created', $signature, '')); + $this->setSignatureValue($this->get('signatureValue', $signature, '')); + + unset($data['signature']); + + $this->setObject($data); + } + + + /** + * @return array + */ + public function jsonSerialize(): array { + return [ + 'type' => $this->getType(), + 'creator' => $this->getCreator(), + 'created' => $this->getCreated(), + 'signatureValue' => $this->getSignatureValue() + ]; + } + +} + diff --git a/lib/Service/ActivityService.php b/lib/Service/ActivityService.php index 1dd74c0c..3d88f2f1 100644 --- a/lib/Service/ActivityService.php +++ b/lib/Service/ActivityService.php @@ -42,6 +42,7 @@ use OCA\Social\Exceptions\ActorDoesNotExistException; use OCA\Social\Exceptions\EmptyQueueException; use OCA\Social\Exceptions\InvalidOriginException; use OCA\Social\Exceptions\InvalidResourceException; +use OCA\Social\Exceptions\LinkedDataSignatureMissingException; use OCA\Social\Exceptions\NoHighPriorityRequestException; use OCA\Social\Exceptions\QueueStatusException; use OCA\Social\Exceptions\Request410Exception; @@ -56,6 +57,7 @@ use OCA\Social\Model\ActivityPub\Activity\Delete; use OCA\Social\Model\ActivityPub\Person; use OCA\Social\Model\ActivityPub\Tombstone; use OCA\Social\Model\InstancePath; +use OCA\Social\Model\LinkedDataSignature; use OCA\Social\Model\RequestQueue; use OCA\Social\Service\ActivityPub\PersonService; use OCP\IRequest; @@ -72,9 +74,6 @@ class ActivityService { const TIMEOUT_ASYNC = 5; const TIMEOUT_SERVICE = 10; - const CONTEXT_ACTIVITYSTREAMS = 'https://www.w3.org/ns/activitystreams'; - const CONTEXT_SECURITY = 'https://w3id.org/security/v1'; - const TO_PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; const DATE_FORMAT = 'D, d M Y H:i:s T'; @@ -452,6 +451,37 @@ class ActivityService { } + /** + * @param ACore $object + * + * @return bool + * @throws InvalidResourceException + * @throws Request410Exception + * @throws RequestException + * @throws SocialAppConfigException + * @throws UrlCloudException + * @throws InvalidOriginException + */ + public function checkObject(ACore $object): bool { + try { + $actorId = $object->getActorId(); + + $signature = new LinkedDataSignature(); + $signature->import(json_decode($object->getSource(), true)); + $signature->setPublicKey($this->retrieveKey($actorId)); + + if ($signature->verify()) { + $object->setOrigin($this->getKeyOrigin($actorId)); + + return true; + } + } catch (LinkedDataSignatureMissingException $e) { + } + + return false; + } + + /** * @param ACore $activity * @@ -482,7 +512,7 @@ class ActivityService { /** * @param IRequest $request * - * @return + * @return string * @throws InvalidResourceException * @throws MalformedArrayException * @throws Request410Exception @@ -492,7 +522,7 @@ class ActivityService { * @throws UrlCloudException * @throws InvalidOriginException */ - private function checkSignature(IRequest $request) { + private function checkSignature(IRequest $request): string { $signatureHeader = $request->getHeader('Signature'); $sign = $this->parseSignatureHeader($signatureHeader); diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index 73072a7e..d34424e2 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -36,6 +36,7 @@ use Exception; use OCA\Social\Exceptions\ActivityPubFormatException; use OCA\Social\Exceptions\InvalidResourceEntryException; use OCA\Social\Exceptions\InvalidResourceException; +use OCA\Social\Exceptions\LinkedDataSignatureMissingException; use OCA\Social\Exceptions\SocialAppConfigException; use OCA\Social\Exceptions\UnknownItemException; use OCA\Social\Exceptions\UrlCloudException; @@ -50,6 +51,7 @@ use OCA\Social\Model\ActivityPub\Follow; use OCA\Social\Model\ActivityPub\Image; use OCA\Social\Model\ActivityPub\Note; use OCA\Social\Model\ActivityPub\Activity\Undo; +use OCA\Social\Model\LinkedDataSignature; use OCA\Social\Service\ActivityPub\DeleteService; use OCA\Social\Service\ActivityPub\FollowService; use OCA\Social\Service\ActivityPub\NoteService; @@ -108,22 +110,23 @@ class ImportService { * @param string $json * * @return ACore + * @throws ActivityPubFormatException + * @throws InvalidResourceEntryException + * @throws SocialAppConfigException * @throws UnknownItemException * @throws UrlCloudException - * @throws SocialAppConfigException - * @throws ActivityPubFormatException */ public function importFromJson(string $json) { $data = json_decode($json, true); if (!is_array($data)) { throw new ActivityPubFormatException(); } + $activity = $this->importFromData($data, null); return $activity; } - /** * @param array $data * @param ACore $root