2018-12-17 09:12:27 +00:00
|
|
|
<?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\Service;
|
|
|
|
|
|
|
|
|
|
|
|
use daita\MySmallPhpTools\Exceptions\MalformedArrayException;
|
2018-12-20 12:22:48 +00:00
|
|
|
use daita\MySmallPhpTools\Model\Request;
|
2018-12-17 09:12:27 +00:00
|
|
|
use daita\MySmallPhpTools\Traits\TArrayTools;
|
|
|
|
use DateTime;
|
|
|
|
use Exception;
|
2018-12-20 12:22:48 +00:00
|
|
|
use OCA\Social\Db\ActorsRequest;
|
|
|
|
use OCA\Social\Exceptions\ActorDoesNotExistException;
|
2018-12-17 09:12:27 +00:00
|
|
|
use OCA\Social\Exceptions\InvalidOriginException;
|
|
|
|
use OCA\Social\Exceptions\InvalidResourceException;
|
2018-12-19 01:14:24 +00:00
|
|
|
use OCA\Social\Exceptions\LinkedDataSignatureMissingException;
|
2018-12-17 09:12:27 +00:00
|
|
|
use OCA\Social\Exceptions\RedundancyLimitException;
|
|
|
|
use OCA\Social\Exceptions\Request410Exception;
|
|
|
|
use OCA\Social\Exceptions\RequestException;
|
|
|
|
use OCA\Social\Exceptions\SignatureException;
|
|
|
|
use OCA\Social\Exceptions\SignatureIsGoneException;
|
|
|
|
use OCA\Social\Exceptions\SocialAppConfigException;
|
|
|
|
use OCA\Social\Exceptions\UnknownItemException;
|
2018-12-19 01:14:24 +00:00
|
|
|
use OCA\Social\Model\ActivityPub\ACore;
|
2018-12-17 09:12:27 +00:00
|
|
|
use OCA\Social\Model\ActivityPub\Actor\Person;
|
2018-12-19 01:14:24 +00:00
|
|
|
use OCA\Social\Model\LinkedDataSignature;
|
2018-12-20 12:22:48 +00:00
|
|
|
use OCA\Social\Model\RequestQueue;
|
2018-12-17 09:12:27 +00:00
|
|
|
use OCP\IRequest;
|
|
|
|
|
|
|
|
class SignatureService {
|
|
|
|
|
|
|
|
|
|
|
|
use TArrayTools;
|
|
|
|
|
|
|
|
|
2018-12-19 17:50:17 +00:00
|
|
|
const ORIGIN_HEADER = 1;
|
|
|
|
const ORIGIN_SIGNATURE = 2;
|
|
|
|
|
|
|
|
|
2018-12-20 12:22:48 +00:00
|
|
|
const DATE_FORMAT = 'D, d M Y H:i:s T';
|
2018-12-17 09:12:27 +00:00
|
|
|
const DATE_DELAY = 30;
|
|
|
|
|
|
|
|
|
|
|
|
/** @var CacheActorService */
|
|
|
|
private $cacheActorService;
|
|
|
|
|
2018-12-20 12:22:48 +00:00
|
|
|
/** @var ActorsRequest */
|
|
|
|
private $actorsRequest;
|
|
|
|
|
2018-12-17 09:12:27 +00:00
|
|
|
/** @var CurlService */
|
|
|
|
private $curlService;
|
|
|
|
|
|
|
|
/** @var ConfigService */
|
|
|
|
private $configService;
|
|
|
|
|
|
|
|
/** @var MiscService */
|
|
|
|
private $miscService;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* ActivityService constructor.
|
|
|
|
*
|
2018-12-20 12:22:48 +00:00
|
|
|
* @param ActorsRequest $actorsRequest
|
2018-12-17 09:12:27 +00:00
|
|
|
* @param CacheActorService $cacheActorService
|
|
|
|
* @param CurlService $curlService
|
|
|
|
* @param ConfigService $configService
|
|
|
|
* @param MiscService $miscService
|
|
|
|
*/
|
|
|
|
public function __construct(
|
2018-12-20 12:22:48 +00:00
|
|
|
ActorsRequest $actorsRequest, CacheActorService $cacheActorService,
|
|
|
|
CurlService $curlService,
|
2018-12-17 09:12:27 +00:00
|
|
|
ConfigService $configService, MiscService $miscService
|
|
|
|
) {
|
2018-12-20 12:22:48 +00:00
|
|
|
$this->actorsRequest = $actorsRequest;
|
2018-12-17 09:12:27 +00:00
|
|
|
$this->cacheActorService = $cacheActorService;
|
|
|
|
$this->curlService = $curlService;
|
|
|
|
$this->configService = $configService;
|
|
|
|
$this->miscService = $miscService;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Person $actor
|
|
|
|
*/
|
|
|
|
public function generateKeys(Person &$actor) {
|
|
|
|
$res = openssl_pkey_new(
|
|
|
|
[
|
|
|
|
"digest_alg" => "rsa",
|
|
|
|
"private_key_bits" => 2048,
|
|
|
|
"private_key_type" => OPENSSL_KEYTYPE_RSA,
|
|
|
|
]
|
|
|
|
);
|
|
|
|
|
|
|
|
openssl_pkey_export($res, $privateKey);
|
|
|
|
$publicKey = openssl_pkey_get_details($res)['key'];
|
|
|
|
|
|
|
|
$actor->setPublicKey($publicKey);
|
|
|
|
$actor->setPrivateKey($privateKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-12-20 12:22:48 +00:00
|
|
|
/**
|
|
|
|
* @param Request $request
|
|
|
|
* @param RequestQueue $queue
|
|
|
|
*
|
|
|
|
* @throws ActorDoesNotExistException
|
|
|
|
* @throws SocialAppConfigException
|
|
|
|
*/
|
|
|
|
public function signRequest(Request $request, RequestQueue $queue) {
|
|
|
|
$date = gmdate(self::DATE_FORMAT);
|
|
|
|
$path = $queue->getInstance();
|
|
|
|
|
|
|
|
$localActor = $this->actorsRequest->getFromId($queue->getAuthor());
|
|
|
|
|
|
|
|
$localActorLink =
|
|
|
|
$this->configService->getUrlSocial() . '@' . $localActor->getPreferredUsername();
|
|
|
|
$signature = "(request-target): post " . $path->getPath() . "\nhost: " . $path->getAddress()
|
|
|
|
. "\ndate: " . $date;
|
|
|
|
|
|
|
|
openssl_sign($signature, $signed, $localActor->getPrivateKey(), OPENSSL_ALGO_SHA256);
|
|
|
|
$signed = base64_encode($signed);
|
|
|
|
|
2018-12-20 12:40:34 +00:00
|
|
|
$header = 'keyId="' . $localActorLink
|
|
|
|
. '",algorithm="rsa-sha256",headers="(request-target) host date",signature="'
|
2018-12-20 12:22:48 +00:00
|
|
|
. $signed . '"';
|
|
|
|
|
2018-12-20 12:26:39 +00:00
|
|
|
$request->addHeader('Host: ' . $path->getAddress());
|
2018-12-20 12:22:48 +00:00
|
|
|
$request->addHeader('Date: ' . $date);
|
|
|
|
$request->addHeader('Signature: ' . $header);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-12-17 09:12:27 +00:00
|
|
|
/**
|
|
|
|
* @param IRequest $request
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
* @throws InvalidOriginException
|
|
|
|
* @throws InvalidResourceException
|
|
|
|
* @throws MalformedArrayException
|
|
|
|
* @throws RedundancyLimitException
|
|
|
|
* @throws RequestException
|
|
|
|
* @throws SignatureException
|
|
|
|
* @throws SignatureIsGoneException
|
|
|
|
* @throws SocialAppConfigException
|
|
|
|
* @throws UnknownItemException
|
|
|
|
*/
|
|
|
|
public function checkRequest(IRequest $request): string {
|
|
|
|
$dTime = new DateTime($request->getHeader('date'));
|
2018-12-20 12:22:48 +00:00
|
|
|
$dTime->format(self::DATE_FORMAT);
|
2018-12-17 09:12:27 +00:00
|
|
|
|
|
|
|
if ($dTime->getTimestamp() < (time() - self::DATE_DELAY)) {
|
|
|
|
throw new SignatureException('object is too old');
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$origin = $this->checkRequestSignature($request);
|
|
|
|
} catch (Request410Exception $e) {
|
|
|
|
throw new SignatureIsGoneException();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $origin;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-12-19 01:14:24 +00:00
|
|
|
/**
|
|
|
|
* @param ACore $object
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
* @throws InvalidOriginException
|
|
|
|
* @throws InvalidResourceException
|
|
|
|
* @throws MalformedArrayException
|
|
|
|
* @throws RedundancyLimitException
|
|
|
|
* @throws Request410Exception
|
|
|
|
* @throws RequestException
|
|
|
|
* @throws SocialAppConfigException
|
|
|
|
* @throws UnknownItemException
|
|
|
|
*/
|
|
|
|
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()) {
|
2018-12-20 12:22:48 +00:00
|
|
|
$object->setOrigin(
|
|
|
|
$this->getKeyOrigin($actorId), SignatureService::ORIGIN_SIGNATURE
|
|
|
|
);
|
2018-12-19 01:14:24 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} catch (LinkedDataSignatureMissingException $e) {
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-12-17 09:12:27 +00:00
|
|
|
/**
|
|
|
|
* @param IRequest $request
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
* @throws InvalidOriginException
|
|
|
|
* @throws InvalidResourceException
|
|
|
|
* @throws MalformedArrayException
|
|
|
|
* @throws RedundancyLimitException
|
|
|
|
* @throws Request410Exception
|
|
|
|
* @throws RequestException
|
|
|
|
* @throws SignatureException
|
|
|
|
* @throws SocialAppConfigException
|
|
|
|
* @throws UnknownItemException
|
|
|
|
*/
|
|
|
|
private function checkRequestSignature(IRequest $request): string {
|
|
|
|
$signatureHeader = $request->getHeader('Signature');
|
|
|
|
|
|
|
|
$sign = $this->parseSignatureHeader($signatureHeader);
|
|
|
|
$this->mustContains(['keyId', 'headers', 'signature'], $sign);
|
|
|
|
|
|
|
|
$keyId = $sign['keyId'];
|
|
|
|
$origin = $this->getKeyOrigin($keyId);
|
|
|
|
|
|
|
|
$headers = $sign['headers'];
|
|
|
|
$signed = base64_decode($sign['signature']);
|
|
|
|
$estimated = $this->generateEstimatedSignature($headers, $request);
|
|
|
|
|
|
|
|
$publicKey = $this->retrieveKey($keyId);
|
2018-12-20 16:51:09 +00:00
|
|
|
$algorithm = $this->getAlgorithmFromSignature($sign);
|
2018-12-20 12:40:34 +00:00
|
|
|
if ($publicKey === ''
|
|
|
|
|| openssl_verify($estimated, $signed, $publicKey, $algorithm) !== 1) {
|
2018-12-17 09:12:27 +00:00
|
|
|
throw new SignatureException('signature cannot be checked');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $origin;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $headers
|
|
|
|
* @param IRequest $request
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
private function generateEstimatedSignature(string $headers, IRequest $request): string {
|
|
|
|
$keys = explode(' ', $headers);
|
|
|
|
|
|
|
|
$target = '';
|
|
|
|
try {
|
|
|
|
$target = strtolower($request->getMethod()) . " " . $request->getRequestUri();
|
|
|
|
} catch (Exception $e) {
|
|
|
|
}
|
|
|
|
|
|
|
|
$estimated = "(request-target): " . $target;
|
|
|
|
|
|
|
|
foreach ($keys as $key) {
|
|
|
|
if ($key === '(request-target)') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$estimated .= "\n" . $key . ': ' . $request->getHeader($key);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $estimated;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param $signatureHeader
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function parseSignatureHeader($signatureHeader) {
|
|
|
|
$sign = [];
|
|
|
|
|
|
|
|
$entries = explode(',', $signatureHeader);
|
|
|
|
foreach ($entries as $entry) {
|
|
|
|
list($k, $v) = explode('=', $entry, 2);
|
|
|
|
preg_match('/"([^"]+)"/', $v, $varr);
|
|
|
|
$v = trim($varr[0], '"');
|
|
|
|
|
|
|
|
$sign[$k] = $v;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $sign;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param $keyId
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
* @throws InvalidResourceException
|
|
|
|
* @throws MalformedArrayException
|
|
|
|
* @throws Request410Exception
|
|
|
|
* @throws RequestException
|
|
|
|
* @throws SocialAppConfigException
|
|
|
|
* @throws UnknownItemException
|
|
|
|
* @throws RedundancyLimitException
|
2018-12-19 17:50:17 +00:00
|
|
|
* @throws InvalidOriginException
|
2018-12-17 09:12:27 +00:00
|
|
|
*/
|
|
|
|
private function retrieveKey($keyId): string {
|
|
|
|
$actor = $this->cacheActorService->getFromId($keyId);
|
|
|
|
|
|
|
|
return $actor->getPublicKey();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param $id
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
* @throws InvalidOriginException
|
|
|
|
*/
|
|
|
|
private function getKeyOrigin($id) {
|
|
|
|
$host = parse_url($id, PHP_URL_HOST);
|
|
|
|
if (is_string($host) && ($host !== '')) {
|
|
|
|
return $host;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new InvalidOriginException();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-12-20 16:51:09 +00:00
|
|
|
/**
|
|
|
|
* @param array $sign
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
private function getAlgorithmFromSignature(array $sign): string {
|
|
|
|
switch ($this->get('algorithm', $sign, '')) {
|
|
|
|
case 'rsa-sha512':
|
|
|
|
return 'sha512';
|
|
|
|
|
|
|
|
default:
|
|
|
|
return 'sha256';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-17 09:12:27 +00:00
|
|
|
}
|
2018-12-20 16:51:09 +00:00
|
|
|
|