diff options
author | Julius Härtl <jus@bitgrid.net> | 2018-12-18 20:31:03 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-18 20:31:03 +0100 |
commit | b89bb4945f4105efc90626f8565a0121b8395a8d (patch) | |
tree | 95ae8d5dac6ed5e530b670bcfda2d36694102879 /lib | |
parent | aee68d4094aa8a2251de620fd06664998f02cb12 (diff) | |
parent | cc779e3d7cf8ad6fc7a97fbe1fd244cdc806abf1 (diff) |
Merge pull request #206 from nextcloud/verify-and-sign-object-RDF-normalization
linked data signature
Diffstat (limited to 'lib')
-rw-r--r-- | lib/Controller/ActivityPubController.php | 18 | ||||
-rw-r--r-- | lib/Db/NotesRequest.php | 2 | ||||
-rw-r--r-- | lib/Exceptions/LinkedDataSignatureMissingException.php | 8 | ||||
-rw-r--r-- | lib/Model/ActivityPub/ACore.php | 52 | ||||
-rw-r--r-- | lib/Model/ActivityPub/Note.php | 3 | ||||
-rw-r--r-- | lib/Model/LinkedDataSignature.php | 318 | ||||
-rw-r--r-- | lib/Service/ActivityPub/NoteService.php | 35 | ||||
-rw-r--r-- | lib/Service/ActivityService.php | 62 | ||||
-rw-r--r-- | lib/Service/ImportService.php | 9 |
9 files changed, 488 insertions, 19 deletions
diff --git a/lib/Controller/ActivityPubController.php b/lib/Controller/ActivityPubController.php index 086c65a6..1bf178ff 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; @@ -180,7 +188,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) { @@ -218,7 +229,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/Db/NotesRequest.php b/lib/Db/NotesRequest.php index 812859f0..9c03a701 100644 --- a/lib/Db/NotesRequest.php +++ b/lib/Db/NotesRequest.php @@ -133,7 +133,7 @@ class NotesRequest extends NotesRequestBuilder { $cursor->closeCursor(); if ($data === false) { - throw new NoteNotFoundException(); + throw new NoteNotFoundException('Post not found'); } return $this->parseNotesSelectSql($data); 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 @@ +<?php + +namespace OCA\Social\Exceptions; + +class LinkedDataSignatureMissingException extends \Exception { + +} + diff --git a/lib/Model/ActivityPub/ACore.php b/lib/Model/ActivityPub/ACore.php index 622ab9d9..c1593e9c 100644 --- a/lib/Model/ActivityPub/ACore.php +++ b/lib/Model/ActivityPub/ACore.php @@ -37,6 +37,7 @@ use OCA\Social\Exceptions\ActivityCantBeVerifiedException; use OCA\Social\Exceptions\InvalidOriginException; use OCA\Social\Exceptions\InvalidResourceEntryException; use OCA\Social\Exceptions\UrlCloudException; +use OCA\Social\Model\LinkedDataSignature; use OCA\Social\Service\ActivityPub\ICoreService; @@ -74,6 +75,10 @@ abstract class ACore extends Item implements JsonSerializable { /** @var ICoreService */ private $saveAs; + /** @var LinkedDataSignature */ + private $signature = null; + + /** * Core constructor. * @@ -168,6 +173,32 @@ abstract class ACore extends Item implements JsonSerializable { /** + * @return bool + */ + public function gotSignature(): bool { + return ($this->signature !== null); + } + + /** + * @return LinkedDataSignature + */ + public function getSignature(): LinkedDataSignature { + return $this->signature; + } + + /** + * @param LinkedDataSignature $signature + * + * @return ACore + */ + public function setSignature(LinkedDataSignature $signature): Acore { + $this->signature = $signature; + + return $this; + } + + + /** * @param ICoreService $class */ public function saveAs(ICoreService $class) { @@ -512,13 +543,21 @@ abstract class ACore extends Item implements JsonSerializable { * @return array */ public function jsonSerialize(): array { + + if ($this->gotSignature()) { + $this->entries['signature'] = $this->getSignature(); + } + if ($this->isRoot()) { - $this->addEntryArray( - '@context', [ - self::CONTEXT_ACTIVITYSTREAMS, - self::CONTEXT_SECURITY - ] - ); + $context = [self::CONTEXT_ACTIVITYSTREAMS]; + + if ($this->gotObject() + && $this->getObject() + ->gotSignature()) { + array_push($context, self::CONTEXT_SECURITY); + } + + $this->addEntryArray('@context', $context); } $this->addEntry('id', $this->getId()); @@ -545,7 +584,6 @@ abstract class ACore extends Item implements JsonSerializable { $this->addEntry('published', $this->getPublished()); $this->addEntryArray('tag', $this->getTags()); -// $arr = $this->getEntries(); if ($this->gotObject()) { $this->addEntryItem('object', $this->getObject()); } else { diff --git a/lib/Model/ActivityPub/Note.php b/lib/Model/ActivityPub/Note.php index 7598305e..03345ac3 100644 --- a/lib/Model/ActivityPub/Note.php +++ b/lib/Model/ActivityPub/Note.php @@ -32,6 +32,7 @@ namespace OCA\Social\Model\ActivityPub; use DateTime; use JsonSerializable; +use OCA\Social\Exceptions\InvalidResourceEntryException; use OCA\Social\Service\ActivityService; class Note extends ACore implements JsonSerializable { @@ -195,6 +196,8 @@ class Note extends ACore implements JsonSerializable { /** * @param array $data + * + * @throws InvalidResourceEntryException */ public function import(array $data) { parent::import($data); diff --git a/lib/Model/LinkedDataSignature.php b/lib/Model/LinkedDataSignature.php new file mode 100644 index 00000000..689a5091 --- /dev/null +++ b/lib/Model/LinkedDataSignature.php @@ -0,0 +1,318 @@ +<?php +declare(strict_types=1); + + +/** + * Nextcloud - Social Support + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + * + * @author Maxence Lange <maxence@artificial-owl.com> + * @copyright 2018, Maxence Lange <maxence@artificial-owl.com> + * @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 <http://www.gnu.org/licenses/>. + * + */ + + +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; + } + + + /** + * @throws LinkedDataSignatureMissingException + */ + public function sign() { + $header = [ + '@context' => 'https://w3id.org/identity/v1', + 'creator' => $this->getCreator(), + 'created' => $this->getCreated() + ]; + + $hash = $this->hashedCanonicalize($header) . $this->hashedCanonicalize($this->getObject()); + + $algo = OPENSSL_ALGO_SHA256; + if ($this->getType() === 'RsaSignature2017') { + $algo = OPENSSL_ALGO_SHA256; + } + + if (!openssl_sign($hash, $signed, $this->getPrivateKey(), $algo)) { + throw new LinkedDataSignatureMissingException(); + } + + $this->setSignatureValue(base64_encode($signed)); + } + + + /** + * @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; + } + + /** + * @param array $data + * + * @return string + */ + 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/ActivityPub/NoteService.php b/lib/Service/ActivityPub/NoteService.php index e721c24c..a17c9c14 100644 --- a/lib/Service/ActivityPub/NoteService.php +++ b/lib/Service/ActivityPub/NoteService.php @@ -31,14 +31,18 @@ declare(strict_types=1); namespace OCA\Social\Service\ActivityPub; +use daita\MySmallPhpTools\Exceptions\MalformedArrayException; use Exception; use OC\User\NoUserException; use OCA\Social\Db\NotesRequest; use OCA\Social\Exceptions\ActorDoesNotExistException; use OCA\Social\Exceptions\InvalidOriginException; +use OCA\Social\Exceptions\InvalidResourceException; use OCA\Social\Exceptions\NoteNotFoundException; +use OCA\Social\Exceptions\Request410Exception; use OCA\Social\Exceptions\RequestException; use OCA\Social\Exceptions\SocialAppConfigException; +use OCA\Social\Exceptions\UrlCloudException; use OCA\Social\Model\ActivityPub\ACore; use OCA\Social\Model\ActivityPub\Activity\Create; use OCA\Social\Model\ActivityPub\Note; @@ -258,16 +262,27 @@ class NoteService implements ICoreService { /** * @param Note $note * @param string $replyTo + * + * @throws InvalidResourceException + * @throws MalformedArrayException + * @throws NoteNotFoundException + * @throws Request410Exception + * @throws RequestException + * @throws SocialAppConfigException + * @throws UrlCloudException */ public function replyTo(Note $note, string $replyTo) { if ($replyTo === '') { return; } + $author = $this->getAuthorFromPostId($replyTo); $note->setInReplyTo($replyTo); // TODO - type can be NOT public ! $note->addInstancePath( - new InstancePath($replyTo, InstancePath::TYPE_PUBLIC, InstancePath::PRIORITY_HIGH) + new InstancePath( + $author->getSharedInbox(), InstancePath::TYPE_INBOX, InstancePath::PRIORITY_HIGH + ) ); } @@ -429,5 +444,23 @@ class NoteService implements ICoreService { return $this->notesRequest->getStreamTimeline($since, $limit, false); } + + /** + * @param $noteId + * + * @return Person + * @throws NoteNotFoundException + * @throws RequestException + * @throws SocialAppConfigException + * @throws InvalidResourceException + * @throws Request410Exception + * @throws UrlCloudException + * @throws MalformedArrayException + */ + public function getAuthorFromPostId($noteId) { + $note = $this->notesRequest->getNoteById($noteId); + + return $this->personService->getFromId($note->getAttributedTo()); + } } diff --git a/lib/Service/ActivityService.php b/lib/Service/ActivityService.php index 1dd74c0c..1b67af90 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'; @@ -160,6 +159,7 @@ class ActivityService { public function createActivity(Person $actor, ACore $item, ACore &$activity = null): string { $activity = new Create(); + $item->setParent($activity); // $this->activityStreamsService->initCore($activity); @@ -174,6 +174,7 @@ class ActivityService { // } $activity->setActor($actor); + $this->signObject($actor, $activity); return $this->request($activity); } @@ -453,6 +454,57 @@ class ActivityService { /** + * @param Person $actor + * @param ACore $object + */ + public function signObject(Person $actor, ACore &$object) { + $signature = new LinkedDataSignature(); + $signature->setPrivateKey($actor->getPrivateKey()); + $signature->setType('RsaSignature2017'); + $signature->setCreator($actor->getId() . '#main-key'); + $signature->setCreated($object->getPublished()); + $signature->setObject(json_decode(json_encode($object), true)); + + try { + $signature->sign(); + $object->setSignature($signature); + } catch (LinkedDataSignatureMissingException $e) { + } + } + + + /** + * @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 * * @return string @@ -482,7 +534,7 @@ class ActivityService { /** * @param IRequest $request * - * @return + * @return string * @throws InvalidResourceException * @throws MalformedArrayException * @throws Request410Exception @@ -492,7 +544,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 |