%PDF- %PDF-
Direktori : /www/varak.net/nextcloud.varak.net/apps_old/apps/circles/lib/Tools/ActivityPub/ |
Current File : //www/varak.net/nextcloud.varak.net/apps_old/apps/circles/lib/Tools/ActivityPub/NCSignature.php |
<?php /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ declare(strict_types=1); /** * Circles - Bring cloud-users closer together. * * 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 2022 * @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\Circles\Tools\ActivityPub; use DateTime; use Exception; use OC; use OCA\Circles\Tools\Exceptions\InvalidOriginException; use OCA\Circles\Tools\Exceptions\ItemNotFoundException; use OCA\Circles\Tools\Exceptions\MalformedArrayException; use OCA\Circles\Tools\Exceptions\SignatoryException; use OCA\Circles\Tools\Exceptions\SignatureException; use OCA\Circles\Tools\Model\NCRequest; use OCA\Circles\Tools\Model\NCSignatory; use OCA\Circles\Tools\Model\NCSignedRequest; use OCA\Circles\Tools\Model\SimpleDataStore; use OCA\Circles\Tools\Traits\TNCSignatory; use OCP\IRequest; class NCSignature { public const DATE_HEADER = 'D, d M Y H:i:s T'; public const DATE_OBJECT = 'Y-m-d\TH:i:s\Z'; public const DATE_TTL = 300; use TNCSignatory; /** @var int */ private $ttl = self::DATE_TTL; private $dateHeader = self::DATE_HEADER; /** * @param string $body * * @return NCSignedRequest * @throws InvalidOriginException * @throws MalformedArrayException * @throws SignatoryException * @throws SignatureException */ public function incomingSignedRequest(string $body = ''): NCSignedRequest { if ($body === '') { $body = file_get_contents('php://input'); } $this->debug('[<<] incoming', ['body' => $body]); $signedRequest = new NCSignedRequest($body); $signedRequest->setIncomingRequest(OC::$server->get(IRequest::class)); $this->verifyIncomingRequestTime($signedRequest); $this->verifyIncomingRequestContent($signedRequest); $this->setIncomingSignatureHeader($signedRequest); $this->setIncomingClearSignature($signedRequest); $this->parseIncomingSignatureHeader($signedRequest); $this->verifyIncomingRequestSignature($signedRequest); return $signedRequest; } /** * @param NCRequest $request * @param NCSignatory $signatory * * @return NCSignedRequest * @throws SignatoryException */ public function signOutgoingRequest(NCRequest $request, NCSignatory $signatory): NCSignedRequest { $signedRequest = new NCSignedRequest($request->getDataBody()); $signedRequest->setOutgoingRequest($request) ->setSignatory($signatory); $this->setOutgoingSignatureHeader($signedRequest); $this->setOutgoingClearSignature($signedRequest); $this->setOutgoingSignedSignature($signedRequest); $this->signingOutgoingRequest($signedRequest); return $signedRequest; } /** * @param NCSignedRequest $signedRequest * * @throws SignatureException */ private function verifyIncomingRequestTime(NCSignedRequest $signedRequest): void { $request = $signedRequest->getIncomingRequest(); try { $dTime = new DateTime($request->getHeader('date')); $signedRequest->setTime($dTime->getTimestamp()); } catch (Exception $e) { $this->e($e, ['header' => $request->getHeader('date')]); throw new SignatureException('datetime exception'); } if ($signedRequest->getTime() < (time() - $this->ttl)) { throw new SignatureException('object is too old'); } } /** * @param NCSignedRequest $signedRequest * * @throws SignatureException */ private function verifyIncomingRequestContent(NCSignedRequest $signedRequest): void { $request = $signedRequest->getIncomingRequest(); if (strlen($signedRequest->getBody()) !== (int)$request->getHeader('content-length')) { throw new SignatureException('issue with content-length'); } if ($request->getHeader('digest') !== '' && $signedRequest->getDigest() !== $request->getHeader('digest')) { throw new SignatureException('issue with digest'); } } /** * @param NCSignedRequest $signedRequest */ private function setIncomingSignatureHeader(NCSignedRequest $signedRequest): void { $sign = []; $request = $signedRequest->getIncomingRequest(); foreach (explode(',', $request->getHeader('Signature')) as $entry) { if ($entry === '' || !strpos($entry, '=')) { continue; } [$k, $v] = explode('=', $entry, 2); preg_match('/"([^"]+)"/', $v, $varr); if ($varr[0] !== null) { $v = trim($varr[0], '"'); } $sign[$k] = $v; } $signedRequest->setSignatureHeader(new SimpleDataStore($sign)); } /** * @param NCSignedRequest $signedRequest * * @throws SignatureException */ private function setIncomingClearSignature(NCSignedRequest $signedRequest): void { $request = $signedRequest->getIncomingRequest(); $headers = explode(' ', $signedRequest->getSignatureHeader()->g('headers')); $enforceHeaders = array_merge( ['content-length', 'date', 'host'], $this->setupArray('enforceSignatureHeaders') ); if (!empty(array_diff($enforceHeaders, $headers))) { throw new SignatureException('missing elements in \'headers\''); } $target = strtolower($request->getMethod()) . " " . $request->getRequestUri(); $estimated = ['(request-target): ' . $target]; foreach ($headers as $key) { $value = $request->getHeader($key); if (strtolower($key) === 'host') { $value = $signedRequest->getIncomingRequest()->getServerHost(); } if ($value === '') { throw new SignatureException('empty elements in \'headers\''); } $estimated[] = $key . ': ' . $value; } $signedRequest->setClearSignature(implode("\n", $estimated)); } /** * @param NCSignedRequest $signedRequest * * @throws MalformedArrayException * @throws InvalidOriginException */ private function parseIncomingSignatureHeader(NCSignedRequest $signedRequest): void { $data = $signedRequest->getSignatureHeader(); $data->hasKeys(['keyId', 'headers', 'signature'], true); $signedRequest->setOrigin($this->getKeyOrigin($data->g('keyId'))); $signedRequest->setSignedSignature($data->g('signature')); } /** * @param NCSignedRequest $signedRequest * * @throws SignatoryException * @throws SignatureException */ private function verifyIncomingRequestSignature(NCSignedRequest $signedRequest) { $data = $signedRequest->getSignatureHeader(); try { $signedRequest->setSignatory($this->retrieveSignatory($data->g('keyId'), false)); $this->verifySignedRequest($signedRequest); } catch (SignatoryException $e) { $signedRequest->setSignatory($this->retrieveSignatory($data->g('keyId'), true)); $this->verifySignedRequest($signedRequest); } } /** * @param NCSignedRequest $signedRequest * * @throws SignatureException */ private function verifySignedRequest(NCSignedRequest $signedRequest) { $publicKey = $signedRequest->getSignatory()->getPublicKey(); if ($publicKey === '') { throw new SignatureException('empty public key'); } try { $this->verifyString( $signedRequest->getClearSignature(), base64_decode($signedRequest->getSignedSignature()), $publicKey, $this->getUsedEncryption($signedRequest) ); } catch (SignatureException $e) { $this->debug('signature issue', ['signed' => $signedRequest]); throw $e; } } /** * @param NCSignedRequest $signedRequest */ private function setOutgoingSignatureHeader(NCSignedRequest $signedRequest): void { $request = $signedRequest->getOutgoingRequest(); $data = new SimpleDataStore(); $data->s('(request-target)', NCRequest::method($request->getType()) . ' ' . $request->getPath()) ->sInt('content-length', strlen($signedRequest->getBody())) ->s('date', gmdate($this->dateHeader)) ->s('digest', $signedRequest->getDigest()) ->s('host', $request->getHost()); $signedRequest->setSignatureHeader($data); } /** * @param NCSignedRequest $signedRequest */ private function setOutgoingClearSignature(NCSignedRequest $signedRequest): void { $signing = []; $data = $signedRequest->getSignatureHeader(); foreach ($data->keys() as $element) { try { $value = $data->gItem($element); $signing[] = $element . ': ' . $value; if ($element !== '(request-target)') { $signedRequest->getOutgoingRequest()->addHeader($element, $value); } } catch (ItemNotFoundException $e) { } } $signedRequest->setClearSignature(implode("\n", $signing)); } /** * @param NCSignedRequest $signedRequest * * @throws SignatoryException */ private function setOutgoingSignedSignature(NCSignedRequest $signedRequest): void { $clear = $signedRequest->getClearSignature(); $signed = $this->signString($clear, $signedRequest->getSignatory()); $signedRequest->setSignedSignature($signed); } /** * @param NCSignedRequest $signedRequest * * @return void */ private function signingOutgoingRequest(NCSignedRequest $signedRequest): void { $headers = array_diff($signedRequest->getSignatureHeader()->keys(), ['(request-target)']); $signatory = $signedRequest->getSignatory(); $signatureElements = [ 'keyId="' . $signatory->getKeyId() . '"', 'algorithm="' . $this->getChosenEncryption($signatory) . '"', 'headers="' . implode(' ', $headers) . '"', 'signature="' . $signedRequest->getSignedSignature() . '"' ]; $signedRequest->getOutgoingRequest()->addHeader('Signature', implode(',', $signatureElements)); } /** * @param NCSignedRequest $signedRequest * * @return string */ private function getUsedEncryption(NCSignedRequest $signedRequest): string { switch ($signedRequest->getSignatureHeader()->g('algorithm')) { case 'rsa-sha512': return NCSignatory::SHA512; case 'rsa-sha256': default: return NCSignatory::SHA256; } } /** * @param NCSignatory $signatory * * @return string */ private function getChosenEncryption(NCSignatory $signatory): string { switch ($signatory->getAlgorithm()) { case NCSignatory::SHA512: return 'ras-sha512'; case NCSignatory::SHA256: default: return 'ras-sha256'; } } /** * @param NCSignatory $signatory * * @return int */ public function getOpenSSLAlgo(NCSignatory $signatory): int { switch ($signatory->getAlgorithm()) { case NCSignatory::SHA512: return OPENSSL_ALGO_SHA512; case NCSignatory::SHA256: default: return OPENSSL_ALGO_SHA256; } } }